1use crate::error::{Error, Result};
38use crate::header::SLOT_COUNT;
39use crate::session::CredentialKind;
40
41pub const ENTRY_LEN: usize = 3;
43
44pub const DIRECTORY_LEN: usize = SLOT_COUNT * ENTRY_LEN;
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum SlotKind {
52 Empty,
54 Vault,
56 Device,
58 Portable,
60 Recovery,
62 Unknown(u8),
67}
68
69impl SlotKind {
70 #[must_use]
72 pub const fn to_byte(self) -> u8 {
73 match self {
74 Self::Empty => 0x00,
75 Self::Vault => 0x01,
76 Self::Device => 0x02,
77 Self::Portable => 0x03,
78 Self::Recovery => 0x04,
79 Self::Unknown(b) => b,
80 }
81 }
82
83 #[must_use]
85 pub const fn from_byte(b: u8) -> Self {
86 match b {
87 0x00 => Self::Empty,
88 0x01 => Self::Vault,
89 0x02 => Self::Device,
90 0x03 => Self::Portable,
91 0x04 => Self::Recovery,
92 other => Self::Unknown(other),
93 }
94 }
95
96 #[must_use]
100 pub const fn is_occupied(self) -> bool {
101 !matches!(self, Self::Empty)
102 }
103
104 #[must_use]
106 pub const fn name(self) -> &'static str {
107 match self {
108 Self::Empty => "empty",
109 Self::Vault => "vault",
110 Self::Device => "device",
111 Self::Portable => "portable",
112 Self::Recovery => "recovery",
113 Self::Unknown(_) => "unknown",
114 }
115 }
116}
117
118impl From<CredentialKind> for SlotKind {
119 fn from(k: CredentialKind) -> Self {
120 match k {
121 CredentialKind::Vault => Self::Vault,
122 CredentialKind::Device => Self::Device,
123 CredentialKind::Portable => Self::Portable,
124 CredentialKind::Recovery => Self::Recovery,
125 }
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub struct SlotEntry {
132 pub kind: SlotKind,
134 pub label_id: u16,
138}
139
140impl SlotEntry {
141 #[must_use]
143 pub const fn empty() -> Self {
144 Self { kind: SlotKind::Empty, label_id: 0 }
145 }
146
147 #[must_use]
149 pub const fn of(kind: SlotKind) -> Self {
150 Self { kind, label_id: 0 }
151 }
152}
153
154impl Default for SlotEntry {
155 fn default() -> Self {
156 Self::empty()
157 }
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162pub struct SlotDirectory {
163 entries: [SlotEntry; SLOT_COUNT],
164}
165
166impl Default for SlotDirectory {
167 fn default() -> Self {
168 Self::new()
169 }
170}
171
172impl SlotDirectory {
173 #[must_use]
175 pub const fn new() -> Self {
176 Self { entries: [SlotEntry::empty(); SLOT_COUNT] }
177 }
178
179 #[must_use]
181 pub const fn entries(&self) -> &[SlotEntry; SLOT_COUNT] {
182 &self.entries
183 }
184
185 #[must_use]
187 pub fn get(&self, i: usize) -> Option<SlotEntry> {
188 self.entries.get(i).copied()
189 }
190
191 pub fn set(&mut self, i: usize, e: SlotEntry) -> Result<()> {
197 let slot = self
198 .entries
199 .get_mut(i)
200 .ok_or(Error::TooManySlots { got: i, max: SLOT_COUNT })?;
201 *slot = e;
202 Ok(())
203 }
204
205 #[must_use]
210 pub fn first_free(&self) -> Option<usize> {
211 self.entries.iter().position(|e| !e.kind.is_occupied())
212 }
213
214 #[must_use]
216 pub fn used(&self) -> usize {
217 self.entries.iter().filter(|e| e.kind.is_occupied()).count()
218 }
219
220 #[must_use]
222 pub fn indices_of(&self, kind: SlotKind) -> Vec<usize> {
223 self.entries
224 .iter()
225 .enumerate()
226 .filter(|(_, e)| e.kind == kind)
227 .map(|(i, _)| i)
228 .collect()
229 }
230
231 #[must_use]
233 pub fn encode(&self) -> Vec<u8> {
234 let mut out = Vec::with_capacity(DIRECTORY_LEN);
235 for e in &self.entries {
236 out.push(e.kind.to_byte());
237 out.extend_from_slice(&e.label_id.to_le_bytes());
238 }
239 debug_assert_eq!(out.len(), DIRECTORY_LEN, "槽位目录必须恰好 {DIRECTORY_LEN} 字节");
240 out
241 }
242
243 pub fn decode(raw: &[u8]) -> Result<Self> {
251 if raw.len() != DIRECTORY_LEN {
252 return Err(Error::MalformedTlv {
253 tlv_type: crate::tlv::types::SLOT_DIRECTORY,
254 reason: "slot directory must be exactly 24 bytes",
255 });
256 }
257 let mut dir = Self::new();
258 for i in 0..SLOT_COUNT {
259 let at = i.saturating_mul(ENTRY_LEN);
260 let kind = SlotKind::from_byte(*raw.get(at).unwrap_or(&0));
261 let lo = *raw.get(at.saturating_add(1)).unwrap_or(&0);
262 let hi = *raw.get(at.saturating_add(2)).unwrap_or(&0);
263 if let Some(slot) = dir.entries.get_mut(i) {
267 *slot = SlotEntry { kind, label_id: u16::from_le_bytes([lo, hi]) };
268 }
269 }
270 Ok(dir)
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn roundtrip_preserves_every_slot() {
280 let mut dir = SlotDirectory::new();
281 dir.set(0, SlotEntry { kind: SlotKind::Vault, label_id: 7 }).unwrap();
282 dir.set(3, SlotEntry { kind: SlotKind::Recovery, label_id: 0 }).unwrap();
283 dir.set(7, SlotEntry { kind: SlotKind::Device, label_id: 65535 }).unwrap();
284
285 let raw = dir.encode();
286 assert_eq!(raw.len(), DIRECTORY_LEN);
287 let back = SlotDirectory::decode(&raw).unwrap();
288 assert_eq!(back, dir);
291 }
292
293 #[test]
294 fn length_is_constant_regardless_of_how_many_slots_are_used() {
295 for n in 0..=SLOT_COUNT {
298 let mut dir = SlotDirectory::new();
299 for i in 0..n {
300 dir.set(i, SlotEntry::of(SlotKind::Vault)).unwrap();
301 }
302 assert_eq!(dir.encode().len(), DIRECTORY_LEN, "{n} 个槽位时长度变了");
303 }
304 }
305
306 #[test]
307 fn unknown_kind_is_preserved_and_counts_as_occupied() {
308 let mut raw = SlotDirectory::new().encode();
311 raw[0] = 0x7F; let dir = SlotDirectory::decode(&raw).unwrap();
313 assert_eq!(dir.get(0).unwrap().kind, SlotKind::Unknown(0x7F));
314 assert!(dir.get(0).unwrap().kind.is_occupied(), "未知类型必须算占用");
315 assert_eq!(dir.first_free(), Some(1), "不该把未知槽当成空闲");
316 assert_eq!(dir.encode()[0], 0x7F, "未知类型的字节被改写了");
318 }
319
320 #[test]
321 fn first_free_finds_a_real_hole_not_just_the_tail() {
322 let mut dir = SlotDirectory::new();
327 for i in 0..SLOT_COUNT {
328 dir.set(i, SlotEntry::of(SlotKind::Vault)).unwrap();
329 }
330 assert_eq!(dir.first_free(), None, "全满时必须返回 None");
331
332 dir.set(2, SlotEntry::empty()).unwrap();
333 assert_eq!(dir.first_free(), Some(2), "中间的空洞要能被找到");
334 }
335
336 #[test]
337 fn rejects_wrong_length() {
338 assert!(SlotDirectory::decode(&[0u8; DIRECTORY_LEN - 1]).is_err());
341 assert!(SlotDirectory::decode(&[0u8; DIRECTORY_LEN + 1]).is_err());
342 assert!(SlotDirectory::decode(&[]).is_err());
343 }
344
345 #[test]
346 fn credential_kind_maps_onto_slot_kind() {
347 assert_eq!(SlotKind::from(CredentialKind::Vault), SlotKind::Vault);
350 assert_eq!(SlotKind::from(CredentialKind::Device), SlotKind::Device);
351 assert_eq!(SlotKind::from(CredentialKind::Portable), SlotKind::Portable);
352 assert_eq!(SlotKind::from(CredentialKind::Recovery), SlotKind::Recovery);
353 }
354
355 #[test]
356 fn byte_encoding_matches_the_spec() {
357 assert_eq!(SlotKind::Empty.to_byte(), 0x00);
359 assert_eq!(SlotKind::Vault.to_byte(), 0x01);
360 assert_eq!(SlotKind::Device.to_byte(), 0x02);
361 assert_eq!(SlotKind::Portable.to_byte(), 0x03);
362 assert_eq!(SlotKind::Recovery.to_byte(), 0x04);
363 }
364
365 #[test]
366 fn indices_and_used_count_agree() {
367 let mut dir = SlotDirectory::new();
368 dir.set(0, SlotEntry::of(SlotKind::Vault)).unwrap();
369 dir.set(1, SlotEntry::of(SlotKind::Recovery)).unwrap();
370 dir.set(5, SlotEntry::of(SlotKind::Vault)).unwrap();
371
372 assert_eq!(dir.used(), 3);
373 assert_eq!(dir.indices_of(SlotKind::Vault), vec![0, 5]);
374 assert_eq!(dir.indices_of(SlotKind::Recovery), vec![1]);
375 assert!(dir.indices_of(SlotKind::Device).is_empty());
376 }
377}