mingw: refuse paths containing reserved names
There are a couple of reserved names that cannot be file names on Windows, such as `AUX`, `NUL`, etc. For an almost complete list, see https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file If one would try to create a directory named `NUL`, it would actually "succeed", i.e. the call would return success, but nothing would be created. Worse, even adding a file extension to the reserved name does not make it a valid file name. To understand the rationale behind that behavior, see https://devblogs.microsoft.com/oldnewthing/20031022-00/?p=42073 Let's just disallow them all. Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>maint
							parent
							
								
									98d9b23e90
								
							
						
					
					
						commit
						4dc42c6c18
					
				
							
								
								
									
										104
									
								
								compat/mingw.c
								
								
								
								
							
							
						
						
									
										104
									
								
								compat/mingw.c
								
								
								
								
							|  | @ -393,7 +393,7 @@ int mingw_mkdir(const char *path, int mode) | |||
| 	int ret; | ||||
| 	wchar_t wpath[MAX_PATH]; | ||||
|  | ||||
| 	if (!is_valid_win32_path(path)) { | ||||
| 	if (!is_valid_win32_path(path, 0)) { | ||||
| 		errno = EINVAL; | ||||
| 		return -1; | ||||
| 	} | ||||
|  | @ -479,7 +479,7 @@ int mingw_open (const char *filename, int oflags, ...) | |||
| 	mode = va_arg(args, int); | ||||
| 	va_end(args); | ||||
|  | ||||
| 	if (!is_valid_win32_path(filename)) { | ||||
| 	if (!is_valid_win32_path(filename, !create)) { | ||||
| 		errno = create ? EINVAL : ENOENT; | ||||
| 		return -1; | ||||
| 	} | ||||
|  | @ -550,14 +550,13 @@ FILE *mingw_fopen (const char *filename, const char *otype) | |||
| 	int hide = needs_hiding(filename); | ||||
| 	FILE *file; | ||||
| 	wchar_t wfilename[MAX_PATH], wotype[4]; | ||||
| 	if (!is_valid_win32_path(filename)) { | ||||
| 	if (filename && !strcmp(filename, "/dev/null")) | ||||
| 		wcscpy(wfilename, L"nul"); | ||||
| 	else if (!is_valid_win32_path(filename, 1)) { | ||||
| 		int create = otype && strchr(otype, 'w'); | ||||
| 		errno = create ? EINVAL : ENOENT; | ||||
| 		return NULL; | ||||
| 	} | ||||
| 	if (filename && !strcmp(filename, "/dev/null")) | ||||
| 		wcscpy(wfilename, L"nul"); | ||||
| 	else if (xutftowcs_path(wfilename, filename) < 0) | ||||
| 	} else if (xutftowcs_path(wfilename, filename) < 0) | ||||
| 		return NULL; | ||||
|  | ||||
| 	if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0) | ||||
|  | @ -580,14 +579,13 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream) | |||
| 	int hide = needs_hiding(filename); | ||||
| 	FILE *file; | ||||
| 	wchar_t wfilename[MAX_PATH], wotype[4]; | ||||
| 	if (!is_valid_win32_path(filename)) { | ||||
| 	if (filename && !strcmp(filename, "/dev/null")) | ||||
| 		wcscpy(wfilename, L"nul"); | ||||
| 	else if (!is_valid_win32_path(filename, 1)) { | ||||
| 		int create = otype && strchr(otype, 'w'); | ||||
| 		errno = create ? EINVAL : ENOENT; | ||||
| 		return NULL; | ||||
| 	} | ||||
| 	if (filename && !strcmp(filename, "/dev/null")) | ||||
| 		wcscpy(wfilename, L"nul"); | ||||
| 	else if (xutftowcs_path(wfilename, filename) < 0) | ||||
| 	} else if (xutftowcs_path(wfilename, filename) < 0) | ||||
| 		return NULL; | ||||
|  | ||||
| 	if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0) | ||||
|  | @ -2412,14 +2410,16 @@ static void setup_windows_environment(void) | |||
| 	} | ||||
| } | ||||
|  | ||||
| int is_valid_win32_path(const char *path) | ||||
| int is_valid_win32_path(const char *path, int allow_literal_nul) | ||||
| { | ||||
| 	const char *p = path; | ||||
| 	int preceding_space_or_period = 0, i = 0, periods = 0; | ||||
|  | ||||
| 	if (!protect_ntfs) | ||||
| 		return 1; | ||||
|  | ||||
| 	skip_dos_drive_prefix((char **)&path); | ||||
| 	goto segment_start; | ||||
|  | ||||
| 	for (;;) { | ||||
| 		char c = *(path++); | ||||
|  | @ -2434,7 +2434,83 @@ int is_valid_win32_path(const char *path) | |||
| 				return 1; | ||||
|  | ||||
| 			i = periods = preceding_space_or_period = 0; | ||||
| 			continue; | ||||
|  | ||||
| segment_start: | ||||
| 			switch (*path) { | ||||
| 			case 'a': case 'A': /* AUX */ | ||||
| 				if (((c = path[++i]) != 'u' && c != 'U') || | ||||
| 				    ((c = path[++i]) != 'x' && c != 'X')) { | ||||
| not_a_reserved_name: | ||||
| 					path += i; | ||||
| 					continue; | ||||
| 				} | ||||
| 				break; | ||||
| 			case 'c': case 'C': /* COM<N>, CON, CONIN$, CONOUT$ */ | ||||
| 				if ((c = path[++i]) != 'o' && c != 'O') | ||||
| 					goto not_a_reserved_name; | ||||
| 				c = path[++i]; | ||||
| 				if (c == 'm' || c == 'M') { /* COM<N> */ | ||||
| 					if (!isdigit(path[++i])) | ||||
| 						goto not_a_reserved_name; | ||||
| 				} else if (c == 'n' || c == 'N') { /* CON */ | ||||
| 					c = path[i + 1]; | ||||
| 					if ((c == 'i' || c == 'I') && | ||||
| 					    ((c = path[i + 2]) == 'n' || | ||||
| 					     c == 'N') && | ||||
| 					    path[i + 3] == '$') | ||||
| 						i += 3; /* CONIN$ */ | ||||
| 					else if ((c == 'o' || c == 'O') && | ||||
| 						 ((c = path[i + 2]) == 'u' || | ||||
| 						  c == 'U') && | ||||
| 						 ((c = path[i + 3]) == 't' || | ||||
| 						  c == 'T') && | ||||
| 						 path[i + 4] == '$') | ||||
| 						i += 4; /* CONOUT$ */ | ||||
| 				} else | ||||
| 					goto not_a_reserved_name; | ||||
| 				break; | ||||
| 			case 'l': case 'L': /* LPT<N> */ | ||||
| 				if (((c = path[++i]) != 'p' && c != 'P') || | ||||
| 				    ((c = path[++i]) != 't' && c != 'T') || | ||||
| 				    !isdigit(path[++i])) | ||||
| 					goto not_a_reserved_name; | ||||
| 				break; | ||||
| 			case 'n': case 'N': /* NUL */ | ||||
| 				if (((c = path[++i]) != 'u' && c != 'U') || | ||||
| 				    ((c = path[++i]) != 'l' && c != 'L') || | ||||
| 				    (allow_literal_nul && | ||||
| 				     !path[i + 1] && p == path)) | ||||
| 					goto not_a_reserved_name; | ||||
| 				break; | ||||
| 			case 'p': case 'P': /* PRN */ | ||||
| 				if (((c = path[++i]) != 'r' && c != 'R') || | ||||
| 				    ((c = path[++i]) != 'n' && c != 'N')) | ||||
| 					goto not_a_reserved_name; | ||||
| 				break; | ||||
| 			default: | ||||
| 				continue; | ||||
| 			} | ||||
|  | ||||
| 			/* | ||||
| 			 * So far, this looks like a reserved name. Let's see | ||||
| 			 * whether it actually is one: trailing spaces, a file | ||||
| 			 * extension, or an NTFS Alternate Data Stream do not | ||||
| 			 * matter, the name is still reserved if any of those | ||||
| 			 * follow immediately after the actual name. | ||||
| 			 */ | ||||
| 			i++; | ||||
| 			if (path[i] == ' ') { | ||||
| 				preceding_space_or_period = 1; | ||||
| 				while (path[++i] == ' ') | ||||
| 					; /* skip all spaces */ | ||||
| 			} | ||||
|  | ||||
| 			c = path[i]; | ||||
| 			if (c && c != '.' && c != ':' && c != '/' && c != '\\') | ||||
| 				goto not_a_reserved_name; | ||||
|  | ||||
| 			/* contains reserved name */ | ||||
| 			return 0; | ||||
| 		case '.': | ||||
| 			periods++; | ||||
| 			/* fallthru */ | ||||
|  |  | |||
|  | @ -461,10 +461,17 @@ char *mingw_query_user_email(void); | |||
|  * | ||||
|  * - contain any of the reserved characters, e.g. `:`, `;`, `*`, etc | ||||
|  * | ||||
|  * - correspond to reserved names (such as `AUX`, `PRN`, etc) | ||||
|  * | ||||
|  * The `allow_literal_nul` parameter controls whether the path `NUL` should | ||||
|  * be considered valid (this makes sense e.g. before opening files, as it is | ||||
|  * perfectly legitimate to open `NUL` on Windows, just as it is to open | ||||
|  * `/dev/null` on Unix/Linux). | ||||
|  * | ||||
|  * Returns 1 upon success, otherwise 0. | ||||
|  */ | ||||
| int is_valid_win32_path(const char *path); | ||||
| #define is_valid_path(path) is_valid_win32_path(path) | ||||
| int is_valid_win32_path(const char *path, int allow_literal_nul); | ||||
| #define is_valid_path(path) is_valid_win32_path(path, 0) | ||||
|  | ||||
| /** | ||||
|  * Converts UTF-8 encoded string to UTF-16LE. | ||||
|  |  | |||
|  | @ -465,11 +465,14 @@ test_expect_success 'match .gitmodules' ' | |||
| ' | ||||
|  | ||||
| test_expect_success MINGW 'is_valid_path() on Windows' ' | ||||
|        test-tool path-utils is_valid_path \ | ||||
| 	test-tool path-utils is_valid_path \ | ||||
| 		win32 \ | ||||
| 		"win32 x" \ | ||||
| 		../hello.txt \ | ||||
| 		C:\\git \ | ||||
| 		comm \ | ||||
| 		conout.c \ | ||||
| 		lptN \ | ||||
| 		\ | ||||
| 		--not \ | ||||
| 		"win32 "  \ | ||||
|  | @ -477,7 +480,13 @@ test_expect_success MINGW 'is_valid_path() on Windows' ' | |||
| 		"win32."  \ | ||||
| 		"win32 . ." \ | ||||
| 		.../hello.txt \ | ||||
| 		colon:test | ||||
| 		colon:test \ | ||||
| 		"AUX.c" \ | ||||
| 		"abc/conOut\$  .xyz/test" \ | ||||
| 		lpt8 \ | ||||
| 		"lpt*" \ | ||||
| 		Nul \ | ||||
| 		"PRN./abc" | ||||
| ' | ||||
|  | ||||
| test_done | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Johannes Schindelin
						Johannes Schindelin