/* mhparse.c -- Parsing of module headers Copyright © 2021-2024 Samuel Lidén Borell Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "internal.h" #include "hash.h" #include #include #define INTERR_MHPARSE(errnum) MAKE_INTERR(errnum, INTERRBASE_MHPARSE) const char modtype2str[][12] = { "invalid", "internal", /* FIXME this may need to be two types, one for dynlibs (PIC) and one for static libs / exes (non-PIC) */ "library", "plugin", "libraryspec", "pluginspec", "app" }; static const char attr2str[][19] = { "", "\\slul", "\\name", "\\type", "\\version", "\\repo", "\\repo.mirror", "\\website", "\\license", "\\license.text", "\\license.list", "\\depends", "\\interface_depends", "\\api_def", "\\source" /*"source.list",*/ /* TODO */ }; static void error_attr(struct CSlul *ctx, enum CSlulErrorCode errorcode) { error_text(ctx, errorcode, ctx->parser.mh.attr_line, ctx->parser.mh.attr_col, attr2str[ctx->parser.mh.attr]); } /** Returns 1 if the filename is a Windows/DOS device filename */ static int is_win_device(const char *filename, size_t len) { if (len == 3) { const char *badname = "aux" "con" "nul" "prn"; int i; for (i = 0; i < 4; i++) { if (UNLIKELY(silly_casestrcmp(filename, badname, 3)==0)) return 1; badname += 3; } } else if (len == 4 && (silly_casestrcmp(filename, "com", 3)==0 || silly_casestrcmp(filename, "lpt", 3)==0)) { /* COM0 and LPT0 are undocumented, but they are disallowed when used without a file extension (e.g. as a directory) in Windows Explorer */ return filename[3] >= '0' && filename[3] <= '9'; } return 0; } static int check_slul_filename(struct CSlul *ctx, const char *name, size_t len) { enum CSlulErrorCode error; const char *cp=NULL, *part; int in_ext; assert(len > 0); assert(len <= 100); /* checked in token.c */ if (UNLIKELY(len < 6 || memcmp(&name[len-5], ".slul", 5))) { error = CSLUL_E_NONSLULFILE; goto err; } else if (UNLIKELY((len == 9 && !memcmp(name, "main.slul", 9)) || (len >= 11 && !memcmp(&name[len-10], "/main.slul", 10)))) { error = CSLUL_E_SOURCEMAINSLUL; goto err; } len -= 4; /* no point in checking ".slul" ending, but keep "." for checking for "/.slul" */ cp = name; part = name; in_ext = 0; for (; len--; cp++) { char c = *cp; size_t partlen; if (LIKELY((c>='a' && c<='z') || (c>='0' && c<='9') || c=='_') || (c>='A' && c<='Z')) { /* XXX consider disallowing uppercase */ continue; /* Valid character */ } else if (UNLIKELY(c != '.' && c != '/')) { error = CSLUL_E_BADFILENAMECHAR; goto err; } /* End of filename component (directory, basename or extension) */ partlen = cp - part; if (UNLIKELY(partlen == 0)) { if (in_ext) { cp--; error = (c == '.' ? CSLUL_E_FILENAMEDOTDOT : /* contains ".." */ CSLUL_E_DIRTRAILINGDOT); /* trailing "." in dir */ } else if (c=='/' && cp==name) { error = CSLUL_E_FILENAMEROOT; } else { error = (c == '.' ? CSLUL_E_FILENAMEDOT : /* filename begins with "." */ CSLUL_E_FILENAMEDOUBLESLASH); /* contains "//" */ } goto err; } else if (UNLIKELY(is_win_device(part, partlen))) { cp = part; error = CSLUL_E_DEVICEFILENAME; goto err; } else if (UNLIKELY(in_ext && partlen == 4 && !memcmp(part, "slul", 4))) { cp -= 5; error = CSLUL_E_BADDIRNAME; /* .slul in directory */ goto err; } part = cp+1; in_ext = (c == '.'); } return 1; err: error_textlen(ctx, error, ctx->tokline, ctx->tokcolumn + (cp ? cp-name : 0), ctx->tokval, ctx->toklen); return 0; } /** Checks a module name. Module names may contain the characters a-z0-9_ and must be 1-50 characters long and must begin with a letter. Also, a module name may not be a device name on Windows (such as lpt1) */ static int check_modname(struct CSlul *ctx, const char *name, size_t len) { enum CSlulErrorCode error; if (UNLIKELY(len == 0 || len > 50)) { error = CSLUL_E_MODNAMELENGTH; } else if (UNLIKELY((*name < 'a' || *name > 'z') && (*name < 'A' || *name > 'Z'))) { error = CSLUL_E_MODNAMESTART; } else { while ((*name >= 'a' && *name <= 'z') || (*name >= '0' && *name <= '9') || *name == '_') { if (!--len) { if (UNLIKELY(is_win_device(name, len))) { error = CSLUL_E_DEVICEMODNAME; goto err; } return 1; } name++; } error = CSLUL_E_MODNAMECHAR; } err: error_tok(ctx, error); return 0; } /** Checks a version string */ static int check_verstr(struct CSlul *ctx, const char *ver, size_t len) { enum CSlulErrorCode error; if (UNLIKELY(len == 0 || len > 50)) { error = CSLUL_E_VERSTRLENGTH; } else if (UNLIKELY(*ver < '0' || *ver > '9')) { error = CSLUL_E_VERSTRSTART; } else if (UNLIKELY(len >= 2 && ver[0] == '0' && ver[1] >= '0' && ver[1] <= '9')) { /* Note: Leading zeros are allowed in the second version component and later, in order to allow "calendar versions" like 2021.01.02 */ error = CSLUL_E_VERSTRZEROPREFIXED; } else { int group_empty = 1; /* TODO check alpha/beta/gamma/rc version strings: - require ~ before - require . after (or end) e.g. 1.0~alpha.1 - or even forbid components that start with a letter and then contain numbers? (but somehow allow git suffixes?) - or even only allow allowlisted strings that start with a letter? e.g. alpha/beta/gamma/rc/unreleased - shouldn't unreleased come before alpha? maybe just use a ~ at the end? but that is not as clear. */ for (; len--; ver++) { char ch = *ver; if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z')) { group_empty = 0; } else if (ch == '.' || ch == '~') { if (UNLIKELY(group_empty)) { error = CSLUL_E_VERSTRDOTDOT; goto err; } else if (UNLIKELY(!len)) { error = CSLUL_E_VERSTRDOTEND; goto err; } group_empty = 1; } else { error = CSLUL_E_VERSTRCHAR; goto err; } } return 1; } err: error_tok(ctx, error); return 0; } #define MFR_IGNORE_ERRORS 0 #define MFR_REPORT_ERRORS 1 /** * Matches a flag of a build dependency */ static unsigned match_flag(struct CSlul *ctx, int report_error) { size_t toklen = ctx->toklen; const char *tokval = ctx->tokval; /* TODO revise or remove the "bundled" flag */ /* TODO revise or remove the "optional" flag */ switch (ctx->tokhash) { case H_NESTEDONLY: TOK_EQ_RETURN("nestedonly", CSLUL_DF_NESTEDONLY) case H_OPTIONAL: TOK_EQ_RETURN("optional", CSLUL_DF_OPTIONAL) case H_UNSTABLE_API: TOK_EQ_RETURN("unstable_api", CSLUL_DF_UNSTABLE_API) } if (report_error) { const char *flagname = aalloc_memzdup(ctx, ctx->tokval, toklen); if (flagname) { error_text(ctx, CSLUL_E_BADDEPFLAG, ctx->tokline, ctx->tokcolumn, flagname); } } return 0; } int is_app(const struct Module *mod) { return mod->type == CSLUL_MT_APP; } static int is_library(const struct Module *mod) { return mod->type == CSLUL_MT_LIBRARY || mod->type == CSLUL_MT_PLUGIN; } static int moduletype_has_own_api(const struct Module *mod) { return mod->type == CSLUL_MT_LIBRARY || mod->type == CSLUL_MT_LIBRARYSPEC || mod->type == CSLUL_MT_PLUGINSPEC; } static void check_interface_depends(struct CSlul *ctx, const struct Module *mod) { const struct CSlulDependency *dep; const struct CSlulInterfaceDep *idep; for (dep = mod->first_dep; dep; dep = dep->next) { /* For each \interface_depends */ for (idep = dep->first_ifacever; idep; idep = idep->sincever_next) { if (idep->node.length != 0) { /* has since-version */ /* The since-version must exist as a \api_defs */ const struct ApiDef *apidef = (const struct ApiDef *) tree_search_node(ctx, mod->apidefs_root, &idep->node); if (UNLIKELY(!apidef)) { message_set_ident(ctx, 0, CSLUL_LT_MAIN, &idep->node); message_set_ident(ctx, 1, CSLUL_LT_DEPENDENCY, &dep->node); message_final(ctx, CSLUL_E_IFACEDEPWITHOUTAPIDEF); } } } } } /** Called when the module header ends, and code begins. */ static void check_mh_end(struct CSlul *ctx, struct Module *mod) { assert(ctx->in_moduleheader); if (UNLIKELY(!mod->min_langver)) { error_text(ctx, CSLUL_E_MISSINGATTR, 0, 0, "slul"); } if (!mod->type) { mod->type = CSLUL_MT_APP; } if (!mod->name) { if (LIKELY(mod->type == CSLUL_MT_APP)) { mod->name = "app"; mod->namelen = 3; } else { error_text(ctx, CSLUL_E_MISSINGATTR, 0, 0, "name"); } } if (UNLIKELY(!ctx->num_sourcefiles && is_library(mod) && ctx->phase == CSLUL_P_MODULEHEADER)) { /* when compiling lib itself */ /* The main.slul file contains the interface only, so there must be at least one source file with the implementation */ error_linecol(ctx, CSLUL_E_MISSINGSOURCE, 0, 0); } if (UNLIKELY(mod->impl_minlangver && is_app(mod))) { error_linecol(ctx, CSLUL_E_APPWITHIMPLSLULVER, 0, 0); } if (UNLIKELY(moduletype_has_own_api(mod) && !mod->is_unstable && ctx->parsed_module->first_apidef == NULL)) { error_linecol(ctx, CSLUL_E_STABLEWITHOUTAPIDEF, 0, 0); } if (UNLIKELY(!ctx->parser.mh.seen_explicit_slulrt_dep && (mod->namehash != H_SLULRT || mod->namelen != 6) && !ctx->cfg->skip_implicit_slulrt_dep)) { /* All modules implicitly depend on slulrt (except slulrt itself) */ if (LIKELY((int)build_add_dep(ctx, H_SLULRT, 6, "slulrt") <= CSLUL_E_NOERROR)) { struct CSlulDependency *dep = mod->last_dep; assert(dep != NULL); dep->min_version = "0.0.0";/* might increase in future lang-versions */ dep->minverlen = 5; /* TODO depend on a specific api_hash here! */ } } determine_effective_langver(ctx, 0); check_interface_depends(ctx, mod); start_of_code(ctx); } static enum ModuleAttr match_attr(struct CSlul *ctx) { size_t toklen = ctx->toklen; const char *tokval = ctx->tokval; switch (ctx->tokhash) { case H_SLUL: TOK_EQ_RETURN("slul", MASlul) case H_NAME: TOK_EQ_RETURN("name", MAName) case H_VERSION: TOK_EQ_RETURN("version", MAVersion) case H_TYPE: TOK_EQ_RETURN("type", MAType) case H_REPO: TOK_EQ_RETURN("repo", MARepo) case H_REPO_DOT_MIRROR: TOK_EQ_RETURN("repo.mirror", MARepoMirror) case H_WEBSITE: TOK_EQ_RETURN("website", MAWebsite) case H_LICENSE: TOK_EQ_RETURN("license", MALicense) case H_LICENSE_DOT_TEXT: TOK_EQ_RETURN("license.text", MALicenseText) case H_LICENSE_DOT_LIST: TOK_EQ_RETURN("license.list", MALicenseList) case H_DEPENDS: TOK_EQ_RETURN("depends", MADepends) case H_INTERFACE_DEPENDS: TOK_EQ_RETURN("interface_depends", MAInterfaceDepends) case H_API_DEF: TOK_EQ_RETURN("api_def", MAApiDef) case H_SOURCE: TOK_EQ_RETURN("source", MASource) } return MANone; } static enum CSlulModuleType match_modtype(struct CSlul *ctx) { size_t toklen = ctx->toklen; const char *tokval = ctx->tokval; switch (ctx->tokhash) { case H_INTERNAL: TOK_EQ_RETURN("internal", CSLUL_MT_INTERNAL) case H_LIBRARY: TOK_EQ_RETURN("library", CSLUL_MT_LIBRARY) case H_PLUGIN: TOK_EQ_RETURN("plugin", CSLUL_MT_PLUGIN) case H_LIBRARYSPEC: TOK_EQ_RETURN("libraryspec", CSLUL_MT_LIBRARYSPEC) case H_PLUGINSPEC: TOK_EQ_RETURN("pluginspec", CSLUL_MT_PLUGINSPEC) case H_APP: TOK_EQ_RETURN("app", CSLUL_MT_APP) } error_tok(ctx, CSLUL_E_BADMODULETYPE); return CSLUL_MT_INVALID; } static enum LanguageVersion match_langver(struct CSlul *ctx) { size_t toklen = ctx->toklen; const char *tokval = ctx->tokval; switch (ctx->tokhash) { /* When adding a version, it must be added in cslul_supported_langver() in context.c also */ case H_0_0_0: TOK_EQ_RETURN("0.0.0", LANGVER_0_0_0) } return LANGVER_BAD; } static HashCode calc_casenorm_hash(const struct CSlul *ctx) { size_t len = ctx->toklen; const char *c; HashCode h = 0; for (c = ctx->tokval; len > 0; c++) { h = HASH(h, *c | 0x20); len--; } return h; } static void set_unstable(struct CSlul *ctx, struct Module *mod) { if (UNLIKELY(!moduletype_has_own_api(mod) && mod->type > 0)) { error_tok(ctx, CSLUL_E_MODTYPEUNSTABLE); } mod->is_unstable = 1; } void parse_moduleheader(struct CSlul *ctx) { enum CSlulModuleHeaderToken tok; struct Module *mod = ctx->parsed_module; const char *value; size_t len; enum ModuleAttr attribute; enum CSlulErrorCode errcode; if (UNLIKELY(ctx->parser.mh.state != MHPDone)) { switch (ctx->parser.mh.state) { case MHPAttribute: goto in_attribute; case MHPSlulImpl: goto in_slul_impl; case MHPSlulImplVer: goto in_slul_implver; case MHPSlulUpTo: goto in_slul_upto; case MHPSlulMaxVer: goto in_slul_maxver; case MHPVersionFlags: goto in_version_flags; case MHPDependsVersion: goto in_depends_version; case MHPDependsFlags: goto in_depends_flags; case MHPIfaceDepVersion: goto in_ifacedep_version; case MHPIfaceDepFlags: goto in_ifacedep_flags; case MHPIfaceDepSinceVer: goto in_ifacedep_sincever; case MHPApiDefHash: goto in_apidef_apihash; case MHPApiDefKeywords: goto in_apidef_keywords; case MHPSkipAttribute: goto skip_attribute; case MHPDone:; } } for (;;) { next_token: tok = cslul_ll_next_mh_token(ctx); reuse_token: switch (tok) { case CSLUL_MHT_EOF: check_mh_end(ctx, mod); return; case CSLUL_MHT_NEEDDATA: ctx->parser.mh.state = MHPDone; goto buffer_end; case CSLUL_MHT_BareIdentifier: if (LIKELY(ctx->toklen == 4 || ctx->toklen == 5)) { enum CSlulToken next_token; if (ctx->tokhash == H_DATA) { next_token = CSLUL_T_KW_Data; } else if (ctx->tokhash == H_FUNC) { next_token = CSLUL_T_KW_Func; } else if (ctx->tokhash == H_TYPE) { next_token = CSLUL_T_KW_Type; } else if (ctx->tokhash == H_SINCE) { next_token = CSLUL_T_KW_Since; ctx->tmplen = 0; assert(ctx->tokline); ctx->parser.slul.version_line = ctx->tokline; } else goto invalid_bare_ident; ctx->reused_token.slul = next_token; end_of_modheader: if (LIKELY(ctx->parser.mh.attr) || LIKELY(mod->min_langver)) { /* A header exists. Check it */ check_mh_end(ctx, mod); } else { error_tok(ctx, CSLUL_E_MISSINGMODULEHEADER); start_of_code(ctx); } return; } invalid_bare_ident: if (ctx->toklen != 1 && !token_could_be_mh_attr(ctx)) { error_tok(ctx, CSLUL_E_BADTOPLEVELTOKEN); goto end_of_modheader; } error_tok(ctx, CSLUL_E_MHNOBACKSLASH); /* Fall through */ case CSLUL_MHT_AttrName: { enum ModuleAttr lastd = ctx->parser.mh.attr; attribute = match_attr(ctx); ctx->parser.mh.attr_line = ctx->tokline; ctx->parser.mh.attr_col = ctx->tokcolumn; ctx->parser.mh.attr = attribute; if (UNLIKELY(!attribute)) { error_tok(ctx, CSLUL_E_BADATTRNAME); goto skip_attribute; } else if (UNLIKELY(attribute == lastd) && attribute != MADepends && attribute != MAApiDef && attribute != MASource && attribute != MAInterfaceDepends) { error_attr(ctx, CSLUL_E_DUPLICATEATTR); } else if (UNLIKELY(attribute < lastd)) { error_attr(ctx, CSLUL_E_WRONGATTRORDER); } goto in_attribute; } case CSLUL_MHT_AttrValue: error_tok(ctx, CSLUL_E_TOOMANYATTRPARAMS); break; } } in_attribute: attribute = ctx->parser.mh.attr; tok = cslul_ll_next_mh_token(ctx); if (UNLIKELY(tok != CSLUL_MHT_AttrValue)) { if (tok == CSLUL_MHT_NEEDDATA) { ctx->parser.mh.state = MHPAttribute; goto buffer_end_noeof; } else { error_text(ctx, CSLUL_E_MISSINGATTRVALUE, ctx->prev_tok_line, ctx->prev_tok_endcol, attr2str[ctx->parser.mh.attr]); /* Set dummy value to prevent error about missing attribute */ switch ((int)attribute) { case MASlul: mod->min_langver = LANGVER_BAD; break; case MAName: mod->name = ""; break; case MAType: mod->type = CSLUL_MT_INVALID; break; default: ; } goto reuse_token; } } cslul_ll_current_value(ctx, &value, &len); if (attribute > MAType && !mod->type) { mod->type = CSLUL_MT_APP; /* No type set. Set default one */ } switch (attribute) { case MASlul: { if (UNLIKELY((mod->min_langver = match_langver(ctx)) == LANGVER_BAD)) { error_tok(ctx, CSLUL_E_UNSUPPORTEDLANGVER); return; /* can't continue safely */ } in_slul_impl: tok = cslul_ll_next_mh_token(ctx); if (UNLIKELY(tok <= 0)) { ctx->parser.mh.state = MHPSlulImpl; goto buffer_end; } else if (tok != CSLUL_MHT_AttrValue) { goto reuse_token; } else if (ctx->tokhash != H_IMPL || ctx->toklen != 4) { goto slul_upto_kw; /* check for "upto" instead */ } in_slul_implver: tok = cslul_ll_next_mh_token(ctx); if (UNLIKELY(tok != CSLUL_MHT_AttrValue)) { if (tok <= 0) { ctx->parser.mh.state = MHPSlulImplVer; goto buffer_end; } else { error_sameline(ctx, CSLUL_E_BADLANGVERSYNTAX); goto reuse_token; } } mod->impl_minlangver = match_langver(ctx); if (UNLIKELY(mod->impl_minlangver > 0 && mod->impl_minlangver < mod->min_langver)) { error_tok(ctx, CSLUL_E_SLULIMPLVEROLDER); } in_slul_upto: tok = cslul_ll_next_mh_token(ctx); if (UNLIKELY(tok <= 0)) { ctx->parser.mh.state = MHPSlulUpTo; goto buffer_end; } else if (tok != CSLUL_MHT_AttrValue) { goto reuse_token; } else { slul_upto_kw: if (UNLIKELY(ctx->tokhash != H_UPTO || ctx->toklen != 4)) { error_sameline(ctx, CSLUL_E_BADLANGVERSYNTAX); goto skip_attribute; } } in_slul_maxver: tok = cslul_ll_next_mh_token(ctx); if (UNLIKELY(tok != CSLUL_MHT_AttrValue)) { if (tok <= 0) { ctx->parser.mh.state = MHPSlulMaxVer; goto buffer_end; } else { error_sameline(ctx, CSLUL_E_BADLANGVERSYNTAX); goto reuse_token; } } mod->max_langver = match_langver(ctx); if (mod->max_langver == LANGVER_BAD) { if (LIKELY(check_verstr(ctx, ctx->tokval, ctx->toklen))) { /* TODO Check that version does not come before the latest supported version. If so, there is a typo. */ } } else if (UNLIKELY(mod->max_langver > 0 && ( mod->max_langver < mod->impl_minlangver || mod->max_langver < mod->min_langver || mod->impl_minlangver == LANGVER_BAD))) { error_tok(ctx, CSLUL_E_SLULMAXVEROLDER); } break; } case MAName: if (LIKELY(check_modname(ctx, value, len))) { char *copy = aalloc_memzdup(ctx, value, len); if (!copy) goto outofmem; mod->name = copy; mod->namelen = len; mod->namehash = ctx->tokhash; } else if (!mod->name) { /* prevent error about missing name attribute */ mod->name = ""; mod->namelen = 0; } break; case MAType: mod->type = match_modtype(ctx); break; case MAVersion: mod->is_unstable = !moduletype_has_own_api(mod); if (ctx->tokhash == H_UNSTABLE_API && len == 12 && !memcmp(value, "unstable_api", 12)) { set_unstable(ctx, mod); break; } else if (LIKELY(check_verstr(ctx, value, len))) { char *copy = aalloc_memzdup(ctx, value, len); if (!copy) goto outofmem; mod->version = copy; mod->versionlen = len; } /* TODO should it be required that modnameN (N: number) has version >= N.x or version < 1.0? - but this could fail with e.g. "html5", "id3", "jpeg2000", "x509", etc. - it could work if major versions are separated with an underscore, e.g. "gtk_2" - This can also be extended to minor versions for packages that stop keeping backwards compatibilty, or that break compatibility at some minor version. E.g. "somepkg_4_1" */ in_version_flags: tok = cslul_ll_next_mh_token(ctx); if (tok != CSLUL_MHT_AttrValue) { if (UNLIKELY(tok == CSLUL_MHT_NEEDDATA)) { ctx->parser.mh.state = MHPVersionFlags; goto buffer_end; } else goto reuse_token; } cslul_ll_current_value(ctx, &value, &len); /* TODO should there be a "unreleased" keyword also? - should unreleased code have a version at all? - if not, how to compare versions? - set it to an "infinite" value? - set it to highest API hash? */ if (ctx->tokhash == H_UNSTABLE_API && len == 12 && !memcmp(value, "unstable_api", 12)) { set_unstable(ctx, mod); } else { error_tok(ctx, CSLUL_E_BADVERSIONFLAG); goto skip_attribute; } break; case MARepo: case MARepoMirror: case MAWebsite: case MALicense: case MALicenseText: case MALicenseList: /* TODO */ break; case MADepends: { struct CSlulDependency *dep; /* Module name of dependency */ if (!check_modname(ctx, value, len)) goto skip_attribute; if (UNLIKELY(ctx->tokhash == mod->namehash && mod->name && len == mod->namelen && !memcmp(value, mod->name, len))) { error_tok(ctx, CSLUL_E_SELFDEPEND); goto skip_attribute; } if (UNLIKELY(ctx->tokhash == H_SLULRT && len == 6)) { ctx->parser.mh.seen_explicit_slulrt_dep = 1; } errcode = build_add_dep(ctx, ctx->tokhash, len, value); if ((int)errcode >= CSLUL_E_FIRST_ERRCODE) goto add_failure; assert(mod->last_dep != NULL); in_depends_version: tok = cslul_ll_next_mh_token(ctx); if (UNLIKELY(tok != CSLUL_MHT_AttrValue)) { if (tok == CSLUL_MHT_NEEDDATA) { ctx->parser.mh.state = MHPDependsVersion; goto buffer_end; } else { error_sameline(ctx, CSLUL_E_NODEPENDSVERSION); goto reuse_token; } } cslul_ll_current_value(ctx, &value, &len); dep = mod->last_dep; if (ctx->tokhash == H_BUNDLED && len == 7 && !memcmp(value, "bundled", 7)) { dep->flags = CSLUL_DF_BUNDLED; } else { if (LIKELY(check_verstr(ctx, value, len))) { dep->min_version = aalloc_memzdup(ctx, value, len); if (!dep->min_version) return; /* out of memory */ dep->minverlen = len; } } in_depends_flags: dep = mod->last_dep; for (;;) { unsigned flag; tok = cslul_ll_next_mh_token(ctx); if (UNLIKELY(tok == CSLUL_MHT_NEEDDATA)) { ctx->parser.mh.state = MHPDependsFlags; goto buffer_end; } else if (tok != CSLUL_MHT_AttrValue) { goto reuse_token; } flag = match_flag(ctx, MFR_REPORT_ERRORS); if (UNLIKELY((dep->flags & flag) != 0)) { error_tok(ctx, CSLUL_E_DUPLDEPFLAG); } else if (UNLIKELY(flag < dep->flags)) { error_tok(ctx, CSLUL_E_DEPFLAGSORDER); } dep->flags |= flag; } break; } /* TODO support for "ABI locks"? i.e. locking to a hash of the module's interface */ /* TODO need an attribute for renaming conflicting symbols: (but it also has to be solved at the binary level. perhaps with symbol versioning?) \rename something St* Sth* \rename stuff St* Stf* \rename badlib * BadLib*_ badlib_* */ case MAInterfaceDepends: { struct CSlulDependency *dep; unsigned ifaceflags; /* Checks */ if (UNLIKELY(is_app(mod))) { error_attr(ctx, CSLUL_E_APPWITHIFACEDEP); } /* Module name of interface dependency */ if (!check_modname(ctx, value, len)) goto skip_attribute; if (UNLIKELY(ctx->tokhash == mod->namehash && mod->name && len == mod->namelen && !memcmp(value, mod->name, len))) { error_tok(ctx, CSLUL_E_SELFDEPEND); goto skip_attribute; } if (UNLIKELY(ctx->tokhash == H_SLULRT && len == 6)) { ctx->parser.mh.seen_explicit_slulrt_dep = 1; } dep = findadd_dep(ctx, ctx->tokhash, len, value); if (!dep) goto outofmem; /* FIXME remove/revise this */ if (UNLIKELY((dep->flags & CSLUL_DF_BUNDLED) != 0 && !mod->is_unstable)) { error_tok(ctx, CSLUL_E_IFACEDEPENDBUNDLED); } ctx->parser.mh.current_dep = dep; in_ifacedep_version: tok = cslul_ll_next_mh_token(ctx); if (UNLIKELY(tok != CSLUL_MHT_AttrValue)) { if (tok == CSLUL_MHT_NEEDDATA) { ctx->parser.mh.state = MHPIfaceDepVersion; goto buffer_end; } else { error_sameline(ctx, CSLUL_E_NOIFACEDEPVERSION); goto reuse_token; } } cslul_ll_current_value(ctx, &value, &len); /* TODO support "bundled"/versionless dependencies? */ /* FIXME support "none" version to remove old interface dependencies? */ if (LIKELY(check_verstr(ctx, value, len))) { int vcmp; const char *minver = aalloc_memzdup(ctx, value, len); if (!minver) return; /* out of memory */ ctx->parser.mh.version = minver; ctx->parser.mh.verlen = len; dep = ctx->parser.mh.current_dep; vcmp = dep->min_version ? versioncmp(value, len, dep->min_version, dep->minverlen) : 0; if ((dep->flags & CSLUL_DF_IFACEONLY) != 0) { if (vcmp >= 0) { dep->min_version = minver; dep->minverlen = len; } } else if (vcmp > 0) { error_tok(ctx, CSLUL_E_IFACEDEPHIGHERTHANDEP); } } else { ctx->parser.mh.version = ""; ctx->parser.mh.verlen = 0; } ctx->parser.mh.depflags = 0; in_ifacedep_flags: dep = ctx->parser.mh.current_dep; ifaceflags = ctx->parser.mh.depflags; for (;;) { unsigned flag; tok = cslul_ll_next_mh_token(ctx); if (UNLIKELY(tok == CSLUL_MHT_NEEDDATA)) { ctx->parser.mh.depflags = ifaceflags; ctx->parser.mh.state = MHPIfaceDepFlags; goto buffer_end; } else if (tok != CSLUL_MHT_AttrValue) { if (UNLIKELY(!mod->is_unstable && mod->type > 0)) { error_sameline(ctx, CSLUL_E_NOSINCE); goto reuse_token; } else { ctx->reused_token.mh = tok; value = NULL; len = 0; goto add_ifacedep; } } flag = match_flag(ctx, MFR_IGNORE_ERRORS); if (!flag) { break; } else if (UNLIKELY((ifaceflags & flag) != 0)) { error_tok(ctx, CSLUL_E_DUPLIFACEDEPFLAG); } else { if (UNLIKELY(flag == CSLUL_DF_UNSTABLE_API && !mod->is_unstable && mod->type > 0)) { /* TODO unstable \interface_depends in stable libraries? - disallow it? - require an explicit "since unreleased" and "\version X.Y.Z unreleased"? */ } if (UNLIKELY(flag < ifaceflags)) { error_tok(ctx, CSLUL_E_IFACEDEPFLAGSORDER); } } ifaceflags |= flag; } /* TODO should "optional" and "bundled" be allowed here, and what should they do? */ if (!dep->last_ifacever) { /* the first \interface_depends */ if ((dep->flags & CSLUL_DF_IFACEONLY) == 0) { unsigned depflags = dep->flags & ~CSLUL_DF_IFACEONLY; /*unsigned flags_added = ifaceflags & ~depflags;*/ unsigned flags_removed = depflags & ~ifaceflags; if ((flags_removed & CSLUL_DF_NESTEDONLY) != 0) { error_tok(ctx, CSLUL_E_MISSINGNESTEDONLY); } } else { assert(dep->flags == CSLUL_DF_IFACEONLY/* only */); dep->flags |= ifaceflags; } dep->iface_flags = ifaceflags; } else if (UNLIKELY(ifaceflags != dep->iface_flags)) { error_tok(ctx, CSLUL_E_IFACEDEPFLAGSCHANGE); } /* TODO should support api hashes also! */ assert(tok == CSLUL_MHT_AttrValue); if (UNLIKELY(ctx->tokhash != H_SINCE || ctx->toklen != 5 || memcmp(ctx->tokval, "since", 5))) { error_sameline(ctx, CSLUL_E_BADIFACEDEPFLAG); goto reuse_token; } in_ifacedep_sincever: tok = cslul_ll_next_mh_token(ctx); if (UNLIKELY(tok != CSLUL_MHT_AttrValue)) { if (tok == CSLUL_MHT_NEEDDATA) { ctx->parser.mh.state = MHPIfaceDepSinceVer; goto buffer_end; } else { error_sameline(ctx, CSLUL_E_NOSINCEVERSION); goto reuse_token; } } /* TODO check that \interface_depends since-versions come in ascending order */ assert(ctx->parser.mh.current_dep != NULL); cslul_ll_current_value(ctx, &value, &len); add_ifacedep: { const char *sincever = NULL; if (value && LIKELY(check_verstr(ctx, value, len))) { sincever = aalloc_memzdup(ctx, value, len); if (!sincever) goto outofmem; } errcode = build_add_ifacedep(ctx, ctx->parser.mh.current_dep, ctx->parser.mh.verlen, ctx->parser.mh.version, ctx->tokhash, len, sincever); if (errcode > 0) goto add_failure; } break; } case MAApiDef: { struct TreeNode *insresult; struct ApiDef *apidef; /* XXX should the api-hash really be optional? - perhaps only one the last version? - and require a specific keyword instead (e.g. unreleased) */ /* Syntax is \api_def VERSION [APIHASH] [retracted] */ /* XXX should be: Syntax is \api_def VERSION APIHASH|unreleased [retracted] */ if (UNLIKELY(is_app(mod))) { error_attr(ctx, CSLUL_E_APPWITHAPI); } if (UNLIKELY(mod->version == NULL)) { error_attr(ctx, CSLUL_E_APIDEFWITHOUTMODVER); } if (UNLIKELY(mod->num_apidefs >= MAX_APIDEFS)) { if (mod->num_apidefs == MAX_APIDEFS) { error_attr(ctx, CSLUL_E_MAXAPIDEFS); mod->num_apidefs++; /* silence further errors */ /* TODO restore off-by-one when leaving module header XXX or instead use a separate field for this an \source, to only report the error once */ } goto skip_attribute; } mod->num_apidefs++; /* Allocate and process version part */ (void)check_verstr(ctx, value, len); apidef = aallocp(ctx, sizeof(struct ApiDef)); if (apidef == NULL) goto outofmem; PROTECT_STRUCT(*apidef); insresult = tree_insert(ctx, &mod->apidefs_root, ctx->tokhash, len, value, (struct TreeNode*)apidef, 0); if (insresult == NULL) goto outofmem; if (UNLIKELY(!insresult->is_new)) { error_attr(ctx, CSLUL_E_DUPLICATEAPIDEF); goto skip_attribute; } if (!mod->first_apidef) mod->first_apidef = apidef; if (mod->last_apidef) mod->last_apidef->next = apidef; mod->last_apidef = apidef; apidef->next = NULL; apidef->flags = 0; apidef->index = mod->num_apidefs-1; in_apidef_apihash: apidef = mod->last_apidef; tok = cslul_ll_next_mh_token(ctx); cslul_ll_current_value(ctx, &value, &len); if (UNLIKELY(tok == CSLUL_MHT_NEEDDATA)) { ctx->parser.mh.state = MHPApiDefHash; goto buffer_end; } else if (tok != CSLUL_MHT_AttrValue) { /* XXX should this really be optional?!? */ goto reuse_token; } if (len <= CSLUL_APIHASHCHARS / 2) { /* Assume that it is a flag keyword */ goto apidef_keyword_reuse; } if (UNLIKELY(len != CSLUL_APIHASHCHARS)) { error_tok(ctx, CSLUL_E_BADAPIHASHLEN); goto skip_attribute; } if (UNLIKELY(!unbase64(value, len, apidef->apihash, CSLUL_APIHASHCHARS))) { error_tok(ctx, CSLUL_E_BADAPIHASH); goto skip_attribute; } apidef->flags |= APIDEF_HAS_HASH; /* TODO the "API version hash" could be computed as follows: - for each version, keep a hash state, and update it: - initially, with the version, previous-version-hash, and flags (such as "retracted") - with each definition that is specified with that exact version (including deprecations, deletions and backports) - hashed data: add/delete/deprecate || name || type (no distinction between add and backport is needed) - at the end, update with the number of definitions (to prevent appending of more data) */ /* TODO build apidefs array when module header has been parsed and restore the apidef count (if needed) */ in_apidef_keywords: apidef = mod->last_apidef; tok = cslul_ll_next_mh_token(ctx); cslul_ll_current_value(ctx, &value, &len); apidef_keyword_reuse: if (UNLIKELY(tok == CSLUL_MHT_NEEDDATA)) { ctx->parser.mh.state = MHPApiDefKeywords; goto buffer_end; } else if (tok != CSLUL_MHT_AttrValue) { goto reuse_token; } else if (ctx->tokhash == H_RETRACTED && len == 9 && !memcmp(value, "retracted", 9)) { apidef->flags |= APIDEF_RETRACTED; } else { error_sameline(ctx, CSLUL_E_BADAPIDEFFLAG); goto skip_attribute; } break; } case MASource: if (UNLIKELY(mod->type == CSLUL_MT_LIBRARYSPEC || mod->type == CSLUL_MT_PLUGINSPEC)) { error_attr(ctx, CSLUL_E_SPECWITHSOURCE); } if (ctx->phase != CSLUL_P_MODULEHEADER) { /* We don't need the source list when parsing dependencies */ break; } /* TODO enforce alphabetic ordering? how about directories? */ if (!check_slul_filename(ctx, value, len)) break; if (UNLIKELY(ctx->num_sourcefiles >= MAX_SOURCEFILES)) { if (ctx->num_sourcefiles == MAX_SOURCEFILES) { error_attr(ctx, CSLUL_E_MAXSOURCEFILES); ctx->num_sourcefiles++; /* silence further errors */ } break; } ctx->num_sourcefiles++; /* TODO consider disallowing UpperCase in filenames */ errcode = build_add_source(ctx, calc_casenorm_hash(ctx), len, value); if (errcode > 0) goto add_failure; break; case MANone: goto skip_attribute_reuse_token; } goto next_token; add_failure: if (errcode == CSLUL_E_OUTOFMEMORY) goto outofmem; if (tok == CSLUL_MHT_AttrValue) error_tok(ctx, errcode); else error_prevtok_line(ctx, errcode); /* Fall through */ skip_attribute: for (;;) { tok = cslul_ll_next_mh_token(ctx); skip_attribute_reuse_token: if (tok == CSLUL_MHT_NEEDDATA) { ctx->parser.mh.state = MHPSkipAttribute; goto buffer_end; } else if (tok != CSLUL_MHT_AttrValue) { goto reuse_token; } } buffer_end_noeof: if (tok == CSLUL_MHT_EOF) { error_tok(ctx, CSLUL_E_UNEXPECTEDEOF); return; } buffer_end: if (tok == CSLUL_MHT_EOF) { check_mh_end(ctx, mod); } outofmem:; /* Error is already reported and cslul_ll_parse checks ctx->has_errors, so nothing has to be done here. */ }