Blame SOURCES/git-1.9.4-cve9390.patch

e5a527
diff --git a/Documentation/config.txt b/Documentation/config.txt
e5a527
index c26a7c8..7076aa9 100644
e5a527
--- a/Documentation/config.txt
e5a527
+++ b/Documentation/config.txt
e5a527
@@ -234,6 +234,17 @@ core.precomposeunicode::
e5a527
 	When false, file names are handled fully transparent by Git,
e5a527
 	which is backward compatible with older versions of Git.
e5a527
 
e5a527
+core.protectHFS::
e5a527
+	If set to true, do not allow checkout of paths that would
e5a527
+	be considered equivalent to `.git` on an HFS+ filesystem.
e5a527
+	Defaults to `true` on Mac OS, and `false` elsewhere.
e5a527
+
e5a527
+core.protectNTFS::
e5a527
+	If set to true, do not allow checkout of paths that would
e5a527
+	cause problems with the NTFS filesystem, e.g. conflict with
e5a527
+	8.3 "short" names.
e5a527
+	Defaults to `true` on Windows, and `false` elsewhere.
e5a527
+
e5a527
 core.trustctime::
e5a527
 	If false, the ctime differences between the index and the
e5a527
 	working tree are ignored; useful when the inode change time
e5a527
--- a/builtin/annotate.c
e5a527
+++ b/builtin/annotate.c
e5a527
@@ -5,20 +5,18 @@
e5a527
  */
e5a527
 #include "git-compat-util.h"
e5a527
 #include "builtin.h"
e5a527
+#include "argv-array.h"
e5a527
 
e5a527
 int cmd_annotate(int argc, const char **argv, const char *prefix)
e5a527
 {
e5a527
-	const char **nargv;
e5a527
+	struct argv_array args = ARGV_ARRAY_INIT;
e5a527
 	int i;
e5a527
-	nargv = xmalloc(sizeof(char *) * (argc + 2));
e5a527
 
e5a527
-	nargv[0] = "annotate";
e5a527
-	nargv[1] = "-c";
e5a527
+	argv_array_pushl(&args, "annotate", "-c", NULL);
e5a527
 
e5a527
 	for (i = 1; i < argc; i++) {
e5a527
-		nargv[i+1] = argv[i];
e5a527
+		argv_array_push(&args, argv[i]);
e5a527
 	}
e5a527
-	nargv[argc + 1] = NULL;
e5a527
 
e5a527
-	return cmd_blame(argc + 1, nargv, prefix);
e5a527
+	return cmd_blame(args.argc, args.argv, prefix);
e5a527
 }
e5a527
diff --git a/builtin/clean.c b/builtin/clean.c
e5a527
index 977a068..8926ca8 100644
e5a527
--- a/builtin/clean.c
e5a527
+++ b/builtin/clean.c
e5a527
@@ -48,7 +48,7 @@ enum color_clean {
e5a527
 	CLEAN_COLOR_PROMPT = 2,
e5a527
 	CLEAN_COLOR_HEADER = 3,
e5a527
 	CLEAN_COLOR_HELP = 4,
e5a527
-	CLEAN_COLOR_ERROR = 5,
e5a527
+	CLEAN_COLOR_ERROR = 5
e5a527
 };
e5a527
 
e5a527
 #define MENU_OPTS_SINGLETON		01
e5a527
diff --git a/cache.h b/cache.h
e5a527
index ebe9a40..017c487 100644
e5a527
--- a/cache.h
e5a527
+++ b/cache.h
e5a527
@@ -587,6 +587,8 @@ extern int fsync_object_files;
e5a527
 extern int core_preload_index;
e5a527
 extern int core_apply_sparse_checkout;
e5a527
 extern int precomposed_unicode;
e5a527
+extern int protect_hfs;
e5a527
+extern int protect_ntfs;
e5a527
 
e5a527
 /*
e5a527
  * The character that begins a commented line in user-editable file
e5a527
@@ -782,6 +784,7 @@ int longest_ancestor_length(const char *path, struct string_list *prefixes);
e5a527
 char *strip_path_suffix(const char *path, const char *suffix);
e5a527
 int daemon_avoid_alias(const char *path);
e5a527
 int offset_1st_component(const char *path);
e5a527
+extern int is_ntfs_dotgit(const char *name);
e5a527
 
e5a527
 /* object replacement */
