From 704ea18a32c92b76685f51f74c4a2cceb0540400 Mon Sep 17 00:00:00 2001 From: Carl Niklas Rydberg Date: Sat, 24 Jan 2026 18:42:01 +0100 Subject: [PATCH] Add space manifest CAS head and read-only /v1/space/manifest endpoint --- CMakeLists.txt | 25 +- README.md | 14 + registry/amduatd-api-contract.v1.json | 29 ++ registry/api-contract.jsonl | 2 +- src/amduatd.c | 198 +++++++++++ src/amduatd_http.c | 3 +- src/amduatd_space_manifest.c | 474 ++++++++++++++++++++++++++ src/amduatd_space_manifest.h | 60 ++++ tests/test_amduatd_space_manifest.c | 303 ++++++++++++++++ 9 files changed, 1105 insertions(+), 3 deletions(-) create mode 100644 src/amduatd_space_manifest.c create mode 100644 src/amduatd_space_manifest.h create mode 100644 tests/test_amduatd_space_manifest.c diff --git a/CMakeLists.txt b/CMakeLists.txt index f992dc4..9b632cb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,7 +31,8 @@ set(amduatd_sources src/amduatd.c src/amduatd_http.c src/amduatd_caps.c src/amduatd_fed_until.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_doctor.c src/amduatd_space_roots.c + src/amduatd_space_manifest.c) if(AMDUATD_ENABLE_UI) list(APPEND amduatd_sources src/amduatd_ui.c) endif() @@ -366,6 +367,28 @@ target_link_libraries(amduatd_test_space_roots add_test(NAME amduatd_space_roots COMMAND amduatd_test_space_roots) +add_executable(amduatd_test_space_manifest + tests/test_amduatd_space_manifest.c + src/amduatd_space_manifest.c + src/amduatd_space.c + src/amduatd_store.c + src/amduatd_http.c +) + +target_include_directories(amduatd_test_space_manifest + 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_manifest + 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_manifest COMMAND amduatd_test_space_manifest) + 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 f94bd14..28be980 100644 --- a/README.md +++ b/README.md @@ -245,6 +245,20 @@ curl --unix-socket amduatd.sock \ -H 'X-Amduat-Space: demo' ``` +## Space manifest + +`/v1/space/manifest` returns the space manifest rooted at the deterministic +pointer head (`manifest/head` or `space//manifest/head`). The manifest +is stored in CAS as a record and returned with its ref plus a decoded, +deterministic JSON payload. If no manifest head is present, the endpoint +returns a 404. + +```sh +curl --unix-socket amduatd.sock \ + 'http://localhost/v1/space/manifest' \ + -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 1dbb6b8..e191fed 100644 --- a/registry/amduatd-api-contract.v1.json +++ b/registry/amduatd-api-contract.v1.json @@ -8,6 +8,7 @@ {"method": "GET", "path": "/v1/contract"}, {"method": "GET", "path": "/v1/space/doctor"}, {"method": "GET", "path": "/v1/space/roots"}, + {"method": "GET", "path": "/v1/space/manifest"}, {"method": "GET", "path": "/v1/space/sync/status"}, {"method": "POST", "path": "/v1/capabilities"}, {"method": "GET", "path": "/v1/cap/resolve"}, @@ -198,6 +199,34 @@ "has_type_tag": {"type": "boolean"}, "type_tag": {"type": "string"} } + }, + "space_manifest_mount": { + "type": "object", + "required": ["name", "peer_key", "space_id", "mode"], + "properties": { + "name": {"type": "string"}, + "peer_key": {"type": "string"}, + "space_id": {"type": "string"}, + "mode": {"type": "string"}, + "pinned_root_ref": {"type": "string"} + } + }, + "space_manifest": { + "type": "object", + "required": ["version", "mounts"], + "properties": { + "version": {"type": "integer"}, + "mounts": {"type": "array", "items": {"$ref": "#/schemas/space_manifest_mount"}} + } + }, + "space_manifest_response": { + "type": "object", + "required": ["effective_space", "manifest_ref", "manifest"], + "properties": { + "effective_space": {"type": "object"}, + "manifest_ref": {"type": "string"}, + "manifest": {"$ref": "#/schemas/space_manifest"} + } } } } diff --git a/registry/api-contract.jsonl b/registry/api-contract.jsonl index cd66978..0505d7f 100644 --- a/registry/api-contract.jsonl +++ b/registry/api-contract.jsonl @@ -1 +1 @@ -{"registry":"AMDUATD/API","contract":"AMDUATD/API/1","handle":"amduat.api.amduatd.contract.v1@1","media_type":"application/json","status":"active","bytes_sha256":"34db2a9929eefe4c4d8d95314c0828746c0484dc178d3f63467a8c3d24c17110","notes":"Seeded into the ASL store at amduatd startup; ref is advertised via /v1/meta."} +{"registry":"AMDUATD/API","contract":"AMDUATD/API/1","handle":"amduat.api.amduatd.contract.v1@1","media_type":"application/json","status":"active","bytes_sha256":"e4f8bfbb6704576a9d90a39be6b1c484c0db1e69c6464d36a947ec3b79cbf6ba","notes":"Seeded into the ASL store at amduatd startup; ref is advertised via /v1/meta."} diff --git a/src/amduatd.c b/src/amduatd.c index 35ab098..95bba11 100644 --- a/src/amduatd.c +++ b/src/amduatd.c @@ -51,6 +51,7 @@ #include "amduatd_derivation_index.h" #include "amduatd_space_doctor.h" #include "amduatd_space_roots.h" +#include "amduatd_space_manifest.h" #include #include @@ -125,6 +126,7 @@ static const char k_amduatd_contract_v1_json[] = "{\"method\":\"GET\",\"path\":\"/v1/contract\"}," "{\"method\":\"GET\",\"path\":\"/v1/space/doctor\"}," "{\"method\":\"GET\",\"path\":\"/v1/space/roots\"}," + "{\"method\":\"GET\",\"path\":\"/v1/space/manifest\"}," "{\"method\":\"GET\",\"path\":\"/v1/space/sync/status\"}," "{\"method\":\"POST\",\"path\":\"/v1/capabilities\"}," "{\"method\":\"GET\",\"path\":\"/v1/cap/resolve\"}," @@ -289,6 +291,34 @@ static const char k_amduatd_contract_v1_json[] = "\"nodes\":{\"type\":\"array\"}," "\"roots\":{\"type\":\"array\"}" "}" + "}," + "\"space_manifest_mount\":{" + "\"type\":\"object\"," + "\"required\":[\"name\",\"peer_key\",\"space_id\",\"mode\"]," + "\"properties\":{" + "\"name\":{\"type\":\"string\"}," + "\"peer_key\":{\"type\":\"string\"}," + "\"space_id\":{\"type\":\"string\"}," + "\"mode\":{\"type\":\"string\"}," + "\"pinned_root_ref\":{\"type\":\"string\"}" + "}" + "}," + "\"space_manifest\":{" + "\"type\":\"object\"," + "\"required\":[\"version\",\"mounts\"]," + "\"properties\":{" + "\"version\":{\"type\":\"integer\"}," + "\"mounts\":{\"type\":\"array\",\"items\":{\"$ref\":\"#/schemas/space_manifest_mount\"}}" + "}" + "}," + "\"space_manifest_response\":{" + "\"type\":\"object\"," + "\"required\":[\"effective_space\",\"manifest_ref\",\"manifest\"]," + "\"properties\":{" + "\"effective_space\":{\"type\":\"object\"}," + "\"manifest_ref\":{\"type\":\"string\"}," + "\"manifest\":{\"$ref\":\"#/schemas/space_manifest\"}" + "}" "}" "}" "}\n"; @@ -1006,6 +1036,164 @@ roots_cleanup: return ok; } +static bool amduatd_handle_get_space_manifest( + 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_manifest_t manifest; + amduatd_space_manifest_status_t status; + amduatd_strbuf_t b; + char *manifest_ref_hex = NULL; + bool ok = false; + + memset(&manifest_ref, 0, sizeof(manifest_ref)); + memset(&manifest, 0, sizeof(manifest)); + 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_manifest_get(store, + &pointer_store, + req->effective_space, + &manifest_ref, + &manifest); + if (status == AMDUATD_SPACE_MANIFEST_ERR_NOT_FOUND) { + return amduatd_send_json_error(fd, 404, "Not Found", + "manifest not found"); + } + if (status == AMDUATD_SPACE_MANIFEST_ERR_STORE) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "store error"); + } + if (status != AMDUATD_SPACE_MANIFEST_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)) { + amduatd_space_manifest_free(&manifest); + 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 manifest_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 manifest_cleanup; + } + } else { + if (!amduatd_strbuf_append_cstr(&b, "\"mode\":\"unscoped\",") || + !amduatd_strbuf_append_cstr(&b, "\"space_id\":null")) { + goto manifest_cleanup; + } + } + if (!amduatd_strbuf_append_cstr(&b, "},\"manifest_ref\":\"") || + !amduatd_strbuf_append_cstr(&b, manifest_ref_hex) || + !amduatd_strbuf_append_cstr(&b, "\",\"manifest\":{")) { + goto manifest_cleanup; + } + { + char tmp[32]; + int n = snprintf(tmp, sizeof(tmp), "%u", manifest.version); + if (n <= 0 || (size_t)n >= sizeof(tmp)) { + goto manifest_cleanup; + } + if (!amduatd_strbuf_append_cstr(&b, "\"version\":") || + !amduatd_strbuf_append_cstr(&b, tmp) || + !amduatd_strbuf_append_cstr(&b, ",\"mounts\":[")) { + goto manifest_cleanup; + } + } + + for (size_t i = 0u; i < manifest.mounts_len; ++i) { + const amduatd_space_manifest_mount_t *mount = &manifest.mounts[i]; + char *root_ref_hex = NULL; + if (i != 0u) { + if (!amduatd_strbuf_append_char(&b, ',')) { + goto manifest_cleanup; + } + } + if (!amduatd_strbuf_append_cstr(&b, "{\"name\":\"") || + !amduatd_strbuf_append_cstr(&b, mount->name) || + !amduatd_strbuf_append_cstr(&b, "\",\"peer_key\":\"") || + !amduatd_strbuf_append_cstr(&b, mount->peer_key) || + !amduatd_strbuf_append_cstr(&b, "\",\"space_id\":\"") || + !amduatd_strbuf_append_cstr(&b, mount->space_id) || + !amduatd_strbuf_append_cstr(&b, "\",\"mode\":\"") || + !amduatd_strbuf_append_cstr( + &b, + mount->mode == AMDUATD_SPACE_MANIFEST_MOUNT_PINNED ? "pinned" + : "track") || + !amduatd_strbuf_append_cstr(&b, "\"")) { + goto manifest_cleanup; + } + if (mount->mode == AMDUATD_SPACE_MANIFEST_MOUNT_PINNED) { + if (!amduat_asl_ref_encode_hex(mount->pinned_root_ref, &root_ref_hex)) { + goto manifest_cleanup; + } + if (!amduatd_strbuf_append_cstr(&b, ",\"pinned_root_ref\":\"") || + !amduatd_strbuf_append_cstr(&b, root_ref_hex) || + !amduatd_strbuf_append_cstr(&b, "\"")) { + free(root_ref_hex); + goto manifest_cleanup; + } + free(root_ref_hex); + } + if (!amduatd_strbuf_append_cstr(&b, "}")) { + goto manifest_cleanup; + } + } + + if (!amduatd_strbuf_append_cstr(&b, "]}}\n")) { + goto manifest_cleanup; + } + + ok = amduatd_http_send_json(fd, 200, "OK", b.data, false); + +manifest_cleanup: + if (!ok) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", "error"); + } + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&manifest_ref); + free(manifest_ref_hex); + amduatd_strbuf_free(&b); + return ok; +} + static bool amduatd_sync_status_append_cursor( amduatd_strbuf_t *b, amduat_asl_store_t *store, @@ -7614,6 +7802,16 @@ static bool amduatd_handle_conn(int fd, root_path); goto conn_cleanup; } + if (strcmp(req.method, "GET") == 0 && + strcmp(no_query, "/v1/space/manifest") == 0) { + ok = amduatd_handle_get_space_manifest(fd, + store, + &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, diff --git a/src/amduatd_http.c b/src/amduatd_http.c index a7f1feb..5205416 100644 --- a/src/amduatd_http.c +++ b/src/amduatd_http.c @@ -639,10 +639,11 @@ bool amduatd_json_parse_u64(const char **p, tmp[n] = '\0'; errno = 0; v = strtoull(tmp, &endp, 10); - free(tmp); if (errno != 0 || endp == NULL || *endp != '\0') { + free(tmp); return false; } + free(tmp); *out = (uint64_t)v; *p = cur; return true; diff --git a/src/amduatd_space_manifest.c b/src/amduatd_space_manifest.c new file mode 100644 index 0000000..c009ab3 --- /dev/null +++ b/src/amduatd_space_manifest.c @@ -0,0 +1,474 @@ +#include "amduatd_space_manifest.h" + +#include "amduat/asl/ref_text.h" +#include "amduat/asl/record.h" +#include "amduatd_http.h" + +#include +#include +#include +#include + +static bool amduatd_space_manifest_decode_ref(const char *s, + size_t len, + amduat_reference_t *out_ref) { + char *tmp = NULL; + bool ok = false; + + if (out_ref == NULL) { + return false; + } + memset(out_ref, 0, sizeof(*out_ref)); + if (!amduatd_copy_json_str(s, len, &tmp)) { + return false; + } + ok = amduat_asl_ref_decode_hex(tmp, out_ref); + free(tmp); + return ok; +} + +static void amduatd_space_manifest_mount_free( + amduatd_space_manifest_mount_t *mount) { + if (mount == NULL) { + return; + } + free(mount->name); + free(mount->peer_key); + free(mount->space_id); + if (mount->has_pinned_root_ref) { + amduat_reference_free(&mount->pinned_root_ref); + } + memset(mount, 0, sizeof(*mount)); +} + +void amduatd_space_manifest_free(amduatd_space_manifest_t *manifest) { + if (manifest == NULL) { + return; + } + if (manifest->mounts != NULL) { + for (size_t i = 0u; i < manifest->mounts_len; ++i) { + amduatd_space_manifest_mount_free(&manifest->mounts[i]); + } + free(manifest->mounts); + } + memset(manifest, 0, sizeof(*manifest)); +} + +static bool amduatd_space_manifest_mounts_reserve( + amduatd_space_manifest_t *manifest, + size_t extra) { + size_t need; + size_t next_cap; + amduatd_space_manifest_mount_t *next; + + if (manifest == NULL) { + return false; + } + if (extra > (SIZE_MAX - manifest->mounts_len)) { + return false; + } + need = manifest->mounts_len + extra; + if (need <= manifest->mounts_cap) { + return true; + } + next_cap = manifest->mounts_cap != 0u ? manifest->mounts_cap : 4u; + while (next_cap < need) { + if (next_cap > (SIZE_MAX / 2u)) { + next_cap = need; + break; + } + next_cap *= 2u; + } + next = (amduatd_space_manifest_mount_t *)realloc( + manifest->mounts, next_cap * sizeof(*next)); + if (next == NULL) { + return false; + } + manifest->mounts = next; + manifest->mounts_cap = next_cap; + return true; +} + +static bool amduatd_space_manifest_add_mount( + amduatd_space_manifest_t *manifest, + amduatd_space_manifest_mount_t *mount) { + if (manifest == NULL || mount == NULL) { + return false; + } + if (!amduatd_space_manifest_mounts_reserve(manifest, 1u)) { + return false; + } + manifest->mounts[manifest->mounts_len++] = *mount; + memset(mount, 0, sizeof(*mount)); + return true; +} + +static int amduatd_space_manifest_mount_cmp(const void *a, const void *b) { + const amduatd_space_manifest_mount_t *lhs = + (const amduatd_space_manifest_mount_t *)a; + const amduatd_space_manifest_mount_t *rhs = + (const amduatd_space_manifest_mount_t *)b; + int cmp; + + if (lhs == NULL || rhs == NULL) { + return 0; + } + cmp = strcmp(lhs->name != NULL ? lhs->name : "", + rhs->name != NULL ? rhs->name : ""); + if (cmp != 0) { + return cmp; + } + cmp = strcmp(lhs->peer_key != NULL ? lhs->peer_key : "", + rhs->peer_key != NULL ? rhs->peer_key : ""); + if (cmp != 0) { + return cmp; + } + return strcmp(lhs->space_id != NULL ? lhs->space_id : "", + rhs->space_id != NULL ? rhs->space_id : ""); +} + +static bool amduatd_space_manifest_parse_mount( + const char **p, + const char *end, + amduatd_space_manifest_mount_t *out_mount) { + const char *key = NULL; + size_t key_len = 0u; + const char *sv = NULL; + size_t sv_len = 0u; + const char *cur = NULL; + bool have_name = false; + bool have_peer = false; + bool have_space = false; + bool have_mode = false; + bool have_pinned_root = false; + amduatd_space_manifest_mount_t mount; + + if (p == NULL || end == NULL || out_mount == NULL) { + return false; + } + memset(&mount, 0, sizeof(mount)); + + if (!amduatd_json_expect(p, end, '{')) { + return false; + } + + for (;;) { + cur = amduatd_json_skip_ws(*p, end); + if (cur < end && *cur == '}') { + *p = cur + 1; + break; + } + if (!amduatd_json_parse_string_noesc(p, end, &key, &key_len) || + !amduatd_json_expect(p, end, ':')) { + amduatd_space_manifest_mount_free(&mount); + return false; + } + + if (key_len == strlen("name") && memcmp(key, "name", key_len) == 0) { + if (have_name || + !amduatd_json_parse_string_noesc(p, end, &sv, &sv_len) || + !amduatd_copy_json_str(sv, sv_len, &mount.name)) { + amduatd_space_manifest_mount_free(&mount); + return false; + } + have_name = true; + } else if (key_len == strlen("peer_key") && + memcmp(key, "peer_key", key_len) == 0) { + if (have_peer || + !amduatd_json_parse_string_noesc(p, end, &sv, &sv_len) || + !amduatd_copy_json_str(sv, sv_len, &mount.peer_key)) { + amduatd_space_manifest_mount_free(&mount); + return false; + } + have_peer = true; + } else if (key_len == strlen("space_id") && + memcmp(key, "space_id", key_len) == 0) { + if (have_space || + !amduatd_json_parse_string_noesc(p, end, &sv, &sv_len) || + !amduatd_copy_json_str(sv, sv_len, &mount.space_id)) { + amduatd_space_manifest_mount_free(&mount); + return false; + } + have_space = true; + } else if (key_len == strlen("mode") && + memcmp(key, "mode", key_len) == 0) { + if (have_mode || + !amduatd_json_parse_string_noesc(p, end, &sv, &sv_len)) { + amduatd_space_manifest_mount_free(&mount); + return false; + } + if (sv_len == strlen("pinned") && memcmp(sv, "pinned", sv_len) == 0) { + mount.mode = AMDUATD_SPACE_MANIFEST_MOUNT_PINNED; + } else if (sv_len == strlen("track") && + memcmp(sv, "track", sv_len) == 0) { + mount.mode = AMDUATD_SPACE_MANIFEST_MOUNT_TRACK; + } else { + amduatd_space_manifest_mount_free(&mount); + return false; + } + have_mode = true; + } else if (key_len == strlen("pinned_root_ref") && + memcmp(key, "pinned_root_ref", key_len) == 0) { + if (have_pinned_root || + !amduatd_json_parse_string_noesc(p, end, &sv, &sv_len) || + !amduatd_space_manifest_decode_ref(sv, sv_len, + &mount.pinned_root_ref)) { + amduatd_space_manifest_mount_free(&mount); + return false; + } + mount.has_pinned_root_ref = true; + have_pinned_root = true; + } else { + if (!amduatd_json_skip_value(p, end, 0)) { + amduatd_space_manifest_mount_free(&mount); + return false; + } + } + + cur = amduatd_json_skip_ws(*p, end); + if (cur < end && *cur == ',') { + *p = cur + 1; + continue; + } + if (cur < end && *cur == '}') { + *p = cur + 1; + break; + } + amduatd_space_manifest_mount_free(&mount); + return false; + } + + if (!have_name || !have_peer || !have_space || !have_mode) { + amduatd_space_manifest_mount_free(&mount); + return false; + } + if (mount.name == NULL || !amduat_asl_pointer_name_is_valid(mount.name)) { + amduatd_space_manifest_mount_free(&mount); + return false; + } + if (mount.peer_key == NULL || + !amduat_asl_pointer_name_is_valid(mount.peer_key)) { + amduatd_space_manifest_mount_free(&mount); + return false; + } + if (mount.space_id == NULL || + !amduatd_space_space_id_is_valid(mount.space_id)) { + amduatd_space_manifest_mount_free(&mount); + return false; + } + if (mount.mode == AMDUATD_SPACE_MANIFEST_MOUNT_PINNED && + !mount.has_pinned_root_ref) { + amduatd_space_manifest_mount_free(&mount); + return false; + } + if (mount.mode == AMDUATD_SPACE_MANIFEST_MOUNT_TRACK && + mount.has_pinned_root_ref) { + amduatd_space_manifest_mount_free(&mount); + return false; + } + + *out_mount = mount; + return true; +} + +static bool amduatd_space_manifest_parse(amduat_octets_t payload, + amduatd_space_manifest_t *manifest) { + const char *p = NULL; + const char *end = NULL; + const char *key = NULL; + size_t key_len = 0u; + const char *cur = NULL; + bool have_version = false; + bool have_mounts = false; + uint64_t version = 0u; + + if (manifest == NULL) { + return false; + } + memset(manifest, 0, sizeof(*manifest)); + if (payload.len != 0u && payload.data == NULL) { + return false; + } + + p = (const char *)payload.data; + end = p + payload.len; + + if (!amduatd_json_expect(&p, end, '{')) { + return false; + } + + for (;;) { + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + if (!amduatd_json_parse_string_noesc(&p, end, &key, &key_len) || + !amduatd_json_expect(&p, end, ':')) { + amduatd_space_manifest_free(manifest); + return false; + } + + if (key_len == strlen("version") && + memcmp(key, "version", key_len) == 0) { + if (have_version || !amduatd_json_parse_u64(&p, end, &version)) { + amduatd_space_manifest_free(manifest); + return false; + } + have_version = true; + } else if (key_len == strlen("mounts") && + memcmp(key, "mounts", key_len) == 0) { + if (have_mounts || !amduatd_json_expect(&p, end, '[')) { + amduatd_space_manifest_free(manifest); + return false; + } + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ']') { + p = cur + 1; + have_mounts = true; + } else { + for (;;) { + amduatd_space_manifest_mount_t mount; + memset(&mount, 0, sizeof(mount)); + if (!amduatd_space_manifest_parse_mount(&p, end, &mount) || + !amduatd_space_manifest_add_mount(manifest, &mount)) { + amduatd_space_manifest_mount_free(&mount); + amduatd_space_manifest_free(manifest); + return false; + } + + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ',') { + p = cur + 1; + continue; + } + if (cur < end && *cur == ']') { + p = cur + 1; + have_mounts = true; + break; + } + amduatd_space_manifest_free(manifest); + return false; + } + } + } else { + if (!amduatd_json_skip_value(&p, end, 0)) { + amduatd_space_manifest_free(manifest); + return false; + } + } + + cur = amduatd_json_skip_ws(p, end); + if (cur < end && *cur == ',') { + p = cur + 1; + continue; + } + if (cur < end && *cur == '}') { + p = cur + 1; + break; + } + amduatd_space_manifest_free(manifest); + return false; + } + + if (!have_version || !have_mounts || version != 1u) { + amduatd_space_manifest_free(manifest); + return false; + } + if (version > UINT32_MAX) { + amduatd_space_manifest_free(manifest); + return false; + } + manifest->version = (uint32_t)version; + + cur = amduatd_json_skip_ws(p, end); + if (cur != end) { + amduatd_space_manifest_free(manifest); + return false; + } + + if (manifest->mounts_len > 1u) { + qsort(manifest->mounts, + manifest->mounts_len, + sizeof(*manifest->mounts), + amduatd_space_manifest_mount_cmp); + } + + return true; +} + +amduatd_space_manifest_status_t amduatd_space_manifest_get( + amduat_asl_store_t *store, + amduat_asl_pointer_store_t *pointer_store, + const amduatd_space_t *effective_space, + amduat_reference_t *out_ref, + amduatd_space_manifest_t *out_manifest) { + amduat_octets_t pointer_name = amduat_octets(NULL, 0u); + amduat_reference_t pointer_ref; + amduat_asl_pointer_error_t perr; + amduat_asl_record_t record; + amduat_asl_store_error_t store_err; + bool exists = false; + + if (out_ref != NULL) { + *out_ref = amduat_reference(0u, amduat_octets(NULL, 0u)); + } + if (out_manifest != NULL) { + memset(out_manifest, 0, sizeof(*out_manifest)); + } + if (store == NULL || pointer_store == NULL || out_manifest == NULL) { + return AMDUATD_SPACE_MANIFEST_ERR_INVALID; + } + + if (!amduatd_space_scope_name(effective_space, + "manifest/head", + &pointer_name)) { + return AMDUATD_SPACE_MANIFEST_ERR_INVALID; + } + + memset(&pointer_ref, 0, sizeof(pointer_ref)); + perr = amduat_asl_pointer_get(pointer_store, + (const char *)pointer_name.data, + &exists, + &pointer_ref); + amduat_octets_free(&pointer_name); + if (perr != AMDUAT_ASL_POINTER_OK) { + return AMDUATD_SPACE_MANIFEST_ERR_STORE; + } + if (!exists) { + return AMDUATD_SPACE_MANIFEST_ERR_NOT_FOUND; + } + + memset(&record, 0, sizeof(record)); + store_err = amduat_asl_record_store_get(store, pointer_ref, &record); + if (store_err != AMDUAT_ASL_STORE_OK) { + amduat_reference_free(&pointer_ref); + return AMDUATD_SPACE_MANIFEST_ERR_STORE; + } + + if (record.schema.len != strlen(AMDUATD_SPACE_MANIFEST_1) || + memcmp(record.schema.data, + AMDUATD_SPACE_MANIFEST_1, + record.schema.len) != 0) { + amduat_asl_record_free(&record); + amduat_reference_free(&pointer_ref); + return AMDUATD_SPACE_MANIFEST_ERR_CODEC; + } + + if (!amduatd_space_manifest_parse(record.payload, out_manifest)) { + amduat_asl_record_free(&record); + amduat_reference_free(&pointer_ref); + return AMDUATD_SPACE_MANIFEST_ERR_CODEC; + } + amduat_asl_record_free(&record); + + if (out_ref != NULL) { + if (!amduat_reference_clone(pointer_ref, out_ref)) { + amduatd_space_manifest_free(out_manifest); + amduat_reference_free(&pointer_ref); + return AMDUATD_SPACE_MANIFEST_ERR_STORE; + } + } + amduat_reference_free(&pointer_ref); + return AMDUATD_SPACE_MANIFEST_OK; +} diff --git a/src/amduatd_space_manifest.h b/src/amduatd_space_manifest.h new file mode 100644 index 0000000..6f4c08e --- /dev/null +++ b/src/amduatd_space_manifest.h @@ -0,0 +1,60 @@ +#ifndef AMDUATD_SPACE_MANIFEST_H +#define AMDUATD_SPACE_MANIFEST_H + +#include "amduat/asl/asl_pointer_fs.h" +#include "amduat/asl/store.h" +#include "amduatd_space.h" + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define AMDUATD_SPACE_MANIFEST_1 "space/manifest_1" + +typedef enum { + AMDUATD_SPACE_MANIFEST_OK = 0, + AMDUATD_SPACE_MANIFEST_ERR_INVALID = 1, + AMDUATD_SPACE_MANIFEST_ERR_NOT_FOUND = 2, + AMDUATD_SPACE_MANIFEST_ERR_STORE = 3, + AMDUATD_SPACE_MANIFEST_ERR_CODEC = 4 +} amduatd_space_manifest_status_t; + +typedef enum { + AMDUATD_SPACE_MANIFEST_MOUNT_PINNED = 0, + AMDUATD_SPACE_MANIFEST_MOUNT_TRACK = 1 +} amduatd_space_manifest_mode_t; + +typedef struct { + char *name; + char *peer_key; + char *space_id; + amduatd_space_manifest_mode_t mode; + bool has_pinned_root_ref; + amduat_reference_t pinned_root_ref; +} amduatd_space_manifest_mount_t; + +typedef struct { + uint32_t version; + amduatd_space_manifest_mount_t *mounts; + size_t mounts_len; + size_t mounts_cap; +} amduatd_space_manifest_t; + +amduatd_space_manifest_status_t amduatd_space_manifest_get( + amduat_asl_store_t *store, + amduat_asl_pointer_store_t *pointer_store, + const amduatd_space_t *effective_space, + amduat_reference_t *out_ref, + amduatd_space_manifest_t *out_manifest); + +void amduatd_space_manifest_free(amduatd_space_manifest_t *manifest); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* AMDUATD_SPACE_MANIFEST_H */ diff --git a/tests/test_amduatd_space_manifest.c b/tests/test_amduatd_space_manifest.c new file mode 100644 index 0000000..e712542 --- /dev/null +++ b/tests/test_amduatd_space_manifest.c @@ -0,0 +1,303 @@ +#ifndef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#endif + +#include "amduatd_space_manifest.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/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-manifest-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_manifest_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; + amduatd_space_manifest_t manifest; + amduat_reference_t ref; + amduatd_space_manifest_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, 0, sizeof(manifest)); + memset(&ref, 0, sizeof(ref)); + status = amduatd_space_manifest_get(&store, + &pointer_store, + &space, + &ref, + &manifest); + expect(status == AMDUATD_SPACE_MANIFEST_ERR_NOT_FOUND, + "missing manifest returns not found"); + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&ref); + free(root); + return failures == 0 ? 0 : 1; +} + +static int amduatd_test_manifest_decode(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_space_manifest_t manifest; + amduat_reference_t fetched_ref; + amduatd_space_manifest_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; + } + + 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); + + memset(&manifest, 0, sizeof(manifest)); + memset(&fetched_ref, 0, sizeof(fetched_ref)); + status = amduatd_space_manifest_get(&store, + &pointer_store, + &space, + &fetched_ref, + &manifest); + expect(status == AMDUATD_SPACE_MANIFEST_OK, "manifest get ok"); + expect(amduat_reference_eq(fetched_ref, record_ref), + "manifest ref matches pointer"); + expect(manifest.version == 1u, "manifest version ok"); + expect(manifest.mounts_len == 3u, "manifest mounts count"); + if (manifest.mounts_len == 3u) { + expect(strcmp(manifest.mounts[0].name, "alpha") == 0, + "mount 0 name"); + expect(strcmp(manifest.mounts[0].peer_key, "peer-1") == 0, + "mount 0 peer"); + expect(strcmp(manifest.mounts[0].space_id, "beta") == 0, + "mount 0 space"); + expect(manifest.mounts[0].mode == AMDUATD_SPACE_MANIFEST_MOUNT_TRACK, + "mount 0 mode"); + + expect(strcmp(manifest.mounts[1].name, "alpha") == 0, + "mount 1 name"); + expect(strcmp(manifest.mounts[1].peer_key, "peer-1") == 0, + "mount 1 peer"); + expect(strcmp(manifest.mounts[1].space_id, "zeta") == 0, + "mount 1 space"); + expect(manifest.mounts[1].mode == AMDUATD_SPACE_MANIFEST_MOUNT_PINNED, + "mount 1 mode"); + expect(manifest.mounts[1].has_pinned_root_ref, + "mount 1 pinned ref present"); + expect(amduat_reference_eq(manifest.mounts[1].pinned_root_ref, pinned_ref), + "mount 1 pinned ref"); + + expect(strcmp(manifest.mounts[2].name, "beta") == 0, + "mount 2 name"); + expect(strcmp(manifest.mounts[2].peer_key, "peer-2") == 0, + "mount 2 peer"); + expect(strcmp(manifest.mounts[2].space_id, "zeta") == 0, + "mount 2 space"); + expect(manifest.mounts[2].mode == AMDUATD_SPACE_MANIFEST_MOUNT_TRACK, + "mount 2 mode"); + } + + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&fetched_ref); + amduat_reference_free(&record_ref); + amduat_reference_free(&pinned_ref); + free(root); + return failures == 0 ? 0 : 1; +} + +int main(void) { + if (amduatd_test_manifest_missing() != 0) { + return 1; + } + if (amduatd_test_manifest_decode() != 0) { + return 1; + } + return failures == 0 ? 0 : 1; +}