diff --git a/.lefthook.yml b/.lefthook.yml index 2495d630..0fa5b928 100644 --- a/.lefthook.yml +++ b/.lefthook.yml @@ -28,6 +28,20 @@ pre-commit: # git diff-index --check "$(git hash-object -t tree /dev/null)" # stage_fixed: true + gofmt: + tags: "always,go,formatting" + glob: "**/*.go" + run: >- + gofmt -w -s -r 'interface{} -> any' -r 'a[b:len(a)] -> a[b:]' {staged_files} + stage_fixed: true + + gofumpt: + tags: "always,go,formatting" + glob: "**/*.go" + run: >- + gofumpt -w -e {staged_files} + stage_fixed: true + markdownlint: tags: "always,docs,formatting" glob: "**/*.md" diff --git a/Makefile b/Makefile index 5f2cdd06..91efe0e5 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,7 @@ install-tools-go: $(GO) install golang.org/x/tools/cmd/godoc@latest $(GO) install golang.org/x/vuln/cmd/govulncheck@latest $(GO) install gotest.tools/gotestsum@latest + $(GO) install mvdan.cc/gofumpt@latest .PHONY: install-tools-mac ## install-tools-mac: [tools]* Install/upgrade the required tools for macOS, including Go packages. diff --git a/corefunc/url.go b/corefunc/url.go index 9d887c3e..ebcd24a8 100644 --- a/corefunc/url.go +++ b/corefunc/url.go @@ -16,6 +16,9 @@ package corefunc import ( + "fmt" + neturl "net/url" + "github.com/nlnwa/whatwg-url/canonicalizer" "github.com/nlnwa/whatwg-url/url" @@ -57,3 +60,19 @@ func URLParse(rawURL string, canon ...types.URLCanonicalizer) (*url.Url, error) // Default return url.Parse(rawURL) // lint:allow_unwrapped_errors } + +/* +URLDecode decodes a URL-encoded string. + +---- + + - s (string): An encoded URL. +*/ +func URLDecode(s string) (string, error) { + q, err := neturl.QueryUnescape(s) + if err != nil { + return "", fmt.Errorf("failed to decode URL '%s': %w", q, err) + } + + return q, nil +} diff --git a/corefunc/url_test.go b/corefunc/url_test.go index 9dcab7d3..59a698c2 100644 --- a/corefunc/url_test.go +++ b/corefunc/url_test.go @@ -103,6 +103,18 @@ func ExampleURLParse_googleSafeBrowsing() { // .80 } +func ExampleURLDecode() { + output, err := URLDecode("hello%20%E4%B8%96%E7%95%8C") + if err != nil { + panic(err) + } + + fmt.Println(output) + + // Output: + // hello 世界 +} + func TestURLParse(t *testing.T) { // lint:allow_complexity for name, tc := range testfixtures.URLParseTestTable { t.Run(name, func(t *testing.T) { @@ -176,3 +188,20 @@ func TestURLParse(t *testing.T) { // lint:allow_complexity }) } } + +func TestURLDecode(t *testing.T) { // lint:allow_complexity + for name, tc := range testfixtures.URLDecodeTestTable { + t.Run(name, func(t *testing.T) { + output, err := URLDecode(tc.Input) + + // We expect an error. + if err != nil && !tc.ExpectedErr { + t.Errorf("Unexpected error: %v", err) + } + + if output != tc.Expected { + t.Errorf("Expected %s, got %s", tc.Expected, output) + } + }) + } +} diff --git a/terratest/functions/terraform_test.go b/terratest/functions/terraform_test.go index 265d4456..354680ba 100644 --- a/terratest/functions/terraform_test.go +++ b/terratest/functions/terraform_test.go @@ -55,7 +55,7 @@ var ( // Both must be installed first. binaries = []string{ "terraform", // 1.8.0+ - "tofu", // 1.7.0+ + "tofu", // 1.7.0+ } ) diff --git a/testfixtures/url.go b/testfixtures/url.go new file mode 100644 index 00000000..d8157eae --- /dev/null +++ b/testfixtures/url.go @@ -0,0 +1,324 @@ +// Copyright 2023-2024, Northwood Labs +// Copyright 2023-2024, Ryan Parman +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testfixtures // lint:no_dupe + +var ( + // URLParseTestTable is used by both the standard Go tests and also the + // Terraform acceptance tests. + // + URLParseTestTable = map[string]struct { // lint:no_dupe + Canonicalizer string + InputURL string + Href string + Protocol string + Scheme string + Username string + Password string + Hostname string + Host string + Port string + Path string + Search string + Query string + Hash string + Fragment string + DecodedPort int + }{ + // --------------------------------------------------------------------- + // No canonicalizer specified. + + "DEFAULT: HTTP://u:p@example.com:80/foo?q=1#bar": { + InputURL: "HTTP://u:p@example.com:80/foo?q=1#bar", + Href: "http://u:p@example.com/foo?q=1#bar", + Protocol: "http:", + Scheme: "http", + Username: "u", + Password: "p", + Hostname: "example.com", + Host: "example.com", + Port: "", + Path: "/foo", + Search: "?q=1", + Query: "q=1", + Hash: "#bar", + Fragment: "bar", + DecodedPort: 80, // lint:allow_raw_number + }, + "DEFAULT: HTTP://u:p@example.com/foo?q=1#bar": { + InputURL: "HTTP://u:p@example.com/foo?q=1#bar", + Href: "http://u:p@example.com/foo?q=1#bar", + Protocol: "http:", + Scheme: "http", + Username: "u", + Password: "p", + Hostname: "example.com", + Host: "example.com", + Port: "", + Path: "/foo", + Search: "?q=1", + Query: "q=1", + Hash: "#bar", + Fragment: "bar", + DecodedPort: 80, // lint:allow_raw_number + }, + "DEFAULT: HTTP://u:p@example.com:8080/foo?q=1#bar": { + InputURL: "HTTP://u:p@example.com:8080/foo?q=1#bar", + Href: "http://u:p@example.com:8080/foo?q=1#bar", + Protocol: "http:", + Scheme: "http", + Username: "u", + Password: "p", + Hostname: "example.com", + Host: "example.com:8080", + Port: "8080", + Path: "/foo", + Search: "?q=1", + Query: "q=1", + Hash: "#bar", + Fragment: "bar", + DecodedPort: 8080, // lint:allow_raw_number + }, + "DEFAULT: HTTPs://example.com": { + InputURL: "HTTPs://example.com", + Href: "https://example.com/", + Protocol: "https:", + Scheme: "https", + Username: "", + Password: "", + Hostname: "example.com", + Host: "example.com", + Port: "", + Path: "/", + Search: "", + Query: "", + Hash: "", + Fragment: "", + DecodedPort: 443, // lint:allow_raw_number + }, + + // --------------------------------------------------------------------- + // Standard canonicalizer specified. + + "STANDARD: HTTP://u:p@example.com:80/foo?q=1#bar": { + Canonicalizer: "standard", + InputURL: "HTTP://u:p@example.com:80/foo?q=1#bar", + Href: "http://u:p@example.com/foo?q=1#bar", + Protocol: "http:", + Scheme: "http", + Username: "u", + Password: "p", + Hostname: "example.com", + Host: "example.com", + Port: "", + Path: "/foo", + Search: "?q=1", + Query: "q=1", + Hash: "#bar", + Fragment: "bar", + DecodedPort: 80, // lint:allow_raw_number + }, + "STANDARD: HTTP://u:p@example.com/foo?q=1#bar": { + Canonicalizer: "standard", + InputURL: "HTTP://u:p@example.com/foo?q=1#bar", + Href: "http://u:p@example.com/foo?q=1#bar", + Protocol: "http:", + Scheme: "http", + Username: "u", + Password: "p", + Hostname: "example.com", + Host: "example.com", + Port: "", + Path: "/foo", + Search: "?q=1", + Query: "q=1", + Hash: "#bar", + Fragment: "bar", + DecodedPort: 80, // lint:allow_raw_number + }, + "STANDARD: HTTP://u:p@example.com:8080/foo?q=1#bar": { + Canonicalizer: "standard", + InputURL: "HTTP://u:p@example.com:8080/foo?q=1#bar", + Href: "http://u:p@example.com:8080/foo?q=1#bar", + Protocol: "http:", + Scheme: "http", + Username: "u", + Password: "p", + Hostname: "example.com", + Host: "example.com:8080", + Port: "8080", + Path: "/foo", + Search: "?q=1", + Query: "q=1", + Hash: "#bar", + Fragment: "bar", + DecodedPort: 8080, // lint:allow_raw_number + }, + "STANDARD: HTTPs://example.com": { + Canonicalizer: "standard", + InputURL: "HTTPs://example.com", + Href: "https://example.com/", + Protocol: "https:", + Scheme: "https", + Username: "", + Password: "", + Hostname: "example.com", + Host: "example.com", + Port: "", + Path: "/", + Search: "", + Query: "", + Hash: "", + Fragment: "", + DecodedPort: 443, // lint:allow_raw_number + }, + + // --------------------------------------------------------------------- + // GoogleSafe canonicalizer specified. + + "GOOGLE SAFE: HTTP://u:p@example.com:80/foo?q=1#bar": { + Canonicalizer: "google_safe_browsing", + InputURL: "HTTP://u:p@example.com:80/foo?q=1#bar", + Href: "http://u:p@example.com/foo?q=1", + Protocol: "http:", + Scheme: "http", + Username: "u", + Password: "p", + Hostname: "example.com", + Host: "example.com", + Port: "", + Path: "/foo", + Search: "?q=1", + Query: "q=1", + Hash: "", + Fragment: "", + DecodedPort: 80, // lint:allow_raw_number + }, + "GOOGLE SAFE: HTTP://u:p@example.com/foo?q=1#bar": { + Canonicalizer: "google_safe_browsing", + InputURL: "HTTP://u:p@example.com/foo?q=1#bar", + Href: "http://u:p@example.com/foo?q=1", + Protocol: "http:", + Scheme: "http", + Username: "u", + Password: "p", + Hostname: "example.com", + Host: "example.com", + Port: "", + Path: "/foo", + Search: "?q=1", + Query: "q=1", + Hash: "", + Fragment: "", + DecodedPort: 80, // lint:allow_raw_number + }, + "GOOGLE SAFE: HTTP://u:p@example.com:8080/foo?q=1#bar": { + Canonicalizer: "google_safe_browsing", + InputURL: "HTTP://u:p@example.com:8080/foo?q=1#bar", + Href: "http://u:p@example.com/foo?q=1", + Protocol: "http:", + Scheme: "http", + Username: "u", + Password: "p", + Hostname: "example.com", + Host: "example.com", + Port: "", + Path: "/foo", + Search: "?q=1", + Query: "q=1", + Hash: "", + Fragment: "", + DecodedPort: 8080, // lint:allow_raw_number + }, + "GOOGLE SAFE: HTTPs://example.com": { + Canonicalizer: "google_safe_browsing", + InputURL: "HTTPs://example.com", + Href: "https://example.com/", + Protocol: "https:", + Scheme: "https", + Username: "", + Password: "", + Hostname: "example.com", + Host: "example.com", + Port: "", + Path: "/", + Search: "", + Query: "", + Hash: "", + Fragment: "", + DecodedPort: 443, // lint:allow_raw_number + }, + } + + // URLDecodeTestTable is used by both the standard Go tests and also the + // Terraform acceptance tests. + // + URLDecodeTestTable = map[string]struct { // lint:no_dupe + Input string + Expected string + ExpectedErr bool + }{ + "abc123-_": { + Input: "abc123-_", + Expected: "abc123-_", + ExpectedErr: false, + }, + "foo%3Abar%40localhost%3Ffoo%3Dbar%26bar%3Dbaz": { + Input: "foo%3Abar%40localhost%3Ffoo%3Dbar%26bar%3Dbaz", + Expected: "foo:bar@localhost?foo=bar&bar=baz", + ExpectedErr: false, + }, + "mailto%3Aemail%3Fsubject%3Dthis%2Bis%2Bmy%2Bsubject": { + Input: "mailto%3Aemail%3Fsubject%3Dthis%2Bis%2Bmy%2Bsubject", + Expected: "mailto:email?subject=this+is+my+subject", + ExpectedErr: false, + }, + "foo%2Fbar": { + Input: "foo%2Fbar", + Expected: "foo/bar", + ExpectedErr: false, + }, + "foo% bar": { + Input: "foo% bar", + Expected: "", + ExpectedErr: true, + }, + "foo%2 bar": { + Input: "foo%2 bar", + Expected: "", + ExpectedErr: true, + }, + "%GGfoo%2bar": { + Input: "%GGfoo%2bar", + Expected: "", + ExpectedErr: true, + }, + "foo%00, bar!": { + Input: "foo%00, bar!", + Expected: "foo\x00, bar!", + ExpectedErr: false, + }, + "hello%20%E4%B8%96%E7%95%8C": { + Input: "hello%20%E4%B8%96%E7%95%8C", // Unicode character support + Expected: "hello 世界", + ExpectedErr: false, + }, + "hello%20%D8%AF%D9%86%DB%8C%D8%A7": { + Input: "hello%20%D8%AF%D9%86%DB%8C%D8%A7", // Unicode character support + Expected: "hello دنیا", + ExpectedErr: false, + }, + } +) diff --git a/testfixtures/url_parse.go b/testfixtures/url_parse.go deleted file mode 100644 index 45f46044..00000000 --- a/testfixtures/url_parse.go +++ /dev/null @@ -1,262 +0,0 @@ -// Copyright 2023-2024, Northwood Labs -// Copyright 2023-2024, Ryan Parman -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package testfixtures // lint:no_dupe - -// URLParseTestTable is used by both the standard Go tests and also the -// Terraform acceptance tests. -// -var URLParseTestTable = map[string]struct { // lint:no_dupe - Canonicalizer string - InputURL string - Href string - Protocol string - Scheme string - Username string - Password string - Hostname string - Host string - Port string - Path string - Search string - Query string - Hash string - Fragment string - DecodedPort int -}{ - // --------------------------------------------------------------------- - // No canonicalizer specified. - - "DEFAULT: HTTP://u:p@example.com:80/foo?q=1#bar": { - InputURL: "HTTP://u:p@example.com:80/foo?q=1#bar", - Href: "http://u:p@example.com/foo?q=1#bar", - Protocol: "http:", - Scheme: "http", - Username: "u", - Password: "p", - Hostname: "example.com", - Host: "example.com", - Port: "", - Path: "/foo", - Search: "?q=1", - Query: "q=1", - Hash: "#bar", - Fragment: "bar", - DecodedPort: 80, // lint:allow_raw_number - }, - "DEFAULT: HTTP://u:p@example.com/foo?q=1#bar": { - InputURL: "HTTP://u:p@example.com/foo?q=1#bar", - Href: "http://u:p@example.com/foo?q=1#bar", - Protocol: "http:", - Scheme: "http", - Username: "u", - Password: "p", - Hostname: "example.com", - Host: "example.com", - Port: "", - Path: "/foo", - Search: "?q=1", - Query: "q=1", - Hash: "#bar", - Fragment: "bar", - DecodedPort: 80, // lint:allow_raw_number - }, - "DEFAULT: HTTP://u:p@example.com:8080/foo?q=1#bar": { - InputURL: "HTTP://u:p@example.com:8080/foo?q=1#bar", - Href: "http://u:p@example.com:8080/foo?q=1#bar", - Protocol: "http:", - Scheme: "http", - Username: "u", - Password: "p", - Hostname: "example.com", - Host: "example.com:8080", - Port: "8080", - Path: "/foo", - Search: "?q=1", - Query: "q=1", - Hash: "#bar", - Fragment: "bar", - DecodedPort: 8080, // lint:allow_raw_number - }, - "DEFAULT: HTTPs://example.com": { - InputURL: "HTTPs://example.com", - Href: "https://example.com/", - Protocol: "https:", - Scheme: "https", - Username: "", - Password: "", - Hostname: "example.com", - Host: "example.com", - Port: "", - Path: "/", - Search: "", - Query: "", - Hash: "", - Fragment: "", - DecodedPort: 443, // lint:allow_raw_number - }, - - // --------------------------------------------------------------------- - // Standard canonicalizer specified. - - "STANDARD: HTTP://u:p@example.com:80/foo?q=1#bar": { - Canonicalizer: "standard", - InputURL: "HTTP://u:p@example.com:80/foo?q=1#bar", - Href: "http://u:p@example.com/foo?q=1#bar", - Protocol: "http:", - Scheme: "http", - Username: "u", - Password: "p", - Hostname: "example.com", - Host: "example.com", - Port: "", - Path: "/foo", - Search: "?q=1", - Query: "q=1", - Hash: "#bar", - Fragment: "bar", - DecodedPort: 80, // lint:allow_raw_number - }, - "STANDARD: HTTP://u:p@example.com/foo?q=1#bar": { - Canonicalizer: "standard", - InputURL: "HTTP://u:p@example.com/foo?q=1#bar", - Href: "http://u:p@example.com/foo?q=1#bar", - Protocol: "http:", - Scheme: "http", - Username: "u", - Password: "p", - Hostname: "example.com", - Host: "example.com", - Port: "", - Path: "/foo", - Search: "?q=1", - Query: "q=1", - Hash: "#bar", - Fragment: "bar", - DecodedPort: 80, // lint:allow_raw_number - }, - "STANDARD: HTTP://u:p@example.com:8080/foo?q=1#bar": { - Canonicalizer: "standard", - InputURL: "HTTP://u:p@example.com:8080/foo?q=1#bar", - Href: "http://u:p@example.com:8080/foo?q=1#bar", - Protocol: "http:", - Scheme: "http", - Username: "u", - Password: "p", - Hostname: "example.com", - Host: "example.com:8080", - Port: "8080", - Path: "/foo", - Search: "?q=1", - Query: "q=1", - Hash: "#bar", - Fragment: "bar", - DecodedPort: 8080, // lint:allow_raw_number - }, - "STANDARD: HTTPs://example.com": { - Canonicalizer: "standard", - InputURL: "HTTPs://example.com", - Href: "https://example.com/", - Protocol: "https:", - Scheme: "https", - Username: "", - Password: "", - Hostname: "example.com", - Host: "example.com", - Port: "", - Path: "/", - Search: "", - Query: "", - Hash: "", - Fragment: "", - DecodedPort: 443, // lint:allow_raw_number - }, - - // --------------------------------------------------------------------- - // GoogleSafe canonicalizer specified. - - "GOOGLE SAFE: HTTP://u:p@example.com:80/foo?q=1#bar": { - Canonicalizer: "google_safe_browsing", - InputURL: "HTTP://u:p@example.com:80/foo?q=1#bar", - Href: "http://u:p@example.com/foo?q=1", - Protocol: "http:", - Scheme: "http", - Username: "u", - Password: "p", - Hostname: "example.com", - Host: "example.com", - Port: "", - Path: "/foo", - Search: "?q=1", - Query: "q=1", - Hash: "", - Fragment: "", - DecodedPort: 80, // lint:allow_raw_number - }, - "GOOGLE SAFE: HTTP://u:p@example.com/foo?q=1#bar": { - Canonicalizer: "google_safe_browsing", - InputURL: "HTTP://u:p@example.com/foo?q=1#bar", - Href: "http://u:p@example.com/foo?q=1", - Protocol: "http:", - Scheme: "http", - Username: "u", - Password: "p", - Hostname: "example.com", - Host: "example.com", - Port: "", - Path: "/foo", - Search: "?q=1", - Query: "q=1", - Hash: "", - Fragment: "", - DecodedPort: 80, // lint:allow_raw_number - }, - "GOOGLE SAFE: HTTP://u:p@example.com:8080/foo?q=1#bar": { - Canonicalizer: "google_safe_browsing", - InputURL: "HTTP://u:p@example.com:8080/foo?q=1#bar", - Href: "http://u:p@example.com/foo?q=1", - Protocol: "http:", - Scheme: "http", - Username: "u", - Password: "p", - Hostname: "example.com", - Host: "example.com", - Port: "", - Path: "/foo", - Search: "?q=1", - Query: "q=1", - Hash: "", - Fragment: "", - DecodedPort: 8080, // lint:allow_raw_number - }, - "GOOGLE SAFE: HTTPs://example.com": { - Canonicalizer: "google_safe_browsing", - InputURL: "HTTPs://example.com", - Href: "https://example.com/", - Protocol: "https:", - Scheme: "https", - Username: "", - Password: "", - Hostname: "example.com", - Host: "example.com", - Port: "", - Path: "/", - Search: "", - Query: "", - Hash: "", - Fragment: "", - DecodedPort: 443, // lint:allow_raw_number - }, -} diff --git a/unit-coverage.png b/unit-coverage.png index a30fbf1c..2dc9bfcc 100644 Binary files a/unit-coverage.png and b/unit-coverage.png differ diff --git a/unit-coverage.svg b/unit-coverage.svg index 2c981ffe..dc7f2fed 100644 --- a/unit-coverage.svg +++ b/unit-coverage.svg @@ -7,7 +7,7 @@ > - + - + caps.go @@ -33,12 +33,12 @@ - + env_ensure.go @@ -46,12 +46,12 @@ - + homedir.go @@ -59,12 +59,12 @@ - + int_pad.go @@ -72,12 +72,12 @@ - + str_iterative_replace.go @@ -85,12 +85,12 @@ - + str_pad.go @@ -98,7 +98,7 @@ - + - + url.go