feat(parity): absolute basis dirs + link-dest relink of up-to-date dest

This commit is contained in:
opencode
2026-09-16 22:34:43 +02:00
parent 3e9f70d9ba
commit a0b9d9794b
3 changed files with 163 additions and 25 deletions
+20 -12
View File
@@ -329,30 +329,38 @@ bool config_has_basis(const Config* config) {
}
/* A basis-dir path travels from the client to the receiver and is resolved
* below the destination root, so it must be a non-empty relative path with no
* "." or ".." component and no traversal: an absolute or escaping path would
* make the receiver read or link files outside its authorized root.
* below the destination root when relative, or used verbatim when absolute
* (matching rsync). Either form must be non-empty, traversal-free (no "..")
* and free of "." components: an escaping path would make the receiver read or
* link files outside its authorized root. An absolute path is still subject to
* the receiver's root confinement at open time (file_open_secure_parent), so a
* basis outside the authorized root is simply not found rather than an escape.
*
* Returns a malloc'd CANONICAL copy of an accepted path, or NULL when the path
* is rejected. Canonicalization collapses interior empty components ("a//b" ->
* "a/b"), drops "." components and trailing "/"s, so validation, the delete
* walker prefix match and the receiver's basis lookup all agree on one form.
* The normalizer is the single source of truth for both config_basis_path_valid
* and config_basis_append. */
* "a/b"), drops "." components and trailing "/"s, and preserves a leading '/'
* for absolute paths, so validation, the delete walker prefix match and the
* receiver's basis lookup all agree on one form. The normalizer is the single
* source of truth for both config_basis_path_valid and config_basis_append. */
static char* basis_path_normalize(const char* path) {
if (!path || path[0] == '\0' || path[0] == '/' || has_path_traversal(path))
if (!path || path[0] == '\0' || has_path_traversal(path))
return NULL;
if (strcmp(path, ".") == 0)
bool absolute = path[0] == '/';
if (!absolute && strcmp(path, ".") == 0)
return NULL;
if (absolute && strcmp(path, "/") == 0)
return NULL;
char* dup = str_dup(path);
if (!dup)
return NULL;
size_t out_len = 0;
char* out = malloc(strlen(path) + 1);
char* out = malloc(strlen(path) + 2);
if (!out) {
free(dup);
return NULL;
}
if (absolute)
out[out_len++] = '/';
char* saveptr = NULL;
bool ok = true;
for (char* part = strtok_r(dup, "/", &saveptr); part; part = strtok_r(NULL, "/", &saveptr)) {
@@ -362,14 +370,14 @@ static char* basis_path_normalize(const char* path) {
}
if (strcmp(part, ".") == 0)
continue;
if (out_len > 0)
if (out_len > 0 && out[out_len - 1] != '/')
out[out_len++] = '/';
size_t len = strlen(part);
memcpy(out + out_len, part, len);
out_len += len;
}
free(dup);
if (!ok || out_len == 0) {
if (!ok || out_len == 0 || (absolute && out_len == 1)) {
free(out);
return NULL;
}