rustferry_core/
file_streams.rs1#![cfg_attr(windows, allow(unsafe_code))]
4
5use std::{error::Error, fmt, fs::File};
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9#[non_exhaustive]
10pub enum RegularFileStreamErrorKind {
11 NotRegularFile,
13 QueryFailed,
15 ResponseTooLarge,
17 MalformedResponse,
19 NamedStream,
21}
22
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub struct RegularFileStreamError {
26 kind: RegularFileStreamErrorKind,
27 os_code: Option<i32>,
28}
29
30impl RegularFileStreamError {
31 const fn new(kind: RegularFileStreamErrorKind, os_code: Option<i32>) -> Self {
32 Self { kind, os_code }
33 }
34
35 pub const fn kind(self) -> RegularFileStreamErrorKind {
37 self.kind
38 }
39
40 pub const fn os_code(self) -> Option<i32> {
42 self.os_code
43 }
44}
45
46impl fmt::Display for RegularFileStreamError {
47 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
48 match self.kind {
49 RegularFileStreamErrorKind::NotRegularFile => {
50 formatter.write_str("stream inspection requires a retained regular-file handle")
51 }
52 RegularFileStreamErrorKind::QueryFailed => {
53 if let Some(code) = self.os_code {
54 write!(
55 formatter,
56 "retained file stream query failed with OS error {code}"
57 )
58 } else {
59 formatter.write_str("retained file stream query failed")
60 }
61 }
62 RegularFileStreamErrorKind::ResponseTooLarge => {
63 formatter.write_str("retained file stream metadata exceeded its fixed bound")
64 }
65 RegularFileStreamErrorKind::MalformedResponse => {
66 formatter.write_str("retained file stream metadata was malformed")
67 }
68 RegularFileStreamErrorKind::NamedStream => {
69 formatter.write_str("retained regular file has a named alternate data stream")
70 }
71 }
72 }
73}
74
75impl Error for RegularFileStreamError {}
76
77pub fn verify_regular_file_has_no_named_streams(file: &File) -> Result<(), RegularFileStreamError> {
88 let metadata = file.metadata().map_err(|error| {
89 RegularFileStreamError::new(
90 RegularFileStreamErrorKind::QueryFailed,
91 error.raw_os_error(),
92 )
93 })?;
94 if !metadata.is_file() {
95 return Err(RegularFileStreamError::new(
96 RegularFileStreamErrorKind::NotRegularFile,
97 None,
98 ));
99 }
100
101 #[cfg(windows)]
102 return windows::verify(file);
103
104 #[cfg(not(windows))]
105 Ok(())
106}
107
108#[cfg(windows)]
109mod windows {
110 use std::{fs::File, io, os::windows::io::AsRawHandle as _, slice};
111
112 use windows_sys::Win32::{
113 Foundation::{ERROR_INSUFFICIENT_BUFFER, ERROR_MORE_DATA},
114 Storage::FileSystem::{FileStreamInfo, GetFileInformationByHandleEx},
115 };
116
117 use super::{RegularFileStreamError, RegularFileStreamErrorKind};
118
119 const INITIAL_QUERY_BYTES: usize = 1_024;
120 const MAX_QUERY_BYTES: usize = 1024 * 1024;
121 const ENTRY_HEADER_BYTES: usize = 24;
122 pub(super) fn verify(file: &File) -> Result<(), RegularFileStreamError> {
123 let mut capacity = INITIAL_QUERY_BYTES;
124 loop {
125 let mut storage = vec![0_u64; capacity / size_of::<u64>()];
126 let succeeded = unsafe {
127 GetFileInformationByHandleEx(
128 file.as_raw_handle(),
129 FileStreamInfo,
130 storage.as_mut_ptr().cast(),
131 u32::try_from(capacity).expect("bounded stream query capacity fits u32"),
132 )
133 };
134 if succeeded != 0 {
135 let bytes =
136 unsafe { slice::from_raw_parts(storage.as_ptr().cast::<u8>(), capacity) };
137 return parse_streams(bytes);
138 }
139
140 let os_code = io::Error::last_os_error().raw_os_error();
141 let retryable = os_code
142 .and_then(|code| u32::try_from(code).ok())
143 .is_some_and(|code| code == ERROR_MORE_DATA || code == ERROR_INSUFFICIENT_BUFFER);
144 if !retryable {
145 return Err(RegularFileStreamError::new(
146 RegularFileStreamErrorKind::QueryFailed,
147 os_code,
148 ));
149 }
150 if capacity == MAX_QUERY_BYTES {
151 return Err(RegularFileStreamError::new(
152 RegularFileStreamErrorKind::ResponseTooLarge,
153 None,
154 ));
155 }
156 capacity = capacity.saturating_mul(2).min(MAX_QUERY_BYTES);
157 }
158 }
159
160 fn parse_streams(bytes: &[u8]) -> Result<(), RegularFileStreamError> {
161 let mut offset = 0_usize;
162 let mut entries = 0_usize;
163 loop {
164 let next = read_u32(bytes, offset)? as usize;
165 let name_bytes = read_u32(bytes, offset + 4)? as usize;
166 if name_bytes == 0 || !name_bytes.is_multiple_of(size_of::<u16>()) {
167 return Err(malformed());
168 }
169 let name_start = offset
170 .checked_add(ENTRY_HEADER_BYTES)
171 .ok_or_else(malformed)?;
172 let name_end = name_start.checked_add(name_bytes).ok_or_else(malformed)?;
173 let entry_end = if next == 0 {
174 bytes.len()
175 } else {
176 if !next.is_multiple_of(align_of::<u64>()) || next < ENTRY_HEADER_BYTES + name_bytes
177 {
178 return Err(malformed());
179 }
180 offset.checked_add(next).ok_or_else(malformed)?
181 };
182 if name_end > entry_end || entry_end > bytes.len() {
183 return Err(malformed());
184 }
185 if !is_default_stream(&bytes[name_start..name_end]) {
186 return Err(RegularFileStreamError::new(
187 RegularFileStreamErrorKind::NamedStream,
188 None,
189 ));
190 }
191 entries = entries.checked_add(1).ok_or_else(malformed)?;
192 if entries != 1 || next == 0 {
193 break;
194 }
195 offset = entry_end;
196 }
197 if entries == 1 {
198 Ok(())
199 } else {
200 Err(malformed())
201 }
202 }
203
204 fn read_u32(bytes: &[u8], offset: usize) -> Result<u32, RegularFileStreamError> {
205 let end = offset.checked_add(size_of::<u32>()).ok_or_else(malformed)?;
206 let raw: [u8; 4] = bytes
207 .get(offset..end)
208 .ok_or_else(malformed)?
209 .try_into()
210 .map_err(|_| malformed())?;
211 Ok(u32::from_ne_bytes(raw))
212 }
213
214 fn is_default_stream(name: &[u8]) -> bool {
215 if name.len() != 14 {
216 return false;
217 }
218 let mut units = [0_u16; 7];
219 for (index, chunk) in name.chunks_exact(2).enumerate() {
220 units[index] = u16::from_ne_bytes([chunk[0], chunk[1]]);
221 }
222 units[0] == u16::from(b':')
223 && units[1] == u16::from(b':')
224 && units[2] == u16::from(b'$')
225 && units[3..].iter().zip(b"DATA").all(|(actual, expected)| {
226 *actual == u16::from(*expected)
227 || *actual == u16::from(expected.to_ascii_lowercase())
228 })
229 }
230
231 const fn malformed() -> RegularFileStreamError {
232 RegularFileStreamError::new(RegularFileStreamErrorKind::MalformedResponse, None)
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use std::{fs, fs::File};
239
240 use super::{RegularFileStreamErrorKind, verify_regular_file_has_no_named_streams};
241
242 #[test]
243 fn regular_file_without_named_streams_is_accepted() {
244 let temporary = tempfile::tempdir().unwrap();
245 let path = temporary.path().join("source.txt");
246 fs::write(&path, b"source").unwrap();
247 verify_regular_file_has_no_named_streams(&File::open(path).unwrap()).unwrap();
248 }
249
250 #[cfg(not(windows))]
251 #[test]
252 fn directory_handle_is_not_a_regular_file() {
253 let temporary = tempfile::tempdir().unwrap();
254 let error =
255 verify_regular_file_has_no_named_streams(&File::open(temporary.path()).unwrap())
256 .unwrap_err();
257 assert_eq!(error.kind(), RegularFileStreamErrorKind::NotRegularFile);
258 }
259
260 #[cfg(windows)]
261 #[test]
262 fn retained_handle_rejects_named_stream_after_path_replacement() {
263 use std::{fs::OpenOptions, os::windows::fs::OpenOptionsExt as _};
264
265 const FILE_SHARE_READ: u32 = 0x0000_0001;
266 const FILE_SHARE_WRITE: u32 = 0x0000_0002;
267 const FILE_SHARE_DELETE: u32 = 0x0000_0004;
268
269 let temporary = tempfile::tempdir().unwrap();
270 let original = temporary.path().join("source.txt");
271 let moved = temporary.path().join("moved.txt");
272 fs::write(&original, b"source").unwrap();
273 fs::write(format!("{}:private", original.display()), b"credential").unwrap();
274 let retained = OpenOptions::new()
275 .read(true)
276 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE)
277 .open(&original)
278 .unwrap();
279 fs::rename(&original, &moved).unwrap();
280 fs::write(&original, b"clean replacement").unwrap();
281
282 let error = verify_regular_file_has_no_named_streams(&retained).unwrap_err();
283 assert_eq!(error.kind(), RegularFileStreamErrorKind::NamedStream);
284 assert!(!error.to_string().contains("private"));
285 verify_regular_file_has_no_named_streams(&File::open(original).unwrap()).unwrap();
286 }
287}