Browse Source

Add PHYSFS_Io StripSuffixIo to skip codesign signatures

When a fused löve executable is signed with codesign on Windows some
data is appended to the executable, preventing physfs from finding the
zip file at the end of the file.
To help physfs out a bit, we try to find the zip ourselves in the case
of regular mount failure by looking for it's characteristic end of
central directory record and, once found, pretend to physfs that the
file is shorter than it actually is and ends at the end of the zip file.
Joel Schumacher 2 years ago
parent
commit
efc54d5124

+ 2 - 0
CMakeLists.txt

@@ -477,6 +477,8 @@ set(LOVE_SRC_MODULE_FILESYSTEM_PHYSFS
 	src/modules/filesystem/physfs/File.h
 	src/modules/filesystem/physfs/File.h
 	src/modules/filesystem/physfs/Filesystem.cpp
 	src/modules/filesystem/physfs/Filesystem.cpp
 	src/modules/filesystem/physfs/Filesystem.h
 	src/modules/filesystem/physfs/Filesystem.h
+	src/modules/filesystem/physfs/PhysfsIo.h
+	src/modules/filesystem/physfs/PhysfsIo.cpp
 )
 )
 
 
 set(LOVE_SRC_MODULE_FILESYSTEM
 set(LOVE_SRC_MODULE_FILESYSTEM

+ 25 - 4
src/modules/filesystem/physfs/Filesystem.cpp

@@ -27,6 +27,7 @@
 
 
 #include "Filesystem.h"
 #include "Filesystem.h"
 #include "File.h"
 #include "File.h"
+#include "PhysfsIo.h"
 
 
 // PhysFS
 // PhysFS
 #include "libraries/physfs/physfs.h"
 #include "libraries/physfs/physfs.h"
@@ -145,7 +146,7 @@ bool Filesystem::isFused() const
 	return fused;
 	return fused;
 }
 }
 
 
-bool Filesystem::setIdentity(const char *ident, bool appendToPath) 
+bool Filesystem::setIdentity(const char *ident, bool appendToPath)
 {
 {
 	if (!PHYSFS_isInit())
 	if (!PHYSFS_isInit())
 		return false;
 		return false;
@@ -165,8 +166,8 @@ bool Filesystem::setIdentity(const char *ident, bool appendToPath)
 	else
 	else
 		save_path_full += save_path_relative;
 		save_path_full += save_path_relative;
 
 
-	save_path_full = normalize(save_path_full);	
-	
+	save_path_full = normalize(save_path_full);
+
 #ifdef LOVE_ANDROID
 #ifdef LOVE_ANDROID
 	if (save_identity == "")
 	if (save_identity == "")
 		save_identity = "unnamed";
 		save_identity = "unnamed";
@@ -285,7 +286,27 @@ bool Filesystem::setSource(const char *source)
 #else
 #else
 	// Add the directory.
 	// Add the directory.
 	if (!PHYSFS_mount(new_search_path.c_str(), nullptr, 1))
 	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
 #endif
 
 
 	// Save the game source.
 	// Save the game source.

+ 203 - 0
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 <cassert>
+#include <algorithm>
+
+#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

+ 160 - 0
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 <cstdint>
+#include <cstdio>
+#include <string>
+
+#include "libraries/physfs/physfs.h"
+
+namespace love
+{
+namespace filesystem
+{
+namespace physfs
+{
+
+template <typename Derived>
+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<Derived*>(reinterpret_cast<PhysfsIo*>(io->opaque));
+	}
+};
+
+struct StripSuffixIo : public PhysfsIo<StripSuffixIo> {
+	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