1use std::collections::BTreeMap;
23
24use crate::error::{Error, Result};
25use crate::index::{IndexBlob, IndexValue};
26use crate::schema::{AttributeFormat, RecordType, Relation, Schema};
27
28pub const SIGNATURE: &[u8; 4] = b"kych";
29
30pub const VERSION: u32 = 0x0001_0000;
32
33pub const HEADER_SIZE_FIELD: u32 = 16;
35
36pub const HEADER_LEN: usize = 20;
37pub const TABLE_HEADER_LEN: usize = 28;
38pub const RECORD_HEADER_LEN: usize = 24;
39
40const ATTRIBUTE_OFFSET_FLAG: u32 = 1;
42
43const SLOT_FREE_FLAG: u32 = 1;
48
49#[derive(Debug, Clone)]
51pub struct Keychain {
52 pub version: u32,
53 pub header_size: u32,
55 pub auth_offset: u32,
57 pub tables: Vec<Table>,
60 pub commit_version: Option<u32>,
66}
67
68#[derive(Debug, Clone)]
69pub struct Table {
70 pub record_type: RecordType,
71 pub unknown_free_list: u32,
76 pub slots: Vec<Slot>,
78 pub indexes: TableIndexes,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84pub enum Slot {
85 Record(Record),
87 Free(u32),
90 Empty,
92}
93
94impl Slot {
95 pub fn record(&self) -> Option<&Record> {
96 match self {
97 Self::Record(record) => Some(record),
98 _ => None,
99 }
100 }
101
102 pub fn record_mut(&mut self) -> Option<&mut Record> {
103 match self {
104 Self::Record(record) => Some(record),
105 _ => None,
106 }
107 }
108
109 pub fn is_empty(&self) -> bool {
110 matches!(self, Self::Empty)
111 }
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
116pub enum TableIndexes {
117 Parsed(IndexBlob),
118 Raw(Vec<u8>),
121}
122
123impl TableIndexes {
124 pub fn to_bytes(&self, table_offset: usize) -> Vec<u8> {
127 match self {
128 Self::Parsed(blob) => blob.to_bytes(table_offset),
129 Self::Raw(bytes) => bytes.clone(),
130 }
131 }
132
133 pub fn len(&self) -> usize {
134 match self {
135 Self::Parsed(blob) => blob.encoded_len(),
136 Self::Raw(bytes) => bytes.len(),
137 }
138 }
139
140 pub fn is_empty(&self) -> bool {
141 self.len() == 0
142 }
143
144 pub fn blob(&self) -> Option<&IndexBlob> {
145 match self {
146 Self::Parsed(blob) => Some(blob),
147 Self::Raw(_) => None,
148 }
149 }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct Record {
154 pub number: u32,
156 pub version: u32,
161 pub unknown3: u32,
164 pub unknown5: u32,
166 pub key_data: Vec<u8>,
169 pub attributes: Vec<Option<Value>>,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq)]
176pub enum Value {
177 String(Vec<u8>),
179 Sint32(i32),
180 Uint32(u32),
181 Date(Vec<u8>),
183 Blob(Vec<u8>),
186}
187
188impl Value {
189 pub fn as_bytes(&self) -> Option<&[u8]> {
190 match self {
191 Self::String(bytes) | Self::Blob(bytes) | Self::Date(bytes) => Some(bytes),
192 _ => None,
193 }
194 }
195
196 pub fn as_u32(&self) -> Option<u32> {
197 match self {
198 Self::Uint32(value) => Some(*value),
199 Self::Sint32(value) => Some(*value as u32),
200 _ => None,
201 }
202 }
203
204 pub fn to_display_string(&self) -> String {
206 match self {
207 Self::Sint32(value) => value.to_string(),
208 Self::Uint32(value) => value.to_string(),
209 Self::Date(bytes) | Self::String(bytes) | Self::Blob(bytes) => {
210 let trimmed = trim_nul(bytes);
211 match std::str::from_utf8(trimmed) {
212 Ok(text) if trimmed.iter().all(|b| !b.is_ascii_control()) => text.to_string(),
213 _ => format!("0x{}", hex::encode(trimmed)),
214 }
215 }
216 }
217 }
218
219 fn encoded_len(&self) -> usize {
220 match self {
221 Self::Sint32(_) | Self::Uint32(_) => 4,
222 Self::Date(_) => 16,
223 Self::String(bytes) | Self::Blob(bytes) => pad4(4 + bytes.len()),
224 }
225 }
226
227 fn write(&self, out: &mut Vec<u8>) {
228 match self {
229 Self::Sint32(value) => out.extend_from_slice(&value.to_be_bytes()),
230 Self::Uint32(value) => out.extend_from_slice(&value.to_be_bytes()),
231 Self::Date(bytes) => {
232 let mut field = [0u8; 16];
233 let take = bytes.len().min(16);
234 field[..take].copy_from_slice(&bytes[..take]);
235 out.extend_from_slice(&field);
236 }
237 Self::String(bytes) | Self::Blob(bytes) => {
238 out.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
239 out.extend_from_slice(bytes);
240 out.resize(pad4(out.len()), 0);
241 }
242 }
243 }
244}
245
246pub fn trim_nul(bytes: &[u8]) -> &[u8] {
247 match bytes.iter().position(|b| *b == 0) {
248 Some(end) => &bytes[..end],
249 None => bytes,
250 }
251}
252
253fn pad4(len: usize) -> usize {
254 (len + 3) & !3
255}
256
257struct Cursor<'a> {
263 data: &'a [u8],
264}
265
266impl<'a> Cursor<'a> {
267 fn u32(&self, at: usize) -> Result<u32> {
268 let end = at.checked_add(4).ok_or_else(|| Error::truncated(at, 4))?;
269 let bytes = self
270 .data
271 .get(at..end)
272 .ok_or_else(|| Error::truncated(at, 4))?;
273 Ok(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
274 }
275
276 fn bytes(&self, at: usize, len: usize) -> Result<&'a [u8]> {
277 let end = at
278 .checked_add(len)
279 .ok_or_else(|| Error::truncated(at, len))?;
280 self.data
281 .get(at..end)
282 .ok_or_else(|| Error::truncated(at, len))
283 }
284}
285
286impl Keychain {
287 pub fn parse(data: &[u8]) -> Result<Self> {
289 let cursor = Cursor { data };
290
291 if data.len() < HEADER_LEN {
292 return Err(Error::truncated(0, HEADER_LEN));
293 }
294 if &data[..4] != SIGNATURE {
295 return Err(Error::NotAKeychain);
296 }
297
298 let version = cursor.u32(4)?;
299 let header_size = cursor.u32(8)?;
300 let tables_offset = cursor.u32(12)? as usize;
301 let auth_offset = cursor.u32(16)?;
302
303 let tables_size = cursor.u32(tables_offset)? as usize;
307 let commit_version = cursor.u32(tables_offset + tables_size).ok();
308
309 let table_count = cursor.u32(tables_offset + 4)? as usize;
310 let mut table_offsets = Vec::with_capacity(table_count);
311 for index in 0..table_count {
312 table_offsets.push(cursor.u32(tables_offset + 8 + index * 4)? as usize);
313 }
314
315 let bootstrap = Schema::bootstrap();
320 let mut schema_tables = Vec::new();
321 for offset in &table_offsets {
322 let at = tables_offset + offset;
323 let record_type = RecordType(cursor.u32(at + 4)?);
324 if bootstrap.relation(record_type).is_some() {
325 schema_tables.push(Table::parse(&cursor, at, &bootstrap)?);
326 }
327 }
328 let schema = Schema::from_tables(&schema_tables)?;
329
330 let mut tables = Vec::with_capacity(table_count);
331 for offset in &table_offsets {
332 tables.push(Table::parse(&cursor, tables_offset + offset, &schema)?);
333 }
334
335 Ok(Self {
336 version,
337 header_size,
338 auth_offset,
339 tables,
340 commit_version,
341 })
342 }
343
344 pub fn schema(&self) -> Result<Schema> {
346 Schema::from_tables(&self.tables)
347 }
348
349 pub fn bump_commit_version(&mut self) {
351 self.commit_version = Some(self.commit_version.unwrap_or(0) + 1);
352 }
353
354 pub fn table(&self, record_type: RecordType) -> Option<&Table> {
355 self.tables
356 .iter()
357 .find(|table| table.record_type == record_type)
358 }
359
360 pub fn table_mut(&mut self, record_type: RecordType) -> Option<&mut Table> {
361 self.tables
362 .iter_mut()
363 .find(|table| table.record_type == record_type)
364 }
365
366 pub fn to_bytes(&self) -> Result<Vec<u8>> {
368 let mut tables = Vec::with_capacity(self.tables.len());
369 for table in &self.tables {
370 tables.push(table.to_bytes()?);
371 }
372
373 let array_header = 8 + 4 * tables.len();
375 let mut offsets = Vec::with_capacity(tables.len());
376 let mut running = array_header;
377 for table in &tables {
378 offsets.push(running as u32);
379 running += table.len();
380 }
381
382 let mut out = Vec::with_capacity(HEADER_LEN + running);
383 out.extend_from_slice(SIGNATURE);
384 out.extend_from_slice(&self.version.to_be_bytes());
385 out.extend_from_slice(&self.header_size.to_be_bytes());
386 out.extend_from_slice(&(HEADER_LEN as u32).to_be_bytes());
387 out.extend_from_slice(&self.auth_offset.to_be_bytes());
388
389 out.extend_from_slice(&(running as u32).to_be_bytes());
390 out.extend_from_slice(&(tables.len() as u32).to_be_bytes());
391 for offset in offsets {
392 out.extend_from_slice(&offset.to_be_bytes());
393 }
394 for table in tables {
395 out.extend_from_slice(&table);
396 }
397 if let Some(commit_version) = self.commit_version {
398 out.extend_from_slice(&commit_version.to_be_bytes());
399 }
400 Ok(out)
401 }
402}
403
404impl Table {
405 fn parse(cursor: &Cursor<'_>, at: usize, schema: &Schema) -> Result<Self> {
406 let _size = cursor.u32(at)?;
407 let record_type = RecordType(cursor.u32(at + 4)?);
408 let _record_count = cursor.u32(at + 8)?;
409 let _records_offset = cursor.u32(at + 12)?;
410 let indexes_offset = cursor.u32(at + 16)? as usize;
411 let unknown_free_list = cursor.u32(at + 20)?;
412 let slot_count = cursor.u32(at + 24)? as usize;
413 if slot_count > 1 << 20 {
414 return Err(Error::format(format!(
415 "table 0x{:08x} claims {slot_count} record slots",
416 record_type.0
417 )));
418 }
419
420 let mut slots = Vec::with_capacity(slot_count);
421 for index in 0..slot_count {
422 let offset = cursor.u32(at + TABLE_HEADER_LEN + index * 4)?;
423 if offset == 0 {
424 slots.push(Slot::Empty);
425 continue;
426 }
427 if offset & SLOT_FREE_FLAG != 0 {
428 slots.push(Slot::Free(offset));
429 continue;
430 }
431 let offset = offset as usize;
432 if offset < TABLE_HEADER_LEN || offset >= indexes_offset {
433 return Err(Error::format(format!(
434 "table 0x{:08x} slot {index} points to {offset}, outside its records",
435 record_type.0
436 )));
437 }
438 slots.push(Slot::Record(Record::parse(
439 cursor,
440 at + offset,
441 record_type,
442 schema,
443 )?));
444 }
445
446 let size = _size as usize;
447 if indexes_offset > size {
448 return Err(Error::format(format!(
449 "table 0x{:08x} index offset {indexes_offset} is past its {size}-byte extent",
450 record_type.0
451 )));
452 }
453 let index_data = cursor.bytes(at + indexes_offset, size - indexes_offset)?;
454
455 let relation = schema.relation(record_type);
458 let indexes = match IndexBlob::parse(index_data, indexes_offset, relation) {
459 Ok(blob) if blob.to_bytes(indexes_offset) == index_data => TableIndexes::Parsed(blob),
460 _ => TableIndexes::Raw(index_data.to_vec()),
461 };
462
463 Ok(Self {
464 record_type,
465 unknown_free_list,
466 slots,
467 indexes,
468 })
469 }
470
471 pub fn records(&self) -> impl Iterator<Item = &Record> {
473 self.slots.iter().filter_map(Slot::record)
474 }
475
476 pub fn records_mut(&mut self) -> impl Iterator<Item = &mut Record> {
477 self.slots.iter_mut().filter_map(Slot::record_mut)
478 }
479
480 pub fn record_count(&self) -> usize {
481 self.records().count()
482 }
483
484 pub fn next_record_number(&self) -> u32 {
486 self.records()
487 .map(|record| record.number)
488 .max()
489 .map_or(0, |max| max + 1)
490 }
491
492 pub fn insert(&mut self, record: Record) {
497 match self.slots.iter_mut().find(|slot| slot.is_empty()) {
498 Some(slot) => *slot = Slot::Record(record),
499 None => self.slots.push(Slot::Record(record)),
500 }
501 self.unknown_free_list = 0;
504 }
505
506 pub fn unique_index_attribute_ids(&self) -> Option<&[u32]> {
511 self.indexes
512 .blob()?
513 .indexes
514 .iter()
515 .find(|index| index.kind == 1)
516 .map(|index| index.attribute_ids.as_slice())
517 }
518
519 pub fn has_record_with_unique_key(
528 &self,
529 relation: &Relation,
530 attributes: &[Option<Value>],
531 ) -> bool {
532 let Some(blob) = self.indexes.blob() else {
533 return false;
534 };
535 let Some(unique) = blob.indexes.iter().find(|index| index.kind == 1) else {
536 return false;
537 };
538 let positions: Vec<usize> = unique
541 .attribute_ids
542 .iter()
543 .filter_map(|id| {
544 relation
545 .attributes
546 .iter()
547 .position(|attribute| attribute.id == *id)
548 })
549 .collect();
550 if positions.len() != unique.attribute_ids.len() {
551 return false;
552 }
553
554 let key_of = |values: &[Option<Value>]| -> Vec<Option<Value>> {
555 positions
556 .iter()
557 .map(|position| values.get(*position).cloned().flatten())
558 .collect()
559 };
560 let wanted = key_of(attributes);
561 self.records()
562 .any(|record| key_of(&record.attributes) == wanted)
563 }
564
565 pub fn rebuild_indexes(&mut self, relation: &Relation) -> Result<()> {
571 let TableIndexes::Parsed(blob) = &mut self.indexes else {
572 return Err(Error::format(format!(
573 "table 0x{:08x} has an index region this build cannot rewrite",
574 self.record_type.0
575 )));
576 };
577
578 let positions: Vec<(u32, usize, AttributeFormat)> = relation
580 .attributes
581 .iter()
582 .enumerate()
583 .map(|(position, attribute)| (attribute.id, position, attribute.format))
584 .collect();
585
586 for index in &mut blob.indexes {
587 index.entries.clear();
588 }
589 for record in self.slots.iter().filter_map(Slot::record) {
590 let position_of = |attribute_id: u32| -> Option<usize> {
591 positions
592 .iter()
593 .find(|(id, _, _)| *id == attribute_id)
594 .map(|(_, position, _)| *position)
595 };
596 let key = |attribute_id: u32| -> IndexValue {
597 match positions.iter().find(|(id, _, _)| *id == attribute_id) {
598 Some((_, position, format)) => {
599 IndexValue::from_value(record.attribute(*position), *format)
600 }
601 None => IndexValue::Bytes(Vec::new()),
602 }
603 };
604 blob.insert_record_where(record.number, key, |attribute_ids: &[u32]| -> bool {
607 attribute_ids.iter().all(|id| {
608 position_of(*id).is_some_and(|position| record.attribute(position).is_some())
609 })
610 });
611 }
612 Ok(())
613 }
614
615 fn to_bytes(&self) -> Result<Vec<u8>> {
616 let mut records = Vec::with_capacity(self.slots.len());
617 for slot in &self.slots {
618 records.push(match slot {
619 Slot::Record(record) => Some(record.to_bytes()?),
620 Slot::Free(_) | Slot::Empty => None,
621 });
622 }
623
624 let records_offset = TABLE_HEADER_LEN + 4 * self.slots.len();
625 let mut slot_offsets = Vec::with_capacity(records.len());
626 let mut running = records_offset;
627 for (slot, record) in self.slots.iter().zip(&records) {
628 match (slot, record) {
629 (Slot::Record(_), Some(bytes)) => {
630 slot_offsets.push(running as u32);
631 running += bytes.len();
632 }
633 (Slot::Free(value), _) => slot_offsets.push(*value),
635 _ => slot_offsets.push(0),
636 }
637 }
638 let indexes_offset = running;
639 let index_data = self.indexes.to_bytes(indexes_offset);
640 let size = indexes_offset + index_data.len();
641
642 let mut out = Vec::with_capacity(size);
643 out.extend_from_slice(&(size as u32).to_be_bytes());
644 out.extend_from_slice(&self.record_type.0.to_be_bytes());
645 out.extend_from_slice(&(self.record_count() as u32).to_be_bytes());
646 out.extend_from_slice(&(records_offset as u32).to_be_bytes());
647 out.extend_from_slice(&(indexes_offset as u32).to_be_bytes());
648 out.extend_from_slice(&self.unknown_free_list.to_be_bytes());
649 out.extend_from_slice(&(self.slots.len() as u32).to_be_bytes());
650 for offset in slot_offsets {
651 out.extend_from_slice(&offset.to_be_bytes());
652 }
653 for record in records.into_iter().flatten() {
654 out.extend_from_slice(&record);
655 }
656 out.extend_from_slice(&index_data);
657 Ok(out)
658 }
659}
660
661impl Record {
662 fn parse(
663 cursor: &Cursor<'_>,
664 at: usize,
665 record_type: RecordType,
666 schema: &Schema,
667 ) -> Result<Self> {
668 let size = cursor.u32(at)? as usize;
669 let number = cursor.u32(at + 4)?;
670 let version = cursor.u32(at + 8)?;
671 let unknown3 = cursor.u32(at + 12)?;
672 let key_data_size = cursor.u32(at + 16)? as usize;
673 let unknown5 = cursor.u32(at + 20)?;
674
675 let formats = schema.attribute_formats(record_type);
676 let mut attributes = Vec::with_capacity(formats.len());
677 let mut offsets = Vec::with_capacity(formats.len());
678 for index in 0..formats.len() {
679 offsets.push(cursor.u32(at + RECORD_HEADER_LEN + index * 4)?);
680 }
681
682 let key_data_at = at + RECORD_HEADER_LEN + 4 * formats.len();
683 let key_data = cursor.bytes(key_data_at, key_data_size)?.to_vec();
684
685 for (offset, format) in offsets.iter().zip(&formats) {
686 if *offset == 0 {
687 attributes.push(None);
688 continue;
689 }
690 let value_at = at + (*offset & !ATTRIBUTE_OFFSET_FLAG) as usize;
691 if value_at >= at + size {
692 return Err(Error::format(format!(
693 "record {number} in table 0x{:08x} points an attribute past its extent",
694 record_type.0
695 )));
696 }
697 attributes.push(Some(read_value(cursor, value_at, *format)?));
698 }
699
700 Ok(Self {
701 number,
702 version,
703 unknown3,
704 unknown5,
705 key_data,
706 attributes,
707 })
708 }
709
710 pub fn attribute(&self, index: usize) -> Option<&Value> {
711 self.attributes.get(index).and_then(Option::as_ref)
712 }
713
714 fn to_bytes(&self) -> Result<Vec<u8>> {
715 let count = self.attributes.len();
716 let header_and_offsets = RECORD_HEADER_LEN + 4 * count;
717 let key_data_len = pad4(self.key_data.len());
718
719 let mut offsets = Vec::with_capacity(count);
721 let mut running = header_and_offsets + key_data_len;
722 for attribute in &self.attributes {
723 match attribute {
724 Some(value) => {
725 offsets.push(running as u32 | ATTRIBUTE_OFFSET_FLAG);
726 running += value.encoded_len();
727 }
728 None => offsets.push(0),
729 }
730 }
731 let size = running;
732
733 let mut out = Vec::with_capacity(size);
734 out.extend_from_slice(&(size as u32).to_be_bytes());
735 out.extend_from_slice(&self.number.to_be_bytes());
736 out.extend_from_slice(&self.version.to_be_bytes());
737 out.extend_from_slice(&self.unknown3.to_be_bytes());
738 out.extend_from_slice(&(self.key_data.len() as u32).to_be_bytes());
739 out.extend_from_slice(&self.unknown5.to_be_bytes());
740 for offset in offsets {
741 out.extend_from_slice(&offset.to_be_bytes());
742 }
743 out.extend_from_slice(&self.key_data);
744 out.resize(header_and_offsets + key_data_len, 0);
745 for value in self.attributes.iter().flatten() {
746 value.write(&mut out);
747 }
748
749 debug_assert_eq!(
750 out.len(),
751 size,
752 "record serialization disagreed with its own layout"
753 );
754 Ok(out)
755 }
756}
757
758fn read_value(cursor: &Cursor<'_>, at: usize, format: AttributeFormat) -> Result<Value> {
759 Ok(match format {
760 AttributeFormat::Sint32 => Value::Sint32(cursor.u32(at)? as i32),
761 AttributeFormat::Uint32 => Value::Uint32(cursor.u32(at)?),
762 AttributeFormat::TimeDate => Value::Date(cursor.bytes(at, 16)?.to_vec()),
763 AttributeFormat::String => {
764 let len = cursor.u32(at)? as usize;
765 Value::String(cursor.bytes(at + 4, len)?.to_vec())
766 }
767 _ => {
769 let len = cursor.u32(at)? as usize;
770 Value::Blob(cursor.bytes(at + 4, len)?.to_vec())
771 }
772 })
773}
774
775pub fn records_by_type(keychain: &Keychain) -> BTreeMap<RecordType, Vec<&Record>> {
777 let mut out = BTreeMap::new();
778 for table in &keychain.tables {
779 out.insert(table.record_type, table.records().collect());
780 }
781 out
782}
783
784#[cfg(test)]
785mod tests {
786 use super::*;
787
788 #[test]
789 fn padding_rounds_up_to_four() {
790 assert_eq!(pad4(0), 0);
791 assert_eq!(pad4(1), 4);
792 assert_eq!(pad4(4), 4);
793 assert_eq!(pad4(13), 16);
794 }
795
796 #[test]
797 fn rejects_files_that_are_not_keychains() {
798 assert!(matches!(
799 Keychain::parse(b"not a keychain at all!!!"),
800 Err(Error::NotAKeychain)
801 ));
802 assert!(matches!(
803 Keychain::parse(b"kych"),
804 Err(Error::Truncated { .. })
805 ));
806 }
807
808 #[test]
809 fn value_encoding_lengths_match_the_layout_rules() {
810 assert_eq!(Value::Uint32(7).encoded_len(), 4);
811 assert_eq!(Value::Date(b"20260725095125Z\0".to_vec()).encoded_len(), 16);
812 assert_eq!(Value::Blob(b"alice".to_vec()).encoded_len(), 12);
814 assert_eq!(Value::Blob(b"myservice".to_vec()).encoded_len(), 16);
815 assert_eq!(Value::Blob(Vec::new()).encoded_len(), 4);
816 }
817
818 #[test]
819 fn values_serialize_with_prefix_and_padding() {
820 let mut out = Vec::new();
821 Value::Blob(b"alice".to_vec()).write(&mut out);
822 assert_eq!(out, b"\x00\x00\x00\x05alice\0\0\0");
823
824 let mut out = Vec::new();
825 Value::Date(b"20260725095125Z\0".to_vec()).write(&mut out);
826 assert_eq!(out.len(), 16);
827 assert_eq!(&out, b"20260725095125Z\0");
828
829 let mut out = Vec::new();
831 Value::Date(b"2026".to_vec()).write(&mut out);
832 assert_eq!(out, b"2026\0\0\0\0\0\0\0\0\0\0\0\0");
833 }
834
835 #[test]
836 fn display_prefers_text_and_falls_back_to_hex() {
837 assert_eq!(
838 Value::Blob(b"alice\0".to_vec()).to_display_string(),
839 "alice"
840 );
841 assert_eq!(Value::Uint32(8080).to_display_string(), "8080");
842 assert_eq!(Value::Blob(vec![0xff, 0xfe]).to_display_string(), "0xfffe");
843 assert_eq!(
844 Value::Date(b"20260725095125Z\0".to_vec()).to_display_string(),
845 "20260725095125Z"
846 );
847 }
848
849 #[test]
850 fn trim_nul_stops_at_the_first_terminator() {
851 assert_eq!(trim_nul(b"abc\0def"), b"abc");
852 assert_eq!(trim_nul(b"abc"), b"abc");
853 assert_eq!(trim_nul(b"\0"), b"");
854 }
855
856 #[test]
857 fn empty_slots_are_reused_before_growing_the_table() {
858 let record = |number| Record {
859 number,
860 version: 0,
861 unknown3: 0,
862 unknown5: 0,
863 key_data: Vec::new(),
864 attributes: vec![None],
865 };
866 let mut table = Table {
867 record_type: RecordType::GENERIC_PASSWORD,
868 unknown_free_list: 0,
869 slots: vec![
870 Slot::Record(record(0)),
871 Slot::Empty,
872 Slot::Record(record(2)),
873 ],
874 indexes: TableIndexes::Parsed(IndexBlob {
875 indexes: Vec::new(),
876 }),
877 };
878
879 assert_eq!(table.record_count(), 2);
880 assert_eq!(table.next_record_number(), 3);
881
882 table.insert(record(3));
883 assert_eq!(table.slots.len(), 3, "should have filled the hole");
884 assert!(table.slots[1].record().is_some());
885
886 table.insert(record(4));
887 assert_eq!(table.slots.len(), 4, "no hole left, so the table grows");
888 }
889
890 #[test]
891 fn free_list_slots_are_neither_records_nor_reusable() {
892 let record = |number| Record {
893 number,
894 version: 0,
895 unknown3: 0,
896 unknown5: 0,
897 key_data: Vec::new(),
898 attributes: vec![None],
899 };
900 let mut table = Table {
901 record_type: RecordType::X509_CERTIFICATE,
902 unknown_free_list: 0x431,
903 slots: vec![Slot::Record(record(0)), Slot::Free(65), Slot::Free(73)],
906 indexes: TableIndexes::Parsed(IndexBlob {
907 indexes: Vec::new(),
908 }),
909 };
910
911 assert_eq!(table.record_count(), 1, "free slots are not records");
912 assert_eq!(table.next_record_number(), 1);
913
914 table.insert(record(1));
916 assert_eq!(table.slots.len(), 4);
917 assert_eq!(table.slots[1], Slot::Free(65));
918 assert_eq!(table.slots[2], Slot::Free(73));
919
920 let bytes = table.to_bytes().unwrap();
922 let offsets: Vec<u32> = (0..4)
923 .map(|index| {
924 let at = TABLE_HEADER_LEN + index * 4;
925 u32::from_be_bytes(bytes[at..at + 4].try_into().unwrap())
926 })
927 .collect();
928 assert_eq!(offsets[1], 65);
929 assert_eq!(offsets[2], 73);
930 }
931}