/* * test_workspace.c — workspace boundary policy. * * The policy is a pure function over an already-canonicalized path, so these are * table tests with no filesystem involved. home_dir and cache_dir are injected. */ #include "../src/foundation/compat.h" #include "test_framework.h" #include "test_helpers.h" #include "foundation/workspace.h" #include "foundation/compat_fs.h" #include "foundation/platform.h" #include #include #include #ifndef _WIN32 #include #endif static const char *HOME = "/Users/dev"; static const char *CACHE = "/Users/dev/.cache/codebase-memory-mcp"; TEST(ws_depth_counts_components_below_the_volume) { ASSERT_EQ(cbm_workspace_path_depth("/"), 0); ASSERT_EQ(cbm_workspace_path_depth("/etc"), 1); ASSERT_EQ(cbm_workspace_path_depth("/etc/"), 1); ASSERT_EQ(cbm_workspace_path_depth("/Users/dev"), 2); ASSERT_EQ(cbm_workspace_path_depth("/Users//dev///x"), 3); /* macOS firmlinks: the /private prefix must not inflate depth, or "/etc" * resolves to "/private/etc" and passes a minimum of two. */ ASSERT_EQ(cbm_workspace_path_depth("/private/etc"), 1); ASSERT_EQ(cbm_workspace_path_depth("/private/tmp/proj"), 2); ASSERT_EQ(cbm_workspace_path_depth("/private"), 0); /* Drive-relative, so an ordinary Windows workspace is one deep. */ ASSERT_EQ(cbm_workspace_path_depth("C:/"), 0); ASSERT_EQ(cbm_workspace_path_depth("D:/repos"), 1); ASSERT_EQ(cbm_workspace_path_depth("D:\\repos\\app"), 2); /* A UNC share root is the share itself. */ ASSERT_EQ(cbm_workspace_path_depth("//srv/share"), 0); ASSERT_EQ(cbm_workspace_path_depth("//srv/share/proj"), 1); PASS(); } TEST(ws_volume_roots_are_absolutely_denied) { ASSERT_EQ(cbm_workspace_classify_root("/", HOME, CACHE), CBM_WS_DENY_ABSOLUTE); ASSERT_EQ(cbm_workspace_classify_root("C:/", HOME, CACHE), CBM_WS_DENY_ABSOLUTE); ASSERT_EQ(cbm_workspace_classify_root("C:\\", HOME, CACHE), CBM_WS_DENY_ABSOLUTE); ASSERT_EQ(cbm_workspace_classify_root("//srv/share", HOME, CACHE), CBM_WS_DENY_ABSOLUTE); /* "/private" carries no components of its own once the macOS firmlink prefix * is discounted, so it is a volume root rather than merely shallow. */ ASSERT_EQ(cbm_workspace_classify_root("/private", HOME, CACHE), CBM_WS_DENY_ABSOLUTE); ASSERT_FALSE(cbm_workspace_verdict_is_overridable(CBM_WS_DENY_ABSOLUTE)); PASS(); } /* A relative or empty path is not a usable root and must not fall through as * allowed just because no rule matched it. */ TEST(ws_non_absolute_paths_are_denied) { ASSERT_EQ(cbm_workspace_classify_root("", HOME, CACHE), CBM_WS_DENY_ABSOLUTE); ASSERT_EQ(cbm_workspace_classify_root("relative/path", HOME, CACHE), CBM_WS_DENY_ABSOLUTE); ASSERT_EQ(cbm_workspace_classify_root(NULL, HOME, CACHE), CBM_WS_DENY_ABSOLUTE); PASS(); } /* One depth rule refuses every POSIX top-level tree without a list to maintain. * This is the whole reason depth carries its weight. */ TEST(ws_posix_top_level_trees_are_too_shallow) { static const char *const shallow[] = {"/etc", "/home", "/Users", "/var", "/opt", "/srv", "/usr", "/private/etc", "/private/var", "/private/tmp"}; for (size_t i = 0; i < sizeof(shallow) / sizeof(shallow[0]); i++) { ASSERT_EQ(cbm_workspace_classify_root(shallow[i], HOME, CACHE), CBM_WS_DENY_TOO_SHALLOW); } ASSERT_FALSE(cbm_workspace_verdict_is_overridable(CBM_WS_DENY_TOO_SHALLOW)); PASS(); } /* A recorded broad approval (allow-root --approve-broad) is the only thing that * lifts CBM_WS_DENY_TOO_SHALLOW, and only for that exact path. Classification * and cbm_workspace_verdict_is_overridable stay exactly as strict as the test * above checks; the approval is a grant, so it is listed like any other human * decision, and it lifts no other verdict. */ TEST(ws_allow_broad_root_lifts_too_shallow_for_an_exact_match_only) { char cache[256]; char *created = th_mktempdir("cbm_ws_allow_broad"); ASSERT_NOT_NULL(created); snprintf(cache, sizeof(cache), "%s", created); char err[1024]; char listed[4096]; /* Refused out of the box, and the refusal names the command a person runs. */ ASSERT_FALSE(cbm_workspace_root_allowed("/usr", HOME, cache, NULL, err, sizeof(err))); ASSERT_NOT_NULL(strstr(err, "allow-root --approve-broad /usr")); /* Neither an ordinary grant nor a sensitive approval lifts breadth; a broad * approval lifts neither an absolute nor a sensitive verdict. A refused grant * records nothing. */ ASSERT_FALSE(cbm_workspace_grant_add(cache, HOME, "/usr", false, false, err, sizeof(err))); ASSERT_NOT_NULL(strstr(err, "--approve-broad")); ASSERT_FALSE(cbm_workspace_grant_add(cache, HOME, "/usr", true, false, err, sizeof(err))); ASSERT_FALSE(cbm_workspace_grant_add(cache, HOME, "/", false, true, err, sizeof(err))); ASSERT_FALSE(cbm_workspace_grant_add(cache, HOME, HOME, false, true, err, sizeof(err))); ASSERT_NOT_NULL(strstr(err, "--approve-sensitive")); ASSERT_FALSE(cbm_workspace_grant_list(cache, listed, sizeof(listed))); /* The explicit approval is recorded and marked. */ ASSERT_TRUE(cbm_workspace_grant_add(cache, HOME, "/usr", false, true, err, sizeof(err))); ASSERT_TRUE(cbm_workspace_grant_list(cache, listed, sizeof(listed))); ASSERT_STR_EQ(listed, "(approved broad) /usr\n"); #ifndef _WIN32 /* Containment resolves real paths, so the rest needs a top-level directory * that exists on every POSIX host: "/usr". A repeated approval is recognized * rather than appended again. */ ASSERT_TRUE(cbm_workspace_grant_add(cache, HOME, "/usr", false, true, err, sizeof(err))); ASSERT_TRUE(cbm_workspace_grant_list(cache, listed, sizeof(listed))); ASSERT_STR_EQ(listed, "(approved broad) /usr\n"); /* It lifts exactly that path. A configured root of "/" puts "/dev" inside the * declared boundary too, so the only thing still refusing it is that the * approval names another path. */ ASSERT_TRUE(cbm_workspace_root_allowed("/usr", HOME, cache, NULL, err, sizeof(err))); ASSERT_TRUE(cbm_workspace_root_allowed("/usr", HOME, cache, "/", err, sizeof(err))); ASSERT_FALSE(cbm_workspace_root_allowed("/dev", HOME, cache, "/", err, sizeof(err))); ASSERT_NOT_NULL(strstr(err, "allow-root --approve-broad /dev")); #endif /* Each marker lifts only its own verdict: a broad mark on a credential * directory, which grant_add would never write, still does not lift it. */ char secret[1024]; snprintf(secret, sizeof(secret), "%s/.ssh", cache); ASSERT_EQ(cbm_mkdir(secret), 0); char store[1024]; ASSERT_TRUE(cbm_workspace_grant_path(cache, store, sizeof(store))); FILE *f = cbm_fopen(store, "a"); ASSERT_NOT_NULL(f); (void)fprintf(f, "^%s\n", secret); ASSERT_EQ(fclose(f), 0); ASSERT_FALSE(cbm_workspace_root_allowed(secret, HOME, cache, NULL, err, sizeof(err))); ASSERT_NOT_NULL(strstr(err, "--approve-sensitive")); th_cleanup(cache); PASS(); } /* Legitimately shallow project roots must survive: these are the false positives * a blanket depth rule would cause, which is why Windows counts drive-relative. */ TEST(ws_legitimate_shallow_roots_are_allowed) { ASSERT_EQ(cbm_workspace_classify_root("/opt/sdk", HOME, CACHE), CBM_WS_ALLOW); ASSERT_EQ(cbm_workspace_classify_root("/srv/protos", HOME, CACHE), CBM_WS_ALLOW); ASSERT_EQ(cbm_workspace_classify_root("D:/repos", HOME, CACHE), CBM_WS_ALLOW); ASSERT_EQ(cbm_workspace_classify_root("//srv/share/proj", HOME, CACHE), CBM_WS_ALLOW); ASSERT_EQ(cbm_workspace_classify_root("/Users/dev/dev/app", HOME, CACHE), CBM_WS_ALLOW); PASS(); } /* $HOME is depth 2 on both macOS and Linux, so depth cannot see it. */ TEST(ws_home_itself_is_sensitive_but_subdirs_are_fine) { ASSERT_EQ(cbm_workspace_classify_root("/Users/dev", HOME, CACHE), CBM_WS_DENY_SENSITIVE); ASSERT_EQ(cbm_workspace_classify_root("/Users/dev/", HOME, CACHE), CBM_WS_DENY_SENSITIVE); ASSERT_EQ(cbm_workspace_classify_root("/Users/dev/projects", HOME, CACHE), CBM_WS_ALLOW); /* A sibling that merely shares a prefix is not the home directory. */ ASSERT_EQ(cbm_workspace_classify_root("/Users/developer", HOME, CACHE), CBM_WS_ALLOW); ASSERT_TRUE(cbm_workspace_verdict_is_overridable(CBM_WS_DENY_SENSITIVE)); PASS(); } /* The enumeration bypass: a credential directory passes every breadth rule, so * it has to be named. Matched on any component, so subdirectories go too. */ TEST(ws_credential_directories_are_sensitive_at_any_depth) { ASSERT_EQ(cbm_workspace_classify_root("/Users/dev/.ssh", HOME, CACHE), CBM_WS_DENY_SENSITIVE); ASSERT_EQ(cbm_workspace_classify_root("/Users/dev/.ssh/keys", HOME, CACHE), CBM_WS_DENY_SENSITIVE); ASSERT_EQ(cbm_workspace_classify_root("/Users/dev/.aws", HOME, CACHE), CBM_WS_DENY_SENSITIVE); ASSERT_EQ(cbm_workspace_classify_root("/Users/dev/.gnupg", HOME, CACHE), CBM_WS_DENY_SENSITIVE); ASSERT_EQ(cbm_workspace_classify_root("/Users/dev/.kube", HOME, CACHE), CBM_WS_DENY_SENSITIVE); ASSERT_EQ(cbm_workspace_classify_root("/Users/dev/Library/Keychains", HOME, CACHE), CBM_WS_DENY_SENSITIVE); /* A name that merely contains a listed one is a different directory. */ ASSERT_EQ(cbm_workspace_classify_root("/Users/dev/.sshconfig", HOME, CACHE), CBM_WS_ALLOW); PASS(); } TEST(ws_windows_system_trees_are_sensitive) { ASSERT_EQ(cbm_workspace_classify_root("C:/Windows", HOME, CACHE), CBM_WS_DENY_SENSITIVE); ASSERT_EQ(cbm_workspace_classify_root("C:/Users", HOME, CACHE), CBM_WS_DENY_SENSITIVE); /* THE regression that matters on Windows: every user's work lives under * C:\Users\, so matching "Users" against every component would refuse * every ordinary project path — including a CI runner's own workspace. Only * the tree root is refused. */ ASSERT_EQ(cbm_workspace_classify_root("C:/Users/dev/projects/app", HOME, CACHE), CBM_WS_ALLOW); ASSERT_EQ(cbm_workspace_classify_root("C:/Users/runneradmin/work/repo", HOME, CACHE), CBM_WS_ALLOW); /* System trees are refused at any depth inside them, not just at the root. */ ASSERT_EQ(cbm_workspace_classify_root("C:/Windows/System32", HOME, CACHE), CBM_WS_DENY_SENSITIVE); /* A project merely named after one is not one. */ ASSERT_EQ(cbm_workspace_classify_root("C:/dev/Windows-app", HOME, CACHE), CBM_WS_ALLOW); ASSERT_EQ(cbm_workspace_classify_root("C:/ProgramData", HOME, CACHE), CBM_WS_DENY_SENSITIVE); ASSERT_EQ(cbm_workspace_classify_root("C:/Program Files/app", HOME, CACHE), CBM_WS_DENY_SENSITIVE); /* Case-insensitive: NTFS is, so a case-flipped name must not slip past. */ ASSERT_EQ(cbm_workspace_classify_root("C:/WINDOWS", HOME, CACHE), CBM_WS_DENY_SENSITIVE); ASSERT_EQ(cbm_workspace_classify_root("C:/users/dev/.SSH", HOME, CACHE), CBM_WS_DENY_SENSITIVE); PASS(); } TEST(ws_windows_user_programs_tree_has_exact_sensitive_boundaries) { static const char *const sensitive[] = { "C:/Users/dev/AppData/Local/Programs", "C:/Users/dev/AppData/Local/Programs/Antigravity", "D:/Users/runneradmin/AppData/Local/Programs/Editor", "c:\\uSeRs\\Dev\\aPpDaTa\\LoCaL\\pRoGrAmS\\IDE", }; for (size_t i = 0; i < sizeof(sensitive) / sizeof(sensitive[0]); i++) { ASSERT_EQ(cbm_workspace_classify_root(sensitive[i], HOME, CACHE), CBM_WS_DENY_SENSITIVE); } static const char *const allowed[] = { "C:/Users/dev/projects/app", "C:/Users/dev/AppData/Local", "C:/Users/dev/AppData/Local/Programs-src", "C:/Users/dev/AppData/Local/ProgramsBackup", "C:/workspace/Users/dev/AppData/Local/Programs", "C:/Users/dev/team/AppData/Local/Programs", }; for (size_t i = 0; i < sizeof(allowed) / sizeof(allowed[0]); i++) { ASSERT_EQ(cbm_workspace_classify_root(allowed[i], HOME, CACHE), CBM_WS_ALLOW); } PASS(); } TEST(ws_sensitive_root_explicit_approval_is_preserved) { char root[256]; char *created = th_mktempdir("cbm_ws_403_home"); ASSERT_NOT_NULL(created); snprintf(root, sizeof(root), "%s", created); char cache[256]; created = th_mktempdir("cbm_ws_403"); ASSERT_NOT_NULL(created); snprintf(cache, sizeof(cache), "%s", created); char err[1024]; ASSERT_FALSE(cbm_workspace_root_allowed(root, root, cache, NULL, err, sizeof(err))); ASSERT_NOT_NULL(strstr(err, "--approve-sensitive")); ASSERT_TRUE(cbm_workspace_grant_add(cache, root, root, true, false, err, sizeof(err))); ASSERT_TRUE(cbm_workspace_root_allowed(root, root, cache, NULL, err, sizeof(err))); th_cleanup(root); th_cleanup(cache); PASS(); } TEST(ws_sensitive_approval_upgrades_existing_ordinary_exact_grant) { char root[256]; char *created = th_mktempdir("cbm_ws_403_exact"); ASSERT_NOT_NULL(created); snprintf(root, sizeof(root), "%s", created); char cache[256]; created = th_mktempdir("cbm_ws_403_exact_cache"); ASSERT_NOT_NULL(created); snprintf(cache, sizeof(cache), "%s", created); char err[1024]; ASSERT_TRUE(cbm_workspace_grant_add(cache, NULL, root, false, false, err, sizeof(err))); ASSERT_FALSE(cbm_workspace_root_allowed(root, root, cache, NULL, err, sizeof(err))); ASSERT_TRUE(cbm_workspace_grant_add(cache, root, root, true, false, err, sizeof(err))); ASSERT_TRUE(cbm_workspace_root_allowed(root, root, cache, NULL, err, sizeof(err))); /* A repeated explicit approval must recognize the marked exact grant rather * than append another exception. */ ASSERT_TRUE(cbm_workspace_grant_add(cache, root, root, true, false, err, sizeof(err))); char listed[4096]; char expected[4096]; ASSERT_TRUE(cbm_workspace_grant_list(cache, listed, sizeof(listed))); snprintf(expected, sizeof(expected), "%s\n(approved) %s\n", root, root); ASSERT_STR_EQ(listed, expected); th_cleanup(root); th_cleanup(cache); PASS(); } TEST(ws_sensitive_approval_adds_exact_exception_under_ordinary_ancestor) { char ancestor[256]; char *created = th_mktempdir("cbm_ws_403_ancestor"); ASSERT_NOT_NULL(created); snprintf(ancestor, sizeof(ancestor), "%s", created); char cache[256]; created = th_mktempdir("cbm_ws_403_ancestor_cache"); ASSERT_NOT_NULL(created); snprintf(cache, sizeof(cache), "%s", created); char sensitive[1024]; snprintf(sensitive, sizeof(sensitive), "%s/private-project", ancestor); ASSERT_EQ(cbm_mkdir(sensitive), 0); char err[1024]; ASSERT_TRUE(cbm_workspace_grant_add(cache, NULL, ancestor, false, false, err, sizeof(err))); ASSERT_FALSE(cbm_workspace_root_allowed(sensitive, sensitive, cache, NULL, err, sizeof(err))); ASSERT_TRUE( cbm_workspace_grant_add(cache, sensitive, sensitive, true, false, err, sizeof(err))); ASSERT_TRUE( cbm_workspace_root_allowed(sensitive, sensitive, cache, NULL, err, sizeof(err))); ASSERT_TRUE( cbm_workspace_grant_add(cache, sensitive, sensitive, true, false, err, sizeof(err))); char listed[4096]; char expected[4096]; ASSERT_TRUE(cbm_workspace_grant_list(cache, listed, sizeof(listed))); snprintf(expected, sizeof(expected), "%s\n(approved) %s\n", ancestor, sensitive); ASSERT_STR_EQ(listed, expected); th_cleanup(ancestor); th_cleanup(cache); PASS(); } /* POSIX is case-sensitive, so a differently-cased directory is a different one * and must not be refused. */ TEST(ws_posix_matching_is_case_sensitive) { ASSERT_EQ(cbm_workspace_classify_root("/Users/dev/.SSH", HOME, CACHE), CBM_WS_ALLOW); PASS(); } /* Absent injected context, the checks that depend on it simply do not fire — * they must not crash or deny everything. */ TEST(ws_null_context_disables_dependent_checks) { ASSERT_EQ(cbm_workspace_classify_root("/Users/dev", NULL, NULL), CBM_WS_ALLOW); /* Depth and volume rules are context-free and still apply. */ ASSERT_EQ(cbm_workspace_classify_root("/etc", NULL, NULL), CBM_WS_DENY_TOO_SHALLOW); ASSERT_EQ(cbm_workspace_classify_root("/", NULL, NULL), CBM_WS_DENY_ABSOLUTE); PASS(); } TEST(ws_every_verdict_has_a_reason) { ASSERT_NOT_NULL(cbm_workspace_verdict_reason(CBM_WS_ALLOW)); ASSERT_NOT_NULL(cbm_workspace_verdict_reason(CBM_WS_DENY_TOO_SHALLOW)); ASSERT_NOT_NULL(cbm_workspace_verdict_reason(CBM_WS_DENY_ABSOLUTE)); ASSERT_NOT_NULL(cbm_workspace_verdict_reason(CBM_WS_DENY_SENSITIVE)); /* Reasons are user-facing; a bare enum name would not help anyone. */ ASSERT_TRUE(strlen(cbm_workspace_verdict_reason(CBM_WS_DENY_TOO_SHALLOW)) > 20); PASS(); } /* ── Per-project request manifest ───────────────────────────────────────── */ static void ws_write(const char *path, const char *body) { FILE *f = cbm_fopen(path, "wb"); if (f) { (void)fputs(body, f); (void)fclose(f); } } TEST(ws_manifest_absent_is_not_an_error) { char *base = th_mktempdir("cbm_ws_m0"); ASSERT(base != NULL); cbm_ws_manifest_t m; ASSERT_TRUE(cbm_workspace_manifest_read(base, &m)); ASSERT_FALSE(m.present); ASSERT_EQ(m.count, 0); th_cleanup(base); PASS(); } TEST(ws_manifest_parses_entries_and_skips_comments) { char *base = th_mktempdir("cbm_ws_m1"); ASSERT(base != NULL); char path[1024]; snprintf(path, sizeof(path), "%s/%s", base, CBM_WS_MANIFEST_NAME); ws_write(path, "# a comment\n/opt/sdk\n\n/srv/protos\n"); cbm_ws_manifest_t m; ASSERT_TRUE(cbm_workspace_manifest_read(base, &m)); ASSERT_TRUE(m.present); ASSERT_EQ(m.count, 2); ASSERT_STR_EQ(m.entries[0], "/opt/sdk"); ASSERT_STR_EQ(m.entries[1], "/srv/protos"); ASSERT_EQ((int)strlen(m.digest), 64); th_cleanup(base); PASS(); } /* A control character is how a crafted entry would smuggle a second value past a * line reader — the same shape as the newline splitting in the scoped file list. */ TEST(ws_manifest_rejects_control_characters) { char *base = th_mktempdir("cbm_ws_m2"); ASSERT(base != NULL); char path[1024]; snprintf(path, sizeof(path), "%s/%s", base, CBM_WS_MANIFEST_NAME); ws_write(path, "/opt/sdk\t\x01evil\n"); cbm_ws_manifest_t m; ASSERT_FALSE(cbm_workspace_manifest_read(base, &m)); ASSERT_EQ(m.count, 0); th_cleanup(base); PASS(); } /* THE property the design rests on: a manifest grants nothing until a person * approves it, and editing it lapses that approval rather than inheriting it. */ TEST(ws_manifest_approval_is_keyed_to_content) { /* th_mktempdir returns one static buffer: copy each result at once. */ char base[256]; char cache[256]; char *created = th_mktempdir("cbm_ws_m3"); ASSERT(created != NULL); snprintf(base, sizeof(base), "%s", created); created = th_mktempdir("cbm_ws_m3c"); ASSERT(created != NULL); snprintf(cache, sizeof(cache), "%s", created); /* Entries must name existing directories. */ char sdk[1024]; char protos[1024]; snprintf(sdk, sizeof(sdk), "%s/sdk", base); snprintf(protos, sizeof(protos), "%s/protos", base); ASSERT_EQ(cbm_mkdir(sdk), 0); ASSERT_EQ(cbm_mkdir(protos), 0); char path[1024]; char body[4096]; snprintf(path, sizeof(path), "%s/%s", base, CBM_WS_MANIFEST_NAME); snprintf(body, sizeof(body), "%s\n", sdk); ws_write(path, body); cbm_ws_manifest_t before; ASSERT_TRUE(cbm_workspace_manifest_read(base, &before)); /* Unapproved grants nothing. */ ASSERT_FALSE(cbm_workspace_manifest_is_approved(cache, base, &before)); char err[1024]; ASSERT_TRUE(cbm_workspace_manifest_approve(cache, HOME, base, err, sizeof(err))); ASSERT_TRUE(cbm_workspace_manifest_is_approved(cache, base, &before)); /* Widen the requests, as a `git pull` would. Approval must lapse. */ snprintf(body, sizeof(body), "%s\n%s\n", sdk, protos); ws_write(path, body); cbm_ws_manifest_t after; ASSERT_TRUE(cbm_workspace_manifest_read(base, &after)); ASSERT_TRUE(strcmp(before.digest, after.digest) != 0); ASSERT_FALSE(cbm_workspace_manifest_is_approved(cache, base, &after)); th_cleanup(base); th_cleanup(cache); PASS(); } /* Approving a manifest must not become a route around the breadth policy. The * entry has to exist, so the overbroad directory is "/etc" on POSIX and, on * Windows, where one component below the drive is already an ordinary * workspace, the drive root of the temporary directory. */ TEST(ws_manifest_approval_refuses_overbroad_requests) { char base[256]; char cache[256]; char *created = th_mktempdir("cbm_ws_m4"); ASSERT(created != NULL); snprintf(base, sizeof(base), "%s", created); created = th_mktempdir("cbm_ws_m4c"); ASSERT(created != NULL); snprintf(cache, sizeof(cache), "%s", created); char path[1024]; char body[1024]; const char *refusal = NULL; snprintf(path, sizeof(path), "%s/%s", base, CBM_WS_MANIFEST_NAME); #ifdef _WIN32 snprintf(body, sizeof(body), "%.3s\n", base); refusal = cbm_workspace_verdict_reason(CBM_WS_DENY_ABSOLUTE); #else snprintf(body, sizeof(body), "/etc\n"); refusal = cbm_workspace_verdict_reason(CBM_WS_DENY_TOO_SHALLOW); #endif ws_write(path, body); char err[1024]; ASSERT_FALSE(cbm_workspace_manifest_approve(cache, HOME, base, err, sizeof(err))); ASSERT_TRUE(strstr(err, refusal) != NULL); th_cleanup(base); th_cleanup(cache); PASS(); } /* #1718: the sensitive-root escape hatch broke on Windows separator spelling. * The grant is recorded from cbm_canonical_path — backslashes — while the * indexer hands the candidate already normalized to '/'. A byte-exact * comparison made the grant never match the candidate, so an approved * "Program Files" root stayed refused. '/' and '\' are equivalent everywhere * else in this module (ws_is_sep); the boundary comparison must treat them * equally. The classify_root home rule uses the same ws_paths_equal, so a * mixed-separator home spelling is the string-level regression check; the * FS-backed grant round-trip lives in the _WIN32 half, where the real * canonicalization exercises it. */ TEST(ws_approve_sensitive_matches_across_separator_styles) { /* canonical spelled with '\' vs home spelled with '/' — must classify as * the same directory. Pre-fix: strncmp mismatch at the separator byte, so * the home rule never matched and an ordinary path under "C:\Users" fell * through to CBM_WS_ALLOW. */ ASSERT_EQ(cbm_workspace_classify_root("C:\\Users\\dev", "C:/Users/dev", CACHE), CBM_WS_DENY_SENSITIVE); ASSERT_EQ(cbm_workspace_classify_root("C:/Users/dev", "C:\\Users\\dev", CACHE), CBM_WS_DENY_SENSITIVE); /* A sibling that differs after the same separator boundary is still a * different directory: "/a/bc" must never look like "/a/b". */ ASSERT_EQ(cbm_workspace_classify_root("C:\\Users\\devx", "C:/Users/dev", CACHE), CBM_WS_ALLOW); #ifdef _WIN32 /* Full grant round-trip on real filesystem paths: the grant stored with * '\' must lift the exact candidate the indexer passes with '/'. The * sensitive class is exercised by a credential-named directory (".ssh"), * which is DENY_SENSITIVE at any depth — equivalent to "Program Files" * for the refusal path under test. */ char *cache = th_mktempdir("cbm_ws_sw"); ASSERT(cache != NULL); char dir[1024]; snprintf(dir, sizeof(dir), "%s/.ssh/cfg", cache); ASSERT_TRUE(cbm_mkdir_p(dir, 0700)); char path[1024]; snprintf(path, sizeof(path), "%s/allowed_roots", cache); char grant[2048]; snprintf(grant, sizeof(grant), "!%s\\.ssh\\cfg\n", cache); FILE *f = cbm_fopen(path, "wb"); ASSERT_NOT_NULL(f); (void)fprintf(f, "%s", grant); (void)fclose(f); char candidate[2048]; snprintf(candidate, sizeof(candidate), "%s/.ssh/cfg", cache); /* Convert to '/' spelling, exactly as cbm_normalize_path_sep does for the * indexer's candidate. */ char *p; for (p = candidate; *p; p++) { if (*p == '\\') { *p = '/'; } } char err[1024]; err[0] = '\0'; ASSERT_TRUE(cbm_workspace_root_allowed(candidate, NULL, cache, NULL, err, sizeof(err))); /* An unapproved sibling under the same sensitive tree stays refused. It * lies outside every granted root, so the refusal is the boundary wording * (kept verbatim by contract), not the sensitive hint. */ char sibling[2048]; snprintf(sibling, sizeof(sibling), "%s/.ssh/keys", cache); for (p = sibling; *p; p++) { if (*p == '\\') { *p = '/'; } } err[0] = '\0'; ASSERT_FALSE(cbm_workspace_root_allowed(sibling, NULL, cache, NULL, err, sizeof(err))); ASSERT_TRUE(strstr(err, "outside the allowed root") != NULL); /* A strict descendant INSIDE the approved sensitive grant is contained * (boundary passes) but not the exact grant, so the sensitive refusal * applies and the hint names the flag. */ char descendant[2048]; snprintf(descendant, sizeof(descendant), "%s/.ssh/cfg/sub", cache); for (p = descendant; *p; p++) { if (*p == '\\') { *p = '/'; } } /* Containment canonicalizes the candidate, which on Windows opens it * (CreateFileW, OPEN_EXISTING): a descendant that is not on disk is * "outside the allowed root" instead of reaching the sensitive hint. */ ASSERT_TRUE(cbm_mkdir_p(descendant, 0700)); err[0] = '\0'; ASSERT_FALSE(cbm_workspace_root_allowed(descendant, NULL, cache, NULL, err, sizeof(err))); ASSERT_TRUE(strstr(err, "--approve-sensitive") != NULL); th_cleanup(cache); #endif PASS(); } /* ── Environment helpers ────────────────────────────────────────────────── */ static char *ws_env_save(const char *name) { const char *value = getenv(name); return value ? strdup(value) : NULL; } static void ws_env_restore(const char *name, char *saved) { if (saved) { (void)cbm_setenv(name, saved, 1); free(saved); } else { (void)cbm_unsetenv(name); } } /* The policy compares the home directory against canonical paths, so the * helper must hand out the canonical form. A plain temp directory already * shows the difference: on macOS /tmp is a firmlink to /private/tmp, and on * Windows the temp path may carry a short (8.3) component. */ TEST(ws_home_dir_is_resolved) { char *created = th_mktempdir("cbm_ws_home"); ASSERT_NOT_NULL(created); char real[256]; snprintf(real, sizeof(real), "%s", created); char expected[4096]; ASSERT_TRUE(cbm_canonical_path(real, expected, sizeof(expected))); cbm_normalize_path_sep(expected); char *saved_home = ws_env_save("HOME"); ASSERT_EQ(cbm_setenv("HOME", real, 1), 0); const char *home = cbm_workspace_home_dir(); bool resolved = home && strcmp(home, expected) == 0; ws_env_restore("HOME", saved_home); th_cleanup(real); ASSERT_TRUE(resolved); PASS(); } /* A home directory reached through a link — "/home" kept on another volume, * an account whose HOME is itself a link — is still the home directory when * a caller presents its resolved path, and must be refused as such. */ TEST(ws_linked_home_classified_as_home) { #ifdef _WIN32 /* symlink() does not exist on Windows, and creating a symbolic link there * needs a privilege an ordinary account (and the CI runner) does not hold. * A directory junction (cmd.exe mklink /J) is the Windows shape of the * same case and would need a cmd.exe fixture; the resolution the case * depends on is covered on Windows by ws_home_dir_is_resolved, and the * link itself is exercised on POSIX. */ SKIP_PLATFORM("Windows: symlink() unavailable; links need a privilege"); #else char *created = th_mktempdir("cbm_ws_linked_home"); ASSERT_NOT_NULL(created); char base[256]; snprintf(base, sizeof(base), "%s", created); char real[512]; char link[512]; snprintf(real, sizeof(real), "%s/real", base); snprintf(link, sizeof(link), "%s/link", base); ASSERT_EQ(cbm_mkdir(real), 0); ASSERT_EQ(symlink(real, link), 0); char canonical_real[4096]; ASSERT_TRUE(cbm_canonical_path(real, canonical_real, sizeof(canonical_real))); /* The fixture proves something only when the two spellings differ. */ ASSERT_TRUE(strcmp(link, canonical_real) != 0); char *saved_home = ws_env_save("HOME"); ASSERT_EQ(cbm_setenv("HOME", link, 1), 0); const char *home = cbm_workspace_home_dir(); bool resolved = home && strcmp(home, canonical_real) == 0; cbm_ws_verdict_t verdict = cbm_workspace_classify_root(canonical_real, home, NULL); ws_env_restore("HOME", saved_home); th_cleanup(base); ASSERT_TRUE(resolved); ASSERT_EQ(verdict, CBM_WS_DENY_SENSITIVE); PASS(); #endif } /* ── Manifest entries are judged in their resolved form ──────────────────── */ enum { WS_T_PATH = 4096 }; /* th_mktempdir hands back one static buffer, so copy at once — and in canonical * form, because the policy compares resolved spellings and /tmp is a link on * macOS. */ static bool ws_tempdir_canonical(const char *prefix, char *out, size_t out_sz) { char *created = th_mktempdir(prefix); return created && cbm_canonical_path(created, out, out_sz) == 1; } static bool ws_join(char *out, size_t out_sz, const char *a, const char *b) { int n = snprintf(out, out_sz, "%s/%s", a, b); return n > 0 && (size_t)n < out_sz; } /* Create / and return it in canonical form. */ static bool ws_mkdir_canonical(const char *parent, const char *name, char *out, size_t out_sz) { char made[WS_T_PATH]; if (!ws_join(made, sizeof(made), parent, name) || cbm_mkdir(made) != 0) { return false; } return cbm_canonical_path(made, out, out_sz) == 1; } static void ws_manifest_write(const char *base, const char *body) { char path[WS_T_PATH]; if (ws_join(path, sizeof(path), base, CBM_WS_MANIFEST_NAME)) { ws_write(path, body); } } static void ws_manifest_write_line(const char *base, const char *entry) { char body[WS_T_PATH]; int n = snprintf(body, sizeof(body), "%s\n", entry); if (n > 0 && (size_t)n < sizeof(body)) { ws_manifest_write(base, body); } } /* Put the manifest on record as approved by writing the store line directly, * in the form cbm_workspace_manifest_approve writes (" "). * This is the record an approval leaves behind when the entry was judged by * its spelling, and the use-time check must stand on its own against it. */ static bool ws_manifest_record_approval(const char *cache, const char *base) { cbm_ws_manifest_t m; if (!cbm_workspace_manifest_read(base, &m) || !m.present) { return false; } char store[WS_T_PATH]; char line[WS_T_PATH]; if (!ws_join(store, sizeof(store), cache, "approved_manifests")) { return false; } int n = snprintf(line, sizeof(line), "%s %s\n", m.digest, base); if (n <= 0 || (size_t)n >= sizeof(line)) { return false; } ws_write(store, line); return cbm_workspace_manifest_is_approved(cache, base, &m); } /* The directory a manifest entry names is the one that would be indexed, so * that is the form the policy judges — at approval and again at use. Spelled * through `..`, an entry reads as an ordinary path several components deep * while naming the home directory or the volume root. */ TEST(ws_manifest_entry_classified_in_resolved_form) { char base[WS_T_PATH]; char cache[WS_T_PATH]; ASSERT_TRUE(ws_tempdir_canonical("cbm_ws_m5", base, sizeof(base))); ASSERT_TRUE(ws_tempdir_canonical("cbm_ws_m5c", cache, sizeof(cache))); char home[WS_T_PATH]; char src[WS_T_PATH]; char home_sub[WS_T_PATH]; ASSERT_TRUE(ws_mkdir_canonical(base, "home", home, sizeof(home))); ASSERT_TRUE(ws_mkdir_canonical(base, "src", src, sizeof(src))); ASSERT_TRUE(ws_mkdir_canonical(home, "notes", home_sub, sizeof(home_sub))); char entry[WS_T_PATH]; char expected[WS_T_PATH]; char err[1024]; /* "/src/../home" is the home directory. */ ASSERT_TRUE(ws_join(entry, sizeof(entry), src, "../home")); ws_manifest_write_line(base, entry); ASSERT_FALSE(cbm_workspace_manifest_approve(cache, home, base, err, sizeof(err))); ASSERT_NOT_NULL(strstr(err, entry)); ASSERT(snprintf(expected, sizeof(expected), "(resolves to %s)", home) > 0); ASSERT_NOT_NULL(strstr(err, expected)); ASSERT_NOT_NULL(strstr(err, cbm_workspace_verdict_reason(CBM_WS_DENY_SENSITIVE))); /* An approval already on record for this spelling does not outrank the * use-time check. */ ASSERT_TRUE(ws_manifest_record_approval(cache, base)); ASSERT_FALSE(cbm_workspace_manifest_allows(cache, home, base, home_sub)); /* Enough `..` segments reach the volume root from anywhere. */ ASSERT_TRUE(ws_join(entry, sizeof(entry), base, "../../../../../../../../../../../..")); ws_manifest_write_line(base, entry); ASSERT_FALSE(cbm_workspace_manifest_approve(cache, home, base, err, sizeof(err))); ASSERT_NOT_NULL(strstr(err, entry)); ASSERT_NOT_NULL(strstr(err, cbm_workspace_verdict_reason(CBM_WS_DENY_ABSOLUTE))); th_cleanup(base); th_cleanup(cache); PASS(); } /* An entry that does not resolve to a directory is refused at approval, naming * the entry; at use it simply matches nothing, and the other entries of the * same manifest keep working. */ TEST(ws_manifest_unresolvable_entry_refused) { char base[WS_T_PATH]; char cache[WS_T_PATH]; ASSERT_TRUE(ws_tempdir_canonical("cbm_ws_m6", base, sizeof(base))); ASSERT_TRUE(ws_tempdir_canonical("cbm_ws_m6c", cache, sizeof(cache))); char entry[WS_T_PATH]; char err[1024]; /* Missing. */ ASSERT_TRUE(ws_join(entry, sizeof(entry), base, "missing")); ws_manifest_write_line(base, entry); ASSERT_FALSE(cbm_workspace_manifest_approve(cache, HOME, base, err, sizeof(err))); ASSERT_NOT_NULL(strstr(err, entry)); ASSERT_NOT_NULL(strstr(err, "must name an existing directory")); /* Present, but a file. */ ASSERT_TRUE(ws_join(entry, sizeof(entry), base, "notes.txt")); ws_write(entry, "not a directory\n"); ws_manifest_write_line(base, entry); ASSERT_FALSE(cbm_workspace_manifest_approve(cache, HOME, base, err, sizeof(err))); ASSERT_NOT_NULL(strstr(err, entry)); ASSERT_NOT_NULL(strstr(err, "is not a directory")); /* Relative: the file is read by a long-lived process whose working * directory is unrelated to the project, so there is no base to resolve * it against. */ ws_manifest_write_line(base, "src"); ASSERT_FALSE(cbm_workspace_manifest_approve(cache, HOME, base, err, sizeof(err))); ASSERT_NOT_NULL(strstr(err, "requested path src:")); ASSERT_NOT_NULL(strstr(err, "must be an absolute path")); /* At use: a directory that disappeared after approval matches nothing and * does not take the rest of the manifest with it. */ char present[WS_T_PATH]; char present_sub[WS_T_PATH]; char body[WS_T_PATH]; ASSERT_TRUE(ws_mkdir_canonical(base, "present", present, sizeof(present))); ASSERT_TRUE(ws_mkdir_canonical(present, "inc", present_sub, sizeof(present_sub))); ASSERT_TRUE(ws_join(entry, sizeof(entry), base, "gone")); ASSERT(snprintf(body, sizeof(body), "%s\n%s\n", entry, present) > 0); ws_manifest_write(base, body); ASSERT_TRUE(ws_manifest_record_approval(cache, base)); ASSERT_TRUE(cbm_workspace_manifest_allows(cache, HOME, base, present_sub)); ASSERT_FALSE(cbm_workspace_manifest_allows(cache, HOME, base, base)); th_cleanup(base); th_cleanup(cache); PASS(); } /* The ordinary shapes keep their result: an absolute existing directory, an * entry equal to the project root, an entry outside every root in the grant * store (the manifest is consulted on its own), and on POSIX a link to an * ordinary directory. */ TEST(ws_manifest_ordinary_entries_unchanged) { char base[WS_T_PATH]; char cache[WS_T_PATH]; ASSERT_TRUE(ws_tempdir_canonical("cbm_ws_m7", base, sizeof(base))); ASSERT_TRUE(ws_tempdir_canonical("cbm_ws_m7c", cache, sizeof(cache))); char sdk[WS_T_PATH]; char sdk_inc[WS_T_PATH]; char granted[WS_T_PATH]; ASSERT_TRUE(ws_mkdir_canonical(base, "sdk", sdk, sizeof(sdk))); ASSERT_TRUE(ws_mkdir_canonical(sdk, "inc", sdk_inc, sizeof(sdk_inc))); ASSERT_TRUE(ws_mkdir_canonical(base, "granted", granted, sizeof(granted))); char err[1024]; /* The grant store names a root the sdk is not under. */ ASSERT_TRUE(cbm_workspace_grant_add(cache, HOME, granted, false, false, err, sizeof(err))); ws_manifest_write_line(base, sdk); ASSERT_TRUE(cbm_workspace_manifest_approve(cache, HOME, base, err, sizeof(err))); ASSERT_STR_EQ(err, ""); ASSERT_TRUE(cbm_workspace_manifest_allows(cache, HOME, base, sdk_inc)); ASSERT_FALSE(cbm_workspace_manifest_allows(cache, HOME, base, granted)); /* Equal to the project root. */ ws_manifest_write_line(base, base); ASSERT_TRUE(cbm_workspace_manifest_approve(cache, HOME, base, err, sizeof(err))); ASSERT_TRUE(cbm_workspace_manifest_allows(cache, HOME, base, base)); ASSERT_TRUE(cbm_workspace_manifest_allows(cache, HOME, base, sdk_inc)); #ifndef _WIN32 /* A link to an ordinary directory is that directory. Windows is not * covered here: symlink(2) has no counterpart in the test helpers, and * CreateSymbolicLinkW needs a privilege the CI runner does not hold; the * resolved-form rules themselves are exercised on every platform by the * `..` cases above. */ char alias[WS_T_PATH]; ASSERT_TRUE(ws_join(alias, sizeof(alias), base, "alias")); ASSERT_EQ(symlink(sdk, alias), 0); ws_manifest_write_line(base, alias); ASSERT_TRUE(cbm_workspace_manifest_approve(cache, HOME, base, err, sizeof(err))); ASSERT_TRUE(cbm_workspace_manifest_allows(cache, HOME, base, sdk_inc)); #endif th_cleanup(base); th_cleanup(cache); PASS(); } #ifndef _WIN32 /* A link is judged by where it leads. Windows is not covered: symlink(2) has * no counterpart in the test helpers and CreateSymbolicLinkW needs a privilege * the CI runner does not hold; the resolved-form rules are exercised on every * platform by ws_manifest_entry_classified_in_resolved_form. */ TEST(ws_manifest_link_to_sensitive_directory_refused) { char base[WS_T_PATH]; char cache[WS_T_PATH]; ASSERT_TRUE(ws_tempdir_canonical("cbm_ws_m8", base, sizeof(base))); ASSERT_TRUE(ws_tempdir_canonical("cbm_ws_m8c", cache, sizeof(cache))); char home[WS_T_PATH]; char home_sub[WS_T_PATH]; char keys[WS_T_PATH]; char keys_sub[WS_T_PATH]; ASSERT_TRUE(ws_mkdir_canonical(base, "home", home, sizeof(home))); ASSERT_TRUE(ws_mkdir_canonical(home, "notes", home_sub, sizeof(home_sub))); ASSERT_TRUE(ws_mkdir_canonical(base, ".ssh", keys, sizeof(keys))); ASSERT_TRUE(ws_mkdir_canonical(keys, "hosts", keys_sub, sizeof(keys_sub))); char to_home[WS_T_PATH]; char to_keys[WS_T_PATH]; ASSERT_TRUE(ws_join(to_home, sizeof(to_home), base, "to-home")); ASSERT_TRUE(ws_join(to_keys, sizeof(to_keys), base, "to-keys")); ASSERT_EQ(symlink(home, to_home), 0); ASSERT_EQ(symlink(keys, to_keys), 0); char expected[WS_T_PATH]; char err[1024]; ws_manifest_write_line(base, to_home); ASSERT_FALSE(cbm_workspace_manifest_approve(cache, home, base, err, sizeof(err))); ASSERT_NOT_NULL(strstr(err, to_home)); ASSERT(snprintf(expected, sizeof(expected), "(resolves to %s)", home) > 0); ASSERT_NOT_NULL(strstr(err, expected)); ASSERT_NOT_NULL(strstr(err, cbm_workspace_verdict_reason(CBM_WS_DENY_SENSITIVE))); ws_manifest_write_line(base, to_keys); ASSERT_FALSE(cbm_workspace_manifest_approve(cache, home, base, err, sizeof(err))); ASSERT_NOT_NULL(strstr(err, to_keys)); ASSERT(snprintf(expected, sizeof(expected), "(resolves to %s)", keys) > 0); ASSERT_NOT_NULL(strstr(err, expected)); ASSERT_NOT_NULL(strstr(err, cbm_workspace_verdict_reason(CBM_WS_DENY_SENSITIVE))); /* On record as approved: the use-time check still refuses both. */ char body[WS_T_PATH]; ASSERT(snprintf(body, sizeof(body), "%s\n%s\n", to_home, to_keys) > 0); ws_manifest_write(base, body); ASSERT_TRUE(ws_manifest_record_approval(cache, base)); ASSERT_FALSE(cbm_workspace_manifest_allows(cache, home, base, home_sub)); ASSERT_FALSE(cbm_workspace_manifest_allows(cache, home, base, keys_sub)); th_cleanup(base); th_cleanup(cache); PASS(); } /* Approval binds to the manifest text; where a link leads is read again each * time the manifest is used. A link approved while it led to an ordinary * directory and later pointed at the home directory grants nothing there. * POSIX only, for the reason given at the test above. */ TEST(ws_manifest_link_retarget_is_seen_at_use) { char base[WS_T_PATH]; char cache[WS_T_PATH]; ASSERT_TRUE(ws_tempdir_canonical("cbm_ws_m9", base, sizeof(base))); ASSERT_TRUE(ws_tempdir_canonical("cbm_ws_m9c", cache, sizeof(cache))); char shared[WS_T_PATH]; char shared_sub[WS_T_PATH]; char home[WS_T_PATH]; char home_sub[WS_T_PATH]; ASSERT_TRUE(ws_mkdir_canonical(base, "shared", shared, sizeof(shared))); ASSERT_TRUE(ws_mkdir_canonical(shared, "proto", shared_sub, sizeof(shared_sub))); ASSERT_TRUE(ws_mkdir_canonical(base, "home", home, sizeof(home))); ASSERT_TRUE(ws_mkdir_canonical(home, "notes", home_sub, sizeof(home_sub))); char extra[WS_T_PATH]; ASSERT_TRUE(ws_join(extra, sizeof(extra), base, "extra")); ASSERT_EQ(symlink(shared, extra), 0); char err[1024]; ws_manifest_write_line(base, extra); ASSERT_TRUE(cbm_workspace_manifest_approve(cache, home, base, err, sizeof(err))); ASSERT_TRUE(cbm_workspace_manifest_allows(cache, home, base, shared_sub)); /* Same text, new target. */ ASSERT_EQ(unlink(extra), 0); ASSERT_EQ(symlink(home, extra), 0); cbm_ws_manifest_t m; ASSERT_TRUE(cbm_workspace_manifest_read(base, &m)); ASSERT_TRUE(cbm_workspace_manifest_is_approved(cache, base, &m)); ASSERT_FALSE(cbm_workspace_manifest_allows(cache, home, base, home_sub)); ASSERT_FALSE(cbm_workspace_manifest_allows(cache, home, base, shared_sub)); th_cleanup(base); th_cleanup(cache); PASS(); } #endif /* _WIN32 */ SUITE(workspace) { RUN_TEST(ws_manifest_absent_is_not_an_error); RUN_TEST(ws_manifest_parses_entries_and_skips_comments); RUN_TEST(ws_manifest_rejects_control_characters); RUN_TEST(ws_manifest_approval_is_keyed_to_content); RUN_TEST(ws_manifest_approval_refuses_overbroad_requests); RUN_TEST(ws_depth_counts_components_below_the_volume); RUN_TEST(ws_volume_roots_are_absolutely_denied); RUN_TEST(ws_non_absolute_paths_are_denied); RUN_TEST(ws_posix_top_level_trees_are_too_shallow); RUN_TEST(ws_allow_broad_root_lifts_too_shallow_for_an_exact_match_only); RUN_TEST(ws_legitimate_shallow_roots_are_allowed); RUN_TEST(ws_home_itself_is_sensitive_but_subdirs_are_fine); RUN_TEST(ws_credential_directories_are_sensitive_at_any_depth); RUN_TEST(ws_windows_system_trees_are_sensitive); RUN_TEST(ws_windows_user_programs_tree_has_exact_sensitive_boundaries); RUN_TEST(ws_sensitive_root_explicit_approval_is_preserved); RUN_TEST(ws_sensitive_approval_upgrades_existing_ordinary_exact_grant); RUN_TEST(ws_sensitive_approval_adds_exact_exception_under_ordinary_ancestor); RUN_TEST(ws_posix_matching_is_case_sensitive); RUN_TEST(ws_null_context_disables_dependent_checks); RUN_TEST(ws_every_verdict_has_a_reason); RUN_TEST(ws_approve_sensitive_matches_across_separator_styles); RUN_TEST(ws_home_dir_is_resolved); RUN_TEST(ws_linked_home_classified_as_home); RUN_TEST(ws_manifest_entry_classified_in_resolved_form); RUN_TEST(ws_manifest_unresolvable_entry_refused); RUN_TEST(ws_manifest_ordinary_entries_unchanged); #ifndef _WIN32 RUN_TEST(ws_manifest_link_to_sensitive_directory_refused); RUN_TEST(ws_manifest_link_retarget_is_seen_at_use); #endif }