diff --git a/crates/scarlet/tests/golden/wire_format.stdout b/crates/scarlet/tests/golden/wire_format.stdout index c019c30b..bde626b3 100644 --- a/crates/scarlet/tests/golden/wire_format.stdout +++ b/crates/scarlet/tests/golden/wire_format.stdout @@ -51,7 +51,10 @@ field name: Ok(Content-Length) lowercased: Ok(content-length) is content-length: True is content-type: False -past the end: <<>> +past the end: Err(Nil) +one byte over: Err(Nil) +drop 36: <<102, 13, 10>> +drop 900: <<>> byte_at 0: 67 byte_at 900: -1 parse_int(Dec): Ok(4210) diff --git a/crates/scarlet/tests/programs/http_parse.scrl b/crates/scarlet/tests/programs/http_parse.scrl index 6682120c..a98763f5 100644 --- a/crates/scarlet/tests/programs/http_parse.scrl +++ b/crates/scarlet/tests/programs/http_parse.scrl @@ -13,6 +13,7 @@ // the connection driver against a loopback listener. import scarlet/binary +import scarlet/result import scarlet/string import scarlet/array import scarlet/http/h1.{ @@ -55,7 +56,7 @@ fn vis_from(b Binary, i Int, len Int, acc String) String { 10 -> '\\n' 9 -> '\\t' 0 -> '\\0' - _ -> binary.to_string(binary.slice_bytes(b, i, 1)) or '?' + _ -> result.then(binary.slice_bytes(b, i, 1), binary.to_string) or '?' } vis_from(b, i + 1, len, '${acc}${piece}') } diff --git a/crates/scarlet/tests/programs/numerics.scrl b/crates/scarlet/tests/programs/numerics.scrl index b01e9c98..5013fb1a 100644 --- a/crates/scarlet/tests/programs/numerics.scrl +++ b/crates/scarlet/tests/programs/numerics.scrl @@ -31,7 +31,7 @@ fn parse_int(s String) Result(Int, Nil) { fn parse_signed(s String) Result(Int, Nil) { b = binary.from_string(s) if binary.byte_size(b) > 0 && binary.byte_at(b, 0) == 45 { - rest = binary.slice_bytes(b, 1, binary.byte_size(b) - 1) + rest = binary.drop_bytes(b, 1) result.map(binary.parse_int(rest, Dec), fn(n) -n) } else { binary.parse_int(b, Dec) diff --git a/crates/scarlet/tests/stdlib.rs b/crates/scarlet/tests/stdlib.rs index 2eac4a0f..f8d81fc6 100644 --- a/crates/scarlet/tests/stdlib.rs +++ b/crates/scarlet/tests/stdlib.rs @@ -394,11 +394,11 @@ fn stdlib_binary() { "import scarlet/binary\n\ pub fn main() {\n\ \tb = binary.from_string('ABC')\n\ - \tprintln(binary.slice(b, 8, 8))\n\ - \tprintln(binary.slice(b, 0, 99))\n\ + \tprintln(binary.slice_bits(b, 8, 8))\n\ + \tprintln(binary.slice_bits(b, 0, 99))\n\ \tjoined = binary.append(binary.from_string('AB'), binary.from_string('C'))\n\ \tprintln(binary.to_string(joined))\n\ - \tprintln(binary.bit_size(binary.slice(b, 0, 5) or binary.from_string('')))\n\ + \tprintln(binary.bit_size(binary.slice_bits(b, 0, 5) or binary.from_string('')))\n\ }\n", "Ok(<<66>>)\nErr(Nil)\nOk(ABC)\n5\n", ); @@ -447,7 +447,7 @@ fn stdlib_binary() { run_outputs( "import scarlet/binary\n\ pub fn main() {\n\ - \tprintln(binary.slice(binary.from_string('ABC'), 0 - 1, 8))\n\ + \tprintln(binary.slice_bits(binary.from_string('ABC'), 0 - 1, 8))\n\ }\n", "Err(Nil)\n", ); @@ -458,6 +458,89 @@ fn stdlib_binary() { ); } +/// The unit a window is measured in lives in the function's name, and the two +/// names differ by a factor of eight. Before this, `slice` was bit-indexed and +/// `byte_size` answered in bytes, so a byte-indexed caller of `slice` read an +/// eighth of the window it asked for and was told nothing: `slice(b, 0, 2)` +/// answered `Ok(<<0:size(2)>>)`, a valid binary, rather than the first two +/// bytes. The old spelling no longer exists, so that call is now a compile +/// error rather than a wrong answer. +#[test] +fn stdlib_binary_slice_units() { + // The name `slice` is gone. This is the guard on reintroducing it: a + // function of that name could only pick one of the two units, and the + // callers meaning the other one would keep compiling. + check_rejects( + "import scarlet/binary\n\ + pub fn main() { println(binary.slice(<<1, 2, 3, 4, 5>>, 0, 2)) }\n", + // Spelled out: bare `slice` is a substring of `slice_bits`, so the + // loose form would pass on a diagnostic about a different name. + "has no member 'slice'", + ); + // Both units, over the same measured windows from T-208, side by side. + run_outputs( + "import scarlet/binary\n\ + pub fn main() {\n\ + \tb = <<1, 2, 3, 4, 5>>\n\ + \tprintln(binary.slice_bytes(b, 0, 2))\n\ + \tprintln(binary.slice_bits(b, 0, 2))\n\ + \tprintln(binary.slice_bytes(b, 1, 2))\n\ + \tprintln(binary.slice_bits(b, 8, 16))\n\ + \tprintln(binary.slice_bytes(b, 0, 5))\n\ + \tprintln(binary.slice_bits(b, 0, 40))\n\ + }\n", + "Ok(<<1, 2>>)\nOk(<<0:size(2)>>)\nOk(<<2, 3>>)\nOk(<<2, 3>>)\n\ + Ok(<<1, 2, 3, 4, 5>>)\nOk(<<1, 2, 3, 4, 5>>)\n", + ); + // T-214: every out-of-range case that used to collapse to `<<>>` — one + // byte over, wholly past the end, negative offset, negative length. The + // last two are why a Gleam `slice(b, byte_size(b), -n)` cannot be + // transliterated: it is an error here, not a backwards window. + run_outputs( + "import scarlet/binary\n\ + pub fn main() {\n\ + \tb = <<1, 2, 3, 4, 5>>\n\ + \tprintln(binary.slice_bytes(b, 4, 1))\n\ + \tprintln(binary.slice_bytes(b, 4, 2))\n\ + \tprintln(binary.slice_bytes(b, 5, 1))\n\ + \tprintln(binary.slice_bytes(b, 0 - 1, 2))\n\ + \tprintln(binary.slice_bytes(b, 5, 0 - 4))\n\ + }\n", + "Ok(<<5>>)\nErr(Nil)\nErr(Nil)\nErr(Nil)\nErr(Nil)\n", + ); + // `byte_size` rounds up, so the last byte of a 12-bit binary is not a + // whole byte and is not addressable by slice_bytes. Reaching it is what + // slice_bits is for. + run_outputs( + "import scarlet/binary\n\ + pub fn main() {\n\ + \tb = <<1, 2:4>>\n\ + \tprintln(binary.byte_size(b))\n\ + \tprintln(binary.slice_bytes(b, 0, 2))\n\ + \tprintln(binary.slice_bits(b, 8, 4))\n\ + }\n", + "2\nErr(Nil)\nOk(<<2:size(4)>>)\n", + ); + // drop_bytes is total because it is given a position, not a length: there + // is no requested size for the answer to fall short of. Past the end the + // remainder really is empty, and it reaches the trailing bits that + // slice_bytes cannot. + run_outputs( + "import scarlet/binary\n\ + import scarlet/string\n\ + pub fn main() {\n\ + \tb = <<1, 2, 3, 4, 5>>\n\ + \tprintln(string.inspect(binary.drop_bytes(b, 0)))\n\ + \tprintln(string.inspect(binary.drop_bytes(b, 3)))\n\ + \tprintln(string.inspect(binary.drop_bytes(b, 5)))\n\ + \tprintln(string.inspect(binary.drop_bytes(b, 900)))\n\ + \tprintln(string.inspect(binary.drop_bytes(b, 0 - 2)))\n\ + \tprintln(binary.bit_size(binary.drop_bytes(<<1, 2:4>>, 1)))\n\ + }\n", + "<<1, 2, 3, 4, 5>>\n<<4, 5>>\n<<>>\n<<>>\n<<1, 2, 3, 4, 5>>\n4\n", + ); +} + #[test] fn stdlib_binary_byte_at() { // byte_at is -1 out of bounds on both sides; a view reads through its diff --git a/crates/scarlet_core/src/bytecode/mod.rs b/crates/scarlet_core/src/bytecode/mod.rs index 6f39c30e..008065f5 100644 --- a/crates/scarlet_core/src/bytecode/mod.rs +++ b/crates/scarlet_core/src/bytecode/mod.rs @@ -67,7 +67,7 @@ fn builtin_op(name: &str) -> Option { "binary__to_string" => Op::BinToString, "binary__bit_size" => Op::BinBitSize, "binary__byte_size" => Op::BinByteSize, - "binary__slice" => Op::BinSlice, + "binary__slice_bits" => Op::BinSlice, "binary__append" => Op::BinAppend, "binary__index_of" => Op::BinIndexOf, "binary__byte_at" => Op::BinByteAt, diff --git a/crates/scarlet_core/src/std/scarlet/binary.scrl b/crates/scarlet_core/src/std/scarlet/binary.scrl index 41a46d1b..fbcd0f11 100644 --- a/crates/scarlet_core/src/std/scarlet/binary.scrl +++ b/crates/scarlet_core/src/std/scarlet/binary.scrl @@ -10,17 +10,53 @@ pub fn bit_size(b Binary) Int @vm(binary__byte_size) pub fn byte_size(b Binary) Int -@vm(binary__slice) -pub fn slice(b Binary, at_bit Int, take_bits Int) Result(Binary, Nil) +// The window `b[at_bit .. at_bit+take_bits]`, in BITS. O(1): shares `b`'s +// backing and copies nothing. A negative bound, or one that runs past +// `bit_size(b)`, is `Err(Nil)` — never a short read. +// +// Every offset and length in this module states its unit in its own name, +// because the two differ by a factor of 8 and a mistaken window is a valid +// binary rather than a failure. There is deliberately no function called +// `slice`: it used to mean bits, and a name that changed unit would leave +// working code silently reading an eighth of what it asked for. +@vm(binary__slice_bits) +pub fn slice_bits(b Binary, at_bit Int, take_bits Int) Result(Binary, Nil) -// Byte-oriented slice of `b[start .. start+len]` (offsets and length in bytes; -// `slice` itself takes bits). O(1): shares `b`'s backing and copies nothing. -// An out-of-range request yields an empty binary, so a parser can take field -// views by offset without per-call error handling. -pub fn slice_bytes(b Binary, start Int, len Int) Binary { - match slice(b, start * 8, len * 8) { - Ok(v) -> v - Err(Nil) -> <<>> +// The window `b[at .. at+len]`, in BYTES. O(1): shares `b`'s backing and +// copies nothing. A negative bound, or one that runs past the end, is +// `Err(Nil)`: a length that came off the wire is exactly the one that must +// not fail quietly. +// +// `byte_size` rounds up, so the last byte of a binary whose bit length is not +// a multiple of 8 is not addressable here — ask for it with `slice_bits`. +pub fn slice_bytes(b Binary, at Int, len Int) Result(Binary, Nil) { + slice_bits(b, at * 8, len * 8) +} + +// Everything from byte `at` onwards. Total, and honestly so: past the end the +// remainder really is `<<>>`, and a negative `at` drops nothing — there is no +// requested length for the answer to fall short of, so nothing is swallowed. +// Use it instead of `slice_bytes(b, at, byte_size(b) - at)`, which repeats the +// bound the callee already knows. +pub fn drop_bytes(b Binary, at Int) Binary { + total = bit_size(b) + // Clamped before the ×8 so a huge `at` cannot wrap into a negative offset. + skip = if at <= 0 { + 0 + } else if at >= byte_size(b) { + total + } else { + at * 8 + } + if skip >= total { + <<>> + } else { + match slice_bits(b, skip, total - skip) { + Ok(v) -> v + // Unreachable: 0 <= skip < total == bit_size(b), so the window is + // exactly the tail. The arm exists because Result has two of them. + Err(Nil) -> <<>> + } } } diff --git a/crates/scarlet_core/src/std/scarlet/decimal.scrl b/crates/scarlet_core/src/std/scarlet/decimal.scrl index 5d698b13..b8707aca 100644 --- a/crates/scarlet_core/src/std/scarlet/decimal.scrl +++ b/crates/scarlet_core/src/std/scarlet/decimal.scrl @@ -315,7 +315,7 @@ fn pow10(n Int) Int { } fn tail(b Binary) Binary { - binary.slice_bytes(b, 1, binary.byte_size(b) - 1) + binary.drop_bytes(b, 1) } fn parse_unsigned(b Binary) Option(Decimal) { @@ -324,11 +324,10 @@ fn parse_unsigned(b Binary) Option(Decimal) { Ok(n) -> Some(Decimal(n, 0)) Err(Nil) -> None } - Some(i) -> - parse_parts( - binary.slice_bytes(b, 0, i), - binary.slice_bytes(b, i + 1, binary.byte_size(b) - i - 1), - ) + Some(i) -> match binary.slice_bytes(b, 0, i) { + Ok(whole) -> parse_parts(whole, binary.drop_bytes(b, i + 1)) + Err(Nil) -> None + } } } diff --git a/crates/scarlet_core/src/std/scarlet/http.scrl b/crates/scarlet_core/src/std/scarlet/http.scrl index 8e0d3833..850a0186 100644 --- a/crates/scarlet_core/src/std/scarlet/http.scrl +++ b/crates/scarlet_core/src/std/scarlet/http.scrl @@ -162,7 +162,12 @@ fn to_method(m Binary) Method { // first `?`, or the whole target if there is no query. O(1) zero-copy slice. pub fn path(r Request) Binary { match binary.index_of(r.target, QUESTION, 0) { - Some(i) -> binary.slice_bytes(r.target, 0, i) + // `index_of` answers with a byte index inside `r.target`, so `0..i` is + // in range and the Err arm is unreachable. + Some(i) -> match binary.slice_bytes(r.target, 0, i) { + Ok(p) -> p + Err(Nil) -> EMPTY + } None -> r.target } } @@ -171,7 +176,7 @@ pub fn path(r Request) Binary { // (not including it), or empty if there is no `?`. pub fn query(r Request) Binary { match binary.index_of(r.target, QUESTION, 0) { - Some(i) -> binary.slice_bytes(r.target, i + 1, binary.byte_size(r.target) - i - 1) + Some(i) -> binary.drop_bytes(r.target, i + 1) None -> EMPTY } } @@ -348,11 +353,10 @@ fn flush(sock Socket, pending Array(Binary)) Result(Nil, NetError) { // concatenate, so the freshly read bytes ARE the new buffer: taking that exit // keeps the read off the copy path entirely. fn carry(buf Binary, off Int, more Binary) Binary { - rest = binary.byte_size(buf) - off - if rest == 0 { + if binary.byte_size(buf) - off == 0 { more } else { - binary.append(binary.slice_bytes(buf, off, rest), more) + binary.append(binary.drop_bytes(buf, off), more) } } @@ -421,7 +425,12 @@ fn read_body( _ <- result.then(maybe_continue(sock, head.flags)) avail = binary.byte_size(buf) - consumed buffered = int.min(avail, n) - head_bytes = binary.slice_bytes(buf, consumed, buffered) + // `buffered <= avail == byte_size(buf) - consumed`, so the window ends at + // or before the end of `buf` and the Err arm is unreachable. + head_bytes = match binary.slice_bytes(buf, consumed, buffered) { + Ok(v) -> v + Err(Nil) -> <<>> + } need = n - buffered tail <- result.then(body.collect(body.content_length(sock, need, deadline), MAX_BODY)) // Pending responses were flushed before the body was read, so this diff --git a/crates/scarlet_core/src/std/scarlet/http/headers.scrl b/crates/scarlet_core/src/std/scarlet/http/headers.scrl index 3283fb16..cb1201d7 100644 --- a/crates/scarlet_core/src/std/scarlet/http/headers.scrl +++ b/crates/scarlet_core/src/std/scarlet/http/headers.scrl @@ -182,6 +182,22 @@ fn has_token(v Binary, token Binary) Bool { value_has_token(v, 0, binary.byte_size(v), token) } +// Whether `v[lo..hi]` is `token`, case-insensitively. Empty list elements +// (`a,,b`, a trailing comma, an empty value) are ignored per RFC 9110 — +// without the `hi > lo` guard an empty element would "match" an empty token. +// A window past the end of `v` is likewise not the token; callers pass bounds +// derived from `byte_size(v)`, so that arm should be unreachable. +fn element_is(v Binary, lo Int, hi Int, token Binary) Bool { + if hi <= lo { + False + } else { + match binary.slice_bytes(v, lo, hi - lo) { + Ok(e) -> binary.eq_ignore_ascii_case(e, token) + Err(Nil) -> False + } + } +} + fn value_has_token(v Binary, from Int, len Int, token Binary) Bool { to = match binary.index_of(v, comma, from) { Some(i) -> i @@ -189,10 +205,7 @@ fn value_has_token(v Binary, from Int, len Int, token Binary) Bool { } lo = skip_ows(v, from, to) hi = trim_ows(v, lo, to) - // Empty list elements (`a,,b`, a trailing comma, an empty value) are - // ignored per RFC 9110 — without the hi > lo guard an empty element would - // "match" an empty token. - if hi > lo && binary.eq_ignore_ascii_case(binary.slice_bytes(v, lo, hi - lo), token) { + if element_is(v, lo, hi, token) { True } else if to < len { value_has_token(v, to + 1, len, token) diff --git a/crates/scarlet_core/src/std/scarlet/json/decode.scrl b/crates/scarlet_core/src/std/scarlet/json/decode.scrl index 4c761cd3..e0c8ad65 100644 --- a/crates/scarlet_core/src/std/scarlet/json/decode.scrl +++ b/crates/scarlet_core/src/std/scarlet/json/decode.scrl @@ -196,7 +196,7 @@ fn parse_signed(s String) Result(Int, Nil) { b = binary.from_string(s) // 45 is `-`. if binary.byte_at(b, 0) == 45 { - match binary.parse_int(binary.slice_bytes(b, 1, binary.byte_size(b) - 1), binary.Dec) { + match binary.parse_int(binary.drop_bytes(b, 1), binary.Dec) { Ok(n) -> Ok(0 - n) Err(Nil) -> Err(Nil) } diff --git a/crates/scarlet_vm/src/vm/text.rs b/crates/scarlet_vm/src/vm/text.rs index 7dfde3ef..17a0fc54 100644 --- a/crates/scarlet_vm/src/vm/text.rs +++ b/crates/scarlet_vm/src/vm/text.rs @@ -113,9 +113,9 @@ impl VM { } pub(super) fn bin_slice(&mut self) -> VmResult<()> { - let take = self.pop_int("binary.slice")?; - let at = self.pop_int("binary.slice")?; - let bin_v = self.pop_binary("binary.slice")?; + let take = self.pop_int("binary.slice_bits")?; + let at = self.pop_int("binary.slice_bits")?; + let bin_v = self.pop_binary("binary.slice_bits")?; let bin = bin_ref(&bin_v); let v = if at < 0 || take < 0 || (at as u64) + (take as u64) > bin.bit_len() { self.make_err_nil()? diff --git a/examples/bench_service.scrl b/examples/bench_service.scrl index 8da78b02..55b273e8 100644 --- a/examples/bench_service.scrl +++ b/examples/bench_service.scrl @@ -176,10 +176,15 @@ fn scan_pairs( None -> n } match binary.index_of(b, EQ, start) { - Some(e) -> if e < end && binary.slice_bytes(b, start, e - start) == key { - Some(binary.slice_bytes(b, e + 1, end - e - 1)) - } else { - next_pair(b, end, n, key, sep, skip_sp) + // An `=` at or past `end` belongs to the next pair, and gives the value + // window a negative length — which slice_bytes reports as Err, so that + // case lands in the fallthrough rather than matching an empty value. + Some(e) -> match ( + binary.slice_bytes(b, start, e - start), + binary.slice_bytes(b, e + 1, end - e - 1), + ) { + (Ok(k), Ok(v)) if e < end && k == key -> Some(v) + _ -> next_pair(b, end, n, key, sep, skip_sp) } None -> None } @@ -260,10 +265,11 @@ fn verify_session(req Request, t Tables) Option(User) { } fn split_token(token Binary) Option((Binary, Binary)) { - n = binary.byte_size(token) match binary.index_of(token, DOT, 0) { - Some(i) -> - Some((binary.slice_bytes(token, 0, i), binary.slice_bytes(token, i + 1, n - i - 1))) + Some(i) -> match binary.slice_bytes(token, 0, i) { + Ok(head) -> Some((head, binary.drop_bytes(token, i + 1))) + Err(Nil) -> None + } None -> None } } @@ -311,8 +317,7 @@ const USERS_PREFIX = <<'/users/'>> const USERS_PREFIX_LEN = 7 fn handle_user_by_id(path Binary, t Tables) Response { - n = binary.byte_size(path) - rest = binary.slice_bytes(path, USERS_PREFIX_LEN, n - USERS_PREFIX_LEN) + rest = binary.drop_bytes(path, USERS_PREFIX_LEN) user = match binary.parse_int(rest, Dec) { Ok(id) -> find_by_id(t, id) Err(Nil) -> None @@ -329,11 +334,10 @@ fn route(req Request, t Tables) Response { Get -> match path { <<'/'>> -> respond(200, VAL_TEXT, 'hello') <<'/me'>> -> handle_me(req, t) - _ -> if binary.byte_size(path) > USERS_PREFIX_LEN && - binary.slice_bytes(path, 0, USERS_PREFIX_LEN) == USERS_PREFIX { - handle_user_by_id(path, t) - } else { - json(404, '{"error":"not found"}') + _ -> match binary.slice_bytes(path, 0, USERS_PREFIX_LEN) { + Ok(prefix) if prefix == USERS_PREFIX && + binary.byte_size(path) > USERS_PREFIX_LEN -> handle_user_by_id(path, t) + _ -> json(404, '{"error":"not found"}') } } Post -> match path { diff --git a/examples/http_client.scrl b/examples/http_client.scrl index d3adb2c9..93a7a876 100644 --- a/examples/http_client.scrl +++ b/examples/http_client.scrl @@ -202,7 +202,12 @@ fn read_body( Length(n) -> { avail = binary.byte_size(buf) - consumed buffered = int.min(avail, n) - head_bytes = binary.slice_bytes(buf, consumed, buffered) + // `buffered <= avail == byte_size(buf) - consumed`, so the window + // ends inside `buf` and the Err arm is unreachable. + head_bytes = match binary.slice_bytes(buf, consumed, buffered) { + Ok(v) -> v + Err(Nil) -> <<>> + } rest = body.content_length(sock, n - buffered, deadline) match body.collect(rest, MAX_BODY) { Ok(tail) -> Ok(binary.append(head_bytes, tail)) diff --git a/examples/wire_format.scrl b/examples/wire_format.scrl index 5ab71a33..6398c326 100644 --- a/examples/wire_format.scrl +++ b/examples/wire_format.scrl @@ -5,6 +5,7 @@ // scanned byte-by-byte without ever building a String. import scarlet/binary.{Dec, Hex, Radix} +import scarlet/result import scarlet/string // One frame on the wire: @@ -64,16 +65,26 @@ fn decode(frame Binary) Result(Sample, String) { // The payload is ASCII, so it is parsed as bytes: find the '=' (61) and the // ' ' (32) with index_of, take the fields around them with slice_bytes, and // read the number straight out of its bytes with parse_int. +// +// slice_bytes is fallible, so a window past the end — or the negative length a +// payload like 'a b=1' produces, where the space precedes the '=' — arrives as +// Err rather than as a silently empty field. The trailing unit needs no bound +// at all: drop_bytes takes whatever is left. fn parse_payload(payload Binary, flags Int) Result(Sample, String) { match (binary.index_of(payload, <<'='>>, 0), binary.index_of(payload, <<' '>>, 0)) { - (Some(eq), Some(sp)) -> { - name = binary.slice_bytes(payload, 0, eq) - digits = binary.slice_bytes(payload, eq + 1, sp - eq - 1) - unit = binary.slice_bytes(payload, sp + 1, binary.byte_size(payload) - sp - 1) - match (binary.to_string(name), binary.parse_int(digits, Dec), binary.to_string(unit)) { + (Some(eq), Some(sp)) -> match ( + binary.slice_bytes(payload, 0, eq), + binary.slice_bytes(payload, eq + 1, sp - eq - 1), + ) { + (Ok(name), Ok(digits)) -> match ( + binary.to_string(name), + binary.parse_int(digits, Dec), + binary.to_string(binary.drop_bytes(payload, sp + 1)), + ) { (Ok(n), Ok(v), Ok(u)) -> Ok(Sample(n, v, u, flags == GAUGE)) _ -> Err('malformed payload') } + _ -> Err('malformed payload') } _ -> Err('payload is not name=value unit') } @@ -239,25 +250,47 @@ pub fn main() { println('index_of "\\r\\n": ${binary.index_of(headers, <<13, 10>>, 0)}') println('index_of "zz": ${binary.index_of(headers, <<'zz'>>, 0)}') - name = binary.slice_bytes(headers, 0, 14) - println('field name: ${string.inspect(binary.to_string(name))}') - println( - 'lowercased: ${string.inspect(binary.to_string(binary.to_ascii_lower(name)))}', - ) - println('is content-length: ${binary.eq_ignore_ascii_case(name, <<'content-length'>>)}') - println('is content-type: ${binary.eq_ignore_ascii_case(name, <<'content-type'>>)}') + // Every offset and length in the binary module states its unit in its own + // name. slice_bytes counts bytes; slice_bits counts bits; there is no + // function called `slice`, because the two differ by a factor of eight and + // a wrong window is a valid binary rather than a failure. + match binary.slice_bytes(headers, 0, 14) { + Ok(name) -> { + println('field name: ${string.inspect(binary.to_string(name))}') + println( + 'lowercased: ${string.inspect(binary.to_string(binary.to_ascii_lower(name)))}', + ) + println('is content-length: ${binary.eq_ignore_ascii_case(name, <<'content-length'>>)}') + println('is content-type: ${binary.eq_ignore_ascii_case(name, <<'content-type'>>)}') + } + // This fixture is longer than 14 bytes, so this arm is not taken here. + // A parser whose field length came off the wire reaches it instead of + // scanning bytes the peer never sent. + Err(Nil) -> println('field name: past the end') + } + + // A window that runs past the end is Err(Nil), never a short or empty read: + // asking for four bytes at 900 is a question about bytes that do not exist. + println('past the end: ${binary.slice_bytes(headers, 900, 4)}') + println('one byte over: ${binary.slice_bytes(headers, 36, 4)}') - // slice_bytes is total: an out-of-range view is empty rather than an error, so - // a scanner can take a field by offset without handling a failure per call. - println('past the end: ${string.inspect(binary.slice_bytes(headers, 900, 4))}') + // drop_bytes needs no bound, so it cannot fall short: past the end the + // remainder really is empty. Contrast the two lines below with the one + // above — the same offset, and here the three bytes that are actually + // there rather than a failure, because nothing was promised a fourth. + println('drop 36: ${string.inspect(binary.drop_bytes(headers, 36))}') + println('drop 900: ${string.inspect(binary.drop_bytes(headers, 900))}') - // byte_at is total too, and returns -1 rather than boxing an Option per probe. + // byte_at is total, and returns -1 rather than boxing an Option per probe. println('byte_at 0: ${binary.byte_at(headers, 0)}') println('byte_at 900: ${binary.byte_at(headers, 900)}') // Numbers are read straight out of the bytes, in whichever base the protocol - // wrote them: decimal for a length, hex for a retry-after cookie. - println('parse_int(Dec): ${binary.parse_int(binary.slice_bytes(headers, 16, 4), Dec)}') + // wrote them: decimal for a length, hex for a retry-after cookie. The window + // is fallible and the parse is fallible, so they chain. + println( + 'parse_int(Dec): ${result.then(binary.slice_bytes(headers, 16, 4), fn(b) binary.parse_int(b, Dec))}', + ) println('"ff" as Dec: ${binary.parse_int(<<'ff'>>, Dec)}') println('"ff" as Hex: ${binary.parse_int(<<'ff'>>, Hex)}') @@ -265,8 +298,8 @@ pub fn main() { println('255 to Hex ascii: ${ascii(255, Hex)}') println('hex ascii round trip: ${binary.parse_int(binary.from_int_ascii(4210, Hex), Hex)}') - // `slice` is the bit-precise version — offsets and lengths in bits — and it - // is the one binary operation that can fail: a view past the end is Err(Nil). - println('slice bits 0..4: ${binary.slice(headers, 0, 4)}') - println('slice bits 8000..8: ${binary.slice(headers, 8000, 8)}') + // slice_bits is the bit-precise version — offsets and lengths in bits — and + // it is what reaches a field that does not start on a byte boundary. + println('slice bits 0..4: ${binary.slice_bits(headers, 0, 4)}') + println('slice bits 8000..8: ${binary.slice_bits(headers, 8000, 8)}') }