Skip to main content

oracledb_protocol/
wire.rs

1#![forbid(unsafe_code)]
2
3use crate::{ProtocolError, Result};
4
5pub const TNS_MAX_SHORT_LENGTH: usize = 252;
6pub const TNS_LONG_LENGTH_INDICATOR: u8 = 0xfe;
7pub const TNS_NULL_LENGTH_INDICATOR: u8 = 0xff;
8
9const MIB: usize = 1024 * 1024;
10
11/// Central resource policy for thin protocol decoding.
12///
13/// The values are deliberately collected in one copyable struct so connection,
14/// packet, TTC, object, vector, LOB, and notification decoders all share the
15/// same vocabulary for resource bounds. W1-T5.2 threads this policy through the
16/// current decoder call graph; this type is the single source of those limits.
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub struct ProtocolLimits {
19    /// Maximum encoded TNS packet size.
20    pub max_packet_bytes: usize,
21    /// Maximum decoded logical TTC frame/message size.
22    pub max_frame_bytes: usize,
23    /// Maximum cumulative bytes accepted for one server response.
24    pub max_response_bytes: usize,
25    /// Maximum number of columns in one describe/fetch shape.
26    pub max_columns: usize,
27    /// Maximum bind count in one execute/batch request.
28    pub max_binds: usize,
29    /// Maximum row count in one client-side batch operation.
30    pub max_batch_rows: usize,
31    /// Maximum recursive object/JSON nesting depth.
32    pub max_object_depth: usize,
33    /// Maximum element/member count in one decoded object/JSON container.
34    pub max_object_elements: usize,
35    /// Maximum VECTOR dimensions.
36    pub max_vector_dimensions: usize,
37    /// Maximum number of LOB chunks in one logical LOB operation.
38    pub max_lob_chunks: usize,
39    /// Maximum elements in generic length-prefixed wire collections.
40    pub max_length_prefixed_elements: usize,
41}
42
43impl Default for ProtocolLimits {
44    fn default() -> Self {
45        Self::DEFAULT
46    }
47}
48
49impl ProtocolLimits {
50    pub const DEFAULT: Self = Self {
51        max_packet_bytes: 16 * MIB,
52        max_frame_bytes: 16 * MIB,
53        max_response_bytes: 256 * MIB,
54        max_columns: 4096,
55        max_binds: 65_535,
56        max_batch_rows: 1_000_000,
57        max_object_depth: 256,
58        max_object_elements: 1_000_000,
59        max_vector_dimensions: 1_000_000,
60        max_lob_chunks: 1_000_000,
61        max_length_prefixed_elements: 1_000_000,
62    };
63
64    /// Validate caller-supplied limits before attaching them to a connection.
65    pub fn validate(self) -> Result<Self> {
66        for (name, value) in self.named_limits() {
67            if value == 0 {
68                return Err(ProtocolError::ResourceLimit {
69                    limit: name,
70                    observed: 0,
71                    maximum: 1,
72                });
73            }
74        }
75        if self.max_packet_bytes > self.max_frame_bytes {
76            return Err(ProtocolError::ResourceLimit {
77                limit: "packet_bytes",
78                observed: self.max_packet_bytes,
79                maximum: self.max_frame_bytes,
80            });
81        }
82        if self.max_frame_bytes > self.max_response_bytes {
83            return Err(ProtocolError::ResourceLimit {
84                limit: "frame_bytes",
85                observed: self.max_frame_bytes,
86                maximum: self.max_response_bytes,
87            });
88        }
89        Ok(self)
90    }
91
92    pub fn check_packet_bytes(&self, observed: usize) -> Result<()> {
93        self.check("packet_bytes", observed, self.max_packet_bytes)
94    }
95
96    pub fn check_frame_bytes(&self, observed: usize) -> Result<()> {
97        self.check("frame_bytes", observed, self.max_frame_bytes)
98    }
99
100    pub fn check_response_bytes(&self, observed: usize) -> Result<()> {
101        self.check("response_bytes", observed, self.max_response_bytes)
102    }
103
104    pub fn check_columns(&self, observed: usize) -> Result<()> {
105        self.check("columns", observed, self.max_columns)
106    }
107
108    pub fn check_binds(&self, observed: usize) -> Result<()> {
109        self.check("binds", observed, self.max_binds)
110    }
111
112    pub fn check_batch_rows(&self, observed: usize) -> Result<()> {
113        self.check("batch_rows", observed, self.max_batch_rows)
114    }
115
116    pub fn check_object_depth(&self, observed: usize) -> Result<()> {
117        self.check("object_depth", observed, self.max_object_depth)
118    }
119
120    pub fn check_object_elements(&self, observed: usize) -> Result<()> {
121        self.check("object_elements", observed, self.max_object_elements)
122    }
123
124    pub fn check_vector_dimensions(&self, observed: usize) -> Result<()> {
125        self.check("vector_dimensions", observed, self.max_vector_dimensions)
126    }
127
128    pub fn check_lob_chunks(&self, observed: usize) -> Result<()> {
129        self.check("lob_chunks", observed, self.max_lob_chunks)
130    }
131
132    pub fn check_length_prefixed_elements(&self, observed: usize) -> Result<()> {
133        self.check(
134            "length_prefixed_elements",
135            observed,
136            self.max_length_prefixed_elements,
137        )
138    }
139
140    fn check(&self, limit: &'static str, observed: usize, maximum: usize) -> Result<()> {
141        if observed <= maximum {
142            Ok(())
143        } else {
144            Err(ProtocolError::ResourceLimit {
145                limit,
146                observed,
147                maximum,
148            })
149        }
150    }
151
152    fn named_limits(&self) -> [(&'static str, usize); 11] {
153        [
154            ("packet_bytes", self.max_packet_bytes),
155            ("frame_bytes", self.max_frame_bytes),
156            ("response_bytes", self.max_response_bytes),
157            ("columns", self.max_columns),
158            ("binds", self.max_binds),
159            ("batch_rows", self.max_batch_rows),
160            ("object_depth", self.max_object_depth),
161            ("object_elements", self.max_object_elements),
162            ("vector_dimensions", self.max_vector_dimensions),
163            ("lob_chunks", self.max_lob_chunks),
164            (
165                "length_prefixed_elements",
166                self.max_length_prefixed_elements,
167            ),
168        ]
169    }
170}
171
172#[derive(Clone, Copy, Debug, Eq, PartialEq)]
173pub enum PacketLengthWidth {
174    Legacy16,
175    Large32,
176}
177
178#[derive(Clone, Debug, Default, Eq, PartialEq)]
179pub struct TtcWriter {
180    bytes: Vec<u8>,
181    seq_num: u8,
182}
183
184impl TtcWriter {
185    pub fn new() -> Self {
186        Self::default()
187    }
188
189    /// A writer whose backing buffer is preallocated to `capacity` bytes. A
190    /// `TtcWriter::new()` starts at zero capacity, so a payload built from many
191    /// small `write_*` pushes grows the `Vec` through several doublings — each a
192    /// separate heap allocation. Sizing the buffer once (to a small-payload
193    /// default or an exact known length) collapses those growth reallocs to a
194    /// single allocation. The written bytes are byte-identical either way; this
195    /// is a pure allocation optimization.
196    pub fn with_capacity(capacity: usize) -> Self {
197        Self {
198            bytes: Vec::with_capacity(capacity),
199            seq_num: 0,
200        }
201    }
202
203    pub fn into_bytes(self) -> Vec<u8> {
204        self.bytes
205    }
206
207    pub fn write_u8(&mut self, value: u8) {
208        self.bytes.push(value);
209    }
210
211    pub fn write_u16be(&mut self, value: u16) {
212        self.bytes.extend_from_slice(&value.to_be_bytes());
213    }
214
215    pub fn write_u16le(&mut self, value: u16) {
216        self.bytes.extend_from_slice(&value.to_le_bytes());
217    }
218
219    pub fn write_u32be(&mut self, value: u32) {
220        self.bytes.extend_from_slice(&value.to_be_bytes());
221    }
222
223    pub fn write_u64be(&mut self, value: u64) {
224        self.bytes.extend_from_slice(&value.to_be_bytes());
225    }
226
227    pub fn write_ub2(&mut self, value: u16) {
228        if value == 0 {
229            self.write_u8(0);
230        } else if value <= u16::from(u8::MAX) {
231            self.write_u8(1);
232            self.write_u8(value as u8);
233        } else {
234            self.write_u8(2);
235            self.write_u16be(value);
236        }
237    }
238
239    pub fn write_ub4(&mut self, value: u32) {
240        if value == 0 {
241            self.write_u8(0);
242        } else if value <= u32::from(u8::MAX) {
243            self.write_u8(1);
244            self.write_u8(value as u8);
245        } else if value <= u32::from(u16::MAX) {
246            self.write_u8(2);
247            self.write_u16be(value as u16);
248        } else {
249            self.write_u8(4);
250            self.write_u32be(value);
251        }
252    }
253
254    pub fn write_ub8(&mut self, value: u64) {
255        if value == 0 {
256            self.write_u8(0);
257        } else if value <= u64::from(u8::MAX) {
258            self.write_u8(1);
259            self.write_u8(value as u8);
260        } else if value <= u64::from(u16::MAX) {
261            self.write_u8(2);
262            self.write_u16be(value as u16);
263        } else if value <= u64::from(u32::MAX) {
264            self.write_u8(4);
265            self.write_u32be(value as u32);
266        } else {
267            self.write_u8(8);
268            self.write_u64be(value);
269        }
270    }
271
272    pub fn write_seq_num(&mut self) {
273        self.seq_num = self.seq_num.wrapping_add(1);
274        if self.seq_num == 0 {
275            self.seq_num = 1;
276        }
277        self.write_u8(self.seq_num);
278    }
279
280    pub fn write_raw(&mut self, value: &[u8]) {
281        self.bytes.extend_from_slice(value);
282    }
283
284    pub fn write_bytes_with_length(&mut self, value: &[u8]) -> Result<()> {
285        if value.len() <= TNS_MAX_SHORT_LENGTH {
286            self.write_u8(value.len() as u8);
287            self.write_raw(value);
288            return Ok(());
289        }
290        self.write_u8(TNS_LONG_LENGTH_INDICATOR);
291        for chunk in value.chunks(32_767) {
292            self.write_ub4(u32::try_from(chunk.len()).map_err(|_| {
293                ProtocolError::InvalidPacketLength {
294                    length: chunk.len(),
295                    minimum: 0,
296                }
297            })?);
298            self.write_raw(chunk);
299        }
300        self.write_ub4(0);
301        Ok(())
302    }
303
304    pub fn write_bytes_with_two_lengths(&mut self, value: Option<&[u8]>) -> Result<()> {
305        match value {
306            Some(bytes) => {
307                self.write_ub4(u32::try_from(bytes.len()).map_err(|_| {
308                    ProtocolError::InvalidPacketLength {
309                        length: bytes.len(),
310                        minimum: 0,
311                    }
312                })?);
313                if !bytes.is_empty() {
314                    self.write_bytes_with_length(bytes)?;
315                }
316            }
317            None => self.write_ub4(0),
318        }
319        Ok(())
320    }
321
322    pub fn write_str_two_lengths(&mut self, value: &str) -> Result<()> {
323        self.write_bytes_with_two_lengths(Some(value.as_bytes()))
324    }
325
326    /// Writes a 32-bit signed integer in Oracle universal (sign-magnitude)
327    /// format: a length byte whose high bit (`0x80`) is set for negatives,
328    /// followed by the big-endian magnitude bytes. Mirrors the reference
329    /// `WriteBuffer.write_sb4` (impl/base/buffer.pyx).
330    pub fn write_sb4(&mut self, value: i32) {
331        let (sign, magnitude) = if value < 0 {
332            (0x80u8, value.unsigned_abs())
333        } else {
334            (0u8, value as u32)
335        };
336        if magnitude == 0 {
337            self.write_u8(0);
338        } else if magnitude <= u32::from(u8::MAX) {
339            self.write_u8(1 | sign);
340            self.write_u8(magnitude as u8);
341        } else if magnitude <= u32::from(u16::MAX) {
342            self.write_u8(2 | sign);
343            self.write_u16be(magnitude as u16);
344        } else {
345            self.write_u8(4 | sign);
346            self.write_u32be(magnitude);
347        }
348    }
349
350    /// Writes a keyword/value pair (text and binary values plus a ub2 keyword)
351    /// as used by the AQ message-property extension list. Mirrors the reference
352    /// `WriteBuffer.write_keyword_value_pair` (impl/thin/packet.pyx:859).
353    pub fn write_keyword_value_pair(
354        &mut self,
355        text_value: Option<&[u8]>,
356        binary_value: Option<&[u8]>,
357        keyword: u16,
358    ) -> Result<()> {
359        self.write_bytes_with_two_lengths(text_value)?;
360        self.write_bytes_with_two_lengths(binary_value)?;
361        self.write_ub2(keyword);
362        Ok(())
363    }
364
365    pub fn write_function_code(&mut self, function_code: u8) {
366        self.write_u8(crate::thin::TNS_MSG_TYPE_FUNCTION);
367        self.write_u8(function_code);
368        self.write_seq_num();
369    }
370
371    pub fn write_function_code_with_seq(&mut self, function_code: u8, seq_num: u8) {
372        self.write_u8(crate::thin::TNS_MSG_TYPE_FUNCTION);
373        self.write_u8(function_code);
374        self.write_u8(seq_num);
375    }
376
377    /// Function-message header with the version-gated ub8 pipeline-token field
378    /// (reference messages/base.pyx `_write_function_code`): the token exists
379    /// only when the negotiated ttc field version is >= 23.1 ext 1. A pre-23ai
380    /// server parses a stray token byte as message content and fails the call
381    /// (observed live: ORA-03120 on Oracle XE 21c).
382    pub fn write_function_header(&mut self, function_code: u8, seq_num: u8, ttc_field_version: u8) {
383        self.write_function_code_with_seq(function_code, seq_num);
384        if crate::thin::version_gates::writes_pipeline_token(ttc_field_version) {
385            self.write_ub8(0);
386        }
387    }
388
389    /// Piggyback-message header with the same version-gated token field
390    /// (reference messages/base.pyx piggyback write path).
391    pub fn write_piggyback_header(
392        &mut self,
393        function_code: u8,
394        seq_num: u8,
395        ttc_field_version: u8,
396    ) {
397        self.write_u8(crate::thin::TNS_MSG_TYPE_PIGGYBACK);
398        self.write_u8(function_code);
399        self.write_u8(seq_num);
400        if crate::thin::version_gates::writes_pipeline_token(ttc_field_version) {
401            self.write_ub8(0);
402        }
403    }
404}
405
406/// The structural OOM-from-length invariant for every wire decoder.
407///
408/// A length/count field read from the wire can **never** drive an allocation
409/// larger than the bytes actually remaining in the current message buffer: you
410/// cannot have `N` elements if fewer than `N * min_bytes_per_elem` bytes remain.
411/// Every reader over an untrusted buffer (`TtcReader`, the OSON / DbObject /
412/// notification cursors, the VECTOR reader) implements this trait, and every
413/// count-driven `Vec::with_capacity` / `reserve` in the decoders routes through
414/// one of its two methods instead of trusting a raw `u16`/`u32`/`u64` count.
415///
416/// This closes the OOM-from-length bug class *by construction*: a new decoder
417/// physically cannot pre-allocate from a wire count without going through a
418/// bound, because the raw `Vec::with_capacity(count)` shape is the thing we
419/// audit against (see `docs/FUZZING.md`).
420///
421/// Two flavors, both anchored on [`remaining`](Self::remaining):
422///
423/// * [`alloc_count_checked`](Self::alloc_count_checked) — fail *closed* early:
424///   returns an `Err` if the declared count cannot possibly fit, before any
425///   allocation. Use where an oversized count is unambiguously malformed.
426/// * [`with_capacity_bounded`](Self::with_capacity_bounded) — cap *the
427///   pre-allocation* at what the buffer could hold while still returning a
428///   normal growable `Vec`. Use where the loop body itself fails closed on the
429///   first truncated element read; legitimate large payloads keep working
430///   because the cap equals the honest count whenever the bytes are really
431///   there.
432pub trait BoundedReader {
433    /// Bytes still unread in the current message buffer. The ceiling on any
434    /// count-driven allocation.
435    fn remaining(&self) -> usize;
436
437    /// Resource policy attached to this decoder. Readers that have not yet
438    /// grown a configurable policy surface use the validated defaults.
439    fn protocol_limits(&self) -> ProtocolLimits {
440        ProtocolLimits::DEFAULT
441    }
442
443    /// Validate a server-declared element `count` against the buffer: a run of
444    /// `count` elements must carry at least `count * min_bytes_per_elem` bytes,
445    /// so a count whose minimum byte footprint exceeds [`Self::remaining`] is a lie.
446    /// Returns the (unchanged) `count` when it fits, or a fail-closed
447    /// [`ProtocolError::TtcDecode`] otherwise — never a panic, never an OOM.
448    ///
449    /// `min_bytes_per_elem` is the *minimum* on-wire size of one element (e.g.
450    /// 4 for a `u32` index, 8 for an `f64`, 1 for a length-prefixed field whose
451    /// shortest legal form is a single length byte). A zero is treated as 1.
452    fn alloc_count_checked(&self, count: usize, min_bytes_per_elem: usize) -> Result<usize> {
453        self.protocol_limits()
454            .check_length_prefixed_elements(count)?;
455        let per_elem = min_bytes_per_elem.max(1);
456        match count.checked_mul(per_elem) {
457            Some(needed) if needed <= self.remaining() => Ok(count),
458            _ => Err(ProtocolError::TtcDecode(
459                "declared element count exceeds remaining buffer",
460            )),
461        }
462    }
463
464    /// Pre-size a `Vec` for `count` elements *without* trusting `count`: the
465    /// reserved capacity is capped at `remaining() / min_bytes_per_elem`, the
466    /// largest number of elements the buffer could actually hold. The returned
467    /// `Vec` is a normal growable `Vec`, so a legitimately large payload (where
468    /// `count` really fits) is pre-sized to the honest count, and a streamed /
469    /// chunked field that grows past the initial buffer still appends correctly
470    /// — the cap only governs the *speculative* up-front reservation.
471    fn with_capacity_bounded<T>(&self, count: usize, min_bytes_per_elem: usize) -> Vec<T> {
472        let per_elem = min_bytes_per_elem.max(1);
473        Vec::with_capacity(count.min(self.remaining() / per_elem))
474    }
475
476    /// Policy-aware form of [`with_capacity_bounded`](Self::with_capacity_bounded):
477    /// the caller supplies the resource family check, then the speculative
478    /// allocation is still capped by the remaining buffer.
479    fn with_capacity_limited<T, F>(
480        &self,
481        count: usize,
482        min_bytes_per_elem: usize,
483        check: F,
484    ) -> Result<Vec<T>>
485    where
486        F: FnOnce(&ProtocolLimits, usize) -> Result<()>,
487    {
488        check(&self.protocol_limits(), count)?;
489        Ok(self.with_capacity_bounded(count, min_bytes_per_elem))
490    }
491}
492
493#[derive(Clone, Debug)]
494pub struct TtcReader<'a> {
495    bytes: &'a [u8],
496    pos: usize,
497    limits: ProtocolLimits,
498}
499
500impl BoundedReader for TtcReader<'_> {
501    fn remaining(&self) -> usize {
502        TtcReader::remaining(self)
503    }
504
505    fn protocol_limits(&self) -> ProtocolLimits {
506        self.limits
507    }
508}
509
510/// Outcome of [`TtcReader::read_bytes_borrowed`]: a borrowed run of the wire
511/// buffer for the common contiguous short-value case, an owned fallback for the
512/// non-contiguous chunked long form, or NULL.
513#[derive(Clone, Debug, PartialEq, Eq)]
514pub enum BorrowedBytes<'a> {
515    /// SQL NULL (length byte `0` or `0xff`).
516    Null,
517    /// A contiguous run borrowed directly from the buffer (zero-copy).
518    Slice(&'a [u8]),
519    /// The chunked long form (`0xfe`), reassembled into an owned `Vec` because
520    /// the chunks are not contiguous on the wire. The rare path.
521    Chunked(Vec<u8>),
522}
523
524impl<'a> TtcReader<'a> {
525    pub fn new(bytes: &'a [u8]) -> Self {
526        Self {
527            bytes,
528            pos: 0,
529            limits: ProtocolLimits::DEFAULT,
530        }
531    }
532
533    pub fn with_limits(bytes: &'a [u8], limits: ProtocolLimits) -> Result<Self> {
534        let limits = limits.validate()?;
535        limits.check_frame_bytes(bytes.len())?;
536        limits.check_response_bytes(bytes.len())?;
537        Ok(Self {
538            bytes,
539            pos: 0,
540            limits,
541        })
542    }
543
544    pub fn limits(&self) -> ProtocolLimits {
545        self.limits
546    }
547
548    pub fn remaining(&self) -> usize {
549        self.bytes.len().saturating_sub(self.pos)
550    }
551
552    pub fn position(&self) -> usize {
553        self.pos
554    }
555
556    pub fn remaining_slice(&self) -> &[u8] {
557        &self.bytes[self.pos.min(self.bytes.len())..]
558    }
559
560    pub fn peek_u8(&self) -> Result<u8> {
561        self.bytes
562            .get(self.pos)
563            .copied()
564            .ok_or(ProtocolError::TtcDecode("missing u8"))
565    }
566
567    pub fn read_u8(&mut self) -> Result<u8> {
568        let value = *self
569            .bytes
570            .get(self.pos)
571            .ok_or(ProtocolError::TtcDecode("missing u8"))?;
572        self.pos += 1;
573        Ok(value)
574    }
575
576    pub fn read_i8(&mut self) -> Result<i8> {
577        Ok(self.read_u8()? as i8)
578    }
579
580    pub fn read_u16be(&mut self) -> Result<u16> {
581        let bytes = self.read_raw(2)?;
582        Ok(u16::from_be_bytes(
583            bytes
584                .try_into()
585                .map_err(|_| ProtocolError::TtcDecode("invalid u16"))?,
586        ))
587    }
588
589    pub fn read_u16le(&mut self) -> Result<u16> {
590        let bytes = self.read_raw(2)?;
591        Ok(u16::from_le_bytes(
592            bytes
593                .try_into()
594                .map_err(|_| ProtocolError::TtcDecode("invalid u16"))?,
595        ))
596    }
597
598    pub fn read_u32be(&mut self) -> Result<u32> {
599        let bytes = self.read_raw(4)?;
600        Ok(u32::from_be_bytes(
601            bytes
602                .try_into()
603                .map_err(|_| ProtocolError::TtcDecode("invalid u32"))?,
604        ))
605    }
606
607    pub fn read_raw(&mut self, len: usize) -> Result<&'a [u8]> {
608        self.limits.check_response_bytes(len)?;
609        let end = self
610            .pos
611            .checked_add(len)
612            .ok_or(ProtocolError::TtcDecode("read offset overflow"))?;
613        let bytes = self
614            .bytes
615            .get(self.pos..end)
616            .ok_or(ProtocolError::TtcDecode("truncated TTC payload"))?;
617        self.pos = end;
618        Ok(bytes)
619    }
620
621    pub fn skip(&mut self, len: usize) -> Result<()> {
622        self.read_raw(len).map(|_| ())
623    }
624
625    pub fn read_ub2(&mut self) -> Result<u16> {
626        let len = self.read_u8()?;
627        match len {
628            0 => Ok(0),
629            1 => Ok(u16::from(self.read_u8()?)),
630            2 => self.read_u16be(),
631            _ => Err(ProtocolError::TtcDecode("invalid ub2 length")),
632        }
633    }
634
635    pub fn read_ub4(&mut self) -> Result<u32> {
636        let len = self.read_u8()?;
637        if len == 0 {
638            return Ok(0);
639        }
640        if len > 4 {
641            return Err(ProtocolError::TtcDecode("invalid ub4 length"));
642        }
643        let mut value = 0u32;
644        for byte in self.read_raw(usize::from(len))? {
645            value = (value << 8) | u32::from(*byte);
646        }
647        Ok(value)
648    }
649
650    pub fn read_sb4(&mut self) -> Result<i32> {
651        let len = self.read_u8()?;
652        let is_negative = len & 0x80 != 0;
653        let len = len & 0x7f;
654        if len == 0 {
655            return Ok(0);
656        }
657        if len > 4 {
658            return Err(ProtocolError::TtcDecode("invalid sb4 length"));
659        }
660        // Accumulate in the unsigned width and reinterpret as signed: a server
661        // can send four bytes whose high bit is set (so the signed value is
662        // i32::MIN) and flag the length as negative. Negating i32::MIN — or even
663        // the intermediate `value << 8` — would overflow and panic under the
664        // debug/overflow-checked fuzz build. `wrapping_neg` matches the
665        // reference C decoder's two's-complement behavior and never panics.
666        let mut value = 0u32;
667        for byte in self.read_raw(usize::from(len))? {
668            value = (value << 8) | u32::from(*byte);
669        }
670        let value = value as i32;
671        Ok(if is_negative {
672            value.wrapping_neg()
673        } else {
674            value
675        })
676    }
677
678    pub fn read_sb8(&mut self) -> Result<i64> {
679        let len = self.read_u8()?;
680        let is_negative = len & 0x80 != 0;
681        let len = len & 0x7f;
682        if len == 0 {
683            return Ok(0);
684        }
685        if len > 8 {
686            return Err(ProtocolError::TtcDecode("invalid sb8 length"));
687        }
688        // See `read_sb4`: unsigned accumulation plus `wrapping_neg` avoids the
689        // i64::MIN negate-overflow panic on adversarial input.
690        let mut value = 0u64;
691        for byte in self.read_raw(usize::from(len))? {
692            value = (value << 8) | u64::from(*byte);
693        }
694        let value = value as i64;
695        Ok(if is_negative {
696            value.wrapping_neg()
697        } else {
698            value
699        })
700    }
701
702    pub fn read_ub8(&mut self) -> Result<u64> {
703        let len = self.read_u8()?;
704        if len == 0 {
705            return Ok(0);
706        }
707        if len > 8 {
708            return Err(ProtocolError::TtcDecode("invalid ub8 length"));
709        }
710        let mut value = 0u64;
711        for byte in self.read_raw(usize::from(len))? {
712            value = (value << 8) | u64::from(*byte);
713        }
714        Ok(value)
715    }
716
717    /// Zero-copy companion to [`read_bytes`](Self::read_bytes) for the borrowed
718    /// fetch path. The common short-value form (length byte 1..=253) is a single
719    /// contiguous run in the buffer, so it is returned as a borrowed slice with
720    /// no allocation. The chunked long form (`0xfe`) is *not* contiguous on the
721    /// wire (it is a sequence of length-prefixed chunks), so it cannot be
722    /// borrowed and falls back to an owned `Vec` — the rare path. `0`/`0xff`
723    /// signal SQL NULL.
724    ///
725    /// Consumes exactly the same number of bytes as `read_bytes` for every
726    /// input, so the two are interchangeable mid-stream.
727    pub fn read_bytes_borrowed(&mut self) -> Result<BorrowedBytes<'a>> {
728        let len = self.read_u8()?;
729        if len == TNS_LONG_LENGTH_INDICATOR {
730            let mut out = Vec::new();
731            let mut chunks = 0usize;
732            let mut total = 0usize;
733            loop {
734                let chunk_len = self.read_ub4()?;
735                if chunk_len == 0 {
736                    break;
737                }
738                chunks = chunks.checked_add(1).ok_or(ProtocolError::ResourceLimit {
739                    limit: "lob_chunks",
740                    observed: usize::MAX,
741                    maximum: self.limits.max_lob_chunks,
742                })?;
743                self.limits.check_lob_chunks(chunks)?;
744                let chunk_len =
745                    usize::try_from(chunk_len).map_err(|_| ProtocolError::InvalidPacketLength {
746                        length: usize::MAX,
747                        minimum: 0,
748                    })?;
749                total = total
750                    .checked_add(chunk_len)
751                    .ok_or(ProtocolError::ResourceLimit {
752                        limit: "response_bytes",
753                        observed: usize::MAX,
754                        maximum: self.limits.max_response_bytes,
755                    })?;
756                self.limits.check_response_bytes(total)?;
757                let chunk = self.read_raw(chunk_len)?;
758                out.extend_from_slice(chunk);
759            }
760            Ok(BorrowedBytes::Chunked(out))
761        } else if len == 0 || len == TNS_NULL_LENGTH_INDICATOR {
762            Ok(BorrowedBytes::Null)
763        } else {
764            self.limits.check_response_bytes(usize::from(len))?;
765            Ok(BorrowedBytes::Slice(self.read_raw(usize::from(len))?))
766        }
767    }
768
769    /// Advance past one length-prefixed TTC byte field (short, NULL, or chunked
770    /// long form) **without allocating** — the zero-copy skip used by the
771    /// borrowed fetch offset-capture pass. Consumes exactly the bytes
772    /// [`read_bytes`](Self::read_bytes) would.
773    pub fn skip_bytes_field(&mut self) -> Result<()> {
774        let len = self.read_u8()?;
775        if len == TNS_LONG_LENGTH_INDICATOR {
776            let mut chunks = 0usize;
777            let mut total = 0usize;
778            loop {
779                let chunk_len = self.read_ub4()?;
780                if chunk_len == 0 {
781                    break;
782                }
783                chunks = chunks.checked_add(1).ok_or(ProtocolError::ResourceLimit {
784                    limit: "lob_chunks",
785                    observed: usize::MAX,
786                    maximum: self.limits.max_lob_chunks,
787                })?;
788                self.limits.check_lob_chunks(chunks)?;
789                let chunk_len =
790                    usize::try_from(chunk_len).map_err(|_| ProtocolError::InvalidPacketLength {
791                        length: usize::MAX,
792                        minimum: 0,
793                    })?;
794                total = total
795                    .checked_add(chunk_len)
796                    .ok_or(ProtocolError::ResourceLimit {
797                        limit: "response_bytes",
798                        observed: usize::MAX,
799                        maximum: self.limits.max_response_bytes,
800                    })?;
801                self.limits.check_response_bytes(total)?;
802                self.skip(chunk_len)?;
803            }
804            Ok(())
805        } else if len == 0 || len == TNS_NULL_LENGTH_INDICATOR {
806            Ok(())
807        } else {
808            self.limits.check_response_bytes(usize::from(len))?;
809            self.skip(usize::from(len))
810        }
811    }
812
813    pub fn read_bytes(&mut self) -> Result<Option<Vec<u8>>> {
814        let len = self.read_u8()?;
815        if len == TNS_LONG_LENGTH_INDICATOR {
816            let mut out = Vec::new();
817            let mut chunks = 0usize;
818            let mut total = 0usize;
819            loop {
820                let chunk_len = self.read_ub4()?;
821                if chunk_len == 0 {
822                    break;
823                }
824                chunks = chunks.checked_add(1).ok_or(ProtocolError::ResourceLimit {
825                    limit: "lob_chunks",
826                    observed: usize::MAX,
827                    maximum: self.limits.max_lob_chunks,
828                })?;
829                self.limits.check_lob_chunks(chunks)?;
830                let chunk_len =
831                    usize::try_from(chunk_len).map_err(|_| ProtocolError::InvalidPacketLength {
832                        length: usize::MAX,
833                        minimum: 0,
834                    })?;
835                total = total
836                    .checked_add(chunk_len)
837                    .ok_or(ProtocolError::ResourceLimit {
838                        limit: "response_bytes",
839                        observed: usize::MAX,
840                        maximum: self.limits.max_response_bytes,
841                    })?;
842                self.limits.check_response_bytes(total)?;
843                let chunk = self.read_raw(chunk_len)?;
844                out.extend_from_slice(chunk);
845            }
846            Ok(Some(out))
847        } else if len == 0 || len == TNS_NULL_LENGTH_INDICATOR {
848            Ok(None)
849        } else {
850            self.limits.check_response_bytes(usize::from(len))?;
851            Ok(Some(self.read_raw(usize::from(len))?.to_vec()))
852        }
853    }
854
855    pub fn read_bytes_with_length(&mut self) -> Result<Option<Vec<u8>>> {
856        let len =
857            usize::try_from(self.read_ub4()?).map_err(|_| ProtocolError::InvalidPacketLength {
858                length: usize::MAX,
859                minimum: 0,
860            })?;
861        self.limits.check_response_bytes(len)?;
862        if len == 0 {
863            return Ok(None);
864        }
865        let value_start = self.pos;
866        match self.read_bytes() {
867            Ok(Some(bytes)) if bytes.len() == len => Ok(Some(bytes)),
868            Ok(_) | Err(_) => {
869                self.pos = value_start;
870                Ok(Some(self.read_raw(len)?.to_vec()))
871            }
872        }
873    }
874
875    pub fn read_string_with_length(&mut self) -> Result<Option<String>> {
876        let Some(bytes) = self.read_bytes_with_length()? else {
877            return Ok(None);
878        };
879        String::from_utf8(bytes)
880            .map(Some)
881            .map_err(|_| ProtocolError::TtcDecode("server sent non-UTF8 string"))
882    }
883
884    pub fn read_string(&mut self) -> Result<Option<String>> {
885        let Some(bytes) = self.read_bytes()? else {
886            return Ok(None);
887        };
888        String::from_utf8(bytes)
889            .map(Some)
890            .map_err(|_| ProtocolError::TtcDecode("server sent non-UTF8 string"))
891    }
892}
893
894pub fn encode_packet(
895    packet_type: u8,
896    packet_flags: u8,
897    data_flags: Option<u16>,
898    payload: &[u8],
899    width: PacketLengthWidth,
900) -> Result<Vec<u8>> {
901    let data_flags_len = usize::from(data_flags.is_some()) * 2;
902    let length = crate::packet::TNS_HEADER_LEN + data_flags_len + payload.len();
903    let mut out = Vec::with_capacity(length);
904    match width {
905        PacketLengthWidth::Legacy16 => {
906            let wire_length =
907                u16::try_from(length).map_err(|_| ProtocolError::PacketTooLarge { length })?;
908            out.extend_from_slice(&wire_length.to_be_bytes());
909            out.extend_from_slice(&0u16.to_be_bytes());
910        }
911        PacketLengthWidth::Large32 => {
912            let wire_length =
913                u32::try_from(length).map_err(|_| ProtocolError::PacketTooLarge { length })?;
914            out.extend_from_slice(&wire_length.to_be_bytes());
915        }
916    }
917    out.push(packet_type);
918    out.push(packet_flags);
919    out.extend_from_slice(&0u16.to_be_bytes());
920    if let Some(flags) = data_flags {
921        out.extend_from_slice(&flags.to_be_bytes());
922    }
923    out.extend_from_slice(payload);
924    Ok(out)
925}
926
927#[cfg(test)]
928mod tests {
929    use super::*;
930
931    fn assert_resource_limit(
932        result: Result<()>,
933        expected_limit: &'static str,
934        expected_observed: usize,
935        expected_maximum: usize,
936    ) {
937        assert!(
938            matches!(
939                result,
940                Err(ProtocolError::ResourceLimit {
941                    limit,
942                    observed,
943                    maximum,
944                }) if limit == expected_limit
945                    && observed == expected_observed
946                    && maximum == expected_maximum
947            ),
948            "expected ResourceLimit {{ limit: {expected_limit}, observed: {expected_observed}, maximum: {expected_maximum} }}"
949        );
950    }
951
952    #[test]
953    fn protocol_limits_default_names_every_resource_family() {
954        let limits = ProtocolLimits::default()
955            .validate()
956            .expect("valid defaults");
957        assert_eq!(limits, ProtocolLimits::DEFAULT);
958        let names: Vec<&'static str> = limits
959            .named_limits()
960            .into_iter()
961            .map(|(name, value)| {
962                assert!(value > 0, "{name} must be non-zero");
963                name
964            })
965            .collect();
966        assert_eq!(
967            names,
968            vec![
969                "packet_bytes",
970                "frame_bytes",
971                "response_bytes",
972                "columns",
973                "binds",
974                "batch_rows",
975                "object_depth",
976                "object_elements",
977                "vector_dimensions",
978                "lob_chunks",
979                "length_prefixed_elements",
980            ]
981        );
982    }
983
984    #[test]
985    fn protocol_limits_check_helpers_return_typed_resource_limit_errors() {
986        let limits = ProtocolLimits {
987            max_packet_bytes: 8,
988            max_frame_bytes: 16,
989            max_response_bytes: 32,
990            max_columns: 2,
991            max_binds: 3,
992            max_batch_rows: 4,
993            max_object_depth: 5,
994            max_object_elements: 6,
995            max_vector_dimensions: 7,
996            max_lob_chunks: 8,
997            max_length_prefixed_elements: 9,
998        }
999        .validate()
1000        .expect("valid test limits");
1001
1002        limits.check_packet_bytes(8).expect("boundary accepted");
1003        limits.check_frame_bytes(16).expect("boundary accepted");
1004        limits.check_response_bytes(32).expect("boundary accepted");
1005        limits.check_columns(2).expect("boundary accepted");
1006        limits.check_binds(3).expect("boundary accepted");
1007        limits.check_batch_rows(4).expect("boundary accepted");
1008        limits.check_object_depth(5).expect("boundary accepted");
1009        limits.check_object_elements(6).expect("boundary accepted");
1010        limits
1011            .check_vector_dimensions(7)
1012            .expect("boundary accepted");
1013        limits.check_lob_chunks(8).expect("boundary accepted");
1014        limits
1015            .check_length_prefixed_elements(9)
1016            .expect("boundary accepted");
1017
1018        assert_resource_limit(limits.check_packet_bytes(9), "packet_bytes", 9, 8);
1019        assert_resource_limit(limits.check_frame_bytes(17), "frame_bytes", 17, 16);
1020        assert_resource_limit(limits.check_response_bytes(33), "response_bytes", 33, 32);
1021        assert_resource_limit(limits.check_columns(3), "columns", 3, 2);
1022        assert_resource_limit(limits.check_binds(4), "binds", 4, 3);
1023        assert_resource_limit(limits.check_batch_rows(5), "batch_rows", 5, 4);
1024        assert_resource_limit(limits.check_object_depth(6), "object_depth", 6, 5);
1025        assert_resource_limit(limits.check_object_elements(7), "object_elements", 7, 6);
1026        assert_resource_limit(limits.check_vector_dimensions(8), "vector_dimensions", 8, 7);
1027        assert_resource_limit(limits.check_lob_chunks(9), "lob_chunks", 9, 8);
1028        assert_resource_limit(
1029            limits.check_length_prefixed_elements(10),
1030            "length_prefixed_elements",
1031            10,
1032            9,
1033        );
1034    }
1035
1036    #[test]
1037    fn protocol_limits_validate_rejects_zero_and_inverted_byte_hierarchy() {
1038        let zero_columns = ProtocolLimits {
1039            max_columns: 0,
1040            ..ProtocolLimits::DEFAULT
1041        };
1042        assert!(matches!(
1043            zero_columns.validate(),
1044            Err(ProtocolError::ResourceLimit {
1045                limit: "columns",
1046                observed: 0,
1047                maximum: 1,
1048            })
1049        ));
1050
1051        let packet_larger_than_frame = ProtocolLimits {
1052            max_packet_bytes: 17,
1053            max_frame_bytes: 16,
1054            ..ProtocolLimits::DEFAULT
1055        };
1056        assert!(matches!(
1057            packet_larger_than_frame.validate(),
1058            Err(ProtocolError::ResourceLimit {
1059                limit: "packet_bytes",
1060                observed: 17,
1061                maximum: 16,
1062            })
1063        ));
1064
1065        let frame_larger_than_response = ProtocolLimits {
1066            max_packet_bytes: 16,
1067            max_frame_bytes: 33,
1068            max_response_bytes: 32,
1069            ..ProtocolLimits::DEFAULT
1070        };
1071        assert!(matches!(
1072            frame_larger_than_response.validate(),
1073            Err(ProtocolError::ResourceLimit {
1074                limit: "frame_bytes",
1075                observed: 33,
1076                maximum: 32,
1077            })
1078        ));
1079    }
1080
1081    #[test]
1082    fn ttc_reader_with_limits_rejects_oversized_raw_reads() {
1083        let limits = ProtocolLimits {
1084            max_packet_bytes: 4,
1085            max_frame_bytes: 4,
1086            max_response_bytes: 4,
1087            ..ProtocolLimits::DEFAULT
1088        };
1089        let mut reader = TtcReader::with_limits(&[1, 2, 3, 4], limits).expect("valid limits");
1090        assert!(matches!(
1091            reader.read_raw(5),
1092            Err(ProtocolError::ResourceLimit {
1093                limit: "response_bytes",
1094                observed: 5,
1095                maximum: 4,
1096            })
1097        ));
1098    }
1099
1100    #[test]
1101    fn ttc_reader_with_limits_rejects_too_many_lob_chunks() {
1102        let limits = ProtocolLimits {
1103            max_lob_chunks: 1,
1104            ..ProtocolLimits::DEFAULT
1105        };
1106        let bytes = [TNS_LONG_LENGTH_INDICATOR, 1, 1, b'a', 1, 1, b'b', 0];
1107        let mut reader = TtcReader::with_limits(&bytes, limits).expect("valid limits");
1108        assert!(matches!(
1109            reader.read_bytes(),
1110            Err(ProtocolError::ResourceLimit {
1111                limit: "lob_chunks",
1112                observed: 2,
1113                maximum: 1,
1114            })
1115        ));
1116    }
1117
1118    // --- BoundedReader invariant (l2p) -----------------------------------
1119    // A length/count field read from the wire can NEVER drive an allocation
1120    // larger than the bytes actually remaining in the buffer. These tests pin
1121    // both flavors of the bounded-allocation primitive: the early-erroring
1122    // `alloc_count_checked` and the cap-and-grow `with_capacity_bounded`.
1123
1124    #[test]
1125    fn alloc_count_checked_errs_when_count_exceeds_remaining() {
1126        // 4 bytes left in the buffer, but a declared count of ~4 billion 8-byte
1127        // elements. The honest minimum is 8 bytes per element, so the claim is
1128        // a lie and must fail closed rather than reserving ~32 GB.
1129        let bytes = [0u8; 4];
1130        let reader = TtcReader::new(&bytes);
1131        assert!(reader.alloc_count_checked(u32::MAX as usize, 8).is_err());
1132        // count * min_bytes that overflows usize must also fail closed.
1133        assert!(reader.alloc_count_checked(usize::MAX, 8).is_err());
1134    }
1135
1136    #[test]
1137    fn alloc_count_checked_ok_when_count_fits() {
1138        // 16 bytes remaining, two 8-byte elements declared: legitimate.
1139        let bytes = [0u8; 16];
1140        let reader = TtcReader::new(&bytes);
1141        assert_eq!(
1142            reader.alloc_count_checked(2, 8).expect("fits"),
1143            2,
1144            "a count whose bytes fit must pass through unchanged"
1145        );
1146        // A zero-minimum element size is treated as 1 byte (defensive) and a
1147        // zero count is always fine.
1148        assert_eq!(reader.alloc_count_checked(0, 0).expect("zero"), 0);
1149    }
1150
1151    #[test]
1152    fn with_capacity_bounded_caps_preallocation_but_still_grows() {
1153        // 8 bytes remaining; a hostile count of ~4 billion 4-byte elements.
1154        let bytes = [0u8; 8];
1155        let reader = TtcReader::new(&bytes);
1156        let v: Vec<u32> = reader.with_capacity_bounded(u32::MAX as usize, 4);
1157        // The pre-allocation is capped at remaining()/elem = 8/4 = 2, NOT 4e9.
1158        assert_eq!(
1159            v.capacity(),
1160            2,
1161            "pre-allocation must be capped by remaining"
1162        );
1163        // But the vec is still a normal growable Vec: pushing past the cap is
1164        // fine (legitimate large payloads keep working as chunks arrive).
1165        let mut v = v;
1166        for i in 0..100u32 {
1167            v.push(i);
1168        }
1169        assert_eq!(v.len(), 100);
1170    }
1171
1172    #[test]
1173    fn with_capacity_bounded_uses_full_count_when_buffer_is_large() {
1174        // 400 bytes remaining, 10 four-byte elements: the real count fits, so
1175        // the pre-allocation is the honest count, not an arbitrary small cap.
1176        let bytes = [0u8; 400];
1177        let reader = TtcReader::new(&bytes);
1178        let v: Vec<u32> = reader.with_capacity_bounded(10, 4);
1179        assert_eq!(v.capacity(), 10);
1180    }
1181
1182    // Regression (w6-fuzz, query_response target): a negative-flagged sb4/sb8
1183    // whose magnitude is i32::MIN / i64::MIN made `-value` overflow and panic
1184    // ("attempt to negate with overflow") under the overflow-checked fuzz
1185    // build. `read_sb4`/`read_sb8` must now wrap instead of panicking.
1186    #[test]
1187    fn sb4_sb8_negate_overflow_does_not_panic() {
1188        // len byte 0x84 => negative, 4 bytes; value bytes 80 00 00 00 => i32::MIN.
1189        let bytes = [0x84u8, 0x80, 0x00, 0x00, 0x00];
1190        let mut reader = TtcReader::new(&bytes);
1191        assert_eq!(reader.read_sb4().expect("sb4 must not panic"), i32::MIN);
1192
1193        // len byte 0x88 => negative, 8 bytes; 80 00.. => i64::MIN.
1194        let bytes8 = [0x88u8, 0x80, 0, 0, 0, 0, 0, 0, 0];
1195        let mut reader8 = TtcReader::new(&bytes8);
1196        assert_eq!(reader8.read_sb8().expect("sb8 must not panic"), i64::MIN);
1197    }
1198
1199    // Round-trip ordinary signed values to confirm the unsigned-accumulation
1200    // rewrite did not change behavior for the common range.
1201    #[test]
1202    fn sb4_decodes_representative_values() {
1203        // Hand-encoded sign-magnitude: len|0x80 for negatives.
1204        let cases: [(&[u8], i32); 4] = [
1205            (&[0x00], 0),
1206            (&[0x01, 0x2a], 42),
1207            (&[0x81, 0x2a], -42),
1208            (&[0x02, 0x01, 0x00], 256),
1209        ];
1210        for (bytes, expected) in cases {
1211            let mut reader = TtcReader::new(bytes);
1212            assert_eq!(
1213                reader.read_sb4().expect("sb4 decode"),
1214                expected,
1215                "{bytes:?}"
1216            );
1217        }
1218    }
1219
1220    #[test]
1221    fn ub4_round_trips_representative_values() {
1222        for value in [0, 1, 255, 256, 65_535, 65_536, u32::MAX] {
1223            let mut writer = TtcWriter::new();
1224            writer.write_ub4(value);
1225            let bytes = writer.into_bytes();
1226            let mut reader = TtcReader::new(&bytes);
1227            assert_eq!(reader.read_ub4().expect("ub4 should decode"), value);
1228            assert_eq!(reader.remaining(), 0);
1229        }
1230    }
1231
1232    // `read_bytes_borrowed` must borrow the contiguous short-value bytes
1233    // directly out of the buffer (the zero-copy hot path), signal `Null` for
1234    // 0/0xff length, and fall back to an owned `Chunked` Vec for the
1235    // 0xfe long-value form (which is not contiguous on the wire). The borrowed
1236    // slice must equal what `read_bytes` would return, and consume exactly the
1237    // same number of bytes.
1238    #[test]
1239    fn read_bytes_borrowed_borrows_short_values_and_owns_chunked() {
1240        // Short value: length byte 3 + "abc".
1241        let short = [0x03u8, b'a', b'b', b'c'];
1242        let mut reader = TtcReader::new(&short);
1243        let borrowed = reader.read_bytes_borrowed().expect("short decode");
1244        assert!(matches!(borrowed, BorrowedBytes::Slice(slice) if slice == b"abc"));
1245        assert_eq!(reader.remaining(), 0);
1246
1247        // NULL value: 0xff.
1248        let null = [TNS_NULL_LENGTH_INDICATOR];
1249        let mut reader = TtcReader::new(&null);
1250        assert!(matches!(
1251            reader.read_bytes_borrowed().expect("null decode"),
1252            BorrowedBytes::Null
1253        ));
1254
1255        // Zero-length value: 0x00 (also NULL in TTC).
1256        let zero = [0x00u8];
1257        let mut reader = TtcReader::new(&zero);
1258        assert!(matches!(
1259            reader.read_bytes_borrowed().expect("zero decode"),
1260            BorrowedBytes::Null
1261        ));
1262
1263        // Long/chunked value: 0xfe then ub4 chunk lengths terminated by 0.
1264        let mut writer = TtcWriter::new();
1265        writer
1266            .write_bytes_with_length(&vec![0x5au8; 600]) // forces the 0xfe chunked form
1267            .expect("chunked encode");
1268        let long = writer.into_bytes();
1269        let mut reader = TtcReader::new(&long);
1270        let expected = vec![0x5au8; 600];
1271        let borrowed = reader.read_bytes_borrowed().expect("chunked decode");
1272        assert!(matches!(&borrowed, BorrowedBytes::Chunked(bytes) if bytes == &expected));
1273        assert_eq!(reader.remaining(), 0);
1274    }
1275
1276    #[test]
1277    fn bytes_with_length_accepts_nested_ttc_bytes() {
1278        let mut writer = TtcWriter::new();
1279        writer
1280            .write_bytes_with_two_lengths(Some(b"abc"))
1281            .expect("bytes should encode");
1282        let bytes = writer.into_bytes();
1283        let mut reader = TtcReader::new(&bytes);
1284        assert_eq!(
1285            reader
1286                .read_bytes_with_length()
1287                .expect("bytes should decode"),
1288            Some(b"abc".to_vec())
1289        );
1290        assert_eq!(reader.remaining(), 0);
1291    }
1292
1293    #[test]
1294    fn bytes_with_length_accepts_direct_payload_bytes() {
1295        let bytes = [1, 3, b'a', b'b', b'c'];
1296        let mut reader = TtcReader::new(&bytes);
1297        assert_eq!(
1298            reader
1299                .read_bytes_with_length()
1300                .expect("bytes should decode"),
1301            Some(b"abc".to_vec())
1302        );
1303        assert_eq!(reader.remaining(), 0);
1304    }
1305
1306    #[test]
1307    fn data_packet_uses_four_byte_length_when_negotiated() {
1308        let packet = encode_packet(
1309            6,
1310            0,
1311            Some(0),
1312            &[0x03, 0x93, 0x01],
1313            PacketLengthWidth::Large32,
1314        )
1315        .expect("packet should encode");
1316        assert_eq!(&packet[..10], &[0, 0, 0, 13, 6, 0, 0, 0, 0, 0]);
1317    }
1318
1319    // Reference packet.pyx:778 gates the packet-length field width on
1320    // protocol_version >= TNS_VERSION_MIN_LARGE_SDU (315): pre-12.1 servers use
1321    // a 2-byte length + 2-byte padding, negotiated servers a 4-byte length. Our
1322    // wire layer takes the width as a parameter (the caller derives it from the
1323    // negotiated version); this pins that the two widths frame the length
1324    // differently for the same payload.
1325    #[test]
1326    fn packet_length_framing_switches_between_legacy16_and_large32() {
1327        let payload = [0x03, 0x93, 0x01];
1328        let legacy = encode_packet(6, 0, Some(0), &payload, PacketLengthWidth::Legacy16)
1329            .expect("legacy packet");
1330        let large = encode_packet(6, 0, Some(0), &payload, PacketLengthWidth::Large32)
1331            .expect("large packet");
1332
1333        // length == 13 in both, but framed as u16+u16-pad vs u32.
1334        assert_eq!(
1335            &legacy[..4],
1336            &[0, 13, 0, 0],
1337            "legacy: u16 length then u16 pad"
1338        );
1339        assert_eq!(&large[..4], &[0, 0, 0, 13], "large: u32 length");
1340        assert_ne!(legacy, large, "the length-width gate changes the wire");
1341    }
1342
1343    // Reference messages/base.pyx:700/714 gates the ub8 pipeline token on the
1344    // function-code and piggyback headers on ttc field version >= 23.1 ext 1
1345    // (18): pre-23ai servers parse a stray token byte as message content and
1346    // fail the call (observed live: ORA-03120 on Oracle XE 21c). The token is
1347    // appended after the fixed header, so at/above the boundary the header is
1348    // exactly the pre-boundary header plus the ub8(0) token byte.
1349    #[test]
1350    fn function_and_piggyback_headers_gate_pipeline_token_on_23_1_ext_1() {
1351        let lo = crate::thin::TNS_CCAP_FIELD_VERSION_23_1_EXT_1 - 1;
1352        let hi = crate::thin::TNS_CCAP_FIELD_VERSION_23_1_EXT_1;
1353
1354        let function_header = |fv| {
1355            let mut w = TtcWriter::new();
1356            w.write_function_header(3, 5, fv);
1357            w.into_bytes()
1358        };
1359        let f_lo = function_header(lo);
1360        let f_hi = function_header(hi);
1361        assert_eq!(f_hi.len(), f_lo.len() + 1, "function header gains ub8(0)");
1362        assert_eq!(&f_hi[..f_lo.len()], f_lo.as_slice(), "prefix unchanged");
1363        assert_eq!(f_hi[f_lo.len()], 0, "the token is ub8(0)");
1364
1365        let piggyback_header = |fv| {
1366            let mut w = TtcWriter::new();
1367            w.write_piggyback_header(3, 5, fv);
1368            w.into_bytes()
1369        };
1370        let p_lo = piggyback_header(lo);
1371        let p_hi = piggyback_header(hi);
1372        assert_eq!(p_hi.len(), p_lo.len() + 1, "piggyback header gains ub8(0)");
1373        assert_eq!(&p_hi[..p_lo.len()], p_lo.as_slice(), "prefix unchanged");
1374        assert_eq!(p_hi[p_lo.len()], 0, "the token is ub8(0)");
1375    }
1376}