kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

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 }