kit

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

defn.c (21766B)


      1 #include "defn.h"
      2 
      3 #include <stdio.h>
      4 #include <string.h>
      5 
      6 static int set_err(char* err, size_t cap, const char* msg) {
      7   if (err && cap) snprintf(err, cap, "%s", msg);
      8   return BUILD_ERR;
      9 }
     10 
     11 static int slice_eq_cstr(KitSlice s, const char* c) {
     12   size_t n = c ? strlen(c) : 0u;
     13   return s.len == n && (n == 0u || memcmp(s.s, c, n) == 0);
     14 }
     15 
     16 static int token_valid(KitSlice s, size_t cap) {
     17   size_t i;
     18   if (s.len == 0u || s.len >= cap || !s.s) return 0;
     19   for (i = 0; i < s.len; ++i) {
     20     unsigned char c = (unsigned char)s.s[i];
     21     if (c == '\0' || c == '\n' || c == '\r' || c == ' ' || c == '\t')
     22       return 0;
     23   }
     24   return 1;
     25 }
     26 
     27 static int local_name_valid(KitSlice s) {
     28   size_t i;
     29   if (!token_valid(s, BUILD_TARGET_MAX)) return 0;
     30   for (i = 0; i < s.len; ++i) {
     31     if (s.s[i] == '/' || s.s[i] == ':' || s.s[i] == '\\') return 0;
     32   }
     33   return 1;
     34 }
     35 
     36 static int type_valid(KitSlice s) {
     37   size_t i;
     38   if (!token_valid(s, BUILD_KEY_MAX)) return 0;
     39   for (i = 0; i < s.len; ++i) {
     40     if (s.s[i] == '/' || s.s[i] == ':' || s.s[i] == '\\') return 0;
     41   }
     42   return 1;
     43 }
     44 
     45 static int package_valid(KitSlice s) {
     46   size_t i, start = 0;
     47   if (!s.s && s.len != 0u) return 0;
     48   if (s.len >= BUILD_PATH_MAX) return 0;
     49   if (s.len == 0u) return 1;
     50   if (s.s[0] == '/') return 0;
     51   for (i = 0; i <= s.len; ++i) {
     52     if (i == s.len || s.s[i] == '/') {
     53       size_t n = i - start;
     54       if (n == 0u) return 0;
     55       if (n == 1u && s.s[start] == '.') return 0;
     56       if (n == 2u && s.s[start] == '.' && s.s[start + 1u] == '.') return 0;
     57       start = i + 1u;
     58     } else if (s.s[i] == '\0' || s.s[i] == '\\' || s.s[i] == ':') {
     59       return 0;
     60     }
     61   }
     62   return 1;
     63 }
     64 
     65 static int rel_path_valid(KitSlice s, int allow_single_dot) {
     66   size_t i;
     67   size_t start = 0;
     68   if (!token_valid(s, BUILD_PATH_MAX)) return 0;
     69   if (allow_single_dot && s.len == 1u && s.s[0] == '.') return 1;
     70   if (s.s[0] == '/') return 0;
     71   for (i = 0; i <= s.len; ++i) {
     72     if (i == s.len || s.s[i] == '/') {
     73       size_t n = i - start;
     74       if (n == 0u) return 0;
     75       if (n == 1u && s.s[start] == '.') return 0;
     76       if (n == 2u && s.s[start] == '.' && s.s[start + 1u] == '.') return 0;
     77       start = i + 1u;
     78     } else if (s.s[i] == '\\' || s.s[i] == ':') {
     79       return 0;
     80     }
     81   }
     82   return 1;
     83 }
     84 
     85 static int recipe_path_valid(KitSlice s) {
     86   if (s.len > 2u && s.s[0] == '/' && s.s[1] == '/')
     87     return rel_path_valid((KitSlice){.s = s.s + 2u, .len = s.len - 2u}, 0);
     88   return rel_path_valid(s, 0);
     89 }
     90 
     91 static int pattern_valid(KitSlice s) {
     92   size_t i;
     93   if (!token_valid(s, BUILD_TARGET_MAX)) return 0;
     94   for (i = 0; i < s.len; ++i) {
     95     if (s.s[i] == '/' || s.s[i] == ':' || s.s[i] == '\\') return 0;
     96   }
     97   return 1;
     98 }
     99 
    100 static int next_line(const uint8_t* data, size_t len, size_t* pos,
    101                      KitSlice* line) {
    102   size_t start = *pos;
    103   size_t end = start;
    104   while (end < len && data[end] != '\n') ++end;
    105   if (end == len) return BUILD_ERR;
    106   line->data = data + start;
    107   line->len = end - start;
    108   *pos = end + 1u;
    109   return BUILD_OK;
    110 }
    111 
    112 static void copy_slice(char* dst, size_t cap, KitSlice s) {
    113   (void)cap;
    114   if (s.len) memcpy(dst, s.s, s.len);
    115   dst[s.len] = '\0';
    116 }
    117 
    118 static int cstr_slice_cmp(const char* a, KitSlice b) {
    119   size_t i = 0;
    120   for (;;) {
    121     int ac = a[i] ? (int)(unsigned char)a[i] : 0;
    122     int bc = i < b.len ? (int)(unsigned char)b.s[i] : 0;
    123     if (ac != bc) return ac < bc ? -1 : 1;
    124     if (!ac) return 0;
    125     ++i;
    126   }
    127 }
    128 
    129 static int parse_bool(KitSlice s, int* out) {
    130   if (slice_eq_cstr(s, "true")) {
    131     *out = 1;
    132     return BUILD_OK;
    133   }
    134   if (slice_eq_cstr(s, "false")) {
    135     *out = 0;
    136     return BUILD_OK;
    137   }
    138   return BUILD_ERR;
    139 }
    140 
    141 static int split_word(KitSlice line, KitSlice* key, KitSlice* rest) {
    142   size_t i = 0;
    143   while (i < line.len && line.s[i] != ' ' && line.s[i] != '\t') ++i;
    144   key->s = line.s;
    145   key->len = i;
    146   while (i < line.len && (line.s[i] == ' ' || line.s[i] == '\t')) ++i;
    147   rest->s = line.s + i;
    148   rest->len = line.len - i;
    149   return key->len != 0u ? BUILD_OK : BUILD_ERR;
    150 }
    151 
    152 static int parse_header(KitSlice line, KitSlice* kind, KitSlice* arg) {
    153   size_t i;
    154   if (line.len < 3u || line.s[0] != '[' || line.s[line.len - 1u] != ']')
    155     return BUILD_ERR;
    156   line.s++;
    157   line.len -= 2u;
    158   for (i = 0; i < line.len && line.s[i] != ' ' && line.s[i] != '\t'; ++i) {}
    159   kind->s = line.s;
    160   kind->len = i;
    161   while (i < line.len && (line.s[i] == ' ' || line.s[i] == '\t')) ++i;
    162   arg->s = line.s + i;
    163   arg->len = line.len - i;
    164   return kind->len != 0u ? BUILD_OK : BUILD_ERR;
    165 }
    166 
    167 static int line_is_header(KitSlice line) {
    168   return line.len >= 2u && line.s[0] == '[' && line.s[line.len - 1u] == ']';
    169 }
    170 
    171 static int glob_match(const char* pat, size_t pn, const char* text, size_t tn) {
    172   size_t pi = 0, ti = 0, star = (size_t)-1, mark = 0;
    173   while (ti < tn) {
    174     if (pi < pn && pat[pi] == '*') {
    175       star = pi++;
    176       mark = ti;
    177     } else if (pi < pn && (pat[pi] == '?' || pat[pi] == text[ti])) {
    178       ++pi;
    179       ++ti;
    180     } else if (star != (size_t)-1) {
    181       pi = star + 1u;
    182       ti = ++mark;
    183     } else {
    184       return 0;
    185     }
    186   }
    187   while (pi < pn && pat[pi] == '*') ++pi;
    188   return pi == pn;
    189 }
    190 
    191 int build_defn_parse(const uint8_t* data, size_t len, BuildDefn* out, char* err,
    192                      size_t errcap) {
    193   size_t pos = 0;
    194   KitSlice line;
    195   enum { ST_NONE, ST_TARGET, ST_DEFAULT, ST_RULE, ST_REDO } stanza = ST_NONE;
    196   BuildTargetDefn* cur_target = NULL;
    197   BuildDefaultDefn* cur_default = NULL;
    198   BuildRuleDefn* cur_rule = NULL;
    199 
    200   if (!data || !out) return set_err(err, errcap, "missing build definition");
    201   if ((out->cap_targets && !out->targets) ||
    202       (out->cap_defaults && !out->defaults) ||
    203       (out->cap_rules && !out->rules))
    204     return set_err(err, errcap, "missing build definition storage");
    205   out->bytes = data;
    206   out->len = len;
    207   out->version = 0;
    208   out->n_targets = 0;
    209   out->n_defaults = 0;
    210   out->n_rules = 0;
    211   memset(&out->redo, 0, sizeof out->redo);
    212   out->redo.walk_parents = 1;
    213 
    214   if (next_line(data, len, &pos, &line) != BUILD_OK)
    215     return set_err(err, errcap, "bad build definition magic/version");
    216   if (kit_slice_eq_cstr(line, "kit-build 1")) {
    217     out->version = 1;
    218   } else if (kit_slice_eq_cstr(line, "kit-build 2")) {
    219     out->version = 2;
    220   } else {
    221     return set_err(err, errcap, "bad build definition magic/version");
    222   }
    223 
    224   while (pos < len) {
    225     KitSlice key, rest, kind, arg;
    226     if (next_line(data, len, &pos, &line) != BUILD_OK)
    227       return set_err(err, errcap, "unterminated stanza");
    228     if (line.len == 0u) continue;
    229 
    230     if (line_is_header(line)) {
    231       if (parse_header(line, &kind, &arg) != BUILD_OK)
    232         return set_err(err, errcap, "bad stanza header");
    233       cur_target = NULL;
    234       cur_default = NULL;
    235       cur_rule = NULL;
    236       if (slice_eq_cstr(kind, "target")) {
    237         if (arg.len == 0u) return set_err(err, errcap, "missing target name");
    238         if (!local_name_valid(arg))
    239           return set_err(err, errcap, "invalid target name");
    240         if (out->n_targets > 0 &&
    241             cstr_slice_cmp(out->targets[out->n_targets - 1u].name, arg) >= 0)
    242           return set_err(err, errcap, "non-canonical target ordering");
    243         if (out->n_targets >= out->cap_targets)
    244           return set_err(err, errcap, "too many targets");
    245         cur_target = &out->targets[out->n_targets++];
    246         memset(cur_target, 0, sizeof *cur_target);
    247         copy_slice(cur_target->name, sizeof cur_target->name, arg);
    248         stanza = ST_TARGET;
    249       } else if (slice_eq_cstr(kind, "default")) {
    250         if (out->version < 2)
    251           return set_err(err, errcap, "default requires kit-build 2");
    252         if (arg.len == 0u) return set_err(err, errcap, "missing default type");
    253         if (!type_valid(arg)) return set_err(err, errcap, "invalid default type");
    254         if (out->n_defaults > 0 &&
    255             cstr_slice_cmp(out->defaults[out->n_defaults - 1u].type, arg) >= 0)
    256           return set_err(err, errcap, "non-canonical default ordering");
    257         if (out->n_defaults >= out->cap_defaults)
    258           return set_err(err, errcap, "too many defaults");
    259         cur_default = &out->defaults[out->n_defaults++];
    260         memset(cur_default, 0, sizeof *cur_default);
    261         copy_slice(cur_default->type, sizeof cur_default->type, arg);
    262         stanza = ST_DEFAULT;
    263       } else if (slice_eq_cstr(kind, "rule")) {
    264         if (out->version < 2)
    265           return set_err(err, errcap, "rule requires kit-build 2");
    266         if (arg.len == 0u) return set_err(err, errcap, "missing rule name");
    267         if (!type_valid(arg)) return set_err(err, errcap, "invalid rule name");
    268         if (out->n_rules > 0 &&
    269             cstr_slice_cmp(out->rules[out->n_rules - 1u].name, arg) >= 0)
    270           return set_err(err, errcap, "non-canonical rule ordering");
    271         if (out->n_rules >= out->cap_rules)
    272           return set_err(err, errcap, "too many rules");
    273         cur_rule = &out->rules[out->n_rules++];
    274         memset(cur_rule, 0, sizeof *cur_rule);
    275         copy_slice(cur_rule->name, sizeof cur_rule->name, arg);
    276         stanza = ST_RULE;
    277       } else if (slice_eq_cstr(kind, "redo-defaults")) {
    278         if (out->version < 2)
    279           return set_err(err, errcap, "redo-defaults requires kit-build 2");
    280         if (arg.len != 0u && !slice_eq_cstr(arg, "settings"))
    281           return set_err(err, errcap, "bad redo-defaults header");
    282         stanza = ST_REDO;
    283       } else {
    284         return set_err(err, errcap, "unknown stanza");
    285       }
    286       continue;
    287     }
    288 
    289     if (split_word(line, &key, &rest) != BUILD_OK)
    290       return set_err(err, errcap, "bad stanza line");
    291     if (stanza == ST_TARGET) {
    292       if (!cur_target) return set_err(err, errcap, "internal target state");
    293       if (slice_eq_cstr(key, "recipe")) {
    294         if (cur_target->has_recipe || !recipe_path_valid(rest))
    295           return set_err(err, errcap, "invalid recipe path");
    296         copy_slice(cur_target->recipe_path, sizeof cur_target->recipe_path,
    297                    rest);
    298         cur_target->has_recipe = 1;
    299       } else if (slice_eq_cstr(key, "type")) {
    300         if (cur_target->has_type || !type_valid(rest))
    301           return set_err(err, errcap, "invalid target type");
    302         copy_slice(cur_target->type, sizeof cur_target->type, rest);
    303         cur_target->has_type = 1;
    304       } else {
    305         return set_err(err, errcap, "unknown target field");
    306       }
    307     } else if (stanza == ST_DEFAULT) {
    308       if (!cur_default) return set_err(err, errcap, "internal default state");
    309       if (!slice_eq_cstr(key, "recipe") || cur_default->recipe_path[0] ||
    310           !recipe_path_valid(rest))
    311         return set_err(err, errcap, "invalid default field");
    312       copy_slice(cur_default->recipe_path, sizeof cur_default->recipe_path,
    313                  rest);
    314     } else if (stanza == ST_RULE) {
    315       if (!cur_rule) return set_err(err, errcap, "internal rule state");
    316       if (slice_eq_cstr(key, "match")) {
    317         if (cur_rule->match[0] || !pattern_valid(rest))
    318           return set_err(err, errcap, "invalid rule match");
    319         copy_slice(cur_rule->match, sizeof cur_rule->match, rest);
    320       } else if (slice_eq_cstr(key, "recipe")) {
    321         if (cur_rule->has_recipe || !recipe_path_valid(rest))
    322           return set_err(err, errcap, "invalid rule recipe");
    323         copy_slice(cur_rule->recipe_path, sizeof cur_rule->recipe_path, rest);
    324         cur_rule->has_recipe = 1;
    325       } else if (slice_eq_cstr(key, "type")) {
    326         if (cur_rule->has_type || !type_valid(rest))
    327           return set_err(err, errcap, "invalid rule type");
    328         copy_slice(cur_rule->type, sizeof cur_rule->type, rest);
    329         cur_rule->has_type = 1;
    330       } else {
    331         return set_err(err, errcap, "unknown rule field");
    332       }
    333     } else if (stanza == ST_REDO) {
    334       if (slice_eq_cstr(key, "enabled")) {
    335         if (parse_bool(rest, &out->redo.enabled) != BUILD_OK)
    336           return set_err(err, errcap, "invalid redo enabled");
    337       } else if (slice_eq_cstr(key, "walk-parents")) {
    338         if (parse_bool(rest, &out->redo.walk_parents) != BUILD_OK)
    339           return set_err(err, errcap, "invalid redo walk-parents");
    340       } else if (slice_eq_cstr(key, "search")) {
    341         KitSlice word;
    342         size_t start = 0, i;
    343         for (i = 0; i <= rest.len; ++i) {
    344           if (i == rest.len || rest.s[i] == ' ' || rest.s[i] == '\t') {
    345             if (i == start) {
    346               start = i + 1u;
    347               continue;
    348             }
    349             word.s = rest.s + start;
    350             word.len = i - start;
    351             if (!rel_path_valid(word, 1) ||
    352                 out->redo.n_search >=
    353                     sizeof out->redo.search / sizeof out->redo.search[0])
    354               return set_err(err, errcap, "invalid redo search");
    355             copy_slice(out->redo.search[out->redo.n_search],
    356                        sizeof out->redo.search[out->redo.n_search], word);
    357             ++out->redo.n_search;
    358             start = i + 1u;
    359           }
    360         }
    361       } else {
    362         return set_err(err, errcap, "unknown redo-defaults field");
    363       }
    364     } else {
    365       return set_err(err, errcap, "field outside stanza");
    366     }
    367   }
    368 
    369   for (pos = 0; pos < out->n_targets; ++pos) {
    370     if (!out->targets[pos].has_recipe && !out->targets[pos].has_type)
    371       return set_err(err, errcap, "target has no recipe or type");
    372   }
    373   for (pos = 0; pos < out->n_defaults; ++pos) {
    374     if (!out->defaults[pos].recipe_path[0])
    375       return set_err(err, errcap, "default has no recipe");
    376   }
    377   for (pos = 0; pos < out->n_rules; ++pos) {
    378     if (!out->rules[pos].match[0])
    379       return set_err(err, errcap, "rule has no match");
    380   }
    381   if (out->redo.enabled && out->redo.n_search == 0u) {
    382     snprintf(out->redo.search[out->redo.n_search++],
    383              sizeof out->redo.search[0], ".");
    384   }
    385   return BUILD_OK;
    386 }
    387 
    388 const BuildTargetDefn* build_defn_find(const BuildDefn* defn, KitSlice name) {
    389   size_t lo, hi;
    390   if (!defn || !local_name_valid(name)) return NULL;
    391   lo = 0;
    392   hi = defn->n_targets;
    393   while (lo < hi) {
    394     size_t mid = lo + (hi - lo) / 2u;
    395     int cmp = cstr_slice_cmp(defn->targets[mid].name, name);
    396     if (cmp == 0) return &defn->targets[mid];
    397     if (cmp < 0)
    398       lo = mid + 1u;
    399     else
    400       hi = mid;
    401   }
    402   return NULL;
    403 }
    404 
    405 const BuildDefaultDefn* build_defn_default_find(const BuildDefn* defn,
    406                                                 KitSlice type) {
    407   size_t lo, hi;
    408   if (!defn || !type_valid(type)) return NULL;
    409   lo = 0;
    410   hi = defn->n_defaults;
    411   while (lo < hi) {
    412     size_t mid = lo + (hi - lo) / 2u;
    413     int cmp = cstr_slice_cmp(defn->defaults[mid].type, type);
    414     if (cmp == 0) return &defn->defaults[mid];
    415     if (cmp < 0)
    416       lo = mid + 1u;
    417     else
    418       hi = mid;
    419   }
    420   return NULL;
    421 }
    422 
    423 const BuildRuleDefn* build_defn_rule_match(const BuildDefn* defn,
    424                                            KitSlice name, int* ambiguous) {
    425   const BuildRuleDefn* match = NULL;
    426   size_t i;
    427   if (ambiguous) *ambiguous = 0;
    428   if (!defn || !local_name_valid(name)) return NULL;
    429   for (i = 0; i < defn->n_rules; ++i) {
    430     const BuildRuleDefn* r = &defn->rules[i];
    431     if (!glob_match(r->match, strlen(r->match), name.s, name.len)) continue;
    432     if (match) {
    433       if (ambiguous) *ambiguous = 1;
    434       return NULL;
    435     }
    436     match = r;
    437   }
    438   return match;
    439 }
    440 
    441 static int last_component(KitSlice s, KitSlice* out) {
    442   size_t i = s.len;
    443   if (s.len == 0u) return BUILD_ERR;
    444   while (i > 0u && s.s[i - 1u] != '/') --i;
    445   out->s = s.s + i;
    446   out->len = s.len - i;
    447   return local_name_valid(*out) ? BUILD_OK : BUILD_ERR;
    448 }
    449 
    450 int build_target_canonicalize(KitSlice label, KitSlice current_repo,
    451                               KitSlice current_package,
    452                               char out[BUILD_TARGET_MAX], char* err,
    453                               size_t errcap) {
    454   KitSlice repo = current_repo;
    455   KitSlice pkg, local;
    456   char repo_buf[BUILD_KEY_MAX];
    457   size_t colon = (size_t)-1, i;
    458   if (!out || !label.s || label.len == 0u || !package_valid(current_package))
    459     return set_err(err, errcap, "invalid target label");
    460   if (repo.len && !token_valid(repo, BUILD_KEY_MAX))
    461     return set_err(err, errcap, "invalid target repo");
    462 
    463   if (label.s[0] == '@') {
    464     KitSlice explicit_repo;
    465     size_t slash = 1u;
    466     while (slash < label.len && label.s[slash] != '/') ++slash;
    467     if (slash <= 1u || slash + 1u >= label.len || label.s[slash + 1u] != '/')
    468       return set_err(err, errcap, "bad target label");
    469     explicit_repo.s = label.s + 1u;
    470     explicit_repo.len = slash - 1u;
    471     if (!token_valid(explicit_repo, BUILD_KEY_MAX))
    472       return set_err(err, errcap, "bad target repo");
    473     if (current_repo.len) {
    474       if (current_repo.len + explicit_repo.len + 2u > sizeof repo_buf)
    475         return set_err(err, errcap, "target repo too long");
    476       memcpy(repo_buf, current_repo.s, current_repo.len);
    477       repo_buf[current_repo.len] = '+';
    478       memcpy(repo_buf + current_repo.len + 1u, explicit_repo.s,
    479              explicit_repo.len);
    480       repo_buf[current_repo.len + 1u + explicit_repo.len] = '\0';
    481       repo = kit_slice_cstr(repo_buf);
    482     } else {
    483       repo = explicit_repo;
    484     }
    485     label.s += slash;
    486     label.len -= slash;
    487   }
    488 
    489   if (label.len >= 2u && label.s[0] == '/' && label.s[1] == '/') {
    490     KitSlice body = {.s = label.s + 2u, .len = label.len - 2u};
    491     for (i = 0; i < body.len; ++i) {
    492       if (body.s[i] == ':') {
    493         if (colon != (size_t)-1) return set_err(err, errcap, "bad target label");
    494         colon = i;
    495       }
    496     }
    497     if (colon == (size_t)-1) {
    498       pkg = body;
    499       if (!package_valid(pkg) || last_component(pkg, &local) != BUILD_OK)
    500         return set_err(err, errcap, "bad target label");
    501     } else {
    502       pkg.s = body.s;
    503       pkg.len = colon;
    504       local.s = body.s + colon + 1u;
    505       local.len = body.len - colon - 1u;
    506       if (!package_valid(pkg) || !local_name_valid(local))
    507         return set_err(err, errcap, "bad target label");
    508     }
    509   } else if (label.s[0] == ':') {
    510     pkg = current_package;
    511     local.s = label.s + 1u;
    512     local.len = label.len - 1u;
    513     if (!local_name_valid(local))
    514       return set_err(err, errcap, "bad target label");
    515   } else {
    516     pkg = current_package;
    517     local = label;
    518     if (!local_name_valid(local))
    519       return set_err(err, errcap, "bad target label");
    520   }
    521 
    522   if (repo.len) {
    523     if (repo.len + pkg.len + local.len + 5u > BUILD_TARGET_MAX)
    524       return set_err(err, errcap, "target label too long");
    525     out[0] = '@';
    526     memcpy(out + 1u, repo.s, repo.len);
    527     out[1u + repo.len] = '/';
    528     out[2u + repo.len] = '/';
    529     if (pkg.len) memcpy(out + 3u + repo.len, pkg.s, pkg.len);
    530     out[3u + repo.len + pkg.len] = ':';
    531     memcpy(out + 4u + repo.len + pkg.len, local.s, local.len);
    532     out[4u + repo.len + pkg.len + local.len] = '\0';
    533   } else if (pkg.len == 0u) {
    534     if (local.len + 4u > BUILD_TARGET_MAX)
    535       return set_err(err, errcap, "target label too long");
    536     out[0] = '/';
    537     out[1] = '/';
    538     out[2] = ':';
    539     memcpy(out + 3u, local.s, local.len);
    540     out[3u + local.len] = '\0';
    541   } else {
    542     if (pkg.len + local.len + 4u > BUILD_TARGET_MAX)
    543       return set_err(err, errcap, "target label too long");
    544     out[0] = '/';
    545     out[1] = '/';
    546     memcpy(out + 2u, pkg.s, pkg.len);
    547     out[2u + pkg.len] = ':';
    548     memcpy(out + 3u + pkg.len, local.s, local.len);
    549     out[3u + pkg.len + local.len] = '\0';
    550   }
    551   return BUILD_OK;
    552 }
    553 
    554 int build_target_split_repo(KitSlice canonical, char repo[BUILD_KEY_MAX],
    555                             char package[BUILD_PATH_MAX],
    556                             char local[BUILD_TARGET_MAX]) {
    557   KitSlice pkg, name;
    558   size_t i, colon = (size_t)-1, body_start = 0;
    559   if (!canonical.s || canonical.len < 4u)
    560     return BUILD_ERR;
    561   if (repo) repo[0] = '\0';
    562   if (canonical.s[0] == '@') {
    563     size_t slash = 1u;
    564     while (slash < canonical.len && canonical.s[slash] != '/') ++slash;
    565     if (slash <= 1u || slash + 1u >= canonical.len ||
    566         canonical.s[slash + 1u] != '/')
    567       return BUILD_ERR;
    568     if (repo) {
    569       KitSlice r = {.s = canonical.s + 1u, .len = slash - 1u};
    570       if (!token_valid(r, BUILD_KEY_MAX)) return BUILD_ERR;
    571       copy_slice(repo, BUILD_KEY_MAX, r);
    572     }
    573     body_start = slash;
    574   }
    575   if (canonical.len < body_start + 4u || canonical.s[body_start] != '/' ||
    576       canonical.s[body_start + 1u] != '/')
    577     return BUILD_ERR;
    578   for (i = body_start + 2u; i < canonical.len; ++i) {
    579     if (canonical.s[i] == ':') {
    580       colon = i;
    581       break;
    582     }
    583   }
    584   if (colon == (size_t)-1) return BUILD_ERR;
    585   pkg.s = canonical.s + body_start + 2u;
    586   pkg.len = colon - body_start - 2u;
    587   name.s = canonical.s + colon + 1u;
    588   name.len = canonical.len - colon - 1u;
    589   if (!package_valid(pkg) || !local_name_valid(name)) return BUILD_ERR;
    590   copy_slice(package, BUILD_PATH_MAX, pkg);
    591   copy_slice(local, BUILD_TARGET_MAX, name);
    592   return BUILD_OK;
    593 }
    594 
    595 int build_target_split(KitSlice canonical, char package[BUILD_PATH_MAX],
    596                        char local[BUILD_TARGET_MAX]) {
    597   return build_target_split_repo(canonical, NULL, package, local);
    598 }
    599 
    600 int build_recipe_path_resolve(KitSlice package, KitSlice recipe,
    601                               char out[BUILD_PATH_MAX]) {
    602   if (!out || !package_valid(package) || !recipe_path_valid(recipe))
    603     return BUILD_ERR;
    604   if (recipe.len > 2u && recipe.s[0] == '/' && recipe.s[1] == '/') {
    605     if (recipe.len - 2u + 1u > BUILD_PATH_MAX) return BUILD_ERR;
    606     memcpy(out, recipe.s + 2u, recipe.len - 2u);
    607     out[recipe.len - 2u] = '\0';
    608     return BUILD_OK;
    609   }
    610   if (package.len == 0u) {
    611     copy_slice(out, BUILD_PATH_MAX, recipe);
    612     return BUILD_OK;
    613   }
    614   if (package.len + 1u + recipe.len + 1u > BUILD_PATH_MAX) return BUILD_ERR;
    615   memcpy(out, package.s, package.len);
    616   out[package.len] = '/';
    617   memcpy(out + package.len + 1u, recipe.s, recipe.len);
    618   out[package.len + 1u + recipe.len] = '\0';
    619   return BUILD_OK;
    620 }