#![no_std]
#![warn(missing_docs)]
#![cfg_attr(feature = "cargo-clippy", allow(clippy::style))]
use core::{fmt, str, slice, char};
#[cfg(debug_assertions)]
macro_rules! unreach {
() => {
unreachable!()
}
}
#[cfg(not(debug_assertions))]
macro_rules! unreach {
() => {
unsafe {
core::hint::unreachable_unchecked()
}
}
}
mod dicts;
pub use dicts::Region;
const VIN_LEN: usize = 17;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum VinError {
InvalidLen,
InvalidChar(usize, char),
}
impl fmt::Display for VinError {
#[inline(always)]
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidLen => fmt.write_str("VIN MUST be 17 characters long"),
Self::InvalidChar(idx, ch) => fmt.write_fmt(format_args!("VIN contains invalid character '{ch}' at idx={idx}")),
}
}
}
#[repr(transparent)]
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct Vin<'a>(&'a str);
impl<'a> Vin<'a> {
pub const fn new(vin: &'a str) -> Self {
match Self::try_new(vin) {
Ok(this) => this,
Err(VinError::InvalidLen) => panic!("Invalid length of VIN"),
Err(VinError::InvalidChar(_, _)) => panic!("VIN contains invalid character"),
}
}
pub const fn try_new(vin: &'a str) -> Result<Self, VinError> {
if vin.len() != VIN_LEN {
return Err(VinError::InvalidLen);
}
let mut idx = 0;
while idx < vin.len() {
let ch = vin.as_bytes()[idx];
match ch as char {
'I' | 'O' | 'Q' => return Err(VinError::InvalidChar(idx, ch as char)),
'A'..='Z' | '0'..='9' => idx += 1,
ch => return Err(VinError::InvalidChar(idx, ch)),
}
}
Ok(Self(vin))
}
pub const fn calculate_checksum(&self) -> u32 {
let mut result = 0u32;
let mut idx = 0;
while idx < self.0.len() {
let ch = self.0.as_bytes()[idx];
result = result.wrapping_add(dicts::vin_char_weight(ch as char) * dicts::WEIGHTS[idx]);
idx += 1;
}
result
}
pub const fn calculate_checksum_digit(&self) -> char {
let checksum = self.calculate_checksum();
match checksum % 11 {
10 => 'X',
digit => match char::from_digit(digit, 10) {
Some(ch) => ch,
None => unreach!(),
}
}
}
#[inline(always)]
pub const fn is_checksum_valid(&self) -> bool {
let expected = self.calculate_checksum_digit();
self.0.as_bytes()[8] as char == expected
}
const fn slice(&self, start: usize, len: usize) -> &str {
unsafe {
str::from_utf8_unchecked(
slice::from_raw_parts(self.0.as_ptr().add(start), len)
)
}
}
#[inline(always)]
pub const fn wmi(&self) -> &str {
self.slice(0, 3)
}
#[inline(always)]
pub const fn vds(&self) -> &str {
self.slice(3, 6)
}
#[inline(always)]
pub const fn vic(&self) -> &str {
self.slice(9, 8)
}
#[inline(always)]
pub const fn manufacturer_region(&self) -> Option<Region> {
Region::from_wmi_region(self.0.as_bytes()[0])
}
#[inline(always)]
pub const fn manufacturer_country(&self) -> &'static str {
dicts::map_wmi_to_country(self.wmi())
}
#[inline(always)]
pub const fn manufacturer_name(&self) -> &'static str {
dicts::map_wmi_to_manufacturer(self.wmi())
}
}
impl fmt::Debug for Vin<'_> {
#[inline(always)]
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(self.0, fmt)
}
}
impl fmt::Display for Vin<'_> {
#[inline(always)]
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self.0, fmt)
}
}