Skip to main content

aamva/
data.rs

1//! Parsed AAMVA DL / ID data model.
2
3use chrono::NaiveDate;
4use std::collections::BTreeMap;
5use strum::{Display, EnumString};
6
7/// Sex / gender as encoded in element `DBC`.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, Display)]
9pub enum Sex {
10    #[strum(serialize = "1")]
11    Male,
12    #[strum(serialize = "2")]
13    Female,
14    /// `9` — not specified / other.
15    #[strum(serialize = "9")]
16    NotSpecified,
17}
18
19/// Eye colour codes — AAMVA §D.12.6. Unknown codes fall into `Other`.
20#[derive(Debug, Clone, PartialEq, Eq, EnumString, Display)]
21#[strum(ascii_case_insensitive)]
22pub enum EyeColor {
23    #[strum(serialize = "BLK")]
24    Black,
25    #[strum(serialize = "BLU")]
26    Blue,
27    #[strum(serialize = "BRO", serialize = "BRN")]
28    Brown,
29    #[strum(serialize = "GRY")]
30    Gray,
31    #[strum(serialize = "GRN")]
32    Green,
33    #[strum(serialize = "HAZ")]
34    Hazel,
35    #[strum(serialize = "MAR")]
36    Maroon,
37    #[strum(serialize = "PNK")]
38    Pink,
39    #[strum(serialize = "DIC")]
40    Dichromatic,
41    #[strum(serialize = "UNK")]
42    Unknown,
43    #[strum(default)]
44    Other(String),
45}
46
47/// Hair colour codes — AAMVA §D.12.5. Unknown codes fall into `Other`.
48#[derive(Debug, Clone, PartialEq, Eq, EnumString, Display)]
49#[strum(ascii_case_insensitive)]
50pub enum HairColor {
51    #[strum(serialize = "BAL")]
52    Bald,
53    #[strum(serialize = "BLK")]
54    Black,
55    #[strum(serialize = "BLN")]
56    Blond,
57    #[strum(serialize = "BRO", serialize = "BRN")]
58    Brown,
59    #[strum(serialize = "GRY")]
60    Gray,
61    #[strum(serialize = "RED")]
62    Red,
63    #[strum(serialize = "SDY")]
64    Sandy,
65    #[strum(serialize = "WHI")]
66    White,
67    #[strum(serialize = "UNK")]
68    Unknown,
69    #[strum(default)]
70    Other(String),
71}
72
73/// Country identifier from element `DCG`.
74#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, Display)]
75pub enum Country {
76    #[strum(serialize = "USA")]
77    Usa,
78    #[strum(serialize = "CAN")]
79    Canada,
80}
81
82/// DHS / REAL ID compliance indicator from element `DDA`.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, Display)]
84pub enum Compliance {
85    /// `F` — fully compliant (REAL ID).
86    #[strum(serialize = "F")]
87    Compliant,
88    /// `N` — non-compliant.
89    #[strum(serialize = "N")]
90    NonCompliant,
91}
92
93/// Height as it appears in element `DAU`.
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum Height {
96    /// Value in inches (USA).
97    Inches(u16),
98    /// Value in centimetres (Canada).
99    Centimetres(u16),
100}
101
102impl Height {
103    pub(crate) fn parse(raw: &str) -> Option<Self> {
104        // AAMVA format: "nnn in" or "nnn cm" (commonly 3-digit value).
105        let trimmed = raw.trim();
106        let digits: String = trimmed.chars().take_while(|c| c.is_ascii_digit()).collect();
107        if digits.is_empty() {
108            return None;
109        }
110        let value: u16 = digits.parse().ok()?;
111        let unit = trimmed[digits.len()..].trim().to_ascii_lowercase();
112        match unit.as_str() {
113            // USA jurisdictions frequently omit the unit; treat bare values as inches.
114            "" | "in" => Some(Height::Inches(value)),
115            "cm" => Some(Height::Centimetres(value)),
116            // Anything else is an unrecognised unit — reject rather than guess.
117            _ => None,
118        }
119    }
120}
121
122/// Truncation flag (`T` / `N` / `U`) applied to name fields `DDE` / `DDF` /
123/// `DDG` — indicates whether the preceding name has been truncated to fit
124/// AAMVA limits.
125#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, Display)]
126pub enum Truncation {
127    /// `T` — truncated.
128    #[strum(serialize = "T")]
129    Truncated,
130    /// `N` — not truncated.
131    #[strum(serialize = "N")]
132    NotTruncated,
133    /// `U` — unknown / not supported by jurisdiction.
134    #[strum(serialize = "U")]
135    Unknown,
136}
137
138/// Parsed AAMVA header — everything before the first subfile.
139#[derive(Debug, Clone)]
140pub struct AamvaHeader {
141    pub iin: String,
142    pub aamva_version: u8,
143    pub jurisdiction_version: u8,
144    pub entry_count: u8,
145    pub subfiles: Vec<SubfileDesignator>,
146}
147
148/// Locates one subfile in the payload.
149#[derive(Debug, Clone)]
150pub struct SubfileDesignator {
151    /// 2-char subfile type (`DL` for driver licence, `ID` for identification,
152    /// `JA`–`JZ` for jurisdiction-specific extensions).
153    pub subfile_type: String,
154    /// Offset from the start of the payload to the subfile's type tag.
155    pub offset: usize,
156    /// Length in bytes of the subfile including the leading type tag and
157    /// trailing segment terminator.
158    pub length: usize,
159}
160
161/// Full parsed AAMVA license.
162#[derive(Debug, Clone, Default)]
163pub struct AamvaLicense {
164    pub header: Option<AamvaHeader>,
165
166    /// Raw 3-letter → value map, exactly as encoded in every subfile.
167    pub elements: BTreeMap<String, String>,
168
169    // -------- Identity --------
170    pub family_name: Option<String>,
171    pub first_name: Option<String>,
172    pub middle_name: Option<String>,
173    pub name_suffix: Option<String>,
174    pub family_name_truncation: Option<Truncation>,
175    pub first_name_truncation: Option<Truncation>,
176    pub middle_name_truncation: Option<Truncation>,
177
178    // -------- Identifiers --------
179    pub document_number: Option<String>,
180    pub document_discriminator: Option<String>,
181    pub country: Option<Country>,
182    pub jurisdiction: Option<String>,
183
184    // -------- Dates --------
185    pub date_of_birth: Option<NaiveDate>,
186    pub issue_date: Option<NaiveDate>,
187    pub expiry_date: Option<NaiveDate>,
188    pub card_revision_date: Option<NaiveDate>,
189    pub under_18_until: Option<NaiveDate>,
190    pub under_19_until: Option<NaiveDate>,
191    pub under_21_until: Option<NaiveDate>,
192
193    // -------- Physical --------
194    pub sex: Option<Sex>,
195    pub eye_color: Option<EyeColor>,
196    pub hair_color: Option<HairColor>,
197    pub height: Option<Height>,
198    pub weight_lb: Option<u32>,
199    pub weight_kg: Option<u32>,
200    pub weight_range: Option<u8>,
201
202    // -------- Address --------
203    pub address_street_1: Option<String>,
204    pub address_street_2: Option<String>,
205    pub city: Option<String>,
206    pub postal_code: Option<String>,
207
208    // -------- Licence classification --------
209    pub vehicle_class: Option<String>,
210    pub restrictions: Option<String>,
211    pub endorsements: Option<String>,
212
213    // -------- Flags --------
214    pub organ_donor: Option<bool>,
215    pub veteran: Option<bool>,
216    pub compliance: Option<Compliance>,
217}