1use std::io;
2
3/// Observed directory metadata, not a stable notebook identity or a file snapshot.
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct DirectoryEntry {
6 pub name: String,
7 pub size: u64,
8 /// LastWriteTime, FILETIME.
9 pub modified: u64,
10 /// MS-FSCC file attributes; directory is 0x10 and reparse point is 0x400.
11 pub attributes: u32,
12}
13
14pub(super) fn decode(mut bytes: &[u8]) -> io::Result<Vec<DirectoryEntry>> {
15 if bytes.len() > 65536 {
16 return Err(io::ErrorKind::InvalidData.into());
17 }
18 let mut entries = Vec::new();
19 loop {
20 if bytes.len() < 64 {
21 return Err(io::ErrorKind::InvalidData.into());
22 }
23 let next = u32::from_le_bytes(bytes[..4].try_into().unwrap()) as usize;
24 let length = u32::from_le_bytes(bytes[60..64].try_into().unwrap()) as usize;
25 let end = 64usize
26 .checked_add(length)
27 .ok_or(io::ErrorKind::InvalidData)?;
28 if length == 0
29 || !length.is_multiple_of(2)
30 || end > bytes.len()
31 || (next != 0 && (next < end || !next.is_multiple_of(8) || next >= bytes.len()))
32 || (next == 0 && bytes.len() - end > 7)
33 {
34 return Err(io::ErrorKind::InvalidData.into());
35 }
36 let units: Vec<_> = bytes[64..end]
37 .chunks_exact(2)
38 .map(|unit| u16::from_le_bytes([unit[0], unit[1]]))
39 .collect();
40 let name = String::from_utf16(&units).map_err(|_| io::ErrorKind::InvalidData)?;
41 if name.contains(['\0', '/', '\\']) {
42 return Err(io::ErrorKind::InvalidData.into());
43 }
44 let size = i64::from_le_bytes(bytes[40..48].try_into().unwrap());
45 entries.push(DirectoryEntry {
46 name,
47 size: size.try_into().map_err(|_| io::ErrorKind::InvalidData)?,
48 modified: u64::from_le_bytes(bytes[24..32].try_into().unwrap()),
49 attributes: u32::from_le_bytes(bytes[56..60].try_into().unwrap()),
50 });
51 if next == 0 {
52 return Ok(entries);
53 }
54 bytes = &bytes[next..];
55 }
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61
62 fn record(name: &str, size: u64, attributes: u32) -> Vec<u8> {
63 let mut bytes = vec![0; 64];
64 bytes[24..32].copy_from_slice(&(size + 7).to_le_bytes());
65 bytes[40..48].copy_from_slice(&size.to_le_bytes());
66 bytes[56..60].copy_from_slice(&attributes.to_le_bytes());
67 let name: Vec<_> = name.encode_utf16().flat_map(u16::to_le_bytes).collect();
68 bytes[60..64].copy_from_slice(&(name.len() as u32).to_le_bytes());
69 bytes.extend(name);
70 bytes
71 }
72
73 #[test]
74 fn directory_records_preserve_names_sizes_and_attributes() {
75 let mut bytes = Vec::new();
76 let mut expected = Vec::new();
77 for i in 0..300u32 {
78 let name = format!("Section {i} 🦀 e\u{301}.one");
79 let size = u64::from(i) * 100_000_000;
80 let attributes = if i % 3 == 0 { 0x410 } else { 0x20 };
81 let mut entry = record(&name, size, attributes);
82 if i != 299 {
83 entry.resize(entry.len().next_multiple_of(8), 0xa5);
84 let length = entry.len() as u32;
85 entry[..4].copy_from_slice(&length.to_le_bytes());
86 }
87 bytes.extend(entry);
88 expected.push(DirectoryEntry {
89 name,
90 size,
91 modified: size + 7,
92 attributes,
93 });
94 }
95 assert_eq!(decode(&bytes).unwrap(), expected);
96 for end in 0..bytes.len() {
97 assert!(decode(&bytes[..end]).is_err(), "accepted prefix {end}");
98 }
99 }
100
101 #[test]
102 fn invalid_directory_records_never_become_partial_results() {
103 assert!(decode(&vec![0; 65537]).is_err());
104 for name in ["", "bad\0name", "a/b", "a\\b"] {
105 assert!(decode(&record(name, 0, 0)).is_err());
106 }
107 let valid = record("a.one", 12, 0x20);
108 for (at, value) in [
109 (0, 8),
110 (0, 65),
111 (0, 72),
112 (0, u32::MAX),
113 (60, 0),
114 (60, 1),
115 (60, u32::MAX),
116 (44, u32::MAX),
117 ] {
118 let mut bytes = valid.clone();
119 bytes[at..at + 4].copy_from_slice(&value.to_le_bytes());
120 assert!(decode(&bytes).is_err(), "offset={at} value={value}");
121 }
122 let mut bytes = valid.clone();
123 bytes[64..66].copy_from_slice(&0xd800u16.to_le_bytes());
124 assert!(decode(&bytes).is_err());
125 let mut bytes = valid;
126 bytes.extend_from_slice(&[0; 8]);
127 assert!(decode(&bytes).is_err());
128 for name in [".", ".."] {
129 assert_eq!(decode(&record(name, 0, 0x10)).unwrap()[0].name, name);
130 }
131 }
132}