From 7a634401337b437efe5ff8752a005eb6ae79ab06 Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Sat, 9 May 2026 11:53:02 +0800 Subject: [PATCH 01/18] feat(verity-go): scaffold Go module and FlatBuffers generated code --- .gitignore | 3 + CLAUDE.md | 8 + verity-go/go.mod | 5 + verity-go/go.sum | 2 + .../metadata/generated/metadata_generated.go | 499 ++++++++++++++++++ .../generated/metadata_hash_generated.go | 148 ++++++ verity-go/metadata/metadata.fbs | 36 ++ verity-go/metadata/metadata_hash.fbs | 19 + 8 files changed, 720 insertions(+) create mode 100644 verity-go/go.mod create mode 100644 verity-go/go.sum create mode 100644 verity-go/metadata/generated/metadata_generated.go create mode 100644 verity-go/metadata/generated/metadata_hash_generated.go create mode 100644 verity-go/metadata/metadata.fbs create mode 100644 verity-go/metadata/metadata_hash.fbs diff --git a/.gitignore b/.gitignore index f5a43d71..3151c7d3 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ cryptpilot-verity/benchmark/verity_source_data/ # Superpowers design docs docs/superpowers/ + +# Claude session artifacts +.claude/ diff --git a/CLAUDE.md b/CLAUDE.md index 9bd8659b..eb4cd473 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,13 @@ # CLAUDE.md +## Rust-Go Sync + +When modifying the Rust `cryptpilot-verity`, `verity-core`, or `verity-fuse` code, always evaluate whether the corresponding Go library (`verity-go/`) needs the same change. If the change affects core algorithms (hash computation, merkle tree, descriptor format) or metadata structures (FlatBuffers schema, serialization), apply the equivalent change to the Go code in the same commit. + +## Excluded Paths + +Never commit files under `docs/superpowers/` or `.claude/` to git. These are Claude session artifacts and should be kept local only. Add them to `.gitignore` if not already present. + ## Git Commit Requirements When creating or amending commits: diff --git a/verity-go/go.mod b/verity-go/go.mod new file mode 100644 index 00000000..eb19100f --- /dev/null +++ b/verity-go/go.mod @@ -0,0 +1,5 @@ +module cryptpilot-verity-go + +go 1.24 + +require github.com/google/flatbuffers v25.12.19+incompatible diff --git a/verity-go/go.sum b/verity-go/go.sum new file mode 100644 index 00000000..799c59f0 --- /dev/null +++ b/verity-go/go.sum @@ -0,0 +1,2 @@ +github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs= +github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= diff --git a/verity-go/metadata/generated/metadata_generated.go b/verity-go/metadata/generated/metadata_generated.go new file mode 100644 index 00000000..34e09ea1 --- /dev/null +++ b/verity-go/metadata/generated/metadata_generated.go @@ -0,0 +1,499 @@ +// Code generated by the FlatBuffers compiler. DO NOT EDIT. + +package generated + +import ( + flatbuffers "github.com/google/flatbuffers/go" +) + +// Code generated by the FlatBuffers compiler. DO NOT EDIT. + + + +type FsVerityDescriptor struct { + _tab flatbuffers.Table +} + +func GetRootAsFsVerityDescriptor(buf []byte, offset flatbuffers.UOffsetT) *FsVerityDescriptor { + n := flatbuffers.GetUOffsetT(buf[offset:]) + x := &FsVerityDescriptor{} + x.Init(buf, n+offset) + return x +} + +func FinishFsVerityDescriptorBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { + builder.Finish(offset) +} + +func GetSizePrefixedRootAsFsVerityDescriptor(buf []byte, offset flatbuffers.UOffsetT) *FsVerityDescriptor { + n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) + x := &FsVerityDescriptor{} + x.Init(buf, n+offset+flatbuffers.SizeUint32) + return x +} + +func FinishSizePrefixedFsVerityDescriptorBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { + builder.FinishSizePrefixed(offset) +} + +func (rcv *FsVerityDescriptor) Init(buf []byte, i flatbuffers.UOffsetT) { + rcv._tab.Bytes = buf + rcv._tab.Pos = i +} + +func (rcv *FsVerityDescriptor) Table() flatbuffers.Table { + return rcv._tab +} + +func (rcv *FsVerityDescriptor) Version() byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(4)) + if o != 0 { + return rcv._tab.GetByte(o + rcv._tab.Pos) + } + return 0 +} + +func (rcv *FsVerityDescriptor) MutateVersion(n byte) bool { + return rcv._tab.MutateByteSlot(4, n) +} + +func (rcv *FsVerityDescriptor) HashAlgorithm() byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(6)) + if o != 0 { + return rcv._tab.GetByte(o + rcv._tab.Pos) + } + return 0 +} + +func (rcv *FsVerityDescriptor) MutateHashAlgorithm(n byte) bool { + return rcv._tab.MutateByteSlot(6, n) +} + +func (rcv *FsVerityDescriptor) LogBlocksize() byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(8)) + if o != 0 { + return rcv._tab.GetByte(o + rcv._tab.Pos) + } + return 0 +} + +func (rcv *FsVerityDescriptor) MutateLogBlocksize(n byte) bool { + return rcv._tab.MutateByteSlot(8, n) +} + +func (rcv *FsVerityDescriptor) DataSize() uint64 { + o := flatbuffers.UOffsetT(rcv._tab.Offset(10)) + if o != 0 { + return rcv._tab.GetUint64(o + rcv._tab.Pos) + } + return 0 +} + +func (rcv *FsVerityDescriptor) MutateDataSize(n uint64) bool { + return rcv._tab.MutateUint64Slot(10, n) +} + +func (rcv *FsVerityDescriptor) RootHash(j int) byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(12)) + if o != 0 { + a := rcv._tab.Vector(o) + return rcv._tab.GetByte(a + flatbuffers.UOffsetT(j*1)) + } + return 0 +} + +func (rcv *FsVerityDescriptor) RootHashLength() int { + o := flatbuffers.UOffsetT(rcv._tab.Offset(12)) + if o != 0 { + return rcv._tab.VectorLen(o) + } + return 0 +} + +func (rcv *FsVerityDescriptor) RootHashBytes() []byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(12)) + if o != 0 { + return rcv._tab.ByteVector(o + rcv._tab.Pos) + } + return nil +} + +func (rcv *FsVerityDescriptor) MutateRootHash(j int, n byte) bool { + o := flatbuffers.UOffsetT(rcv._tab.Offset(12)) + if o != 0 { + a := rcv._tab.Vector(o) + return rcv._tab.MutateByte(a+flatbuffers.UOffsetT(j*1), n) + } + return false +} + +func (rcv *FsVerityDescriptor) Salt(j int) byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(14)) + if o != 0 { + a := rcv._tab.Vector(o) + return rcv._tab.GetByte(a + flatbuffers.UOffsetT(j*1)) + } + return 0 +} + +func (rcv *FsVerityDescriptor) SaltLength() int { + o := flatbuffers.UOffsetT(rcv._tab.Offset(14)) + if o != 0 { + return rcv._tab.VectorLen(o) + } + return 0 +} + +func (rcv *FsVerityDescriptor) SaltBytes() []byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(14)) + if o != 0 { + return rcv._tab.ByteVector(o + rcv._tab.Pos) + } + return nil +} + +func (rcv *FsVerityDescriptor) MutateSalt(j int, n byte) bool { + o := flatbuffers.UOffsetT(rcv._tab.Offset(14)) + if o != 0 { + a := rcv._tab.Vector(o) + return rcv._tab.MutateByte(a+flatbuffers.UOffsetT(j*1), n) + } + return false +} + +func FsVerityDescriptorStart(builder *flatbuffers.Builder) { + builder.StartObject(6) +} +func FsVerityDescriptorAddVersion(builder *flatbuffers.Builder, version byte) { + builder.PrependByteSlot(0, version, 0) +} +func FsVerityDescriptorAddHashAlgorithm(builder *flatbuffers.Builder, hashAlgorithm byte) { + builder.PrependByteSlot(1, hashAlgorithm, 0) +} +func FsVerityDescriptorAddLogBlocksize(builder *flatbuffers.Builder, logBlocksize byte) { + builder.PrependByteSlot(2, logBlocksize, 0) +} +func FsVerityDescriptorAddDataSize(builder *flatbuffers.Builder, dataSize uint64) { + builder.PrependUint64Slot(3, dataSize, 0) +} +func FsVerityDescriptorAddRootHash(builder *flatbuffers.Builder, rootHash flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(4, flatbuffers.UOffsetT(rootHash), 0) +} +func FsVerityDescriptorStartRootHashVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT { + return builder.StartVector(1, numElems, 1) +} +func FsVerityDescriptorAddSalt(builder *flatbuffers.Builder, salt flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(5, flatbuffers.UOffsetT(salt), 0) +} +func FsVerityDescriptorStartSaltVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT { + return builder.StartVector(1, numElems, 1) +} +func FsVerityDescriptorEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { + return builder.EndObject() +} + +// Code generated by the FlatBuffers compiler. DO NOT EDIT. + + + +type FileInfo struct { + _tab flatbuffers.Table +} + +func GetRootAsFileInfo(buf []byte, offset flatbuffers.UOffsetT) *FileInfo { + n := flatbuffers.GetUOffsetT(buf[offset:]) + x := &FileInfo{} + x.Init(buf, n+offset) + return x +} + +func FinishFileInfoBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { + builder.Finish(offset) +} + +func GetSizePrefixedRootAsFileInfo(buf []byte, offset flatbuffers.UOffsetT) *FileInfo { + n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) + x := &FileInfo{} + x.Init(buf, n+offset+flatbuffers.SizeUint32) + return x +} + +func FinishSizePrefixedFileInfoBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { + builder.FinishSizePrefixed(offset) +} + +func (rcv *FileInfo) Init(buf []byte, i flatbuffers.UOffsetT) { + rcv._tab.Bytes = buf + rcv._tab.Pos = i +} + +func (rcv *FileInfo) Table() flatbuffers.Table { + return rcv._tab +} + +func (rcv *FileInfo) Path() []byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(4)) + if o != 0 { + return rcv._tab.ByteVector(o + rcv._tab.Pos) + } + return nil +} + +func (rcv *FileInfo) Descriptor(obj *FsVerityDescriptor) *FsVerityDescriptor { + o := flatbuffers.UOffsetT(rcv._tab.Offset(6)) + if o != 0 { + x := rcv._tab.Indirect(o + rcv._tab.Pos) + if obj == nil { + obj = new(FsVerityDescriptor) + } + obj.Init(rcv._tab.Bytes, x) + return obj + } + return nil +} + +func (rcv *FileInfo) MerkleTreeLevel1(j int) byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(8)) + if o != 0 { + a := rcv._tab.Vector(o) + return rcv._tab.GetByte(a + flatbuffers.UOffsetT(j*1)) + } + return 0 +} + +func (rcv *FileInfo) MerkleTreeLevel1Length() int { + o := flatbuffers.UOffsetT(rcv._tab.Offset(8)) + if o != 0 { + return rcv._tab.VectorLen(o) + } + return 0 +} + +func (rcv *FileInfo) MerkleTreeLevel1Bytes() []byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(8)) + if o != 0 { + return rcv._tab.ByteVector(o + rcv._tab.Pos) + } + return nil +} + +func (rcv *FileInfo) MutateMerkleTreeLevel1(j int, n byte) bool { + o := flatbuffers.UOffsetT(rcv._tab.Offset(8)) + if o != 0 { + a := rcv._tab.Vector(o) + return rcv._tab.MutateByte(a+flatbuffers.UOffsetT(j*1), n) + } + return false +} + +func (rcv *FileInfo) DescriptorHash() []byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(10)) + if o != 0 { + return rcv._tab.ByteVector(o + rcv._tab.Pos) + } + return nil +} + +func FileInfoStart(builder *flatbuffers.Builder) { + builder.StartObject(4) +} +func FileInfoAddPath(builder *flatbuffers.Builder, path flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(path), 0) +} +func FileInfoAddDescriptor(builder *flatbuffers.Builder, descriptor flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(descriptor), 0) +} +func FileInfoAddMerkleTreeLevel1(builder *flatbuffers.Builder, merkleTreeLevel1 flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(merkleTreeLevel1), 0) +} +func FileInfoStartMerkleTreeLevel1Vector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT { + return builder.StartVector(1, numElems, 1) +} +func FileInfoAddDescriptorHash(builder *flatbuffers.Builder, descriptorHash flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(3, flatbuffers.UOffsetT(descriptorHash), 0) +} +func FileInfoEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { + return builder.EndObject() +} + +// Code generated by the FlatBuffers compiler. DO NOT EDIT. + + + +type KeyValue struct { + _tab flatbuffers.Table +} + +func GetRootAsKeyValue(buf []byte, offset flatbuffers.UOffsetT) *KeyValue { + n := flatbuffers.GetUOffsetT(buf[offset:]) + x := &KeyValue{} + x.Init(buf, n+offset) + return x +} + +func FinishKeyValueBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { + builder.Finish(offset) +} + +func GetSizePrefixedRootAsKeyValue(buf []byte, offset flatbuffers.UOffsetT) *KeyValue { + n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) + x := &KeyValue{} + x.Init(buf, n+offset+flatbuffers.SizeUint32) + return x +} + +func FinishSizePrefixedKeyValueBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { + builder.FinishSizePrefixed(offset) +} + +func (rcv *KeyValue) Init(buf []byte, i flatbuffers.UOffsetT) { + rcv._tab.Bytes = buf + rcv._tab.Pos = i +} + +func (rcv *KeyValue) Table() flatbuffers.Table { + return rcv._tab +} + +func (rcv *KeyValue) Key() []byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(4)) + if o != 0 { + return rcv._tab.ByteVector(o + rcv._tab.Pos) + } + return nil +} + +func (rcv *KeyValue) Value() []byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(6)) + if o != 0 { + return rcv._tab.ByteVector(o + rcv._tab.Pos) + } + return nil +} + +func KeyValueStart(builder *flatbuffers.Builder) { + builder.StartObject(2) +} +func KeyValueAddKey(builder *flatbuffers.Builder, key flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(key), 0) +} +func KeyValueAddValue(builder *flatbuffers.Builder, value flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(value), 0) +} +func KeyValueEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { + return builder.EndObject() +} + +// Code generated by the FlatBuffers compiler. DO NOT EDIT. + + + +type Metadata struct { + _tab flatbuffers.Table +} + +func GetRootAsMetadata(buf []byte, offset flatbuffers.UOffsetT) *Metadata { + n := flatbuffers.GetUOffsetT(buf[offset:]) + x := &Metadata{} + x.Init(buf, n+offset) + return x +} + +func FinishMetadataBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { + builder.Finish(offset) +} + +func GetSizePrefixedRootAsMetadata(buf []byte, offset flatbuffers.UOffsetT) *Metadata { + n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) + x := &Metadata{} + x.Init(buf, n+offset+flatbuffers.SizeUint32) + return x +} + +func FinishSizePrefixedMetadataBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { + builder.FinishSizePrefixed(offset) +} + +func (rcv *Metadata) Init(buf []byte, i flatbuffers.UOffsetT) { + rcv._tab.Bytes = buf + rcv._tab.Pos = i +} + +func (rcv *Metadata) Table() flatbuffers.Table { + return rcv._tab +} + +func (rcv *Metadata) Version() uint32 { + o := flatbuffers.UOffsetT(rcv._tab.Offset(4)) + if o != 0 { + return rcv._tab.GetUint32(o + rcv._tab.Pos) + } + return 1 +} + +func (rcv *Metadata) MutateVersion(n uint32) bool { + return rcv._tab.MutateUint32Slot(4, n) +} + +func (rcv *Metadata) Files(obj *FileInfo, j int) bool { + o := flatbuffers.UOffsetT(rcv._tab.Offset(6)) + if o != 0 { + x := rcv._tab.Vector(o) + x += flatbuffers.UOffsetT(j) * 4 + x = rcv._tab.Indirect(x) + obj.Init(rcv._tab.Bytes, x) + return true + } + return false +} + +func (rcv *Metadata) FilesLength() int { + o := flatbuffers.UOffsetT(rcv._tab.Offset(6)) + if o != 0 { + return rcv._tab.VectorLen(o) + } + return 0 +} + +func (rcv *Metadata) Labels(obj *KeyValue, j int) bool { + o := flatbuffers.UOffsetT(rcv._tab.Offset(8)) + if o != 0 { + x := rcv._tab.Vector(o) + x += flatbuffers.UOffsetT(j) * 4 + x = rcv._tab.Indirect(x) + obj.Init(rcv._tab.Bytes, x) + return true + } + return false +} + +func (rcv *Metadata) LabelsLength() int { + o := flatbuffers.UOffsetT(rcv._tab.Offset(8)) + if o != 0 { + return rcv._tab.VectorLen(o) + } + return 0 +} + +func MetadataStart(builder *flatbuffers.Builder) { + builder.StartObject(3) +} +func MetadataAddVersion(builder *flatbuffers.Builder, version uint32) { + builder.PrependUint32Slot(0, version, 1) +} +func MetadataAddFiles(builder *flatbuffers.Builder, files flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(files), 0) +} +func MetadataStartFilesVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT { + return builder.StartVector(4, numElems, 4) +} +func MetadataAddLabels(builder *flatbuffers.Builder, labels flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(labels), 0) +} +func MetadataStartLabelsVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT { + return builder.StartVector(4, numElems, 4) +} +func MetadataEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { + return builder.EndObject() +} + diff --git a/verity-go/metadata/generated/metadata_hash_generated.go b/verity-go/metadata/generated/metadata_hash_generated.go new file mode 100644 index 00000000..72f2b182 --- /dev/null +++ b/verity-go/metadata/generated/metadata_hash_generated.go @@ -0,0 +1,148 @@ +// Code generated by the FlatBuffers compiler. DO NOT EDIT. + +package generated + +import ( + flatbuffers "github.com/google/flatbuffers/go" +) + +// Code generated by the FlatBuffers compiler. DO NOT EDIT. + + + +type FileHashEntry struct { + _tab flatbuffers.Table +} + +func GetRootAsFileHashEntry(buf []byte, offset flatbuffers.UOffsetT) *FileHashEntry { + n := flatbuffers.GetUOffsetT(buf[offset:]) + x := &FileHashEntry{} + x.Init(buf, n+offset) + return x +} + +func FinishFileHashEntryBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { + builder.Finish(offset) +} + +func GetSizePrefixedRootAsFileHashEntry(buf []byte, offset flatbuffers.UOffsetT) *FileHashEntry { + n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) + x := &FileHashEntry{} + x.Init(buf, n+offset+flatbuffers.SizeUint32) + return x +} + +func FinishSizePrefixedFileHashEntryBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { + builder.FinishSizePrefixed(offset) +} + +func (rcv *FileHashEntry) Init(buf []byte, i flatbuffers.UOffsetT) { + rcv._tab.Bytes = buf + rcv._tab.Pos = i +} + +func (rcv *FileHashEntry) Table() flatbuffers.Table { + return rcv._tab +} + +func (rcv *FileHashEntry) Path() []byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(4)) + if o != 0 { + return rcv._tab.ByteVector(o + rcv._tab.Pos) + } + return nil +} + +func (rcv *FileHashEntry) DescriptorHash() []byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(6)) + if o != 0 { + return rcv._tab.ByteVector(o + rcv._tab.Pos) + } + return nil +} + +func FileHashEntryStart(builder *flatbuffers.Builder) { + builder.StartObject(2) +} +func FileHashEntryAddPath(builder *flatbuffers.Builder, path flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(path), 0) +} +func FileHashEntryAddDescriptorHash(builder *flatbuffers.Builder, descriptorHash flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(descriptorHash), 0) +} +func FileHashEntryEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { + return builder.EndObject() +} + +// Code generated by the FlatBuffers compiler. DO NOT EDIT. + + + +type MetadataHash struct { + _tab flatbuffers.Table +} + +func GetRootAsMetadataHash(buf []byte, offset flatbuffers.UOffsetT) *MetadataHash { + n := flatbuffers.GetUOffsetT(buf[offset:]) + x := &MetadataHash{} + x.Init(buf, n+offset) + return x +} + +func FinishMetadataHashBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { + builder.Finish(offset) +} + +func GetSizePrefixedRootAsMetadataHash(buf []byte, offset flatbuffers.UOffsetT) *MetadataHash { + n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) + x := &MetadataHash{} + x.Init(buf, n+offset+flatbuffers.SizeUint32) + return x +} + +func FinishSizePrefixedMetadataHashBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { + builder.FinishSizePrefixed(offset) +} + +func (rcv *MetadataHash) Init(buf []byte, i flatbuffers.UOffsetT) { + rcv._tab.Bytes = buf + rcv._tab.Pos = i +} + +func (rcv *MetadataHash) Table() flatbuffers.Table { + return rcv._tab +} + +func (rcv *MetadataHash) Files(obj *FileHashEntry, j int) bool { + o := flatbuffers.UOffsetT(rcv._tab.Offset(4)) + if o != 0 { + x := rcv._tab.Vector(o) + x += flatbuffers.UOffsetT(j) * 4 + x = rcv._tab.Indirect(x) + obj.Init(rcv._tab.Bytes, x) + return true + } + return false +} + +func (rcv *MetadataHash) FilesLength() int { + o := flatbuffers.UOffsetT(rcv._tab.Offset(4)) + if o != 0 { + return rcv._tab.VectorLen(o) + } + return 0 +} + +func MetadataHashStart(builder *flatbuffers.Builder) { + builder.StartObject(1) +} +func MetadataHashAddFiles(builder *flatbuffers.Builder, files flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(files), 0) +} +func MetadataHashStartFilesVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT { + return builder.StartVector(4, numElems, 4) +} +func MetadataHashEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { + return builder.EndObject() +} + diff --git a/verity-go/metadata/metadata.fbs b/verity-go/metadata/metadata.fbs new file mode 100644 index 00000000..746c8d0c --- /dev/null +++ b/verity-go/metadata/metadata.fbs @@ -0,0 +1,36 @@ +// FlatBuffers schema for cryptpilot-verity metadata + +namespace cryptpilot.verity; + +// FsVerity descriptor information +table FsVerityDescriptor { + version: ubyte; // must be 1 + hash_algorithm: ubyte; // Merkle tree hash algorithm (1=SHA256, 2=SHA512) + log_blocksize: ubyte; // log2 of size of data and tree blocks + data_size: ulong; // size of file the Merkle tree is built over + root_hash: [ubyte]; // Merkle tree root hash (binary) + salt: [ubyte]; // salt prepended to each hashed block +} + +// Key-value pair for metadata labels +table KeyValue { + key: string; + value: string; +} + +// Single file information with fs-verity data +table FileInfo { + path: string; // relative path of the file + descriptor: FsVerityDescriptor; // fs-verity descriptor + merkle_tree_level1: [ubyte]; // Level 1 merkle tree hashes (concatenated binary) + descriptor_hash: string; // hex-encoded descriptor hash (final measurement) +} + +// Root metadata structures containing all file information +table Metadata { + version: uint = 1; // metadata format version for backward compatibility + files: [FileInfo]; + labels: [KeyValue]; +} + +root_type Metadata; diff --git a/verity-go/metadata/metadata_hash.fbs b/verity-go/metadata/metadata_hash.fbs new file mode 100644 index 00000000..daf678c5 --- /dev/null +++ b/verity-go/metadata/metadata_hash.fbs @@ -0,0 +1,19 @@ +// FlatBuffers schema for metadata hash calculation +// This defines a minimal structure containing only the fields +// needed for hash calculation, avoiding redundancy. + +namespace cryptpilot.verity.hash; + +// File entry for hash calculation (only essential fields) +table FileHashEntry { + path: string; // relative path of the file + descriptor_hash: string; // hex-encoded descriptor hash +} + +// Metadata structure for hash calculation +// This contains only the minimal information needed to verify integrity +table MetadataHash { + files: [FileHashEntry]; // sorted by path for deterministic hash +} + +root_type MetadataHash; From 255287078d718df50bf3d6d6a5e9f0740986a7ae Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Sat, 9 May 2026 12:01:18 +0800 Subject: [PATCH 02/18] feat(verity-go): add FsVerityDescriptor and HashAlgorithm types --- verity-go/verity/verity.go | 139 +++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 verity-go/verity/verity.go diff --git a/verity-go/verity/verity.go b/verity-go/verity/verity.go new file mode 100644 index 00000000..48a8b79b --- /dev/null +++ b/verity-go/verity/verity.go @@ -0,0 +1,139 @@ +// verity-go/verity/verity.go +package verity + +import ( + "crypto/sha256" + "crypto/sha512" + "hash" +) + +const ( + MaxDigestSize = 64 + MaxSaltSize = 32 + MaxLevels = 8 // FS_VERITY_MAX_LEVELS + DefaultBlockSize = 4096 +) + +// HashAlgorithm matches kernel FS_VERITY_HASH_ALG_* values. +type HashAlgorithm uint8 + +const ( + HashSHA256 HashAlgorithm = 1 + HashSHA512 HashAlgorithm = 2 +) + +func (h HashAlgorithm) String() string { + switch h { + case HashSHA256: + return "sha256" + case HashSHA512: + return "sha512" + default: + return "unknown" + } +} + +func (h HashAlgorithm) digestSize() int { + switch h { + case HashSHA256: + return sha256.Size + case HashSHA512: + return sha512.Size + default: + return 0 + } +} + +func (h HashAlgorithm) blockSize() int { + switch h { + case HashSHA256: + return sha256.BlockSize + case HashSHA512: + return sha512.BlockSize + default: + return 0 + } +} + +func (h HashAlgorithm) newHash() hash.Hash { + switch h { + case HashSHA256: + return sha256.New() + case HashSHA512: + return sha512.New() + default: + return nil + } +} + +// FsVerityDescriptor is a kernel-compatible fs-verity descriptor. +type FsVerityDescriptor struct { + Version uint8 + HashAlgorithm HashAlgorithm + LogBlocksize uint8 + DataSize uint64 + RootHash []byte // len = digestSize (32 for SHA-256, 64 for SHA-512) + Salt []byte // 0..32 bytes +} + +// BlockSize returns the Merkle tree block size (1 << LogBlocksize). +func (d *FsVerityDescriptor) BlockSize() int { + return 1 << d.LogBlocksize +} + +// ToDescriptorHash computes the salted hash of the descriptor (kernel format). +// See: https://www.kernel.org/doc/html/latest/filesystems/fsverity.html#fs-verity-descriptor +func (d *FsVerityDescriptor) ToDescriptorHash() []byte { + h := saltToDigest(d.HashAlgorithm, d.Salt) + h.Write([]byte{d.Version}) + h.Write([]byte{uint8(d.HashAlgorithm)}) + h.Write([]byte{d.LogBlocksize}) + h.Write([]byte{uint8(len(d.Salt))}) + h.Write(make([]byte, 4)) // __reserved_0x04 + h.Write(d.DataSizeToBytes()) + hashPadded(h, d.RootHash, 64) + hashPadded(h, d.Salt, 32) + h.Write(make([]byte, 144)) // __reserved[144] + return h.Sum(nil) +} + +// DataSizeToBytes returns data_size as little-endian bytes. +func (d *FsVerityDescriptor) DataSizeToBytes() []byte { + b := make([]byte, 8) + b[0] = byte(d.DataSize) + b[1] = byte(d.DataSize >> 8) + b[2] = byte(d.DataSize >> 16) + b[3] = byte(d.DataSize >> 24) + b[4] = byte(d.DataSize >> 32) + b[5] = byte(d.DataSize >> 40) + b[6] = byte(d.DataSize >> 48) + b[7] = byte(d.DataSize >> 56) + return b +} + +// saltToDigest creates a hash.Hash initialized with the salt padded to block boundaries. +func saltToDigest(algo HashAlgorithm, salt []byte) hash.Hash { + h := algo.newHash() + blockSize := algo.blockSize() + for i := 0; i < len(salt); i += blockSize { + end := i + blockSize + if end > len(salt) { + end = len(salt) + } + h.Write(salt[i:end]) + // Pad this chunk to block size + padding := blockSize - (end - i) + if padding < blockSize { + h.Write(make([]byte, padding)) + } + } + return h +} + +// hashPadded writes data to the hash, padding with zeros to the target size. +func hashPadded(h hash.Hash, data []byte, targetSize int) { + h.Write(data) + if len(data) < targetSize { + h.Write(make([]byte, targetSize-len(data))) + } +} From 91c75ad91a71688b3580ec2367483896adfcd837 Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Sat, 9 May 2026 13:48:23 +0800 Subject: [PATCH 03/18] feat(verity-go): add MerkleTree type --- verity-go/verity/tree.go | 75 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 verity-go/verity/tree.go diff --git a/verity-go/verity/tree.go b/verity-go/verity/tree.go new file mode 100644 index 00000000..ad8ea0dc --- /dev/null +++ b/verity-go/verity/tree.go @@ -0,0 +1,75 @@ +// verity-go/verity/tree.go +package verity + +// MerkleTree stores level-1 hashes (one per data block). +type MerkleTree struct { + level1 [][]byte + algo HashAlgorithm +} + +// NewMerkleTree creates a tree from level-1 hashes. +func NewMerkleTree(hashes [][]byte, algo HashAlgorithm) *MerkleTree { + return &MerkleTree{level1: hashes, algo: algo} +} + +// Level1AsBytes returns the level-1 hashes as concatenated bytes. +func (t *MerkleTree) Level1AsBytes() []byte { + digestSize := t.algo.digestSize() + out := make([]byte, 0, len(t.level1)*digestSize) + for _, h := range t.level1 { + out = append(out, h...) + } + return out +} + +// RebuildRootHash reconstructs the root hash from level-1 hashes. +// Used when loading a tree from serialized metadata. +func (t *MerkleTree) RebuildRootHash(salt []byte, blockSize int) []byte { + if len(t.level1) == 1 { + return t.level1[0] + } + + h := saltToDigest(t.algo, salt) + for _, hash := range t.level1 { + h.Write(hash) + } + // Pad to block boundary + padding := (blockSize - (len(t.level1)*t.algo.digestSize())%blockSize) % blockSize + if padding > 0 { + h.Write(make([]byte, padding)) + } + return h.Sum(nil) +} + +// VerifyDataBlock verifies a single data block against the merkle tree. +func (t *MerkleTree) VerifyDataBlock(blockIndex int, blockSize int, data []byte) bool { + if len(data) > blockSize { + return false + } + if blockIndex >= len(t.level1) { + return false + } + expected := t.level1[blockIndex] + + h := t.algo.newHash() + h.Write(data) + // Pad to block size + if len(data) < blockSize { + h.Write(make([]byte, blockSize-len(data))) + } + actual := h.Sum(nil) + + return equalBytes(actual, expected) +} + +func equalBytes(a, b []byte) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} From 2674c98710527d9049e3982594fbcac579a51481 Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Sat, 9 May 2026 14:28:16 +0800 Subject: [PATCH 04/18] feat(verity-go): add FsVerityDigest streaming hasher --- verity-go/verity/digest.go | 224 +++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 verity-go/verity/digest.go diff --git a/verity-go/verity/digest.go b/verity-go/verity/digest.go new file mode 100644 index 00000000..023a02a5 --- /dev/null +++ b/verity-go/verity/digest.go @@ -0,0 +1,224 @@ +// verity-go/verity/digest.go +package verity + +import "hash" + +// fixedSizeBlock tracks a hash being filled to block_size bytes. +type fixedSizeBlock struct { + h hash.Hash + remaining int + algo HashAlgorithm + salt []byte + blockSize int +} + +func newFixedSizeBlock(algo HashAlgorithm, salt []byte, blockSize int) fixedSizeBlock { + return fixedSizeBlock{ + h: newSaltedHash(algo, salt), + remaining: blockSize, + algo: algo, + salt: salt, + blockSize: blockSize, + } +} + +// newSaltedHash creates a hash.Hash initialized with the salt. +func newSaltedHash(algo HashAlgorithm, salt []byte) hash.Hash { + h := algo.newHash() + blockSize := algo.blockSize() + for i := 0; i < len(salt); i += blockSize { + end := i + blockSize + if end > len(salt) { + end = len(salt) + } + h.Write(salt[i:end]) + padding := blockSize - (end - i) + if padding < blockSize { + h.Write(make([]byte, padding)) + } + } + return h +} + +// append writes data to the block. +func (b *fixedSizeBlock) append(data []byte) { + b.h.Write(data) + b.remaining -= len(data) +} + +// fillToEnd pads with zeros to block_size and returns the hash. +func (b *fixedSizeBlock) fillToEnd() []byte { + if b.remaining > 0 { + b.h.Write(make([]byte, b.remaining)) + b.remaining = 0 + } + return b.h.Sum(nil) +} + +// overflowingAppend appends as much as possible, returning the overflow. +func (b *fixedSizeBlock) overflowingAppend(data []byte) []byte { + n := b.remaining + if len(data) < n { + n = len(data) + } + b.append(data[:n]) + return data[n:] +} + +// finalizeAndReset computes the hash and resets to a fresh salted state. +func (b *fixedSizeBlock) finalizeAndReset() []byte { + out := b.fillToEnd() + b.h = newSaltedHash(b.algo, b.salt) + b.remaining = b.blockSize + return out +} + +// FsVerityConfig holds the parameters for fs-verity hashing. +type FsVerityConfig struct { + blockSize int + salt []byte + algo HashAlgorithm +} + +// FsVerityDigest is a streaming fs-verity hasher. +type FsVerityDigest struct { + config FsVerityConfig + levels []fixedSizeBlock + merkleTree *MerkleTree +} + +// NewFsVerity creates a new fs-verity hasher with empty salt. +func NewFsVerity(algo HashAlgorithm) *FsVerityDigest { + return NewFsVerityWithSaltAndBlockSize(algo, nil, DefaultBlockSize) +} + +// NewFsVerityWithSalt creates a new fs-verity hasher with the given salt. +func NewFsVerityWithSalt(algo HashAlgorithm, salt []byte) *FsVerityDigest { + return NewFsVerityWithSaltAndBlockSize(algo, salt, DefaultBlockSize) +} + +// NewFsVerityWithSaltAndBlockSize creates a new fs-verity hasher with custom parameters. +func NewFsVerityWithSaltAndBlockSize(algo HashAlgorithm, salt []byte, blockSize int) *FsVerityDigest { + return &FsVerityDigest{ + config: FsVerityConfig{ + blockSize: blockSize, + salt: salt, + algo: algo, + }, + levels: make([]fixedSizeBlock, 0), + merkleTree: &MerkleTree{algo: algo}, + } +} + +// Write implements io.Writer. +func (d *FsVerityDigest) Write(data []byte) (int, error) { + d.update(data) + return len(data), nil +} + +// update processes data in block_size chunks, matching the Rust implementation. +func (d *FsVerityDigest) update(data []byte) { + digestSize := d.config.algo.digestSize() + + for chunkStart := 0; chunkStart < len(data); chunkStart += d.config.blockSize { + chunkEnd := chunkStart + d.config.blockSize + if chunkEnd > len(data) { + chunkEnd = len(data) + } + overflow := data[chunkStart:chunkEnd] + + keepSpace := false + for levelIdx := range d.levels { + level := &d.levels[levelIdx] + overflow = level.overflowingAppend(overflow) + + if keepSpace { + if level.remaining >= digestSize { + if len(overflow) == 0 { + break + } + // overflow but not enough room — finalize this level + } + } else { + if len(overflow) == 0 { + break + } + } + + hash := level.finalizeAndReset() + if levelIdx == 0 { + d.merkleTree.level1 = append(d.merkleTree.level1, hash) + } + overflow = hash + keepSpace = true + } + + if len(overflow) > 0 { + level := newFixedSizeBlock(d.config.algo, d.config.salt, d.config.blockSize) + level.append(overflow) + d.levels = append(d.levels, level) + } + } +} + +// Finalize flushes all levels and returns the descriptor and merkle tree. +func (d *FsVerityDigest) Finalize() (FsVerityDescriptor, *MerkleTree) { + digestSize := d.config.algo.digestSize() + compressionFactor := d.config.blockSize / digestSize + + var totalSize int + scale := 1 + + // Flush all levels from bottom to top + var lastHash []byte + overflow := lastHash // initially empty (zeros) + + for levelIdx, level := range d.levels { + totalSize += scale * (d.config.blockSize - level.remaining) + level.append(overflow) + lastHash = level.fillToEnd() + if levelIdx == 0 { + d.merkleTree.level1 = append(d.merkleTree.level1, lastHash) + } + overflow = lastHash + scale *= compressionFactor + } + + if lastHash == nil { + // Empty file: root hash is all zeros + lastHash = make([]byte, digestSize) + } + + desc := FsVerityDescriptor{ + Version: 1, + HashAlgorithm: d.config.algo, + LogBlocksize: uint8(trailingZeros(uint(d.config.blockSize))), + DataSize: uint64(totalSize), + RootHash: lastHash, + Salt: append([]byte(nil), d.config.salt...), + } + + // Return merkleTree reference for caller + tree := d.merkleTree + d.merkleTree = &MerkleTree{algo: d.config.algo} + + return desc, tree +} + +// InnerHashAlgorithm returns the hash algorithm in use. +func (d *FsVerityDigest) InnerHashAlgorithm() HashAlgorithm { + return d.config.algo +} + +// trailingZeros counts trailing zeros in an unsigned integer. +func trailingZeros(n uint) int { + if n == 0 { + return 0 + } + count := 0 + for n&1 == 0 { + count++ + n >>= 1 + } + return count +} From 125b9c94c2e13fb9d2838c983189712d09d22c1f Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Sat, 9 May 2026 15:59:23 +0800 Subject: [PATCH 05/18] feat(verity-go): add cross-validation tests for FsVerityDigest --- verity-go/verity/digest.go | 2 + verity-go/verity/tree.go | 20 +++-- verity-go/verity/verity_test.go | 145 ++++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 verity-go/verity/verity_test.go diff --git a/verity-go/verity/digest.go b/verity-go/verity/digest.go index 023a02a5..578dcfb0 100644 --- a/verity-go/verity/digest.go +++ b/verity-go/verity/digest.go @@ -149,6 +149,8 @@ func (d *FsVerityDigest) update(data []byte) { if levelIdx == 0 { d.merkleTree.level1 = append(d.merkleTree.level1, hash) } + // Append the overflow to the freshly reset level (matches Rust behavior) + level.append(overflow) overflow = hash keepSpace = true } diff --git a/verity-go/verity/tree.go b/verity-go/verity/tree.go index ad8ea0dc..0b0409fe 100644 --- a/verity-go/verity/tree.go +++ b/verity-go/verity/tree.go @@ -24,21 +24,31 @@ func (t *MerkleTree) Level1AsBytes() []byte { // RebuildRootHash reconstructs the root hash from level-1 hashes. // Used when loading a tree from serialized metadata. +// When there are more level-1 hashes than fit in one block, this builds +// intermediate tree levels using the full FsVerityDigest pipeline. func (t *MerkleTree) RebuildRootHash(salt []byte, blockSize int) []byte { + if len(t.level1) == 0 { + // Empty file: root hash is all zeros + return make([]byte, t.algo.digestSize()) + } if len(t.level1) == 1 { return t.level1[0] } - h := saltToDigest(t.algo, salt) + // Use a full FsVerityDigest to process the level-1 hashes, + // matching the Rust implementation's rebuild_root_hash. + d := NewFsVerityWithSaltAndBlockSize(t.algo, salt, blockSize) for _, hash := range t.level1 { - h.Write(hash) + d.Write(hash) } // Pad to block boundary - padding := (blockSize - (len(t.level1)*t.algo.digestSize())%blockSize) % blockSize + totalBytes := len(t.level1) * t.algo.digestSize() + padding := (blockSize - totalBytes%blockSize) % blockSize if padding > 0 { - h.Write(make([]byte, padding)) + d.Write(make([]byte, padding)) } - return h.Sum(nil) + desc, _ := d.Finalize() + return desc.RootHash } // VerifyDataBlock verifies a single data block against the merkle tree. diff --git a/verity-go/verity/verity_test.go b/verity-go/verity/verity_test.go new file mode 100644 index 00000000..a54adf6a --- /dev/null +++ b/verity-go/verity/verity_test.go @@ -0,0 +1,145 @@ +// verity-go/verity/verity_test.go +package verity + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + "testing" +) + +func TestEmptyFile(t *testing.T) { + d := NewFsVerity(HashSHA256) + desc, tree := d.Finalize() + + // sha256:3d248ca542a24fc62d1c43b916eae5016878e2533c88238480b26128a1f1af95 + expected := "3d248ca542a24fc62d1c43b916eae5016878e2533c88238480b26128a1f1af95" + actual := hex.EncodeToString(desc.ToDescriptorHash()) + if actual != expected { + t.Errorf("empty: expected %s, got %s", expected, actual) + } + + // Root hash should be all zeros for empty file + expectedRoot := make([]byte, 32) + if !equalBytes(desc.RootHash, expectedRoot) { + t.Errorf("empty: root hash should be all zeros, got %s", hex.EncodeToString(desc.RootHash)) + } + + rebuiltRoot := tree.RebuildRootHash(desc.Salt, desc.BlockSize()) + if !equalBytes(rebuiltRoot, desc.RootHash) { + t.Errorf("empty: root hash mismatch: desc=%s rebuilt=%s", + hex.EncodeToString(desc.RootHash), hex.EncodeToString(rebuiltRoot)) + } +} + +func TestOneByte(t *testing.T) { + d := NewFsVerity(HashSHA256) + d.Write([]byte{'A'}) // matches Python: b'A' + desc, tree := d.Finalize() + + expected := "9845e616f7d2f7a1cd6742f0546a36d2e74d4eb8ae7d9bdc0b0df982c27861b7" + actual := hex.EncodeToString(desc.ToDescriptorHash()) + if actual != expected { + t.Errorf("onebyte: expected %s, got %s", expected, actual) + } + + // Verify root hash + rebuiltRoot := tree.RebuildRootHash(desc.Salt, desc.BlockSize()) + if !equalBytes(rebuiltRoot, desc.RootHash) { + t.Errorf("onebyte: root hash mismatch") + } + + // Verify data block + block := make([]byte, desc.BlockSize()) + block[0] = 'A' + if !tree.VerifyDataBlock(0, desc.BlockSize(), block[:1]) { + t.Errorf("onebyte: data block verification failed") + } +} + +func TestOneBlock(t *testing.T) { + // Exactly 4096 bytes of 'A' + data := make([]byte, DefaultBlockSize) + for i := range data { + data[i] = 'A' + } + d := NewFsVerity(HashSHA256) + d.Write(data) + desc, tree := d.Finalize() + + expected := "3fd7a78101899a79cd337b1b4e5414be8bcb376b133370156ef6e65026d930ed" + actual := hex.EncodeToString(desc.ToDescriptorHash()) + if actual != expected { + t.Errorf("oneblock: expected %s, got %s", expected, actual) + } + + rebuiltRoot := tree.RebuildRootHash(desc.Salt, desc.BlockSize()) + if !equalBytes(rebuiltRoot, desc.RootHash) { + t.Errorf("oneblock: root hash mismatch") + } +} + +func TestOneBlockPlusOneByte(t *testing.T) { + // 4096 bytes of 'A' + 1 byte 'B' + data := make([]byte, DefaultBlockSize+1) + for i := 0; i < DefaultBlockSize; i++ { + data[i] = 'A' + } + data[DefaultBlockSize] = 'B' + d := NewFsVerity(HashSHA256) + d.Write(data) + desc, tree := d.Finalize() + + expected := "c0b9455d545b6b1ee5e7b227bd1ed463aaa530a4840dcd93465163a2b3aff0da" + actual := hex.EncodeToString(desc.ToDescriptorHash()) + if actual != expected { + t.Errorf("oneblockplusonebyte: expected %s, got %s", expected, actual) + } + + rebuiltRoot := tree.RebuildRootHash(desc.Salt, desc.BlockSize()) + if !equalBytes(rebuiltRoot, desc.RootHash) { + t.Errorf("oneblockplusonebyte: root hash mismatch") + } +} + +// hashblock test cases: block_size * (hashes_per_block + i) + j bytes of 'A' +// hashes_per_block = 4096 / 32 = 128 +func TestHashblock(t *testing.T) { + hashesPerBlock := DefaultBlockSize / sha256.Size // 128 + tests := []struct { + i, j int + expected string + }{ + {0, 0, "f5c2b9ded1595acfe8a996795264d488dd6140531f6a01f8f8086a83fd835935"}, + {0, -1, "5c00a54bd1d8341d7bbad060ff1b8e88ed2646d7bb38db6e752cd1cff66c0a78"}, + {0, 1, "a7abb76568871169a79104d00679fae6521dfdb2a2648e380c02b10e96e217ff"}, + {-1, 0, "c4b519068d8c8c68fd5e362fc3526c5b11e15f8eb72d4678017906f9e7f2d137"}, + {1, 0, "09510d2dbb55fa16f2768165c42d19c4da43301dfaa05705b2ecb4aaa4a5686a"}, + {-1, -1, "7aa0bb537c623562f898386ac88acd319267e4ab3200f3fd1cf648cfdb4a0379"}, + {-1, 1, "f804e9777f91d3697ca015303c23251ad3d80205184cfa3d1066ab28cb906330"}, + {1, -1, "26159b4fc68c63881c25c33b23f2583ffaa64fee411af33c3b03238eea56755c"}, + {1, 1, "57bed0934bf3ab4610d54938f03cff27bd0d9d76c9a77e283f9fb2b7e29c5ab8"}, + } + for _, tc := range tests { + name := fmt.Sprintf("hashblock_%d_%d", tc.i, tc.j) + size := DefaultBlockSize*(hashesPerBlock+tc.i) + tc.j + if size < 0 { + size = 0 + } + data := bytes.Repeat([]byte{'A'}, size) + d := NewFsVerity(HashSHA256) + d.Write(data) + desc, tree := d.Finalize() + + actual := hex.EncodeToString(desc.ToDescriptorHash()) + if actual != tc.expected { + t.Errorf("%s: expected %s, got %s", name, tc.expected, actual) + } + + rebuiltRoot := tree.RebuildRootHash(desc.Salt, desc.BlockSize()) + if !equalBytes(rebuiltRoot, desc.RootHash) { + t.Errorf("%s: root hash mismatch", name) + } + } +} From 1580f6660f91158c581e7349de7ed18bc30d0f33 Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Sat, 9 May 2026 18:23:53 +0800 Subject: [PATCH 06/18] refactor(verity-go): export DigestSize and BlockSize methods These methods are needed by the metadata package to calculate descriptor hashes and rebuild root hashes. --- verity-go/verity/digest.go | 6 +++--- verity-go/verity/tree.go | 6 +++--- verity-go/verity/verity.go | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/verity-go/verity/digest.go b/verity-go/verity/digest.go index 578dcfb0..b7542ca9 100644 --- a/verity-go/verity/digest.go +++ b/verity-go/verity/digest.go @@ -25,7 +25,7 @@ func newFixedSizeBlock(algo HashAlgorithm, salt []byte, blockSize int) fixedSize // newSaltedHash creates a hash.Hash initialized with the salt. func newSaltedHash(algo HashAlgorithm, salt []byte) hash.Hash { h := algo.newHash() - blockSize := algo.blockSize() + blockSize := algo.BlockSize() for i := 0; i < len(salt); i += blockSize { end := i + blockSize if end > len(salt) { @@ -118,7 +118,7 @@ func (d *FsVerityDigest) Write(data []byte) (int, error) { // update processes data in block_size chunks, matching the Rust implementation. func (d *FsVerityDigest) update(data []byte) { - digestSize := d.config.algo.digestSize() + digestSize := d.config.algo.DigestSize() for chunkStart := 0; chunkStart < len(data); chunkStart += d.config.blockSize { chunkEnd := chunkStart + d.config.blockSize @@ -165,7 +165,7 @@ func (d *FsVerityDigest) update(data []byte) { // Finalize flushes all levels and returns the descriptor and merkle tree. func (d *FsVerityDigest) Finalize() (FsVerityDescriptor, *MerkleTree) { - digestSize := d.config.algo.digestSize() + digestSize := d.config.algo.DigestSize() compressionFactor := d.config.blockSize / digestSize var totalSize int diff --git a/verity-go/verity/tree.go b/verity-go/verity/tree.go index 0b0409fe..07671a61 100644 --- a/verity-go/verity/tree.go +++ b/verity-go/verity/tree.go @@ -14,7 +14,7 @@ func NewMerkleTree(hashes [][]byte, algo HashAlgorithm) *MerkleTree { // Level1AsBytes returns the level-1 hashes as concatenated bytes. func (t *MerkleTree) Level1AsBytes() []byte { - digestSize := t.algo.digestSize() + digestSize := t.algo.DigestSize() out := make([]byte, 0, len(t.level1)*digestSize) for _, h := range t.level1 { out = append(out, h...) @@ -29,7 +29,7 @@ func (t *MerkleTree) Level1AsBytes() []byte { func (t *MerkleTree) RebuildRootHash(salt []byte, blockSize int) []byte { if len(t.level1) == 0 { // Empty file: root hash is all zeros - return make([]byte, t.algo.digestSize()) + return make([]byte, t.algo.DigestSize()) } if len(t.level1) == 1 { return t.level1[0] @@ -42,7 +42,7 @@ func (t *MerkleTree) RebuildRootHash(salt []byte, blockSize int) []byte { d.Write(hash) } // Pad to block boundary - totalBytes := len(t.level1) * t.algo.digestSize() + totalBytes := len(t.level1) * t.algo.DigestSize() padding := (blockSize - totalBytes%blockSize) % blockSize if padding > 0 { d.Write(make([]byte, padding)) diff --git a/verity-go/verity/verity.go b/verity-go/verity/verity.go index 48a8b79b..a6a34b93 100644 --- a/verity-go/verity/verity.go +++ b/verity-go/verity/verity.go @@ -10,7 +10,7 @@ import ( const ( MaxDigestSize = 64 MaxSaltSize = 32 - MaxLevels = 8 // FS_VERITY_MAX_LEVELS + MaxLevels = 8 // FS_VERITY_MAX_LEVELS DefaultBlockSize = 4096 ) @@ -33,7 +33,7 @@ func (h HashAlgorithm) String() string { } } -func (h HashAlgorithm) digestSize() int { +func (h HashAlgorithm) DigestSize() int { switch h { case HashSHA256: return sha256.Size @@ -44,7 +44,7 @@ func (h HashAlgorithm) digestSize() int { } } -func (h HashAlgorithm) blockSize() int { +func (h HashAlgorithm) BlockSize() int { switch h { case HashSHA256: return sha256.BlockSize @@ -114,7 +114,7 @@ func (d *FsVerityDescriptor) DataSizeToBytes() []byte { // saltToDigest creates a hash.Hash initialized with the salt padded to block boundaries. func saltToDigest(algo HashAlgorithm, salt []byte) hash.Hash { h := algo.newHash() - blockSize := algo.blockSize() + blockSize := algo.BlockSize() for i := 0; i < len(salt); i += blockSize { end := i + blockSize if end > len(salt) { From dcbd1a21c81e97bafd3a6f360588b5a9e7b37ec5 Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Sat, 9 May 2026 18:38:08 +0800 Subject: [PATCH 07/18] feat(verity-go): add metadata package with FlatBuffers serialization Implements SerializeMetadata, DeserializeMetadata, VerifySelf, CalculateFsVerityHash, and CalculateMetadataHash. Includes: - Round-trip and tamper detection tests - Cross-language fixture tests for Rust interop - Interop tests using Rust CLI format/verify commands - Golden fixture tests with Rust-generated expected hashes - Shared .fbs schema files via symlips to Rust source --- verity-go/metadata/cross_test.go | 159 ++++++++++ .../metadata/generated/metadata_generated.go | 9 - .../generated/metadata_hash_generated.go | 5 - verity-go/metadata/interop_test.go | 153 ++++++++++ verity-go/metadata/metadata.fbs | 37 +-- verity-go/metadata/metadata.go | 278 ++++++++++++++++++ verity-go/metadata/metadata_hash.fbs | 20 +- verity-go/metadata/metadata_hash.go | 51 ++++ verity-go/metadata/metadata_test.go | 207 +++++++++++++ verity-go/verity/golden_fixtures.json | 44 +++ verity-go/verity/golden_test.go | 85 ++++++ 11 files changed, 979 insertions(+), 69 deletions(-) create mode 100644 verity-go/metadata/cross_test.go create mode 100644 verity-go/metadata/interop_test.go mode change 100644 => 120000 verity-go/metadata/metadata.fbs create mode 100644 verity-go/metadata/metadata.go mode change 100644 => 120000 verity-go/metadata/metadata_hash.fbs create mode 100644 verity-go/metadata/metadata_hash.go create mode 100644 verity-go/metadata/metadata_test.go create mode 100644 verity-go/verity/golden_fixtures.json create mode 100644 verity-go/verity/golden_test.go diff --git a/verity-go/metadata/cross_test.go b/verity-go/metadata/cross_test.go new file mode 100644 index 00000000..17a1d2d5 --- /dev/null +++ b/verity-go/metadata/cross_test.go @@ -0,0 +1,159 @@ +// verity-go/metadata/cross_test.go +// Cross-language round-trip tests. +// +// These tests embed binary data generated by the Rust implementation to verify +// that the Go code can correctly deserialize Rust-produced FlatBuffers, and vice versa. +// +// To update the Rust fixture: +// 1. Run: cargo test --package cryptpilot-verity -- test_cross_serialize --nocapture +// 2. The test prints base64-encoded serialized bytes to stdout +// 3. Paste the base64 string into rustFixtureBase64 below +// +// To update the Go fixture (for the Rust side): +// 1. Run: go test ./metadata/ -v -run TestCross_GoSerialize +// 2. The test prints base64-encoded serialized bytes to stdout +// 3. Paste into the Rust test's go_fixture_base64 constant +package metadata + +import ( + "encoding/base64" + "testing" +) + +// rustFixtureBase64 is a base64-encoded FlatBuffers Metadata binary produced by +// the Rust serialize_metadata() function. It contains: +// - 3 files: "a.txt" (data: "hello"), "b.txt" (data: "world"), "c.txt" (data: "") +// - labels: {"env": "prod", "version": "1.0"} +// +// Generated by: cryptpilot-verity Rust implementation. +// To regenerate, see instructions at top of this file. +const rustFixtureBase64 = "" // TODO: paste base64 from Rust when available + +// TestCross_DeserializeRustMetadata verifies that Go can deserialize metadata +// produced by the Rust implementation. +func TestCross_DeserializeRustMetadata(t *testing.T) { + if rustFixtureBase64 == "" { + t.Skip("rustFixtureBase64 not set — run Rust test to generate") + } + + data, err := base64.StdEncoding.DecodeString(rustFixtureBase64) + if err != nil { + t.Fatalf("decode base64: %v", err) + } + + info, err := DeserializeMetadata(data) + if err != nil { + t.Fatalf("deserialize: %v", err) + } + + // Verify expected content + if len(info.FileInfos) != 3 { + t.Errorf("expected 3 files, got %d", len(info.FileInfos)) + } + + // Verify sorted order + expectedPaths := []string{"a.txt", "b.txt", "c.txt"} + for i, fi := range info.FileInfos { + if fi.Path != expectedPaths[i] { + t.Errorf("file[%d].path: expected %q, got %q", i, expectedPaths[i], fi.Path) + } + } + + // Verify labels + if info.Labels["env"] != "prod" { + t.Errorf("labels[env]: expected prod, got %q", info.Labels["env"]) + } + if info.Labels["version"] != "1.0" { + t.Errorf("labels[version]: expected 1.0, got %q", info.Labels["version"]) + } + + // Verify each file's integrity + for i, fi := range info.FileInfos { + if err := fi.VerifySelf(); err != nil { + t.Errorf("file[%d] (%s) verification failed: %v", i, fi.Path, err) + } + } +} + +// TestCross_GoSerialize serializes known test data and prints base64 output. +// The printed base64 should be pasted into the Rust test's go_fixture_base64. +func TestCross_GoSerialize(t *testing.T) { + // Create test file info matching the Rust fixture + fileInfos := []FileVerityInfo{ + makeTestFileVerityInfo("a.txt", []byte("hello")), + makeTestFileVerityInfo("b.txt", []byte("world")), + makeTestFileVerityInfo("c.txt", []byte("")), + } + labels := map[string]string{ + "env": "prod", + "version": "1.0", + } + + serialized, err := SerializeMetadata(fileInfos, labels) + if err != nil { + t.Fatalf("serialize: %v", err) + } + + // Verify round-trip + info, err := DeserializeMetadata(serialized) + if err != nil { + t.Fatalf("deserialize own output: %v", err) + } + + if len(info.FileInfos) != 3 { + t.Errorf("expected 3 files, got %d", len(info.FileInfos)) + } + + // Verify descriptor hashes are consistent + for _, fi := range info.FileInfos { + if err := fi.VerifySelf(); err != nil { + t.Errorf("verify %s: %v", fi.Path, err) + } + } + + // Print base64 for Rust side to consume + t.Logf("Go serialized metadata (base64 for Rust test):\n%s", + base64.StdEncoding.EncodeToString(serialized)) +} + +// TestCross_MetadataHash verifies that the metadata hash calculation produces +// a deterministic result for the same input. +func TestCross_MetadataHash(t *testing.T) { + fileInfos := []FileVerityInfo{ + makeTestFileVerityInfo("a.txt", []byte("hello")), + makeTestFileVerityInfo("b.txt", []byte("world")), + } + labels := map[string]string{"key": "value"} + + serialized, err := SerializeMetadata(fileInfos, labels) + if err != nil { + t.Fatalf("serialize: %v", err) + } + + hash1, err := CalculateMetadataHash(serialized) + if err != nil { + t.Fatalf("hash 1: %v", err) + } + + // Deserialize and re-serialize + info, err := DeserializeMetadata(serialized) + if err != nil { + t.Fatalf("deserialize: %v", err) + } + + serialized2, err := SerializeMetadata(info.FileInfos, info.Labels) + if err != nil { + t.Fatalf("re-serialize: %v", err) + } + + hash2, err := CalculateMetadataHash(serialized2) + if err != nil { + t.Fatalf("hash 2: %v", err) + } + + if hash1 != hash2 { + t.Errorf("hash not deterministic: %s != %s", hash1, hash2) + } + + t.Logf("Metadata hash: %s", hash1) +} diff --git a/verity-go/metadata/generated/metadata_generated.go b/verity-go/metadata/generated/metadata_generated.go index 34e09ea1..e61e0bb7 100644 --- a/verity-go/metadata/generated/metadata_generated.go +++ b/verity-go/metadata/generated/metadata_generated.go @@ -8,8 +8,6 @@ import ( // Code generated by the FlatBuffers compiler. DO NOT EDIT. - - type FsVerityDescriptor struct { _tab flatbuffers.Table } @@ -194,8 +192,6 @@ func FsVerityDescriptorEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { // Code generated by the FlatBuffers compiler. DO NOT EDIT. - - type FileInfo struct { _tab flatbuffers.Table } @@ -318,8 +314,6 @@ func FileInfoEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { // Code generated by the FlatBuffers compiler. DO NOT EDIT. - - type KeyValue struct { _tab flatbuffers.Table } @@ -386,8 +380,6 @@ func KeyValueEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { // Code generated by the FlatBuffers compiler. DO NOT EDIT. - - type Metadata struct { _tab flatbuffers.Table } @@ -496,4 +488,3 @@ func MetadataStartLabelsVector(builder *flatbuffers.Builder, numElems int) flatb func MetadataEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { return builder.EndObject() } - diff --git a/verity-go/metadata/generated/metadata_hash_generated.go b/verity-go/metadata/generated/metadata_hash_generated.go index 72f2b182..fb4f8522 100644 --- a/verity-go/metadata/generated/metadata_hash_generated.go +++ b/verity-go/metadata/generated/metadata_hash_generated.go @@ -8,8 +8,6 @@ import ( // Code generated by the FlatBuffers compiler. DO NOT EDIT. - - type FileHashEntry struct { _tab flatbuffers.Table } @@ -76,8 +74,6 @@ func FileHashEntryEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { // Code generated by the FlatBuffers compiler. DO NOT EDIT. - - type MetadataHash struct { _tab flatbuffers.Table } @@ -145,4 +141,3 @@ func MetadataHashStartFilesVector(builder *flatbuffers.Builder, numElems int) fl func MetadataHashEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { return builder.EndObject() } - diff --git a/verity-go/metadata/interop_test.go b/verity-go/metadata/interop_test.go new file mode 100644 index 00000000..b5763767 --- /dev/null +++ b/verity-go/metadata/interop_test.go @@ -0,0 +1,153 @@ +// verity-go/metadata/interop_test.go +package metadata + +import ( + "encoding/hex" + "os" + "path/filepath" + "testing" + + "cryptpilot-verity-go/verity" +) + +func interopDir() string { + dir := os.Getenv("INTEROP_DIR") + if dir == "" { + dir = "/tmp/cryptpilot-interop" + } + return dir +} + +// TestInterop_RustProducesGoVerifies reads metadata produced by the Rust +// `format` command and verifies it using Go's DeserializeMetadata + VerifySelf. +// +// Note: The metadata root hash (SHA-256 of FlatBuffers bytes) is NOT compared +// between Go and Rust because the FlatBuffers binary encoding differs across +// language implementations (vtable placement, byte alignment). Instead, we +// verify semantic equivalence: same file count, paths, labels, descriptor +// hashes, and per-file fs-verity calculations. +// +// Expected test data layout (created by `make interop-rust-produces`): +// +// INTEROP_DIR/data/a.txt (content: "hello") +// INTEROP_DIR/data/b.txt (content: "world") +// INTEROP_DIR/data/empty.txt (content: "") +// INTEROP_DIR/data/cryptpilot-verity.metadata.fb (Rust format output) +// INTEROP_DIR/root_hash.txt (root hash from Rust format) +// +// Set INTEROP_DIR env var to the directory, or defaults to /tmp/cryptpilot-interop. +func TestInterop_RustProducesGoVerifies(t *testing.T) { + dir := interopDir() + dataDir := filepath.Join(dir, "data") + + metadataPath := filepath.Join(dataDir, "cryptpilot-verity.metadata.fb") + data, err := os.ReadFile(metadataPath) + if err != nil { + t.Fatalf("read metadata: %v", err) + } + + info, err := DeserializeMetadata(data) + if err != nil { + t.Fatalf("deserialize: %v", err) + } + + // Verify file count + if len(info.FileInfos) != 3 { + t.Fatalf("expected 3 files, got %d", len(info.FileInfos)) + } + + // Verify sorted paths + expectedPaths := []string{"a.txt", "b.txt", "empty.txt"} + for i, fi := range info.FileInfos { + if fi.Path != expectedPaths[i] { + t.Errorf("file[%d].path: expected %q, got %q", i, expectedPaths[i], fi.Path) + } + } + + // Verify labels + if info.Labels["env"] != "prod" { + t.Errorf("labels[env]: expected prod, got %q", info.Labels["env"]) + } + + // Verify each file's integrity (descriptor hash + root hash) + for i, fi := range info.FileInfos { + if err := fi.VerifySelf(); err != nil { + t.Errorf("file[%d] (%s) verification failed: %v", i, fi.Path, err) + } + } + + // Re-read each data file and do block-level verify: recalculate fs-verity + // and compare descriptor hash against what Rust stored. + for _, fi := range info.FileInfos { + filePath := filepath.Join(dataDir, fi.Path) + content, err := os.ReadFile(filePath) + if err != nil { + t.Fatalf("read file %s: %v", fi.Path, err) + } + + d := verity.NewFsVerity(verity.HashSHA256) + d.Write(content) + desc, tree := d.Finalize() + recalcHash := hex.EncodeToString(desc.ToDescriptorHash()) + if recalcHash != fi.DescriptorHash { + t.Errorf("block-level verify failed for %s: expected %s, got %s", + fi.Path, fi.DescriptorHash, recalcHash) + } + + rebuiltRoot := tree.RebuildRootHash(desc.Salt, desc.BlockSize()) + if !equalBytes(rebuiltRoot, desc.RootHash) { + t.Errorf("root hash mismatch for %s", fi.Path) + } + } +} + +// TestInterop_GoProducesRustVerifies creates test files and metadata, +// then writes them to INTEROP_DIR. The Makefile target `interop-go-produces` +// then runs `cargo run verify` to confirm Rust can consume Go's output. +func TestInterop_GoProducesRustVerifies(t *testing.T) { + dir := interopDir() + dataDir := filepath.Join(dir, "data") + + if err := os.MkdirAll(dataDir, 0755); err != nil { + t.Fatalf("mkdir: %v", err) + } + + testFiles := []struct { + path string + content []byte + }{ + {"hello.txt", []byte("hello")}, + {"world.txt", []byte("world")}, + {"empty.txt", []byte{}}, + } + for _, tf := range testFiles { + if err := os.WriteFile(filepath.Join(dataDir, tf.path), tf.content, 0644); err != nil { + t.Fatalf("write file %s: %v", tf.path, err) + } + } + + // Build FileVerityInfo for each file + fileInfos := make([]FileVerityInfo, len(testFiles)) + for i, tf := range testFiles { + desc, tree := CalculateFsVerityHash(tf.content) + fileInfos[i] = FileVerityInfo{ + Path: tf.path, + Descriptor: desc, + MerkleTree: tree, + DescriptorHash: hex.EncodeToString(desc.ToDescriptorHash()), + } + } + + labels := map[string]string{"env": "prod"} + metadataBytes, err := SerializeMetadata(fileInfos, labels) + if err != nil { + t.Fatalf("serialize: %v", err) + } + + metadataPath := filepath.Join(dataDir, "cryptpilot-verity.metadata.fb") + if err := os.WriteFile(metadataPath, metadataBytes, 0644); err != nil { + t.Fatalf("write metadata: %v", err) + } + + t.Logf("Go produced metadata (%d bytes) for Rust verify", len(metadataBytes)) +} diff --git a/verity-go/metadata/metadata.fbs b/verity-go/metadata/metadata.fbs deleted file mode 100644 index 746c8d0c..00000000 --- a/verity-go/metadata/metadata.fbs +++ /dev/null @@ -1,36 +0,0 @@ -// FlatBuffers schema for cryptpilot-verity metadata - -namespace cryptpilot.verity; - -// FsVerity descriptor information -table FsVerityDescriptor { - version: ubyte; // must be 1 - hash_algorithm: ubyte; // Merkle tree hash algorithm (1=SHA256, 2=SHA512) - log_blocksize: ubyte; // log2 of size of data and tree blocks - data_size: ulong; // size of file the Merkle tree is built over - root_hash: [ubyte]; // Merkle tree root hash (binary) - salt: [ubyte]; // salt prepended to each hashed block -} - -// Key-value pair for metadata labels -table KeyValue { - key: string; - value: string; -} - -// Single file information with fs-verity data -table FileInfo { - path: string; // relative path of the file - descriptor: FsVerityDescriptor; // fs-verity descriptor - merkle_tree_level1: [ubyte]; // Level 1 merkle tree hashes (concatenated binary) - descriptor_hash: string; // hex-encoded descriptor hash (final measurement) -} - -// Root metadata structures containing all file information -table Metadata { - version: uint = 1; // metadata format version for backward compatibility - files: [FileInfo]; - labels: [KeyValue]; -} - -root_type Metadata; diff --git a/verity-go/metadata/metadata.fbs b/verity-go/metadata/metadata.fbs new file mode 120000 index 00000000..265a2fdc --- /dev/null +++ b/verity-go/metadata/metadata.fbs @@ -0,0 +1 @@ +../../cryptpilot-verity/src/metadata/metadata.fbs \ No newline at end of file diff --git a/verity-go/metadata/metadata.go b/verity-go/metadata/metadata.go new file mode 100644 index 00000000..8e868ead --- /dev/null +++ b/verity-go/metadata/metadata.go @@ -0,0 +1,278 @@ +// verity-go/metadata/metadata.go +package metadata + +import ( + "encoding/hex" + "fmt" + "sort" + + "cryptpilot-verity-go/metadata/generated" + "cryptpilot-verity-go/verity" + + flatbuffers "github.com/google/flatbuffers/go" +) + +// FileVerityInfo holds fs-verity data for a single file. +type FileVerityInfo struct { + Path string + Descriptor verity.FsVerityDescriptor + MerkleTree *verity.MerkleTree + DescriptorHash string // hex-encoded +} + +// VerifySelf checks that the descriptor hash matches and the Merkle tree is consistent. +func (f *FileVerityInfo) VerifySelf() error { + calculated := hex.EncodeToString(f.Descriptor.ToDescriptorHash()) + if calculated != f.DescriptorHash { + return &VerificationError{ + Path: f.Path, + Field: "descriptor_hash", + Expected: f.DescriptorHash, + Got: calculated, + } + } + rebuiltRoot := f.MerkleTree.RebuildRootHash(f.Descriptor.Salt, f.Descriptor.BlockSize()) + if !equalBytes(rebuiltRoot, f.Descriptor.RootHash) { + return &VerificationError{ + Path: f.Path, + Field: "root_hash", + Expected: hex.EncodeToString(f.Descriptor.RootHash), + Got: hex.EncodeToString(rebuiltRoot), + } + } + return nil +} + +// MetadataInfo holds deserialized metadata with file info and labels. +type MetadataInfo struct { + FileInfos []FileVerityInfo + Labels map[string]string +} + +// VerificationError describes an integrity check failure. +type VerificationError struct { + Path string + Field string + Expected string + Got string +} + +func (e *VerificationError) Error() string { + return fmt.Sprintf("verification failed for %s: %s mismatch, expected %s, got %s", + e.Path, e.Field, e.Expected, e.Got) +} + +// MetadataVersionError is returned when the metadata format version is unsupported. +type MetadataVersionError struct { + Version uint32 +} + +func (e *MetadataVersionError) Error() string { + return fmt.Sprintf("unsupported metadata version: %d, expected version 1", e.Version) +} + +// ParseError wraps FlatBuffers parsing failures. +type ParseError struct { + Message string +} + +func (e *ParseError) Error() string { + return fmt.Sprintf("failed to parse metadata: %s", e.Message) +} + +// SerializeMetadata serializes file info and labels to FlatBuffers bytes. +// Files are sorted by path for deterministic output. +func SerializeMetadata(fileInfos []FileVerityInfo, labels map[string]string) ([]byte, error) { + builder := flatbuffers.NewBuilder(0) + + // Sort by path for stable output + sorted := make([]*FileVerityInfo, len(fileInfos)) + for i := range fileInfos { + sorted[i] = &fileInfos[i] + } + sort.Slice(sorted, func(i, j int) bool { + return sorted[i].Path < sorted[j].Path + }) + + fileOffsets := make([]flatbuffers.UOffsetT, len(sorted)) + for i, info := range sorted { + pathOff := builder.CreateString(info.Path) + hashOff := builder.CreateString(info.DescriptorHash) + + rootHashVec := builder.CreateByteVector(info.Descriptor.RootHash) + saltVec := builder.CreateByteVector(info.Descriptor.Salt) + + generated.FsVerityDescriptorStart(builder) + generated.FsVerityDescriptorAddVersion(builder, info.Descriptor.Version) + generated.FsVerityDescriptorAddHashAlgorithm(builder, uint8(info.Descriptor.HashAlgorithm)) + generated.FsVerityDescriptorAddLogBlocksize(builder, info.Descriptor.LogBlocksize) + generated.FsVerityDescriptorAddDataSize(builder, info.Descriptor.DataSize) + generated.FsVerityDescriptorAddRootHash(builder, rootHashVec) + generated.FsVerityDescriptorAddSalt(builder, saltVec) + descOff := generated.FsVerityDescriptorEnd(builder) + + merkleVec := builder.CreateByteVector(info.MerkleTree.Level1AsBytes()) + + generated.FileInfoStart(builder) + generated.FileInfoAddPath(builder, pathOff) + generated.FileInfoAddDescriptor(builder, descOff) + generated.FileInfoAddMerkleTreeLevel1(builder, merkleVec) + generated.FileInfoAddDescriptorHash(builder, hashOff) + fileOffsets[i] = generated.FileInfoEnd(builder) + } + + filesVec := builder.CreateVectorOfTables(fileOffsets) + + // Build labels vector (sorted by key) + labelKeys := make([]string, 0, len(labels)) + for k := range labels { + labelKeys = append(labelKeys, k) + } + sort.Strings(labelKeys) + + labelOffsets := make([]flatbuffers.UOffsetT, len(labelKeys)) + for i, k := range labelKeys { + keyOff := builder.CreateString(k) + valOff := builder.CreateString(labels[k]) + generated.KeyValueStart(builder) + generated.KeyValueAddKey(builder, keyOff) + generated.KeyValueAddValue(builder, valOff) + labelOffsets[i] = generated.KeyValueEnd(builder) + } + + var labelsVec flatbuffers.UOffsetT + if len(labelOffsets) > 0 { + labelsVec = builder.CreateVectorOfTables(labelOffsets) + } + + generated.MetadataStart(builder) + generated.MetadataAddVersion(builder, 1) + generated.MetadataAddFiles(builder, filesVec) + if len(labelOffsets) > 0 { + generated.MetadataAddLabels(builder, labelsVec) + } + metadataOff := generated.MetadataEnd(builder) + + builder.Finish(metadataOff) + return builder.FinishedBytes(), nil +} + +// DeserializeMetadata reads FlatBuffers metadata and returns structured data. +func DeserializeMetadata(data []byte) (result *MetadataInfo, err error) { + defer func() { + if r := recover(); r != nil { + result = nil + err = &ParseError{Message: fmt.Sprintf("invalid flatbuffers data: %v", r)} + } + }() + + md := generated.GetRootAsMetadata(data, 0) + + version := md.Version() + if version != 1 { + return nil, &MetadataVersionError{Version: version} + } + + var fileInfos []FileVerityInfo + if filesLen := md.FilesLength(); filesLen > 0 { + fileInfos = make([]FileVerityInfo, filesLen) + var fi generated.FileInfo + for i := 0; i < filesLen; i++ { + if !md.Files(&fi, i) { + return nil, &ParseError{Message: fmt.Sprintf("missing FileInfo at index %d", i)} + } + + path := string(fi.Path()) + descriptorHash := string(fi.DescriptorHash()) + + var fbDesc generated.FsVerityDescriptor + if fi.Descriptor(&fbDesc) == nil { + return nil, &ParseError{Message: "missing descriptor for " + path} + } + + rootHash := fbDesc.RootHashBytes() + if rootHash == nil { + return nil, &ParseError{Message: "missing root_hash in descriptor for " + path} + } + + salt := fbDesc.SaltBytes() + if salt == nil { + salt = []byte{} + } + + descriptor := verity.FsVerityDescriptor{ + Version: fbDesc.Version(), + HashAlgorithm: verity.HashAlgorithm(fbDesc.HashAlgorithm()), + LogBlocksize: fbDesc.LogBlocksize(), + DataSize: fbDesc.DataSize(), + RootHash: rootHash, + Salt: salt, + } + + merkleLevel1 := fi.MerkleTreeLevel1Bytes() + if merkleLevel1 == nil { + merkleLevel1 = []byte{} + } + + digestSize := descriptor.HashAlgorithm.DigestSize() + if len(merkleLevel1)%digestSize != 0 { + return nil, &ParseError{ + Message: fmt.Sprintf("broken merkle tree for %s: level 1 length %d not a multiple of hash size %d", + path, len(merkleLevel1), digestSize), + } + } + + nHashes := len(merkleLevel1) / digestSize + hashes := make([][]byte, nHashes) + for j := 0; j < nHashes; j++ { + h := make([]byte, digestSize) + copy(h, merkleLevel1[j*digestSize:(j+1)*digestSize]) + hashes[j] = h + } + + merkleTree := verity.NewMerkleTree(hashes, descriptor.HashAlgorithm) + + fileInfos[i] = FileVerityInfo{ + Path: path, + Descriptor: descriptor, + MerkleTree: merkleTree, + DescriptorHash: descriptorHash, + } + } + } + + labels := make(map[string]string) + if labelsLen := md.LabelsLength(); labelsLen > 0 { + var kv generated.KeyValue + for i := 0; i < labelsLen; i++ { + if !md.Labels(&kv, i) { + return nil, &ParseError{Message: fmt.Sprintf("missing KeyValue at index %d", i)} + } + labels[string(kv.Key())] = string(kv.Value()) + } + } + + return &MetadataInfo{ + FileInfos: fileInfos, + Labels: labels, + }, nil +} + +// CalculateFsVerityHash computes the fs-verity descriptor hash for raw file data. +func CalculateFsVerityHash(data []byte) (verity.FsVerityDescriptor, *verity.MerkleTree) { + d := verity.NewFsVerity(verity.HashSHA256) + d.Write(data) + return d.Finalize() +} + +func equalBytes(a, b []byte) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} diff --git a/verity-go/metadata/metadata_hash.fbs b/verity-go/metadata/metadata_hash.fbs deleted file mode 100644 index daf678c5..00000000 --- a/verity-go/metadata/metadata_hash.fbs +++ /dev/null @@ -1,19 +0,0 @@ -// FlatBuffers schema for metadata hash calculation -// This defines a minimal structure containing only the fields -// needed for hash calculation, avoiding redundancy. - -namespace cryptpilot.verity.hash; - -// File entry for hash calculation (only essential fields) -table FileHashEntry { - path: string; // relative path of the file - descriptor_hash: string; // hex-encoded descriptor hash -} - -// Metadata structure for hash calculation -// This contains only the minimal information needed to verify integrity -table MetadataHash { - files: [FileHashEntry]; // sorted by path for deterministic hash -} - -root_type MetadataHash; diff --git a/verity-go/metadata/metadata_hash.fbs b/verity-go/metadata/metadata_hash.fbs new file mode 120000 index 00000000..d21b7f61 --- /dev/null +++ b/verity-go/metadata/metadata_hash.fbs @@ -0,0 +1 @@ +../../cryptpilot-verity/src/metadata/metadata_hash.fbs \ No newline at end of file diff --git a/verity-go/metadata/metadata_hash.go b/verity-go/metadata/metadata_hash.go new file mode 100644 index 00000000..6fd56dd4 --- /dev/null +++ b/verity-go/metadata/metadata_hash.go @@ -0,0 +1,51 @@ +// verity-go/metadata/metadata_hash.go +package metadata + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + + "cryptpilot-verity-go/metadata/generated" + + flatbuffers "github.com/google/flatbuffers/go" +) + +// CalculateMetadataHash extracts path+descriptor_hash from metadata, serializes +// to the minimal MetadataHash format, and returns its SHA-256 digest (hex-encoded). +func CalculateMetadataHash(metadataBytes []byte) (string, error) { + md := generated.GetRootAsMetadata(metadataBytes, 0) + + builder := flatbuffers.NewBuilder(0) + + filesLen := md.FilesLength() + var filesVec flatbuffers.UOffsetT + + if filesLen > 0 { + fileOffsets := make([]flatbuffers.UOffsetT, filesLen) + var fi generated.FileInfo + for i := 0; i < filesLen; i++ { + if !md.Files(&fi, i) { + return "", &ParseError{Message: fmt.Sprintf("missing FileInfo at index %d", i)} + } + pathOff := builder.CreateString(string(fi.Path())) + hashOff := builder.CreateString(string(fi.DescriptorHash())) + generated.FileHashEntryStart(builder) + generated.FileHashEntryAddPath(builder, pathOff) + generated.FileHashEntryAddDescriptorHash(builder, hashOff) + fileOffsets[i] = generated.FileHashEntryEnd(builder) + } + filesVec = builder.CreateVectorOfTables(fileOffsets) + } + + generated.MetadataHashStart(builder) + if filesLen > 0 { + generated.MetadataHashAddFiles(builder, filesVec) + } + hashOff := generated.MetadataHashEnd(builder) + builder.Finish(hashOff) + + h := sha256.New() + h.Write(builder.FinishedBytes()) + return hex.EncodeToString(h.Sum(nil)), nil +} diff --git a/verity-go/metadata/metadata_test.go b/verity-go/metadata/metadata_test.go new file mode 100644 index 00000000..ce66900c --- /dev/null +++ b/verity-go/metadata/metadata_test.go @@ -0,0 +1,207 @@ +// verity-go/metadata/metadata_test.go +package metadata + +import ( + "bytes" + "encoding/hex" + "testing" + + "cryptpilot-verity-go/verity" +) + +func makeTestFileVerityInfo(path string, data []byte) FileVerityInfo { + d := verity.NewFsVerity(verity.HashSHA256) + d.Write(data) + desc, tree := d.Finalize() + return FileVerityInfo{ + Path: path, + Descriptor: desc, + MerkleTree: tree, + DescriptorHash: hex.EncodeToString(desc.ToDescriptorHash()), + } +} + +func TestSerializeDeserializeRoundTrip(t *testing.T) { + info := makeTestFileVerityInfo("test.txt", []byte("test file content")) + info.VerifySelf() // ensure test data is valid + + labels := map[string]string{"env": "prod"} + serialized, err := SerializeMetadata([]FileVerityInfo{info}, labels) + if err != nil { + t.Fatalf("SerializeMetadata: %v", err) + } + + deserialized, err := DeserializeMetadata(serialized) + if err != nil { + t.Fatalf("DeserializeMetadata: %v", err) + } + + if len(deserialized.FileInfos) != 1 { + t.Fatalf("expected 1 file, got %d", len(deserialized.FileInfos)) + } + + if deserialized.FileInfos[0].Path != "test.txt" { + t.Errorf("path: expected test.txt, got %s", deserialized.FileInfos[0].Path) + } + if deserialized.FileInfos[0].DescriptorHash != info.DescriptorHash { + t.Errorf("descriptor hash mismatch") + } + if deserialized.Labels["env"] != "prod" { + t.Errorf("labels: expected env=prod, got %v", deserialized.Labels) + } +} + +func TestSerializeDeserializeEmptyLabels(t *testing.T) { + info := makeTestFileVerityInfo("test.txt", []byte("test file content")) + serialized, err := SerializeMetadata([]FileVerityInfo{info}, map[string]string{}) + if err != nil { + t.Fatalf("SerializeMetadata: %v", err) + } + + deserialized, err := DeserializeMetadata(serialized) + if err != nil { + t.Fatalf("DeserializeMetadata: %v", err) + } + + if len(deserialized.Labels) != 0 { + t.Errorf("expected empty labels, got %v", deserialized.Labels) + } +} + +func TestSerializeDeserializeMultipleFiles(t *testing.T) { + infos := []FileVerityInfo{ + makeTestFileVerityInfo("b.txt", []byte("content b")), + makeTestFileVerityInfo("a.txt", []byte("content a")), + makeTestFileVerityInfo("c.txt", []byte("content c")), + } + labels := map[string]string{"key1": "val1", "key2": "val2"} + + serialized, err := SerializeMetadata(infos, labels) + if err != nil { + t.Fatalf("SerializeMetadata: %v", err) + } + + deserialized, err := DeserializeMetadata(serialized) + if err != nil { + t.Fatalf("DeserializeMetadata: %v", err) + } + + if len(deserialized.FileInfos) != 3 { + t.Fatalf("expected 3 files, got %d", len(deserialized.FileInfos)) + } + + // Verify sorted order by path + paths := []string{"a.txt", "b.txt", "c.txt"} + for i, fi := range deserialized.FileInfos { + if fi.Path != paths[i] { + t.Errorf("file[%d]: expected path %s, got %s", i, paths[i], fi.Path) + } + } +} + +func TestVerifySelf(t *testing.T) { + info := makeTestFileVerityInfo("test.txt", []byte("test file content")) + + err := info.VerifySelf() + if err != nil { + t.Fatalf("VerifySelf: %v", err) + } + + // Tamper with descriptor hash + tampered := info + tampered.DescriptorHash = "deadbeef" + err = tampered.VerifySelf() + if err == nil { + t.Fatal("VerifySelf should fail with tampered descriptor hash") + } + if _, ok := err.(*VerificationError); !ok { + t.Errorf("expected VerificationError, got %T", err) + } +} + +func TestVerifySelfRootHash(t *testing.T) { + info := makeTestFileVerityInfo("test.txt", []byte("test file content")) + + // Tamper with root hash in descriptor + tampered := info + tampered.Descriptor.RootHash = bytes.Repeat([]byte{0xFF}, 32) + err := tampered.VerifySelf() + if err == nil { + t.Fatal("VerifySelf should fail with tampered root hash") + } +} + +func TestMetadataVersionError(t *testing.T) { + // The serialized data has version 1. If we manually tamper with it... + // We can't easily tamper with FlatBuffers version field without low-level manipulation, + // so just test the error type string representation. + err := &MetadataVersionError{Version: 2} + expected := "unsupported metadata version: 2, expected version 1" + if err.Error() != expected { + t.Errorf("error string: expected %q, got %q", expected, err.Error()) + } +} + +func TestMetadataHashDeterminism(t *testing.T) { + infos := []FileVerityInfo{ + makeTestFileVerityInfo("b.txt", []byte("content b")), + makeTestFileVerityInfo("a.txt", []byte("content a")), + } + + serialized, _ := SerializeMetadata(infos, map[string]string{"label": "value"}) + + hash1, err := CalculateMetadataHash(serialized) + if err != nil { + t.Fatalf("CalculateMetadataHash: %v", err) + } + + // Deserialize and re-serialize to verify hash is deterministic + deserialized, _ := DeserializeMetadata(serialized) + serialized2, _ := SerializeMetadata(deserialized.FileInfos, deserialized.Labels) + + hash2, err := CalculateMetadataHash(serialized2) + if err != nil { + t.Fatalf("CalculateMetadataHash (2): %v", err) + } + + if hash1 != hash2 { + t.Errorf("metadata hash not deterministic: first=%s second=%s", hash1, hash2) + } +} + +func TestMetadataHashEmpty(t *testing.T) { + // Empty metadata: no files + serialized, err := SerializeMetadata([]FileVerityInfo{}, map[string]string{}) + if err != nil { + t.Fatalf("SerializeMetadata: %v", err) + } + + hash, err := CalculateMetadataHash(serialized) + if err != nil { + t.Fatalf("CalculateMetadataHash: %v", err) + } + if len(hash) != 64 { // SHA-256 hex = 64 chars + t.Errorf("hash length: expected 64, got %d", len(hash)) + } +} + +func TestDeserializeError(t *testing.T) { + _, err := DeserializeMetadata([]byte("not valid flatbuffers")) + if err == nil { + t.Fatal("expected error from invalid data") + } + if _, ok := err.(*ParseError); !ok { + t.Errorf("expected ParseError, got %T", err) + } +} + +func TestCalculateFsVerityHash(t *testing.T) { + desc, tree := CalculateFsVerityHash([]byte("test")) + + if desc.HashAlgorithm != verity.HashSHA256 { + t.Errorf("expected SHA256 algorithm") + } + if tree == nil { + t.Fatal("tree should not be nil") + } +} diff --git a/verity-go/verity/golden_fixtures.json b/verity-go/verity/golden_fixtures.json new file mode 100644 index 00000000..0e6293a5 --- /dev/null +++ b/verity-go/verity/golden_fixtures.json @@ -0,0 +1,44 @@ +[ + { + "name": "empty", + "data_b64": "", + "expected_descriptor_hash": "3d248ca542a24fc62d1c43b916eae5016878e2533c88238480b26128a1f1af95", + "generate": "empty" + }, + { + "name": "one_byte_A", + "data_b64": "QQ==", + "expected_descriptor_hash": "9845e616f7d2f7a1cd6742f0546a36d2e74d4eb8ae7d9bdc0b0df982c27861b7", + "generate": "b'A'" + }, + { + "name": "one_block", + "data_b64": "", + "expected_descriptor_hash": "3fd7a78101899a79cd337b1b4e5414be8bcb376b133370156ef6e65026d930ed", + "generate": "4096 bytes of 'A'" + }, + { + "name": "one_block_plus_one", + "data_b64": "", + "expected_descriptor_hash": "c0b9455d545b6b1ee5e7b227bd1ed463aaa530a4840dcd93465163a2b3aff0da", + "generate": "4097 bytes: 4096*'A' + 'B'" + }, + { + "name": "hashblock_0_0", + "data_b64": "", + "expected_descriptor_hash": "f5c2b9ded1595acfe8a996795264d488dd6140531f6a01f8f8086a83fd835935", + "generate": "4096*128 bytes of 'A'" + }, + { + "name": "hashblock_0_minus1", + "data_b64": "", + "expected_descriptor_hash": "5c00a54bd1d8341d7bbad060ff1b8e88ed2646d7bb38db6e752cd1cff66c0a78", + "generate": "4096*128-1 bytes of 'A'" + }, + { + "name": "hashblock_0_1", + "data_b64": "", + "expected_descriptor_hash": "a7abb76568871169a79104d00679fae6521dfdb2a2648e380c02b10e96e217ff", + "generate": "4096*128+1 bytes of 'A'" + } +] diff --git a/verity-go/verity/golden_test.go b/verity-go/verity/golden_test.go new file mode 100644 index 00000000..a08d1948 --- /dev/null +++ b/verity-go/verity/golden_test.go @@ -0,0 +1,85 @@ +// verity-go/verity/golden_test.go +package verity + +import ( + "bytes" + "encoding/base64" + "encoding/hex" + "encoding/json" + "os" + "path/filepath" + "testing" +) + +type goldenFixture struct { + Name string `json:"name"` + DataB64 string `json:"data_b64"` + ExpectedDescriptorHash string `json:"expected_descriptor_hash"` + Generate string `json:"generate"` +} + +func generateTestData(f goldenFixture) ([]byte, error) { + if f.DataB64 != "" { + return base64.StdEncoding.DecodeString(f.DataB64) + } + switch f.Generate { + case "empty": + return []byte{}, nil + case "b'A'": + return []byte{'A'}, nil + case "4096 bytes of 'A'": + return bytes.Repeat([]byte{'A'}, DefaultBlockSize), nil + case "4097 bytes: 4096*'A' + 'B'": + data := bytes.Repeat([]byte{'A'}, DefaultBlockSize+1) + data[DefaultBlockSize] = 'B' + return data, nil + case "4096*128 bytes of 'A'": + return bytes.Repeat([]byte{'A'}, DefaultBlockSize*128), nil + case "4096*128-1 bytes of 'A'": + return bytes.Repeat([]byte{'A'}, DefaultBlockSize*128-1), nil + case "4096*128+1 bytes of 'A'": + return bytes.Repeat([]byte{'A'}, DefaultBlockSize*128+1), nil + default: + return nil, nil + } +} + +func TestGoldenFixtures(t *testing.T) { + fixturePath := filepath.Join("golden_fixtures.json") + data, err := os.ReadFile(fixturePath) + if err != nil { + t.Fatalf("failed to read fixtures: %v", err) + } + + var fixtures []goldenFixture + if err := json.Unmarshal(data, &fixtures); err != nil { + t.Fatalf("failed to parse fixtures: %v", err) + } + + for _, f := range fixtures { + t.Run(f.Name, func(t *testing.T) { + testData, err := generateTestData(f) + if err != nil { + t.Fatalf("generate test data: %v", err) + } + if testData == nil { + t.Fatalf("unsupported generate field: %q", f.Generate) + } + + d := NewFsVerity(HashSHA256) + d.Write(testData) + desc, tree := d.Finalize() + + actualHash := hex.EncodeToString(desc.ToDescriptorHash()) + if actualHash != f.ExpectedDescriptorHash { + t.Errorf("descriptor hash mismatch\nexpected: %s\ngot: %s", + f.ExpectedDescriptorHash, actualHash) + } + + rebuiltRoot := tree.RebuildRootHash(desc.Salt, desc.BlockSize()) + if !equalBytes(rebuiltRoot, desc.RootHash) { + t.Errorf("root hash mismatch after rebuild") + } + }) + } +} From c464a0b55899f19f7c4e6c5c1b1ada04edd102f9 Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Sat, 9 May 2026 18:24:05 +0800 Subject: [PATCH 08/18] test(cryptpilot-verity): add cross-language deserialize tests Add test_cross_deserialize_go_metadata and test_cross_serialize_rust_metadata to verify Rust can consume Go's FlatBuffers output and vice versa. --- cryptpilot-verity/Cargo.toml | 1 + cryptpilot-verity/src/metadata/mod.rs | 64 +++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/cryptpilot-verity/Cargo.toml b/cryptpilot-verity/Cargo.toml index dfde413e..3bd91115 100644 --- a/cryptpilot-verity/Cargo.toml +++ b/cryptpilot-verity/Cargo.toml @@ -6,6 +6,7 @@ version.workspace = true [dependencies] anyhow = {workspace = true} +base64 = {workspace = true} async-trait = {workspace = true} async-walkdir = {workspace = true} clap = {workspace = true} diff --git a/cryptpilot-verity/src/metadata/mod.rs b/cryptpilot-verity/src/metadata/mod.rs index 33973ee5..2529667c 100644 --- a/cryptpilot-verity/src/metadata/mod.rs +++ b/cryptpilot-verity/src/metadata/mod.rs @@ -360,4 +360,68 @@ mod tests { assert_eq!(file_infos.len(), deserialized.file_infos.len()); assert!(deserialized.labels.is_empty()); } + + // Cross-language test: deserialize metadata produced by the Go implementation. + // To regenerate the Go fixture, run: + // go test ./metadata/ -v -run TestCross_GoSerialize + // in the verity-go directory and update the base64 string below. + #[test] + fn test_cross_deserialize_go_metadata() { + // Base64-encoded FlatBuffers metadata from Go's SerializeMetadata. + // Contains: a.txt ("hello"), b.txt ("world"), c.txt ("") + // Labels: {"env": "prod", "version": "1.0"} + let go_fixture_b64 = "EAAAAAAACgAMAAAACAAEAAoAAAAIAAAAWAAAAAIAAAAwAAAABAAAAOD///8IAAAADAAAAAMAAAAxLjAABwAAAHZlcnNpb24ACAAMAAgABAAIAAAACAAAABAAAAAEAAAAcHJvZAAAAAADAAAAZW52AAMAAACYAQAAvAAAAAQAAACA/v//XAAAAAwAAAAcAAAAmAAAAAAAAAAQABAADwAOAA0AAAAIAAQAEAAAAAwAAAAMAAAAAAwBAQAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAM2QyNDhjYTU0MmEyNGZjNjJkMWM0M2I5MTZlYWU1MDE2ODc4ZTI1MzNjODgyMzg0ODBiMjYxMjhhMWYxYWY5NQAAAAAFAAAAYy50eHQAAAA0////dAAAAAwAAAAsAAAAsAAAACAAAACw45uhEBrNC1SNnL+5WHuNe1KY+wlojtACMjl6opDSHCj///8UAAAAFAAAAAUAAAAAAAAAAAwBAQAAAAAgAAAAcOOboRAazQtUjZy/uVh7jXtSmPsJaI7QAjI5eqKQ0hxAAAAAMzEyOGVlYmEzZTQ2M2MyZmY1MjlkM2FhMWE1YzQ4NDQzMTEzYmQwMzdjMTc1OWY1NWFiODhiYTVjMmQ0NDVlZAAAAAAFAAAAYi50eHQAAAAMABQAEAAMAAgABAAMAAAAhAAAAAwAAAA8AAAAwAAAACAAAACxUFbJqNt3q1cI0Zt/Mw/hPYjurj0KsnEIHTQ4wPRiZBAAGAAXABYAFQAMAAgABAAQAAAAFAAAABQAAAAFAAAAAAAAAAAMAQEAAAAAIAAAALFQVsmo23erVwjRm38zD+E9iO6uPQqycQgdNDjA9GJkQAAAADU1NWI1ODljMjZlZTQzYjdhMjUxMGU2YzY3Y2VkOWZiMzE5MGI2ZGE2ZTllNjgzOTg0NTUxZjVkNzdhNzYzZGUAAAAABQAAAGEudHh0AAAA"; + + let data = base64::decode(go_fixture_b64).expect("decode base64"); + let info = deserialize_metadata(&data).expect("deserialize Go metadata"); + + assert_eq!(info.file_infos.len(), 3, "expected 3 files"); + assert_eq!(info.file_infos[0].path, "a.txt"); + assert_eq!(info.file_infos[1].path, "b.txt"); + assert_eq!(info.file_infos[2].path, "c.txt"); + assert_eq!(info.labels.get("env"), Some(&"prod".to_string())); + assert_eq!(info.labels.get("version"), Some(&"1.0".to_string())); + + for fi in &info.file_infos { + fi.verify_self().expect(&format!("verify {}", fi.path)); + } + } + + // Cross-language test: serialize metadata and print base64 for Go to consume. + // Run with --nocapture to see the base64 output. + // Paste the output into rustFixtureBase64 in verity-go/metadata/cross_test.go. + #[test] + fn test_cross_serialize_rust_metadata() { + let file_infos: Vec = vec![ + make_test_info("a.txt", b"hello"), + make_test_info("b.txt", b"world"), + make_test_info("c.txt", b""), + ]; + let mut labels = BTreeMap::new(); + labels.insert("env".to_string(), "prod".to_string()); + labels.insert("version".to_string(), "1.0".to_string()); + + let serialized = serialize_metadata(&file_infos, &labels).unwrap(); + let b64 = base64::encode(&serialized); + + println!("Rust serialized metadata (base64 for Go test):"); + println!("{}", b64); + + let info = deserialize_metadata(&serialized).unwrap(); + assert_eq!(info.file_infos.len(), 3); + for fi in &info.file_infos { + fi.verify_self().unwrap(); + } + } + + fn make_test_info(path: &str, data: &[u8]) -> FileVerityInfo { + let (descriptor, merkle_tree) = calculate_fsverity_hash(data); + let descriptor_hash = hex::encode(descriptor.to_descriptor_hash()); + FileVerityInfo { + path: path.to_string(), + descriptor, + merkle_tree, + descriptor_hash, + } + } } From 2065ff6880ef8f611ac94bcd54cc0ba530a65be5 Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Sat, 9 May 2026 18:24:29 +0800 Subject: [PATCH 09/18] chore: regenerate FlatBuffers Rust code from updated flatc --- .../src/metadata/metadata_generated.rs | 1523 +++++++---------- .../src/metadata/metadata_hash_generated.rs | 640 +++---- 2 files changed, 923 insertions(+), 1240 deletions(-) diff --git a/cryptpilot-verity/src/metadata/metadata_generated.rs b/cryptpilot-verity/src/metadata/metadata_generated.rs index d80afbdd..4bb4a3fd 100644 --- a/cryptpilot-verity/src/metadata/metadata_generated.rs +++ b/cryptpilot-verity/src/metadata/metadata_generated.rs @@ -2,879 +2,656 @@ // @generated extern crate alloc; + #[allow(unused_imports, dead_code)] pub mod cryptpilot { - #[allow(unused_imports, dead_code)] - pub mod verity { - - pub enum FsVerityDescriptorOffset {} - #[derive(Copy, Clone, PartialEq)] - - pub struct FsVerityDescriptor<'a> { - pub _tab: ::flatbuffers::Table<'a>, - } - - impl<'a> ::flatbuffers::Follow<'a> for FsVerityDescriptor<'a> { - type Inner = FsVerityDescriptor<'a>; - #[inline] - unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { - Self { - _tab: unsafe { ::flatbuffers::Table::new(buf, loc) }, - } - } - } - - impl<'a> FsVerityDescriptor<'a> { - pub const VT_VERSION: ::flatbuffers::VOffsetT = 4; - pub const VT_HASH_ALGORITHM: ::flatbuffers::VOffsetT = 6; - pub const VT_LOG_BLOCKSIZE: ::flatbuffers::VOffsetT = 8; - pub const VT_DATA_SIZE: ::flatbuffers::VOffsetT = 10; - pub const VT_ROOT_HASH: ::flatbuffers::VOffsetT = 12; - pub const VT_SALT: ::flatbuffers::VOffsetT = 14; - - #[inline] - pub unsafe fn init_from_table(table: ::flatbuffers::Table<'a>) -> Self { - FsVerityDescriptor { _tab: table } - } - #[allow(unused_mut)] - pub fn create< - 'bldr: 'args, - 'args: 'mut_bldr, - 'mut_bldr, - A: ::flatbuffers::Allocator + 'bldr, - >( - _fbb: &'mut_bldr mut ::flatbuffers::FlatBufferBuilder<'bldr, A>, - args: &'args FsVerityDescriptorArgs<'args>, - ) -> ::flatbuffers::WIPOffset> { - let mut builder = FsVerityDescriptorBuilder::new(_fbb); - builder.add_data_size(args.data_size); - if let Some(x) = args.salt { - builder.add_salt(x); - } - if let Some(x) = args.root_hash { - builder.add_root_hash(x); - } - builder.add_log_blocksize(args.log_blocksize); - builder.add_hash_algorithm(args.hash_algorithm); - builder.add_version(args.version); - builder.finish() - } - - #[inline] - pub fn version(&self) -> u8 { - // Safety: - // Created from valid Table for this object - // which contains a valid value in this slot - unsafe { - self._tab - .get::(FsVerityDescriptor::VT_VERSION, Some(0)) - .unwrap() - } - } - #[inline] - pub fn hash_algorithm(&self) -> u8 { - // Safety: - // Created from valid Table for this object - // which contains a valid value in this slot - unsafe { - self._tab - .get::(FsVerityDescriptor::VT_HASH_ALGORITHM, Some(0)) - .unwrap() - } - } - #[inline] - pub fn log_blocksize(&self) -> u8 { - // Safety: - // Created from valid Table for this object - // which contains a valid value in this slot - unsafe { - self._tab - .get::(FsVerityDescriptor::VT_LOG_BLOCKSIZE, Some(0)) - .unwrap() - } - } - #[inline] - pub fn data_size(&self) -> u64 { - // Safety: - // Created from valid Table for this object - // which contains a valid value in this slot - unsafe { - self._tab - .get::(FsVerityDescriptor::VT_DATA_SIZE, Some(0)) - .unwrap() - } - } - #[inline] - pub fn root_hash(&self) -> Option<::flatbuffers::Vector<'a, u8>> { - // Safety: - // Created from valid Table for this object - // which contains a valid value in this slot - unsafe { - self._tab - .get::<::flatbuffers::ForwardsUOffset<::flatbuffers::Vector<'a, u8>>>( - FsVerityDescriptor::VT_ROOT_HASH, - None, - ) - } - } - #[inline] - pub fn salt(&self) -> Option<::flatbuffers::Vector<'a, u8>> { - // Safety: - // Created from valid Table for this object - // which contains a valid value in this slot - unsafe { - self._tab - .get::<::flatbuffers::ForwardsUOffset<::flatbuffers::Vector<'a, u8>>>( - FsVerityDescriptor::VT_SALT, - None, - ) - } - } - } - - impl ::flatbuffers::Verifiable for FsVerityDescriptor<'_> { - #[inline] - fn run_verifier( - v: &mut ::flatbuffers::Verifier, - pos: usize, - ) -> Result<(), ::flatbuffers::InvalidFlatbuffer> { - v.visit_table(pos)? - .visit_field::("version", Self::VT_VERSION, false)? - .visit_field::("hash_algorithm", Self::VT_HASH_ALGORITHM, false)? - .visit_field::("log_blocksize", Self::VT_LOG_BLOCKSIZE, false)? - .visit_field::("data_size", Self::VT_DATA_SIZE, false)? - .visit_field::<::flatbuffers::ForwardsUOffset<::flatbuffers::Vector<'_, u8>>>( - "root_hash", - Self::VT_ROOT_HASH, - false, - )? - .visit_field::<::flatbuffers::ForwardsUOffset<::flatbuffers::Vector<'_, u8>>>( - "salt", - Self::VT_SALT, - false, - )? - .finish(); - Ok(()) - } - } - pub struct FsVerityDescriptorArgs<'a> { - pub version: u8, - pub hash_algorithm: u8, - pub log_blocksize: u8, - pub data_size: u64, - pub root_hash: Option<::flatbuffers::WIPOffset<::flatbuffers::Vector<'a, u8>>>, - pub salt: Option<::flatbuffers::WIPOffset<::flatbuffers::Vector<'a, u8>>>, - } - impl<'a> Default for FsVerityDescriptorArgs<'a> { - #[inline] - fn default() -> Self { - FsVerityDescriptorArgs { - version: 0, - hash_algorithm: 0, - log_blocksize: 0, - data_size: 0, - root_hash: None, - salt: None, - } - } - } - - pub struct FsVerityDescriptorBuilder<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> { - fbb_: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, - start_: ::flatbuffers::WIPOffset<::flatbuffers::TableUnfinishedWIPOffset>, - } - impl<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> FsVerityDescriptorBuilder<'a, 'b, A> { - #[inline] - pub fn add_version(&mut self, version: u8) { - self.fbb_ - .push_slot::(FsVerityDescriptor::VT_VERSION, version, 0); - } - #[inline] - pub fn add_hash_algorithm(&mut self, hash_algorithm: u8) { - self.fbb_ - .push_slot::(FsVerityDescriptor::VT_HASH_ALGORITHM, hash_algorithm, 0); - } - #[inline] - pub fn add_log_blocksize(&mut self, log_blocksize: u8) { - self.fbb_ - .push_slot::(FsVerityDescriptor::VT_LOG_BLOCKSIZE, log_blocksize, 0); - } - #[inline] - pub fn add_data_size(&mut self, data_size: u64) { - self.fbb_ - .push_slot::(FsVerityDescriptor::VT_DATA_SIZE, data_size, 0); - } - #[inline] - pub fn add_root_hash( - &mut self, - root_hash: ::flatbuffers::WIPOffset<::flatbuffers::Vector<'b, u8>>, - ) { - self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>( - FsVerityDescriptor::VT_ROOT_HASH, - root_hash, - ); - } - #[inline] - pub fn add_salt( - &mut self, - salt: ::flatbuffers::WIPOffset<::flatbuffers::Vector<'b, u8>>, - ) { - self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>( - FsVerityDescriptor::VT_SALT, - salt, - ); - } - #[inline] - pub fn new( - _fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, - ) -> FsVerityDescriptorBuilder<'a, 'b, A> { - let start = _fbb.start_table(); - FsVerityDescriptorBuilder { - fbb_: _fbb, - start_: start, - } - } - #[inline] - pub fn finish(self) -> ::flatbuffers::WIPOffset> { - let o = self.fbb_.end_table(self.start_); - ::flatbuffers::WIPOffset::new(o.value()) - } - } - - impl ::core::fmt::Debug for FsVerityDescriptor<'_> { - fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - let mut ds = f.debug_struct("FsVerityDescriptor"); - ds.field("version", &self.version()); - ds.field("hash_algorithm", &self.hash_algorithm()); - ds.field("log_blocksize", &self.log_blocksize()); - ds.field("data_size", &self.data_size()); - ds.field("root_hash", &self.root_hash()); - ds.field("salt", &self.salt()); - ds.finish() - } - } - pub enum KeyValueOffset {} - #[derive(Copy, Clone, PartialEq)] - - pub struct KeyValue<'a> { - pub _tab: ::flatbuffers::Table<'a>, - } - - impl<'a> ::flatbuffers::Follow<'a> for KeyValue<'a> { - type Inner = KeyValue<'a>; - #[inline] - unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { - Self { - _tab: unsafe { ::flatbuffers::Table::new(buf, loc) }, - } - } - } - - impl<'a> KeyValue<'a> { - pub const VT_KEY: ::flatbuffers::VOffsetT = 4; - pub const VT_VALUE: ::flatbuffers::VOffsetT = 6; - - #[inline] - pub unsafe fn init_from_table(table: ::flatbuffers::Table<'a>) -> Self { - KeyValue { _tab: table } - } - #[allow(unused_mut)] - pub fn create< - 'bldr: 'args, - 'args: 'mut_bldr, - 'mut_bldr, - A: ::flatbuffers::Allocator + 'bldr, - >( - _fbb: &'mut_bldr mut ::flatbuffers::FlatBufferBuilder<'bldr, A>, - args: &'args KeyValueArgs<'args>, - ) -> ::flatbuffers::WIPOffset> { - let mut builder = KeyValueBuilder::new(_fbb); - if let Some(x) = args.value { - builder.add_value(x); - } - if let Some(x) = args.key { - builder.add_key(x); - } - builder.finish() - } - - #[inline] - pub fn key(&self) -> Option<&'a str> { - // Safety: - // Created from valid Table for this object - // which contains a valid value in this slot - unsafe { - self._tab - .get::<::flatbuffers::ForwardsUOffset<&str>>(KeyValue::VT_KEY, None) - } - } - #[inline] - pub fn value(&self) -> Option<&'a str> { - // Safety: - // Created from valid Table for this object - // which contains a valid value in this slot - unsafe { - self._tab - .get::<::flatbuffers::ForwardsUOffset<&str>>(KeyValue::VT_VALUE, None) - } - } - } - - impl ::flatbuffers::Verifiable for KeyValue<'_> { - #[inline] - fn run_verifier( - v: &mut ::flatbuffers::Verifier, - pos: usize, - ) -> Result<(), ::flatbuffers::InvalidFlatbuffer> { - v.visit_table(pos)? - .visit_field::<::flatbuffers::ForwardsUOffset<&str>>( - "key", - Self::VT_KEY, - false, - )? - .visit_field::<::flatbuffers::ForwardsUOffset<&str>>( - "value", - Self::VT_VALUE, - false, - )? - .finish(); - Ok(()) - } - } - pub struct KeyValueArgs<'a> { - pub key: Option<::flatbuffers::WIPOffset<&'a str>>, - pub value: Option<::flatbuffers::WIPOffset<&'a str>>, - } - impl<'a> Default for KeyValueArgs<'a> { - #[inline] - fn default() -> Self { - KeyValueArgs { - key: None, - value: None, - } - } - } - - pub struct KeyValueBuilder<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> { - fbb_: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, - start_: ::flatbuffers::WIPOffset<::flatbuffers::TableUnfinishedWIPOffset>, - } - impl<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> KeyValueBuilder<'a, 'b, A> { - #[inline] - pub fn add_key(&mut self, key: ::flatbuffers::WIPOffset<&'b str>) { - self.fbb_ - .push_slot_always::<::flatbuffers::WIPOffset<_>>(KeyValue::VT_KEY, key); - } - #[inline] - pub fn add_value(&mut self, value: ::flatbuffers::WIPOffset<&'b str>) { - self.fbb_ - .push_slot_always::<::flatbuffers::WIPOffset<_>>(KeyValue::VT_VALUE, value); - } - #[inline] - pub fn new( - _fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, - ) -> KeyValueBuilder<'a, 'b, A> { - let start = _fbb.start_table(); - KeyValueBuilder { - fbb_: _fbb, - start_: start, - } - } - #[inline] - pub fn finish(self) -> ::flatbuffers::WIPOffset> { - let o = self.fbb_.end_table(self.start_); - ::flatbuffers::WIPOffset::new(o.value()) - } - } - - impl ::core::fmt::Debug for KeyValue<'_> { - fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - let mut ds = f.debug_struct("KeyValue"); - ds.field("key", &self.key()); - ds.field("value", &self.value()); - ds.finish() - } - } - pub enum FileInfoOffset {} - #[derive(Copy, Clone, PartialEq)] - - pub struct FileInfo<'a> { - pub _tab: ::flatbuffers::Table<'a>, - } - - impl<'a> ::flatbuffers::Follow<'a> for FileInfo<'a> { - type Inner = FileInfo<'a>; - #[inline] - unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { - Self { - _tab: unsafe { ::flatbuffers::Table::new(buf, loc) }, - } - } - } - - impl<'a> FileInfo<'a> { - pub const VT_PATH: ::flatbuffers::VOffsetT = 4; - pub const VT_DESCRIPTOR: ::flatbuffers::VOffsetT = 6; - pub const VT_MERKLE_TREE_LEVEL1: ::flatbuffers::VOffsetT = 8; - pub const VT_DESCRIPTOR_HASH: ::flatbuffers::VOffsetT = 10; - - #[inline] - pub unsafe fn init_from_table(table: ::flatbuffers::Table<'a>) -> Self { - FileInfo { _tab: table } - } - #[allow(unused_mut)] - pub fn create< - 'bldr: 'args, - 'args: 'mut_bldr, - 'mut_bldr, - A: ::flatbuffers::Allocator + 'bldr, - >( - _fbb: &'mut_bldr mut ::flatbuffers::FlatBufferBuilder<'bldr, A>, - args: &'args FileInfoArgs<'args>, - ) -> ::flatbuffers::WIPOffset> { - let mut builder = FileInfoBuilder::new(_fbb); - if let Some(x) = args.descriptor_hash { - builder.add_descriptor_hash(x); - } - if let Some(x) = args.merkle_tree_level1 { - builder.add_merkle_tree_level1(x); - } - if let Some(x) = args.descriptor { - builder.add_descriptor(x); - } - if let Some(x) = args.path { - builder.add_path(x); - } - builder.finish() - } - - #[inline] - pub fn path(&self) -> Option<&'a str> { - // Safety: - // Created from valid Table for this object - // which contains a valid value in this slot - unsafe { - self._tab - .get::<::flatbuffers::ForwardsUOffset<&str>>(FileInfo::VT_PATH, None) - } - } - #[inline] - pub fn descriptor(&self) -> Option> { - // Safety: - // Created from valid Table for this object - // which contains a valid value in this slot - unsafe { - self._tab - .get::<::flatbuffers::ForwardsUOffset>( - FileInfo::VT_DESCRIPTOR, - None, - ) - } - } - #[inline] - pub fn merkle_tree_level1(&self) -> Option<::flatbuffers::Vector<'a, u8>> { - // Safety: - // Created from valid Table for this object - // which contains a valid value in this slot - unsafe { - self._tab - .get::<::flatbuffers::ForwardsUOffset<::flatbuffers::Vector<'a, u8>>>( - FileInfo::VT_MERKLE_TREE_LEVEL1, - None, - ) - } - } - #[inline] - pub fn descriptor_hash(&self) -> Option<&'a str> { - // Safety: - // Created from valid Table for this object - // which contains a valid value in this slot - unsafe { - self._tab.get::<::flatbuffers::ForwardsUOffset<&str>>( - FileInfo::VT_DESCRIPTOR_HASH, - None, - ) - } - } - } - - impl ::flatbuffers::Verifiable for FileInfo<'_> { - #[inline] - fn run_verifier( - v: &mut ::flatbuffers::Verifier, - pos: usize, - ) -> Result<(), ::flatbuffers::InvalidFlatbuffer> { - v.visit_table(pos)? - .visit_field::<::flatbuffers::ForwardsUOffset<&str>>( - "path", - Self::VT_PATH, - false, - )? - .visit_field::<::flatbuffers::ForwardsUOffset>( - "descriptor", - Self::VT_DESCRIPTOR, - false, - )? - .visit_field::<::flatbuffers::ForwardsUOffset<::flatbuffers::Vector<'_, u8>>>( - "merkle_tree_level1", - Self::VT_MERKLE_TREE_LEVEL1, - false, - )? - .visit_field::<::flatbuffers::ForwardsUOffset<&str>>( - "descriptor_hash", - Self::VT_DESCRIPTOR_HASH, - false, - )? - .finish(); - Ok(()) - } - } - pub struct FileInfoArgs<'a> { - pub path: Option<::flatbuffers::WIPOffset<&'a str>>, - pub descriptor: Option<::flatbuffers::WIPOffset>>, - pub merkle_tree_level1: Option<::flatbuffers::WIPOffset<::flatbuffers::Vector<'a, u8>>>, - pub descriptor_hash: Option<::flatbuffers::WIPOffset<&'a str>>, - } - impl<'a> Default for FileInfoArgs<'a> { - #[inline] - fn default() -> Self { - FileInfoArgs { - path: None, - descriptor: None, - merkle_tree_level1: None, - descriptor_hash: None, - } - } - } - - pub struct FileInfoBuilder<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> { - fbb_: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, - start_: ::flatbuffers::WIPOffset<::flatbuffers::TableUnfinishedWIPOffset>, - } - impl<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> FileInfoBuilder<'a, 'b, A> { - #[inline] - pub fn add_path(&mut self, path: ::flatbuffers::WIPOffset<&'b str>) { - self.fbb_ - .push_slot_always::<::flatbuffers::WIPOffset<_>>(FileInfo::VT_PATH, path); - } - #[inline] - pub fn add_descriptor( - &mut self, - descriptor: ::flatbuffers::WIPOffset>, - ) { - self.fbb_ - .push_slot_always::<::flatbuffers::WIPOffset>( - FileInfo::VT_DESCRIPTOR, - descriptor, - ); - } - #[inline] - pub fn add_merkle_tree_level1( - &mut self, - merkle_tree_level1: ::flatbuffers::WIPOffset<::flatbuffers::Vector<'b, u8>>, - ) { - self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>( - FileInfo::VT_MERKLE_TREE_LEVEL1, - merkle_tree_level1, - ); - } - #[inline] - pub fn add_descriptor_hash( - &mut self, - descriptor_hash: ::flatbuffers::WIPOffset<&'b str>, - ) { - self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>( - FileInfo::VT_DESCRIPTOR_HASH, - descriptor_hash, - ); - } - #[inline] - pub fn new( - _fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, - ) -> FileInfoBuilder<'a, 'b, A> { - let start = _fbb.start_table(); - FileInfoBuilder { - fbb_: _fbb, - start_: start, - } - } - #[inline] - pub fn finish(self) -> ::flatbuffers::WIPOffset> { - let o = self.fbb_.end_table(self.start_); - ::flatbuffers::WIPOffset::new(o.value()) - } - } - - impl ::core::fmt::Debug for FileInfo<'_> { - fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - let mut ds = f.debug_struct("FileInfo"); - ds.field("path", &self.path()); - ds.field("descriptor", &self.descriptor()); - ds.field("merkle_tree_level1", &self.merkle_tree_level1()); - ds.field("descriptor_hash", &self.descriptor_hash()); - ds.finish() - } - } - pub enum MetadataOffset {} - #[derive(Copy, Clone, PartialEq)] - - pub struct Metadata<'a> { - pub _tab: ::flatbuffers::Table<'a>, - } - - impl<'a> ::flatbuffers::Follow<'a> for Metadata<'a> { - type Inner = Metadata<'a>; - #[inline] - unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { - Self { - _tab: unsafe { ::flatbuffers::Table::new(buf, loc) }, - } - } - } - - impl<'a> Metadata<'a> { - pub const VT_VERSION: ::flatbuffers::VOffsetT = 4; - pub const VT_FILES: ::flatbuffers::VOffsetT = 6; - pub const VT_LABELS: ::flatbuffers::VOffsetT = 8; - - #[inline] - pub unsafe fn init_from_table(table: ::flatbuffers::Table<'a>) -> Self { - Metadata { _tab: table } - } - #[allow(unused_mut)] - pub fn create< - 'bldr: 'args, - 'args: 'mut_bldr, - 'mut_bldr, - A: ::flatbuffers::Allocator + 'bldr, - >( - _fbb: &'mut_bldr mut ::flatbuffers::FlatBufferBuilder<'bldr, A>, - args: &'args MetadataArgs<'args>, - ) -> ::flatbuffers::WIPOffset> { - let mut builder = MetadataBuilder::new(_fbb); - if let Some(x) = args.labels { - builder.add_labels(x); - } - if let Some(x) = args.files { - builder.add_files(x); - } - builder.add_version(args.version); - builder.finish() - } - - #[inline] - pub fn version(&self) -> u32 { - // Safety: - // Created from valid Table for this object - // which contains a valid value in this slot - unsafe { self._tab.get::(Metadata::VT_VERSION, Some(1)).unwrap() } - } - #[inline] - pub fn files( - &self, - ) -> Option<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>> - { - // Safety: - // Created from valid Table for this object - // which contains a valid value in this slot - unsafe { - self._tab.get::<::flatbuffers::ForwardsUOffset< - ::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>, - >>(Metadata::VT_FILES, None) - } - } - #[inline] - pub fn labels( - &self, - ) -> Option<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>> - { - // Safety: - // Created from valid Table for this object - // which contains a valid value in this slot - unsafe { - self._tab.get::<::flatbuffers::ForwardsUOffset< - ::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>, - >>(Metadata::VT_LABELS, None) - } - } - } - - impl ::flatbuffers::Verifiable for Metadata<'_> { - #[inline] - fn run_verifier( - v: &mut ::flatbuffers::Verifier, - pos: usize, - ) -> Result<(), ::flatbuffers::InvalidFlatbuffer> { - v.visit_table(pos)? - .visit_field::("version", Self::VT_VERSION, false)? - .visit_field::<::flatbuffers::ForwardsUOffset< - ::flatbuffers::Vector<'_, ::flatbuffers::ForwardsUOffset>, - >>("files", Self::VT_FILES, false)? - .visit_field::<::flatbuffers::ForwardsUOffset< - ::flatbuffers::Vector<'_, ::flatbuffers::ForwardsUOffset>, - >>("labels", Self::VT_LABELS, false)? - .finish(); - Ok(()) - } - } - pub struct MetadataArgs<'a> { - pub version: u32, - pub files: Option< - ::flatbuffers::WIPOffset< - ::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>, - >, - >, - pub labels: Option< - ::flatbuffers::WIPOffset< - ::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>, - >, - >, - } - impl<'a> Default for MetadataArgs<'a> { - #[inline] - fn default() -> Self { - MetadataArgs { - version: 1, - files: None, - labels: None, - } - } - } - - pub struct MetadataBuilder<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> { - fbb_: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, - start_: ::flatbuffers::WIPOffset<::flatbuffers::TableUnfinishedWIPOffset>, - } - impl<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> MetadataBuilder<'a, 'b, A> { - #[inline] - pub fn add_version(&mut self, version: u32) { - self.fbb_.push_slot::(Metadata::VT_VERSION, version, 1); - } - #[inline] - pub fn add_files( - &mut self, - files: ::flatbuffers::WIPOffset< - ::flatbuffers::Vector<'b, ::flatbuffers::ForwardsUOffset>>, - >, - ) { - self.fbb_ - .push_slot_always::<::flatbuffers::WIPOffset<_>>(Metadata::VT_FILES, files); - } - #[inline] - pub fn add_labels( - &mut self, - labels: ::flatbuffers::WIPOffset< - ::flatbuffers::Vector<'b, ::flatbuffers::ForwardsUOffset>>, - >, - ) { - self.fbb_ - .push_slot_always::<::flatbuffers::WIPOffset<_>>(Metadata::VT_LABELS, labels); - } - #[inline] - pub fn new( - _fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, - ) -> MetadataBuilder<'a, 'b, A> { - let start = _fbb.start_table(); - MetadataBuilder { - fbb_: _fbb, - start_: start, - } - } - #[inline] - pub fn finish(self) -> ::flatbuffers::WIPOffset> { - let o = self.fbb_.end_table(self.start_); - ::flatbuffers::WIPOffset::new(o.value()) - } - } - - impl ::core::fmt::Debug for Metadata<'_> { - fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - let mut ds = f.debug_struct("Metadata"); - ds.field("version", &self.version()); - ds.field("files", &self.files()); - ds.field("labels", &self.labels()); - ds.finish() - } - } - #[inline] - /// Verifies that a buffer of bytes contains a `Metadata` - /// and returns it. - /// Note that verification is still experimental and may not - /// catch every error, or be maximally performant. For the - /// previous, unchecked, behavior use - /// `root_as_metadata_unchecked`. - pub fn root_as_metadata( - buf: &[u8], - ) -> Result, ::flatbuffers::InvalidFlatbuffer> { - ::flatbuffers::root::(buf) - } - #[inline] - /// Verifies that a buffer of bytes contains a size prefixed - /// `Metadata` and returns it. - /// Note that verification is still experimental and may not - /// catch every error, or be maximally performant. For the - /// previous, unchecked, behavior use - /// `size_prefixed_root_as_metadata_unchecked`. - pub fn size_prefixed_root_as_metadata( - buf: &[u8], - ) -> Result, ::flatbuffers::InvalidFlatbuffer> { - ::flatbuffers::size_prefixed_root::(buf) - } - #[inline] - /// Verifies, with the given options, that a buffer of bytes - /// contains a `Metadata` and returns it. - /// Note that verification is still experimental and may not - /// catch every error, or be maximally performant. For the - /// previous, unchecked, behavior use - /// `root_as_metadata_unchecked`. - pub fn root_as_metadata_with_opts<'b, 'o>( - opts: &'o ::flatbuffers::VerifierOptions, - buf: &'b [u8], - ) -> Result, ::flatbuffers::InvalidFlatbuffer> { - ::flatbuffers::root_with_opts::>(opts, buf) - } - #[inline] - /// Verifies, with the given verifier options, that a buffer of - /// bytes contains a size prefixed `Metadata` and returns - /// it. Note that verification is still experimental and may not - /// catch every error, or be maximally performant. For the - /// previous, unchecked, behavior use - /// `root_as_metadata_unchecked`. - pub fn size_prefixed_root_as_metadata_with_opts<'b, 'o>( - opts: &'o ::flatbuffers::VerifierOptions, - buf: &'b [u8], - ) -> Result, ::flatbuffers::InvalidFlatbuffer> { - ::flatbuffers::size_prefixed_root_with_opts::>(opts, buf) - } - #[inline] - /// Assumes, without verification, that a buffer of bytes contains a Metadata and returns it. - /// # Safety - /// Callers must trust the given bytes do indeed contain a valid `Metadata`. - pub unsafe fn root_as_metadata_unchecked(buf: &[u8]) -> Metadata<'_> { - unsafe { ::flatbuffers::root_unchecked::(buf) } - } - #[inline] - /// Assumes, without verification, that a buffer of bytes contains a size prefixed Metadata and returns it. - /// # Safety - /// Callers must trust the given bytes do indeed contain a valid size prefixed `Metadata`. - pub unsafe fn size_prefixed_root_as_metadata_unchecked(buf: &[u8]) -> Metadata<'_> { - unsafe { ::flatbuffers::size_prefixed_root_unchecked::(buf) } - } - #[inline] - pub fn finish_metadata_buffer<'a, 'b, A: ::flatbuffers::Allocator + 'a>( - fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, - root: ::flatbuffers::WIPOffset>, - ) { - fbb.finish(root, None); - } - - #[inline] - pub fn finish_size_prefixed_metadata_buffer<'a, 'b, A: ::flatbuffers::Allocator + 'a>( - fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, - root: ::flatbuffers::WIPOffset>, - ) { - fbb.finish_size_prefixed(root, None); - } - } // pub mod verity -} // pub mod cryptpilot +#[allow(unused_imports, dead_code)] +pub mod verity { + + +pub enum FsVerityDescriptorOffset {} +#[derive(Copy, Clone, PartialEq)] + +pub struct FsVerityDescriptor<'a> { + pub _tab: ::flatbuffers::Table<'a>, +} + +impl<'a> ::flatbuffers::Follow<'a> for FsVerityDescriptor<'a> { + type Inner = FsVerityDescriptor<'a>; + #[inline] + unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + Self { _tab: unsafe { ::flatbuffers::Table::new(buf, loc) } } + } +} + +impl<'a> FsVerityDescriptor<'a> { + pub const VT_VERSION: ::flatbuffers::VOffsetT = 4; + pub const VT_HASH_ALGORITHM: ::flatbuffers::VOffsetT = 6; + pub const VT_LOG_BLOCKSIZE: ::flatbuffers::VOffsetT = 8; + pub const VT_DATA_SIZE: ::flatbuffers::VOffsetT = 10; + pub const VT_ROOT_HASH: ::flatbuffers::VOffsetT = 12; + pub const VT_SALT: ::flatbuffers::VOffsetT = 14; + + #[inline] + pub unsafe fn init_from_table(table: ::flatbuffers::Table<'a>) -> Self { + FsVerityDescriptor { _tab: table } + } + #[allow(unused_mut)] + pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr, A: ::flatbuffers::Allocator + 'bldr>( + _fbb: &'mut_bldr mut ::flatbuffers::FlatBufferBuilder<'bldr, A>, + args: &'args FsVerityDescriptorArgs<'args> + ) -> ::flatbuffers::WIPOffset> { + let mut builder = FsVerityDescriptorBuilder::new(_fbb); + builder.add_data_size(args.data_size); + if let Some(x) = args.salt { builder.add_salt(x); } + if let Some(x) = args.root_hash { builder.add_root_hash(x); } + builder.add_log_blocksize(args.log_blocksize); + builder.add_hash_algorithm(args.hash_algorithm); + builder.add_version(args.version); + builder.finish() + } + + + #[inline] + pub fn version(&self) -> u8 { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::(FsVerityDescriptor::VT_VERSION, Some(0)).unwrap()} + } + #[inline] + pub fn hash_algorithm(&self) -> u8 { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::(FsVerityDescriptor::VT_HASH_ALGORITHM, Some(0)).unwrap()} + } + #[inline] + pub fn log_blocksize(&self) -> u8 { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::(FsVerityDescriptor::VT_LOG_BLOCKSIZE, Some(0)).unwrap()} + } + #[inline] + pub fn data_size(&self) -> u64 { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::(FsVerityDescriptor::VT_DATA_SIZE, Some(0)).unwrap()} + } + #[inline] + pub fn root_hash(&self) -> Option<::flatbuffers::Vector<'a, u8>> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::<::flatbuffers::ForwardsUOffset<::flatbuffers::Vector<'a, u8>>>(FsVerityDescriptor::VT_ROOT_HASH, None)} + } + #[inline] + pub fn salt(&self) -> Option<::flatbuffers::Vector<'a, u8>> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::<::flatbuffers::ForwardsUOffset<::flatbuffers::Vector<'a, u8>>>(FsVerityDescriptor::VT_SALT, None)} + } +} + +impl ::flatbuffers::Verifiable for FsVerityDescriptor<'_> { + #[inline] + fn run_verifier( + v: &mut ::flatbuffers::Verifier, pos: usize + ) -> Result<(), ::flatbuffers::InvalidFlatbuffer> { + v.visit_table(pos)? + .visit_field::("version", Self::VT_VERSION, false)? + .visit_field::("hash_algorithm", Self::VT_HASH_ALGORITHM, false)? + .visit_field::("log_blocksize", Self::VT_LOG_BLOCKSIZE, false)? + .visit_field::("data_size", Self::VT_DATA_SIZE, false)? + .visit_field::<::flatbuffers::ForwardsUOffset<::flatbuffers::Vector<'_, u8>>>("root_hash", Self::VT_ROOT_HASH, false)? + .visit_field::<::flatbuffers::ForwardsUOffset<::flatbuffers::Vector<'_, u8>>>("salt", Self::VT_SALT, false)? + .finish(); + Ok(()) + } +} +pub struct FsVerityDescriptorArgs<'a> { + pub version: u8, + pub hash_algorithm: u8, + pub log_blocksize: u8, + pub data_size: u64, + pub root_hash: Option<::flatbuffers::WIPOffset<::flatbuffers::Vector<'a, u8>>>, + pub salt: Option<::flatbuffers::WIPOffset<::flatbuffers::Vector<'a, u8>>>, +} +impl<'a> Default for FsVerityDescriptorArgs<'a> { + #[inline] + fn default() -> Self { + FsVerityDescriptorArgs { + version: 0, + hash_algorithm: 0, + log_blocksize: 0, + data_size: 0, + root_hash: None, + salt: None, + } + } +} + +pub struct FsVerityDescriptorBuilder<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> { + fbb_: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + start_: ::flatbuffers::WIPOffset<::flatbuffers::TableUnfinishedWIPOffset>, +} +impl<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> FsVerityDescriptorBuilder<'a, 'b, A> { + #[inline] + pub fn add_version(&mut self, version: u8) { + self.fbb_.push_slot::(FsVerityDescriptor::VT_VERSION, version, 0); + } + #[inline] + pub fn add_hash_algorithm(&mut self, hash_algorithm: u8) { + self.fbb_.push_slot::(FsVerityDescriptor::VT_HASH_ALGORITHM, hash_algorithm, 0); + } + #[inline] + pub fn add_log_blocksize(&mut self, log_blocksize: u8) { + self.fbb_.push_slot::(FsVerityDescriptor::VT_LOG_BLOCKSIZE, log_blocksize, 0); + } + #[inline] + pub fn add_data_size(&mut self, data_size: u64) { + self.fbb_.push_slot::(FsVerityDescriptor::VT_DATA_SIZE, data_size, 0); + } + #[inline] + pub fn add_root_hash(&mut self, root_hash: ::flatbuffers::WIPOffset<::flatbuffers::Vector<'b , u8>>) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>(FsVerityDescriptor::VT_ROOT_HASH, root_hash); + } + #[inline] + pub fn add_salt(&mut self, salt: ::flatbuffers::WIPOffset<::flatbuffers::Vector<'b , u8>>) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>(FsVerityDescriptor::VT_SALT, salt); + } + #[inline] + pub fn new(_fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>) -> FsVerityDescriptorBuilder<'a, 'b, A> { + let start = _fbb.start_table(); + FsVerityDescriptorBuilder { + fbb_: _fbb, + start_: start, + } + } + #[inline] + pub fn finish(self) -> ::flatbuffers::WIPOffset> { + let o = self.fbb_.end_table(self.start_); + ::flatbuffers::WIPOffset::new(o.value()) + } +} + +impl ::core::fmt::Debug for FsVerityDescriptor<'_> { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + let mut ds = f.debug_struct("FsVerityDescriptor"); + ds.field("version", &self.version()); + ds.field("hash_algorithm", &self.hash_algorithm()); + ds.field("log_blocksize", &self.log_blocksize()); + ds.field("data_size", &self.data_size()); + ds.field("root_hash", &self.root_hash()); + ds.field("salt", &self.salt()); + ds.finish() + } +} +pub enum KeyValueOffset {} +#[derive(Copy, Clone, PartialEq)] + +pub struct KeyValue<'a> { + pub _tab: ::flatbuffers::Table<'a>, +} + +impl<'a> ::flatbuffers::Follow<'a> for KeyValue<'a> { + type Inner = KeyValue<'a>; + #[inline] + unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + Self { _tab: unsafe { ::flatbuffers::Table::new(buf, loc) } } + } +} + +impl<'a> KeyValue<'a> { + pub const VT_KEY: ::flatbuffers::VOffsetT = 4; + pub const VT_VALUE: ::flatbuffers::VOffsetT = 6; + + #[inline] + pub unsafe fn init_from_table(table: ::flatbuffers::Table<'a>) -> Self { + KeyValue { _tab: table } + } + #[allow(unused_mut)] + pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr, A: ::flatbuffers::Allocator + 'bldr>( + _fbb: &'mut_bldr mut ::flatbuffers::FlatBufferBuilder<'bldr, A>, + args: &'args KeyValueArgs<'args> + ) -> ::flatbuffers::WIPOffset> { + let mut builder = KeyValueBuilder::new(_fbb); + if let Some(x) = args.value { builder.add_value(x); } + if let Some(x) = args.key { builder.add_key(x); } + builder.finish() + } + + + #[inline] + pub fn key(&self) -> Option<&'a str> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::<::flatbuffers::ForwardsUOffset<&str>>(KeyValue::VT_KEY, None)} + } + #[inline] + pub fn value(&self) -> Option<&'a str> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::<::flatbuffers::ForwardsUOffset<&str>>(KeyValue::VT_VALUE, None)} + } +} + +impl ::flatbuffers::Verifiable for KeyValue<'_> { + #[inline] + fn run_verifier( + v: &mut ::flatbuffers::Verifier, pos: usize + ) -> Result<(), ::flatbuffers::InvalidFlatbuffer> { + v.visit_table(pos)? + .visit_field::<::flatbuffers::ForwardsUOffset<&str>>("key", Self::VT_KEY, false)? + .visit_field::<::flatbuffers::ForwardsUOffset<&str>>("value", Self::VT_VALUE, false)? + .finish(); + Ok(()) + } +} +pub struct KeyValueArgs<'a> { + pub key: Option<::flatbuffers::WIPOffset<&'a str>>, + pub value: Option<::flatbuffers::WIPOffset<&'a str>>, +} +impl<'a> Default for KeyValueArgs<'a> { + #[inline] + fn default() -> Self { + KeyValueArgs { + key: None, + value: None, + } + } +} + +pub struct KeyValueBuilder<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> { + fbb_: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + start_: ::flatbuffers::WIPOffset<::flatbuffers::TableUnfinishedWIPOffset>, +} +impl<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> KeyValueBuilder<'a, 'b, A> { + #[inline] + pub fn add_key(&mut self, key: ::flatbuffers::WIPOffset<&'b str>) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>(KeyValue::VT_KEY, key); + } + #[inline] + pub fn add_value(&mut self, value: ::flatbuffers::WIPOffset<&'b str>) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>(KeyValue::VT_VALUE, value); + } + #[inline] + pub fn new(_fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>) -> KeyValueBuilder<'a, 'b, A> { + let start = _fbb.start_table(); + KeyValueBuilder { + fbb_: _fbb, + start_: start, + } + } + #[inline] + pub fn finish(self) -> ::flatbuffers::WIPOffset> { + let o = self.fbb_.end_table(self.start_); + ::flatbuffers::WIPOffset::new(o.value()) + } +} + +impl ::core::fmt::Debug for KeyValue<'_> { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + let mut ds = f.debug_struct("KeyValue"); + ds.field("key", &self.key()); + ds.field("value", &self.value()); + ds.finish() + } +} +pub enum FileInfoOffset {} +#[derive(Copy, Clone, PartialEq)] + +pub struct FileInfo<'a> { + pub _tab: ::flatbuffers::Table<'a>, +} + +impl<'a> ::flatbuffers::Follow<'a> for FileInfo<'a> { + type Inner = FileInfo<'a>; + #[inline] + unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + Self { _tab: unsafe { ::flatbuffers::Table::new(buf, loc) } } + } +} + +impl<'a> FileInfo<'a> { + pub const VT_PATH: ::flatbuffers::VOffsetT = 4; + pub const VT_DESCRIPTOR: ::flatbuffers::VOffsetT = 6; + pub const VT_MERKLE_TREE_LEVEL1: ::flatbuffers::VOffsetT = 8; + pub const VT_DESCRIPTOR_HASH: ::flatbuffers::VOffsetT = 10; + + #[inline] + pub unsafe fn init_from_table(table: ::flatbuffers::Table<'a>) -> Self { + FileInfo { _tab: table } + } + #[allow(unused_mut)] + pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr, A: ::flatbuffers::Allocator + 'bldr>( + _fbb: &'mut_bldr mut ::flatbuffers::FlatBufferBuilder<'bldr, A>, + args: &'args FileInfoArgs<'args> + ) -> ::flatbuffers::WIPOffset> { + let mut builder = FileInfoBuilder::new(_fbb); + if let Some(x) = args.descriptor_hash { builder.add_descriptor_hash(x); } + if let Some(x) = args.merkle_tree_level1 { builder.add_merkle_tree_level1(x); } + if let Some(x) = args.descriptor { builder.add_descriptor(x); } + if let Some(x) = args.path { builder.add_path(x); } + builder.finish() + } + + + #[inline] + pub fn path(&self) -> Option<&'a str> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::<::flatbuffers::ForwardsUOffset<&str>>(FileInfo::VT_PATH, None)} + } + #[inline] + pub fn descriptor(&self) -> Option> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::<::flatbuffers::ForwardsUOffset>(FileInfo::VT_DESCRIPTOR, None)} + } + #[inline] + pub fn merkle_tree_level1(&self) -> Option<::flatbuffers::Vector<'a, u8>> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::<::flatbuffers::ForwardsUOffset<::flatbuffers::Vector<'a, u8>>>(FileInfo::VT_MERKLE_TREE_LEVEL1, None)} + } + #[inline] + pub fn descriptor_hash(&self) -> Option<&'a str> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::<::flatbuffers::ForwardsUOffset<&str>>(FileInfo::VT_DESCRIPTOR_HASH, None)} + } +} + +impl ::flatbuffers::Verifiable for FileInfo<'_> { + #[inline] + fn run_verifier( + v: &mut ::flatbuffers::Verifier, pos: usize + ) -> Result<(), ::flatbuffers::InvalidFlatbuffer> { + v.visit_table(pos)? + .visit_field::<::flatbuffers::ForwardsUOffset<&str>>("path", Self::VT_PATH, false)? + .visit_field::<::flatbuffers::ForwardsUOffset>("descriptor", Self::VT_DESCRIPTOR, false)? + .visit_field::<::flatbuffers::ForwardsUOffset<::flatbuffers::Vector<'_, u8>>>("merkle_tree_level1", Self::VT_MERKLE_TREE_LEVEL1, false)? + .visit_field::<::flatbuffers::ForwardsUOffset<&str>>("descriptor_hash", Self::VT_DESCRIPTOR_HASH, false)? + .finish(); + Ok(()) + } +} +pub struct FileInfoArgs<'a> { + pub path: Option<::flatbuffers::WIPOffset<&'a str>>, + pub descriptor: Option<::flatbuffers::WIPOffset>>, + pub merkle_tree_level1: Option<::flatbuffers::WIPOffset<::flatbuffers::Vector<'a, u8>>>, + pub descriptor_hash: Option<::flatbuffers::WIPOffset<&'a str>>, +} +impl<'a> Default for FileInfoArgs<'a> { + #[inline] + fn default() -> Self { + FileInfoArgs { + path: None, + descriptor: None, + merkle_tree_level1: None, + descriptor_hash: None, + } + } +} + +pub struct FileInfoBuilder<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> { + fbb_: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + start_: ::flatbuffers::WIPOffset<::flatbuffers::TableUnfinishedWIPOffset>, +} +impl<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> FileInfoBuilder<'a, 'b, A> { + #[inline] + pub fn add_path(&mut self, path: ::flatbuffers::WIPOffset<&'b str>) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>(FileInfo::VT_PATH, path); + } + #[inline] + pub fn add_descriptor(&mut self, descriptor: ::flatbuffers::WIPOffset>) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset>(FileInfo::VT_DESCRIPTOR, descriptor); + } + #[inline] + pub fn add_merkle_tree_level1(&mut self, merkle_tree_level1: ::flatbuffers::WIPOffset<::flatbuffers::Vector<'b , u8>>) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>(FileInfo::VT_MERKLE_TREE_LEVEL1, merkle_tree_level1); + } + #[inline] + pub fn add_descriptor_hash(&mut self, descriptor_hash: ::flatbuffers::WIPOffset<&'b str>) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>(FileInfo::VT_DESCRIPTOR_HASH, descriptor_hash); + } + #[inline] + pub fn new(_fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>) -> FileInfoBuilder<'a, 'b, A> { + let start = _fbb.start_table(); + FileInfoBuilder { + fbb_: _fbb, + start_: start, + } + } + #[inline] + pub fn finish(self) -> ::flatbuffers::WIPOffset> { + let o = self.fbb_.end_table(self.start_); + ::flatbuffers::WIPOffset::new(o.value()) + } +} + +impl ::core::fmt::Debug for FileInfo<'_> { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + let mut ds = f.debug_struct("FileInfo"); + ds.field("path", &self.path()); + ds.field("descriptor", &self.descriptor()); + ds.field("merkle_tree_level1", &self.merkle_tree_level1()); + ds.field("descriptor_hash", &self.descriptor_hash()); + ds.finish() + } +} +pub enum MetadataOffset {} +#[derive(Copy, Clone, PartialEq)] + +pub struct Metadata<'a> { + pub _tab: ::flatbuffers::Table<'a>, +} + +impl<'a> ::flatbuffers::Follow<'a> for Metadata<'a> { + type Inner = Metadata<'a>; + #[inline] + unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + Self { _tab: unsafe { ::flatbuffers::Table::new(buf, loc) } } + } +} + +impl<'a> Metadata<'a> { + pub const VT_VERSION: ::flatbuffers::VOffsetT = 4; + pub const VT_FILES: ::flatbuffers::VOffsetT = 6; + pub const VT_LABELS: ::flatbuffers::VOffsetT = 8; + + #[inline] + pub unsafe fn init_from_table(table: ::flatbuffers::Table<'a>) -> Self { + Metadata { _tab: table } + } + #[allow(unused_mut)] + pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr, A: ::flatbuffers::Allocator + 'bldr>( + _fbb: &'mut_bldr mut ::flatbuffers::FlatBufferBuilder<'bldr, A>, + args: &'args MetadataArgs<'args> + ) -> ::flatbuffers::WIPOffset> { + let mut builder = MetadataBuilder::new(_fbb); + if let Some(x) = args.labels { builder.add_labels(x); } + if let Some(x) = args.files { builder.add_files(x); } + builder.add_version(args.version); + builder.finish() + } + + + #[inline] + pub fn version(&self) -> u32 { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::(Metadata::VT_VERSION, Some(1)).unwrap()} + } + #[inline] + pub fn files(&self) -> Option<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::<::flatbuffers::ForwardsUOffset<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>>(Metadata::VT_FILES, None)} + } + #[inline] + pub fn labels(&self) -> Option<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::<::flatbuffers::ForwardsUOffset<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>>(Metadata::VT_LABELS, None)} + } +} + +impl ::flatbuffers::Verifiable for Metadata<'_> { + #[inline] + fn run_verifier( + v: &mut ::flatbuffers::Verifier, pos: usize + ) -> Result<(), ::flatbuffers::InvalidFlatbuffer> { + v.visit_table(pos)? + .visit_field::("version", Self::VT_VERSION, false)? + .visit_field::<::flatbuffers::ForwardsUOffset<::flatbuffers::Vector<'_, ::flatbuffers::ForwardsUOffset>>>("files", Self::VT_FILES, false)? + .visit_field::<::flatbuffers::ForwardsUOffset<::flatbuffers::Vector<'_, ::flatbuffers::ForwardsUOffset>>>("labels", Self::VT_LABELS, false)? + .finish(); + Ok(()) + } +} +pub struct MetadataArgs<'a> { + pub version: u32, + pub files: Option<::flatbuffers::WIPOffset<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>>>, + pub labels: Option<::flatbuffers::WIPOffset<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>>>, +} +impl<'a> Default for MetadataArgs<'a> { + #[inline] + fn default() -> Self { + MetadataArgs { + version: 1, + files: None, + labels: None, + } + } +} + +pub struct MetadataBuilder<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> { + fbb_: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + start_: ::flatbuffers::WIPOffset<::flatbuffers::TableUnfinishedWIPOffset>, +} +impl<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> MetadataBuilder<'a, 'b, A> { + #[inline] + pub fn add_version(&mut self, version: u32) { + self.fbb_.push_slot::(Metadata::VT_VERSION, version, 1); + } + #[inline] + pub fn add_files(&mut self, files: ::flatbuffers::WIPOffset<::flatbuffers::Vector<'b , ::flatbuffers::ForwardsUOffset>>>) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>(Metadata::VT_FILES, files); + } + #[inline] + pub fn add_labels(&mut self, labels: ::flatbuffers::WIPOffset<::flatbuffers::Vector<'b , ::flatbuffers::ForwardsUOffset>>>) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>(Metadata::VT_LABELS, labels); + } + #[inline] + pub fn new(_fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>) -> MetadataBuilder<'a, 'b, A> { + let start = _fbb.start_table(); + MetadataBuilder { + fbb_: _fbb, + start_: start, + } + } + #[inline] + pub fn finish(self) -> ::flatbuffers::WIPOffset> { + let o = self.fbb_.end_table(self.start_); + ::flatbuffers::WIPOffset::new(o.value()) + } +} + +impl ::core::fmt::Debug for Metadata<'_> { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + let mut ds = f.debug_struct("Metadata"); + ds.field("version", &self.version()); + ds.field("files", &self.files()); + ds.field("labels", &self.labels()); + ds.finish() + } +} +#[inline] +/// Verifies that a buffer of bytes contains a `Metadata` +/// and returns it. +/// Note that verification is still experimental and may not +/// catch every error, or be maximally performant. For the +/// previous, unchecked, behavior use +/// `root_as_metadata_unchecked`. +pub fn root_as_metadata(buf: &[u8]) -> Result, ::flatbuffers::InvalidFlatbuffer> { + ::flatbuffers::root::(buf) +} +#[inline] +/// Verifies that a buffer of bytes contains a size prefixed +/// `Metadata` and returns it. +/// Note that verification is still experimental and may not +/// catch every error, or be maximally performant. For the +/// previous, unchecked, behavior use +/// `size_prefixed_root_as_metadata_unchecked`. +pub fn size_prefixed_root_as_metadata(buf: &[u8]) -> Result, ::flatbuffers::InvalidFlatbuffer> { + ::flatbuffers::size_prefixed_root::(buf) +} +#[inline] +/// Verifies, with the given options, that a buffer of bytes +/// contains a `Metadata` and returns it. +/// Note that verification is still experimental and may not +/// catch every error, or be maximally performant. For the +/// previous, unchecked, behavior use +/// `root_as_metadata_unchecked`. +pub fn root_as_metadata_with_opts<'b, 'o>( + opts: &'o ::flatbuffers::VerifierOptions, + buf: &'b [u8], +) -> Result, ::flatbuffers::InvalidFlatbuffer> { + ::flatbuffers::root_with_opts::>(opts, buf) +} +#[inline] +/// Verifies, with the given verifier options, that a buffer of +/// bytes contains a size prefixed `Metadata` and returns +/// it. Note that verification is still experimental and may not +/// catch every error, or be maximally performant. For the +/// previous, unchecked, behavior use +/// `root_as_metadata_unchecked`. +pub fn size_prefixed_root_as_metadata_with_opts<'b, 'o>( + opts: &'o ::flatbuffers::VerifierOptions, + buf: &'b [u8], +) -> Result, ::flatbuffers::InvalidFlatbuffer> { + ::flatbuffers::size_prefixed_root_with_opts::>(opts, buf) +} +#[inline] +/// Assumes, without verification, that a buffer of bytes contains a Metadata and returns it. +/// # Safety +/// Callers must trust the given bytes do indeed contain a valid `Metadata`. +pub unsafe fn root_as_metadata_unchecked(buf: &[u8]) -> Metadata<'_> { + unsafe { ::flatbuffers::root_unchecked::(buf) } +} +#[inline] +/// Assumes, without verification, that a buffer of bytes contains a size prefixed Metadata and returns it. +/// # Safety +/// Callers must trust the given bytes do indeed contain a valid size prefixed `Metadata`. +pub unsafe fn size_prefixed_root_as_metadata_unchecked(buf: &[u8]) -> Metadata<'_> { + unsafe { ::flatbuffers::size_prefixed_root_unchecked::(buf) } +} +#[inline] +pub fn finish_metadata_buffer<'a, 'b, A: ::flatbuffers::Allocator + 'a>( + fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + root: ::flatbuffers::WIPOffset>) { + fbb.finish(root, None); +} + +#[inline] +pub fn finish_size_prefixed_metadata_buffer<'a, 'b, A: ::flatbuffers::Allocator + 'a>(fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, root: ::flatbuffers::WIPOffset>) { + fbb.finish_size_prefixed(root, None); +} +} // pub mod verity +} // pub mod cryptpilot + diff --git a/cryptpilot-verity/src/metadata/metadata_hash_generated.rs b/cryptpilot-verity/src/metadata/metadata_hash_generated.rs index 81557fdc..2d767e06 100644 --- a/cryptpilot-verity/src/metadata/metadata_hash_generated.rs +++ b/cryptpilot-verity/src/metadata/metadata_hash_generated.rs @@ -2,392 +2,298 @@ // @generated extern crate alloc; + #[allow(unused_imports, dead_code)] pub mod cryptpilot { - #[allow(unused_imports, dead_code)] - pub mod verity { +#[allow(unused_imports, dead_code)] +pub mod verity { + +#[allow(unused_imports, dead_code)] +pub mod hash { + + +pub enum FileHashEntryOffset {} +#[derive(Copy, Clone, PartialEq)] + +pub struct FileHashEntry<'a> { + pub _tab: ::flatbuffers::Table<'a>, +} - #[allow(unused_imports, dead_code)] - pub mod hash { +impl<'a> ::flatbuffers::Follow<'a> for FileHashEntry<'a> { + type Inner = FileHashEntry<'a>; + #[inline] + unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + Self { _tab: unsafe { ::flatbuffers::Table::new(buf, loc) } } + } +} - pub enum FileHashEntryOffset {} - #[derive(Copy, Clone, PartialEq)] +impl<'a> FileHashEntry<'a> { + pub const VT_PATH: ::flatbuffers::VOffsetT = 4; + pub const VT_DESCRIPTOR_HASH: ::flatbuffers::VOffsetT = 6; - pub struct FileHashEntry<'a> { - pub _tab: ::flatbuffers::Table<'a>, - } + #[inline] + pub unsafe fn init_from_table(table: ::flatbuffers::Table<'a>) -> Self { + FileHashEntry { _tab: table } + } + #[allow(unused_mut)] + pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr, A: ::flatbuffers::Allocator + 'bldr>( + _fbb: &'mut_bldr mut ::flatbuffers::FlatBufferBuilder<'bldr, A>, + args: &'args FileHashEntryArgs<'args> + ) -> ::flatbuffers::WIPOffset> { + let mut builder = FileHashEntryBuilder::new(_fbb); + if let Some(x) = args.descriptor_hash { builder.add_descriptor_hash(x); } + if let Some(x) = args.path { builder.add_path(x); } + builder.finish() + } - impl<'a> ::flatbuffers::Follow<'a> for FileHashEntry<'a> { - type Inner = FileHashEntry<'a>; - #[inline] - unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { - Self { - _tab: unsafe { ::flatbuffers::Table::new(buf, loc) }, - } - } - } - impl<'a> FileHashEntry<'a> { - pub const VT_PATH: ::flatbuffers::VOffsetT = 4; - pub const VT_DESCRIPTOR_HASH: ::flatbuffers::VOffsetT = 6; + #[inline] + pub fn path(&self) -> Option<&'a str> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::<::flatbuffers::ForwardsUOffset<&str>>(FileHashEntry::VT_PATH, None)} + } + #[inline] + pub fn descriptor_hash(&self) -> Option<&'a str> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::<::flatbuffers::ForwardsUOffset<&str>>(FileHashEntry::VT_DESCRIPTOR_HASH, None)} + } +} - #[inline] - pub unsafe fn init_from_table(table: ::flatbuffers::Table<'a>) -> Self { - FileHashEntry { _tab: table } - } - #[allow(unused_mut)] - pub fn create< - 'bldr: 'args, - 'args: 'mut_bldr, - 'mut_bldr, - A: ::flatbuffers::Allocator + 'bldr, - >( - _fbb: &'mut_bldr mut ::flatbuffers::FlatBufferBuilder<'bldr, A>, - args: &'args FileHashEntryArgs<'args>, - ) -> ::flatbuffers::WIPOffset> { - let mut builder = FileHashEntryBuilder::new(_fbb); - if let Some(x) = args.descriptor_hash { - builder.add_descriptor_hash(x); - } - if let Some(x) = args.path { - builder.add_path(x); - } - builder.finish() - } +impl ::flatbuffers::Verifiable for FileHashEntry<'_> { + #[inline] + fn run_verifier( + v: &mut ::flatbuffers::Verifier, pos: usize + ) -> Result<(), ::flatbuffers::InvalidFlatbuffer> { + v.visit_table(pos)? + .visit_field::<::flatbuffers::ForwardsUOffset<&str>>("path", Self::VT_PATH, false)? + .visit_field::<::flatbuffers::ForwardsUOffset<&str>>("descriptor_hash", Self::VT_DESCRIPTOR_HASH, false)? + .finish(); + Ok(()) + } +} +pub struct FileHashEntryArgs<'a> { + pub path: Option<::flatbuffers::WIPOffset<&'a str>>, + pub descriptor_hash: Option<::flatbuffers::WIPOffset<&'a str>>, +} +impl<'a> Default for FileHashEntryArgs<'a> { + #[inline] + fn default() -> Self { + FileHashEntryArgs { + path: None, + descriptor_hash: None, + } + } +} - #[inline] - pub fn path(&self) -> Option<&'a str> { - // Safety: - // Created from valid Table for this object - // which contains a valid value in this slot - unsafe { - self._tab.get::<::flatbuffers::ForwardsUOffset<&str>>( - FileHashEntry::VT_PATH, - None, - ) - } - } - #[inline] - pub fn descriptor_hash(&self) -> Option<&'a str> { - // Safety: - // Created from valid Table for this object - // which contains a valid value in this slot - unsafe { - self._tab.get::<::flatbuffers::ForwardsUOffset<&str>>( - FileHashEntry::VT_DESCRIPTOR_HASH, - None, - ) - } - } - } +pub struct FileHashEntryBuilder<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> { + fbb_: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + start_: ::flatbuffers::WIPOffset<::flatbuffers::TableUnfinishedWIPOffset>, +} +impl<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> FileHashEntryBuilder<'a, 'b, A> { + #[inline] + pub fn add_path(&mut self, path: ::flatbuffers::WIPOffset<&'b str>) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>(FileHashEntry::VT_PATH, path); + } + #[inline] + pub fn add_descriptor_hash(&mut self, descriptor_hash: ::flatbuffers::WIPOffset<&'b str>) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>(FileHashEntry::VT_DESCRIPTOR_HASH, descriptor_hash); + } + #[inline] + pub fn new(_fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>) -> FileHashEntryBuilder<'a, 'b, A> { + let start = _fbb.start_table(); + FileHashEntryBuilder { + fbb_: _fbb, + start_: start, + } + } + #[inline] + pub fn finish(self) -> ::flatbuffers::WIPOffset> { + let o = self.fbb_.end_table(self.start_); + ::flatbuffers::WIPOffset::new(o.value()) + } +} - impl ::flatbuffers::Verifiable for FileHashEntry<'_> { - #[inline] - fn run_verifier( - v: &mut ::flatbuffers::Verifier, - pos: usize, - ) -> Result<(), ::flatbuffers::InvalidFlatbuffer> { - v.visit_table(pos)? - .visit_field::<::flatbuffers::ForwardsUOffset<&str>>( - "path", - Self::VT_PATH, - false, - )? - .visit_field::<::flatbuffers::ForwardsUOffset<&str>>( - "descriptor_hash", - Self::VT_DESCRIPTOR_HASH, - false, - )? - .finish(); - Ok(()) - } - } - pub struct FileHashEntryArgs<'a> { - pub path: Option<::flatbuffers::WIPOffset<&'a str>>, - pub descriptor_hash: Option<::flatbuffers::WIPOffset<&'a str>>, - } - impl<'a> Default for FileHashEntryArgs<'a> { - #[inline] - fn default() -> Self { - FileHashEntryArgs { - path: None, - descriptor_hash: None, - } - } - } +impl ::core::fmt::Debug for FileHashEntry<'_> { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + let mut ds = f.debug_struct("FileHashEntry"); + ds.field("path", &self.path()); + ds.field("descriptor_hash", &self.descriptor_hash()); + ds.finish() + } +} +pub enum MetadataHashOffset {} +#[derive(Copy, Clone, PartialEq)] - pub struct FileHashEntryBuilder<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> { - fbb_: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, - start_: ::flatbuffers::WIPOffset<::flatbuffers::TableUnfinishedWIPOffset>, - } - impl<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> FileHashEntryBuilder<'a, 'b, A> { - #[inline] - pub fn add_path(&mut self, path: ::flatbuffers::WIPOffset<&'b str>) { - self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>( - FileHashEntry::VT_PATH, - path, - ); - } - #[inline] - pub fn add_descriptor_hash( - &mut self, - descriptor_hash: ::flatbuffers::WIPOffset<&'b str>, - ) { - self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>( - FileHashEntry::VT_DESCRIPTOR_HASH, - descriptor_hash, - ); - } - #[inline] - pub fn new( - _fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, - ) -> FileHashEntryBuilder<'a, 'b, A> { - let start = _fbb.start_table(); - FileHashEntryBuilder { - fbb_: _fbb, - start_: start, - } - } - #[inline] - pub fn finish(self) -> ::flatbuffers::WIPOffset> { - let o = self.fbb_.end_table(self.start_); - ::flatbuffers::WIPOffset::new(o.value()) - } - } +pub struct MetadataHash<'a> { + pub _tab: ::flatbuffers::Table<'a>, +} - impl ::core::fmt::Debug for FileHashEntry<'_> { - fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - let mut ds = f.debug_struct("FileHashEntry"); - ds.field("path", &self.path()); - ds.field("descriptor_hash", &self.descriptor_hash()); - ds.finish() - } - } - pub enum MetadataHashOffset {} - #[derive(Copy, Clone, PartialEq)] +impl<'a> ::flatbuffers::Follow<'a> for MetadataHash<'a> { + type Inner = MetadataHash<'a>; + #[inline] + unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + Self { _tab: unsafe { ::flatbuffers::Table::new(buf, loc) } } + } +} - pub struct MetadataHash<'a> { - pub _tab: ::flatbuffers::Table<'a>, - } +impl<'a> MetadataHash<'a> { + pub const VT_FILES: ::flatbuffers::VOffsetT = 4; - impl<'a> ::flatbuffers::Follow<'a> for MetadataHash<'a> { - type Inner = MetadataHash<'a>; - #[inline] - unsafe fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { - Self { - _tab: unsafe { ::flatbuffers::Table::new(buf, loc) }, - } - } - } + #[inline] + pub unsafe fn init_from_table(table: ::flatbuffers::Table<'a>) -> Self { + MetadataHash { _tab: table } + } + #[allow(unused_mut)] + pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr, A: ::flatbuffers::Allocator + 'bldr>( + _fbb: &'mut_bldr mut ::flatbuffers::FlatBufferBuilder<'bldr, A>, + args: &'args MetadataHashArgs<'args> + ) -> ::flatbuffers::WIPOffset> { + let mut builder = MetadataHashBuilder::new(_fbb); + if let Some(x) = args.files { builder.add_files(x); } + builder.finish() + } - impl<'a> MetadataHash<'a> { - pub const VT_FILES: ::flatbuffers::VOffsetT = 4; - #[inline] - pub unsafe fn init_from_table(table: ::flatbuffers::Table<'a>) -> Self { - MetadataHash { _tab: table } - } - #[allow(unused_mut)] - pub fn create< - 'bldr: 'args, - 'args: 'mut_bldr, - 'mut_bldr, - A: ::flatbuffers::Allocator + 'bldr, - >( - _fbb: &'mut_bldr mut ::flatbuffers::FlatBufferBuilder<'bldr, A>, - args: &'args MetadataHashArgs<'args>, - ) -> ::flatbuffers::WIPOffset> { - let mut builder = MetadataHashBuilder::new(_fbb); - if let Some(x) = args.files { - builder.add_files(x); - } - builder.finish() - } + #[inline] + pub fn files(&self) -> Option<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>> { + // Safety: + // Created from valid Table for this object + // which contains a valid value in this slot + unsafe { self._tab.get::<::flatbuffers::ForwardsUOffset<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>>(MetadataHash::VT_FILES, None)} + } +} - #[inline] - pub fn files( - &self, - ) -> Option< - ::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>, - > { - // Safety: - // Created from valid Table for this object - // which contains a valid value in this slot - unsafe { - self._tab.get::<::flatbuffers::ForwardsUOffset< - ::flatbuffers::Vector< - 'a, - ::flatbuffers::ForwardsUOffset, - >, - >>(MetadataHash::VT_FILES, None) - } - } - } +impl ::flatbuffers::Verifiable for MetadataHash<'_> { + #[inline] + fn run_verifier( + v: &mut ::flatbuffers::Verifier, pos: usize + ) -> Result<(), ::flatbuffers::InvalidFlatbuffer> { + v.visit_table(pos)? + .visit_field::<::flatbuffers::ForwardsUOffset<::flatbuffers::Vector<'_, ::flatbuffers::ForwardsUOffset>>>("files", Self::VT_FILES, false)? + .finish(); + Ok(()) + } +} +pub struct MetadataHashArgs<'a> { + pub files: Option<::flatbuffers::WIPOffset<::flatbuffers::Vector<'a, ::flatbuffers::ForwardsUOffset>>>>, +} +impl<'a> Default for MetadataHashArgs<'a> { + #[inline] + fn default() -> Self { + MetadataHashArgs { + files: None, + } + } +} - impl ::flatbuffers::Verifiable for MetadataHash<'_> { - #[inline] - fn run_verifier( - v: &mut ::flatbuffers::Verifier, - pos: usize, - ) -> Result<(), ::flatbuffers::InvalidFlatbuffer> { - v.visit_table(pos)? - .visit_field::<::flatbuffers::ForwardsUOffset< - ::flatbuffers::Vector< - '_, - ::flatbuffers::ForwardsUOffset, - >, - >>("files", Self::VT_FILES, false)? - .finish(); - Ok(()) - } - } - pub struct MetadataHashArgs<'a> { - pub files: Option< - ::flatbuffers::WIPOffset< - ::flatbuffers::Vector< - 'a, - ::flatbuffers::ForwardsUOffset>, - >, - >, - >, - } - impl<'a> Default for MetadataHashArgs<'a> { - #[inline] - fn default() -> Self { - MetadataHashArgs { files: None } - } - } +pub struct MetadataHashBuilder<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> { + fbb_: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + start_: ::flatbuffers::WIPOffset<::flatbuffers::TableUnfinishedWIPOffset>, +} +impl<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> MetadataHashBuilder<'a, 'b, A> { + #[inline] + pub fn add_files(&mut self, files: ::flatbuffers::WIPOffset<::flatbuffers::Vector<'b , ::flatbuffers::ForwardsUOffset>>>) { + self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>(MetadataHash::VT_FILES, files); + } + #[inline] + pub fn new(_fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>) -> MetadataHashBuilder<'a, 'b, A> { + let start = _fbb.start_table(); + MetadataHashBuilder { + fbb_: _fbb, + start_: start, + } + } + #[inline] + pub fn finish(self) -> ::flatbuffers::WIPOffset> { + let o = self.fbb_.end_table(self.start_); + ::flatbuffers::WIPOffset::new(o.value()) + } +} - pub struct MetadataHashBuilder<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> { - fbb_: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, - start_: ::flatbuffers::WIPOffset<::flatbuffers::TableUnfinishedWIPOffset>, - } - impl<'a: 'b, 'b, A: ::flatbuffers::Allocator + 'a> MetadataHashBuilder<'a, 'b, A> { - #[inline] - pub fn add_files( - &mut self, - files: ::flatbuffers::WIPOffset< - ::flatbuffers::Vector< - 'b, - ::flatbuffers::ForwardsUOffset>, - >, - >, - ) { - self.fbb_.push_slot_always::<::flatbuffers::WIPOffset<_>>( - MetadataHash::VT_FILES, - files, - ); - } - #[inline] - pub fn new( - _fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, - ) -> MetadataHashBuilder<'a, 'b, A> { - let start = _fbb.start_table(); - MetadataHashBuilder { - fbb_: _fbb, - start_: start, - } - } - #[inline] - pub fn finish(self) -> ::flatbuffers::WIPOffset> { - let o = self.fbb_.end_table(self.start_); - ::flatbuffers::WIPOffset::new(o.value()) - } - } +impl ::core::fmt::Debug for MetadataHash<'_> { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + let mut ds = f.debug_struct("MetadataHash"); + ds.field("files", &self.files()); + ds.finish() + } +} +#[inline] +/// Verifies that a buffer of bytes contains a `MetadataHash` +/// and returns it. +/// Note that verification is still experimental and may not +/// catch every error, or be maximally performant. For the +/// previous, unchecked, behavior use +/// `root_as_metadata_hash_unchecked`. +pub fn root_as_metadata_hash(buf: &[u8]) -> Result, ::flatbuffers::InvalidFlatbuffer> { + ::flatbuffers::root::(buf) +} +#[inline] +/// Verifies that a buffer of bytes contains a size prefixed +/// `MetadataHash` and returns it. +/// Note that verification is still experimental and may not +/// catch every error, or be maximally performant. For the +/// previous, unchecked, behavior use +/// `size_prefixed_root_as_metadata_hash_unchecked`. +pub fn size_prefixed_root_as_metadata_hash(buf: &[u8]) -> Result, ::flatbuffers::InvalidFlatbuffer> { + ::flatbuffers::size_prefixed_root::(buf) +} +#[inline] +/// Verifies, with the given options, that a buffer of bytes +/// contains a `MetadataHash` and returns it. +/// Note that verification is still experimental and may not +/// catch every error, or be maximally performant. For the +/// previous, unchecked, behavior use +/// `root_as_metadata_hash_unchecked`. +pub fn root_as_metadata_hash_with_opts<'b, 'o>( + opts: &'o ::flatbuffers::VerifierOptions, + buf: &'b [u8], +) -> Result, ::flatbuffers::InvalidFlatbuffer> { + ::flatbuffers::root_with_opts::>(opts, buf) +} +#[inline] +/// Verifies, with the given verifier options, that a buffer of +/// bytes contains a size prefixed `MetadataHash` and returns +/// it. Note that verification is still experimental and may not +/// catch every error, or be maximally performant. For the +/// previous, unchecked, behavior use +/// `root_as_metadata_hash_unchecked`. +pub fn size_prefixed_root_as_metadata_hash_with_opts<'b, 'o>( + opts: &'o ::flatbuffers::VerifierOptions, + buf: &'b [u8], +) -> Result, ::flatbuffers::InvalidFlatbuffer> { + ::flatbuffers::size_prefixed_root_with_opts::>(opts, buf) +} +#[inline] +/// Assumes, without verification, that a buffer of bytes contains a MetadataHash and returns it. +/// # Safety +/// Callers must trust the given bytes do indeed contain a valid `MetadataHash`. +pub unsafe fn root_as_metadata_hash_unchecked(buf: &[u8]) -> MetadataHash<'_> { + unsafe { ::flatbuffers::root_unchecked::(buf) } +} +#[inline] +/// Assumes, without verification, that a buffer of bytes contains a size prefixed MetadataHash and returns it. +/// # Safety +/// Callers must trust the given bytes do indeed contain a valid size prefixed `MetadataHash`. +pub unsafe fn size_prefixed_root_as_metadata_hash_unchecked(buf: &[u8]) -> MetadataHash<'_> { + unsafe { ::flatbuffers::size_prefixed_root_unchecked::(buf) } +} +#[inline] +pub fn finish_metadata_hash_buffer<'a, 'b, A: ::flatbuffers::Allocator + 'a>( + fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, + root: ::flatbuffers::WIPOffset>) { + fbb.finish(root, None); +} - impl ::core::fmt::Debug for MetadataHash<'_> { - fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - let mut ds = f.debug_struct("MetadataHash"); - ds.field("files", &self.files()); - ds.finish() - } - } - #[inline] - /// Verifies that a buffer of bytes contains a `MetadataHash` - /// and returns it. - /// Note that verification is still experimental and may not - /// catch every error, or be maximally performant. For the - /// previous, unchecked, behavior use - /// `root_as_metadata_hash_unchecked`. - pub fn root_as_metadata_hash( - buf: &[u8], - ) -> Result, ::flatbuffers::InvalidFlatbuffer> { - ::flatbuffers::root::(buf) - } - #[inline] - /// Verifies that a buffer of bytes contains a size prefixed - /// `MetadataHash` and returns it. - /// Note that verification is still experimental and may not - /// catch every error, or be maximally performant. For the - /// previous, unchecked, behavior use - /// `size_prefixed_root_as_metadata_hash_unchecked`. - pub fn size_prefixed_root_as_metadata_hash( - buf: &[u8], - ) -> Result, ::flatbuffers::InvalidFlatbuffer> { - ::flatbuffers::size_prefixed_root::(buf) - } - #[inline] - /// Verifies, with the given options, that a buffer of bytes - /// contains a `MetadataHash` and returns it. - /// Note that verification is still experimental and may not - /// catch every error, or be maximally performant. For the - /// previous, unchecked, behavior use - /// `root_as_metadata_hash_unchecked`. - pub fn root_as_metadata_hash_with_opts<'b, 'o>( - opts: &'o ::flatbuffers::VerifierOptions, - buf: &'b [u8], - ) -> Result, ::flatbuffers::InvalidFlatbuffer> { - ::flatbuffers::root_with_opts::>(opts, buf) - } - #[inline] - /// Verifies, with the given verifier options, that a buffer of - /// bytes contains a size prefixed `MetadataHash` and returns - /// it. Note that verification is still experimental and may not - /// catch every error, or be maximally performant. For the - /// previous, unchecked, behavior use - /// `root_as_metadata_hash_unchecked`. - pub fn size_prefixed_root_as_metadata_hash_with_opts<'b, 'o>( - opts: &'o ::flatbuffers::VerifierOptions, - buf: &'b [u8], - ) -> Result, ::flatbuffers::InvalidFlatbuffer> { - ::flatbuffers::size_prefixed_root_with_opts::>(opts, buf) - } - #[inline] - /// Assumes, without verification, that a buffer of bytes contains a MetadataHash and returns it. - /// # Safety - /// Callers must trust the given bytes do indeed contain a valid `MetadataHash`. - pub unsafe fn root_as_metadata_hash_unchecked(buf: &[u8]) -> MetadataHash<'_> { - unsafe { ::flatbuffers::root_unchecked::(buf) } - } - #[inline] - /// Assumes, without verification, that a buffer of bytes contains a size prefixed MetadataHash and returns it. - /// # Safety - /// Callers must trust the given bytes do indeed contain a valid size prefixed `MetadataHash`. - pub unsafe fn size_prefixed_root_as_metadata_hash_unchecked( - buf: &[u8], - ) -> MetadataHash<'_> { - unsafe { ::flatbuffers::size_prefixed_root_unchecked::(buf) } - } - #[inline] - pub fn finish_metadata_hash_buffer<'a, 'b, A: ::flatbuffers::Allocator + 'a>( - fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, - root: ::flatbuffers::WIPOffset>, - ) { - fbb.finish(root, None); - } +#[inline] +pub fn finish_size_prefixed_metadata_hash_buffer<'a, 'b, A: ::flatbuffers::Allocator + 'a>(fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, root: ::flatbuffers::WIPOffset>) { + fbb.finish_size_prefixed(root, None); +} +} // pub mod hash +} // pub mod verity +} // pub mod cryptpilot - #[inline] - pub fn finish_size_prefixed_metadata_hash_buffer< - 'a, - 'b, - A: ::flatbuffers::Allocator + 'a, - >( - fbb: &'b mut ::flatbuffers::FlatBufferBuilder<'a, A>, - root: ::flatbuffers::WIPOffset>, - ) { - fbb.finish_size_prefixed(root, None); - } - } // pub mod hash - } // pub mod verity -} // pub mod cryptpilot From bdb6694698672de8b1177b9ce783635d967d0b9c Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Sat, 9 May 2026 18:38:51 +0800 Subject: [PATCH 10/18] ci: add Go test job and interop test targets Add go-test job to CI with race detection, schema sync check, cross-language tests, and interop tests. Add Makefile targets for check-fbs, go-test, cross-test, interop-rust-produces, interop-go-produces, interop-test, and check-all. --- .github/workflows/test.yml | 26 +++++++++++++++ Cargo.lock | 1 + Makefile | 55 ++++++++++++++++++++++++++++++++ tests/cross_lang_test.sh | 65 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 147 insertions(+) create mode 100755 tests/cross_lang_test.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f00cdcd1..554e1a1a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -72,3 +72,29 @@ jobs: - name: Run test script from repo run: make run-test + + go-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: 'true' + - uses: actions/setup-go@v5 + with: + go-version: '1.24' + - name: Run Go tests + run: | + cd verity-go + go test -race -v ./... + - name: Check FlatBuffers schemas in sync + run: make check-fbs + - name: Run cross-language tests + run: | + cd verity-go + go test -v ./... -run TestCross + - name: Install Rust toolchain for interop tests + uses: dtolnay/rust-toolchain@1.85.0 + - name: Run interop tests + run: | + yum-builddep -y --skip-unavailable ./cryptpilot.spec + make interop-test diff --git a/Cargo.lock b/Cargo.lock index b6e32001..950e5923 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1065,6 +1065,7 @@ dependencies = [ "anyhow", "async-trait", "async-walkdir", + "base64 0.22.1", "clap", "flatbuffers", "flatc", diff --git a/Makefile b/Makefile index ac61f6e8..deaa605e 100644 --- a/Makefile +++ b/Makefile @@ -352,3 +352,58 @@ docker-build-verity: .PHONY: docker-build-all docker-build-all: docker-build +# Cross-language quality assurance targets + +.PHONY: check-fbs +check-fbs: + @test -L verity-go/metadata/metadata.fbs || { echo "ERROR: metadata.fbs should be a symlink to Rust source"; exit 1; } + @test -L verity-go/metadata/metadata_hash.fbs || { echo "ERROR: metadata_hash.fbs should be a symlink to Rust source"; exit 1; } + @echo "FlatBuffers schemas are shared via symlink." + +.PHONY: go-test +go-test: + cd verity-go && /usr/local/go/bin/go test -race -v ./... -skip TestInterop + +.PHONY: cross-test +cross-test: + bash tests/cross_lang_test.sh + +.PHONY: check-all +check-all: clippy go-test check-fbs + cargo fmt --check + +# Interop tests: Rust CLI produces → Go lib consumes, and vice versa + +.PHONY: interop-rust-produces +interop-rust-produces: + @echo "=== Rust produces, Go consumes ===" + @rm -rf /tmp/cryptpilot-interop && mkdir -p /tmp/cryptpilot-interop/data + @printf 'hello' > /tmp/cryptpilot-interop/data/a.txt + @printf 'world' > /tmp/cryptpilot-interop/data/b.txt + @touch /tmp/cryptpilot-interop/data/empty.txt + @cargo run -p cryptpilot-verity --quiet -- format \ + /tmp/cryptpilot-interop/data \ + --hash-output - \ + --label env=prod \ + --force > /tmp/cryptpilot-interop/root_hash.txt + @cd verity-go && INTEROP_DIR=/tmp/cryptpilot-interop \ + /usr/local/go/bin/go test -count=1 -v ./metadata/ -run TestInterop_RustProducesGoVerifies + +.PHONY: interop-go-produces +interop-go-produces: + @echo "=== Go produces, Rust verifies ===" + @rm -rf /tmp/cryptpilot-interop2 && mkdir -p /tmp/cryptpilot-interop2/data + @cd verity-go && INTEROP_DIR=/tmp/cryptpilot-interop2 \ + /usr/local/go/bin/go test -count=1 -v ./metadata/ -run TestInterop_GoProducesRustVerifies + @echo "Running Rust verify..." + @HASH=$$(cargo run -p cryptpilot-verity --quiet -- dump \ + /tmp/cryptpilot-interop2/data --print-root-hash 2>&1 | tail -1) && \ + cargo run -p cryptpilot-verity --quiet -- verify \ + /tmp/cryptpilot-interop2/data "$$HASH" + @rm -rf /tmp/cryptpilot-interop /tmp/cryptpilot-interop2 + @echo "Interop dirs cleaned up" + +.PHONY: interop-test +interop-test: interop-rust-produces interop-go-produces + @echo "=== All interop tests passed ===" + diff --git a/tests/cross_lang_test.sh b/tests/cross_lang_test.sh new file mode 100755 index 00000000..33eb709a --- /dev/null +++ b/tests/cross_lang_test.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# tests/cross_lang_test.sh +# Cross-language round-trip test between Rust and Go fs-verity implementations. +# +# Strategy: Each language embeds a binary fixture produced by the other in its +# test code. The Go test deserializes the Rust fixture and vice versa. This +# avoids needing both toolchains in a single process. +# +# What this script does: +# 1. Runs Go cross-language tests (TestCross_*) +# 2. If cargo is available, runs Rust cross-language tests +# 3. Compares descriptor hashes from both sides for consistency +# +# Requires: go (required), cargo (optional, for full coverage) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +VERITY_GO="$REPO_ROOT/verity-go" +VERITY_RUST="$REPO_ROOT/cryptpilot-verity" + +echo "=== Cross-language Round-Trip Test ===" + +# Check prerequisites +if ! command -v go &>/dev/null; then + echo "SKIP: go not found" + exit 0 +fi + +fail=0 + +# --- Go side --- +echo "" +echo "--- Go cross-language tests ---" +cd "$VERITY_GO" +if go test -v ./metadata/ -run TestCross 2>&1; then + echo " PASS: Go cross-language tests" +else + echo " FAIL: Go cross-language tests" + fail=1 +fi + +# --- Rust side (if available) --- +echo "" +echo "--- Rust cross-language tests ---" +if command -v cargo &>/dev/null; then + cd "$VERITY_RUST" + if cargo test --package cryptpilot-verity -- test_cross --nocapture 2>&1; then + echo " PASS: Rust cross-language tests" + else + echo " FAIL: Rust cross-language tests" + fail=1 + fi +else + echo " SKIP: cargo not found (Rust side not tested)" +fi + +echo "" +if [ "$fail" -eq 0 ]; then + echo "ALL CROSS-LANGUAGE TESTS PASSED" + exit 0 +else + echo "CROSS-LANGUAGE TESTS FAILED" + exit 1 +fi From 6381f241ca4fe16ceeeac8b1142ed3f44292ac2e Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Tue, 12 May 2026 10:41:55 +0800 Subject: [PATCH 11/18] fix: address code review feedback for interop infrastructure - Move interop tests to a dedicated CI job running in a RHEL container (was failing on ubuntu-latest due to yum-builddep) - Populate rustFixtureBase64 in cross_test.go with Rust-serialized output - Add skip guard to TestInterop_RustProducesGoVerifies when interop data directory is missing - Move cleanup of interop temp dirs from interop-go-produces to interop-test parent target --- .github/workflows/test.yml | 26 ++++++++++++++++++++++++-- Makefile | 4 ++-- verity-go/metadata/cross_test.go | 2 +- verity-go/metadata/interop_test.go | 3 +++ 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 554e1a1a..3f62980f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -92,9 +92,31 @@ jobs: run: | cd verity-go go test -v ./... -run TestCross - - name: Install Rust toolchain for interop tests + + interop-test: + runs-on: ubuntu-latest + container: + image: alibaba-cloud-linux-3-registry.cn-hangzhou.cr.aliyuncs.com/alinux3/alinux3:latest + options: --privileged + steps: + - uses: actions/checkout@v6 + with: + submodules: 'true' + - name: Install dependencies + run: | + sed -i -E 's|https?://mirrors.openanolis.cn/anolis/|https://mirrors.aliyun.com/anolis/|g' /etc/yum.repos.d/*.repo + sed -i -E 's|https?://mirrors.cloud.aliyuncs.com/|https://mirrors.aliyun.com/|g' /etc/yum.repos.d/*.repo + yum update -y + yum install -y git yum-utils device-mapper kmod systemd systemd-udev lvm2 util-linux veritysetup fuse3 fuse3-libs + yum-builddep -y --skip-unavailable ./cryptpilot.spec + - name: Install Rust toolchain uses: dtolnay/rust-toolchain@1.85.0 + - name: Install Go + run: | + curl -fsSL https://go.dev/dl/go1.24.0.linux-amd64.tar.gz | tar -C /usr/local -xz + export PATH=/usr/local/go/bin:$PATH + go version - name: Run interop tests run: | - yum-builddep -y --skip-unavailable ./cryptpilot.spec + export PATH=/usr/local/go/bin:$PATH make interop-test diff --git a/Makefile b/Makefile index deaa605e..fdb24e2c 100644 --- a/Makefile +++ b/Makefile @@ -400,10 +400,10 @@ interop-go-produces: /tmp/cryptpilot-interop2/data --print-root-hash 2>&1 | tail -1) && \ cargo run -p cryptpilot-verity --quiet -- verify \ /tmp/cryptpilot-interop2/data "$$HASH" - @rm -rf /tmp/cryptpilot-interop /tmp/cryptpilot-interop2 - @echo "Interop dirs cleaned up" .PHONY: interop-test interop-test: interop-rust-produces interop-go-produces + @rm -rf /tmp/cryptpilot-interop /tmp/cryptpilot-interop2 + @echo "Interop dirs cleaned up" @echo "=== All interop tests passed ===" diff --git a/verity-go/metadata/cross_test.go b/verity-go/metadata/cross_test.go index 17a1d2d5..5c3f3401 100644 --- a/verity-go/metadata/cross_test.go +++ b/verity-go/metadata/cross_test.go @@ -27,7 +27,7 @@ import ( // // Generated by: cryptpilot-verity Rust implementation. // To regenerate, see instructions at top of this file. -const rustFixtureBase64 = "" // TODO: paste base64 from Rust when available +const rustFixtureBase64 = "FAAAAAAAAAAAAAoADAAAAAQACAAKAAAAXAAAAAQAAAACAAAAMAAAAAQAAADg////EAAAAAQAAAADAAAAMS4wAAcAAAB2ZXJzaW9uAAgADAAEAAgACAAAABQAAAAEAAAABAAAAHByb2QAAAAAAwAAAGVudgADAAAAqAEAALwAAAAEAAAAcP7//6QAAAAgAAAACAAAAFAAAAAAAAAAEAAQAAUABgAHAAAACAAMABAAAAAAAQEMDAAAAAQAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAADNkMjQ4Y2E1NDJhMjRmYzYyZDFjNDNiOTE2ZWFlNTAxNjg3OGUyNTMzYzg4MjM4NDgwYjI2MTI4YTFmMWFmOTUAAAAABQAAAGMudHh0AAAAJP///8wAAABAAAAACAAAAHgAAAAgAAAAcOOboRAazQtUjZy/uVh7jXtSmPsJaI7QAjI5eqKQ0hwQABgABQAGAAcAEAAIAAwAEAAAAAABAQwUAAAADAAAAAUAAAAAAAAAAAAAACAAAABw45uhEBrNC1SNnL+5WHuNe1KY+wlojtACMjl6opDSHEAAAAAzMTI4ZWViYTNlNDYzYzJmZjUyOWQzYWExYTVjNDg0NDMxMTNiZDAzN2MxNzU5ZjU1YWI4OGJhNWMyZDQ0NWVkAAAAAAUAAABiLnR4dAAAAAwAFAAEAAgADAAQAAwAAADQAAAAQAAAAAgAAAB8AAAAIAAAALFQVsmo23erVwjRm38zD+E9iO6uPQqycQgdNDjA9GJkEAAcAAUABgAHABAACAAMABAAAAAAAQEMGAAAABAAAAAFAAAAAAAAAAAAAAAAAAAAIAAAALFQVsmo23erVwjRm38zD+E9iO6uPQqycQgdNDjA9GJkQAAAADU1NWI1ODljMjZlZTQzYjdhMjUxMGU2YzY3Y2VkOWZiMzE5MGI2ZGE2ZTllNjgzOTg0NTUxZjVkNzdhNzYzZGUAAAAABQAAAGEudHh0AAAA" // TestCross_DeserializeRustMetadata verifies that Go can deserialize metadata // produced by the Rust implementation. diff --git a/verity-go/metadata/interop_test.go b/verity-go/metadata/interop_test.go index b5763767..0f895d41 100644 --- a/verity-go/metadata/interop_test.go +++ b/verity-go/metadata/interop_test.go @@ -41,6 +41,9 @@ func TestInterop_RustProducesGoVerifies(t *testing.T) { dataDir := filepath.Join(dir, "data") metadataPath := filepath.Join(dataDir, "cryptpilot-verity.metadata.fb") + if _, err := os.Stat(metadataPath); os.IsNotExist(err) { + t.Skip("interop data not found — run `make interop-rust-produces` first") + } data, err := os.ReadFile(metadataPath) if err != nil { t.Fatalf("read metadata: %v", err) From fc9dfd6b661b59585969d0de4c9e2e6d4b03d497 Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Tue, 12 May 2026 15:59:12 +0800 Subject: [PATCH 12/18] refactor(test): simplify testing to two clean categories - verity algorithm tests: exec call to make_testfiles.py to generate test files, compare against reference values from digest.rs - Rust/Go interop tests: load file-based metadata fixture generated by Rust CLI, verify Go deserialization and descriptor hashes Changes: - Delete golden_fixtures.json (no longer needed, values inlined) - Delete cross_test.go (merged into interop_test.go) - Add testdata/rust.metadata.fb fixture and gen_fixture.sh - Add fixture staleness check to CI (regenerate + diff) - Add gen-interop-fixture Makefile target - Remove cross_lang_test.sh and cross-test Makefile target --- .github/workflows/test.yml | 12 +- Makefile | 12 +- tests/cross_lang_test.sh | 65 ------- verity-go/metadata/cross_test.go | 159 ---------------- verity-go/metadata/gen_fixture.sh | 19 ++ verity-go/metadata/interop_test.go | 188 ++++++++++--------- verity-go/metadata/testdata/rust.metadata.fb | Bin 0 -> 39640 bytes verity-go/verity/golden_fixtures.json | 44 ----- verity-go/verity/golden_test.go | 103 +++++----- 9 files changed, 183 insertions(+), 419 deletions(-) delete mode 100755 tests/cross_lang_test.sh delete mode 100644 verity-go/metadata/cross_test.go create mode 100755 verity-go/metadata/gen_fixture.sh create mode 100644 verity-go/metadata/testdata/rust.metadata.fb delete mode 100644 verity-go/verity/golden_fixtures.json diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3f62980f..f29f0508 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -82,16 +82,20 @@ jobs: - uses: actions/setup-go@v5 with: go-version: '1.24' + - name: Verify interop fixture is up to date + run: | + python3 verity-core/make_testfiles.py + cargo run -p cryptpilot-verity -- format verity-core/testfiles --hash-output - --label env=prod --force + diff verity-core/testfiles/cryptpilot-verity.metadata.fb verity-go/metadata/testdata/rust.metadata.fb \ + || { echo "ERROR: interop fixture is stale. Run verity-go/metadata/gen_fixture.sh to regenerate."; exit 1; } + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@1.85.0 - name: Run Go tests run: | cd verity-go go test -race -v ./... - name: Check FlatBuffers schemas in sync run: make check-fbs - - name: Run cross-language tests - run: | - cd verity-go - go test -v ./... -run TestCross interop-test: runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index fdb24e2c..92607376 100644 --- a/Makefile +++ b/Makefile @@ -364,14 +364,18 @@ check-fbs: go-test: cd verity-go && /usr/local/go/bin/go test -race -v ./... -skip TestInterop -.PHONY: cross-test -cross-test: - bash tests/cross_lang_test.sh - .PHONY: check-all check-all: clippy go-test check-fbs cargo fmt --check +.PHONY: gen-interop-fixture +gen-interop-fixture: + @echo "=== Generating interop fixture ===" + cd verity-core && python3 make_testfiles.py + cargo run -p cryptpilot-verity -- format verity-core/testfiles --hash-output - --label env=prod --force + cp verity-core/testfiles/cryptpilot-verity.metadata.fb verity-go/metadata/testdata/rust.metadata.fb + @echo "Fixture updated: verity-go/metadata/testdata/rust.metadata.fb" + # Interop tests: Rust CLI produces → Go lib consumes, and vice versa .PHONY: interop-rust-produces diff --git a/tests/cross_lang_test.sh b/tests/cross_lang_test.sh deleted file mode 100755 index 33eb709a..00000000 --- a/tests/cross_lang_test.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash -# tests/cross_lang_test.sh -# Cross-language round-trip test between Rust and Go fs-verity implementations. -# -# Strategy: Each language embeds a binary fixture produced by the other in its -# test code. The Go test deserializes the Rust fixture and vice versa. This -# avoids needing both toolchains in a single process. -# -# What this script does: -# 1. Runs Go cross-language tests (TestCross_*) -# 2. If cargo is available, runs Rust cross-language tests -# 3. Compares descriptor hashes from both sides for consistency -# -# Requires: go (required), cargo (optional, for full coverage) -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -VERITY_GO="$REPO_ROOT/verity-go" -VERITY_RUST="$REPO_ROOT/cryptpilot-verity" - -echo "=== Cross-language Round-Trip Test ===" - -# Check prerequisites -if ! command -v go &>/dev/null; then - echo "SKIP: go not found" - exit 0 -fi - -fail=0 - -# --- Go side --- -echo "" -echo "--- Go cross-language tests ---" -cd "$VERITY_GO" -if go test -v ./metadata/ -run TestCross 2>&1; then - echo " PASS: Go cross-language tests" -else - echo " FAIL: Go cross-language tests" - fail=1 -fi - -# --- Rust side (if available) --- -echo "" -echo "--- Rust cross-language tests ---" -if command -v cargo &>/dev/null; then - cd "$VERITY_RUST" - if cargo test --package cryptpilot-verity -- test_cross --nocapture 2>&1; then - echo " PASS: Rust cross-language tests" - else - echo " FAIL: Rust cross-language tests" - fail=1 - fi -else - echo " SKIP: cargo not found (Rust side not tested)" -fi - -echo "" -if [ "$fail" -eq 0 ]; then - echo "ALL CROSS-LANGUAGE TESTS PASSED" - exit 0 -else - echo "CROSS-LANGUAGE TESTS FAILED" - exit 1 -fi diff --git a/verity-go/metadata/cross_test.go b/verity-go/metadata/cross_test.go deleted file mode 100644 index 5c3f3401..00000000 --- a/verity-go/metadata/cross_test.go +++ /dev/null @@ -1,159 +0,0 @@ -// verity-go/metadata/cross_test.go -// Cross-language round-trip tests. -// -// These tests embed binary data generated by the Rust implementation to verify -// that the Go code can correctly deserialize Rust-produced FlatBuffers, and vice versa. -// -// To update the Rust fixture: -// 1. Run: cargo test --package cryptpilot-verity -- test_cross_serialize --nocapture -// 2. The test prints base64-encoded serialized bytes to stdout -// 3. Paste the base64 string into rustFixtureBase64 below -// -// To update the Go fixture (for the Rust side): -// 1. Run: go test ./metadata/ -v -run TestCross_GoSerialize -// 2. The test prints base64-encoded serialized bytes to stdout -// 3. Paste into the Rust test's go_fixture_base64 constant -package metadata - -import ( - "encoding/base64" - "testing" -) - -// rustFixtureBase64 is a base64-encoded FlatBuffers Metadata binary produced by -// the Rust serialize_metadata() function. It contains: -// - 3 files: "a.txt" (data: "hello"), "b.txt" (data: "world"), "c.txt" (data: "") -// - labels: {"env": "prod", "version": "1.0"} -// -// Generated by: cryptpilot-verity Rust implementation. -// To regenerate, see instructions at top of this file. -const rustFixtureBase64 = "FAAAAAAAAAAAAAoADAAAAAQACAAKAAAAXAAAAAQAAAACAAAAMAAAAAQAAADg////EAAAAAQAAAADAAAAMS4wAAcAAAB2ZXJzaW9uAAgADAAEAAgACAAAABQAAAAEAAAABAAAAHByb2QAAAAAAwAAAGVudgADAAAAqAEAALwAAAAEAAAAcP7//6QAAAAgAAAACAAAAFAAAAAAAAAAEAAQAAUABgAHAAAACAAMABAAAAAAAQEMDAAAAAQAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAADNkMjQ4Y2E1NDJhMjRmYzYyZDFjNDNiOTE2ZWFlNTAxNjg3OGUyNTMzYzg4MjM4NDgwYjI2MTI4YTFmMWFmOTUAAAAABQAAAGMudHh0AAAAJP///8wAAABAAAAACAAAAHgAAAAgAAAAcOOboRAazQtUjZy/uVh7jXtSmPsJaI7QAjI5eqKQ0hwQABgABQAGAAcAEAAIAAwAEAAAAAABAQwUAAAADAAAAAUAAAAAAAAAAAAAACAAAABw45uhEBrNC1SNnL+5WHuNe1KY+wlojtACMjl6opDSHEAAAAAzMTI4ZWViYTNlNDYzYzJmZjUyOWQzYWExYTVjNDg0NDMxMTNiZDAzN2MxNzU5ZjU1YWI4OGJhNWMyZDQ0NWVkAAAAAAUAAABiLnR4dAAAAAwAFAAEAAgADAAQAAwAAADQAAAAQAAAAAgAAAB8AAAAIAAAALFQVsmo23erVwjRm38zD+E9iO6uPQqycQgdNDjA9GJkEAAcAAUABgAHABAACAAMABAAAAAAAQEMGAAAABAAAAAFAAAAAAAAAAAAAAAAAAAAIAAAALFQVsmo23erVwjRm38zD+E9iO6uPQqycQgdNDjA9GJkQAAAADU1NWI1ODljMjZlZTQzYjdhMjUxMGU2YzY3Y2VkOWZiMzE5MGI2ZGE2ZTllNjgzOTg0NTUxZjVkNzdhNzYzZGUAAAAABQAAAGEudHh0AAAA" - -// TestCross_DeserializeRustMetadata verifies that Go can deserialize metadata -// produced by the Rust implementation. -func TestCross_DeserializeRustMetadata(t *testing.T) { - if rustFixtureBase64 == "" { - t.Skip("rustFixtureBase64 not set — run Rust test to generate") - } - - data, err := base64.StdEncoding.DecodeString(rustFixtureBase64) - if err != nil { - t.Fatalf("decode base64: %v", err) - } - - info, err := DeserializeMetadata(data) - if err != nil { - t.Fatalf("deserialize: %v", err) - } - - // Verify expected content - if len(info.FileInfos) != 3 { - t.Errorf("expected 3 files, got %d", len(info.FileInfos)) - } - - // Verify sorted order - expectedPaths := []string{"a.txt", "b.txt", "c.txt"} - for i, fi := range info.FileInfos { - if fi.Path != expectedPaths[i] { - t.Errorf("file[%d].path: expected %q, got %q", i, expectedPaths[i], fi.Path) - } - } - - // Verify labels - if info.Labels["env"] != "prod" { - t.Errorf("labels[env]: expected prod, got %q", info.Labels["env"]) - } - if info.Labels["version"] != "1.0" { - t.Errorf("labels[version]: expected 1.0, got %q", info.Labels["version"]) - } - - // Verify each file's integrity - for i, fi := range info.FileInfos { - if err := fi.VerifySelf(); err != nil { - t.Errorf("file[%d] (%s) verification failed: %v", i, fi.Path, err) - } - } -} - -// TestCross_GoSerialize serializes known test data and prints base64 output. -// The printed base64 should be pasted into the Rust test's go_fixture_base64. -func TestCross_GoSerialize(t *testing.T) { - // Create test file info matching the Rust fixture - fileInfos := []FileVerityInfo{ - makeTestFileVerityInfo("a.txt", []byte("hello")), - makeTestFileVerityInfo("b.txt", []byte("world")), - makeTestFileVerityInfo("c.txt", []byte("")), - } - labels := map[string]string{ - "env": "prod", - "version": "1.0", - } - - serialized, err := SerializeMetadata(fileInfos, labels) - if err != nil { - t.Fatalf("serialize: %v", err) - } - - // Verify round-trip - info, err := DeserializeMetadata(serialized) - if err != nil { - t.Fatalf("deserialize own output: %v", err) - } - - if len(info.FileInfos) != 3 { - t.Errorf("expected 3 files, got %d", len(info.FileInfos)) - } - - // Verify descriptor hashes are consistent - for _, fi := range info.FileInfos { - if err := fi.VerifySelf(); err != nil { - t.Errorf("verify %s: %v", fi.Path, err) - } - } - - // Print base64 for Rust side to consume - t.Logf("Go serialized metadata (base64 for Rust test):\n%s", - base64.StdEncoding.EncodeToString(serialized)) -} - -// TestCross_MetadataHash verifies that the metadata hash calculation produces -// a deterministic result for the same input. -func TestCross_MetadataHash(t *testing.T) { - fileInfos := []FileVerityInfo{ - makeTestFileVerityInfo("a.txt", []byte("hello")), - makeTestFileVerityInfo("b.txt", []byte("world")), - } - labels := map[string]string{"key": "value"} - - serialized, err := SerializeMetadata(fileInfos, labels) - if err != nil { - t.Fatalf("serialize: %v", err) - } - - hash1, err := CalculateMetadataHash(serialized) - if err != nil { - t.Fatalf("hash 1: %v", err) - } - - // Deserialize and re-serialize - info, err := DeserializeMetadata(serialized) - if err != nil { - t.Fatalf("deserialize: %v", err) - } - - serialized2, err := SerializeMetadata(info.FileInfos, info.Labels) - if err != nil { - t.Fatalf("re-serialize: %v", err) - } - - hash2, err := CalculateMetadataHash(serialized2) - if err != nil { - t.Fatalf("hash 2: %v", err) - } - - if hash1 != hash2 { - t.Errorf("hash not deterministic: %s != %s", hash1, hash2) - } - - t.Logf("Metadata hash: %s", hash1) -} diff --git a/verity-go/metadata/gen_fixture.sh b/verity-go/metadata/gen_fixture.sh new file mode 100755 index 00000000..df630890 --- /dev/null +++ b/verity-go/metadata/gen_fixture.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Regenerate the Rust metadata fixture for Go interop tests. +# Run from the repo root: bash verity-go/metadata/gen_fixture.sh +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +echo "=== Generating testfiles ===" +cd "$REPO_ROOT/verity-core" +python3 make_testfiles.py + +echo "=== Running Rust format ===" +cd "$REPO_ROOT" +cargo run -p cryptpilot-verity -- format verity-core/testfiles --hash-output - --label env=prod --force + +echo "=== Copying fixture ===" +cp verity-core/testfiles/cryptpilot-verity.metadata.fb verity-go/metadata/testdata/rust.metadata.fb +echo "Fixture updated: verity-go/metadata/testdata/rust.metadata.fb" diff --git a/verity-go/metadata/interop_test.go b/verity-go/metadata/interop_test.go index 0f895d41..5e87c872 100644 --- a/verity-go/metadata/interop_test.go +++ b/verity-go/metadata/interop_test.go @@ -2,51 +2,68 @@ package metadata import ( + "bytes" "encoding/hex" "os" + "os/exec" "path/filepath" + "runtime" "testing" "cryptpilot-verity-go/verity" ) -func interopDir() string { - dir := os.Getenv("INTEROP_DIR") - if dir == "" { - dir = "/tmp/cryptpilot-interop" - } - return dir +// expectedDescriptorHashes maps test file name to expected SHA-256 descriptor hash. +// Copied from verity-core/src/digest.rs tests (lines 538-551). +var expectedDescriptorHashes = map[string]string{ + "empty": "3d248ca542a24fc62d1c43b916eae5016878e2533c88238480b26128a1f1af95", + "onebyte": "9845e616f7d2f7a1cd6742f0546a36d2e74d4eb8ae7d9bdc0b0df982c27861b7", + "oneblock": "3fd7a78101899a79cd337b1b4e5414be8bcb376b133370156ef6e65026d930ed", + "oneblockplusonebyte": "c0b9455d545b6b1ee5e7b227bd1ed463aaa530a4840dcd93465163a2b3aff0da", + "hashblock_-1_0": "c4b519068d8c8c68fd5e362fc3526c5b11e15f8eb72d4678017906f9e7f2d137", + "hashblock_-1_-1": "7aa0bb537c623562f898386ac88acd319267e4ab3200f3fd1cf648cfdb4a0379", + "hashblock_-1_1": "f804e9777f91d3697ca015303c23251ad3d80205184cfa3d1066ab28cb906330", + "hashblock_0_0": "f5c2b9ded1595acfe8a996795264d488dd6140531f6a01f8f8086a83fd835935", + "hashblock_0_-1": "5c00a54bd1d8341d7bbad060ff1b8e88ed2646d7bb38db6e752cd1cff66c0a78", + "hashblock_0_1": "a7abb76568871169a79104d00679fae6521dfdb2a2648e380c02b10e96e217ff", + "hashblock_1_0": "09510d2dbb55fa16f2768165c42d19c4da43301dfaa05705b2ecb4aaa4a5686a", + "hashblock_1_-1": "26159b4fc68c63881c25c33b23f2583ffaa64fee411af33c3b03238eea56755c", + "hashblock_1_1": "57bed0934bf3ab4610d54938f03cff27bd0d9d76c9a77e283f9fb2b7e29c5ab8", } -// TestInterop_RustProducesGoVerifies reads metadata produced by the Rust -// `format` command and verifies it using Go's DeserializeMetadata + VerifySelf. -// -// Note: The metadata root hash (SHA-256 of FlatBuffers bytes) is NOT compared -// between Go and Rust because the FlatBuffers binary encoding differs across -// language implementations (vtable placement, byte alignment). Instead, we -// verify semantic equivalence: same file count, paths, labels, descriptor -// hashes, and per-file fs-verity calculations. -// -// Expected test data layout (created by `make interop-rust-produces`): -// -// INTEROP_DIR/data/a.txt (content: "hello") -// INTEROP_DIR/data/b.txt (content: "world") -// INTEROP_DIR/data/empty.txt (content: "") -// INTEROP_DIR/data/cryptpilot-verity.metadata.fb (Rust format output) -// INTEROP_DIR/root_hash.txt (root hash from Rust format) -// -// Set INTEROP_DIR env var to the directory, or defaults to /tmp/cryptpilot-interop. -func TestInterop_RustProducesGoVerifies(t *testing.T) { - dir := interopDir() - dataDir := filepath.Join(dir, "data") - - metadataPath := filepath.Join(dataDir, "cryptpilot-verity.metadata.fb") - if _, err := os.Stat(metadataPath); os.IsNotExist(err) { - t.Skip("interop data not found — run `make interop-rust-produces` first") - } - data, err := os.ReadFile(metadataPath) +// expectedPaths is the sorted list of file names from make_testfiles.py. +var expectedPaths = []string{ + "empty", + "hashblock_-1_-1", + "hashblock_-1_0", + "hashblock_-1_1", + "hashblock_0_-1", + "hashblock_0_0", + "hashblock_0_1", + "hashblock_1_-1", + "hashblock_1_0", + "hashblock_1_1", + "oneblock", + "oneblockplusonebyte", + "onebyte", +} + +// fixtureDir returns the directory containing test fixtures. +func fixtureDir() string { + _, filename, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(filename), "testdata") +} + +// TestInterop_DeserializeRustMetadata loads a metadata file generated by the Rust CLI +// and verifies all fields using Go's implementation. +func TestInterop_DeserializeRustMetadata(t *testing.T) { + fbPath := filepath.Join(fixtureDir(), "rust.metadata.fb") + data, err := os.ReadFile(fbPath) if err != nil { - t.Fatalf("read metadata: %v", err) + if os.IsNotExist(err) { + t.Skip("fixture not found — run `make gen-interop-fixture` first") + } + t.Fatalf("read fixture: %v", err) } info, err := DeserializeMetadata(data) @@ -55,12 +72,11 @@ func TestInterop_RustProducesGoVerifies(t *testing.T) { } // Verify file count - if len(info.FileInfos) != 3 { - t.Fatalf("expected 3 files, got %d", len(info.FileInfos)) + if len(info.FileInfos) != len(expectedPaths) { + t.Fatalf("expected %d files, got %d", len(expectedPaths), len(info.FileInfos)) } // Verify sorted paths - expectedPaths := []string{"a.txt", "b.txt", "empty.txt"} for i, fi := range info.FileInfos { if fi.Path != expectedPaths[i] { t.Errorf("file[%d].path: expected %q, got %q", i, expectedPaths[i], fi.Path) @@ -72,25 +88,54 @@ func TestInterop_RustProducesGoVerifies(t *testing.T) { t.Errorf("labels[env]: expected prod, got %q", info.Labels["env"]) } - // Verify each file's integrity (descriptor hash + root hash) - for i, fi := range info.FileInfos { + // Verify descriptor hashes against Rust reference + for _, fi := range info.FileInfos { + expectedHash, ok := expectedDescriptorHashes[fi.Path] + if !ok { + t.Errorf("unexpected file in metadata: %s", fi.Path) + continue + } + if fi.DescriptorHash != expectedHash { + t.Errorf("%s: descriptor hash mismatch\nexpected: %s\ngot: %s", + fi.Path, expectedHash, fi.DescriptorHash) + } if err := fi.VerifySelf(); err != nil { - t.Errorf("file[%d] (%s) verification failed: %v", i, fi.Path, err) + t.Errorf("%s: VerifySelf failed: %v", fi.Path, err) } } +} + +// TestInterop_BlockLevelRecalculation re-reads the original test files from the fixture +// directory (if present) and recalculates descriptor hashes to ensure consistency. +func TestInterop_BlockLevelRecalculation(t *testing.T) { + fbPath := filepath.Join(fixtureDir(), "rust.metadata.fb") + data, err := os.ReadFile(fbPath) + if err != nil { + t.Skip("fixture not found — run `make gen-interop-fixture` first") + } + + info, err := DeserializeMetadata(data) + if err != nil { + t.Fatalf("deserialize: %v", err) + } + + // Check if verity-core/testfiles exists (only in the repo, not in CI testdata) + testfilesDir := findTestfilesDir(t) + if testfilesDir == "" { + t.Skip("testfiles directory not found — this test requires the repo source tree") + } - // Re-read each data file and do block-level verify: recalculate fs-verity - // and compare descriptor hash against what Rust stored. for _, fi := range info.FileInfos { - filePath := filepath.Join(dataDir, fi.Path) + filePath := filepath.Join(testfilesDir, fi.Path) content, err := os.ReadFile(filePath) if err != nil { - t.Fatalf("read file %s: %v", fi.Path, err) + t.Fatalf("read testfile %s: %v", fi.Path, err) } d := verity.NewFsVerity(verity.HashSHA256) d.Write(content) desc, tree := d.Finalize() + recalcHash := hex.EncodeToString(desc.ToDescriptorHash()) if recalcHash != fi.DescriptorHash { t.Errorf("block-level verify failed for %s: expected %s, got %s", @@ -104,53 +149,16 @@ func TestInterop_RustProducesGoVerifies(t *testing.T) { } } -// TestInterop_GoProducesRustVerifies creates test files and metadata, -// then writes them to INTEROP_DIR. The Makefile target `interop-go-produces` -// then runs `cargo run verify` to confirm Rust can consume Go's output. -func TestInterop_GoProducesRustVerifies(t *testing.T) { - dir := interopDir() - dataDir := filepath.Join(dir, "data") - - if err := os.MkdirAll(dataDir, 0755); err != nil { - t.Fatalf("mkdir: %v", err) - } - - testFiles := []struct { - path string - content []byte - }{ - {"hello.txt", []byte("hello")}, - {"world.txt", []byte("world")}, - {"empty.txt", []byte{}}, - } - for _, tf := range testFiles { - if err := os.WriteFile(filepath.Join(dataDir, tf.path), tf.content, 0644); err != nil { - t.Fatalf("write file %s: %v", tf.path, err) - } - } - - // Build FileVerityInfo for each file - fileInfos := make([]FileVerityInfo, len(testFiles)) - for i, tf := range testFiles { - desc, tree := CalculateFsVerityHash(tf.content) - fileInfos[i] = FileVerityInfo{ - Path: tf.path, - Descriptor: desc, - MerkleTree: tree, - DescriptorHash: hex.EncodeToString(desc.ToDescriptorHash()), - } - } - - labels := map[string]string{"env": "prod"} - metadataBytes, err := SerializeMetadata(fileInfos, labels) +// findTestfilesDir looks for verity-core/testfiles relative to the repo root. +func findTestfilesDir(t *testing.T) string { + out, err := exec.Command("git", "rev-parse", "--show-toplevel").CombinedOutput() if err != nil { - t.Fatalf("serialize: %v", err) + return "" } - - metadataPath := filepath.Join(dataDir, "cryptpilot-verity.metadata.fb") - if err := os.WriteFile(metadataPath, metadataBytes, 0644); err != nil { - t.Fatalf("write metadata: %v", err) + repoRoot := string(bytes.TrimSpace(out)) + dir := filepath.Join(repoRoot, "verity-core", "testfiles") + if _, err := os.Stat(dir); err == nil { + return dir } - - t.Logf("Go produced metadata (%d bytes) for Rust verify", len(metadataBytes)) + return "" } diff --git a/verity-go/metadata/testdata/rust.metadata.fb b/verity-go/metadata/testdata/rust.metadata.fb new file mode 100644 index 0000000000000000000000000000000000000000..e6cbf7a8ed38a41bb4921704d98b36c26861efce GIT binary patch literal 39640 zcmeHQeT-E{6`!RGtnDJN2(o@4Pbt}Mg*qR1K5i}9hXi38l*N*|)&SnQGXsqai!6|q zw2=xTZTSPFqSVlu2&FBJf?`Gaa8r<&6q^+r>QV|wj6vk9TN?{#pXa`Xz_Pnt`$waj zoVz)1=FOcqJ9F=ybAI!?XJ&?yE60tb5~Ag#g4_t9Da8pXgD^|4fY0n)1;mG5K35Lm z`~^)80&)Y0pSPF{g0OWxAytnM^5Ff1w9JMwzf8#F&lA!p30Zv$A=^Jh$c}!5yaI*) z3G$c93B_Uy#3@MjKN>;uAWv+$y=BmkD`#vxbLzomUs%@i$K4;8^0UQH|7ykBwqtuw zZn~vmcCkpx$||cMZw@4@@3luBzVuc`0rJEql3pa+J(Wd4Z&yC-9yrtTgSYMlt_|93YtVFqAqo@h?HcI%B@I3 zpe}(T2;9O_TN|6wug`B>)O@AgW=^qq0L~RavUbBGP`Z!3wL3?uNPjW_u3-*-S6RB3 z*?qmz&W;2q?7u9p0Qp34I?;wvX5u)}aq5K7mf6V5h(*>Ln_8ja3J6H2Oe!CgSCVoC zji$czTxV^_+LpBwqNM7+V(~a!^Lmi%+$snQAicE@&%gU~kJksU&%5bGd_M5`z~`f9 z+<^T9`v>+9>>oXiD^Ne6en91d{n}5CGBLk0C_ZzZm%y-8R;5q-Mz0z`+)WV?L*J@;q6B|^BSZZ zUZX1a2w(oj{4>k$&h3_Y!e#mQd^+Q6v zhI$S4TF-h7`v>+9>>t=aa9oJvLL3(&e!Sj~AMpX=1H=b?7ayEZ-_eH-Z5?`X#Hbyc zmThT2z548L>eZI|g@6CcJr{-@p4j@s&b&r(a94kgH%8S>eR9;Wv7dYV#L1VIHw;<1 z*3GS3^4ME<>x=s*RDZZJ<2ATqGPY4jl}VK_hNWDlLRc;Wm*AZNe1?jG_Yq@B5F!

