feat(libc): implement generic error category messages

This commit is contained in:
Evgeniy Kazarin 2026-10-13 14:36:47 +03:00
parent e99a1b0aa9
commit e3cef332e6
7 changed files with 241 additions and 16 deletions

View File

@ -34,7 +34,7 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceAmpr>")
endif()
if(MINGW)
if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux")
enable_language(ASM)
add_test_executable(guest_ampr_cpp_error_tests
${LIBS_INCLUDE_DIR}/tests/GuestAmprCppError.cpp
@ -43,7 +43,10 @@ if(BUILD_TESTING)
target_link_libraries(guest_ampr_cpp_error_tests PRIVATE libSceAmpr libkernel libc)
configure_windows_unwind(guest_ampr_cpp_error_tests)
add_test(NAME guest_ampr_cpp_error COMMAND guest_ampr_cpp_error_tests)
set_tests_properties(guest_ampr_cpp_error PROPERTIES TIMEOUT 10 ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceAmpr>")
set_tests_properties(guest_ampr_cpp_error PROPERTIES TIMEOUT 10)
if(WIN32)
set_tests_properties(guest_ampr_cpp_error PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceAmpr>")
endif()
endif()
endif()

View File

@ -442,6 +442,16 @@ if(BUILD_TESTING)
target_include_directories(guest_new_handler_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_new_handler_tests PRIVATE libc)
configure_windows_unwind(guest_new_handler_tests)
add_test_executable(guest_generic_category_tests ${LIBS_INCLUDE_DIR}/tests/GuestGenericCategory.cpp)
target_include_directories(guest_generic_category_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_generic_category_tests PRIVATE libc)
configure_windows_unwind(guest_generic_category_tests)
add_test(NAME guest_generic_category COMMAND guest_generic_category_tests)
set_tests_properties(guest_generic_category PROPERTIES TIMEOUT 10)
if(WIN32)
set_tests_properties(guest_generic_category PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME guest_new_handler COMMAND guest_new_handler_tests)
if(WIN32)
set_tests_properties(guest_new_handler PROPERTIES ENVIRONMENT_MODIFICATION

View File

@ -1,4 +1,5 @@
#include "prx/libc/include/exceptions/Runtime.hpp"
#include "prx/libc/include/GuestLocale.hpp"
#include <cstdio>
#include <iterator>
#include <limits>
@ -6,6 +7,8 @@
#include <stdexcept>
#include <string>
extern "C" void* APS5_VABI _Znwm_nid_postfix(std::size_t size);
namespace LibcException {
struct TypeRecord { const void* const* vtable; const char* name; const TypeRecord* base; };
struct ExceptionObject { const void* vtable; const char* message; };
@ -129,6 +132,86 @@ struct SystemErrorObject { ExceptionObject base; ErrorCode code; };
static_assert(sizeof(ErrorCode) == 16 && sizeof(SystemErrorObject) == 32 && offsetof(SystemErrorObject, code) == 16);
struct CppError { int value; const char* message; };
static const char* GenericErrorMessage(int value) {
static constexpr CppError errors[] {
{1, "operation not permitted"},
{2, "no such file or directory"},
{3, "no such process"},
{4, "interrupted"},
{5, "io error"},
{6, "no such device or address"},
{7, "argument list too long"},
{8, "executable format error"},
{9, "bad file descriptor"},
{10, "no child process"},
{11, "resource deadlock would occur"},
{12, "not enough memory"},
{13, "permission denied"},
{14, "bad address"},
{16, "device or resource busy"},
{17, "file exists"},
{18, "cross device link"},
{19, "no such device"},
{20, "not a directory"},
{21, "is a directory"},
{22, "invalid argument"},
{23, "too many files open in system"},
{24, "too many files open"},
{25, "inappropriate io control operation"},
{26, "text file busy"},
{27, "file too large"},
{28, "no space on device"},
{29, "invalid seek"},
{30, "read only file system"},
{31, "too many links"},
{32, "broken pipe"},
{33, "argument out of domain"},
{34, "result out of range"},
{35, "operation would block"},
{36, "operation in progress"},
{37, "connection already in progress"},
{38, "not a socket"},
{39, "destination address required"},
{40, "message size"},
{41, "wrong protocol type"},
{42, "no protocol option"},
{43, "protocol not supported"},
{45, "not supported"},
{47, "address family not supported"},
{48, "address in use"},
{49, "address not available"},
{50, "network down"},
{51, "network unreachable"},
{52, "network reset"},
{53, "connection aborted"},
{54, "connection reset"},
{55, "no buffer space"},
{56, "already connected"},
{57, "not connected"},
{60, "timed out"},
{61, "connection refused"},
{62, "too many symbolic link levels"},
{63, "filename too long"},
{65, "host unreachable"},
{66, "directory not empty"},
{77, "no lock available"},
{78, "function not supported"},
{82, "identifier removed"},
{83, "no message"},
{84, "value too large"},
{85, "operation canceled"},
{86, "illegal byte sequence"},
{89, "bad message"},
{91, "no link"},
{92, "protocol error"},
{95, "state not recoverable"},
{96, "owner dead"}
};
for (const auto& error : errors)
if (error.value == value) return error.message;
return nullptr;
}
const TypeRecord ErrorCategoryType {ClassTypeVtable + 2, "St14error_category", nullptr};
const TypeRecord GenericCategoryType {SingleTypeVtable + 2, "St23_Generic_error_category", &ErrorCategoryType};
static void APS5_VABI GenericCategoryDestroy_nid_no_patch(CategoryObject*) {}
@ -136,8 +219,18 @@ static void APS5_VABI GenericCategoryDelete_nid_no_patch(CategoryObject*) {
throw std::invalid_argument("std::_Generic_error_category: the generic category object is not heap allocated");
}
static const char* APS5_VABI GenericCategoryName_nid_no_patch(const CategoryObject*) { return "generic"; }
static void* APS5_VABI GenericCategoryMessage_nid_no_patch(void* result, const CategoryObject*, int) {
NotImplemented_nid_no_patch("std::_Generic_error_category::message");
static GuestLocale::String* APS5_VABI GenericCategoryMessage_nid_no_patch(GuestLocale::String* result, const CategoryObject*, int value) {
if (!result) throw std::invalid_argument("std::_Generic_error_category::message: null result");
const char* message = GenericErrorMessage(value);
if (!message) message = "unknown error";
const auto length = std::strlen(message);
const auto capacity = length | std::size_t{15};
char* storage = length <= 15 ? result->buffer : static_cast<char*>(_Znwm_nid_postfix(capacity + 1));
std::memcpy(storage, message, length + 1);
result->reserved = 0;
if (length > 15) result->pointer = storage;
result->size = length;
result->capacity = capacity;
return result;
}
static ErrorCode APS5_VABI GenericCategoryCondition_nid_no_patch(const CategoryObject* self, int value) { return {value, self}; }
@ -159,6 +252,16 @@ const CategoryObject GenericCategory {GenericCategoryVtable + 2};
}
extern "C" {
const char* APS5_VABI _ZSt13_Syserror_mapi_nid_postfix(int value) {
return LibcException::GenericErrorMessage(value);
}
const LibcException::CategoryObject* APS5_VABI _ZSt16generic_categoryv_nid_postfix() {
return &LibcException::GenericCategory;
}
GuestLocale::String* APS5_VABI _ZNKSt23_Generic_error_category7messageEi_nid_postfix(
GuestLocale::String* result, const LibcException::CategoryObject* self, int value) {
return LibcException::GenericCategoryMessage_nid_no_patch(result, self, value);
}
LibcException::TypeRecord _ZTISt8ios_base_nid_postfix {LibcException::ClassTypeVtable + 2, "St8ios_base", nullptr};
LibcException::TypeRecord _ZTISt9exception_nid_postfix {LibcException::ClassTypeVtable + 2, "St9exception", nullptr};
LibcException::ExceptionVtable _ZTVSt9exception_nid_postfix {0, &_ZTISt9exception_nid_postfix, LibcException::GuestDestroyPlain_nid_no_patch, LibcException::GuestDeletePlain_nid_no_patch, LibcException::GuestPlainWhat_nid_no_patch};
@ -382,7 +485,7 @@ const char* APS5_VABI _ZNKSt8ios_base7failure4whatEv_nid_postfix(const LibcExcep
throw std::out_of_range("_Throw_Cpp_error: " + std::to_string(code) + " is not a _Cpp_error code");
const auto& error = errors[code];
char message[128];
std::snprintf(message, sizeof(message), "%s: %s", error.message, error.message);
std::snprintf(message, sizeof(message), "%s: %s", error.message, LibcException::GenericErrorMessage(error.value));
auto* object = static_cast<LibcException::SystemErrorObject*>(__cxa_allocate_exception_nid_postfix(sizeof(LibcException::SystemErrorObject)));
LibcException::Construct(&object->base, _ZTVSt12system_error_nid_postfix, message);
object->code = {error.value, &LibcException::GenericCategory};

View File

@ -31,7 +31,11 @@ GuestCatch:
call FixtureFail
ud2
.cfi_endproc
#ifdef _WIN32
.section .rdata,"dr"
#else
.section .rodata
#endif
.Lcatch_lsda:
.byte 0xff, 0x80
.uleb128 .Lcatch_types_end-.Lcatch_types_offset
@ -48,3 +52,6 @@ GuestCatch:
.quad FixtureCatchType
.quad FixtureOtherType
.Lcatch_types_end:
#ifndef _WIN32
.section .note.GNU-stack,"",@progbits
#endif

View File

@ -1,4 +1,7 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libc/include/GuestLocale.hpp"
#include "prx/libc/include/ApplicationHeap.hpp"
#include <array>
#include <cstddef>
#include <cstdio>
#include <cstdlib>
@ -29,7 +32,7 @@ static constexpr Expected Errors[] {
{12, "not enough memory: not enough memory"},
{1, "operation not permitted: operation not permitted"},
{11, "resource deadlock would occur: resource deadlock would occur"},
{35, "resource unavailable try again: resource unavailable try again"}
{35, "resource unavailable try again: operation would block"}
};
extern "C" {
@ -41,6 +44,7 @@ extern const unsigned char _ZTISt12system_error_nid_postfix[];
void APS5_VABI _ZNSt12system_errorC1ERKS__nid_postfix(SystemError* self, const SystemError* source);
SystemError* APS5_VABI _ZNSt12system_erroraSERKS__nid_postfix(SystemError* self, const SystemError* source);
void APS5_VABI _ZNSt12system_errorD1Ev_nid_postfix(SystemError* self);
void APS5_VABI _ZdlPv_nid_postfix(void* pointer);
void APS5_VABI GuestCatch(int code);
const void* FixtureCatchType = nullptr;
@ -86,14 +90,11 @@ static void CheckCategory(const SystemError& error) {
Require(equivalentCode(category, &error.code, error.code.value), 28);
Require(!equivalentCode(category, &error.code, error.code.value + 1), 29);
Require(!equivalentCode(category, &foreign, error.code.value), 30);
bool messageThrew = false;
try {
unsigned char result[32] {};
Slot<CategoryMessage>(category->vtable, 3)(result, category, error.code.value);
} catch (const std::runtime_error&) {
messageThrew = true;
}
Require(messageThrew, 31);
GuestLocale::String result {};
Require(Slot<CategoryMessage>(category->vtable, 3)(&result, category, error.code.value) == &result, 33);
const char* data = result.capacity <= 15 ? result.buffer : result.pointer;
Require(std::strcmp(data, std::strstr(FixtureExpected->what, ": ") + 2) == 0, 34);
if (result.capacity > 15) _ZdlPv_nid_postfix(result.pointer);
bool deleteThrew = false;
try {
Slot<CategoryDelete>(category->vtable, 1)(category);
@ -148,6 +149,8 @@ static void CheckOutOfRange(int code) {
}
int main() {
const std::array<void*, 10> api {};
ApplicationHeapRegister_nid_no_patch(api.data());
FixtureOtherType = _ZTISt11logic_error_nid_postfix;
for (int code = 0; code < static_cast<int>(std::size(Errors)); ++code) Catch(_ZTISt12system_error_nid_postfix, code);
Catch(_ZTISt13runtime_error_nid_postfix, 5);

View File

@ -0,0 +1,99 @@
#include "prx/libc/include/GuestLocale.hpp"
#include "prx/libc/include/ApplicationHeap.hpp"
#include <array>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <stdexcept>
struct Category { const void* const* vtable; };
struct ErrorCode { int value; const Category* category; };
using Message = GuestLocale::String* (APS5_VABI *)(GuestLocale::String*, const Category*, int);
using Name = const char* (APS5_VABI *)(const Category*);
using Condition = ErrorCode (APS5_VABI *)(const Category*, int);
extern "C" {
const char* APS5_VABI _ZSt13_Syserror_mapi_nid_postfix(int value);
const Category* APS5_VABI _ZSt16generic_categoryv_nid_postfix();
GuestLocale::String* APS5_VABI _ZNKSt23_Generic_error_category7messageEi_nid_postfix(
GuestLocale::String* result, const Category* self, int value);
int* APS5_VABI __error_nid_postfix();
void APS5_VABI _ZdlPv_nid_postfix(void* pointer);
}
static void Require(bool value, int line) {
if (value) return;
std::fprintf(stderr, "generic category check failed at line %d\n", line);
std::exit(1);
}
#define REQUIRE(value) Require((value), __LINE__)
static void CheckMessage(Message message, const Category* category, int value, const char* expected) {
constexpr std::uint64_t marker = 0xA55AA55AA55AA55Aull;
struct Guarded { std::uint64_t before; GuestLocale::String result; std::uint64_t after; };
Guarded guarded {marker, {}, marker};
std::memset(&guarded.result, 0xcd, sizeof(guarded.result));
*__error_nid_postfix() = 61;
REQUIRE(message(&guarded.result, category, value) == &guarded.result);
REQUIRE(*__error_nid_postfix() == 61);
REQUIRE(guarded.before == marker && guarded.after == marker);
const auto length = std::strlen(expected);
REQUIRE(guarded.result.reserved == 0 && guarded.result.size == length);
REQUIRE(guarded.result.capacity >= length && (guarded.result.capacity & 15) == 15);
const char* text = guarded.result.capacity <= 15 ? guarded.result.buffer : guarded.result.pointer;
REQUIRE(std::memcmp(text, expected, length + 1) == 0);
REQUIRE((guarded.result.capacity <= 15) == (length <= 15));
if (length > 15) _ZdlPv_nid_postfix(guarded.result.pointer);
}
int main() {
const std::array<void*, 10> api {};
ApplicationHeapRegister_nid_no_patch(api.data());
const Category* category = _ZSt16generic_categoryv_nid_postfix();
REQUIRE(category && category == _ZSt16generic_categoryv_nid_postfix());
REQUIRE(std::strcmp(reinterpret_cast<Name>(category->vtable[2])(category), "generic") == 0);
const auto message = reinterpret_cast<Message>(category->vtable[3]);
const auto condition = reinterpret_cast<Condition>(category->vtable[4])(category, 35);
REQUIRE(condition.value == 35 && condition.category == category);
struct Expected { int value; const char* text; };
const Expected expected[] {
{3, "no such process"}, {14, "bad address"}, {13, "permission denied"},
{25, "inappropriate io control operation"}, {35, "operation would block"},
{45, "not supported"}, {47, "address family not supported"}, {60, "timed out"},
{78, "function not supported"}, {95, "state not recoverable"}, {96, "owner dead"}
};
for (const auto& error : expected) {
*__error_nid_postfix() = 61;
const char* mapped = _ZSt13_Syserror_mapi_nid_postfix(error.value);
REQUIRE(mapped && std::strcmp(mapped, error.text) == 0 && *__error_nid_postfix() == 61);
CheckMessage(message, category, error.value, error.text);
CheckMessage(_ZNKSt23_Generic_error_category7messageEi_nid_postfix, category, error.value, error.text);
}
unsigned known = 0;
for (int value = 1; value <= 96; ++value) {
const char* mapped = _ZSt13_Syserror_mapi_nid_postfix(value);
if (mapped) ++known;
CheckMessage(message, category, value, mapped ? mapped : "unknown error");
}
REQUIRE(known == 72);
for (int value : {std::numeric_limits<int>::min(), -1, 0, 15, 44, 97, std::numeric_limits<int>::max()}) {
*__error_nid_postfix() = 61;
REQUIRE(_ZSt13_Syserror_mapi_nid_postfix(value) == nullptr && *__error_nid_postfix() == 61);
CheckMessage(message, category, value, "unknown error");
}
GuestLocale::String first {}, second {};
REQUIRE(message(&first, category, 25) == &first && message(&second, category, 11) == &second);
REQUIRE(first.pointer != second.pointer);
_ZdlPv_nid_postfix(second.pointer);
REQUIRE(std::strcmp(first.pointer, "inappropriate io control operation") == 0);
_ZdlPv_nid_postfix(first.pointer);
bool rejected = false;
try { message(nullptr, category, 22); }
catch (const std::invalid_argument&) { rejected = true; }
REQUIRE(rejected);
std::puts("guest generic category, FreeBSD error map and message ownership: PASS");
}

View File

@ -62,4 +62,4 @@
- [daemon](../../../core/libs/prx/libc/src/Process.cpp) (libc) throws: forking into the background is not supported. PPSA85300 imports it and fails to load without it. VLC 3.0 calls it only when its `daemon` option is set ([libvlc.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/libvlc.c#L190-L197)), and the title starts libVLC with `--ignore-config` and without `--daemon` ([player.cc](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/src/player.cc#L960-L981))
- [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert
- [flock](../../../core/libs/prx/libc/src/FileLock.cpp) (libkernel) on Windows shares one lock state between duplicated descriptors, following FreeBSD 11.4's [flock(2)](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/sys/flock.2). Independent opens have separate states; closing a descriptor or reopening a stream drops only its reference. Lock transitions on one open file description are serialized. The host's [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) locks also restrict ordinary reads and writes through other handles, unlike FreeBSD's advisory locks, and do not restrict mapped views. Console behavior, signal interruption and concurrent closure of a descriptor while its lock call is pending are unverified.
- [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libSceAmpr) follows Dinkumware's `thread0.cpp` as shipped with [Visual C++ 2013](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/thread0.cpp) and in [microsoft/STL](https://github.com/microsoft/STL/blob/da52dc0fcadbd83690cfd1ed32408d2847439493/stl/src/thread0.cpp), without console verification. `_Cpp_error` codes 0 to 6 throw a guest `std::system_error` whose `error_code` holds the FreeBSD errno of the `errc` value (EBUSY 16, EINVAL 22, ESRCH 3, ENOMEM 12, EPERM 1, EDEADLK 11, EAGAIN 35) and the generic category, and whose `what()` is the table message, `": "` and the generic category's message for that errno, taken to be the same text as in VC12's [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/syserror.cpp); another code throws `std::out_of_range` on the host instead of reading past the table. The guest `system_error` is taken to be [VC12's](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/system_error): the 16-byte `runtime_error` followed by `error_code {int, const error_category*}`, 32 bytes, so its copy constructor and assignment copy the code too. The generic category is a static libc object with VC12's `error_category` virtual order (destructors, `name`, `message`, `default_error_condition`, the two `equivalent`), not the console's `_Error_objects<int>::_Generic_object`, and no `generic_category()` export returns it, so it compares unequal to a title's own `generic_category()`. Its `message` throws, as the guest `std::string` layout is not modelled, and so does its deleting destructor
- [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libSceAmpr) follows Dinkumware's `thread0.cpp` as shipped with [Visual C++ 2013](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/thread0.cpp) and in [microsoft/STL](https://github.com/microsoft/STL/blob/da52dc0fcadbd83690cfd1ed32408d2847439493/stl/src/thread0.cpp), without console verification. `_Cpp_error` codes 0 to 6 throw a guest `std::system_error` whose `error_code` holds the FreeBSD errno of the `errc` value (EBUSY 16, EINVAL 22, ESRCH 3, ENOMEM 12, EPERM 1, EDEADLK 11, EAGAIN 35) and the generic category, and whose `what()` is the table message, `": "` and the generic category's message for that errno, selected by the first matching errno entry in VC12's [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/syserror.cpp); another code throws `std::out_of_range` on the host instead of reading past the table. The guest `system_error` is taken to be [VC12's](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/system_error): the 16-byte `runtime_error` followed by `error_code {int, const error_category*}`, 32 bytes, so its copy constructor and assignment copy the code too. The generic category is a static libc object with VC12's `error_category` virtual order (destructors, `name`, `message`, `default_error_condition`, the two `equivalent`), not the console's `_Error_objects<int>::_Generic_object`, and the new `generic_category()` export returns that object; a title's inline category object in a bundled libc can still have a different identity. Its `message` and direct message export construct the existing [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) model: a reserved eight-byte word, 16-byte inline buffer, length and capacity, with guest `operator new` storage above 15 bytes. This 40-byte string layout and virtual return ABI have not been verified against a console library. The deleting destructor still throws. `_Syserror_map` covers the 72 distinct Dinkumware `errc` values present in FreeBSD 11.4 [errno.h](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h), returning null for other values; category messages use `unknown error` for those. First-match aliases are `EAGAIN`/`EWOULDBLOCK` -> `operation would block` and `ENOTSUP`/`EOPNOTSUPP` -> `not supported`. STREAMS-only errno assignments absent from that header (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are not inferred