Add space manifest CAS head and read-only /v1/space/manifest endpoint

This commit is contained in:
Carl Niklas Rydberg 2026-01-24 18:42:01 +01:00
parent f99ec3ee89
commit 704ea18a32
9 changed files with 1105 additions and 3 deletions

View file

@ -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

View file

@ -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/<space_id>/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

View file

@ -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"}
}
}
}
}

View file

@ -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."}

View file

@ -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 <errno.h>
#include <signal.h>
@ -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,

View file

@ -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;

View file

@ -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 <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
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;
}

View file

@ -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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#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 */

View file

@ -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 <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}