1#![no_std]
4#![warn(missing_docs)]
5#![cfg_attr(feature = "cargo-clippy", allow(clippy::style))]
6
7use core::{fmt, str, slice, char};
8
9#[cfg(debug_assertions)]
10macro_rules! unreach {
11 () => {
12 unreachable!()
13 }
14}
15
16#[cfg(not(debug_assertions))]
17macro_rules! unreach {
18 () => {
19 unsafe {
20 core::hint::unreachable_unchecked()
21 }
22 }
23}
24
25mod dicts;
26pub use dicts::Region;
27
28const VIN_LEN: usize = 17;
29
30#[derive(Copy, Clone, Debug, PartialEq, Eq)]
32pub enum VinError {
33 InvalidLen,
35 InvalidChar(usize, char),
37}
38
39impl fmt::Display for VinError {
40 #[inline(always)]
41 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
42 match self {
43 Self::InvalidLen => fmt.write_str("VIN MUST be 17 characters long"),
44 Self::InvalidChar(idx, ch) => fmt.write_fmt(format_args!("VIN contains invalid character '{ch}' at idx={idx}")),
45 }
46 }
47}
48
49#[repr(transparent)]
50#[derive(Copy, Clone, PartialEq, Eq)]
51pub struct Vin<'a>(&'a str);
53
54impl<'a> Vin<'a> {
55 pub const fn new(vin: &'a str) -> Self {
57 match Self::try_new(vin) {
58 Ok(this) => this,
59 Err(VinError::InvalidLen) => panic!("Invalid length of VIN"),
60 Err(VinError::InvalidChar(_, _)) => panic!("VIN contains invalid character"),
61 }
62 }
63
64 pub const fn try_new(vin: &'a str) -> Result<Self, VinError> {
66 if vin.len() != VIN_LEN {
67 return Err(VinError::InvalidLen);
68 }
69
70 let mut idx = 0;
71 while idx < vin.len() {
72 let ch = vin.as_bytes()[idx];
73 match ch as char {
74 'I' | 'O' | 'Q' => return Err(VinError::InvalidChar(idx, ch as char)),
76 'A'..='Z' | '0'..='9' => idx += 1,
77 ch => return Err(VinError::InvalidChar(idx, ch)),
78 }
79 }
80
81 Ok(Self(vin))
82 }
83
84 pub const fn calculate_checksum(&self) -> u32 {
86 let mut result = 0u32;
87 let mut idx = 0;
88
89 while idx < self.0.len() {
90 let ch = self.0.as_bytes()[idx];
91 result = result.wrapping_add(dicts::vin_char_weight(ch as char) * dicts::WEIGHTS[idx]);
94
95 idx += 1;
96 }
97
98 result
99 }
100
101 pub const fn calculate_checksum_digit(&self) -> char {
105 let checksum = self.calculate_checksum();
106 match checksum % 11 {
107 10 => 'X',
108 digit => match char::from_digit(digit, 10) {
109 Some(ch) => ch,
110 None => unreach!(),
111 }
112 }
113 }
114
115 #[inline(always)]
116 pub const fn is_checksum_valid(&self) -> bool {
118 let expected = self.calculate_checksum_digit();
119 self.0.as_bytes()[8] as char == expected
120 }
121
122 const fn slice(&self, start: usize, len: usize) -> &str {
123 unsafe {
124 str::from_utf8_unchecked(
125 slice::from_raw_parts(self.0.as_ptr().add(start), len)
126 )
127 }
128 }
129
130 #[inline(always)]
131 pub const fn wmi(&self) -> &str {
135 self.slice(0, 3)
136 }
137
138 #[inline(always)]
139 pub const fn vds(&self) -> &str {
144 self.slice(3, 6)
145 }
146
147 #[inline(always)]
148 pub const fn vic(&self) -> &str {
155 self.slice(9, 8)
156 }
157
158 #[inline(always)]
159 pub const fn manufacturer_region(&self) -> Option<Region> {
161 Region::from_wmi_region(self.0.as_bytes()[0])
162 }
163
164 #[inline(always)]
165 pub const fn manufacturer_country(&self) -> &'static str {
167 dicts::map_wmi_to_country(self.wmi())
168 }
169
170 #[inline(always)]
171 pub const fn manufacturer_name(&self) -> &'static str {
173 dicts::map_wmi_to_manufacturer(self.wmi())
174 }
175}
176
177impl fmt::Debug for Vin<'_> {
178 #[inline(always)]
179 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
180 fmt::Debug::fmt(self.0, fmt)
181 }
182}
183
184impl fmt::Display for Vin<'_> {
185 #[inline(always)]
186 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
187 fmt::Display::fmt(self.0, fmt)
188 }
189}