#include "amduatd_space_doctor.h" #include "amduat/asl/record.h" #include "amduat/enc/asl_log.h" #include "amduat/enc/asl1_core_codec.h" #include #include #include typedef struct { char *data; size_t len; size_t cap; } amduatd_doctor_strbuf_t; static void amduatd_doctor_strbuf_free(amduatd_doctor_strbuf_t *b) { if (b == NULL) { return; } free(b->data); b->data = NULL; b->len = 0u; b->cap = 0u; } static bool amduatd_doctor_strbuf_reserve(amduatd_doctor_strbuf_t *b, size_t extra) { size_t need; size_t next_cap; char *next; if (b == NULL) { return false; } if (extra == 0u) { return true; } if (b->len > SIZE_MAX - extra) { return false; } need = b->len + extra; if (need <= b->cap) { return true; } next_cap = b->cap != 0u ? b->cap : 256u; while (next_cap < need) { if (next_cap > (SIZE_MAX / 2u)) { next_cap = need; break; } next_cap *= 2u; } next = (char *)realloc(b->data, next_cap); if (next == NULL) { return false; } b->data = next; b->cap = next_cap; return true; } static bool amduatd_doctor_strbuf_append(amduatd_doctor_strbuf_t *b, const char *data, size_t len) { if (b == NULL) { return false; } if (data == NULL) { len = 0u; } if (!amduatd_doctor_strbuf_reserve(b, len + 1u)) { return false; } if (len != 0u) { memcpy(b->data + b->len, data, len); } b->len += len; b->data[b->len] = '\0'; return true; } static bool amduatd_doctor_strbuf_append_cstr(amduatd_doctor_strbuf_t *b, const char *s) { return amduatd_doctor_strbuf_append(b, s != NULL ? s : "", s != NULL ? strlen(s) : 0u); } static bool amduatd_doctor_strbuf_append_char(amduatd_doctor_strbuf_t *b, char c) { return amduatd_doctor_strbuf_append(b, &c, 1u); } static char *amduatd_doctor_strdup(const char *s) { size_t len; char *copy; if (s == NULL) { return NULL; } len = strlen(s); copy = (char *)malloc(len + 1u); if (copy == NULL) { return NULL; } memcpy(copy, s, len); copy[len] = '\0'; return copy; } static const char *amduatd_space_doctor_status_name( amduatd_space_doctor_status_t status) { switch (status) { case AMDUATD_DOCTOR_OK: return "ok"; case AMDUATD_DOCTOR_WARN: return "warn"; case AMDUATD_DOCTOR_FAIL: return "fail"; case AMDUATD_DOCTOR_SKIPPED: return "skipped"; default: return "unknown"; } } static void amduatd_space_doctor_report_init( amduatd_space_doctor_report_t *report) { if (report == NULL) { return; } memset(report, 0, sizeof(*report)); report->backend = AMDUATD_STORE_BACKEND_FS; } static void amduatd_space_doctor_report_clear( amduatd_space_doctor_report_t *report) { if (report == NULL) { return; } for (size_t i = 0u; i < report->checks_len; ++i) { free(report->checks[i].name); free(report->checks[i].detail); } free(report->checks); amduatd_space_doctor_report_init(report); } void amduatd_space_doctor_report_free(amduatd_space_doctor_report_t *report) { amduatd_space_doctor_report_clear(report); } static bool amduatd_space_doctor_report_add( amduatd_space_doctor_report_t *report, const char *name, amduatd_space_doctor_status_t status, const char *detail) { size_t next_len; amduatd_space_doctor_check_t *next; amduatd_space_doctor_check_t *entry; if (report == NULL || name == NULL) { return false; } if (report->checks_len > SIZE_MAX - 1u) { return false; } next_len = report->checks_len + 1u; next = (amduatd_space_doctor_check_t *)realloc( report->checks, next_len * sizeof(*next)); if (next == NULL) { return false; } report->checks = next; entry = &report->checks[report->checks_len]; memset(entry, 0, sizeof(*entry)); entry->name = amduatd_doctor_strdup(name); entry->detail = amduatd_doctor_strdup(detail != NULL ? detail : ""); if (entry->name == NULL || entry->detail == NULL) { free(entry->name); free(entry->detail); return false; } entry->status = status; report->checks_len = next_len; switch (status) { case AMDUATD_DOCTOR_OK: report->ok_count++; break; case AMDUATD_DOCTOR_WARN: report->warn_count++; break; case AMDUATD_DOCTOR_FAIL: report->fail_count++; break; case AMDUATD_DOCTOR_SKIPPED: report->skipped_count++; break; default: break; } return true; } static bool amduatd_space_doctor_build_collection_head_name( const char *name, char **out_name) { size_t name_len; size_t total_len; char *buffer; size_t offset = 0u; if (name == NULL || out_name == NULL) { return false; } if (!amduat_asl_pointer_name_is_valid(name)) { return false; } name_len = strlen(name); total_len = 11u + name_len + 5u + 1u; buffer = (char *)malloc(total_len); if (buffer == NULL) { return false; } memcpy(buffer + offset, "collection/", 11u); offset += 11u; memcpy(buffer + offset, name, name_len); offset += name_len; memcpy(buffer + offset, "/head", 5u); offset += 5u; buffer[offset] = '\0'; *out_name = buffer; return true; } static bool amduatd_space_doctor_build_collection_log_head_name( const char *name, char **out_name) { size_t name_len; size_t log_name_len; size_t total_len; char *buffer; size_t offset = 0u; if (name == NULL || out_name == NULL) { return false; } if (!amduat_asl_pointer_name_is_valid(name)) { return false; } name_len = strlen(name); log_name_len = 11u + name_len + 4u; total_len = 4u + log_name_len + 5u + 1u; buffer = (char *)malloc(total_len); if (buffer == NULL) { return false; } memcpy(buffer + offset, "log/", 4u); offset += 4u; memcpy(buffer + offset, "collection/", 11u); offset += 11u; memcpy(buffer + offset, name, name_len); offset += name_len; memcpy(buffer + offset, "/log", 4u); offset += 4u; memcpy(buffer + offset, "/head", 5u); offset += 5u; buffer[offset] = '\0'; *out_name = buffer; return true; } enum { AMDUATD_EDGE_INDEX_MAGIC_LEN = 8, AMDUATD_EDGE_INDEX_VERSION = 1 }; static const uint8_t k_amduatd_edge_index_magic[AMDUATD_EDGE_INDEX_MAGIC_LEN] = { 'A', 'S', 'L', 'E', 'I', 'X', '1', '\0' }; typedef struct { bool has_graph_ref; amduat_reference_t graph_ref; } amduatd_edge_index_state_view_t; static void amduatd_edge_index_state_view_free( amduatd_edge_index_state_view_t *state) { if (state == NULL) { return; } if (state->has_graph_ref) { amduat_reference_free(&state->graph_ref); } memset(state, 0, sizeof(*state)); } static bool amduatd_doctor_read_u32_le(const uint8_t *data, size_t len, size_t *offset, uint32_t *out) { if (len - *offset < 4u) { return false; } *out = (uint32_t)data[*offset] | ((uint32_t)data[*offset + 1u] << 8) | ((uint32_t)data[*offset + 2u] << 16) | ((uint32_t)data[*offset + 3u] << 24); *offset += 4u; return true; } static bool amduatd_doctor_read_u64_le(const uint8_t *data, size_t len, size_t *offset, uint64_t *out) { if (len - *offset < 8u) { return false; } *out = (uint64_t)data[*offset] | ((uint64_t)data[*offset + 1u] << 8) | ((uint64_t)data[*offset + 2u] << 16) | ((uint64_t)data[*offset + 3u] << 24) | ((uint64_t)data[*offset + 4u] << 32) | ((uint64_t)data[*offset + 5u] << 40) | ((uint64_t)data[*offset + 6u] << 48) | ((uint64_t)data[*offset + 7u] << 56); *offset += 8u; return true; } static bool amduatd_edge_index_state_view_decode( amduat_octets_t payload, amduatd_edge_index_state_view_t *out_state) { size_t offset = 0u; uint32_t version = 0u; uint32_t ref_len = 0u; amduat_octets_t ref_bytes; uint64_t ignored_offset = 0u; if (out_state == NULL) { return false; } memset(out_state, 0, sizeof(*out_state)); if (payload.data == NULL || payload.len < AMDUATD_EDGE_INDEX_MAGIC_LEN + 4u + 8u + 4u) { return false; } if (memcmp(payload.data, k_amduatd_edge_index_magic, AMDUATD_EDGE_INDEX_MAGIC_LEN) != 0) { return false; } offset += AMDUATD_EDGE_INDEX_MAGIC_LEN; if (!amduatd_doctor_read_u32_le(payload.data, payload.len, &offset, &version) || version != AMDUATD_EDGE_INDEX_VERSION) { return false; } if (!amduatd_doctor_read_u64_le(payload.data, payload.len, &offset, &ignored_offset) || !amduatd_doctor_read_u32_le(payload.data, payload.len, &offset, &ref_len)) { return false; } (void)ignored_offset; if (payload.len - offset < ref_len) { return false; } if (ref_len != 0u) { ref_bytes = amduat_octets(payload.data + offset, ref_len); if (!amduat_enc_asl1_core_decode_reference_v1(ref_bytes, &out_state->graph_ref)) { amduatd_edge_index_state_view_free(out_state); return false; } out_state->has_graph_ref = true; offset += ref_len; } return offset == payload.len; } static amduatd_store_backend_t amduatd_space_doctor_backend_from_store( const amduat_asl_store_t *store) { if (store == NULL) { return AMDUATD_STORE_BACKEND_FS; } if (store->ops.log_scan != NULL && store->ops.current_state != NULL) { return AMDUATD_STORE_BACKEND_INDEX; } return AMDUATD_STORE_BACKEND_FS; } typedef struct { const char *label; char *pointer_name; bool pointer_exists; amduat_reference_t pointer_ref; } amduatd_doctor_pointer_t; static void amduatd_space_doctor_pointer_free(amduatd_doctor_pointer_t *ptr) { if (ptr == NULL) { return; } free(ptr->pointer_name); ptr->pointer_name = NULL; ptr->pointer_exists = false; amduat_reference_free(&ptr->pointer_ref); } bool amduatd_space_doctor_run(amduat_asl_store_t *store, amduat_asl_pointer_store_t *pointer_store, const amduatd_space_t *effective_space, const amduatd_cfg_t *cfg, const amduatd_fed_cfg_t *fed_cfg, amduatd_space_doctor_report_t *out_report) { amduatd_doctor_pointer_t pointers[3]; amduat_octets_t edges_collection = amduat_octets(NULL, 0u); amduat_octets_t edges_index_head = amduat_octets(NULL, 0u); char *collection_head = NULL; char *collection_log_head = NULL; bool names_ok = true; bool ok = false; size_t i; if (out_report == NULL) { return false; } amduatd_space_doctor_report_init(out_report); if (store == NULL || pointer_store == NULL || cfg == NULL) { return false; } (void)cfg; out_report->backend = amduatd_space_doctor_backend_from_store(store); if (effective_space != NULL && effective_space->enabled && effective_space->space_id.data != NULL) { out_report->scoped = true; snprintf(out_report->space_id, sizeof(out_report->space_id), "%s", (const char *)effective_space->space_id.data); } memset(pointers, 0, sizeof(pointers)); pointers[0].label = "edges_index_head"; pointers[1].label = "edges_collection_snapshot_head"; pointers[2].label = "edges_collection_log_head"; for (i = 0u; i < 3u; ++i) { memset(&pointers[i].pointer_ref, 0, sizeof(pointers[i].pointer_ref)); } if (!amduatd_space_edges_collection_name(effective_space, &edges_collection) || !amduatd_space_edges_index_head_name(effective_space, &edges_index_head)) { (void)amduatd_space_doctor_report_add(out_report, "pointer_name_validation", AMDUATD_DOCTOR_FAIL, "failed to scope pointer names"); names_ok = false; goto doctor_after_names; } if (!amduatd_space_doctor_build_collection_head_name( (const char *)edges_collection.data, &collection_head) || !amduatd_space_doctor_build_collection_log_head_name( (const char *)edges_collection.data, &collection_log_head)) { (void)amduatd_space_doctor_report_add(out_report, "pointer_name_validation", AMDUATD_DOCTOR_FAIL, "failed to build collection heads"); names_ok = false; goto doctor_after_names; } if (!amduat_asl_pointer_name_is_valid( (const char *)edges_collection.data) || !amduat_asl_pointer_name_is_valid( (const char *)edges_index_head.data) || !amduat_asl_pointer_name_is_valid(collection_head) || !amduat_asl_pointer_name_is_valid(collection_log_head)) { (void)amduatd_space_doctor_report_add(out_report, "pointer_name_validation", AMDUATD_DOCTOR_FAIL, "invalid pointer name encoding"); names_ok = false; goto doctor_after_names; } (void)amduatd_space_doctor_report_add(out_report, "pointer_name_validation", AMDUATD_DOCTOR_OK, "scoped pointer names valid"); doctor_after_names: if (!names_ok) { (void)amduatd_space_doctor_report_add(out_report, "edges_index_head", AMDUATD_DOCTOR_SKIPPED, "invalid pointer names"); (void)amduatd_space_doctor_report_add(out_report, "edges_collection_snapshot_head", AMDUATD_DOCTOR_SKIPPED, "invalid pointer names"); (void)amduatd_space_doctor_report_add(out_report, "edges_collection_log_head", AMDUATD_DOCTOR_SKIPPED, "invalid pointer names"); (void)amduatd_space_doctor_report_add(out_report, "cas_edges_index_head", AMDUATD_DOCTOR_SKIPPED, "invalid pointer names"); (void)amduatd_space_doctor_report_add(out_report, "cas_edges_collection_snapshot_head", AMDUATD_DOCTOR_SKIPPED, "invalid pointer names"); (void)amduatd_space_doctor_report_add(out_report, "cas_edges_collection_log_head", AMDUATD_DOCTOR_SKIPPED, "invalid pointer names"); (void)amduatd_space_doctor_report_add(out_report, "edge_index_state_parse", AMDUATD_DOCTOR_SKIPPED, "invalid pointer names"); (void)amduatd_space_doctor_report_add(out_report, "edge_index_graph_ref", AMDUATD_DOCTOR_SKIPPED, "invalid pointer names"); goto doctor_index_checks; } pointers[0].pointer_name = amduatd_doctor_strdup( (const char *)edges_index_head.data); pointers[1].pointer_name = amduatd_doctor_strdup(collection_head); pointers[2].pointer_name = amduatd_doctor_strdup(collection_log_head); if (pointers[0].pointer_name == NULL || pointers[1].pointer_name == NULL || pointers[2].pointer_name == NULL) { (void)amduatd_space_doctor_report_add(out_report, "pointer_store", AMDUATD_DOCTOR_FAIL, "oom"); goto doctor_cleanup; } for (i = 0u; i < 3u; ++i) { bool exists = false; amduat_asl_pointer_error_t perr = amduat_asl_pointer_get(pointer_store, pointers[i].pointer_name, &exists, &pointers[i].pointer_ref); if (perr != AMDUAT_ASL_POINTER_OK) { char detail[256]; snprintf(detail, sizeof(detail), "pointer error: %s", pointers[i].pointer_name); (void)amduatd_space_doctor_report_add(out_report, pointers[i].label, AMDUATD_DOCTOR_FAIL, detail); continue; } pointers[i].pointer_exists = exists; if (exists) { char detail[256]; snprintf(detail, sizeof(detail), "present: %s", pointers[i].pointer_name); (void)amduatd_space_doctor_report_add(out_report, pointers[i].label, AMDUATD_DOCTOR_OK, detail); } else { char detail[256]; snprintf(detail, sizeof(detail), "missing: %s", pointers[i].pointer_name); (void)amduatd_space_doctor_report_add(out_report, pointers[i].label, AMDUATD_DOCTOR_WARN, detail); } } for (i = 0u; i < 3u; ++i) { const char *check_name = NULL; switch (i) { case 0u: check_name = "cas_edges_index_head"; break; case 1u: check_name = "cas_edges_collection_snapshot_head"; break; case 2u: check_name = "cas_edges_collection_log_head"; break; default: check_name = "cas_pointer_ref"; break; } if (!pointers[i].pointer_exists) { (void)amduatd_space_doctor_report_add(out_report, check_name, AMDUATD_DOCTOR_SKIPPED, "pointer missing"); continue; } { amduat_artifact_t artifact; memset(&artifact, 0, sizeof(artifact)); if (amduat_asl_store_get(store, pointers[i].pointer_ref, &artifact) != AMDUAT_ASL_STORE_OK) { (void)amduatd_space_doctor_report_add(out_report, check_name, AMDUATD_DOCTOR_FAIL, "store get failed"); } else { (void)amduatd_space_doctor_report_add(out_report, check_name, AMDUATD_DOCTOR_OK, "ref readable"); } amduat_artifact_free(&artifact); } } if (!pointers[0].pointer_exists) { (void)amduatd_space_doctor_report_add(out_report, "edge_index_state_parse", AMDUATD_DOCTOR_SKIPPED, "pointer missing"); (void)amduatd_space_doctor_report_add(out_report, "edge_index_graph_ref", AMDUATD_DOCTOR_SKIPPED, "pointer missing"); } else { amduat_asl_record_t record; amduatd_edge_index_state_view_t state; const char *schema = "tgk/edge_index_state"; bool parsed = false; memset(&record, 0, sizeof(record)); memset(&state, 0, sizeof(state)); if (amduat_asl_record_store_get(store, pointers[0].pointer_ref, &record) != AMDUAT_ASL_STORE_OK) { (void)amduatd_space_doctor_report_add(out_report, "edge_index_state_parse", AMDUATD_DOCTOR_FAIL, "record load failed"); } else if (record.schema.len != strlen(schema) || memcmp(record.schema.data, schema, record.schema.len) != 0) { (void)amduatd_space_doctor_report_add(out_report, "edge_index_state_parse", AMDUATD_DOCTOR_FAIL, "unexpected schema"); } else if (!amduatd_edge_index_state_view_decode(record.payload, &state)) { (void)amduatd_space_doctor_report_add(out_report, "edge_index_state_parse", AMDUATD_DOCTOR_FAIL, "payload decode failed"); } else { (void)amduatd_space_doctor_report_add(out_report, "edge_index_state_parse", AMDUATD_DOCTOR_OK, "edge index state decoded"); parsed = true; } if (parsed && state.has_graph_ref) { amduat_artifact_t graph_artifact; memset(&graph_artifact, 0, sizeof(graph_artifact)); if (amduat_asl_store_get(store, state.graph_ref, &graph_artifact) != AMDUAT_ASL_STORE_OK) { (void)amduatd_space_doctor_report_add(out_report, "edge_index_graph_ref", AMDUATD_DOCTOR_FAIL, "graph ref missing"); } else { (void)amduatd_space_doctor_report_add(out_report, "edge_index_graph_ref", AMDUATD_DOCTOR_OK, "graph ref readable"); } amduat_artifact_free(&graph_artifact); } else if (parsed) { (void)amduatd_space_doctor_report_add(out_report, "edge_index_graph_ref", AMDUATD_DOCTOR_SKIPPED, "no graph ref"); } else { (void)amduatd_space_doctor_report_add(out_report, "edge_index_graph_ref", AMDUATD_DOCTOR_SKIPPED, "edge index state unreadable"); } amduatd_edge_index_state_view_free(&state); amduat_asl_record_free(&record); } doctor_index_checks: if (out_report->backend == AMDUATD_STORE_BACKEND_INDEX) { amduat_asl_index_state_t state; amduat_asl_log_record_t *records = NULL; size_t record_count = 0u; if (!amduat_asl_index_current_state(store, &state)) { (void)amduatd_space_doctor_report_add(out_report, "index_current_state", AMDUATD_DOCTOR_FAIL, "current_state failed"); } else { (void)amduatd_space_doctor_report_add(out_report, "index_current_state", AMDUATD_DOCTOR_OK, "current_state ok"); } { amduat_asl_store_error_t scan_err = amduat_asl_log_scan(store, &records, &record_count); if (scan_err == AMDUAT_ASL_STORE_OK) { (void)amduatd_space_doctor_report_add(out_report, "index_log_scan", AMDUATD_DOCTOR_OK, "log_scan ok"); } else { (void)amduatd_space_doctor_report_add(out_report, "index_log_scan", AMDUATD_DOCTOR_FAIL, "log_scan failed"); } if (records != NULL) { amduat_enc_asl_log_free(records, record_count); } } } else { (void)amduatd_space_doctor_report_add(out_report, "index_current_state", AMDUATD_DOCTOR_SKIPPED, "requires index backend"); (void)amduatd_space_doctor_report_add(out_report, "index_log_scan", AMDUATD_DOCTOR_SKIPPED, "requires index backend"); } { char detail[256]; bool fed_enabled = fed_cfg != NULL && fed_cfg->enabled; bool require_index_backend = fed_enabled; snprintf(detail, sizeof(detail), "enabled=%s require_index_backend=%s", fed_enabled ? "true" : "false", require_index_backend ? "true" : "false"); (void)amduatd_space_doctor_report_add(out_report, "federation", AMDUATD_DOCTOR_OK, detail); } ok = true; doctor_cleanup: for (i = 0u; i < 3u; ++i) { amduatd_space_doctor_pointer_free(&pointers[i]); } free((void *)edges_collection.data); free((void *)edges_index_head.data); free(collection_head); free(collection_log_head); if (!ok) { amduatd_space_doctor_report_clear(out_report); } return ok; } bool amduatd_space_doctor_report_json( const amduatd_space_doctor_report_t *report, char **out_json) { amduatd_doctor_strbuf_t b; char header[256]; if (out_json != NULL) { *out_json = NULL; } if (report == NULL || out_json == NULL) { return false; } memset(&b, 0, sizeof(b)); if (!amduatd_doctor_strbuf_append_cstr(&b, "{")) { amduatd_doctor_strbuf_free(&b); return false; } if (!amduatd_doctor_strbuf_append_cstr(&b, "\"effective_space\":{")) { amduatd_doctor_strbuf_free(&b); return false; } if (report->scoped) { if (snprintf(header, sizeof(header), "\"mode\":\"scoped\",\"space_id\":\"%s\"", report->space_id) <= 0 || !amduatd_doctor_strbuf_append_cstr(&b, header)) { amduatd_doctor_strbuf_free(&b); return false; } } else { if (!amduatd_doctor_strbuf_append_cstr(&b, "\"mode\":\"unscoped\"")) { amduatd_doctor_strbuf_free(&b); return false; } } if (!amduatd_doctor_strbuf_append_cstr(&b, "},")) { amduatd_doctor_strbuf_free(&b); return false; } if (snprintf(header, sizeof(header), "\"store_backend\":\"%s\",", amduatd_store_backend_name(report->backend)) <= 0 || !amduatd_doctor_strbuf_append_cstr(&b, header)) { amduatd_doctor_strbuf_free(&b); return false; } if (!amduatd_doctor_strbuf_append_cstr(&b, "\"checks\":[")) { amduatd_doctor_strbuf_free(&b); return false; } for (size_t i = 0u; i < report->checks_len; ++i) { const amduatd_space_doctor_check_t *check = &report->checks[i]; if (i != 0u) { (void)amduatd_doctor_strbuf_append_char(&b, ','); } if (!amduatd_doctor_strbuf_append_cstr(&b, "{\"name\":\"") || !amduatd_doctor_strbuf_append_cstr(&b, check->name) || !amduatd_doctor_strbuf_append_cstr(&b, "\",\"status\":\"") || !amduatd_doctor_strbuf_append_cstr( &b, amduatd_space_doctor_status_name(check->status)) || !amduatd_doctor_strbuf_append_cstr(&b, "\",\"detail\":\"") || !amduatd_doctor_strbuf_append_cstr(&b, check->detail) || !amduatd_doctor_strbuf_append_cstr(&b, "\"}")) { amduatd_doctor_strbuf_free(&b); return false; } } if (!amduatd_doctor_strbuf_append_cstr(&b, "],")) { amduatd_doctor_strbuf_free(&b); return false; } if (snprintf(header, sizeof(header), "\"summary\":{" "\"ok_count\":%llu," "\"warn_count\":%llu," "\"fail_count\":%llu," "\"skipped_count\":%llu" "}}\n", (unsigned long long)report->ok_count, (unsigned long long)report->warn_count, (unsigned long long)report->fail_count, (unsigned long long)report->skipped_count) <= 0 || !amduatd_doctor_strbuf_append_cstr(&b, header)) { amduatd_doctor_strbuf_free(&b); return false; } *out_json = b.data; return true; }