use bytes::{BufMut, BytesMut};
use std::fmt;
#[inline]
fn copy_len(n: usize) -> u32 {
u32::try_from(n).expect("HyperBinary COPY value length exceeds u32::MAX")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CopyReadError {
BufferTooShort {
type_name: &'static str,
expected: usize,
actual: usize,
},
LengthExceedsBuffer {
declared: usize,
available: usize,
},
}
impl fmt::Display for CopyReadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CopyReadError::BufferTooShort {
type_name,
expected,
actual,
} => write!(
f,
"Buffer too short for {type_name}: expected {expected} bytes, got {actual}"
),
CopyReadError::LengthExceedsBuffer {
declared,
available,
} => write!(
f,
"Declared length {declared} exceeds available buffer space {available}"
),
}
}
}
impl std::error::Error for CopyReadError {}
pub const HYPER_BINARY_SIGNATURE: &[u8] = b"HPRCPY";
pub const HYPER_BINARY_HEADER: &[u8] = &[
b'H', b'P', b'R', b'C', b'P', b'Y', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
pub const HYPER_BINARY_HEADER_SIZE: usize = 19;
pub const HYPER_BINARY_FORMAT: u8 = 2;
#[inline]
pub fn write_header(buf: &mut BytesMut) {
buf.put_slice(HYPER_BINARY_HEADER);
}
#[inline]
pub fn write_tuple_start(_buf: &mut BytesMut, _field_count: i16) {
}
#[inline]
pub fn write_trailer(_buf: &mut BytesMut) {
}
#[inline]
pub fn write_null(buf: &mut BytesMut) {
buf.put_u8(1); }
#[inline]
pub fn write_i8(buf: &mut BytesMut, value: i8) {
buf.put_u8(0); buf.put_i8(value);
}
#[inline]
pub fn write_i8_not_null(buf: &mut BytesMut, value: i8) {
buf.put_i8(value);
}
#[inline]
pub fn write_i16(buf: &mut BytesMut, value: i16) {
buf.put_u8(0); buf.put_i16_le(value);
}
#[inline]
pub fn write_i16_not_null(buf: &mut BytesMut, value: i16) {
buf.put_i16_le(value);
}
#[inline]
pub fn write_i32(buf: &mut BytesMut, value: i32) {
buf.put_u8(0); buf.put_i32_le(value);
}
#[inline]
pub fn write_i32_not_null(buf: &mut BytesMut, value: i32) {
buf.put_i32_le(value);
}
#[inline]
pub fn write_i64(buf: &mut BytesMut, value: i64) {
buf.put_u8(0); buf.put_i64_le(value);
}
#[inline]
pub fn write_i64_not_null(buf: &mut BytesMut, value: i64) {
buf.put_i64_le(value);
}
#[inline]
pub fn write_data128(buf: &mut BytesMut, value: &[u8; 16]) {
buf.put_u8(0); buf.put_slice(value);
}
#[inline]
pub fn write_data128_not_null(buf: &mut BytesMut, value: &[u8; 16]) {
buf.put_slice(value);
}
#[inline]
pub fn write_varbinary(buf: &mut BytesMut, data: &[u8]) {
buf.put_u8(0); buf.put_u32_le(copy_len(data.len()));
buf.put_slice(data);
}
#[inline]
pub fn write_varbinary_not_null(buf: &mut BytesMut, data: &[u8]) {
buf.put_u32_le(copy_len(data.len()));
buf.put_slice(data);
}
#[inline]
pub fn write_f32(buf: &mut BytesMut, value: f32) {
buf.put_u8(0); buf.put_f32_le(value);
}
#[inline]
pub fn write_f32_not_null(buf: &mut BytesMut, value: f32) {
buf.put_f32_le(value);
}
#[inline]
pub fn write_f64(buf: &mut BytesMut, value: f64) {
buf.put_u8(0); buf.put_f64_le(value);
}
#[inline]
pub fn write_f64_not_null(buf: &mut BytesMut, value: f64) {
buf.put_f64_le(value);
}
#[inline]
#[must_use]
pub fn read_i8(buf: &[u8]) -> i8 {
#[expect(
clippy::cast_possible_wrap,
reason = "intentional i8 bit-pattern reinterpret; inverse of i8 write"
)]
let value = buf[0] as i8;
value
}
#[inline]
pub fn read_i16(buf: &[u8]) -> Result<i16, CopyReadError> {
if buf.len() < 2 {
return Err(CopyReadError::BufferTooShort {
type_name: "i16",
expected: 2,
actual: buf.len(),
});
}
Ok(i16::from_le_bytes([buf[0], buf[1]]))
}
#[inline]
pub fn read_i32(buf: &[u8]) -> Result<i32, CopyReadError> {
if buf.len() < 4 {
return Err(CopyReadError::BufferTooShort {
type_name: "i32",
expected: 4,
actual: buf.len(),
});
}
Ok(i32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]))
}
#[inline]
pub fn read_i64(buf: &[u8]) -> Result<i64, CopyReadError> {
if buf.len() < 8 {
return Err(CopyReadError::BufferTooShort {
type_name: "i64",
expected: 8,
actual: buf.len(),
});
}
Ok(i64::from_le_bytes([
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7],
]))
}
#[inline]
pub fn read_data128(buf: &[u8]) -> Result<[u8; 16], CopyReadError> {
if buf.len() < 16 {
return Err(CopyReadError::BufferTooShort {
type_name: "data128",
expected: 16,
actual: buf.len(),
});
}
let mut result = [0u8; 16];
result.copy_from_slice(&buf[0..16]);
Ok(result)
}
#[inline]
pub fn read_varbinary(buf: &[u8]) -> Result<&[u8], CopyReadError> {
if buf.len() < 4 {
return Err(CopyReadError::BufferTooShort {
type_name: "varbinary length field",
expected: 4,
actual: buf.len(),
});
}
let len_u32 = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
let Ok(len) = usize::try_from(len_u32) else {
return Err(CopyReadError::LengthExceedsBuffer {
declared: len_u32 as usize,
available: buf.len().saturating_sub(4),
});
};
let available = buf.len() - 4;
if available < len {
return Err(CopyReadError::LengthExceedsBuffer {
declared: len,
available,
});
}
let Some(end) = 4_usize.checked_add(len) else {
return Err(CopyReadError::LengthExceedsBuffer {
declared: len,
available,
});
};
Ok(&buf[4..end])
}
#[derive(Debug)]
pub struct CopyDataBuilder {
buffer: BytesMut,
header_written: bool,
}
impl CopyDataBuilder {
#[must_use]
pub fn new(capacity: usize) -> Self {
CopyDataBuilder {
buffer: BytesMut::with_capacity(capacity),
header_written: false,
}
}
#[must_use]
pub fn with_default_capacity() -> Self {
Self::new(1024 * 1024)
}
pub fn ensure_header(&mut self) {
if !self.header_written {
write_header(&mut self.buffer);
self.header_written = true;
}
}
pub fn write_null(&mut self) {
self.ensure_header();
write_null(&mut self.buffer);
}
pub fn write_bool(&mut self, value: bool, nullable: bool) {
self.ensure_header();
let int_value = i8::from(value);
if nullable {
write_i8(&mut self.buffer, int_value);
} else {
write_i8_not_null(&mut self.buffer, int_value);
}
}
pub fn write_i16(&mut self, value: i16, nullable: bool) {
self.ensure_header();
if nullable {
write_i16(&mut self.buffer, value);
} else {
write_i16_not_null(&mut self.buffer, value);
}
}
pub fn write_i32(&mut self, value: i32, nullable: bool) {
self.ensure_header();
if nullable {
write_i32(&mut self.buffer, value);
} else {
write_i32_not_null(&mut self.buffer, value);
}
}
pub fn write_i64(&mut self, value: i64, nullable: bool) {
self.ensure_header();
if nullable {
write_i64(&mut self.buffer, value);
} else {
write_i64_not_null(&mut self.buffer, value);
}
}
pub fn write_data128(&mut self, value: &[u8; 16], nullable: bool) {
self.ensure_header();
if nullable {
write_data128(&mut self.buffer, value);
} else {
write_data128_not_null(&mut self.buffer, value);
}
}
pub fn write_varbinary(&mut self, value: &[u8], nullable: bool) {
self.ensure_header();
if nullable {
write_varbinary(&mut self.buffer, value);
} else {
write_varbinary_not_null(&mut self.buffer, value);
}
}
pub fn write_str(&mut self, value: &str, nullable: bool) {
self.write_varbinary(value.as_bytes(), nullable);
}
#[must_use]
pub fn len(&self) -> usize {
self.buffer.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.buffer.is_empty()
}
pub fn take(&mut self) -> BytesMut {
self.header_written = false;
std::mem::take(&mut self.buffer)
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.buffer
}
pub fn clear(&mut self) {
self.buffer.clear();
self.header_written = false;
}
}
impl Default for CopyDataBuilder {
fn default() -> Self {
Self::with_default_capacity()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_header() {
let mut buf = BytesMut::new();
write_header(&mut buf);
assert_eq!(buf.as_ref(), HYPER_BINARY_HEADER);
assert_eq!(buf.len(), HYPER_BINARY_HEADER_SIZE);
assert_eq!(&buf[..6], b"HPRCPY");
}
#[test]
fn test_i32_little_endian() {
let mut buf = BytesMut::new();
write_i32_not_null(&mut buf, 0x01020304);
assert_eq!(buf.as_ref(), &[0x04, 0x03, 0x02, 0x01]);
}
#[test]
fn test_nullable_value() {
let mut buf = BytesMut::new();
write_i32(&mut buf, 42);
assert_eq!(buf.as_ref(), &[0, 42, 0, 0, 0]);
}
#[test]
fn test_null() {
let mut buf = BytesMut::new();
write_null(&mut buf);
assert_eq!(buf.as_ref(), &[1]);
}
#[test]
fn test_varbinary() {
let mut buf = BytesMut::new();
write_varbinary_not_null(&mut buf, b"hello");
assert_eq!(buf.as_ref(), &[5, 0, 0, 0, b'h', b'e', b'l', b'l', b'o']);
}
#[test]
fn test_varbinary_nullable() {
let mut buf = BytesMut::new();
write_varbinary(&mut buf, b"hi");
assert_eq!(buf.as_ref(), &[0, 2, 0, 0, 0, b'h', b'i']);
}
#[test]
fn test_read_i32() {
let buf = [0x04, 0x03, 0x02, 0x01];
assert_eq!(read_i32(&buf).unwrap(), 0x01020304);
}
#[test]
fn test_read_i32_too_short() {
let buf = [0x04, 0x03, 0x02]; let err = read_i32(&buf).unwrap_err();
assert!(matches!(
err,
CopyReadError::BufferTooShort {
type_name: "i32",
expected: 4,
actual: 3
}
));
}
#[test]
fn test_read_i16() {
let buf = [0x34, 0x12];
assert_eq!(read_i16(&buf).unwrap(), 0x1234);
}
#[test]
fn test_read_i16_too_short() {
let buf = [0x34]; let err = read_i16(&buf).unwrap_err();
assert!(matches!(
err,
CopyReadError::BufferTooShort {
type_name: "i16",
expected: 2,
actual: 1
}
));
}
#[test]
fn test_read_i64() {
let buf = [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01];
assert_eq!(read_i64(&buf).unwrap(), 0x0102030405060708);
}
#[test]
fn test_read_i64_too_short() {
let buf = [0x08, 0x07, 0x06, 0x05]; let err = read_i64(&buf).unwrap_err();
assert!(matches!(
err,
CopyReadError::BufferTooShort {
type_name: "i64",
expected: 8,
actual: 4
}
));
}
#[test]
fn test_read_varbinary_valid() {
let buf = [0x05, 0x00, 0x00, 0x00, b'h', b'e', b'l', b'l', b'o'];
let result = read_varbinary(&buf).unwrap();
assert_eq!(result, b"hello");
}
#[test]
fn test_read_varbinary_length_field_too_short() {
let buf = [0x05, 0x00, 0x00]; let err = read_varbinary(&buf).unwrap_err();
assert!(matches!(
err,
CopyReadError::BufferTooShort {
type_name: "varbinary length field",
expected: 4,
actual: 3
}
));
}
#[test]
fn test_read_varbinary_data_too_short() {
let buf = [0x05, 0x00, 0x00, 0x00, b'h', b'e']; let err = read_varbinary(&buf).unwrap_err();
assert!(matches!(
err,
CopyReadError::LengthExceedsBuffer {
declared: 5,
available: 2
}
));
}
#[test]
fn read_varbinary_zero_length() {
let buf = [0x00, 0x00, 0x00, 0x00];
let bytes = read_varbinary(&buf).unwrap();
assert!(bytes.is_empty());
}
#[test]
fn read_varbinary_exact_fit() {
let buf = [0x03, 0x00, 0x00, 0x00, b'a', b'b', b'c'];
let bytes = read_varbinary(&buf).unwrap();
assert_eq!(bytes, b"abc");
}
#[test]
fn read_varbinary_rejects_huge_declared_length_on_short_buf() {
let buf = [0xFF, 0xFF, 0xFF, 0xFF, b'h', b'i'];
let err = read_varbinary(&buf).unwrap_err();
assert!(matches!(err, CopyReadError::LengthExceedsBuffer { .. }));
}
#[test]
fn test_copy_read_error_display() {
let err = CopyReadError::BufferTooShort {
type_name: "i32",
expected: 4,
actual: 2,
};
assert_eq!(
err.to_string(),
"Buffer too short for i32: expected 4 bytes, got 2"
);
let err = CopyReadError::LengthExceedsBuffer {
declared: 100,
available: 10,
};
assert_eq!(
err.to_string(),
"Declared length 100 exceeds available buffer space 10"
);
}
#[test]
fn test_copy_data_builder() {
let mut builder = CopyDataBuilder::new(1024);
builder.write_i32(42, false);
builder.write_str("hello", false);
let data = builder.as_bytes();
assert!(data.starts_with(HYPER_BINARY_HEADER));
}
#[test]
fn test_f64_not_null() {
let mut buf = BytesMut::new();
write_f64_not_null(&mut buf, std::f64::consts::PI);
assert_eq!(buf.len(), 8);
let read_value = f64::from_le_bytes([
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7],
]);
assert!((read_value - std::f64::consts::PI).abs() < 1e-10);
}
}