Browse Source

Merge pull request #1930 from pfirsich/physfs_skip_codesign

Add PHYSFS_Io StripSuffixIo to skip codesign signatures
Sasha Szpakowski 2 years ago
parent
commit
6cc188b8ba

+ 2 - 0
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

+ 25 - 4
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.

+ 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