Sync with 2.14.6
* maint-2.14: (28 commits) Git 2.14.6 mingw: handle `subst`-ed "DOS drives" mingw: refuse to access paths with trailing spaces or periods mingw: refuse to access paths with illegal characters unpack-trees: let merged_entry() pass through do_add_entry()'s errors quote-stress-test: offer to test quoting arguments for MSYS2 sh t6130/t9350: prepare for stringent Win32 path validation quote-stress-test: allow skipping some trials quote-stress-test: accept arguments to test via the command-line tests: add a helper to stress test argument quoting mingw: fix quoting of arguments Disallow dubiously-nested submodule git directories protect_ntfs: turn on NTFS protection by default path: also guard `.gitmodules` against NTFS Alternate Data Streams is_ntfs_dotgit(): speed it up mingw: disallow backslash characters in tree objects' file names path: safeguard `.git` against NTFS Alternate Streams Accesses clone --recurse-submodules: prevent name squatting on Windows is_ntfs_dotgit(): only verify the leading segment test-path-utils: offer to run a protectNTFS/protectHFS benchmark ...maint
						commit
						d3ac8c3f27
					
				|  | @ -0,0 +1,54 @@ | |||
| Git v2.14.6 Release Notes | ||||
| ========================= | ||||
|  | ||||
| This release addresses the security issues CVE-2019-1348, | ||||
| CVE-2019-1349, CVE-2019-1350, CVE-2019-1351, CVE-2019-1352, | ||||
| CVE-2019-1353, CVE-2019-1354, and CVE-2019-1387. | ||||
|  | ||||
| Fixes since v2.14.5 | ||||
| ------------------- | ||||
|  | ||||
|  * CVE-2019-1348: | ||||
|    The --export-marks option of git fast-import is exposed also via | ||||
|    the in-stream command feature export-marks=... and it allows | ||||
|    overwriting arbitrary paths. | ||||
|  | ||||
|  * CVE-2019-1349: | ||||
|    When submodules are cloned recursively, under certain circumstances | ||||
|    Git could be fooled into using the same Git directory twice. We now | ||||
|    require the directory to be empty. | ||||
|  | ||||
|  * CVE-2019-1350: | ||||
|    Incorrect quoting of command-line arguments allowed remote code | ||||
|    execution during a recursive clone in conjunction with SSH URLs. | ||||
|  | ||||
|  * CVE-2019-1351: | ||||
|    While the only permitted drive letters for physical drives on | ||||
|    Windows are letters of the US-English alphabet, this restriction | ||||
|    does not apply to virtual drives assigned via subst <letter>: | ||||
|    <path>. Git mistook such paths for relative paths, allowing writing | ||||
|    outside of the worktree while cloning. | ||||
|  | ||||
|  * CVE-2019-1352: | ||||
|    Git was unaware of NTFS Alternate Data Streams, allowing files | ||||
|    inside the .git/ directory to be overwritten during a clone. | ||||
|  | ||||
|  * CVE-2019-1353: | ||||
|    When running Git in the Windows Subsystem for Linux (also known as | ||||
|    "WSL") while accessing a working directory on a regular Windows | ||||
|    drive, none of the NTFS protections were active. | ||||
|  | ||||
|  * CVE-2019-1354: | ||||
|    Filenames on Linux/Unix can contain backslashes. On Windows, | ||||
|    backslashes are directory separators. Git did not use to refuse to | ||||
|    write out tracked files with such filenames. | ||||
|  | ||||
|  * CVE-2019-1387: | ||||
|    Recursive clones are currently affected by a vulnerability that is | ||||
|    caused by too-lax validation of submodule names, allowing very | ||||
|    targeted attacks via remote code execution in recursive clones. | ||||
|  | ||||
| Credit for finding these vulnerabilities goes to Microsoft Security | ||||
| Response Center, in particular to Nicolas Joly. The `fast-import` | ||||
| fixes were provided by Jeff King, the other fixes by Johannes | ||||
| Schindelin with help from Garima Singh. | ||||
|  | @ -50,6 +50,21 @@ OPTIONS | |||
| 	memory used by fast-import during this run.  Showing this output | ||||
| 	is currently the default, but can be disabled with --quiet. | ||||
|  | ||||
| --allow-unsafe-features:: | ||||
| 	Many command-line options can be provided as part of the | ||||
| 	fast-import stream itself by using the `feature` or `option` | ||||
| 	commands. However, some of these options are unsafe (e.g., | ||||
| 	allowing fast-import to access the filesystem outside of the | ||||
| 	repository). These options are disabled by default, but can be | ||||
| 	allowed by providing this option on the command line.  This | ||||
| 	currently impacts only the `export-marks`, `import-marks`, and | ||||
| 	`import-marks-if-exists` feature commands. | ||||
| + | ||||
| 	Only enable this option if you trust the program generating the | ||||
| 	fast-import stream! This option is enabled automatically for | ||||
| 	remote-helpers that use the `import` capability, as they are | ||||
| 	already trusted to run their own code. | ||||
|  | ||||
| Options for Frontends | ||||
| ~~~~~~~~~~~~~~~~~~~~~ | ||||
|  | ||||
|  |  | |||
|  | @ -758,7 +758,7 @@ static int checkout(int submodule_progress) | |||
|  | ||||
| 	if (!err && (option_recurse_submodules.nr > 0)) { | ||||
| 		struct argv_array args = ARGV_ARRAY_INIT; | ||||
| 		argv_array_pushl(&args, "submodule", "update", "--init", "--recursive", NULL); | ||||
| 		argv_array_pushl(&args, "submodule", "update", "--require-init", "--recursive", NULL); | ||||
|  | ||||
| 		if (option_shallow_submodules == 1) | ||||
| 			argv_array_push(&args, "--depth=1"); | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ | |||
| #include "remote.h" | ||||
| #include "refs.h" | ||||
| #include "connect.h" | ||||
| #include "dir.h" | ||||
|  | ||||
| static char *get_default_remote(void) | ||||
| { | ||||
|  | @ -616,6 +617,7 @@ static int module_clone(int argc, const char **argv, const char *prefix) | |||
| 	char *p, *path = NULL, *sm_gitdir; | ||||
| 	struct strbuf sb = STRBUF_INIT; | ||||
| 	struct string_list reference = STRING_LIST_INIT_NODUP; | ||||
| 	int require_init = 0; | ||||
| 	char *sm_alternate = NULL, *error_strategy = NULL; | ||||
|  | ||||
| 	struct option module_clone_options[] = { | ||||
|  | @ -640,6 +642,8 @@ static int module_clone(int argc, const char **argv, const char *prefix) | |||
| 		OPT__QUIET(&quiet, "Suppress output for cloning a submodule"), | ||||
| 		OPT_BOOL(0, "progress", &progress, | ||||
| 			   N_("force cloning progress")), | ||||
| 		OPT_BOOL(0, "require-init", &require_init, | ||||
| 			   N_("disallow cloning into non-empty directory")), | ||||
| 		OPT_END() | ||||
| 	}; | ||||
|  | ||||
|  | @ -667,6 +671,10 @@ static int module_clone(int argc, const char **argv, const char *prefix) | |||
| 	} else | ||||
| 		path = xstrdup(path); | ||||
|  | ||||
| 	if (validate_submodule_git_dir(sm_gitdir, name) < 0) | ||||
| 		die(_("refusing to create/use '%s' in another submodule's " | ||||
| 			"git dir"), sm_gitdir); | ||||
|  | ||||
| 	if (!file_exists(sm_gitdir)) { | ||||
| 		if (safe_create_leading_directories_const(sm_gitdir) < 0) | ||||
| 			die(_("could not create directory '%s'"), sm_gitdir); | ||||
|  | @ -678,6 +686,8 @@ static int module_clone(int argc, const char **argv, const char *prefix) | |||
| 			die(_("clone of '%s' into submodule path '%s' failed"), | ||||
| 			    url, path); | ||||
| 	} else { | ||||
| 		if (require_init && !access(path, X_OK) && !is_empty_dir(path)) | ||||
| 			die(_("directory not empty: '%s'"), path); | ||||
| 		if (safe_create_leading_directories_const(path) < 0) | ||||
| 			die(_("could not create directory '%s'"), path); | ||||
| 		strbuf_addf(&sb, "%s/index", sm_gitdir); | ||||
|  | @ -726,6 +736,7 @@ struct submodule_update_clone { | |||
| 	int quiet; | ||||
| 	int recommend_shallow; | ||||
| 	struct string_list references; | ||||
| 	unsigned require_init; | ||||
| 	const char *depth; | ||||
| 	const char *recursive_prefix; | ||||
| 	const char *prefix; | ||||
|  | @ -741,7 +752,7 @@ struct submodule_update_clone { | |||
| 	int failed_clones_nr, failed_clones_alloc; | ||||
| }; | ||||
| #define SUBMODULE_UPDATE_CLONE_INIT {0, MODULE_LIST_INIT, 0, \ | ||||
| 	SUBMODULE_UPDATE_STRATEGY_INIT, 0, 0, -1, STRING_LIST_INIT_DUP, \ | ||||
| 	SUBMODULE_UPDATE_STRATEGY_INIT, 0, 0, -1, STRING_LIST_INIT_DUP, 0, \ | ||||
| 	NULL, NULL, NULL, \ | ||||
| 	STRING_LIST_INIT_DUP, 0, NULL, 0, 0} | ||||
|  | ||||
|  | @ -860,6 +871,8 @@ static int prepare_to_clone_next_submodule(const struct cache_entry *ce, | |||
| 		argv_array_pushl(&child->args, "--prefix", suc->prefix, NULL); | ||||
| 	if (suc->recommend_shallow && sub->recommend_shallow == 1) | ||||
| 		argv_array_push(&child->args, "--depth=1"); | ||||
| 	if (suc->require_init) | ||||
| 		argv_array_push(&child->args, "--require-init"); | ||||
| 	argv_array_pushl(&child->args, "--path", sub->path, NULL); | ||||
| 	argv_array_pushl(&child->args, "--name", sub->name, NULL); | ||||
| 	argv_array_pushl(&child->args, "--url", url, NULL); | ||||
|  | @ -1011,6 +1024,8 @@ static int update_clone(int argc, const char **argv, const char *prefix) | |||
| 		OPT__QUIET(&suc.quiet, N_("don't print cloning progress")), | ||||
| 		OPT_BOOL(0, "progress", &suc.progress, | ||||
| 			    N_("force cloning progress")), | ||||
| 		OPT_BOOL(0, "require-init", &suc.require_init, | ||||
| 			   N_("disallow cloning into non-empty directory")), | ||||
| 		OPT_END() | ||||
| 	}; | ||||
|  | ||||
|  |  | |||
							
								
								
									
										100
									
								
								compat/mingw.c
								
								
								
								
							
							
						
						
									
										100
									
								
								compat/mingw.c
								
								
								
								
							|  | @ -333,6 +333,12 @@ int mingw_mkdir(const char *path, int mode) | |||
| { | ||||
| 	int ret; | ||||
| 	wchar_t wpath[MAX_PATH]; | ||||
|  | ||||
| 	if (!is_valid_win32_path(path)) { | ||||
| 		errno = EINVAL; | ||||
| 		return -1; | ||||
| 	} | ||||
|  | ||||
| 	if (xutftowcs_path(wpath, path) < 0) | ||||
| 		return -1; | ||||
| 	ret = _wmkdir(wpath); | ||||
|  | @ -345,13 +351,18 @@ int mingw_open (const char *filename, int oflags, ...) | |||
| { | ||||
| 	va_list args; | ||||
| 	unsigned mode; | ||||
| 	int fd; | ||||
| 	int fd, create = (oflags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL); | ||||
| 	wchar_t wfilename[MAX_PATH]; | ||||
|  | ||||
| 	va_start(args, oflags); | ||||
| 	mode = va_arg(args, int); | ||||
| 	va_end(args); | ||||
|  | ||||
| 	if (!is_valid_win32_path(filename)) { | ||||
| 		errno = create ? EINVAL : ENOENT; | ||||
| 		return -1; | ||||
| 	} | ||||
|  | ||||
| 	if (filename && !strcmp(filename, "/dev/null")) | ||||
| 		filename = "nul"; | ||||
|  | ||||
|  | @ -413,6 +424,11 @@ 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)) { | ||||
| 		int create = otype && strchr(otype, 'w'); | ||||
| 		errno = create ? EINVAL : ENOENT; | ||||
| 		return NULL; | ||||
| 	} | ||||
| 	if (filename && !strcmp(filename, "/dev/null")) | ||||
| 		filename = "nul"; | ||||
| 	if (xutftowcs_path(wfilename, filename) < 0 || | ||||
|  | @ -435,6 +451,11 @@ 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)) { | ||||
| 		int create = otype && strchr(otype, 'w'); | ||||
| 		errno = create ? EINVAL : ENOENT; | ||||
| 		return NULL; | ||||
| 	} | ||||
| 	if (filename && !strcmp(filename, "/dev/null")) | ||||
| 		filename = "nul"; | ||||
| 	if (xutftowcs_path(wfilename, filename) < 0 || | ||||
|  | @ -872,7 +893,7 @@ static const char *quote_arg(const char *arg) | |||
| 				p++; | ||||
| 				len++; | ||||
| 			} | ||||
| 			if (*p == '"') | ||||
| 			if (*p == '"' || !*p) | ||||
| 				n += count*2 + 1; | ||||
| 			continue; | ||||
| 		} | ||||
|  | @ -894,16 +915,19 @@ static const char *quote_arg(const char *arg) | |||
| 				count++; | ||||
| 				*d++ = *arg++; | ||||
| 			} | ||||
| 			if (*arg == '"') { | ||||
| 			if (*arg == '"' || !*arg) { | ||||
| 				while (count-- > 0) | ||||
| 					*d++ = '\\'; | ||||
| 				/* don't escape the surrounding end quote */ | ||||
| 				if (!*arg) | ||||
| 					break; | ||||
| 				*d++ = '\\'; | ||||
| 			} | ||||
| 		} | ||||
| 		*d++ = *arg++; | ||||
| 	} | ||||
| 	*d++ = '"'; | ||||
| 	*d++ = 0; | ||||
| 	*d++ = '\0'; | ||||
| 	return q; | ||||
| } | ||||
|  | ||||
|  | @ -1965,6 +1989,30 @@ pid_t waitpid(pid_t pid, int *status, int options) | |||
| 	return -1; | ||||
| } | ||||
|  | ||||
| int mingw_has_dos_drive_prefix(const char *path) | ||||
| { | ||||
| 	int i; | ||||
|  | ||||
| 	/* | ||||
| 	 * Does it start with an ASCII letter (i.e. highest bit not set), | ||||
| 	 * followed by a colon? | ||||
| 	 */ | ||||
| 	if (!(0x80 & (unsigned char)*path)) | ||||
| 		return *path && path[1] == ':' ? 2 : 0; | ||||
|  | ||||
| 	/* | ||||
| 	 * While drive letters must be letters of the English alphabet, it is | ||||
| 	 * possible to assign virtually _any_ Unicode character via `subst` as | ||||
| 	 * a drive letter to "virtual drives". Even `1`, or `ä`. Or fun stuff | ||||
| 	 * like this: | ||||
| 	 * | ||||
| 	 *      subst ֍: %USERPROFILE%\Desktop | ||||
| 	 */ | ||||
| 	for (i = 1; i < 4 && (0x80 & (unsigned char)path[i]); i++) | ||||
| 		; /* skip first UTF-8 character */ | ||||
| 	return path[i] == ':' ? i + 1 : 0; | ||||
| } | ||||
|  | ||||
| int mingw_skip_dos_drive_prefix(char **path) | ||||
| { | ||||
| 	int ret = has_dos_drive_prefix(*path); | ||||
|  | @ -2106,6 +2154,50 @@ static void setup_windows_environment(void) | |||
| 		setenv("TERM", "cygwin", 1); | ||||
| } | ||||
|  | ||||
| int is_valid_win32_path(const char *path) | ||||
| { | ||||
| 	int preceding_space_or_period = 0, i = 0, periods = 0; | ||||
|  | ||||
| 	if (!protect_ntfs) | ||||
| 		return 1; | ||||
|  | ||||
| 	skip_dos_drive_prefix((char **)&path); | ||||
|  | ||||
| 	for (;;) { | ||||
| 		char c = *(path++); | ||||
| 		switch (c) { | ||||
| 		case '\0': | ||||
| 		case '/': case '\\': | ||||
| 			/* cannot end in ` ` or `.`, except for `.` and `..` */ | ||||
| 			if (preceding_space_or_period && | ||||
| 			    (i != periods || periods > 2)) | ||||
| 				return 0; | ||||
| 			if (!c) | ||||
| 				return 1; | ||||
|  | ||||
| 			i = periods = preceding_space_or_period = 0; | ||||
| 			continue; | ||||
| 		case '.': | ||||
| 			periods++; | ||||
| 			/* fallthru */ | ||||
| 		case ' ': | ||||
| 			preceding_space_or_period = 1; | ||||
| 			i++; | ||||
| 			continue; | ||||
| 		case ':': /* DOS drive prefix was already skipped */ | ||||
| 		case '<': case '>': case '"': case '|': case '?': case '*': | ||||
| 			/* illegal character */ | ||||
| 			return 0; | ||||
| 		default: | ||||
| 			if (c > '\0' && c < '\x20') | ||||
| 				/* illegal character */ | ||||
| 				return 0; | ||||
| 		} | ||||
| 		preceding_space_or_period = 0; | ||||
| 		i++; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * Disable MSVCRT command line wildcard expansion (__getmainargs called from | ||||
|  * mingw startup code, see init.c in mingw runtime). | ||||
|  |  | |||
|  | @ -394,8 +394,8 @@ HANDLE winansi_get_osfhandle(int fd); | |||
|  * git specific compatibility | ||||
|  */ | ||||
|  | ||||
| #define has_dos_drive_prefix(path) \ | ||||
| 	(isalpha(*(path)) && (path)[1] == ':' ? 2 : 0) | ||||
| int mingw_has_dos_drive_prefix(const char *path); | ||||
| #define has_dos_drive_prefix mingw_has_dos_drive_prefix | ||||
| int mingw_skip_dos_drive_prefix(char **path); | ||||
| #define skip_dos_drive_prefix mingw_skip_dos_drive_prefix | ||||
| static inline int mingw_is_dir_sep(int c) | ||||
|  | @ -428,6 +428,20 @@ int mingw_offset_1st_component(const char *path); | |||
| #include <inttypes.h> | ||||
| #endif | ||||
|  | ||||
| /** | ||||
|  * Verifies that the given path is a valid one on Windows. | ||||
|  * | ||||
|  * In particular, path segments are disallowed which | ||||
|  * | ||||
|  * - end in a period or a space (except the special directories `.` and `..`). | ||||
|  * | ||||
|  * - contain any of the reserved characters, e.g. `:`, `;`, `*`, etc | ||||
|  * | ||||
|  * Returns 1 upon success, otherwise 0. | ||||
|  */ | ||||
| int is_valid_win32_path(const char *path); | ||||
| #define is_valid_path(path) is_valid_win32_path(path) | ||||
|  | ||||
| /** | ||||
|  * Converts UTF-8 encoded string to UTF-16LE. | ||||
|  * | ||||
|  |  | |||
|  | @ -381,7 +381,6 @@ ifeq ($(uname_S),Windows) | |||
| 	EXTLIBS = user32.lib advapi32.lib shell32.lib wininet.lib ws2_32.lib invalidcontinue.obj | ||||
| 	PTHREAD_LIBS = | ||||
| 	lib = | ||||
| 	BASIC_CFLAGS += -DPROTECT_NTFS_DEFAULT=1 | ||||
| ifndef DEBUG | ||||
| 	BASIC_CFLAGS += -GL -Os -MD | ||||
| 	BASIC_LDFLAGS += -LTCG | ||||
|  | @ -519,7 +518,6 @@ ifneq (,$(findstring MINGW,$(uname_S))) | |||
| 	COMPAT_OBJS += compat/mingw.o compat/winansi.o \ | ||||
| 		compat/win32/pthread.o compat/win32/syslog.o \ | ||||
| 		compat/win32/dirent.o | ||||
| 	BASIC_CFLAGS += -DPROTECT_NTFS_DEFAULT=1 | ||||
| 	EXTLIBS += -lws2_32 | ||||
| 	GITLIBS += git.res | ||||
| 	PTHREAD_LIBS = | ||||
|  |  | |||
|  | @ -264,7 +264,7 @@ int url_is_local_not_ssh(const char *url) | |||
| 	const char *colon = strchr(url, ':'); | ||||
| 	const char *slash = strchr(url, '/'); | ||||
| 	return !colon || (slash && slash < colon) || | ||||
| 		has_dos_drive_prefix(url); | ||||
| 		(has_dos_drive_prefix(url) && is_valid_path(url)); | ||||
| } | ||||
|  | ||||
| static const char *prot_name(enum protocol protocol) | ||||
|  |  | |||
|  | @ -73,7 +73,7 @@ enum log_refs_config log_all_ref_updates = LOG_REFS_UNSET; | |||
| int protect_hfs = PROTECT_HFS_DEFAULT; | ||||
|  | ||||
| #ifndef PROTECT_NTFS_DEFAULT | ||||
| #define PROTECT_NTFS_DEFAULT 0 | ||||
| #define PROTECT_NTFS_DEFAULT 1 | ||||
| #endif | ||||
| int protect_ntfs = PROTECT_NTFS_DEFAULT; | ||||
|  | ||||
|  |  | |||
|  | @ -367,6 +367,7 @@ static uintmax_t next_mark; | |||
| static struct strbuf new_data = STRBUF_INIT; | ||||
| static int seen_data_command; | ||||
| static int require_explicit_termination; | ||||
| static int allow_unsafe_features; | ||||
|  | ||||
| /* Signal handling */ | ||||
| static volatile sig_atomic_t checkpoint_requested; | ||||
|  | @ -1862,6 +1863,12 @@ static void dump_marks(void) | |||
| 	if (!export_marks_file || (import_marks_file && !import_marks_file_done)) | ||||
| 		return; | ||||
|  | ||||
| 	if (safe_create_leading_directories_const(export_marks_file)) { | ||||
| 		failure |= error_errno("unable to create leading directories of %s", | ||||
| 				       export_marks_file); | ||||
| 		return; | ||||
| 	} | ||||
|  | ||||
| 	if (hold_lock_file_for_update(&mark_lock, export_marks_file, 0) < 0) { | ||||
| 		failure |= error_errno("Unable to write marks file %s", | ||||
| 				       export_marks_file); | ||||
|  | @ -3229,7 +3236,6 @@ static void option_import_marks(const char *marks, | |||
| 	} | ||||
|  | ||||
| 	import_marks_file = make_fast_import_path(marks); | ||||
| 	safe_create_leading_directories_const(import_marks_file); | ||||
| 	import_marks_file_from_stream = from_stream; | ||||
| 	import_marks_file_ignore_missing = ignore_missing; | ||||
| } | ||||
|  | @ -3270,7 +3276,6 @@ static void option_active_branches(const char *branches) | |||
| static void option_export_marks(const char *marks) | ||||
| { | ||||
| 	export_marks_file = make_fast_import_path(marks); | ||||
| 	safe_create_leading_directories_const(export_marks_file); | ||||
| } | ||||
|  | ||||
| static void option_cat_blob_fd(const char *fd) | ||||
|  | @ -3313,10 +3318,12 @@ static int parse_one_option(const char *option) | |||
| 		option_active_branches(option); | ||||
| 	} else if (skip_prefix(option, "export-pack-edges=", &option)) { | ||||
| 		option_export_pack_edges(option); | ||||
| 	} else if (starts_with(option, "quiet")) { | ||||
| 	} else if (!strcmp(option, "quiet")) { | ||||
| 		show_stats = 0; | ||||
| 	} else if (starts_with(option, "stats")) { | ||||
| 	} else if (!strcmp(option, "stats")) { | ||||
| 		show_stats = 1; | ||||
| 	} else if (!strcmp(option, "allow-unsafe-features")) { | ||||
| 		; /* already handled during early option parsing */ | ||||
| 	} else { | ||||
| 		return 0; | ||||
| 	} | ||||
|  | @ -3324,6 +3331,13 @@ static int parse_one_option(const char *option) | |||
| 	return 1; | ||||
| } | ||||
|  | ||||
| static void check_unsafe_feature(const char *feature, int from_stream) | ||||
| { | ||||
| 	if (from_stream && !allow_unsafe_features) | ||||
| 		die(_("feature '%s' forbidden in input without --allow-unsafe-features"), | ||||
| 		    feature); | ||||
| } | ||||
|  | ||||
| static int parse_one_feature(const char *feature, int from_stream) | ||||
| { | ||||
| 	const char *arg; | ||||
|  | @ -3331,10 +3345,13 @@ static int parse_one_feature(const char *feature, int from_stream) | |||
| 	if (skip_prefix(feature, "date-format=", &arg)) { | ||||
| 		option_date_format(arg); | ||||
| 	} else if (skip_prefix(feature, "import-marks=", &arg)) { | ||||
| 		check_unsafe_feature("import-marks", from_stream); | ||||
| 		option_import_marks(arg, from_stream, 0); | ||||
| 	} else if (skip_prefix(feature, "import-marks-if-exists=", &arg)) { | ||||
| 		check_unsafe_feature("import-marks-if-exists", from_stream); | ||||
| 		option_import_marks(arg, from_stream, 1); | ||||
| 	} else if (skip_prefix(feature, "export-marks=", &arg)) { | ||||
| 		check_unsafe_feature(feature, from_stream); | ||||
| 		option_export_marks(arg); | ||||
| 	} else if (!strcmp(feature, "get-mark")) { | ||||
| 		; /* Don't die - this feature is supported */ | ||||
|  | @ -3461,6 +3478,20 @@ int cmd_main(int argc, const char **argv) | |||
| 	avail_tree_table = xcalloc(avail_tree_table_sz, sizeof(struct avail_tree_content*)); | ||||
| 	marks = pool_calloc(1, sizeof(struct mark_set)); | ||||
|  | ||||
| 	/* | ||||
| 	 * We don't parse most options until after we've seen the set of | ||||
| 	 * "feature" lines at the start of the stream (which allows the command | ||||
| 	 * line to override stream data). But we must do an early parse of any | ||||
| 	 * command-line options that impact how we interpret the feature lines. | ||||
| 	 */ | ||||
| 	for (i = 1; i < argc; i++) { | ||||
| 		const char *arg = argv[i]; | ||||
| 		if (*arg != '-' || !strcmp(arg, "--")) | ||||
| 			break; | ||||
| 		if (!strcmp(arg, "--allow-unsafe-features")) | ||||
| 			allow_unsafe_features = 1; | ||||
| 	} | ||||
|  | ||||
| 	global_argc = argc; | ||||
| 	global_argv = argv; | ||||
|  | ||||
|  |  | |||
							
								
								
									
										11
									
								
								fsck.c
								
								
								
								
							
							
						
						
									
										11
									
								
								fsck.c
								
								
								
								
							|  | @ -551,7 +551,7 @@ static int fsck_tree(struct tree *item, struct fsck_options *options) | |||
|  | ||||
| 	while (desc.size) { | ||||
| 		unsigned mode; | ||||
| 		const char *name; | ||||
| 		const char *name, *backslash; | ||||
| 		const struct object_id *oid; | ||||
|  | ||||
| 		oid = tree_entry_extract(&desc, &name, &mode); | ||||
|  | @ -565,6 +565,15 @@ static int fsck_tree(struct tree *item, struct fsck_options *options) | |||
| 			       is_hfs_dotgit(name) || | ||||
| 			       is_ntfs_dotgit(name)); | ||||
| 		has_zero_pad |= *(char *)desc.buffer == '0'; | ||||
|  | ||||
| 		if ((backslash = strchr(name, '\\'))) { | ||||
| 			while (backslash) { | ||||
| 				backslash++; | ||||
| 				has_dotgit |= is_ntfs_dotgit(backslash); | ||||
| 				backslash = strchr(backslash, '\\'); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if (update_tree_entry_gently(&desc)) { | ||||
| 			retval += report(options, &item->object, FSCK_MSG_BAD_TREE, "cannot be parsed as a tree"); | ||||
| 			break; | ||||
|  |  | |||
|  | @ -370,6 +370,10 @@ static inline int git_offset_1st_component(const char *path) | |||
| #define offset_1st_component git_offset_1st_component | ||||
| #endif | ||||
|  | ||||
| #ifndef is_valid_path | ||||
| #define is_valid_path(path) 1 | ||||
| #endif | ||||
|  | ||||
| #ifndef find_last_dir_sep | ||||
| static inline char *git_find_last_dir_sep(const char *path) | ||||
| { | ||||
|  |  | |||
|  | @ -34,6 +34,7 @@ reference= | |||
| cached= | ||||
| recursive= | ||||
| init= | ||||
| require_init= | ||||
| files= | ||||
| remote= | ||||
| nofetch= | ||||
|  | @ -528,6 +529,10 @@ cmd_update() | |||
| 		-i|--init) | ||||
| 			init=1 | ||||
| 			;; | ||||
| 		--require-init) | ||||
| 			init=1 | ||||
| 			require_init=1 | ||||
| 			;; | ||||
| 		--remote) | ||||
| 			remote=1 | ||||
| 			;; | ||||
|  | @ -606,6 +611,7 @@ cmd_update() | |||
| 		${update:+--update "$update"} \ | ||||
| 		${reference:+"$reference"} \ | ||||
| 		${depth:+--depth "$depth"} \ | ||||
| 		${require_init:+--require-init} \ | ||||
| 		${recommend_shallow:+"$recommend_shallow"} \ | ||||
| 		${jobs:+$jobs} \ | ||||
| 		"$@" || echo "#unmatched" $? | ||||
|  |  | |||
							
								
								
									
										94
									
								
								path.c
								
								
								
								
							
							
						
						
									
										94
									
								
								path.c
								
								
								
								
							|  | @ -1289,36 +1289,76 @@ int daemon_avoid_alias(const char *p) | |||
| 	} | ||||
| } | ||||
|  | ||||
| static int only_spaces_and_periods(const char *path, size_t len, size_t skip) | ||||
| { | ||||
| 	if (len < skip) | ||||
| 		return 0; | ||||
| 	len -= skip; | ||||
| 	path += skip; | ||||
| 	while (len-- > 0) { | ||||
| 		char c = *(path++); | ||||
| 		if (c != ' ' && c != '.') | ||||
| 			return 0; | ||||
| 	} | ||||
| 	return 1; | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * On NTFS, we need to be careful to disallow certain synonyms of the `.git/` | ||||
|  * directory: | ||||
|  * | ||||
|  * - For historical reasons, file names that end in spaces or periods are | ||||
|  *   automatically trimmed. Therefore, `.git . . ./` is a valid way to refer | ||||
|  *   to `.git/`. | ||||
|  * | ||||
|  * - For other historical reasons, file names that do not conform to the 8.3 | ||||
|  *   format (up to eight characters for the basename, three for the file | ||||
|  *   extension, certain characters not allowed such as `+`, etc) are associated | ||||
|  *   with a so-called "short name", at least on the `C:` drive by default. | ||||
|  *   Which means that `git~1/` is a valid way to refer to `.git/`. | ||||
|  * | ||||
|  *   Note: Technically, `.git/` could receive the short name `git~2` if the | ||||
|  *   short name `git~1` were already used. In Git, however, we guarantee that | ||||
|  *   `.git` is the first item in a directory, therefore it will be associated | ||||
|  *   with the short name `git~1` (unless short names are disabled). | ||||
|  * | ||||
|  * - For yet other historical reasons, NTFS supports so-called "Alternate Data | ||||
|  *   Streams", i.e. metadata associated with a given file, referred to via | ||||
|  *   `<filename>:<stream-name>:<stream-type>`. There exists a default stream | ||||
|  *   type for directories, allowing `.git/` to be accessed via | ||||
|  *   `.git::$INDEX_ALLOCATION/`. | ||||
|  * | ||||
|  * When this function returns 1, it indicates that the specified file/directory | ||||
|  * name refers to a `.git` file or directory, or to any of these synonyms, and | ||||
|  * Git should therefore not track it. | ||||
|  * | ||||
|  * For performance reasons, _all_ Alternate Data Streams of `.git/` are | ||||
|  * forbidden, not just `::$INDEX_ALLOCATION`. | ||||
|  * | ||||
|  * This function is intended to be used by `git fsck` even on platforms where | ||||
|  * the backslash is a regular filename character, therefore it needs to handle | ||||
|  * backlash characters in the provided `name` specially: they are interpreted | ||||
|  * as directory separators. | ||||
|  */ | ||||
| int is_ntfs_dotgit(const char *name) | ||||
| { | ||||
| 	size_t len; | ||||
| 	char c; | ||||
|  | ||||
| 	for (len = 0; ; len++) | ||||
| 		if (!name[len] || name[len] == '\\' || is_dir_sep(name[len])) { | ||||
| 			if (only_spaces_and_periods(name, len, 4) && | ||||
| 					!strncasecmp(name, ".git", 4)) | ||||
| 				return 1; | ||||
| 			if (only_spaces_and_periods(name, len, 5) && | ||||
| 					!strncasecmp(name, "git~1", 5)) | ||||
| 				return 1; | ||||
| 			if (name[len] != '\\') | ||||
| 	/* | ||||
| 	 * Note that when we don't find `.git` or `git~1` we end up with `name` | ||||
| 	 * advanced partway through the string. That's okay, though, as we | ||||
| 	 * return immediately in those cases, without looking at `name` any | ||||
| 	 * further. | ||||
| 	 */ | ||||
| 	c = *(name++); | ||||
| 	if (c == '.') { | ||||
| 		/* .git */ | ||||
| 		if (((c = *(name++)) != 'g' && c != 'G') || | ||||
| 		    ((c = *(name++)) != 'i' && c != 'I') || | ||||
| 		    ((c = *(name++)) != 't' && c != 'T')) | ||||
| 			return 0; | ||||
| 	} else if (c == 'g' || c == 'G') { | ||||
| 		/* git ~1 */ | ||||
| 		if (((c = *(name++)) != 'i' && c != 'I') || | ||||
| 		    ((c = *(name++)) != 't' && c != 'T') || | ||||
| 		    *(name++) != '~' || | ||||
| 		    *(name++) != '1') | ||||
| 			return 0; | ||||
| 	} else | ||||
| 		return 0; | ||||
|  | ||||
| 	for (;;) { | ||||
| 		c = *(name++); | ||||
| 		if (!c || c == '\\' || c == '/' || c == ':') | ||||
| 			return 1; | ||||
| 		if (c != '.' && c != ' ') | ||||
| 			return 0; | ||||
| 			name += len + 1; | ||||
| 			len = -1; | ||||
| 	} | ||||
| } | ||||
|  | ||||
|  | @ -1335,7 +1375,7 @@ static int is_ntfs_dot_generic(const char *name, | |||
| only_spaces_and_periods: | ||||
| 		for (;;) { | ||||
| 			char c = name[i++]; | ||||
| 			if (!c) | ||||
| 			if (!c || c == ':') | ||||
| 				return 1; | ||||
| 			if (c != ' ' && c != '.') | ||||
| 				return 0; | ||||
|  |  | |||
							
								
								
									
										11
									
								
								read-cache.c
								
								
								
								
							
							
						
						
									
										11
									
								
								read-cache.c
								
								
								
								
							|  | @ -848,6 +848,9 @@ int verify_path(const char *path, unsigned mode) | |||
| 	if (has_dos_drive_prefix(path)) | ||||
| 		return 0; | ||||
|  | ||||
| 	if (!is_valid_path(path)) | ||||
| 		return 0; | ||||
|  | ||||
| 	goto inside; | ||||
| 	for (;;) { | ||||
| 		if (!c) | ||||
|  | @ -875,7 +878,15 @@ inside: | |||
| 			if ((c == '.' && !verify_dotfile(path, mode)) || | ||||
| 			    is_dir_sep(c) || c == '\0') | ||||
| 				return 0; | ||||
| 		} else if (c == '\\' && protect_ntfs) { | ||||
| 			if (is_ntfs_dotgit(path)) | ||||
| 				return 0; | ||||
| 			if (S_ISLNK(mode)) { | ||||
| 				if (is_ntfs_dotgitmodules(path)) | ||||
| 					return 0; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		c = *path++; | ||||
| 	} | ||||
| } | ||||
|  |  | |||
							
								
								
									
										49
									
								
								submodule.c
								
								
								
								
							
							
						
						
									
										49
									
								
								submodule.c
								
								
								
								
							|  | @ -1794,6 +1794,47 @@ int merge_submodule(struct object_id *result, const char *path, | |||
| 	return 0; | ||||
| } | ||||
|  | ||||
| int validate_submodule_git_dir(char *git_dir, const char *submodule_name) | ||||
| { | ||||
| 	size_t len = strlen(git_dir), suffix_len = strlen(submodule_name); | ||||
| 	char *p; | ||||
| 	int ret = 0; | ||||
|  | ||||
| 	if (len <= suffix_len || (p = git_dir + len - suffix_len)[-1] != '/' || | ||||
| 	    strcmp(p, submodule_name)) | ||||
| 		BUG("submodule name '%s' not a suffix of git dir '%s'", | ||||
| 		    submodule_name, git_dir); | ||||
|  | ||||
| 	/* | ||||
| 	 * We prevent the contents of sibling submodules' git directories to | ||||
| 	 * clash. | ||||
| 	 * | ||||
| 	 * Example: having a submodule named `hippo` and another one named | ||||
| 	 * `hippo/hooks` would result in the git directories | ||||
| 	 * `.git/modules/hippo/` and `.git/modules/hippo/hooks/`, respectively, | ||||
| 	 * but the latter directory is already designated to contain the hooks | ||||
| 	 * of the former. | ||||
| 	 */ | ||||
| 	for (; *p; p++) { | ||||
| 		if (is_dir_sep(*p)) { | ||||
| 			char c = *p; | ||||
|  | ||||
| 			*p = '\0'; | ||||
| 			if (is_git_directory(git_dir)) | ||||
| 				ret = -1; | ||||
| 			*p = c; | ||||
|  | ||||
| 			if (ret < 0) | ||||
| 				return error(_("submodule git dir '%s' is " | ||||
| 					       "inside git dir '%.*s'"), | ||||
| 					     git_dir, | ||||
| 					     (int)(p - git_dir), git_dir); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return 0; | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * Embeds a single submodules git directory into the superprojects git dir, | ||||
|  * non recursively. | ||||
|  | @ -1802,7 +1843,7 @@ static void relocate_single_git_dir_into_superproject(const char *prefix, | |||
| 						      const char *path) | ||||
| { | ||||
| 	char *old_git_dir = NULL, *real_old_git_dir = NULL, *real_new_git_dir = NULL; | ||||
| 	const char *new_git_dir; | ||||
| 	char *new_git_dir; | ||||
| 	const struct submodule *sub; | ||||
|  | ||||
| 	if (submodule_uses_worktrees(path)) | ||||
|  | @ -1820,10 +1861,14 @@ static void relocate_single_git_dir_into_superproject(const char *prefix, | |||
| 	if (!sub) | ||||
| 		die(_("could not lookup name for submodule '%s'"), path); | ||||
|  | ||||
| 	new_git_dir = git_path("modules/%s", sub->name); | ||||
| 	new_git_dir = git_pathdup("modules/%s", sub->name); | ||||
| 	if (validate_submodule_git_dir(new_git_dir, sub->name) < 0) | ||||
| 		die(_("refusing to move '%s' into an existing git dir"), | ||||
| 		    real_old_git_dir); | ||||
| 	if (safe_create_leading_directories_const(new_git_dir) < 0) | ||||
| 		die(_("could not create directory '%s'"), new_git_dir); | ||||
| 	real_new_git_dir = real_pathdup(new_git_dir, 1); | ||||
| 	free(new_git_dir); | ||||
|  | ||||
| 	fprintf(stderr, _("Migrating git directory of '%s%s' from\n'%s' to\n'%s'\n"), | ||||
| 		get_super_prefix_or_empty(), path, | ||||
|  |  | |||
|  | @ -111,6 +111,11 @@ extern void connect_work_tree_and_git_dir(const char *work_tree, const char *git | |||
|  */ | ||||
| int submodule_to_gitdir(struct strbuf *buf, const char *submodule); | ||||
|  | ||||
| /* | ||||
|  * Make sure that no submodule's git dir is nested in a sibling submodule's. | ||||
|  */ | ||||
| int validate_submodule_git_dir(char *git_dir, const char *submodule_name); | ||||
|  | ||||
| #define SUBMODULE_MOVE_HEAD_DRY_RUN (1<<0) | ||||
| #define SUBMODULE_MOVE_HEAD_FORCE   (1<<1) | ||||
| extern int submodule_move_head(const char *path, | ||||
|  |  | |||
|  | @ -176,6 +176,99 @@ static int is_dotgitmodules(const char *path) | |||
| 	return is_hfs_dotgitmodules(path) || is_ntfs_dotgitmodules(path); | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * A very simple, reproducible pseudo-random generator. Copied from | ||||
|  * `test-genrandom.c`. | ||||
|  */ | ||||
| static uint64_t my_random_value = 1234; | ||||
|  | ||||
| static uint64_t my_random(void) | ||||
| { | ||||
| 	my_random_value = my_random_value * 1103515245 + 12345; | ||||
| 	return my_random_value; | ||||
| } | ||||
|  | ||||
| /* | ||||
|  * A fast approximation of the square root, without requiring math.h. | ||||
|  * | ||||
|  * It uses Newton's method to approximate the solution of 0 = x^2 - value. | ||||
|  */ | ||||
| static double my_sqrt(double value) | ||||
| { | ||||
| 	const double epsilon = 1e-6; | ||||
| 	double x = value; | ||||
|  | ||||
| 	if (value == 0) | ||||
| 		return 0; | ||||
|  | ||||
| 	for (;;) { | ||||
| 		double delta = (value / x - x) / 2; | ||||
| 		if (delta < epsilon && delta > -epsilon) | ||||
| 			return x + delta; | ||||
| 		x += delta; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| static int protect_ntfs_hfs_benchmark(int argc, const char **argv) | ||||
| { | ||||
| 	size_t i, j, nr, min_len = 3, max_len = 20; | ||||
| 	char **names; | ||||
| 	int repetitions = 15, file_mode = 0100644; | ||||
| 	uint64_t begin, end; | ||||
| 	double m[3][2], v[3][2]; | ||||
| 	uint64_t cumul; | ||||
| 	double cumul2; | ||||
|  | ||||
| 	if (argc > 1 && !strcmp(argv[1], "--with-symlink-mode")) { | ||||
| 		file_mode = 0120000; | ||||
| 		argc--; | ||||
| 		argv++; | ||||
| 	} | ||||
|  | ||||
| 	nr = argc > 1 ? strtoul(argv[1], NULL, 0) : 1000000; | ||||
| 	ALLOC_ARRAY(names, nr); | ||||
|  | ||||
| 	if (argc > 2) { | ||||
| 		min_len = strtoul(argv[2], NULL, 0); | ||||
| 		if (argc > 3) | ||||
| 			max_len = strtoul(argv[3], NULL, 0); | ||||
| 		if (min_len > max_len) | ||||
| 			die("min_len > max_len"); | ||||
| 	} | ||||
|  | ||||
| 	for (i = 0; i < nr; i++) { | ||||
| 		size_t len = min_len + (my_random() % (max_len + 1 - min_len)); | ||||
|  | ||||
| 		names[i] = xmallocz(len); | ||||
| 		while (len > 0) | ||||
| 			names[i][--len] = (char)(' ' + (my_random() % ('\x7f' - ' '))); | ||||
| 	} | ||||
|  | ||||
| 	for (protect_ntfs = 0; protect_ntfs < 2; protect_ntfs++) | ||||
| 		for (protect_hfs = 0; protect_hfs < 2; protect_hfs++) { | ||||
| 			cumul = 0; | ||||
| 			cumul2 = 0; | ||||
| 			for (i = 0; i < repetitions; i++) { | ||||
| 				begin = getnanotime(); | ||||
| 				for (j = 0; j < nr; j++) | ||||
| 					verify_path(names[j], file_mode); | ||||
| 				end = getnanotime(); | ||||
| 				printf("protect_ntfs = %d, protect_hfs = %d: %lfms\n", protect_ntfs, protect_hfs, (end-begin) / (double)1e6); | ||||
| 				cumul += end - begin; | ||||
| 				cumul2 += (end - begin) * (end - begin); | ||||
| 			} | ||||
| 			m[protect_ntfs][protect_hfs] = cumul / (double)repetitions; | ||||
| 			v[protect_ntfs][protect_hfs] = my_sqrt(cumul2 / (double)repetitions - m[protect_ntfs][protect_hfs] * m[protect_ntfs][protect_hfs]); | ||||
| 			printf("mean: %lfms, stddev: %lfms\n", m[protect_ntfs][protect_hfs] / (double)1e6, v[protect_ntfs][protect_hfs] / (double)1e6); | ||||
| 		} | ||||
|  | ||||
| 	for (protect_ntfs = 0; protect_ntfs < 2; protect_ntfs++) | ||||
| 		for (protect_hfs = 0; protect_hfs < 2; protect_hfs++) | ||||
| 			printf("ntfs=%d/hfs=%d: %lf%% slower\n", protect_ntfs, protect_hfs, (m[protect_ntfs][protect_hfs] - m[0][0]) * 100 / m[0][0]); | ||||
|  | ||||
| 	return 0; | ||||
| } | ||||
|  | ||||
| int cmd_main(int argc, const char **argv) | ||||
| { | ||||
| 	if (argc == 3 && !strcmp(argv[1], "normalize_path_copy")) { | ||||
|  | @ -290,6 +383,26 @@ int cmd_main(int argc, const char **argv) | |||
| 		return !!res; | ||||
| 	} | ||||
|  | ||||
| 	if (argc > 1 && !strcmp(argv[1], "protect_ntfs_hfs")) | ||||
| 		return !!protect_ntfs_hfs_benchmark(argc - 1, argv + 1); | ||||
|  | ||||
| 	if (argc > 1 && !strcmp(argv[1], "is_valid_path")) { | ||||
| 		int res = 0, expect = 1, i; | ||||
|  | ||||
| 		for (i = 2; i < argc; i++) | ||||
| 			if (!strcmp("--not", argv[i])) | ||||
| 				expect = 0; | ||||
| 			else if (expect != is_valid_path(argv[i])) | ||||
| 				res = error("'%s' is%s a valid path", | ||||
| 					    argv[i], expect ? " not" : ""); | ||||
| 			else | ||||
| 				fprintf(stderr, | ||||
| 					"'%s' is%s a valid path\n", | ||||
| 					argv[i], expect ? "" : " not"); | ||||
|  | ||||
| 		return !!res; | ||||
| 	} | ||||
|  | ||||
| 	fprintf(stderr, "%s: unknown function name: %s\n", argv[0], | ||||
| 		argv[1] ? argv[1] : "(there was none)"); | ||||
| 	return 1; | ||||
|  |  | |||
|  | @ -12,8 +12,8 @@ | |||
| #include "run-command.h" | ||||
| #include "argv-array.h" | ||||
| #include "strbuf.h" | ||||
| #include <string.h> | ||||
| #include <errno.h> | ||||
| #include "gettext.h" | ||||
| #include "parse-options.h" | ||||
|  | ||||
| static int number_callbacks; | ||||
| static int parallel_next(struct child_process *cp, | ||||
|  | @ -49,11 +49,145 @@ static int task_finished(int result, | |||
| 	return 1; | ||||
| } | ||||
|  | ||||
| static uint64_t my_random_next = 1234; | ||||
|  | ||||
| static uint64_t my_random(void) | ||||
| { | ||||
| 	uint64_t res = my_random_next; | ||||
| 	my_random_next = my_random_next * 1103515245 + 12345; | ||||
| 	return res; | ||||
| } | ||||
|  | ||||
| static int quote_stress_test(int argc, const char **argv) | ||||
| { | ||||
| 	/* | ||||
| 	 * We are running a quote-stress test. | ||||
| 	 * spawn a subprocess that runs quote-stress with a | ||||
| 	 * special option that echoes back the arguments that | ||||
| 	 * were passed in. | ||||
| 	 */ | ||||
| 	char special[] = ".?*\\^_\"'`{}()[]<>@~&+:;$%"; // \t\r\n\a"; | ||||
| 	int i, j, k, trials = 100, skip = 0, msys2 = 0; | ||||
| 	struct strbuf out = STRBUF_INIT; | ||||
| 	struct argv_array args = ARGV_ARRAY_INIT; | ||||
| 	struct option options[] = { | ||||
| 		OPT_INTEGER('n', "trials", &trials, "Number of trials"), | ||||
| 		OPT_INTEGER('s', "skip", &skip, "Skip <n> trials"), | ||||
| 		OPT_BOOL('m', "msys2", &msys2, "Test quoting for MSYS2's sh"), | ||||
| 		OPT_END() | ||||
| 	}; | ||||
| 	const char * const usage[] = { | ||||
| 		"test-run-command quote-stress-test <options>", | ||||
| 		NULL | ||||
| 	}; | ||||
|  | ||||
| 	argc = parse_options(argc, argv, NULL, options, usage, 0); | ||||
|  | ||||
| 	setenv("MSYS_NO_PATHCONV", "1", 0); | ||||
|  | ||||
| 	for (i = 0; i < trials; i++) { | ||||
| 		struct child_process cp = CHILD_PROCESS_INIT; | ||||
| 		size_t arg_count, arg_offset; | ||||
| 		int ret = 0; | ||||
|  | ||||
| 		argv_array_clear(&args); | ||||
| 		if (msys2) | ||||
| 			argv_array_pushl(&args, "sh", "-c", | ||||
| 					 "printf %s\\\\0 \"$@\"", "skip", NULL); | ||||
| 		else | ||||
| 			argv_array_pushl(&args, "test-run-command", | ||||
| 					 "quote-echo", NULL); | ||||
| 		arg_offset = args.argc; | ||||
|  | ||||
| 		if (argc > 0) { | ||||
| 			trials = 1; | ||||
| 			arg_count = argc; | ||||
| 			for (j = 0; j < arg_count; j++) | ||||
| 				argv_array_push(&args, argv[j]); | ||||
| 		} else { | ||||
| 			arg_count = 1 + (my_random() % 5); | ||||
| 			for (j = 0; j < arg_count; j++) { | ||||
| 				char buf[20]; | ||||
| 				size_t min_len = 1; | ||||
| 				size_t arg_len = min_len + | ||||
| 					(my_random() % (ARRAY_SIZE(buf) - min_len)); | ||||
|  | ||||
| 				for (k = 0; k < arg_len; k++) | ||||
| 					buf[k] = special[my_random() % | ||||
| 						ARRAY_SIZE(special)]; | ||||
| 				buf[arg_len] = '\0'; | ||||
|  | ||||
| 				argv_array_push(&args, buf); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if (i < skip) | ||||
| 			continue; | ||||
|  | ||||
| 		cp.argv = args.argv; | ||||
| 		strbuf_reset(&out); | ||||
| 		if (pipe_command(&cp, NULL, 0, &out, 0, NULL, 0) < 0) | ||||
| 			return error("Failed to spawn child process"); | ||||
|  | ||||
| 		for (j = 0, k = 0; j < arg_count; j++) { | ||||
| 			const char *arg = args.argv[j + arg_offset]; | ||||
|  | ||||
| 			if (strcmp(arg, out.buf + k)) | ||||
| 				ret = error("incorrectly quoted arg: '%s', " | ||||
| 					    "echoed back as '%s'", | ||||
| 					     arg, out.buf + k); | ||||
| 			k += strlen(out.buf + k) + 1; | ||||
| 		} | ||||
|  | ||||
| 		if (k != out.len) | ||||
| 			ret = error("got %d bytes, but consumed only %d", | ||||
| 				     (int)out.len, (int)k); | ||||
|  | ||||
| 		if (ret) { | ||||
| 			fprintf(stderr, "Trial #%d failed. Arguments:\n", i); | ||||
| 			for (j = 0; j < arg_count; j++) | ||||
| 				fprintf(stderr, "arg #%d: '%s'\n", | ||||
| 					(int)j, args.argv[j + arg_offset]); | ||||
|  | ||||
| 			strbuf_release(&out); | ||||
| 			argv_array_clear(&args); | ||||
|  | ||||
| 			return ret; | ||||
| 		} | ||||
|  | ||||
| 		if (i && (i % 100) == 0) | ||||
| 			fprintf(stderr, "Trials completed: %d\n", (int)i); | ||||
| 	} | ||||
|  | ||||
| 	strbuf_release(&out); | ||||
| 	argv_array_clear(&args); | ||||
|  | ||||
| 	return 0; | ||||
| } | ||||
|  | ||||
| static int quote_echo(int argc, const char **argv) | ||||
| { | ||||
| 	while (argc > 1) { | ||||
| 		fwrite(argv[1], strlen(argv[1]), 1, stdout); | ||||
| 		fputc('\0', stdout); | ||||
| 		argv++; | ||||
| 		argc--; | ||||
| 	} | ||||
|  | ||||
| 	return 0; | ||||
| } | ||||
|  | ||||
| int cmd_main(int argc, const char **argv) | ||||
| { | ||||
| 	struct child_process proc = CHILD_PROCESS_INIT; | ||||
| 	int jobs; | ||||
|  | ||||
| 	if (argc >= 2 && !strcmp(argv[1], "quote-stress-test")) | ||||
| 		return !!quote_stress_test(argc - 1, argv + 1); | ||||
|  | ||||
| 	if (argc >= 2 && !strcmp(argv[1], "quote-echo")) | ||||
| 		return !!quote_echo(argc - 1, argv + 1); | ||||
|  | ||||
| 	if (argc < 3) | ||||
| 		return 1; | ||||
| 	proc.argv = (const char **)argv + 2; | ||||
|  |  | |||
|  | @ -165,6 +165,15 @@ test_expect_success 'absolute path rejects the empty string' ' | |||
| 	test_must_fail test-path-utils absolute_path "" | ||||
| ' | ||||
|  | ||||
| test_expect_success MINGW '<drive-letter>:\\abc is an absolute path' ' | ||||
| 	for letter in : \" C Z 1 ä | ||||
| 	do | ||||
| 		path=$letter:\\abc && | ||||
| 		absolute="$(test-path-utils absolute_path "$path")" && | ||||
| 		test "$path" = "$absolute" || return 1 | ||||
| 	done | ||||
| ' | ||||
|  | ||||
| test_expect_success 'real path rejects the empty string' ' | ||||
| 	test_must_fail test-path-utils real_path "" | ||||
| ' | ||||
|  | @ -408,6 +417,9 @@ test_expect_success 'match .gitmodules' ' | |||
| 		~1000000 \ | ||||
| 		~9999999 \ | ||||
| 		\ | ||||
| 		.gitmodules:\$DATA \ | ||||
| 		"gitmod~4 . :\$DATA" \ | ||||
| 		\ | ||||
| 		--not \ | ||||
| 		".gitmodules x"  \ | ||||
| 		".gitmodules .x" \ | ||||
|  | @ -432,7 +444,25 @@ test_expect_success 'match .gitmodules' ' | |||
| 		\ | ||||
| 		GI7EB~1 \ | ||||
| 		GI7EB~01 \ | ||||
| 		GI7EB~1X | ||||
| 		GI7EB~1X \ | ||||
| 		\ | ||||
| 		.gitmodules,:\$DATA | ||||
| ' | ||||
|  | ||||
| test_expect_success MINGW 'is_valid_path() on Windows' ' | ||||
|        test-path-utils is_valid_path \ | ||||
| 		win32 \ | ||||
| 		"win32 x" \ | ||||
| 		../hello.txt \ | ||||
| 		C:\\git \ | ||||
| 		\ | ||||
| 		--not \ | ||||
| 		"win32 "  \ | ||||
| 		"win32 /x "  \ | ||||
| 		"win32."  \ | ||||
| 		"win32 . ." \ | ||||
| 		.../hello.txt \ | ||||
| 		colon:test | ||||
| ' | ||||
|  | ||||
| test_done | ||||
|  |  | |||
|  | @ -49,6 +49,7 @@ git~1 | |||
| .git.SPACE .git.{space} | ||||
| .\\\\.GIT\\\\foobar backslashes | ||||
| .git\\\\foobar backslashes2 | ||||
| .git...:alternate-stream | ||||
| EOF | ||||
|  | ||||
| test_expect_success 'utf-8 paths allowed with core.protectHFS off' ' | ||||
|  |  | |||
|  | @ -419,6 +419,7 @@ while read name path pretty; do | |||
| 		( | ||||
| 			git init $name-$type && | ||||
| 			cd $name-$type && | ||||
| 			git config core.protectNTFS false && | ||||
| 			echo content >file && | ||||
| 			git add file && | ||||
| 			git commit -m base && | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ test_expect_success 'create commits with glob characters' ' | |||
| 	# the name "f*" in the worktree, because it is not allowed | ||||
| 	# on Windows (the tests below do not depend on the presence | ||||
| 	# of the file in the worktree) | ||||
| 	git config core.protectNTFS false && | ||||
| 	git update-index --add --cacheinfo 100644 "$(git rev-parse HEAD:foo)" "f*" && | ||||
| 	test_tick && | ||||
| 	git commit -m star && | ||||
|  |  | |||
|  | @ -73,4 +73,60 @@ test_expect_success 'clone evil superproject' ' | |||
| 	! grep "RUNNING POST CHECKOUT" output | ||||
| ' | ||||
|  | ||||
| test_expect_success MINGW 'prevent git~1 squatting on Windows' ' | ||||
| 	git init squatting && | ||||
| 	( | ||||
| 		cd squatting && | ||||
| 		mkdir a && | ||||
| 		touch a/..git && | ||||
| 		git add a/..git && | ||||
| 		test_tick && | ||||
| 		git commit -m initial && | ||||
|  | ||||
| 		modules="$(test_write_lines \ | ||||
| 			"[submodule \"b.\"]" "url = ." "path = c" \ | ||||
| 			"[submodule \"b\"]" "url = ." "path = d\\\\a" | | ||||
| 			git hash-object -w --stdin)" && | ||||
| 		rev="$(git rev-parse --verify HEAD)" && | ||||
| 		hash="$(echo x | git hash-object -w --stdin)" && | ||||
| 		git -c core.protectNTFS=false update-index --add \ | ||||
| 			--cacheinfo 100644,$modules,.gitmodules \ | ||||
| 			--cacheinfo 160000,$rev,c \ | ||||
| 			--cacheinfo 160000,$rev,d\\a \ | ||||
| 			--cacheinfo 100644,$hash,d./a/x \ | ||||
| 			--cacheinfo 100644,$hash,d./a/..git && | ||||
| 		test_tick && | ||||
| 		git -c core.protectNTFS=false commit -m "module" && | ||||
| 		test_must_fail git show HEAD: 2>err && | ||||
| 		test_i18ngrep backslash err | ||||
| 	) && | ||||
| 	test_must_fail git -c core.protectNTFS=false \ | ||||
| 		clone --recurse-submodules squatting squatting-clone 2>err && | ||||
| 	test_i18ngrep -e "directory not empty" -e "not an empty directory" err && | ||||
| 	! grep gitdir squatting-clone/d/a/git~2 | ||||
| ' | ||||
|  | ||||
| test_expect_success 'git dirs of sibling submodules must not be nested' ' | ||||
| 	git init nested && | ||||
| 	test_commit -C nested nested && | ||||
| 	( | ||||
| 		cd nested && | ||||
| 		cat >.gitmodules <<-EOF && | ||||
| 		[submodule "hippo"] | ||||
| 			url = . | ||||
| 			path = thing1 | ||||
| 		[submodule "hippo/hooks"] | ||||
| 			url = . | ||||
| 			path = thing2 | ||||
| 		EOF | ||||
| 		git clone . thing1 && | ||||
| 		git clone . thing2 && | ||||
| 		git add .gitmodules thing1 thing2 && | ||||
| 		test_tick && | ||||
| 		git commit -m nested | ||||
| 	) && | ||||
| 	test_must_fail git clone --recurse-submodules nested clone 2>err && | ||||
| 	test_i18ngrep "is inside git dir" err | ||||
| ' | ||||
|  | ||||
| test_done | ||||
|  |  | |||
|  | @ -31,4 +31,18 @@ test_expect_success 'clone rejects unprotected dash' ' | |||
| 	test_i18ngrep ignoring err | ||||
| ' | ||||
|  | ||||
| test_expect_success 'trailing backslash is handled correctly' ' | ||||
| 	git init testmodule && | ||||
| 	test_commit -C testmodule c && | ||||
| 	git submodule add ./testmodule && | ||||
| 	: ensure that the name ends in a double backslash && | ||||
| 	sed -e "s|\\(submodule \"testmodule\\)\"|\\1\\\\\\\\\"|" \ | ||||
| 		-e "s|url = .*|url = \" --should-not-be-an-option\"|" \ | ||||
| 		<.gitmodules >.new && | ||||
| 	mv .new .gitmodules && | ||||
| 	git commit -am "Add testmodule" && | ||||
| 	test_must_fail git clone --verbose --recurse-submodules . dolly 2>err && | ||||
| 	test_i18ngrep ! "unknown option" err | ||||
| ' | ||||
|  | ||||
| test_done | ||||
|  |  | |||
|  | @ -17,4 +17,21 @@ test_expect_success 'clone rejects unprotected dash' ' | |||
| 	test_i18ngrep ignoring err | ||||
| ' | ||||
|  | ||||
| test_expect_success MINGW 'submodule paths disallows trailing spaces' ' | ||||
| 	git init super && | ||||
| 	test_must_fail git -C super submodule add ../upstream "sub " && | ||||
|  | ||||
| 	: add "sub", then rename "sub" to "sub ", the hard way && | ||||
| 	git -C super submodule add ../upstream sub && | ||||
| 	tree=$(git -C super write-tree) && | ||||
| 	git -C super ls-tree $tree >tree && | ||||
| 	sed "s/sub/sub /" <tree >tree.new && | ||||
| 	tree=$(git -C super mktree <tree.new) && | ||||
| 	commit=$(echo with space | git -C super commit-tree $tree) && | ||||
| 	git -C super update-ref refs/heads/master $commit && | ||||
|  | ||||
| 	test_must_fail git clone --recurse-submodules super dst 2>err && | ||||
| 	test_i18ngrep "sub " err | ||||
| ' | ||||
|  | ||||
| test_done | ||||
|  |  | |||
|  | @ -2106,12 +2106,27 @@ test_expect_success 'R: abort on receiving feature after data command' ' | |||
| 	test_must_fail git fast-import <input | ||||
| ' | ||||
|  | ||||
| test_expect_success 'R: import-marks features forbidden by default' ' | ||||
| 	>git.marks && | ||||
| 	echo "feature import-marks=git.marks" >input && | ||||
| 	test_must_fail git fast-import <input && | ||||
| 	echo "feature import-marks-if-exists=git.marks" >input && | ||||
| 	test_must_fail git fast-import <input | ||||
| ' | ||||
|  | ||||
| test_expect_success 'R: only one import-marks feature allowed per stream' ' | ||||
| 	>git.marks && | ||||
| 	>git2.marks && | ||||
| 	cat >input <<-EOF && | ||||
| 	feature import-marks=git.marks | ||||
| 	feature import-marks=git2.marks | ||||
| 	EOF | ||||
|  | ||||
| 	test_must_fail git fast-import --allow-unsafe-features <input | ||||
| ' | ||||
|  | ||||
| test_expect_success 'R: export-marks feature forbidden by default' ' | ||||
| 	echo "feature export-marks=git.marks" >input && | ||||
| 	test_must_fail git fast-import <input | ||||
| ' | ||||
|  | ||||
|  | @ -2125,19 +2140,29 @@ test_expect_success 'R: export-marks feature results in a marks file being creat | |||
|  | ||||
| 	EOF | ||||
|  | ||||
| 	cat input | git fast-import && | ||||
| 	git fast-import --allow-unsafe-features <input && | ||||
| 	grep :1 git.marks | ||||
| ' | ||||
|  | ||||
| test_expect_success 'R: export-marks options can be overridden by commandline options' ' | ||||
| 	cat input | git fast-import --export-marks=other.marks && | ||||
| 	grep :1 other.marks | ||||
| 	cat >input <<-\EOF && | ||||
| 	feature export-marks=feature-sub/git.marks | ||||
| 	blob | ||||
| 	mark :1 | ||||
| 	data 3 | ||||
| 	hi | ||||
|  | ||||
| 	EOF | ||||
| 	git fast-import --allow-unsafe-features \ | ||||
| 			--export-marks=cmdline-sub/other.marks <input && | ||||
| 	grep :1 cmdline-sub/other.marks && | ||||
| 	test_path_is_missing feature-sub | ||||
| ' | ||||
|  | ||||
| test_expect_success 'R: catch typo in marks file name' ' | ||||
| 	test_must_fail git fast-import --import-marks=nonexistent.marks </dev/null && | ||||
| 	echo "feature import-marks=nonexistent.marks" | | ||||
| 	test_must_fail git fast-import | ||||
| 	test_must_fail git fast-import --allow-unsafe-features | ||||
| ' | ||||
|  | ||||
| test_expect_success 'R: import and output marks can be the same file' ' | ||||
|  | @ -2193,7 +2218,8 @@ test_expect_success 'R: feature import-marks-if-exists' ' | |||
| 	rm -f io.marks && | ||||
| 	>expect && | ||||
|  | ||||
| 	git fast-import --export-marks=io.marks <<-\EOF && | ||||
| 	git fast-import --export-marks=io.marks \ | ||||
| 			--allow-unsafe-features <<-\EOF && | ||||
| 	feature import-marks-if-exists=not_io.marks | ||||
| 	EOF | ||||
| 	test_cmp expect io.marks && | ||||
|  | @ -2204,7 +2230,8 @@ test_expect_success 'R: feature import-marks-if-exists' ' | |||
| 	echo ":1 $blob" >expect && | ||||
| 	echo ":2 $blob" >>expect && | ||||
|  | ||||
| 	git fast-import --export-marks=io.marks <<-\EOF && | ||||
| 	git fast-import --export-marks=io.marks \ | ||||
| 			--allow-unsafe-features <<-\EOF && | ||||
| 	feature import-marks-if-exists=io.marks | ||||
| 	blob | ||||
| 	mark :2 | ||||
|  | @ -2217,7 +2244,8 @@ test_expect_success 'R: feature import-marks-if-exists' ' | |||
| 	echo ":3 $blob" >>expect && | ||||
|  | ||||
| 	git fast-import --import-marks=io.marks \ | ||||
| 			--export-marks=io.marks <<-\EOF && | ||||
| 			--export-marks=io.marks \ | ||||
| 			--allow-unsafe-features <<-\EOF && | ||||
| 	feature import-marks-if-exists=not_io.marks | ||||
| 	blob | ||||
| 	mark :3 | ||||
|  | @ -2230,7 +2258,8 @@ test_expect_success 'R: feature import-marks-if-exists' ' | |||
| 	>expect && | ||||
|  | ||||
| 	git fast-import --import-marks-if-exists=not_io.marks \ | ||||
| 			--export-marks=io.marks <<-\EOF && | ||||
| 			--export-marks=io.marks \ | ||||
| 			--allow-unsafe-features <<-\EOF && | ||||
| 	feature import-marks-if-exists=io.marks | ||||
| 	EOF | ||||
| 	test_cmp expect io.marks | ||||
|  | @ -2242,7 +2271,7 @@ test_expect_success 'R: import to output marks works without any content' ' | |||
| 	feature export-marks=marks.new | ||||
| 	EOF | ||||
|  | ||||
| 	cat input | git fast-import && | ||||
| 	git fast-import --allow-unsafe-features <input && | ||||
| 	test_cmp marks.out marks.new | ||||
| ' | ||||
|  | ||||
|  | @ -2252,7 +2281,7 @@ test_expect_success 'R: import marks prefers commandline marks file over the str | |||
| 	feature export-marks=marks.new | ||||
| 	EOF | ||||
|  | ||||
| 	cat input | git fast-import --import-marks=marks.out && | ||||
| 	git fast-import --import-marks=marks.out --allow-unsafe-features <input && | ||||
| 	test_cmp marks.out marks.new | ||||
| ' | ||||
|  | ||||
|  | @ -2265,7 +2294,8 @@ test_expect_success 'R: multiple --import-marks= should be honoured' ' | |||
|  | ||||
| 	head -n2 marks.out > one.marks && | ||||
| 	tail -n +3 marks.out > two.marks && | ||||
| 	git fast-import --import-marks=one.marks --import-marks=two.marks <input && | ||||
| 	git fast-import --import-marks=one.marks --import-marks=two.marks \ | ||||
| 		--allow-unsafe-features <input && | ||||
| 	test_cmp marks.out combined.marks | ||||
| ' | ||||
|  | ||||
|  | @ -2278,7 +2308,7 @@ test_expect_success 'R: feature relative-marks should be honoured' ' | |||
|  | ||||
| 	mkdir -p .git/info/fast-import/ && | ||||
| 	cp marks.new .git/info/fast-import/relative.in && | ||||
| 	git fast-import <input && | ||||
| 	git fast-import --allow-unsafe-features <input && | ||||
| 	test_cmp marks.new .git/info/fast-import/relative.out | ||||
| ' | ||||
|  | ||||
|  | @ -2290,7 +2320,7 @@ test_expect_success 'R: feature no-relative-marks should be honoured' ' | |||
| 	feature export-marks=non-relative.out | ||||
| 	EOF | ||||
|  | ||||
| 	git fast-import <input && | ||||
| 	git fast-import --allow-unsafe-features <input && | ||||
| 	test_cmp marks.new non-relative.out | ||||
| ' | ||||
|  | ||||
|  | @ -2560,7 +2590,7 @@ test_expect_success 'R: quiet option results in no stats being output' ' | |||
|  | ||||
| 	EOF | ||||
|  | ||||
| 	cat input | git fast-import 2> output && | ||||
| 	git fast-import 2>output <input && | ||||
| 	test_must_be_empty output | ||||
| ' | ||||
|  | ||||
|  |  | |||
|  | @ -421,9 +421,10 @@ test_expect_success 'directory becomes symlink'        ' | |||
|  | ||||
| test_expect_success 'fast-export quotes pathnames' ' | ||||
| 	git init crazy-paths && | ||||
| 	test_config -C crazy-paths core.protectNTFS false && | ||||
| 	(cd crazy-paths && | ||||
| 	 blob=$(echo foo | git hash-object -w --stdin) && | ||||
| 	 git update-index --add \ | ||||
| 	 git -c core.protectNTFS=false update-index --add \ | ||||
| 		--cacheinfo 100644 $blob "$(printf "path with\\nnewline")" \ | ||||
| 		--cacheinfo 100644 $blob "path with \"quote\"" \ | ||||
| 		--cacheinfo 100644 $blob "path with \\backslash" \ | ||||
|  |  | |||
|  | @ -431,6 +431,7 @@ static int get_importer(struct transport *transport, struct child_process *fasti | |||
| 	child_process_init(fastimport); | ||||
| 	fastimport->in = helper->out; | ||||
| 	argv_array_push(&fastimport->args, "fast-import"); | ||||
| 	argv_array_push(&fastimport->args, "--allow-unsafe-features"); | ||||
| 	argv_array_push(&fastimport->args, debug ? "--stats" : "--quiet"); | ||||
|  | ||||
| 	if (data->bidi_import) { | ||||
|  |  | |||
|  | @ -41,6 +41,12 @@ static int decode_tree_entry(struct tree_desc *desc, const char *buf, unsigned l | |||
| 		strbuf_addstr(err, _("empty filename in tree entry")); | ||||
| 		return -1; | ||||
| 	} | ||||
| #ifdef GIT_WINDOWS_NATIVE | ||||
| 	if (protect_ntfs && strchr(path, '\\')) { | ||||
| 		strbuf_addf(err, _("filename in tree entry contains backslash: '%s'"), path); | ||||
| 		return -1; | ||||
| 	} | ||||
| #endif | ||||
| 	len = strlen(path) + 1; | ||||
|  | ||||
| 	/* Initialize the descriptor entry */ | ||||
|  |  | |||
|  | @ -1809,7 +1809,8 @@ static int merged_entry(const struct cache_entry *ce, | |||
| 		invalidate_ce_path(old, o); | ||||
| 	} | ||||
|  | ||||
| 	do_add_entry(o, merge, update, CE_STAGEMASK); | ||||
| 	if (do_add_entry(o, merge, update, CE_STAGEMASK) < 0) | ||||
| 		return -1; | ||||
| 	return 1; | ||||
| } | ||||
|  | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Johannes Schindelin
						Johannes Schindelin