Skip to main content

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}