github.com/storacha/go-ucanto@v0.7.2/core/car/car.go (about) 1 package car 2 3 import ( 4 "bufio" 5 "fmt" 6 "io" 7 "iter" 8 9 "github.com/ipfs/go-cid" 10 cbor "github.com/ipfs/go-ipld-cbor" 11 ipldcar "github.com/ipld/go-car" 12 "github.com/ipld/go-car/util" 13 cidlink "github.com/ipld/go-ipld-prime/linking/cid" 14 "github.com/multiformats/go-varint" 15 "github.com/storacha/go-ucanto/core/ipld" 16 "github.com/storacha/go-ucanto/core/ipld/block" 17 ) 18 19 // ContentType is the value the HTTP Content-Type header should have for CARs. 20 // See https://www.iana.org/assignments/media-types/application/vnd.ipld.car 21 const ContentType = "application/vnd.ipld.car" 22 23 func Encode(roots []ipld.Link, blocks iter.Seq2[ipld.Block, error]) io.ReadCloser { 24 reader, writer := io.Pipe() 25 go func() { 26 cids := []cid.Cid{} 27 for _, r := range roots { 28 _, cid, err := cid.CidFromBytes([]byte(r.Binary())) 29 if err != nil { 30 writer.CloseWithError(fmt.Errorf("decoding CAR root: %s: %w", r, err)) 31 return 32 } 33 cids = append(cids, cid) 34 } 35 h := ipldcar.CarHeader{ 36 Roots: cids, 37 Version: 1, 38 } 39 hb, err := cbor.DumpObject(h) 40 if err != nil { 41 writer.CloseWithError(fmt.Errorf("writing CAR header: %w", err)) 42 return 43 } 44 util.LdWrite(writer, hb) 45 for block, err := range blocks { 46 if err != nil { 47 writer.CloseWithError(fmt.Errorf("writing CAR blocks: %w", err)) 48 return 49 } 50 err = util.LdWrite(writer, []byte(block.Link().Binary()), block.Bytes()) 51 if err != nil { 52 writer.CloseWithError(fmt.Errorf("writing CAR blocks: %w", err)) 53 return 54 } 55 } 56 writer.Close() 57 }() 58 return reader 59 } 60 61 type CarBlock interface { 62 ipld.Block 63 Offset() uint64 64 Length() uint64 65 } 66 67 type carBlock struct { 68 ipld.Block 69 offset uint64 70 length uint64 71 } 72 73 func (cb carBlock) Offset() uint64 { 74 return cb.offset 75 } 76 77 func (cb carBlock) Length() uint64 { 78 return cb.length 79 } 80 81 func Decode(reader io.Reader) ([]ipld.Link, iter.Seq2[ipld.Block, error], error) { 82 br := bufio.NewReader(reader) 83 84 h, err := ipldcar.ReadHeader(br) 85 if err != nil { 86 return nil, nil, err 87 } 88 89 if h.Version != 1 { 90 return nil, nil, fmt.Errorf("invalid car version: %d", h.Version) 91 } 92 93 offset, err := ipldcar.HeaderSize(h) 94 if err != nil { 95 return nil, nil, err 96 } 97 98 var roots []ipld.Link 99 for _, r := range h.Roots { 100 roots = append(roots, cidlink.Link{Cid: r}) 101 } 102 103 r := &blkReader{br, offset} 104 return roots, func(yield func(ipld.Block, error) bool) { 105 for { 106 blk, err := r.next() 107 if err == io.EOF { 108 return 109 } 110 if !yield(blk, err) { 111 return 112 } 113 } 114 }, nil 115 } 116 117 type blkReader struct { 118 br *bufio.Reader 119 offset uint64 120 } 121 122 func (r *blkReader) next() (CarBlock, error) { 123 cid, bytes, err := util.ReadNode(r.br) 124 if err != nil { 125 return nil, err 126 } 127 128 hashed, err := cid.Prefix().Sum(bytes) 129 if err != nil { 130 return nil, err 131 } 132 133 if !hashed.Equals(cid) { 134 return nil, fmt.Errorf("mismatch in content integrity, name: %s, data: %s", cid, hashed) 135 } 136 137 ss := uint64(cid.ByteLen()) + uint64(len(bytes)) 138 r.offset += uint64(varint.UvarintSize(ss)) + ss 139 140 return carBlock{block.NewBlock(cidlink.Link{Cid: cid}, bytes), r.offset - uint64(len(bytes)), uint64(len(bytes))}, nil 141 }