#include #include #include #include #include #include #include #include #include #include #include #include "espfsformat.h" #include "heatshrink_encoder.h" #include "parsing.h" #include "httpd-logging.h" void show_version(char **argv); void show_help(int retval, char **argv); #define DEFAULT_GZIP_EXTS "css,js,svg,png,jpg,jpeg,webm,ico,gif" #define DEFAULT_C_VARNAME "espfs_image" struct InputFileLinkedListEntry { char *name; struct InputFileLinkedListEntry *next; }; /// two ends of a linked list with input files static struct InputFileLinkedListEntry *s_inputFiles = NULL; static struct InputFileLinkedListEntry *s_lastInputFile = NULL; /// Output file FD static int s_outFd = STDOUT_FILENO; /// Array of gzipped extensions, ends with a NULL pointer static char **s_gzipExtensions = NULL; /// Gzip all files static bool s_gzipAll = false; // impls to satisfy defs in the config header void *httpdPlatMalloc(size_t len) { return malloc(len); } void httpdPlatFree(void *ptr) { free(ptr); } /** * Compress a file using Heatshrink * * @param[in] in - pointer to the uncompressed input * @param insize - len of the uncompressed input * @param[out] out - destination buffer for the compressed data * @param outcap - capacity of the output buffer * @param level - compression level, 1-9; -1 for default. * @return actual length of the compressed data */ size_t compressHeatshrink(const uint8_t *in, size_t insize, uint8_t *out, size_t outcap, int level) { const uint8_t *inp = in; uint8_t *outp = out; size_t len; uint8_t ws[] = {5, 6, 8, 11, 13}; uint8_t ls[] = {3, 3, 4, 4, 4}; HSE_poll_res pres = 0; HSE_sink_res sres = 0; size_t r; if (level == -1) { level = 8; } level = (level - 1) / 2; //level is now 0, 1, 2, 3, 4 heatshrink_encoder *enc = heatshrink_encoder_alloc(ws[level], ls[level]); if (enc == NULL) { espfs_error("allocating mem for heatshrink"); exit(1); } //Save encoder parms as first byte *outp = (uint8_t) ((ws[level] << 4) | ls[level]); outp++; outcap--; r = 1; do { if (insize > 0) { sres = heatshrink_encoder_sink(enc, inp, insize, &len); if (sres != HSER_SINK_OK) { break; } inp += len; insize -= len; if (insize == 0) { heatshrink_encoder_finish(enc); } } do { pres = heatshrink_encoder_poll(enc, outp, outcap, &len); if (pres != HSER_POLL_MORE && pres != HSER_POLL_EMPTY) { break; } outp += len; outcap -= len; r += len; } while (pres == HSER_POLL_MORE); } while (insize != 0); if (insize != 0) { espfs_error("Heatshrink: Bug? insize is still %d. sres=%d pres=%d", (int) insize, sres, pres); exit(1); } heatshrink_encoder_free(enc); return r; } /** * Compress a file using Gzip * * @param[in] in - pointer to the uncompressed input * @param insize - len of the uncompressed input * @param[out] out - destination buffer for the compressed data * @param outcap - capacity of the output buffer * @return actual length of the compressed data */ size_t compressGzip(const uint8_t *in, size_t insize, uint8_t *out, size_t outcap) { z_stream stream; int zresult; stream.zalloc = Z_NULL; stream.zfree = Z_NULL; stream.opaque = Z_NULL; stream.next_in = in; stream.avail_in = (uInt) insize; stream.next_out = out; stream.avail_out = (uInt) outcap; // 31 -> 15 window bits + 16 for gzip zresult = deflateInit2(&stream, Z_BEST_COMPRESSION /* we want the smallest possible files */, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY); if (zresult != Z_OK) { espfs_error("DeflateInit2 failed with code %d", zresult); exit(1); } zresult = deflate(&stream, Z_FINISH); if (zresult != Z_STREAM_END) { espfs_error("Deflate failed with code %d", zresult); exit(1); } zresult = deflateEnd(&stream); if (zresult != Z_OK) { espfs_error("DeflateEnd failed with code %d", zresult); exit(1); } return stream.total_out; } /** * Check if a file name should be compressed by gzip * * @param name - file name * @return true if should compress */ bool shouldCompressGzip(const char *name) { if (!s_gzipExtensions) { return false; } if (s_gzipAll) { return true; } const char *ext = name + strlen(name); while (*ext != '.') { ext--; if (ext < name) { // no dot in file name -> no extension -> nothing to match against return false; } } ext++; int i = 0; while (s_gzipExtensions[i] != NULL) { if (strcasecmp(ext, s_gzipExtensions[i]) == 0) { return true; } i++; } return false; } /** * Parse a list of gzipped extensions * * @param input - list of comma-separated extensions, e.g. "jpg,png" * @return */ void parseGzipExtensions(char *input) { char *token; char *extList = input; size_t count = 2; // one for first element, second for terminator // count elements while (*extList != 0) { if (*extList == ',') { count++; } extList++; } // split string extList = input; s_gzipExtensions = malloc(count * sizeof(char *)); count = 0; token = strtok(extList, ","); while (token) { s_gzipExtensions[count++] = token; token = strtok(NULL, ","); } // terminate list s_gzipExtensions[count] = NULL; } /** * Process a file. * * @param fd - filedes * @param[in] name - filename to embed in the archive * @param compression_mode - compression mode * @param level - compression level for heatshrink, 1-9 * @param[out] compName - the used compression is output here (for debug print) * @return - size of the output, in percent (100% = no compression) */ int handleFile(int fd, const char *name, int compression_mode, int level, const char **compName) { uint8_t *fdat = NULL, *cdat = NULL, *cdatbuf = NULL; size_t size, csize; EspFsHeader h; uint16_t realNameLen; uint8_t flags = 0; size = (size_t) lseek(fd, 0, SEEK_END); fdat = malloc(size); lseek(fd, 0, SEEK_SET); read(fd, fdat, size); if (shouldCompressGzip(name)) { csize = size * 3; if (csize < 100) { // gzip has some headers that do not fit when trying to compress small files csize = 100; } // enlarge buffer if this is the case cdat = cdatbuf = malloc(csize); csize = compressGzip(fdat, size, cdat, csize); compression_mode = EFS_COMPRESS_NONE; // don't use heatshrink if gzip was already used - it would only make it bigger flags = EFS_FLAG_GZIP; } else if (compression_mode == EFS_COMPRESS_NONE) { csize = size; cdat = fdat; } else if (compression_mode == EFS_COMPRESS_HEATSHRINK) { cdat = cdatbuf = malloc(size * 2); csize = compressHeatshrink(fdat, size, cdat, size * 2, level); } else { espfs_error("Unknown compression - %d", compression_mode); exit(1); } if (csize > size) { espfs_dbg("! Compression enbiggened %s, embed as plain", name); //Compressing enbiggened this file. Revert to uncompressed store. compression_mode = EFS_COMPRESS_NONE; csize = size; cdat = fdat; flags = 0; } //Fill header data h.magic = htole32(EFS_MAGIC); // ('E' << 0) + ('S' << 8) + ('f' << 16) + ('s' << 24); h.flags = flags; h.compression = (uint8_t) compression_mode; h.nameLen = realNameLen = (uint16_t) strlen(name) + 1; // zero terminator uint16_t padbytes = 0; if (h.nameLen & 3) { //Round to next 32bit boundary padbytes = 4 - (h.nameLen & 3); h.nameLen += padbytes; // include the bytes in "name" to make parsing easier - these will be zeroed out, so the c-string remains the same. } h.nameLen = htole16(h.nameLen); h.fileLenComp = htole32((uint32_t) csize); h.fileLenDecomp = htole32((uint32_t) size); write(s_outFd, &h, sizeof(EspFsHeader)); write(s_outFd, name, realNameLen); if (padbytes) { write(s_outFd, "\0\0\0", padbytes); // these zeros are included in h.nameLen } write(s_outFd, cdat, csize); //Pad out to 32bit boundary - the parser does this automatically when walking over the archive. if (csize & 3) { padbytes = 4 - (csize & 3); write(s_outFd, "\0\0\0", padbytes); csize += padbytes; } free(fdat); if (cdatbuf) { // free the buffer allocated for compression output free(cdatbuf); } // debug outputs ... if (compName != NULL) { if (h.compression == EFS_COMPRESS_HEATSHRINK) { *compName = "heatshrink"; } else if (h.compression == EFS_COMPRESS_NONE) { if (h.flags & EFS_FLAG_GZIP) { *compName = "gzip"; } else { *compName = "none"; } } else { *compName = "unknown"; } } // get compression % (lower is better) return size ? (int) ((csize * 100) / size) : 100; } /** * Write final dummy header with FLAG_LASTFILE set. */ void finishArchive(void) { EspFsHeader h; h.magic = htole32(EFS_MAGIC); h.flags = EFS_FLAG_LASTFILE; h.compression = EFS_COMPRESS_NONE; h.nameLen = 0; h.fileLenComp = 0; h.fileLenDecomp = 0; write(s_outFd, &h, sizeof(EspFsHeader)); } /** * Queue a file for adding to the archive. * Appends it to the `s_inputFiles` linked list. * * @param name - file name to add */ void queueInputFile(const char *name) { espfs_dbg("INFILE: %s", name); struct InputFileLinkedListEntry *tmp = malloc(sizeof(struct InputFileLinkedListEntry)); tmp->name = strdup(name); tmp->next = NULL; if (s_lastInputFile == NULL) { s_inputFiles = tmp; s_lastInputFile = tmp; } else { s_lastInputFile->next = tmp; s_lastInputFile = tmp; } } enum OpMode { OM_INVALID = 0, OM_PACK = 'P', OM_LIST = 'L', OM_EXTRACT = 'X', OM_EMBED = 'M', }; #define BUFLEN 1024 int main(const int argc, char **argv) { int f; char inputFileName[BUFLEN]; char tempbuf[BUFLEN]; struct stat statBuf; int serr; int rate; int compType; //default compression type - heatshrink int compLvl = -1; bool use_gzip = false; enum OpMode opmode = OM_INVALID; compType = EFS_COMPRESS_HEATSHRINK; int c; char *outFileName = NULL; char *c_varname = NULL; char *stripPath = NULL; char *extractFileName = NULL; size_t num_input_files = 0; bool read_from_stdin = false; while (1) { int option_index = 0; static struct option long_options[] = { // Help {"help", no_argument, 0, 'h'}, {"version", no_argument, 0, 'V'}, // Main operation (one at a time) {"pack", no_argument, 0, 'P'}, {"list", no_argument, 0, 'L'}, {"extract", required_argument, 0, 'X'}, {"embed", no_argument, 0, 'M'}, // Common options {"input", required_argument, 0, 'i'}, // input file; "-" to read them as lines on stdin; can be repeated in case of --pack. // Input files can also be given as stand-alone arguments at the end, e.g. as a result of glob {"output", required_argument, 0, 'o'}, // output file; "-" for stdout (default) // Options for --pack {"compress", required_argument, 0, 'c'}, // 0 = no, 1 = heatshrink (def) {"level", required_argument, 0, 'l'}, // Heatshrink compression level 1-9, -1 = default {"gzip", optional_argument, 0, 'z'}, // Gzipped extensions; no arg = default, "*" = all, comma-separated list = custom exts {"strip", required_argument, 0, 's'}, // path removed from all input files, e.g. when they are in a subfolder // Options for --embed {"varname", required_argument, 0, 'n'}, // name of the array; the length variable is {varname}_len { /* end marker */ } }; c = getopt_long(argc, argv, "h?VPLX:Mi:o:c:l:z::s:n:", long_options, &option_index); if (c == -1) { break; } switch (c) { case 'h': case '?': show_help(0, argv); case 'V': show_version(argv); case 'P': opmode = OM_PACK; break; case 'L': opmode = OM_LIST; break; case 'X': opmode = OM_EXTRACT; if (extractFileName) { espfs_error("can extract only one file at a time!"); exit(1); } extractFileName = strdup(optarg); break; case 'M': opmode = OM_EMBED; break; case 'i': if (0 == strcmp(optarg, "-")) { read_from_stdin = true; } else { queueInputFile(optarg); } num_input_files++; break; case 'o': outFileName = strdup(optarg); break; case 'c': errno = 0; compType = (int) strtol(optarg, NULL, 10); if (errno != 0 || compType < 0 || compType > 1) { espfs_error("Bad compression mode: %s", optarg); exit(1); } break; case 'l': errno = 0; compLvl = (int) strtol(optarg, NULL, 10); if (errno != 0 || compLvl < 1 || compLvl > 9) { espfs_error("Bad compression level: %s", optarg); exit(1); } break; case 'z': use_gzip = true; if (optarg) { if (0 == strcmp("*", optarg)) { s_gzipAll = true; } else { parseGzipExtensions(optarg); } } else { parseGzipExtensions(strdup(DEFAULT_GZIP_EXTS)); // memory leak! ehh } break; case 's': stripPath = strdup(optarg); break; case 'n': c_varname = strdup(optarg); break; default: espfs_error("Unknown option: %c", c); exit(1); } } if (!use_gzip) { s_gzipExtensions = NULL; } bool want_output; bool allows_multiple_inputs; switch (opmode) { case OM_PACK: want_output = true; allows_multiple_inputs = true; break; case OM_LIST: want_output = false; allows_multiple_inputs = false; break; case OM_EXTRACT: case OM_EMBED: want_output = true; allows_multiple_inputs = false; break; default: espfs_error("Specify one of the operation modes: -P, -L, -X, -M"); exit(1); } /* Input */ while (optind < argc) { char *s = argv[optind++]; if (0 == strcmp(s, "-")) { read_from_stdin = true; } else { queueInputFile(s); num_input_files++; } } if (num_input_files == 0 && read_from_stdin && opmode == OM_PACK) { read_from_stdin = false; espfs_dbg("Reading input file names from stdin"); while (fgets(inputFileName, sizeof(inputFileName), stdin)) { //Kill off '\n' at the end inputFileName[strlen(inputFileName) - 1] = 0; queueInputFile(inputFileName); num_input_files++; } } if (!read_from_stdin) { if (num_input_files == 0) { if (allows_multiple_inputs) { espfs_error("Specify input file(s)!"); } else { espfs_error("Specify input file!"); } exit(1); } else if (!allows_multiple_inputs && num_input_files > 1) { espfs_error("Mode %c requires exactly one input file!", opmode); exit(1); } } char *inFileName = read_from_stdin ? NULL : s_inputFiles->name; /* Output */ if (!want_output && outFileName) { espfs_error("Output file is not allowed in %c mode!", opmode); exit(1); } FILE *outFile = NULL; bool write_to_stdout = outFileName && (0 == strcmp("-", outFileName)); if (outFileName && !write_to_stdout) { espfs_dbg("Writing to %s", outFileName); outFile = fopen(outFileName, "w+"); if (!outFile) { perror(outFileName); return 1; } s_outFd = fileno(outFile); ftruncate(s_outFd, 0); } else { if (outFileName) { free(outFileName); outFileName = NULL; } if (want_output && !write_to_stdout) { espfs_error("Specify output file! Use -o - for stdout"); exit(1); } } /* Do it! */ switch (opmode) { case OM_PACK: { struct InputFileLinkedListEntry *entry = s_inputFiles; while (entry) { char *name = entry->name; //Only include files serr = stat(name, &statBuf); if ((serr == 0) && S_ISREG(statBuf.st_mode)) { //Strip off './' or '/' madness. char *embeddedName = name; f = open(name, O_RDONLY); if (f > 0) { // relative path starting with ./, remove that if (embeddedName[0] == '.' && embeddedName[1] == '/') { embeddedName += 2; } // remove prefix if (stripPath && 0 == strncmp(embeddedName, stripPath, strlen(stripPath))) { embeddedName += strlen(stripPath); } // remove leading slash, if any if (embeddedName[0] == '/') { embeddedName++; } const char *compName = "unknown"; rate = handleFile(f, embeddedName, compType, compLvl, &compName); (void) rate; espfs_dbg("%s (%d%%, %s)", embeddedName, rate, compName); close(f); } else { snprintf(tempbuf, BUFLEN, "Open %s for reading: %s", name, strerror(errno)); espfs_error("%s", tempbuf); exit(1); } } else if (serr != 0) { snprintf(tempbuf, BUFLEN, "Stat %s: %s", name, strerror(errno)); espfs_error("%s", tempbuf); exit(1); } entry = entry->next; } finishArchive(); fsync(s_outFd); if (outFile) { fclose(outFile); } } break; case OM_LIST: { if (!inFileName) { espfs_error("Input file required!"); exit(1); } parseEspfsImage(inFileName, NULL, s_outFd); } break; case OM_EXTRACT: { if (!inFileName) { espfs_error("Input file required!"); exit(1); } parseEspfsImage(inFileName, extractFileName, s_outFd); } break; case OM_EMBED: { FILE *inFile = NULL; int inFD = STDIN_FILENO; if (!read_from_stdin) { inFile = fopen(inFileName, "r"); if (!inFile) { snprintf(tempbuf, BUFLEN, "Open %s for reading: %s", inFileName, strerror(errno)); espfs_error("%s", tempbuf); exit(1); } inFD = fileno(inFile); } if (!c_varname) { c_varname = strdup(DEFAULT_C_VARNAME); } size_t len; len = (size_t) snprintf(tempbuf, BUFLEN, "unsigned char %s[] = {", c_varname); write(s_outFd, tempbuf, len); uint8_t u; size_t imageLen = 0; while (1 == read(inFD, &u, 1)) { len = (size_t) snprintf(tempbuf, BUFLEN, "%s0x%02x,", ((imageLen % 16) ? " " : "\n "), u); write(s_outFd, tempbuf, len); imageLen++; } len = (size_t) snprintf(tempbuf, BUFLEN, "\n};\nunsigned int %s_len = %lu;\n", c_varname, imageLen); write(s_outFd, tempbuf, len); fsync(s_outFd); if (outFile) { fclose(outFile); } if (inFile) { fclose(inFile); } } break; default: __builtin_unreachable(); } return 0; } __attribute__((noreturn)) void show_version(char **argv) { // to stdout printf("%s #%s\n", argv[0], GIT_HASH); exit(0); } __attribute__((noreturn)) void show_help(int retval, char **argv) { // to stderr, this can happen as a response to bad args // ##########**********##########**********##########----------$$$$$$$$$$----------$$$$$$$$$$----------| 80 chars fprintf(stderr, "%s #%s - Program to create and parse espfs images\n", argv[0], GIT_HASH); fprintf(stderr, "\n" "One main operation mode must be selected:\n" " -P, --pack Create a binary fs image\n" " -L, --list Read a binary fs image and show the contents\n" " -X, --extract=NAME Read a binary fs image and extract a file with the given name\n" " -M, --embed Read a binary file (typically the binary fs image produced by -P) and\n" " convert it to C syntax byte array and a constant with its length.\n" " -h, -?, --help Show this help (has precedence over other options)\n" "\n" "Additional arguments specify the input, output and parameters:\n" " -i, --input=FILE Input file name. Can be used multiple times. For convenience with globs,\n" " input files can be given at the end of the option string without -i.\n" " The \"-\" filename means \"read from stdin\":\n" " - pack: reads file names to pack from stdin (e.g. piped from `find`)\n" " - embed: read the binary file from stdin (e.g. piped from the --pack\n" " option, avoiding the creation of a temporary binary file)\n" " Stdin reading is not supported for options --extract and --list.\n" "\n" " -o, --output=FILE Output file, \"-\" for stdout.\n" " Output can't be changed in --list mode.\n" "\n" "Pack options:\n" " -c, --compress=MODE Compression mode, 0=none, 1=heatshrink (default). Not used if the file\n" " is gzipped - additional compression wouldn't be useful.\n" " -l, --level=LEVEL Compression level, 1-9, -1=default. 1=worst, 9=best compression, but uses\n" " more RAM to unpack.\n" " -s, --strip=PATH Strip a path prefix from all packed files (e.g. a subfolder name)\n" " -z, --gzip[=EXTS] Enable gzip compression for some file types (filename extensions).\n" " By default, enabled for "DEFAULT_GZIP_EXTS".\n" " Set EXTS to * to enable gzip for all files. Custom extensions are set as\n" " a comma-separated list.\n" "\n" "Embed options:\n" " -n, --varname=VARNAME Set custom array name to use in the generated C code. The length constant\n" " is called {VARNAME}_len. The default VARNAME is \""DEFAULT_C_VARNAME"\"\n" ); exit(retval); }