diff --git a/README.md b/README.md index 28be980..8160b1d 100644 --- a/README.md +++ b/README.md @@ -259,6 +259,29 @@ curl --unix-socket amduatd.sock \ -H 'X-Amduat-Space: demo' ``` +Create the manifest (only if no head exists yet): + +```sh +curl --unix-socket amduatd.sock \ + -X PUT 'http://localhost/v1/space/manifest' \ + -H 'Content-Type: application/json' \ + -H 'X-Amduat-Space: demo' \ + --data-binary '{"version":1,"mounts":[]}' +``` + +Update with optimistic concurrency: + +```sh +curl --unix-socket amduatd.sock \ + -X PUT 'http://localhost/v1/space/manifest' \ + -H 'Content-Type: application/json' \ + -H 'X-Amduat-Space: demo' \ + -H 'If-Match: ' \ + --data-binary @manifest.json +``` + +`If-Match` can be replaced with `?expected_ref=` if needed. + 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 e191fed..10807ef 100644 --- a/registry/amduatd-api-contract.v1.json +++ b/registry/amduatd-api-contract.v1.json @@ -9,6 +9,7 @@ {"method": "GET", "path": "/v1/space/doctor"}, {"method": "GET", "path": "/v1/space/roots"}, {"method": "GET", "path": "/v1/space/manifest"}, + {"method": "PUT", "path": "/v1/space/manifest"}, {"method": "GET", "path": "/v1/space/sync/status"}, {"method": "POST", "path": "/v1/capabilities"}, {"method": "GET", "path": "/v1/cap/resolve"}, @@ -227,6 +228,17 @@ "manifest_ref": {"type": "string"}, "manifest": {"$ref": "#/schemas/space_manifest"} } + }, + "space_manifest_put_response": { + "type": "object", + "required": ["effective_space", "manifest_ref", "updated", "manifest"], + "properties": { + "effective_space": {"type": "object"}, + "manifest_ref": {"type": "string"}, + "updated": {"type": "boolean"}, + "previous_ref": {"type": "string"}, + "manifest": {"$ref": "#/schemas/space_manifest"} + } } } } diff --git a/src/amduatd.c b/src/amduatd.c index 95bba11..968f7b8 100644 --- a/src/amduatd.c +++ b/src/amduatd.c @@ -115,6 +115,7 @@ static const char *const AMDUATD_DEFAULT_SOCK = "amduatd.sock"; static const uint64_t AMDUATD_FED_TICK_MS = 1000u; static const size_t AMDUATD_FED_INGEST_MAX_BYTES = 8u * 1024u * 1024u; static const size_t AMDUATD_REF_TEXT_MAX = 256u; +static const size_t AMDUATD_SPACE_MANIFEST_MAX_BYTES = 64u * 1024u; static const char k_amduatd_contract_v1_json[] = "{" "\"contract\":\"AMDUATD/API/1\"," @@ -127,6 +128,7 @@ static const char k_amduatd_contract_v1_json[] = "{\"method\":\"GET\",\"path\":\"/v1/space/doctor\"}," "{\"method\":\"GET\",\"path\":\"/v1/space/roots\"}," "{\"method\":\"GET\",\"path\":\"/v1/space/manifest\"}," + "{\"method\":\"PUT\",\"path\":\"/v1/space/manifest\"}," "{\"method\":\"GET\",\"path\":\"/v1/space/sync/status\"}," "{\"method\":\"POST\",\"path\":\"/v1/capabilities\"}," "{\"method\":\"GET\",\"path\":\"/v1/cap/resolve\"}," @@ -319,6 +321,17 @@ static const char k_amduatd_contract_v1_json[] = "\"manifest_ref\":{\"type\":\"string\"}," "\"manifest\":{\"$ref\":\"#/schemas/space_manifest\"}" "}" + "}," + "\"space_manifest_put_response\":{" + "\"type\":\"object\"," + "\"required\":[\"effective_space\",\"manifest_ref\",\"updated\",\"manifest\"]," + "\"properties\":{" + "\"effective_space\":{\"type\":\"object\"}," + "\"manifest_ref\":{\"type\":\"string\"}," + "\"updated\":{\"type\":\"boolean\"}," + "\"previous_ref\":{\"type\":\"string\"}," + "\"manifest\":{\"$ref\":\"#/schemas/space_manifest\"}" + "}" "}" "}" "}\n"; @@ -678,6 +691,47 @@ static bool amduatd_decode_ref_hex_str(const char *s, return ok; } +static bool amduatd_trim_header_value(const char *value, + char *out, + size_t cap) { + const char *start; + const char *end; + size_t len; + + if (out != NULL && cap != 0u) { + out[0] = '\0'; + } + if (value == NULL || out == NULL || cap == 0u) { + return false; + } + + start = value; + while (*start == ' ' || *start == '\t') { + start++; + } + end = start + strlen(start); + while (end > start && (end[-1] == ' ' || end[-1] == '\t')) { + end--; + } + if (end <= start) { + return false; + } + if (*start == '"' && end > start + 1 && end[-1] == '"') { + start++; + end--; + } + if (end <= start) { + return false; + } + len = (size_t)(end - start); + if (len >= cap) { + len = cap - 1u; + } + memcpy(out, start, len); + out[len] = '\0'; + return true; +} + static bool amduatd_parse_type_tag_hex(const char *text, bool *out_has_type_tag, amduat_type_tag_t *out_type_tag) { @@ -1036,6 +1090,65 @@ roots_cleanup: return ok; } +static bool amduatd_send_manifest_conflict( + int fd, + const amduat_reference_t *expected_ref, + const amduat_reference_t *current_ref, + bool current_present) { + amduatd_strbuf_t b; + char *expected_hex = NULL; + char *current_hex = NULL; + bool ok = false; + + memset(&b, 0, sizeof(b)); + + if (expected_ref != NULL) { + if (!amduat_asl_ref_encode_hex(*expected_ref, &expected_hex)) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "encode error"); + } + } + if (current_present && current_ref != NULL) { + if (!amduat_asl_ref_encode_hex(*current_ref, ¤t_hex)) { + free(expected_hex); + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "encode error"); + } + } + + if (!amduatd_strbuf_append_cstr(&b, "{\"error\":\"manifest conflict\"")) { + goto manifest_conflict_cleanup; + } + if (expected_hex != NULL) { + if (!amduatd_strbuf_append_cstr(&b, ",\"expected_ref\":\"") || + !amduatd_strbuf_append_cstr(&b, expected_hex) || + !amduatd_strbuf_append_cstr(&b, "\"")) { + goto manifest_conflict_cleanup; + } + } + if (current_hex != NULL) { + if (!amduatd_strbuf_append_cstr(&b, ",\"current_ref\":\"") || + !amduatd_strbuf_append_cstr(&b, current_hex) || + !amduatd_strbuf_append_cstr(&b, "\"")) { + goto manifest_conflict_cleanup; + } + } + if (!amduatd_strbuf_append_cstr(&b, "}\n")) { + goto manifest_conflict_cleanup; + } + + ok = amduatd_http_send_json(fd, 409, "Conflict", b.data, false); + +manifest_conflict_cleanup: + amduatd_strbuf_free(&b); + free(expected_hex); + free(current_hex); + if (!ok) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", "error"); + } + return ok; +} + static bool amduatd_handle_get_space_manifest( int fd, amduat_asl_store_t *store, @@ -1194,6 +1307,243 @@ manifest_cleanup: return ok; } +static bool amduatd_handle_put_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; + amduatd_space_manifest_t manifest; + amduat_reference_t new_ref; + amduat_reference_t expected_ref; + amduatd_space_manifest_status_t status; + amduatd_strbuf_t b; + uint8_t *body = NULL; + char expected_param[AMDUATD_REF_TEXT_MAX]; + char if_match_value[AMDUATD_REF_TEXT_MAX]; + const char *expected_text = NULL; + bool have_expected = false; + char *manifest_ref_hex = NULL; + char *previous_ref_hex = NULL; + char *manifest_json = NULL; + size_t manifest_json_len = 0u; + bool ok = false; + + memset(&manifest, 0, sizeof(manifest)); + memset(&new_ref, 0, sizeof(new_ref)); + memset(&expected_ref, 0, sizeof(expected_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 (req->content_length == 0u) { + return amduatd_send_json_error(fd, 400, "Bad Request", "missing body"); + } + if (req->content_length > AMDUATD_SPACE_MANIFEST_MAX_BYTES) { + return amduatd_send_json_error(fd, 413, "Payload Too Large", + "payload too large"); + } + + body = (uint8_t *)malloc(req->content_length); + if (body == NULL) { + return amduatd_send_json_error(fd, 500, "Internal Server Error", "oom"); + } + if (!amduatd_read_exact(fd, body, req->content_length)) { + free(body); + return false; + } + + if (req->if_match[0] != '\0') { + if (!amduatd_trim_header_value(req->if_match, + if_match_value, + sizeof(if_match_value)) || + if_match_value[0] == '\0' || + strcmp(if_match_value, "*") == 0) { + free(body); + return amduatd_send_json_error(fd, 400, "Bad Request", + "invalid If-Match"); + } + expected_text = if_match_value; + } + if (amduatd_query_param(req->path, + "expected_ref", + expected_param, + sizeof(expected_param)) != NULL && + expected_param[0] != '\0') { + if (expected_text != NULL && + strcmp(expected_text, expected_param) != 0) { + free(body); + return amduatd_send_json_error(fd, 400, "Bad Request", + "conflicting expected_ref"); + } + expected_text = expected_param; + } + if (expected_text != NULL) { + if (!amduat_asl_ref_decode_hex(expected_text, &expected_ref)) { + free(body); + return amduatd_send_json_error(fd, 400, "Bad Request", + "invalid expected_ref"); + } + have_expected = true; + } + + if (!amduat_asl_pointer_store_init(&pointer_store, root_path)) { + free(body); + if (have_expected) { + amduat_reference_free(&expected_ref); + } + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "pointer store error"); + } + + status = amduatd_space_manifest_put( + store, + &pointer_store, + req->effective_space, + amduat_octets(body, req->content_length), + have_expected ? &expected_ref : NULL, + &new_ref, + &manifest); + free(body); + body = NULL; + + if (status == AMDUATD_SPACE_MANIFEST_ERR_CODEC) { + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&new_ref); + if (have_expected) { + amduat_reference_free(&expected_ref); + } + return amduatd_send_json_error(fd, 400, "Bad Request", + "invalid manifest"); + } + if (status == AMDUATD_SPACE_MANIFEST_ERR_CONFLICT) { + amduat_octets_t pointer_name = amduat_octets(NULL, 0u); + amduat_reference_t current_ref; + bool current_present = false; + amduat_asl_pointer_error_t perr; + + memset(¤t_ref, 0, sizeof(current_ref)); + if (amduatd_space_scope_name(req->effective_space, + "manifest/head", + &pointer_name)) { + perr = amduat_asl_pointer_get(&pointer_store, + (const char *)pointer_name.data, + ¤t_present, + ¤t_ref); + amduat_octets_free(&pointer_name); + if (perr != AMDUAT_ASL_POINTER_OK) { + current_present = false; + } + } + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&new_ref); + ok = amduatd_send_manifest_conflict( + fd, + have_expected ? &expected_ref : NULL, + current_present ? ¤t_ref : NULL, + current_present); + if (current_present) { + amduat_reference_free(¤t_ref); + } + if (have_expected) { + amduat_reference_free(&expected_ref); + } + return ok; + } + if (status != AMDUATD_SPACE_MANIFEST_OK) { + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&new_ref); + if (have_expected) { + amduat_reference_free(&expected_ref); + } + return amduatd_send_json_error(fd, 500, "Internal Server Error", + "manifest update failed"); + } + + if (!amduat_asl_ref_encode_hex(new_ref, &manifest_ref_hex)) { + goto manifest_put_cleanup; + } + if (have_expected && + !amduat_asl_ref_encode_hex(expected_ref, &previous_ref_hex)) { + goto manifest_put_cleanup; + } + if (!amduatd_space_manifest_encode_json(&manifest, + &manifest_json, + &manifest_json_len)) { + goto manifest_put_cleanup; + } + + if (!amduatd_strbuf_append_cstr(&b, "{\"effective_space\":{")) { + goto manifest_put_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_put_cleanup; + } + } else { + if (!amduatd_strbuf_append_cstr(&b, "\"mode\":\"unscoped\",") || + !amduatd_strbuf_append_cstr(&b, "\"space_id\":null")) { + goto manifest_put_cleanup; + } + } + if (!amduatd_strbuf_append_cstr(&b, "},\"manifest_ref\":\"") || + !amduatd_strbuf_append_cstr(&b, manifest_ref_hex) || + !amduatd_strbuf_append_cstr(&b, "\",\"updated\":true")) { + goto manifest_put_cleanup; + } + if (previous_ref_hex != NULL) { + if (!amduatd_strbuf_append_cstr(&b, ",\"previous_ref\":\"") || + !amduatd_strbuf_append_cstr(&b, previous_ref_hex) || + !amduatd_strbuf_append_cstr(&b, "\"")) { + goto manifest_put_cleanup; + } + } + if (!amduatd_strbuf_append_cstr(&b, ",\"manifest\":") || + !amduatd_strbuf_append(&b, manifest_json, manifest_json_len) || + !amduatd_strbuf_append_cstr(&b, "}\n")) { + goto manifest_put_cleanup; + } + + ok = amduatd_http_send_json(fd, 200, "OK", b.data, false); + +manifest_put_cleanup: + if (!ok) { + ok = amduatd_send_json_error(fd, 500, "Internal Server Error", "error"); + } + amduatd_strbuf_free(&b); + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&new_ref); + if (have_expected) { + amduat_reference_free(&expected_ref); + } + free(manifest_ref_hex); + free(previous_ref_hex); + free(manifest_json); + return ok; +} + static bool amduatd_sync_status_append_cursor( amduatd_strbuf_t *b, amduat_asl_store_t *store, @@ -7812,6 +8162,16 @@ static bool amduatd_handle_conn(int fd, 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, + 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 5205416..d4d1fbf 100644 --- a/src/amduatd_http.c +++ b/src/amduatd_http.c @@ -243,6 +243,12 @@ bool amduatd_http_parse_request(int fd, amduatd_http_req_t *out_req) { v++; } strncpy(out_req->accept, v, sizeof(out_req->accept) - 1); + } else if (strncasecmp(line, "If-Match:", 9) == 0) { + const char *v = line + 9; + while (*v == ' ' || *v == '\t') { + v++; + } + strncpy(out_req->if_match, v, sizeof(out_req->if_match) - 1); } else if (strncasecmp(line, "X-Amduat-Type-Tag:", 18) == 0) { const char *v = line + 18; while (*v == ' ' || *v == '\t') { diff --git a/src/amduatd_http.h b/src/amduatd_http.h index 5466e64..a67c982 100644 --- a/src/amduatd_http.h +++ b/src/amduatd_http.h @@ -18,6 +18,7 @@ typedef struct { char path[1024]; char content_type[128]; char accept[128]; + char if_match[256]; char x_type_tag[64]; char x_capability[2048]; char x_space[AMDUAT_ASL_POINTER_NAME_MAX + 1u]; diff --git a/src/amduatd_space_manifest.c b/src/amduatd_space_manifest.c index c009ab3..ef6b438 100644 --- a/src/amduatd_space_manifest.c +++ b/src/amduatd_space_manifest.c @@ -5,10 +5,92 @@ #include "amduatd_http.h" #include +#include #include #include #include +typedef struct { + char *data; + size_t len; + size_t cap; +} amduatd_manifest_buf_t; + +static void amduatd_manifest_buf_free(amduatd_manifest_buf_t *b) { + if (b == NULL) { + return; + } + free(b->data); + b->data = NULL; + b->len = 0; + b->cap = 0; +} + +static bool amduatd_manifest_buf_reserve(amduatd_manifest_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_manifest_buf_append(amduatd_manifest_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_manifest_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_manifest_buf_append_cstr(amduatd_manifest_buf_t *b, + const char *s) { + return amduatd_manifest_buf_append( + b, s != NULL ? s : "", s != NULL ? strlen(s) : 0u); +} + +static bool amduatd_manifest_buf_append_char(amduatd_manifest_buf_t *b, + char c) { + return amduatd_manifest_buf_append(b, &c, 1u); +} + static bool amduatd_space_manifest_decode_ref(const char *s, size_t len, amduat_reference_t *out_ref) { @@ -271,6 +353,96 @@ static bool amduatd_space_manifest_parse_mount( return true; } +bool amduatd_space_manifest_encode_json(const amduatd_space_manifest_t *manifest, + char **out_json, + size_t *out_len) { + amduatd_manifest_buf_t b; + + if (out_json != NULL) { + *out_json = NULL; + } + if (out_len != NULL) { + *out_len = 0u; + } + if (manifest == NULL || out_json == NULL || out_len == NULL) { + return false; + } + + memset(&b, 0, sizeof(b)); + + if (!amduatd_manifest_buf_append_cstr(&b, "{\"version\":")) { + amduatd_manifest_buf_free(&b); + return false; + } + { + char tmp[32]; + int n = snprintf(tmp, sizeof(tmp), "%u", manifest->version); + if (n <= 0 || (size_t)n >= sizeof(tmp)) { + amduatd_manifest_buf_free(&b); + return false; + } + if (!amduatd_manifest_buf_append_cstr(&b, tmp) || + !amduatd_manifest_buf_append_cstr(&b, ",\"mounts\":[")) { + amduatd_manifest_buf_free(&b); + return false; + } + } + + 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_manifest_buf_append_char(&b, ',')) { + amduatd_manifest_buf_free(&b); + return false; + } + } + if (!amduatd_manifest_buf_append_cstr(&b, "{\"name\":\"") || + !amduatd_manifest_buf_append_cstr(&b, mount->name) || + !amduatd_manifest_buf_append_cstr(&b, "\",\"peer_key\":\"") || + !amduatd_manifest_buf_append_cstr(&b, mount->peer_key) || + !amduatd_manifest_buf_append_cstr(&b, "\",\"space_id\":\"") || + !amduatd_manifest_buf_append_cstr(&b, mount->space_id) || + !amduatd_manifest_buf_append_cstr(&b, "\",\"mode\":\"") || + !amduatd_manifest_buf_append_cstr( + &b, + mount->mode == AMDUATD_SPACE_MANIFEST_MOUNT_PINNED ? "pinned" + : "track") || + !amduatd_manifest_buf_append_cstr(&b, "\"")) { + amduatd_manifest_buf_free(&b); + return false; + } + if (mount->mode == AMDUATD_SPACE_MANIFEST_MOUNT_PINNED) { + if (!amduat_asl_ref_encode_hex(mount->pinned_root_ref, &root_ref_hex)) { + amduatd_manifest_buf_free(&b); + return false; + } + if (!amduatd_manifest_buf_append_cstr(&b, ",\"pinned_root_ref\":\"") || + !amduatd_manifest_buf_append_cstr(&b, root_ref_hex) || + !amduatd_manifest_buf_append_cstr(&b, "\"")) { + free(root_ref_hex); + amduatd_manifest_buf_free(&b); + return false; + } + free(root_ref_hex); + } + if (!amduatd_manifest_buf_append_cstr(&b, "}")) { + amduatd_manifest_buf_free(&b); + return false; + } + } + + if (!amduatd_manifest_buf_append_cstr(&b, "]}")) { + amduatd_manifest_buf_free(&b); + return false; + } + + *out_json = b.data; + *out_len = b.len; + return true; +} + static bool amduatd_space_manifest_parse(amduat_octets_t payload, amduatd_space_manifest_t *manifest) { const char *p = NULL; @@ -397,6 +569,99 @@ static bool amduatd_space_manifest_parse(amduat_octets_t payload, return true; } +amduatd_space_manifest_status_t amduatd_space_manifest_put( + amduat_asl_store_t *store, + amduat_asl_pointer_store_t *pointer_store, + const amduatd_space_t *effective_space, + amduat_octets_t payload, + const amduat_reference_t *expected_ref, + amduat_reference_t *out_new_ref, + amduatd_space_manifest_t *out_manifest) { + amduat_octets_t pointer_name = amduat_octets(NULL, 0u); + amduat_reference_t record_ref; + amduat_asl_store_error_t store_err; + amduat_asl_pointer_error_t perr; + bool swapped = false; + char *encoded = NULL; + size_t encoded_len = 0u; + amduatd_space_manifest_t local_manifest; + amduatd_space_manifest_t *manifest = out_manifest != NULL ? out_manifest + : &local_manifest; + + if (out_new_ref != NULL) { + *out_new_ref = amduat_reference(0u, amduat_octets(NULL, 0u)); + } + if (manifest != NULL) { + memset(manifest, 0, sizeof(*manifest)); + } + if (store == NULL || pointer_store == NULL || manifest == NULL) { + return AMDUATD_SPACE_MANIFEST_ERR_INVALID; + } + + if (!amduatd_space_manifest_parse(payload, manifest)) { + amduatd_space_manifest_free(manifest); + return AMDUATD_SPACE_MANIFEST_ERR_CODEC; + } + + if (!amduatd_space_manifest_encode_json(manifest, &encoded, &encoded_len)) { + amduatd_space_manifest_free(manifest); + return AMDUATD_SPACE_MANIFEST_ERR_CODEC; + } + + memset(&record_ref, 0, sizeof(record_ref)); + store_err = amduat_asl_record_store_put( + store, + amduat_octets(AMDUATD_SPACE_MANIFEST_1, + strlen(AMDUATD_SPACE_MANIFEST_1)), + amduat_octets((const uint8_t *)encoded, encoded_len), + &record_ref); + free(encoded); + encoded = NULL; + if (store_err != AMDUAT_ASL_STORE_OK) { + amduatd_space_manifest_free(manifest); + return AMDUATD_SPACE_MANIFEST_ERR_STORE; + } + + if (!amduatd_space_scope_name(effective_space, + "manifest/head", + &pointer_name)) { + amduat_reference_free(&record_ref); + amduatd_space_manifest_free(manifest); + return AMDUATD_SPACE_MANIFEST_ERR_INVALID; + } + + perr = amduat_asl_pointer_cas(pointer_store, + (const char *)pointer_name.data, + expected_ref != NULL, + expected_ref, + &record_ref, + &swapped); + amduat_octets_free(&pointer_name); + if (perr != AMDUAT_ASL_POINTER_OK) { + amduat_reference_free(&record_ref); + amduatd_space_manifest_free(manifest); + return AMDUATD_SPACE_MANIFEST_ERR_STORE; + } + if (!swapped) { + amduat_reference_free(&record_ref); + amduatd_space_manifest_free(manifest); + return AMDUATD_SPACE_MANIFEST_ERR_CONFLICT; + } + + if (out_new_ref != NULL) { + if (!amduat_reference_clone(record_ref, out_new_ref)) { + amduat_reference_free(&record_ref); + amduatd_space_manifest_free(manifest); + return AMDUATD_SPACE_MANIFEST_ERR_STORE; + } + } + amduat_reference_free(&record_ref); + if (out_manifest == NULL) { + amduatd_space_manifest_free(manifest); + } + return AMDUATD_SPACE_MANIFEST_OK; +} + amduatd_space_manifest_status_t amduatd_space_manifest_get( amduat_asl_store_t *store, amduat_asl_pointer_store_t *pointer_store, diff --git a/src/amduatd_space_manifest.h b/src/amduatd_space_manifest.h index 6f4c08e..6135a20 100644 --- a/src/amduatd_space_manifest.h +++ b/src/amduatd_space_manifest.h @@ -20,7 +20,8 @@ typedef enum { 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_ERR_CODEC = 4, + AMDUATD_SPACE_MANIFEST_ERR_CONFLICT = 5 } amduatd_space_manifest_status_t; typedef enum { @@ -51,6 +52,19 @@ amduatd_space_manifest_status_t amduatd_space_manifest_get( amduat_reference_t *out_ref, amduatd_space_manifest_t *out_manifest); +amduatd_space_manifest_status_t amduatd_space_manifest_put( + amduat_asl_store_t *store, + amduat_asl_pointer_store_t *pointer_store, + const amduatd_space_t *effective_space, + amduat_octets_t payload, + const amduat_reference_t *expected_ref, + amduat_reference_t *out_new_ref, + amduatd_space_manifest_t *out_manifest); + +bool amduatd_space_manifest_encode_json(const amduatd_space_manifest_t *manifest, + char **out_json, + size_t *out_len); + void amduatd_space_manifest_free(amduatd_space_manifest_t *manifest); #ifdef __cplusplus diff --git a/tests/test_amduatd_space_manifest.c b/tests/test_amduatd_space_manifest.c index e712542..24b8b16 100644 --- a/tests/test_amduatd_space_manifest.c +++ b/tests/test_amduatd_space_manifest.c @@ -292,6 +292,212 @@ static int amduatd_test_manifest_decode(void) { return failures == 0 ? 0 : 1; } +static int amduatd_test_manifest_put(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]; + amduatd_space_manifest_t manifest; + amduat_reference_t first_ref; + amduat_reference_t second_ref; + amduat_reference_t wrong_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(0x22, &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); + + memset(&manifest, 0, sizeof(manifest)); + memset(&first_ref, 0, sizeof(first_ref)); + status = amduatd_space_manifest_put(&store, + &pointer_store, + &space, + amduat_octets((const uint8_t *)payload, + strlen(payload)), + NULL, + &first_ref, + &manifest); + expect(status == AMDUATD_SPACE_MANIFEST_OK, "manifest put ok"); + if (status == AMDUATD_SPACE_MANIFEST_OK) { + expect(manifest.mounts_len == 3u, "manifest put mounts count"); + if (manifest.mounts_len == 3u) { + expect(strcmp(manifest.mounts[0].name, "alpha") == 0, + "put mount 0 name"); + expect(strcmp(manifest.mounts[0].peer_key, "peer-1") == 0, + "put mount 0 peer"); + expect(strcmp(manifest.mounts[0].space_id, "beta") == 0, + "put mount 0 space"); + expect(manifest.mounts[0].mode == AMDUATD_SPACE_MANIFEST_MOUNT_TRACK, + "put mount 0 mode"); + expect(strcmp(manifest.mounts[1].name, "alpha") == 0, + "put mount 1 name"); + expect(strcmp(manifest.mounts[1].peer_key, "peer-1") == 0, + "put mount 1 peer"); + expect(strcmp(manifest.mounts[1].space_id, "zeta") == 0, + "put mount 1 space"); + expect(manifest.mounts[1].mode == AMDUATD_SPACE_MANIFEST_MOUNT_PINNED, + "put mount 1 mode"); + expect(manifest.mounts[1].has_pinned_root_ref, + "put mount 1 pinned ref"); + expect(strcmp(manifest.mounts[2].name, "beta") == 0, + "put mount 2 name"); + expect(strcmp(manifest.mounts[2].peer_key, "peer-2") == 0, + "put mount 2 peer"); + expect(strcmp(manifest.mounts[2].space_id, "zeta") == 0, + "put mount 2 space"); + expect(manifest.mounts[2].mode == AMDUATD_SPACE_MANIFEST_MOUNT_TRACK, + "put mount 2 mode"); + } + } + amduatd_space_manifest_free(&manifest); + + memset(&manifest, 0, sizeof(manifest)); + memset(&second_ref, 0, sizeof(second_ref)); + status = amduatd_space_manifest_get(&store, + &pointer_store, + &space, + &second_ref, + &manifest); + expect(status == AMDUATD_SPACE_MANIFEST_OK, "manifest get after put ok"); + expect(amduat_reference_eq(second_ref, first_ref), + "manifest get ref matches put"); + if (manifest.mounts_len == 3u) { + expect(strcmp(manifest.mounts[0].name, "alpha") == 0, + "get mount 0 name"); + expect(strcmp(manifest.mounts[1].name, "alpha") == 0, + "get mount 1 name"); + expect(strcmp(manifest.mounts[2].name, "beta") == 0, + "get mount 2 name"); + } + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&second_ref); + + memset(&manifest, 0, sizeof(manifest)); + memset(&second_ref, 0, sizeof(second_ref)); + status = amduatd_space_manifest_put(&store, + &pointer_store, + &space, + amduat_octets((const uint8_t *)payload, + strlen(payload)), + NULL, + &second_ref, + &manifest); + expect(status == AMDUATD_SPACE_MANIFEST_ERR_CONFLICT, + "manifest put conflict without If-Match"); + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&second_ref); + + memset(&manifest, 0, sizeof(manifest)); + memset(&second_ref, 0, sizeof(second_ref)); + status = amduatd_space_manifest_put(&store, + &pointer_store, + &space, + amduat_octets((const uint8_t *)payload, + strlen(payload)), + &first_ref, + &second_ref, + &manifest); + expect(status == AMDUATD_SPACE_MANIFEST_OK, + "manifest put ok with If-Match"); + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&second_ref); + + if (!amduatd_make_test_ref(0x99, &wrong_ref)) { + fprintf(stderr, "failed to make wrong ref\n"); + amduat_reference_free(&first_ref); + amduat_reference_free(&pinned_ref); + free(root); + return 1; + } + memset(&manifest, 0, sizeof(manifest)); + status = amduatd_space_manifest_put(&store, + &pointer_store, + &space, + amduat_octets((const uint8_t *)payload, + strlen(payload)), + &wrong_ref, + &second_ref, + &manifest); + expect(status == AMDUATD_SPACE_MANIFEST_ERR_CONFLICT, + "manifest put conflict with wrong If-Match"); + amduatd_space_manifest_free(&manifest); + amduat_reference_free(&wrong_ref); + + amduat_reference_free(&first_ref); + amduat_reference_free(&pinned_ref); + free(root); + return failures == 0 ? 0 : 1; +} + int main(void) { if (amduatd_test_manifest_missing() != 0) { return 1; @@ -299,5 +505,8 @@ int main(void) { if (amduatd_test_manifest_decode() != 0) { return 1; } + if (amduatd_test_manifest_put() != 0) { + return 1; + } return failures == 0 ? 0 : 1; }