Skip to content

Commit 3d4542c

Browse files
authored
Correctly reject device files with multi-dot filename extensions. (#378)
* Correctly reject device files with multi-dot filename extensions. When rejecting device names such as "CON" and "CON.txt", reject filenames with multiple-dot extensions too, such as "CON.txt.gz". * Strip trailing whitespace too.
1 parent cc7d2b9 commit 3d4542c

File tree

2 files changed

+76
-26
lines changed

2 files changed

+76
-26
lines changed

cap-primitives/src/windows/fs/open_impl.rs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::fs::{manually, OpenOptions};
2+
use std::ffi::OsStr;
23
use std::path::Path;
34
use std::{fs, io};
45
use windows_sys::Win32::Foundation::ERROR_FILE_NOT_FOUND;
@@ -11,9 +12,9 @@ pub(crate) fn open_impl(
1112
// Windows reserves several special device paths. Disallow opening any
1213
// of them.
1314
// See: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions
14-
if let Some(stem) = path.file_stem() {
15+
if let Some(stem) = file_prefix(path) {
1516
if let Some(stemstr) = stem.to_str() {
16-
match stemstr.to_uppercase().as_str() {
17+
match stemstr.trim_end().to_uppercase().as_str() {
1718
"CON" | "PRN" | "AUX" | "NUL" | "COM0" | "COM1" | "COM2" | "COM3" | "COM4"
1819
| "COM5" | "COM6" | "COM7" | "COM8" | "COM9" | "COM¹" | "COM²" | "COM³"
1920
| "LPT0" | "LPT1" | "LPT2" | "LPT3" | "LPT4" | "LPT5" | "LPT6" | "LPT7"
@@ -27,3 +28,39 @@ pub(crate) fn open_impl(
2728

2829
manually::open(start, path, options)
2930
}
31+
32+
// TODO: Replace this with `Path::file_prefix` once that's stable. For now,
33+
// we use a copy of the code. This code is derived from
34+
// https://github.com/rust-lang/rust/blob/9fe9041cc8eddaed402d17aa4facb2ce8f222e95/library/std/src/path.rs#L2648
35+
fn file_prefix(path: &Path) -> Option<&OsStr> {
36+
path.file_name()
37+
.map(split_file_at_dot)
38+
.and_then(|(before, _after)| Some(before))
39+
}
40+
41+
// This code is derived from
42+
// https://github.com/rust-lang/rust/blob/9fe9041cc8eddaed402d17aa4facb2ce8f222e95/library/std/src/path.rs#L340
43+
#[allow(unsafe_code)]
44+
fn split_file_at_dot(file: &OsStr) -> (&OsStr, Option<&OsStr>) {
45+
let slice = file.as_encoded_bytes();
46+
if slice == b".." {
47+
return (file, None);
48+
}
49+
50+
// The unsafety here stems from converting between &OsStr and &[u8]
51+
// and back. This is safe to do because (1) we only look at ASCII
52+
// contents of the encoding and (2) new &OsStr values are produced
53+
// only from ASCII-bounded slices of existing &OsStr values.
54+
let i = match slice[1..].iter().position(|b| *b == b'.') {
55+
Some(i) => i + 1,
56+
None => return (file, None),
57+
};
58+
let before = &slice[..i];
59+
let after = &slice[i + 1..];
60+
unsafe {
61+
(
62+
OsStr::from_encoded_bytes_unchecked(before),
63+
Some(OsStr::from_encoded_bytes_unchecked(after)),
64+
)
65+
}
66+
}

tests/windows-open.rs

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -134,30 +134,43 @@ fn windows_open_special() {
134134
// Opening any of these should fail.
135135
for device in &[
136136
"CON", "PRN", "AUX", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7",
137-
"COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8",
138-
"LPT9",
137+
"COM8", "COM9", "COM¹", "COM²", "COM³", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5",
138+
"LPT6", "LPT7", "LPT8", "LPT9", "LPT¹", "LPT²", "LPT³",
139139
] {
140-
tmpdir.open(device).unwrap_err();
141-
tmpdir.open(&format!(".\\{}", device)).unwrap_err();
142-
tmpdir.open(&format!("{}.ext", device)).unwrap_err();
143-
tmpdir.open(&format!(".\\{}.ext", device)).unwrap_err();
144-
145-
let mut options = cap_std::fs::OpenOptions::new();
146-
options.write(true);
147-
tmpdir.open_with(device, &options).unwrap_err();
148-
tmpdir
149-
.open_with(&format!(".\\{}", device), &options)
150-
.unwrap_err();
151-
tmpdir
152-
.open_with(&format!("{}.ext", device), &options)
153-
.unwrap_err();
154-
tmpdir
155-
.open_with(&format!(".\\{}.ext", device), &options)
156-
.unwrap_err();
157-
158-
tmpdir.create(device).unwrap_err();
159-
tmpdir.create(&format!(".\\{}", device)).unwrap_err();
160-
tmpdir.create(&format!("{}.ext", device)).unwrap_err();
161-
tmpdir.create(&format!(".\\{}.ext", device)).unwrap_err();
140+
for suffix in &[
141+
"",
142+
" ",
143+
".",
144+
". ",
145+
".ext",
146+
".ext.",
147+
".ext. ",
148+
".ext ",
149+
".ext.more",
150+
".ext.more.",
151+
".ext.more ",
152+
".ext.more. ",
153+
".ext.more .",
154+
] {
155+
let name = format!("{}{}", device, suffix);
156+
eprintln!("testing '{}'", name);
157+
158+
match tmpdir.open(&name).unwrap_err().kind() {
159+
std::io::ErrorKind::NotFound | std::io::ErrorKind::PermissionDenied => {}
160+
kind => panic!("unexpected error: {:?}", kind),
161+
}
162+
163+
let mut options = cap_std::fs::OpenOptions::new();
164+
options.write(true);
165+
match tmpdir.open_with(&name, &options).unwrap_err().kind() {
166+
std::io::ErrorKind::NotFound | std::io::ErrorKind::PermissionDenied => {}
167+
kind => panic!("unexpected error: {:?}", kind),
168+
}
169+
170+
match tmpdir.create(&name).unwrap_err().kind() {
171+
std::io::ErrorKind::NotFound | std::io::ErrorKind::PermissionDenied => {}
172+
kind => panic!("unexpected error: {:?}", kind),
173+
}
174+
}
162175
}
163176
}

0 commit comments

Comments
 (0)