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    encode_oracle_timestamp_tz_with_offset(year, month, day, hour, minute, second, nanosecond, 0)
67}
68
69#[allow(clippy::too_many_arguments)]
70pub(crate) fn encode_oracle_timestamp_tz_with_offset(
71    year: i32,
72    month: u8,
73    day: u8,
74    hour: u8,
75    minute: u8,
76    second: u8,
77    nanosecond: u32,
78    offset_minutes: i32,
79) -> Result<Vec<u8>> {
80    if nanosecond > 999_999_999 {
81        return Err(ProtocolError::TtcDecode(
82            "invalid TIMESTAMP WITH TIME ZONE fraction",
83        ));
84    }
85    let offset_hours = offset_minutes / 60;
86    let offset_minute_part = offset_minutes % 60;
87    let encoded_hour = offset_hours + i32::from(TZ_HOUR_OFFSET);
88    let encoded_minute = offset_minute_part + i32::from(TZ_MINUTE_OFFSET);
89    let encoded_hour = u8::try_from(encoded_hour)
90        .map_err(|_| ProtocolError::TtcDecode("invalid TIMESTAMP WITH TIME ZONE offset hour"))?;
91    let encoded_minute = u8::try_from(encoded_minute)
92        .map_err(|_| ProtocolError::TtcDecode("invalid TIMESTAMP WITH TIME ZONE offset minute"))?;
93    let mut bytes = Vec::with_capacity(ORA_TYPE_SIZE_TIMESTAMP_TZ as usize);
94    let date = encode_oracle_date(year, month, day, hour, minute, second)?;
95    bytes.extend_from_slice(&date);
96    bytes.extend_from_slice(&nanosecond.to_be_bytes());
97    bytes.push(encoded_hour);
98    bytes.push(encoded_minute);
99    Ok(bytes)
100}
101
102pub fn decode_datetime_value(bytes: &[u8]) -> Result<QueryValue> {
103    if bytes.len() < ORA_TYPE_SIZE_DATE as usize {
104        return Err(ProtocolError::TtcDecode("DATE value too short"));
105    }
106    let year = (i32::from(bytes[0]) - 100) * 100 + i32::from(bytes[1]) - 100;
107    let month = bytes[2];
108    let day = bytes[3];
109    let hour = bytes[4].saturating_sub(1);
110    let minute = bytes[5].saturating_sub(1);
111    let second = bytes[6].saturating_sub(1);
112    let nanosecond = if bytes.len() >= ORA_TYPE_SIZE_TIMESTAMP as usize {
113        u32::from_be_bytes(
114            bytes[7..11]
115                .try_into()
116                .map_err(|_| ProtocolError::TtcDecode("invalid TIMESTAMP fraction"))?,
117        )
118    } else {
119        0
120    };
121    if bytes.len() >= ORA_TYPE_SIZE_TIMESTAMP_TZ as usize && bytes[11] != 0 && bytes[12] != 0 {
122        if bytes[11] & TNS_HAS_REGION_ID != 0 {
123            return Err(ProtocolError::UnsupportedFeature(
124                "named TIMESTAMP WITH TIME ZONE region",
125            ));
126        }
127        let offset_minutes = (i32::from(bytes[11]) - i32::from(TZ_HOUR_OFFSET)) * 60
128            + i32::from(bytes[12])
129            - i32::from(TZ_MINUTE_OFFSET);
130        return Ok(QueryValue::TimestampTz {
131            year,
132            month,
133            day,
134            hour,
135            minute,
136            second,
137            nanosecond,
138            offset_minutes,
139        });
140    }
141    Ok(QueryValue::DateTime {
142        year,
143        month,
144        day,
145        hour,
146        minute,
147        second,
148        nanosecond,
149    })
150}
151
152pub(crate) fn adjust_datetime_by_minutes(
153    year: i32,
154    month: u8,
155    day: u8,
156    hour: u8,
157    minute: u8,
158    second: u8,
159    offset_minutes: i32,
160) -> Result<(i32, u8, u8, u8, u8, u8)> {
161    let days = days_from_civil(year, month, day)?;
162    let seconds_of_day = i64::from(hour) * 3_600 + i64::from(minute) * 60 + i64::from(second);
163    let total_seconds = days
164        .checked_mul(86_400)
165        .and_then(|value| value.checked_add(seconds_of_day))
166        .and_then(|value| value.checked_add(i64::from(offset_minutes) * 60))
167        .ok_or(ProtocolError::TtcDecode(
168            "TIMESTAMP WITH TIME ZONE offset overflow",
169        ))?;
170    let adjusted_days = total_seconds.div_euclid(86_400);
171    let adjusted_seconds = total_seconds.rem_euclid(86_400);
172    let (year, month, day) = civil_from_days(adjusted_days)?;
173    let hour = u8::try_from(adjusted_seconds / 3_600)
174        .map_err(|_| ProtocolError::TtcDecode("invalid adjusted TIMESTAMP hour"))?;
175    let minute = u8::try_from((adjusted_seconds % 3_600) / 60)
176        .map_err(|_| ProtocolError::TtcDecode("invalid adjusted TIMESTAMP minute"))?;
177    let second = u8::try_from(adjusted_seconds % 60)
178        .map_err(|_| ProtocolError::TtcDecode("invalid adjusted TIMESTAMP second"))?;
179    Ok((year, month, day, hour, minute, second))
180}
181
182pub(crate) fn days_from_civil(year: i32, month: u8, day: u8) -> Result<i64> {
183    if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
184        return Err(ProtocolError::TtcDecode("invalid TIMESTAMP date"));
185    }
186    let year = year - i32::from(month <= 2);
187    let era = if year >= 0 { year } else { year - 399 } / 400;
188    let year_of_era = year - era * 400;
189    let month = i32::from(month);
190    let day = i32::from(day);
191    let month_prime = month + if month > 2 { -3 } else { 9 };
192    let day_of_year = (153 * month_prime + 2) / 5 + day - 1;
193    let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
194    Ok(i64::from(era) * 146_097 + i64::from(day_of_era) - 719_468)
195}
196
197pub(crate) fn civil_from_days(days: i64) -> Result<(i32, u8, u8)> {
198    let days = days + 719_468;
199    let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
200    let day_of_era = days - era * 146_097;
201    let year_of_era =
202        (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
203    let year = year_of_era + era * 400;
204    let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
205    let month_prime = (5 * day_of_year + 2) / 153;
206    let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
207    let month = month_prime + if month_prime < 10 { 3 } else { -9 };
208    let year = year + i64::from(month <= 2);
209    Ok((
210        i32::try_from(year)
211            .map_err(|_| ProtocolError::TtcDecode("invalid adjusted TIMESTAMP year"))?,
212        u8::try_from(month)
213            .map_err(|_| ProtocolError::TtcDecode("invalid adjusted TIMESTAMP month"))?,
214        u8::try_from(day)
215            .map_err(|_| ProtocolError::TtcDecode("invalid adjusted TIMESTAMP day"))?,
216    ))
217}
218
219pub(crate) fn encode_binary_double(value: f64) -> [u8; 8] {
220    let mut bytes = value.to_bits().to_be_bytes();
221    if bytes[0] & 0x80 == 0 {
222        bytes[0] |= 0x80;
223    } else {
224        for byte in &mut bytes {
225            *byte = !*byte;
226        }
227    }
228    bytes
229}
230
231pub(crate) fn encode_binary_float(value: f32) -> [u8; 4] {
232    let mut bytes = value.to_bits().to_be_bytes();
233    if bytes[0] & 0x80 == 0 {
234        bytes[0] |= 0x80;
235    } else {
236        for byte in &mut bytes {
237            *byte = !*byte;
238        }
239    }
240    bytes
241}
242
243pub(crate) fn decode_binary_float(bytes: &[u8]) -> Result<f32> {
244    let bytes: [u8; 4] = bytes
245        .try_into()
246        .map_err(|_| ProtocolError::TtcDecode("invalid BINARY_FLOAT length"))?;
247    let mut decoded = bytes;
248    if decoded[0] & 0x80 != 0 {
249        decoded[0] &= 0x7f;
250    } else {
251        for byte in &mut decoded {
252            *byte = !*byte;
253        }
254    }
255    Ok(f32::from_bits(u32::from_be_bytes(decoded)))
256}
257
258pub(crate) fn encode_interval_ds(days: i32, seconds: i32, nanoseconds: i32) -> Result<[u8; 11]> {
259    let mut bytes = [0u8; 11];
260    let wire_days = u32::try_from(i64::from(days) + TNS_DURATION_MID)
261        .map_err(|_| ProtocolError::TtcDecode("INTERVAL DS days out of range"))?;
262    bytes[..4].copy_from_slice(&wire_days.to_be_bytes());
263    let to_offset_byte = |value: i32| -> Result<u8> {
264        u8::try_from(value + TNS_DURATION_OFFSET)
265            .map_err(|_| ProtocolError::TtcDecode("INTERVAL DS component out of range"))
266    };
267    bytes[4] = to_offset_byte(seconds / 3600)?;
268    bytes[5] = to_offset_byte((seconds % 3600) / 60)?;
269    bytes[6] = to_offset_byte(seconds % 60)?;
270    let fseconds = i64::from(nanoseconds);
271    let wire_fseconds = u32::try_from(fseconds + TNS_DURATION_MID)
272        .map_err(|_| ProtocolError::TtcDecode("INTERVAL DS fractional seconds out of range"))?;
273    bytes[7..].copy_from_slice(&wire_fseconds.to_be_bytes());
274    Ok(bytes)
275}
276
277pub(crate) fn decode_interval_ds(bytes: &[u8]) -> Result<QueryValue> {
278    if bytes.len() < 11 {
279        return Err(ProtocolError::TtcDecode("invalid INTERVAL DS length"));
280    }
281    let days_wire = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
282    let fseconds_wire = u32::from_be_bytes([bytes[7], bytes[8], bytes[9], bytes[10]]);
283    let to_component = |value: i64| -> Result<i32> {
284        i32::try_from(value).map_err(|_| ProtocolError::TtcDecode("INTERVAL DS out of range"))
285    };
286    Ok(QueryValue::IntervalDS {
287        days: to_component(i64::from(days_wire) - TNS_DURATION_MID)?,
288        hours: i32::from(bytes[4]) - TNS_DURATION_OFFSET,
289        minutes: i32::from(bytes[5]) - TNS_DURATION_OFFSET,
290        seconds: i32::from(bytes[6]) - TNS_DURATION_OFFSET,
291        fseconds: to_component(i64::from(fseconds_wire) - TNS_DURATION_MID)?,
292    })
293}
294
295/// Encodes an INTERVAL YEAR TO MONTH value (reference
296/// impl/base/encoders.pyx:151-161): big-endian years offset by
297/// TNS_DURATION_MID followed by months offset by TNS_DURATION_OFFSET.
298pub(crate) fn encode_interval_ym(years: i32, months: i32) -> Result<[u8; 5]> {
299    let mut bytes = [0u8; 5];
300    let wire_years = u32::try_from(i64::from(years) + TNS_DURATION_MID)
301        .map_err(|_| ProtocolError::TtcDecode("INTERVAL YM years out of range"))?;
302    bytes[..4].copy_from_slice(&wire_years.to_be_bytes());
303    bytes[4] = u8::try_from(months + TNS_DURATION_OFFSET)
304        .map_err(|_| ProtocolError::TtcDecode("INTERVAL YM months out of range"))?;
305    Ok(bytes)
306}
307
308/// Decodes an INTERVAL YEAR TO MONTH value (reference
309/// impl/base/decoders.pyx:147-155). Components are signed: negative
310/// intervals subtract below the offsets.
311pub(crate) fn decode_interval_ym(bytes: &[u8]) -> Result<QueryValue> {
312    if bytes.len() < 5 {
313        return Err(ProtocolError::TtcDecode("invalid INTERVAL YM length"));
314    }
315    let years_wire = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
316    let years = i32::try_from(i64::from(years_wire) - TNS_DURATION_MID)
317        .map_err(|_| ProtocolError::TtcDecode("INTERVAL YM out of range"))?;
318    Ok(QueryValue::IntervalYM {
319        years,
320        months: i32::from(bytes[4]) - TNS_DURATION_OFFSET,
321    })
322}
323
324pub(crate) fn decode_binary_double(bytes: &[u8]) -> Result<f64> {
325    let bytes: [u8; 8] = bytes
326        .try_into()
327        .map_err(|_| ProtocolError::TtcDecode("invalid BINARY_DOUBLE length"))?;
328    let mut decoded = bytes;
329    if decoded[0] & 0x80 != 0 {
330        decoded[0] &= 0x7f;
331    } else {
332        for byte in &mut decoded {
333            *byte = !*byte;
334        }
335    }
336    Ok(f64::from_bits(u64::from_be_bytes(decoded)))
337}
338
339/// Encode a canonical decimal `value` into the Oracle `NUMBER` wire form
340/// (the inverse of [`decode_number_value`]). Public so benches / parity
341/// harnesses can synthesize fetch payloads. Reference
342/// impl/base/encoders.pyx.
343pub fn encode_number_text(value: &str) -> Result<Vec<u8>> {
344    let value = value.as_bytes();
345    if value.is_empty() {
346        return Err(ProtocolError::TtcDecode("empty NUMBER bind"));
347    }
348    if value.len() > NUMBER_AS_TEXT_CHARS {
349        return Err(ProtocolError::TtcDecode("NUMBER bind text too long"));
350    }
351
352    let mut pos = 0;
353    let mut is_negative = false;
354    if matches!(value.first(), Some(&b'-')) {
355        is_negative = true;
356        pos += 1;
357    }
358
359    let mut digits = Vec::with_capacity(NUMBER_AS_TEXT_CHARS);
360    while let Some(byte) = value.get(pos).copied() {
361        if matches!(byte, b'.' | b'e' | b'E') {
362            break;
363        }
364        if !byte.is_ascii_digit() {
365            return Err(ProtocolError::TtcDecode("invalid NUMBER bind"));
366        }
367        let digit = byte - b'0';
368        pos += 1;
369        if digit == 0 && digits.is_empty() {
370            continue;
371        }
372        digits.push(digit);
373    }
374    let mut decimal_point_index = i32::try_from(digits.len()).unwrap_or(i32::MAX);
375
376    if matches!(value.get(pos), Some(&b'.')) {
377        pos += 1;
378        while let Some(byte) = value.get(pos).copied() {
379            if matches!(byte, b'e' | b'E') {
380                break;
381            }
382            if !byte.is_ascii_digit() {
383                return Err(ProtocolError::TtcDecode("invalid NUMBER bind"));
384            }
385            let digit = byte - b'0';
386            pos += 1;
387            if digit == 0 && digits.is_empty() {
388                decimal_point_index -= 1;
389                continue;
390            }
391            digits.push(digit);
392        }
393    }
394
395    if matches!(value.get(pos).copied(), Some(b'e' | b'E')) {
396        pos += 1;
397        let mut exponent_is_negative = false;
398        if let Some(byte) = value.get(pos).copied() {
399            if byte == b'-' {
400                exponent_is_negative = true;
401                pos += 1;
402            } else if byte == b'+' {
403                pos += 1;
404            }
405        }
406        let exponent_start = pos;
407        while let Some(byte) = value.get(pos).copied() {
408            if !byte.is_ascii_digit() {
409                return Err(ProtocolError::TtcDecode("invalid NUMBER exponent"));
410            }
411            pos += 1;
412        }
413        if exponent_start == pos {
414            return Err(ProtocolError::TtcDecode("empty NUMBER exponent"));
415        }
416        let exponent_text = std::str::from_utf8(&value[exponent_start..pos])
417            .map_err(|_| ProtocolError::TtcDecode("invalid NUMBER exponent"))?;
418        let mut exponent = exponent_text
419            .parse::<i32>()
420            .map_err(|_| ProtocolError::TtcDecode("invalid NUMBER exponent"))?;
421        if exponent_is_negative {
422            exponent = -exponent;
423        }
424        // `exponent` is parsed as a full i32 (the sign is stripped before
425        // parsing, so it is in [0, i32::MAX] before negation) while the
426        // reference treats it as int16_t. A crafted bind such as
427        // "1"*160 + "e+2147483647" (within NUMBER_AS_TEXT_CHARS) would overflow
428        // this add — panicking in debug builds — so add checked and reject
429        // out-of-range like the reference's range check does (encoders.pyx).
430        decimal_point_index = decimal_point_index
431            .checked_add(exponent)
432            .ok_or(ProtocolError::TtcDecode("NUMBER bind out of range"))?;
433    }
434
435    if pos < value.len() {
436        return Err(ProtocolError::TtcDecode("invalid NUMBER bind suffix"));
437    }
438
439    while digits.last().is_some_and(|digit| *digit == 0) {
440        digits.pop();
441    }
442    if digits.len() > NUMBER_MAX_DIGITS || !(-129..=126).contains(&decimal_point_index) {
443        return Err(ProtocolError::TtcDecode("NUMBER bind out of range"));
444    }
445
446    let mut prepend_zero = false;
447    if decimal_point_index % 2 != 0 {
448        prepend_zero = true;
449        if !digits.is_empty() {
450            digits.push(0);
451            decimal_point_index += 1;
452        }
453    }
454    if digits.len() % 2 == 1 {
455        digits.push(0);
456    }
457
458    if digits.is_empty() {
459        return Ok(vec![128]);
460    }
461
462    let mut encoded = Vec::with_capacity(digits.len() / 2 + 2);
463    let exponent_on_wire = decimal_point_index / 2 + 192;
464    if !(0..=255).contains(&exponent_on_wire) {
465        return Err(ProtocolError::TtcDecode(
466            "NUMBER bind exponent out of range",
467        ));
468    }
469    let exponent_byte = exponent_on_wire as u8;
470    encoded.push(if is_negative {
471        !exponent_byte
472    } else {
473        exponent_byte
474    });
475
476    let mut digit_pos = 0;
477    for pair_num in 0..(digits.len() / 2) {
478        let mut digit = if pair_num == 0 && prepend_zero {
479            let digit = digits[digit_pos];
480            digit_pos += 1;
481            digit
482        } else {
483            let digit = digits[digit_pos] * 10 + digits[digit_pos + 1];
484            digit_pos += 2;
485            digit
486        };
487        if is_negative {
488            digit = 101 - digit;
489        } else {
490            digit += 1;
491        }
492        encoded.push(digit);
493    }
494
495    if is_negative && digits.len() < NUMBER_MAX_DIGITS {
496        encoded.push(102);
497    }
498
499    Ok(encoded)
500}
501
502pub fn decode_number_value(bytes: &[u8]) -> Result<QueryValue> {
503    Ok(QueryValue::Number(super::number::OracleNumber::from_wire(
504        bytes,
505    )?))
506}
507
508/// Decode the Oracle `NUMBER` wire form into canonical decimal text, **appending
509/// to `text`** and returning whether the value is integral. `digits` is a
510/// caller-owned scratch buffer (cleared on entry) so a tight decode loop can
511/// reuse one allocation across many values — this is the allocation-free core
512/// the borrowed fetch path drives, writing straight into its per-row arena.
513/// [`decode_number_value`] is the owning convenience wrapper.
514///
515/// Implemented in terms of `decode_number_parts` plus the shared formatter
516/// fragment below, so the borrowed-arena text and the owned inline
517/// [`super::number::OracleNumber`] are byte-identical by construction (they walk
518/// the same digits and format with the same code).
519pub fn decode_number_text_into(
520    bytes: &[u8],
521    digits: &mut Vec<u8>,
522    text: &mut String,
523) -> Result<bool> {
524    match decode_number_parts(bytes, digits, text)? {
525        // The single-byte sentinels already wrote their canonical text.
526        super::number::DecodedNumber::Text { is_integer } => Ok(is_integer),
527        super::number::DecodedNumber::Parts {
528            is_negative,
529            decimal_point_index,
530            is_integer,
531        } => {
532            format_number_digits(digits, is_negative, decimal_point_index, text);
533            Ok(is_integer)
534        }
535    }
536}
537
538/// Walk the Oracle `NUMBER` wire form into `digits` (significant decimal digits,
539/// each 0..=9) and report the parts needed to format the canonical text and to
540/// build the inline [`super::number::OracleNumber`]. The single-byte sentinels
541/// (positive zero, the `-1e126` negative sentinel) write their canonical text
542/// directly into `text` and return [`super::number::DecodedNumber::Text`].
543///
544/// This is the SINGLE digit-decoding source of truth: both the owned inline
545/// representation and the borrowed-arena text path drive it.
546pub(crate) fn decode_number_parts(
547    bytes: &[u8],
548    digits: &mut Vec<u8>,
549    text: &mut String,
550) -> Result<super::number::DecodedNumber> {
551    use super::number::DecodedNumber;
552
553    if bytes.len() > 21 {
554        return Err(ProtocolError::TtcDecode("encoded NUMBER too long"));
555    }
556    let Some(&first) = bytes.first() else {
557        return Err(ProtocolError::TtcDecode("empty NUMBER"));
558    };
559    let is_positive = first & 0x80 != 0;
560    digits.clear();
561    if bytes.len() == 1 {
562        if is_positive {
563            text.push('0');
564        } else {
565            text.push_str("-1e126");
566        }
567        return Ok(DecodedNumber::Text { is_integer: true });
568    }
569
570    let exponent_byte = if is_positive { first } else { !first };
571    let exponent = i16::from(exponent_byte) - 193;
572    let mut decimal_point_index = exponent * 2 + 2;
573    let mut end = bytes.len();
574    if !is_positive && bytes[end - 1] == 102 {
575        end -= 1;
576    }
577
578    for (index, encoded) in bytes.iter().enumerate().take(end).skip(1) {
579        let value = if is_positive {
580            encoded.saturating_sub(1)
581        } else {
582            101u8.saturating_sub(*encoded)
583        };
584
585        let first_digit = value / 10;
586        if first_digit == 0 && digits.is_empty() {
587            decimal_point_index -= 1;
588        } else if first_digit == 10 {
589            digits.push(1);
590            digits.push(0);
591            decimal_point_index += 1;
592        } else if first_digit != 0 || index > 0 {
593            digits.push(first_digit);
594        }
595
596        let second_digit = value % 10;
597        if second_digit != 0 || index < end - 1 {
598            digits.push(second_digit);
599        }
600    }
601
602    // `is_integer` is true unless the canonical text gets a decimal point: that
603    // happens when `decimal_point_index <= 0` (leading "0.") or the point falls
604    // strictly inside the significant digits (`0 < dpi < len`).
605    let len = i16::try_from(digits.len()).unwrap_or(i16::MAX);
606    let is_integer = decimal_point_index > 0 && decimal_point_index >= len;
607
608    Ok(DecodedNumber::Parts {
609        is_negative: !is_positive,
610        decimal_point_index,
611        is_integer,
612    })
613}
614
615/// Stack-buffer twin of [`decode_number_parts`]: walks the wire NUMBER digits
616/// into `digit_buf` (no heap allocation) and reports the parts needed to build
617/// the inline [`super::number::OracleNumber`]. The single-byte sentinels return
618/// their fixed canonical text. The owned per-cell NUMBER decode drives this so a
619/// NUMBER-heavy row allocates nothing per cell.
620///
621/// `digit_buf` MUST be at least [`super::number::MAX_DIGITS`] long. This shares
622/// the exact digit-walk logic with [`decode_number_parts`]; keep them aligned.
623pub(crate) fn decode_number_parts_stack(
624    bytes: &[u8],
625    digit_buf: &mut [u8],
626) -> Result<super::number::DecodedNumberStack> {
627    use super::number::DecodedNumberStack;
628
629    if bytes.len() > 21 {
630        return Err(ProtocolError::TtcDecode("encoded NUMBER too long"));
631    }
632    let Some(&first) = bytes.first() else {
633        return Err(ProtocolError::TtcDecode("empty NUMBER"));
634    };
635    let is_positive = first & 0x80 != 0;
636    if bytes.len() == 1 {
637        return Ok(DecodedNumberStack::Sentinel {
638            text: if is_positive { "0" } else { "-1e126" },
639            is_integer: true,
640        });
641    }
642
643    let exponent_byte = if is_positive { first } else { !first };
644    let exponent = i16::from(exponent_byte) - 193;
645    let mut decimal_point_index = exponent * 2 + 2;
646    let mut end = bytes.len();
647    if !is_positive && bytes[end - 1] == 102 {
648        end -= 1;
649    }
650
651    let mut len = 0usize;
652    // FUSED i128 coefficient (bead rust-oracledb-shh): folded as each significant
653    // digit is emitted, removing the second `digits_to_i128` walk over the digit
654    // buffer for the common in-range NUMBER. `Some(acc)` accumulates `acc*10 + d`
655    // over the SAME digit sequence, in the SAME order, that `digits_to_i128`
656    // walks — so the result is byte-identical. On overflow it latches to `None`
657    // and the digit buffer (still filled below) drives the unchanged spill path.
658    let mut coeff: Option<i128> = Some(0);
659    // The digit count is provably <= MAX_DIGITS for valid wire forms; guard
660    // defensively so a crafted oversize input cannot index out of bounds. Each
661    // emitted digit is also folded into the i128 accumulator.
662    let push = |buf: &mut [u8], d: u8, len: &mut usize, coeff: &mut Option<i128>| {
663        if *len < buf.len() {
664            buf[*len] = d;
665            *len += 1;
666        }
667        *coeff = coeff
668            .and_then(|acc| acc.checked_mul(10))
669            .and_then(|acc| acc.checked_add(i128::from(d)));
670    };
671
672    for (index, encoded) in bytes.iter().enumerate().take(end).skip(1) {
673        let value = if is_positive {
674            encoded.saturating_sub(1)
675        } else {
676            101u8.saturating_sub(*encoded)
677        };
678
679        let first_digit = value / 10;
680        if first_digit == 0 && len == 0 {
681            decimal_point_index -= 1;
682        } else if first_digit == 10 {
683            push(digit_buf, 1, &mut len, &mut coeff);
684            push(digit_buf, 0, &mut len, &mut coeff);
685            decimal_point_index += 1;
686        } else if first_digit != 0 || index > 0 {
687            push(digit_buf, first_digit, &mut len, &mut coeff);
688        }
689
690        let second_digit = value % 10;
691        if second_digit != 0 || index < end - 1 {
692            push(digit_buf, second_digit, &mut len, &mut coeff);
693        }
694    }
695
696    let len_i16 = i16::try_from(len).unwrap_or(i16::MAX);
697    let is_integer = decimal_point_index > 0 && decimal_point_index >= len_i16;
698
699    // Apply the sign to the fused coefficient, matching `digits_to_i128`'s
700    // `if is_negative { -acc }`. Negating a non-overflowed magnitude can itself
701    // never overflow i128 here (the magnitude already fit), so this preserves the
702    // exact spill boundary.
703    let coefficient = coeff.map(|acc| if is_positive { acc } else { -acc });
704
705    Ok(DecodedNumberStack::Parts {
706        digit_len: len,
707        is_negative: !is_positive,
708        decimal_point_index,
709        is_integer,
710        coefficient,
711    })
712}
713
714/// Append the canonical decimal text for `digits` (significant decimal digits,
715/// each 0..=9) positioned by `decimal_point_index`, with the given sign. This is
716/// the legacy `decode_number_text_into` formatting tail, factored out so the
717/// inline [`super::number::OracleNumber`] formatter is the same logic. Keep it
718/// byte-for-byte aligned with `super::number::fmt_inline_into`.
719pub(crate) fn format_number_digits(
720    digits: &[u8],
721    is_negative: bool,
722    decimal_point_index: i16,
723    text: &mut String,
724) {
725    if is_negative {
726        text.push('-');
727    }
728    if decimal_point_index <= 0 {
729        text.push_str("0.");
730        for _ in decimal_point_index..0 {
731            text.push('0');
732        }
733    }
734    for (index, digit) in digits.iter().enumerate() {
735        if index > 0
736            && matches!(
737                i16::try_from(index)
738                    .unwrap_or(i16::MAX)
739                    .cmp(&decimal_point_index),
740                std::cmp::Ordering::Equal
741            )
742        {
743            text.push('.');
744        }
745        text.push(char::from(b'0' + *digit));
746    }
747    if decimal_point_index > i16::try_from(digits.len()).unwrap_or(i16::MAX) {
748        for _ in i16::try_from(digits.len()).unwrap_or(i16::MAX)..decimal_point_index {
749            text.push('0');
750        }
751    }
752}
753
754pub(crate) fn decode_text_value(bytes: &[u8], csfrm: u8) -> Result<String> {
755    if csfrm == CS_FORM_NCHAR {
756        let units = bytes
757            .chunks_exact(2)
758            .map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]))
759            .collect::<Vec<_>>();
760        if units.len() * 2 != bytes.len() {
761            return Err(ProtocolError::TtcDecode("invalid UTF-16 text length"));
762        }
763        String::from_utf16(&units).map_err(|_| ProtocolError::TtcDecode("invalid UTF-16 text"))
764    } else {
765        String::from_utf8(bytes.to_vec())
766            .map_err(|_| ProtocolError::TtcDecode("invalid UTF-8 text"))
767    }
768}
769
770#[cfg(test)]
771mod tests {
772    use super::*;
773
774    // Regression: bead rust-oracledb-jmc. A crafted BindValue::Number whose
775    // text packs many leading digits and a huge exponent (within the
776    // NUMBER_AS_TEXT_CHARS cap) used to overflow `decimal_point_index +=
777    // exponent`, panicking in debug builds. The reference rejects such values;
778    // we must too (clean Err, never a panic).
779    #[test]
780    fn number_text_huge_exponent_rejected_not_panicked() {
781        // 160 digits + "e+2147483647" == 172 bytes == NUMBER_AS_TEXT_CHARS.
782        let crafted = format!("{}e+2147483647", "1".repeat(160));
783        assert_eq!(crafted.len(), NUMBER_AS_TEXT_CHARS);
784        assert!(encode_number_text(&crafted).is_err());
785
786        // Negative-exponent counterpart must also reject without panicking.
787        let crafted_neg = format!("0.{}e-2147483647", "0".repeat(158));
788        assert!(encode_number_text(&crafted_neg).is_err());
789    }
790
791    #[test]
792    fn number_text_ordinary_values_still_encode() {
793        for ok in [
794            "0",
795            "1",
796            "-1",
797            "3.14159",
798            "1e10",
799            "-2.5e-3",
800            "12345678901234567890",
801        ] {
802            assert!(encode_number_text(ok).is_ok(), "expected {ok} to encode");
803        }
804    }
805
806    #[test]
807    fn interval_ds_roundtrip_preserves_nanoseconds() {
808        let wire = encode_interval_ds(2, 3 * 3600 + 4 * 60 + 5, 123_456_789)
809            .expect("encode nanosecond interval");
810        assert_eq!(
811            decode_interval_ds(&wire).expect("decode nanosecond interval"),
812            QueryValue::IntervalDS {
813                days: 2,
814                hours: 3,
815                minutes: 4,
816                seconds: 5,
817                fseconds: 123_456_789,
818            }
819        );
820    }
821
822    #[test]
823    fn interval_ds_rejects_truncated_wire_value() {
824        let wire = encode_interval_ds(-2, -(3 * 3600 + 4 * 60 + 5), -123_456_789)
825            .expect("encode negative interval");
826        assert!(decode_interval_ds(&wire[..10]).is_err());
827    }
828
829    #[test]
830    fn number_decode_heap_and_stack_paths_match_edge_corpus() {
831        for (text, is_integer) in [
832            ("0", true),
833            ("0.01", false),
834            ("0.1", false),
835            ("0.99", false),
836            ("9.9", false),
837            ("10", true),
838            ("99", true),
839            ("100", true),
840            ("101", true),
841            ("-0.01", false),
842            ("-0.1", false),
843            ("-0.99", false),
844            ("-9.9", false),
845            ("-10", true),
846            ("-99", true),
847            ("-100", true),
848            ("-101", true),
849            ("12345678901234567890123456789012345678", true),
850            ("-12345678901234567890123456789012345678", true),
851        ] {
852            let wire = encode_number_text(text).expect("encode NUMBER edge");
853            let mut digits = Vec::new();
854            let mut decoded = String::new();
855            let decoded_is_integer =
856                decode_number_text_into(&wire, &mut digits, &mut decoded).expect("decode text");
857            let owned = decode_number_value(&wire).expect("decode owned NUMBER");
858
859            assert_eq!(decoded, text, "canonical NUMBER text for wire {wire:02x?}");
860            assert_eq!(
861                decoded_is_integer, is_integer,
862                "integer flag for NUMBER wire {wire:02x?}"
863            );
864            assert_eq!(
865                owned.as_number_text().as_deref(),
866                Some(decoded.as_str()),
867                "owned stack decode must match heap text decode for {text}"
868            );
869        }
870    }
871
872    #[test]
873    fn number_part_decoders_emit_exact_digits_decimal_and_coefficient() {
874        use super::super::number::{DecodedNumber, DecodedNumberStack, MAX_DIGITS};
875
876        struct Case<'a> {
877            wire: &'a [u8],
878            digits: &'a [u8],
879            is_negative: bool,
880            decimal_point_index: i16,
881            is_integer: bool,
882            coefficient: Option<i128>,
883            text: &'a str,
884        }
885
886        let cases = [
887            // Forces the `first_digit == 10` carry branch. The wire is unusual
888            // but valid for the decoder contract: it must produce canonical
889            // "100", not "10", "1.0", or a zero-length stack result.
890            Case {
891                wire: &[193, 101],
892                digits: &[1, 0],
893                is_negative: false,
894                decimal_point_index: 3,
895                is_integer: true,
896                coefficient: Some(10),
897                text: "100",
898            },
899            // Leading zero base-100 group: decimal point moves left before any
900            // significant digit is emitted, but the intermediate zero digit is
901            // still retained because another group follows.
902            Case {
903                wire: &[193, 1, 2],
904                digits: &[0, 0, 1],
905                is_negative: false,
906                decimal_point_index: 1,
907                is_integer: false,
908                coefficient: Some(1),
909                text: "0.01",
910            },
911            // Negative terminator byte must be excluded from the digit walk.
912            Case {
913                wire: &[62, 100, 102],
914                digits: &[1],
915                is_negative: true,
916                decimal_point_index: 1,
917                is_integer: true,
918                coefficient: Some(-1),
919                text: "-1",
920            },
921            // Long digit sequence: the final zero of the last base-100 group is
922            // suppressed, and the fused coefficient must match the emitted
923            // digit stream exactly.
924            Case {
925                wire: &[
926                    211, 13, 35, 57, 79, 91, 13, 35, 57, 79, 91, 13, 35, 57, 79, 91, 13, 35, 57,
927                    79, 91,
928                ],
929                digits: &[
930                    1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6,
931                    7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
932                ],
933                is_negative: false,
934                decimal_point_index: 38,
935                is_integer: false,
936                coefficient: Some(123456789012345678901234567890123456789),
937                text: "12345678901234567890123456789012345678.9",
938            },
939        ];
940
941        for case in cases {
942            let mut heap_digits = Vec::new();
943            let mut text = String::new();
944            let decoded = decode_number_parts(case.wire, &mut heap_digits, &mut text)
945                .expect("heap NUMBER part decode");
946            assert!(
947                matches!(decoded, DecodedNumber::Parts { .. }),
948                "expected heap NUMBER parts for {:02x?}",
949                case.wire
950            );
951            if let DecodedNumber::Parts {
952                is_negative,
953                decimal_point_index,
954                is_integer,
955            } = decoded
956            {
957                assert_eq!(
958                    heap_digits, case.digits,
959                    "heap digits for {:02x?}",
960                    case.wire
961                );
962                assert_eq!(
963                    is_negative, case.is_negative,
964                    "heap sign for {:02x?}",
965                    case.wire
966                );
967                assert_eq!(
968                    decimal_point_index, case.decimal_point_index,
969                    "heap decimal point for {:02x?}",
970                    case.wire
971                );
972                assert_eq!(
973                    is_integer, case.is_integer,
974                    "heap integer flag for {:02x?}",
975                    case.wire
976                );
977                format_number_digits(&heap_digits, is_negative, decimal_point_index, &mut text);
978                assert_eq!(text, case.text, "heap text for {:02x?}", case.wire);
979            }
980
981            let mut stack_digits = [0u8; MAX_DIGITS];
982            let decoded = decode_number_parts_stack(case.wire, &mut stack_digits)
983                .expect("stack NUMBER part decode");
984            assert!(
985                matches!(decoded, DecodedNumberStack::Parts { .. }),
986                "expected stack NUMBER parts for {:02x?}",
987                case.wire
988            );
989            if let DecodedNumberStack::Parts {
990                digit_len,
991                is_negative,
992                decimal_point_index,
993                is_integer,
994                coefficient,
995            } = decoded
996            {
997                assert_eq!(
998                    &stack_digits[..digit_len],
999                    case.digits,
1000                    "stack digits for {:02x?}",
1001                    case.wire
1002                );
1003                assert_eq!(
1004                    is_negative, case.is_negative,
1005                    "stack sign for {:02x?}",
1006                    case.wire
1007                );
1008                assert_eq!(
1009                    decimal_point_index, case.decimal_point_index,
1010                    "stack decimal point for {:02x?}",
1011                    case.wire
1012                );
1013                assert_eq!(
1014                    is_integer, case.is_integer,
1015                    "stack integer flag for {:02x?}",
1016                    case.wire
1017                );
1018                assert_eq!(
1019                    coefficient, case.coefficient,
1020                    "stack coefficient for {:02x?}",
1021                    case.wire
1022                );
1023                let mut stack_text = String::new();
1024                format_number_digits(
1025                    &stack_digits[..digit_len],
1026                    is_negative,
1027                    decimal_point_index,
1028                    &mut stack_text,
1029                );
1030                assert_eq!(stack_text, case.text, "stack text for {:02x?}", case.wire);
1031            }
1032        }
1033    }
1034}