Skip to content

Commit

Permalink
capnp: add Canonicalize function
Browse files Browse the repository at this point in the history
Fixes #92
  • Loading branch information
zombiezen committed Aug 15, 2017
1 parent bae255b commit ece6d30
Show file tree
Hide file tree
Showing 2 changed files with 353 additions and 0 deletions.
161 changes: 161 additions & 0 deletions canonical.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package capnp

import (
"errors"
"fmt"
)

// Canonicalize encodes a struct into its canonical form: a single-
// segment blob without a segment table. The result will be identical
// for equivalent structs, even as the schema evolves. The blob is
// suitable for hashing or signing.
func Canonicalize(s Struct) ([]byte, error) {
msg, seg, _ := NewMessage(SingleSegment(nil))
if !s.IsValid() {
return seg.Data(), nil
}
root, err := NewRootStruct(seg, canonicalStructSize(s))
if err != nil {
return nil, fmt.Errorf("canonicalize: %v", err)
}
if err := msg.SetRootPtr(root.ToPtr()); err != nil {
return nil, fmt.Errorf("canonicalize: %v", err)
}
if err := fillCanonicalStruct(root, s); err != nil {
return nil, fmt.Errorf("canonicalize: %v", err)
}
return seg.Data(), nil
}

func canonicalPtr(dst *Segment, p Ptr) (Ptr, error) {
if !p.IsValid() {
return Ptr{}, nil
}
switch p.flags.ptrType() {
case structPtrType:
ss, err := NewStruct(dst, canonicalStructSize(p.Struct()))
if err != nil {
return Ptr{}, err
}
if err := fillCanonicalStruct(ss, p.Struct()); err != nil {
return Ptr{}, err
}
return ss.ToPtr(), nil
case listPtrType:
ll, err := canonicalList(dst, p.List())
if err != nil {
return Ptr{}, err
}
return ll.ToPtr(), nil
case interfacePtrType:
return Ptr{}, errors.New("cannot canonicalize interface")
default:
panic("unreachable")
}
}

func fillCanonicalStruct(dst, s Struct) error {
copy(dst.seg.slice(dst.off, dst.size.DataSize), s.seg.slice(s.off, s.size.DataSize))
for i := uint16(0); i < dst.size.PointerCount; i++ {
p, err := s.Ptr(i)
if err != nil {
return fmt.Errorf("pointer %d: %v", i, err)
}
cp, err := canonicalPtr(dst.seg, p)
if err != nil {
return fmt.Errorf("pointer %d: %v", i, err)
}
if err := dst.SetPtr(i, cp); err != nil {
return fmt.Errorf("pointer %d: %v", i, err)
}
}
return nil
}

func canonicalStructSize(s Struct) ObjectSize {
if !s.IsValid() {
return ObjectSize{}
}
var sz ObjectSize
// int32 will not overflow because max struct data size is 2^16 words.
for off := int32(s.size.DataSize &^ (wordSize - 1)); off >= 0; off -= int32(wordSize) {
if s.Uint64(DataOffset(off)) != 0 {
sz.DataSize = Size(off) + wordSize
break
}
}
for i := int32(s.size.PointerCount) - 1; i >= 0; i-- {
if s.seg.readRawPointer(s.pointerAddress(uint16(i))) != 0 {
sz.PointerCount = uint16(i + 1)
break
}
}
return sz
}

func canonicalList(dst *Segment, l List) (List, error) {
if !l.IsValid() {
return List{}, nil
}
if l.size.PointerCount == 0 {
// Data only, just copy over.
sz := l.allocSize()
_, newAddr, err := alloc(dst, sz)
if err != nil {
return List{}, err
}
cl := List{
seg: dst,
off: newAddr,
length: l.length,
size: l.size,
flags: l.flags,
depthLimit: maxDepth,
}
end, _ := l.off.addSize(sz) // list was already validated
copy(dst.data[newAddr:], l.seg.data[l.off:end])
return cl, nil
}
if l.flags&isCompositeList == 0 {
cl, err := NewPointerList(dst, l.length)
if err != nil {
return List{}, err
}
for i := 0; i < l.Len(); i++ {
p, err := PointerList{l}.PtrAt(i)
if err != nil {
return List{}, fmt.Errorf("element %d: %v", i, err)
}
cp, err := canonicalPtr(dst, p)
if err != nil {
return List{}, fmt.Errorf("element %d: %v", i, err)
}
if err := cl.SetPtr(i, cp); err != nil {
return List{}, fmt.Errorf("element %d: %v", i, err)
}
}
return cl.List, nil
}

// Struct/composite list
var elemSize ObjectSize
for i := 0; i < l.Len(); i++ {
sz := canonicalStructSize(l.Struct(i))
if sz.DataSize > elemSize.DataSize {
elemSize.DataSize = sz.DataSize
}
if sz.PointerCount > elemSize.PointerCount {
elemSize.PointerCount = sz.PointerCount
}
}
cl, err := NewCompositeList(dst, elemSize, l.length)
if err != nil {
return List{}, err
}
for i := 0; i < cl.Len(); i++ {
if err := fillCanonicalStruct(cl.Struct(i), l.Struct(i)); err != nil {
return List{}, fmt.Errorf("element %d: %v", i, err)
}
}
return cl, nil
}
192 changes: 192 additions & 0 deletions canonical_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package capnp

