File Coverage

deps/libgit2/src/libgit2/path.c
Criterion Covered Total %
statement 55 152 36.1
branch 36 134 26.8
condition n/a
subroutine n/a
pod n/a
total 91 286 31.8


line stmt bran cond sub pod time code
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             #include "utf8.h"
13              
14             typedef struct {
15             git_repository *repo;
16             uint16_t file_mode;
17             unsigned int flags;
18             } repository_path_validate_data;
19              
20 0           static int32_t next_hfs_char(const char **in, size_t *len)
21             {
22 0 0         while (*len) {
23             uint32_t codepoint;
24 0           int cp_len = git_utf8_iterate(&codepoint, *in, *len);
25 0 0         if (cp_len < 0)
26 0           return -1;
27              
28 0           (*in) += cp_len;
29 0           (*len) -= cp_len;
30              
31             /* these code points are ignored completely */
32 0 0         switch (codepoint) {
33             case 0x200c: /* ZERO WIDTH NON-JOINER */
34             case 0x200d: /* ZERO WIDTH JOINER */
35             case 0x200e: /* LEFT-TO-RIGHT MARK */
36             case 0x200f: /* RIGHT-TO-LEFT MARK */
37             case 0x202a: /* LEFT-TO-RIGHT EMBEDDING */
38             case 0x202b: /* RIGHT-TO-LEFT EMBEDDING */
39             case 0x202c: /* POP DIRECTIONAL FORMATTING */
40             case 0x202d: /* LEFT-TO-RIGHT OVERRIDE */
41             case 0x202e: /* RIGHT-TO-LEFT OVERRIDE */
42             case 0x206a: /* INHIBIT SYMMETRIC SWAPPING */
43             case 0x206b: /* ACTIVATE SYMMETRIC SWAPPING */
44             case 0x206c: /* INHIBIT ARABIC FORM SHAPING */
45             case 0x206d: /* ACTIVATE ARABIC FORM SHAPING */
46             case 0x206e: /* NATIONAL DIGIT SHAPES */
47             case 0x206f: /* NOMINAL DIGIT SHAPES */
48             case 0xfeff: /* ZERO WIDTH NO-BREAK SPACE */
49 0           continue;
50             }
51              
52             /* fold into lowercase -- this will only fold characters in
53             * the ASCII range, which is perfectly fine, because the
54             * git folder name can only be composed of ascii characters
55             */
56 0           return git__tolower((int)codepoint);
57             }
58 0           return 0; /* NULL byte -- end of string */
59             }
60              
61 0           static bool validate_dotgit_hfs_generic(
62             const char *path,
63             size_t len,
64             const char *needle,
65             size_t needle_len)
66             {
67             size_t i;
68             char c;
69              
70 0 0         if (next_hfs_char(&path, &len) != '.')
71 0           return true;
72              
73 0 0         for (i = 0; i < needle_len; i++) {
74 0           c = next_hfs_char(&path, &len);
75 0 0         if (c != needle[i])
76 0           return true;
77             }
78              
79 0 0         if (next_hfs_char(&path, &len) != '\0')
80 0           return true;
81              
82 0           return false;
83             }
84              
85 0           static bool validate_dotgit_hfs(const char *path, size_t len)
86             {
87 0           return validate_dotgit_hfs_generic(path, len, "git", CONST_STRLEN("git"));
88             }
89              
90 535           GIT_INLINE(bool) validate_dotgit_ntfs(
91             git_repository *repo,
92             const char *path,
93             size_t len)
94             {
95 535           git_str *reserved = git_repository__reserved_names_win32;
96 535           size_t reserved_len = git_repository__reserved_names_win32_len;
97 535           size_t start = 0, i;
98              
99 535 100         if (repo)
100 370           git_repository__reserved_names(&reserved, &reserved_len, repo, true);
101              
102 1579 100         for (i = 0; i < reserved_len; i++) {
103 1057           git_str *r = &reserved[i];
104              
105 1057 100         if (len >= r->size &&
    100          
106 836           strncasecmp(path, r->ptr, r->size) == 0) {
107 13           start = r->size;
108 13           break;
109             }
110             }
111              
112 535 100         if (!start)
113 522           return true;
114              
115             /*
116             * Reject paths that start with Windows-style directory separators
117             * (".git\") or NTFS alternate streams (".git:") and could be used
118             * to write to the ".git" directory on Windows platforms.
119             */
120 13 50         if (path[start] == '\\' || path[start] == ':')
    50          
121 0           return false;
122              
123             /* Reject paths like '.git ' or '.git.' */
124 13 50         for (i = start; i < len; i++) {
125 13 50         if (path[i] != ' ' && path[i] != '.')
    50          
126 13           return true;
127             }
128              
129 535           return false;
130             }
131              
132             /*
133             * Windows paths that end with spaces and/or dots are elided to the
134             * path without them for backward compatibility. That is to say
135             * that opening file "foo ", "foo." or even "foo . . ." will all
136             * map to a filename of "foo". This function identifies spaces and
137             * dots at the end of a filename, whether the proper end of the
138             * filename (end of string) or a colon (which would indicate a
139             * Windows alternate data stream.)
140             */
141 0           GIT_INLINE(bool) ntfs_end_of_filename(const char *path)
142             {
143 0           const char *c = path;
144              
145 0           for (;; c++) {
146 0 0         if (*c == '\0' || *c == ':')
    0          
147 0           return true;
148 0 0         if (*c != ' ' && *c != '.')
    0          
149 0           return false;
150 0           }
151              
152             return true;
153             }
154              
155 0           GIT_INLINE(bool) validate_dotgit_ntfs_generic(
156             const char *name,
157             size_t len,
158             const char *dotgit_name,
159             size_t dotgit_len,
160             const char *shortname_pfix)
161             {
162             int i, saw_tilde;
163              
164 0 0         if (name[0] == '.' && len >= dotgit_len &&
    0          
    0          
165 0           !strncasecmp(name + 1, dotgit_name, dotgit_len)) {
166 0           return !ntfs_end_of_filename(name + dotgit_len + 1);
167             }
168              
169             /* Detect the basic NTFS shortname with the first six chars */
170 0 0         if (!strncasecmp(name, dotgit_name, 6) && name[6] == '~' &&
    0          
    0          
171 0 0         name[7] >= '1' && name[7] <= '4')
172 0           return !ntfs_end_of_filename(name + 8);
173              
174             /* Catch fallback names */
175 0 0         for (i = 0, saw_tilde = 0; i < 8; i++) {
176 0 0         if (name[i] == '\0') {
177 0           return true;
178 0 0         } else if (saw_tilde) {
179 0 0         if (name[i] < '0' || name[i] > '9')
    0          
180 0           return true;
181 0 0         } else if (name[i] == '~') {
182 0 0         if (name[i+1] < '1' || name[i+1] > '9')
    0          
183 0           return true;
184 0           saw_tilde = 1;
185 0 0         } else if (i >= 6) {
186 0           return true;
187 0 0         } else if ((unsigned char)name[i] > 127) {
188 0           return true;
189 0 0         } else if (git__tolower(name[i]) != shortname_pfix[i]) {
190 0           return true;
191             }
192             }
193              
194 0           return !ntfs_end_of_filename(name + i);
195             }
196              
197             /*
198             * Return the length of the common prefix between str and prefix, comparing them
199             * case-insensitively (must be ASCII to match).
200             */
201 0           GIT_INLINE(size_t) common_prefix_icase(const char *str, size_t len, const char *prefix)
202             {
203 0           size_t count = 0;
204              
205 0 0         while (len > 0 && tolower(*str) == tolower(*prefix)) {
    0          
206 0           count++;
207 0           str++;
208 0           prefix++;
209 0           len--;
210             }
211              
212 0           return count;
213             }
214              
215 803           static bool validate_repo_component(
216             const char *component,
217             size_t len,
218             void *payload)
219             {
220 803           repository_path_validate_data *data = (repository_path_validate_data *)payload;
221              
222 803 50         if (data->flags & GIT_PATH_REJECT_DOT_GIT_HFS) {
223 0 0         if (!validate_dotgit_hfs(component, len))
224 0           return false;
225              
226 0           if (S_ISLNK(data->file_mode) &&
227 0           git_path_is_gitfile(component, len, GIT_PATH_GITFILE_GITMODULES, GIT_PATH_FS_HFS))
228 0           return false;
229             }
230              
231 803 100         if (data->flags & GIT_PATH_REJECT_DOT_GIT_NTFS) {
232 535 50         if (!validate_dotgit_ntfs(data->repo, component, len))
233 0           return false;
234              
235 535           if (S_ISLNK(data->file_mode) &&
236 0           git_path_is_gitfile(component, len, GIT_PATH_GITFILE_GITMODULES, GIT_PATH_FS_NTFS))
237 0           return false;
238             }
239              
240             /* don't bother rerunning the `.git` test if we ran the HFS or NTFS
241             * specific tests, they would have already rejected `.git`.
242             */
243 803 50         if ((data->flags & GIT_PATH_REJECT_DOT_GIT_HFS) == 0 &&
    100          
244 268 50         (data->flags & GIT_PATH_REJECT_DOT_GIT_NTFS) == 0 &&
245 268           (data->flags & GIT_PATH_REJECT_DOT_GIT_LITERAL)) {
246 0 0         if (len >= 4 &&
    0          
247 0 0         component[0] == '.' &&
248 0 0         (component[1] == 'g' || component[1] == 'G') &&
    0          
249 0 0         (component[2] == 'i' || component[2] == 'I') &&
    0          
250 0 0         (component[3] == 't' || component[3] == 'T')) {
251 0 0         if (len == 4)
252 0           return false;
253              
254 0           if (S_ISLNK(data->file_mode) &&
255 0           common_prefix_icase(component, len, ".gitmodules") == len)
256 0           return false;
257             }
258             }
259              
260 803           return true;
261             }
262              
263 463           GIT_INLINE(unsigned int) dotgit_flags(
264             git_repository *repo,
265             unsigned int flags)
266             {
267 463           int protectHFS = 0, protectNTFS = 1;
268 463           int error = 0;
269              
270 463           flags |= GIT_PATH_REJECT_DOT_GIT_LITERAL;
271              
272             #ifdef __APPLE__
273             protectHFS = 1;
274             #endif
275              
276 463 100         if (repo && !protectHFS)
    50          
277 351           error = git_repository__configmap_lookup(&protectHFS, repo, GIT_CONFIGMAP_PROTECTHFS);
278 463 50         if (!error && protectHFS)
    50          
279 0           flags |= GIT_PATH_REJECT_DOT_GIT_HFS;
280              
281 463 100         if (repo)
282 351           error = git_repository__configmap_lookup(&protectNTFS, repo, GIT_CONFIGMAP_PROTECTNTFS);
283 463 50         if (!error && protectNTFS)
    50          
284 463           flags |= GIT_PATH_REJECT_DOT_GIT_NTFS;
285              
286 463           return flags;
287             }
288              
289 6651           GIT_INLINE(unsigned int) length_flags(
290             git_repository *repo,
291             unsigned int flags)
292             {
293             #ifdef GIT_WIN32
294             int allow = 0;
295              
296             if (repo &&
297             git_repository__configmap_lookup(&allow, repo, GIT_CONFIGMAP_LONGPATHS) < 0)
298             allow = 0;
299              
300             if (allow)
301             flags &= ~GIT_FS_PATH_REJECT_LONG_PATHS;
302              
303             #else
304 6651           GIT_UNUSED(repo);
305 6651           flags &= ~GIT_FS_PATH_REJECT_LONG_PATHS;
306             #endif
307              
308 6651           return flags;
309             }
310              
311 7229           bool git_path_str_is_valid(
312             git_repository *repo,
313             const git_str *path,
314             uint16_t file_mode,
315             unsigned int flags)
316             {
317 7229           repository_path_validate_data data = {0};
318              
319             /* Upgrade the ".git" checks based on platform */
320 7229 100         if ((flags & GIT_PATH_REJECT_DOT_GIT))
321 463           flags = dotgit_flags(repo, flags);
322              
323             /* Update the length checks based on platform */
324 7229 100         if ((flags & GIT_FS_PATH_REJECT_LONG_PATHS))
325 6651           flags = length_flags(repo, flags);
326              
327 7229           data.repo = repo;
328 7229           data.file_mode = file_mode;
329 7229           data.flags = flags;
330              
331 7229           return git_fs_path_str_is_valid_ext(path, flags, NULL, validate_repo_component, NULL, &data);
332             }
333              
334             static const struct {
335             const char *file;
336             const char *hash;
337             size_t filelen;
338             } gitfiles[] = {
339             { "gitignore", "gi250a", CONST_STRLEN("gitignore") },
340             { "gitmodules", "gi7eba", CONST_STRLEN("gitmodules") },
341             { "gitattributes", "gi7d29", CONST_STRLEN("gitattributes") }
342             };
343              
344 0           extern int git_path_is_gitfile(
345             const char *path,
346             size_t pathlen,
347             git_path_gitfile gitfile,
348             git_path_fs fs)
349             {
350             const char *file, *hash;
351             size_t filelen;
352              
353 0 0         if (!(gitfile >= GIT_PATH_GITFILE_GITIGNORE && gitfile < ARRAY_SIZE(gitfiles))) {
354 0           git_error_set(GIT_ERROR_OS, "invalid gitfile for path validation");
355 0           return -1;
356             }
357              
358 0           file = gitfiles[gitfile].file;
359 0           filelen = gitfiles[gitfile].filelen;
360 0           hash = gitfiles[gitfile].hash;
361              
362 0           switch (fs) {
363             case GIT_PATH_FS_GENERIC:
364 0           return !validate_dotgit_ntfs_generic(path, pathlen, file, filelen, hash) ||
365 0           !validate_dotgit_hfs_generic(path, pathlen, file, filelen);
366             case GIT_PATH_FS_NTFS:
367 0           return !validate_dotgit_ntfs_generic(path, pathlen, file, filelen, hash);
368             case GIT_PATH_FS_HFS:
369 0           return !validate_dotgit_hfs_generic(path, pathlen, file, filelen);
370             default:
371 0           git_error_set(GIT_ERROR_OS, "invalid filesystem for path validation");
372 0           return -1;
373             }
374             }
375