fix: size the compress path with size_t and prune the cache from the walk

Two loose ends from the bounds work.

The decompression side already took size_t so a >2 GiB capacity could not wrap
through int, but compression still took int for both the source length and the
destination capacity, and the artifact export cast a size_t database size down
to reach it. A database past 2 GiB would have handed the encoder a negative
length. Both lengths and the bound helper are size_t now, and the function
returns int64_t like its decompressing counterpart.

The discovery walk now prunes the cache directory by absolute path. A custom
CBM_CACHE_DIR may sit inside a repository — tests do it routinely — and walking
into it pulls every other project's graph database into this project's file
list. This is the narrow form of a concern that was briefly implemented as
refusing any root that contained the cache; refusing a whole root was too blunt,
and not walking the cache is what the concern actually asks for.

The test fails without the prune: a .go file planted under the cache is
otherwise discovered, and "cache" is not in the built-in skip list, so the
assertion is not vacuous.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-08-06 16:06:43 +02:00
1 parent 0d6f26a170
commit 7814dacf15
5 files changed
+79 -11

No files matched your search

+5 -5
View File
@@ -7,12 +7,12 @@
#include <stddef.h>
#include <stdint.h>
int cbm_zstd_compress(const char *src, int srcLen, char *dst, int dstCap, int level) {
size_t rc = ZSTD_compress(dst, (size_t)dstCap, src, (size_t)srcLen, level);
int64_t cbm_zstd_compress(const char *src, size_t srcLen, char *dst, size_t dstCap, int level) {
size_t rc = ZSTD_compress(dst, dstCap, src, srcLen, level);
if (ZSTD_isError(rc)) {
return 0;
}
return (int)rc;
return (int64_t)rc;
}
int64_t cbm_zstd_decompress(const char *src, size_t srcLen, char *dst, size_t dstCap) {
@@ -31,6 +31,6 @@ size_t cbm_zstd_frame_content_size(const char *src, size_t srcLen) {
return (size_t)n;
}
size_t cbm_zstd_compress_bound(int inputSize) {
return ZSTD_compressBound((size_t)inputSize);
size_t cbm_zstd_compress_bound(size_t inputSize) {
return ZSTD_compressBound(inputSize);
}
+5 -3
View File
@@ -5,8 +5,10 @@
#include <stdint.h>
// Zstd compression at specified level (1=fast .. 22=best).
// Returns compressed size on success, 0 on error.
int cbm_zstd_compress(const char *src, int srcLen, char *dst, int dstCap, int level);
// srcLen/dstCap are size_t for the same reason decompression's are: a >2 GiB
// database would otherwise wrap through int and hand the encoder a capacity that
// disagrees with the real buffer. Returns compressed size on success, 0 on error.
int64_t cbm_zstd_compress(const char *src, size_t srcLen, char *dst, size_t dstCap, int level);
// Zstd decompression. srcLen/dstCap are size_t so a >2 GiB destination capacity
// is never truncated (a large DB artifact, or a crafted one, would otherwise
@@ -20,6 +22,6 @@ int64_t cbm_zstd_decompress(const char *src, size_t srcLen, char *dst, size_t ds
size_t cbm_zstd_frame_content_size(const char *src, size_t srcLen);
// Maximum compressed size bound for given input size.
size_t cbm_zstd_compress_bound(int inputSize);
size_t cbm_zstd_compress_bound(size_t inputSize);
#endif // CBM_ZSTD_STORE_H
+27 -1
View File
@@ -13,6 +13,7 @@
#include "foundation/constants.h"
#include "foundation/compat_fs.h"
#include "foundation/workspace.h"
#include "foundation/platform.h"
#ifdef _WIN32
#include "foundation/win_utf8.h"
@@ -562,6 +563,30 @@ static bool is_safety_core_dir(const char *name) {
}
/* Check if a directory entry should be skipped (hardcoded dirs + gitignore). */
/* The cache directory holds every indexed project's graph database. When a custom
* CBM_CACHE_DIR sits inside a repository — which happens in tests and is legal in
* production — walking into it would pull other projects' databases into this
* project's file list. Prune it by absolute path.
*
* This is the narrow remedy for a concern that was briefly implemented as
* refusing any root containing the cache: refusing a whole root was too blunt,
* and not walking the cache is what the concern actually asks for. */
static bool dir_is_cache_tree(const char *abs_path) {
const char *cache = cbm_workspace_cache_dir();
if (!cache || !cache[0] || !abs_path || !abs_path[0]) {
return false;
}
size_t n = strlen(cache);
while (n > 1 && (cache[n - 1] == '/' || cache[n - 1] == '\\')) {
n--;
}
if (strncmp(abs_path, cache, n) != 0) {
return false;
}
/* Boundary-aware so "<cache>x" is not treated as living under "<cache>". */
return abs_path[n] == '\0' || abs_path[n] == '/' || abs_path[n] == '\\';
}
static bool should_skip_directory(const char *entry_name, const char *rel_path,
const cbm_discover_opts_t *opts, const cbm_gitignore_t *gitignore,
const cbm_gitignore_t *global_gi,
@@ -853,7 +878,8 @@ static void walk_dir_process_entry(cbm_dirent_t *entry, const walk_frame_t *fram
}
if (S_ISDIR(st.st_mode)) {
if (!should_skip_directory(entry->name, rel_path, opts, gitignore, global_gi, cbmignore,
if (!dir_is_cache_tree(abs_path) &&
!should_skip_directory(entry->name, rel_path, opts, gitignore, global_gi, cbmignore,
frame->local_gi, frame->local_gi_prefix)) {
walk_push_subdir(ws, abs_path, rel_path, frame, out);
} else {
+2 -2
View File
@@ -589,14 +589,14 @@ int cbm_artifact_export(const char *db_path, const char *repo_path, const char *
}
/* Compress with zstd */
size_t bound = cbm_zstd_compress_bound((int)db_size);
size_t bound = cbm_zstd_compress_bound(db_size);
char *compressed = malloc(bound);
if (!compressed) {
free(db_data);
return artifact_export_fail("compress", NULL, "alloc_compressed_buffer", 0);
}
int clen = cbm_zstd_compress(db_data, (int)db_size, compressed, (int)bound, compression_level);
int64_t clen = cbm_zstd_compress(db_data, db_size, compressed, bound, compression_level);
free(db_data);
if (clen <= 0) {
+40
View File
@@ -297,6 +297,45 @@ TEST(pattern_dts_full) {
/* ── File discovery (integration) — cross-platform via test_helpers.h ── */
/* A custom CBM_CACHE_DIR may legitimately sit inside a repository — tests do it
* routinely. Walking into it would pull every other project's graph database into
* this project's file list, so the walk prunes it by absolute path. */
TEST(discover_prunes_the_cache_tree) {
char *base = th_mktempdir("cbm_disc_cache");
ASSERT(base != NULL);
th_write_file(TH_PATH(base, "src/main.go"), "package main\n");
/* Source-looking files inside the cache must not be discovered. */
th_write_file(TH_PATH(base, "cache/other_project/leaked.go"), "package leaked\n");
const char *saved = getenv("CBM_CACHE_DIR");
char *saved_copy = saved ? strdup(saved) : NULL;
char cache_dir[1024];
snprintf(cache_dir, sizeof(cache_dir), "%s/cache", base);
cbm_setenv("CBM_CACHE_DIR", cache_dir, 1);
cbm_discover_opts_t opts = {0};
cbm_file_info_t *files = NULL;
int count = 0;
int rc = cbm_discover(base, &opts, &files, &count);
if (saved_copy) {
cbm_setenv("CBM_CACHE_DIR", saved_copy, 1);
free(saved_copy);
} else {
cbm_unsetenv("CBM_CACHE_DIR");
}
ASSERT_EQ(rc, 0);
for (int i = 0; i < count; i++) {
ASSERT(strstr(files[i].rel_path, "leaked.go") == NULL);
}
ASSERT_EQ(count, 1); /* only src/main.go */
cbm_discover_free(files, count);
th_cleanup(base);
PASS();
}
TEST(discover_simple) {
char *base = th_mktempdir("cbm_disc_simple");
ASSERT(base != NULL);
@@ -1359,6 +1398,7 @@ TEST(discover_many_nested_gitignores_do_not_exhaust_matcher_ownership) {
/* ── Suite ─────────────────────────────────────────────────────── */
SUITE(discover) {
RUN_TEST(discover_prunes_the_cache_tree);
/* Directory skip — always */
RUN_TEST(skip_git);
RUN_TEST(skip_node_modules);