1#![doc = include_str!("../README.md")]
4#![no_std]
5
6#[cfg(feature = "alloc")]
7extern crate alloc;
8
9#[cfg(feature = "alloc")]
10mod alloc_;
11#[cfg(feature = "serde")]
12mod serde_;
13
14#[cfg(feature = "serde")]
15use ::serde::{Deserialize, Serialize};
16use core::{
17 borrow::Borrow,
18 fmt::{Display, Formatter, Result as FmtResult},
19 num::ParseIntError,
20 ops::Deref,
21 str::FromStr,
22};
23use ref_cast::{RefCastCustom, ref_cast_custom};
24use thiserror::Error as ThisError;
25
26const LEI_SIZE: usize = 20;
28
29const ISSUER_SIZE: usize = 4;
31
32const LOU_START: usize = 0;
33const LOU_END: usize = LOU_START + ISSUER_SIZE;
34
35const ID_SIZE: usize = 14;
37
38const ID_START: usize = LOU_END;
39const ID_END: usize = ID_START + ID_SIZE;
40
41const CHECKED_SIZE: usize = ISSUER_SIZE + ID_SIZE;
43
44const CHECK_TENS_POS: usize = 18;
46
47const CHECK_ONES_POS: usize = 19;
49
50const fn validate(bytes: &[u8]) -> Result<(), Error> {
51 if bytes.len() != LEI_SIZE {
52 return Err(Error::InvalidLength(bytes.len(), LEI_SIZE));
53 }
54
55 let mut check_str_bytes = [0u8; LEI_SIZE * 2];
56
57 let mut i = 0;
58 let mut check_pos = 0;
59 while i < CHECKED_SIZE {
60 if bytes[i].is_ascii_uppercase() {
61 let checkval = bytes[i] - 55;
62 let tens = checkval / 10;
63 let ones = checkval % 10;
64 check_str_bytes[check_pos] = tens + 48;
65 check_pos += 1;
66 check_str_bytes[check_pos] = ones + 48;
67 check_pos += 1;
68 } else if bytes[i].is_ascii_digit() {
69 check_str_bytes[check_pos] = bytes[i];
70 check_pos += 1;
71 } else {
72 return Err(Error::InvalidCharacter(i));
73 }
74
75 i += 1;
76 }
77
78 check_str_bytes[check_pos] = b'0';
79 check_pos += 1;
80 check_str_bytes[check_pos] = b'0';
81 check_pos += 1;
82
83 let (check_bytes, _trailer) = check_str_bytes.as_slice().split_at(check_pos);
84
85 #[allow(unsafe_code)]
87 let src = unsafe { str::from_utf8_unchecked(check_bytes) };
88
89 let result = u128::from_str_radix(src, 10);
90 if let Ok(check_sum) = result {
91 let check_digits = 98 - (check_sum % 97);
92 if check_digits < 1 || check_digits > 98 {
93 return Err(Error::CheckDigitFail);
94 }
95
96 #[allow(clippy::cast_possible_truncation)]
97 let tens = check_digits as u8 / 10;
98 #[allow(clippy::cast_possible_truncation)]
99 let ones = check_digits as u8 % 10;
100
101 if bytes[CHECK_TENS_POS] != tens + 48 || bytes[CHECK_ONES_POS] != ones + 48 {
102 Err(Error::CheckDigitFail)
103 } else {
104 Ok(())
105 }
106 } else {
107 Err(Error::CheckDigitParse)
108 }
109}
110
111#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, ThisError)]
113#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
114pub enum Error {
115 #[error("The string has the wrong length for an LEI.")]
117 InvalidLength(usize, usize),
118
119 #[error("The string contains an invalid character at {0} for an LEI.")]
121 InvalidCharacter(usize),
122
123 #[error("The check digits string could not be parsed.")]
125 CheckDigitParse,
126
127 #[error("The check digits did not validate.")]
129 CheckDigitFail,
130}
131
132impl From<ParseIntError> for Error {
133 fn from(_value: ParseIntError) -> Self {
134 Self::CheckDigitParse
135 }
136}
137
138#[derive(Debug, Eq, Hash, Ord, PartialEq, PartialOrd, RefCastCustom)]
140#[repr(transparent)]
141#[allow(non_camel_case_types)]
142pub struct lei([u8]);
143
144impl lei {
145 #[ref_cast_custom]
146 pub(crate) const fn ref_cast(bytes: &[u8]) -> &Self;
147
148 pub const fn from_bytes(bytes: &[u8]) -> Result<&Self, Error> {
157 if let Err(e) = validate(bytes) {
158 Err(e)
159 } else {
160 Ok(Self::ref_cast(bytes))
161 }
162 }
163
164 pub const fn from_str_slice(s: &str) -> Result<&Self, Error> {
173 lei::from_bytes(s.as_bytes())
174 }
175
176 #[must_use]
178 pub const fn as_bytes(&self) -> &[u8] {
179 &self.0
180 }
181
182 #[allow(unsafe_code)]
184 #[must_use]
185 pub const fn as_str(&self) -> &str {
186 unsafe { str::from_utf8_unchecked(&self.0) }
189 }
190
191 #[must_use]
193 #[expect(
194 clippy::missing_panics_doc,
195 reason = "Invariants failure in check digit validation"
196 )]
197 pub const fn split(&self) -> (&str, &str, u8) {
198 let whole = self.as_str();
199
200 let (issuer, remainder) = whole.split_at(LOU_END);
201 let (id, check_digits) = remainder.split_at(ID_END);
202
203 if let Ok(val) = u8::from_str_radix(check_digits, 10) {
204 (issuer, id, val)
205 } else {
206 panic!("Unparseable check digits somehow passed validation.");
207 }
208 }
209
210 #[must_use]
212 pub const fn lou(&self) -> &str {
213 let (issuer, _remainder) = self.as_str().split_at(LOU_END);
214 issuer
215 }
216
217 #[must_use]
219 pub const fn id(&self) -> &str {
220 let (_issuer, remainder) = self.as_str().split_at(LOU_END);
221 let (id, _remainder) = remainder.split_at(ID_END);
222 id
223 }
224
225 #[must_use]
227 pub const fn check_digits(&self) -> u8 {
228 self.split().2
229 }
230}
231
232impl AsRef<[u8]> for lei {
233 fn as_ref(&self) -> &[u8] {
234 self.as_bytes()
235 }
236}
237
238impl AsRef<str> for lei {
239 fn as_ref(&self) -> &str {
240 self.as_str()
241 }
242}
243
244impl Display for lei {
245 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
246 write!(f, "{}", self.as_str())
247 }
248}
249
250#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
252#[repr(transparent)]
253pub struct Lei([u8; LEI_SIZE]);
254
255impl Lei {
256 #[must_use]
258 pub const fn from_lei(src: &lei) -> Self {
259 Self::from_bytes_unchecked(src.as_bytes())
260 }
261
262 pub const fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
284 if let Err(e) = validate(bytes) {
285 Err(e)
286 } else {
287 Ok(Self::from_bytes_unchecked(bytes))
288 }
289 }
290
291 pub const fn from_byte_array(bytes: [u8; LEI_SIZE]) -> Result<Self, Error> {
311 if let Err(e) = validate(&bytes) {
312 Err(e)
313 } else {
314 Ok(Self(bytes))
315 }
316 }
317
318 pub const fn from_str_slice(src: &str) -> Result<Self, Error> {
338 Self::from_bytes(src.as_bytes())
339 }
340
341 pub(crate) const fn from_bytes_unchecked(slice: &[u8]) -> Self {
343 let mut bytes = [0u8; LEI_SIZE];
344 bytes.copy_from_slice(slice);
345
346 Self(bytes)
347 }
348}
349
350impl Borrow<lei> for Lei {
351 fn borrow(&self) -> &lei {
352 self
353 }
354}
355
356impl Deref for Lei {
357 type Target = lei;
358
359 fn deref(&self) -> &Self::Target {
360 lei::ref_cast(&self.0)
361 }
362}
363
364impl Display for Lei {
365 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
366 write!(f, "{}", self.as_str())
367 }
368}
369
370impl From<&lei> for Lei {
371 fn from(value: &lei) -> Self {
372 Lei::from_lei(value)
373 }
374}
375
376impl TryFrom<[u8; LEI_SIZE]> for Lei {
377 type Error = Error;
378
379 fn try_from(bytes: [u8; LEI_SIZE]) -> Result<Self, Self::Error> {
380 Self::from_byte_array(bytes)
381 }
382}
383
384impl TryFrom<&[u8]> for Lei {
385 type Error = Error;
386
387 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
388 Self::from_bytes(bytes)
389 }
390}
391
392impl FromStr for Lei {
393 type Err = Error;
394
395 fn from_str(s: &str) -> Result<Self, Self::Err> {
396 Self::from_str_slice(s)
397 }
398}
399
400#[cfg(test)]
401mod test {
402 use super::*;
403 use alloc::borrow::ToOwned;
404 use core::{borrow::Borrow, str::FromStr};
405
406 #[yare::parameterized(
407 ok_1 = { "YZ83GD8L7GG84979J516", None },
408 poo = { "YZ83GD8L7GG849💩16", Some(Error::InvalidCharacter(14)) },
409 bad_check_1 = { "YZ83GD8L7GG84979J563", Some(Error::CheckDigitFail) },
410 bad_check_2 = { "315700K7NYVSQJNTN401", Some(Error::CheckDigitFail) },
411 missing_check = { "315700K7NYVSQJNTN4", Some(Error::InvalidLength(18, LEI_SIZE)) },
412 blank = { "", Some(Error::InvalidLength(0, LEI_SIZE)) },
413 )]
414 fn check(s: &str, err: Option<Error>) {
415 let result = lei::from_str_slice(s);
416 assert_eq!(err, result.err());
417
418 if let Ok(l) = result {
419 let owned = Lei::from_str(s).expect("Could not parse as owned?");
420 assert_eq!(l.to_owned(), owned);
421 assert_eq!(<Lei as Borrow<lei>>::borrow(&owned), l);
422 }
423 }
424}