#include "digest/digest.h" #include "macros.h" #include "l2su.h" #include #include #include #include #include #include #include #include #include #include #include #include #include /* handcoded string functions * * NOTE: I am aware that this is inefficient but since these are used only a handful of times * during initialization, I don't think performance is really a huge deal here. * * remember that this is a Minecraft launcher, so the PC has to meet the minimum specs of that, * which are well above the minimum specs of this launcher */ char *l2_launcher_strapp(char *buf, const char *src) { size_t buflen = strlen(buf); size_t srclen = strlen(src); char *ret = realloc(buf, buflen + srclen + 1); if (!ret) return NULL; memcpy(ret + buflen, src, srclen); ret[buflen + srclen] = '\0'; /* realloc does not initialize like calloc does */ return ret; } bool l2_launcher_strpre(const char *pre, const char *text) { while (*pre) { if (*(pre++) != *(text++)) return false; } return true; } char *l2_launcher_sprintf_alloc(const char *fmt, ...) { va_list pva; va_start(pva, fmt); size_t len = vsnprintf(NULL, 0, fmt, pva); va_end(pva); char *ret = calloc(len + 1, sizeof(char)); if (!ret) { return ret; } va_start(pva, fmt); vsnprintf(ret, len + 1, fmt, pva); va_end(pva); return ret; } char *l2_launcher_sprintf_resize(char **buf, size_t *len, const char *fmt, ...) { va_list pva; va_start(pva, fmt); size_t needlen = vsnprintf(NULL, 0, fmt, pva); va_end(pva); if (needlen + 1 > *len) { char *temp = realloc(*buf, needlen + 1); if (!temp) return NULL; *buf = temp; *len = needlen + 1; } va_start(pva, fmt); vsnprintf(*buf, *len, fmt, pva); va_end(pva); return *buf; } char *l2_launcher_find_config_path(void) { /* check for $L2SU_CONFIG */ char *config = getenv(PROJECT_NAME_UPPER "_CONFIG"); if (config) { return strdup(config); } /* check for $XDG_CONFIG_HOME */ config = getenv("XDG_CONFIG_HOME"); if (config) { /* want to append '"/" PROJECT_NAME' to our string */ char *ret = strdup(config); if (!ret) return NULL; return l2_launcher_strapp(ret, "/" PROJECT_NAME); } /* check for $HOME/.config */ config = getenv("HOME"); if (config) { char *ret = strdup(config); if (!ret) return NULL; return l2_launcher_strapp(ret, "/.config/" PROJECT_NAME); } /* fail (do NOT attempt to find home directory from passwd */ return NULL; } char *l2_launcher_find_data_path(void) { /* check for $L2SU_DATA */ char *config = getenv(PROJECT_NAME_UPPER "_DATA"); if (config) { return strdup(config); } /* check for $XDG_DATA_HOME */ config = getenv("XDG_DATA_HOME"); if (config) { char *ret = strdup(config); if (!ret) return NULL; return l2_launcher_strapp(ret, "/" PROJECT_NAME); } /* check for $HOME/.local/share */ config = getenv("HOME"); if (config) { char *ret = strdup(config); if (!ret) return NULL; return l2_launcher_strapp(ret, "/.local/share/" PROJECT_NAME); } return NULL; } int l2_launcher_open_config(const char *path, int flags, mode_t mode) { int conffd = open(l2_state.paths.config, O_RDONLY | O_DIRECTORY); if (conffd < 0) return INSTANCE_ERRNO; int instfd = openat(conffd, path, flags, mode); int en = errno; /* back up errno because close can fail */ close(conffd); errno = en; return instfd; } int l2_launcher__rmtree_ftw_delete_with_reckless_abandon(const char *fpath, const struct stat *st, int typeflag, struct FTW *ftwbuf) { L2_UNUSED(ftwbuf); L2_UNUSED(st); switch (typeflag) { case FTW_F: case FTW_SL: if (unlink(fpath) < 0) { CMD_WARN("l2_rm_tree: Failed to delete file or symlink %s: %s", fpath, strerror(errno)); return 1; } break; case FTW_DP: if (rmdir(fpath) < 0) { CMD_WARN("l2_rm_tree: Failed to delete directory %s: %s", fpath, strerror(errno)); return 1; } break; case FTW_DNR: CMD_WARN("l2_rm_tree: Not traversing into directory I cannot read: %s", fpath); break; case FTW_NS: CMD_WARN("l2_rm_tree: Not deleting/traversing file I cannot stat: %s", fpath); break; } return 0; } int l2_launcher_rm_tree(const char *path, int nfds) { if (nfds <= 0) nfds = 64; int res = nftw(path, &l2_launcher__rmtree_ftw_delete_with_reckless_abandon, nfds, FTW_DEPTH | FTW_PHYS); if (res != 0) return -1; return 0; } int l2_launcher_mkdir_parents(const char *path) { return l2_launcher_mkdir_parents_ex(path, 0); } /* NOTE: There's no portable (or otherwise - see open(2) BUGS) way to do this without race conditions. */ int l2_launcher_mkdir_parents_ex(const char *path, unsigned ignore) { if (*path != '/') return -1; char *pathbuf; size_t pathlen; L2_ASTRDUP(pathbuf, pathlen, path); char *pcurelem = pathbuf; for (char *cur = pathbuf + pathlen; ignore && cur != pathbuf; --cur) { if (*cur == '/') { --ignore; *cur = '\0'; } } struct stat stbuf = { 0 }; do { /* strtok is off-limits because it smells bad */ *pcurelem = '/'; pcurelem = strchr(pcurelem + 1, '/'); if (pcurelem) { *pcurelem = '\0'; } /* now pathbuf contains our truncated path name which may or may not exist */ if (mkdir(pathbuf, 0755) < 0) { if (errno == EEXIST) { /* racy: stat the file and continue if it is a directory */ if (stat(pathbuf, &stbuf) < 0) { return -1; } if (!S_ISDIR(stbuf.st_mode)) { return -1; } } else { return -1; } } } while (pcurelem); return 0; } char *l2_launcher_parse_iso_time(const char *str, struct tm *ts) { return strptime(str, "%FT%T%z", ts); /* TODO: replace with something portable */ } void l2_launcher_download_init(struct l2_dlbuf *buf) { buf->data = NULL; buf->size = 0; buf->capacity = 0; } size_t l2_launcher__download_callback(char *data, size_t size, size_t nmemb, void *user) { struct l2_dlbuf *buf = user; size_t realsz = size * nmemb; if (buf->size + realsz > buf->capacity) { size_t newcap = 16; while (newcap && buf->size + realsz > newcap) newcap <<= 1; if (!newcap) return CURLE_WRITE_ERROR; void *newbuf = realloc(buf->data, newcap); if (!newbuf) return CURLE_WRITE_ERROR; buf->data = newbuf; buf->capacity = newcap; } memcpy((char *)buf->data + buf->size, data, realsz); buf->size += realsz; return realsz; } void *l2_launcher_download_finalize(struct l2_dlbuf *buf, size_t *psz) { void *smaller = realloc(buf->data, buf->size); if (smaller) buf->data = smaller; void *data = buf->data; buf->data = NULL; *psz = buf->size; return data; } void l2_launcher_download_cleanup(struct l2_dlbuf *buf) { free(buf->data); } const curl_write_callback l2_dlcb = &l2_launcher__download_callback; CURLcode l2_launcher_download(CURL *cd, const char *url, void **odata, size_t *osize) { struct l2_dlbuf db; CURLcode code; l2_launcher_download_init(&db); curl_easy_setopt(cd, CURLOPT_URL, url); curl_easy_setopt(cd, CURLOPT_WRITEDATA, &db); curl_easy_setopt(cd, CURLOPT_WRITEFUNCTION, l2_dlcb); curl_easy_setopt(cd, CURLOPT_USERAGENT, L2_USER_AGENT); if ((code = curl_easy_perform(cd)) != CURLE_OK) { l2_launcher_download_cleanup(&db); return code; } *odata = l2_launcher_download_finalize(&db, osize); l2_launcher_download_cleanup(&db); /* not strictly necessary but ok */ return CURLE_OK; } int l2_json_merge_objects(json_t *j1, json_t *j2) { const char *key; size_t keylen; json_t *val; json_t *myval; json_object_keylen_foreach(j2, key, keylen, val) { myval = json_object_getn(j1, key, keylen); if (json_is_object(myval) && json_is_object(val)) { if (l2_json_merge_objects(myval, val) < 0) return -1; } else if (json_is_array(myval) && json_is_array(val)) { if (json_array_extend(myval, val) < 0) return -1; } else if (!myval) { if (json_object_setn_nocheck(j1, key, keylen, val) < 0) return -1; } } return 0; } int l2_launcher_check_integrity(FILE *fp, const 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; if (!digest && sz == 0) return 1; 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; } } int l2_launcher_should_download(const char *path, const l2_sha1_digest_t *expectdigest, size_t expectsize) { FILE *lfile = fopen(path, "rb"); if (!lfile) { if (errno == ENOENT) { return 1; } else { CMD_DEBUG("Failed to open %s for reading: %s", path, strerror(errno)); return -1; } } int res = l2_launcher_check_integrity(lfile, expectdigest, expectsize); fclose(lfile); switch (res) { case 0: CMD_DEBUG("SHOULD redownload %s, the SHA1 or size doesn't match.", path); return 1; case 1: CMD_DEBUG("SHOULDN'T redownload %s.", path); return 0; default: return res; } } struct l2_launcher__download_data { l2_sha1_state_t digest_state; size_t recv_size; FILE *fp; }; size_t l2_launcher__download_writecb(char *ptr, size_t size, size_t nmemb, void *user) { struct l2_launcher__download_data *data = user; size_t realsz = size * nmemb; /* size should be 1 but whatever */ if (fwrite(ptr, size, nmemb, data->fp) < nmemb) { return CURL_WRITEFUNC_ERROR; } l2_sha1_update(&data->digest_state, ptr, realsz); data->recv_size += realsz; return realsz; } int l2_launcher_download_checksummed(const char *url, const char *pathstr, l2_sha1_digest_t *expect_digest, size_t expect_size) { int res = -1; CURL *pc = NULL; /* check if we even need to redownload the thing */ int rdres = l2_launcher_should_download(pathstr, expect_digest, expect_size); if (rdres < 0) { return -1; } else if (!rdres) { CMD_DEBUG("Not downloading %s", pathstr); return 0; } if (!url) { CMD_WARN("Cannot redownload %s, even though I need to! (no URL specified)", pathstr); return -1; } /* redownload the file */ struct l2_launcher__download_data dldata; char errbuf[CURL_ERROR_SIZE]; memset(&dldata, 0, sizeof(struct l2_launcher__download_data)); memset(&errbuf, 0, sizeof(errbuf)); l2_sha1_init(&dldata.digest_state); if (l2_launcher_mkdir_parents_ex(pathstr, 1) < 0) { goto cleanup; } dldata.fp = fopen(pathstr, "wb"); if (!dldata.fp) { CMD_WARN("Failed to open %s for writing: %s", pathstr, strerror(errno)); goto cleanup; } pc = curl_easy_init(); if (!pc) { goto cleanup; } curl_easy_setopt(pc, CURLOPT_USERAGENT, L2_USER_AGENT); curl_easy_setopt(pc, CURLOPT_URL, url); curl_easy_setopt(pc, CURLOPT_WRITEDATA, &dldata); curl_easy_setopt(pc, CURLOPT_WRITEFUNCTION, &l2_launcher__download_writecb); curl_easy_setopt(pc, CURLOPT_ERRORBUFFER, errbuf); CURLcode cres = curl_easy_perform(pc); if (cres != CURLE_OK) { CMD_WARN("Failed to download %s: %s: %s", pathstr, curl_easy_strerror(cres), errbuf); goto cleanup; } fclose(dldata.fp); dldata.fp = NULL; curl_easy_cleanup(pc); pc = NULL; l2_sha1_digest_t recvdigest; l2_sha1_finalize(&dldata.digest_state, &recvdigest); if (expect_digest && l2_sha1_digest_compare(&recvdigest, expect_digest)) { char expstr[L2_SHA1_HEX_STRLEN + 1]; char gotstr[L2_SHA1_HEX_STRLEN + 1]; l2_sha1_digest_to_hex(expect_digest, expstr); l2_sha1_digest_to_hex(&recvdigest, gotstr); CMD_WARN("Downloaded %s has wrong digest! (expected: %s, got: %s)", pathstr, expstr, gotstr); if (unlink(pathstr) < 0) { CMD_WARN("Failed to delete %s: %s", pathstr, strerror(errno)); } goto cleanup; } if (expect_size > 0 && expect_size != dldata.recv_size) { CMD_WARN("Downloaded %s has wrong size! (expected: %zu bytes, got: %zu bytes)", pathstr, expect_size, dldata.recv_size); if (unlink(pathstr) < 0) { CMD_WARN("Failed to delete %s: %s", pathstr, strerror(errno)); } goto cleanup; } CMD_INFO("Downloaded %s successfully.", pathstr); curl_easy_cleanup(pc); return 1; cleanup: if (dldata.fp) fclose(dldata.fp); if (pc) curl_easy_cleanup(pc); return res; } struct l2_ftw__data_int { struct l2_ftw_data user; size_t pathcap; size_t pathlen; int abort; }; int l2_launcher__ftw_update_path(char **path, struct l2_ftw__data_int *ftw, const char *entname) { size_t entlen = strlen(entname); if (ftw->pathlen + entlen + 2 > ftw->pathcap) { char *newpathbuf = realloc(*path, ftw->pathlen + entlen + 2); if (!newpathbuf) return -1; *path = newpathbuf; ftw->pathcap = ftw->pathlen + entlen + 2; } (*path)[ftw->pathlen] = '/'; ++ftw->pathlen; memcpy(*path + ftw->pathlen, entname, entlen + 1); ftw->pathlen += entlen; return 0; } int l2_launcher__ftw_internal(char **path, int depth, l2_ftw_proc_t *proc, struct l2_ftw__data_int *ftw) { if (depth <= 0) { CMD_DEBUG0("myftw: interation depth exceeded"); return -1; } DIR *thisdir = opendir(*path); int res; if (!thisdir) { if (errno != ENOENT || ftw->user.depth == 0) { CMD_DEBUG("myftw: could not open %s: %s", *path, strerror(errno)); return -1; } CMD_DEBUG("myftw: directory %s does not exist. (maybe it was deleted?)", *path); return 0; } ++ftw->user.depth; struct dirent *ent; struct stat st; size_t pathlen = ftw->pathlen; errno = 0; while ((ent = readdir(thisdir))) { if (!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) continue; int flags = 0; if (fstatat(dirfd(thisdir), ent->d_name, &st, AT_SYMLINK_NOFOLLOW) < 0) { CMD_DEBUG("myftw: stat failed %s: %s", *path, strerror(errno)); res = -1; goto cleanup; } ftw->pathlen = pathlen; (*path)[pathlen] = '\0'; l2_launcher__ftw_update_path(path, ftw, ent->d_name); if (S_ISREG(st.st_mode)) { flags |= L2_FTW_FILE; } if (S_ISDIR(st.st_mode)) { flags |= L2_FTW_DIR; } if (S_ISLNK(st.st_mode)) { flags |= L2_FTW_SYMLINK; } ftw->user.baseoffset = (int)pathlen + 1; if ((res = (*proc)(*path, &st, flags, (struct l2_ftw_data *)ftw))) { CMD_DEBUG("myftw: user aborted %s: %d", *path, res); ftw->abort = 1; goto cleanup; } if (S_ISDIR(st.st_mode)) { if ((res = l2_launcher__ftw_internal(path, depth - 1, proc, ftw)) < 0 || ftw->abort) { goto cleanup; } } errno = 0; } if (errno) { CMD_DEBUG("myftw: readdir failed on %s: %s", *path, strerror(errno)); res = -1; goto cleanup; } --ftw->user.depth; res = 0; cleanup: closedir(thisdir); return res; } int l2_launcher_ftw(const char *path, int depth, l2_ftw_proc_t *proc, void *user) { struct l2_ftw__data_int data; size_t pathlen = strlen(path); data.user.depth = 0; data.user.reloffset = pathlen + 1; data.user.user = user; data.abort = 0; char *pathdup = strdup(path); data.pathcap = pathlen + 1; data.pathlen = pathlen; if (!pathdup) return -1; int res = l2_launcher__ftw_internal(&pathdup, depth, proc, &data); free(pathdup); return res; }