use super::IcrcError;
pub fn normalize_subaccount_hex(value: &str) -> Result<String, IcrcError> {
Ok(validated_subaccount_hex(value)?.to_ascii_lowercase())
}
#[cfg(feature = "icrc-host")]
pub(in crate::icrc) fn normalize_optional_subaccount_hex(
value: Option<&str>,
) -> Result<Option<String>, IcrcError> {
value.map(normalize_subaccount_hex).transpose()
}
#[cfg(any(test, feature = "icrc-host"))]
pub(in crate::icrc) fn subaccount_bytes_from_hex(value: &str) -> Result<Vec<u8>, IcrcError> {
Ok(validated_subaccount_hex(value)?
.as_bytes()
.as_chunks::<2>()
.0
.iter()
.map(|pair| {
let pair = std::str::from_utf8(pair).expect("validated ASCII hexadecimal");
u8::from_str_radix(pair, 16).expect("validated hexadecimal byte")
})
.collect())
}
fn validated_subaccount_hex(value: &str) -> Result<&str, IcrcError> {
let value = value.trim();
if !value.len().is_multiple_of(2) {
return Err(IcrcError::InvalidSubaccountHex {
reason: "hex string must contain an even number of characters".to_string(),
});
}
if !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
return Err(IcrcError::InvalidSubaccountHex {
reason: "hex string must contain only ASCII hexadecimal digits".to_string(),
});
}
let bytes = value.len() / 2;
if bytes != 32 {
return Err(IcrcError::InvalidSubaccountLength { bytes });
}
Ok(value)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn malformed_subaccounts_return_typed_hex_errors() {
for value in [
format!("aé{}", "0".repeat(61)),
"🦀".repeat(16),
"+a".repeat(32),
"gg".repeat(32),
"a".repeat(63),
] {
assert!(
matches!(
normalize_subaccount_hex(&value),
Err(IcrcError::InvalidSubaccountHex { .. })
),
"malformed subaccount {value:?}",
);
assert!(matches!(
subaccount_bytes_from_hex(&value),
Err(IcrcError::InvalidSubaccountHex { .. })
));
}
}
#[test]
fn subaccounts_normalize_case_and_whitespace_and_require_32_bytes() {
let mixed_case = "aB01".repeat(16);
assert_eq!(
normalize_subaccount_hex(&format!(" \t{mixed_case}\n")).expect("valid subaccount"),
"ab01".repeat(16),
);
assert_eq!(
subaccount_bytes_from_hex(&format!(" \t{mixed_case}\n")).expect("wire bytes"),
[0xab, 0x01].repeat(16),
);
for bytes in [0, 31, 33] {
assert!(matches!(
normalize_subaccount_hex(&"ab".repeat(bytes)),
Err(IcrcError::InvalidSubaccountLength { bytes: actual }) if actual == bytes,
));
assert!(matches!(
subaccount_bytes_from_hex(&"ab".repeat(bytes)),
Err(IcrcError::InvalidSubaccountLength { bytes: actual }) if actual == bytes,
));
}
}
}