use crate::error::{Error, Result};
use crate::header::SLOT_COUNT;
use crate::session::CredentialKind;
pub const ENTRY_LEN: usize = 3;
pub const DIRECTORY_LEN: usize = SLOT_COUNT * ENTRY_LEN;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SlotKind {
Empty,
Vault,
Device,
Portable,
Recovery,
Unknown(u8),
}
impl SlotKind {
#[must_use]
pub const fn to_byte(self) -> u8 {
match self {
Self::Empty => 0x00,
Self::Vault => 0x01,
Self::Device => 0x02,
Self::Portable => 0x03,
Self::Recovery => 0x04,
Self::Unknown(b) => b,
}
}
#[must_use]
pub const fn from_byte(b: u8) -> Self {
match b {
0x00 => Self::Empty,
0x01 => Self::Vault,
0x02 => Self::Device,
0x03 => Self::Portable,
0x04 => Self::Recovery,
other => Self::Unknown(other),
}
}
#[must_use]
pub const fn is_occupied(self) -> bool {
!matches!(self, Self::Empty)
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Empty => "empty",
Self::Vault => "vault",
Self::Device => "device",
Self::Portable => "portable",
Self::Recovery => "recovery",
Self::Unknown(_) => "unknown",
}
}
}
impl From<CredentialKind> for SlotKind {
fn from(k: CredentialKind) -> Self {
match k {
CredentialKind::Vault => Self::Vault,
CredentialKind::Device => Self::Device,
CredentialKind::Portable => Self::Portable,
CredentialKind::Recovery => Self::Recovery,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SlotEntry {
pub kind: SlotKind,
pub label_id: u16,
}
impl SlotEntry {
#[must_use]
pub const fn empty() -> Self {
Self { kind: SlotKind::Empty, label_id: 0 }
}
#[must_use]
pub const fn of(kind: SlotKind) -> Self {
Self { kind, label_id: 0 }
}
}
impl Default for SlotEntry {
fn default() -> Self {
Self::empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SlotDirectory {
entries: [SlotEntry; SLOT_COUNT],
}
impl Default for SlotDirectory {
fn default() -> Self {
Self::new()
}
}
impl SlotDirectory {
#[must_use]
pub const fn new() -> Self {
Self { entries: [SlotEntry::empty(); SLOT_COUNT] }
}
#[must_use]
pub const fn entries(&self) -> &[SlotEntry; SLOT_COUNT] {
&self.entries
}
#[must_use]
pub fn get(&self, i: usize) -> Option<SlotEntry> {
self.entries.get(i).copied()
}
pub fn set(&mut self, i: usize, e: SlotEntry) -> Result<()> {
let slot = self
.entries
.get_mut(i)
.ok_or(Error::TooManySlots { got: i, max: SLOT_COUNT })?;
*slot = e;
Ok(())
}
#[must_use]
pub fn first_free(&self) -> Option<usize> {
self.entries.iter().position(|e| !e.kind.is_occupied())
}
#[must_use]
pub fn used(&self) -> usize {
self.entries.iter().filter(|e| e.kind.is_occupied()).count()
}
#[must_use]
pub fn indices_of(&self, kind: SlotKind) -> Vec<usize> {
self.entries
.iter()
.enumerate()
.filter(|(_, e)| e.kind == kind)
.map(|(i, _)| i)
.collect()
}
#[must_use]
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(DIRECTORY_LEN);
for e in &self.entries {
out.push(e.kind.to_byte());
out.extend_from_slice(&e.label_id.to_le_bytes());
}
debug_assert_eq!(out.len(), DIRECTORY_LEN, "槽位目录必须恰好 {DIRECTORY_LEN} 字节");
out
}
pub fn decode(raw: &[u8]) -> Result<Self> {
if raw.len() != DIRECTORY_LEN {
return Err(Error::MalformedTlv {
tlv_type: crate::tlv::types::SLOT_DIRECTORY,
reason: "slot directory must be exactly 24 bytes",
});
}
let mut dir = Self::new();
for i in 0..SLOT_COUNT {
let at = i.saturating_mul(ENTRY_LEN);
let kind = SlotKind::from_byte(*raw.get(at).unwrap_or(&0));
let lo = *raw.get(at.saturating_add(1)).unwrap_or(&0);
let hi = *raw.get(at.saturating_add(2)).unwrap_or(&0);
if let Some(slot) = dir.entries.get_mut(i) {
*slot = SlotEntry { kind, label_id: u16::from_le_bytes([lo, hi]) };
}
}
Ok(dir)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_preserves_every_slot() {
let mut dir = SlotDirectory::new();
dir.set(0, SlotEntry { kind: SlotKind::Vault, label_id: 7 }).unwrap();
dir.set(3, SlotEntry { kind: SlotKind::Recovery, label_id: 0 }).unwrap();
dir.set(7, SlotEntry { kind: SlotKind::Device, label_id: 65535 }).unwrap();
let raw = dir.encode();
assert_eq!(raw.len(), DIRECTORY_LEN);
let back = SlotDirectory::decode(&raw).unwrap();
assert_eq!(back, dir);
}
#[test]
fn length_is_constant_regardless_of_how_many_slots_are_used() {
for n in 0..=SLOT_COUNT {
let mut dir = SlotDirectory::new();
for i in 0..n {
dir.set(i, SlotEntry::of(SlotKind::Vault)).unwrap();
}
assert_eq!(dir.encode().len(), DIRECTORY_LEN, "{n} 个槽位时长度变了");
}
}
#[test]
fn unknown_kind_is_preserved_and_counts_as_occupied() {
let mut raw = SlotDirectory::new().encode();
raw[0] = 0x7F; let dir = SlotDirectory::decode(&raw).unwrap();
assert_eq!(dir.get(0).unwrap().kind, SlotKind::Unknown(0x7F));
assert!(dir.get(0).unwrap().kind.is_occupied(), "未知类型必须算占用");
assert_eq!(dir.first_free(), Some(1), "不该把未知槽当成空闲");
assert_eq!(dir.encode()[0], 0x7F, "未知类型的字节被改写了");
}
#[test]
fn first_free_finds_a_real_hole_not_just_the_tail() {
let mut dir = SlotDirectory::new();
for i in 0..SLOT_COUNT {
dir.set(i, SlotEntry::of(SlotKind::Vault)).unwrap();
}
assert_eq!(dir.first_free(), None, "全满时必须返回 None");
dir.set(2, SlotEntry::empty()).unwrap();
assert_eq!(dir.first_free(), Some(2), "中间的空洞要能被找到");
}
#[test]
fn rejects_wrong_length() {
assert!(SlotDirectory::decode(&[0u8; DIRECTORY_LEN - 1]).is_err());
assert!(SlotDirectory::decode(&[0u8; DIRECTORY_LEN + 1]).is_err());
assert!(SlotDirectory::decode(&[]).is_err());
}
#[test]
fn credential_kind_maps_onto_slot_kind() {
assert_eq!(SlotKind::from(CredentialKind::Vault), SlotKind::Vault);
assert_eq!(SlotKind::from(CredentialKind::Device), SlotKind::Device);
assert_eq!(SlotKind::from(CredentialKind::Portable), SlotKind::Portable);
assert_eq!(SlotKind::from(CredentialKind::Recovery), SlotKind::Recovery);
}
#[test]
fn byte_encoding_matches_the_spec() {
assert_eq!(SlotKind::Empty.to_byte(), 0x00);
assert_eq!(SlotKind::Vault.to_byte(), 0x01);
assert_eq!(SlotKind::Device.to_byte(), 0x02);
assert_eq!(SlotKind::Portable.to_byte(), 0x03);
assert_eq!(SlotKind::Recovery.to_byte(), 0x04);
}
#[test]
fn indices_and_used_count_agree() {
let mut dir = SlotDirectory::new();
dir.set(0, SlotEntry::of(SlotKind::Vault)).unwrap();
dir.set(1, SlotEntry::of(SlotKind::Recovery)).unwrap();
dir.set(5, SlotEntry::of(SlotKind::Vault)).unwrap();
assert_eq!(dir.used(), 3);
assert_eq!(dir.indices_of(SlotKind::Vault), vec![0, 5]);
assert_eq!(dir.indices_of(SlotKind::Recovery), vec![1]);
assert!(dir.indices_of(SlotKind::Device).is_empty());
}
}