1pub mod reason {
10 pub const DATA_OVERWRITE: u32 = 0x0000_0001;
12 pub const DATA_EXTEND: u32 = 0x0000_0002;
14 pub const DATA_TRUNCATION: u32 = 0x0000_0004;
16 pub const BASIC_INFO_CHANGE: u32 = 0x0000_8000;
18 pub const FILE_CREATE: u32 = 0x0000_0100;
20 pub const FILE_DELETE: u32 = 0x0000_0200;
22 pub const RENAME_OLD_NAME: u32 = 0x0000_1000;
24 pub const RENAME_NEW_NAME: u32 = 0x0000_2000;
26 pub const HARD_LINK_CHANGE: u32 = 0x0001_0000;
28 pub const CLOSE: u32 = 0x8000_0000;
30}
31
32pub const FILE_ATTRIBUTE_HIDDEN: u32 = 0x2;
34pub const FILE_ATTRIBUTE_SYSTEM: u32 = 0x4;
36pub const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x10;
38pub const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct UsnRecord {
44 pub usn: i64,
46 pub frn: u64,
48 pub parent_frn: u64,
50 pub reason: u32,
52 pub attributes: u32,
54 pub name: Vec<u16>,
57}
58
59impl UsnRecord {
60 #[must_use]
62 pub const fn is_dir(&self) -> bool {
63 self.attributes & FILE_ATTRIBUTE_DIRECTORY != 0
64 }
65 #[must_use]
67 pub const fn is_reparse(&self) -> bool {
68 self.attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0
69 }
70 #[must_use]
72 pub const fn is_hidden(&self) -> bool {
73 self.attributes & FILE_ATTRIBUTE_HIDDEN != 0
74 }
75 #[must_use]
77 pub const fn is_system(&self) -> bool {
78 self.attributes & FILE_ATTRIBUTE_SYSTEM != 0
79 }
80}
81
82#[inline]
83fn u16_at(b: &[u8], off: usize) -> u16 {
84 u16::from_le_bytes([b[off], b[off + 1]])
85}
86#[inline]
87fn u32_at(b: &[u8], off: usize) -> u32 {
88 u32::from_le_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]])
89}
90#[inline]
91fn u64_at(b: &[u8], off: usize) -> u64 {
92 let mut a = [0u8; 8];
93 a.copy_from_slice(&b[off..off + 8]);
94 u64::from_le_bytes(a)
95}
96
97#[must_use]
103pub fn parse_buffer(buf: &[u8]) -> (u64, Vec<UsnRecord>, bool) {
104 let mut records = Vec::new();
105 let mut truncated = false;
106 if buf.len() < 8 {
107 return (0, records, !buf.is_empty());
108 }
109 let next = u64_at(buf, 0);
110 let mut off = 8usize;
111
112 while off + 60 <= buf.len() {
113 let rec = &buf[off..];
114 let record_length = u32_at(rec, 0) as usize;
115 if record_length < 60 || off + record_length > buf.len() {
116 truncated = true;
117 break;
118 }
119 let major = u16_at(rec, 4);
120 if major == 2 {
121 let name_len = u16_at(rec, 56) as usize; let name_off = u16_at(rec, 58) as usize;
123 if name_off + name_len <= record_length {
124 let mut name = Vec::with_capacity(name_len / 2);
125 let nb = &rec[name_off..name_off + name_len];
126 for ch in nb.chunks_exact(2) {
127 name.push(u16::from_le_bytes([ch[0], ch[1]]));
128 }
129 records.push(UsnRecord {
130 usn: u64_at(rec, 24) as i64,
131 frn: u64_at(rec, 8),
132 parent_frn: u64_at(rec, 16),
133 reason: u32_at(rec, 40),
134 attributes: u32_at(rec, 52),
135 name,
136 });
137 } else {
138 truncated = true;
142 }
143 }
144 off += record_length.next_multiple_of(8);
146 }
147 if off != buf.len() {
148 truncated = true; }
150 (next, records, truncated)
151}
152
153#[must_use]
156pub fn encode_buffer(next: u64, records: &[UsnRecord]) -> Vec<u8> {
157 let mut out = Vec::new();
158 out.extend_from_slice(&next.to_le_bytes());
159 for r in records {
160 let name_bytes: Vec<u8> = r.name.iter().flat_map(|u| u.to_le_bytes()).collect();
161 let len = (60 + name_bytes.len()).next_multiple_of(8);
162 let start = out.len();
163 out.resize(start + len, 0);
164 let w = &mut out[start..];
165 w[0..4].copy_from_slice(&(len as u32).to_le_bytes());
166 w[4..6].copy_from_slice(&2u16.to_le_bytes()); w[8..16].copy_from_slice(&r.frn.to_le_bytes());
168 w[16..24].copy_from_slice(&r.parent_frn.to_le_bytes());
169 w[24..32].copy_from_slice(&(r.usn as u64).to_le_bytes());
170 w[40..44].copy_from_slice(&r.reason.to_le_bytes());
171 w[52..56].copy_from_slice(&r.attributes.to_le_bytes());
172 w[56..58].copy_from_slice(&(name_bytes.len() as u16).to_le_bytes());
173 w[58..60].copy_from_slice(&60u16.to_le_bytes());
174 w[60..60 + name_bytes.len()].copy_from_slice(&name_bytes);
175 }
176 out
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 fn rec(frn: u64, parent: u64, reason: u32, name: &str) -> UsnRecord {
184 UsnRecord {
185 usn: 1000,
186 frn,
187 parent_frn: parent,
188 reason,
189 attributes: 0x20,
190 name: name.encode_utf16().collect(),
191 }
192 }
193
194 #[test]
195 fn roundtrip() {
196 let records = vec![
197 rec(
198 0x1_0000_0000_0007,
199 5,
200 reason::FILE_CREATE | reason::CLOSE,
201 "new file.txt",
202 ),
203 rec(
204 0x2_0000_0000_0008,
205 5,
206 reason::FILE_DELETE | reason::CLOSE,
207 "夢.dat",
208 ),
209 ];
210 let buf = encode_buffer(42, &records);
211 let (next, parsed, truncated) = parse_buffer(&buf);
212 assert!(!truncated);
213 assert_eq!(next, 42);
214 assert_eq!(parsed, records);
215 }
216
217 #[test]
218 fn truncated_tail_is_dropped() {
219 let records = vec![rec(7, 5, reason::FILE_CREATE, "abc.txt")];
220 let mut buf = encode_buffer(9, &records);
221 buf.truncate(buf.len() - 4);
222 let (next, parsed, truncated) = parse_buffer(&buf);
223 assert!(truncated);
224 assert_eq!(next, 9);
225 assert!(parsed.is_empty());
226 }
227
228 #[test]
229 fn empty_buffer() {
230 assert_eq!(parse_buffer(&[]).1, vec![]);
231 assert_eq!(parse_buffer(&7u64.to_le_bytes()).1, vec![]);
232 }
233}