diff --git a/gateway/blocks_backend.go b/gateway/blocks_backend.go index 61a38df889..e5ddf87626 100644 --- a/gateway/blocks_backend.go +++ b/gateway/blocks_backend.go @@ -259,13 +259,12 @@ func (bb *BlocksBackend) GetCAR(ctx context.Context, p path.ImmutablePath, param // Setup the UnixFS resolver. f := newNodeGetterFetcherSingleUseFactory(ctx, blockGetter) pathResolver := resolver.NewBasicResolver(f) - ip := ipfspath.FromString(p.String()) - _, _, err = pathResolver.ResolveToLastNode(ctx, ip) + _, _, err = pathResolver.ResolveToLastNode(ctx, p) if isErrNotFound(err) { return ContentPathMetadata{ PathSegmentRoots: nil, - LastSegment: ifacepath.NewResolvedPath(ip, rootCid, rootCid, ""), + LastSegment: path.NewIPFSPath(rootCid), ContentType: "", }, io.NopCloser(&buf), nil } diff --git a/gateway/gateway_test.go b/gateway/gateway_test.go index 3326eacae3..785c338ca6 100644 --- a/gateway/gateway_test.go +++ b/gateway/gateway_test.go @@ -58,10 +58,10 @@ func TestGatewayGet(t *testing.T) { text string }{ {"127.0.0.1:8080", "/", http.StatusNotFound, "404 page not found\n"}, - {"127.0.0.1:8080", "/ipfs", http.StatusBadRequest, "invalid path \"/ipfs/\": not enough path components\n"}, - {"127.0.0.1:8080", "/ipns", http.StatusBadRequest, "invalid path \"/ipns/\": not enough path components\n"}, + {"127.0.0.1:8080", "/ipfs", http.StatusBadRequest, "invalid path \"/ipfs/\": path does not have enough components\n"}, + {"127.0.0.1:8080", "/ipns", http.StatusBadRequest, "invalid path \"/ipns/\": path does not have enough components\n"}, {"127.0.0.1:8080", "/" + k.Cid().String(), http.StatusNotFound, "404 page not found\n"}, - {"127.0.0.1:8080", "/ipfs/this-is-not-a-cid", http.StatusBadRequest, "invalid path \"/ipfs/this-is-not-a-cid\": invalid CID: invalid cid: illegal base32 data at input byte 3\n"}, + {"127.0.0.1:8080", "/ipfs/this-is-not-a-cid", http.StatusBadRequest, "invalid path \"/ipfs/this-is-not-a-cid\": invalid cid: illegal base32 data at input byte 3\n"}, {"127.0.0.1:8080", k.String(), http.StatusOK, "fnord"}, {"127.0.0.1:8080", "/ipns/nxdomain.example.com", http.StatusInternalServerError, "failed to resolve /ipns/nxdomain.example.com: " + namesys.ErrResolveFailed.Error() + "\n"}, {"127.0.0.1:8080", "/ipns/%0D%0A%0D%0Ahello", http.StatusInternalServerError, "failed to resolve /ipns/\\r\\n\\r\\nhello: " + namesys.ErrResolveFailed.Error() + "\n"}, diff --git a/path/error.go b/path/error.go index cad4348a86..34f5993804 100644 --- a/path/error.go +++ b/path/error.go @@ -1,20 +1,27 @@ package path import ( + "errors" "fmt" ) +var ( + ErrExpectedImmutable = errors.New("path was expected to be immutable") + ErrInsufficientComponents = errors.New("path does not have enough components") + ErrUnknownNamespace = errors.New("unknown namespace") +) + type ErrInvalidPath struct { - error error - path string + err error + path string } func (e ErrInvalidPath) Error() string { - return fmt.Sprintf("invalid path %q: %s", e.path, e.error) + return fmt.Sprintf("invalid path %q: %s", e.path, e.err) } func (e ErrInvalidPath) Unwrap() error { - return e.error + return e.err } func (e ErrInvalidPath) Is(err error) bool { diff --git a/path/error_test.go b/path/error_test.go index 07aab64081..512195e04a 100644 --- a/path/error_test.go +++ b/path/error_test.go @@ -6,11 +6,11 @@ import ( ) func TestErrorIs(t *testing.T) { - if !errors.Is(ErrInvalidPath{path: "foo", error: errors.New("bar")}, ErrInvalidPath{}) { + if !errors.Is(ErrInvalidPath{path: "foo", err: errors.New("bar")}, ErrInvalidPath{}) { t.Fatal("error must be error") } - if !errors.Is(&ErrInvalidPath{path: "foo", error: errors.New("bar")}, ErrInvalidPath{}) { + if !errors.Is(&ErrInvalidPath{path: "foo", err: errors.New("bar")}, ErrInvalidPath{}) { t.Fatal("pointer to error must be error") } } diff --git a/path/path.go b/path/path.go index 20c1e92793..75eb4d3d09 100644 --- a/path/path.go +++ b/path/path.go @@ -111,13 +111,13 @@ type immutablePath struct { func NewImmutablePath(p Path) (ImmutablePath, error) { if p.Namespace().Mutable() { - return nil, fmt.Errorf("path was expected to be immutable: %s", p.String()) + return nil, ErrInvalidPath{err: ErrExpectedImmutable, path: p.String()} } segments := p.Segments() cid, err := cid.Decode(segments[1]) if err != nil { - return nil, &ErrInvalidPath{error: fmt.Errorf("invalid CID: %w", err), path: p.String()} + return nil, &ErrInvalidPath{err: err, path: p.String()} } return immutablePath{path: p, cid: cid}, nil @@ -149,7 +149,7 @@ func (ip immutablePath) Remainder() string { // NewIPFSPath returns a new "/ipfs" path with the provided CID. func NewIPFSPath(cid cid.Cid) ImmutablePath { - return &immutablePath{ + return immutablePath{ path: path{ str: fmt.Sprintf("/%s/%s", IPFSNamespace, cid.String()), namespace: IPFSNamespace, @@ -160,7 +160,7 @@ func NewIPFSPath(cid cid.Cid) ImmutablePath { // NewIPLDPath returns a new "/ipld" path with the provided CID. func NewIPLDPath(cid cid.Cid) ImmutablePath { - return &immutablePath{ + return immutablePath{ path: path{ str: fmt.Sprintf("/%s/%s", IPLDNamespace, cid.String()), namespace: IPLDNamespace, @@ -169,10 +169,10 @@ func NewIPLDPath(cid cid.Cid) ImmutablePath { } } -// NewPath takes the given string and returns a well-forme and sanitized [Path]. +// NewPath takes the given string and returns a well-formed and sanitized [Path]. // The given string is cleaned through [gopath.Clean], but preserving the final // trailing slash. This function returns an error when the given string is not -// a valid path. +// a valid content path. func NewPath(str string) (Path, error) { cleaned := gopath.Clean(str) components := strings.Split(cleaned, "/") @@ -186,18 +186,18 @@ func NewPath(str string) (Path, error) { // components: [" " "{namespace}" "{element}"]. The first component must therefore // be empty. if len(components) < 3 || components[0] != "" { - return nil, &ErrInvalidPath{error: fmt.Errorf("not enough path components"), path: str} + return nil, &ErrInvalidPath{err: ErrInsufficientComponents, path: str} } switch components[1] { case "ipfs", "ipld": if components[2] == "" { - return nil, &ErrInvalidPath{error: fmt.Errorf("not enough path components"), path: str} + return nil, &ErrInvalidPath{err: ErrInsufficientComponents, path: str} } cid, err := cid.Decode(components[2]) if err != nil { - return nil, &ErrInvalidPath{error: fmt.Errorf("invalid CID: %w", err), path: str} + return nil, &ErrInvalidPath{err: err, path: str} } ns := IPFSNamespace @@ -214,7 +214,7 @@ func NewPath(str string) (Path, error) { }, nil case "ipns": if components[2] == "" { - return nil, &ErrInvalidPath{error: fmt.Errorf("not enough path components"), path: str} + return nil, &ErrInvalidPath{err: ErrInsufficientComponents, path: str} } return path{ @@ -222,7 +222,7 @@ func NewPath(str string) (Path, error) { namespace: IPNSNamespace, }, nil default: - return nil, &ErrInvalidPath{error: fmt.Errorf("unknown namespace %q", components[1]), path: str} + return nil, &ErrInvalidPath{err: fmt.Errorf("%w: %q", ErrUnknownNamespace, components[1]), path: str} } } diff --git a/path/path_test.go b/path/path_test.go index 45b12f0624..1a77fcf809 100644 --- a/path/path_test.go +++ b/path/path_test.go @@ -3,11 +3,16 @@ package path import ( "testing" + "github.com/ipfs/go-cid" "github.com/stretchr/testify/assert" ) func TestNewPath(t *testing.T) { + t.Parallel() + t.Run("Valid Paths", func(t *testing.T) { + t.Parallel() + testCases := []struct { src string canonical string @@ -66,39 +71,44 @@ func TestNewPath(t *testing.T) { }) t.Run("Invalid Paths", func(t *testing.T) { + t.Parallel() + testCases := []struct { src string err error }{ - {"QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", ErrInvalidPath{}}, - {"QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a", ErrInvalidPath{}}, - {"bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a", ErrInvalidPath{}}, - {"/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", ErrInvalidPath{}}, - {"/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a", ErrInvalidPath{}}, - {"/ipfs/foo", ErrInvalidPath{}}, - {"/ipfs/", ErrInvalidPath{}}, - {"ipfs/", ErrInvalidPath{}}, - {"ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", ErrInvalidPath{}}, + {"QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", ErrInsufficientComponents}, + {"QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a", ErrInsufficientComponents}, + {"bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a", ErrInsufficientComponents}, + {"/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", ErrInsufficientComponents}, + {"/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a", ErrUnknownNamespace}, + {"/ipfs/foo", cid.ErrInvalidCid{}}, + {"/ipfs/", ErrInsufficientComponents}, + {"ipfs/", ErrInsufficientComponents}, + {"ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", ErrInsufficientComponents}, {"/ipld/foo", ErrInvalidPath{}}, - {"/ipld/", ErrInvalidPath{}}, - {"ipld/", ErrInvalidPath{}}, - {"ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", ErrInvalidPath{}}, - {"/ipns", ErrInvalidPath{}}, - {"/ipfs/", ErrInvalidPath{}}, - {"/ipns/", ErrInvalidPath{}}, - {"/ipld/", ErrInvalidPath{}}, - {"/ipfs", ErrInvalidPath{}}, - {"/testfs", ErrInvalidPath{}}, - {"/", ErrInvalidPath{}}, + {"/ipld/", ErrInsufficientComponents}, + {"ipld/", ErrInsufficientComponents}, + {"ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", ErrInsufficientComponents}, + {"/ipns", ErrInsufficientComponents}, + {"/ipfs/", ErrInsufficientComponents}, + {"/ipns/", ErrInsufficientComponents}, + {"/ipld/", ErrInsufficientComponents}, + {"/ipfs", ErrInsufficientComponents}, + {"/testfs", ErrInsufficientComponents}, + {"/", ErrInsufficientComponents}, } for _, testCase := range testCases { _, err := NewPath(testCase.src) assert.ErrorIs(t, err, testCase.err) + assert.ErrorIs(t, err, ErrInvalidPath{}) // Always an ErrInvalidPath! } }) t.Run("Returns ImmutablePath for IPFS and IPLD Paths", func(t *testing.T) { + t.Parallel() + testCases := []struct { src string }{ @@ -117,3 +127,161 @@ func TestNewPath(t *testing.T) { } }) } + +func TestNewIPFSPath(t *testing.T) { + t.Parallel() + + t.Run("Works with CIDv0", func(t *testing.T) { + t.Parallel() + + c, err := cid.Decode("QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n") + assert.NoError(t, err) + + p := NewIPFSPath(c) + assert.IsType(t, immutablePath{}, p) + assert.Equal(t, "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", p.String()) + assert.Equal(t, c, p.Cid()) + }) + + t.Run("Works with CIDv1", func(t *testing.T) { + t.Parallel() + + c, err := cid.Decode("bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku") + assert.NoError(t, err) + + p := NewIPFSPath(c) + assert.IsType(t, immutablePath{}, p) + assert.Equal(t, "/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", p.String()) + assert.Equal(t, c, p.Cid()) + }) + + t.Run("NewIPLDPath returns correct ImmutablePath", func(t *testing.T) { + c, err := cid.Decode("QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n") + assert.NoError(t, err) + + p := NewIPLDPath(c) + assert.IsType(t, immutablePath{}, p) + assert.Equal(t, "/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", p.String()) + assert.Equal(t, c, p.Cid()) + + // Check if CID encoding is preserved. + c, err = cid.Decode("bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku") + assert.NoError(t, err) + + p = NewIPLDPath(c) + assert.IsType(t, immutablePath{}, p) + assert.Equal(t, "/ipld/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", p.String()) + assert.Equal(t, c, p.Cid()) + }) +} + +func TestNewIPLDPath(t *testing.T) { + t.Parallel() + + t.Run("Works with CIDv0", func(t *testing.T) { + t.Parallel() + + c, err := cid.Decode("QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n") + assert.NoError(t, err) + + p := NewIPLDPath(c) + assert.IsType(t, immutablePath{}, p) + assert.Equal(t, "/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", p.String()) + assert.Equal(t, c, p.Cid()) + }) + + t.Run("Works with CIDv1", func(t *testing.T) { + t.Parallel() + + c, err := cid.Decode("bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku") + assert.NoError(t, err) + + p := NewIPLDPath(c) + assert.IsType(t, immutablePath{}, p) + assert.Equal(t, "/ipld/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", p.String()) + assert.Equal(t, c, p.Cid()) + }) +} + +func TestNewImmutablePath(t *testing.T) { + t.Parallel() + + t.Run("Fails on Mutable Path", func(t *testing.T) { + for _, path := range []string{ + "/ipns/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", + "/ipns/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", + "/ipns/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/with/path", + "/ipns/domain.net", + } { + p, err := NewPath(path) + assert.NoError(t, err) + + _, err = NewImmutablePath(p) + assert.ErrorIs(t, err, ErrExpectedImmutable) + assert.ErrorIs(t, err, ErrInvalidPath{}) + } + }) + + t.Run("Succeeds on Immutable Path", func(t *testing.T) { + testCases := []struct { + path string + cid cid.Cid + remainder string + }{ + {"/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", cid.MustParse("QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n"), ""}, + {"/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b", cid.MustParse("QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n"), "/a/b"}, + {"/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b/", cid.MustParse("QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n"), "/a/b"}, + + {"/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", cid.MustParse("bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"), ""}, + {"/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a/b", cid.MustParse("bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"), "/a/b"}, + {"/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a/b/", cid.MustParse("bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"), "/a/b"}, + + {"/ipld/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", cid.MustParse("bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"), ""}, + {"/ipld/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a/b", cid.MustParse("bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"), "/a/b"}, + {"/ipld/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a/b/", cid.MustParse("bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"), "/a/b"}, + } + + for _, testCase := range testCases { + p, err := NewPath(testCase.path) + assert.NoError(t, err) + + ip, err := NewImmutablePath(p) + assert.NoError(t, err) + assert.Equal(t, testCase.path, ip.String()) + assert.Equal(t, testCase.cid, ip.Cid()) + assert.Equal(t, testCase.remainder, ip.Remainder()) + } + }) +} + +func TestJoin(t *testing.T) { + t.Parallel() + + testCases := []struct { + path string + segments []string + expected string + }{ + {"/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", []string{"a/b"}, "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b"}, + {"/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", []string{"/a/b"}, "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b"}, + {"/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/", []string{"/a/b"}, "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b"}, + {"/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", []string{"a", "b"}, "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b"}, + {"/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", []string{"a/b/../"}, "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/"}, + {"/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", []string{"a/b", "/"}, "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b/"}, + + {"/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", []string{"a/b"}, "/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a/b"}, + {"/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", []string{"/a/b"}, "/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a/b"}, + {"/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/", []string{"/a/b"}, "/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a/b"}, + {"/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", []string{"a", "b"}, "/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a/b"}, + {"/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", []string{"a/b/../"}, "/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a/"}, + {"/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", []string{"a/b", "/"}, "/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a/b/"}, + } + + for _, testCase := range testCases { + p, err := NewPath(testCase.path) + assert.NoError(t, err) + jp, err := Join(p, testCase.segments...) + assert.NoError(t, err) + assert.Equal(t, testCase.expected, jp.String()) + } +}