#include #include /* creat() etc. */ #include /* uint32_t */ #if HAS_POSIX #include /* ssize_t */ #include /* S_IRUSR */ #endif /* HAS_POSIX */ #include #include #include #include #include "pack.h" #include "log.h" #include "md5.h" /** * @file pack.c * * @brief Stores data in funky format. * * Format: * -- Index. * -- Magic number (16 bytes). * -- Number of files (uint32_t). * -- Files in format name/location. * -- File name (128 bytes max, ending in NUL). * -- File location (uint32_t). * -- File data in format Size/Data. * -- File size (uint32_t). * -- File data (char*). * -- File MD5 (16 byte char*). * -- EOF. * * -- Write magic number and number of files. * -- Write the index. * -- Pack the files. */ /** * @brief Abstracts around packfiles. */ struct Packfile_s { #if HAS_POSIX int fd; /**< File descriptor. */ #else char* name; /**< Hack to emulate dup2 by calling fopen again. */ FILE* fp; /**< For non-posix. */ #endif /* HAS_POSIX */ uint32_t pos; /**< Cursor position. */ uint32_t start; /**< File start. */ uint32_t end; /**< File end. */ uint32_t flags; /**< Special control flags. */ }; /** * @brief Allows much faster creation of packfiles. */ struct Packcache_s { #if HAS_POSIX int fd; /**< File descriptor. */ #else FILE* fp; /* For non-posix. */ #endif /* HAS_POSIX */ char** index; /**< Cached index for faster lookups. */ uint32_t* start; /**< Cached index starts. */ uint32_t* nindex; /**< Number of index entries. */ }; /* Helper defines. */ #if HAS_POSIX #define READ(f, b, n) if(read((f)->fd, (b), (n)) != (n)) { \ ERR("Fewer bytes read then expected"); \ return NULL; } #else #define READ(f, b, n) if(fread((b), 1, (n),(f)->fp) != (n)) { \ ERR("Fewer bytes read then expected"); \ return NULL; } #endif /* HAS_POSIX */ #undef DEBUG /* This will be spammy. */ #define DEBUG(str, args...) do{;} while(0) /**< Hack to disable debugging. */ #define BLOCKSIZE 128*1024 /**< The read/write block size */ /* Max filename length. */ #ifndef PATH_MAX #define PATH_MAX 256 #endif #define PERMS S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH /**< Default permissions. */ const uint64_t magic = 0x25524573; /**< File magic number: sER% */ /* Flags. */ #define PACKFILE_FROMCACHE (1<<0) /**< Packfile comes from a packcache. */ static off_t getfilesize(const char* filename); /* RWops stuff. */ static int packrw_seek(SDL_RWops* rw, int offset, int whence); static int packrw_read(SDL_RWops* rw, void* ptr, int size, int maxnum); static int packrw_write(SDL_RWops* rw, const void* ptr, int size, int num); static int packrw_close(SDL_RWops* rw); /** * @brief Open a packfile as a cache. * @param packfile Name of the packfile to cache. * @return NULL if an error occured or the Packcache. */ Packcache_t* pack_openCache(const char* packfile) { int j; uint32_t i; char buf[PATH_MAX]; Packcache_t* cache; /* Allocate memory. */ cache = calloc(1, sizeof(Packcache_t)); if(cache == NULL) { ERR("Out of memory."); return NULL; } /* Open file. */ #if HAS_POSIX cache->fd = open(packfile, O_RDONLY); if(cache->fd == -1) { #else cache->name = strdup(packfile); cache->fp = fopen(packfile, "rb"); if(cache->fp == NULL) { #endif /* HAS_POSIX */ ERR("Error opening %s: %s", packfile, strerror(errno)); return NULL; } /* Check for validity. */ READ(cache, buf, sizeof(magic)); if(memcmp(buf, &magic, sizeof(magic))) { ERR("File %s is not a valid packfile", packfile); return NULL; } /* Get the number of files and allocate memory. */ READ(cache, &cache->nindex, 4); cache->index = calloc(cache->nindex, sizeof(char*)); cache->start = calloc(cache->nindex, sizeof(uint32_t)); /* Read index. */ for(i = 0; i < cache->nindex; i++) { /* Start to search files. */ j = 0; READ(cache, &buf[j], 1); /* Get the name. */ while(buf[j++] != '\0') READ(cache, &buf[j], 1); cache->index[i] = strdup(buf); READ(cache, &cache->start[i], 4); DEBUG("'%s' found at %d", filename, cache->start[i]); } /* Return the built cache. */ return cache; } /** * @brief Close a Packcache. * @param cache Packcache to close. */ void pack_closeCache(Packcache_t* cache) { uint32_t i; /* Close file. */ #if HAS_POSIX close(cache->fd); #else free(cache->name); fclose(cache->fp); #endif /* HAS_POSIX */ /* Free memory. */ if(cache->nindex > 0) { for(i = 0; i < cache->nindex; i++) free(cache->index[i]); free(cache->index); free(cache->start); } free(cache); } /** * @brief Open a packfile from a Packcache. * @param cache Packcache to create Packfile from. * @param filename Name of the file to open in the Cache. * @return A packfile for filename from cache. */ Packfile_t* pack_openFromCache(Packcache_t* cache, const char* filename) { uint32_t i; Packfile_t* file; file = calloc(1, sizeof(Packfile_t)); for(i = 0; i < cache->nindex; i++) { if(strcmp(cache->index[i], filename)==0) { /* Copy file. */ #if HAS_POSIX file->fd = dup(cache->fd); #else file->fp = fopen(cache->name, "rb"); #endif /* Copy information. */ file->flags |= PACKFILE_FROMCACHE; file->start = cache->start[i]; /* Seek. */ if(file->start) { /* Go to the beginning of the file. */ #if HAS_POSIX if((uint32_t)lseek(file->fd, file->start, SEEK_SET) != file->start) { #else fseek(file->fp, file->start, SEEK_SET); if(errno) { #endif /* HAS_POSIX */ ERR("Failure to seek to file start: %s", strerror(errno)); return NULL; } READ(file, &file->end, 4); DEBUG("\t%d bytes", file->end); file->pos = file->start; file->end += file->start; } return file; } } free(file); WARN("File '%s' not found in packfile.", filename); return NULL; } /** * @brief Get the file size. * @param filename File to get the size of. * @return The size of the file. */ static off_t getfilesize(const char* filename) { #if HAS_POSIX struct stat file; if(!stat(filename, &file)) return file.st_size; ERR("Unable to get filesize of %s", filename); return 0; #else long size; FILE* fp = fopen(filename, "rb"); if(fp == NULL) return 0; fseek(fp, 0, SEEK_END); size = ftell(fp); fclose(fp); return size; #endif /* HAS_POSIX */ } /** * @fn int pack_check(const char* filename) * * @brief Check to see if a file is a packfile * @param filename Name of the file to check. * @return 0 if it is a packfile, 1 if it isn't and -1 on error. */ int pack_check(const char* filename) { int ret; char* buf; buf = malloc(sizeof(magic)); #if HAS_POSIX int fd = open(filename, O_RDONLY); if(fd == -1) { ERR("Error opening %s: %s", filename, strerror(errno)); return -1; } if(read(fd, buf, sizeof(magic)) != sizeof(magic)) { ERR("Error reading magic number: %s", strerror(errno)); free(buf); return -1; } ret = (memcmp(buf, &magic, sizeof(magic))==0) ? 0 : 1; close(fd); #else FILE* file = fopen(filename, "rb"); if(file == NULL) { ERR("Error opening '%s': %s", filename, strerror(errno)); return -1; } buf = malloc(sizeof(magic)); if(fread(buf, 1, sizeof(magic), file) != sizeof(magic)) { ERR("Error reading magic number: %s", strerror(errno)); free(buf); return -1; } ret = (memcmp(buf, &magic, sizeof(magic))==0) ? 0 : 1; fclose(file); #endif /* HAS_POSIX */ free(buf); return ret; } #if HAS_POSIX #define WRITE(b,n) if(write(outfd, b, n)==-1) { \ ERR("Error writing to file: %s", strerror(errno)); \ free(buf); return -1; } /**< Macro to help check for errors. #else #define WRITE(b,n) if(fwrite(b, 1, n, outf)==0) { \ ERR("Error writing to file: %s", strerror(errno)); \ free(buf); return -1; } /**< Macro to help check for errors. */ #endif /* HAS_POSIX */ /** * @brief Packages files into a packfile. * @param outfile Name of the file to output to. * @param infiles Array of filenames to package. * @param nfiles Number of filenames in infiles. * @return 0 on success. */ int pack_files(const char* outfile, const char** infiles, const uint32_t nfiles) { void* buf; #if HAS_POSIX struct stat file; int outfd, infd; #else FILE* outf, *inf; #endif /* HAS_POSIX */ uint32_t i; int namesize; uint32_t indexsize, pointer; int bytes; const uint8_t b = '\0'; for(namesize = 0, i = 0; i < nfiles; i++) { /* Make sure file exists before writing. */ #if HAS_POSIX if(stat(infiles[i], &file)) { #else if(getfilesize(infiles[i]) == 0) { #endif /* HAS_POSIX */ ERR("File %s does not exist", infiles[i]); return -1; } if(strlen(infiles[i]) > PATH_MAX) { ERR("filename '%s' is too long, should be only %d characters", infiles[i], PATH_MAX); return -1; } namesize += strlen(infiles[i]); } indexsize = (sizeof(magic) + 4 + /* Magic number and number of files. */ namesize + /* Total length of file names. */ (1+4)*nfiles); /* File size and extra end of string char '\0'. */ DEBUG("Index size is %d", indexsize); /* Create the output file. */ #if HAS_POSIX outfd = creat(outfile, PERMS); if(outfd == -1) { #else outf = fopen(outfile, "wb"); if(outf == NULL) { #endif /* HAS_POSIX */ ERR("Unable to open %s for writing", outfile); return -1; } /* Index! */ buf = malloc(BLOCKSIZE); /* Magic number. */ WRITE(&magic, sizeof(magic)); DEBUG("Wrote magic number"); /* Number of files. */ WRITE(&nfiles, sizeof(nfiles)); DEBUG("Wrote number of files: %d", nfiles); /* Create file dependent index part. */ pointer = indexsize; for(i = 0; i < nfiles; i++) { WRITE(infiles[i], strlen(infiles[i])); DEBUG("File: '%s' at %d", infiles[i], pointer); WRITE(&b, 1); WRITE(&pointer, 4); pointer += 4 + getfilesize(infiles[i]) + 16; /* Set pointer to be next file pos. */ } /* Data! */ md5_state_t md5; md5_byte_t* md5val = malloc(16); for(i = 0; i < nfiles; i++) { bytes = (uint32_t)getfilesize(infiles[i]); WRITE(&bytes, 4); /* filesize. */ DEBUG("About to write '%s' of %d bytes", infiles[i], bytes); md5_init(&md5); #if HAS_POSIX infd = open(infiles[i], O_RDONLY); while((bytes = read(infd, buf, BLOCKSIZE))) { #else inf = fopen(infiles[i], "rb"); while((bytes = fread(buf, 1, BLOCKSIZE, inf))) { #endif /* HAS_POSIX */ WRITE(buf, bytes); /* Data. */ md5_append(&md5, buf, bytes); } md5_finish(&md5, md5val); WRITE(md5val, 16); #if HAS_POSIX close(infd); #else fclose(inf); #endif /* HAS_POSIX */ DEBUG("Wrote file '%s'", infiles[i]); } free(md5val); #if HAS_POSIX close(outfd); #else fclose(outf); #endif /* HAS_POSIX */ free(buf); DEBUG("Packfile success\n\t%d files\n\t%d bytes", nfiles, (int)getfilesize(outfile)); return 0; } #undef WRITE /** * @brief Open a file in the packfile for reading. * @param packfile Path to real packfile. * @param filename Name of the file within th. packfile. * @return The newly created packfile or NULL on error. */ Packfile_t* pack_open(const char* packfile, const char* filename) { int j; uint32_t nfiles, i; char buf[PATH_MAX]; Packfile_t* file; /* Allocate memory. */ file = malloc(sizeof(Packfile_t)); memset(file, 0, sizeof(Packfile_t)); #if HAS_POSIX file->fd = open(packfile, O_RDONLY); if(file->fd == -1) { #else file->fp = fopen(packfile, "rb"); if(file->fp == NULL) { #endif /* HAS_POSIX */ ERR("Error opening %s: %s", filename, strerror(errno)); return NULL; } READ(file, buf, sizeof(magic)); /* Make sure it's a packfile. */ if(memcmp(buf, &magic, sizeof(magic))) { ERR("File %s is not a valid packfile", filename); return NULL; } READ(file, &nfiles, 4); for(i = 0; i < nfiles; i++) { /* Start to search files. */ j = 0; READ(file, &buf[j], 1); /* Get the name. */ while(buf[j++] != '\0') READ(file, &buf[j], 1); if(strcmp(filename, buf) == 0) { /* We found the file. */ READ(file, &file->start, 4); DEBUG("'%s' found at %d", filename, file->start); break; } #if HAS_POSIX lseek(file->fd, 4, SEEK_CUR); /* Ignore the file location. */ #else fseek(file->fp, 4, SEEK_CUR); #endif /* HAS_POSIX */ } if(file->start) { /* Go to the beginning of the file. */ #if HAS_POSIX if((uint32_t)lseek(file->fd, file->start, SEEK_SET) != file->start) { #else fseek(file->fp, file->start, SEEK_SET); if(errno) { #endif /* HAS_POSIX */ ERR("Failure to seek to file start: %s", strerror(errno)); return NULL; } READ(file, &file->end, 4); DEBUG("\t%d bytes", file->end); file->pos = file->start; file->end += file->start; } else { ERR("File '%s' not found in packfile '%s'", filename, packfile); return NULL; } return file; } /** * @brief Reads data from a packfile. * * Behaves like POSIX read. * @param file Opened packfile to read data from. * @param buf Allocated buffer to read into. * @param count Bytes to read. * @return Bytes to read or -1 on error. */ ssize_t pack_read(Packfile_t* file, void* buf, size_t count) { int bytes; if(file->pos + count > file->end) count = file->end - file->pos; /* Can't go past the end! */ if(count == 0) return 0; #if HAS_POSIX if((bytes = read(file->fd, buf, count)) == -1) { #else if((bytes = fread(buf, 1, count, file->fp)) == -1) { #endif /* HAS_POSIX */ ERR("Error while reading file: %s", strerror(errno)); return -1; } file->pos += bytes; return bytes; } /** * @brief Seeks within a file inside a packfile. * * Behaves like lseek/fseek. * * @todo It's broken, needs fixing. * @param file File to seek. * @param offset Position to seek to. * @param whence Either SEEK_SET, SEEK_CUR or SEEK_END. * @return The position moved to. */ off_t pack_seek(Packfile_t* file, off_t offset, int whence) { off_t ret; DEBUG("Attempting to seek offset: %d, whence: %d", offset, whence); switch(whence) { #if HAS_POSIX case SEEK_SET: if((file->start + offset) > file->end) return -1; ret = lseek(file->fd, file->start + offset, SEEK_SET); if(ret != ((off_t)file->start + offset)) return -1; break; case SEEK_CUR: if((file->start + offset) > file->end) return -1; ret = lseek(file->fd, file->pos + offset, SEEK_SET); if(ret != ((off_t)file->pos + offset)) return -1; break; case SEEK_END: if((file->end + offset) < file->start) return -1; ret = lseek(file->fd, file->end + offset, SEEK_SET); if(ret != ((off_t)file->end - offset)) return -1; break; #else case SEEK_SET: if((file->start + offset) > file->end) return -1; ret = fseek(file->fd, file->start + offset, SEEK_SET); if(ret != (file->start + offset)) return -1; break; case SEEK_CUR: if((file->start + offset) > file->end) return -1; ret = fseek(file->fd, file->pos + offset -1, SEEK_SET); if(ret != (file->pos + offset)) return -1; break; case SEEK_END: if((file->end + offset) < file->start) return -1; ret = fseek(file->fd, file->end + offset, SEEK_SET); if(ret != (file->end - offset)) return -1; break; #endif /* HAS_POSIX */ default: ERR("Whence is not one of SEEK_SET, SEEK_CUR or SEEK_END"); return -1; } return ret - file->start; } /** * @brief Get the current position in the file. * @param file Packfile to get the position from. * @return The current position in the file. */ long pack_tell(Packfile_t* file) { return file->pos - file->start; } /** * @brief Read a file from a packfile. */ static void* pack_readfilePack(Packfile_t* file, const char* filename, uint32_t* filesize) { void* buf; char* str; int size, bytes; /* Read the entire file. */ size = file->end - file->start; buf = malloc(size+1); if(buf == NULL) { ERR("Unable to allocate %d bytes of memory!", size+1); free(file); return NULL; } if((bytes = pack_read(file, buf, size)) != size) { ERR("Reading '%s' from packfile. Expected %d bytes got %d bytes", filename, size, bytes); free(buf); free(file); return NULL; } DEBUG("Read %d bytes from '%s'", bytes, filename); str = buf; str[size] = '\0'; /* Append size '\0' for it to validate as a string. */ /* Check the md5. */ md5_state_t md5; md5_byte_t* md5val = malloc(16); md5_byte_t* md5fd = malloc(16); md5_init(&md5); md5_append(&md5, buf, bytes); md5_finish(&md5, md5val); #if HAS_POSIX if((bytes = read(file->fd, md5fd, 16)) == -1) #else if((bytes = fread(md5fd, 1, 16, file->fp)) == -1) #endif /* HAS_POSIX */ WARN("Failure to read MD5, continuing anyway.."); else if(memcmp(md5val, md5fd, 16)) WARN("MD5 gives different value, possible memory corruption, continuing.."); free(md5val); free(md5fd); /* Cleanup. */ if(pack_close(file) == -1) { ERR("Closing packfile"); free(file); return NULL; } DEBUG("Closed '%s' in '%s'", filename, packfile); if(filesize) *filesize = size; return buf; } /** * @brief Read an entire file into memory. * @param packfile Name of the packfile to read from. * @param filename Name of the packed file to read. * @param filesize Is set to the size of the file. * @return A pointer to the sata in the file or NULL if an error accured. */ void* pack_readfile(const char* packfile, const char* filename, uint32_t* filesize) { Packfile_t* file; /* Initialize size to 0. */ if(filesize) *filesize = 0; /* Open the packfile. */ file = pack_open(packfile, filename); if(file == NULL) { ERR("Opening packfile '%s'.", packfile); return NULL; } DEBUG("Opened file '%s' from '%s'", filename, packfile); return pack_readfilePack(file, filename, filesize); } /** * @fn char** pack_listfiles(const char* packfile, uint32_t* nfiles) * * @brief Get what files are in the packfile. * * Each name must be freed individually afterwords and the array of names too. * @param packfile Packfile to query it's internal files. * @param nfiles Stores the amount of files in packfile. * @return An array of filenames in packfile. */ char** pack_listfiles(const char* packfile, uint32_t* nfiles) { int j; uint32_t i; Packfile_t file; char** filenames; char* buf = malloc(sizeof(magic)); *nfiles = 0; #if HAS_POSIX file.fd = open(packfile, O_RDONLY); if(file.fd == -1) { #else file.fp = fopen(packfile, "rb"); if(file.fp == NULL) { #endif /* HAS_POSIX */ ERR("opening %s: %s", packfile, strerror(errno)); return NULL; } READ(&file, buf, sizeof(magic)); /* Make sure it's a packfile. */ if(memcmp(buf, &magic, sizeof(magic))) { ERR("File %s is not a valid packfile", packfile); return NULL; } READ(&file, nfiles, 4); filenames = malloc(((*nfiles)+1)*sizeof(char*)); for(i = 0; i < *nfiles; i++) { /* Start searching files. */ j = 0; filenames[i] = malloc(PATH_MAX * sizeof(char)); READ(&file, &filenames[i][j], 1); /* Get the name. */ while(filenames[i][j++] != '\0') READ(&file, &filenames[i][j], 1); READ(&file, buf, 4); /* skip the location. */ } free(buf); #if HAS_POSIX close(file.fd); #else fclose(file.fp); #endif /* HAS_POSIX */ return filenames; } /** * @brief Read an entire file from the cache. */ void* pack_readfileCached(Packcache_t* cache, const char* filename, uint32_t* filesize) { Packfile_t* file; file = pack_openFromCache(cache, filename); if(file == NULL) ERR("Unable to create packfile from packcache."); return pack_readfilePack(file, filename, filesize); } /** * @brief Get the list of files in a packcache. * @param cache Cache to get list of files from. * @param nfiles Number of files in the list. * @return A read only list of the files from the pack cache. */ const char** pack_listfilesCached(Packcache_t* cache, uint32_t* nfiles) { *nfiles = cache->nindex; return (const char**)cache->index; } /** * @brief Closes a packfile. * @param file Packfile to close. * @return 0 on success. */ int pack_close(Packfile_t* file) { int i; /* Close files. */ #if HAS_POSIX i = close(file->fd); #else i = fclose(file->fp); #endif /* HAS_POSIX */ /* Free memory. */ free(file); return (i) ? -1 : 0; } static int packrw_seek(SDL_RWops* rw, int offset, int whence) { Packfile_t* packfile; packfile = rw->hidden.unknown.data1; return pack_seek(packfile, offset, whence); } static int packrw_read(SDL_RWops* rw, void* ptr, int size, int maxnum) { int i; ssize_t ret; char* buf; Packfile_t* packfile; packfile = rw->hidden.unknown.data1; buf = ptr; /* Read the data. */ for(i = 0; i < maxnum; i++) { ret = pack_read(packfile, &buf[i*size], size); if(ret != size) break; } return i; } static int packrw_write(SDL_RWops* rw, const void* ptr, int size, int num) { (void) rw; (void) ptr; (void) size; (void) num; return -1; } static int packrw_close(SDL_RWops* rw) { Packfile_t* packfile; packfile = rw->hidden.unknown.data1; return pack_close(packfile); } /** * @brief Create a rwops from a packfile. * @param packfile Packfile to create rwops from. * @return rwops created from packfile. */ static SDL_RWops* pack_rwopsRaw(Packfile_t* packfile) { SDL_RWops* rw; /* Create the rwops. */ rw = SDL_AllocRW(); if(rw == NULL) { WARN("Unable to allocate SDL_RWops."); return NULL; } /* Set the functions. */ rw->seek = packrw_seek; rw->read = packrw_read; rw->write = packrw_write; rw->close = packrw_close; /* Set the packfile as the hidden data. */ rw->hidden.unknown.data1 = packfile; return rw; } /** * @brief Create an rwops for a file in a packfile. * @param packfile Packfile to get file from. * @param filename File within packfile to create rwops from. * @return SDL_RWops interacting with the file in the packfile. */ SDL_RWops* pack_rwops(const char* packfile, const char* filename) { Packfile_t* pack; /* Open the packfile. */ pack = pack_open(packfile, filename); if(pack == NULL) return NULL; /* Return the rwops. */ return pack_rwopsRaw(pack); } /** * @brief Create an rwops for a file in a packcache. * @param cache Packcache to get file from. * @param filename File within the cache to create rwops from. * @return SDL_RWops interacting with the file in the packcache. */ SDL_RWops* pack_rwopsCached(Packcache_t* cache, const char* filename) { Packfile_t* packfile; /* Open the packfile. */ packfile = pack_openFromCache(cache, filename); if(packfile == NULL) return NULL; /* Return the rwops. */ return pack_rwopsRaw(packfile); }