diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b632cb..aee397b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,8 @@ set(amduatd_sources src/amduatd.c src/amduatd_http.c src/amduatd_caps.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_roots.c - src/amduatd_space_manifest.c) + src/amduatd_space_manifest.c + src/amduatd_space_mounts.c) if(AMDUATD_ENABLE_UI) list(APPEND amduatd_sources src/amduatd_ui.c) endif() @@ -389,6 +390,30 @@ target_link_libraries(amduatd_test_space_manifest add_test(NAME amduatd_space_manifest COMMAND amduatd_test_space_manifest) +add_executable(amduatd_test_space_mounts + tests/test_amduatd_space_mounts.c + src/amduatd_space_mounts.c + src/amduatd_space_manifest.c + src/amduatd_http.c + src/amduatd_fed_cursor.c + src/amduatd_space.c + src/amduatd_store.c +) + +target_include_directories(amduatd_test_space_mounts + 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_mounts + 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_mounts COMMAND amduatd_test_space_mounts) + add_executable(amduatd_test_space_sync_status tests/test_amduatd_space_sync_status.c src/amduatd_space_roots.c diff --git a/README.md b/README.md index 8160b1d..b4e16d5 100644 --- a/README.md +++ b/README.md @@ -282,6 +282,20 @@ curl --unix-socket amduatd.sock \ `If-Match` can be replaced with `?expected_ref=` if needed. +## Space mount resolution + +`/v1/space/mounts/resolve` returns a deterministic, local-only view of the +space manifest mounts with their local pull cursor state. It performs no +network I/O and does not mutate storage. Track mounts indicate intent; syncing +remains a separate concern. +If no manifest head is present, the endpoint returns a 404. + +```sh +curl --unix-socket amduatd.sock \ + 'http://localhost/v1/space/mounts/resolve' \ + -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 10807ef..aaaf862 100644 --- a/registry/amduatd-api-contract.v1.json +++ b/registry/amduatd-api-contract.v1.json @@ -10,6 +10,7 @@ {"method": "GET", "path": "/v1/space/roots"}, {"method": "GET", "path": "/v1/space/manifest"}, {"method": "PUT", "path": "/v1/space/manifest"}, + {"method": "GET", "path": "/v1/space/mounts/resolve"}, {"method": "GET", "path": "/v1/space/sync/status"}, {"method": "POST", "path": "/v1/capabilities"}, {"method": "GET", "path": "/v1/cap/resolve"}, @@ -239,6 +240,15 @@ "previous_ref": {"type": "string"}, "manifest": {"$ref": "#/schemas/space_manifest"} } + }, + "space_mounts_resolve_response": { + "type": "object", + "required": ["effective_space", "manifest_ref", "mounts"], + "properties": { + "effective_space": {"type": "object"}, + "manifest_ref": {"type": "string"}, + "mounts": {"type": "array"} + } } } } diff --git a/src/amduatd.c b/src/amduatd.c index 968f7b8..8dcaad1 100644 --- a/src/amduatd.c +++ b/src/amduatd.c @@ -52,6 +52,7 @@ #include "amduatd_space_doctor.h" #include "amduatd_space_roots.h" #include "amduatd_space_manifest.h" +#include "amduatd_space_mounts.h" #include #include @@ -129,6 +130,7 @@ static const char k_amduatd_contract_v1_json[] = "{\"method\":\"GET\",\"path\":\"/v1/space/roots\"}," "{\"method\":\"GET\",\"path\":\"/v1/space/manifest\"}," "{\"method\":\"PUT\",\"path\":\"/v1/space/manifest\"}," + "{\"method\":\"GET\",\"path\":\"/v1/space/mounts/resolve\"}," "{\"method\":\"GET\",\"path\":\"/v1/space/sync/status\"}," "{\"method\":\"POST\",\"path\":\"/v1/capabilities\"}," "{\"method\":\"GET\",\"path\":\"/v1/cap/resolve\"}," @@ -332,6 +334,15 @@ static const char k_amduatd_contract_v1_json[] = "\"previous_ref\":{\"type\":\"string\"}," "\"manifest\":{\"$ref\":\"#/schemas/space_manifest\"}" "}" + "}," + "\"space_mounts_resolve_response\":{" + "\"type\":\"object\"," + "\"required\":[\"effective_space\",\"manifest_ref\",\"mounts\"]," + "\"properties\":{" + "\"effective_space\":{\"type\":\"object\"}," + "\"manifest_ref\":{\"type\":\"string\"}," + "\"mounts\":{\"type\":\"array\"}" + "}" "}" "}" "}\n"; @@ -1307,6 +1318,112 @@ manifest_cleanup: return ok; } +static bool amduatd_handle_get_space_mounts_resolve( + int fd, + amduat_asl_store_t *store, + 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; + amduat_reference_t manifest_ref; + amduatd_space_mounts_status_t status; + amduatd_strbuf_t b; + char *manifest_ref_hex = NULL; + char *mounts_json = NULL; + size_t mounts_len = 0u; + bool ok = false; + + memset(&manifest_ref, 0, sizeof(manifest_ref)); + memset(&b, 0, sizeof(b)); + + if (store == NULL || 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"); + } + + status = amduatd_space_mounts_resolve(store, + &pointer_store, + req->effective_space, + &manifest_ref, + &mounts_json, + &mounts_len); + if (status == AMDUATD_SPACE_MOUNTS_ERR_NOT_FOUND) { + return amduatd_send_json_error(fd, 404, "Not Found", + "manifest not found"); + } + if (status == AMDUATD_SPACE_MOUNTS_ERR_STORE) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "store error"); + } + if (status != AMDUATD_SPACE_MOUNTS_OK) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "manifest decode failed"); + } + + if (!amduat_asl_ref_encode_hex(manifest_ref, &manifest_ref_hex)) { + free(mounts_json); + amduat_reference_free(&manifest_ref); + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "encode error"); + } + + if (!amduatd_strbuf_append_cstr(&b, "{\"effective_space\":{")) { + goto mounts_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 mounts_cleanup; + } + } else { + if (!amduatd_strbuf_append_cstr(&b, "\"mode\":\"unscoped\",") || + !amduatd_strbuf_append_cstr(&b, "\"space_id\":null")) { + goto mounts_cleanup; + } + } + if (!amduatd_strbuf_append_cstr(&b, "},\"manifest_ref\":\"") || + !amduatd_strbuf_append_cstr(&b, manifest_ref_hex) || + !amduatd_strbuf_append_cstr(&b, "\",\"mounts\":") || + !amduatd_strbuf_append(&b, mounts_json, mounts_len) || + !amduatd_strbuf_append_cstr(&b, "}\n")) { + goto mounts_cleanup; + } + + ok = amduatd_http_send_json(fd, 200, "OK", b.data, false); + +mounts_cleanup: + if (!ok) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", "error"); + } + amduat_reference_free(&manifest_ref); + free(manifest_ref_hex); + free(mounts_json); + amduatd_strbuf_free(&b); + return ok; +} + static bool amduatd_handle_put_space_manifest( int fd, amduat_asl_store_t *store, @@ -8162,6 +8279,16 @@ static bool amduatd_handle_conn(int fd, root_path); goto conn_cleanup; } + if (strcmp(req.method, "GET") == 0 && + strcmp(no_query, "/v1/space/mounts/resolve") == 0) { + ok = amduatd_handle_get_space_mounts_resolve(fd, + store, + &req, + effective_cfg, + caps, + root_path); + goto conn_cleanup; + } if (strcmp(req.method, "PUT") == 0 && strcmp(no_query, "/v1/space/manifest") == 0) { ok = amduatd_handle_put_space_manifest(fd, diff --git a/src/amduatd_space_mounts.c b/src/amduatd_space_mounts.c new file mode 100644 index 0000000..2f5db23 --- /dev/null +++ b/src/amduatd_space_mounts.c @@ -0,0 +1,331 @@ +#include "amduatd_space_mounts.h" + +#include "amduat/asl/ref_text.h" +#include "amduatd_fed_cursor.h" +#include "amduatd_space_manifest.h" + +#include +#include +#include +#include + +typedef struct { + char *data; + size_t len; + size_t cap; +} amduatd_mounts_buf_t; + +static void amduatd_mounts_buf_free(amduatd_mounts_buf_t *b) { + if (b == NULL) { + return; + } + free(b->data); + b->data = NULL; + b->len = 0; + b->cap = 0; +} + +static bool amduatd_mounts_buf_reserve(amduatd_mounts_buf_t *b, size_t extra) { + size_t need; + size_t next_cap; + char *next; + + if (b == NULL) { + return false; + } + if (extra > (SIZE_MAX - b->len)) { + 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_mounts_buf_append(amduatd_mounts_buf_t *b, + const char *s, + size_t n) { + if (b == NULL) { + return false; + } + if (n == 0u) { + return true; + } + if (s == NULL) { + return false; + } + if (!amduatd_mounts_buf_reserve(b, n + 1u)) { + return false; + } + memcpy(b->data + b->len, s, n); + b->len += n; + b->data[b->len] = '\0'; + return true; +} + +static bool amduatd_mounts_buf_append_cstr(amduatd_mounts_buf_t *b, + const char *s) { + return amduatd_mounts_buf_append( + b, s != NULL ? s : "", s != NULL ? strlen(s) : 0u); +} + +static bool amduatd_mounts_buf_append_char(amduatd_mounts_buf_t *b, char c) { + return amduatd_mounts_buf_append(b, &c, 1u); +} + +static amduatd_space_mounts_status_t amduatd_space_mounts_append_tracking( + amduatd_mounts_buf_t *b, + amduat_asl_store_t *store, + amduat_asl_pointer_store_t *pointer_store, + const amduatd_space_t *effective_space, + const amduatd_space_manifest_mount_t *mount) { + amduatd_fed_cursor_record_t cursor; + amduat_reference_t cursor_ref; + amduatd_fed_cursor_status_t status; + bool cursor_present = false; + char *cursor_ref_hex = NULL; + + amduatd_fed_cursor_record_init(&cursor); + cursor_ref = amduat_reference(0u, amduat_octets(NULL, 0u)); + + if (mount->mode == AMDUATD_SPACE_MANIFEST_MOUNT_TRACK) { + status = amduatd_fed_cursor_get(store, + pointer_store, + effective_space, + mount->peer_key, + &cursor, + &cursor_ref); + if (status == AMDUATD_FED_CURSOR_ERR_NOT_FOUND) { + cursor_present = false; + } else if (status == AMDUATD_FED_CURSOR_OK) { + cursor_present = true; + } else if (status == AMDUATD_FED_CURSOR_ERR_CODEC) { + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&cursor_ref); + return AMDUATD_SPACE_MOUNTS_ERR_CODEC; + } else { + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&cursor_ref); + return AMDUATD_SPACE_MOUNTS_ERR_STORE; + } + } + + if (!amduatd_mounts_buf_append_cstr(b, ",\"local_tracking\":{") || + !amduatd_mounts_buf_append_cstr( + b, + "\"cursor_scope\":\"per-peer-per-local-space\"," + "\"remote_space_id\":\"") || + !amduatd_mounts_buf_append_cstr(b, mount->space_id) || + !amduatd_mounts_buf_append_cstr(b, "\",\"pull_cursor\":{") || + !amduatd_mounts_buf_append_cstr(b, + cursor_present ? "\"present\":true" + : "\"present\":false")) { + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&cursor_ref); + return AMDUATD_SPACE_MOUNTS_ERR_STORE; + } + + if (cursor_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)) { + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&cursor_ref); + return AMDUATD_SPACE_MOUNTS_ERR_STORE; + } + if (!amduatd_mounts_buf_append_cstr(b, ",\"last_logseq\":") || + !amduatd_mounts_buf_append_cstr(b, tmp)) { + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&cursor_ref); + return AMDUATD_SPACE_MOUNTS_ERR_STORE; + } + } + + if (cursor_present && cursor.has_record_ref) { + if (!amduat_asl_ref_encode_hex(cursor.last_record_ref, &cursor_ref_hex)) { + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&cursor_ref); + return AMDUATD_SPACE_MOUNTS_ERR_STORE; + } + if (!amduatd_mounts_buf_append_cstr(b, ",\"ref\":\"") || + !amduatd_mounts_buf_append_cstr(b, cursor_ref_hex) || + !amduatd_mounts_buf_append_cstr(b, "\"")) { + free(cursor_ref_hex); + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&cursor_ref); + return AMDUATD_SPACE_MOUNTS_ERR_STORE; + } + free(cursor_ref_hex); + } + + if (!amduatd_mounts_buf_append_cstr(b, "}}")) { + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&cursor_ref); + return AMDUATD_SPACE_MOUNTS_ERR_STORE; + } + + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&cursor_ref); + return AMDUATD_SPACE_MOUNTS_OK; +} + +amduatd_space_mounts_status_t amduatd_space_mounts_resolve( + amduat_asl_store_t *store, + amduat_asl_pointer_store_t *pointer_store, + const amduatd_space_t *effective_space, + amduat_reference_t *out_manifest_ref, + char **out_mounts_json, + size_t *out_mounts_len) { + amduatd_space_manifest_t manifest; + amduat_reference_t manifest_ref; + amduatd_space_manifest_status_t status; + amduatd_mounts_buf_t b; + + if (out_manifest_ref != NULL) { + *out_manifest_ref = amduat_reference(0u, amduat_octets(NULL, 0u)); + } + if (out_mounts_json != NULL) { + *out_mounts_json = NULL; + } + if (out_mounts_len != NULL) { + *out_mounts_len = 0u; + } + if (store == NULL || pointer_store == NULL || out_manifest_ref == NULL || + out_mounts_json == NULL || out_mounts_len == NULL) { + return AMDUATD_SPACE_MOUNTS_ERR_INVALID; + } + + memset(&manifest, 0, sizeof(manifest)); + memset(&manifest_ref, 0, sizeof(manifest_ref)); + status = amduatd_space_manifest_get(store, + pointer_store, + effective_space, + &manifest_ref, + &manifest); + if (status == AMDUATD_SPACE_MANIFEST_ERR_NOT_FOUND) { + return AMDUATD_SPACE_MOUNTS_ERR_NOT_FOUND; + } + if (status == AMDUATD_SPACE_MANIFEST_ERR_STORE) { + return AMDUATD_SPACE_MOUNTS_ERR_STORE; + } + if (status != AMDUATD_SPACE_MANIFEST_OK) { + return AMDUATD_SPACE_MOUNTS_ERR_CODEC; + } + + memset(&b, 0, sizeof(b)); + if (!amduatd_mounts_buf_append_char(&b, '[')) { + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&manifest_ref); + return AMDUATD_SPACE_MOUNTS_ERR_STORE; + } + + for (size_t i = 0u; i < manifest.mounts_len; ++i) { + const amduatd_space_manifest_mount_t *mount = &manifest.mounts[i]; + char *pinned_hex = NULL; + + if (i != 0u) { + if (!amduatd_mounts_buf_append_char(&b, ',')) { + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&manifest_ref); + amduatd_mounts_buf_free(&b); + return AMDUATD_SPACE_MOUNTS_ERR_STORE; + } + } + + if (!amduatd_mounts_buf_append_cstr(&b, "{\"name\":\"") || + !amduatd_mounts_buf_append_cstr(&b, mount->name) || + !amduatd_mounts_buf_append_cstr(&b, "\",\"peer_key\":\"") || + !amduatd_mounts_buf_append_cstr(&b, mount->peer_key) || + !amduatd_mounts_buf_append_cstr(&b, "\",\"space_id\":\"") || + !amduatd_mounts_buf_append_cstr(&b, mount->space_id) || + !amduatd_mounts_buf_append_cstr(&b, "\",\"mode\":\"") || + !amduatd_mounts_buf_append_cstr( + &b, + mount->mode == AMDUATD_SPACE_MANIFEST_MOUNT_PINNED ? "pinned" + : "track") || + !amduatd_mounts_buf_append_cstr(&b, "\"")) { + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&manifest_ref); + amduatd_mounts_buf_free(&b); + return AMDUATD_SPACE_MOUNTS_ERR_STORE; + } + + if (mount->mode == AMDUATD_SPACE_MANIFEST_MOUNT_PINNED && + mount->has_pinned_root_ref) { + if (!amduat_asl_ref_encode_hex(mount->pinned_root_ref, &pinned_hex)) { + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&manifest_ref); + amduatd_mounts_buf_free(&b); + return AMDUATD_SPACE_MOUNTS_ERR_STORE; + } + if (!amduatd_mounts_buf_append_cstr(&b, ",\"pinned_root_ref\":\"") || + !amduatd_mounts_buf_append_cstr(&b, pinned_hex) || + !amduatd_mounts_buf_append_cstr(&b, "\"")) { + free(pinned_hex); + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&manifest_ref); + amduatd_mounts_buf_free(&b); + return AMDUATD_SPACE_MOUNTS_ERR_STORE; + } + free(pinned_hex); + } + + { + amduatd_space_mounts_status_t track_status = + amduatd_space_mounts_append_tracking(&b, + store, + pointer_store, + effective_space, + mount); + if (track_status != AMDUATD_SPACE_MOUNTS_OK) { + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&manifest_ref); + amduatd_mounts_buf_free(&b); + return track_status; + } + } + + if (!amduatd_mounts_buf_append_cstr(&b, "}")) { + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&manifest_ref); + amduatd_mounts_buf_free(&b); + return AMDUATD_SPACE_MOUNTS_ERR_STORE; + } + } + + if (!amduatd_mounts_buf_append_char(&b, ']')) { + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&manifest_ref); + amduatd_mounts_buf_free(&b); + return AMDUATD_SPACE_MOUNTS_ERR_STORE; + } + + if (!amduat_reference_clone(manifest_ref, out_manifest_ref)) { + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&manifest_ref); + amduatd_mounts_buf_free(&b); + return AMDUATD_SPACE_MOUNTS_ERR_STORE; + } + + *out_mounts_json = b.data; + *out_mounts_len = b.len; + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&manifest_ref); + return AMDUATD_SPACE_MOUNTS_OK; +} diff --git a/src/amduatd_space_mounts.h b/src/amduatd_space_mounts.h new file mode 100644 index 0000000..3c29024 --- /dev/null +++ b/src/amduatd_space_mounts.h @@ -0,0 +1,35 @@ +#ifndef AMDUATD_SPACE_MOUNTS_H +#define AMDUATD_SPACE_MOUNTS_H + +#include "amduat/asl/asl_pointer_fs.h" +#include "amduat/asl/store.h" +#include "amduatd_space.h" + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + AMDUATD_SPACE_MOUNTS_OK = 0, + AMDUATD_SPACE_MOUNTS_ERR_INVALID = 1, + AMDUATD_SPACE_MOUNTS_ERR_NOT_FOUND = 2, + AMDUATD_SPACE_MOUNTS_ERR_STORE = 3, + AMDUATD_SPACE_MOUNTS_ERR_CODEC = 4 +} amduatd_space_mounts_status_t; + +amduatd_space_mounts_status_t amduatd_space_mounts_resolve( + amduat_asl_store_t *store, + amduat_asl_pointer_store_t *pointer_store, + const amduatd_space_t *effective_space, + amduat_reference_t *out_manifest_ref, + char **out_mounts_json, + size_t *out_mounts_len); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* AMDUATD_SPACE_MOUNTS_H */ diff --git a/tests/test_amduatd_space_mounts.c b/tests/test_amduatd_space_mounts.c new file mode 100644 index 0000000..2f423b2 --- /dev/null +++ b/tests/test_amduatd_space_mounts.c @@ -0,0 +1,356 @@ +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#endif + +#include "amduatd_space_mounts.h" + +#include "amduatd_fed_cursor.h" +#include "amduatd_space.h" +#include "amduatd_space_manifest.h" +#include "amduatd_store.h" + +#include "amduat/asl/asl_pointer_fs.h" +#include "amduat/asl/asl_store_fs_meta.h" +#include "amduat/asl/record.h" +#include "amduat/asl/ref_text.h" +#include "amduat/hash/asl1.h" + +#include +#include +#include +#include + +static int failures = 0; + +static void expect(bool cond, const char *msg) { + if (!cond) { + fprintf(stderr, "FAIL: %s\n", msg); + failures++; + } +} + +static char *amduatd_test_make_temp_dir(void) { + char tmpl[] = "/tmp/amduatd-space-mounts-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_make_test_ref(uint8_t fill, amduat_reference_t *out_ref) { + uint8_t digest_bytes[32]; + amduat_octets_t digest; + if (out_ref == NULL) { + return false; + } + memset(digest_bytes, fill, sizeof(digest_bytes)); + if (!amduat_octets_clone(amduat_octets(digest_bytes, sizeof(digest_bytes)), + &digest)) { + return false; + } + *out_ref = amduat_reference(AMDUAT_HASH_ASL1_ID_SHA256, digest); + return true; +} + +static int amduatd_test_mounts_missing(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_reference_t manifest_ref; + char *mounts_json = NULL; + size_t mounts_len = 0u; + amduatd_space_mounts_status_t status; + + 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; + } + + memset(&manifest_ref, 0, sizeof(manifest_ref)); + status = amduatd_space_mounts_resolve(&store, + &pointer_store, + &space, + &manifest_ref, + &mounts_json, + &mounts_len); + expect(status == AMDUATD_SPACE_MOUNTS_ERR_NOT_FOUND, + "missing manifest returns not found"); + expect(mounts_json == NULL, "mounts json unset on missing"); + amduat_reference_free(&manifest_ref); + free(root); + return failures == 0 ? 0 : 1; +} + +static int amduatd_test_mounts_resolve(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_reference_t pinned_ref; + char *pinned_hex = NULL; + char payload[512]; + amduat_reference_t record_ref; + amduat_octets_t pointer_name = amduat_octets(NULL, 0u); + bool swapped = false; + amduatd_fed_cursor_record_t cursor; + amduat_reference_t cursor_ref; + amduat_reference_t cursor_last_ref; + char *cursor_last_hex = NULL; + amduat_reference_t manifest_ref; + char *mounts_json = NULL; + size_t mounts_len = 0u; + amduatd_space_mounts_status_t status; + const char *m0 = NULL; + const char *m1 = NULL; + const char *m2 = NULL; + + 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 (!amduatd_make_test_ref(0x11, &pinned_ref)) { + fprintf(stderr, "failed to make pinned ref\n"); + free(root); + return 1; + } + if (!amduat_asl_ref_encode_hex(pinned_ref, &pinned_hex)) { + fprintf(stderr, "failed to encode pinned ref\n"); + amduat_reference_free(&pinned_ref); + free(root); + return 1; + } + { + int n = snprintf( + payload, + sizeof(payload), + "{" + "\"version\":1," + "\"mounts\":[" + "{\"name\":\"beta\",\"peer_key\":\"peer-2\",\"space_id\":\"zeta\"," + "\"mode\":\"track\"}," + "{\"name\":\"alpha\",\"peer_key\":\"peer-1\",\"space_id\":\"zeta\"," + "\"mode\":\"pinned\",\"pinned_root_ref\":\"%s\"}," + "{\"name\":\"alpha\",\"peer_key\":\"peer-1\",\"space_id\":\"beta\"," + "\"mode\":\"track\"}" + "]" + "}", + pinned_hex); + if (n <= 0 || (size_t)n >= sizeof(payload)) { + fprintf(stderr, "failed to build manifest payload\n"); + free(pinned_hex); + amduat_reference_free(&pinned_ref); + free(root); + return 1; + } + } + free(pinned_hex); + + if (amduat_asl_record_store_put( + &store, + amduat_octets(AMDUATD_SPACE_MANIFEST_1, + strlen(AMDUATD_SPACE_MANIFEST_1)), + amduat_octets((const uint8_t *)payload, strlen(payload)), + &record_ref) != AMDUAT_ASL_STORE_OK) { + fprintf(stderr, "failed to store manifest record\n"); + amduat_reference_free(&pinned_ref); + free(root); + return 1; + } + + if (!amduatd_space_scope_name(&space, "manifest/head", &pointer_name)) { + fprintf(stderr, "failed to build manifest pointer name\n"); + amduat_reference_free(&record_ref); + amduat_reference_free(&pinned_ref); + free(root); + return 1; + } + + if (amduat_asl_pointer_cas(&pointer_store, + (const char *)pointer_name.data, + false, + NULL, + &record_ref, + &swapped) != AMDUAT_ASL_POINTER_OK || + !swapped) { + fprintf(stderr, "failed to set manifest pointer\n"); + amduat_octets_free(&pointer_name); + amduat_reference_free(&record_ref); + amduat_reference_free(&pinned_ref); + free(root); + return 1; + } + amduat_octets_free(&pointer_name); + + if (!amduatd_make_test_ref(0x22, &cursor_last_ref)) { + fprintf(stderr, "failed to make cursor ref\n"); + amduat_reference_free(&record_ref); + amduat_reference_free(&pinned_ref); + free(root); + return 1; + } + if (!amduat_asl_ref_encode_hex(cursor_last_ref, &cursor_last_hex)) { + fprintf(stderr, "failed to encode cursor ref\n"); + amduat_reference_free(&cursor_last_ref); + amduat_reference_free(&record_ref); + amduat_reference_free(&pinned_ref); + free(root); + return 1; + } + + amduatd_fed_cursor_record_init(&cursor); + cursor.peer_key = strdup("peer-1"); + cursor.space_id = strdup("alpha"); + cursor.has_logseq = true; + cursor.last_logseq = 42u; + cursor.has_record_ref = true; + cursor.last_record_ref = cursor_last_ref; + memset(&cursor_ref, 0, sizeof(cursor_ref)); + if (amduatd_fed_cursor_cas_set(&store, + &pointer_store, + &space, + "peer-1", + NULL, + &cursor, + &cursor_ref) != AMDUATD_FED_CURSOR_OK) { + fprintf(stderr, "failed to set cursor\n"); + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&cursor_ref); + amduat_reference_free(&record_ref); + amduat_reference_free(&pinned_ref); + free(cursor_last_hex); + free(root); + return 1; + } + amduatd_fed_cursor_record_free(&cursor); + amduat_reference_free(&cursor_ref); + + memset(&manifest_ref, 0, sizeof(manifest_ref)); + status = amduatd_space_mounts_resolve(&store, + &pointer_store, + &space, + &manifest_ref, + &mounts_json, + &mounts_len); + expect(status == AMDUATD_SPACE_MOUNTS_OK, "mounts resolve ok"); + expect(amduat_reference_eq(manifest_ref, record_ref), + "resolve manifest ref matches pointer"); + expect(mounts_json != NULL && mounts_len != 0u, + "resolve mounts json populated"); + + if (mounts_json != NULL) { + m0 = strstr(mounts_json, + "\"name\":\"alpha\",\"peer_key\":\"peer-1\",\"space_id\":\"beta\""); + m1 = strstr(mounts_json, + "\"name\":\"alpha\",\"peer_key\":\"peer-1\",\"space_id\":\"zeta\""); + m2 = strstr(mounts_json, + "\"name\":\"beta\",\"peer_key\":\"peer-2\",\"space_id\":\"zeta\""); + expect(m0 != NULL && m1 != NULL && m2 != NULL, + "mounts include all entries"); + if (m0 != NULL && m1 != NULL && m2 != NULL) { + expect(m0 < m1 && m1 < m2, "mounts are in canonical order"); + } + expect(strstr(mounts_json, "\"mode\":\"pinned\"") != NULL, + "pinned mode present"); + expect(strstr(mounts_json, "\"pinned_root_ref\":\"") != NULL, + "pinned root ref present"); + expect(strstr(mounts_json, "\"cursor_scope\":\"per-peer-per-local-space\"") != NULL, + "cursor scope present"); + expect(strstr(mounts_json, "\"remote_space_id\":\"beta\"") != NULL, + "remote space id present"); + expect(strstr(mounts_json, "\"pull_cursor\":{\"present\":true") != NULL, + "cursor present true"); + expect(strstr(mounts_json, "\"last_logseq\":42") != NULL, + "cursor last_logseq present"); + expect(strstr(mounts_json, cursor_last_hex) != NULL, + "cursor ref present"); + expect(strstr(mounts_json, "\"pull_cursor\":{\"present\":false") != NULL, + "cursor present false"); + } + + free(mounts_json); + amduat_reference_free(&manifest_ref); + amduat_reference_free(&record_ref); + amduat_reference_free(&pinned_ref); + free(cursor_last_hex); + free(root); + return failures == 0 ? 0 : 1; +} + +int main(void) { + if (amduatd_test_mounts_missing() != 0) { + return 1; + } + if (amduatd_test_mounts_resolve() != 0) { + return 1; + } + return failures == 0 ? 0 : 1; +}