1use alloc::string::String;
6use alloc::vec::Vec;
7use core::fmt;
8
9use crate::rand::{simple_seed, xorshift64};
10use crate::util::{self, impl_document_traits};
11
12const CPF_LEN: usize = 11;
13const WEIGHTS_D1: [u32; 9] = [10, 9, 8, 7, 6, 5, 4, 3, 2];
14const WEIGHTS_D2: [u32; 10] = [11, 10, 9, 8, 7, 6, 5, 4, 3, 2];
15const FORMATTED_DIGIT_POS: [usize; 11] = [0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13];
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19#[repr(u8)]
20pub enum FiscalRegion {
21 Rs = 0,
22 DfGoMsMtTo = 1,
23 AcAmApPaRoRr = 2,
24 CeMaPi = 3,
25 AlPbPeRn = 4,
26 BaSe = 5,
27 Mg = 6,
28 EsRj = 7,
29 Sp = 8,
30 PrSc = 9,
31}
32
33impl FiscalRegion {
34 fn from_digit(d: u8) -> Self {
35 assert!(d <= 9, "digit must be 0..=9");
36 unsafe { core::mem::transmute(d) }
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum CpfError {
43 InvalidLength,
44 InvalidCharacter,
45 InvalidFormat,
46 AllDigitsEqual,
47 InvalidCheckDigits,
48}
49
50impl fmt::Display for CpfError {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 f.write_str(match self {
53 Self::InvalidLength => "CPF must contain exactly 11 digits",
54 Self::InvalidCharacter => "CPF contains invalid characters",
55 Self::InvalidFormat => "CPF format must be ###.###.###-## or 11 digits",
56 Self::AllDigitsEqual => "CPF with all equal digits is invalid",
57 Self::InvalidCheckDigits => "CPF check digits are invalid",
58 })
59 }
60}
61
62#[derive(Clone, Copy, PartialEq, Eq, Hash)]
75pub struct Cpf {
76 bytes: [u8; CPF_LEN],
77}
78
79impl Cpf {
80 pub fn as_str(&self) -> &str {
82 unsafe { core::str::from_utf8_unchecked(&self.bytes) }
84 }
85
86 pub fn digits(&self) -> [u8; CPF_LEN] {
88 self.bytes.map(|b| b - b'0')
89 }
90
91 pub fn fiscal_region(&self) -> FiscalRegion {
93 FiscalRegion::from_digit(self.bytes[8] - b'0')
94 }
95
96 pub fn masked(&self) -> String {
98 let s = self.as_str();
99 alloc::format!("{}.***.***-{}", &s[0..3], &s[9..11])
100 }
101
102 pub fn check_digits(&self) -> (u8, u8) {
104 (self.bytes[9] - b'0', self.bytes[10] - b'0')
105 }
106
107 fn from_numeric(digits: [u8; CPF_LEN]) -> Self {
108 Self {
109 bytes: digits.map(|d| d + b'0'),
110 }
111 }
112}
113
114impl fmt::Display for Cpf {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 let s = self.as_str();
117 write!(f, "{}.{}.{}-{}", &s[0..3], &s[3..6], &s[6..9], &s[9..11])
118 }
119}
120
121impl_document_traits!(Cpf, CpfError);
122
123pub fn remove_symbols(cpf: &str) -> String {
124 cpf.chars().filter(char::is_ascii_digit).collect()
125}
126
127pub fn is_valid(cpf: &str) -> bool {
128 let raw = remove_symbols(cpf);
129 if raw.len() != CPF_LEN {
130 return false;
131 }
132 let d: Vec<u8> = raw.bytes().map(|b| b - b'0').collect();
133 validate_digits(&d)
134}
135
136pub fn is_valid_strict(cpf: &str) -> Result<(), CpfError> {
138 parse_strict(cpf).map(|_| ())
139}
140
141pub fn format_cpf(cpf: &str) -> Option<String> {
143 let d = remove_symbols(cpf);
144 (d.len() == CPF_LEN)
145 .then(|| alloc::format!("{}.{}.{}-{}", &d[0..3], &d[3..6], &d[6..9], &d[9..11]))
146}
147
148pub fn generate() -> String {
150 generate_cpf().as_str().into()
151}
152
153pub fn generate_cpf() -> Cpf {
155 generate_with_seed(simple_seed())
156}
157
158pub fn generate_for_region(region: FiscalRegion) -> Cpf {
160 let mut seed = simple_seed();
161 let mut digits = [0u8; CPF_LEN];
162
163 loop {
164 for d in &mut digits[..8] {
165 seed = xorshift64(seed);
166 *d = (seed % 10) as u8;
167 }
168 digits[8] = region as u8;
169 if !all_equal(&digits[..9]) {
170 break;
171 }
172 }
173
174 append_check_digits(&mut digits);
175 Cpf::from_numeric(digits)
176}
177
178pub fn compute_check_digits(base: &str) -> Option<(u8, u8)> {
179 let raw = remove_symbols(base);
180 if raw.len() != 9 {
181 return None;
182 }
183
184 let d: Vec<u8> = raw.bytes().map(|b| b - b'0').collect();
185 if all_equal(&d) {
186 return None;
187 }
188
189 let d1 = check_digit(&d, &WEIGHTS_D1);
190 let mut full = [0u8; 10];
191 full[..9].copy_from_slice(&d);
192 full[9] = d1;
193 let d2 = check_digit(&full, &WEIGHTS_D2);
194
195 Some((d1, d2))
196}
197
198fn all_equal(digits: &[u8]) -> bool {
199 util::all_equal(digits)
200}
201
202fn validate_digits(d: &[u8]) -> bool {
203 !all_equal(d)
204 && d[9] == check_digit(&d[..9], &WEIGHTS_D1)
205 && d[10] == check_digit(&d[..10], &WEIGHTS_D2)
206}
207
208fn check_digit(digits: &[u8], weights: &[u32]) -> u8 {
209 let sum: u32 = digits
210 .iter()
211 .zip(weights)
212 .map(|(&d, &w)| u32::from(d) * w)
213 .sum();
214 let rem = (sum * 10) % 11;
215 if rem == 10 { 0 } else { rem as u8 }
216}
217
218fn append_check_digits(digits: &mut [u8; CPF_LEN]) {
219 digits[9] = check_digit(&digits[..9], &WEIGHTS_D1);
220 digits[10] = check_digit(&digits[..10], &WEIGHTS_D2);
221}
222
223fn parse_strict(s: &str) -> Result<Cpf, CpfError> {
224 let raw = s.as_bytes();
225
226 let numeric: Vec<u8> = match raw.len() {
227 11 => {
228 if !raw.iter().all(u8::is_ascii_digit) {
229 return Err(CpfError::InvalidCharacter);
230 }
231 raw.iter().map(|b| b - b'0').collect()
232 }
233 14 => {
234 if raw[3] != b'.' || raw[7] != b'.' || raw[11] != b'-' {
235 return Err(CpfError::InvalidFormat);
236 }
237 for &i in &FORMATTED_DIGIT_POS {
238 if !raw[i].is_ascii_digit() {
239 return Err(CpfError::InvalidCharacter);
240 }
241 }
242 FORMATTED_DIGIT_POS.iter().map(|&i| raw[i] - b'0').collect()
243 }
244 _ => return Err(CpfError::InvalidLength),
245 };
246
247 if all_equal(&numeric) {
248 return Err(CpfError::AllDigitsEqual);
249 }
250
251 let d1 = check_digit(&numeric[..9], &WEIGHTS_D1);
252 let d2 = check_digit(&numeric[..10], &WEIGHTS_D2);
253 if numeric[9] != d1 || numeric[10] != d2 {
254 return Err(CpfError::InvalidCheckDigits);
255 }
256
257 let mut digits = [0u8; CPF_LEN];
258 digits.copy_from_slice(&numeric);
259 Ok(Cpf::from_numeric(digits))
260}
261
262fn generate_with_seed(mut seed: u64) -> Cpf {
263 let mut digits = [0u8; CPF_LEN];
264
265 loop {
266 for d in &mut digits[..9] {
267 seed = xorshift64(seed);
268 *d = (seed % 10) as u8;
269 }
270 if !all_equal(&digits[..9]) {
271 break;
272 }
273 }
274
275 append_check_digits(&mut digits);
276 Cpf::from_numeric(digits)
277}
278
279#[cfg(test)]
280fn make_cpf(base: [u8; 9]) -> Cpf {
281 let mut digits = [0u8; CPF_LEN];
282 digits[..9].copy_from_slice(&base);
283 append_check_digits(&mut digits);
284 Cpf::from_numeric(digits)
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use alloc::string::ToString;
291
292 fn cpf_a() -> Cpf {
293 make_cpf([5, 2, 9, 9, 8, 2, 2, 4, 7])
294 }
295 fn cpf_b() -> Cpf {
296 make_cpf([3, 4, 7, 0, 6, 6, 1, 2, 0])
297 }
298 fn cpf_c() -> Cpf {
299 make_cpf([0, 0, 1, 2, 3, 4, 5, 6, 7])
300 }
301
302 #[test]
303 fn is_valid_accepts_valid_unformatted() {
304 assert!(is_valid(cpf_a().as_str()));
305 assert!(is_valid(cpf_b().as_str()));
306 assert!(is_valid(cpf_c().as_str()));
307 }
308
309 #[test]
310 fn is_valid_accepts_valid_formatted() {
311 let a = cpf_a().to_string();
312 let b = cpf_b().to_string();
313 assert!(is_valid(&a));
314 assert!(is_valid(&b));
315 }
316
317 #[test]
318 fn is_valid_accepts_leading_zero_cpf() {
319 let cpf = cpf_c();
320 assert!(cpf.as_str().starts_with("00"));
321 assert!(is_valid(cpf.as_str()));
322 }
323
324 #[test]
325 fn is_valid_lenient_strips_garbage() {
326 let cpf = cpf_a();
327 let s = cpf.as_str();
328 let garbage = alloc::format!("{}${}#{}!{}", &s[0..3], &s[3..6], &s[6..9], &s[9..11]);
329 assert!(is_valid(&garbage));
330
331 let padded = alloc::format!(" {} ", cpf_a());
332 assert!(is_valid(&padded));
333
334 let mut spaced = String::new();
335 for c in s.bytes() {
336 use core::fmt::Write;
337 write!(spaced, "{} ", c as char).unwrap();
338 }
339 assert!(is_valid(spaced.trim()));
340 }
341
342 #[test]
343 fn is_valid_rejects_wrong_check_digits() {
344 let mut bad = cpf_a().digits();
345 bad[10] = (bad[10] + 1) % 10;
346 let s: String = bad.iter().map(|&d| (b'0' + d) as char).collect();
347 assert!(!is_valid(&s));
348 }
349
350 #[test]
351 fn is_valid_rejects_all_same_digits() {
352 for d in 0..=9u8 {
353 let cpf: String = core::iter::repeat_n(char::from(b'0' + d), 11).collect();
354 assert!(!is_valid(&cpf), "should reject {cpf}");
355 }
356 }
357
358 #[test]
359 fn is_valid_rejects_wrong_length() {
360 assert!(!is_valid(""));
361 assert!(!is_valid("1234567890"));
362 assert!(!is_valid("123456789012"));
363 }
364
365 #[test]
366 fn is_valid_rejects_no_digits() {
367 assert!(!is_valid("abc.def.ghi-jk"));
368 assert!(!is_valid("...---"));
369 }
370
371 #[test]
372 fn is_valid_rejects_embedded_digits_in_long_string() {
373 assert!(!is_valid("abc1234567890123def"));
374 }
375
376 #[test]
377 fn strict_accepts_valid_unformatted() {
378 assert!(is_valid_strict(cpf_a().as_str()).is_ok());
379 assert!(is_valid_strict(cpf_b().as_str()).is_ok());
380 }
381
382 #[test]
383 fn strict_accepts_valid_formatted() {
384 let a = cpf_a().to_string();
385 let b = cpf_b().to_string();
386 assert!(is_valid_strict(&a).is_ok());
387 assert!(is_valid_strict(&b).is_ok());
388 }
389
390 #[test]
391 fn strict_rejects_garbage_between_digits() {
392 let cpf = cpf_a();
393 let s = cpf.as_str();
394 let garbage = alloc::format!("{}${}#{}!{}", &s[0..3], &s[3..6], &s[6..9], &s[9..11]);
395 assert!(is_valid_strict(&garbage).is_err());
396 }
397
398 #[test]
399 fn strict_rejects_whitespace() {
400 let padded = alloc::format!(" {} ", cpf_a().as_str());
401 assert_eq!(is_valid_strict(&padded), Err(CpfError::InvalidLength));
402
403 let padded_fmt = alloc::format!(" {} ", cpf_a());
404 assert_eq!(is_valid_strict(&padded_fmt), Err(CpfError::InvalidLength));
405 }
406
407 #[test]
408 fn strict_rejects_misplaced_separators() {
409 let cpf = cpf_a();
410 let s = cpf.as_str();
411 let bad_fmt = alloc::format!("{}.{}.{}.{}", &s[0..4], &s[4..6], &s[6..9], &s[9..11]);
412 assert!(is_valid_strict(&bad_fmt).is_err());
413 }
414
415 #[test]
416 fn strict_rejects_letters() {
417 assert_eq!(
418 is_valid_strict("abcdefghijk"),
419 Err(CpfError::InvalidCharacter)
420 );
421 }
422
423 #[test]
424 fn strict_rejects_all_same_digits() {
425 assert_eq!(
426 is_valid_strict("11111111111"),
427 Err(CpfError::AllDigitsEqual)
428 );
429 assert_eq!(
430 is_valid_strict("000.000.000-00"),
431 Err(CpfError::AllDigitsEqual)
432 );
433 }
434
435 #[test]
436 fn strict_rejects_invalid_check_digits() {
437 let mut bad = cpf_a().digits();
438 bad[10] = (bad[10] + 1) % 10;
439 let s: String = bad.iter().map(|&d| (b'0' + d) as char).collect();
440 assert_eq!(is_valid_strict(&s), Err(CpfError::InvalidCheckDigits));
441 }
442
443 #[test]
444 fn parse_roundtrip() {
445 let cpf = cpf_a();
446 let parsed: Cpf = cpf.to_string().parse().unwrap();
447 assert_eq!(cpf, parsed);
448 assert_eq!(parsed.as_str(), cpf.as_str());
449 }
450
451 #[test]
452 fn parse_unformatted() {
453 let cpf = cpf_a();
454 let parsed: Cpf = cpf.as_str().parse().unwrap();
455 assert_eq!(cpf, parsed);
456 }
457
458 #[test]
459 fn parse_equality_across_formats() {
460 let from_fmt: Cpf = cpf_a().to_string().parse().unwrap();
461 let from_raw: Cpf = cpf_a().as_str().parse().unwrap();
462 assert_eq!(from_fmt, from_raw);
463 }
464
465 #[test]
466 fn cpf_is_copy() {
467 let a = cpf_a();
468 let b = a;
469 assert_eq!(a, b);
470 }
471
472 #[test]
473 fn cpf_as_ref_str() {
474 let cpf = cpf_a();
475 let r: &str = cpf.as_ref();
476 assert_eq!(r, cpf.as_str());
477 }
478
479 #[test]
480 fn debug_format() {
481 let cpf = cpf_a();
482 let dbg = alloc::format!("{cpf:?}");
483 assert!(dbg.starts_with("Cpf("));
484 assert!(dbg.ends_with(')'));
485 assert!(dbg.contains('.'));
486 assert!(dbg.contains('-'));
487 }
488
489 #[test]
490 fn fiscal_region() {
491 let cpf = cpf_a();
492 let d = cpf.digits();
493 assert_eq!(cpf.fiscal_region(), FiscalRegion::from_digit(d[8]));
494
495 let cpf = cpf_b();
496 assert_eq!(cpf.digits()[8], 0);
497 assert_eq!(cpf.fiscal_region(), FiscalRegion::Rs);
498 }
499
500 #[test]
501 fn digits_array() {
502 let cpf = cpf_a();
503 assert_eq!(cpf.digits()[..9], [5, 2, 9, 9, 8, 2, 2, 4, 7]);
504 }
505
506 #[test]
507 fn parse_rejects_invalid() {
508 let mut bad = cpf_a().digits();
509 bad[10] = (bad[10] + 1) % 10;
510 let s: String = bad.iter().map(|&d| (b'0' + d) as char).collect();
511 assert!(s.parse::<Cpf>().is_err());
512 assert!("abc".parse::<Cpf>().is_err());
513 assert!("".parse::<Cpf>().is_err());
514 }
515
516 #[test]
517 fn masked() {
518 let cpf = cpf_a();
519 let s = cpf.as_str();
520 let expected = alloc::format!("{}.***.***-{}", &s[0..3], &s[9..11]);
521 assert_eq!(cpf.masked(), expected);
522 assert_eq!(cpf.masked().len(), 14);
523 }
524
525 #[test]
526 fn check_digits() {
527 let cpf = cpf_a();
528 let (d1, d2) = cpf.check_digits();
529 assert_eq!(d1, cpf.digits()[9]);
530 assert_eq!(d2, cpf.digits()[10]);
531 }
532
533 #[test]
534 fn remove_symbols_strips_formatting() {
535 let cpf = cpf_a();
536 let formatted = cpf.to_string();
537 assert_eq!(remove_symbols(&formatted), cpf.as_str());
538 assert_eq!(remove_symbols(cpf.as_str()), cpf.as_str());
539 assert_eq!(remove_symbols(""), "");
540 }
541
542 #[test]
543 fn remove_symbols_strips_arbitrary_chars() {
544 assert_eq!(remove_symbols("abc123def456ghi78901"), "12345678901");
545 }
546
547 #[test]
548 fn format_cpf_produces_formatted_output() {
549 let cpf = cpf_a();
550 let formatted = cpf.to_string();
551 assert_eq!(format_cpf(cpf.as_str()), Some(formatted.clone()));
552 assert_eq!(format_cpf(&formatted), Some(formatted));
553 }
554
555 #[test]
556 fn format_cpf_returns_none_on_bad_length() {
557 assert_eq!(format_cpf("1234"), None);
558 assert_eq!(format_cpf(""), None);
559 }
560
561 #[test]
562 fn format_cpf_preserves_leading_zeros() {
563 let cpf = cpf_c();
564 let formatted = format_cpf(cpf.as_str()).unwrap();
565 assert!(formatted.starts_with("001."));
566 }
567
568 #[test]
569 fn generate_produces_valid_cpfs() {
570 for _ in 0..100 {
571 let cpf = generate();
572 assert_eq!(cpf.len(), 11);
573 assert!(is_valid(&cpf), "generated invalid CPF: {cpf}");
574 }
575 }
576
577 #[test]
578 fn generate_cpf_roundtrips() {
579 for _ in 0..100 {
580 let cpf = generate_cpf();
581 assert!(is_valid(cpf.as_str()));
582 let parsed: Cpf = cpf.as_str().parse().unwrap();
583 assert_eq!(cpf, parsed);
584 }
585 }
586
587 #[test]
588 fn generate_for_region_respects_region_digit() {
589 let regions = [
590 FiscalRegion::Rs,
591 FiscalRegion::DfGoMsMtTo,
592 FiscalRegion::AcAmApPaRoRr,
593 FiscalRegion::CeMaPi,
594 FiscalRegion::AlPbPeRn,
595 FiscalRegion::BaSe,
596 FiscalRegion::Mg,
597 FiscalRegion::EsRj,
598 FiscalRegion::Sp,
599 FiscalRegion::PrSc,
600 ];
601 for region in regions {
602 let cpf = generate_for_region(region);
603 assert_eq!(cpf.fiscal_region(), region);
604 assert!(is_valid(cpf.as_str()));
605 }
606 }
607
608 #[test]
609 fn compute_check_digits_known_base() {
610 let cpf = cpf_a();
611 let base = &cpf.as_str()[..9];
612 let (d1, d2) = compute_check_digits(base).unwrap();
613 assert_eq!(d1, cpf.digits()[9]);
614 assert_eq!(d2, cpf.digits()[10]);
615 }
616
617 #[test]
618 fn compute_check_digits_rejects_bad_input() {
619 assert_eq!(compute_check_digits("12345678"), None);
620 assert_eq!(compute_check_digits("1234567890"), None);
621 assert_eq!(compute_check_digits("000000000"), None);
622 }
623
624 #[test]
625 fn leading_zero_cpf() {
626 let cpf = cpf_c();
627 assert!(cpf.as_str().starts_with("00"));
628 assert!(is_valid(cpf.as_str()));
629
630 let parsed: Cpf = cpf.as_str().parse().unwrap();
631 assert_eq!(parsed.digits()[0], 0);
632 assert_eq!(parsed.digits()[1], 0);
633 }
634}