vU+llg zyCLs}yc>>haD4OL#y7pqLwV-;&b&rhX_bqNW#~Ba5WKVMn{OW*es6pA&Rw@H`fAf>+H^7fgZ9=vA5=*9DXFA9(+(TTNMf$xRUjHzt-4MrnJsnT&sV3iFHz8cQ*gsaGS zVQ5OZWz@&YbEZRpBCeZzXKh6@otA$~&qg!l>Z6XK`7h@ZZ;yEAXm!470BLdTJZVBC~{->`HmJ-lMy{hv8- zs`mC|eOjw3_fOiNziIXI-|l&;0C|E;xs9E7;FckiGn?VcnTL8!u6lRxzqlPm=#sXHj?aEtFNUMkXA#Z`W6mcoy(redyM%<0K8*w+{Zp7V)yZa*U zM*D^K3+)%$FSK9X+ppf{kyO3hnYSnwvt4Aa9(jmPJNt`QE;a1UwVrJF=jo|+=g)2Q z{hpaSa`F>5-`2i$=ax|g$Rkrq6B)r4Ef$S>uiI4hRgDB@5%M@>=rL zF<25*sg%MdDNw|9^GN8dY#vDk)DL+Jv!|A?`)oi?|nYFXG{wGJ_JLd7}-w+h&OR-{~T$r2}=p`1!)qBJDJF{)Hzxk+tI z6|DH#mA4pM+OVVr>bDf;g`6MU;R!MZ;>RJ5b_wkg+9kA0XqWn?T|)hh`Wy8(>TlHF zsK3!)2H#8Kdr5pR+3CIHxz4-<>4ukhdU{Rmk~`j-c4FF~;WzD^bZh&|2l+!w4?nxT zX?L4wo;f$;CDO=B1{8yFCWVzbO>oi+#X~A!CP*qRW1h)iyw%)AN*l^FpS6k z#7CzPA0a+MeDwc`k9wQuks}``{mB3_5C;Agq%s@+6H-=IS=B)Wkm9+n{u2#DcVFDG zNdI#4!!O*o?d~H7?%H?Zu6>^xPo}p{8ksw?w!Q$r)v(_w?1?ILs<@CqP?*>VSdk(% zCP|%OF<070i4~mE0IO8Mln+>NGliu$ql5|_LlFerBCgp#1pFsp;zuPJO3E+Sv=Yv5 z1j&PBoJBoIcI4o%qfJ@*)i!1M%S$;resek6V^DxI!f~h;%wT~PL9^XZfJ8|nVPmFz s;w5FuXydtr9Z-yc{ZAr8Eo_~_O~L|fj1rgD-RKYH__^~JJ^ Date: Tue, 12 May 2026 16:14:47 +0800 Subject: [PATCH 13/18] docs(verity-go): add bilingual README and fix CI fixture ordering Add English and Chinese README files documenting package usage and installation. Also fix the CI go-test job to install Rust toolchain before the interop fixture verification step that calls cargo, and add testfiles/ to .gitignore. --- .github/workflows/test.yml | 4 +- .gitignore | 3 ++ verity-go/README.md | 105 +++++++++++++++++++++++++++++++++++++ verity-go/README_CN.md | 102 +++++++++++++++++++++++++++++++++++ 4 files changed, 212 insertions(+), 2 deletions(-) create mode 100644 verity-go/README.md create mode 100644 verity-go/README_CN.md diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f29f0508..8819a0b8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -82,14 +82,14 @@ jobs: - uses: actions/setup-go@v5 with: go-version: '1.24' + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@1.85.0 - name: Verify interop fixture is up to date run: | python3 verity-core/make_testfiles.py cargo run -p cryptpilot-verity -- format verity-core/testfiles --hash-output - --label env=prod --force diff verity-core/testfiles/cryptpilot-verity.metadata.fb verity-go/metadata/testdata/rust.metadata.fb \ || { echo "ERROR: interop fixture is stale. Run verity-go/metadata/gen_fixture.sh to regenerate."; exit 1; } - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@1.85.0 - name: Run Go tests run: | cd verity-go diff --git a/.gitignore b/.gitignore index 3151c7d3..eb844bfd 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ docs/superpowers/ # Claude session artifacts .claude/ + +# Generated test files +testfiles/ diff --git a/verity-go/README.md b/verity-go/README.md new file mode 100644 index 00000000..a96d6768 --- /dev/null +++ b/verity-go/README.md @@ -0,0 +1,105 @@ +# verity-go + +A Go implementation of Linux fs-verity, providing file integrity verification compatible with the kernel's fs-verity mechanism. + +This is the Go counterpart of [cryptpilot-verity](https://github.com/openanolis/cryptpilot), sharing the same FlatBuffers schema via symlinks. + +## Installation + +```bash +go get github.com/openanolis/cryptpilot/verity-go +``` + +Or reference a specific commit: + +```bash +go get github.com/openanolis/cryptpilot/verity-go@master +``` + +## Usage + +### Compute fs-verity digest + +```go +package main + +import ( + "encoding/hex" + "fmt" + + "github.com/openanolis/cryptpilot/verity-go/verity" +) + +func main() { + data := []byte("hello, world") + + d := verity.NewFsVerity(verity.HashSHA256) + d.Write(data) + desc, tree := d.Finalize() + + // The descriptor hash — the fs-verity "measurement" of this file + fmt.Println(hex.EncodeToString(desc.ToDescriptorHash())) + + // Rebuild and verify the Merkle tree root hash + root := tree.RebuildRootHash(desc.Salt, desc.BlockSize()) + fmt.Println(hex.EncodeToString(root)) +} +``` + +### Serialize / deserialize directory metadata + +The `metadata` package serializes a directory tree's integrity data: each file's +fs-verity descriptor, Merkle tree hashes, and a root hash for the entire directory. + +```go +package main + +import ( + "encoding/hex" + "fmt" + "os" + + "github.com/openanolis/cryptpilot/verity-go/metadata" +) + +func main() { + // Compute fs-verity for a file + data, _ := os.ReadFile("somefile.txt") + desc, tree := metadata.CalculateFsVerityHash(data) + + // Build metadata + fileInfos := []metadata.FileVerityInfo{ + { + Path: "somefile.txt", + Descriptor: desc, + MerkleTree: tree, + DescriptorHash: hex.EncodeToString(desc.ToDescriptorHash()), + }, + } + + labels := map[string]string{"env": "prod"} + + // Serialize to FlatBuffers bytes + fb, _ := metadata.SerializeMetadata(fileInfos, labels) + os.WriteFile("metadata.fb", fb, 0644) + + // Deserialize and verify + info, _ := metadata.DeserializeMetadata(fb) + for _, fi := range info.FileInfos { + if err := fi.VerifySelf(); err != nil { + fmt.Printf("verification failed: %v\n", err) + } + } +} +``` + +## Packages + +| Package | Description | +|---------|-------------| +| `verity` | Core fs-verity: streaming hasher, Merkle tree, descriptor | +| `metadata` | FlatBuffers serialization/deserialization of file verity metadata | + +## License + +Same as the parent cryptpilot project. See the root `LICENSE` file. diff --git a/verity-go/README_CN.md b/verity-go/README_CN.md new file mode 100644 index 00000000..4b3c5676 --- /dev/null +++ b/verity-go/README_CN.md @@ -0,0 +1,102 @@ +# verity-go + +Linux fs-verity 的 Go 实现,提供与内核 fs-verity 机制兼容的文件完整性校验。 + +这是 [cryptpilot-verity](https://github.com/openanolis/cryptpilot) Rust 实现的 Go 对应版本,通过 symlink 共享相同的 FlatBuffers schema。 + +## 安装 + +```bash +go get github.com/openanolis/cryptpilot/verity-go +``` + +或者引用特定提交: + +```bash +go get github.com/openanolis/cryptpilot/verity-go@master +``` + +## 使用方法 + +### 计算 fs-verity 摘要 + +```go +package main + +import ( + "encoding/hex" + "fmt" + + "github.com/openanolis/cryptpilot/verity-go/verity" +) + +func main() { + data := []byte("hello, world") + + d := verity.NewFsVerity(verity.HashSHA256) + d.Write(data) + desc, tree := d.Finalize() + + // 文件的 fs-verity "度量值"(descriptor hash) + fmt.Println(hex.EncodeToString(desc.ToDescriptorHash())) + + // 重建并验证 Merkle tree root hash + root := tree.RebuildRootHash(desc.Salt, desc.BlockSize()) + fmt.Println(hex.EncodeToString(root)) +} +``` + +### 序列化 / 反序列化元数据 + +```go +package main + +import ( + "encoding/hex" + "fmt" + "os" + + "github.com/openanolis/cryptpilot/verity-go/metadata" +) + +func main() { + // 计算文件的 fs-verity + data, _ := os.ReadFile("somefile.txt") + desc, tree := metadata.CalculateFsVerityHash(data) + + // 构建元数据 + fileInfos := []metadata.FileVerityInfo{ + { + Path: "somefile.txt", + Descriptor: desc, + MerkleTree: tree, + DescriptorHash: hex.EncodeToString(desc.ToDescriptorHash()), + }, + } + + labels := map[string]string{"env": "prod"} + + // 序列化为 FlatBuffers 字节 + fb, _ := metadata.SerializeMetadata(fileInfos, labels) + os.WriteFile("metadata.fb", fb, 0644) + + // 反序列化并验证 + info, _ := metadata.DeserializeMetadata(fb) + for _, fi := range info.FileInfos { + if err := fi.VerifySelf(); err != nil { + fmt.Printf("验证失败: %v\n", err) + } + } +} +``` + +## 包说明 + +| 包 | 说明 | +|----|------| +| `verity` | 核心 fs-verity:流式 hasher、Merkle tree、descriptor | +| `metadata` | FlatBuffers 序列化/反序列化文件 verity 元数据 | + +## 许可证 + +与父项目 cryptpilot 相同。见根目录下的 `LICENSE` 文件。 From 23ca68cb407481895ad23ba662d4e0b33a4610da Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Tue, 12 May 2026 16:22:50 +0800 Subject: [PATCH 14/18] docs(verity-go): rewrite README with serialize/verify sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split Usage into two parts: (1) Serialize directory metadata with Rust CLI (recommended) and Go implementation options, and (2) Deserialize and verify with three levels — metadata root hash comparison, per-file self-verification, and per-block verification. --- verity-go/README.md | 92 +++++++++++++++++++++++++++--------------- verity-go/README_CN.md | 89 ++++++++++++++++++++++++++-------------- 2 files changed, 117 insertions(+), 64 deletions(-) diff --git a/verity-go/README.md b/verity-go/README.md index a96d6768..f25cf49a 100644 --- a/verity-go/README.md +++ b/verity-go/README.md @@ -18,38 +18,24 @@ go get github.com/openanolis/cryptpilot/verity-go@master ## Usage -### Compute fs-verity digest +### 1. Serialize directory metadata -```go -package main - -import ( - "encoding/hex" - "fmt" - - "github.com/openanolis/cryptpilot/verity-go/verity" -) - -func main() { - data := []byte("hello, world") +The `metadata` package serializes a directory tree's integrity data: each file's +fs-verity descriptor, Merkle tree hashes, and a root hash for the entire directory. - d := verity.NewFsVerity(verity.HashSHA256) - d.Write(data) - desc, tree := d.Finalize() +#### Option A: Rust CLI (recommended) - // The descriptor hash — the fs-verity "measurement" of this file - fmt.Println(hex.EncodeToString(desc.ToDescriptorHash())) +The Rust `cryptpilot-verity` CLI handles the full pipeline — walk the directory, +compute fs-verity for each file, build the Merkle tree, and write the FlatBuffers +metadata file: - // Rebuild and verify the Merkle tree root hash - root := tree.RebuildRootHash(desc.Salt, desc.BlockSize()) - fmt.Println(hex.EncodeToString(root)) -} +```bash +cargo run -p cryptpilot-verity -- format /path/to/dir --label env=prod --hash-output metadata.fb ``` -### Serialize / deserialize directory metadata +#### Option B: Go implementation -The `metadata` package serializes a directory tree's integrity data: each file's -fs-verity descriptor, Merkle tree hashes, and a root hash for the entire directory. +For programmatic control, compute per-file fs-verity and serialize in Go: ```go package main @@ -63,11 +49,9 @@ import ( ) func main() { - // Compute fs-verity for a file data, _ := os.ReadFile("somefile.txt") desc, tree := metadata.CalculateFsVerityHash(data) - // Build metadata fileInfos := []metadata.FileVerityInfo{ { Path: "somefile.txt", @@ -78,16 +62,58 @@ func main() { } labels := map[string]string{"env": "prod"} - - // Serialize to FlatBuffers bytes fb, _ := metadata.SerializeMetadata(fileInfos, labels) os.WriteFile("metadata.fb", fb, 0644) +} +``` + +### 2. Deserialize and verify - // Deserialize and verify - info, _ := metadata.DeserializeMetadata(fb) +Load the metadata bytes and run integrity checks. Three levels of verification +are available: + +```go +package main + +import ( + "encoding/hex" + "fmt" + "os" + + "github.com/openanolis/cryptpilot/verity-go/metadata" +) + +func main() { + data, _ := os.ReadFile("metadata.fb") + info, _ := metadata.DeserializeMetadata(data) + + // (a) Metadata root hash — compare against a trusted reference value + rootHash, _ := metadata.CalculateMetadataHash(data) + expectedRootHash := "..." // trusted hash from build time or signing + if rootHash != expectedRootHash { + panic("metadata root hash mismatch!") + } + + // (b) Self-verification — each file's descriptor hash and Merkle root for _, fi := range info.FileInfos { if err := fi.VerifySelf(); err != nil { - fmt.Printf("verification failed: %v\n", err) + fmt.Printf("%s: verification failed: %v\n", fi.Path, err) + } + } + + // (c) Per-block verification — verify a specific data block against the tree + fi := info.FileInfos[0] + blockSize := fi.Descriptor.BlockSize() + fileData, _ := os.ReadFile(fi.Path) + for blockIndex := 0; blockIndex*blockSize < len(fileData); blockIndex++ { + start := blockIndex * blockSize + end := start + blockSize + if end > len(fileData) { + end = len(fileData) + } + block := fileData[start:end] + if !fi.MerkleTree.VerifyDataBlock(blockIndex, blockSize, block) { + fmt.Printf("%s: block %d is corrupted!\n", fi.Path, blockIndex) } } } @@ -98,7 +124,7 @@ func main() { | Package | Description | |---------|-------------| | `verity` | Core fs-verity: streaming hasher, Merkle tree, descriptor | -| `metadata` | FlatBuffers serialization/deserialization of file verity metadata | +| `metadata` | FlatBuffers serialization/deserialization of directory integrity metadata | ## License diff --git a/verity-go/README_CN.md b/verity-go/README_CN.md index 4b3c5676..7e8e47fc 100644 --- a/verity-go/README_CN.md +++ b/verity-go/README_CN.md @@ -18,35 +18,23 @@ go get github.com/openanolis/cryptpilot/verity-go@master ## 使用方法 -### 计算 fs-verity 摘要 +### 1. 序列化目录元数据 -```go -package main - -import ( - "encoding/hex" - "fmt" - - "github.com/openanolis/cryptpilot/verity-go/verity" -) - -func main() { - data := []byte("hello, world") +`metadata` 包用于将整个目录树的完整性数据序列化:包含每个文件的 +fs-verity 描述符、Merkle 树哈希,以及整个目录的根哈希。 - d := verity.NewFsVerity(verity.HashSHA256) - d.Write(data) - desc, tree := d.Finalize() +#### 方式 A:Rust CLI(推荐) - // 文件的 fs-verity "度量值"(descriptor hash) - fmt.Println(hex.EncodeToString(desc.ToDescriptorHash())) +Rust `cryptpilot-verity` CLI 可自动完成完整流程 — 遍历目录、 +计算每个文件的 fs-verity、构建 Merkle 树、输出 FlatBuffers 元数据文件: - // 重建并验证 Merkle tree root hash - root := tree.RebuildRootHash(desc.Salt, desc.BlockSize()) - fmt.Println(hex.EncodeToString(root)) -} +```bash +cargo run -p cryptpilot-verity -- format /path/to/dir --label env=prod --hash-output metadata.fb ``` -### 序列化 / 反序列化元数据 +#### 方式 B:Go 代码实现 + +如需程序化控制,可在 Go 中逐文件计算 fs-verity 并序列化: ```go package main @@ -60,11 +48,9 @@ import ( ) func main() { - // 计算文件的 fs-verity data, _ := os.ReadFile("somefile.txt") desc, tree := metadata.CalculateFsVerityHash(data) - // 构建元数据 fileInfos := []metadata.FileVerityInfo{ { Path: "somefile.txt", @@ -75,16 +61,57 @@ func main() { } labels := map[string]string{"env": "prod"} - - // 序列化为 FlatBuffers 字节 fb, _ := metadata.SerializeMetadata(fileInfos, labels) os.WriteFile("metadata.fb", fb, 0644) +} +``` + +### 2. 反序列化与验证 + +加载元数据字节后,可执行三级完整性校验: - // 反序列化并验证 - info, _ := metadata.DeserializeMetadata(fb) +```go +package main + +import ( + "encoding/hex" + "fmt" + "os" + + "github.com/openanolis/cryptpilot/verity-go/metadata" +) + +func main() { + data, _ := os.ReadFile("metadata.fb") + info, _ := metadata.DeserializeMetadata(data) + + // (a) 元数据根哈希 — 与构建期或签名时的可信值比对 + rootHash, _ := metadata.CalculateMetadataHash(data) + expectedRootHash := "..." // 构建或签名时记录的可信哈希 + if rootHash != expectedRootHash { + panic("元数据根哈希不匹配!") + } + + // (b) 自验证 — 每个文件的描述符哈希和 Merkle 根哈希一致性 for _, fi := range info.FileInfos { if err := fi.VerifySelf(); err != nil { - fmt.Printf("验证失败: %v\n", err) + fmt.Printf("%s: 验证失败: %v\n", fi.Path, err) + } + } + + // (c) 逐块验证 — 校验文件的指定数据块 + fi := info.FileInfos[0] + blockSize := fi.Descriptor.BlockSize() + fileData, _ := os.ReadFile(fi.Path) + for blockIndex := 0; blockIndex*blockSize < len(fileData); blockIndex++ { + start := blockIndex * blockSize + end := start + blockSize + if end > len(fileData) { + end = len(fileData) + } + block := fileData[start:end] + if !fi.MerkleTree.VerifyDataBlock(blockIndex, blockSize, block) { + fmt.Printf("%s: 块 %d 已被篡改!\n", fi.Path, blockIndex) } } } @@ -95,7 +122,7 @@ func main() { | 包 | 说明 | |----|------| | `verity` | 核心 fs-verity:流式 hasher、Merkle tree、descriptor | -| `metadata` | FlatBuffers 序列化/反序列化文件 verity 元数据 | +| `metadata` | FlatBuffers 序列化/反序列化目录完整性元数据 | ## 许可证 From 60a82c88b7bce91c35a321ecdfe7c2dd497df41e Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Wed, 13 May 2026 09:57:50 +0800 Subject: [PATCH 15/18] refactor(cryptpilot-verity): clean up metadata module and remove cross-lang tests Add #[rustfmt::skip] to generated FlatBuffers imports, remove unused imports, rename chunk_size to CHUNK_SIZE per clippy, and delete the base64-based cross-language tests that are superseded by Go's interop tests. --- cryptpilot-verity/src/metadata/mod.rs | 84 ++++----------------------- 1 file changed, 12 insertions(+), 72 deletions(-) diff --git a/cryptpilot-verity/src/metadata/mod.rs b/cryptpilot-verity/src/metadata/mod.rs index 2529667c..1ca404e4 100644 --- a/cryptpilot-verity/src/metadata/mod.rs +++ b/cryptpilot-verity/src/metadata/mod.rs @@ -1,9 +1,13 @@ // FlatBuffers serialization and deserialization utilities -#![allow(clippy::all)] -#![allow(warnings)] +#[rustfmt::skip] +#[allow(clippy::all)] +#[allow(warnings)] #[allow(unused_imports, dead_code)] mod metadata_generated; +#[rustfmt::skip] +#[allow(clippy::all)] +#[allow(warnings)] #[allow(unused_imports, dead_code)] mod metadata_hash_generated; @@ -16,11 +20,11 @@ pub use metadata_hash_generated::cryptpilot::verity::hash::{ }; use anyhow::{bail, Result}; -use flatbuffers::{FlatBufferBuilder, WIPOffset}; +use flatbuffers::FlatBufferBuilder; use sha2::digest::typenum::Unsigned; use sha2::{digest::OutputSizeUser, Digest, Sha256}; use std::collections::BTreeMap; -use verity_core::digest::{FsVeritySha256, InnerHash}; +use verity_core::digest::FsVeritySha256; use verity_core::tree::MerkleTree; use verity_fuse::file_verifier::file_verity_info::FileVerityInfo; @@ -189,18 +193,18 @@ pub fn deserialize_metadata(data: &[u8]) -> Result { // convert merkle_tree_level1 to Vec> let merkle_tree_level1: Vec> = { - const chunk_size: usize = ::OutputSize::USIZE; - let iter = merkle_tree_level1.chunks_exact(chunk_size); + const CHUNK_SIZE: usize = ::OutputSize::USIZE; + let iter = merkle_tree_level1.chunks_exact(CHUNK_SIZE); if !iter.remainder().is_empty() { bail!( "Broken merkle tree for {}: level 1 length is {} not a multiple of the hash size {}", path, merkle_tree_level1.len(), - chunk_size + CHUNK_SIZE ); } iter.map(|chunk| { - let array: [u8; chunk_size] = chunk.try_into().unwrap(); // this should never fail + let array: [u8; CHUNK_SIZE] = chunk.try_into().unwrap(); // this should never fail From::from(array) }) .collect() @@ -360,68 +364,4 @@ mod tests { assert_eq!(file_infos.len(), deserialized.file_infos.len()); assert!(deserialized.labels.is_empty()); } - - // Cross-language test: deserialize metadata produced by the Go implementation. - // To regenerate the Go fixture, run: - // go test ./metadata/ -v -run TestCross_GoSerialize - // in the verity-go directory and update the base64 string below. - #[test] - fn test_cross_deserialize_go_metadata() { - // Base64-encoded FlatBuffers metadata from Go's SerializeMetadata. - // Contains: a.txt ("hello"), b.txt ("world"), c.txt ("") - // Labels: {"env": "prod", "version": "1.0"} - let go_fixture_b64 = "EAAAAAAACgAMAAAACAAEAAoAAAAIAAAAWAAAAAIAAAAwAAAABAAAAOD///8IAAAADAAAAAMAAAAxLjAABwAAAHZlcnNpb24ACAAMAAgABAAIAAAACAAAABAAAAAEAAAAcHJvZAAAAAADAAAAZW52AAMAAACYAQAAvAAAAAQAAACA/v//XAAAAAwAAAAcAAAAmAAAAAAAAAAQABAADwAOAA0AAAAIAAQAEAAAAAwAAAAMAAAAAAwBAQAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAM2QyNDhjYTU0MmEyNGZjNjJkMWM0M2I5MTZlYWU1MDE2ODc4ZTI1MzNjODgyMzg0ODBiMjYxMjhhMWYxYWY5NQAAAAAFAAAAYy50eHQAAAA0////dAAAAAwAAAAsAAAAsAAAACAAAACw45uhEBrNC1SNnL+5WHuNe1KY+wlojtACMjl6opDSHCj///8UAAAAFAAAAAUAAAAAAAAAAAwBAQAAAAAgAAAAcOOboRAazQtUjZy/uVh7jXtSmPsJaI7QAjI5eqKQ0hxAAAAAMzEyOGVlYmEzZTQ2M2MyZmY1MjlkM2FhMWE1YzQ4NDQzMTEzYmQwMzdjMTc1OWY1NWFiODhiYTVjMmQ0NDVlZAAAAAAFAAAAYi50eHQAAAAMABQAEAAMAAgABAAMAAAAhAAAAAwAAAA8AAAAwAAAACAAAACxUFbJqNt3q1cI0Zt/Mw/hPYjurj0KsnEIHTQ4wPRiZBAAGAAXABYAFQAMAAgABAAQAAAAFAAAABQAAAAFAAAAAAAAAAAMAQEAAAAAIAAAALFQVsmo23erVwjRm38zD+E9iO6uPQqycQgdNDjA9GJkQAAAADU1NWI1ODljMjZlZTQzYjdhMjUxMGU2YzY3Y2VkOWZiMzE5MGI2ZGE2ZTllNjgzOTg0NTUxZjVkNzdhNzYzZGUAAAAABQAAAGEudHh0AAAA"; - - let data = base64::decode(go_fixture_b64).expect("decode base64"); - let info = deserialize_metadata(&data).expect("deserialize Go metadata"); - - assert_eq!(info.file_infos.len(), 3, "expected 3 files"); - assert_eq!(info.file_infos[0].path, "a.txt"); - assert_eq!(info.file_infos[1].path, "b.txt"); - assert_eq!(info.file_infos[2].path, "c.txt"); - assert_eq!(info.labels.get("env"), Some(&"prod".to_string())); - assert_eq!(info.labels.get("version"), Some(&"1.0".to_string())); - - for fi in &info.file_infos { - fi.verify_self().expect(&format!("verify {}", fi.path)); - } - } - - // Cross-language test: serialize metadata and print base64 for Go to consume. - // Run with --nocapture to see the base64 output. - // Paste the output into rustFixtureBase64 in verity-go/metadata/cross_test.go. - #[test] - fn test_cross_serialize_rust_metadata() { - let file_infos: Vec = vec![ - make_test_info("a.txt", b"hello"), - make_test_info("b.txt", b"world"), - make_test_info("c.txt", b""), - ]; - let mut labels = BTreeMap::new(); - labels.insert("env".to_string(), "prod".to_string()); - labels.insert("version".to_string(), "1.0".to_string()); - - let serialized = serialize_metadata(&file_infos, &labels).unwrap(); - let b64 = base64::encode(&serialized); - - println!("Rust serialized metadata (base64 for Go test):"); - println!("{}", b64); - - let info = deserialize_metadata(&serialized).unwrap(); - assert_eq!(info.file_infos.len(), 3); - for fi in &info.file_infos { - fi.verify_self().unwrap(); - } - } - - fn make_test_info(path: &str, data: &[u8]) -> FileVerityInfo { - let (descriptor, merkle_tree) = calculate_fsverity_hash(data); - let descriptor_hash = hex::encode(descriptor.to_descriptor_hash()); - FileVerityInfo { - path: path.to_string(), - descriptor, - merkle_tree, - descriptor_hash, - } - } } From 9e731f5e10a743155a579713e0057516f0b25a73 Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Wed, 13 May 2026 10:00:34 +0800 Subject: [PATCH 16/18] fix(ci): cd into verity-core before running make_testfiles.py The script creates testfiles/ relative to CWD. When run from repo root, files land at testfiles/ instead of verity-core/testfiles/, causing the format command to find 0 files. --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8819a0b8..1f0d3ce7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -86,7 +86,7 @@ jobs: uses: dtolnay/rust-toolchain@1.85.0 - name: Verify interop fixture is up to date run: | - python3 verity-core/make_testfiles.py + (cd verity-core && python3 make_testfiles.py) cargo run -p cryptpilot-verity -- format verity-core/testfiles --hash-output - --label env=prod --force diff verity-core/testfiles/cryptpilot-verity.metadata.fb verity-go/metadata/testdata/rust.metadata.fb \ || { echo "ERROR: interop fixture is stale. Run verity-go/metadata/gen_fixture.sh to regenerate."; exit 1; } From 38d8e88f5d276a5c5374195b8c3789075befacb3 Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Wed, 13 May 2026 10:05:24 +0800 Subject: [PATCH 17/18] ci: remove redundant interop-test job go-test job already covers interop verification: fixture drift detection, per-file hash comparison, and block-level recalculation. --- .github/workflows/test.yml | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1f0d3ce7..ae6b223d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -96,31 +96,3 @@ jobs: go test -race -v ./... - name: Check FlatBuffers schemas in sync run: make check-fbs - - interop-test: - runs-on: ubuntu-latest - container: - image: alibaba-cloud-linux-3-registry.cn-hangzhou.cr.aliyuncs.com/alinux3/alinux3:latest - options: --privileged - steps: - - uses: actions/checkout@v6 - with: - submodules: 'true' - - name: Install dependencies - run: | - sed -i -E 's|https?://mirrors.openanolis.cn/anolis/|https://mirrors.aliyun.com/anolis/|g' /etc/yum.repos.d/*.repo - sed -i -E 's|https?://mirrors.cloud.aliyuncs.com/|https://mirrors.aliyun.com/|g' /etc/yum.repos.d/*.repo - yum update -y - yum install -y git yum-utils device-mapper kmod systemd systemd-udev lvm2 util-linux veritysetup fuse3 fuse3-libs - yum-builddep -y --skip-unavailable ./cryptpilot.spec - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@1.85.0 - - name: Install Go - run: | - curl -fsSL https://go.dev/dl/go1.24.0.linux-amd64.tar.gz | tar -C /usr/local -xz - export PATH=/usr/local/go/bin:$PATH - go version - - name: Run interop tests - run: | - export PATH=/usr/local/go/bin:$PATH - make interop-test From f78773ded741a869c53ac8b354b32cb6de60349f Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Wed, 13 May 2026 10:06:34 +0800 Subject: [PATCH 18/18] ci: remove redundant interop-test Makefile targets interop-rust-produces and interop-go-produces reference Go test functions that no longer exist. Only gen-interop-fixture is kept for manually regenerating the test fixture. --- Makefile | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/Makefile b/Makefile index 92607376..5a8ac348 100644 --- a/Makefile +++ b/Makefile @@ -376,38 +376,3 @@ gen-interop-fixture: cp verity-core/testfiles/cryptpilot-verity.metadata.fb verity-go/metadata/testdata/rust.metadata.fb @echo "Fixture updated: verity-go/metadata/testdata/rust.metadata.fb" -# Interop tests: Rust CLI produces → Go lib consumes, and vice versa - -.PHONY: interop-rust-produces -interop-rust-produces: - @echo "=== Rust produces, Go consumes ===" - @rm -rf /tmp/cryptpilot-interop && mkdir -p /tmp/cryptpilot-interop/data - @printf 'hello' > /tmp/cryptpilot-interop/data/a.txt - @printf 'world' > /tmp/cryptpilot-interop/data/b.txt - @touch /tmp/cryptpilot-interop/data/empty.txt - @cargo run -p cryptpilot-verity --quiet -- format \ - /tmp/cryptpilot-interop/data \ - --hash-output - \ - --label env=prod \ - --force > /tmp/cryptpilot-interop/root_hash.txt - @cd verity-go && INTEROP_DIR=/tmp/cryptpilot-interop \ - /usr/local/go/bin/go test -count=1 -v ./metadata/ -run TestInterop_RustProducesGoVerifies - -.PHONY: interop-go-produces -interop-go-produces: - @echo "=== Go produces, Rust verifies ===" - @rm -rf /tmp/cryptpilot-interop2 && mkdir -p /tmp/cryptpilot-interop2/data - @cd verity-go && INTEROP_DIR=/tmp/cryptpilot-interop2 \ - /usr/local/go/bin/go test -count=1 -v ./metadata/ -run TestInterop_GoProducesRustVerifies - @echo "Running Rust verify..." - @HASH=$$(cargo run -p cryptpilot-verity --quiet -- dump \ - /tmp/cryptpilot-interop2/data --print-root-hash 2>&1 | tail -1) && \ - cargo run -p cryptpilot-verity --quiet -- verify \ - /tmp/cryptpilot-interop2/data "$$HASH" - -.PHONY: interop-test -interop-test: interop-rust-produces interop-go-produces - @rm -rf /tmp/cryptpilot-interop /tmp/cryptpilot-interop2 - @echo "Interop dirs cleaned up" - @echo "=== All interop tests passed ===" -