184 lines
4.0 KiB
Go
184 lines
4.0 KiB
Go
package opentimestamps
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
)
|
|
|
|
type dumpConfig struct {
|
|
showMessage bool
|
|
showFlat bool
|
|
}
|
|
|
|
var defaultDumpConfig dumpConfig = dumpConfig{
|
|
showMessage: true,
|
|
showFlat: false,
|
|
}
|
|
|
|
// A timestampLink with the opCode being the link edge. The reference
|
|
// implementation uses a map, but the implementation is a bit complex. A list
|
|
// should work as well.
|
|
type tsLink struct {
|
|
opCode opCode
|
|
timestamp *Timestamp
|
|
}
|
|
|
|
// A Timestamp can contain many attestations and operations.
|
|
type Timestamp struct {
|
|
Message []byte
|
|
Attestations []Attestation
|
|
ops []tsLink
|
|
}
|
|
|
|
// Walk calls the passed function f for this timestamp and all
|
|
// downstream timestamps that are chained via operations.
|
|
func (t *Timestamp) Walk(f func(t *Timestamp)) {
|
|
f(t)
|
|
for _, l := range t.ops {
|
|
l.timestamp.Walk(f)
|
|
}
|
|
}
|
|
|
|
func (t *Timestamp) encode(ctx *serializationContext) error {
|
|
n := len(t.Attestations) + len(t.ops)
|
|
if n == 0 {
|
|
return fmt.Errorf("cannot encode empty timestamp")
|
|
}
|
|
prefixAtt := []byte{0x00}
|
|
prefixOp := []byte{}
|
|
nextNode := func(prefix []byte) error {
|
|
n -= 1
|
|
if n > 0 {
|
|
return ctx.writeByte(0xff)
|
|
}
|
|
if len(prefix) > 0 {
|
|
return ctx.writeBytes(prefix)
|
|
}
|
|
return nil
|
|
}
|
|
// FIXME attestations should be sorted
|
|
for _, att := range t.Attestations {
|
|
if err := nextNode(prefixAtt); err != nil {
|
|
return err
|
|
}
|
|
if err := encodeAttestation(ctx, att); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// FIXME ops should be sorted
|
|
for _, op := range t.ops {
|
|
if err := nextNode(prefixOp); err != nil {
|
|
return err
|
|
}
|
|
if err := op.opCode.encode(ctx); err != nil {
|
|
return err
|
|
}
|
|
if err := op.timestamp.encode(ctx); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (t *Timestamp) DumpIndent(w io.Writer, indent int, cfg dumpConfig) {
|
|
if cfg.showMessage {
|
|
fmt.Fprintf(w, strings.Repeat(" ", indent))
|
|
fmt.Fprintf(w, "message %x\n", t.Message)
|
|
}
|
|
for _, att := range t.Attestations {
|
|
fmt.Fprint(w, strings.Repeat(" ", indent))
|
|
fmt.Fprintln(w, att)
|
|
}
|
|
|
|
for _, tsLink := range t.ops {
|
|
fmt.Fprint(w, strings.Repeat(" ", indent))
|
|
fmt.Fprintln(w, tsLink.opCode)
|
|
// fmt.Fprint(w, strings.Repeat(" ", indent))
|
|
// if the timestamp is indeed tree-shaped, show it like that
|
|
if !cfg.showFlat || len(t.ops) > 1 {
|
|
indent += 1
|
|
}
|
|
tsLink.timestamp.DumpIndent(w, indent, cfg)
|
|
}
|
|
}
|
|
|
|
func (t *Timestamp) DumpWithConfig(cfg dumpConfig) string {
|
|
b := &bytes.Buffer{}
|
|
t.DumpIndent(b, 0, cfg)
|
|
return b.String()
|
|
}
|
|
|
|
func (t *Timestamp) Dump() string {
|
|
return t.DumpWithConfig(defaultDumpConfig)
|
|
}
|
|
|
|
func parseTagOrAttestation(ts *Timestamp, ctx *deserializationContext, tag byte, message []byte, limit int) error {
|
|
if tag == 0x00 {
|
|
a, err := ParseAttestation(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ts.Attestations = append(ts.Attestations, a)
|
|
} else {
|
|
op, err := parseOp(ctx, tag)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newMessage, err := op.apply(message)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nextTs := &Timestamp{Message: newMessage}
|
|
err = parse(nextTs, ctx, newMessage, limit-1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ts.ops = append(ts.ops, tsLink{op, nextTs})
|
|
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parse(ts *Timestamp, ctx *deserializationContext, message []byte, limit int) error {
|
|
if limit == 0 {
|
|
return fmt.Errorf("recursion limit")
|
|
}
|
|
var tag byte
|
|
var err error
|
|
for {
|
|
tag, err = ctx.readByte()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tag == 0xff {
|
|
tag, err = ctx.readByte()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err := parseTagOrAttestation(ts, ctx, tag, message, limit)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
return parseTagOrAttestation(ts, ctx, tag, message, limit)
|
|
}
|
|
|
|
func newTimestampFromContext(ctx *deserializationContext, message []byte) (*Timestamp, error) {
|
|
recursionLimit := 1000
|
|
ts := &Timestamp{Message: message}
|
|
err := parse(ts, ctx, message, recursionLimit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ts, nil
|
|
}
|
|
|
|
func NewTimestampFromReader(r io.Reader, message []byte) (*Timestamp, error) {
|
|
return newTimestampFromContext(newDeserializationContext(r), message)
|
|
}
|