hyperdb_api_core/protocol/copy.rs
1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! `HyperBinary` COPY format support.
5//!
6//! This module provides support for Hyper's binary COPY format (`HyperBinary`),
7//! which differs from standard `PostgreSQL` binary COPY in several ways
8//! optimized for throughput.
9//!
10//! # Format Overview
11//!
12//! The `HyperBinary` format consists of:
13//! - **Header**: `"HPRCPY"` + 13 null bytes (19 bytes total)
14//! - **Data**: Values with null indicators (1 byte each for nullable columns)
15//! - All values are **little-endian** encoded
16//! - No tuple start markers (unlike `PostgreSQL`)
17//!
18//! For nullable columns: `[1 byte null indicator (0=not null, 1=null)] + [value]`
19//! For non-nullable columns: just `[value]` (no indicator)
20//!
21//! # Differences from `PostgreSQL` Binary COPY
22//!
23//! | Aspect | `PostgreSQL` | `HyperBinary` |
24//! |---|---|---|
25//! | Header signature | `"PGCOPY\n\xff\r\n\0"` (11 bytes) + flags + ext | `"HPRCPY"` + 13 null bytes (19 bytes) |
26//! | Byte order | **Big-endian** (network order) | **Little-endian** (x86 native) |
27//! | Row framing | 2-byte field count per row | No per-row framing |
28//! | Null encoding | `-1` length prefix (4 bytes) | 1-byte indicator on nullable columns only |
29//! | Non-null values | 4-byte length prefix + data | Raw data (fixed-size) or length-prefixed (variable) |
30//! | Trailer | 2-byte `-1` sentinel | None |
31//!
32//! The little-endian encoding and absence of per-row framing reduce both
33//! encoding overhead and byte-swapping on x86/ARM-LE architectures, which
34//! is where Hyper is typically deployed.
35
36use bytes::{BufMut, BytesMut};
37use std::fmt;
38
39/// Narrows a `usize` length to the `HyperBinary` 4-byte little-endian length
40/// prefix. Panics on overflow; individual COPY values >`u32::MAX` bytes are a
41/// programming error per the `HyperBinary` format contract.
42#[inline]
43fn copy_len(n: usize) -> u32 {
44 u32::try_from(n).expect("HyperBinary COPY value length exceeds u32::MAX")
45}
46
47// =============================================================================
48// Error Types
49// =============================================================================
50
51/// Errors that can occur when reading `HyperBinary` COPY data.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum CopyReadError {
54 /// Buffer is too short to read the expected type.
55 BufferTooShort {
56 /// The type being read (e.g., "i16", "i32", "varbinary").
57 type_name: &'static str,
58 /// The expected minimum buffer size.
59 expected: usize,
60 /// The actual buffer size.
61 actual: usize,
62 },
63 /// The declared length in a variable-length field exceeds available data.
64 LengthExceedsBuffer {
65 /// The declared length from the length prefix.
66 declared: usize,
67 /// The available buffer space after the length prefix.
68 available: usize,
69 },
70}
71
72impl fmt::Display for CopyReadError {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 match self {
75 CopyReadError::BufferTooShort {
76 type_name,
77 expected,
78 actual,
79 } => write!(
80 f,
81 "Buffer too short for {type_name}: expected {expected} bytes, got {actual}"
82 ),
83 CopyReadError::LengthExceedsBuffer {
84 declared,
85 available,
86 } => write!(
87 f,
88 "Declared length {declared} exceeds available buffer space {available}"
89 ),
90 }
91 }
92}
93
94impl std::error::Error for CopyReadError {}
95
96/// The `HyperBinary` COPY signature ("HPRCPY" + 13 null bytes).
97pub const HYPER_BINARY_SIGNATURE: &[u8] = b"HPRCPY";
98
99/// The full `HyperBinary` COPY header (19 bytes).
100pub const HYPER_BINARY_HEADER: &[u8] = &[
101 // Signature: "HPRCPY" (6 bytes)
102 b'H', b'P', b'R', b'C', b'P', b'Y', // Padding: 13 null bytes to make total 19 bytes
103 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
104];
105
106/// Size of the `HyperBinary` header (19 bytes).
107pub const HYPER_BINARY_HEADER_SIZE: usize = 19;
108
109/// The `HyperBinary` data format identifier for Hyper's protocol.
110pub const HYPER_BINARY_FORMAT: u8 = 2;
111
112/// Writes the `HyperBinary` COPY header to a buffer.
113///
114/// This should be written at the start of a COPY data stream.
115#[inline]
116pub fn write_header(buf: &mut BytesMut) {
117 buf.put_slice(HYPER_BINARY_HEADER);
118}
119
120/// Writes a tuple start marker (no-op for `HyperBinary`).
121///
122/// `HyperBinary` format does not use tuple start markers.
123/// This is kept for API compatibility but does nothing.
124#[inline]
125pub fn write_tuple_start(_buf: &mut BytesMut, _field_count: i16) {
126 // HyperBinary does not use tuple start markers
127}
128
129/// Writes the COPY trailer (no-op for `HyperBinary`).
130///
131/// `HyperBinary` format does not require a trailer.
132/// This is kept for API compatibility but does nothing.
133#[inline]
134pub fn write_trailer(_buf: &mut BytesMut) {
135 // HyperBinary does not use a trailer
136}
137
138/// Writes a NULL value (1 byte with value 1).
139#[inline]
140pub fn write_null(buf: &mut BytesMut) {
141 buf.put_u8(1); // 1 indicates NULL
142}
143
144/// Writes an i8 value (bool) for a nullable column.
145/// Format: [not-null indicator: 1 byte (0)][value: 1 byte]
146#[inline]
147pub fn write_i8(buf: &mut BytesMut, value: i8) {
148 buf.put_u8(0); // not-null indicator
149 buf.put_i8(value);
150}
151
152/// Writes an i8 value for a non-nullable column.
153/// Format: [value: 1 byte] (no null indicator)
154#[inline]
155pub fn write_i8_not_null(buf: &mut BytesMut, value: i8) {
156 buf.put_i8(value);
157}
158
159/// Writes an i16 value (SMALLINT) for a nullable column.
160/// Format: [not-null indicator: 1 byte (0)][value: 2 bytes LittleEndian]
161#[inline]
162pub fn write_i16(buf: &mut BytesMut, value: i16) {
163 buf.put_u8(0); // not-null indicator
164 buf.put_i16_le(value);
165}
166
167/// Writes an i16 value for a non-nullable column.
168/// Format: [value: 2 bytes `LittleEndian`]
169#[inline]
170pub fn write_i16_not_null(buf: &mut BytesMut, value: i16) {
171 buf.put_i16_le(value);
172}
173
174/// Writes an i32 value (INT) for a nullable column.
175/// Format: [not-null indicator: 1 byte (0)][value: 4 bytes LittleEndian]
176#[inline]
177pub fn write_i32(buf: &mut BytesMut, value: i32) {
178 buf.put_u8(0); // not-null indicator
179 buf.put_i32_le(value);
180}
181
182/// Writes an i32 value for a non-nullable column.
183/// Format: [value: 4 bytes `LittleEndian`]
184#[inline]
185pub fn write_i32_not_null(buf: &mut BytesMut, value: i32) {
186 buf.put_i32_le(value);
187}
188
189/// Writes an i64 value (BIGINT) for a nullable column.
190/// Format: [not-null indicator: 1 byte (0)][value: 8 bytes LittleEndian]
191#[inline]
192pub fn write_i64(buf: &mut BytesMut, value: i64) {
193 buf.put_u8(0); // not-null indicator
194 buf.put_i64_le(value);
195}
196
197/// Writes an i64 value for a non-nullable column.
198/// Format: [value: 8 bytes `LittleEndian`]
199#[inline]
200pub fn write_i64_not_null(buf: &mut BytesMut, value: i64) {
201 buf.put_i64_le(value);
202}
203
204/// Writes a 128-bit value (NUMERIC, INTERVAL) for a nullable column.
205/// Format: [not-null indicator: 1 byte (0)][value: 16 bytes LittleEndian]
206#[inline]
207pub fn write_data128(buf: &mut BytesMut, value: &[u8; 16]) {
208 buf.put_u8(0); // not-null indicator
209 buf.put_slice(value);
210}
211
212/// Writes a 128-bit value for a non-nullable column.
213/// Format: [value: 16 bytes `LittleEndian`]
214#[inline]
215pub fn write_data128_not_null(buf: &mut BytesMut, value: &[u8; 16]) {
216 buf.put_slice(value);
217}
218
219/// Writes a variable-length binary value (TEXT, BYTEA) for a nullable column.
220/// Format: [not-null indicator: 1 byte (0)][length: 4 bytes LittleEndian][data: N bytes]
221#[inline]
222pub fn write_varbinary(buf: &mut BytesMut, data: &[u8]) {
223 buf.put_u8(0); // not-null indicator
224 buf.put_u32_le(copy_len(data.len()));
225 buf.put_slice(data);
226}
227
228/// Writes a variable-length binary value for a non-nullable column.
229/// Format: [length: 4 bytes `LittleEndian`][data: N bytes]
230#[inline]
231pub fn write_varbinary_not_null(buf: &mut BytesMut, data: &[u8]) {
232 buf.put_u32_le(copy_len(data.len()));
233 buf.put_slice(data);
234}
235
236/// Writes an f32 value (REAL/FLOAT4) for a nullable column.
237/// Format: [not-null indicator: 1 byte (0)][value: 4 bytes LittleEndian]
238#[inline]
239pub fn write_f32(buf: &mut BytesMut, value: f32) {
240 buf.put_u8(0); // not-null indicator
241 buf.put_f32_le(value);
242}
243
244/// Writes an f32 value for a non-nullable column.
245/// Format: [value: 4 bytes `LittleEndian`]
246#[inline]
247pub fn write_f32_not_null(buf: &mut BytesMut, value: f32) {
248 buf.put_f32_le(value);
249}
250
251/// Writes an f64 value (DOUBLE PRECISION/FLOAT8) for a nullable column.
252/// Format: [not-null indicator: 1 byte (0)][value: 8 bytes LittleEndian]
253#[inline]
254pub fn write_f64(buf: &mut BytesMut, value: f64) {
255 buf.put_u8(0); // not-null indicator
256 buf.put_f64_le(value);
257}
258
259/// Writes an f64 value for a non-nullable column.
260/// Format: [value: 8 bytes `LittleEndian`]
261#[inline]
262pub fn write_f64_not_null(buf: &mut BytesMut, value: f64) {
263 buf.put_f64_le(value);
264}
265
266/// Reads an i8 value from `HyperBinary` format (`LittleEndian`).
267#[inline]
268#[must_use]
269pub fn read_i8(buf: &[u8]) -> i8 {
270 // Bit-pattern reinterpret: inverse of whatever wrote the byte.
271 #[expect(
272 clippy::cast_possible_wrap,
273 reason = "intentional i8 bit-pattern reinterpret; inverse of i8 write"
274 )]
275 let value = buf[0] as i8;
276 value
277}
278
279/// Reads an i16 value from `HyperBinary` format (`LittleEndian`).
280///
281/// # Errors
282///
283/// Returns [`CopyReadError::BufferTooShort`] if the buffer is too short (< 2 bytes).
284///
285/// # Example
286///
287/// ```
288/// use hyperdb_api_core::protocol::copy::{read_i16, CopyReadError};
289///
290/// let buf = [0x34, 0x12];
291/// assert_eq!(read_i16(&buf).unwrap(), 0x1234);
292///
293/// let short_buf = [0x34];
294/// assert!(matches!(read_i16(&short_buf), Err(CopyReadError::BufferTooShort { .. })));
295/// ```
296#[inline]
297pub fn read_i16(buf: &[u8]) -> Result<i16, CopyReadError> {
298 if buf.len() < 2 {
299 return Err(CopyReadError::BufferTooShort {
300 type_name: "i16",
301 expected: 2,
302 actual: buf.len(),
303 });
304 }
305 Ok(i16::from_le_bytes([buf[0], buf[1]]))
306}
307
308/// Reads an i32 value from `HyperBinary` format (`LittleEndian`).
309///
310/// # Errors
311///
312/// Returns [`CopyReadError::BufferTooShort`] if the buffer is too short (< 4 bytes).
313///
314/// # Example
315///
316/// ```
317/// use hyperdb_api_core::protocol::copy::{read_i32, CopyReadError};
318///
319/// let buf = [0x04, 0x03, 0x02, 0x01];
320/// assert_eq!(read_i32(&buf).unwrap(), 0x01020304);
321///
322/// let short_buf = [0x04, 0x03, 0x02];
323/// assert!(matches!(read_i32(&short_buf), Err(CopyReadError::BufferTooShort { .. })));
324/// ```
325#[inline]
326pub fn read_i32(buf: &[u8]) -> Result<i32, CopyReadError> {
327 if buf.len() < 4 {
328 return Err(CopyReadError::BufferTooShort {
329 type_name: "i32",
330 expected: 4,
331 actual: buf.len(),
332 });
333 }
334 Ok(i32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]))
335}
336
337/// Reads an i64 value from `HyperBinary` format (`LittleEndian`).
338///
339/// # Errors
340///
341/// Returns [`CopyReadError::BufferTooShort`] if the buffer is too short (< 8 bytes).
342///
343/// # Example
344///
345/// ```
346/// use hyperdb_api_core::protocol::copy::{read_i64, CopyReadError};
347///
348/// let buf = [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01];
349/// assert_eq!(read_i64(&buf).unwrap(), 0x0102030405060708);
350///
351/// let short_buf = [0x08, 0x07, 0x06, 0x05];
352/// assert!(matches!(read_i64(&short_buf), Err(CopyReadError::BufferTooShort { .. })));
353/// ```
354#[inline]
355pub fn read_i64(buf: &[u8]) -> Result<i64, CopyReadError> {
356 if buf.len() < 8 {
357 return Err(CopyReadError::BufferTooShort {
358 type_name: "i64",
359 expected: 8,
360 actual: buf.len(),
361 });
362 }
363 Ok(i64::from_le_bytes([
364 buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7],
365 ]))
366}
367
368/// Reads a 128-bit value from `HyperBinary` format.
369///
370/// # Errors
371///
372/// Returns [`CopyReadError::BufferTooShort`] if the buffer is too short (< 16 bytes).
373#[inline]
374pub fn read_data128(buf: &[u8]) -> Result<[u8; 16], CopyReadError> {
375 if buf.len() < 16 {
376 return Err(CopyReadError::BufferTooShort {
377 type_name: "data128",
378 expected: 16,
379 actual: buf.len(),
380 });
381 }
382 let mut result = [0u8; 16];
383 result.copy_from_slice(&buf[0..16]);
384 Ok(result)
385}
386
387/// Reads a variable-length binary value from `HyperBinary` format.
388///
389/// Returns the data slice (without the length prefix).
390///
391/// # Errors
392///
393/// - [`CopyReadError::BufferTooShort`] if the buffer is too short to read the length field
394/// - [`CopyReadError::LengthExceedsBuffer`] if the declared length exceeds available data
395///
396/// # Example
397///
398/// ```
399/// use hyperdb_api_core::protocol::copy::{read_varbinary, CopyReadError};
400///
401/// // "hello" with 4-byte length prefix
402/// let buf = [0x05, 0x00, 0x00, 0x00, b'h', b'e', b'l', b'l', b'o'];
403/// assert_eq!(read_varbinary(&buf).unwrap(), b"hello");
404///
405/// // Too short for length field
406/// let short_buf = [0x05, 0x00, 0x00];
407/// assert!(matches!(read_varbinary(&short_buf), Err(CopyReadError::BufferTooShort { .. })));
408///
409/// // Declared length exceeds buffer
410/// let bad_len = [0x10, 0x00, 0x00, 0x00, b'h', b'i']; // declares 16 bytes, has 2
411/// assert!(matches!(read_varbinary(&bad_len), Err(CopyReadError::LengthExceedsBuffer { .. })));
412/// ```
413#[inline]
414pub fn read_varbinary(buf: &[u8]) -> Result<&[u8], CopyReadError> {
415 if buf.len() < 4 {
416 return Err(CopyReadError::BufferTooShort {
417 type_name: "varbinary length field",
418 expected: 4,
419 actual: buf.len(),
420 });
421 }
422 let len_u32 = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
423 // On 32-bit platforms, u32 may exceed usize::MAX. Reject explicitly rather
424 // than silently truncating.
425 let Ok(len) = usize::try_from(len_u32) else {
426 return Err(CopyReadError::LengthExceedsBuffer {
427 declared: len_u32 as usize,
428 available: buf.len().saturating_sub(4),
429 });
430 };
431 let available = buf.len() - 4;
432 if available < len {
433 return Err(CopyReadError::LengthExceedsBuffer {
434 declared: len,
435 available,
436 });
437 }
438 let Some(end) = 4_usize.checked_add(len) else {
439 return Err(CopyReadError::LengthExceedsBuffer {
440 declared: len,
441 available,
442 });
443 };
444 Ok(&buf[4..end])
445}
446
447/// A builder for `HyperBinary` COPY data.
448///
449/// Provides a convenient interface for constructing `HyperBinary` COPY format data.
450/// Automatically writes the header before the first data value.
451///
452/// # Example
453///
454/// ```
455/// use hyperdb_api_core::protocol::copy::CopyDataBuilder;
456///
457/// let mut builder = CopyDataBuilder::new(1024);
458/// builder.write_i32(42, false);
459/// builder.write_str("hello", false);
460/// let data = builder.take();
461/// ```
462#[derive(Debug)]
463pub struct CopyDataBuilder {
464 /// Buffer containing the COPY data.
465 buffer: BytesMut,
466 /// Whether the `HyperBinary` header has been written.
467 header_written: bool,
468}
469
470impl CopyDataBuilder {
471 /// Creates a new builder with the specified initial capacity.
472 ///
473 /// # Arguments
474 ///
475 /// * `capacity` - Initial buffer capacity in bytes
476 #[must_use]
477 pub fn new(capacity: usize) -> Self {
478 CopyDataBuilder {
479 buffer: BytesMut::with_capacity(capacity),
480 header_written: false,
481 }
482 }
483
484 /// Creates a new builder with default capacity (1 MB).
485 #[must_use]
486 pub fn with_default_capacity() -> Self {
487 Self::new(1024 * 1024)
488 }
489
490 /// Ensures the `HyperBinary` COPY header is written.
491 ///
492 /// Called automatically before writing any data. Writes the header
493 /// only once, even if called multiple times.
494 pub fn ensure_header(&mut self) {
495 if !self.header_written {
496 write_header(&mut self.buffer);
497 self.header_written = true;
498 }
499 }
500
501 /// Writes a NULL value.
502 pub fn write_null(&mut self) {
503 self.ensure_header();
504 write_null(&mut self.buffer);
505 }
506
507 /// Writes a boolean value.
508 pub fn write_bool(&mut self, value: bool, nullable: bool) {
509 self.ensure_header();
510 let int_value = i8::from(value);
511 if nullable {
512 write_i8(&mut self.buffer, int_value);
513 } else {
514 write_i8_not_null(&mut self.buffer, int_value);
515 }
516 }
517
518 /// Writes an i16 value.
519 pub fn write_i16(&mut self, value: i16, nullable: bool) {
520 self.ensure_header();
521 if nullable {
522 write_i16(&mut self.buffer, value);
523 } else {
524 write_i16_not_null(&mut self.buffer, value);
525 }
526 }
527
528 /// Writes an i32 value.
529 pub fn write_i32(&mut self, value: i32, nullable: bool) {
530 self.ensure_header();
531 if nullable {
532 write_i32(&mut self.buffer, value);
533 } else {
534 write_i32_not_null(&mut self.buffer, value);
535 }
536 }
537
538 /// Writes an i64 value.
539 pub fn write_i64(&mut self, value: i64, nullable: bool) {
540 self.ensure_header();
541 if nullable {
542 write_i64(&mut self.buffer, value);
543 } else {
544 write_i64_not_null(&mut self.buffer, value);
545 }
546 }
547
548 /// Writes a 128-bit value.
549 pub fn write_data128(&mut self, value: &[u8; 16], nullable: bool) {
550 self.ensure_header();
551 if nullable {
552 write_data128(&mut self.buffer, value);
553 } else {
554 write_data128_not_null(&mut self.buffer, value);
555 }
556 }
557
558 /// Writes a variable-length binary value (text, bytea, etc.).
559 pub fn write_varbinary(&mut self, value: &[u8], nullable: bool) {
560 self.ensure_header();
561 if nullable {
562 write_varbinary(&mut self.buffer, value);
563 } else {
564 write_varbinary_not_null(&mut self.buffer, value);
565 }
566 }
567
568 /// Writes a string value.
569 pub fn write_str(&mut self, value: &str, nullable: bool) {
570 self.write_varbinary(value.as_bytes(), nullable);
571 }
572
573 /// Returns the current buffer size in bytes.
574 ///
575 /// Includes the header if it has been written.
576 #[must_use]
577 pub fn len(&self) -> usize {
578 self.buffer.len()
579 }
580
581 /// Returns true if the buffer is empty (no data written yet).
582 ///
583 /// Note: This returns true even if the header has been written,
584 /// since the header is written automatically before data.
585 #[must_use]
586 pub fn is_empty(&self) -> bool {
587 self.buffer.is_empty()
588 }
589
590 /// Returns the buffer contents and resets the builder.
591 ///
592 /// The builder can be reused after calling this method.
593 /// The header flag is reset, so the header will be written again
594 /// on the next write operation.
595 pub fn take(&mut self) -> BytesMut {
596 self.header_written = false;
597 std::mem::take(&mut self.buffer)
598 }
599
600 /// Returns a reference to the buffer contents.
601 ///
602 /// Useful for inspecting the data without consuming it.
603 #[must_use]
604 pub fn as_bytes(&self) -> &[u8] {
605 &self.buffer
606 }
607
608 /// Clears the buffer and resets the header flag.
609 ///
610 /// The builder can be reused after clearing. The capacity is preserved.
611 pub fn clear(&mut self) {
612 self.buffer.clear();
613 self.header_written = false;
614 }
615}
616
617impl Default for CopyDataBuilder {
618 fn default() -> Self {
619 Self::with_default_capacity()
620 }
621}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626
627 #[test]
628 fn test_header() {
629 let mut buf = BytesMut::new();
630 write_header(&mut buf);
631 assert_eq!(buf.as_ref(), HYPER_BINARY_HEADER);
632 assert_eq!(buf.len(), HYPER_BINARY_HEADER_SIZE);
633 // Verify header starts with "HPRCPY"
634 assert_eq!(&buf[..6], b"HPRCPY");
635 }
636
637 #[test]
638 fn test_i32_little_endian() {
639 let mut buf = BytesMut::new();
640 write_i32_not_null(&mut buf, 0x01020304);
641 // LittleEndian: least significant byte first
642 assert_eq!(buf.as_ref(), &[0x04, 0x03, 0x02, 0x01]);
643 }
644
645 #[test]
646 fn test_nullable_value() {
647 let mut buf = BytesMut::new();
648 write_i32(&mut buf, 42);
649 // [not-null indicator (0)][value LE: 42, 0, 0, 0]
650 assert_eq!(buf.as_ref(), &[0, 42, 0, 0, 0]);
651 }
652
653 #[test]
654 fn test_null() {
655 let mut buf = BytesMut::new();
656 write_null(&mut buf);
657 // NULL indicator is 1
658 assert_eq!(buf.as_ref(), &[1]);
659 }
660
661 #[test]
662 fn test_varbinary() {
663 let mut buf = BytesMut::new();
664 write_varbinary_not_null(&mut buf, b"hello");
665 // [length LE: 5, 0, 0, 0][data: "hello"]
666 assert_eq!(buf.as_ref(), &[5, 0, 0, 0, b'h', b'e', b'l', b'l', b'o']);
667 }
668
669 #[test]
670 fn test_varbinary_nullable() {
671 let mut buf = BytesMut::new();
672 write_varbinary(&mut buf, b"hi");
673 // [not-null (0)][length LE: 2, 0, 0, 0][data: "hi"]
674 assert_eq!(buf.as_ref(), &[0, 2, 0, 0, 0, b'h', b'i']);
675 }
676
677 #[test]
678 fn test_read_i32() {
679 let buf = [0x04, 0x03, 0x02, 0x01];
680 assert_eq!(read_i32(&buf).unwrap(), 0x01020304);
681 }
682
683 #[test]
684 fn test_read_i32_too_short() {
685 let buf = [0x04, 0x03, 0x02]; // Only 3 bytes
686 let err = read_i32(&buf).unwrap_err();
687 assert!(matches!(
688 err,
689 CopyReadError::BufferTooShort {
690 type_name: "i32",
691 expected: 4,
692 actual: 3
693 }
694 ));
695 }
696
697 #[test]
698 fn test_read_i16() {
699 let buf = [0x34, 0x12];
700 assert_eq!(read_i16(&buf).unwrap(), 0x1234);
701 }
702
703 #[test]
704 fn test_read_i16_too_short() {
705 let buf = [0x34]; // Only 1 byte
706 let err = read_i16(&buf).unwrap_err();
707 assert!(matches!(
708 err,
709 CopyReadError::BufferTooShort {
710 type_name: "i16",
711 expected: 2,
712 actual: 1
713 }
714 ));
715 }
716
717 #[test]
718 fn test_read_i64() {
719 let buf = [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01];
720 assert_eq!(read_i64(&buf).unwrap(), 0x0102030405060708);
721 }
722
723 #[test]
724 fn test_read_i64_too_short() {
725 let buf = [0x08, 0x07, 0x06, 0x05]; // Only 4 bytes
726 let err = read_i64(&buf).unwrap_err();
727 assert!(matches!(
728 err,
729 CopyReadError::BufferTooShort {
730 type_name: "i64",
731 expected: 8,
732 actual: 4
733 }
734 ));
735 }
736
737 #[test]
738 fn test_read_varbinary_valid() {
739 let buf = [0x05, 0x00, 0x00, 0x00, b'h', b'e', b'l', b'l', b'o'];
740 let result = read_varbinary(&buf).unwrap();
741 assert_eq!(result, b"hello");
742 }
743
744 #[test]
745 fn test_read_varbinary_length_field_too_short() {
746 let buf = [0x05, 0x00, 0x00]; // Only 3 bytes for length
747 let err = read_varbinary(&buf).unwrap_err();
748 assert!(matches!(
749 err,
750 CopyReadError::BufferTooShort {
751 type_name: "varbinary length field",
752 expected: 4,
753 actual: 3
754 }
755 ));
756 }
757
758 #[test]
759 fn test_read_varbinary_data_too_short() {
760 let buf = [0x05, 0x00, 0x00, 0x00, b'h', b'e']; // Declares 5 bytes but only 2 present
761 let err = read_varbinary(&buf).unwrap_err();
762 assert!(matches!(
763 err,
764 CopyReadError::LengthExceedsBuffer {
765 declared: 5,
766 available: 2
767 }
768 ));
769 }
770
771 #[test]
772 fn read_varbinary_zero_length() {
773 // A zero-length value is valid: 4 bytes of length prefix, no data.
774 let buf = [0x00, 0x00, 0x00, 0x00];
775 let bytes = read_varbinary(&buf).unwrap();
776 assert!(bytes.is_empty());
777 }
778
779 #[test]
780 fn read_varbinary_exact_fit() {
781 // Length declares exactly the bytes available.
782 let buf = [0x03, 0x00, 0x00, 0x00, b'a', b'b', b'c'];
783 let bytes = read_varbinary(&buf).unwrap();
784 assert_eq!(bytes, b"abc");
785 }
786
787 #[test]
788 fn read_varbinary_rejects_huge_declared_length_on_short_buf() {
789 // A hostile server could declare u32::MAX but only send a few bytes.
790 // The check must reject this rather than slicing past buffer end.
791 let buf = [0xFF, 0xFF, 0xFF, 0xFF, b'h', b'i'];
792 let err = read_varbinary(&buf).unwrap_err();
793 assert!(matches!(err, CopyReadError::LengthExceedsBuffer { .. }));
794 }
795
796 #[test]
797 fn test_copy_read_error_display() {
798 let err = CopyReadError::BufferTooShort {
799 type_name: "i32",
800 expected: 4,
801 actual: 2,
802 };
803 assert_eq!(
804 err.to_string(),
805 "Buffer too short for i32: expected 4 bytes, got 2"
806 );
807
808 let err = CopyReadError::LengthExceedsBuffer {
809 declared: 100,
810 available: 10,
811 };
812 assert_eq!(
813 err.to_string(),
814 "Declared length 100 exceeds available buffer space 10"
815 );
816 }
817
818 #[test]
819 fn test_copy_data_builder() {
820 let mut builder = CopyDataBuilder::new(1024);
821 builder.write_i32(42, false);
822 builder.write_str("hello", false);
823
824 let data = builder.as_bytes();
825 // Header + i32(42) + varbinary("hello")
826 assert!(data.starts_with(HYPER_BINARY_HEADER));
827 }
828
829 #[test]
830 fn test_f64_not_null() {
831 let mut buf = BytesMut::new();
832 write_f64_not_null(&mut buf, std::f64::consts::PI);
833 assert_eq!(buf.len(), 8);
834 // Verify it's little-endian
835 let read_value = f64::from_le_bytes([
836 buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6], buf[7],
837 ]);
838 assert!((read_value - std::f64::consts::PI).abs() < 1e-10);
839 }
840}