Skip to content

Commit 2fb245d

Browse files
committed
feat: EmbedMessage WrapV1 option & Reader#ReadEmbeddedMessage
EmbedMessage sets the second-to-leftmost bit to indicate that there is a length-prefixed dag-cbor message object directly after the header. If that bit is set, Reader#ReadEmbeddedMessage will decode and return that message. Ref: ipld/js-car#89
1 parent 771fb74 commit 2fb245d

File tree

9 files changed

+274
-54
lines changed

9 files changed

+274
-54
lines changed

v2/block_reader_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,14 @@ func TestMaxHeaderLength(t *testing.T) {
159159
require.EqualError(t, err, "invalid header data, length of read beyond allowable maximum")
160160
}
161161

162+
func TestCanReadMessagingCar(t *testing.T) {
163+
car, err := carv2.NewBlockReader(requireReaderFromPath(t, "testdata/messaging.car"))
164+
require.NoError(t, err)
165+
readBlock, err := car.Next()
166+
require.NoError(t, err)
167+
require.Equal(t, "random meaningless bytes", string(readBlock.RawData()))
168+
}
169+
162170
func requireReaderFromPath(t *testing.T, path string) io.Reader {
163171
f, err := os.Open(path)
164172
require.NoError(t, err)

v2/car.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,12 @@ type (
4444
}
4545
)
4646

47-
// fullyIndexedCharPos is the position of Characteristics.Hi bit that specifies whether the index is a catalog af all CIDs or not.
47+
// fullyIndexedCharPos is the position of Characteristics.Hi bit that specifies whether the index is
48+
// a catalog af all CIDs or not.
4849
const fullyIndexedCharPos = 7 // left-most bit
50+
// messageAfterHeaderCharPos is the position of Characteristics.Hi bit that specifies whether there
51+
// is a length-prefixed dag-cbor message object directly after the header.
52+
const messageAfterHeaderCharPos = 6 // second-to-left-most bit
4953

5054
// WriteTo writes this characteristics to the given w.
5155
func (c Characteristics) WriteTo(w io.Writer) (n int64, err error) {
@@ -83,6 +87,22 @@ func (c *Characteristics) SetFullyIndexed(b bool) {
8387
}
8488
}
8589

