-
Notifications
You must be signed in to change notification settings - Fork 101
feat: add URL detection and integrity hash parsing to harness package #1095
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| package harness | ||
|
|
||
| import ( | ||
| "net/url" | ||
| "path/filepath" | ||
| "strings" | ||
| ) | ||
|
|
||
| // IsURL returns true if s is a valid HTTPS URL suitable for remote resource references. | ||
| func IsURL(s string) bool { | ||
| if s == "" { | ||
| return false | ||
| } | ||
| u, err := url.Parse(s) | ||
| if err != nil || u.Scheme != "https" { | ||
| return false | ||
| } | ||
| if u.Host == "" || u.User != nil { | ||
| return false | ||
| } | ||
| if u.Hostname() == "" { | ||
| return false | ||
| } | ||
| // Belt-and-suspenders: reject userinfo that url.Parse may not catch in all edge cases | ||
| if strings.Contains(u.Host, "@") { | ||
| return false | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| // IsAbsPath returns true if s is an absolute file path. | ||
| func IsAbsPath(s string) bool { | ||
| return filepath.IsAbs(s) | ||
| } | ||
|
|
||
| // IsRelPath returns true if s is a non-empty relative file path (not a URL and not absolute). | ||
| func IsRelPath(s string) bool { | ||
| return s != "" && !IsURL(s) && !IsAbsPath(s) | ||
| } | ||
|
|
||
| // ParseIntegrityHash extracts the SHA256 hash from a URL fragment (#sha256=...). | ||
| // Returns the URL without the fragment, the hash value, and whether a valid hash was found. | ||
| // The hash is normalized to lowercase; both "sha256=ABC..." and "sha256=abc..." are accepted. | ||
| func ParseIntegrityHash(rawURL string) (cleanURL, hash string, hasHash bool) { | ||
|
ggallen marked this conversation as resolved.
|
||
| idx := strings.LastIndex(rawURL, "#") | ||
| if idx == -1 { | ||
| return rawURL, "", false | ||
| } | ||
| fragment := rawURL[idx+1:] | ||
| if !strings.HasPrefix(fragment, "sha256=") { | ||
| return rawURL, "", false | ||
| } | ||
| hash = strings.ToLower(strings.TrimPrefix(fragment, "sha256=")) | ||
| if len(hash) != 64 { | ||
| return rawURL, "", false | ||
| } | ||
| for _, c := range hash { | ||
| if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) { | ||
| return rawURL, "", false | ||
| } | ||
| } | ||
| return rawURL[:idx], hash, true | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| package harness | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| func TestIsURL(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| input string | ||
| want bool | ||
| }{ | ||
| {"valid https", "https://example.com/path/file.md", true}, | ||
| {"valid https with port", "https://example.com:8443/path", true}, | ||
| {"valid https with query", "https://example.com/path?q=1", true}, | ||
| {"valid https with fragment", "https://example.com/path#sha256=abc", true}, | ||
| {"http rejected", "http://example.com/path", false}, | ||
| {"file scheme rejected", "file:///etc/passwd", false}, | ||
| {"ftp rejected", "ftp://example.com/file", false}, | ||
| {"empty string", "", false}, | ||
| {"relative path", "agents/code.md", false}, | ||
| {"relative path with dots", "../agents/code.md", false}, | ||
| {"absolute path", "/opt/agents/code.md", false}, | ||
| {"empty host", "https:///path", false}, | ||
| {"scheme only", "https://", false}, | ||
| {"userinfo", "https://user:pass@example.com/path", false}, | ||
| {"userinfo user only", "https://user@example.com/path", false}, | ||
| {"plain text", "not a url at all", false}, | ||
| {"just a word", "https", false}, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| assert.Equal(t, tt.want, IsURL(tt.input)) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestIsAbsPath(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| input string | ||
| want bool | ||
| }{ | ||
| {"absolute unix", "/opt/agents/code.md", true}, | ||
| {"relative", "agents/code.md", false}, | ||
| {"relative with dots", "../agents/code.md", false}, | ||
| {"url", "https://example.com/path", false}, | ||
| {"empty", "", false}, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| assert.Equal(t, tt.want, IsAbsPath(tt.input)) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestIsRelPath(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| input string | ||
| want bool | ||
| }{ | ||
| {"relative", "agents/code.md", true}, | ||
| {"relative with dots", "../agents/code.md", true}, | ||
| {"dot slash", "./agents/code.md", true}, | ||
| {"bare filename", "code.md", true}, | ||
| {"empty string", "", false}, | ||
| {"absolute path", "/opt/agents/code.md", false}, | ||
| {"url", "https://example.com/path", false}, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| assert.Equal(t, tt.want, IsRelPath(tt.input)) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestParseIntegrityHash(t *testing.T) { | ||
| validHash := "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| input string | ||
| wantURL string | ||
| wantHash string | ||
| wantHasHash bool | ||
| }{ | ||
| { | ||
| name: "valid hash", | ||
| input: "https://example.com/file.md#sha256=" + validHash, | ||
| wantURL: "https://example.com/file.md", | ||
| wantHash: validHash, | ||
| wantHasHash: true, | ||
| }, | ||
| { | ||
| name: "valid hash with query params", | ||
| input: "https://example.com/file.md?v=1#sha256=" + validHash, | ||
| wantURL: "https://example.com/file.md?v=1", | ||
| wantHash: validHash, | ||
| wantHasHash: true, | ||
| }, | ||
| { | ||
| name: "no fragment", | ||
| input: "https://example.com/file.md", | ||
| wantURL: "https://example.com/file.md", | ||
| wantHash: "", | ||
| wantHasHash: false, | ||
| }, | ||
| { | ||
| name: "non-sha256 fragment", | ||
| input: "https://example.com/file.md#section1", | ||
| wantURL: "https://example.com/file.md#section1", | ||
| wantHash: "", | ||
| wantHasHash: false, | ||
| }, | ||
| { | ||
| name: "wrong prefix", | ||
| input: "https://example.com/file.md#md5=abc123", | ||
| wantURL: "https://example.com/file.md#md5=abc123", | ||
| wantHash: "", | ||
| wantHasHash: false, | ||
| }, | ||
| { | ||
| name: "hash too short 63 chars", | ||
| input: "https://example.com/file.md#sha256=" + validHash[:63], | ||
| wantURL: "https://example.com/file.md#sha256=" + validHash[:63], | ||
| wantHash: "", | ||
| wantHasHash: false, | ||
| }, | ||
| { | ||
| name: "hash too long 65 chars", | ||
| input: "https://example.com/file.md#sha256=" + validHash + "a", | ||
| wantURL: "https://example.com/file.md#sha256=" + validHash + "a", | ||
| wantHash: "", | ||
| wantHasHash: false, | ||
| }, | ||
| { | ||
| name: "uppercase hex normalized", | ||
| input: "https://example.com/file.md#sha256=ABCDEF0123456789abcdef0123456789abcdef0123456789abcdef0123456789", | ||
| wantURL: "https://example.com/file.md", | ||
| wantHash: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", | ||
| wantHasHash: true, | ||
| }, | ||
| { | ||
| name: "empty hash value", | ||
| input: "https://example.com/file.md#sha256=", | ||
| wantURL: "https://example.com/file.md#sha256=", | ||
| wantHash: "", | ||
| wantHasHash: false, | ||
| }, | ||
| { | ||
| name: "path traversal in hash rejected", | ||
| input: "https://example.com/file.md#sha256=../../../../../../etc/shadow//////////////////////////////////", | ||
| wantURL: "https://example.com/file.md#sha256=../../../../../../etc/shadow//////////////////////////////////", | ||
| wantHash: "", | ||
| wantHasHash: false, | ||
| }, | ||
| { | ||
| name: "relative path unchanged", | ||
| input: "agents/code.md", | ||
| wantURL: "agents/code.md", | ||
| wantHash: "", | ||
| wantHasHash: false, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| gotURL, gotHash, gotHasHash := ParseIntegrityHash(tt.input) | ||
| assert.Equal(t, tt.wantURL, gotURL) | ||
| assert.Equal(t, tt.wantHash, gotHash) | ||
| assert.Equal(t, tt.wantHasHash, gotHasHash) | ||
| }) | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.