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}