Skip to content

Commit 55206a8

Browse files
authored
Merge pull request DeusData#2903 from tanishqbaweja/fix/manifest-walk-nul-file
fix(pipeline): skip a file named nul in the control-file walk instead of aborting the index (DeusData#2646)
2 parents c1af82f + f139c40 commit 55206a8

2 files changed

Lines changed: 144 additions & 13 deletions

File tree

‎src/pipeline/pipeline_incremental.c‎

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -379,10 +379,37 @@ static int semantic_manifest_walk_controls(semantic_manifest_builder_t *builder,
379379
const char *rel_dir, int depth, char **excluded_dirs,
380380
int excluded_count);
381381

382+
/* Control files are the only regular files the walk records. */
383+
static bool semantic_manifest_control_name(const char *rel_dir, const char *name) {
384+
bool root_control = (!rel_dir || !rel_dir[0]) && (strcmp(name, ".cbmignore") == 0 ||
385+
strcmp(name, ".codebase-memory.json") == 0);
386+
/* A build context's ignore file (or BuildKit's <Dockerfile>.dockerignore)
387+
* shapes which files an image holds (pass_docker.c image_ignore), and
388+
* discovery indexes neither. */
389+
static const char DOCKERIGNORE[] = ".dockerignore";
390+
size_t nl = strlen(name);
391+
bool dockerignore = nl >= sizeof(DOCKERIGNORE) - SKIP_ONE &&
392+
strcmp(name + nl - (sizeof(DOCKERIGNORE) - SKIP_ONE), DOCKERIGNORE) == 0;
393+
return strcmp(name, ".gitignore") == 0 || root_control ||
394+
semantic_manifest_package_control(name) || dockerignore;
395+
}
396+
397+
/* True when the directory listing alone shows that the entry is not a
398+
* directory. A Windows listing always carries the attributes; on POSIX a
399+
* d_type of DT_UNKNOWN (0), which readdir could not resolve, proves nothing. */
400+
static bool semantic_manifest_listed_non_directory(const cbm_dirent_t *entry) {
401+
#ifdef _WIN32
402+
return !entry->is_dir;
403+
#else
404+
return entry->d_type != 0 && !entry->is_dir;
405+
#endif
406+
}
407+
382408
static int semantic_manifest_control_entry(semantic_manifest_builder_t *builder,
383409
const char *project, const char *abs_dir,
384410
const char *rel_dir, int depth, char **excluded_dirs,
385-
int excluded_count, const char *name) {
411+
int excluded_count, const cbm_dirent_t *entry) {
412+
const char *name = entry->name;
386413
if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0)
387414
return 0;
388415
char abs_path[CBM_SZ_4K], rel_path[CBM_SZ_4K];
@@ -392,6 +419,15 @@ static int semantic_manifest_control_entry(semantic_manifest_builder_t *builder,
392419
if (abs_n < 0 || (size_t)abs_n >= sizeof(abs_path) || rel_n < 0 ||
393420
(size_t)rel_n >= sizeof(rel_path))
394421
return CBM_NOT_FOUND;
422+
/* Only directories and control files contribute, so an entry the listing
423+
* already shows to be neither is skipped without reading its metadata.
424+
* That read can fail on such an entry: on Windows the plain path of a file
425+
* named nul names the NUL device (#2646), and the plain path of a name
426+
* ending in a dot or a space does not reach the file. */
427+
bool control = semantic_manifest_control_name(rel_dir, name);
428+
if (!control && semantic_manifest_listed_non_directory(entry)) {
429+
return 0;
430+
}
395431
cbm_path_info_t info;
396432
if (cbm_path_info_utf8(abs_path, &info) != 0)
397433
return CBM_NOT_FOUND;
@@ -404,17 +440,6 @@ static int semantic_manifest_control_entry(semantic_manifest_builder_t *builder,
404440
return semantic_manifest_walk_controls(builder, project, abs_path, rel_path, depth + 1,
405441
excluded_dirs, excluded_count);
406442
}
407-
bool root_control = (!rel_dir || !rel_dir[0]) && (strcmp(name, ".cbmignore") == 0 ||
408-
strcmp(name, ".codebase-memory.json") == 0);
409-
/* A build context's ignore file (or BuildKit's <Dockerfile>.dockerignore)
410-
* shapes which files an image holds (pass_docker.c image_ignore), and
411-
* discovery indexes neither. */
412-
static const char DOCKERIGNORE[] = ".dockerignore";
413-
size_t nl = strlen(name);
414-
bool dockerignore = nl >= sizeof(DOCKERIGNORE) - SKIP_ONE &&
415-
strcmp(name + nl - (sizeof(DOCKERIGNORE) - SKIP_ONE), DOCKERIGNORE) == 0;
416-
bool control = strcmp(name, ".gitignore") == 0 || root_control ||
417-
semantic_manifest_package_control(name) || dockerignore;
418443
return info.is_regular && control ? semantic_manifest_add(builder, project, rel_path, abs_path)
419444
: 0;
420445
}
@@ -439,7 +464,7 @@ static int semantic_manifest_walk_controls(semantic_manifest_builder_t *builder,
439464
rc = cbm_pipeline_frozen_checkpoint(builder->frozen);
440465
if (rc == 0)
441466
rc = semantic_manifest_control_entry(builder, project, abs_dir, rel_dir, depth,
442-
excluded_dirs, excluded_count, entry->name);
467+
excluded_dirs, excluded_count, entry);
443468
}
444469
cbm_closedir(dir);
445470
return rc;

