1use crate::crypto::{CipherId, Fek, Kek, ZERO_NONCE, header_mac};
67use crate::error::{Error, Result};
68use crate::header::{
69 FIXED_HEADER_LEN, FixedHeader, HEADER_MAC_LEN, SLOT_AREA_LEN, SLOT_AREA_OFFSET, SLOT_COUNT,
70 SLOT_LEN, TLV_AREA_OFFSET,
71};
72use crate::util::Writer;
73
74#[derive(Debug)]
76pub struct RewriteOutcome {
77 pub bytes: Vec<u8>,
79 pub opened_slot: u16,
81 pub slot_used: usize,
83 pub carried_opaque: usize,
92 pub may_have_evicted: bool,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum OtherSlots {
116 Carry,
144 Discard,
149}
150
151pub fn rewrite_slots(
168 data: &[u8],
169 unlock: &[Kek],
170 keep: &[Kek],
171 others: OtherSlots,
172) -> Result<RewriteOutcome> {
173 if keep.is_empty() {
174 return Err(Error::MalformedHeader {
175 reason: "refusing to leave a file with zero key slots; it could never be opened again",
176 });
177 }
178 if keep.len() > SLOT_COUNT {
179 return Err(Error::TooManySlots { got: keep.len(), max: SLOT_COUNT });
180 }
181
182 let opened = crate::file::open(data, unlock)?;
185 let header = FixedHeader::parse(data)?;
186
187 let old_area = data.get(SLOT_AREA_OFFSET..SLOT_AREA_OFFSET.saturating_add(SLOT_AREA_LEN))
188 .ok_or(Error::Truncated {
189 context: "key slot area",
190 need: SLOT_AREA_OFFSET.saturating_add(SLOT_AREA_LEN),
191 got: data.len(),
192 })?;
193 let (slot_area, carried_opaque) = build_area(
194 keep,
195 opened.fek(),
196 &header.file_uuid,
197 header.cipher_id,
198 old_area,
199 others,
200 )?;
201
202 let tlv_end = TLV_AREA_OFFSET.saturating_add(header.tlv_len as usize);
205 let tlv_blob = data.get(TLV_AREA_OFFSET..tlv_end).ok_or(Error::Truncated {
206 context: "tlv area",
207 need: tlv_end,
208 got: data.len(),
209 })?;
210 let fixed = data.get(..FIXED_HEADER_LEN).ok_or(Error::Truncated {
211 context: "fixed header",
212 need: FIXED_HEADER_LEN,
213 got: data.len(),
214 })?;
215 let payload_start = tlv_end.saturating_add(HEADER_MAC_LEN);
216 let payload = data.get(payload_start..).ok_or(Error::Truncated {
217 context: "payload",
218 need: payload_start,
219 got: data.len(),
220 })?;
221
222 let mut covered = Writer::with_capacity(header.header_len as usize);
223 covered.bytes(fixed).bytes(&slot_area).bytes(tlv_blob);
224 let mac_key = opened.fek().derive_header_mac_key(&header.file_uuid);
225 let mac = header_mac(&mac_key, covered.as_slice());
226
227 let mut out =
228 Writer::with_capacity((header.header_len as usize).saturating_add(payload.len()));
229 out.bytes(covered.as_slice()).bytes(&mac).bytes(payload);
230 let bytes = out.into_vec();
231
232 debug_assert_eq!(bytes.len(), data.len(), "slot 改写不得改变文件长度");
233 Ok(RewriteOutcome {
234 bytes,
235 opened_slot: opened.slot_index,
236 slot_used: keep.len(),
237 carried_opaque,
238 may_have_evicted: others == OtherSlots::Carry && keep.len() > 1,
241 })
242}
243
244fn build_area(
252 keks: &[Kek],
253 fek: &Fek,
254 file_uuid: &[u8; 16],
255 cipher: CipherId,
256 old_area: &[u8],
257 others: OtherSlots,
258) -> Result<(Vec<u8>, usize)> {
259 let mut w = Writer::with_capacity(SLOT_AREA_LEN);
260 for (i, kek) in keks.iter().enumerate() {
261 let idx = u16::try_from(i)
262 .map_err(|_| Error::TooManySlots { got: keks.len(), max: SLOT_COUNT })?;
263 let wrap_key = kek.derive_slot_key(file_uuid, idx);
264 let wrapped = cipher.encrypt(&wrap_key, &ZERO_NONCE, fek.as_key().as_bytes(), &[])?;
265 w.bytes(&wrapped);
266 }
267
268 let mut carried = 0usize;
269 match others {
270 OtherSlots::Carry => {
291 for i in keks.len()..SLOT_COUNT {
292 let start = i.saturating_mul(SLOT_LEN);
293 let end = start.saturating_add(SLOT_LEN);
294 match old_area.get(start..end) {
295 Some(slot) => {
296 w.bytes(slot);
297 carried = carried.saturating_add(1);
298 }
299 None => {
303 w.random(SLOT_LEN);
304 }
305 }
306 }
307 }
308 OtherSlots::Discard => {
310 let used = keks.len().saturating_mul(SLOT_LEN);
311 w.random(SLOT_AREA_LEN.saturating_sub(used));
312 }
313 }
314
315 let out = w.into_vec();
316 debug_assert_eq!(out.len(), SLOT_AREA_LEN, "slot 区必须恰好 384 字节");
317 Ok((out, carried))
318}
319
320#[derive(Debug)]
325pub enum SlotPlan {
326 Keep,
331 Write(Kek),
333 Clear,
335}
336
337pub fn rewrite_slots_managed(
362 data: &[u8],
363 unlock: &[Kek],
364 plans: &[SlotPlan],
365 new_directory: &crate::slotdir::SlotDirectory,
366) -> Result<RewriteOutcome> {
367 if plans.len() != SLOT_COUNT {
368 return Err(Error::TooManySlots { got: plans.len(), max: SLOT_COUNT });
369 }
370 if !plans.iter().any(|p| matches!(p, SlotPlan::Keep | SlotPlan::Write(_))) {
373 return Err(Error::MalformedHeader {
374 reason: "refusing to leave a file with zero key slots; it could never be opened again",
375 });
376 }
377
378 {
390 let surviving: Vec<crate::slotdir::SlotKind> = (0..SLOT_COUNT)
391 .filter(|i| {
392 plans
393 .get(*i)
394 .is_some_and(|p| matches!(p, SlotPlan::Keep | SlotPlan::Write(_)))
395 })
396 .filter_map(|i| new_directory.get(i).map(|e| e.kind))
397 .filter(|k| k.is_occupied())
398 .collect();
399 if !surviving.is_empty()
402 && surviving
403 .iter()
404 .all(|k| *k == crate::slotdir::SlotKind::Device)
405 {
406 return Err(Error::MalformedHeader {
407 reason: "refusing to leave only device keys; they stop working when the machine, \
408 OS or biometrics change, and there would be no way back in",
409 });
410 }
411 }
412
413 let opened = crate::file::open(data, unlock)?;
416 let header = FixedHeader::parse(data)?;
417 if !header.has_flag(crate::header::flags::SLOT_DIRECTORY) {
418 return Err(Error::MissingTlv { tlv_type: crate::tlv::types::SLOT_DIRECTORY });
419 }
420
421 let old_area = data
422 .get(SLOT_AREA_OFFSET..SLOT_AREA_OFFSET.saturating_add(SLOT_AREA_LEN))
423 .ok_or(Error::Truncated {
424 context: "key slot area",
425 need: SLOT_AREA_OFFSET.saturating_add(SLOT_AREA_LEN),
426 got: data.len(),
427 })?;
428
429 let mut w = Writer::with_capacity(SLOT_AREA_LEN);
430 let mut carried = 0usize;
431 let mut used = 0usize;
432 for (i, plan) in plans.iter().enumerate() {
433 match plan {
434 SlotPlan::Keep => {
435 let start = i.saturating_mul(SLOT_LEN);
436 let end = start.saturating_add(SLOT_LEN);
437 match old_area.get(start..end) {
438 Some(slot) => {
439 w.bytes(slot);
440 carried = carried.saturating_add(1);
441 used = used.saturating_add(1);
442 }
443 None => {
446 w.random(SLOT_LEN);
447 }
448 }
449 }
450 SlotPlan::Write(kek) => {
451 let idx = u16::try_from(i)
452 .map_err(|_| Error::TooManySlots { got: i, max: SLOT_COUNT })?;
453 let wrap_key = kek.derive_slot_key(&header.file_uuid, idx);
454 let wrapped = header.cipher_id.encrypt(
455 &wrap_key,
456 &ZERO_NONCE,
457 opened.fek().as_key().as_bytes(),
458 &[],
459 )?;
460 w.bytes(&wrapped);
461 used = used.saturating_add(1);
462 }
463 SlotPlan::Clear => {
466 w.random(SLOT_LEN);
467 }
468 }
469 }
470 let slot_area = w.into_vec();
471 debug_assert_eq!(slot_area.len(), SLOT_AREA_LEN, "slot 区必须恰好 384 字节");
472
473 let tlv_end = TLV_AREA_OFFSET.saturating_add(header.tlv_len as usize);
479 let tlv_blob = data.get(TLV_AREA_OFFSET..tlv_end).ok_or(Error::Truncated {
480 context: "tlv area",
481 need: tlv_end,
482 got: data.len(),
483 })?;
484 let new_value = crate::tlv::encrypt_entry(
485 crate::tlv::types::SLOT_DIRECTORY,
486 crate::tlv::tlv_flags::CRITICAL,
487 &new_directory.encode(),
488 opened.fek(),
489 header.cipher_id,
490 )?
491 .value;
492 let new_tlv = replace_tlv_value(tlv_blob, crate::tlv::types::SLOT_DIRECTORY, &new_value)?;
493
494 let fixed = data.get(..FIXED_HEADER_LEN).ok_or(Error::Truncated {
495 context: "fixed header",
496 need: FIXED_HEADER_LEN,
497 got: data.len(),
498 })?;
499 let payload_start = tlv_end.saturating_add(HEADER_MAC_LEN);
500 let payload = data.get(payload_start..).ok_or(Error::Truncated {
501 context: "payload",
502 need: payload_start,
503 got: data.len(),
504 })?;
505
506 let mut covered = Writer::with_capacity(header.header_len as usize);
507 covered.bytes(fixed).bytes(&slot_area).bytes(&new_tlv);
508 let mac_key = opened.fek().derive_header_mac_key(&header.file_uuid);
509 let mac = header_mac(&mac_key, covered.as_slice());
510
511 let mut out =
512 Writer::with_capacity((header.header_len as usize).saturating_add(payload.len()));
513 out.bytes(covered.as_slice()).bytes(&mac).bytes(payload);
514 let bytes = out.into_vec();
515
516 debug_assert_eq!(bytes.len(), data.len(), "slot 改写不得改变文件长度");
517 Ok(RewriteOutcome {
518 bytes,
519 opened_slot: opened.slot_index,
520 slot_used: used,
521 carried_opaque: carried,
522 may_have_evicted: false,
525 })
526}
527
528fn replace_tlv_value(blob: &[u8], want_type: u16, new_value: &[u8]) -> Result<Vec<u8>> {
534 let mut out = blob.to_vec();
535 let mut at = 0usize;
536 while at.saturating_add(crate::tlv::TLV_HEADER_LEN) <= blob.len() {
537 let t = u16::from_le_bytes([
538 *blob.get(at).unwrap_or(&0),
539 *blob.get(at.saturating_add(1)).unwrap_or(&0),
540 ]);
541 let len = u32::from_le_bytes([
542 *blob.get(at.saturating_add(4)).unwrap_or(&0),
543 *blob.get(at.saturating_add(5)).unwrap_or(&0),
544 *blob.get(at.saturating_add(6)).unwrap_or(&0),
545 *blob.get(at.saturating_add(7)).unwrap_or(&0),
546 ]) as usize;
547 let vstart = at.saturating_add(crate::tlv::TLV_HEADER_LEN);
548 let vend = vstart.saturating_add(len);
549 if vend > blob.len() {
550 break;
551 }
552 if t == want_type {
553 if len != new_value.len() {
554 return Err(Error::MalformedTlv {
555 tlv_type: want_type,
556 reason: "slot directory length changed; header_len would no longer match",
557 });
558 }
559 out.get_mut(vstart..vend)
560 .ok_or(Error::MalformedTlv {
561 tlv_type: want_type,
562 reason: "tlv value out of range",
563 })?
564 .copy_from_slice(new_value);
565 return Ok(out);
566 }
567 at = vend;
568 }
569 Err(Error::MissingTlv { tlv_type: want_type })
570}
571#[must_use]
573pub const fn slot_area_range() -> (usize, usize) {
574 (SLOT_AREA_OFFSET, SLOT_AREA_OFFSET + SLOT_AREA_LEN)
575}
576
577#[cfg(test)]
578#[expect(clippy::unwrap_used, clippy::indexing_slicing, reason = "测试断言需要")]
579mod tests {
580 use super::*;
581 use crate::crypto::{Argon2Params, SecretKey};
582 use crate::file::{EncryptOptions, RandomMaterial, encrypt};
583
584 fn kek(seed: u8) -> Kek {
585 Kek::from_key(SecretKey::from_bytes(core::array::from_fn(|i| {
587 (i as u8).wrapping_mul(31) ^ seed
588 })))
589 }
590
591 fn sample(keks: &[Kek]) -> Vec<u8> {
592 let plain: Vec<u8> = (0..5000u32).map(|i| (i % 251) as u8).collect();
593 let opts = EncryptOptions {
594 argon2: Argon2Params { m_kib: 8, t: 1, p: 1 },
595 ..Default::default()
596 };
597 encrypt(&plain, keks, &[7u8; 16], &opts, &RandomMaterial::generate())
598 .unwrap()
599 .bytes
600 }
601
602 #[test]
607 fn add_keeps_both_passwords() {
608 let a = kek(0x11);
609 let b = kek(0x22);
610 let data = sample(&[a.duplicate()]);
611
612 let out = rewrite_slots(&data, &[a.duplicate()], &[a.duplicate(), b.duplicate()], OtherSlots::Carry).unwrap();
613
614 assert!(crate::file::open(&out.bytes, &[a]).is_ok(), "原密码必须仍可用");
615 assert!(crate::file::open(&out.bytes, &[b]).is_ok(), "新密码必须可用");
616 assert_eq!(out.slot_used, 2);
617 }
618
619 #[test]
624 fn removed_password_no_longer_opens() {
625 let a = kek(0x33);
626 let b = kek(0x44);
627 let data = sample(&[a.duplicate(), b.duplicate()]);
628 assert!(crate::file::open(&data, &[b.duplicate()]).is_ok());
630
631 let out =
632 rewrite_slots(&data, &[a.duplicate()], &[a.duplicate()], OtherSlots::Discard).unwrap();
633
634 assert!(crate::file::open(&out.bytes, &[a]).is_ok(), "保留的密码应可用");
635 let err = crate::file::open(&out.bytes, &[b]).unwrap_err();
636 assert!(
637 matches!(err, Error::NoMatchingSlot),
638 "被移除的密码必须打不开,实际 {err:?}"
639 );
640 }
641
642 #[test]
646 fn payload_and_uuid_untouched() {
647 let a = kek(0x55);
648 let b = kek(0x66);
649 let data = sample(&[a.duplicate()]);
650 let out = rewrite_slots(&data, &[a.duplicate()], &[b], OtherSlots::Carry).unwrap();
651
652 let h0 = FixedHeader::parse(&data).unwrap();
653 let h1 = FixedHeader::parse(&out.bytes).unwrap();
654 assert_eq!(h0.file_uuid, h1.file_uuid, "file_uuid 不得改变");
655 assert_eq!(h0.base_nonce, h1.base_nonce, "base_nonce 不得改变");
656 assert_eq!(data.len(), out.bytes.len(), "文件长度不得改变");
657
658 let (lo, hi) = slot_area_range();
659 assert_eq!(data[..lo], out.bytes[..lo], "固定头不得改变");
660 let mac_end = h0.header_len as usize;
661 let mac_start = mac_end - HEADER_MAC_LEN;
662 assert_eq!(data[hi..mac_start], out.bytes[hi..mac_start], "TLV 区不得改变");
663 assert_eq!(data[mac_end..], out.bytes[mac_end..], "载荷必须逐字节相同");
664 assert_ne!(data[lo..hi], out.bytes[lo..hi], "slot 区应当变了");
665 assert_ne!(
666 data[mac_start..mac_end],
667 out.bytes[mac_start..mac_end],
668 "头部 MAC 应当随 slot 区一起变"
669 );
670 }
671
672 #[test]
677 fn plaintext_still_decrypts() {
678 let a = kek(0x77);
679 let b = kek(0x88);
680 let plain: Vec<u8> = (0..5000u32).map(|i| (i % 251) as u8).collect();
681 let data = sample(&[a.duplicate()]);
682
683 let out = rewrite_slots(&data, &[a], &[b.duplicate()], OtherSlots::Carry).unwrap();
684 let opened = crate::file::open(&out.bytes, &[b]).unwrap();
685 assert_eq!(opened.decrypt_all(&out.bytes).unwrap(), plain);
686 }
687
688 #[test]
690 fn empty_keep_is_rejected() {
691 let a = kek(0x99);
692 let data = sample(&[a.duplicate()]);
693 assert!(
694 rewrite_slots(&data, &[a], &[], OtherSlots::Carry).is_err(),
695 "keep 为空必须报错"
696 );
697 }
698
699 #[test]
701 fn too_many_slots_is_rejected() {
702 let a = kek(0xAA);
703 let data = sample(&[a.duplicate()]);
704 let many: Vec<Kek> = (0..9).map(|i| kek(0xB0 + i)).collect();
705 let err = rewrite_slots(&data, &[a], &many, OtherSlots::Carry).unwrap_err();
706 assert!(matches!(err, Error::TooManySlots { got: 9, max: 8 }), "实际 {err:?}");
707 }
708
709 #[test]
711 fn wrong_unlock_password_fails() {
712 let a = kek(0xCC);
713 let wrong = kek(0xDD);
714 let data = sample(&[a]);
715 let err =
716 rewrite_slots(&data, &[wrong.duplicate()], &[wrong], OtherSlots::Carry).unwrap_err();
717 assert!(matches!(err, Error::NoMatchingSlot), "实际 {err:?}");
718 }
719
720 #[test]
725 fn tampered_header_is_rejected() {
726 let a = kek(0xEE);
727 let mut data = sample(&[a.duplicate()]);
728 data[64] ^= 0x01;
731 let err =
732 rewrite_slots(&data, &[a.duplicate()], &[a], OtherSlots::Carry).unwrap_err();
733 assert!(
734 matches!(err, Error::HeaderMacMismatch | Error::MalformedHeader { .. }),
735 "篡改必须被拒绝,实际 {err:?}"
736 );
737 }
738
739 #[test]
744 fn unused_slots_are_random_not_zero() {
745 let a = kek(0x12);
746 let data = sample(&[a.duplicate()]);
747 let out =
748 rewrite_slots(&data, &[a.duplicate()], &[a], OtherSlots::Discard).unwrap();
749 let (lo, hi) = slot_area_range();
750 let pad = &out.bytes[lo + SLOT_LEN..hi];
751 assert_eq!(pad.len(), SLOT_LEN * 7);
752 assert!(pad.iter().any(|&b| b != 0), "空槽不得填零");
753
754 let out2 =
756 rewrite_slots(&data, &[kek(0x12)], &[kek(0x12)], OtherSlots::Discard).unwrap();
757 assert_ne!(
758 &out.bytes[lo + SLOT_LEN..hi],
759 &out2.bytes[lo + SLOT_LEN..hi],
760 "两次改写的随机填充不应相同"
761 );
762 }
763
764 #[test]
766 fn header_len_still_consistent() {
767 let a = kek(0x34);
768 let data = sample(&[a.duplicate()]);
769 let out =
770 rewrite_slots(&data, &[a.duplicate()], &[a, kek(0x56)], OtherSlots::Carry).unwrap();
771 let h = FixedHeader::parse(&out.bytes).unwrap();
772 h.validate().unwrap();
773 assert!(out.bytes.len() >= h.header_len as usize);
774 }
775
776 #[test]
786 fn carry_preserves_unknown_slot_across_password_change() {
787 let main = kek(0x41);
788 let reco = kek(0x42); let fresh = kek(0x43);
790 let data = sample(&[main.duplicate(), reco.duplicate()]);
791 assert!(crate::file::open(&data, &[reco.duplicate()]).is_ok(), "基线:恢复码应可用");
793
794 let out =
795 rewrite_slots(&data, &[main.duplicate()], &[fresh.duplicate()], OtherSlots::Carry)
796 .unwrap();
797
798 assert!(crate::file::open(&out.bytes, &[fresh]).is_ok(), "新密码必须可用");
799 assert!(
800 crate::file::open(&out.bytes, &[reco]).is_ok(),
801 "没参与重建的槽必须被原样保住——这是本次改动的全部意义"
802 );
803 assert!(
804 crate::file::open(&out.bytes, &[main]).is_err(),
805 "旧主密码必须失效,否则 change 是假的"
806 );
807 }
808
809 #[test]
814 fn carried_slot_decrypts_correct_plaintext() {
815 let main = kek(0x44);
816 let reco = kek(0x45);
817 let fresh = kek(0x46);
818 let plain: Vec<u8> = (0..5000u32).map(|i| (i % 251) as u8).collect();
819 let data = sample(&[main.duplicate(), reco.duplicate()]);
820
821 let out = rewrite_slots(&data, &[main], &[fresh], OtherSlots::Carry).unwrap();
822 let opened = crate::file::open(&out.bytes, &[reco]).unwrap();
823 assert_eq!(opened.decrypt_all(&out.bytes).unwrap(), plain, "搬运后明文必须原样");
824 }
825
826 #[test]
832 fn carry_keeps_slot_indices_aligned() {
833 let main = kek(0x47);
834 let far = kek(0x48);
835 let fresh = kek(0x49);
836 let filler = kek(0x4A);
839 let data = sample(&[main.duplicate(), filler, far.duplicate()]);
840 assert!(crate::file::open(&data, &[far.duplicate()]).is_ok(), "基线");
841
842 let out = rewrite_slots(&data, &[main], &[fresh], OtherSlots::Carry).unwrap();
843
844 assert!(
845 crate::file::open(&out.bytes, &[far]).is_ok(),
846 "slot 2 的槽必须仍在 slot 2 上,压缩到前面会让它永远解不开"
847 );
848
849 let (lo, _) = slot_area_range();
851 let s2 = lo + SLOT_LEN * 2;
852 assert_eq!(
853 &data[s2..s2 + SLOT_LEN],
854 &out.bytes[s2..s2 + SLOT_LEN],
855 "slot 2 必须逐字节原样"
856 );
857 }
858
859 #[test]
864 fn discard_really_wipes_other_slots() {
865 let main = kek(0x4B);
866 let other = kek(0x4C);
867 let data = sample(&[main.duplicate(), other.duplicate()]);
868
869 let out =
870 rewrite_slots(&data, &[main.duplicate()], &[main], OtherSlots::Discard).unwrap();
871
872 let err = crate::file::open(&out.bytes, &[other]).unwrap_err();
873 assert!(matches!(err, Error::NoMatchingSlot), "Discard 必须让其它密码失效,实际 {err:?}");
874 }
875
876 #[test]
881 fn carry_and_discard_differ_observably() {
882 let a = kek(0x4D);
883 let b = kek(0x4E);
884 let data = sample(&[a.duplicate(), b]);
885
886 let carried =
887 rewrite_slots(&data, &[a.duplicate()], &[a.duplicate()], OtherSlots::Carry).unwrap();
888 let wiped =
889 rewrite_slots(&data, &[a.duplicate()], &[a], OtherSlots::Discard).unwrap();
890
891 assert_eq!(carried.carried_opaque, 7, "保留了 8-1=7 个来历不明的槽");
892 assert_eq!(wiped.carried_opaque, 0, "清场不搬运任何槽");
893
894 let (lo, hi) = slot_area_range();
895 assert_ne!(
896 &carried.bytes[lo..hi],
897 &wiped.bytes[lo..hi],
898 "两种策略必须产出不同的 slot 区"
899 );
900 assert_eq!(
902 &data[lo + SLOT_LEN..hi],
903 &carried.bytes[lo + SLOT_LEN..hi],
904 "Carry 下未涉及的槽必须逐字节不变"
905 );
906 }
907
908 #[test]
913 fn carry_preserves_length_and_slot_area_size() {
914 let a = kek(0x4F);
915 let data = sample(&[a.duplicate()]);
916 let out = rewrite_slots(&data, &[a.duplicate()], &[a], OtherSlots::Carry).unwrap();
917
918 assert_eq!(out.bytes.len(), data.len(), "文件长度不得改变");
919 let (lo, hi) = slot_area_range();
920 assert_eq!(hi - lo, SLOT_LEN * 8, "slot 区必须恒为 8 槽");
921 let h = FixedHeader::parse(&out.bytes).unwrap();
922 h.validate().unwrap();
923 }
924
925 #[test]
937 fn carry_reports_possible_eviction_when_keep_grows() {
938 let main = kek(0x51);
939 let reco = kek(0x52); let extra = kek(0x53);
941 let data = sample(&[main.duplicate(), reco.duplicate()]);
942
943 let grown = rewrite_slots(
945 &data,
946 &[main.duplicate()],
947 &[main.duplicate(), extra],
948 OtherSlots::Carry,
949 )
950 .unwrap();
951 assert!(grown.may_have_evicted, "keep 增长必须如实报告可能顶掉了槽");
952 assert!(
953 crate::file::open(&grown.bytes, &[reco.duplicate()]).is_err(),
954 "确认这个边界真实存在——若哪天能保住了,本断言会提醒来更新文档"
955 );
956
957 let same = rewrite_slots(&data, &[main.duplicate()], &[kek(0x54)], OtherSlots::Carry)
959 .unwrap();
960 assert!(!same.may_have_evicted, "只保留一个密码时不该报告顶掉");
961 assert!(
962 crate::file::open(&same.bytes, &[reco]).is_ok(),
963 "change 必须保住 slot 1 上的恢复码"
964 );
965 }
966
967 #[test]
969 fn discard_never_reports_eviction() {
970 let a = kek(0x55);
971 let b = kek(0x56);
972 let data = sample(&[a.duplicate(), b.duplicate()]);
973 let out =
974 rewrite_slots(&data, &[a.duplicate()], &[a, b], OtherSlots::Discard).unwrap();
975 assert!(
976 !out.may_have_evicted,
977 "Discard 的语义就是作废其它全部,不存在「意外顶掉」这回事"
978 );
979 }
980}