import (
"bytes"
"encoding/hex"
"testing"
)

func TestCanonicalize(t *testing.T) {
{
// null
b, err := Canonicalize(Struct{})
if err != nil {
t.Fatal("Canonicalize(Struct{}):", err)
}
if want := ([]byte{0, 0, 0, 0, 0, 0, 0, 0}); !bytes.Equal(b, want) {
t.Errorf("Canonicalize(Struct{}) =\n%s\n; want\n%s", hex.Dump(b), hex.Dump(want))
}
}
{
// empty struct
_, seg, _ := NewMessage(SingleSegment(nil))
s, _ := NewStruct(seg, ObjectSize{})
b, err := Canonicalize(s)
if err != nil {
t.Fatal("Canonicalize(empty struct):", err)
}
want := ([]byte{0xfc, 0xff, 0xff, 0xff, 0, 0, 0, 0})
if !bytes.Equal(b, want) {
t.Errorf("Canonicalize(empty struct) =\n%s\n; want\n%s", hex.Dump(b), hex.Dump(want))
}
}
{
// zero data, zero pointer struct
_, seg, _ := NewMessage(SingleSegment(nil))
s, _ := NewStruct(seg, ObjectSize{DataSize: 8, PointerCount: 1})
b, err := Canonicalize(s)
if err != nil {
t.Fatal("Canonicalize(zero data, zero pointer struct):", err)
}
want := ([]byte{0xfc, 0xff, 0xff, 0xff, 0, 0, 0, 0})
if !bytes.Equal(b, want) {
t.Errorf("Canonicalize(zero data, zero pointer struct) =\n%s\n; want\n%s", hex.Dump(b), hex.Dump(want))
}
}
{
// one word data struct
_, seg, _ := NewMessage(SingleSegment(nil))
s, _ := NewStruct(seg, ObjectSize{DataSize: 8, PointerCount: 1})
s.SetUint16(0, 0xbeef)
b, err := Canonicalize(s)
if err != nil {
t.Fatal("Canonicalize(one word data struct):", err)
}
want := ([]byte{
0, 0, 0, 0, 1, 0, 0, 0,
0xef, 0xbe, 0, 0, 0, 0, 0, 0,
})
if !bytes.Equal(b, want) {
t.Errorf("Canonicalize(one word data struct) =\n%s\n; want\n%s", hex.Dump(b), hex.Dump(want))
}
}
{
// two pointers to zero structs
_, seg, _ := NewMessage(SingleSegment(nil))
s, _ := NewStruct(seg, ObjectSize{PointerCount: 2})
e1, _ := NewStruct(seg, ObjectSize{DataSize: 8})
e2, _ := NewStruct(seg, ObjectSize{DataSize: 8})
s.SetPtr(0, e1.ToPtr())
s.SetPtr(1, e2.ToPtr())
b, err := Canonicalize(s)
if err != nil {
t.Fatal("Canonicalize(two pointers to zero structs):", err)
}
want := ([]byte{
0, 0, 0, 0, 0, 0, 2, 0,
0xfc, 0xff, 0xff, 0xff, 0, 0, 0, 0,
0xfc, 0xff, 0xff, 0xff, 0, 0, 0, 0,
})
if !bytes.Equal(b, want) {
t.Errorf("Canonicalize(two pointers to zero structs) =\n%s\n; want\n%s", hex.Dump(b), hex.Dump(want))
}
}
{
// int list
_, seg, _ := NewMessage(SingleSegment(nil))
s, _ := NewStruct(seg, ObjectSize{PointerCount: 1})
l, _ := NewInt8List(seg, 5)
s.SetPtr(0, l.ToPtr())
l.Set(0, 1)
l.Set(1, 2)
l.Set(2, 3)
l.Set(3, 4)
l.Set(4, 5)
b, err := Canonicalize(s)
if err != nil {
t.Fatal("Canonicalize(int list):", err)
}
want := ([]byte{
0, 0, 0, 0, 0, 0, 1, 0,
0x01, 0, 0, 0, 0x2a, 0, 0, 0,
1, 2, 3, 4, 5, 0, 0, 0,
})
if !bytes.Equal(b, want) {
t.Errorf("Canonicalize(int list) =\n%s\n; want\n%s", hex.Dump(b), hex.Dump(want))
}
}
{
// zero int list
_, seg, _ := NewMessage(SingleSegment(nil))
s, _ := NewStruct(seg, ObjectSize{PointerCount: 1})
l, _ := NewInt8List(seg, 5)
s.SetPtr(0, l.ToPtr())
b, err := Canonicalize(s)
if err != nil {
t.Fatal("Canonicalize(zero int list):", err)
}
want := ([]byte{
0, 0, 0, 0, 0, 0, 1, 0,
0x01, 0, 0, 0, 0x2a, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
})
if !bytes.Equal(b, want) {
t.Errorf("Canonicalize(zero int list) =\n%s\n; want\n%s", hex.Dump(b), hex.Dump(want))
}
}
{
// struct list
_, seg, _ := NewMessage(SingleSegment(nil))
s, _ := NewStruct(seg, ObjectSize{PointerCount: 1})
l, _ := NewCompositeList(seg, ObjectSize{DataSize: 8, PointerCount: 1}, 2)
s.SetPtr(0, l.ToPtr())
l.Struct(0).SetUint64(0, 0xdeadbeef)
txt, _ := NewText(seg, "xyzzy")
l.Struct(1).SetPtr(0, txt.ToPtr())
b, err := Canonicalize(s)
if err != nil {
t.Fatal("Canonicalize(struct list):", err)
}
want := ([]byte{
0, 0, 0, 0, 0, 0, 1, 0,
0x01, 0, 0, 0, 0x27, 0, 0, 0,
0x08, 0, 0, 0, 1, 0, 1, 0,
0xef, 0xbe, 0xad, 0xde, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0x01, 0, 0, 0, 0x32, 0, 0, 0,
'x', 'y', 'z', 'z', 'y', 0, 0, 0,
})
if !bytes.Equal(b, want) {
t.Errorf("Canonicalize(struct list) =\n%s\n; want\n%s", hex.Dump(b), hex.Dump(want))
}
}
{
// zero struct list
_, seg, _ := NewMessage(SingleSegment(nil))
s, _ := NewStruct(seg, ObjectSize{PointerCount: 1})
l, _ := NewCompositeList(seg, ObjectSize{DataSize: 16, PointerCount: 2}, 3)
s.SetPtr(0, l.ToPtr())
b, err := Canonicalize(s)
if err != nil {
t.Fatal("Canonicalize(zero struct list):", err)
}
want := ([]byte{
0, 0, 0, 0, 0, 0, 1, 0,
0x01, 0, 0, 0, 0x07, 0, 0, 0,
0x0c, 0, 0, 0, 0, 0, 0, 0,
})
if !bytes.Equal(b, want) {
t.Errorf("Canonicalize(zero struct list) =\n%s\n; want\n%s", hex.Dump(b), hex.Dump(want))
}
}
{
// zero-length struct list
_, seg, _ := NewMessage(SingleSegment(nil))
s, _ := NewStruct(seg, ObjectSize{PointerCount: 1})
l, _ := NewCompositeList(seg, ObjectSize{DataSize: 16, PointerCount: 2}, 0)
s.SetPtr(0, l.ToPtr())
b, err := Canonicalize(s)
if err != nil {
t.Fatal("Canonicalize(zero-length struct list):", err)
}
want := ([]byte{
0, 0, 0, 0, 0, 0, 1, 0,
0x01, 0, 0, 0, 0x07, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
})
if !bytes.Equal(b, want) {
t.Errorf("Canonicalize(zero-length struct list) =\n%s\n; want\n%s", hex.Dump(b), hex.Dump(want))
}
}
}

0 comments on commit ece6d30

Please sign in to comment.