sherlock_nsf_parser/note.rs
1//! Note record parsing.
2//!
3//! A note is the unit of user-visible data inside an NSF database -
4//! emails, calendar entries, contacts, design notes, the ACL, etc. all
5//! live as notes distinguished by `note_class`.
6//!
7//! Note header layout per `libnsfdb/nsfdb_note.h` (100 bytes):
8//!
9//! ```text
10//! offset width field
11//! 0 2 signature (0x0004)
12//! 2 4 size
13//! 6 4 rrv_identifier
14//! 10 8 file_identifier (TIMEDATE-shaped opaque)
15//! 18 8 note_identifier (TIMEDATE-shaped opaque - half of UNID)
16//! 26 4 sequence_number
17//! 30 8 sequence_time (TIMEDATE)
18//! 38 2 status_flags
19//! 40 2 note_class
20//! 42 8 modification_time (TIMEDATE)
21//! 50 2 number_of_note_items
22//! 52 2 unknown1
23//! 54 2 number_of_responses
24//! 56 4 non_summary_data_identifier
25//! 60 4 non_summary_data_size
26//! 64 8 access_time (TIMEDATE)
27//! 72 8 creation_time (TIMEDATE)
28//! 80 4 parent_note_identifier
29//! 84 2 unknown3
30//! 86 4 folder_reference_count
31//! 90 4 unknown4
32//! 94 4 folder_note_identifier
33//! 98 2 unknown5
34//! ```
35//!
36//! Note class catalogue (bit flags; a note can carry multiple class
37//! bits but in practice each note is one class):
38//!
39//! ```text
40//! NOTE_CLASS_DOCUMENT 0x0001 // user-visible documents (mail, etc)
41//! NOTE_CLASS_INFO 0x0002 // database info note
42//! NOTE_CLASS_FORM 0x0004 // form design
43//! NOTE_CLASS_VIEW 0x0008 // view design
44//! NOTE_CLASS_ICON 0x0010 // database icon
45//! NOTE_CLASS_DESIGN 0x0020 // design collection
46//! NOTE_CLASS_ACL 0x0040 // access control list
47//! NOTE_CLASS_HELP_INDEX 0x0080
48//! NOTE_CLASS_HELP 0x0100
49//! NOTE_CLASS_FILTER 0x0200 // agent / mail rule
50//! NOTE_CLASS_FIELD 0x0400 // shared field
51//! NOTE_CLASS_REPLFORMULA 0x0800
52//! NOTE_CLASS_PRIVATE 0x1000
53//! ```
54
55use crate::error::NsfError;
56use crate::time::Timedate;
57
58/// Magic two bytes at offset 0 of every note header.
59pub const NOTE_SIGNATURE: [u8; 2] = [0x04, 0x00];
60/// Note header size in bytes.
61pub const NOTE_HEADER_BYTES: usize = 100;
62
63/// Note class flag values. A note's `note_class` is typically one of
64/// these; multi-bit values are uncommon in practice.
65#[allow(missing_docs)]
66pub mod class {
67 pub const DOCUMENT: u16 = 0x0001;
68 pub const INFO: u16 = 0x0002;
69 pub const FORM: u16 = 0x0004;
70 pub const VIEW: u16 = 0x0008;
71 pub const ICON: u16 = 0x0010;
72 pub const DESIGN: u16 = 0x0020;
73 pub const ACL: u16 = 0x0040;
74 pub const HELP_INDEX: u16 = 0x0080;
75 pub const HELP: u16 = 0x0100;
76 pub const FILTER: u16 = 0x0200;
77 pub const FIELD: u16 = 0x0400;
78 pub const REPLFORMULA: u16 = 0x0800;
79 pub const PRIVATE: u16 = 0x1000;
80}
81
82/// Parsed note header. Self-contained snapshot - the reader does not
83/// retain a reference into bucket bytes.
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct NoteHeader {
86 /// Total note size in bytes (header + item descriptors + item data).
87 pub size: u32,
88 /// RRV identifier the note was reached through. Local to one NSF.
89 pub rrv_identifier: u32,
90 /// File identifier portion of the UNID (8 bytes).
91 pub file_identifier: Timedate,
92 /// Note identifier portion of the UNID (8 bytes). Together with
93 /// `file_identifier` this forms the 16-byte Universal Note ID
94 /// (UNID) which is globally unique across replicas.
95 pub note_identifier: Timedate,
96 /// Replication-sequence number. Increments on every modification.
97 pub sequence_number: u32,
98 /// Replication-sequence time.
99 pub sequence_time: Timedate,
100 /// Status flags word.
101 pub status_flags: u16,
102 /// Note class (DOCUMENT / FORM / VIEW / ACL / etc). See [`class`]
103 /// constants.
104 pub note_class: u16,
105 /// Most recent modification time. Operator-facing "when was this
106 /// note last touched".
107 pub modification_time: Timedate,
108 /// Number of items (fields) attached to this note. Each item has
109 /// its own descriptor block immediately after the note header.
110 pub number_of_note_items: u16,
111 /// Number of response notes (replies to this note as a parent in
112 /// a discussion-style database).
113 pub number_of_responses: u16,
114 /// Identifier into the non-summary data area for items too large
115 /// to fit in summary slots (rich-text bodies, attachments).
116 pub non_summary_data_identifier: u32,
117 /// Size in bytes of the non-summary data area associated with this
118 /// note.
119 pub non_summary_data_size: u32,
120 /// Most recent access time.
121 pub access_time: Timedate,
122 /// File-creation time (first-write timestamp).
123 pub creation_time: Timedate,
124 /// NoteID of the parent (for response notes).
125 pub parent_note_identifier: u32,
126 /// Number of folders that reference this note.
127 pub folder_reference_count: u32,
128 /// NoteID of an associated folder (if any).
129 pub folder_note_identifier: u32,
130}
131
132impl NoteHeader {
133 /// Parse a note header from at least the first 100 bytes of a note
134 /// record. Errors on signature mismatch or short input.
135 pub fn parse(bytes: &[u8]) -> Result<Self, NsfError> {
136 if bytes.len() < NOTE_HEADER_BYTES {
137 return Err(NsfError::TooShort {
138 actual: bytes.len(),
139 required: NOTE_HEADER_BYTES,
140 });
141 }
142 if bytes[0] != NOTE_SIGNATURE[0] || bytes[1] != NOTE_SIGNATURE[1] {
143 return Err(NsfError::BadFileSignature {
144 observed: [bytes[0], bytes[1]],
145 });
146 }
147 let u16_at = |o: usize| u16::from_le_bytes([bytes[o], bytes[o + 1]]);
148 let u32_at = |o: usize| {
149 u32::from_le_bytes([bytes[o], bytes[o + 1], bytes[o + 2], bytes[o + 3]])
150 };
151 Ok(Self {
152 size: u32_at(2),
153 rrv_identifier: u32_at(6),
154 file_identifier: Timedate::from_bytes(&bytes[10..18])?,
155 note_identifier: Timedate::from_bytes(&bytes[18..26])?,
156 sequence_number: u32_at(26),
157 sequence_time: Timedate::from_bytes(&bytes[30..38])?,
158 status_flags: u16_at(38),
159 note_class: u16_at(40),
160 modification_time: Timedate::from_bytes(&bytes[42..50])?,
161 number_of_note_items: u16_at(50),
162 number_of_responses: u16_at(54),
163 non_summary_data_identifier: u32_at(56),
164 non_summary_data_size: u32_at(60),
165 access_time: Timedate::from_bytes(&bytes[64..72])?,
166 creation_time: Timedate::from_bytes(&bytes[72..80])?,
167 parent_note_identifier: u32_at(80),
168 folder_reference_count: u32_at(86),
169 folder_note_identifier: u32_at(94),
170 })
171 }
172
173 /// True if any DOCUMENT bit is set in the note class. User-visible
174 /// emails, calendar entries, contacts, and custom-form documents
175 /// all carry this bit.
176 pub fn is_document(&self) -> bool {
177 self.note_class & class::DOCUMENT != 0
178 }
179
180 /// True if the note carries any design-related class bit (FORM,
181 /// VIEW, ICON, DESIGN, HELP, FILTER, FIELD, REPLFORMULA, PRIVATE).
182 pub fn is_design(&self) -> bool {
183 const DESIGN_MASK: u16 = class::FORM
184 | class::VIEW
185 | class::ICON
186 | class::DESIGN
187 | class::HELP
188 | class::HELP_INDEX
189 | class::FILTER
190 | class::FIELD
191 | class::REPLFORMULA
192 | class::PRIVATE;
193 self.note_class & DESIGN_MASK != 0
194 }
195
196 /// 16-byte UNID (Universal Note Identifier) as a hex string. This
197 /// is the globally-unique identifier that survives replication and
198 /// compaction. Two replicas of the same logical note carry the
199 /// same UNID.
200 pub fn unid_hex(&self) -> String {
201 format!(
202 "{}{}",
203 self.file_identifier.as_hex_id(),
204 self.note_identifier.as_hex_id()
205 )
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 fn synthetic_note(note_class: u16, item_count: u16) -> Vec<u8> {
214 let mut buf = vec![0u8; NOTE_HEADER_BYTES + 32];
215 buf[0..2].copy_from_slice(&NOTE_SIGNATURE);
216 buf[2..6].copy_from_slice(&512u32.to_le_bytes());
217 buf[6..10].copy_from_slice(&12345u32.to_le_bytes());
218 buf[40..42].copy_from_slice(¬e_class.to_le_bytes());
219 buf[50..52].copy_from_slice(&item_count.to_le_bytes());
220 buf
221 }
222
223 #[test]
224 fn parses_document_note() {
225 let buf = synthetic_note(class::DOCUMENT, 17);
226 let n = NoteHeader::parse(&buf).unwrap();
227 assert!(n.is_document());
228 assert!(!n.is_design());
229 assert_eq!(n.number_of_note_items, 17);
230 assert_eq!(n.rrv_identifier, 12345);
231 assert_eq!(n.size, 512);
232 }
233
234 #[test]
235 fn parses_form_note_as_design() {
236 let buf = synthetic_note(class::FORM, 8);
237 let n = NoteHeader::parse(&buf).unwrap();
238 assert!(!n.is_document());
239 assert!(n.is_design());
240 }
241
242 #[test]
243 fn parses_acl_note_neither_document_nor_design() {
244 let buf = synthetic_note(class::ACL, 3);
245 let n = NoteHeader::parse(&buf).unwrap();
246 assert!(!n.is_document());
247 assert!(!n.is_design());
248 }
249
250 #[test]
251 fn rejects_bad_signature() {
252 let mut buf = synthetic_note(class::DOCUMENT, 1);
253 buf[0] = 0xFF;
254 assert!(NoteHeader::parse(&buf).is_err());
255 }
256
257 #[test]
258 fn unid_hex_is_32_chars() {
259 let buf = synthetic_note(class::DOCUMENT, 1);
260 let n = NoteHeader::parse(&buf).unwrap();
261 assert_eq!(n.unid_hex().len(), 32);
262 }
263}