-
Notifications
You must be signed in to change notification settings - Fork 163
feat(path): add NewPathFromURI for ipfs:// URIs #1182
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
+180
−0
Merged
Changes from 2 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
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,57 @@ | ||
| package path | ||
|
|
||
| import "strings" | ||
|
|
||
| // NewPathFromURI is like [NewPath] but also accepts a native IPFS URI, rewriting | ||
| // it to the equivalent canonical content path before parsing: | ||
| // | ||
| // ipfs://{cid}/sub -> /ipfs/{cid}/sub | ||
| // ipns://{name}/sub -> /ipns/{name}/sub | ||
| // ipld://{cid}/sub -> /ipld/{cid}/sub | ||
| // | ||
| // The schemeless ipfs:{cid}, ipns:{name}, and ipld:{cid} forms are accepted too. | ||
| // The scheme is matched case-insensitively over ASCII, as URI schemes are | ||
| // case-insensitive (RFC 3986). Everything after the scheme is preserved | ||
| // byte-for-byte, so a case-sensitive CIDv0 root or a DNSLink name is not altered. | ||
| // A string that is already a content path, or is not an IPFS URI, is handed to | ||
| // [NewPath] unchanged. | ||
| // | ||
| // Use this only at input boundaries where values may be copied from a browser or | ||
| // another tool. [NewPath] stays strict, so contexts that must accept canonical | ||
| // content paths only (such as DNSLink records) are not loosened by this helper. | ||
| func NewPathFromURI(str string) (Path, error) { | ||
| return NewPath(normalizeURIScheme(str)) | ||
| } | ||
|
|
||
| // normalizeURIScheme rewrites a native IPFS URI into a canonical content path. | ||
| // A string that is not an IPFS URI is returned unchanged. See [NewPathFromURI]. | ||
| func normalizeURIScheme(str string) string { | ||
| for _, ns := range [...]string{IPFSNamespace, IPNSNamespace, IPLDNamespace} { | ||
| if hasURIScheme(str, ns) { | ||
| rest := strings.TrimPrefix(str[len(ns)+1:], "//") // drop the optional "//" authority separator | ||
| return "/" + ns + "/" + rest | ||
| } | ||
| } | ||
| return str | ||
| } | ||
|
|
||
| // hasURIScheme reports whether str begins with the scheme ns followed by ":" | ||
| // (e.g. "ipfs:"). ns is matched case-insensitively over ASCII. | ||
| func hasURIScheme(str, ns string) bool { | ||
| if len(str) <= len(ns) || str[len(ns)] != ':' { | ||
| return false | ||
| } | ||
| for i := 0; i < len(ns); i++ { | ||
| if toLowerASCII(str[i]) != ns[i] { | ||
| return false | ||
| } | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| func toLowerASCII(b byte) byte { | ||
| if b >= 'A' && b <= 'Z' { | ||
| return b + ('a' - 'A') | ||
| } | ||
| return b | ||
| } | ||
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,118 @@ | ||
| package path | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| func TestNewPathFromURI(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| t.Run("Valid URIs", func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| testCases := []struct { | ||
| src string | ||
| canonical string | ||
| namespace string | ||
| mutable bool | ||
| }{ | ||
| // ipfs:// with CIDv1, with and without a sub-path | ||
| {"ipfs://bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", "/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", IPFSNamespace, false}, | ||
| {"ipfs://bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a/b", "/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a/b", IPFSNamespace, false}, | ||
|
|
||
| // CIDv0 is base58 and case-sensitive: the root must survive untouched. | ||
| {"ipfs://QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", IPFSNamespace, false}, | ||
| {"ipfs://QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a", "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a", IPFSNamespace, false}, | ||
|
|
||
| // ipns:// with an IPNS key and with a DNSLink name | ||
| {"ipns://bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", "/ipns/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", IPNSNamespace, true}, | ||
| {"ipns://docs.ipfs.tech/concepts", "/ipns/docs.ipfs.tech/concepts", IPNSNamespace, true}, | ||
|
|
||
| // ipld:// maps to the /ipld namespace | ||
| {"ipld://bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a", "/ipld/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a", IPLDNamespace, false}, | ||
|
|
||
| // Schemeless forms: ipfs:{cid}, ipns:{name}, ipld:{cid} | ||
| {"ipfs:bafkqaaa", "/ipfs/bafkqaaa", IPFSNamespace, false}, | ||
| {"ipns:docs.ipfs.tech", "/ipns/docs.ipfs.tech", IPNSNamespace, true}, | ||
| {"ipld:bafkqaaa", "/ipld/bafkqaaa", IPLDNamespace, false}, | ||
|
|
||
| // Scheme is case-insensitive (RFC 3986); the rest is preserved. | ||
| {"IPFS://bafkqaaa", "/ipfs/bafkqaaa", IPFSNamespace, false}, | ||
| {"IpNs://docs.ipfs.tech", "/ipns/docs.ipfs.tech", IPNSNamespace, true}, | ||
|
|
||
| // Trailing slash is preserved, mirroring /ipfs/cid/ behaviour. | ||
| {"ipfs://bafkqaaa/", "/ipfs/bafkqaaa/", IPFSNamespace, false}, | ||
|
|
||
| // Cleaning still applies after normalization. | ||
| {"ipfs://bafkqaaa/a/b/../c", "/ipfs/bafkqaaa/a/c", IPFSNamespace, false}, | ||
|
|
||
| // A canonical path passes straight through to NewPath. | ||
| {"/ipfs/bafkqaaa", "/ipfs/bafkqaaa", IPFSNamespace, false}, | ||
| {"/ipns/docs.ipfs.tech", "/ipns/docs.ipfs.tech", IPNSNamespace, true}, | ||
| } | ||
|
|
||
| for _, testCase := range testCases { | ||
| p, err := NewPathFromURI(testCase.src) | ||
| assert.NoErrorf(t, err, "input %q", testCase.src) | ||
| if err != nil { | ||
| continue | ||
| } | ||
| assert.Equalf(t, testCase.canonical, p.String(), "input %q", testCase.src) | ||
| assert.Equalf(t, testCase.namespace, p.Namespace(), "input %q", testCase.src) | ||
| assert.Equalf(t, testCase.mutable, p.Mutable(), "input %q", testCase.src) | ||
| } | ||
| }) | ||
|
|
||
| t.Run("Invalid URIs still error", func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| // Empty-after-scheme reduces to "/ipfs/" etc., which NewPath rejects. | ||
| for _, src := range []string{"ipfs:", "ipns:", "ipld:", "ipfs://", "ipns://", "IPFS://"} { | ||
| _, err := NewPathFromURI(src) | ||
| assert.ErrorIsf(t, err, ErrInsufficientComponents, "input %q", src) | ||
| assert.ErrorIsf(t, err, &ErrInvalidPath{}, "input %q always an ErrInvalidPath", src) | ||
| } | ||
|
|
||
| // A malformed CID still fails (as an ErrInvalidPath wrapping the CID error). | ||
| _, err := NewPathFromURI("ipfs://notacid") | ||
| assert.ErrorIs(t, err, &ErrInvalidPath{}) | ||
| }) | ||
|
|
||
| t.Run("Returns ImmutablePath for ipfs:// URIs", func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| p, err := NewPathFromURI("ipfs://bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a") | ||
| assert.NoError(t, err) | ||
| assert.IsType(t, ImmutablePath{}, p) | ||
| }) | ||
|
|
||
| // A literal value that merely starts with "ipns"/"ipfs" but has no ":" right | ||
| // after the namespace must not be rewritten. | ||
| t.Run("Does not misfire on non-URI input", func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| for _, src := range []string{"ipfsfile", "ipns-notes.txt", "ipfsbar:baz", "ipns", "/ipfs/bafkqaaa"} { | ||
| assert.Equalf(t, src, normalizeURIScheme(src), "input %q must be unchanged", src) | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| // TestNewPathStaysStrict guards the security boundary: NewPath itself must keep | ||
| // rejecting native IPFS URIs, so contexts that parse untrusted strings (such as | ||
| // DNSLink records) are not loosened. Only NewPathFromURI accepts URIs. | ||
| func TestNewPathStaysStrict(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| for _, src := range []string{ | ||
| "ipfs://bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", | ||
| "ipns://attacker.example", | ||
| "ipfs:bafkqaaa", | ||
| "ipns:attacker.example", | ||
| } { | ||
| _, err := NewPath(src) | ||
| assert.Errorf(t, err, "NewPath must reject URI %q", src) | ||
| assert.ErrorIsf(t, err, &ErrInvalidPath{}, "input %q", src) | ||
| } | ||
| } |
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.