e5a527
 #define LOOKUP_REPLACE_OBJECT 1
e5a527
diff --git a/config.c b/config.c
e5a527
index 314d8ee..8984ad2 100644
e5a527
--- a/config.c
e5a527
+++ b/config.c
e5a527
@@ -885,6 +885,16 @@ static int git_default_core_config(const char *var, const char *value)
e5a527
 		return 0;
e5a527
 	}
e5a527
 
e5a527
+	if (!strcmp(var, "core.protecthfs")) {
e5a527
+		protect_hfs = git_config_bool(var, value);
e5a527
+		return 0;
e5a527
+	}
e5a527
+
e5a527
+	if (!strcmp(var, "core.protectntfs")) {
e5a527
+		protect_ntfs = git_config_bool(var, value);
e5a527
+		return 0;
e5a527
+	}
e5a527
+
e5a527
 	/* Add other config variables here and to Documentation/config.txt. */
e5a527
 	return 0;
e5a527
 }
e5a527
diff --git a/config.mak.uname b/config.mak.uname
e5a527
index efaed94..f3cdcbc 100644
e5a527
--- a/config.mak.uname
e5a527
+++ b/config.mak.uname
e5a527
@@ -97,6 +97,7 @@ ifeq ($(uname_S),Darwin)
e5a527
 	HAVE_DEV_TTY = YesPlease
e5a527
 	COMPAT_OBJS += compat/precompose_utf8.o
e5a527
 	BASIC_CFLAGS += -DPRECOMPOSE_UNICODE
e5a527
+	BASIC_CFLAGS += -DPROTECT_HFS_DEFAULT=1
e5a527
 endif
e5a527
 ifeq ($(uname_S),SunOS)
e5a527
 	NEEDS_SOCKET = YesPlease
e5a527
@@ -369,6 +370,7 @@ ifeq ($(uname_S),Windows)
e5a527
 	EXTLIBS = user32.lib advapi32.lib shell32.lib wininet.lib ws2_32.lib
e5a527
 	PTHREAD_LIBS =
e5a527
 	lib =
e5a527
+	BASIC_CFLAGS += -DPROTECT_NTFS_DEFAULT=1
e5a527
 ifndef DEBUG
e5a527
 	BASIC_CFLAGS += -GL -Os -MT
e5a527
 	BASIC_LDFLAGS += -LTCG
e5a527
@@ -513,6 +515,7 @@ ifneq (,$(findstring MINGW,$(uname_S)))
e5a527
 	COMPAT_OBJS += compat/mingw.o compat/winansi.o \
e5a527
 		compat/win32/pthread.o compat/win32/syslog.o \
e5a527
 		compat/win32/dirent.o
e5a527
+	BASIC_CFLAGS += -DPROTECT_NTFS_DEFAULT=1
e5a527
 	BASIC_LDFLAGS += -Wl,--large-address-aware
e5a527
 	EXTLIBS += -lws2_32
e5a527
 	GITLIBS += git.res
e5a527
diff --git a/environment.c b/environment.c
e5a527
index 4a3437d..39a8c6c 100644
e5a527
--- a/environment.c
e5a527
+++ b/environment.c
e5a527
@@ -64,6 +64,16 @@ int precomposed_unicode = -1; /* see probe_utf8_pathname_composition() */
e5a527
 struct startup_info *startup_info;
e5a527
 unsigned long pack_size_limit_cfg;
e5a527
 
