- client_cli: capture errno before output_escape() in read_patterns_from_file() so an over-long line is still reported as EFBIG instead of the (possibly malloc-clobbered) errno. - file_list: guard string_list_add() capacity doubling against overflow (capacity > INT_MAX / 2), matching filter_rule_list_add(); callers already surface the false as a memory-allocation error. - compression: ZSTD_isError() is true for ZSTD_CONTENTSIZE_UNKNOWN, which made the 3x unknown-size fallback dead code. Test the CONTENTSIZE_ERROR/UNKNOWN sentinels explicitly so unknown-size frames reach the estimate path (still bounded by the existing hard limit) while invalid frames are rejected. Known-size frames and the 100 MB ceiling/overflow checks are unchanged. - tests: add an unknown-content-size-frame decompression test. Tests: ./build/tests and ./build-asan/tests all pass (42/42); clang-format + cppcheck clean.
243 lines
7.2 KiB
C
243 lines
7.2 KiB
C
#include "file_list.h"
|
|
#include "log.h"
|
|
#include "utils.h"
|
|
#include <errno.h>
|
|
#include <limits.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
typedef struct {
|
|
char** items;
|
|
int count;
|
|
int capacity;
|
|
} StringList;
|
|
|
|
static void string_list_destroy(StringList* list) {
|
|
if (!list)
|
|
return;
|
|
for (int i = 0; i < list->count; i++)
|
|
free(list->items[i]);
|
|
free(list->items);
|
|
}
|
|
|
|
static bool string_list_add(StringList* list, const char* text) {
|
|
if (list->count == list->capacity) {
|
|
if (list->capacity > INT_MAX / 2)
|
|
return false;
|
|
int new_cap = list->capacity > 0 ? list->capacity * 2 : 16;
|
|
char** grown = realloc(list->items, (size_t)new_cap * sizeof(char*));
|
|
if (!grown)
|
|
return false;
|
|
list->items = grown;
|
|
list->capacity = new_cap;
|
|
}
|
|
list->items[list->count] = str_dup(text);
|
|
if (!list->items[list->count])
|
|
return false;
|
|
list->count++;
|
|
return true;
|
|
}
|
|
|
|
/* Validate and normalize one entry. Returns:
|
|
* 1 -> added to `out`
|
|
* 0 -> blank entry, skip
|
|
* -1 -> invalid (message set in `err`)
|
|
* `strip_line_endings` trims a trailing CR/LF (line mode only); NUL mode keeps
|
|
* the entry bytes verbatim so names ending in CR/LF survive. */
|
|
static int normalize_entry(const char* raw, size_t len, bool strip_line_endings, StringList* out,
|
|
char* err, size_t err_size) {
|
|
if (strip_line_endings) {
|
|
while (len > 0 && (raw[len - 1] == '\n' || raw[len - 1] == '\r'))
|
|
len--;
|
|
}
|
|
if (len == 0)
|
|
return 0;
|
|
if (raw[0] == '/') {
|
|
int print_len = len > (size_t)INT_MAX ? INT_MAX : (int)len;
|
|
snprintf(err, err_size, "absolute path entries are not allowed: '%.*s'", print_len, raw);
|
|
return -1;
|
|
}
|
|
/* Reject NUL bytes inside a token defensively. In NUL-delimited mode the
|
|
* delimiter itself is the final byte and is expected; in line mode any NUL is
|
|
* embedded garbage (strlen-based parsing would otherwise silently truncate). */
|
|
size_t scan_len = strip_line_endings ? len : len - 1;
|
|
if (memchr(raw, '\0', scan_len)) {
|
|
snprintf(err, err_size, "entry contains an embedded NUL byte");
|
|
return -1;
|
|
}
|
|
char* dup = malloc(len + 1);
|
|
if (!dup) {
|
|
snprintf(err, err_size, "memory allocation failed");
|
|
return -1;
|
|
}
|
|
memcpy(dup, raw, len);
|
|
dup[len] = '\0';
|
|
|
|
/* Rebuild the path token-by-token: skip '.' and empty segments, reject '..'. */
|
|
size_t out_len = 0;
|
|
for (const char* part = dup;;) {
|
|
const char* slash = strchr(part, '/');
|
|
size_t part_len = slash ? (size_t)(slash - part) : strlen(part);
|
|
if (part_len == 1 && part[0] == '.') {
|
|
/* skip "." segment */
|
|
} else if (part_len == 2 && part[0] == '.' && part[1] == '.') {
|
|
snprintf(err, err_size, "path traversal entry is not allowed: '%s'", dup);
|
|
free(dup);
|
|
return -1;
|
|
} else if (part_len > 0) {
|
|
if (out_len > 0)
|
|
dup[out_len++] = '/';
|
|
memmove(dup + out_len, part, part_len);
|
|
out_len += part_len;
|
|
}
|
|
if (!slash)
|
|
break;
|
|
part = slash + 1;
|
|
}
|
|
dup[out_len] = '\0';
|
|
|
|
int result;
|
|
if (out_len == 0) {
|
|
/* "." / "./" lists the source root: the whole tree is transferred. */
|
|
result = string_list_add(out, "") ? 1 : -1;
|
|
if (result < 0)
|
|
snprintf(err, err_size, "memory allocation failed");
|
|
} else {
|
|
result = string_list_add(out, dup) ? 1 : -1;
|
|
if (result < 0)
|
|
snprintf(err, err_size, "memory allocation failed");
|
|
}
|
|
free(dup);
|
|
return result;
|
|
}
|
|
|
|
/* Build the membership index over the exact entries only. `file_list_affects`
|
|
combines the exact/descendant lookups with a walk of the query's own ancestor
|
|
prefixes, so no ancestor prefix is ever materialized as a copy and the index
|
|
stays O(entry count) memory regardless of path depth. An empty entry (the
|
|
source root) sets whole_tree and short-circuits every query. */
|
|
static bool file_list_index_build(FileListSet* set, char* err, size_t err_size) {
|
|
if (!path_index_build(&set->index, (const char* const*)set->entries, (size_t)set->count)) {
|
|
snprintf(err, err_size, "memory allocation failed");
|
|
return false;
|
|
}
|
|
for (int i = 0; i < set->count; i++) {
|
|
if (set->entries[i][0] == '\0') {
|
|
set->whole_tree = true;
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static FileListSet* string_list_to_set(StringList* raw, char* err, size_t err_size) {
|
|
FileListSet* set = calloc(1, sizeof(FileListSet));
|
|
if (!set) {
|
|
snprintf(err, err_size, "memory allocation failed");
|
|
return NULL;
|
|
}
|
|
set->count = raw->count;
|
|
set->entries = raw->items;
|
|
raw->items = NULL;
|
|
raw->count = 0;
|
|
if (!file_list_index_build(set, err, err_size)) {
|
|
file_list_destroy(set);
|
|
return NULL;
|
|
}
|
|
return set;
|
|
}
|
|
|
|
FileListSet* file_list_load(const char* path, bool null_separated, char* err, size_t err_size) {
|
|
if (err && err_size > 0)
|
|
err[0] = '\0';
|
|
if (!path || !*path) {
|
|
snprintf(err, err_size, "no file given");
|
|
return NULL;
|
|
}
|
|
FILE* fp = fopen(path, "r");
|
|
if (!fp) {
|
|
char* escaped = output_escape(path, false);
|
|
snprintf(err, err_size, "could not open '%s': %s", escaped ? escaped : path, strerror(errno));
|
|
free(escaped);
|
|
return NULL;
|
|
}
|
|
|
|
StringList raw = {0};
|
|
char* line = NULL;
|
|
size_t line_cap = 0;
|
|
bool ok = true;
|
|
char delim = null_separated ? '\0' : '\n';
|
|
while (ok) {
|
|
ssize_t n = utils_getdelim_bounded(fp, &line, &line_cap, delim, UTILS_MAX_LINE_LEN);
|
|
if (n < 0) {
|
|
if (errno == EFBIG)
|
|
snprintf(err, err_size, "entry in file list exceeds %d bytes", (int)UTILS_MAX_LINE_LEN);
|
|
else
|
|
snprintf(err, err_size, "error reading file list: %s", strerror(errno));
|
|
ok = false;
|
|
break;
|
|
}
|
|
if (n == 0)
|
|
break;
|
|
int r = normalize_entry(line, (size_t)n, !null_separated, &raw, err, err_size);
|
|
if (r < 0) {
|
|
ok = false;
|
|
break;
|
|
}
|
|
}
|
|
free(line);
|
|
fclose(fp);
|
|
if (!ok) {
|
|
string_list_destroy(&raw);
|
|
return NULL;
|
|
}
|
|
FileListSet* set = string_list_to_set(&raw, err, err_size);
|
|
if (!set)
|
|
string_list_destroy(&raw);
|
|
return set;
|
|
}
|
|
|
|
void file_list_destroy(FileListSet* set) {
|
|
if (!set)
|
|
return;
|
|
path_index_free(&set->index);
|
|
for (int i = 0; i < set->count; i++)
|
|
free(set->entries[i]);
|
|
free(set->entries);
|
|
free(set);
|
|
}
|
|
|
|
bool file_list_affects(const FileListSet* set, const char* rel) {
|
|
if (!set)
|
|
return true;
|
|
if (!rel)
|
|
return false;
|
|
if (set->whole_tree)
|
|
return true; /* whole tree listed */
|
|
/* An exact entry match means `rel` itself is listed. */
|
|
if (path_index_contains(&set->index, rel))
|
|
return true;
|
|
/* Otherwise `rel` is affected when a listed entry is an ancestor directory of
|
|
it; walk rel's own directory prefixes (which preserve path-boundary
|
|
semantics) and test each for an exact entry. No prefixes are stored. */
|
|
size_t len = strlen(rel);
|
|
while (len > 0) {
|
|
const char* slash = NULL;
|
|
for (size_t i = len; i-- > 0;) {
|
|
if (rel[i] == '/') {
|
|
slash = rel + i;
|
|
break;
|
|
}
|
|
}
|
|
if (!slash)
|
|
break;
|
|
len = (size_t)(slash - rel);
|
|
if (path_index_contains_n(&set->index, rel, len))
|
|
return true;
|
|
}
|
|
/* Finally `rel` is affected when it is an ancestor directory of a listed
|
|
entry (binary search for the first entry at or after `rel` + '/'). */
|
|
return path_index_has_descendant(&set->index, rel);
|
|
}
|