use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;
use crate::rand::{simple_seed, xorshift64};
use crate::uf::State;
const RG_MAX_LEN: usize = 14;
const SP_BODY_LEN: u8 = 9;
const SP_FORMATTED_LEN: u8 = 12;
const SP_BASE_LEN: usize = 8;
const SP_WEIGHTS: [u32; SP_BASE_LEN] = [9, 8, 7, 6, 5, 4, 3, 2];
const SP_FORMATTED_DIGIT_POS: [usize; 9] = [0, 1, 3, 4, 5, 7, 8, 9, 11];
const STRUCTURAL_MIN_LEN: u8 = 5;
const STRUCTURAL_MAX_LEN: u8 = 14;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RgError {
InvalidLength,
InvalidCharacter,
InvalidFormat,
InvalidCheckDigit,
UnsupportedUfForGeneration,
}
impl fmt::Display for RgError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::InvalidLength => "RG length is outside the accepted range for this UF",
Self::InvalidCharacter => "RG contains invalid characters",
Self::InvalidFormat => "RG format does not match the canonical mask for this UF",
Self::InvalidCheckDigit => "RG check digit is invalid",
Self::UnsupportedUfForGeneration => {
"RG generation is not supported for this UF (no verified algorithm)"
}
})
}
}
#[derive(Clone, Copy)]
struct UfSpec {
body_len: Option<u8>,
has_check_digit: bool,
allow_x_terminator: bool,
separators: &'static [(u8, char)],
formatted_len: Option<u8>,
}
const STRUCTURAL_DEFAULT: UfSpec = UfSpec {
body_len: None,
has_check_digit: false,
allow_x_terminator: false,
separators: &[],
formatted_len: None,
};
const SP_SPEC: UfSpec = UfSpec {
body_len: Some(SP_BODY_LEN),
has_check_digit: true,
allow_x_terminator: true,
separators: &[(2, '.'), (5, '.'), (8, '-')],
formatted_len: Some(SP_FORMATTED_LEN),
};
const fn uf_spec(uf: State) -> UfSpec {
match uf {
State::SP => SP_SPEC,
_ => STRUCTURAL_DEFAULT,
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Rg {
bytes: [u8; RG_MAX_LEN],
len: u8,
uf: State,
}
impl Rg {
pub fn as_str(&self) -> &str {
unsafe { core::str::from_utf8_unchecked(&self.bytes[..self.len as usize]) }
}
pub fn uf(&self) -> State {
self.uf
}
pub fn formatted(&self) -> String {
format_with_spec(self.as_str(), uf_spec(self.uf)).unwrap_or_else(|| self.as_str().into())
}
pub fn masked(&self) -> String {
let s = self.as_str();
let spec = uf_spec(self.uf);
if spec.has_check_digit && s.len() == SP_BODY_LEN as usize {
let mut out = String::with_capacity(SP_FORMATTED_LEN as usize);
out.push_str(&s[..2]);
out.push('.');
out.push_str("***");
out.push('.');
out.push_str("***");
out.push('-');
out.push('*');
out
} else {
let mut out = String::with_capacity(s.len());
for (i, _) in s.chars().enumerate() {
if i < 2 {
out.push(s.as_bytes()[i] as char);
} else {
out.push('*');
}
}
out
}
}
pub fn body(&self) -> &str {
let spec = uf_spec(self.uf);
if spec.has_check_digit && self.len as usize == SP_BODY_LEN as usize {
unsafe { core::str::from_utf8_unchecked(&self.bytes[..SP_BASE_LEN]) }
} else {
self.as_str()
}
}
pub fn check_digit(&self) -> Option<u8> {
let spec = uf_spec(self.uf);
if !spec.has_check_digit {
return None;
}
let last = self.bytes[self.len as usize - 1];
if last == b'X' {
Some(10)
} else {
Some(last - b'0')
}
}
}
impl AsRef<str> for Rg {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for Rg {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.formatted())
}
}
impl fmt::Debug for Rg {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Rg({}, {})", self.uf.abbreviation(), self.formatted())
}
}
pub fn remove_symbols(rg: &str, uf: State) -> String {
let spec = uf_spec(uf);
let mut out = String::with_capacity(rg.len());
for c in rg.chars() {
if c.is_ascii_digit() {
out.push(c);
} else if spec.allow_x_terminator && (c == 'X' || c == 'x') {
out.push('X');
}
}
out
}
pub fn is_valid(rg: &str, uf: State) -> bool {
let spec = uf_spec(uf);
let raw = remove_symbols(rg, uf);
if !validate_body_length(&raw, spec) {
return false;
}
if !validate_charset(&raw, spec) {
return false;
}
if spec.has_check_digit {
return sp_check_digit_ok(&raw);
}
true
}
pub fn is_valid_strict(rg: &str, uf: State) -> Result<(), RgError> {
parse_strict(rg, uf).map(|_| ())
}
pub fn format_rg(rg: &str, uf: State) -> Option<String> {
let spec = uf_spec(uf);
let raw = remove_symbols(rg, uf);
if !validate_body_length(&raw, spec) {
return None;
}
Some(format_with_spec(&raw, spec).unwrap_or(raw))
}
pub fn compute_check_digit(base: &str, uf: State) -> Option<u8> {
if !matches!(uf, State::SP) {
return None;
}
let raw: Vec<u8> = base.bytes().filter(u8::is_ascii_digit).collect();
if raw.len() != SP_BASE_LEN {
return None;
}
Some(sp_check_digit(&raw))
}
pub fn parse_strict(raw: &str, uf: State) -> Result<Rg, RgError> {
let spec = uf_spec(uf);
let bytes = raw.as_bytes();
let body = if let Some(len) = spec.formatted_len.filter(|&l| bytes.len() == l as usize) {
let _ = len;
parse_formatted_sp(bytes)?
} else if let Some(body_len) = spec.body_len {
if bytes.len() != body_len as usize {
return Err(RgError::InvalidLength);
}
parse_unformatted(bytes, spec)?
} else {
if bytes.len() < STRUCTURAL_MIN_LEN as usize || bytes.len() > STRUCTURAL_MAX_LEN as usize {
return Err(RgError::InvalidLength);
}
parse_unformatted(bytes, spec)?
};
if spec.has_check_digit && !sp_check_digit_ok_bytes(&body) {
return Err(RgError::InvalidCheckDigit);
}
Ok(Rg::from_body(&body, uf))
}
pub fn generate(uf: State) -> Result<Rg, RgError> {
if !matches!(uf, State::SP) {
return Err(RgError::UnsupportedUfForGeneration);
}
let mut seed = simple_seed();
let mut digits = [0u8; SP_BASE_LEN];
for d in &mut digits {
seed = xorshift64(seed);
*d = (seed % 10) as u8;
}
let mut body = [0u8; 9];
for (i, &d) in digits.iter().enumerate() {
body[i] = d + b'0';
}
let dv = sp_check_digit(&digits);
body[8] = if dv == 10 { b'X' } else { b'0' + dv };
Ok(Rg::from_body(&body, State::SP))
}
impl Rg {
fn from_body(body: &[u8], uf: State) -> Self {
let mut bytes = [0u8; RG_MAX_LEN];
bytes[..body.len()].copy_from_slice(body);
Self {
bytes,
len: body.len() as u8,
uf,
}
}
}
fn validate_body_length(raw: &str, spec: UfSpec) -> bool {
match spec.body_len {
Some(n) => raw.len() == n as usize,
None => {
let n = raw.len();
n >= STRUCTURAL_MIN_LEN as usize && n <= STRUCTURAL_MAX_LEN as usize
}
}
}
fn validate_charset(raw: &str, spec: UfSpec) -> bool {
let bytes = raw.as_bytes();
if bytes.is_empty() {
return false;
}
if spec.allow_x_terminator {
let (last_idx, rest) = (bytes.len() - 1, &bytes[..bytes.len() - 1]);
if !rest.iter().all(u8::is_ascii_digit) {
return false;
}
let last = bytes[last_idx];
last.is_ascii_digit() || last == b'X'
} else {
bytes.iter().all(u8::is_ascii_digit)
}
}
fn parse_unformatted(bytes: &[u8], spec: UfSpec) -> Result<Vec<u8>, RgError> {
if !validate_charset(
unsafe { core::str::from_utf8_unchecked(bytes) },
spec,
) {
return Err(RgError::InvalidCharacter);
}
Ok(bytes.to_vec())
}
fn parse_formatted_sp(bytes: &[u8]) -> Result<Vec<u8>, RgError> {
if bytes[2] != b'.' || bytes[6] != b'.' || bytes[10] != b'-' {
return Err(RgError::InvalidFormat);
}
let mut out = Vec::with_capacity(SP_BODY_LEN as usize);
for (i, &idx) in SP_FORMATTED_DIGIT_POS.iter().enumerate() {
let b = bytes[idx];
let last = i == SP_FORMATTED_DIGIT_POS.len() - 1;
if b.is_ascii_digit() || (last && b == b'X') {
out.push(b);
} else if last && b == b'x' {
out.push(b'X');
} else {
return Err(RgError::InvalidCharacter);
}
}
Ok(out)
}
fn format_with_spec(body: &str, spec: UfSpec) -> Option<String> {
if spec.separators.is_empty() {
return None;
}
let body_len = spec.body_len? as usize;
if body.len() != body_len {
return None;
}
let total = body_len + spec.separators.len();
let mut out = String::with_capacity(total);
let mut sep_iter = spec.separators.iter().peekable();
for (i, ch) in body.chars().enumerate() {
while let Some(&&(pos, sep_ch)) = sep_iter.peek() {
if pos as usize == i && i != 0 {
out.push(sep_ch);
sep_iter.next();
} else {
break;
}
}
out.push(ch);
}
Some(out)
}
fn sp_check_digit(digits: &[u8]) -> u8 {
let sum: u32 = digits
.iter()
.zip(SP_WEIGHTS.iter())
.map(|(&d, &w)| u32::from(d) * w)
.sum();
(sum % 11) as u8
}
fn sp_check_digit_ok(body: &str) -> bool {
sp_check_digit_ok_bytes(body.as_bytes())
}
fn sp_check_digit_ok_bytes(bytes: &[u8]) -> bool {
if bytes.len() != SP_BODY_LEN as usize {
return false;
}
let mut digits = [0u8; SP_BASE_LEN];
for (i, &b) in bytes[..SP_BASE_LEN].iter().enumerate() {
if !b.is_ascii_digit() {
return false;
}
digits[i] = b - b'0';
}
let expected = sp_check_digit(&digits);
let last = bytes[SP_BASE_LEN];
let actual = if last == b'X' {
10
} else if last.is_ascii_digit() {
last - b'0'
} else {
return false;
};
actual == expected
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
#[test]
fn real_rg_29465327_2() {
assert!(is_valid("294653272", State::SP));
assert!(is_valid("29.465.327-2", State::SP));
let parsed = parse_strict("294653272", State::SP).unwrap();
assert_eq!(parsed.as_str(), "294653272");
assert_eq!(parsed.check_digit(), Some(2));
assert_eq!(parsed.formatted(), "29.465.327-2");
let parsed_fmt = parse_strict("29.465.327-2", State::SP).unwrap();
assert_eq!(parsed_fmt, parsed);
}
#[test]
fn real_rg_39406714_9() {
assert!(is_valid("394067149", State::SP));
assert!(is_valid("39.406.714-9", State::SP));
let parsed = parse_strict("394067149", State::SP).unwrap();
assert_eq!(parsed.as_str(), "394067149");
assert_eq!(parsed.check_digit(), Some(9));
assert_eq!(parsed.formatted(), "39.406.714-9");
let parsed_fmt = parse_strict("39.406.714-9", State::SP).unwrap();
assert_eq!(parsed_fmt, parsed);
}
#[test]
fn compute_check_digit_real_rgs() {
assert_eq!(compute_check_digit("29465327", State::SP), Some(2));
assert_eq!(compute_check_digit("39406714", State::SP), Some(9));
}
#[test]
fn format_real_rgs() {
assert_eq!(
format_rg("294653272", State::SP),
Some("29.465.327-2".into())
);
assert_eq!(
format_rg("394067149", State::SP),
Some("39.406.714-9".into())
);
}
#[test]
fn sp_check_digit_known_values() {
assert_eq!(sp_check_digit(&[1, 2, 3, 4, 5, 6, 7, 8]), 2);
assert_eq!(sp_check_digit(&[4, 4, 4, 4, 4, 4, 4, 4]), 0);
assert_eq!(sp_check_digit(&[1, 1, 1, 1, 1, 1, 1, 1]), 0);
assert_eq!(sp_check_digit(&[6, 0, 0, 0, 0, 0, 0, 0]), 10);
}
#[test]
fn is_valid_sp_accepts_valid() {
assert!(is_valid("123456782", State::SP));
assert!(is_valid("12.345.678-2", State::SP));
}
#[test]
fn is_valid_sp_rejects_wrong_dv() {
assert!(!is_valid("123456789", State::SP));
assert!(!is_valid("12.345.678-9", State::SP));
assert!(!is_valid("294653271", State::SP));
}
#[test]
fn is_valid_sp_x_terminator() {
assert!(is_valid("60000000X", State::SP));
assert!(is_valid("60.000.000-X", State::SP));
assert!(is_valid("60.000.000-x", State::SP));
}
#[test]
fn is_valid_sp_lenient_strips_garbage() {
let s = "123456782";
let garbage = alloc::format!("{}!{}@{}#{}", &s[0..2], &s[2..5], &s[5..8], &s[8..9]);
assert!(is_valid(&garbage, State::SP));
}
#[test]
fn is_valid_sp_rejects_wrong_length() {
assert!(!is_valid("", State::SP));
assert!(!is_valid("12345", State::SP));
assert!(!is_valid("1234567890", State::SP));
}
#[test]
fn structural_other_uf_accepts_any_digits_in_range() {
assert!(is_valid("12345", State::RJ));
assert!(is_valid("1234567890", State::MG));
assert!(is_valid("12345678901234", State::PR));
}
#[test]
fn structural_other_uf_rejects_too_short_or_long() {
assert!(!is_valid("1234", State::RJ));
assert!(!is_valid("123456789012345", State::RJ));
}
#[test]
fn structural_other_uf_rejects_letters() {
assert!(!is_valid("abcdef", State::RJ));
assert!(!is_valid("", State::RJ));
}
#[test]
fn strict_sp_accepts_unformatted() {
assert!(is_valid_strict("123456782", State::SP).is_ok());
assert!(is_valid_strict("294653272", State::SP).is_ok());
}
#[test]
fn strict_sp_accepts_formatted() {
assert!(is_valid_strict("12.345.678-2", State::SP).is_ok());
assert!(is_valid_strict("29.465.327-2", State::SP).is_ok());
}
#[test]
fn strict_sp_rejects_misplaced_separators() {
assert_eq!(
is_valid_strict("123.45.678-2", State::SP),
Err(RgError::InvalidFormat)
);
assert_eq!(
is_valid_strict("12.345.6782-", State::SP),
Err(RgError::InvalidFormat)
);
}
#[test]
fn strict_sp_rejects_garbage() {
assert_eq!(
is_valid_strict("12.345.678!2", State::SP),
Err(RgError::InvalidFormat)
);
}
#[test]
fn strict_sp_rejects_bad_dv() {
assert_eq!(
is_valid_strict("123456789", State::SP),
Err(RgError::InvalidCheckDigit)
);
}
#[test]
fn strict_sp_rejects_x_in_middle() {
assert_eq!(
is_valid_strict("1234X6782", State::SP),
Err(RgError::InvalidCharacter)
);
}
#[test]
fn strict_other_uf_accepts_digits_only() {
assert!(is_valid_strict("1234567", State::RJ).is_ok());
}
#[test]
fn strict_other_uf_rejects_separators() {
assert!(is_valid_strict("12.345.67", State::RJ).is_err());
}
#[test]
fn parse_sp_roundtrip() {
let parsed = parse_strict("123456782", State::SP).unwrap();
assert_eq!(parsed.as_str(), "123456782");
let parsed_fmt = parse_strict("12.345.678-2", State::SP).unwrap();
assert_eq!(parsed_fmt, parsed);
}
#[test]
fn parse_sp_x_terminator() {
let parsed = parse_strict("60.000.000-X", State::SP).unwrap();
assert_eq!(parsed.as_str(), "60000000X");
assert_eq!(parsed.check_digit(), Some(10));
}
#[test]
fn parse_other_uf_returns_digits() {
let parsed = parse_strict("1234567", State::RJ).unwrap();
assert_eq!(parsed.as_str(), "1234567");
assert_eq!(parsed.uf(), State::RJ);
assert_eq!(parsed.check_digit(), None);
}
#[test]
fn format_sp_inserts_separators() {
assert_eq!(
format_rg("123456782", State::SP),
Some("12.345.678-2".into())
);
assert_eq!(
format_rg("60000000X", State::SP),
Some("60.000.000-X".into())
);
}
#[test]
fn format_sp_passes_through_already_formatted() {
assert_eq!(
format_rg("12.345.678-2", State::SP),
Some("12.345.678-2".into())
);
}
#[test]
fn format_other_uf_returns_digits_unchanged() {
assert_eq!(format_rg("1234567", State::RJ), Some("1234567".into()));
}
#[test]
fn format_returns_none_on_bad_length() {
assert_eq!(format_rg("12", State::SP), None);
assert_eq!(format_rg("12", State::RJ), None);
}
#[test]
fn remove_symbols_sp_keeps_x() {
assert_eq!(remove_symbols("60.000.000-X", State::SP), "60000000X");
assert_eq!(remove_symbols("60.000.000-x", State::SP), "60000000X");
}
#[test]
fn remove_symbols_other_uf_drops_letters() {
assert_eq!(remove_symbols("12.345-67", State::RJ), "1234567");
assert_eq!(remove_symbols("X1234567", State::RJ), "1234567");
}
#[test]
fn compute_check_digit_sp() {
assert_eq!(compute_check_digit("12345678", State::SP), Some(2));
assert_eq!(compute_check_digit("44444444", State::SP), Some(0));
assert_eq!(compute_check_digit("60000000", State::SP), Some(10));
}
#[test]
fn compute_check_digit_rejects_other_uf() {
assert_eq!(compute_check_digit("1234567", State::RJ), None);
}
#[test]
fn compute_check_digit_rejects_bad_length() {
assert_eq!(compute_check_digit("1234567", State::SP), None);
assert_eq!(compute_check_digit("123456789", State::SP), None);
}
#[test]
fn generate_produces_valid() {
for _ in 0..100 {
let rg = generate(State::SP).unwrap();
assert!(is_valid(rg.as_str(), State::SP));
let parsed = parse_strict(rg.as_str(), State::SP).unwrap();
assert_eq!(parsed, rg);
}
}
#[test]
fn generate_format_roundtrip() {
for _ in 0..100 {
let rg = generate(State::SP).unwrap();
let formatted = rg.formatted();
let parsed = parse_strict(&formatted, State::SP).unwrap();
assert_eq!(parsed, rg);
}
}
#[test]
fn generate_ok_others_err() {
assert!(generate(State::SP).is_ok());
assert_eq!(
generate(State::RJ),
Err(RgError::UnsupportedUfForGeneration)
);
}
#[test]
fn masked_sp_real_rgs() {
let rg = parse_strict("294653272", State::SP).unwrap();
assert_eq!(rg.masked(), "29.***.***-*");
let rg = parse_strict("60000000X", State::SP).unwrap();
assert_eq!(rg.masked(), "60.***.***-*");
}
#[test]
fn masked_other_uf() {
let rg = parse_strict("1234567", State::RJ).unwrap();
assert_eq!(rg.masked(), "12*****");
}
#[test]
fn masked_generated_sp() {
let rg = generate(State::SP).unwrap();
let m = rg.masked();
assert!(m.starts_with(&rg.as_str()[..2]));
assert_eq!(m, alloc::format!("{}.***.***-*", &rg.as_str()[..2]));
}
#[test]
fn body_sp_real_rgs() {
let rg = parse_strict("294653272", State::SP).unwrap();
assert_eq!(rg.body(), "29465327");
let rg = parse_strict("60000000X", State::SP).unwrap();
assert_eq!(rg.body(), "60000000");
}
#[test]
fn body_other_uf_returns_full() {
let rg = parse_strict("1234567", State::RJ).unwrap();
assert_eq!(rg.body(), "1234567");
}
#[test]
fn rg_is_copy() {
let rg = generate(State::SP).unwrap();
let copy = rg;
assert_eq!(rg, copy);
}
#[test]
fn rg_as_ref_str() {
let rg = generate(State::SP).unwrap();
let r: &str = rg.as_ref();
assert_eq!(r, rg.as_str());
}
#[test]
fn debug_format_includes_uf() {
let rg = parse_strict("294653272", State::SP).unwrap();
let dbg = alloc::format!("{rg:?}");
assert!(dbg.starts_with("Rg(SP, "));
assert!(dbg.ends_with(')'));
}
#[test]
fn display_uses_formatted() {
let rg = parse_strict("294653272", State::SP).unwrap();
assert_eq!(rg.to_string(), "29.465.327-2");
}
#[test]
fn other_uf_display_passes_through() {
let rg = parse_strict("1234567", State::RJ).unwrap();
assert_eq!(rg.to_string(), "1234567");
}
}