///|
/// One operation owns its budget, including keys and pointer traversal.
priv struct Decode {
data : Bytes
base : Int
end : Int
limits : Limits
mut remaining : Int
mut payload : Int
active : Array[Int]
}
///|
fn decoder(data : Bytes, base : Int, end : Int, limits : Limits) -> Decode {
{
data,
base,
end,
limits,
remaining: limits.max_values,
payload: limits.max_payload_bytes,
active: [],
}
}
///|
fn bounded(ctx : Decode, offset : Int, size : Int) -> Unit raise MmdbError {
if offset < ctx.base ||
size < 0 ||
offset > ctx.end ||
size > ctx.end - offset {
raise MmdbError("out-of-bounds", offset, "Value exceeds its section")
}
}
///|
fn uint_be(ctx : Decode, offset : Int, size : Int) -> UInt64 raise MmdbError {
bounded(ctx, offset, size)
let mut value : UInt64 = 0
for i in offset..<(offset + size) {
value = (value << 8) | ctx.data[i].to_uint64()
}
value
}
///|
/// Base-10 arithmetic avoids any floating-point conversion of UInt128.
fn decimal_bytes(data : Bytes, offset : Int, size : Int) -> String {
let digits : Array[Int] = [0]
for i in offset..<(offset + size) {
let mut carry = data[i].to_int()
for j in 0.. 0 {
digits.push(carry % 10)
carry = carry / 10
}
}
let out = StringBuilder()
for i = digits.length() - 1; i >= 0; i = i - 1 {
out.write_string(digits[i].to_string())
}
out.to_string()
}
///|
fn decode(
ctx : Decode,
offset : Int,
depth : Int,
) -> (Value, Int) raise MmdbError {
bounded(ctx, offset, 1)
if depth > ctx.limits.max_depth {
raise MmdbError("depth-limit", offset, "Nesting budget exhausted")
}
if ctx.remaining <= 0 {
raise MmdbError("value-limit", offset, "Decode work budget exhausted")
}
if ctx.active.contains(offset) {
raise MmdbError("pointer-cycle", offset, "Cyclic data reference")
}
ctx.remaining = ctx.remaining - 1
ctx.active.push(offset)
let result = decode_value(ctx, offset, depth)
ignore(ctx.active.pop())
result
}
///|
fn decode_value(
ctx : Decode,
offset : Int,
depth : Int,
) -> (Value, Int) raise MmdbError {
let control = ctx.data[offset].to_int()
let mut kind = control >> 5
let mut pos = offset + 1
if kind == 1 {
let width = ((control >> 3) & 3) + 1
let mut pointer = uint_be(ctx, pos, width)
if width < 4 {
pointer = pointer | ((control & 7).to_uint64() << (width * 8))
}
if width == 2 {
pointer = pointer + 2048
}
if width == 3 {
pointer = pointer + 526336
}
if pointer >= (ctx.end - ctx.base).to_uint64() {
raise MmdbError("out-of-bounds", offset, "Pointer outside data section")
}
let target = ctx.base + pointer.to_int()
if ctx.data[target].to_int() >> 5 == 1 {
raise MmdbError(
"pointer-to-pointer", offset, "Pointer targets another pointer",
)
}
let (value, _) = decode(ctx, target, depth + 1)
return (value, pos + width)
}
if kind == 0 {
bounded(ctx, pos, 1)
kind = ctx.data[pos].to_int() + 7
pos = pos + 1
}
let mut size = control & 31
if size >= 29 {
let extra = size - 28
let base = if size == 29 { 29 } else if size == 30 { 285 } else { 65821 }
size = base + uint_be(ctx, pos, extra).to_int()
pos = pos + extra
}
if kind == 7 || kind == 11 {
let children = if kind == 7 { size * 2 } else { size }
if children > ctx.remaining {
raise MmdbError(
"value-limit", offset, "Declared container exceeds work budget",
)
}
if kind == 11 {
let items : Array[Value] = []
for _ in 0.. s
_ =>
raise MmdbError(
"invalid-map-key", pos, "Map key must be a UTF-8 string",
)
}
if seen.contains(name) {
raise MmdbError(
"duplicate-key", pos, "Duplicate map key is outside reader profile",
)
}
seen[name] = true
let (value, after) = decode(ctx, next, depth + 1)
items.push((name, value))
pos = after
}
return (Object(items), pos)
}
if kind == 14 {
if size > 1 {
raise MmdbError(
"invalid-size", offset, "Boolean size must be zero or one",
)
}
return (Boolean(size == 1), pos)
}
let max_size = match kind {
2 | 4 => size
3 => 8
5 => 2
6 | 8 | 15 => 4
9 => 8
10 => 16
_ =>
raise MmdbError(
"unsupported-type", offset, "Unsupported or reserved MMDB type",
)
}
if size > max_size || ((kind == 3 || kind == 15) && size != max_size) {
raise MmdbError("invalid-size", offset, "Invalid encoded scalar size")
}
bounded(ctx, pos, size)
let end = pos + size
let value = match kind {
2 | 4 => {
if size > ctx.payload {
raise MmdbError(
"payload-limit", offset, "Expanded bytes budget exhausted",
)
}
ctx.payload = ctx.payload - size
if kind == 2 {
let text = @utf8.decode(ctx.data[pos:end], ignore_bom=false) catch {
_ => raise MmdbError("invalid-utf8", pos, "String is not valid UTF-8")
}
Text(text)
} else {
Blob(ctx.data[pos:end].to_owned())
}
}
3 => Real64({ bits: uint_be(ctx, pos, size), })
5 => Unsigned16(uint_be(ctx, pos, size).to_uint())
6 => Unsigned32(uint_be(ctx, pos, size).to_uint())
8 => Signed32(uint_be(ctx, pos, size).to_int())
9 => Unsigned64(uint_be(ctx, pos, size))
10 => Unsigned128(decimal_bytes(ctx.data, pos, size))
15 => Real32({ bits: uint_be(ctx, pos, size).to_uint(), })
_ => raise MmdbError("unsupported-type", offset, "Unsupported MMDB scalar")
}
(value, end)
}