diff --git a/coreiface/path/path.go b/coreiface/path/path.go deleted file mode 100644 index c26b8692b0..0000000000 --- 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/gateway/assets/assets.go b/gateway/assets/assets.go index f411a47e0b..fa8c0735b1 100644 --- a/gateway/assets/assets.go +++ b/gateway/assets/assets.go @@ -133,7 +133,7 @@ type Breadcrumb struct { func Breadcrumbs(urlPath string, dnslinkOrigin bool) []Breadcrumb { var ret []Breadcrumb - p, err := ipfspath.ParsePath(urlPath) + p, err := ipfspath.NewPath(urlPath) if err != nil { // No assets.Breadcrumbs, fallback to bare Path in template return ret diff --git a/gateway/blocks_gateway.go b/gateway/blocks_gateway.go index 1b47525c36..42595cce83 100644 --- a/gateway/blocks_gateway.go +++ b/gateway/blocks_gateway.go @@ -14,7 +14,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" bsfetcher "github.com/ipfs/boxo/fetcher/impl/blockservice" "github.com/ipfs/boxo/files" "github.com/ipfs/boxo/ipld/car" @@ -139,7 +138,7 @@ func NewBlocksGateway(blockService blockservice.BlockService, opts ...BlockGatew }, nil } -func (api *BlocksGateway) Get(ctx context.Context, path ImmutablePath, ranges ...ByteRange) (ContentPathMetadata, *GetResponse, error) { +func (api *BlocksGateway) Get(ctx context.Context, path ipfspath.ImmutablePath, ranges ...ByteRange) (ContentPathMetadata, *GetResponse, error) { md, nd, err := api.getNode(ctx, path) if err != nil { return md, nil, err @@ -180,7 +179,7 @@ func (api *BlocksGateway) 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 (api *BlocksGateway) GetAll(ctx context.Context, path ImmutablePath) (ContentPathMetadata, files.Node, error) { +func (api *BlocksGateway) GetAll(ctx context.Context, path ipfspath.ImmutablePath) (ContentPathMetadata, files.Node, error) { md, nd, err := api.getNode(ctx, path) if err != nil { return md, nil, err @@ -194,7 +193,7 @@ func (api *BlocksGateway) GetAll(ctx context.Context, path ImmutablePath) (Conte return md, n, nil } -func (api *BlocksGateway) GetBlock(ctx context.Context, path ImmutablePath) (ContentPathMetadata, files.File, error) { +func (api *BlocksGateway) GetBlock(ctx context.Context, path ipfspath.ImmutablePath) (ContentPathMetadata, files.File, error) { md, nd, err := api.getNode(ctx, path) if err != nil { return md, nil, err @@ -203,7 +202,7 @@ func (api *BlocksGateway) GetBlock(ctx context.Context, path ImmutablePath) (Con return md, files.NewBytesFile(nd.RawData()), nil } -func (api *BlocksGateway) Head(ctx context.Context, path ImmutablePath) (ContentPathMetadata, files.Node, error) { +func (api *BlocksGateway) Head(ctx context.Context, path ipfspath.ImmutablePath) (ContentPathMetadata, files.Node, error) { md, nd, err := api.getNode(ctx, path) if err != nil { return md, nil, err @@ -223,7 +222,7 @@ func (api *BlocksGateway) Head(ctx context.Context, path ImmutablePath) (Content return md, fileNode, nil } -func (api *BlocksGateway) GetCAR(ctx context.Context, path ImmutablePath) (ContentPathMetadata, io.ReadCloser, <-chan error, error) { +func (api *BlocksGateway) GetCAR(ctx context.Context, path ipfspath.ImmutablePath) (ContentPathMetadata, io.ReadCloser, <-chan error, error) { // Same go-car settings as dag.export command store := dagStore{api: api, ctx: ctx} @@ -257,7 +256,7 @@ func (api *BlocksGateway) GetCAR(ctx context.Context, path ImmutablePath) (Conte return md, r, errCh, nil } -func (api *BlocksGateway) getNode(ctx context.Context, path ImmutablePath) (ContentPathMetadata, format.Node, error) { +func (api *BlocksGateway) getNode(ctx context.Context, path ipfspath.ImmutablePath) (ContentPathMetadata, format.Node, error) { roots, lastSeg, err := api.getPathRoots(ctx, path) if err != nil { return ContentPathMetadata{}, nil, err @@ -278,7 +277,7 @@ func (api *BlocksGateway) getNode(ctx context.Context, path ImmutablePath) (Cont return md, nd, err } -func (api *BlocksGateway) getPathRoots(ctx context.Context, contentPath ImmutablePath) ([]cid.Cid, ifacepath.Resolved, error) { +func (api *BlocksGateway) getPathRoots(ctx context.Context, contentPath ipfspath.ImmutablePath) ([]cid.Cid, ipfspath.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. @@ -302,14 +301,18 @@ func (api *BlocksGateway) getPathRoots(ctx context.Context, contentPath Immutabl contentPathStr := contentPath.String() pathSegments := strings.Split(contentPathStr[6:], "/") sp.WriteString(contentPathStr[:5]) // /ipfs or /ipns - var lastPath ifacepath.Resolved + var lastPath ipfspath.ResolvedPath for _, root := range pathSegments { if root == "" { continue } sp.WriteString("/") sp.WriteString(root) - resolvedSubPath, err := api.resolvePath(ctx, ifacepath.New(sp.String())) + p, err := ipfspath.NewPath(sp.String()) + if err != nil { + return nil, nil, err + } + resolvedSubPath, err := api.resolvePath(ctx, p) if err != nil { // TODO: should we be more explicit here and is this part of the Gateway API contract? // The issue here was that we returned datamodel.ErrWrongKind instead of this resolver error @@ -336,32 +339,26 @@ func (ds dagStore) Get(_ context.Context, c cid.Cid) (blocks.Block, error) { return ds.api.blockService.GetBlock(ds.ctx, c) } -func (api *BlocksGateway) ResolveMutable(ctx context.Context, p ifacepath.Path) (ImmutablePath, error) { - err := p.IsValid() - if err != nil { - return ImmutablePath{}, err - } - - ipath := ipfspath.Path(p.String()) +func (api *BlocksGateway) ResolveMutable(ctx context.Context, ipath ipfspath.Path) (ipfspath.ImmutablePath, error) { switch ipath.Segments()[0] { case "ipns": - ipath, err = resolve.ResolveIPNS(ctx, api.namesys, ipath) + ipath, err := resolve.ResolveIPNS(ctx, api.namesys, ipath) if err != nil { - return ImmutablePath{}, err + return ipfspath.ImmutablePath{}, err } - imPath, err := NewImmutablePath(ifacepath.New(ipath.String())) + imPath, err := ipfspath.NewImmutablePath(ipath) if err != nil { - return ImmutablePath{}, err + return ipfspath.ImmutablePath{}, err } return imPath, nil case "ipfs": - imPath, err := NewImmutablePath(ifacepath.New(ipath.String())) + imPath, err := ipfspath.NewImmutablePath(ipath) if err != nil { - return ImmutablePath{}, err + return ipfspath.ImmutablePath{}, err } return imPath, nil default: - return ImmutablePath{}, NewErrorResponse(fmt.Errorf("unsupported path namespace: %s", p.Namespace()), http.StatusNotImplemented) + return ipfspath.ImmutablePath{}, NewErrorResponse(fmt.Errorf("unsupported path namespace: %s", ipath.Namespace()), http.StatusNotImplemented) } } @@ -385,19 +382,19 @@ func (api *BlocksGateway) GetIPNSRecord(ctx context.Context, c cid.Cid) ([]byte, return api.routing.GetValue(ctx, "/ipns/"+string(id)) } -func (api *BlocksGateway) GetDNSLinkRecord(ctx context.Context, hostname string) (ifacepath.Path, error) { +func (api *BlocksGateway) GetDNSLinkRecord(ctx context.Context, hostname string) (ipfspath.Path, error) { if api.namesys != nil { p, err := api.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, NewErrorResponse(errors.New("not implemented"), http.StatusNotImplemented) } -func (api *BlocksGateway) IsCached(ctx context.Context, p ifacepath.Path) bool { +func (api *BlocksGateway) IsCached(ctx context.Context, p ipfspath.Path) bool { rp, err := api.resolvePath(ctx, p) if err != nil { return false @@ -407,7 +404,7 @@ func (api *BlocksGateway) IsCached(ctx context.Context, p ifacepath.Path) bool { return has } -func (api *BlocksGateway) ResolvePath(ctx context.Context, path ImmutablePath) (ContentPathMetadata, error) { +func (api *BlocksGateway) ResolvePath(ctx context.Context, path ipfspath.ImmutablePath) (ContentPathMetadata, error) { roots, lastSeg, err := api.getPathRoots(ctx, path) if err != nil { return ContentPathMetadata{}, err @@ -419,17 +416,13 @@ func (api *BlocksGateway) ResolvePath(ctx context.Context, path ImmutablePath) ( return md, nil } -func (api *BlocksGateway) resolvePath(ctx context.Context, p ifacepath.Path) (ifacepath.Resolved, error) { - if _, ok := p.(ifacepath.Resolved); ok { - return p.(ifacepath.Resolved), nil +func (api *BlocksGateway) resolvePath(ctx context.Context, ipath ipfspath.Path) (ipfspath.ResolvedPath, error) { + if _, ok := ipath.(ipfspath.ResolvedPath); ok { + return ipath.(ipfspath.ResolvedPath), nil } - err := p.IsValid() - if err != nil { - return nil, err - } + var err error - ipath := ipfspath.Path(p.String()) if ipath.Segments()[0] == "ipns" { ipath, err = resolve.ResolveIPNS(ctx, api.namesys, ipath) if err != nil { @@ -438,7 +431,7 @@ func (api *BlocksGateway) resolvePath(ctx context.Context, p ifacepath.Path) (if } if ipath.Segments()[0] != "ipfs" { - return nil, fmt.Errorf("unsupported path namespace: %s", p.Namespace()) + return nil, fmt.Errorf("unsupported path namespace: %s", ipath.Namespace()) } node, rest, err := api.resolver.ResolveToLastNode(ctx, ipath) @@ -446,10 +439,5 @@ func (api *BlocksGateway) resolvePath(ctx context.Context, p ifacepath.Path) (if return nil, err } - root, err := cid.Parse(ipath.Segments()[1]) - if err != nil { - return nil, err - } - - return ifacepath.NewResolvedPath(ipath, node, root, gopath.Join(rest...)), nil + return ipfspath.NewResolvedPath(ipath, node, gopath.Join(rest...)), nil } diff --git a/gateway/gateway.go b/gateway/gateway.go index 3859793c58..c574c31e44 100644 --- a/gateway/gateway.go +++ b/gateway/gateway.go @@ -2,15 +2,14 @@ package gateway import ( "context" - "fmt" "io" "net/http" "sort" - "github.com/ipfs/boxo/coreiface/path" "github.com/ipfs/boxo/files" "github.com/ipfs/boxo/gateway/assets" "github.com/ipfs/boxo/ipld/unixfs" + ipfspath "github.com/ipfs/boxo/path" "github.com/ipfs/go-cid" ) @@ -86,39 +85,9 @@ type Specification struct { DeserializedResponses bool } -// 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 ContentPathMetadata struct { PathSegmentRoots []cid.Cid - LastSegment path.Resolved + LastSegment ipfspath.ResolvedPath ContentType string // Only used for UnixFS requests } @@ -173,36 +142,36 @@ 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, ipfspath.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, ipfspath.ImmutablePath) (ContentPathMetadata, files.Node, error) // GetBlock returns a single block of data - GetBlock(context.Context, ImmutablePath) (ContentPathMetadata, files.File, error) + GetBlock(context.Context, ipfspath.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, ipfspath.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, ipfspath.ImmutablePath) (ContentPathMetadata, error) // GetCAR returns a CAR file for the given immutable path // Returns an initial error if there was an issue before the CAR streaming begins as well as a channel with a single // that may contain a single error for if any errors occur during the streaming. If there was an initial error the // error channel is nil // TODO: Make this function signature better - GetCAR(context.Context, ImmutablePath) (ContentPathMetadata, io.ReadCloser, <-chan error, error) + GetCAR(context.Context, ipfspath.ImmutablePath) (ContentPathMetadata, io.ReadCloser, <-chan error, error) // IsCached returns whether or not the path exists locally. - IsCached(context.Context, path.Path) bool + IsCached(context.Context, ipfspath.Path) bool // GetIPNSRecord retrieves the best IPNS record for a given CID (libp2p-key) // from the routing system. @@ -213,13 +182,13 @@ 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, ipfspath.Path) (ipfspath.ImmutablePath, error) // GetDNSLinkRecord returns the DNSLink TXT record for the provided FQDN. // Unlike ResolvePath, it does not perform recursive resolution. It only // checks for the existence of a DNSLink TXT record with path starting with // /ipfs/ or /ipns/ and returns the path as-is. - GetDNSLinkRecord(context.Context, string) (path.Path, error) + GetDNSLinkRecord(context.Context, string) (ipfspath.Path, error) } // A helper function to clean up a set of headers: diff --git a/gateway/gateway_test.go b/gateway/gateway_test.go index f8f5868743..43a74539af 100644 --- a/gateway/gateway_test.go +++ b/gateway/gateway_test.go @@ -15,21 +15,20 @@ 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" carblockstore "github.com/ipfs/boxo/ipld/car/v2/blockstore" "github.com/ipfs/boxo/namesys" - path "github.com/ipfs/boxo/path" + ipfspath "github.com/ipfs/boxo/path" "github.com/ipfs/go-cid" "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/routing" "github.com/stretchr/testify/assert" ) -type mockNamesys map[string]path.Path +type mockNamesys map[string]ipfspath.Path -func (m mockNamesys) Resolve(ctx context.Context, name string, opts ...nsopts.ResolveOpt) (value path.Path, err error) { +func (m mockNamesys) Resolve(ctx context.Context, name string, opts ...nsopts.ResolveOpt) (value ipfspath.Path, err error) { cfg := nsopts.DefaultResolveOpts() for _, o := range opts { o(&cfg) @@ -48,7 +47,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() } @@ -63,7 +62,7 @@ func (m mockNamesys) ResolveAsync(ctx context.Context, name string, opts ...nsop return out } -func (m mockNamesys) Publish(ctx context.Context, name crypto.PrivKey, value path.Path, opts ...nsopts.PublishOption) error { +func (m mockNamesys) Publish(ctx context.Context, name crypto.PrivKey, value ipfspath.Path, opts ...nsopts.PublishOption) error { return errors.New("not implemented for mockNamesys") } @@ -108,27 +107,27 @@ func newMockAPI(t *testing.T) (*mockAPI, cid.Cid) { }, cids[0] } -func (api *mockAPI) Get(ctx context.Context, immutablePath ImmutablePath, ranges ...ByteRange) (ContentPathMetadata, *GetResponse, error) { +func (api *mockAPI) Get(ctx context.Context, immutablePath ipfspath.ImmutablePath, ranges ...ByteRange) (ContentPathMetadata, *GetResponse, error) { return api.gw.Get(ctx, immutablePath, ranges...) } -func (api *mockAPI) GetAll(ctx context.Context, immutablePath ImmutablePath) (ContentPathMetadata, files.Node, error) { +func (api *mockAPI) GetAll(ctx context.Context, immutablePath ipfspath.ImmutablePath) (ContentPathMetadata, files.Node, error) { return api.gw.GetAll(ctx, immutablePath) } -func (api *mockAPI) GetBlock(ctx context.Context, immutablePath ImmutablePath) (ContentPathMetadata, files.File, error) { +func (api *mockAPI) GetBlock(ctx context.Context, immutablePath ipfspath.ImmutablePath) (ContentPathMetadata, files.File, error) { return api.gw.GetBlock(ctx, immutablePath) } -func (api *mockAPI) Head(ctx context.Context, immutablePath ImmutablePath) (ContentPathMetadata, files.Node, error) { +func (api *mockAPI) Head(ctx context.Context, immutablePath ipfspath.ImmutablePath) (ContentPathMetadata, files.Node, error) { return api.gw.Head(ctx, immutablePath) } -func (api *mockAPI) GetCAR(ctx context.Context, immutablePath ImmutablePath) (ContentPathMetadata, io.ReadCloser, <-chan error, error) { +func (api *mockAPI) GetCAR(ctx context.Context, immutablePath ipfspath.ImmutablePath) (ContentPathMetadata, io.ReadCloser, <-chan error, error) { return api.gw.GetCAR(ctx, immutablePath) } -func (api *mockAPI) ResolveMutable(ctx context.Context, p ipath.Path) (ImmutablePath, error) { +func (api *mockAPI) ResolveMutable(ctx context.Context, p ipfspath.Path) (ipfspath.ImmutablePath, error) { return api.gw.ResolveMutable(ctx, p) } @@ -136,28 +135,28 @@ func (api *mockAPI) GetIPNSRecord(ctx context.Context, c cid.Cid) ([]byte, error return nil, routing.ErrNotSupported } -func (api *mockAPI) GetDNSLinkRecord(ctx context.Context, hostname string) (ipath.Path, error) { +func (api *mockAPI) GetDNSLinkRecord(ctx context.Context, hostname string) (ipfspath.Path, error) { if api.namesys != nil { p, err := api.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 (api *mockAPI) IsCached(ctx context.Context, p ipath.Path) bool { +func (api *mockAPI) IsCached(ctx context.Context, p ipfspath.Path) bool { return api.gw.IsCached(ctx, p) } -func (api *mockAPI) ResolvePath(ctx context.Context, immutablePath ImmutablePath) (ContentPathMetadata, error) { +func (api *mockAPI) ResolvePath(ctx context.Context, immutablePath ipfspath.ImmutablePath) (ContentPathMetadata, error) { return api.gw.ResolvePath(ctx, immutablePath) } -func (api *mockAPI) resolvePathNoRootsReturned(ctx context.Context, ip ipath.Path) (ipath.Resolved, error) { - var imPath ImmutablePath +func (api *mockAPI) resolvePathNoRootsReturned(ctx context.Context, ip ipfspath.Path) (ipfspath.ResolvedPath, error) { + var imPath ipfspath.ImmutablePath var err error if ip.Mutable() { imPath, err = api.ResolveMutable(ctx, ip) @@ -165,7 +164,7 @@ func (api *mockAPI) resolvePathNoRootsReturned(ctx context.Context, ip ipath.Pat return nil, err } } else { - imPath, err = NewImmutablePath(ip) + imPath, err = ipfspath.NewImmutablePath(ip) if err != nil { return nil, err } @@ -232,14 +231,17 @@ func TestGatewayGet(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - k, err := api.resolvePathNoRootsReturned(ctx, ipath.Join(ipath.IpfsPath(root), t.Name(), "fnord")) + p, err := ipfspath.Join(ipfspath.NewIPFSPath(root), t.Name(), "fnord") assert.NoError(t, err) - api.namesys["/ipns/example.com"] = path.FromCid(k.Cid()) - api.namesys["/ipns/working.example.com"] = path.FromString(k.String()) - api.namesys["/ipns/double.example.com"] = path.FromString("/ipns/working.example.com") - api.namesys["/ipns/triple.example.com"] = path.FromString("/ipns/double.example.com") - api.namesys["/ipns/broken.example.com"] = path.FromString("/ipns/" + k.Cid().String()) + k, err := api.resolvePathNoRootsReturned(ctx, p) + assert.NoError(t, err) + + api.namesys["/ipns/example.com"] = ipfspath.NewIPFSPath(k.Cid()) + api.namesys["/ipns/working.example.com"] = k + api.namesys["/ipns/double.example.com"] = ipfspath.NewDNSLinkPath("working.example.com") + api.namesys["/ipns/triple.example.com"] = ipfspath.NewDNSLinkPath("double.example.com") + api.namesys["/ipns/broken.example.com"] = ipfspath.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 @@ -247,7 +249,7 @@ func TestGatewayGet(t *testing.T) { // // Unfortunately, this may not work on all platforms as file type // detection is platform dependent. - api.namesys["/ipns/example.man"] = path.FromString(k.String()) + api.namesys["/ipns/example.man"] = k t.Log(ts.URL) for _, test := range []struct { @@ -341,11 +343,14 @@ func TestIPNSHostnameRedirect(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - k, err := api.resolvePathNoRootsReturned(ctx, ipath.Join(ipath.IpfsPath(root), t.Name())) + p, err := ipfspath.Join(ipfspath.NewIPFSPath(root), t.Name()) + assert.NoError(t, err) + + k, err := api.resolvePathNoRootsReturned(ctx, p) assert.NoError(t, err) t.Logf("k: %s\n", k) - api.namesys["/ipns/example.net"] = path.FromString(k.String()) + api.namesys["/ipns/example.net"] = k // make request to directory containing index.html req, err := http.NewRequest(http.MethodGet, ts.URL+"/foo", nil) @@ -396,18 +401,25 @@ func TestIPNSHostnameBacklinks(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - k, err := api.resolvePathNoRootsReturned(ctx, ipath.Join(ipath.IpfsPath(root), t.Name())) + p, err := ipfspath.Join(ipfspath.NewIPFSPath(root), t.Name()) + assert.NoError(t, err) + + k, err := api.resolvePathNoRootsReturned(ctx, p) assert.NoError(t, err) // create /ipns/example.net/foo/ - k2, err := api.resolvePathNoRootsReturned(ctx, ipath.Join(k, "foo? #<'")) + p2, err := ipfspath.Join(k, "foo? #<'") + assert.NoError(t, err) + k2, err := api.resolvePathNoRootsReturned(ctx, p2) assert.NoError(t, err) - k3, err := api.resolvePathNoRootsReturned(ctx, ipath.Join(k, "foo? #<'/bar")) + p3, err := ipfspath.Join(k, "foo? #<'/bar") + assert.NoError(t, err) + k3, err := api.resolvePathNoRootsReturned(ctx, p3) assert.NoError(t, err) t.Logf("k: %s\n", k) - api.namesys["/ipns/example.net"] = path.FromString(k.String()) + api.namesys["/ipns/example.net"] = k // make request to directory listing req, err := http.NewRequest(http.MethodGet, ts.URL+"/foo%3F%20%23%3C%27/", nil) @@ -480,11 +492,14 @@ func TestPretty404(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - k, err := api.resolvePathNoRootsReturned(ctx, ipath.Join(ipath.IpfsPath(root), t.Name())) + p, err := ipfspath.Join(ipfspath.NewIPFSPath(root), t.Name()) + assert.NoError(t, err) + + k, err := api.resolvePathNoRootsReturned(ctx, p) assert.NoError(t, err) host := "example.net" - api.namesys["/ipns/"+host] = path.FromString(k.String()) + api.namesys["/ipns/"+host] = k for _, test := range []struct { path string @@ -701,8 +716,8 @@ func TestIpfsTrustlessMode(t *testing.T) { func TestIpnsTrustlessMode(t *testing.T) { api, root := newMockAPI(t) - api.namesys["/ipns/trustless.com"] = path.FromCid(root) - api.namesys["/ipns/trusted.com"] = path.FromCid(root) + api.namesys["/ipns/trustless.com"] = ipfspath.NewIPFSPath(root) + api.namesys["/ipns/trusted.com"] = ipfspath.NewIPFSPath(root) ts := newTestServerWithConfig(t, api, Config{ Headers: map[string][]string{}, @@ -764,7 +779,10 @@ func TestDagJsonCborPreview(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - resolvedPath, err := api.resolvePathNoRootsReturned(ctx, ipath.Join(ipath.IpfsPath(root), t.Name(), "example")) + p, err := ipfspath.Join(ipfspath.NewIPFSPath(root), t.Name(), "example") + assert.NoError(t, err) + + resolvedPath, err := api.resolvePathNoRootsReturned(ctx, p) assert.NoError(t, err) cidStr := resolvedPath.Cid().String() diff --git a/gateway/handler.go b/gateway/handler.go index 90ff7720a8..dddf660614 100644 --- a/gateway/handler.go +++ b/gateway/handler.go @@ -16,8 +16,8 @@ import ( "strings" "time" - ipath "github.com/ipfs/boxo/coreiface/path" "github.com/ipfs/boxo/gateway/assets" + ipfspath "github.com/ipfs/boxo/path" cid "github.com/ipfs/go-cid" logging "github.com/ipfs/go-log" "github.com/libp2p/go-libp2p/core/peer" @@ -198,20 +198,20 @@ func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) { return } - contentPath := ipath.New(r.URL.Path) - ctx := context.WithValue(r.Context(), ContentPathKey, contentPath) - r = r.WithContext(ctx) - - if requestHandled := i.handleOnlyIfCached(w, r, contentPath, logger); requestHandled { + if requestHandled := i.handleSuperfluousNamespace(w, r); requestHandled { return } - if requestHandled := i.handleSuperfluousNamespace(w, r, contentPath); requestHandled { + contentPath, err := ipfspath.NewPath(r.URL.Path) + if err != nil { + i.webError(w, r, err, http.StatusBadRequest) return } - if err := contentPath.IsValid(); err != nil { - i.webError(w, r, err, http.StatusBadRequest) + ctx := context.WithValue(r.Context(), ContentPathKey, contentPath) + r = r.WithContext(ctx) + + if requestHandled := i.handleOnlyIfCached(w, r, contentPath, logger); requestHandled { return } @@ -246,7 +246,7 @@ func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) { return } - var immutableContentPath ImmutablePath + var immutableContentPath ipfspath.ImmutablePath if contentPath.Mutable() { immutableContentPath, err = i.api.ResolveMutable(r.Context(), contentPath) if err != nil { @@ -256,7 +256,7 @@ func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) { return } } else { - immutableContentPath, err = NewImmutablePath(contentPath) + immutableContentPath, err = ipfspath.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) @@ -273,7 +273,7 @@ func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) { // If we already did the path resolution no need to do it again maybeResolvedImPath := immutableContentPath if ifNoneMatchResolvedPath != nil { - maybeResolvedImPath, err = NewImmutablePath(ifNoneMatchResolvedPath) + maybeResolvedImPath, err = ipfspath.NewImmutablePath(ifNoneMatchResolvedPath) if err != nil { i.webError(w, r, err, http.StatusInternalServerError) return @@ -349,7 +349,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 ipfspath.Path, responseFormat string) bool { // Only allow "/{#1}/{#2}"-like paths. trimmedPath := strings.Trim(contentPath.String(), "/") pathComponents := strings.Split(trimmedPath, "/") @@ -395,7 +395,7 @@ func panicHandler(w http.ResponseWriter) { } } -func addCacheControlHeaders(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, fileCid cid.Cid) (modtime time.Time) { +func addCacheControlHeaders(w http.ResponseWriter, r *http.Request, contentPath ipfspath.Path, fileCid cid.Cid) (modtime time.Time) { // Set Etag to based on CID (override whatever was set before) w.Header().Set("Etag", getEtag(r, fileCid)) @@ -424,7 +424,7 @@ func addCacheControlHeaders(w http.ResponseWriter, r *http.Request, contentPath } // Set Content-Disposition if filename URL query param is present, return preferred filename -func addContentDispositionHeader(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) string { +func addContentDispositionHeader(w http.ResponseWriter, r *http.Request, contentPath ipfspath.Path) string { /* This logic enables: * - creation of HTML links that trigger "Save As.." dialog instead of being rendered by the browser * - overriding the filename used when saving subresource assets on HTML page @@ -491,7 +491,7 @@ func (i *handler) setIpfsRootsHeader(w http.ResponseWriter, pathMetadata Content return nil } -func getFilename(contentPath ipath.Path) string { +func getFilename(contentPath ipfspath.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. @@ -648,7 +648,7 @@ func debugStr(path string) string { return q } -func (i *handler) handleIfNoneMatch(w http.ResponseWriter, r *http.Request, responseFormat string, contentPath ipath.Path, imPath ImmutablePath, logger *zap.SugaredLogger) (ipath.Resolved, bool) { +func (i *handler) handleIfNoneMatch(w http.ResponseWriter, r *http.Request, responseFormat string, contentPath ipfspath.Path, imPath ipfspath.ImmutablePath, logger *zap.SugaredLogger) (ipfspath.ResolvedPath, bool) { // Detect when If-None-Match HTTP header allows returning HTTP 304 Not Modified if inm := r.Header.Get("If-None-Match"); inm != "" { pathMetadata, err := i.api.ResolvePath(r.Context(), imPath) @@ -678,7 +678,7 @@ func (i *handler) handleIfNoneMatch(w http.ResponseWriter, r *http.Request, resp } // 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 ipfspath.Path, err error) bool { if err == nil { return true } @@ -690,7 +690,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 ipfspath.ImmutablePath, contentPath ipfspath.Path, err error, logger *zap.SugaredLogger) (ipfspath.ImmutablePath, bool) { if err == nil { return maybeResolvedImPath, true } @@ -698,7 +698,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 ipfspath.ImmutablePath{}, false } // If we have origin isolation (subdomain gw, DNSLink website), @@ -720,17 +720,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.Debugw("served legacy 404") - return ImmutablePath{}, false + return ipfspath.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 ipfspath.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, logger *zap.SugaredLogger) (requestHandled bool) { +func (i *handler) handleOnlyIfCached(w http.ResponseWriter, r *http.Request, contentPath ipfspath.Path, logger *zap.SugaredLogger) (requestHandled bool) { if r.Header.Get("Cache-Control") == "only-if-cached" { if !i.api.IsCached(r.Context(), contentPath) { if r.Method == http.MethodHead { @@ -846,23 +846,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) (requestHandled 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) (requestHandled 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 := ipfspath.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 @@ -887,7 +883,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 ipfspath.Path) assets.GlobalData { // gatewayURL is used to link to other root CIDs. THis will be blank unless // subdomain or DNSLink resolution is being used for this request. var gatewayURL string diff --git a/gateway/handler_block.go b/gateway/handler_block.go index b21926a027..ae571ad28d 100644 --- a/gateway/handler_block.go +++ b/gateway/handler_block.go @@ -5,13 +5,13 @@ import ( "net/http" "time" - ipath "github.com/ipfs/boxo/coreiface/path" + ipfspath "github.com/ipfs/boxo/path" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) // serveRawBlock returns bytes behind a raw block -func (i *handler) serveRawBlock(ctx context.Context, w http.ResponseWriter, r *http.Request, imPath ImmutablePath, contentPath ipath.Path, begin time.Time) bool { +func (i *handler) serveRawBlock(ctx context.Context, w http.ResponseWriter, r *http.Request, imPath ipfspath.ImmutablePath, contentPath ipfspath.Path, begin time.Time) bool { ctx, span := spanTrace(ctx, "Handler.ServeRawBlock", trace.WithAttributes(attribute.String("path", imPath.String()))) defer span.End() diff --git a/gateway/handler_car.go b/gateway/handler_car.go index 61033aaf81..cd8ac48faf 100644 --- a/gateway/handler_car.go +++ b/gateway/handler_car.go @@ -7,14 +7,14 @@ import ( "net/http" "time" - ipath "github.com/ipfs/boxo/coreiface/path" + ipfspath "github.com/ipfs/boxo/path" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "go.uber.org/multierr" ) // serveCAR returns a CAR stream for specific DAG+selector -func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.Request, imPath ImmutablePath, contentPath ipath.Path, carVersion string, begin time.Time) bool { +func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.Request, imPath ipfspath.ImmutablePath, contentPath ipfspath.Path, carVersion string, begin time.Time) bool { ctx, span := spanTrace(ctx, "Handler.ServeCAR", trace.WithAttributes(attribute.String("path", imPath.String()))) defer span.End() diff --git a/gateway/handler_codec.go b/gateway/handler_codec.go index af3a252c67..fcd93ce256 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" + ipfspath "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" @@ -58,7 +58,7 @@ var contentTypeToExtension = map[string]string{ "application/vnd.ipld.dag-cbor": ".cbor", } -func (i *handler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http.Request, imPath ImmutablePath, contentPath ipath.Path, begin time.Time, requestedContentType string) bool { +func (i *handler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http.Request, imPath ipfspath.ImmutablePath, contentPath ipfspath.Path, begin time.Time, requestedContentType string) bool { ctx, span := spanTrace(ctx, "Handler.ServeCodec", trace.WithAttributes(attribute.String("path", imPath.String()), attribute.String("requestedContentType", requestedContentType))) defer span.End() @@ -77,7 +77,7 @@ func (i *handler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http return i.renderCodec(ctx, w, r, resolvedPath, data, contentPath, begin, requestedContentType) } -func (i *handler) renderCodec(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, blockData io.ReadSeekCloser, contentPath ipath.Path, begin time.Time, requestedContentType string) bool { +func (i *handler) renderCodec(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipfspath.ResolvedPath, blockData io.ReadSeekCloser, contentPath ipfspath.Path, begin time.Time, requestedContentType string) bool { ctx, span := spanTrace(ctx, "Handler.RenderCodec", trace.WithAttributes(attribute.String("path", resolvedPath.String()), attribute.String("requestedContentType", requestedContentType))) defer span.End() @@ -153,7 +153,7 @@ func (i *handler) renderCodec(ctx context.Context, w http.ResponseWriter, r *htt return i.serveCodecConverted(ctx, w, r, blockCid, blockData, contentPath, toCodec, modtime, 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 ipfspath.ResolvedPath, contentPath ipfspath.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) @@ -233,7 +233,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 ipfspath.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) @@ -247,7 +247,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 ipfspath.Path, toCodec mc.Code, modtime, begin time.Time) bool { codec := blockCid.Prefix().Codec decoder, err := multicodec.LookupDecoder(codec) if err != nil { @@ -292,7 +292,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 ipfspath.ResolvedPath, contentType string) string { var dispType, name string ext, ok := contentTypeToExtension[contentType] diff --git a/gateway/handler_defaults.go b/gateway/handler_defaults.go index fad35beeec..5ea7105a89 100644 --- a/gateway/handler_defaults.go +++ b/gateway/handler_defaults.go @@ -11,15 +11,15 @@ import ( "time" "github.com/ipfs/boxo/files" + ipfspath "github.com/ipfs/boxo/path" mc "github.com/multiformats/go-multicodec" - ipath "github.com/ipfs/boxo/coreiface/path" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" ) -func (i *handler) serveDefaults(ctx context.Context, w http.ResponseWriter, r *http.Request, maybeResolvedImPath ImmutablePath, immutableContentPath ImmutablePath, contentPath ipath.Path, begin time.Time, requestedContentType string, logger *zap.SugaredLogger) bool { +func (i *handler) serveDefaults(ctx context.Context, w http.ResponseWriter, r *http.Request, maybeResolvedImPath, immutableContentPath ipfspath.ImmutablePath, contentPath ipfspath.Path, begin time.Time, requestedContentType string, logger *zap.SugaredLogger) bool { ctx, span := spanTrace(ctx, "Handler.ServeDefaults", trace.WithAttributes(attribute.String("path", contentPath.String()))) defer span.End() diff --git a/gateway/handler_ipns_record.go b/gateway/handler_ipns_record.go index 6d42836b34..b1c82a44f9 100644 --- a/gateway/handler_ipns_record.go +++ b/gateway/handler_ipns_record.go @@ -10,15 +10,15 @@ import ( "time" "github.com/cespare/xxhash/v2" - ipath "github.com/ipfs/boxo/coreiface/path" "github.com/ipfs/boxo/ipns" + ipfspath "github.com/ipfs/boxo/path" "github.com/ipfs/go-cid" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" ) -func (i *handler) serveIpnsRecord(ctx context.Context, w http.ResponseWriter, r *http.Request, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) bool { +func (i *handler) serveIpnsRecord(ctx context.Context, w http.ResponseWriter, r *http.Request, contentPath ipfspath.Path, begin time.Time, logger *zap.SugaredLogger) bool { ctx, span := spanTrace(ctx, "Handler.ServeIPNSRecord", trace.WithAttributes(attribute.String("path", contentPath.String()))) defer span.End() diff --git a/gateway/handler_tar.go b/gateway/handler_tar.go index 27ab8fa409..63cb4f8eb2 100644 --- a/gateway/handler_tar.go +++ b/gateway/handler_tar.go @@ -6,8 +6,8 @@ import ( "net/http" "time" - ipath "github.com/ipfs/boxo/coreiface/path" "github.com/ipfs/boxo/files" + ipfspath "github.com/ipfs/boxo/path" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" @@ -15,7 +15,7 @@ import ( var unixEpochTime = time.Unix(0, 0) -func (i *handler) serveTAR(ctx context.Context, w http.ResponseWriter, r *http.Request, imPath ImmutablePath, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) bool { +func (i *handler) serveTAR(ctx context.Context, w http.ResponseWriter, r *http.Request, imPath ipfspath.ImmutablePath, contentPath ipfspath.Path, begin time.Time, logger *zap.SugaredLogger) bool { ctx, span := spanTrace(ctx, "Handler.ServeTAR", trace.WithAttributes(attribute.String("path", imPath.String()))) defer span.End() diff --git a/gateway/handler_test.go b/gateway/handler_test.go index 9addab9e22..4e2a4f79ef 100644 --- a/gateway/handler_test.go +++ b/gateway/handler_test.go @@ -10,8 +10,8 @@ import ( "testing" "time" - ipath "github.com/ipfs/boxo/coreiface/path" "github.com/ipfs/boxo/files" + ipfspath "github.com/ipfs/boxo/path" "github.com/ipfs/boxo/path/resolver" cid "github.com/ipfs/go-cid" ipld "github.com/ipfs/go-ipld-format" @@ -45,43 +45,43 @@ type errorMockAPI struct { err error } -func (api *errorMockAPI) Get(ctx context.Context, path ImmutablePath, getRange ...ByteRange) (ContentPathMetadata, *GetResponse, error) { +func (api *errorMockAPI) Get(ctx context.Context, path ipfspath.ImmutablePath, getRange ...ByteRange) (ContentPathMetadata, *GetResponse, error) { return ContentPathMetadata{}, nil, api.err } -func (api *errorMockAPI) GetAll(ctx context.Context, path ImmutablePath) (ContentPathMetadata, files.Node, error) { +func (api *errorMockAPI) GetAll(ctx context.Context, path ipfspath.ImmutablePath) (ContentPathMetadata, files.Node, error) { return ContentPathMetadata{}, nil, api.err } -func (api *errorMockAPI) GetBlock(ctx context.Context, path ImmutablePath) (ContentPathMetadata, files.File, error) { +func (api *errorMockAPI) GetBlock(ctx context.Context, path ipfspath.ImmutablePath) (ContentPathMetadata, files.File, error) { return ContentPathMetadata{}, nil, api.err } -func (api *errorMockAPI) Head(ctx context.Context, path ImmutablePath) (ContentPathMetadata, files.Node, error) { +func (api *errorMockAPI) Head(ctx context.Context, path ipfspath.ImmutablePath) (ContentPathMetadata, files.Node, error) { return ContentPathMetadata{}, nil, api.err } -func (api *errorMockAPI) GetCAR(ctx context.Context, path ImmutablePath) (ContentPathMetadata, io.ReadCloser, <-chan error, error) { +func (api *errorMockAPI) GetCAR(ctx context.Context, path ipfspath.ImmutablePath) (ContentPathMetadata, io.ReadCloser, <-chan error, error) { return ContentPathMetadata{}, nil, nil, api.err } -func (api *errorMockAPI) ResolveMutable(ctx context.Context, path ipath.Path) (ImmutablePath, error) { - return ImmutablePath{}, api.err +func (api *errorMockAPI) ResolveMutable(ctx context.Context, path ipfspath.Path) (ipfspath.ImmutablePath, error) { + return ipfspath.ImmutablePath{}, api.err } func (api *errorMockAPI) GetIPNSRecord(ctx context.Context, c cid.Cid) ([]byte, error) { return nil, api.err } -func (api *errorMockAPI) GetDNSLinkRecord(ctx context.Context, hostname string) (ipath.Path, error) { +func (api *errorMockAPI) GetDNSLinkRecord(ctx context.Context, hostname string) (ipfspath.Path, error) { return nil, api.err } -func (api *errorMockAPI) IsCached(ctx context.Context, p ipath.Path) bool { +func (api *errorMockAPI) IsCached(ctx context.Context, p ipfspath.Path) bool { return false } -func (api *errorMockAPI) ResolvePath(ctx context.Context, path ImmutablePath) (ContentPathMetadata, error) { +func (api *errorMockAPI) ResolvePath(ctx context.Context, path ipfspath.ImmutablePath) (ContentPathMetadata, error) { return ContentPathMetadata{}, api.err } @@ -157,27 +157,27 @@ type panicMockAPI struct { panicOnHostnameHandler bool } -func (api *panicMockAPI) Get(ctx context.Context, immutablePath ImmutablePath, ranges ...ByteRange) (ContentPathMetadata, *GetResponse, error) { +func (api *panicMockAPI) Get(ctx context.Context, immutablePath ipfspath.ImmutablePath, ranges ...ByteRange) (ContentPathMetadata, *GetResponse, error) { panic("i am panicking") } -func (api *panicMockAPI) GetAll(ctx context.Context, immutablePath ImmutablePath) (ContentPathMetadata, files.Node, error) { +func (api *panicMockAPI) GetAll(ctx context.Context, immutablePath ipfspath.ImmutablePath) (ContentPathMetadata, files.Node, error) { panic("i am panicking") } -func (api *panicMockAPI) GetBlock(ctx context.Context, immutablePath ImmutablePath) (ContentPathMetadata, files.File, error) { +func (api *panicMockAPI) GetBlock(ctx context.Context, immutablePath ipfspath.ImmutablePath) (ContentPathMetadata, files.File, error) { panic("i am panicking") } -func (api *panicMockAPI) Head(ctx context.Context, immutablePath ImmutablePath) (ContentPathMetadata, files.Node, error) { +func (api *panicMockAPI) Head(ctx context.Context, immutablePath ipfspath.ImmutablePath) (ContentPathMetadata, files.Node, error) { panic("i am panicking") } -func (api *panicMockAPI) GetCAR(ctx context.Context, immutablePath ImmutablePath) (ContentPathMetadata, io.ReadCloser, <-chan error, error) { +func (api *panicMockAPI) GetCAR(ctx context.Context, immutablePath ipfspath.ImmutablePath) (ContentPathMetadata, io.ReadCloser, <-chan error, error) { panic("i am panicking") } -func (api *panicMockAPI) ResolveMutable(ctx context.Context, p ipath.Path) (ImmutablePath, error) { +func (api *panicMockAPI) ResolveMutable(ctx context.Context, p ipfspath.Path) (ipfspath.ImmutablePath, error) { panic("i am panicking") } @@ -185,7 +185,7 @@ func (api *panicMockAPI) GetIPNSRecord(ctx context.Context, c cid.Cid) ([]byte, panic("i am panicking") } -func (api *panicMockAPI) GetDNSLinkRecord(ctx context.Context, hostname string) (ipath.Path, error) { +func (api *panicMockAPI) GetDNSLinkRecord(ctx context.Context, hostname string) (ipfspath.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. @@ -196,11 +196,11 @@ func (api *panicMockAPI) GetDNSLinkRecord(ctx context.Context, hostname string) return nil, errors.New("not implemented") } -func (api *panicMockAPI) IsCached(ctx context.Context, p ipath.Path) bool { +func (api *panicMockAPI) IsCached(ctx context.Context, p ipfspath.Path) bool { panic("i am panicking") } -func (api *panicMockAPI) ResolvePath(ctx context.Context, immutablePath ImmutablePath) (ContentPathMetadata, error) { +func (api *panicMockAPI) ResolvePath(ctx context.Context, immutablePath ipfspath.ImmutablePath) (ContentPathMetadata, error) { panic("i am panicking") } diff --git a/gateway/handler_unixfs__redirects.go b/gateway/handler_unixfs__redirects.go index de5d0105f7..5785e296a0 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" + ipfspath "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 ipfspath.ImmutablePath, contentPath ipfspath.Path, logger *zap.SugaredLogger) (newContentPath ipfspath.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 ipfspath.ImmutablePath{}, false, true + } + + redirectsPath, err := ipfspath.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 ipfspath.ImmutablePath{}, false, true + } + + imRedirectsPath, err := ipfspath.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 ipfspath.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 ipfspath.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 ipfspath.ImmutablePath{}, false, true } if redirected { - return ImmutablePath{}, false, true + return ipfspath.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 := ipfspath.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 ipfspath.ImmutablePath{}, false, true + } + imPath, err := ipfspath.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 ipfspath.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) (redirected bool, newContentPath string, err error) { +func (i *handler) handleRedirectsFileRules(w http.ResponseWriter, r *http.Request, immutableContentPath ipfspath.ImmutablePath, cPath ipfspath.Path, redirectRules []redirects.Rule) (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 := ipfspath.NewPath(toPath) + if err != nil { + return true, toPath, err + } + + imContent4xxPath, err := ipfspath.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 := ipfspath.NewPath(contentRootPath + rule.To) + if err != nil { + return true, toPath, err + } + err = i.serve4xx(w, r, imContent4xxPath, content4xxPath, rule.Status) 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 ipfspath.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 { +func getRootPath(path ipfspath.Path) (ipfspath.Path, error) { parts := strings.Split(path.String(), "/") - return ipath.New(gopath.Join("/", path.Namespace(), parts[2])) + return ipfspath.NewPath(gopath.Join("/", path.Namespace(), parts[2])) } -func (i *handler) serve4xx(w http.ResponseWriter, r *http.Request, content4xxPathImPath ImmutablePath, content4xxPath ipath.Path, status int) error { +func (i *handler) serve4xx(w http.ResponseWriter, r *http.Request, content4xxPathImPath ipfspath.ImmutablePath, content4xxPath ipfspath.Path, status int) error { pathMetadata, getresp, err := i.api.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) bool { +func (i *handler) serveLegacy404IfPresent(w http.ResponseWriter, r *http.Request, imPath ipfspath.ImmutablePath) 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 ipfspath.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 := ipfspath.NewPath("/" + pretty404) + if err != nil { break } - imparsed404Path, err := NewImmutablePath(parsed404Path) + imparsed404Path, err := ipfspath.NewImmutablePath(parsed404Path) if err != nil { break } diff --git a/gateway/handler_unixfs_dir.go b/gateway/handler_unixfs_dir.go index 1fac5afd95..e9814d5780 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" + ipfspath "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 ipfspath.ResolvedPath, contentPath ipfspath.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 := ipfspath.Join(contentPath, "index.html") + if err != nil { + i.webError(w, r, err, http.StatusInternalServerError) + return false + } + + indexPath, err := ipfspath.Join(resolvedPath, "index.html") + if err != nil { + i.webError(w, r, err, http.StatusInternalServerError) + return false + } + + imIndexPath, err := ipfspath.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_file.go b/gateway/handler_unixfs_file.go index fec73505c2..57ff446031 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" + ipfspath "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 ipfspath.ResolvedPath, contentPath ipfspath.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 7eb03d2646..2177ec65c7 100644 --- a/gateway/hostname_test.go +++ b/gateway/hostname_test.go @@ -17,8 +17,8 @@ func TestToSubdomainURL(t *testing.T) { testCID, err := cid.Decode("bafkqaglimvwgy3zakrsxg5cun5jxkyten5wwc2lokvjeycq") assert.NoError(t, err) - gwAPI.namesys["/ipns/dnslink.long-name.example.com"] = path.FromString(testCID.String()) - gwAPI.namesys["/ipns/dnslink.too-long.f1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5o.example.com"] = path.FromString(testCID.String()) + gwAPI.namesys["/ipns/dnslink.long-name.example.com"] = path.NewIPFSPath(testCID) + gwAPI.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 d6368f92e5..0e16c8c060 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" + ipfspath "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) updateApiCallMetric(name string, err error, beg } } -func (b *ipfsBackendWithMetrics) Get(ctx context.Context, path ImmutablePath, ranges ...ByteRange) (ContentPathMetadata, *GetResponse, error) { +func (b *ipfsBackendWithMetrics) Get(ctx context.Context, path ipfspath.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 ipfspath.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 ipfspath.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 ipfspath.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 ipfspath.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) (ContentPathMetadata, io.ReadCloser, <-chan error, error) { +func (b *ipfsBackendWithMetrics) GetCAR(ctx context.Context, path ipfspath.ImmutablePath) (ContentPathMetadata, io.ReadCloser, <-chan error, error) { begin := time.Now() name := "IPFSBackend.GetCAR" ctx, span := spanTrace(ctx, name, trace.WithAttributes(attribute.String("path", path.String()))) @@ -133,7 +133,7 @@ func (b *ipfsBackendWithMetrics) GetCAR(ctx context.Context, path ImmutablePath) return md, rc, errCh, err } -func (b *ipfsBackendWithMetrics) IsCached(ctx context.Context, path path.Path) bool { +func (b *ipfsBackendWithMetrics) IsCached(ctx context.Context, path ipfspath.Path) bool { begin := time.Now() name := "IPFSBackend.IsCached" ctx, span := spanTrace(ctx, name, trace.WithAttributes(attribute.String("path", path.String()))) @@ -157,7 +157,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 ipfspath.Path) (ipfspath.ImmutablePath, error) { begin := time.Now() name := "IPFSBackend.ResolveMutable" ctx, span := spanTrace(ctx, name, trace.WithAttributes(attribute.String("path", path.String()))) @@ -169,7 +169,7 @@ func (b *ipfsBackendWithMetrics) ResolveMutable(ctx context.Context, path path.P return p, err } -func (b *ipfsBackendWithMetrics) GetDNSLinkRecord(ctx context.Context, fqdn string) (path.Path, error) { +func (b *ipfsBackendWithMetrics) GetDNSLinkRecord(ctx context.Context, fqdn string) (ipfspath.Path, error) { begin := time.Now() name := "IPFSBackend.GetDNSLinkRecord" ctx, span := spanTrace(ctx, name, trace.WithAttributes(attribute.String("fqdn", fqdn))) diff --git a/namesys/cache.go b/namesys/cache.go index 8b7f50794c..25df246313 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 6f846fcdac..5b3d853e1b 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/ipns_resolver_validation_test.go b/namesys/ipns_resolver_validation_test.go index 9799e5ba37..309692a0ba 100644 --- a/namesys/ipns_resolver_validation_test.go +++ b/namesys/ipns_resolver_validation_test.go @@ -8,7 +8,6 @@ import ( opts "github.com/ipfs/boxo/coreiface/options/namesys" "github.com/ipfs/boxo/ipns" ipns_pb "github.com/ipfs/boxo/ipns/pb" - "github.com/ipfs/boxo/path" mockrouting "github.com/ipfs/boxo/routing/mock" "github.com/ipfs/boxo/routing/offline" ds "github.com/ipfs/go-datastore" @@ -76,7 +75,7 @@ func testResolverValidation(t *testing.T, keyType int) { if err != nil { t.Fatal(err) } - if resp != path.Path(p) { + if resp.String() != string(p) { t.Fatalf("Mismatch between published path %s and resolved path %s", p, resp) } // Create expired entry diff --git a/namesys/namesys.go b/namesys/namesys.go index d031064e63..8aa5a4421c 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 52fce67944..940bccb070 100644 --- a/namesys/namesys_test.go +++ b/namesys/namesys_test.go @@ -39,7 +39,7 @@ func testResolution(t *testing.T, resolver Resolver, name string, depth uint, ex } 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 +118,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 +158,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 24a0b8e4d1..2fd4258efe 100644 --- a/namesys/publisher.go +++ b/namesys/publisher.go @@ -163,7 +163,7 @@ func (p *IpnsPublisher) updateRecord(ctx context.Context, k crypto.PrivKey, valu } seqno := rec.GetSequence() // returns 0 if rec is nil - if rec != nil && value != path.Path(rec.GetValue()) { + if rec != nil && value.String() != string(rec.GetValue()) { // Don't bother incrementing the sequence number unless the // value changes. seqno++ @@ -172,7 +172,7 @@ func (p *IpnsPublisher) updateRecord(ctx context.Context, k crypto.PrivKey, valu opts := opts.ProcessPublishOptions(options) // Create record - entry, err := ipns.Create(k, []byte(value), seqno, opts.EOL, opts.TTL) + entry, err := ipns.Create(k, []byte(value.String()), seqno, opts.EOL, opts.TTL) if err != nil { return nil, err } diff --git a/namesys/publisher_test.go b/namesys/publisher_test.go index b40593c760..97fabc9543 100644 --- a/namesys/publisher_test.go +++ b/namesys/publisher_test.go @@ -125,7 +125,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/resolve/resolve.go b/namesys/resolve/resolve.go index b2acf06028..da586123f9 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 d2da312152..60324ce356 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) } @@ -51,10 +55,13 @@ 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.Create(identity.PrivateKey(), []byte(h), 0, eol, 0) + entry, err := ipns.Create(identity.PrivateKey(), []byte(h.String()), 0, eol, 0) if err != nil { t.Fatal(err) } @@ -85,9 +92,12 @@ 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.Create(identity.PrivateKey(), []byte(h), 0, eol, 0) + entry, err := ipns.Create(identity.PrivateKey(), []byte(h.String()), 0, eol, 0) if err != nil { t.Fatal(err) } diff --git a/namesys/routing.go b/namesys/routing.go index c51d6f72b8..b4971382e2 100644 --- a/namesys/routing.go +++ b/namesys/routing.go @@ -118,10 +118,10 @@ func (r *IpnsResolver) resolveOnceAsync(ctx context.Context, name string, option if valh, err := mh.Cast(entry.GetValue()); err == nil { // Its an old style multihash record log.Debugf("encountered CIDv0 ipns entry: %s", valh) - p = path.FromCid(cid.NewCidV0(valh)) + p = path.NewIPFSPath(cid.NewCidV0(valh)) } else { // Not a multihash, probably a new style record - p, err = path.ParsePath(string(entry.GetValue())) + p, err = path.NewPath(string(entry.GetValue())) if err != nil { emitOnceResult(ctx, out, onceResult{err: err}) return diff --git a/path/go_path.go b/path/go_path.go new file mode 100644 index 0000000000..376871ee8f --- /dev/null +++ b/path/go_path.go @@ -0,0 +1,72 @@ +package path + +// // 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, "/") + +// // Ignore leading slash +// if len(segments[0]) == 0 { +// segments = segments[1:] +// } + +// return segments +// } + +// // String converts a path to string. +// func (p path) String() string { +// return string(p) +// } + +// // 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") +// } + +// // PopLastSegment returns a new Path without its final segment, and the final +// // segment, separately. If there is no more to pop (the path is just a key), +// // the original path is returned. +// func (p path) PopLastSegment() (Path, string, error) { + +// if p.IsJustAKey() { +// return p, "", nil +// } + +// segs := p.Segments() +// newPath, err := ParsePath("/" + strings.Join(segs[:len(segs)-1], "/")) +// if err != nil { +// return "", "", err +// } + +// return newPath, segs[len(segs)-1], nil +// } + +// // FromSegments returns a path given its different segments. +// func FromSegments(prefix string, seg ...string) (Path, error) { +// return ParsePath(prefix + strings.Join(seg, "/")) +// } + +// // SplitAbsPath clean up and split fpath. It extracts the first component (which +// // must be a Multihash) and return it separately. +// func SplitAbsPath(fpath Path) (cid.Cid, []string, error) { +// parts := fpath.Segments() +// if parts[0] == "ipfs" || parts[0] == "ipld" { +// parts = parts[1:] +// } + +// // if nothing, bail. +// if len(parts) == 0 { +// return cid.Cid{}, nil, &ErrInvalidPath{error: fmt.Errorf("empty"), path: string(fpath)} +// } + +// c, err := decodeCid(parts[0]) +// // first element in the path is a cid +// if err != nil { +// return cid.Cid{}, nil, &ErrInvalidPath{error: fmt.Errorf("invalid CID: %w", err), path: string(fpath)} +// } + +// return c, parts[1:], nil +// } diff --git a/path/path.go b/path/path.go index 6d53ade047..3d45595ee9 100644 --- a/path/path.go +++ b/path/path.go @@ -3,38 +3,92 @@ 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 +const ( + IPFSNamespace = "ipfs" + IPNSNamespace = "ipns" + IPLDNamespace = "ipld" +) + +// 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 +} + +// 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 +} + +// ImmutablePath is a [Path] which is guaranteed to return "false" to [Mutable]. +type ImmutablePath struct { + Path +} + +func NewImmutablePath(p Path) (ImmutablePath, error) { + if p.Mutable() { + return ImmutablePath{}, fmt.Errorf("path was expected to be immutable: %s", p.String()) + } -// ^^^ -// TODO: debate making this a private struct wrapped in a public interface -// would allow us to control creation, and cache segments. + return ImmutablePath{p}, nil +} + +type path struct { + str string + root cid.Cid + namespace string +} -// FromString safely converts a string type to a Path type. -func FromString(s string) Path { - return Path(s) +func (p *path) String() string { + return p.str } -// FromCid safely converts a cid.Cid type to a Path type. -func FromCid(c cid.Cid) Path { - return Path("/ipfs/" + c.String()) +func (p *path) Namespace() string { + return p.namespace } -// Segments returns the different elements of a path -// (elements are delimited by a /). -func (p Path) Segments() []string { - cleaned := path.Clean(string(p)) - segments := strings.Split(cleaned, "/") +func (p *path) Mutable() bool { + return p.namespace == IPNSNamespace +} + +func (p *path) Root() cid.Cid { + return p.root +} + +func (p *path) Segments() []string { + segments := strings.Split(p.str, "/") // Ignore leading slash if len(segments[0]) == 0 { @@ -44,147 +98,200 @@ func (p Path) Segments() []string { return segments } -// String converts a path to string. -func (p Path) String() string { - return string(p) +type resolvedPath struct { + path + cid cid.Cid + remainder string } -// 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 *resolvedPath) Cid() cid.Cid { + return p.cid } -// PopLastSegment returns a new Path without its final segment, and the final -// segment, separately. If there is no more to pop (the path is just a key), -// the original path is returned. -func (p Path) PopLastSegment() (Path, string, error) { +func (p *resolvedPath) Remainder() string { + return p.remainder +} - if p.IsJustAKey() { - return p, "", nil +// 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) { + str = gopath.Clean(str) + components := strings.Split(str, "/") + + // 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: str, + 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} - } - case "ipns": - if parts[2] == "" { - return "", &ErrInvalidPath{error: fmt.Errorf("not enough path components"), path: txt} + return nil, &ErrInvalidPath{error: fmt.Errorf("cid is invalid: %w", err), path: str} } - default: - return "", &ErrInvalidPath{error: fmt.Errorf("unknown namespace %q", parts[1]), path: txt} - } - return Path(txt), nil -} + return &path{ + str: str, + root: root, + namespace: components[1], + }, nil + case IPNSNamespace: + if components[2] == "" { + return nil, &ErrInvalidPath{error: fmt.Errorf("not enough path components"), path: str} + } -// 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} - } + var root cid.Cid + pid, err := peer.Decode(components[2]) + if err != nil { + // DNSLink. + root = cid.Undef + } else { + root = peer.ToCid(pid) + } - c, err := decodeCid(txt) - if err != nil { - return "", &ErrInvalidPath{error: err, path: txt} + return &path{ + str: str, + root: root, + namespace: IPFSNamespace, + }, nil + default: + return nil, &ErrInvalidPath{error: fmt.Errorf("unknown namespace %q", components[1]), path: str} } - - return FromCid(c), nil } -// IsValid checks if a path is a valid ipfs Path. -func (p *Path) IsValid() error { - _, err := ParsePath(p.String()) - return err -} +// 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:] + } + } -// Join joins strings slices using / -func Join(pths []string) string { - return strings.Join(pths, "/") + return NewPath("/" + strings.Join(segments, "/")) } -// SplitList splits strings usings / -func SplitList(pth string) []string { - return strings.Split(pth, "/") -} +// 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. -// 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) +func Join(p Path, segments ...string) (Path, error) { + s := p.Segments() + s = append(s, segments...) + return NewPathFromSegments(s...) +} + +func NewResolvedPath(p Path, cid cid.Cid, remainder string) ResolvedPath { + return &resolvedPath{ + path: path{ + str: fmt.Sprintf("%s/%s", p.String(), remainder), + root: p.Root(), + namespace: p.Namespace(), + }, + cid: cid, + remainder: remainder, } - return c, err } diff --git a/path/path_test.go b/path/path_test.go index 2b26a56786..8f880ef490 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 8192a9eb0b..c9d00651ae 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 } @@ -198,13 +193,7 @@ func (r *basicResolver) ResolvePathComponents(ctx context.Context, fpath path.Pa evt := log.EventBegin(ctx, "resolvePathComponents", logging.LoggableMap{"fpath": fpath}) defer evt.Done() - // validate path - if err := fpath.IsValid(); err != nil { - evt.Append(logging.LoggableMap{"error": err.Error()}) - return nil, err - } - - c, p, err := path.SplitAbsPath(fpath) + c, p, err := path.SplitImmutablePath(fpath) if err != nil { evt.Append(logging.LoggableMap{"error": err.Error()}) return nil, err diff --git a/path/resolver/resolver_test.go b/path/resolver/resolver_test.go index c91d950f1b..8137c37681 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))