#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum IntegerInputError {
OutOfRange,
InvalidSyntax,
}
pub(crate) fn parse_int8(text: &str) -> Result<i64, IntegerInputError> {
let bytes = text.as_bytes();
let mut position = 0;
while bytes.get(position).is_some_and(|byte| is_space(*byte)) {
position += 1;
}
let negative = match bytes.get(position) {
Some(b'-') => {
position += 1;
true
}
Some(b'+') => {
position += 1;
false
}
_ => false,
};
let (radix, prefixed): (u32, bool) = match (bytes.get(position), bytes.get(position + 1)) {
(Some(b'0'), Some(b'x' | b'X')) => (16, true),
(Some(b'0'), Some(b'o' | b'O')) => (8, true),
(Some(b'0'), Some(b'b' | b'B')) => (2, true),
_ => (10, false),
};
if prefixed {
position += 2;
}
let first_digit = position;
let limit = i64::MIN.unsigned_abs() / u64::from(radix);
let mut magnitude: u64 = 0;
while let Some(&byte) = bytes.get(position) {
if let Some(digit) = char::from(byte).to_digit(radix) {
if magnitude > limit {
return Err(IntegerInputError::OutOfRange);
}
magnitude = magnitude * u64::from(radix) + u64::from(digit);
position += 1;
} else if byte == b'_' {
if !prefixed && position == first_digit {
return Err(IntegerInputError::InvalidSyntax);
}
position += 1;
if !bytes
.get(position)
.is_some_and(|next| char::from(*next).is_digit(radix))
{
return Err(IntegerInputError::InvalidSyntax);
}
} else {
break;
}
}
if position == first_digit {
return Err(IntegerInputError::InvalidSyntax);
}
while bytes.get(position).is_some_and(|byte| is_space(*byte)) {
position += 1;
}
if position != bytes.len() {
return Err(IntegerInputError::InvalidSyntax);
}
if negative {
if magnitude > i64::MIN.unsigned_abs() {
return Err(IntegerInputError::OutOfRange);
}
Ok(0_i64.wrapping_sub_unsigned(magnitude))
} else {
i64::try_from(magnitude).map_err(|_| IntegerInputError::OutOfRange)
}
}
const fn is_space(byte: u8) -> bool {
matches!(byte, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')
}
#[cfg(test)]
mod tests {
use super::{parse_int8, IntegerInputError};
#[test]
fn reads_every_form_postgresql_accepts() {
for (text, value) in [
("42", 42),
(" -42\n", -42),
("+7", 7),
("1_000_000", 1_000_000),
("0x1F", 31),
("0X_1f", 31),
("-0o17", -15),
("0b1010", 10),
("9223372036854775807", i64::MAX),
("-9223372036854775808", i64::MIN),
("-0x8000000000000000", i64::MIN),
] {
assert_eq!(parse_int8(text), Ok(value), "{text}");
}
}
#[test]
fn distinguishes_an_out_of_range_integer_from_other_text() {
for text in [
"9223372036854775808",
"-9223372036854775809",
"0x8000000000000000",
"99999999999999999999",
] {
assert_eq!(
parse_int8(text),
Err(IntegerInputError::OutOfRange),
"{text}"
);
}
for text in [
"", " ", "-", "1.5", "1e3", "_1", "1_", "1__0", "0x", "0x_", "12a", "0b2", "--1",
] {
assert_eq!(
parse_int8(text),
Err(IntegerInputError::InvalidSyntax),
"{text}"
);
}
}
}