Skip to main content

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(&note_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}