Skip to main content

oracledb_protocol/
vector.rs

1//! Oracle VECTOR wire codec (reference `impl/base/vector.pyx`).
2//!
3//! A VECTOR value is serialized as a self-describing binary "image" that the
4//! server stores/returns inside a LOB wrapper (see `parse_vector_value` /
5//! `write_vector_bind` in `thin.rs`). This module is concerned only with the
6//! image itself: the header, the element values, and the optional sparse
7//! index list.
8//!
9//! Image layout (all multi-byte integers are big-endian):
10//!
11//! ```text
12//!   u8   magic byte (0xDB)
13//!   u8   version (0 base / 1 binary / 2 sparse)
14//!   u16  flags
15//!   u8   element format (2=f32, 3=f64, 4=int8, 5=binary)
16//!   u32  num_elements  (for binary format: number of *bits*; for sparse:
17//!                       the number of dimensions)
18//!   [8]  reserved norm space (zero on write; skipped on read when a NORM
19//!        flag is set)
20//!   -- dense:  num_elements values
21//!   -- sparse: u16 num_sparse_elements,
22//!              num_sparse_elements * u32 indices,
23//!              num_sparse_elements values
24//! ```
25//!
26//! The codec is fail-closed: unknown magic bytes, versions, or element
27//! formats produce an error rather than a best-effort guess.
28
29use crate::wire::{BoundedReader, ProtocolLimits, TtcReader, TtcWriter};
30use crate::{ProtocolError, Result};
31
32/// VECTOR image magic byte (`TNS_VECTOR_MAGIC_BYTE`).
33pub const TNS_VECTOR_MAGIC_BYTE: u8 = 0xDB;
34
35/// VECTOR image versions (`TNS_VECTOR_VERSION_*`).
36pub const TNS_VECTOR_VERSION_BASE: u8 = 0;
37pub const TNS_VECTOR_VERSION_WITH_BINARY: u8 = 1;
38pub const TNS_VECTOR_VERSION_WITH_SPARSE: u8 = 2;
39
40/// VECTOR image flags (`TNS_VECTOR_FLAG_*`).
41pub const TNS_VECTOR_FLAG_NORM: u16 = 0x0002;
42pub const TNS_VECTOR_FLAG_NORM_RESERVED: u16 = 0x0010;
43pub const TNS_VECTOR_FLAG_SPARSE: u16 = 0x0020;
44
45/// VECTOR element storage formats (`VECTOR_FORMAT_*`).
46pub const VECTOR_FORMAT_FLOAT32: u8 = 2;
47pub const VECTOR_FORMAT_FLOAT64: u8 = 3;
48pub const VECTOR_FORMAT_INT8: u8 = 4;
49pub const VECTOR_FORMAT_BINARY: u8 = 5;
50
51/// Decoded VECTOR element values, one variant per storage format.
52///
53/// The variant determines both the wire encoding and the Python `array.array`
54/// typecode the shim layer materializes (`f`/`d`/`b`/`B`).
55#[derive(Clone, Debug, PartialEq)]
56pub enum VectorValues {
57    /// FLOAT32 elements (`array.array('f')`).
58    Float32(Vec<f32>),
59    /// FLOAT64 elements (`array.array('d')`).
60    Float64(Vec<f64>),
61    /// INT8 elements (`array.array('b')`).
62    Int8(Vec<i8>),
63    /// BINARY elements: one byte packs 8 dimensions (`array.array('B')`).
64    Binary(Vec<u8>),
65}
66
67impl VectorValues {
68    /// Storage format byte for these values.
69    pub fn format(&self) -> u8 {
70        match self {
71            VectorValues::Float32(_) => VECTOR_FORMAT_FLOAT32,
72            VectorValues::Float64(_) => VECTOR_FORMAT_FLOAT64,
73            VectorValues::Int8(_) => VECTOR_FORMAT_INT8,
74            VectorValues::Binary(_) => VECTOR_FORMAT_BINARY,
75        }
76    }
77
78    /// Number of stored elements (for BINARY this is the byte count, i.e.
79    /// num_dimensions / 8).
80    pub fn len(&self) -> usize {
81        match self {
82            VectorValues::Float32(v) => v.len(),
83            VectorValues::Float64(v) => v.len(),
84            VectorValues::Int8(v) => v.len(),
85            VectorValues::Binary(v) => v.len(),
86        }
87    }
88
89    pub fn is_empty(&self) -> bool {
90        self.len() == 0
91    }
92}
93
94/// A decoded VECTOR value: either dense (values only) or sparse (a dimension
95/// count plus parallel index/value arrays of the non-zero entries).
96#[derive(Clone, Debug, PartialEq)]
97pub enum Vector {
98    Dense(VectorValues),
99    Sparse {
100        num_dimensions: u32,
101        indices: Vec<u32>,
102        values: VectorValues,
103    },
104}
105
106/// Decode a VECTOR image (the bytes carried inside the LOB wrapper).
107pub fn decode_vector(data: &[u8]) -> Result<Vector> {
108    decode_vector_with_limits(data, ProtocolLimits::DEFAULT)
109}
110
111/// Decode a VECTOR image under the caller's protocol resource policy.
112pub fn decode_vector_with_limits(data: &[u8], limits: ProtocolLimits) -> Result<Vector> {
113    limits.check_response_bytes(data.len())?;
114    let mut reader = TtcReader::with_limits(data, limits)?;
115
116    let magic = reader.read_u8()?;
117    if magic != TNS_VECTOR_MAGIC_BYTE {
118        return Err(ProtocolError::TtcDecode("vector: bad magic byte"));
119    }
120    let version = reader.read_u8()?;
121    if version > TNS_VECTOR_VERSION_WITH_SPARSE {
122        return Err(ProtocolError::TtcDecode("vector: unsupported version"));
123    }
124    let flags = read_u16be(&mut reader)?;
125    let format = reader.read_u8()?;
126    let mut num_elements = read_u32be(&mut reader)?;
127    reader
128        .limits()
129        .check_vector_dimensions(num_elements as usize)?;
130    if flags & TNS_VECTOR_FLAG_NORM_RESERVED != 0 || flags & TNS_VECTOR_FLAG_NORM != 0 {
131        reader.skip(8)?;
132    }
133
134    if flags & TNS_VECTOR_FLAG_SPARSE != 0 {
135        let num_dimensions = num_elements;
136        let num_sparse = read_u16be(&mut reader)?;
137        reader
138            .limits()
139            .check_vector_dimensions(usize::from(num_sparse))?;
140        // Each sparse index is a 4-byte u32 on the wire, so bound the
141        // pre-allocation by the buffer (BoundedReader invariant): a declared
142        // count larger than remaining()/4 cannot be honest.
143        let mut indices: Vec<u32> = reader.with_capacity_limited(
144            usize::from(num_sparse),
145            4,
146            ProtocolLimits::check_vector_dimensions,
147        )?;
148        for _ in 0..num_sparse {
149            indices.push(read_u32be(&mut reader)?);
150        }
151        let values = decode_values(&mut reader, u32::from(num_sparse), format)?;
152        ensure_vector_image_fully_consumed(&reader)?;
153        return Ok(Vector::Sparse {
154            num_dimensions,
155            indices,
156            values,
157        });
158    }
159
160    // dense binary format encodes the bit-count; values are bytes
161    if format == VECTOR_FORMAT_BINARY {
162        if num_elements % 8 != 0 {
163            return Err(ProtocolError::TtcDecode(
164                "vector: binary dimension count is not byte-aligned",
165            ));
166        }
167        num_elements /= 8;
168    }
169    let values = decode_values(&mut reader, num_elements, format)?;
170    ensure_vector_image_fully_consumed(&reader)?;
171    Ok(Vector::Dense(values))
172}
173
174fn ensure_vector_image_fully_consumed(reader: &TtcReader<'_>) -> Result<()> {
175    if reader.remaining() == 0 {
176        return Ok(());
177    }
178    Err(ProtocolError::TtcDecode(
179        "vector: trailing bytes after image payload",
180    ))
181}
182
183fn decode_values(reader: &mut TtcReader<'_>, count: u32, format: u8) -> Result<VectorValues> {
184    let count = count as usize;
185    reader.limits().check_vector_dimensions(count)?;
186    // `count` is read straight off the wire (a u32, up to ~4e9). Reserving that
187    // many elements up front lets a hostile/buggy server force a multi-gigabyte
188    // allocation (OOM) before the first element read even fails on truncation.
189    // A legitimate image always carries `count * element_size` value bytes, so
190    // `BoundedReader::with_capacity_bounded` caps the reservation by what
191    // remains in the buffer — never affecting a valid vector while making the
192    // allocation fail-closed. The per-element `read_raw` below still
193    // bounds-checks each read.
194    match format {
195        VECTOR_FORMAT_FLOAT32 => {
196            let mut out: Vec<f32> =
197                reader.with_capacity_limited(count, 4, ProtocolLimits::check_vector_dimensions)?;
198            for _ in 0..count {
199                let raw = reader.read_raw(4)?;
200                out.push(decode_binary_float([raw[0], raw[1], raw[2], raw[3]]));
201            }
202            Ok(VectorValues::Float32(out))
203        }
204        VECTOR_FORMAT_FLOAT64 => {
205            let mut out: Vec<f64> =
206                reader.with_capacity_limited(count, 8, ProtocolLimits::check_vector_dimensions)?;
207            for _ in 0..count {
208                let raw = reader.read_raw(8)?;
209                out.push(decode_binary_double([
210                    raw[0], raw[1], raw[2], raw[3], raw[4], raw[5], raw[6], raw[7],
211                ]));
212            }
213            Ok(VectorValues::Float64(out))
214        }
215        VECTOR_FORMAT_INT8 => {
216            let mut out: Vec<i8> =
217                reader.with_capacity_limited(count, 1, ProtocolLimits::check_vector_dimensions)?;
218            for _ in 0..count {
219                out.push(reader.read_u8()? as i8);
220            }
221            Ok(VectorValues::Int8(out))
222        }
223        VECTOR_FORMAT_BINARY => Ok(VectorValues::Binary(reader.read_raw(count)?.to_vec())),
224        _ => Err(ProtocolError::TtcDecode(
225            "vector: unsupported element format",
226        )),
227    }
228}
229
230/// Encode a VECTOR value into its image (the bytes that go inside the LOB
231/// wrapper). Mirrors `VectorEncoder.encode` in `vector.pyx`.
232///
233/// # Panics
234///
235/// Panics if `vector` is not a valid VECTOR value (e.g. a sparse vector whose
236/// index/value counts disagree or whose dimension exceeds the protocol limit).
237/// This is the infallible convenience wrapper; callers that accept
238/// caller-constructed (potentially invalid) vectors should use the crate-local
239/// `encode_vector_checked`, which returns the error instead of panicking.
240pub fn encode_vector(vector: &Vector) -> Vec<u8> {
241    match encode_vector_checked(vector) {
242        Ok(image) => image,
243        Err(err) => panic!("invalid VECTOR value for encoding: {err}"),
244    }
245}
246
247pub(crate) fn encode_vector_checked(vector: &Vector) -> Result<Vec<u8>> {
248    let mut buf = Vec::new();
249
250    let mut flags = TNS_VECTOR_FLAG_NORM_RESERVED;
251    let (format, version, num_elements) = match vector {
252        Vector::Sparse {
253            num_dimensions,
254            values,
255            ..
256        } => {
257            flags |= TNS_VECTOR_FLAG_SPARSE | TNS_VECTOR_FLAG_NORM;
258            (
259                values.format(),
260                TNS_VECTOR_VERSION_WITH_SPARSE,
261                *num_dimensions,
262            )
263        }
264        Vector::Dense(values) => {
265            let format = values.format();
266            if format == VECTOR_FORMAT_BINARY {
267                (
268                    format,
269                    TNS_VECTOR_VERSION_WITH_BINARY,
270                    (values.len() as u32) * 8,
271                )
272            } else {
273                flags |= TNS_VECTOR_FLAG_NORM;
274                (format, TNS_VECTOR_VERSION_BASE, values.len() as u32)
275            }
276        }
277    };
278
279    buf.push(TNS_VECTOR_MAGIC_BYTE);
280    buf.push(version);
281    buf.extend_from_slice(&flags.to_be_bytes());
282    buf.push(format);
283    buf.extend_from_slice(&num_elements.to_be_bytes());
284    buf.extend_from_slice(&[0u8; 8]); // reserved norm space
285
286    match vector {
287        Vector::Dense(values) => encode_values(&mut buf, values),
288        Vector::Sparse {
289            indices, values, ..
290        } => {
291            if indices.len() != values.len() {
292                return Err(ProtocolError::TtcDecode(
293                    "vector: sparse index/value count mismatch",
294                ));
295            }
296            let num_sparse =
297                u16::try_from(indices.len()).map_err(|_| ProtocolError::InvalidPacketLength {
298                    length: indices.len(),
299                    minimum: 0,
300                })?;
301            buf.extend_from_slice(&num_sparse.to_be_bytes());
302            for index in indices {
303                buf.extend_from_slice(&index.to_be_bytes());
304            }
305            encode_values(&mut buf, values);
306        }
307    }
308
309    Ok(buf)
310}
311
312fn encode_values(buf: &mut Vec<u8>, values: &VectorValues) {
313    match values {
314        VectorValues::Float32(v) => {
315            for value in v {
316                buf.extend_from_slice(&encode_binary_float(*value));
317            }
318        }
319        VectorValues::Float64(v) => {
320            for value in v {
321                buf.extend_from_slice(&encode_binary_double(*value));
322            }
323        }
324        VectorValues::Int8(v) => {
325            for value in v {
326                buf.push(*value as u8);
327            }
328        }
329        VectorValues::Binary(v) => buf.extend_from_slice(v),
330    }
331}
332
333// VECTOR float elements are stored in Oracle's BINARY_FLOAT / BINARY_DOUBLE wire
334// form (reference `VectorDecoder._decode_values` / `VectorEncoder._encode_values`
335// in `impl/base/vector.pyx`, which call `decode_binary_float` / `encode_binary_double`
336// from `decoders.pyx` / `encoders.pyx`), NOT plain IEEE-754 big-endian. The
337// transform makes the byte order sort-comparable: a positive value gets its sign
338// bit set; a negative value has every bit inverted.
339
340/// Decode an Oracle BINARY_DOUBLE-encoded f64 element.
341fn decode_binary_double(bytes: [u8; 8]) -> f64 {
342    let mut decoded = bytes;
343    if decoded[0] & 0x80 != 0 {
344        decoded[0] &= 0x7f;
345    } else {
346        for byte in &mut decoded {
347            *byte = !*byte;
348        }
349    }
350    f64::from_bits(u64::from_be_bytes(decoded))
351}
352
353/// Decode an Oracle BINARY_FLOAT-encoded f32 element.
354fn decode_binary_float(bytes: [u8; 4]) -> f32 {
355    let mut decoded = bytes;
356    if decoded[0] & 0x80 != 0 {
357        decoded[0] &= 0x7f;
358    } else {
359        for byte in &mut decoded {
360            *byte = !*byte;
361        }
362    }
363    f32::from_bits(u32::from_be_bytes(decoded))
364}
365
366/// Encode an f64 element in Oracle BINARY_DOUBLE wire form.
367fn encode_binary_double(value: f64) -> [u8; 8] {
368    let mut bytes = value.to_bits().to_be_bytes();
369    if bytes[0] & 0x80 == 0 {
370        bytes[0] |= 0x80;
371    } else {
372        for byte in &mut bytes {
373            *byte = !*byte;
374        }
375    }
376    bytes
377}
378
379/// Encode an f32 element in Oracle BINARY_FLOAT wire form.
380fn encode_binary_float(value: f32) -> [u8; 4] {
381    let mut bytes = value.to_bits().to_be_bytes();
382    if bytes[0] & 0x80 == 0 {
383        bytes[0] |= 0x80;
384    } else {
385        for byte in &mut bytes {
386            *byte = !*byte;
387        }
388    }
389    bytes
390}
391
392// VECTOR images use plain big-endian fixed-width integers in the header (not
393// the TTC ubN variable-length forms), so read them directly from raw bytes.
394fn read_u16be(reader: &mut TtcReader<'_>) -> Result<u16> {
395    let raw = reader.read_raw(2)?;
396    Ok(u16::from_be_bytes([raw[0], raw[1]]))
397}
398
399fn read_u32be(reader: &mut TtcReader<'_>) -> Result<u32> {
400    let raw = reader.read_raw(4)?;
401    Ok(u32::from_be_bytes([raw[0], raw[1], raw[2], raw[3]]))
402}
403
404/// Convenience for the bind path: a VECTOR image written inside the LOB
405/// wrapper is the qlocator (40 bytes, data-length encoded) followed by the
406/// raw image bytes-with-length. This helper writes just that pair given a
407/// pre-encoded image, mirroring `write_vector` -> `write_qlocator` +
408/// `_write_raw_bytes_and_length` in `packet.pyx`.
409pub fn write_vector_image(writer: &mut TtcWriter, image: &[u8]) -> Result<()> {
410    write_qlocator(writer, image.len() as u64, true);
411    writer.write_bytes_with_length(image)?;
412    Ok(())
413}
414
415/// Writes an OSON image as an AQ JSON payload (reference `write_oson` with
416/// `write_length=False`): a QLocator without the 1-byte chunk-length prefix,
417/// followed by the OSON bytes as `_write_raw_bytes_and_length`.
418pub fn write_oson_aq_payload(writer: &mut TtcWriter, image: &[u8]) -> Result<()> {
419    write_qlocator(writer, image.len() as u64, false);
420    writer.write_bytes_with_length(image)?;
421    Ok(())
422}
423
424/// Writes a 40-byte QLocator carrying the data length (reference
425/// `write_qlocator` in `packet.pyx`). `write_length` controls the 1-byte
426/// chunk-length prefix (present for VECTOR/JSON binds, absent for the AQ JSON
427/// payload path).
428fn write_qlocator(writer: &mut TtcWriter, data_length: u64, write_length: bool) {
429    const TNS_LOB_QLOCATOR_VERSION: u16 = 4;
430    const TNS_LOB_LOC_FLAGS_VALUE_BASED: u8 = 0x20;
431    const TNS_LOB_LOC_FLAGS_BLOB: u8 = 0x01;
432    const TNS_LOB_LOC_FLAGS_ABSTRACT: u8 = 0x40;
433    const TNS_LOB_LOC_FLAGS_INIT: u8 = 0x08;
434
435    writer.write_ub4(40); // QLocator length
436    if write_length {
437        writer.write_u8(40); // chunk length
438    }
439    writer.write_u16be(38); // QLocator length less 2 bytes
440    writer.write_u16be(TNS_LOB_QLOCATOR_VERSION);
441    writer.write_u8(
442        TNS_LOB_LOC_FLAGS_VALUE_BASED | TNS_LOB_LOC_FLAGS_BLOB | TNS_LOB_LOC_FLAGS_ABSTRACT,
443    );
444    writer.write_u8(TNS_LOB_LOC_FLAGS_INIT);
445    writer.write_u16be(0); // additional flags
446    writer.write_u16be(1); // byt1
447    writer.write_u64be(data_length);
448    writer.write_u16be(0); // unused
449    writer.write_u16be(0); // csid
450    writer.write_u16be(0); // unused
451    writer.write_u64be(0); // unused
452    writer.write_u64be(0); // unused
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458    use serde_json::Value;
459
460    fn roundtrip(vector: Vector) {
461        let image = encode_vector(&vector);
462        let decoded = decode_vector(&image).expect("decode");
463        assert_eq!(decoded, vector);
464    }
465
466    // BoundedReader invariant (l2p), behavior-preservation: a legitimately
467    // large vector (where count * element_size really fits the buffer) must
468    // still decode in full. The bound is "can't exceed what's in the buffer,"
469    // not an arbitrary small cap, so real large results are unaffected.
470    #[test]
471    fn legitimate_large_vector_still_decodes_fully() {
472        let big_f32: Vec<f32> = (0..4096).map(|i| i as f32 * 0.5 - 1024.0).collect();
473        roundtrip(Vector::Dense(VectorValues::Float32(big_f32)));
474        let big_f64: Vec<f64> = (0..2048).map(|i| i as f64 * 0.25).collect();
475        roundtrip(Vector::Dense(VectorValues::Float64(big_f64)));
476        // A large sparse vector exercises the bounded sparse-index path.
477        roundtrip(Vector::Sparse {
478            num_dimensions: 100_000,
479            indices: (0..1000).map(|i| i * 7).collect(),
480            values: VectorValues::Float32((0..1000).map(|i| i as f32).collect()),
481        });
482    }
483
484    #[test]
485    fn roundtrips_every_dense_format() {
486        roundtrip(Vector::Dense(VectorValues::Float32(vec![
487            1.5, -2.25, 3.0, 0.0,
488        ])));
489        roundtrip(Vector::Dense(VectorValues::Float64(vec![
490            6501.0, 25.25, 18.125, -3.5,
491        ])));
492        roundtrip(Vector::Dense(VectorValues::Int8(vec![
493            -5, 1, -2, 127, -128,
494        ])));
495        roundtrip(Vector::Dense(VectorValues::Binary(vec![0xA5, 0x3C])));
496    }
497
498    #[test]
499    fn roundtrips_every_sparse_format() {
500        roundtrip(Vector::Sparse {
501            num_dimensions: 8,
502            indices: vec![1, 4, 6],
503            values: VectorValues::Float64(vec![1.5, -2.0, 9.25]),
504        });
505        roundtrip(Vector::Sparse {
506            num_dimensions: 6,
507            indices: vec![0, 3],
508            values: VectorValues::Float32(vec![2.5, -7.0]),
509        });
510        roundtrip(Vector::Sparse {
511            num_dimensions: 5,
512            indices: vec![2],
513            values: VectorValues::Int8(vec![42]),
514        });
515    }
516
517    #[test]
518    fn sparse_int8_roundtrips_max_u16_count() {
519        let indices = (0..u16::MAX).map(u32::from).collect::<Vec<_>>();
520        let values = VectorValues::Int8((0..u16::MAX).map(|i| (i % 127) as i8).collect::<Vec<_>>());
521        let vector = Vector::Sparse {
522            num_dimensions: u32::from(u16::MAX),
523            indices,
524            values,
525        };
526
527        let image = encode_vector_checked(&vector).expect("encode max u16 sparse vector");
528        let decoded = decode_vector(&image).expect("decode max u16 sparse vector");
529        assert_eq!(decoded, vector);
530    }
531
532    #[test]
533    fn sparse_int8_rejects_count_that_exceeds_wire_field() {
534        let count = usize::from(u16::MAX) + 1;
535        let vector = Vector::Sparse {
536            num_dimensions: count as u32,
537            indices: (0..count as u32).collect(),
538            values: VectorValues::Int8(vec![1; count]),
539        };
540
541        let err = encode_vector_checked(&vector).expect_err("oversized sparse count must fail");
542        assert!(
543            matches!(
544                err,
545                ProtocolError::InvalidPacketLength {
546                    length,
547                    minimum: 0
548                } if length == count
549            ),
550            "got {err:?}"
551        );
552    }
553
554    #[test]
555    fn sparse_encode_rejects_mismatched_index_value_counts() {
556        let vector = Vector::Sparse {
557            num_dimensions: 4,
558            indices: vec![0, 1, 2],
559            values: VectorValues::Int8(vec![7, 8]),
560        };
561
562        let err = encode_vector_checked(&vector).expect_err("mismatched sparse vector must fail");
563        assert!(matches!(err, ProtocolError::TtcDecode(_)), "got {err:?}");
564    }
565
566    // Regression: VECTOR float elements use Oracle's BINARY_FLOAT/DOUBLE
567    // sign-transform wire form, NOT plain IEEE-754 big-endian. A positive value
568    // gets its sign bit set; a negative value has every bit inverted. Pinning
569    // the exact element bytes guards against a regression back to plain
570    // `to_be_bytes`/`from_be_bytes`, which silently negates positive values and
571    // corrupts negatives (the w3-async P0 bug).
572    #[test]
573    fn float_elements_use_oracle_binary_transform() {
574        // f64 1.0 -> sign bit set -> 0xbff0_0000_0000_0000 (NOT 0x3ff0...).
575        let image = encode_vector(&Vector::Dense(VectorValues::Float64(vec![1.0, -2.0])));
576        let body = &image[17..]; // 1 magic + 1 ver + 2 flags + 1 fmt + 4 num + 8 norm
577        assert_eq!(&body[0..8], &[0xbf, 0xf0, 0, 0, 0, 0, 0, 0], "f64 +1.0");
578        // f64 -2.0 -> negative -> every bit inverted from 0xc000... -> 0x3fff...
579        assert_eq!(
580            &body[8..16],
581            &[0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
582            "f64 -2.0"
583        );
584
585        // f32 1.0 -> 0xbf80_0000 (NOT 0x3f80_0000).
586        let image32 = encode_vector(&Vector::Dense(VectorValues::Float32(vec![1.0, -2.0])));
587        let body32 = &image32[17..];
588        assert_eq!(&body32[0..4], &[0xbf, 0x80, 0, 0], "f32 +1.0");
589        assert_eq!(&body32[4..8], &[0x3f, 0xff, 0xff, 0xff], "f32 -2.0");
590
591        // Decoding the same bytes must recover the originals exactly.
592        assert_eq!(
593            decode_vector(&image).expect("decode f64"),
594            Vector::Dense(VectorValues::Float64(vec![1.0, -2.0]))
595        );
596        assert_eq!(
597            decode_vector(&image32).expect("decode f32"),
598            Vector::Dense(VectorValues::Float32(vec![1.0, -2.0]))
599        );
600    }
601
602    #[test]
603    fn rejects_bad_magic() {
604        let err = decode_vector(&[0x00, 0, 0, 0, 0, 0, 0, 0, 0]).expect_err("bad magic must fail");
605        assert!(matches!(err, ProtocolError::TtcDecode(_)));
606    }
607
608    #[test]
609    fn rejects_unsupported_version() {
610        let mut image = encode_vector(&Vector::Dense(VectorValues::Int8(vec![1])));
611        image[1] = 99; // bump version past WITH_SPARSE
612        let err = decode_vector(&image).expect_err("bad version must fail");
613        assert!(matches!(err, ProtocolError::TtcDecode(_)));
614    }
615
616    // Regression (w6-fuzz, vector_decoder target): a header advertising a huge
617    // FLOAT64 element count (here ~905M via num_elements 0x36000000) made the
618    // decoder `Vec::with_capacity` ~7 GB before the first truncated element
619    // read failed, tripping libFuzzer's OOM detector. The decoder must now
620    // fail closed (truncated payload) without the giant allocation.
621    #[test]
622    fn fuzz_regression_oom_oversized_element_count() {
623        let input = [219, 0, 0, 18, 3, 54, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
624        let err = decode_vector(&input).expect_err("oversized count must fail closed");
625        assert!(
626            matches!(
627                err,
628                ProtocolError::TtcDecode(_) | ProtocolError::ResourceLimit { .. }
629            ),
630            "got {err:?}"
631        );
632    }
633
634    #[test]
635    fn decode_vector_with_limits_rejects_dense_dimensions() {
636        let image = encode_vector(&Vector::Dense(VectorValues::Int8(vec![1, 2, 3, 4, 5])));
637        let limits = ProtocolLimits {
638            max_vector_dimensions: 4,
639            ..ProtocolLimits::DEFAULT
640        };
641        assert!(matches!(
642            decode_vector_with_limits(&image, limits),
643            Err(ProtocolError::ResourceLimit {
644                limit: "vector_dimensions",
645                observed: 5,
646                maximum: 4,
647            })
648        ));
649    }
650
651    // BoundedReader invariant (l2p), VECTOR sparse family: a sparse image
652    // declaring a huge num_sparse_elements (0xFFFF u16) but carrying none of the
653    // 0xFFFF * 4 = 256 KiB of index bytes must fail closed, not pre-allocate
654    // from the count. The `with_capacity_bounded(num_sparse, 4)` cap keeps the
655    // reservation at remaining()/4 and the per-index read_u32be then errors.
656    #[test]
657    fn sparse_oversized_index_count_fails_closed_not_oom() {
658        // magic, version=2 (sparse), flags=0x0020 (SPARSE), format=3 (f64),
659        // num_elements/num_dimensions = 0 (u32), then num_sparse = 0xFFFF (u16)
660        // with NO index/value bytes following.
661        let input = [
662            TNS_VECTOR_MAGIC_BYTE,
663            TNS_VECTOR_VERSION_WITH_SPARSE,
664            0x00,
665            0x20, // flags: SPARSE
666            VECTOR_FORMAT_FLOAT64,
667            0x00,
668            0x00,
669            0x00,
670            0x00, // num_elements (u32) = 0
671            0xFF,
672            0xFF, // num_sparse = 65535, but no indices follow
673        ];
674        let err = decode_vector(&input).expect_err("oversized sparse count must fail closed");
675        assert!(matches!(err, ProtocolError::TtcDecode(_)), "got {err:?}");
676    }
677
678    #[test]
679    fn binary_dense_bit_count_header() {
680        // 2 bytes => 16 dimensions encoded in the header
681        let image = encode_vector(&Vector::Dense(VectorValues::Binary(vec![0xA5, 0x3C])));
682        let num_elements = u32::from_be_bytes([image[5], image[6], image[7], image[8]]);
683        assert_eq!(num_elements, 16);
684        assert_eq!(image[1], TNS_VECTOR_VERSION_WITH_BINARY);
685    }
686
687    #[test]
688    fn binary_dense_rejects_non_byte_aligned_bit_count() {
689        let mut image = encode_vector(&Vector::Dense(VectorValues::Binary(vec![0xA5, 0x3C])));
690        image[5..9].copy_from_slice(&9_u32.to_be_bytes());
691        image.truncate(18); // 17-byte header + one payload byte
692
693        let err = decode_vector(&image).expect_err("9-bit binary vector must fail closed");
694        assert!(matches!(err, ProtocolError::TtcDecode(_)), "got {err:?}");
695    }
696
697    #[test]
698    fn decode_vector_rejects_trailing_image_bytes() {
699        let mut dense = encode_vector(&Vector::Dense(VectorValues::Float32(vec![1.0, 2.0])));
700        dense.push(0xAA);
701        let err = decode_vector(&dense).expect_err("dense image tail must fail closed");
702        assert!(matches!(err, ProtocolError::TtcDecode(_)), "got {err:?}");
703
704        let mut sparse = encode_vector(&Vector::Sparse {
705            num_dimensions: 8,
706            indices: vec![1, 4],
707            values: VectorValues::Int8(vec![7, 9]),
708        });
709        sparse.extend_from_slice(&[0xAA, 0xBB]);
710        let err = decode_vector(&sparse).expect_err("sparse image tail must fail closed");
711        assert!(matches!(err, ProtocolError::TtcDecode(_)), "got {err:?}");
712    }
713
714    // -- Golden: images captured from the real python-oracledb 4.0.1 driver
715    //    (DB-validated round-trips). See tests/golden/vectors.json. --
716
717    fn build_from_golden(entry: &Value) -> Vector {
718        let typecode = entry["typecode"].as_str().expect("typecode");
719        let f64_at = |x: &Value| x.as_f64().expect("number");
720        let i64_at = |x: &Value| x.as_i64().expect("int");
721        let u64_at = |x: &Value| x.as_u64().expect("uint");
722        let make_values = |arr: &Value| -> VectorValues {
723            let v = arr.as_array().expect("array");
724            match typecode {
725                "f" => VectorValues::Float32(v.iter().map(|x| f64_at(x) as f32).collect()),
726                "d" => VectorValues::Float64(v.iter().map(f64_at).collect()),
727                "b" => VectorValues::Int8(v.iter().map(|x| i64_at(x) as i8).collect()),
728                "B" => VectorValues::Binary(v.iter().map(|x| u64_at(x) as u8).collect()),
729                other => panic!("unknown typecode {other}"),
730            }
731        };
732        if entry["kind"] == "sparse" {
733            Vector::Sparse {
734                num_dimensions: u64_at(&entry["num_dimensions"]) as u32,
735                indices: entry["indices"]
736                    .as_array()
737                    .expect("indices array")
738                    .iter()
739                    .map(|x| u64_at(x) as u32)
740                    .collect(),
741                values: make_values(&entry["values"]),
742            }
743        } else {
744            Vector::Dense(make_values(&entry["values"]))
745        }
746    }
747
748    #[test]
749    fn matches_golden_capture() {
750        let raw = include_str!("../tests/golden/vectors.json");
751        let golden: Value = serde_json::from_str(raw).expect("parse golden json");
752        let obj = golden.as_object().expect("golden is an object");
753        assert!(!obj.is_empty(), "golden capture must not be empty");
754        for (name, entry) in obj {
755            let expected_hex = entry["image_hex"].as_str().expect("image_hex");
756            let expected = hex::decode(expected_hex).expect("decode golden hex");
757
758            // encode our model -> must equal the captured image byte-for-byte
759            let vector = build_from_golden(entry);
760            let image = encode_vector(&vector);
761            assert_eq!(
762                hex::encode(&image),
763                expected_hex,
764                "encode mismatch for golden case {name}"
765            );
766
767            // decode the captured image -> must equal our model
768            let decoded = decode_vector(&expected).expect("decode golden image");
769            assert_eq!(decoded, vector, "decode mismatch for golden case {name}");
770        }
771    }
772}