diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e7ea3d8..d3794c8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,7 +44,7 @@ jobs: uses: jandelgado/gcov2lcov-action@v1 - name: Coveralls - uses: coverallsapp/github-action@v2.3.4 + uses: coverallsapp/github-action@v2.3.6 with: github-token: ${{ secrets.GITHUB_TOKEN }} path-to-lcov: coverage.lcov diff --git a/README.md b/README.md index 310e6cf..7ccd9b4 100644 --- a/README.md +++ b/README.md @@ -252,6 +252,6 @@ additional features. This will be not be considered a breaking change. ## Who uses hamba/avro? -- [Apache Arrow for Go](https://github.com/apache/arrow/tree/main/go) +- [Apache Arrow for Go](https://github.com/apache/arrow-go) - [confluent-kafka-go](https://github.com/confluentinc/confluent-kafka-go) - [pulsar-client-go](https://github.com/apache/pulsar-client-go) diff --git a/go.mod b/go.mod index 65ff924..0cb1c34 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 github.com/modern-go/reflect2 v1.0.2 github.com/stretchr/testify v1.9.0 - golang.org/x/tools v0.28.0 + golang.org/x/tools v0.29.0 ) require ( diff --git a/go.sum b/go.sum index 439c6d1..771cbc7 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,8 @@ golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= -golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= +golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= +golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/ocf/codec.go b/ocf/codec.go index 4811bf0..8d0cab2 100644 --- a/ocf/codec.go +++ b/ocf/codec.go @@ -24,19 +24,29 @@ const ( ZStandard CodecName = "zstandard" ) -func resolveCodec(name CodecName, lvl int) (Codec, error) { +type codecOptions struct { + DeflateCompressionLevel int + ZStandardOptions zstdOptions +} + +type zstdOptions struct { + EOptions []zstd.EOption + DOptions []zstd.DOption +} + +func resolveCodec(name CodecName, codecOpts codecOptions) (Codec, error) { switch name { case Null, "": return &NullCodec{}, nil case Deflate: - return &DeflateCodec{compLvl: lvl}, nil + return &DeflateCodec{compLvl: codecOpts.DeflateCompressionLevel}, nil case Snappy: return &SnappyCodec{}, nil case ZStandard: - return newZStandardCodec(), nil + return newZStandardCodec(codecOpts.ZStandardOptions), nil default: return nil, fmt.Errorf("unknown codec %s", name) @@ -132,9 +142,9 @@ type ZStandardCodec struct { encoder *zstd.Encoder } -func newZStandardCodec() *ZStandardCodec { - decoder, _ := zstd.NewReader(nil) - encoder, _ := zstd.NewWriter(nil) +func newZStandardCodec(opts zstdOptions) *ZStandardCodec { + decoder, _ := zstd.NewReader(nil, opts.DOptions...) + encoder, _ := zstd.NewWriter(nil, opts.EOptions...) return &ZStandardCodec{ decoder: decoder, encoder: encoder, diff --git a/ocf/codec_test.go b/ocf/codec_test.go index 2fb21f2..761437f 100644 --- a/ocf/codec_test.go +++ b/ocf/codec_test.go @@ -59,7 +59,7 @@ func BenchmarkZstdEncodeDecodeLowEntropyLong(b *testing.B) { input := makeTestData(8762, func() byte { return 'a' }) - codec, err := resolveCodec(ZStandard, 0) + codec, err := resolveCodec(ZStandard, codecOptions{}) require.NoError(b, err) b.ReportAllocs() @@ -74,7 +74,7 @@ func BenchmarkZstdEncodeDecodeLowEntropyLong(b *testing.B) { func BenchmarkZstdEncodeDecodeHighEntropyLong(b *testing.B) { input := makeTestData(8762, func() byte { return byte(rand.Uint32()) }) - codec, err := resolveCodec(ZStandard, 0) + codec, err := resolveCodec(ZStandard, codecOptions{}) require.NoError(b, err) b.ReportAllocs() @@ -87,7 +87,7 @@ func BenchmarkZstdEncodeDecodeHighEntropyLong(b *testing.B) { } func verifyZstdEncodeDecode(t *testing.T, input []byte) { - codec, err := resolveCodec(ZStandard, 0) + codec, err := resolveCodec(ZStandard, codecOptions{}) require.NoError(t, err) compressed := codec.Encode(input) diff --git a/ocf/example_test.go b/ocf/example_test.go index b8b014a..ee09e5c 100644 --- a/ocf/example_test.go +++ b/ocf/example_test.go @@ -34,7 +34,7 @@ func ExampleNewDecoder() { // Do something with the data } - if dec.Error() != nil { + if err := dec.Error(); err != nil { log.Fatal(err) } } diff --git a/ocf/ocf.go b/ocf/ocf.go index d980e4f..b3d7408 100644 --- a/ocf/ocf.go +++ b/ocf/ocf.go @@ -5,6 +5,7 @@ package ocf import ( "bytes" + "compress/flate" "crypto/rand" "encoding/json" "errors" @@ -14,6 +15,7 @@ import ( "github.com/hamba/avro/v2" "github.com/hamba/avro/v2/internal/bytesx" + "github.com/klauspost/compress/zstd" ) const ( @@ -54,6 +56,7 @@ type Header struct { type decoderConfig struct { DecoderConfig avro.API SchemaCache *avro.SchemaCache + CodecOptions codecOptions } // DecoderFunc represents a configuration function for Decoder. @@ -74,6 +77,13 @@ func WithDecoderSchemaCache(cache *avro.SchemaCache) DecoderFunc { } } +// WithZStandardDecoderOptions sets the options for the ZStandard decoder. +func WithZStandardDecoderOptions(opts ...zstd.DOption) DecoderFunc { + return func(cfg *decoderConfig) { + cfg.CodecOptions.ZStandardOptions.DOptions = append(cfg.CodecOptions.ZStandardOptions.DOptions, opts...) + } +} + // Decoder reads and decodes Avro values from a container file. type Decoder struct { reader *avro.Reader @@ -93,6 +103,9 @@ func NewDecoder(r io.Reader, opts ...DecoderFunc) (*Decoder, error) { cfg := decoderConfig{ DecoderConfig: avro.DefaultConfig, SchemaCache: avro.DefaultSchemaCache, + CodecOptions: codecOptions{ + DeflateCompressionLevel: flate.DefaultCompression, + }, } for _, opt := range opts { opt(&cfg) @@ -100,7 +113,7 @@ func NewDecoder(r io.Reader, opts ...DecoderFunc) (*Decoder, error) { reader := avro.NewReader(r, 1024) - h, err := readHeader(reader, cfg.SchemaCache) + h, err := readHeader(reader, cfg.SchemaCache, cfg.CodecOptions) if err != nil { return nil, fmt.Errorf("decoder: %w", err) } @@ -174,7 +187,8 @@ func (d *Decoder) readBlock() int64 { size := d.reader.ReadLong() // Read the blocks data - if count > 0 { + switch { + case count > 0: data := make([]byte, size) d.reader.Read(data) @@ -184,6 +198,11 @@ func (d *Decoder) readBlock() int64 { } d.resetReader.Reset(data) + + case size > 0: + // Skip the block data when count is 0 + data := make([]byte, size) + d.reader.Read(data) } // Read the sync. @@ -197,14 +216,14 @@ func (d *Decoder) readBlock() int64 { } type encoderConfig struct { - BlockLength int - CodecName CodecName - CodecCompression int - Metadata map[string][]byte - Sync [16]byte - EncodingConfig avro.API - SchemaCache *avro.SchemaCache - SchemaMarshaler func(avro.Schema) ([]byte, error) + BlockLength int + CodecName CodecName + CodecOptions codecOptions + Metadata map[string][]byte + Sync [16]byte + EncodingConfig avro.API + SchemaCache *avro.SchemaCache + SchemaMarshaler func(avro.Schema) ([]byte, error) } // EncoderFunc represents a configuration function for Encoder. @@ -229,7 +248,14 @@ func WithCodec(codec CodecName) EncoderFunc { func WithCompressionLevel(compLvl int) EncoderFunc { return func(cfg *encoderConfig) { cfg.CodecName = Deflate - cfg.CodecCompression = compLvl + cfg.CodecOptions.DeflateCompressionLevel = compLvl + } +} + +// WithZStandardEncoderOptions sets the options for the ZStandard encoder. +func WithZStandardEncoderOptions(opts ...zstd.EOption) EncoderFunc { + return func(cfg *encoderConfig) { + cfg.CodecOptions.ZStandardOptions.EOptions = append(cfg.CodecOptions.ZStandardOptions.EOptions, opts...) } } @@ -316,7 +342,7 @@ func newEncoder(schema avro.Schema, w io.Writer, cfg encoderConfig) (*Encoder, e if info.Size() > 0 { reader := avro.NewReader(file, 1024) - h, err := readHeader(reader, cfg.SchemaCache) + h, err := readHeader(reader, cfg.SchemaCache, cfg.CodecOptions) if err != nil { return nil, err } @@ -354,7 +380,7 @@ func newEncoder(schema avro.Schema, w io.Writer, cfg encoderConfig) (*Encoder, e _, _ = rand.Read(header.Sync[:]) } - codec, err := resolveCodec(cfg.CodecName, cfg.CodecCompression) + codec, err := resolveCodec(cfg.CodecName, cfg.CodecOptions) if err != nil { return nil, err } @@ -379,13 +405,15 @@ func newEncoder(schema avro.Schema, w io.Writer, cfg encoderConfig) (*Encoder, e func computeEncoderConfig(opts []EncoderFunc) encoderConfig { cfg := encoderConfig{ - BlockLength: 100, - CodecName: Null, - CodecCompression: -1, - Metadata: map[string][]byte{}, - EncodingConfig: avro.DefaultConfig, - SchemaCache: avro.DefaultSchemaCache, - SchemaMarshaler: DefaultSchemaMarshaler, + BlockLength: 100, + CodecName: Null, + CodecOptions: codecOptions{ + DeflateCompressionLevel: flate.DefaultCompression, + }, + Metadata: map[string][]byte{}, + EncodingConfig: avro.DefaultConfig, + SchemaCache: avro.DefaultSchemaCache, + SchemaMarshaler: DefaultSchemaMarshaler, } for _, opt := range opts { opt(&cfg) @@ -469,7 +497,7 @@ type ocfHeader struct { Sync [16]byte } -func readHeader(reader *avro.Reader, schemaCache *avro.SchemaCache) (*ocfHeader, error) { +func readHeader(reader *avro.Reader, schemaCache *avro.SchemaCache, codecOpts codecOptions) (*ocfHeader, error) { var h Header reader.ReadVal(HeaderSchema, &h) if reader.Error != nil { @@ -484,7 +512,7 @@ func readHeader(reader *avro.Reader, schemaCache *avro.SchemaCache) (*ocfHeader, return nil, err } - codec, err := resolveCodec(CodecName(h.Meta[codecKey]), -1) + codec, err := resolveCodec(CodecName(h.Meta[codecKey]), codecOpts) if err != nil { return nil, err } diff --git a/ocf/ocf_test.go b/ocf/ocf_test.go index 200a779..3f5eb6a 100644 --- a/ocf/ocf_test.go +++ b/ocf/ocf_test.go @@ -13,6 +13,7 @@ import ( "github.com/hamba/avro/v2" "github.com/hamba/avro/v2/ocf" + "github.com/klauspost/compress/zstd" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -409,6 +410,52 @@ func TestDecoder_WithZStandardHandlesInvalidData(t *testing.T) { assert.Error(t, dec.Error()) } +func TestDecoder_WithZStandardOptions(t *testing.T) { + unionStr := "union value" + want := FullRecord{ + Strings: []string{"string1", "string2", "string3", "string4", "string5"}, + Longs: []int64{1, 2, 3, 4, 5}, + Enum: "C", + Map: map[string]int{ + "ke\xa9\xb1": 1, + "\x00\x00y2": 2, + "key3": 3, + "key4": 4, + "key5": 5, + }, + Nullable: &unionStr, + Fixed: [16]byte{0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04}, + Record: &TestRecord{ + Long: 1925639126735, + String: "I am a test record", + Int: 666, + Float: 7171.17, + Double: 916734926348163.01973408746523, + Bool: true, + }, + } + + f, err := os.Open("testdata/zstd-invalid-data.avro") + require.NoError(t, err) + t.Cleanup(func() { _ = f.Close() }) + + dec, err := ocf.NewDecoder(f, ocf.WithZStandardDecoderOptions(zstd.IgnoreChecksum(true))) + require.NoError(t, err) + + dec.HasNext() + + var got FullRecord + err = dec.Decode(&got) + + require.NoError(t, err, "should not cause an error because checksum is ignored") + require.NoError(t, dec.Error(), "should not cause an error because checksum is ignored") + assert.Equal(t, want, got, "should read corrupted data as valid because checksum is ignored") + + dec.HasNext() + + assert.ErrorContains(t, dec.Error(), "decoder: invalid block", "trailing byte in file should cause error before hitting zstd decoder") +} + func TestDecoder_DecodeAvroError(t *testing.T) { data := []byte{'O', 'b', 'j', 0x01, 0x01, 0x26, 0x16, 'a', 'v', 'r', 'o', '.', 's', 'c', 'h', 'e', 'm', 'a', 0x0c, '"', 'l', 'o', 'n', 'g', '"', 0x00, 0xfb, 0x2b, 0x0f, 0x1a, 0xdd, 0xfd, 0x90, 0x7d, 0x87, 0x12, @@ -878,6 +925,43 @@ func TestEncoder_EncodeCompressesZStandard(t *testing.T) { assert.Equal(t, 951, buf.Len()) } +func TestEncoder_EncodeCompressesZStandardWithLevel(t *testing.T) { + unionStr := "union value" + record := FullRecord{ + Strings: []string{"string1", "string2", "string3", "string4", "string5"}, + Longs: []int64{1, 2, 3, 4, 5}, + Enum: "C", + Map: map[string]int{ + "key1": 1, + "key2": 2, + "key3": 3, + "key4": 4, + "key5": 5, + }, + Nullable: &unionStr, + Fixed: [16]byte{0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04}, + Record: &TestRecord{ + Long: 1925639126735, + String: "I am a test record", + Int: 666, + Float: 7171.17, + Double: 916734926348163.01973408746523, + Bool: true, + }, + } + + buf := &bytes.Buffer{} + enc, _ := ocf.NewEncoder(schema, buf, ocf.WithCodec(ocf.ZStandard), ocf.WithZStandardEncoderOptions(zstd.WithEncoderLevel(zstd.SpeedBestCompression))) + + err := enc.Encode(record) + assert.NoError(t, err) + + err = enc.Close() + + require.NoError(t, err) + assert.Equal(t, 942, buf.Len()) +} + func TestEncoder_EncodeError(t *testing.T) { buf := &bytes.Buffer{} enc, err := ocf.NewEncoder(`"long"`, buf) diff --git a/pkg/crc64/crc64.go b/pkg/crc64/crc64.go index 889e4b3..6ca8205 100644 --- a/pkg/crc64/crc64.go +++ b/pkg/crc64/crc64.go @@ -13,6 +13,17 @@ func init() { // Size is the of a CRC-64 checksum in bytes. const Size = 8 +// ByteOrder denotes how integers are encoded into bytes. The ByteOrder +// interface in encoding/binary cancels some optimizations, so use a more +// direct implementation. +type ByteOrder int + +// ByteOrder constants. +const ( + LittleEndian ByteOrder = iota + BigEndian +) + // Empty is the empty checksum. const Empty = 0xc15d213aa4d7a795 @@ -38,16 +49,28 @@ func buildTable() { } type digest struct { - crc uint64 - tab *Table + crc uint64 + tab *Table + byteOrder ByteOrder } // New creates a new hash.Hash64 computing the Avro CRC-64 checksum. // Its Sum method will lay the value out in big-endian byte order. func New() hash.Hash64 { + return newDigest(BigEndian) +} + +// NewWithByteOrder creates a new hash.Hash64 computing the Avro CRC-64 +// checksum. Its Sum method will lay the value out in specified byte order. +func NewWithByteOrder(byteOrder ByteOrder) hash.Hash64 { + return newDigest(byteOrder) +} + +func newDigest(byteOrder ByteOrder) *digest { return &digest{ - crc: Empty, - tab: crc64Table, + crc: Empty, + tab: crc64Table, + byteOrder: byteOrder, } } @@ -82,16 +105,50 @@ func (d *digest) Sum64() uint64 { // Sum returns the checksum as a byte slice, using the given byte slice. func (d *digest) Sum(in []byte) []byte { + b := d.sumBytes() + return append(in, b[:]...) +} + +// sumBytes returns the checksum as a byte array in digest byte order. +func (d *digest) sumBytes() [Size]byte { s := d.Sum64() - return append(in, byte(s>>56), byte(s>>48), byte(s>>40), byte(s>>32), byte(s>>24), byte(s>>16), byte(s>>8), byte(s)) + + switch d.byteOrder { + case LittleEndian: + return [Size]byte{ + byte(s), + byte(s >> 8), + byte(s >> 16), + byte(s >> 24), + byte(s >> 32), + byte(s >> 40), + byte(s >> 48), + byte(s >> 56), + } + case BigEndian: + return [Size]byte{ + byte(s >> 56), + byte(s >> 48), + byte(s >> 40), + byte(s >> 32), + byte(s >> 24), + byte(s >> 16), + byte(s >> 8), + byte(s), + } + } + panic("unknown byte order") } -// Sum returns the MD5 checksum of the data. +// Sum returns the CRC64 checksum of the data, in big-endian byte order. func Sum(data []byte) [Size]byte { - d := digest{crc: Empty, tab: crc64Table} - d.Reset() + return SumWithByteOrder(data, BigEndian) +} + +// SumWithByteOrder returns the CRC64 checksum of the data, in specified byte +// order. +func SumWithByteOrder(data []byte, byteOrder ByteOrder) [Size]byte { + d := newDigest(byteOrder) _, _ = d.Write(data) - s := d.Sum64() - //nolint:lll - return [Size]byte{byte(s >> 56), byte(s >> 48), byte(s >> 40), byte(s >> 32), byte(s >> 24), byte(s >> 16), byte(s >> 8), byte(s)} + return d.sumBytes() } diff --git a/pkg/crc64/crc64_test.go b/pkg/crc64/crc64_test.go index 57fc162..569157a 100644 --- a/pkg/crc64/crc64_test.go +++ b/pkg/crc64/crc64_test.go @@ -82,6 +82,40 @@ func TestDigest_BlockSize(t *testing.T) { assert.Equal(t, 1, hash.BlockSize()) } +func TestGoldenSumWithByteOrder(t *testing.T) { + tests := []struct { + in string + be []byte + le []byte + }{ + { + in: `"null"`, + be: []byte{0x63, 0xdd, 0x24, 0xe7, 0xcc, 0x25, 0x8f, 0x8a}, + le: []byte{0x8a, 0x8f, 0x25, 0xcc, 0xe7, 0x24, 0xdd, 0x63}, + }, + { + in: `{"name":"foo","type":"fixed","size":15}`, + be: []byte{0x18, 0x60, 0x2e, 0xc3, 0xed, 0x31, 0xa5, 0x04}, + le: []byte{0x04, 0xa5, 0x31, 0xed, 0xc3, 0x2e, 0x60, 0x18}, + }, + { + in: `{"name":"foo","type":"record","fields":[{"name":"f1","type":"boolean"}]}`, + be: []byte{0x6c, 0xd8, 0xea, 0xf1, 0xc9, 0x68, 0xa3, 0x3b}, + le: []byte{0x3b, 0xa3, 0x68, 0xc9, 0xf1, 0xea, 0xd8, 0x6c}, + }, + } + + for i, test := range tests { + t.Run(strconv.Itoa(i), func(t *testing.T) { + got := SumWithByteOrder([]byte(test.in), BigEndian) + assert.Equal(t, test.be, got[:]) + + got = SumWithByteOrder([]byte(test.in), LittleEndian) + assert.Equal(t, test.le, got[:]) + }) + } +} + func bench(b *testing.B, size int64) { b.SetBytes(size) @@ -115,3 +149,27 @@ func BenchmarkCrc64(b *testing.B) { bench(b, 1<<10) }) } + +func BenchmarkSum(b *testing.B) { + data := make([]byte, 4<<10) + for i := range data { + data[i] = byte(i) + } + + b.Run("BigEndian", func(b *testing.B) { + b.SetBytes(int64(len(data))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = SumWithByteOrder(data, BigEndian) + } + }) + b.Run("LittleEndian", func(b *testing.B) { + b.SetBytes(int64(len(data))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = SumWithByteOrder(data, LittleEndian) + } + }) +} diff --git a/schema.go b/schema.go index b3b97d6..7cba4c8 100644 --- a/schema.go +++ b/schema.go @@ -106,9 +106,10 @@ type FingerprintType string // Fingerprint type constants. const ( - CRC64Avro FingerprintType = "CRC64-AVRO" - MD5 FingerprintType = "MD5" - SHA256 FingerprintType = "SHA256" + CRC64Avro FingerprintType = "CRC64-AVRO" + CRC64AvroLE FingerprintType = "CRC64-AVRO-LE" + MD5 FingerprintType = "MD5" + SHA256 FingerprintType = "SHA256" ) // SchemaCache is a cache of schemas. @@ -306,6 +307,9 @@ func (f *fingerprinter) FingerprintUsing(typ FingerprintType, stringer fmt.Strin case CRC64Avro: h := crc64.Sum(data) fingerprint = h[:] + case CRC64AvroLE: + h := crc64.SumWithByteOrder(data, crc64.LittleEndian) + fingerprint = h[:] case MD5: h := md5.Sum(data) fingerprint = h[:] diff --git a/schema_test.go b/schema_test.go index cc8ccea..c5f70a9 100644 --- a/schema_test.go +++ b/schema_test.go @@ -1216,6 +1216,12 @@ func TestSchema_FingerprintUsing(t *testing.T) { typ: avro.CRC64Avro, want: []byte{0x63, 0xdd, 0x24, 0xe7, 0xcc, 0x25, 0x8f, 0x8a}, }, + { + name: "Null CRC64-AVRO-LE", + schema: "null", + typ: avro.CRC64AvroLE, + want: []byte{0x8a, 0x8f, 0x25, 0xcc, 0xe7, 0x24, 0xdd, 0x63}, + }, { name: "Null MD5", schema: "null",