diff --git a/core/libs/prx/libSceFreeTypeOt/CMakeLists.txt b/core/libs/prx/libSceFreeTypeOt/CMakeLists.txt index 7df600d7e0..6b94a30eec 100644 --- a/core/libs/prx/libSceFreeTypeOt/CMakeLists.txt +++ b/core/libs/prx/libSceFreeTypeOt/CMakeLists.txt @@ -60,3 +60,20 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +target_sources(libSceFreeTypeOt PRIVATE src/Raccess.cpp) +if(BUILD_TESTING) + add_test_executable(guest_freetype_ot_raccess_tests ${LIBS_INCLUDE_DIR}/tests/GuestFreeTypeOtRaccess.cpp) + target_include_directories(guest_freetype_ot_raccess_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_freetype_ot_raccess_tests PRIVATE libSceFreeTypeOt) + if(WIN32) + target_link_libraries(guest_freetype_ot_raccess_tests PRIVATE libc) + endif() + configure_windows_unwind(guest_freetype_ot_raccess_tests) + add_test(NAME guest_freetype_ot_raccess COMMAND guest_freetype_ot_raccess_tests) + set_tests_properties(guest_freetype_ot_raccess PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_freetype_ot_raccess PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceFreeTypeOt/Export.cpp b/core/libs/prx/libSceFreeTypeOt/Export.cpp index fcbf9746ed..f34b074db6 100644 --- a/core/libs/prx/libSceFreeTypeOt/Export.cpp +++ b/core/libs/prx/libSceFreeTypeOt/Export.cpp @@ -289,16 +289,6 @@ int APS5_VABI sceFreeTypeOtUnknown_KVbmKCGGowg_nid_no_patch() { } APS5_EXPORT("KVbmKCGGowg", sceFreeTypeOtUnknown_KVbmKCGGowg_nid_no_patch); -int APS5_VABI sceFreeTypeOtUnknown_hF3stKUWbWg_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("hF3stKUWbWg", sceFreeTypeOtUnknown_hF3stKUWbWg_nid_no_patch); - -int APS5_VABI sceFreeTypeOtUnknown_p5GNJaXO2yE_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); -} -APS5_EXPORT("p5GNJaXO2yE", sceFreeTypeOtUnknown_p5GNJaXO2yE_nid_no_patch); - int APS5_VABI sceFreeTypeOtUnknown_8cnVx1yH9_I_nid_no_patch() { NotImplemented_nid_no_patch(__func__); } diff --git a/core/libs/prx/libSceFreeTypeOt/include/Raccess.hpp b/core/libs/prx/libSceFreeTypeOt/include/Raccess.hpp new file mode 100644 index 0000000000..7e1f427923 --- /dev/null +++ b/core/libs/prx/libSceFreeTypeOt/include/Raccess.hpp @@ -0,0 +1,28 @@ +// Derived from FreeType include/freetype/internal/ftrfork.h (FreeType 2.4.12, commit cecf93ef90c660a8b0b45e5adbbfb5ea443fb6b9). +// Copyright 2004, 2006, 2007, 2012 by Masatake YAMATO and Redhat K.K. +// This file is part of the FreeType project, and may only be used, modified, and distributed +// under the terms of the FreeType project license, LICENSE.TXT. By continuing to use, modify, +// or distribute this file you indicate that you have read the license and understand and accept it fully. + +#pragma once + +#include "prx/libSceFreeTypeOt/include/Stream.hpp" + +#include + +namespace FreeType { + +struct GuestRaccessLibrary { + GuestMemoryRec* memory; +}; + +} + +extern "C" { + +int APS5_VABI FT_Raccess_Get_HeaderInfo_nid_postfix(FreeType::GuestRaccessLibrary* library, FreeType::GuestStreamRec* stream, std::int64_t rforkOffset, + std::int64_t* mapOffset, std::int64_t* rdataPos); +int APS5_VABI FT_Raccess_Get_DataOffsets_nid_postfix(FreeType::GuestRaccessLibrary* library, FreeType::GuestStreamRec* stream, std::int64_t mapOffset, + std::int64_t rdataPos, std::int64_t tag, std::int64_t** offsets, std::int64_t* count); + +} diff --git a/core/libs/prx/libSceFreeTypeOt/libSceFreeTypeOt.hpp b/core/libs/prx/libSceFreeTypeOt/libSceFreeTypeOt.hpp index 1155682fa1..8518083a79 100644 --- a/core/libs/prx/libSceFreeTypeOt/libSceFreeTypeOt.hpp +++ b/core/libs/prx/libSceFreeTypeOt/libSceFreeTypeOt.hpp @@ -4,3 +4,4 @@ #include "prx/libSceFreeType/include/Api.hpp" #include "prx/libSceFreeTypeOt/include/Calc.hpp" #include "prx/libSceFreeTypeOt/include/Memory.hpp" +#include "prx/libSceFreeTypeOt/include/Raccess.hpp" diff --git a/core/libs/prx/libSceFreeTypeOt/src/Raccess.cpp b/core/libs/prx/libSceFreeTypeOt/src/Raccess.cpp new file mode 100644 index 0000000000..e0b7ed08af --- /dev/null +++ b/core/libs/prx/libSceFreeTypeOt/src/Raccess.cpp @@ -0,0 +1,142 @@ +// Derived from FreeType src/base/ftrfork.c (FreeType 2.4.12, commit cecf93ef90c660a8b0b45e5adbbfb5ea443fb6b9). +// Copyright 2004-2010, 2013 by Masatake YAMATO and Redhat K.K. +// This file is part of the FreeType project, and may only be used, modified, and distributed +// under the terms of the FreeType project license, LICENSE.TXT. By continuing to use, modify, +// or distribute this file you indicate that you have read the license and understand and accept it fully. + +#include "prx/libSceFreeTypeOt/include/Raccess.hpp" +#include "prx/libSceFreeTypeOt/include/Memory.hpp" +#include "prx/libc/include/General.hpp" + +#include +#include +#include +#include + +using namespace FreeType; + +namespace { + +constexpr int CannotOpenResource = 0x01; +constexpr int UnknownFileFormat = 0x02; +constexpr std::int64_t RefSize = 16; +constexpr std::int64_t LongMax = std::numeric_limits::max(); +constexpr std::int64_t LongMin = std::numeric_limits::min(); + +struct GuestRForkRef { + std::uint16_t resId; + std::uint64_t offset; +}; + +static_assert(sizeof(GuestRForkRef) == RefSize && offsetof(GuestRForkRef, offset) == 8); + +std::int64_t Add(std::int64_t a, std::int64_t b, const char* function) { + if ((b > 0 && a > LongMax - b) || (b < 0 && a < LongMin - b)) NotImplemented_nid_no_patch(function); + return a + b; +} + +std::int64_t Field(const std::uint8_t* head, unsigned index, const char* function) { + if (head[index] >= 0x80) NotImplemented_nid_no_patch(function); + return static_cast(head[index]) << 24 | head[index + 1] << 16 | head[index + 2] << 8 | head[index + 3]; +} + +} + +extern "C" { + +int APS5_VABI FT_Raccess_Get_HeaderInfo_nid_postfix(GuestRaccessLibrary*, GuestStreamRec* stream, std::int64_t rforkOffset, + std::int64_t* mapOffset, std::int64_t* rdataPos) { + const char* function = __func__; + std::uint8_t head[16]; + std::uint8_t head2[16]; + int error = FT_Stream_Seek_nid_postfix(stream, static_cast(rforkOffset)); + if (error) return error; + error = FT_Stream_Read_nid_postfix(stream, head, 16); + if (error) return error; + const std::int64_t dataStart = Add(rforkOffset, Field(head, 0, function), function); + const std::int64_t mapPos = Add(rforkOffset, Field(head, 4, function), function); + const std::int64_t dataLength = Field(head, 8, function); + if (!rdataPos) NotImplemented_nid_no_patch(function); + *rdataPos = dataStart; + if (Add(dataStart, dataLength, function) != mapPos || mapPos == rforkOffset) return UnknownFileFormat; + error = FT_Stream_Seek_nid_postfix(stream, static_cast(mapPos)); + if (error) return error; + head2[15] = static_cast(head[15] + 1); + error = FT_Stream_Read_nid_postfix(stream, head2, 16); + if (error) return error; + bool allZeros = true; + bool allMatch = true; + for (unsigned index = 0; index < 16; ++index) { + if (head2[index] != 0) allZeros = false; + if (head2[index] != head[index]) allMatch = false; + } + if (!allZeros && !allMatch) return UnknownFileFormat; + FT_Stream_Skip_nid_postfix(stream, 8); + const std::int64_t typeList = FT_Stream_ReadUShort_nid_postfix(stream, &error); + if (error) return error; + error = FT_Stream_Seek_nid_postfix(stream, static_cast(Add(mapPos, typeList, function))); + if (error) return error; + if (!mapOffset) NotImplemented_nid_no_patch(function); + *mapOffset = mapPos + typeList; + return 0; +} + +int APS5_VABI FT_Raccess_Get_DataOffsets_nid_postfix(GuestRaccessLibrary* library, GuestStreamRec* stream, std::int64_t mapOffset, + std::int64_t rdataPos, std::int64_t tag, std::int64_t** offsets, std::int64_t* count) { + const char* function = __func__; + if (!library) NotImplemented_nid_no_patch(function); + GuestMemoryRec* memory = library->memory; + int error = FT_Stream_Seek_nid_postfix(stream, static_cast(mapOffset)); + if (error) return error; + const int types = FT_Stream_ReadUShort_nid_postfix(stream, &error) + 1; + if (error) return error; + for (int type = 0; type < types; ++type) { + const std::int64_t typeTag = FT_Stream_ReadLong_nid_postfix(stream, &error); + if (error) return error; + const int subCount = FT_Stream_ReadUShort_nid_postfix(stream, &error); + if (error) return error; + const std::int64_t refList = FT_Stream_ReadUShort_nid_postfix(stream, &error); + if (error) return error; + if (typeTag != tag) continue; + if (!count) NotImplemented_nid_no_patch(function); + const std::int64_t refs = subCount + 1; + *count = refs; + error = FT_Stream_Seek_nid_postfix(stream, static_cast(Add(refList, mapOffset, function))); + if (error) return error; + if (!memory) NotImplemented_nid_no_patch(function); + auto* ref = static_cast(ft_mem_realloc_nid_postfix(memory, RefSize, 0, refs, nullptr, &error)); + if (error) return error; + for (std::int64_t index = 0; index < refs; ++index) { + ref[index].resId = FT_Stream_ReadUShort_nid_postfix(stream, &error); + if (!error) error = FT_Stream_Skip_nid_postfix(stream, 2); + std::int64_t value = 0; + if (!error) value = FT_Stream_ReadLong_nid_postfix(stream, &error); + if (!error) error = FT_Stream_Skip_nid_postfix(stream, 4); + if (error) { + ft_mem_free_nid_postfix(memory, ref); + return error; + } + ref[index].offset = static_cast(value) & 0xFFFFFF; + } + std::stable_sort(ref, ref + refs, [](const GuestRForkRef& a, const GuestRForkRef& b) { return a.resId < b.resId; }); + for (std::int64_t index = 1; index < refs; ++index) { + if (ref[index].resId == ref[index - 1].resId && ref[index].offset != ref[index - 1].offset) { + ft_mem_free_nid_postfix(memory, ref); + NotImplemented_nid_no_patch(function); + } + } + auto* result = static_cast(ft_mem_realloc_nid_postfix(memory, sizeof(std::int64_t), 0, refs, nullptr, &error)); + if (!error) { + for (std::int64_t index = 0; index < refs; ++index) { + result[index] = Add(rdataPos, static_cast(ref[index].offset), function); + } + if (!offsets) NotImplemented_nid_no_patch(function); + *offsets = result; + } + ft_mem_free_nid_postfix(memory, ref); + return error; + } + return CannotOpenResource; +} + +} diff --git a/core/libs/tests/GuestFreeTypeOtRaccess.cpp b/core/libs/tests/GuestFreeTypeOtRaccess.cpp new file mode 100644 index 0000000000..d75bf31c1e --- /dev/null +++ b/core/libs/tests/GuestFreeTypeOtRaccess.cpp @@ -0,0 +1,185 @@ +#include "prx/libSceFreeTypeOt/include/Memory.hpp" +#include "prx/libSceFreeTypeOt/include/Raccess.hpp" +#include +#include +#include +#include +#include + +namespace { + +using FreeType::GuestMemoryRec; +using FreeType::GuestRaccessLibrary; +using FreeType::GuestStreamRec; + +constexpr std::int64_t Post = 0x504F5354; +constexpr std::int64_t Sfnt = 0x73666E74; + +struct Recorder { + GuestMemoryRec memory{}; + int allocs = 0; + int frees = 0; +}; + +void* APS5_VABI RecordAlloc(GuestMemoryRec* memory, std::int64_t size) { + ++static_cast(memory->user)->allocs; + return std::malloc(static_cast(size)); +} + +void APS5_VABI RecordFree(GuestMemoryRec* memory, void* block) { + ++static_cast(memory->user)->frees; + std::free(block); +} + +void* APS5_VABI RecordRealloc(GuestMemoryRec*, std::int64_t, std::int64_t size, void* block) { + return std::realloc(block, static_cast(size)); +} + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "FreeType Ot raccess: %s\n", message); + std::exit(1); +} + +void Put16(std::vector& bytes, std::size_t at, std::uint32_t value) { + bytes[at] = static_cast(value >> 8); + bytes[at + 1] = static_cast(value); +} + +void Put32(std::vector& bytes, std::size_t at, std::uint32_t value) { + Put16(bytes, at, value >> 16); + Put16(bytes, at + 2, value); +} + +struct Fork { + std::vector bytes; + std::size_t prefix; + std::uint32_t dataStart = 0x100; + std::uint32_t dataLength = 0x40; + std::uint32_t mapPos = 0x140; + std::uint32_t typeList = 28; +}; + +Fork MakeFork(std::size_t prefix, bool zeroCopy) { + Fork fork; + fork.prefix = prefix; + fork.bytes.assign(prefix + 0x140 + 0x70, 0xEE); + const std::size_t base = prefix; + Put32(fork.bytes, base, fork.dataStart); + Put32(fork.bytes, base + 4, fork.mapPos); + Put32(fork.bytes, base + 8, fork.dataLength); + Put32(fork.bytes, base + 12, 0x60); + const std::size_t map = base + fork.mapPos; + for (std::size_t index = 0; index < 16; ++index) fork.bytes[map + index] = zeroCopy ? 0 : fork.bytes[base + index]; + Put16(fork.bytes, map + 24, fork.typeList); + const std::size_t types = map + fork.typeList; + Put16(fork.bytes, types, 1); + Put32(fork.bytes, types + 2, Sfnt); + Put16(fork.bytes, types + 6, 0); + Put16(fork.bytes, types + 8, 22); + Put32(fork.bytes, types + 10, Post); + Put16(fork.bytes, types + 14, 2); + Put16(fork.bytes, types + 16, 34); + const std::uint32_t refs[4][2] = {{0x0080, 0x00000010}, {0x0102, 0xA0000020}, {0x0100, 0x00000030}, {0x0101, 0x7F000008}}; + for (std::size_t index = 0; index < 4; ++index) { + const std::size_t ref = types + 22 + index * 12; + Put16(fork.bytes, ref, refs[index][0]); + Put16(fork.bytes, ref + 2, 0xFFFF); + Put32(fork.bytes, ref + 4, refs[index][1]); + Put32(fork.bytes, ref + 8, 0); + } + return fork; +} + +struct Session { + Recorder recorder; + GuestRaccessLibrary library{}; + GuestStreamRec stream{}; + + explicit Session(const std::vector& bytes) { + recorder.memory = {&recorder, RecordAlloc, RecordFree, RecordRealloc}; + library.memory = &recorder.memory; + FT_Stream_OpenMemory_nid_postfix(&stream, bytes.data(), bytes.size()); + } +}; + +void HeaderAndOffsets(std::size_t prefix, bool zeroCopy) { + const Fork fork = MakeFork(prefix, zeroCopy); + Session session(fork.bytes); + std::int64_t mapOffset = -1; + std::int64_t rdataPos = -1; + const auto offset = static_cast(prefix); + Require(FT_Raccess_Get_HeaderInfo_nid_postfix(&session.library, &session.stream, offset, &mapOffset, &rdataPos) == 0, "a resource fork header is accepted"); + Require(rdataPos == offset + 0x100 && mapOffset == offset + 0x140 + 28, "header info adds the fork offset to the data and type list positions"); + Require(session.stream.pos == static_cast(mapOffset), "header info leaves the stream at the type list"); + + std::int64_t* offsets = nullptr; + std::int64_t count = 0; + Require(FT_Raccess_Get_DataOffsets_nid_postfix(&session.library, &session.stream, mapOffset, rdataPos, Post, &offsets, &count) == 0, "a present tag is found"); + Require(count == 3 && offsets, "the subcount plus one references are returned"); + Require(offsets[0] == rdataPos + 0x30 && offsets[1] == rdataPos + 0x08 && offsets[2] == rdataPos + 0x20, "offsets are sorted by resource ID and drop the attribute byte"); + Require(session.recorder.allocs == 2 && session.recorder.frees == 1, "the reference array is freed and the offsets array is kept"); + ft_mem_free_nid_postfix(&session.recorder.memory, offsets); + + offsets = nullptr; + count = 0; + Require(FT_Raccess_Get_DataOffsets_nid_postfix(&session.library, &session.stream, mapOffset, rdataPos, Sfnt, &offsets, &count) == 0, "the first type is found"); + Require(count == 1 && offsets[0] == rdataPos + 0x10, "a single reference is returned"); + ft_mem_free_nid_postfix(&session.recorder.memory, offsets); + + offsets = nullptr; + count = -1; + const int allocs = session.recorder.allocs; + Require(FT_Raccess_Get_DataOffsets_nid_postfix(&session.library, &session.stream, mapOffset, rdataPos, 0x4E464E54, &offsets, &count) == 0x01, "a missing tag is Cannot_Open_Resource"); + Require(count == -1 && !offsets && session.recorder.allocs == allocs, "a missing tag writes and allocates nothing"); +} + +void RejectedHeaders() { + Fork fork = MakeFork(0, false); + Put32(fork.bytes, 8, 0x41); + Session lengths(fork.bytes); + std::int64_t mapOffset = -1; + std::int64_t rdataPos = -1; + Require(FT_Raccess_Get_HeaderInfo_nid_postfix(&lengths.library, &lengths.stream, 0, &mapOffset, &rdataPos) == 0x02, "data that does not end at the map is Unknown_File_Format"); + Require(rdataPos == 0x100 && mapOffset == -1, "the data position is written before the length check"); + + fork = MakeFork(0, false); + fork.bytes[0x140 + 3] ^= 1; + Session copy(fork.bytes); + rdataPos = -1; + Require(FT_Raccess_Get_HeaderInfo_nid_postfix(©.library, ©.stream, 0, &mapOffset, &rdataPos) == 0x02, "a map header that neither matches nor is zero is Unknown_File_Format"); + + fork = MakeFork(0, false); + Put32(fork.bytes, 0, 0); + Put32(fork.bytes, 4, 0); + Put32(fork.bytes, 8, 0); + Session empty(fork.bytes); + Require(FT_Raccess_Get_HeaderInfo_nid_postfix(&empty.library, &empty.stream, 0, &mapOffset, &rdataPos) == 0x02, "a map at the fork offset is Unknown_File_Format"); + + const std::vector shortStream(10, 0); + Session truncated(shortStream); + Require(FT_Raccess_Get_HeaderInfo_nid_postfix(&truncated.library, &truncated.stream, 0, &mapOffset, &rdataPos) == 0x55, "a short header is Invalid_Stream_Operation"); + Require(FT_Raccess_Get_HeaderInfo_nid_postfix(&truncated.library, &truncated.stream, 11, &mapOffset, &rdataPos) == 0x55, "a fork offset past the stream is Invalid_Stream_Operation"); +} + +void TruncatedReferences() { + Fork fork = MakeFork(0, false); + fork.bytes.resize(0x140 + 28 + 22 + 12 + 6); + Session session(fork.bytes); + std::int64_t* offsets = nullptr; + std::int64_t count = 0; + Require(FT_Raccess_Get_DataOffsets_nid_postfix(&session.library, &session.stream, 0x140 + 28, 0x100, Post, &offsets, &count) == 0x55, "a cut reference list is Invalid_Stream_Operation"); + Require(count == 3 && !offsets, "the count is written before the references are read"); + Require(session.recorder.allocs == 1 && session.recorder.frees == 1, "the reference array is freed on a read error"); +} + +} + +int main() { + HeaderAndOffsets(0, false); + HeaderAndOffsets(0x80, true); + RejectedHeaders(); + TruncatedReferences(); + std::puts("Guest FreeType Ot raccess tests passed"); + return 0; +} diff --git a/docs/dev/debt/libSceFreeTypeOt.md b/docs/dev/debt/libSceFreeTypeOt.md index 25fd2dfd7d..a1f61aa820 100644 --- a/docs/dev/debt/libSceFreeTypeOt.md +++ b/docs/dev/debt/libSceFreeTypeOt.md @@ -1,7 +1,7 @@ # libSceFreeTypeOt technical debt - The FT_Glyph_Copy, FT_Outline_Get_Orientation, FT_Request_Size, FT_Sin, FT_Tan, FT_Vector_Unit, FT_New_Size, FT_Outline_Copy, FT_Outline_Decompose, FT_Stroker_New, FT_Stroker_Done, FT_Stroker_GetCounts, FT_Stroker_BeginSubPath, FT_Stroker_LineTo, FT_Stroker_EndSubPath, FT_Stroker_Set, FT_Stroker_Rewind, FT_Stroker_GetBorderCounts, FT_Stroker_ConicTo and FT_Stroker_CubicTo NIDs in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are identified, but their provider-specific prototypes and behavior remain unverified. Their named helpers retain the explicit inventory aliases and throw; the separate libSceFreeType implementations do not supply this provider. -- The 68 `sceFreeTypeOtUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. +- The 66 `sceFreeTypeOtUnknown_*_nid_no_patch` exports in [Export.cpp](../../../core/libs/prx/libSceFreeTypeOt/Export.cpp) are present in the PS5 3.20 export inventory from [PS5-3.20_Libs](https://github.com/DNNDHH/PS5-3.20_Libs). Their prototypes and behavior are unverified. Each uses the no-parameter import-resolution convention and throws through `NotImplemented_nid_no_patch`; these declarations make no ABI or behavior claim. ### Unknown function info - [FT_Outline_GetInsideBorder, FT_Outline_GetOutsideBorder](../../../core/libs/prx/libSceFreeTypeOt/CMakeLists.txt) (libSceFreeTypeOl, libSceFreeTypeOt) - console prototypes, enum/layout equivalence, FreeType configuration and module/library versions are unverified. The existing LP64 outline layout and unsigned 16-bit counts follow bundled FreeType 2.13.3; border selection follows [ftstroke.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftstroke.c), with the shifted polygon area from [ftoutln.c](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c) accumulated in 64 bits. Null positive-count arrays and out-of-range or non-increasing contour endpoints throw when reached; their console behavior is unknown. Tags and flags are not read, and input arrays remain caller-owned. @@ -19,3 +19,5 @@ - [ft_mem_alloc, ft_mem_qalloc, ft_mem_realloc, ft_mem_qrealloc, ft_mem_free, ft_mem_dup, ft_mem_strdup, ft_mem_strcpyn](../../../core/libs/prx/libSceFreeTypeOt/src/Memory.cpp) (libSceFreeTypeOt) - the exports carry the NIDs of these FreeType names; console prototypes and FreeType version equivalence are unverified. The LP64 signatures, the guest `FT_MemoryRec` callbacks and the error codes (Invalid_Argument 0x06, Array_Too_Large 0x0A, Out_Of_Memory 0x40) follow FreeType 2.4.12 [ftutil.c](https://github.com/freetype/freetype/blob/cecf93ef90c660a8b0b45e5adbbfb5ea443fb6b9/src/base/ftutil.c#L49-L221). Bundled FreeType 2.13.3 differs only in `ft_mem_qrealloc` from a count of 0, which it does not zero. As in a release build, `ft_mem_qrealloc` does not check the `FT_ASSERT` that a block with no current items is null. The current byte size passed to `realloc` wraps where the source's signed product would overflow. - [FT_Sin, FT_Tan, FT_Vector_Unit, FT_Outline_Get_Orientation, FT_Outline_Copy, FT_Outline_Decompose](../../../core/libs/prx/libSceFreeTypeOt/CMakeLists.txt) (libSceFreeTypeOt) - built from the 2.13.3-based libSceFreeType sources; the provider is dated to 2.4.11-2.4.12 elsewhere and its version is unmeasured. Against [2.4.12](https://github.com/freetype/freetype/blob/cecf93ef90c660a8b0b45e5adbbfb5ea443fb6b9/src/base/ftoutln.c): `FT_Outline_Copy` returns `Invalid_Outline`, not `Invalid_Argument`, for a null outline; `FT_Outline_Get_Orientation` returns `FT_ORIENTATION_NONE` for a zero-width or zero-height box and beyond ±0x1000000, and shifts coordinates rather than differences; counts of 0x8000 or more are not negative `short`s. 2.4.11 does not shift the area and uses other CORDIC constants than the 2.4.12 ones used here. FT_Cos and FT_Angle_Diff are also exported but absent from the 3.20 inventory. + +- [FT_Raccess_Get_HeaderInfo, FT_Raccess_Get_DataOffsets](../../../core/libs/prx/libSceFreeTypeOt/src/Raccess.cpp) (libSceFreeTypeOt) - console prototypes and FreeType version equivalence are unverified. Both follow 2.4.12 [ftrfork.c](https://github.com/freetype/freetype/blob/cecf93ef90c660a8b0b45e5adbbfb5ea443fb6b9/src/base/ftrfork.c#L49-L237) and read only `memory` from the library record. `FT_Raccess_Get_DataOffsets` takes the seven arguments of 2.4.11-2.5.2; from 2.5.3 `FT_Bool sort_by_res_id` precedes `offsets`. Header fields with the top bit set and overflowing `FT_Long` sums, undefined in that source, throw; 2.13.3 adds overlap, stream size and count checks that 2.4.12 lacks. Equal resource IDs with different offsets throw, since `ft_qsort` leaves their order unspecified. Null pointers throw where 2.4.12 dereferences them. `FT_Raccess_Guess` still throws.