commit a86d224d78a3ac0f8a1901b0e9e2aee1e15d6f73
Author: Edward Thomson <ethomson@microsoft.com>
Date:   Thu Dec 18 12:41:59 2014 -0600

    index tests: test capitalization before mkdir

commit 86b9eb3bf5dba342d0a5d805e6fe35c3e9c861cc
Author: Carlos Martín Nieto <cmn@dwim.me>
Date:   Thu Dec 18 02:11:06 2014 +0100

    Plug leaks

commit 07164371d10109ba564835947a62fcedf288dce9
Author: Carlos Martín Nieto <cmn@dwim.me>
Date:   Thu Dec 18 02:07:36 2014 +0100

    Create miscapitialised dirs for case-sensitive filesystems
    
    We need these directories to exist so cl_git_mkfile() can create the
    files we ask it to.

commit 5d5d6136aaeea22903ed5d30a858f8d106876771
Author: Edward Thomson <ethomson@microsoft.com>
Date:   Tue Dec 16 18:53:55 2014 -0600

    Introduce core.protectHFS and core.protectNTFS
    
    Validate HFS ignored char ".git" paths when `core.protectHFS` is
    specified.  Validate NTFS invalid ".git" paths when `core.protectNTFS`
    is specified.

commit 2698e209d895856df9900899948269e2e490abd3
Author: Vicent Marti <tanoku@gmail.com>
Date:   Tue Dec 16 13:03:02 2014 +0100

    path: Use UTF8 iteration for HFS chars

commit d7026dc574b79723008bba72989f74a801f4dfb5
Author: Edward Thomson <ethomson@microsoft.com>
Date:   Wed Dec 10 19:12:16 2014 -0500

    checkout: disallow bad paths on HFS
    
    HFS filesystems ignore some characters like U+200C.  When these
    characters are included in a path, they will be ignored for the
    purposes of comparison with other paths.  Thus, if you have a ".git"
    folder, a folder of ".git<U+200C>" will also match.  Protect our
    ".git" folder by ensuring that ".git<U+200C>" and friends do not match it.

commit 37221f8cb02554297710f703047711a61e1169bb
Author: Edward Thomson <ethomson@microsoft.com>
Date:   Tue Nov 25 18:13:00 2014 -0500

    checkout: disallow bad paths on win32
    
    Disallow:
     1. paths with trailing dot
     2. paths with trailing space
     3. paths with trailing colon
     4. paths that are 8.3 short names of .git folders ("GIT~1")
     5. paths that are reserved path names (COM1, LPT1, etc).
     6. paths with reserved DOS characters (colons, asterisks, etc)
    
    These paths would (without \\?\ syntax) be elided to other paths - for
    example, ".git." would be written as ".git".  As a result, writing these
    paths literally (using \\?\ syntax) makes them hard to operate with from
    the shell, Windows Explorer or other tools.  Disallow these.

commit cb6a309d8667310d3323f5b601a2f2fa893c37d0
Author: Vicent Marti <tanoku@gmail.com>
Date:   Tue Nov 25 00:58:03 2014 +0100

    index: Check for valid paths before creating an index entry

commit 928a41d189f068010a32c6dea4bf921baa81d21c
Author: Vicent Marti <tanoku@gmail.com>
Date:   Tue Nov 25 00:14:52 2014 +0100

    tree: Check for `.git` with case insensitivy

commit f45baf7a94a75cfb1855c9a750f38bbcfa22b199
Author: Edward Thomson <ethomson@microsoft.com>
Date:   Mon Dec 1 13:09:58 2014 -0500

    win32: use NT-prefixed "\\?\" paths
    
    When turning UTF-8 paths into UCS-2 paths for Windows, always use
    the \\?\-prefixed paths.  Because this bypasses the system's
    path canonicalization, handle the canonicalization functions ourselves.
    
    We must:
     1. always use a backslash as a directory separator
     2. only use a single backslash between directories
     3. not rely on the system to translate "." and ".." in paths
     4. remove trailing backslashes, except at the drive root (C:\)

commit 2e37e214e3d85da2a68476c7ae54051d525b05eb
Author: Edward Thomson <ethomson@microsoft.com>
Date:   Mon Dec 1 13:06:11 2014 -0500

    clar: wide character comparisons

commit f2e46110c9f72d5eca539c76972b87003c5922be
Author: Edward Thomson <ethomson@microsoft.com>
Date:   Wed Nov 26 16:24:37 2014 -0500

    tests: use p_ instead of posix func directly
diff --git a/src/checkout.c b/src/checkout.c
index 20763fd..9adc6c6 100644
--- a/src/checkout.c
+++ b/src/checkout.c
@@ -1078,6 +1078,30 @@ done:
 	return error;
 }
 
