loader: Add support for BPS patches
The BPS format allows distributing patches that are smaller and that do not contain copyrighted content if data is relocated (unlike non-trivial IPS patches). This is essential for games such as Majora's Mask 3D that have three barely different code revisions. Supporting all three versions would demand an unreasonable amount of work; with BPS patches only one version has to be supported. The patcher is written in C++ in order to make it possible to share the implementation with Citra and because a C version would be a lot more ugly and tedious to write. The patcher is non-intrusive for the rest of the codebase and self-contained so hopefully that isn't an issue. This adds roughly ~0x500 bytes of code to the loader sysmodule. Code looks reasonably optimised (in IDA). Tested and works on an o3DS.
This commit is contained in:
parent
ca49956219
commit
8233d4e226
283
sysmodules/loader/source/bps_patcher.cpp
Normal file
283
sysmodules/loader/source/bps_patcher.cpp
Normal file
@ -0,0 +1,283 @@
|
||||
#include "bps_patcher.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
|
||||
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
|
||||
{
|
||||
namespace Bps
|
||||
{
|
||||
// The BPS format uses variable length encoding for all integers.
|
||||
// Realistically uint32s are more than enough for code patching.
|
||||
using Number = u32;
|
||||
|
||||
constexpr std::size_t FooterSize = 12;
|
||||
|
||||
// The BPS format uses CRC32 checksums.
|
||||
[[gnu::optimize("Os")]] static u32 crc32(const u8 *data, std::size_t size)
|
||||
{
|
||||
u32 crc = 0xFFFFFFFF;
|
||||
for(std::size_t i = 0; i < size; ++i)
|
||||
{
|
||||
crc ^= data[i];
|
||||
for(std::size_t j = 0; j < 8; ++j)
|
||||
{
|
||||
u32 mask = -(crc & 1);
|
||||
crc = (crc >> 1) ^ (0xEDB88320 & mask);
|
||||
}
|
||||
}
|
||||
return ~crc;
|
||||
}
|
||||
|
||||
// Utility class to make keeping track of offsets and bound checks less error prone.
|
||||
template <typename T>
|
||||
class Stream
|
||||
{
|
||||
public:
|
||||
Stream(T *ptr, std::size_t size) : m_ptr{ptr}, m_size{size} {}
|
||||
|
||||
bool Read(void *buffer, std::size_t length)
|
||||
{
|
||||
if(m_offset + length > m_size)
|
||||
return false;
|
||||
std::memcpy(buffer, m_ptr + m_offset, length);
|
||||
m_offset += length;
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename OtherType>
|
||||
[[gnu::optimize("Os")]] bool CopyFrom(Stream<OtherType> &other, std::size_t length)
|
||||
{
|
||||
if(m_offset + length > m_size)
|
||||
return false;
|
||||
if(!other.Read(m_ptr + m_offset, length))
|
||||
return false;
|
||||
m_offset += length;
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename ValueType>
|
||||
std::optional<ValueType> Read()
|
||||
{
|
||||
static_assert(std::is_pod_v<ValueType>);
|
||||
ValueType val{};
|
||||
if(!Read(&val, sizeof(val)))
|
||||
return std::nullopt;
|
||||
return val;
|
||||
}
|
||||
|
||||
[[gnu::optimize("Os")]] Number ReadNumber()
|
||||
{
|
||||
Number data = 0, shift = 1;
|
||||
std::optional<u8> x;
|
||||
while((x = Read<u8>()))
|
||||
{
|
||||
data += (*x & 0x7f) * shift;
|
||||
if(*x & 0x80)
|
||||
break;
|
||||
shift <<= 7;
|
||||
data += shift;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
auto data() const { return m_ptr; }
|
||||
std::size_t size() const { return m_size; }
|
||||
std::size_t Tell() const { return m_offset; }
|
||||
|
||||
bool Seek(size_t offset)
|
||||
{
|
||||
m_offset = offset;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
T *m_ptr = nullptr;
|
||||
std::size_t m_size = 0;
|
||||
std::size_t m_offset = 0;
|
||||
};
|
||||
|
||||
class PatchApplier
|
||||
{
|
||||
public:
|
||||
PatchApplier(Stream<const u8> source, Stream<u8> target, Stream<const u8> patch)
|
||||
: m_source{source}, m_target{target}, m_patch{patch}
|
||||
{
|
||||
}
|
||||
|
||||
[[gnu::always_inline]] bool Apply()
|
||||
{
|
||||
const auto magic = *m_patch.Read<std::array<char, 4>>();
|
||||
if(std::string_view(magic.data(), magic.size()) != "BPS1")
|
||||
return false;
|
||||
|
||||
const Bps::Number source_size = m_patch.ReadNumber();
|
||||
const Bps::Number target_size = m_patch.ReadNumber();
|
||||
const Bps::Number metadata_size = m_patch.ReadNumber();
|
||||
if(source_size > m_source.size() || target_size > m_target.size() || metadata_size != 0)
|
||||
return false;
|
||||
|
||||
const std::size_t command_start_offset = m_patch.Tell();
|
||||
const std::size_t command_end_offset = m_patch.size() - FooterSize;
|
||||
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);
|
||||
|
||||
if(crc32(m_source.data(), source_size) != source_crc32)
|
||||
return false;
|
||||
|
||||
// Process all patch commands.
|
||||
std::memset(m_target.data(), 0, m_target.size());
|
||||
while(m_patch.Tell() < command_end_offset)
|
||||
{
|
||||
const bool ok = HandleCommand();
|
||||
if(!ok)
|
||||
return false;
|
||||
}
|
||||
|
||||
return crc32(m_target.data(), target_size) == target_crc32;
|
||||
}
|
||||
|
||||
private:
|
||||
bool HandleCommand()
|
||||
{
|
||||
const Number data = m_patch.ReadNumber();
|
||||
const Number command = data & 3;
|
||||
const Number 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)
|
||||
{
|
||||
return m_source.Seek(m_target.Tell()) && m_target.CopyFrom(m_source, length);
|
||||
}
|
||||
|
||||
bool TargetRead(Number length) { return m_target.CopyFrom(m_patch, length); }
|
||||
|
||||
bool SourceCopy(Number length)
|
||||
{
|
||||
const Number data = m_patch.ReadNumber();
|
||||
m_source_relative_offset += (data & 1 ? -1 : +1) * int(data >> 1);
|
||||
if(!m_source.Seek(m_source_relative_offset) || !m_target.CopyFrom(m_source, length))
|
||||
return false;
|
||||
m_source_relative_offset += length;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TargetCopy(Number length)
|
||||
{
|
||||
const Number data = m_patch.ReadNumber();
|
||||
m_target_relative_offset += (data & 1 ? -1 : +1) * int(data >> 1);
|
||||
if(m_target.Tell() + length > m_target.size())
|
||||
return false;
|
||||
if(m_target_relative_offset + length > m_target.size())
|
||||
return false;
|
||||
// Byte by byte copy.
|
||||
for(size_t i = 0; i < length; ++i)
|
||||
m_target.data()[m_target.Tell() + i] = m_target.data()[m_target_relative_offset++];
|
||||
m_target.Seek(m_target.Tell() + length);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::size_t m_source_relative_offset = 0;
|
||||
std::size_t m_target_relative_offset = 0;
|
||||
Stream<const u8> m_source;
|
||||
Stream<u8> m_target;
|
||||
Stream<const u8> m_patch;
|
||||
};
|
||||
|
||||
} // namespace Bps
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
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;
|
||||
const u32 patch_size = u32(patch_file.GetSize().value_or(0));
|
||||
|
||||
// Temporarily use APPLICATION memory to store the source and patch data.
|
||||
ScopedAppHeap memory;
|
||||
|
||||
u8 *source_data = reinterpret_cast<u8 *>(memory.BaseAddress);
|
||||
u8 *patch_data = source_data + size;
|
||||
std::memcpy(source_data, code, size);
|
||||
if(!patch_file.Read(patch_data, patch_size, 0))
|
||||
return false;
|
||||
|
||||
Bps::Stream<const u8> source_stream{source_data, size};
|
||||
Bps::Stream target_stream{code, size};
|
||||
Bps::Stream<const u8> patch_stream{patch_data, patch_size};
|
||||
Bps::PatchApplier applier{source_stream, target_stream, patch_stream};
|
||||
if(!applier.Apply())
|
||||
svcBreak(USERBREAK_PANIC);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace patcher
|
||||
|
||||
extern "C"
|
||||
{
|
||||
bool patcherApplyCodeBpsPatch(u64 progId, u8 *code, u32 size)
|
||||
{
|
||||
return patcher::ApplyCodeBpsPatch(progId, code, size);
|
||||
}
|
||||
}
|
12
sysmodules/loader/source/bps_patcher.h
Normal file
12
sysmodules/loader/source/bps_patcher.h
Normal file
@ -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
|
76
sysmodules/loader/source/file_util.h
Normal file
76
sysmodules/loader/source/file_util.h
Normal file
@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string.h>
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
} // namespace util
|
@ -1,5 +1,6 @@
|
||||
#include <3ds.h>
|
||||
#include "patcher.h"
|
||||
#include "bps_patcher.h"
|
||||
#include "memory.h"
|
||||
#include "strings.h"
|
||||
#include "romfsredir.h"
|
||||
@ -845,6 +846,7 @@ void patchCode(u64 progId, u16 progVer, u8 *code, u32 size, u32 textSize, u32 ro
|
||||
|
||||
if(CONFIG(PATCHGAMES))
|
||||
{
|
||||
if(!patcherApplyCodeBpsPatch(progId, code, size)) goto error;
|
||||
if(!applyCodeIpsPatch(progId, code, size)) goto error;
|
||||
|
||||
if((u32)((progId >> 0x20) & 0xFFFFFFEDULL) == 0x00040000)
|
||||
|
Reference in New Issue
Block a user