// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// A cursor over one decoded MySQL packet payload. Every field type the protocol
/// uses — fixed-width little-endian integers, length-encoded integers and strings,
/// NUL-terminated strings, and raw byte runs — is read through this, advancing a
/// position and raising [`MysqlError::ProtocolError`] on any short read. It holds
/// no socket: the transport reads a full payload into `Bytes` first, then parses
/// it here, which is what lets the whole codec be tested off any backend.
pub struct PacketReader {
data : Bytes
mut pos : Int
}
///|
/// Wrap a decoded packet payload for reading from the start.
pub fn PacketReader::new(data : Bytes) -> PacketReader {
{ data, pos: 0 }
}
///|
/// Bytes not yet consumed.
pub fn PacketReader::remaining(self : PacketReader) -> Int {
self.data.length() - self.pos
}
///|
/// Whether the cursor has consumed the whole payload.
pub fn PacketReader::at_end(self : PacketReader) -> Bool {
self.pos >= self.data.length()
}
///|
/// Peek the next byte without advancing; `-1` at end of payload.
pub fn PacketReader::peek(self : PacketReader) -> Int {
if self.pos >= self.data.length() {
-1
} else {
self.data[self.pos].to_int()
}
}
///|
fn PacketReader::need(self : PacketReader, n : Int) -> Unit raise MysqlError {
if self.pos + n > self.data.length() {
raise ProtocolError(
"short packet: need " +
n.to_string() +
" byte(s) at offset " +
self.pos.to_string() +
", have " +
self.data.length().to_string(),
)
}
}
///|
/// Read one byte as an `Int` in `0..=255`.
pub fn PacketReader::u8(self : PacketReader) -> Int raise MysqlError {
self.need(1)
let v = self.data[self.pos].to_int()
self.pos += 1
v
}
///|
/// Read an `n`-byte little-endian unsigned integer.
pub fn PacketReader::uint_le(
self : PacketReader,
n : Int,
) -> Int64 raise MysqlError {
self.need(n)
let mut v = 0L
for i in 0.. Bytes raise MysqlError {
self.need(n)
let s = self.data[self.pos:self.pos + n].to_owned()
self.pos += n
s
}
///|
/// Skip `n` bytes.
pub fn PacketReader::skip(
self : PacketReader,
n : Int,
) -> Unit raise MysqlError {
self.need(n)
self.pos += n
}
///|
/// Read a length-encoded unsigned integer (the `int` type). The `0xFB`
/// NULL sentinel and `0xFF` are rejected here — NULL only has meaning inside a
/// row, which [`lenenc_bytes`] handles.
pub fn PacketReader::lenenc_uint(self : PacketReader) -> Int64 raise MysqlError {
let first = self.u8()
if first < 0xFB {
first.to_int64()
} else if first == 0xFC {
self.uint_le(2)
} else if first == 0xFD {
self.uint_le(3)
} else if first == 0xFE {
self.uint_le(8)
} else {
raise ProtocolError(
"invalid length-encoded integer prefix " + first.to_string(),
)
}
}
///|
/// Read a length-encoded string (the `string` type), returning `None`
/// for the `0xFB` NULL sentinel that appears in text-protocol rows.
pub fn PacketReader::lenenc_bytes(
self : PacketReader,
) -> Bytes? raise MysqlError {
self.need(1)
if self.data[self.pos].to_int() == 0xFB {
self.pos += 1
None
} else {
let len = self.lenenc_uint().to_int()
Some(self.bytes(len))
}
}
///|
/// Read a NUL-terminated string, consuming the terminator.
pub fn PacketReader::string_nul(self : PacketReader) -> Bytes raise MysqlError {
let start = self.pos
while self.pos < self.data.length() && self.data[self.pos].to_int() != 0 {
self.pos += 1
}
if self.pos >= self.data.length() {
raise ProtocolError("unterminated NUL-string")
}
let s = self.data[start:self.pos].to_owned()
self.pos += 1
s
}
///|
/// Read everything left in the payload (an `EOF`-terminated string field).
pub fn PacketReader::rest(self : PacketReader) -> Bytes {
let s = self.data[self.pos:].to_owned()
self.pos = self.data.length()
s
}
// --- writers (compose a payload into a Buffer) --------------------------------
///|
/// Append an `n`-byte little-endian unsigned integer.
pub fn put_uint_le(buf : Buffer, v : Int64, n : Int) -> Unit {
for i in 0..> (i * 8)) & 0xFFL).to_int().to_byte())
}
}
///|
/// Append a length-encoded unsigned integer.
pub fn put_lenenc_uint(buf : Buffer, v : Int64) -> Unit {
if v < 0xFBL {
buf.write_byte(v.to_int().to_byte())
} else if v < 0x10000L {
buf.write_byte(b'\xFC')
put_uint_le(buf, v, 2)
} else if v < 0x1000000L {
buf.write_byte(b'\xFD')
put_uint_le(buf, v, 3)
} else {
buf.write_byte(b'\xFE')
put_uint_le(buf, v, 8)
}
}
///|
/// Append raw bytes followed by a NUL terminator.
pub fn put_string_nul(buf : Buffer, s : Bytes) -> Unit {
buf.write_bytes(s[:])
buf.write_byte(b'\x00')
}
///|
/// Append a length-encoded string (its `lenenc` length prefix, then the bytes).
pub fn put_lenenc_bytes(buf : Buffer, s : Bytes) -> Unit {
put_lenenc_uint(buf, s.length().to_int64())
buf.write_bytes(s[:])
}
///|
/// Concatenate two byte strings.
pub fn concat_bytes(a : Bytes, b : Bytes) -> Bytes {
let buf = Buffer()
buf.write_bytes(a[:])
buf.write_bytes(b[:])
buf.to_bytes()
}