+static int checkout_verify_paths(
+	git_repository *repo,
+	int action,
+	git_diff_delta *delta)
+{
+	unsigned int flags = GIT_PATH_REJECT_DEFAULTS | GIT_PATH_REJECT_DOT_GIT;
+
+	if (action & CHECKOUT_ACTION__REMOVE) {
+		if (!git_path_isvalid(repo, delta->old_file.path, flags)) {
+			giterr_set(GITERR_CHECKOUT, "Cannot remove invalid path '%s'", delta->old_file.path);
+			return -1;
+		}
+	}
+
+	if (action & ~CHECKOUT_ACTION__REMOVE) {
+		if (!git_path_isvalid(repo, delta->new_file.path, flags)) {
+			giterr_set(GITERR_CHECKOUT, "Cannot checkout to invalid path '%s'", delta->old_file.path);
+			return -1;
+		}
+	}
+
+	return 0;
+}
+
 static int checkout_get_actions(
 	uint32_t **actions_ptr,
 	size_t **counts_ptr,
@@ -1111,7 +1135,9 @@ static int checkout_get_actions(
 	}
 
 	git_vector_foreach(deltas, i, delta) {
-		error = checkout_action(&act, data, delta, workdir, &wditem, &pathspec);
+		if ((error = checkout_action(&act, data, delta, workdir, &wditem, &pathspec)) == 0)
+			error = checkout_verify_paths(data->repo, act, delta);
+
 		if (error != 0)
 			goto fail;
 
diff --git a/src/config_cache.c b/src/config_cache.c
index 45c39ce..d397a4b 100644
--- a/src/config_cache.c
+++ b/src/config_cache.c
@@ -76,6 +76,8 @@ static struct map_data _cvar_maps[] = {
 	{"core.precomposeunicode", NULL, 0, GIT_PRECOMPOSE_DEFAULT },
 	{"core.safecrlf", _cvar_map_safecrlf, ARRAY_SIZE(_cvar_map_safecrlf), GIT_SAFE_CRLF_DEFAULT},
 	{"core.logallrefupdates", NULL, 0, GIT_LOGALLREFUPDATES_DEFAULT },
+	{"core.protecthfs", NULL, 0, GIT_PROTECTHFS_DEFAULT },
+	{"core.protectntfs", NULL, 0, GIT_PROTECTNTFS_DEFAULT },
 };
 
 int git_config__cvar(int *out, git_config *config, git_cvar_cached cvar)
diff --git a/src/index.c b/src/index.c
index b63a0be..db2a5e8 100644
--- a/src/index.c
+++ b/src/index.c
@@ -756,23 +756,35 @@ void git_index_entry__init_from_stat(
 	entry->file_size = st->st_size;
 }
 
-static git_index_entry *index_entry_alloc(const char *path)
+static int index_entry_create(
+	git_index_entry **out,
+	git_repository *repo,
+	const char *path)
 {
 	size_t pathlen = strlen(path);
-	struct entry_internal *entry =
-		git__calloc(sizeof(struct entry_internal) + pathlen + 1, 1);
-	if (!entry)
-		return NULL;
+	struct entry_internal *entry;
+
+	if (!git_path_isvalid(repo, path,
+		GIT_PATH_REJECT_DEFAULTS | GIT_PATH_REJECT_DOT_GIT)) {
+		giterr_set(GITERR_INDEX, "Invalid path: '%s'", path);
+		return -1;
+	}
+
+	entry = git__calloc(sizeof(struct entry_internal) + pathlen + 1, 1);
+	GITERR_CHECK_ALLOC(entry);
 
 	entry->pathlen = pathlen;
 	memcpy(entry->path, path, pathlen);
 	entry->entry.path = entry->path;
 
-	return (git_index_entry *)entry;
+	*out = (git_index_entry *)entry;
+	return 0;
 }
 
 static int index_entry_init(
-	git_index_entry **entry_out, git_index *index, const char *rel_path)
+	git_index_entry **entry_out,
+	git_index *index,
+	const char *rel_path)
 {
 	int error = 0;
 	git_index_entry *entry = NULL;
@@ -784,14 +796,17 @@ static int index_entry_init(
 			"Could not initialize index entry. "
 			"Index is not backed up by an existing repository.");
 
+	if (index_entry_create(&entry, INDEX_OWNER(index), rel_path) < 0)
+		return -1;
+
 	/* write the blob to disk and get the oid and stat info */
 	error = git_blob__create_from_paths(
 		&oid, &st, INDEX_OWNER(index), NULL, rel_path, 0, true);
-	if (error < 0)
-		return error;
 
-	entry = index_entry_alloc(rel_path);
-	GITERR_CHECK_ALLOC(entry);
+	if (error < 0) {
+		index_entry_free(entry);
+		return error;
+	}
 
 	entry->id = oid;
 	git_index_entry__init_from_stat(entry, &st, !index->distrust_filemode);
@@ -847,7 +862,10 @@ static void index_entry_cpy(git_index_entry *tgt, const git_index_entry *src)
 	tgt->path = tgt_path; /* reset to existing path data */
 }
 
-static int index_entry_dup(git_index_entry **out, const git_index_entry *src)
+static int index_entry_dup(
+	git_index_entry **out,
+	git_repository *repo,
+	const git_index_entry *src)
 {
 	git_index_entry *entry;
 
@@ -856,11 +874,11 @@ static int index_entry_dup(git_index_entry **out, const git_index_entry *src)
 		return 0;
 	}
 
-	*out = entry = index_entry_alloc(src->path);
-	GITERR_CHECK_ALLOC(entry);
+	if (index_entry_create(&entry, repo, src->path) < 0)
+		return -1;
 
 	index_entry_cpy(entry, src);
-
+	*out = entry;
 	return 0;
 }
 
@@ -1125,7 +1143,7 @@ int git_index_add(git_index *index, const git_index_entry *source_entry)
 		return -1;
 	}
 
