diff --git a/AGENTS.md b/AGENTS.md index cb77e1c..a65aa35 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,12 +81,12 @@ unreleased changelog, so update it before the PR is merged. ## Tool availability and Lima - When a required development tool is unavailable on the host, use the - repository's `docker-udf-client` Lima VM instead of installing a host-wide + repository's `udf-runner-cpp-dev` Lima VM instead of installing a host-wide package. - Start the VM with - `limactl start ./ext/lima_vm_templates/docker-udf-client.yaml` and run - commands in it with `limactl shell docker-udf-client `. + `limactl start ./ext/lima_vm_templates/udf-runner-cpp-dev.yaml` and run + commands in it with `limactl shell udf-runner-cpp-dev `. - If a required tool is missing in the VM, install it there and add its package to the provisioning list in - [`ext/lima_vm_templates/docker-udf-client.yaml`](ext/lima_vm_templates/docker-udf-client.yaml) + [`ext/lima_vm_templates/udf-runner-cpp-dev.yaml`](ext/lima_vm_templates/udf-runner-cpp-dev.yaml) so future VMs provide it automatically. diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index be433b9..66cde3b 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -11,6 +11,7 @@ n/a - #70: Added the v2 UDF protocol design and validation artifacts - #61: Added stacktrace-aware exceptions and assertions to v2 - #54: Added v2 socket-library interface design + - #75: Added the v2 Unix-domain stream socket library - #32: Added clang tidy to v2 - #36: Added developer guide for clang-tidy and clang-format - #38: Added Sonar Qube Public diff --git a/doc/design/v2/runner/socket_library_design.md b/doc/design/v2/runner/socket_library_design.md index 32b621a..1ca2adf 100644 --- a/doc/design/v2/runner/socket_library_design.md +++ b/doc/design/v2/runner/socket_library_design.md @@ -34,9 +34,8 @@ core `Socket` abstraction: resolution, and transport-specific socket-option policy. - Timeouts, polling loops, and protocol framing. -The socket abstraction does not provide implicit unlinking of a filesystem path -before bind or during destruction, or thread safety -for concurrent mutation of an individual socket object. +The socket abstraction does not provide implicit unlinking before bind or +thread safety for concurrent mutation of an individual socket object. The library is Linux/POSIX-specific. This follows the v2 library's existing use of Linux readiness descriptors. @@ -47,8 +46,10 @@ Public types belong to `exasol::udf::v2::socket`. The implementation should prov small public header surface, for example: ``` -include/exasol/udf/v2/socket.hpp -include/exasol/udf/v2/unix_socket.hpp +include/exasol/udf/v2/socket/owned_file_descriptor.hpp +include/exasol/udf/v2/socket/socket.hpp +include/exasol/udf/v2/socket/unix_socket.hpp +include/exasol/udf/v2/socket/unix_socket_listener.hpp ``` The core header must not expose a TLS implementation or a TCP address type. @@ -62,12 +63,12 @@ This keeps the contract usable by TLS, whose I/O does not map one-for-one to ```cpp namespace exasol::udf::v2::socket { -enum class Shutdown { receive, send, both }; +enum class Shutdown { Receive, Send, Both }; class OwnedFileDescriptor { public: OwnedFileDescriptor() noexcept; - ~OwnedFileDescriptor(); + ~OwnedFileDescriptor() noexcept; // Takes ownership of a valid raw file descriptor. [[nodiscard]] static OwnedFileDescriptor adopt_native_handle(int owned_fd); @@ -199,7 +200,7 @@ public: class UnixSocketListener { public: UnixSocketListener() noexcept; - ~UnixSocketListener(); + ~UnixSocketListener() noexcept; UnixSocketListener(const UnixSocketListener&) = delete; UnixSocketListener& operator=(const UnixSocketListener&) = delete; @@ -214,7 +215,6 @@ public: void close() noexcept; [[nodiscard]] OwnedFileDescriptor release_native_handle() noexcept; [[nodiscard]] UnixSocket accept(); - void unlink_path(); }; } // namespace exasol::udf::v2::socket @@ -246,10 +246,12 @@ socket. `accept()` accepts exactly one connection and returns it as an owning, close-on-exec `UnixSocket`. In nonblocking mode it throws `std::system_error` for would-block, just like stream I/O. -The listener retains the path supplied at bind only to support `unlink_path()`. -That operation is explicit and must be called only after closing the listener -and only when the caller knows it still owns the pathname. Destruction and -`close()` close the descriptor but never unlink the pathname. +The listener owns the path after a successful bind. `close()` and destruction +close the descriptor and then remove the path with best-effort cleanup. If +`bind()` succeeds but `listen()` fails, an internal scope guard removes the +newly created path before the exception leaves `bind()`. Releasing the native +descriptor also removes the path, leaving the released listener reachable only +through that descriptor. ## Future TCP and TLS diff --git a/doc/developer_guide/slc.md b/doc/developer_guide/slc.md index 096f1ab..587c199 100644 --- a/doc/developer_guide/slc.md +++ b/doc/developer_guide/slc.md @@ -14,8 +14,8 @@ If no Docker daemon is available on the host, use the repository’s Lima instance: ```bash -limactl start ./ext/lima_vm_templates/docker-udf-client.yaml -export DOCKER_HOST="$(limactl list docker-udf-client \ +limactl start ./ext/lima_vm_templates/udf-runner-cpp-dev.yaml +export DOCKER_HOST="$(limactl list udf-runner-cpp-dev \ --format 'unix://{{.Dir}}/sock/docker.sock')" ``` diff --git a/doc/developer_guide/v2/v2_code_quality.md b/doc/developer_guide/v2/v2_code_quality.md index 4daf696..523704d 100644 --- a/doc/developer_guide/v2/v2_code_quality.md +++ b/doc/developer_guide/v2/v2_code_quality.md @@ -86,7 +86,7 @@ alias( Mutation testing for the functional v2 C++ tests uses [Mull](https://mull-project.com/) with the pinned Mull 0.34.1 release and matching LLVM 22 toolchain. The -`docker-udf-client` Lima template provisions these tools automatically; on +`udf-runner-cpp-dev` Lima template provisions these tools automatically; on other environments install the LLVM 22 compiler and `mull-22`, then verify that `mull-runner-22` and `/usr/lib/mull-ir-frontend-22` are available. diff --git a/ext/lima_vm_templates/docker-udf-client.yaml b/ext/lima_vm_templates/udf-runner-cpp-dev.yaml similarity index 94% rename from ext/lima_vm_templates/docker-udf-client.yaml rename to ext/lima_vm_templates/udf-runner-cpp-dev.yaml index 62e744b..3d448fb 100644 --- a/ext/lima_vm_templates/docker-udf-client.yaml +++ b/ext/lima_vm_templates/udf-runner-cpp-dev.yaml @@ -3,12 +3,12 @@ # Also, it sets all environment variables needed to build the UDF client for Python and Java # TODO: Add R dependencies # Start this VM when the host has no Docker daemon: -# $ limactl start ./ext/lima_vm_templates/docker-udf-client.yaml +# $ limactl start ./ext/lima_vm_templates/udf-runner-cpp-dev.yaml # # Connect Docker and exaslct to the Lima Docker daemon: -# $ export DOCKER_HOST=$(limactl list docker-udf-client --format 'unix://{{.Dir}}/sock/docker.sock') +# $ export DOCKER_HOST=$(limactl list udf-runner-cpp-dev --format 'unix://{{.Dir}}/sock/docker.sock') # cd ./exaudfclient -# $ limactl shell docker-udf-client bazel build --config no-tty --config optimize --config python --config fast-binary +# $ limactl shell udf-runner-cpp-dev bazel build --config no-tty --config optimize --config python --config fast-binary # To build the SLC Docker images/stages only, run # $ exaslct build --flavor-path ./flavors/ diff --git a/udf-runner-cpp/v2/BUILD.bazel b/udf-runner-cpp/v2/BUILD.bazel index fbca2d0..b4fb5de 100644 --- a/udf-runner-cpp/v2/BUILD.bazel +++ b/udf-runner-cpp/v2/BUILD.bazel @@ -355,6 +355,62 @@ cc_library( target_compatible_with = ["@platforms//os:linux"], ) +cc_library( + name = "socket", + srcs = [ + "socket/owned_file_descriptor.cc", + "socket/socket.cc", + "socket/unix_socket.cc", + "socket/unix_socket_detail.cc", + "socket/unix_socket_listener.cc", + ], + hdrs = [ + "include/exasol/udf/v2/socket/owned_file_descriptor.hpp", + "include/exasol/udf/v2/socket/socket.hpp", + "include/exasol/udf/v2/socket/unix_socket.hpp", + "include/exasol/udf/v2/socket/unix_socket_listener.hpp", + "socket/unix_socket_detail.hpp", + ], + includes = ["include"], + copts = ["-std=c++23"], + target_compatible_with = ["@platforms//os:linux"], +) + +cc_test( + name = "owned_file_descriptor_test", + srcs = ["socket/owned_file_descriptor_test.cc"], + copts = ["-std=c++23"], + # Exclude this target from Mull until its mutation coverage is expanded. + tags = ["no-mull"], + deps = [ + ":socket", + "@googletest//:gtest_main", + ], + target_compatible_with = ["@platforms//os:linux"], +) + +cc_test( + name = "unix_socket_test", + srcs = ["socket/unix_socket_test.cc"], + copts = ["-std=c++23"], + tags = ["no-mull"], + deps = [ + ":socket", + "@googletest//:gtest_main", + ], + target_compatible_with = ["@platforms//os:linux"], +) + +cc_fuzz_test( + name = "socket_fuzz_test", + srcs = ["socket/socket_fuzz_test.cc"], + corpus = glob(["fuzz/corpus/socket/**"]), + copts = ["-std=c++23"], + deps = [":socket"], + tags = ["fuzz-test"], + target_compatible_with = ["@platforms//os:linux"], +) + cc_library( name = "linux_event_fd", srcs = ["linux_event_fd.cc"], diff --git a/udf-runner-cpp/v2/fuzz/corpus/socket/binary_bytes b/udf-runner-cpp/v2/fuzz/corpus/socket/binary_bytes new file mode 100644 index 0000000..5fe5579 Binary files /dev/null and b/udf-runner-cpp/v2/fuzz/corpus/socket/binary_bytes differ diff --git a/udf-runner-cpp/v2/fuzz/corpus/socket/empty b/udf-runner-cpp/v2/fuzz/corpus/socket/empty new file mode 100644 index 0000000..e69de29 diff --git a/udf-runner-cpp/v2/fuzz/corpus/socket/exact_4k b/udf-runner-cpp/v2/fuzz/corpus/socket/exact_4k new file mode 100644 index 0000000..9d235ed --- /dev/null +++ b/udf-runner-cpp/v2/fuzz/corpus/socket/exact_4k @@ -0,0 +1 @@ +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ No newline at end of file diff --git a/udf-runner-cpp/v2/fuzz/corpus/socket/more_than_4k b/udf-runner-cpp/v2/fuzz/corpus/socket/more_than_4k new file mode 100644 index 0000000..7c1c939 --- /dev/null +++ b/udf-runner-cpp/v2/fuzz/corpus/socket/more_than_4k @@ -0,0 +1 @@ +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ No newline at end of file diff --git a/udf-runner-cpp/v2/fuzz/corpus/socket/one_byte b/udf-runner-cpp/v2/fuzz/corpus/socket/one_byte new file mode 100644 index 0000000..4d92977 --- /dev/null +++ b/udf-runner-cpp/v2/fuzz/corpus/socket/one_byte @@ -0,0 +1,2 @@ +e + diff --git a/udf-runner-cpp/v2/fuzz/corpus/socket/only_ascii b/udf-runner-cpp/v2/fuzz/corpus/socket/only_ascii new file mode 100644 index 0000000..842b729 --- /dev/null +++ b/udf-runner-cpp/v2/fuzz/corpus/socket/only_ascii @@ -0,0 +1 @@ +this is for testing ASCII only content diff --git a/udf-runner-cpp/v2/include/exasol/udf/v2/socket/owned_file_descriptor.hpp b/udf-runner-cpp/v2/include/exasol/udf/v2/socket/owned_file_descriptor.hpp new file mode 100644 index 0000000..028ae93 --- /dev/null +++ b/udf-runner-cpp/v2/include/exasol/udf/v2/socket/owned_file_descriptor.hpp @@ -0,0 +1,45 @@ +#pragma once + +namespace exasol::udf::v2::socket +{ + +/** Owns a native file descriptor and closes it on destruction. */ +class OwnedFileDescriptor +{ +public: + /** Creates an empty descriptor that does not own a native handle. */ + OwnedFileDescriptor() noexcept; + + /** Closes the owned descriptor, if any. */ + ~OwnedFileDescriptor() noexcept; + + /** Takes ownership of an open native file descriptor. + * + * @throws std::invalid_argument if @p owned_fd is negative. + */ + [[nodiscard]] static OwnedFileDescriptor adopt_native_handle(int owned_fd); + + OwnedFileDescriptor(const OwnedFileDescriptor&) = delete; + OwnedFileDescriptor& operator=(const OwnedFileDescriptor&) = delete; + OwnedFileDescriptor(OwnedFileDescriptor&& other) noexcept; + OwnedFileDescriptor& operator=(OwnedFileDescriptor&& other) noexcept; + + /** Returns the owned native descriptor, or -1 when empty. */ + [[nodiscard]] int native_handle() const noexcept; + + /** Returns whether this object owns a native descriptor. */ + [[nodiscard]] bool is_open() const noexcept; + + /** Closes the owned descriptor and becomes empty. */ + void close() noexcept; + + /** Releases ownership and returns the native descriptor without closing it. */ + [[nodiscard]] int release_native_handle() noexcept; + +private: + explicit OwnedFileDescriptor(int owned_fd) noexcept; + + int file_descriptor = -1; +}; + +} // namespace exasol::udf::v2::socket diff --git a/udf-runner-cpp/v2/include/exasol/udf/v2/socket/socket.hpp b/udf-runner-cpp/v2/include/exasol/udf/v2/socket/socket.hpp new file mode 100644 index 0000000..bb3f581 --- /dev/null +++ b/udf-runner-cpp/v2/include/exasol/udf/v2/socket/socket.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include +#include + +#include + +namespace exasol::udf::v2::socket +{ + +/** Direction used when shutting down part or all of a socket connection. */ +enum class Shutdown +{ + Receive, + Send, + Both, +}; + +/** Common interface for owned stream sockets. */ +class Socket +{ +public: + /** Destroys the socket and releases its native descriptor. */ + virtual ~Socket() = default; + + Socket(const Socket&) = delete; + Socket& operator=(const Socket&) = delete; + + /** Returns the native socket descriptor, or -1 when closed. */ + [[nodiscard]] virtual int native_handle() const noexcept = 0; + + /** Returns whether the socket owns an open native descriptor. */ + [[nodiscard]] virtual bool is_open() const noexcept = 0; + + /** Closes the socket. This operation is idempotent. */ + virtual void close() noexcept = 0; + + /** Releases the native descriptor without closing it. */ + [[nodiscard]] virtual OwnedFileDescriptor release_native_handle() noexcept = 0; + + /** Shuts down the selected direction of the connection. + * + * @throws std::system_error if the operating-system shutdown fails. + */ + virtual void shutdown(Shutdown how) = 0; + + /** Reads up to the capacity described by the buffers. + * + * A successful call may read fewer bytes than requested. A return value of + * zero indicates end-of-file. + */ + virtual std::size_t read_some(std::span> buffers) = 0; + + /** Writes up to the size described by the buffers. + * + * A successful call may write fewer bytes than requested. + */ + virtual std::size_t write_some(std::span> buffers) = 0; + + /** Reads into one contiguous buffer. */ + std::size_t read_some(std::span buffer); + + /** Writes from one contiguous buffer. */ + std::size_t write_some(std::span buffer); + +protected: + Socket() = default; + Socket(Socket&&) = default; + Socket& operator=(Socket&&) = default; +}; + +} // namespace exasol::udf::v2::socket diff --git a/udf-runner-cpp/v2/include/exasol/udf/v2/socket/unix_socket.hpp b/udf-runner-cpp/v2/include/exasol/udf/v2/socket/unix_socket.hpp new file mode 100644 index 0000000..c8ea790 --- /dev/null +++ b/udf-runner-cpp/v2/include/exasol/udf/v2/socket/unix_socket.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include + +#include + +namespace exasol::udf::v2::socket +{ + +/** Owns one connected Unix-domain stream socket. */ +class UnixSocket final : public Socket +{ +public: + using Socket::read_some; + using Socket::write_some; + + UnixSocket() noexcept; + ~UnixSocket() override; + + UnixSocket(const UnixSocket&) = delete; + UnixSocket& operator=(const UnixSocket&) = delete; + UnixSocket(UnixSocket&& other) noexcept; + UnixSocket& operator=(UnixSocket&& other) noexcept; + + /** Connects to a Unix-domain socket at @p path. + * + * @throws std::system_error if socket creation or connection fails. + * @throws std::invalid_argument if @p path is not a valid Unix socket path. + */ + [[nodiscard]] static UnixSocket connect(const std::filesystem::path& path); + + /** Takes ownership of an open Unix stream socket descriptor. + * + * @throws std::system_error if @p owned_fd is closed or is not a Unix stream socket. + */ + [[nodiscard]] static UnixSocket adopt_native_handle(OwnedFileDescriptor owned_fd); + + [[nodiscard]] int native_handle() const noexcept override; + [[nodiscard]] bool is_open() const noexcept override; + void close() noexcept override; + [[nodiscard]] OwnedFileDescriptor release_native_handle() noexcept override; + void shutdown(Shutdown how) override; + std::size_t read_some(std::span> buffers) override; + std::size_t write_some(std::span> buffers) override; + +private: + explicit UnixSocket(OwnedFileDescriptor owned_fd) noexcept; + + OwnedFileDescriptor file_descriptor; +}; + +} // namespace exasol::udf::v2::socket diff --git a/udf-runner-cpp/v2/include/exasol/udf/v2/socket/unix_socket_listener.hpp b/udf-runner-cpp/v2/include/exasol/udf/v2/socket/unix_socket_listener.hpp new file mode 100644 index 0000000..18a2373 --- /dev/null +++ b/udf-runner-cpp/v2/include/exasol/udf/v2/socket/unix_socket_listener.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include + +#include + +#include + +namespace exasol::udf::v2::socket +{ + +/** Owns a Unix-domain stream listener. */ +class UnixSocketListener +{ +public: + UnixSocketListener() noexcept; + ~UnixSocketListener() noexcept; + + UnixSocketListener(const UnixSocketListener&) = delete; + UnixSocketListener& operator=(const UnixSocketListener&) = delete; + UnixSocketListener(UnixSocketListener&& other) noexcept; + UnixSocketListener& operator=(UnixSocketListener&& other) noexcept; + + /** Creates a listener at @p path and starts listening for connections. + * + * @param path Filesystem path for the Unix socket. + * @param backlog Maximum number of pending connections. + * @throws std::system_error if socket creation, binding, or listening fails. + * @throws std::invalid_argument if @p path is not a valid Unix socket path. + */ + [[nodiscard]] static UnixSocketListener bind(const std::filesystem::path& path, + int backlog = SOMAXCONN); + + /** Returns the native listener descriptor, or -1 when closed. */ + [[nodiscard]] int native_handle() const noexcept; + + /** Returns whether the listener owns an open native descriptor. */ + [[nodiscard]] bool is_open() const noexcept; + + /** Closes the listener and removes its filesystem path. */ + void close() noexcept; + + /** Releases the native descriptor and removes the listener's filesystem path. */ + [[nodiscard]] OwnedFileDescriptor release_native_handle() noexcept; + + /** Accepts one connection from the listener. + * + * @param flags Zero or a bitwise combination of SOCK_NONBLOCK and SOCK_CLOEXEC. + */ + [[nodiscard]] UnixSocket accept(int flags = SOCK_CLOEXEC) const; + +private: + UnixSocketListener(OwnedFileDescriptor owned_fd, std::filesystem::path path) noexcept; + void unlink_path() noexcept; + + OwnedFileDescriptor file_descriptor; + std::filesystem::path bound_path; +}; + +} // namespace exasol::udf::v2::socket diff --git a/udf-runner-cpp/v2/socket/owned_file_descriptor.cc b/udf-runner-cpp/v2/socket/owned_file_descriptor.cc new file mode 100644 index 0000000..bb864b9 --- /dev/null +++ b/udf-runner-cpp/v2/socket/owned_file_descriptor.cc @@ -0,0 +1,69 @@ +#include + +#include +#include +#include + +namespace exasol::udf::v2::socket +{ + +OwnedFileDescriptor::OwnedFileDescriptor() noexcept = default; + +OwnedFileDescriptor::~OwnedFileDescriptor() noexcept +{ + this->close(); +} + +OwnedFileDescriptor OwnedFileDescriptor::adopt_native_handle(const int owned_fd) +{ + if (owned_fd < 0) + { + throw std::invalid_argument("adopt invalid file descriptor"); + } + return OwnedFileDescriptor(owned_fd); +} + +OwnedFileDescriptor::OwnedFileDescriptor(OwnedFileDescriptor&& other) noexcept + : file_descriptor(std::exchange(other.file_descriptor, -1)) +{ +} + +OwnedFileDescriptor& OwnedFileDescriptor::operator=(OwnedFileDescriptor&& other) noexcept +{ + if (this != &other) + { + this->close(); + this->file_descriptor = std::exchange(other.file_descriptor, -1); + } + return *this; +} + +int OwnedFileDescriptor::native_handle() const noexcept +{ + return this->file_descriptor; +} + +bool OwnedFileDescriptor::is_open() const noexcept +{ + return this->file_descriptor != -1; +} + +void OwnedFileDescriptor::close() noexcept +{ + const int closed_fd = std::exchange(this->file_descriptor, -1); + if (closed_fd != -1) + { + ::close(closed_fd); + } +} + +int OwnedFileDescriptor::release_native_handle() noexcept +{ + return std::exchange(this->file_descriptor, -1); +} + +OwnedFileDescriptor::OwnedFileDescriptor(const int owned_fd) noexcept : file_descriptor(owned_fd) +{ +} + +} // namespace exasol::udf::v2::socket diff --git a/udf-runner-cpp/v2/socket/owned_file_descriptor_test.cc b/udf-runner-cpp/v2/socket/owned_file_descriptor_test.cc new file mode 100644 index 0000000..d8c5b3f --- /dev/null +++ b/udf-runner-cpp/v2/socket/owned_file_descriptor_test.cc @@ -0,0 +1,69 @@ +#include + +#include +#include +#include +#include + +#include + +#include + +namespace +{ + +using exasol::udf::v2::socket::OwnedFileDescriptor; + +TEST(OwnedFileDescriptorTest, ClosesExactlyOnceAfterMoveAndRelease) +{ + std::array pipe_fds{}; + ASSERT_EQ(::pipe(pipe_fds.data()), 0); + OwnedFileDescriptor descriptor = OwnedFileDescriptor::adopt_native_handle(pipe_fds[0]); + OwnedFileDescriptor moved(std::move(descriptor)); + // NOLINTBEGIN(bugprone-use-after-move, clang-analyzer-cplusplus.Move): Inspecting the + // specified moved-from state is intentional. + EXPECT_EQ(descriptor.release_native_handle(), -1); // NOSONAR + // NOLINTEND(bugprone-use-after-move, clang-analyzer-cplusplus.Move) + const int released_fd = moved.release_native_handle(); + EXPECT_FALSE(moved.is_open()); + ASSERT_EQ(::close(released_fd), 0); + EXPECT_EQ(::close(released_fd), -1); + EXPECT_EQ(errno, EBADF); + ASSERT_EQ(::close(pipe_fds[1]), 0); +} + +TEST(OwnedFileDescriptorTest, RejectsInvalidDescriptorAdoption) +{ + EXPECT_THROW(static_cast(OwnedFileDescriptor::adopt_native_handle(-1)), + std::invalid_argument); +} + +TEST(OwnedFileDescriptorTest, DefaultDescriptorIsClosed) +{ + const OwnedFileDescriptor descriptor; + EXPECT_FALSE(descriptor.is_open()); + EXPECT_EQ(descriptor.native_handle(), -1); +} + +TEST(OwnedFileDescriptorTest, MoveAssignmentClosesReplacedDescriptor) +{ + std::array first_pipe{}; + std::array second_pipe{}; + ASSERT_EQ(::pipe(first_pipe.data()), 0); + ASSERT_EQ(::pipe(second_pipe.data()), 0); + OwnedFileDescriptor source = OwnedFileDescriptor::adopt_native_handle(first_pipe[0]); + OwnedFileDescriptor destination = OwnedFileDescriptor::adopt_native_handle(second_pipe[0]); + destination = std::move(source); + // NOLINTBEGIN(bugprone-use-after-move, clang-analyzer-cplusplus.Move): Inspecting the + // specified moved-from state is intentional. + EXPECT_EQ(source.release_native_handle(), -1); // NOSONAR + // NOLINTEND(bugprone-use-after-move, clang-analyzer-cplusplus.Move) + EXPECT_EQ(destination.native_handle(), first_pipe[0]); + EXPECT_EQ(::close(second_pipe[0]), -1); + EXPECT_EQ(errno, EBADF); + ASSERT_EQ(::close(destination.release_native_handle()), 0); + ASSERT_EQ(::close(first_pipe[1]), 0); + ASSERT_EQ(::close(second_pipe[1]), 0); +} + +} // namespace diff --git a/udf-runner-cpp/v2/socket/socket.cc b/udf-runner-cpp/v2/socket/socket.cc new file mode 100644 index 0000000..6111511 --- /dev/null +++ b/udf-runner-cpp/v2/socket/socket.cc @@ -0,0 +1,20 @@ +#include + +#include + +namespace exasol::udf::v2::socket +{ + +std::size_t Socket::read_some(const std::span buffer) +{ + const std::array buffers{buffer}; + return read_some(buffers); +} + +std::size_t Socket::write_some(const std::span buffer) +{ + const std::array buffers{buffer}; + return write_some(buffers); +} + +} // namespace exasol::udf::v2::socket diff --git a/udf-runner-cpp/v2/socket/socket_fuzz_test.cc b/udf-runner-cpp/v2/socket/socket_fuzz_test.cc new file mode 100644 index 0000000..db3e204 --- /dev/null +++ b/udf-runner-cpp/v2/socket/socket_fuzz_test.cc @@ -0,0 +1,53 @@ +#include + +#include + +#include +#include +#include +#include + +/** + * Exercises vectored Unix-socket writes and matching peer reads using arbitrary fuzzer input. + * + * Inputs larger than the fixed 4 KiB receive buffer are ignored. The socket API permits partial + * writes, so the peer reads exactly the byte count reported by write_some(), rather than the + * entire fuzzer input. A socketpair creation failure is an infrastructure failure and aborts the + * fuzzing run. + */ +extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t* data, const std::size_t size) +{ + if (size > 4096) + { + return 0; + } + std::array descriptors{}; + if (::socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, descriptors.data()) != 0) + { + __builtin_trap(); + } + auto writer = exasol::udf::v2::socket::UnixSocket::adopt_native_handle( + exasol::udf::v2::socket::OwnedFileDescriptor::adopt_native_handle(descriptors[0])); + auto reader = exasol::udf::v2::socket::UnixSocket::adopt_native_handle( + exasol::udf::v2::socket::OwnedFileDescriptor::adopt_native_handle(descriptors[1])); + const std::span bytes(reinterpret_cast(data), size); + const std::size_t written = + writer.write_some(std::array{bytes.first(size / 2), bytes.subspan(size / 2)}); + std::array received{}; + std::size_t total_read = 0; + while (total_read < written) + { + const std::size_t read = + reader.read_some(std::span(received).subspan(total_read, written - total_read)); + if (read == 0) + { + __builtin_trap(); + } + total_read += read; + } + if (total_read != written) + { + __builtin_trap(); + } + return 0; +} diff --git a/udf-runner-cpp/v2/socket/unix_socket.cc b/udf-runner-cpp/v2/socket/unix_socket.cc new file mode 100644 index 0000000..0aac652 --- /dev/null +++ b/udf-runner-cpp/v2/socket/unix_socket.cc @@ -0,0 +1,217 @@ +#include + +#include "unix_socket_detail.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace exasol::udf::v2::socket +{ + +namespace +{ + + class InvalidShutdownDirection final : public std::logic_error + { + public: + using std::logic_error::logic_error; + }; + + void check_unix_stream_socket(const int file_descriptor) + { + int domain = 0; + if (socklen_t domain_length = sizeof(domain); + ::getsockopt(file_descriptor, SOL_SOCKET, SO_DOMAIN, &domain, &domain_length) == -1) + { + throw std::system_error(errno, std::generic_category(), "inspect socket domain"); + } + + int type = 0; + if (socklen_t type_length = sizeof(type); + ::getsockopt(file_descriptor, SOL_SOCKET, SO_TYPE, &type, &type_length) == -1) + { + throw std::system_error(errno, std::generic_category(), "inspect socket type"); + } + if (domain != AF_UNIX || type != SOCK_STREAM) + { + throw std::system_error(EPROTOTYPE, std::generic_category(), + "expected Unix stream socket"); + } + } + + template + std::vector make_iovecs(const std::span buffers) + { + if (std::size(buffers) > IOV_MAX) + { + throw std::system_error(EINVAL, std::generic_category(), "too many I/O buffers"); + } + + std::size_t total_size = 0; + std::vector iovecs; + iovecs.reserve(std::size(buffers)); + for (const Buffer buffer : std::span(buffers)) + { + if (std::size(buffer) > + static_cast(std::numeric_limits::max()) - total_size) + { + throw std::system_error(EINVAL, std::generic_category(), + "I/O buffer size is too large"); + } + total_size += std::size(buffer); + // POSIX declares iovec::iov_base as void* even for sendmsg(), which does not mutate it. + iovecs.push_back( + {const_cast(std::data(buffer)), std::size(buffer)}); // NOSONAR + } + return iovecs; + } + + int shutdown_direction(const Shutdown how) + { + using enum Shutdown; + switch (how) + { + case Receive: + return SHUT_RD; + case Send: + return SHUT_WR; + case Both: + return SHUT_RDWR; + } + throw InvalidShutdownDirection("invalid socket shutdown direction"); + } + +} // namespace + +UnixSocket::UnixSocket() noexcept = default; + +UnixSocket::~UnixSocket() +{ + close(); +} + +UnixSocket::UnixSocket(UnixSocket&& other) noexcept = default; + +UnixSocket& UnixSocket::operator=(UnixSocket&& other) noexcept = default; + +UnixSocket UnixSocket::connect(const std::filesystem::path& path) +{ + socklen_t address_length = 0; + const sockaddr_un address = detail::make_socket_address(path, address_length); + OwnedFileDescriptor socket_fd = detail::create_socket(); + while (::connect(socket_fd.native_handle(), detail::as_socket_address(address), + address_length) == -1) + { + if (errno != EINTR) + { + const int error = errno; + throw std::system_error(error, std::generic_category(), + "connect Unix socket '" + path.string() + "'"); + } + } + return UnixSocket(std::move(socket_fd)); +} + +UnixSocket UnixSocket::adopt_native_handle(OwnedFileDescriptor owned_fd) +{ + if (!owned_fd.is_open()) + { + throw std::system_error(EBADF, std::generic_category(), "adopt closed socket"); + } + check_unix_stream_socket(owned_fd.native_handle()); + return UnixSocket(std::move(owned_fd)); +} + +int UnixSocket::native_handle() const noexcept +{ + return file_descriptor.native_handle(); +} + +bool UnixSocket::is_open() const noexcept +{ + return file_descriptor.is_open(); +} + +void UnixSocket::close() noexcept +{ + file_descriptor.close(); +} + +OwnedFileDescriptor UnixSocket::release_native_handle() noexcept +{ + const int released_fd = file_descriptor.release_native_handle(); + if (released_fd == -1) + { + return {}; + } + return OwnedFileDescriptor::adopt_native_handle(released_fd); +} + +void UnixSocket::shutdown(const Shutdown how) +{ + if (::shutdown(file_descriptor.native_handle(), shutdown_direction(how)) == -1) + { + throw std::system_error(errno, std::generic_category(), "shutdown Unix socket"); + } +} + +std::size_t UnixSocket::read_some(const std::span> buffers) +{ + std::vector iovecs = make_iovecs(buffers); + if (iovecs.empty()) + { + return 0; + } + + ssize_t result = -1; + do + { + result = ::readv(file_descriptor.native_handle(), iovecs.data(), + static_cast(iovecs.size())); + } while (result == -1 && errno == EINTR); + + if (result == -1) + { + throw std::system_error(errno, std::generic_category(), "read from Unix socket"); + } + return static_cast(result); +} + +std::size_t UnixSocket::write_some(const std::span> buffers) +{ + std::vector iovecs = make_iovecs(buffers); + if (iovecs.empty()) + { + return 0; + } + + msghdr message{}; + message.msg_iov = iovecs.data(); + message.msg_iovlen = iovecs.size(); + ssize_t result = -1; + do + { + result = ::sendmsg(file_descriptor.native_handle(), &message, MSG_NOSIGNAL); + } while (result == -1 && errno == EINTR); + + if (result == -1) + { + throw std::system_error(errno, std::generic_category(), "write to Unix socket"); + } + return static_cast(result); +} + +UnixSocket::UnixSocket(OwnedFileDescriptor owned_fd) noexcept : file_descriptor(std::move(owned_fd)) +{ +} + +} // namespace exasol::udf::v2::socket diff --git a/udf-runner-cpp/v2/socket/unix_socket_detail.cc b/udf-runner-cpp/v2/socket/unix_socket_detail.cc new file mode 100644 index 0000000..32bf3a6 --- /dev/null +++ b/udf-runner-cpp/v2/socket/unix_socket_detail.cc @@ -0,0 +1,47 @@ +#include "unix_socket_detail.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace exasol::udf::v2::socket::detail +{ + +sockaddr_un make_socket_address(const std::filesystem::path& path, socklen_t& length) +{ + const std::string path_string = path.string(); + if (path_string.empty() || path_string.contains('\0') || + path_string.size() >= sizeof(sockaddr_un::sun_path)) + { + throw std::invalid_argument("invalid Unix socket path: " + path_string); + } + + sockaddr_un address{}; + address.sun_family = AF_UNIX; + std::memcpy(address.sun_path, path_string.data(), path_string.size()); + length = static_cast(offsetof(sockaddr_un, sun_path) + path_string.size() + 1); + return address; +} + +OwnedFileDescriptor create_socket() +{ + const int socket_fd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (socket_fd == -1) + { + throw std::system_error(errno, std::generic_category(), "create Unix socket"); + } + return OwnedFileDescriptor::adopt_native_handle(socket_fd); +} + +const sockaddr* as_socket_address(const sockaddr_un& address) noexcept +{ + return std::bit_cast(&address); +} + +} // namespace exasol::udf::v2::socket::detail diff --git a/udf-runner-cpp/v2/socket/unix_socket_detail.hpp b/udf-runner-cpp/v2/socket/unix_socket_detail.hpp new file mode 100644 index 0000000..4771b60 --- /dev/null +++ b/udf-runner-cpp/v2/socket/unix_socket_detail.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +#include + +#include + +namespace exasol::udf::v2::socket::detail +{ + +[[nodiscard]] sockaddr_un make_socket_address(const std::filesystem::path& path, socklen_t& length); +[[nodiscard]] OwnedFileDescriptor create_socket(); +[[nodiscard]] const sockaddr* as_socket_address(const sockaddr_un& address) noexcept; + +} // namespace exasol::udf::v2::socket::detail diff --git a/udf-runner-cpp/v2/socket/unix_socket_listener.cc b/udf-runner-cpp/v2/socket/unix_socket_listener.cc new file mode 100644 index 0000000..d3853be --- /dev/null +++ b/udf-runner-cpp/v2/socket/unix_socket_listener.cc @@ -0,0 +1,158 @@ +#include + +#include "unix_socket_detail.hpp" + +#include +#include +#include + +#include + +namespace exasol::udf::v2::socket +{ + +namespace +{ + + void unlink_socket_path(std::filesystem::path& path) noexcept + { + std::filesystem::path path_to_unlink = std::exchange(path, {}); + if (!path_to_unlink.empty()) + { + static_cast(::unlink(path_to_unlink.c_str())); + } + } + + class SocketPathCleanup final + { + public: + explicit SocketPathCleanup(std::filesystem::path& path) noexcept : path(path) + { + } + + SocketPathCleanup(const SocketPathCleanup&) = delete; + SocketPathCleanup& operator=(const SocketPathCleanup&) = delete; + SocketPathCleanup(SocketPathCleanup&&) = delete; + SocketPathCleanup& operator=(SocketPathCleanup&&) = delete; + + ~SocketPathCleanup() + { + if (cleanup) + { + unlink_socket_path(path); + } + } + + void dont_clean() noexcept + { + cleanup = false; + } + + private: + std::filesystem::path& path; + bool cleanup = true; + }; + +} // namespace + +UnixSocketListener::UnixSocketListener() noexcept = default; + +UnixSocketListener::~UnixSocketListener() noexcept +{ + this->close(); +} + +UnixSocketListener::UnixSocketListener(UnixSocketListener&& other) noexcept + : file_descriptor(std::move(other.file_descriptor)), + bound_path(std::exchange(other.bound_path, {})) +{ +} + +UnixSocketListener& UnixSocketListener::operator=(UnixSocketListener&& other) noexcept +{ + if (this != &other) + { + this->close(); + this->file_descriptor = std::move(other.file_descriptor); + this->bound_path = std::exchange(other.bound_path, {}); + } + return *this; +} + +UnixSocketListener UnixSocketListener::bind(const std::filesystem::path& path, const int backlog) +{ + socklen_t address_length = 0; + const sockaddr_un address = detail::make_socket_address(path, address_length); + OwnedFileDescriptor socket_fd = detail::create_socket(); + std::filesystem::path bound_path(path); + if (::bind(socket_fd.native_handle(), detail::as_socket_address(address), address_length) == -1) + { + const int error = errno; + throw std::system_error(error, std::generic_category(), + "bind Unix socket listener '" + path.string() + "'"); + } + + SocketPathCleanup cleanup_path(bound_path); + if (::listen(socket_fd.native_handle(), backlog) == -1) + { + throw std::system_error(errno, std::generic_category(), "listen on Unix socket"); + } + UnixSocketListener listener(std::move(socket_fd), std::move(bound_path)); + cleanup_path.dont_clean(); + return listener; +} + +int UnixSocketListener::native_handle() const noexcept +{ + return this->file_descriptor.native_handle(); +} + +bool UnixSocketListener::is_open() const noexcept +{ + return this->file_descriptor.is_open(); +} + +void UnixSocketListener::close() noexcept +{ + this->file_descriptor.close(); + this->unlink_path(); +} + +OwnedFileDescriptor UnixSocketListener::release_native_handle() noexcept +{ + const int released_fd = this->file_descriptor.release_native_handle(); + this->unlink_path(); + if (released_fd == -1) + { + return {}; + } + return OwnedFileDescriptor::adopt_native_handle(released_fd); +} + +UnixSocket UnixSocketListener::accept(const int flags) const +{ + int accepted_fd = -1; + do + { + accepted_fd = ::accept4(this->file_descriptor.native_handle(), nullptr, nullptr, flags); + } while (accepted_fd == -1 && errno == EINTR); + + if (accepted_fd == -1) + { + throw std::system_error(errno, std::generic_category(), "accept Unix socket connection"); + } + return UnixSocket::adopt_native_handle(OwnedFileDescriptor::adopt_native_handle(accepted_fd)); +} + +void UnixSocketListener::unlink_path() noexcept +{ + unlink_socket_path(this->bound_path); +} + +UnixSocketListener::UnixSocketListener(OwnedFileDescriptor owned_fd, + std::filesystem::path path) noexcept + : file_descriptor(std::move(owned_fd)), bound_path(std::move(path)) +{ +} + +} // namespace exasol::udf::v2::socket diff --git a/udf-runner-cpp/v2/socket/unix_socket_test.cc b/udf-runner-cpp/v2/socket/unix_socket_test.cc new file mode 100644 index 0000000..9986bce --- /dev/null +++ b/udf-runner-cpp/v2/socket/unix_socket_test.cc @@ -0,0 +1,355 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace +{ + +using exasol::udf::v2::socket::OwnedFileDescriptor; +using exasol::udf::v2::socket::Shutdown; +using exasol::udf::v2::socket::UnixSocket; +using exasol::udf::v2::socket::UnixSocketListener; + +void expect_system_error(const auto& function, const std::errc expected) +{ + try + { + function(); + ADD_FAILURE() << "expected std::system_error"; + } + catch (const std::system_error& error) + { + EXPECT_EQ(error.code(), std::make_error_code(expected)); + } +} + +class UnixSocketPathTest : public testing::Test +{ +protected: + void SetUp() override + { + // Each test gets an isolated directory so socket paths cannot collide. + auto directory_template = + std::to_array(P_tmpdir "/udf-runner-cpp-socket-XXXXXX"); // NOSONAR + char* directory = ::mkdtemp(directory_template.data()); // NOSONAR + ASSERT_NE(directory, nullptr); + temporary_directory = directory; + } + + void TearDown() override + { + // The listener owns its path; the test removes its private directory. + std::error_code error; + std::filesystem::remove_all(temporary_directory, error); + EXPECT_FALSE(error) << error.message(); + } + + [[nodiscard]] std::filesystem::path get_socket_path() const + { + return temporary_directory / "socket"; + } + +private: + std::filesystem::path temporary_directory; +}; + +TEST_F(UnixSocketPathTest, ListenerConnectsAcceptsAndCleansUpPath) +{ + const std::filesystem::path path = get_socket_path(); + UnixSocketListener listener = UnixSocketListener::bind(path); + ASSERT_TRUE(std::filesystem::exists(path)); + // Connecting and accepting through the path verifies both listener endpoints. + UnixSocket client = UnixSocket::connect(path); + UnixSocket server = listener.accept(); + + const std::array sent{std::byte{'o'}, std::byte{'k'}}; + ASSERT_EQ(client.write_some(sent), sent.size()); + std::array received{}; + ASSERT_EQ(server.read_some(received), received.size()); + EXPECT_EQ(received, sent); + + listener.close(); + EXPECT_FALSE(std::filesystem::exists(path)); +} + +TEST_F(UnixSocketPathTest, AcceptUsesCallerProvidedFlags) +{ + UnixSocketListener listener = UnixSocketListener::bind(get_socket_path()); + + UnixSocket blocking_client = UnixSocket::connect(get_socket_path()); + UnixSocket blocking_server = listener.accept(0); + const int blocking_status_flags = ::fcntl(blocking_server.native_handle(), F_GETFL); + const int blocking_descriptor_flags = ::fcntl(blocking_server.native_handle(), F_GETFD); + ASSERT_NE(blocking_status_flags, -1); + ASSERT_NE(blocking_descriptor_flags, -1); + const auto blocking_status = static_cast(blocking_status_flags); + const auto blocking_descriptor = static_cast(blocking_descriptor_flags); + EXPECT_EQ(blocking_status & static_cast(O_NONBLOCK), 0U); + EXPECT_EQ(blocking_descriptor & static_cast(FD_CLOEXEC), 0U); + + UnixSocket nonblocking_client = UnixSocket::connect(get_socket_path()); + UnixSocket nonblocking_server = listener.accept(SOCK_NONBLOCK | SOCK_CLOEXEC); + const int nonblocking_status_flags = ::fcntl(nonblocking_server.native_handle(), F_GETFL); + const int nonblocking_descriptor_flags = ::fcntl(nonblocking_server.native_handle(), F_GETFD); + ASSERT_NE(nonblocking_status_flags, -1); + ASSERT_NE(nonblocking_descriptor_flags, -1); + const auto nonblocking_status = static_cast(nonblocking_status_flags); + const auto nonblocking_descriptor = static_cast(nonblocking_descriptor_flags); + EXPECT_NE(nonblocking_status & static_cast(O_NONBLOCK), 0U); + EXPECT_NE(nonblocking_descriptor & static_cast(FD_CLOEXEC), 0U); +} + +TEST_F(UnixSocketPathTest, ConnectReportsAddressError) +{ + const std::filesystem::path path = get_socket_path().parent_path() / "missing" / "socket"; + expect_system_error([path] { static_cast(UnixSocket::connect(path)); }, + std::errc::no_such_file_or_directory); +} + +TEST_F(UnixSocketPathTest, ListenerDoesNotOverwriteExistingPath) +{ + const std::filesystem::path path = get_socket_path(); + UnixSocketListener listener = UnixSocketListener::bind(path); + expect_system_error([path] { static_cast(UnixSocketListener::bind(path)); }, + std::errc::address_in_use); + listener.close(); + EXPECT_FALSE(std::filesystem::exists(path)); +} + +TEST_F(UnixSocketPathTest, BindReportsAddressError) +{ + const std::filesystem::path path = get_socket_path().parent_path() / "missing" / "socket"; + expect_system_error([path] { static_cast(UnixSocketListener::bind(path)); }, + std::errc::no_such_file_or_directory); +} + +TEST_F(UnixSocketPathTest, DefaultListenerCanBeInspectedAndReleased) +{ + UnixSocketListener listener; + EXPECT_FALSE(listener.is_open()); + EXPECT_EQ(listener.native_handle(), -1); + EXPECT_FALSE(listener.release_native_handle().is_open()); +} + +TEST_F(UnixSocketPathTest, AcceptOnClosedListenerReportsError) +{ + UnixSocketListener listener; + expect_system_error([&listener] { static_cast(listener.accept()); }, + std::errc::bad_file_descriptor); +} + +TEST_F(UnixSocketPathTest, RejectsEmptyPath) +{ + EXPECT_THROW(static_cast(UnixSocketListener::bind({})), std::invalid_argument); +} + +TEST_F(UnixSocketPathTest, DestructionCleansUpPath) +{ + const std::filesystem::path path = get_socket_path(); + { + UnixSocketListener listener = UnixSocketListener::bind(path); + EXPECT_TRUE(std::filesystem::exists(path)); + } + EXPECT_FALSE(std::filesystem::exists(path)); +} + +TEST_F(UnixSocketPathTest, ReleasingDescriptorCleansUpPath) +{ + const std::filesystem::path path = get_socket_path(); + UnixSocketListener listener = UnixSocketListener::bind(path); + + OwnedFileDescriptor released = listener.release_native_handle(); + + EXPECT_TRUE(released.is_open()); + EXPECT_FALSE(std::filesystem::exists(path)); +} + +TEST_F(UnixSocketPathTest, MoveAssignmentCleansUpReplacedPath) +{ + const std::filesystem::path first_path = get_socket_path(); + const std::filesystem::path second_path = first_path.parent_path() / "second_socket"; + UnixSocketListener source = UnixSocketListener::bind(first_path); + UnixSocketListener destination = UnixSocketListener::bind(second_path); + + destination = std::move(source); + + EXPECT_TRUE(std::filesystem::exists(first_path)); + EXPECT_FALSE(std::filesystem::exists(second_path)); + destination.close(); + EXPECT_FALSE(std::filesystem::exists(first_path)); +} + +TEST(UnixSocketTest, SupportsScatterGatherIo) +{ + std::array sockets{}; + ASSERT_EQ(::socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sockets.data()), 0); + // Wrap both ends so descriptor ownership is transferred to UnixSocket. + UnixSocket writer = + UnixSocket::adopt_native_handle(OwnedFileDescriptor::adopt_native_handle(sockets[0])); + UnixSocket reader = + UnixSocket::adopt_native_handle(OwnedFileDescriptor::adopt_native_handle(sockets[1])); + const std::array first{std::byte{'a'}, std::byte{'b'}}; + const std::array second{std::byte{'c'}, std::byte{'d'}}; + // Send two buffers in one operation and verify that the peer sees them contiguously. + const std::array, 2> buffers{first, second}; + ASSERT_EQ(writer.write_some(buffers), 4); + std::array received{}; + ASSERT_EQ(reader.read_some(received), 4); + EXPECT_EQ(received, (std::array{std::byte{'a'}, std::byte{'b'}, std::byte{'c'}, + std::byte{'d'}})); +} + +TEST(UnixSocketTest, ReadReturnsZeroAtPeerEof) +{ + std::array sockets{}; + ASSERT_EQ(::socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sockets.data()), 0); + UnixSocket writer = + UnixSocket::adopt_native_handle(OwnedFileDescriptor::adopt_native_handle(sockets[0])); + UnixSocket reader = + UnixSocket::adopt_native_handle(OwnedFileDescriptor::adopt_native_handle(sockets[1])); + writer.close(); + std::array eof_buffer{}; + // Closing the writer must make the reader observe peer EOF. + EXPECT_EQ(reader.read_some(eof_buffer), 0); +} + +// A nonblocking read maps EAGAIN to resource_unavailable_try_again. +TEST(UnixSocketTest, HandlesNonblockingRead) +{ + std::array sockets{}; + ASSERT_EQ(::socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sockets.data()), 0); + UnixSocket reader = + UnixSocket::adopt_native_handle(OwnedFileDescriptor::adopt_native_handle(sockets[0])); + // Enable nonblocking mode so read_some returns immediately when no data is available. + ASSERT_NE(::fcntl(reader.native_handle(), F_SETFL, O_NONBLOCK), -1); + std::array buffer{}; + expect_system_error([&reader, &buffer] { static_cast(reader.read_some(buffer)); }, + std::errc::resource_unavailable_try_again); + ASSERT_EQ(::close(sockets[1]), 0); +} + +TEST(UnixSocketTest, SendShutdownDisablesOnlyWrites) +{ + std::array sockets{}; + ASSERT_EQ(::socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sockets.data()), 0); + UnixSocket writer = + UnixSocket::adopt_native_handle(OwnedFileDescriptor::adopt_native_handle(sockets[0])); + UnixSocket reader = + UnixSocket::adopt_native_handle(OwnedFileDescriptor::adopt_native_handle(sockets[1])); + writer.shutdown(Shutdown::Send); + + const std::array data{std::byte{'x'}}; + expect_system_error([&writer, &data] { static_cast(writer.write_some(data)); }, + std::errc::broken_pipe); + + ASSERT_EQ(reader.write_some(data), data.size()); + std::array received{}; + ASSERT_EQ(writer.read_some(received), received.size()); + EXPECT_EQ(received, data); +} + +TEST(UnixSocketTest, ReleasesDescriptor) +{ + std::array sockets{}; + ASSERT_EQ(::socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sockets.data()), 0); + UnixSocket socket = + UnixSocket::adopt_native_handle(OwnedFileDescriptor::adopt_native_handle(sockets[0])); + OwnedFileDescriptor peer = OwnedFileDescriptor::adopt_native_handle(sockets[1]); + + OwnedFileDescriptor released = socket.release_native_handle(); + // Releasing transfers the descriptor out without closing it. + EXPECT_FALSE(socket.is_open()); + EXPECT_TRUE(released.is_open()); +} + +TEST(UnixSocketTest, SupportsMoveConstructionAndAssignment) +{ + std::array sockets{}; + ASSERT_EQ(::socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sockets.data()), 0); + UnixSocket source = + UnixSocket::adopt_native_handle(OwnedFileDescriptor::adopt_native_handle(sockets[0])); + UnixSocket moved(std::move(source)); + EXPECT_TRUE(moved.is_open()); + + UnixSocket destination = + UnixSocket::adopt_native_handle(OwnedFileDescriptor::adopt_native_handle(sockets[1])); + destination = std::move(moved); + EXPECT_TRUE(destination.is_open()); +} + +TEST(UnixSocketTest, RejectsClosedDescriptor) +{ + EXPECT_THROW(static_cast(UnixSocket::adopt_native_handle({})), std::system_error); +} + +TEST(UnixSocketTest, RejectsNonUnixSocketDescriptor) +{ + const int descriptor = ::socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + ASSERT_NE(descriptor, -1); + OwnedFileDescriptor owned = OwnedFileDescriptor::adopt_native_handle(descriptor); + EXPECT_THROW(static_cast(UnixSocket::adopt_native_handle(std::move(owned))), + std::system_error); +} + +TEST(UnixSocketTest, HandlesEmptyIoBuffers) +{ + std::array sockets{}; + ASSERT_EQ(::socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sockets.data()), 0); + UnixSocket socket = + UnixSocket::adopt_native_handle(OwnedFileDescriptor::adopt_native_handle(sockets[0])); + const std::span> empty_read{}; + const std::span> empty_write{}; + EXPECT_EQ(socket.read_some(empty_read), 0U); + EXPECT_EQ(socket.write_some(empty_write), 0U); + ASSERT_EQ(::close(sockets[1]), 0); +} + +TEST(UnixSocketTest, RejectsTooManyIoBuffers) +{ + std::array sockets{}; + ASSERT_EQ(::socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sockets.data()), 0); + UnixSocket socket = + UnixSocket::adopt_native_handle(OwnedFileDescriptor::adopt_native_handle(sockets[0])); + std::array byte{}; + std::array, IOV_MAX + 1> buffers{}; + buffers.fill(byte); + EXPECT_THROW(static_cast(socket.read_some(buffers)), std::system_error); + ASSERT_EQ(::close(sockets[1]), 0); +} + +TEST(UnixSocketTest, HandlesAllShutdownDirectionsAndInvalidDirection) +{ + std::array sockets{}; + ASSERT_EQ(::socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sockets.data()), 0); + UnixSocket socket = + UnixSocket::adopt_native_handle(OwnedFileDescriptor::adopt_native_handle(sockets[0])); + EXPECT_NO_THROW(socket.shutdown(Shutdown::Receive)); + EXPECT_NO_THROW(socket.shutdown(Shutdown::Send)); + EXPECT_NO_THROW(socket.shutdown(Shutdown::Both)); + EXPECT_THROW(socket.shutdown(static_cast(-1)), std::logic_error); + ASSERT_EQ(::close(sockets[1]), 0); +} + +TEST(UnixSocketTest, ReportsShutdownAndReleaseOnClosedSocket) +{ + UnixSocket socket; + EXPECT_THROW(socket.shutdown(Shutdown::Both), std::system_error); + EXPECT_FALSE(socket.release_native_handle().is_open()); +} + +} // namespace