Merge pull request #3568 from DariusHQ/feat/libkernel-sendfile
feat(libkernel): implement sendfile
This commit is contained in:
commit
57ba0e7bca
|
|
@ -720,6 +720,16 @@ if(BUILD_TESTING)
|
|||
set_tests_properties(guest_execvp PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
|
||||
endif()
|
||||
add_test_executable(guest_sendfile_tests ${LIBS_INCLUDE_DIR}/tests/GuestSendfile.cpp)
|
||||
target_include_directories(guest_sendfile_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_sendfile_tests PRIVATE libkernel libc)
|
||||
configure_windows_unwind(guest_sendfile_tests)
|
||||
add_test(NAME guest_sendfile COMMAND guest_sendfile_tests)
|
||||
set_tests_properties(guest_sendfile PROPERTIES TIMEOUT 60)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_sendfile PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
|
||||
endif()
|
||||
add_test_executable(guest_kernel_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestKernelErrors.cpp)
|
||||
target_include_directories(guest_kernel_error_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_kernel_error_tests PRIVATE libkernel libc)
|
||||
|
|
|
|||
|
|
@ -858,6 +858,86 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message,
|
|||
message->controlLength = 0;
|
||||
return received;
|
||||
}
|
||||
struct GuestSendfileHeaders {
|
||||
GuestIovec* headers;
|
||||
int headerCount;
|
||||
GuestIovec* trailers;
|
||||
int trailerCount;
|
||||
};
|
||||
static_assert(sizeof(GuestSendfileHeaders) == 32 && offsetof(GuestSendfileHeaders, trailers) == 16);
|
||||
std::int64_t APS5_VABI sceKernelPread(int descriptor, void* buffer, std::size_t length, std::int64_t offset);
|
||||
static bool SendfileIovecs(const GuestIovec* iov, int count) {
|
||||
if (!iov) return true;
|
||||
if (count < 0 || count > 1024) return false;
|
||||
std::uint64_t total = 0;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (iov[i].length > INT_MAX - total) return false;
|
||||
total += iov[i].length;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static int SendAll(const Socket& socket, const void* data, std::uint64_t length, std::int64_t& sent) {
|
||||
if (!data && length) return 14;
|
||||
const char* bytes = static_cast<const char*>(data);
|
||||
while (length) {
|
||||
const auto result = ::send(socket.value, bytes, static_cast<int>(std::min<std::uint64_t>(length, INT_MAX)), NativeSendFlags(GuestNoSignal));
|
||||
if (result < 0) return NativeError();
|
||||
bytes += result;
|
||||
length -= static_cast<std::uint64_t>(result);
|
||||
sent += result;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
static int SendIovecs(const Socket& socket, const GuestIovec* iov, int count, std::int64_t& sent) {
|
||||
for (int i = 0; iov && i < count; ++i)
|
||||
if (const int error = SendAll(socket, iov[i].base, iov[i].length, sent)) return error;
|
||||
return 0;
|
||||
}
|
||||
int APS5_VABI sendfile_nid_postfix(int file, int descriptor, std::int64_t offset, std::uint64_t length,
|
||||
const GuestSendfileHeaders* headers, std::int64_t* sbytes, int flags) {
|
||||
(void)flags;
|
||||
if (offset < 0) return Fail(22);
|
||||
if (headers && (!SendfileIovecs(headers->headers, headers->headerCount) || !SendfileIovecs(headers->trailers, headers->trailerCount)))
|
||||
return Fail(22);
|
||||
std::vector<char> chunk;
|
||||
try {
|
||||
chunk.resize(0x10000);
|
||||
} catch (const std::bad_alloc&) {
|
||||
return Fail(12);
|
||||
}
|
||||
const auto read = [&](std::int64_t position, std::uint64_t remaining) {
|
||||
const auto result = sceKernelPread(file, chunk.data(), static_cast<std::size_t>(std::min<std::uint64_t>(remaining, chunk.size())), position);
|
||||
return result < 0 ? -(result & 0xffff) : result;
|
||||
};
|
||||
std::uint64_t remaining = length ? length : UINT64_MAX;
|
||||
auto received = read(offset, remaining);
|
||||
if (received == -9) return Fail(9);
|
||||
std::int64_t sent = 0;
|
||||
const auto finish = [&](int error) {
|
||||
if (sbytes) *sbytes = sent;
|
||||
return error ? Fail(error) : 0;
|
||||
};
|
||||
if (received < 0) return finish(received == -21 || received == -29 ? 22 : static_cast<int>(-received));
|
||||
const auto socket = Lookup(descriptor);
|
||||
if (!socket) return finish(9);
|
||||
if (socket->type != 1) return finish(22);
|
||||
sockaddr_storage peer{};
|
||||
socklen_t size = sizeof(peer);
|
||||
if (::getpeername(socket->value, reinterpret_cast<sockaddr*>(&peer), &size)) return finish(57);
|
||||
if (headers)
|
||||
if (const int error = SendIovecs(*socket, headers->headers, headers->headerCount, sent)) return finish(error);
|
||||
while (received > 0) {
|
||||
if (const int error = SendAll(*socket, chunk.data(), static_cast<std::uint64_t>(received), sent)) return finish(error);
|
||||
offset += received;
|
||||
remaining -= static_cast<std::uint64_t>(received);
|
||||
if (remaining == 0) break;
|
||||
received = read(offset, remaining);
|
||||
if (received < 0) return finish(static_cast<int>(-received));
|
||||
}
|
||||
if (headers)
|
||||
if (const int error = SendIovecs(*socket, headers->trailers, headers->trailerCount, sent)) return finish(error);
|
||||
return finish(0);
|
||||
}
|
||||
int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t count, int timeout) {
|
||||
if (timeout < -1) return Fail(22);
|
||||
if (count && !descriptors) return Fail(14);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
struct Iovec {
|
||||
const void* base;
|
||||
std::uint64_t length;
|
||||
};
|
||||
struct Hdtr {
|
||||
const Iovec* headers;
|
||||
int headerCount;
|
||||
const Iovec* trailers;
|
||||
int trailerCount;
|
||||
};
|
||||
extern "C" {
|
||||
int APS5_VABI open_nid_postfix(const char*, int, int);
|
||||
int APS5_VABI close_nid_postfix(int);
|
||||
std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int);
|
||||
int APS5_VABI socket_nid_postfix(int, int, int);
|
||||
int APS5_VABI socketpair_nid_postfix(int, int, int, int*);
|
||||
std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int);
|
||||
int APS5_VABI ioctl_nid_postfix(int, std::uint64_t, void*);
|
||||
int APS5_VABI sendfile_nid_postfix(int, int, std::int64_t, std::uint64_t, const Hdtr*, std::int64_t*, int);
|
||||
int* APS5_VABI __error_nid_postfix();
|
||||
}
|
||||
static void Require(bool value) { if (!value) std::abort(); }
|
||||
static bool Fails(int result, int error) { return result == -1 && *__error_nid_postfix() == error; }
|
||||
static std::string Receive(int descriptor, std::size_t length) {
|
||||
std::string data(length, 0);
|
||||
std::size_t done = 0;
|
||||
while (done < length) {
|
||||
const auto result = recv_nid_postfix(descriptor, data.data() + done, length - done, 0);
|
||||
Require(result > 0);
|
||||
done += static_cast<std::size_t>(result);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
static std::string Content(std::size_t size) {
|
||||
std::string content(size, 0);
|
||||
for (std::size_t i = 0; i < size; ++i) content[i] = static_cast<char>(i * 7 % 251);
|
||||
return content;
|
||||
}
|
||||
static int OpenWritten(const char* path, const std::string& content) {
|
||||
{
|
||||
std::ofstream stream(path, std::ios::binary);
|
||||
stream.write(content.data(), static_cast<std::streamsize>(content.size()));
|
||||
}
|
||||
const int file = open_nid_postfix(path, 0, 0);
|
||||
Require(file >= 0);
|
||||
return file;
|
||||
}
|
||||
int main() {
|
||||
const std::string content = Content(200000);
|
||||
const auto size = static_cast<std::int64_t>(content.size());
|
||||
const int file = OpenWritten("sendfile_probe.bin", content);
|
||||
int pair[2];
|
||||
Require(socketpair_nid_postfix(1, 1, 0, pair) == 0);
|
||||
const Iovec header[] = {{"GET", 3}, {" ", 1}};
|
||||
const Iovec trailer[] = {{"END", 3}};
|
||||
const Hdtr parts{header, 2, trailer, 1};
|
||||
std::int64_t sent = -1;
|
||||
Require(sendfile_nid_postfix(file, pair[0], 5, 10, &parts, &sent, 0) == 0 && sent == 17);
|
||||
Require(Receive(pair[1], 17) == "GET " + content.substr(5, 10) + "END");
|
||||
Require(lseek_nid_postfix(file, 0, 1) == 0);
|
||||
std::string rest;
|
||||
std::thread reader([&] { rest = Receive(pair[1], content.size() - 100); });
|
||||
Require(sendfile_nid_postfix(file, pair[0], 100, 0, nullptr, &sent, 0) == 0 && sent == size - 100);
|
||||
reader.join();
|
||||
Require(rest == content.substr(100));
|
||||
Require(sendfile_nid_postfix(file, pair[0], size - 4, 1000, nullptr, &sent, 0) == 0 && sent == 4);
|
||||
Require(Receive(pair[1], 4) == content.substr(content.size() - 4));
|
||||
Require(sendfile_nid_postfix(file, pair[0], size + 10, 0, &parts, &sent, 0) == 0 && sent == 7);
|
||||
Require(Receive(pair[1], 7) == "GET END");
|
||||
const Hdtr nullHeaders{nullptr, 5, trailer, 1};
|
||||
Require(sendfile_nid_postfix(file, pair[0], 0, 3, &nullHeaders, nullptr, 0) == 0);
|
||||
Require(Receive(pair[1], 6) == content.substr(0, 3) + "END");
|
||||
Require(lseek_nid_postfix(file, 0, 1) == 0);
|
||||
|
||||
sent = 77;
|
||||
Require(Fails(sendfile_nid_postfix(file, pair[0], -1, 0, nullptr, &sent, 0), 22) && sent == 77);
|
||||
const std::vector<Iovec> many(1025, Iovec{"x", 1});
|
||||
const Hdtr tooMany{many.data(), 1025, nullptr, 0};
|
||||
Require(Fails(sendfile_nid_postfix(file, pair[0], 0, 0, &tooMany, &sent, 0), 22) && sent == 77);
|
||||
const Hdtr negative{nullptr, 0, trailer, -1};
|
||||
Require(Fails(sendfile_nid_postfix(file, pair[0], 0, 0, &negative, &sent, 0), 22) && sent == 77);
|
||||
Require(Fails(sendfile_nid_postfix(pair[1], pair[0], 0, 0, nullptr, &sent, 0), 22) && sent == 0);
|
||||
sent = 77;
|
||||
Require(Fails(sendfile_nid_postfix(file, file, 0, 0, nullptr, &sent, 0), 9) && sent == 0);
|
||||
const int datagram = socket_nid_postfix(2, 2, 0);
|
||||
Require(datagram >= 0);
|
||||
sent = 77;
|
||||
Require(Fails(sendfile_nid_postfix(file, datagram, 0, 0, nullptr, &sent, 0), 22) && sent == 0);
|
||||
const int unconnected = socket_nid_postfix(2, 1, 0);
|
||||
Require(unconnected >= 0);
|
||||
sent = 77;
|
||||
Require(Fails(sendfile_nid_postfix(file, unconnected, size + 10, 0, nullptr, &sent, 0), 57) && sent == 0);
|
||||
|
||||
const std::string large = Content(32 << 20);
|
||||
const int largeFile = OpenWritten("sendfile_large.bin", large);
|
||||
int on = 1;
|
||||
Require(ioctl_nid_postfix(pair[0], 0x8004667e, &on) == 0);
|
||||
sent = -1;
|
||||
Require(Fails(sendfile_nid_postfix(largeFile, pair[0], 0, 0, nullptr, &sent, 0), 35));
|
||||
Require(sent > 0 && sent < static_cast<std::int64_t>(large.size()));
|
||||
Require(Receive(pair[1], static_cast<std::size_t>(sent)) == large.substr(0, static_cast<std::size_t>(sent)));
|
||||
|
||||
Require(close_nid_postfix(largeFile) == 0 && close_nid_postfix(file) == 0);
|
||||
sent = 77;
|
||||
Require(Fails(sendfile_nid_postfix(file, pair[0], 0, 0, nullptr, &sent, 0), 9) && sent == 77);
|
||||
Require(close_nid_postfix(datagram) == 0 && close_nid_postfix(unconnected) == 0);
|
||||
Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0);
|
||||
std::filesystem::remove("sendfile_probe.bin");
|
||||
std::filesystem::remove("sendfile_large.bin");
|
||||
}
|
||||
|
|
@ -609,6 +609,7 @@ Throughout the project, every function at every stage either **does exactly what
|
|||
- [kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported
|
||||
- [sceKernelFcntl](../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) handles socket descriptors through the POSIX `fcntl` export (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`) and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use; the command semantics follow FreeBSD's [fcntl](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_descrip.c), and a closed descriptor returns `SCE_KERNEL_ERROR_EBADF`. File descriptors throw: their open flags are not tracked, and `F_DUPFD`, `F_GETFD` and `F_SETFD` are unsupported on all descriptors. The return values and error codes are unverified on a console
|
||||
- [ioctl, _ioctl](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket. Any other request on a socket fails with `ENOTTY`, and any request on a file or pipe descriptor fails with `EBADF`, where FreeBSD answers `FIONBIO` and `FIONREAD` there too and fails other requests, termios ones included, with `ENOTTY`. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests
|
||||
- [sendfile](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A socket as the file fails with `EINVAL`, as on FreeBSD, but any other descriptor `sceKernelPread` reads, such as `/dev/urandom`, is sent where FreeBSD requires a regular file or shared memory, and a file descriptor as the socket fails with `EBADF`, as for `send`, where FreeBSD answers `ENOTSOCK`. `flags` are ignored: reads complete before sending, so `SF_NODISKIO` never fails with `EBUSY`, and `SF_SYNC` returns once the data is queued on the socket rather than sent. PS5MC (PPSA99420) imports it
|
||||
- [pthread_kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. Signal 0 reports any other thread handle as existing, without checking that the thread is still alive. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread throws, since guest signals are not delivered to a chosen thread
|
||||
- [select](../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) validates `tv_usec` but saturates a valid timeout beyond the remaining `steady_clock` range at `time_point::max()`, so the wait can continue effectively indefinitely. The console's behavior for a valid timeout outside the host clock's range is unknown.
|
||||
- [AJM](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp), [APR](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) and [NGS2](../../core/libs/prx/libSceNgs2.native/src/System.cpp) state is allocated with `new` and never freed on purpose: title threads still call into these libraries while `std::exit` runs static destructors, so ordinary statics would be destroyed under them. Keep them leaked; `guest_ajm_teardown` covers the AJM case on Unix.
|
||||
|
|
|
|||
Loading…
Reference in New Issue