use thiserror::Error;
pub use crate::{hex_decode, hex_encode};
use crate::dotns::scale_compact_len;
#[derive(Debug, Error, PartialEq)]
pub enum IdentityError {
#[error("username is too long: {len} bytes (max 32)")]
UsernameTooLong { len: usize },
#[error("username must not be empty")]
UsernameEmpty,
#[error("username contains invalid character: {ch:?}")]
InvalidCharacter { ch: char },
#[error("username has invalid dot placement: {reason}")]
InvalidDotPlacement { reason: &'static str },
#[error("invalid response: {msg}")]
InvalidResponse { msg: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConsumerInfo {
pub identifier_key: Vec<u8>,
pub full_username: Option<String>,
pub lite_username: String,
pub credibility: Credibility,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Credibility {
Lite,
Person {
alias: [u8; 32],
last_update: u64,
},
}
pub fn normalize_username(username: &str) -> Result<Vec<u8>, IdentityError> {
if username.is_empty() {
return Err(IdentityError::UsernameEmpty);
}
if username.contains('\0') {
return Err(IdentityError::InvalidCharacter { ch: '\0' });
}
let lower = username.to_ascii_lowercase();
let bytes = lower.as_bytes();
for &b in bytes {
let ch = b as char;
if !matches!(b, b'a'..=b'z' | b'0'..=b'9' | b'.') {
return Err(IdentityError::InvalidCharacter { ch });
}
}
if bytes.len() > 32 {
return Err(IdentityError::UsernameTooLong { len: bytes.len() });
}
if bytes.first() == Some(&b'.') {
return Err(IdentityError::InvalidDotPlacement {
reason: "leading dot",
});
}
if bytes.last() == Some(&b'.') {
return Err(IdentityError::InvalidDotPlacement {
reason: "trailing dot",
});
}
if bytes.windows(2).any(|w| w == b"..") {
return Err(IdentityError::InvalidDotPlacement {
reason: "consecutive dots",
});
}
Ok(bytes.to_vec())
}
pub fn username_info_of_key(username_bytes: &[u8]) -> Vec<u8> {
let pallet_hash = twox_128(b"Identity");
let storage_hash = twox_128(b"UsernameInfoOf");
let mut encoded_username = Vec::with_capacity(username_bytes.len() + 4);
scale_compact_len(&mut encoded_username, username_bytes.len())
.expect("username_bytes.len() <= 32 is always within compact single-byte range");
encoded_username.extend_from_slice(username_bytes);
let hash_prefix = blake2_128(&encoded_username);
let mut key = Vec::with_capacity(16 + 16 + 16 + encoded_username.len());
key.extend_from_slice(&pallet_hash);
key.extend_from_slice(&storage_hash);
key.extend_from_slice(&hash_prefix);
key.extend_from_slice(&encoded_username);
key
}
pub fn username_owner_of_key(username_bytes: &[u8]) -> Vec<u8> {
let pallet_hash = twox_128(b"Resources");
let storage_hash = twox_128(b"UsernameOwnerOf");
let mut encoded_username = Vec::with_capacity(username_bytes.len() + 4);
scale_compact_len(&mut encoded_username, username_bytes.len())
.expect("username_bytes.len() <= 32 is always within compact single-byte range");
encoded_username.extend_from_slice(username_bytes);
let hash_prefix = blake2_128(&encoded_username);
let mut key = Vec::with_capacity(16 + 16 + 16 + encoded_username.len());
key.extend_from_slice(&pallet_hash);
key.extend_from_slice(&storage_hash);
key.extend_from_slice(&hash_prefix);
key.extend_from_slice(&encoded_username);
key
}
pub fn decode_username_owner(data: &[u8]) -> Result<Option<[u8; 32]>, IdentityError> {
if data.is_empty() {
return Ok(None);
}
if data.len() < 32 {
return Err(IdentityError::InvalidResponse {
msg: format!(
"UsernameOwnerOf truncated: expected 32 bytes, got {}",
data.len()
),
});
}
let mut account = [0u8; 32];
account.copy_from_slice(&data[..32]);
Ok(Some(account))
}
pub fn consumers_key(account_id: &[u8; 32]) -> Vec<u8> {
let pallet_hash = twox_128(b"Resources");
let storage_hash = twox_128(b"Consumers");
let hash_prefix = blake2_128(account_id);
let mut key = Vec::with_capacity(16 + 16 + 16 + 32);
key.extend_from_slice(&pallet_hash);
key.extend_from_slice(&storage_hash);
key.extend_from_slice(&hash_prefix);
key.extend_from_slice(account_id);
key
}
pub fn decode_option_account_id(data: &[u8]) -> Result<Option<[u8; 32]>, IdentityError> {
if data.is_empty() {
return Ok(None);
}
match data[0] {
0x00 => {
if data.len() != 1 {
return Err(IdentityError::InvalidResponse {
msg: format!(
"Option::None tag followed by {} unexpected byte(s)",
data.len() - 1
),
});
}
Ok(None)
}
0x01 => {
let payload = &data[1..];
if payload.len() < 32 {
return Err(IdentityError::InvalidResponse {
msg: format!(
"Option::Some truncated: expected 32 bytes, got {}",
payload.len()
),
});
}
if payload.len() > 32 {
return Err(IdentityError::InvalidResponse {
msg: format!(
"Option::Some has {} trailing byte(s) after AccountId32",
payload.len() - 32
),
});
}
let mut account = [0u8; 32];
account.copy_from_slice(payload);
Ok(Some(account))
}
tag => Err(IdentityError::InvalidResponse {
msg: format!("unknown Option tag byte: 0x{tag:02x}"),
}),
}
}
pub fn decode_username_info_owner(data: &[u8]) -> Result<Option<[u8; 32]>, IdentityError> {
if data.is_empty() {
return Ok(None);
}
if data.len() < 32 {
return Err(IdentityError::InvalidResponse {
msg: format!(
"UsernameInformation truncated: expected >= 32 bytes, got {}",
data.len()
),
});
}
let mut account = [0u8; 32];
account.copy_from_slice(&data[..32]);
Ok(Some(account))
}
pub fn decode_consumer_info(data: &[u8]) -> Result<ConsumerInfo, IdentityError> {
if data.is_empty() {
return Err(IdentityError::InvalidResponse {
msg: "empty data — storage slot absent".into(),
});
}
let mut cursor = 0usize;
let identifier_key = read_fixed(data, &mut cursor, 65)?;
let full_username = decode_option_bounded_vec_utf8(data, &mut cursor, "full_username")?;
let lite_username = decode_bounded_vec_utf8(data, &mut cursor, "lite_username")?;
let credibility = decode_credibility(data, &mut cursor)?;
Ok(ConsumerInfo {
identifier_key,
full_username,
lite_username,
credibility,
})
}
pub fn account_id_to_hex(account_id: &[u8; 32]) -> String {
hex_encode(account_id)
}
fn read_fixed(data: &[u8], cursor: &mut usize, n: usize) -> Result<Vec<u8>, IdentityError> {
let end = cursor
.checked_add(n)
.ok_or_else(|| IdentityError::InvalidResponse {
msg: "byte offset overflow".into(),
})?;
if end > data.len() {
return Err(IdentityError::InvalidResponse {
msg: format!(
"truncated: need {} bytes at offset {}, only {} available",
n,
*cursor,
data.len() - *cursor
),
});
}
let bytes = data[*cursor..end].to_vec();
*cursor = end;
Ok(bytes)
}
fn read_compact(data: &[u8], cursor: &mut usize, field: &str) -> Result<usize, IdentityError> {
if *cursor >= data.len() {
return Err(IdentityError::InvalidResponse {
msg: format!("truncated: missing compact length byte for {field}"),
});
}
let first = data[*cursor];
let mode = first & 0b11;
match mode {
0b00 => {
*cursor += 1;
Ok((first >> 2) as usize)
}
0b01 => {
if *cursor + 2 > data.len() {
return Err(IdentityError::InvalidResponse {
msg: format!("truncated: 2-byte compact integer for {field}"),
});
}
let second = data[*cursor + 1];
let value = first as usize >> 2 | (second as usize) << 6;
*cursor += 2;
Ok(value)
}
_ => Err(IdentityError::InvalidResponse {
msg: format!("unsupported compact mode 0b{mode:02b} for {field}"),
}),
}
}
fn decode_bounded_vec_utf8(
data: &[u8],
cursor: &mut usize,
field: &str,
) -> Result<String, IdentityError> {
let len = read_compact(data, cursor, field)?;
let raw = read_fixed(data, cursor, len)?;
String::from_utf8(raw).map_err(|_| IdentityError::InvalidResponse {
msg: format!("{field} contains invalid UTF-8"),
})
}
fn decode_option_bounded_vec_utf8(
data: &[u8],
cursor: &mut usize,
field: &str,
) -> Result<Option<String>, IdentityError> {
if *cursor >= data.len() {
return Err(IdentityError::InvalidResponse {
msg: format!("truncated: missing Option tag for {field}"),
});
}
let tag = data[*cursor];
*cursor += 1;
match tag {
0x00 => Ok(None),
0x01 => Ok(Some(decode_bounded_vec_utf8(data, cursor, field)?)),
_ => Err(IdentityError::InvalidResponse {
msg: format!("unknown Option tag 0x{tag:02x} for {field}"),
}),
}
}
fn decode_credibility(data: &[u8], cursor: &mut usize) -> Result<Credibility, IdentityError> {
if *cursor >= data.len() {
return Err(IdentityError::InvalidResponse {
msg: "truncated: missing credibility tag byte".into(),
});
}
let tag = data[*cursor];
*cursor += 1;
match tag {
0x00 => Ok(Credibility::Lite),
0x01 => {
let alias_bytes = read_fixed(data, cursor, 32)?;
let ts_bytes = read_fixed(data, cursor, 8)?;
let mut alias = [0u8; 32];
alias.copy_from_slice(&alias_bytes);
let last_update = u64::from_le_bytes(
ts_bytes
.as_slice()
.try_into()
.expect("ts_bytes is exactly 8 bytes from read_fixed"),
);
Ok(Credibility::Person { alias, last_update })
}
tag => Err(IdentityError::InvalidResponse {
msg: format!("unknown Credibility variant tag: 0x{tag:02x}"),
}),
}
}
fn twox_128(data: &[u8]) -> [u8; 16] {
use std::hash::Hasher;
use twox_hash::XxHash64;
let mut h0 = XxHash64::with_seed(0);
h0.write(data);
let mut h1 = XxHash64::with_seed(1);
h1.write(data);
let mut result = [0u8; 16];
result[..8].copy_from_slice(&h0.finish().to_le_bytes());
result[8..].copy_from_slice(&h1.finish().to_le_bytes());
result
}
fn blake2_128(data: &[u8]) -> [u8; 16] {
use blake2::digest::consts::U16;
use blake2::{Blake2b, Digest};
let mut hasher = Blake2b::<U16>::new();
hasher.update(data);
let result = hasher.finalize();
let mut out = [0u8; 16];
out.copy_from_slice(&result);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rejects_empty_username() {
assert_eq!(normalize_username(""), Err(IdentityError::UsernameEmpty));
}
#[test]
fn test_accepts_single_char_username() {
assert_eq!(normalize_username("a"), Ok(b"a".to_vec()));
}
#[test]
fn test_accepts_max_length_username() {
let username = "a".repeat(32);
let result = normalize_username(&username).unwrap();
assert_eq!(result.len(), 32);
}
#[test]
fn test_rejects_username_one_over_max_length() {
let username = "a".repeat(33);
assert_eq!(
normalize_username(&username),
Err(IdentityError::UsernameTooLong { len: 33 })
);
}
#[test]
fn test_normalizes_mixed_case_username() {
let result = normalize_username("Alice").unwrap();
assert_eq!(result, b"alice");
}
#[test]
fn test_accepts_already_lowercase_username() {
let result = normalize_username("alice").unwrap();
assert_eq!(result, b"alice");
}
#[test]
fn test_rejects_space_character() {
assert_eq!(
normalize_username("ali ce"),
Err(IdentityError::InvalidCharacter { ch: ' ' })
);
}
#[test]
fn test_rejects_tab_character() {
assert_eq!(
normalize_username("ali\tce"),
Err(IdentityError::InvalidCharacter { ch: '\t' })
);
}
#[test]
fn test_rejects_null_byte() {
assert_eq!(
normalize_username("ali\0ce"),
Err(IdentityError::InvalidCharacter { ch: '\0' })
);
}
#[test]
fn test_rejects_slash_character() {
assert_eq!(
normalize_username("ali/ce"),
Err(IdentityError::InvalidCharacter { ch: '/' })
);
}
#[test]
fn test_rejects_exclamation_character() {
assert_eq!(
normalize_username("alice!"),
Err(IdentityError::InvalidCharacter { ch: '!' })
);
}
#[test]
fn test_accepts_username_with_internal_dot() {
let result = normalize_username("alice.dot").unwrap();
assert_eq!(result, b"alice.dot");
}
#[test]
fn test_rejects_username_with_leading_dot() {
assert_eq!(
normalize_username(".alice"),
Err(IdentityError::InvalidDotPlacement {
reason: "leading dot"
})
);
}
#[test]
fn test_rejects_username_with_trailing_dot() {
assert_eq!(
normalize_username("alice."),
Err(IdentityError::InvalidDotPlacement {
reason: "trailing dot"
})
);
}
#[test]
fn test_rejects_username_with_consecutive_dots() {
assert_eq!(
normalize_username("alice..1"),
Err(IdentityError::InvalidDotPlacement {
reason: "consecutive dots"
})
);
}
#[test]
fn test_rejects_username_with_only_dots() {
assert_eq!(
normalize_username("."),
Err(IdentityError::InvalidDotPlacement {
reason: "leading dot"
})
);
}
#[test]
fn test_accepts_alphanumeric_username() {
let result = normalize_username("user42").unwrap();
assert_eq!(result, b"user42");
}
#[test]
fn test_username_info_of_key_length_single_byte_username() {
let key = username_info_of_key(b"a");
assert_eq!(key.len(), 50);
}
#[test]
fn test_username_info_of_key_length_longer_username() {
let key = username_info_of_key(b"alice.dot");
assert_eq!(key.len(), 16 + 16 + 16 + 1 + 9);
}
#[test]
fn test_username_info_of_key_prefix_is_stable() {
let key_a = username_info_of_key(b"alice");
let key_b = username_info_of_key(b"bob");
assert_eq!(&key_a[..32], &key_b[..32]);
}
#[test]
fn test_different_usernames_produce_different_storage_keys() {
let key_a = username_info_of_key(b"alice");
let key_b = username_info_of_key(b"bob");
assert_ne!(key_a, key_b);
}
#[test]
fn test_username_info_of_key_same_for_lowercase_and_normalised_input() {
let lower = normalize_username("Alice").unwrap();
let direct = normalize_username("alice").unwrap();
assert_eq!(username_info_of_key(&lower), username_info_of_key(&direct));
}
#[test]
fn test_username_info_of_key_prefix_pinned() {
let key = username_info_of_key(b"a");
let identity_hash = hex_decode(&hex_encode(&twox_128(b"Identity"))).unwrap();
let storage_hash = hex_decode(&hex_encode(&twox_128(b"UsernameInfoOf"))).unwrap();
let mut expected_prefix = Vec::new();
expected_prefix.extend_from_slice(&identity_hash);
expected_prefix.extend_from_slice(&storage_hash);
assert_eq!(&key[..32], expected_prefix.as_slice());
}
#[test]
fn test_username_info_of_key_prefix_matches_live_chain() {
let key = username_info_of_key(b"a");
let expected_hex = "2aeddc77fe58c98d50bd37f1b90840f93da3e15a0621aae33d5d5d4a5487e798";
let expected_bytes = hex_decode(&format!("0x{expected_hex}")).unwrap();
assert_eq!(&key[..32], expected_bytes.as_slice());
}
#[test]
fn test_username_owner_of_key_prefix_pinned() {
let key = username_owner_of_key(b"a");
let resources_hash = twox_128(b"Resources");
let storage_hash = twox_128(b"UsernameOwnerOf");
let mut expected_prefix = Vec::new();
expected_prefix.extend_from_slice(&resources_hash);
expected_prefix.extend_from_slice(&storage_hash);
assert_eq!(&key[..32], expected_prefix.as_slice());
}
#[test]
fn test_username_owner_of_key_differs_from_info_key() {
let username = b"alice";
let info_key = username_info_of_key(username);
let owner_key = username_owner_of_key(username);
assert_ne!(info_key, owner_key);
assert_ne!(&info_key[..32], &owner_key[..32]);
}
#[test]
fn test_decode_username_owner_valid() {
let account = [0x42u8; 32];
assert_eq!(decode_username_owner(&account), Ok(Some(account)));
}
#[test]
fn test_decode_username_owner_empty() {
assert_eq!(decode_username_owner(&[]), Ok(None));
}
#[test]
fn test_decode_username_owner_truncated() {
let data = vec![0x42u8; 10];
assert!(matches!(
decode_username_owner(&data),
Err(IdentityError::InvalidResponse { .. })
));
}
#[test]
fn test_consumers_key_has_correct_length() {
let account_id = [0x42u8; 32];
let key = consumers_key(&account_id);
assert_eq!(key.len(), 80);
}
#[test]
fn test_consumers_key_prefix_is_stable() {
let key_a = consumers_key(&[0x01u8; 32]);
let key_b = consumers_key(&[0x02u8; 32]);
assert_eq!(&key_a[..32], &key_b[..32]);
}
#[test]
fn test_consumers_key_differs_for_different_account_ids() {
let key_a = consumers_key(&[0x01u8; 32]);
let key_b = consumers_key(&[0x02u8; 32]);
assert_ne!(key_a, key_b);
}
#[test]
fn test_consumers_key_prefix_uses_resources_pallet() {
let key = consumers_key(&[0x00u8; 32]);
let resources_hash = twox_128(b"Resources");
let consumers_hash = twox_128(b"Consumers");
assert_eq!(&key[..16], &resources_hash);
assert_eq!(&key[16..32], &consumers_hash);
}
#[test]
fn test_consumers_key_does_not_length_prefix_account_id() {
let account_id = [0xabu8; 32];
let key = consumers_key(&account_id);
assert_eq!(&key[48..], &account_id);
}
#[test]
fn test_decode_username_info_owner_empty_returns_none() {
assert_eq!(decode_username_info_owner(&[]), Ok(None));
}
#[test]
fn test_decode_username_info_owner_valid_33_bytes() {
let owner = [0x42u8; 32];
let mut data = Vec::with_capacity(33);
data.extend_from_slice(&owner);
data.push(0x00); assert_eq!(decode_username_info_owner(&data), Ok(Some(owner)));
}
#[test]
fn test_decode_username_info_owner_ignores_extra_bytes() {
let owner = [0xabu8; 32];
let mut data = Vec::with_capacity(40);
data.extend_from_slice(&owner);
data.extend_from_slice(&[0x01u8; 8]); assert_eq!(decode_username_info_owner(&data), Ok(Some(owner)));
}
#[test]
fn test_decode_username_info_owner_truncated_returns_error() {
let data = vec![0x42u8; 10];
assert!(matches!(
decode_username_info_owner(&data),
Err(IdentityError::InvalidResponse { .. })
));
}
#[test]
fn test_decode_username_info_owner_exactly_32_bytes() {
let owner = [0x11u8; 32];
assert_eq!(decode_username_info_owner(&owner), Ok(Some(owner)));
}
#[test]
fn test_decodes_empty_slice_as_none() {
assert_eq!(decode_option_account_id(&[]), Ok(None));
}
#[test]
fn test_decodes_zero_tag_as_none() {
assert_eq!(decode_option_account_id(&[0x00]), Ok(None));
}
#[test]
fn test_decodes_some_with_valid_account_id() {
let mut data = vec![0x01u8];
let account = [0x42u8; 32];
data.extend_from_slice(&account);
let result = decode_option_account_id(&data).unwrap();
assert_eq!(result, Some(account));
}
#[test]
fn test_decodes_all_zeros_account_id() {
let mut data = vec![0x01u8];
data.extend_from_slice(&[0x00u8; 32]);
let result = decode_option_account_id(&data).unwrap();
assert_eq!(result, Some([0x00u8; 32]));
}
#[test]
fn test_decodes_all_ff_account_id() {
let mut data = vec![0x01u8];
data.extend_from_slice(&[0xffu8; 32]);
let result = decode_option_account_id(&data).unwrap();
assert_eq!(result, Some([0xffu8; 32]));
}
#[test]
fn test_rejects_truncated_some_response() {
let mut data = vec![0x01u8];
data.extend_from_slice(&[0x00u8; 10]);
assert!(matches!(
decode_option_account_id(&data),
Err(IdentityError::InvalidResponse { .. })
));
}
#[test]
fn test_rejects_trailing_bytes_after_none() {
let data = vec![0x00u8, 0xde, 0xad];
assert!(matches!(
decode_option_account_id(&data),
Err(IdentityError::InvalidResponse { .. })
));
}
#[test]
fn test_rejects_trailing_bytes_after_some() {
let mut data = vec![0x01u8];
data.extend_from_slice(&[0x42u8; 32]);
data.push(0xff); assert!(matches!(
decode_option_account_id(&data),
Err(IdentityError::InvalidResponse { .. })
));
}
#[test]
fn test_rejects_unknown_tag_byte() {
let data = vec![0x02u8];
assert!(matches!(
decode_option_account_id(&data),
Err(IdentityError::InvalidResponse { .. })
));
}
#[test]
fn test_rejects_garbage_tag_byte() {
let data = vec![0xffu8, 0x00, 0x00];
assert!(matches!(
decode_option_account_id(&data),
Err(IdentityError::InvalidResponse { .. })
));
}
#[test]
fn test_converts_account_id_to_known_hex_string() {
let account = [0xabu8; 32];
let hex = account_id_to_hex(&account);
assert_eq!(hex.len(), 66);
assert!(hex.starts_with("0x"));
let expected = format!("0x{}", "ab".repeat(32));
assert_eq!(hex, expected);
}
#[test]
fn test_converts_zero_account_id_to_hex() {
let account = [0x00u8; 32];
let hex = account_id_to_hex(&account);
assert_eq!(
hex,
"0x0000000000000000000000000000000000000000000000000000000000000000"
);
}
#[test]
fn test_converts_mixed_account_id_to_lowercase_hex() {
let mut account = [0x00u8; 32];
account[0] = 0xAB;
account[31] = 0xCD;
let hex = account_id_to_hex(&account);
assert!(hex.chars().all(|c| !c.is_uppercase()));
assert!(hex.starts_with("0xab"));
assert!(hex.ends_with("cd"));
}
fn build_consumer_info_bytes(
identifier_key: &[u8; 65],
full_username: Option<&[u8]>,
lite_username: &[u8],
credibility_tag: u8,
alias: Option<&[u8; 32]>,
last_update: Option<u64>,
) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(identifier_key);
match full_username {
None => buf.push(0x00),
Some(s) => {
buf.push(0x01);
buf.push((s.len() as u8) << 2);
buf.extend_from_slice(s);
}
}
buf.push((lite_username.len() as u8) << 2);
buf.extend_from_slice(lite_username);
buf.push(credibility_tag);
if credibility_tag == 0x01 {
buf.extend_from_slice(alias.unwrap());
buf.extend_from_slice(&last_update.unwrap().to_le_bytes());
}
buf
}
#[test]
fn test_decode_consumer_info_lite_credibility() {
let identifier_key = [0x04u8; 65];
let lite_username = b"alice.dot";
let data = build_consumer_info_bytes(
&identifier_key,
None,
lite_username,
0x00, None,
None,
);
let info = decode_consumer_info(&data).unwrap();
assert_eq!(info.identifier_key, identifier_key);
assert_eq!(info.full_username, None);
assert_eq!(info.lite_username, "alice.dot");
assert_eq!(info.credibility, Credibility::Lite);
}
#[test]
fn test_decode_consumer_info_person_credibility() {
let identifier_key = [0x04u8; 65];
let full_username = b"alice.person";
let lite_username = b"alice";
let alias = [0xbbu8; 32];
let last_update: u64 = 1_700_000_000;
let data = build_consumer_info_bytes(
&identifier_key,
Some(full_username),
lite_username,
0x01, Some(&alias),
Some(last_update),
);
let info = decode_consumer_info(&data).unwrap();
assert_eq!(info.identifier_key, identifier_key);
assert_eq!(info.full_username, Some("alice.person".to_string()));
assert_eq!(info.lite_username, "alice");
assert_eq!(info.credibility, Credibility::Person { alias, last_update });
}
#[test]
fn test_decode_consumer_info_rejects_empty_data() {
assert!(matches!(
decode_consumer_info(&[]),
Err(IdentityError::InvalidResponse { .. })
));
}
#[test]
fn test_decode_consumer_info_rejects_truncated_identifier_key() {
let data = vec![0x04u8; 10];
assert!(matches!(
decode_consumer_info(&data),
Err(IdentityError::InvalidResponse { .. })
));
}
#[test]
fn test_decode_consumer_info_rejects_unknown_credibility_tag() {
let identifier_key = [0x04u8; 65];
let mut data = build_consumer_info_bytes(&identifier_key, None, b"alice", 0x00, None, None);
*data.last_mut().unwrap() = 0x05;
assert!(matches!(
decode_consumer_info(&data),
Err(IdentityError::InvalidResponse { .. })
));
}
#[test]
fn test_decode_consumer_info_person_truncated_alias() {
let identifier_key = [0x04u8; 65];
let lite_username = b"alice";
let mut data = Vec::new();
data.extend_from_slice(&identifier_key);
data.push(0x00); data.push((lite_username.len() as u8) << 2);
data.extend_from_slice(lite_username);
data.push(0x01); data.extend_from_slice(&[0xaau8; 10]);
assert!(matches!(
decode_consumer_info(&data),
Err(IdentityError::InvalidResponse { .. })
));
}
#[test]
fn test_decode_consumer_info_ignores_trailing_bytes() {
let identifier_key = [0x04u8; 65];
let mut data = build_consumer_info_bytes(&identifier_key, None, b"alice", 0x00, None, None);
data.extend_from_slice(&[0xff, 0xff, 0xff]);
assert!(decode_consumer_info(&data).is_ok());
}
}