Skip to main content

kc/
index.rs

1//! Table indexes: the sorted lookup structures in the trailing part of a table.
2//!
3//! Not described by the dtformats specification, and *not* opaque. Two things
4//! make it impossible to treat this region as bytes to be copied:
5//!
6//! * Every offset in it is relative to the start of the *table*, so inserting a
7//!   record moves the index region and invalidates all of them. macOS then
8//!   follows a stale offset into the middle of a record and reports
9//!   `errSecNoSuchAttr`.
10//! * It holds one entry per record. A record with no index entry is not
11//!   findable through the Security framework.
12//!
13//! ```text
14//! region  := size, count, count x index offset
15//! index   := size, id, kind, attribute count, attribute ids,
16//!            entry count, entry offsets, record numbers, entries
17//! entry   := payload size (excluding itself), key values
18//! value   := four raw bytes for an integer attribute,
19//!            else length prefix and bytes padded to four
20//! ```
21//!
22//! Entries are sorted by key; the record-number array is in the same order, so
23//! together they map a sorted key to a record. `tests/keychain_index.rs` parses
24//! every index region in keychains written by macOS, re-serializes them byte for
25//! byte, and checks that rebuilding an index from scratch reproduces macOS's
26//! ordering.
27
28use std::cmp::Ordering;
29
30use crate::error::{Error, Result};
31use crate::format::Value;
32use crate::schema::{AttributeFormat, Relation};
33
34/// One key value inside an index entry.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum IndexValue {
37    /// An integer attribute: four raw bytes, no length prefix.
38    Integer(u32),
39    /// Anything else: length-prefixed bytes, padded to a 4-byte boundary.
40    Bytes(Vec<u8>),
41}
42
43impl IndexValue {
44    /// The key form of an attribute value, given the attribute's format.
45    pub fn from_value(value: Option<&Value>, format: AttributeFormat) -> Self {
46        match (value, format) {
47            (None, AttributeFormat::Sint32 | AttributeFormat::Uint32) => Self::Integer(0),
48            (None, _) => Self::Bytes(Vec::new()),
49            (Some(Value::Uint32(number)), _) => Self::Integer(*number),
50            (Some(Value::Sint32(number)), _) => Self::Integer(*number as u32),
51            (Some(Value::Date(bytes) | Value::String(bytes) | Value::Blob(bytes)), _) => {
52                Self::Bytes(bytes.clone())
53            }
54        }
55    }
56
57    fn encoded_len(&self) -> usize {
58        match self {
59            Self::Integer(_) => 4,
60            Self::Bytes(bytes) => pad4(4 + bytes.len()),
61        }
62    }
63
64    fn write(&self, out: &mut Vec<u8>) {
65        match self {
66            Self::Integer(number) => out.extend_from_slice(&number.to_be_bytes()),
67            Self::Bytes(bytes) => {
68                out.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
69                out.extend_from_slice(bytes);
70                out.resize(pad4(out.len()), 0);
71            }
72        }
73    }
74}
75
76impl Ord for IndexValue {
77    fn cmp(&self, other: &Self) -> Ordering {
78        match (self, other) {
79            (Self::Integer(left), Self::Integer(right)) => left.cmp(right),
80            (Self::Bytes(left), Self::Bytes(right)) => left.cmp(right),
81            // Mixed formats never occur within one index.
82            (Self::Integer(_), Self::Bytes(_)) => Ordering::Less,
83            (Self::Bytes(_), Self::Integer(_)) => Ordering::Greater,
84        }
85    }
86}
87
88impl PartialOrd for IndexValue {
89    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
90        Some(self.cmp(other))
91    }
92}
93
94/// One indexed record: its key, and the record it points at.
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct IndexEntry {
97    pub record_number: u32,
98    pub key: Vec<IndexValue>,
99}
100
101impl IndexEntry {
102    /// Size as stored, which excludes the size word itself.
103    fn payload_len(&self) -> usize {
104        self.key.iter().map(IndexValue::encoded_len).sum()
105    }
106
107    fn encoded_len(&self) -> usize {
108        4 + self.payload_len()
109    }
110}
111
112/// One index over one or more attributes of a relation.
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct Index {
115    /// Matches `IndexID` in the schema's index table.
116    pub id: u32,
117    /// Matches `IndexType`: `1` for the relation's unique index, `0` otherwise.
118    pub kind: u32,
119    /// The attributes indexed, by id.
120    pub attribute_ids: Vec<u32>,
121    /// Entries, sorted by key.
122    pub entries: Vec<IndexEntry>,
123}
124
125impl Index {
126    /// Words before the entry section: size, id, kind, count, ids.
127    fn header_len(&self) -> usize {
128        4 * (4 + self.attribute_ids.len())
129    }
130
131    fn encoded_len(&self) -> usize {
132        self.header_len()
133            + 4                                   // entry count
134            + 8 * self.entries.len()              // offsets, then record numbers
135            + self.entries.iter().map(IndexEntry::encoded_len).sum::<usize>()
136    }
137
138    /// Insert in sorted position, keeping the entry and record-number arrays in
139    /// step.
140    fn insert(&mut self, entry: IndexEntry) {
141        let at = self
142            .entries
143            .iter()
144            .position(|existing| existing.key > entry.key)
145            .unwrap_or(self.entries.len());
146        self.entries.insert(at, entry);
147    }
148}
149
150/// The index region of one table.
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct IndexBlob {
153    pub indexes: Vec<Index>,
154}
155
156impl IndexBlob {
157    /// Parse the region.
158    ///
159    /// `table_offset` is where the region sits within its table, which the stored
160    /// offsets are measured against. `relation` supplies the attribute formats
161    /// needed to tell an integer key from a length-prefixed one; without it the
162    /// entries cannot be decoded.
163    pub fn parse(data: &[u8], table_offset: usize, relation: Option<&Relation>) -> Result<Self> {
164        let mut reader = Reader { data, at: 0 };
165        let size = reader.u32()? as usize;
166        if size != data.len() {
167            return Err(Error::format(format!(
168                "index region claims {size} bytes but has {}",
169                data.len()
170            )));
171        }
172        let count = reader.u32()? as usize;
173        if count > 64 {
174            return Err(Error::format(format!(
175                "index region claims {count} indexes"
176            )));
177        }
178
179        let mut offsets = Vec::with_capacity(count);
180        for _ in 0..count {
181            offsets.push(reader.u32()? as usize);
182        }
183
184        let mut indexes = Vec::with_capacity(count);
185        for offset in offsets {
186            let start = offset
187                .checked_sub(table_offset)
188                .ok_or_else(|| Error::format("index offset points before its table"))?;
189            indexes.push(parse_index(data, start, table_offset, relation)?);
190        }
191        Ok(Self { indexes })
192    }
193
194    /// Serialize, recomputing every offset from `table_offset`.
195    pub fn to_bytes(&self, table_offset: usize) -> Vec<u8> {
196        let header_len = 8 + 4 * self.indexes.len();
197        let mut out = Vec::with_capacity(self.encoded_len());
198
199        out.extend_from_slice(&(self.encoded_len() as u32).to_be_bytes());
200        out.extend_from_slice(&(self.indexes.len() as u32).to_be_bytes());
201        let mut position = header_len;
202        for index in &self.indexes {
203            out.extend_from_slice(&((table_offset + position) as u32).to_be_bytes());
204            position += index.encoded_len();
205        }
206
207        let mut position = header_len;
208        for index in &self.indexes {
209            write_index(&mut out, index, table_offset, position);
210            position += index.encoded_len();
211        }
212        out
213    }
214
215    pub fn encoded_len(&self) -> usize {
216        8 + 4 * self.indexes.len() + self.indexes.iter().map(Index::encoded_len).sum::<usize>()
217    }
218
219    /// Index a record in every index of the table.
220    ///
221    /// `key` maps an attribute id to that attribute's key value.
222    pub fn insert_record(&mut self, record_number: u32, key: impl Fn(u32) -> IndexValue) {
223        self.insert_record_where(record_number, key, |_| true);
224    }
225
226    /// Index a record only in the indexes `indexable` accepts.
227    ///
228    /// It is called with an index's attribute ids, and answers whether the
229    /// record belongs in that index — macOS leaves a record out of an index when
230    /// it has no value for an indexed attribute.
231    pub fn insert_record_where(
232        &mut self,
233        record_number: u32,
234        key: impl Fn(u32) -> IndexValue,
235        indexable: impl Fn(&[u32]) -> bool,
236    ) {
237        for index in self
238            .indexes
239            .iter_mut()
240            .filter(|index| indexable(&index.attribute_ids))
241        {
242            let values = index.attribute_ids.iter().map(|id| key(*id)).collect();
243            index.insert(IndexEntry {
244                record_number,
245                key: values,
246            });
247        }
248    }
249
250    /// Drop entries for a record, and renumber the entries that referred to
251    /// later records.
252    pub fn remove_record(&mut self, record_number: u32) {
253        for index in &mut self.indexes {
254            index
255                .entries
256                .retain(|entry| entry.record_number != record_number);
257        }
258    }
259
260    pub fn entry_count(&self) -> usize {
261        self.indexes.iter().map(|index| index.entries.len()).sum()
262    }
263
264    /// Record numbers this region indexes, in key order, for the given index.
265    pub fn record_numbers(&self, index_id: u32) -> Option<Vec<u32>> {
266        self.indexes
267            .iter()
268            .find(|index| index.id == index_id)
269            .map(|index| {
270                index
271                    .entries
272                    .iter()
273                    .map(|entry| entry.record_number)
274                    .collect()
275            })
276    }
277}
278
279fn parse_index(
280    data: &[u8],
281    start: usize,
282    table_offset: usize,
283    relation: Option<&Relation>,
284) -> Result<Index> {
285    let mut reader = Reader { data, at: start };
286    let size = reader.u32()? as usize;
287    let id = reader.u32()?;
288    let kind = reader.u32()?;
289    let attribute_count = reader.u32()? as usize;
290    if attribute_count > 32 {
291        return Err(Error::format(format!(
292            "index claims {attribute_count} attributes"
293        )));
294    }
295    let mut attribute_ids = Vec::with_capacity(attribute_count);
296    for _ in 0..attribute_count {
297        attribute_ids.push(reader.u32()?);
298    }
299
300    let formats: Vec<AttributeFormat> = attribute_ids
301        .iter()
302        .map(|id| format_of(relation, *id))
303        .collect();
304
305    let entry_count = reader.u32()? as usize;
306    let mut entry_offsets = Vec::with_capacity(entry_count);
307    for _ in 0..entry_count {
308        entry_offsets.push(reader.u32()? as usize);
309    }
310    let mut record_numbers = Vec::with_capacity(entry_count);
311    for _ in 0..entry_count {
312        record_numbers.push(reader.u32()?);
313    }
314
315    let mut entries = Vec::with_capacity(entry_count);
316    for (offset, record_number) in entry_offsets.into_iter().zip(record_numbers) {
317        let at = offset
318            .checked_sub(table_offset)
319            .ok_or_else(|| Error::format("index entry offset points before its table"))?;
320        entries.push(parse_entry(data, at, record_number, &formats)?);
321    }
322
323    let index = Index {
324        id,
325        kind,
326        attribute_ids,
327        entries,
328    };
329    if index.encoded_len() != size {
330        return Err(Error::format(format!(
331            "index {id} claims {size} bytes but parses as {}",
332            index.encoded_len()
333        )));
334    }
335    Ok(index)
336}
337
338/// The format of an attribute, defaulting to a length-prefixed blob when the
339/// relation is unknown. Getting this wrong misreads every key in the index, so
340/// the caller is expected to pass the relation.
341fn format_of(relation: Option<&Relation>, attribute_id: u32) -> AttributeFormat {
342    relation
343        .and_then(|relation| {
344            relation
345                .attributes
346                .iter()
347                .find(|attribute| attribute.id == attribute_id)
348        })
349        .map(|attribute| attribute.format)
350        .unwrap_or(AttributeFormat::Blob)
351}
352
353fn parse_entry(
354    data: &[u8],
355    at: usize,
356    record_number: u32,
357    formats: &[AttributeFormat],
358) -> Result<IndexEntry> {
359    let mut reader = Reader { data, at };
360    let payload = reader.u32()? as usize;
361    let mut key = Vec::with_capacity(formats.len());
362    for format in formats {
363        key.push(match format {
364            AttributeFormat::Sint32 | AttributeFormat::Uint32 => IndexValue::Integer(reader.u32()?),
365            _ => IndexValue::Bytes(reader.value()?),
366        });
367    }
368    let entry = IndexEntry { record_number, key };
369    if entry.payload_len() != payload {
370        return Err(Error::format(format!(
371            "index entry claims {payload} bytes but parses as {}",
372            entry.payload_len()
373        )));
374    }
375    Ok(entry)
376}
377
378fn write_index(out: &mut Vec<u8>, index: &Index, table_offset: usize, position: usize) {
379    out.extend_from_slice(&(index.encoded_len() as u32).to_be_bytes());
380    out.extend_from_slice(&index.id.to_be_bytes());
381    out.extend_from_slice(&index.kind.to_be_bytes());
382    out.extend_from_slice(&(index.attribute_ids.len() as u32).to_be_bytes());
383    for id in &index.attribute_ids {
384        out.extend_from_slice(&id.to_be_bytes());
385    }
386
387    out.extend_from_slice(&(index.entries.len() as u32).to_be_bytes());
388    let mut entry_position = position + index.header_len() + 4 + 8 * index.entries.len();
389    for entry in &index.entries {
390        out.extend_from_slice(&((table_offset + entry_position) as u32).to_be_bytes());
391        entry_position += entry.encoded_len();
392    }
393    for entry in &index.entries {
394        out.extend_from_slice(&entry.record_number.to_be_bytes());
395    }
396    for entry in &index.entries {
397        out.extend_from_slice(&(entry.payload_len() as u32).to_be_bytes());
398        for value in &entry.key {
399            value.write(out);
400        }
401    }
402}
403
404struct Reader<'a> {
405    data: &'a [u8],
406    at: usize,
407}
408
409impl Reader<'_> {
410    fn u32(&mut self) -> Result<u32> {
411        let bytes = self
412            .data
413            .get(self.at..self.at + 4)
414            .ok_or_else(|| Error::format("index region ends mid-word"))?;
415        self.at += 4;
416        Ok(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
417    }
418
419    fn value(&mut self) -> Result<Vec<u8>> {
420        let len = self.u32()? as usize;
421        let bytes = self
422            .data
423            .get(self.at..self.at + len)
424            .ok_or_else(|| Error::format("index value runs past the region"))?
425            .to_vec();
426        self.at += pad4(4 + len) - 4;
427        Ok(bytes)
428    }
429}
430
431fn pad4(len: usize) -> usize {
432    (len + 3) & !3
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438    use crate::schema::{AttributeDef, RecordType};
439
440    fn bytes(text: &str) -> IndexValue {
441        IndexValue::Bytes(text.as_bytes().to_vec())
442    }
443
444    fn sample() -> IndexBlob {
445        IndexBlob {
446            indexes: vec![
447                Index {
448                    id: 0,
449                    kind: 1,
450                    attribute_ids: vec![u32::from_be_bytes(*b"acct"), u32::from_be_bytes(*b"svce")],
451                    entries: vec![
452                        IndexEntry {
453                            record_number: 0,
454                            key: vec![bytes("alice"), bytes("myservice")],
455                        },
456                        IndexEntry {
457                            record_number: 1,
458                            key: vec![bytes("carol"), bytes("other")],
459                        },
460                    ],
461                },
462                Index {
463                    id: 3,
464                    kind: 0,
465                    attribute_ids: vec![u32::from_be_bytes(*b"port")],
466                    entries: vec![
467                        IndexEntry {
468                            record_number: 1,
469                            key: vec![IndexValue::Integer(80)],
470                        },
471                        IndexEntry {
472                            record_number: 0,
473                            key: vec![IndexValue::Integer(8080)],
474                        },
475                    ],
476                },
477            ],
478        }
479    }
480
481    fn relation() -> Relation {
482        Relation {
483            record_type: RecordType::GENERIC_PASSWORD,
484            name: None,
485            attributes: vec![
486                AttributeDef {
487                    id: u32::from_be_bytes(*b"acct"),
488                    name: "acct".into(),
489                    format: AttributeFormat::Blob,
490                },
491                AttributeDef {
492                    id: u32::from_be_bytes(*b"svce"),
493                    name: "svce".into(),
494                    format: AttributeFormat::Blob,
495                },
496                AttributeDef {
497                    id: u32::from_be_bytes(*b"port"),
498                    name: "port".into(),
499                    format: AttributeFormat::Uint32,
500                },
501            ],
502        }
503    }
504
505    #[test]
506    fn round_trips_at_any_table_offset() {
507        let blob = sample();
508        for table_offset in [0, 32, 492, 4096] {
509            let encoded = blob.to_bytes(table_offset);
510            assert_eq!(encoded.len(), blob.encoded_len());
511            assert_eq!(
512                IndexBlob::parse(&encoded, table_offset, Some(&relation())).unwrap(),
513                blob
514            );
515        }
516    }
517
518    #[test]
519    fn offsets_follow_the_regions_position_in_the_table() {
520        let blob = sample();
521        let low = blob.to_bytes(100);
522        let high = blob.to_bytes(200);
523        assert_eq!(low.len(), high.len(), "only the offsets differ");
524        assert_ne!(low, high, "a moved region must renumber its offsets");
525    }
526
527    #[test]
528    fn integer_keys_are_stored_raw_and_blob_keys_length_prefixed() {
529        let blob = IndexBlob {
530            indexes: vec![Index {
531                id: 0,
532                kind: 1,
533                attribute_ids: vec![u32::from_be_bytes(*b"port"), u32::from_be_bytes(*b"acct")],
534                entries: vec![IndexEntry {
535                    record_number: 7,
536                    key: vec![IndexValue::Integer(8080), bytes("bob")],
537                }],
538            }],
539        };
540        // 4 raw bytes for the integer, 4 + 4 (padded from 3) for the blob.
541        assert_eq!(blob.indexes[0].entries[0].payload_len(), 4 + 8);
542
543        let encoded = blob.to_bytes(0);
544        assert_eq!(
545            IndexBlob::parse(&encoded, 0, Some(&relation())).unwrap(),
546            blob
547        );
548    }
549
550    #[test]
551    fn empty_indexes_have_no_entry_arrays() {
552        let blob = IndexBlob {
553            indexes: vec![Index {
554                id: 0,
555                kind: 1,
556                attribute_ids: vec![u32::from_be_bytes(*b"acct")],
557                entries: Vec::new(),
558            }],
559        };
560        // region header 8 + one offset 4 + index header 20 + entry count 4
561        assert_eq!(blob.encoded_len(), 36);
562        let encoded = blob.to_bytes(32);
563        assert_eq!(
564            IndexBlob::parse(&encoded, 32, Some(&relation())).unwrap(),
565            blob
566        );
567    }
568
569    #[test]
570    fn insert_keeps_keys_sorted_and_record_numbers_aligned() {
571        let mut blob = IndexBlob {
572            indexes: vec![Index {
573                id: 0,
574                kind: 1,
575                attribute_ids: vec![u32::from_be_bytes(*b"acct")],
576                entries: Vec::new(),
577            }],
578        };
579
580        for (number, account) in [(0u32, "carol"), (1, "alice"), (2, "bob")] {
581            blob.insert_record(number, |_| bytes(account));
582        }
583
584        let entries = &blob.indexes[0].entries;
585        assert_eq!(
586            entries
587                .iter()
588                .map(|entry| entry.key[0].clone())
589                .collect::<Vec<_>>(),
590            vec![bytes("alice"), bytes("bob"), bytes("carol")]
591        );
592        // The record numbers travel with their keys, not with insertion order.
593        assert_eq!(blob.record_numbers(0).unwrap(), vec![1, 2, 0]);
594    }
595
596    #[test]
597    fn remove_drops_only_that_records_entries() {
598        let mut blob = sample();
599        assert_eq!(blob.entry_count(), 4);
600        blob.remove_record(0);
601        assert_eq!(blob.entry_count(), 2);
602        assert!(
603            blob.indexes
604                .iter()
605                .all(|index| index.entries.iter().all(|entry| entry.record_number != 0))
606        );
607    }
608
609    #[test]
610    fn absent_values_index_as_empty_or_zero() {
611        assert_eq!(
612            IndexValue::from_value(None, AttributeFormat::Blob),
613            IndexValue::Bytes(Vec::new())
614        );
615        assert_eq!(
616            IndexValue::from_value(None, AttributeFormat::Uint32),
617            IndexValue::Integer(0)
618        );
619        assert_eq!(
620            IndexValue::from_value(Some(&Value::Uint32(5)), AttributeFormat::Uint32),
621            IndexValue::Integer(5)
622        );
623        assert_eq!(
624            IndexValue::from_value(Some(&Value::Blob(b"x".to_vec())), AttributeFormat::Blob),
625            IndexValue::Bytes(b"x".to_vec())
626        );
627    }
628
629    #[test]
630    fn malformed_regions_are_rejected() {
631        assert!(IndexBlob::parse(&[], 0, None).is_err());
632        let mut encoded = sample().to_bytes(0);
633        encoded[3] = 0xff;
634        assert!(IndexBlob::parse(&encoded, 0, Some(&relation())).is_err());
635    }
636}