std/binary
ByteReader, ByteWriter, BitReader and BitWriter - numbers of every width in either byte order, IEEE 754 floats, LEB128, runs of bytes and text, read with a ReadError instead of a panic and written into a growable buffer.
The generated reference of std/binary has every public construct with its signature, its doc comment and its examples.
std/binary reads and writes binary formats. A ByteReader is a cursor over Bytes whose every read answers a
Result, so bytes from a file or the network are read without a check in front of each field and cannot make a
program panic; a ByteWriter is a growable buffer with the same typed writes. BitReader and BitWriter do the same
for numbers packed into bits. std/dns, std/compression and std/archive read and write their formats with it. It is
ordinary TorbScript with no native, and the design is BINARY.md.
Import#
use ByteReader, ByteWriter, BitReader, BitWriter, ReadError from "std/binary"use ByteReader, ByteWriter from "std/binary"
var writer = ByteWriter()
writer.uint16 443
writer.varUInt 300
writer.terminatedText "host"
var reader = ByteReader writer.bytes()
print reader.uint16()
print reader.varUInt()
print reader.terminatedText()ByteOrder (of std/core) is re-exported, so a file that names an order needs no second import.
Declarations#
ByteReader#
type ByteReader {
input: Bytes
order: ByteOrder = ByteOrder.BigEndian
fn position(): Int
fn offset(): Int
fn remaining(): Int
var fn seek(position: Int): Result<Void, ReadError>
var fn skip(count: Int): Result<Void, ReadError>
var fn uint8(): Result<UInt8, ReadError>
var fn uint16(order: ByteOrder? = None): Result<UInt16, ReadError>
var fn uint32(order: ByteOrder? = None): Result<UInt32, ReadError>
var fn uint64(order: ByteOrder? = None): Result<UInt64, ReadError>
var fn int8(): Result<Int8, ReadError>
var fn int16(order: ByteOrder? = None): Result<Int16, ReadError>
var fn int32(order: ByteOrder? = None): Result<Int32, ReadError>
var fn int64(order: ByteOrder? = None): Result<Int64, ReadError>
var fn float32(order: ByteOrder? = None): Result<Float32, ReadError>
var fn float64(order: ByteOrder? = None): Result<Float64, ReadError>
var fn varUInt(): Result<UInt64, ReadError>
var fn varInt(encoding: SignedEncoding = SignedEncoding.SignExtended): Result<Int64, ReadError>
var fn bytes(count: Int): Result<Bytes, ReadError>
var fn limited(count: Int): Result<Self, ReadError>
var fn text(count: Int): Result<String, ReadError>
var fn terminatedText(): Result<String, ReadError>
}ByteReader(bytes) reads big endian, the order of network protocols; ByteReader(bytes, order: ByteOrder.LittleEndian)
reads little endian, and every read of a number can name its own order with order: Some(.LittleEndian). Every read
moves the cursor past what it read, and a read that fails moves nothing.
- The numbers answer the width types:
uint16()aUInt16,int32()anInt32in two's complement.Int.from(...)widens every one of them butUInt64. float32()andfloat64()read the IEEE 754 bit patterns and answer aFloat32and aFloat64. Both keep every bit, the payload of ananincluded, so what a writer wrote is what a reader reads back.varUInt()is unsigned LEB128 (protobuf's varint, WebAssembly'su64);varInt()is signed LEB128 as WebAssembly and DWARF write it, andvarInt(encoding: .Zigzag)is protobuf'ssint64. At most 10 bytes; a longer encoding of a small value is read, as both formats allow.bytes(count)answers the next bytes,text(count)the next bytes as UTF-8, andterminatedText()the UTF-8 up to a NUL byte, which it reads too.limited(count)answers a reader over the nextcountbytes only, and moves this one past them: a record whose length comes first is read with it, and a read past its end fails even where the parent has more.position()counts from the start of the reader's own bytes (0 for a new limited reader);offset()counts from the start ofinput- the byte aReadErrornames.seek(position)moves to a position, where the end is one too.
use ByteReader, ReadError from "std/binary"
var reader = ByteReader([0, 5, 104, 101, 108, 108, 111, 33])
const length = Int.from reader.uint16().expect("a length")
var record = reader.limited(length).expect("the record")
print record.text(5)
print reader.uint8()
print(record.uint8() == Fail(ReadError.Truncated(7, "a UInt8", 1, 0)))A ByteReader reads bytes that are all there. Off a socket, std/stream's Reader waits for a whole frame
first - a length of 1 to 8 bytes, then that many bytes, wherever the chunks of the stream end - and the frame is
Bytes for a ByteReader:
use ByteReader, ReadError from "std/binary"
use Reader, FramingError from "std/stream"
type WireError {
case Framing(problem: FramingError)
case Malformed(problem: ReadError)
}
extend WireError with From<FramingError> {
static fn from(value: FramingError): WireError {
WireError.Framing value
}
}
extend WireError with From<ReadError> {
static fn from(value: ReadError): WireError {
WireError.Malformed value
}
}
/** The identifier of the next message: a frame behind two bytes of length, which starts with the identifier. */
fn nextIdentifier(reader: Reader<WireError>): Task<Result<UInt32?, WireError>> {
var reading = reader
match reading.frame(prefix: 2, limit: 1024).await()? {
Some(frame) => {
var fields = ByteReader frame
Ok Some(fields.uint32()?)
}
None => Ok None
}
}ReadError#
type ReadError with Show, Error {
case Truncated(offset: Int, expected: String, needed: Int, available: Int)
case TruncatedBits(offset: Int, needed: Int, available: Int)
case Unterminated(offset: Int, available: Int)
case Overlong(offset: Int, expected: String)
case Overflow(offset: Int, expected: String)
case InvalidText(offset: Int)
case OutOfRange(offset: Int, position: Int, length: Int)
fn offset(): Int
fn show(): String
}Why a read failed, and where: truncated at byte 12: 2 bytes for a UInt16, 1 left. A format turns it into its own
error with an extend ... with From<ReadError>, and ? converts on the way out of every function that answers that
error: DnsError, CompressionError and ArchiveError do.
ByteWriter#
type ByteWriter {
order: ByteOrder = ByteOrder.BigEndian
fn length(): Int
fn bytes(): Bytes
var fn uint8(value: UInt8)
var fn uint16(value: UInt16, order: ByteOrder? = None)
var fn uint32(value: UInt32, order: ByteOrder? = None)
var fn uint64(value: UInt64, order: ByteOrder? = None)
var fn int8(value: Int8)
var fn int16(value: Int16, order: ByteOrder? = None)
var fn int32(value: Int32, order: ByteOrder? = None)
var fn int64(value: Int64, order: ByteOrder? = None)
var fn float32(value: Float32, order: ByteOrder? = None)
var fn float64(value: Float64, order: ByteOrder? = None)
var fn varUInt(value: UInt64)
var fn varInt(value: Int64, encoding: SignedEncoding = SignedEncoding.SignExtended)
var fn append(bytes: Bytes)
var fn text(value: String)
var fn terminatedText(value: String)
var fn reserve16(order: ByteOrder? = None): Mark<UInt16>
var fn reserve32(order: ByteOrder? = None): Mark<UInt32>
fn lengthAfter<Width>(mark: Mark<Width>): Int
var fn patch<Width: Into<Int>>(mark: Mark<Width>, value: Width)
}Nothing here fails. The writes take width types, so a number that does not fit its field is refused where the
caller narrows it - UInt16.tryFrom(port) or port.tryInto() - with the caller's own error. float32 takes a
Float32, which the caller narrows from a Float64 with Float32.tryFrom; that fails for a finite value that rounds
to an infinity, and what to write then is the caller's choice. A length that comes before what it counts is left open
with reserve16() or reserve32() and filled in with patch once lengthAfter(mark) is known:
use ByteWriter from "std/binary"
var writer = ByteWriter()
const mark = writer.reserve16()
writer.text "hello"
writer.patch mark, UInt16.tryFrom(writer.lengthAfter(mark)).expect("a short length")
print writer.bytes()BitReader and BitWriter#
type BitReader {
input: Bytes
order: BitOrder = BitOrder.MostSignificantFirst
fn position(): Int
fn remaining(): Int
var fn bits(width: Int): Result<Int, ReadError>
var fn bit(): Result<Bool, ReadError>
var fn alignToByte()
var fn seek(position: Int): Result<Void, ReadError>
var fn bytes(count: Int): Result<Bytes, ReadError>
}
type BitWriter {
order: BitOrder = BitOrder.MostSignificantFirst
fn length(): Int
var fn bits(value: Int, width: Int)
var fn bit(value: Bool)
var fn alignToByte()
fn bytes(): Bytes
}Numbers of 0 to 56 bits. In BitOrder.MostSignificantFirst - JPEG, H.264, the samples of PNG - the first bit read is
the highest of the number; in BitOrder.LeastSignificantFirst - DEFLATE - it is the lowest. position() of a reader
is the number of bytes it has started, which is where a format goes on after alignToByte(); bytes() of a writer
fills a byte that is not full yet with zero bits and leaves the writer as it was.
use BitOrder, BitReader, BitWriter from "std/binary"
var writer = BitWriter order: BitOrder.LeastSignificantFirst
writer.bit true
writer.bits 1, 2
var reader = BitReader writer.bytes(), order: BitOrder.LeastSignificantFirst
print reader.bit()
print reader.bits(2)Pitfalls#
bytes(count) copies today. A range of a Bytes value reaches the default List.slice, which copies the items,
where ArrayList's own range would share the storage (BINARY.md section 4). A format that only
looks at a record reads it through limited(count), which copies nothing.
The order of a single read is Some(...). order: .LittleEndian does not find the case through the Option the
parameter is; write order: Some(.LittleEndian), or make the reader little endian.
A width outside 0 to 56 is a panic of bits, because it is the caller's mistake and not the data's.
Related#
- std/dns, std/compression and std/archive - the formats written with it.
- std/stream -
Bytes, andtextOf, whichtextandterminatedTextdecode with. - The standard library - the other packages.