unmarshal.go 5.6 KB
Newer Older
1
package dagcbor
Eric Myhre's avatar
Eric Myhre committed
2 3

import (
4
	"errors"
Eric Myhre's avatar
Eric Myhre committed
5 6 7 8 9
	"fmt"
	"math"

	"github.com/polydawn/refmt/shared"
	"github.com/polydawn/refmt/tok"
10
	cid "gitlab.dms3.io/dms3/go-cid"
Eric Myhre's avatar
Eric Myhre committed
11

tavit ohanian's avatar
tavit ohanian committed
12
	ld "gitlab.dms3.io/ld/go-ld-prime"
tavit ohanian's avatar
tavit ohanian committed
13
	cidlink "gitlab.dms3.io/ld/go-ld-prime/linking/cid"
Eric Myhre's avatar
Eric Myhre committed
14 15
)

16
var (
17
	ErrInvalidMultibase         = errors.New("invalid multibase on LD link")
18 19 20 21 22 23
	ErrAllocationBudgetExceeded = errors.New("message structure demanded too many resources to process")
)

const (
	mapEntryGasScore  = 8
	listEntryGasScore = 4
24
)
Eric Myhre's avatar
Eric Myhre committed
25

Eric Myhre's avatar
Eric Myhre committed
26
// This file should be identical to the general feature in the parent package,
27 28
// except for the `case tok.TBytes` block,
// which has dag-cbor's special sauce for detecting schemafree links.
Eric Myhre's avatar
Eric Myhre committed
29

tavit ohanian's avatar
tavit ohanian committed
30
func Unmarshal(na ld.NodeAssembler, tokSrc shared.TokenSource, allowLinks bool) error {
31 32 33 34 35
	// 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 int = 1048576 * 10
Will Scott's avatar
Will Scott committed
36
	return unmarshal1(na, tokSrc, &gas, allowLinks)
37 38
}

tavit ohanian's avatar
tavit ohanian committed
39
func unmarshal1(na ld.NodeAssembler, tokSrc shared.TokenSource, gas *int, allowLinks bool) error {
Eric Myhre's avatar
Eric Myhre committed
40 41 42 43 44 45
	var tk tok.Token
	done, err := tokSrc.Step(&tk)
	if err != nil {
		return err
	}
	if done && !tk.Type.IsValue() {
46
		return fmt.Errorf("unexpected eof")
Eric Myhre's avatar
Eric Myhre committed
47
	}
Will Scott's avatar
Will Scott committed
48
	return unmarshal2(na, tokSrc, &tk, gas, allowLinks)
Eric Myhre's avatar
Eric Myhre committed
49 50 51 52
}

