Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/pipeline/pipeline_incremental.c
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,10 @@ static int semantic_manifest_walk_controls(semantic_manifest_builder_t *builder,
}
cbm_dir_t *dir = cbm_opendir(abs_dir);
if (!dir) {
return CBM_NOT_FOUND;
/* A nested directory we can't open has no control files to
* contribute; skip it like discover.c's walk_dir already does.
* The root call (depth 0) still fails closed. */
return depth == 0 ? CBM_NOT_FOUND : 0;
}
int rc = 0;
cbm_dirent_t *entry;
Expand Down
42 changes: 42 additions & 0 deletions tests/test_pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -4123,6 +4123,45 @@ TEST(pipeline_semantic_manifest_rejects_non_directory_root) {
PASS();
}

#ifndef _WIN32
/* A nested directory the process cannot open (permission-denied) must not
* abort the whole manifest walk: it has no control files to contribute, so
* it is skipped, and control files in sibling directories still get found.
* Root-not-a-directory (above) is a different, still fail-closed case. */
TEST(pipeline_semantic_manifest_skips_unreadable_subdirectory) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_manifest_locked_dir_XXXXXX");
ASSERT_NOT_NULL(cbm_mkdtemp(tmp));
write_temp_file(tmp, "readable.py", "def Readable():\n return 1\n");
write_temp_file(tmp, "locked/secret.txt", "should never be opened\n");
write_temp_file(tmp, "sibling/.gitignore", "*.log\n");

char locked_path[512];
snprintf(locked_path, sizeof(locked_path), "%s/locked", tmp);
ASSERT_EQ(chmod(locked_path, 0), 0);

cbm_file_hash_t *manifest = NULL;
int manifest_count = -1;
int rc = cbm_pipeline_build_semantic_manifest("manifest-skip-locked", tmp, NULL, 0, NULL, 0,
NULL, NULL, &manifest, &manifest_count);

bool found_sibling_gitignore = false;
for (int i = 0; i < manifest_count; i++) {
if (manifest[i].rel_path && strcmp(manifest[i].rel_path, "sibling/.gitignore") == 0) {
found_sibling_gitignore = true;
}
}

chmod(locked_path, 0755);
cbm_pipeline_free_semantic_manifest(manifest, manifest_count > 0 ? manifest_count : 0);
th_rmtree(tmp);

ASSERT_EQ(rc, 0);
ASSERT_TRUE(found_sibling_gitignore);
PASS();
}
#endif /* !_WIN32 */

/* A fully validated staged graph must be able to recover from a definitely
* non-SQLite destination without deleting evidence or overwriting an earlier
* quarantine. The replacement happens only after the corrupt bytes are moved. */
Expand Down Expand Up @@ -13862,6 +13901,9 @@ SUITE(pipeline_semantic_manifest_repro) {
RUN_TEST(pipeline_incremental_successful_publication_preserves_adr);
RUN_TEST(pipeline_full_adr_capture_failure_preserves_previous_generation);
RUN_TEST(pipeline_semantic_manifest_rejects_non_directory_root);
#ifndef _WIN32
RUN_TEST(pipeline_semantic_manifest_skips_unreadable_subdirectory);
#endif
RUN_TEST(pipeline_full_reindex_quarantines_corrupt_destination_without_overwrite);
RUN_TEST(pipeline_full_reindex_replaces_legacy_schema_without_quarantine);
#endif
Expand Down
Loading