cas.c (12236B)
1 #include <kit/cas.h> 2 #include <kit/core.h> 3 #include <stddef.h> 4 #include <stdint.h> 5 #include <string.h> 6 7 #include "dist_host.h" 8 #include "driver.h" 9 #include "env.h" 10 11 #define CAS_TOOL "cas" 12 #define CAS_HEX_MAX (2u * KIT_CAS_HASH_LEN + 1u) 13 14 void driver_help_cas(void) { 15 driver_printf( 16 "kit cas — shared blob/tree content-addressed store\n" 17 "\n" 18 "USAGE\n" 19 " kit cas add-blob --cas DIR FILE\n" 20 " kit cas add-tree --cas DIR --root DIR\n" 21 " kit cas add-tree --cas DIR --map FILE\n" 22 " kit cas inspect-tree --cas DIR TREE_ID\n" 23 " kit cas verify-tree --cas DIR TREE_ID\n" 24 " kit cas materialize --cas DIR TREE_ID -C DIR\n" 25 "\n" 26 "DESCRIPTION\n" 27 " Stores immutable blobs and canonical file trees by content ID, then\n" 28 " inspects, verifies, or materializes those trees. CAS objects are\n" 29 " content-verified but are not signed.\n" 30 "\n" 31 "OPTIONS\n" 32 " --cas DIR content-addressed store (required)\n" 33 " --root DIR recursively add a directory tree\n" 34 " --map FILE add a tree described by a mapping file\n" 35 " -C DIR materialization destination\n" 36 " -h, --help show this help and exit\n" 37 "\n" 38 "STORE AND IDS\n" 39 " --cas DIR is required; there is no implicit store. add-blob and\n" 40 " add-tree print one lowercase 64-hex content ID to stdout. A tree ID\n" 41 " names a canonical manifest of relative paths, modes, sizes, and blob\n" 42 " identities.\n" 43 "\n" 44 "ADD TREE INPUTS\n" 45 " --root DIR recursively store regular files from DIR.\n" 46 " --map FILE read whitespace-separated rows:\n" 47 " TREE_PATH MODE SOURCE_PATH\n" 48 " MODE is - for a regular file or x for an executable.\n" 49 " Blank lines and lines whose first non-space byte is #\n" 50 " are ignored. Paths cannot contain spaces or tabs in this\n" 51 " map format. SOURCE_PATH is resolved from the current\n" 52 " working directory; TREE_PATH must be a safe relative\n" 53 " tree path. Row order does not affect canonical ordering.\n" 54 "\n" 55 "INSPECT, VERIFY, MATERIALIZE\n" 56 " inspect-tree writes the canonical manifest to stdout. verify-tree\n" 57 " checks every referenced blob's presence, size, and content identity.\n" 58 " materialize performs the same verification, then recreates the tree\n" 59 " below -C DIR and reapplies regular/executable modes.\n" 60 "\n" 61 "EXAMPLES\n" 62 " CAS=./cas-store\n" 63 " blob=$(kit cas add-blob --cas \"$CAS\" README.txt)\n" 64 " tree=$(kit cas add-tree --cas \"$CAS\" --root ./release-root)\n" 65 " kit cas inspect-tree --cas \"$CAS\" \"$tree\"\n" 66 " kit cas verify-tree --cas \"$CAS\" \"$tree\"\n" 67 " kit cas materialize --cas \"$CAS\" \"$tree\" -C ./restored\n" 68 " printf 'bin/tool x ./build/tool\\n' > tree.map\n" 69 " kit cas add-tree --cas \"$CAS\" --map tree.map\n" 70 "\n" 71 "OUTPUT AND ERRORS\n" 72 " Normal IDs/manifests go to stdout and diagnostics to stderr. Missing,\n" 73 " malformed, or corrupted stored data is an operational failure.\n" 74 "\n" 75 "EXIT CODES\n" 76 " 0 success 1 store/I/O/integrity error\n" 77 " 2 bad command-line usage or malformed ID\n"); 78 } 79 80 static int cas_parse_id(const char* s, uint8_t out[KIT_CAS_HASH_LEN]) { 81 if (driver_strlen(s) != 2u * KIT_CAS_HASH_LEN) return 1; 82 return kit_hex_decode(out, s, KIT_CAS_HASH_LEN) == KIT_OK ? 0 : 1; 83 } 84 85 static int cas_open(DriverEnv* env, const char* dir, KitCas** out) { 86 KitContext ctx = driver_env_to_context(env); 87 KitCasHost host = driver_cas_host(env); 88 if (kit_cas_open(&ctx, &host, dir, out) != KIT_OK) { 89 driver_errf(CAS_TOOL, "failed to open store: %s", dir); 90 return 1; 91 } 92 return 0; 93 } 94 95 static int cas_cmd_add_blob(DriverEnv* env, int argc, char** argv) { 96 const char* cas_dir = NULL; 97 const char* file = NULL; 98 DriverLoad load; 99 KitSlice in; 100 KitCas* cas; 101 KitBlobInfo bi; 102 char hex[CAS_HEX_MAX]; 103 int i; 104 int rc = 1; 105 load.loaded = 0; 106 for (i = 2; i < argc; ++i) { 107 if (driver_streq(argv[i], "--cas") && i + 1 < argc) { 108 cas_dir = argv[++i]; 109 } else if (!file) { 110 file = argv[i]; 111 } else { 112 driver_errf(CAS_TOOL, "unexpected argument: %s", argv[i]); 113 return 2; 114 } 115 } 116 if (!cas_dir || !file) { 117 driver_errf(CAS_TOOL, "usage: kit cas add-blob --cas DIR FILE"); 118 return 2; 119 } 120 if (cas_open(env, cas_dir, &cas) != 0) return 1; 121 if (driver_load_bytes(&env->file_io, CAS_TOOL, file, &load, &in) != 0) { 122 kit_cas_close(cas); 123 return 1; 124 } 125 if (kit_cas_add_blob(cas, in.data, in.len, &bi) == KIT_OK) { 126 kit_hex_encode(hex, bi.id, KIT_CAS_HASH_LEN); 127 driver_printf("%s\n", hex); 128 rc = 0; 129 } 130 driver_release_bytes(&env->file_io, &load); 131 kit_cas_close(cas); 132 return rc; 133 } 134 135 /* Map-file parsing stays in the driver: it is the CLI's input format. Each 136 * line is "tree/path mode source/path"; the source bytes are read here and 137 * handed to the library tree builder. */ 138 static int cas_read_token(const uint8_t* line, size_t len, size_t* pos, 139 const uint8_t** start, size_t* tok_len) { 140 size_t i = *pos; 141 while (i < len && (line[i] == ' ' || line[i] == '\t')) ++i; 142 if (i >= len) return 0; 143 *start = line + i; 144 while (i < len && line[i] != ' ' && line[i] != '\t') ++i; 145 *tok_len = (size_t)(line + i - *start); 146 *pos = i; 147 return 1; 148 } 149 150 static int cas_map_add_line(DriverEnv* env, KitCasTreeBuilder* b, 151 const uint8_t* line, size_t len, unsigned line_no) { 152 const uint8_t *path_b, *mode_b, *src_b; 153 size_t path_l, mode_l, src_l; 154 size_t pos = 0; 155 char path[KIT_CAS_HASH_LEN * 4]; 156 char* src; 157 KitFileData fd; 158 KitTreeMode mode; 159 int rc = 1; 160 while (len && line[len - 1u] == '\r') --len; 161 while (pos < len && (line[pos] == ' ' || line[pos] == '\t')) ++pos; 162 if (pos == len || line[pos] == '#') return 0; 163 if (!cas_read_token(line, len, &pos, &path_b, &path_l) || 164 !cas_read_token(line, len, &pos, &mode_b, &mode_l) || 165 !cas_read_token(line, len, &pos, &src_b, &src_l)) { 166 driver_errf(CAS_TOOL, "bad map line %u", line_no); 167 return 1; 168 } 169 while (pos < len && (line[pos] == ' ' || line[pos] == '\t')) ++pos; 170 if (pos != len || path_l >= sizeof path || mode_l != 1u || src_l == 0) { 171 driver_errf(CAS_TOOL, "bad map line %u", line_no); 172 return 1; 173 } 174 memcpy(path, path_b, path_l); 175 path[path_l] = '\0'; 176 if (mode_b[0] == '-') { 177 mode = KIT_TREE_MODE_FILE; 178 } else if (mode_b[0] == 'x') { 179 mode = KIT_TREE_MODE_EXEC; 180 } else { 181 driver_errf(CAS_TOOL, "bad mode on map line %u", line_no); 182 return 1; 183 } 184 src = (char*)driver_alloc(env, src_l + 1u); 185 if (!src) { 186 driver_errf(CAS_TOOL, "out of memory"); 187 return 1; 188 } 189 memcpy(src, src_b, src_l); 190 src[src_l] = '\0'; 191 fd.data = NULL; 192 fd.size = 0; 193 fd.token = NULL; 194 if (env->file_io.read_all(env->file_io.user, src, &fd) != KIT_OK) { 195 driver_errf(CAS_TOOL, "failed to read: %s", src); 196 driver_free(env, src, src_l + 1u); 197 return 1; 198 } 199 /* v1 map files intentionally split on ASCII whitespace, so paths with 200 * spaces are not representable yet. */ 201 if (kit_cas_tree_builder_add(b, path, mode, fd.data, fd.size) == KIT_OK) 202 rc = 0; 203 if (env->file_io.release) env->file_io.release(env->file_io.user, &fd); 204 driver_free(env, src, src_l + 1u); 205 return rc; 206 } 207 208 static int cas_add_tree_map(DriverEnv* env, KitCasTreeBuilder* b, 209 const uint8_t* data, size_t len) { 210 size_t start = 0; 211 unsigned line_no = 1; 212 while (start <= len) { 213 size_t end = start; 214 while (end < len && data[end] != '\n') ++end; 215 if (cas_map_add_line(env, b, data + start, end - start, line_no) != 0) 216 return 1; 217 if (end == len) break; 218 start = end + 1u; 219 ++line_no; 220 } 221 return 0; 222 } 223 224 static int cas_cmd_add_tree(DriverEnv* env, int argc, char** argv) { 225 const char* cas_dir = NULL; 226 const char* root = NULL; 227 const char* map = NULL; 228 KitCas* cas; 229 uint8_t tree_id[KIT_CAS_HASH_LEN]; 230 char hex[CAS_HEX_MAX]; 231 int i; 232 int rc = 1; 233 for (i = 2; i < argc; ++i) { 234 if (driver_streq(argv[i], "--cas") && i + 1 < argc) { 235 cas_dir = argv[++i]; 236 } else if (driver_streq(argv[i], "--root") && i + 1 < argc) { 237 root = argv[++i]; 238 } else if (driver_streq(argv[i], "--map") && i + 1 < argc) { 239 map = argv[++i]; 240 } else { 241 driver_errf(CAS_TOOL, "unexpected argument: %s", argv[i]); 242 return 2; 243 } 244 } 245 if (!cas_dir || ((root != NULL) == (map != NULL))) { 246 driver_errf(CAS_TOOL, 247 "usage: kit cas add-tree --cas DIR (--root DIR | --map FILE)"); 248 return 2; 249 } 250 if (cas_open(env, cas_dir, &cas) != 0) return 1; 251 if (root) { 252 if (kit_cas_add_tree_from_dir(cas, root, tree_id) == KIT_OK) rc = 0; 253 } else { 254 KitCasTreeBuilder* b; 255 KitFileData fd; 256 fd.data = NULL; 257 fd.size = 0; 258 fd.token = NULL; 259 if (kit_cas_tree_builder_new(cas, &b) != KIT_OK) { 260 kit_cas_close(cas); 261 driver_errf(CAS_TOOL, "out of memory"); 262 return 1; 263 } 264 if (env->file_io.read_all(env->file_io.user, map, &fd) != KIT_OK) { 265 driver_errf(CAS_TOOL, "failed to read map: %s", map); 266 } else { 267 if (cas_add_tree_map(env, b, fd.data, fd.size) == 0 && 268 kit_cas_tree_builder_finish(b, tree_id) == KIT_OK) 269 rc = 0; 270 if (env->file_io.release) env->file_io.release(env->file_io.user, &fd); 271 } 272 kit_cas_tree_builder_free(b); 273 } 274 if (rc == 0) { 275 kit_hex_encode(hex, tree_id, KIT_CAS_HASH_LEN); 276 driver_printf("%s\n", hex); 277 } 278 kit_cas_close(cas); 279 return rc; 280 } 281 282 static int cas_cmd_tree_common(DriverEnv* env, int argc, char** argv, 283 const char* cmd) { 284 const char* cas_dir = NULL; 285 const char* tree_s = NULL; 286 const char* out_dir = NULL; 287 uint8_t tree_id[KIT_CAS_HASH_LEN]; 288 KitCas* cas; 289 int i; 290 int rc = 1; 291 for (i = 2; i < argc; ++i) { 292 if (driver_streq(argv[i], "--cas") && i + 1 < argc) { 293 cas_dir = argv[++i]; 294 } else if (driver_streq(argv[i], "-C") && i + 1 < argc) { 295 out_dir = argv[++i]; 296 } else if (!tree_s) { 297 tree_s = argv[i]; 298 } else { 299 driver_errf(CAS_TOOL, "unexpected argument: %s", argv[i]); 300 return 2; 301 } 302 } 303 if (!cas_dir || !tree_s || (driver_streq(cmd, "materialize") && !out_dir) || 304 (!driver_streq(cmd, "materialize") && out_dir)) { 305 if (driver_streq(cmd, "materialize")) 306 driver_errf(CAS_TOOL, 307 "usage: kit cas materialize --cas DIR TREE_ID -C DIR"); 308 else 309 driver_errf(CAS_TOOL, "usage: kit cas %s --cas DIR TREE_ID", cmd); 310 return 2; 311 } 312 if (cas_parse_id(tree_s, tree_id) != 0) { 313 driver_errf(CAS_TOOL, "bad tree id: %s", tree_s); 314 return 2; 315 } 316 if (cas_open(env, cas_dir, &cas) != 0) return 1; 317 if (driver_streq(cmd, "inspect-tree")) { 318 KitWriter* w = driver_stdout_writer(env); 319 if (w) { 320 if (kit_cas_inspect_tree(cas, tree_id, w) == KIT_OK) rc = 0; 321 kit_writer_close(w); 322 } else { 323 driver_errf(CAS_TOOL, "failed to open stdout"); 324 } 325 } else if (driver_streq(cmd, "verify-tree")) { 326 if (kit_cas_verify_tree(cas, tree_id) == KIT_OK) { 327 driver_printf("ok\n"); 328 rc = 0; 329 } 330 } else { 331 if (kit_cas_materialize_tree(cas, tree_id, out_dir) == KIT_OK) rc = 0; 332 } 333 kit_cas_close(cas); 334 return rc; 335 } 336 337 int driver_cas(int argc, char** argv) { 338 DriverEnv env; 339 int rc; 340 if (argc < 2 || driver_argv_wants_help(argc, argv, 1)) { 341 driver_help_cas(); 342 return 0; 343 } 344 driver_env_init(&env); 345 if (driver_streq(argv[1], "add-blob")) { 346 rc = cas_cmd_add_blob(&env, argc, argv); 347 } else if (driver_streq(argv[1], "add-tree")) { 348 rc = cas_cmd_add_tree(&env, argc, argv); 349 } else if (driver_streq(argv[1], "inspect-tree") || 350 driver_streq(argv[1], "verify-tree") || 351 driver_streq(argv[1], "materialize")) { 352 rc = cas_cmd_tree_common(&env, argc, argv, argv[1]); 353 } else { 354 driver_errf(CAS_TOOL, "unknown command: %s", argv[1]); 355 rc = 2; 356 } 357 driver_env_fini(&env); 358 return rc; 359 }