diff --git a/CHANGELOG.md b/CHANGELOG.md index 2afccc4b5..111044407 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,17 @@ The following emojis are used to highlight certain changes: `files.Node`. * `boxo/routing/http/client.Client` is now exported. This means you can now pass it around functions, or add it to a struct if you want. +* 🛠 The `path` package has been massively refactored. With this refactor, we have + condensed the different path-related and/or Kubo-specific packages under a single generic one. Therefore, there + are many breaking changes. Please consult the [documentation](https://pkg.go.dev/github.com/ipfs/boxo/path) + for more details on how to use the new package. + * Note: content paths created with `boxo/path` are automatically normalized: + - Replace multiple slashes with a single slash. + - Eliminate each `.` path name element (the current directory). + - Eliminate each inner `..` path name element (the parent directory) along with the non-`..` element that precedes it. + - Eliminate `..` elements that begin a rooted path: that is, replace "`/..`" by "`/`" at the beginning of a path. +* 🛠 The signature of `CoreAPI.ResolvePath` in `coreiface` has changed to now return + the remainder segments as a second return value, matching the signature of `resolver.ResolveToLastNode`. ### Removed diff --git a/coreiface/block.go b/coreiface/block.go index dbe31e9f8..cdd5fcee2 100644 --- a/coreiface/block.go +++ b/coreiface/block.go @@ -4,9 +4,8 @@ import ( "context" "io" - path "github.com/ipfs/boxo/coreiface/path" - "github.com/ipfs/boxo/coreiface/options" + "github.com/ipfs/boxo/path" ) // BlockStat contains information about a block @@ -15,7 +14,7 @@ type BlockStat interface { Size() int // Path returns path to the block - Path() path.Resolved + Path() path.ImmutablePath } // BlockAPI specifies the interface to the block layer diff --git a/coreiface/coreapi.go b/coreiface/coreapi.go index 7276a3f60..25e54a37b 100644 --- a/coreiface/coreapi.go +++ b/coreiface/coreapi.go @@ -5,9 +5,8 @@ package iface import ( "context" - path "github.com/ipfs/boxo/coreiface/path" - "github.com/ipfs/boxo/coreiface/options" + "github.com/ipfs/boxo/path" ipld "github.com/ipfs/go-ipld-format" ) @@ -47,8 +46,10 @@ type CoreAPI interface { // Routing returns an implementation of Routing API Routing() RoutingAPI - // ResolvePath resolves the path using Unixfs resolver - ResolvePath(context.Context, path.Path) (path.Resolved, error) + // ResolvePath resolves the path using UnixFS resolver, and returns the resolved + // immutable path, and the remainder of the path segments that cannot be resolved + // within UnixFS. + ResolvePath(context.Context, path.Path) (path.ImmutablePath, []string, error) // ResolveNode resolves the path (if not resolved already) using Unixfs // resolver, gets and returns the resolved Node diff --git a/coreiface/dht.go b/coreiface/dht.go index 93027a406..d9418ebfc 100644 --- a/coreiface/dht.go +++ b/coreiface/dht.go @@ -3,7 +3,7 @@ package iface import ( "context" - "github.com/ipfs/boxo/coreiface/path" + "github.com/ipfs/boxo/path" "github.com/ipfs/boxo/coreiface/options" diff --git a/coreiface/key.go b/coreiface/key.go index 118fe2e4f..4a1cbae80 100644 --- a/coreiface/key.go +++ b/coreiface/key.go @@ -3,7 +3,7 @@ package iface import ( "context" - "github.com/ipfs/boxo/coreiface/path" + "github.com/ipfs/boxo/path" "github.com/ipfs/boxo/coreiface/options" diff --git a/coreiface/name.go b/coreiface/name.go index 8c3e8e89a..f832033ef 100644 --- a/coreiface/name.go +++ b/coreiface/name.go @@ -4,10 +4,9 @@ import ( "context" "errors" - path "github.com/ipfs/boxo/coreiface/path" - "github.com/ipfs/boxo/ipns" - "github.com/ipfs/boxo/coreiface/options" + "github.com/ipfs/boxo/ipns" + "github.com/ipfs/boxo/path" ) var ErrResolveFailed = errors.New("could not resolve name") diff --git a/coreiface/object.go b/coreiface/object.go index d983fa49b..4a73f22ea 100644 --- a/coreiface/object.go +++ b/coreiface/object.go @@ -4,9 +4,8 @@ import ( "context" "io" - path "github.com/ipfs/boxo/coreiface/path" - "github.com/ipfs/boxo/coreiface/options" + "github.com/ipfs/boxo/path" "github.com/ipfs/go-cid" ipld "github.com/ipfs/go-ipld-format" @@ -60,11 +59,11 @@ type ObjectChange struct { // Before holds the link path before the change. Note that when a link is // added, this will be nil. - Before path.Resolved + Before path.ImmutablePath // After holds the link path after the change. Note that when a link is // removed, this will be nil. - After path.Resolved + After path.ImmutablePath } // ObjectAPI specifies the interface to MerkleDAG and contains useful utilities @@ -74,7 +73,7 @@ type ObjectAPI interface { New(context.Context, ...options.ObjectNewOption) (ipld.Node, error) // Put imports the data into merkledag - Put(context.Context, io.Reader, ...options.ObjectPutOption) (path.Resolved, error) + Put(context.Context, io.Reader, ...options.ObjectPutOption) (path.ImmutablePath, error) // Get returns the node for the path Get(context.Context, path.Path) (ipld.Node, error) @@ -91,16 +90,16 @@ type ObjectAPI interface { // AddLink adds a link under the specified path. child path can point to a // subdirectory within the patent which must be present (can be overridden // with WithCreate option). - AddLink(ctx context.Context, base path.Path, name string, child path.Path, opts ...options.ObjectAddLinkOption) (path.Resolved, error) + AddLink(ctx context.Context, base path.Path, name string, child path.Path, opts ...options.ObjectAddLinkOption) (path.ImmutablePath, error) // RmLink removes a link from the node - RmLink(ctx context.Context, base path.Path, link string) (path.Resolved, error) + RmLink(ctx context.Context, base path.Path, link string) (path.ImmutablePath, error) // AppendData appends data to the node - AppendData(context.Context, path.Path, io.Reader) (path.Resolved, error) + AppendData(context.Context, path.Path, io.Reader) (path.ImmutablePath, error) // SetData sets the data contained in the node - SetData(context.Context, path.Path, io.Reader) (path.Resolved, error) + SetData(context.Context, path.Path, io.Reader) (path.ImmutablePath, error) // Diff returns a set of changes needed to transform the first object into the // second. diff --git a/coreiface/path/path.go b/coreiface/path/path.go deleted file mode 100644 index c26b8692b..000000000 --- a/coreiface/path/path.go +++ /dev/null @@ -1,199 +0,0 @@ -package path - -import ( - "strings" - - ipfspath "github.com/ipfs/boxo/path" - cid "github.com/ipfs/go-cid" -) - -// Path is a generic wrapper for paths used in the API. A path can be resolved -// to a CID using one of Resolve functions in the API. -// -// Paths must be prefixed with a valid prefix: -// -// * /ipfs - Immutable unixfs path (files) -// * /ipld - Immutable ipld path (data) -// * /ipns - Mutable names. Usually resolves to one of the immutable paths -// TODO: /local (MFS) -type Path interface { - // String returns the path as a string. - String() string - - // Namespace returns the first component of the path. - // - // For example path "/ipfs/QmHash", calling Namespace() will return "ipfs" - // - // Calling this method on invalid paths (IsValid() != nil) will result in - // empty string - Namespace() string - - // Mutable returns false if the data pointed to by this path in guaranteed - // to not change. - // - // Note that resolved mutable path can be immutable. - Mutable() bool - - // IsValid checks if this path is a valid ipfs Path, returning nil iff it is - // valid - IsValid() error -} - -// Resolved is a path which was resolved to the last resolvable node. -// ResolvedPaths are guaranteed to return nil from `IsValid` -type Resolved interface { - // Cid returns the CID of the node referenced by the path. Remainder of the - // path is guaranteed to be within the node. - // - // Examples: - // If you have 3 linked objects: QmRoot -> A -> B: - // - // cidB := {"foo": {"bar": 42 }} - // cidA := {"B": {"/": cidB }} - // cidRoot := {"A": {"/": cidA }} - // - // And resolve paths: - // - // * "/ipfs/${cidRoot}" - // * Calling Cid() will return `cidRoot` - // * Calling Root() will return `cidRoot` - // * Calling Remainder() will return `` - // - // * "/ipfs/${cidRoot}/A" - // * Calling Cid() will return `cidA` - // * Calling Root() will return `cidRoot` - // * Calling Remainder() will return `` - // - // * "/ipfs/${cidRoot}/A/B/foo" - // * Calling Cid() will return `cidB` - // * Calling Root() will return `cidRoot` - // * Calling Remainder() will return `foo` - // - // * "/ipfs/${cidRoot}/A/B/foo/bar" - // * Calling Cid() will return `cidB` - // * Calling Root() will return `cidRoot` - // * Calling Remainder() will return `foo/bar` - Cid() cid.Cid - - // Root returns the CID of the root object of the path - // - // Example: - // If you have 3 linked objects: QmRoot -> A -> B, and resolve path - // "/ipfs/QmRoot/A/B", the Root method will return the CID of object QmRoot - // - // For more examples see the documentation of Cid() method - Root() cid.Cid - - // Remainder returns unresolved part of the path - // - // Example: - // If you have 2 linked objects: QmRoot -> A, where A is a CBOR node - // containing the following data: - // - // {"foo": {"bar": 42 }} - // - // When resolving "/ipld/QmRoot/A/foo/bar", Remainder will return "foo/bar" - // - // For more examples see the documentation of Cid() method - Remainder() string - - Path -} - -// path implements coreiface.Path -type path struct { - path string -} - -// resolvedPath implements coreiface.resolvedPath -type resolvedPath struct { - path - cid cid.Cid - root cid.Cid - remainder string -} - -// Join appends provided segments to the base path -func Join(base Path, a ...string) Path { - s := strings.Join(append([]string{base.String()}, a...), "/") - return &path{path: s} -} - -// IpfsPath creates new /ipfs path from the provided CID -func IpfsPath(c cid.Cid) Resolved { - return &resolvedPath{ - path: path{"/ipfs/" + c.String()}, - cid: c, - root: c, - remainder: "", - } -} - -// IpldPath creates new /ipld path from the provided CID -func IpldPath(c cid.Cid) Resolved { - return &resolvedPath{ - path: path{"/ipld/" + c.String()}, - cid: c, - root: c, - remainder: "", - } -} - -// New parses string path to a Path -func New(p string) Path { - if pp, err := ipfspath.ParsePath(p); err == nil { - p = pp.String() - } - - return &path{path: p} -} - -// NewResolvedPath creates new Resolved path. This function performs no checks -// and is intended to be used by resolver implementations. Incorrect inputs may -// cause panics. Handle with care. -func NewResolvedPath(ipath ipfspath.Path, c cid.Cid, root cid.Cid, remainder string) Resolved { - return &resolvedPath{ - path: path{ipath.String()}, - cid: c, - root: root, - remainder: remainder, - } -} - -func (p *path) String() string { - return p.path -} - -func (p *path) Namespace() string { - ip, err := ipfspath.ParsePath(p.path) - if err != nil { - return "" - } - - if len(ip.Segments()) < 1 { - panic("path without namespace") // this shouldn't happen under any scenario - } - return ip.Segments()[0] -} - -func (p *path) Mutable() bool { - // TODO: MFS: check for /local - return p.Namespace() == "ipns" -} - -func (p *path) IsValid() error { - _, err := ipfspath.ParsePath(p.path) - return err -} - -func (p *resolvedPath) Cid() cid.Cid { - return p.cid -} - -func (p *resolvedPath) Root() cid.Cid { - return p.root -} - -func (p *resolvedPath) Remainder() string { - return p.remainder -} diff --git a/coreiface/pin.go b/coreiface/pin.go index 6b97c6ca5..057516d08 100644 --- a/coreiface/pin.go +++ b/coreiface/pin.go @@ -3,7 +3,7 @@ package iface import ( "context" - path "github.com/ipfs/boxo/coreiface/path" + "github.com/ipfs/boxo/path" "github.com/ipfs/boxo/coreiface/options" ) @@ -11,7 +11,7 @@ import ( // Pin holds information about pinned resource type Pin interface { // Path to the pinned object - Path() path.Resolved + Path() path.ImmutablePath // Type of the pin Type() string @@ -35,7 +35,7 @@ type PinStatus interface { // BadPinNode is a node that has been marked as bad by Pin.Verify type BadPinNode interface { // Path is the path of the node - Path() path.Resolved + Path() path.ImmutablePath // Err is the reason why the node has been marked as bad Err() error diff --git a/coreiface/tests/block.go b/coreiface/tests/block.go index 5dcb16e4f..6e254063e 100644 --- a/coreiface/tests/block.go +++ b/coreiface/tests/block.go @@ -9,9 +9,8 @@ import ( coreiface "github.com/ipfs/boxo/coreiface" opt "github.com/ipfs/boxo/coreiface/options" - "github.com/ipfs/boxo/coreiface/path" + "github.com/ipfs/boxo/path" ipld "github.com/ipfs/go-ipld-format" - mh "github.com/multiformats/go-multihash" ) @@ -68,8 +67,8 @@ func (tp *TestSuite) TestBlockPut(t *testing.T) { t.Fatal(err) } - if res.Path().Cid().String() != rawCid { - t.Errorf("got wrong cid: %s", res.Path().Cid().String()) + if res.Path().RootCid().String() != rawCid { + t.Errorf("got wrong cid: %s", res.Path().RootCid().String()) } } @@ -88,8 +87,8 @@ func (tp *TestSuite) TestBlockPutFormatDagCbor(t *testing.T) { t.Fatal(err) } - if res.Path().Cid().String() != cborCid { - t.Errorf("got wrong cid: %s", res.Path().Cid().String()) + if res.Path().RootCid().String() != cborCid { + t.Errorf("got wrong cid: %s", res.Path().RootCid().String()) } } @@ -108,8 +107,8 @@ func (tp *TestSuite) TestBlockPutFormatDagPb(t *testing.T) { t.Fatal(err) } - if res.Path().Cid().String() != pbCid { - t.Errorf("got wrong cid: %s", res.Path().Cid().String()) + if res.Path().RootCid().String() != pbCid { + t.Errorf("got wrong cid: %s", res.Path().RootCid().String()) } } @@ -128,8 +127,8 @@ func (tp *TestSuite) TestBlockPutFormatV0(t *testing.T) { t.Fatal(err) } - if res.Path().Cid().String() != pbCidV0 { - t.Errorf("got wrong cid: %s", res.Path().Cid().String()) + if res.Path().RootCid().String() != pbCidV0 { + t.Errorf("got wrong cid: %s", res.Path().RootCid().String()) } } @@ -146,8 +145,8 @@ func (tp *TestSuite) TestBlockPutCidCodecDagCbor(t *testing.T) { t.Fatal(err) } - if res.Path().Cid().String() != cborCid { - t.Errorf("got wrong cid: %s", res.Path().Cid().String()) + if res.Path().RootCid().String() != cborCid { + t.Errorf("got wrong cid: %s", res.Path().RootCid().String()) } } @@ -164,8 +163,8 @@ func (tp *TestSuite) TestBlockPutCidCodecDagPb(t *testing.T) { t.Fatal(err) } - if res.Path().Cid().String() != pbCid { - t.Errorf("got wrong cid: %s", res.Path().Cid().String()) + if res.Path().RootCid().String() != pbCid { + t.Errorf("got wrong cid: %s", res.Path().RootCid().String()) } } @@ -187,8 +186,8 @@ func (tp *TestSuite) TestBlockPutHash(t *testing.T) { t.Fatal(err) } - if res.Path().Cid().String() != cborKCid { - t.Errorf("got wrong cid: %s", res.Path().Cid().String()) + if res.Path().RootCid().String() != cborKCid { + t.Errorf("got wrong cid: %s", res.Path().RootCid().String()) } } @@ -219,13 +218,13 @@ func (tp *TestSuite) TestBlockGet(t *testing.T) { t.Error("didn't get correct data back") } - p := path.New("/ipfs/" + res.Path().Cid().String()) + p := path.FromCid(res.Path().RootCid()) - rp, err := api.ResolvePath(ctx, p) + rp, _, err := api.ResolvePath(ctx, p) if err != nil { t.Fatal(err) } - if rp.Cid().String() != res.Path().Cid().String() { + if rp.RootCid().String() != res.Path().RootCid().String() { t.Error("paths didn't match") } } diff --git a/coreiface/tests/dag.go b/coreiface/tests/dag.go index b9a03c8f4..a106788d6 100644 --- a/coreiface/tests/dag.go +++ b/coreiface/tests/dag.go @@ -3,13 +3,11 @@ package tests import ( "context" "math" - gopath "path" "strings" "testing" - path "github.com/ipfs/boxo/coreiface/path" - coreiface "github.com/ipfs/boxo/coreiface" + "github.com/ipfs/boxo/path" ipldcbor "github.com/ipfs/go-ipld-cbor" ipld "github.com/ipfs/go-ipld-format" @@ -113,14 +111,17 @@ func (tp *TestSuite) TestDagPath(t *testing.T) { t.Fatal(err) } - p := path.New(gopath.Join(nd.Cid().String(), "lnk")) + p, err := path.Join(path.FromCid(nd.Cid()), "lnk") + if err != nil { + t.Fatal(err) + } - rp, err := api.ResolvePath(ctx, p) + rp, _, err := api.ResolvePath(ctx, p) if err != nil { t.Fatal(err) } - ndd, err := api.Dag().Get(ctx, rp.Cid()) + ndd, err := api.Dag().Get(ctx, rp.RootCid()) if err != nil { t.Fatal(err) } diff --git a/coreiface/tests/name.go b/coreiface/tests/name.go index 74d88edff..2b6b7ec49 100644 --- a/coreiface/tests/name.go +++ b/coreiface/tests/name.go @@ -4,15 +4,14 @@ import ( "context" "io" "math/rand" - gopath "path" "testing" "time" coreiface "github.com/ipfs/boxo/coreiface" opt "github.com/ipfs/boxo/coreiface/options" - path "github.com/ipfs/boxo/coreiface/path" "github.com/ipfs/boxo/files" "github.com/ipfs/boxo/ipns" + "github.com/ipfs/boxo/path" "github.com/stretchr/testify/require" ) @@ -35,10 +34,6 @@ func addTestObject(ctx context.Context, api coreiface.CoreAPI) (path.Path, error return api.Unixfs().Add(ctx, files.NewReaderFile(&io.LimitedReader{R: rnd, N: 4092})) } -func appendPath(p path.Path, sub string) path.Path { - return path.New(gopath.Join(p.String(), sub)) -} - func (tp *TestSuite) TestPublishResolve(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -68,7 +63,10 @@ func (tp *TestSuite) TestPublishResolve(t *testing.T) { t.Run("publishPath", func(t *testing.T) { api, p := init() - name, err := api.Name().Publish(ctx, appendPath(p, "/test")) + p, err := path.Join(p, "/test") + require.NoError(t, err) + + name, err := api.Name().Publish(ctx, p) require.NoError(t, err) self, err := api.Key().Self(ctx) @@ -77,7 +75,7 @@ func (tp *TestSuite) TestPublishResolve(t *testing.T) { resPath, err := api.Name().Resolve(ctx, name.String(), ropts...) require.NoError(t, err) - require.Equal(t, p.String()+"/test", resPath.String()) + require.Equal(t, p.String(), resPath.String()) }) t.Run("revolvePath", func(t *testing.T) { @@ -96,7 +94,10 @@ func (tp *TestSuite) TestPublishResolve(t *testing.T) { t.Run("publishRevolvePath", func(t *testing.T) { api, p := init() - name, err := api.Name().Publish(ctx, appendPath(p, "/a")) + p, err := path.Join(p, "/a") + require.NoError(t, err) + + name, err := api.Name().Publish(ctx, p) require.NoError(t, err) self, err := api.Key().Self(ctx) @@ -105,7 +106,7 @@ func (tp *TestSuite) TestPublishResolve(t *testing.T) { resPath, err := api.Name().Resolve(ctx, name.String()+"/b", ropts...) require.NoError(t, err) - require.Equal(t, p.String()+"/a/b", resPath.String()) + require.Equal(t, p.String()+"/b", resPath.String()) }) } diff --git a/coreiface/tests/object.go b/coreiface/tests/object.go index 63c218eb3..5c6ba828c 100644 --- a/coreiface/tests/object.go +++ b/coreiface/tests/object.go @@ -166,7 +166,7 @@ func (tp *TestSuite) TestObjectLinks(t *testing.T) { t.Fatal(err) } - p2, err := api.Object().Put(ctx, strings.NewReader(`{"Links":[{"Name":"bar", "Hash":"`+p1.Cid().String()+`"}]}`)) + p2, err := api.Object().Put(ctx, strings.NewReader(`{"Links":[{"Name":"bar", "Hash":"`+p1.RootCid().String()+`"}]}`)) if err != nil { t.Fatal(err) } @@ -180,7 +180,7 @@ func (tp *TestSuite) TestObjectLinks(t *testing.T) { t.Errorf("unexpected number of links: %d", len(links)) } - if links[0].Cid.String() != p1.Cid().String() { + if links[0].Cid.String() != p1.RootCid().String() { t.Fatal("cids didn't batch") } @@ -202,7 +202,7 @@ func (tp *TestSuite) TestObjectStat(t *testing.T) { t.Fatal(err) } - p2, err := api.Object().Put(ctx, strings.NewReader(`{"Data":"bazz", "Links":[{"Name":"bar", "Hash":"`+p1.Cid().String()+`", "Size":3}]}`)) + p2, err := api.Object().Put(ctx, strings.NewReader(`{"Data":"bazz", "Links":[{"Name":"bar", "Hash":"`+p1.RootCid().String()+`", "Size":3}]}`)) if err != nil { t.Fatal(err) } @@ -212,7 +212,7 @@ func (tp *TestSuite) TestObjectStat(t *testing.T) { t.Fatal(err) } - if stat.Cid.String() != p2.Cid().String() { + if stat.Cid.String() != p2.RootCid().String() { t.Error("unexpected stat.Cid") } @@ -250,7 +250,7 @@ func (tp *TestSuite) TestObjectAddLink(t *testing.T) { t.Fatal(err) } - p2, err := api.Object().Put(ctx, strings.NewReader(`{"Data":"bazz", "Links":[{"Name":"bar", "Hash":"`+p1.Cid().String()+`", "Size":3}]}`)) + p2, err := api.Object().Put(ctx, strings.NewReader(`{"Data":"bazz", "Links":[{"Name":"bar", "Hash":"`+p1.RootCid().String()+`", "Size":3}]}`)) if err != nil { t.Fatal(err) } @@ -291,7 +291,7 @@ func (tp *TestSuite) TestObjectAddLinkCreate(t *testing.T) { t.Fatal(err) } - p2, err := api.Object().Put(ctx, strings.NewReader(`{"Data":"bazz", "Links":[{"Name":"bar", "Hash":"`+p1.Cid().String()+`", "Size":3}]}`)) + p2, err := api.Object().Put(ctx, strings.NewReader(`{"Data":"bazz", "Links":[{"Name":"bar", "Hash":"`+p1.RootCid().String()+`", "Size":3}]}`)) if err != nil { t.Fatal(err) } @@ -340,7 +340,7 @@ func (tp *TestSuite) TestObjectRmLink(t *testing.T) { t.Fatal(err) } - p2, err := api.Object().Put(ctx, strings.NewReader(`{"Data":"bazz", "Links":[{"Name":"bar", "Hash":"`+p1.Cid().String()+`", "Size":3}]}`)) + p2, err := api.Object().Put(ctx, strings.NewReader(`{"Data":"bazz", "Links":[{"Name":"bar", "Hash":"`+p1.RootCid().String()+`", "Size":3}]}`)) if err != nil { t.Fatal(err) } diff --git a/coreiface/tests/path.go b/coreiface/tests/path.go index 06f3aa1f8..116aed2e7 100644 --- a/coreiface/tests/path.go +++ b/coreiface/tests/path.go @@ -2,17 +2,26 @@ package tests import ( "context" + "fmt" "math" "strings" "testing" - "github.com/ipfs/boxo/coreiface/path" - "github.com/ipfs/boxo/coreiface/options" - + "github.com/ipfs/boxo/path" + "github.com/ipfs/go-cid" ipldcbor "github.com/ipfs/go-ipld-cbor" + "github.com/stretchr/testify/require" ) +func newIPLDPath(t *testing.T, cid cid.Cid) path.ImmutablePath { + p, err := path.NewPath(fmt.Sprintf("/%s/%s", path.IPLDNamespace, cid.String())) + require.NoError(t, err) + im, err := path.NewImmutablePath(p) + require.NoError(t, err) + return im +} + func (tp *TestSuite) TestPath(t *testing.T) { t.Run("TestMutablePath", tp.TestMutablePath) t.Run("TestPathRemainder", tp.TestPathRemainder) @@ -25,173 +34,115 @@ func (tp *TestSuite) TestPath(t *testing.T) { func (tp *TestSuite) TestMutablePath(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + api, err := tp.makeAPI(t, ctx) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) blk, err := api.Block().Put(ctx, strings.NewReader(`foo`)) - if err != nil { - t.Fatal(err) - } - - if blk.Path().Mutable() { - t.Error("expected /ipld path to be immutable") - } - - // get self /ipns path - - if api.Key() == nil { - t.Fatal(".Key not implemented") - } + require.NoError(t, err) + require.False(t, blk.Path().Mutable()) + require.NotNil(t, api.Key()) keys, err := api.Key().List(ctx) - if err != nil { - t.Fatal(err) - } - - if !keys[0].Path().Mutable() { - t.Error("expected self /ipns path to be mutable") - } + require.NoError(t, err) + require.True(t, keys[0].Path().Mutable()) } func (tp *TestSuite) TestPathRemainder(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - api, err := tp.makeAPI(t, ctx) - if err != nil { - t.Fatal(err) - } - if api.Dag() == nil { - t.Fatal(".Dag not implemented") - } + api, err := tp.makeAPI(t, ctx) + require.NoError(t, err) + require.NotNil(t, api.Dag()) nd, err := ipldcbor.FromJSON(strings.NewReader(`{"foo": {"bar": "baz"}}`), math.MaxUint64, -1) - if err != nil { - t.Fatal(err) - } - - if err := api.Dag().Add(ctx, nd); err != nil { - t.Fatal(err) - } - - rp1, err := api.ResolvePath(ctx, path.New(nd.String()+"/foo/bar")) - if err != nil { - t.Fatal(err) - } - - if rp1.Remainder() != "foo/bar" { - t.Error("expected to get path remainder") - } + require.NoError(t, err) + + err = api.Dag().Add(ctx, nd) + require.NoError(t, err) + + p, err := path.Join(path.FromCid(nd.Cid()), "foo", "bar") + require.NoError(t, err) + + _, remainder, err := api.ResolvePath(ctx, p) + require.NoError(t, err) + require.Equal(t, "/foo/bar", path.SegmentsToString(remainder...)) } func (tp *TestSuite) TestEmptyPathRemainder(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - api, err := tp.makeAPI(t, ctx) - if err != nil { - t.Fatal(err) - } - if api.Dag() == nil { - t.Fatal(".Dag not implemented") - } + api, err := tp.makeAPI(t, ctx) + require.NoError(t, err) + require.NotNil(t, api.Dag()) nd, err := ipldcbor.FromJSON(strings.NewReader(`{"foo": {"bar": "baz"}}`), math.MaxUint64, -1) - if err != nil { - t.Fatal(err) - } - - if err := api.Dag().Add(ctx, nd); err != nil { - t.Fatal(err) - } - - rp1, err := api.ResolvePath(ctx, path.New(nd.Cid().String())) - if err != nil { - t.Fatal(err) - } - - if rp1.Remainder() != "" { - t.Error("expected the resolved path to not have a remainder") - } + require.NoError(t, err) + + err = api.Dag().Add(ctx, nd) + require.NoError(t, err) + + _, remainder, err := api.ResolvePath(ctx, path.FromCid(nd.Cid())) + require.NoError(t, err) + require.Empty(t, remainder) } func (tp *TestSuite) TestInvalidPathRemainder(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - api, err := tp.makeAPI(t, ctx) - if err != nil { - t.Fatal(err) - } - if api.Dag() == nil { - t.Fatal(".Dag not implemented") - } + api, err := tp.makeAPI(t, ctx) + require.NoError(t, err) + require.NotNil(t, api.Dag()) nd, err := ipldcbor.FromJSON(strings.NewReader(`{"foo": {"bar": "baz"}}`), math.MaxUint64, -1) - if err != nil { - t.Fatal(err) - } - - if err := api.Dag().Add(ctx, nd); err != nil { - t.Fatal(err) - } - - _, err = api.ResolvePath(ctx, path.New("/ipld/"+nd.Cid().String()+"/bar/baz")) - if err == nil || !strings.Contains(err.Error(), `no link named "bar"`) { - t.Fatalf("unexpected error: %s", err) - } + require.NoError(t, err) + + err = api.Dag().Add(ctx, nd) + require.NoError(t, err) + + p, err := path.Join(newIPLDPath(t, nd.Cid()), "/bar/baz") + require.NoError(t, err) + + _, _, err = api.ResolvePath(ctx, p) + require.NotNil(t, err) + require.ErrorContains(t, err, `no link named "bar"`) } func (tp *TestSuite) TestPathRoot(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - api, err := tp.makeAPI(t, ctx) - if err != nil { - t.Fatal(err) - } - if api.Block() == nil { - t.Fatal(".Block not implemented") - } + api, err := tp.makeAPI(t, ctx) + require.NoError(t, err) + require.NotNil(t, api.Block()) blk, err := api.Block().Put(ctx, strings.NewReader(`foo`), options.Block.Format("raw")) - if err != nil { - t.Fatal(err) - } - - if api.Dag() == nil { - t.Fatal(".Dag not implemented") - } - - nd, err := ipldcbor.FromJSON(strings.NewReader(`{"foo": {"/": "`+blk.Path().Cid().String()+`"}}`), math.MaxUint64, -1) - if err != nil { - t.Fatal(err) - } - - if err := api.Dag().Add(ctx, nd); err != nil { - t.Fatal(err) - } - - rp, err := api.ResolvePath(ctx, path.New("/ipld/"+nd.Cid().String()+"/foo")) - if err != nil { - t.Fatal(err) - } - - if rp.Root().String() != nd.Cid().String() { - t.Error("unexpected path root") - } - - if rp.Cid().String() != blk.Path().Cid().String() { - t.Error("unexpected path cid") - } + require.NoError(t, err) + require.NotNil(t, api.Dag()) + + nd, err := ipldcbor.FromJSON(strings.NewReader(`{"foo": {"/": "`+blk.Path().RootCid().String()+`"}}`), math.MaxUint64, -1) + require.NoError(t, err) + + err = api.Dag().Add(ctx, nd) + require.NoError(t, err) + + p, err := path.Join(newIPLDPath(t, nd.Cid()), "/foo") + require.NoError(t, err) + + rp, _, err := api.ResolvePath(ctx, p) + require.NoError(t, err) + require.Equal(t, rp.RootCid().String(), blk.Path().RootCid().String()) } func (tp *TestSuite) TestPathJoin(t *testing.T) { - p1 := path.New("/ipfs/QmYNmQKp6SuaVrpgWRsPTgCQCnpxUYGq76YEKBXuj2N4H6/bar/baz") + p1, err := path.NewPath("/ipfs/QmYNmQKp6SuaVrpgWRsPTgCQCnpxUYGq76YEKBXuj2N4H6/bar/baz") + require.NoError(t, err) + + p2, err := path.Join(p1, "foo") + require.NoError(t, err) - if path.Join(p1, "foo").String() != "/ipfs/QmYNmQKp6SuaVrpgWRsPTgCQCnpxUYGq76YEKBXuj2N4H6/bar/baz/foo" { - t.Error("unexpected path") - } + require.Equal(t, "/ipfs/QmYNmQKp6SuaVrpgWRsPTgCQCnpxUYGq76YEKBXuj2N4H6/bar/baz/foo", p2.String()) } diff --git a/coreiface/tests/pin.go b/coreiface/tests/pin.go index 4b0fea01d..49499b36a 100644 --- a/coreiface/tests/pin.go +++ b/coreiface/tests/pin.go @@ -8,8 +8,7 @@ import ( iface "github.com/ipfs/boxo/coreiface" opt "github.com/ipfs/boxo/coreiface/options" - "github.com/ipfs/boxo/coreiface/path" - + "github.com/ipfs/boxo/path" "github.com/ipfs/go-cid" ipldcbor "github.com/ipfs/go-ipld-cbor" ipld "github.com/ipfs/go-ipld-format" @@ -77,7 +76,7 @@ func (tp *TestSuite) TestPinSimple(t *testing.T) { t.Errorf("unexpected pin list len: %d", len(list)) } - if list[0].Path().Cid().String() != p.Cid().String() { + if list[0].Path().RootCid().String() != p.RootCid().String() { t.Error("paths don't match") } @@ -120,12 +119,12 @@ func (tp *TestSuite) TestPinRecursive(t *testing.T) { t.Fatal(err) } - nd2, err := ipldcbor.FromJSON(strings.NewReader(`{"lnk": {"/": "`+p0.Cid().String()+`"}}`), math.MaxUint64, -1) + nd2, err := ipldcbor.FromJSON(strings.NewReader(`{"lnk": {"/": "`+p0.RootCid().String()+`"}}`), math.MaxUint64, -1) if err != nil { t.Fatal(err) } - nd3, err := ipldcbor.FromJSON(strings.NewReader(`{"lnk": {"/": "`+p1.Cid().String()+`"}}`), math.MaxUint64, -1) + nd3, err := ipldcbor.FromJSON(strings.NewReader(`{"lnk": {"/": "`+p1.RootCid().String()+`"}}`), math.MaxUint64, -1) if err != nil { t.Fatal(err) } @@ -134,12 +133,12 @@ func (tp *TestSuite) TestPinRecursive(t *testing.T) { t.Fatal(err) } - err = api.Pin().Add(ctx, path.IpldPath(nd2.Cid())) + err = api.Pin().Add(ctx, path.FromCid(nd2.Cid())) if err != nil { t.Fatal(err) } - err = api.Pin().Add(ctx, path.IpldPath(nd3.Cid()), opt.Pin.Recursive(false)) + err = api.Pin().Add(ctx, path.FromCid(nd3.Cid()), opt.Pin.Recursive(false)) if err != nil { t.Fatal(err) } @@ -162,8 +161,8 @@ func (tp *TestSuite) TestPinRecursive(t *testing.T) { t.Errorf("unexpected pin list len: %d", len(list)) } - if list[0].Path().String() != path.IpldPath(nd3.Cid()).String() { - t.Errorf("unexpected path, %s != %s", list[0].Path().String(), path.IpfsPath(nd3.Cid()).String()) + if list[0].Path().String() != path.FromCid(nd3.Cid()).String() { + t.Errorf("unexpected path, %s != %s", list[0].Path().String(), path.FromCid(nd3.Cid()).String()) } list, err = accPins(api.Pin().Ls(ctx, opt.Pin.Ls.Recursive())) @@ -175,8 +174,8 @@ func (tp *TestSuite) TestPinRecursive(t *testing.T) { t.Errorf("unexpected pin list len: %d", len(list)) } - if list[0].Path().String() != path.IpldPath(nd2.Cid()).String() { - t.Errorf("unexpected path, %s != %s", list[0].Path().String(), path.IpldPath(nd2.Cid()).String()) + if list[0].Path().String() != path.FromCid(nd2.Cid()).String() { + t.Errorf("unexpected path, %s != %s", list[0].Path().String(), path.FromCid(nd2.Cid()).String()) } list, err = accPins(api.Pin().Ls(ctx, opt.Pin.Ls.Indirect())) @@ -188,8 +187,8 @@ func (tp *TestSuite) TestPinRecursive(t *testing.T) { t.Errorf("unexpected pin list len: %d", len(list)) } - if list[0].Path().Cid().String() != p0.Cid().String() { - t.Errorf("unexpected path, %s != %s", list[0].Path().Cid().String(), p0.Cid().String()) + if list[0].Path().RootCid().String() != p0.RootCid().String() { + t.Errorf("unexpected path, %s != %s", list[0].Path().RootCid().String(), p0.RootCid().String()) } res, err := api.Pin().Verify(ctx) @@ -259,12 +258,12 @@ func (tp *TestSuite) TestPinLsIndirect(t *testing.T) { leaf, parent, grandparent := getThreeChainedNodes(t, ctx, api, "foo") - err = api.Pin().Add(ctx, path.IpldPath(grandparent.Cid())) + err = api.Pin().Add(ctx, path.FromCid(grandparent.Cid())) if err != nil { t.Fatal(err) } - err = api.Pin().Add(ctx, path.IpldPath(parent.Cid()), opt.Pin.Recursive(false)) + err = api.Pin().Add(ctx, path.FromCid(parent.Cid()), opt.Pin.Recursive(false)) if err != nil { t.Fatal(err) } @@ -293,12 +292,12 @@ func (tp *TestSuite) TestPinLsPredenceRecursiveIndirect(t *testing.T) { // Test recursive > indirect leaf, parent, grandparent := getThreeChainedNodes(t, ctx, api, "recursive > indirect") - err = api.Pin().Add(ctx, path.IpldPath(grandparent.Cid())) + err = api.Pin().Add(ctx, path.FromCid(grandparent.Cid())) if err != nil { t.Fatal(err) } - err = api.Pin().Add(ctx, path.IpldPath(parent.Cid())) + err = api.Pin().Add(ctx, path.FromCid(parent.Cid())) if err != nil { t.Fatal(err) } @@ -317,12 +316,12 @@ func (tp *TestSuite) TestPinLsPrecedenceDirectIndirect(t *testing.T) { // Test direct > indirect leaf, parent, grandparent := getThreeChainedNodes(t, ctx, api, "direct > indirect") - err = api.Pin().Add(ctx, path.IpldPath(grandparent.Cid())) + err = api.Pin().Add(ctx, path.FromCid(grandparent.Cid())) if err != nil { t.Fatal(err) } - err = api.Pin().Add(ctx, path.IpldPath(parent.Cid()), opt.Pin.Recursive(false)) + err = api.Pin().Add(ctx, path.FromCid(parent.Cid()), opt.Pin.Recursive(false)) if err != nil { t.Fatal(err) } @@ -341,24 +340,24 @@ func (tp *TestSuite) TestPinLsPrecedenceRecursiveDirect(t *testing.T) { // Test recursive > direct leaf, parent, grandparent := getThreeChainedNodes(t, ctx, api, "recursive + direct = error") - err = api.Pin().Add(ctx, path.IpldPath(parent.Cid())) + err = api.Pin().Add(ctx, path.FromCid(parent.Cid())) if err != nil { t.Fatal(err) } - err = api.Pin().Add(ctx, path.IpldPath(parent.Cid()), opt.Pin.Recursive(false)) + err = api.Pin().Add(ctx, path.FromCid(parent.Cid()), opt.Pin.Recursive(false)) if err == nil { t.Fatal("expected error directly pinning a recursively pinned node") } assertPinTypes(t, ctx, api, []cidContainer{parent}, []cidContainer{}, []cidContainer{leaf}) - err = api.Pin().Add(ctx, path.IpldPath(grandparent.Cid()), opt.Pin.Recursive(false)) + err = api.Pin().Add(ctx, path.FromCid(grandparent.Cid()), opt.Pin.Recursive(false)) if err != nil { t.Fatal(err) } - err = api.Pin().Add(ctx, path.IpldPath(grandparent.Cid())) + err = api.Pin().Add(ctx, path.FromCid(grandparent.Cid())) if err != nil { t.Fatal(err) } @@ -376,40 +375,48 @@ func (tp *TestSuite) TestPinIsPinned(t *testing.T) { leaf, parent, grandparent := getThreeChainedNodes(t, ctx, api, "foofoo") - assertNotPinned(t, ctx, api, path.IpldPath(grandparent.Cid())) - assertNotPinned(t, ctx, api, path.IpldPath(parent.Cid())) - assertNotPinned(t, ctx, api, path.IpldPath(leaf.Cid())) + assertNotPinned(t, ctx, api, newIPLDPath(t, grandparent.Cid())) + assertNotPinned(t, ctx, api, newIPLDPath(t, parent.Cid())) + assertNotPinned(t, ctx, api, newIPLDPath(t, leaf.Cid())) - err = api.Pin().Add(ctx, path.IpldPath(parent.Cid()), opt.Pin.Recursive(true)) + err = api.Pin().Add(ctx, newIPLDPath(t, parent.Cid()), opt.Pin.Recursive(true)) if err != nil { t.Fatal(err) } - assertNotPinned(t, ctx, api, path.IpldPath(grandparent.Cid())) - assertIsPinned(t, ctx, api, path.IpldPath(parent.Cid()), "recursive") - assertIsPinned(t, ctx, api, path.IpldPath(leaf.Cid()), "indirect") + assertNotPinned(t, ctx, api, newIPLDPath(t, grandparent.Cid())) + assertIsPinned(t, ctx, api, newIPLDPath(t, parent.Cid()), "recursive") + assertIsPinned(t, ctx, api, newIPLDPath(t, leaf.Cid()), "indirect") - err = api.Pin().Add(ctx, path.IpldPath(grandparent.Cid()), opt.Pin.Recursive(false)) + err = api.Pin().Add(ctx, newIPLDPath(t, grandparent.Cid()), opt.Pin.Recursive(false)) if err != nil { t.Fatal(err) } - assertIsPinned(t, ctx, api, path.IpldPath(grandparent.Cid()), "direct") - assertIsPinned(t, ctx, api, path.IpldPath(parent.Cid()), "recursive") - assertIsPinned(t, ctx, api, path.IpldPath(leaf.Cid()), "indirect") + assertIsPinned(t, ctx, api, newIPLDPath(t, grandparent.Cid()), "direct") + assertIsPinned(t, ctx, api, newIPLDPath(t, parent.Cid()), "recursive") + assertIsPinned(t, ctx, api, newIPLDPath(t, leaf.Cid()), "indirect") } type cidContainer interface { Cid() cid.Cid } +type immutablePathCidContainer struct { + path.ImmutablePath +} + +func (i immutablePathCidContainer) Cid() cid.Cid { + return i.RootCid() +} + func getThreeChainedNodes(t *testing.T, ctx context.Context, api iface.CoreAPI, leafData string) (cidContainer, cidContainer, cidContainer) { leaf, err := api.Unixfs().Add(ctx, strFile(leafData)()) if err != nil { t.Fatal(err) } - parent, err := ipldcbor.FromJSON(strings.NewReader(`{"lnk": {"/": "`+leaf.Cid().String()+`"}}`), math.MaxUint64, -1) + parent, err := ipldcbor.FromJSON(strings.NewReader(`{"lnk": {"/": "`+leaf.RootCid().String()+`"}}`), math.MaxUint64, -1) if err != nil { t.Fatal(err) } @@ -423,7 +430,7 @@ func getThreeChainedNodes(t *testing.T, ctx context.Context, api iface.CoreAPI, t.Fatal(err) } - return leaf, parent, grandparent + return immutablePathCidContainer{leaf}, parent, grandparent } func assertPinTypes(t *testing.T, ctx context.Context, api iface.CoreAPI, recusive, direct, indirect []cidContainer) { @@ -466,7 +473,7 @@ func assertPinCids(t *testing.T, pins []iface.Pin, cids ...cidContainer) { valid := true for _, p := range pins { - c := p.Path().Cid() + c := p.Path().RootCid() if cSet.Has(c) { cSet.Remove(c) } else { @@ -480,7 +487,7 @@ func assertPinCids(t *testing.T, pins []iface.Pin, cids ...cidContainer) { if !valid { pinStrs := make([]string, len(pins)) for i, p := range pins { - pinStrs[i] = p.Path().Cid().String() + pinStrs[i] = p.Path().RootCid().String() } pathStrs := make([]string, len(cids)) for i, c := range cids { @@ -511,13 +518,13 @@ func assertPinLsAllConsistency(t *testing.T, ctx context.Context, api iface.Core } for _, p := range allPins { - if !all.Visit(p.Path().Cid()) { + if !all.Visit(p.Path().RootCid()) { t.Fatalf("pin ls returned the same cid multiple times") } typeStr := p.Type() if typeSet, ok := typeMap[p.Type()]; ok { - typeSet.Add(p.Path().Cid()) + typeSet.Add(p.Path().RootCid()) } else { t.Fatalf("unknown pin type: %s", typeStr) } @@ -538,7 +545,7 @@ func assertPinLsAllConsistency(t *testing.T, ctx context.Context, api iface.Core t.Fatalf("returned wrong pin type: expected %s, got %s", typeStr, pinType) } - if c := p.Path().Cid(); !pinProps.Has(c) { + if c := p.Path().RootCid(); !pinProps.Has(c) { t.Fatalf("%s expected to be in pin ls all as type %s", c.String(), typeStr) } } diff --git a/coreiface/tests/routing.go b/coreiface/tests/routing.go index fd10dffcd..c56e91659 100644 --- a/coreiface/tests/routing.go +++ b/coreiface/tests/routing.go @@ -7,8 +7,8 @@ import ( iface "github.com/ipfs/boxo/coreiface" "github.com/ipfs/boxo/coreiface/options" - "github.com/ipfs/boxo/coreiface/path" "github.com/ipfs/boxo/ipns" + "github.com/ipfs/boxo/path" "github.com/stretchr/testify/require" ) diff --git a/coreiface/tests/unixfs.go b/coreiface/tests/unixfs.go index 25c3ac1b7..e0c37fce4 100644 --- a/coreiface/tests/unixfs.go +++ b/coreiface/tests/unixfs.go @@ -14,10 +14,9 @@ import ( "sync" "testing" - "github.com/ipfs/boxo/coreiface/path" - coreiface "github.com/ipfs/boxo/coreiface" "github.com/ipfs/boxo/coreiface/options" + "github.com/ipfs/boxo/path" "github.com/ipfs/boxo/files" mdag "github.com/ipfs/boxo/ipld/merkledag" @@ -106,12 +105,12 @@ func (tp *TestSuite) TestAdd(t *testing.T) { t.Fatal(err) } - p := func(h string) path.Resolved { + p := func(h string) path.ImmutablePath { c, err := cid.Parse(h) if err != nil { t.Fatal(err) } - return path.IpfsPath(c) + return path.FromCid(c) } rf, err := os.CreateTemp(os.TempDir(), "unixfs-add-real") @@ -410,7 +409,7 @@ func (tp *TestSuite) TestAdd(t *testing.T) { } if expected[0].Path != nil && event.Path != nil { - if expected[0].Path.Cid().String() != event.Path.Cid().String() { + if expected[0].Path.RootCid().String() != event.Path.RootCid().String() { t.Errorf("Event.Hash didn't match, %s != %s", expected[0].Path, event.Path) } } else if event.Path != expected[0].Path { @@ -553,7 +552,7 @@ func (tp *TestSuite) TestAddPinned(t *testing.T) { t.Fatalf("expected 1 pin, got %d", len(pins)) } - if pins[0].Path().String() != "/ipld/QmQy2Dw4Wk7rdJKjThjYXzfFJNaRKRHhHP5gHHXroJMYxk" { + if pins[0].Path().String() != "/ipfs/QmQy2Dw4Wk7rdJKjThjYXzfFJNaRKRHhHP5gHHXroJMYxk" { t.Fatalf("got unexpected pin: %s", pins[0].Path().String()) } } @@ -597,7 +596,10 @@ func (tp *TestSuite) TestGetEmptyFile(t *testing.T) { t.Fatal(err) } - emptyFilePath := path.New(emptyFile) + emptyFilePath, err := path.NewPath(emptyFile) + if err != nil { + t.Fatal(err) + } r, err := api.Unixfs().Get(ctx, emptyFilePath) if err != nil { @@ -626,18 +628,18 @@ func (tp *TestSuite) TestGetDir(t *testing.T) { if err != nil { t.Fatal(err) } - p := path.IpfsPath(edir.Cid()) + p := path.FromCid(edir.Cid()) emptyDir, err := api.Object().New(ctx, options.Object.Type("unixfs-dir")) if err != nil { t.Fatal(err) } - if p.String() != path.IpfsPath(emptyDir.Cid()).String() { + if p.String() != path.FromCid(emptyDir.Cid()).String() { t.Fatalf("expected path %s, got: %s", emptyDir.Cid(), p.String()) } - r, err := api.Unixfs().Get(ctx, path.IpfsPath(emptyDir.Cid())) + r, err := api.Unixfs().Get(ctx, path.FromCid(emptyDir.Cid())) if err != nil { t.Fatal(err) } @@ -661,7 +663,7 @@ func (tp *TestSuite) TestGetNonUnixfs(t *testing.T) { t.Fatal(err) } - _, err = api.Unixfs().Get(ctx, path.IpfsPath(nd.Cid())) + _, err = api.Unixfs().Get(ctx, path.FromCid(nd.Cid())) if !strings.Contains(err.Error(), "proto: required field") { t.Fatalf("expected protobuf error, got: %s", err) } @@ -787,7 +789,7 @@ func (tp *TestSuite) TestLsEmptyDir(t *testing.T) { t.Fatal(err) } - links, err := api.Unixfs().Ls(ctx, path.IpfsPath(emptyDir.Cid())) + links, err := api.Unixfs().Ls(ctx, path.FromCid(emptyDir.Cid())) if err != nil { t.Fatal(err) } @@ -816,7 +818,7 @@ func (tp *TestSuite) TestLsNonUnixfs(t *testing.T) { t.Fatal(err) } - links, err := api.Unixfs().Ls(ctx, path.IpfsPath(nd.Cid())) + links, err := api.Unixfs().Ls(ctx, path.FromCid(nd.Cid())) if err != nil { t.Fatal(err) } diff --git a/coreiface/unixfs.go b/coreiface/unixfs.go index 606bc8e78..35e108c02 100644 --- a/coreiface/unixfs.go +++ b/coreiface/unixfs.go @@ -4,17 +4,16 @@ import ( "context" "github.com/ipfs/boxo/coreiface/options" - path "github.com/ipfs/boxo/coreiface/path" - "github.com/ipfs/boxo/files" + "github.com/ipfs/boxo/path" "github.com/ipfs/go-cid" ) type AddEvent struct { Name string - Path path.Resolved `json:",omitempty"` - Bytes int64 `json:",omitempty"` - Size string `json:",omitempty"` + Path path.ImmutablePath `json:",omitempty"` + Bytes int64 `json:",omitempty"` + Size string `json:",omitempty"` } // FileType is an enum of possible UnixFS file types. @@ -66,7 +65,7 @@ type UnixfsAPI interface { // Add imports the data from the reader into merkledag file // // TODO: a long useful comment on how to use this for many different scenarios - Add(context.Context, files.Node, ...options.UnixfsAddOption) (path.Resolved, error) + Add(context.Context, files.Node, ...options.UnixfsAddOption) (path.ImmutablePath, error) // Get returns a read-only handle to a file tree referenced by a path // diff --git a/examples/routing/delegated-routing-client/main_test.go b/examples/routing/delegated-routing-client/main_test.go index 7ad813f13..2dab7b13a 100644 --- a/examples/routing/delegated-routing-client/main_test.go +++ b/examples/routing/delegated-routing-client/main_test.go @@ -9,9 +9,8 @@ import ( "testing" "time" - "github.com/ipfs/boxo/coreiface/path" "github.com/ipfs/boxo/ipns" - ipfspath "github.com/ipfs/boxo/path" + "github.com/ipfs/boxo/path" "github.com/ipfs/go-cid" "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peer" @@ -82,11 +81,11 @@ func makeNameAndRecord(t *testing.T) (ipns.Name, []byte) { cid, err := cid.Decode("bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4") require.NoError(t, err) - path := path.IpfsPath(cid) + path := path.FromCid(cid) eol := time.Now().Add(time.Hour * 48) ttl := time.Second * 20 - record, err := ipns.NewRecord(sk, ipfspath.FromString(path.String()), 1, eol, ttl) + record, err := ipns.NewRecord(sk, path, 1, eol, ttl) require.NoError(t, err) rawRecord, err := ipns.MarshalRecord(record) diff --git a/gateway/assets/assets.go b/gateway/assets/assets.go index 3c0265f0c..4a629c366 100644 --- a/gateway/assets/assets.go +++ b/gateway/assets/assets.go @@ -10,8 +10,7 @@ import ( "strings" "github.com/cespare/xxhash/v2" - - ipfspath "github.com/ipfs/boxo/path" + "github.com/ipfs/boxo/path" ) //go:embed *.html *.css @@ -132,7 +131,7 @@ type Breadcrumb struct { func Breadcrumbs(urlPath string, dnslinkOrigin bool) []Breadcrumb { var ret []Breadcrumb - p, err := ipfspath.ParsePath(urlPath) + p, err := path.NewPath(urlPath) if err != nil { // No assets.Breadcrumbs, fallback to bare Path in template return ret diff --git a/gateway/blocks_backend.go b/gateway/blocks_backend.go index fc0685d62..6868d3247 100644 --- a/gateway/blocks_backend.go +++ b/gateway/blocks_backend.go @@ -7,13 +7,11 @@ import ( "fmt" "io" "net/http" - gopath "path" "strings" "github.com/ipfs/boxo/blockservice" blockstore "github.com/ipfs/boxo/blockstore" nsopts "github.com/ipfs/boxo/coreiface/options/namesys" - ifacepath "github.com/ipfs/boxo/coreiface/path" "github.com/ipfs/boxo/fetcher" bsfetcher "github.com/ipfs/boxo/fetcher/impl/blockservice" "github.com/ipfs/boxo/files" @@ -22,7 +20,7 @@ import ( uio "github.com/ipfs/boxo/ipld/unixfs/io" "github.com/ipfs/boxo/namesys" "github.com/ipfs/boxo/namesys/resolve" - ipfspath "github.com/ipfs/boxo/path" + "github.com/ipfs/boxo/path" "github.com/ipfs/boxo/path/resolver" blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-cid" @@ -147,7 +145,7 @@ func NewBlocksBackend(blockService blockservice.BlockService, opts ...BlocksBack }, nil } -func (bb *BlocksBackend) Get(ctx context.Context, path ImmutablePath, ranges ...ByteRange) (ContentPathMetadata, *GetResponse, error) { +func (bb *BlocksBackend) Get(ctx context.Context, path path.ImmutablePath, ranges ...ByteRange) (ContentPathMetadata, *GetResponse, error) { md, nd, err := bb.getNode(ctx, path) if err != nil { return md, nil, err @@ -224,7 +222,7 @@ func (bb *BlocksBackend) Get(ctx context.Context, path ImmutablePath, ranges ... return ContentPathMetadata{}, nil, fmt.Errorf("data was not a valid file or directory: %w", ErrInternalServerError) // TODO: should there be a gateway invalid content type to abstract over the various IPLD error types? } -func (bb *BlocksBackend) GetAll(ctx context.Context, path ImmutablePath) (ContentPathMetadata, files.Node, error) { +func (bb *BlocksBackend) GetAll(ctx context.Context, path path.ImmutablePath) (ContentPathMetadata, files.Node, error) { md, nd, err := bb.getNode(ctx, path) if err != nil { return md, nil, err @@ -238,7 +236,7 @@ func (bb *BlocksBackend) GetAll(ctx context.Context, path ImmutablePath) (Conten return md, n, nil } -func (bb *BlocksBackend) GetBlock(ctx context.Context, path ImmutablePath) (ContentPathMetadata, files.File, error) { +func (bb *BlocksBackend) GetBlock(ctx context.Context, path path.ImmutablePath) (ContentPathMetadata, files.File, error) { md, nd, err := bb.getNode(ctx, path) if err != nil { return md, nil, err @@ -247,7 +245,7 @@ func (bb *BlocksBackend) GetBlock(ctx context.Context, path ImmutablePath) (Cont return md, files.NewBytesFile(nd.RawData()), nil } -func (bb *BlocksBackend) Head(ctx context.Context, path ImmutablePath) (ContentPathMetadata, *HeadResponse, error) { +func (bb *BlocksBackend) Head(ctx context.Context, path path.ImmutablePath) (ContentPathMetadata, *HeadResponse, error) { md, nd, err := bb.getNode(ctx, path) if err != nil { return md, nil, err @@ -290,7 +288,7 @@ func (bb *BlocksBackend) Head(ctx context.Context, path ImmutablePath) (ContentP // https://ipld.io/specs/transport/car/carv1/#number-of-roots var emptyRoot = []cid.Cid{cid.MustParse("bafkqaaa")} -func (bb *BlocksBackend) GetCAR(ctx context.Context, p ImmutablePath, params CarParams) (ContentPathMetadata, io.ReadCloser, error) { +func (bb *BlocksBackend) GetCAR(ctx context.Context, p path.ImmutablePath, params CarParams) (ContentPathMetadata, io.ReadCloser, error) { pathMetadata, err := bb.ResolvePath(ctx, p) if err != nil { rootCid, err := cid.Decode(strings.Split(p.String(), "/")[2]) @@ -314,21 +312,19 @@ func (bb *BlocksBackend) GetCAR(ctx context.Context, p ImmutablePath, params Car // 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.FromCid(rootCid), ContentType: "", }, io.NopCloser(&buf), nil } return ContentPathMetadata{}, nil, err } - contentPathStr := p.String() - if !strings.HasPrefix(contentPathStr, "/ipfs/") { + if p.Namespace() != path.IPFSNamespace { return ContentPathMetadata{}, nil, fmt.Errorf("path does not have /ipfs/ prefix") } @@ -336,7 +332,7 @@ func (bb *BlocksBackend) GetCAR(ctx context.Context, p ImmutablePath, params Car go func() { cw, err := storage.NewWritable( w, - []cid.Cid{pathMetadata.LastSegment.Cid()}, + []cid.Cid{pathMetadata.LastSegment.RootCid()}, car.WriteAsCarV1(true), car.AllowDuplicatePuts(params.Duplicates.Bool()), ) @@ -363,7 +359,7 @@ func (bb *BlocksBackend) GetCAR(ctx context.Context, p ImmutablePath, params Car // TODO: support selectors passed as request param: https://github.com/ipfs/kubo/issues/8769 // TODO: this is very slow if blocks are remote due to linear traversal. Do we need deterministic traversals here? - carWriteErr := walkGatewaySimpleSelector(ctx, ipfspath.Path(contentPathStr), params, &lsys, pathResolver) + carWriteErr := walkGatewaySimpleSelector(ctx, p, params, &lsys, pathResolver) // io.PipeWriter.CloseWithError always returns nil. _ = w.CloseWithError(carWriteErr) @@ -373,7 +369,7 @@ func (bb *BlocksBackend) GetCAR(ctx context.Context, p ImmutablePath, params Car } // walkGatewaySimpleSelector walks the subgraph described by the path and terminal element parameters -func walkGatewaySimpleSelector(ctx context.Context, p ipfspath.Path, params CarParams, lsys *ipld.LinkSystem, pathResolver resolver.Resolver) error { +func walkGatewaySimpleSelector(ctx context.Context, p path.ImmutablePath, params CarParams, lsys *ipld.LinkSystem, pathResolver resolver.Resolver) error { // First resolve the path since we always need to. lastCid, remainder, err := pathResolver.ResolveToLastNode(ctx, p) if err != nil { @@ -541,18 +537,19 @@ func walkGatewaySimpleSelector(ctx context.Context, p ipfspath.Path, params CarP } } -func (bb *BlocksBackend) getNode(ctx context.Context, path ImmutablePath) (ContentPathMetadata, format.Node, error) { - roots, lastSeg, err := bb.getPathRoots(ctx, path) +func (bb *BlocksBackend) getNode(ctx context.Context, path path.ImmutablePath) (ContentPathMetadata, format.Node, error) { + roots, lastSeg, remainder, err := bb.getPathRoots(ctx, path) if err != nil { return ContentPathMetadata{}, nil, err } md := ContentPathMetadata{ - PathSegmentRoots: roots, - LastSegment: lastSeg, + PathSegmentRoots: roots, + LastSegment: lastSeg, + LastSegmentRemainder: remainder, } - lastRoot := lastSeg.Cid() + lastRoot := lastSeg.RootCid() nd, err := bb.dagService.Get(ctx, lastRoot) if err != nil { @@ -562,7 +559,7 @@ func (bb *BlocksBackend) getNode(ctx context.Context, path ImmutablePath) (Conte return md, nd, err } -func (bb *BlocksBackend) getPathRoots(ctx context.Context, contentPath ImmutablePath) ([]cid.Cid, ifacepath.Resolved, error) { +func (bb *BlocksBackend) getPathRoots(ctx context.Context, contentPath path.ImmutablePath) ([]cid.Cid, path.ImmutablePath, []string, error) { /* These are logical roots where each CID represent one path segment and resolves to either a directory or the root block of a file. @@ -586,56 +583,50 @@ func (bb *BlocksBackend) getPathRoots(ctx context.Context, contentPath Immutable contentPathStr := contentPath.String() pathSegments := strings.Split(contentPathStr[6:], "/") sp.WriteString(contentPathStr[:5]) // /ipfs or /ipns - var lastPath ifacepath.Resolved + var ( + lastPath path.ImmutablePath + remainder []string + ) for _, root := range pathSegments { if root == "" { continue } sp.WriteString("/") sp.WriteString(root) - resolvedSubPath, err := bb.resolvePath(ctx, ifacepath.New(sp.String())) + p, err := path.NewPath(sp.String()) + if err != nil { + return nil, nil, nil, err + } + resolvedSubPath, remainderSubPath, err := bb.resolvePath(ctx, p) if err != nil { // TODO: should we be more explicit here and is this part of the IPFSBackend contract? // The issue here was that we returned datamodel.ErrWrongKind instead of this resolver error if isErrNotFound(err) { - return nil, nil, resolver.ErrNoLink{Name: root, Node: lastPath.Cid()} + return nil, nil, nil, &resolver.ErrNoLink{Name: root, Node: lastPath.RootCid()} } - return nil, nil, err + return nil, nil, nil, err } lastPath = resolvedSubPath - pathRoots = append(pathRoots, lastPath.Cid()) + remainder = remainderSubPath + pathRoots = append(pathRoots, lastPath.RootCid()) } pathRoots = pathRoots[:len(pathRoots)-1] - return pathRoots, lastPath, nil + return pathRoots, lastPath, remainder, nil } -func (bb *BlocksBackend) ResolveMutable(ctx context.Context, p ifacepath.Path) (ImmutablePath, error) { - err := p.IsValid() - if err != nil { - return ImmutablePath{}, err - } - - ipath := ipfspath.Path(p.String()) - switch ipath.Segments()[0] { - case "ipns": - ipath, err = resolve.ResolveIPNS(ctx, bb.namesys, ipath) - if err != nil { - return ImmutablePath{}, err - } - imPath, err := NewImmutablePath(ifacepath.New(ipath.String())) +func (bb *BlocksBackend) ResolveMutable(ctx context.Context, p path.Path) (path.ImmutablePath, error) { + switch p.Namespace() { + case path.IPNSNamespace: + p, err := resolve.ResolveIPNS(ctx, bb.namesys, p) if err != nil { - return ImmutablePath{}, err - } - return imPath, nil - case "ipfs": - imPath, err := NewImmutablePath(ifacepath.New(ipath.String())) - if err != nil { - return ImmutablePath{}, err + return nil, err } - return imPath, nil + return path.NewImmutablePath(p) + case path.IPFSNamespace: + return path.NewImmutablePath(p) default: - return ImmutablePath{}, NewErrorStatusCode(fmt.Errorf("unsupported path namespace: %s", p.Namespace()), http.StatusNotImplemented) + return nil, NewErrorStatusCode(fmt.Errorf("unsupported path namespace: %s", p.Namespace()), http.StatusNotImplemented) } } @@ -659,73 +650,75 @@ func (bb *BlocksBackend) GetIPNSRecord(ctx context.Context, c cid.Cid) ([]byte, return bb.routing.GetValue(ctx, "/ipns/"+string(id)) } -func (bb *BlocksBackend) GetDNSLinkRecord(ctx context.Context, hostname string) (ifacepath.Path, error) { +func (bb *BlocksBackend) GetDNSLinkRecord(ctx context.Context, hostname string) (path.Path, error) { if bb.namesys != nil { p, err := bb.namesys.Resolve(ctx, "/ipns/"+hostname, nsopts.Depth(1)) if err == namesys.ErrResolveRecursion { err = nil } - return ifacepath.New(p.String()), err + return p, err } return nil, NewErrorStatusCode(errors.New("not implemented"), http.StatusNotImplemented) } -func (bb *BlocksBackend) IsCached(ctx context.Context, p ifacepath.Path) bool { - rp, err := bb.resolvePath(ctx, p) +func (bb *BlocksBackend) IsCached(ctx context.Context, p path.Path) bool { + rp, _, err := bb.resolvePath(ctx, p) if err != nil { return false } - has, _ := bb.blockStore.Has(ctx, rp.Cid()) + has, _ := bb.blockStore.Has(ctx, rp.RootCid()) return has } -func (bb *BlocksBackend) ResolvePath(ctx context.Context, path ImmutablePath) (ContentPathMetadata, error) { - roots, lastSeg, err := bb.getPathRoots(ctx, path) +func (bb *BlocksBackend) ResolvePath(ctx context.Context, path path.ImmutablePath) (ContentPathMetadata, error) { + roots, lastSeg, remainder, err := bb.getPathRoots(ctx, path) if err != nil { return ContentPathMetadata{}, err } md := ContentPathMetadata{ - PathSegmentRoots: roots, - LastSegment: lastSeg, + PathSegmentRoots: roots, + LastSegment: lastSeg, + LastSegmentRemainder: remainder, } return md, nil } -func (bb *BlocksBackend) resolvePath(ctx context.Context, p ifacepath.Path) (ifacepath.Resolved, error) { - if _, ok := p.(ifacepath.Resolved); ok { - return p.(ifacepath.Resolved), nil +func (bb *BlocksBackend) resolvePath(ctx context.Context, p path.Path) (path.ImmutablePath, []string, error) { + var err error + if p.Namespace() == path.IPNSNamespace { + p, err = resolve.ResolveIPNS(ctx, bb.namesys, p) + if err != nil { + return nil, nil, err + } } - err := p.IsValid() - if err != nil { - return nil, err + if p.Namespace() != path.IPFSNamespace { + return nil, nil, fmt.Errorf("unsupported path namespace: %s", p.Namespace()) } - ipath := ipfspath.Path(p.String()) - if ipath.Segments()[0] == "ipns" { - ipath, err = resolve.ResolveIPNS(ctx, bb.namesys, ipath) - if err != nil { - return nil, err - } + imPath, err := path.NewImmutablePath(p) + if err != nil { + return nil, nil, err } - if ipath.Segments()[0] != "ipfs" { - return nil, fmt.Errorf("unsupported path namespace: %s", p.Namespace()) + node, remainder, err := bb.resolver.ResolveToLastNode(ctx, imPath) + if err != nil { + return nil, nil, err } - node, rest, err := bb.resolver.ResolveToLastNode(ctx, ipath) + p, err = path.Join(path.FromCid(node), remainder...) if err != nil { - return nil, err + return nil, nil, err } - root, err := cid.Parse(ipath.Segments()[1]) + imPath, err = path.NewImmutablePath(p) if err != nil { - return nil, err + return nil, nil, err } - return ifacepath.NewResolvedPath(ipath, node, root, gopath.Join(rest...)), nil + return imPath, remainder, nil } type nodeGetterToCarExporer struct { diff --git a/gateway/errors.go b/gateway/errors.go index a39bd2a01..08b532e82 100644 --- a/gateway/errors.go +++ b/gateway/errors.go @@ -179,15 +179,14 @@ func webError(w http.ResponseWriter, r *http.Request, c *Config, err error, defa // isErrNotFound returns true for IPLD errors that should return 4xx errors (e.g. the path doesn't exist, the data is // the wrong type, etc.), rather than issues with just finding and retrieving the data. func isErrNotFound(err error) bool { + if errors.Is(err, &resolver.ErrNoLink{}) { + return true + } + // Checks if err is of a type that does not implement the .Is interface and // cannot be directly compared to. Therefore, errors.Is cannot be used. for { - _, ok := err.(resolver.ErrNoLink) - if ok { - return true - } - - _, ok = err.(datamodel.ErrWrongKind) + _, ok := err.(datamodel.ErrWrongKind) if ok { return true } diff --git a/gateway/gateway.go b/gateway/gateway.go index 089dd236c..bfafa48e1 100644 --- a/gateway/gateway.go +++ b/gateway/gateway.go @@ -9,10 +9,10 @@ import ( "strconv" "strings" - "github.com/ipfs/boxo/coreiface/path" "github.com/ipfs/boxo/files" "github.com/ipfs/boxo/gateway/assets" "github.com/ipfs/boxo/ipld/unixfs" + "github.com/ipfs/boxo/path" "github.com/ipfs/go-cid" ) @@ -93,38 +93,6 @@ type PublicGateway struct { DeserializedResponses bool } -// ImmutablePath represents a [path.Path] that is not mutable. -// -// TODO: Is this what we want for ImmutablePath? -type ImmutablePath struct { - p path.Path -} - -func NewImmutablePath(p path.Path) (ImmutablePath, error) { - if p.Mutable() { - return ImmutablePath{}, fmt.Errorf("path cannot be mutable") - } - return ImmutablePath{p: p}, nil -} - -func (i ImmutablePath) String() string { - return i.p.String() -} - -func (i ImmutablePath) Namespace() string { - return i.p.Namespace() -} - -func (i ImmutablePath) Mutable() bool { - return false -} - -func (i ImmutablePath) IsValid() error { - return i.p.IsValid() -} - -var _ path.Path = (*ImmutablePath)(nil) - type CarParams struct { Range *DagByteRange Scope DagScope @@ -241,9 +209,10 @@ func (d DuplicateBlocksPolicy) String() string { } type ContentPathMetadata struct { - PathSegmentRoots []cid.Cid - LastSegment path.Resolved - ContentType string // Only used for UnixFS requests + PathSegmentRoots []cid.Cid + LastSegment path.ImmutablePath + LastSegmentRemainder []string + ContentType string // Only used for UnixFS requests } // ByteRange describes a range request within a UnixFS file. "From" and "To" mostly @@ -363,14 +332,14 @@ type IPFSBackend interface { // block rather than as an [io.ReadCloser] that starts at the beginning of the range request. // // [HTTP Byte Ranges]: https://httpwg.org/specs/rfc9110.html#rfc.section.14.1.2 - Get(context.Context, ImmutablePath, ...ByteRange) (ContentPathMetadata, *GetResponse, error) + Get(context.Context, path.ImmutablePath, ...ByteRange) (ContentPathMetadata, *GetResponse, error) // GetAll returns a UnixFS file or directory depending on what the path is that has been requested. Directories should // include all content recursively. - GetAll(context.Context, ImmutablePath) (ContentPathMetadata, files.Node, error) + GetAll(context.Context, path.ImmutablePath) (ContentPathMetadata, files.Node, error) // GetBlock returns a single block of data - GetBlock(context.Context, ImmutablePath) (ContentPathMetadata, files.File, error) + GetBlock(context.Context, path.ImmutablePath) (ContentPathMetadata, files.File, error) // Head returns a [HeadResponse] depending on what the path is that has been requested. // For UnixFS files (and raw blocks) should return the size of the file and either set the ContentType in @@ -381,16 +350,16 @@ type IPFSBackend interface { // // For all other data types (e.g. (DAG-)CBOR/JSON blocks) returning the size information as a file while setting // the content-type is sufficient. - Head(context.Context, ImmutablePath) (ContentPathMetadata, *HeadResponse, error) + Head(context.Context, path.ImmutablePath) (ContentPathMetadata, *HeadResponse, error) // ResolvePath resolves the path using UnixFS resolver. If the path does not // exist due to a missing link, it should return an error of type: // NewErrorResponse(fmt.Errorf("no link named %q under %s", name, cid), http.StatusNotFound) - ResolvePath(context.Context, ImmutablePath) (ContentPathMetadata, error) + ResolvePath(context.Context, path.ImmutablePath) (ContentPathMetadata, error) // GetCAR returns a CAR file for the given immutable path. It returns an error // if there was an issue before the CAR streaming begins. - GetCAR(context.Context, ImmutablePath, CarParams) (ContentPathMetadata, io.ReadCloser, error) + GetCAR(context.Context, path.ImmutablePath, CarParams) (ContentPathMetadata, io.ReadCloser, error) // IsCached returns whether or not the path exists locally. IsCached(context.Context, path.Path) bool @@ -404,7 +373,7 @@ type IPFSBackend interface { // // For example, given a mapping from `/ipns/dnslink.tld -> /ipns/ipns-id/mydirectory` and `/ipns/ipns-id` to // `/ipfs/some-cid`, the result of passing `/ipns/dnslink.tld/myfile` would be `/ipfs/some-cid/mydirectory/myfile`. - ResolveMutable(context.Context, path.Path) (ImmutablePath, error) + ResolveMutable(context.Context, path.Path) (path.ImmutablePath, error) // GetDNSLinkRecord returns the DNSLink TXT record for the provided FQDN. // Unlike ResolvePath, it does not perform recursive resolution. It only diff --git a/gateway/gateway_test.go b/gateway/gateway_test.go index d041cad55..775a4e24b 100644 --- a/gateway/gateway_test.go +++ b/gateway/gateway_test.go @@ -9,10 +9,9 @@ import ( "testing" "time" - ipath "github.com/ipfs/boxo/coreiface/path" "github.com/ipfs/boxo/files" "github.com/ipfs/boxo/namesys" - path "github.com/ipfs/boxo/path" + "github.com/ipfs/boxo/path" "github.com/ipfs/boxo/path/resolver" "github.com/ipfs/go-cid" ipld "github.com/ipfs/go-ipld-format" @@ -26,14 +25,23 @@ func TestGatewayGet(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - k, err := backend.resolvePathNoRootsReturned(ctx, ipath.Join(ipath.IpfsPath(root), "subdir", "fnord")) + p, err := path.Join(path.FromCid(root), "subdir", "fnord") require.NoError(t, err) - backend.namesys["/ipns/example.com"] = path.FromCid(k.Cid()) - backend.namesys["/ipns/working.example.com"] = path.FromString(k.String()) - backend.namesys["/ipns/double.example.com"] = path.FromString("/ipns/working.example.com") - backend.namesys["/ipns/triple.example.com"] = path.FromString("/ipns/double.example.com") - backend.namesys["/ipns/broken.example.com"] = path.FromString("/ipns/" + k.Cid().String()) + k, err := backend.resolvePathNoRootsReturned(ctx, p) + require.NoError(t, err) + + mustMakeDNSLinkPath := func(domain string) path.Path { + p, err := path.NewPath("/ipns/" + domain) + require.NoError(t, err) + return p + } + + backend.namesys["/ipns/example.com"] = path.FromCid(k.RootCid()) + backend.namesys["/ipns/working.example.com"] = k + backend.namesys["/ipns/double.example.com"] = mustMakeDNSLinkPath("working.example.com") + backend.namesys["/ipns/triple.example.com"] = mustMakeDNSLinkPath("double.example.com") + backend.namesys["/ipns/broken.example.com"] = mustMakeDNSLinkPath(k.RootCid().String()) // We picked .man because: // 1. It's a valid TLD. // 2. Go treats it as the file extension for "man" files (even though @@ -41,7 +49,7 @@ func TestGatewayGet(t *testing.T) { // // Unfortunately, this may not work on all platforms as file type // detection is platform dependent. - backend.namesys["/ipns/example.man"] = path.FromString(k.String()) + backend.namesys["/ipns/example.man"] = k for _, test := range []struct { host string @@ -50,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", "/" + 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", 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.RootCid().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: 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"}, @@ -64,7 +72,7 @@ func TestGatewayGet(t *testing.T) { {"working.example.com", "/", http.StatusOK, "fnord"}, {"double.example.com", "/", http.StatusOK, "fnord"}, {"triple.example.com", "/", http.StatusOK, "fnord"}, - {"working.example.com", k.String(), http.StatusNotFound, "failed to resolve /ipns/working.example.com" + k.String() + ": no link named \"ipfs\" under " + k.Cid().String() + "\n"}, + {"working.example.com", k.String(), http.StatusNotFound, "failed to resolve /ipns/working.example.com" + k.String() + ": no link named \"ipfs\" under " + k.RootCid().String() + "\n"}, {"broken.example.com", "/", http.StatusInternalServerError, "failed to resolve /ipns/broken.example.com/: " + namesys.ErrResolveFailed.Error() + "\n"}, {"broken.example.com", k.String(), http.StatusInternalServerError, "failed to resolve /ipns/broken.example.com" + k.String() + ": " + namesys.ErrResolveFailed.Error() + "\n"}, // This test case ensures we don't treat the TLD as a file extension. @@ -707,43 +715,43 @@ type errorMockBackend struct { err error } -func (mb *errorMockBackend) Get(ctx context.Context, path ImmutablePath, getRange ...ByteRange) (ContentPathMetadata, *GetResponse, error) { +func (mb *errorMockBackend) Get(ctx context.Context, path path.ImmutablePath, getRange ...ByteRange) (ContentPathMetadata, *GetResponse, error) { return ContentPathMetadata{}, nil, mb.err } -func (mb *errorMockBackend) GetAll(ctx context.Context, path ImmutablePath) (ContentPathMetadata, files.Node, error) { +func (mb *errorMockBackend) GetAll(ctx context.Context, path path.ImmutablePath) (ContentPathMetadata, files.Node, error) { return ContentPathMetadata{}, nil, mb.err } -func (mb *errorMockBackend) GetBlock(ctx context.Context, path ImmutablePath) (ContentPathMetadata, files.File, error) { +func (mb *errorMockBackend) GetBlock(ctx context.Context, path path.ImmutablePath) (ContentPathMetadata, files.File, error) { return ContentPathMetadata{}, nil, mb.err } -func (mb *errorMockBackend) Head(ctx context.Context, path ImmutablePath) (ContentPathMetadata, *HeadResponse, error) { +func (mb *errorMockBackend) Head(ctx context.Context, path path.ImmutablePath) (ContentPathMetadata, *HeadResponse, error) { return ContentPathMetadata{}, nil, mb.err } -func (mb *errorMockBackend) GetCAR(ctx context.Context, path ImmutablePath, params CarParams) (ContentPathMetadata, io.ReadCloser, error) { +func (mb *errorMockBackend) GetCAR(ctx context.Context, path path.ImmutablePath, params CarParams) (ContentPathMetadata, io.ReadCloser, error) { return ContentPathMetadata{}, nil, mb.err } -func (mb *errorMockBackend) ResolveMutable(ctx context.Context, path ipath.Path) (ImmutablePath, error) { - return ImmutablePath{}, mb.err +func (mb *errorMockBackend) ResolveMutable(ctx context.Context, p path.Path) (path.ImmutablePath, error) { + return nil, mb.err } func (mb *errorMockBackend) GetIPNSRecord(ctx context.Context, c cid.Cid) ([]byte, error) { return nil, mb.err } -func (mb *errorMockBackend) GetDNSLinkRecord(ctx context.Context, hostname string) (ipath.Path, error) { +func (mb *errorMockBackend) GetDNSLinkRecord(ctx context.Context, hostname string) (path.Path, error) { return nil, mb.err } -func (mb *errorMockBackend) IsCached(ctx context.Context, p ipath.Path) bool { +func (mb *errorMockBackend) IsCached(ctx context.Context, p path.Path) bool { return false } -func (mb *errorMockBackend) ResolvePath(ctx context.Context, path ImmutablePath) (ContentPathMetadata, error) { +func (mb *errorMockBackend) ResolvePath(ctx context.Context, path path.ImmutablePath) (ContentPathMetadata, error) { return ContentPathMetadata{}, mb.err } @@ -763,7 +771,7 @@ func TestErrorBubblingFromBackend(t *testing.T) { } testError("500 Not Found from IPLD", &ipld.ErrNotFound{}, http.StatusInternalServerError) - testError("404 Not Found from path resolver", resolver.ErrNoLink{}, http.StatusNotFound) + testError("404 Not Found from path resolver", &resolver.ErrNoLink{}, http.StatusNotFound) testError("502 Bad Gateway", ErrBadGateway, http.StatusBadGateway) testError("504 Gateway Timeout", ErrGatewayTimeout, http.StatusGatewayTimeout) @@ -791,27 +799,27 @@ type panicMockBackend struct { panicOnHostnameHandler bool } -func (mb *panicMockBackend) Get(ctx context.Context, immutablePath ImmutablePath, ranges ...ByteRange) (ContentPathMetadata, *GetResponse, error) { +func (mb *panicMockBackend) Get(ctx context.Context, immutablePath path.ImmutablePath, ranges ...ByteRange) (ContentPathMetadata, *GetResponse, error) { panic("i am panicking") } -func (mb *panicMockBackend) GetAll(ctx context.Context, immutablePath ImmutablePath) (ContentPathMetadata, files.Node, error) { +func (mb *panicMockBackend) GetAll(ctx context.Context, immutablePath path.ImmutablePath) (ContentPathMetadata, files.Node, error) { panic("i am panicking") } -func (mb *panicMockBackend) GetBlock(ctx context.Context, immutablePath ImmutablePath) (ContentPathMetadata, files.File, error) { +func (mb *panicMockBackend) GetBlock(ctx context.Context, immutablePath path.ImmutablePath) (ContentPathMetadata, files.File, error) { panic("i am panicking") } -func (mb *panicMockBackend) Head(ctx context.Context, immutablePath ImmutablePath) (ContentPathMetadata, *HeadResponse, error) { +func (mb *panicMockBackend) Head(ctx context.Context, immutablePath path.ImmutablePath) (ContentPathMetadata, *HeadResponse, error) { panic("i am panicking") } -func (mb *panicMockBackend) GetCAR(ctx context.Context, immutablePath ImmutablePath, params CarParams) (ContentPathMetadata, io.ReadCloser, error) { +func (mb *panicMockBackend) GetCAR(ctx context.Context, immutablePath path.ImmutablePath, params CarParams) (ContentPathMetadata, io.ReadCloser, error) { panic("i am panicking") } -func (mb *panicMockBackend) ResolveMutable(ctx context.Context, p ipath.Path) (ImmutablePath, error) { +func (mb *panicMockBackend) ResolveMutable(ctx context.Context, p path.Path) (path.ImmutablePath, error) { panic("i am panicking") } @@ -819,7 +827,7 @@ func (mb *panicMockBackend) GetIPNSRecord(ctx context.Context, c cid.Cid) ([]byt panic("i am panicking") } -func (mb *panicMockBackend) GetDNSLinkRecord(ctx context.Context, hostname string) (ipath.Path, error) { +func (mb *panicMockBackend) GetDNSLinkRecord(ctx context.Context, hostname string) (path.Path, error) { // GetDNSLinkRecord is also called on the WithHostname handler. We have this option // to disable panicking here so we can test if both the regular gateway handler // and the hostname handler can handle panics. @@ -830,11 +838,11 @@ func (mb *panicMockBackend) GetDNSLinkRecord(ctx context.Context, hostname strin return nil, errors.New("not implemented") } -func (mb *panicMockBackend) IsCached(ctx context.Context, p ipath.Path) bool { +func (mb *panicMockBackend) IsCached(ctx context.Context, p path.Path) bool { panic("i am panicking") } -func (mb *panicMockBackend) ResolvePath(ctx context.Context, immutablePath ImmutablePath) (ContentPathMetadata, error) { +func (mb *panicMockBackend) ResolvePath(ctx context.Context, immutablePath path.ImmutablePath) (ContentPathMetadata, error) { panic("i am panicking") } diff --git a/gateway/handler.go b/gateway/handler.go index af20e2b6e..6bcd515f9 100644 --- a/gateway/handler.go +++ b/gateway/handler.go @@ -16,9 +16,9 @@ import ( "strings" "time" - ipath "github.com/ipfs/boxo/coreiface/path" "github.com/ipfs/boxo/gateway/assets" "github.com/ipfs/boxo/ipns" + "github.com/ipfs/boxo/path" cid "github.com/ipfs/go-cid" logging "github.com/ipfs/go-log/v2" "github.com/libp2p/go-libp2p/core/peer" @@ -188,12 +188,12 @@ type requestData struct { // Defined for all requests. begin time.Time logger *zap.SugaredLogger - contentPath ipath.Path + contentPath path.Path responseFormat string responseParams map[string]string // Defined for non IPNS Record requests. - immutablePath ImmutablePath + immutablePath path.ImmutablePath // Defined if resolution has already happened. pathMetadata *ContentPathMetadata @@ -202,9 +202,9 @@ type requestData struct { // mostlyResolvedPath is an opportunistic optimization that returns the mostly // resolved version of ImmutablePath available. It does not guarantee it is fully // resolved, nor that it is the original. -func (rq *requestData) mostlyResolvedPath() ImmutablePath { +func (rq *requestData) mostlyResolvedPath() path.ImmutablePath { if rq.pathMetadata != nil { - imPath, err := NewImmutablePath(rq.pathMetadata.LastSegment) + imPath, err := path.NewImmutablePath(rq.pathMetadata.LastSegment) if err != nil { // This will never happen. This error has previously been checked in // [handleIfNoneMatch] and the request will have returned 500. @@ -223,12 +223,18 @@ func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) { if handleProtocolHandlerRedirect(w, r, i.config) || i.handleServiceWorkerRegistration(w, r) || - handleIpnsB58mhToCidRedirection(w, r) { + handleIpnsB58mhToCidRedirection(w, r) || + i.handleSuperfluousNamespace(w, r) { return } var success bool - contentPath := ipath.New(r.URL.Path) + contentPath, err := path.NewPath(r.URL.Path) + if err != nil { + i.webError(w, r, err, http.StatusBadRequest) + return + } + ctx := context.WithValue(r.Context(), ContentPathKey, contentPath) r = r.WithContext(ctx) @@ -238,13 +244,7 @@ func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) { } }() - if i.handleOnlyIfCached(w, r, contentPath) || - i.handleSuperfluousNamespace(w, r, contentPath) { - return - } - - if err := contentPath.IsValid(); err != nil { - i.webError(w, r, err, http.StatusBadRequest) + if i.handleOnlyIfCached(w, r, contentPath) { return } @@ -292,7 +292,7 @@ func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) { return } } else { - rq.immutablePath, err = NewImmutablePath(contentPath) + rq.immutablePath, err = path.NewImmutablePath(contentPath) if err != nil { err = fmt.Errorf("path was expected to be immutable, but was not %s: %w", debugStr(contentPath.String()), err) i.webError(w, r, err, http.StatusInternalServerError) @@ -371,7 +371,7 @@ func (i *handler) isDeserializedResponsePossible(r *http.Request) bool { // in the [Trustless Gateway] spec. // // [Trustless Gateway]: https://specs.ipfs.tech/http-gateways/trustless-gateway/ -func (i *handler) isTrustlessRequest(contentPath ipath.Path, responseFormat string) bool { +func (i *handler) isTrustlessRequest(contentPath path.Path, responseFormat string) bool { // Only allow "/{#1}/{#2}"-like paths. trimmedPath := strings.Trim(contentPath.String(), "/") pathComponents := strings.Split(trimmedPath, "/") @@ -379,7 +379,7 @@ func (i *handler) isTrustlessRequest(contentPath ipath.Path, responseFormat stri return false } - if contentPath.Namespace() == "ipns" { + if contentPath.Namespace() == path.IPNSNamespace { // TODO: only ipns records allowed until https://github.com/ipfs/specs/issues/369 is resolved if responseFormat != ipnsRecordResponseFormat { return false @@ -415,7 +415,7 @@ func panicHandler(w http.ResponseWriter) { } } -func addCacheControlHeaders(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, cid cid.Cid, responseFormat string) (modtime time.Time) { +func addCacheControlHeaders(w http.ResponseWriter, r *http.Request, contentPath path.Path, cid cid.Cid, responseFormat string) (modtime time.Time) { // Best effort attempt to set an Etag based on the CID and response format. // Setting an ETag is handled separately for CARs and IPNS records. if etag := getEtag(r, cid, responseFormat); etag != "" { @@ -452,7 +452,7 @@ func addCacheControlHeaders(w http.ResponseWriter, r *http.Request, contentPath // - Creation of HTML links that trigger "Save As.." dialog instead of being rendered by the browser // - Overriding the filename used when saving sub-resource assets on HTML page // - providing a default filename for HTTP clients when downloading direct /ipfs/CID without any subpath -func addContentDispositionHeader(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) string { +func addContentDispositionHeader(w http.ResponseWriter, r *http.Request, contentPath path.Path) string { // URL param ?filename=cat.jpg triggers Content-Disposition: [..] filename // which impacts default name used in "Save As.." dialog name := getFilename(contentPath) @@ -470,7 +470,7 @@ func addContentDispositionHeader(w http.ResponseWriter, r *http.Request, content return name } -func getFilename(contentPath ipath.Path) string { +func getFilename(contentPath path.Path) string { s := contentPath.String() if (strings.HasPrefix(s, ipfsPathPrefix) || strings.HasPrefix(s, ipnsPathPrefix)) && strings.Count(gopath.Clean(s), "/") <= 2 { // Don't want to treat ipfs.io in /ipns/ipfs.io as a filename. @@ -520,7 +520,7 @@ func setIpfsRootsHeader(w http.ResponseWriter, rq *requestData, md *ContentPathM for _, c := range rq.pathMetadata.PathSegmentRoots { pathRoots = append(pathRoots, c.String()) } - pathRoots = append(pathRoots, rq.pathMetadata.LastSegment.Cid().String()) + pathRoots = append(pathRoots, rq.pathMetadata.LastSegment.RootCid().String()) rootCidList := strings.Join(pathRoots, ",") // convention from rfc2616#sec4.2 w.Header().Set("X-Ipfs-Roots", rootCidList) @@ -675,7 +675,7 @@ func (i *handler) handleIfNoneMatch(w http.ResponseWriter, r *http.Request, rq * if ifNoneMatch := r.Header.Get("If-None-Match"); ifNoneMatch != "" { pathMetadata, err := i.backend.ResolvePath(r.Context(), rq.immutablePath) if err != nil { - var forwardedPath ImmutablePath + var forwardedPath path.ImmutablePath var continueProcessing bool if isWebRequest(rq.responseFormat) { forwardedPath, continueProcessing = i.handleWebRequestErrors(w, r, rq.mostlyResolvedPath(), rq.immutablePath, rq.contentPath, err, rq.logger) @@ -690,7 +690,7 @@ func (i *handler) handleIfNoneMatch(w http.ResponseWriter, r *http.Request, rq * } } - pathCid := pathMetadata.LastSegment.Cid() + pathCid := pathMetadata.LastSegment.RootCid() // Checks against both file, dir listing, and dag index Etags. // This is an inexpensive check, and it happens before we do any I/O. @@ -705,7 +705,7 @@ func (i *handler) handleIfNoneMatch(w http.ResponseWriter, r *http.Request, rq * } // Check if the resolvedPath is an immutable path. - _, err = NewImmutablePath(pathMetadata.LastSegment) + _, err = path.NewImmutablePath(pathMetadata.LastSegment) if err != nil { i.webError(w, r, err, http.StatusInternalServerError) return true @@ -726,7 +726,7 @@ func isWebRequest(responseFormat string) bool { } // handleRequestErrors is used when request type is other than Web+UnixFS -func (i *handler) handleRequestErrors(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, err error) bool { +func (i *handler) handleRequestErrors(w http.ResponseWriter, r *http.Request, contentPath path.Path, err error) bool { if err == nil { return true } @@ -737,7 +737,7 @@ func (i *handler) handleRequestErrors(w http.ResponseWriter, r *http.Request, co // handleWebRequestErrors is used when request type is Web+UnixFS and err could // be a 404 (Not Found) that should be recovered via _redirects file (IPIP-290) -func (i *handler) handleWebRequestErrors(w http.ResponseWriter, r *http.Request, maybeResolvedImPath, immutableContentPath ImmutablePath, contentPath ipath.Path, err error, logger *zap.SugaredLogger) (ImmutablePath, bool) { +func (i *handler) handleWebRequestErrors(w http.ResponseWriter, r *http.Request, maybeResolvedImPath, immutableContentPath path.ImmutablePath, contentPath path.Path, err error, logger *zap.SugaredLogger) (path.ImmutablePath, bool) { if err == nil { return maybeResolvedImPath, true } @@ -745,14 +745,14 @@ func (i *handler) handleWebRequestErrors(w http.ResponseWriter, r *http.Request, if errors.Is(err, ErrServiceUnavailable) { err = fmt.Errorf("failed to resolve %s: %w", debugStr(contentPath.String()), err) i.webError(w, r, err, http.StatusServiceUnavailable) - return ImmutablePath{}, false + return nil, false } // If the error is not an IPLD traversal error then we should not be looking for _redirects or legacy 404s if !isErrNotFound(err) { err = fmt.Errorf("failed to resolve %s: %w", debugStr(contentPath.String()), err) i.webError(w, r, err, http.StatusInternalServerError) - return ImmutablePath{}, false + return nil, false } // If we have origin isolation (subdomain gw, DNSLink website), @@ -774,17 +774,17 @@ func (i *handler) handleWebRequestErrors(w http.ResponseWriter, r *http.Request, // follow https://docs.ipfs.tech/how-to/websites-on-ipfs/redirects-and-custom-404s/ instead. if i.serveLegacy404IfPresent(w, r, immutableContentPath, logger) { logger.Debugw("served legacy 404") - return ImmutablePath{}, false + return nil, false } err = fmt.Errorf("failed to resolve %s: %w", debugStr(contentPath.String()), err) i.webError(w, r, err, http.StatusInternalServerError) - return ImmutablePath{}, false + return nil, false } // Detect 'Cache-Control: only-if-cached' in request and return data if it is already in the local datastore. // https://github.com/ipfs/specs/blob/main/http-gateways/PATH_GATEWAY.md#cache-control-request-header -func (i *handler) handleOnlyIfCached(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) bool { +func (i *handler) handleOnlyIfCached(w http.ResponseWriter, r *http.Request, contentPath path.Path) bool { if r.Header.Get("Cache-Control") == "only-if-cached" { if !i.backend.IsCached(r.Context(), contentPath) { if r.Method == http.MethodHead { @@ -894,23 +894,19 @@ func handleIpnsB58mhToCidRedirection(w http.ResponseWriter, r *http.Request) boo // 'intended' path is valid. This is in case gremlins were tickled // wrong way and user ended up at /ipfs/ipfs/{cid} or /ipfs/ipns/{id} // like in bafybeien3m7mdn6imm425vc2s22erzyhbvk5n3ofzgikkhmdkh5cuqbpbq :^)) -func (i *handler) handleSuperfluousNamespace(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) bool { - // If the path is valid, there's nothing to do - if pathErr := contentPath.IsValid(); pathErr == nil { - return false - } - +func (i *handler) handleSuperfluousNamespace(w http.ResponseWriter, r *http.Request) bool { // If there's no superflous namespace, there's nothing to do if !(strings.HasPrefix(r.URL.Path, "/ipfs/ipfs/") || strings.HasPrefix(r.URL.Path, "/ipfs/ipns/")) { return false } // Attempt to fix the superflous namespace - intendedPath := ipath.New(strings.TrimPrefix(r.URL.Path, "/ipfs")) - if err := intendedPath.IsValid(); err != nil { + intendedPath, err := path.NewPath(strings.TrimPrefix(r.URL.Path, "/ipfs")) + if err != nil { i.webError(w, r, fmt.Errorf("invalid ipfs path: %w", err), http.StatusBadRequest) return true } + intendedURL := intendedPath.String() if r.URL.RawQuery != "" { // we render HTML, so ensure query entries are properly escaped @@ -935,7 +931,7 @@ func (i *handler) handleSuperfluousNamespace(w http.ResponseWriter, r *http.Requ } // getTemplateGlobalData returns the global data necessary by most templates. -func (i *handler) getTemplateGlobalData(r *http.Request, contentPath ipath.Path) assets.GlobalData { +func (i *handler) getTemplateGlobalData(r *http.Request, contentPath path.Path) assets.GlobalData { // gatewayURL is used to link to other root CIDs. THis will be blank unless // subdomain or DNSLink resolution is being used for this request. var gatewayURL string diff --git a/gateway/handler_block.go b/gateway/handler_block.go index 9d2a93b38..461e306d8 100644 --- a/gateway/handler_block.go +++ b/gateway/handler_block.go @@ -22,7 +22,7 @@ func (i *handler) serveRawBlock(ctx context.Context, w http.ResponseWriter, r *h setIpfsRootsHeader(w, rq, &pathMetadata) - blockCid := pathMetadata.LastSegment.Cid() + blockCid := pathMetadata.LastSegment.RootCid() // Set Content-Disposition var name string diff --git a/gateway/handler_car.go b/gateway/handler_car.go index 000e0dc9c..0b2162c4f 100644 --- a/gateway/handler_car.go +++ b/gateway/handler_car.go @@ -10,6 +10,7 @@ import ( "time" "github.com/cespare/xxhash/v2" + "github.com/ipfs/boxo/path" "github.com/ipfs/go-cid" "go.opentelemetry.io/otel/attribute" @@ -203,7 +204,7 @@ func buildContentTypeFromCarParams(params CarParams) string { return h.String() } -func getCarRootCidAndLastSegment(imPath ImmutablePath) (cid.Cid, string, error) { +func getCarRootCidAndLastSegment(imPath path.ImmutablePath) (cid.Cid, string, error) { imPathStr := imPath.String() if !strings.HasPrefix(imPathStr, "/ipfs/") { return cid.Undef, "", fmt.Errorf("path does not have /ipfs/ prefix") @@ -224,7 +225,7 @@ func getCarRootCidAndLastSegment(imPath ImmutablePath) (cid.Cid, string, error) return rootCid, lastSegment, err } -func getCarEtag(imPath ImmutablePath, params CarParams, rootCid cid.Cid) string { +func getCarEtag(imPath path.ImmutablePath, params CarParams, rootCid cid.Cid) string { data := imPath.String() if params.Scope != DagScopeAll { data += string(params.Scope) diff --git a/gateway/handler_car_test.go b/gateway/handler_car_test.go index 65777453d..efbad2b22 100644 --- a/gateway/handler_car_test.go +++ b/gateway/handler_car_test.go @@ -4,7 +4,7 @@ import ( "net/http" "testing" - "github.com/ipfs/boxo/coreiface/path" + "github.com/ipfs/boxo/path" "github.com/ipfs/go-cid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -143,7 +143,7 @@ func TestGetCarEtag(t *testing.T) { cid, err := cid.Parse("bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4") require.NoError(t, err) - imPath, err := NewImmutablePath(path.IpfsPath(cid)) + imPath, err := path.NewImmutablePath(path.FromCid(cid)) require.NoError(t, err) t.Run("Etag with entity-bytes=0:* is the same as without query param", func(t *testing.T) { diff --git a/gateway/handler_codec.go b/gateway/handler_codec.go index 97dfaad0a..c1abb1d27 100644 --- a/gateway/handler_codec.go +++ b/gateway/handler_codec.go @@ -10,8 +10,8 @@ import ( "strings" "time" - ipath "github.com/ipfs/boxo/coreiface/path" "github.com/ipfs/boxo/gateway/assets" + "github.com/ipfs/boxo/path" "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime/multicodec" "github.com/ipld/go-ipld-prime/node/basicnode" @@ -83,16 +83,17 @@ func (i *handler) renderCodec(ctx context.Context, w http.ResponseWriter, r *htt ctx, span := spanTrace(ctx, "Handler.RenderCodec", trace.WithAttributes(attribute.String("path", resolvedPath.String()), attribute.String("requestedContentType", rq.responseFormat))) defer span.End() - blockCid := resolvedPath.Cid() + blockCid := resolvedPath.RootCid() cidCodec := mc.Code(blockCid.Prefix().Codec) responseContentType := rq.responseFormat // If the resolved path still has some remainder, return error for now. // TODO: handle this when we have IPLD Patch (https://ipld.io/specs/patch/) via HTTP PUT // TODO: (depends on https://github.com/ipfs/kubo/issues/4801 and https://github.com/ipfs/kubo/issues/4782) - if resolvedPath.Remainder() != "" { - path := strings.TrimSuffix(resolvedPath.String(), resolvedPath.Remainder()) - err := fmt.Errorf("%q of %q could not be returned: reading IPLD Kinds other than Links (CBOR Tag 42) is not implemented: try reading %q instead", resolvedPath.Remainder(), resolvedPath.String(), path) + if len(rq.pathMetadata.LastSegmentRemainder) != 0 { + remainderStr := path.SegmentsToString(rq.pathMetadata.LastSegmentRemainder...) + path := strings.TrimSuffix(resolvedPath.String(), remainderStr) + err := fmt.Errorf("%q of %q could not be returned: reading IPLD Kinds other than Links (CBOR Tag 42) is not implemented: try reading %q instead", remainderStr, resolvedPath.String(), path) i.webError(w, r, err, http.StatusNotImplemented) return false } @@ -110,7 +111,7 @@ func (i *handler) renderCodec(ctx context.Context, w http.ResponseWriter, r *htt } // Set HTTP headers (for caching, etc). Etag will be replaced if handled by serveCodecHTML. - modtime := addCacheControlHeaders(w, r, rq.contentPath, resolvedPath.Cid(), responseContentType) + modtime := addCacheControlHeaders(w, r, rq.contentPath, resolvedPath.RootCid(), responseContentType) _ = setCodecContentDisposition(w, r, resolvedPath, responseContentType) w.Header().Set("Content-Type", responseContentType) w.Header().Set("X-Content-Type-Options", "nosniff") @@ -155,7 +156,7 @@ func (i *handler) renderCodec(ctx context.Context, w http.ResponseWriter, r *htt return i.serveCodecConverted(ctx, w, r, blockCid, blockData, rq.contentPath, toCodec, modtime, rq.begin) } -func (i *handler) serveCodecHTML(ctx context.Context, w http.ResponseWriter, r *http.Request, blockCid cid.Cid, blockData io.Reader, resolvedPath ipath.Resolved, contentPath ipath.Path) bool { +func (i *handler) serveCodecHTML(ctx context.Context, w http.ResponseWriter, r *http.Request, blockCid cid.Cid, blockData io.Reader, resolvedPath path.ImmutablePath, contentPath path.Path) bool { // WithHostname may have constructed an IPFS (or IPNS) path using the Host header. // In this case, we need the original path for constructing the redirect. requestURI, err := url.ParseRequestURI(r.RequestURI) @@ -185,7 +186,7 @@ func (i *handler) serveCodecHTML(ctx context.Context, w http.ResponseWriter, r * w.Header().Del("Content-Disposition") // Generated index requires custom Etag (output may change between Kubo versions) - dagEtag := getDagIndexEtag(resolvedPath.Cid()) + dagEtag := getDagIndexEtag(resolvedPath.RootCid()) w.Header().Set("Etag", dagEtag) // Remove Cache-Control for now to match UnixFS dir-index-html responses @@ -193,11 +194,11 @@ func (i *handler) serveCodecHTML(ctx context.Context, w http.ResponseWriter, r * // TODO: if we ever change behavior for UnixFS dir listings, same changes should be applied here w.Header().Del("Cache-Control") - cidCodec := mc.Code(resolvedPath.Cid().Prefix().Codec) + cidCodec := mc.Code(resolvedPath.RootCid().Prefix().Codec) if err := assets.DagTemplate.Execute(w, assets.DagTemplateData{ GlobalData: i.getTemplateGlobalData(r, contentPath), Path: contentPath.String(), - CID: resolvedPath.Cid().String(), + CID: resolvedPath.RootCid().String(), CodecName: cidCodec.String(), CodecHex: fmt.Sprintf("0x%x", uint64(cidCodec)), Node: parseNode(blockCid, blockData), @@ -235,7 +236,7 @@ func parseNode(blockCid cid.Cid, blockData io.Reader) *assets.ParsedNode { } // serveCodecRaw returns the raw block without any conversion -func (i *handler) serveCodecRaw(ctx context.Context, w http.ResponseWriter, r *http.Request, blockSize int64, blockData io.ReadSeekCloser, contentPath ipath.Path, modtime, begin time.Time) bool { +func (i *handler) serveCodecRaw(ctx context.Context, w http.ResponseWriter, r *http.Request, blockSize int64, blockData io.ReadSeekCloser, contentPath path.Path, modtime, begin time.Time) bool { // ServeContent will take care of // If-None-Match+Etag, Content-Length and setting range request headers after we've already seeked to the start of // the first range @@ -253,7 +254,7 @@ func (i *handler) serveCodecRaw(ctx context.Context, w http.ResponseWriter, r *h } // serveCodecConverted returns payload converted to codec specified in toCodec -func (i *handler) serveCodecConverted(ctx context.Context, w http.ResponseWriter, r *http.Request, blockCid cid.Cid, blockData io.ReadCloser, contentPath ipath.Path, toCodec mc.Code, modtime, begin time.Time) bool { +func (i *handler) serveCodecConverted(ctx context.Context, w http.ResponseWriter, r *http.Request, blockCid cid.Cid, blockData io.ReadCloser, contentPath path.Path, toCodec mc.Code, modtime, begin time.Time) bool { codec := blockCid.Prefix().Codec decoder, err := multicodec.LookupDecoder(codec) if err != nil { @@ -298,7 +299,7 @@ func (i *handler) serveCodecConverted(ctx context.Context, w http.ResponseWriter return false } -func setCodecContentDisposition(w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentType string) string { +func setCodecContentDisposition(w http.ResponseWriter, r *http.Request, resolvedPath path.ImmutablePath, contentType string) string { var dispType, name string ext, ok := contentTypeToExtension[contentType] @@ -310,7 +311,7 @@ func setCodecContentDisposition(w http.ResponseWriter, r *http.Request, resolved if urlFilename := r.URL.Query().Get("filename"); urlFilename != "" { name = urlFilename } else { - name = resolvedPath.Cid().String() + ext + name = resolvedPath.RootCid().String() + ext } // JSON should be inlined, but ?download=true should still override diff --git a/gateway/handler_codec_test.go b/gateway/handler_codec_test.go index c79b07689..d22579027 100644 --- a/gateway/handler_codec_test.go +++ b/gateway/handler_codec_test.go @@ -7,7 +7,7 @@ import ( "net/http" "testing" - ipath "github.com/ipfs/boxo/coreiface/path" + "github.com/ipfs/boxo/path" "github.com/stretchr/testify/require" ) @@ -31,10 +31,13 @@ func TestDagJsonCborPreview(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - resolvedPath, err := backend.resolvePathNoRootsReturned(ctx, ipath.Join(ipath.IpfsPath(root), "subdir", "dag-cbor-document")) + p, err := path.Join(path.FromCid(root), "subdir", "dag-cbor-document") require.NoError(t, err) - cidStr := resolvedPath.Cid().String() + resolvedPath, err := backend.resolvePathNoRootsReturned(ctx, p) + require.NoError(t, err) + + cidStr := resolvedPath.RootCid().String() t.Run("path gateway normalizes to trailing slash", func(t *testing.T) { t.Parallel() diff --git a/gateway/handler_defaults.go b/gateway/handler_defaults.go index 22c397ade..e56e6c5d9 100644 --- a/gateway/handler_defaults.go +++ b/gateway/handler_defaults.go @@ -95,7 +95,7 @@ func (i *handler) serveDefaults(ctx context.Context, w http.ResponseWriter, r *h setIpfsRootsHeader(w, rq, &pathMetadata) resolvedPath := pathMetadata.LastSegment - switch mc.Code(resolvedPath.Cid().Prefix().Codec) { + switch mc.Code(resolvedPath.RootCid().Prefix().Codec) { case mc.Json, mc.DagJson, mc.Cbor, mc.DagCbor: rq.logger.Debugw("serving codec", "path", rq.contentPath) var blockSize int64 diff --git a/gateway/handler_ipns_record.go b/gateway/handler_ipns_record.go index b077fa59a..93d1621f2 100644 --- a/gateway/handler_ipns_record.go +++ b/gateway/handler_ipns_record.go @@ -11,6 +11,7 @@ import ( "github.com/cespare/xxhash/v2" "github.com/ipfs/boxo/ipns" + "github.com/ipfs/boxo/path" "github.com/ipfs/go-cid" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -20,7 +21,7 @@ func (i *handler) serveIpnsRecord(ctx context.Context, w http.ResponseWriter, r ctx, span := spanTrace(ctx, "Handler.ServeIPNSRecord", trace.WithAttributes(attribute.String("path", rq.contentPath.String()))) defer span.End() - if rq.contentPath.Namespace() != "ipns" { + if rq.contentPath.Namespace() != path.IPNSNamespace { err := fmt.Errorf("%s is not an IPNS link", rq.contentPath.String()) i.webError(w, r, err, http.StatusBadRequest) return false diff --git a/gateway/handler_tar.go b/gateway/handler_tar.go index 784e51993..6af1d0c4d 100644 --- a/gateway/handler_tar.go +++ b/gateway/handler_tar.go @@ -28,7 +28,7 @@ func (i *handler) serveTAR(ctx context.Context, w http.ResponseWriter, r *http.R defer file.Close() setIpfsRootsHeader(w, rq, &pathMetadata) - rootCid := pathMetadata.LastSegment.Cid() + rootCid := pathMetadata.LastSegment.RootCid() // Set Cache-Control and read optional Last-Modified time modtime := addCacheControlHeaders(w, r, rq.contentPath, rootCid, tarResponseFormat) diff --git a/gateway/handler_unixfs__redirects.go b/gateway/handler_unixfs__redirects.go index 978f55647..6b63a793a 100644 --- a/gateway/handler_unixfs__redirects.go +++ b/gateway/handler_unixfs__redirects.go @@ -8,10 +8,9 @@ import ( "strconv" "strings" - "go.uber.org/zap" - - ipath "github.com/ipfs/boxo/coreiface/path" + "github.com/ipfs/boxo/path" redirects "github.com/ipfs/go-ipfs-redirects-file" + "go.uber.org/zap" ) // Resolving a UnixFS path involves determining if the provided `path.Path` exists and returning the `path.Resolved` @@ -36,23 +35,35 @@ import ( // // Note that for security reasons, redirect rules are only processed when the request has origin isolation. // See https://github.com/ipfs/specs/pull/290 for more information. -func (i *handler) serveRedirectsIfPresent(w http.ResponseWriter, r *http.Request, maybeResolvedImPath, immutableContentPath ImmutablePath, contentPath ipath.Path, logger *zap.SugaredLogger) (newContentPath ImmutablePath, continueProcessing bool, hadMatchingRule bool) { +func (i *handler) serveRedirectsIfPresent(w http.ResponseWriter, r *http.Request, maybeResolvedImPath, immutableContentPath path.ImmutablePath, contentPath path.Path, logger *zap.SugaredLogger) (newContentPath path.ImmutablePath, continueProcessing bool, hadMatchingRule bool) { // contentPath is the full ipfs path to the requested resource, // regardless of whether path or subdomain resolution is used. - rootPath := getRootPath(immutableContentPath) - redirectsPath := ipath.Join(rootPath, "_redirects") - imRedirectsPath, err := NewImmutablePath(redirectsPath) + rootPath, err := getRootPath(immutableContentPath) + if err != nil { + err = fmt.Errorf("trouble processing _redirects path %q: %w", immutableContentPath.String(), err) + i.webError(w, r, err, http.StatusInternalServerError) + return nil, false, true + } + + redirectsPath, err := path.Join(rootPath, "_redirects") + if err != nil { + err = fmt.Errorf("trouble processing _redirects path %q: %w", rootPath.String(), err) + i.webError(w, r, err, http.StatusInternalServerError) + return nil, false, true + } + + imRedirectsPath, err := path.NewImmutablePath(redirectsPath) if err != nil { err = fmt.Errorf("trouble processing _redirects path %q: %w", redirectsPath, err) i.webError(w, r, err, http.StatusInternalServerError) - return ImmutablePath{}, false, true + return nil, false, true } foundRedirect, redirectRules, err := i.getRedirectRules(r, imRedirectsPath) if err != nil { err = fmt.Errorf("trouble processing _redirects file at %q: %w", redirectsPath, err) i.webError(w, r, err, http.StatusInternalServerError) - return ImmutablePath{}, false, true + return nil, false, true } if foundRedirect { @@ -60,22 +71,27 @@ func (i *handler) serveRedirectsIfPresent(w http.ResponseWriter, r *http.Request if err != nil { err = fmt.Errorf("trouble processing _redirects file at %q: %w", redirectsPath, err) i.webError(w, r, err, http.StatusInternalServerError) - return ImmutablePath{}, false, true + return nil, false, true } if redirected { - return ImmutablePath{}, false, true + return nil, false, true } // 200 is treated as a rewrite, so update the path and continue if newPath != "" { // Reassign contentPath and resolvedPath since the URL was rewritten - p := ipath.New(newPath) - imPath, err := NewImmutablePath(p) + p, err := path.NewPath(newPath) + if err != nil { + err = fmt.Errorf("could not use _redirects file to %q: %w", p, err) + i.webError(w, r, err, http.StatusInternalServerError) + return nil, false, true + } + imPath, err := path.NewImmutablePath(p) if err != nil { err = fmt.Errorf("could not use _redirects file to %q: %w", p, err) i.webError(w, r, err, http.StatusInternalServerError) - return ImmutablePath{}, false, true + return nil, false, true } return imPath, true, true } @@ -85,7 +101,7 @@ func (i *handler) serveRedirectsIfPresent(w http.ResponseWriter, r *http.Request return maybeResolvedImPath, true, false } -func (i *handler) handleRedirectsFileRules(w http.ResponseWriter, r *http.Request, immutableContentPath ImmutablePath, cPath ipath.Path, redirectRules []redirects.Rule, logger *zap.SugaredLogger) (redirected bool, newContentPath string, err error) { +func (i *handler) handleRedirectsFileRules(w http.ResponseWriter, r *http.Request, immutableContentPath path.ImmutablePath, cPath path.Path, redirectRules []redirects.Rule, logger *zap.SugaredLogger) (redirected bool, newContentPath string, err error) { // Attempt to match a rule to the URL path, and perform the corresponding redirect or rewrite pathParts := strings.Split(immutableContentPath.String(), "/") if len(pathParts) > 3 { @@ -113,7 +129,12 @@ func (i *handler) handleRedirectsFileRules(w http.ResponseWriter, r *http.Reques // Or 4xx if rule.Status == 404 || rule.Status == 410 || rule.Status == 451 { toPath := rootPath + rule.To - imContent4xxPath, err := NewImmutablePath(ipath.New(toPath)) + p, err := path.NewPath(toPath) + if err != nil { + return true, toPath, err + } + + imContent4xxPath, err := path.NewImmutablePath(p) if err != nil { return true, toPath, err } @@ -127,7 +148,11 @@ func (i *handler) handleRedirectsFileRules(w http.ResponseWriter, r *http.Reques } // All paths should start with /ip(f|n)s//, so get the path after that contentRootPath := strings.Join(contentPathParts[:3], "/") - content4xxPath := ipath.New(contentRootPath + rule.To) + content4xxPath, err := path.NewPath(contentRootPath + rule.To) + if err != nil { + return true, toPath, err + } + err = i.serve4xx(w, r, imContent4xxPath, content4xxPath, rule.Status, logger) return true, toPath, err } @@ -147,7 +172,7 @@ func (i *handler) handleRedirectsFileRules(w http.ResponseWriter, r *http.Reques // getRedirectRules fetches the _redirects file corresponding to a given path and returns the rules // Returns whether _redirects was found, the rules (if they exist) and if there was an error (other than a missing _redirects) // If there is an error returns (false, nil, err) -func (i *handler) getRedirectRules(r *http.Request, redirectsPath ImmutablePath) (bool, []redirects.Rule, error) { +func (i *handler) getRedirectRules(r *http.Request, redirectsPath path.ImmutablePath) (bool, []redirects.Rule, error) { // Check for _redirects file. // Any path resolution failures are ignored and we just assume there's no _redirects file. // Note that ignoring these errors also ensures that the use of the empty CID (bafkqaaa) in tests doesn't fail. @@ -174,12 +199,12 @@ func (i *handler) getRedirectRules(r *http.Request, redirectsPath ImmutablePath) } // Returns the root CID Path for the given path -func getRootPath(path ipath.Path) ipath.Path { - parts := strings.Split(path.String(), "/") - return ipath.New(gopath.Join("/", path.Namespace(), parts[2])) +func getRootPath(p path.Path) (path.Path, error) { + parts := strings.Split(p.String(), "/") + return path.NewPath(gopath.Join("/", p.Namespace(), parts[2])) } -func (i *handler) serve4xx(w http.ResponseWriter, r *http.Request, content4xxPathImPath ImmutablePath, content4xxPath ipath.Path, status int, logger *zap.SugaredLogger) error { +func (i *handler) serve4xx(w http.ResponseWriter, r *http.Request, content4xxPathImPath path.ImmutablePath, content4xxPath path.Path, status int, logger *zap.SugaredLogger) error { pathMetadata, getresp, err := i.backend.Get(r.Context(), content4xxPathImPath) if err != nil { return err @@ -191,7 +216,7 @@ func (i *handler) serve4xx(w http.ResponseWriter, r *http.Request, content4xxPat } content4xxFile := getresp.bytes - content4xxCid := pathMetadata.LastSegment.Cid() + content4xxCid := pathMetadata.LastSegment.RootCid() size := getresp.bytesSize @@ -218,7 +243,7 @@ func hasOriginIsolation(r *http.Request) bool { // Deprecated: legacy ipfs-404.html files are superseded by _redirects file // This is provided only for backward-compatibility, until websites migrate // to 404s managed via _redirects file (https://github.com/ipfs/specs/pull/290) -func (i *handler) serveLegacy404IfPresent(w http.ResponseWriter, r *http.Request, imPath ImmutablePath, logger *zap.SugaredLogger) bool { +func (i *handler) serveLegacy404IfPresent(w http.ResponseWriter, r *http.Request, imPath path.ImmutablePath, logger *zap.SugaredLogger) bool { resolved404File, resolved404FileSize, ctype, err := i.searchUpTreeFor404(r, imPath) if err != nil { return false @@ -233,7 +258,7 @@ func (i *handler) serveLegacy404IfPresent(w http.ResponseWriter, r *http.Request return err == nil } -func (i *handler) searchUpTreeFor404(r *http.Request, imPath ImmutablePath) (io.ReadCloser, int64, string, error) { +func (i *handler) searchUpTreeFor404(r *http.Request, imPath path.ImmutablePath) (io.ReadCloser, int64, string, error) { filename404, ctype, err := preferred404Filename(r.Header.Values("Accept")) if err != nil { return nil, 0, "", err @@ -243,11 +268,11 @@ func (i *handler) searchUpTreeFor404(r *http.Request, imPath ImmutablePath) (io. for idx := len(pathComponents); idx >= 3; idx-- { pretty404 := gopath.Join(append(pathComponents[0:idx], filename404)...) - parsed404Path := ipath.New("/" + pretty404) - if parsed404Path.IsValid() != nil { + parsed404Path, err := path.NewPath("/" + pretty404) + if err != nil { break } - imparsed404Path, err := NewImmutablePath(parsed404Path) + imparsed404Path, err := path.NewImmutablePath(parsed404Path) if err != nil { break } diff --git a/gateway/handler_unixfs_dir.go b/gateway/handler_unixfs_dir.go index 1ece9c96d..479f4e3c9 100644 --- a/gateway/handler_unixfs_dir.go +++ b/gateway/handler_unixfs_dir.go @@ -11,10 +11,9 @@ import ( "time" "github.com/dustin/go-humanize" - ipath "github.com/ipfs/boxo/coreiface/path" "github.com/ipfs/boxo/files" "github.com/ipfs/boxo/gateway/assets" - path "github.com/ipfs/boxo/path" + "github.com/ipfs/boxo/path" cid "github.com/ipfs/go-cid" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -24,7 +23,7 @@ import ( // serveDirectory returns the best representation of UnixFS directory // // It will return index.html if present, or generate directory listing otherwise. -func (i *handler) serveDirectory(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, isHeadRequest bool, directoryMetadata *directoryMetadata, ranges []ByteRange, begin time.Time, logger *zap.SugaredLogger) bool { +func (i *handler) serveDirectory(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath path.ImmutablePath, contentPath path.Path, isHeadRequest bool, directoryMetadata *directoryMetadata, ranges []ByteRange, begin time.Time, logger *zap.SugaredLogger) bool { ctx, span := spanTrace(ctx, "Handler.ServeDirectory", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) defer span.End() @@ -60,16 +59,19 @@ func (i *handler) serveDirectory(ctx context.Context, w http.ResponseWriter, r * } // Check if directory has index.html, if so, serveFile - appendIndexHtml := func(p ipath.Path) ipath.Path { - basePath := p.String() - if basePath[len(basePath)-1] != '/' { - basePath += "/" - } - return ipath.New(basePath + "index.html") + idxPath, err := path.Join(contentPath, "index.html") + if err != nil { + i.webError(w, r, err, http.StatusInternalServerError) + return false + } + + indexPath, err := path.Join(resolvedPath, "index.html") + if err != nil { + i.webError(w, r, err, http.StatusInternalServerError) + return false } - idxPath := appendIndexHtml(contentPath) - imIndexPath, err := NewImmutablePath(appendIndexHtml(resolvedPath)) + imIndexPath, err := path.NewImmutablePath(indexPath) if err != nil { i.webError(w, r, err, http.StatusInternalServerError) return false @@ -132,7 +134,7 @@ func (i *handler) serveDirectory(ctx context.Context, w http.ResponseWriter, r * w.Header().Set("Content-Type", "text/html") // Generated dir index requires custom Etag (output may change between go-libipfs versions) - dirEtag := getDirListingEtag(resolvedPath.Cid()) + dirEtag := getDirListingEtag(resolvedPath.RootCid()) w.Header().Set("Etag", dirEtag) if r.Method == http.MethodHead { @@ -167,7 +169,7 @@ func (i *handler) serveDirectory(ctx context.Context, w http.ResponseWriter, r * backLink := originalURLPath // don't go further up than /ipfs/$hash/ - pathSplit := path.SplitList(contentPath.String()) + pathSplit := strings.Split(contentPath.String(), "/") switch { // skip backlink when listing a content root case len(pathSplit) == 3: // url: /ipfs/$hash @@ -187,7 +189,7 @@ func (i *handler) serveDirectory(ctx context.Context, w http.ResponseWriter, r * } size := humanize.Bytes(directoryMetadata.dagSize) - hash := resolvedPath.Cid().String() + hash := resolvedPath.RootCid().String() globalData := i.getTemplateGlobalData(r, contentPath) // See comment above where originalUrlPath is declared. diff --git a/gateway/handler_unixfs_dir_test.go b/gateway/handler_unixfs_dir_test.go index a8ce04778..48f2625d6 100644 --- a/gateway/handler_unixfs_dir_test.go +++ b/gateway/handler_unixfs_dir_test.go @@ -6,8 +6,7 @@ import ( "net/http" "testing" - ipath "github.com/ipfs/boxo/coreiface/path" - path "github.com/ipfs/boxo/path" + "github.com/ipfs/boxo/path" "github.com/stretchr/testify/require" ) @@ -19,10 +18,14 @@ func TestIPNSHostnameBacklinks(t *testing.T) { defer cancel() // create /ipns/example.net/foo/ - k2, err := backend.resolvePathNoRootsReturned(ctx, ipath.Join(ipath.IpfsPath(root), "foo? #<'")) + p2, err := path.Join(path.FromCid(root), "foo? #<'") + require.NoError(t, err) + k2, err := backend.resolvePathNoRootsReturned(ctx, p2) require.NoError(t, err) - k3, err := backend.resolvePathNoRootsReturned(ctx, ipath.Join(ipath.IpfsPath(root), "foo? #<'/bar")) + p3, err := path.Join(path.FromCid(root), "foo? #<'/bar") + require.NoError(t, err) + k3, err := backend.resolvePathNoRootsReturned(ctx, p3) require.NoError(t, err) backend.namesys["/ipns/example.net"] = path.FromCid(root) @@ -44,7 +47,7 @@ func TestIPNSHostnameBacklinks(t *testing.T) { require.Contains(t, s, "", "expected backlink in directory listing") require.Contains(t, s, "", "expected file in directory listing") - require.Contains(t, s, s, k2.Cid().String(), "expected hash in directory listing") + require.Contains(t, s, s, k2.RootCid().String(), "expected hash in directory listing") // make request to directory listing at root req = mustNewRequest(t, http.MethodGet, ts.URL, nil) @@ -83,5 +86,5 @@ func TestIPNSHostnameBacklinks(t *testing.T) { require.True(t, matchPathOrBreadcrumbs(s, "/ipns/example.net/foo? #<'/bar"), "expected a path in directory listing") require.Contains(t, s, "", "expected backlink in directory listing") require.Contains(t, s, "", "expected file in directory listing") - require.Contains(t, s, k3.Cid().String(), "expected hash in directory listing") + require.Contains(t, s, k3.RootCid().String(), "expected hash in directory listing") } diff --git a/gateway/handler_unixfs_file.go b/gateway/handler_unixfs_file.go index 8375bfcd2..fb1f9940c 100644 --- a/gateway/handler_unixfs_file.go +++ b/gateway/handler_unixfs_file.go @@ -12,19 +12,19 @@ import ( "time" "github.com/gabriel-vasile/mimetype" - ipath "github.com/ipfs/boxo/coreiface/path" + "github.com/ipfs/boxo/path" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) // serveFile returns data behind a file along with HTTP headers based on // the file itself, its CID and the contentPath used for accessing it. -func (i *handler) serveFile(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, fileSize int64, fileBytes io.ReadCloser, isSymlink bool, returnRangeStartsAtZero bool, fileContentType string, begin time.Time) bool { +func (i *handler) serveFile(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath path.ImmutablePath, contentPath path.Path, fileSize int64, fileBytes io.ReadCloser, isSymlink bool, returnRangeStartsAtZero bool, fileContentType string, begin time.Time) bool { _, span := spanTrace(ctx, "Handler.ServeFile", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) defer span.End() // Set Cache-Control and read optional Last-Modified time - modtime := addCacheControlHeaders(w, r, contentPath, resolvedPath.Cid(), "") + modtime := addCacheControlHeaders(w, r, contentPath, resolvedPath.RootCid(), "") // Set Content-Disposition name := addContentDispositionHeader(w, r, contentPath) diff --git a/gateway/hostname_test.go b/gateway/hostname_test.go index f7cee35a2..3a6809555 100644 --- a/gateway/hostname_test.go +++ b/gateway/hostname_test.go @@ -7,7 +7,7 @@ import ( "net/http/httptest" "testing" - path "github.com/ipfs/boxo/path" + "github.com/ipfs/boxo/path" cid "github.com/ipfs/go-cid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -20,8 +20,8 @@ func TestToSubdomainURL(t *testing.T) { testCID, err := cid.Decode("bafkqaglimvwgy3zakrsxg5cun5jxkyten5wwc2lokvjeycq") require.NoError(t, err) - backend.namesys["/ipns/dnslink.long-name.example.com"] = path.FromString(testCID.String()) - backend.namesys["/ipns/dnslink.too-long.f1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5o.example.com"] = path.FromString(testCID.String()) + backend.namesys["/ipns/dnslink.long-name.example.com"] = path.FromCid(testCID) + backend.namesys["/ipns/dnslink.too-long.f1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5o.example.com"] = path.FromCid(testCID) httpRequest := httptest.NewRequest("GET", "http://127.0.0.1:8080", nil) httpsRequest := httptest.NewRequest("GET", "https://https-request-stub.example.com", nil) httpsProxiedRequest := httptest.NewRequest("GET", "http://proxied-https-request-stub.example.com", nil) diff --git a/gateway/metrics.go b/gateway/metrics.go index 6035c74b5..bccaefef2 100644 --- a/gateway/metrics.go +++ b/gateway/metrics.go @@ -6,8 +6,8 @@ import ( "io" "time" - "github.com/ipfs/boxo/coreiface/path" "github.com/ipfs/boxo/files" + "github.com/ipfs/boxo/path" "github.com/ipfs/go-cid" prometheus "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel" @@ -60,7 +60,7 @@ func (b *ipfsBackendWithMetrics) updateBackendCallMetric(name string, err error, } } -func (b *ipfsBackendWithMetrics) Get(ctx context.Context, path ImmutablePath, ranges ...ByteRange) (ContentPathMetadata, *GetResponse, error) { +func (b *ipfsBackendWithMetrics) Get(ctx context.Context, path path.ImmutablePath, ranges ...ByteRange) (ContentPathMetadata, *GetResponse, error) { begin := time.Now() name := "IPFSBackend.Get" ctx, span := spanTrace(ctx, name, trace.WithAttributes(attribute.String("path", path.String()), attribute.Int("ranges", len(ranges)))) @@ -72,7 +72,7 @@ func (b *ipfsBackendWithMetrics) Get(ctx context.Context, path ImmutablePath, ra return md, f, err } -func (b *ipfsBackendWithMetrics) GetAll(ctx context.Context, path ImmutablePath) (ContentPathMetadata, files.Node, error) { +func (b *ipfsBackendWithMetrics) GetAll(ctx context.Context, path path.ImmutablePath) (ContentPathMetadata, files.Node, error) { begin := time.Now() name := "IPFSBackend.GetAll" ctx, span := spanTrace(ctx, name, trace.WithAttributes(attribute.String("path", path.String()))) @@ -84,7 +84,7 @@ func (b *ipfsBackendWithMetrics) GetAll(ctx context.Context, path ImmutablePath) return md, n, err } -func (b *ipfsBackendWithMetrics) GetBlock(ctx context.Context, path ImmutablePath) (ContentPathMetadata, files.File, error) { +func (b *ipfsBackendWithMetrics) GetBlock(ctx context.Context, path path.ImmutablePath) (ContentPathMetadata, files.File, error) { begin := time.Now() name := "IPFSBackend.GetBlock" ctx, span := spanTrace(ctx, name, trace.WithAttributes(attribute.String("path", path.String()))) @@ -96,7 +96,7 @@ func (b *ipfsBackendWithMetrics) GetBlock(ctx context.Context, path ImmutablePat return md, n, err } -func (b *ipfsBackendWithMetrics) Head(ctx context.Context, path ImmutablePath) (ContentPathMetadata, *HeadResponse, error) { +func (b *ipfsBackendWithMetrics) Head(ctx context.Context, path path.ImmutablePath) (ContentPathMetadata, *HeadResponse, error) { begin := time.Now() name := "IPFSBackend.Head" ctx, span := spanTrace(ctx, name, trace.WithAttributes(attribute.String("path", path.String()))) @@ -108,7 +108,7 @@ func (b *ipfsBackendWithMetrics) Head(ctx context.Context, path ImmutablePath) ( return md, n, err } -func (b *ipfsBackendWithMetrics) ResolvePath(ctx context.Context, path ImmutablePath) (ContentPathMetadata, error) { +func (b *ipfsBackendWithMetrics) ResolvePath(ctx context.Context, path path.ImmutablePath) (ContentPathMetadata, error) { begin := time.Now() name := "IPFSBackend.ResolvePath" ctx, span := spanTrace(ctx, name, trace.WithAttributes(attribute.String("path", path.String()))) @@ -120,7 +120,7 @@ func (b *ipfsBackendWithMetrics) ResolvePath(ctx context.Context, path Immutable return md, err } -func (b *ipfsBackendWithMetrics) GetCAR(ctx context.Context, path ImmutablePath, params CarParams) (ContentPathMetadata, io.ReadCloser, error) { +func (b *ipfsBackendWithMetrics) GetCAR(ctx context.Context, path path.ImmutablePath, params CarParams) (ContentPathMetadata, io.ReadCloser, error) { begin := time.Now() name := "IPFSBackend.GetCAR" ctx, span := spanTrace(ctx, name, trace.WithAttributes(attribute.String("path", path.String()))) @@ -155,7 +155,7 @@ func (b *ipfsBackendWithMetrics) GetIPNSRecord(ctx context.Context, cid cid.Cid) return r, err } -func (b *ipfsBackendWithMetrics) ResolveMutable(ctx context.Context, path path.Path) (ImmutablePath, error) { +func (b *ipfsBackendWithMetrics) ResolveMutable(ctx context.Context, path path.Path) (path.ImmutablePath, error) { begin := time.Now() name := "IPFSBackend.ResolveMutable" ctx, span := spanTrace(ctx, name, trace.WithAttributes(attribute.String("path", path.String()))) diff --git a/gateway/utilities_test.go b/gateway/utilities_test.go index 9d58d8c11..89f955cbc 100644 --- a/gateway/utilities_test.go +++ b/gateway/utilities_test.go @@ -14,11 +14,10 @@ import ( "github.com/ipfs/boxo/blockservice" nsopts "github.com/ipfs/boxo/coreiface/options/namesys" - ipath "github.com/ipfs/boxo/coreiface/path" offline "github.com/ipfs/boxo/exchange/offline" "github.com/ipfs/boxo/files" "github.com/ipfs/boxo/namesys" - path "github.com/ipfs/boxo/path" + "github.com/ipfs/boxo/path" "github.com/ipfs/go-cid" carblockstore "github.com/ipld/go-car/v2/blockstore" "github.com/libp2p/go-libp2p/core/crypto" @@ -73,7 +72,7 @@ func (m mockNamesys) Resolve(ctx context.Context, name string, opts ...nsopts.Re var ok bool value, ok = m[name] if !ok { - return "", namesys.ErrResolveFailed + return nil, namesys.ErrResolveFailed } name = value.String() } @@ -133,27 +132,27 @@ func newMockBackend(t *testing.T, fixturesFile string) (*mockBackend, cid.Cid) { }, cids[0] } -func (mb *mockBackend) Get(ctx context.Context, immutablePath ImmutablePath, ranges ...ByteRange) (ContentPathMetadata, *GetResponse, error) { +func (mb *mockBackend) Get(ctx context.Context, immutablePath path.ImmutablePath, ranges ...ByteRange) (ContentPathMetadata, *GetResponse, error) { return mb.gw.Get(ctx, immutablePath, ranges...) } -func (mb *mockBackend) GetAll(ctx context.Context, immutablePath ImmutablePath) (ContentPathMetadata, files.Node, error) { +func (mb *mockBackend) GetAll(ctx context.Context, immutablePath path.ImmutablePath) (ContentPathMetadata, files.Node, error) { return mb.gw.GetAll(ctx, immutablePath) } -func (mb *mockBackend) GetBlock(ctx context.Context, immutablePath ImmutablePath) (ContentPathMetadata, files.File, error) { +func (mb *mockBackend) GetBlock(ctx context.Context, immutablePath path.ImmutablePath) (ContentPathMetadata, files.File, error) { return mb.gw.GetBlock(ctx, immutablePath) } -func (mb *mockBackend) Head(ctx context.Context, immutablePath ImmutablePath) (ContentPathMetadata, *HeadResponse, error) { +func (mb *mockBackend) Head(ctx context.Context, immutablePath path.ImmutablePath) (ContentPathMetadata, *HeadResponse, error) { return mb.gw.Head(ctx, immutablePath) } -func (mb *mockBackend) GetCAR(ctx context.Context, immutablePath ImmutablePath, params CarParams) (ContentPathMetadata, io.ReadCloser, error) { +func (mb *mockBackend) GetCAR(ctx context.Context, immutablePath path.ImmutablePath, params CarParams) (ContentPathMetadata, io.ReadCloser, error) { return mb.gw.GetCAR(ctx, immutablePath, params) } -func (mb *mockBackend) ResolveMutable(ctx context.Context, p ipath.Path) (ImmutablePath, error) { +func (mb *mockBackend) ResolveMutable(ctx context.Context, p path.Path) (path.ImmutablePath, error) { return mb.gw.ResolveMutable(ctx, p) } @@ -161,28 +160,28 @@ func (mb *mockBackend) GetIPNSRecord(ctx context.Context, c cid.Cid) ([]byte, er return nil, routing.ErrNotSupported } -func (mb *mockBackend) GetDNSLinkRecord(ctx context.Context, hostname string) (ipath.Path, error) { +func (mb *mockBackend) GetDNSLinkRecord(ctx context.Context, hostname string) (path.Path, error) { if mb.namesys != nil { p, err := mb.namesys.Resolve(ctx, "/ipns/"+hostname, nsopts.Depth(1)) if err == namesys.ErrResolveRecursion { err = nil } - return ipath.New(p.String()), err + return p, err } return nil, errors.New("not implemented") } -func (mb *mockBackend) IsCached(ctx context.Context, p ipath.Path) bool { +func (mb *mockBackend) IsCached(ctx context.Context, p path.Path) bool { return mb.gw.IsCached(ctx, p) } -func (mb *mockBackend) ResolvePath(ctx context.Context, immutablePath ImmutablePath) (ContentPathMetadata, error) { +func (mb *mockBackend) ResolvePath(ctx context.Context, immutablePath path.ImmutablePath) (ContentPathMetadata, error) { return mb.gw.ResolvePath(ctx, immutablePath) } -func (mb *mockBackend) resolvePathNoRootsReturned(ctx context.Context, ip ipath.Path) (ipath.Resolved, error) { - var imPath ImmutablePath +func (mb *mockBackend) resolvePathNoRootsReturned(ctx context.Context, ip path.Path) (path.ImmutablePath, error) { + var imPath path.ImmutablePath var err error if ip.Mutable() { imPath, err = mb.ResolveMutable(ctx, ip) @@ -190,7 +189,7 @@ func (mb *mockBackend) resolvePathNoRootsReturned(ctx context.Context, ip ipath. return nil, err } } else { - imPath, err = NewImmutablePath(ip) + imPath, err = path.NewImmutablePath(ip) if err != nil { return nil, err } diff --git a/ipns/README.md b/ipns/README.md index 502f6dded..28989e0c9 100644 --- a/ipns/README.md +++ b/ipns/README.md @@ -30,7 +30,10 @@ func main() { } // Define the path this record will point to. - path := path.FromString("/ipfs/bafkqac3jobxhgidsn5rww4yk") + path, err := path.NewPath("/ipfs/bafkqac3jobxhgidsn5rww4yk") + if err != nil { + panic(err) + } // Until when the record is valid. eol := time.Now().Add(time.Hour) diff --git a/ipns/name.go b/ipns/name.go index 2a6bbdbf4..b35e8f9d8 100644 --- a/ipns/name.go +++ b/ipns/name.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "github.com/ipfs/boxo/path" "github.com/ipfs/go-cid" "github.com/libp2p/go-libp2p/core/peer" mb "github.com/multiformats/go-multibase" @@ -133,3 +134,12 @@ func (n Name) MarshalJSON() ([]byte, error) { func (n Name) Equal(other Name) bool { return bytes.Equal(n.src, other.src) } + +// AsPath returns the IPNS Name as a [path.Path] prefixed by [path.IPNSNamespace]. +func (n Name) AsPath() path.Path { + p, err := path.NewPathFromSegments(path.IPNSNamespace, n.String()) + if err != nil { + panic(fmt.Errorf("path.NewPathFromSegments was called with invalid parameters: %w", err)) + } + return p +} diff --git a/ipns/name_test.go b/ipns/name_test.go index 4b8ccd414..c3c31878c 100644 --- a/ipns/name_test.go +++ b/ipns/name_test.go @@ -34,6 +34,16 @@ func TestName(t *testing.T) { }) } + testPath := func(t *testing.T, name, input, expected string) { + t.Run("AsPath method: "+name, func(t *testing.T) { + t.Parallel() + + name, err := NameFromString(input) + require.NoError(t, err) + require.Equal(t, expected, name.AsPath().String()) + }) + } + testMarshalJSON := func(t *testing.T, name, input, expected string) { t.Run("Marshal JSON: "+name, func(t *testing.T) { t.Parallel() @@ -66,6 +76,7 @@ func TestName(t *testing.T) { testFromCid(t, v[0], v[2], v[2]) testString(t, v[0], v[1], v[2]) testString(t, v[0], NamespacePrefix+v[1], v[2]) + testPath(t, v[0], v[1], NamespacePrefix+v[2]) testMarshalJSON(t, v[0], v[1], `"`+v[2]+`"`) testMarshalJSON(t, v[0], NamespacePrefix+v[1], `"`+v[2]+`"`) testUnmarshalJSON(t, v[0], []byte(`"`+v[2]+`"`), v[2]) diff --git a/ipns/record.go b/ipns/record.go index cdb901af0..ddb57ac42 100644 --- a/ipns/record.go +++ b/ipns/record.go @@ -85,12 +85,12 @@ func MarshalRecord(rec *Record) ([]byte, error) { func (rec *Record) Value() (path.Path, error) { value, err := rec.getBytesValue(cborValueKey) if err != nil { - return "", err + return nil, err } - p := path.FromString(string(value)) - if err := p.IsValid(); err != nil { - return "", multierr.Combine(ErrInvalidPath, err) + p, err := path.NewPath(string(value)) + if err != nil { + return nil, multierr.Combine(ErrInvalidPath, err) } return p, nil @@ -259,7 +259,7 @@ func NewRecord(sk ic.PrivKey, value path.Path, seq uint64, eol time.Time, ttl ti } if options.v1Compatibility { - pb.Value = []byte(value) + pb.Value = []byte(value.String()) typ := ipns_pb.IpnsRecord_EOL pb.ValidityType = &typ pb.Sequence = &seq @@ -306,7 +306,7 @@ func createNode(value path.Path, seq uint64, eol time.Time, ttl time.Duration) ( m := make(map[string]ipld.Node) var keys []string - m[cborValueKey] = basicnode.NewBytes([]byte(value)) + m[cborValueKey] = basicnode.NewBytes([]byte(value.String())) keys = append(keys, cborValueKey) m[cborValidityKey] = basicnode.NewBytes([]byte(util.FormatRFC3339(eol))) diff --git a/ipns/record_test.go b/ipns/record_test.go index d761ecfc5..db92a6a17 100644 --- a/ipns/record_test.go +++ b/ipns/record_test.go @@ -17,10 +17,18 @@ import ( "google.golang.org/protobuf/proto" ) -const ( - testPath = path.Path("/ipfs/bafkqac3jobxhgidsn5rww4yk") +var ( + testPath path.Path ) +func init() { + var err error + testPath, err = path.NewPath("/ipfs/bafkqac3jobxhgidsn5rww4yk") + if err != nil { + panic(err) + } +} + func mustKeyPair(t *testing.T, typ int) (ic.PrivKey, ic.PubKey, Name) { sr := util.NewTimeSeededRand() sk, pk, err := ic.GenerateKeyPairWithReader(typ, 2048, sr) @@ -195,14 +203,15 @@ func TestCBORDataSerialization(t *testing.T) { sk, _, _ := mustKeyPair(t, ic.Ed25519) eol := time.Now().Add(time.Hour) - path := path.FromString(string(append([]byte("/path/1"), 0x00))) + path, err := path.Join(testPath, string([]byte{0x00})) + require.NoError(t, err) seq := uint64(1) ttl := time.Hour rec := mustNewRecord(t, sk, path, seq, eol, ttl) builder := basicnode.Prototype__Map{}.NewBuilder() - err := dagcbor.Decode(builder, bytes.NewReader(rec.pb.GetData())) + err = dagcbor.Decode(builder, bytes.NewReader(rec.pb.GetData())) require.NoError(t, err) node := builder.Build() @@ -218,7 +227,7 @@ func TestCBORDataSerialization(t *testing.T) { case cborValueKey: b, err := v.AsBytes() require.NoError(t, err) - require.Equal(t, b, []byte(path)) + require.Equal(t, b, []byte(path.String())) case cborSequenceKey: s, err := v.AsInt() require.NoError(t, err) diff --git a/ipns/validation_test.go b/ipns/validation_test.go index 126c357e7..8195fde23 100644 --- a/ipns/validation_test.go +++ b/ipns/validation_test.go @@ -164,8 +164,14 @@ func TestValidate(t *testing.T) { v := Validator{} - rec1 := mustNewRecord(t, sk, path.FromString("/path/1"), 1, eol, 0, WithV1Compatibility(true)) - rec2 := mustNewRecord(t, sk, path.FromString("/path/2"), 2, eol, 0, WithV1Compatibility(true)) + path1, err := path.Join(testPath, "1") + require.NoError(t, err) + + path2, err := path.Join(testPath, "2") + require.NoError(t, err) + + rec1 := mustNewRecord(t, sk, path1, 1, eol, 0, WithV1Compatibility(true)) + rec2 := mustNewRecord(t, sk, path2, 2, eol, 0, WithV1Compatibility(true)) best, err := v.Select(ipnsRoutingKey, [][]byte{mustMarshal(t, rec1), mustMarshal(t, rec2)}) require.NoError(t, err) @@ -210,8 +216,10 @@ func TestValidate(t *testing.T) { sk, pk, _ := mustKeyPair(t, ic.RSA) // Create a record that is too large (value + other fields). - value := make([]byte, MaxRecordSize) - rec, err := NewRecord(sk, path.FromString(string(value)), 1, eol, 0) + path, err := path.Join(testPath, string(make([]byte, MaxRecordSize))) + require.NoError(t, err) + + rec, err := NewRecord(sk, path, 1, eol, 0) require.NoError(t, err) err = Validate(rec, pk) diff --git a/mfs/mfs_test.go b/mfs/mfs_test.go index 9ecdbffd5..2bdf82994 100644 --- a/mfs/mfs_test.go +++ b/mfs/mfs_test.go @@ -12,6 +12,7 @@ import ( "os" gopath "path" "sort" + "strings" "sync" "testing" "time" @@ -24,7 +25,6 @@ import ( ft "github.com/ipfs/boxo/ipld/unixfs" importer "github.com/ipfs/boxo/ipld/unixfs/importer" uio "github.com/ipfs/boxo/ipld/unixfs/io" - path "github.com/ipfs/boxo/path" u "github.com/ipfs/boxo/util" cid "github.com/ipfs/go-cid" @@ -59,7 +59,7 @@ func fileNodeFromReader(t *testing.T, ds ipld.DAGService, r io.Reader) ipld.Node } func mkdirP(t *testing.T, root *Directory, pth string) *Directory { - dirs := path.SplitList(pth) + dirs := strings.Split(pth, "/") cur := root for _, d := range dirs { n, err := cur.Mkdir(d) @@ -145,7 +145,7 @@ func assertFileAtPath(ds ipld.DAGService, root *Directory, expn ipld.Node, pth s return dag.ErrNotProtobuf } - parts := path.SplitList(pth) + parts := strings.Split(pth, "/") cur := root for i, d := range parts[:len(parts)-1] { next, err := cur.Child(d) diff --git a/mfs/ops.go b/mfs/ops.go index 78156dd52..b3d9f2a5e 100644 --- a/mfs/ops.go +++ b/mfs/ops.go @@ -7,8 +7,6 @@ import ( gopath "path" "strings" - path "github.com/ipfs/boxo/path" - cid "github.com/ipfs/go-cid" ipld "github.com/ipfs/go-ipld-format" ) @@ -131,7 +129,7 @@ func Mkdir(r *Root, pth string, opts MkdirOpts) error { if pth == "" { return fmt.Errorf("no path given to Mkdir") } - parts := path.SplitList(pth) + parts := strings.Split(pth, "/") if parts[0] == "" { parts = parts[1:] } @@ -167,7 +165,7 @@ func Mkdir(r *Root, pth string, opts MkdirOpts) error { next, ok := fsn.(*Directory) if !ok { - return fmt.Errorf("%s was not a directory", path.Join(parts[:i])) + return fmt.Errorf("%s was not a directory", strings.Join(parts[:i], "/")) } cur = next } @@ -205,7 +203,7 @@ func Lookup(r *Root, path string) (FSNode, error) { // under the directory 'd' func DirLookup(d *Directory, pth string) (FSNode, error) { pth = strings.Trim(pth, "/") - parts := path.SplitList(pth) + parts := strings.Split(pth, "/") if len(parts) == 1 && parts[0] == "" { return d, nil } @@ -215,7 +213,7 @@ func DirLookup(d *Directory, pth string) (FSNode, error) { for i, p := range parts { chdir, ok := cur.(*Directory) if !ok { - return nil, fmt.Errorf("cannot access %s: Not a directory", path.Join(parts[:i+1])) + return nil, fmt.Errorf("cannot access %s: Not a directory", strings.Join(parts[:i+1], "/")) } child, err := chdir.Child(p) diff --git a/namesys/base.go b/namesys/base.go index 06b24bedc..6b8e41ab5 100644 --- a/namesys/base.go +++ b/namesys/base.go @@ -6,7 +6,7 @@ import ( "time" opts "github.com/ipfs/boxo/coreiface/options/namesys" - path "github.com/ipfs/boxo/path" + "github.com/ipfs/boxo/path" ) type onceResult struct { diff --git a/namesys/cache.go b/namesys/cache.go index 8b7f50794..51fe3149b 100644 --- a/namesys/cache.go +++ b/namesys/cache.go @@ -3,7 +3,7 @@ package namesys import ( "time" - path "github.com/ipfs/boxo/path" + "github.com/ipfs/boxo/path" ) func (ns *mpns) cacheGet(name string) (path.Path, bool) { @@ -16,12 +16,12 @@ func (ns *mpns) cacheGet(name string) (path.Path, bool) { } if ns.cache == nil { - return "", false + return nil, false } ientry, ok := ns.cache.Get(name) if !ok { - return "", false + return nil, false } entry, ok := ientry.(cacheEntry) @@ -36,7 +36,7 @@ func (ns *mpns) cacheGet(name string) (path.Path, bool) { ns.cache.Remove(name) - return "", false + return nil, false } func (ns *mpns) cacheSet(name string, val path.Path, ttl time.Duration) { diff --git a/namesys/dns.go b/namesys/dns.go index 6f846fcda..48becfc1c 100644 --- a/namesys/dns.go +++ b/namesys/dns.go @@ -5,11 +5,12 @@ import ( "errors" "fmt" "net" - gpath "path" + gopath "path" "strings" opts "github.com/ipfs/boxo/coreiface/options/namesys" - path "github.com/ipfs/boxo/path" + "github.com/ipfs/boxo/path" + "github.com/ipfs/go-cid" dns "github.com/miekg/dns" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -84,7 +85,7 @@ func (r *DNSResolver) resolveOnceAsync(ctx context.Context, name string, options appendPath := func(p path.Path) (path.Path, error) { if len(segments) > 1 { - return path.FromSegments("", strings.TrimRight(p.String(), "/"), segments[1]) + return path.Join(p, segments[1]) } return p, nil } @@ -133,7 +134,7 @@ func (r *DNSResolver) resolveOnceAsync(ctx context.Context, name string, options // dnslink, then output a more specific error message if rootResErr == ErrResolveFailed && subResErr == ErrResolveFailed { // Wrap error so that it can be tested if it is a ErrResolveFailed - err := fmt.Errorf("%w: _dnslink subdomain at %q is missing a TXT record (https://docs.ipfs.tech/concepts/dnslink/)", ErrResolveFailed, gpath.Base(name)) + err := fmt.Errorf("%w: _dnslink subdomain at %q is missing a TXT record (https://docs.ipfs.tech/concepts/dnslink/)", ErrResolveFailed, gopath.Base(name)) emitOnceResult(ctx, out, onceResult{err: err}) } return @@ -160,7 +161,7 @@ func workDomain(ctx context.Context, r *DNSResolver, name string, res chan looku } } // Could not look up any text records for name - res <- lookupRes{"", err} + res <- lookupRes{nil, err} return } @@ -173,23 +174,36 @@ func workDomain(ctx context.Context, r *DNSResolver, name string, res chan looku } // There were no TXT records with a dnslink - res <- lookupRes{"", ErrResolveFailed} + res <- lookupRes{nil, ErrResolveFailed} } func parseEntry(txt string) (path.Path, error) { - p, err := path.ParseCidToPath(txt) // bare IPFS multihashes + p, err := path.NewPath(txt) // bare IPFS multihashes if err == nil { return p, nil } + // Support legacy DNSLink entries composed by the CID only. + if cid, err := cid.Decode(txt); err == nil { + return path.FromCid(cid), nil + } + return tryParseDNSLink(txt) } func tryParseDNSLink(txt string) (path.Path, error) { parts := strings.SplitN(txt, "=", 2) if len(parts) == 2 && parts[0] == "dnslink" { - return path.ParsePath(parts[1]) + p, err := path.NewPath(parts[1]) + if err == nil { + return p, nil + } + + // Support legacy DNSLink entries composed by "dnslink={CID}". + if cid, err := cid.Decode(parts[1]); err == nil { + return path.FromCid(cid), nil + } } - return "", errors.New("not a valid dnslink entry") + return nil, errors.New("not a valid dnslink entry") } diff --git a/namesys/namesys.go b/namesys/namesys.go index df4403570..381e5bff1 100644 --- a/namesys/namesys.go +++ b/namesys/namesys.go @@ -100,7 +100,10 @@ func NewNameSystem(r routing.ValueStore, opts ...Option) (NameSystem, error) { for _, pair := range strings.Split(list, ",") { mapping := strings.SplitN(pair, ":", 2) key := mapping[0] - value := path.FromString(mapping[1]) + value, err := path.NewPath(mapping[1]) + if err != nil { + return nil, err + } staticMap[key] = value } } @@ -139,11 +142,11 @@ func (ns *mpns) Resolve(ctx context.Context, name string, options ...opts.Resolv defer span.End() if strings.HasPrefix(name, "/ipfs/") { - return path.ParsePath(name) + return path.NewPath(name) } if !strings.HasPrefix(name, "/") { - return path.ParsePath("/ipfs/" + name) + return path.NewPath("/ipfs/" + name) } return resolve(ctx, ns, name, opts.ProcessOpts(options)) @@ -154,7 +157,7 @@ func (ns *mpns) ResolveAsync(ctx context.Context, name string, options ...opts.R defer span.End() if strings.HasPrefix(name, "/ipfs/") { - p, err := path.ParsePath(name) + p, err := path.NewPath(name) res := make(chan Result, 1) res <- Result{p, err} close(res) @@ -162,7 +165,7 @@ func (ns *mpns) ResolveAsync(ctx context.Context, name string, options ...opts.R } if !strings.HasPrefix(name, "/") { - p, err := path.ParsePath("/ipfs/" + name) + p, err := path.NewPath("/ipfs/" + name) res := make(chan Result, 1) res <- Result{p, err} close(res) @@ -220,7 +223,7 @@ func (ns *mpns) resolveOnceAsync(ctx context.Context, name string, options opts. if p, ok := ns.cacheGet(cacheKey); ok { var err error if len(segments) > 3 { - p, err = path.FromSegments("", strings.TrimRight(p.String(), "/"), segments[3]) + p, err = path.Join(p, segments[3]) } span.SetAttributes(attribute.Bool("CacheHit", true)) span.RecordError(err) @@ -263,7 +266,7 @@ func (ns *mpns) resolveOnceAsync(ctx context.Context, name string, options opts. // Attach rest of the path if len(segments) > 3 { - p, err = path.FromSegments("", strings.TrimRight(p.String(), "/"), segments[3]) + p, err = path.Join(p, segments[3]) } emitOnceResult(ctx, out, onceResult{value: p, ttl: ttl, err: err}) diff --git a/namesys/namesys_test.go b/namesys/namesys_test.go index 52fce6794..9e1d9f5f6 100644 --- a/namesys/namesys_test.go +++ b/namesys/namesys_test.go @@ -17,6 +17,7 @@ import ( ci "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoremem" + "github.com/stretchr/testify/assert" ) type mockResolver struct { @@ -31,15 +32,15 @@ func testResolution(t *testing.T, resolver Resolver, name string, depth uint, ex "expected %s with a depth of %d to have a '%s' error, but got '%s'", name, depth, expError, err)) } - if p.String() != expected { - t.Fatal(fmt.Errorf( - "%s with depth %d resolved to %s != %s", - name, depth, p.String(), expected)) + if expected == "" { + assert.Nil(t, p, "%s with depth %d", name, depth) + } else { + assert.Equal(t, p.String(), expected, "%s with depth %d", name, depth) } } func (r *mockResolver) resolveOnceAsync(ctx context.Context, name string, options opts.ResolveOpts) <-chan onceResult { - p, err := path.ParsePath(r.entries[name]) + p, err := path.NewPath(r.entries[name]) out := make(chan onceResult, 1) out <- onceResult{value: p, err: err} close(out) @@ -118,7 +119,7 @@ func TestPublishWithCache0(t *testing.T) { } // CID is arbitrary. - p, err := path.ParsePath("QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn") + p, err := path.NewPath("/ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn") if err != nil { t.Fatal(err) } @@ -158,7 +159,7 @@ func TestPublishWithTTL(t *testing.T) { } // CID is arbitrary. - p, err := path.ParsePath("QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn") + p, err := path.NewPath("/ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn") if err != nil { t.Fatal(err) } diff --git a/namesys/publisher.go b/namesys/publisher.go index c913b0bbc..9cb3ae66a 100644 --- a/namesys/publisher.go +++ b/namesys/publisher.go @@ -168,7 +168,7 @@ func (p *IpnsPublisher) updateRecord(ctx context.Context, k crypto.PrivKey, valu if err != nil { return nil, err } - if value != path.Path(p.String()) { + if value.String() != p.String() { // Don't bother incrementing the sequence number unless the // value changes. seqno++ diff --git a/namesys/publisher_test.go b/namesys/publisher_test.go index ad975f59a..536e72771 100644 --- a/namesys/publisher_test.go +++ b/namesys/publisher_test.go @@ -56,7 +56,10 @@ func testNamekeyPublisher(t *testing.T, keyType int, expectedErr error, expected } // Value - value := path.Path("ipfs/TESTING") + value, err := path.NewPath("/ipfs/bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4") + if err != nil { + t.Fatal(err) + } // Seqnum seqnum := uint64(0) @@ -125,7 +128,7 @@ func TestAsyncDS(t *testing.T) { publisher := NewIpnsPublisher(rt, ds) ipnsFakeID := testutil.RandIdentityOrFatal(t) - ipnsVal, err := path.ParsePath("/ipns/foo.bar") + ipnsVal, err := path.NewPath("/ipns/foo.bar") if err != nil { t.Fatal(err) } diff --git a/namesys/republisher/repub.go b/namesys/republisher/repub.go index 87200ff5c..bb7a5e2b0 100644 --- a/namesys/republisher/repub.go +++ b/namesys/republisher/repub.go @@ -9,7 +9,6 @@ import ( keystore "github.com/ipfs/boxo/keystore" "github.com/ipfs/boxo/namesys" - "github.com/ipfs/boxo/path" "go.opentelemetry.io/otel/attribute" opts "github.com/ipfs/boxo/coreiface/options/namesys" @@ -165,7 +164,7 @@ func (rp *Republisher) republishEntry(ctx context.Context, priv ic.PrivKey) erro if prevEol.After(eol) { eol = prevEol } - err = rp.ns.Publish(ctx, priv, path.Path(p.String()), opts.PublishWithEOL(eol)) + err = rp.ns.Publish(ctx, priv, p, opts.PublishWithEOL(eol)) span.RecordError(err) return err } diff --git a/namesys/republisher/repub_test.go b/namesys/republisher/repub_test.go index d6c7b0d85..6b5d2abf0 100644 --- a/namesys/republisher/repub_test.go +++ b/namesys/republisher/repub_test.go @@ -91,7 +91,11 @@ func TestRepublish(t *testing.T) { // have one node publish a record that is valid for 1 second publisher := nodes[3] - p := path.FromString("/ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn") // does not need to be valid + p, err := path.NewPath("/ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn") // does not need to be valid + if err != nil { + t.Fatal(err) + } + rp := namesys.NewIpnsPublisher(publisher.dht, publisher.store) name := "/ipns/" + publisher.id @@ -172,12 +176,16 @@ func TestLongEOLRepublish(t *testing.T) { // have one node publish a record that is valid for 1 second publisher := nodes[3] - p := path.FromString("/ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn") // does not need to be valid + p, err := path.NewPath("/ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn") // does not need to be valid + if err != nil { + t.Fatal(err) + } + rp := namesys.NewIpnsPublisher(publisher.dht, publisher.store) name := "/ipns/" + publisher.id expiration := time.Now().Add(time.Hour) - err := rp.Publish(ctx, publisher.privKey, p, opts.PublishWithEOL(expiration)) + err = rp.Publish(ctx, publisher.privKey, p, opts.PublishWithEOL(expiration)) if err != nil { t.Fatal(err) } @@ -239,7 +247,7 @@ func verifyResolution(nsystems []namesys.NameSystem, key string, exp path.Path) return err } - if val != exp { + if val.String() != exp.String() { return errors.New("resolved wrong record") } } diff --git a/namesys/resolve/resolve.go b/namesys/resolve/resolve.go index b2acf0602..b01fd38f7 100644 --- a/namesys/resolve/resolve.go +++ b/namesys/resolve/resolve.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "strings" "github.com/ipfs/boxo/path" "go.opentelemetry.io/otel/attribute" @@ -22,34 +21,35 @@ var ErrNoNamesys = errors.New( func ResolveIPNS(ctx context.Context, nsys namesys.NameSystem, p path.Path) (path.Path, error) { ctx, span := namesys.StartSpan(ctx, "ResolveIPNS", trace.WithAttributes(attribute.String("Path", p.String()))) defer span.End() - if strings.HasPrefix(p.String(), "/ipns/") { + + if p.Namespace() == path.IPNSNamespace { // TODO(cryptix): we should be able to query the local cache for the path if nsys == nil { - return "", ErrNoNamesys + return nil, ErrNoNamesys } seg := p.Segments() if len(seg) < 2 || seg[1] == "" { // just "/" without further segments err := fmt.Errorf("invalid path %q: ipns path missing IPNS ID", p) - return "", err + return nil, err } extensions := seg[2:] - resolvable, err := path.FromSegments("/", seg[0], seg[1]) + resolvable, err := path.NewPathFromSegments(seg[0], seg[1]) if err != nil { - return "", err + return nil, err } respath, err := nsys.Resolve(ctx, resolvable.String()) if err != nil { - return "", err + return nil, err } segments := append(respath.Segments(), extensions...) - p, err = path.FromSegments("/", segments...) + p, err = path.NewPathFromSegments(segments...) if err != nil { - return "", err + return nil, err } } return p, nil diff --git a/namesys/resolve_test.go b/namesys/resolve_test.go index 3aecdccaf..158a9a26c 100644 --- a/namesys/resolve_test.go +++ b/namesys/resolve_test.go @@ -7,7 +7,7 @@ import ( "time" ipns "github.com/ipfs/boxo/ipns" - path "github.com/ipfs/boxo/path" + "github.com/ipfs/boxo/path" mockrouting "github.com/ipfs/boxo/routing/mock" ds "github.com/ipfs/go-datastore" dssync "github.com/ipfs/go-datastore/sync" @@ -25,8 +25,12 @@ func TestRoutingResolve(t *testing.T) { identity := tnet.RandIdentityOrFatal(t) - h := path.FromString("/ipfs/QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN") - err := publisher.Publish(context.Background(), identity.PrivateKey(), h) + h, err := path.NewPath("/ipfs/QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN") + if err != nil { + t.Fatal(err) + } + + err = publisher.Publish(context.Background(), identity.PrivateKey(), h) if err != nil { t.Fatal(err) } @@ -36,7 +40,7 @@ func TestRoutingResolve(t *testing.T) { t.Fatal(err) } - if res != h { + if res.String() != h.String() { t.Fatal("Got back incorrect value.") } } @@ -51,7 +55,10 @@ func TestPrexistingExpiredRecord(t *testing.T) { identity := tnet.RandIdentityOrFatal(t) // Make an expired record and put it in the datastore - h := path.FromString("/ipfs/QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN") + h, err := path.NewPath("/ipfs/QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN") + if err != nil { + t.Fatal(err) + } eol := time.Now().Add(time.Hour * -1) entry, err := ipns.NewRecord(identity.PrivateKey(), h, 0, eol, 0) @@ -85,7 +92,10 @@ func TestPrexistingRecord(t *testing.T) { identity := tnet.RandIdentityOrFatal(t) // Make a good record and put it in the datastore - h := path.FromString("/ipfs/QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN") + h, err := path.NewPath("/ipfs/QmZULkCELmmk5XNfCgTnCyFgAVxBRBXyDHGGMVoLFLiXEN") + if err != nil { + t.Fatal(err) + } eol := time.Now().Add(time.Hour) entry, err := ipns.NewRecord(identity.PrivateKey(), h, 0, eol, 0) if err != nil { @@ -114,7 +124,7 @@ func verifyCanResolve(r Resolver, name string, exp path.Path) error { return err } - if res != exp { + if res.String() != exp.String() { return errors.New("got back wrong record") } diff --git a/namesys/routing.go b/namesys/routing.go index 6b706bd92..1153341ab 100644 --- a/namesys/routing.go +++ b/namesys/routing.go @@ -136,7 +136,7 @@ func (r *IpnsResolver) resolveOnceAsync(ctx context.Context, name string, option return } - emitOnceResult(ctx, out, onceResult{value: path.Path(p.String()), ttl: ttl}) + emitOnceResult(ctx, out, onceResult{value: p, ttl: ttl}) case <-ctx.Done(): return } diff --git a/path/error.go b/path/error.go index dafc446b5..0863c7cf4 100644 --- a/path/error.go +++ b/path/error.go @@ -1,25 +1,32 @@ 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) +func (e *ErrInvalidPath) Error() string { + return fmt.Sprintf("invalid path %q: %s", e.path, e.err) } -func (e ErrInvalidPath) Unwrap() error { - return e.error +func (e *ErrInvalidPath) Unwrap() error { + return e.err } -func (e ErrInvalidPath) Is(err error) bool { +func (e *ErrInvalidPath) Is(err error) bool { switch err.(type) { - case ErrInvalidPath: + case *ErrInvalidPath: return true default: return false diff --git a/path/error_test.go b/path/error_test.go index 07aab6408..2b5f92945 100644 --- a/path/error_test.go +++ b/path/error_test.go @@ -6,11 +6,7 @@ import ( ) func TestErrorIs(t *testing.T) { - if !errors.Is(ErrInvalidPath{path: "foo", error: 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/internal/tracing.go b/path/internal/tracing.go deleted file mode 100644 index f9eda2f92..000000000 --- a/path/internal/tracing.go +++ /dev/null @@ -1,13 +0,0 @@ -package internal - -import ( - "context" - "fmt" - - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/trace" -) - -func StartSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { - return otel.Tracer("go-path").Start(ctx, fmt.Sprintf("Path.%s", name), opts...) -} diff --git a/path/path.go b/path/path.go index a9b36c3ce..5e39eea3a 100644 --- a/path/path.go +++ b/path/path.go @@ -3,187 +3,221 @@ package path import ( "fmt" - "path" + gopath "path" "strings" - cid "github.com/ipfs/go-cid" + "github.com/ipfs/go-cid" ) -// A Path represents an ipfs content path: -// - /path/to/file -// - /ipfs/ -// - /ipns//path/to/folder -// - etc -type Path string +const ( + IPFSNamespace = "ipfs" + IPNSNamespace = "ipns" + IPLDNamespace = "ipld" +) + +// Path is a generic, valid, and well-formed path. A valid path is shaped as follows: +// +// /{namespace}/{root}[/remaining/path] +// +// Where: +// +// 1. Namespace is "ipfs", "ipld", or "ipns". +// 2. If namespace is "ipfs" or "ipld", "root" must be a valid [cid.Cid]. +// 3. If namespace is "ipns", "root" may be a [ipns.Name] or a [DNSLink] FQDN. +// +// [DNSLink]: https://dnslink.dev/ +type Path interface { + // String returns the path as a string. + String() string + + // Namespace returns the first component of the path. For example, the namespace + // of "/ipfs/bafy" is "ipfs". + Namespace() string + + // Mutable returns false if the data under this path's namespace is guaranteed to not change. + Mutable() bool + + // Segments returns the different elements of a path delimited by a forward + // slash ("/"). The returned array must not contain any empty segments, and + // must have a length of at least two: the first element must be the namespace, + // and the second must be root. + // + // Examples: + // - "/ipld/bafkqaaa" returns ["ipld", "bafkqaaa"] + // - "/ipfs/bafkqaaa/a/b/" returns ["ipfs", "bafkqaaa", "a", "b"] + // - "/ipns/dnslink.net" returns ["ipns", "dnslink.net"] + Segments() []string +} -// ^^^ -// TODO: debate making this a private struct wrapped in a public interface -// would allow us to control creation, and cache segments. +var _ Path = path{} -// FromString safely converts a string type to a Path type. -func FromString(s string) Path { - return Path(s) +type path struct { + str string + namespace string } -// FromCid safely converts a cid.Cid type to a Path type. -func FromCid(c cid.Cid) Path { - return Path("/ipfs/" + c.String()) +func (p path) String() string { + return p.str } -// Segments returns the different elements of a path -// (elements are delimited by a /). -func (p Path) Segments() []string { - cleaned := path.Clean(string(p)) - segments := strings.Split(cleaned, "/") +func (p path) Namespace() string { + return p.namespace +} - // Ignore leading slash - if len(segments[0]) == 0 { - segments = segments[1:] - } +func (p path) Mutable() bool { + return p.Namespace() != IPFSNamespace && p.Namespace() != IPLDNamespace +} - return segments +func (p path) Segments() []string { + return StringToSegments(p.str) } -// String converts a path to string. -func (p Path) String() string { - return string(p) +// ImmutablePath is a [Path] which is guaranteed to have an immutable [Namespace]. +type ImmutablePath interface { + Path + + // RootCid returns the [cid.Cid] of the root object of the path. + RootCid() cid.Cid } -// IsJustAKey returns true if the path is of the form or /ipfs/, or -// /ipld/ -func (p Path) IsJustAKey() bool { - parts := p.Segments() - return len(parts) == 2 && (parts[0] == "ipfs" || parts[0] == "ipld") +var _ Path = immutablePath{} +var _ ImmutablePath = immutablePath{} + +type immutablePath struct { + path Path + rootCid cid.Cid } -// PopLastSegment returns a new Path without its final segment, and the final -// segment, separately. If there is no more to pop (the path is just a key), -// the original path is returned. -func (p Path) PopLastSegment() (Path, string, error) { - if p.IsJustAKey() { - return p, "", nil +func NewImmutablePath(p Path) (ImmutablePath, error) { + if p.Mutable() { + return nil, &ErrInvalidPath{err: ErrExpectedImmutable, path: p.String()} } - segs := p.Segments() - newPath, err := ParsePath("/" + strings.Join(segs[:len(segs)-1], "/")) + segments := p.Segments() + cid, err := cid.Decode(segments[1]) if err != nil { - return "", "", err + return nil, &ErrInvalidPath{err: err, path: p.String()} } - return newPath, segs[len(segs)-1], nil + return immutablePath{path: p, rootCid: cid}, nil } -// FromSegments returns a path given its different segments. -func FromSegments(prefix string, seg ...string) (Path, error) { - return ParsePath(prefix + strings.Join(seg, "/")) +func (ip immutablePath) String() string { + return ip.path.String() } -// ParsePath returns a well-formed ipfs Path. -// The returned path will always be prefixed with /ipfs/ or /ipns/. -// The prefix will be added if not present in the given string. -// This function will return an error when the given string is -// not a valid ipfs path. -func ParsePath(txt string) (Path, error) { - parts := strings.Split(txt, "/") - if len(parts) == 1 { - kp, err := ParseCidToPath(txt) - if err == nil { - return kp, nil - } - } +func (ip immutablePath) Namespace() string { + return ip.path.Namespace() +} - // if the path doesnt begin with a '/' - // we expect this to start with a hash, and be an 'ipfs' path - if parts[0] != "" { - if _, err := decodeCid(parts[0]); err != nil { - return "", &ErrInvalidPath{error: err, path: txt} - } - // The case when the path starts with hash without a protocol prefix - return Path("/ipfs/" + txt), nil - } +func (ip immutablePath) Mutable() bool { + return false +} - if len(parts) < 3 { - return "", &ErrInvalidPath{error: fmt.Errorf("invalid ipfs path"), path: txt} - } +func (ip immutablePath) Segments() []string { + return ip.path.Segments() +} - // TODO: make this smarter - switch parts[1] { - case "ipfs", "ipld": - if parts[2] == "" { - return "", &ErrInvalidPath{error: fmt.Errorf("not enough path components"), path: txt} - } - // Validate Cid. - _, err := decodeCid(parts[2]) - if err != nil { - return "", &ErrInvalidPath{error: fmt.Errorf("invalid CID: %w", err), path: txt} - } - case "ipns": - if parts[2] == "" { - return "", &ErrInvalidPath{error: fmt.Errorf("not enough path components"), path: txt} - } - default: - return "", &ErrInvalidPath{error: fmt.Errorf("unknown namespace %q", parts[1]), path: txt} - } +func (ip immutablePath) RootCid() cid.Cid { + return ip.rootCid +} - return Path(txt), nil +// FromCid returns a new "/ipfs" path with the provided CID. +func FromCid(cid cid.Cid) ImmutablePath { + return immutablePath{ + path: path{ + str: fmt.Sprintf("/%s/%s", IPFSNamespace, cid.String()), + namespace: IPFSNamespace, + }, + rootCid: cid, + } } -// ParseCidToPath takes a CID in string form and returns a valid ipfs Path. -func ParseCidToPath(txt string) (Path, error) { - if txt == "" { - return "", &ErrInvalidPath{error: fmt.Errorf("empty"), path: txt} +// 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 content path. +func NewPath(str string) (Path, error) { + segments := StringToSegments(str) + + // Shortest valid path is "/{namespace}/{root}". That yields at least two + // segments: ["{namespace}" "{root}"]. Therefore, here we check if the original + // string begins with "/" (any path must), if we have at least two segments, and if + // the root is non-empty. The namespace is checked further below. + if !strings.HasPrefix(str, "/") || len(segments) < 2 || segments[1] == "" { + return nil, &ErrInvalidPath{err: ErrInsufficientComponents, path: str} } - c, err := decodeCid(txt) - if err != nil { - return "", &ErrInvalidPath{error: err, path: txt} + cleaned := SegmentsToString(segments...) + if strings.HasSuffix(str, "/") { + // Do not forget to preserve the trailing slash! + cleaned += "/" } - return FromCid(c), nil -} + switch segments[0] { + case IPFSNamespace, IPLDNamespace: + cid, err := cid.Decode(segments[1]) + if err != nil { + return nil, &ErrInvalidPath{err: err, path: str} + } -// IsValid checks if a path is a valid ipfs Path. -func (p *Path) IsValid() error { - _, err := ParsePath(p.String()) - return err + return immutablePath{ + path: path{ + str: cleaned, + namespace: segments[0], + }, + rootCid: cid, + }, nil + case "ipns": + return path{ + str: cleaned, + namespace: segments[0], + }, nil + default: + return nil, &ErrInvalidPath{err: fmt.Errorf("%w: %q", ErrUnknownNamespace, segments[0]), path: str} + } } -// Join joins strings slices using / -func Join(pths []string) string { - return strings.Join(pths, "/") +// NewPathFromSegments creates a new [Path] from the provided segments. This +// function simply calls [NewPath] internally with the segments concatenated +// using a forward slash "/" as separator. Please see [Path.Segments] for more +// information about how segments must be structured. +func NewPathFromSegments(segments ...string) (Path, error) { + return NewPath(SegmentsToString(segments...)) } -// SplitList splits strings usings / -func SplitList(pth string) []string { - return strings.Split(pth, "/") +// Join joins a [Path] with certain segments and returns a new [Path]. +func Join(p Path, segments ...string) (Path, error) { + s := p.Segments() + s = append(s, segments...) + return NewPathFromSegments(s...) } -// SplitAbsPath clean up and split fpath. It extracts the first component (which -// must be a Multihash) and return it separately. -func SplitAbsPath(fpath Path) (cid.Cid, []string, error) { - parts := fpath.Segments() - if parts[0] == "ipfs" || parts[0] == "ipld" { - parts = parts[1:] +// SegmentsToString converts an array of segments into a string. The returned string +// will always be prefixed with a "/" if there are any segments. For example, if the +// given segments array is ["foo", "bar"], the returned value will be "/foo/bar". +// Given an empty array, an empty string is returned. +func SegmentsToString(segments ...string) string { + str := strings.Join(segments, "/") + if str != "" { + str = "/" + str } - - // if nothing, bail. - if len(parts) == 0 { - return cid.Cid{}, nil, &ErrInvalidPath{error: fmt.Errorf("empty"), path: string(fpath)} - } - - c, err := decodeCid(parts[0]) - // first element in the path is a cid - if err != nil { - return cid.Cid{}, nil, &ErrInvalidPath{error: fmt.Errorf("invalid CID: %w", err), path: string(fpath)} - } - - return c, parts[1:], nil + return str } -func decodeCid(cstr string) (cid.Cid, error) { - c, err := cid.Decode(cstr) - if err != nil && len(cstr) == 46 && cstr[:2] == "qm" { // https://github.com/ipfs/go-ipfs/issues/7792 - return cid.Cid{}, fmt.Errorf("%v (possible lowercased CIDv0; consider converting to a case-agnostic CIDv1, such as base32)", err) +// StringToSegments converts a string into an array of segments. This function follows +// the rules of [Path.Segments]: the path is first cleaned through [gopath.Clean] and +// no empty segments are returned. +func StringToSegments(str string) []string { + str = gopath.Clean(str) + if str == "." { + return nil + } + // Trim slashes from beginning and end, such that we do not return empty segments. + str = strings.TrimSuffix(str, "/") + str = strings.TrimPrefix(str, "/") + if str == "" { + return nil } - return c, err + return strings.Split(str, "/") } diff --git a/path/path_test.go b/path/path_test.go index 2b26a5678..2f944e39e 100644 --- a/path/path_test.go +++ b/path/path_test.go @@ -1,128 +1,305 @@ package path import ( - "strings" + "fmt" "testing" + + "github.com/ipfs/go-cid" + "github.com/stretchr/testify/assert" ) -func TestPathParsing(t *testing.T) { - cases := map[string]bool{ - "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": true, - "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a": true, - "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b/c/d/e/f": true, - "/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": true, - "/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a": true, - "/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b/c/d/e/f": true, - "/ipns/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b/c/d/e/f": true, - "/ipns/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": true, - "QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b/c/d/e/f": true, - "QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": true, - "/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": false, - "/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a": false, - "/ipfs/foo": false, - "/ipfs/": false, - "ipfs/": false, - "ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": false, - "/ipld/foo": false, - "/ipld/": false, - "ipld/": false, - "ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": false, +func newIPLDPath(cid cid.Cid) ImmutablePath { + return immutablePath{ + path: path{ + str: fmt.Sprintf("/%s/%s", IPLDNamespace, cid.String()), + namespace: IPLDNamespace, + }, + rootCid: cid, } +} + +func TestNewPath(t *testing.T) { + t.Parallel() + + t.Run("Valid Paths", func(t *testing.T) { + t.Parallel() + + testCases := []struct { + src string + canonical string + namespace string + mutable bool + }{ + // IPFS CIDv0 + {"/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", IPFSNamespace, false}, + {"/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a", "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a", IPFSNamespace, false}, + {"/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b/c/d/e/f", "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b/c/d/e/f", IPFSNamespace, false}, + + // IPFS CIDv1 + {"/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", "/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", IPFSNamespace, false}, + {"/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a", "/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a", IPFSNamespace, false}, + {"/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a/b/c/d/e/f", "/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a/b/c/d/e/f", IPFSNamespace, false}, + + // IPLD CIDv0 + {"/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", "/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", IPLDNamespace, false}, + {"/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a", "/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a", IPLDNamespace, false}, + {"/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b/c/d/e/f", "/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b/c/d/e/f", IPLDNamespace, false}, - for p, expected := range cases { - _, err := ParsePath(p) - valid := err == nil - if valid != expected { - t.Fatalf("expected %s to have valid == %t", p, expected) + // IPLD CIDv1 + {"/ipld/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", "/ipld/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", IPLDNamespace, false}, + {"/ipld/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a", "/ipld/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a", IPLDNamespace, false}, + {"/ipld/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a/b/c/d/e/f", "/ipld/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a/b/c/d/e/f", IPLDNamespace, false}, + + // IPNS CIDv0 + {"/ipns/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", "/ipns/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", IPNSNamespace, true}, + {"/ipns/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a", "/ipns/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a", IPNSNamespace, true}, + {"/ipns/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b/c/d/e/f", "/ipns/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b/c/d/e/f", IPNSNamespace, true}, + + // IPNS CIDv1 + {"/ipns/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", "/ipns/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", IPNSNamespace, true}, + {"/ipns/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a", "/ipns/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a", IPNSNamespace, true}, + {"/ipns/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a/b/c/d/e/f", "/ipns/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a/b/c/d/e/f", IPNSNamespace, true}, + + // IPNS DNSLink + {"/ipns/domain.net", "/ipns/domain.net", IPNSNamespace, true}, + {"/ipns/domain.net/a/b/c/d", "/ipns/domain.net/a/b/c/d", IPNSNamespace, true}, + + // Cleaning checks + {"/ipfs/bafkqaaa/", "/ipfs/bafkqaaa/", IPFSNamespace, false}, + {"/ipfs/bafkqaaa//", "/ipfs/bafkqaaa/", IPFSNamespace, false}, + {"/ipfs///bafkqaaa//", "/ipfs/bafkqaaa/", IPFSNamespace, false}, + {"/ipfs///bafkqaaa/a/b/../c", "/ipfs/bafkqaaa/a/c", IPFSNamespace, false}, + {"/ipfs///bafkqaaa/a/b/../c/", "/ipfs/bafkqaaa/a/c/", IPFSNamespace, false}, } - } -} -func TestNoComponents(t *testing.T) { - for _, s := range []string{ - "/ipfs/", - "/ipns/", - "/ipld/", - } { - _, err := ParsePath(s) - if err == nil || !strings.Contains(err.Error(), "not enough path components") || !strings.Contains(err.Error(), s) { - t.Error("wrong error") + for _, testCase := range testCases { + p, err := NewPath(testCase.src) + assert.NoError(t, err) + assert.Equal(t, testCase.canonical, p.String()) + assert.Equal(t, testCase.namespace, p.Namespace()) + assert.Equal(t, testCase.mutable, p.Mutable()) } - } -} + }) + + t.Run("Invalid Paths", func(t *testing.T) { + t.Parallel() -func TestInvalidPaths(t *testing.T) { - for _, s := range []string{ - "/ipfs", - "/testfs", - "/", - } { - _, err := ParsePath(s) - if err == nil || !strings.Contains(err.Error(), "invalid ipfs path") || !strings.Contains(err.Error(), s) { - t.Error("wrong error") + testCases := []struct { + src string + err error + }{ + {"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/", ErrInsufficientComponents}, + {"ipld/", ErrInsufficientComponents}, + {"ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", ErrInsufficientComponents}, + {"/ipns", ErrInsufficientComponents}, + {"/ipfs/", ErrInsufficientComponents}, + {"/ipns/", ErrInsufficientComponents}, + {"/ipld/", ErrInsufficientComponents}, + {"/ipfs", ErrInsufficientComponents}, + {"/testfs", ErrInsufficientComponents}, + {"/", ErrInsufficientComponents}, } - } -} -func TestIsJustAKey(t *testing.T) { - cases := map[string]bool{ - "QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": true, - "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": true, - "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a": false, - "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b": false, - "/ipns/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": false, - "/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b": false, - "/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": true, - } + 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() - for p, expected := range cases { - path, err := ParsePath(p) - if err != nil { - t.Fatalf("ParsePath failed to parse \"%s\", but should have succeeded", p) + testCases := []struct { + src string + }{ + {"/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"}, + {"/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a"}, + {"/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a/b/c/d/e/f"}, + {"/ipld/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"}, + {"/ipld/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a"}, + {"/ipld/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a/b/c/d/e/f"}, } - result := path.IsJustAKey() - if result != expected { - t.Fatalf("expected IsJustAKey(%s) to return %v, not %v", p, expected, result) + + for _, testCase := range testCases { + p, err := NewPath(testCase.src) + assert.NoError(t, err) + assert.IsType(t, immutablePath{}, p) } - } + }) } -func TestPopLastSegment(t *testing.T) { - cases := map[string][]string{ - "QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": {"/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", ""}, - "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": {"/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", ""}, - "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a": {"/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", "a"}, - "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b": {"/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a", "b"}, - "/ipns/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/x/y/z": {"/ipns/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/x/y", "z"}, - "/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/x/y/z": {"/ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/x/y", "z"}, - } +func TestFromCid(t *testing.T) { + t.Parallel() - for p, expected := range cases { - path, err := ParsePath(p) - if err != nil { - t.Fatalf("ParsePath failed to parse \"%s\", but should have succeeded", p) - } - head, tail, err := path.PopLastSegment() - if err != nil { - t.Fatalf("PopLastSegment failed, but should have succeeded: %s", err) + t.Run("Works with CIDv0", func(t *testing.T) { + t.Parallel() + + c, err := cid.Decode("QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n") + assert.NoError(t, err) + + p := FromCid(c) + assert.IsType(t, immutablePath{}, p) + assert.Equal(t, "/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", p.String()) + assert.Equal(t, c, p.RootCid()) + }) + + t.Run("Works with CIDv1", func(t *testing.T) { + t.Parallel() + + c, err := cid.Decode("bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku") + assert.NoError(t, err) + + p := FromCid(c) + assert.IsType(t, immutablePath{}, p) + assert.Equal(t, "/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", p.String()) + assert.Equal(t, c, p.RootCid()) + }) + + 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.RootCid()) + + // 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.RootCid()) + }) +} + +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{}) } - headStr := head.String() - if headStr != expected[0] { - t.Fatalf("expected head of PopLastSegment(%s) to return %v, not %v", p, expected[0], headStr) + }) + + t.Run("Succeeds on Immutable Path", func(t *testing.T) { + testCases := []struct { + path string + cid cid.Cid + }{ + {"/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n", cid.MustParse("QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n")}, + {"/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b", cid.MustParse("QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n")}, + {"/ipfs/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n/a/b/", cid.MustParse("QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n")}, + + {"/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", cid.MustParse("bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku")}, + {"/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a/b", cid.MustParse("bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku")}, + {"/ipfs/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a/b/", cid.MustParse("bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku")}, + + {"/ipld/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", cid.MustParse("bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku")}, + {"/ipld/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a/b", cid.MustParse("bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku")}, + {"/ipld/bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku/a/b/", cid.MustParse("bafybeihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku")}, } - if tail != expected[1] { - t.Fatalf("expected tail of PopLastSegment(%s) to return %v, not %v", p, expected[1], tail) + + 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.RootCid()) } + }) +} + +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()) } } -func TestV0ErrorDueToLowercase(t *testing.T) { - badb58 := "/ipfs/qmbwqxbekc3p8tqskc98xmwnzrzdtrlmimpl8wbutgsmnr" - _, err := ParsePath(badb58) - if err == nil { - t.Fatal("should have failed to decode") +func TestStringToSegments(t *testing.T) { + testCases := []struct { + str string + expected []string + }{ + {"", nil}, + {"/..", nil}, + {"/a/b/c/d/./../../../../../..", nil}, + {"/a/b/c/d/./../../../", []string{"a"}}, + {"/a/b//c/d/./../../", []string{"a", "b"}}, + {"/a/b/////c/../d///f", []string{"a", "b", "d", "f"}}, } - if !strings.HasSuffix(err.Error(), "(possible lowercased CIDv0; consider converting to a case-agnostic CIDv1, such as base32)") { - t.Fatal("should have meaningful info about case-insensitive fix") + + for _, testCase := range testCases { + segments := StringToSegments(testCase.str) + assert.Equal(t, testCase.expected, segments) + } +} + +func TestSegmentsToString(t *testing.T) { + testCases := []struct { + segments []string + expected string + }{ + {[]string{"a", "b"}, "/a/b"}, + {[]string{"a", "b", "d", "f"}, "/a/b/d/f"}, + {[]string{""}, ""}, + {[]string{}, ""}, + {nil, ""}, + } + + for _, testCase := range testCases { + str := SegmentsToString(testCase.segments...) + assert.Equal(t, testCase.expected, str) } } diff --git a/path/resolver/resolver.go b/path/resolver/resolver.go index f666d4b79..e7cc25f92 100644 --- a/path/resolver/resolver.go +++ b/path/resolver/resolver.go @@ -3,19 +3,17 @@ package resolver import ( "context" - "errors" "fmt" "time" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "github.com/ipfs/boxo/fetcher" fetcherhelpers "github.com/ipfs/boxo/fetcher/helpers" - path "github.com/ipfs/boxo/path" - "github.com/ipfs/boxo/path/internal" + "github.com/ipfs/boxo/path" cid "github.com/ipfs/go-cid" - format "github.com/ipfs/go-ipld-format" logging "github.com/ipfs/go-log/v2" "github.com/ipld/go-ipld-prime" cidlink "github.com/ipld/go-ipld-prime/linking/cid" @@ -24,76 +22,73 @@ import ( "github.com/ipld/go-ipld-prime/traversal/selector/builder" ) -var log = logging.Logger("pathresolv") +var log = logging.Logger("path/resolver") -// ErrNoComponents is used when Paths after a protocol -// do not contain at least one component -var ErrNoComponents = errors.New( - "path must contain at least one component") - -// ErrNoLink is returned when a link is not found in a path +// ErrNoLink is returned when a link is not found in a path. type ErrNoLink struct { Name string Node cid.Cid } -// Error implements the Error interface for ErrNoLink with a useful -// human readable message. -func (e ErrNoLink) Error() string { +// Error implements the [errors.Error] interface. +func (e *ErrNoLink) Error() string { return fmt.Sprintf("no link named %q under %s", e.Name, e.Node.String()) } +// Is implements [errors.Is] interface. +func (e *ErrNoLink) Is(err error) bool { + switch err.(type) { + case *ErrNoLink: + return true + default: + return false + } +} + // Resolver provides path resolution to IPFS. type Resolver interface { - // ResolveToLastNode walks the given path and returns the cid of the - // last block referenced by the path, and the path segments to - // traverse from the final block boundary to the final node within the - // block. - ResolveToLastNode(ctx context.Context, fpath path.Path) (cid.Cid, []string, error) - // ResolvePath fetches the node for given path. It returns the last - // item returned by ResolvePathComponents and the last link traversed - // which can be used to recover the block. - ResolvePath(ctx context.Context, fpath path.Path) (ipld.Node, ipld.Link, error) - // ResolvePathComponents fetches the nodes for each segment of the given path. - // It uses the first path component as a hash (key) of the first node, then - // resolves all other components walking the links via a selector traversal - ResolvePathComponents(ctx context.Context, fpath path.Path) ([]ipld.Node, error) + // ResolveToLastNode walks the given path and returns the CID of the last block + // referenced by the path, as well as the remainder of the path segments to traverse + // from the final block boundary to the final node within the block. + ResolveToLastNode(context.Context, path.ImmutablePath) (cid.Cid, []string, error) + + // ResolvePath fetches the node for the given path. It returns the last item returned + // by [Resolver.ResolvePathComponents] and the last link traversed which can be used + // to recover the block. + ResolvePath(context.Context, path.ImmutablePath) (ipld.Node, ipld.Link, error) + + // ResolvePathComponents fetches the nodes for each segment of the given path. It + // uses the first path component as the CID of the first node, then resolves all + // other components walking the links via a selector traversal. + ResolvePathComponents(context.Context, path.ImmutablePath) ([]ipld.Node, error) } -// basicResolver implements the Resolver interface. -// It references a FetcherFactory, which is uses to resolve nodes. -// TODO: now that this is more modular, try to unify this code with the -// -// the resolvers in namesys. +// basicResolver implements the [Resolver] interface. It requires a [fetcher.Factory], +// which is used to resolve the nodes. type basicResolver struct { FetcherFactory fetcher.Factory } -// NewBasicResolver constructs a new basic resolver. -func NewBasicResolver(fetcherFactory fetcher.Factory) Resolver { +// NewBasicResolver constructs a new basic resolver using the given [fetcher.Factory]. +func NewBasicResolver(factory fetcher.Factory) Resolver { return &basicResolver{ - FetcherFactory: fetcherFactory, + FetcherFactory: factory, } } -// ResolveToLastNode walks the given path and returns the cid of the last -// block referenced by the path, and the path segments to traverse from the -// final block boundary to the final node within the block. -func (r *basicResolver) ResolveToLastNode(ctx context.Context, fpath path.Path) (cid.Cid, []string, error) { - ctx, span := internal.StartSpan(ctx, "basicResolver.ResolveToLastNode", trace.WithAttributes(attribute.Stringer("Path", fpath))) +// ResolveToLastNode implements [Resolver.ResolveToLastNode]. +func (r *basicResolver) ResolveToLastNode(ctx context.Context, fpath path.ImmutablePath) (cid.Cid, []string, error) { + ctx, span := startSpan(ctx, "basicResolver.ResolveToLastNode", trace.WithAttributes(attribute.Stringer("Path", fpath))) defer span.End() - c, p, err := path.SplitAbsPath(fpath) - if err != nil { - return cid.Cid{}, nil, err - } + c, remainder := fpath.RootCid(), fpath.Segments()[2:] - if len(p) == 0 { + if len(remainder) == 0 { return c, nil, nil } // create a selector to traverse and match all path segments - pathSelector := pathAllSelector(p[:len(p)-1]) + pathSelector := pathAllSelector(remainder[:len(remainder)-1]) // create a new cancellable session ctx, cancel := context.WithTimeout(ctx, time.Minute) @@ -107,19 +102,19 @@ func (r *basicResolver) ResolveToLastNode(ctx context.Context, fpath path.Path) if len(nodes) < 1 { return cid.Cid{}, nil, fmt.Errorf("path %v did not resolve to a node", fpath) - } else if len(nodes) < len(p) { - return cid.Undef, nil, ErrNoLink{Name: p[len(nodes)-1], Node: lastCid} + } else if len(nodes) < len(remainder) { + return cid.Undef, nil, &ErrNoLink{Name: remainder[len(nodes)-1], Node: lastCid} } parent := nodes[len(nodes)-1] - lastSegment := p[len(p)-1] + lastSegment := remainder[len(remainder)-1] // find final path segment within node nd, err := parent.LookupBySegment(ipld.ParsePathSegment(lastSegment)) switch err.(type) { case nil: case schema.ErrNoSuchField: - return cid.Undef, nil, ErrNoLink{Name: lastSegment, Node: lastCid} + return cid.Undef, nil, &ErrNoLink{Name: lastSegment, Node: lastCid} default: return cid.Cid{}, nil, err } @@ -127,7 +122,7 @@ func (r *basicResolver) ResolveToLastNode(ctx context.Context, fpath path.Path) // if last node is not a link, just return it's cid, add path to remainder and return if nd.Kind() != ipld.Kind_Link { // return the cid and the remainder of the path - return lastCid, p[len(p)-depth-1:], nil + return lastCid, remainder[len(remainder)-depth-1:], nil } lnk, err := nd.AsLink() @@ -143,27 +138,18 @@ func (r *basicResolver) ResolveToLastNode(ctx context.Context, fpath path.Path) return clnk.Cid, []string{}, nil } -// ResolvePath fetches the node for given path. It returns the last item -// returned by ResolvePathComponents and the last link traversed which can be used to recover the block. +// ResolvePath implements [Resolver.ResolvePath]. // -// Note: if/when the context is cancelled or expires then if a multi-block ADL node is returned then it may not be -// possible to load certain values. -func (r *basicResolver) ResolvePath(ctx context.Context, fpath path.Path) (ipld.Node, ipld.Link, error) { - ctx, span := internal.StartSpan(ctx, "basicResolver.ResolvePath", trace.WithAttributes(attribute.Stringer("Path", fpath))) +// Note: if/when the context is cancelled or expires then if a multi-block ADL +// node is returned then it may not be possible to load certain values. +func (r *basicResolver) ResolvePath(ctx context.Context, fpath path.ImmutablePath) (ipld.Node, ipld.Link, error) { + ctx, span := startSpan(ctx, "basicResolver.ResolvePath", trace.WithAttributes(attribute.Stringer("Path", fpath))) defer span.End() - // validate path - if err := fpath.IsValid(); err != nil { - return nil, nil, err - } - - c, p, err := path.SplitAbsPath(fpath) - if err != nil { - return nil, nil, err - } + c, remainder := fpath.RootCid(), fpath.Segments()[2:] // create a selector to traverse all path segments but only match the last - pathSelector := pathLeafSelector(p) + pathSelector := pathLeafSelector(remainder) nodes, c, _, err := r.resolveNodes(ctx, c, pathSelector) if err != nil { @@ -175,81 +161,29 @@ func (r *basicResolver) ResolvePath(ctx context.Context, fpath path.Path) (ipld. return nodes[len(nodes)-1], cidlink.Link{Cid: c}, nil } -// ResolveSingle simply resolves one hop of a path through a graph with no -// extra context (does not opaquely resolve through sharded nodes) -// Deprecated: fetch node as ipld-prime or convert it and then use a selector to traverse through it. -func ResolveSingle(ctx context.Context, ds format.NodeGetter, nd format.Node, names []string) (*format.Link, []string, error) { - _, span := internal.StartSpan(ctx, "ResolveSingle", trace.WithAttributes(attribute.Stringer("CID", nd.Cid()))) - defer span.End() - return nd.ResolveLink(names) -} - -// ResolvePathComponents fetches the nodes for each segment of the given path. -// It uses the first path component as a hash (key) of the first node, then -// resolves all other components walking the links via a selector traversal +// ResolvePathComponents implements [Resolver.ResolvePathComponents]. // -// Note: if/when the context is cancelled or expires then if a multi-block ADL node is returned then it may not be -// possible to load certain values. -func (r *basicResolver) ResolvePathComponents(ctx context.Context, fpath path.Path) (nodes []ipld.Node, err error) { - ctx, span := internal.StartSpan(ctx, "basicResolver.ResolvePathComponents", trace.WithAttributes(attribute.Stringer("Path", fpath))) +// Note: if/when the context is cancelled or expires then if a multi-block ADL +// node is returned then it may not be possible to load certain values. +func (r *basicResolver) ResolvePathComponents(ctx context.Context, fpath path.ImmutablePath) (nodes []ipld.Node, err error) { + ctx, span := startSpan(ctx, "basicResolver.ResolvePathComponents", trace.WithAttributes(attribute.Stringer("Path", fpath))) defer span.End() defer log.Debugw("resolvePathComponents", "fpath", fpath, "error", err) - // validate path - if err := fpath.IsValid(); err != nil { - return nil, err - } - - c, p, err := path.SplitAbsPath(fpath) - if err != nil { - return nil, err - } + c, remainder := fpath.RootCid(), fpath.Segments()[2:] // create a selector to traverse and match all path segments - pathSelector := pathAllSelector(p) + pathSelector := pathAllSelector(remainder) nodes, _, _, err = r.resolveNodes(ctx, c, pathSelector) return nodes, err } -// ResolveLinks iteratively resolves names by walking the link hierarchy. -// Every node is fetched from the Fetcher, resolving the next name. -// Returns the list of nodes forming the path, starting with ndd. This list is -// guaranteed never to be empty. -// -// ResolveLinks(nd, []string{"foo", "bar", "baz"}) -// would retrieve "baz" in ("bar" in ("foo" in nd.Links).Links).Links -// -// Note: if/when the context is cancelled or expires then if a multi-block ADL node is returned then it may not be -// possible to load certain values. -func (r *basicResolver) ResolveLinks(ctx context.Context, ndd ipld.Node, names []string) (nodes []ipld.Node, err error) { - ctx, span := internal.StartSpan(ctx, "basicResolver.ResolveLinks") - defer span.End() - - defer log.Debugw("resolvePathComponents", "names", names, "error", err) - // create a selector to traverse and match all path segments - pathSelector := pathAllSelector(names) - - session := r.FetcherFactory.NewSession(ctx) - - // traverse selector - nodes = []ipld.Node{ndd} - err = session.NodeMatching(ctx, ndd, pathSelector, func(res fetcher.FetchResult) error { - nodes = append(nodes, res.Node) - return nil - }) - if err != nil { - return nil, err - } - - return nodes, err -} - // Finds nodes matching the selector starting with a cid. Returns the matched nodes, the cid of the block containing // the last node, and the depth of the last node within its block (root is depth 0). func (r *basicResolver) resolveNodes(ctx context.Context, c cid.Cid, sel ipld.Node) ([]ipld.Node, cid.Cid, int, error) { - ctx, span := internal.StartSpan(ctx, "basicResolver.resolveNodes", trace.WithAttributes(attribute.Stringer("CID", c))) + ctx, span := startSpan(ctx, "basicResolver.resolveNodes", trace.WithAttributes(attribute.Stringer("CID", c))) defer span.End() session := r.FetcherFactory.NewSession(ctx) @@ -308,3 +242,7 @@ func pathSelector(path []string, ssb builder.SelectorSpecBuilder, reduce func(st } return spec.Node() } + +func startSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { + return otel.Tracer("boxo/path/resolver").Start(ctx, fmt.Sprintf("Path.%s", name), opts...) +} diff --git a/path/resolver/resolver_test.go b/path/resolver/resolver_test.go index c20f9306d..91f05e7d6 100644 --- a/path/resolver/resolver_test.go +++ b/path/resolver/resolver_test.go @@ -3,7 +3,6 @@ package resolver_test import ( "bytes" "context" - "fmt" "math/rand" "strings" "testing" @@ -21,12 +20,11 @@ import ( merkledag "github.com/ipfs/boxo/ipld/merkledag" dagmock "github.com/ipfs/boxo/ipld/merkledag/test" - path "github.com/ipfs/boxo/path" + "github.com/ipfs/boxo/path" "github.com/ipfs/boxo/path/resolver" "github.com/ipfs/go-unixfsnode" dagcbor "github.com/ipld/go-ipld-prime/codec/dagcbor" dagjson "github.com/ipld/go-ipld-prime/codec/dagjson" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -38,7 +36,7 @@ func randNode() *merkledag.ProtoNode { return node } -func TestRecurivePathResolution(t *testing.T) { +func TestRecursivePathResolution(t *testing.T) { ctx := context.Background() bsrv := dagmock.Bserv() @@ -47,29 +45,21 @@ func TestRecurivePathResolution(t *testing.T) { c := randNode() err := b.AddNodeLink("grandchild", c) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) err = a.AddNodeLink("child", b) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) for _, n := range []*merkledag.ProtoNode{a, b, c} { err = bsrv.AddBlock(ctx, n) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) } - aKey := a.Cid() + p, err := path.Join(path.FromCid(a.Cid()), "child", "grandchild") + require.NoError(t, err) - segments := []string{aKey.String(), "child", "grandchild"} - p, err := path.FromSegments("/ipfs/", segments...) - if err != nil { - t.Fatal(err) - } + imPath, err := path.NewImmutablePath(p) + require.NoError(t, err) fetcherFactory := bsfetcher.NewFetcherConfig(bsrv) fetcherFactory.NodeReifier = unixfsnode.Reify @@ -81,56 +71,26 @@ func TestRecurivePathResolution(t *testing.T) { }) resolver := resolver.NewBasicResolver(fetcherFactory) - node, lnk, err := resolver.ResolvePath(ctx, p) - if err != nil { - t.Fatal(err) - } + node, lnk, err := resolver.ResolvePath(ctx, imPath) + require.NoError(t, err) uNode, ok := node.(unixfsnode.PathedPBNode) require.True(t, ok) fd := uNode.FieldData() byts, err := fd.Must().AsBytes() require.NoError(t, err) + require.Equal(t, cidlink.Link{Cid: c.Cid()}, lnk) + require.Equal(t, c.Data(), byts) - assert.Equal(t, cidlink.Link{Cid: c.Cid()}, lnk) - - assert.Equal(t, c.Data(), byts) - cKey := c.Cid() - - rCid, rest, err := resolver.ResolveToLastNode(ctx, p) - if err != nil { - t.Fatal(err) - } - - if len(rest) != 0 { - t.Error("expected rest to be empty") - } - - if rCid.String() != cKey.String() { - t.Fatal(fmt.Errorf( - "ResolveToLastNode failed for %s: %s != %s", - p.String(), rCid.String(), cKey.String())) - } - - p2, err := path.FromSegments("/ipfs/", aKey.String()) - if err != nil { - t.Fatal(err) - } - - rCid, rest, err = resolver.ResolveToLastNode(ctx, p2) - if err != nil { - t.Fatal(err) - } - - if len(rest) != 0 { - t.Error("expected rest to be empty") - } + rCid, remainder, err := resolver.ResolveToLastNode(ctx, imPath) + require.NoError(t, err) + require.Empty(t, remainder) + require.Equal(t, c.Cid().String(), rCid.String()) - if rCid.String() != aKey.String() { - t.Fatal(fmt.Errorf( - "ResolveToLastNode failed for %s: %s != %s", - p.String(), rCid.String(), cKey.String())) - } + rCid, remainder, err = resolver.ResolveToLastNode(ctx, path.FromCid(a.Cid())) + require.NoError(t, err) + require.Empty(t, remainder) + require.Equal(t, a.Cid().String(), rCid.String()) } func TestResolveToLastNode_ErrNoLink(t *testing.T) { @@ -142,24 +102,16 @@ func TestResolveToLastNode_ErrNoLink(t *testing.T) { c := randNode() err := b.AddNodeLink("grandchild", c) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) err = a.AddNodeLink("child", b) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) for _, n := range []*merkledag.ProtoNode{a, b, c} { err = bsrv.AddBlock(ctx, n) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) } - aKey := a.Cid() - fetcherFactory := bsfetcher.NewFetcherConfig(bsrv) fetcherFactory.PrototypeChooser = dagpb.AddSupportToChooser(func(lnk ipld.Link, lnkCtx ipld.LinkContext) (ipld.NodePrototype, error) { if tlnkNd, ok := lnkCtx.LinkNode.(schema.TypedLinkNode); ok { @@ -171,21 +123,27 @@ func TestResolveToLastNode_ErrNoLink(t *testing.T) { r := resolver.NewBasicResolver(fetcherFactory) // test missing link intermediate segment - segments := []string{aKey.String(), "cheese", "time"} - p, err := path.FromSegments("/ipfs/", segments...) + p, err := path.Join(path.FromCid(a.Cid()), "cheese", "time") require.NoError(t, err) - _, _, err = r.ResolveToLastNode(ctx, p) - require.EqualError(t, err, resolver.ErrNoLink{Name: "cheese", Node: aKey}.Error()) + imPath, err := path.NewImmutablePath(p) + require.NoError(t, err) + + _, _, err = r.ResolveToLastNode(ctx, imPath) + require.ErrorIs(t, err, &resolver.ErrNoLink{}) + require.Equal(t, "cheese", err.(*resolver.ErrNoLink).Name) + require.Equal(t, a.Cid(), err.(*resolver.ErrNoLink).Node) // test missing link at end - bKey := b.Cid() - segments = []string{aKey.String(), "child", "apples"} - p, err = path.FromSegments("/ipfs/", segments...) + p, err = path.Join(path.FromCid(a.Cid()), "child", "apples") + require.NoError(t, err) + + imPath, err = path.NewImmutablePath(p) require.NoError(t, err) - _, _, err = r.ResolveToLastNode(ctx, p) - require.EqualError(t, err, resolver.ErrNoLink{Name: "apples", Node: bKey}.Error()) + _, _, err = r.ResolveToLastNode(ctx, imPath) + require.Equal(t, "apples", err.(*resolver.ErrNoLink).Name) + require.Equal(t, b.Cid(), err.(*resolver.ErrNoLink).Node) } func TestResolveToLastNode_NoUnnecessaryFetching(t *testing.T) { @@ -201,10 +159,10 @@ func TestResolveToLastNode_NoUnnecessaryFetching(t *testing.T) { err = bsrv.AddBlock(ctx, a) require.NoError(t, err) - aKey := a.Cid() + p, err := path.Join(path.FromCid(a.Cid()), "child") + require.NoError(t, err) - segments := []string{aKey.String(), "child"} - p, err := path.FromSegments("/ipfs/", segments...) + imPath, err := path.NewImmutablePath(p) require.NoError(t, err) fetcherFactory := bsfetcher.NewFetcherConfig(bsrv) @@ -217,7 +175,7 @@ func TestResolveToLastNode_NoUnnecessaryFetching(t *testing.T) { fetcherFactory.NodeReifier = unixfsnode.Reify resolver := resolver.NewBasicResolver(fetcherFactory) - resolvedCID, remainingPath, err := resolver.ResolveToLastNode(ctx, p) + resolvedCID, remainingPath, err := resolver.ResolveToLastNode(ctx, imPath) require.NoError(t, err) require.Equal(t, len(remainingPath), 0, "cannot have remaining path") @@ -232,9 +190,11 @@ func TestPathRemainder(t *testing.T) { nb := basicnode.Prototype.Any.NewBuilder() err := dagjson.Decode(nb, strings.NewReader(`{"foo": {"bar": "baz"}}`)) require.NoError(t, err) + out := new(bytes.Buffer) err = dagcbor.Encode(nb.Build(), out) require.NoError(t, err) + lnk, err := cid.Prefix{ Version: 1, Codec: cid.DagCBOR, @@ -242,37 +202,46 @@ func TestPathRemainder(t *testing.T) { MhLength: 32, }.Sum(out.Bytes()) require.NoError(t, err) + blk, err := blocks.NewBlockWithCid(out.Bytes(), lnk) require.NoError(t, err) + bsrv.AddBlock(ctx, blk) fetcherFactory := bsfetcher.NewFetcherConfig(bsrv) resolver := resolver.NewBasicResolver(fetcherFactory) - rp1, remainder, err := resolver.ResolveToLastNode(ctx, path.FromString(lnk.String()+"/foo/bar")) + p, err := path.Join(path.FromCid(lnk), "foo", "bar") require.NoError(t, err) - assert.Equal(t, lnk, rp1) - require.Equal(t, "foo/bar", path.Join(remainder)) + imPath, err := path.NewImmutablePath(p) + require.NoError(t, err) + + rp, remainder, err := resolver.ResolveToLastNode(ctx, imPath) + require.NoError(t, err) + + require.Equal(t, lnk, rp) + require.Equal(t, "foo/bar", strings.Join(remainder, "/")) } func TestResolveToLastNode_MixedSegmentTypes(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + bsrv := dagmock.Bserv() a := randNode() err := bsrv.AddBlock(ctx, a) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) nb := basicnode.Prototype.Any.NewBuilder() json := `{"foo":{"bar":[0,{"boom":["baz",1,2,{"/":"CID"},"blop"]}]}}` json = strings.ReplaceAll(json, "CID", a.Cid().String()) err = dagjson.Decode(nb, strings.NewReader(json)) require.NoError(t, err) + out := new(bytes.Buffer) err = dagcbor.Encode(nb.Build(), out) require.NoError(t, err) + lnk, err := cid.Prefix{ Version: 1, Codec: cid.DagCBOR, @@ -280,15 +249,22 @@ func TestResolveToLastNode_MixedSegmentTypes(t *testing.T) { MhLength: 32, }.Sum(out.Bytes()) require.NoError(t, err) + blk, err := blocks.NewBlockWithCid(out.Bytes(), lnk) require.NoError(t, err) + bsrv.AddBlock(ctx, blk) fetcherFactory := bsfetcher.NewFetcherConfig(bsrv) resolver := resolver.NewBasicResolver(fetcherFactory) - cid, remainder, err := resolver.ResolveToLastNode(ctx, path.FromString(lnk.String()+"/foo/bar/1/boom/3")) + newPath, err := path.Join(path.FromCid(lnk), "foo", "bar", "1", "boom", "3") require.NoError(t, err) - assert.Equal(t, 0, len(remainder)) - assert.True(t, cid.Equals(a.Cid())) + imPath, err := path.NewImmutablePath(newPath) + require.NoError(t, err) + + cid, remainder, err := resolver.ResolveToLastNode(ctx, imPath) + require.NoError(t, err) + require.Equal(t, 0, len(remainder)) + require.True(t, cid.Equals(a.Cid())) } diff --git a/routing/http/client/client_test.go b/routing/http/client/client_test.go index 3822862dd..b6aa8456b 100644 --- a/routing/http/client/client_test.go +++ b/routing/http/client/client_test.go @@ -12,9 +12,8 @@ import ( "time" "github.com/benbjohnson/clock" - "github.com/ipfs/boxo/coreiface/path" ipns "github.com/ipfs/boxo/ipns" - ipfspath "github.com/ipfs/boxo/path" + "github.com/ipfs/boxo/path" "github.com/ipfs/boxo/routing/http/server" "github.com/ipfs/boxo/routing/http/types" "github.com/ipfs/boxo/routing/http/types/iter" @@ -627,11 +626,11 @@ func makeIPNSRecord(t *testing.T, sk crypto.PrivKey, opts ...ipns.Option) (*ipns cid, err := cid.Decode("bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4") require.NoError(t, err) - path := path.IpfsPath(cid) + path := path.FromCid(cid) eol := time.Now().Add(time.Hour * 48) ttl := time.Second * 20 - record, err := ipns.NewRecord(sk, ipfspath.FromString(path.String()), 1, eol, ttl, opts...) + record, err := ipns.NewRecord(sk, path, 1, eol, ttl, opts...) require.NoError(t, err) rawRecord, err := ipns.MarshalRecord(record) diff --git a/routing/http/contentrouter/contentrouter_test.go b/routing/http/contentrouter/contentrouter_test.go index 83a086997..2147dc975 100644 --- a/routing/http/contentrouter/contentrouter_test.go +++ b/routing/http/contentrouter/contentrouter_test.go @@ -6,9 +6,8 @@ import ( "testing" "time" - "github.com/ipfs/boxo/coreiface/path" "github.com/ipfs/boxo/ipns" - ipfspath "github.com/ipfs/boxo/path" + "github.com/ipfs/boxo/path" "github.com/ipfs/boxo/routing/http/types" "github.com/ipfs/boxo/routing/http/types/iter" "github.com/ipfs/go-cid" @@ -217,11 +216,11 @@ func makeIPNSRecord(t *testing.T, sk crypto.PrivKey, opts ...ipns.Option) (*ipns cid, err := cid.Decode("bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4") require.NoError(t, err) - path := path.IpfsPath(cid) + path := path.FromCid(cid) eol := time.Now().Add(time.Hour * 48) ttl := time.Second * 20 - record, err := ipns.NewRecord(sk, ipfspath.FromString(path.String()), 1, eol, ttl, opts...) + record, err := ipns.NewRecord(sk, path, 1, eol, ttl, opts...) require.NoError(t, err) rawRecord, err := ipns.MarshalRecord(record) diff --git a/routing/http/server/server_test.go b/routing/http/server/server_test.go index f6d4a3dba..c2c752057 100644 --- a/routing/http/server/server_test.go +++ b/routing/http/server/server_test.go @@ -10,9 +10,8 @@ import ( "testing" "time" - "github.com/ipfs/boxo/coreiface/path" "github.com/ipfs/boxo/ipns" - ipfspath "github.com/ipfs/boxo/path" + "github.com/ipfs/boxo/path" "github.com/ipfs/boxo/routing/http/types" "github.com/ipfs/boxo/routing/http/types/iter" "github.com/ipfs/go-cid" @@ -242,11 +241,11 @@ func makeName(t *testing.T) (crypto.PrivKey, ipns.Name) { } func makeIPNSRecord(t *testing.T, cid cid.Cid, sk crypto.PrivKey, opts ...ipns.Option) (*ipns.Record, []byte) { - path := path.IpfsPath(cid) + path := path.FromCid(cid) eol := time.Now().Add(time.Hour * 48) ttl := time.Second * 20 - record, err := ipns.NewRecord(sk, ipfspath.FromString(path.String()), 1, eol, ttl, opts...) + record, err := ipns.NewRecord(sk, path, 1, eol, ttl, opts...) require.NoError(t, err) rawRecord, err := ipns.MarshalRecord(record)