diff --git a/CMakeLists.txt b/CMakeLists.txt index c89eb61df..ae1bf6bdd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -477,6 +477,8 @@ set(LOVE_SRC_MODULE_FILESYSTEM_PHYSFS src/modules/filesystem/physfs/File.h src/modules/filesystem/physfs/Filesystem.cpp src/modules/filesystem/physfs/Filesystem.h + src/modules/filesystem/physfs/PhysfsIo.h + src/modules/filesystem/physfs/PhysfsIo.cpp ) set(LOVE_SRC_MODULE_FILESYSTEM diff --git a/src/modules/filesystem/physfs/Filesystem.cpp b/src/modules/filesystem/physfs/Filesystem.cpp index e5d7d93d3..09edabdd0 100644 --- a/src/modules/filesystem/physfs/Filesystem.cpp +++ b/src/modules/filesystem/physfs/Filesystem.cpp @@ -27,6 +27,7 @@ #include "Filesystem.h" #include "File.h" +#include "PhysfsIo.h" // PhysFS #include "libraries/physfs/physfs.h" @@ -152,7 +153,7 @@ bool Filesystem::isFused() const return fused; } -bool Filesystem::setIdentity(const char *ident, bool appendToPath) +bool Filesystem::setIdentity(const char *ident, bool appendToPath) { if (!PHYSFS_isInit()) return false; @@ -172,8 +173,8 @@ bool Filesystem::setIdentity(const char *ident, bool appendToPath) else save_path_full += save_path_relative; - save_path_full = normalize(save_path_full); - + save_path_full = normalize(save_path_full); + #ifdef LOVE_ANDROID if (save_identity == "") save_identity = "unnamed"; @@ -292,7 +293,27 @@ bool Filesystem::setSource(const char *source) #else // Add the directory. if (!PHYSFS_mount(new_search_path.c_str(), nullptr, 1)) - return false; + { + // It's possible there is additional data at the end of the fused executable, + // e.g. for signed windows executables (the signature). + // In this case let's try a little bit harder to find the zip file. + // This is not used by default because I assume that the physfs IOs are probably + // more robust and more performant, so they should be favored, if possible. + auto io = StripSuffixIo::create(new_search_path); + if (!io->determineStrippedLength()) + { + delete io; + return false; + } + if (!PHYSFS_mountIo(io, io->filename.c_str(), nullptr, 1)) + { + // If PHYSFS_mountIo fails, io->destroy(io) is not called and we have + // to delete ourselves. + delete io; + return false; + } + return true; + } #endif // Save the game source. diff --git a/src/modules/filesystem/physfs/PhysfsIo.cpp b/src/modules/filesystem/physfs/PhysfsIo.cpp new file mode 100644 index 000000000..cdcea8dd2 --- /dev/null +++ b/src/modules/filesystem/physfs/PhysfsIo.cpp @@ -0,0 +1,203 @@ +/** + * Copyright (c) 2006-2023 LOVE Development Team + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + **/ + +#include +#include + +#include "PhysfsIo.h" + +namespace love +{ +namespace filesystem +{ +namespace physfs +{ + +bool StripSuffixIo::determineStrippedLength() +{ + if (!file) { + return false; + } + const int64_t fullSize = fullLength(); + int64_t chunkSize = std::min(fullSize, (int64_t)8192); + std::string buffer; + buffer.reserve(chunkSize); + int64_t i = fullSize - chunkSize; + // I don't think we really need to go through the whole file. The main known use + // case for this functionality is to skip windows codesign signatures, which are + // from what I have seen ~12KB or so, but trying is better than just failing. + while (i >= 0) + { + buffer.resize(chunkSize); + if (seek(i) == 0) + return false; + const auto n = read(&buffer[0], chunkSize); + if (n <= 0) + return false; + buffer.resize(n); + // We are looking for the magic bytes that indicate the start + // of the "End of cental directory record (EOCD)". + // As this is most likely not a multi-disk zip, we could include 4 bytes of 0x00, + // but I'd rather make fewer assumptions. + const auto endOfCentralDirectory = buffer.rfind("\x50\x4B\x05\x06"); + if (endOfCentralDirectory != std::string::npos) + { + i = i + endOfCentralDirectory; + break; + } + if (i == 0) + break; + i = std::max((int64_t)0, i - chunkSize); + } + + if (i > 0) + { + // The EOCD record is at least 22 bytes but may include a comment + if (i + 22 > fullSize) + return false; // Incomplete central directory + // The comment length (u16) is located 20 bytes from the start of the EOCD record + if (seek(i + 20) == 0) + return false; + uint8_t buffer[2]; + const auto n = read(buffer, 2); + if (n <= 0) + return false; + const auto commentSize = (buffer[1] << 8) | buffer[0]; + if (i + 22 + commentSize > fullSize) // Comment incomplete + return false; + // We pretend the file ends just after the comment + // (which should be the end of the embedded zip file) + strippedLength_ = i + 22 + commentSize; + } + else + { + strippedLength_ = fullSize; + } + + if (seek(0) == 0) + return false; + return true; +} + +int64_t StripSuffixIo::read(void* buf, uint64_t len) +{ + if (!file) + { + PHYSFS_setErrorCode(PHYSFS_ERR_OS_ERROR); + return -1; + } + const auto ret = std::fread(buf, 1, len, file); + if (ret == 0) + { + if (std::feof(file)) + { + PHYSFS_setErrorCode(PHYSFS_ERR_OK); + return 0; + } + else + { + PHYSFS_setErrorCode(PHYSFS_ERR_OS_ERROR); + return -1; + } + } + else if (ret < len && std::ferror(file)) + { + PHYSFS_setErrorCode(PHYSFS_ERR_OS_ERROR); + return -1; + } + PHYSFS_setErrorCode(PHYSFS_ERR_OK); + return ret; +} + +int64_t StripSuffixIo::write(const void* /*buf*/, uint64_t /*len*/) +{ + PHYSFS_setErrorCode(PHYSFS_ERR_READ_ONLY); + return -1; +} + +int64_t StripSuffixIo::seek(uint64_t offset) +{ + if (!file) + { + PHYSFS_setErrorCode(PHYSFS_ERR_OS_ERROR); + return 0; + } + const auto ret = std::fseek(file, offset, SEEK_SET); + PHYSFS_setErrorCode(ret != 0 ? PHYSFS_ERR_OS_ERROR : PHYSFS_ERR_OK); + return ret == 0 ? 1 : 0; +} + +int64_t StripSuffixIo::tell() +{ + if (!file) + { + PHYSFS_setErrorCode(PHYSFS_ERR_OS_ERROR); + return -1; + } + return std::ftell(file); +} + +int64_t StripSuffixIo::length() +{ + return strippedLength_; +} + +int64_t StripSuffixIo::flush() +{ + if (!file) + { + PHYSFS_setErrorCode(PHYSFS_ERR_OS_ERROR); + return 0; + } + return std::fflush(file) == 0 ? 1 : 0; +} + +int64_t StripSuffixIo::fullLength() +{ + assert(file); + const auto cur = std::ftell(file); + if (cur == -1) + { + PHYSFS_setErrorCode(PHYSFS_ERR_OS_ERROR); + return -1; + } + if (std::fseek(file, 0, SEEK_END) != 0) + { + PHYSFS_setErrorCode(PHYSFS_ERR_OS_ERROR); + return -1; + } + const auto len = std::ftell(file); + if (len == -1) + { + PHYSFS_setErrorCode(PHYSFS_ERR_OS_ERROR); + return -1; + } + if (std::fseek(file, cur, SEEK_SET) != 0) + { + // We do have the length now, but something is wrong, so we return an error anyways + PHYSFS_setErrorCode(PHYSFS_ERR_OS_ERROR); + return -1; + } + return len; +} + +} // physfs +} // filesystem +} // love diff --git a/src/modules/filesystem/physfs/PhysfsIo.h b/src/modules/filesystem/physfs/PhysfsIo.h new file mode 100644 index 000000000..ee2454d0d --- /dev/null +++ b/src/modules/filesystem/physfs/PhysfsIo.h @@ -0,0 +1,160 @@ +/** + * Copyright (c) 2006-2023 LOVE Development Team + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + **/ + +#ifndef LOVE_FILESYSTEM_PHYSFS_PHYSFSIO_H +#define LOVE_FILESYSTEM_PHYSFS_PHYSFSIO_H + +#include +#include +#include + +#include "libraries/physfs/physfs.h" + +namespace love +{ +namespace filesystem +{ +namespace physfs +{ + +template +struct PhysfsIo : PHYSFS_Io { +protected: + PhysfsIo() + : PHYSFS_Io + { + /*.version =*/ Derived::version, + /*.opaque =*/ this, + /*.read =*/ staticRead, // may be NULL + /*.write =*/ staticWrite, // may be NULL + /*.seek =*/ staticSeek, + /*.tell =*/ staticTell, + /*.length =*/ staticLength, + /*.duplicate =*/ staticDuplicate, + /*.flush =*/ staticFlush, // may be NULL + /*.destroy =*/ staticDestroy, + } + { + } + +private: + // Returns: number of bytes read, 0 on EOF, -1 on failure + static PHYSFS_sint64 staticRead(struct PHYSFS_Io* io, void* buf, PHYSFS_uint64 len) + { + return derived(io)->read(buf, len); + } + + // Returns: number of bytes written, -1 on failure + static PHYSFS_sint64 staticWrite(struct PHYSFS_Io* io, const void* buf, PHYSFS_uint64 len) + { + return derived(io)->write(buf, len); + } + + // Returns: non-zero on success, zero on error + static int staticSeek(struct PHYSFS_Io* io, PHYSFS_uint64 offset) + { + return derived(io)->seek(offset); + } + + // Returns: current offset from start, -1 on error + static PHYSFS_sint64 staticTell(struct PHYSFS_Io* io) + { + return derived(io)->tell(); + } + + // Returns: total size in bytes, -1 on error + static PHYSFS_sint64 staticLength(struct PHYSFS_Io* io) + { + return derived(io)->length(); + } + + static struct PHYSFS_Io* staticDuplicate(struct PHYSFS_Io* io) + { + // Just use copy constructor + return new Derived(*derived(io)); + } + + // Returns: non-zero on success, zero on error + static int staticFlush(struct PHYSFS_Io* io) + { + return derived(io)->flush(); + } + + static void staticDestroy(struct PHYSFS_Io* io) + { + // Just use destructor + delete derived(io); + } + + static Derived* derived(PHYSFS_Io* io) + { + return static_cast(reinterpret_cast(io->opaque)); + } +}; + +struct StripSuffixIo : public PhysfsIo { + static const uint32_t version = 0; + + std::string filename; + FILE* file = nullptr; + + // The constructor is private in favor of this function to prevent stack allocation + // because Physfs will take ownership of this object and call destroy on it later. + static StripSuffixIo* create(std::string f) { return new StripSuffixIo(f); } + + ~StripSuffixIo() + { + if (file) + { + std::fclose(file); + } + } + + StripSuffixIo(const StripSuffixIo& other) + : StripSuffixIo(other.filename) + { + } + + bool determineStrippedLength(); + + int64_t read(void* buf, uint64_t len); + int64_t write(const void* buf, uint64_t len); + int64_t seek(uint64_t offset); + int64_t tell(); + int64_t length(); + int64_t flush(); + +private: + StripSuffixIo(std::string f) + : filename(std::move(f)) + , file(std::fopen(filename.c_str(), "rb")) + { + } + + int64_t fullLength(); + + int64_t strippedLength_ = -1; +}; + +} // physfs +} // filesystem +} // love + +#endif