Skip to main content

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}