Major integrations and fixes: - Added BACKBEAT SDK integration for P2P operation timing - Implemented beat-aware status tracking for distributed operations - Added Docker secrets support for secure license management - Resolved KACHING license validation via HTTPS/TLS - Updated docker-compose configuration for clean stack deployment - Disabled rollback policies to prevent deployment failures - Added license credential storage (CHORUS-DEV-MULTI-001) Technical improvements: - BACKBEAT P2P operation tracking with phase management - Enhanced configuration system with file-based secrets - Improved error handling for license validation - Clean separation of KACHING and CHORUS deployment stacks 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
308 lines
9.4 KiB
Go
308 lines
9.4 KiB
Go
package dagcbor
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
|
|
cid "github.com/ipfs/go-cid"
|
|
"github.com/polydawn/refmt/cbor"
|
|
"github.com/polydawn/refmt/shared"
|
|
"github.com/polydawn/refmt/tok"
|
|
|
|
"github.com/ipld/go-ipld-prime/datamodel"
|
|
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
|
|
"github.com/ipld/go-ipld-prime/node/basicnode"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidMultibase = errors.New("invalid multibase on IPLD link")
|
|
ErrAllocationBudgetExceeded = errors.New("message structure demanded too many resources to process")
|
|
ErrTrailingBytes = errors.New("unexpected content after end of cbor object")
|
|
)
|
|
|
|
const (
|
|
mapEntryGasScore = 8
|
|
listEntryGasScore = 4
|
|
)
|
|
|
|
// This file should be identical to the general feature in the parent package,
|
|
// except for the `case tok.TBytes` block,
|
|
// which has dag-cbor's special sauce for detecting schemafree links.
|
|
|
|
// DecodeOptions can be used to customize the behavior of a decoding function.
|
|
// The Decode method on this struct fits the codec.Decoder function interface.
|
|
type DecodeOptions struct {
|
|
// If true, parse DAG-CBOR tag(42) as Link nodes, otherwise reject them
|
|
AllowLinks bool
|
|
|
|
// TODO: ExperimentalDeterminism enforces map key order, but not the other parts
|
|
// of the spec such as integers or floats. See the fuzz failures spotted in
|
|
// https://github.com/ipld/go-ipld-prime/pull/389.
|
|
// When we're done implementing strictness, deprecate the option in favor of
|
|
// StrictDeterminism, but keep accepting both for backwards compatibility.
|
|
|
|
// ExperimentalDeterminism requires decoded DAG-CBOR bytes to be canonical as per
|
|
// the spec. For example, this means that integers and floats be encoded in
|
|
// a particular way, and map keys be sorted.
|
|
//
|
|
// The decoder does not enforce this requirement by default, as the codec
|
|
// was originally implemented without these rules. Because of that, there's
|
|
// a significant amount of published data that isn't canonical but should
|
|
// still decode with the default settings for backwards compatibility.
|
|
//
|
|
// Note that this option is experimental as it only implements partial strictness.
|
|
ExperimentalDeterminism bool
|
|
|
|
// If true, the decoder stops reading from the stream at the end of a full,
|
|
// valid CBOR object. This may be useful for parsing a stream of undelimited
|
|
// CBOR objects.
|
|
// As per standard IPLD behavior, in the default mode the parser considers the
|
|
// entire block to be part of the CBOR object and will error if there is
|
|
// extraneous data after the end of the object.
|
|
DontParseBeyondEnd bool
|
|
}
|
|
|
|
// Decode deserializes data from the given io.Reader and feeds it into the given datamodel.NodeAssembler.
|
|
// Decode fits the codec.Decoder function interface.
|
|
//
|
|
// The behavior of the decoder can be customized by setting fields in the DecodeOptions struct before calling this method.
|
|
func (cfg DecodeOptions) Decode(na datamodel.NodeAssembler, r io.Reader) error {
|
|
// Probe for a builtin fast path. Shortcut to that if possible.
|
|
type detectFastPath interface {
|
|
DecodeDagCbor(io.Reader) error
|
|
}
|
|
if na2, ok := na.(detectFastPath); ok {
|
|
return na2.DecodeDagCbor(r)
|
|
}
|
|
// Okay, generic builder path.
|
|
err := Unmarshal(na, cbor.NewDecoder(cbor.DecodeOptions{
|
|
CoerceUndefToNull: true,
|
|
}, r), cfg)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if cfg.DontParseBeyondEnd {
|
|
return nil
|
|
}
|
|
|
|
var buf [1]byte
|
|
_, err = io.ReadFull(r, buf[:])
|
|
switch err {
|
|
case io.EOF:
|
|
return nil
|
|
case nil:
|
|
return ErrTrailingBytes
|
|
default:
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Future work: we would like to remove the Unmarshal function,
|
|
// and in particular, stop seeing types from refmt (like shared.TokenSource) be visible.
|
|
// Right now, some kinds of configuration (e.g. for whitespace and prettyprint) are only available through interacting with the refmt types;
|
|
// we should improve our API so that this can be done with only our own types in this package.
|
|
|
|
// Unmarshal is a deprecated function.
|
|
// Please consider switching to DecodeOptions.Decode instead.
|
|
func Unmarshal(na datamodel.NodeAssembler, tokSrc shared.TokenSource, options DecodeOptions) error {
|
|
// Have a gas budget, which will be decremented as we allocate memory, and an error returned when execeeded (or about to be exceeded).
|
|
// This is a DoS defense mechanism.
|
|
// It's *roughly* in units of bytes (but only very, VERY roughly) -- it also treats words as 1 in many cases.
|
|
// FUTURE: this ought be configurable somehow. (How, and at what granularity though?)
|
|
var gas int64 = 1048576 * 10
|
|
return unmarshal1(na, tokSrc, &gas, options)
|
|
}
|
|
|
|
func unmarshal1(na datamodel.NodeAssembler, tokSrc shared.TokenSource, gas *int64, options DecodeOptions) error {
|
|
var tk tok.Token
|
|
done, err := tokSrc.Step(&tk)
|
|
if err == io.EOF {
|
|
return io.ErrUnexpectedEOF
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if done && !tk.Type.IsValue() && tk.Type != tok.TNull {
|
|
return fmt.Errorf("unexpected eof")
|
|
}
|
|
return unmarshal2(na, tokSrc, &tk, gas, options)
|
|
}
|
|
|
|
// starts with the first token already primed. Necessary to get recursion
|
|
//
|
|
// to flow right without a peek+unpeek system.
|
|
func unmarshal2(na datamodel.NodeAssembler, tokSrc shared.TokenSource, tk *tok.Token, gas *int64, options DecodeOptions) error {
|
|
// FUTURE: check for schema.TypedNodeBuilder that's going to parse a Link (they can slurp any token kind they want).
|
|
switch tk.Type {
|
|
case tok.TMapOpen:
|
|
expectLen := int64(tk.Length)
|
|
allocLen := int64(tk.Length)
|
|
if tk.Length == -1 {
|
|
expectLen = math.MaxInt64
|
|
allocLen = 0
|
|
} else {
|
|
if *gas-allocLen < 0 { // halt early if this will clearly demand too many resources
|
|
return ErrAllocationBudgetExceeded
|
|
}
|
|
}
|
|
ma, err := na.BeginMap(allocLen)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var observedLen int64
|
|
lastKey := ""
|
|
for {
|
|
_, err := tokSrc.Step(tk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch tk.Type {
|
|
case tok.TMapClose:
|
|
if expectLen != math.MaxInt64 && observedLen != expectLen {
|
|
return fmt.Errorf("unexpected mapClose before declared length")
|
|
}
|
|
return ma.Finish()
|
|
case tok.TString:
|
|
*gas -= int64(len(tk.Str) + mapEntryGasScore)
|
|
if *gas < 0 {
|
|
return ErrAllocationBudgetExceeded
|
|
}
|
|
// continue
|
|
default:
|
|
return fmt.Errorf("unexpected %s token while expecting map key", tk.Type)
|
|
}
|
|
observedLen++
|
|
if observedLen > expectLen {
|
|
return fmt.Errorf("unexpected continuation of map elements beyond declared length")
|
|
}
|
|
if observedLen > 1 && options.ExperimentalDeterminism {
|
|
if len(lastKey) > len(tk.Str) || lastKey > tk.Str {
|
|
return fmt.Errorf("map key %q is not after %q as per RFC7049", tk.Str, lastKey)
|
|
}
|
|
}
|
|
lastKey = tk.Str
|
|
mva, err := ma.AssembleEntry(tk.Str)
|
|
if err != nil { // return in error if the key was rejected
|
|
return err
|
|
}
|
|
err = unmarshal1(mva, tokSrc, gas, options)
|
|
if err != nil { // return in error if some part of the recursion errored
|
|
return err
|
|
}
|
|
}
|
|
case tok.TMapClose:
|
|
return fmt.Errorf("unexpected mapClose token")
|
|
case tok.TArrOpen:
|
|
expectLen := int64(tk.Length)
|
|
allocLen := int64(tk.Length)
|
|
if tk.Length == -1 {
|
|
expectLen = math.MaxInt64
|
|
allocLen = 0
|
|
} else {
|
|
if *gas-allocLen < 0 { // halt early if this will clearly demand too many resources
|
|
return ErrAllocationBudgetExceeded
|
|
}
|
|
}
|
|
la, err := na.BeginList(allocLen)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var observedLen int64
|
|
for {
|
|
_, err := tokSrc.Step(tk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch tk.Type {
|
|
case tok.TArrClose:
|
|
if expectLen != math.MaxInt64 && observedLen != expectLen {
|
|
return fmt.Errorf("unexpected arrClose before declared length")
|
|
}
|
|
return la.Finish()
|
|
default:
|
|
*gas -= listEntryGasScore
|
|
if *gas < 0 {
|
|
return ErrAllocationBudgetExceeded
|
|
}
|
|
observedLen++
|
|
if observedLen > expectLen {
|
|
return fmt.Errorf("unexpected continuation of array elements beyond declared length")
|
|
}
|
|
err := unmarshal2(la.AssembleValue(), tokSrc, tk, gas, options)
|
|
if err != nil { // return in error if some part of the recursion errored
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
case tok.TArrClose:
|
|
return fmt.Errorf("unexpected arrClose token")
|
|
case tok.TNull:
|
|
return na.AssignNull()
|
|
case tok.TString:
|
|
*gas -= int64(len(tk.Str))
|
|
if *gas < 0 {
|
|
return ErrAllocationBudgetExceeded
|
|
}
|
|
return na.AssignString(tk.Str)
|
|
case tok.TBytes:
|
|
*gas -= int64(len(tk.Bytes))
|
|
if *gas < 0 {
|
|
return ErrAllocationBudgetExceeded
|
|
}
|
|
if !tk.Tagged {
|
|
return na.AssignBytes(tk.Bytes)
|
|
}
|
|
switch tk.Tag {
|
|
case linkTag:
|
|
if !options.AllowLinks {
|
|
return fmt.Errorf("unhandled cbor tag %d", tk.Tag)
|
|
}
|
|
if len(tk.Bytes) < 1 || tk.Bytes[0] != 0 {
|
|
return ErrInvalidMultibase
|
|
}
|
|
elCid, err := cid.Cast(tk.Bytes[1:])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return na.AssignLink(cidlink.Link{Cid: elCid})
|
|
default:
|
|
return fmt.Errorf("unhandled cbor tag %d", tk.Tag)
|
|
}
|
|
case tok.TBool:
|
|
*gas -= 1
|
|
if *gas < 0 {
|
|
return ErrAllocationBudgetExceeded
|
|
}
|
|
return na.AssignBool(tk.Bool)
|
|
case tok.TInt:
|
|
*gas -= 1
|
|
if *gas < 0 {
|
|
return ErrAllocationBudgetExceeded
|
|
}
|
|
return na.AssignInt(tk.Int)
|
|
case tok.TUint:
|
|
*gas -= 1
|
|
if *gas < 0 {
|
|
return ErrAllocationBudgetExceeded
|
|
}
|
|
// note that this pushes any overflow errors up the stack when AsInt() may
|
|
// be called on a UintNode that is too large to cast to an int64
|
|
if tk.Uint > math.MaxInt64 {
|
|
return na.AssignNode(basicnode.NewUint(tk.Uint))
|
|
}
|
|
return na.AssignInt(int64(tk.Uint))
|
|
case tok.TFloat64:
|
|
*gas -= 1
|
|
if *gas < 0 {
|
|
return ErrAllocationBudgetExceeded
|
|
}
|
|
return na.AssignFloat(tk.Float64)
|
|
default:
|
|
panic("unreachable")
|
|
}
|
|
}
|