unmarshal.go 5.41 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
	"fmt"
	"math"

8
	cid "github.com/ipfs/go-cid"
Eric Myhre's avatar
Eric Myhre committed
9 10 11
	"github.com/polydawn/refmt/shared"
	"github.com/polydawn/refmt/tok"

12 13
	ipld "github.com/ipld/go-ipld-prime"
	cidlink "github.com/ipld/go-ipld-prime/linking/cid"
Eric Myhre's avatar
Eric Myhre committed
14 15
)

16
var (
17 18 19 20 21 22 23
	ErrInvalidMultibase         = errors.New("invalid multibase on IPLD link")
	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

26 27 28
// This 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.
Eric Myhre's avatar
Eric Myhre committed
29 30

func Unmarshal(na ipld.NodeAssembler, tokSrc shared.TokenSource) error {
31 32 33 34 35 36 37 38 39
	// 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
	return unmarshal1(na, tokSrc, &gas)
}

func unmarshal1(na ipld.NodeAssembler, tokSrc shared.TokenSource, gas *int) 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
	}
48
	return unmarshal2(na, tokSrc, &tk, gas)
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.
53
func unmarshal2(na ipld.NodeAssembler, tokSrc shared.TokenSource, tk *tok.Token, gas *int) 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 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
		}
		ma, err := na.BeginMap(allocLen)
		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
			}
100
			err = unmarshal1(mva, tokSrc, gas)
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 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
		}
		la, err := na.BeginList(allocLen)
		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")
				}
143
				err := unmarshal2(la.AssembleValue(), tokSrc, tk, gas)
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 169 170 171 172 173 174 175 176 177 178 179
		if !tk.Tagged {
			return na.AssignBytes(tk.Bytes)
		}
		switch tk.Tag {
		case linkTag:
			if tk.Bytes[0] != 0 {
				return ErrInvalidMultibase
			}
			elCid, err := cid.Cast(tk.Bytes[1:])
			if err != nil {
				return err
			}
			return na.AssignLink(cidlink.Link{elCid})
		default:
			return fmt.Errorf("unhandled cbor tag %d", tk.Tag)
		}
Eric Myhre's avatar
Eric Myhre committed
180
	case tok.TBool:
181 182 183 184
		*gas -= 1
		if *gas < 0 {
			return ErrAllocationBudgetExceeded
		}
Eric Myhre's avatar
Eric Myhre committed
185 186
		return na.AssignBool(tk.Bool)
	case tok.TInt:
187 188 189 190
		*gas -= 1
		if *gas < 0 {
			return ErrAllocationBudgetExceeded
		}
Eric Myhre's avatar
Eric Myhre committed
191 192
		return na.AssignInt(int(tk.Int)) // FIXME overflow check
	case tok.TUint:
193 194 195 196
		*gas -= 1
		if *gas < 0 {
			return ErrAllocationBudgetExceeded
		}
Eric Myhre's avatar
Eric Myhre committed
197 198
		return na.AssignInt(int(tk.Uint)) // FIXME overflow check
	case tok.TFloat64:
199 200 201 202
		*gas -= 1
		if *gas < 0 {
			return ErrAllocationBudgetExceeded
		}
Eric Myhre's avatar
Eric Myhre committed
203 204 205 206 207
		return na.AssignFloat(tk.Float64)
	default:
		panic("unreachable")
	}
}