1use alloc::string::String;
6use alloc::vec::Vec;
7use core::fmt;
8
9use crate::rand::{RandomSource, SeededRng, below_u8, simple_seed};
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
62impl core::error::Error for CpfError {}
63
64#[derive(Clone, Copy, PartialEq, Eq, Hash)]
77pub struct Cpf {
78 bytes: [u8; CPF_LEN],
79}
80
81impl Cpf {
82 pub fn as_str(&self) -> &str {
84 unsafe { core::str::from_utf8_unchecked(&self.bytes) }
86 }
87
88 pub fn digits(&self) -> [u8; CPF_LEN] {
90 self.bytes.map(|b| b - b'0')
91 }
92
93 pub fn fiscal_region(&self) -> FiscalRegion {
95 FiscalRegion::from_digit(self.bytes[8] - b'0')
96 }
97
98 pub fn masked(&self) -> String {
100 let s = self.as_str();
101 alloc::format!("{}.***.***-{}", &s[0..3], &s[9..11])
102 }
103
104 pub fn check_digits(&self) -> (u8, u8) {
106 (self.bytes[9] - b'0', self.bytes[10] - b'0')
107 }
108
109 fn from_numeric(digits: [u8; CPF_LEN]) -> Self {
110 Self {
111 bytes: digits.map(|d| d + b'0'),
112 }
113 }
114}
115
116impl fmt::Display for Cpf {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 let s = self.as_str();
119 write!(f, "{}.{}.{}-{}", &s[0..3], &s[3..6], &s[6..9], &s[9..11])
120 }
121}
122
123impl_document_traits!(Cpf, CpfError);
124
125pub fn normalize(cpf: &str) -> String {
127 cpf.chars().filter(char::is_ascii_digit).collect()
128}
129
130pub fn remove_symbols(cpf: &str) -> String {
132 normalize(cpf)
133}
134
135pub fn is_valid_lenient(cpf: &str) -> bool {
137 let raw = normalize(cpf);
138 if raw.len() != CPF_LEN {
139 return false;
140 }
141 let d: Vec<u8> = raw.bytes().map(|b| b - b'0').collect();
142 validate_digits(&d)
143}
144
145pub fn is_valid(cpf: &str) -> bool {
149 is_valid_lenient(cpf)
150}
151
152pub fn is_valid_strict(cpf: &str) -> Result<(), CpfError> {
154 parse_strict(cpf).map(|_| ())
155}
156
157pub fn format_cpf(cpf: &str) -> Option<String> {
159 let d = remove_symbols(cpf);
160 (d.len() == CPF_LEN)
161 .then(|| alloc::format!("{}.{}.{}-{}", &d[0..3], &d[3..6], &d[6..9], &d[9..11]))
162}
163
164pub fn generate() -> String {
168 generate_cpf().as_str().into()
169}
170
171pub fn generate_cpf() -> Cpf {
175 let mut rng = SeededRng::new(simple_seed());
176 generate_cpf_with_rng(&mut rng)
177}
178
179pub fn generate_cpf_with_rng<R: RandomSource + ?Sized>(rng: &mut R) -> Cpf {
183 generate_with_rng(rng)
184}
185
186pub fn generate_for_region(region: FiscalRegion) -> Cpf {
190 let mut rng = SeededRng::new(simple_seed());
191 generate_for_region_with_rng(&mut rng, region)
192}
193
194pub fn generate_for_region_with_seed(seed: u64, region: FiscalRegion) -> Cpf {
198 let mut rng = SeededRng::new(seed);
199 generate_for_region_with_rng(&mut rng, region)
200}
201
202pub fn generate_for_region_with_rng<R: RandomSource + ?Sized>(
206 rng: &mut R,
207 region: FiscalRegion,
208) -> Cpf {
209 let mut digits = [0u8; CPF_LEN];
210
211 loop {
212 for d in &mut digits[..8] {
213 *d = below_u8(rng, 10);
214 }
215 digits[8] = region as u8;
216 if !all_equal(&digits[..9]) {
217 break;
218 }
219 }
220
221 append_check_digits(&mut digits);
222 Cpf::from_numeric(digits)
223}
224
225pub fn compute_check_digits(base: &str) -> Option<(u8, u8)> {
226 let raw = remove_symbols(base);
227 if raw.len() != 9 {
228 return None;
229 }
230
231 let d: Vec<u8> = raw.bytes().map(|b| b - b'0').collect();
232 if all_equal(&d) {
233 return None;
234 }
235
236 let d1 = check_digit(&d, &WEIGHTS_D1);
237 let mut full = [0u8; 10];
238 full[..9].copy_from_slice(&d);
239 full[9] = d1;
240 let d2 = check_digit(&full, &WEIGHTS_D2);
241
242 Some((d1, d2))
243}
244
245fn all_equal(digits: &[u8]) -> bool {
246 util::all_equal(digits)
247}
248
249fn validate_digits(d: &[u8]) -> bool {
250 !all_equal(d)
251 && d[9] == check_digit(&d[..9], &WEIGHTS_D1)
252 && d[10] == check_digit(&d[..10], &WEIGHTS_D2)
253}
254
255fn check_digit(digits: &[u8], weights: &[u32]) -> u8 {
256 let sum: u32 = digits
257 .iter()
258 .zip(weights)
259 .map(|(&d, &w)| u32::from(d) * w)
260 .sum();
261 let rem = (sum * 10) % 11;
262 if rem == 10 { 0 } else { rem as u8 }
263}
264
265fn append_check_digits(digits: &mut [u8; CPF_LEN]) {
266 digits[9] = check_digit(&digits[..9], &WEIGHTS_D1);
267 digits[10] = check_digit(&digits[..10], &WEIGHTS_D2);
268}
269
270fn parse_strict(s: &str) -> Result<Cpf, CpfError> {
271 let raw = s.as_bytes();
272
273 let numeric: Vec<u8> = match raw.len() {
274 11 => {
275 if !raw.iter().all(u8::is_ascii_digit) {
276 return Err(CpfError::InvalidCharacter);
277 }
278 raw.iter().map(|b| b - b'0').collect()
279 }
280 14 => {
281 if raw[3] != b'.' || raw[7] != b'.' || raw[11] != b'-' {
282 return Err(CpfError::InvalidFormat);
283 }
284 for &i in &FORMATTED_DIGIT_POS {
285 if !raw[i].is_ascii_digit() {
286 return Err(CpfError::InvalidCharacter);
287 }
288 }
289 FORMATTED_DIGIT_POS.iter().map(|&i| raw[i] - b'0').collect()
290 }
291 _ => return Err(CpfError::InvalidLength),
292 };
293
294 if all_equal(&numeric) {
295 return Err(CpfError::AllDigitsEqual);
296 }
297
298 let d1 = check_digit(&numeric[..9], &WEIGHTS_D1);
299 let d2 = check_digit(&numeric[..10], &WEIGHTS_D2);
300 if numeric[9] != d1 || numeric[10] != d2 {
301 return Err(CpfError::InvalidCheckDigits);
302 }
303
304 let mut digits = [0u8; CPF_LEN];
305 digits.copy_from_slice(&numeric);
306 Ok(Cpf::from_numeric(digits))
307}
308
309pub fn generate_with_seed(seed: u64) -> Cpf {
313 let mut rng = SeededRng::new(seed);
314 generate_with_rng(&mut rng)
315}
316
317pub fn generate_with_rng<R: RandomSource + ?Sized>(rng: &mut R) -> Cpf {
321 let mut digits = [0u8; CPF_LEN];
322
323 loop {
324 for d in &mut digits[..9] {
325 *d = below_u8(rng, 10);
326 }
327 if !all_equal(&digits[..9]) {
328 break;
329 }
330 }
331
332 append_check_digits(&mut digits);
333 Cpf::from_numeric(digits)
334}
335
336#[cfg(test)]
337fn make_cpf(base: [u8; 9]) -> Cpf {
338 let mut digits = [0u8; CPF_LEN];
339 digits[..9].copy_from_slice(&base);
340 append_check_digits(&mut digits);
341 Cpf::from_numeric(digits)
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347 use alloc::string::ToString;
348
349 fn cpf_a() -> Cpf {
350 make_cpf([5, 2, 9, 9, 8, 2, 2, 4, 7])
351 }
352 fn cpf_b() -> Cpf {
353 make_cpf([3, 4, 7, 0, 6, 6, 1, 2, 0])
354 }
355 fn cpf_c() -> Cpf {
356 make_cpf([0, 0, 1, 2, 3, 4, 5, 6, 7])
357 }
358
359 #[test]
360 fn is_valid_accepts_valid_unformatted() {
361 assert!(is_valid(cpf_a().as_str()));
362 assert!(is_valid(cpf_b().as_str()));
363 assert!(is_valid(cpf_c().as_str()));
364 }
365
366 #[test]
367 fn is_valid_accepts_valid_formatted() {
368 let a = cpf_a().to_string();
369 let b = cpf_b().to_string();
370 assert!(is_valid(&a));
371 assert!(is_valid(&b));
372 }
373
374 #[test]
375 fn is_valid_accepts_leading_zero_cpf() {
376 let cpf = cpf_c();
377 assert!(cpf.as_str().starts_with("00"));
378 assert!(is_valid(cpf.as_str()));
379 }
380
381 #[test]
382 fn is_valid_lenient_strips_garbage() {
383 let cpf = cpf_a();
384 let s = cpf.as_str();
385 let garbage = alloc::format!("{}${}#{}!{}", &s[0..3], &s[3..6], &s[6..9], &s[9..11]);
386 assert!(is_valid(&garbage));
387
388 let padded = alloc::format!(" {} ", cpf_a());
389 assert!(is_valid(&padded));
390
391 let mut spaced = String::new();
392 for c in s.bytes() {
393 use core::fmt::Write;
394 write!(spaced, "{} ", c as char).unwrap();
395 }
396 assert!(is_valid(spaced.trim()));
397 }
398
399 #[test]
400 fn is_valid_rejects_wrong_check_digits() {
401 let mut bad = cpf_a().digits();
402 bad[10] = (bad[10] + 1) % 10;
403 let s: String = bad.iter().map(|&d| (b'0' + d) as char).collect();
404 assert!(!is_valid(&s));
405 }
406
407 #[test]
408 fn is_valid_rejects_all_same_digits() {
409 for d in 0..=9u8 {
410 let cpf: String = core::iter::repeat_n(char::from(b'0' + d), 11).collect();
411 assert!(!is_valid(&cpf), "should reject {cpf}");
412 }
413 }
414
415 #[test]
416 fn is_valid_rejects_wrong_length() {
417 assert!(!is_valid(""));
418 assert!(!is_valid("1234567890"));
419 assert!(!is_valid("123456789012"));
420 }
421
422 #[test]
423 fn is_valid_rejects_no_digits() {
424 assert!(!is_valid("abc.def.ghi-jk"));
425 assert!(!is_valid("...---"));
426 }
427
428 #[test]
429 fn is_valid_rejects_embedded_digits_in_long_string() {
430 assert!(!is_valid("abc1234567890123def"));
431 }
432
433 #[test]
434 fn strict_accepts_valid_unformatted() {
435 assert!(is_valid_strict(cpf_a().as_str()).is_ok());
436 assert!(is_valid_strict(cpf_b().as_str()).is_ok());
437 }
438
439 #[test]
440 fn strict_accepts_valid_formatted() {
441 let a = cpf_a().to_string();
442 let b = cpf_b().to_string();
443 assert!(is_valid_strict(&a).is_ok());
444 assert!(is_valid_strict(&b).is_ok());
445 }
446
447 #[test]
448 fn strict_rejects_garbage_between_digits() {
449 let cpf = cpf_a();
450 let s = cpf.as_str();
451 let garbage = alloc::format!("{}${}#{}!{}", &s[0..3], &s[3..6], &s[6..9], &s[9..11]);
452 assert!(is_valid_strict(&garbage).is_err());
453 }
454
455 #[test]
456 fn strict_rejects_whitespace() {
457 let padded = alloc::format!(" {} ", cpf_a().as_str());
458 assert_eq!(is_valid_strict(&padded), Err(CpfError::InvalidLength));
459
460 let padded_fmt = alloc::format!(" {} ", cpf_a());
461 assert_eq!(is_valid_strict(&padded_fmt), Err(CpfError::InvalidLength));
462 }
463
464 #[test]
465 fn strict_rejects_misplaced_separators() {
466 let cpf = cpf_a();
467 let s = cpf.as_str();
468 let bad_fmt = alloc::format!("{}.{}.{}.{}", &s[0..4], &s[4..6], &s[6..9], &s[9..11]);
469 assert!(is_valid_strict(&bad_fmt).is_err());
470 }
471
472 #[test]
473 fn strict_rejects_letters() {
474 assert_eq!(
475 is_valid_strict("abcdefghijk"),
476 Err(CpfError::InvalidCharacter)
477 );
478 }
479
480 #[test]
481 fn strict_rejects_all_same_digits() {
482 assert_eq!(
483 is_valid_strict("11111111111"),
484 Err(CpfError::AllDigitsEqual)
485 );
486 assert_eq!(
487 is_valid_strict("000.000.000-00"),
488 Err(CpfError::AllDigitsEqual)
489 );
490 }
491
492 #[test]
493 fn strict_rejects_invalid_check_digits() {
494 let mut bad = cpf_a().digits();
495 bad[10] = (bad[10] + 1) % 10;
496 let s: String = bad.iter().map(|&d| (b'0' + d) as char).collect();
497 assert_eq!(is_valid_strict(&s), Err(CpfError::InvalidCheckDigits));
498 }
499
500 #[test]
501 fn parse_roundtrip() {
502 let cpf = cpf_a();
503 let parsed: Cpf = cpf.to_string().parse().unwrap();
504 assert_eq!(cpf, parsed);
505 assert_eq!(parsed.as_str(), cpf.as_str());
506 }
507
508 #[test]
509 fn parse_unformatted() {
510 let cpf = cpf_a();
511 let parsed: Cpf = cpf.as_str().parse().unwrap();
512 assert_eq!(cpf, parsed);
513 }
514
515 #[test]
516 fn parse_equality_across_formats() {
517 let from_fmt: Cpf = cpf_a().to_string().parse().unwrap();
518 let from_raw: Cpf = cpf_a().as_str().parse().unwrap();
519 assert_eq!(from_fmt, from_raw);
520 }
521
522 #[test]
523 fn cpf_is_copy() {
524 let a = cpf_a();
525 let b = a;
526 assert_eq!(a, b);
527 }
528
529 #[test]
530 fn cpf_as_ref_str() {
531 let cpf = cpf_a();
532 let r: &str = cpf.as_ref();
533 assert_eq!(r, cpf.as_str());
534 }
535
536 #[test]
537 fn debug_format() {
538 let cpf = cpf_a();
539 let dbg = alloc::format!("{cpf:?}");
540 assert!(dbg.starts_with("Cpf("));
541 assert!(dbg.ends_with(')'));
542 assert!(dbg.contains('.'));
543 assert!(dbg.contains('-'));
544 }
545
546 #[test]
547 fn fiscal_region() {
548 let cpf = cpf_a();
549 let d = cpf.digits();
550 assert_eq!(cpf.fiscal_region(), FiscalRegion::from_digit(d[8]));
551
552 let cpf = cpf_b();
553 assert_eq!(cpf.digits()[8], 0);
554 assert_eq!(cpf.fiscal_region(), FiscalRegion::Rs);
555 }
556
557 #[test]
558 fn digits_array() {
559 let cpf = cpf_a();
560 assert_eq!(cpf.digits()[..9], [5, 2, 9, 9, 8, 2, 2, 4, 7]);
561 }
562
563 #[test]
564 fn parse_rejects_invalid() {
565 let mut bad = cpf_a().digits();
566 bad[10] = (bad[10] + 1) % 10;
567 let s: String = bad.iter().map(|&d| (b'0' + d) as char).collect();
568 assert!(s.parse::<Cpf>().is_err());
569 assert!("abc".parse::<Cpf>().is_err());
570 assert!("".parse::<Cpf>().is_err());
571 }
572
573 #[test]
574 fn masked() {
575 let cpf = cpf_a();
576 let s = cpf.as_str();
577 let expected = alloc::format!("{}.***.***-{}", &s[0..3], &s[9..11]);
578 assert_eq!(cpf.masked(), expected);
579 assert_eq!(cpf.masked().len(), 14);
580 }
581
582 #[test]
583 fn check_digits() {
584 let cpf = cpf_a();
585 let (d1, d2) = cpf.check_digits();
586 assert_eq!(d1, cpf.digits()[9]);
587 assert_eq!(d2, cpf.digits()[10]);
588 }
589
590 #[test]
591 fn remove_symbols_strips_formatting() {
592 let cpf = cpf_a();
593 let formatted = cpf.to_string();
594 assert_eq!(remove_symbols(&formatted), cpf.as_str());
595 assert_eq!(remove_symbols(cpf.as_str()), cpf.as_str());
596 assert_eq!(remove_symbols(""), "");
597 }
598
599 #[test]
600 fn remove_symbols_strips_arbitrary_chars() {
601 assert_eq!(remove_symbols("abc123def456ghi78901"), "12345678901");
602 }
603
604 #[test]
605 fn format_cpf_produces_formatted_output() {
606 let cpf = cpf_a();
607 let formatted = cpf.to_string();
608 assert_eq!(format_cpf(cpf.as_str()), Some(formatted.clone()));
609 assert_eq!(format_cpf(&formatted), Some(formatted));
610 }
611
612 #[test]
613 fn format_cpf_returns_none_on_bad_length() {
614 assert_eq!(format_cpf("1234"), None);
615 assert_eq!(format_cpf(""), None);
616 }
617
618 #[test]
619 fn format_cpf_preserves_leading_zeros() {
620 let cpf = cpf_c();
621 let formatted = format_cpf(cpf.as_str()).unwrap();
622 assert!(formatted.starts_with("001."));
623 }
624
625 #[test]
626 fn generate_produces_valid_cpfs() {
627 for _ in 0..100 {
628 let cpf = generate();
629 assert_eq!(cpf.len(), 11);
630 assert!(is_valid(&cpf), "generated invalid CPF: {cpf}");
631 }
632 }
633
634 #[test]
635 fn generate_cpf_roundtrips() {
636 for _ in 0..100 {
637 let cpf = generate_cpf();
638 assert!(is_valid(cpf.as_str()));
639 let parsed: Cpf = cpf.as_str().parse().unwrap();
640 assert_eq!(cpf, parsed);
641 }
642 }
643
644 #[test]
645 fn seeded_generation_accepts_zero_and_is_deterministic() {
646 assert_eq!(generate_with_seed(0), generate_with_seed(0));
647 assert!(is_valid(generate_with_seed(0).as_str()));
648 }
649
650 #[test]
651 fn generate_for_region_respects_region_digit() {
652 let regions = [
653 FiscalRegion::Rs,
654 FiscalRegion::DfGoMsMtTo,
655 FiscalRegion::AcAmApPaRoRr,
656 FiscalRegion::CeMaPi,
657 FiscalRegion::AlPbPeRn,
658 FiscalRegion::BaSe,
659 FiscalRegion::Mg,
660 FiscalRegion::EsRj,
661 FiscalRegion::Sp,
662 FiscalRegion::PrSc,
663 ];
664 for region in regions {
665 let cpf = generate_for_region(region);
666 assert_eq!(cpf.fiscal_region(), region);
667 assert!(is_valid(cpf.as_str()));
668 }
669 }
670
671 #[test]
672 fn compute_check_digits_known_base() {
673 let cpf = cpf_a();
674 let base = &cpf.as_str()[..9];
675 let (d1, d2) = compute_check_digits(base).unwrap();
676 assert_eq!(d1, cpf.digits()[9]);
677 assert_eq!(d2, cpf.digits()[10]);
678 }
679
680 #[test]
681 fn compute_check_digits_rejects_bad_input() {
682 assert_eq!(compute_check_digits("12345678"), None);
683 assert_eq!(compute_check_digits("1234567890"), None);
684 assert_eq!(compute_check_digits("000000000"), None);
685 }
686
687 #[test]
688 fn leading_zero_cpf() {
689 let cpf = cpf_c();
690 assert!(cpf.as_str().starts_with("00"));
691 assert!(is_valid(cpf.as_str()));
692
693 let parsed: Cpf = cpf.as_str().parse().unwrap();
694 assert_eq!(parsed.digits()[0], 0);
695 assert_eq!(parsed.digits()[1], 0);
696 }
697}