1use std::collections::BTreeMap;
18
19use crate::error::{Error, Result};
20use crate::format::{Record, Table, Value, trim_nul};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
24pub struct RecordType(pub u32);
25
26impl RecordType {
27 pub const SCHEMA_INFO: Self = Self(0x0000_0000);
28 pub const SCHEMA_INDEXES: Self = Self(0x0000_0001);
29 pub const SCHEMA_ATTRIBUTES: Self = Self(0x0000_0002);
30 pub const SCHEMA_PARSING_MODULE: Self = Self(0x0000_0003);
31
32 pub const CERT: Self = Self(0x0000_000b);
33 pub const CRL: Self = Self(0x0000_000c);
34 pub const POLICY: Self = Self(0x0000_000d);
35 pub const GENERIC: Self = Self(0x0000_000e);
36 pub const PUBLIC_KEY: Self = Self(0x0000_000f);
37 pub const PRIVATE_KEY: Self = Self(0x0000_0010);
38 pub const SYMMETRIC_KEY: Self = Self(0x0000_0011);
39
40 pub const GENERIC_PASSWORD: Self = Self(0x8000_0000);
41 pub const INTERNET_PASSWORD: Self = Self(0x8000_0001);
42 pub const APPLESHARE_PASSWORD: Self = Self(0x8000_0002);
43 pub const USER_TRUST: Self = Self(0x8000_0003);
44 pub const X509_CRL: Self = Self(0x8000_0004);
45 pub const UNLOCK_REFERRAL: Self = Self(0x8000_0005);
46 pub const EXTENDED_ATTRIBUTE: Self = Self(0x8000_0006);
47 pub const X509_CERTIFICATE: Self = Self(0x8000_1000);
48 pub const METADATA: Self = Self(0x8000_8000);
49
50 pub fn is_application_specific(self) -> bool {
52 self.0 & 0x8000_0000 != 0
53 }
54
55 pub fn name(self) -> &'static str {
56 match self {
57 Self::SCHEMA_INFO => "CSSM_DL_DB_SCHEMA_INFO",
58 Self::SCHEMA_INDEXES => "CSSM_DL_DB_SCHEMA_INDEXES",
59 Self::SCHEMA_ATTRIBUTES => "CSSM_DL_DB_SCHEMA_ATTRIBUTES",
60 Self::SCHEMA_PARSING_MODULE => "CSSM_DL_DB_SCHEMA_PARSING_MODULE",
61 Self::CERT => "CSSM_DL_DB_RECORD_CERT",
62 Self::CRL => "CSSM_DL_DB_RECORD_CRL",
63 Self::POLICY => "CSSM_DL_DB_RECORD_POLICY",
64 Self::GENERIC => "CSSM_DL_DB_RECORD_GENERIC",
65 Self::PUBLIC_KEY => "CSSM_DL_DB_RECORD_PUBLIC_KEY",
66 Self::PRIVATE_KEY => "CSSM_DL_DB_RECORD_PRIVATE_KEY",
67 Self::SYMMETRIC_KEY => "CSSM_DL_DB_RECORD_SYMMETRIC_KEY",
68 Self::GENERIC_PASSWORD => "CSSM_DL_DB_RECORD_GENERIC_PASSWORD",
69 Self::INTERNET_PASSWORD => "CSSM_DL_DB_RECORD_INTERNET_PASSWORD",
70 Self::APPLESHARE_PASSWORD => "CSSM_DL_DB_RECORD_APPLESHARE_PASSWORD",
71 Self::USER_TRUST => "CSSM_DL_DB_RECORD_USER_TRUST",
72 Self::X509_CRL => "CSSM_DL_DB_RECORD_X509_CRL",
73 Self::UNLOCK_REFERRAL => "CSSM_DL_DB_RECORD_UNLOCK_REFERRAL",
74 Self::EXTENDED_ATTRIBUTE => "CSSM_DL_DB_RECORD_EXTENDED_ATTRIBUTE",
75 Self::X509_CERTIFICATE => "CSSM_DL_DB_RECORD_X509_CERTIFICATE",
76 Self::METADATA => "CSSM_DL_DB_RECORD_METADATA",
77 _ => "unknown",
78 }
79 }
80
81 pub fn short_name(self) -> Option<&'static str> {
83 match self {
84 Self::GENERIC_PASSWORD => Some("generic"),
85 Self::INTERNET_PASSWORD => Some("internet"),
86 Self::APPLESHARE_PASSWORD => Some("appleshare"),
87 _ => None,
88 }
89 }
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum AttributeFormat {
95 String,
96 Sint32,
97 Uint32,
98 BigNum,
99 Real,
100 TimeDate,
101 Blob,
102 MultiUint32,
103 Complex,
104 Unknown(u32),
106}
107
108impl AttributeFormat {
109 pub fn from_u32(value: u32) -> Self {
110 match value {
111 0 => Self::String,
112 1 => Self::Sint32,
113 2 => Self::Uint32,
114 3 => Self::BigNum,
115 4 => Self::Real,
116 5 => Self::TimeDate,
117 6 => Self::Blob,
118 7 => Self::MultiUint32,
119 8 => Self::Complex,
120 other => Self::Unknown(other),
121 }
122 }
123
124 pub fn as_u32(self) -> u32 {
125 match self {
126 Self::String => 0,
127 Self::Sint32 => 1,
128 Self::Uint32 => 2,
129 Self::BigNum => 3,
130 Self::Real => 4,
131 Self::TimeDate => 5,
132 Self::Blob => 6,
133 Self::MultiUint32 => 7,
134 Self::Complex => 8,
135 Self::Unknown(other) => other,
136 }
137 }
138}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct AttributeDef {
143 pub id: u32,
144 pub name: String,
146 pub format: AttributeFormat,
147}
148
149#[derive(Debug, Clone)]
150pub struct Relation {
151 pub record_type: RecordType,
152 pub name: Option<String>,
154 pub attributes: Vec<AttributeDef>,
156}
157
158impl Relation {
159 pub fn index_of(&self, name: &str) -> Option<usize> {
161 self.attributes
162 .iter()
163 .position(|attr| attr.name.eq_ignore_ascii_case(name))
164 }
165}
166
167#[derive(Debug, Clone, Default)]
169pub struct Schema {
170 relations: BTreeMap<RecordType, Relation>,
171}
172
173impl Schema {
174 pub fn bootstrap() -> Self {
177 let uint32 = AttributeFormat::Uint32;
178 let string = AttributeFormat::String;
179 let blob = AttributeFormat::Blob;
180
181 let mut relations = BTreeMap::new();
182 let mut add = |record_type: RecordType, attrs: &[(&str, AttributeFormat)]| {
183 relations.insert(
184 record_type,
185 Relation {
186 record_type,
187 name: Some(record_type.name().to_string()),
188 attributes: attrs
189 .iter()
190 .enumerate()
191 .map(|(index, (name, format))| AttributeDef {
192 id: index as u32,
193 name: (*name).to_string(),
194 format: *format,
195 })
196 .collect(),
197 },
198 );
199 };
200
201 add(
202 RecordType::SCHEMA_INFO,
203 &[("RelationID", uint32), ("RelationName", string)],
204 );
205 add(
206 RecordType::SCHEMA_INDEXES,
207 &[
208 ("RelationID", uint32),
209 ("IndexID", uint32),
210 ("AttributeID", uint32),
211 ("IndexType", uint32),
212 ("IndexedDataLocation", uint32),
213 ],
214 );
215 add(
216 RecordType::SCHEMA_ATTRIBUTES,
217 &[
218 ("RelationID", uint32),
219 ("AttributeID", uint32),
220 ("AttributeNameFormat", uint32),
221 ("AttributeName", string),
222 ("AttributeNameID", blob),
223 ("AttributeFormat", uint32),
224 ],
225 );
226 add(
227 RecordType::SCHEMA_PARSING_MODULE,
228 &[
229 ("RelationID", uint32),
230 ("AttributeID", uint32),
231 ("ModuleID", blob),
232 ("AddinVersion", string),
233 ("SSID", uint32),
234 ("SubserviceType", uint32),
235 ],
236 );
237
238 Self { relations }
239 }
240
241 pub fn from_tables(tables: &[Table]) -> Result<Self> {
243 let bootstrap = Self::bootstrap();
244 let find =
245 |record_type: RecordType| tables.iter().find(|table| table.record_type == record_type);
246
247 let mut names: BTreeMap<RecordType, String> = BTreeMap::new();
249 if let Some(table) = find(RecordType::SCHEMA_INFO) {
250 for record in table.records() {
251 let Some(id) = record.attribute(0).and_then(Value::as_u32) else {
252 continue;
253 };
254 if let Some(name) = record.attribute(1).and_then(Value::as_bytes) {
255 let name = String::from_utf8_lossy(trim_nul(name)).into_owned();
256 if !name.is_empty() {
257 names.insert(RecordType(id), name);
258 }
259 }
260 }
261 }
262
263 let attributes_table = find(RecordType::SCHEMA_ATTRIBUTES)
264 .ok_or(Error::MissingTable("CSSM_DL_DB_SCHEMA_ATTRIBUTES"))?;
265
266 let mut relations: BTreeMap<RecordType, Relation> = BTreeMap::new();
267 for record in attributes_table.records() {
268 let relation_id = record
269 .attribute(0)
270 .and_then(Value::as_u32)
271 .ok_or_else(|| Error::format("schema attribute record has no RelationID"))?;
272 let attribute_id = record
273 .attribute(1)
274 .and_then(Value::as_u32)
275 .ok_or_else(|| Error::format("schema attribute record has no AttributeID"))?;
276 let format =
277 AttributeFormat::from_u32(record.attribute(5).and_then(Value::as_u32).unwrap_or(6));
278
279 let named = record
280 .attribute(3)
281 .and_then(Value::as_bytes)
282 .map(|bytes| String::from_utf8_lossy(trim_nul(bytes)).into_owned())
283 .filter(|name| !name.is_empty());
284 let name = named
285 .or_else(|| {
286 record
287 .attribute(4)
288 .and_then(Value::as_bytes)
289 .map(|bytes| String::from_utf8_lossy(trim_nul(bytes)).into_owned())
290 .filter(|name| !name.is_empty())
291 })
292 .unwrap_or_else(|| attribute_name_from_id(attribute_id));
293
294 let record_type = RecordType(relation_id);
295 relations
296 .entry(record_type)
297 .or_insert_with(|| Relation {
298 record_type,
299 name: names.get(&record_type).cloned(),
300 attributes: Vec::new(),
301 })
302 .attributes
303 .push(AttributeDef {
304 id: attribute_id,
305 name,
306 format,
307 });
308 }
309
310 for (record_type, relation) in bootstrap.relations {
313 relations.insert(record_type, relation);
314 }
315
316 for table in tables {
318 relations
319 .entry(table.record_type)
320 .or_insert_with(|| Relation {
321 record_type: table.record_type,
322 name: names.get(&table.record_type).cloned(),
323 attributes: Vec::new(),
324 });
325 }
326
327 Ok(Self { relations })
328 }
329
330 pub fn relation(&self, record_type: RecordType) -> Option<&Relation> {
331 self.relations.get(&record_type)
332 }
333
334 pub fn relations(&self) -> impl Iterator<Item = &Relation> {
335 self.relations.values()
336 }
337
338 pub fn attribute_formats(&self, record_type: RecordType) -> Vec<AttributeFormat> {
341 self.relations
342 .get(&record_type)
343 .map(|relation| relation.attributes.iter().map(|attr| attr.format).collect())
344 .unwrap_or_default()
345 }
346
347 pub fn attribute<'r>(
349 &self,
350 record_type: RecordType,
351 record: &'r Record,
352 name: &str,
353 ) -> Option<&'r Value> {
354 let index = self.relation(record_type)?.index_of(name)?;
355 record.attribute(index)
356 }
357
358 pub fn named_attributes<'r>(
360 &self,
361 record_type: RecordType,
362 record: &'r Record,
363 ) -> Vec<(&str, &'r Value)> {
364 let Some(relation) = self.relation(record_type) else {
365 return Vec::new();
366 };
367 relation
368 .attributes
369 .iter()
370 .enumerate()
371 .filter_map(|(index, attr)| {
372 record
373 .attribute(index)
374 .map(|value| (attr.name.as_str(), value))
375 })
376 .collect()
377 }
378}
379
380fn attribute_name_from_id(id: u32) -> String {
382 let bytes = id.to_be_bytes();
383 if bytes.iter().all(|byte| (0x20..0x7f).contains(byte)) {
384 String::from_utf8_lossy(&bytes).into_owned()
385 } else {
386 format!("attr{id}")
387 }
388}
389
390pub const fn four_char_code(code: &[u8; 4]) -> u32 {
392 u32::from_be_bytes(*code)
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn record_types_report_their_names_and_kind() {
401 assert_eq!(
402 RecordType::GENERIC_PASSWORD.name(),
403 "CSSM_DL_DB_RECORD_GENERIC_PASSWORD"
404 );
405 assert!(RecordType::GENERIC_PASSWORD.is_application_specific());
406 assert!(!RecordType::SYMMETRIC_KEY.is_application_specific());
407 assert_eq!(RecordType::INTERNET_PASSWORD.short_name(), Some("internet"));
408 assert_eq!(RecordType::SYMMETRIC_KEY.short_name(), None);
409 assert_eq!(RecordType(0x1234).name(), "unknown");
410 }
411
412 #[test]
413 fn attribute_formats_round_trip() {
414 for value in 0..=8 {
415 assert_eq!(AttributeFormat::from_u32(value).as_u32(), value);
416 }
417 assert_eq!(AttributeFormat::from_u32(99), AttributeFormat::Unknown(99));
418 assert_eq!(AttributeFormat::from_u32(99).as_u32(), 99);
419 }
420
421 #[test]
422 fn bootstrap_schema_describes_the_four_schema_relations() {
423 let schema = Schema::bootstrap();
424 assert_eq!(schema.attribute_formats(RecordType::SCHEMA_INFO).len(), 2);
425 assert_eq!(
426 schema.attribute_formats(RecordType::SCHEMA_INDEXES).len(),
427 5
428 );
429 assert_eq!(
430 schema
431 .attribute_formats(RecordType::SCHEMA_ATTRIBUTES)
432 .len(),
433 6
434 );
435 assert_eq!(
436 schema
437 .attribute_formats(RecordType::SCHEMA_PARSING_MODULE)
438 .len(),
439 6
440 );
441 assert!(
443 schema
444 .attribute_formats(RecordType::GENERIC_PASSWORD)
445 .is_empty()
446 );
447 }
448
449 #[test]
450 fn attribute_names_fall_back_to_four_char_codes() {
451 assert_eq!(attribute_name_from_id(four_char_code(b"acct")), "acct");
452 assert_eq!(attribute_name_from_id(four_char_code(b"svce")), "svce");
453 assert_eq!(attribute_name_from_id(0), "attr0");
454 assert_eq!(attribute_name_from_id(7), "attr7");
455 }
456
457 #[test]
458 fn four_char_codes_are_big_endian() {
459 assert_eq!(four_char_code(b"acct"), 0x6163_6374);
460 assert_eq!(four_char_code(b"svce").to_be_bytes(), *b"svce");
461 }
462
463 #[test]
464 fn attribute_lookup_is_case_insensitive() {
465 let relation = Relation {
466 record_type: RecordType::GENERIC_PASSWORD,
467 name: None,
468 attributes: vec![
469 AttributeDef {
470 id: 0,
471 name: "acct".into(),
472 format: AttributeFormat::Blob,
473 },
474 AttributeDef {
475 id: 1,
476 name: "PrintName".into(),
477 format: AttributeFormat::Blob,
478 },
479 ],
480 };
481 assert_eq!(relation.index_of("acct"), Some(0));
482 assert_eq!(relation.index_of("ACCT"), Some(0));
483 assert_eq!(relation.index_of("printname"), Some(1));
484 assert_eq!(relation.index_of("nope"), None);
485 }
486}