Skip to main content

oracledb_protocol/thin/
codecs.rs

1#![forbid(unsafe_code)]
2
3use super::*;
4
5pub(crate) fn encode_oracle_date(
6    year: i32,
7    month: u8,
8    day: u8,
9    hour: u8,
10    minute: u8,
11    second: u8,
12) -> Result<[u8; ORA_TYPE_SIZE_DATE as usize]> {
13    if !(1..=9999).contains(&year)
14        || !(1..=12).contains(&month)
15        || !(1..=31).contains(&day)
16        || hour > 23
17        || minute > 59
18        || second > 59
19    {
20        return Err(ProtocolError::TtcDecode("invalid DATE bind"));
21    }
22    let century = year / 100 + 100;
23    let year_in_century = year % 100 + 100;
24    Ok([
25        u8::try_from(century).map_err(|_| ProtocolError::TtcDecode("invalid DATE century"))?,
26        u8::try_from(year_in_century).map_err(|_| ProtocolError::TtcDecode("invalid DATE year"))?,
27        month,
28        day,
29        hour + 1,
30        minute + 1,
31        second + 1,
32    ])
33}
34
35pub(crate) fn encode_oracle_timestamp(
36    year: i32,
37    month: u8,
38    day: u8,
39    hour: u8,
40    minute: u8,
41    second: u8,
42    nanosecond: u32,
43) -> Result<Vec<u8>> {
44    if nanosecond > 999_999_999 {
45        return Err(ProtocolError::TtcDecode("invalid TIMESTAMP fraction"));
46    }
47    let date = encode_oracle_date(year, month, day, hour, minute, second)?;
48    if nanosecond == 0 {
49        return Ok(date.to_vec());
50    }
51    let mut bytes = Vec::with_capacity(ORA_TYPE_SIZE_TIMESTAMP as usize);
52    bytes.extend_from_slice(&date);
53    bytes.extend_from_slice(&nanosecond.to_be_bytes());
54    Ok(bytes)
55}
56
57pub(crate) fn encode_oracle_timestamp_tz(
58    year: i32,
59    month: u8,
60    day: u8,
61    hour: u8,
62    minute: u8,
63    second: u8,
64    nanosecond: u32,
65) -> Result<Vec<u8>> {
66    if nanosecond > 999_999_999 {
67        return Err(ProtocolError::TtcDecode(
68            "invalid TIMESTAMP WITH TIME ZONE fraction",
69        ));
70    }
71    let mut bytes = Vec::with_capacity(ORA_TYPE_SIZE_TIMESTAMP_TZ as usize);
72    let date = encode_oracle_date(year, month, day, hour, minute, second)?;
73    bytes.extend_from_slice(&date);
74    bytes.extend_from_slice(&nanosecond.to_be_bytes());
75    bytes.push(TZ_HOUR_OFFSET);
76    bytes.push(TZ_MINUTE_OFFSET);
77    Ok(bytes)
78}
79
80pub fn decode_datetime_value(bytes: &[u8]) -> Result<QueryValue> {
81    if bytes.len() < ORA_TYPE_SIZE_DATE as usize {
82        return Err(ProtocolError::TtcDecode("DATE value too short"));
83    }
84    let mut year = (i32::from(bytes[0]) - 100) * 100 + i32::from(bytes[1]) - 100;
85    let mut month = bytes[2];
86    let mut day = bytes[3];
87    let mut hour = bytes[4].saturating_sub(1);
88    let mut minute = bytes[5].saturating_sub(1);
89    let mut second = bytes[6].saturating_sub(1);
90    let nanosecond = if bytes.len() >= ORA_TYPE_SIZE_TIMESTAMP as usize {
91        u32::from_be_bytes(
92            bytes[7..11]
93                .try_into()
94                .map_err(|_| ProtocolError::TtcDecode("invalid TIMESTAMP fraction"))?,
95        )
96    } else {
97        0
98    };
99    if bytes.len() >= ORA_TYPE_SIZE_TIMESTAMP_TZ as usize && bytes[11] != 0 && bytes[12] != 0 {
100        if bytes[11] & TNS_HAS_REGION_ID != 0 {
101            return Err(ProtocolError::UnsupportedFeature(
102                "named TIMESTAMP WITH TIME ZONE region",
103            ));
104        }
105        let offset_minutes = (i32::from(bytes[11]) - i32::from(TZ_HOUR_OFFSET)) * 60
106            + i32::from(bytes[12])
107            - i32::from(TZ_MINUTE_OFFSET);
108        (year, month, day, hour, minute, second) =
109            adjust_datetime_by_minutes(year, month, day, hour, minute, second, offset_minutes)?;
110    }
111    Ok(QueryValue::DateTime {
112        year,
113        month,
114        day,
115        hour,
116        minute,
117        second,
118        nanosecond,
119    })
120}
121
122pub(crate) fn adjust_datetime_by_minutes(
123    year: i32,
124    month: u8,
125    day: u8,
126    hour: u8,
127    minute: u8,
128    second: u8,
129    offset_minutes: i32,
130) -> Result<(i32, u8, u8, u8, u8, u8)> {
131    let days = days_from_civil(year, month, day)?;
132    let seconds_of_day = i64::from(hour) * 3_600 + i64::from(minute) * 60 + i64::from(second);
133    let total_seconds = days
134        .checked_mul(86_400)
135        .and_then(|value| value.checked_add(seconds_of_day))
136        .and_then(|value| value.checked_add(i64::from(offset_minutes) * 60))
137        .ok_or(ProtocolError::TtcDecode(
138            "TIMESTAMP WITH TIME ZONE offset overflow",
139        ))?;
140    let adjusted_days = total_seconds.div_euclid(86_400);
141    let adjusted_seconds = total_seconds.rem_euclid(86_400);
142    let (year, month, day) = civil_from_days(adjusted_days)?;
143    let hour = u8::try_from(adjusted_seconds / 3_600)
144        .map_err(|_| ProtocolError::TtcDecode("invalid adjusted TIMESTAMP hour"))?;
145    let minute = u8::try_from((adjusted_seconds % 3_600) / 60)
146        .map_err(|_| ProtocolError::TtcDecode("invalid adjusted TIMESTAMP minute"))?;
147    let second = u8::try_from(adjusted_seconds % 60)
148        .map_err(|_| ProtocolError::TtcDecode("invalid adjusted TIMESTAMP second"))?;
149    Ok((year, month, day, hour, minute, second))
150}
151
152pub(crate) fn days_from_civil(year: i32, month: u8, day: u8) -> Result<i64> {
153    if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
154        return Err(ProtocolError::TtcDecode("invalid TIMESTAMP date"));
155    }
156    let year = year - i32::from(month <= 2);
157    let era = if year >= 0 { year } else { year - 399 } / 400;
158    let year_of_era = year - era * 400;
159    let month = i32::from(month);
160    let day = i32::from(day);
161    let month_prime = month + if month > 2 { -3 } else { 9 };
162    let day_of_year = (153 * month_prime + 2) / 5 + day - 1;
163    let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
164    Ok(i64::from(era) * 146_097 + i64::from(day_of_era) - 719_468)
165}
166
167pub(crate) fn civil_from_days(days: i64) -> Result<(i32, u8, u8)> {
168    let days = days + 719_468;
169    let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
170    let day_of_era = days - era * 146_097;
171    let year_of_era =
172        (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
173    let year = year_of_era + era * 400;
174    let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
175    let month_prime = (5 * day_of_year + 2) / 153;
176    let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
177    let month = month_prime + if month_prime < 10 { 3 } else { -9 };
178    let year = year + i64::from(month <= 2);
179    Ok((
180        i32::try_from(year)
181            .map_err(|_| ProtocolError::TtcDecode("invalid adjusted TIMESTAMP year"))?,
182        u8::try_from(month)
183            .map_err(|_| ProtocolError::TtcDecode("invalid adjusted TIMESTAMP month"))?,
184        u8::try_from(day)
185            .map_err(|_| ProtocolError::TtcDecode("invalid adjusted TIMESTAMP day"))?,
186    ))
187}
188
189pub(crate) fn encode_binary_double(value: f64) -> [u8; 8] {
190    let mut bytes = value.to_bits().to_be_bytes();
191    if bytes[0] & 0x80 == 0 {
192        bytes[0] |= 0x80;
193    } else {
194        for byte in &mut bytes {
195            *byte = !*byte;
196        }
197    }
198    bytes
199}
200
201pub(crate) fn encode_binary_float(value: f32) -> [u8; 4] {
202    let mut bytes = value.to_bits().to_be_bytes();
203    if bytes[0] & 0x80 == 0 {
204        bytes[0] |= 0x80;
205    } else {
206        for byte in &mut bytes {
207            *byte = !*byte;
208        }
209    }
210    bytes
211}
212
213pub(crate) fn decode_binary_float(bytes: &[u8]) -> Result<f32> {
214    let bytes: [u8; 4] = bytes
215        .try_into()
216        .map_err(|_| ProtocolError::TtcDecode("invalid BINARY_FLOAT length"))?;
217    let mut decoded = bytes;
218    if decoded[0] & 0x80 != 0 {
219        decoded[0] &= 0x7f;
220    } else {
221        for byte in &mut decoded {
222            *byte = !*byte;
223        }
224    }
225    Ok(f32::from_bits(u32::from_be_bytes(decoded)))
226}
227
228pub(crate) fn encode_interval_ds(days: i32, seconds: i32, nanoseconds: i32) -> Result<[u8; 11]> {
229    let mut bytes = [0u8; 11];
230    let wire_days = u32::try_from(i64::from(days) + TNS_DURATION_MID)
231        .map_err(|_| ProtocolError::TtcDecode("INTERVAL DS days out of range"))?;
232    bytes[..4].copy_from_slice(&wire_days.to_be_bytes());
233    let to_offset_byte = |value: i32| -> Result<u8> {
234        u8::try_from(value + TNS_DURATION_OFFSET)
235            .map_err(|_| ProtocolError::TtcDecode("INTERVAL DS component out of range"))
236    };
237    bytes[4] = to_offset_byte(seconds / 3600)?;
238    bytes[5] = to_offset_byte((seconds % 3600) / 60)?;
239    bytes[6] = to_offset_byte(seconds % 60)?;
240    let fseconds = i64::from(nanoseconds);
241    let wire_fseconds = u32::try_from(fseconds + TNS_DURATION_MID)
242        .map_err(|_| ProtocolError::TtcDecode("INTERVAL DS fractional seconds out of range"))?;
243    bytes[7..].copy_from_slice(&wire_fseconds.to_be_bytes());
244    Ok(bytes)
245}
246
247pub(crate) fn decode_interval_ds(bytes: &[u8]) -> Result<QueryValue> {
248    if bytes.len() < 11 {
249        return Err(ProtocolError::TtcDecode("invalid INTERVAL DS length"));
250    }
251    let days_wire = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
252    let fseconds_wire = u32::from_be_bytes([bytes[7], bytes[8], bytes[9], bytes[10]]);
253    let to_component = |value: i64| -> Result<i32> {
254        i32::try_from(value).map_err(|_| ProtocolError::TtcDecode("INTERVAL DS out of range"))
255    };
256    Ok(QueryValue::IntervalDS {
257        days: to_component(i64::from(days_wire) - TNS_DURATION_MID)?,
258        hours: i32::from(bytes[4]) - TNS_DURATION_OFFSET,
259        minutes: i32::from(bytes[5]) - TNS_DURATION_OFFSET,
260        seconds: i32::from(bytes[6]) - TNS_DURATION_OFFSET,
261        fseconds: to_component(i64::from(fseconds_wire) - TNS_DURATION_MID)?,
262    })
263}
264
265/// Encodes an INTERVAL YEAR TO MONTH value (reference
266/// impl/base/encoders.pyx:151-161): big-endian years offset by
267/// TNS_DURATION_MID followed by months offset by TNS_DURATION_OFFSET.
268pub(crate) fn encode_interval_ym(years: i32, months: i32) -> Result<[u8; 5]> {
269    let mut bytes = [0u8; 5];
270    let wire_years = u32::try_from(i64::from(years) + TNS_DURATION_MID)
271        .map_err(|_| ProtocolError::TtcDecode("INTERVAL YM years out of range"))?;
272    bytes[..4].copy_from_slice(&wire_years.to_be_bytes());
273    bytes[4] = u8::try_from(months + TNS_DURATION_OFFSET)
274        .map_err(|_| ProtocolError::TtcDecode("INTERVAL YM months out of range"))?;
275    Ok(bytes)
276}
277
278/// Decodes an INTERVAL YEAR TO MONTH value (reference
279/// impl/base/decoders.pyx:147-155). Components are signed: negative
280/// intervals subtract below the offsets.
281pub(crate) fn decode_interval_ym(bytes: &[u8]) -> Result<QueryValue> {
282    if bytes.len() < 5 {
283        return Err(ProtocolError::TtcDecode("invalid INTERVAL YM length"));
284    }
285    let years_wire = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
286    let years = i32::try_from(i64::from(years_wire) - TNS_DURATION_MID)
287        .map_err(|_| ProtocolError::TtcDecode("INTERVAL YM out of range"))?;
288    Ok(QueryValue::IntervalYM {
289        years,
290        months: i32::from(bytes[4]) - TNS_DURATION_OFFSET,
291    })
292}
293
294pub(crate) fn decode_binary_double(bytes: &[u8]) -> Result<f64> {
295    let bytes: [u8; 8] = bytes
296        .try_into()
297        .map_err(|_| ProtocolError::TtcDecode("invalid BINARY_DOUBLE length"))?;
298    let mut decoded = bytes;
299    if decoded[0] & 0x80 != 0 {
300        decoded[0] &= 0x7f;
301    } else {
302        for byte in &mut decoded {
303            *byte = !*byte;
304        }
305    }
306    Ok(f64::from_bits(u64::from_be_bytes(decoded)))
307}
308
309/// Encode a canonical decimal `value` into the Oracle `NUMBER` wire form
310/// (the inverse of [`decode_number_value`]). Public so benches / parity
311/// harnesses can synthesize fetch payloads. Reference
312/// impl/base/encoders.pyx.
313pub fn encode_number_text(value: &str) -> Result<Vec<u8>> {
314    let value = value.as_bytes();
315    if value.is_empty() {
316        return Err(ProtocolError::TtcDecode("empty NUMBER bind"));
317    }
318    if value.len() > NUMBER_AS_TEXT_CHARS {
319        return Err(ProtocolError::TtcDecode("NUMBER bind text too long"));
320    }
321
322    let mut pos = 0;
323    let mut is_negative = false;
324    if matches!(value.first(), Some(&b'-')) {
325        is_negative = true;
326        pos += 1;
327    }
328
329    let mut digits = Vec::with_capacity(NUMBER_AS_TEXT_CHARS);
330    while let Some(byte) = value.get(pos).copied() {
331        if matches!(byte, b'.' | b'e' | b'E') {
332            break;
333        }
334        if !byte.is_ascii_digit() {
335            return Err(ProtocolError::TtcDecode("invalid NUMBER bind"));
336        }
337        let digit = byte - b'0';
338        pos += 1;
339        if digit == 0 && digits.is_empty() {
340            continue;
341        }
342        digits.push(digit);
343    }
344    let mut decimal_point_index = i32::try_from(digits.len()).unwrap_or(i32::MAX);
345
346    if matches!(value.get(pos), Some(&b'.')) {
347        pos += 1;
348        while let Some(byte) = value.get(pos).copied() {
349            if matches!(byte, b'e' | b'E') {
350                break;
351            }
352            if !byte.is_ascii_digit() {
353                return Err(ProtocolError::TtcDecode("invalid NUMBER bind"));
354            }
355            let digit = byte - b'0';
356            pos += 1;
357            if digit == 0 && digits.is_empty() {
358                decimal_point_index -= 1;
359                continue;
360            }
361            digits.push(digit);
362        }
363    }
364
365    if matches!(value.get(pos).copied(), Some(b'e' | b'E')) {
366        pos += 1;
367        let mut exponent_is_negative = false;
368        if let Some(byte) = value.get(pos).copied() {
369            if byte == b'-' {
370                exponent_is_negative = true;
371                pos += 1;
372            } else if byte == b'+' {
373                pos += 1;
374            }
375        }
376        let exponent_start = pos;
377        while let Some(byte) = value.get(pos).copied() {
378            if !byte.is_ascii_digit() {
379                return Err(ProtocolError::TtcDecode("invalid NUMBER exponent"));
380            }
381            pos += 1;
382        }
383        if exponent_start == pos {
384            return Err(ProtocolError::TtcDecode("empty NUMBER exponent"));
385        }
386        let exponent_text = std::str::from_utf8(&value[exponent_start..pos])
387            .map_err(|_| ProtocolError::TtcDecode("invalid NUMBER exponent"))?;
388        let mut exponent = exponent_text
389            .parse::<i32>()
390            .map_err(|_| ProtocolError::TtcDecode("invalid NUMBER exponent"))?;
391        if exponent_is_negative {
392            exponent = -exponent;
393        }
394        // `exponent` is parsed as a full i32 (the sign is stripped before
395        // parsing, so it is in [0, i32::MAX] before negation) while the
396        // reference treats it as int16_t. A crafted bind such as
397        // "1"*160 + "e+2147483647" (within NUMBER_AS_TEXT_CHARS) would overflow
398        // this add — panicking in debug builds — so add checked and reject
399        // out-of-range like the reference's range check does (encoders.pyx).
400        decimal_point_index = decimal_point_index
401            .checked_add(exponent)
402            .ok_or(ProtocolError::TtcDecode("NUMBER bind out of range"))?;
403    }
404
405    if pos < value.len() {
406        return Err(ProtocolError::TtcDecode("invalid NUMBER bind suffix"));
407    }
408
409    while digits.last().is_some_and(|digit| *digit == 0) {
410        digits.pop();
411    }
412    if digits.len() > NUMBER_MAX_DIGITS || !(-129..=126).contains(&decimal_point_index) {
413        return Err(ProtocolError::TtcDecode("NUMBER bind out of range"));
414    }
415
416    let mut prepend_zero = false;
417    if decimal_point_index % 2 != 0 {
418        prepend_zero = true;
419        if !digits.is_empty() {
420            digits.push(0);
421            decimal_point_index += 1;
422        }
423    }
424    if digits.len() % 2 == 1 {
425        digits.push(0);
426    }
427
428    if digits.is_empty() {
429        return Ok(vec![128]);
430    }
431
432    let mut encoded = Vec::with_capacity(digits.len() / 2 + 2);
433    let exponent_on_wire = decimal_point_index / 2 + 192;
434    if !(0..=255).contains(&exponent_on_wire) {
435        return Err(ProtocolError::TtcDecode(
436            "NUMBER bind exponent out of range",
437        ));
438    }
439    let exponent_byte = exponent_on_wire as u8;
440    encoded.push(if is_negative {
441        !exponent_byte
442    } else {
443        exponent_byte
444    });
445
446    let mut digit_pos = 0;
447    for pair_num in 0..(digits.len() / 2) {
448        let mut digit = if pair_num == 0 && prepend_zero {
449            let digit = digits[digit_pos];
450            digit_pos += 1;
451            digit
452        } else {
453            let digit = digits[digit_pos] * 10 + digits[digit_pos + 1];
454            digit_pos += 2;
455            digit
456        };
457        if is_negative {
458            digit = 101 - digit;
459        } else {
460            digit += 1;
461        }
462        encoded.push(digit);
463    }
464
465    if is_negative && digits.len() < NUMBER_MAX_DIGITS {
466        encoded.push(102);
467    }
468
469    Ok(encoded)
470}
471
472pub fn decode_number_value(bytes: &[u8]) -> Result<QueryValue> {
473    Ok(QueryValue::Number(super::number::OracleNumber::from_wire(
474        bytes,
475    )?))
476}
477
478/// Decode the Oracle `NUMBER` wire form into canonical decimal text, **appending
479/// to `text`** and returning whether the value is integral. `digits` is a
480/// caller-owned scratch buffer (cleared on entry) so a tight decode loop can
481/// reuse one allocation across many values — this is the allocation-free core
482/// the borrowed fetch path drives, writing straight into its per-row arena.
483/// [`decode_number_value`] is the owning convenience wrapper.
484///
485/// Implemented in terms of [`decode_number_parts`] + the shared formatter
486/// fragment below, so the borrowed-arena text and the owned inline
487/// [`super::number::OracleNumber`] are byte-identical by construction (they walk
488/// the same digits and format with the same code).
489pub fn decode_number_text_into(
490    bytes: &[u8],
491    digits: &mut Vec<u8>,
492    text: &mut String,
493) -> Result<bool> {
494    match decode_number_parts(bytes, digits, text)? {
495        // The single-byte sentinels already wrote their canonical text.
496        super::number::DecodedNumber::Text { is_integer } => Ok(is_integer),
497        super::number::DecodedNumber::Parts {
498            is_negative,
499            decimal_point_index,
500            is_integer,
501        } => {
502            format_number_digits(digits, is_negative, decimal_point_index, text);
503            Ok(is_integer)
504        }
505    }
506}
507
508/// Walk the Oracle `NUMBER` wire form into `digits` (significant decimal digits,
509/// each 0..=9) and report the parts needed to format the canonical text and to
510/// build the inline [`super::number::OracleNumber`]. The single-byte sentinels
511/// (positive zero, the `-1e126` negative sentinel) write their canonical text
512/// directly into `text` and return [`super::number::DecodedNumber::Text`].
513///
514/// This is the SINGLE digit-decoding source of truth: both the owned inline
515/// representation and the borrowed-arena text path drive it.
516pub(crate) fn decode_number_parts(
517    bytes: &[u8],
518    digits: &mut Vec<u8>,
519    text: &mut String,
520) -> Result<super::number::DecodedNumber> {
521    use super::number::DecodedNumber;
522
523    if bytes.len() > 21 {
524        return Err(ProtocolError::TtcDecode("encoded NUMBER too long"));
525    }
526    let Some(&first) = bytes.first() else {
527        return Err(ProtocolError::TtcDecode("empty NUMBER"));
528    };
529    let is_positive = first & 0x80 != 0;
530    digits.clear();
531    if bytes.len() == 1 {
532        if is_positive {
533            text.push('0');
534        } else {
535            text.push_str("-1e126");
536        }
537        return Ok(DecodedNumber::Text { is_integer: true });
538    }
539
540    let exponent_byte = if is_positive { first } else { !first };
541    let exponent = i16::from(exponent_byte) - 193;
542    let mut decimal_point_index = exponent * 2 + 2;
543    let mut end = bytes.len();
544    if !is_positive && bytes[end - 1] == 102 {
545        end -= 1;
546    }
547
548    for (index, encoded) in bytes.iter().enumerate().take(end).skip(1) {
549        let value = if is_positive {
550            encoded.saturating_sub(1)
551        } else {
552            101u8.saturating_sub(*encoded)
553        };
554
555        let first_digit = value / 10;
556        if first_digit == 0 && digits.is_empty() {
557            decimal_point_index -= 1;
558        } else if first_digit == 10 {
559            digits.push(1);
560            digits.push(0);
561            decimal_point_index += 1;
562        } else if first_digit != 0 || index > 0 {
563            digits.push(first_digit);
564        }
565
566        let second_digit = value % 10;
567        if second_digit != 0 || index < end - 1 {
568            digits.push(second_digit);
569        }
570    }
571
572    // `is_integer` is true unless the canonical text gets a decimal point: that
573    // happens when `decimal_point_index <= 0` (leading "0.") or the point falls
574    // strictly inside the significant digits (`0 < dpi < len`).
575    let len = i16::try_from(digits.len()).unwrap_or(i16::MAX);
576    let is_integer = decimal_point_index > 0 && decimal_point_index >= len;
577
578    Ok(DecodedNumber::Parts {
579        is_negative: !is_positive,
580        decimal_point_index,
581        is_integer,
582    })
583}
584
585/// Stack-buffer twin of [`decode_number_parts`]: walks the wire NUMBER digits
586/// into `digit_buf` (no heap allocation) and reports the parts needed to build
587/// the inline [`super::number::OracleNumber`]. The single-byte sentinels return
588/// their fixed canonical text. The owned per-cell NUMBER decode drives this so a
589/// NUMBER-heavy row allocates nothing per cell.
590///
591/// `digit_buf` MUST be at least [`super::number::MAX_DIGITS`] long. This shares
592/// the exact digit-walk logic with [`decode_number_parts`]; keep them aligned.
593pub(crate) fn decode_number_parts_stack(
594    bytes: &[u8],
595    digit_buf: &mut [u8],
596) -> Result<super::number::DecodedNumberStack> {
597    use super::number::DecodedNumberStack;
598
599    if bytes.len() > 21 {
600        return Err(ProtocolError::TtcDecode("encoded NUMBER too long"));
601    }
602    let Some(&first) = bytes.first() else {
603        return Err(ProtocolError::TtcDecode("empty NUMBER"));
604    };
605    let is_positive = first & 0x80 != 0;
606    if bytes.len() == 1 {
607        return Ok(DecodedNumberStack::Sentinel {
608            text: if is_positive { "0" } else { "-1e126" },
609            is_integer: true,
610        });
611    }
612
613    let exponent_byte = if is_positive { first } else { !first };
614    let exponent = i16::from(exponent_byte) - 193;
615    let mut decimal_point_index = exponent * 2 + 2;
616    let mut end = bytes.len();
617    if !is_positive && bytes[end - 1] == 102 {
618        end -= 1;
619    }
620
621    let mut len = 0usize;
622    // FUSED i128 coefficient (bead rust-oracledb-shh): folded as each significant
623    // digit is emitted, removing the second `digits_to_i128` walk over the digit
624    // buffer for the common in-range NUMBER. `Some(acc)` accumulates `acc*10 + d`
625    // over the SAME digit sequence, in the SAME order, that `digits_to_i128`
626    // walks — so the result is byte-identical. On overflow it latches to `None`
627    // and the digit buffer (still filled below) drives the unchanged spill path.
628    let mut coeff: Option<i128> = Some(0);
629    // The digit count is provably <= MAX_DIGITS for valid wire forms; guard
630    // defensively so a crafted oversize input cannot index out of bounds. Each
631    // emitted digit is also folded into the i128 accumulator.
632    let push = |buf: &mut [u8], d: u8, len: &mut usize, coeff: &mut Option<i128>| {
633        if *len < buf.len() {
634            buf[*len] = d;
635            *len += 1;
636        }
637        *coeff = coeff
638            .and_then(|acc| acc.checked_mul(10))
639            .and_then(|acc| acc.checked_add(i128::from(d)));
640    };
641
642    for (index, encoded) in bytes.iter().enumerate().take(end).skip(1) {
643        let value = if is_positive {
644            encoded.saturating_sub(1)
645        } else {
646            101u8.saturating_sub(*encoded)
647        };
648
649        let first_digit = value / 10;
650        if first_digit == 0 && len == 0 {
651            decimal_point_index -= 1;
652        } else if first_digit == 10 {
653            push(digit_buf, 1, &mut len, &mut coeff);
654            push(digit_buf, 0, &mut len, &mut coeff);
655            decimal_point_index += 1;
656        } else if first_digit != 0 || index > 0 {
657            push(digit_buf, first_digit, &mut len, &mut coeff);
658        }
659
660        let second_digit = value % 10;
661        if second_digit != 0 || index < end - 1 {
662            push(digit_buf, second_digit, &mut len, &mut coeff);
663        }
664    }
665
666    let len_i16 = i16::try_from(len).unwrap_or(i16::MAX);
667    let is_integer = decimal_point_index > 0 && decimal_point_index >= len_i16;
668
669    // Apply the sign to the fused coefficient, matching `digits_to_i128`'s
670    // `if is_negative { -acc }`. Negating a non-overflowed magnitude can itself
671    // never overflow i128 here (the magnitude already fit), so this preserves the
672    // exact spill boundary.
673    let coefficient = coeff.map(|acc| if is_positive { acc } else { -acc });
674
675    Ok(DecodedNumberStack::Parts {
676        digit_len: len,
677        is_negative: !is_positive,
678        decimal_point_index,
679        is_integer,
680        coefficient,
681    })
682}
683
684/// Append the canonical decimal text for `digits` (significant decimal digits,
685/// each 0..=9) positioned by `decimal_point_index`, with the given sign. This is
686/// the legacy `decode_number_text_into` formatting tail, factored out so the
687/// inline [`super::number::OracleNumber`] formatter is the same logic. Keep it
688/// byte-for-byte aligned with `super::number::fmt_inline_into`.
689pub(crate) fn format_number_digits(
690    digits: &[u8],
691    is_negative: bool,
692    decimal_point_index: i16,
693    text: &mut String,
694) {
695    if is_negative {
696        text.push('-');
697    }
698    if decimal_point_index <= 0 {
699        text.push_str("0.");
700        for _ in decimal_point_index..0 {
701            text.push('0');
702        }
703    }
704    for (index, digit) in digits.iter().enumerate() {
705        if index > 0
706            && matches!(
707                i16::try_from(index)
708                    .unwrap_or(i16::MAX)
709                    .cmp(&decimal_point_index),
710                std::cmp::Ordering::Equal
711            )
712        {
713            text.push('.');
714        }
715        text.push(char::from(b'0' + *digit));
716    }
717    if decimal_point_index > i16::try_from(digits.len()).unwrap_or(i16::MAX) {
718        for _ in i16::try_from(digits.len()).unwrap_or(i16::MAX)..decimal_point_index {
719            text.push('0');
720        }
721    }
722}
723
724pub(crate) fn decode_text_value(bytes: &[u8], csfrm: u8) -> Result<String> {
725    if csfrm == CS_FORM_NCHAR {
726        let units = bytes
727            .chunks_exact(2)
728            .map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]))
729            .collect::<Vec<_>>();
730        if units.len() * 2 != bytes.len() {
731            return Err(ProtocolError::TtcDecode("invalid UTF-16 text length"));
732        }
733        String::from_utf16(&units).map_err(|_| ProtocolError::TtcDecode("invalid UTF-16 text"))
734    } else {
735        String::from_utf8(bytes.to_vec())
736            .map_err(|_| ProtocolError::TtcDecode("invalid UTF-8 text"))
737    }
738}
739
740#[cfg(test)]
741mod tests {
742    use super::*;
743
744    // Regression: bead rust-oracledb-jmc. A crafted BindValue::Number whose
745    // text packs many leading digits and a huge exponent (within the
746    // NUMBER_AS_TEXT_CHARS cap) used to overflow `decimal_point_index +=
747    // exponent`, panicking in debug builds. The reference rejects such values;
748    // we must too (clean Err, never a panic).
749    #[test]
750    fn number_text_huge_exponent_rejected_not_panicked() {
751        // 160 digits + "e+2147483647" == 172 bytes == NUMBER_AS_TEXT_CHARS.
752        let crafted = format!("{}e+2147483647", "1".repeat(160));
753        assert_eq!(crafted.len(), NUMBER_AS_TEXT_CHARS);
754        assert!(encode_number_text(&crafted).is_err());
755
756        // Negative-exponent counterpart must also reject without panicking.
757        let crafted_neg = format!("0.{}e-2147483647", "0".repeat(158));
758        assert!(encode_number_text(&crafted_neg).is_err());
759    }
760
761    #[test]
762    fn number_text_ordinary_values_still_encode() {
763        for ok in [
764            "0",
765            "1",
766            "-1",
767            "3.14159",
768            "1e10",
769            "-2.5e-3",
770            "12345678901234567890",
771        ] {
772            assert!(encode_number_text(ok).is_ok(), "expected {ok} to encode");
773        }
774    }
775
776    #[test]
777    fn interval_ds_roundtrip_preserves_nanoseconds() {
778        let wire = encode_interval_ds(2, 3 * 3600 + 4 * 60 + 5, 123_456_789)
779            .expect("encode nanosecond interval");
780        assert_eq!(
781            decode_interval_ds(&wire).expect("decode nanosecond interval"),
782            QueryValue::IntervalDS {
783                days: 2,
784                hours: 3,
785                minutes: 4,
786                seconds: 5,
787                fseconds: 123_456_789,
788            }
789        );
790    }
791}