// starts with the first token already primed.  Necessary to get recursion
//  to flow right without a peek+unpeek system.
tavit ohanian's avatar
tavit ohanian committed
53
func unmarshal2(na ld.NodeAssembler, tokSrc shared.TokenSource, tk *tok.Token, gas *int, allowLinks bool) error {
54
	// FUTURE: check for schema.TypedNodeBuilder that's going to parse a Link (they can slurp any token kind they want).
Eric Myhre's avatar
Eric Myhre committed
55 56 57 58 59 60 61
	switch tk.Type {
	case tok.TMapOpen:
		expectLen := tk.Length
		allocLen := tk.Length
		if tk.Length == -1 {
			expectLen = math.MaxInt32
			allocLen = 0
62 63 64 65
		} else {
			if *gas-allocLen < 0 { // halt early if this will clearly demand too many resources
				return ErrAllocationBudgetExceeded
			}
Eric Myhre's avatar
Eric Myhre committed
66
		}
67
		ma, err := na.BeginMap(int64(allocLen))
Eric Myhre's avatar
Eric Myhre committed
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
		if err != nil {
			return err
		}
		observedLen := 0
		for {
			_, err := tokSrc.Step(tk)
			if err != nil {
				return err
			}
			switch tk.Type {
			case tok.TMapClose:
				if expectLen != math.MaxInt32 && observedLen != expectLen {
					return fmt.Errorf("unexpected mapClose before declared length")
				}
				return ma.Finish()
			case tok.TString:
84 85 86 87
				*gas -= len(tk.Str) + mapEntryGasScore
				if *gas < 0 {
					return ErrAllocationBudgetExceeded
				}
Eric Myhre's avatar
Eric Myhre committed
88 89 90 91 92 93 94 95
				// 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")
			}
96
			mva, err := ma.AssembleEntry(tk.Str)
Eric Myhre's avatar
Eric Myhre committed
97 98 99
			if err != nil { // return in error if the key was rejected
				return err
			}
Will Scott's avatar
Will Scott committed
100
			err = unmarshal1(mva, tokSrc, gas, allowLinks)
Eric Myhre's avatar
Eric Myhre committed
101 102 103 104 105 106 107 108 109 110 111 112
			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 := tk.Length
		allocLen := tk.Length
		if tk.Length == -1 {
			expectLen = math.MaxInt32
			allocLen = 0
113 114 115 116
		} else {
			if *gas-allocLen < 0 { // halt early if this will clearly demand too many resources
				return ErrAllocationBudgetExceeded
			}
Eric Myhre's avatar
Eric Myhre committed
117
		}
118
		la, err := na.BeginList(int64(allocLen))
Eric Myhre's avatar
Eric Myhre committed
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
		if err != nil {
			return err
		}
		observedLen := 0
		for {
			_, err := tokSrc.Step(tk)
			if err != nil {
				return err
			}
			switch tk.Type {
			case tok.TArrClose:
				if expectLen != math.MaxInt32 && observedLen != expectLen {
					return fmt.Errorf("unexpected arrClose before declared length")
				}
				return la.Finish()
			default:
135 136 137 138
				*gas -= listEntryGasScore
				if *gas < 0 {
					return ErrAllocationBudgetExceeded
				}
Eric Myhre's avatar
Eric Myhre committed
139 140 141 142
				observedLen++
				if observedLen > expectLen {
					return fmt.Errorf("unexpected continuation of array elements beyond declared length")
				}
Will Scott's avatar
Will Scott committed
143
				err := unmarshal2(la.AssembleValue(), tokSrc, tk, gas, allowLinks)
Eric Myhre's avatar
Eric Myhre committed
144 145 146 147 148 149 150 151 152 153
				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:
154 155 156 157
		*gas -= len(tk.Str)
		if *gas < 0 {
			return ErrAllocationBudgetExceeded
		}
Eric Myhre's avatar
Eric Myhre committed
158 159
		return na.AssignString(tk.Str)
	case tok.TBytes:
160 161 162 163
		*gas -= len(tk.Bytes)
		if *gas < 0 {
			return ErrAllocationBudgetExceeded
		}
164 165 166 167 168
		if !tk.Tagged {
			return na.AssignBytes(tk.Bytes)
		}
		switch tk.Tag {
		case linkTag:
Will Scott's avatar
Will Scott committed
169 170 171
			if !allowLinks {
				return fmt.Errorf("unhandled cbor tag %d", tk.Tag)
			}
172
			if len(tk.Bytes) < 1 || tk.Bytes[0] != 0 {
173 174 175 176 177 178
				return ErrInvalidMultibase
			}
			elCid, err := cid.Cast(tk.Bytes[1:])
			if err != nil {
				return err
			}
Will Scott's avatar
Will Scott committed
179
			return na.AssignLink(cidlink.Link{Cid: elCid})
180 181 182
		default:
			return fmt.Errorf("unhandled cbor tag %d", tk.Tag)
		}
Eric Myhre's avatar
Eric Myhre committed
183
	case tok.TBool:
184 185 186 187
		*gas -= 1
		if *gas < 0 {
			return ErrAllocationBudgetExceeded
		}
Eric Myhre's avatar
Eric Myhre committed
188 189
		return na.AssignBool(tk.Bool)
	case tok.TInt:
190 191 192 193
		*gas -= 1
		if *gas < 0 {
			return ErrAllocationBudgetExceeded
		}
194
		return na.AssignInt(tk.Int)
Eric Myhre's avatar
Eric Myhre committed
195
	case tok.TUint:
196 197 198 199
		*gas -= 1
		if *gas < 0 {
			return ErrAllocationBudgetExceeded
		}
200
		return na.AssignInt(int64(tk.Uint)) // FIXME overflow check
Eric Myhre's avatar
Eric Myhre committed
201
	case tok.TFloat64:
202 203 204 205
		*gas -= 1
		if *gas < 0 {
			return ErrAllocationBudgetExceeded
		}
Eric Myhre's avatar
Eric Myhre committed
206 207 208 209 210
		return na.AssignFloat(tk.Float64)
	default:
		panic("unreachable")
	}
}