commit 0a73bca056ea4cd22e9ba7e16d9d237e388ecdba Author: eleksir Date: Sat Mar 14 22:51:43 2026 +0300 Initial commit diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..b3f8a03 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,803 @@ +version: "2" +run: + go: "1.25" + modules-download-mode: readonly + tests: false + allow-parallel-runners: true +linters: + default: none + enable: + - asasalint + - asciicheck + - bodyclose + - containedctx + - contextcheck + - decorder + - durationcheck + - errcheck + - errchkjson + - errorlint + - exhaustive + - exptostd + - ginkgolinter + - gocheckcompilerdirectives + - gocritic + - godoclint + - godot + - godox + - goprintffuncname + - gosec + - gosmopolitan + - govet + - grouper + - importas + - ineffassign + - intrange + - ireturn + - makezero + - mirror + - misspell + - modernize + - nakedret + - nilerr + - nilnil + - nlreturn + - noctx + - nolintlint + - nonamedreturns + - perfsprint + - predeclared + - promlinter + - reassign + - revive + - rowserrcheck + - staticcheck + - unconvert + - unused + - usestdlibvars + - wastedassign + - wsl_v5 + settings: + decorder: + dec-order: + - type + - const + - var + - func + gocritic: + disable-all: false + enable-all: true + disabled-checks: + - singleCaseSwitch + - octalLiteral + - regexpSimplify + - paramTypeCombine + - whyNoLint + - hugeParam + godoclint: + default: all + ignore-unexported: false + ignore-exported: false + include-unexported: true + disable: + - max-len + godot: + scope: all + exclude: + # Exclude todo and fixme comments. + - "^fixme:" + - "^todo:" + period: true + capital: false + godox: + keywords: + - FIXME + gosec: # go-fucking-sic + excludes: + # Боже, как же вы затрахали своей крипто-ебучкой, наркоманы хреновы! + - G405 # Detect the usage of DES or RC4 + - G406 # Detect the usage of MD4 or RIPEMD160 + # Оно не используется для повышения безопасности или каких-то таких секъюрных вещей, оно используется для выборки + # произвольного элемента из массива, так что ИДИ НАХУЙ с этой проверкой и не еби мозги липовой безопасностью. + - G404 # Insecure random number source (rand) + - G501 # Import blocklist: crypto/md5 + - G502 # Import blocklist: crypto/des + - G503 # Import blocklist: crypto/rc4 + - G504 # Import blocklist: net/http/cgi + - G505 # Import blocklist: crypto/sha1 + - G506 # Import blocklist: golang.org/x/crypto/md4 + - G507 # Import blocklist: golang.org/x/crypto/ripemd160 + config: + global: + audit: true + # Ваша сраная паранойя при создании файлов и каталогов мешает нормальной работе сервисов. Группы придуманы для + # удобства, группе удобно давать право чтения. Поэтому идите нахуй со своими ограничениями на чтение для + # группы, "безопасные" клоуны. + G301: "0755" + G306: "0644" + govet: + disable-all: false + enable-all: true + disable: + - shadow + - fieldalignment + grouper: + # Require the use of a single global 'const' declaration only. + # Default: false + const-require-single-const: true + # Require the use of grouped global 'const' declarations. + # Default: false + const-require-grouping: true + # Require the use of a single 'import' declaration only. + # Default: false + import-require-single-import: true + # Require the use of grouped 'import' declarations. + # Default: false + import-require-grouping: true + # Require the use of a single global 'type' declaration only. + # Default: false + type-require-single-type: true + # Require the use of grouped global 'type' declarations. + # Default: false + type-require-grouping: true + # Require the use of a single global 'var' declaration only. + # Default: false + var-require-single-var: true + # Require the use of grouped global 'var' declarations. + # Default: false + var-require-grouping: true + misspell: + # Correct spellings using locale preferences for US or UK. + # Setting locale to US will correct the British spelling of 'colour' to 'color'. + # Default is to use a neutral variety of English. + locale: US + # Typos to ignore. + # Should be in lower case. + # Default: [] + ignore-rules: + - someword + # Extra word corrections. + # `typo` and `correction` should only contain letters. + # The words are case-insensitive. + # Default: [] + extra-words: + - typo: "iff" + correction: "if" + - typo: "cancelation" + correction: "cancellation" + # Mode of the analysis: + # - default: checks all the file content. + # - restricted: checks only comments. + # Default: "" + mode: restricted + nonamedreturns: + # Report named error if it is assigned inside defer. + # Default: false + report-error-in-defer: true + perfsprint: + integer-format: true + int-conversion: true + error-format: true + err-error: false + errorf: true + string-format: true + sprintf1: true + strconcat: true + bool-format: true + hex-format: true + concat-loop: true + loop-other-ops: false + revive: + enable-all-rules: false + staticcheck: + # https://staticcheck.dev/docs/configuration/options/#dot_import_whitelist + # Default: ["github.com/mmcloughlin/avo/build", "github.com/mmcloughlin/avo/operand", "github.com/mmcloughlin/avo/reg"] + dot-import-whitelist: + - fmt + # https://staticcheck.dev/docs/configuration/options/#initialisms + # Default: ["ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "GID", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS", "SIP", "RTP", "AMQP", "DB", "TS"] + initialisms: + [ + "ACL", + "API", + "ASCII", + "CPU", + "CSS", + "DNS", + "EOF", + "GUID", + "HTML", + "HTTP", + "HTTPS", + "ID", + "IP", + "JSON", + "QPS", + "RAM", + "RPC", + "SLA", + "SMTP", + "SQL", + "SSH", + "TCP", + "TLS", + "TTL", + "UDP", + "UI", + "GID", + "UID", + "UUID", + "URI", + "URL", + "UTF8", + "VM", + "XML", + "XMPP", + "XSRF", + "XSS", + "SIP", + "RTP", + "AMQP", + "DB", + "TS", + ] + # https://staticcheck.dev/docs/configuration/options/#http_status_code_whitelist + # Default: ["200", "400", "404", "500"] + http-status-code-whitelist: ["200", "400", "404", "500"] + # SAxxxx checks in https://staticcheck.dev/docs/configuration/options/#checks + # Example (to disable some checks): [ "all", "-SA1000", "-SA1001"] + # Run `GL_DEBUG=staticcheck golangci-lint run --enable=staticcheck` to see all available checks and enabled by config checks. + # Default: ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022"] + checks: + # Invalid regular expression. + # https://staticcheck.dev/docs/checks/#SA1000 + - SA1000 + # Invalid template. + # https://staticcheck.dev/docs/checks/#SA1001 + - SA1001 + # Invalid format in 'time.Parse'. + # https://staticcheck.dev/docs/checks/#SA1002 + - SA1002 + # Unsupported argument to functions in 'encoding/binary'. + # https://staticcheck.dev/docs/checks/#SA1003 + - SA1003 + # Suspiciously small untyped constant in 'time.Sleep'. + # https://staticcheck.dev/docs/checks/#SA1004 + - SA1004 + # Invalid first argument to 'exec.Command'. + # https://staticcheck.dev/docs/checks/#SA1005 + - SA1005 + # 'Printf' with dynamic first argument and no further arguments. + # https://staticcheck.dev/docs/checks/#SA1006 + - SA1006 + # Invalid URL in 'net/url.Parse'. + # https://staticcheck.dev/docs/checks/#SA1007 + - SA1007 + # Non-canonical key in 'http.Header' map. + # https://staticcheck.dev/docs/checks/#SA1008 + - SA1008 + # '(*regexp.Regexp).FindAll' called with 'n == 0', which will always return zero results. + # https://staticcheck.dev/docs/checks/#SA1010 + - SA1010 + # Various methods in the "strings" package expect valid UTF-8, but invalid input is provided. + # https://staticcheck.dev/docs/checks/#SA1011 + - SA1011 + # A nil 'context.Context' is being passed to a function, consider using 'context.TODO' instead. + # https://staticcheck.dev/docs/checks/#SA1012 + - SA1012 + # 'io.Seeker.Seek' is being called with the whence constant as the first argument, but it should be the second. + # https://staticcheck.dev/docs/checks/#SA1013 + - SA1013 + # Non-pointer value passed to 'Unmarshal' or 'Decode'. + # https://staticcheck.dev/docs/checks/#SA1014 + - SA1014 + # Using 'time.Tick' in a way that will leak. Consider using 'time.NewTicker', and only use 'time.Tick' in tests, commands and endless functions. + # https://staticcheck.dev/docs/checks/#SA1015 + - SA1015 + # Trapping a signal that cannot be trapped. + # https://staticcheck.dev/docs/checks/#SA1016 + - SA1016 + # Channels used with 'os/signal.Notify' should be buffered. + # https://staticcheck.dev/docs/checks/#SA1017 + - SA1017 + # 'strings.Replace' called with 'n == 0', which does nothing. + # https://staticcheck.dev/docs/checks/#SA1018 + - SA1018 + # Using a deprecated function, variable, constant or field. + # https://staticcheck.dev/docs/checks/#SA1019 + - SA1019 + # Using an invalid host:port pair with a 'net.Listen'-related function. + # https://staticcheck.dev/docs/checks/#SA1020 + - SA1020 + # Using 'bytes.Equal' to compare two 'net.IP'. + # https://staticcheck.dev/docs/checks/#SA1021 + - SA1021 + # Modifying the buffer in an 'io.Writer' implementation. + # https://staticcheck.dev/docs/checks/#SA1023 + - SA1023 + # A string cutset contains duplicate characters. + # https://staticcheck.dev/docs/checks/#SA1024 + - SA1024 + # It is not possible to use '(*time.Timer).Reset''s return value correctly. + # https://staticcheck.dev/docs/checks/#SA1025 + - SA1025 + # Cannot marshal channels or functions. + # https://staticcheck.dev/docs/checks/#SA1026 + - SA1026 + # Atomic access to 64-bit variable must be 64-bit aligned. + # https://staticcheck.dev/docs/checks/#SA1027 + - SA1027 + # 'sort.Slice' can only be used on slices. + # https://staticcheck.dev/docs/checks/#SA1028 + - SA1028 + # Inappropriate key in call to 'context.WithValue'. + # https://staticcheck.dev/docs/checks/#SA1029 + - SA1029 + # Invalid argument in call to a 'strconv' function. + # https://staticcheck.dev/docs/checks/#SA1030 + - SA1030 + # Overlapping byte slices passed to an encoder. + # https://staticcheck.dev/docs/checks/#SA1031 + - SA1031 + # Wrong order of arguments to 'errors.Is'. + # https://staticcheck.dev/docs/checks/#SA1032 + - SA1032 + # 'sync.WaitGroup.Add' called inside the goroutine, leading to a race condition. + # https://staticcheck.dev/docs/checks/#SA2000 + - SA2000 + # Empty critical section, did you mean to defer the unlock?. + # https://staticcheck.dev/docs/checks/#SA2001 + - SA2001 + # Called 'testing.T.FailNow' or 'SkipNow' in a goroutine, which isn't allowed. + # https://staticcheck.dev/docs/checks/#SA2002 + - SA2002 + # Deferred 'Lock' right after locking, likely meant to defer 'Unlock' instead. + # https://staticcheck.dev/docs/checks/#SA2003 + - SA2003 + # 'TestMain' doesn't call 'os.Exit', hiding test failures. + # https://staticcheck.dev/docs/checks/#SA3000 + - SA3000 + # Assigning to 'b.N' in benchmarks distorts the results. + # https://staticcheck.dev/docs/checks/#SA3001 + - SA3001 + # Binary operator has identical expressions on both sides. + # https://staticcheck.dev/docs/checks/#SA4000 + - SA4000 + # '&*x' gets simplified to 'x', it does not copy 'x'. + # https://staticcheck.dev/docs/checks/#SA4001 + - SA4001 + # Comparing unsigned values against negative values is pointless. + # https://staticcheck.dev/docs/checks/#SA4003 + - SA4003 + # The loop exits unconditionally after one iteration. + # https://staticcheck.dev/docs/checks/#SA4004 + - SA4004 + # Field assignment that will never be observed. Did you mean to use a pointer receiver?. + # https://staticcheck.dev/docs/checks/#SA4005 + - SA4005 + # A value assigned to a variable is never read before being overwritten. Forgotten error check or dead code?. + # https://staticcheck.dev/docs/checks/#SA4006 + - SA4006 + # The variable in the loop condition never changes, are you incrementing the wrong variable?. + # https://staticcheck.dev/docs/checks/#SA4008 + - SA4008 + # A function argument is overwritten before its first use. + # https://staticcheck.dev/docs/checks/#SA4009 + - SA4009 + # The result of 'append' will never be observed anywhere. + # https://staticcheck.dev/docs/checks/#SA4010 + - SA4010 + # Break statement with no effect. Did you mean to break out of an outer loop?. + # https://staticcheck.dev/docs/checks/#SA4011 + - SA4011 + # Comparing a value against NaN even though no value is equal to NaN. + # https://staticcheck.dev/docs/checks/#SA4012 + - SA4012 + # Negating a boolean twice ('!!b') is the same as writing 'b'. This is either redundant, or a typo. + # https://staticcheck.dev/docs/checks/#SA4013 + - SA4013 + # An if/else if chain has repeated conditions and no side-effects; if the condition didn't match the first time, it won't match the second time, either. + # https://staticcheck.dev/docs/checks/#SA4014 + - SA4014 + # Calling functions like 'math.Ceil' on floats converted from integers doesn't do anything useful. + # https://staticcheck.dev/docs/checks/#SA4015 + - SA4015 + # Certain bitwise operations, such as 'x ^ 0', do not do anything useful. + # https://staticcheck.dev/docs/checks/#SA4016 + - SA4016 + # Discarding the return values of a function without side effects, making the call pointless. + # https://staticcheck.dev/docs/checks/#SA4017 + - SA4017 + # Self-assignment of variables. + # https://staticcheck.dev/docs/checks/#SA4018 + - SA4018 + # Multiple, identical build constraints in the same file. + # https://staticcheck.dev/docs/checks/#SA4019 + - SA4019 + # Unreachable case clause in a type switch. + # https://staticcheck.dev/docs/checks/#SA4020 + - SA4020 + # "x = append(y)" is equivalent to "x = y". + # https://staticcheck.dev/docs/checks/#SA4021 + - SA4021 + # Comparing the address of a variable against nil. + # https://staticcheck.dev/docs/checks/#SA4022 + - SA4022 + # Impossible comparison of interface value with untyped nil. + # https://staticcheck.dev/docs/checks/#SA4023 + - SA4023 + # Checking for impossible return value from a builtin function. + # https://staticcheck.dev/docs/checks/#SA4024 + - SA4024 + # Integer division of literals that results in zero. + # https://staticcheck.dev/docs/checks/#SA4025 + - SA4025 + # Go constants cannot express negative zero. + # https://staticcheck.dev/docs/checks/#SA4026 + - SA4026 + # '(*net/url.URL).Query' returns a copy, modifying it doesn't change the URL. + # https://staticcheck.dev/docs/checks/#SA4027 + - SA4027 + # 'x % 1' is always zero. + # https://staticcheck.dev/docs/checks/#SA4028 + - SA4028 + # Ineffective attempt at sorting slice. + # https://staticcheck.dev/docs/checks/#SA4029 + - SA4029 + # Ineffective attempt at generating random number. + # https://staticcheck.dev/docs/checks/#SA4030 + - SA4030 + # Checking never-nil value against nil. + # https://staticcheck.dev/docs/checks/#SA4031 + - SA4031 + # Comparing 'runtime.GOOS' or 'runtime.GOARCH' against impossible value. + # https://staticcheck.dev/docs/checks/#SA4032 + - SA4032 + # Assignment to nil map. + # https://staticcheck.dev/docs/checks/#SA5000 + - SA5000 + # Deferring 'Close' before checking for a possible error. + # https://staticcheck.dev/docs/checks/#SA5001 + - SA5001 + # The empty for loop ("for {}") spins and can block the scheduler. + # https://staticcheck.dev/docs/checks/#SA5002 + - SA5002 + # Defers in infinite loops will never execute. + # https://staticcheck.dev/docs/checks/#SA5003 + - SA5003 + # "for { select { ..." with an empty default branch spins. + # https://staticcheck.dev/docs/checks/#SA5004 + - SA5004 + # The finalizer references the finalized object, preventing garbage collection. + # https://staticcheck.dev/docs/checks/#SA5005 + - SA5005 + # Infinite recursive call. + # https://staticcheck.dev/docs/checks/#SA5007 + - SA5007 + # Invalid struct tag. + # https://staticcheck.dev/docs/checks/#SA5008 + - SA5008 + # Invalid Printf call. + # https://staticcheck.dev/docs/checks/#SA5009 + - SA5009 + # Impossible type assertion. + # https://staticcheck.dev/docs/checks/#SA5010 + - SA5010 + # Possible nil pointer dereference. + # https://staticcheck.dev/docs/checks/#SA5011 + - SA5011 + # Passing odd-sized slice to function expecting even size. + # https://staticcheck.dev/docs/checks/#SA5012 + - SA5012 + # Using 'regexp.Match' or related in a loop, should use 'regexp.Compile'. + # https://staticcheck.dev/docs/checks/#SA6000 + - SA6000 + # Missing an optimization opportunity when indexing maps by byte slices. + # https://staticcheck.dev/docs/checks/#SA6001 + - SA6001 + # Storing non-pointer values in 'sync.Pool' allocates memory. + # https://staticcheck.dev/docs/checks/#SA6002 + - SA6002 + # Converting a string to a slice of runes before ranging over it. + # https://staticcheck.dev/docs/checks/#SA6003 + - SA6003 + # Inefficient string comparison with 'strings.ToLower' or 'strings.ToUpper'. + # https://staticcheck.dev/docs/checks/#SA6005 + - SA6005 + # Using io.WriteString to write '[]byte'. + # https://staticcheck.dev/docs/checks/#SA6006 + - SA6006 + # Defers in range loops may not run when you expect them to. + # https://staticcheck.dev/docs/checks/#SA9001 + - SA9001 + # Using a non-octal 'os.FileMode' that looks like it was meant to be in octal. + # https://staticcheck.dev/docs/checks/#SA9002 + - SA9002 + # Empty body in an if or else branch. + # https://staticcheck.dev/docs/checks/#SA9003 + - SA9003 + # Only the first constant has an explicit type. + # https://staticcheck.dev/docs/checks/#SA9004 + - SA9004 + # Trying to marshal a struct with no public fields nor custom marshaling. + # https://staticcheck.dev/docs/checks/#SA9005 + - SA9005 + # Dubious bit shifting of a fixed size integer value. + # https://staticcheck.dev/docs/checks/#SA9006 + - SA9006 + # Deleting a directory that shouldn't be deleted. + # https://staticcheck.dev/docs/checks/#SA9007 + - SA9007 + # 'else' branch of a type assertion is probably not reading the right value. + # https://staticcheck.dev/docs/checks/#SA9008 + - SA9008 + # Ineffectual Go compiler directive. + # https://staticcheck.dev/docs/checks/#SA9009 + - SA9009 + # Incorrect or missing package comment. + # https://staticcheck.dev/docs/checks/#ST1000 + - ST1000 + # Dot imports are discouraged. + # https://staticcheck.dev/docs/checks/#ST1001 + - ST1001 + # Poorly chosen identifier. + # https://staticcheck.dev/docs/checks/#ST1003 + - ST1003 + # Incorrectly formatted error string. + # https://staticcheck.dev/docs/checks/#ST1005 + # Commenting out this bullshit does not work, looks like golangci-lint fucking broken in this particular place :( + #- ST1005 + # Poorly chosen receiver name. + # https://staticcheck.dev/docs/checks/#ST1006 + - ST1006 + # A function's error value should be its last return value. + # https://staticcheck.dev/docs/checks/#ST1008 + - ST1008 + # Poorly chosen name for variable of type 'time.Duration'. + # https://staticcheck.dev/docs/checks/#ST1011 + - ST1011 + # Poorly chosen name for error variable. + # https://staticcheck.dev/docs/checks/#ST1012 + - ST1012 + # Should use constants for HTTP error codes, not magic numbers. + # https://staticcheck.dev/docs/checks/#ST1013 + - ST1013 + # A switch's default case should be the first or last case. + # https://staticcheck.dev/docs/checks/#ST1015 + - ST1015 + # Use consistent method receiver names. + # https://staticcheck.dev/docs/checks/#ST1016 + - ST1016 + # Don't use Yoda conditions. + # https://staticcheck.dev/docs/checks/#ST1017 + - ST1017 + # Avoid zero-width and control characters in string literals. + # https://staticcheck.dev/docs/checks/#ST1018 + - ST1018 + # Importing the same package multiple times. + # https://staticcheck.dev/docs/checks/#ST1019 + - ST1019 + # The documentation of an exported function should start with the function's name. + # https://staticcheck.dev/docs/checks/#ST1020 + - ST1020 + # The documentation of an exported type should start with type's name. + # https://staticcheck.dev/docs/checks/#ST1021 + - ST1021 + # The documentation of an exported variable or constant should start with variable's name. + # https://staticcheck.dev/docs/checks/#ST1022 + - ST1022 + # Redundant type in variable declaration. + # https://staticcheck.dev/docs/checks/#ST1023 + - ST1023 + # Use plain channel send or receive instead of single-case select. + # https://staticcheck.dev/docs/checks/#S1000 + - S1000 + # Replace for loop with call to copy. + # https://staticcheck.dev/docs/checks/#S1001 + - S1001 + # Omit comparison with boolean constant. + # https://staticcheck.dev/docs/checks/#S1002 + - S1002 + # Replace call to 'strings.Index' with 'strings.Contains'. + # https://staticcheck.dev/docs/checks/#S1003 + - S1003 + # Replace call to 'bytes.Compare' with 'bytes.Equal'. + # https://staticcheck.dev/docs/checks/#S1004 + - S1004 + # Drop unnecessary use of the blank identifier. + # https://staticcheck.dev/docs/checks/#S1005 + - S1005 + # Use "for { ... }" for infinite loops. + # https://staticcheck.dev/docs/checks/#S1006 + - S1006 + # Simplify regular expression by using raw string literal. + # https://staticcheck.dev/docs/checks/#S1007 + - S1007 + # Simplify returning boolean expression. + # https://staticcheck.dev/docs/checks/#S1008 + - S1008 + # Omit redundant nil check on slices, maps, and channels. + # https://staticcheck.dev/docs/checks/#S1009 + - S1009 + # Omit default slice index. + # https://staticcheck.dev/docs/checks/#S1010 + - S1010 + # Use a single 'append' to concatenate two slices. + # https://staticcheck.dev/docs/checks/#S1011 + - S1011 + # Replace 'time.Now().Sub(x)' with 'time.Since(x)'. + # https://staticcheck.dev/docs/checks/#S1012 + - S1012 + # Use a type conversion instead of manually copying struct fields. + # https://staticcheck.dev/docs/checks/#S1016 + - S1016 + # Replace manual trimming with 'strings.TrimPrefix'. + # https://staticcheck.dev/docs/checks/#S1017 + - S1017 + # Use "copy" for sliding elements. + # https://staticcheck.dev/docs/checks/#S1018 + - S1018 + # Simplify "make" call by omitting redundant arguments. + # https://staticcheck.dev/docs/checks/#S1019 + - S1019 + # Omit redundant nil check in type assertion. + # https://staticcheck.dev/docs/checks/#S1020 + - S1020 + # Merge variable declaration and assignment. + # https://staticcheck.dev/docs/checks/#S1021 + - S1021 + # Omit redundant control flow. + # https://staticcheck.dev/docs/checks/#S1023 + - S1023 + # Replace 'x.Sub(time.Now())' with 'time.Until(x)'. + # https://staticcheck.dev/docs/checks/#S1024 + - S1024 + # Don't use 'fmt.Sprintf("%s", x)' unnecessarily. + # https://staticcheck.dev/docs/checks/#S1025 + - S1025 + # Simplify error construction with 'fmt.Errorf'. + # https://staticcheck.dev/docs/checks/#S1028 + - S1028 + # Range over the string directly. + # https://staticcheck.dev/docs/checks/#S1029 + - S1029 + # Use 'bytes.Buffer.String' or 'bytes.Buffer.Bytes'. + # https://staticcheck.dev/docs/checks/#S1030 + - S1030 + # Omit redundant nil check around loop. + # https://staticcheck.dev/docs/checks/#S1031 + - S1031 + # Use 'sort.Ints(x)', 'sort.Float64s(x)', and 'sort.Strings(x)'. + # https://staticcheck.dev/docs/checks/#S1032 + - S1032 + # Unnecessary guard around call to "delete". + # https://staticcheck.dev/docs/checks/#S1033 + - S1033 + # Use result of type assertion to simplify cases. + # https://staticcheck.dev/docs/checks/#S1034 + - S1034 + # Redundant call to 'net/http.CanonicalHeaderKey' in method call on 'net/http.Header'. + # https://staticcheck.dev/docs/checks/#S1035 + - S1035 + # Unnecessary guard around map access. + # https://staticcheck.dev/docs/checks/#S1036 + - S1036 + # Elaborate way of sleeping. + # https://staticcheck.dev/docs/checks/#S1037 + - S1037 + # Unnecessarily complex way of printing formatted string. + # https://staticcheck.dev/docs/checks/#S1038 + - S1038 + # Unnecessary use of 'fmt.Sprint'. + # https://staticcheck.dev/docs/checks/#S1039 + - S1039 + # Type assertion to current type. + # https://staticcheck.dev/docs/checks/#S1040 + - S1040 + # Apply De Morgan's law. + # https://staticcheck.dev/docs/checks/#QF1001 + - QF1001 + # Convert untagged switch to tagged switch. + # https://staticcheck.dev/docs/checks/#QF1002 + - QF1002 + # Convert if/else-if chain to tagged switch. + # https://staticcheck.dev/docs/checks/#QF1003 + - QF1003 + # Use 'strings.ReplaceAll' instead of 'strings.Replace' with 'n == -1'. + # https://staticcheck.dev/docs/checks/#QF1004 + - QF1004 + # Expand call to 'math.Pow'. + # https://staticcheck.dev/docs/checks/#QF1005 + - QF1005 + # Lift 'if'+'break' into loop condition. + # https://staticcheck.dev/docs/checks/#QF1006 + - QF1006 + # Merge conditional assignment into variable declaration. + # https://staticcheck.dev/docs/checks/#QF1007 + - QF1007 + # Omit embedded fields from selector expression. + # https://staticcheck.dev/docs/checks/#QF1008 + - QF1008 + # Use 'time.Time.Equal' instead of '==' operator. + # https://staticcheck.dev/docs/checks/#QF1009 + - QF1009 + # Convert slice of bytes to string when printing it. + # https://staticcheck.dev/docs/checks/#QF1010 + - QF1010 + # Omit redundant type from variable declaration. + # https://staticcheck.dev/docs/checks/#QF1011 + - QF1011 + # Use 'fmt.Fprintf(x, ...)' instead of 'x.Write(fmt.Sprintf(...))'. + # https://staticcheck.dev/docs/checks/#QF1012 + - QF1012 + unconvert: + # Remove conversions that force intermediate rounding. + # Default: false + fast-math: true + # Be more conservative (experimental). + # Default: false + safe: true + usestdlibvars: + # Suggest the use of http.MethodXX. + # Default: true + http-method: false + # Suggest the use of http.StatusXX. + # Default: true + http-status-code: false + # Suggest the use of time.Month in time.Date. + # Default: false + time-date-month: true + # Suggest the use of time.Weekday.String(). + # Default: true + time-weekday: true + # Suggest the use of time.Month.String(). + # Default: false + time-month: true + # Suggest the use of time.Layout. + # Default: false + time-layout: true + # Suggest the use of crypto.Hash.String(). + # Default: false + crypto-hash: true + # Suggest the use of rpc.DefaultXXPath. + # Default: false + default-rpc-path: true + # Suggest the use of sql.LevelXX.String(). + # Default: false + sql-isolation-level: true + # Suggest the use of tls.SignatureScheme.String(). + # Default: false + tls-signature-scheme: true + # Suggest the use of constant.Kind.String(). + # Default: false + constant-kind: true + wsl_v5: + allow-first-in-block: true + allow-whole-block: true + branch-max-lines: 2 + case-max-lines: 2 + default: all + disable: + - if + - err + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9ea41cf --- /dev/null +++ b/Makefile @@ -0,0 +1,76 @@ +#!/usr/bin/env gmake -f + +GOOPTS=CGO_ENABLED=0 +BUILDOPTS=-ldflags="-s -w" -a -gcflags=all=-l -trimpath -buildvcs=false +MYNAME1=tg-holy-bot +BINARY1=${MYNAME1} +UNIX_BINARY1=${MYNAME1} +WINDOWS_BINARY1=${MYNAME1}.exe +RMCMD=rm -rf + +# On windows binary name can depend not only on platform selected, but target too, we need no .exe suffix for linux bin. +ifeq ($(OS),Windows_NT) +ifdef GOOS +ifeq ($(GOOS),windows) +BINARY1=${WINDOWS_BINARY1} +else # not ifeq ($(GOOS),windows) +BINARY1=${MYNAME1} +endif # ifeq ($(GOOS),windows) +else # not ifdef GOOS +BINARY1=${WINDOWS_BINARY1} +endif # ifdef GOOS +ifeq ($(SHELL), sh.exe) +RMCMD=DEL /Q /F +endif +endif + +# Set newline symbol explicitly to aboud undefined behaviour in windows. +define IFS + +endef + + +all: clean build + + +build: +ifeq ($(OS),Windows_NT) +# Looks like on windows gnu make explicitly set SHELL to sh.exe, if it was not set. +ifeq ($(SHELL), sh.exe) +# # Vanilla cmd.exe / powershell. + SET "CGO_ENABLED=0" + go build ${BUILDOPTS} -o ${BINARY1} ./cmd/${MYNAME1} +else ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL)))) +# # git-bash + CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY1} ./cmd/${MYNAME1} +else # not ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL)))) +# # Some other shell. +# # TODO: handle it. + $(info "-- Dunno how to handle this shell: ${SHELL}") +endif # ifeq (,$(findstring(Git/usr/bin/sh.exe, $(SHELL)))) +else # not ($(OS),Windows_NT) + CGO_ENABLED=0 go build ${BUILDOPTS} -o ${BINARY1} ./cmd/${MYNAME1} +endif # ifeq ($(OS),Windows_NT) + + +clean: +ifeq ($(OS),Windows_NT) +ifeq ($(SHELL),sh.exe) +# # Vanilla cmd.exe / powershell. + if exist ${WINDOWS_BINARY1} ${RMCMD} ${WINDOWS_BINARY1} + if exist ${UNIX_BINARY1} ${RMCMD} ${UNIX_BINARY1} +else # not ifeq ($(SHELL),sh.exe) + ${RMCMD} ./${WINDOWS_BINARY1} + ${RMCMD} ./${UNIX_BINARY1} +endif # ifeq ($(SHELL),sh.exe) +else # not ifeq ($(OS),Windows_NT) + ${RMCMD} ./${UNIX_BINARY1} +endif + + +upgrade: + go get -u ./... + go mod tidy + go mod vendor + +# vim: set ft=make noet ai ts=4 sw=4 sts=4: \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..bc16729 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +## tg-holy-bot + +Телеграммный бот для канала с праздниками. Раз в сутки забирает список праздников с к сайта celebratoday.com и +показывает его на канальчике в телеграме. + +## Как это собрать? + +Понадобится компилятор go версии 1.25.3 или более новый. + +Сам процесс сборки простой, достаточно запустить make или gmake, в зависимости от платформы: + +```bash +make +``` + +и через некоторое время мы получаем бинарник **tg-holy-bot**. Это и есть наше приложение. + +### Как это запустить? + +Приложение тестировалось на alpine linux x86_64 3.16, но должно работать на любом (более свежем) linux-е. + +Предполагается, что конфиг написан в формате [human json](https://hjson.github.io), который допускает коментарии и +некоторые другие вольности в форматировании json-а. Конфиг ищется последовательно по следующим путям по принципу "кто +первый того и тапки": + +``` +~/.tg-holy-bot.json +/etc/tg-holy-bot.json +data/config.json +``` + +В каталоге contrib находится скрипт для alpine linux, системы инициализации openrc. Этот скрипт достаточно положить в +**/etc/init.d/tg-holy-bot**. После чего скопировать бинарник в каталог **/var/lib/tg-holy-bot**, положить +конфиг в одну из дефолтных локаций (тестировалась **/var/lib/tg-holy-bot/data/config.json**) и после этого сервис +запускается командой: + +```bash +service tg-holy-bot start +``` + +В автозагрузку его можно прописать через команду: + +```bash +rc-update add tg-holy-bot default +``` + +### Nota Bene + +Go не поддерживает системный вызов fork() из-за чего демонизация программ на гошке средствами самой гошки - это в +некотором роде проблема. Решается она с помощью утилиты [daemonize](https://github.com/bmc/daemonize). + +Соответствено, если для запуска этой программы предполагается использовать возможности демонизации, то можно +воспользоваться этой программой. + +В alpine linux есть средства openrc для супервайзинга foreground-сервисов. \ No newline at end of file diff --git a/cmd/tg-holy-bot/main.go b/cmd/tg-holy-bot/main.go new file mode 100644 index 0000000..9917821 --- /dev/null +++ b/cmd/tg-holy-bot/main.go @@ -0,0 +1,71 @@ +// Package main implements entry point for this telegram bot service. +package main + +import ( + "os/signal" + "syscall" + "tg-holy-bot/internal/holy" + "tg-holy-bot/internal/log" + + "github.com/NicoNex/echotron/v3" + "github.com/robfig/cron/v3" +) + +func main() { + var ( + err error + ) + + if err = holy.ReadConfig(); err != nil { + log.Fatalf("%s", err) + } + + if err = log.Init(holy.Config.Loglevel, holy.Config.Log); err != nil { + log.Fatalf("%s", err) + } + + log.Info("Starting bot") + + // Самое время поставить трапы на сигналы. + signal.Notify(holy.SigChan, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT) + + // Запустим обработчик сигналов. + log.Info("Install signal handler") + + go holy.SigHandler() + + // Инициализируем бота. + log.Info("Initialize bot api client") + + holy.Bot = echotron.NewAPI(holy.Config.Token) + + log.Info("Create cron") + + c := cron.New() + + log.Infof("Add cron task %s to periodically run holiday client", holy.Config.When) + + if _, err := c.AddFunc(holy.Config.When, holy.CronTask); err != nil { + log.Fatalf("Unable to add task to cron: %s", err) + } + + log.Infof("Run cron") + + c.Start() + + // Залочимся до момента завершения программы. + <-holy.Run + + log.Infof("Stop cron") + + // Jobs usually runs in a fraction of second, don't bother waiting for their completion. + _ = c.Stop() + + log.Infof("Shutdown bot") + log.Close() +} + +/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/contrib/tg-holy-bot.openrc b/contrib/tg-holy-bot.openrc new file mode 100644 index 0000000..91e055f --- /dev/null +++ b/contrib/tg-holy-bot.openrc @@ -0,0 +1,21 @@ +#!/sbin/openrc-run + +name="$SVCNAME" +command="/var/lib/$SVCNAME/$SVCNAME" +supervisor="supervise-daemon" +pidfile="/run/$SVCNAME/$SVCNAME.pid" +command_user=nobody:nogroup + +depend() { + need net + use dns +} + +start_pre() { + mkdir -p /var/log/$SVCNAME + chown -R nobody:nogroup /var/log/$SVCNAME + mkdir -p /run/$SVCNAME + chown -R nobody:nogroup /run/$SVCNAME +} + +# vim: set ft=sh noet ai ts=4 sw=4 sts=4: \ No newline at end of file diff --git a/data/config.json b/data/config.json new file mode 100644 index 0000000..4bf539d --- /dev/null +++ b/data/config.json @@ -0,0 +1,51 @@ +{ + /* Put here bot api token that is generated by @bot_father. */ + "token": "something:something", + + /* Put here valid channel id. */ + "channel_id": -12345678901234, + + /* See https://pkg.go.dev/github.com/robfig/cron/v3#section-documentation for format of crontabs. */ + "when": "CRON_TZ=Europe/Moscow 01 03 * * *", + + /* There are error, warn, info and debug log levels with different vebosity. Default is info. */ + "loglevel": "info", + + /* Log file location. If omited or empty stderr is used. */ + "log": "/var/log/tg-holy-bot/tg-holy-bot.log", + + /* List of user agents. */ + "user_agents": [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.81 Safari/537.36 OPR/83.0.4254.27", + "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.81 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Vivaldi/3.5", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36 Edg/88.0.705.81", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.2 Safari/605.1.15", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36 Edg/95.0.1020.44", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36 Edg/96.0.1054.62", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36 OPR/82.0.4227.50", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36 Edg/97.0.1072.55", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.81 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.81 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.81 Safari/537.36 Edg/97.0.1072.69", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.81 Safari/537.36 Vivaldi/4.3", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.81 YaBrowser/22.1.0 Yowser/2.5 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:95.0) Gecko/20100101 Firefox/95.0", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.1 Safari/605.1.15", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:96.0) Gecko/20100101 Firefox/96.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:97.0) Gecko/20100101 Firefox/97.0", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Vivaldi/3.5", + "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko", + "Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.81 Safari/537.36 OPR/83.0.4254.27", + "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.81 Safari/537.36 OPR/83.0.4254.27", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.81 Safari/537.36 Vivaldi/4.3", + "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:91.0) Gecko/20100101 Firefox/91.0", + "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:97.0) Gecko/20100101 Firefox/97.0", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36" + ] +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6931287 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module tg-holy-bot + +go 1.25.0 + +require ( + github.com/NicoNex/echotron/v3 v3.45.0 + github.com/carlmjohnson/requests v0.25.1 + github.com/hjson/hjson-go v3.3.0+incompatible + github.com/robfig/cron/v3 v3.0.1 + golang.org/x/net v0.51.0 +) + +require golang.org/x/time v0.5.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..80b774b --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/NicoNex/echotron/v3 v3.45.0 h1:hORujGwc6X2yaiHZIt8/rYZR48L524YtdiHxpqBwlko= +github.com/NicoNex/echotron/v3 v3.45.0/go.mod h1:7LvjveJmezuUOeaoA3nzQduNlSPQYfq219Z+baKY04Q= +github.com/carlmjohnson/requests v0.25.1 h1:17zNRLecxtAjhtdEIV+F+wrYfe+AGZUjWJtpndcOUYA= +github.com/carlmjohnson/requests v0.25.1/go.mod h1:z3UEf8IE4sZxZ78spW6/tLdqBkfCu1Fn4RaYMnZ8SRM= +github.com/hjson/hjson-go v3.3.0+incompatible h1:Rqr+Ya+0aCJMjaE4s8E9YKvuJLuLVpEvz4ONum52vnI= +github.com/hjson/hjson-go v3.3.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= diff --git a/internal/holy/celebratoday.go b/internal/holy/celebratoday.go new file mode 100644 index 0000000..e3a5d30 --- /dev/null +++ b/internal/holy/celebratoday.go @@ -0,0 +1,110 @@ +// Package holy implements main business logic of bot service. +package holy + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "slices" + "strings" + "time" + + "github.com/carlmjohnson/requests" + "golang.org/x/net/html" +) + +// APIClient клиент сервиса https://celebratoday.com/. +func APIClient() (string, error) { + var ( + ctx = context.Background() + url = "https://celebratoday.com/ru/today" + respBody string + ) + + // Формальная попытка притвориться браузером. + _, _, day := time.Now().Date() + userAgent := Config.UserAgents[day] + + if err := requests.URL(url).UserAgent(userAgent). + Header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"). + Header("Accept-Language", "en-US,en;q=0.5"). + Header("User-Agent", "ru-RU"). + Header("Accept-Charset", "utf-8").ToString(&respBody).Fetch(ctx); err != nil { + return "", fmt.Errorf("Unable to GET %s: %w", url, err) //nolint: staticcheck // Looks like golangci-lint ignoring settings about fucking ST1005 check. We want readable errors despite what wants author of linter or assholes from go dev team. + } + + holidays, err := parser(respBody) + + if err != nil { + return "", fmt.Errorf("Unable to parse response from celebratoday.com: %w", err) //nolint: staticcheck // Looks like golangci-lint ignoring settings about fucking ST1005 check. We want readable errors despite what wants author of linter or assholes from go dev team. + } + + if len(holidays) == 0 { + return "", errors.New("Unable to parse response from celebratoday.com: no holidays found") //nolint: staticcheck // Looks like golangci-lint ignoring settings about fucking ST1005 check. We want readable errors despite what wants author of linter or assholes from go dev team. + } + + slices.Sort(holidays) + + holidays = slices.Compact(holidays) + + return "* " + strings.Join(holidays, "\n* "), nil +} + +// parser разбирает буфер с html-ем, выискивая праздники. +func parser(data string) ([]string, error) { + var ( + holidays = make([]string, 0, 1) + err error + s = make([]Holy, 0, 1) + ) + + doc, err := html.Parse(strings.NewReader(data)) + + if err != nil { + return holidays, err + } + + traverse(doc, &s) + + // А теперь пробежимся по полученной структуре и вытянем из неё все строки с праздниками. + for _, ass := range s { + for _, entity := range ass.MainEntity { + for _, sugestedAnswer := range entity.SuggestedAnswer { + holidays = append(holidays, sugestedAnswer.Text) + } + } + } + + return holidays, err +} + +// traverse рекурсивная функция, ищущая " that closes the next token. If + // non-empty, the subsequent call to Next will return a raw or RCDATA text + // token: one that treats "

