Files
opentimestamps/timestamp.go
2023-09-19 21:18:40 -03:00

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)
}