@@ -11,11 +11,11 @@ pub mod component {
11
11
Empty ,
12
12
#[ error( "Path separators like / or \\ are not allowed" ) ]
13
13
PathSeparator ,
14
- #[ error( "Window path prefixes are not allowed" ) ]
14
+ #[ error( "Windows path prefixes are not allowed" ) ]
15
15
WindowsPathPrefix ,
16
16
#[ error( "Windows device-names may have side-effects and are not allowed" ) ]
17
17
WindowsReservedName ,
18
- #[ error( "Trailing spaces or dots and the following characters are forbidden in Windows paths, along with non-printable ones: <>:\" |?*" ) ]
18
+ #[ error( "Trailing spaces or dots, and the following characters anywhere, are forbidden in Windows paths, along with non-printable ones: <>:\" |?*" ) ]
19
19
WindowsIllegalCharacter ,
20
20
#[ error( "The .git name may never be used" ) ]
21
21
DotGitDir ,
@@ -37,7 +37,7 @@ pub mod component {
37
37
/// This field is equivalent to `core.protectHFS`.
38
38
pub protect_hfs : bool ,
39
39
/// If `true`, protections for Windows NTFS specific features will be active. This adds special handling
40
- /// for `8.3` filenames and alternate data streams, both of which could be used to mask th etrue name of
40
+ /// for `8.3` filenames and alternate data streams, both of which could be used to mask the true name of
41
41
/// what would be created on disk.
42
42
///
43
43
/// This field is equivalent to `core.protectNTFS`.
@@ -64,7 +64,7 @@ pub mod component {
64
64
65
65
/// Assure the given `input` resembles a valid name for a tree or blob, and in that sense, a path component.
66
66
/// `mode` indicates the kind of `input` and it should be `Some` if `input` is the last component in the underlying
67
- /// path. Currently, this is only used to determine if `.gitmodules` is a symlink.
67
+ /// path.
68
68
///
69
69
/// `input` must not make it possible to exit the repository, or to specify absolute paths.
70
70
pub fn component (
@@ -148,7 +148,8 @@ fn check_win_devices_and_illegal_characters(input: &BStr) -> Option<component::E
148
148
return Some ( component:: Error :: WindowsReservedName ) ;
149
149
}
150
150
if in3. eq_ignore_ascii_case ( b"con" )
151
- && ( ( input. get ( 3 ..6 ) . map_or ( false , |n| n. eq_ignore_ascii_case ( b"in$" ) ) && is_done_windows ( input. get ( 6 ..) ) )
151
+ && ( 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 ..) ) )
152
153
|| ( input. get ( 3 ..7 ) . map_or ( false , |n| n. eq_ignore_ascii_case ( b"out$" ) ) && is_done_windows ( input. get ( 7 ..) ) ) )
153
154
{
154
155
return Some ( component:: Error :: WindowsReservedName ) ;
@@ -168,22 +169,26 @@ fn is_symlink(mode: Option<component::Mode>) -> bool {
168
169
169
170
fn is_dot_hfs ( input : & BStr , search_case_insensitive : & str ) -> bool {
170
171
let mut input = input. chars ( ) . filter ( |c| match * c as u32 {
171
- 0x200c | /* ZERO WIDTH NON-JOINER */
172
- 0x200d | /* ZERO WIDTH JOINER */
173
- 0x200e | /* LEFT-TO-RIGHT MARK */
174
- 0x200f | /* RIGHT-TO-LEFT MARK */
175
- 0x202a | /* LEFT-TO-RIGHT EMBEDDING */
176
- 0x202b | /* RIGHT-TO-LEFT EMBEDDING */
177
- 0x202c | /* POP DIRECTIONAL FORMATTING */
178
- 0x202d | /* LEFT-TO-RIGHT OVERRIDE */
179
- 0x202e | /* RIGHT-TO-LEFT OVERRIDE */
180
- 0x206a | /* INHIBIT SYMMETRIC SWAPPING */
181
- 0x206b | /* ACTIVATE SYMMETRIC SWAPPING */
182
- 0x206c | /* INHIBIT ARABIC FORM SHAPING */
183
- 0x206d | /* ACTIVATE ARABIC FORM SHAPING */
184
- 0x206e | /* NATIONAL DIGIT SHAPES */
185
- 0x206f | /* NOMINAL DIGIT SHAPES */
186
- 0xfeff => false , /* ZERO WIDTH NO-BREAK SPACE */
172
+ // Case-insensitive HFS+ skips these code points as "ignorable" when comparing filenames. See:
173
+ // https://github.com/git/git/commit/6162a1d323d24fd8cbbb1a6145a91fb849b2568f
174
+ // https://developer.apple.com/library/archive/technotes/tn/tn1150.html#StringComparisonAlgorithm
175
+ // https://github.com/apple-oss-distributions/hfs/blob/main/core/UCStringCompareData.h
176
+ 0x200c | // ZERO WIDTH NON-JOINER
177
+ 0x200d | // ZERO WIDTH JOINER
178
+ 0x200e | // LEFT-TO-RIGHT MARK
179
+ 0x200f | // RIGHT-TO-LEFT MARK
180
+ 0x202a | // LEFT-TO-RIGHT EMBEDDING
181
+ 0x202b | // RIGHT-TO-LEFT EMBEDDING
182
+ 0x202c | // POP DIRECTIONAL FORMATTING
183
+ 0x202d | // LEFT-TO-RIGHT OVERRIDE
184
+ 0x202e | // RIGHT-TO-LEFT OVERRIDE
185
+ 0x206a | // INHIBIT SYMMETRIC SWAPPING
186
+ 0x206b | // ACTIVATE SYMMETRIC SWAPPING
187
+ 0x206c | // INHIBIT ARABIC FORM SHAPING
188
+ 0x206d | // ACTIVATE ARABIC FORM SHAPING
189
+ 0x206e | // NATIONAL DIGIT SHAPES
190
+ 0x206f | // NOMINAL DIGIT SHAPES
191
+ 0xfeff => false , // ZERO WIDTH NO-BREAK SPACE
187
192
_ => true
188
193
} ) ;
189
194
if input. next ( ) != Some ( '.' ) {
@@ -278,7 +283,9 @@ fn is_dot_ntfs(input: &BStr, search_case_insensitive: &str, ntfs_shortname_prefi
278
283
}
279
284
}
280
285
286
+ /// Check if trailing filename bytes leave a match to special files like `.git` unchanged in NTFS.
281
287
fn is_done_ntfs ( input : Option < & [ u8 ] > ) -> bool {
288
+ // Skip spaces and dots. Then return true if we are at the end or a colon.
282
289
let Some ( input) = input else { return true } ;
283
290
for b in input. bytes ( ) {
284
291
if b == b':' {
@@ -291,9 +298,11 @@ fn is_done_ntfs(input: Option<&[u8]>) -> bool {
291
298
true
292
299
}
293
300
301
+ /// Check if trailing filename bytes leave a match to Windows reserved device names unchanged.
294
302
fn is_done_windows ( input : Option < & [ u8 ] > ) -> bool {
303
+ // Skip spaces. Then return true if we are at the end or a dot or colon.
295
304
let Some ( input) = input else { return true } ;
296
305
let skip = input. bytes ( ) . take_while ( |b| * b == b' ' ) . count ( ) ;
297
306
let Some ( next) = input. get ( skip) else { return true } ;
298
- ! ( * next != b'.' && * next != b':' )
307
+ * next == b'.' || * next == b':'
299
308
}
0 commit comments