2 * Copyright (C) the libgit2 contributors. All rights reserved.
4 * This file is part of libgit2, distributed under the GNU GPL v2 with
5 * a Linking Exception. For full terms see the included COPYING file.
10 #include "repository.h"
17 } repository_path_validate_data
;
19 static int32_t next_hfs_char(const char **in
, size_t *len
)
23 int cp_len
= git_utf8_iterate(&codepoint
, *in
, *len
);
30 /* these code points are ignored completely */
32 case 0x200c: /* ZERO WIDTH NON-JOINER */
33 case 0x200d: /* ZERO WIDTH JOINER */
34 case 0x200e: /* LEFT-TO-RIGHT MARK */
35 case 0x200f: /* RIGHT-TO-LEFT MARK */
36 case 0x202a: /* LEFT-TO-RIGHT EMBEDDING */
37 case 0x202b: /* RIGHT-TO-LEFT EMBEDDING */
38 case 0x202c: /* POP DIRECTIONAL FORMATTING */
39 case 0x202d: /* LEFT-TO-RIGHT OVERRIDE */
40 case 0x202e: /* RIGHT-TO-LEFT OVERRIDE */
41 case 0x206a: /* INHIBIT SYMMETRIC SWAPPING */
42 case 0x206b: /* ACTIVATE SYMMETRIC SWAPPING */
43 case 0x206c: /* INHIBIT ARABIC FORM SHAPING */
44 case 0x206d: /* ACTIVATE ARABIC FORM SHAPING */
45 case 0x206e: /* NATIONAL DIGIT SHAPES */
46 case 0x206f: /* NOMINAL DIGIT SHAPES */
47 case 0xfeff: /* ZERO WIDTH NO-BREAK SPACE */
51 /* fold into lowercase -- this will only fold characters in
52 * the ASCII range, which is perfectly fine, because the
53 * git folder name can only be composed of ascii characters
55 return git__tolower((int)codepoint
);
57 return 0; /* NULL byte -- end of string */
60 static bool validate_dotgit_hfs_generic(
69 if (next_hfs_char(&path
, &len
) != '.')
72 for (i
= 0; i
< needle_len
; i
++) {
73 c
= next_hfs_char(&path
, &len
);
78 if (next_hfs_char(&path
, &len
) != '\0')
84 static bool validate_dotgit_hfs(const char *path
, size_t len
)
86 return validate_dotgit_hfs_generic(path
, len
, "git", CONST_STRLEN("git"));
89 GIT_INLINE(bool) validate_dotgit_ntfs(
94 git_str
*reserved
= git_repository__reserved_names_win32
;
95 size_t reserved_len
= git_repository__reserved_names_win32_len
;
99 git_repository__reserved_names(&reserved
, &reserved_len
, repo
, true);
101 for (i
= 0; i
< reserved_len
; i
++) {
102 git_str
*r
= &reserved
[i
];
104 if (len
>= r
->size
&&
105 strncasecmp(path
, r
->ptr
, r
->size
) == 0) {
115 * Reject paths that start with Windows-style directory separators
116 * (".git\") or NTFS alternate streams (".git:") and could be used
117 * to write to the ".git" directory on Windows platforms.
119 if (path
[start
] == '\\' || path
[start
] == ':')
122 /* Reject paths like '.git ' or '.git.' */
123 for (i
= start
; i
< len
; i
++) {
124 if (path
[i
] != ' ' && path
[i
] != '.')
132 * Windows paths that end with spaces and/or dots are elided to the
133 * path without them for backward compatibility. That is to say
134 * that opening file "foo ", "foo." or even "foo . . ." will all
135 * map to a filename of "foo". This function identifies spaces and
136 * dots at the end of a filename, whether the proper end of the
137 * filename (end of string) or a colon (which would indicate a
138 * Windows alternate data stream.)
140 GIT_INLINE(bool) ntfs_end_of_filename(const char *path
)
142 const char *c
= path
;
145 if (*c
== '\0' || *c
== ':')
147 if (*c
!= ' ' && *c
!= '.')
154 GIT_INLINE(bool) validate_dotgit_ntfs_generic(
157 const char *dotgit_name
,
159 const char *shortname_pfix
)
163 if (name
[0] == '.' && len
>= dotgit_len
&&
164 !strncasecmp(name
+ 1, dotgit_name
, dotgit_len
)) {
165 return !ntfs_end_of_filename(name
+ dotgit_len
+ 1);
168 /* Detect the basic NTFS shortname with the first six chars */
169 if (!strncasecmp(name
, dotgit_name
, 6) && name
[6] == '~' &&
170 name
[7] >= '1' && name
[7] <= '4')
171 return !ntfs_end_of_filename(name
+ 8);
173 /* Catch fallback names */
174 for (i
= 0, saw_tilde
= 0; i
< 8; i
++) {
175 if (name
[i
] == '\0') {
177 } else if (saw_tilde
) {
178 if (name
[i
] < '0' || name
[i
] > '9')
180 } else if (name
[i
] == '~') {
181 if (name
[i
+1] < '1' || name
[i
+1] > '9')
186 } else if ((unsigned char)name
[i
] > 127) {
188 } else if (git__tolower(name
[i
]) != shortname_pfix
[i
]) {
193 return !ntfs_end_of_filename(name
+ i
);
197 * Return the length of the common prefix between str and prefix, comparing them
198 * case-insensitively (must be ASCII to match).
200 GIT_INLINE(size_t) common_prefix_icase(const char *str
, size_t len
, const char *prefix
)
204 while (len
> 0 && tolower(*str
) == tolower(*prefix
)) {
214 static bool validate_repo_component(
215 const char *component
,
219 repository_path_validate_data
*data
= (repository_path_validate_data
*)payload
;
221 if (data
->flags
& GIT_PATH_REJECT_DOT_GIT_HFS
) {
222 if (!validate_dotgit_hfs(component
, len
))
225 if (S_ISLNK(data
->file_mode
) &&
226 git_path_is_gitfile(component
, len
, GIT_PATH_GITFILE_GITMODULES
, GIT_PATH_FS_HFS
))
230 if (data
->flags
& GIT_PATH_REJECT_DOT_GIT_NTFS
) {
231 if (!validate_dotgit_ntfs(data
->repo
, component
, len
))
234 if (S_ISLNK(data
->file_mode
) &&
235 git_path_is_gitfile(component
, len
, GIT_PATH_GITFILE_GITMODULES
, GIT_PATH_FS_NTFS
))
239 /* don't bother rerunning the `.git` test if we ran the HFS or NTFS
240 * specific tests, they would have already rejected `.git`.
242 if ((data
->flags
& GIT_PATH_REJECT_DOT_GIT_HFS
) == 0 &&
243 (data
->flags
& GIT_PATH_REJECT_DOT_GIT_NTFS
) == 0 &&
244 (data
->flags
& GIT_PATH_REJECT_DOT_GIT_LITERAL
)) {
246 component
[0] == '.' &&
247 (component
[1] == 'g' || component
[1] == 'G') &&
248 (component
[2] == 'i' || component
[2] == 'I') &&
249 (component
[3] == 't' || component
[3] == 'T')) {
253 if (S_ISLNK(data
->file_mode
) &&
254 common_prefix_icase(component
, len
, ".gitmodules") == len
)
262 GIT_INLINE(unsigned int) dotgit_flags(
263 git_repository
*repo
,
266 int protectHFS
= 0, protectNTFS
= 1;
269 flags
|= GIT_PATH_REJECT_DOT_GIT_LITERAL
;
275 if (repo
&& !protectHFS
)
276 error
= git_repository__configmap_lookup(&protectHFS
, repo
, GIT_CONFIGMAP_PROTECTHFS
);
277 if (!error
&& protectHFS
)
278 flags
|= GIT_PATH_REJECT_DOT_GIT_HFS
;
281 error
= git_repository__configmap_lookup(&protectNTFS
, repo
, GIT_CONFIGMAP_PROTECTNTFS
);
282 if (!error
&& protectNTFS
)
283 flags
|= GIT_PATH_REJECT_DOT_GIT_NTFS
;
288 GIT_INLINE(unsigned int) length_flags(
289 git_repository
*repo
,
296 git_repository__configmap_lookup(&allow
, repo
, GIT_CONFIGMAP_LONGPATHS
) < 0)
300 flags
&= ~GIT_FS_PATH_REJECT_LONG_PATHS
;
304 flags
&= ~GIT_FS_PATH_REJECT_LONG_PATHS
;
310 bool git_path_str_is_valid(
311 git_repository
*repo
,
316 repository_path_validate_data data
= {0};
318 /* Upgrade the ".git" checks based on platform */
319 if ((flags
& GIT_PATH_REJECT_DOT_GIT
))
320 flags
= dotgit_flags(repo
, flags
);
322 /* Update the length checks based on platform */
323 if ((flags
& GIT_FS_PATH_REJECT_LONG_PATHS
))
324 flags
= length_flags(repo
, flags
);
327 data
.file_mode
= file_mode
;
330 return git_fs_path_str_is_valid_ext(path
, flags
, NULL
, validate_repo_component
, NULL
, &data
);
333 static const struct {
338 { "gitignore", "gi250a", CONST_STRLEN("gitignore") },
339 { "gitmodules", "gi7eba", CONST_STRLEN("gitmodules") },
340 { "gitattributes", "gi7d29", CONST_STRLEN("gitattributes") }
343 extern int git_path_is_gitfile(
346 git_path_gitfile gitfile
,
349 const char *file
, *hash
;
352 if (!(gitfile
>= GIT_PATH_GITFILE_GITIGNORE
&& gitfile
< ARRAY_SIZE(gitfiles
))) {
353 git_error_set(GIT_ERROR_OS
, "invalid gitfile for path validation");
357 file
= gitfiles
[gitfile
].file
;
358 filelen
= gitfiles
[gitfile
].filelen
;
359 hash
= gitfiles
[gitfile
].hash
;
362 case GIT_PATH_FS_GENERIC
:
363 return !validate_dotgit_ntfs_generic(path
, pathlen
, file
, filelen
, hash
) ||
364 !validate_dotgit_hfs_generic(path
, pathlen
, file
, filelen
);
365 case GIT_PATH_FS_NTFS
:
366 return !validate_dotgit_ntfs_generic(path
, pathlen
, file
, filelen
, hash
);
367 case GIT_PATH_FS_HFS
:
368 return !validate_dotgit_hfs_generic(path
, pathlen
, file
, filelen
);
370 git_error_set(GIT_ERROR_OS
, "invalid filesystem for path validation");