90+
// IsMessageAfterHeader specifies whether there is a length-prefixed dag-cbor message embedded
91+
// directly after the CARv2 header that can optionally be decoded.
92+
func (c *Characteristics) IsMessageAfterHeader() bool {
93+
return isBitSet(c.Hi, messageAfterHeaderCharPos)
94+
}
95+
96+
// SetMessageAfterHeader sets whether there is a length-prefixed dag-cbor message embedded directly
97+
// after the CARv2 header.
98+
func (c *Characteristics) SetMessageAfterHeader(b bool) {
99+
if b {
100+
c.Hi = setBit(c.Hi, messageAfterHeaderCharPos)
101+
} else {
102+
c.Hi = unsetBit(c.Hi, messageAfterHeaderCharPos)
103+
}
104+
}
105+
86106
func setBit(n uint64, pos uint) uint64 {
87107
n |= 1 << pos
88108
return n

v2/car_test.go

Lines changed: 65 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -205,43 +205,69 @@ func TestNewHeaderHasExpectedValues(t *testing.T) {
205205
assert.Equal(t, want, got, "NewHeader got = %v, want = %v", got, want)
206206
}
207207

208-
func TestCharacteristics_StoreIdentityCIDs(t *testing.T) {
209-
subject := carv2.Characteristics{}
210-
require.False(t, subject.IsFullyIndexed())
211-
212-
subject.SetFullyIndexed(true)
213-
require.True(t, subject.IsFullyIndexed())
214-
215-
var buf bytes.Buffer
216-
written, err := subject.WriteTo(&buf)
217-
require.NoError(t, err)
218-
require.Equal(t, int64(16), written)
219-
require.Equal(t, []byte{
220-
0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
221-
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
222-
}, buf.Bytes())
223-
224-
var decodedSubject carv2.Characteristics
225-
read, err := decodedSubject.ReadFrom(&buf)
226-
require.NoError(t, err)
227-
require.Equal(t, int64(16), read)
228-
require.True(t, decodedSubject.IsFullyIndexed())
229-
230-
buf.Reset()
231-
subject.SetFullyIndexed(false)
232-
require.False(t, subject.IsFullyIndexed())
233-
234-
written, err = subject.WriteTo(&buf)
235-
require.NoError(t, err)
236-
require.Equal(t, int64(16), written)
237-
require.Equal(t, []byte{
238-
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
239-
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
240-
}, buf.Bytes())
241-
242-
var decodedSubjectAgain carv2.Characteristics
243-
read, err = decodedSubjectAgain.ReadFrom(&buf)
244-
require.NoError(t, err)
245-
require.Equal(t, int64(16), read)
246-
require.False(t, decodedSubjectAgain.IsFullyIndexed())
208+
func TestCharacteristics(t *testing.T) {
209+
tests := []struct {
210+
name string
211+
isset func(carv2.Characteristics) bool
212+
set func(*carv2.Characteristics, bool)
213+
expectBytes []byte
214+
}{
215+
{
216+
"FullyIndexed",
217+
func(c carv2.Characteristics) bool { return c.IsFullyIndexed() },
218+
func(c *carv2.Characteristics, s bool) { c.SetFullyIndexed(s) },
219+
[]byte{
220+
0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
221+
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
222+
},
223+
},
224+
{
225+
"MessageAfterHeader",
226+
func(c carv2.Characteristics) bool { return c.IsMessageAfterHeader() },
227+
func(c *carv2.Characteristics, s bool) { c.SetMessageAfterHeader(s) },
228+
[]byte{
229+
0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
230+
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
231+
},
232+
},
233+
}
234+
for _, tt := range tests {
235+
tt := tt
236+
t.Run(tt.name, func(t *testing.T) {
237+
subject := carv2.Characteristics{}
238+
require.False(t, tt.isset(subject))
239+
tt.set(&subject, true)
240+
require.True(t, tt.isset(subject))
241+
242+
var buf bytes.Buffer
243+
written, err := subject.WriteTo(&buf)
244+
require.NoError(t, err)
245+
require.Equal(t, int64(16), written)
246+
require.Equal(t, tt.expectBytes, buf.Bytes())
247+
248+
var decodedSubject carv2.Characteristics
249+
read, err := decodedSubject.ReadFrom(&buf)
250+
require.NoError(t, err)
251+
require.Equal(t, int64(16), read)
252+
require.True(t, tt.isset(decodedSubject))
253+
254+
buf.Reset()
255+
tt.set(&subject, false)
256+
require.False(t, tt.isset(subject))
257+
258+
written, err = subject.WriteTo(&buf)
259+
require.NoError(t, err)
260+
require.Equal(t, int64(16), written)
261+
require.Equal(t, []byte{
262+
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
263+
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
264+
}, buf.Bytes())
265+
266+
var decodedSubjectAgain carv2.Characteristics
267+
read, err = decodedSubjectAgain.ReadFrom(&buf)
268+
require.NoError(t, err)
269+
require.Equal(t, int64(16), read)
270+
require.False(t, tt.isset(decodedSubjectAgain))
271+
})
272+
}
247273
}

v2/options.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"math"
55

66
"github.com/ipld/go-car/v2/index"
7+
"github.com/ipld/go-ipld-prime/datamodel"
78
"github.com/ipld/go-ipld-prime/traversal"
89
"github.com/multiformats/go-multicodec"
910

@@ -63,6 +64,8 @@ type Options struct {
6364

6465
MaxAllowedHeaderSize uint64
6566
MaxAllowedSectionSize uint64
67+
68+
EmbeddedMessage datamodel.Node
6669
}
6770

6871
// ApplyOptions applies given opts and returns the resulting Options.
@@ -172,3 +175,12 @@ func MaxAllowedSectionSize(max uint64) Option {
172175
o.MaxAllowedSectionSize = max
173176
}
174177
}
178+
179+
// EmbedMessage writes a length-prefixed dag-cbor message after the CARv2 header
180+
// and sets the 'MessageAfterHeader' characteristic bit is set for the resulting
181+
// CAR
182+
func EmbedMessage(msg datamodel.Node) Option {
183+
return func(o *Options) {
184+
o.EmbeddedMessage = msg
185+
}
186+
}

v2/reader.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import (
1111
"github.com/ipld/go-car/v2/internal/carv1"
1212
"github.com/ipld/go-car/v2/internal/carv1/util"
1313
internalio "github.com/ipld/go-car/v2/internal/io"
14+
ipld "github.com/ipld/go-ipld-prime"
15+
"github.com/ipld/go-ipld-prime/codec/dagcbor"
16+
"github.com/ipld/go-ipld-prime/datamodel"
1417
"github.com/multiformats/go-multicodec"
1518
"github.com/multiformats/go-multihash"
1619
"github.com/multiformats/go-varint"
@@ -349,6 +352,26 @@ func (r *Reader) Inspect(validateBlockHash bool) (Stats, error) {
349352
return stats, nil
350353
}
351354

355+
// ReadEmbeddedMessage reads a length-prefixed dag-cbor message embedded after the CARv2 header
356+
// if the 'MessageAfterHeader' characteristic bit is set for this CAR and the message exists.
357+
func (r *Reader) ReadEmbeddedMessage() (datamodel.Node, error) {
358+
if !r.Header.Characteristics.IsMessageAfterHeader() {
359+
return nil, errors.New("'MessageAfterHeader' bit is not set in the characteristics for this CAR")
360+
}
361+
362+
msgStart := int64(PragmaSize + HeaderSize)
363+
gapLen := int64(r.Header.DataOffset) - msgStart
364+
if gapLen <= 0 {
365+
return nil, errors.New("invalid MessageAfterHeader, no space after header")
366+
}
367+
msgReader := io.NewSectionReader(r.r, msgStart, gapLen)
368+
byts, err := util.LdRead(msgReader, false, r.opts.MaxAllowedSectionSize)
369+
if err != nil {
370+
return nil, err
371+
}
372+
return ipld.Decode(byts, dagcbor.Decode)
373+
}
374+
352375
// Close closes the underlying reader if it was opened by OpenReader.
353376
func (r *Reader) Close() error {
354377
if r.closer != nil {

v2/reader_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
carv2 "github.com/ipld/go-car/v2"
1313
"github.com/ipld/go-car/v2/index"
1414
"github.com/ipld/go-car/v2/internal/carv1"
15+
ipld "github.com/ipld/go-ipld-prime"
16+
"github.com/ipld/go-ipld-prime/codec/dagjson"
1517
"github.com/multiformats/go-multicodec"
1618
"github.com/stretchr/testify/require"
1719
)
@@ -424,6 +426,31 @@ func TestInspect(t *testing.T) {
424426
MinCidLength: 25,
425427
},
426428
},
429+
// CARv2 with embedded message
430+
{
431+
name: "CarWithMessage",
432+
path: "testdata/messaging.car",
433+
zerLenAsEOF: true,
434+
expectedStats: carv2.Stats{
435+
Version: 2,
436+
Header: carv2.Header{
437+
Characteristics: carv2.Characteristics{Hi: 64},
438+
DataOffset: 158,
439+
DataSize: 120,
440+
},
441+
Roots: []cid.Cid{mustCidDecode("bafkreihwkf6mtnjobdqrkiksr7qhp6tiiqywux64aylunbvmfhzeql2coa")},
442+
RootsPresent: true,
443+
AvgBlockLength: 24,
444+
MinBlockLength: 24,
445+
MaxBlockLength: 24,
446+
AvgCidLength: 36,
447+
MinCidLength: 36,
448+
MaxCidLength: 36,
449+
BlockCount: 1,
450+
CodecCounts: map[multicodec.Code]uint64{multicodec.Raw: 1},
451+
MhTypeCounts: map[multicodec.Code]uint64{multicodec.Sha2_256: 1},
452+
},
453+
},
427454
}
428455

429456
for _, tt := range tests {
@@ -560,3 +587,32 @@ func mustCidDecode(s string) cid.Cid {
560587
}
561588
return c
562589
}
590+
591+
func TestEmbeddedMessage(t *testing.T) {
592+
t.Run("has", func(t *testing.T) {
593+
r, err := carv2.OpenReader("testdata/messaging.car")
594+
require.NoError(t, err)
595+
msg, err := r.ReadEmbeddedMessage()
596+
require.NoError(t, err)
597+
enc, err := ipld.Encode(msg, dagjson.Encode)
598+
require.NoError(t, err)
599+
require.Equal(t,
600+
`{"expectedRoot":{"/":"bafkreihwkf6mtnjobdqrkiksr7qhp6tiiqywux64aylunbvmfhzeql2coa"},"sneaky":"sending a message outside of CARv1 payload"}`,
601+
string(enc),
602+
)
603+
})
604+
t.Run("hasnot-v1", func(t *testing.T) {
605+
r, err := carv2.OpenReader("testdata/sample-v1.car")
606+
require.NoError(t, err)
607+
_, err = r.ReadEmbeddedMessage()
608+
require.NotNil(t, err)
609+
require.Equal(t, err.Error(), "'MessageAfterHeader' bit is not set in the characteristics for this CAR")
610+
})
611+
t.Run("hasnot-v2", func(t *testing.T) {
612+
r, err := carv2.OpenReader("testdata/sample-wrapped-v2.car")
613+
require.NoError(t, err)
614+
_, err = r.ReadEmbeddedMessage()
615+
require.NotNil(t, err)
616+
require.Equal(t, err.Error(), "'MessageAfterHeader' bit is not set in the characteristics for this CAR")
617+
})
618+
}

v2/testdata/messaging.car

278 Bytes
Binary file not shown.

v2/writer.go

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import (
1111
"github.com/ipld/go-car/v2/index"
1212
"github.com/ipld/go-car/v2/internal/carv1"
1313
internalio "github.com/ipld/go-car/v2/internal/io"
14+
ipld "github.com/ipld/go-ipld-prime"
15+
"github.com/ipld/go-ipld-prime/codec/dagcbor"
16+
"github.com/multiformats/go-varint"
1417
)
1518

1619
// ErrAlreadyV1 signals that the given payload is already in CARv1 format.
@@ -46,21 +49,27 @@ func WrapV1File(srcPath, dstPath string) error {
4649
return nil
4750
}
4851

49-
// WrapV1 takes a CARv1 file and wraps it as a CARv2 file with an index.
52+
// WrapV1 takes a CARv1 file and wraps it as a CARv2 file with an index (unless
53+
// the WithoutIndex option is supplied).
5054
// The resulting CARv2 file's inner CARv1 payload is left unmodified,
51-
// and does not use any padding before the innner CARv1 or index.
55+
// and does not use any padding before the inner CARv1 or index.
56+
// The EmbedMessage option may be used to insert an additional message after the
57+
// CARv2 header.
5258
func WrapV1(src io.ReadSeeker, dst io.Writer, opts ...Option) error {
5359
// TODO: verify src is indeed a CARv1 to prevent misuse.
5460
// GenerateIndex should probably be in charge of that.
5561

5662
o := ApplyOptions(opts...)
57-
idx, err := index.New(o.IndexCodec)
58-
if err != nil {
59-
return err
60-
}
61-
62-
if err := LoadIndex(idx, src, opts...); err != nil {
63-
return err
63+
var idx index.Index
64+
var err error
65+
if o.IndexCodec != index.CarIndexNone {
66+
idx, err = index.New(o.IndexCodec)
67+
if err != nil {
68+
return err
69+
}
70+
if err := LoadIndex(idx, src, opts...); err != nil {
71+
return err
72+
}
6473
}
6574

6675
// Use Seek to learn the size of the CARv1 before reading it.
@@ -74,18 +83,44 @@ func WrapV1(src io.ReadSeeker, dst io.Writer, opts ...Option) error {
7483

7584
// Similar to the writer API, write all components of a CARv2 to the
7685
// destination file: Pragma, Header, CARv1, Index.
77-
v2Header := NewHeader(uint64(v1Size))
7886
if _, err := dst.Write(Pragma); err != nil {
7987
return err
8088
}
81-
if _, err := v2Header.WriteTo(dst); err != nil {
82-
return err
89+
v2Header := NewHeader(uint64(v1Size))
90+
if o.IndexCodec == index.CarIndexNone {
91+
v2Header.IndexOffset = 0
92+
}
93+
94+
if o.EmbeddedMessage != nil {
95+
v2Header.Characteristics.SetMessageAfterHeader(true)
96+
msgBytes, err := ipld.Encode(o.EmbeddedMessage, dagcbor.Encode)
97+
if err != nil {
98+
return err
99+
}
100+
lenBuf := make([]byte, 8)
101+
lenLen := varint.PutUvarint(lenBuf, uint64(len(msgBytes)))
102+
v2Header.DataOffset += uint64(lenLen + len(msgBytes))
103+
if _, err := v2Header.WriteTo(dst); err != nil {
104+
return err
105+
}
106+
if _, err = dst.Write(lenBuf[:lenLen]); err != nil {
107+
return err
108+
}
109+
if _, err = dst.Write(msgBytes); err != nil {
110+
return err
111+
}
112+
} else {
113+
if _, err := v2Header.WriteTo(dst); err != nil {
114+
return err
115+
}
83116
}
84117
if _, err := io.Copy(dst, src); err != nil {
85118
return err
86119
}
87-
if _, err := index.WriteTo(idx, dst); err != nil {
88-
return err
120+
if idx != nil {
121+
if _, err := index.WriteTo(idx, dst); err != nil {
122+
return err
123+
}
89124
}
90125

91126
return nil

0 commit comments

Comments
 (0)