Skip to content

Commit fcc3b69

Browse files
ByronEliahKagan
andcommitted
address review comments
- assure `con` is checked for, and that it's not overzealous. - reduce code duplication - improve documentation about more obscure parts of the code, based on the description in [this commit](git/git@e7cb0b4) - upper-case device names in comparisons as this is their canonical form, which also is more recognizable for people who are looking for them. - make clear why there is asymmetry between COM and LPT numbers. - Don't make a partial control-character check, but a complete one (i.e. *b < 32|0x20) - Add more variants for stream type tests (as regression protection, the code doesn't really care) - various clarifications in path-related tests on Windows Co-authored-by: Eliah Kagan <[email protected]>
1 parent bad9a79 commit fcc3b69

File tree

11 files changed

+137
-91
lines changed

11 files changed

+137
-91
lines changed

gix-dir/src/walk/classify.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -163,13 +163,7 @@ pub fn path(
163163
stack
164164
.at_entry(
165165
rela_path.as_bstr(),
166-
disk_kind.map(|ft| {
167-
if ft.is_dir() {
168-
gix_index::entry::Mode::DIR
169-
} else {
170-
gix_index::entry::Mode::FILE
171-
}
172-
}),
166+
disk_kind.map(|ft| is_dir_to_mode(ft.is_dir())),
173167
ctx.objects,
174168
)
175169
.map(|platform| platform.excluded_kind())

gix-fs/tests/stack/mod.rs

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ fn path_join_handling() {
3737
let absolute = p("/absolute");
3838
assert!(
3939
absolute.is_relative(),
40-
"on Windows, absolute linux paths are considered relative"
40+
"on Windows, absolute Linux paths are considered relative (and relative to the current drive)"
4141
);
4242
let bs_absolute = p("\\absolute");
4343
assert!(
4444
absolute.is_relative(),
45-
"on Windows, strange single-backslash paths are relative"
45+
"on Windows, strange single-backslash paths are relative (and relative to the current drive)"
4646
);
4747
assert_eq!(
4848
p("relative").join(absolute),
@@ -58,7 +58,7 @@ fn path_join_handling() {
5858
assert_eq!(
5959
p("c:").join("relative"),
6060
p("c:relative"),
61-
"absolute + relative = strange joined result with missing slash - but that shouldn't usually happen"
61+
"absolute + relative = strange joined result with missing back-slash, but it's a valid path that works just like `c:\relative`"
6262
);
6363
assert_eq!(
6464
p("c:\\").join("relative"),
@@ -69,34 +69,52 @@ fn path_join_handling() {
6969
assert_eq!(
7070
p("\\\\?\\base").join(absolute),
7171
p("\\\\?\\base\\absolute"),
72-
"absolute1 + absolute2 = joined result with backslash"
72+
"absolute1 + unix-absolute2 = joined result with backslash"
73+
);
74+
assert_eq!(
75+
p("\\\\.\\base").join(absolute),
76+
p("\\\\.\\base\\absolute"),
77+
"absolute1 + absolute2 = joined result with backslash (device relative)"
7378
);
7479
assert_eq!(
7580
p("\\\\?\\base").join(bs_absolute),
7681
p("\\\\?\\base\\absolute"),
7782
"absolute1 + absolute2 = joined result"
7883
);
84+
assert_eq!(
85+
p("\\\\.\\base").join(bs_absolute),
86+
p("\\\\.\\base\\absolute"),
87+
"absolute1 + absolute2 = joined result (device relative)"
88+
);
7989

90+
assert_eq!(p("/").join("C:"), p("C:"), "unix-absolute + win-drive = win-drive");
8091
assert_eq!(
81-
p("/").join("C:"),
92+
p("d:/").join("C:"),
8293
p("C:"),
83-
"unix-absolute + win-absolute = win-absolute"
94+
"d-drive + c-drive = c-drive - interesting, as C: is supposed to be relative"
8495
);
8596
assert_eq!(
86-
p("/").join("C:/"),
97+
p("d:\\").join("C:\\"),
8798
p("C:\\"),
88-
"unix-absolute + win-absolute = win-result, strangely enough it changed the trailing slash to backslash, so better not have trailing slashes"
99+
"d-drive-with-bs + c-drive-with-bs = c-drive-with-bs - nothing special happens with backslashes"
89100
);
90101
assert_eq!(
91-
p("/").join("C:\\"),
102+
p("c:\\").join("\\\\.\\"),
103+
p("\\\\.\\"),
104+
"d-drive-with-bs + device-relative-unc = device-relative-unc"
105+
);
106+
assert_eq!(
107+
p("/").join("C:/"),
92108
p("C:\\"),
93-
"unix-absolute + win-absolute = win-result"
109+
"unix-absolute + win-drive = win-drive, strangely enough it changed the trailing slash to backslash, so better not have trailing slashes"
94110
);
111+
assert_eq!(p("/").join("C:\\"), p("C:\\"), "unix-absolute + win-drive = win-drive");
95112
assert_eq!(
96-
p("relative").join("C:"),
113+
p("\\\\.").join("C:"),
97114
p("C:"),
98-
"relative + win-absolute = win-result"
115+
"device-relative-unc + win-drive-relative = win-drive-relative - c: was supposed to be relative, but it's not acting like it."
99116
);
117+
assert_eq!(p("relative").join("C:"), p("C:"), "relative + win-drive = win-drive");
100118

101119
assert_eq!(
102120
p("/").join("\\\\localhost"),
@@ -202,6 +220,17 @@ fn relative_components_are_invalid() {
202220
if cfg!(windows) { ".\\a\\b" } else { "./a/b" },
203221
"dot is silently ignored"
204222
);
223+
s.make_relative_path_current("a//b/".as_ref(), &mut r)
224+
.expect("multiple-slashes are ignored");
225+
assert_eq!(
226+
r,
227+
Record {
228+
push_dir: 2,
229+
dirs: vec![".".into(), "./a".into()],
230+
push: 2,
231+
},
232+
"nothing changed"
233+
);
205234
}
206235

207236
#[test]
@@ -226,7 +255,7 @@ fn absolute_paths_are_invalid() -> crate::Result {
226255
assert_eq!(
227256
s.current(),
228257
p("./b\\"),
229-
"trailing back-slashes are fine both on Windows and unix - on Unix it's part fo the filename"
258+
"trailing backslashes are fine both on Windows and Unix - on Unix it's part fo the filename"
230259
);
231260

232261
#[cfg(windows)]
@@ -235,22 +264,28 @@ fn absolute_paths_are_invalid() -> crate::Result {
235264
assert_eq!(
236265
err.to_string(),
237266
"Input path \"\\\" contains relative or absolute components",
238-
"on windows, backslashes are considered absolute and replace the base if it is relative, \
267+
"on Windows, backslashes are considered absolute and replace the base if it is relative, \
239268
hence they are forbidden."
240269
);
241270

242271
let err = s.make_relative_path_current("c:".as_ref(), &mut r).unwrap_err();
243272
assert_eq!(
244273
err.to_string(),
245274
"Input path \"c:\" contains relative or absolute components",
246-
"on windows, drive-letters are also absolute"
275+
"on Windows, drive-letters without trailing backslash or slash are also absolute (even though they ought to be relative)"
276+
);
277+
let err = s.make_relative_path_current("c:\\".as_ref(), &mut r).unwrap_err();
278+
assert_eq!(
279+
err.to_string(),
280+
"Input path \"c:\\\" contains relative or absolute components",
281+
"on Windows, drive-letters are absolute, which is expected"
247282
);
248283

249284
s.make_relative_path_current("֍:".as_ref(), &mut r)?;
250285
assert_eq!(
251286
s.current().to_string_lossy(),
252287
".\\֍:",
253-
"on windows, any unicode character will do as virtual drive-letter actually with `subst`, \
288+
"on Windows, almost any unicode character will do as virtual drive-letter actually with `subst`, \
254289
but we just turn it into a presumably invalid path which is fine, i.e. we get a joined path"
255290
);
256291
let err = s
@@ -440,7 +475,7 @@ fn delegate_calls_are_consistent() -> crate::Result {
440475
dirs: dirs.clone(),
441476
push: 19,
442477
},
443-
"a backslash is a normal character outside of windows, so it's fine to have it as component"
478+
"a backslash is a normal character outside of Windows, so it's fine to have it as component"
444479
);
445480

446481
s.make_relative_path_current("\\".as_ref(), &mut r)?;

gix-status/src/index_as_worktree/function.rs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use crate::{
1919
types::{Error, Options},
2020
Change, Conflict, EntryStatus, Outcome, VisitEntry,
2121
},
22-
SymlinkCheck,
22+
is_dir_to_mode, SymlinkCheck,
2323
};
2424

2525
/// Calculates the changes that need to be applied to an `index` to match the state of the `worktree` and makes them
@@ -276,15 +276,7 @@ impl<'index> State<'_, 'index> {
276276
&mut |relative_path, case, is_dir, out| {
277277
self.attr_stack
278278
.set_case(case)
279-
.at_entry(
280-
relative_path,
281-
Some(if is_dir {
282-
gix_index::entry::Mode::DIR
283-
} else {
284-
gix_index::entry::Mode::FILE
285-
}),
286-
objects,
287-
)
279+
.at_entry(relative_path, Some(is_dir_to_mode(is_dir)), objects)
288280
.map_or(false, |platform| platform.matching_attributes(out))
289281
},
290282
)

gix-status/src/index_as_worktree_with_renames/mod.rs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub(super) mod function {
99
use crate::index_as_worktree::traits::{CompareBlobs, SubmoduleStatus};
1010
use crate::index_as_worktree_with_renames::function::rewrite::ModificationOrDirwalkEntry;
1111
use crate::index_as_worktree_with_renames::{Context, Entry, Error, Options, Outcome, RewriteSource, VisitEntry};
12+
use crate::is_dir_to_mode;
1213
use bstr::ByteSlice;
1314
use gix_worktree::stack::State;
1415
use std::borrow::Cow;
@@ -99,15 +100,7 @@ pub(super) mod function {
99100
.expect("can only be called if attributes are used in patterns");
100101
stack
101102
.set_case(case)
102-
.at_entry(
103-
relative_path,
104-
Some(if is_dir {
105-
gix_index::entry::Mode::DIR
106-
} else {
107-
gix_index::entry::Mode::FILE
108-
}),
109-
&objects,
110-
)
103+
.at_entry(relative_path, Some(is_dir_to_mode(is_dir)), &objects)
111104
.map_or(false, |platform| platform.matching_attributes(out))
112105
},
113106
excludes: excludes.as_mut(),

gix-status/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,11 @@ pub struct SymlinkCheck {
3232
}
3333

3434
mod stack;
35+
36+
fn is_dir_to_mode(is_dir: bool) -> gix_index::entry::Mode {
37+
if is_dir {
38+
gix_index::entry::Mode::DIR
39+
} else {
40+
gix_index::entry::Mode::FILE
41+
}
42+
}

gix-validate/src/path.rs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -126,35 +126,41 @@ pub fn component(
126126

127127
fn check_win_devices_and_illegal_characters(input: &BStr) -> Option<component::Error> {
128128
let in3 = input.get(..3)?;
129-
if in3.eq_ignore_ascii_case(b"aux") && is_done_windows(input.get(3..)) {
129+
if in3.eq_ignore_ascii_case(b"AUX") && is_done_windows(input.get(3..)) {
130130
return Some(component::Error::WindowsReservedName);
131131
}
132-
if in3.eq_ignore_ascii_case(b"nul") && is_done_windows(input.get(3..)) {
132+
if in3.eq_ignore_ascii_case(b"NUL") && is_done_windows(input.get(3..)) {
133133
return Some(component::Error::WindowsReservedName);
134134
}
135-
if in3.eq_ignore_ascii_case(b"prn") && is_done_windows(input.get(3..)) {
135+
if in3.eq_ignore_ascii_case(b"PRN") && is_done_windows(input.get(3..)) {
136136
return Some(component::Error::WindowsReservedName);
137137
}
138-
if in3.eq_ignore_ascii_case(b"com")
138+
// Note that the following allows `COM0`, even though `LPT0` is not allowed.
139+
// Even though tests seem to indicate that neither `LPT0` nor `COM0` are valid
140+
// device names, it's unclear this truly is the case in all possible versions and editions
141+
// of Windows.
142+
// Hence, justification for this asymmetry is merely to do exactly the same as Git does,
143+
// and to have exactly the same behaviour during validation (for worktree-writes).
144+
if in3.eq_ignore_ascii_case(b"COM")
139145
&& input.get(3).map_or(false, |n| *n >= b'1' && *n <= b'9')
140146
&& is_done_windows(input.get(4..))
141147
{
142148
return Some(component::Error::WindowsReservedName);
143149
}
144-
if in3.eq_ignore_ascii_case(b"lpt")
150+
if in3.eq_ignore_ascii_case(b"LPT")
145151
&& input.get(3).map_or(false, u8::is_ascii_digit)
146152
&& is_done_windows(input.get(4..))
147153
{
148154
return Some(component::Error::WindowsReservedName);
149155
}
150-
if in3.eq_ignore_ascii_case(b"con")
156+
if in3.eq_ignore_ascii_case(b"CON")
151157
&& (is_done_windows(input.get(3..))
152-
|| (input.get(3..6).map_or(false, |n| n.eq_ignore_ascii_case(b"in$")) && is_done_windows(input.get(6..)))
153-
|| (input.get(3..7).map_or(false, |n| n.eq_ignore_ascii_case(b"out$")) && is_done_windows(input.get(7..))))
158+
|| (input.get(3..6).map_or(false, |n| n.eq_ignore_ascii_case(b"IN$")) && is_done_windows(input.get(6..)))
159+
|| (input.get(3..7).map_or(false, |n| n.eq_ignore_ascii_case(b"OUT$")) && is_done_windows(input.get(7..))))
154160
{
155161
return Some(component::Error::WindowsReservedName);
156162
}
157-
if input.iter().any(|b| *b < 0x20 || b":<>\"|?*".contains(b)) {
163+
if input.iter().any(|b| b.is_ascii_control() || b":<>\"|?*".contains(b)) {
158164
return Some(component::Error::WindowsIllegalCharacter);
159165
}
160166
if input.ends_with(b".") || input.ends_with(b" ") {
@@ -225,6 +231,10 @@ fn is_dot_git_ntfs(input: &BStr) -> bool {
225231
false
226232
}
227233

234+
/// The `search_case_insensitive` name is the actual name to look for (in a case-insensitive way).
235+
/// Opposed to that there is the special `ntfs_shortname_prefix` which is derived from `search_case_insensitive`
236+
/// but looks more like a hash, one that NTFS uses to disambiguate things, for when there is a lot of files
237+
/// with the same prefix.
228238
fn is_dot_ntfs(input: &BStr, search_case_insensitive: &str, ntfs_shortname_prefix: &str) -> bool {
229239
if input.first() == Some(&b'.') {
230240
let end_pos = 1 + search_case_insensitive.len();
@@ -243,6 +253,8 @@ fn is_dot_ntfs(input: &BStr, search_case_insensitive: &str, ntfs_shortname_prefi
243253
.map_or(false, |(ntfs_prefix, first_6_of_input)| {
244254
first_6_of_input.eq_ignore_ascii_case(ntfs_prefix)
245255
&& input.get(6) == Some(&b'~')
256+
// It's notable that only `~1` to `~4` are possible before the disambiguation algorithm
257+
// switches to using the `ntfs_shortname_prefix`, which is checked hereafter.
246258
&& input.get(7).map_or(false, |num| (b'1'..=b'4').contains(num))
247259
})
248260
{

gix-validate/tests/path/mod.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ mod component {
6565
mktest!(conout_without_dollar_with_extension, b"conout.c");
6666
mktest!(conin_without_dollar_with_extension, b"conin.c");
6767
mktest!(conin_without_dollar, b"conin");
68+
mktest!(not_con, b"com");
69+
mktest!(also_not_con, b"co");
6870
mktest!(not_nul, b"null");
6971
mktest!(
7072
not_dot_gitmodules_shorter_hfs,
@@ -115,7 +117,7 @@ mod component {
115117
mktest!(empty, b"", Error::Empty);
116118
mktest!(dot_git_lower, b".git", Error::DotGitDir, NO_OPTS);
117119
mktest!(dot_git_lower_hfs, ".g\u{200c}it".as_bytes(), Error::DotGitDir);
118-
mktest!(dot_git_lower_hfs_simple, ".Git".as_bytes(), Error::DotGitDir);
120+
mktest!(dot_git_mixed_hfs_simple, b".Git", Error::DotGitDir);
119121
mktest!(dot_git_upper, b".GIT", Error::DotGitDir, NO_OPTS);
120122
mktest!(dot_git_upper_hfs, ".GIT\u{200e}".as_bytes(), Error::DotGitDir);
121123
mktest!(dot_git_upper_ntfs_8_3, b"GIT~1", Error::DotGitDir);
@@ -164,6 +166,30 @@ mod component {
164166
Symlink,
165167
ALL_OPTS
166168
);
169+
mktest!(
170+
dot_gitmodules_lower_ntfs_stream_default_implicit,
171+
b".gitmodules::$DATA",
172+
Error::SymlinkedGitModules,
173+
Symlink,
174+
ALL_OPTS
175+
);
176+
mktest!(
177+
ntfs_stream_default_implicit,
178+
b"file::$DATA",
179+
Error::WindowsIllegalCharacter
180+
);
181+
mktest!(
182+
ntfs_stream_default_explicit,
183+
b"file:$ANYTHING_REALLY:$DATA",
184+
Error::WindowsIllegalCharacter
185+
);
186+
mktest!(
187+
dot_gitmodules_lower_ntfs_stream_default_explicit,
188+
b".gitmodules:$DATA:$DATA",
189+
Error::SymlinkedGitModules,
190+
Symlink,
191+
ALL_OPTS
192+
);
167193
mktest!(
168194
not_gitmodules_trailing_space,
169195
b".gitmodules x ",
@@ -201,6 +227,7 @@ mod component {
201227
mktest!(nul_mixed, b"NuL", Error::WindowsReservedName);
202228
mktest!(prn_mixed_with_extension, b"PrN.abc", Error::WindowsReservedName);
203229
mktest!(con, b"CON", Error::WindowsReservedName);
230+
mktest!(con_with_extension, b"CON.abc", Error::WindowsReservedName);
204231
mktest!(
205232
conout_mixed_with_extension,
206233
b"ConOut$ .xyz",

gix/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,3 +345,12 @@ pub mod shallow;
345345
pub mod discover;
346346

347347
pub mod env;
348+
349+
#[cfg(feature = "index")]
350+
fn is_dir_to_mode(is_dir: bool) -> gix_index::entry::Mode {
351+
if is_dir {
352+
gix_index::entry::Mode::DIR
353+
} else {
354+
gix_index::entry::Mode::FILE
355+
}
356+
}

0 commit comments

Comments
 (0)