mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-07 08:43:35 +08:00
Merge pull request #745 from DeusData/fix/project-name-length-cap
fix(fqn): cap derived project-name length to avoid ENAMETOOLONG
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h> // NULL
|
||||
#include <stdint.h> // uint32_t
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h> // strdup
|
||||
@@ -18,6 +19,13 @@
|
||||
#define FQN_MAX_PATH_SEGS 254
|
||||
#define FQN_MAX_DIR_SEGS 255
|
||||
|
||||
/* Max bytes for a derived project name. The name becomes a filename component
|
||||
* ("<cache>/<name>.db" and sidecars ".db-wal"/".db.corrupt"), so it must stay
|
||||
* under the filesystem's 255-byte component limit. 200 leaves headroom for the
|
||||
* longest sidecar suffix. #571 hex-encodes each non-ASCII byte to 2 chars, so a
|
||||
* deep CJK path can triple past 255 and make the DB file un-openable (#624). */
|
||||
#define FQN_MAX_NAME_LEN 200
|
||||
|
||||
/* ── Internal helpers ─────────────────────────────────────────────── */
|
||||
|
||||
/* Build a dot-joined string from segments. Returns heap-allocated string. */
|
||||
@@ -351,6 +359,31 @@ char *cbm_pipeline_fqn_folder(const char *project, const char *rel_dir) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Bound a derived project name to FQN_MAX_NAME_LEN bytes so "<cache>/<name>.db"
|
||||
* stays within the filesystem's 255-byte filename-component limit (#624). Names
|
||||
* within the cap are returned UNCHANGED (no drift). Longer names keep their first
|
||||
* (CAP-9) bytes and get a "-XXXXXXXX" FNV-1a hash of the FULL name appended, so
|
||||
* two long paths that share a prefix but differ later still map to distinct
|
||||
* names. The suffix ends in a hex digit, so the result stays validator-safe. */
|
||||
static char *fqn_bound_name_len(char *name) {
|
||||
if (!name) {
|
||||
return name;
|
||||
}
|
||||
size_t n = strlen(name);
|
||||
if (n <= FQN_MAX_NAME_LEN) {
|
||||
return name; /* within cap → unchanged, no drift */
|
||||
}
|
||||
uint32_t h = 2166136261u; /* FNV-1a offset basis over the FULL name */
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
h ^= (unsigned char)name[i];
|
||||
h *= 16777619u;
|
||||
}
|
||||
/* Keep first (CAP-9) bytes + "-" + 8 hex = CAP total. The buffer holds n+1
|
||||
* bytes and n > CAP, so writing 9 chars + NUL at offset (CAP-9) fits. */
|
||||
snprintf(name + (FQN_MAX_NAME_LEN - 9), 10, "-%08x", h);
|
||||
return name;
|
||||
}
|
||||
|
||||
char *cbm_project_name_from_path(const char *abs_path) {
|
||||
if (!abs_path || !abs_path[0]) {
|
||||
return strdup("root");
|
||||
@@ -433,5 +466,8 @@ char *cbm_project_name_from_path(const char *abs_path) {
|
||||
|
||||
char *result = strdup(start);
|
||||
free(path);
|
||||
if (result) {
|
||||
result = fqn_bound_name_len(result); /* #624: cap filename-component length */
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -491,6 +491,63 @@ TEST(project_name_encodes_unicode_segments_issue571) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* issue #624: #571 preserves non-ASCII path segments by hex-encoding each byte
|
||||
* (1 byte -> 2 hex chars), so a DEEP non-ASCII path triples in length and can
|
||||
* blow past the filesystem's 255-byte filename-component limit. Then
|
||||
* "<cache>/<name>.db" is un-openable (ENAMETOOLONG). The derived name must be
|
||||
* length-capped (with a hash suffix that disambiguates otherwise-identical
|
||||
* prefixes) while staying validator-safe — and SHORT names must NOT drift. */
|
||||
#define FQN_CAP_UNDER_TEST 200
|
||||
TEST(project_name_length_capped_issue624) {
|
||||
/* 开 = U+5F00, UTF-8 "\xe5\xbc\x80" (3 bytes -> 6 hex chars). 60 copies makes
|
||||
* the raw hex-encoded name ~360 chars, well past the 200-byte cap. */
|
||||
static const char KAI[] = "\xe5\xbc\x80";
|
||||
char deepA[512];
|
||||
char deepB[512];
|
||||
const char *prefix = "/Users/dev/";
|
||||
size_t p = strlen(prefix);
|
||||
memcpy(deepA, prefix, p);
|
||||
for (int i = 0; i < 60; i++) {
|
||||
memcpy(deepA + p, KAI, 3);
|
||||
p += 3;
|
||||
}
|
||||
/* deepB shares the entire deep prefix and differs ONLY in the trailing seg */
|
||||
memcpy(deepB, deepA, p);
|
||||
memcpy(deepA + p, "/alpha", 7); /* includes NUL */
|
||||
memcpy(deepB + p, "/omega", 7);
|
||||
|
||||
char *nameA = cbm_project_name_from_path(deepA);
|
||||
char *nameB = cbm_project_name_from_path(deepB);
|
||||
ASSERT_NOT_NULL(nameA);
|
||||
ASSERT_NOT_NULL(nameB);
|
||||
|
||||
/* (a) capped: name must fit within the filename-component budget */
|
||||
ASSERT_LTE(strlen(nameA), FQN_CAP_UNDER_TEST);
|
||||
ASSERT_LTE(strlen(nameB), FQN_CAP_UNDER_TEST);
|
||||
/* (b) still a valid project name (resolve_store must accept it) */
|
||||
ASSERT_TRUE(cbm_validate_project_name(nameA));
|
||||
ASSERT_TRUE(cbm_validate_project_name(nameB));
|
||||
/* (c) two deep paths differing only in the trailing segment must NOT
|
||||
* collide after capping — the hash suffix disambiguates them. */
|
||||
ASSERT_STR_NEQ(nameA, nameB);
|
||||
|
||||
free(nameA);
|
||||
free(nameB);
|
||||
|
||||
/* Short CJK path (the issue #571 case) must be UNCHANGED — no drift. */
|
||||
char *shortName = cbm_project_name_from_path(
|
||||
"/Users/yunxin/Desktop/\xe5\xbc\x80\xe5\x8f\x91/"
|
||||
"\xe5\x90\x8e\xe7\xab\xaf/"
|
||||
"\xe4\xbf\xa1\xe7\xa7\x9f\xe9\xa3\x8e\xe6\x8e\xa7\xe9\x80\x9a\xe5\x90\x8e\xe7\xab\xaf");
|
||||
ASSERT_NOT_NULL(shortName);
|
||||
ASSERT_LTE(strlen(shortName), FQN_CAP_UNDER_TEST);
|
||||
ASSERT_STR_EQ(shortName, "Users-yunxin-Desktop-e5bc80e58f91-e5908ee7abaf-"
|
||||
"e4bfa1e7a79fe9a38ee68ea7e9809ae5908ee7abaf");
|
||||
free(shortName);
|
||||
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
* Suite
|
||||
* ================================================================ */
|
||||
@@ -593,6 +650,7 @@ SUITE(fqn) {
|
||||
RUN_TEST(project_name_deep_path);
|
||||
RUN_TEST(project_name_always_validator_safe_issue349);
|
||||
RUN_TEST(project_name_encodes_unicode_segments_issue571);
|
||||
RUN_TEST(project_name_length_capped_issue624);
|
||||
RUN_TEST(project_name_colon_only);
|
||||
RUN_TEST(project_name_backslash_only);
|
||||
RUN_TEST(project_name_consecutive_colons);
|
||||
|
||||
Reference in New Issue
Block a user