nimona/go-nimona

View on GitHub
document_hash.go

Summary

Maintainability
A
0 mins
Test Coverage
package nimona

import (
    "bytes"
    "crypto/sha256"
    "errors"
    "fmt"
    "sort"
    "strings"

    "github.com/mr-tron/base58"
    "github.com/vikyd/zero"

    "nimona.io/tilde"
)

var errDocumentHashValueIsNil = fmt.Errorf("value is nil")

type (
    DocumentHash [32]byte
)

func (h DocumentHash) String() string {
    if h.IsEmpty() {
        return ""
    }
    return base58.Encode(h[:])
}

func (h DocumentHash) IsEqual(other DocumentHash) bool {
    return bytes.Equal(h[:], other[:])
}

func (h DocumentHash) IsEmpty() bool {
    return zero.IsZeroVal(h)
}

func (h DocumentHash) TildeValue() tilde.Value {
    return tilde.Ref(h)
}

func ParseDocumentHash(s string) (DocumentHash, error) {
    var h DocumentHash
    b, err := base58.Decode(s)
    if err != nil {
        return h, err
    }
    copy(h[:], b)
    return h, nil
}

// documentHashRaw hash the given value using sha256, prepending the given type
func documentHashRaw(h tilde.Hint, b []byte) []byte {
    d := sha256.New()
    d.Write([]byte(string(h)))
    d.Write(b)
    return d.Sum(nil)
}

func NewDocumentHash(dm *Document) (h DocumentHash) {
    x, err := documentHashMap(dm.Map())
    if err != nil {
        panic(fmt.Errorf("error hashing map: %w", err))
    }
    copy(h[:], x)
    return
}

// nolint: gocyclo
func documentHashAny(valueAny tilde.Value) (h []byte, err error) {
    switch value := valueAny.(type) {
    case tilde.Ref:
        return value[:], nil
    case tilde.Uint64:
        return documentHashRaw(tilde.HintUint64, []byte(fmt.Sprintf("%d", value))), nil
    case tilde.Int64:
        return documentHashRaw(tilde.HintInt64, []byte(fmt.Sprintf("%d", value))), nil
    case tilde.Bytes:
        return documentHashRaw(tilde.HintBytes, value), nil
    case tilde.String:
        return documentHashRaw(tilde.HintString, []byte(value)), nil
    case tilde.Bool:
        if value {
            return documentHashRaw(tilde.HintBool, []byte{1}), nil
        }
        return documentHashRaw(tilde.HintBool, []byte{0}), nil
    case tilde.Map:
        if len(value) == 0 {
            return h, errDocumentHashValueIsNil
        }
        return documentHashMap(value)
    case tilde.List:
        if len(value) == 0 {
            return h, errDocumentHashValueIsNil
        }
        hr := new(bytes.Buffer)
        for _, value := range value {
            if value == nil {
                continue
            }
            hh, err := documentHashAny(value)
            if errors.Is(err, errDocumentHashValueIsNil) {
                continue
            }
            if err != nil {
                return h, fmt.Errorf("error hashing list value: %w", err)
            }
            hr.Write(hh)
        }
        return documentHashRaw(tilde.HintList, hr.Bytes()), nil
    default:
        panic(fmt.Errorf("unhandled type: %T", valueAny))
    }
}

type DocumentHashEntry struct {
    k     string
    khash []byte
    vhash []byte
}

type byKDocumentHash []DocumentHashEntry

func (h byKDocumentHash) Len() int      { return len(h) }
func (h byKDocumentHash) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h byKDocumentHash) Less(i, j int) bool {
    return bytes.Compare(h[i].khash, h[j].khash) < 0
}

func documentHashMap(m tilde.Map) (h []byte, err error) {
    e := byKDocumentHash{}
    for key, value := range m {
        // skip ephemeral fields
        if strings.HasPrefix(key, "_") {
            continue
        }
        // skip zero values
        // TODO Reconsider if we should be skipping all zero values
        if zero.IsZeroVal(value) {
            continue
        }
        // hash the value
        hh, err := documentHashAny(value)
        if errors.Is(err, errDocumentHashValueIsNil) {
            continue
        }
        if err != nil {
            return h, fmt.Errorf("error hashing value of key %s: %w", key, err)
        }
        e = append(e, DocumentHashEntry{
            k:     key,
            khash: documentHashRaw(tilde.HintString, []byte(key)),
            vhash: hh,
        })
    }

    sort.Sort(e)
    hr := new(bytes.Buffer)
    for _, ee := range e {
        hr.Write(ee.khash)
        hr.Write(ee.vhash)
    }
    return documentHashRaw(tilde.HintMap, hr.Bytes()), nil
}