// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// Concatenate two byte strings. A small helper the MD5 auth path and message
/// framing lean on; MoonBit's `Bytes` is immutable so this allocates once.
pub fn concat_bytes(a : Bytes, b : Bytes) -> Bytes {
let buf = Buffer()
buf.write_bytes(a[:])
buf.write_bytes(b[:])
buf.to_bytes()
}
///|
/// A forward cursor over a message payload. The backend framing hands us one
/// `Bytes` per message body (type byte and length already consumed) and every
/// field is read in order, so a single moving offset is all the decoder needs.
/// Reads past the end raise `QueryError` rather than reading garbage — a
/// truncated or malformed backend message surfaces as a protocol error.
priv struct ByteReader {
data : Bytes
mut pos : Int
}
///|
fn ByteReader::new(data : Bytes) -> ByteReader {
{ data, pos: 0 }
}
///|
fn ByteReader::u8(self : ByteReader) -> Int raise DbError {
guard self.pos < self.data.length() else {
raise QueryError("protocol: read past end of message")
}
let v = self.data[self.pos].to_int()
self.pos += 1
v
}
///|
/// Read a big-endian signed 16-bit integer (network byte order, as every
/// multi-byte field in the PostgreSQL wire protocol is encoded).
fn ByteReader::i16(self : ByteReader) -> Int raise DbError {
let hi = self.u8()
let lo = self.u8()
let v = (hi << 8) | lo
if v >= 0x8000 {
v - 0x10000
} else {
v
}
}
///|
/// Read a big-endian signed 32-bit integer.
fn ByteReader::i32(self : ByteReader) -> Int raise DbError {
let b0 = self.u8()
let b1 = self.u8()
let b2 = self.u8()
let b3 = self.u8()
(b0 << 24) | (b1 << 16) | (b2 << 8) | b3
}
///|
/// Read exactly `n` raw bytes.
fn ByteReader::take(self : ByteReader, n : Int) -> Bytes raise DbError {
guard n >= 0 && self.pos + n <= self.data.length() else {
raise QueryError("protocol: read past end of message")
}
let out = self.data[self.pos:self.pos + n].to_owned()
self.pos += n
out
}
///|
/// Read a C string: bytes up to (and consuming) the next `NUL`, decoded UTF-8.
fn ByteReader::cstring(self : ByteReader) -> String raise DbError {
let start = self.pos
for i = self.pos; i < self.data.length(); i = i + 1 {
if self.data[i] == 0 {
let s = utf8_decode(self.data[start:i].to_owned())
self.pos = i + 1
return s
}
}
raise QueryError("protocol: unterminated C string")
}
///|
/// Decode UTF-8 bytes to a `String`. PostgreSQL text values, column names, and
/// error fields all arrive UTF-8 (the startup message negotiates
/// `client_encoding`). Decodes the ASCII fast path directly and multi-byte
/// sequences by code point; malformed input yields U+FFFD rather than raising,
/// matching a lenient text codec.
pub fn utf8_decode(data : Bytes) -> String {
let sb = StringBuilder::new()
let n = data.length()
let mut i = 0
while i < n {
let b0 = data[i].to_int()
if b0 < 0x80 {
sb.write_char(b0.unsafe_to_char())
i += 1
} else if b0 >= 0xc0 && b0 < 0xe0 && i + 1 < n {
let cp = ((b0 & 0x1f) << 6) | (data[i + 1].to_int() & 0x3f)
sb.write_char(cp.unsafe_to_char())
i += 2
} else if b0 >= 0xe0 && b0 < 0xf0 && i + 2 < n {
let cp = ((b0 & 0x0f) << 12) |
((data[i + 1].to_int() & 0x3f) << 6) |
(data[i + 2].to_int() & 0x3f)
sb.write_char(cp.unsafe_to_char())
i += 3
} else if b0 >= 0xf0 && i + 3 < n {
let cp = ((b0 & 0x07) << 18) |
((data[i + 1].to_int() & 0x3f) << 12) |
((data[i + 2].to_int() & 0x3f) << 6) |
(data[i + 3].to_int() & 0x3f)
sb.write_char(cp.unsafe_to_char())
i += 4
} else {
sb.write_char('\u{FFFD}')
i += 1
}
}
sb.to_string()
}