‎tests/test_pipeline.c‎

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9708,6 +9708,108 @@ TEST(pipeline_semantic_manifest_skips_unreadable_subdirectory) {
97089708
}
97099709
#endif /* !_WIN32 */
97109710

9711+
#ifdef _WIN32
9712+
/* Create or delete a real file named nul in dir, as a Git Bash redirect such
9713+
* as `cmd > nul` leaves behind (#2646). The plain Win32 path <dir>/nul names
9714+
* the NUL device, so the file is reached only through the extended-length
9715+
* namespace, which needs an absolute path with backslashes (CI's TEMP uses
9716+
* forward slashes). */
9717+
static bool pipeline_test_nul_file(const char *dir, bool create) {
9718+
enum { NUL_PATH_CAP = 1024, PREFIX_LEN = 4, LEAF_LEN = 4 };
9719+
static const wchar_t prefix[] = L"\\\\?\\";
9720+
static const wchar_t leaf[] = L"\\nul";
9721+
wchar_t path[NUL_PATH_CAP];
9722+
const DWORD room = NUL_PATH_CAP - PREFIX_LEN - LEAF_LEN;
9723+
wchar_t *wide_dir = cbm_utf8_to_wide(dir);
9724+
DWORD length = wide_dir ? GetFullPathNameW(wide_dir, room, path + PREFIX_LEN, NULL) : 0;
9725+
free(wide_dir);
9726+
if (length == 0 || length >= room) {
9727+
return false;
9728+
}
9729+
wmemcpy(path, prefix, PREFIX_LEN);
9730+
wmemcpy(path + PREFIX_LEN + length, leaf, LEAF_LEN + 1);
9731+
if (!create) {
9732+
return DeleteFileW(path) != 0;
9733+
}
9734+
HANDLE file =
9735+
CreateFileW(path, GENERIC_WRITE, 0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
9736+
if (file == INVALID_HANDLE_VALUE) {
9737+
return false;
9738+
}
9739+
CloseHandle(file);
9740+
return true;
9741+
}
9742+
9743+
/* #2646: the control-file walk cannot inspect a file named nul through the
9744+
* plain path, and it never needs to: only directories and control files can
9745+
* contribute to the manifest. Listing nul in .gitignore does not help, since
9746+
* the walk visits every entry. */
9747+
TEST(pipeline_semantic_manifest_skips_nul_device_file_issue2646) {
9748+
char tmp[256];
9749+
snprintf(tmp, sizeof(tmp), "/tmp/cbm_manifest_nul_XXXXXX");
9750+
ASSERT_NOT_NULL(cbm_mkdtemp(tmp));
9751+
write_temp_file(tmp, "app.py", "def NulNeighbor():\n return 1\n");
9752+
write_temp_file(tmp, ".gitignore", "nul\n");
9753+
bool created = pipeline_test_nul_file(tmp, true);
9754+
9755+
cbm_file_hash_t *manifest = NULL;
9756+
int manifest_count = -1;
9757+
int rc = cbm_pipeline_build_semantic_manifest(NULL, "manifest-skip-nul", tmp, NULL, 0, NULL, 0,
9758+
NULL, NULL, &manifest, &manifest_count);
9759+
bool found_gitignore = false;
9760+
for (int i = 0; i < manifest_count; i++) {
9761+
if (manifest[i].rel_path && strcmp(manifest[i].rel_path, ".gitignore") == 0) {
9762+
found_gitignore = true;
9763+
}
9764+
}
9765+
cbm_pipeline_free_semantic_manifest(manifest, manifest_count > 0 ? manifest_count : 0);
9766+
bool deleted = created && pipeline_test_nul_file(tmp, false);
9767+
th_rmtree(tmp);
9768+
9769+
ASSERT_TRUE(created);
9770+
ASSERT_EQ(rc, 0);
9771+
ASSERT_TRUE(found_gitignore);
9772+
ASSERT_TRUE(deleted);
9773+
PASS();
9774+
}
9775+
9776+
/* The user-visible half of #2646: one such file made every index run return
9777+
* CBM_PIPELINE_ABORT_PRESERVE_DB (aborted_previous_preserved), so a new
9778+
* project never got an index. */
9779+
TEST(pipeline_full_index_survives_nul_device_file_issue2646) {
9780+
char tmp[256];
9781+
snprintf(tmp, sizeof(tmp), "/tmp/cbm_index_nul_XXXXXX");
9782+
ASSERT_NOT_NULL(cbm_mkdtemp(tmp));
9783+
write_temp_file(tmp, "app.py", "def NulNeighbor():\n return 1\n");
9784+
write_temp_file(tmp, ".gitignore", "nul\n");
9785+
bool created = pipeline_test_nul_file(tmp, true);
9786+
9787+
char db_path[512];
9788+
snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp);
9789+
cbm_pipeline_t *pipeline = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
9790+
int run_rc = pipeline ? cbm_pipeline_run(pipeline) : -1;
9791+
char project[256] = "";
9792+
if (pipeline) {
9793+
snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(pipeline));
9794+
cbm_pipeline_free(pipeline);
9795+
}
9796+
int indexed = -1;
9797+
cbm_store_t *store = cbm_store_open_path_query(db_path);
9798+
if (store) {
9799+
indexed = named_node_count(store, project, "NulNeighbor");
9800+
cbm_store_close(store);
9801+
}
9802+
bool deleted = created && pipeline_test_nul_file(tmp, false);
9803+
th_rmtree(tmp);
9804+
9805+
ASSERT_TRUE(created);
9806+
ASSERT_EQ(run_rc, 0);
9807+
ASSERT_EQ(indexed, 1);
9808+
ASSERT_TRUE(deleted);
9809+
PASS();
9810+
}
9811+
#endif /* _WIN32 */
9812+
97119813
/* A fully validated staged graph must be able to recover from a definitely
97129814
* non-SQLite destination without deleting evidence or overwriting an earlier
97139815
* quarantine. The replacement happens only after the corrupt bytes are moved. */
@@ -32362,6 +32464,10 @@ SUITE(pipeline_semantic_manifest_repro) {
3236232464
RUN_TEST(pipeline_semantic_manifest_rejects_non_directory_root);
3236332465
#ifndef _WIN32
3236432466
RUN_TEST(pipeline_semantic_manifest_skips_unreadable_subdirectory);
32467+
#endif
32468+
#ifdef _WIN32
32469+
RUN_TEST(pipeline_semantic_manifest_skips_nul_device_file_issue2646);
32470+
RUN_TEST(pipeline_full_index_survives_nul_device_file_issue2646);
3236532471
#endif
3236632472
RUN_TEST(pipeline_full_reindex_quarantines_corrupt_destination_without_overwrite);
3236732473
RUN_TEST(pipeline_full_reindex_replaces_legacy_schema_without_quarantine);

0 commit comments

Comments
 (0)