master
go 1,085 lines 25.6 KB
Raw
1 package tests
2
3 import (
4 "bytes"
5 "context"
6 "encoding/hex"
7 "fmt"
8 "io"
9 "math"
10 "math/rand"
11 "os"
12 "strconv"
13 "strings"
14 "sync"
15 "testing"
16
17 "github.com/ipfs/boxo/path"
18 coreiface "github.com/ipfs/kubo/core/coreiface"
19 "github.com/ipfs/kubo/core/coreiface/options"
20
21 "github.com/ipfs/boxo/files"
22 mdag "github.com/ipfs/boxo/ipld/merkledag"
23 "github.com/ipfs/boxo/ipld/unixfs"
24 "github.com/ipfs/boxo/ipld/unixfs/importer/helpers"
25 "github.com/ipfs/go-cid"
26 cbor "github.com/ipfs/go-ipld-cbor"
27 ipld "github.com/ipfs/go-ipld-format"
28 mh "github.com/multiformats/go-multihash"
29 )
30
31 func (tp *TestSuite) TestUnixfs(t *testing.T) {
32 tp.hasApi(t, func(api coreiface.CoreAPI) error {
33 if api.Unixfs() == nil {
34 return errAPINotImplemented
35 }
36 return nil
37 })
38
39 t.Run("TestAdd", tp.TestAdd)
40 t.Run("TestAddPinned", tp.TestAddPinned)
41 t.Run("TestAddHashOnly", tp.TestAddHashOnly)
42 t.Run("TestGetEmptyFile", tp.TestGetEmptyFile)
43 t.Run("TestGetDir", tp.TestGetDir)
44 t.Run("TestGetNonUnixfs", tp.TestGetNonUnixfs)
45 t.Run("TestLs", tp.TestLs)
46 t.Run("TestEntriesExpired", tp.TestEntriesExpired)
47 t.Run("TestLsEmptyDir", tp.TestLsEmptyDir)
48 t.Run("TestLsNonUnixfs", tp.TestLsNonUnixfs)
49 t.Run("TestAddCloses", tp.TestAddCloses)
50 t.Run("TestGetSeek", tp.TestGetSeek)
51 t.Run("TestGetReadAt", tp.TestGetReadAt)
52 }
53
54 // `echo -n 'hello, world!' | ipfs add`
55 var (
56 hello = "/ipfs/QmQy2Dw4Wk7rdJKjThjYXzfFJNaRKRHhHP5gHHXroJMYxk"
57 helloStr = "hello, world!"
58 )
59
60 // `echo -n | ipfs add`
61 var emptyFile = "/ipfs/QmbFMke1KXqnYyBBWxB74N4c5SBnJMVAiMNRcGu6x1AwQH"
62
63 func strFile(data string) func() files.Node {
64 return func() files.Node {
65 return files.NewBytesFile([]byte(data))
66 }
67 }
68
69 func twoLevelDir() func() files.Node {
70 return func() files.Node {
71 return files.NewMapDirectory(map[string]files.Node{
72 "abc": files.NewMapDirectory(map[string]files.Node{
73 "def": files.NewBytesFile([]byte("world")),
74 }),
75
76 "bar": files.NewBytesFile([]byte("hello2")),
77 "foo": files.NewBytesFile([]byte("hello1")),
78 })
79 }
80 }
81
82 func flatDir() files.Node {
83 return files.NewMapDirectory(map[string]files.Node{
84 "bar": files.NewBytesFile([]byte("hello2")),
85 "foo": files.NewBytesFile([]byte("hello1")),
86 })
87 }
88
89 func wrapped(names ...string) func(f files.Node) files.Node {
90 return func(f files.Node) files.Node {
91 for i := range names {
92 f = files.NewMapDirectory(map[string]files.Node{
93 names[len(names)-i-1]: f,
94 })
95 }
96 return f
97 }
98 }
99
100 func (tp *TestSuite) TestAdd(t *testing.T) {
101 ctx := t.Context()
102 api, err := tp.makeAPI(t, ctx)
103 if err != nil {
104 t.Fatal(err)
105 }
106
107 p := func(h string) path.ImmutablePath {
108 c, err := cid.Parse(h)
109 if err != nil {
110 t.Fatal(err)
111 }
112 return path.FromCid(c)
113 }
114
115 rf, err := os.CreateTemp(os.TempDir(), "unixfs-add-real")
116 if err != nil {
117 t.Fatal(err)
118 }
119 rfp := rf.Name()
120
121 if _, err := rf.Write([]byte(helloStr)); err != nil {
122 t.Fatal(err)
123 }
124
125 stat, err := rf.Stat()
126 if err != nil {
127 t.Fatal(err)
128 }
129
130 if err := rf.Close(); err != nil {
131 t.Fatal(err)
132 }
133 defer os.Remove(rfp)
134
135 realFile := func() files.Node {
136 n, err := files.NewReaderPathFile(rfp, io.NopCloser(strings.NewReader(helloStr)), stat)
137 if err != nil {
138 t.Fatal(err)
139 }
140 return n
141 }
142
143 cases := []struct {
144 name string
145 data func() files.Node
146 expect func(files.Node) files.Node
147
148 apiOpts []options.ApiOption
149
150 path string
151 err string
152
153 wrap string
154
155 events []coreiface.AddEvent
156
157 opts []options.UnixfsAddOption
158 }{
159 // Simple cases
160 {
161 name: "simpleAdd",
162 data: strFile(helloStr),
163 path: hello,
164 opts: []options.UnixfsAddOption{},
165 },
166 {
167 name: "addEmpty",
168 data: strFile(""),
169 path: emptyFile,
170 },
171 // CIDv1 version / rawLeaves
172 {
173 name: "addCidV1",
174 data: strFile(helloStr),
175 path: "/ipfs/bafkreidi4zlleupgp2bvrpxyja5lbvi4mym7hz5bvhyoowby2qp7g2hxfa",
176 opts: []options.UnixfsAddOption{options.Unixfs.CidVersion(1)},
177 },
178 {
179 name: "addCidV1NoLeaves",
180 data: strFile(helloStr),
181 path: "/ipfs/bafybeibhbcn7k7o2m6xsqkrlfiokod3nxwe47viteynhruh6uqx7hvkjfu",
182 opts: []options.UnixfsAddOption{options.Unixfs.CidVersion(1), options.Unixfs.RawLeaves(false)},
183 },
184 // Non sha256 hash vs CID
185 {
186 name: "addCidSha3",
187 data: strFile(helloStr),
188 path: "/ipfs/bafkrmichjflejeh6aren53o7pig7zk3m3vxqcoc2i5dv326k3x6obh7jry",
189 opts: []options.UnixfsAddOption{options.Unixfs.Hash(mh.SHA3_256)},
190 },
191 {
192 name: "addCidSha3Cid0",
193 data: strFile(helloStr),
194 err: "CIDv0 only supports sha2-256",
195 opts: []options.UnixfsAddOption{options.Unixfs.CidVersion(0), options.Unixfs.Hash(mh.SHA3_256)},
196 },
197 // Inline
198 {
199 name: "addInline",
200 data: strFile(helloStr),
201 path: "/ipfs/bafyaafikcmeaeeqnnbswy3dpfqqho33snrsccgan",
202 opts: []options.UnixfsAddOption{options.Unixfs.Inline(true)},
203 },
204 {
205 name: "addInlineLimit",
206 data: strFile(helloStr),
207 path: "/ipfs/bafyaafikcmeaeeqnnbswy3dpfqqho33snrsccgan",
208 opts: []options.UnixfsAddOption{options.Unixfs.InlineLimit(32), options.Unixfs.Inline(true)},
209 },
210 {
211 name: "addInlineZero",
212 data: strFile(""),
213 path: "/ipfs/bafkqaaa",
214 opts: []options.UnixfsAddOption{options.Unixfs.InlineLimit(0), options.Unixfs.Inline(true), options.Unixfs.RawLeaves(true)},
215 },
216 { // TODO: after coreapi add is used in `ipfs add`, consider making this default for inline
217 name: "addInlineRaw",
218 data: strFile(helloStr),
219 path: "/ipfs/bafkqadlimvwgy3zmeb3w64tmmqqq",
220 opts: []options.UnixfsAddOption{options.Unixfs.InlineLimit(32), options.Unixfs.Inline(true), options.Unixfs.RawLeaves(true)},
221 },
222 // Chunker / Layout
223 {
224 name: "addChunks",
225 data: strFile(strings.Repeat("aoeuidhtns", 200)),
226 path: "/ipfs/QmRo11d4QJrST47aaiGVJYwPhoNA4ihRpJ5WaxBWjWDwbX",
227 opts: []options.UnixfsAddOption{options.Unixfs.Chunker("size-4")},
228 },
229 {
230 name: "addChunksTrickle",
231 data: strFile(strings.Repeat("aoeuidhtns", 200)),
232 path: "/ipfs/QmNNhDGttafX3M1wKWixGre6PrLFGjnoPEDXjBYpTv93HP",
233 opts: []options.UnixfsAddOption{options.Unixfs.Chunker("size-4"), options.Unixfs.Layout(options.TrickleLayout)},
234 },
235 // Local
236 {
237 name: "addLocal", // better cases in sharness
238 data: strFile(helloStr),
239 path: hello,
240 apiOpts: []options.ApiOption{options.Api.Offline(true)},
241 },
242 {
243 name: "hashOnly", // test (non)fetchability
244 data: strFile(helloStr),
245 path: hello,
246 opts: []options.UnixfsAddOption{options.Unixfs.HashOnly(true)},
247 },
248 // multi file
249 {
250 name: "simpleDirNoWrap",
251 data: flatDir,
252 path: "/ipfs/QmRKGpFfR32FVXdvJiHfo4WJ5TDYBsM1P9raAp1p6APWSp",
253 },
254 {
255 name: "simpleDir",
256 data: flatDir,
257 wrap: "t",
258 expect: wrapped("t"),
259 path: "/ipfs/Qmc3nGXm1HtUVCmnXLQHvWcNwfdZGpfg2SRm1CxLf7Q2Rm",
260 },
261 {
262 name: "twoLevelDir",
263 data: twoLevelDir(),
264 wrap: "t",
265 expect: wrapped("t"),
266 path: "/ipfs/QmPwsL3T5sWhDmmAWZHAzyjKtMVDS9a11aHNRqb3xoVnmg",
267 },
268 // wrapped
269 {
270 name: "addWrapped",
271 path: "/ipfs/QmVE9rNpj5doj7XHzp5zMUxD7BJgXEqx4pe3xZ3JBReWHE",
272 data: func() files.Node {
273 return files.NewBytesFile([]byte(helloStr))
274 },
275 wrap: "foo",
276 expect: wrapped("foo"),
277 },
278 // hidden
279 {
280 name: "hiddenFilesAdded",
281 data: func() files.Node {
282 return files.NewMapDirectory(map[string]files.Node{
283 ".bar": files.NewBytesFile([]byte("hello2")),
284 "bar": files.NewBytesFile([]byte("hello2")),
285 "foo": files.NewBytesFile([]byte("hello1")),
286 })
287 },
288 wrap: "t",
289 expect: wrapped("t"),
290 path: "/ipfs/QmPXLSBX382vJDLrGakcbrZDkU3grfkjMox7EgSC9KFbtQ",
291 },
292 // NoCopy
293 {
294 name: "simpleNoCopy",
295 data: realFile,
296 path: "/ipfs/bafkreidi4zlleupgp2bvrpxyja5lbvi4mym7hz5bvhyoowby2qp7g2hxfa",
297 opts: []options.UnixfsAddOption{options.Unixfs.Nocopy(true)},
298 },
299 {
300 name: "noCopyNoRaw",
301 data: realFile,
302 path: "/ipfs/bafkreidi4zlleupgp2bvrpxyja5lbvi4mym7hz5bvhyoowby2qp7g2hxfa",
303 opts: []options.UnixfsAddOption{options.Unixfs.Nocopy(true), options.Unixfs.RawLeaves(false)},
304 err: "nocopy option requires '--raw-leaves' to be enabled as well",
305 },
306 {
307 name: "noCopyNoPath",
308 data: strFile(helloStr),
309 path: "/ipfs/bafkreidi4zlleupgp2bvrpxyja5lbvi4mym7hz5bvhyoowby2qp7g2hxfa",
310 opts: []options.UnixfsAddOption{options.Unixfs.Nocopy(true)},
311 err: helpers.ErrMissingFsRef.Error(),
312 },
313 // Events / Progress
314 {
315 name: "simpleAddEvent",
316 data: strFile(helloStr),
317 path: "/ipfs/bafkreidi4zlleupgp2bvrpxyja5lbvi4mym7hz5bvhyoowby2qp7g2hxfa",
318 events: []coreiface.AddEvent{
319 {Name: "bafkreidi4zlleupgp2bvrpxyja5lbvi4mym7hz5bvhyoowby2qp7g2hxfa", Path: p("bafkreidi4zlleupgp2bvrpxyja5lbvi4mym7hz5bvhyoowby2qp7g2hxfa"), Size: strconv.Itoa(len(helloStr))},
320 },
321 opts: []options.UnixfsAddOption{options.Unixfs.RawLeaves(true)},
322 },
323 {
324 name: "silentAddEvent",
325 data: twoLevelDir(),
326 path: "/ipfs/QmVG2ZYCkV1S4TK8URA3a4RupBF17A8yAr4FqsRDXVJASr",
327 events: []coreiface.AddEvent{
328 {Name: "abc", Path: p("QmU7nuGs2djqK99UNsNgEPGh6GV4662p6WtsgccBNGTDxt"), Size: "62"},
329 {Name: "", Path: p("QmVG2ZYCkV1S4TK8URA3a4RupBF17A8yAr4FqsRDXVJASr"), Size: "229"},
330 },
331 opts: []options.UnixfsAddOption{options.Unixfs.Silent(true)},
332 },
333 {
334 name: "dirAddEvents",
335 data: twoLevelDir(),
336 path: "/ipfs/QmVG2ZYCkV1S4TK8URA3a4RupBF17A8yAr4FqsRDXVJASr",
337 events: []coreiface.AddEvent{
338 {Name: "abc/def", Path: p("QmNyJpQkU1cEkBwMDhDNFstr42q55mqG5GE5Mgwug4xyGk"), Size: "13"},
339 {Name: "bar", Path: p("QmS21GuXiRMvJKHos4ZkEmQDmRBqRaF5tQS2CQCu2ne9sY"), Size: "14"},
340 {Name: "foo", Path: p("QmfAjGiVpTN56TXi6SBQtstit5BEw3sijKj1Qkxn6EXKzJ"), Size: "14"},
341 {Name: "abc", Path: p("QmU7nuGs2djqK99UNsNgEPGh6GV4662p6WtsgccBNGTDxt"), Size: "62"},
342 {Name: "", Path: p("QmVG2ZYCkV1S4TK8URA3a4RupBF17A8yAr4FqsRDXVJASr"), Size: "229"},
343 },
344 },
345 {
346 name: "progress1M",
347 data: func() files.Node {
348 return files.NewReaderFile(bytes.NewReader(bytes.Repeat([]byte{0}, 1000000)))
349 },
350 path: "/ipfs/QmXXNNbwe4zzpdMg62ZXvnX1oU7MwSrQ3vAEtuwFKCm1oD",
351 events: []coreiface.AddEvent{
352 {Name: "", Bytes: 262144},
353 {Name: "", Bytes: 524288},
354 {Name: "", Bytes: 786432},
355 {Name: "", Bytes: 1000000},
356 {Name: "QmXXNNbwe4zzpdMg62ZXvnX1oU7MwSrQ3vAEtuwFKCm1oD", Path: p("QmXXNNbwe4zzpdMg62ZXvnX1oU7MwSrQ3vAEtuwFKCm1oD"), Size: "1000256"},
357 },
358 wrap: "",
359 opts: []options.UnixfsAddOption{options.Unixfs.Progress(true)},
360 },
361 }
362
363 for _, testCase := range cases {
364 t.Run(testCase.name, func(t *testing.T) {
365 ctx, cancel := context.WithCancel(ctx)
366 defer cancel()
367
368 // recursive logic
369
370 data := testCase.data()
371 if testCase.wrap != "" {
372 data = files.NewMapDirectory(map[string]files.Node{
373 testCase.wrap: data,
374 })
375 }
376
377 // handle events if relevant to test case
378
379 opts := testCase.opts
380 eventOut := make(chan any)
381 var evtWg sync.WaitGroup
382 if len(testCase.events) > 0 {
383 opts = append(opts, options.Unixfs.Events(eventOut))
384
385 evtWg.Go(func() {
386 expected := testCase.events
387
388 for evt := range eventOut {
389 event, ok := evt.(*coreiface.AddEvent)
390 if !ok {
391 t.Error("unexpected event type")
392 continue
393 }
394
395 if len(expected) < 1 {
396 t.Error("got more events than expected")
397 continue
398 }
399
400 if expected[0].Size != event.Size {
401 t.Errorf("Event.Size didn't match, %s != %s", expected[0].Size, event.Size)
402 }
403
404 if expected[0].Name != event.Name {
405 t.Errorf("Event.Name didn't match, %s != %s", expected[0].Name, event.Name)
406 }
407
408 if (expected[0].Path != path.ImmutablePath{} && event.Path != path.ImmutablePath{}) {
409 if expected[0].Path.RootCid().String() != event.Path.RootCid().String() {
410 t.Errorf("Event.Hash didn't match, %s != %s", expected[0].Path, event.Path)
411 }
412 } else if event.Path != expected[0].Path {
413 t.Errorf("Event.Hash didn't match, %s != %s", expected[0].Path, event.Path)
414 }
415 if expected[0].Bytes != event.Bytes {
416 t.Errorf("Event.Bytes didn't match, %d != %d", expected[0].Bytes, event.Bytes)
417 }
418
419 expected = expected[1:]
420 }
421
422 if len(expected) > 0 {
423 t.Errorf("%d event(s) didn't arrive", len(expected))
424 }
425 })
426 }
427
428 tapi, err := api.WithOptions(testCase.apiOpts...)
429 if err != nil {
430 t.Fatal(err)
431 }
432
433 // Add!
434
435 p, err := tapi.Unixfs().Add(ctx, data, opts...)
436 close(eventOut)
437 evtWg.Wait()
438 if testCase.err != "" {
439 if err == nil {
440 t.Fatalf("expected an error: %s", testCase.err)
441 }
442 if err.Error() != testCase.err {
443 t.Fatalf("expected an error: '%s' != '%s'", err.Error(), testCase.err)
444 }
445 return
446 }
447 if err != nil {
448 t.Fatal(err)
449 }
450
451 if p.String() != testCase.path {
452 t.Errorf("expected path %s, got: %s", testCase.path, p)
453 }
454
455 // compare file structure with Unixfs().Get
456
457 var cmpFile func(origName string, orig files.Node, gotName string, got files.Node)
458 cmpFile = func(origName string, orig files.Node, gotName string, got files.Node) {
459 _, origDir := orig.(files.Directory)
460 _, gotDir := got.(files.Directory)
461
462 if origName != gotName {
463 t.Errorf("file name mismatch, orig='%s', got='%s'", origName, gotName)
464 }
465
466 if origDir != gotDir {
467 t.Fatalf("file type mismatch on %s", origName)
468 }
469
470 if !gotDir {
471 defer orig.Close()
472 defer got.Close()
473
474 do, err := io.ReadAll(orig.(files.File))
475 if err != nil {
476 t.Fatal(err)
477 }
478
479 dg, err := io.ReadAll(got.(files.File))
480 if err != nil {
481 t.Fatal(err)
482 }
483
484 if !bytes.Equal(do, dg) {
485 t.Fatal("data not equal")
486 }
487
488 return
489 }
490
491 origIt := orig.(files.Directory).Entries()
492 gotIt := got.(files.Directory).Entries()
493
494 for {
495 if origIt.Next() {
496 if !gotIt.Next() {
497 t.Fatal("gotIt out of entries before origIt")
498 }
499 } else {
500 if gotIt.Next() {
501 t.Fatal("origIt out of entries before gotIt")
502 }
503 break
504 }
505
506 cmpFile(origIt.Name(), origIt.Node(), gotIt.Name(), gotIt.Node())
507 }
508 if origIt.Err() != nil {
509 t.Fatal(origIt.Err())
510 }
511 if gotIt.Err() != nil {
512 t.Fatal(gotIt.Err())
513 }
514 }
515
516 f, err := tapi.Unixfs().Get(ctx, p)
517 if err != nil {
518 t.Fatal(err)
519 }
520
521 orig := testCase.data()
522 if testCase.expect != nil {
523 orig = testCase.expect(orig)
524 }
525
526 cmpFile("", orig, "", f)
527 })
528 }
529 }
530
531 func (tp *TestSuite) TestAddPinned(t *testing.T) {
532 ctx := t.Context()
533 api, err := tp.makeAPI(t, ctx)
534 if err != nil {
535 t.Fatal(err)
536 }
537
538 _, err = api.Unixfs().Add(ctx, strFile(helloStr)(), options.Unixfs.Pin(true, ""))
539 if err != nil {
540 t.Fatal(err)
541 }
542
543 pins, err := accPins(ctx, api)
544 if err != nil {
545 t.Fatal(err)
546 }
547 if len(pins) != 1 {
548 t.Fatalf("expected 1 pin, got %d", len(pins))
549 }
550
551 if pins[0].Path().String() != "/ipfs/QmQy2Dw4Wk7rdJKjThjYXzfFJNaRKRHhHP5gHHXroJMYxk" {
552 t.Fatalf("got unexpected pin: %s", pins[0].Path().String())
553 }
554 }
555
556 func (tp *TestSuite) TestAddHashOnly(t *testing.T) {
557 ctx := t.Context()
558 api, err := tp.makeAPI(t, ctx)
559 if err != nil {
560 t.Fatal(err)
561 }
562
563 p, err := api.Unixfs().Add(ctx, strFile(helloStr)(), options.Unixfs.HashOnly(true))
564 if err != nil {
565 t.Fatal(err)
566 }
567
568 if p.String() != hello {
569 t.Errorf("unexpected path: %s", p.String())
570 }
571
572 _, err = api.Block().Get(ctx, p)
573 if err == nil {
574 t.Fatal("expected an error")
575 }
576 if !ipld.IsNotFound(err) {
577 t.Errorf("unexpected error: %s", err.Error())
578 }
579 }
580
581 func (tp *TestSuite) TestGetEmptyFile(t *testing.T) {
582 ctx := t.Context()
583 api, err := tp.makeAPI(t, ctx)
584 if err != nil {
585 t.Fatal(err)
586 }
587
588 _, err = api.Unixfs().Add(ctx, files.NewBytesFile([]byte{}))
589 if err != nil {
590 t.Fatal(err)
591 }
592
593 emptyFilePath, err := path.NewPath(emptyFile)
594 if err != nil {
595 t.Fatal(err)
596 }
597
598 r, err := api.Unixfs().Get(ctx, emptyFilePath)
599 if err != nil {
600 t.Fatal(err)
601 }
602
603 buf := make([]byte, 1) // non-zero so that Read() actually tries to read
604 n, err := io.ReadFull(r.(files.File), buf)
605 if err != nil && err != io.EOF {
606 t.Error(err)
607 }
608 if !bytes.HasPrefix(buf, []byte{0x00}) {
609 t.Fatalf("expected empty data, got [%s] [read=%d]", buf, n)
610 }
611 }
612
613 func (tp *TestSuite) TestGetDir(t *testing.T) {
614 ctx := t.Context()
615 api, err := tp.makeAPI(t, ctx)
616 if err != nil {
617 t.Fatal(err)
618 }
619 edir := unixfs.EmptyDirNode()
620 err = api.Dag().Add(ctx, edir)
621 if err != nil {
622 t.Fatal(err)
623 }
624 p := path.FromCid(edir.Cid())
625
626 if p.String() != path.FromCid(edir.Cid()).String() {
627 t.Fatalf("expected path %s, got: %s", edir.Cid(), p.String())
628 }
629
630 r, err := api.Unixfs().Get(ctx, path.FromCid(edir.Cid()))
631 if err != nil {
632 t.Fatal(err)
633 }
634
635 if _, ok := r.(files.Directory); !ok {
636 t.Fatalf("expected a directory")
637 }
638 }
639
640 func (tp *TestSuite) TestGetNonUnixfs(t *testing.T) {
641 ctx := t.Context()
642 api, err := tp.makeAPI(t, ctx)
643 if err != nil {
644 t.Fatal(err)
645 }
646
647 nd := new(mdag.ProtoNode)
648 err = api.Dag().Add(ctx, nd)
649 if err != nil {
650 t.Fatal(err)
651 }
652
653 _, err = api.Unixfs().Get(ctx, path.FromCid(nd.Cid()))
654 if !strings.Contains(err.Error(), "proto:") || !strings.Contains(err.Error(), "required field") {
655 t.Fatalf("expected \"proto: required field\", got: %q", err)
656 }
657 }
658
659 func (tp *TestSuite) TestLs(t *testing.T) {
660 ctx := t.Context()
661 api, err := tp.makeAPI(t, ctx)
662 if err != nil {
663 t.Fatal(err)
664 }
665
666 r := strings.NewReader("content-of-file")
667 p, err := api.Unixfs().Add(ctx, files.NewMapDirectory(map[string]files.Node{
668 "name-of-file": files.NewReaderFile(r),
669 "name-of-symlink": files.NewLinkFile("/foo/bar", nil),
670 }))
671 if err != nil {
672 t.Fatal(err)
673 }
674
675 errCh := make(chan error, 1)
676 entries := make(chan coreiface.DirEntry)
677 go func() {
678 errCh <- api.Unixfs().Ls(ctx, p, entries)
679 }()
680
681 entry, ok := <-entries
682 if !ok {
683 t.Fatal("expected another entry")
684 }
685 if entry.Size != 15 {
686 t.Errorf("expected size = 15, got %d", entry.Size)
687 }
688 if entry.Name != "name-of-file" {
689 t.Errorf("expected name = name-of-file, got %s", entry.Name)
690 }
691 if entry.Type != coreiface.TFile {
692 t.Errorf("wrong type %s", entry.Type)
693 }
694 if entry.Cid.String() != "QmX3qQVKxDGz3URVC3861Z3CKtQKGBn6ffXRBBWGMFz9Lr" {
695 t.Errorf("expected cid = QmX3qQVKxDGz3URVC3861Z3CKtQKGBn6ffXRBBWGMFz9Lr, got %s", entry.Cid)
696 }
697 entry, ok = <-entries
698 if !ok {
699 t.Fatal("expected another entry")
700 }
701 if entry.Type != coreiface.TSymlink {
702 t.Errorf("wrong type %s", entry.Type)
703 }
704 if entry.Name != "name-of-symlink" {
705 t.Errorf("expected name = name-of-symlink, got %s", entry.Name)
706 }
707 if entry.Target != "/foo/bar" {
708 t.Errorf("expected symlink target to be /foo/bar, got %s", entry.Target)
709 }
710
711 _, ok = <-entries
712 if ok {
713 t.Errorf("didn't expect a another link")
714 }
715 if err = <-errCh; err != nil {
716 t.Error(err)
717 }
718 }
719
720 func (tp *TestSuite) TestEntriesExpired(t *testing.T) {
721 ctx, cancel := context.WithCancel(context.Background())
722 defer cancel()
723 api, err := tp.makeAPI(t, ctx)
724 if err != nil {
725 t.Fatal(err)
726 }
727
728 r := strings.NewReader("content-of-file")
729 p, err := api.Unixfs().Add(ctx, files.NewMapDirectory(map[string]files.Node{
730 "name-of-file": files.NewReaderFile(r),
731 }))
732 if err != nil {
733 t.Fatal(err)
734 }
735
736 ctx, cancel = context.WithCancel(ctx)
737
738 nd, err := api.Unixfs().Get(ctx, p)
739 if err != nil {
740 t.Fatal(err)
741 }
742 cancel()
743
744 it := files.ToDir(nd).Entries()
745 if it == nil {
746 t.Fatal("it was nil")
747 }
748
749 if it.Next() {
750 t.Fatal("Next succeeded")
751 }
752
753 if it.Err() != context.Canceled {
754 t.Fatalf("unexpected error %s", it.Err())
755 }
756
757 if it.Next() {
758 t.Fatal("Next succeeded")
759 }
760 }
761
762 func (tp *TestSuite) TestLsEmptyDir(t *testing.T) {
763 ctx := t.Context()
764 api, err := tp.makeAPI(t, ctx)
765 if err != nil {
766 t.Fatal(err)
767 }
768
769 p, err := api.Unixfs().Add(ctx, files.NewSliceDirectory([]files.DirEntry{}))
770 if err != nil {
771 t.Fatal(err)
772 }
773
774 errCh := make(chan error, 1)
775 links := make(chan coreiface.DirEntry)
776 go func() {
777 errCh <- api.Unixfs().Ls(ctx, p, links)
778 }()
779
780 var count int
781 for range links {
782 count++
783 }
784 if err = <-errCh; err != nil {
785 t.Fatal(err)
786 }
787
788 if count != 0 {
789 t.Fatalf("expected 0 links, got %d", count)
790 }
791 }
792
793 // TODO(lgierth) this should test properly, with len(links) > 0
794 func (tp *TestSuite) TestLsNonUnixfs(t *testing.T) {
795 ctx := t.Context()
796 api, err := tp.makeAPI(t, ctx)
797 if err != nil {
798 t.Fatal(err)
799 }
800
801 nd, err := cbor.WrapObject(map[string]any{"foo": "bar"}, math.MaxUint64, -1)
802 if err != nil {
803 t.Fatal(err)
804 }
805
806 err = api.Dag().Add(ctx, nd)
807 if err != nil {
808 t.Fatal(err)
809 }
810
811 errCh := make(chan error, 1)
812 links := make(chan coreiface.DirEntry)
813 go func() {
814 errCh <- api.Unixfs().Ls(ctx, path.FromCid(nd.Cid()), links)
815 }()
816
817 var count int
818 for range links {
819 count++
820 }
821 if err = <-errCh; err != nil {
822 t.Fatal(err)
823 }
824
825 if count != 0 {
826 t.Fatalf("expected 0 links, got %d", count)
827 }
828 }
829
830 type closeTestF struct {
831 files.File
832 closed bool
833
834 t *testing.T
835 }
836
837 type closeTestD struct {
838 files.Directory
839 closed bool
840
841 t *testing.T
842 }
843
844 func (f *closeTestD) Close() error {
845 f.t.Helper()
846 if f.closed {
847 f.t.Fatal("already closed")
848 }
849 f.closed = true
850 return nil
851 }
852
853 func (f *closeTestF) Close() error {
854 if f.closed {
855 f.t.Fatal("already closed")
856 }
857 f.closed = true
858 return nil
859 }
860
861 func (tp *TestSuite) TestAddCloses(t *testing.T) {
862 ctx := t.Context()
863 api, err := tp.makeAPI(t, ctx)
864 if err != nil {
865 t.Fatal(err)
866 }
867
868 n4 := &closeTestF{files.NewBytesFile([]byte("foo")), false, t}
869 d3 := &closeTestD{files.NewMapDirectory(map[string]files.Node{
870 "sub": n4,
871 }), false, t}
872 n2 := &closeTestF{files.NewBytesFile([]byte("bar")), false, t}
873 n1 := &closeTestF{files.NewBytesFile([]byte("baz")), false, t}
874 d0 := &closeTestD{files.NewMapDirectory(map[string]files.Node{
875 "a": d3,
876 "b": n1,
877 "c": n2,
878 }), false, t}
879
880 _, err = api.Unixfs().Add(ctx, d0)
881 if err != nil {
882 t.Fatal(err)
883 }
884
885 for i, n := range []*closeTestF{n1, n2, n4} {
886 if !n.closed {
887 t.Errorf("file %d not closed!", i)
888 }
889 }
890
891 for i, n := range []*closeTestD{d0, d3} {
892 if !n.closed {
893 t.Errorf("dir %d not closed!", i)
894 }
895 }
896 }
897
898 func (tp *TestSuite) TestGetSeek(t *testing.T) {
899 ctx := t.Context()
900 api, err := tp.makeAPI(t, ctx)
901 if err != nil {
902 t.Fatal(err)
903 }
904
905 dataSize := int64(100000)
906 tf := files.NewReaderFile(io.LimitReader(rand.New(rand.NewSource(1403768328)), dataSize))
907
908 p, err := api.Unixfs().Add(ctx, tf, options.Unixfs.Chunker("size-100"))
909 if err != nil {
910 t.Fatal(err)
911 }
912
913 r, err := api.Unixfs().Get(ctx, p)
914 if err != nil {
915 t.Fatal(err)
916 }
917
918 f := files.ToFile(r)
919 if f == nil {
920 t.Fatal("not a file")
921 }
922
923 orig := make([]byte, dataSize)
924 if _, err := io.ReadFull(f, orig); err != nil {
925 t.Fatal(err)
926 }
927 f.Close()
928
929 origR := bytes.NewReader(orig)
930
931 r, err = api.Unixfs().Get(ctx, p)
932 if err != nil {
933 t.Fatal(err)
934 }
935
936 f = files.ToFile(r)
937 if f == nil {
938 t.Fatal("not a file")
939 }
940
941 fSeeker, ok := f.(io.Seeker)
942 if !ok {
943 t.Fatal("file does not support seeking")
944 }
945
946 test := func(offset int64, whence int, read int, expect int64, shouldEof bool) {
947 t.Run(fmt.Sprintf("seek%d+%d-r%d-%d", whence, offset, read, expect), func(t *testing.T) {
948 n, err := fSeeker.Seek(offset, whence)
949 if err != nil {
950 t.Fatal(err)
951 }
952 origN, err := origR.Seek(offset, whence)
953 if err != nil {
954 t.Fatal(err)
955 }
956
957 if n != origN {
958 t.Fatalf("offsets didn't match, expected %d, got %d", origN, n)
959 }
960
961 buf := make([]byte, read)
962 origBuf := make([]byte, read)
963 origRead, err := origR.Read(origBuf)
964 if err != nil {
965 t.Fatalf("orig: %s", err)
966 }
967 r, err := io.ReadFull(f, buf)
968 switch {
969 case shouldEof && err != nil && err != io.ErrUnexpectedEOF:
970 fallthrough
971 case !shouldEof && err != nil:
972 t.Fatalf("f: %s", err)
973 case shouldEof:
974 _, err := f.Read([]byte{0})
975 if err != io.EOF {
976 t.Fatal("expected EOF")
977 }
978 _, err = origR.Read([]byte{0})
979 if err != io.EOF {
980 t.Fatal("expected EOF (orig)")
981 }
982 }
983
984 if int64(r) != expect {
985 t.Fatal("read wrong amount of data")
986 }
987 if r != origRead {
988 t.Fatal("read different amount of data than bytes.Reader")
989 }
990 if !bytes.Equal(buf, origBuf) {
991 fmt.Fprintf(os.Stderr, "original:\n%s\n", hex.Dump(origBuf))
992 fmt.Fprintf(os.Stderr, "got:\n%s\n", hex.Dump(buf))
993 t.Fatal("data didn't match")
994 }
995 })
996 }
997
998 test(3, io.SeekCurrent, 10, 10, false)
999 test(3, io.SeekCurrent, 10, 10, false)
1000 test(500, io.SeekCurrent, 10, 10, false)
1001 test(350, io.SeekStart, 100, 100, false)
1002 test(-123, io.SeekCurrent, 100, 100, false)
1003 test(0, io.SeekStart, int(dataSize), dataSize, false)
1004 test(dataSize-50, io.SeekStart, 100, 50, true)
1005 test(-5, io.SeekEnd, 100, 5, true)
1006 }
1007
1008 func (tp *TestSuite) TestGetReadAt(t *testing.T) {
1009 ctx := t.Context()
1010 api, err := tp.makeAPI(t, ctx)
1011 if err != nil {
1012 t.Fatal(err)
1013 }
1014
1015 dataSize := int64(100000)
1016 tf := files.NewReaderFile(io.LimitReader(rand.New(rand.NewSource(1403768328)), dataSize))
1017
1018 p, err := api.Unixfs().Add(ctx, tf, options.Unixfs.Chunker("size-100"))
1019 if err != nil {
1020 t.Fatal(err)
1021 }
1022
1023 r, err := api.Unixfs().Get(ctx, p)
1024 if err != nil {
1025 t.Fatal(err)
1026 }
1027
1028 f, ok := r.(interface {
1029 files.File
1030 io.ReaderAt
1031 })
1032 if !ok {
1033 t.Skip("ReaderAt not implemented")
1034 }
1035
1036 orig := make([]byte, dataSize)
1037 if _, err := io.ReadFull(f, orig); err != nil {
1038 t.Fatal(err)
1039 }
1040 f.Close()
1041
1042 origR := bytes.NewReader(orig)
1043
1044 if _, err := api.Unixfs().Get(ctx, p); err != nil {
1045 t.Fatal(err)
1046 }
1047
1048 test := func(offset int64, read int, expect int64, shouldEof bool) {
1049 t.Run(fmt.Sprintf("readat%d-r%d-%d", offset, read, expect), func(t *testing.T) {
1050 origBuf := make([]byte, read)
1051 origRead, err := origR.ReadAt(origBuf, offset)
1052 if err != nil && err != io.EOF {
1053 t.Fatalf("orig: %s", err)
1054 }
1055 buf := make([]byte, read)
1056 r, err := f.ReadAt(buf, offset)
1057 if shouldEof {
1058 if err != io.EOF {
1059 t.Fatal("expected EOF, got: ", err)
1060 }
1061 } else if err != nil {
1062 t.Fatal("got: ", err)
1063 }
1064
1065 if int64(r) != expect {
1066 t.Fatal("read wrong amount of data")
1067 }
1068 if r != origRead {
1069 t.Fatal("read different amount of data than bytes.Reader")
1070 }
1071 if !bytes.Equal(buf, origBuf) {
1072 fmt.Fprintf(os.Stderr, "original:\n%s\n", hex.Dump(origBuf))
1073 fmt.Fprintf(os.Stderr, "got:\n%s\n", hex.Dump(buf))
1074 t.Fatal("data didn't match")
1075 }
1076 })
1077 }
1078
1079 test(3, 10, 10, false)
1080 test(13, 10, 10, false)
1081 test(513, 10, 10, false)
1082 test(350, 100, 100, false)
1083 test(0, int(dataSize), dataSize, false)
1084 test(dataSize-50, 100, 50, true)
1085 }