#![no_std]
#![deny(missing_docs)]
use core::fmt::{self, Display, Formatter};
use core::str::{FromStr, from_utf8_unchecked};
#[cfg(feature = "alloc")]
extern crate alloc;
#[cfg(feature = "alloc")]
use alloc::string::String;
#[cfg(feature = "bitcode")]
use bitcode::{Decode, Encode};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
#[cfg_attr(feature = "bitcode", derive(Encode, Decode))]
pub struct Isrc {
agency_prefix: [u8; 2],
registrant_prefix: u16,
rest: u32,
}
#[test]
fn test_isrc_size() {
assert_eq!(size_of::<Isrc>(), 8);
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Error)]
#[cfg_attr(feature = "bitcode", derive(Encode, Decode))]
pub enum IsrcParseError {
#[error("Invalid length (expected 12B input, found {found}B)")]
InvalidLength {
found: usize,
},
#[error(r"Invalid agency prefix (expected [a-zA-Z], found '\x{found:x}' at {pos})")]
InvalidAgencyPrefix {
found: u8,
pos: u8,
},
#[error(r"Invalid registrant prefix (expected [a-zA-Z0-9], found '\x{found:x}' at {pos})")]
InvalidRegistrantPrefix {
found: u8,
pos: u8,
},
#[error(r"Registrant prefix out of range (expected 0 <= value < 36*36*36, found {value})")]
RegistrantPrefixOutOfRange {
value: u16,
},
#[error(r"Invalid digit (expected [0-9], found '\x{found:x}' at {pos})")]
InvalidDigit {
found: u8,
pos: u8,
},
#[error(r"Rest value out of range (expected 0 <= value < 10000000, found {value})")]
ValueOutOfRange {
value: u32,
},
}
impl Isrc {
pub const fn from_code(code: &str) -> Result<Self, IsrcParseError> {
let code = code.as_bytes();
if code.len() != 12 {
return Err(IsrcParseError::InvalidLength { found: code.len() });
}
macro_rules! agency {
($pos:expr) => {
match code[$pos] {
b'a'..=b'z' => code[$pos] ^ 0b0010_0000, b'A'..=b'Z' => code[$pos],
_ => {
return Err(IsrcParseError::InvalidAgencyPrefix {
found: code[$pos],
pos: $pos,
})
}
}
};
}
macro_rules! registrant {
($pos:expr) => {
match code[$pos] {
b'0'..=b'9' => code[$pos] - b'0',
b'a'..=b'z' => code[$pos] - b'a' + 10,
b'A'..=b'Z' => code[$pos] - b'A' + 10,
_ => return Err(IsrcParseError::InvalidRegistrantPrefix { found: code[$pos], pos: $pos }),
} as u16
};
}
macro_rules! d {
($pos:expr) => {
match code[$pos] {
b'0'..=b'9' => code[$pos] - b'0',
_ => return Err(IsrcParseError::InvalidDigit { found: code[$pos], pos: $pos }),
} as u32
};
}
Ok(Isrc {
agency_prefix: [agency!(0), agency!(1)],
registrant_prefix: registrant!(2) * 36 * 36 + registrant!(3) * 36 + registrant!(4),
rest: d!(5) * 1_000_000
+ d!(6) * 100_000
+ d!(7) * 10_000
+ d!(8) * 1_000
+ d!(9) * 100
+ d!(10) * 10
+ d!(11),
})
}
#[cfg(feature = "alloc")]
pub fn to_code(&self) -> String {
let mut n = self.registrant_prefix;
let d2 = ascii_uppercase_from_digit_base36(n) as char;
n /= 36;
let d1 = ascii_uppercase_from_digit_base36(n) as char;
n /= 36;
let d0 = ascii_uppercase_from_digit_base36(n) as char;
alloc::format!(
"{}{}{}{}{:07}",
unsafe { from_utf8_unchecked(&self.agency_prefix) },
d0,
d1,
d2,
self.rest,
)
}
pub const fn to_code_fixed(&self) -> [u8; 12] {
use core::mem::MaybeUninit;
let mut ret = [const { MaybeUninit::<u8>::uninit() }; 12];
let mut n = self.rest;
ret[11].write(b'0' + (n % 10) as u8);
n /= 10;
ret[10].write(b'0' + (n % 10) as u8);
n /= 10;
ret[9].write(b'0' + (n % 10) as u8);
n /= 10;
ret[8].write(b'0' + (n % 10) as u8);
n /= 10;
ret[7].write(b'0' + (n % 10) as u8);
n /= 10;
ret[6].write(b'0' + (n % 10) as u8);
n /= 10;
ret[5].write(b'0' + (n % 10) as u8);
let mut n = self.registrant_prefix;
ret[4].write(ascii_uppercase_from_digit_base36(n));
n /= 36;
ret[3].write(ascii_uppercase_from_digit_base36(n));
n /= 36;
ret[2].write(ascii_uppercase_from_digit_base36(n));
ret[1].write(self.agency_prefix[1]);
ret[0].write(self.agency_prefix[0]);
unsafe { core::mem::transmute::<_, [u8; 12]>(ret) }
}
pub const fn from_bytes(bytes: &[u8; 8]) -> Result<Self, IsrcParseError> {
let rest = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
let agency_prefix = [bytes[4], bytes[5]];
let registrant_prefix = u16::from_le_bytes([bytes[6], bytes[7]]);
if !agency_prefix[0].is_ascii_uppercase() {
return Err(IsrcParseError::InvalidAgencyPrefix {
found: agency_prefix[0],
pos: 4,
});
}
if !agency_prefix[1].is_ascii_uppercase() {
return Err(IsrcParseError::InvalidAgencyPrefix {
found: agency_prefix[1],
pos: 5,
});
}
if registrant_prefix >= 36 * 36 * 36 {
return Err(IsrcParseError::RegistrantPrefixOutOfRange {
value: registrant_prefix,
});
}
if rest >= 10_000_000 {
return Err(IsrcParseError::ValueOutOfRange { value: rest });
}
Ok(Isrc {
agency_prefix,
registrant_prefix,
rest,
})
}
pub const fn to_bytes(&self) -> [u8; 8] {
[
self.rest as u8,
(self.rest >> 8) as u8,
(self.rest >> 16) as u8,
(self.rest >> 24) as u8,
self.agency_prefix[0],
self.agency_prefix[1],
self.registrant_prefix as u8,
(self.registrant_prefix >> 8) as u8,
]
}
}
#[test]
fn test_isrc() -> Result<(), IsrcParseError> {
let isrc = Isrc::from_code("AA6Q72000047")?;
assert_eq!(
isrc,
Isrc {
agency_prefix: [b'A', b'A'],
registrant_prefix: 8719,
rest: 20_00047,
}
);
#[cfg(feature = "alloc")]
assert_eq!(isrc.to_code(), "AA6Q72000047");
assert_eq!(isrc.to_code_fixed(), *b"AA6Q72000047");
let isrc = Isrc::from_code("aa6q72000047")?;
assert_eq!(
isrc,
Isrc {
agency_prefix: [b'A', b'A'],
registrant_prefix: 8719,
rest: 20_00047,
}
);
#[cfg(feature = "alloc")]
assert_eq!(isrc.to_code(), "AA6Q72000047");
assert_eq!(isrc.to_code_fixed(), *b"AA6Q72000047");
assert_eq!(
Isrc::from_code("00A6Q7200047"),
Err(IsrcParseError::InvalidAgencyPrefix {
found: b'0',
pos: 0
})
);
assert_eq!(
Isrc::from_code("AA-6Q7200047"),
Err(IsrcParseError::InvalidRegistrantPrefix {
found: b'-',
pos: 2
})
);
assert_eq!(
Isrc::from_code("aa6q7200047\n"),
Err(IsrcParseError::InvalidDigit {
found: b'\n',
pos: 11
})
);
Ok(())
}
#[test]
fn test_isrc_from_bytes() -> Result<(), IsrcParseError> {
let isrc = Isrc::from_bytes(&[0xB1, 0xCB, 0x74, 0x00, 0x5A, 0x5A, 0x0F, 0x22])?;
assert_eq!(
isrc,
Isrc {
agency_prefix: [b'Z', b'Z'],
registrant_prefix: 8719,
rest: 76_54321,
}
);
#[cfg(feature = "alloc")]
assert_eq!(isrc.to_code(), "ZZ6Q77654321");
assert_eq!(isrc.to_code_fixed(), *b"ZZ6Q77654321");
let fail = Isrc::from_bytes(&[0xB1, 0xCB, 0x74, 0x00, 0x5A, 0x00, 0x0F, 0x22]);
assert_eq!(
fail,
Err(IsrcParseError::InvalidAgencyPrefix {
found: 0x00,
pos: 5
})
);
let fail = Isrc::from_bytes(&[0xB1, 0xCB, 0x74, 0x00, 0x5A, 0x5A, 0xFF, 0xFF]);
assert_eq!(
fail,
Err(IsrcParseError::RegistrantPrefixOutOfRange { value: 0xFFFF })
);
let fail = Isrc::from_bytes(&[0xFF, 0xFF, 0xFF, 0xFF, 0x5A, 0x5A, 0x0F, 0x22]);
assert_eq!(
fail,
Err(IsrcParseError::ValueOutOfRange { value: 0xFFFFFFFF })
);
Ok(())
}
#[test]
fn test_isrc_to_bytes() {
assert_eq!(
Isrc {
agency_prefix: [b'A', b'Z'],
registrant_prefix: 8719,
rest: 76_54321,
}
.to_bytes(),
[0xB1, 0xCB, 0x74, 0x00, b'A', b'Z', 0x0F, 0x22]
);
}
impl FromStr for Isrc {
type Err = IsrcParseError;
fn from_str(code: &str) -> Result<Self, IsrcParseError> {
Isrc::from_code(code)
}
}
#[test]
fn test_isrc_from_str() -> Result<(), IsrcParseError> {
let isrc: Isrc = "AA6Q72000047".parse()?;
assert_eq!(
isrc,
Isrc {
agency_prefix: [b'A', b'A'],
registrant_prefix: 8719,
rest: 20_00047,
}
);
#[cfg(feature = "alloc")]
assert_eq!(isrc.to_code(), "AA6Q72000047");
assert_eq!(isrc.to_code_fixed(), *b"AA6Q72000047");
let isrc: Isrc = "aa6q72000047".parse()?;
assert_eq!(
isrc,
Isrc {
agency_prefix: [b'A', b'A'],
registrant_prefix: 8719,
rest: 20_00047,
}
);
#[cfg(feature = "alloc")]
assert_eq!(isrc.to_code(), "AA6Q72000047");
assert_eq!(isrc.to_code_fixed(), *b"AA6Q72000047");
assert_eq!(
"00A6Q7200047".parse::<Isrc>(),
Err(IsrcParseError::InvalidAgencyPrefix {
found: b'0',
pos: 0
})
);
assert_eq!(
"AA6Q7200047".parse::<Isrc>(),
Err(IsrcParseError::InvalidLength { found: 11 })
);
assert_eq!(
"AA6Q7200047910".parse::<Isrc>(),
Err(IsrcParseError::InvalidLength { found: 14 })
);
Ok(())
}
impl Display for Isrc {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let mut n = self.registrant_prefix;
let d2 = ascii_uppercase_from_digit_base36(n % 36) as char;
n /= 36;
let d1 = ascii_uppercase_from_digit_base36(n % 36) as char;
n /= 36;
let d0 = ascii_uppercase_from_digit_base36(n % 36) as char;
write!(
f,
"ISRC {}-{}{}{}-{:02}-{:05}",
unsafe { from_utf8_unchecked(&self.agency_prefix) },
d0,
d1,
d2,
self.rest / 100_000,
self.rest % 100_000,
)
}
}
#[test]
fn test_isrc_display() -> anyhow::Result<()> {
use core::fmt::Write;
let mut buffer = heapless::String::<32>::new();
let isrc = Isrc::from_code("AA6Q72000047")?;
write!(&mut buffer, "{}", isrc)?;
assert_eq!(buffer, "ISRC AA-6Q7-20-00047");
let mut buffer = heapless::String::<20>::new();
let isrc = Isrc::from_code("Zz6q72412345")?;
write!(&mut buffer, "{}", isrc)?;
assert_eq!(buffer, "ISRC ZZ-6Q7-24-12345");
Ok(())
}
#[cfg(feature = "serde")]
impl Serialize for Isrc {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use core::str::from_utf8_unchecked;
if serializer.is_human_readable() {
let code = self.to_code_fixed();
unsafe { from_utf8_unchecked(&code) }.serialize(serializer)
} else {
self.to_bytes().serialize(serializer)
}
}
}
#[test]
#[cfg(feature = "serde")]
fn test_isrc_serialize() -> anyhow::Result<()> {
let isrc = Isrc::from_code("AA6Q72000047")?;
assert_eq!(serde_json::to_string(&isrc)?, r#""AA6Q72000047""#);
#[cfg(feature = "alloc")]
{
use alloc::collections::BTreeMap;
let table: BTreeMap<&str, Isrc> = BTreeMap::from_iter([("isrc", isrc)]);
assert_eq!(
toml::to_string(&table)?,
r#"isrc = "AA6Q72000047"
"#
);
}
assert_eq!(
bincode::serialize(&isrc)?,
b"\xAF\x84\x1E\x00\x41\x41\x0f\x22"
);
Ok(())
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for Isrc {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
if deserializer.is_human_readable() {
Isrc::from_code(&heapless::String::<12>::deserialize(deserializer)?)
} else {
Isrc::from_bytes(&<[u8; 8]>::deserialize(deserializer)?)
}
.map_err(serde::de::Error::custom)
}
}
#[test]
#[cfg(feature = "serde")]
fn test_isrc_deserialize() -> anyhow::Result<()> {
#[derive(Debug, Deserialize)]
struct TestInput {
isrc: Isrc,
}
let v: TestInput = serde_json::from_str(r#"{"isrc":"AA6Q72000047"}"#)?;
assert_eq!(
v.isrc,
Isrc {
agency_prefix: [b'A', b'A'],
registrant_prefix: 8719,
rest: 20_00047,
}
);
let v: TestInput = toml::from_str(r#"isrc = "AA6Q72000047""#)?;
assert_eq!(
v.isrc,
Isrc {
agency_prefix: [b'A', b'A'],
registrant_prefix: 8719,
rest: 20_00047,
}
);
let v: TestInput = bincode::deserialize(b"\xAF\x84\x1E\x00\x41\x41\x0f\x22")?;
assert_eq!(
v.isrc,
Isrc {
agency_prefix: [b'A', b'A'],
registrant_prefix: 8719,
rest: 20_00047,
}
);
let r: Result<TestInput, _> = serde_json::from_str(r#"{"isrc":"AA6Q72000047777777"}"#);
let Err(serde_json::Error { .. }) = r else {
panic!("Expected error, but got: {r:?}");
};
let r: Result<TestInput, _> = toml::from_str(r#"isrc = "AA6Q72000""#);
let Err(toml::de::Error { .. }) = r else {
panic!("Expected error, but got: {r:?}");
};
let r: Result<TestInput, _> = bincode::deserialize(b"\xAF\x84\x00\x41\x41\x0f\x22");
let Err(bincode::Error { .. }) = r else {
panic!("Expected error, but got: {r:?}");
};
Ok(())
}
const fn ascii_uppercase_from_digit_base36(d: u16) -> u8 {
let d = (d % 36) as u8;
match d {
0..=9 => d + b'0',
10..=35 => d + b'A' - 10,
_ => unreachable!(),
}
}