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 .as_chunks::<2>()
38 .0
39 .iter()
40 .map(|unit| u16::from_le_bytes([unit[0], unit[1]]))
41 .collect();
42 let name = String::from_utf16(&units).map_err(|_| io::ErrorKind::InvalidData)?;
43 if name.contains(['\0', '/', '\\']) {
44 return Err(io::ErrorKind::InvalidData.into());
45 }
46 let size = i64::from_le_bytes(bytes[40..48].try_into().unwrap());
47 entries.push(DirectoryEntry {
48 name,
49 size: size.try_into().map_err(|_| io::ErrorKind::InvalidData)?,
50 modified: u64::from_le_bytes(bytes[24..32].try_into().unwrap()),
51 attributes: u32::from_le_bytes(bytes[56..60].try_into().unwrap()),
52 });
53 if next == 0 {
54 return Ok(entries);
55 }
56 bytes = &bytes[next..];
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 fn record(name: &str, size: u64, attributes: u32) -> Vec<u8> {
65 let mut bytes = vec![0; 64];
66 bytes[24..32].copy_from_slice(&(size + 7).to_le_bytes());
67 bytes[40..48].copy_from_slice(&size.to_le_bytes());
68 bytes[56..60].copy_from_slice(&attributes.to_le_bytes());
69 let name: Vec<_> = name.encode_utf16().flat_map(u16::to_le_bytes).collect();
70 bytes[60..64].copy_from_slice(&(name.len() as u32).to_le_bytes());
71 bytes.extend(name);
72 bytes
73 }
74
75 #[test]
76 fn directory_records_preserve_names_sizes_and_attributes() {
77 let mut bytes = Vec::new();
78 let mut expected = Vec::new();
79 for i in 0..300u32 {
80 let name = format!("Section {i} 🦀 e\u{301}.one");
81 let size = u64::from(i) * 100_000_000;
82 let attributes = if i % 3 == 0 { 0x410 } else { 0x20 };
83 let mut entry = record(&name, size, attributes);
84 if i != 299 {
85 entry.resize(entry.len().next_multiple_of(8), 0xa5);
86 let length = entry.len() as u32;
87 entry[..4].copy_from_slice(&length.to_le_bytes());
88 }
89 bytes.extend(entry);
90 expected.push(DirectoryEntry {
91 name,
92 size,
93 modified: size + 7,
94 attributes,
95 });
96 }
97 assert_eq!(decode(&bytes).unwrap(), expected);
98 for end in 0..bytes.len() {
99 assert!(decode(&bytes[..end]).is_err(), "accepted prefix {end}");
100 }
101 }
102
103 #[test]
104 fn invalid_directory_records_never_become_partial_results() {
105 assert!(decode(&vec![0; 65537]).is_err());
106 for name in ["", "bad\0name", "a/b", "a\\b"] {
107 assert!(decode(&record(name, 0, 0)).is_err());
108 }
109 let valid = record("a.one", 12, 0x20);
110 for (at, value) in [
111 (0, 8),
112 (0, 65),
113 (0, 72),
114 (0, u32::MAX),
115 (60, 0),
116 (60, 1),
117 (60, u32::MAX),
118 (44, u32::MAX),
119 ] {
120 let mut bytes = valid.clone();
121 bytes[at..at + 4].copy_from_slice(&value.to_le_bytes());
122 assert!(decode(&bytes).is_err(), "offset={at} value={value}");
123 }
124 let mut bytes = valid.clone();
125 bytes[64..66].copy_from_slice(&0xd800u16.to_le_bytes());
126 assert!(decode(&bytes).is_err());
127 let mut bytes = valid;
128 bytes.extend_from_slice(&[0; 8]);
129 assert!(decode(&bytes).is_err());
130 for name in [".", ".."] {
131 assert_eq!(decode(&record(name, 0, 0x10)).unwrap()[0].name, name);
132 }
133 }
134}