]> git.proxmox.com Git - libgit2.git/blob - src/path.c
New upstream version 1.4.3+dfsg.1
[libgit2.git] / src / path.c
1 /*
2 * Copyright (C) the libgit2 contributors. All rights reserved.
3 *
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.
6 */
7
8 #include "path.h"
9
10 #include "repository.h"
11 #include "fs_path.h"
12
13 typedef struct {
14 git_repository *repo;
15 uint16_t file_mode;
16 unsigned int flags;
17 } repository_path_validate_data;
18
19 static int32_t next_hfs_char(const char **in, size_t *len)
20 {
21 while (*len) {
22 uint32_t codepoint;
23 int cp_len = git_utf8_iterate(&codepoint, *in, *len);
24 if (cp_len < 0)
25 return -1;
26
27 (*in) += cp_len;
28 (*len) -= cp_len;
29
30 /* these code points are ignored completely */
31 switch (codepoint) {
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 */
48 continue;
49 }
50
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
54 */
55 return git__tolower((int)codepoint);
56 }
57 return 0; /* NULL byte -- end of string */
58 }
59
60 static bool validate_dotgit_hfs_generic(
61 const char *path,
62 size_t len,
63 const char *needle,
64 size_t needle_len)
65 {
66 size_t i;
67 char c;
68
69 if (next_hfs_char(&path, &len) != '.')
70 return true;
71
72 for (i = 0; i < needle_len; i++) {
73 c = next_hfs_char(&path, &len);
74 if (c != needle[i])
75 return true;
76 }
77
78 if (next_hfs_char(&path, &len) != '\0')
79 return true;
80
81 return false;
82 }
83
84 static bool validate_dotgit_hfs(const char *path, size_t len)
85 {
86 return validate_dotgit_hfs_generic(path, len, "git", CONST_STRLEN("git"));
87 }
88
89 GIT_INLINE(bool) validate_dotgit_ntfs(
90 git_repository *repo,
91 const char *path,
92 size_t len)
93 {
94 git_str *reserved = git_repository__reserved_names_win32;
95 size_t reserved_len = git_repository__reserved_names_win32_len;
96 size_t start = 0, i;
97
98 if (repo)
99 git_repository__reserved_names(&reserved, &reserved_len, repo, true);
100
101 for (i = 0; i < reserved_len; i++) {
102 git_str *r = &reserved[i];
103
104 if (len >= r->size &&
105 strncasecmp(path, r->ptr, r->size) == 0) {
106 start = r->size;
107 break;
108 }
109 }
110
111 if (!start)
112 return true;
113
114 /*
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.
118 */
119 if (path[start] == '\\' || path[start] == ':')
120 return false;
121
122 /* Reject paths like '.git ' or '.git.' */
123 for (i = start; i < len; i++) {
124 if (path[i] != ' ' && path[i] != '.')
125 return true;
126 }
127
128 return false;
129 }
130
131 /*
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.)
139 */
140 GIT_INLINE(bool) ntfs_end_of_filename(const char *path)
141 {
142 const char *c = path;
143
144 for (;; c++) {
145 if (*c == '\0' || *c == ':')
146 return true;
147 if (*c != ' ' && *c != '.')
148 return false;
149 }
150
151 return true;
152 }
153
154 GIT_INLINE(bool) validate_dotgit_ntfs_generic(
155 const char *name,
156 size_t len,
157 const char *dotgit_name,
158 size_t dotgit_len,
159 const char *shortname_pfix)
160 {
161 int i, saw_tilde;
162
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);
166 }
167
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);
172
173 /* Catch fallback names */
174 for (i = 0, saw_tilde = 0; i < 8; i++) {
175 if (name[i] == '\0') {
176 return true;
177 } else if (saw_tilde) {
178 if (name[i] < '0' || name[i] > '9')
179 return true;
180 } else if (name[i] == '~') {
181 if (name[i+1] < '1' || name[i+1] > '9')
182 return true;
183 saw_tilde = 1;
184 } else if (i >= 6) {
185 return true;
186 } else if ((unsigned char)name[i] > 127) {
187 return true;
188 } else if (git__tolower(name[i]) != shortname_pfix[i]) {
189 return true;
190 }
191 }
192
193 return !ntfs_end_of_filename(name + i);
194 }
195
196 /*
197 * Return the length of the common prefix between str and prefix, comparing them
198 * case-insensitively (must be ASCII to match).
199 */
200 GIT_INLINE(size_t) common_prefix_icase(const char *str, size_t len, const char *prefix)
201 {
202 size_t count = 0;
203
204 while (len > 0 && tolower(*str) == tolower(*prefix)) {
205 count++;
206 str++;
207 prefix++;
208 len--;
209 }
210
211 return count;
212 }
213
214 static bool validate_repo_component(
215 const char *component,
216 size_t len,
217 void *payload)
218 {
219 repository_path_validate_data *data = (repository_path_validate_data *)payload;
220
221 if (data->flags & GIT_PATH_REJECT_DOT_GIT_HFS) {
222 if (!validate_dotgit_hfs(component, len))
223 return false;
224
225 if (S_ISLNK(data->file_mode) &&
226 git_path_is_gitfile(component, len, GIT_PATH_GITFILE_GITMODULES, GIT_PATH_FS_HFS))
227 return false;
228 }
229
230 if (data->flags & GIT_PATH_REJECT_DOT_GIT_NTFS) {
231 if (!validate_dotgit_ntfs(data->repo, component, len))
232 return false;
233
234 if (S_ISLNK(data->file_mode) &&
235 git_path_is_gitfile(component, len, GIT_PATH_GITFILE_GITMODULES, GIT_PATH_FS_NTFS))
236 return false;
237 }
238
239 /* don't bother rerunning the `.git` test if we ran the HFS or NTFS
240 * specific tests, they would have already rejected `.git`.
241 */
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)) {
245 if (len >= 4 &&
246 component[0] == '.' &&
247 (component[1] == 'g' || component[1] == 'G') &&
248 (component[2] == 'i' || component[2] == 'I') &&
249 (component[3] == 't' || component[3] == 'T')) {
250 if (len == 4)
251 return false;
252
253 if (S_ISLNK(data->file_mode) &&
254 common_prefix_icase(component, len, ".gitmodules") == len)
255 return false;
256 }
257 }
258
259 return true;
260 }
261
262 GIT_INLINE(unsigned int) dotgit_flags(
263 git_repository *repo,
264 unsigned int flags)
265 {
266 int protectHFS = 0, protectNTFS = 1;
267 int error = 0;
268
269 flags |= GIT_PATH_REJECT_DOT_GIT_LITERAL;
270
271 #ifdef __APPLE__
272 protectHFS = 1;
273 #endif
274
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;
279
280 if (repo)
281 error = git_repository__configmap_lookup(&protectNTFS, repo, GIT_CONFIGMAP_PROTECTNTFS);
282 if (!error && protectNTFS)
283 flags |= GIT_PATH_REJECT_DOT_GIT_NTFS;
284
285 return flags;
286 }
287
288 GIT_INLINE(unsigned int) length_flags(
289 git_repository *repo,
290 unsigned int flags)
291 {
292 #ifdef GIT_WIN32
293 int allow = 0;
294
295 if (repo &&
296 git_repository__configmap_lookup(&allow, repo, GIT_CONFIGMAP_LONGPATHS) < 0)
297 allow = 0;
298
299 if (allow)
300 flags &= ~GIT_FS_PATH_REJECT_LONG_PATHS;
301
302 #else
303 GIT_UNUSED(repo);
304 flags &= ~GIT_FS_PATH_REJECT_LONG_PATHS;
305 #endif
306
307 return flags;
308 }
309
310 bool git_path_str_is_valid(
311 git_repository *repo,
312 const git_str *path,
313 uint16_t file_mode,
314 unsigned int flags)
315 {
316 repository_path_validate_data data = {0};
317
318 /* Upgrade the ".git" checks based on platform */
319 if ((flags & GIT_PATH_REJECT_DOT_GIT))
320 flags = dotgit_flags(repo, flags);
321
322 /* Update the length checks based on platform */
323 if ((flags & GIT_FS_PATH_REJECT_LONG_PATHS))
324 flags = length_flags(repo, flags);
325
326 data.repo = repo;
327 data.file_mode = file_mode;
328 data.flags = flags;
329
330 return git_fs_path_str_is_valid_ext(path, flags, NULL, validate_repo_component, NULL, &data);
331 }
332
333 static const struct {
334 const char *file;
335 const char *hash;
336 size_t filelen;
337 } gitfiles[] = {
338 { "gitignore", "gi250a", CONST_STRLEN("gitignore") },
339 { "gitmodules", "gi7eba", CONST_STRLEN("gitmodules") },
340 { "gitattributes", "gi7d29", CONST_STRLEN("gitattributes") }
341 };
342
343 extern int git_path_is_gitfile(
344 const char *path,
345 size_t pathlen,
346 git_path_gitfile gitfile,
347 git_path_fs fs)
348 {
349 const char *file, *hash;
350 size_t filelen;
351
352 if (!(gitfile >= GIT_PATH_GITFILE_GITIGNORE && gitfile < ARRAY_SIZE(gitfiles))) {
353 git_error_set(GIT_ERROR_OS, "invalid gitfile for path validation");
354 return -1;
355 }
356
357 file = gitfiles[gitfile].file;
358 filelen = gitfiles[gitfile].filelen;
359 hash = gitfiles[gitfile].hash;
360
361 switch (fs) {
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);
369 default:
370 git_error_set(GIT_ERROR_OS, "invalid filesystem for path validation");
371 return -1;
372 }
373 }
374