ftracker_identifiers/cnpj.rs
1//! CNPJ (Cadastro Nacional da Pessoa Jurídica) — Brazil's national registry identifier for legal
2//! entities, issued by the Receita Federal.
3//!
4//! This module provides the validated Rust representation ([`Cnpj`]) and the parsing, formatting,
5//! validation, and error types that surround it. It accepts both the conventional punctuated
6//! `AA.AAA.AAA/AAAA-DD` form and the compact 14-character form, normalizes ASCII case, and
7//! guarantees that any constructed [`Cnpj`] satisfies the format and Módulo 11 checksum rules
8//! described below. There is no partially-validated state: if you hold a [`Cnpj`], it is valid.
9//!
10//! # What this type represents
11//!
12//! A CNPJ has 14 meaningful characters, split into three logical segments:
13//!
14//! | Positions | Length | Segment | Meaning |
15//! |-----------|--------|---------------------|-----------------------------------------------------------------|
16//! | 1–8 | 8 | Root (raiz) | Identifies the entity itself; shared by the head office and every branch |
17//! | 9–12 | 4 | Branch/order (ordem) | `"0001"` conventionally denotes the head office (matriz) |
18//! | 13–14 | 2 | Verification digits | Computed from the first 12 characters via Módulo 11 |
19//!
20//! [`Cnpj`] stores those 14 characters as normalized uppercase ASCII and exposes borrowed
21//! accessors for the root ([`Cnpj::root`]), the branch/order segment ([`Cnpj::branch_code`]), and
22//! both the compact ([`Cnpj::as_str`]) and punctuated ([`Cnpj::formatted`]) renderings.
23//!
24//! # Numeric and alphanumeric formats
25//!
26//! The public format changed in 2026: the first 12 positions (root + branch/order) may now
27//! contain uppercase letters as well as digits, while the final two verification digits remain
28//! numeric. This crate follows `Nota Técnica Conjunta COCAD/SUARA/RFB nº 49/2024`, which keeps the
29//! legacy numeric-only Módulo 11 calculation unchanged as a special case: each character
30//! contributes its ASCII code minus `'0'` to the checksum (so `'A'` = 17, ..., `'Z'` = 42, and
31//! digits contribute their own value), meaning a purely numeric CNPJ produces exactly the checksum
32//! it always has.
33//!
34//! Older numeric-only CNPJs remain valid and are treated as a special case of the same
35//! 14-character, same-checksum format — [`Cnpj`] represents both uniformly; there is no separate
36//! legacy type, and no separate code path to keep in sync.
37//!
38//! # Validation rules
39//!
40//! Every fallible constructor runs the same rules, in order, and each maps to one [`CnpjError`]
41//! variant:
42//!
43//! 1. **Length** — after formatting is stripped, the input must contain exactly 14 meaningful
44//! characters ([`CnpjError::InvalidLength`]).
45//! 2. **Character class** — positions 1–12 accept a digit or an uppercase letter; positions 13–14
46//! accept only a digit ([`CnpjError::InvalidCharacter`]).
47//! 3. **Not degenerate** — the 14 characters cannot all be identical, e.g. `"00000000000000"`
48//! ([`CnpjError::RepeatedDigits`]). Such inputs are structurally well-formed, and can even
49//! satisfy the checksum for certain repeated digits, but the Receita Federal never issues them;
50//! they are reliably placeholder or data-entry artifacts.
51//! 4. **Checksum** — both verification digits must match the Módulo 11 algorithm applied to the
52//! preceding characters ([`CnpjError::InvalidCheckDigits`]).
53//!
54//! [`Cnpj::parse`] additionally strips conventional punctuation (`.`, `/`, `-`, ASCII spaces)
55//! before these rules apply, and rejects empty input up front ([`CnpjError::Empty`]).
56//! [`Cnpj::from_bytes`] skips the punctuation-stripping step but still enforces every rule above.
57//!
58//! # Design notes
59//!
60//! - **No invalid state is representable.** [`Cnpj`]'s only field is private; the only ways to
61//! obtain one are [`Cnpj::parse`], [`Cnpj::new`], [`Cnpj::from_bytes`], [`FromStr`], and
62//! [`TryFrom<&str>`] — every one of them runs full validation. There is no unchecked or
63//! "trust me" constructor exposed publicly.
64//! - **Zero allocation, `Copy`, `no_std`-friendly.** [`Cnpj`] is a 14-byte value type wrapping
65//! `[u8; 14]`. Parsing, validating, formatting, and every accessor operate on the stack; nothing
66//! in this module requires an allocator.
67//! - **Ordering and hashing are byte-wise.** [`Cnpj`] derives [`Ord`] and [`Hash`] directly over
68//! its underlying ASCII bytes, which matches [`str`] ordering on [`Cnpj::as_str`]. Because ASCII
69//! digits (`'0'..='9'`) sort before uppercase letters (`'A'..='Z'`), a numeric-format CNPJ always
70//! sorts before any alphanumeric CNPJ sharing the same leading digits. This is lexicographic
71//! string order, not numeric order — don't read it as meaning "issued earlier" or
72//! "smaller root number".
73//! - **Safe to use as a map/set key.** [`Cnpj`] implements [`Eq`] and [`Hash`] consistently with
74//! [`PartialEq`], so it works as a `HashMap`/`HashSet` key or a `BTreeMap`/`BTreeSet` key out of
75//! the box.
76//!
77//! # Feature flags
78//!
79//! This module's optional integrations are off by default and purely additive — enabling one
80//! never changes the behavior of [`Cnpj::parse`] or the validation rules above:
81//!
82//! - **`serde`** — (de)serializes [`Cnpj`] as its compact 14-character string (e.g.
83//! `"12ABC34501DE35"`), never the punctuated form. Deserialization re-runs full validation, so an
84//! untrusted payload can never produce an invalid [`Cnpj`].
85//! - **`schemars`** — implements `JsonSchema` for [`Cnpj`], describing it as a pattern-constrained
86//! string (`^[A-Z0-9]{12}[0-9]{2}$`). Implies `serde`.
87//! - **`arbitrary`** — implements `Arbitrary` for [`Cnpj`], generating structurally valid,
88//! checksum-correct values for fuzz targets.
89//! - **`proptest`** — exposes reusable `proptest` strategies (`ftracker_identifiers::cnpj::proptest`,
90//! when this feature is enabled) for generating checksum-valid [`Cnpj`] values and their
91//! formatted string representations, so downstream property tests don't need to hand-roll a
92//! generator.
93//!
94//! # Error handling
95//!
96//! Every fallible constructor returns [`CnpjError`], which is `Clone + PartialEq + Eq` and
97//! implements [`core::error::Error`] and [`core::fmt::Display`], so it composes with `?` and with
98//! error-aggregation crates alike. Match on it when you need to react to a specific failure mode
99//! (for example, surfacing "which character was wrong" to a form field) rather than just the
100//! human-readable message:
101//!
102//! ```
103//! use ftracker_identifiers::{Cnpj, CnpjError};
104//!
105//! match Cnpj::parse("12.345.678/0001-XX") {
106//! Ok(cnpj) => println!("valid: {cnpj}"),
107//! Err(CnpjError::InvalidCheckDigits { expected, found, .. }) => {
108//! println!("checksum mismatch: expected {expected}, found {found}");
109//! }
110//! Err(other) => println!("rejected: {other}"),
111//! }
112//! ```
113//!
114//! # Examples
115//!
116//! ```
117//! use ftracker_identifiers::Cnpj;
118//!
119//! let numeric = Cnpj::parse("00.000.000/0001-91").unwrap();
120//! assert!(numeric.is_root());
121//! assert_eq!(numeric.as_str(), "00000000000191");
122//! assert_eq!(numeric.formatted().as_str(), "00.000.000/0001-91");
123//!
124//! let alpha = Cnpj::parse("12ABC34501DE35").unwrap();
125//! assert_eq!(alpha.branch_code(), "01DE");
126//! assert_eq!(alpha.branch_number(), None);
127//! ```
128//!
129//! Sorting and deduplicating a batch of CNPJs, e.g. after importing them from a spreadsheet:
130//!
131//! ```
132//! use ftracker_identifiers::Cnpj;
133//!
134//! let mut cnpjs: Vec<Cnpj> = ["11.222.333/0002-62", "00.000.000/0001-91", "00.000.000/0001-91"]
135//! .into_iter()
136//! .map(|s| Cnpj::parse(s).unwrap())
137//! .collect();
138//! cnpjs.sort();
139//! cnpjs.dedup();
140//! assert_eq!(cnpjs.len(), 2);
141//! ```
142
143mod error;
144mod fmt;
145mod parser;
146mod validation;
147
148#[cfg(feature = "serde")]
149mod serde;
150
151#[cfg(feature = "schemars")]
152mod schema;
153
154#[cfg(feature = "arbitrary")]
155mod arbitrary;
156
157#[cfg(any(test, feature = "proptest"))]
158pub mod proptest;
159
160#[cfg(test)]
161mod tests;
162
163pub use error::CnpjError;
164pub use fmt::FormattedCnpj;
165
166use core::convert::TryFrom;
167use core::str::{FromStr, from_utf8_unchecked};
168
169/// A validated CNPJ (Cadastro Nacional da Pessoa Jurídica).
170///
171/// `Cnpj` is a 14-byte, `Copy`, allocation-free value object. Once constructed, it is guaranteed to
172/// satisfy the structural rules and Módulo 11 checksum required by the crate — there is no way to
173/// obtain a `Cnpj` that hasn't passed validation.
174///
175/// Internally, the identifier is stored as raw uppercase ASCII bytes (`'0'...='9'` or `'A'...='Z'`).
176/// This keeps the compact representation lossless and makes borrowed access to the normalized form cheap.
177///
178/// # Constructing a `Cnpj`
179///
180/// | Constructor | Accepts |
181/// |-----------------------------------|----------------------------------------------------------------|
182/// | [`Cnpj::parse`] / [`Cnpj::new`] | Punctuated or compact strings, any ASCII case |
183/// | [`Cnpj::from_bytes`] | Exactly 14 pre-normalized ASCII bytes, no punctuation |
184/// | [`FromStr`] / [`TryFrom<&str>`] | Same as `parse`, for use in generic code |
185///
186/// All of them run the same validation and return [`CnpjError`] on failure. See the [module-level
187/// documentation](self) for the field layout, format history, and design rationale.
188#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
189#[must_use = "a parsed Cnpj should be used; discarding it wastes the validation work"]
190pub struct Cnpj {
191 bytes: [u8; 14],
192}
193
194impl Cnpj {
195 /// Parses a CNPJ from a string.
196 ///
197 /// The parser accepts the conventional `AA.AAA.AAA/AAAA-DD` form as well as the compact
198 /// 14-character form. It also tolerates surrounding and embedded ASCII spaces and folds ASCII
199 /// letters to uppercase before validation.
200 ///
201 /// This is the primary constructor; [`Cnpj::new`], [`FromStr`], and [`TryFrom<&str>`] all delegate to it.
202 ///
203 /// # Errors
204 ///
205 /// Returns [`CnpjError`] if the input is empty, does not contain exactly 14 meaningful
206 /// characters after formatting is removed, contains a character invalid for its position,
207 /// consists of a single repeated character, or fails the checksum.
208 ///
209 /// # Examples
210 ///
211 /// ```
212 /// use ftracker_identifiers::Cnpj;
213 ///
214 /// assert!(Cnpj::parse("00.000.000/0001-91").is_ok());
215 /// assert!(Cnpj::parse("00000000000191").is_ok());
216 /// assert!(Cnpj::parse("12abc34501de35").is_ok()); // lowercase is folded automatically
217 /// assert!(Cnpj::parse("not-a-cnpj").is_err());
218 /// ```
219 pub fn parse(input: &str) -> Result<Self, CnpjError> {
220 let candidate = parser::normalize(input)?;
221 Self::from_bytes(candidate)
222 }
223
224 /// Alias for [`Cnpj::parse`].
225 ///
226 /// # Errors
227 ///
228 /// See [`Cnpj::parse`].
229 ///
230 /// # Examples
231 ///
232 /// ```
233 /// use ftracker_identifiers::Cnpj;
234 ///
235 /// assert_eq!(Cnpj::new("00000000000191"), Cnpj::parse("00000000000191"));
236 /// ```
237 #[inline]
238 pub fn new(input: &str) -> Result<Self, CnpjError> {
239 Self::parse(input)
240 }
241
242 /// Constructs a `Cnpj` directly from 14 raw ASCII bytes.
243 ///
244 /// Each byte must already be an ASCII digit, and for the first 12 positions may also be an
245 /// uppercase ASCII letter. Use [`Cnpj::parse`] if the input might contain punctuation or
246 /// lowercase letters.
247 ///
248 /// Numeric-only CNPJs remain fully supported. Pass ASCII digit bytes (`b'0'...=b'9'`), not raw
249 /// numeric values.
250 ///
251 /// # Errors
252 ///
253 /// Returns [`CnpjError`] under the same conditions as [`Cnpj::parse`], except that length is
254 /// guaranteed by the `[u8; 14]` type itself: [`CnpjError::InvalidLength`] cannot occur here.
255 ///
256 /// # Examples
257 ///
258 /// ```
259 /// use ftracker_identifiers::Cnpj;
260 ///
261 /// let cnpj = Cnpj::from_bytes(*b"00000000000191").unwrap();
262 /// assert_eq!(cnpj.as_str(), "00000000000191");
263 ///
264 /// // A malformed checksum is rejected just like it would be through `parse`.
265 /// assert!(Cnpj::from_bytes(*b"00000000000192").is_err());
266 /// ```
267 #[doc(alias = "from_digits")]
268 pub fn from_bytes(bytes: [u8; 14]) -> Result<Self, CnpjError> {
269 validation::validate(&bytes)?;
270 Ok(Cnpj { bytes })
271 }
272
273 /// Returns the 14 raw ASCII bytes backing this CNPJ.
274 ///
275 /// The returned bytes are in compact form, without punctuation (for example, `b"12ABC34501DE35"`).
276 ///
277 /// # Examples
278 ///
279 /// ```
280 /// use ftracker_identifiers::Cnpj;
281 ///
282 /// let cnpj = Cnpj::parse("00000000000191").unwrap();
283 /// assert_eq!(cnpj.as_bytes(), b"00000000000191");
284 /// ```
285 #[doc(alias = "digits")]
286 #[inline]
287 #[must_use]
288 pub fn as_bytes(&self) -> &[u8; 14] {
289 &self.bytes
290 }
291
292 /// Returns the compact CNPJ as a `&str`.
293 ///
294 /// This never allocates: the bytes are guaranteed to be valid ASCII by construction.
295 ///
296 /// # Examples
297 ///
298 /// ```
299 /// use ftracker_identifiers::Cnpj;
300 ///
301 /// let cnpj = Cnpj::parse("00.000.000/0001-91").unwrap();
302 /// assert_eq!(cnpj.as_str(), "00000000000191");
303 /// ```
304 #[inline]
305 #[must_use]
306 pub fn as_str(&self) -> &str {
307 // SAFETY: The bytes array is strictly guaranteed to contain only
308 // valid ASCII uppercase alphanumeric characters by `Cnpj::from_bytes`.
309 unsafe { from_utf8_unchecked(&self.bytes) }
310 }
311
312 /// Renders the punctuated `AA.AAA.AAA/AAAA-DD` form without heap allocation.
313 ///
314 /// See [`FormattedCnpj`]. [`Cnpj`]'s own [`Display`](core::fmt::Display) implementation
315 /// delegates to this, so `cnpj.to_string()` and `cnpj.formatted().to_string()` are equivalent.
316 ///
317 /// # Examples
318 ///
319 /// ```
320 /// use ftracker_identifiers::Cnpj;
321 ///
322 /// let cnpj = Cnpj::parse("00000000000191").unwrap();
323 /// assert_eq!(cnpj.formatted().as_str(), "00.000.000/0001-91");
324 /// assert_eq!(cnpj.to_string(), cnpj.formatted().as_str());
325 /// ```
326 #[inline]
327 #[must_use]
328 pub fn formatted(&self) -> FormattedCnpj {
329 FormattedCnpj::new(self)
330 }
331
332 /// Returns the 8-character root segment.
333 ///
334 /// This identifies the entity itself and is shared by the company and all of its branches.
335 ///
336 /// # Examples
337 ///
338 /// ```
339 /// use ftracker_identifiers::Cnpj;
340 ///
341 /// let cnpj = Cnpj::parse("00000000000191").unwrap();
342 /// assert_eq!(cnpj.root(), "00000000");
343 /// ```
344 #[inline]
345 #[must_use]
346 pub fn root(&self) -> &str {
347 &self.as_str()[0..8]
348 }
349
350 /// Returns the 4-character branch/order segment.
351 ///
352 /// `"0001"` conventionally denotes the head office (matriz); see [`Cnpj::is_root`].
353 ///
354 /// # Examples
355 ///
356 /// ```
357 /// use ftracker_identifiers::Cnpj;
358 ///
359 /// let cnpj = Cnpj::parse("11.222.333/0002-62").unwrap();
360 /// assert_eq!(cnpj.branch_code(), "0002");
361 /// ```
362 #[inline]
363 #[must_use]
364 pub fn branch_code(&self) -> &str {
365 &self.as_str()[8..12]
366 }
367
368 /// Returns `true` when the branch/order segment is `"0001"`.
369 ///
370 /// # Examples
371 ///
372 /// ```
373 /// use ftracker_identifiers::Cnpj;
374 ///
375 /// assert!(Cnpj::parse("00000000000191").unwrap().is_root());
376 /// assert!(!Cnpj::parse("11.222.333/0002-62").unwrap().is_root());
377 /// ```
378 #[inline]
379 #[must_use]
380 pub fn is_root(&self) -> bool {
381 self.branch_code() == "0001"
382 }
383
384 /// Returns the branch/order segment as a number when it is purely numeric.
385 ///
386 /// Returns `None` when the segment contains a letter, which is only possible for
387 /// alphanumeric-format CNPJs. Numeric CNPJs, including the conventional matriz marker
388 /// (`"0001"`), always parse successfully.
389 ///
390 /// # Examples
391 ///
392 /// ```
393 /// use ftracker_identifiers::Cnpj;
394 ///
395 /// let matriz = Cnpj::parse("00000000000191").unwrap();
396 /// assert_eq!(matriz.branch_number(), Some(1));
397 ///
398 /// let alphanumeric_branch = Cnpj::parse("12ABC34501DE35").unwrap();
399 /// assert_eq!(alphanumeric_branch.branch_code(), "01DE");
400 /// assert_eq!(alphanumeric_branch.branch_number(), None);
401 /// ```
402 #[must_use]
403 pub fn branch_number(&self) -> Option<u16> {
404 self.branch_code().parse().ok()
405 }
406
407 /// Returns the two verification digits as numeric values.
408 ///
409 /// # Examples
410 ///
411 /// ```
412 /// use ftracker_identifiers::Cnpj;
413 ///
414 /// let cnpj = Cnpj::parse("00000000000191").unwrap();
415 /// assert_eq!(cnpj.check_digits(), (9, 1));
416 /// ```
417 #[inline]
418 #[must_use]
419 pub fn check_digits(&self) -> (u8, u8) {
420 (self.bytes[12] - b'0', self.bytes[13] - b'0')
421 }
422}
423
424impl FromStr for Cnpj {
425 type Err = CnpjError;
426
427 /// Delegates to [`Cnpj::parse`], enabling `input.parse::<Cnpj>()` and use in generic code
428 /// bounded by [`FromStr`].
429 fn from_str(s: &str) -> Result<Self, Self::Err> {
430 Self::parse(s)
431 }
432}
433
434impl TryFrom<&str> for Cnpj {
435 type Error = CnpjError;
436
437 /// Delegates to [`Cnpj::parse`], enabling `Cnpj::try_from(input)` and use in generic code
438 /// bounded by [`TryFrom<&str>`].
439 fn try_from(value: &str) -> Result<Self, Self::Error> {
440 Self::parse(value)
441 }
442}
443
444impl TryFrom<[u8; 14]> for Cnpj {
445 type Error = CnpjError;
446
447 /// Delegates to [`Cnpj::from_bytes`]. The bytes must already be pre normalized ASCII, without
448 /// punctuation.
449 fn try_from(value: [u8; 14]) -> Result<Self, Self::Error> {
450 Self::from_bytes(value)
451 }
452}
453
454impl TryFrom<&[u8]> for Cnpj {
455 type Error = CnpjError;
456
457 /// Validates a byte slice as a CNPJ. The slice must be exactly 14 pre normalized ASCII bytes,
458 /// without punctuation; any other length yields [`CnpjError::InvalidLength`]. Once the length is
459 /// confirmed, this behaves like [`Cnpj::from_bytes`].
460 fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
461 let bytes: [u8; 14] = value
462 .try_into()
463 .map_err(|_| CnpjError::InvalidLength { found: value.len() })?;
464 Self::from_bytes(bytes)
465 }
466}
467
468impl PartialEq<str> for Cnpj {
469 /// Compares against a string slice by its compact 14 character representation (not the
470 /// punctuated form).
471 fn eq(&self, other: &str) -> bool {
472 self.as_str() == other
473 }
474}
475
476impl PartialEq<&str> for Cnpj {
477 /// Compares against a string slice by its compact 14 character representation (not the
478 /// punctuated form).
479 fn eq(&self, other: &&str) -> bool {
480 self.as_str() == *other
481 }
482}
483
484impl PartialEq<Cnpj> for str {
485 fn eq(&self, other: &Cnpj) -> bool {
486 self == other.as_str()
487 }
488}
489
490impl PartialEq<Cnpj> for &str {
491 fn eq(&self, other: &Cnpj) -> bool {
492 *self == other.as_str()
493 }
494}
495
496impl AsRef<[u8]> for Cnpj {
497 /// Equivalent to [`Cnpj::as_bytes`], borrowed as a slice.
498 fn as_ref(&self) -> &[u8] {
499 &self.bytes
500 }
501}
502
503impl AsRef<str> for Cnpj {
504 /// Equivalent to [`Cnpj::as_str`].
505 fn as_ref(&self) -> &str {
506 self.as_str()
507 }
508}