commit c2795974bd602276efb43a46f376c2028ad55366
parent 5ae5d511771ef843bee75c53b3d6bbf27c94d493
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 17 Jun 2026 21:36:15 -0700
cas: expose by-id blob reads
Diffstat:
5 files changed, 226 insertions(+), 3 deletions(-)
diff --git a/include/kit/cas.h b/include/kit/cas.h
@@ -76,7 +76,8 @@ KIT_API KitStatus kit_cas_add_blob(KitCas* cas, const uint8_t* data, size_t len,
KitBlobInfo* out);
/* Report whether a blob is present locally (KIT_OK present, KIT_NOT_FOUND
- * absent). A light existence probe — it does not read or re-verify the bytes.
+ * absent). The current host file boundary has no stat hook, so this verifies
+ * the stored bytes and releases them immediately.
*/
KIT_API KitStatus kit_cas_has_blob(KitCas* cas,
const uint8_t id[KIT_CAS_HASH_LEN]);
diff --git a/mk/test.mk b/mk/test.mk
@@ -83,6 +83,7 @@ TEST_TARGETS = \
test-gram \
test-driver-gram \
test-hash \
+ test-cas-api \
test-release-index \
test-driver-strip \
test-dwarf \
@@ -248,7 +249,7 @@ test-t1-driver:
@$(T1_LOG) driver env KIT=$(abspath $(BIN)) sh test/tier1/system/driver-kitchen.sh
test-t1-core-api:
- @$(T1_LOG) core-api $(MAKE) --no-print-directory test-hash test-release-index test-abi-classify
+ @$(T1_LOG) core-api $(MAKE) --no-print-directory test-hash test-cas-api test-release-index test-abi-classify
test-t1-pp: bin
@$(T1_LOG) pp env KIT=$(abspath $(BIN)) test/tier1/frontend/run-kitchen.sh pp
@@ -664,6 +665,7 @@ CG_CONST_TEST_BIN = build/test/cg_const_test
STRENGTH_REDUCE_TEST_BIN = build/test/strength_reduce_test
TARGET_TEST_BIN = build/test/target_test
HASH_TEST_BIN = build/test/hash_test
+CAS_TEST_BIN = build/test/cas_test
RELEASE_INDEX_TEST_BIN = build/test/release_index_test
PANIC_RECOVERY_TEST_BIN = build/test/panic_recovery_test
ABI_CLASSIFY_TEST_BIN = build/test/abi_classify_test
@@ -687,6 +689,9 @@ test-cg-api: $(TARGET_TEST_BIN) $(CG_API_TEST_BIN) $(CG_SWITCH_TEST_BIN) \
test-hash: $(HASH_TEST_BIN)
$(HASH_TEST_BIN)
+test-cas-api: $(CAS_TEST_BIN)
+ $(CAS_TEST_BIN)
+
test-release-index: $(RELEASE_INDEX_TEST_BIN)
$(RELEASE_INDEX_TEST_BIN)
diff --git a/mk/test_unit.mk b/mk/test_unit.mk
@@ -32,7 +32,7 @@ UNIT_CFLAGS_INTERNAL = $(HOST_CFLAGS) -Iinclude -Isrc -Itest
UNIT_TESTS_PUBLIC := \
ar_test target_test arm32_target_features_test cg_api_test cg_switch_test \
cg_fp_cmp_test \
- cg_control_test cg_const_test hash_test release_index_test \
+ cg_control_test cg_const_test hash_test cas_test release_index_test \
panic_recovery_test profile_test \
link_script_test \
rv64_jit_test rv32_jit_test aa64_inline_test rv64_inline_test x64_inline_test \
@@ -42,6 +42,7 @@ ar_test_SRC := test/ar/ar_test.c
target_test_SRC := test/api/target_test.c
arm32_target_features_test_SRC := test/api/arm32_target_features_test.c
hash_test_SRC := test/api/hash_test.c
+cas_test_SRC := test/api/cas_test.c
release_index_test_SRC := test/api/release_index_test.c
panic_recovery_test_SRC := test/api/panic_recovery_test.c
profile_test_SRC := test/api/profile_test.c
diff --git a/src/api/cas.c b/src/api/cas.c
@@ -92,6 +92,38 @@ KitStatus kit_cas_add_blob(KitCas* cas, const uint8_t* data, size_t len,
return KIT_OK;
}
+KitStatus kit_cas_has_blob(KitCas* cas,
+ const uint8_t id[KIT_CAS_HASH_LEN]) {
+ KitFileData fd;
+ KitStatus st;
+ if (!cas || !id) return KIT_INVALID;
+ fd.data = NULL;
+ fd.size = 0;
+ fd.token = NULL;
+ st = kit_cas_get_blob(cas, id, &fd);
+ if (st == KIT_OK) kit_cas_release(cas, &fd);
+ return st;
+}
+
+KitStatus kit_cas_get_blob(KitCas* cas, const uint8_t id[KIT_CAS_HASH_LEN],
+ KitFileData* out) {
+ if (!cas || !id || !out) return KIT_INVALID;
+ out->data = NULL;
+ out->size = 0;
+ out->token = NULL;
+ if (dist_cas_get_blob(&cas->dist, id, out) != DIST_OK) return KIT_NOT_FOUND;
+ return KIT_OK;
+}
+
+void kit_cas_release(KitCas* cas, KitFileData* fd) {
+ if (!cas || !fd) return;
+ if (fd->data && cas->host.file_io && cas->host.file_io->release)
+ cas->host.file_io->release(cas->host.file_io->user, fd);
+ fd->data = NULL;
+ fd->size = 0;
+ fd->token = NULL;
+}
+
KitStatus kit_cas_tree_builder_new(KitCas* cas, KitCasTreeBuilder** out) {
KitCasTreeBuilder* b;
KitHeap* h;
diff --git a/test/api/cas_test.c b/test/api/cas_test.c
@@ -0,0 +1,184 @@
+/* cas_test -- public <kit/cas.h> by-id blob read surface.
+ *
+ * Run by: make test-cas-api
+ */
+
+#include <kit/cas.h>
+#include <kit/core.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include "lib/kit_unit.h"
+
+static KitUnit g_u;
+#define EXPECT(c, ...) CU_EXPECT(&g_u, c, __VA_ARGS__)
+
+typedef struct CasTestWriter {
+ KitWriter w;
+ FILE* f;
+} CasTestWriter;
+
+static KitStatus cas_test_writer_write(KitWriter* w, const void* data,
+ size_t n) {
+ CasTestWriter* cw = (CasTestWriter*)w;
+ return fwrite(data, 1, n, cw->f) == n ? KIT_OK : KIT_IO;
+}
+
+static KitStatus cas_test_writer_seek(KitWriter* w, uint64_t offset) {
+ CasTestWriter* cw = (CasTestWriter*)w;
+ return fseek(cw->f, (long)offset, SEEK_SET) == 0 ? KIT_OK : KIT_IO;
+}
+
+static uint64_t cas_test_writer_tell(KitWriter* w) {
+ CasTestWriter* cw = (CasTestWriter*)w;
+ long pos = ftell(cw->f);
+ return pos < 0 ? 0 : (uint64_t)pos;
+}
+
+static KitStatus cas_test_writer_status(KitWriter* w) {
+ CasTestWriter* cw = (CasTestWriter*)w;
+ return ferror(cw->f) ? KIT_IO : KIT_OK;
+}
+
+static void cas_test_writer_close(KitWriter* w) {
+ CasTestWriter* cw = (CasTestWriter*)w;
+ if (cw->f) fclose(cw->f);
+ free(cw);
+}
+
+static KitStatus cas_test_read_all(void* user, const char* path,
+ KitFileData* out) {
+ FILE* f;
+ long end;
+ uint8_t* data;
+ (void)user;
+ out->data = NULL;
+ out->size = 0;
+ out->token = NULL;
+ f = fopen(path, "rb");
+ if (!f) return KIT_NOT_FOUND;
+ if (fseek(f, 0, SEEK_END) != 0) {
+ fclose(f);
+ return KIT_IO;
+ }
+ end = ftell(f);
+ if (end < 0 || fseek(f, 0, SEEK_SET) != 0) {
+ fclose(f);
+ return KIT_IO;
+ }
+ data = (uint8_t*)malloc((size_t)end ? (size_t)end : 1u);
+ if (!data) {
+ fclose(f);
+ return KIT_NOMEM;
+ }
+ if ((size_t)end &&
+ fread(data, 1, (size_t)end, f) != (size_t)end) {
+ free(data);
+ fclose(f);
+ return KIT_IO;
+ }
+ fclose(f);
+ out->data = data;
+ out->size = (size_t)end;
+ out->token = data;
+ return KIT_OK;
+}
+
+static void cas_test_release(void* user, KitFileData* fd) {
+ (void)user;
+ free(fd->token);
+ fd->data = NULL;
+ fd->size = 0;
+ fd->token = NULL;
+}
+
+static KitStatus cas_test_open_writer(void* user, const char* path,
+ KitWriter** out) {
+ CasTestWriter* cw;
+ (void)user;
+ *out = NULL;
+ cw = (CasTestWriter*)calloc(1, sizeof *cw);
+ if (!cw) return KIT_NOMEM;
+ cw->f = fopen(path, "wb");
+ if (!cw->f) {
+ free(cw);
+ return KIT_IO;
+ }
+ cw->w.write = cas_test_writer_write;
+ cw->w.seek = cas_test_writer_seek;
+ cw->w.tell = cas_test_writer_tell;
+ cw->w.status = cas_test_writer_status;
+ cw->w.close = cas_test_writer_close;
+ *out = &cw->w;
+ return KIT_OK;
+}
+
+static int cas_test_mkdir_p(void* user, const char* path) {
+ char buf[1024];
+ size_t i, n;
+ (void)user;
+ n = strlen(path);
+ if (n == 0 || n >= sizeof buf) return 1;
+ memcpy(buf, path, n + 1u);
+ for (i = 1; i <= n; ++i) {
+ if (buf[i] != '/' && buf[i] != '\0') continue;
+ {
+ char save = buf[i];
+ buf[i] = '\0';
+ if (mkdir(buf, 0777) != 0 && errno != EEXIST) return 1;
+ buf[i] = save;
+ }
+ }
+ return 0;
+}
+
+int main(void) {
+ static const uint8_t data[] = "config=debug\n";
+ static const char root[] = "build/test/cas_api_test_store";
+ KitFileIO fio;
+ KitCasHost host;
+ KitCas* cas = NULL;
+ KitBlobInfo info;
+ KitFileData fd;
+ uint8_t missing[KIT_CAS_HASH_LEN];
+
+ kit_unit_init(&g_u);
+ memset(&fio, 0, sizeof fio);
+ fio.read_all = cas_test_read_all;
+ fio.release = cas_test_release;
+ fio.open_writer = cas_test_open_writer;
+ memset(&host, 0, sizeof host);
+ host.file_io = &fio;
+ host.mkdir_p = cas_test_mkdir_p;
+
+ EXPECT(kit_cas_open(&g_u.ctx, &host, root, &cas) == KIT_OK, "open CAS");
+ EXPECT(kit_cas_add_blob(cas, data, sizeof data - 1u, &info) == KIT_OK,
+ "add blob");
+ EXPECT(kit_cas_has_blob(cas, info.id) == KIT_OK, "has added blob");
+
+ memset(&fd, 0, sizeof fd);
+ EXPECT(kit_cas_get_blob(cas, info.id, &fd) == KIT_OK, "get added blob");
+ EXPECT(fd.size == sizeof data - 1u, "blob size matches");
+ EXPECT(memcmp(fd.data, data, sizeof data - 1u) == 0, "blob bytes match");
+ kit_cas_release(cas, &fd);
+ kit_cas_release(cas, &fd);
+ EXPECT(fd.data == NULL && fd.size == 0 && fd.token == NULL,
+ "release is idempotent");
+
+ memset(missing, 0, sizeof missing);
+ EXPECT(kit_cas_has_blob(cas, missing) == KIT_NOT_FOUND,
+ "has missing blob reports not found");
+ memset(&fd, 0xff, sizeof fd);
+ EXPECT(kit_cas_get_blob(cas, missing, &fd) == KIT_NOT_FOUND,
+ "get missing blob reports not found");
+ EXPECT(fd.data == NULL && fd.size == 0 && fd.token == NULL,
+ "miss leaves output empty");
+
+ kit_cas_close(cas);
+ kit_unit_summary(&g_u, "cas_test");
+ return kit_unit_status(&g_u);
+}