// license:BSD-3-Clause // copyright-holders:Nathan Woods /*************************************************************************** zippath.c File/directory/path operations that work with ZIP files ***************************************************************************/ #include "zippath.h" #include "unzip.h" #include "corestr.h" #include "osdcore.h" #include #include #include #include #include namespace util { /*************************************************************************** TYPE DEFINITIONS ***************************************************************************/ /** * @class zippath_directory * * @brief A zippath directory. */ class zippath_directory { public: zippath_directory() : returned_parent(false) , directory() , called_zip_first(false) , zipfile() , returned_dirlist() { } /* common */ /** @brief true to returned parent. */ bool returned_parent; /** @brief The returned entry. */ osd::directory::entry returned_entry; /* specific to normal directories */ /** @brief Pathname of the directory. */ osd::directory::ptr directory; /* specific to ZIP directories */ /** @brief true to called zip first. */ bool called_zip_first; /** @brief The zipfile. */ archive_file::ptr zipfile; /** @brief The zipprefix. */ std::string zipprefix; /** @brief The returned dirlist. */ std::forward_list returned_dirlist; }; /*************************************************************************** FUNCTION PROTOTYPES ***************************************************************************/ static int zippath_find_sub_path(archive_file &zipfile, std::string const &subpath, osd::directory::entry::entry_type &type); static bool is_zip_file(std::string const &path); static bool is_zip_file_separator(char c); static bool is_7z_file(std::string const &path); /*************************************************************************** PATH OPERATIONS ***************************************************************************/ /** * @fn int is_path_separator(char c) * * @brief ============================================================ * is_path_separator * ============================================================. * * @param c The character. * * @return An int. */ int is_path_separator(char c) { return (c == '/') || (c == '\\'); } // ------------------------------------------------- // parse_parent_path - parses out the parent path // ------------------------------------------------- /** * @fn static void parse_parent_path(const std::string &path, std::string::size_type *beginpos, std::string::size_type *endpos) * * @brief Parse parent path. * * @param path Full pathname of the file. * @param [in,out] beginpos If non-null, the beginpos. * @param [in,out] endpos If non-null, the endpos. */ static void parse_parent_path(const std::string &path, std::string::size_type *beginpos, std::string::size_type *endpos) { std::string::size_type length = path.length(); std::string::size_type pos; // skip over trailing path separators pos = length ? (length - 1) : std::string::npos; while ((pos > 0) && (pos != std::string::npos) && is_path_separator(path[pos])) pos--; // return endpos if (endpos != nullptr) *endpos = pos; // now skip until we find a path separator while ((pos != std::string::npos) && !is_path_separator(path[pos])) pos = (pos > 0) ? pos - 1 : std::string::npos; // return beginpos if (beginpos != nullptr) *beginpos = pos; } // ------------------------------------------------- // zippath_parent - retrieves the parent directory // ------------------------------------------------- std::string &zippath_parent(std::string &dst, const std::string &path) { std::string::size_type pos; parse_parent_path(path, &pos, nullptr); if (pos != std::string::npos) dst = path.substr(0, pos + 1); else dst.clear(); return dst; } // ------------------------------------------------- // zippath_parent - retrieves the parent directory // ------------------------------------------------- std::string zippath_parent(const std::string &path) { std::string result; zippath_parent(result, path); return result; } // ------------------------------------------------- // zippath_parent_basename - retrieves the parent // directory basename // ------------------------------------------------- /** * @fn std::string &zippath_parent_basename(std::string &dst, const std::string &path) * * @brief Zippath parent basename. * * @param [in,out] dst Destination for the. * @param path Full pathname of the file. * * @return A std::string& */ std::string &zippath_parent_basename(std::string &dst, const std::string &path) { std::string::size_type beginpos, endpos; parse_parent_path(path, &beginpos, &endpos); dst.copy((char*)(path.c_str() + beginpos + 1), endpos - beginpos); return dst; } // ------------------------------------------------- // zippath_parent_basename - retrieves the parent // directory basename // ------------------------------------------------- std::string zippath_parent_basename(const std::string &path) { std::string result; zippath_parent_basename(result, path); return result; } // ------------------------------------------------- // zippath_combine - combines two paths // ------------------------------------------------- /** * @fn std::string &zippath_combine(std::string &dst, const char *path1, const char *path2) * * @brief Zippath combine. * * @param [in,out] dst Destination for the. * @param path1 The first path. * @param path2 The second path. * * @return A std::string& */ std::string &zippath_combine(std::string &dst, const std::string &path1, const std::string &path2) { if (path2 == ".") { dst.assign(path1); } else if (path2 == "..") { dst = zippath_parent(path1); } else if (osd_is_absolute_path(path2)) { dst.assign(path2); } else if (!path1.empty() && !is_path_separator(*path1.rbegin())) { dst.assign(path1).append(PATH_SEPARATOR).append(path2); } else { dst.assign(path1).append(path2); } return dst; } // ------------------------------------------------- // zippath_combine - combines two paths // ------------------------------------------------- std::string zippath_combine(const std::string &path1, const std::string &path2) { std::string result; zippath_combine(result, path1, path2); return result; } /*************************************************************************** FILE OPERATIONS ***************************************************************************/ // ------------------------------------------------- // file_error_from_zip_error - translates a // osd_file::error to a zip_error // ------------------------------------------------- /** * @fn static osd_file::error file_error_from_zip_error(archive_file::error ziperr) * * @brief File error from zip error. * * @param ziperr The ziperr. * * @return A osd_file::error. */ static osd_file::error file_error_from_zip_error(archive_file::error ziperr) { osd_file::error filerr; switch(ziperr) { case archive_file::error::NONE: filerr = osd_file::error::NONE; break; case archive_file::error::OUT_OF_MEMORY: filerr = osd_file::error::OUT_OF_MEMORY; break; case archive_file::error::BAD_SIGNATURE: case archive_file::error::DECOMPRESS_ERROR: case archive_file::error::FILE_TRUNCATED: case archive_file::error::FILE_CORRUPT: case archive_file::error::UNSUPPORTED: case archive_file::error::FILE_ERROR: filerr = osd_file::error::INVALID_DATA; break; case archive_file::error::BUFFER_TOO_SMALL: default: filerr = osd_file::error::FAILURE; break; } return filerr; } // ------------------------------------------------- // create_core_file_from_zip - creates a core_file // from a zip file entry // ------------------------------------------------- /** * @fn static osd_file::error create_core_file_from_zip(archive_file *zip, util::core_file::ptr &file) * * @brief Creates core file from zip. * * @param [in,out] zip If non-null, the zip. * @param header The header. * @param [in,out] file [in,out] If non-null, the file. * * @return The new core file from zip. */ static osd_file::error create_core_file_from_zip(archive_file &zip, util::core_file::ptr &file) { osd_file::error filerr; archive_file::error ziperr; void *ptr; ptr = malloc(zip.current_uncompressed_length()); if (ptr == nullptr) { filerr = osd_file::error::OUT_OF_MEMORY; goto done; } ziperr = zip.decompress(ptr, zip.current_uncompressed_length()); if (ziperr != archive_file::error::NONE) { filerr = file_error_from_zip_error(ziperr); goto done; } filerr = util::core_file::open_ram_copy(ptr, zip.current_uncompressed_length(), OPEN_FLAG_READ, file); if (filerr != osd_file::error::NONE) goto done; done: if (ptr != nullptr) free(ptr); return filerr; } // ------------------------------------------------- // zippath_fopen - opens a zip path file // ------------------------------------------------- /** * @fn osd_file::error zippath_fopen(const std::string &filename, uint32_t openflags, util::core_file::ptr &file, std::string &revised_path) * * @brief Zippath fopen. * * @param filename Filename of the file. * @param openflags The openflags. * @param [in,out] file [in,out] If non-null, the file. * @param [in,out] revised_path Full pathname of the revised file. * * @return A osd_file::error. */ osd_file::error zippath_fopen(const std::string &filename, uint32_t openflags, util::core_file::ptr &file, std::string &revised_path) { osd_file::error filerr = osd_file::error::NOT_FOUND; archive_file::error ziperr; archive_file::ptr zip; int header; osd::directory::entry::entry_type entry_type; int len; /* first, set up the two types of paths */ std::string mainpath(filename); std::string subpath; file = nullptr; /* loop through */ while((file == nullptr) && (mainpath.length() > 0) && ((openflags == OPEN_FLAG_READ) || (subpath.length() == 0))) { /* is the mainpath a ZIP path? */ if (is_zip_file(mainpath) || is_7z_file(mainpath)) { /* this file might be a zip file - lets take a look */ ziperr = is_zip_file(mainpath) ? archive_file::open_zip(mainpath, zip) : archive_file::open_7z(mainpath, zip); if (ziperr == archive_file::error::NONE) { /* it is a zip file - error if we're not opening for reading */ if (openflags != OPEN_FLAG_READ) { filerr = osd_file::error::ACCESS_DENIED; goto done; } if (subpath.length() > 0) header = zippath_find_sub_path(*zip, subpath, entry_type); else header = zip->first_file(); if (header < 0) { filerr = osd_file::error::NOT_FOUND; goto done; } /* attempt to read the file */ filerr = create_core_file_from_zip(*zip, file); if (filerr != osd_file::error::NONE) goto done; /* update subpath, if appropriate */ if (subpath.length() == 0) subpath.assign(zip->current_name()); /* we're done */ goto done; } } if (subpath.length() == 0) filerr = util::core_file::open(filename, openflags, file); else filerr = osd_file::error::NOT_FOUND; /* if we errored, then go up a directory */ if (filerr != osd_file::error::NONE) { /* go up a directory */ auto temp = zippath_parent(mainpath); /* append to the sub path */ if (subpath.length() > 0) { std::string temp2; mainpath = mainpath.substr(temp.length()); temp2.assign(mainpath).append(PATH_SEPARATOR).append(subpath); subpath.assign(temp2); } else { mainpath = mainpath.substr(temp.length()); subpath.assign(mainpath); } /* get the new main path, truncating path separators */ len = temp.length(); while (len > 0 && is_zip_file_separator(temp[len - 1])) len--; temp = temp.substr(0, len); mainpath.assign(temp); } } done: /* store the revised path */ revised_path.clear(); if (filerr == osd_file::error::NONE) { /* cannonicalize mainpath */ std::string alloc_fullpath; filerr = osd_get_full_path(alloc_fullpath, mainpath); if (filerr == osd_file::error::NONE) { revised_path = alloc_fullpath; if (subpath.length() > 0) revised_path.append(PATH_SEPARATOR).append(subpath); } } return filerr; } /*************************************************************************** DIRECTORY OPERATIONS ***************************************************************************/ // ------------------------------------------------- // is_root - tests to see if this path is the root // ------------------------------------------------- /** * @fn static int is_root(const char *path) * * @brief Is root. * * @param path Full pathname of the file. * * @return An int. */ static int is_root(const char *path) { int i = 0; /* skip drive letter */ if (isalpha(path[i]) && (path[i + 1] == ':')) i += 2; /* skip path separators */ while (is_path_separator(path[i])) i++; return path[i] == '\0'; } // ------------------------------------------------- // is_7z_file - tests to see if this file is a // 7-zip file // ------------------------------------------------- /** * @fn static int is_7z_file(const char *path) * * @brief Is 7z file. * * @param path Full pathname of the file. * * @return An int. */ static bool is_7z_file(std::string const &path) { auto const s = path.rfind('.'); return (std::string::npos != s) && !core_stricmp(path.c_str() + s, ".7z"); } // ------------------------------------------------- // is_zip_file - tests to see if this file is a // ZIP file // ------------------------------------------------- static bool is_zip_file(std::string const &path) { auto const s = path.rfind('.'); return (std::string::npos != s) && !core_stricmp(path.c_str() + s, ".zip"); } // ------------------------------------------------- // is_zip_file_separator - returns whether this // character is a path separator within a ZIP file // ------------------------------------------------- /** * @fn static int is_zip_file_separator(char c) * * @brief Is zip file separator. * * @param c The character. * * @return An int. */ static bool is_zip_file_separator(char c) { return (c == '/') || (c == '\\'); } // ------------------------------------------------- // is_zip_path_separator - returns whether this // character is a path separator within a ZIP path // ------------------------------------------------- /** * @fn static int is_zip_path_separator(char c) * * @brief Is zip path separator. * * @param c The character. * * @return An int. */ static bool is_zip_path_separator(char c) { return is_zip_file_separator(c) || is_path_separator(c); } // ------------------------------------------------- // next_path_char - lexes out the next path // character, normalizing separators as '/' // ------------------------------------------------- /** * @fn static char next_path_char(const char *s, int *pos) * * @brief Next path character. * * @param s The const char * to process. * @param [in,out] pos If non-null, the position. * * @return A char. */ static char next_path_char(std::string const &s, std::string::size_type &pos) { // skip over any initial separators if (pos == 0) { while ((pos < s.length()) && is_zip_file_separator(s[pos])) pos++; } // are we at a path separator? if (pos == s.length()) { // return NUL return '\0'; } else if (is_zip_file_separator(s[pos])) { // skip over path separators while((pos < s.length()) && is_zip_file_separator(s[pos])) pos++; // normalize as '/' return '/'; } else { // return character return std::tolower(s[pos++]); } } // ------------------------------------------------- // zippath_find_sub_path - attempts to identify the // type of a sub path in a zip file // ------------------------------------------------- /** * @fn static const zip_file_header *zippath_find_sub_path(archive_file *zipfile, const char *subpath, osd::directory::entry::entry_type *type) * * @brief Zippath find sub path. * * @param [in,out] zipfile If non-null, the zipfile. * @param subpath The subpath. * @param [in,out] type If non-null, the type. * * @return null if it fails, else a zip_file_header*. */ static int zippath_find_sub_path(archive_file &zipfile, std::string const &subpath, osd::directory::entry::entry_type &type) { for (int header = zipfile.first_file(); header >= 0; header = zipfile.next_file()) { std::string::size_type i = 0, j = 0; char c1, c2; do { c1 = next_path_char(zipfile.current_name(), i); c2 = next_path_char(subpath, j); } while ((c1 == c2) && c1 && c2); if (!c2 || ((c2 == '/') && !(c2 = next_path_char(subpath, j)))) { if (!c1) { type = zipfile.current_is_directory() ? osd::directory::entry::entry_type::DIR : osd::directory::entry::entry_type::FILE; return header; } else if ((c1 == '/') || (i <= 1U)) { type = osd::directory::entry::entry_type::DIR; return header; } } } type = osd::directory::entry::entry_type::NONE; return -1; } // ------------------------------------------------- // zippath_resolve - separates a ZIP path out into // true path and ZIP entry components // ------------------------------------------------- /** * @fn static osd_file::error zippath_resolve(const char *path, osd::directory::entry::entry_type &entry_type, archive_file *&zipfile, std::string &newpath) * * @brief Zippath resolve. * * @param path Full pathname of the file. * @param [in,out] entry_type Type of the entry. * @param [in,out] zipfile [in,out] If non-null, the zipfile. * @param [in,out] newpath The newpath. * * @return A osd_file::error. */ static osd_file::error zippath_resolve(const char *path, osd::directory::entry::entry_type &entry_type, archive_file::ptr &zipfile, std::string &newpath) { newpath.clear(); // be conservative entry_type = osd::directory::entry::entry_type::NONE; zipfile.reset(); std::string apath(path); std::string apath_trimmed; osd::directory::entry::entry_type current_entry_type; bool went_up = false; do { // trim the path of trailing path separators auto i = apath.length(); while ((i > 1) && is_path_separator(apath[i - 1])) i--; apath.resize(i); apath_trimmed = apath; // stat the path auto current_entry = osd_stat(apath_trimmed); // did we find anything? if (current_entry) { // get the entry type and free the stat entry current_entry_type = current_entry->type; } else { // if we have not found the file or directory, go up current_entry_type = osd::directory::entry::entry_type::NONE; went_up = true; apath = zippath_parent(apath); } } while ((current_entry_type == osd::directory::entry::entry_type::NONE) && !is_root(apath.c_str())); // if we did not find anything, then error out if (current_entry_type == osd::directory::entry::entry_type::NONE) return osd_file::error::NOT_FOUND; // is this file a ZIP file? if ((current_entry_type == osd::directory::entry::entry_type::FILE) && ((is_zip_file(apath_trimmed) && (archive_file::open_zip(apath_trimmed, zipfile) == archive_file::error::NONE)) || (is_7z_file(apath_trimmed) && (archive_file::open_7z(apath_trimmed, zipfile) == archive_file::error::NONE)))) { auto i = strlen(path + apath.length()); while ((i > 0) && is_zip_path_separator(path[apath.length() + i - 1])) i--; newpath.assign(path + apath.length(), i); // this was a true ZIP path - attempt to identify the type of path zippath_find_sub_path(*zipfile, newpath, current_entry_type); if (current_entry_type == osd::directory::entry::entry_type::NONE) return osd_file::error::NOT_FOUND; } else { // this was a normal path if (went_up) return osd_file::error::NOT_FOUND; newpath = path; } // success! entry_type = current_entry_type; return osd_file::error::NONE; } // ------------------------------------------------- // zippath_opendir - opens a directory // ------------------------------------------------- /** * @fn osd_file::error zippath_opendir(const std::string &path, zippath_directory **directory) * * @brief Zippath opendir. * * @param path Full pathname of the file. * @param [in,out] directory If non-null, pathname of the directory. * * @return A osd_file::error. */ osd_file::error zippath_opendir(const std::string &path, zippath_directory **directory) { osd_file::error err; /* allocate a directory */ zippath_directory *result = nullptr; try { result = new zippath_directory; } catch (std::bad_alloc &) { err = osd_file::error::OUT_OF_MEMORY; goto done; } /* resolve the path */ osd::directory::entry::entry_type entry_type; err = zippath_resolve(path.c_str(), entry_type, result->zipfile, result->zipprefix); if (err != osd_file::error::NONE) goto done; /* we have to be a directory */ if (entry_type != osd::directory::entry::entry_type::DIR) { err = osd_file::error::NOT_FOUND; goto done; } /* was the result a ZIP? */ if (!result->zipfile) { /* a conventional directory */ result->directory = osd::directory::open(path); if (!result->directory) { err = osd_file::error::FAILURE; goto done; } /* is this path the root? if so, pretend we've already returned the parent */ if (is_root(path.c_str())) result->returned_parent = true; } done: if ((directory == nullptr || err != osd_file::error::NONE) && result != nullptr) { zippath_closedir(result); result = nullptr; } if (directory != nullptr) *directory = result; return err; } // ------------------------------------------------- // zippath_closedir - closes a directory // ------------------------------------------------- /** * @fn void zippath_closedir(zippath_directory *directory) * * @brief Zippath closedir. * * @param [in,out] directory If non-null, pathname of the directory. */ void zippath_closedir(zippath_directory *directory) { if (directory->directory != nullptr) directory->directory.reset(); if (directory->zipfile != nullptr) directory->zipfile.reset(); directory->returned_dirlist.clear(); delete directory; } // ------------------------------------------------- // get_relative_path - checks to see if a specified // header is in the zippath_directory, and if so // returns the relative path // ------------------------------------------------- /** * @fn static const char *get_relative_path(zippath_directory *directory, const zip_file_header *header) * * @brief Gets relative path. * * @param [in,out] directory If non-null, pathname of the directory. * @param header The header. * * @return null if it fails, else the relative path. */ static const char *get_relative_path(zippath_directory const &directory) { auto len = directory.zipprefix.length(); char const *prefix = directory.zipprefix.c_str(); while (is_zip_file_separator(*prefix)) { len--; prefix++; } std::string const ¤t(directory.zipfile->current_name()); char const *result = directory.zipfile->current_name().c_str() + len; if ((current.length() >= len) && !strncmp(prefix, current.c_str(), len) && (!*prefix || is_zip_file_separator(*result) || is_zip_file_separator(directory.zipprefix.back()))) { while (is_zip_file_separator(*result)) result++; return *result ? result : nullptr; } else { return nullptr; } } // ------------------------------------------------- // zippath_readdir - reads a directory // ------------------------------------------------- /** * @fn const osd::directory::entry *zippath_readdir(zippath_directory *directory) * * @brief Zippath readdir. * * @param [in,out] directory If non-null, pathname of the directory. * * @return null if it fails, else an osd::directory::entry*. */ const osd::directory::entry *zippath_readdir(zippath_directory *directory) { const osd::directory::entry *result = nullptr; if (!directory->returned_parent) { /* first thing's first - return parent directory */ directory->returned_parent = true; directory->returned_entry.name = ".."; directory->returned_entry.type = osd::directory::entry::entry_type::DIR; directory->returned_entry.size = 0; // FIXME: what would stat say? // FIXME: modified time? result = &directory->returned_entry; } else if (directory->directory) { /* a normal directory read */ do { result = directory->directory->read(); } while (result && (!strcmp(result->name, ".") || !strcmp(result->name, ".."))); /* special case - is this entry a ZIP file? if so we need to return it as a "directory" */ if (result && (is_zip_file(result->name) || is_7z_file(result->name))) { /* copy; but change the entry type */ directory->returned_entry = *result; directory->returned_entry.type = osd::directory::entry::entry_type::DIR; directory->returned_entry.size = 0; // FIXME: what would stat say? // FIXME: modified time? result = &directory->returned_entry; } } else if (directory->zipfile) { char const *relpath; do { /* a zip file read */ int header; do { header = directory->called_zip_first ? directory->zipfile->next_file() : directory->zipfile->first_file(); directory->called_zip_first = true; relpath = nullptr; } while ((header >= 0) && ((relpath = get_relative_path(*directory)) == nullptr)); if (relpath) { /* we've found a ZIP entry; but this may be an entry deep within the target directory */ char const *separator = relpath; while (*separator && !is_zip_file_separator(*separator)) separator++; if (*separator || directory->zipfile->current_is_directory()) { /* a nested entry; loop through returned_dirlist to see if we've returned the parent directory */ auto const len(separator - relpath); auto rdent = directory->returned_dirlist.begin(); while (directory->returned_dirlist.end() != rdent) { if ((rdent->length() == len) && !core_strnicmp(rdent->c_str(), relpath, len)) break; else ++rdent; } if (directory->returned_dirlist.end() == rdent) { /* we've found a new directory; add this to returned_dirlist */ directory->returned_dirlist.emplace_front(relpath, separator - relpath); /* ...and return it */ directory->returned_entry.name = directory->returned_dirlist.front().c_str(); directory->returned_entry.type = osd::directory::entry::entry_type::DIR; directory->returned_entry.size = 0; // FIXME: what would stat say? // FIXME: modified time? result = &directory->returned_entry; } } else { /* a real file */ directory->returned_entry.name = relpath; directory->returned_entry.type = osd::directory::entry::entry_type::FILE; directory->returned_entry.size = directory->zipfile->current_uncompressed_length(); directory->returned_entry.last_modified = directory->zipfile->current_last_modified(); result = &directory->returned_entry; } } } while (relpath && !result); } return result; } // ------------------------------------------------- // zippath_is_zip - returns true if this path is // a ZIP path or false if not // ------------------------------------------------- /** * @fn int zippath_is_zip(zippath_directory *directory) * * @brief Zippath is zip. * * @param [in,out] directory If non-null, pathname of the directory. * * @return An int. */ bool zippath_is_zip(zippath_directory *directory) { return directory->zipfile != nullptr; } } // namespace util