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