diff --git a/Makefile b/Makefile index 87505e5df83ebf..90146a1822a551 100644 --- a/Makefile +++ b/Makefile @@ -1247,6 +1247,7 @@ LIB_OBJS += parse-options.o LIB_OBJS += patch-delta.o LIB_OBJS += patch-ids.o LIB_OBJS += path.o +LIB_OBJS += path-trie.o LIB_OBJS += path-walk.o LIB_OBJS += pathspec.o LIB_OBJS += pkt-line.o @@ -1540,6 +1541,7 @@ CLAR_TEST_SUITES += u-odb-inmemory CLAR_TEST_SUITES += u-oid-array CLAR_TEST_SUITES += u-oidmap CLAR_TEST_SUITES += u-oidtree +CLAR_TEST_SUITES += u-path-trie CLAR_TEST_SUITES += u-prio-queue CLAR_TEST_SUITES += u-reftable-basics CLAR_TEST_SUITES += u-reftable-block diff --git a/compat/mingw.c b/compat/mingw.c index 92946af422770c..973d58656bc3b3 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -9,6 +9,7 @@ #include "dir.h" #include "environment.h" #include "gettext.h" +#include "path-trie.h" #include "repository.h" #include "run-command.h" #include "strbuf.h" @@ -448,43 +449,159 @@ process_phantom_symlink(const wchar_t *wtarget, const wchar_t *wlink) return PHANTOM_SYMLINK_RETRY; } -/* keep track of newly created symlinks to non-existing targets */ +/* + * Newly created symlinks to non-existing targets are indexed by a + * path trie keyed by their (canonicalized, absolute) target path, so + * that mkdir() creating that path -- or a symlink at that path + * turning out to be a directory symlink -- wakes only the entries + * actually waiting on it, instead of re-probing every phantom + * symlink created so far. A trie rather than a flat map because a + * phantom symlink's target may pass through *another* symlink; once + * that one resolves, everything registered through it is moved to + * the corresponding real path (path_trie_move()), where later + * mkdir()s can find it. + */ struct phantom_symlink_info { - struct phantom_symlink_info *next; + struct path_trie_entry ent; wchar_t *wlink; wchar_t *wtarget; }; -static struct phantom_symlink_info *phantom_symlinks = NULL; +static struct path_trie phantom_symlinks; static CRITICAL_SECTION phantom_symlinks_cs; -static void process_phantom_symlinks(void) +static void free_phantom_symlink(struct phantom_symlink_info *psi) { - struct phantom_symlink_info *current, **psi; + path_trie_entry_clear(&psi->ent); + free(psi->wlink); + free(psi->wtarget); + free(psi); +} + +static wchar_t *xwcsdup(const wchar_t *s) +{ + size_t size = sizeof(wchar_t) * (wcslen(s) + 1); + return memcpy(xmalloc(size), s, size); +} + +/* Returns the canonicalized, absolute UTF-8 form of wpath. */ +static char *canonicalize_path(const wchar_t *wpath) +{ + wchar_t wfullpath[MAX_LONG_PATH]; + char utf8[MAX_LONG_PATH * 3]; + int len = GetFullPathNameW(wpath, ARRAY_SIZE(wfullpath), wfullpath, NULL); + + if (!len || len >= ARRAY_SIZE(wfullpath) || + xwcstoutf(utf8, wfullpath, sizeof(utf8)) < 0) + return NULL; + return xstrdup(utf8); +} + +/* + * The canonicalized, absolute UTF-8 form of wtarget, resolved against + * wlink's directory if relative: the path process_phantom_symlink() + * itself probes, and thus the path whose creation can resolve the + * symlink. + */ +static char *canonicalize_target(const wchar_t *wtarget, const wchar_t *wlink) +{ + wchar_t relative[MAX_LONG_PATH]; + const wchar_t *rel = make_relative_to(wtarget, wlink, relative, + ARRAY_SIZE(relative)); + + return rel ? canonicalize_path(rel) : NULL; +} + +/* + * Wakes every phantom symlink registered at or below `key` (a + * canonicalized, absolute UTF-8 path; ownership is taken): the path + * just came into existence, which may let them resolve. Every + * resolved directory symlink queues its own target for waking in + * turn, since other phantom symlinks may point through it. + */ +static void process_phantom_symlinks_at(char *key) +{ + char **queue = NULL; + size_t queue_nr = 0, queue_alloc = 0; + + ALLOC_GROW(queue, queue_nr + 1, queue_alloc); + queue[queue_nr++] = key; + EnterCriticalSection(&phantom_symlinks_cs); - /* process phantom symlinks list */ - psi = &phantom_symlinks; - while ((current = *psi)) { - enum phantom_symlink_result result = process_phantom_symlink( - current->wtarget, current->wlink); - if (result == PHANTOM_SYMLINK_RETRY) { - psi = ¤t->next; - } else { - /* symlink was processed, remove from list */ - *psi = current->next; - free(current); - /* if symlink was a directory, start over */ - if (result == PHANTOM_SYMLINK_DIRECTORY) - psi = &phantom_symlinks; + while (queue_nr) { + char *current = queue[--queue_nr]; + struct path_trie_entry *drained = + path_trie_drain(&phantom_symlinks, current); + + while (drained) { + struct phantom_symlink_info *psi = container_of( + drained, struct phantom_symlink_info, ent); + + drained = drained->next; + + switch (process_phantom_symlink(psi->wtarget, + psi->wlink)) { + case PHANTOM_SYMLINK_RETRY: + path_trie_add(&phantom_symlinks, psi->ent.key, + &psi->ent); + break; + case PHANTOM_SYMLINK_DIRECTORY: { + /* + * This symlink is now a directory symlink; + * anything registered through its own path + * is reachable via its target from now on, + * and may be able to resolve. + */ + char *own = canonicalize_path(psi->wlink); + + if (own) { + path_trie_move(&phantom_symlinks, own, + psi->ent.key); + free(own); + } + ALLOC_GROW(queue, queue_nr + 1, queue_alloc); + queue[queue_nr++] = xstrdup(psi->ent.key); + free_phantom_symlink(psi); + break; + } + default: + free_phantom_symlink(psi); + break; + } } + free(current); } LeaveCriticalSection(&phantom_symlinks_cs); + free(queue); } -static int create_phantom_symlink(wchar_t *wtarget, wchar_t *wlink) +static void process_phantom_symlinks(const wchar_t *wpath) { - int len; + char *key = canonicalize_path(wpath); + if (key) + process_phantom_symlinks_at(key); +} + +/* A directory symlink wlink -> wtarget was created (or so resolved). */ +static void directory_symlink_created(const wchar_t *wtarget, + const wchar_t *wlink) +{ + char *own = canonicalize_path(wlink); + char *target_key = canonicalize_target(wtarget, wlink); + + if (own && target_key) { + EnterCriticalSection(&phantom_symlinks_cs); + path_trie_move(&phantom_symlinks, own, target_key); + LeaveCriticalSection(&phantom_symlinks_cs); + } + free(own); + if (target_key) + process_phantom_symlinks_at(target_key); +} + +static int create_phantom_symlink(wchar_t *wtarget, wchar_t *wlink) +{ /* create file symlink */ if (!CreateSymbolicLinkW(wlink, wtarget, symlink_file_flags)) { errno = err_win_to_posix(GetLastError()); @@ -494,34 +611,35 @@ static int create_phantom_symlink(wchar_t *wtarget, wchar_t *wlink) /* convert to directory symlink if target exists */ switch (process_phantom_symlink(wtarget, wlink)) { case PHANTOM_SYMLINK_RETRY: { - /* if target doesn't exist, add to phantom symlinks list */ - wchar_t wfullpath[MAX_LONG_PATH]; + wchar_t wfulllink[MAX_LONG_PATH]; + char *target_key = canonicalize_target(wtarget, wlink); struct phantom_symlink_info *psi; + int len; + + if (!target_key) + break; /* convert to absolute path to be independent of cwd */ - len = GetFullPathNameW(wlink, MAX_LONG_PATH, wfullpath, NULL); - if (!len || len >= MAX_LONG_PATH) { + len = GetFullPathNameW(wlink, ARRAY_SIZE(wfulllink), wfulllink, NULL); + if (!len || len >= ARRAY_SIZE(wfulllink)) { errno = err_win_to_posix(GetLastError()); + free(target_key); return -1; } - /* over-allocate and fill phantom_symlink_info structure */ - psi = xmalloc(sizeof(struct phantom_symlink_info) + - sizeof(wchar_t) * (len + wcslen(wtarget) + 2)); - psi->wlink = (wchar_t *)(psi + 1); - wcscpy(psi->wlink, wfullpath); - psi->wtarget = psi->wlink + len + 1; - wcscpy(psi->wtarget, wtarget); + psi = xcalloc(1, sizeof(*psi)); + psi->wlink = xwcsdup(wfulllink); + psi->wtarget = xwcsdup(wtarget); EnterCriticalSection(&phantom_symlinks_cs); - psi->next = phantom_symlinks; - phantom_symlinks = psi; + path_trie_add(&phantom_symlinks, target_key, &psi->ent); LeaveCriticalSection(&phantom_symlinks_cs); + free(target_key); break; } case PHANTOM_SYMLINK_DIRECTORY: - /* if we created a dir symlink, process other phantom symlinks */ - process_phantom_symlinks(); + /* if we created a dir symlink, wake others waiting on it */ + directory_symlink_created(wtarget, wlink); break; default: break; @@ -759,7 +877,7 @@ int mingw_mkdir(const char *path, int mode UNUSED) ret = _wmkdir(wpath); if (!ret) - process_phantom_symlinks(); + process_phantom_symlinks(wpath); if (!ret && needs_hiding(path)) return set_hidden_flag(wpath, 1); return ret; @@ -3495,7 +3613,7 @@ int mingw_create_symlink(struct index_state *index, const char *target, const ch break; /* There may be dangling phantom symlinks that point at this * one, which should now morph into directory symlinks. */ - process_phantom_symlinks(); + directory_symlink_created(wtarget, wlink); return 0; default: BUG("unhandled symlink type"); @@ -4448,6 +4566,7 @@ int wmain(int argc, const wchar_t **wargv) /* initialize critical section for waitpid pinfo_t list */ InitializeCriticalSection(&pinfo_cs); InitializeCriticalSection(&phantom_symlinks_cs); + path_trie_init(&phantom_symlinks, 1); /* initialize critical section for fscache */ InitializeCriticalSection(&fscache_cs); diff --git a/path-trie.c b/path-trie.c new file mode 100644 index 00000000000000..6f8134f1fee145 --- /dev/null +++ b/path-trie.c @@ -0,0 +1,253 @@ +#include "git-compat-util.h" +#include "path-trie.h" +#include "strbuf.h" + +struct path_trie_node { + struct hashmap_entry ent; /* in the parent node's `children` */ + struct hashmap children; + struct path_trie_entry *entries; + char component[FLEX_ARRAY]; +}; + +static int node_cmp(const void *cmp_data, + const struct hashmap_entry *eptr, + const struct hashmap_entry *entry_or_key, + const void *keydata) +{ + const unsigned int icase = *(const unsigned int *)cmp_data; + const struct path_trie_node *e = + container_of(eptr, const struct path_trie_node, ent); + const char *key = keydata ? keydata : + container_of(entry_or_key, const struct path_trie_node, + ent)->component; + + return icase ? strcasecmp(e->component, key) : + strcmp(e->component, key); +} + +static unsigned int component_hash(const struct path_trie *trie, + const char *component) +{ + return trie->icase ? strihash(component) : strhash(component); +} + +static struct path_trie_node *make_node(struct path_trie *trie, + const char *component) +{ + struct path_trie_node *node; + + FLEX_ALLOC_STR(node, component, component); + hashmap_init(&node->children, node_cmp, &trie->icase, 0); + hashmap_entry_init(&node->ent, component_hash(trie, component)); + return node; +} + +static struct path_trie_node *get_child(struct path_trie *trie, + struct path_trie_node *node, + const char *component, int create) +{ + unsigned int hash = component_hash(trie, component); + struct path_trie_node *child = hashmap_get_entry_from_hash( + &node->children, hash, component, + struct path_trie_node, ent); + + if (!child && create) { + child = make_node(trie, component); + hashmap_add(&node->children, &child->ent); + } + return child; +} + +/* + * Walks the trie to the node for `path`, optionally creating missing + * nodes on the way. Returns NULL if the node does not exist (and + * `create` is not set), or if `path` contains no components at all. + */ +static struct path_trie_node *walk(struct path_trie *trie, const char *path, + int create) +{ + struct path_trie_node *node = trie->root; + struct strbuf component = STRBUF_INIT; + const char *p = path; + + while (*p && node) { + const char *start; + + while (is_dir_sep(*p)) + p++; + if (!*p) + break; + start = p; + while (*p && !is_dir_sep(*p)) + p++; + + strbuf_reset(&component); + strbuf_add(&component, start, p - start); + node = get_child(trie, node, component.buf, create); + } + + strbuf_release(&component); + return node == trie->root ? NULL : node; +} + +void path_trie_init(struct path_trie *trie, int icase) +{ + trie->icase = !!icase; + trie->root = make_node(trie, ""); +} + +static void free_node(struct path_trie_node *node) +{ + struct hashmap_iter iter; + struct path_trie_node *child; + + hashmap_for_each_entry(&node->children, &iter, child, ent) + free_node(child); + hashmap_clear(&node->children); + free(node); +} + +void path_trie_clear(struct path_trie *trie) +{ + if (trie->root) { + free_node(trie->root); + trie->root = NULL; + } +} + +void path_trie_add(struct path_trie *trie, const char *path, + struct path_trie_entry *entry) +{ + struct path_trie_node *node = walk(trie, path, 1); + char *key = xstrdup(path); /* `path` may be `entry->key` itself */ + + if (!node) + BUG("cannot add to path trie under '%s'", path); + free(entry->key); + entry->key = key; + entry->next = node->entries; + node->entries = entry; +} + +/* Unlinks and returns all entries at `node` and below, prepended to `list`. */ +static struct path_trie_entry *drain_node(struct path_trie_node *node, + struct path_trie_entry *list) +{ + struct hashmap_iter iter; + struct path_trie_node *child; + + while (node->entries) { + struct path_trie_entry *e = node->entries; + + node->entries = e->next; + e->next = list; + list = e; + } + + hashmap_for_each_entry(&node->children, &iter, child, ent) + list = drain_node(child, list); + return list; +} + +struct path_trie_entry *path_trie_drain(struct path_trie *trie, + const char *path) +{ + struct path_trie_node *node = walk(trie, path, 0); + + return node ? drain_node(node, NULL) : NULL; +} + +/* + * Moves entries at `from` and below to the same position under `to`; + * `to_path` holds `to`'s path and is extended and restored around + * each recursion step, so moved entries' keys can be rewritten. + */ +static void move_node(struct path_trie *trie, struct path_trie_node *to, + struct path_trie_node *from, struct strbuf *to_path) +{ + struct hashmap_iter iter; + struct path_trie_node *from_child; + + while (from->entries) { + struct path_trie_entry *e = from->entries; + + from->entries = e->next; + e->next = to->entries; + to->entries = e; + free(e->key); + e->key = xstrdup(to_path->buf); + } + + hashmap_for_each_entry(&from->children, &iter, from_child, ent) { + struct path_trie_node *to_child = + get_child(trie, to, from_child->component, 1); + size_t len = to_path->len; + + strbuf_addch(to_path, '/'); + strbuf_addstr(to_path, from_child->component); + move_node(trie, to_child, from_child, to_path); + strbuf_setlen(to_path, len); + } +} + +static size_t component_len(const char *path) +{ + size_t len = 0; + + while (path[len] && !is_dir_sep(path[len])) + len++; + return len; +} + +/* Is `path` equal to, or nested somewhere below, `prefix`? */ +static int path_is_at_or_below(const char *path, const char *prefix, + unsigned int icase) +{ + while (1) { + size_t p_len, x_len; + + while (is_dir_sep(*prefix)) + prefix++; + while (is_dir_sep(*path)) + path++; + if (!*prefix) + return 1; + if (!*path) + return 0; + + p_len = component_len(prefix); + x_len = component_len(path); + if (p_len != x_len) + return 0; + if (icase ? strncasecmp(path, prefix, p_len) : + strncmp(path, prefix, p_len)) + return 0; + prefix += p_len; + path += x_len; + } +} + +void path_trie_move(struct path_trie *trie, const char *from, + const char *to) +{ + struct path_trie_node *from_node; + struct path_trie_node *to_node; + struct strbuf to_path = STRBUF_INIT; + + /* + * Moving a subtree into itself (or below itself) cannot + * terminate meaningfully; treat it as a no-op. + */ + if (path_is_at_or_below(to, from, trie->icase)) + return; + + from_node = walk(trie, from, 0); + if (!from_node) + return; + to_node = walk(trie, to, 1); + if (!to_node) + return; + strbuf_addstr(&to_path, to); + move_node(trie, to_node, from_node, &to_path); + strbuf_release(&to_path); +} diff --git a/path-trie.h b/path-trie.h new file mode 100644 index 00000000000000..753826f2355309 --- /dev/null +++ b/path-trie.h @@ -0,0 +1,89 @@ +#ifndef PATH_TRIE_H +#define PATH_TRIE_H + +#include "hashmap.h" + +/* + * A trie over the components of file system paths, mapping each path + * to a list of caller-provided entries. + * + * Entries are intrusive, like `struct hashmap_entry`: embed a + * `struct path_trie_entry` in your own struct and use `container_of` + * to get back to it. An entry belongs to at most one trie at a time. + * + * Paths are split on directory separators (both '/' and '\\'); + * repeated separators are ignored. Components are compared + * case-insensitively if `icase` is set at init time. Callers are + * expected to pass paths in a consistent (e.g. canonicalized, + * absolute) form; the trie does not resolve '.' or '..'. + * + * Unlike a flat hashmap keyed by whole paths, a trie can answer + * prefix questions: all entries registered at or below a path can be + * removed in one operation (path_trie_drain()) or moved onto another + * path (path_trie_move()). Both are useful when a path turns out to + * be an alias for another (e.g. a symbolic link), and everything + * registered through the alias must be found via the real path from + * then on. + */ + +struct path_trie_entry { + struct path_trie_entry *next; + /* + * The path this entry is currently registered under, owned by + * the trie: set by path_trie_add(), rewritten by + * path_trie_move(). After draining, the caller may pass the + * entry back to path_trie_add() (e.g. with `entry->key` + * itself) to re-register it, or free the key -- e.g. via + * path_trie_entry_clear() -- once done with the entry. + */ + char *key; +}; + +static inline void path_trie_entry_clear(struct path_trie_entry *entry) +{ + FREE_AND_NULL(entry->key); +} + +struct path_trie_node; + +struct path_trie { + struct path_trie_node *root; + unsigned int icase; +}; + +void path_trie_init(struct path_trie *trie, int icase); + +/* + * Removes all nodes from the trie. Entries themselves are not freed + * (the trie does not own them); drain first if they need releasing. + */ +void path_trie_clear(struct path_trie *trie); + +/* + * Adds an entry at `path`, recording the path in `entry->key` + * (replacing -- and releasing -- any previous key, so passing + * `entry->key` itself as `path` re-registers a drained entry). + */ +void path_trie_add(struct path_trie *trie, const char *path, + struct path_trie_entry *entry); + +/* + * Removes and returns all entries registered at `path` or nested + * anywhere below it, linked through their `next` fields (in no + * particular order). Returns NULL if there are none. + */ +struct path_trie_entry *path_trie_drain(struct path_trie *trie, + const char *path); + +/* + * Moves every entry registered at or below `from` to the + * corresponding path with the `from` prefix replaced by `to`, e.g. + * moving "a/b" to "x" moves entries at "a/b/c" to "x/c", updating + * each moved entry's `key` accordingly. Entries already present + * under `to` are kept. Moving a path into itself or below itself is + * a no-op. + */ +void path_trie_move(struct path_trie *trie, const char *from, + const char *to); + +#endif /* PATH_TRIE_H */ diff --git a/t/t2041-checkout-symlink-through-symlink.sh b/t/t2041-checkout-symlink-through-symlink.sh new file mode 100755 index 00000000000000..fd7771be3e3652 --- /dev/null +++ b/t/t2041-checkout-symlink-through-symlink.sh @@ -0,0 +1,56 @@ +#!/bin/sh + +test_description='checkout a symlink nested through another symlink on Windows + +A phantom symlink may target a path that goes through another +symlink. Ensures that such a symlink is still upgraded to a directory +symlink once its real target appears, even when that target directory +is only populated after the leading symlink has already resolved.' + +# Tell MSYS to create native symlinks. Without this flag test-lib's +# prerequisite detection for SYMLINKS doesn't detect the right thing. +MSYS=winsymlinks:nativestrict && export MSYS + +. ./test-lib.sh + +if ! test_have_prereq MINGW,SYMLINKS +then + skip_all='skipping $0: MinGW-only test, which requires symlink support.' + test_done +fi + +# Adds a symlink to the index without clobbering the work tree. +cache_symlink () { + sha=$(printf '%s' "$1" | git hash-object --stdin -w) && + git update-index --add --cacheinfo 120000,$sha,"$2" +} + +# Adds a regular file to the index without clobbering the work tree. +cache_file () { + sha=$(printf '%s' "$1" | git hash-object --stdin -w) && + git update-index --add --cacheinfo 100644,$sha,"$2" +} + +test_expect_success 'symlink nested through another symlink resolves' ' + test_when_finished "rm -rf chained" && + mkdir chained && + ( + cd chained && + git init -q && + + cache_symlink realdir leading && + cache_symlink leading/sub nested && + cache_file content realdir/sub/file && + + git checkout -- . && + + # "cmd.exe /c dir" marks directory symlinks as + # and file symlinks as ; unlike opening a path + # through the symlink, this distinguishes the two even + # though both resolve identically for plain reads. + cmd.exe //c dir . >dir-listing && + grep "SYMLINKD.*nested" dir-listing + ) +' + +test_done diff --git a/t/unit-tests/u-path-trie.c b/t/unit-tests/u-path-trie.c new file mode 100644 index 00000000000000..688c4347c693a6 --- /dev/null +++ b/t/unit-tests/u-path-trie.c @@ -0,0 +1,224 @@ +#include "unit-test.h" +#include "path-trie.h" + +struct test_entry { + struct path_trie_entry ent; + const char *tag; +}; + +static struct test_entry *entry(const char *tag) +{ + struct test_entry *e = xcalloc(1, sizeof(*e)); + + e->tag = tag; + return e; +} + +static size_t drain_count(struct path_trie *trie, const char *path) +{ + struct path_trie_entry *list = path_trie_drain(trie, path); + size_t n = 0; + + while (list) { + struct path_trie_entry *next = list->next; + + path_trie_entry_clear(list); + free(container_of(list, struct test_entry, ent)); + list = next; + n++; + } + return n; +} + +static int drained_tags_contain(struct path_trie_entry *list, const char *tag) +{ + for (; list; list = list->next) { + struct test_entry *e = + container_of(list, struct test_entry, ent); + + if (!strcmp(e->tag, tag)) + return 1; + } + return 0; +} + +static void free_drained(struct path_trie_entry *list) +{ + while (list) { + struct path_trie_entry *next = list->next; + + path_trie_entry_clear(list); + free(container_of(list, struct test_entry, ent)); + list = next; + } +} + +void test_path_trie__drain_empty(void) +{ + struct path_trie trie; + + path_trie_init(&trie, 0); + cl_assert_equal_p(path_trie_drain(&trie, "a/b"), NULL); + path_trie_clear(&trie); +} + +void test_path_trie__drain_exact_path(void) +{ + struct path_trie trie; + + path_trie_init(&trie, 0); + path_trie_add(&trie, "a/b/c", &entry("x")->ent); + cl_assert_equal_i(drain_count(&trie, "a/b/c"), 1); + cl_assert_equal_i(drain_count(&trie, "a/b/c"), 0); + path_trie_clear(&trie); +} + +void test_path_trie__drain_covers_subtree(void) +{ + struct path_trie trie; + + path_trie_init(&trie, 0); + path_trie_add(&trie, "a/b", &entry("shallow")->ent); + path_trie_add(&trie, "a/b/c/d", &entry("deep")->ent); + path_trie_add(&trie, "a/other", &entry("sibling")->ent); + cl_assert_equal_i(drain_count(&trie, "a/b"), 2); + cl_assert_equal_i(drain_count(&trie, "a"), 1); + path_trie_clear(&trie); +} + +void test_path_trie__drain_does_not_cover_ancestors(void) +{ + struct path_trie trie; + + path_trie_init(&trie, 0); + path_trie_add(&trie, "a", &entry("above")->ent); + cl_assert_equal_i(drain_count(&trie, "a/b"), 0); + cl_assert_equal_i(drain_count(&trie, "a"), 1); + path_trie_clear(&trie); +} + +void test_path_trie__multiple_entries_per_path(void) +{ + struct path_trie trie; + + path_trie_init(&trie, 0); + path_trie_add(&trie, "a/b", &entry("one")->ent); + path_trie_add(&trie, "a/b", &entry("two")->ent); + path_trie_add(&trie, "a/b", &entry("three")->ent); + cl_assert_equal_i(drain_count(&trie, "a/b"), 3); + path_trie_clear(&trie); +} + +void test_path_trie__separators_are_normalized(void) +{ + struct path_trie trie; + + path_trie_init(&trie, 0); + path_trie_add(&trie, "a/b/c", &entry("slash")->ent); + path_trie_add(&trie, "a//b///c", &entry("doubled")->ent); + cl_assert_equal_i(drain_count(&trie, "/a/b/c/"), 2); + path_trie_clear(&trie); +} + +void test_path_trie__case_sensitivity(void) +{ + struct path_trie trie; + + path_trie_init(&trie, 0); + path_trie_add(&trie, "a/b", &entry("lower")->ent); + cl_assert_equal_i(drain_count(&trie, "A/B"), 0); + cl_assert_equal_i(drain_count(&trie, "a/b"), 1); + path_trie_clear(&trie); + + path_trie_init(&trie, 1); + path_trie_add(&trie, "a/b", &entry("lower")->ent); + cl_assert_equal_i(drain_count(&trie, "A/B"), 1); + path_trie_clear(&trie); +} + +void test_path_trie__move_relocates_subtree(void) +{ + struct path_trie trie; + struct path_trie_entry *drained; + + path_trie_init(&trie, 0); + path_trie_add(&trie, "link/sub", &entry("via-link")->ent); + path_trie_add(&trie, "link/sub/deeper", &entry("nested")->ent); + + path_trie_move(&trie, "link", "real"); + + cl_assert_equal_i(drain_count(&trie, "link"), 0); + drained = path_trie_drain(&trie, "real/sub"); + cl_assert(drained != NULL); + cl_assert(drained_tags_contain(drained, "via-link")); + cl_assert(drained_tags_contain(drained, "nested")); + free_drained(drained); + path_trie_clear(&trie); +} + +void test_path_trie__move_merges_with_existing(void) +{ + struct path_trie trie; + + path_trie_init(&trie, 0); + path_trie_add(&trie, "from/x", &entry("moved")->ent); + path_trie_add(&trie, "to/x", &entry("already-there")->ent); + + path_trie_move(&trie, "from", "to"); + + cl_assert_equal_i(drain_count(&trie, "to/x"), 2); + path_trie_clear(&trie); +} + +void test_path_trie__move_missing_source_is_noop(void) +{ + struct path_trie trie; + + path_trie_init(&trie, 0); + path_trie_add(&trie, "to/x", &entry("existing")->ent); + path_trie_move(&trie, "does/not/exist", "to"); + cl_assert_equal_i(drain_count(&trie, "to"), 1); + path_trie_clear(&trie); +} + +void test_path_trie__move_into_itself_is_noop(void) +{ + struct path_trie trie; + + path_trie_init(&trie, 0); + path_trie_add(&trie, "a/b", &entry("kept")->ent); + path_trie_move(&trie, "a", "a/b/c"); + path_trie_move(&trie, "a", "a"); + cl_assert_equal_i(drain_count(&trie, "a/b"), 1); + path_trie_clear(&trie); +} + +void test_path_trie__move_to_ancestor(void) +{ + struct path_trie trie; + + path_trie_init(&trie, 0); + path_trie_add(&trie, "a/b/c", &entry("moves-up")->ent); + path_trie_move(&trie, "a/b", "a"); + cl_assert_equal_i(drain_count(&trie, "a/c"), 1); + path_trie_clear(&trie); +} + +void test_path_trie__move_rewrites_keys(void) +{ + struct path_trie trie; + struct path_trie_entry *drained; + + path_trie_init(&trie, 0); + path_trie_add(&trie, "link/sub", &entry("aliased")->ent); + path_trie_move(&trie, "link", "real"); + + drained = path_trie_drain(&trie, "real"); + cl_assert(drained != NULL); + cl_assert_equal_s(drained->key, "real/sub"); + + /* re-registering by the entry's own key must be safe */ + path_trie_add(&trie, drained->key, drained); + cl_assert_equal_i(drain_count(&trie, "real/sub"), 1); + path_trie_clear(&trie); +}