" as text instead of an element. + // rawTag's contents are lower-cased. + rawTag string + // textIsRaw is whether the current text token's data is not escaped. + textIsRaw bool + // convertNUL is whether NUL bytes in the current token's data should + // be converted into \ufffd replacement characters. + convertNUL bool + // allowCDATA is whether CDATA sections are allowed in the current context. + allowCDATA bool +} + +// AllowCDATA sets whether or not the tokenizer recognizes as +// the text "foo". The default value is false, which means to recognize it as +// a bogus comment "" instead. +// +// Strictly speaking, an HTML5 compliant tokenizer should allow CDATA if and +// only if tokenizing foreign content, such as MathML and SVG. However, +// tracking foreign-contentness is difficult to do purely in the tokenizer, +// as opposed to the parser, due to HTML integration points: an element +// can contain a that is foreign-to-SVG but not foreign-to- +// HTML. For strict compliance with the HTML5 tokenization algorithm, it is the +// responsibility of the user of a tokenizer to call AllowCDATA as appropriate. +// In practice, if using the tokenizer without caring whether MathML or SVG +// CDATA is text or comments, such as tokenizing HTML to find all the anchor +// text, it is acceptable to ignore this responsibility. +func (z *Tokenizer) AllowCDATA(allowCDATA bool) { + z.allowCDATA = allowCDATA +} + +// NextIsNotRawText instructs the tokenizer that the next token should not be +// considered as 'raw text'. Some elements, such as script and title elements, +// normally require the next token after the opening tag to be 'raw text' that +// has no child elements. For example, tokenizing "a<b>c</b>d" +// yields a start tag token for "", a text token for "a<b>c</b>d", and +// an end tag token for "". There are no distinct start tag or end tag +// tokens for the "" and "". +// +// This tokenizer implementation will generally look for raw text at the right +// times. Strictly speaking, an HTML5 compliant tokenizer should not look for +// raw text if in foreign content: generally needs raw text, but a +// <title> inside an <svg> does not. Another example is that a <textarea> +// generally needs raw text, but a <textarea> is not allowed as an immediate +// child of a <select>; in normal parsing, a <textarea> implies </select>, but +// one cannot close the implicit element when parsing a <select>'s InnerHTML. +// Similarly to AllowCDATA, tracking the correct moment to override raw-text- +// ness is difficult to do purely in the tokenizer, as opposed to the parser. +// For strict compliance with the HTML5 tokenization algorithm, it is the +// responsibility of the user of a tokenizer to call NextIsNotRawText as +// appropriate. In practice, like AllowCDATA, it is acceptable to ignore this +// responsibility for basic usage. +// +// Note that this 'raw text' concept is different from the one offered by the +// Tokenizer.Raw method. +func (z *Tokenizer) NextIsNotRawText() { + z.rawTag = "" +} + +// Err returns the error associated with the most recent ErrorToken token. +// This is typically io.EOF, meaning the end of tokenization. +func (z *Tokenizer) Err() error { + if z.tt != ErrorToken { + return nil + } + return z.err +} + +// readByte returns the next byte from the input stream, doing a buffered read +// from z.r into z.buf if necessary. z.buf[z.raw.start:z.raw.end] remains a contiguous byte +// slice that holds all the bytes read so far for the current token. +// It sets z.err if the underlying reader returns an error. +// Pre-condition: z.err == nil. +func (z *Tokenizer) readByte() byte { + if z.raw.end >= len(z.buf) { + // Our buffer is exhausted and we have to read from z.r. Check if the + // previous read resulted in an error. + if z.readErr != nil { + z.err = z.readErr + return 0 + } + // We copy z.buf[z.raw.start:z.raw.end] to the beginning of z.buf. If the length + // z.raw.end - z.raw.start is more than half the capacity of z.buf, then we + // allocate a new buffer before the copy. + c := cap(z.buf) + d := z.raw.end - z.raw.start + var buf1 []byte + if 2*d > c { + buf1 = make([]byte, d, 2*c) + } else { + buf1 = z.buf[:d] + } + copy(buf1, z.buf[z.raw.start:z.raw.end]) + if x := z.raw.start; x != 0 { + // Adjust the data/attr spans to refer to the same contents after the copy. + z.data.start -= x + z.data.end -= x + z.pendingAttr[0].start -= x + z.pendingAttr[0].end -= x + z.pendingAttr[1].start -= x + z.pendingAttr[1].end -= x + for i := range z.attr { + z.attr[i][0].start -= x + z.attr[i][0].end -= x + z.attr[i][1].start -= x + z.attr[i][1].end -= x + } + } + z.raw.start, z.raw.end, z.buf = 0, d, buf1[:d] + // Now that we have copied the live bytes to the start of the buffer, + // we read from z.r into the remainder. + var n int + n, z.readErr = readAtLeastOneByte(z.r, buf1[d:cap(buf1)]) + if n == 0 { + z.err = z.readErr + return 0 + } + z.buf = buf1[:d+n] + } + x := z.buf[z.raw.end] + z.raw.end++ + if z.maxBuf > 0 && z.raw.end-z.raw.start >= z.maxBuf { + z.err = ErrBufferExceeded + return 0 + } + return x +} + +// Buffered returns a slice containing data buffered but not yet tokenized. +func (z *Tokenizer) Buffered() []byte { + return z.buf[z.raw.end:] +} + +// readAtLeastOneByte wraps an io.Reader so that reading cannot return (0, nil). +// It returns io.ErrNoProgress if the underlying r.Read method returns (0, nil) +// too many times in succession. +func readAtLeastOneByte(r io.Reader, b []byte) (int, error) { + for i := 0; i < 100; i++ { + if n, err := r.Read(b); n != 0 || err != nil { + return n, err + } + } + return 0, io.ErrNoProgress +} + +// skipWhiteSpace skips past any white space. +func (z *Tokenizer) skipWhiteSpace() { + if z.err != nil { + return + } + for { + c := z.readByte() + if z.err != nil { + return + } + switch c { + case ' ', '\n', '\r', '\t', '\f': + // No-op. + default: + z.raw.end-- + return + } + } +} + +// readRawOrRCDATA reads until the next "</foo>", where "foo" is z.rawTag and +// is typically something like "script" or "textarea". +func (z *Tokenizer) readRawOrRCDATA() { + if z.rawTag == "script" { + z.readScript() + z.textIsRaw = true + z.rawTag = "" + return + } +loop: + for { + c := z.readByte() + if z.err != nil { + break loop + } + if c != '<' { + continue loop + } + c = z.readByte() + if z.err != nil { + break loop + } + if c != '/' { + z.raw.end-- + continue loop + } + if z.readRawEndTag() || z.err != nil { + break loop + } + } + z.data.end = z.raw.end + // A textarea's or title's RCDATA can contain escaped entities. + z.textIsRaw = z.rawTag != "textarea" && z.rawTag != "title" + z.rawTag = "" +} + +// readRawEndTag attempts to read a tag like "</foo>", where "foo" is z.rawTag. +// If it succeeds, it backs up the input position to reconsume the tag and +// returns true. Otherwise it returns false. The opening "</" has already been +// consumed. +func (z *Tokenizer) readRawEndTag() bool { + for i := 0; i < len(z.rawTag); i++ { + c := z.readByte() + if z.err != nil { + return false + } + if c != z.rawTag[i] && c != z.rawTag[i]-('a'-'A') { + z.raw.end-- + return false + } + } + c := z.readByte() + if z.err != nil { + return false + } + switch c { + case ' ', '\n', '\r', '\t', '\f', '/', '>': + // The 3 is 2 for the leading "</" plus 1 for the trailing character c. + z.raw.end -= 3 + len(z.rawTag) + return true + } + z.raw.end-- + return false +} + +// readScript reads until the next </script> tag, following the byzantine +// rules for escaping/hiding the closing tag. +func (z *Tokenizer) readScript() { + defer func() { + z.data.end = z.raw.end + }() + var c byte + +scriptData: + c = z.readByte() + if z.err != nil { + return + } + if c == '<' { + goto scriptDataLessThanSign + } + goto scriptData + +scriptDataLessThanSign: + c = z.readByte() + if z.err != nil { + return + } + switch c { + case '/': + goto scriptDataEndTagOpen + case '!': + goto scriptDataEscapeStart + } + z.raw.end-- + goto scriptData + +scriptDataEndTagOpen: + if z.readRawEndTag() || z.err != nil { + return + } + goto scriptData + +scriptDataEscapeStart: + c = z.readByte() + if z.err != nil { + return + } + if c == '-' { + goto scriptDataEscapeStartDash + } + z.raw.end-- + goto scriptData + +scriptDataEscapeStartDash: + c = z.readByte() + if z.err != nil { + return + } + if c == '-' { + goto scriptDataEscapedDashDash + } + z.raw.end-- + goto scriptData + +scriptDataEscaped: + c = z.readByte() + if z.err != nil { + return + } + switch c { + case '-': + goto scriptDataEscapedDash + case '<': + goto scriptDataEscapedLessThanSign + } + goto scriptDataEscaped + +scriptDataEscapedDash: + c = z.readByte() + if z.err != nil { + return + } + switch c { + case '-': + goto scriptDataEscapedDashDash + case '<': + goto scriptDataEscapedLessThanSign + } + goto scriptDataEscaped + +scriptDataEscapedDashDash: + c = z.readByte() + if z.err != nil { + return + } + switch c { + case '-': + goto scriptDataEscapedDashDash + case '<': + goto scriptDataEscapedLessThanSign + case '>': + goto scriptData + } + goto scriptDataEscaped + +scriptDataEscapedLessThanSign: + c = z.readByte() + if z.err != nil { + return + } + if c == '/' { + goto scriptDataEscapedEndTagOpen + } + if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' { + goto scriptDataDoubleEscapeStart + } + z.raw.end-- + goto scriptData + +scriptDataEscapedEndTagOpen: + if z.readRawEndTag() || z.err != nil { + return + } + goto scriptDataEscaped + +scriptDataDoubleEscapeStart: + z.raw.end-- + for i := 0; i < len("script"); i++ { + c = z.readByte() + if z.err != nil { + return + } + if c != "script"[i] && c != "SCRIPT"[i] { + z.raw.end-- + goto scriptDataEscaped + } + } + c = z.readByte() + if z.err != nil { + return + } + switch c { + case ' ', '\n', '\r', '\t', '\f', '/', '>': + goto scriptDataDoubleEscaped + } + z.raw.end-- + goto scriptDataEscaped + +scriptDataDoubleEscaped: + c = z.readByte() + if z.err != nil { + return + } + switch c { + case '-': + goto scriptDataDoubleEscapedDash + case '<': + goto scriptDataDoubleEscapedLessThanSign + } + goto scriptDataDoubleEscaped + +scriptDataDoubleEscapedDash: + c = z.readByte() + if z.err != nil { + return + } + switch c { + case '-': + goto scriptDataDoubleEscapedDashDash + case '<': + goto scriptDataDoubleEscapedLessThanSign + } + goto scriptDataDoubleEscaped + +scriptDataDoubleEscapedDashDash: + c = z.readByte() + if z.err != nil { + return + } + switch c { + case '-': + goto scriptDataDoubleEscapedDashDash + case '<': + goto scriptDataDoubleEscapedLessThanSign + case '>': + goto scriptData + } + goto scriptDataDoubleEscaped + +scriptDataDoubleEscapedLessThanSign: + c = z.readByte() + if z.err != nil { + return + } + if c == '/' { + goto scriptDataDoubleEscapeEnd + } + z.raw.end-- + goto scriptDataDoubleEscaped + +scriptDataDoubleEscapeEnd: + if z.readRawEndTag() { + z.raw.end += len("</script>") + goto scriptDataEscaped + } + if z.err != nil { + return + } + goto scriptDataDoubleEscaped +} + +// readComment reads the next comment token starting with "<!--". The opening +// "<!--" has already been consumed. +func (z *Tokenizer) readComment() { + // When modifying this function, consider manually increasing the + // maxSuffixLen constant in func TestComments, from 6 to e.g. 9 or more. + // That increase should only be temporary, not committed, as it + // exponentially affects the test running time. + + z.data.start = z.raw.end + defer func() { + if z.data.end < z.data.start { + // It's a comment with no data, like <!-->. + z.data.end = z.data.start + } + }() + + var dashCount int + beginning := true + for { + c := z.readByte() + if z.err != nil { + z.data.end = z.calculateAbruptCommentDataEnd() + return + } + switch c { + case '-': + dashCount++ + continue + case '>': + if dashCount >= 2 || beginning { + z.data.end = z.raw.end - len("-->") + return + } + case '!': + if dashCount >= 2 { + c = z.readByte() + if z.err != nil { + z.data.end = z.calculateAbruptCommentDataEnd() + return + } else if c == '>' { + z.data.end = z.raw.end - len("--!>") + return + } else if c == '-' { + dashCount = 1 + beginning = false + continue + } + } + } + dashCount = 0 + beginning = false + } +} + +func (z *Tokenizer) calculateAbruptCommentDataEnd() int { + raw := z.Raw() + const prefixLen = len("<!--") + if len(raw) >= prefixLen { + raw = raw[prefixLen:] + if hasSuffix(raw, "--!") { + return z.raw.end - 3 + } else if hasSuffix(raw, "--") { + return z.raw.end - 2 + } else if hasSuffix(raw, "-") { + return z.raw.end - 1 + } + } + return z.raw.end +} + +func hasSuffix(b []byte, suffix string) bool { + if len(b) < len(suffix) { + return false + } + b = b[len(b)-len(suffix):] + for i := range b { + if b[i] != suffix[i] { + return false + } + } + return true +} + +// readUntilCloseAngle reads until the next ">". +func (z *Tokenizer) readUntilCloseAngle() { + z.data.start = z.raw.end + for { + c := z.readByte() + if z.err != nil { + z.data.end = z.raw.end + return + } + if c == '>' { + z.data.end = z.raw.end - len(">") + return + } + } +} + +// readMarkupDeclaration reads the next token starting with "<!". It might be +// a "<!--comment-->", a "<!DOCTYPE foo>", a "<![CDATA[section]]>" or +// "<!a bogus comment". The opening "<!" has already been consumed. +func (z *Tokenizer) readMarkupDeclaration() TokenType { + z.data.start = z.raw.end + var c [2]byte + for i := 0; i < 2; i++ { + c[i] = z.readByte() + if z.err != nil { + z.data.end = z.raw.end + return CommentToken + } + } + if c[0] == '-' && c[1] == '-' { + z.readComment() + return CommentToken + } + z.raw.end -= 2 + if z.readDoctype() { + return DoctypeToken + } + if z.allowCDATA && z.readCDATA() { + z.convertNUL = true + return TextToken + } + // It's a bogus comment. + z.readUntilCloseAngle() + return CommentToken +} + +// readDoctype attempts to read a doctype declaration and returns true if +// successful. The opening "<!" has already been consumed. +func (z *Tokenizer) readDoctype() bool { + const s = "DOCTYPE" + for i := 0; i < len(s); i++ { + c := z.readByte() + if z.err != nil { + z.data.end = z.raw.end + return false + } + if c != s[i] && c != s[i]+('a'-'A') { + // Back up to read the fragment of "DOCTYPE" again. + z.raw.end = z.data.start + return false + } + } + if z.skipWhiteSpace(); z.err != nil { + z.data.start = z.raw.end + z.data.end = z.raw.end + return true + } + z.readUntilCloseAngle() + return true +} + +// readCDATA attempts to read a CDATA section and returns true if +// successful. The opening "<!" has already been consumed. +func (z *Tokenizer) readCDATA() bool { + const s = "[CDATA[" + for i := 0; i < len(s); i++ { + c := z.readByte() + if z.err != nil { + z.data.end = z.raw.end + return false + } + if c != s[i] { + // Back up to read the fragment of "[CDATA[" again. + z.raw.end = z.data.start + return false + } + } + z.data.start = z.raw.end + brackets := 0 + for { + c := z.readByte() + if z.err != nil { + z.data.end = z.raw.end + return true + } + switch c { + case ']': + brackets++ + case '>': + if brackets >= 2 { + z.data.end = z.raw.end - len("]]>") + return true + } + brackets = 0 + default: + brackets = 0 + } + } +} + +// startTagIn returns whether the start tag in z.buf[z.data.start:z.data.end] +// case-insensitively matches any element of ss. +func (z *Tokenizer) startTagIn(ss ...string) bool { +loop: + for _, s := range ss { + if z.data.end-z.data.start != len(s) { + continue loop + } + for i := 0; i < len(s); i++ { + c := z.buf[z.data.start+i] + if 'A' <= c && c <= 'Z' { + c += 'a' - 'A' + } + if c != s[i] { + continue loop + } + } + return true + } + return false +} + +// readStartTag reads the next start tag token. The opening "<a" has already +// been consumed, where 'a' means anything in [A-Za-z]. +func (z *Tokenizer) readStartTag() TokenType { + z.readTag(true) + if z.err != nil { + return ErrorToken + } + // Several tags flag the tokenizer's next token as raw. + c, raw := z.buf[z.data.start], false + if 'A' <= c && c <= 'Z' { + c += 'a' - 'A' + } + switch c { + case 'i': + raw = z.startTagIn("iframe") + case 'n': + raw = z.startTagIn("noembed", "noframes", "noscript") + case 'p': + raw = z.startTagIn("plaintext") + case 's': + raw = z.startTagIn("script", "style") + case 't': + raw = z.startTagIn("textarea", "title") + case 'x': + raw = z.startTagIn("xmp") + } + if raw { + z.rawTag = strings.ToLower(string(z.buf[z.data.start:z.data.end])) + } + // Look for a self-closing token (e.g. <br/>). + // + // Originally, we did this by just checking that the last character of the + // tag (ignoring the closing bracket) was a solidus (/) character, but this + // is not always accurate. + // + // We need to be careful that we don't misinterpret a non-self-closing tag + // as self-closing, as can happen if the tag contains unquoted attribute + // values (i.e. <p a=/>). + // + // To avoid this, we check that the last non-bracket character of the tag + // (z.raw.end-2) isn't the same character as the last non-quote character of + // the last attribute of the tag (z.pendingAttr[1].end-1), if the tag has + // attributes. + nAttrs := len(z.attr) + if z.err == nil && z.buf[z.raw.end-2] == '/' && (nAttrs == 0 || z.raw.end-2 != z.attr[nAttrs-1][1].end-1) { + return SelfClosingTagToken + } + return StartTagToken +} + +// readTag reads the next tag token and its attributes. If saveAttr, those +// attributes are saved in z.attr, otherwise z.attr is set to an empty slice. +// The opening "<a" or "</a" has already been consumed, where 'a' means anything +// in [A-Za-z]. +func (z *Tokenizer) readTag(saveAttr bool) { + z.attr = z.attr[:0] + z.nAttrReturned = 0 + // Read the tag name and attribute key/value pairs. + z.readTagName() + if z.skipWhiteSpace(); z.err != nil { + return + } + for { + c := z.readByte() + if z.err != nil || c == '>' { + break + } + z.raw.end-- + z.readTagAttrKey() + z.readTagAttrVal() + // Save pendingAttr if saveAttr and that attribute has a non-empty key. + if saveAttr && z.pendingAttr[0].start != z.pendingAttr[0].end { + z.attr = append(z.attr, z.pendingAttr) + } + if z.skipWhiteSpace(); z.err != nil { + break + } + } +} + +// readTagName sets z.data to the "div" in "<div k=v>". The reader (z.raw.end) +// is positioned such that the first byte of the tag name (the "d" in "<div") +// has already been consumed. +func (z *Tokenizer) readTagName() { + z.data.start = z.raw.end - 1 + for { + c := z.readByte() + if z.err != nil { + z.data.end = z.raw.end + return + } + switch c { + case ' ', '\n', '\r', '\t', '\f': + z.data.end = z.raw.end - 1 + return + case '/', '>': + z.raw.end-- + z.data.end = z.raw.end + return + } + } +} + +// readTagAttrKey sets z.pendingAttr[0] to the "k" in "<div k=v>". +// Precondition: z.err == nil. +func (z *Tokenizer) readTagAttrKey() { + z.pendingAttr[0].start = z.raw.end + for { + c := z.readByte() + if z.err != nil { + z.pendingAttr[0].end = z.raw.end + return + } + switch c { + case '=': + if z.pendingAttr[0].start+1 == z.raw.end { + // WHATWG 13.2.5.32, if we see an equals sign before the attribute name + // begins, we treat it as a character in the attribute name and continue. + continue + } + fallthrough + case ' ', '\n', '\r', '\t', '\f', '/', '>': + // WHATWG 13.2.5.33 Attribute name state + // We need to reconsume the char in the after attribute name state to support the / character + z.raw.end-- + z.pendingAttr[0].end = z.raw.end + return + } + } +} + +// readTagAttrVal sets z.pendingAttr[1] to the "v" in "<div k=v>". +func (z *Tokenizer) readTagAttrVal() { + z.pendingAttr[1].start = z.raw.end + z.pendingAttr[1].end = z.raw.end + if z.skipWhiteSpace(); z.err != nil { + return + } + c := z.readByte() + if z.err != nil { + return + } + if c == '/' { + // WHATWG 13.2.5.34 After attribute name state + // U+002F SOLIDUS (/) - Switch to the self-closing start tag state. + return + } + if c != '=' { + z.raw.end-- + return + } + if z.skipWhiteSpace(); z.err != nil { + return + } + quote := z.readByte() + if z.err != nil { + return + } + switch quote { + case '>': + z.raw.end-- + return + + case '\'', '"': + z.pendingAttr[1].start = z.raw.end + for { + c := z.readByte() + if z.err != nil { + z.pendingAttr[1].end = z.raw.end + return + } + if c == quote { + z.pendingAttr[1].end = z.raw.end - 1 + return + } + } + + default: + z.pendingAttr[1].start = z.raw.end - 1 + for { + c := z.readByte() + if z.err != nil { + z.pendingAttr[1].end = z.raw.end + return + } + switch c { + case ' ', '\n', '\r', '\t', '\f': + z.pendingAttr[1].end = z.raw.end - 1 + return + case '>': + z.raw.end-- + z.pendingAttr[1].end = z.raw.end + return + } + } + } +} + +// Next scans the next token and returns its type. +func (z *Tokenizer) Next() TokenType { + z.raw.start = z.raw.end + z.data.start = z.raw.end + z.data.end = z.raw.end + if z.err != nil { + z.tt = ErrorToken + return z.tt + } + if z.rawTag != "" { + if z.rawTag == "plaintext" { + // Read everything up to EOF. + for z.err == nil { + z.readByte() + } + z.data.end = z.raw.end + z.textIsRaw = true + } else { + z.readRawOrRCDATA() + } + if z.data.end > z.data.start { + z.tt = TextToken + z.convertNUL = true + return z.tt + } + } + z.textIsRaw = false + z.convertNUL = false + +loop: + for { + c := z.readByte() + if z.err != nil { + break loop + } + if c != '<' { + continue loop + } + + // Check if the '<' we have just read is part of a tag, comment + // or doctype. If not, it's part of the accumulated text token. + c = z.readByte() + if z.err != nil { + break loop + } + var tokenType TokenType + switch { + case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z': + tokenType = StartTagToken + case c == '/': + tokenType = EndTagToken + case c == '!' || c == '?': + // We use CommentToken to mean any of "<!--actual comments-->", + // "<!DOCTYPE declarations>" and "<?xml processing instructions?>". + tokenType = CommentToken + default: + // Reconsume the current character. + z.raw.end-- + continue + } + + // We have a non-text token, but we might have accumulated some text + // before that. If so, we return the text first, and return the non- + // text token on the subsequent call to Next. + if x := z.raw.end - len("<a"); z.raw.start < x { + z.raw.end = x + z.data.end = x + z.tt = TextToken + return z.tt + } + switch tokenType { + case StartTagToken: + z.tt = z.readStartTag() + return z.tt + case EndTagToken: + c = z.readByte() + if z.err != nil { + break loop + } + if c == '>' { + // "</>" does not generate a token at all. Generate an empty comment + // to allow passthrough clients to pick up the data using Raw. + // Reset the tokenizer state and start again. + z.tt = CommentToken + return z.tt + } + if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' { + z.readTag(false) + if z.err != nil { + z.tt = ErrorToken + } else { + z.tt = EndTagToken + } + return z.tt + } + z.raw.end-- + z.readUntilCloseAngle() + z.tt = CommentToken + return z.tt + case CommentToken: + if c == '!' { + z.tt = z.readMarkupDeclaration() + return z.tt + } + z.raw.end-- + z.readUntilCloseAngle() + z.tt = CommentToken + return z.tt + } + } + if z.raw.start < z.raw.end { + z.data.end = z.raw.end + z.tt = TextToken + return z.tt + } + z.tt = ErrorToken + return z.tt +} + +// Raw returns the unmodified text of the current token. Calling Next, Token, +// Text, TagName or TagAttr may change the contents of the returned slice. +// +// The token stream's raw bytes partition the byte stream (up until an +// ErrorToken). There are no overlaps or gaps between two consecutive token's +// raw bytes. One implication is that the byte offset of the current token is +// the sum of the lengths of all previous tokens' raw bytes. +func (z *Tokenizer) Raw() []byte { + return z.buf[z.raw.start:z.raw.end] +} + +// convertNewlines converts "\r" and "\r\n" in s to "\n". +// The conversion happens in place, but the resulting slice may be shorter. +func convertNewlines(s []byte) []byte { + for i, c := range s { + if c != '\r' { + continue + } + + src := i + 1 + if src >= len(s) || s[src] != '\n' { + s[i] = '\n' + continue + } + + dst := i + for src < len(s) { + if s[src] == '\r' { + if src+1 < len(s) && s[src+1] == '\n' { + src++ + } + s[dst] = '\n' + } else { + s[dst] = s[src] + } + src++ + dst++ + } + return s[:dst] + } + return s +} + +var ( + nul = []byte("\x00") + replacement = []byte("\ufffd") +) + +// Text returns the unescaped text of a text, comment or doctype token. The +// contents of the returned slice may change on the next call to Next. +func (z *Tokenizer) Text() []byte { + switch z.tt { + case TextToken, CommentToken, DoctypeToken: + s := z.buf[z.data.start:z.data.end] + z.data.start = z.raw.end + z.data.end = z.raw.end + s = convertNewlines(s) + if (z.convertNUL || z.tt == CommentToken) && bytes.Contains(s, nul) { + s = bytes.Replace(s, nul, replacement, -1) + } + if !z.textIsRaw { + s = unescape(s, false) + } + return s + } + return nil +} + +// TagName returns the lower-cased name of a tag token (the `img` out of +// `<IMG SRC="foo">`) and whether the tag has attributes. +// The contents of the returned slice may change on the next call to Next. +func (z *Tokenizer) TagName() (name []byte, hasAttr bool) { + if z.data.start < z.data.end { + switch z.tt { + case StartTagToken, EndTagToken, SelfClosingTagToken: + s := z.buf[z.data.start:z.data.end] + z.data.start = z.raw.end + z.data.end = z.raw.end + return lower(s), z.nAttrReturned < len(z.attr) + } + } + return nil, false +} + +// TagAttr returns the lower-cased key and unescaped value of the next unparsed +// attribute for the current tag token and whether there are more attributes. +// The contents of the returned slices may change on the next call to Next. +func (z *Tokenizer) TagAttr() (key, val []byte, moreAttr bool) { + if z.nAttrReturned < len(z.attr) { + switch z.tt { + case StartTagToken, SelfClosingTagToken: + x := z.attr[z.nAttrReturned] + z.nAttrReturned++ + key = z.buf[x[0].start:x[0].end] + val = z.buf[x[1].start:x[1].end] + return lower(key), unescape(convertNewlines(val), true), z.nAttrReturned < len(z.attr) + } + } + return nil, nil, false +} + +// Token returns the current Token. The result's Data and Attr values remain +// valid after subsequent Next calls. +func (z *Tokenizer) Token() Token { + t := Token{Type: z.tt} + switch z.tt { + case TextToken, CommentToken, DoctypeToken: + t.Data = string(z.Text()) + case StartTagToken, SelfClosingTagToken, EndTagToken: + name, moreAttr := z.TagName() + for moreAttr { + var key, val []byte + key, val, moreAttr = z.TagAttr() + t.Attr = append(t.Attr, Attribute{"", atom.String(key), string(val)}) + } + if a := atom.Lookup(name); a != 0 { + t.DataAtom, t.Data = a, a.String() + } else { + t.DataAtom, t.Data = 0, string(name) + } + } + return t +} + +// SetMaxBuf sets a limit on the amount of data buffered during tokenization. +// A value of 0 means unlimited. +func (z *Tokenizer) SetMaxBuf(n int) { + z.maxBuf = n +} + +// NewTokenizer returns a new HTML Tokenizer for the given Reader. +// The input is assumed to be UTF-8 encoded. +func NewTokenizer(r io.Reader) *Tokenizer { + return NewTokenizerFragment(r, "") +} + +// NewTokenizerFragment returns a new HTML Tokenizer for the given Reader, for +// tokenizing an existing element's InnerHTML fragment. contextTag is that +// element's tag, such as "div" or "iframe". +// +// For example, how the InnerHTML "a<b" is tokenized depends on whether it is +// for a <p> tag or a <script> tag. +// +// The input is assumed to be UTF-8 encoded. +func NewTokenizerFragment(r io.Reader, contextTag string) *Tokenizer { + z := &Tokenizer{ + r: r, + buf: make([]byte, 0, 4096), + } + if contextTag != "" { + switch s := strings.ToLower(contextTag); s { + case "iframe", "noembed", "noframes", "noscript", "plaintext", "script", "style", "title", "textarea", "xmp": + z.rawTag = s + } + } + return z +} diff --git a/vendor/golang.org/x/net/publicsuffix/data/children b/vendor/golang.org/x/net/publicsuffix/data/children new file mode 100644 index 0000000..1f16913 Binary files /dev/null and b/vendor/golang.org/x/net/publicsuffix/data/children differ diff --git a/vendor/golang.org/x/net/publicsuffix/data/nodes b/vendor/golang.org/x/net/publicsuffix/data/nodes new file mode 100644 index 0000000..7ac74a1 Binary files /dev/null and b/vendor/golang.org/x/net/publicsuffix/data/nodes differ diff --git a/vendor/golang.org/x/net/publicsuffix/data/text b/vendor/golang.org/x/net/publicsuffix/data/text new file mode 100644 index 0000000..38f06ea --- /dev/null +++ b/vendor/golang.org/x/net/publicsuffix/data/text @@ -0,0 +1 @@ +bonagasukeymachinebondigitaloceanspaces3-website-us-west-1bones3-website-us-west-2boomla1-plenitvedestrandiskstationcillair-traffic-controllagdenesnaaseinet-freaksakurastorageboschristmasakikuchikuseihicampinashikiminohostfoldiskussionsbereicheap-east-2bostik-serverrankoshigayachiyodaklakasamatsudoes-itjmaxxxn--12c1fe0brandisrechtrainingkpmgdbarclays3-fips-us-gov-west-1bostonakijinsekikogentlentapisa-geekarlsoyoriikarmoyoshiokanravoues3-eu-west-3botdashgabadaddjabbottjomelhus-northeast-1bouncemerckmsdsclouditchyouriparsakuratanishiwakinderoyurihonjournalistreaklinksakurawebredirectmelbourneboutiquebecologialaichaugianglassessmentsakyotanabellunoorepairbusanagochigasakishimabarakawagoeboutireserve-onlineboyfriendoftheinternetflixn--12cfi8ixb8lorenskogleezebozen-sudtirolovableprojectjxn--12co0c3b4evalleaostamayukuhashimokitayamaxarnetbankanzakiyosatokorozawap-southeast-7bozen-suedtirolovepopartindevsalangenissandoyusuharazurefdienbienishikatakayamatsushigemrstudio-prodoyolasitequipmentateshinanomachintaifun-dnshome-webservercellillesandefjordietateyamapartments3-ca-central-1bplacedogawarabikomaezakirunord-frontierepbodynathomebuiltwithdarklangevagrarmeniazurestaticappspaceusercontentproxy9guacuedaeguambulancechireadmyblogoip-dynamica-west-180recipescaracalculatorskeninjambylimanowarudaetnaamesjevuemielnogatabuseating-organicbcg123homepagexlimitedeltaitogliattips3-ap-northeast-3utilitiesmall-websozaibetsubamericanfamilydstcgroupperimo-siemenscaledekadena4ufcfaninohekinanporovnospamproxyokoteatonamidsundeportebetsukubank123kotisivultrobjectselinogradimo-i-ranamizuhobby-siteaches-yogano-ip-ddnsgurugbydgoszczecin-addrammenuorogerscblackbaudcdn-edgestackhero-networkinggroupowiat-band-campaignieznoboribetsubsc-paywhirlimodumemergencymruovatlassian-dev-buildereclaims3-ap-south-12hparasiteasypanelblagrigentobamaceiobbcn-north-123websitebuildersvp4lima-citychyattorneyagawafaicloudinedre-eiker2-deloitteastus2000123webseiteckidsmynascloudfrontendofinternet-dnsnasaarlandds3-ap-northeast-123sitewebcamauction-acornimsite164-balsan-suedtirolillyokosukanoyakage2balsfjorddnss3-accesspoint-fips3-ap-east-123paginawebadorsiteshikagamiishibechambagrice-labss3-123minsidaarborteamsterdamnserverbaniamallamazonwebservices-123miwebaccelastx4432-b-datacenterprisesakievennodebalancernfshostrowwlkpnftstorage123hjemmeside5brasiliadboxosascoli-picenord-odalovesickarpaczest-a-la-maisondre-landivtasvuodnakamurataiwanumatajimidorivnebravendbarefootballangenovarahkkeravjuh-ohtawaramotoineppueblockbusternikkoelnishikatsuragit-repostre-toteneiheijiitatebayashikaoizumizakitchenishikawazukamisatokonamerikawaueu-2bresciaogashimadachicappadovaapstecnologiazurewebsitests3-external-1bridgestonebrindisicilynxn--1ck2e1baremetalvdalipaynow-dnsdojobservablehqhaccaltanissettaikikugawaltervistablogivestbyglandroverhallaakesvuemielecceu-3broadwayusuitarumizusawabroke-itkmaxxn--1ctwolominamatargithubpreviewskrakowebview-assetsalatrobeneventochiokinoshimagentositempurlplfinancialpusercontentksatmalluccalvinklein-brb-hostingliwicebrokereportmpartsalon-1brothercules-developerauniteroirmeteorappartypo3serverevistathellebrumunddaluhanskartuzyuullensvanguardivttasvuotnakaniikawatanagurabrusselsaloonissayokoshibahikariyalibabacloudcsaltdalukoweddinglobodontexisteingeekaruizawabryanskierniewicebrynebwcloud-os-instancesaludixn--1lqs03nissedaluroyuzawabzhitomirhcloudiyclientozsdegreeclinicapitalonecliniquenoharaclothingdustdatadetectranbycngouv0cnpyatigorskiptveterinaireadymadethis-a-anarchistjordalshalsencntrani-andria-barletta-trani-andriacodespotenzagancoffeedbackanagawarszawashtenawsapprunnerdpoliticaarpharmaciensanjosoyrocommunity-prochowicecomochizukillvivanovoldacompanyantagonistockholmestrandurumisakimobetsumidanangodoesntexistmein-iservschulegallerycomparemarkerryhotelsannancomputercomsecretrosnubargainsureadthedocs-hosteditorxn--0trq7p7nnishimeraugustow-corp-staticblitzgierzgoraktyubinskaunicommuneencoreapiacenzabc01kapp-ionosegawadlugolekaascolipicenocelotennishiawakuracingheannakadomarineat-urlive-oninomiyakonojorpelandeus-canvasitebinatsukigatajiri234condoshiibabybluebitemasekd1conferenceconstruction-vaporcloudplatformshangriladeskjakamaiedge-stagingreaterconsuladobeio-static-accesscamdvrcampaniaconsultantraniandriabarlettatraniandriaconsultingrebedocapooguycontactivetrailwaycontagematsubaracontractorstababymilkashiwaraconvexecute-apictetcieszyncookingretakahatakaishimokawacooperativano-frankivskjervoyagecoprofesionalchikugodaddyn-o-saurealestatefarmerseinecorsicable-modemoneycosenzakopanecosidnsiskinkyowariasahikawasmercouchpotatofriesannoheliohostrodawaracouncil-central-1couponstackitagawassamukawatarikuzentakatairacozoracpservernamegataishinomakiloappsanokashiwazakiyosellsyourhomeftpharmacyonabaruminamiizukaminokawanishiaizubangecqldyndns-at-homedepotaruiocrankycrdyndns-at-workisboringsakershus-central-1creditcardyndns-blogsytecreditunion-webpaaskoyabenogiftsantamariakecremonasharissadistoloseyouriphdfcbankasserversembokutamakiyosunndalcrewp2cricketnedalcrimeast-kazakhstanangercrispmanagercrminamimakinfinitigooglecodebergrimstadyndns-freeboxosloisirsantoandrealtysnesanukinternationalcrotonecrowniphilipsaobernardovre-eikercrsaogoncanthoboleslawiecommerce-shopitsitecruisesaotomeldalcryptonomichiharacuiabacgiangiangrycuisinellahppictureshinordeste-idclkasukabeatsardegnarvikasumigaurayasudacuneocuritibackdropalermoarekembuchikumagayagawakkanaikawachinaganoharamcoacharitydalaheadjuegoshikibichuocutegirlfriendyndns-homednsardiniafedoraproject-studynaliasnesoddeno-stagingroks-thisayamanobearalvahkijoburgrayjayleagueschokokekscholarshipschoolbusinessebytomaridagawalmartransiphotographysiofeirafembetsukuintuitranslatefermockaszubytemarketingvollferraraferrarinuyamashinazawaferreroticahcesuolohmusashimurayamaizurunschuldockatowicefetsundyndns-remotewdyndns-iphonefossarlfgrongrossetouchijiwadediboxn--2m4a15efhvalerfilegear-sg-1filminamioguni5finalfinancefinnoyfirebaseapplinzinvestmentschulplattforminamisanrikubetsupersalevangerfirenetlibp2phutholdingsmartlabelingroundhandlingroznysaikisosakitahatakamatsukawafirenzefirestonefirmdaleilaocairtelebitbucketrzynh-servebeero-stageiseiroutingthecloudyndns-serverisignfishingokaseljeephuyenfitjarfitnessettsurugiminamitanefjalerflesbergrphxn--2scrj9caravanylvenetoeidsvollutrausercontentoyotsukaidownloadnpassenger-associationl-ams-1flickragerotikagaminordlandyndns-webhareidsbergriwataraindropikeflierneflirflogintohmalopolskanitransportefloppymntransurlfloraclegovcloudappschulserverflorencefloripadualstackatsushikabeautypedreamhosterschwarzgwesleyfloristanohatakahamalselveruminamiuonumatrixn--30rr7yflororoscrapper-sitefltrapanikolaeventscrappingrueflutterflowest1-us1-plenitravelersinsuranceflyfncarbonia-iglesias-carboniaiglesiascarboniafndyndns-wikindlegnicagliaricoharulezajskierval-d-aosta-valleyfoolfor-ourfor-somedusajscryptedyndns-worksarufutsunomiyawakasaikaitakokamikoaniikappudopaaskvolloanswatchesasayamattelemarkhangelskasuyakumodsasebofagefor-theaterfordeatnuniversitysvardoforexrotheshopwarezzoforgotdnscrysecuritytacticscwesteuropencraftravinhlonganforli-cesena-forlicesenaforlifestyleirfjordyndns1forsalesforceforsandasuolojcloud-ver-jpcargoboavistanbulsan-sudtirolutskarumaifminamifuranofortalfosneservehttpbincheonfotrdynnsassarintlon-2foxn--32vp30hachinoheavyfozfr-par-1fr-par-2franalytics-gatewayfredrikstadynservebbsaudafreedesktopazimuthaibinhphuocprapidynuddnsfreebox-osauheradyndns-mailovecollegefantasyleaguefreemyiphostyhostinguidedyn-berlincolnfreesitefreetlservehumourfreightrentin-sudtirolfrenchkisshikirkeneserveircarrdrayddns-ipatriafresenius-central-2friuli-v-giuliarafriuli-ve-giuliafriuli-vegiuliafriuli-venezia-giuliafriuli-veneziagiuliafriuli-vgiuliafriuliv-giuliafriulive-giuliafriulivegiuliafriulivenezia-giuliafriuliveneziagiuliafriulivgiuliafrlfroganserveminecraftrentin-sued-tirolfrognfrolandynuhosting-clusterfrom-akamaiorigin-staginguitarservemp3from-alfrom-arfrom-azureedgekey-stagingujaratmetacentrumbriafrom-callyfrom-cockpitrentin-suedtirolfrom-ctrentino-a-adigefrom-dcasacampinagrandebulsan-suedtiroluxenonconnectoyourafrom-debianfrom-flatangerfrom-gamvikatsuyamashikizunokuniminamiashigarafrom-hidnservep2pimientakazakinzais-a-bruinsfanfrom-iafrom-idynv6from-ilfrom-in-the-bandairtrafficplexus-2from-kservepicservequakefrom-kyfrom-lamericanexpresseljordyroyrvikingroceryfrom-malvikaufentigerfrom-mdfrom-meetrentino-aadigefrom-mifunefrom-mnfrom-modalenfrom-mservesarcasmolaquilarvikautokeinotionfrom-mtlservicebuskerudfrom-ncasertainairflowersalvadorfrom-ndfrom-nefrom-nhlfanfrom-njsevastopolitiendafrom-nminamiyamashirokawanabeepsongdalenviknagaraholtaleniwaizumiotsurugashimagazinefrom-nvalled-aostaobaolbia-tempio-olbiatempioolbialowiezachpomorskiengiangujohanamakinoharafrom-nyatomigrationidfrom-ohdancefrom-okegawatsonionjukujitawarafrom-orfrom-palmasfjordenfrom-praxihuanfrom-ris-a-bulls-fanfrom-schmidtre-gauldalfrom-sdfrom-tnfrom-txn--3bst00minanofrom-utsiracusagamiharafrom-val-daostavalleyfrom-vtrentino-alto-adigefrom-wafrom-wiardwebspace-hostorachampionshiptodayfrom-wvalledaostargetrentino-altoadigefrom-wyfrosinonefrostalowa-wolawafroyal-commissionporterfruskydivingulenfujiiderafujikawaguchikonefujiminokamoenais-a-candidatefujinomiyadatsunanjoetsulublindesnesevenassieradzfujiokazakirovogradoyfujisatoshoesewestus2fujisawafujishiroishidakabiratoridecafederation-ranchernigovallee-aosteroyfujitsuruokagoshimamurogawafujiyoshidattorelayfukayabeagleboardfukuchiyamadattoweberlevagangaviikanonjis-a-catererfukudomigawafukuis-a-celticsfanfukumitsubishigakiryuohkurafukuokakamigaharafukuroishikariwakunigamihamadavvenjargalsacefukusakisarazure-apigeefukuyamagatakaharunjargaularavellinodeobjectstoragefunabashiriuchinadavvesiidaknongunmaoris-a-chefarsundyndns-office-on-the-webflowtest-iservebloginlinefunagatakahashimamakishiwadazaifudaigoguovdageaidnunusualpersonfunahashikamiamakusatsumasendaisenergyeonggildeskaliszfundfunkfeuerfunnelsexyfuoiskujukuriyamandalfuosskodjeezfurubirafurudonordre-landfurukawaiishoppingushikamifuranore-og-uvdalfusodegaurafussagemakerfutabayamaguchinomihachimanagementrentino-s-tirolfutboldlygoingnowhere-for-more-og-romsdalfuttsurutashinais-a-conservativefsnoasakakinokiafuturecmsheezyfuturehostingxn--3ds443gzfuturemailingfvghakonehakubaclieu-1hakuis-a-cpaneliv-dnshimosuwalkis-a-cubicle-slaveroykenhakusandnessjoenhaldenhalfmoonscaleforcehalsaitamatsukuris-a-democratrentino-stirolham-radio-opocznortonkotsumomodelscapetownnews-staginghamburghammarfeastasiahamurakamigoris-a-designerhanamigawahanawahandahandcraftedugit-pages-researchedmarketplacehangglidinghangoutrentino-sud-tirolhannannestadhannoshiroomghanoipinbrowsersafetymarketshimotsukehanyuzenhappoumuginowaniihamatamakawajimangolffanshimotsumayfirstreamlitappinkddiamondshinichinanhasamazoncognito-idpdnshinjotelulucaniahasaminami-alpshinjukuleuvenicehashbanghasudahasura-appinokofuefukihaborovigoldpoint2thisamitsukehasvikfh-muensterhatenablogisticsxn--3e0b707ehatenadiaryhatinhachiojiyachtshellhatogayahabacninhbinhdinhktrentino-sudtirolhatoyamazakitakamiizumisanofidongthapmircloudnsupdaterhatsukaichikawamisatohokkaidonnakanotoddenhattfjelldalhayashimamotobusellfyis-a-doctoruncontainershinkamigotourshinshinotsupplyhazuminobushibuyahikobearblogsiteleaf-south-1helpgfoggiahelsinkitakatakanabeardubaioirasebastopoleapcellclstagehirnhemneshinshirohemsedalhepforgeblockshintokushimaheroyhetemlbfanheyflowhoswholidayhigashiagatsumagoianiahigashichichibuzentsujiiehigashihiroshimanehigashiizumozakitakyushunantankhakassiahigashikagawahigashikagurasoedahigashikawakitaaikitamiharunzenhigashikurumegurownproviderhigashimatsushimarcherkasykkylvenneslaskerrypropertieshintomikasaharahigashimatsuyamakitaakitadaitoigawahigashimurayamamotorcycleshinyoshitomiokamishihorohigashinarusells-for-lesshiojirishirifujiedahigashinehigashiomitamamurausukitamotosumy-routerhigashiosakasayamanakakogawahigashishirakawamatakanezawahigashisumiyoshikawaminamiaikitanakagusukumodenaklodzkobierzycehigashitsunotairesindevicenzamamihokksundhigashiurawa-mazowszexposeducationhercules-appioneerhigashiyamatokoriyamanashijonawatehigashiyodogawahigashiyoshinogaris-a-financialadvisor-aurdalhiphoplixn--3hcrj9cashorokanaiehippythonanywherealtorhiraizumisatokaizukakudamatsuehirakatashinagawahiranais-a-fullstackharkivallee-d-aostehirarahiratsukagawahirayahoooshikamagayaitakaokalmykiahitachiomiyakehitachiotaketakarazukaluganskharkovalleeaostehitradinghjartdalhjelmelandholyhomegoodshioyaltaketomisatoyakokonoehomeipippugliahomelinuxn--3pxu8khersonyhomesecuritymacaparecidahomesecuritypccwuozuerichardliguriahomesenseeringhomeskleppivohostinghomeunixn--41ahondahonjyoitakasagonohejis-a-geekhmelnitskiyamashikokuchuohornindalhorsells-for-usgovcloudapilottotalhortenkawahospitalhotelwithflightshirahamatonbetsupportrentino-sued-tirolhotmailhoyangerhoylandetakasakitashiobarahrsnillfjordhungyenhurdalhurumajis-a-goodyearhyllestadhyogoris-a-greenhypernodessaitokamachippubetsuikitaurahyugawarahyundaiwafuneis-not-certifiedis-savedis-slickhplayitrentinos-tirolis-uberleetrentinostirolis-very-badis-very-evillasalleitungsenis-very-goodis-very-niceis-very-sweetpepperugiais-with-thebandoomdnshisuifuettertdasnetzisk01isk02jenv-arubahcavuotnagahamaroygardengerdalp1jeonnamsosnowiecateringebumbleshrimperiajetztrentinosud-tiroljevnakerjewelryjlljls-sto1jls-sto2jls-sto365jmpiwatejnjdfirmalborkdaljouwwebhoptokigawajoyokaichibahccavuotnagaivuotnagaokakyotambabia-goraclecloudappssejny-2jozis-a-knightpointtokashikiwakuratejpmorgangwonjpncatfoodrivelandrobakamaihd-stagingloomy-gatewayjprshitaramakoseis-a-libertariankosherokuappizzakoshimizumakis-a-linux-useranishiaritabashikshacknetlifylkesbiblackfridaynightrentino-suedtirolkoshugheshizuokamitsuekosugekotohiradomainshoujis-a-llamarugame-hostrowieconomiasadogadobeioruntimedicinakanojogaszkolamdongnairlineedleasingkotourakouhokumakogenkounosunnydaykouyamassa-carrara-massacarraramassabuzzkouzushimassivegridkozagawakozakis-a-musiciankozowienkppspbarsycenterprisecloudbeesusercontentaveusercontentawktoyonakagyokutoyonezawauiusercontentdllive-websitebizenakasatsunairportashkentatamotors3-deprecatedgcaffeinehimejibxos3-eu-central-1krasnikahokutokyotangopensocialkrasnodarkredumbrellapykrelliankristiansandcatshowakristiansundkrodsheradkrokstadelvaldaostaticsigdalkropyvnytskyis-a-nascarfankrymisasaguris-a-nursells-itrentinoa-adigekumamotoyamasudakumanowtvaomoriguchiharag-cloud-charternopilawakayamafeloabatochigiehtavuoatnabudejjurkumatorinokumejimatlabgkumenanyokkaichirurgiens-dentistes-en-francekundenkunisakis-a-painterhostsolutionshiranukamisunagawakunitachiaraisaijolsterkunitomigusukukis-a-patsfankunneppubtlsiiitesilknx-serversicherungkuokgroupkomatsushimasoykurgankurobeebyteappenginekurogiminamiawajikis-a-personaltrainerkuroisoftwarendalenugkuromatsunais-a-photographermesserlikescandypoppdalkuronkurotakikawasakis-a-playershiftrentinoaadigekushirogawakustanais-a-republicanonoichinosekigaharakusupabaseoullensakerkutchanelkutnokuzumakis-a-rockstarachowicekvafjordkvalsundkvamfamplifyappchizipifony-1kvanangenkvinesdalkvinnheradkviteseidatingkvitsoykwpspdnsimple-urlmktgorymmvareservdmoliserniamombetsuppliesimplesitemonza-brianzapposirdalmonza-e-della-brianzaptomobegetmyipirangallocustomer-ocienciamonzabrianzaramonzaebrianzamonzaedellabrianzamordoviamorenarashinoharamoriyamatsumotofukemoriyoshiminamibosogndalmormonstermoroyamatsunomortgagemoscowiiheyaizuwakamatsubushikusakadogawamoseushimoichikuzenmosjoenmoskenesiskomaganemosslingmotegirlymoviemovimientonsbergmtnmtranaritakurashikis-a-socialistordalmuikaminoyamaxunison-serviceslupskomforbarrell-of-knowledgeu-central-2mukodairamunakatanemuosattemupl-wawsappspacehostedpicardmurmanskommunalforbundmurotorcraftrentinosued-tirolmusashinodesakatakatsukis-a-soxfanmuseumisawamusicampobassociateslzmutsuzawamutualmyactivedirectorymyaddrangedalmyamazeplaystation-cloudyclustersmushcdn77-sslgbtrentinosuedtirolmyasustor-elvdalmycloudnasushiobaramydattolocalcertificationmydbservermyddnskingmydissentrentinsud-tirolmydnsokamogawamydobissmarterthanyousrcfdmydsokndalmyeffectrentinsudtirolmyfastly-edgemyfirewalledreplittlestargardmyforumisconfusedmyfritzmyftpaccessolardalmyhome-servermyjinomykolaivencloud66mymailermymediapcatholicp1mynetnamegawamyokohamamatsudamypeplatter-applcube-serversusakis-a-studentalmypetsolundbeckommunemyphotoshibalena-devicesomamypigboatsomnaturalmypsxn--45br5cylmyrdbxn--45brj9caxiaskimitsubatamicrolightingloppennemysecuritycamerakermyshopblocksoowilliamhillmyshopifymyspreadshopselectrentinsued-tirolmysynologyeongnamdinhs-heilbronnoysundmytabitordermythic-beastsopotrentinsuedtirolmytis-a-bloggermytuleap-partnersor-odalmyvnchernovtsydneymywiredbladehostingpodhalepodlasiellakdnepropetrovskanlandpodzonepohlpoivronpokerpokrovskomonotteroypolkowicepoltavalle-aostavangerpolyspacepomorzeszowinbarsyonlinexus-3ponpesaro-urbino-pesarourbinopesaromasvuotnarusawapordenonepornporsangerporsangugeporsgrunnanpoznanprdprereleaserveftplockerprgmrprimeteleportrentoyookanazawaprincipenzaprivatelinkyard-cloudletsor-varangerprivatizehealthinsuranceprogressivegarsheiyufueliv-apiemontepromoldefinimaringatlangsondriobranconakamai-stagingpropertysfjordprotectionprotonettrevisohuissier-justiceprudentialpruszkowindowsservegame-serverprvcyou2-localtonetroandindependent-inquest-a-la-masionprvwineprzeworskogpunyukis-a-teacherkassyncloudpupulawypussycatanzarowinnersorfoldpvhachirogatakamoriokakegawapvtrogstadpwchiryukyuragifuchungbukharavennakaiwanairforceopzqotoyohashimotottoris-a-techietis-a-gurusgovcloudappnodeartheworkpcasinorddaluxuryqponiatowadaqsldqualifioapplumbingotembaixadaqualyhqpartnerqualyhqportalquangngais-a-therapistoiaquangninhthuanquangtritonoshonais-an-accountantshiraois-a-hard-workershirakolobrzegersundojin-dslattuminisitequickconnectroitskomorotsukamiminequicksytesorocabalestrandabergamobaragusabaerobaticketsorreisahayakawakamiichinomiyagitbookinghosteurovisionrenderquipelementsortlandquizzesorumishimatsumaebashimogosenqzzventurestaurantulaspeziavestfoldvestnesquaresinstagingvestre-slidrecifedexperts-comptablesrhtrustkaneyamazoevestre-totenris-an-anarchistorfjordvestvagoyvevelstadvfsrlvibo-valentiavibovalentiavideovinhphuchonanbungotakadaptableclercaobanglogowegroweiboliviajessheimmobilienisshingucciminamiechizeniyodogawavinnicanva-hosted-embedzin-buttervinnytsiavipsinaapplurinacionalvirginankokubunjis-an-artistorjdevcloudjiffyresdalvirtual-uservecounterstrikevirtualservervirtualuserveexchangevisakuholeckochikushinonsenasakuchinotsuchiurakawaviterboknowsitallvivianvivoryvixn--4dbgdty6choseikarugallupfizervkis-an-engineeringvlaanderenvladikavkazimierz-dolnyvladimirennesoyvlogvmitoyoakevolvologdanskonskowolayangroupixolinodeusercontentrentinosudtirolvolyngdalvoorlopervossevangenvotevotingvotoyosatoyonovpnplus-west-3vps-hostrynvusercontentunespritesoundcastripperwithgoogleapiszwithyoutubentrendhostingwiwatsukiyonotebook-fipstuff-4-salewixsitewixstudio-fipstufftoread-booksnesowawjgorawkzwloclawekonsulatinowruzhgorodwmcloudwmeloywmflabsurveyspectrumisugitolgap-north-1wnextdirectwpdevcloudwoodsideliveryworldworse-thanhphohochiminhackerwowiosrvrlessourcecraftromsakegawawpenginepoweredwphostedmailwpmucdn77-storagencywpmudevinappsusonowpsquaredwroclawsglobalacceleratorahimeshimagine-proxywtcp4wtfastly-terrariuminamiminowawwwitdkontogurawzmiuwajimaxn--54b7fta0cchoshichikashukudoyamalatvuopmicrosoftbankasaokamikitayamatsurindigenamsskoganeindustriaxn--55qw42gxn--55qx5dxn--5dbhl8dxn--5js045dxn--5rtp49chowderxn--5rtq34konyvelolipopmckinseyxn--5su34j936bgsgxn--5tzm5gxn--6btw5axn--6frz82gxn--6orx2rxn--6qq986b3xlxn--7t0a264choyodobashichinohealthcareersame-previeweirxn--80aaa0cvacationsuzakarpattiaaxn--80adxhksuzukananiimilanoticiassurgerydxn--80ao21axn--80aqecdr1axn--80asehdbasicserver-on-k3s3-me-south-1xn--80aswgxn--80audiopsysuzukis-an-actorxn--8dbq2axn--8ltr62koobindalxn--8pvr4uzhhorodxn--8y0a063axn--90a1affinitylotterybnikeeneticp0xn--90a3academiamibubbleappspotagerxn--90aeroportsinfolkebibleangaviikafjordpabianicentralus-1xn--90aishobaraoxn--90amcprequalifymeiwamizawaxn--90azhytomyradweblikes-piedmontunkoninfernovecorespeedpartnerxn--9dbq2axn--9et52uzsprytromsojampanasonichitachinakagawarmiastaplesame-appaviaxn--9krt00axn--9tfkyxn--andy-iraxn--aroport-byamembersvalbarduponthewifidelitypeformitourismilexn--asky-iraxn--aurskog-hland-jnbasilicataniaukraanghkeisenebakkeshibukawakeliwebhostingdyniakunemurorangecloudscalebookonlineustarostwodzislawdev-myqnapcloudflarecn-northwest-1xn--avery-yuasakuragawaxn--b-5gausdalxn--b4w605ferdxn--balsan-sdtirol-nsbasketballfinanzjaworznoticeableksvikapsiciliaurland-4-salernombrendlyngenflfanpachihayaakasakawaharaffleentrycloudflare-ipfstgstageorgeorgiap-southeast-4xn--bck1b9a5dre4chrome-central-1xn--bdddj-mrabdxn--bearalvhki-y4axn--berlevg-jxaxn--bhcavuotna-s4axn--bhccavuotna-k7axn--bidr-5nachikatsuuraxn--bievt-0qa2hosted-by-previderxn--bjddar-ptarnobrzegxn--blt-elabkhaziaxn--bmlo-grafana-developmentunnelmolexn--bod-2naturbruksgymnxn--bozen-sdtirol-2obihirosakikamijimatsuzakis-an-entertainerxn--brnny-wuacademy-firewall-gatewayxn--brnnysund-m8accident-investigation-aptibleadpagespeedmobilizeropschaefflerxn--brum-voagaturindalxn--btsfjord-9zaxn--bulsan-sdtirol-nsbatsfjordigickaracologneu-south-1xn--c1avgxn--c2br7gxn--c3s14mittwaldserverxn--cck2b3bauhauspostman-echofunatoriginstitutemp-dns3-object-lambda-urlolitapunkaragandaurskog-holandinggff5xn--cckwcxetdxn--cesena-forl-mcbnpparibashkiriaxn--cesenaforl-i8axn--cg4bkis-byklecznagatoromskoguchilloutsystemscloudsitevaksdalxn--ciqpnxn--clchc0ea0b2g2a9gcdxn--czr694beppublic-inquiryonagoyaustevollivingitlabbvieeemfakefurniturealtimedio-campidano-mediocampidanomediobninsk8s3-eu-north-1xn--czrs0t0xn--czru2dxn--d1acj3beskidyn-ip24xn--d1alfastlylbarrel-of-knowledgesuite-stagingivingjemnes3-globalatinabelementorayomitanobservereggio-emilia-romagnarutoolsztynsetatsunofficialivornomniwebspaceconfigma-governmentattoolforgeu-4xn--d1aturystykanieruchomoscientistreakusercontentrvarggatrysiljanewayxn--d5qv7z876chungnamdalseidfjordrrppgwangjulvikashibatakatorindustriesteinkjerxn--davvenjrga-y4axn--djrs72d6uyxn--djty4kooris-a-lawyerxn--dnna-graingerxn--drbak-wuaxn--dyry-iraxn--e1a4churchateblobanazawanggoupilefrakkestadtvsamegawaxn--eckvdtc9dxn--efvn9svchitosetogakushimotoganexn--efvy88hadanorth-kazakhstanxn--ehqz56nxn--elqq16hadselbuyshouseshimonitayanagitappwritesthisblogdnsfor-better-thanhhoamishirasatohnoshookuwanakatsugawaxn--eveni-0qa01gaxn--f6qx53axn--fct429kopervikmpspawnbaseminexn--fhbeiarnxn--finny-yuaxn--fiq228c5hsbciprianiigataipeigersundtwhitesnowflakeyword-onfabricafjsamnangerxn--fiq64bestbuyshoparenagareyamagicpatternsapporokunohealth-carereformemorialombardiademergentagents3-sa-east-1xn--fiqs8sveioxn--fiqz9svelvikongsvingerxn--fjord-lraxn--fjq720axn--fl-ziaxn--flor-jraxn--flw351exn--forl-cesena-fcbremangerxn--forlcesena-c8axn--fpcrj9c3dxn--frde-grajewolterskluwerxn--frna-woarais-certifiedxn--frya-hraxn--fzc2c9e2circleaninglugsjcbgmbhartinnxn--fzys8d69uvgmailxn--g2xx48ciscofreakadnsaliases121xn--gckr3f0fastvps-serveronakatombetsumitakagiizeaburxn--gecrj9cistrondheiminamiiseharaxn--ggaviika-8ya47haebaruericssonlanxesshimonosekikawaxn--gildeskl-g0axn--givuotna-8yanagawaxn--gjvik-wuaxn--gk3at1exn--gls-elacaixaxn--gmq050is-coolblogspotrentinoalto-adigexn--gmqw5axn--gnstigbestellen-zvbetaharanzanquangnamasteigenkainanaejrietiengiangjerdrumemsetaxiijimarnardalombardynamisches-dns3-us-east-2xn--gnstigliefern-wobiraxn--h-2failxn--h1ahnxn--h1alizxn--h2breg3evenesvn-reposphinxn--45q11cooldns-cloudflareglobalashovhackclubartowhmincommbankazoxn--h2brj9c8citadelhichisoctrangminakamichikaiseiyoichipsamparaglidingmodellingmx-central-1xn--h3cuzk1dielddanuorrittogojomediatechnologyeongbukoryokamikawanehonbetsuwanouchikuhokuryugasakis-a-liberalxn--hbmer-xqaxn--hcesuolo-7ya35bhzc66xn--hebda8bialystokkepnord-aurdalwaysdatabase44-sandboxfuseekarasjohkameyamatotakadaustrheimbamblebtimnetzgorzeleccocottemprendealstahaugesundereggio-calabriap-southeast-5xn--hery-iraxn--hgebostad-g3axn--hkkinen-5waxn--hmmrfeasta-s4accident-prevention-fleeklogesquare7xn--hnefoss-q1axn--hobl-iraxn--holtlen-hxaxn--hpmir-xqaxn--hxt814exn--hyanger-q1axn--hylandet-54axn--i1b6b1a6a2exn--imr513nxn--indery-fyanaizuxn--io0a7is-foundationxn--j1adpmnxn--j1aefauskedsmokorsetagayaseralingenoaiusercontentranoyxn--j1ael8bielawalbrzychaselfiparliamentayninhachijoinmcdireggiocalabriauth-fipsiqcxjavald-aostatichostreak-linkanumazuryokozempresashibetsukumiyamagasakinkobayashimofusagaeroclubmedecin-berlindasdaejeonbuk0emmafann-arborlanddl-o-g-i-nayoro0o0g0xn--j1amhagakhanhhoabinhduongxn--j6w193gxn--jlq480n2rgxn--jlster-byandexcloudxn--jrpeland-54axn--jvr189miuraxn--k7yn95exn--karmy-yuaxn--kbrq7oxn--kcrx77d1x4axn--kfjord-iuaxn--klbu-woaxn--klt787dxn--kltp7dxn--kltx9axn--klty5xn--4dbrk0cexn--koluokta-7ya57hagebostadxn--kprw13dxn--kpry57dxn--kput3is-gonexn--krager-gyaotsurnadalxn--kranghke-b0axn--krdsherad-m8axn--krehamn-dxaxn--krjohka-hwab49jejusgovtrafficmanagerxn--ksnes-uuaxn--kvfjord-nxaxn--kvitsy-fyasakaiminatoyotap-southeast-3xn--kvnangen-k0axn--l-1fairwindsurfbsbxn--1qqw23axn--l1accentureklamborghinikonantoshimatsusakahoginozawaonsennanmokurennebunkyonanaoshimamateramochausercontentuscanyxn--laheadju-7yasugithubusercontentushungryxn--langevg-jxaxn--lcvr32dxn--ldingen-q1axn--leagaviika-52biella-speziauthgear-stagingitpagemrappui-productions3-eu-west-1xn--lesund-huaxn--lgbbat1ad8jelasticbeanstalklabudhabikinokawabajddarvanedgecompute-1xn--lgrd-poacctfcloudflareanycastdlibestadultuvalle-daostakkomakis-an-actresshiraokamitondabayashiogamagoriziaxn--lhppi-xqaxn--linds-pratoyotomiyazakis-into-animeinforumzxn--loabt-0qaxn--lrdal-sraxn--lrenskog-54axn--lt-liaciticurus-4xn--lten-granexn--lury-iraxn--m3ch0j3axn--mely-iraxn--merker-kuaxn--mgb2ddeswidnicanva-appspjelkavikomvuxn--42c2d9axn--mgb9awbfbx-osaveincloudyndns-picsbsarpsborgripeeweeklylotteryxn--mgba3a3ejtuxfamilyxn--mgba3a4f16axn--mgba3a4fra1-dell-ogliastrapiappleyxn--mgba7c0bbn0axn--mgbaam7a8haibarakitahiroshimap-south-2xn--mgbab2bdxn--mgbah1a3hjkrdxn--mgbai9a5eva00bielskoczow-credentialless-staticblitzlgjerstadiscordsays3-us-gov-east-1xn--mgbai9azgqp6jelenia-goraxn--mgbayh7gparallelxn--mgbbh1a71exn--mgbc0a9azcgxn--mgbca7dzdoxn--mgbcpq6gpa1axn--mgberp4a5d4a87gxn--mgberp4a5d4arxn--mgbgu82axn--mgbi4ecexperimentswidnikitagatakinouexn--mgbpl2fhskosaigawaxn--mgbqly7c0a67fbcivilaviation-riopretogitsulidluyaniizaporizhzhiaxn--mgbqly7cvafricanvacode-builder-stg-builderxn--mgbt3dhdxn--mgbtf8fldrvaroyxn--mgbtx2bieszczadygeyachimataijiiyamanouchikujoinvilleirvikarasjoketokuyamarumorimachidauthgearapps-1and1xn--mgbx4cd0abogadobeaemcloud-ip6xn--mix082fbxosaves-the-whalessandria-trani-barletta-andriatranibarlettaandriaxn--mix891fedjeducatorprojectransfer-webapp-fipsavonatalxn--mjndalen-64axn--mk0axindependent-inquiryxn--mk1bu44clanbibaiduckdnsamsclubin-vpndnsamsungotsukisofukushimaniwamannordreisa-hockeynutwentertainmentoystre-slidrettozawaxn--mkru45is-into-carshiratakahagiangxn--mlatvuopmi-s4axn--mli-tlavagiskexn--mlselv-iuaxn--moreke-juaxn--mori-qsakurais-into-cartoonshishikuis-a-hunterxn--mosjen-eyasuokanmakiyokawaraxn--mot-tlavangenxn--mre-og-romsdal-qqbuserveboltuyenquangbinhthuanxn--msy-ula0haiduongxn--mtta-vrjjat-k7aflakstadaokayamazonaws-cloud9xn--muost-0qaxn--mxtq1miyazure-mobilexn--ngbc5azdxn--ngbe9e0axn--ngbrxn--4gbriminiserverxn--nit225kosakaerodromegadgets-itcouldbeworfashionstorebaseballooningroks-theatrentin-sud-tirolxn--nmesjevuemie-tcbalsan-sudtirolkuszczytnoopstmnxn--nnx388axn--nodellogliastraderxn--nqv7fs00emaxn--nry-yla5gxn--ntso0iqx3axn--ntsq17gxn--nttery-byaeservehalflifeinsurancexn--nvuotna-hwaxn--nyqy26axn--o1achernivtsienaharimakeupsunappgafanxn--o3cw4haiphongonnakayamangyshlakamaized-stagingxn--o3cyx2axn--od0algardxn--od0aq3bievathletajimabaria-vungtaudibleborkangereggioemiliaromagnarviikamiokameokamakurazakiwielunnerehabmereisenishinomiyashironomurauthordalandroidgnishiizunazukifr-1xn--ogbpf8flekkefjordxn--oppegrd-ixaxn--ostery-fyatsukannamimatakasugais-into-gamessinaplesknshisognexn--osyro-wuaxn--otu796dxn--p1acfolkswiebodzindependent-commissionxn--p1ais-leetrentinoaltoadigexn--pgbs0dhlxn--4it168dxn--porsgu-sta26fedorainfracloudfunctionsaxoxn--pssu33lxn--pssy2uxn--q7ce6axn--q9jyb4cldmail-boxn--1lqs71durbanamexnetgamersandvikcoromantovalle-d-aostavernxn--qcka1pmclerkstagexn--qqqt11miyotamanoxn--qxa6axn--qxamjondalenxn--rady-iraxn--rdal-poaxn--rde-ulazioxn--rdy-0nabaris-localplayerxn--rennesy-v1axn--rhkkervju-01afedorapeopleikangerxn--rholt-mragowoltlab-democraciaxn--rhqv96gxn--rht27zxn--rht3dxn--rht61exn--risa-5navigationxn--risr-iraxn--rland-uuaxn--rlingen-mxaxn--rmskog-byatsushiroxn--rny31hair-surveillancexn--rovu88bifukagawalesundiscordsezpisdnipropetrovskypecorindependent-paneliv-cdn77-securealmesswithdns3-us-gov-west-1xn--rros-granvindafjordxn--rskog-uuaxn--rst-0navois-lostrolekamaishimodatexn--rsta-framercanvaswinoujsciencexn--rvc1e0am3exn--ryken-vuaxn--ryrvik-byawaraxn--s-1faithainguyenxn--s9brj9clever-clouderavpagexn--sandnessjen-ogbizxn--sandy-yuaxn--sdtirol-n2axn--seral-lraxn--ses554gxn--sgne-graphicswisspockongsbergxn--skierv-utazurecontainerimakanegasakis-not-axn--skjervy-v1axn--skjk-soaxn--sknit-yqaxn--sknland-fxaxn--slat-5navuotnaroyxn--slt-elabrdns-dynamic-dnsabruzzombieidskogasawarackmazerbaijan-mayenbaidarchitectestingrok-freeddnsgeekgalaxyzxn--smla-hraxn--smna-gratangenxn--snase-nraxn--sndre-land-0cbigv-infolldalomodxn--11b4c3discountry-snowplowiczeladzw-staticblitzxn--snes-poaxn--snsa-roaxn--sr-aurdal-l8axn--sr-fron-q1axn--sr-odal-q1axn--sr-varanger-ggbiharstadotsubetsugaruhr-uni-bochumsochimkenthickarasuyamashikeu-south-2xn--srfold-byawatahamaxn--srreisa-q1axn--srum-gratis-a-bookkeepermarriottwmailxn--stfold-9xaxn--stjrdal-s1axn--stjrdalshalsen-sqbihoronobeokagakikiraraumaintenanceu1-plenittedalomzaporizhzhegurindependent-review3s3-us-west-1xn--stre-toten-zcbikedaemongolianishinoomotegoismailillehammerfeste-iparmatta-varjjathruherebungoonomutazas3-us-west-2xn--t60b56axn--tckwebthingsxn--tiq49xqyjellybeanxn--tjme-hraxn--tn0agrondarqtxn--tnsberg-q1axn--tor131oxn--trany-yuaxn--trentin-sd-tirol-rzbioxn--trentin-sdtirol-7vbirkenesoddtangentapps3-website-ap-northeast-1xn--trentino-sd-tirol-c3bittermezproxyonagunicloudiscourses3-website-ap-southeast-1xn--trentino-sdtirol-szbjerkreimdbarcelonagawakuyabukihokuizumocha-sandboxmitakeharaudnedalnishigorlicebinordkapparisor-fronishiharakrehamnishiazaibradescotaribeiraogakicks-assncf-ipfs3-ap-southeast-2ixboxeroxajuniperecreationirasakibigawaknoluoktachikawafflecellpagest-mon-blogueurodirumaceratagajobojibmdeuxfleurs3-ap-southeast-1337xn--trentinosd-tirol-rzbjugnishinoshimatsuurautoscanaryggeemrnotebooks-prodeobservableusercontentatarantoyokawap-southeast-6116-bambinagisobetsuldalpha-myqnapcloudaccess3-ap-northeast-2038xn--trentinosdtirol-7vbloombergentingjesdalondonetskaratsuginamikatagamimozaokinawashirosatobishimadridvagsoyereithuathienhueusc-de-east-1xn--trentinsd-tirol-6vblushakotanishiokoppegardiscoverdalondrinapolicevervaultjeldsundisharparochernihivgubarclaycards3-fips-us-gov-east-1xn--trentinsdtirol-nsbmoattachments3-website-ap-southeast-2xn--trgstad-r1axn--trna-woaxn--troms-zuaxn--tysvr-vraxn--uc0atvegaspydebergxn--uc0ay4axn--uist22hakatanorthflankazunotogawaxn--uisz3gxn--unjrga-rtarpitxn--unup4yxn--uuwu58axn--vads-jraxn--valle-aoste-ebbtxn--valle-d-aoste-ehboehringerikerxn--valleaoste-e7axn--valledaoste-ebbvadsoccerxn--vard-jraxn--vegrshei-c0axn--vermgensberater-ctb-hostingxn--vermgensberatung-pwbms3-website-eu-west-1xn--vestvgy-ixa6oxn--vg-yiabmwcloudnonproddagestangevje-og-hornnes3-website-sa-east-1xn--vgan-qoaxn--vgsy-qoa0j0xn--vgu402cleverappsangotpantheonsitexn--vhquvelvetuckerxn--vler-qoaxn--vre-eiker-k8axn--vrggt-xqadxn--vry-yla5gxn--vuq861bnrweatherchannelsdvrdns3-website-us-east-1xn--w4r85el8fhu5dnraxn--w4rs40lxn--wcvs22dxn--wgbh1clickrisinglesjaguarvodkafkashiharaxn--wgbl6axn--xhq521bolognagasakikonaircraftraeumtgeradealerdalcest-le-patron-forgerockyotobetsucks3-website-us-gov-west-1xn--xkc2al3hye2axn--xkc2dl3a5ee0hakodatexn--y9a3aquarellebesbyencowayxn--yer-znavyxn--yfro4i67oxn--ygarden-p1axn--ygbi2ammxn--4it797kontumintshizukuishimojis-a-landscaperspectakashimarshallstatebankhmelnytskyivalleedaostexn--ystre-slidre-ujbolzano-altoadigextraspace-to-rentalstomakomaibaravocats3-eu-west-2xn--zbx025dxn--zf0avxn--4pvxs4allxn--zfr164bomlodingenishitosashimizunaminamidaitomanaustdalopparachutingjovikareliancexnbayernxtooldevicexz \ No newline at end of file diff --git a/vendor/golang.org/x/net/publicsuffix/list.go b/vendor/golang.org/x/net/publicsuffix/list.go new file mode 100644 index 0000000..7ab8b3c --- /dev/null +++ b/vendor/golang.org/x/net/publicsuffix/list.go @@ -0,0 +1,210 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:generate go run gen.go + +// Package publicsuffix provides a public suffix list based on data from +// https://publicsuffix.org/ +// +// A public suffix is one under which Internet users can directly register +// names. It is related to, but different from, a TLD (top level domain). +// +// "com" is a TLD (top level domain). Top level means it has no dots. +// +// "com" is also a public suffix. Amazon and Google have registered different +// siblings under that domain: "amazon.com" and "google.com". +// +// "au" is another TLD, again because it has no dots. But it's not "amazon.au". +// Instead, it's "amazon.com.au". +// +// "com.au" isn't an actual TLD, because it's not at the top level (it has +// dots). But it is an eTLD (effective TLD), because that's the branching point +// for domain name registrars. +// +// Another name for "an eTLD" is "a public suffix". Often, what's more of +// interest is the eTLD+1, or one more label than the public suffix. For +// example, browsers partition read/write access to HTTP cookies according to +// the eTLD+1. Web pages served from "amazon.com.au" can't read cookies from +// "google.com.au", but web pages served from "maps.google.com" can share +// cookies from "www.google.com", so you don't have to sign into Google Maps +// separately from signing into Google Web Search. Note that all four of those +// domains have 3 labels and 2 dots. The first two domains are each an eTLD+1, +// the last two are not (but share the same eTLD+1: "google.com"). +// +// All of these domains have the same eTLD+1: +// - "www.books.amazon.co.uk" +// - "books.amazon.co.uk" +// - "amazon.co.uk" +// +// Specifically, the eTLD+1 is "amazon.co.uk", because the eTLD is "co.uk". +// +// There is no closed form algorithm to calculate the eTLD of a domain. +// Instead, the calculation is data driven. This package provides a +// pre-compiled snapshot of Mozilla's PSL (Public Suffix List) data at +// https://publicsuffix.org/ +package publicsuffix // import "golang.org/x/net/publicsuffix" + +// TODO: specify case sensitivity and leading/trailing dot behavior for +// func PublicSuffix and func EffectiveTLDPlusOne. + +import ( + "fmt" + "net/http/cookiejar" + "net/netip" + "strings" +) + +// List implements the cookiejar.PublicSuffixList interface by calling the +// PublicSuffix function. +var List cookiejar.PublicSuffixList = list{} + +type list struct{} + +func (list) PublicSuffix(domain string) string { + ps, _ := PublicSuffix(domain) + return ps +} + +func (list) String() string { + return version +} + +// PublicSuffix returns the public suffix of the domain using a copy of the +// publicsuffix.org database compiled into the library. +// +// icann is whether the public suffix is managed by the Internet Corporation +// for Assigned Names and Numbers. If not, the public suffix is either a +// privately managed domain (and in practice, not a top level domain) or an +// unmanaged top level domain (and not explicitly mentioned in the +// publicsuffix.org list). For example, "foo.org" and "foo.co.uk" are ICANN +// domains, "foo.dyndns.org" is a private domain and +// "cromulent" is an unmanaged top level domain. +// +// Use cases for distinguishing ICANN domains like "foo.com" from private +// domains like "foo.appspot.com" can be found at +// https://wiki.mozilla.org/Public_Suffix_List/Use_Cases +func PublicSuffix(domain string) (publicSuffix string, icann bool) { + if _, err := netip.ParseAddr(domain); err == nil { + return domain, false + } + + lo, hi := uint32(0), uint32(numTLD) + s, suffix, icannNode, wildcard := domain, len(domain), false, false +loop: + for { + dot := strings.LastIndexByte(s, '.') + if wildcard { + icann = icannNode + suffix = 1 + dot + } + if lo == hi { + break + } + f := find(s[1+dot:], lo, hi) + if f == notFound { + break + } + + u := uint32(nodes.get(f) >> (nodesBitsTextOffset + nodesBitsTextLength)) + icannNode = u&(1<<nodesBitsICANN-1) != 0 + u >>= nodesBitsICANN + u = children.get(u & (1<<nodesBitsChildren - 1)) + lo = u & (1<<childrenBitsLo - 1) + u >>= childrenBitsLo + hi = u & (1<<childrenBitsHi - 1) + u >>= childrenBitsHi + switch u & (1<<childrenBitsNodeType - 1) { + case nodeTypeNormal: + suffix = 1 + dot + case nodeTypeException: + suffix = 1 + len(s) + break loop + } + u >>= childrenBitsNodeType + wildcard = u&(1<<childrenBitsWildcard-1) != 0 + if !wildcard { + icann = icannNode + } + + if dot == -1 { + break + } + s = s[:dot] + } + if suffix == len(domain) { + // If no rules match, the prevailing rule is "*". + return domain[1+strings.LastIndexByte(domain, '.'):], icann + } + return domain[suffix:], icann +} + +const notFound uint32 = 1<<32 - 1 + +// find returns the index of the node in the range [lo, hi) whose label equals +// label, or notFound if there is no such node. The range is assumed to be in +// strictly increasing node label order. +func find(label string, lo, hi uint32) uint32 { + for lo < hi { + mid := lo + (hi-lo)/2 + s := nodeLabel(mid) + if s < label { + lo = mid + 1 + } else if s == label { + return mid + } else { + hi = mid + } + } + return notFound +} + +// nodeLabel returns the label for the i'th node. +func nodeLabel(i uint32) string { + x := nodes.get(i) + length := x & (1<<nodesBitsTextLength - 1) + x >>= nodesBitsTextLength + offset := x & (1<<nodesBitsTextOffset - 1) + return text[offset : offset+length] +} + +// EffectiveTLDPlusOne returns the effective top level domain plus one more +// label. For example, the eTLD+1 for "foo.bar.golang.org" is "golang.org". +func EffectiveTLDPlusOne(domain string) (string, error) { + if strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") || strings.Contains(domain, "..") { + return "", fmt.Errorf("publicsuffix: empty label in domain %q", domain) + } + + suffix, _ := PublicSuffix(domain) + if len(domain) <= len(suffix) { + return "", fmt.Errorf("publicsuffix: cannot derive eTLD+1 for domain %q", domain) + } + i := len(domain) - len(suffix) - 1 + if domain[i] != '.' { + return "", fmt.Errorf("publicsuffix: invalid public suffix %q for domain %q", suffix, domain) + } + return domain[1+strings.LastIndexByte(domain[:i], '.'):], nil +} + +type uint32String string + +func (u uint32String) get(i uint32) uint32 { + off := i * 4 + u = u[off:] // help the compiler reduce bounds checks + return uint32(u[3]) | + uint32(u[2])<<8 | + uint32(u[1])<<16 | + uint32(u[0])<<24 +} + +type uint40String string + +func (u uint40String) get(i uint32) uint64 { + off := uint64(i * (nodesBits / 8)) + u = u[off:] // help the compiler reduce bounds checks + return uint64(u[4]) | + uint64(u[3])<<8 | + uint64(u[2])<<16 | + uint64(u[1])<<24 | + uint64(u[0])<<32 +} diff --git a/vendor/golang.org/x/net/publicsuffix/table.go b/vendor/golang.org/x/net/publicsuffix/table.go new file mode 100644 index 0000000..a376632 --- /dev/null +++ b/vendor/golang.org/x/net/publicsuffix/table.go @@ -0,0 +1,70 @@ +// generated by go run gen.go; DO NOT EDIT + +package publicsuffix + +import _ "embed" + +const version = "publicsuffix.org's public_suffix_list.dat, git revision d6c92f1bbb7433e5db7b8405c25d4035fb8ff376 (2026-02-06T07:36:33Z)" + +const ( + nodesBits = 40 + nodesBitsChildren = 10 + nodesBitsICANN = 1 + nodesBitsTextOffset = 16 + nodesBitsTextLength = 6 + + childrenBitsWildcard = 1 + childrenBitsNodeType = 2 + childrenBitsHi = 14 + childrenBitsLo = 14 +) + +const ( + nodeTypeNormal = 0 + nodeTypeException = 1 + nodeTypeParentOnly = 2 +) + +// numTLD is the number of top level domains. +const numTLD = 1450 + +// text is the combined text of all labels. +// +//go:embed data/text +var text string + +// nodes is the list of nodes. Each node is represented as a 40-bit integer, +// which encodes the node's children, wildcard bit and node type (as an index +// into the children array), ICANN bit and text. +// +// The layout within the node, from MSB to LSB, is: +// +// [ 7 bits] unused +// [10 bits] children index +// [ 1 bits] ICANN bit +// [16 bits] text index +// [ 6 bits] text length +// +//go:embed data/nodes +var nodes uint40String + +// children is the list of nodes' children, the parent's wildcard bit and the +// parent's node type. If a node has no children then their children index +// will be in the range [0, 6), depending on the wildcard bit and node type. +// +// The layout within the uint32, from MSB to LSB, is: +// +// [ 1 bits] unused +// [ 1 bits] wildcard bit +// [ 2 bits] node type +// [14 bits] high nodes index (exclusive) of children +// [14 bits] low nodes index (inclusive) of children +// +//go:embed data/children +var children uint32String + +// max children 935 (capacity 1023) +// max text offset 32332 (capacity 65535) +// max text length 31 (capacity 63) +// max hi 10533 (capacity 16383) +// max lo 10528 (capacity 16383) diff --git a/vendor/golang.org/x/time/LICENSE b/vendor/golang.org/x/time/LICENSE new file mode 100644 index 0000000..6a66aea --- /dev/null +++ b/vendor/golang.org/x/time/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/golang.org/x/time/PATENTS b/vendor/golang.org/x/time/PATENTS new file mode 100644 index 0000000..7330990 --- /dev/null +++ b/vendor/golang.org/x/time/PATENTS @@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the Go project. + +Google hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) +patent license to make, have made, use, offer to sell, sell, import, +transfer and otherwise run, modify and propagate the contents of this +implementation of Go, where such license applies only to those patent +claims, both currently owned or controlled by Google and acquired in +the future, licensable by Google that are necessarily infringed by this +implementation of Go. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute or +order or agree to the institution of patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging +that this implementation of Go or any code incorporated within this +implementation of Go constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation of Go +shall terminate as of the date such litigation is filed. diff --git a/vendor/golang.org/x/time/rate/rate.go b/vendor/golang.org/x/time/rate/rate.go new file mode 100644 index 0000000..8f6c7f4 --- /dev/null +++ b/vendor/golang.org/x/time/rate/rate.go @@ -0,0 +1,430 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package rate provides a rate limiter. +package rate + +import ( + "context" + "fmt" + "math" + "sync" + "time" +) + +// Limit defines the maximum frequency of some events. +// Limit is represented as number of events per second. +// A zero Limit allows no events. +type Limit float64 + +// Inf is the infinite rate limit; it allows all events (even if burst is zero). +const Inf = Limit(math.MaxFloat64) + +// Every converts a minimum time interval between events to a Limit. +func Every(interval time.Duration) Limit { + if interval <= 0 { + return Inf + } + return 1 / Limit(interval.Seconds()) +} + +// A Limiter controls how frequently events are allowed to happen. +// It implements a "token bucket" of size b, initially full and refilled +// at rate r tokens per second. +// Informally, in any large enough time interval, the Limiter limits the +// rate to r tokens per second, with a maximum burst size of b events. +// As a special case, if r == Inf (the infinite rate), b is ignored. +// See https://en.wikipedia.org/wiki/Token_bucket for more about token buckets. +// +// The zero value is a valid Limiter, but it will reject all events. +// Use NewLimiter to create non-zero Limiters. +// +// Limiter has three main methods, Allow, Reserve, and Wait. +// Most callers should use Wait. +// +// Each of the three methods consumes a single token. +// They differ in their behavior when no token is available. +// If no token is available, Allow returns false. +// If no token is available, Reserve returns a reservation for a future token +// and the amount of time the caller must wait before using it. +// If no token is available, Wait blocks until one can be obtained +// or its associated context.Context is canceled. +// +// The methods AllowN, ReserveN, and WaitN consume n tokens. +// +// Limiter is safe for simultaneous use by multiple goroutines. +type Limiter struct { + mu sync.Mutex + limit Limit + burst int + tokens float64 + // last is the last time the limiter's tokens field was updated + last time.Time + // lastEvent is the latest time of a rate-limited event (past or future) + lastEvent time.Time +} + +// Limit returns the maximum overall event rate. +func (lim *Limiter) Limit() Limit { + lim.mu.Lock() + defer lim.mu.Unlock() + return lim.limit +} + +// Burst returns the maximum burst size. Burst is the maximum number of tokens +// that can be consumed in a single call to Allow, Reserve, or Wait, so higher +// Burst values allow more events to happen at once. +// A zero Burst allows no events, unless limit == Inf. +func (lim *Limiter) Burst() int { + lim.mu.Lock() + defer lim.mu.Unlock() + return lim.burst +} + +// TokensAt returns the number of tokens available at time t. +func (lim *Limiter) TokensAt(t time.Time) float64 { + lim.mu.Lock() + _, tokens := lim.advance(t) // does not mutate lim + lim.mu.Unlock() + return tokens +} + +// Tokens returns the number of tokens available now. +func (lim *Limiter) Tokens() float64 { + return lim.TokensAt(time.Now()) +} + +// NewLimiter returns a new Limiter that allows events up to rate r and permits +// bursts of at most b tokens. +func NewLimiter(r Limit, b int) *Limiter { + return &Limiter{ + limit: r, + burst: b, + } +} + +// Allow reports whether an event may happen now. +func (lim *Limiter) Allow() bool { + return lim.AllowN(time.Now(), 1) +} + +// AllowN reports whether n events may happen at time t. +// Use this method if you intend to drop / skip events that exceed the rate limit. +// Otherwise use Reserve or Wait. +func (lim *Limiter) AllowN(t time.Time, n int) bool { + return lim.reserveN(t, n, 0).ok +} + +// A Reservation holds information about events that are permitted by a Limiter to happen after a delay. +// A Reservation may be canceled, which may enable the Limiter to permit additional events. +type Reservation struct { + ok bool + lim *Limiter + tokens int + timeToAct time.Time + // This is the Limit at reservation time, it can change later. + limit Limit +} + +// OK returns whether the limiter can provide the requested number of tokens +// within the maximum wait time. If OK is false, Delay returns InfDuration, and +// Cancel does nothing. +func (r *Reservation) OK() bool { + return r.ok +} + +// Delay is shorthand for DelayFrom(time.Now()). +func (r *Reservation) Delay() time.Duration { + return r.DelayFrom(time.Now()) +} + +// InfDuration is the duration returned by Delay when a Reservation is not OK. +const InfDuration = time.Duration(math.MaxInt64) + +// DelayFrom returns the duration for which the reservation holder must wait +// before taking the reserved action. Zero duration means act immediately. +// InfDuration means the limiter cannot grant the tokens requested in this +// Reservation within the maximum wait time. +func (r *Reservation) DelayFrom(t time.Time) time.Duration { + if !r.ok { + return InfDuration + } + delay := r.timeToAct.Sub(t) + if delay < 0 { + return 0 + } + return delay +} + +// Cancel is shorthand for CancelAt(time.Now()). +func (r *Reservation) Cancel() { + r.CancelAt(time.Now()) +} + +// CancelAt indicates that the reservation holder will not perform the reserved action +// and reverses the effects of this Reservation on the rate limit as much as possible, +// considering that other reservations may have already been made. +func (r *Reservation) CancelAt(t time.Time) { + if !r.ok { + return + } + + r.lim.mu.Lock() + defer r.lim.mu.Unlock() + + if r.lim.limit == Inf || r.tokens == 0 || r.timeToAct.Before(t) { + return + } + + // calculate tokens to restore + // The duration between lim.lastEvent and r.timeToAct tells us how many tokens were reserved + // after r was obtained. These tokens should not be restored. + restoreTokens := float64(r.tokens) - r.limit.tokensFromDuration(r.lim.lastEvent.Sub(r.timeToAct)) + if restoreTokens <= 0 { + return + } + // advance time to now + t, tokens := r.lim.advance(t) + // calculate new number of tokens + tokens += restoreTokens + if burst := float64(r.lim.burst); tokens > burst { + tokens = burst + } + // update state + r.lim.last = t + r.lim.tokens = tokens + if r.timeToAct == r.lim.lastEvent { + prevEvent := r.timeToAct.Add(r.limit.durationFromTokens(float64(-r.tokens))) + if !prevEvent.Before(t) { + r.lim.lastEvent = prevEvent + } + } +} + +// Reserve is shorthand for ReserveN(time.Now(), 1). +func (lim *Limiter) Reserve() *Reservation { + return lim.ReserveN(time.Now(), 1) +} + +// ReserveN returns a Reservation that indicates how long the caller must wait before n events happen. +// The Limiter takes this Reservation into account when allowing future events. +// The returned Reservation’s OK() method returns false if n exceeds the Limiter's burst size. +// Usage example: +// +// r := lim.ReserveN(time.Now(), 1) +// if !r.OK() { +// // Not allowed to act! Did you remember to set lim.burst to be > 0 ? +// return +// } +// time.Sleep(r.Delay()) +// Act() +// +// Use this method if you wish to wait and slow down in accordance with the rate limit without dropping events. +// If you need to respect a deadline or cancel the delay, use Wait instead. +// To drop or skip events exceeding rate limit, use Allow instead. +func (lim *Limiter) ReserveN(t time.Time, n int) *Reservation { + r := lim.reserveN(t, n, InfDuration) + return &r +} + +// Wait is shorthand for WaitN(ctx, 1). +func (lim *Limiter) Wait(ctx context.Context) (err error) { + return lim.WaitN(ctx, 1) +} + +// WaitN blocks until lim permits n events to happen. +// It returns an error if n exceeds the Limiter's burst size, the Context is +// canceled, or the expected wait time exceeds the Context's Deadline. +// The burst limit is ignored if the rate limit is Inf. +func (lim *Limiter) WaitN(ctx context.Context, n int) (err error) { + // The test code calls lim.wait with a fake timer generator. + // This is the real timer generator. + newTimer := func(d time.Duration) (<-chan time.Time, func() bool, func()) { + timer := time.NewTimer(d) + return timer.C, timer.Stop, func() {} + } + + return lim.wait(ctx, n, time.Now(), newTimer) +} + +// wait is the internal implementation of WaitN. +func (lim *Limiter) wait(ctx context.Context, n int, t time.Time, newTimer func(d time.Duration) (<-chan time.Time, func() bool, func())) error { + lim.mu.Lock() + burst := lim.burst + limit := lim.limit + lim.mu.Unlock() + + if n > burst && limit != Inf { + return fmt.Errorf("rate: Wait(n=%d) exceeds limiter's burst %d", n, burst) + } + // Check if ctx is already cancelled + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + // Determine wait limit + waitLimit := InfDuration + if deadline, ok := ctx.Deadline(); ok { + waitLimit = deadline.Sub(t) + } + // Reserve + r := lim.reserveN(t, n, waitLimit) + if !r.ok { + return fmt.Errorf("rate: Wait(n=%d) would exceed context deadline", n) + } + // Wait if necessary + delay := r.DelayFrom(t) + if delay == 0 { + return nil + } + ch, stop, advance := newTimer(delay) + defer stop() + advance() // only has an effect when testing + select { + case <-ch: + // We can proceed. + return nil + case <-ctx.Done(): + // Context was canceled before we could proceed. Cancel the + // reservation, which may permit other events to proceed sooner. + r.Cancel() + return ctx.Err() + } +} + +// SetLimit is shorthand for SetLimitAt(time.Now(), newLimit). +func (lim *Limiter) SetLimit(newLimit Limit) { + lim.SetLimitAt(time.Now(), newLimit) +} + +// SetLimitAt sets a new Limit for the limiter. The new Limit, and Burst, may be violated +// or underutilized by those which reserved (using Reserve or Wait) but did not yet act +// before SetLimitAt was called. +func (lim *Limiter) SetLimitAt(t time.Time, newLimit Limit) { + lim.mu.Lock() + defer lim.mu.Unlock() + + t, tokens := lim.advance(t) + + lim.last = t + lim.tokens = tokens + lim.limit = newLimit +} + +// SetBurst is shorthand for SetBurstAt(time.Now(), newBurst). +func (lim *Limiter) SetBurst(newBurst int) { + lim.SetBurstAt(time.Now(), newBurst) +} + +// SetBurstAt sets a new burst size for the limiter. +func (lim *Limiter) SetBurstAt(t time.Time, newBurst int) { + lim.mu.Lock() + defer lim.mu.Unlock() + + t, tokens := lim.advance(t) + + lim.last = t + lim.tokens = tokens + lim.burst = newBurst +} + +// reserveN is a helper method for AllowN, ReserveN, and WaitN. +// maxFutureReserve specifies the maximum reservation wait duration allowed. +// reserveN returns Reservation, not *Reservation, to avoid allocation in AllowN and WaitN. +func (lim *Limiter) reserveN(t time.Time, n int, maxFutureReserve time.Duration) Reservation { + lim.mu.Lock() + defer lim.mu.Unlock() + + if lim.limit == Inf { + return Reservation{ + ok: true, + lim: lim, + tokens: n, + timeToAct: t, + } + } else if lim.limit == 0 { + var ok bool + if lim.burst >= n { + ok = true + lim.burst -= n + } + return Reservation{ + ok: ok, + lim: lim, + tokens: lim.burst, + timeToAct: t, + } + } + + t, tokens := lim.advance(t) + + // Calculate the remaining number of tokens resulting from the request. + tokens -= float64(n) + + // Calculate the wait duration + var waitDuration time.Duration + if tokens < 0 { + waitDuration = lim.limit.durationFromTokens(-tokens) + } + + // Decide result + ok := n <= lim.burst && waitDuration <= maxFutureReserve + + // Prepare reservation + r := Reservation{ + ok: ok, + lim: lim, + limit: lim.limit, + } + if ok { + r.tokens = n + r.timeToAct = t.Add(waitDuration) + + // Update state + lim.last = t + lim.tokens = tokens + lim.lastEvent = r.timeToAct + } + + return r +} + +// advance calculates and returns an updated state for lim resulting from the passage of time. +// lim is not changed. +// advance requires that lim.mu is held. +func (lim *Limiter) advance(t time.Time) (newT time.Time, newTokens float64) { + last := lim.last + if t.Before(last) { + last = t + } + + // Calculate the new number of tokens, due to time that passed. + elapsed := t.Sub(last) + delta := lim.limit.tokensFromDuration(elapsed) + tokens := lim.tokens + delta + if burst := float64(lim.burst); tokens > burst { + tokens = burst + } + return t, tokens +} + +// durationFromTokens is a unit conversion function from the number of tokens to the duration +// of time it takes to accumulate them at a rate of limit tokens per second. +func (limit Limit) durationFromTokens(tokens float64) time.Duration { + if limit <= 0 { + return InfDuration + } + seconds := tokens / float64(limit) + return time.Duration(float64(time.Second) * seconds) +} + +// tokensFromDuration is a unit conversion function from a time duration to the number of tokens +// which could be accumulated during that duration at a rate of limit tokens per second. +func (limit Limit) tokensFromDuration(d time.Duration) float64 { + if limit <= 0 { + return 0 + } + return d.Seconds() * float64(limit) +} diff --git a/vendor/golang.org/x/time/rate/sometimes.go b/vendor/golang.org/x/time/rate/sometimes.go new file mode 100644 index 0000000..6ba99dd --- /dev/null +++ b/vendor/golang.org/x/time/rate/sometimes.go @@ -0,0 +1,67 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package rate + +import ( + "sync" + "time" +) + +// Sometimes will perform an action occasionally. The First, Every, and +// Interval fields govern the behavior of Do, which performs the action. +// A zero Sometimes value will perform an action exactly once. +// +// # Example: logging with rate limiting +// +// var sometimes = rate.Sometimes{First: 3, Interval: 10*time.Second} +// func Spammy() { +// sometimes.Do(func() { log.Info("here I am!") }) +// } +type Sometimes struct { + First int // if non-zero, the first N calls to Do will run f. + Every int // if non-zero, every Nth call to Do will run f. + Interval time.Duration // if non-zero and Interval has elapsed since f's last run, Do will run f. + + mu sync.Mutex + count int // number of Do calls + last time.Time // last time f was run +} + +// Do runs the function f as allowed by First, Every, and Interval. +// +// The model is a union (not intersection) of filters. The first call to Do +// always runs f. Subsequent calls to Do run f if allowed by First or Every or +// Interval. +// +// A non-zero First:N causes the first N Do(f) calls to run f. +// +// A non-zero Every:M causes every Mth Do(f) call, starting with the first, to +// run f. +// +// A non-zero Interval causes Do(f) to run f if Interval has elapsed since +// Do last ran f. +// +// Specifying multiple filters produces the union of these execution streams. +// For example, specifying both First:N and Every:M causes the first N Do(f) +// calls and every Mth Do(f) call, starting with the first, to run f. See +// Examples for more. +// +// If Do is called multiple times simultaneously, the calls will block and run +// serially. Therefore, Do is intended for lightweight operations. +// +// Because a call to Do may block until f returns, if f causes Do to be called, +// it will deadlock. +func (s *Sometimes) Do(f func()) { + s.mu.Lock() + defer s.mu.Unlock() + if s.count == 0 || + (s.First > 0 && s.count < s.First) || + (s.Every > 0 && s.count%s.Every == 0) || + (s.Interval > 0 && time.Since(s.last) >= s.Interval) { + f() + s.last = time.Now() + } + s.count++ +} diff --git a/vendor/modules.txt b/vendor/modules.txt new file mode 100644 index 0000000..77eef06 --- /dev/null +++ b/vendor/modules.txt @@ -0,0 +1,21 @@ +# github.com/NicoNex/echotron/v3 v3.45.0 +## explicit; go 1.19 +github.com/NicoNex/echotron/v3 +# github.com/carlmjohnson/requests v0.25.1 +## explicit; go 1.23.0 +github.com/carlmjohnson/requests +github.com/carlmjohnson/requests/internal/minitrue +# github.com/hjson/hjson-go v3.3.0+incompatible +## explicit +github.com/hjson/hjson-go +# github.com/robfig/cron/v3 v3.0.1 +## explicit; go 1.12 +github.com/robfig/cron/v3 +# golang.org/x/net v0.51.0 +## explicit; go 1.25.0 +golang.org/x/net/html +golang.org/x/net/html/atom +golang.org/x/net/publicsuffix +# golang.org/x/time v0.5.0 +## explicit; go 1.18 +golang.org/x/time/rate