-	if ((ret = index_entry_dup(&entry, source_entry)) < 0 ||
+	if ((ret = index_entry_dup(&entry, INDEX_OWNER(index), source_entry)) < 0 ||
 		(ret = index_insert(index, &entry, 1)) < 0)
 		return ret;
 
@@ -1245,9 +1263,9 @@ int git_index_conflict_add(git_index *index,
 
 	assert (index);
 
-	if ((ret = index_entry_dup(&entries[0], ancestor_entry)) < 0 ||
-		(ret = index_entry_dup(&entries[1], our_entry)) < 0 ||
-		(ret = index_entry_dup(&entries[2], their_entry)) < 0)
+	if ((ret = index_entry_dup(&entries[0], INDEX_OWNER(index), ancestor_entry)) < 0 ||
+		(ret = index_entry_dup(&entries[1], INDEX_OWNER(index), our_entry)) < 0 ||
+		(ret = index_entry_dup(&entries[2], INDEX_OWNER(index), their_entry)) < 0)
 		goto on_error;
 
 	for (i = 0; i < 3; i++) {
@@ -1764,7 +1782,10 @@ static int read_conflict_names(git_index *index, const char *buffer, size_t size
 }
 
 static size_t read_entry(
-	git_index_entry **out, const void *buffer, size_t buffer_size)
+	git_index_entry **out,
+	git_index *index,
+	const void *buffer,
+	size_t buffer_size)
 {
 	size_t path_length, entry_size;
 	uint16_t flags_raw;
@@ -1821,7 +1842,7 @@ static size_t read_entry(
 
 	entry.path = (char *)path_ptr;
 
-	if (index_entry_dup(out, &entry) < 0)
+	if (index_entry_dup(out, INDEX_OWNER(index), &entry) < 0)
 		return 0;
 
 	return entry_size;
@@ -1924,7 +1945,7 @@ static int parse_index(git_index *index, const char *buffer, size_t buffer_size)
 	/* Parse all the entries */
 	for (i = 0; i < header.entry_count && buffer_size > INDEX_FOOTER_SIZE; ++i) {
 		git_index_entry *entry;
-		size_t entry_size = read_entry(&entry, buffer, buffer_size);
+		size_t entry_size = read_entry(&entry, index, buffer, buffer_size);
 
 		/* 0 bytes read means an object corruption */
 		if (entry_size == 0) {
@@ -2263,6 +2284,7 @@ int git_index_entry_stage(const git_index_entry *entry)
 }
 
 typedef struct read_tree_data {
+	git_index *index;
 	git_vector *old_entries;
 	git_vector *new_entries;
 	git_vector_cmp entry_cmp;
@@ -2282,8 +2304,8 @@ static int read_tree_cb(
 	if (git_buf_joinpath(&path, root, tentry->filename) < 0)
 		return -1;
 
-	entry = index_entry_alloc(path.ptr);
-	GITERR_CHECK_ALLOC(entry);
+	if (index_entry_create(&entry, INDEX_OWNER(data->index), path.ptr) < 0)
+		return -1;
 
 	entry->mode = tentry->attr;
 	entry->id = tentry->oid;
@@ -2323,6 +2345,7 @@ int git_index_read_tree(git_index *index, const git_tree *tree)
 
 	git_vector_set_cmp(&entries, index->entries._cmp); /* match sort */
 
+	data.index = index;
 	data.old_entries = &index->entries;
 	data.new_entries = &entries;
 	data.entry_cmp   = index->entries_search;
@@ -2435,7 +2458,7 @@ int git_index_add_all(
 			break;
 
 		/* make the new entry to insert */
-		if ((error = index_entry_dup(&entry, wd)) < 0)
+		if ((error = index_entry_dup(&entry, INDEX_OWNER(index), wd)) < 0)
 			break;
 
 		entry->id = blobid;
diff --git a/src/path.c b/src/path.c
index 5beab97..3e2efea 100644
--- a/src/path.c
+++ b/src/path.c
@@ -7,6 +7,7 @@
 #include "common.h"
 #include "path.h"
 #include "posix.h"
+#include "repository.h"
 #ifdef GIT_WIN32
 #include "win32/posix.h"
 #include "win32/w32_util.h"
@@ -1145,3 +1146,258 @@ int git_path_from_url_or_path(git_buf *local_path_out, const char *url_or_path)
 
 	return 0;
 }
+
+/* Reject paths like AUX or COM1, or those versions that end in a dot or
+ * colon.  ("AUX." or "AUX:")
+ */
+GIT_INLINE(bool) verify_dospath(
+	const char *component,
+	size_t len,
+	const char dospath[3],
+	bool trailing_num)
+{
+	size_t last = trailing_num ? 4 : 3;
+
+	if (len < last || git__strncasecmp(component, dospath, 3) != 0)
+		return true;
+
+	if (trailing_num && !git__isdigit(component[3]))
+		return true;
+
+	return (len > last &&
+		component[last] != '.' &&
+		component[last] != ':');
+}
+
+static int32_t next_hfs_char(const char **in, size_t *len)
+{
+	while (*len) {
+		int32_t codepoint;
+		int cp_len = git__utf8_iterate((const uint8_t *)(*in), (int)(*len), &codepoint);
+		if (cp_len < 0)
+			return -1;
+
+		(*in) += cp_len;
+		(*len) -= cp_len;
+
+		/* these code points are ignored completely */
+		switch (codepoint) {
+		case 0x200c: /* ZERO WIDTH NON-JOINER */
+		case 0x200d: /* ZERO WIDTH JOINER */
+		case 0x200e: /* LEFT-TO-RIGHT MARK */
+		case 0x200f: /* RIGHT-TO-LEFT MARK */
+		case 0x202a: /* LEFT-TO-RIGHT EMBEDDING */
+		case 0x202b: /* RIGHT-TO-LEFT EMBEDDING */
+		case 0x202c: /* POP DIRECTIONAL FORMATTING */
+		case 0x202d: /* LEFT-TO-RIGHT OVERRIDE */
+		case 0x202e: /* RIGHT-TO-LEFT OVERRIDE */
+		case 0x206a: /* INHIBIT SYMMETRIC SWAPPING */
+		case 0x206b: /* ACTIVATE SYMMETRIC SWAPPING */
+		case 0x206c: /* INHIBIT ARABIC FORM SHAPING */
+		case 0x206d: /* ACTIVATE ARABIC FORM SHAPING */
+		case 0x206e: /* NATIONAL DIGIT SHAPES */
+		case 0x206f: /* NOMINAL DIGIT SHAPES */
+		case 0xfeff: /* ZERO WIDTH NO-BREAK SPACE */
+			continue;
+		}
+
+		/* fold into lowercase -- this will only fold characters in
+		 * the ASCII range, which is perfectly fine, because the
+		 * git folder name can only be composed of ascii characters
+		 */
+		return tolower(codepoint);
+	}
+	return 0; /* NULL byte -- end of string */
+}
+
+static bool verify_dotgit_hfs(const char *path, size_t len)
+{
+	if (next_hfs_char(&path, &len) != '.' ||
+		next_hfs_char(&path, &len) != 'g' ||
+		next_hfs_char(&path, &len) != 'i' ||
+		next_hfs_char(&path, &len) != 't' ||
+		next_hfs_char(&path, &len) != 0)
+		return true;
+
+	return false;
+}
+
+GIT_INLINE(bool) verify_dotgit_ntfs(git_repository *repo, const char *path, size_t len)
+{
+	const char *shortname = NULL;
+	size_t i, start, shortname_len = 0;
+
+	/* See if the repo has a custom shortname (not "GIT~1") */
+	if (repo &&
+		(shortname = git_repository__8dot3_name(repo)) &&
+		shortname != git_repository__8dot3_default)
+		shortname_len = strlen(shortname);
+
+	if (len >= 4 && strncasecmp(path, ".git", 4) == 0)
+		start = 4;
+	else if (len >= git_repository__8dot3_default_len &&
+		strncasecmp(path, git_repository__8dot3_default, git_repository__8dot3_default_len) == 0)
+		start = git_repository__8dot3_default_len;
+	else if (shortname_len && len >= shortname_len &&
+		strncasecmp(path, shortname, shortname_len) == 0)
+		start = shortname_len;
+	else
+		return true;
+
+	/* Reject paths beginning with ".git\" */
+	if (path[start] == '\\')
+		return false;
+
+	for (i = start; i < len; i++) {
+		if (path[i] != ' ' && path[i] != '.')
+			return true;
+	}
+
+	return false;
+}
+
+GIT_INLINE(bool) verify_char(unsigned char c, unsigned int flags)
+{
+	if ((flags & GIT_PATH_REJECT_BACKSLASH) && c == '\\')
+		return false;
+
+	if ((flags & GIT_PATH_REJECT_SLASH) && c == '/')
+		return false;
+
+	if (flags & GIT_PATH_REJECT_NT_CHARS) {
+		if (c < 32)
+			return false;
+
+		switch (c) {
+		case '<':
+		case '>':
+		case ':':
+		case '"':
+		case '|':
+		case '?':
+		case '*':
+			return false;
+		}
+	}
+
+	return true;
+}
+
+/*
+ * We fundamentally don't like some paths when dealing with user-inputted
+ * strings (in checkout or ref names): we don't want dot or dot-dot
+ * anywhere, we want to avoid writing weird paths on Windows that can't
+ * be handled by tools that use the non-\\?\ APIs, we don't want slashes
+ * or double slashes at the end of paths that can make them ambiguous.
+ *
+ * For checkout, we don't want to recurse into ".git" either.
+ */
+static bool verify_component(
+	git_repository *repo,
+	const char *component,
+	size_t len,
+	unsigned int flags)
+{
+	if (len == 0)
+		return false;
+
+	if ((flags & GIT_PATH_REJECT_TRAVERSAL) &&
+		len == 1 && component[0] == '.')
+		return false;
+
+	if ((flags & GIT_PATH_REJECT_TRAVERSAL) &&
+		len == 2 && component[0] == '.' && component[1] == '.')
+		return false;
+
+	if ((flags & GIT_PATH_REJECT_TRAILING_DOT) && component[len-1] == '.')
+		return false;
+
+	if ((flags & GIT_PATH_REJECT_TRAILING_SPACE) && component[len-1] == ' ')
+		return false;
+
+	if ((flags & GIT_PATH_REJECT_TRAILING_COLON) && component[len-1] == ':')
+		return false;
+
+	if (flags & GIT_PATH_REJECT_DOS_PATHS) {
+		if (!verify_dospath(component, len, "CON", false) ||
+			!verify_dospath(component, len, "PRN", false) ||
+			!verify_dospath(component, len, "AUX", false) ||
+			!verify_dospath(component, len, "NUL", false) ||
+			!verify_dospath(component, len, "COM", true)  ||
+			!verify_dospath(component, len, "LPT", true))
+			return false;
+	}
+
+	if (flags & GIT_PATH_REJECT_DOT_GIT_HFS &&
+		!verify_dotgit_hfs(component, len))
+		return false;
+
+	if (flags & GIT_PATH_REJECT_DOT_GIT_NTFS &&
+		!verify_dotgit_ntfs(repo, component, len))
+		return false;
+
+	if ((flags & GIT_PATH_REJECT_DOT_GIT_HFS) == 0 &&
+		(flags & GIT_PATH_REJECT_DOT_GIT_NTFS) == 0 &&
+		(flags & GIT_PATH_REJECT_DOT_GIT) &&
+		len == 4 &&
+		component[0] == '.' &&
+		(component[1] == 'g' || component[1] == 'G') &&
+		(component[2] == 'i' || component[2] == 'I') &&
+		(component[3] == 't' || component[3] == 'T'))
+		return false;
+
+	return true;
+}
+
+GIT_INLINE(unsigned int) dotgit_flags(
+	git_repository *repo,
+	unsigned int flags)
+{
+	int protectHFS = 0, protectNTFS = 0;
+
+#ifdef __APPLE__
+	protectHFS = 1;
+#endif
+
+#ifdef GIT_WIN32
+	protectNTFS = 1;
+#endif
+
+	if (repo && !protectHFS)
+		git_repository__cvar(&protectHFS, repo, GIT_CVAR_PROTECTHFS);
+	if (protectHFS)
+		flags |= GIT_PATH_REJECT_DOT_GIT_HFS;
+
+	if (repo && !protectNTFS)
+		git_repository__cvar(&protectNTFS, repo, GIT_CVAR_PROTECTNTFS);
+	if (protectNTFS)
+		flags |= GIT_PATH_REJECT_DOT_GIT_NTFS;
+
+	return flags;
+}
+
+bool git_path_isvalid(
+	git_repository *repo,
+	const char *path,
+	unsigned int flags)
+{
+	const char *start, *c;
+
+	/* Upgrade the ".git" checks based on platform */
+	if ((flags & GIT_PATH_REJECT_DOT_GIT))
+		flags = dotgit_flags(repo, flags);
+
+	for (start = c = path; *c; c++) {
+		if (!verify_char(*c, flags))
+			return false;
+
+		if (*c == '/') {
+			if (!verify_component(repo, start, (c - start), flags))
+				return false;
+
+			start = c+1;
+		}
+	}
+
+	return verify_component(repo, start, (c - start), flags);
+}
diff --git a/src/path.h b/src/path.h
index 3e6efe3..6c50334 100644
--- a/src/path.h
+++ b/src/path.h
@@ -441,4 +441,47 @@ extern bool git_path_does_fs_decompose_unicode(const char *root);
 /* Used for paths to repositories on the filesystem */
 extern int git_path_from_url_or_path(git_buf *local_path_out, const char *url_or_path);
 
+/* Flags to determine path validity in `git_path_isvalid` */
+#define GIT_PATH_REJECT_TRAVERSAL          (1 << 0)
+#define GIT_PATH_REJECT_DOT_GIT            (1 << 1)
+#define GIT_PATH_REJECT_SLASH              (1 << 2)
+#define GIT_PATH_REJECT_BACKSLASH          (1 << 3)
+#define GIT_PATH_REJECT_TRAILING_DOT       (1 << 4)
+#define GIT_PATH_REJECT_TRAILING_SPACE     (1 << 5)
+#define GIT_PATH_REJECT_TRAILING_COLON     (1 << 6)
+#define GIT_PATH_REJECT_DOS_PATHS          (1 << 7)
+#define GIT_PATH_REJECT_NT_CHARS           (1 << 8)
+#define GIT_PATH_REJECT_DOT_GIT_HFS        (1 << 9)
+#define GIT_PATH_REJECT_DOT_GIT_NTFS       (1 << 10)
+
+/* Default path safety for writing files to disk: since we use the
+ * Win32 "File Namespace" APIs ("\\?\") we need to protect from
+ * paths that the normal Win32 APIs would not write.
+ */
+#ifdef GIT_WIN32
+# define GIT_PATH_REJECT_DEFAULTS \
+	GIT_PATH_REJECT_TRAVERSAL | \
+	GIT_PATH_REJECT_BACKSLASH | \
+	GIT_PATH_REJECT_TRAILING_DOT | \
+	GIT_PATH_REJECT_TRAILING_SPACE | \
+	GIT_PATH_REJECT_TRAILING_COLON | \
+	GIT_PATH_REJECT_DOS_PATHS | \
+	GIT_PATH_REJECT_NT_CHARS
+#else
+# define GIT_PATH_REJECT_DEFAULTS GIT_PATH_REJECT_TRAVERSAL
+#endif
+
+/*
+ * Determine whether a path is a valid git path or not - this must not contain
+ * a '.' or '..' component, or a component that is ".git" (in any case).
+ *
+ * `repo` is optional.  If specified, it will be used to determine the short
+ * path name to reject (if `GIT_PATH_REJECT_DOS_SHORTNAME` is specified),
+ * in addition to the default of "git~1".
+ */
+extern bool git_path_isvalid(
+	git_repository *repo,
+	const char *path,
+	unsigned int flags);
+
 #endif
diff --git a/src/refdb_fs.c b/src/refdb_fs.c
index 0e36ca8..682372f 100644
--- a/src/refdb_fs.c
+++ b/src/refdb_fs.c
@@ -712,6 +712,11 @@ static int loose_lock(git_filebuf *file, refdb_fs_backend *backend, const char *
 
 	assert(file && backend && name);
 
+	if (!git_path_isvalid(backend->repo, name, GIT_PATH_REJECT_DEFAULTS)) {
+		giterr_set(GITERR_INVALID, "Invalid reference name '%s'.", name);
+		return GIT_EINVALIDSPEC;
+	}
+
 	/* Remove a possibly existing empty directory hierarchy
 	 * which name would collide with the reference name
 	 */
@@ -1573,6 +1578,11 @@ static int lock_reflog(git_filebuf *file, refdb_fs_backend *backend, const char
 
 	repo = backend->repo;
 
+	if (!git_path_isvalid(backend->repo, refname, GIT_PATH_REJECT_DEFAULTS)) {
+		giterr_set(GITERR_INVALID, "Invalid reference name '%s'.", refname);
+		return GIT_EINVALIDSPEC;
+	}
+
 	if (retrieve_reflog_path(&log_path, repo, refname) < 0)
 		return -1;
 
diff --git a/src/repository.c b/src/repository.c
index e8d50ae..647704b 100644
--- a/src/repository.c
+++ b/src/repository.c
@@ -37,6 +37,9 @@
 
 #define GIT_REPO_VERSION 0
 
+const char *git_repository__8dot3_default = "GIT~1";
+size_t git_repository__8dot3_default_len = 5;
+
 static void set_odb(git_repository *repo, git_odb *odb)
 {
 	if (odb) {
@@ -120,6 +123,7 @@ void git_repository_free(git_repository *repo)
 	git__free(repo->path_repository);
 	git__free(repo->workdir);
 	git__free(repo->namespace);
+	git__free(repo->name_8dot3);
 
 	git__memzero(repo, sizeof(*repo));
 	git__free(repo);
@@ -791,6 +795,27 @@ const char *git_repository_get_namespace(git_repository *repo)
 	return repo->namespace;
 }
 
+const char *git_repository__8dot3_name(git_repository *repo)
+{
+	if (!repo->has_8dot3) {
+		repo->has_8dot3 = 1;
+
+#ifdef GIT_WIN32
+		if (!repo->is_bare) {
+			repo->name_8dot3 = git_win32_path_8dot3_name(repo->path_repository);
+
+			/* We anticipate the 8.3 name is "GIT~1", so use a static for
+			 * easy testing in the common case */
+			if (strcasecmp(repo->name_8dot3, git_repository__8dot3_default) == 0)
+				repo->has_8dot3_default = 1;
+		}
+#endif
+	}
+
+	return repo->has_8dot3_default ?
+		git_repository__8dot3_default : repo->name_8dot3;
+}
+
 static int check_repositoryformatversion(git_config *config)
 {
 	int version;
diff --git a/src/repository.h b/src/repository.h
index aba16a0..0718e64 100644
--- a/src/repository.h
+++ b/src/repository.h
@@ -40,6 +40,8 @@ typedef enum {
 	GIT_CVAR_PRECOMPOSE,    /* core.precomposeunicode */
 	GIT_CVAR_SAFE_CRLF,		/* core.safecrlf */
 	GIT_CVAR_LOGALLREFUPDATES, /* core.logallrefupdates */
+	GIT_CVAR_PROTECTHFS,    /* core.protectHFS */
+	GIT_CVAR_PROTECTNTFS,   /* core.protectNTFS */
 	GIT_CVAR_CACHE_MAX
 } git_cvar_cached;
 
@@ -96,6 +98,10 @@ typedef enum {
 	/* core.logallrefupdates */
 	GIT_LOGALLREFUPDATES_UNSET = 2,
 	GIT_LOGALLREFUPDATES_DEFAULT = GIT_LOGALLREFUPDATES_UNSET,
+	/* core.protectHFS */
+	GIT_PROTECTHFS_DEFAULT = GIT_CVAR_FALSE,
+	/* core.protectNTFS */
+	GIT_PROTECTNTFS_DEFAULT = GIT_CVAR_FALSE,
 } git_cvar_value;
 
 /* internal repository init flags */
@@ -120,8 +126,11 @@ struct git_repository {
 	char *path_repository;
 	char *workdir;
 	char *namespace;
+	char *name_8dot3;
 
-	unsigned is_bare:1;
+	unsigned is_bare:1,
+		has_8dot3:1,
+		has_8dot3_default:1;
 	unsigned int lru_counter;
 
 	git_cvar_value cvar_cache[GIT_CVAR_CACHE_MAX];
@@ -172,4 +181,19 @@ GIT_INLINE(int) git_repository__ensure_not_bare(
 
 int git_repository__cleanup_files(git_repository *repo, const char *files[], size_t files_len);
 
+/*
+ * Gets the DOS-compatible 8.3 "short name".  This will return only the
+ * short name for the repository directory (ie, "git~1" for ".git").  This
+ * will always return a pointer to `git_repository__8dot3_default` when
+ * "GIT~1" is the short name.  This will return NULL for bare repositories,
+ * and systems that do not have a short name.
+ */
+const char *git_repository__8dot3_name(git_repository *repo);
+
+/* The default DOS-compatible 8.3 "short name" for a git repository,
+ * "GIT~1".
+ */
+extern const char *git_repository__8dot3_default;
+extern size_t git_repository__8dot3_default_len;
+
 #endif
diff --git a/src/tree.c b/src/tree.c
index 4ddb26b..bfdcb82 100644
--- a/src/tree.c
+++ b/src/tree.c
@@ -55,7 +55,7 @@ static int valid_entry_name(const char *filename)
 		(*filename != '.' ||
 		 (strcmp(filename, ".") != 0 &&
 		  strcmp(filename, "..") != 0 &&
-		  strcmp(filename, DOT_GIT) != 0));
+		  strcasecmp(filename, DOT_GIT) != 0));
 }
 
 static int entry_sort_cmp(const void *a, const void *b)
diff --git a/src/util.c b/src/util.c
index f9d37e4..898db77 100644
--- a/src/util.c
+++ b/src/util.c
@@ -250,6 +250,21 @@ int git__prefixcmp_icase(const char *str, const char *prefix)
 	return strncasecmp(str, prefix, strlen(prefix));
 }
 
+int git__prefixncmp_icase(const char *str, size_t str_n, const char *prefix)
+{
+	int s, p;
+
+	while(str_n--) {
+		s = (unsigned char)tolower(*str++);
+		p = (unsigned char)tolower(*prefix++);
+
+		if (s != p)
+			return s - p;
+	}
+
+	return (0 - *prefix);
+}
+
 int git__suffixcmp(const char *str, const char *suffix)
 {
 	size_t a = strlen(str);
@@ -648,3 +663,79 @@ void git__insertsort_r(
 	if (freeswap)
 		git__free(swapel);
 }
+
+static const int8_t utf8proc_utf8class[256] = {
+	1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+	1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+	1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+	1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+	1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+	1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+	1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+	1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+	2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+	3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
+	4, 4, 4, 4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0
+};
+
+int git__utf8_charlen(const uint8_t *str, int str_len)
+{
+	int length, i;
+
+	length = utf8proc_utf8class[str[0]];
+	if (!length)
+		return -1;
+
+	if (str_len >= 0 && length > str_len)
+		return -str_len;
+
+	for (i = 1; i < length; i++) {
+		if ((str[i] & 0xC0) != 0x80)
+			return -i;
+	}
+
+	return length;
+}
+
+int git__utf8_iterate(const uint8_t *str, int str_len, int32_t *dst)
+{
+	int length;
+	int32_t uc = -1;
+
+	*dst = -1;
+	length = git__utf8_charlen(str, str_len);
+	if (length < 0)
+		return -1;
+
+	switch (length) {
+		case 1:
+			uc = str[0];
+			break;
+		case 2:
+			uc = ((str[0] & 0x1F) <<  6) + (str[1] & 0x3F);
+			if (uc < 0x80) uc = -1;
+			break;
+		case 3:
+			uc = ((str[0] & 0x0F) << 12) + ((str[1] & 0x3F) <<  6)
+				+ (str[2] & 0x3F);
+			if (uc < 0x800 || (uc >= 0xD800 && uc < 0xE000) ||
+					(uc >= 0xFDD0 && uc < 0xFDF0)) uc = -1;
+			break;
+		case 4:
+			uc = ((str[0] & 0x07) << 18) + ((str[1] & 0x3F) << 12)
+				+ ((str[2] & 0x3F) <<  6) + (str[3] & 0x3F);
+			if (uc < 0x10000 || uc >= 0x110000) uc = -1;
+			break;
+	}
+
+	if (uc < 0 || ((uc & 0xFFFF) >= 0xFFFE))
+		return -1;
+
+	*dst = uc;
+	return length;
+}
diff --git a/src/util.h b/src/util.h
index ca676c0..ad1e21e 100644
--- a/src/util.h
+++ b/src/util.h
@@ -106,6 +106,7 @@ GIT_INLINE(void) git__free(void *ptr)
 
 extern int git__prefixcmp(const char *str, const char *prefix);
 extern int git__prefixcmp_icase(const char *str, const char *prefix);
+extern int git__prefixncmp_icase(const char *str, size_t str_n, const char *prefix);
 extern int git__suffixcmp(const char *str, const char *suffix);
 
 GIT_INLINE(int) git__signum(int val)
@@ -367,6 +368,17 @@ extern int git__date_rfc2822_fmt(char *out, size_t len, const git_time *date);
 extern size_t git__unescape(char *str);
 
 /*
+ * Iterate through an UTF-8 string, yielding one
+ * codepoint at a time.
+ *
+ * @param str current position in the string
+ * @param str_len size left in the string; -1 if the string is NULL-terminated
+ * @param dst pointer where to store the current codepoint
+ * @return length in bytes of the read codepoint; -1 if the codepoint was invalid
+ */
+extern int git__utf8_iterate(const uint8_t *str, int str_len, int32_t *dst);
+
+/*
  * Safely zero-out memory, making sure that the compiler
  * doesn't optimize away the operation.
  */
diff --git a/tests/clar.c b/tests/clar.c
index 1546447..51f1635 100644
--- a/tests/clar.c
+++ b/tests/clar.c
@@ -11,6 +11,7 @@
 #include <string.h>
 #include <math.h>
 #include <stdarg.h>
+#include <wchar.h>
 
 /* required for sandboxing */
 #include <sys/types.h>
@@ -525,6 +526,41 @@ void clar__assert_equal(
 			}
 		}
 	}
+	else if (!strcmp("%ls", fmt)) {
+		const wchar_t *wcs1 = va_arg(args, const wchar_t *);
+		const wchar_t *wcs2 = va_arg(args, const wchar_t *);
+		is_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcscmp(wcs1, wcs2);
+
+		if (!is_equal) {
+			if (wcs1 && wcs2) {
+				int pos;
+				for (pos = 0; wcs1[pos] == wcs2[pos] && wcs1[pos] && wcs2[pos]; ++pos)
+					/* find differing byte offset */;
+				p_snprintf(buf, sizeof(buf), "'%ls' != '%ls' (at byte %d)",
+					wcs1, wcs2, pos);
+			} else {
+				p_snprintf(buf, sizeof(buf), "'%ls' != '%ls'", wcs1, wcs2);
+			}
+		}
+	}
+	else if(!strcmp("%.*ls", fmt)) {
+		const wchar_t *wcs1 = va_arg(args, const wchar_t *);
+		const wchar_t *wcs2 = va_arg(args, const wchar_t *);
+		int len = va_arg(args, int);
+		is_equal = (!wcs1 || !wcs2) ? (wcs1 == wcs2) : !wcsncmp(wcs1, wcs2, len);
+
+		if (!is_equal) {
+			if (wcs1 && wcs2) {
+				int pos;
+				for (pos = 0; wcs1[pos] == wcs2[pos] && pos < len; ++pos)
+					/* find differing byte offset */;
+				p_snprintf(buf, sizeof(buf), "'%.*ls' != '%.*ls' (at byte %d)",
+					len, wcs1, len, wcs2, pos);
+			} else {
+				p_snprintf(buf, sizeof(buf), "'%.*ls' != '%.*ls'", len, wcs1, len, wcs2);
+			}
+		}
+	}
 	else if (!strcmp("%"PRIuZ, fmt) || !strcmp("%"PRIxZ, fmt)) {
 		size_t sz1 = va_arg(args, size_t), sz2 = va_arg(args, size_t);
 		is_equal = (sz1 == sz2);
diff --git a/tests/clar.h b/tests/clar.h
index f9df72e..514203f 100644
--- a/tests/clar.h
+++ b/tests/clar.h
@@ -74,9 +74,15 @@ void cl_fixture_cleanup(const char *fixture_name);
 #define cl_assert_equal_s(s1,s2) clar__assert_equal(__FILE__,__LINE__,"String mismatch: " #s1 " != " #s2, 1, "%s", (s1), (s2))
 #define cl_assert_equal_s_(s1,s2,note) clar__assert_equal(__FILE__,__LINE__,"String mismatch: " #s1 " != " #s2 " (" #note ")", 1, "%s", (s1), (s2))
 
+#define cl_assert_equal_wcs(wcs1,wcs2) clar__assert_equal(__FILE__,__LINE__,"String mismatch: " #wcs1 " != " #wcs2, 1, "%ls", (wcs1), (wcs2))
+#define cl_assert_equal_wcs_(wcs1,wcs2,note) clar__assert_equal(__FILE__,__LINE__,"String mismatch: " #wcs1 " != " #wcs2 " (" #note ")", 1, "%ls", (wcs1), (wcs2))
+
 #define cl_assert_equal_strn(s1,s2,len) clar__assert_equal(__FILE__,__LINE__,"String mismatch: " #s1 " != " #s2, 1, "%.*s", (s1), (s2), (int)(len))
 #define cl_assert_equal_strn_(s1,s2,len,note) clar__assert_equal(__FILE__,__LINE__,"String mismatch: " #s1 " != " #s2 " (" #note ")", 1, "%.*s", (s1), (s2), (int)(len))
 
+#define cl_assert_equal_wcsn(wcs1,wcs2,len) clar__assert_equal(__FILE__,__LINE__,"String mismatch: " #wcs1 " != " #wcs2, 1, "%.*ls", (wcs1), (wcs2), (int)(len))
+#define cl_assert_equal_wcsn_(wcs1,wcs2,len,note) clar__assert_equal(__FILE__,__LINE__,"String mismatch: " #wcs1 " != " #wcs2 " (" #note ")", 1, "%.*ls", (wcs1), (wcs2), (int)(len))
+
 #define cl_assert_equal_i(i1,i2) clar__assert_equal(__FILE__,__LINE__,#i1 " != " #i2, 1, "%d", (int)(i1), (int)(i2))
 #define cl_assert_equal_i_(i1,i2,note) clar__assert_equal(__FILE__,__LINE__,#i1 " != " #i2 " (" #note ")", 1, "%d", (i1), (i2))
 #define cl_assert_equal_i_fmt(i1,i2,fmt) clar__assert_equal(__FILE__,__LINE__,#i1 " != " #i2, 1, (fmt), (int)(i1), (int)(i2))
diff --git a/tests/config/include.c b/tests/config/include.c
index 58bc690..88ea091 100644
--- a/tests/config/include.c
+++ b/tests/config/include.c
@@ -106,6 +106,6 @@ void test_config_include__depth(void)
 
 	cl_git_fail(git_config_open_ondisk(&cfg, "a"));
 
-	unlink("a");
-	unlink("b");
+	p_unlink("a");
+	p_unlink("b");
 }
diff --git a/tests/core/link.c b/tests/core/link.c
index 1794a38..2674e35 100644
--- a/tests/core/link.c
+++ b/tests/core/link.c
@@ -196,19 +196,6 @@ static void do_custom_reparse(const char *path)
 
 #endif
 
-git_buf *unslashify(git_buf *buf)
-{
-#ifdef GIT_WIN32
-	size_t i;
-
-	for (i = 0; i < buf->size; i++)
-		if (buf->ptr[i] == '/')
-			buf->ptr[i] = '\\';
-#endif
-
-	return buf;
-}
-
 void test_core_link__stat_regular_file(void)
 {
 	struct stat st;
@@ -547,7 +534,7 @@ void test_core_link__readlink_symlink(void)
 
 	buf[len] = 0;
 
-	cl_assert_equal_s(git_buf_cstr(unslashify(&target_path)), buf);
+	cl_assert_equal_s(git_buf_cstr(&target_path), buf);
 
 	git_buf_free(&target_path);
 }
@@ -567,7 +554,7 @@ void test_core_link__readlink_dangling(void)
 
 	buf[len] = 0;
 
-	cl_assert_equal_s(git_buf_cstr(unslashify(&target_path)), buf);
+	cl_assert_equal_s(git_buf_cstr(&target_path), buf);
 
 	git_buf_free(&target_path);
 }
@@ -593,7 +580,7 @@ void test_core_link__readlink_multiple(void)
 
 	buf[len] = 0;
 
-	cl_assert_equal_s(git_buf_cstr(unslashify(&path2)), buf);
+	cl_assert_equal_s(git_buf_cstr(&path2), buf);
 
 	git_buf_free(&path1);
 	git_buf_free(&path2);
diff --git a/tests/index/tests.c b/tests/index/tests.c
index fa5c0bb..42f4483 100644
--- a/tests/index/tests.c
+++ b/tests/index/tests.c
@@ -309,31 +309,124 @@ void test_index_tests__add_bypath_to_a_bare_repository_returns_EBAREPO(void)
 	git_repository_free(bare_repo);
 }
 
+static void add_invalid_filename(git_repository *repo, const char *fn)
+{
+	git_index *index;
+	git_buf path = GIT_BUF_INIT;
+
+	cl_git_pass(git_repository_index(&index, repo));
+	cl_assert(git_index_entrycount(index) == 0);
+
+	git_buf_joinpath(&path, "./invalid", fn);
+
+	cl_git_mkfile(path.ptr, NULL);
+	cl_git_fail(git_index_add_bypath(index, fn));
+	cl_must_pass(p_unlink(path.ptr));
+
+	cl_assert(git_index_entrycount(index) == 0);
+
+	git_buf_free(&path);
+	git_index_free(index);
+}
+
 /* Test that writing an invalid filename fails */
-void test_index_tests__write_invalid_filename(void)
+void test_index_tests__add_invalid_filename(void)
 {
 	git_repository *repo;
+
+	p_mkdir("invalid", 0700);
+
+	cl_git_pass(git_repository_init(&repo, "./invalid", 0));
+	cl_must_pass(p_mkdir("./invalid/subdir", 0777));
+
+	/* cl_git_mkfile() needs the dir to exist */
+	if (!git_path_exists("./invalid/.GIT"))
+		cl_must_pass(p_mkdir("./invalid/.GIT", 0777));
+	if (!git_path_exists("./invalid/.GiT"))
+		cl_must_pass(p_mkdir("./invalid/.GiT", 0777));
+
+	add_invalid_filename(repo, ".git/hello");
+	add_invalid_filename(repo, ".GIT/hello");
+	add_invalid_filename(repo, ".GiT/hello");
+	add_invalid_filename(repo, "./.git/hello");
+	add_invalid_filename(repo, "./foo");
+	add_invalid_filename(repo, "./bar");
+	add_invalid_filename(repo, "subdir/../bar");
+
+	git_repository_free(repo);
+
+	cl_fixture_cleanup("invalid");
+}
+
+static void replace_char(char *str, char in, char out)
+{
+	char *c = str;
+
+	while (*c++)
+		if (*c == in)
+			*c = out;
+}
+
+static void write_invalid_filename(git_repository *repo, const char *fn_orig)
+{
 	git_index *index;
 	git_oid expected;
+	const git_index_entry *entry;
+	git_buf path = GIT_BUF_INIT;
+	char *fn;
 
-	p_mkdir("read_tree", 0700);
-
-	cl_git_pass(git_repository_init(&repo, "./read_tree", 0));
 	cl_git_pass(git_repository_index(&index, repo));
-
 	cl_assert(git_index_entrycount(index) == 0);
 
-	cl_git_mkfile("./read_tree/.git/hello", NULL);
+	/*
+	 * Sneak a valid path into the index, we'll update it
+	 * to an invalid path when we try to write the index.
+	 */
+	fn = git__strdup(fn_orig);
+	replace_char(fn, '/', '_');
+
+	git_buf_joinpath(&path, "./invalid", fn);
+
+	cl_git_mkfile(path.ptr, NULL);
+
+	cl_git_pass(git_index_add_bypath(index, fn));
+
+	cl_assert(entry = git_index_get_bypath(index, fn, 0));
 
-	cl_git_pass(git_index_add_bypath(index, ".git/hello"));
+	/* kids, don't try this at home */
+	replace_char((char *)entry->path, '_', '/');
 
 	/* write-tree */
 	cl_git_fail(git_index_write_tree(&expected, index));
 
+	p_unlink(path.ptr);
+
+	cl_git_pass(git_index_remove_all(index, NULL, NULL, NULL));
+	git_buf_free(&path);
 	git_index_free(index);
+	git__free(fn);
+}
+
+/* Test that writing an invalid filename fails */
+void test_index_tests__write_invalid_filename(void)
+{
+	git_repository *repo;
+
+	p_mkdir("invalid", 0700);
+
+	cl_git_pass(git_repository_init(&repo, "./invalid", 0));
+
+	write_invalid_filename(repo, ".git/hello");
+	write_invalid_filename(repo, ".GIT/hello");
+	write_invalid_filename(repo, ".GiT/hello");
+	write_invalid_filename(repo, "./.git/hello");
+	write_invalid_filename(repo, "./foo");
+	write_invalid_filename(repo, "./bar");
+	write_invalid_filename(repo, "foo/../bar");
+
 	git_repository_free(repo);
 
-	cl_fixture_cleanup("read_tree");
+	cl_fixture_cleanup("invalid");
 }
 
 void test_index_tests__remove_entry(void)
diff --git a/tests/path/core.c b/tests/path/core.c
new file mode 100644
index 0000000..8a29004
--- /dev/null
+++ b/tests/path/core.c
@@ -0,0 +1,241 @@
+#include "clar_libgit2.h"
+#include "path.h"
+
+void test_path_core__isvalid_standard(void)
+{
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo/bar", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo/bar/file.txt", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo/bar/.file", 0));
+}
+
+void test_path_core__isvalid_empty_dir_component(void)
+{
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo//bar", 0));
+
+	/* leading slash */
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "/", 0));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "/foo", 0));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "/foo/bar", 0));
+
+	/* trailing slash */
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo/", 0));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo/bar/", 0));
+}
+
+void test_path_core__isvalid_dot_and_dotdot(void)
+{
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ".", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "./foo", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo/.", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "./foo", 0));
+
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "..", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "../foo", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo/..", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "../foo", 0));
+
+	cl_assert_equal_b(false, git_path_isvalid(NULL, ".", GIT_PATH_REJECT_TRAVERSAL));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "./foo", GIT_PATH_REJECT_TRAVERSAL));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo/.", GIT_PATH_REJECT_TRAVERSAL));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "./foo", GIT_PATH_REJECT_TRAVERSAL));
+
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "..", GIT_PATH_REJECT_TRAVERSAL));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "../foo", GIT_PATH_REJECT_TRAVERSAL));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo/..", GIT_PATH_REJECT_TRAVERSAL));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "../foo", GIT_PATH_REJECT_TRAVERSAL));
+}
+
+void test_path_core__isvalid_dot_git(void)
+{
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ".git", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ".git/foo", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo/.git", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo/.git/bar", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo/.GIT/bar", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo/bar/.Git", 0));
+
+	cl_assert_equal_b(false, git_path_isvalid(NULL, ".git", GIT_PATH_REJECT_DOT_GIT));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, ".git/foo", GIT_PATH_REJECT_DOT_GIT));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo/.git", GIT_PATH_REJECT_DOT_GIT));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo/.git/bar", GIT_PATH_REJECT_DOT_GIT));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo/.GIT/bar", GIT_PATH_REJECT_DOT_GIT));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo/bar/.Git", GIT_PATH_REJECT_DOT_GIT));
+
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "!git", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo/!git", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "!git/bar", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ".tig", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo/.tig", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ".tig/bar", 0));
+}
+
+void test_path_core__isvalid_backslash(void)
+{
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo\\file.txt", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo/bar\\file.txt", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo/bar\\", 0));
+
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo\\file.txt", GIT_PATH_REJECT_BACKSLASH));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo/bar\\file.txt", GIT_PATH_REJECT_BACKSLASH));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo/bar\\", GIT_PATH_REJECT_BACKSLASH));
+}
+
+void test_path_core__isvalid_trailing_dot(void)
+{
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo.", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo...", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo/bar.", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo./bar", 0));
+
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo.", GIT_PATH_REJECT_TRAILING_DOT));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo...", GIT_PATH_REJECT_TRAILING_DOT));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo/bar.", GIT_PATH_REJECT_TRAILING_DOT));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo./bar", GIT_PATH_REJECT_TRAILING_DOT));
+}
+
+void test_path_core__isvalid_trailing_space(void)
+{
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo ", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo   ", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo/bar ", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, " ", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo /bar", 0));
+
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo ", GIT_PATH_REJECT_TRAILING_SPACE));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo   ", GIT_PATH_REJECT_TRAILING_SPACE));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo/bar ", GIT_PATH_REJECT_TRAILING_SPACE));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, " ", GIT_PATH_REJECT_TRAILING_SPACE));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo /bar", GIT_PATH_REJECT_TRAILING_SPACE));
+}
+
+void test_path_core__isvalid_trailing_colon(void)
+{
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo:", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo/bar:", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ":", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "foo:/bar", 0));
+
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo:", GIT_PATH_REJECT_TRAILING_COLON));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo/bar:", GIT_PATH_REJECT_TRAILING_COLON));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, ":", GIT_PATH_REJECT_TRAILING_COLON));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "foo:/bar", GIT_PATH_REJECT_TRAILING_COLON));
+}
+
+void test_path_core__isvalid_dotgit_ntfs(void)
+{
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ".git", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ".git ", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ".git.", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ".git.. .", 0));
+
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "git~1", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "git~1 ", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "git~1.", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "git~1.. .", 0));
+
+	cl_assert_equal_b(false, git_path_isvalid(NULL, ".git", GIT_PATH_REJECT_DOT_GIT_NTFS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, ".git ", GIT_PATH_REJECT_DOT_GIT_NTFS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, ".git.", GIT_PATH_REJECT_DOT_GIT_NTFS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, ".git.. .", GIT_PATH_REJECT_DOT_GIT_NTFS));
+
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "git~1", GIT_PATH_REJECT_DOT_GIT_NTFS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "git~1 ", GIT_PATH_REJECT_DOT_GIT_NTFS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "git~1.", GIT_PATH_REJECT_DOT_GIT_NTFS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "git~1.. .", GIT_PATH_REJECT_DOT_GIT_NTFS));
+}
+
+void test_path_core__isvalid_dos_paths(void)
+{
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "aux", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "aux.", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "aux:", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "aux.asdf", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "aux.asdf\\zippy", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "aux:asdf\\foobar", 0));
+
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "aux", GIT_PATH_REJECT_DOS_PATHS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "aux.", GIT_PATH_REJECT_DOS_PATHS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "aux:", GIT_PATH_REJECT_DOS_PATHS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "aux.asdf", GIT_PATH_REJECT_DOS_PATHS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "aux.asdf\\zippy", GIT_PATH_REJECT_DOS_PATHS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "aux:asdf\\foobar", GIT_PATH_REJECT_DOS_PATHS));
+
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "aux1", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "aux1", GIT_PATH_REJECT_DOS_PATHS));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "auxn", GIT_PATH_REJECT_DOS_PATHS));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "aux\\foo", GIT_PATH_REJECT_DOS_PATHS));
+}
+
+void test_path_core__isvalid_dos_paths_withnum(void)
+{
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "com1", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "com1.", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "com1:", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "com1.asdf", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "com1.asdf\\zippy", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "com1:asdf\\foobar", 0));
+
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "com1", GIT_PATH_REJECT_DOS_PATHS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "com1.", GIT_PATH_REJECT_DOS_PATHS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "com1:", GIT_PATH_REJECT_DOS_PATHS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "com1.asdf", GIT_PATH_REJECT_DOS_PATHS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "com1.asdf\\zippy", GIT_PATH_REJECT_DOS_PATHS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "com1:asdf\\foobar", GIT_PATH_REJECT_DOS_PATHS));
+
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "com10", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "com10", GIT_PATH_REJECT_DOS_PATHS));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "comn", GIT_PATH_REJECT_DOS_PATHS));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "com1\\foo", GIT_PATH_REJECT_DOS_PATHS));
+}
+
+void test_path_core__isvalid_nt_chars(void)
+{
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "asdf\001foo", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "asdf\037bar", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "asdf<bar", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "asdf>foo", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "asdf:foo", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "asdf\"bar", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "asdf|foo", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "asdf?bar", 0));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "asdf*bar", 0));
+
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "asdf\001foo", GIT_PATH_REJECT_NT_CHARS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "asdf\037bar", GIT_PATH_REJECT_NT_CHARS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "asdf<bar", GIT_PATH_REJECT_NT_CHARS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "asdf>foo", GIT_PATH_REJECT_NT_CHARS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "asdf:foo", GIT_PATH_REJECT_NT_CHARS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "asdf\"bar", GIT_PATH_REJECT_NT_CHARS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "asdf|foo", GIT_PATH_REJECT_NT_CHARS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "asdf?bar", GIT_PATH_REJECT_NT_CHARS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "asdf*bar", GIT_PATH_REJECT_NT_CHARS));
+}
+
+void test_path_core__isvalid_dotgit_with_hfs_ignorables(void)
+{
+	cl_assert_equal_b(false, git_path_isvalid(NULL, ".git", GIT_PATH_REJECT_DOT_GIT_HFS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, ".git\xe2\x80\x8c", GIT_PATH_REJECT_DOT_GIT_HFS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, ".gi\xe2\x80\x8dT", GIT_PATH_REJECT_DOT_GIT_HFS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, ".g\xe2\x80\x8eIt", GIT_PATH_REJECT_DOT_GIT_HFS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, ".\xe2\x80\x8fgIt", GIT_PATH_REJECT_DOT_GIT_HFS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "\xe2\x80\xaa.gIt", GIT_PATH_REJECT_DOT_GIT_HFS));
+
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "\xe2\x80\xab.\xe2\x80\xacG\xe2\x80\xadI\xe2\x80\xaet", GIT_PATH_REJECT_DOT_GIT_HFS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "\xe2\x81\xab.\xe2\x80\xaaG\xe2\x81\xabI\xe2\x80\xact", GIT_PATH_REJECT_DOT_GIT_HFS));
+	cl_assert_equal_b(false, git_path_isvalid(NULL, "\xe2\x81\xad.\xe2\x80\xaeG\xef\xbb\xbfIT", GIT_PATH_REJECT_DOT_GIT_HFS));
+
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ".", GIT_PATH_REJECT_DOT_GIT_HFS));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ".g", GIT_PATH_REJECT_DOT_GIT_HFS));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ".gi", GIT_PATH_REJECT_DOT_GIT_HFS));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, " .git", GIT_PATH_REJECT_DOT_GIT_HFS));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "..git\xe2\x80\x8c", GIT_PATH_REJECT_DOT_GIT_HFS));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ".gi\xe2\x80\x8dT.", GIT_PATH_REJECT_DOT_GIT_HFS));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ".g\xe2\x80It", GIT_PATH_REJECT_DOT_GIT_HFS));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ".\xe2gIt", GIT_PATH_REJECT_DOT_GIT_HFS));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, "\xe2\x80\xaa.gi", GIT_PATH_REJECT_DOT_GIT_HFS));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ".gi\x80\x8dT", GIT_PATH_REJECT_DOT_GIT_HFS));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ".gi\x8dT", GIT_PATH_REJECT_DOT_GIT_HFS));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ".g\xe2i\x80T\x8e", GIT_PATH_REJECT_DOT_GIT_HFS));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ".git\xe2\x80\xbf", GIT_PATH_REJECT_DOT_GIT_HFS));
+	cl_assert_equal_b(true, git_path_isvalid(NULL, ".git\xe2\xab\x81", GIT_PATH_REJECT_DOT_GIT_HFS));
+}
