From 1c53e8a3830e9885de7a6e6bd022881051d58e87 Mon Sep 17 00:00:00 2001 From: Adin Schmahmann Date: Wed, 24 May 2023 20:56:15 -0400 Subject: [PATCH] feat: add more CAR tests --- .../t0118-multiblock-file-in-directory.car | Bin 0 -> 1657 bytes fixtures/t0118-one-layer-hamt.car | Bin 0 -> 84273 bytes go.mod | 3 +- go.sum | 8 ++ tests/t0118_gateway_car_test.go | 114 +++++++++++++---- tooling/car/unixfs.go | 116 +++++++++++++++++- 6 files changed, 212 insertions(+), 29 deletions(-) create mode 100644 fixtures/t0118-multiblock-file-in-directory.car create mode 100644 fixtures/t0118-one-layer-hamt.car diff --git a/fixtures/t0118-multiblock-file-in-directory.car b/fixtures/t0118-multiblock-file-in-directory.car new file mode 100644 index 0000000000000000000000000000000000000000..f32f10fd89f1c58298feefc67f57fe1c20393354 GIT binary patch literal 1657 zcma)+dx%t39LL?uLesPujT>&P(vPMhY?!d1u*AiE^q|&_Vns5AGv}U}J$fG3^SDV+ zZWgoKUWvbQ#V`pUXPqciWGfW_T^KlR|?>8W&idGFiPr+%2) z)JU1JT&pg+km%B?;?*JUv*FhU-rHB5Jv?46^d5Xd4lKKFI_mEI=gRXtx6m6Oex|fo zdf|9YXu7rPw!6MF7Z;;K*UQm@E?vN;i1FLTsi8uT=^NHNN>E~U04jlXC z@WwSqca5*PZ%JuU-(*zEgU^+ZbkDAg9;qK+yI)*-?)~xdE$^HjJ9*}X&fc?|=0~&ynZH9{sq`V+MBrvATT8U(vTO zp8an4fu&!6wQ}}x433IR3Q^zA0!&7ozv#_qAtT?T4U@nV>m|fmXaklzAd#jp^!S;pTryVDqw~) zWw6))wytRt01k{*+C!$ObG|`USQ|DB$B;a~-)@t6sPP(dg*%reK#Nm?pyEc*Y+ah} zwM1?DiS=VZB11nNUMnb5D4H*(M4w5ASsa52fq*iL&gLZG-r&P>ZFrXEd(~X^e^2~- AQ2+n{ literal 0 HcmV?d00001 diff --git a/fixtures/t0118-one-layer-hamt.car b/fixtures/t0118-one-layer-hamt.car new file mode 100644 index 0000000000000000000000000000000000000000..bc2ae7554ef07da1b3181c52d0e3b9f61054697a GIT binary patch literal 84273 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 y66$}9U44e0Pf+7PP{ip+=^Hh^|7sFrx literal 0 HcmV?d00001 diff --git a/go.mod b/go.mod index 47500e3c6..f83e45bd2 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.20 require ( github.com/ipfs/boxo v0.8.2-0.20230510114019-33e3f0cd052b github.com/ipfs/go-cid v0.4.1 + github.com/ipfs/go-unixfsnode v1.6.0 github.com/ipld/go-ipld-prime v0.20.0 github.com/libp2p/go-libp2p v0.26.3 github.com/stretchr/testify v1.8.2 @@ -33,7 +34,7 @@ require ( require ( github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/gogo/protobuf v1.3.2 + github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.3.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/ipfs/bbloom v0.0.4 // indirect diff --git a/go.sum b/go.sum index cf7a061c8..c897a087c 100644 --- a/go.sum +++ b/go.sum @@ -55,6 +55,7 @@ github.com/ipfs/go-block-format v0.0.2/go.mod h1:AWR46JfpcObNfg3ok2JHDUfdiHRgWhJ github.com/ipfs/go-block-format v0.0.3/go.mod h1:4LmD4ZUw0mhO+JSKdpWwrzATiEfM7WWgQ8H5l6P8MVk= github.com/ipfs/go-block-format v0.1.2 h1:GAjkfhVx1f4YTODS6Esrj1wt2HhrtwTnhEr+DyPUaJo= github.com/ipfs/go-block-format v0.1.2/go.mod h1:mACVcrxarQKstUU3Yf/RdwbC4DzPV6++rO2a3d+a/KE= +github.com/ipfs/go-blockservice v0.5.0 h1:B2mwhhhVQl2ntW2EIpaWPwSCxSuqr5fFA93Ms4bYLEY= github.com/ipfs/go-cid v0.0.1/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM= github.com/ipfs/go-cid v0.0.2/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM= github.com/ipfs/go-cid v0.0.3/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM= @@ -71,6 +72,8 @@ github.com/ipfs/go-ipfs-blocksutil v0.0.1 h1:Eh/H4pc1hsvhzsQoMEP3Bke/aW5P5rVM1IW github.com/ipfs/go-ipfs-chunker v0.0.5 h1:ojCf7HV/m+uS2vhUGWcogIIxiO5ubl5O57Q7NapWLY8= github.com/ipfs/go-ipfs-delay v0.0.1 h1:r/UXYyRcddO6thwOnhiznIAiSvxMECGgtv35Xs1IeRQ= github.com/ipfs/go-ipfs-ds-help v1.1.0 h1:yLE2w9RAsl31LtfMt91tRZcrx+e61O5mDxFRR994w4Q= +github.com/ipfs/go-ipfs-exchange-interface v0.2.0 h1:8lMSJmKogZYNo2jjhUs0izT+dck05pqUw4mWNW9Pw6Y= +github.com/ipfs/go-ipfs-exchange-offline v0.3.0 h1:c/Dg8GDPzixGd0MC8Jh6mjOwU57uYokgWRFidfvEkuA= github.com/ipfs/go-ipfs-pq v0.0.3 h1:YpoHVJB+jzK15mr/xsWC574tyDLkezVrDNeaalQBsTE= github.com/ipfs/go-ipfs-util v0.0.1/go.mod h1:spsl5z8KUnrve+73pOhSVZND1SIxPW5RyBCNzQxlJBc= github.com/ipfs/go-ipfs-util v0.0.2 h1:59Sswnk1MFaiq+VcaknX7aYEyGyGDAA73ilhEK2POp8= @@ -88,10 +91,15 @@ github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JP github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= +github.com/ipfs/go-merkledag v0.10.0 h1:IUQhj/kzTZfam4e+LnaEpoiZ9vZF6ldimVlby+6OXL4= github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg= github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY= github.com/ipfs/go-peertaskqueue v0.8.1 h1:YhxAs1+wxb5jk7RvS0LHdyiILpNmRIRnZVztekOF0pg= +github.com/ipfs/go-unixfs v0.4.5 h1:wj8JhxvV1G6CD7swACwSKYa+NgtdWC1RUit+gFnymDU= github.com/ipfs/go-unixfsnode v1.6.0 h1:JOSA02yaLylRNi2rlB4ldPr5VcZhcnaIVj5zNLcOjDo= +github.com/ipfs/go-unixfsnode v1.6.0/go.mod h1:PVfoyZkX1B34qzT3vJO4nsLUpRCyhnMuHBznRcXirlk= +github.com/ipfs/go-verifcid v0.0.2 h1:XPnUv0XmdH+ZIhLGKg6U2vaPaRDXb9urMyNVCE7uvTs= +github.com/ipld/go-car/v2 v2.9.1-0.20230325062757-fff0e4397a3d h1:22g+x1tgWSXK34i25qjs+afr7basaneEkHaglBshd2g= github.com/ipld/go-codec-dagpb v1.6.0 h1:9nYazfyu9B1p3NAgfVdpRco3Fs2nFC72DqVsMj6rOcc= github.com/ipld/go-codec-dagpb v1.6.0/go.mod h1:ANzFhfP2uMJxRBr8CE+WQWs5UsNa0pYtmKZ+agnUw9s= github.com/ipld/go-ipld-prime v0.9.1-0.20210324083106-dc342a9917db/go.mod h1:KvBLMr4PX1gWptgkzRjVZCrLmSGcZCb/jioOQwCqZN8= diff --git a/tests/t0118_gateway_car_test.go b/tests/t0118_gateway_car_test.go index 9ab4aaac6..4fcca3976 100644 --- a/tests/t0118_gateway_car_test.go +++ b/tests/t0118_gateway_car_test.go @@ -11,6 +11,7 @@ import ( func TestGatewayCar(t *testing.T) { fixture := car.MustOpenUnixfsCar("t0118-test-dag.car") + oneLayerHAMTFixture := car.MustOpenUnixfsCar("t0118-one-layer-hamt.car") tests := SugarTests{ { @@ -67,6 +68,30 @@ func TestGatewayCar(t *testing.T) { InThatOrder(), ), }, + { + Name: "GET CAR with dag-scope=block pathing through a sharded directory", + Hint: ` + dag-scope=block should return a CAR file with only the root block and a + block for each optional path component. Pathing through a sharded directory should return + the blocks needed for the traversal, not the entire HAMT and not skipping all intermediate nodes + `, + Request: Request(). + Path("ipfs/{{cid}}/1.txt", oneLayerHAMTFixture.MustGetCid()). + Query("format", "car"). + Query("dag-scope", "block"), + Response: Expect(). + Status(200). + Body( + IsCar(). + HasRoot(oneLayerHAMTFixture.MustGetCid()). + HasBlocks(flattenStrings(t, + oneLayerHAMTFixture.MustGetCid(), + oneLayerHAMTFixture.MustGetCIDsInHAMTTraversal(nil, "1.txt"))..., + ). + Exactly(). + InThatOrder(), + ), + }, { Name: "GET CAR with dag-scope=entity", Hint: ` @@ -90,6 +115,30 @@ func TestGatewayCar(t *testing.T) { InThatOrder(), ), }, + { + Name: "GET CAR with dag-scope=entity for a sharded directory", + Hint: ` + dag-scope=entity for a sharded directory should return a CAR file with all of the path blocks as well + as all of the blocks in the HAMT, but not any of blocks below the HAMT. + `, + Request: Request(). + Path("ipfs/{{cid}}", oneLayerHAMTFixture.MustGetCid()). + Query("format", "car"). + Query("dag-scope", "entity"), + Response: Expect(). + Status(200). + Body( + IsCar(). + HasRoot(oneLayerHAMTFixture.MustGetCid()). + HasBlocks( + flattenStrings(t, + oneLayerHAMTFixture.MustGetCid(), + oneLayerHAMTFixture.MustGetCidsInHAMT())..., + ). + Exactly(). + InThatOrder(), + ), + }, { Name: "GET CAR with dag-scope=all", Hint: ` @@ -122,7 +171,7 @@ func TestGatewayCar(t *testing.T) { func TestGatewayCarEntityBytes(t *testing.T) { multiBlockFileInDirFixture := car.MustOpenUnixfsCar("t0118-multiblock-file-in-directory.car") - fixture := multiBlockFileInDirFixture + oneLayerHAMTFixture := car.MustOpenUnixfsCar("t0118-one-layer-hamt.car") tests := SugarTests{ { @@ -132,7 +181,7 @@ func TestGatewayCarEntityBytes(t *testing.T) { the full UnixFS file at the end of the specified path `, Request: Request(). - Path("ipfs/{{cid}}", multiBlockFileInDirFixture.MustGetCid("multiblock.txt")). + Path("ipfs/{{cid}}/multiblock.txt", multiBlockFileInDirFixture.MustGetCid()). Query("format", "car"). Query("dag-scope", "entity"). Query("entity-bytes", "0:*"), @@ -144,7 +193,7 @@ func TestGatewayCarEntityBytes(t *testing.T) { HasBlocks(flattenStrings(t, multiBlockFileInDirFixture.MustGetCid(), multiBlockFileInDirFixture.MustGetCid("multiblock.txt"), - multiBlockFileInDirFixture.MustGetChildrenCids("multiblock.txt"), + multiBlockFileInDirFixture.MustGetIPLDChildrenCids("multiblock.txt"), )...). Exactly(). InThatOrder(), @@ -158,16 +207,19 @@ func TestGatewayCarEntityBytes(t *testing.T) { (i.e. entity-bytes is effectively optional if the entity is not a file) `, Request: Request(). - Path("ipfs/{{cid}}", fixture.MustGetCid()). + Path("ipfs/{{cid}}", oneLayerHAMTFixture.MustGetCid()). Query("format", "car"). - Query("dag-scope", "entity"), + Query("dag-scope", "entity"). + Query("entity-bytes", "0:*"), Response: Expect(). Status(200). Body( IsCar(). - HasRoot(fixture.MustGetCid()). + HasRoot(oneLayerHAMTFixture.MustGetCid()). HasBlocks( - fixture.MustGetCid(), + flattenStrings(t, + oneLayerHAMTFixture.MustGetCid(), + oneLayerHAMTFixture.MustGetCidsInHAMT())..., ). Exactly(). InThatOrder(), @@ -179,58 +231,67 @@ func TestGatewayCarEntityBytes(t *testing.T) { The response MUST contain only the minimal set of blocks necessary for fulfilling the range request `, Request: Request(). - Path("ipfs/{{cid}}", fixture.MustGetCid()). + Path("ipfs/{{cid}}", multiBlockFileInDirFixture.MustGetCid("multiblock.txt")). Query("format", "car"). - Query("dag-scope", "entity"), + Query("dag-scope", "entity"). + Query("entity-bytes", "512:*"), Response: Expect(). Status(200). Body( IsCar(). - HasRoot(fixture.MustGetCid()). + HasRoot(multiBlockFileInDirFixture.MustGetCid("multiblock.txt")). HasBlocks( - fixture.MustGetCid(), + flattenStrings(t, + multiBlockFileInDirFixture.MustGetCid("multiblock.txt"), + multiBlockFileInDirFixture.MustGetIPLDChildrenCids("multiblock.txt")[2:])..., ). Exactly(). InThatOrder(), ), }, { - Name: "GET CAR with entity-bytes equivalent to a HTTP Range Request for the middle of a large file", + Name: "GET CAR with entity-bytes equivalent to a HTTP Range Request for the middle of a file", Hint: ` The response MUST contain only the minimal set of blocks necessary for fulfilling the range request `, Request: Request(). - Path("ipfs/{{cid}}", fixture.MustGetCid()). + Path("ipfs/{{cid}}", multiBlockFileInDirFixture.MustGetCid("multiblock.txt")). Query("format", "car"). - Query("dag-scope", "entity"), + Query("dag-scope", "entity"). + Query("entity-bytes", "512:1024"), Response: Expect(). Status(200). Body( IsCar(). - HasRoot(fixture.MustGetCid()). + HasRoot(multiBlockFileInDirFixture.MustGetCid("multiblock.txt")). HasBlocks( - fixture.MustGetCid(), + flattenStrings(t, + multiBlockFileInDirFixture.MustGetCid("multiblock.txt"), + multiBlockFileInDirFixture.MustGetIPLDChildrenCids("multiblock.txt")[2:4])..., ). Exactly(). InThatOrder(), ), }, { - Name: "GET CAR with entity-bytes equivalent to HTTP Suffix Range Request for part of a small file", + Name: "GET CAR with entity-bytes equivalent to HTTP Suffix Range Request for part of a file", Hint: ` The response MUST contain only the minimal set of blocks necessary for fulfilling the range request `, Request: Request(). - Path("ipfs/{{cid}}", fixture.MustGetCid()). + Path("ipfs/{{cid}}", multiBlockFileInDirFixture.MustGetCid("multiblock.txt")). Query("format", "car"). - Query("dag-scope", "entity"), + Query("dag-scope", "entity"). + Query("entity-bytes", "-5:*"), Response: Expect(). Status(200). Body( IsCar(). - HasRoot(fixture.MustGetCid()). + HasRoot(multiBlockFileInDirFixture.MustGetCid("multiblock.txt")). HasBlocks( - fixture.MustGetCid(), + flattenStrings(t, + multiBlockFileInDirFixture.MustGetCid("multiblock.txt"), + multiBlockFileInDirFixture.MustGetIPLDChildrenCids("multiblock.txt")[3:])..., ). Exactly(). InThatOrder(), @@ -242,16 +303,19 @@ func TestGatewayCarEntityBytes(t *testing.T) { The response MUST contain only the minimal set of blocks necessary for fulfilling the range request `, Request: Request(). - Path("ipfs/{{cid}}", fixture.MustGetCid()). + Path("ipfs/{{cid}}", multiBlockFileInDirFixture.MustGetCid("multiblock.txt")). Query("format", "car"). - Query("dag-scope", "entity"), + Query("dag-scope", "entity"). + Query("entity-bytes", "-999999:-3"), Response: Expect(). Status(200). Body( IsCar(). - HasRoot(fixture.MustGetCid()). + HasRoot(multiBlockFileInDirFixture.MustGetCid("multiblock.txt")). HasBlocks( - fixture.MustGetCid(), + flattenStrings(t, + multiBlockFileInDirFixture.MustGetCid("multiblock.txt"), + multiBlockFileInDirFixture.MustGetIPLDChildrenCids("multiblock.txt")[:5])..., ). Exactly(). InThatOrder(), diff --git a/tooling/car/unixfs.go b/tooling/car/unixfs.go index 90437f1ca..5bdf0fa1a 100644 --- a/tooling/car/unixfs.go +++ b/tooling/car/unixfs.go @@ -7,25 +7,34 @@ import ( "bytes" "context" "fmt" + dagpb "github.com/ipld/go-codec-dagpb" + "io" "os" "path" "sort" "strings" + "sync" "github.com/ipfs/boxo/blockservice" "github.com/ipfs/boxo/ipld/car/v2/blockstore" "github.com/ipfs/boxo/ipld/merkledag" - "github.com/ipfs/boxo/ipld/unixfs/io" - "github.com/ipfs/gateway-conformance/tooling/fixtures" + "github.com/ipfs/boxo/ipld/unixfs/hamt" + uio "github.com/ipfs/boxo/ipld/unixfs/io" "github.com/ipfs/go-cid" format "github.com/ipfs/go-ipld-format" + "github.com/ipfs/go-unixfsnode" "github.com/ipld/go-ipld-prime" _ "github.com/ipld/go-ipld-prime/codec/cbor" _ "github.com/ipld/go-ipld-prime/codec/dagcbor" _ "github.com/ipld/go-ipld-prime/codec/dagjson" _ "github.com/ipld/go-ipld-prime/codec/json" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/linking" + cidlink "github.com/ipld/go-ipld-prime/linking/cid" "github.com/ipld/go-ipld-prime/multicodec" mc "github.com/multiformats/go-multicodec" + + "github.com/ipfs/gateway-conformance/tooling/fixtures" ) type UnixfsDag struct { @@ -54,7 +63,7 @@ func newUnixfsDagFromCar(file string) (*UnixfsDag, error) { func (d *UnixfsDag) loadLinks(node format.Node) (map[string]*UnixfsDag, error) { result := make(map[string]*UnixfsDag) - dir, err := io.NewDirectoryFromNode(d.dsvc, node) + dir, err := uio.NewDirectoryFromNode(d.dsvc, node) if err != nil { return nil, err } @@ -178,6 +187,72 @@ func (d *UnixfsDag) MustGetChildrenCids(names ...string) []string { return cids } +func (d *UnixfsDag) MustGetIPLDChildrenCids(names ...string) []string { + node := d.MustGetNode(names...) + lnks := node.node.Links() + var cids []string + for _, l := range lnks { + cids = append(cids, l.Cid.String()) + } + return cids +} + +// MustGetCidsInHAMT returns the cids in the HAMT at the given path. Does not include the CID of the HAMT root +func (d *UnixfsDag) MustGetCidsInHAMT(names ...string) []string { + node := d.MustGetNode(names...) + var cids []string + tracker := dservTrackingWrapper{ + DAGService: node.dsvc, + } + + lsys := cidlink.DefaultLinkSystem() + unixfsnode.AddUnixFSReificationToLinkSystem(&lsys) + lsys.StorageReadOpener = func(linkContext linking.LinkContext, link datamodel.Link) (io.Reader, error) { + nd, err := tracker.Get(linkContext.Ctx, link.(cidlink.Link).Cid) + if err != nil { + return nil, err + } + return bytes.NewReader(nd.RawData()), nil + } + + primeNodeBuilder := dagpb.Type.PBNode.NewBuilder() + err := dagpb.DecodeBytes(primeNodeBuilder, node.node.RawData()) + if err != nil { + panic(err) + } + primeNode := primeNodeBuilder.Build() + _, err = lsys.KnownReifiers["unixfs-preload"](linking.LinkContext{}, primeNode, &lsys) + if err != nil { + panic(err) + } + + for _, c := range tracker.requestedCids { + cids = append(cids, c.String()) + } + return cids +} + +// MustGetCIDsInHAMTTraversal returns the cids needed for a given HAMT traversal. Does not include the HAMT root. +func (d *UnixfsDag) MustGetCIDsInHAMTTraversal(path []string, child string) []string { + node := d.MustGetNode(path...) + var cids []string + tracker := dservTrackingWrapper{ + DAGService: node.dsvc, + } + h, err := hamt.NewHamtFromDag(&tracker, node.node) + if err != nil { + panic(err) + } + _, err = h.Find(context.Background(), child) + if err != nil { + panic(err) + } + for _, c := range tracker.requestedCids { + cids = append(cids, c.String()) + } + return cids +} + func (d *UnixfsDag) MustGetRoot() *FixtureNode { return d.MustGetNode() } @@ -231,3 +306,38 @@ func MustOpenUnixfsCar(file string) *UnixfsDag { } return dag } + +type dservTrackingWrapper struct { + format.DAGService + reqMx sync.Mutex + requestedCids []cid.Cid +} + +func (d *dservTrackingWrapper) Get(ctx context.Context, c cid.Cid) (format.Node, error) { + nd, err := d.DAGService.Get(ctx, c) + if err != nil { + return nil, err + } + d.reqMx.Lock() + d.requestedCids = append(d.requestedCids, c) + d.reqMx.Unlock() + return nd, nil +} + +func (d *dservTrackingWrapper) GetMany(ctx context.Context, cids []cid.Cid) <-chan *format.NodeOption { + innerCh := d.DAGService.GetMany(ctx, cids) + outCh := make(chan *format.NodeOption, 1) + go func() { + defer close(outCh) + for i := range innerCh { + if i.Err == nil { + c := i.Node.Cid() + d.reqMx.Lock() + d.requestedCids = append(d.requestedCids, c) + d.reqMx.Unlock() + } + outCh <- i + } + }() + return outCh +}