On 9 February 2015 at 09:36, Mehdi Dogguy <[email protected]> wrote: > I'm afraid we cannot accept 0.21.3-1.1 in Jessie because the changes are > quite large. Can you please prepare an upload targetting jessie based on > 0.21.1-2.1? >
Thanks for looking at this. I have created a patch that backport the relevant changes to the 0.21.1-2.1 Mehdi. I'm so sorry for the noise :( -- Cheers, Russell Sim
diff -Nru libgit2-0.21.1/debian/changelog libgit2-0.21.1/debian/changelog --- libgit2-0.21.1/debian/changelog 2015-01-09 09:51:34.000000000 +1100 +++ libgit2-0.21.1/debian/changelog 2015-02-11 23:09:15.000000000 +1100 @@ -1,3 +1,10 @@ +libgit2 (0.21.1-3) jessie; urgency=medium + + * Backported fix for case insensitive filesystems (CVE-2014-9390). + (Closes: #774048) + + -- Russell Sim <[email protected]> Tue, 10 Feb 2015 20:29:05 +1100 + libgit2 (0.21.1-2.1) jessie; urgency=medium * Non-maintainer upload. diff -Nru libgit2-0.21.1/debian/patches/CVE-2014-9390.patch libgit2-0.21.1/debian/patches/CVE-2014-9390.patch --- libgit2-0.21.1/debian/patches/CVE-2014-9390.patch 1970-01-01 10:00:00.000000000 +1000 +++ libgit2-0.21.1/debian/patches/CVE-2014-9390.patch 2015-02-11 23:09:15.000000000 +1100 @@ -0,0 +1,2483 @@ +commit a86d224d78a3ac0f8a1901b0e9e2aee1e15d6f73 +Author: Edward Thomson <[email protected]> +Date: Thu Dec 18 12:41:59 2014 -0600 + + index tests: test capitalization before mkdir + +commit 86b9eb3bf5dba342d0a5d805e6fe35c3e9c861cc +Author: Carlos Martín Nieto <[email protected]> +Date: Thu Dec 18 02:11:06 2014 +0100 + + Plug leaks + +commit 07164371d10109ba564835947a62fcedf288dce9 +Author: Carlos Martín Nieto <[email protected]> +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 <[email protected]> +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 <[email protected]> +Date: Tue Dec 16 13:03:02 2014 +0100 + + path: Use UTF8 iteration for HFS chars + +commit d7026dc574b79723008bba72989f74a801f4dfb5 +Author: Edward Thomson <[email protected]> +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 <[email protected]> +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 <[email protected]> +Date: Tue Nov 25 00:58:03 2014 +0100 + + index: Check for valid paths before creating an index entry + +commit 928a41d189f068010a32c6dea4bf921baa81d21c +Author: Vicent Marti <[email protected]> +Date: Tue Nov 25 00:14:52 2014 +0100 + + tree: Check for `.git` with case insensitivy + +commit f45baf7a94a75cfb1855c9a750f38bbcfa22b199 +Author: Edward Thomson <[email protected]> +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 <[email protected]> +Date: Mon Dec 1 13:06:11 2014 -0500 + + clar: wide character comparisons + +commit f2e46110c9f72d5eca539c76972b87003c5922be +Author: Edward Thomson <[email protected]> +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/src/win32/findfile.c b/src/win32/findfile.c +index 86d4ef5..de27dd0 100644 +--- a/src/win32/findfile.c ++++ b/src/win32/findfile.c +@@ -5,6 +5,7 @@ + * a Linking Exception. For full terms see the included COPYING file. + */ + ++#include "path_w32.h" + #include "utf-conv.h" + #include "path.h" + #include "findfile.h" +diff --git a/src/win32/path_w32.c b/src/win32/path_w32.c +new file mode 100644 +index 0000000..d66969c +--- /dev/null ++++ b/src/win32/path_w32.c +@@ -0,0 +1,305 @@ ++/* ++ * Copyright (C) the libgit2 contributors. All rights reserved. ++ * ++ * This file is part of libgit2, distributed under the GNU GPL v2 with ++ * a Linking Exception. For full terms see the included COPYING file. ++ */ ++ ++#include "common.h" ++#include "path.h" ++#include "path_w32.h" ++#include "utf-conv.h" ++ ++#define PATH__NT_NAMESPACE L"\\\\?\\" ++#define PATH__NT_NAMESPACE_LEN 4 ++ ++#define PATH__ABSOLUTE_LEN 3 ++ ++#define path__is_dirsep(p) ((p) == '/' || (p) == '\\') ++ ++#define path__is_absolute(p) \ ++ (git__isalpha((p)[0]) && (p)[1] == ':' && ((p)[2] == '\\' || (p)[2] == '/')) ++ ++#define path__is_nt_namespace(p) \ ++ (((p)[0] == '\\' && (p)[1] == '\\' && (p)[2] == '?' && (p)[3] == '\\') || \ ++ ((p)[0] == '/' && (p)[1] == '/' && (p)[2] == '?' && (p)[3] == '/')) ++ ++#define path__is_unc(p) \ ++ (((p)[0] == '\\' && (p)[1] == '\\') || ((p)[0] == '/' && (p)[1] == '/')) ++ ++GIT_INLINE(int) path__cwd(wchar_t *path, int size) ++{ ++ int len; ++ ++ if ((len = GetCurrentDirectoryW(size, path)) == 0) { ++ errno = GetLastError() == ERROR_ACCESS_DENIED ? EACCES : ENOENT; ++ return -1; ++ } else if (len > size) { ++ errno = ENAMETOOLONG; ++ return -1; ++ } ++ ++ /* The Win32 APIs may return "\\?\" once you've used it first. ++ * But it may not. What a gloriously predictible API! ++ */ ++ if (wcsncmp(path, PATH__NT_NAMESPACE, PATH__NT_NAMESPACE_LEN)) ++ return len; ++ ++ len -= PATH__NT_NAMESPACE_LEN; ++ ++ memmove(path, path + PATH__NT_NAMESPACE_LEN, sizeof(wchar_t) * len); ++ return len; ++} ++ ++static wchar_t *path__skip_server(wchar_t *path) ++{ ++ wchar_t *c; ++ ++ for (c = path; *c; c++) { ++ if (path__is_dirsep(*c)) ++ return c + 1; ++ } ++ ++ return c; ++} ++ ++static wchar_t *path__skip_prefix(wchar_t *path) ++{ ++ if (path__is_nt_namespace(path)) { ++ path += PATH__NT_NAMESPACE_LEN; ++ ++ if (wcsncmp(path, L"UNC\\", 4) == 0) ++ path = path__skip_server(path + 4); ++ else if (path__is_absolute(path)) ++ path += PATH__ABSOLUTE_LEN; ++ } else if (path__is_absolute(path)) { ++ path += PATH__ABSOLUTE_LEN; ++ } else if (path__is_unc(path)) { ++ path = path__skip_server(path + 2); ++ } ++ ++ return path; ++} ++ ++int git_win32_path_canonicalize(git_win32_path path) ++{ ++ wchar_t *base, *from, *to, *next; ++ size_t len; ++ ++ base = to = path__skip_prefix(path); ++ ++ /* Unposixify if the prefix */ ++ for (from = path; from < to; from++) { ++ if (*from == L'/') ++ *from = L'\\'; ++ } ++ ++ while (*from) { ++ for (next = from; *next; ++next) { ++ if (*next == L'/') { ++ *next = L'\\'; ++ break; ++ } ++ ++ if (*next == L'\\') ++ break; ++ } ++ ++ len = next - from; ++ ++ if (len == 1 && from[0] == L'.') ++ /* do nothing with singleton dot */; ++ ++ else if (len == 2 && from[0] == L'.' && from[1] == L'.') { ++ if (to == base) { ++ /* no more path segments to strip, eat the "../" */ ++ if (*next == L'\\') ++ len++; ++ ++ base = to; ++ } else { ++ /* back up a path segment */ ++ while (to > base && to[-1] == L'\\') to--; ++ while (to > base && to[-1] != L'\\') to--; ++ } ++ } else { ++ if (*next == L'\\' && *from != L'\\') ++ len++; ++ ++ if (to != from) ++ memmove(to, from, sizeof(wchar_t) * len); ++ ++ to += len; ++ } ++ ++ from += len; ++ ++ while (*from == L'\\') from++; ++ } ++ ++ /* Strip trailing backslashes */ ++ while (to > base && to[-1] == L'\\') to--; ++ ++ *to = L'\0'; ++ ++ return (to - path); ++} ++ ++int git_win32_path__cwd(wchar_t *out, size_t len) ++{ ++ int cwd_len; ++ ++ if ((cwd_len = path__cwd(out, len)) < 0) ++ return -1; ++ ++ /* UNC paths */ ++ if (wcsncmp(L"\\\\", out, 2) == 0) { ++ /* Our buffer must be at least 5 characters larger than the ++ * current working directory: we swallow one of the leading ++ * '\'s, but we we add a 'UNC' specifier to the path, plus ++ * a trailing directory separator, plus a NUL. ++ */ ++ if (cwd_len > MAX_PATH - 4) { ++ errno = ENAMETOOLONG; ++ return -1; ++ } ++ ++ memmove(out+2, out, sizeof(wchar_t) * cwd_len); ++ out[0] = L'U'; ++ out[1] = L'N'; ++ out[2] = L'C'; ++ ++ cwd_len += 2; ++ } ++ ++ /* Our buffer must be at least 2 characters larger than the current ++ * working directory. (One character for the directory separator, ++ * one for the null. ++ */ ++ else if (cwd_len > MAX_PATH - 2) { ++ errno = ENAMETOOLONG; ++ return -1; ++ } ++ ++ return cwd_len; ++} ++ ++int git_win32_path_from_utf8(git_win32_path out, const char *src) ++{ ++ wchar_t *dest = out; ++ ++ /* All win32 paths are in NT-prefixed format, beginning with "\\?\". */ ++ memcpy(dest, PATH__NT_NAMESPACE, sizeof(wchar_t) * PATH__NT_NAMESPACE_LEN); ++ dest += PATH__NT_NAMESPACE_LEN; ++ ++ /* See if this is an absolute path (beginning with a drive letter) */ ++ if (path__is_absolute(src)) { ++ if (git__utf8_to_16(dest, MAX_PATH, src) < 0) ++ return -1; ++ } ++ /* File-prefixed NT-style paths beginning with \\?\ */ ++ else if (path__is_nt_namespace(src)) { ++ /* Skip the NT prefix, the destination already contains it */ ++ if (git__utf8_to_16(dest, MAX_PATH, src + PATH__NT_NAMESPACE_LEN) < 0) ++ return -1; ++ } ++ /* UNC paths */ ++ else if (path__is_unc(src)) { ++ memcpy(dest, L"UNC\\", sizeof(wchar_t) * 4); ++ dest += 4; ++ ++ /* Skip the leading "\\" */ ++ if (git__utf8_to_16(dest, MAX_PATH - 2, src + 2) < 0) ++ return -1; ++ } ++ /* Absolute paths omitting the drive letter */ ++ else if (src[0] == '\\' || src[0] == '/') { ++ if (path__cwd(dest, MAX_PATH) < 0) ++ return -1; ++ ++ if (!path__is_absolute(dest)) { ++ errno = ENOENT; ++ return -1; ++ } ++ ++ /* Skip the drive letter specification ("C:") */ ++ if (git__utf8_to_16(dest + 2, MAX_PATH - 2, src) < 0) ++ return -1; ++ } ++ /* Relative paths */ ++ else { ++ int cwd_len; ++ ++ if ((cwd_len = git_win32_path__cwd(dest, MAX_PATH)) < 0) ++ return -1; ++ ++ dest[cwd_len++] = L'\\'; ++ ++ if (git__utf8_to_16(dest + cwd_len, MAX_PATH - cwd_len, src) < 0) ++ return -1; ++ } ++ ++ return git_win32_path_canonicalize(out); ++} ++ ++int git_win32_path_to_utf8(git_win32_utf8_path dest, const wchar_t *src) ++{ ++ char *out = dest; ++ int len; ++ ++ /* Strip NT namespacing "\\?\" */ ++ if (path__is_nt_namespace(src)) { ++ src += 4; ++ ++ /* "\\?\UNC\server\share" -> "\\server\share" */ ++ if (wcsncmp(src, L"UNC\\", 4) == 0) { ++ src += 4; ++ ++ memcpy(dest, "\\\\", 2); ++ out = dest + 2; ++ } ++ } ++ ++ if ((len = git__utf16_to_8(out, GIT_WIN_PATH_UTF8, src)) < 0) ++ return len; ++ ++ git_path_mkposix(dest); ++ ++ return len; ++} ++ ++char *git_win32_path_8dot3_name(const char *path) ++{ ++ git_win32_path longpath, shortpath; ++ wchar_t *start; ++ char *shortname; ++ int len, namelen = 1; ++ ++ if (git_win32_path_from_utf8(longpath, path) < 0) ++ return NULL; ++ ++ len = GetShortPathNameW(longpath, shortpath, GIT_WIN_PATH_UTF16); ++ ++ while (len && shortpath[len-1] == L'\\') ++ shortpath[--len] = L'\0'; ++ ++ if (len == 0 || len >= GIT_WIN_PATH_UTF16) ++ return NULL; ++ ++ for (start = shortpath + (len - 1); ++ start > shortpath && *(start-1) != '/' && *(start-1) != '\\'; ++ start--) ++ namelen++; ++ ++ /* We may not have actually been given a short name. But if we have, ++ * it will be in the ASCII byte range, so we don't need to worry about ++ * multi-byte sequences and can allocate naively. ++ */ ++ if (namelen > 12 || (shortname = git__malloc(namelen + 1)) == NULL) ++ return NULL; ++ ++ if ((len = git__utf16_to_8(shortname, namelen + 1, start)) < 0) ++ return NULL; ++ ++ return shortname; ++} +diff --git a/src/win32/path_w32.h b/src/win32/path_w32.h +new file mode 100644 +index 0000000..1d10166 +--- /dev/null ++++ b/src/win32/path_w32.h +@@ -0,0 +1,80 @@ ++/* ++ * Copyright (C) the libgit2 contributors. All rights reserved. ++ * ++ * This file is part of libgit2, distributed under the GNU GPL v2 with ++ * a Linking Exception. For full terms see the included COPYING file. ++ */ ++#ifndef INCLUDE_git_path_w32_h__ ++#define INCLUDE_git_path_w32_h__ ++ ++/* ++ * Provides a large enough buffer to support Windows paths: MAX_PATH is ++ * 260, corresponding to a maximum path length of 259 characters plus a ++ * NULL terminator. Prefixing with "\\?\" adds 4 characters, but if the ++ * original was a UNC path, then we turn "\\server\share" into ++ * "\\?\UNC\server\share". So we replace the first two characters with ++ * 8 characters, a net gain of 6, so the maximum length is MAX_PATH+6. ++ */ ++#define GIT_WIN_PATH_UTF16 MAX_PATH+6 ++ ++/* Maximum size of a UTF-8 Win32 path. We remove the "\\?\" or "\\?\UNC\" ++ * prefixes for presentation, bringing us back to 259 (non-NULL) ++ * characters. UTF-8 does have 4-byte sequences, but they are encoded in ++ * UTF-16 using surrogate pairs, which takes up the space of two characters. ++ * Two characters in the range U+0800 -> U+FFFF take up more space in UTF-8 ++ * (6 bytes) than one surrogate pair (4 bytes). ++ */ ++#define GIT_WIN_PATH_UTF8 (259 * 3 + 1) ++ ++/* ++ * The length of a Windows "shortname", for 8.3 compatibility. ++ */ ++#define GIT_WIN_PATH_SHORTNAME 13 ++ ++/* Win32 path types */ ++typedef wchar_t git_win32_path[GIT_WIN_PATH_UTF16]; ++typedef char git_win32_utf8_path[GIT_WIN_PATH_UTF8]; ++ ++/** ++ * Create a Win32 path (in UCS-2 format) from a UTF-8 string. ++ * ++ * @param dest The buffer to receive the wide string. ++ * @param src The UTF-8 string to convert. ++ * @return The length of the wide string, in characters (not counting the NULL terminator), or < 0 for failure ++ */ ++extern int git_win32_path_from_utf8(git_win32_path dest, const char *src); ++ ++/** ++ * Canonicalize a Win32 UCS-2 path so that it is suitable for delivery to the ++ * Win32 APIs: remove multiple directory separators, squashing to a single one, ++ * strip trailing directory separators, ensure directory separators are all ++ * canonical (always backslashes, never forward slashes) and process any ++ * directory entries of '.' or '..'. ++ * ++ * This processes the buffer in place. ++ * ++ * @param path The buffer to process ++ * @return The new length of the buffer, in wchar_t's (not counting the NULL terminator) ++ */ ++extern int git_win32_path_canonicalize(git_win32_path path); ++ ++/** ++ * Create an internal format (posix-style) UTF-8 path from a Win32 UCS-2 path. ++ * ++ * @param dest The buffer to receive the UTF-8 string. ++ * @param src The wide string to convert. ++ * @return The length of the UTF-8 string, in bytes (not counting the NULL terminator), or < 0 for failure ++ */ ++extern int git_win32_path_to_utf8(git_win32_utf8_path dest, const wchar_t *src); ++ ++/** ++ * Get the short name for the terminal path component in the given path. ++ * For example, given "C:\Foo\Bar\Asdf.txt", this will return the short name ++ * for the file "Asdf.txt". ++ * ++ * @param path The given path in UTF-8 ++ * @return The name of the shortname for the given path ++ */ ++extern char *git_win32_path_8dot3_name(const char *path); ++ ++#endif +diff --git a/src/win32/posix.h b/src/win32/posix.h +index 2cbea18..ea56d4f 100644 +--- a/src/win32/posix.h ++++ b/src/win32/posix.h +@@ -9,6 +9,7 @@ + + #include "common.h" + #include "../posix.h" ++#include "path_w32.h" + #include "utf-conv.h" + #include "dir.h" + +diff --git a/src/win32/posix_w32.c b/src/win32/posix_w32.c +index 3493843..0f91294 100644 +--- a/src/win32/posix_w32.c ++++ b/src/win32/posix_w32.c +@@ -7,6 +7,7 @@ + #include "../posix.h" + #include "../fileops.h" + #include "path.h" ++#include "path_w32.h" + #include "utf-conv.h" + #include "repository.h" + #include "reparse.h" +@@ -31,29 +32,13 @@ + /* GetFinalPathNameByHandleW signature */ + typedef DWORD(WINAPI *PFGetFinalPathNameByHandleW)(HANDLE, LPWSTR, DWORD, DWORD); + +-/* Helper function which converts UTF-8 paths to UTF-16. +- * On failure, errno is set. */ +-static int utf8_to_16_with_errno(git_win32_path dest, const char *src) +-{ +- int len = git_win32_path_from_utf8(dest, src); +- +- if (len < 0) { +- if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) +- errno = ENAMETOOLONG; +- else +- errno = EINVAL; /* Bad code point, presumably */ +- } +- +- return len; +-} +- + int p_mkdir(const char *path, mode_t mode) + { + git_win32_path buf; + + GIT_UNUSED(mode); + +- if (utf8_to_16_with_errno(buf, path) < 0) ++ if (git_win32_path_from_utf8(buf, path) < 0) + return -1; + + return _wmkdir(buf); +@@ -64,7 +49,7 @@ int p_unlink(const char *path) + git_win32_path buf; + int error; + +- if (utf8_to_16_with_errno(buf, path) < 0) ++ if (git_win32_path_from_utf8(buf, path) < 0) + return -1; + + error = _wunlink(buf); +@@ -260,7 +245,7 @@ static int do_lstat(const char *path, struct stat *buf, bool posixly_correct) + git_win32_path path_w; + int len; + +- if ((len = utf8_to_16_with_errno(path_w, path)) < 0) ++ if ((len = git_win32_path_from_utf8(path_w, path)) < 0) + return -1; + + git_win32__path_trim_end(path_w, len); +@@ -291,7 +276,7 @@ int p_readlink(const char *path, char *buf, size_t bufsiz) + * could occur in the middle of the encoding of a code point, + * we need to buffer the result on the stack. */ + +- if (utf8_to_16_with_errno(path_w, path) < 0 || ++ if (git_win32_path_from_utf8(path_w, path) < 0 || + readlink_w(target_w, path_w) < 0 || + (len = git_win32_path_to_utf8(target, target_w)) < 0) + return -1; +@@ -315,7 +300,7 @@ int p_open(const char *path, int flags, ...) + git_win32_path buf; + mode_t mode = 0; + +- if (utf8_to_16_with_errno(buf, path) < 0) ++ if (git_win32_path_from_utf8(buf, path) < 0) + return -1; + + if (flags & O_CREAT) { +@@ -333,7 +318,7 @@ int p_creat(const char *path, mode_t mode) + { + git_win32_path buf; + +- if (utf8_to_16_with_errno(buf, path) < 0) ++ if (git_win32_path_from_utf8(buf, path) < 0) + return -1; + + return _wopen(buf, _O_WRONLY | _O_CREAT | _O_TRUNC | STANDARD_OPEN_FLAGS, mode); +@@ -431,7 +416,7 @@ int p_stat(const char* path, struct stat* buf) + git_win32_path path_w; + int len; + +- if ((len = utf8_to_16_with_errno(path_w, path)) < 0) ++ if ((len = git_win32_path_from_utf8(path_w, path)) < 0) + return -1; + + git_win32__path_trim_end(path_w, len); +@@ -451,7 +436,7 @@ int p_chdir(const char* path) + { + git_win32_path buf; + +- if (utf8_to_16_with_errno(buf, path) < 0) ++ if (git_win32_path_from_utf8(buf, path) < 0) + return -1; + + return _wchdir(buf); +@@ -461,7 +446,7 @@ int p_chmod(const char* path, mode_t mode) + { + git_win32_path buf; + +- if (utf8_to_16_with_errno(buf, path) < 0) ++ if (git_win32_path_from_utf8(buf, path) < 0) + return -1; + + return _wchmod(buf, mode); +@@ -472,7 +457,7 @@ int p_rmdir(const char* path) + git_win32_path buf; + int error; + +- if (utf8_to_16_with_errno(buf, path) < 0) ++ if (git_win32_path_from_utf8(buf, path) < 0) + return -1; + + error = _wrmdir(buf); +@@ -501,7 +486,7 @@ char *p_realpath(const char *orig_path, char *buffer) + { + git_win32_path orig_path_w, buffer_w; + +- if (utf8_to_16_with_errno(orig_path_w, orig_path) < 0) ++ if (git_win32_path_from_utf8(orig_path_w, orig_path) < 0) + return NULL; + + /* Note that if the path provided is a relative path, then the current directory +@@ -522,20 +507,17 @@ char *p_realpath(const char *orig_path, char *buffer) + return NULL; + } + +- /* Convert the path to UTF-8. */ +- if (buffer) { +- /* If the caller provided a buffer, then it is assumed to be GIT_WIN_PATH_UTF8 +- * characters in size. If it isn't, then we may overflow. */ +- if (git__utf16_to_8(buffer, GIT_WIN_PATH_UTF8, buffer_w) < 0) +- return NULL; +- } else { +- /* If the caller did not provide a buffer, then we allocate one for the caller +- * from the heap. */ +- if (git__utf16_to_8_alloc(&buffer, buffer_w) < 0) +- return NULL; ++ if (!buffer && !(buffer = git__malloc(GIT_WIN_PATH_UTF8))) { ++ errno = ENOMEM; ++ return NULL; + } + +- /* Convert backslashes to forward slashes */ ++ /* Convert the path to UTF-8. If the caller provided a buffer, then it ++ * is assumed to be GIT_WIN_PATH_UTF8 characters in size. If it isn't, ++ * then we may overflow. */ ++ if (git_win32_path_to_utf8(buffer, buffer_w) < 0) ++ return NULL; ++ + git_path_mkposix(buffer); + + return buffer; +@@ -568,6 +550,7 @@ int p_snprintf(char *buffer, size_t count, const char *format, ...) + return r; + } + ++/* TODO: wut? */ + int p_mkstemp(char *tmp_path) + { + #if defined(_MSC_VER) +@@ -585,7 +568,7 @@ int p_access(const char* path, mode_t mode) + { + git_win32_path buf; + +- if (utf8_to_16_with_errno(buf, path) < 0) ++ if (git_win32_path_from_utf8(buf, path) < 0) + return -1; + + return _waccess(buf, mode); +@@ -599,8 +582,8 @@ int p_rename(const char *from, const char *to) + int rename_succeeded; + int error; + +- if (utf8_to_16_with_errno(wfrom, from) < 0 || +- utf8_to_16_with_errno(wto, to) < 0) ++ if (git_win32_path_from_utf8(wfrom, from) < 0 || ++ git_win32_path_from_utf8(wto, to) < 0) + return -1; + + /* wait up to 50ms if file is locked by another thread or process */ +diff --git a/src/win32/utf-conv.c b/src/win32/utf-conv.c +index b9ccfb5..b0205b0 100644 +--- a/src/win32/utf-conv.c ++++ b/src/win32/utf-conv.c +@@ -26,6 +26,14 @@ GIT_INLINE(DWORD) get_wc_flags(void) + return flags; + } + ++GIT_INLINE(void) git__set_errno(void) ++{ ++ if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) ++ errno = ENAMETOOLONG; ++ else ++ errno = EINVAL; ++} ++ + /** + * Converts a UTF-8 string to wide characters. + * +@@ -36,10 +44,15 @@ GIT_INLINE(DWORD) get_wc_flags(void) + */ + int git__utf8_to_16(wchar_t *dest, size_t dest_size, const char *src) + { ++ int len; ++ + /* Length of -1 indicates NULL termination of the input string. Subtract 1 from the result to + * turn 0 into -1 (an error code) and to not count the NULL terminator as part of the string's + * length. MultiByteToWideChar never returns int's minvalue, so underflow is not possible */ +- return MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, src, -1, dest, (int)dest_size) - 1; ++ if ((len = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, src, -1, dest, (int)dest_size) - 1) < 0) ++ git__set_errno(); ++ ++ return len; + } + + /** +@@ -52,10 +65,15 @@ int git__utf8_to_16(wchar_t *dest, size_t dest_size, const char *src) + */ + int git__utf16_to_8(char *dest, size_t dest_size, const wchar_t *src) + { ++ int len; ++ + /* Length of -1 indicates NULL termination of the input string. Subtract 1 from the result to + * turn 0 into -1 (an error code) and to not count the NULL terminator as part of the string's + * length. WideCharToMultiByte never returns int's minvalue, so underflow is not possible */ +- return WideCharToMultiByte(CP_UTF8, get_wc_flags(), src, -1, dest, (int)dest_size, NULL, NULL) - 1; ++ if ((len = WideCharToMultiByte(CP_UTF8, get_wc_flags(), src, -1, dest, (int)dest_size, NULL, NULL) - 1) < 0) ++ git__set_errno(); ++ ++ return len; + } + + /** +@@ -76,17 +94,23 @@ int git__utf8_to_16_alloc(wchar_t **dest, const char *src) + /* Length of -1 indicates NULL termination of the input string */ + utf16_size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, src, -1, NULL, 0); + +- if (!utf16_size) ++ if (!utf16_size) { ++ git__set_errno(); + return -1; ++ } + + *dest = git__malloc(utf16_size * sizeof(wchar_t)); + +- if (!*dest) ++ if (!*dest) { ++ errno = ENOMEM; + return -1; ++ } + + utf16_size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, src, -1, *dest, utf16_size); + + if (!utf16_size) { ++ git__set_errno(); ++ + git__free(*dest); + *dest = NULL; + } +@@ -116,17 +140,23 @@ int git__utf16_to_8_alloc(char **dest, const wchar_t *src) + /* Length of -1 indicates NULL termination of the input string */ + utf8_size = WideCharToMultiByte(CP_UTF8, dwFlags, src, -1, NULL, 0, NULL, NULL); + +- if (!utf8_size) ++ if (!utf8_size) { ++ git__set_errno(); + return -1; ++ } + + *dest = git__malloc(utf8_size); + +- if (!*dest) ++ if (!*dest) { ++ errno = ENOMEM; + return -1; ++ } + + utf8_size = WideCharToMultiByte(CP_UTF8, dwFlags, src, -1, *dest, utf8_size, NULL, NULL); + + if (!utf8_size) { ++ git__set_errno(); ++ + git__free(*dest); + *dest = NULL; + } +diff --git a/src/win32/utf-conv.h b/src/win32/utf-conv.h +index a480cd9..89cdb96 100644 +--- a/src/win32/utf-conv.h ++++ b/src/win32/utf-conv.h +@@ -10,21 +10,6 @@ + #include <wchar.h> + #include "common.h" + +-/* Equal to the Win32 MAX_PATH constant. The maximum path length is 259 +- * characters plus a NULL terminator. */ +-#define GIT_WIN_PATH_UTF16 260 +- +-/* Maximum size of a UTF-8 Win32 path. UTF-8 does have 4-byte sequences, +- * but they are encoded in UTF-16 using surrogate pairs, which takes up +- * the space of two characters. Two characters in the range U+0800 -> +- * U+FFFF take up more space in UTF-8 (6 bytes) than one surrogate pair +- * (4 bytes). */ +-#define GIT_WIN_PATH_UTF8 (259 * 3 + 1) +- +-/* Win32 path types */ +-typedef wchar_t git_win32_path[GIT_WIN_PATH_UTF16]; +-typedef char git_win32_utf8_path[GIT_WIN_PATH_UTF8]; +- + /** + * Converts a UTF-8 string to wide characters. + * +@@ -67,28 +52,4 @@ int git__utf8_to_16_alloc(wchar_t **dest, const char *src); + */ + int git__utf16_to_8_alloc(char **dest, const wchar_t *src); + +-/** +- * Converts a UTF-8 Win32 path to wide characters. +- * +- * @param dest The buffer to receive the wide string. +- * @param src The UTF-8 string to convert. +- * @return The length of the wide string, in characters (not counting the NULL terminator), or < 0 for failure +- */ +-GIT_INLINE(int) git_win32_path_from_utf8(git_win32_path dest, const char *src) +-{ +- return git__utf8_to_16(dest, GIT_WIN_PATH_UTF16, src); +-} +- +-/** +- * Converts a wide Win32 path to UTF-8. +- * +- * @param dest The buffer to receive the UTF-8 string. +- * @param src The wide string to convert. +- * @return The length of the UTF-8 string, in bytes (not counting the NULL terminator), or < 0 for failure +- */ +-GIT_INLINE(int) git_win32_path_to_utf8(git_win32_utf8_path dest, const wchar_t *src) +-{ +- return git__utf16_to_8(dest, GIT_WIN_PATH_UTF8, src); +-} +- + #endif +diff --git a/src/win32/w32_util.h b/src/win32/w32_util.h +index a1d388a..9c1b943 100644 +--- a/src/win32/w32_util.h ++++ b/src/win32/w32_util.h +@@ -9,6 +9,7 @@ + #define INCLUDE_w32_util_h__ + + #include "utf-conv.h" ++#include "path_w32.h" + + GIT_INLINE(bool) git_win32__isalpha(wchar_t c) + { +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)); ++} +diff --git a/tests/path/win32.c b/tests/path/win32.c +new file mode 100644 +index 0000000..22742f8 +--- /dev/null ++++ b/tests/path/win32.c +@@ -0,0 +1,214 @@ ++ ++#include "clar_libgit2.h" ++#include "path.h" ++ ++#ifdef GIT_WIN32 ++#include "win32/path_w32.h" ++#endif ++ ++void test_utf8_to_utf16(const char *utf8_in, const wchar_t *utf16_expected) ++{ ++#ifdef GIT_WIN32 ++ git_win32_path path_utf16; ++ int path_utf16len; ++ ++ cl_assert((path_utf16len = git_win32_path_from_utf8(path_utf16, utf8_in)) >= 0); ++ cl_assert_equal_wcs(utf16_expected, path_utf16); ++ cl_assert_equal_i(wcslen(utf16_expected), path_utf16len); ++#else ++ GIT_UNUSED(utf8_in); ++ GIT_UNUSED(utf16_expected); ++#endif ++} ++ ++void test_path_win32__utf8_to_utf16(void) ++{ ++#ifdef GIT_WIN32 ++ test_utf8_to_utf16("C:\\", L"\\\\?\\C:\\"); ++ test_utf8_to_utf16("c:\\", L"\\\\?\\c:\\"); ++ test_utf8_to_utf16("C:/", L"\\\\?\\C:\\"); ++ test_utf8_to_utf16("c:/", L"\\\\?\\c:\\"); ++#endif ++} ++ ++void test_path_win32__removes_trailing_slash(void) ++{ ++#ifdef GIT_WIN32 ++ test_utf8_to_utf16("C:\\Foo\\", L"\\\\?\\C:\\Foo"); ++ test_utf8_to_utf16("C:\\Foo\\\\", L"\\\\?\\C:\\Foo"); ++ test_utf8_to_utf16("C:\\Foo\\\\", L"\\\\?\\C:\\Foo"); ++ test_utf8_to_utf16("C:/Foo/", L"\\\\?\\C:\\Foo"); ++ test_utf8_to_utf16("C:/Foo///", L"\\\\?\\C:\\Foo"); ++#endif ++} ++ ++void test_path_win32__squashes_multiple_slashes(void) ++{ ++#ifdef GIT_WIN32 ++ test_utf8_to_utf16("C:\\\\Foo\\Bar\\\\Foobar", L"\\\\?\\C:\\Foo\\Bar\\Foobar"); ++ test_utf8_to_utf16("C://Foo/Bar///Foobar", L"\\\\?\\C:\\Foo\\Bar\\Foobar"); ++#endif ++} ++ ++void test_path_win32__unc(void) ++{ ++#ifdef GIT_WIN32 ++ test_utf8_to_utf16("\\\\server\\c$\\unc\\path", L"\\\\?\\UNC\\server\\c$\\unc\\path"); ++ test_utf8_to_utf16("//server/git/style/unc/path", L"\\\\?\\UNC\\server\\git\\style\\unc\\path"); ++#endif ++} ++ ++void test_path_win32__honors_max_path(void) ++{ ++#ifdef GIT_WIN32 ++ git_win32_path path_utf16; ++ ++ test_utf8_to_utf16("C:\\This path is 259 chars and is the max length in windows\\0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij", ++ L"\\\\?\\C:\\This path is 259 chars and is the max length in windows\\0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij"); ++ test_utf8_to_utf16("\\\\unc\\paths may also be 259 characters including the server\\123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij", ++ L"\\\\?\\UNC\\unc\\paths may also be 259 characters including the server\\123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij"); ++ ++ cl_check_fail(git_win32_path_from_utf8(path_utf16, "C:\\This path is 260 chars and is sadly too long for windows\\0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij")); ++ cl_check_fail(git_win32_path_from_utf8(path_utf16, "\\\\unc\\paths are also bound by 260 character restrictions\\including the server name portion\\bcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij0123456789abcdefghij")); ++#endif ++} ++ ++void test_path_win32__dot_and_dotdot(void) ++{ ++#ifdef GIT_WIN32 ++ test_utf8_to_utf16("C:\\Foo\\..\\Foobar", L"\\\\?\\C:\\Foobar"); ++ test_utf8_to_utf16("C:\\Foo\\Bar\\..\\Foobar", L"\\\\?\\C:\\Foo\\Foobar"); ++ test_utf8_to_utf16("C:\\Foo\\Bar\\..\\Foobar\\..", L"\\\\?\\C:\\Foo"); ++ test_utf8_to_utf16("C:\\Foobar\\..", L"\\\\?\\C:\\"); ++ test_utf8_to_utf16("C:/Foo/Bar/../Foobar", L"\\\\?\\C:\\Foo\\Foobar"); ++ test_utf8_to_utf16("C:/Foo/Bar/../Foobar/../Asdf/", L"\\\\?\\C:\\Foo\\Asdf"); ++ test_utf8_to_utf16("C:/Foo/Bar/../Foobar/..", L"\\\\?\\C:\\Foo"); ++ test_utf8_to_utf16("C:/Foo/..", L"\\\\?\\C:\\"); ++ ++ test_utf8_to_utf16("C:\\Foo\\Bar\\.\\Foobar", L"\\\\?\\C:\\Foo\\Bar\\Foobar"); ++ test_utf8_to_utf16("C:\\.\\Foo\\.\\Bar\\.\\Foobar\\.\\", L"\\\\?\\C:\\Foo\\Bar\\Foobar"); ++ test_utf8_to_utf16("C:/Foo/Bar/./Foobar", L"\\\\?\\C:\\Foo\\Bar\\Foobar"); ++ test_utf8_to_utf16("C:/Foo/../Bar/./Foobar/../", L"\\\\?\\C:\\Bar"); ++ ++ test_utf8_to_utf16("C:\\Foo\\..\\..\\Bar", L"\\\\?\\C:\\Bar"); ++#endif ++} ++ ++void test_path_win32__absolute_from_no_drive_letter(void) ++{ ++#ifdef GIT_WIN32 ++ test_utf8_to_utf16("\\Foo", L"\\\\?\\C:\\Foo"); ++ test_utf8_to_utf16("\\Foo\\Bar", L"\\\\?\\C:\\Foo\\Bar"); ++ test_utf8_to_utf16("/Foo/Bar", L"\\\\?\\C:\\Foo\\Bar"); ++#endif ++} ++ ++void test_path_win32__absolute_from_relative(void) ++{ ++#ifdef GIT_WIN32 ++ char cwd_backup[MAX_PATH]; ++ ++ cl_must_pass(p_getcwd(cwd_backup, MAX_PATH)); ++ cl_must_pass(p_chdir("C:/")); ++ ++ test_utf8_to_utf16("Foo", L"\\\\?\\C:\\Foo"); ++ test_utf8_to_utf16("..\\..\\Foo", L"\\\\?\\C:\\Foo"); ++ test_utf8_to_utf16("Foo\\..", L"\\\\?\\C:\\"); ++ test_utf8_to_utf16("Foo\\..\\..", L"\\\\?\\C:\\"); ++ test_utf8_to_utf16("", L"\\\\?\\C:\\"); ++ ++ cl_must_pass(p_chdir("C:/Windows")); ++ ++ test_utf8_to_utf16("Foo", L"\\\\?\\C:\\Windows\\Foo"); ++ test_utf8_to_utf16("Foo\\Bar", L"\\\\?\\C:\\Windows\\Foo\\Bar"); ++ test_utf8_to_utf16("..\\Foo", L"\\\\?\\C:\\Foo"); ++ test_utf8_to_utf16("Foo\\..\\Bar", L"\\\\?\\C:\\Windows\\Bar"); ++ test_utf8_to_utf16("", L"\\\\?\\C:\\Windows"); ++ ++ cl_must_pass(p_chdir(cwd_backup)); ++#endif ++} ++ ++void test_canonicalize(const wchar_t *in, const wchar_t *expected) ++{ ++#ifdef GIT_WIN32 ++ git_win32_path canonical; ++ ++ cl_assert(wcslen(in) < MAX_PATH); ++ wcscpy(canonical, in); ++ ++ cl_must_pass(git_win32_path_canonicalize(canonical)); ++ cl_assert_equal_wcs(expected, canonical); ++#else ++ GIT_UNUSED(in); ++ GIT_UNUSED(expected); ++#endif ++} ++ ++void test_path_win32__canonicalize(void) ++{ ++#ifdef GIT_WIN32 ++ test_canonicalize(L"C:\\Foo\\Bar", L"C:\\Foo\\Bar"); ++ test_canonicalize(L"C:\\Foo\\", L"C:\\Foo"); ++ test_canonicalize(L"C:\\Foo\\\\", L"C:\\Foo"); ++ test_canonicalize(L"C:\\Foo\\..\\Bar", L"C:\\Bar"); ++ test_canonicalize(L"C:\\Foo\\..\\..\\Bar", L"C:\\Bar"); ++ test_canonicalize(L"C:\\Foo\\..\\..\\..\\..\\", L"C:\\"); ++ test_canonicalize(L"C:/Foo/Bar", L"C:\\Foo\\Bar"); ++ test_canonicalize(L"C:/", L"C:\\"); ++ ++ test_canonicalize(L"Foo\\\\Bar\\\\Asdf\\\\", L"Foo\\Bar\\Asdf"); ++ test_canonicalize(L"Foo\\\\Bar\\\\..\\\\Asdf\\", L"Foo\\Asdf"); ++ test_canonicalize(L"Foo\\\\Bar\\\\.\\\\Asdf\\", L"Foo\\Bar\\Asdf"); ++ test_canonicalize(L"Foo\\\\..\\Bar\\\\.\\\\Asdf\\", L"Bar\\Asdf"); ++ test_canonicalize(L"\\", L""); ++ test_canonicalize(L"", L""); ++ test_canonicalize(L"Foo\\..\\..\\..\\..", L""); ++ test_canonicalize(L"..\\..\\..\\..", L""); ++ test_canonicalize(L"\\..\\..\\..\\..", L""); ++ ++ test_canonicalize(L"\\\\?\\C:\\Foo\\Bar", L"\\\\?\\C:\\Foo\\Bar"); ++ test_canonicalize(L"\\\\?\\C:\\Foo\\Bar\\", L"\\\\?\\C:\\Foo\\Bar"); ++ test_canonicalize(L"\\\\?\\C:\\\\Foo\\.\\Bar\\\\..\\", L"\\\\?\\C:\\Foo"); ++ test_canonicalize(L"\\\\?\\C:\\\\", L"\\\\?\\C:\\"); ++ test_canonicalize(L"//?/C:/", L"\\\\?\\C:\\"); ++ test_canonicalize(L"//?/C:/../../Foo/", L"\\\\?\\C:\\Foo"); ++ test_canonicalize(L"//?/C:/Foo/../../", L"\\\\?\\C:\\"); ++ ++ test_canonicalize(L"\\\\?\\UNC\\server\\C$\\folder", L"\\\\?\\UNC\\server\\C$\\folder"); ++ test_canonicalize(L"\\\\?\\UNC\\server\\C$\\folder\\", L"\\\\?\\UNC\\server\\C$\\folder"); ++ test_canonicalize(L"\\\\?\\UNC\\server\\C$\\folder\\", L"\\\\?\\UNC\\server\\C$\\folder"); ++ test_canonicalize(L"\\\\?\\UNC\\server\\C$\\folder\\..\\..\\..\\..\\share\\", L"\\\\?\\UNC\\server\\share"); ++ ++ test_canonicalize(L"\\\\server\\share", L"\\\\server\\share"); ++ test_canonicalize(L"\\\\server\\share\\", L"\\\\server\\share"); ++ test_canonicalize(L"\\\\server\\share\\\\foo\\\\bar", L"\\\\server\\share\\foo\\bar"); ++ test_canonicalize(L"\\\\server\\\\share\\\\foo\\\\bar", L"\\\\server\\share\\foo\\bar"); ++ test_canonicalize(L"\\\\server\\share\\..\\foo", L"\\\\server\\foo"); ++ test_canonicalize(L"\\\\server\\..\\..\\share\\.\\foo", L"\\\\server\\share\\foo"); ++#endif ++} ++ ++void test_path_win32__8dot3_name(void) ++{ ++#ifdef GIT_WIN32 ++ char *shortname; ++ ++ /* Some guaranteed short names */ ++ cl_assert_equal_s("PROGRA~1", (shortname = git_win32_path_8dot3_name("C:\\Program Files"))); ++ git__free(shortname); ++ ++ cl_assert_equal_s("WINDOWS", (shortname = git_win32_path_8dot3_name("C:\\WINDOWS"))); ++ git__free(shortname); ++ ++ /* Create some predictible short names */ ++ cl_must_pass(p_mkdir(".foo", 0777)); ++ cl_assert_equal_s("FOO~1", (shortname = git_win32_path_8dot3_name(".foo"))); ++ git__free(shortname); ++ ++ cl_git_write2file("bar~1", "foobar\n", 7, O_RDWR|O_CREAT, 0666); ++ cl_must_pass(p_mkdir(".bar", 0777)); ++ cl_assert_equal_s("BAR~2", (shortname = git_win32_path_8dot3_name(".bar"))); ++ git__free(shortname); ++#endif ++} diff -Nru libgit2-0.21.1/debian/patches/series libgit2-0.21.1/debian/patches/series --- libgit2-0.21.1/debian/patches/series 2015-01-09 09:51:34.000000000 +1100 +++ libgit2-0.21.1/debian/patches/series 2015-02-11 23:09:15.000000000 +1100 @@ -1 +1,2 @@ disable_tests.patch +CVE-2014-9390.patch

