diff --git a/src/cli/windows_launcher_state.h b/src/cli/windows_launcher_state.h index 2948ff8b7..c306ad7b3 100644 --- a/src/cli/windows_launcher_state.h +++ b/src/cli/windows_launcher_state.h @@ -16,6 +16,7 @@ #define CBM_WINDOWS_CURRENT_V1_SIZE 128U #define CBM_WINDOWS_RELEASE_DESCRIPTOR_V1_SIZE 128U +#define CBM_WINDOWS_PAYLOAD_SHA256_BUF_LEN 65U #ifndef CBM_WINDOWS_LAUNCHER_ABI_CURRENT #define CBM_WINDOWS_LAUNCHER_ABI_CURRENT 1U #endif @@ -37,7 +38,7 @@ typedef struct { uint32_t launcher_abi_min; uint32_t launcher_abi_max; uint64_t payload_size; - char payload_sha256[65]; + char payload_sha256[CBM_WINDOWS_PAYLOAD_SHA256_BUF_LEN]; } cbm_windows_current_v1_t; typedef struct { @@ -45,7 +46,7 @@ typedef struct { uint32_t payload_launcher_abi_min; uint32_t payload_launcher_abi_max; uint64_t payload_size; - char payload_sha256[65]; + char payload_sha256[CBM_WINDOWS_PAYLOAD_SHA256_BUF_LEN]; } cbm_windows_release_descriptor_v1_t; typedef enum { @@ -77,17 +78,19 @@ cbm_windows_transition_plan_t cbm_windows_transition_plan( /* Resolve the immutable generation pair using the canonical launcher's * directory. A managed generation contains exactly these two executables: * the payload and the launcher's hard-link backing. */ -bool cbm_windows_generation_payload_path(const wchar_t *canonical_launcher_path, - const char payload_sha256[65], wchar_t *path_out, - size_t path_capacity); -bool cbm_windows_generation_launcher_path(const wchar_t *canonical_launcher_path, - const char payload_sha256[65], wchar_t *path_out, - size_t path_capacity); +bool cbm_windows_generation_payload_path( + const wchar_t *canonical_launcher_path, + const char payload_sha256[CBM_WINDOWS_PAYLOAD_SHA256_BUF_LEN], wchar_t *path_out, + size_t path_capacity); +bool cbm_windows_generation_launcher_path( + const wchar_t *canonical_launcher_path, + const char payload_sha256[CBM_WINDOWS_PAYLOAD_SHA256_BUF_LEN], wchar_t *path_out, + size_t path_capacity); /* Derive the race-free retired state sibling shared by the uninstall payload * (its own PID) and supervising launcher (the authenticated child PID). */ bool cbm_windows_retired_state_path(const wchar_t *canonical_launcher_path, - const char payload_sha256[65], uint32_t payload_pid, - wchar_t *path_out, size_t path_capacity); + const char payload_sha256[CBM_WINDOWS_PAYLOAD_SHA256_BUF_LEN], + uint32_t payload_pid, wchar_t *path_out, size_t path_capacity); /* Match main's top-level dispatch. Tokens after a mode selector (cli, * install, config, hook-augment, help/version) are opaque user input. */ @@ -104,7 +107,7 @@ typedef struct { bool private_activation; cbm_windows_launcher_action_t action; uint64_t payload_size; - char expected_payload_sha256[65]; + char expected_payload_sha256[CBM_WINDOWS_PAYLOAD_SHA256_BUF_LEN]; wchar_t canonical_launcher_path[CBM_WINDOWS_LAUNCHER_PATH_CAP]; /* Opaque one-shot authority retained only until startup validation has * completed. Callers must use context_complete, never inspect it. */ @@ -144,13 +147,13 @@ bool cbm_windows_launcher_replace_atomic(const wchar_t *target_path, const wchar bool cbm_windows_launcher_remove_posix(const wchar_t *target_path, char *error, size_t error_size); /* Retire .cbm to its generation/PID-qualified sibling, then unlink canonical * as the final uninstall commit. A failed unlink restores .cbm. */ -bool cbm_windows_launcher_uninstall_commit(const wchar_t *canonical_launcher_path, - const char payload_sha256[65], char *error, - size_t error_size); -bool cbm_windows_generation_rollback_if_unreferenced(const wchar_t *canonical_launcher_path, - const char payload_sha256[65], - bool created_by_activation, char *error, - size_t error_size); +bool cbm_windows_launcher_uninstall_commit( + const wchar_t *canonical_launcher_path, + const char payload_sha256[CBM_WINDOWS_PAYLOAD_SHA256_BUF_LEN], char *error, size_t error_size); +bool cbm_windows_generation_rollback_if_unreferenced( + const wchar_t *canonical_launcher_path, + const char payload_sha256[CBM_WINDOWS_PAYLOAD_SHA256_BUF_LEN], bool created_by_activation, + char *error, size_t error_size); bool cbm_windows_generations_prune(const wchar_t *canonical_launcher_path, size_t *removed_out, char *error, size_t error_size); diff --git a/src/daemon/bootstrap.h b/src/daemon/bootstrap.h index acb6c657b..ab3a713f0 100644 --- a/src/daemon/bootstrap.h +++ b/src/daemon/bootstrap.h @@ -49,9 +49,11 @@ cbm_daemon_ipc_endpoint_t *cbm_daemon_bootstrap_endpoint_new(const char *runtime * directly (never through a shell), with exactly argv[0] plus the one hidden * internal argument. It is detached from the launching client's lifetime and * inherits no standard handles; logical client leases govern its lifetime. */ +#include "../foundation/constants.h" + typedef struct { const char *executable_path; - const char *argv[CBM_DAEMON_BOOTSTRAP_LAUNCH_ARGC + 1U]; + const char *argv[CBM_DAEMON_BOOTSTRAP_LAUNCH_ARGC + SKIP_ONE]; size_t argc; bool detached; bool inherit_standard_handles; diff --git a/src/daemon/runtime.h b/src/daemon/runtime.h index 6de58f3b2..057c8efef 100644 --- a/src/daemon/runtime.h +++ b/src/daemon/runtime.h @@ -254,6 +254,8 @@ bool cbm_daemon_runtime_request_activation_shutdown( cbm_daemon_runtime_activation_action_t action, uint32_t timeout_ms, cbm_daemon_runtime_activation_result_t *result_out); +#define CBM_DAEMON_SEMVER_SIZE 12U + typedef struct { bool permanent; bool stopping; @@ -262,7 +264,7 @@ typedef struct { uint8_t client_count; /* entries in client_pids, capped at the wire limit */ uint32_t client_pids[CBM_DAEMON_CONTROL_CLIENT_CAP]; char build_fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]; - char semantic_version[12]; + char semantic_version[CBM_DAEMON_SEMVER_SIZE]; } cbm_daemon_runtime_status_t; typedef struct { diff --git a/src/foundation/sha256.h b/src/foundation/sha256.h index bdfcb1b73..2e44d6d0e 100644 --- a/src/foundation/sha256.h +++ b/src/foundation/sha256.h @@ -9,13 +9,17 @@ #include #include -#define CBM_SHA256_DIGEST_LEN 32 /* raw digest bytes */ -#define CBM_SHA256_HEX_LEN 64 /* lowercase hex chars (no NUL) */ +#include "constants.h" + +#define CBM_SHA256_STATE_WORDS CBM_SZ_8 +#define CBM_SHA256_BLOCK_SIZE CBM_SZ_64 +#define CBM_SHA256_DIGEST_LEN CBM_SZ_32 /* raw digest bytes */ +#define CBM_SHA256_HEX_LEN CBM_SZ_64 /* lowercase hex chars (no NUL) */ typedef struct { - uint32_t state[8]; + uint32_t state[CBM_SHA256_STATE_WORDS]; uint64_t bitlen; - uint8_t buf[64]; + uint8_t buf[CBM_SHA256_BLOCK_SIZE]; size_t buflen; } cbm_sha256_ctx; @@ -24,7 +28,7 @@ void cbm_sha256_update(cbm_sha256_ctx *c, const void *data, size_t len); void cbm_sha256_final(cbm_sha256_ctx *c, uint8_t out[CBM_SHA256_DIGEST_LEN]); /* One-shot hash of a buffer to lowercase hex. `out` must hold - * CBM_SHA256_HEX_LEN + 1 bytes (hex chars + NUL). */ -void cbm_sha256_hex(const void *data, size_t len, char out[CBM_SHA256_HEX_LEN + 1]); + * CBM_SHA256_HEX_LEN + SKIP_ONE bytes (hex chars + NUL). */ +void cbm_sha256_hex(const void *data, size_t len, char out[CBM_SHA256_HEX_LEN + SKIP_ONE]); #endif /* CBM_SHA256_H */ diff --git a/src/pipeline/lsp_resolve.h b/src/pipeline/lsp_resolve.h index 2bb72e8f9..12e462ce8 100644 --- a/src/pipeline/lsp_resolve.h +++ b/src/pipeline/lsp_resolve.h @@ -57,7 +57,7 @@ static inline const char *cbm_lsp_bare_segment(const char *name) { * closes the `->` arrow (preceded by '-'); a bare '>' closes a template * argument list ("identity") and must NOT split, else the segment * would be the empty string after the trailing '>'. */ - if (*p == '.' || *p == ':' || (*p == '>' && p != name && p[-1] == '-')) { + if (*p == '.' || *p == ':' || (*p == '>' && p != name && p[-SKIP_ONE] == '-')) { seg = p + SKIP_ONE; } } @@ -81,7 +81,7 @@ static inline const char *cbm_pipeline_qn_class_method_tail(const char *qn) { if (second == qn) { return qn; } - return second + 1; + return second + SKIP_ONE; } } return qn; @@ -110,6 +110,39 @@ static inline int cbm_pipeline_qn_class_method_tail_eq(const char *qn, const cha return qt && tail && strcmp(qt, tail) == 0; } +static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution_tail( + const CBMResolvedCallArray *arr, const CBMCall *call) { + const char *call_tail = cbm_pipeline_qn_class_method_tail(call->enclosing_func_qn); + if (!call_tail) { + return NULL; + } + + const CBMResolvedCall *best_tail = NULL; + for (int i = 0; i < arr->count; i++) { + const CBMResolvedCall *rc = &arr->items[i]; + if (!rc->caller_qn || !rc->callee_qn) { + continue; + } + if (rc->confidence < CBM_LSP_CONFIDENCE_FLOOR) { + continue; + } + const char *short_name = strrchr(rc->callee_qn, '.'); + short_name = short_name ? short_name + SKIP_ONE : rc->callee_qn; + const char *call_leaf = cbm_pipeline_call_callee_leaf(call->callee_name); + if (!call_leaf || strcmp(short_name, call_leaf) != 0) { + continue; + } + if (!cbm_pipeline_qn_class_method_tail_eq(rc->caller_qn, call_tail)) { + continue; + } + if (best_tail) { + return NULL; + } + best_tail = rc; + } + return best_tail; +} + /* Look up the highest-confidence LSP-resolved call entry whose caller QN * matches the textual call's enclosing function and whose callee QN * short-name matches the textual callee. Returns a pointer into `arr` @@ -191,36 +224,7 @@ static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution( if (!allow_tail_match) { return NULL; } - - const char *call_tail = cbm_pipeline_qn_class_method_tail(call->enclosing_func_qn); - if (!call_tail) { - return NULL; - } - - const CBMResolvedCall *best_tail = NULL; - for (int i = 0; i < arr->count; i++) { - const CBMResolvedCall *rc = &arr->items[i]; - if (!rc->caller_qn || !rc->callee_qn) { - continue; - } - if (rc->confidence < CBM_LSP_CONFIDENCE_FLOOR) { - continue; - } - const char *short_name = strrchr(rc->callee_qn, '.'); - short_name = short_name ? short_name + SKIP_ONE : rc->callee_qn; - const char *call_leaf = cbm_pipeline_call_callee_leaf(call->callee_name); - if (!call_leaf || strcmp(short_name, call_leaf) != 0) { - continue; - } - if (!cbm_pipeline_qn_class_method_tail_eq(rc->caller_qn, call_tail)) { - continue; - } - if (best_tail) { - return NULL; - } - best_tail = rc; - } - return best_tail; + return cbm_pipeline_find_lsp_resolution_tail(arr, call); } /* Resolve an LSP-emitted callee_qn to a graph-buffer node. @@ -237,34 +241,8 @@ static inline const CBMResolvedCall *cbm_pipeline_find_lsp_resolution( * tail. * * Returns the matching node, or NULL if neither lookup hits. */ -static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node(const cbm_gbuf_t *gbuf, - const char *project_name, - const char *callee_qn, - bool allow_tail_match) { - if (!gbuf || !callee_qn) { - return NULL; - } - const cbm_gbuf_node_t *direct = cbm_gbuf_find_by_qn(gbuf, callee_qn); - if (direct) { - return direct; - } - if (project_name && project_name[0]) { - size_t proj_len = strlen(project_name); - if (!(strncmp(callee_qn, project_name, proj_len) == 0 && callee_qn[proj_len] == '.')) { - char buf[CBM_SZ_1K]; - int written = snprintf(buf, sizeof(buf), "%s.%s", project_name, callee_qn); - if (written > 0 && (size_t)written < sizeof(buf)) { - const cbm_gbuf_node_t *prefixed = cbm_gbuf_find_by_qn(gbuf, buf); - if (prefixed) { - return prefixed; - } - } - } - } - if (!allow_tail_match) { - return NULL; - } - +static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_tail_match(const cbm_gbuf_t *gbuf, + const char *callee_qn) { const char *short_name = strrchr(callee_qn, '.'); short_name = short_name ? short_name + SKIP_ONE : callee_qn; const char *callee_tail = cbm_pipeline_qn_class_method_tail(callee_qn); @@ -297,4 +275,34 @@ static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node(const cbm_gbuf return match; } +static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node(const cbm_gbuf_t *gbuf, + const char *project_name, + const char *callee_qn, + bool allow_tail_match) { + if (!gbuf || !callee_qn) { + return NULL; + } + const cbm_gbuf_node_t *direct = cbm_gbuf_find_by_qn(gbuf, callee_qn); + if (direct) { + return direct; + } + if (project_name && project_name[0]) { + size_t proj_len = strlen(project_name); + if (!(strncmp(callee_qn, project_name, proj_len) == 0 && callee_qn[proj_len] == '.')) { + char buf[CBM_SZ_1K]; + int written = snprintf(buf, sizeof(buf), "%s.%s", project_name, callee_qn); + if (written > 0 && (size_t)written < sizeof(buf)) { + const cbm_gbuf_node_t *prefixed = cbm_gbuf_find_by_qn(gbuf, buf); + if (prefixed) { + return prefixed; + } + } + } + } + if (!allow_tail_match) { + return NULL; + } + return cbm_pipeline_lsp_target_tail_match(gbuf, callee_qn); +} + #endif /* CBM_PIPELINE_LSP_RESOLVE_H */ diff --git a/src/pipeline/pass_lsp_cross.h b/src/pipeline/pass_lsp_cross.h index b63a1ce11..18a8f9a57 100644 --- a/src/pipeline/pass_lsp_cross.h +++ b/src/pipeline/pass_lsp_cross.h @@ -116,8 +116,9 @@ typedef struct { * if none was built (or language has no cross-LSP entrypoint). */ static inline CBMTypeRegistry *cbm_pxc_registry_for_lang(const CBMCrossLspRegistries *r, CBMLanguage lang) { - if (!r) + if (!r) { return NULL; + } switch (lang) { case CBM_LANG_GO: return r->go; @@ -148,17 +149,17 @@ const struct CBMCargoManifest *cbm_pxc_get_rust_manifest(void); /* Run the cross-file LSP resolver for non-TS languages. Appends * resolved CALLS into r->resolved_calls (lives in r->arena). Caller - * owns source, module_qn, all_defs, imp_keys, imp_vals. - * NOTE: all_defs is read-only in practice but typed non-const to match + * owns source, module_qn, defs, imp_names, imp_qns. + * NOTE: defs is read-only in practice but typed non-const to match * the existing cbm_run_X_lsp_cross callee signatures. */ void cbm_pxc_run_one(CBMLanguage lang, CBMFileResult *r, const char *source, int source_len, - const char *module_qn, CBMLSPDef *all_defs, int def_count, - const char **imp_keys, const char **imp_vals, int imp_count); + const char *module_qn, CBMLSPDef *defs, int def_count, const char **imp_names, + const char **imp_qns, int imp_count); /* TS / JS / JSX / TSX variant with explicit dialect flags. */ void cbm_pxc_run_one_ts(CBMFileResult *r, const char *source, int source_len, const char *module_qn, - CBMLSPDef *all_defs, int def_count, const char **imp_keys, - const char **imp_vals, int imp_count, bool js_mode, bool jsx_mode, + CBMLSPDef *defs, int def_count, const char **imp_names, + const char **imp_qns, int imp_count, bool js_mode, bool jsx_mode, bool dts_mode); /* Per-file cross-LSP dispatch shared by the parallel resolve worker AND the diff --git a/src/store/store.c b/src/store/store.c index 39a546dd3..54e9a06a5 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -4131,7 +4131,7 @@ int cbm_store_bfs_multi(cbm_store_t *s, const int64_t *seed_ids, int seed_count, int cap = ST_INIT_CAP_16; int n = 0; - cbm_node_hop_t *visited = malloc(cap * sizeof(cbm_node_hop_t)); + cbm_node_hop_t *visited = calloc(cap, sizeof(cbm_node_hop_t)); int scan_rc16; while ((scan_rc16 = sqlite3_step(stmt)) == SQLITE_ROW) { if (n > max_results) { diff --git a/src/ui/httpd.h b/src/ui/httpd.h index dccfbe072..3f83d8baa 100644 --- a/src/ui/httpd.h +++ b/src/ui/httpd.h @@ -37,6 +37,8 @@ #define CBM_HTTP_MAX_BODY (1024 * 1024) /* Default per-connection receive deadline. */ #define CBM_HTTP_RECV_DEADLINE_MS 5000 +#include "../foundation/constants.h" + typedef struct cbm_httpd cbm_httpd_t; /* listener */ typedef struct cbm_http_conn cbm_http_conn_t; /* accepted connection */ @@ -51,14 +53,14 @@ typedef enum { * Selected headers are copied for the routing/security layer ("" when * absent). `body` is heap-allocated and NUL-terminated. */ typedef struct { - char method[16]; - char path[2048]; - char query[2048]; + char method[CBM_SZ_16]; + char path[CBM_SZ_2K]; + char query[CBM_SZ_2K]; unsigned char http_minor; /* 0 for HTTP/1.0, 1 for HTTP/1.1 */ - char origin[256]; - char host[256]; - char content_type[128]; - char accept_language[256]; + char origin[CBM_SZ_256]; + char host[CBM_SZ_256]; + char content_type[CBM_SZ_128]; + char accept_language[CBM_SZ_256]; char *body; size_t body_len; } cbm_http_req_t; diff --git a/src/watcher/watcher.c b/src/watcher/watcher.c index 38c1b0163..e9bef377f 100644 --- a/src/watcher/watcher.c +++ b/src/watcher/watcher.c @@ -125,7 +125,7 @@ struct cbm_watcher { * helpers (for example fsmonitor). Every invocation therefore has both a hard * wall-clock deadline and a finite capture budget. */ #define WATCHER_GIT_DEADLINE_MS 30000 -#define WATCHER_GIT_OUTPUT_MAX (64U * 1024U * 1024U) +#define WATCHER_GIT_OUTPUT_MAX ((size_t)64 * 1024 * 1024) #define WATCHER_GIT_HEAD_MAX 4096U #define WATCHER_GIT_POLL_US 10000U @@ -454,7 +454,7 @@ static watcher_git_status_t git_repo_status(cbm_watcher_t *w, project_state_t *s static watcher_git_status_t git_head(cbm_watcher_t *w, project_state_t *state, char *out, size_t out_size) { - if (!out || out_size < 2) { + if (!out || out_size < CBM_SZ_2) { return WATCHER_GIT_SUPERVISION_FAILED; } out[0] = '\0'; @@ -532,60 +532,38 @@ static uint64_t sig_fold_path_stat(uint64_t h, const char *root_path, const char * untracked directories individually (a nested addition under `?? dir/` * would otherwise be invisible); -z gives unquoted NUL-separated paths that * hash identically across polls and stat cleanly. */ -static watcher_git_status_t git_dirty_signature(cbm_watcher_t *w, project_state_t *state, - uint64_t *signature_out) { - if (!signature_out) { - return WATCHER_GIT_SUPERVISION_FAILED; - } - *signature_out = 0; - const char *status_argv[] = {"git", "--no-optional-locks", "-C", state->root_path, - "status", "--porcelain", "-uall", "-z", - NULL}; - watcher_git_output_t output; - watcher_git_status_t status = - watcher_git_run(w, state, status_argv, WATCHER_GIT_OUTPUT_MAX, &output); - if (status != WATCHER_GIT_OK) { - return status; - } - FILE *fp = cbm_fopen(output.path, "rb"); - if (!fp) { - watcher_git_output_cleanup(&output); - return WATCHER_GIT_SUPERVISION_FAILED; - } - - uint64_t h = SIG_FNV_OFFSET; - bool any = false; +static bool git_parse_porcelain_stream(FILE *fp, const char *root_path, uint64_t *h_out, + bool *any_out) { + uint64_t h = *h_out; + bool any = *any_out; char entry[CBM_SZ_4K]; size_t elen = 0; bool overflow = false; - /* Rename/copy entries are followed by a second NUL token (the origin - * path) that carries no XY prefix — fold it as text, don't stat it. */ bool origin_token = false; - int c; for (;;) { - c = fgetc(fp); + int c = fgetc(fp); if (c != EOF && c != '\0') { - if (elen + 1 < sizeof(entry)) { + if (elen + SKIP_ONE < sizeof(entry)) { entry[elen] = (char)c; } else { - overflow = true; /* keep hashing prefix; skip stat for entry */ + overflow = true; } elen++; continue; } - size_t stored = elen < sizeof(entry) ? elen : sizeof(entry) - 1; + size_t stored = elen < sizeof(entry) ? elen : sizeof(entry) - SKIP_ONE; entry[stored] = '\0'; if (stored > 0) { any = true; h = sig_fold(h, entry, stored); - h = sig_fold(h, "", 1); /* token separator */ + h = sig_fold(h, "", SKIP_ONE); if (origin_token) { origin_token = false; - } else if (!overflow && stored > 3 && entry[2] == ' ') { + } else if (!overflow && stored > CBM_SZ_3 && entry[CBM_SZ_2] == ' ') { if (entry[0] == 'R' || entry[0] == 'C') { origin_token = true; } - h = sig_fold_path_stat(h, state->root_path, entry + 3); + h = sig_fold_path_stat(h, root_path, entry + CBM_SZ_3); } } elen = 0; @@ -594,17 +572,14 @@ static watcher_git_status_t git_dirty_signature(cbm_watcher_t *w, project_state_ break; } } - bool parsed = !ferror(fp) && fclose(fp) == 0; - watcher_git_output_cleanup(&output); - if (!parsed) { - return WATCHER_GIT_SUPERVISION_FAILED; - } + *h_out = h; + *any_out = any; + return !ferror(fp) && fclose(fp) == 0; +} -#if !defined(_WIN32) - /* `submodule foreach` necessarily evaluates its final fixed argument in - * Git's documented POSIX-shell environment. No repository/user text is - * interpolated into that argument; the outer process is still argv-spawned - * and its complete descendant tree remains supervised. */ +#ifndef _WIN32 +static watcher_git_status_t git_dirty_signature_submodules(cbm_watcher_t *w, project_state_t *state, + uint64_t *h_inout, bool *any_inout) { const char *submodule_argv[] = { "git", "--no-optional-locks", @@ -617,15 +592,18 @@ static watcher_git_status_t git_dirty_signature(cbm_watcher_t *w, project_state_ "git status --porcelain -uall 2>/dev/null | sed -e \"s@^\\(..\\) @\\1 $displaypath/@\"", NULL, }; + watcher_git_output_t output; watcher_git_status_t submodule_status = watcher_git_run(w, state, submodule_argv, WATCHER_GIT_OUTPUT_MAX, &output); if (submodule_status == WATCHER_GIT_OK) { - fp = cbm_fopen(output.path, "rb"); + FILE *fp = cbm_fopen(output.path, "rb"); if (!fp) { watcher_git_output_cleanup(&output); return WATCHER_GIT_SUPERVISION_FAILED; } char line[CBM_SZ_4K]; + uint64_t h = *h_inout; + bool any = *any_inout; while (fgets(line, sizeof(line), fp)) { size_t len = strlen(line); while (len > 0 && (line[len - SKIP_ONE] == '\n' || line[len - SKIP_ONE] == '\r')) { @@ -636,22 +614,66 @@ static watcher_git_status_t git_dirty_signature(cbm_watcher_t *w, project_state_ } any = true; h = sig_fold(h, line, len); - h = sig_fold(h, "", 1); - if (len > 3 && line[2] == ' ') { - h = sig_fold_path_stat(h, state->root_path, line + 3); + h = sig_fold(h, "", SKIP_ONE); + if (len > CBM_SZ_3 && line[CBM_SZ_2] == ' ') { + h = sig_fold_path_stat(h, state->root_path, line + CBM_SZ_3); } } - parsed = !ferror(fp) && fclose(fp) == 0; + bool parsed = !ferror(fp) && fclose(fp) == 0; watcher_git_output_cleanup(&output); if (!parsed) { return WATCHER_GIT_SUPERVISION_FAILED; } + *h_inout = h; + *any_inout = any; } else if (submodule_status != WATCHER_GIT_COMMAND_FAILED) { return submodule_status; } + return WATCHER_GIT_OK; +} #endif - *signature_out = any ? (h ? h : 1) : 0; /* reserve 0 for "clean" */ +static watcher_git_status_t git_dirty_signature(cbm_watcher_t *w, project_state_t *state, + uint64_t *signature_out) { + if (!signature_out) { + return WATCHER_GIT_SUPERVISION_FAILED; + } + *signature_out = 0; + const char *status_argv[] = {"git", "--no-optional-locks", "-C", state->root_path, + "status", "--porcelain", "-uall", "-z", + NULL}; + watcher_git_output_t output; + watcher_git_status_t status = + watcher_git_run(w, state, status_argv, WATCHER_GIT_OUTPUT_MAX, &output); + if (status != WATCHER_GIT_OK) { + return status; + } + FILE *fp = cbm_fopen(output.path, "rb"); + if (!fp) { + watcher_git_output_cleanup(&output); + return WATCHER_GIT_SUPERVISION_FAILED; + } + + uint64_t h = SIG_FNV_OFFSET; + bool any = false; + bool parsed = git_parse_porcelain_stream(fp, state->root_path, &h, &any); + watcher_git_output_cleanup(&output); + if (!parsed) { + return WATCHER_GIT_SUPERVISION_FAILED; + } + +#ifndef _WIN32 + watcher_git_status_t sub_st = git_dirty_signature_submodules(w, state, &h, &any); + if (sub_st != WATCHER_GIT_OK) { + return sub_st; + } +#endif + + uint64_t sig = 0; + if (any) { + sig = h ? h : (uint64_t)SKIP_ONE; + } + *signature_out = sig; return WATCHER_GIT_OK; } @@ -678,7 +700,7 @@ static watcher_git_status_t git_file_count(cbm_watcher_t *w, project_state_t *st int count = 0; char buf[CBM_SZ_1K]; size_t n; - while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) { + while ((n = fread(buf, CBM_ALLOC_ONE, sizeof(buf), fp)) > 0) { for (size_t i = 0; i < n; i++) { if (buf[i] == '\0' && count < INT_MAX) { count++; @@ -731,7 +753,7 @@ static void state_free(project_state_t *s) { * a use-after-free against an in-flight poll snapshot. */ static bool defer_state_free(cbm_watcher_t *w, project_state_t *s) { if (w->pending_free_count >= w->pending_free_cap) { - int new_cap = w->pending_free_cap ? w->pending_free_cap * 2 : 8; + int new_cap = w->pending_free_cap ? w->pending_free_cap * CBM_SZ_2 : CBM_SZ_8; project_state_t **tmp = realloc(w->pending_free, (size_t)new_cap * sizeof(project_state_t *)); if (!tmp) { @@ -788,7 +810,7 @@ static long prune_grace_s(void) { if (raw && raw[0]) { errno = 0; char *end = NULL; - long v = strtol(raw, &end, 10); + long v = strtol(raw, &end, CBM_DECIMAL_BASE); if (errno == 0 && end != raw && *end == '\0' && v >= 0) { return v; } @@ -1218,31 +1240,17 @@ static void prune_missing_project(cbm_watcher_t *w, project_state_t *s) { free(root_path); } -static void poll_project(const char *key, void *val, void *ud) { - (void)key; - poll_ctx_t *ctx = ud; - project_state_t *s = val; - if (!s || !atomic_load_explicit(&s->registered, memory_order_acquire)) { - return; - } - - /* Stale-root pruning (#286): classify the root BEFORE the baseline / - * is_git / interval gates so vanished roots are noticed even for - * non-git projects and regardless of adaptive backoff. */ +static bool poll_project_root_check(project_state_t *s, poll_ctx_t *ctx) { int stat_errno = 0; root_status_t rs = root_status(s->root_path, &stat_errno); if (rs == ROOT_UNCERTAIN) { - /* EACCES / EIO / network blip / TCC revocation — the root may still - * exist. Never count toward pruning; restart the streak so only an - * uninterrupted run of genuine ENOENT/ENOTDIR observations can - * delete user data. */ if (s->missing_root_count > 0) { s->missing_root_count = 0; s->first_missing_ms = 0; } cbm_log_warn("watcher.root_stat_error", "project", s->project_name, "path", s->root_path, "errno", itoa_buf(stat_errno)); - return; + return false; } if (rs == ROOT_MISSING) { uint64_t now_ms = cbm_now_ms(); @@ -1256,13 +1264,27 @@ static void poll_project(const char *key, void *val, void *ud) { now_ms - s->first_missing_ms >= (uint64_t)prune_grace_s() * CBM_MSEC_PER_SEC) { prune_missing_project(ctx->w, s); } - return; + return false; } if (s->missing_root_count > 0) { cbm_log_info("watcher.root_restored", "project", s->project_name, "path", s->root_path); s->missing_root_count = 0; s->first_missing_ms = 0; } + return true; +} + +static void poll_project(const char *key, void *val, void *ud) { + (void)key; + poll_ctx_t *ctx = ud; + project_state_t *s = val; + if (!s || !atomic_load_explicit(&s->registered, memory_order_acquire)) { + return; + } + + if (!poll_project_root_check(s, ctx)) { + return; + } /* Initialize baseline on first poll */ if (!s->baseline_done) { @@ -1358,12 +1380,12 @@ int cbm_watcher_poll_once(cbm_watcher_t *w) { } w->pending_free_count = 0; - int n = cbm_ht_count(w->projects); + int n = (int)cbm_ht_count(w->projects); if (n == 0) { cbm_mutex_unlock(&w->projects_lock); return 0; } - project_state_t **snap = malloc(n * sizeof(project_state_t *)); + project_state_t **snap = malloc((size_t)n * sizeof(project_state_t *)); if (!snap) { cbm_mutex_unlock(&w->projects_lock); return 0; @@ -1397,7 +1419,7 @@ static void cancel_active_git_entry(const char *key, void *value, void *user_dat void cbm_watcher_stop(cbm_watcher_t *w) { if (w) { - atomic_store_explicit(&w->stopped, 1, memory_order_release); + atomic_store_explicit(&w->stopped, true, memory_order_release); cbm_mutex_lock(&w->projects_lock); cbm_ht_foreach(w->projects, cancel_active_git_entry, NULL); for (int i = 0; i < w->pending_free_count; i++) {