e5a527
+#ifndef PROTECT_HFS_DEFAULT
e5a527
+#define PROTECT_HFS_DEFAULT 0
e5a527
+#endif
e5a527
+int protect_hfs = PROTECT_HFS_DEFAULT;
e5a527
+
e5a527
+#ifndef PROTECT_NTFS_DEFAULT
e5a527
+#define PROTECT_NTFS_DEFAULT 0
e5a527
+#endif
e5a527
+int protect_ntfs = PROTECT_NTFS_DEFAULT;
e5a527
+
e5a527
 /*
e5a527
  * The character that begins a commented line in user-editable file
e5a527
  * that is subject to stripspace.
e5a527
diff --git a/fsck.c b/fsck.c
e5a527
index 64bf279..ee7f531 100644
e5a527
--- a/fsck.c
e5a527
+++ b/fsck.c
e5a527
@@ -6,6 +6,7 @@
e5a527
 #include "commit.h"
e5a527
 #include "tag.h"
e5a527
 #include "fsck.h"
e5a527
+#include "utf8.h"
e5a527
 
e5a527
 static int fsck_walk_tree(struct tree *tree, fsck_walk_func walk, void *data)
e5a527
 {
e5a527
@@ -175,7 +176,8 @@ static int fsck_tree(struct tree *item, int strict, fsck_error error_func)
e5a527
 			has_dot = 1;
e5a527
 		if (!strcmp(name, ".."))
e5a527
 			has_dotdot = 1;
e5a527
-		if (!strcmp(name, ".git"))
e5a527
+		if (!strcasecmp(name, ".git") || is_hfs_dotgit(name) ||
e5a527
+				is_ntfs_dotgit(name))
e5a527
 			has_dotgit = 1;
e5a527
 		has_zero_pad |= *(char *)desc.buffer == '0';
e5a527
 		update_tree_entry(&desc);
e5a527
diff --git a/path.c b/path.c
e5a527
index f9c5062..dfd58f4 100644
e5a527
--- a/path.c
e5a527
+++ b/path.c
e5a527
@@ -830,3 +830,36 @@ int offset_1st_component(const char *path)
e5a527
 		return 2 + is_dir_sep(path[2]);
e5a527
 	return is_dir_sep(path[0]);
e5a527
 }
e5a527
+
e5a527
+static int only_spaces_and_periods(const char *path, size_t len, size_t skip)
e5a527
+{
e5a527
+	if (len < skip)
e5a527
+		return 0;
e5a527
+	len -= skip;
e5a527
+	path += skip;
e5a527
+	while (len-- > 0) {
e5a527
+		char c = *(path++);
e5a527
+		if (c != ' ' && c != '.')
e5a527
+			return 0;
e5a527
+	}
e5a527
+	return 1;
e5a527
+}
e5a527
+
e5a527
+int is_ntfs_dotgit(const char *name)
e5a527
+{
e5a527
+	int len;
e5a527
+
e5a527
+	for (len = 0; ; len++)
e5a527
+		if (!name[len] || name[len] == '\\' || is_dir_sep(name[len])) {
e5a527
+			if (only_spaces_and_periods(name, len, 4) &&
e5a527
+					!strncasecmp(name, ".git", 4))
e5a527
+				return 1;
e5a527
+			if (only_spaces_and_periods(name, len, 5) &&
e5a527
+					!strncasecmp(name, "git~1", 5))
e5a527
+				return 1;
e5a527
+			if (name[len] != '\\')
e5a527
+				return 0;
e5a527
+			name += len + 1;
e5a527
+			len = -1;
e5a527
+		}
e5a527
+}
e5a527
diff --git a/pretty.c b/pretty.c
e5a527
index 6e266dd..f64ff9a 100644
e5a527
--- a/pretty.c
e5a527
+++ b/pretty.c
e5a527
@@ -274,7 +274,7 @@ static void add_rfc822_quoted(struct strbuf *out, const char *s, int len)
e5a527
 
e5a527
 enum rfc2047_type {
e5a527
 	RFC2047_SUBJECT,
e5a527
-	RFC2047_ADDRESS,
e5a527
+	RFC2047_ADDRESS
e5a527
 };
e5a527
 
e5a527
 static int is_rfc2047_special(char ch, enum rfc2047_type type)
e5a527
diff --git a/read-cache.c b/read-cache.c
e5a527
index 4b4effd..ee07cd6 100644
e5a527
--- a/read-cache.c
e5a527
+++ b/read-cache.c
e5a527
@@ -14,6 +14,7 @@
e5a527
 #include "resolve-undo.h"
e5a527
 #include "strbuf.h"
e5a527
 #include "varint.h"
e5a527
+#include "utf8.h"
e5a527
 
e5a527
 static struct cache_entry *refresh_cache_entry(struct cache_entry *ce,
e5a527
 					       unsigned int options);
e5a527
@@ -752,9 +753,10 @@ static int verify_dotfile(const char *rest)
e5a527
 	 * shares the path end test with the ".." case.
e5a527
 	 */
