From 7e3e30298827847781d31dc48a659d8d2ff4c69d Mon Sep 17 00:00:00 2001 From: Carl Niklas Rydberg Date: Sat, 21 Feb 2026 18:04:15 +0100 Subject: [PATCH] Add Minio adapter. --- CMakeLists.txt | 11 + docs/minio-adapter.md | 67 + include/amduat/asl/asl_store_minio.h | 67 + .../asl_store_minio/asl_store_minio.c | 1316 +++++++++++++ src/adapters/asl_store_minio/s3_sigv4.c | 288 +++ src/adapters/asl_store_minio/s3_sigv4.h | 25 + src/tools/amduat_conformance_run.c | 8 +- .../asl_store/asl_store_conformance.c | 410 +++- .../asl_store/test_asl_store_conformance.c | 2 +- tests/conformance/conformance_result.h | 6 + .../tgk_store/test_tgk_store_conformance.c | 2 +- .../tgk_store/tgk_store_conformance.c | 1686 ++++++++++++++++- .../conformance/tgk_edges/manifest.json | 26 + 13 files changed, 3826 insertions(+), 88 deletions(-) create mode 100644 docs/minio-adapter.md create mode 100644 include/amduat/asl/asl_store_minio.h create mode 100644 src/adapters/asl_store_minio/asl_store_minio.c create mode 100644 src/adapters/asl_store_minio/s3_sigv4.c create mode 100644 src/adapters/asl_store_minio/s3_sigv4.h create mode 100644 tests/fixtures/conformance/tgk_edges/manifest.json diff --git a/CMakeLists.txt b/CMakeLists.txt index f6753b0..ff87da7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -157,6 +157,11 @@ set(AMDUAT_ASL_STORE_INDEX_FS_SRCS src/adapters/asl_store_index_fs/asl_store_index_fs_layout.c ) +set(AMDUAT_ASL_STORE_MINIO_SRCS + src/adapters/asl_store_minio/asl_store_minio.c + src/adapters/asl_store_minio/s3_sigv4.c +) + set(AMDUAT_ASL_DERIVATION_INDEX_FS_SRCS src/adapters/asl_derivation_index_fs/asl_derivation_index_fs.c ) @@ -213,6 +218,10 @@ amduat_add_lib(asl_store_index_fs SRCS ${AMDUAT_ASL_STORE_INDEX_FS_SRCS}) amduat_link(asl_store_index_fs amduat_asl amduat_enc amduat_hash_asl1 amduat_util) target_compile_definitions(amduat_asl_store_index_fs_obj PRIVATE _POSIX_C_SOURCE=200809L) +amduat_add_lib(asl_store_minio SRCS ${AMDUAT_ASL_STORE_MINIO_SRCS}) +amduat_link(asl_store_minio amduat_asl amduat_enc amduat_hash_asl1 amduat_util) +target_compile_definitions(amduat_asl_store_minio_obj PRIVATE _POSIX_C_SOURCE=200809L) + amduat_add_lib(asl_derivation_index_fs SRCS ${AMDUAT_ASL_DERIVATION_INDEX_FS_SRCS}) amduat_link(asl_derivation_index_fs amduat_asl amduat_enc amduat_hash_asl1 amduat_util) target_compile_definitions(amduat_asl_derivation_index_fs_obj PRIVATE _POSIX_C_SOURCE=200809L) @@ -318,6 +327,7 @@ target_include_directories(amduat_conformance_run ) target_link_libraries(amduat_conformance_run PRIVATE amduat_asl_store_fs amduat_asl_store_index_fs + amduat_asl_store_minio amduat_tgk_store_mem amduat_tgk_store_fs amduat_tgk_store_asl_index_fs amduat_tgk amduat_asl amduat_enc amduat_hash_asl1 amduat_util amduat_format @@ -598,6 +608,7 @@ target_compile_definitions(amduat_test_asl_store_conformance ) target_link_libraries(amduat_test_asl_store_conformance PRIVATE amduat_asl_store_fs amduat_asl_store_index_fs + amduat_asl_store_minio amduat_asl amduat_enc amduat_hash_asl1 amduat_util ) add_test(NAME asl_store_conformance diff --git a/docs/minio-adapter.md b/docs/minio-adapter.md new file mode 100644 index 0000000..0bdd186 --- /dev/null +++ b/docs/minio-adapter.md @@ -0,0 +1,67 @@ +# MinIO ASL Store Adapter + +This adapter provides an S3-compatible `ASL/1-STORE` backend for MinIO. It is +opt-in for conformance runs and does not require MinIO or any external services +at build time. + +## Required environment + +These are required when running conformance against the MinIO backend: + +- `AMDUAT_MINIO_ENDPOINT` (e.g. `http://localhost:9000`) +- `AMDUAT_MINIO_BUCKET` (e.g. `amduat`) +- `AMDUAT_MINIO_ACCESS_KEY` +- `AMDUAT_MINIO_SECRET_KEY` + +Optional configuration: + +- `AMDUAT_MINIO_REGION` (default: `us-east-1`) +- `AMDUAT_MINIO_PREFIX` (default: `asl`) +- `AMDUAT_MINIO_FORCE_PATH_STYLE` (default: `1`) +- `AMDUAT_MINIO_TLS_VERIFY` (default: `1`) +- `AMDUAT_MINIO_TIMEOUT_MS` (default: `30000`) +- `AMDUAT_MINIO_RETRY_MAX` (default: `3`) + +## Conformance runs + +MinIO is skipped unless explicitly enabled. + +Example (opt-in conformance run): + +```sh +AMDUAT_CONFORMANCE_ENABLE_MINIO=1 \ +AMDUAT_MINIO_ENDPOINT=http://localhost:9000 \ +AMDUAT_MINIO_BUCKET=amduat \ +AMDUAT_MINIO_ACCESS_KEY=minioadmin \ +AMDUAT_MINIO_SECRET_KEY=minioadmin \ +./build/amduat_conformance_run --suite asl --backend minio --workdir /tmp/amduat_conformance +``` + +If required environment variables are missing, the backend is reported as +`SKIPPED` with the missing variable names. + +## Key layout + +Objects are stored under a sharded key derived from the canonical reference +hex: + +``` +/// +``` + +## Error mapping + +| HTTP/S3 response | Canonical taxonomy | Store error | +| --- | --- | --- | +| 404 | NotFound | `AMDUAT_ASL_STORE_ERR_NOT_FOUND` | +| 401, 403 | PermissionDenied | `AMDUAT_ASL_STORE_ERR_IO` | +| 408, 429 | TransientIO | `AMDUAT_ASL_STORE_ERR_IO` | +| 5xx | TransientIO | `AMDUAT_ASL_STORE_ERR_IO` | +| 4xx (other) | InvalidArtifact | `AMDUAT_ASL_STORE_ERR_INTEGRITY` | +| Network/TLS/parse errors | InternalError/TransientIO | `AMDUAT_ASL_STORE_ERR_IO` | + +## Limitations + +- HTTPS is not supported yet (HTTP-only). Use `http://` endpoints for now. +- Multipart uploads, streaming artifacts, and very large objects are not + implemented in Phase 0. diff --git a/include/amduat/asl/asl_store_minio.h b/include/amduat/asl/asl_store_minio.h new file mode 100644 index 0000000..42e2bd3 --- /dev/null +++ b/include/amduat/asl/asl_store_minio.h @@ -0,0 +1,67 @@ +#ifndef AMDUAT_ASL_STORE_MINIO_H +#define AMDUAT_ASL_STORE_MINIO_H + +#include "amduat/asl/store.h" + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum { + AMDUAT_ASL_STORE_MINIO_ENDPOINT_MAX = 1024, + AMDUAT_ASL_STORE_MINIO_HOST_MAX = 256, + AMDUAT_ASL_STORE_MINIO_BUCKET_MAX = 256, + AMDUAT_ASL_STORE_MINIO_ACCESS_KEY_MAX = 256, + AMDUAT_ASL_STORE_MINIO_SECRET_KEY_MAX = 256, + AMDUAT_ASL_STORE_MINIO_REGION_MAX = 64, + AMDUAT_ASL_STORE_MINIO_PREFIX_MAX = 128 +}; + +typedef struct { + amduat_asl_store_config_t store_config; + const char *endpoint; + const char *bucket; + const char *access_key; + const char *secret_key; + const char *region; + const char *prefix; + bool force_path_style; + bool tls_verify; + uint32_t timeout_ms; + uint32_t retry_max; +} amduat_asl_store_minio_config_t; + +typedef struct { + amduat_asl_store_config_t store_config; + char endpoint[AMDUAT_ASL_STORE_MINIO_ENDPOINT_MAX]; + char host[AMDUAT_ASL_STORE_MINIO_HOST_MAX]; + uint16_t port; + bool use_tls; + char bucket[AMDUAT_ASL_STORE_MINIO_BUCKET_MAX]; + char access_key[AMDUAT_ASL_STORE_MINIO_ACCESS_KEY_MAX]; + char secret_key[AMDUAT_ASL_STORE_MINIO_SECRET_KEY_MAX]; + char region[AMDUAT_ASL_STORE_MINIO_REGION_MAX]; + char prefix[AMDUAT_ASL_STORE_MINIO_PREFIX_MAX]; + bool force_path_style; + bool tls_verify; + uint32_t timeout_ms; + uint32_t retry_max; +} amduat_asl_store_minio_t; + +void amduat_asl_store_minio_config_defaults( + amduat_asl_store_minio_config_t *config); + +bool amduat_asl_store_minio_init(amduat_asl_store_minio_t *minio, + amduat_asl_store_minio_config_t config); + +amduat_asl_store_ops_t amduat_asl_store_minio_ops(void); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* AMDUAT_ASL_STORE_MINIO_H */ diff --git a/src/adapters/asl_store_minio/asl_store_minio.c b/src/adapters/asl_store_minio/asl_store_minio.c new file mode 100644 index 0000000..189780f --- /dev/null +++ b/src/adapters/asl_store_minio/asl_store_minio.c @@ -0,0 +1,1316 @@ +#include "amduat/asl/asl_store_minio.h" + +#include "s3_sigv4.h" + +#include "amduat/asl/core.h" +#include "amduat/asl/ref_derive.h" +#include "amduat/asl/ref_text.h" +#include "amduat/enc/asl1_core.h" +#include "amduat/enc/asl1_core_codec.h" +#include "amduat/hash/asl1.h" +#include "amduat/util/hex.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum { + AMDUAT_ASL_STORE_MINIO_MIN_DIGEST_BYTES = 2, + AMDUAT_ASL_STORE_MINIO_DEFAULT_TIMEOUT_MS = 30000u, + AMDUAT_ASL_STORE_MINIO_DEFAULT_RETRY_MAX = 3u +}; + +typedef enum { + AMDUAT_MINIO_ERR_OK = 0, + AMDUAT_MINIO_ERR_NOT_FOUND, + AMDUAT_MINIO_ERR_PERMISSION, + AMDUAT_MINIO_ERR_TRANSIENT, + AMDUAT_MINIO_ERR_INVALID, + AMDUAT_MINIO_ERR_INTERNAL +} amduat_minio_error_t; + +typedef struct { + int status; + char *body; + size_t body_len; +} amduat_http_response_t; + +static bool amduat_asl_store_minio_copy_string(char *dst, + size_t dst_len, + const char *src) { + size_t len; + + if (dst == NULL || dst_len == 0u) { + return false; + } + dst[0] = '\0'; + if (src == NULL) { + return false; + } + len = strlen(src); + if (len == 0u || len >= dst_len) { + return false; + } + memcpy(dst, src, len); + dst[len] = '\0'; + return true; +} + +static bool amduat_asl_store_minio_parse_endpoint(const char *endpoint, + char *out_host, + size_t host_len, + uint16_t *out_port, + bool *out_tls) { + const char *scheme; + const char *host_start; + const char *host_end; + const char *port_start; + char host_buf[AMDUAT_ASL_STORE_MINIO_HOST_MAX]; + size_t host_size; + uint16_t port = 0u; + bool tls = false; + + if (endpoint == NULL || out_host == NULL || out_port == NULL || + out_tls == NULL) { + return false; + } + + if (strncmp(endpoint, "http://", 7u) == 0) { + scheme = "http://"; + tls = false; + } else if (strncmp(endpoint, "https://", 8u) == 0) { + scheme = "https://"; + tls = true; + } else { + return false; + } + + host_start = endpoint + strlen(scheme); + if (*host_start == '\0') { + return false; + } + + host_end = strchr(host_start, '/'); + if (host_end != NULL && host_end[1] != '\0') { + return false; + } + if (host_end == NULL) { + host_end = host_start + strlen(host_start); + } + + port_start = memchr(host_start, ':', (size_t)(host_end - host_start)); + if (port_start != NULL) { + char *endptr = NULL; + long port_val; + size_t host_part_len = (size_t)(port_start - host_start); + if (host_part_len == 0u || host_part_len >= sizeof(host_buf)) { + return false; + } + memcpy(host_buf, host_start, host_part_len); + host_buf[host_part_len] = '\0'; + port_start++; + if (port_start >= host_end) { + return false; + } + port_val = strtol(port_start, &endptr, 10); + if (endptr == NULL || endptr != host_end || port_val <= 0 || port_val > 65535) { + return false; + } + port = (uint16_t)port_val; + } else { + size_t host_part_len = (size_t)(host_end - host_start); + if (host_part_len == 0u || host_part_len >= sizeof(host_buf)) { + return false; + } + memcpy(host_buf, host_start, host_part_len); + host_buf[host_part_len] = '\0'; + port = tls ? 443u : 80u; + } + + host_size = strlen(host_buf); + if (host_size == 0u || host_size >= host_len) { + return false; + } + memcpy(out_host, host_buf, host_size); + out_host[host_size] = '\0'; + *out_port = port; + *out_tls = tls; + return true; +} + +static bool amduat_asl_store_minio_sha256_hex(const uint8_t *data, + size_t len, + char out_hex[65]) { + uint8_t digest[32]; + + if (!amduat_hash_asl1_digest(AMDUAT_HASH_ASL1_ID_SHA256, + amduat_octets(data, len), + digest, + sizeof(digest))) { + return false; + } + if (!amduat_hex_encode_lower(digest, sizeof(digest), out_hex, 65u)) { + return false; + } + out_hex[64] = '\0'; + return true; +} + +static bool amduat_asl_store_minio_build_key(const char *prefix, + const char *ref_hex, + char **out_key) { + size_t prefix_len; + size_t ref_len; + size_t key_len; + char *key; + size_t offset = 0u; + + if (out_key != NULL) { + *out_key = NULL; + } + if (prefix == NULL || ref_hex == NULL || out_key == NULL) { + return false; + } + ref_len = strlen(ref_hex); + if (ref_len < 4u) { + return false; + } + + prefix_len = strlen(prefix); + key_len = (prefix_len > 0u ? prefix_len + 1u : 0u) + 2u + 1u + 2u + 1u + ref_len; + key = (char *)malloc(key_len + 1u); + if (key == NULL) { + return false; + } + + if (prefix_len > 0u) { + memcpy(key + offset, prefix, prefix_len); + offset += prefix_len; + key[offset++] = '/'; + } + + memcpy(key + offset, ref_hex, 2u); + offset += 2u; + key[offset++] = '/'; + memcpy(key + offset, ref_hex + 2u, 2u); + offset += 2u; + key[offset++] = '/'; + memcpy(key + offset, ref_hex, ref_len); + offset += ref_len; + key[offset] = '\0'; + + *out_key = key; + return true; +} + +static bool amduat_asl_store_minio_build_request_path( + const amduat_asl_store_minio_t *minio, + const char *key, + char **out_path) { + size_t bucket_len; + size_t key_len; + size_t path_len; + char *path; + + if (out_path != NULL) { + *out_path = NULL; + } + if (minio == NULL || key == NULL || out_path == NULL) { + return false; + } + + bucket_len = strlen(minio->bucket); + key_len = strlen(key); + + if (minio->force_path_style) { + path_len = 1u + bucket_len + 1u + key_len; + path = (char *)malloc(path_len + 1u); + if (path == NULL) { + return false; + } + snprintf(path, path_len + 1u, "/%s/%s", minio->bucket, key); + } else { + path_len = 1u + key_len; + path = (char *)malloc(path_len + 1u); + if (path == NULL) { + return false; + } + snprintf(path, path_len + 1u, "/%s", key); + } + + *out_path = path; + return true; +} + +static bool amduat_asl_store_minio_build_host_header( + const amduat_asl_store_minio_t *minio, + char **out_host, + char **out_connect_host) { + const char *base_host; + char *host_header; + char *connect_host; + size_t host_len; + size_t base_len; + size_t bucket_len; + bool add_bucket; + bool add_port; + size_t total_len; + char port_buf[16]; + size_t port_len = 0u; + + if (out_host != NULL) { + *out_host = NULL; + } + if (out_connect_host != NULL) { + *out_connect_host = NULL; + } + if (minio == NULL || out_host == NULL || out_connect_host == NULL) { + return false; + } + + base_host = minio->host; + base_len = strlen(base_host); + bucket_len = strlen(minio->bucket); + + add_bucket = !minio->force_path_style; + add_port = (minio->use_tls && minio->port != 443u) || + (!minio->use_tls && minio->port != 80u); + if (add_port) { + int rc = snprintf(port_buf, sizeof(port_buf), ":%u", minio->port); + if (rc <= 0 || (size_t)rc >= sizeof(port_buf)) { + return false; + } + port_len = (size_t)rc; + } + + host_len = base_len + (add_bucket ? bucket_len + 1u : 0u); + total_len = host_len + port_len; + connect_host = (char *)malloc(host_len + 1u); + if (connect_host == NULL) { + return false; + } + + if (add_bucket) { + memcpy(connect_host, minio->bucket, bucket_len); + connect_host[bucket_len] = '.'; + memcpy(connect_host + bucket_len + 1u, base_host, base_len); + } else { + memcpy(connect_host, base_host, base_len); + } + connect_host[host_len] = '\0'; + + host_header = (char *)malloc(total_len + 1u); + if (host_header == NULL) { + free(connect_host); + return false; + } + memcpy(host_header, connect_host, host_len); + if (add_port) { + memcpy(host_header + host_len, port_buf, port_len); + } + host_header[total_len] = '\0'; + + *out_host = host_header; + *out_connect_host = connect_host; + return true; +} + +static bool amduat_asl_store_minio_set_timeouts(int fd, uint32_t timeout_ms) { + struct timeval tv; + + if (timeout_ms == 0u) { + return true; + } + tv.tv_sec = (time_t)(timeout_ms / 1000u); + tv.tv_usec = (suseconds_t)((timeout_ms % 1000u) * 1000u); + if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) != 0) { + return false; + } + if (setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) != 0) { + return false; + } + return true; +} + +static int amduat_asl_store_minio_connect(const char *host, + uint16_t port, + uint32_t timeout_ms) { + struct addrinfo hints; + struct addrinfo *result = NULL; + struct addrinfo *rp = NULL; + char port_buf[16]; + int fd = -1; + + if (host == NULL) { + return -1; + } + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + snprintf(port_buf, sizeof(port_buf), "%u", port); + + if (getaddrinfo(host, port_buf, &hints, &result) != 0) { + return -1; + } + + for (rp = result; rp != NULL; rp = rp->ai_next) { + int flags; + fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (fd < 0) { + continue; + } + + flags = fcntl(fd, F_GETFL, 0); + if (flags < 0) { + close(fd); + fd = -1; + continue; + } + if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) { + close(fd); + fd = -1; + continue; + } + + if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) { + (void)fcntl(fd, F_SETFL, flags); + if (!amduat_asl_store_minio_set_timeouts(fd, timeout_ms)) { + close(fd); + fd = -1; + } + break; + } + + if (errno == EINPROGRESS) { + fd_set wfds; + struct timeval tv; + int sel; + int so_error = 0; + socklen_t so_len = sizeof(so_error); + + FD_ZERO(&wfds); + FD_SET(fd, &wfds); + tv.tv_sec = (time_t)(timeout_ms / 1000u); + tv.tv_usec = (suseconds_t)((timeout_ms % 1000u) * 1000u); + + sel = select(fd + 1, NULL, &wfds, NULL, + timeout_ms == 0u ? NULL : &tv); + if (sel > 0 && FD_ISSET(fd, &wfds) && + getsockopt(fd, SOL_SOCKET, SO_ERROR, &so_error, &so_len) == 0 && + so_error == 0) { + (void)fcntl(fd, F_SETFL, flags); + if (!amduat_asl_store_minio_set_timeouts(fd, timeout_ms)) { + close(fd); + fd = -1; + } + break; + } + } + + close(fd); + fd = -1; + } + + freeaddrinfo(result); + return fd; +} + +static bool amduat_asl_store_minio_send_all(int fd, + const uint8_t *data, + size_t len) { + size_t sent = 0u; + + if (len == 0u) { + return true; + } + if (data == NULL) { + return false; + } + + while (sent < len) { + ssize_t rc = send(fd, data + sent, len - sent, 0); + if (rc < 0) { + if (errno == EINTR) { + continue; + } + return false; + } + if (rc == 0) { + return false; + } + sent += (size_t)rc; + } + + return true; +} + +static bool amduat_asl_store_minio_read_all(int fd, + char **out_buf, + size_t *out_len) { + char *buf = NULL; + size_t len = 0u; + size_t cap = 0u; + + if (out_buf != NULL) { + *out_buf = NULL; + } + if (out_len != NULL) { + *out_len = 0u; + } + if (out_buf == NULL || out_len == NULL) { + return false; + } + + for (;;) { + char tmp[4096]; + ssize_t rc = recv(fd, tmp, sizeof(tmp), 0); + if (rc < 0) { + if (errno == EINTR) { + continue; + } + free(buf); + return false; + } + if (rc == 0) { + break; + } + if (len + (size_t)rc + 1u > cap) { + size_t next_cap = cap == 0u ? 8192u : cap * 2u; + while (next_cap < len + (size_t)rc + 1u) { + next_cap *= 2u; + } + char *next = (char *)realloc(buf, next_cap); + if (next == NULL) { + free(buf); + return false; + } + buf = next; + cap = next_cap; + } + memcpy(buf + len, tmp, (size_t)rc); + len += (size_t)rc; + } + + if (buf == NULL) { + buf = (char *)malloc(1u); + if (buf == NULL) { + return false; + } + } + buf[len] = '\0'; + *out_buf = buf; + *out_len = len; + return true; +} + +static bool amduat_asl_store_minio_decode_chunked(const char *in, + size_t in_len, + char **out, + size_t *out_len) { + size_t offset = 0u; + char *buf = NULL; + size_t buf_len = 0u; + + if (out != NULL) { + *out = NULL; + } + if (out_len != NULL) { + *out_len = 0u; + } + if (in == NULL || out == NULL || out_len == NULL) { + return false; + } + + while (offset < in_len) { + size_t line_start = offset; + size_t line_end = offset; + size_t chunk_len = 0u; + char *endptr = NULL; + + while (line_end + 1u < in_len && + !(in[line_end] == '\r' && in[line_end + 1u] == '\n')) { + line_end++; + } + if (line_end + 1u >= in_len) { + free(buf); + return false; + } + + { + char size_buf[32]; + size_t size_len = line_end - line_start; + if (size_len >= sizeof(size_buf)) { + free(buf); + return false; + } + memcpy(size_buf, in + line_start, size_len); + size_buf[size_len] = '\0'; + chunk_len = (size_t)strtoul(size_buf, &endptr, 16); + if (endptr == size_buf) { + free(buf); + return false; + } + } + + offset = line_end + 2u; + if (chunk_len == 0u) { + break; + } + if (offset + chunk_len + 2u > in_len) { + free(buf); + return false; + } + + { + char *next = (char *)realloc(buf, buf_len + chunk_len + 1u); + if (next == NULL) { + free(buf); + return false; + } + buf = next; + memcpy(buf + buf_len, in + offset, chunk_len); + buf_len += chunk_len; + buf[buf_len] = '\0'; + } + + offset += chunk_len + 2u; + } + + if (buf == NULL) { + buf = (char *)malloc(1u); + if (buf == NULL) { + return false; + } + buf[0] = '\0'; + } + + *out = buf; + *out_len = buf_len; + return true; +} + +static bool amduat_asl_store_minio_parse_response(const char *raw, + size_t raw_len, + amduat_http_response_t *out) { + const char *header_end; + const char *status_line_end; + int status = 0; + const char *cursor; + const char *body_start; + size_t body_len; + bool chunked = false; + size_t content_length = 0u; + bool has_content_length = false; + char *body = NULL; + + if (out == NULL) { + return false; + } + out->status = 0; + out->body = NULL; + out->body_len = 0u; + + if (raw == NULL || raw_len == 0u) { + return false; + } + + header_end = strstr(raw, "\r\n\r\n"); + if (header_end == NULL) { + return false; + } + status_line_end = strstr(raw, "\r\n"); + if (status_line_end == NULL || status_line_end > header_end) { + return false; + } + + { + const char *p = strchr(raw, ' '); + if (p == NULL || p > status_line_end) { + return false; + } + while (*p == ' ' && p < status_line_end) { + p++; + } + if (p + 3 > status_line_end) { + return false; + } + status = (int)strtol(p, NULL, 10); + } + + cursor = status_line_end + 2u; + while (cursor < header_end) { + const char *line_end = strstr(cursor, "\r\n"); + size_t line_len; + if (line_end == NULL || line_end > header_end) { + break; + } + line_len = (size_t)(line_end - cursor); + if (line_len > 0u) { + if (line_len >= 16u && + strncasecmp(cursor, "Content-Length:", 15u) == 0) { + const char *value = cursor + 15u; + while (*value == ' ' || *value == '\t') { + value++; + } + content_length = (size_t)strtoul(value, NULL, 10); + has_content_length = true; + } else if (line_len >= 18u && + strncasecmp(cursor, "Transfer-Encoding:", 18u) == 0) { + const char *value = cursor + 18u; + while (*value == ' ' || *value == '\t') { + value++; + } + if (strncasecmp(value, "chunked", 7u) == 0) { + chunked = true; + } + } + } + cursor = line_end + 2u; + } + + body_start = header_end + 4u; + body_len = raw_len - (size_t)(body_start - raw); + + if (chunked) { + if (!amduat_asl_store_minio_decode_chunked(body_start, body_len, + &body, &body_len)) { + return false; + } + } else if (has_content_length) { + if (body_len < content_length) { + return false; + } + body = (char *)malloc(content_length + 1u); + if (body == NULL) { + return false; + } + memcpy(body, body_start, content_length); + body[content_length] = '\0'; + body_len = content_length; + } else { + body = (char *)malloc(body_len + 1u); + if (body == NULL) { + return false; + } + memcpy(body, body_start, body_len); + body[body_len] = '\0'; + } + + out->status = status; + out->body = body; + out->body_len = body_len; + return true; +} + +static amduat_minio_error_t amduat_asl_store_minio_map_http(int status) { + if (status == 404) { + return AMDUAT_MINIO_ERR_NOT_FOUND; + } + if (status == 401 || status == 403) { + return AMDUAT_MINIO_ERR_PERMISSION; + } + if (status == 408 || status == 429) { + return AMDUAT_MINIO_ERR_TRANSIENT; + } + if (status >= 500 && status < 600) { + return AMDUAT_MINIO_ERR_TRANSIENT; + } + if (status >= 400 && status < 500) { + return AMDUAT_MINIO_ERR_INVALID; + } + return status >= 200 && status < 300 ? AMDUAT_MINIO_ERR_OK + : AMDUAT_MINIO_ERR_INTERNAL; +} + +static bool amduat_asl_store_minio_build_amz_date(char out[17]) { + time_t now; + struct tm tm_now; + + now = time(NULL); + if (gmtime_r(&now, &tm_now) == NULL) { + return false; + } + if (strftime(out, 17u, "%Y%m%dT%H%M%SZ", &tm_now) == 0u) { + return false; + } + return true; +} + +static bool amduat_asl_store_minio_http_request( + const amduat_asl_store_minio_t *minio, + const char *method, + const char *path, + const uint8_t *payload, + size_t payload_len, + bool want_body, + amduat_http_response_t *out_resp, + amduat_minio_error_t *out_err) { + uint32_t attempt; + char payload_hash[65]; + char amz_date[17]; + char *auth_header = NULL; + char *host_header = NULL; + char *connect_host = NULL; + char *request = NULL; + amduat_minio_error_t last_err = AMDUAT_MINIO_ERR_INTERNAL; + + if (out_resp == NULL || out_err == NULL) { + return false; + } + out_resp->status = 0; + out_resp->body = NULL; + out_resp->body_len = 0u; + *out_err = AMDUAT_MINIO_ERR_INTERNAL; + + if (minio == NULL || method == NULL || path == NULL) { + return false; + } + + if (!amduat_asl_store_minio_sha256_hex(payload, payload_len, payload_hash)) { + return false; + } + if (!amduat_asl_store_minio_build_amz_date(amz_date)) { + return false; + } + + if (!amduat_asl_store_minio_build_host_header(minio, &host_header, + &connect_host)) { + return false; + } + + if (!amduat_s3_sigv4_build_authorization(method, + path, + "", + host_header, + payload_hash, + minio->access_key, + minio->secret_key, + minio->region, + amz_date, + &auth_header)) { + free(host_header); + free(connect_host); + return false; + } + + for (attempt = 0u; attempt <= minio->retry_max; ++attempt) { + int fd; + amduat_http_response_t resp; + char *raw = NULL; + size_t raw_len = 0u; + int rc; + + fd = amduat_asl_store_minio_connect(connect_host, + minio->port, + minio->timeout_ms); + if (fd < 0) { + last_err = AMDUAT_MINIO_ERR_TRANSIENT; + goto maybe_retry; + } + + rc = snprintf(NULL, + 0, + "%s %s HTTP/1.1\r\n" + "Host: %s\r\n" + "x-amz-date: %s\r\n" + "x-amz-content-sha256: %s\r\n" + "Authorization: %s\r\n" + "Content-Length: %zu\r\n" + "Connection: close\r\n\r\n", + method, + path, + host_header, + amz_date, + payload_hash, + auth_header, + payload_len); + if (rc <= 0) { + close(fd); + last_err = AMDUAT_MINIO_ERR_INTERNAL; + goto maybe_retry; + } + + request = (char *)malloc((size_t)rc + 1u); + if (request == NULL) { + close(fd); + last_err = AMDUAT_MINIO_ERR_INTERNAL; + goto maybe_retry; + } + snprintf(request, + (size_t)rc + 1u, + "%s %s HTTP/1.1\r\n" + "Host: %s\r\n" + "x-amz-date: %s\r\n" + "x-amz-content-sha256: %s\r\n" + "Authorization: %s\r\n" + "Content-Length: %zu\r\n" + "Connection: close\r\n\r\n", + method, + path, + host_header, + amz_date, + payload_hash, + auth_header, + payload_len); + + if (!amduat_asl_store_minio_send_all( + fd, (const uint8_t *)request, (size_t)rc) || + (payload_len > 0u && + !amduat_asl_store_minio_send_all(fd, payload, payload_len))) { + free(request); + request = NULL; + close(fd); + last_err = AMDUAT_MINIO_ERR_TRANSIENT; + goto maybe_retry; + } + + free(request); + request = NULL; + + if (!amduat_asl_store_minio_read_all(fd, &raw, &raw_len)) { + close(fd); + free(raw); + last_err = AMDUAT_MINIO_ERR_TRANSIENT; + goto maybe_retry; + } + close(fd); + + if (!amduat_asl_store_minio_parse_response(raw, raw_len, &resp)) { + free(raw); + last_err = AMDUAT_MINIO_ERR_INTERNAL; + goto maybe_retry; + } + free(raw); + + last_err = amduat_asl_store_minio_map_http(resp.status); + if (last_err == AMDUAT_MINIO_ERR_TRANSIENT && + attempt < minio->retry_max) { + free(resp.body); + resp.body = NULL; + resp.body_len = 0u; + goto maybe_retry; + } + + if (!want_body) { + free(resp.body); + resp.body = NULL; + resp.body_len = 0u; + } + *out_resp = resp; + *out_err = last_err; + break; + + maybe_retry: + if (attempt >= minio->retry_max || last_err != AMDUAT_MINIO_ERR_TRANSIENT) { + break; + } + usleep(200000u); + } + + free(auth_header); + free(host_header); + free(connect_host); + *out_err = last_err; + return last_err == AMDUAT_MINIO_ERR_OK || last_err == AMDUAT_MINIO_ERR_NOT_FOUND || + last_err == AMDUAT_MINIO_ERR_PERMISSION || + last_err == AMDUAT_MINIO_ERR_INVALID || + last_err == AMDUAT_MINIO_ERR_INTERNAL || + last_err == AMDUAT_MINIO_ERR_TRANSIENT; +} + +static amduat_asl_store_error_t amduat_asl_store_minio_map_store_error( + amduat_minio_error_t err) { + switch (err) { + case AMDUAT_MINIO_ERR_OK: + return AMDUAT_ASL_STORE_OK; + case AMDUAT_MINIO_ERR_NOT_FOUND: + return AMDUAT_ASL_STORE_ERR_NOT_FOUND; + case AMDUAT_MINIO_ERR_PERMISSION: + return AMDUAT_ASL_STORE_ERR_IO; + case AMDUAT_MINIO_ERR_TRANSIENT: + return AMDUAT_ASL_STORE_ERR_IO; + case AMDUAT_MINIO_ERR_INVALID: + return AMDUAT_ASL_STORE_ERR_INTEGRITY; + case AMDUAT_MINIO_ERR_INTERNAL: + default: + return AMDUAT_ASL_STORE_ERR_IO; + } +} + +static amduat_asl_store_error_t amduat_asl_store_minio_put_impl( + void *ctx, + amduat_artifact_t artifact, + amduat_reference_t *out_ref) { + amduat_asl_store_minio_t *minio; + const amduat_hash_asl1_desc_t *hash_desc; + amduat_octets_t artifact_bytes; + amduat_reference_t derived_ref; + char *ref_hex = NULL; + char *key = NULL; + char *path = NULL; + amduat_http_response_t resp; + amduat_minio_error_t req_err; + + if (ctx == NULL || out_ref == NULL) { + return AMDUAT_ASL_STORE_ERR_IO; + } + minio = (amduat_asl_store_minio_t *)ctx; + + if (minio->store_config.encoding_profile_id != AMDUAT_ENC_ASL1_CORE_V1) { + return AMDUAT_ASL_STORE_ERR_UNSUPPORTED; + } + hash_desc = amduat_hash_asl1_desc_lookup(minio->store_config.hash_id); + if (hash_desc == NULL || hash_desc->digest_len == 0 || + hash_desc->impl.digest == NULL) { + return AMDUAT_ASL_STORE_ERR_UNSUPPORTED; + } + if (hash_desc->digest_len < AMDUAT_ASL_STORE_MINIO_MIN_DIGEST_BYTES) { + return AMDUAT_ASL_STORE_ERR_UNSUPPORTED; + } + + if (!amduat_asl_ref_derive(artifact, + minio->store_config.encoding_profile_id, + minio->store_config.hash_id, + &derived_ref, + &artifact_bytes)) { + return AMDUAT_ASL_STORE_ERR_INTEGRITY; + } + + if (!amduat_asl_ref_encode_hex(derived_ref, &ref_hex)) { + amduat_octets_free(&artifact_bytes); + amduat_reference_free(&derived_ref); + return AMDUAT_ASL_STORE_ERR_IO; + } + + if (!amduat_asl_store_minio_build_key(minio->prefix, ref_hex, &key) || + !amduat_asl_store_minio_build_request_path(minio, key, &path)) { + free(ref_hex); + free(key); + free(path); + amduat_octets_free(&artifact_bytes); + amduat_reference_free(&derived_ref); + return AMDUAT_ASL_STORE_ERR_IO; + } + + if (!amduat_asl_store_minio_http_request(minio, + "PUT", + path, + artifact_bytes.data, + artifact_bytes.len, + false, + &resp, + &req_err)) { + free(ref_hex); + free(key); + free(path); + amduat_octets_free(&artifact_bytes); + amduat_reference_free(&derived_ref); + return AMDUAT_ASL_STORE_ERR_IO; + } + + free(resp.body); + free(ref_hex); + free(key); + free(path); + amduat_octets_free(&artifact_bytes); + + if (req_err != AMDUAT_MINIO_ERR_OK) { + amduat_reference_free(&derived_ref); + return amduat_asl_store_minio_map_store_error(req_err); + } + + *out_ref = derived_ref; + return AMDUAT_ASL_STORE_OK; +} + +static amduat_asl_store_error_t amduat_asl_store_minio_get_impl( + void *ctx, + amduat_reference_t ref, + amduat_artifact_t *out_artifact) { + amduat_asl_store_minio_t *minio; + const amduat_hash_asl1_desc_t *hash_desc; + char *ref_hex = NULL; + char *key = NULL; + char *path = NULL; + amduat_http_response_t resp; + amduat_minio_error_t req_err; + uint8_t *computed_digest = NULL; + bool decoded = false; + + if (ctx == NULL || out_artifact == NULL) { + return AMDUAT_ASL_STORE_ERR_IO; + } + minio = (amduat_asl_store_minio_t *)ctx; + + if (minio->store_config.encoding_profile_id != AMDUAT_ENC_ASL1_CORE_V1) { + return AMDUAT_ASL_STORE_ERR_UNSUPPORTED; + } + if (ref.hash_id != minio->store_config.hash_id) { + return AMDUAT_ASL_STORE_ERR_UNSUPPORTED; + } + + hash_desc = amduat_hash_asl1_desc_lookup(minio->store_config.hash_id); + if (hash_desc == NULL || hash_desc->digest_len == 0 || + hash_desc->impl.digest == NULL) { + return AMDUAT_ASL_STORE_ERR_UNSUPPORTED; + } + if (hash_desc->digest_len < AMDUAT_ASL_STORE_MINIO_MIN_DIGEST_BYTES) { + return AMDUAT_ASL_STORE_ERR_UNSUPPORTED; + } + if (ref.digest.len != hash_desc->digest_len || + (ref.digest.len != 0u && ref.digest.data == NULL)) { + return AMDUAT_ASL_STORE_ERR_INTEGRITY; + } + + if (!amduat_asl_ref_encode_hex(ref, &ref_hex)) { + return AMDUAT_ASL_STORE_ERR_IO; + } + + if (!amduat_asl_store_minio_build_key(minio->prefix, ref_hex, &key) || + !amduat_asl_store_minio_build_request_path(minio, key, &path)) { + free(ref_hex); + free(key); + free(path); + return AMDUAT_ASL_STORE_ERR_IO; + } + + if (!amduat_asl_store_minio_http_request(minio, + "GET", + path, + NULL, + 0u, + true, + &resp, + &req_err)) { + free(ref_hex); + free(key); + free(path); + return AMDUAT_ASL_STORE_ERR_IO; + } + + free(ref_hex); + free(key); + free(path); + + if (req_err != AMDUAT_MINIO_ERR_OK) { + free(resp.body); + return amduat_asl_store_minio_map_store_error(req_err); + } + + computed_digest = (uint8_t *)malloc(hash_desc->digest_len); + if (computed_digest == NULL) { + free(resp.body); + return AMDUAT_ASL_STORE_ERR_IO; + } + if (!amduat_hash_asl1_digest(minio->store_config.hash_id, + amduat_octets((uint8_t *)resp.body, resp.body_len), + computed_digest, + hash_desc->digest_len)) { + free(computed_digest); + free(resp.body); + return AMDUAT_ASL_STORE_ERR_IO; + } + + if (memcmp(computed_digest, ref.digest.data, hash_desc->digest_len) != 0) { + free(computed_digest); + free(resp.body); + return AMDUAT_ASL_STORE_ERR_INTEGRITY; + } + free(computed_digest); + + decoded = amduat_enc_asl1_core_decode_artifact_v1( + amduat_octets((uint8_t *)resp.body, resp.body_len), out_artifact); + free(resp.body); + if (!decoded) { + return AMDUAT_ASL_STORE_ERR_INTEGRITY; + } + + return AMDUAT_ASL_STORE_OK; +} + +static void amduat_asl_store_minio_fill_index_state( + amduat_asl_index_state_t *out_state) { + if (out_state == NULL) { + return; + } + out_state->snapshot_id = 0u; + out_state->log_position = 0u; +} + +static amduat_asl_store_error_t amduat_asl_store_minio_put_indexed_impl( + void *ctx, + amduat_artifact_t artifact, + amduat_reference_t *out_ref, + amduat_asl_index_state_t *out_state) { + amduat_asl_store_error_t err; + + err = amduat_asl_store_minio_put_impl(ctx, artifact, out_ref); + if (err != AMDUAT_ASL_STORE_OK) { + return err; + } + amduat_asl_store_minio_fill_index_state(out_state); + return AMDUAT_ASL_STORE_OK; +} + +static amduat_asl_store_error_t amduat_asl_store_minio_get_indexed_impl( + void *ctx, + amduat_reference_t ref, + amduat_asl_index_state_t state, + amduat_artifact_t *out_artifact) { + (void)state; + return amduat_asl_store_minio_get_impl(ctx, ref, out_artifact); +} + +static bool amduat_asl_store_minio_current_state_impl( + void *ctx, + amduat_asl_index_state_t *out_state) { + if (ctx == NULL) { + return false; + } + amduat_asl_store_minio_fill_index_state(out_state); + return true; +} + +static amduat_asl_store_error_t amduat_asl_store_minio_validate_config( + void *ctx, + amduat_asl_store_config_t config) { + const amduat_hash_asl1_desc_t *hash_desc; + + (void)ctx; + + if (config.encoding_profile_id != AMDUAT_ENC_ASL1_CORE_V1) { + return AMDUAT_ASL_STORE_ERR_UNSUPPORTED; + } + hash_desc = amduat_hash_asl1_desc_lookup(config.hash_id); + if (hash_desc == NULL || hash_desc->digest_len == 0 || + hash_desc->impl.digest == NULL) { + return AMDUAT_ASL_STORE_ERR_UNSUPPORTED; + } + if (hash_desc->digest_len < AMDUAT_ASL_STORE_MINIO_MIN_DIGEST_BYTES) { + return AMDUAT_ASL_STORE_ERR_UNSUPPORTED; + } + + return AMDUAT_ASL_STORE_OK; +} + +void amduat_asl_store_minio_config_defaults( + amduat_asl_store_minio_config_t *config) { + if (config == NULL) { + return; + } + memset(config, 0, sizeof(*config)); + config->store_config.encoding_profile_id = AMDUAT_ENC_ASL1_CORE_V1; + config->store_config.hash_id = AMDUAT_HASH_ASL1_ID_SHA256; + config->region = "us-east-1"; + config->prefix = "asl"; + config->force_path_style = true; + config->tls_verify = true; + config->timeout_ms = AMDUAT_ASL_STORE_MINIO_DEFAULT_TIMEOUT_MS; + config->retry_max = AMDUAT_ASL_STORE_MINIO_DEFAULT_RETRY_MAX; +} + +bool amduat_asl_store_minio_init(amduat_asl_store_minio_t *minio, + amduat_asl_store_minio_config_t config) { + if (minio == NULL) { + return false; + } + memset(minio, 0, sizeof(*minio)); + + if (config.endpoint == NULL || config.bucket == NULL || + config.access_key == NULL || config.secret_key == NULL) { + return false; + } + + if (!amduat_asl_store_minio_copy_string(minio->endpoint, + sizeof(minio->endpoint), + config.endpoint) || + !amduat_asl_store_minio_copy_string(minio->bucket, + sizeof(minio->bucket), + config.bucket) || + !amduat_asl_store_minio_copy_string(minio->access_key, + sizeof(minio->access_key), + config.access_key) || + !amduat_asl_store_minio_copy_string(minio->secret_key, + sizeof(minio->secret_key), + config.secret_key)) { + return false; + } + + if (!amduat_asl_store_minio_parse_endpoint(minio->endpoint, + minio->host, + sizeof(minio->host), + &minio->port, + &minio->use_tls)) { + return false; + } + + if (minio->use_tls) { + return false; + } + + if (config.region != NULL) { + if (!amduat_asl_store_minio_copy_string(minio->region, + sizeof(minio->region), + config.region)) { + return false; + } + } else { + if (!amduat_asl_store_minio_copy_string(minio->region, + sizeof(minio->region), + "us-east-1")) { + return false; + } + } + + if (config.prefix != NULL) { + if (!amduat_asl_store_minio_copy_string(minio->prefix, + sizeof(minio->prefix), + config.prefix)) { + return false; + } + } else { + if (!amduat_asl_store_minio_copy_string(minio->prefix, + sizeof(minio->prefix), + "asl")) { + return false; + } + } + + minio->store_config = config.store_config; + minio->force_path_style = config.force_path_style; + minio->tls_verify = config.tls_verify; + minio->timeout_ms = config.timeout_ms != 0u ? + config.timeout_ms : + AMDUAT_ASL_STORE_MINIO_DEFAULT_TIMEOUT_MS; + minio->retry_max = config.retry_max; + return true; +} + +amduat_asl_store_ops_t amduat_asl_store_minio_ops(void) { + amduat_asl_store_ops_t ops; + + amduat_asl_store_ops_init(&ops); + ops.put = amduat_asl_store_minio_put_impl; + ops.get = amduat_asl_store_minio_get_impl; + ops.put_indexed = amduat_asl_store_minio_put_indexed_impl; + ops.get_indexed = amduat_asl_store_minio_get_indexed_impl; + ops.current_state = amduat_asl_store_minio_current_state_impl; + ops.validate_config = amduat_asl_store_minio_validate_config; + return ops; +} diff --git a/src/adapters/asl_store_minio/s3_sigv4.c b/src/adapters/asl_store_minio/s3_sigv4.c new file mode 100644 index 0000000..9288007 --- /dev/null +++ b/src/adapters/asl_store_minio/s3_sigv4.c @@ -0,0 +1,288 @@ +#include "s3_sigv4.h" + +#include "amduat/asl/core.h" +#include "amduat/hash/asl1.h" +#include "amduat/util/hex.h" + +#include +#include +#include +#include +#include +#include + +enum { AMDUAT_S3_SIGV4_SHA256_LEN = 32u, AMDUAT_S3_SIGV4_BLOCK_LEN = 64u }; + +static bool sha256_digest(const uint8_t *data, size_t len, uint8_t out[32]) { + amduat_octets_t input; + + input.data = data; + input.len = len; + return amduat_hash_asl1_digest(AMDUAT_HASH_ASL1_ID_SHA256, input, out, 32u); +} + +static bool sha256_hex(const uint8_t *data, size_t len, char out_hex[65]) { + uint8_t digest[AMDUAT_S3_SIGV4_SHA256_LEN]; + + if (!sha256_digest(data, len, digest)) { + return false; + } + if (!amduat_hex_encode_lower(digest, sizeof(digest), out_hex, 65u)) { + return false; + } + out_hex[64] = '\0'; + return true; +} + +static bool hmac_sha256(const uint8_t *key, + size_t key_len, + const uint8_t *msg, + size_t msg_len, + uint8_t out[32]) { + uint8_t key_block[AMDUAT_S3_SIGV4_BLOCK_LEN]; + uint8_t ipad[AMDUAT_S3_SIGV4_BLOCK_LEN]; + uint8_t opad[AMDUAT_S3_SIGV4_BLOCK_LEN]; + uint8_t inner_hash[AMDUAT_S3_SIGV4_SHA256_LEN]; + size_t i; + amduat_hash_asl1_stream_t stream; + + memset(key_block, 0, sizeof(key_block)); + if (key_len > sizeof(key_block)) { + if (!sha256_digest(key, key_len, key_block)) { + return false; + } + key_len = AMDUAT_S3_SIGV4_SHA256_LEN; + } else if (key_len > 0 && key != NULL) { + memcpy(key_block, key, key_len); + } + + for (i = 0; i < sizeof(key_block); ++i) { + ipad[i] = (uint8_t)(key_block[i] ^ 0x36u); + opad[i] = (uint8_t)(key_block[i] ^ 0x5cu); + } + + if (!amduat_hash_asl1_stream_init(AMDUAT_HASH_ASL1_ID_SHA256, &stream)) { + return false; + } + if (!amduat_hash_asl1_stream_update( + &stream, amduat_octets(ipad, sizeof(ipad))) || + (msg_len > 0 && + !amduat_hash_asl1_stream_update( + &stream, amduat_octets(msg, msg_len))) || + !amduat_hash_asl1_stream_final(&stream, inner_hash, sizeof(inner_hash))) { + amduat_hash_asl1_stream_destroy(&stream); + return false; + } + amduat_hash_asl1_stream_destroy(&stream); + + if (!amduat_hash_asl1_stream_init(AMDUAT_HASH_ASL1_ID_SHA256, &stream)) { + return false; + } + if (!amduat_hash_asl1_stream_update( + &stream, amduat_octets(opad, sizeof(opad))) || + !amduat_hash_asl1_stream_update( + &stream, amduat_octets(inner_hash, sizeof(inner_hash))) || + !amduat_hash_asl1_stream_final(&stream, out, AMDUAT_S3_SIGV4_SHA256_LEN)) { + amduat_hash_asl1_stream_destroy(&stream); + return false; + } + amduat_hash_asl1_stream_destroy(&stream); + return true; +} + +bool amduat_s3_sigv4_build_authorization(const char *method, + const char *canonical_uri, + const char *canonical_query, + const char *host, + const char *payload_hash_hex, + const char *access_key, + const char *secret_key, + const char *region, + const char *amz_date, + char **out_authorization) { + const char *signed_headers = "host;x-amz-content-sha256;x-amz-date"; + char date_stamp[9]; + char *canonical_request = NULL; + char *string_to_sign = NULL; + char canonical_request_hash[65]; + uint8_t k_date[AMDUAT_S3_SIGV4_SHA256_LEN]; + uint8_t k_region[AMDUAT_S3_SIGV4_SHA256_LEN]; + uint8_t k_service[AMDUAT_S3_SIGV4_SHA256_LEN]; + uint8_t k_signing[AMDUAT_S3_SIGV4_SHA256_LEN]; + uint8_t signature_bytes[AMDUAT_S3_SIGV4_SHA256_LEN]; + char signature_hex[65]; + char *auth_header = NULL; + size_t auth_len; + int rc; + + if (out_authorization != NULL) { + *out_authorization = NULL; + } + if (method == NULL || canonical_uri == NULL || canonical_query == NULL || + host == NULL || payload_hash_hex == NULL || access_key == NULL || + secret_key == NULL || region == NULL || amz_date == NULL || + out_authorization == NULL) { + return false; + } + if (strlen(amz_date) < 8u) { + return false; + } + memcpy(date_stamp, amz_date, 8u); + date_stamp[8] = '\0'; + + rc = snprintf(NULL, + 0, + "%s\n%s\n%s\n" + "host:%s\n" + "x-amz-content-sha256:%s\n" + "x-amz-date:%s\n\n" + "%s\n" + "%s", + method, + canonical_uri, + canonical_query, + host, + payload_hash_hex, + amz_date, + signed_headers, + payload_hash_hex); + if (rc <= 0) { + return false; + } + canonical_request = (char *)malloc((size_t)rc + 1u); + if (canonical_request == NULL) { + return false; + } + snprintf(canonical_request, + (size_t)rc + 1u, + "%s\n%s\n%s\n" + "host:%s\n" + "x-amz-content-sha256:%s\n" + "x-amz-date:%s\n\n" + "%s\n" + "%s", + method, + canonical_uri, + canonical_query, + host, + payload_hash_hex, + amz_date, + signed_headers, + payload_hash_hex); + + if (!sha256_hex((const uint8_t *)canonical_request, + strlen(canonical_request), + canonical_request_hash)) { + free(canonical_request); + return false; + } + + rc = snprintf(NULL, + 0, + "AWS4-HMAC-SHA256\n%s\n%s/%s/s3/aws4_request\n%s", + amz_date, + date_stamp, + region, + canonical_request_hash); + if (rc <= 0) { + free(canonical_request); + return false; + } + string_to_sign = (char *)malloc((size_t)rc + 1u); + if (string_to_sign == NULL) { + free(canonical_request); + return false; + } + snprintf(string_to_sign, + (size_t)rc + 1u, + "AWS4-HMAC-SHA256\n%s\n%s/%s/s3/aws4_request\n%s", + amz_date, + date_stamp, + region, + canonical_request_hash); + + { + size_t secret_len = strlen(secret_key); + size_t key_len = 4u + secret_len; + char *key_prefix = (char *)malloc(key_len + 1u); + if (key_prefix == NULL) { + free(canonical_request); + free(string_to_sign); + return false; + } + memcpy(key_prefix, "AWS4", 4u); + memcpy(key_prefix + 4u, secret_key, secret_len); + key_prefix[key_len] = '\0'; + + if (!hmac_sha256((const uint8_t *)key_prefix, + key_len, + (const uint8_t *)date_stamp, + strlen(date_stamp), + k_date) || + !hmac_sha256(k_date, + sizeof(k_date), + (const uint8_t *)region, + strlen(region), + k_region) || + !hmac_sha256(k_region, + sizeof(k_region), + (const uint8_t *)"s3", + 2u, + k_service) || + !hmac_sha256(k_service, + sizeof(k_service), + (const uint8_t *)"aws4_request", + 12u, + k_signing) || + !hmac_sha256(k_signing, + sizeof(k_signing), + (const uint8_t *)string_to_sign, + strlen(string_to_sign), + signature_bytes)) { + free(key_prefix); + free(canonical_request); + free(string_to_sign); + return false; + } + free(key_prefix); + } + + if (!amduat_hex_encode_lower(signature_bytes, + sizeof(signature_bytes), + signature_hex, + sizeof(signature_hex))) { + free(canonical_request); + free(string_to_sign); + return false; + } + + auth_len = (size_t)snprintf(NULL, + 0, + "AWS4-HMAC-SHA256 Credential=%s/%s/%s/s3/aws4_request, " + "SignedHeaders=%s, Signature=%s", + access_key, + date_stamp, + region, + signed_headers, + signature_hex); + auth_header = (char *)malloc(auth_len + 1u); + if (auth_header == NULL) { + free(canonical_request); + free(string_to_sign); + return false; + } + snprintf(auth_header, + auth_len + 1u, + "AWS4-HMAC-SHA256 Credential=%s/%s/%s/s3/aws4_request, " + "SignedHeaders=%s, Signature=%s", + access_key, + date_stamp, + region, + signed_headers, + signature_hex); + + free(canonical_request); + free(string_to_sign); + *out_authorization = auth_header; + return true; +} diff --git a/src/adapters/asl_store_minio/s3_sigv4.h b/src/adapters/asl_store_minio/s3_sigv4.h new file mode 100644 index 0000000..c19086c --- /dev/null +++ b/src/adapters/asl_store_minio/s3_sigv4.h @@ -0,0 +1,25 @@ +#ifndef AMDUAT_ADAPTERS_ASL_STORE_MINIO_S3_SIGV4_H +#define AMDUAT_ADAPTERS_ASL_STORE_MINIO_S3_SIGV4_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +bool amduat_s3_sigv4_build_authorization(const char *method, + const char *canonical_uri, + const char *canonical_query, + const char *host, + const char *payload_hash_hex, + const char *access_key, + const char *secret_key, + const char *region, + const char *amz_date, + char **out_authorization); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* AMDUAT_ADAPTERS_ASL_STORE_MINIO_S3_SIGV4_H */ diff --git a/src/tools/amduat_conformance_run.c b/src/tools/amduat_conformance_run.c index 0e11d90..24e0bf8 100644 --- a/src/tools/amduat_conformance_run.c +++ b/src/tools/amduat_conformance_run.c @@ -70,9 +70,13 @@ static int print_results(const char *suite, } for (i = 0; i < results_len; ++i) { - const char *status = results[i].status == 0 ? "PASS" : "FAIL"; + const char *status = + results[i].status == AMDUAT_CONFORMANCE_STATUS_PASS + ? "PASS" + : (results[i].status == AMDUAT_CONFORMANCE_STATUS_SKIP ? "SKIPPED" + : "FAIL"); printf("%s backend=%s %s\n", suite, results[i].backend, status); - if (results[i].status != 0) { + if (results[i].status == AMDUAT_CONFORMANCE_STATUS_FAIL) { rc = 1; } } diff --git a/tests/conformance/asl_store/asl_store_conformance.c b/tests/conformance/asl_store/asl_store_conformance.c index da4abef..90d69f6 100644 --- a/tests/conformance/asl_store/asl_store_conformance.c +++ b/tests/conformance/asl_store/asl_store_conformance.c @@ -2,11 +2,15 @@ #include "amduat/asl/asl_store_fs.h" #include "amduat/asl/asl_store_index_fs.h" +#include "amduat/asl/asl_store_minio.h" #include "amduat/asl/core.h" +#include "amduat/asl/ref_text.h" #include "amduat/asl/store.h" #include "amduat/enc/asl1_core.h" #include "amduat/enc/asl1_core_codec.h" +#include "amduat/enc/asl_log.h" #include "amduat/hash/asl1.h" +#include "amduat/asl/index_replay.h" #include #include @@ -41,6 +45,18 @@ typedef struct { amduat_asl_store_index_fs_t fs; } amduat_asl_index_fs_ctx_t; +typedef struct { + amduat_asl_store_minio_t minio; +} amduat_asl_minio_ctx_t; + +static bool conformance_env_truthy(const char *value) { + if (value == NULL) { + return false; + } + return strcmp(value, "1") == 0 || strcmp(value, "true") == 0 || + strcmp(value, "yes") == 0; +} + static bool conformance_join_path(const char *base, const char *segment, char **out_path) { @@ -82,6 +98,82 @@ static bool conformance_join_path(const char *base, return true; } +static bool conformance_append_list(char **list, const char *entry) { + size_t list_len; + size_t entry_len; + size_t next_len; + char *next; + + if (list == NULL || entry == NULL) { + return false; + } + + if (*list == NULL) { + *list = strdup(entry); + return *list != NULL; + } + + list_len = strlen(*list); + entry_len = strlen(entry); + next_len = list_len + 2u + entry_len + 1u; + + next = (char *)realloc(*list, next_len); + if (next == NULL) { + return false; + } + + next[list_len] = ','; + next[list_len + 1u] = ' '; + memcpy(next + list_len + 2u, entry, entry_len); + next[next_len - 1u] = '\0'; + *list = next; + return true; +} + +static bool conformance_minio_missing_envs(char **out_missing) { + if (out_missing != NULL) { + *out_missing = NULL; + } + if (out_missing == NULL) { + return false; + } + + { + const char *value = getenv("AMDUAT_MINIO_ENDPOINT"); + if (value == NULL || value[0] == '\0') { + if (!conformance_append_list(out_missing, "AMDUAT_MINIO_ENDPOINT")) { + return false; + } + } + } + { + const char *value = getenv("AMDUAT_MINIO_BUCKET"); + if (value == NULL || value[0] == '\0') { + if (!conformance_append_list(out_missing, "AMDUAT_MINIO_BUCKET")) { + return false; + } + } + } + { + const char *value = getenv("AMDUAT_MINIO_ACCESS_KEY"); + if (value == NULL || value[0] == '\0') { + if (!conformance_append_list(out_missing, "AMDUAT_MINIO_ACCESS_KEY")) { + return false; + } + } + } + { + const char *value = getenv("AMDUAT_MINIO_SECRET_KEY"); + if (value == NULL || value[0] == '\0') { + if (!conformance_append_list(out_missing, "AMDUAT_MINIO_SECRET_KEY")) { + return false; + } + } + } + + return true; +} + static bool conformance_remove_tree(const char *path) { struct stat st; DIR *dir; @@ -536,6 +628,18 @@ static bool conformance_build_artifact(amduat_octets_t payload, return true; } +static void conformance_free_ref_hexes(char **refs, size_t refs_len) { + size_t i; + + if (refs == NULL) { + return; + } + for (i = 0; i < refs_len; ++i) { + free(refs[i]); + } + free(refs); +} + static bool conformance_asl_is_not_found(amduat_asl_store_error_t err) { return err == AMDUAT_ASL_STORE_ERR_NOT_FOUND; } @@ -543,7 +647,8 @@ static bool conformance_asl_is_not_found(amduat_asl_store_error_t err) { static int conformance_test_put_get(amduat_asl_store_t *store, const amduat_conformance_fixture_t *fixtures, size_t fixtures_len, - const char *fixture_root) { + const char *fixture_root, + char **out_ref_hexes) { size_t i; for (i = 0; i < fixtures_len; ++i) { @@ -594,6 +699,18 @@ static int conformance_test_put_get(amduat_asl_store_t *store, return 1; } + if (out_ref_hexes != NULL) { + char *hex = NULL; + if (!amduat_asl_ref_encode_hex(ref, &hex)) { + fprintf(stderr, "ref encode failed for fixture %s\n", fixtures[i].id); + amduat_reference_free(&ref); + amduat_artifact_free(&artifact); + amduat_artifact_free(&fetched); + return 1; + } + out_ref_hexes[i] = hex; + } + if (!amduat_artifact_eq(artifact, fetched)) { fprintf(stderr, "payload mismatch for fixture %s\n", fixtures[i].id); amduat_reference_free(&ref); @@ -697,6 +814,88 @@ static int conformance_test_get_not_found(amduat_asl_store_t *store) { return 0; } +static bool conformance_replay_state_equal( + const amduat_asl_replay_state_t *lhs, + const amduat_asl_replay_state_t *rhs) { + size_t i; + + if (lhs->segments_len != rhs->segments_len || + lhs->tombstones_len != rhs->tombstones_len || + lhs->state.log_position != rhs->state.log_position) { + return false; + } + + for (i = 0; i < lhs->segments_len; ++i) { + const amduat_asl_segment_seal_t *left = &lhs->segments[i]; + const amduat_asl_segment_seal_t *right = &rhs->segments[i]; + if (left->segment_id != right->segment_id || + memcmp(left->segment_hash, right->segment_hash, + sizeof(left->segment_hash)) != 0) { + return false; + } + } + + for (i = 0; i < lhs->tombstones_len; ++i) { + const amduat_asl_tombstone_entry_t *left = &lhs->tombstones[i]; + const amduat_asl_tombstone_entry_t *right = &rhs->tombstones[i]; + if (left->tombstone_logseq != right->tombstone_logseq || + !amduat_reference_eq(left->ref, right->ref)) { + return false; + } + } + + return true; +} + +static int conformance_test_replay_determinism(amduat_asl_store_t *store) { + amduat_asl_log_record_t *records = NULL; + size_t record_count = 0u; + amduat_asl_replay_state_t first; + amduat_asl_replay_state_t second; + amduat_asl_store_error_t err; + amduat_asl_log_position_t log_position = 0u; + int rc = 1; + + err = amduat_asl_log_scan(store, &records, &record_count); + if (err != AMDUAT_ASL_STORE_OK) { + fprintf(stderr, "log scan failed: %d\n", err); + return 1; + } + if (record_count == 0u) { + fprintf(stderr, "log scan returned no records\n"); + amduat_enc_asl_log_free(records, record_count); + return 1; + } + + log_position = records[record_count - 1u].logseq; + + if (!amduat_asl_replay_init(&first) || + !amduat_asl_replay_init(&second)) { + fprintf(stderr, "replay init failed\n"); + goto cleanup; + } + + if (!amduat_asl_replay_apply_log(records, record_count, log_position, &first) || + !amduat_asl_replay_apply_log(records, record_count, log_position, &second)) { + fprintf(stderr, "replay apply failed\n"); + goto cleanup_states; + } + + if (!conformance_replay_state_equal(&first, &second)) { + fprintf(stderr, "replay determinism mismatch\n"); + goto cleanup_states; + } + + rc = 0; + +cleanup_states: + amduat_asl_replay_free(&first); + amduat_asl_replay_free(&second); +cleanup: + amduat_enc_asl_log_free(records, record_count); + return rc; +} + static bool conformance_init_asl_store_fs(const char *root, amduat_asl_store_t *out_store, void **out_ctx) { @@ -757,6 +956,71 @@ static bool conformance_init_asl_store_index_fs( return true; } +static bool conformance_init_asl_store_minio( + const char *root, + amduat_asl_store_t *out_store, + void **out_ctx) { + amduat_asl_minio_ctx_t *ctx; + amduat_asl_store_minio_config_t config; + const char *timeout_env; + const char *retry_env; + const char *force_env; + const char *tls_env; + + (void)root; + + if (out_store == NULL || out_ctx == NULL) { + return false; + } + + ctx = (amduat_asl_minio_ctx_t *)calloc(1, sizeof(*ctx)); + if (ctx == NULL) { + return false; + } + + amduat_asl_store_minio_config_defaults(&config); + config.endpoint = getenv("AMDUAT_MINIO_ENDPOINT"); + config.bucket = getenv("AMDUAT_MINIO_BUCKET"); + config.access_key = getenv("AMDUAT_MINIO_ACCESS_KEY"); + config.secret_key = getenv("AMDUAT_MINIO_SECRET_KEY"); + config.region = getenv("AMDUAT_MINIO_REGION") != NULL + ? getenv("AMDUAT_MINIO_REGION") + : config.region; + config.prefix = getenv("AMDUAT_MINIO_PREFIX") != NULL + ? getenv("AMDUAT_MINIO_PREFIX") + : config.prefix; + + force_env = getenv("AMDUAT_MINIO_FORCE_PATH_STYLE"); + if (force_env != NULL) { + config.force_path_style = conformance_env_truthy(force_env); + } + + tls_env = getenv("AMDUAT_MINIO_TLS_VERIFY"); + if (tls_env != NULL) { + config.tls_verify = conformance_env_truthy(tls_env); + } + + timeout_env = getenv("AMDUAT_MINIO_TIMEOUT_MS"); + if (timeout_env != NULL) { + config.timeout_ms = (uint32_t)strtoul(timeout_env, NULL, 10); + } + + retry_env = getenv("AMDUAT_MINIO_RETRY_MAX"); + if (retry_env != NULL) { + config.retry_max = (uint32_t)strtoul(retry_env, NULL, 10); + } + + if (!amduat_asl_store_minio_init(&ctx->minio, config)) { + free(ctx); + return false; + } + + amduat_asl_store_init(out_store, config.store_config, + amduat_asl_store_minio_ops(), &ctx->minio); + *out_ctx = ctx; + return true; +} + static void conformance_free_ctx(void *ctx) { free(ctx); } @@ -766,7 +1030,8 @@ static int conformance_run_backend( const char *workdir, const amduat_conformance_fixture_t *fixtures, size_t fixtures_len, - const char *fixture_root) { + const char *fixture_root, + char **out_ref_hexes) { amduat_asl_store_t store; void *ctx = NULL; int rc = 1; @@ -778,7 +1043,8 @@ static int conformance_run_backend( return 1; } - if (conformance_test_put_get(&store, fixtures, fixtures_len, fixture_root) != 0 || + if (conformance_test_put_get(&store, fixtures, fixtures_len, fixture_root, + out_ref_hexes) != 0 || conformance_test_put_idempotent(&store, &fixtures[0], fixture_root) != 0 || conformance_test_get_not_found(&store) != 0) { rc = 1; @@ -786,6 +1052,12 @@ static int conformance_run_backend( rc = 0; } + if (rc == 0 && strcmp(backend->name, "asl_store_index_fs") == 0) { + if (conformance_test_replay_determinism(&store) != 0) { + rc = 1; + } + } + if (backend->cleanup != NULL) { backend->cleanup(ctx); } @@ -804,6 +1076,7 @@ int amduat_asl_store_conformance_run( {"asl_store_fs", false, conformance_init_asl_store_fs, conformance_free_ctx}, {"asl_store_index_fs", false, conformance_init_asl_store_index_fs, conformance_free_ctx}, + {"minio", false, conformance_init_asl_store_minio, conformance_free_ctx}, }; char *fixture_dir = NULL; char *manifest_path = NULL; @@ -813,6 +1086,8 @@ int amduat_asl_store_conformance_run( size_t i; size_t results_len = 0u; amduat_conformance_backend_result_t *results = NULL; + char **baseline_refs = NULL; + size_t baseline_len = 0u; if (out_results == NULL || out_len == NULL) { return 1; @@ -855,10 +1130,97 @@ int amduat_asl_store_conformance_run( if (backend->optional && !include_optional) { continue; } + if (strcmp(backend->name, "minio") == 0) { + const char *enable_env = getenv("AMDUAT_CONFORMANCE_ENABLE_MINIO"); + char *missing = NULL; + const char *endpoint_env = getenv("AMDUAT_MINIO_ENDPOINT"); + + if (!conformance_env_truthy(enable_env)) { + results = (amduat_conformance_backend_result_t *)realloc( + results, (results_len + 1u) * sizeof(*results)); + if (results == NULL) { + conformance_fixture_list_free(fixtures, fixtures_len); + free(fixture_dir); + free(manifest_path); + free(artifact_dir); + return 1; + } + results[results_len].backend = backend->name; + results[results_len].status = AMDUAT_CONFORMANCE_STATUS_SKIP; + results_len++; + if (verbose) { + fprintf(stdout, + "asl minio SKIPPED: set AMDUAT_CONFORMANCE_ENABLE_MINIO=1\n"); + } + continue; + } + + if (!conformance_minio_missing_envs(&missing)) { + conformance_fixture_list_free(fixtures, fixtures_len); + free(fixture_dir); + free(manifest_path); + free(artifact_dir); + free(results); + return 1; + } + if (missing != NULL) { + results = (amduat_conformance_backend_result_t *)realloc( + results, (results_len + 1u) * sizeof(*results)); + if (results == NULL) { + free(missing); + conformance_fixture_list_free(fixtures, fixtures_len); + free(fixture_dir); + free(manifest_path); + free(artifact_dir); + return 1; + } + results[results_len].backend = backend->name; + results[results_len].status = AMDUAT_CONFORMANCE_STATUS_SKIP; + results_len++; + if (verbose) { + fprintf(stdout, "asl minio SKIPPED: missing %s\n", missing); + } + free(missing); + continue; + } + + if (endpoint_env != NULL && strncmp(endpoint_env, "https://", 8u) == 0) { + results = (amduat_conformance_backend_result_t *)realloc( + results, (results_len + 1u) * sizeof(*results)); + if (results == NULL) { + conformance_fixture_list_free(fixtures, fixtures_len); + free(fixture_dir); + free(manifest_path); + free(artifact_dir); + return 1; + } + results[results_len].backend = backend->name; + results[results_len].status = AMDUAT_CONFORMANCE_STATUS_SKIP; + results_len++; + if (verbose) { + fprintf(stdout, + "asl minio SKIPPED: https endpoints not supported\n"); + } + continue; + } + } + + char **ref_hexes = + (char **)calloc(fixtures_len, sizeof(char *)); + if (ref_hexes == NULL) { + fprintf(stderr, "out of memory\n"); + conformance_fixture_list_free(fixtures, fixtures_len); + free(fixture_dir); + free(manifest_path); + free(artifact_dir); + free(results); + return 1; + } if (!conformance_prepare_workdir(workdir, backend->name, "asl", &backend_workdir)) { fprintf(stderr, "workdir prep failed for %s\n", backend->name); + conformance_free_ref_hexes(ref_hexes, fixtures_len); conformance_fixture_list_free(fixtures, fixtures_len); free(fixture_dir); free(manifest_path); @@ -881,11 +1243,44 @@ int amduat_asl_store_conformance_run( results[results_len].backend = backend->name; results[results_len].status = conformance_run_backend(backend, backend_workdir, fixtures, - fixtures_len, artifact_dir); + fixtures_len, artifact_dir, ref_hexes); if (verbose) { - fprintf(stdout, "asl %s %s\n", backend->name, - results[results_len].status == 0 ? "PASS" : "FAIL"); + const char *status = + results[results_len].status == AMDUAT_CONFORMANCE_STATUS_PASS + ? "PASS" + : (results[results_len].status == AMDUAT_CONFORMANCE_STATUS_SKIP + ? "SKIPPED" + : "FAIL"); + fprintf(stdout, "asl %s %s\n", backend->name, status); } + + if (results[results_len].status == + AMDUAT_CONFORMANCE_STATUS_PASS) { + if (baseline_refs == NULL) { + baseline_refs = ref_hexes; + baseline_len = fixtures_len; + ref_hexes = NULL; + } else { + size_t j; + if (baseline_len != fixtures_len) { + fprintf(stderr, "cross-backend ref length mismatch\n"); + results[results_len].status = 1; + } else { + for (j = 0; j < fixtures_len; ++j) { + if (baseline_refs[j] == NULL || ref_hexes[j] == NULL || + strcmp(baseline_refs[j], ref_hexes[j]) != 0) { + fprintf(stderr, + "cross-backend ref mismatch for fixture %s\n", + fixtures[j].id); + results[results_len].status = 1; + break; + } + } + } + } + } + + conformance_free_ref_hexes(ref_hexes, fixtures_len); results_len++; free(backend_workdir); } @@ -894,12 +1289,13 @@ int amduat_asl_store_conformance_run( free(fixture_dir); free(manifest_path); free(artifact_dir); + conformance_free_ref_hexes(baseline_refs, baseline_len); *out_results = results; *out_len = results_len; for (i = 0; i < results_len; ++i) { - if (results[i].status != 0) { + if (results[i].status == AMDUAT_CONFORMANCE_STATUS_FAIL) { return 1; } } diff --git a/tests/conformance/asl_store/test_asl_store_conformance.c b/tests/conformance/asl_store/test_asl_store_conformance.c index 7e0e71b..8e22458 100644 --- a/tests/conformance/asl_store/test_asl_store_conformance.c +++ b/tests/conformance/asl_store/test_asl_store_conformance.c @@ -76,7 +76,7 @@ int main(void) { } for (i = 0; i < results_len; ++i) { - if (results[i].status != 0) { + if (results[i].status == AMDUAT_CONFORMANCE_STATUS_FAIL) { rc = 1; } } diff --git a/tests/conformance/conformance_result.h b/tests/conformance/conformance_result.h index 45a23f0..b4c5746 100644 --- a/tests/conformance/conformance_result.h +++ b/tests/conformance/conformance_result.h @@ -12,6 +12,12 @@ typedef struct { int status; } amduat_conformance_backend_result_t; +enum { + AMDUAT_CONFORMANCE_STATUS_PASS = 0, + AMDUAT_CONFORMANCE_STATUS_FAIL = 1, + AMDUAT_CONFORMANCE_STATUS_SKIP = 2 +}; + #ifdef __cplusplus } /* extern "C" */ #endif diff --git a/tests/conformance/tgk_store/test_tgk_store_conformance.c b/tests/conformance/tgk_store/test_tgk_store_conformance.c index 705954a..5968920 100644 --- a/tests/conformance/tgk_store/test_tgk_store_conformance.c +++ b/tests/conformance/tgk_store/test_tgk_store_conformance.c @@ -76,7 +76,7 @@ int main(void) { } for (i = 0; i < results_len; ++i) { - if (results[i].status != 0) { + if (results[i].status == AMDUAT_CONFORMANCE_STATUS_FAIL) { rc = 1; } } diff --git a/tests/conformance/tgk_store/tgk_store_conformance.c b/tests/conformance/tgk_store/tgk_store_conformance.c index be25f2d..5070747 100644 --- a/tests/conformance/tgk_store/tgk_store_conformance.c +++ b/tests/conformance/tgk_store/tgk_store_conformance.c @@ -2,6 +2,8 @@ #include "amduat/asl/asl_store_fs.h" #include "amduat/asl/asl_store_index_fs.h" +#include "amduat/asl/ref_derive.h" +#include "amduat/asl/ref_text.h" #include "amduat/asl/store.h" #include "amduat/enc/asl1_core.h" #include "amduat/enc/tgk1_edge.h" @@ -24,13 +26,43 @@ #include #include +typedef struct { + char *id; + uint32_t type; + amduat_reference_t *from; + size_t from_len; + amduat_reference_t *to; + size_t to_len; + amduat_reference_t payload; + bool has_payload; + amduat_reference_t ref; + bool has_ref; +} amduat_tgk_fixture_edge_t; + typedef struct { const char *name; bool optional; - bool (*init)(const char *root, amduat_tgk_store_t *out_store, void **out_ctx); + bool (*init)(const char *root, void **out_ctx); + bool (*ingest)(void *ctx, + amduat_tgk_fixture_edge_t *edges, + size_t edges_len); + bool (*build_store)(void *ctx, amduat_tgk_store_t *out_store); void (*cleanup)(void *ctx); } amduat_tgk_conformance_backend_t; +typedef struct { + amduat_reference_t *nodes; + size_t nodes_len; + amduat_reference_t *scan_edges; + size_t scan_edges_len; + amduat_reference_t **edges_from; + size_t *edges_from_len; + amduat_reference_t **edges_to; + size_t *edges_to_len; + amduat_reference_t **edges_incident; + size_t *edges_incident_len; +} amduat_tgk_equivalence_snapshot_t; + typedef struct { amduat_tgk_store_config_t config; amduat_tgk_identity_domain_t domains[1]; @@ -38,6 +70,7 @@ typedef struct { amduat_tgk_edge_type_id_t edge_types[1]; amduat_asl_encoding_profile_id_t encodings[1]; amduat_tgk_store_mem_t mem; + amduat_tgk_store_t store; } amduat_tgk_mem_ctx_t; typedef struct { @@ -48,7 +81,9 @@ typedef struct { amduat_asl_encoding_profile_id_t encodings[1]; amduat_asl_store_fs_t asl_fs; amduat_asl_store_t asl_store; + char *manifest_path; amduat_tgk_store_fs_t fs; + amduat_tgk_store_t store; } amduat_tgk_fs_ctx_t; typedef struct { @@ -58,7 +93,9 @@ typedef struct { amduat_tgk_edge_type_id_t edge_types[1]; amduat_asl_encoding_profile_id_t encodings[1]; amduat_asl_store_index_fs_t asl_fs; + amduat_asl_store_t asl_store; amduat_tgk_store_asl_index_fs_t tgk; + amduat_tgk_store_t store; } amduat_tgk_asl_index_ctx_t; static bool conformance_join_path(const char *base, @@ -270,6 +307,580 @@ static bool conformance_backend_selected(const char *selection, return false; } +static bool conformance_read_file(const char *path, + uint8_t **out_bytes, + size_t *out_len) { + FILE *fp; + long size; + uint8_t *buffer; + size_t read_len; + + if (path == NULL || out_bytes == NULL || out_len == NULL) { + return false; + } + *out_bytes = NULL; + *out_len = 0u; + + fp = fopen(path, "rb"); + if (fp == NULL) { + return false; + } + if (fseek(fp, 0, SEEK_END) != 0) { + fclose(fp); + return false; + } + size = ftell(fp); + if (size < 0) { + fclose(fp); + return false; + } + if (fseek(fp, 0, SEEK_SET) != 0) { + fclose(fp); + return false; + } + + buffer = (uint8_t *)malloc((size_t)size + 1u); + if (buffer == NULL) { + fclose(fp); + return false; + } + read_len = fread(buffer, 1u, (size_t)size, fp); + fclose(fp); + if (read_len != (size_t)size) { + free(buffer); + return false; + } + buffer[size] = '\0'; + + *out_bytes = buffer; + *out_len = (size_t)size; + return true; +} + +static void conformance_skip_ws(const char **cursor) { + while (*cursor != NULL && **cursor != '\0' && + isspace((unsigned char)**cursor)) { + (*cursor)++; + } +} + +static bool conformance_parse_string(const char **cursor, char **out_text) { + const char *start; + const char *end; + size_t len; + char *buffer; + + if (cursor == NULL || out_text == NULL) { + return false; + } + conformance_skip_ws(cursor); + if (**cursor != '"') { + return false; + } + (*cursor)++; + start = *cursor; + end = strchr(start, '"'); + if (end == NULL) { + return false; + } + len = (size_t)(end - start); + buffer = (char *)malloc(len + 1u); + if (buffer == NULL) { + return false; + } + memcpy(buffer, start, len); + buffer[len] = '\0'; + *cursor = end + 1; + *out_text = buffer; + return true; +} + +static bool conformance_parse_uint(const char **cursor, uint32_t *out_value) { + char *end; + unsigned long value; + + if (cursor == NULL || out_value == NULL) { + return false; + } + conformance_skip_ws(cursor); + if (**cursor == '\0') { + return false; + } + value = strtoul(*cursor, &end, 10); + if (end == *cursor) { + return false; + } + *out_value = (uint32_t)value; + *cursor = end; + return true; +} + +static bool conformance_parse_string_array(const char **cursor, + char ***out_items, + size_t *out_len) { + char **items = NULL; + size_t items_len = 0u; + size_t items_cap = 0u; + + if (cursor == NULL || out_items == NULL || out_len == NULL) { + return false; + } + *out_items = NULL; + *out_len = 0u; + + conformance_skip_ws(cursor); + if (**cursor != '[') { + return false; + } + (*cursor)++; + + while (true) { + char *value = NULL; + conformance_skip_ws(cursor); + if (**cursor == ']') { + (*cursor)++; + break; + } + + if (!conformance_parse_string(cursor, &value)) { + goto fail; + } + + if (items_len == items_cap) { + size_t next_cap = items_cap == 0u ? 4u : items_cap * 2u; + char **next = (char **)realloc(items, next_cap * sizeof(*items)); + if (next == NULL) { + free(value); + goto fail; + } + items = next; + items_cap = next_cap; + } + items[items_len++] = value; + + conformance_skip_ws(cursor); + if (**cursor == ',') { + (*cursor)++; + continue; + } + if (**cursor == ']') { + (*cursor)++; + break; + } + } + + *out_items = items; + *out_len = items_len; + return true; + +fail: + if (items != NULL) { + size_t i; + for (i = 0; i < items_len; ++i) { + free(items[i]); + } + } + free(items); + return false; +} + +static bool conformance_skip_value(const char **cursor) { + const char *c; + int depth; + + if (cursor == NULL) { + return false; + } + conformance_skip_ws(cursor); + c = *cursor; + + if (*c == '"') { + char *tmp = NULL; + bool ok = conformance_parse_string(cursor, &tmp); + free(tmp); + return ok; + } + if (*c == '{') { + depth = 1; + c++; + while (*c != '\0' && depth > 0) { + if (*c == '{') { + depth++; + } else if (*c == '}') { + depth--; + } + c++; + } + *cursor = c; + return depth == 0; + } + if (*c == '[') { + depth = 1; + c++; + while (*c != '\0' && depth > 0) { + if (*c == '[') { + depth++; + } else if (*c == ']') { + depth--; + } + c++; + } + *cursor = c; + return depth == 0; + } + + while (*c != '\0' && !isspace((unsigned char)*c) && *c != ',' && *c != '}' && + *c != ']') { + c++; + } + *cursor = c; + return true; +} + +static void conformance_free_fixture_edge(amduat_tgk_fixture_edge_t *edge) { + size_t i; + + if (edge == NULL) { + return; + } + free(edge->id); + edge->id = NULL; + if (edge->from != NULL) { + for (i = 0; i < edge->from_len; ++i) { + amduat_reference_free(&edge->from[i]); + } + } + free(edge->from); + edge->from = NULL; + edge->from_len = 0u; + if (edge->to != NULL) { + for (i = 0; i < edge->to_len; ++i) { + amduat_reference_free(&edge->to[i]); + } + } + free(edge->to); + edge->to = NULL; + edge->to_len = 0u; + if (edge->has_payload) { + amduat_reference_free(&edge->payload); + edge->has_payload = false; + } + if (edge->has_ref) { + amduat_reference_free(&edge->ref); + edge->has_ref = false; + } +} + +static void conformance_free_fixture_edges(amduat_tgk_fixture_edge_t *edges, + size_t edges_len) { + size_t i; + + if (edges == NULL) { + return; + } + for (i = 0; i < edges_len; ++i) { + conformance_free_fixture_edge(&edges[i]); + } + free(edges); +} + +static void conformance_clear_fixture_refs(amduat_tgk_fixture_edge_t *edges, + size_t edges_len) { + size_t i; + + if (edges == NULL) { + return; + } + for (i = 0; i < edges_len; ++i) { + if (edges[i].has_ref) { + amduat_reference_free(&edges[i].ref); + edges[i].has_ref = false; + } + } +} + +static bool conformance_parse_ref_list(char **items, + size_t items_len, + amduat_reference_t **out_refs, + size_t *out_len) { + amduat_reference_t *refs; + size_t i; + + if (out_refs == NULL || out_len == NULL) { + return false; + } + *out_refs = NULL; + *out_len = 0u; + + if (items_len == 0u) { + return true; + } + + refs = (amduat_reference_t *)calloc(items_len, sizeof(*refs)); + if (refs == NULL) { + return false; + } + for (i = 0; i < items_len; ++i) { + if (!amduat_asl_ref_decode_hex(items[i], &refs[i])) { + size_t j; + for (j = 0; j < i; ++j) { + amduat_reference_free(&refs[j]); + } + free(refs); + return false; + } + } + + *out_refs = refs; + *out_len = items_len; + return true; +} + +static bool conformance_parse_tgk_fixtures( + const char *manifest_path, + amduat_tgk_fixture_edge_t **out_edges, + size_t *out_len) { + uint8_t *bytes = NULL; + size_t len = 0u; + const char *cursor; + const char *edges_key; + const char *array_start; + amduat_tgk_fixture_edge_t *edges = NULL; + size_t edges_len = 0u; + size_t edges_cap = 0u; + + if (out_edges == NULL || out_len == NULL || manifest_path == NULL) { + return false; + } + *out_edges = NULL; + *out_len = 0u; + + if (!conformance_read_file(manifest_path, &bytes, &len)) { + return false; + } + + cursor = (const char *)bytes; + edges_key = strstr(cursor, "\"edges\""); + if (edges_key == NULL) { + free(bytes); + return false; + } + array_start = strchr(edges_key, '['); + if (array_start == NULL) { + free(bytes); + return false; + } + cursor = array_start + 1; + + while (true) { + amduat_tgk_fixture_edge_t edge; + char *key = NULL; + + memset(&edge, 0, sizeof(edge)); + + conformance_skip_ws(&cursor); + if (*cursor == '\0' || *cursor == ']') { + break; + } + if (*cursor != '{') { + break; + } + cursor++; + + while (true) { + conformance_skip_ws(&cursor); + if (*cursor == '}') { + cursor++; + break; + } + + if (!conformance_parse_string(&cursor, &key)) { + conformance_free_fixture_edge(&edge); + free(bytes); + conformance_free_fixture_edges(edges, edges_len); + return false; + } + conformance_skip_ws(&cursor); + if (*cursor != ':') { + free(key); + conformance_free_fixture_edge(&edge); + free(bytes); + conformance_free_fixture_edges(edges, edges_len); + return false; + } + cursor++; + + if (strcmp(key, "id") == 0) { + if (!conformance_parse_string(&cursor, &edge.id)) { + free(key); + conformance_free_fixture_edge(&edge); + free(bytes); + conformance_free_fixture_edges(edges, edges_len); + return false; + } + } else if (strcmp(key, "type") == 0) { + if (!conformance_parse_uint(&cursor, &edge.type)) { + free(key); + conformance_free_fixture_edge(&edge); + free(bytes); + conformance_free_fixture_edges(edges, edges_len); + return false; + } + } else if (strcmp(key, "payload") == 0) { + char *payload_hex = NULL; + if (!conformance_parse_string(&cursor, &payload_hex)) { + free(key); + conformance_free_fixture_edge(&edge); + free(bytes); + conformance_free_fixture_edges(edges, edges_len); + return false; + } + if (!amduat_asl_ref_decode_hex(payload_hex, &edge.payload)) { + free(payload_hex); + free(key); + conformance_free_fixture_edge(&edge); + free(bytes); + conformance_free_fixture_edges(edges, edges_len); + return false; + } + edge.has_payload = true; + free(payload_hex); + } else if (strcmp(key, "from") == 0 || strcmp(key, "to") == 0) { + char **items = NULL; + size_t items_len = 0u; + amduat_reference_t *refs = NULL; + size_t refs_len = 0u; + + if (!conformance_parse_string_array(&cursor, &items, &items_len)) { + free(key); + conformance_free_fixture_edge(&edge); + free(bytes); + conformance_free_fixture_edges(edges, edges_len); + return false; + } + + if (!conformance_parse_ref_list(items, items_len, &refs, &refs_len)) { + size_t i; + for (i = 0; i < items_len; ++i) { + free(items[i]); + } + free(items); + free(key); + conformance_free_fixture_edge(&edge); + free(bytes); + conformance_free_fixture_edges(edges, edges_len); + return false; + } + + if (strcmp(key, "from") == 0) { + edge.from = refs; + edge.from_len = refs_len; + } else { + edge.to = refs; + edge.to_len = refs_len; + } + + if (items != NULL) { + size_t i; + for (i = 0; i < items_len; ++i) { + free(items[i]); + } + } + free(items); + } else { + if (!conformance_skip_value(&cursor)) { + free(key); + conformance_free_fixture_edge(&edge); + free(bytes); + conformance_free_fixture_edges(edges, edges_len); + return false; + } + } + + free(key); + key = NULL; + + conformance_skip_ws(&cursor); + if (*cursor == ',') { + cursor++; + continue; + } + if (*cursor == '}') { + cursor++; + break; + } + } + + if (edge.id == NULL || edge.from_len == 0u || edge.to_len == 0u || + !edge.has_payload) { + conformance_free_fixture_edge(&edge); + free(bytes); + conformance_free_fixture_edges(edges, edges_len); + return false; + } + + if (edges_len == edges_cap) { + size_t next_cap = edges_cap == 0u ? 4u : edges_cap * 2u; + amduat_tgk_fixture_edge_t *next = + (amduat_tgk_fixture_edge_t *)realloc(edges, + next_cap * sizeof(*edges)); + if (next == NULL) { + conformance_free_fixture_edge(&edge); + free(bytes); + conformance_free_fixture_edges(edges, edges_len); + return false; + } + edges = next; + edges_cap = next_cap; + } + + edges[edges_len++] = edge; + + conformance_skip_ws(&cursor); + if (*cursor == ',') { + cursor++; + continue; + } + if (*cursor == ']') { + break; + } + } + + free(bytes); + *out_edges = edges; + *out_len = edges_len; + return edges_len > 0u; +} + +static bool conformance_load_tgk_fixtures( + const char *fixtures_root, + amduat_tgk_fixture_edge_t **out_edges, + size_t *out_len) { + char *fixture_dir = NULL; + char *manifest_path = NULL; + bool ok; + + if (fixtures_root == NULL || out_edges == NULL || out_len == NULL) { + return false; + } + + if (!conformance_join_path(fixtures_root, "tgk_edges", &fixture_dir)) { + return false; + } + if (!conformance_join_path(fixture_dir, "manifest.json", &manifest_path)) { + free(fixture_dir); + return false; + } + + ok = conformance_parse_tgk_fixtures(manifest_path, out_edges, out_len); + if (!ok) { + fprintf(stderr, "failed to parse tgk fixtures: %s\n", manifest_path); + } + free(fixture_dir); + free(manifest_path); + return ok; +} + static void conformance_init_tgk_config(amduat_tgk_store_config_t *config, amduat_tgk_identity_domain_t *domains, uint32_t *edge_tags, @@ -293,13 +904,107 @@ static void conformance_init_tgk_config(amduat_tgk_store_config_t *config, config->tgk_profiles.encodings_len = 1; } -static bool conformance_init_tgk_store_mem(const char *root, - amduat_tgk_store_t *out_store, - void **out_ctx) { +static bool conformance_encode_edge_artifact( + const amduat_tgk_fixture_edge_t *edge, + amduat_artifact_t *out_artifact) { + amduat_tgk_edge_body_t body; + amduat_octets_t encoded = amduat_octets(NULL, 0); + + if (edge == NULL || out_artifact == NULL) { + return false; + } + + memset(&body, 0, sizeof(body)); + body.type = edge->type; + body.from = edge->from; + body.from_len = edge->from_len; + body.to = edge->to; + body.to_len = edge->to_len; + body.payload = edge->payload; + + if (!amduat_enc_tgk1_edge_encode_v1(&body, &encoded)) { + return false; + } + + *out_artifact = amduat_artifact_with_type(encoded, + amduat_type_tag(TYPE_TAG_TGK1_EDGE_V1)); + return true; +} + +static bool conformance_ingest_edge_mem(amduat_tgk_store_t *store, + amduat_tgk_fixture_edge_t *edge) { + amduat_artifact_t artifact; + amduat_reference_t ref; + amduat_octets_t artifact_bytes = amduat_octets(NULL, 0); + + if (!conformance_encode_edge_artifact(edge, &artifact)) { + return false; + } + + if (!amduat_asl_ref_derive(artifact, AMDUAT_ENC_ASL1_CORE_V1, + AMDUAT_HASH_ASL1_ID_SHA256, &ref, + &artifact_bytes)) { + amduat_artifact_free(&artifact); + return false; + } + amduat_octets_free(&artifact_bytes); + + if (!amduat_tgk_store_ingest_artifact(store, ref, artifact)) { + amduat_reference_free(&ref); + amduat_artifact_free(&artifact); + return false; + } + + edge->ref = ref; + edge->has_ref = true; + amduat_artifact_free(&artifact); + return true; +} + +static bool conformance_ingest_edge_asl_store(amduat_asl_store_t *store, + amduat_tgk_fixture_edge_t *edge, + FILE *manifest_stream) { + amduat_artifact_t artifact; + amduat_reference_t ref; + amduat_asl_store_error_t err; + + if (!conformance_encode_edge_artifact(edge, &artifact)) { + return false; + } + + err = amduat_asl_store_put(store, artifact, &ref); + if (err != AMDUAT_ASL_STORE_OK) { + amduat_artifact_free(&artifact); + return false; + } + + if (manifest_stream != NULL) { + char *hex = NULL; + if (!amduat_asl_ref_encode_hex(ref, &hex)) { + amduat_reference_free(&ref); + amduat_artifact_free(&artifact); + return false; + } + if (fprintf(manifest_stream, "%s\n", hex) < 0) { + free(hex); + amduat_reference_free(&ref); + amduat_artifact_free(&artifact); + return false; + } + free(hex); + } + + edge->ref = ref; + edge->has_ref = true; + amduat_artifact_free(&artifact); + return true; +} + +static bool conformance_init_tgk_store_mem(const char *root, void **out_ctx) { amduat_tgk_mem_ctx_t *ctx; (void)root; - if (out_store == NULL || out_ctx == NULL) { + if (out_ctx == NULL) { return false; } @@ -315,29 +1020,50 @@ static bool conformance_init_tgk_store_mem(const char *root, return false; } - amduat_tgk_store_init(out_store, ctx->config, amduat_tgk_store_mem_ops(), + amduat_tgk_store_init(&ctx->store, ctx->config, amduat_tgk_store_mem_ops(), &ctx->mem); + *out_ctx = ctx; return true; } -static bool conformance_write_empty_manifest(const char *path) { - FILE *stream = fopen(path, "wb"); - if (stream == NULL) { +static bool conformance_ingest_tgk_store_mem( + void *ctx, + amduat_tgk_fixture_edge_t *edges, + size_t edges_len) { + amduat_tgk_mem_ctx_t *mem = (amduat_tgk_mem_ctx_t *)ctx; + size_t i; + + if (mem == NULL) { return false; } - return fclose(stream) == 0; + + for (i = 0; i < edges_len; ++i) { + if (!conformance_ingest_edge_mem(&mem->store, &edges[i])) { + return false; + } + } + + return true; } -static bool conformance_init_tgk_store_fs(const char *root, - amduat_tgk_store_t *out_store, - void **out_ctx) { +static bool conformance_build_tgk_store_mem(void *ctx, + amduat_tgk_store_t *out_store) { + amduat_tgk_mem_ctx_t *mem = (amduat_tgk_mem_ctx_t *)ctx; + if (mem == NULL || out_store == NULL) { + return false; + } + *out_store = mem->store; + return true; +} + +static bool conformance_init_tgk_store_fs(const char *root, void **out_ctx) { amduat_tgk_fs_ctx_t *ctx; amduat_asl_store_config_t asl_config; char *asl_root = NULL; char *manifest_path = NULL; - if (out_store == NULL || out_ctx == NULL) { + if (out_ctx == NULL) { return false; } @@ -364,13 +1090,6 @@ static bool conformance_init_tgk_store_fs(const char *root, return false; } - if (!conformance_write_empty_manifest(manifest_path)) { - free(asl_root); - free(manifest_path); - free(ctx); - return false; - } - memset(&asl_config, 0, sizeof(asl_config)); asl_config.encoding_profile_id = AMDUAT_ENC_ASL1_CORE_V1; asl_config.hash_id = AMDUAT_HASH_ASL1_ID_SHA256; @@ -383,32 +1102,67 @@ static bool conformance_init_tgk_store_fs(const char *root, amduat_asl_store_init(&ctx->asl_store, asl_config, amduat_asl_store_fs_ops(), &ctx->asl_fs); - if (!amduat_tgk_store_fs_init(&ctx->fs, ctx->config, manifest_path, - AMDUAT_FORMAT_REF_HEX, &ctx->asl_store)) { - free(asl_root); - free(manifest_path); - free(ctx); - return false; - } - - amduat_tgk_store_init(out_store, ctx->config, amduat_tgk_store_fs_ops(), - &ctx->fs); - *out_ctx = ctx; - + ctx->manifest_path = manifest_path; free(asl_root); - free(manifest_path); + + *out_ctx = ctx; return true; } -static bool conformance_init_tgk_store_asl_index_fs( - const char *root, - amduat_tgk_store_t *out_store, - void **out_ctx) { +static bool conformance_ingest_tgk_store_fs( + void *ctx, + amduat_tgk_fixture_edge_t *edges, + size_t edges_len) { + amduat_tgk_fs_ctx_t *fs = (amduat_tgk_fs_ctx_t *)ctx; + FILE *manifest_stream; + size_t i; + + if (fs == NULL || fs->manifest_path == NULL) { + return false; + } + + manifest_stream = fopen(fs->manifest_path, "wb"); + if (manifest_stream == NULL) { + return false; + } + + for (i = 0; i < edges_len; ++i) { + if (!conformance_ingest_edge_asl_store(&fs->asl_store, &edges[i], + manifest_stream)) { + fclose(manifest_stream); + return false; + } + } + + return fclose(manifest_stream) == 0; +} + +static bool conformance_build_tgk_store_fs(void *ctx, + amduat_tgk_store_t *out_store) { + amduat_tgk_fs_ctx_t *fs = (amduat_tgk_fs_ctx_t *)ctx; + + if (fs == NULL || out_store == NULL || fs->manifest_path == NULL) { + return false; + } + + if (!amduat_tgk_store_fs_init(&fs->fs, fs->config, fs->manifest_path, + AMDUAT_FORMAT_REF_HEX, &fs->asl_store)) { + return false; + } + + amduat_tgk_store_init(out_store, fs->config, amduat_tgk_store_fs_ops(), + &fs->fs); + fs->store = *out_store; + return true; +} + +static bool conformance_init_tgk_store_asl_index_fs(const char *root, + void **out_ctx) { amduat_tgk_asl_index_ctx_t *ctx; amduat_asl_store_config_t asl_config; char *asl_root = NULL; - if (out_store == NULL || out_ctx == NULL) { + if (out_ctx == NULL) { return false; } @@ -438,18 +1192,52 @@ static bool conformance_init_tgk_store_asl_index_fs( free(ctx); return false; } + amduat_asl_store_init(&ctx->asl_store, asl_config, + amduat_asl_store_index_fs_ops(), &ctx->asl_fs); - if (!amduat_tgk_store_asl_index_fs_init(&ctx->tgk, ctx->config, &ctx->asl_fs)) { - free(asl_root); - free(ctx); + free(asl_root); + + *out_ctx = ctx; + return true; +} + +static bool conformance_ingest_tgk_store_asl_index_fs( + void *ctx, + amduat_tgk_fixture_edge_t *edges, + size_t edges_len) { + amduat_tgk_asl_index_ctx_t *tgk = (amduat_tgk_asl_index_ctx_t *)ctx; + size_t i; + + if (tgk == NULL) { return false; } - amduat_tgk_store_init(out_store, ctx->config, - amduat_tgk_store_asl_index_fs_ops(), &ctx->tgk); - *out_ctx = ctx; + for (i = 0; i < edges_len; ++i) { + if (!conformance_ingest_edge_asl_store(&tgk->asl_store, &edges[i], NULL)) { + return false; + } + } - free(asl_root); + return true; +} + +static bool conformance_build_tgk_store_asl_index_fs( + void *ctx, + amduat_tgk_store_t *out_store) { + amduat_tgk_asl_index_ctx_t *tgk = (amduat_tgk_asl_index_ctx_t *)ctx; + + if (tgk == NULL || out_store == NULL) { + return false; + } + + if (!amduat_tgk_store_asl_index_fs_init(&tgk->tgk, tgk->config, + &tgk->asl_fs)) { + return false; + } + + amduat_tgk_store_init(out_store, tgk->config, + amduat_tgk_store_asl_index_fs_ops(), &tgk->tgk); + tgk->store = *out_store; return true; } @@ -468,6 +1256,7 @@ static void conformance_free_tgk_fs(void *ctx) { return; } amduat_tgk_store_fs_free(&fs->fs); + free(fs->manifest_path); free(fs); } @@ -480,12 +1269,698 @@ static void conformance_free_tgk_asl_index(void *ctx) { free(tgk); } +static int conformance_ref_cmp(const void *left, const void *right) { + const amduat_reference_t *a = (const amduat_reference_t *)left; + const amduat_reference_t *b = (const amduat_reference_t *)right; + size_t min_len; + int cmp; + + if (a->hash_id != b->hash_id) { + return (a->hash_id < b->hash_id) ? -1 : 1; + } + + min_len = a->digest.len < b->digest.len ? a->digest.len : b->digest.len; + cmp = memcmp(a->digest.data, b->digest.data, min_len); + if (cmp != 0) { + return cmp; + } + if (a->digest.len == b->digest.len) { + return 0; + } + return (a->digest.len < b->digest.len) ? -1 : 1; +} + +static void conformance_free_ref_list(amduat_reference_t *refs, size_t len) { + size_t i; + + if (refs == NULL) { + return; + } + for (i = 0; i < len; ++i) { + amduat_reference_free(&refs[i]); + } + free(refs); +} + +static bool conformance_clone_edge_refs( + const amduat_tgk_graph_edge_view_list_t *edges, + amduat_reference_t **out_refs, + size_t *out_len) { + amduat_reference_t *refs; + size_t i; + + if (edges == NULL || out_refs == NULL || out_len == NULL) { + return false; + } + *out_refs = NULL; + *out_len = 0u; + + if (edges->len == 0u) { + return true; + } + + refs = (amduat_reference_t *)calloc(edges->len, sizeof(*refs)); + if (refs == NULL) { + return false; + } + + for (i = 0; i < edges->len; ++i) { + if (!amduat_reference_clone(edges->edges[i].edge_ref, &refs[i])) { + size_t j; + for (j = 0; j < i; ++j) { + amduat_reference_free(&refs[j]); + } + free(refs); + return false; + } + } + + *out_refs = refs; + *out_len = edges->len; + return true; +} + +static bool conformance_clone_scan_refs( + const amduat_tgk_graph_scan_result_t *scan, + amduat_reference_t **out_refs, + size_t *out_len) { + return conformance_clone_edge_refs(&scan->edges, out_refs, out_len); +} + +static void conformance_free_equivalence_snapshot( + amduat_tgk_equivalence_snapshot_t *snapshot) { + size_t i; + + if (snapshot == NULL) { + return; + } + conformance_free_ref_list(snapshot->nodes, snapshot->nodes_len); + conformance_free_ref_list(snapshot->scan_edges, snapshot->scan_edges_len); + + if (snapshot->edges_from != NULL) { + for (i = 0; i < snapshot->nodes_len; ++i) { + conformance_free_ref_list(snapshot->edges_from[i], + snapshot->edges_from_len[i]); + } + } + if (snapshot->edges_to != NULL) { + for (i = 0; i < snapshot->nodes_len; ++i) { + conformance_free_ref_list(snapshot->edges_to[i], + snapshot->edges_to_len[i]); + } + } + if (snapshot->edges_incident != NULL) { + for (i = 0; i < snapshot->nodes_len; ++i) { + conformance_free_ref_list(snapshot->edges_incident[i], + snapshot->edges_incident_len[i]); + } + } + + free(snapshot->edges_from); + free(snapshot->edges_from_len); + free(snapshot->edges_to); + free(snapshot->edges_to_len); + free(snapshot->edges_incident); + free(snapshot->edges_incident_len); + + memset(snapshot, 0, sizeof(*snapshot)); +} + +static bool conformance_build_equivalence_snapshot( + amduat_tgk_store_t *store, + const amduat_tgk_fixture_edge_t *edges, + size_t edges_len, + amduat_tgk_equivalence_snapshot_t *out_snapshot) { + amduat_reference_t *nodes = NULL; + size_t nodes_len = 0u; + size_t nodes_cap = 0u; + size_t i; + amduat_tgk_edge_type_filter_t filter; + amduat_tgk_graph_scan_result_t scan; + + if (store == NULL || out_snapshot == NULL) { + return false; + } + memset(out_snapshot, 0, sizeof(*out_snapshot)); + + for (i = 0; i < edges_len; ++i) { + size_t j; + for (j = 0; j < edges[i].from_len; ++j) { + if (!conformance_collect_node(edges[i].from[j], &nodes, &nodes_len, + &nodes_cap)) { + conformance_free_ref_list(nodes, nodes_len); + return false; + } + } + for (j = 0; j < edges[i].to_len; ++j) { + if (!conformance_collect_node(edges[i].to[j], &nodes, &nodes_len, + &nodes_cap)) { + conformance_free_ref_list(nodes, nodes_len); + return false; + } + } + } + + qsort(nodes, nodes_len, sizeof(*nodes), conformance_ref_cmp); + + filter.types = NULL; + filter.types_len = 0u; + memset(&scan, 0, sizeof(scan)); + if (!amduat_tgk_store_scan_edges(store, filter, amduat_octets(NULL, 0), false, + &scan)) { + conformance_free_ref_list(nodes, nodes_len); + return false; + } + + if (!conformance_clone_scan_refs(&scan, &out_snapshot->scan_edges, + &out_snapshot->scan_edges_len)) { + amduat_tgk_graph_scan_result_free(&scan); + conformance_free_ref_list(nodes, nodes_len); + return false; + } + amduat_tgk_graph_scan_result_free(&scan); + + out_snapshot->nodes = nodes; + out_snapshot->nodes_len = nodes_len; + + out_snapshot->edges_from = + (amduat_reference_t **)calloc(nodes_len, sizeof(*out_snapshot->edges_from)); + out_snapshot->edges_from_len = + (size_t *)calloc(nodes_len, sizeof(*out_snapshot->edges_from_len)); + out_snapshot->edges_to = + (amduat_reference_t **)calloc(nodes_len, sizeof(*out_snapshot->edges_to)); + out_snapshot->edges_to_len = + (size_t *)calloc(nodes_len, sizeof(*out_snapshot->edges_to_len)); + out_snapshot->edges_incident = + (amduat_reference_t **)calloc(nodes_len, + sizeof(*out_snapshot->edges_incident)); + out_snapshot->edges_incident_len = + (size_t *)calloc(nodes_len, sizeof(*out_snapshot->edges_incident_len)); + + if (out_snapshot->edges_from == NULL || out_snapshot->edges_from_len == NULL || + out_snapshot->edges_to == NULL || out_snapshot->edges_to_len == NULL || + out_snapshot->edges_incident == NULL || + out_snapshot->edges_incident_len == NULL) { + conformance_free_equivalence_snapshot(out_snapshot); + return false; + } + + for (i = 0; i < nodes_len; ++i) { + amduat_tgk_graph_edge_view_list_t list; + + memset(&list, 0, sizeof(list)); + if (!amduat_tgk_store_edges_from(store, nodes[i], filter, &list)) { + conformance_free_equivalence_snapshot(out_snapshot); + return false; + } + if (!conformance_clone_edge_refs(&list, &out_snapshot->edges_from[i], + &out_snapshot->edges_from_len[i])) { + amduat_tgk_graph_edge_view_list_free(&list); + conformance_free_equivalence_snapshot(out_snapshot); + return false; + } + amduat_tgk_graph_edge_view_list_free(&list); + + memset(&list, 0, sizeof(list)); + if (!amduat_tgk_store_edges_to(store, nodes[i], filter, &list)) { + conformance_free_equivalence_snapshot(out_snapshot); + return false; + } + if (!conformance_clone_edge_refs(&list, &out_snapshot->edges_to[i], + &out_snapshot->edges_to_len[i])) { + amduat_tgk_graph_edge_view_list_free(&list); + conformance_free_equivalence_snapshot(out_snapshot); + return false; + } + amduat_tgk_graph_edge_view_list_free(&list); + + memset(&list, 0, sizeof(list)); + if (!amduat_tgk_store_edges_incident(store, nodes[i], filter, &list)) { + conformance_free_equivalence_snapshot(out_snapshot); + return false; + } + if (!conformance_clone_edge_refs(&list, &out_snapshot->edges_incident[i], + &out_snapshot->edges_incident_len[i])) { + amduat_tgk_graph_edge_view_list_free(&list); + conformance_free_equivalence_snapshot(out_snapshot); + return false; + } + amduat_tgk_graph_edge_view_list_free(&list); + } + + return true; +} + +static int conformance_compare_equivalence_snapshot( + const amduat_tgk_equivalence_snapshot_t *baseline, + const amduat_tgk_equivalence_snapshot_t *current) { + size_t i; + + if (baseline == NULL || current == NULL) { + return 1; + } + if (baseline->nodes_len != current->nodes_len || + baseline->scan_edges_len != current->scan_edges_len) { + return 1; + } + for (i = 0; i < baseline->nodes_len; ++i) { + if (!amduat_reference_eq(baseline->nodes[i], current->nodes[i])) { + return 1; + } + if (baseline->edges_from_len[i] != current->edges_from_len[i] || + baseline->edges_to_len[i] != current->edges_to_len[i] || + baseline->edges_incident_len[i] != current->edges_incident_len[i]) { + return 1; + } + for (size_t j = 0; j < baseline->edges_from_len[i]; ++j) { + if (!amduat_reference_eq(baseline->edges_from[i][j], + current->edges_from[i][j])) { + return 1; + } + } + for (size_t j = 0; j < baseline->edges_to_len[i]; ++j) { + if (!amduat_reference_eq(baseline->edges_to[i][j], + current->edges_to[i][j])) { + return 1; + } + } + for (size_t j = 0; j < baseline->edges_incident_len[i]; ++j) { + if (!amduat_reference_eq(baseline->edges_incident[i][j], + current->edges_incident[i][j])) { + return 1; + } + } + } + + for (i = 0; i < baseline->scan_edges_len; ++i) { + if (!amduat_reference_eq(baseline->scan_edges[i], + current->scan_edges[i])) { + return 1; + } + } + + return 0; +} + +static bool conformance_ref_list_sorted( + const amduat_tgk_graph_edge_view_list_t *edges) { + size_t i; + + if (edges == NULL || edges->len < 2u) { + return true; + } + + for (i = 1; i < edges->len; ++i) { + if (conformance_ref_cmp(&edges->edges[i - 1u].edge_ref, + &edges->edges[i].edge_ref) > 0) { + return false; + } + } + + return true; +} + +static bool conformance_collect_node(amduat_reference_t node, + amduat_reference_t **nodes, + size_t *nodes_len, + size_t *nodes_cap) { + size_t i; + amduat_reference_t *out_nodes; + + for (i = 0; i < *nodes_len; ++i) { + if (amduat_reference_eq((*nodes)[i], node)) { + return true; + } + } + + if (*nodes_len == *nodes_cap) { + size_t next_cap = *nodes_cap == 0u ? 4u : (*nodes_cap) * 2u; + out_nodes = (amduat_reference_t *)realloc(*nodes, + next_cap * sizeof(**nodes)); + if (out_nodes == NULL) { + return false; + } + *nodes = out_nodes; + *nodes_cap = next_cap; + } + + if (!amduat_reference_clone(node, &(*nodes)[*nodes_len])) { + return false; + } + (*nodes_len)++; + return true; +} + +static bool conformance_node_in_list(const amduat_reference_t *list, + size_t list_len, + amduat_reference_t node) { + size_t i; + for (i = 0; i < list_len; ++i) { + if (amduat_reference_eq(list[i], node)) { + return true; + } + } + return false; +} + +static bool conformance_collect_expected_edges( + const amduat_tgk_fixture_edge_t *edges, + size_t edges_len, + amduat_reference_t node, + amduat_tgk_graph_direction_t direction, + amduat_reference_t **out_refs, + size_t *out_len) { + amduat_reference_t *refs = NULL; + size_t refs_len = 0u; + size_t refs_cap = 0u; + size_t i; + + if (out_refs == NULL || out_len == NULL) { + return false; + } + *out_refs = NULL; + *out_len = 0u; + + for (i = 0; i < edges_len; ++i) { + bool include = false; + if (!edges[i].has_ref) { + continue; + } + if (direction == AMDUAT_TGK_GRAPH_DIR_OUT) { + include = conformance_node_in_list(edges[i].from, edges[i].from_len, node); + } else if (direction == AMDUAT_TGK_GRAPH_DIR_IN) { + include = conformance_node_in_list(edges[i].to, edges[i].to_len, node); + } else { + include = conformance_node_in_list(edges[i].from, edges[i].from_len, node) || + conformance_node_in_list(edges[i].to, edges[i].to_len, node); + } + + if (!include) { + continue; + } + + if (refs_len == refs_cap) { + size_t next_cap = refs_cap == 0u ? 4u : refs_cap * 2u; + amduat_reference_t *next = + (amduat_reference_t *)realloc(refs, next_cap * sizeof(*refs)); + if (next == NULL) { + size_t j; + for (j = 0; j < refs_len; ++j) { + amduat_reference_free(&refs[j]); + } + free(refs); + return false; + } + refs = next; + refs_cap = next_cap; + } + + if (!amduat_reference_clone(edges[i].ref, &refs[refs_len])) { + size_t j; + for (j = 0; j < refs_len; ++j) { + amduat_reference_free(&refs[j]); + } + free(refs); + return false; + } + refs_len++; + } + + qsort(refs, refs_len, sizeof(*refs), conformance_ref_cmp); + *out_refs = refs; + *out_len = refs_len; + return true; +} + +static bool conformance_collect_expected_scan( + const amduat_tgk_fixture_edge_t *edges, + size_t edges_len, + amduat_reference_t **out_refs, + size_t *out_len) { + amduat_reference_t *refs = NULL; + size_t refs_len = 0u; + size_t refs_cap = 0u; + size_t i; + + if (out_refs == NULL || out_len == NULL) { + return false; + } + *out_refs = NULL; + *out_len = 0u; + + for (i = 0; i < edges_len; ++i) { + if (!edges[i].has_ref) { + continue; + } + if (refs_len == refs_cap) { + size_t next_cap = refs_cap == 0u ? 4u : refs_cap * 2u; + amduat_reference_t *next = + (amduat_reference_t *)realloc(refs, next_cap * sizeof(*refs)); + if (next == NULL) { + size_t j; + for (j = 0; j < refs_len; ++j) { + amduat_reference_free(&refs[j]); + } + free(refs); + return false; + } + refs = next; + refs_cap = next_cap; + } + if (!amduat_reference_clone(edges[i].ref, &refs[refs_len])) { + size_t j; + for (j = 0; j < refs_len; ++j) { + amduat_reference_free(&refs[j]); + } + free(refs); + return false; + } + refs_len++; + } + + qsort(refs, refs_len, sizeof(*refs), conformance_ref_cmp); + *out_refs = refs; + *out_len = refs_len; + return true; +} + +static int conformance_assert_edge_list( + const char *label, + const amduat_reference_t *expected, + size_t expected_len, + const amduat_tgk_graph_edge_view_list_t *actual) { + size_t i; + + if (actual == NULL) { + fprintf(stderr, "%s: missing results\n", label); + return 1; + } + + if (!conformance_ref_list_sorted(actual)) { + fprintf(stderr, "%s: ordering violation\n", label); + return 1; + } + + if (actual->len != expected_len) { + fprintf(stderr, "%s: expected %zu edges, got %zu\n", label, expected_len, + actual->len); + return 1; + } + + for (i = 0; i < expected_len; ++i) { + if (!amduat_reference_eq(actual->edges[i].edge_ref, expected[i])) { + fprintf(stderr, "%s: edge list mismatch at %zu\n", label, i); + return 1; + } + } + + return 0; +} + +static int conformance_test_adjacency( + amduat_tgk_store_t *store, + const amduat_tgk_fixture_edge_t *edges, + size_t edges_len) { + amduat_reference_t *nodes = NULL; + size_t nodes_len = 0u; + size_t nodes_cap = 0u; + size_t i; + int rc = 0; + + for (i = 0; i < edges_len; ++i) { + size_t j; + for (j = 0; j < edges[i].from_len; ++j) { + if (!conformance_collect_node(edges[i].from[j], &nodes, &nodes_len, + &nodes_cap)) { + rc = 1; + goto cleanup; + } + } + for (j = 0; j < edges[i].to_len; ++j) { + if (!conformance_collect_node(edges[i].to[j], &nodes, &nodes_len, + &nodes_cap)) { + rc = 1; + goto cleanup; + } + } + } + + for (i = 0; i < nodes_len; ++i) { + amduat_reference_t node = nodes[i]; + amduat_tgk_edge_type_filter_t filter; + amduat_reference_t *expected = NULL; + size_t expected_len = 0u; + amduat_tgk_graph_edge_view_list_t actual; + + filter.types = NULL; + filter.types_len = 0u; + + memset(&actual, 0, sizeof(actual)); + if (!amduat_tgk_store_edges_from(store, node, filter, &actual)) { + fprintf(stderr, "edges_from failed\n"); + rc = 1; + goto cleanup; + } + if (!conformance_collect_expected_edges(edges, edges_len, node, + AMDUAT_TGK_GRAPH_DIR_OUT, + &expected, &expected_len)) { + amduat_tgk_graph_edge_view_list_free(&actual); + rc = 1; + goto cleanup; + } + if (conformance_assert_edge_list("edges_from", expected, expected_len, + &actual) != 0) { + rc = 1; + } + amduat_tgk_graph_edge_view_list_free(&actual); + for (size_t k = 0; k < expected_len; ++k) { + amduat_reference_free(&expected[k]); + } + free(expected); + expected = NULL; + + memset(&actual, 0, sizeof(actual)); + if (!amduat_tgk_store_edges_to(store, node, filter, &actual)) { + fprintf(stderr, "edges_to failed\n"); + rc = 1; + goto cleanup; + } + if (!conformance_collect_expected_edges(edges, edges_len, node, + AMDUAT_TGK_GRAPH_DIR_IN, + &expected, &expected_len)) { + amduat_tgk_graph_edge_view_list_free(&actual); + rc = 1; + goto cleanup; + } + if (conformance_assert_edge_list("edges_to", expected, expected_len, + &actual) != 0) { + rc = 1; + } + amduat_tgk_graph_edge_view_list_free(&actual); + for (size_t k = 0; k < expected_len; ++k) { + amduat_reference_free(&expected[k]); + } + free(expected); + expected = NULL; + + memset(&actual, 0, sizeof(actual)); + if (!amduat_tgk_store_edges_incident(store, node, filter, &actual)) { + fprintf(stderr, "edges_incident failed\n"); + rc = 1; + goto cleanup; + } + if (!conformance_collect_expected_edges(edges, edges_len, node, + AMDUAT_TGK_GRAPH_DIR_BOTH, + &expected, &expected_len)) { + amduat_tgk_graph_edge_view_list_free(&actual); + rc = 1; + goto cleanup; + } + if (conformance_assert_edge_list("edges_incident", expected, expected_len, + &actual) != 0) { + rc = 1; + } + amduat_tgk_graph_edge_view_list_free(&actual); + for (size_t k = 0; k < expected_len; ++k) { + amduat_reference_free(&expected[k]); + } + free(expected); + } + +cleanup: + if (nodes != NULL) { + for (i = 0; i < nodes_len; ++i) { + amduat_reference_free(&nodes[i]); + } + } + free(nodes); + return rc; +} + +static int conformance_test_scan_edges( + amduat_tgk_store_t *store, + const amduat_tgk_fixture_edge_t *edges, + size_t edges_len) { + amduat_reference_t *expected = NULL; + size_t expected_len = 0u; + amduat_tgk_edge_type_filter_t filter; + amduat_tgk_graph_scan_result_t scan; + int rc = 0; + + if (!conformance_collect_expected_scan(edges, edges_len, &expected, + &expected_len)) { + return 1; + } + + filter.types = NULL; + filter.types_len = 0u; + + memset(&scan, 0, sizeof(scan)); + if (!amduat_tgk_store_scan_edges(store, filter, amduat_octets(NULL, 0), false, + &scan)) { + fprintf(stderr, "scan_edges failed\n"); + rc = 1; + goto cleanup; + } + + if (!conformance_ref_list_sorted(&scan.edges)) { + fprintf(stderr, "scan_edges ordering violation\n"); + rc = 1; + } + + if (scan.edges.len != expected_len || scan.has_next_page) { + fprintf(stderr, "scan_edges expected %zu edges, got %zu\n", expected_len, + scan.edges.len); + rc = 1; + } else { + size_t i; + for (i = 0; i < expected_len; ++i) { + if (!amduat_reference_eq(scan.edges.edges[i].edge_ref, expected[i])) { + fprintf(stderr, "scan_edges mismatch at %zu\n", i); + rc = 1; + break; + } + } + } + + amduat_tgk_graph_scan_result_free(&scan); + +cleanup: + if (expected != NULL) { + size_t i; + for (i = 0; i < expected_len; ++i) { + amduat_reference_free(&expected[i]); + } + } + free(expected); + return rc; +} + static int conformance_test_empty_graph(amduat_tgk_store_t *store) { uint8_t digest[32]; amduat_reference_t node; amduat_tgk_edge_type_filter_t filter; amduat_tgk_graph_edge_view_list_t edges; - amduat_tgk_graph_scan_result_t scan; memset(digest, 0x42, sizeof(digest)); node = amduat_reference(AMDUAT_HASH_ASL1_ID_SHA256, @@ -506,40 +1981,54 @@ static int conformance_test_empty_graph(amduat_tgk_store_t *store) { } amduat_tgk_graph_edge_view_list_free(&edges); - memset(&scan, 0, sizeof(scan)); - if (!amduat_tgk_store_scan_edges(store, filter, amduat_octets(NULL, 0), - false, &scan)) { - fprintf(stderr, "scan_edges failed\n"); - return 1; - } - if (scan.edges.len != 0u || scan.has_next_page) { - fprintf(stderr, "scan_edges expected empty list\n"); - amduat_tgk_graph_scan_result_free(&scan); - return 1; - } - amduat_tgk_graph_scan_result_free(&scan); - - /* TODO: add ordering/adjacency checks per docs/tgk-ordering-enforcement.md */ - return 0; } static int conformance_run_backend( const amduat_tgk_conformance_backend_t *backend, - const char *workdir) { + const char *workdir, + amduat_tgk_fixture_edge_t *edges, + size_t edges_len, + amduat_tgk_equivalence_snapshot_t *out_snapshot) { amduat_tgk_store_t store; void *ctx = NULL; - int rc; + int rc = 1; memset(&store, 0, sizeof(store)); - if (!backend->init(workdir, &store, &ctx)) { + if (!backend->init(workdir, &ctx)) { fprintf(stderr, "backend init failed: %s\n", backend->name); return 1; } - rc = conformance_test_empty_graph(&store); + conformance_clear_fixture_refs(edges, edges_len); + if (!backend->ingest(ctx, edges, edges_len)) { + fprintf(stderr, "backend ingest failed: %s\n", backend->name); + goto cleanup; + } + if (!backend->build_store(ctx, &store)) { + fprintf(stderr, "backend build failed: %s\n", backend->name); + goto cleanup; + } + + if (conformance_test_empty_graph(&store) != 0 || + conformance_test_adjacency(&store, edges, edges_len) != 0 || + conformance_test_scan_edges(&store, edges, edges_len) != 0) { + rc = 1; + } else { + rc = 0; + } + + if (rc == 0 && out_snapshot != NULL) { + if (!conformance_build_equivalence_snapshot(&store, edges, edges_len, + out_snapshot)) { + fprintf(stderr, "equivalence snapshot failed for %s\n", backend->name); + rc = 1; + } + } + +cleanup: if (backend->cleanup != NULL) { backend->cleanup(ctx); } @@ -557,17 +2046,22 @@ int amduat_tgk_store_conformance_run( size_t *out_len) { const amduat_tgk_conformance_backend_t backends[] = { {"tgk_store_mem", false, conformance_init_tgk_store_mem, + conformance_ingest_tgk_store_mem, conformance_build_tgk_store_mem, conformance_free_tgk_mem}, {"tgk_store_fs", false, conformance_init_tgk_store_fs, + conformance_ingest_tgk_store_fs, conformance_build_tgk_store_fs, conformance_free_tgk_fs}, {"tgk_store_asl_index_fs", true, conformance_init_tgk_store_asl_index_fs, - conformance_free_tgk_asl_index}, + conformance_ingest_tgk_store_asl_index_fs, + conformance_build_tgk_store_asl_index_fs, conformance_free_tgk_asl_index}, }; size_t i; size_t results_len = 0u; amduat_conformance_backend_result_t *results = NULL; - - (void)fixtures_root; + amduat_tgk_fixture_edge_t *edges = NULL; + size_t edges_len = 0u; + amduat_tgk_equivalence_snapshot_t baseline; + bool baseline_ready = false; if (out_results == NULL || out_len == NULL) { return 1; @@ -575,11 +2069,20 @@ int amduat_tgk_store_conformance_run( *out_results = NULL; *out_len = 0u; - if (workdir == NULL) { + if (workdir == NULL || fixtures_root == NULL) { return 1; } - bool selection_all =\n+ (backend_selection == NULL || backend_selection[0] == '\\0' ||\n+ strcmp(backend_selection, \"all\") == 0);\n+\n+ for (i = 0; i < sizeof(backends) / sizeof(backends[0]); ++i) { + if (!conformance_load_tgk_fixtures(fixtures_root, &edges, &edges_len)) { + return 1; + } + memset(&baseline, 0, sizeof(baseline)); + + bool selection_all = + (backend_selection == NULL || backend_selection[0] == '\0' || + strcmp(backend_selection, "all") == 0); + + for (i = 0; i < sizeof(backends) / sizeof(backends[0]); ++i) { const amduat_tgk_conformance_backend_t *backend = &backends[i]; char *backend_workdir = NULL; @@ -593,6 +2096,8 @@ int amduat_tgk_store_conformance_run( if (!conformance_prepare_workdir(workdir, backend->name, "tgk", &backend_workdir)) { fprintf(stderr, "workdir prep failed for %s\n", backend->name); + conformance_free_fixture_edges(edges, edges_len); + conformance_free_equivalence_snapshot(&baseline); free(results); return 1; } @@ -601,25 +2106,52 @@ int amduat_tgk_store_conformance_run( results, (results_len + 1u) * sizeof(*results)); if (results == NULL) { free(backend_workdir); + conformance_free_fixture_edges(edges, edges_len); return 1; } results[results_len].backend = backend->name; - results[results_len].status = conformance_run_backend(backend, - backend_workdir); - if (verbose) { - fprintf(stdout, "tgk %s %s\n", backend->name, - results[results_len].status == 0 ? "PASS" : "FAIL"); + amduat_tgk_equivalence_snapshot_t current_snapshot; + memset(¤t_snapshot, 0, sizeof(current_snapshot)); + results[results_len].status = conformance_run_backend( + backend, backend_workdir, edges, edges_len, ¤t_snapshot); + if (results[results_len].status == + AMDUAT_CONFORMANCE_STATUS_PASS) { + if (!baseline_ready) { + baseline = current_snapshot; + baseline_ready = true; + memset(¤t_snapshot, 0, sizeof(current_snapshot)); + } else { + if (conformance_compare_equivalence_snapshot(&baseline, + ¤t_snapshot) != 0) { + fprintf(stderr, "cross-backend equivalence failed for %s\n", + backend->name); + results[results_len].status = 1; + } + } } + if (verbose) { + const char *status = + results[results_len].status == AMDUAT_CONFORMANCE_STATUS_PASS + ? "PASS" + : (results[results_len].status == AMDUAT_CONFORMANCE_STATUS_SKIP + ? "SKIPPED" + : "FAIL"); + fprintf(stdout, "tgk %s %s\n", backend->name, status); + } + conformance_free_equivalence_snapshot(¤t_snapshot); results_len++; free(backend_workdir); } + conformance_free_fixture_edges(edges, edges_len); + conformance_free_equivalence_snapshot(&baseline); + *out_results = results; *out_len = results_len; for (i = 0; i < results_len; ++i) { - if (results[i].status != 0) { + if (results[i].status == AMDUAT_CONFORMANCE_STATUS_FAIL) { return 1; } } diff --git a/tests/fixtures/conformance/tgk_edges/manifest.json b/tests/fixtures/conformance/tgk_edges/manifest.json new file mode 100644 index 0000000..1af4924 --- /dev/null +++ b/tests/fixtures/conformance/tgk_edges/manifest.json @@ -0,0 +1,26 @@ +{ + "schema": "amduat.conformance.tgk_edges.v0", + "edges": [ + { + "id": "e1", + "type": 16, + "from": ["00010a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a"], + "to": ["00010b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"], + "payload": "00011a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a" + }, + { + "id": "e2", + "type": 16, + "from": ["00010a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a"], + "to": ["00010c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c"], + "payload": "00011b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b" + }, + { + "id": "e3", + "type": 32, + "from": ["00010b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"], + "to": ["00010a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a"], + "payload": "00011c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c" + } + ] +}