#include "version.h" #include "config.h" #include "digest/digest.h" #include "l2su.h" #include "macros.h" #include "endian.h" #include "command.h" #include #include #include #include #include #include #include #include #include const char *const l2_version__messages[] = { "Success", "Malformed version manifest", "Version manifest JSON error", "Allocation failed", "OS error", "Error downloading version manifest", "Version not found", "Max recursion depth exceeded", NULL }; #define L2_VERSION_MANIFEST_ETAG_PATH "/.l2_vermanifest_etag" #define L2_VERSION_MANIFEST_PATH "/version_manifest_v2.json" #define L2_VERSION_MANIFEST_EXP_TIME ((time_t)120) unsigned l2_version__load_manifest(json_t **out_json); unsigned l2_version__load_all_from_json(json_t *json); unsigned l2_version__add_remote(json_t *obj); unsigned l2_version_load_remote(void) { json_t *vers = NULL; unsigned r = l2_version__load_manifest(&vers); if (r != VERSION_SUCCESS) goto done; r = l2_version__load_all_from_json(vers); done: json_decref(vers); return r; } void l2_version__load_local(const char *id, size_t expectsz, l2_sha1_digest_t *digest, json_t **ojson); bool l2_version__download_remote(struct l2_version_remote *remote, json_t **ojson); unsigned l2_version__load_or_download(const char *name, json_t **ojs) { json_t *js = NULL; struct l2_version_remote *remote = NULL; for (struct l2_version_remote *cur = l2_state.ver_remote_head; cur; cur = cur->next) { if (!strcmp(cur->id, name)) { remote = cur; break; } } if (remote) { l2_version__load_local(remote->id, 0, &remote->sha1, &js); if (!js) { CMD_DEBUG0("local version not found, downloading"); if (!l2_version__download_remote(remote, &js) || !js) return VERSION_EDOWNLOAD; } } else { l2_version__load_local(name, 0, NULL, &js); if (!js) return VERSION_ENOTFOUND; } *ojs = js; return VERSION_SUCCESS; } unsigned l2_version__load_recursive(const char *name, json_t *target, int depth) { json_t *other = NULL; json_t *inherit; if (depth <= 0) return VERSION_ERECURSE; unsigned res = l2_version__load_or_download(name, &other); if (res != VERSION_SUCCESS) return res; l2_json_merge_objects(target, other); inherit = json_object_get(other, "inheritsFrom"); if (!json_is_string(inherit)) { json_decref(other); return VERSION_SUCCESS; } json_decref(other); return l2_version__load_recursive(json_string_value(inherit), target, depth - 1); } unsigned l2_version_load_local(const char *name, json_t **ojson) { json_t *js = json_object(); if (!js) return VERSION_EJSON; unsigned res = l2_version__load_recursive(name, js, 10); *ojson = js; return res; } void l2_version__load_local(const char *id, size_t expectsz, l2_sha1_digest_t *digest, json_t **ojson) { char *path; size_t temp; L2_ASPRINTF(path, temp, "%s/versions/%s/%s.json", l2_state.paths.data, id, id); FILE *ver = fopen(path, "r"); if (!ver) return; int res = l2_version_check_integrity(ver, digest, expectsz); if (res < 0) { CMD_WARN("Error checking version file integrity for %s", id); return; } else if (res == 0) { CMD_DEBUG("Version %s (%s) has bad integrity", id, path); return; } json_t *js; json_error_t jerr; rewind(ver); js = json_loadf(ver, JSON_REJECT_DUPLICATES, &jerr); if (!js) { CMD_WARN("JSON error loading %s: %s", id, jerr.text); return; } *ojson = js; } /* downloads a remote version */ bool l2_version__download_remote(struct l2_version_remote *remote, json_t **ojson) { char *dirname; char *fname; size_t temp; FILE *save = NULL; json_t *js = NULL; L2_ASPRINTF(dirname, temp, "%s/versions/%s", l2_state.paths.data, remote->id); L2_ASPRINTF(fname, temp, "%s/%s.json", dirname, remote->id); if (l2_launcher_mkdir_parents(dirname) < 0) { CMD_ERROR("Could not create directory for %s: %s", remote->id, strerror(errno)); return false; } CURL *cu = curl_easy_init(); l2_sha1_state_t st; l2_sha1_digest_t dig; json_error_t err; if (!cu) return false; void *data = NULL; char errbuf[CURL_ERROR_SIZE]; size_t sz; memset(errbuf, 0, CURL_ERROR_SIZE * sizeof(char)); curl_easy_setopt(cu, CURLOPT_ERRORBUFFER, errbuf); CURLcode code = l2_launcher_download(cu, remote->url, &data, &sz); if (code != CURLE_OK) { CMD_WARN("Error downloading version %s: %s: %s", remote->id, curl_easy_strerror(code), errbuf); goto cleanup; } l2_sha1_init(&st); l2_sha1_update(&st, data, sz); l2_sha1_finalize(&st, &dig); if (l2_sha1_digest_compare(&dig, &remote->sha1)) { char d1[L2_SHA1_DIGESTLEN + 1]; char d2[L2_SHA1_DIGESTLEN + 1]; l2_sha1_digest_to_hex(&dig, d1); l2_sha1_digest_to_hex(&remote->sha1, d2); CMD_WARN("Downloaded file for %s has incorrect hash: expected %s, got %s!", remote->id, d2, d1); goto cleanup; } js = json_loadb(data, sz, JSON_REJECT_DUPLICATES, &err); if (!js) { CMD_WARN("JSON parse error reading version %s: %s", remote->id, err.text); goto cleanup; } save = fopen(fname, "w"); if (!save) { CMD_WARN("Error opening version file %s", fname); goto cleanup; } if (fwrite(data, sz, 1, save) < 1) { CMD_WARN("Error saving version info to %s", fname); goto cleanup; } *ojson = js; curl_easy_cleanup(cu); fclose(save); free(data); return true; cleanup: curl_easy_cleanup(cu); free(data); if (save) fclose(save); if (js) json_decref(js); return false; } char *l2_version__read_etag(time_t *lastmod) { size_t dpathlen = strlen(l2_state.paths.data); char *etagpath; L2_ASTRCAT2(etagpath, l2_state.paths.data, dpathlen, L2_VERSION_MANIFEST_ETAG_PATH, L2_CSTRLEN(L2_VERSION_MANIFEST_ETAG_PATH)); FILE *etagfp = fopen(etagpath, "rb"); char *etag = NULL; uint32_t buf; uint64_t ts; CMD_DEBUG0("Reading etag file"); if (!etagfp) { CMD_DEBUG0("Failed to open etag file"); return NULL; } if (fread(&buf, sizeof(uint32_t), 1, etagfp) < 1) goto cleanup; if (fread(&ts, sizeof(uint64_t), 1, etagfp) < 1) goto cleanup; buf = l2_betoh32(buf); ts = l2_betoh64(ts); etag = calloc(buf + 1, 1); if (!etag) goto cleanup; if (fread(etag, 1, buf, etagfp) < buf) goto cleanup; etag[buf] = '\0'; *lastmod = (time_t)ts; fclose(etagfp); CMD_DEBUG("Etag read: %s", etag); return etag; cleanup: free(etag); fclose(etagfp); return NULL; } /* this function does not report errors on failure because its operation is not essential */ void l2_version__write_etag(time_t modtime, const char *etag) { size_t dpathlen = strlen(l2_state.paths.data); char *etagpath; L2_ASTRCAT2(etagpath, l2_state.paths.data, dpathlen, L2_VERSION_MANIFEST_ETAG_PATH, L2_CSTRLEN(L2_VERSION_MANIFEST_ETAG_PATH)); size_t etaglen = strlen(etag); uint32_t etaglenbe = l2_htobe32((uint32_t)etaglen); uint64_t mt_write = l2_htobe64((uint64_t)modtime); FILE *etagfp = fopen(etagpath, "wb"); if (!etagfp) return; if (fwrite(&etaglenbe, sizeof(uint32_t), 1, etagfp) < 1) goto cleanup; if (fwrite(&mt_write, sizeof(uint64_t), 1, etagfp) < 1) goto cleanup; if (fwrite(etag, 1, etaglen, etagfp) < etaglen) goto cleanup; cleanup: fclose(etagfp); } void l2_version__delete_etag(void) { size_t dpathlen = strlen(l2_state.paths.data); char *etagpath; L2_ASTRCAT2(etagpath, l2_state.paths.data, dpathlen, L2_VERSION_MANIFEST_ETAG_PATH, L2_CSTRLEN(L2_VERSION_MANIFEST_ETAG_PATH)); if (unlink(etagpath) < 0) { CMD_WARN("Failed to delete etag: %s", strerror(errno)); } } int l2_version__read_manifest_file(json_t **manifest) { size_t dpathlen = strlen(l2_state.paths.data); char *manpath; L2_ASTRCAT2(manpath, l2_state.paths.data, dpathlen, L2_VERSION_MANIFEST_PATH, L2_CSTRLEN(L2_VERSION_MANIFEST_PATH)); json_error_t err; json_t *man = json_load_file(manpath, JSON_REJECT_DUPLICATES, &err); if (!man) { CMD_WARN("Error loading version manifest file: %s", err.text); return -1; } *manifest = man; return 0; } int l2_version__write_manifest_file(void *data, size_t d) { size_t dpathlen = strlen(l2_state.paths.data); char *manpath; L2_ASTRCAT2(manpath, l2_state.paths.data, dpathlen, L2_VERSION_MANIFEST_PATH, L2_CSTRLEN(L2_VERSION_MANIFEST_PATH)); FILE* fp = fopen(manpath, "w"); if (!fp) return -2; if (fwrite(data, 1, d, fp) < d) { fclose(fp); return -1; } fclose(fp); return 0; } unsigned l2_version__load_manifest(json_t **omanifest) { time_t lmod; char *etag = l2_version__read_etag(&lmod); time_t now = time(NULL); CMD_DEBUG("etag: %s, time: %jd", etag ? etag : "(null)", etag ? (uintmax_t)(now - lmod) : UINTMAX_C(0)); if (etag && now - lmod < L2_VERSION_MANIFEST_EXP_TIME) { CMD_DEBUG0("Not downloading (cached)."); if (l2_version__read_manifest_file(omanifest) == 0) { free(etag); return VERSION_SUCCESS; /* if it failed, redownload the file */ } } CMD_DEBUG0("Downloading"); CURL *cu = curl_easy_init(); struct curl_slist *headers = NULL; CURLcode cres; long httpres; unsigned retval = VERSION_EDOWNLOAD; char ebuf[CURL_ERROR_SIZE]; /* assuming CURL_ERROR_SIZE is small enough to fit on the stack... */ memset(ebuf, 0, sizeof(ebuf)); void *data = NULL; size_t dlen; if (!cu) { return VERSION_EDOWNLOAD; } if (etag) { char *headerstr; size_t etaglen = strlen(etag); #define H_INM "If-None-Match: " L2_ASTRCAT2(headerstr, H_INM, L2_CSTRLEN(H_INM), etag, etaglen); #undef H_INM headers = curl_slist_append(NULL, headerstr); if (!headers) { retval = VERSION_EDOWNLOAD; goto cleanup; } CMD_DEBUG("Adding etag header %s", etag); curl_easy_setopt(cu, CURLOPT_HTTPHEADER, headers); } curl_easy_setopt(cu, CURLOPT_ERRORBUFFER, ebuf); if ((cres = l2_launcher_download(cu, L2_URL_META_VERSION_MANIFEST, &data, &dlen)) != CURLE_OK) { CMD_ERROR("Error downloading version manifest: %s: %s", curl_easy_strerror(cres), ebuf); goto cleanup; } curl_easy_getinfo(cu, CURLINFO_RESPONSE_CODE, &httpres); if (httpres == 200) { CMD_DEBUG0("manifest download OK"); /* yay we're good and *data contains our info */ if (l2_version__write_manifest_file(data, dlen) < 0) { CMD_WARN0("Error writing version manifest"); } else { struct curl_header *outh; if (curl_easy_header(cu, "etag", 0, CURLH_HEADER, 0, &outh) == CURLHE_OK) { l2_version__write_etag(time(NULL), outh->value); CMD_DEBUG("New etag returned: %s", outh->value); } } json_error_t err; json_t *js = json_loadb(data, dlen, JSON_REJECT_DUPLICATES, &err); if (!js) { CMD_ERROR("Failed to parse version manifest: %s", err.text); retval = VERSION_EJSON; goto cleanup; } *omanifest = js; } else if (httpres == 304) { /* not modified */ CMD_DEBUG0("manifest download not modified"); if (l2_version__read_manifest_file(omanifest) < 0) { CMD_WARN0("Could not read cached version manifest. Please try again."); l2_version__delete_etag(); retval = VERSION_EJSON; goto cleanup; } /* write the new time because we know the file is good (at least valid json) */ CMD_DEBUG0("writing new etag (read success)"); l2_version__write_etag(time(NULL), etag); /* same etag but we know the file is still good */ } else { CMD_ERROR("Received unexpected HTTP status code: %ld", httpres); goto cleanup; } free(data); free(etag); data = NULL; if (headers) curl_slist_free_all(headers); curl_easy_cleanup(cu); return VERSION_SUCCESS; cleanup: if (headers) curl_slist_free_all(headers); if (cu) curl_easy_cleanup(cu); if (data) free(data); if (etag) free(etag); return retval; } /* parses an instance of version_manifest_v2.json */ unsigned l2_version__load_all_from_json(json_t *json) { if (!json_is_object(json)) { return VERSION_EFORMAT; } json_t *latest = json_object_get(json, "latest"); json_t *versions = json_object_get(json, "versions"); if (!json_is_object(latest) || !json_is_array(versions)) { return VERSION_EFORMAT; } const char *latestrel = json_string_value(json_object_get(latest, "release")); const char *latestsnap = json_string_value(json_object_get(latest, "snapshot")); size_t index; json_t *value; unsigned res; if (!latestrel || !latestsnap) { return VERSION_EFORMAT; } json_array_foreach(versions, index, value) { if ((res = l2_version__add_remote(value)) != VERSION_SUCCESS) return res; } return VERSION_SUCCESS; } unsigned l2_version__add_remote(json_t *js) { struct l2_version_remote *ver = NULL; struct tm ts; json_t *val; unsigned res = VERSION_SUCCESS; if (!json_is_object(js)) { return VERSION_EFORMAT; } ver = calloc(1, sizeof(struct l2_version_remote)); if (!ver) return VERSION_EALLOC; res = VERSION_EFORMAT; val = json_object_get(js, "id"); if (!json_is_string(val)) goto cleanup; ver->id = strdup(json_string_value(val)); val = json_object_get(js, "type"); if (!json_is_string(val)) goto cleanup; ver->type = strdup(json_string_value(val)); val = json_object_get(js, "url"); if (!json_is_string(val)) goto cleanup; ver->url = strdup(json_string_value(val)); val = json_object_get(js, "sha1"); if (!json_is_string(val)) goto cleanup; if (l2_sha1_digest_from_hex(&ver->sha1, json_string_value(val)) < 0) goto cleanup; val = json_object_get(js, "complianceLevel"); if (!json_is_number(val)) goto cleanup; ver->compliance_level = json_integer_value(val); val = json_object_get(js, "time"); if (!json_is_string(val)) goto cleanup; memset(&ts, 0, sizeof(struct tm)); if (!l2_launcher_parse_iso_time(json_string_value(val), &ts)) goto cleanup; ver->update_time = mktime(&ts); val = json_object_get(js, "releaseTime"); if (!json_is_string(val)) goto cleanup; memset(&ts, 0, sizeof(struct tm)); if (!l2_launcher_parse_iso_time(json_string_value(val), &ts)) goto cleanup; ver->release_time = mktime(&ts); /* add the thing to the global list */ if (l2_state.ver_remote_tail) { l2_state.ver_remote_tail->next = ver; ver->prev = l2_state.ver_remote_tail; l2_state.ver_remote_tail = ver; } else { l2_state.ver_remote_head = l2_state.ver_remote_tail = ver; } return VERSION_SUCCESS; cleanup: if (ver) { free(ver->id); free(ver->type); free(ver->url); } free(ver); return res; } int l2_version_check_integrity(FILE *fp, l2_sha1_digest_t *digest, size_t sz) { #define VER_READBUF_SZ (1024) size_t len = 0, nread; uint8_t buf[VER_READBUF_SZ]; l2_sha1_digest_t rdigest; l2_sha1_state_t st; l2_sha1_init(&st); while ((nread = fread(buf, 1, VER_READBUF_SZ, fp)) > 0) { len += nread; l2_sha1_update(&st, buf, nread); } if (ferror(fp)) return -1; l2_sha1_finalize(&st, &rdigest); if (sz > 0 && sz != len) return 0; if (digest) { return !l2_sha1_digest_compare(&rdigest, digest) ? 1 : 0; } else { return 1; } } unsigned l2_version__collect_libraries(json_t *jlibs, l2_subst_t *subst, struct l2_version_library **libs, l2_version_feature_match_proc_t *feature_matcher); void l2_version__free_libraries(struct l2_version_library *libs); int l2_version__get_library_artifact_path(json_t *lib, l2_subst_t *subst, const char *classifier, char **path); unsigned l2_version_download_libraries(json_t *jlibraries, l2_version_feature_match_proc_t *feature_matcher) { struct l2_version_library *plib = NULL; l2_subst_t *sp; l2_subst_init(&sp); l2_subst_add(sp, "arch", L2SU_LAUNCHER_ARCH_BITS); unsigned u = l2_version__collect_libraries(jlibraries, sp, &plib, feature_matcher); char hexdig[L2_SHA1_HEX_STRLEN + 1]; for (struct l2_version_library *lib = plib; lib; lib = lib->next) { if (lib->has_digest) { l2_sha1_digest_to_hex(&lib->digest, hexdig); } CMD_DEBUG("Library: %s url: %s dig: %s", lib->artifactpath, lib->url, lib->has_digest ? hexdig : "(none)"); if (lib->extract.extracttype != EXTRACT_NONE) { CMD_DEBUG("Extract: %s", lib->extract.extracttype == EXTRACT_NEW ? "new" : "old"); } } l2_version__free_libraries(plib); l2_subst_free(sp); return u; } unsigned l2_version__library_get_classifier(json_t *lib, const char **class) { json_t *natives = json_object_get(lib, "natives"); if (json_is_object(natives)) { json_t *mynative = json_object_get(natives, L2SU_LAUNCHER_OS); if (json_is_string(mynative)) { *class = json_string_value(mynative); } else if (mynative) { return VERSION_EJSON; } else { *class = NULL; } } else if (natives) { return VERSION_EJSON; } else { *class = NULL; } return VERSION_SUCCESS; } unsigned l2_version__gen_one_library(json_t *lib, l2_subst_t *subst, struct l2_version_library **olib) { const char *classifier; unsigned res; int has_class; if ((res = l2_version__library_get_classifier(lib, &classifier)) != VERSION_SUCCESS) { return res; } struct l2_version_library *retlib = calloc(1, sizeof(struct l2_version_library)); if (!retlib) return VERSION_EALLOC; if ((has_class = l2_version__get_library_artifact_path(lib, subst, classifier, &retlib->artifactpath)) < 0) { res = VERSION_EJSON; goto cleanup; } if (classifier) { retlib->extract.extracttype = EXTRACT_OLD; } else if (has_class) { retlib->extract.extracttype = EXTRACT_NEW; } else { retlib->extract.extracttype = EXTRACT_NONE; } /* first, find the URL of the library */ json_t *jurl = json_object_get(lib, "url"); if (json_is_string(jurl)) { retlib->url = l2_launcher_sprintf_alloc("%s%s", json_string_value(jurl), retlib->artifactpath); retlib->has_digest = false; if (!retlib->url) { res = VERSION_EALLOC; goto cleanup; } } else if (jurl) { res = VERSION_EJSON; goto cleanup; } json_t *jdownloads = json_object_get(lib, "downloads"); if (json_is_object(jdownloads)) { json_t *dljson = NULL; if (classifier) { if (json_unpack(jdownloads, "{s:{s:o}}", "classifiers", classifier, &dljson) < 0 || !dljson) { res = VERSION_EJSON; goto cleanup; } } else { dljson = json_object_get(jdownloads, "artifact"); } if (!json_is_object(dljson)) { res = VERSION_EJSON; goto cleanup; } const char *courl, *cosha1; json_int_t asize; if (json_unpack(dljson, "{s:s, s:s, s:I}", "url", &courl, "sha1", &cosha1, "size", &asize) < 0) { res = VERSION_EJSON; goto cleanup; } retlib->url = strdup(courl); if (!retlib->url) { res = VERSION_EJSON; goto cleanup; } retlib->size = (size_t)asize; retlib->has_digest = true; if (l2_sha1_digest_from_hex(&retlib->digest, cosha1) < 0) { res = VERSION_EJSON; goto cleanup; } } else if (jdownloads) { res = VERSION_EJSON; goto cleanup; } else { /* guess from name */ retlib->url = l2_launcher_sprintf_alloc("https://libraries.minecraft.net/%s", retlib->artifactpath); retlib->has_digest = false; if (!retlib->url) { res = VERSION_EALLOC; goto cleanup; } res = VERSION_EJSON; goto cleanup; } json_t *jextract = json_object_get(lib, "extract"); if (json_is_object(jextract)) { json_t *jexclude = json_object_get(jextract, "exclude"); if (json_is_array(jexclude)) { size_t idx; json_t *val; retlib->extract.exclude = calloc(json_array_size(jexclude) + 1, sizeof(char *)); if (!retlib->extract.exclude) { res = VERSION_EALLOC; goto cleanup; } json_array_foreach(jexclude, idx, val) { if (!json_is_string(val)) { res = VERSION_EJSON; goto cleanup; } retlib->extract.exclude[idx] = strdup(json_string_value(val)); if (!retlib->extract.exclude[idx]) { res = VERSION_EALLOC; goto cleanup; } } } else if (jexclude) { res = VERSION_EJSON; goto cleanup; } } else if (jextract) { res = VERSION_EJSON; goto cleanup; } *olib = retlib; return VERSION_SUCCESS; cleanup: l2_version__free_libraries(retlib); return res; } unsigned l2_version__collect_libraries(json_t *jlibs, l2_subst_t *subst, struct l2_version_library **libs, l2_version_feature_match_proc_t *feature_matcher) { struct l2_version_library *libraries = NULL, *tail = NULL; struct l2_version_library *curlib = NULL; unsigned ret = VERSION_EJSON; enum l2_version_check_result applies; size_t idx; json_t *val; if (!json_is_array(jlibs)) return VERSION_EJSON; json_array_foreach(jlibs, idx, val) { if (!json_is_object(val)) { ret = VERSION_EJSON; goto cleanup; } json_t *rules = json_object_get(val, "rules"); if (rules) { applies = l2_version_check_rules(rules, feature_matcher); if (applies == RULE_CHECK_ERROR) { ret = VERSION_EJSON; goto cleanup; } else if (applies != RULE_CHECK_ALLOW) { continue; } } /* this library applies, now generate the download for it */ const char *name = json_string_value(json_object_get(val, "name")); CMD_DEBUG("Library: %s", name); curlib = NULL; ret = l2_version__gen_one_library(val, subst, &curlib); if (ret == VERSION_EJSON && !curlib) { CMD_WARN("Library %p was not decoded properly. This is likely a launcher deficiency.", (void *)curlib); continue; } else if (ret != VERSION_SUCCESS) { goto cleanup; } if (tail) { tail->next = curlib; tail = curlib; } else { libraries = tail = curlib; } } *libs = libraries; return VERSION_SUCCESS; cleanup: /* free libraries */ l2_version__free_libraries(libraries); return ret; } void l2_version__free_libraries(struct l2_version_library *libs) { if (!libs) return; char **excludecur; for (struct l2_version_library *next = libs->next; libs; libs = next) { next = libs->next; free(libs->artifactpath); free(libs->url); excludecur = libs->extract.exclude; if (excludecur) { for (; *excludecur; ++excludecur) { free(*excludecur); } free(libs->extract.exclude); } free(libs); } } int l2_version__get_library_artifact_path(json_t *lib, l2_subst_t *subst, const char *classifier, char **path) { json_t *jname = json_object_get(lib, "name"); if (!json_is_string(jname)) return -1; const char *name = json_string_value(jname); const char *group_id_end = NULL; const char *artifact_id = NULL; int artifact_id_len = 0; const char *version = NULL; int version_len = 0; const char *tclass = NULL; char *basepath; size_t basepathlen; for (const char *cur = name; *cur; ++cur) { if (*cur == ':') { if (!group_id_end) { group_id_end = cur; artifact_id = cur + 1; } else if (!version) { version = cur + 1; artifact_id_len = (int)(cur - artifact_id); } else if (!tclass) { tclass = cur + 1; version_len = (int)(cur - version); } else { return -1; } } } if (!group_id_end || !artifact_id || !version) return -1; basepathlen = group_id_end - name; basepath = alloca(basepathlen + 1); memcpy(basepath, name, basepathlen); basepath[basepathlen] = '\0'; for (char *cur = basepath; *cur; ++cur) { if (*cur == '.') *cur = '/'; } size_t bufsz; char *buf; int has_class = 0; if (tclass || classifier) { L2_ASPRINTF(buf, bufsz, "%s/%*.*s-%*.*s-%s.jar", basepath, artifact_id_len, artifact_id_len, artifact_id, version_len, version_len, version, tclass ? tclass : classifier); has_class = 1; } else { L2_ASPRINTF(buf, bufsz, "%s/%*.*s-%s.jar", basepath, artifact_id_len, artifact_id_len, artifact_id, version); } return l2_subst_apply(subst, buf, path) < 0 ? -1 : has_class; } int l2_version__whole_string_matches(const char *pattern, const char *subject) { PCRE2_SIZE erroffset; int errcode; pcre2_code *cd = pcre2_compile((PCRE2_SPTR8)pattern, PCRE2_ZERO_TERMINATED, PCRE2_ANCHORED | PCRE2_ENDANCHORED, &errcode, &erroffset, NULL); pcre2_match_data *md; if (!cd) { return -1; } md = pcre2_match_data_create(1, NULL); errcode = pcre2_match(cd, (PCRE2_SPTR8)subject, PCRE2_ZERO_TERMINATED, 0, 0, md, NULL); /* we care about neither the code nor the match data */ pcre2_code_free(cd); pcre2_match_data_free(md); if (errcode == PCRE2_ERROR_NOMATCH) return 0; return errcode >= 0 ? 1 : -1; } enum l2_version_check_result l2_version__check_one_rule(json_t *rule, l2_version_feature_match_proc_t *feature_matcher) { if (!json_is_object(rule)) return RULE_CHECK_ERROR; enum l2_version_check_result myaction; json_t *action = json_object_get(rule, "action"); if (!json_is_string(action)) return RULE_CHECK_ERROR; const char *actionstr = json_string_value(action); if (!strcmp(actionstr, "allow")) { myaction = RULE_CHECK_ALLOW; } else if (!strcmp(actionstr, "disallow")) { myaction = RULE_CHECK_DISALLOW; } else { return RULE_CHECK_ERROR; } json_t *val = json_object_get(rule, "os"); if (json_is_object(val)) { json_t *checkval = json_object_get(val, "name"); if (json_is_string(checkval)) { if (strcmp(json_string_value(checkval), L2SU_LAUNCHER_OS)) { /* wrong OS, rule doesn't apply */ return RULE_CHECK_DEFAULT; } } else if (checkval) { return RULE_CHECK_ERROR; } checkval = json_object_get(val, "arch"); if (json_is_string(checkval)) { int res = l2_version__whole_string_matches(json_string_value(checkval), L2SU_LAUNCHER_ARCH); if (res < 0) return RULE_CHECK_ERROR; if (!res) return RULE_CHECK_DEFAULT; } else if (checkval) { return RULE_CHECK_ERROR; } /* FIXME: don't check OS version because idk how to get that portably yet */ return myaction; } else if (val) { return RULE_CHECK_ERROR; } val = json_object_get(rule, "features"); if (json_is_object(val)) { json_t *feat; const char *fname; json_object_foreach(val, fname, feat) { if (!(*feature_matcher)(fname, feat)) { return RULE_CHECK_DEFAULT; } } } else if (val) { return RULE_CHECK_ERROR; } return myaction; } enum l2_version_check_result l2_version_check_rules(json_t *rules, l2_version_feature_match_proc_t *feature_matcher) { size_t idx; json_t *val; enum l2_version_check_result res = RULE_CHECK_DEFAULT; if (!json_is_array(rules)) { return RULE_CHECK_ERROR; } json_array_foreach(rules, idx, val) { /* first, check if the rule actually matches us */ switch (l2_version__check_one_rule(val, feature_matcher)) { case RULE_CHECK_ERROR: return RULE_CHECK_ERROR; case RULE_CHECK_ALLOW: res = RULE_CHECK_ALLOW; break; case RULE_CHECK_DISALLOW: res = RULE_CHECK_DISALLOW; break; default: break; } } return res; }