e5a527
 	case 'g':
e5a527
-		if (rest[1] != 'i')
e5a527
+	case 'G':
e5a527
+		if (rest[1] != 'i' && rest[1] != 'I')
e5a527
 			break;
e5a527
-		if (rest[2] != 't')
e5a527
+		if (rest[2] != 't' && rest[2] != 'T')
e5a527
 			break;
e5a527
 		rest += 2;
e5a527
 	/* fallthrough */
e5a527
@@ -778,6 +780,10 @@ int verify_path(const char *path)
e5a527
 			return 1;
e5a527
 		if (is_dir_sep(c)) {
e5a527
 inside:
e5a527
+			if (protect_hfs && is_hfs_dotgit(path))
e5a527
+				return 0;
e5a527
+			if (protect_ntfs && is_ntfs_dotgit(path))
e5a527
+				return 0;
e5a527
 			c = *path++;
e5a527
 			if ((c == '.' && !verify_dotfile(path)) ||
e5a527
 			    is_dir_sep(c) || c == '\0')
e5a527
diff --git a/t/t1014-read-tree-confusing.sh b/t/t1014-read-tree-confusing.sh
e5a527
new file mode 100755
e5a527
index 0000000..2f5a25d
e5a527
--- /dev/null
e5a527
+++ b/t/t1014-read-tree-confusing.sh
e5a527
@@ -0,0 +1,62 @@
e5a527
+#!/bin/sh
e5a527
+
e5a527
+test_description='check that read-tree rejects confusing paths'
e5a527
+. ./test-lib.sh
e5a527
+
e5a527
+test_expect_success 'create base tree' '
e5a527
+	echo content >file &&
e5a527
+	git add file &&
e5a527
+	git commit -m base &&
e5a527
+	blob=$(git rev-parse HEAD:file) &&
e5a527
+	tree=$(git rev-parse HEAD^{tree})
e5a527
+'
e5a527
+
e5a527
+test_expect_success 'enable core.protectHFS for rejection tests' '
e5a527
+	git config core.protectHFS true
e5a527
+'
e5a527
+
e5a527
+test_expect_success 'enable core.protectNTFS for rejection tests' '
e5a527
+	git config core.protectNTFS true
e5a527
+'
e5a527
+
e5a527
+while read path pretty; do
e5a527
+	: ${pretty:=$path}
e5a527
+	case "$path" in
e5a527
+	*SPACE)
e5a527
+		path="${path%SPACE} "
e5a527
+		;;
e5a527
+	esac
e5a527
+	test_expect_success "reject $pretty at end of path" '
e5a527
+		printf "100644 blob %s\t%s" "$blob" "$path" >tree &&
e5a527
+		bogus=$(git mktree 
e5a527
+		test_must_fail git read-tree $bogus
e5a527
+	'
e5a527
+
e5a527
+	test_expect_success "reject $pretty as subtree" '
e5a527
+		printf "040000 tree %s\t%s" "$tree" "$path" >tree &&
e5a527
+		bogus=$(git mktree 
e5a527
+		test_must_fail git read-tree $bogus
e5a527
+	'
e5a527
+done <<-EOF
e5a527
+.
e5a527
+..
e5a527
+.git
e5a527
+.GIT
e5a527
+${u200c}.Git {u200c}.Git
e5a527
+.gI${u200c}T .gI{u200c}T
e5a527
+.GiT${u200c} .GiT{u200c}
e5a527
+git~1
e5a527
+.git.SPACE .git.{space}
e5a527
+.\\\\.GIT\\\\foobar backslashes
e5a527
+.git\\\\foobar backslashes2
e5a527
+EOF
e5a527
+
e5a527
+test_expect_success 'utf-8 paths allowed with core.protectHFS off' '
e5a527
+	test_when_finished "git read-tree HEAD" &&
e5a527
+	test_config core.protectHFS false &&
e5a527
+	printf "100644 blob %s\t%s" "$blob" ".gi${u200c}t" >tree &&
e5a527
+	ok=$(git mktree 
e5a527
+	git read-tree $ok
e5a527
+'
e5a527
+
e5a527
+test_done
e5a527
diff --git a/t/t1450-fsck.sh b/t/t1450-fsck.sh
e5a527
index 8c739c9..983568a 100755
e5a527
--- a/t/t1450-fsck.sh
e5a527
+++ b/t/t1450-fsck.sh
e5a527
@@ -251,35 +251,40 @@ test_expect_success 'fsck notices submodule entry pointing to null sha1' '
e5a527
 	)
e5a527
 '
e5a527
 
e5a527
-test_expect_success 'fsck notices "." and ".." in trees' '
e5a527
-	(
e5a527
-		git init dots &&
e5a527
-		cd dots &&
e5a527
-		blob=$(echo foo | git hash-object -w --stdin) &&
e5a527
-		tab=$(printf "\\t") &&
e5a527
-		git mktree <<-EOF &&
e5a527
-		100644 blob $blob$tab.
e5a527
-		100644 blob $blob$tab..
e5a527
-		EOF
e5a527
-		git fsck 2>out &&
e5a527
-		cat out &&
e5a527
-		grep "warning.*\\." out
e5a527
-	)
e5a527
-'
e5a527
-
e5a527
-test_expect_success 'fsck notices ".git" in trees' '
e5a527
-	(
e5a527
-		git init dotgit &&
e5a527
-		cd dotgit &&
e5a527
-		blob=$(echo foo | git hash-object -w --stdin) &&
e5a527
-		tab=$(printf "\\t") &&
e5a527
-		git mktree <<-EOF &&
e5a527
-		100644 blob $blob$tab.git
e5a527
-		EOF
e5a527
-		git fsck 2>out &&
e5a527
-		cat out &&
e5a527
-		grep "warning.*\\.git" out
e5a527
-	)
e5a527
-'
e5a527
+while read name path pretty; do
e5a527
+	while read mode type; do
e5a527
+		: ${pretty:=$path}
e5a527
+		test_expect_success "fsck notices $pretty as $type" '
e5a527
+		(
e5a527
+			git init $name-$type &&
e5a527
+			cd $name-$type &&
e5a527
+			echo content >file &&
e5a527
+			git add file &&
e5a527
+			git commit -m base &&
e5a527
+			blob=$(git rev-parse :file) &&
e5a527
+			tree=$(git rev-parse HEAD^{tree}) &&
e5a527
+			value=$(eval "echo \$$type") &&
e5a527
+			printf "$mode $type %s\t%s" "$value" "$path" >bad &&
e5a527
+			bad_tree=$(git mktree 
e5a527
+			git fsck 2>out &&
e5a527
+			cat out &&
e5a527
+			grep "warning.*tree $bad_tree" out
e5a527
+		)'
e5a527
+	done <<-\EOF
e5a527
+	100644 blob
e5a527
+	040000 tree
e5a527
+	EOF
e5a527
+done <<-EOF
e5a527
+dot .
e5a527
+dotdot ..
e5a527
+dotgit .git
e5a527
+dotgit-case .GIT
e5a527
+dotgit-unicode .gI${u200c}T .gI{u200c}T
e5a527
+dotgit-case2 .Git
e5a527
+git-tilde1 git~1
e5a527
+dotgitdot .git.
e5a527
+dot-backslash-case .\\\\.GIT\\\\foobar
e5a527
+dotgit-case-backslash .git\\\\foobar
e5a527
+EOF
e5a527
 
e5a527
 test_done
e5a527
diff --git a/t/t7300-clean.sh b/t/t7300-clean.sh
e5a527
index 74de814..04118ad 100755
e5a527
--- a/t/t7300-clean.sh
e5a527
+++ b/t/t7300-clean.sh
e5a527
@@ -426,10 +426,10 @@ test_expect_success SANITY 'removal failure' '
e5a527
 
e5a527
 	mkdir foo &&
e5a527
 	touch foo/bar &&
e5a527
+	test_when_finished "chmod 755 foo" &&
e5a527
 	(exec 
e5a527
 	 chmod 0 foo &&
e5a527
-	 test_must_fail git clean -f -d &&
e5a527
-	 chmod 755 foo)
e5a527
+	 test_must_fail git clean -f -d)
e5a527
 '
e5a527
 
e5a527
 test_expect_success 'nested git work tree' '
e5a527
diff --git a/t/test-lib.sh b/t/test-lib.sh
e5a527
index 3c7cb1d..afa411e 100644
e5a527
--- a/t/test-lib.sh
e5a527
+++ b/t/test-lib.sh
e5a527
@@ -158,7 +158,11 @@ _z40=0000000000000000000000000000000000000000
e5a527
 LF='
e5a527
 '
e5a527
 
e5a527
-export _x05 _x40 _z40 LF
e5a527
+# UTF-8 ZERO WIDTH NON-JOINER, which HFS+ ignores
e5a527
+# when case-folding filenames
e5a527
+u200c=$(printf '\342\200\214')
e5a527
+
e5a527
+export _x05 _x40 _z40 LF u200c
e5a527
 
e5a527
 # Each test should start with something like this, after copyright notices:
e5a527
 #
e5a527
diff --git a/unpack-trees.c b/unpack-trees.c
e5a527
index 164354d..ca7dd0f 100644
e5a527
--- a/unpack-trees.c
e5a527
+++ b/unpack-trees.c
e5a527
@@ -102,7 +102,7 @@ void setup_unpack_trees_porcelain(struct unpack_trees_options *opts,
e5a527
 		opts->unpack_rejects[i].strdup_strings = 1;
e5a527
 }
e5a527
 
e5a527
-static void do_add_entry(struct unpack_trees_options *o, struct cache_entry *ce,
e5a527
+static int do_add_entry(struct unpack_trees_options *o, struct cache_entry *ce,
e5a527
 			 unsigned int set, unsigned int clear)
e5a527
 {
e5a527
 	clear |= CE_HASHED | CE_UNHASHED;
e5a527
@@ -112,8 +112,8 @@ static void do_add_entry(struct unpack_trees_options *o, struct cache_entry *ce,
e5a527
 
e5a527
 	ce->next = NULL;
e5a527
 	ce->ce_flags = (ce->ce_flags & ~clear) | set;
e5a527
-	add_index_entry(&o->result, ce,
e5a527
-			ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE);
e5a527
+	return add_index_entry(&o->result, ce,
e5a527
+			       ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE);
e5a527
 }
e5a527
 
e5a527
 static struct cache_entry *dup_entry(const struct cache_entry *ce)
e5a527
@@ -608,7 +608,9 @@ static int unpack_nondirectories(int n, unsigned long mask,
e5a527
 
e5a527
 	for (i = 0; i < n; i++)
e5a527
 		if (src[i] && src[i] != o->df_conflict_entry)
e5a527
-			do_add_entry(o, src[i], 0, 0);
e5a527
+			if (do_add_entry(o, src[i], 0, 0))
e5a527
+				return -1;
e5a527
+
e5a527
 	return 0;
e5a527
 }
e5a527
 
e5a527
diff --git a/utf8.c b/utf8.c
e5a527
index 536a9c8..015c815 100644
e5a527
--- a/utf8.c
e5a527
+++ b/utf8.c
e5a527
@@ -627,3 +627,67 @@ int mbs_chrlen(const char **text, size_t *remainder_p, const char *encoding)
e5a527
 
e5a527
 	return chrlen;
e5a527
 }
e5a527
+
e5a527
+/*
e5a527
+ * Pick the next char from the stream, folding as an HFS+ filename comparison
e5a527
+ * would. Note that this is _not_ complete by any means. It's just enough
e5a527
+ * to make is_hfs_dotgit() work, and should not be used otherwise.
e5a527
+ */
e5a527
+static ucs_char_t next_hfs_char(const char **in)
e5a527
+{
e5a527
+	while (1) {
e5a527
+		ucs_char_t out = pick_one_utf8_char(in, NULL);
e5a527
+		/*
e5a527
+		 * check for malformed utf8. Technically this
e5a527
+		 * gets converted to a percent-sequence, but
e5a527
+		 * returning 0 is good enough for is_hfs_dotgit
e5a527
+		 * to realize it cannot be .git
e5a527
+		 */
e5a527
+		if (!*in)
e5a527
+			return 0;
e5a527
+
e5a527
+		/* these code points are ignored completely */
e5a527
+		switch (out) {
e5a527
+		case 0x200c: /* ZERO WIDTH NON-JOINER */
e5a527
+		case 0x200d: /* ZERO WIDTH JOINER */
e5a527
+		case 0x200e: /* LEFT-TO-RIGHT MARK */
e5a527
+		case 0x200f: /* RIGHT-TO-LEFT MARK */
e5a527
+		case 0x202a: /* LEFT-TO-RIGHT EMBEDDING */
e5a527
+		case 0x202b: /* RIGHT-TO-LEFT EMBEDDING */
e5a527
+		case 0x202c: /* POP DIRECTIONAL FORMATTING */
e5a527
+		case 0x202d: /* LEFT-TO-RIGHT OVERRIDE */
e5a527
+		case 0x202e: /* RIGHT-TO-LEFT OVERRIDE */
e5a527
+		case 0x206a: /* INHIBIT SYMMETRIC SWAPPING */
e5a527
+		case 0x206b: /* ACTIVATE SYMMETRIC SWAPPING */
e5a527
+		case 0x206c: /* INHIBIT ARABIC FORM SHAPING */
e5a527
+		case 0x206d: /* ACTIVATE ARABIC FORM SHAPING */
e5a527
+		case 0x206e: /* NATIONAL DIGIT SHAPES */
e5a527
+		case 0x206f: /* NOMINAL DIGIT SHAPES */
e5a527
+		case 0xfeff: /* ZERO WIDTH NO-BREAK SPACE */
e5a527
+			continue;
e5a527
+		}
e5a527
+
e5a527
+		/*
e5a527
+		 * there's a great deal of other case-folding that occurs,
e5a527
+		 * but this is enough to catch anything that will convert
e5a527
+		 * to ".git"
e5a527
+		 */
e5a527
+		return tolower(out);
e5a527
+	}
e5a527
+}
e5a527
+
e5a527
+int is_hfs_dotgit(const char *path)
e5a527
+{
e5a527
+	ucs_char_t c;
e5a527
+
e5a527
+	if (next_hfs_char(&path) != '.' ||
e5a527
+	    next_hfs_char(&path) != 'g' ||
e5a527
+	    next_hfs_char(&path) != 'i' ||
e5a527
+	    next_hfs_char(&path) != 't')
e5a527
+		return 0;
e5a527
+	c = next_hfs_char(&path);
e5a527
+	if (c && !is_dir_sep(c))
e5a527
+		return 0;
e5a527
+
e5a527
+	return 1;
e5a527
+}
e5a527
diff --git a/utf8.h b/utf8.h
e5a527
index 65d0e42..e4d9183 100644
e5a527
--- a/utf8.h
e5a527
+++ b/utf8.h
e5a527
@@ -42,4 +42,12 @@ static inline char *reencode_string(const char *in,
e5a527
 
e5a527
 int mbs_chrlen(const char **text, size_t *remainder_p, const char *encoding);
e5a527
 
e5a527
+/*
e5a527
+ * Returns true if the the path would match ".git" after HFS case-folding.
e5a527
+ * The path should be NUL-terminated, but we will match variants of both ".git\0"
e5a527
+ * and ".git/..." (but _not_ ".../.git"). This makes it suitable for both fsck
e5a527
+ * and verify_path().
e5a527
+ */
e5a527
+int is_hfs_dotgit(const char *path);
e5a527
+
e5a527
 #endif
e5a527