diff --git a/coreiface/block.go b/coreiface/block.go index dbe31e9f8..6e7ba9476 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.ResolvedPath } // BlockAPI specifies the interface to the block layer diff --git a/coreiface/coreapi.go b/coreiface/coreapi.go index 7276a3f60..aa1550fdf 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" ) @@ -48,7 +47,7 @@ type CoreAPI interface { Routing() RoutingAPI // ResolvePath resolves the path using Unixfs resolver - ResolvePath(context.Context, path.Path) (path.Resolved, error) + ResolvePath(context.Context, path.Path) (path.ResolvedPath, 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..623ebe51b 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.ResolvedPath // After holds the link path after the change. Note that when a link is // removed, this will be nil. - After path.Resolved + After path.ResolvedPath } // 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.ResolvedPath, 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.ResolvedPath, 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.ResolvedPath, 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.ResolvedPath, 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.ResolvedPath, 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..edc41fd28 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.ResolvedPath // 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.ResolvedPath // 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..4647165d1 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" ) @@ -219,7 +218,7 @@ 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.NewIPFSPath(res.Path().Cid()) rp, err := api.ResolvePath(ctx, p) if err != nil { diff --git a/coreiface/tests/dag.go b/coreiface/tests/dag.go index ba74031f9..686b5d40c 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" @@ -115,7 +113,10 @@ func (tp *TestSuite) TestDagPath(t *testing.T) { t.Fatal(err) } - p := path.New(gopath.Join(nd.Cid().String(), "lnk")) + p, err := path.Join(path.NewIPFSPath(nd.Cid()), "lnk") + if err != nil { + t.Fatal(err) + } rp, err := api.ResolvePath(ctx, p) if err != nil { diff --git a/coreiface/tests/name.go b/coreiface/tests/name.go index ab55d0425..b0e02fe9e 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) @@ -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) diff --git a/coreiface/tests/path.go b/coreiface/tests/path.go index 06f3aa1f8..32e3b33f1 100644 --- a/coreiface/tests/path.go +++ b/coreiface/tests/path.go @@ -6,11 +6,10 @@ import ( "strings" "testing" - "github.com/ipfs/boxo/coreiface/path" - "github.com/ipfs/boxo/coreiface/options" - + "github.com/ipfs/boxo/path" ipldcbor "github.com/ipfs/go-ipld-cbor" + "github.com/stretchr/testify/require" ) func (tp *TestSuite) TestPath(t *testing.T) { @@ -76,7 +75,12 @@ func (tp *TestSuite) TestPathRemainder(t *testing.T) { t.Fatal(err) } - rp1, err := api.ResolvePath(ctx, path.New(nd.String()+"/foo/bar")) + p, err := path.NewPath(nd.String() + "/foo/bar") + if err != nil { + t.Fatal(err) + } + + rp1, err := api.ResolvePath(ctx, p) if err != nil { t.Fatal(err) } @@ -107,7 +111,7 @@ func (tp *TestSuite) TestEmptyPathRemainder(t *testing.T) { t.Fatal(err) } - rp1, err := api.ResolvePath(ctx, path.New(nd.Cid().String())) + rp1, err := api.ResolvePath(ctx, path.NewIPFSPath(nd.Cid())) if err != nil { t.Fatal(err) } @@ -138,7 +142,12 @@ func (tp *TestSuite) TestInvalidPathRemainder(t *testing.T) { t.Fatal(err) } - _, err = api.ResolvePath(ctx, path.New("/ipld/"+nd.Cid().String()+"/bar/baz")) + p, err := path.Join(path.NewIPLDPath(nd.Cid()), "/bar/baz") + if err != nil { + t.Fatal(err) + } + + _, err = api.ResolvePath(ctx, p) if err == nil || !strings.Contains(err.Error(), `no link named "bar"`) { t.Fatalf("unexpected error: %s", err) } @@ -174,7 +183,12 @@ func (tp *TestSuite) TestPathRoot(t *testing.T) { t.Fatal(err) } - rp, err := api.ResolvePath(ctx, path.New("/ipld/"+nd.Cid().String()+"/foo")) + p, err := path.Join(path.NewIPLDPath(nd.Cid()), "/foo") + if err != nil { + t.Fatal(err) + } + + rp, err := api.ResolvePath(ctx, p) if err != nil { t.Fatal(err) } @@ -189,9 +203,11 @@ func (tp *TestSuite) TestPathRoot(t *testing.T) { } 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) - if path.Join(p1, "foo").String() != "/ipfs/QmYNmQKp6SuaVrpgWRsPTgCQCnpxUYGq76YEKBXuj2N4H6/bar/baz/foo" { - t.Error("unexpected path") - } + p2, err := path.Join(p1, "foo") + require.NoError(t, err) + + require.Equal(t, "/ipfs/QmYNmQKp6SuaVrpgWRsPTgCQCnpxUYGq76YEKBXuj2N4H6/bar/baz/foo", p2.String()) } diff --git a/coreiface/tests/pin.go b/coreiface/tests/pin.go index 4b0fea01d..8b6e8a15c 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" @@ -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.NewIPLDPath(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.NewIPLDPath(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.NewIPLDPath(nd3.Cid()).String() { + t.Errorf("unexpected path, %s != %s", list[0].Path().String(), path.NewIPFSPath(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.NewIPLDPath(nd2.Cid()).String() { + t.Errorf("unexpected path, %s != %s", list[0].Path().String(), path.NewIPLDPath(nd2.Cid()).String()) } list, err = accPins(api.Pin().Ls(ctx, opt.Pin.Ls.Indirect())) @@ -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.NewIPLDPath(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.NewIPLDPath(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.NewIPLDPath(grandparent.Cid())) if err != nil { t.Fatal(err) } - err = api.Pin().Add(ctx, path.IpldPath(parent.Cid())) + err = api.Pin().Add(ctx, path.NewIPLDPath(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.NewIPLDPath(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.NewIPLDPath(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.NewIPLDPath(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.NewIPLDPath(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.NewIPLDPath(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.NewIPLDPath(grandparent.Cid())) if err != nil { t.Fatal(err) } @@ -376,27 +375,27 @@ 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, path.NewIPLDPath(grandparent.Cid())) + assertNotPinned(t, ctx, api, path.NewIPLDPath(parent.Cid())) + assertNotPinned(t, ctx, api, path.NewIPLDPath(leaf.Cid())) - err = api.Pin().Add(ctx, path.IpldPath(parent.Cid()), opt.Pin.Recursive(true)) + err = api.Pin().Add(ctx, path.NewIPLDPath(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, path.NewIPLDPath(grandparent.Cid())) + assertIsPinned(t, ctx, api, path.NewIPLDPath(parent.Cid()), "recursive") + assertIsPinned(t, ctx, api, path.NewIPLDPath(leaf.Cid()), "indirect") - err = api.Pin().Add(ctx, path.IpldPath(grandparent.Cid()), opt.Pin.Recursive(false)) + err = api.Pin().Add(ctx, path.NewIPLDPath(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, path.NewIPLDPath(grandparent.Cid()), "direct") + assertIsPinned(t, ctx, api, path.NewIPLDPath(parent.Cid()), "recursive") + assertIsPinned(t, ctx, api, path.NewIPLDPath(leaf.Cid()), "indirect") } type cidContainer interface { 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 2842b47bc..f1d9fcab8 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" @@ -104,12 +103,12 @@ func (tp *TestSuite) TestAdd(t *testing.T) { t.Fatal(err) } - p := func(h string) path.Resolved { + p := func(h string) path.ResolvedPath { c, err := cid.Parse(h) if err != nil { t.Fatal(err) } - return path.IpfsPath(c) + return path.NewIPFSPath(c) } rf, err := os.CreateTemp(os.TempDir(), "unixfs-add-real") @@ -595,7 +594,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 { @@ -624,18 +626,18 @@ func (tp *TestSuite) TestGetDir(t *testing.T) { if err != nil { t.Fatal(err) } - p := path.IpfsPath(edir.Cid()) + p := path.NewIPFSPath(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.NewIPFSPath(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.NewIPFSPath(emptyDir.Cid())) if err != nil { t.Fatal(err) } @@ -659,7 +661,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.NewIPFSPath(nd.Cid())) if !strings.Contains(err.Error(), "proto: required field") { t.Fatalf("expected protobuf error, got: %s", err) } @@ -785,7 +787,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.NewIPFSPath(emptyDir.Cid())) if err != nil { t.Fatal(err) } @@ -814,7 +816,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.NewIPFSPath(nd.Cid())) if err != nil { t.Fatal(err) } diff --git a/coreiface/unixfs.go b/coreiface/unixfs.go index 606bc8e78..69bbc6c61 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.ResolvedPath `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.ResolvedPath, error) // Get returns a read-only handle to a file tree referenced by a path // diff --git a/gateway/assets/assets.go b/gateway/assets/assets.go index f411a47e0..accb00dd4 100644 --- a/gateway/assets/assets.go +++ b/gateway/assets/assets.go @@ -11,8 +11,7 @@ import ( "strings" "github.com/cespare/xxhash/v2" - - ipfspath "github.com/ipfs/boxo/path" + "github.com/ipfs/boxo/path" ) //go:embed *.html *.css @@ -133,7 +132,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 01ca49fec..254389b03 100644 --- a/gateway/blocks_backend.go +++ b/gateway/blocks_backend.go @@ -13,7 +13,6 @@ import ( "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 +21,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 +146,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 @@ -188,7 +187,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 @@ -202,7 +201,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 @@ -211,7 +210,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, files.Node, error) { +func (bb *BlocksBackend) Head(ctx context.Context, path path.ImmutablePath) (ContentPathMetadata, files.Node, error) { md, nd, err := bb.getNode(ctx, path) if err != nil { return md, nil, err @@ -232,14 +231,13 @@ func (bb *BlocksBackend) Head(ctx context.Context, path ImmutablePath) (ContentP return md, fileNode, nil } -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 { return ContentPathMetadata{}, nil, err } - contentPathStr := p.String() - if !strings.HasPrefix(contentPathStr, "/ipfs/") { + if p.Namespace() != "ipfs" { return ContentPathMetadata{}, nil, fmt.Errorf("path does not have /ipfs/ prefix") } @@ -274,7 +272,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) @@ -284,7 +282,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.Path, 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 { @@ -452,7 +450,7 @@ func walkGatewaySimpleSelector(ctx context.Context, p ipfspath.Path, params CarP } } -func (bb *BlocksBackend) getNode(ctx context.Context, path ImmutablePath) (ContentPathMetadata, format.Node, error) { +func (bb *BlocksBackend) getNode(ctx context.Context, path path.ImmutablePath) (ContentPathMetadata, format.Node, error) { roots, lastSeg, err := bb.getPathRoots(ctx, path) if err != nil { return ContentPathMetadata{}, nil, err @@ -473,7 +471,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.ResolvedPath, 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. @@ -497,14 +495,18 @@ 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.ResolvedPath 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, err + } + resolvedSubPath, 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 @@ -521,32 +523,18 @@ func (bb *BlocksBackend) getPathRoots(ctx context.Context, contentPath Immutable return pathRoots, lastPath, 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] { +func (bb *BlocksBackend) ResolveMutable(ctx context.Context, p path.Path) (path.ImmutablePath, error) { + switch p.Namespace() { case "ipns": - ipath, err = resolve.ResolveIPNS(ctx, bb.namesys, ipath) - if err != nil { - return ImmutablePath{}, err - } - imPath, err := NewImmutablePath(ifacepath.New(ipath.String())) + p, err := resolve.ResolveIPNS(ctx, bb.namesys, p) if err != nil { - return ImmutablePath{}, err + return path.ImmutablePath{}, err } - return imPath, nil + return path.NewImmutablePath(p) case "ipfs": - imPath, err := NewImmutablePath(ifacepath.New(ipath.String())) - if err != nil { - return ImmutablePath{}, err - } - return imPath, nil + return path.NewImmutablePath(p) default: - return ImmutablePath{}, NewErrorStatusCode(fmt.Errorf("unsupported path namespace: %s", p.Namespace()), http.StatusNotImplemented) + return path.ImmutablePath{}, NewErrorStatusCode(fmt.Errorf("unsupported path namespace: %s", p.Namespace()), http.StatusNotImplemented) } } @@ -570,19 +558,19 @@ 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 { +func (bb *BlocksBackend) IsCached(ctx context.Context, p path.Path) bool { rp, err := bb.resolvePath(ctx, p) if err != nil { return false @@ -592,7 +580,7 @@ func (bb *BlocksBackend) IsCached(ctx context.Context, p ifacepath.Path) bool { return has } -func (bb *BlocksBackend) ResolvePath(ctx context.Context, path ImmutablePath) (ContentPathMetadata, error) { +func (bb *BlocksBackend) ResolvePath(ctx context.Context, path path.ImmutablePath) (ContentPathMetadata, error) { roots, lastSeg, err := bb.getPathRoots(ctx, path) if err != nil { return ContentPathMetadata{}, err @@ -604,39 +592,29 @@ func (bb *BlocksBackend) ResolvePath(ctx context.Context, path ImmutablePath) (C 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 - } - - err := p.IsValid() - if err != nil { - return nil, err +func (bb *BlocksBackend) resolvePath(ctx context.Context, p path.Path) (path.ResolvedPath, error) { + if _, ok := p.(path.ResolvedPath); ok { + return p.(path.ResolvedPath), nil } - ipath := ipfspath.Path(p.String()) - if ipath.Segments()[0] == "ipns" { - ipath, err = resolve.ResolveIPNS(ctx, bb.namesys, ipath) + var err error + if p.Namespace() == "ipns" { + p, err = resolve.ResolveIPNS(ctx, bb.namesys, p) if err != nil { return nil, err } } - if ipath.Segments()[0] != "ipfs" { + if p.Namespace() != "ipfs" { return nil, fmt.Errorf("unsupported path namespace: %s", p.Namespace()) } - node, rest, err := bb.resolver.ResolveToLastNode(ctx, ipath) - if err != nil { - return nil, err - } - - root, err := cid.Parse(ipath.Segments()[1]) + node, rest, err := bb.resolver.ResolveToLastNode(ctx, p) if err != nil { return nil, err } - return ifacepath.NewResolvedPath(ipath, node, root, gopath.Join(rest...)), nil + return path.NewResolvedPath(p, node, gopath.Join(rest...)), nil } type nodeGetterToCarExporer struct { diff --git a/gateway/gateway.go b/gateway/gateway.go index 780691a45..305fd2bb8 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" ) @@ -88,38 +88,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 @@ -237,7 +205,7 @@ func (d DuplicateBlocksPolicy) String() string { type ContentPathMetadata struct { PathSegmentRoots []cid.Cid - LastSegment path.Resolved + LastSegment path.ResolvedPath ContentType string // Only used for UnixFS requests } @@ -302,30 +270,30 @@ type IPFSBackend interface { // - A range request for a directory currently holds no semantic meaning. // // [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 file or directory depending on what the path is that has been requested. // For UnixFS files should return a file which has the correct file size and either returns the ContentType in ContentPathMetadata or // enough data (e.g. 3kiB) such that the content type can be determined by sniffing. // For all other data types returning just size information is sufficient // TODO: give function more explicit return types - Head(context.Context, ImmutablePath) (ContentPathMetadata, files.Node, error) + Head(context.Context, path.ImmutablePath) (ContentPathMetadata, files.Node, 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 @@ -339,7 +307,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 7fae55f3e..af994fe55 100644 --- a/gateway/gateway_test.go +++ b/gateway/gateway_test.go @@ -9,7 +9,6 @@ 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" @@ -26,14 +25,17 @@ 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.NewIPFSPath(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) + + backend.namesys["/ipns/example.com"] = path.NewIPFSPath(k.Cid()) + backend.namesys["/ipns/working.example.com"] = k + backend.namesys["/ipns/double.example.com"] = path.NewDNSLinkPath("working.example.com") + backend.namesys["/ipns/triple.example.com"] = path.NewDNSLinkPath("double.example.com") + backend.namesys["/ipns/broken.example.com"] = path.NewDNSLinkPath(k.Cid().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 +43,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 @@ -90,7 +92,7 @@ func TestPretty404(t *testing.T) { t.Logf("test server url: %s", ts.URL) host := "example.net" - backend.namesys["/ipns/"+host] = path.FromCid(root) + backend.namesys["/ipns/"+host] = path.NewIPFSPath(root) for _, test := range []struct { path string @@ -493,7 +495,7 @@ func TestRedirects(t *testing.T) { t.Parallel() ts, backend, root := newTestServerAndNode(t, nil, "ipns-hostname-redirects.car") - backend.namesys["/ipns/example.net"] = path.FromCid(root) + backend.namesys["/ipns/example.net"] = path.NewIPFSPath(root) // make request to directory containing index.html req := mustNewRequest(t, http.MethodGet, ts.URL+"/foo", nil) @@ -528,7 +530,7 @@ func TestRedirects(t *testing.T) { t.Parallel() backend, root := newMockBackend(t, "redirects-spa.car") - backend.namesys["/ipns/example.com"] = path.FromCid(root) + backend.namesys["/ipns/example.com"] = path.NewIPFSPath(root) ts := newTestServerWithConfig(t, backend, Config{ Headers: map[string][]string{}, @@ -665,8 +667,8 @@ func TestDeserializedResponses(t *testing.T) { t.Parallel() backend, root := newMockBackend(t, "fixtures.car") - backend.namesys["/ipns/trustless.com"] = path.FromCid(root) - backend.namesys["/ipns/trusted.com"] = path.FromCid(root) + backend.namesys["/ipns/trustless.com"] = path.NewIPFSPath(root) + backend.namesys["/ipns/trusted.com"] = path.NewIPFSPath(root) ts := newTestServerWithConfig(t, backend, Config{ Headers: map[string][]string{}, @@ -708,43 +710,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, files.Node, error) { +func (mb *errorMockBackend) Head(ctx context.Context, path path.ImmutablePath) (ContentPathMetadata, files.Node, 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 path.ImmutablePath{}, 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 } @@ -792,27 +794,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, files.Node, error) { +func (mb *panicMockBackend) Head(ctx context.Context, immutablePath path.ImmutablePath) (ContentPathMetadata, files.Node, 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") } @@ -820,7 +822,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. @@ -831,11 +833,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 ecf505617..1b28bb7b9 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" @@ -182,12 +182,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 @@ -196,9 +196,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. @@ -222,7 +222,12 @@ func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) { } 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) @@ -237,11 +242,6 @@ func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) { return } - if err := contentPath.IsValid(); err != nil { - i.webError(w, r, err, http.StatusBadRequest) - return - } - // Detect when explicit Accept header or ?format parameter are present responseFormat, formatParams, err := customResponseFormat(r) if err != nil { @@ -286,7 +286,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) @@ -365,7 +365,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, "/") @@ -409,7 +409,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 != "" { @@ -446,7 +446,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) @@ -464,7 +464,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. @@ -703,7 +703,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) @@ -733,7 +733,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 @@ -754,7 +754,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 } @@ -765,7 +765,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 } @@ -773,7 +773,7 @@ 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 path.ImmutablePath{}, false } // If we have origin isolation (subdomain gw, DNSLink website), @@ -795,17 +795,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 path.ImmutablePath{}, false } err = fmt.Errorf("failed to resolve %s: %w", debugStr(contentPath.String()), err) i.webError(w, r, err, http.StatusInternalServerError) - return ImmutablePath{}, false + return path.ImmutablePath{}, 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 { @@ -915,23 +915,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, contentPath path.Path) 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 @@ -956,7 +952,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_car.go b/gateway/handler_car.go index 553519988..4bd6e9b94 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" @@ -204,7 +205,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") @@ -225,7 +226,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..f4f566e2d 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.NewIPFSPath(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 007a52fda..fce308049 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" @@ -149,7 +149,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.ReadSeekCloser, 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.ReadSeekCloser, resolvedPath path.ResolvedPath, 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) @@ -229,7 +229,7 @@ func parseNode(blockCid cid.Cid, blockData io.ReadSeekCloser) *assets.ParsedNode } // serveCodecRaw returns the raw block without any conversion -func (i *handler) serveCodecRaw(ctx context.Context, w http.ResponseWriter, r *http.Request, blockData io.ReadSeekCloser, contentPath ipath.Path, name string, modtime, begin time.Time) bool { +func (i *handler) serveCodecRaw(ctx context.Context, w http.ResponseWriter, r *http.Request, blockData io.ReadSeekCloser, contentPath path.Path, name string, modtime, begin time.Time) bool { // ServeContent will take care of // If-None-Match+Etag, Content-Length and range requests _, dataSent, _ := serveContent(w, r, name, modtime, blockData) @@ -243,7 +243,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.ReadSeekCloser, 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.ReadSeekCloser, contentPath path.Path, toCodec mc.Code, modtime, begin time.Time) bool { codec := blockCid.Prefix().Codec decoder, err := multicodec.LookupDecoder(codec) if err != nil { @@ -288,7 +288,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.ResolvedPath, contentType string) string { var dispType, name string ext, ok := contentTypeToExtension[contentType] diff --git a/gateway/handler_codec_test.go b/gateway/handler_codec_test.go index c79b07689..f1af5ea2b 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,7 +31,10 @@ 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.NewIPFSPath(root), "subdir", "dag-cbor-document") + require.NoError(t, err) + + resolvedPath, err := backend.resolvePathNoRootsReturned(ctx, p) require.NoError(t, err) cidStr := resolvedPath.Cid().String() diff --git a/gateway/handler_unixfs__redirects.go b/gateway/handler_unixfs__redirects.go index a96b87d36..a8fbee723 100644 --- a/gateway/handler_unixfs__redirects.go +++ b/gateway/handler_unixfs__redirects.go @@ -10,8 +10,8 @@ import ( "go.uber.org/zap" - ipath "github.com/ipfs/boxo/coreiface/path" "github.com/ipfs/boxo/files" + "github.com/ipfs/boxo/path" redirects "github.com/ipfs/go-ipfs-redirects-file" ) @@ -38,23 +38,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 path.ImmutablePath{}, 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 path.ImmutablePath{}, 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 path.ImmutablePath{}, 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 path.ImmutablePath{}, false, true } if foundRedirect { @@ -62,22 +74,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 path.ImmutablePath{}, false, true } if redirected { - return ImmutablePath{}, false, true + return path.ImmutablePath{}, 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 ImmutablePath{}, false, true + return path.ImmutablePath{}, 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 path.ImmutablePath{}, false, true } return imPath, true, true } @@ -87,7 +104,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 { @@ -115,7 +132,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 } @@ -129,7 +151,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 } @@ -149,7 +175,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. @@ -176,12 +202,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 @@ -223,7 +249,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, ctype, err := i.searchUpTreeFor404(r, imPath) if err != nil { return false @@ -243,7 +269,7 @@ func (i *handler) serveLegacy404IfPresent(w http.ResponseWriter, r *http.Request return err == nil } -func (i *handler) searchUpTreeFor404(r *http.Request, imPath ImmutablePath) (files.File, string, error) { +func (i *handler) searchUpTreeFor404(r *http.Request, imPath path.ImmutablePath) (files.File, string, error) { filename404, ctype, err := preferred404Filename(r.Header.Values("Accept")) if err != nil { return nil, "", err @@ -253,11 +279,11 @@ func (i *handler) searchUpTreeFor404(r *http.Request, imPath ImmutablePath) (fil 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 2808cfdc4..f20b60db5 100644 --- a/gateway/handler_unixfs_dir.go +++ b/gateway/handler_unixfs_dir.go @@ -10,10 +10,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" @@ -23,7 +22,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.ResolvedPath, 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() @@ -59,8 +58,19 @@ func (i *handler) serveDirectory(ctx context.Context, w http.ResponseWriter, r * } // Check if directory has index.html, if so, serveFile - idxPath := ipath.Join(contentPath, "index.html") - imIndexPath, err := NewImmutablePath(ipath.Join(resolvedPath, "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 + } + + imIndexPath, err := path.NewImmutablePath(indexPath) if err != nil { i.webError(w, r, err, http.StatusInternalServerError) return false @@ -148,7 +158,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 diff --git a/gateway/handler_unixfs_dir_test.go b/gateway/handler_unixfs_dir_test.go index a8ce04778..90a4e169f 100644 --- a/gateway/handler_unixfs_dir_test.go +++ b/gateway/handler_unixfs_dir_test.go @@ -6,7 +6,6 @@ import ( "net/http" "testing" - ipath "github.com/ipfs/boxo/coreiface/path" path "github.com/ipfs/boxo/path" "github.com/stretchr/testify/require" ) @@ -19,13 +18,17 @@ 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.NewIPFSPath(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.NewIPFSPath(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) + backend.namesys["/ipns/example.net"] = path.NewIPFSPath(root) // make request to directory listing req := mustNewRequest(t, http.MethodGet, ts.URL+"/foo%3F%20%23%3C%27/", nil) diff --git a/gateway/handler_unixfs_file.go b/gateway/handler_unixfs_file.go index cd924e5aa..d2ffffaa2 100644 --- a/gateway/handler_unixfs_file.go +++ b/gateway/handler_unixfs_file.go @@ -11,15 +11,15 @@ import ( "time" "github.com/gabriel-vasile/mimetype" - ipath "github.com/ipfs/boxo/coreiface/path" "github.com/ipfs/boxo/files" + "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, file files.File, fileContentType string, begin time.Time) bool { +func (i *handler) serveFile(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath path.ResolvedPath, contentPath path.Path, file files.File, fileContentType string, begin time.Time) bool { _, span := spanTrace(ctx, "Handler.ServeFile", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) defer span.End() diff --git a/gateway/hostname_test.go b/gateway/hostname_test.go index a58e0d404..6599797a1 100644 --- a/gateway/hostname_test.go +++ b/gateway/hostname_test.go @@ -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.NewIPFSPath(testCID) + backend.namesys["/ipns/dnslink.too-long.f1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5o.example.com"] = path.NewIPFSPath(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 69e81425f..0597d56a0 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, files.Node, error) { +func (b *ipfsBackendWithMetrics) Head(ctx context.Context, path path.ImmutablePath) (ContentPathMetadata, files.Node, 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 27ba43a14..8ecb1c8ae 100644 --- a/gateway/utilities_test.go +++ b/gateway/utilities_test.go @@ -14,7 +14,6 @@ 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" @@ -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, files.Node, error) { +func (mb *mockBackend) Head(ctx context.Context, immutablePath path.ImmutablePath) (ContentPathMetadata, files.Node, 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.ResolvedPath, 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/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..6023318a5 100644 --- a/ipns/record_test.go +++ b/ipns/record_test.go @@ -2,12 +2,14 @@ package ipns import ( "bytes" + "strings" "testing" "time" ipns_pb "github.com/ipfs/boxo/ipns/pb" "github.com/ipfs/boxo/path" "github.com/ipfs/boxo/util" + "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime/codec/dagcbor" basicnode "github.com/ipld/go-ipld-prime/node/basic" ic "github.com/libp2p/go-libp2p/core/crypto" @@ -17,10 +19,46 @@ 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) + } +} + +type badPath string + +func (p badPath) String() string { + return string(p) +} + +func (p badPath) Namespace() string { + seg := p.Segments() + if len(seg) > 0 { + return seg[0] + } + return "" +} + +func (p badPath) Mutable() bool { + return p.Namespace() == path.IPNSNamespace +} + +func (p badPath) Root() cid.Cid { + return cid.Undef +} + +func (p badPath) Segments() []string { + str := strings.TrimSuffix(string(p), "/") + str = strings.TrimPrefix(str, "/") + return strings.Split(str, "/") +} + func mustKeyPair(t *testing.T, typ int) (ic.PrivKey, ic.PubKey, Name) { sr := util.NewTimeSeededRand() sk, pk, err := ic.GenerateKeyPairWithReader(typ, 2048, sr) @@ -195,7 +233,7 @@ 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 := badPath(string(append([]byte("/path/1"), 0x00))) seq := uint64(1) ttl := time.Hour diff --git a/ipns/validation_test.go b/ipns/validation_test.go index 126c357e7..117a717f1 100644 --- a/ipns/validation_test.go +++ b/ipns/validation_test.go @@ -5,7 +5,6 @@ import ( "testing" "time" - "github.com/ipfs/boxo/path" ic "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peerstore" "github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoremem" @@ -164,8 +163,8 @@ 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)) + rec1 := mustNewRecord(t, sk, badPath("/path/1"), 1, eol, 0, WithV1Compatibility(true)) + rec2 := mustNewRecord(t, sk, badPath("/path/2"), 2, eol, 0, WithV1Compatibility(true)) best, err := v.Select(ipnsRoutingKey, [][]byte{mustMarshal(t, rec1), mustMarshal(t, rec2)}) require.NoError(t, err) @@ -211,7 +210,7 @@ func TestValidate(t *testing.T) { // 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) + rec, err := NewRecord(sk, badPath(string(value)), 1, eol, 0) require.NoError(t, err) err = Validate(rec, pk) diff --git a/mfs/mfs_test.go b/mfs/mfs_test.go index e57404b82..16b05f36f 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/cache.go b/namesys/cache.go index 8b7f50794..25df24631 100644 --- a/namesys/cache.go +++ b/namesys/cache.go @@ -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..5b3d853e1 100644 --- a/namesys/dns.go +++ b/namesys/dns.go @@ -84,7 +84,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.NewPathFromSegments("", strings.TrimRight(p.String(), "/"), segments[1]) } return p, nil } @@ -160,7 +160,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,11 +173,11 @@ 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 } @@ -188,8 +188,8 @@ func parseEntry(txt string) (path.Path, error) { func tryParseDNSLink(txt string) (path.Path, error) { parts := strings.SplitN(txt, "=", 2) if len(parts) == 2 && parts[0] == "dnslink" { - return path.ParsePath(parts[1]) + return path.NewPath(parts[1]) } - 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 de74f1488..6f6e1eb75 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) @@ -221,7 +224,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.NewPathFromSegments("", strings.TrimRight(p.String(), "/"), segments[3]) } span.SetAttributes(attribute.Bool("CacheHit", true)) span.RecordError(err) @@ -264,7 +267,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.NewPathFromSegments("", strings.TrimRight(p.String(), "/"), 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..81d54765f 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("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("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..da586123f 100644 --- a/namesys/resolve/resolve.go +++ b/namesys/resolve/resolve.go @@ -25,31 +25,31 @@ func ResolveIPNS(ctx context.Context, nsys namesys.NameSystem, p path.Path) (pat if strings.HasPrefix(p.String(), "/ipns/") { // 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..0e771940d 100644 --- a/namesys/resolve_test.go +++ b/namesys/resolve_test.go @@ -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/path.go b/path/path.go index 6d53ade04..508b4c1d6 100644 --- a/path/path.go +++ b/path/path.go @@ -3,188 +3,301 @@ package path import ( "fmt" - "path" + gopath "path" "strings" - cid "github.com/ipfs/go-cid" + "github.com/ipfs/go-cid" + "github.com/libp2p/go-libp2p/core/peer" ) -// A Path represents an ipfs content path: -// - /path/to/file -// - /ipfs/ -// - /ipns//path/to/folder -// - etc -type Path string - -// ^^^ -// TODO: debate making this a private struct wrapped in a public interface -// would allow us to control creation, and cache segments. +const ( + IPFSNamespace = "ipfs" + IPNSNamespace = "ipns" + IPLDNamespace = "ipld" +) -// FromString safely converts a string type to a Path type. -func FromString(s string) Path { - return Path(s) +// Path is a generic path. Paths must be prefixed with a valid prefix. +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 pointed to by this path is guaranteed to not + // change. Note that resolved mutable paths can be immutable. + Mutable() bool + + // Root returns the [cid.Cid] of the root object of the path. Root can return + // [cid.Undef] for Mutable IPNS paths that use [DNSLink]. + // + // [DNSLink]: https://dnslink.dev/ + Root() cid.Cid + + // Segments returns the different elements ofd a path, which are delimited + // by a forward slash ("/"). The leading slash must be ignored, that is, no + // segment should be empty. + Segments() []string } -// FromCid safely converts a cid.Cid type to a Path type. -func FromCid(c cid.Cid) Path { - return Path("/ipfs/" + c.String()) +// ResolvedPath is a [Path] which was resolved to the last resolvable node. +type ResolvedPath interface { + Path + + // Cid returns the [cid.Cid] of the node referenced by the path. + Cid() cid.Cid + + // Remainder returns the unresolved parts of the path. + Remainder() string } -// 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, "/") +// ImmutablePath is a [Path] which is guaranteed to return "false" to [Mutable]. +type ImmutablePath struct { + Path +} - // Ignore leading slash - if len(segments[0]) == 0 { - segments = segments[1:] +func NewImmutablePath(p Path) (ImmutablePath, error) { + if p.Mutable() { + return ImmutablePath{}, fmt.Errorf("path was expected to be immutable: %s", p.String()) } - return segments + return ImmutablePath{p}, nil +} + +type path struct { + str string + root cid.Cid + namespace string +} + +func (p *path) String() string { + return p.str +} + +func (p *path) Namespace() string { + return p.namespace } -// String converts a path to string. -func (p Path) String() string { - return string(p) +func (p *path) Mutable() bool { + return p.namespace == IPNSNamespace } -// 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") +func (p *path) Root() cid.Cid { + return p.root } -// 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) { +func (p *path) Segments() []string { + // Trim slashes from beginning and end, such that we do not return empty segments. + str := strings.TrimSuffix(p.str, "/") + str = strings.TrimPrefix(str, "/") - if p.IsJustAKey() { - return p, "", nil + return strings.Split(str, "/") +} + +type resolvedPath struct { + path + cid cid.Cid + remainder string +} + +func (p *resolvedPath) Cid() cid.Cid { + return p.cid +} + +func (p *resolvedPath) Remainder() string { + return p.remainder +} + +// NewIPFSPath returns a new "/ipfs" path with the provided CID. +func NewIPFSPath(cid cid.Cid) ResolvedPath { + return &resolvedPath{ + path: path{ + str: fmt.Sprintf("/%s/%s", IPFSNamespace, cid.String()), + root: cid, + namespace: IPFSNamespace, + }, + cid: cid, + remainder: "", } +} - segs := p.Segments() - newPath, err := ParsePath("/" + strings.Join(segs[:len(segs)-1], "/")) - if err != nil { - return "", "", err +// NewIPLDPath returns a new "/ipld" path with the provided CID. +func NewIPLDPath(cid cid.Cid) ResolvedPath { + return &resolvedPath{ + path: path{ + str: fmt.Sprintf("/%s/%s", IPLDNamespace, cid.String()), + root: cid, + namespace: IPLDNamespace, + }, + cid: cid, + remainder: "", } +} - return newPath, segs[len(segs)-1], nil +// NewIPNSPath returns a new "/ipns" path with the provided CID. +func NewIPNSPath(cid cid.Cid) Path { + return &path{ + str: fmt.Sprintf("/%s/%s", IPNSNamespace, cid.String()), + root: cid, + namespace: IPNSNamespace, + } } -// FromSegments returns a path given its different segments. -func FromSegments(prefix string, seg ...string) (Path, error) { - return ParsePath(prefix + strings.Join(seg, "/")) +func NewDNSLinkPath(domain string) Path { + return &path{ + str: fmt.Sprintf("/%s/%s", IPNSNamespace, domain), + root: cid.Undef, + namespace: IPNSNamespace, + } } -// 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) +// NewPath returns a well-formed [Path]. The returned path will always be prefixed +// with a valid namespace (/ipfs, /ipld, or /ipns). The prefix will be added if not +// present in the given string. The rules are: +// +// 1. If the path has a single component (no slashes) ans it is a valid CID, +// an /ipfs path is returned. If the CID is encoded with the Libp2pKey codec, +// then a /ipns path is returned. +// 2. If the path has a valid CID root but does not have a namespace, the /ipfs +// namespace is automatically added. +// +// This function returns an error when the given string is not a valid path. +func NewPath(str string) (Path, error) { + cleaned := gopath.Clean(str) + components := strings.Split(cleaned, "/") + + if strings.HasSuffix(str, "/") { + // Do not forget to store the trailing slash! + cleaned += "/" + } + + // If there's only one component, check if it's a CID, or Peer ID. + if len(components) == 1 { + c, err := cid.Decode(components[0]) if err == nil { - return kp, nil + if c.Prefix().GetCodec() == cid.Libp2pKey { + return NewIPNSPath(c), nil + } else { + return NewIPFSPath(c), nil + } } } - // 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} + // If the path doesn't begin with a "/", we expect it to start with a CID and + // be an IPFS Path. + if components[0] != "" { + root, err := cid.Decode(components[0]) + if err != nil { + return nil, &ErrInvalidPath{error: err, path: str} } - // The case when the path starts with hash without a protocol prefix - return Path("/ipfs/" + txt), nil + + return &path{ + str: cleaned, + root: root, + namespace: IPFSNamespace, + }, nil } - if len(parts) < 3 { - return "", &ErrInvalidPath{error: fmt.Errorf("invalid ipfs path"), path: txt} + if len(components) < 3 { + return nil, &ErrInvalidPath{error: fmt.Errorf("not enough path components"), path: str} } - //TODO: make this smarter - switch parts[1] { - case "ipfs", "ipld": - if parts[2] == "" { - return "", &ErrInvalidPath{error: fmt.Errorf("not enough path components"), path: txt} + switch components[1] { + case IPFSNamespace, IPLDNamespace: + if components[2] == "" { + return nil, &ErrInvalidPath{error: fmt.Errorf("not enough path components"), path: str} } - // Validate Cid. - _, err := decodeCid(parts[2]) + + root, err := cid.Decode(components[2]) if err != nil { - return "", &ErrInvalidPath{error: fmt.Errorf("invalid CID: %w", err), path: txt} + return nil, &ErrInvalidPath{error: fmt.Errorf("invalid CID: %w", err), path: str} } - case "ipns": - if parts[2] == "" { - return "", &ErrInvalidPath{error: fmt.Errorf("not enough path components"), path: txt} + + return &path{ + str: cleaned, + root: root, + namespace: components[1], + }, nil + case IPNSNamespace: + if components[2] == "" { + return nil, &ErrInvalidPath{error: fmt.Errorf("not enough path components"), path: str} } - default: - return "", &ErrInvalidPath{error: fmt.Errorf("unknown namespace %q", parts[1]), path: txt} - } - return Path(txt), nil -} + var root cid.Cid + pid, err := peer.Decode(components[2]) + if err != nil { + // DNSLink. + root = cid.Undef + } else { + root = peer.ToCid(pid) + } -// 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} + return &path{ + str: cleaned, + root: root, + namespace: IPNSNamespace, + }, nil + default: + return nil, &ErrInvalidPath{error: fmt.Errorf("unknown namespace %q", components[1]), path: str} } +} - c, err := decodeCid(txt) - if err != nil { - return "", &ErrInvalidPath{error: err, path: txt} +// 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. +func NewPathFromSegments(segments ...string) (Path, error) { + if len(segments) > 1 { + if segments[0] == "" { + segments = segments[1:] + } } - return FromCid(c), nil + return NewPath("/" + strings.Join(segments, "/")) } -// IsValid checks if a path is a valid ipfs Path. -func (p *Path) IsValid() error { - _, err := ParsePath(p.String()) - return err +// NewResolvedPath creates a new [ResolvedPath] from an existing path, with a +// resolved CID and remainder path. This function is intended to be used only +// by resolver implementations. +func NewResolvedPath(p Path, cid cid.Cid, remainder string) ResolvedPath { + return &resolvedPath{ + path: path{ + str: p.String(), + root: p.Root(), + namespace: p.Namespace(), + }, + cid: cid, + remainder: remainder, + } } -// Join joins strings slices using / -func Join(pths []string) string { - return strings.Join(pths, "/") -} +// SplitImmutablePath cleans up and splits the given path. It extracts the first +// component, which must be a CID, and returns it separately. +func SplitImmutablePath(fpath Path) (cid.Cid, []string, error) { + // TODO: probably rewrite this and use the .Namespace and .Root. -// SplitList splits strings usings / -func SplitList(pth string) []string { - return strings.Split(pth, "/") -} - -// 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" { + if parts[0] == IPFSNamespace || parts[0] == IPLDNamespace { parts = parts[1:] } // if nothing, bail. if len(parts) == 0 { - return cid.Cid{}, nil, &ErrInvalidPath{error: fmt.Errorf("empty"), path: string(fpath)} + return cid.Undef, nil, &ErrInvalidPath{error: fmt.Errorf("empty"), path: fpath.String()} } - c, err := decodeCid(parts[0]) + c, err := cid.Decode(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 cid.Undef, nil, &ErrInvalidPath{error: fmt.Errorf("invalid CID: %w", err), path: fpath.String()} } return c, parts[1:], nil } -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) - } - return c, err +// 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...) } diff --git a/path/path_test.go b/path/path_test.go index 2b26a5678..8f880ef49 100644 --- a/path/path_test.go +++ b/path/path_test.go @@ -1,8 +1,9 @@ package path import ( - "strings" "testing" + + "github.com/stretchr/testify/assert" ) func TestPathParsing(t *testing.T) { @@ -27,14 +28,14 @@ func TestPathParsing(t *testing.T) { "/ipld/": false, "ipld/": false, "ipld/QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n": false, + "/ipns": false, + "/ipns/domain.net": true, } for p, expected := range cases { - _, err := ParsePath(p) + _, err := NewPath(p) valid := err == nil - if valid != expected { - t.Fatalf("expected %s to have valid == %t", p, expected) - } + assert.Equal(t, expected, valid, "expected %s to have valid == %t", p, expected) } } @@ -44,10 +45,8 @@ func TestNoComponents(t *testing.T) { "/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") - } + _, err := NewPath(s) + assert.ErrorContains(t, err, "not enough path components") } } @@ -57,72 +56,7 @@ func TestInvalidPaths(t *testing.T) { "/testfs", "/", } { - _, err := ParsePath(s) - if err == nil || !strings.Contains(err.Error(), "invalid ipfs path") || !strings.Contains(err.Error(), s) { - t.Error("wrong error") - } - } -} - -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 p, expected := range cases { - path, err := ParsePath(p) - if err != nil { - t.Fatalf("ParsePath failed to parse \"%s\", but should have succeeded", p) - } - result := path.IsJustAKey() - if result != expected { - t.Fatalf("expected IsJustAKey(%s) to return %v, not %v", p, expected, result) - } - } -} - -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"}, - } - - 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) - } - headStr := head.String() - if headStr != expected[0] { - t.Fatalf("expected head of PopLastSegment(%s) to return %v, not %v", p, expected[0], headStr) - } - if tail != expected[1] { - t.Fatalf("expected tail of PopLastSegment(%s) to return %v, not %v", p, expected[1], tail) - } - } -} - -func TestV0ErrorDueToLowercase(t *testing.T) { - badb58 := "/ipfs/qmbwqxbekc3p8tqskc98xmwnzrzdtrlmimpl8wbutgsmnr" - _, err := ParsePath(badb58) - if err == nil { - t.Fatal("should have failed to decode") - } - 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") + _, err := NewPath(s) + assert.ErrorContains(t, err, "not enough path components") } } diff --git a/path/resolver/resolver.go b/path/resolver/resolver.go index f666d4b79..5a32ec3b3 100644 --- a/path/resolver/resolver.go +++ b/path/resolver/resolver.go @@ -83,7 +83,7 @@ func (r *basicResolver) ResolveToLastNode(ctx context.Context, fpath path.Path) ctx, span := internal.StartSpan(ctx, "basicResolver.ResolveToLastNode", trace.WithAttributes(attribute.Stringer("Path", fpath))) defer span.End() - c, p, err := path.SplitAbsPath(fpath) + c, p, err := path.SplitImmutablePath(fpath) if err != nil { return cid.Cid{}, nil, err } @@ -152,12 +152,7 @@ func (r *basicResolver) ResolvePath(ctx context.Context, fpath path.Path) (ipld. ctx, span := internal.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) + c, p, err := path.SplitImmutablePath(fpath) if err != nil { return nil, nil, err } @@ -196,12 +191,7 @@ func (r *basicResolver) ResolvePathComponents(ctx context.Context, fpath path.Pa 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) + c, p, err := path.SplitImmutablePath(fpath) if err != nil { return nil, err } diff --git a/path/resolver/resolver_test.go b/path/resolver/resolver_test.go index c91d950f1..8137c3768 100644 --- a/path/resolver/resolver_test.go +++ b/path/resolver/resolver_test.go @@ -65,8 +65,8 @@ func TestRecurivePathResolution(t *testing.T) { aKey := a.Cid() - segments := []string{aKey.String(), "child", "grandchild"} - p, err := path.FromSegments("/ipfs/", segments...) + segments := []string{"ipfs", aKey.String(), "child", "grandchild"} + p, err := path.NewPathFromSegments(segments...) if err != nil { t.Fatal(err) } @@ -112,7 +112,7 @@ func TestRecurivePathResolution(t *testing.T) { p.String(), rCid.String(), cKey.String())) } - p2, err := path.FromSegments("/ipfs/", aKey.String()) + p2, err := path.NewPathFromSegments("/ipfs/", aKey.String()) if err != nil { t.Fatal(err) } @@ -170,8 +170,8 @@ 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...) + segments := []string{"ipfs", aKey.String(), "cheese", "time"} + p, err := path.NewPathFromSegments(segments...) require.NoError(t, err) _, _, err = r.ResolveToLastNode(ctx, p) @@ -179,8 +179,8 @@ func TestResolveToLastNode_ErrNoLink(t *testing.T) { // test missing link at end bKey := b.Cid() - segments = []string{aKey.String(), "child", "apples"} - p, err = path.FromSegments("/ipfs/", segments...) + segments = []string{"ipfs", aKey.String(), "child", "apples"} + p, err = path.NewPathFromSegments(segments...) require.NoError(t, err) _, _, err = r.ResolveToLastNode(ctx, p) @@ -202,8 +202,8 @@ func TestResolveToLastNode_NoUnnecessaryFetching(t *testing.T) { aKey := a.Cid() - segments := []string{aKey.String(), "child"} - p, err := path.FromSegments("/ipfs/", segments...) + segments := []string{"ipfs", aKey.String(), "child"} + p, err := path.NewPathFromSegments(segments...) require.NoError(t, err) fetcherFactory := bsfetcher.NewFetcherConfig(bsrv) @@ -247,11 +247,14 @@ func TestPathRemainder(t *testing.T) { fetcherFactory := bsfetcher.NewFetcherConfig(bsrv) resolver := resolver.NewBasicResolver(fetcherFactory) - rp1, remainder, err := resolver.ResolveToLastNode(ctx, path.FromString(lnk.String()+"/foo/bar")) + newPath, err := path.NewPath(lnk.String() + "/foo/bar") + require.NoError(t, err) + + rp1, remainder, err := resolver.ResolveToLastNode(ctx, newPath) require.NoError(t, err) assert.Equal(t, lnk, rp1) - require.Equal(t, "foo/bar", path.Join(remainder)) + require.Equal(t, "foo/bar", strings.Join(remainder, "/")) } func TestResolveToLastNode_MixedSegmentTypes(t *testing.T) { @@ -285,7 +288,10 @@ func TestResolveToLastNode_MixedSegmentTypes(t *testing.T) { 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.NewPath(lnk.String() + "/foo/bar/1/boom/3") + require.NoError(t, err) + + cid, remainder, err := resolver.ResolveToLastNode(ctx, newPath) require.NoError(t, err) assert.Equal(t, 0, len(remainder))