Skip to main content

oracledb_protocol/thin/
dbobject.rs

1#![forbid(unsafe_code)]
2
3use super::*;
4
5pub struct DbObjectPackedReader<'a> {
6    bytes: &'a [u8],
7    pos: usize,
8    limits: crate::wire::ProtocolLimits,
9}
10
11impl<'a> DbObjectPackedReader<'a> {
12    pub fn new(bytes: &'a [u8]) -> Self {
13        Self {
14            bytes,
15            pos: 0,
16            limits: crate::wire::ProtocolLimits::DEFAULT,
17        }
18    }
19
20    pub fn with_limits(bytes: &'a [u8], limits: crate::wire::ProtocolLimits) -> Result<Self> {
21        Ok(Self {
22            bytes,
23            pos: 0,
24            limits: limits.validate()?,
25        })
26    }
27
28    pub fn limits(&self) -> crate::wire::ProtocolLimits {
29        self.limits
30    }
31
32    pub fn read_u8(&mut self) -> Result<u8> {
33        let value = self
34            .bytes
35            .get(self.pos)
36            .copied()
37            .ok_or(ProtocolError::TtcDecode("truncated DbObject packed data"))?;
38        self.pos += 1;
39        Ok(value)
40    }
41
42    fn read_raw(&mut self, len: usize) -> Result<&'a [u8]> {
43        self.limits.check_response_bytes(len)?;
44        let end = self.pos.checked_add(len).ok_or(ProtocolError::TtcDecode(
45            "DbObject packed data offset overflow",
46        ))?;
47        let bytes = self
48            .bytes
49            .get(self.pos..end)
50            .ok_or(ProtocolError::TtcDecode("truncated DbObject packed data"))?;
51        self.pos = end;
52        Ok(bytes)
53    }
54
55    fn skip(&mut self, len: usize) -> Result<()> {
56        self.read_raw(len).map(|_| ())
57    }
58
59    fn read_u32be(&mut self) -> Result<u32> {
60        let bytes = self.read_raw(4)?;
61        Ok(u32::from_be_bytes(bytes.try_into().map_err(|_| {
62            ProtocolError::TtcDecode("invalid DbObject u32")
63        })?))
64    }
65
66    pub fn read_i32be(&mut self) -> Result<i32> {
67        let bytes = self.read_raw(4)?;
68        Ok(i32::from_be_bytes(bytes.try_into().map_err(|_| {
69            ProtocolError::TtcDecode("invalid DbObject i32")
70        })?))
71    }
72
73    pub fn read_length(&mut self) -> Result<usize> {
74        match self.read_u8()? {
75            TNS_LONG_LENGTH_INDICATOR => usize::try_from(self.read_u32be()?)
76                .map_err(|_| ProtocolError::TtcDecode("DbObject length overflow")),
77            length => Ok(usize::from(length)),
78        }
79    }
80
81    fn skip_length(&mut self) -> Result<()> {
82        if self.read_u8()? == TNS_LONG_LENGTH_INDICATOR {
83            self.skip(4)?;
84        }
85        Ok(())
86    }
87
88    pub fn read_value_bytes(&mut self) -> Result<Option<Vec<u8>>> {
89        let length = match self.read_u8()? {
90            TNS_NULL_LENGTH_INDICATOR => return Ok(None),
91            TNS_LONG_LENGTH_INDICATOR => usize::try_from(self.read_u32be()?)
92                .map_err(|_| ProtocolError::TtcDecode("DbObject value length overflow"))?,
93            length => usize::from(length),
94        };
95        Ok(Some(self.read_raw(length)?.to_vec()))
96    }
97
98    pub fn read_header(&mut self) -> Result<()> {
99        let flags = self.read_u8()?;
100        let _version = self.read_u8()?;
101        self.skip_length()?;
102        if flags & TNS_OBJ_IS_DEGENERATE != 0 {
103            return Err(ProtocolError::UnsupportedFeature(
104                "DbObject stored in a LOB",
105            ));
106        }
107        if flags & TNS_OBJ_NO_PREFIX_SEG == 0 {
108            let prefix_len = self.read_length()?;
109            self.skip(prefix_len)?;
110        }
111        Ok(())
112    }
113
114    fn bytes_left(&self) -> usize {
115        self.bytes.len().saturating_sub(self.pos)
116    }
117
118    /// Bytes still unread in the packed image. Exposed so a caller materializing
119    /// a collection (whose element count is a server-declared `read_length`) can
120    /// bound its `Vec` pre-allocation against the buffer via the
121    /// [`BoundedReader`](crate::wire::BoundedReader) trait — closing the
122    /// OOM-from-length class for DbObject collections too.
123    pub fn remaining(&self) -> usize {
124        self.bytes_left()
125    }
126
127    pub fn read_atomic_null(&mut self, is_collection_context: bool) -> Result<bool> {
128        let value = self.read_u8()?;
129        match (value, is_collection_context) {
130            (TNS_OBJ_ATOMIC_NULL, _) | (TNS_NULL_LENGTH_INDICATOR, true) => Ok(true),
131            _ => {
132                self.pos = self.pos.saturating_sub(1);
133                Ok(false)
134            }
135        }
136    }
137}
138
139impl crate::wire::BoundedReader for DbObjectPackedReader<'_> {
140    fn remaining(&self) -> usize {
141        self.bytes_left()
142    }
143
144    fn protocol_limits(&self) -> crate::wire::ProtocolLimits {
145        self.limits
146    }
147}
148
149/// Writes a length-prefixed value into a DbObject pickle image buffer using the
150/// DbObject image `write_length` format: 245-byte short cutoff, or `0xfe`
151/// followed by one big-endian `uint32` length and the full value bytes.
152pub fn image_write_value_bytes(buf: &mut Vec<u8>, value: &[u8]) -> Result<()> {
153    if value.len() <= TNS_OBJ_MAX_SHORT_LENGTH {
154        buf.push(value.len() as u8);
155        buf.extend_from_slice(value);
156        return Ok(());
157    }
158    let length = u32::try_from(value.len()).map_err(|_| ProtocolError::InvalidPacketLength {
159        length: value.len(),
160        minimum: 0,
161    })?;
162    buf.push(TNS_LONG_LENGTH_INDICATOR);
163    buf.extend_from_slice(&length.to_be_bytes());
164    buf.extend_from_slice(value);
165    Ok(())
166}
167
168/// Writes a collection/element count length into a pickle image buffer using
169/// the 245-cutoff scheme (reference `DbObjectPickleBuffer.write_length`).
170pub fn image_write_length(buf: &mut Vec<u8>, length: usize) -> Result<()> {
171    if length <= TNS_OBJ_MAX_SHORT_LENGTH {
172        buf.push(length as u8);
173    } else {
174        buf.push(TNS_LONG_LENGTH_INDICATOR);
175        buf.extend_from_slice(
176            &u32::try_from(length)
177                .map_err(|_| ProtocolError::InvalidPacketLength { length, minimum: 0 })?
178                .to_be_bytes(),
179        );
180    }
181    Ok(())
182}
183
184/// Builds the pickle image header (reference `write_header` + image_flags from
185/// `create_new_object`). Returns the buffer pre-seeded with the header; the
186/// caller appends the body and then calls [`image_finalize`] to back-patch the
187/// total size (4-byte BE at offset 3).
188pub fn image_begin(is_collection: bool) -> Vec<u8> {
189    let mut image_flags = TNS_OBJ_IS_VERSION_81;
190    if is_collection {
191        image_flags |= TNS_OBJ_IS_COLLECTION;
192    } else {
193        image_flags |= TNS_OBJ_NO_PREFIX_SEG;
194    }
195    let mut buf = Vec::new();
196    buf.push(image_flags);
197    buf.push(TNS_OBJ_IMAGE_VERSION);
198    buf.push(TNS_LONG_LENGTH_INDICATOR);
199    buf.extend_from_slice(&0u32.to_be_bytes()); // size placeholder (offset 3)
200    if is_collection {
201        buf.push(1); // length of prefix segment
202        buf.push(1); // prefix segment contents
203    }
204    buf
205}
206
207/// Back-patches the total image size (reference `_get_packed_data`: the 4-byte
208/// BE size at offset 3, after flags + version + 0xFE).
209pub fn image_finalize(buf: &mut [u8]) -> Result<()> {
210    let size = u32::try_from(buf.len()).map_err(|_| ProtocolError::InvalidPacketLength {
211        length: buf.len(),
212        minimum: 0,
213    })?;
214    let slot = buf.get_mut(3..7).ok_or(ProtocolError::TtcDecode(
215        "DbObject image too short to finalize",
216    ))?;
217    slot.copy_from_slice(&size.to_be_bytes());
218    Ok(())
219}
220
221/// Collection flags byte written at the start of a collection body
222/// (`TNS_OBJ_HAS_INDEXES` for associative arrays, else 0). Reference
223/// `_parse_tds` collection_flags + `_pack_data`.
224pub fn collection_flags_for(is_assoc_array: bool) -> u8 {
225    if is_assoc_array {
226        TNS_OBJ_HAS_INDEXES
227    } else {
228        0
229    }
230}
231
232/// Writes a NULL element/attribute marker into the image. Non-collection object
233/// attributes use `TNS_OBJ_ATOMIC_NULL` (253); scalars and collection elements
234/// use `TNS_NULL_LENGTH_INDICATOR` (255). Reference `_pack_value` None branch.
235pub fn image_write_null(buf: &mut Vec<u8>, atomic_null: bool) {
236    if atomic_null {
237        buf.push(TNS_OBJ_ATOMIC_NULL);
238    } else {
239        buf.push(TNS_NULL_LENGTH_INDICATOR);
240    }
241}
242
243/// Packs a single scalar `BindValue` into a DbObject pickle image buffer,
244/// mirroring `_pack_value` (reference impl/thin/dbobject.pyx:247-306). Object
245/// (nested) and Null/Array values are handled by the caller (the pyshim owns
246/// the recursion and null framing); this serves scalar attributes and
247/// collection elements only.
248pub fn pack_bindvalue_into_image(buf: &mut Vec<u8>, value: &BindValue, csfrm: u8) -> Result<()> {
249    match value {
250        BindValue::Text(text) => {
251            let bytes = encode_text_value(text, csfrm);
252            image_write_value_bytes(buf, &bytes)
253        }
254        BindValue::Raw(bytes) => image_write_value_bytes(buf, bytes),
255        BindValue::Number(text) => {
256            let bytes = encode_number_text(text)?;
257            image_write_value_bytes(buf, &bytes)
258        }
259        // PLS_INTEGER / BINARY_INTEGER pack as uint8(4) + uint32be (NOT Oracle
260        // number text) inside an object image.
261        BindValue::BinaryInteger(text) => {
262            let value = parse_binary_integer_u32(text)?;
263            buf.push(4);
264            buf.extend_from_slice(&value.to_be_bytes());
265            Ok(())
266        }
267        // BOOLEAN inside an image is the 4-byte form, NOT [1,1]/[0].
268        BindValue::Boolean(value) => {
269            buf.push(4);
270            buf.extend_from_slice(&u32::from(*value).to_be_bytes());
271            Ok(())
272        }
273        BindValue::BinaryDouble(value) => {
274            let bytes = encode_binary_double(*value);
275            image_write_value_bytes(buf, &bytes)
276        }
277        BindValue::BinaryFloat(value) => {
278            let bytes = encode_binary_float(*value as f32);
279            image_write_value_bytes(buf, &bytes)
280        }
281        BindValue::DateTime {
282            year,
283            month,
284            day,
285            hour,
286            minute,
287            second,
288        } => {
289            let bytes = encode_oracle_date(*year, *month, *day, *hour, *minute, *second)?;
290            image_write_value_bytes(buf, &bytes)
291        }
292        BindValue::Timestamp {
293            year,
294            month,
295            day,
296            hour,
297            minute,
298            second,
299            nanosecond,
300            ora_type_num,
301        } => {
302            let bytes = if matches!(*ora_type_num, ORA_TYPE_NUM_TIMESTAMP_TZ) {
303                encode_oracle_timestamp_tz(
304                    *year,
305                    *month,
306                    *day,
307                    *hour,
308                    *minute,
309                    *second,
310                    *nanosecond,
311                )?
312            } else {
313                encode_oracle_timestamp(*year, *month, *day, *hour, *minute, *second, *nanosecond)?
314            };
315            image_write_value_bytes(buf, &bytes)
316        }
317        BindValue::TimestampTz {
318            year,
319            month,
320            day,
321            hour,
322            minute,
323            second,
324            nanosecond,
325            offset_minutes,
326        } => {
327            let bytes = encode_oracle_timestamp_tz_with_offset(
328                *year,
329                *month,
330                *day,
331                *hour,
332                *minute,
333                *second,
334                *nanosecond,
335                *offset_minutes,
336            )?;
337            image_write_value_bytes(buf, &bytes)
338        }
339        BindValue::Lob { locator, .. } => image_write_value_bytes(buf, locator),
340        BindValue::IntervalDS {
341            days,
342            seconds,
343            microseconds,
344        } => {
345            let nanoseconds = microseconds
346                .checked_mul(1000)
347                .ok_or(ProtocolError::TtcDecode(
348                    "INTERVAL DS fractional seconds out of range",
349                ))?;
350            let bytes = encode_interval_ds(*days, *seconds, nanoseconds)?;
351            image_write_value_bytes(buf, &bytes)
352        }
353        BindValue::IntervalYM { years, months } => {
354            let bytes = encode_interval_ym(*years, *months)?;
355            image_write_value_bytes(buf, &bytes)
356        }
357        BindValue::Null => {
358            image_write_null(buf, false);
359            Ok(())
360        }
361        _ => Err(ProtocolError::UnsupportedFeature(
362            "DbObject attribute type not supported for input binding",
363        )),
364    }
365}
366
367pub(crate) fn parse_binary_integer_u32(text: &str) -> Result<u32> {
368    let trimmed = text.trim();
369    let parsed: i64 = trimmed
370        .parse()
371        .map_err(|_| ProtocolError::TtcDecode("invalid BINARY_INTEGER value"))?;
372    Ok(parsed as u32)
373}
374
375/// Frames a fully-packed DbObject pickle `image` into the outgoing data row,
376/// replacing the zero stub used for empty OUT binds. Mirrors
377/// `WriteBuffer.write_dbobject` (reference impl/thin/packet.pyx:842-857). The
378/// `toid` is derived from the type `oid` per `create_new_object` (620-622).
379pub fn write_dbobject_bind(writer: &mut TtcWriter, oid: &[u8], image: &[u8]) -> Result<()> {
380    let mut toid = Vec::with_capacity(4 + oid.len() + TNS_EXTENT_OID.len());
381    toid.extend_from_slice(&[0x00, 0x22, TNS_OBJ_NON_NULL_OID, TNS_OBJ_HAS_EXTENT_OID]);
382    toid.extend_from_slice(oid);
383    toid.extend_from_slice(&TNS_EXTENT_OID);
384    writer.write_bytes_with_two_lengths(Some(&toid))?;
385    writer.write_bytes_with_two_lengths(Some(oid))?;
386    writer.write_ub4(0); // snapshot
387    writer.write_ub4(0); // version
388    writer.write_ub4(u32::try_from(image.len()).map_err(|_| {
389        ProtocolError::InvalidPacketLength {
390            length: image.len(),
391            minimum: 0,
392        }
393    })?);
394    writer.write_ub4(TNS_OBJ_TOP_LEVEL);
395    writer.write_bytes_with_length(image)
396}
397
398pub fn decode_dbobject_text(bytes: &[u8], dbtype_name: &str) -> Result<String> {
399    if matches!(dbtype_name, "DB_TYPE_NCHAR" | "DB_TYPE_NVARCHAR") {
400        let mut chunks = bytes.chunks_exact(2);
401        let units = chunks
402            .by_ref()
403            .map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]))
404            .collect::<Vec<_>>();
405        if !chunks.remainder().is_empty() {
406            return Err(ProtocolError::TtcDecode("invalid DbObject UTF-16 text"));
407        }
408        return String::from_utf16(&units)
409            .map_err(|_| ProtocolError::TtcDecode("invalid DbObject UTF-16 text"));
410    }
411    String::from_utf8(bytes.to_vec())
412        .map_err(|_| ProtocolError::TtcDecode("invalid DbObject UTF-8 text"))
413}
414
415pub fn decode_dbobject_xmltype_text(bytes: &[u8]) -> Result<Option<String>> {
416    let mut reader = DbObjectPackedReader::new(bytes);
417    reader.read_header()?;
418    reader.skip(1)?;
419    let xml_flag = reader.read_u32be()?;
420    if xml_flag & TNS_XML_TYPE_FLAG_SKIP_NEXT_4 != 0 {
421        reader.skip(4)?;
422    }
423    let bytes = reader.read_raw(reader.bytes_left())?;
424    if xml_flag & TNS_XML_TYPE_STRING != 0 {
425        return decode_dbobject_text(bytes, "DB_TYPE_VARCHAR").map(Some);
426    }
427    if xml_flag & TNS_XML_TYPE_LOB != 0 {
428        return Ok(None);
429    }
430    Err(ProtocolError::TtcDecode("unexpected XMLTYPE flag"))
431}
432
433pub fn decode_lob_text(bytes: &[u8], csfrm: u8, locator: Option<&[u8]>) -> Result<String> {
434    let (use_utf16, little_endian) = lob_text_uses_utf16(csfrm, locator);
435    if !use_utf16 {
436        // Validate UTF-8 in place over the borrowed bytes, then allocate the
437        // owned String once. Equivalent to `String::from_utf8(bytes.to_vec())`
438        // but without the temporary Vec that was copied, validated, and moved.
439        return core::str::from_utf8(bytes)
440            .map(str::to_owned)
441            .map_err(|_| ProtocolError::TtcDecode("invalid LOB UTF-8 text"));
442    }
443    // UTF-16 (almost always AL16UTF16 from the server for a multi-byte CLOB).
444    // An odd byte count is malformed; reject it before decoding, matching the
445    // previous `chunks_exact().remainder()` check.
446    if !bytes.len().is_multiple_of(2) {
447        return Err(ProtocolError::TtcDecode("invalid LOB UTF-16 text"));
448    }
449    // LOB text is overwhelmingly ASCII/Latin, where every UTF-16 code unit is a
450    // single ASCII byte (high byte 0, low byte < 0x80 in big-endian; the mirror
451    // in little-endian). Decode those inline — one `String::push` of a 1-byte
452    // char, no intermediate buffer — and only on the first non-ASCII or
453    // surrogate unit hand the *remaining* bytes to the general
454    // `char::decode_utf16` decoder. This skips the old intermediate `Vec<u16>`
455    // (a second large allocation filled by a separate byte-swap pass) for the
456    // common case while staying byte-for-byte identical to the previous
457    // `String::from_utf16` output, including its rejection of lone surrogates.
458    // The byte-index walk means the fallback never rescans what was already
459    // decoded, so the worst case matches the general decoder rather than
460    // doubling it.
461    let mut out = String::with_capacity(bytes.len() / 2);
462    let mut i = 0;
463    while i < bytes.len() {
464        let b0 = bytes[i];
465        let b1 = bytes[i + 1];
466        let is_ascii = if little_endian {
467            b1 == 0 && b0 < 0x80
468        } else {
469            b0 == 0 && b1 < 0x80
470        };
471        if is_ascii {
472            // The non-zero byte is the ASCII code point regardless of endianness.
473            let ascii = if little_endian { b0 } else { b1 };
474            out.push(ascii as char);
475            i += 2;
476        } else {
477            let units = bytes[i..].chunks_exact(2).map(|chunk| {
478                if little_endian {
479                    u16::from_le_bytes([chunk[0], chunk[1]])
480                } else {
481                    u16::from_be_bytes([chunk[0], chunk[1]])
482                }
483            });
484            for unit in char::decode_utf16(units) {
485                let ch = unit.map_err(|_| ProtocolError::TtcDecode("invalid LOB UTF-16 text"))?;
486                out.push(ch);
487            }
488            return Ok(out);
489        }
490    }
491    Ok(out)
492}
493
494/// Stateful, incremental decoder for streamed CLOB/NCLOB text.
495///
496/// [`decode_lob_text`] decodes a whole LOB buffer in one shot. A *lazy* LOB
497/// reader instead pulls the LOB in chunks, and a chunk boundary can fall in the
498/// middle of a multi-byte codepoint — an incomplete UTF-8 sequence, or (for the
499/// UTF-16 / AL16UTF16 form the server uses for multi-byte CLOBs) an odd trailing
500/// byte or a high surrogate whose low half is in the next chunk. Feeding those
501/// chunks to this decoder yields exactly the same `String` as decoding the whole
502/// buffer at once: the incomplete tail is carried across [`push`](Self::push)
503/// calls and stitched to the next chunk, and [`finish`](Self::finish) reports a
504/// truncated stream.
505///
506/// This is a new untrusted-decode surface (LOB bytes come off the wire), so its
507/// boundary handling is validated offline against `decode_lob_text` at every
508/// split point and is a fuzz target.
509#[derive(Clone, Debug)]
510pub struct LobTextDecoder {
511    use_utf16: bool,
512    little_endian: bool,
513    /// Bytes that could not yet form a complete unit: a single odd byte for the
514    /// UTF-16 form, or up to three continuation-pending bytes for UTF-8.
515    carry: Vec<u8>,
516    /// A UTF-16 high surrogate awaiting its low half in a later chunk.
517    pending_high: Option<u16>,
518}
519
520impl LobTextDecoder {
521    /// Build a decoder for the given character-set form and encoding direction.
522    pub fn new(use_utf16: bool, little_endian: bool) -> Self {
523        LobTextDecoder {
524            use_utf16,
525            little_endian,
526            carry: Vec::new(),
527            pending_high: None,
528        }
529    }
530
531    /// Build a decoder for a LOB from its `csfrm` and locator flags, matching the
532    /// same UTF-16/endianness decision [`decode_lob_text`] makes.
533    pub fn from_lob(csfrm: u8, locator: Option<&[u8]>) -> Self {
534        let (use_utf16, little_endian) = lob_text_uses_utf16(csfrm, locator);
535        Self::new(use_utf16, little_endian)
536    }
537
538    /// Decode as much of `bytes` (prefixed by any carried tail) as forms complete
539    /// codepoints, returning the newly decoded text. Any incomplete trailing
540    /// sequence is retained for the next call.
541    pub fn push(&mut self, bytes: &[u8]) -> Result<String> {
542        if self.use_utf16 {
543            self.push_utf16(bytes)
544        } else {
545            self.push_utf8(bytes)
546        }
547    }
548
549    /// Assert the stream ended on a codepoint boundary. Errors if a partial
550    /// unit or a dangling high surrogate remains (a truncated LOB stream).
551    pub fn finish(self) -> Result<()> {
552        if self.pending_high.is_some() || !self.carry.is_empty() {
553            return Err(ProtocolError::TtcDecode(if self.use_utf16 {
554                "incomplete LOB UTF-16 text"
555            } else {
556                "incomplete LOB UTF-8 text"
557            }));
558        }
559        Ok(())
560    }
561
562    fn push_utf16(&mut self, bytes: &[u8]) -> Result<String> {
563        let mut buf = Vec::with_capacity(self.carry.len() + bytes.len());
564        buf.extend_from_slice(&self.carry);
565        buf.extend_from_slice(bytes);
566        self.carry.clear();
567
568        let mut out = String::with_capacity(buf.len() / 2);
569        let mut i = 0;
570        while i + 2 <= buf.len() {
571            let unit = if self.little_endian {
572                u16::from_le_bytes([buf[i], buf[i + 1]])
573            } else {
574                u16::from_be_bytes([buf[i], buf[i + 1]])
575            };
576            i += 2;
577            self.push_unit(unit, &mut out)?;
578        }
579        if i < buf.len() {
580            // A lone trailing byte: carry it to the next chunk.
581            self.carry.push(buf[i]);
582        }
583        Ok(out)
584    }
585
586    fn push_unit(&mut self, unit: u16, out: &mut String) -> Result<()> {
587        const HIGH: core::ops::RangeInclusive<u16> = 0xD800..=0xDBFF;
588        const LOW: core::ops::RangeInclusive<u16> = 0xDC00..=0xDFFF;
589        if let Some(high) = self.pending_high.take() {
590            if LOW.contains(&unit) {
591                let combined =
592                    0x1_0000 + (((u32::from(high) - 0xD800) << 10) | (u32::from(unit) - 0xDC00));
593                let ch = char::from_u32(combined)
594                    .ok_or(ProtocolError::TtcDecode("invalid LOB UTF-16 text"))?;
595                out.push(ch);
596                return Ok(());
597            }
598            // High surrogate not followed by a low half.
599            return Err(ProtocolError::TtcDecode("invalid LOB UTF-16 text"));
600        }
601        if HIGH.contains(&unit) {
602            self.pending_high = Some(unit);
603            Ok(())
604        } else if LOW.contains(&unit) {
605            Err(ProtocolError::TtcDecode("invalid LOB UTF-16 text"))
606        } else {
607            out.push(
608                char::from_u32(u32::from(unit))
609                    .ok_or(ProtocolError::TtcDecode("invalid LOB UTF-16 text"))?,
610            );
611            Ok(())
612        }
613    }
614
615    fn push_utf8(&mut self, bytes: &[u8]) -> Result<String> {
616        let mut buf = Vec::with_capacity(self.carry.len() + bytes.len());
617        buf.extend_from_slice(&self.carry);
618        buf.extend_from_slice(bytes);
619        self.carry.clear();
620
621        match core::str::from_utf8(&buf) {
622            Ok(s) => Ok(s.to_owned()),
623            Err(err) => {
624                let valid = err.valid_up_to();
625                match err.error_len() {
626                    // Incomplete sequence at the tail: emit the valid prefix and
627                    // carry the remainder (at most three bytes) to the next chunk.
628                    None => {
629                        let out = core::str::from_utf8(&buf[..valid])
630                            .map_err(|_| ProtocolError::TtcDecode("invalid LOB UTF-8 text"))?
631                            .to_owned();
632                        self.carry.extend_from_slice(&buf[valid..]);
633                        Ok(out)
634                    }
635                    // A genuine encoding error mid-buffer.
636                    Some(_) => Err(ProtocolError::TtcDecode("invalid LOB UTF-8 text")),
637                }
638            }
639        }
640    }
641}
642
643pub fn encode_lob_text(value: &str, csfrm: u8, locator: Option<&[u8]>) -> Vec<u8> {
644    let (use_utf16, little_endian) = lob_text_uses_utf16(csfrm, locator);
645    if !use_utf16 {
646        return value.as_bytes().to_vec();
647    }
648    let mut bytes = Vec::with_capacity(value.len() * 2);
649    for unit in value.encode_utf16() {
650        let encoded = if little_endian {
651            unit.to_le_bytes()
652        } else {
653            unit.to_be_bytes()
654        };
655        bytes.extend_from_slice(&encoded);
656    }
657    bytes
658}
659
660pub fn decode_bfile_locator_name(locator: &[u8]) -> Option<(String, String)> {
661    for dir_len_pos in 0..locator.len().saturating_sub(4) {
662        let dir_len = u16::from_be_bytes([locator[dir_len_pos], locator[dir_len_pos + 1]]) as usize;
663        if dir_len == 0 {
664            continue;
665        }
666        let dir_start = dir_len_pos + 2;
667        let dir_end = dir_start.checked_add(dir_len)?;
668        let file_len_end = dir_end.checked_add(2)?;
669        if file_len_end > locator.len() {
670            continue;
671        }
672        let file_len = u16::from_be_bytes([locator[dir_end], locator[dir_end + 1]]) as usize;
673        if file_len == 0 {
674            continue;
675        }
676        let file_start = file_len_end;
677        let file_end = file_start.checked_add(file_len)?;
678        if file_end != locator.len() {
679            continue;
680        }
681        let dir = std::str::from_utf8(&locator[dir_start..dir_end]).ok()?;
682        let file = std::str::from_utf8(&locator[file_start..file_end]).ok()?;
683        return Some((dir.to_string(), file.to_string()));
684    }
685    None
686}
687
688pub(crate) fn lob_text_uses_utf16(csfrm: u8, locator: Option<&[u8]>) -> (bool, bool) {
689    let use_utf16 = csfrm == CS_FORM_NCHAR
690        || locator
691            .and_then(|locator| locator.get(TNS_LOB_LOC_OFFSET_FLAG_3))
692            .is_some_and(|flags| flags & TNS_LOB_LOC_FLAGS_VAR_LENGTH_CHARSET != 0);
693    let little_endian = locator
694        .and_then(|locator| locator.get(TNS_LOB_LOC_OFFSET_FLAG_4))
695        .is_some_and(|flags| flags & TNS_LOB_LOC_FLAGS_LITTLE_ENDIAN != 0);
696    (use_utf16, little_endian)
697}
698
699pub fn decode_dbobject_binary_float(bytes: &[u8]) -> Result<f32> {
700    let mut bytes: [u8; 4] = bytes
701        .try_into()
702        .map_err(|_| ProtocolError::TtcDecode("invalid DbObject BINARY_FLOAT"))?;
703    if bytes[0] & 0x80 != 0 {
704        bytes[0] &= 0x7f;
705    } else {
706        for byte in &mut bytes {
707            *byte = !*byte;
708        }
709    }
710    Ok(f32::from_bits(u32::from_be_bytes(bytes)))
711}
712
713pub fn decode_dbobject_binary_double(bytes: &[u8]) -> Result<f64> {
714    let mut bytes: [u8; 8] = bytes
715        .try_into()
716        .map_err(|_| ProtocolError::TtcDecode("invalid DbObject BINARY_DOUBLE"))?;
717    if bytes[0] & 0x80 != 0 {
718        bytes[0] &= 0x7f;
719    } else {
720        for byte in &mut bytes {
721            *byte = !*byte;
722        }
723    }
724    Ok(f64::from_bits(u64::from_be_bytes(bytes)))
725}
726
727#[cfg(test)]
728mod value_bytes_tests {
729    use super::*;
730
731    #[test]
732    fn value_bytes_roundtrip_uses_dbobject_single_u32be_length_format() {
733        for len in [244usize, 245, 246, 250, 65_535, 70_000] {
734            let value = (0..len).map(|i| (i % 251) as u8).collect::<Vec<_>>();
735            let mut image = Vec::new();
736            image_write_value_bytes(&mut image, &value).expect("encode value bytes");
737
738            let mut expected = Vec::new();
739            if len <= TNS_OBJ_MAX_SHORT_LENGTH {
740                expected.push(len as u8);
741            } else {
742                expected.push(TNS_LONG_LENGTH_INDICATOR);
743                expected.extend_from_slice(&(len as u32).to_be_bytes());
744            }
745            expected.extend_from_slice(&value);
746            assert_eq!(image, expected, "wire bytes for length {len}");
747
748            if len == 250 {
749                assert_eq!(
750                    &image[..5],
751                    &[TNS_LONG_LENGTH_INDICATOR, 0x00, 0x00, 0x00, 0xfa],
752                    "250-byte values use one big-endian u32 length"
753                );
754            }
755
756            let mut reader = DbObjectPackedReader::new(&image);
757            let decoded = reader
758                .read_value_bytes()
759                .expect("decode value bytes")
760                .expect("non-null value bytes");
761            assert_eq!(decoded, value, "length {len}");
762            assert_eq!(reader.remaining(), 0, "length {len}");
763        }
764    }
765}
766
767#[cfg(test)]
768mod bounded_reader_tests {
769    use super::*;
770    use crate::wire::BoundedReader;
771
772    // BoundedReader invariant (l2p), DbObject collection family: a packed image
773    // declaring a huge collection element count (via the long-length indicator
774    // + a ub4 ~620M) but carrying no element bytes must NOT drive a
775    // gigabyte-scale Vec pre-allocation. The collection decode loop lives in the
776    // pyshim, but the bound is structural: DbObjectPackedReader exposes
777    // `remaining()` so the count can be checked/capped against the buffer.
778    #[test]
779    fn dbobject_oversized_collection_count_is_bounded_by_remaining() {
780        // read_length long form: 0xfe then ub4 0x25000000 (~620M), no elements.
781        let bytes = [TNS_LONG_LENGTH_INDICATOR, 0x25, 0x00, 0x00, 0x00];
782        let mut reader = DbObjectPackedReader::new(&bytes);
783        let num_elements = reader.read_length().expect("length decodes");
784        assert_eq!(num_elements, 0x2500_0000);
785
786        // Only the (now zero) remaining bytes can be honestly allocated: an
787        // element needs at least one byte, so alloc_count_checked must reject
788        // the lie rather than letting a caller reserve ~620M slots.
789        assert!(
790            reader.alloc_count_checked(num_elements, 1).is_err(),
791            "declared count must not exceed the empty remaining buffer"
792        );
793        // The cap-and-grow flavor caps the pre-allocation at remaining() (0).
794        let v: Vec<u32> = reader.with_capacity_bounded(num_elements, 1);
795        assert_eq!(
796            v.capacity(),
797            0,
798            "pre-allocation must be capped by remaining"
799        );
800    }
801
802    // A legitimate small collection count whose elements really fit passes
803    // through unchanged (no false rejection of valid DbObjects).
804    #[test]
805    fn dbobject_legitimate_collection_count_passes() {
806        // 8 bytes of element payload remaining, two declared elements.
807        let bytes = [1u8, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11];
808        let reader = DbObjectPackedReader::new(&bytes);
809        assert_eq!(reader.alloc_count_checked(2, 1).expect("fits"), 2);
810        let v: Vec<u32> = reader.with_capacity_bounded(2, 1);
811        assert_eq!(v.capacity(), 2);
812    }
813}
814
815#[cfg(test)]
816mod decode_lob_text_tests {
817    use super::*;
818
819    /// A locator that drives the UTF-16 decode path, with selectable endianness.
820    fn utf16_locator(little_endian: bool) -> Vec<u8> {
821        let mut loc = vec![0u8; 40];
822        loc[TNS_LOB_LOC_OFFSET_FLAG_3] = TNS_LOB_LOC_FLAGS_VAR_LENGTH_CHARSET;
823        if little_endian {
824            loc[TNS_LOB_LOC_OFFSET_FLAG_4] = TNS_LOB_LOC_FLAGS_LITTLE_ENDIAN;
825        }
826        loc
827    }
828
829    fn encode_utf16(s: &str, little_endian: bool) -> Vec<u8> {
830        let mut bytes = Vec::with_capacity(s.len() * 2);
831        for unit in s.encode_utf16() {
832            let pair = if little_endian {
833                unit.to_le_bytes()
834            } else {
835                unit.to_be_bytes()
836            };
837            bytes.extend_from_slice(&pair);
838        }
839        bytes
840    }
841
842    /// Reference decoder = the previous implementation, used as the isomorphism
843    /// oracle for the optimized `decode_lob_text`.
844    fn reference_from_utf16(bytes: &[u8], little_endian: bool) -> Result<String> {
845        let mut chunks = bytes.chunks_exact(2);
846        let units = chunks
847            .by_ref()
848            .map(|chunk| {
849                if little_endian {
850                    u16::from_le_bytes([chunk[0], chunk[1]])
851                } else {
852                    u16::from_be_bytes([chunk[0], chunk[1]])
853                }
854            })
855            .collect::<Vec<_>>();
856        if !chunks.remainder().is_empty() {
857            return Err(ProtocolError::TtcDecode("invalid LOB UTF-16 text"));
858        }
859        String::from_utf16(&units).map_err(|_| ProtocolError::TtcDecode("invalid LOB UTF-16 text"))
860    }
861
862    #[test]
863    fn utf16_matches_reference_for_varied_text_both_endians() {
864        let samples = [
865            "",
866            "a",
867            "the quick brown fox 0123456789",
868            "café résumé naïve",           // BMP non-ASCII (Latin-1 supplement)
869            "ASCII then 漢字 then more",   // BMP CJK
870            "emoji: 😀🎉 mixed with text", // surrogate pairs
871            "\u{0000}\u{007f}\u{0080}\u{07ff}\u{0800}\u{ffff}", // boundary code points
872        ];
873        for sample in samples {
874            for little_endian in [false, true] {
875                let bytes = encode_utf16(sample, little_endian);
876                let loc = utf16_locator(little_endian);
877                let got =
878                    decode_lob_text(&bytes, CS_FORM_NCHAR, Some(&loc)).expect("optimized decode");
879                let expected =
880                    reference_from_utf16(&bytes, little_endian).expect("reference decode");
881                assert_eq!(got, expected, "sample {sample:?} le={little_endian}");
882                assert_eq!(got, sample);
883            }
884        }
885    }
886
887    #[test]
888    fn utf16_odd_length_is_rejected_like_reference() {
889        let loc = utf16_locator(false);
890        // 3 bytes: one full unit plus a dangling byte.
891        let bytes = [0x00, 0x41, 0x00];
892        assert!(decode_lob_text(&bytes, CS_FORM_NCHAR, Some(&loc)).is_err());
893        assert!(reference_from_utf16(&bytes, false).is_err());
894    }
895
896    #[test]
897    fn utf16_lone_surrogate_is_rejected_like_reference() {
898        let loc = utf16_locator(false);
899        // ASCII prefix then a lone high surrogate (no following low surrogate).
900        let mut bytes = encode_utf16("ok ", false);
901        bytes.extend_from_slice(&0xD83Du16.to_be_bytes());
902        bytes.extend_from_slice(&encode_utf16("tail", false));
903        assert!(decode_lob_text(&bytes, CS_FORM_NCHAR, Some(&loc)).is_err());
904        assert!(reference_from_utf16(&bytes, false).is_err());
905    }
906
907    #[test]
908    fn utf8_path_matches_from_utf8() {
909        // csfrm != NCHAR and no UTF-16 locator flag -> UTF-8 path.
910        let loc = vec![0u8; 40];
911        let sample = "café — utf8 path ✓";
912        let bytes = sample.as_bytes();
913        let got = decode_lob_text(bytes, 1, Some(&loc)).expect("utf8 decode");
914        assert_eq!(
915            got,
916            String::from_utf8(bytes.to_vec()).expect("sample is valid UTF-8")
917        );
918        assert_eq!(got, sample);
919        // invalid UTF-8 errors like String::from_utf8.
920        let bad = [0x66, 0x6f, 0xff, 0x6f];
921        assert!(decode_lob_text(&bad, 1, Some(&loc)).is_err());
922    }
923
924    /// Drive the streaming decoder over `bytes` split into fixed `chunk`-byte
925    /// pieces (so multi-byte codepoints and surrogate pairs land across the
926    /// boundary), concatenating what each `push` returns.
927    fn stream_utf16(bytes: &[u8], little_endian: bool, chunk: usize) -> Result<String> {
928        let mut dec = LobTextDecoder::new(true, little_endian);
929        let mut out = String::new();
930        for piece in bytes.chunks(chunk.max(1)) {
931            out.push_str(&dec.push(piece)?);
932        }
933        dec.finish()?;
934        Ok(out)
935    }
936
937    #[test]
938    fn streaming_utf16_matches_whole_decode_at_every_boundary() {
939        // Astral codepoints (surrogate pairs) interleaved with BMP + ASCII so a
940        // 1-, 2-, or 3-byte chunk always splits a pair somewhere.
941        let samples = [
942            "emoji 😀 party 🎉🎊 end",
943            "漢字 😀 café 🚀 mix",
944            "\u{10000}\u{10FFFF} boundary astral",
945            "plain ascii only",
946            "",
947        ];
948        for sample in samples {
949            for little_endian in [false, true] {
950                let bytes = encode_utf16(sample, little_endian);
951                let loc = utf16_locator(little_endian);
952                let whole =
953                    decode_lob_text(&bytes, CS_FORM_NCHAR, Some(&loc)).expect("whole decode");
954                assert_eq!(whole, sample);
955                // Every chunk size from a single byte upward, plus a size that
956                // exceeds the buffer (single push).
957                for chunk in 1..=bytes.len().max(1) + 1 {
958                    let streamed = stream_utf16(&bytes, little_endian, chunk).unwrap_or_else(|e| {
959                        panic!("stream {sample:?} le={little_endian} chunk={chunk}: {e:?}")
960                    });
961                    assert_eq!(
962                        streamed, whole,
963                        "sample {sample:?} le={little_endian} chunk={chunk}"
964                    );
965                }
966            }
967        }
968    }
969
970    #[test]
971    fn streaming_utf8_matches_whole_decode_at_every_boundary() {
972        let samples = ["café — utf8 ✓ 漢字 😀 tail", "ascii", ""];
973        for sample in samples {
974            let bytes = sample.as_bytes();
975            for chunk in 1..=bytes.len().max(1) + 1 {
976                let mut dec = LobTextDecoder::new(false, false);
977                let mut out = String::new();
978                for piece in bytes.chunks(chunk) {
979                    out.push_str(&dec.push(piece).expect("utf8 push"));
980                }
981                dec.finish().expect("utf8 finish");
982                assert_eq!(out, sample, "sample {sample:?} chunk={chunk}");
983            }
984        }
985    }
986
987    #[test]
988    fn streaming_finish_rejects_truncated_streams() {
989        // Dangling high surrogate (pushed whole, no low half).
990        let mut dec = LobTextDecoder::new(true, false);
991        dec.push(&0xD83Du16.to_be_bytes()).expect("push high");
992        assert!(dec.finish().is_err(), "dangling high surrogate must fail");
993
994        // Odd trailing byte carried with no partner.
995        let mut dec = LobTextDecoder::new(true, false);
996        assert_eq!(dec.push(&[0x00]).expect("push odd"), "");
997        assert!(dec.finish().is_err(), "odd trailing byte must fail");
998
999        // Incomplete UTF-8 sequence at the tail (lead byte of a 3-byte codepoint).
1000        let mut dec = LobTextDecoder::new(false, false);
1001        assert_eq!(dec.push(&[0xE6]).expect("push utf8 lead"), "");
1002        assert!(dec.finish().is_err(), "incomplete UTF-8 tail must fail");
1003    }
1004
1005    #[test]
1006    fn streaming_rejects_lone_low_surrogate_like_whole() {
1007        // A low surrogate with no preceding high half is invalid mid-stream.
1008        let mut dec = LobTextDecoder::new(true, false);
1009        assert!(dec.push(&0xDC00u16.to_be_bytes()).is_err());
1010    }
1011}