diff --git a/go.mod b/go.mod index bffed4b7..2bdedf15 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,14 @@ module github.com/eko/monday -go 1.24.0 - -toolchain go1.24.1 +go 1.24.7 require ( github.com/jroimartin/gocui v0.5.0 github.com/manifoldco/promptui v0.9.0 github.com/radovskyb/watcher v1.0.7 - github.com/spf13/cobra v1.9.1 + github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.10.0 - github.com/txn2/txeh v1.5.5 + github.com/txn2/txeh v1.8.0 go.uber.org/mock v0.5.2 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.30.3 @@ -46,7 +44,7 @@ require ( github.com/nsf/termbox-go v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/spf13/pflag v1.0.6 // indirect + github.com/spf13/pflag v1.0.9 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/x448/float16 v0.8.4 // indirect golang.org/x/net v0.38.0 // indirect diff --git a/go.sum b/go.sum index b23beed4..64939473 100644 --- a/go.sum +++ b/go.sum @@ -97,24 +97,25 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/txn2/txeh v1.5.5 h1:UN4e/lCK5HGw/gGAi2GCVrNKg0GTCUWs7gs5riaZlz4= -github.com/txn2/txeh v1.5.5/go.mod h1:qYzGG9kCzeVEI12geK4IlanHWY8X4uy/I3NcW7mk8g4= +github.com/txn2/txeh v1.8.0 h1:G1vZgom6+P/xWwU53AMOpcZgC5ni382ukcPP1TDVYHk= +github.com/txn2/txeh v1.8.0/go.mod h1:rRI3Egi3+AFmEXQjft051YdYbxeCT3nFmBLsNCZZaxM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -179,6 +180,8 @@ k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUy k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= diff --git a/vendor/github.com/spf13/cobra/.golangci.yml b/vendor/github.com/spf13/cobra/.golangci.yml index 2c8f4808..104dc244 100644 --- a/vendor/github.com/spf13/cobra/.golangci.yml +++ b/vendor/github.com/spf13/cobra/.golangci.yml @@ -12,14 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. +version: "2" + run: - deadline: 5m + timeout: 5m + +formatters: + enable: + - gofmt + - goimports linters: - disable-all: true + default: none enable: #- bodyclose - # - deadcode ! deprecated since v1.49.0; replaced by 'unused' #- depguard #- dogsled #- dupl @@ -30,28 +36,31 @@ linters: - goconst - gocritic #- gocyclo - - gofmt - - goimports - #- gomnd #- goprintffuncname - gosec - - gosimple - govet - ineffassign #- lll - misspell + #- mnd #- nakedret #- noctx - nolintlint #- rowserrcheck - #- scopelint - staticcheck - #- structcheck ! deprecated since v1.49.0; replaced by 'unused' - - stylecheck - #- typecheck - unconvert #- unparam - unused - # - varcheck ! deprecated since v1.49.0; replaced by 'unused' #- whitespace - fast: false + exclusions: + presets: + - common-false-positives + - legacy + - std-error-handling + settings: + govet: + # Disable buildtag check to allow dual build tag syntax (both //go:build and // +build). + # This is necessary for Go 1.15 compatibility since //go:build was introduced in Go 1.17. + # This can be removed once Cobra requires Go 1.17 or higher. + disable: + - buildtag diff --git a/vendor/github.com/spf13/cobra/README.md b/vendor/github.com/spf13/cobra/README.md index 71757151..8416275f 100644 --- a/vendor/github.com/spf13/cobra/README.md +++ b/vendor/github.com/spf13/cobra/README.md @@ -1,8 +1,14 @@ - -![cobra logo](https://github.com/user-attachments/assets/cbc3adf8-0dff-46e9-a88d-5e2d971c169e) +
+ +cobra-logo + +
Cobra is a library for creating powerful modern CLI applications. +Visit Cobra.dev for extensive documentation + + Cobra is used in many Go projects such as [Kubernetes](https://kubernetes.io/), [Hugo](https://gohugo.io), and [GitHub CLI](https://github.com/cli/cli) to name a few. [This list](site/content/projects_using_cobra.md) contains a more extensive list of projects using Cobra. @@ -11,6 +17,20 @@ name a few. [This list](site/content/projects_using_cobra.md) contains a more ex [![Go Reference](https://pkg.go.dev/badge/github.com/spf13/cobra.svg)](https://pkg.go.dev/github.com/spf13/cobra) [![Go Report Card](https://goreportcard.com/badge/github.com/spf13/cobra)](https://goreportcard.com/report/github.com/spf13/cobra) [![Slack](https://img.shields.io/badge/Slack-cobra-brightgreen)](https://gophers.slack.com/archives/CD3LP1199) +
+
+ Supported by: +
+
+ + Warp sponsorship + + +### [Warp, the AI terminal for devs](https://www.warp.dev/cobra) +[Try Cobra in Warp today](https://www.warp.dev/cobra)
+ +
+
# Overview diff --git a/vendor/github.com/spf13/cobra/SECURITY.md b/vendor/github.com/spf13/cobra/SECURITY.md new file mode 100644 index 00000000..54e60c28 --- /dev/null +++ b/vendor/github.com/spf13/cobra/SECURITY.md @@ -0,0 +1,105 @@ +# Security Policy + +## Reporting a Vulnerability + +The `cobra` maintainers take security issues seriously and +we appreciate your efforts to _**responsibly**_ disclose your findings. +We will make every effort to swiftly respond and address concerns. + +To report a security vulnerability: + +1. **DO NOT** create a public GitHub issue for the vulnerability! +2. **DO NOT** create a public GitHub Pull Request with a fix for the vulnerability! +3. Send an email to `cobra-security@googlegroups.com`. +4. Include the following details in your report: + - Description of the vulnerability + - Steps to reproduce + - Potential impact of the vulnerability (to your downstream project, to the Go ecosystem, etc.) + - Any potential mitigations you've already identified +5. Allow up to 7 days for an initial response. + You should receive an acknowledgment of your report and an estimated timeline for a fix. +6. (Optional) If you have a fix and would like to contribute your patch, please work + directly with the maintainers via `cobra-security@googlegroups.com` to + coordinate pushing the patch to GitHub, cutting a new release, and disclosing the change. + +## Response Process + +When a security vulnerability report is received, the `cobra` maintainers will: + +1. Confirm receipt of the vulnerability report within 7 days. +2. Assess the report to determine if it constitutes a security vulnerability. +3. If confirmed, assign the vulnerability a severity level and create a timeline for addressing it. +4. Develop and test a fix. +5. Patch the vulnerability and make a new GitHub release: the maintainers will coordinate disclosure with the reporter. +6. Create a new GitHub Security Advisory to inform the broader Go ecosystem + +## Disclosure Policy + +The `cobra` maintainers follow a coordinated disclosure process: + +1. Security vulnerabilities will be addressed as quickly as possible. +2. A CVE (Common Vulnerabilities and Exposures) identifier will be requested for significant vulnerabilities + that are within `cobra` itself. +3. Once a fix is ready, the maintainers will: + - Release a new version containing the fix. + - Update the security advisory with details about the vulnerability. + - Credit the reporter (unless they wish to remain anonymous). + - Credit the fixer (unless they wish to remain anonymous, this may be the same as the reporter). + - Announce the vulnerability through appropriate channels + (GitHub Security Advisory, mailing lists, GitHub Releases, etc.) + +## Supported Versions + +Security fixes will typically only be released for the most recent major release. + +## Upstream Security Issues + +`cobra` generally will not accept vulnerability reports that originate in upstream +dependencies. I.e., if there is a problem in Go code that `cobra` depends on, +it is best to engage that project's maintainers and owners. + +This security policy primarily pertains only to `cobra` itself but if you believe you've +identified a problem that originates in an upstream dependency and is being widely +distributed by `cobra`, please follow the disclosure procedure above: the `cobra` +maintainers will work with you to determine the severity and ecosystem impact. + +## Security Updates and CVEs + +Information about known security vulnerabilities and CVEs affecting `cobra` will +be published as GitHub Security Advisories at +https://github.com/spf13/cobra/security/advisories. + +All users are encouraged to watch the repository and upgrade promptly when +security releases are published. + +## `cobra` Security Best Practices for Users + +When using `cobra` in your CLIs, the `cobra` maintainers recommend the following: + +1. Always use the latest version of `cobra`. +2. [Use Go modules](https://go.dev/blog/using-go-modules) for dependency management. +3. Always use the latest possible version of Go. + +## Security Best Practices for Contributors + +When contributing to `cobra`: + +1. Be mindful of security implications when adding new features or modifying existing ones. +2. Be aware of `cobra`'s extremely large reach: it is used in nearly every Go CLI + (like Kubernetes, Docker, Prometheus, etc. etc.) +3. Write tests that explicitly cover edge cases and potential issues. +4. If you discover a security issue while working on `cobra`, please report it + following the process above rather than opening a public pull request or issue that + addresses the vulnerability. +5. Take personal sec-ops seriously and secure your GitHub account: use [two-factor authentication](https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa), + [sign your commits with a GPG or SSH key](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification), + etc. + +## Acknowledgments + +The `cobra` maintainers would like to thank all security researchers and +community members who help keep cobra, its users, and the entire Go ecosystem secure through responsible disclosures!! + +--- + +*This security policy is inspired by the [Open Web Application Security Project (OWASP)](https://owasp.org/) guidelines and security best practices.* diff --git a/vendor/github.com/spf13/cobra/command.go b/vendor/github.com/spf13/cobra/command.go index dbb2c298..c05fed45 100644 --- a/vendor/github.com/spf13/cobra/command.go +++ b/vendor/github.com/spf13/cobra/command.go @@ -39,7 +39,7 @@ const ( ) // FParseErrWhitelist configures Flag parse errors to be ignored -type FParseErrWhitelist flag.ParseErrorsWhitelist +type FParseErrWhitelist flag.ParseErrorsAllowlist // Group Structure to manage groups for commands type Group struct { @@ -557,7 +557,7 @@ func (c *Command) FlagErrorFunc() (f func(*Command, error) error) { } } -var minUsagePadding = 25 +const minUsagePadding = 25 // UsagePadding return padding for the usage. func (c *Command) UsagePadding() int { @@ -567,7 +567,7 @@ func (c *Command) UsagePadding() int { return c.parent.commandsMaxUseLen } -var minCommandPathPadding = 11 +const minCommandPathPadding = 11 // CommandPathPadding return padding for the command path. func (c *Command) CommandPathPadding() int { @@ -577,7 +577,7 @@ func (c *Command) CommandPathPadding() int { return c.parent.commandsMaxCommandPathLen } -var minNamePadding = 11 +const minNamePadding = 11 // NamePadding returns padding for the name. func (c *Command) NamePadding() int { @@ -1296,6 +1296,11 @@ Simply type ` + c.DisplayName() + ` help [path to command] for full details.`, c.Printf("Unknown help topic %#q\n", args) CheckErr(c.Root().Usage()) } else { + // FLow the context down to be used in help text + if cmd.ctx == nil { + cmd.ctx = c.ctx + } + cmd.InitDefaultHelpFlag() // make possible 'help' flag to be shown cmd.InitDefaultVersionFlag() // make possible 'version' flag to be shown CheckErr(cmd.Help()) @@ -1872,7 +1877,7 @@ func (c *Command) ParseFlags(args []string) error { c.mergePersistentFlags() // do it here after merging all flags and just before parse - c.Flags().ParseErrorsWhitelist = flag.ParseErrorsWhitelist(c.FParseErrWhitelist) + c.Flags().ParseErrorsAllowlist = flag.ParseErrorsAllowlist(c.FParseErrWhitelist) err := c.Flags().Parse(args) // Print warnings if they occurred (e.g. deprecated flag messages). @@ -1934,7 +1939,7 @@ type tmplFunc struct { fn func(io.Writer, interface{}) error } -var defaultUsageTemplate = `Usage:{{if .Runnable}} +const defaultUsageTemplate = `Usage:{{if .Runnable}} {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}} @@ -2020,7 +2025,7 @@ func defaultUsageFunc(w io.Writer, in interface{}) error { fmt.Fprint(w, trimRightSpace(c.InheritedFlags().FlagUsages())) } if c.HasHelpSubCommands() { - fmt.Fprintf(w, "\n\nAdditional help topcis:") + fmt.Fprintf(w, "\n\nAdditional help topics:") for _, subcmd := range c.Commands() { if subcmd.IsAdditionalHelpTopicCommand() { fmt.Fprintf(w, "\n %s %s", rpad(subcmd.CommandPath(), subcmd.CommandPathPadding()), subcmd.Short) @@ -2034,7 +2039,7 @@ func defaultUsageFunc(w io.Writer, in interface{}) error { return nil } -var defaultHelpTemplate = `{{with (or .Long .Short)}}{{. | trimTrailingWhitespaces}} +const defaultHelpTemplate = `{{with (or .Long .Short)}}{{. | trimTrailingWhitespaces}} {{end}}{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}` @@ -2056,7 +2061,7 @@ func defaultHelpFunc(w io.Writer, in interface{}) error { return nil } -var defaultVersionTemplate = `{{with .DisplayName}}{{printf "%s " .}}{{end}}{{printf "version %s" .Version}} +const defaultVersionTemplate = `{{with .DisplayName}}{{printf "%s " .}}{{end}}{{printf "version %s" .Version}} ` // defaultVersionFunc is equivalent to executing defaultVersionTemplate. The two should be changed in sync. diff --git a/vendor/github.com/spf13/cobra/completions.go b/vendor/github.com/spf13/cobra/completions.go index a1752f76..d3607c2d 100644 --- a/vendor/github.com/spf13/cobra/completions.go +++ b/vendor/github.com/spf13/cobra/completions.go @@ -115,6 +115,13 @@ type CompletionOptions struct { DisableDescriptions bool // HiddenDefaultCmd makes the default 'completion' command hidden HiddenDefaultCmd bool + // DefaultShellCompDirective sets the ShellCompDirective that is returned + // if no special directive can be determined + DefaultShellCompDirective *ShellCompDirective +} + +func (receiver *CompletionOptions) SetDefaultShellCompDirective(directive ShellCompDirective) { + receiver.DefaultShellCompDirective = &directive } // Completion is a string that can be used for completions @@ -375,7 +382,7 @@ func (c *Command) getCompletions(args []string) (*Command, []Completion, ShellCo // Error while attempting to parse flags if flagErr != nil { // If error type is flagCompError and we don't want flagCompletion we should ignore the error - if _, ok := flagErr.(*flagCompError); !(ok && !flagCompletion) { + if _, ok := flagErr.(*flagCompError); !ok || flagCompletion { return finalCmd, []Completion{}, ShellCompDirectiveDefault, flagErr } } @@ -480,6 +487,14 @@ func (c *Command) getCompletions(args []string) (*Command, []Completion, ShellCo } } else { directive = ShellCompDirectiveDefault + // check current and parent commands for a custom DefaultShellCompDirective + for cmd := finalCmd; cmd != nil; cmd = cmd.parent { + if cmd.CompletionOptions.DefaultShellCompDirective != nil { + directive = *cmd.CompletionOptions.DefaultShellCompDirective + break + } + } + if flag == nil { foundLocalNonPersistentFlag := false // If TraverseChildren is true on the root command we don't check for @@ -773,7 +788,7 @@ See each sub-command's help for details on how to use the generated script. // shell completion for it (prog __complete completion '') subCmd, cmdArgs, err := c.Find(args) if err != nil || subCmd.Name() != compCmdName && - !(subCmd.Name() == ShellCompRequestCmd && len(cmdArgs) > 1 && cmdArgs[0] == compCmdName) { + (subCmd.Name() != ShellCompRequestCmd || len(cmdArgs) <= 1 || cmdArgs[0] != compCmdName) { // The completion command is not being called or being completed so we remove it. c.RemoveCommand(completionCmd) return diff --git a/vendor/github.com/spf13/pflag/README.md b/vendor/github.com/spf13/pflag/README.md index 7eacc5bd..388c4e5e 100644 --- a/vendor/github.com/spf13/pflag/README.md +++ b/vendor/github.com/spf13/pflag/README.md @@ -284,6 +284,33 @@ func main() { } ``` +### Using pflag with go test +`pflag` does not parse the shorthand versions of go test's built-in flags (i.e., those starting with `-test.`). +For more context, see issues [#63](https://github.com/spf13/pflag/issues/63) and [#238](https://github.com/spf13/pflag/issues/238) for more details. + +For example, if you use pflag in your `TestMain` function and call `pflag.Parse()` after defining your custom flags, running a test like this: +```bash +go test /your/tests -run ^YourTest -v --your-test-pflags +``` +will result in the `-v` flag being ignored. This happens because of the way pflag handles flag parsing, skipping over go test's built-in shorthand flags. +To work around this, you can use the `ParseSkippedFlags` function, which ensures that go test's flags are parsed separately using the standard flag package. + +**Example**: You want to parse go test flags that are otherwise ignore by `pflag.Parse()` +```go +import ( + goflag "flag" + flag "github.com/spf13/pflag" +) + +var ip *int = flag.Int("flagname", 1234, "help message for flagname") + +func main() { + flag.CommandLine.AddGoFlagSet(goflag.CommandLine) + flag.ParseSkippedFlags(os.Args[1:], goflag.CommandLine) + flag.Parse() +} +``` + ## More info You can see the full reference documentation of the pflag package diff --git a/vendor/github.com/spf13/pflag/bool_func.go b/vendor/github.com/spf13/pflag/bool_func.go new file mode 100644 index 00000000..83d77afa --- /dev/null +++ b/vendor/github.com/spf13/pflag/bool_func.go @@ -0,0 +1,40 @@ +package pflag + +// -- func Value +type boolfuncValue func(string) error + +func (f boolfuncValue) Set(s string) error { return f(s) } + +func (f boolfuncValue) Type() string { return "boolfunc" } + +func (f boolfuncValue) String() string { return "" } // same behavior as stdlib 'flag' package + +func (f boolfuncValue) IsBoolFlag() bool { return true } + +// BoolFunc defines a func flag with specified name, callback function and usage string. +// +// The callback function will be called every time "--{name}" (or any form that matches the flag) is parsed +// on the command line. +func (f *FlagSet) BoolFunc(name string, usage string, fn func(string) error) { + f.BoolFuncP(name, "", usage, fn) +} + +// BoolFuncP is like BoolFunc, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) BoolFuncP(name, shorthand string, usage string, fn func(string) error) { + var val Value = boolfuncValue(fn) + flag := f.VarPF(val, name, shorthand, usage) + flag.NoOptDefVal = "true" +} + +// BoolFunc defines a func flag with specified name, callback function and usage string. +// +// The callback function will be called every time "--{name}" (or any form that matches the flag) is parsed +// on the command line. +func BoolFunc(name string, usage string, fn func(string) error) { + CommandLine.BoolFuncP(name, "", usage, fn) +} + +// BoolFuncP is like BoolFunc, but accepts a shorthand letter that can be used after a single dash. +func BoolFuncP(name, shorthand string, usage string, fn func(string) error) { + CommandLine.BoolFuncP(name, shorthand, usage, fn) +} diff --git a/vendor/github.com/spf13/pflag/count.go b/vendor/github.com/spf13/pflag/count.go index a0b2679f..d49c0143 100644 --- a/vendor/github.com/spf13/pflag/count.go +++ b/vendor/github.com/spf13/pflag/count.go @@ -85,7 +85,7 @@ func (f *FlagSet) CountP(name, shorthand string, usage string) *int { // Count defines a count flag with specified name, default value, and usage string. // The return value is the address of an int variable that stores the value of the flag. -// A count flag will add 1 to its value evey time it is found on the command line +// A count flag will add 1 to its value every time it is found on the command line func Count(name string, usage string) *int { return CommandLine.CountP(name, "", usage) } diff --git a/vendor/github.com/spf13/pflag/errors.go b/vendor/github.com/spf13/pflag/errors.go new file mode 100644 index 00000000..ff11b66b --- /dev/null +++ b/vendor/github.com/spf13/pflag/errors.go @@ -0,0 +1,149 @@ +package pflag + +import "fmt" + +// notExistErrorMessageType specifies which flavor of "flag does not exist" +// is printed by NotExistError. This allows the related errors to be grouped +// under a single NotExistError struct without making a breaking change to +// the error message text. +type notExistErrorMessageType int + +const ( + flagNotExistMessage notExistErrorMessageType = iota + flagNotDefinedMessage + flagNoSuchFlagMessage + flagUnknownFlagMessage + flagUnknownShorthandFlagMessage +) + +// NotExistError is the error returned when trying to access a flag that +// does not exist in the FlagSet. +type NotExistError struct { + name string + specifiedShorthands string + messageType notExistErrorMessageType +} + +// Error implements error. +func (e *NotExistError) Error() string { + switch e.messageType { + case flagNotExistMessage: + return fmt.Sprintf("flag %q does not exist", e.name) + + case flagNotDefinedMessage: + return fmt.Sprintf("flag accessed but not defined: %s", e.name) + + case flagNoSuchFlagMessage: + return fmt.Sprintf("no such flag -%v", e.name) + + case flagUnknownFlagMessage: + return fmt.Sprintf("unknown flag: --%s", e.name) + + case flagUnknownShorthandFlagMessage: + c := rune(e.name[0]) + return fmt.Sprintf("unknown shorthand flag: %q in -%s", c, e.specifiedShorthands) + } + + panic(fmt.Errorf("unknown flagNotExistErrorMessageType: %v", e.messageType)) +} + +// GetSpecifiedName returns the name of the flag (without dashes) as it +// appeared in the parsed arguments. +func (e *NotExistError) GetSpecifiedName() string { + return e.name +} + +// GetSpecifiedShortnames returns the group of shorthand arguments +// (without dashes) that the flag appeared within. If the flag was not in a +// shorthand group, this will return an empty string. +func (e *NotExistError) GetSpecifiedShortnames() string { + return e.specifiedShorthands +} + +// ValueRequiredError is the error returned when a flag needs an argument but +// no argument was provided. +type ValueRequiredError struct { + flag *Flag + specifiedName string + specifiedShorthands string +} + +// Error implements error. +func (e *ValueRequiredError) Error() string { + if len(e.specifiedShorthands) > 0 { + c := rune(e.specifiedName[0]) + return fmt.Sprintf("flag needs an argument: %q in -%s", c, e.specifiedShorthands) + } + + return fmt.Sprintf("flag needs an argument: --%s", e.specifiedName) +} + +// GetFlag returns the flag for which the error occurred. +func (e *ValueRequiredError) GetFlag() *Flag { + return e.flag +} + +// GetSpecifiedName returns the name of the flag (without dashes) as it +// appeared in the parsed arguments. +func (e *ValueRequiredError) GetSpecifiedName() string { + return e.specifiedName +} + +// GetSpecifiedShortnames returns the group of shorthand arguments +// (without dashes) that the flag appeared within. If the flag was not in a +// shorthand group, this will return an empty string. +func (e *ValueRequiredError) GetSpecifiedShortnames() string { + return e.specifiedShorthands +} + +// InvalidValueError is the error returned when an invalid value is used +// for a flag. +type InvalidValueError struct { + flag *Flag + value string + cause error +} + +// Error implements error. +func (e *InvalidValueError) Error() string { + flag := e.flag + var flagName string + if flag.Shorthand != "" && flag.ShorthandDeprecated == "" { + flagName = fmt.Sprintf("-%s, --%s", flag.Shorthand, flag.Name) + } else { + flagName = fmt.Sprintf("--%s", flag.Name) + } + return fmt.Sprintf("invalid argument %q for %q flag: %v", e.value, flagName, e.cause) +} + +// Unwrap implements errors.Unwrap. +func (e *InvalidValueError) Unwrap() error { + return e.cause +} + +// GetFlag returns the flag for which the error occurred. +func (e *InvalidValueError) GetFlag() *Flag { + return e.flag +} + +// GetValue returns the invalid value that was provided. +func (e *InvalidValueError) GetValue() string { + return e.value +} + +// InvalidSyntaxError is the error returned when a bad flag name is passed on +// the command line. +type InvalidSyntaxError struct { + specifiedFlag string +} + +// Error implements error. +func (e *InvalidSyntaxError) Error() string { + return fmt.Sprintf("bad flag syntax: %s", e.specifiedFlag) +} + +// GetSpecifiedName returns the exact flag (with dashes) as it +// appeared in the parsed arguments. +func (e *InvalidSyntaxError) GetSpecifiedFlag() string { + return e.specifiedFlag +} diff --git a/vendor/github.com/spf13/pflag/flag.go b/vendor/github.com/spf13/pflag/flag.go index 7c058de3..eeed1e92 100644 --- a/vendor/github.com/spf13/pflag/flag.go +++ b/vendor/github.com/spf13/pflag/flag.go @@ -27,23 +27,32 @@ unaffected. Define flags using flag.String(), Bool(), Int(), etc. This declares an integer flag, -flagname, stored in the pointer ip, with type *int. + var ip = flag.Int("flagname", 1234, "help message for flagname") + If you like, you can bind the flag to a variable using the Var() functions. + var flagvar int func init() { flag.IntVar(&flagvar, "flagname", 1234, "help message for flagname") } + Or you can create custom flags that satisfy the Value interface (with pointer receivers) and couple them to flag parsing by + flag.Var(&flagVal, "name", "help message for flagname") + For such flags, the default value is just the initial value of the variable. After all flags are defined, call + flag.Parse() + to parse the command line into the defined flags. Flags may then be used directly. If you're using the flags themselves, they are all pointers; if you bind to variables, they're values. + fmt.Println("ip has value ", *ip) fmt.Println("flagvar has value ", flagvar) @@ -54,22 +63,26 @@ The arguments are indexed from 0 through flag.NArg()-1. The pflag package also defines some new functions that are not in flag, that give one-letter shorthands for flags. You can use these by appending 'P' to the name of any function that defines a flag. + var ip = flag.IntP("flagname", "f", 1234, "help message") var flagvar bool func init() { flag.BoolVarP(&flagvar, "boolname", "b", true, "help message") } flag.VarP(&flagval, "varname", "v", "help message") + Shorthand letters can be used with single dashes on the command line. Boolean shorthand flags can be combined with other shorthand flags. Command line flag syntax: + --flag // boolean flags only --flag=x Unlike the flag package, a single dash before an option means something different than a double dash. Single dashes signify a series of shorthand letters for flags. All but the last shorthand letter must be boolean flags. + // boolean flags -f -abc @@ -124,12 +137,16 @@ const ( PanicOnError ) -// ParseErrorsWhitelist defines the parsing errors that can be ignored -type ParseErrorsWhitelist struct { +// ParseErrorsAllowlist defines the parsing errors that can be ignored +type ParseErrorsAllowlist struct { // UnknownFlags will ignore unknown flags errors and continue parsing rest of the flags UnknownFlags bool } +// DEPRECATED: please use ParseErrorsAllowlist instead +// This type will be removed in a future release +type ParseErrorsWhitelist = ParseErrorsAllowlist + // NormalizedName is a flag name that has been normalized according to rules // for the FlagSet (e.g. making '-' and '_' equivalent). type NormalizedName string @@ -145,8 +162,12 @@ type FlagSet struct { // help/usage messages. SortFlags bool - // ParseErrorsWhitelist is used to configure a whitelist of errors - ParseErrorsWhitelist ParseErrorsWhitelist + // ParseErrorsAllowlist is used to configure an allowlist of errors + ParseErrorsAllowlist ParseErrorsAllowlist + + // DEPRECATED: please use ParseErrorsAllowlist instead + // This field will be removed in a future release + ParseErrorsWhitelist ParseErrorsAllowlist name string parsed bool @@ -381,7 +402,7 @@ func (f *FlagSet) lookup(name NormalizedName) *Flag { func (f *FlagSet) getFlagType(name string, ftype string, convFunc func(sval string) (interface{}, error)) (interface{}, error) { flag := f.Lookup(name) if flag == nil { - err := fmt.Errorf("flag accessed but not defined: %s", name) + err := &NotExistError{name: name, messageType: flagNotDefinedMessage} return nil, err } @@ -411,7 +432,7 @@ func (f *FlagSet) ArgsLenAtDash() int { func (f *FlagSet) MarkDeprecated(name string, usageMessage string) error { flag := f.Lookup(name) if flag == nil { - return fmt.Errorf("flag %q does not exist", name) + return &NotExistError{name: name, messageType: flagNotExistMessage} } if usageMessage == "" { return fmt.Errorf("deprecated message for flag %q must be set", name) @@ -427,7 +448,7 @@ func (f *FlagSet) MarkDeprecated(name string, usageMessage string) error { func (f *FlagSet) MarkShorthandDeprecated(name string, usageMessage string) error { flag := f.Lookup(name) if flag == nil { - return fmt.Errorf("flag %q does not exist", name) + return &NotExistError{name: name, messageType: flagNotExistMessage} } if usageMessage == "" { return fmt.Errorf("deprecated message for flag %q must be set", name) @@ -441,7 +462,7 @@ func (f *FlagSet) MarkShorthandDeprecated(name string, usageMessage string) erro func (f *FlagSet) MarkHidden(name string) error { flag := f.Lookup(name) if flag == nil { - return fmt.Errorf("flag %q does not exist", name) + return &NotExistError{name: name, messageType: flagNotExistMessage} } flag.Hidden = true return nil @@ -464,18 +485,16 @@ func (f *FlagSet) Set(name, value string) error { normalName := f.normalizeFlagName(name) flag, ok := f.formal[normalName] if !ok { - return fmt.Errorf("no such flag -%v", name) + return &NotExistError{name: name, messageType: flagNoSuchFlagMessage} } err := flag.Value.Set(value) if err != nil { - var flagName string - if flag.Shorthand != "" && flag.ShorthandDeprecated == "" { - flagName = fmt.Sprintf("-%s, --%s", flag.Shorthand, flag.Name) - } else { - flagName = fmt.Sprintf("--%s", flag.Name) + return &InvalidValueError{ + flag: flag, + value: value, + cause: err, } - return fmt.Errorf("invalid argument %q for %q flag: %v", value, flagName, err) } if !flag.Changed { @@ -501,7 +520,7 @@ func (f *FlagSet) SetAnnotation(name, key string, values []string) error { normalName := f.normalizeFlagName(name) flag, ok := f.formal[normalName] if !ok { - return fmt.Errorf("no such flag -%v", name) + return &NotExistError{name: name, messageType: flagNoSuchFlagMessage} } if flag.Annotations == nil { flag.Annotations = map[string][]string{} @@ -538,7 +557,7 @@ func (f *FlagSet) PrintDefaults() { func (f *Flag) defaultIsZeroValue() bool { switch f.Value.(type) { case boolFlag: - return f.DefValue == "false" + return f.DefValue == "false" || f.DefValue == "" case *durationValue: // Beginning in Go 1.7, duration zero values are "0s" return f.DefValue == "0" || f.DefValue == "0s" @@ -551,7 +570,7 @@ func (f *Flag) defaultIsZeroValue() bool { case *intSliceValue, *stringSliceValue, *stringArrayValue: return f.DefValue == "[]" default: - switch f.Value.String() { + switch f.DefValue { case "false": return true case "": @@ -588,8 +607,10 @@ func UnquoteUsage(flag *Flag) (name string, usage string) { name = flag.Value.Type() switch name { - case "bool": + case "bool", "boolfunc": name = "" + case "func": + name = "value" case "float64": name = "float" case "int64": @@ -707,7 +728,7 @@ func (f *FlagSet) FlagUsagesWrapped(cols int) string { switch flag.Value.Type() { case "string": line += fmt.Sprintf("[=\"%s\"]", flag.NoOptDefVal) - case "bool": + case "bool", "boolfunc": if flag.NoOptDefVal != "true" { line += fmt.Sprintf("[=%s]", flag.NoOptDefVal) } @@ -911,12 +932,10 @@ func VarP(value Value, name, shorthand, usage string) { CommandLine.VarP(value, name, shorthand, usage) } -// failf prints to standard error a formatted error and usage message and +// fail prints an error message and usage message to standard error and // returns the error. -func (f *FlagSet) failf(format string, a ...interface{}) error { - err := fmt.Errorf(format, a...) +func (f *FlagSet) fail(err error) error { if f.errorHandling != ContinueOnError { - fmt.Fprintln(f.Output(), err) f.usage() } return err @@ -934,9 +953,9 @@ func (f *FlagSet) usage() { } } -//--unknown (args will be empty) -//--unknown --next-flag ... (args will be --next-flag ...) -//--unknown arg ... (args will be arg ...) +// --unknown (args will be empty) +// --unknown --next-flag ... (args will be --next-flag ...) +// --unknown arg ... (args will be arg ...) func stripUnknownFlagValue(args []string) []string { if len(args) == 0 { //--unknown @@ -960,7 +979,7 @@ func (f *FlagSet) parseLongArg(s string, args []string, fn parseFunc) (a []strin a = args name := s[2:] if len(name) == 0 || name[0] == '-' || name[0] == '=' { - err = f.failf("bad flag syntax: %s", s) + err = f.fail(&InvalidSyntaxError{specifiedFlag: s}) return } @@ -974,6 +993,8 @@ func (f *FlagSet) parseLongArg(s string, args []string, fn parseFunc) (a []strin f.usage() return a, ErrHelp case f.ParseErrorsWhitelist.UnknownFlags: + fallthrough + case f.ParseErrorsAllowlist.UnknownFlags: // --unknown=unknownval arg ... // we do not want to lose arg in this case if len(split) >= 2 { @@ -982,7 +1003,7 @@ func (f *FlagSet) parseLongArg(s string, args []string, fn parseFunc) (a []strin return stripUnknownFlagValue(a), nil default: - err = f.failf("unknown flag: --%s", name) + err = f.fail(&NotExistError{name: name, messageType: flagUnknownFlagMessage}) return } } @@ -1000,13 +1021,16 @@ func (f *FlagSet) parseLongArg(s string, args []string, fn parseFunc) (a []strin a = a[1:] } else { // '--flag' (arg was required) - err = f.failf("flag needs an argument: %s", s) + err = f.fail(&ValueRequiredError{ + flag: flag, + specifiedName: name, + }) return } err = fn(flag, value) if err != nil { - f.failf(err.Error()) + f.fail(err) } return } @@ -1014,7 +1038,7 @@ func (f *FlagSet) parseLongArg(s string, args []string, fn parseFunc) (a []strin func (f *FlagSet) parseSingleShortArg(shorthands string, args []string, fn parseFunc) (outShorts string, outArgs []string, err error) { outArgs = args - if strings.HasPrefix(shorthands, "test.") { + if isGotestShorthandFlag(shorthands) { return } @@ -1029,6 +1053,8 @@ func (f *FlagSet) parseSingleShortArg(shorthands string, args []string, fn parse err = ErrHelp return case f.ParseErrorsWhitelist.UnknownFlags: + fallthrough + case f.ParseErrorsAllowlist.UnknownFlags: // '-f=arg arg ...' // we do not want to lose arg in this case if len(shorthands) > 2 && shorthands[1] == '=' { @@ -1039,7 +1065,11 @@ func (f *FlagSet) parseSingleShortArg(shorthands string, args []string, fn parse outArgs = stripUnknownFlagValue(outArgs) return default: - err = f.failf("unknown shorthand flag: %q in -%s", c, shorthands) + err = f.fail(&NotExistError{ + name: string(c), + specifiedShorthands: shorthands, + messageType: flagUnknownShorthandFlagMessage, + }) return } } @@ -1062,7 +1092,11 @@ func (f *FlagSet) parseSingleShortArg(shorthands string, args []string, fn parse outArgs = args[1:] } else { // '-f' (arg was required) - err = f.failf("flag needs an argument: %q in -%s", c, shorthands) + err = f.fail(&ValueRequiredError{ + flag: flag, + specifiedName: string(c), + specifiedShorthands: shorthands, + }) return } @@ -1072,7 +1106,7 @@ func (f *FlagSet) parseSingleShortArg(shorthands string, args []string, fn parse err = fn(flag, value) if err != nil { - f.failf(err.Error()) + f.fail(err) } return } @@ -1135,12 +1169,12 @@ func (f *FlagSet) Parse(arguments []string) error { } f.parsed = true - if len(arguments) < 0 { + f.args = make([]string, 0, len(arguments)) + + if len(arguments) == 0 { return nil } - f.args = make([]string, 0, len(arguments)) - set := func(flag *Flag, value string) error { return f.Set(flag.Name, value) } @@ -1151,7 +1185,10 @@ func (f *FlagSet) Parse(arguments []string) error { case ContinueOnError: return err case ExitOnError: - fmt.Println(err) + if errors.Is(err, ErrHelp) { + os.Exit(0) + } + fmt.Fprintln(f.Output(), err) os.Exit(2) case PanicOnError: panic(err) @@ -1177,6 +1214,10 @@ func (f *FlagSet) ParseAll(arguments []string, fn func(flag *Flag, value string) case ContinueOnError: return err case ExitOnError: + if errors.Is(err, ErrHelp) { + os.Exit(0) + } + fmt.Fprintln(f.Output(), err) os.Exit(2) case PanicOnError: panic(err) diff --git a/vendor/github.com/spf13/pflag/func.go b/vendor/github.com/spf13/pflag/func.go new file mode 100644 index 00000000..9f4d88f2 --- /dev/null +++ b/vendor/github.com/spf13/pflag/func.go @@ -0,0 +1,37 @@ +package pflag + +// -- func Value +type funcValue func(string) error + +func (f funcValue) Set(s string) error { return f(s) } + +func (f funcValue) Type() string { return "func" } + +func (f funcValue) String() string { return "" } // same behavior as stdlib 'flag' package + +// Func defines a func flag with specified name, callback function and usage string. +// +// The callback function will be called every time "--{name}={value}" (or equivalent) is +// parsed on the command line, with "{value}" as an argument. +func (f *FlagSet) Func(name string, usage string, fn func(string) error) { + f.FuncP(name, "", usage, fn) +} + +// FuncP is like Func, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) FuncP(name string, shorthand string, usage string, fn func(string) error) { + var val Value = funcValue(fn) + f.VarP(val, name, shorthand, usage) +} + +// Func defines a func flag with specified name, callback function and usage string. +// +// The callback function will be called every time "--{name}={value}" (or equivalent) is +// parsed on the command line, with "{value}" as an argument. +func Func(name string, usage string, fn func(string) error) { + CommandLine.FuncP(name, "", usage, fn) +} + +// FuncP is like Func, but accepts a shorthand letter that can be used after a single dash. +func FuncP(name, shorthand string, usage string, fn func(string) error) { + CommandLine.FuncP(name, shorthand, usage, fn) +} diff --git a/vendor/github.com/spf13/pflag/golangflag.go b/vendor/github.com/spf13/pflag/golangflag.go index d3dd72b7..e62eab53 100644 --- a/vendor/github.com/spf13/pflag/golangflag.go +++ b/vendor/github.com/spf13/pflag/golangflag.go @@ -8,8 +8,18 @@ import ( goflag "flag" "reflect" "strings" + "time" ) +// go test flags prefixes +func isGotestFlag(flag string) bool { + return strings.HasPrefix(flag, "-test.") +} + +func isGotestShorthandFlag(flag string) bool { + return strings.HasPrefix(flag, "test.") +} + // flagValueWrapper implements pflag.Value around a flag.Value. The main // difference here is the addition of the Type method that returns a string // name of the type. As this is generally unknown, we approximate that with @@ -103,3 +113,49 @@ func (f *FlagSet) AddGoFlagSet(newSet *goflag.FlagSet) { } f.addedGoFlagSets = append(f.addedGoFlagSets, newSet) } + +// CopyToGoFlagSet will add all current flags to the given Go flag set. +// Deprecation remarks get copied into the usage description. +// Whenever possible, a flag gets added for which Go flags shows +// a proper type in the help message. +func (f *FlagSet) CopyToGoFlagSet(newSet *goflag.FlagSet) { + f.VisitAll(func(flag *Flag) { + usage := flag.Usage + if flag.Deprecated != "" { + usage += " (DEPRECATED: " + flag.Deprecated + ")" + } + + switch value := flag.Value.(type) { + case *stringValue: + newSet.StringVar((*string)(value), flag.Name, flag.DefValue, usage) + case *intValue: + newSet.IntVar((*int)(value), flag.Name, *(*int)(value), usage) + case *int64Value: + newSet.Int64Var((*int64)(value), flag.Name, *(*int64)(value), usage) + case *uintValue: + newSet.UintVar((*uint)(value), flag.Name, *(*uint)(value), usage) + case *uint64Value: + newSet.Uint64Var((*uint64)(value), flag.Name, *(*uint64)(value), usage) + case *durationValue: + newSet.DurationVar((*time.Duration)(value), flag.Name, *(*time.Duration)(value), usage) + case *float64Value: + newSet.Float64Var((*float64)(value), flag.Name, *(*float64)(value), usage) + default: + newSet.Var(flag.Value, flag.Name, usage) + } + }) +} + +// ParseSkippedFlags explicitly Parses go test flags (i.e. the one starting with '-test.') with goflag.Parse(), +// since by default those are skipped by pflag.Parse(). +// Typical usage example: `ParseGoTestFlags(os.Args[1:], goflag.CommandLine)` +func ParseSkippedFlags(osArgs []string, goFlagSet *goflag.FlagSet) error { + var skippedFlags []string + for _, f := range osArgs { + if isGotestFlag(f) { + skippedFlags = append(skippedFlags, f) + } + } + return goFlagSet.Parse(skippedFlags) +} + diff --git a/vendor/github.com/spf13/pflag/ipnet_slice.go b/vendor/github.com/spf13/pflag/ipnet_slice.go index 6b541aa8..c6e89da1 100644 --- a/vendor/github.com/spf13/pflag/ipnet_slice.go +++ b/vendor/github.com/spf13/pflag/ipnet_slice.go @@ -73,7 +73,7 @@ func (s *ipNetSliceValue) String() string { func ipNetSliceConv(val string) (interface{}, error) { val = strings.Trim(val, "[]") - // Emtpy string would cause a slice with one (empty) entry + // Empty string would cause a slice with one (empty) entry if len(val) == 0 { return []net.IPNet{}, nil } diff --git a/vendor/github.com/spf13/pflag/string_to_string.go b/vendor/github.com/spf13/pflag/string_to_string.go index 890a01af..1d1e3bf9 100644 --- a/vendor/github.com/spf13/pflag/string_to_string.go +++ b/vendor/github.com/spf13/pflag/string_to_string.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/csv" "fmt" + "sort" "strings" ) @@ -62,8 +63,15 @@ func (s *stringToStringValue) Type() string { } func (s *stringToStringValue) String() string { + keys := make([]string, 0, len(*s.value)) + for k := range *s.value { + keys = append(keys, k) + } + sort.Strings(keys) + records := make([]string, 0, len(*s.value)>>1) - for k, v := range *s.value { + for _, k := range keys { + v := (*s.value)[k] records = append(records, k+"="+v) } diff --git a/vendor/github.com/spf13/pflag/text.go b/vendor/github.com/spf13/pflag/text.go new file mode 100644 index 00000000..886d5a3d --- /dev/null +++ b/vendor/github.com/spf13/pflag/text.go @@ -0,0 +1,81 @@ +package pflag + +import ( + "encoding" + "fmt" + "reflect" +) + +// following is copied from go 1.23.4 flag.go +type textValue struct{ p encoding.TextUnmarshaler } + +func newTextValue(val encoding.TextMarshaler, p encoding.TextUnmarshaler) textValue { + ptrVal := reflect.ValueOf(p) + if ptrVal.Kind() != reflect.Ptr { + panic("variable value type must be a pointer") + } + defVal := reflect.ValueOf(val) + if defVal.Kind() == reflect.Ptr { + defVal = defVal.Elem() + } + if defVal.Type() != ptrVal.Type().Elem() { + panic(fmt.Sprintf("default type does not match variable type: %v != %v", defVal.Type(), ptrVal.Type().Elem())) + } + ptrVal.Elem().Set(defVal) + return textValue{p} +} + +func (v textValue) Set(s string) error { + return v.p.UnmarshalText([]byte(s)) +} + +func (v textValue) Get() interface{} { + return v.p +} + +func (v textValue) String() string { + if m, ok := v.p.(encoding.TextMarshaler); ok { + if b, err := m.MarshalText(); err == nil { + return string(b) + } + } + return "" +} + +//end of copy + +func (v textValue) Type() string { + return reflect.ValueOf(v.p).Type().Name() +} + +// GetText set out, which implements encoding.UnmarshalText, to the value of a flag with given name +func (f *FlagSet) GetText(name string, out encoding.TextUnmarshaler) error { + flag := f.Lookup(name) + if flag == nil { + return fmt.Errorf("flag accessed but not defined: %s", name) + } + if flag.Value.Type() != reflect.TypeOf(out).Name() { + return fmt.Errorf("trying to get %s value of flag of type %s", reflect.TypeOf(out).Name(), flag.Value.Type()) + } + return out.UnmarshalText([]byte(flag.Value.String())) +} + +// TextVar defines a flag with a specified name, default value, and usage string. The argument p must be a pointer to a variable that will hold the value of the flag, and p must implement encoding.TextUnmarshaler. If the flag is used, the flag value will be passed to p's UnmarshalText method. The type of the default value must be the same as the type of p. +func (f *FlagSet) TextVar(p encoding.TextUnmarshaler, name string, value encoding.TextMarshaler, usage string) { + f.VarP(newTextValue(value, p), name, "", usage) +} + +// TextVarP is like TextVar, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) TextVarP(p encoding.TextUnmarshaler, name, shorthand string, value encoding.TextMarshaler, usage string) { + f.VarP(newTextValue(value, p), name, shorthand, usage) +} + +// TextVar defines a flag with a specified name, default value, and usage string. The argument p must be a pointer to a variable that will hold the value of the flag, and p must implement encoding.TextUnmarshaler. If the flag is used, the flag value will be passed to p's UnmarshalText method. The type of the default value must be the same as the type of p. +func TextVar(p encoding.TextUnmarshaler, name string, value encoding.TextMarshaler, usage string) { + CommandLine.VarP(newTextValue(value, p), name, "", usage) +} + +// TextVarP is like TextVar, but accepts a shorthand letter that can be used after a single dash. +func TextVarP(p encoding.TextUnmarshaler, name, shorthand string, value encoding.TextMarshaler, usage string) { + CommandLine.VarP(newTextValue(value, p), name, shorthand, usage) +} diff --git a/vendor/github.com/spf13/pflag/time.go b/vendor/github.com/spf13/pflag/time.go new file mode 100644 index 00000000..3dee4247 --- /dev/null +++ b/vendor/github.com/spf13/pflag/time.go @@ -0,0 +1,124 @@ +package pflag + +import ( + "fmt" + "strings" + "time" +) + +// TimeValue adapts time.Time for use as a flag. +type timeValue struct { + *time.Time + formats []string +} + +func newTimeValue(val time.Time, p *time.Time, formats []string) *timeValue { + *p = val + return &timeValue{ + Time: p, + formats: formats, + } +} + +// Set time.Time value from string based on accepted formats. +func (d *timeValue) Set(s string) error { + s = strings.TrimSpace(s) + for _, f := range d.formats { + v, err := time.Parse(f, s) + if err != nil { + continue + } + *d.Time = v + return nil + } + + formatsString := "" + for i, f := range d.formats { + if i > 0 { + formatsString += ", " + } + formatsString += fmt.Sprintf("`%s`", f) + } + + return fmt.Errorf("invalid time format `%s` must be one of: %s", s, formatsString) +} + +// Type name for time.Time flags. +func (d *timeValue) Type() string { + return "time" +} + +func (d *timeValue) String() string { + if d.Time.IsZero() { + return "" + } else { + return d.Time.Format(time.RFC3339Nano) + } +} + +// GetTime return the time value of a flag with the given name +func (f *FlagSet) GetTime(name string) (time.Time, error) { + flag := f.Lookup(name) + if flag == nil { + err := fmt.Errorf("flag accessed but not defined: %s", name) + return time.Time{}, err + } + + if flag.Value.Type() != "time" { + err := fmt.Errorf("trying to get %s value of flag of type %s", "time", flag.Value.Type()) + return time.Time{}, err + } + + val, ok := flag.Value.(*timeValue) + if !ok { + return time.Time{}, fmt.Errorf("value %s is not a time", flag.Value) + } + + return *val.Time, nil +} + +// TimeVar defines a time.Time flag with specified name, default value, and usage string. +// The argument p points to a time.Time variable in which to store the value of the flag. +func (f *FlagSet) TimeVar(p *time.Time, name string, value time.Time, formats []string, usage string) { + f.TimeVarP(p, name, "", value, formats, usage) +} + +// TimeVarP is like TimeVar, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) TimeVarP(p *time.Time, name, shorthand string, value time.Time, formats []string, usage string) { + f.VarP(newTimeValue(value, p, formats), name, shorthand, usage) +} + +// TimeVar defines a time.Time flag with specified name, default value, and usage string. +// The argument p points to a time.Time variable in which to store the value of the flag. +func TimeVar(p *time.Time, name string, value time.Time, formats []string, usage string) { + CommandLine.TimeVarP(p, name, "", value, formats, usage) +} + +// TimeVarP is like TimeVar, but accepts a shorthand letter that can be used after a single dash. +func TimeVarP(p *time.Time, name, shorthand string, value time.Time, formats []string, usage string) { + CommandLine.VarP(newTimeValue(value, p, formats), name, shorthand, usage) +} + +// Time defines a time.Time flag with specified name, default value, and usage string. +// The return value is the address of a time.Time variable that stores the value of the flag. +func (f *FlagSet) Time(name string, value time.Time, formats []string, usage string) *time.Time { + return f.TimeP(name, "", value, formats, usage) +} + +// TimeP is like Time, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) TimeP(name, shorthand string, value time.Time, formats []string, usage string) *time.Time { + p := new(time.Time) + f.TimeVarP(p, name, shorthand, value, formats, usage) + return p +} + +// Time defines a time.Time flag with specified name, default value, and usage string. +// The return value is the address of a time.Time variable that stores the value of the flag. +func Time(name string, value time.Time, formats []string, usage string) *time.Time { + return CommandLine.TimeP(name, "", value, formats, usage) +} + +// TimeP is like Time, but accepts a shorthand letter that can be used after a single dash. +func TimeP(name, shorthand string, value time.Time, formats []string, usage string) *time.Time { + return CommandLine.TimeP(name, shorthand, value, formats, usage) +} diff --git a/vendor/github.com/txn2/txeh/.gitignore b/vendor/github.com/txn2/txeh/.gitignore index ac0b5296..5ecf7646 100644 --- a/vendor/github.com/txn2/txeh/.gitignore +++ b/vendor/github.com/txn2/txeh/.gitignore @@ -1,2 +1,37 @@ -.idea -dist \ No newline at end of file +# IDE +.idea/ +.vscode/ +*.swp +*~ + +# OS +.DS_Store +Thumbs.db + +# Build artifacts +dist/ +coverage.out +coverage.html + +# Secrets and credentials +.env +.env.* +*.pem +*.key +credentials.json +secrets.yaml +secrets.yml + +# Project-specific +.claude +RELEASE_* +NOTES_* + +# MkDocs build output +site/ + +# Python (MkDocs dependencies) +__pycache__/ +*.pyc +venv/ +.venv/ diff --git a/vendor/github.com/txn2/txeh/.golangci.yml b/vendor/github.com/txn2/txeh/.golangci.yml new file mode 100644 index 00000000..eb558031 --- /dev/null +++ b/vendor/github.com/txn2/txeh/.golangci.yml @@ -0,0 +1,207 @@ +version: "2" + +run: + timeout: 10m + modules-download-mode: readonly + +linters: + default: none + enable: + # ===== Bug Detection ===== + - errcheck # Check error returns are handled + - govet # Official Go vet checks + - staticcheck # Comprehensive static analysis + - ineffassign # Detect ineffective assignments + - unused # Find unused code + - bodyclose # Ensure HTTP response bodies are closed + - noctx # Check context usage in HTTP requests + - nilerr # Check nil errors are returned properly + - nilnil # Check for nil return with nil error + - contextcheck # Check context propagation + - durationcheck # Detect duration multiplication issues + - makezero # Find slice append issues + - wastedassign # Find wasted assignments + - copyloopvar # Loop variable copying + + # ===== Code Style ===== + - misspell # Spelling mistakes + - unconvert # Unnecessary type conversions + - whitespace # Whitespace issues + - godot # Comment punctuation enforcement + - revive # Comprehensive style/safety rules + + # ===== Code Quality ===== + - gocritic # Comprehensive checks + - gocyclo # Cyclomatic complexity + - gocognit # Cognitive complexity + - nakedret # Naked returns in long functions + - prealloc # Slice preallocation suggestions + - predeclared # Shadowing predeclared identifiers + - nestif # Deeply nested ifs + - unparam # Unused parameters + - errname # Error naming conventions + - errorlint # Error wrapping issues + - goconst # Detect repeated string literals + - thelper # Test helper validation + - modernize # Go modernization suggestions + + # ===== Security ===== + - gosec # Security scanning (OWASP) + + # ===== Error Handling ===== + - wrapcheck # Ensure external package errors are wrapped + + settings: + errcheck: + check-type-assertions: true + + gocyclo: + min-complexity: 10 + + gocognit: + min-complexity: 15 + + govet: + disable: + - fieldalignment + - shadow + + misspell: + locale: US + + nakedret: + max-func-lines: 30 + + nestif: + min-complexity: 5 + + prealloc: + simple: true + range-loops: true + for-loops: true + + gocritic: + enabled-tags: + - diagnostic + - performance + - style + - opinionated + disabled-checks: + - hugeParam + - rangeValCopy + + errorlint: + errorf: true + errorf-multi: true + asserts: true + comparison: true + + gosec: + excludes: + - G104 + config: + G306: "0644" + + goconst: + min-len: 3 + min-occurrences: 3 + + wrapcheck: + ignore-sigs: + - ".Errorf(" + - "errors.New(" + - "errors.Unwrap(" + - "errors.Join(" + - ".Wrap(" + - ".Wrapf(" + - ".WithMessage(" + - ".WithMessagef(" + - ".WithStack(" + + revive: + rules: + # Style + - name: blank-imports + - name: context-as-argument + - name: dot-imports + - name: error-return + - name: error-strings + - name: error-naming + - name: exported + arguments: + - "checkPrivateReceivers" + - name: increment-decrement + - name: var-naming + - name: var-declaration + - name: package-comments + - name: range + - name: receiver-naming + - name: indent-error-flow + - name: superfluous-else + - name: modifies-parameter + - name: unreachable-code + - name: modifies-value-receiver + - name: constant-logical-expr + - name: bool-literal-in-expr + - name: redefines-builtin-id + - name: range-val-in-closure + - name: range-val-address + - name: waitgroup-by-value + - name: atomic + - name: call-to-gc + - name: duplicated-imports + - name: string-of-int + - name: defer + arguments: + - ["call-chain", "loop", "method-call", "recover", "immediate-recover", "return"] + - name: unconditional-recursion + - name: identical-branches + - name: unexported-naming + - name: unnecessary-stmt + - name: useless-break + - name: context-keys-type + - name: time-equal + - name: time-naming + - name: errorf + - name: empty-block + - name: get-return + - name: early-return + - name: unused-parameter + disabled: true + - name: confusing-naming + - name: confusing-results + - name: argument-limit + arguments: + - 5 + + exclusions: + generated: lax + warn-unused: true + rules: + # Allow longer functions in cmd/ + - path: cmd/ + linters: + - gocyclo + - gocognit + - wrapcheck + + # Generated files + - path: generated + linters: + - all + +formatters: + enable: + - gofmt + - goimports + - gofumpt + exclusions: + generated: lax + settings: + goimports: + local-prefixes: + - github.com/txn2/txeh + +issues: + max-issues-per-linter: 50 + max-same-issues: 10 diff --git a/vendor/github.com/txn2/txeh/.goreleaser.yml b/vendor/github.com/txn2/txeh/.goreleaser.yml index 73011b7e..f0df9b7d 100644 --- a/vendor/github.com/txn2/txeh/.goreleaser.yml +++ b/vendor/github.com/txn2/txeh/.goreleaser.yml @@ -1,94 +1,128 @@ -# Build customization +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj + +version: 2 + +env: + - GO111MODULE=on + +before: + hooks: + - go mod download + - go mod verify + builds: - id: txeh - # Path to main.go file. - # Default is `main.go` main: ./txeh/txeh.go binary: txeh - - env: - - CGO_ENABLED=0 - - # GOOS list to build in. - # For more info refer to https://golang.org/doc/install/source#environment - # Defaults are darwin and linux goos: - - linux - - darwin - - windows - - # GOARCH to build in. - # For more info refer to https://golang.org/doc/install/source#environment - # Defaults are 386 and amd64 + - linux + - darwin + - windows goarch: - - amd64 - - arm - - arm64 - - goarm: - - "6" - - "7" + - "386" + - amd64 + - arm + - arm64 + ignore: + - goos: windows + goarch: arm64 + - goos: windows + goarch: arm + mod_timestamp: '{{ .CommitTimestamp }}' + env: + - CGO_ENABLED=0 + flags: + - -trimpath + - -tags=netgo + - -a + - -v + ldflags: -s -w -X github.com/txn2/txeh/txeh/cmd.Version={{.Version}} - ldflags: "-s -w -X github.com/txn2/txeh/txeh/cmd.Version={{.Version}}" +archives: + - id: default + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + format_overrides: + - goos: windows + formats: [zip] checksum: name_template: '{{ .ProjectName }}_checksums.txt' -release: - github: - owner: txn2 - name: txeh - name_template: "{{.ProjectName}}-v{{.Version}} {{.Env.USER}}" +signs: + - cmd: cosign + args: + - sign-blob + - '--bundle=${signature}' + - '${artifact}' + - '--yes' + artifacts: checksum + output: true + signature: '${artifact}.bundle' - # You can disable this pipe in order to not upload any artifacts to - # GitHub. - # Defaults to false. - disable: false +sboms: + - artifacts: archive + +changelog: + sort: asc + groups: + - title: Features + regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$' + order: 0 + - title: Bug Fixes + regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$' + order: 1 + - title: Security + regexp: '^.*?sec(\([[:word:]]+\))??!?:.+$' + order: 2 + - title: Others + order: 999 + filters: + exclude: + - '^docs:' + - '^test:' + - '^chore:' + - Merge pull request + - Merge branch + - go mod tidy nfpms: - - file_name_template: '{{ .ProjectName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' - homepage: https://github.com/txn2/txeh + - id: default + file_name_template: '{{ .ProjectName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' + homepage: https://github.com/txn2/txeh description: Etc Hosts Management Utility. - maintainer: Craig Johnston + maintainer: Craig Johnston license: Apache 2.0 - vendor: https://github.com/txn2 + vendor: txn2 formats: - apk - deb - rpm - -archives: - - name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' - replacements: - darwin: Darwin - linux: Linux - windows: Windows - 386: i386 - amd64: x86_64 - format_overrides: - - goos: windows - format: zip +release: + github: + owner: txn2 + name: txeh + name_template: "{{.ProjectName}}-v{{.Version}}" brews: - name: txeh - tap: + repository: owner: txn2 name: homebrew-tap + token: '{{ .Env.HOMEBREW_TAP_TOKEN }}' commit_author: name: Craig Johnston - email: cjimti@gmail.com - folder: Formula + email: cj@imti.co + directory: Formula homepage: https://github.com/txn2/txeh description: "Etc Hosts Management Utility." - skip_upload: false - test: |- - txeh version - -snapcrafts: - - name_template: '{{ .ProjectName }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' - summary: Etc Hosts Management Utility. - description: | - Kubernetes bulk port forwarding utility. - grade: stable - confinement: classic + license: "Apache-2.0" + test: | + system "#{bin}/txeh version" diff --git a/vendor/github.com/txn2/txeh/.pre-commit-config.yaml b/vendor/github.com/txn2/txeh/.pre-commit-config.yaml new file mode 100644 index 00000000..dd3cbaf8 --- /dev/null +++ b/vendor/github.com/txn2/txeh/.pre-commit-config.yaml @@ -0,0 +1,30 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-merge-conflict + - id: detect-private-key + - id: end-of-file-fixer + - id: trailing-whitespace + - id: check-yaml + - id: check-added-large-files + args: ['--maxkb=500'] + + - repo: https://github.com/gitleaks/gitleaks + rev: v8.24.2 + hooks: + - id: gitleaks + + - repo: https://github.com/compilerla/conventional-pre-commit + rev: v4.1.0 + hooks: + - id: conventional-pre-commit + stages: [commit-msg] + args: [feat, fix, docs, style, refactor, perf, test, chore, ci, security] + + - repo: https://github.com/dnephin/pre-commit-golang + rev: v0.5.1 + hooks: + - id: go-fmt + - id: go-vet + - id: go-mod-tidy diff --git a/vendor/github.com/txn2/txeh/CLAUDE.md b/vendor/github.com/txn2/txeh/CLAUDE.md new file mode 100644 index 00000000..8ac603b9 --- /dev/null +++ b/vendor/github.com/txn2/txeh/CLAUDE.md @@ -0,0 +1,255 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +txeh is a Go library and CLI utility for managing /etc/hosts file entries. It provides programmatic and command-line access to add, remove, and query hostname-to-IP mappings. Originally built to support [kubefwd](https://github.com/txn2/kubefwd) for Kubernetes port-forwarding. + +**Key Features:** +- Thread-safe operations (mutex-protected for concurrent use) +- IPv4 and IPv6 support +- CIDR range operations for bulk add/remove +- Inline comment support for tracking entry sources +- Preserves comments and file formatting +- Cross-platform (Linux, macOS, Windows) + +## Quick Reference + +```bash +# Full verification (run before every commit) +make verify # fmt, lint, test-unit, security, coverage-check, go mod verify + +# Extended verification (adds dead-code analysis) +make verify-full # fmt, lint, test, security, coverage-check, dead-code, go mod verify + +# Format, lint, test individually +make fmt # gofmt + goimports +make lint # golangci-lint (37 linters, no test relaxations) +make test-unit # go test -race ./... +make test-short # Fast tests (no race detection) +make coverage # Coverage report (HTML + summary) +make coverage-check # Enforce 80% coverage threshold +make security # gosec + govulncheck +make mutate # Mutation testing (gremlins, 60% efficacy threshold) +``` + +## Architecture + +``` +┌─────────────────┐ +│ txeh/txeh.go │ CLI entry point (Cobra-based) +│ (cmd/) │ +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ +│ txeh.go │ Core library (public API) +│ (package) │ +└─────────────────┘ + │ + ▼ +┌─────────────────┐ +│ /etc/hosts │ System hosts file +└─────────────────┘ +``` + +### Key Components + +1. **Library** (`txeh.go`): Core functionality for parsing, modifying, and rendering hosts files + - `Hosts` struct: Thread-safe wrapper with mutex protection + - `HostFileLine`: Represents a single line (ADDRESS, COMMENT, EMPTY, UNKNOWN) + - `HostsConfig`: Configuration for read/write paths or raw text input + +2. **CLI** (`txeh/` directory): Cobra-based command-line interface + - Entry point: `txeh/txeh.go` + - Commands in `txeh/cmd/`: add, remove, list, show, version + - Global flags: `--dryrun`, `--quiet`, `--read`, `--write`, `--max-hosts-per-line` + +## Code Standards + +### Style +- Follow [Google Go Style Guide](https://google.github.io/styleguide/go/) +- All code must pass `golangci-lint` with project config +- Maximum cyclomatic complexity: 10 per function +- Maximum cognitive complexity: 15 per function +- Maximum function arguments: 5 + +### Documentation +- All exported functions, types, and packages require doc comments +- Doc comments must start with the name being documented +- Comments should end with a period + +### Error Handling +- Always check and handle errors (no `_` for errors) +- Wrap errors at boundaries: `fmt.Errorf("context: %w", err)` +- Use `errors.Is` and `errors.As`, never string comparison +- Define sentinel errors for expected conditions + +```go +// Example error handling pattern +func (h *Hosts) Save() error { + if err := h.writeFile(); err != nil { + return fmt.Errorf("save hosts file: %w", err) + } + return nil +} +``` + +### Testing +- Test coverage minimum: 80% (CI enforced) +- Use table-driven tests for multiple cases +- Use `t.Parallel()` for independent tests +- Race detection required: `go test -race ./...` +- Property-based tests with `rapid` for invariant verification + +### Verification Stack + +All changes go through a 4-level verification process (see [AI-Verified Development](https://imti.co/ai-verified-development/)): + +1. **Static analysis** - `make lint` runs golangci-lint with 37 linters. Runs in seconds, catches type errors, complexity violations, security anti-patterns. +2. **Unit tests** - `make test-unit` runs with `-race`. Coverage gate at 80%. `make coverage-check` enforces this locally. +3. **Integration/E2E tests** - Tagged test suites (`-tags=integration`, `-tags=e2e`) for system-level validation. +4. **Mutation testing** - `make mutate` runs gremlins with a 60% efficacy threshold. Validates that tests catch real bugs, not just exercise code. + +Do not review AI code until every level passes. `make verify` runs levels 1-2 plus security. `make verify-full` adds dead-code analysis. + +### Acceptance Criteria + +Define specs as Given/When/Then assertions before writing code. Each "then" becomes a concrete test with a hardcoded expected value. + +``` +Given a hosts file with "127.0.0.1 myhost" +When RemoveHost("myhost") is called +Then ListHostsByIP("127.0.0.1") returns an empty list +``` + +Write the test first, confirm it fails, then implement. + +### AI-Specific Rules + +- **No tautological tests.** Tests must encode specific expected outputs as hardcoded values. Never reimplement function logic inside assertions. +- **No hallucinated imports.** Verify every dependency exists and is actively maintained before adding it. +- **Human review required.** A human must review and approve every line of code before commit. +- **Non-obvious decisions require comments.** If a choice isn't self-evident, explain why in a comment or commit message. +- **Dead code detection.** Run `make dead-code` to remove untested utilities. Unreachable code is a sign of tautological tests. + +## Security Requirements + +- Never log secrets, tokens, or passwords +- Validate all inputs at system boundaries +- File paths are sanitized (hosts file manager reads arbitrary paths by design) +- Run `gosec ./...` before commits + +## Dependencies + +- **Prefer stdlib** over external packages +- All dependencies pinned to exact versions +- Run `go mod verify` before committing +- Regular `govulncheck ./...` for vulnerability scanning + +## Project-Specific Patterns + +### Adding Hosts +```go +// Always use the thread-safe methods +hosts.AddHost("127.0.0.1", "myhost") +hosts.AddHosts("127.0.0.1", []string{"host1", "host2"}) +hosts.AddHostWithComment("127.0.0.1", "myhost", "added by myapp") +``` + +### Removing Entries +```go +hosts.RemoveHost("myhost") +hosts.RemoveAddress("127.0.0.1") +hosts.RemoveCIDRs([]string{"10.0.0.0/8"}) // Returns error +hosts.RemoveByComment("added by myapp") +``` + +### Querying +```go +ips := hosts.ListAddressesByHost("myhost", true) // true = lookup all +hostnames := hosts.ListHostsByIP("127.0.0.1") +entries := hosts.ListHostsByComment("myapp") +``` + +## Common Commands + +```bash +# Development +make build # Build CLI binary to txeh/dist/ +make test-unit # Run unit tests with race detection +make lint # Run golangci-lint +make lint-fix # Run golangci-lint with auto-fix +make fmt # Format code (gofmt + goimports) + +# Before committing +make verify # fmt, lint, test-unit, security, coverage-check, go mod verify + +# Extended verification +make verify-full # fmt, lint, test, security, coverage-check, dead-code, go mod verify + +# Testing +make test-integration # Integration tests (tagged) +make test-e2e # E2E tests (tagged, requires Docker) +make coverage # Generate coverage report (HTML + summary) + +# Code quality +make security # gosec + govulncheck +make dead-code # Check for unreachable code (deadcode) +make check # go vet + staticcheck + golangci-lint +make mutate # Mutation testing (gremlins, threshold 60%) + +# Debugging +go test -v -run TestSpecific ./... # Run specific test + +# Build release (goreleaser v2) +goreleaser release --snapshot --clean +``` + +## File Naming Conventions + +| Pattern | Purpose | +|---------|---------| +| `*_test.go` | Test files (same package) | +| `txeh/cmd/*.go` | CLI command implementations | +| `test/integration/` | Integration tests (`-tags=integration`) | +| `test/e2e/` | End-to-end tests (`-tags=e2e`) | +| `GNUmakefile` | Build automation (use `make`) | + +## Git Commit Guidelines + +Use conventional commits: +- `feat:` New features +- `fix:` Bug fixes +- `docs:` Documentation changes +- `test:` Test additions/changes +- `refactor:` Code refactoring +- `perf:` Performance improvements +- `security:` Security fixes +- `chore:` Maintenance tasks + +**Important:** A human must review and approve every line of code before commit. + +## Troubleshooting + +### Common Issues + +**"package not found" errors:** +```bash +go mod tidy +go mod verify +``` + +**Linter failures:** +```bash +golangci-lint run --fix ./... # Auto-fix where possible +``` + +**Permission denied on /etc/hosts:** +```bash +sudo txeh add 127.0.0.1 myhost # Requires root/admin +``` + +**Windows hosts file location:** +The library auto-detects via `%SystemRoot%\System32\drivers\etc\hosts` diff --git a/vendor/github.com/txn2/txeh/CODE_OF_CONDUCT.md b/vendor/github.com/txn2/txeh/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..357fdcfb --- /dev/null +++ b/vendor/github.com/txn2/txeh/CODE_OF_CONDUCT.md @@ -0,0 +1,19 @@ +# Contributor Covenant Code of Conduct + +This project has adopted the [Contributor Covenant](https://www.contributor-covenant.org/) version 2.1 as its code of conduct. + +The full text is available at: https://www.contributor-covenant.org/version/2/1/code_of_conduct/ + +## Summary + +We are committed to providing a friendly, safe, and welcoming environment for all contributors, regardless of background or identity. + +## Reporting + +Instances of unacceptable behavior may be reported to the project maintainer at cj@imti.co. + +All complaints will be reviewed and investigated promptly and fairly. + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 2.1. diff --git a/vendor/github.com/txn2/txeh/CONTRIBUTING.md b/vendor/github.com/txn2/txeh/CONTRIBUTING.md new file mode 100644 index 00000000..dcbfeab0 --- /dev/null +++ b/vendor/github.com/txn2/txeh/CONTRIBUTING.md @@ -0,0 +1,129 @@ +# Contributing to txeh + +Thank you for your interest in contributing to txeh! + +## Development Setup + +### Prerequisites + +1. **Go 1.21+** required +2. Install development tools (pin to specific versions for reproducibility): + +```bash +# Linting +go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.56.2 + +# Security scanning +go install github.com/securego/gosec/v2/cmd/gosec@v2.19.0 +go install golang.org/x/vuln/cmd/govulncheck@v1.0.4 + +# Import organization +go install golang.org/x/tools/cmd/goimports@v0.18.0 +``` + +### Clone and Build + +```bash +git clone https://github.com/txn2/txeh.git +cd txeh +make deps +make build +``` + +## Pull Request Process + +1. Fork the repository +2. Create a feature branch: `git checkout -b feat/your-feature` +3. Make changes with tests +4. Run `make verify` before committing +5. Submit PR with description using the PR template + +## Code Style + +### Formatting +- Run `make fmt` before committing +- Code must pass `golangci-lint` with project config + +### Documentation +- All exported functions need doc comments +- Doc comments must start with the name being documented +- Comments should end with a period + +### Error Handling +- Always check and handle errors (no `_` for errors) +- Wrap errors at boundaries: `fmt.Errorf("context: %w", err)` +- Use `errors.Is` and `errors.As`, never string comparison + +### Naming +- Follow [Effective Go](https://go.dev/doc/effective_go) naming conventions +- Error messages should be lowercase, no punctuation +- Avoid stuttering (e.g., `http.HTTPServer` → `http.Server`) + +## Commit Messages + +Use conventional commits: +- `feat:` New features +- `fix:` Bug fixes +- `docs:` Documentation changes +- `test:` Test additions/changes +- `refactor:` Code refactoring +- `perf:` Performance improvements +- `security:` Security fixes +- `chore:` Maintenance tasks + +Examples: +``` +feat: add support for IPv6 CIDR ranges +fix: handle empty hostname in AddHost +docs: update README with new CLI flags +test: add coverage for RemoveAddress edge cases +``` + +## Testing + +### Requirements +- Add tests for new functionality +- Maintain 82%+ coverage +- Run `go test -race ./...` to check for race conditions + +### Running Tests + +```bash +# Run all tests +make test + +# Run tests with coverage +make coverage + +# Run linting +make lint + +# Run full verification suite +make verify + +# Run specific test +go test -v -run TestSpecific ./... +``` + +## Verification Checklist + +Before submitting a PR, ensure: + +- [ ] `make verify` passes +- [ ] New code has tests +- [ ] Documentation updated if needed +- [ ] Commit messages follow conventions + +## Reporting Issues + +- Check existing issues before creating a new one +- Use the issue templates provided +- Include version, OS, and reproduction steps + +## Security Vulnerabilities + +Please report security vulnerabilities privately. See [SECURITY.md](SECURITY.md) for details. + +## License + +By contributing, you agree that your contributions will be licensed under the Apache License 2.0. diff --git a/vendor/github.com/txn2/txeh/GNUmakefile b/vendor/github.com/txn2/txeh/GNUmakefile new file mode 100644 index 00000000..39fa68f2 --- /dev/null +++ b/vendor/github.com/txn2/txeh/GNUmakefile @@ -0,0 +1,162 @@ +APP_NAME := txeh +MODULE := $(shell go list -m) +BRANCH_NAME := $(shell git rev-parse --abbrev-ref HEAD) +TAG ?= $(BRANCH_NAME) +IMAGE_NAME := $(APP_NAME):$(TAG) + +.PHONY: help +help: + @echo "Usage: make " + @echo "" + @echo "Available targets:" + @echo " build - Build the Go project" + @echo " test - Run unit and integration tests" + @echo " test-unit - Run unit tests only" + @echo " test-short - Run fast tests (no race detection)" + @echo " test-integration - Run integration tests" + @echo " test-e2e - Run E2E tests (requires Docker)" + @echo " lint - Run linters" + @echo " lint-fix - Run linters with auto-fix" + @echo " fmt - Format code" + @echo " coverage - Generate coverage report" + @echo " coverage-check - Enforce 80% coverage threshold" + @echo " security - Run security scans" + @echo " verify - Full verification (fmt, lint, test, security, coverage gate)" + @echo " verify-full - Extended verification (adds dead-code analysis)" + @echo " deps - Download and verify dependencies" + @echo " update - Update dependencies" + @echo " image - Build Docker image" + @echo " clean - Clean build artifacts" + @echo " dead-code - Check for unreachable code" + @echo " mutate - Run mutation testing" + @echo " all - Run verify and build" + @echo " help - Show this help message" + @echo "" + +.PHONY: default +default: help + +.PHONY: build +build: + go build -C txeh/ -ldflags='-s -w -X github.com/txn2/txeh/txeh/cmd.Version={{.Version}}' -trimpath -v -o dist/$(APP_NAME) + +.PHONY: test +test: test-unit + +.PHONY: test-unit +test-unit: + go test -race -v ./... + +.PHONY: test-short +test-short: + go test -short -v ./... + +.PHONY: test-integration +test-integration: + go test -race -v -tags=integration ./test/integration/... + +.PHONY: test-e2e +test-e2e: + go test -race -v -tags=e2e ./test/e2e/... + +.PHONY: lint +lint: + golangci-lint run ./... + +.PHONY: lint-fix +lint-fix: + golangci-lint run --fix ./... + +.PHONY: fmt +fmt: + gofmt -s -w . + goimports -w -local $(MODULE) . + +.PHONY: coverage +coverage: + go test -race -coverprofile=coverage.out ./... + go tool cover -html=coverage.out -o coverage.html + @go tool cover -func=coverage.out | grep total + +.PHONY: coverage-check +coverage-check: + @go test -race -coverprofile=coverage.out ./... > /dev/null 2>&1 + @COVERAGE=$$(go tool cover -func=coverage.out | grep total | awk '{print $$3}' | tr -d '%'); \ + echo "Coverage: $${COVERAGE}%"; \ + if [ $$(echo "$${COVERAGE} < 80" | bc -l) -eq 1 ]; then \ + echo "FAIL: Coverage $${COVERAGE}% is below 80% threshold"; \ + exit 1; \ + fi; \ + echo "OK: Coverage meets 80% threshold" + +.PHONY: security +security: + gosec ./... + govulncheck ./... + +.PHONY: verify +verify: fmt lint test-unit security coverage-check + go mod verify + @echo "All checks passed" + +.PHONY: verify-full +verify-full: fmt lint test security coverage-check dead-code + go mod verify + @echo "All checks (including integration and dead-code) passed" + +.PHONY: deps +deps: + go mod download + go mod verify + +.PHONY: update +update: + go version + go get -u ./... + go mod tidy + +.PHONY: image +image: + @echo "building $(APP_NAME):$(IMAGE_NAME)" + docker build --progress plain -t $(IMAGE_NAME) . + +.PHONY: all +all: verify build + +.PHONY: clean +clean: + rm -f coverage.out coverage.html + go clean -cache + +.PHONY: docs +docs: + pip install -r requirements-docs.txt + mkdocs serve + +.PHONY: docs-build +docs-build: + pip install -r requirements-docs.txt + mkdocs build --strict + +# Public API methods not called within the module are excluded. +# See: Hosts.Reload, Hosts.RemoveByComments, Hosts.HostAddressLookup +DEADCODE_EXCLUDE := Hosts\.Reload|Hosts\.RemoveByComments|Hosts\.HostAddressLookup + +.PHONY: dead-code +dead-code: + @OUTPUT=$$(deadcode ./... 2>&1 | grep -Ev '$(DEADCODE_EXCLUDE)') || true; \ + if [ -n "$$OUTPUT" ]; then \ + echo "Dead code detected:"; \ + echo "$$OUTPUT"; \ + exit 1; \ + fi + +.PHONY: mutate +mutate: + gremlins unleash --threshold-efficacy 60 + +.PHONY: check +check: + go vet ./... + staticcheck ./... + golangci-lint run ./... diff --git a/vendor/github.com/txn2/txeh/README.md b/vendor/github.com/txn2/txeh/README.md index 2e7846a1..6eaed6ec 100644 --- a/vendor/github.com/txn2/txeh/README.md +++ b/vendor/github.com/txn2/txeh/README.md @@ -3,7 +3,14 @@ # Etc Hosts Management Utility & Go Library -[![GoDoc](https://godoc.org/github.com/txn2/irsync/txeh?status.svg)](https://godoc.org/github.com/txn2/txeh) +[![CI](https://github.com/txn2/txeh/actions/workflows/ci.yml/badge.svg)](https://github.com/txn2/txeh/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/txn2/txeh/branch/master/graph/badge.svg)](https://codecov.io/gh/txn2/txeh) +[![Go Report Card](https://goreportcard.com/badge/github.com/txn2/txeh)](https://goreportcard.com/report/github.com/txn2/txeh) +[![Go Reference](https://pkg.go.dev/badge/github.com/txn2/txeh.svg)](https://pkg.go.dev/github.com/txn2/txeh) +[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/txn2/txeh/badge)](https://securityscorecards.dev/viewer/?uri=github.com/txn2/txeh) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) + +**[Documentation](https://txeh.txn2.com)** | **[Go Reference](https://pkg.go.dev/github.com/txn2/txeh)** | **[Releases](https://github.com/txn2/txeh/releases)** ### /etc/hosts Management @@ -13,6 +20,13 @@ It is easy to open your [/etc/hosts] file in text editor and add or remove entri A computer's [/etc/hosts] file is a powerful utility for developers and system administrators to create localized, custom DNS entries. This small go library and utility were developed to encapsulate the complexity of working with [/etc/hosts] directly by providing a simple interface for adding and removing entries in a [/etc/hosts] file. +**Features:** +- Thread-safe operations (mutex-protected for concurrent use) +- IPv4 and IPv6 support +- CIDR range operations for bulk add/remove +- Preserves comments and file formatting +- Cross-platform (Linux, macOS, Windows) + ## txeh Utility ### Install @@ -21,7 +35,7 @@ MacOS [homebrew](https://brew.sh) users can `brew install txn2/tap/txeh`, otherw #### Install with go install -When installing with Go please use the latest stable Go release. At least go1.16 or greater is required. +When installing with Go please use the latest stable Go release. At least go1.24 or greater is required. To install use: `go install github.com/txn2/txeh/txeh@master` @@ -46,7 +60,7 @@ The txeh CLI application allows command line or scripted access to /etc/hosts fi | |___ _____| |__ | __\ \/ / _ \ '_ \ | |_ > < __/ | | | - \__/_/\_\___|_| |_| v1.5.0 + \__/_/\_\___|_| |_| Add, remove and re-associate hostname entries in your /etc/hosts file. Read more including usage as a Go library at https://github.com/txn2/txeh @@ -66,11 +80,12 @@ Available Commands: Flags: - -d, --dryrun dry run, output to stdout (ignores quiet) - -h, --help help for txeh - -q, --quiet no output - -r, --read string (override) Path to read /etc/hosts file. - -w, --write string (override) Path to write /etc/hosts file. + -d, --dryrun dry run, output to stdout (ignores quiet) + -h, --help help for txeh + -m, --max-hosts-per-line int Max hostnames per line (0=auto, -1=unlimited, >0=explicit) + -q, --quiet no output + -r, --read string (override) Path to read /etc/hosts file. + -w, --write string (override) Path to write /etc/hosts file. ``` @@ -108,11 +123,96 @@ sudo txeh add 127.1.27.100 dev.example.com -q -d > hosts.test # default to the specified read path. txeh add 127.1.27.100 dev2.example.com -q -r ./hosts.test -# specify a separate read and write oath +# specify a separate read and write path txeh add 127.1.27.100 dev3.example.com -r ./hosts.test -w ./hosts.test2 ``` +## Comments + +txeh supports inline comments on host entries, useful for tracking which tool or purpose added specific entries (e.g., docker, development environments). + +### How Comments Work + +Comments use the standard hosts file format: `IP HOSTNAME [HOSTNAME...] # comment` + +**Key behavior:** +- Comments are used for **grouping**: hosts with the same IP AND same comment are placed on the same line +- Hosts added **without** a comment only match lines **without** a comment +- Hosts added **with** a comment only match lines **with the same comment** +- Comments are **never modified** on existing lines +- If no matching line exists, a new line is created + +### CLI Usage + +```bash +# Add host with a comment +sudo txeh add 127.0.0.1 myapp --comment "local development" +# Result: 127.0.0.1 myapp # local development + +# Add another host with the same comment (groups together) +sudo txeh add 127.0.0.1 myapp2 --comment "local development" +# Result: 127.0.0.1 myapp myapp2 # local development + +# Add host with a different comment (new line) +sudo txeh add 127.0.0.1 api --comment "staging environment" +# Result: 127.0.0.1 api # staging environment + +# Add host without a comment (only matches lines without comments) +sudo txeh add 127.0.0.1 test +# Result: goes to first 127.0.0.1 line that has NO comment, or creates new line +``` + +### Library Usage + +```go +// Add hosts with a comment +hosts.AddHostWithComment("127.0.0.1", "myapp", "local development") +hosts.AddHostsWithComment("127.0.0.1", []string{"svc1", "svc2"}, "my project services") + +// Add hosts without a comment (original behavior) +hosts.AddHost("127.0.0.1", "myhost") +hosts.AddHosts("127.0.0.1", []string{"a", "b", "c"}) +``` + +### Example Scenario + +Starting hosts file: +``` +127.0.0.1 localhost +127.0.0.1 app1 app2 # dev services +``` + +| Command | Result | +|---------|--------| +| `txeh add 127.0.0.1 app3 -c "dev services"` | app3 added to "dev services" line | +| `txeh add 127.0.0.1 api -c "staging env"` | New line: `127.0.0.1 api # staging env` | +| `txeh add 127.0.0.1 test` | test added to localhost line (no comment) | + +### Listing and Removing by Comment + +List all hosts with a specific comment: +```bash +sudo txeh list bycomment "dev services" +``` + +Remove all entries with a specific comment (removes entire lines): +```bash +sudo txeh remove bycomment "dev services" +``` + +### Modifying Comments + +Comments are never modified on existing lines. To change a comment, remove the hosts first and re-add them with the new comment: + +```bash +# Remove all entries with the old comment +sudo txeh remove bycomment "old comment" + +# Re-add with the new comment +sudo txeh add 127.0.0.1 app1 app2 --comment "new comment" +``` + ## txeh Go Library **Dependency:** @@ -122,50 +222,66 @@ go get github.com/txn2/txeh **Example Golang Implementation**: ```go - package main import ( "fmt" - "strings" "github.com/txn2/txeh" ) func main() { + // Load the system hosts file hosts, err := txeh.NewHostsDefault() if err != nil { panic(err) } - hosts.AddHost("127.100.100.100", "test") - hosts.AddHost("127.100.100.101", "logstash") - hosts.AddHosts("127.100.100.102", []string{"a", "b", "c"}) + // Add hosts (without comments) + hosts.AddHost("127.100.100.100", "myapp") + hosts.AddHost("127.100.100.101", "database") + hosts.AddHosts("127.100.100.102", []string{"cache", "queue", "search"}) - hosts.RemoveHosts([]string{"example", "example.machine", "example.machine.example.com"}) - hosts.RemoveHosts(strings.Fields("example2 example.machine2 example.machine.example.com2")) + // Add hosts with comments (for tracking/organization) + hosts.AddHostWithComment("127.100.100.200", "api.local", "development services") + hosts.AddHostsWithComment("127.100.100.201", []string{"web.local", "admin.local"}, "frontend apps") + // Query existing entries + addresses := hosts.ListAddressesByHost("myapp", true) + fmt.Printf("myapp resolves to: %v\n", addresses) - hosts.RemoveAddress("127.1.27.1") + hostnames := hosts.ListHostsByIP("127.100.100.102") + fmt.Printf("Hosts at 127.100.100.102: %v\n", hostnames) - removeList := []string{ - "127.1.27.15", - "127.1.27.14", - "127.1.27.13", - } + // List all hosts with a specific comment + devHosts := hosts.ListHostsByComment("development services") + fmt.Printf("Dev service hosts: %v\n", devHosts) - hosts.RemoveAddresses(removeList) + // Remove entries + hosts.RemoveHost("database") + hosts.RemoveHosts([]string{"cache", "queue"}) + hosts.RemoveAddress("127.1.27.1") + hosts.RemoveAddresses([]string{"127.1.27.15", "127.1.27.14"}) - hfData := hosts.RenderHostsFile() + // Remove all entries with specific comments + hosts.RemoveByComment("frontend apps") + hosts.RemoveByComments([]string{"old environment", "deprecated"}) - // if you like to see what the outcome will - // look like - fmt.Println(hfData) + // RemoveCIDRs returns an error (CIDR parsing can fail) + if err := hosts.RemoveCIDRs([]string{"10.0.0.0/8"}); err != nil { + panic(err) + } - hosts.Save() - // or hosts.SaveAs("./test.hosts") -} + // Preview changes + fmt.Println(hosts.RenderHostsFile()) + // Save changes + err = hosts.Save() + if err != nil { + panic(err) + } + // Or save to a specific file: hosts.SaveAs("./custom.hosts") +} ``` ## Build Release @@ -177,11 +293,15 @@ goreleaser --skip-publish --clean --skip-validate Build and release: ```bash -GITHUB_TOKEN=$GITHUB_TOKEN goreleaser --rm-dist +GITHUB_TOKEN=$GITHUB_TOKEN goreleaser --clean ``` ### License Apache License 2.0 +--- + +Open source by [Craig Johnston](https://twitter.com/cjimti), sponsored by [Deasil Works, Inc.](https://deasil.works/) + [/etc/hosts]:https://en.wikipedia.org/wiki/Hosts_(file) diff --git a/vendor/github.com/txn2/txeh/SECURITY.md b/vendor/github.com/txn2/txeh/SECURITY.md new file mode 100644 index 00000000..b6fd0d6c --- /dev/null +++ b/vendor/github.com/txn2/txeh/SECURITY.md @@ -0,0 +1,32 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 1.x.x | :white_check_mark: | +| < 1.0 | :x: | + +## Reporting a Vulnerability + +1. **Do not** open a public issue +2. Email security concerns to cj@imti.co +3. Include: + - Description of the vulnerability + - Steps to reproduce + - Potential impact +4. Expect response within 48 hours + +## Disclosure Policy + +- We aim to patch critical vulnerabilities within 7 days +- Public disclosure after patch is available +- Credit will be given to reporters (unless anonymity is requested) + +## Security Measures + +This project follows security best practices: +- Dependencies are monitored via Dependabot +- Code is scanned with gosec and CodeQL +- Vulnerabilities are checked with govulncheck +- Supply chain security via OpenSSF Scorecard diff --git a/vendor/github.com/txn2/txeh/codecov.yml b/vendor/github.com/txn2/txeh/codecov.yml new file mode 100644 index 00000000..7912bce2 --- /dev/null +++ b/vendor/github.com/txn2/txeh/codecov.yml @@ -0,0 +1,23 @@ +codecov: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "70...100" + + status: + project: + default: + target: 82% + threshold: 2% + patch: + default: + target: 80% + threshold: 5% + informational: false + +comment: + layout: "reach,diff,flags,files" + behavior: default + require_changes: true diff --git a/vendor/github.com/txn2/txeh/flush.go b/vendor/github.com/txn2/txeh/flush.go new file mode 100644 index 00000000..88586be9 --- /dev/null +++ b/vendor/github.com/txn2/txeh/flush.go @@ -0,0 +1,62 @@ +package txeh + +import ( + "fmt" + "os/exec" + "strings" +) + +// execCommandFunc is a variable that wraps exec.Command for testability. +var execCommandFunc = exec.Command + +// ExecCommandFunc returns the current exec command function. +// This is intended for test code that needs to save and restore the original. +func ExecCommandFunc() func(string, ...string) *exec.Cmd { + return execCommandFunc +} + +// SetExecCommandFunc replaces the exec command function. +// This is intended for test code that needs to mock command execution. +func SetExecCommandFunc(fn func(string, ...string) *exec.Cmd) { + execCommandFunc = fn +} + +// FlushError represents a DNS cache flush failure with platform context. +type FlushError struct { + Platform string + Command string + Err error +} + +// Error returns a human-readable error message. +func (e *FlushError) Error() string { + return fmt.Sprintf("flush DNS cache on %s (%s): %s", e.Platform, e.Command, e.Err) +} + +// Unwrap returns the underlying error. +func (e *FlushError) Unwrap() error { + return e.Err +} + +// FlushDNSCache flushes the operating system's DNS cache. +// +// Platform commands (all are OS-provided utilities, not installable dependencies): +// - macOS: dscacheutil -flushcache + killall -HUP mDNSResponder (ships with macOS) +// - Linux: resolvectl flush-caches (systemd 239+) or systemd-resolve --flush-caches (older systemd). +// Returns ErrNoResolver if neither binary is found, which typically means the system +// does not cache DNS locally and hosts file changes take effect immediately. +// - Windows: ipconfig /flushdns (ships with Windows) +// +// Returns a *FlushError on failure, or nil on unsupported platforms +// where no resolver is detected. +func FlushDNSCache() error { + return flushDNSCachePlatform() +} + +// joinArgs joins command arguments for error display. +func joinArgs(name string, args []string) string { + parts := make([]string, 0, len(args)+1) + parts = append(parts, name) + parts = append(parts, args...) + return strings.Join(parts, " ") +} diff --git a/vendor/github.com/txn2/txeh/flush_darwin.go b/vendor/github.com/txn2/txeh/flush_darwin.go new file mode 100644 index 00000000..6e5b5c88 --- /dev/null +++ b/vendor/github.com/txn2/txeh/flush_darwin.go @@ -0,0 +1,40 @@ +//go:build darwin + +package txeh + +import "runtime" + +// flushDNSCachePlatform flushes the DNS cache on macOS. +// +// Commands: +// - dscacheutil -flushcache: flushes the Directory Service cache. +// Source: man dscacheutil (local man page, -flushcache flag). +// Present since macOS 10.5 Leopard (2007). +// - killall -HUP mDNSResponder: restarts the mDNS responder to clear its cache. +// Standard Apple convention, widely documented. +// +// Both commands together are the standard recommendation from macOS 10.15 Catalina (2019) +// through current releases. On 10.12-10.14, only the killall was strictly needed, but +// running dscacheutil there is harmless (flushes the Directory Service cache, a no-op +// if DNS isn't cached there). +// +// Both binaries ship with macOS. No external dependencies. +// +// The killall step is non-fatal since mDNSResponder may not be running. +func flushDNSCachePlatform() error { + cmd := execCommandFunc("dscacheutil", "-flushcache") // #nosec G204 -- hardcoded command, not user input + if err := cmd.Run(); err != nil { + return &FlushError{ + Platform: runtime.GOOS, + Command: joinArgs("dscacheutil", []string{"-flushcache"}), + Err: err, + } + } + + // Attempt to restart mDNSResponder. This may fail if the process + // is not running, which is not an error condition. + cmd = execCommandFunc("killall", "-HUP", "mDNSResponder") // #nosec G204 -- hardcoded command, not user input + _ = cmd.Run() + + return nil +} diff --git a/vendor/github.com/txn2/txeh/flush_linux.go b/vendor/github.com/txn2/txeh/flush_linux.go new file mode 100644 index 00000000..bf0c2265 --- /dev/null +++ b/vendor/github.com/txn2/txeh/flush_linux.go @@ -0,0 +1,64 @@ +//go:build linux + +package txeh + +import ( + "errors" + "os/exec" + "runtime" +) + +// ErrNoResolver is returned when no supported DNS resolver is found on the system. +// If your system does not cache DNS locally (e.g., no systemd-resolved, dnsmasq, or +// unbound), hosts file changes take effect immediately without flushing. +var ErrNoResolver = errors.New( + "systemd-resolved not detected (tried resolvectl, systemd-resolve). " + + "If your system does not cache DNS locally, hosts file changes take effect immediately without flushing", +) + +// flushDNSCachePlatform flushes the DNS cache on Linux. +// +// Commands: +// - resolvectl flush-caches: systemd-resolved 239+ (2018). +// Source: https://man7.org/linux/man-pages/man1/resolvectl.1.html +// - systemd-resolve --flush-caches: pre-239 name for the same operation. +// +// Tries resolvectl first, then falls back to systemd-resolve. +// Returns ErrNoResolver (wrapped in FlushError) if neither is available. +// +// Other resolvers (dnsmasq, unbound, nscd) are intentionally not supported here +// because flushing them reliably depends on per-site configuration (socket paths, +// service names, etc.) that we can't safely assume. +func flushDNSCachePlatform() error { + // Try resolvectl first (systemd 239+). + if _, err := exec.LookPath("resolvectl"); err == nil { + cmd := execCommandFunc("resolvectl", "flush-caches") // #nosec G204 -- hardcoded command, not user input + if err := cmd.Run(); err != nil { + return &FlushError{ + Platform: runtime.GOOS, + Command: joinArgs("resolvectl", []string{"flush-caches"}), + Err: err, + } + } + return nil + } + + // Fallback to systemd-resolve (older systemd). + if _, err := exec.LookPath("systemd-resolve"); err == nil { + cmd := execCommandFunc("systemd-resolve", "--flush-caches") // #nosec G204 -- hardcoded command, not user input + if err := cmd.Run(); err != nil { + return &FlushError{ + Platform: runtime.GOOS, + Command: joinArgs("systemd-resolve", []string{"--flush-caches"}), + Err: err, + } + } + return nil + } + + return &FlushError{ + Platform: runtime.GOOS, + Command: "", + Err: ErrNoResolver, + } +} diff --git a/vendor/github.com/txn2/txeh/flush_other.go b/vendor/github.com/txn2/txeh/flush_other.go new file mode 100644 index 00000000..4a2298e1 --- /dev/null +++ b/vendor/github.com/txn2/txeh/flush_other.go @@ -0,0 +1,17 @@ +//go:build !darwin && !linux && !windows + +package txeh + +import ( + "errors" + "runtime" +) + +// flushDNSCachePlatform returns an error on unsupported platforms. +func flushDNSCachePlatform() error { + return &FlushError{ + Platform: runtime.GOOS, + Command: "", + Err: errors.New("unsupported platform"), + } +} diff --git a/vendor/github.com/txn2/txeh/flush_windows.go b/vendor/github.com/txn2/txeh/flush_windows.go new file mode 100644 index 00000000..21a5bd88 --- /dev/null +++ b/vendor/github.com/txn2/txeh/flush_windows.go @@ -0,0 +1,23 @@ +//go:build windows + +package txeh + +import "runtime" + +// flushDNSCachePlatform flushes the DNS cache on Windows. +// +// Command: ipconfig /flushdns +// Source: https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/ipconfig +// Present in all supported Windows versions (XP through current). +// Ships with Windows. No external dependencies. +func flushDNSCachePlatform() error { + cmd := execCommandFunc("ipconfig", "/flushdns") // #nosec G204 -- hardcoded command, not user input + if err := cmd.Run(); err != nil { + return &FlushError{ + Platform: runtime.GOOS, + Command: joinArgs("ipconfig", []string{"/flushdns"}), + Err: err, + } + } + return nil +} diff --git a/vendor/github.com/txn2/txeh/mkdocs.yml b/vendor/github.com/txn2/txeh/mkdocs.yml new file mode 100644 index 00000000..be1db1d0 --- /dev/null +++ b/vendor/github.com/txn2/txeh/mkdocs.yml @@ -0,0 +1,116 @@ +site_name: txn2/txeh +site_url: https://txeh.txn2.com +site_description: /etc/hosts management library and CLI utility for Go +site_author: Craig Johnston +repo_url: https://github.com/txn2/txeh +repo_name: txn2/txeh +edit_uri: edit/master/docs/ + +copyright: Copyright © 2018 - 2026 Craig Johnston / Deasil Works, Inc. + +theme: + name: material + custom_dir: docs/overrides + logo: images/txn2-logo.png + favicon: images/logo.png + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to light mode + font: + text: Inter + code: JetBrains Mono + icon: + repo: fontawesome/brands/github + features: + - navigation.instant + - navigation.instant.progress + - navigation.tracking + - navigation.tabs + - navigation.tabs.sticky + - navigation.top + - navigation.footer + - toc.integrate + - search.suggest + - search.highlight + - search.share + - content.code.copy + - content.code.annotate + - content.tabs.link + - content.action.edit + - toc.follow + - announce.dismiss + +nav: + - Home: index.md + - Getting Started: getting-started.md + - CLI Reference: cli.md + - Go Library: library.md + - API Reference: api.md + - Troubleshooting: troubleshooting.md + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/txn2/txeh + name: txeh on GitHub + generator: false + version: + provider: mike + +extra_css: + - stylesheets/extra.css + +markdown_extensions: + - abbr + - admonition + - attr_list + - def_list + - footnotes + - md_in_html + - tables + - toc: + permalink: true + toc_depth: 3 + - pymdownx.arithmatex: + generic: true + - pymdownx.betterem: + smart_enable: all + - pymdownx.caret + - pymdownx.details + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.keys + - pymdownx.magiclink: + repo_url_shorthand: true + user: txn2 + repo: txeh + - pymdownx.mark + - pymdownx.smartsymbols + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: + alternate_style: true + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.tilde + +plugins: + - search: + separator: '[\s\-,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])' diff --git a/vendor/github.com/txn2/txeh/requirements-docs.txt b/vendor/github.com/txn2/txeh/requirements-docs.txt new file mode 100644 index 00000000..643418ba --- /dev/null +++ b/vendor/github.com/txn2/txeh/requirements-docs.txt @@ -0,0 +1,2 @@ +mkdocs==1.6.1 +mkdocs-material==9.7.1 diff --git a/vendor/github.com/txn2/txeh/txeh.go b/vendor/github.com/txn2/txeh/txeh.go index 0744bc64..7f687f30 100644 --- a/vendor/github.com/txn2/txeh/txeh.go +++ b/vendor/github.com/txn2/txeh/txeh.go @@ -1,3 +1,6 @@ +// Package txeh provides /etc/hosts file management capabilities. +// It offers a simple interface for adding, removing, and querying hostname-to-IP mappings +// with thread-safe operations and cross-platform support (Linux, macOS, Windows). package txeh import ( @@ -5,47 +8,73 @@ import ( "fmt" "net" "os" + "path/filepath" "regexp" "runtime" "strings" "sync" ) -const UNKNOWN = 0 -const EMPTY = 10 -const COMMENT = 20 -const ADDRESS = 30 +// Line type constants for HostFileLine. +const ( + UNKNOWN = 0 // Unknown line type. + EMPTY = 10 // Empty line. + COMMENT = 20 // Comment line starting with #. + ADDRESS = 30 // Address line with IP and hostnames. +) + +// DefaultMaxHostsPerLineWindows is the default maximum number of hostnames per line on Windows. +// Windows has a limitation where lines with more than ~9 hostnames may not resolve correctly. +const DefaultMaxHostsPerLineWindows = 9 +// osWindows is the runtime.GOOS value for Windows. +const osWindows = "windows" + +// IPFamily represents the IP address family (IPv4 or IPv6). type IPFamily int64 +// IP address family constants. const ( - IPFamilyV4 IPFamily = iota - IPFamilyV6 + IPFamilyV4 IPFamily = iota // IPv4 address family. + IPFamilyV6 // IPv6 address family. ) +// HostsConfig contains configuration for reading and writing hosts files. type HostsConfig struct { ReadFilePath string WriteFilePath string // RawText for input. If RawText is set ReadFilePath, WriteFilePath are ignored. Use RenderHostsFile rather // than save to get the results. RawText *string + // MaxHostsPerLine limits the number of hostnames per line when adding hosts. + // This is useful for Windows which has a limitation of ~9 hostnames per line. + // Values: + // 0 = auto-detect (Windows: 9, others: unlimited) + // -1 = force unlimited (no limit) + // >0 = explicit limit + MaxHostsPerLine int + // AutoFlush triggers a DNS cache flush after every successful Save/SaveAs. + // Flush failures are returned as *FlushError, distinguishable via errors.As. + AutoFlush bool } +// Hosts represents a parsed hosts file with thread-safe operations. type Hosts struct { - sync.Mutex + mu sync.Mutex *HostsConfig hostFileLines HostFileLines } -// AddressLocations the location of an address in the HFL +// AddressLocations maps an address to its location in the HFL. type AddressLocations map[string]int -// HostLocations maps a hostname -// to an original line number +// HostLocations maps a hostname to an original line number. type HostLocations map[string]int +// HostFileLines is a slice of HostFileLine entries. type HostFileLines []HostFileLine +// HostFileLine represents a single line in a hosts file. type HostFileLine struct { OriginalLineNum int LineType int @@ -57,22 +86,21 @@ type HostFileLine struct { Comment string } -// NewHostsDefault returns a hosts object with -// default configuration +// NewHostsDefault returns a hosts object with default configuration. func NewHostsDefault() (*Hosts, error) { return NewHosts(&HostsConfig{}) } -// NewHosts returns a new hosts object +// NewHosts returns a new hosts object. func NewHosts(hc *HostsConfig) (*Hosts, error) { h := &Hosts{HostsConfig: hc} - h.Lock() - defer h.Unlock() + h.mu.Lock() + defer h.mu.Unlock() defaultHostsFile := "/etc/hosts" - if runtime.GOOS == "windows" { - defaultHostsFile = `C:\Windows\System32\Drivers\etc\hosts` + if runtime.GOOS == osWindows { + defaultHostsFile = winDefaultHostsFile() } if h.ReadFilePath == "" && h.RawText == nil { @@ -103,36 +131,43 @@ func NewHosts(hc *HostsConfig) (*Hosts, error) { return h, nil } -// Save rendered hosts file +// Save renders and writes the hosts file to the configured write path. func (h *Hosts) Save() error { return h.SaveAs(h.WriteFilePath) } -// SaveAs saves rendered hosts file to the filename specified +// SaveAs saves rendered hosts file to the filename specified. func (h *Hosts) SaveAs(fileName string) error { if h.RawText != nil { return errors.New("cannot call Save or SaveAs with RawText. Use RenderHostsFile to return a string") } hfData := []byte(h.RenderHostsFile()) - h.Lock() - defer h.Unlock() + h.mu.Lock() + defer h.mu.Unlock() - err := os.WriteFile(fileName, hfData, 0644) + err := os.WriteFile(filepath.Clean(fileName), hfData, 0o644) // #nosec G306 -- hosts file must be world-readable (0644) for DNS resolution if err != nil { - return err + return fmt.Errorf("write hosts file %s: %w", fileName, err) + } + + if h.AutoFlush { + if flushErr := FlushDNSCache(); flushErr != nil { + return flushErr + } } return nil } -// Reload hosts file +// Reload re-reads the hosts file from disk and replaces the in-memory state. +// This is part of the public API for consumers who manage long-lived Hosts instances. func (h *Hosts) Reload() error { if h.RawText != nil { return errors.New("cannot call Reload with RawText") } - h.Lock() - defer h.Unlock() + h.mu.Lock() + defer h.mu.Unlock() hfl, err := ParseHosts(h.ReadFilePath) if err != nil { @@ -162,8 +197,8 @@ func (h *Hosts) RemoveAddress(address string) { // RemoveFirstAddress removes the first entry (line) found with the provided address. func (h *Hosts) RemoveFirstAddress(address string) bool { - h.Lock() - defer h.Unlock() + h.mu.Lock() + defer h.mu.Unlock() for hflIdx := range h.hostFileLines { if address == h.hostFileLines[hflIdx].Address { @@ -185,15 +220,14 @@ func (h *Hosts) RemoveCIDRs(cidrs []string) error { // loop through all the CIDR ranges (we probably have less ranges than IPs) for _, cidr := range cidrs { - _, ipnet, err := net.ParseCIDR(cidr) if err != nil { - return err + return fmt.Errorf("parse CIDR %s: %w", cidr, err) } hfLines := h.GetHostFileLines() - for _, hfl := range *hfLines { + for _, hfl := range hfLines { ip := net.ParseIP(hfl.Address) if ip != nil { if ipnet.Contains(ip) { @@ -208,7 +242,7 @@ func (h *Hosts) RemoveCIDRs(cidrs []string) error { return nil } -// RemoveHosts removes all hostname entries of the provided host slice +// RemoveHosts removes all hostname entries of the provided host slice. func (h *Hosts) RemoveHosts(hosts []string) { for _, host := range hosts { if h.RemoveFirstHost(host) { @@ -217,18 +251,18 @@ func (h *Hosts) RemoveHosts(hosts []string) { } } -// RemoveHost removes all hostname entries of provided host +// RemoveHost removes all hostname entries of provided host. func (h *Hosts) RemoveHost(host string) { if h.RemoveFirstHost(host) { h.RemoveHost(host) } } -// RemoveFirstHost the first hostname entry found and returns true if successful +// RemoveFirstHost removes the first hostname entry found and returns true if successful. func (h *Hosts) RemoveFirstHost(host string) bool { host = strings.TrimSpace(strings.ToLower(host)) - h.Lock() - defer h.Unlock() + h.mu.Lock() + defer h.mu.Unlock() for hflIdx := range h.hostFileLines { for hidx, hst := range h.hostFileLines[hflIdx].Hostnames { @@ -247,19 +281,75 @@ func (h *Hosts) RemoveFirstHost(host string) bool { return false } +// RemoveByComments removes all host entries that have any of the specified comments. +// This removes entire lines where the comment matches. +// This is part of the public API for bulk comment-based cleanup (e.g., kubefwd teardown). +func (h *Hosts) RemoveByComments(comments []string) { + for _, comment := range comments { + h.RemoveByComment(comment) + } +} + +// RemoveByComment removes all host entries that have the specified comment. +// This removes entire lines where the comment matches. +func (h *Hosts) RemoveByComment(comment string) { + h.mu.Lock() + defer h.mu.Unlock() + + comment = strings.TrimSpace(comment) + var newLines HostFileLines + + for _, hfl := range h.hostFileLines { + if hfl.Comment != comment { + newLines = append(newLines, hfl) + } + } + + h.hostFileLines = newLines +} + // AddHosts adds an array of hosts to the first matching address it finds -// or creates the address and adds the hosts +// or creates the address and adds the hosts. func (h *Hosts) AddHosts(address string, hosts []string) { for _, hst := range hosts { h.AddHost(address, hst) } } +// AddHostsWithComment adds an array of hosts to an address with a comment. +// All hosts will share the same comment. If the address already exists with +// the same comment, hosts are appended to that line (respecting MaxHostsPerLine). +// If the address exists with a different comment, a new line is created. +func (h *Hosts) AddHostsWithComment(address string, hosts []string, comment string) { + for _, hst := range hosts { + h.AddHostWithComment(address, hst, comment) + } +} + // AddHost adds a host to an address and removes the host -// from any existing address is may be associated with -func (h *Hosts) AddHost(addressRaw string, hostRaw string) { +// from any existing address it may be associated with. +func (h *Hosts) AddHost(addressRaw, hostRaw string) { + h.addHostWithComment(addressRaw, hostRaw, "") +} + +// AddHostWithComment adds a host to an address with an inline comment. +// The comment will appear after the hostnames on the line (e.g., "127.0.0.1 host # comment"). +// If the address already exists with the same comment, the host is appended to that line +// (respecting MaxHostsPerLine). If the address exists with a different comment, a new line +// is created with the specified comment. +func (h *Hosts) AddHostWithComment(addressRaw, hostRaw, comment string) { + h.addHostWithComment(addressRaw, hostRaw, comment) +} + +// addHostWithComment is the internal implementation that handles both +// commented and non-commented host additions. +func (h *Hosts) addHostWithComment(addressRaw, hostRaw, comment string) { //nolint:revive // internal method mirrors public API naming host := strings.TrimSpace(strings.ToLower(hostRaw)) address := strings.TrimSpace(strings.ToLower(addressRaw)) + // Normalize comment: trim spaces, but don't add/remove the # prefix + // (the # is handled during rendering) + comment = strings.TrimSpace(comment) + addressIP := net.ParseIP(address) if addressIP == nil { return @@ -269,62 +359,49 @@ func (h *Hosts) AddHost(addressRaw string, hostRaw string) { ipFamily = IPFamilyV6 } + h.mu.Lock() + defer h.mu.Unlock() + // does the host already exist - if ok, exAdd, hflIdx := h.HostAddressLookup(host, ipFamily); ok { - // if the address is the same we are done + ok, exAdd, hflIdx := h.hostAddressLookupLocked(host, ipFamily) + if ok { if address == exAdd { - return - } - - // if the hostname is at a different address, go and remove it from the address - for hidx, hst := range h.hostFileLines[hflIdx].Hostnames { - // for localhost, we can match more than one host - if isLocalhost(address) { - break - } - if hst == host { - h.Lock() - h.hostFileLines[hflIdx].Hostnames = removeStringElement(h.hostFileLines[hflIdx].Hostnames, hidx) - h.Unlock() - - // remove the address line if empty - if len(h.hostFileLines[hflIdx].Hostnames) < 1 { - h.Lock() - h.hostFileLines = removeHFLElement(h.hostFileLines, hflIdx) - h.Unlock() - } - - break // unless we should continue because it could have duplicates - } + return // already at correct address } + // hostname is at a different address, remove it from there + h.removeHostFromLineLocked(hflIdx, host, address) } - // if the address exists add it to the address line + // Get the effective max hosts per line limit + maxPerLine := h.getEffectiveMaxHostsPerLine() + + // if the address exists with matching comment, add it to that line if there's room for i, hfl := range h.hostFileLines { - if hfl.Address == address { - h.Lock() - h.hostFileLines[i].Hostnames = append(h.hostFileLines[i].Hostnames, host) - h.Unlock() - return + if hfl.Address == address && hfl.Comment == comment { + // Check if this line has room (0 means unlimited) + if maxPerLine <= 0 || len(h.hostFileLines[i].Hostnames) < maxPerLine { + h.hostFileLines[i].Hostnames = append(h.hostFileLines[i].Hostnames, host) + return + } + // This line is full, continue looking for another line with the same address and comment } } - // the address and host do not already exist so go ahead and create them + // No existing line with matching address and comment found (or all are full), create a new line hfl := HostFileLine{ LineType: ADDRESS, Address: address, Hostnames: []string{host}, + Comment: comment, } - h.Lock() h.hostFileLines = append(h.hostFileLines, hfl) - h.Unlock() } -// ListHostsByIP returns a list of hostnames associated with a given IP address +// ListHostsByIP returns a list of hostnames associated with a given IP address. func (h *Hosts) ListHostsByIP(address string) []string { - h.Lock() - defer h.Unlock() + h.mu.Lock() + defer h.mu.Unlock() var hosts []string @@ -337,10 +414,10 @@ func (h *Hosts) ListHostsByIP(address string) []string { return hosts } -// ListAddressesByHost returns a list of IPs associated with a given hostname +// ListAddressesByHost returns a list of IPs associated with a given hostname. func (h *Hosts) ListAddressesByHost(hostname string, exact bool) [][]string { - h.Lock() - defer h.Unlock() + h.mu.Lock() + defer h.mu.Unlock() var addresses [][]string @@ -349,7 +426,7 @@ func (h *Hosts) ListAddressesByHost(hostname string, exact bool) [][]string { if hst == hostname { addresses = append(addresses, []string{hsl.Address, hst}) } - if exact == false && hst != hostname && strings.Contains(hst, hostname) { + if !exact && hst != hostname && strings.Contains(hst, hostname) { addresses = append(addresses, []string{hsl.Address, hst}) } } @@ -358,14 +435,18 @@ func (h *Hosts) ListAddressesByHost(hostname string, exact bool) [][]string { return addresses } -// ListHostsByCIDR returns a list of IPs and hostnames associated with a given CIDR +// ListHostsByCIDR returns a list of IPs and hostnames associated with a given CIDR. func (h *Hosts) ListHostsByCIDR(cidr string) [][]string { - h.Lock() - defer h.Unlock() + h.mu.Lock() + defer h.mu.Unlock() var ipHosts [][]string - _, subnet, _ := net.ParseCIDR(cidr) + _, subnet, err := net.ParseCIDR(cidr) + if err != nil { + return ipHosts + } + for _, hsl := range h.hostFileLines { if subnet.Contains(net.ParseIP(hsl.Address)) { for _, hst := range hsl.Hostnames { @@ -377,12 +458,35 @@ func (h *Hosts) ListHostsByCIDR(cidr string) [][]string { return ipHosts } -// HostAddressLookup returns true if the host is found, a string -// containing the address and the index of the hfl -func (h *Hosts) HostAddressLookup(host string, ipFamily IPFamily) (bool, string, int) { - h.Lock() - defer h.Unlock() +// ListHostsByComment returns all hostnames on lines with the given comment. +func (h *Hosts) ListHostsByComment(comment string) []string { + h.mu.Lock() + defer h.mu.Unlock() + + comment = strings.TrimSpace(comment) + var hosts []string + + for _, hsl := range h.hostFileLines { + if hsl.Comment == comment { + hosts = append(hosts, hsl.Hostnames...) + } + } + + return hosts +} +// HostAddressLookup returns true if the host is found, the address string, +// and the index of the host file line. This is part of the public API for +// consumers that need direct address lookups by IP family. +func (h *Hosts) HostAddressLookup(host string, ipFamily IPFamily) (found bool, address string, idx int) { + h.mu.Lock() + defer h.mu.Unlock() + + return h.hostAddressLookupLocked(host, ipFamily) +} + +// hostAddressLookupLocked is the internal version that assumes the lock is already held. +func (h *Hosts) hostAddressLookupLocked(host string, ipFamily IPFamily) (found bool, address string, idx int) { host = strings.ToLower(strings.TrimSpace(host)) for i, hfl := range h.hostFileLines { @@ -403,40 +507,78 @@ func (h *Hosts) HostAddressLookup(host string, ipFamily IPFamily) (bool, string, return false, "", 0 } +// RenderHostsFile returns the hosts file content as a formatted string. func (h *Hosts) RenderHostsFile() string { - h.Lock() - defer h.Unlock() - - hf := "" + h.mu.Lock() + defer h.mu.Unlock() + var sb strings.Builder for _, hfl := range h.hostFileLines { - hf = hf + fmt.Sprintln(lineFormatter(hfl)) + sb.WriteString(lineFormatter(hfl)) + sb.WriteByte('\n') } - return hf + return sb.String() } -func (h *Hosts) GetHostFileLines() *HostFileLines { - h.Lock() - defer h.Unlock() +// GetHostFileLines returns a copy of all parsed host file lines. +func (h *Hosts) GetHostFileLines() HostFileLines { + h.mu.Lock() + defer h.mu.Unlock() - return &h.hostFileLines + // Return a copy to prevent external modification of internal state + result := make(HostFileLines, len(h.hostFileLines)) + copy(result, h.hostFileLines) + return result } +// getEffectiveMaxHostsPerLine returns the effective maximum hosts per line. +// Returns 0 for unlimited. +func (h *Hosts) getEffectiveMaxHostsPerLine() int { + if h.HostsConfig == nil { + // No config, use auto-detect + if runtime.GOOS == osWindows { + return DefaultMaxHostsPerLineWindows + } + return 0 + } + + if h.MaxHostsPerLine > 0 { + // Explicit limit set + return h.MaxHostsPerLine + } + + if h.MaxHostsPerLine < 0 { + // Explicitly unlimited + return 0 + } + + // MaxHostsPerLine == 0: auto-detect based on OS + if runtime.GOOS == osWindows { + return DefaultMaxHostsPerLineWindows + } + return 0 +} + +// ParseHosts reads and parses a hosts file from the given path. func ParseHosts(path string) ([]HostFileLine, error) { - input, err := os.ReadFile(path) + input, err := os.ReadFile(filepath.Clean(path)) if err != nil { - return nil, err + return nil, fmt.Errorf("read hosts file %s: %w", path, err) } return ParseHostsFromString(string(input)) } +// ParseHostsFromString parses hosts file content from a string. func ParseHostsFromString(input string) ([]HostFileLine, error) { - inputNormalized := strings.Replace(input, "\r\n", "\n", -1) + inputNormalized := strings.ReplaceAll(input, "\r\n", "\n") dataLines := strings.Split(inputNormalized, "\n") - //remove extra blank line at end that does not exist in /etc/hosts file - dataLines = dataLines[:len(dataLines)-1] + // remove extra blank line at end that does not exist in /etc/hosts file + // only if the file ended with a newline (last element is empty) + if len(dataLines) > 0 && dataLines[len(dataLines)-1] == "" { + dataLines = dataLines[:len(dataLines)-1] + } hostFileLines := make([]HostFileLine, len(dataLines)) @@ -462,7 +604,7 @@ func ParseHostsFromString(input string) ([]HostFileLine, error) { curLineSplit := strings.SplitN(curLine.Trimmed, "#", 2) if len(curLineSplit) > 1 { - curLine.Comment = curLineSplit[1] + curLine.Comment = strings.TrimSpace(curLineSplit[1]) } curLine.Trimmed = curLineSplit[0] @@ -482,40 +624,70 @@ func ParseHostsFromString(input string) ([]HostFileLine, error) { // if we can't figure out what this line is // at this point mark it as unknown curLine.LineType = UNKNOWN - } return hostFileLines, nil } -// removeStringElement removed an element of a string slice +// removeStringElement removes an element of a string slice. func removeStringElement(slice []string, s int) []string { return append(slice[:s], slice[s+1:]...) } -// removeHFLElement removed an element of a HostFileLine slice +// removeHFLElement removes an element of a HostFileLine slice. func removeHFLElement(slice []HostFileLine, s int) []HostFileLine { return append(slice[:s], slice[s+1:]...) } -// lineFormatter -func lineFormatter(hfl HostFileLine) string { +// removeHostFromLineLocked removes a hostname from a specific line and cleans up empty lines. +// Must be called with lock held. For localhost addresses, this is a no-op since +// the same hostname can exist at multiple localhost addresses. +func (h *Hosts) removeHostFromLineLocked(hflIdx int, host, newAddress string) { + if isLocalhost(newAddress) { + return + } + + for hidx, hst := range h.hostFileLines[hflIdx].Hostnames { + if hst != host { + continue + } + h.hostFileLines[hflIdx].Hostnames = removeStringElement(h.hostFileLines[hflIdx].Hostnames, hidx) + if len(h.hostFileLines[hflIdx].Hostnames) == 0 { + h.hostFileLines = removeHFLElement(h.hostFileLines, hflIdx) + } + return + } +} +// lineFormatter formats a single host file line as a string. +func lineFormatter(hfl HostFileLine) string { if hfl.LineType < ADDRESS { return hfl.Raw } - if len(hfl.Comment) > 0 { - return fmt.Sprintf("%-16s %s #%s", hfl.Address, strings.Join(hfl.Hostnames, " "), hfl.Comment) + if hfl.Comment != "" { + return fmt.Sprintf("%-16s %s # %s", hfl.Address, strings.Join(hfl.Hostnames, " "), hfl.Comment) } return fmt.Sprintf("%-16s %s", hfl.Address, strings.Join(hfl.Hostnames, " ")) } -// IPLocalhost is a regex pattern for IPv4 or IPv6 loopback range. -const ipLocalhost = `((127\.([0-9]{1,3}\.){2}[0-9]{1,3})|(::1)$)` +// ipLocalhost is a regex pattern for IPv4 or IPv6 loopback range. +const ipLocalhost = `((127\.(\d{1,3}\.){2}\d{1,3})|(::1)$)` var localhostIPRegexp = regexp.MustCompile(ipLocalhost) func isLocalhost(address string) bool { return localhostIPRegexp.MatchString(address) } + +// winDefaultHostsFile returns the default hosts file path for Windows. +// It tries to use the SystemRoot environment variable. If that is not set, +// it falls back to C:\Windows\System32\Drivers\etc\hosts. +func winDefaultHostsFile() string { + if r := os.Getenv("SystemRoot"); r != "" { + return filepath.Join(r, "System32", "drivers", "etc", "hosts") + } + + // fallback to C:\ + return `C:\Windows\System32\drivers\etc\hosts` +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 2b4526ab..f7152160 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -100,10 +100,10 @@ github.com/radovskyb/watcher # github.com/rivo/uniseg v0.4.7 ## explicit; go 1.18 github.com/rivo/uniseg -# github.com/spf13/cobra v1.9.1 +# github.com/spf13/cobra v1.10.2 ## explicit; go 1.15 github.com/spf13/cobra -# github.com/spf13/pflag v1.0.6 +# github.com/spf13/pflag v1.0.9 ## explicit; go 1.12 github.com/spf13/pflag # github.com/stretchr/objx v0.5.2 @@ -114,8 +114,8 @@ github.com/stretchr/objx github.com/stretchr/testify/assert github.com/stretchr/testify/assert/yaml github.com/stretchr/testify/mock -# github.com/txn2/txeh v1.5.5 -## explicit; go 1.20 +# github.com/txn2/txeh v1.8.0 +## explicit; go 1.24.7 github.com/txn2/txeh # github.com/x448/float16 v0.8.4 ## explicit; go 1.11