@@ -11,11 +11,11 @@ pub mod component {
1111 Empty ,
1212 #[ error( "Path separators like / or \\ are not allowed" ) ]
1313 PathSeparator ,
14- #[ error( "Window path prefixes are not allowed" ) ]
14+ #[ error( "Windows path prefixes are not allowed" ) ]
1515 WindowsPathPrefix ,
1616 #[ error( "Windows device-names may have side-effects and are not allowed" ) ]
1717 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: <>:\" |?*" ) ]
1919 WindowsIllegalCharacter ,
2020 #[ error( "The .git name may never be used" ) ]
2121 DotGitDir ,
@@ -37,7 +37,7 @@ pub mod component {
3737 /// This field is equivalent to `core.protectHFS`.
3838 pub protect_hfs : bool ,
3939 /// 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
4141 /// what would be created on disk.
4242 ///
4343 /// This field is equivalent to `core.protectNTFS`.
@@ -64,7 +64,7 @@ pub mod component {
6464
6565/// Assure the given `input` resembles a valid name for a tree or blob, and in that sense, a path component.
6666/// `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.
6868///
6969/// `input` must not make it possible to exit the repository, or to specify absolute paths.
7070pub fn component (
@@ -148,7 +148,8 @@ fn check_win_devices_and_illegal_characters(input: &BStr) -> Option<component::E
148148 return Some ( component:: Error :: WindowsReservedName ) ;
149149 }
150150 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 ..) ) )
152153 || ( input. get ( 3 ..7 ) . map_or ( false , |n| n. eq_ignore_ascii_case ( b"out$" ) ) && is_done_windows ( input. get ( 7 ..) ) ) )
153154 {
154155 return Some ( component:: Error :: WindowsReservedName ) ;
@@ -168,22 +169,26 @@ fn is_symlink(mode: Option<component::Mode>) -> bool {
168169
169170fn is_dot_hfs ( input : & BStr , search_case_insensitive : & str ) -> bool {
170171 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
187192 _ => true
188193 } ) ;
189194 if input. next ( ) != Some ( '.' ) {
@@ -278,7 +283,9 @@ fn is_dot_ntfs(input: &BStr, search_case_insensitive: &str, ntfs_shortname_prefi
278283 }
279284}
280285
286+ /// Check if trailing filename bytes leave a match to special files like `.git` unchanged in NTFS.
281287fn is_done_ntfs ( input : Option < & [ u8 ] > ) -> bool {
288+ // Skip spaces and dots. Then return true if we are at the end or a colon.
282289 let Some ( input) = input else { return true } ;
283290 for b in input. bytes ( ) {
284291 if b == b':' {
@@ -291,9 +298,11 @@ fn is_done_ntfs(input: Option<&[u8]>) -> bool {
291298 true
292299}
293300
301+ /// Check if trailing filename bytes leave a match to Windows reserved device names unchanged.
294302fn is_done_windows ( input : Option < & [ u8 ] > ) -> bool {
303+ // Skip spaces. Then return true if we are at the end or a dot or colon.
295304 let Some ( input) = input else { return true } ;
296305 let skip = input. bytes ( ) . take_while ( |b| * b == b' ' ) . count ( ) ;
297306 let Some ( next) = input. get ( skip) else { return true } ;
298- ! ( * next != b'.' && * next != b':' )
307+ * next == b'.' || * next == b':'
299308}
0 commit comments