forked from LumaTeam/Luma3DS
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
IPS is way too limited as a patch format. It doesn't support delta patches, which would be useful to avoid distributing copyrighted content when releasing mods for which it is impractical to support three different game executables (especially when there are only tiny differences in them).
- Loading branch information
Showing
5 changed files
with
364 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
#include "bps_patcher.h" | ||
|
||
#include <array> | ||
#include <string_view> | ||
|
||
extern "C" | ||
{ | ||
#include <3ds/os.h> | ||
#include <3ds/result.h> | ||
#include <3ds/services/fs.h> | ||
#include <3ds/svc.h> | ||
|
||
#include "patcher.h" | ||
#include "strings.h" | ||
} | ||
|
||
#include "file_util.h" | ||
|
||
namespace patcher | ||
{ | ||
[[gnu::noinline]] static u32 crc32(const u8 *data, size_t size) | ||
{ | ||
u32 crc = 0xFFFFFFFF; | ||
for (size_t i = 0; i < size; ++i) | ||
{ | ||
crc ^= data[i]; | ||
for (size_t j = 0; j < 8; ++j) | ||
{ | ||
u32 mask = -(crc & 1); | ||
crc = (crc >> 1) ^ (0xEDB88320 & mask); | ||
} | ||
} | ||
return ~crc; | ||
} | ||
|
||
class ScopedAppHeap | ||
{ | ||
public: | ||
ScopedAppHeap() | ||
{ | ||
u32 tmp; | ||
m_size = osGetMemRegionFree(MEMREGION_APPLICATION); | ||
if (!R_SUCCEEDED(svcControlMemory(&tmp, BaseAddress, 0, m_size, | ||
MemOp(MEMOP_ALLOC | MEMOP_REGION_APP), | ||
MemPerm(MEMPERM_READ | MEMPERM_WRITE)))) | ||
{ | ||
svcBreak(USERBREAK_PANIC); | ||
} | ||
} | ||
|
||
~ScopedAppHeap() | ||
{ | ||
u32 tmp; | ||
svcControlMemory(&tmp, BaseAddress, 0, m_size, MEMOP_FREE, MemPerm(0)); | ||
} | ||
|
||
static constexpr u32 BaseAddress = 0x08000000; | ||
|
||
private: | ||
u32 m_size; | ||
}; | ||
|
||
namespace bps | ||
{ | ||
using Number = u32; | ||
|
||
[[gnu::noinline]] static Number Decode(util::MemoryStream &stream) | ||
{ | ||
Number data = 0, shift = 1; | ||
while (true) | ||
{ | ||
const u8 x = stream.Read<u8>(); | ||
data += (x & 0x7f) * shift; | ||
if (x & 0x80) | ||
break; | ||
shift <<= 7; | ||
data += shift; | ||
} | ||
return data; | ||
} | ||
|
||
class PatchApplier | ||
{ | ||
public: | ||
// patch should point at the start of the commands. | ||
PatchApplier(util::MemoryStream source, util::MemoryStream target, util::MemoryStream patch) | ||
: m_source{source}, m_target{target}, m_patch{patch} | ||
{ | ||
} | ||
|
||
bool Apply() | ||
{ | ||
const u32 command_start_offset = m_patch.Tell(); | ||
const u32 command_end_offset = m_patch.size() - 12; | ||
m_patch.Seek(command_end_offset); | ||
const u32 source_crc32 = m_patch.Read<u32>(); | ||
const u32 target_crc32 = m_patch.Read<u32>(); | ||
m_patch.Seek(command_start_offset); | ||
|
||
// Ensure we are patching the right executable. | ||
if (crc32(m_source.data(), m_source.size()) != source_crc32) | ||
{ | ||
svcBreak(USERBREAK_USER); | ||
return false; | ||
} | ||
|
||
// Process all patch commands. | ||
while (m_patch.Tell() < command_end_offset) | ||
{ | ||
const bool ok = HandleCommand(); | ||
if (!ok) | ||
{ | ||
svcBreak(USERBREAK_PANIC); | ||
return false; | ||
} | ||
} | ||
|
||
// Verify that the executable was patched correctly. | ||
if (crc32(m_target.data(), m_target.size()) != target_crc32) | ||
{ | ||
svcBreak(USERBREAK_PANIC); | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
private: | ||
bool HandleCommand() | ||
{ | ||
const Number data = Decode(m_patch); | ||
const u32 command = data & 3; | ||
const u32 length = (data >> 2) + 1; | ||
switch (command) | ||
{ | ||
case 0: | ||
return SourceRead(length); | ||
case 1: | ||
return TargetRead(length); | ||
case 2: | ||
return SourceCopy(length); | ||
case 3: | ||
return TargetCopy(length); | ||
default: | ||
return false; | ||
} | ||
} | ||
|
||
bool SourceRead(Number length) | ||
{ | ||
memcpy(&m_target[m_output_offset], &m_source[m_output_offset], length); | ||
m_output_offset += length; | ||
return true; | ||
} | ||
|
||
bool TargetRead(Number length) | ||
{ | ||
m_patch.Read(&m_target[m_output_offset], length); | ||
m_output_offset += length; | ||
return true; | ||
} | ||
|
||
bool SourceCopy(Number length) | ||
{ | ||
const Number data = Decode(m_patch); | ||
m_source_relative_offset += (data & 1 ? -1 : +1) * (data >> 1); | ||
memcpy(&m_target[m_output_offset], &m_source[m_source_relative_offset], length); | ||
m_output_offset += length; | ||
m_source_relative_offset += length; | ||
return true; | ||
} | ||
|
||
bool TargetCopy(Number length) | ||
{ | ||
const Number data = Decode(m_patch); | ||
m_target_relative_offset += (data & 1 ? -1 : +1) * (data >> 1); | ||
while (length--) | ||
m_target[m_output_offset++] = m_target[m_target_relative_offset++]; | ||
return true; | ||
} | ||
|
||
u32 m_source_relative_offset = 0; | ||
u32 m_target_relative_offset = 0; | ||
u32 m_output_offset = 0; | ||
util::MemoryStream m_source, m_target, m_patch; | ||
}; | ||
|
||
} // namespace bps | ||
|
||
static inline bool ApplyCodeBpsPatch(u64 prog_id, u8 *code, u32 size) | ||
{ | ||
char bps_path[] = "/luma/titles/0000000000000000/code.bps"; | ||
progIdToStr(bps_path + 28, prog_id); | ||
util::File patch_file; | ||
if (!patch_file.Open(bps_path, FS_OPEN_READ)) | ||
return true; | ||
|
||
ScopedAppHeap memory; | ||
|
||
const u64 patch_size = patch_file.GetSize().value_or(0); | ||
util::MemoryStream patch{(u8 *)memory.BaseAddress, static_cast<u32>(patch_size)}; | ||
if (!patch_file.Read(patch.data(), patch.size(), 0)) | ||
return false; | ||
|
||
const auto magic = patch.Read<std::array<char, 4>>(); | ||
if (std::string_view(magic.data(), magic.size()) != "BPS1") | ||
return false; | ||
|
||
const bps::Number source_size = bps::Decode(patch); | ||
const bps::Number target_size = bps::Decode(patch); | ||
const bps::Number metadata_size = bps::Decode(patch); | ||
if (std::max(source_size, target_size) > size || metadata_size != 0) | ||
return false; | ||
|
||
util::MemoryStream source{patch.end(), source_size}; | ||
// Patch in-place. source and patch will be deallocated when patching is complete. | ||
memcpy(source.data(), code, source.size()); | ||
memset(code, 0, size); | ||
util::MemoryStream target{code, target_size}; | ||
|
||
bps::PatchApplier applier{source, target, patch}; | ||
return applier.Apply(); | ||
} | ||
|
||
} // namespace patcher | ||
|
||
extern "C" | ||
{ | ||
bool patcherApplyCodeBpsPatch(u64 progId, u8 *code, u32 size) | ||
{ | ||
return patcher::ApplyCodeBpsPatch(progId, code, size); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
#pragma once | ||
|
||
#ifdef __cplusplus | ||
extern "C" { | ||
#endif | ||
#include <3ds/types.h> | ||
|
||
bool patcherApplyCodeBpsPatch(u64 progId, u8* code, u32 size); | ||
|
||
#ifdef __cplusplus | ||
} | ||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
#pragma once | ||
|
||
#include <optional> | ||
#include <string.h> | ||
#include <type_traits> | ||
|
||
extern "C" | ||
{ | ||
#include <3ds/result.h> | ||
#include <3ds/services/fs.h> | ||
#include <3ds/svc.h> | ||
#include <3ds/types.h> | ||
} | ||
|
||
namespace util | ||
{ | ||
inline FS_Path MakePath(const char *path) | ||
{ | ||
return {PATH_ASCII, strnlen(path, 255) + 1, path}; | ||
} | ||
|
||
// A small wrapper to make forgetting to close a file and | ||
// to check read lengths impossible. | ||
class File | ||
{ | ||
public: | ||
File() = default; | ||
File(const File &other) = delete; | ||
File &operator=(const File &) = delete; | ||
File(File &&other) { *this = std::move(other); } | ||
File &operator=(File &&other) | ||
{ | ||
std::swap(m_handle, other.m_handle); | ||
return *this; | ||
} | ||
|
||
~File() { Close(); } | ||
|
||
bool Close() | ||
{ | ||
const bool ok = !m_handle || R_SUCCEEDED(FSFILE_Close(*m_handle)); | ||
if (ok) | ||
m_handle = std::nullopt; | ||
return ok; | ||
} | ||
|
||
bool Open(const char *path, int open_flags) | ||
{ | ||
const FS_Path archive_path = {PATH_EMPTY, 1, ""}; | ||
Handle handle; | ||
const bool ok = R_SUCCEEDED(FSUSER_OpenFileDirectly(&handle, ARCHIVE_SDMC, archive_path, | ||
MakePath(path), open_flags, 0)); | ||
if (ok) | ||
m_handle = handle; | ||
return ok; | ||
} | ||
|
||
bool Read(void *buffer, u32 size, u64 offset) | ||
{ | ||
u32 bytes_read = 0; | ||
const Result res = FSFILE_Read(*m_handle, &bytes_read, offset, buffer, size); | ||
return R_SUCCEEDED(res) && bytes_read == size; | ||
} | ||
|
||
std::optional<u64> GetSize() const | ||
{ | ||
u64 size; | ||
if (!R_SUCCEEDED(FSFILE_GetSize(*m_handle, &size))) | ||
return std::nullopt; | ||
return size; | ||
} | ||
|
||
private: | ||
std::optional<Handle> m_handle; | ||
}; | ||
|
||
// A small utility class that provides file-like reading for an in-memory buffer. | ||
class MemoryStream | ||
{ | ||
public: | ||
MemoryStream(u8 *ptr, u32 size) : m_ptr{ptr}, m_size{size} {} | ||
|
||
void Read(void *buffer, u32 read_length) | ||
{ | ||
memcpy(buffer, m_ptr + m_offset, read_length); | ||
m_offset += read_length; | ||
} | ||
|
||
template <typename T> | ||
T Read() | ||
{ | ||
static_assert(std::is_pod_v<T>); | ||
T val{}; | ||
Read(&val, sizeof(val)); | ||
return val; | ||
} | ||
|
||
u8 *data() const { return m_ptr; } | ||
u32 size() const { return m_size; } | ||
u8 *begin() const { return m_ptr; } | ||
u8 *end() const { return m_ptr + m_size; } | ||
u8 &operator[](size_t pos) const { return m_ptr[pos]; } | ||
|
||
u32 Tell() const { return m_offset; } | ||
void Seek(u32 offset) { m_offset = offset; } | ||
|
||
private: | ||
u8 *m_ptr = nullptr; | ||
u32 m_size = 0; | ||
u32 m_offset = 0; | ||
}; | ||
|
||
} // namespace util |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters