From 67c837be3c0e5db4f1002071e793aac5eab2c164 Mon Sep 17 00:00:00 2001 From: Carl Niklas Rydberg Date: Sat, 24 Jan 2026 17:43:51 +0100 Subject: [PATCH] Add space sync status endpoint with cursor peer discovery --- CMakeLists.txt | 46 +- README.md | 29 ++ registry/amduatd-api-contract.v1.json | 3 + src/amduatd.c | 404 ++++++++++++++++- src/amduatd_space_roots.c | 603 +++++++++++++++++++++++++ src/amduatd_space_roots.h | 44 ++ tests/test_amduatd_space_roots.c | 325 +++++++++++++ tests/test_amduatd_space_sync_status.c | 228 ++++++++++ 8 files changed, 1679 insertions(+), 3 deletions(-) create mode 100644 src/amduatd_space_roots.c create mode 100644 src/amduatd_space_roots.h create mode 100644 tests/test_amduatd_space_roots.c create mode 100644 tests/test_amduatd_space_sync_status.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 2a1b933..b8254e5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,7 +30,7 @@ set(amduatd_sources src/amduatd.c src/amduatd_http.c src/amduatd_caps.c src/amduatd_fed.c src/amduatd_fed_cursor.c src/amduatd_fed_pull_plan.c src/amduatd_fed_push_plan.c src/amduatd_fed_pull_apply.c src/amduatd_fed_push_apply.c - src/amduatd_space_doctor.c) + src/amduatd_space_doctor.c src/amduatd_space_roots.c) if(AMDUATD_ENABLE_UI) list(APPEND amduatd_sources src/amduatd_ui.c) endif() @@ -286,3 +286,47 @@ target_link_libraries(amduatd_test_space_doctor ) add_test(NAME amduatd_space_doctor COMMAND amduatd_test_space_doctor) + +add_executable(amduatd_test_space_roots + tests/test_amduatd_space_roots.c + src/amduatd_space_roots.c + src/amduatd_space.c + src/amduatd_fed_cursor.c + src/amduatd_store.c +) + +target_include_directories(amduatd_test_space_roots + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/include +) + +target_link_libraries(amduatd_test_space_roots + PRIVATE amduat_asl_store_fs amduat_asl_pointer_fs amduat_asl_record + amduat_asl_store_index_fs amduat_asl amduat_enc amduat_util + amduat_hash_asl1 +) + +add_test(NAME amduatd_space_roots COMMAND amduatd_test_space_roots) + +add_executable(amduatd_test_space_sync_status + tests/test_amduatd_space_sync_status.c + src/amduatd_space_roots.c + src/amduatd_space.c + src/amduatd_fed_cursor.c + src/amduatd_store.c +) + +target_include_directories(amduatd_test_space_sync_status + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor/amduat/include +) + +target_link_libraries(amduatd_test_space_sync_status + PRIVATE amduat_asl_store_fs amduat_asl_pointer_fs amduat_asl_record + amduat_asl_store_index_fs amduat_asl amduat_enc amduat_util + amduat_hash_asl1 +) + +add_test(NAME amduatd_space_sync_status COMMAND amduatd_test_space_sync_status) diff --git a/README.md b/README.md index c217c16..c96670d 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,35 @@ Run the daemon with derivation indexing enabled: ./build/amduatd --root .amduat-asl --sock amduatd.sock --enable-derivation-index ``` +## Space roots (GC) + +`/v1/space/roots` enumerates the pointer heads that must be treated as GC roots +for the effective space, including federation cursor heads. + +GC root sets MUST include federation cursors to avoid trimming artifacts still +reachable via replication state. Use the roots listing to build your root set +before running GC tooling. + +Example: + +```sh +curl --unix-socket amduatd.sock \ + 'http://localhost/v1/space/roots' \ + -H 'X-Amduat-Space: demo' +``` + +## Space sync status + +`/v1/space/sync/status` is a read-only summary of federation readiness and +per-peer cursor positions for the effective space. Peers are discovered from +cursor head pointers (pull and push) and returned in deterministic order. + +```sh +curl --unix-socket amduatd.sock \ + 'http://localhost/v1/space/sync/status' \ + -H 'X-Amduat-Space: demo' +``` + To fail `/v1/pel/run` if the derivation index write fails: ```sh diff --git a/registry/amduatd-api-contract.v1.json b/registry/amduatd-api-contract.v1.json index 00a16bf..1eccb38 100644 --- a/registry/amduatd-api-contract.v1.json +++ b/registry/amduatd-api-contract.v1.json @@ -6,6 +6,9 @@ {"method": "GET", "path": "/v1/meta"}, {"method": "HEAD", "path": "/v1/meta"}, {"method": "GET", "path": "/v1/contract"}, + {"method": "GET", "path": "/v1/space/doctor"}, + {"method": "GET", "path": "/v1/space/roots"}, + {"method": "GET", "path": "/v1/space/sync/status"}, {"method": "POST", "path": "/v1/capabilities"}, {"method": "GET", "path": "/v1/cap/resolve"}, {"method": "GET", "path": "/v1/fed/records"}, diff --git a/src/amduatd.c b/src/amduatd.c index 11f6ed9..15e9874 100644 --- a/src/amduatd.c +++ b/src/amduatd.c @@ -49,6 +49,7 @@ #include "amduatd_store.h" #include "amduatd_derivation_index.h" #include "amduatd_space_doctor.h" +#include "amduatd_space_roots.h" #include #include @@ -122,6 +123,8 @@ static const char k_amduatd_contract_v1_json[] = "{\"method\":\"HEAD\",\"path\":\"/v1/meta\"}," "{\"method\":\"GET\",\"path\":\"/v1/contract\"}," "{\"method\":\"GET\",\"path\":\"/v1/space/doctor\"}," + "{\"method\":\"GET\",\"path\":\"/v1/space/roots\"}," + "{\"method\":\"GET\",\"path\":\"/v1/space/sync/status\"}," "{\"method\":\"POST\",\"path\":\"/v1/capabilities\"}," "{\"method\":\"GET\",\"path\":\"/v1/cap/resolve\"}," "{\"method\":\"GET\",\"path\":\"/v1/fed/records\"}," @@ -865,6 +868,380 @@ static bool amduatd_handle_get_space_doctor(int fd, return ok; } +static bool amduatd_handle_get_space_roots(int fd, + const amduatd_http_req_t *req, + const amduatd_cfg_t *dcfg, + const amduatd_caps_t *caps, + const char *root_path) { + amduat_asl_pointer_store_t pointer_store; + amduatd_space_roots_list_t roots; + amduatd_strbuf_t b; + bool ok = false; + + memset(&roots, 0, sizeof(roots)); + memset(&b, 0, sizeof(b)); + + if (req == NULL || dcfg == NULL || root_path == NULL) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "internal error"); + } + + if (caps != NULL && caps->enabled && req->x_capability[0] != '\0') { + const char *reason = NULL; + if (!amduatd_caps_check_space(caps, dcfg, req, &reason)) { + if (reason != NULL && strcmp(reason, "wrong-space") == 0) { + return amduatd_send_json_error(fd, 403, "Forbidden", + "space not permitted by capability"); + } + return amduatd_send_json_error(fd, 403, "Forbidden", + "invalid capability"); + } + } + + if (!amduat_asl_pointer_store_init(&pointer_store, root_path)) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "pointer store error"); + } + if (!amduatd_space_roots_list(root_path, + &pointer_store, + req->effective_space, + &roots)) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "root enumeration failed"); + } + + if (!amduatd_strbuf_append_cstr(&b, "{\"effective_space\":{")) { + goto roots_cleanup; + } + if (req->effective_space != NULL && req->effective_space->enabled && + req->effective_space->space_id.data != NULL) { + const char *space_id = (const char *)req->effective_space->space_id.data; + if (!amduatd_strbuf_append_cstr(&b, "\"mode\":\"scoped\",") || + !amduatd_strbuf_append_cstr(&b, "\"space_id\":\"") || + !amduatd_strbuf_append_cstr(&b, space_id) || + !amduatd_strbuf_append_cstr(&b, "\"")) { + goto roots_cleanup; + } + } else { + if (!amduatd_strbuf_append_cstr(&b, "\"mode\":\"unscoped\",") || + !amduatd_strbuf_append_cstr(&b, "\"space_id\":null")) { + goto roots_cleanup; + } + } + if (!amduatd_strbuf_append_cstr(&b, "},\"roots\":[")) { + goto roots_cleanup; + } + + for (size_t i = 0u; i < roots.len; ++i) { + const char *pointer_name = roots.names[i]; + bool exists = false; + amduat_reference_t ref; + amduat_asl_pointer_error_t perr; + char *ref_hex = NULL; + + memset(&ref, 0, sizeof(ref)); + if (i != 0u) { + if (!amduatd_strbuf_append_char(&b, ',')) { + goto roots_cleanup; + } + } + + perr = amduat_asl_pointer_get(&pointer_store, + pointer_name, + &exists, + &ref); + if (perr != AMDUAT_ASL_POINTER_OK) { + amduat_reference_free(&ref); + goto roots_cleanup; + } + if (exists) { + if (!amduat_asl_ref_encode_hex(ref, &ref_hex)) { + amduat_reference_free(&ref); + goto roots_cleanup; + } + } + + if (!amduatd_strbuf_append_cstr(&b, "{\"pointer_name\":\"") || + !amduatd_strbuf_append_cstr(&b, pointer_name) || + !amduatd_strbuf_append_cstr(&b, "\",\"head_ref_present\":") || + !amduatd_strbuf_append_cstr(&b, exists ? "true" : "false")) { + free(ref_hex); + amduat_reference_free(&ref); + goto roots_cleanup; + } + if (exists) { + if (!amduatd_strbuf_append_cstr(&b, ",\"head_ref\":\"") || + !amduatd_strbuf_append_cstr(&b, ref_hex) || + !amduatd_strbuf_append_cstr(&b, "\"")) { + free(ref_hex); + amduat_reference_free(&ref); + goto roots_cleanup; + } + } + if (!amduatd_strbuf_append_cstr(&b, "}")) { + free(ref_hex); + amduat_reference_free(&ref); + goto roots_cleanup; + } + + free(ref_hex); + amduat_reference_free(&ref); + } + + if (!amduatd_strbuf_append_cstr(&b, "]}\n")) { + goto roots_cleanup; + } + + ok = amduatd_http_send_json(fd, 200, "OK", b.data, false); + +roots_cleanup: + if (!ok) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", "error"); + } + amduatd_space_roots_list_free(&roots); + amduatd_strbuf_free(&b); + return ok; +} + +static bool amduatd_sync_status_append_cursor( + amduatd_strbuf_t *b, + amduat_asl_store_t *store, + amduat_asl_pointer_store_t *pointer_store, + const amduatd_space_t *effective_space, + const char *peer_key, + bool push) { + amduatd_fed_cursor_record_t cursor; + amduat_reference_t ref; + amduatd_fed_cursor_status_t status; + bool present = false; + const char *error = NULL; + char *ref_hex = NULL; + + amduatd_fed_cursor_record_init(&cursor); + ref = amduat_reference(0u, amduat_octets(NULL, 0u)); + + status = push + ? amduatd_fed_push_cursor_get(store, + pointer_store, + effective_space, + peer_key, + &cursor, + &ref) + : amduatd_fed_cursor_get(store, + pointer_store, + effective_space, + peer_key, + &cursor, + &ref); + + if (status == AMDUATD_FED_CURSOR_OK) { + present = true; + } else if (status == AMDUATD_FED_CURSOR_ERR_NOT_FOUND) { + present = false; + } else if (status == AMDUATD_FED_CURSOR_ERR_CODEC) { + present = false; + error = "codec"; + } else { + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&ref); + return false; + } + + if (present && cursor.has_record_ref) { + if (!amduat_asl_ref_encode_hex(cursor.last_record_ref, &ref_hex)) { + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&ref); + return false; + } + } + + if (!amduatd_strbuf_append_cstr(b, "{\"present\":") || + !amduatd_strbuf_append_cstr(b, present ? "true" : "false")) { + free(ref_hex); + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&ref); + return false; + } + if (error != NULL) { + if (!amduatd_strbuf_append_cstr(b, ",\"error\":\"") || + !amduatd_strbuf_append_cstr(b, error) || + !amduatd_strbuf_append_cstr(b, "\"")) { + free(ref_hex); + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&ref); + return false; + } + } + if (present && cursor.has_logseq) { + char tmp[32]; + int n = snprintf(tmp, sizeof(tmp), "%llu", + (unsigned long long)cursor.last_logseq); + if (n <= 0 || (size_t)n >= sizeof(tmp)) { + free(ref_hex); + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&ref); + return false; + } + if (!amduatd_strbuf_append_cstr(b, ",\"last_logseq\":") || + !amduatd_strbuf_append_cstr(b, tmp)) { + free(ref_hex); + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&ref); + return false; + } + } + if (present && ref_hex != NULL) { + if (!amduatd_strbuf_append_cstr(b, ",\"ref\":\"") || + !amduatd_strbuf_append_cstr(b, ref_hex) || + !amduatd_strbuf_append_cstr(b, "\"")) { + free(ref_hex); + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&ref); + return false; + } + } + if (!amduatd_strbuf_append_cstr(b, "}")) { + free(ref_hex); + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&ref); + return false; + } + + free(ref_hex); + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&ref); + return true; +} + +static bool amduatd_handle_get_space_sync_status( + int fd, + amduat_asl_store_t *store, + const amduatd_http_req_t *req, + const amduatd_cfg_t *dcfg, + const amduatd_fed_cfg_t *fed_cfg, + const amduatd_caps_t *caps, + const char *root_path, + amduatd_store_backend_t store_backend) { + amduat_asl_pointer_store_t pointer_store; + amduatd_space_roots_list_t peers; + amduatd_strbuf_t b; + bool ok = false; + + memset(&peers, 0, sizeof(peers)); + memset(&b, 0, sizeof(b)); + + if (store == NULL || req == NULL || dcfg == NULL || fed_cfg == NULL || + root_path == NULL) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "internal error"); + } + + if (caps != NULL && caps->enabled && req->x_capability[0] != '\0') { + const char *reason = NULL; + if (!amduatd_caps_check_space(caps, dcfg, req, &reason)) { + if (reason != NULL && strcmp(reason, "wrong-space") == 0) { + return amduatd_send_json_error(fd, 403, "Forbidden", + "space not permitted by capability"); + } + return amduatd_send_json_error(fd, 403, "Forbidden", + "invalid capability"); + } + } + + if (!amduat_asl_pointer_store_init(&pointer_store, root_path)) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "pointer store error"); + } + if (!amduatd_space_roots_list_cursor_peers(root_path, + req->effective_space, + &peers)) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "cursor scan failed"); + } + + if (!amduatd_strbuf_append_cstr(&b, "{\"effective_space\":{")) { + goto sync_cleanup; + } + if (req->effective_space != NULL && req->effective_space->enabled && + req->effective_space->space_id.data != NULL) { + const char *space_id = (const char *)req->effective_space->space_id.data; + if (!amduatd_strbuf_append_cstr(&b, "\"mode\":\"scoped\",") || + !amduatd_strbuf_append_cstr(&b, "\"space_id\":\"") || + !amduatd_strbuf_append_cstr(&b, space_id) || + !amduatd_strbuf_append_cstr(&b, "\"")) { + goto sync_cleanup; + } + } else { + if (!amduatd_strbuf_append_cstr(&b, "\"mode\":\"unscoped\",") || + !amduatd_strbuf_append_cstr(&b, "\"space_id\":null")) { + goto sync_cleanup; + } + } + if (!amduatd_strbuf_append_cstr(&b, "},\"store_backend\":\"") || + !amduatd_strbuf_append_cstr(&b, amduatd_store_backend_name(store_backend)) || + !amduatd_strbuf_append_cstr(&b, "\",\"federation\":{")) { + goto sync_cleanup; + } + if (!amduatd_strbuf_append_cstr(&b, "\"enabled\":") || + !amduatd_strbuf_append_cstr(&b, fed_cfg->enabled ? "true" : "false") || + !amduatd_strbuf_append_cstr(&b, ",\"transport\":\"") || + !amduatd_strbuf_append_cstr(&b, amduatd_fed_transport_name( + fed_cfg->transport_kind)) || + !amduatd_strbuf_append_cstr(&b, "\"},\"peers\":[")) { + goto sync_cleanup; + } + + for (size_t i = 0u; i < peers.len; ++i) { + const char *peer_key = peers.names[i]; + if (i != 0u) { + if (!amduatd_strbuf_append_char(&b, ',')) { + goto sync_cleanup; + } + } + if (!amduatd_strbuf_append_cstr(&b, "{\"peer_key\":\"") || + !amduatd_strbuf_append_cstr(&b, peer_key) || + !amduatd_strbuf_append_cstr(&b, "\",\"pull_cursor\":")) { + goto sync_cleanup; + } + if (!amduatd_sync_status_append_cursor(&b, + store, + &pointer_store, + req->effective_space, + peer_key, + false)) { + goto sync_cleanup; + } + if (!amduatd_strbuf_append_cstr(&b, ",\"push_cursor\":")) { + goto sync_cleanup; + } + if (!amduatd_sync_status_append_cursor(&b, + store, + &pointer_store, + req->effective_space, + peer_key, + true)) { + goto sync_cleanup; + } + if (!amduatd_strbuf_append_cstr(&b, "}")) { + goto sync_cleanup; + } + } + + if (!amduatd_strbuf_append_cstr(&b, "]}\n")) { + goto sync_cleanup; + } + + ok = amduatd_http_send_json(fd, 200, "OK", b.data, false); + +sync_cleanup: + if (!ok) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", "error"); + } + amduatd_space_roots_list_free(&peers); + amduatd_strbuf_free(&b); + return ok; +} + static bool amduatd_seed_api_contract(amduat_asl_store_t *store, const amduat_asl_store_fs_config_t *cfg, amduat_reference_t *out_ref) { @@ -6512,7 +6889,8 @@ static bool amduatd_handle_conn(int fd, const amduat_fed_coord_t *coord, const amduatd_allowlist_t *allowlist, amduatd_caps_t *caps, - const char *root_path) { + const char *root_path, + amduatd_store_backend_t store_backend) { amduatd_http_req_t req; char no_query[1024]; bool ok = false; @@ -6619,6 +6997,27 @@ static bool amduatd_handle_conn(int fd, root_path); goto conn_cleanup; } + if (strcmp(req.method, "GET") == 0 && + strcmp(no_query, "/v1/space/roots") == 0) { + ok = amduatd_handle_get_space_roots(fd, + &req, + effective_cfg, + caps, + root_path); + goto conn_cleanup; + } + if (strcmp(req.method, "GET") == 0 && + strcmp(no_query, "/v1/space/sync/status") == 0) { + ok = amduatd_handle_get_space_sync_status(fd, + store, + &req, + effective_cfg, + fed_cfg, + caps, + root_path, + store_backend); + goto conn_cleanup; + } if (strcmp(req.method, "GET") == 0 && strcmp(no_query, "/v1/fed/records") == 0) { ok = amduatd_handle_get_fed_records(fd, store, fed_cfg, &req, root_path); @@ -7171,7 +7570,8 @@ int main(int argc, char **argv) { fed_coord, &allowlist, &caps, - root); + root, + store_backend); (void)close(cfd); if (dcfg.edges_refresh_ms != 0u) { diff --git a/src/amduatd_space_roots.c b/src/amduatd_space_roots.c new file mode 100644 index 0000000..6698b01 --- /dev/null +++ b/src/amduatd_space_roots.c @@ -0,0 +1,603 @@ +#include "amduatd_space_roots.h" + +#include "amduat/asl/asl_pointer_fs.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +static bool amduatd_space_roots_join_path(const char *base, + const char *segment, + char **out_path) { + size_t base_len; + size_t seg_len; + bool needs_sep; + size_t total_len; + char *buffer; + size_t offset = 0u; + + if (base == NULL || segment == NULL || out_path == NULL) { + return false; + } + if (base[0] == '\0') { + return false; + } + + base_len = strlen(base); + seg_len = strlen(segment); + needs_sep = base[base_len - 1u] != '/'; + if (seg_len > SIZE_MAX - base_len - 2u) { + return false; + } + total_len = base_len + (needs_sep ? 1u : 0u) + seg_len + 1u; + buffer = (char *)malloc(total_len); + if (buffer == NULL) { + return false; + } + + memcpy(buffer + offset, base, base_len); + offset += base_len; + if (needs_sep) { + buffer[offset++] = '/'; + } + if (seg_len != 0u) { + memcpy(buffer + offset, segment, seg_len); + offset += seg_len; + } + buffer[offset] = '\0'; + + *out_path = buffer; + return true; +} + +static bool amduatd_space_roots_segment_valid(const char *segment) { + size_t len; + size_t i; + + if (segment == NULL) { + return false; + } + len = strlen(segment); + if (len == 0u) { + return false; + } + if (len == 1u && segment[0] == '.') { + return false; + } + if (len == 2u && segment[0] == '.' && segment[1] == '.') { + return false; + } + for (i = 0u; i < len; ++i) { + unsigned char c = (unsigned char)segment[i]; + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-') { + continue; + } + return false; + } + return true; +} + +static bool amduatd_space_roots_list_reserve( + amduatd_space_roots_list_t *list, + size_t extra) { + size_t need; + size_t next_cap; + char **next; + + if (list == NULL) { + return false; + } + if (extra > (SIZE_MAX - list->len)) { + return false; + } + need = list->len + extra; + if (need <= list->cap) { + return true; + } + next_cap = list->cap != 0u ? list->cap : 8u; + while (next_cap < need) { + if (next_cap > (SIZE_MAX / 2u)) { + next_cap = need; + break; + } + next_cap *= 2u; + } + next = (char **)realloc(list->names, next_cap * sizeof(*next)); + if (next == NULL) { + return false; + } + list->names = next; + list->cap = next_cap; + return true; +} + +static bool amduatd_space_roots_list_add(amduatd_space_roots_list_t *list, + const char *name) { + size_t len; + char *copy; + + if (list == NULL || name == NULL) { + return false; + } + if (!amduat_asl_pointer_name_is_valid(name)) { + return false; + } + len = strlen(name); + if (len > SIZE_MAX - 1u) { + return false; + } + if (!amduatd_space_roots_list_reserve(list, 1u)) { + return false; + } + copy = (char *)malloc(len + 1u); + if (copy == NULL) { + return false; + } + memcpy(copy, name, len); + copy[len] = '\0'; + list->names[list->len++] = copy; + return true; +} + +static int amduatd_space_roots_list_cmp(const void *a, const void *b) { + const char *const *lhs = (const char *const *)a; + const char *const *rhs = (const char *const *)b; + if (lhs == NULL || rhs == NULL || *lhs == NULL || *rhs == NULL) { + return 0; + } + return strcmp(*lhs, *rhs); +} + +static void amduatd_space_roots_list_sort_dedupe( + amduatd_space_roots_list_t *list) { + size_t out = 0u; + if (list == NULL || list->len == 0u) { + return; + } + qsort(list->names, list->len, sizeof(*list->names), + amduatd_space_roots_list_cmp); + for (size_t i = 0u; i < list->len; ++i) { + if (out != 0u && strcmp(list->names[i], list->names[out - 1u]) == 0) { + free(list->names[i]); + continue; + } + list->names[out++] = list->names[i]; + } + list->len = out; +} + +static bool amduatd_space_roots_collect_dir( + const char *dir_path, + const char *rel_name, + amduatd_space_roots_list_t *list) { + DIR *dir; + struct dirent *entry; + + if (dir_path == NULL || rel_name == NULL || list == NULL) { + return false; + } + + dir = opendir(dir_path); + if (dir == NULL) { + if (errno == ENOENT) { + return true; + } + return false; + } + + while ((entry = readdir(dir)) != NULL) { + const char *name = entry->d_name; + char *child_path = NULL; + struct stat st; + bool is_dir = false; + bool is_file = false; + + if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) { + continue; + } + if (!amduatd_space_roots_segment_valid(name)) { + continue; + } + if (!amduatd_space_roots_join_path(dir_path, name, &child_path)) { + closedir(dir); + return false; + } + if (stat(child_path, &st) != 0) { + free(child_path); + closedir(dir); + return false; + } + is_dir = S_ISDIR(st.st_mode); + is_file = S_ISREG(st.st_mode); + if (is_dir) { + char *next_rel = NULL; + size_t rel_len = strlen(rel_name); + size_t name_len = strlen(name); + size_t total_len = rel_len + 1u + name_len + 1u; + if (rel_len == 0u) { + total_len = name_len + 1u; + } + next_rel = (char *)malloc(total_len); + if (next_rel == NULL) { + free(child_path); + closedir(dir); + return false; + } + if (rel_len != 0u) { + memcpy(next_rel, rel_name, rel_len); + next_rel[rel_len] = '/'; + memcpy(next_rel + rel_len + 1u, name, name_len); + next_rel[rel_len + 1u + name_len] = '\0'; + } else { + memcpy(next_rel, name, name_len); + next_rel[name_len] = '\0'; + } + if (!amduatd_space_roots_collect_dir(child_path, next_rel, list)) { + free(next_rel); + free(child_path); + closedir(dir); + return false; + } + free(next_rel); + } else if (is_file && strcmp(name, "head") == 0) { + if (rel_name[0] != '\0') { + if (!amduatd_space_roots_list_add(list, rel_name)) { + free(child_path); + closedir(dir); + return false; + } + } + } + free(child_path); + } + + closedir(dir); + return true; +} + +static bool amduatd_space_roots_collect_cursor_prefix( + const char *pointers_root, + const char *prefix_name, + amduatd_space_roots_list_t *list) { + char *prefix_path = NULL; + bool ok = false; + + if (pointers_root == NULL || prefix_name == NULL || list == NULL) { + return false; + } + if (!amduat_asl_pointer_name_is_valid(prefix_name)) { + return false; + } + if (!amduatd_space_roots_join_path(pointers_root, prefix_name, + &prefix_path)) { + return false; + } + ok = amduatd_space_roots_collect_dir(prefix_path, prefix_name, list); + free(prefix_path); + return ok; +} + +static bool amduatd_space_roots_append_cursor_heads( + const char *store_root, + const amduatd_space_t *effective_space, + bool push, + amduatd_space_roots_list_t *list) { + char *pointers_root = NULL; + amduat_octets_t prefix = amduat_octets(NULL, 0u); + bool ok = false; + + if (store_root == NULL || list == NULL) { + return false; + } + if (!amduatd_space_scope_name(effective_space, + push ? "fed/push_cursor" : "fed/cursor", + &prefix)) { + return false; + } + if (!amduatd_space_roots_join_path(store_root, "pointers", + &pointers_root)) { + amduat_octets_free(&prefix); + return false; + } + ok = amduatd_space_roots_collect_cursor_prefix( + pointers_root, (const char *)prefix.data, list); + free(pointers_root); + amduat_octets_free(&prefix); + return ok; +} + +static bool amduatd_space_roots_cursor_peer_from_name( + const char *prefix, + const char *pointer_name, + char **out_peer) { + const char suffix[] = "/head"; + size_t prefix_len; + size_t name_len; + size_t suffix_len = sizeof(suffix) - 1u; + size_t peer_len; + char *peer; + + if (prefix == NULL || pointer_name == NULL || out_peer == NULL) { + return false; + } + *out_peer = NULL; + prefix_len = strlen(prefix); + name_len = strlen(pointer_name); + if (name_len <= prefix_len + 1u + suffix_len) { + return false; + } + if (strncmp(pointer_name, prefix, prefix_len) != 0) { + return false; + } + if (pointer_name[prefix_len] != '/') { + return false; + } + if (strcmp(pointer_name + name_len - suffix_len, suffix) != 0) { + return false; + } + peer_len = name_len - prefix_len - 1u - suffix_len; + if (peer_len == 0u) { + return false; + } + peer = (char *)malloc(peer_len + 1u); + if (peer == NULL) { + return false; + } + memcpy(peer, pointer_name + prefix_len + 1u, peer_len); + peer[peer_len] = '\0'; + *out_peer = peer; + return true; +} + +static bool amduatd_space_roots_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_roots_build_collection_log_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 = 4u + 11u + name_len + 4u + 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; +} + +bool amduatd_space_roots_list( + const char *store_root, + const amduat_asl_pointer_store_t *pointer_store, + const amduatd_space_t *effective_space, + amduatd_space_roots_list_t *out_list) { + 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 ok = false; + + if (out_list != NULL) { + memset(out_list, 0, sizeof(*out_list)); + } + if (store_root == NULL || pointer_store == NULL || out_list == NULL) { + return false; + } + (void)pointer_store; + + if (!amduatd_space_edges_collection_name(effective_space, + &edges_collection) || + !amduatd_space_edges_index_head_name(effective_space, + &edges_index_head)) { + goto cleanup; + } + if (!amduatd_space_roots_build_collection_head_name( + (const char *)edges_collection.data, &collection_head) || + !amduatd_space_roots_build_collection_log_head_name( + (const char *)edges_collection.data, &collection_log_head)) { + goto cleanup; + } + + if (!amduatd_space_roots_list_add(out_list, + (const char *)edges_index_head.data) || + !amduatd_space_roots_list_add(out_list, collection_head) || + !amduatd_space_roots_list_add(out_list, collection_log_head)) { + goto cleanup; + } + + if (!amduatd_space_roots_append_cursor_heads(store_root, + effective_space, + false, + out_list) || + !amduatd_space_roots_append_cursor_heads(store_root, + effective_space, + true, + out_list)) { + goto cleanup; + } + + amduatd_space_roots_list_sort_dedupe(out_list); + ok = true; + +cleanup: + free((void *)edges_collection.data); + free((void *)edges_index_head.data); + free(collection_head); + free(collection_log_head); + if (!ok) { + amduatd_space_roots_list_free(out_list); + } + return ok; +} + +bool amduatd_space_roots_list_cursor_heads( + const char *store_root, + const amduatd_space_t *effective_space, + bool push, + amduatd_space_roots_list_t *out_list) { + bool ok = false; + if (out_list != NULL) { + memset(out_list, 0, sizeof(*out_list)); + } + if (store_root == NULL || out_list == NULL) { + return false; + } + if (!amduatd_space_roots_append_cursor_heads(store_root, + effective_space, + push, + out_list)) { + goto cleanup; + } + amduatd_space_roots_list_sort_dedupe(out_list); + ok = true; + +cleanup: + if (!ok) { + amduatd_space_roots_list_free(out_list); + } + return ok; +} + +bool amduatd_space_roots_list_cursor_peers( + const char *store_root, + const amduatd_space_t *effective_space, + amduatd_space_roots_list_t *out_list) { + amduatd_space_roots_list_t pull_heads; + amduatd_space_roots_list_t push_heads; + amduat_octets_t pull_prefix = amduat_octets(NULL, 0u); + amduat_octets_t push_prefix = amduat_octets(NULL, 0u); + bool ok = false; + + memset(&pull_heads, 0, sizeof(pull_heads)); + memset(&push_heads, 0, sizeof(push_heads)); + if (out_list != NULL) { + memset(out_list, 0, sizeof(*out_list)); + } + if (store_root == NULL || out_list == NULL) { + return false; + } + if (!amduatd_space_scope_name(effective_space, + "fed/cursor", + &pull_prefix) || + !amduatd_space_scope_name(effective_space, + "fed/push_cursor", + &push_prefix)) { + goto cleanup; + } + if (!amduatd_space_roots_list_cursor_heads(store_root, + effective_space, + false, + &pull_heads) || + !amduatd_space_roots_list_cursor_heads(store_root, + effective_space, + true, + &push_heads)) { + goto cleanup; + } + + for (size_t i = 0u; i < pull_heads.len; ++i) { + char *peer = NULL; + if (amduatd_space_roots_cursor_peer_from_name( + (const char *)pull_prefix.data, + pull_heads.names[i], + &peer)) { + if (!amduatd_space_roots_list_add(out_list, peer)) { + free(peer); + goto cleanup; + } + free(peer); + } + } + for (size_t i = 0u; i < push_heads.len; ++i) { + char *peer = NULL; + if (amduatd_space_roots_cursor_peer_from_name( + (const char *)push_prefix.data, + push_heads.names[i], + &peer)) { + if (!amduatd_space_roots_list_add(out_list, peer)) { + free(peer); + goto cleanup; + } + free(peer); + } + } + + amduatd_space_roots_list_sort_dedupe(out_list); + ok = true; + +cleanup: + amduatd_space_roots_list_free(&pull_heads); + amduatd_space_roots_list_free(&push_heads); + amduat_octets_free(&pull_prefix); + amduat_octets_free(&push_prefix); + if (!ok) { + amduatd_space_roots_list_free(out_list); + } + return ok; +} + +void amduatd_space_roots_list_free(amduatd_space_roots_list_t *list) { + if (list == NULL) { + return; + } + for (size_t i = 0u; i < list->len; ++i) { + free(list->names[i]); + } + free(list->names); + memset(list, 0, sizeof(*list)); +} diff --git a/src/amduatd_space_roots.h b/src/amduatd_space_roots.h new file mode 100644 index 0000000..d6c4e05 --- /dev/null +++ b/src/amduatd_space_roots.h @@ -0,0 +1,44 @@ +#ifndef AMDUATD_SPACE_ROOTS_H +#define AMDUATD_SPACE_ROOTS_H + +#include "amduatd_space.h" + +#include "amduat/asl/asl_pointer_fs.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + char **names; + size_t len; + size_t cap; +} amduatd_space_roots_list_t; + +bool amduatd_space_roots_list( + const char *store_root, + const amduat_asl_pointer_store_t *pointer_store, + const amduatd_space_t *effective_space, + amduatd_space_roots_list_t *out_list); + +bool amduatd_space_roots_list_cursor_heads( + const char *store_root, + const amduatd_space_t *effective_space, + bool push, + amduatd_space_roots_list_t *out_list); + +bool amduatd_space_roots_list_cursor_peers( + const char *store_root, + const amduatd_space_t *effective_space, + amduatd_space_roots_list_t *out_list); + +void amduatd_space_roots_list_free(amduatd_space_roots_list_t *list); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* AMDUATD_SPACE_ROOTS_H */ diff --git a/tests/test_amduatd_space_roots.c b/tests/test_amduatd_space_roots.c new file mode 100644 index 0000000..f90948f --- /dev/null +++ b/tests/test_amduatd_space_roots.c @@ -0,0 +1,325 @@ +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#endif + +#include "amduatd_space_roots.h" + +#include "amduatd_fed_cursor.h" +#include "amduatd_space.h" +#include "amduatd_store.h" + +#include "amduat/asl/asl_store_fs_meta.h" +#include "amduat/asl/asl_pointer_fs.h" +#include "amduat/asl/none.h" + +#include +#include +#include + +static char *amduatd_test_make_temp_dir(void) { + char tmpl[] = "/tmp/amduatd-roots-XXXXXX"; + char *dir = mkdtemp(tmpl); + size_t len; + char *copy; + if (dir == NULL) { + perror("mkdtemp"); + return NULL; + } + len = strlen(dir); + copy = (char *)malloc(len + 1u); + if (copy == NULL) { + fprintf(stderr, "failed to allocate temp dir copy\n"); + return NULL; + } + memcpy(copy, dir, len + 1u); + return copy; +} + +static bool amduatd_list_contains(const amduatd_space_roots_list_t *list, + const char *name) { + if (list == NULL || name == NULL) { + return false; + } + for (size_t i = 0u; i < list->len; ++i) { + if (strcmp(list->names[i], name) == 0) { + return true; + } + } + return false; +} + +static bool amduatd_list_sorted(const amduatd_space_roots_list_t *list) { + if (list == NULL || list->len < 2u) { + return true; + } + for (size_t i = 1u; i < list->len; ++i) { + if (strcmp(list->names[i - 1u], list->names[i]) > 0) { + return false; + } + } + return true; +} + +static bool amduatd_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_build_collection_log_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 = 4u + 11u + name_len + 4u + 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; +} + +static int amduatd_test_empty_store(void) { + char *root = amduatd_test_make_temp_dir(); + amduat_asl_store_fs_config_t cfg; + amduat_asl_pointer_store_t pointer_store; + amduatd_space_t space; + amduatd_space_roots_list_t list; + + if (root == NULL) { + return 1; + } + memset(&cfg, 0, sizeof(cfg)); + if (!amduat_asl_store_fs_init_root(root, NULL, &cfg)) { + fprintf(stderr, "failed to init store root\n"); + free(root); + return 1; + } + if (!amduat_asl_pointer_store_init(&pointer_store, root)) { + fprintf(stderr, "failed to init pointer store\n"); + free(root); + return 1; + } + if (!amduatd_space_init(&space, NULL, false)) { + fprintf(stderr, "failed to init space\n"); + free(root); + return 1; + } + + if (!amduatd_space_roots_list(root, &pointer_store, &space, &list)) { + fprintf(stderr, "roots list failed\n"); + free(root); + return 1; + } + + if (list.len != 3u || + !amduatd_list_contains(&list, "daemon/edges/index/head") || + !amduatd_list_contains(&list, "collection/daemon/edges/head") || + !amduatd_list_contains(&list, "log/collection/daemon/edges/log/head") || + !amduatd_list_sorted(&list)) { + fprintf(stderr, "unexpected empty-store roots list\n"); + amduatd_space_roots_list_free(&list); + free(root); + return 1; + } + + amduatd_space_roots_list_free(&list); + free(root); + return 0; +} + +static int amduatd_test_cursor_roots(void) { + char *root = amduatd_test_make_temp_dir(); + amduat_asl_store_fs_config_t cfg; + amduatd_store_ctx_t store_ctx; + amduat_asl_store_t store; + amduat_asl_pointer_store_t pointer_store; + amduatd_space_t space; + amduat_artifact_t none_artifact; + amduat_reference_t none_ref; + amduat_octets_t cursor_name = amduat_octets(NULL, 0u); + amduat_octets_t push_name = amduat_octets(NULL, 0u); + 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; + amduatd_space_roots_list_t list; + bool swapped = false; + + if (root == NULL) { + return 1; + } + memset(&cfg, 0, sizeof(cfg)); + if (!amduat_asl_store_fs_init_root(root, NULL, &cfg)) { + fprintf(stderr, "failed to init store root\n"); + free(root); + return 1; + } + memset(&store_ctx, 0, sizeof(store_ctx)); + memset(&store, 0, sizeof(store)); + if (!amduatd_store_init(&store, + &cfg, + &store_ctx, + root, + AMDUATD_STORE_BACKEND_FS)) { + fprintf(stderr, "failed to init store\n"); + free(root); + return 1; + } + if (!amduat_asl_pointer_store_init(&pointer_store, root)) { + fprintf(stderr, "failed to init pointer store\n"); + free(root); + return 1; + } + if (!amduatd_space_init(&space, "alpha", false)) { + fprintf(stderr, "failed to init space\n"); + free(root); + return 1; + } + + if (!amduat_asl_none_artifact(&none_artifact)) { + fprintf(stderr, "failed to build none artifact\n"); + free(root); + return 1; + } + if (amduat_asl_store_put(&store, none_artifact, &none_ref) != + AMDUAT_ASL_STORE_OK) { + fprintf(stderr, "failed to store none artifact\n"); + amduat_artifact_free(&none_artifact); + free(root); + return 1; + } + amduat_artifact_free(&none_artifact); + + if (!amduatd_fed_cursor_pointer_name(&space, "peer-a", &cursor_name) || + !amduatd_fed_push_cursor_pointer_name(&space, "peer-b", &push_name)) { + fprintf(stderr, "failed to build cursor names\n"); + free(root); + return 1; + } + + if (amduat_asl_pointer_cas(&pointer_store, + (const char *)cursor_name.data, + false, + NULL, + &none_ref, + &swapped) != AMDUAT_ASL_POINTER_OK || !swapped || + amduat_asl_pointer_cas(&pointer_store, + (const char *)push_name.data, + false, + NULL, + &none_ref, + &swapped) != AMDUAT_ASL_POINTER_OK || !swapped) { + fprintf(stderr, "failed to seed cursor pointers\n"); + amduat_octets_free(&cursor_name); + amduat_octets_free(&push_name); + free(root); + return 1; + } + + if (!amduatd_space_roots_list(root, &pointer_store, &space, &list)) { + fprintf(stderr, "roots list failed\n"); + amduat_octets_free(&cursor_name); + amduat_octets_free(&push_name); + free(root); + return 1; + } + + if (!amduatd_space_edges_collection_name(&space, &edges_collection) || + !amduatd_space_edges_index_head_name(&space, &edges_index_head) || + !amduatd_build_collection_head_name( + (const char *)edges_collection.data, &collection_head) || + !amduatd_build_collection_log_head_name( + (const char *)edges_collection.data, &collection_log_head)) { + fprintf(stderr, "failed to build static root names\n"); + amduat_octets_free(&cursor_name); + amduat_octets_free(&push_name); + amduatd_space_roots_list_free(&list); + free(root); + return 1; + } + + if (list.len != 5u || + !amduatd_list_contains(&list, (const char *)cursor_name.data) || + !amduatd_list_contains(&list, (const char *)push_name.data) || + !amduatd_list_contains(&list, (const char *)edges_index_head.data) || + !amduatd_list_contains(&list, collection_head) || + !amduatd_list_contains(&list, collection_log_head) || + !amduatd_list_sorted(&list)) { + fprintf(stderr, "unexpected cursor roots list\n"); + amduat_octets_free(&cursor_name); + amduat_octets_free(&push_name); + amduat_octets_free(&edges_collection); + amduat_octets_free(&edges_index_head); + free(collection_head); + free(collection_log_head); + amduatd_space_roots_list_free(&list); + free(root); + return 1; + } + + amduat_octets_free(&cursor_name); + amduat_octets_free(&push_name); + amduat_octets_free(&edges_collection); + amduat_octets_free(&edges_index_head); + free(collection_head); + free(collection_log_head); + amduatd_space_roots_list_free(&list); + free(root); + return 0; +} + +int main(void) { + if (amduatd_test_empty_store() != 0) { + return 1; + } + if (amduatd_test_cursor_roots() != 0) { + return 1; + } + return 0; +} diff --git a/tests/test_amduatd_space_sync_status.c b/tests/test_amduatd_space_sync_status.c new file mode 100644 index 0000000..d0c50c6 --- /dev/null +++ b/tests/test_amduatd_space_sync_status.c @@ -0,0 +1,228 @@ +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#endif + +#include "amduatd_space_roots.h" + +#include "amduatd_fed_cursor.h" +#include "amduatd_space.h" +#include "amduatd_store.h" + +#include "amduat/asl/asl_pointer_fs.h" +#include "amduat/asl/asl_store_fs_meta.h" +#include "amduat/asl/none.h" + +#include +#include +#include + +static char *amduatd_test_make_temp_dir(void) { + char tmpl[] = "/tmp/amduatd-sync-XXXXXX"; + char *dir = mkdtemp(tmpl); + size_t len; + char *copy; + if (dir == NULL) { + perror("mkdtemp"); + return NULL; + } + len = strlen(dir); + copy = (char *)malloc(len + 1u); + if (copy == NULL) { + fprintf(stderr, "failed to allocate temp dir copy\n"); + return NULL; + } + memcpy(copy, dir, len + 1u); + return copy; +} + +static bool amduatd_list_contains(const amduatd_space_roots_list_t *list, + const char *name) { + if (list == NULL || name == NULL) { + return false; + } + for (size_t i = 0u; i < list->len; ++i) { + if (strcmp(list->names[i], name) == 0) { + return true; + } + } + return false; +} + +static bool amduatd_list_sorted(const amduatd_space_roots_list_t *list) { + if (list == NULL || list->len < 2u) { + return true; + } + for (size_t i = 1u; i < list->len; ++i) { + if (strcmp(list->names[i - 1u], list->names[i]) > 0) { + return false; + } + } + return true; +} + +static int amduatd_test_empty_peers(void) { + char *root = amduatd_test_make_temp_dir(); + amduat_asl_store_fs_config_t cfg; + amduat_asl_pointer_store_t pointer_store; + amduatd_space_t space; + amduatd_space_roots_list_t peers; + + if (root == NULL) { + return 1; + } + memset(&cfg, 0, sizeof(cfg)); + if (!amduat_asl_store_fs_init_root(root, NULL, &cfg)) { + fprintf(stderr, "failed to init store root\n"); + free(root); + return 1; + } + if (!amduat_asl_pointer_store_init(&pointer_store, root)) { + fprintf(stderr, "failed to init pointer store\n"); + free(root); + return 1; + } + if (!amduatd_space_init(&space, "alpha", false)) { + fprintf(stderr, "failed to init space\n"); + free(root); + return 1; + } + + if (!amduatd_space_roots_list_cursor_peers(root, &space, &peers)) { + fprintf(stderr, "cursor peer list failed\n"); + free(root); + return 1; + } + + if (peers.len != 0u) { + fprintf(stderr, "expected empty peers list\n"); + amduatd_space_roots_list_free(&peers); + free(root); + return 1; + } + + amduatd_space_roots_list_free(&peers); + free(root); + return 0; +} + +static int amduatd_test_peer_discovery(void) { + char *root = amduatd_test_make_temp_dir(); + amduat_asl_store_fs_config_t cfg; + amduatd_store_ctx_t store_ctx; + amduat_asl_store_t store; + amduat_asl_pointer_store_t pointer_store; + amduatd_space_t space; + amduat_artifact_t none_artifact; + amduat_reference_t none_ref; + amduat_octets_t pull_name = amduat_octets(NULL, 0u); + amduat_octets_t push_name = amduat_octets(NULL, 0u); + amduatd_space_roots_list_t peers; + bool swapped = false; + + if (root == NULL) { + return 1; + } + memset(&cfg, 0, sizeof(cfg)); + if (!amduat_asl_store_fs_init_root(root, NULL, &cfg)) { + fprintf(stderr, "failed to init store root\n"); + free(root); + return 1; + } + memset(&store_ctx, 0, sizeof(store_ctx)); + memset(&store, 0, sizeof(store)); + if (!amduatd_store_init(&store, + &cfg, + &store_ctx, + root, + AMDUATD_STORE_BACKEND_FS)) { + fprintf(stderr, "failed to init store\n"); + free(root); + return 1; + } + if (!amduat_asl_pointer_store_init(&pointer_store, root)) { + fprintf(stderr, "failed to init pointer store\n"); + free(root); + return 1; + } + if (!amduatd_space_init(&space, "alpha", false)) { + fprintf(stderr, "failed to init space\n"); + free(root); + return 1; + } + + if (!amduat_asl_none_artifact(&none_artifact)) { + fprintf(stderr, "failed to build none artifact\n"); + free(root); + return 1; + } + if (amduat_asl_store_put(&store, none_artifact, &none_ref) != + AMDUAT_ASL_STORE_OK) { + fprintf(stderr, "failed to store none artifact\n"); + amduat_artifact_free(&none_artifact); + free(root); + return 1; + } + amduat_artifact_free(&none_artifact); + + if (!amduatd_fed_cursor_pointer_name(&space, "1", &pull_name) || + !amduatd_fed_push_cursor_pointer_name(&space, "2", &push_name)) { + fprintf(stderr, "failed to build cursor names\n"); + free(root); + return 1; + } + + if (amduat_asl_pointer_cas(&pointer_store, + (const char *)pull_name.data, + false, + NULL, + &none_ref, + &swapped) != AMDUAT_ASL_POINTER_OK || !swapped || + amduat_asl_pointer_cas(&pointer_store, + (const char *)push_name.data, + false, + NULL, + &none_ref, + &swapped) != AMDUAT_ASL_POINTER_OK || !swapped) { + fprintf(stderr, "failed to seed cursor pointers\n"); + amduat_octets_free(&pull_name); + amduat_octets_free(&push_name); + free(root); + return 1; + } + + if (!amduatd_space_roots_list_cursor_peers(root, &space, &peers)) { + fprintf(stderr, "cursor peer list failed\n"); + amduat_octets_free(&pull_name); + amduat_octets_free(&push_name); + free(root); + return 1; + } + + if (peers.len != 2u || + !amduatd_list_contains(&peers, "1") || + !amduatd_list_contains(&peers, "2") || + !amduatd_list_sorted(&peers)) { + fprintf(stderr, "unexpected peers list\n"); + amduatd_space_roots_list_free(&peers); + amduat_octets_free(&pull_name); + amduat_octets_free(&push_name); + free(root); + return 1; + } + + amduatd_space_roots_list_free(&peers); + amduat_octets_free(&pull_name); + amduat_octets_free(&push_name); + free(root); + return 0; +} + +int main(void) { + if (amduatd_test_empty_peers() != 0) { + return 1; + } + if (amduatd_test_peer_discovery() != 0) { + return 1; + } + return 0; +}