#![warn(missing_docs)]
use std::fmt;
use std::str::FromStr;
use bstr::ByteSlice;
use iso_iec_7064::{System, MOD_97_10};
pub mod error;
pub use error::LEIError;
mod digits;
use digits::DigitsIterator;
fn compute_check_digits(s: &[u8]) -> [u8; 2] {
let it = DigitsIterator::new(s);
match MOD_97_10.checksum_ascii_bytes_iter(it) {
Some(sum) => {
let d1 = b'0' + (sum / 10) as u8;
let d0 = b'0' + (sum % 10) as u8;
let r: [u8; 2] = [d1, d0];
r
}
None => {
panic!("MOD_97_10::checksum() failed to produce a checksum! Invalid input characters?")
}
}
}
fn validate_lou_id_format(li: &[u8]) -> Result<(), LEIError> {
if li.len() != 4 {
panic!("Expected 4 bytes for LOU ID, but got {}", li.len());
}
for b in li {
if !(b.is_ascii_digit() || (b.is_ascii_alphabetic() && b.is_ascii_uppercase())) {
let mut li_copy: [u8; 4] = [0; 4];
li_copy.copy_from_slice(li);
return Err(LEIError::InvalidLouId { was: li_copy });
}
}
Ok(())
}
fn validate_entity_id_format(ei: &[u8]) -> Result<(), LEIError> {
if ei.len() != 14 {
panic!("Expected 14 bytes for Entity ID, but got {}", ei.len());
}
for b in ei {
if !(b.is_ascii_digit() || (b.is_ascii_alphabetic() && b.is_ascii_uppercase())) {
let mut ei_copy: [u8; 14] = [0; 14];
ei_copy.copy_from_slice(ei);
return Err(LEIError::InvalidEntityId { was: ei_copy });
}
}
Ok(())
}
fn validate_check_digits_format(cd: &[u8]) -> Result<(), LEIError> {
if cd.len() != 2 {
panic!("Expected 2 bytes for Check Digits, but got {}", cd.len());
}
for b in cd {
if !(b.is_ascii_digit()) {
let mut cd_copy: [u8; 2] = [0; 2];
cd_copy.copy_from_slice(cd);
return Err(LEIError::InvalidCheckDigits { was: cd_copy });
}
}
Ok(())
}
pub fn parse(value: &str) -> Result<LEI, LEIError> {
let v: String = value.into();
if v.len() != 20 {
return Err(LEIError::InvalidLength { was: v.len() });
}
let b = v.as_bytes();
let lou_id: &[u8] = &b[0..4];
validate_lou_id_format(lou_id)?;
let entity_id: &[u8] = &b[4..18];
validate_entity_id_format(entity_id)?;
let check_digits = &b[18..20];
validate_check_digits_format(check_digits)?;
let payload = &b[0..18];
let computed_check_digits = compute_check_digits(payload);
let incorrect_check_digits = check_digits != computed_check_digits;
if incorrect_check_digits {
let mut cd_copy: [u8; 2] = [0; 2];
cd_copy.copy_from_slice(check_digits);
return Err(LEIError::IncorrectCheckDigits {
was: cd_copy,
expected: computed_check_digits,
});
}
let mut bb = [0u8; 20];
bb.copy_from_slice(b);
Ok(LEI(bb))
}
pub fn parse_loose(value: &str) -> Result<LEI, LEIError> {
let uc = value.to_ascii_uppercase();
let temp = uc.trim();
parse(temp)
}
pub fn build_from_payload(payload: &str) -> Result<LEI, LEIError> {
if payload.len() != 18 {
return Err(LEIError::InvalidPayloadLength { was: payload.len() });
}
let b = &payload.as_bytes()[0..18];
let lou_id = &b[0..4];
validate_lou_id_format(lou_id)?;
let entity_id = &b[4..18];
validate_entity_id_format(entity_id)?;
let mut bb = [0u8; 20];
bb[0..18].copy_from_slice(b);
let temp = compute_check_digits(b);
bb[18..20].copy_from_slice(&temp);
Ok(LEI(bb))
}
pub fn build_from_parts(lou_id: &str, entity_id: &str) -> Result<LEI, LEIError> {
if lou_id.len() != 4 {
return Err(LEIError::InvalidLouIdLength { was: lou_id.len() });
}
let lou_id: &[u8] = &lou_id.as_bytes()[0..4];
validate_lou_id_format(lou_id)?;
if entity_id.len() != 14 {
return Err(LEIError::InvalidEntityIdLength {
was: entity_id.len(),
});
}
let entity_id: &[u8] = &entity_id.as_bytes()[0..14];
validate_entity_id_format(entity_id)?;
let mut bb = [0u8; 20];
bb[0..4].copy_from_slice(lou_id);
bb[4..18].copy_from_slice(entity_id);
let temp = compute_check_digits(&bb[0..18]);
bb[18..20].copy_from_slice(&temp);
Ok(LEI(bb))
}
pub fn validate(value: &str) -> bool {
if value.len() != 20 {
return false;
}
let b = value.as_bytes();
let lou_id: &[u8] = &b[0..4];
if validate_lou_id_format(lou_id).is_err() {
return false;
}
let entity_id: &[u8] = &b[4..18];
if validate_entity_id_format(entity_id).is_err() {
return false;
}
let check_digits = &b[18..20];
if validate_check_digits_format(check_digits).is_err() {
return false;
}
let payload = &b[0..18];
let computed_check_digits = compute_check_digits(payload);
if check_digits[0] != computed_check_digits[0] {
return false;
}
if check_digits[1] != computed_check_digits[1] {
return false;
}
true
}
#[doc = include_str!("../README.md")]
#[cfg(doctest)]
pub struct ReadmeDoctests;
#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Hash)]
#[repr(transparent)]
#[allow(clippy::upper_case_acronyms)]
pub struct LEI([u8; 20]);
impl fmt::Display for LEI {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let temp = unsafe { self.as_bytes().to_str_unchecked() }; write!(f, "{}", temp)
}
}
impl fmt::Debug for LEI {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let temp = unsafe { self.as_bytes().to_str_unchecked() }; write!(f, "LEI({})", temp)
}
}
impl FromStr for LEI {
type Err = LEIError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
parse_loose(s)
}
}
impl LEI {
fn as_bytes(&self) -> &[u8] {
&self.0[..]
}
pub fn lou_id(&self) -> &str {
unsafe { self.0[0..4].to_str_unchecked() } }
pub fn entity_id(&self) -> &str {
unsafe { self.0[4..18].to_str_unchecked() } }
pub fn payload(&self) -> &str {
unsafe { self.0[0..18].to_str_unchecked() } }
pub fn check_digits(&self) -> &str {
unsafe { self.0[18..20].to_str_unchecked() } }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_digits() {
let payload = "635400B4JJBON4TCHF";
let cd = compute_check_digits(payload.as_bytes());
assert_eq!(cd[0], 48); assert_eq!(cd[1], 50); }
#[test]
fn parse_bulk() {
let cases = [
"635400B4JJBON4TCHF02",
"529900ODI3047E2LIV03",
"5493002F3N6V3Z14SP04",
"549300IYKILIU506KA05",
"JJKC32MCHWDI71265Z06",
"549300RIPPWJB5Z0FK07",
"Z2VZBHUMB7PWWJ63I008",
"FRQ78DFDYWMT3XY6UR09",
"337KMNHEWWWR6B7Q7W10",
"549300E9PC51EN656011",
"5493003WHB7TFLYQFS12",
"549300C04BJ0G297NC13",
"T68X8LLAQYRNDV034K14",
"8HWWA59ZS6Z54QLX6S15",
"54930018SOOHBHRLWC16",
"95980020140005346817",
"549300HMMEWVG3PPQU18",
"5JQ7W3GWO8J5DAE5WR19",
"AJ6VL0Z1WDC42KKJZO20",
];
for case in cases.iter() {
let parsed = parse(case).unwrap();
assert_eq!(
*case,
parsed.to_string(),
"Successfully parsed {:?} but to_string() didn't match input!",
case
);
let is_valid = validate(case);
assert_eq!(
true, is_valid,
"Successfully parsed {:?} but got false from validate()!",
case
);
}
}
#[test]
fn parse_bulk_bad() {
let cases = [
"31570010000000045200",
"3157006B6JVZ5DFMSN00",
"315700BBRQHDWX6SHZ00",
"315700G5G24XYL1TXH00",
"31570010000000048401",
"31570010000000067801",
"315700WH3YMKHCVYW201",
];
for case in cases.iter() {
let parsed = parse(case).unwrap();
assert_eq!(
*case,
parsed.to_string(),
"Successfully parsed {:?} but to_string() didn't match input!",
case
);
let is_valid = validate(case);
assert_eq!(
true, is_valid,
"Successfully parsed {:?} but got false from validate()!",
case
);
}
}
}