From 4866d9e9760eaa06549871c460c0b6e30b0d2b06 Mon Sep 17 00:00:00 2001 From: Bram910 Date: Sat, 24 Oct 2026 11:15:05 +0200 Subject: [PATCH] fix(io): avoid allocating the entire file prefix limit --- core/relinker/CMakeLists.txt | 12 +++ core/relinker/io/src/FileReader.cpp | 19 ++-- core/relinker/io/tests/FileReaderTests.cpp | 107 +++++++++++++++++++++ 3 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 core/relinker/io/tests/FileReaderTests.cpp diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index dda82e2047..a1364f2941 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -318,6 +318,18 @@ if(BUILD_TESTING) add_test(NAME file_identity COMMAND file_identity_tests) set_tests_properties(file_identity PROPERTIES TIMEOUT 20) + add_test_executable(file_reader_tests + ${CMAKE_CURRENT_SOURCE_DIR}/io/tests/FileReaderTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileReader.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/NativePath.cpp + ) + target_include_directories(file_reader_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ) + add_test(NAME file_reader_prefix COMMAND file_reader_tests) + set_tests_properties(file_reader_prefix PROPERTIES TIMEOUT 20) + add_test_executable(param_json_utf8_tests ${CMAKE_CURRENT_SOURCE_DIR}/../libs/tests/ParamJsonParser.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp) diff --git a/core/relinker/io/src/FileReader.cpp b/core/relinker/io/src/FileReader.cpp index 4f012529d3..5cbed8069b 100644 --- a/core/relinker/io/src/FileReader.cpp +++ b/core/relinker/io/src/FileReader.cpp @@ -1,6 +1,8 @@ #include #include #include +#include +#include #include #include #include @@ -17,12 +19,17 @@ std::vector FileReader::ReadPrefix(const std::string& path, const std::ifstream f(native, std::ios::binary); if (!f) throw Domain::RelinkerException("Cannot open file: " + path); - std::vector buf(size); - f.read(reinterpret_cast(buf.data()), static_cast(size)); - const auto count = f.gcount(); - if (f.bad()) - throw Domain::RelinkerException("Cannot read file: " + path); - buf.resize(static_cast(count)); + std::vector buf; + std::array chunk; + while (buf.size() < size) { + const auto requested = std::min(size - buf.size(), chunk.size()); + f.read(reinterpret_cast(chunk.data()), static_cast(requested)); + const auto count = static_cast(f.gcount()); + if (f.bad()) + throw Domain::RelinkerException("Cannot read file: " + path); + buf.insert(buf.end(), chunk.begin(), chunk.begin() + count); + if (count < requested) break; + } return buf; } diff --git a/core/relinker/io/tests/FileReaderTests.cpp b/core/relinker/io/tests/FileReaderTests.cpp new file mode 100644 index 0000000000..769bb03303 --- /dev/null +++ b/core/relinker/io/tests/FileReaderTests.cpp @@ -0,0 +1,107 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using Bytes = std::vector; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +template +void requireError(const TAction& action, const std::string& message) { + try { + action(); + } catch (const Domain::RelinkerException& error) { + require(error.what() == message, "Unexpected prefix-read error: " + std::string(error.what())); + return; + } + throw std::runtime_error("Invalid prefix read was accepted: " + message); +} + +void prefixes(const std::filesystem::path& path, const Bytes& expected) { + const Io::FileReader reader; + const auto name = Io::Utf8Path(path); + const auto maximum = static_cast(std::numeric_limits::max()); + for (const std::size_t limit : {std::size_t{0}, std::size_t{1}, std::size_t{2}, std::size_t{4095}, + std::size_t{4096}, std::size_t{4097}, std::size_t{8191}, std::size_t{8192}, std::size_t{8193}, + expected.size(), expected.size() + 1, maximum}) { + const auto count = std::min(expected.size(), limit); + require(reader.ReadPrefix(name, limit) == Bytes(expected.begin(), expected.begin() + count), + "Incorrect file prefix for limit " + std::to_string(limit)); + } +} + +void fileContents(const std::filesystem::path& directory) { + const auto path = directory / Io::NativePath("prefix-\xc3\xa9-\xe6\x97\xa5.bin"); + for (const std::size_t size : {std::size_t{0}, std::size_t{3}, std::size_t{4096}, std::size_t{4097}, + std::size_t{8192}, std::size_t{10003}}) { + Bytes expected(size); + for (std::size_t index = 0; index < size; ++index) + expected[index] = static_cast(index * 37 + 13); + std::ofstream output(path, std::ios::binary); + if (!expected.empty()) + output.write(reinterpret_cast(expected.data()), static_cast(expected.size())); + output.close(); + require(static_cast(output), "Cannot prepare the prefix-read fixture"); + prefixes(path, expected); + require(Io::FileReader().Read(Io::Utf8Path(path)) == expected, "Prefix reads changed the input file"); + } +} + +void invalidRequests(const std::filesystem::path& directory) { + const Io::FileReader reader; + const auto missing = Io::Utf8Path(directory / "missing.bin"); + const auto folder = Io::Utf8Path(directory); + for (const std::size_t size : {std::size_t{0}, std::size_t{1}, std::size_t{4097}}) { + requireError([&] { reader.ReadPrefix(missing, size); }, "Cannot open file: " + missing); + requireError([&] { reader.ReadPrefix(folder, size); }, "Cannot open file: " + folder); + } + const auto maximum = static_cast(std::numeric_limits::max()); + if (maximum < std::numeric_limits::max()) { + requireError([&] { reader.ReadPrefix(missing, maximum + 1); }, "File prefix is too large: " + missing); + } +} + +void zeroReportedFileSize() { +#ifdef __linux__ + const std::filesystem::path path = "/proc/self/cmdline"; + require(std::filesystem::is_regular_file(path) && std::filesystem::file_size(path) == 0, + "Expected a regular proc file with a zero reported size"); + std::ifstream input(path, std::ios::binary); + const Bytes expected{std::istreambuf_iterator(input), std::istreambuf_iterator()}; + require(!expected.empty(), "Cannot read the proc prefix fixture"); + prefixes(path, expected); +#endif +} + +} + +int main() { + const auto directory = std::filesystem::temp_directory_path() / + ("anyps5-file-reader-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + try { + std::filesystem::create_directory(directory); + fileContents(directory); + invalidRequests(directory); + zeroReportedFileSize(); + std::filesystem::remove_all(directory); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + std::error_code ignored; + std::filesystem::remove_all(directory, ignored); + return 1; + } + std::cout << "File prefix-read tests passed\n"; + return 0; +}