~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
184 lines · 4.5 KB Raw History
package repo

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/b64"

	"github.com/bluesky-social/indigo/atproto/atdata"
	"github.com/bluesky-social/indigo/atproto/syntax"
	"github.com/ipfs/go-cid"
	"github.com/multiformats/go-multihash"
)

func marshalDAGCBOR(obj interface{ MarshalCBOR(w io.Writer) error }) ([]byte, cid.Cid, error) {
	var buf bytes.Buffer
	if err := obj.MarshalCBOR(&buf); err != nil {
		return nil, cid.Undef, err
	}
	data := buf.Bytes()
	c, err := cid.NewPrefixV1(cid.DagCBOR, multihash.SHA2_256).Sum(data)
	if err != nil {
		return nil, cid.Undef, err
	}
	return data, c, nil
}

func marshalCommit(commit *Commit) ([]byte, cid.Cid, error) {
	return marshalDAGCBOR(commit)
}

func parseCommitBlock(data []byte) (*Commit, error) {
	var commit Commit
	if err := commit.UnmarshalCBOR(bytes.NewReader(data)); err != nil {
		return nil, fmt.Errorf("parsing commit: %w", err)
	}
	return &commit, nil
}

func prepareRecord(collection syntax.NSID, record map[string]any) ([]byte, cid.Cid, map[string]any, error) {
	if record == nil {
		return nil, cid.Undef, nil, fmt.Errorf("%w: missing record", ErrInvalidRequest)
	}
	raw, err := json.Marshal(b64.NormalizeJSON(record))
	if err != nil {
		return nil, cid.Undef, nil, fmt.Errorf("%w: encoding record: %v", ErrInvalidRequest, err)
	}
	if len(raw) > atdata.MAX_JSON_RECORD_SIZE {
		return nil, cid.Undef, nil, fmt.Errorf("%w: record exceeds maximum size", ErrInvalidRequest)
	}
	obj, err := atdata.UnmarshalJSON(raw)
	if err != nil {
		return nil, cid.Undef, nil, fmt.Errorf("%w: invalid record data: %v", ErrInvalidRequest, err)
	}
	typ, ok := obj["$type"].(string)
	if !ok {
		return nil, cid.Undef, nil, fmt.Errorf("%w: record has no $type", ErrInvalidRequest)
	}
	if typ != collection.String() {
		return nil, cid.Undef, nil, fmt.Errorf("%w: record $type %q does not match collection %q", ErrInvalidRequest, typ, collection)
	}
	data, err := atdata.MarshalCBOR(obj)
	if err != nil {
		return nil, cid.Undef, nil, fmt.Errorf("%w: encoding record: %v", ErrInvalidRequest, err)
	}
	if len(data) > atdata.MAX_CBOR_RECORD_SIZE {
		return nil, cid.Undef, nil, fmt.Errorf("%w: record exceeds maximum size", ErrInvalidRequest)
	}
	c, err := cid.NewPrefixV1(cid.DagCBOR, multihash.SHA2_256).Sum(data)
	if err != nil {
		return nil, cid.Undef, nil, fmt.Errorf("hashing record: %w", err)
	}
	return data, c, obj, nil
}

func decodeRecord(data []byte) (map[string]any, error) {
	return atdata.UnmarshalCBOR(data)
}

func parseCID(raw string) (cid.Cid, error) {
	if raw == "" {
		return cid.Undef, nil
	}
	c, err := cid.Parse(raw)
	if err != nil {
		return cid.Undef, fmt.Errorf("parsing cid %q: %w", raw, err)
	}
	return c, nil
}

func parseCIDPtr(raw string) (*cid.Cid, error) {
	if raw == "" {
		return nil, nil
	}
	c, err := parseCID(raw)
	if err != nil {
		return nil, err
	}
	return &c, nil
}

func formatCIDPtr(c *cid.Cid) string {
	if c == nil {
		return ""
	}
	return c.String()
}

type storedOp struct {
	Action string `json:"action"`
	Path   string `json:"path"`
	CID    string `json:"cid,omitempty"`
	Prev   string `json:"prev,omitempty"`
}

func encodeOps(ops []CommitOp) ([]byte, error) {
	out := make([]storedOp, len(ops))
	for i, op := range ops {
		out[i] = storedOp{
			Action: string(op.Action),
			Path:   op.Path,
			CID:    formatCIDPtr(op.CID),
			Prev:   formatCIDPtr(op.Prev),
		}
	}
	return json.Marshal(out)
}

func encodeCIDs(cids []cid.Cid) ([]byte, error) {
	out := make([]string, len(cids))
	for i, c := range cids {
		out[i] = c.String()
	}
	return json.Marshal(out)
}

func decodeCIDs(data []byte) ([]cid.Cid, error) {
	if len(data) == 0 {
		return nil, nil
	}
	var in []string
	if err := json.Unmarshal(data, &in); err != nil {
		return nil, fmt.Errorf("decoding cids: %w", err)
	}
	out := make([]cid.Cid, 0, len(in))
	for _, raw := range in {
		c, err := parseCID(raw)
		if err != nil {
			return nil, err
		}
		out = append(out, c)
	}
	return out, nil
}

func decodeOps(data []byte) ([]CommitOp, error) {
	if len(data) == 0 {
		return nil, nil
	}
	var in []storedOp
	if err := json.Unmarshal(data, &in); err != nil {
		return nil, fmt.Errorf("decoding ops: %w", err)
	}
	out := make([]CommitOp, 0, len(in))
	for _, op := range in {
		c, err := parseCIDPtr(op.CID)
		if err != nil {
			return nil, err
		}
		p, err := parseCIDPtr(op.Prev)
		if err != nil {
			return nil, err
		}
		out = append(out, CommitOp{
			Action: WriteAction(op.Action),
			Path:   op.Path,
			CID:    c,
			Prev:   p,
		})
	}
	return out, nil
}