Skip to content

Commit 74c1726

Browse files
etrclaude
andcommitted
TASK-004: Add httpserver::iovec_entry POD with layout-pinning asserts
Introduces a library-defined POD `httpserver::iovec_entry { const void* base; std::size_t len; }` in a new public header `<httpserver/iovec_entry.hpp>`, included by `<httpserver/http_response.hpp>` and the umbrella header. The type replaces POSIX `struct iovec` at the public API surface, keeping `<sys/uio.h>` out of every public header. Layout pinning lives in `src/iovec_response.cpp` as six unconditional static_asserts: three against POSIX `struct iovec` (size + iov_base / iov_len offsets) per the spec, and three parallel asserts against libmicrohttpd `MHD_IoVec` because that is the actual cast target on the dispatch path. The MHD_IoVec asserts are an addition over the spec — without them the reinterpret_cast bridge is the unsafe one. A TODO sentinel comment (LIBHTTPSERVER_TODO_TASK004_MEMCPY_FALLBACK) documents the memcpy fallback strategy that would activate if a divergent-layout platform ever trips one of the asserts. Today every supported platform (glibc, musl, macOS, FreeBSD, NetBSD, OpenBSD, illumos) shares the same layout so the asserts pass and the reinterpret_cast is well-defined. `iovec_response::get_raw_response()` now builds a contiguous `std::vector<iovec_entry>` from its owned std::strings and reinterpret_casts to `const MHD_IoVec*` when calling MHD. This proves the cast bridge in production code today; TASK-010 will move the same line into the future `details/body.hpp` factory. Two new TDD-driven test programs: - `test/unit/iovec_entry_test.cpp` — verifies POD traits (standard layout, trivially copyable), member types, layout equivalence with POSIX `struct iovec` from a consumer perspective, and the reinterpret_cast bridge round-trip. - `test/unit/header_hygiene_iovec_test.cpp` — declares a colliding `struct iovec` before including `iovec_entry.hpp` directly. The TU compiling at all proves the new public header pulls in nothing from `<sys/uio.h>`. (The broader umbrella-leak concern — current umbrella transitively pulls `<sys/uio.h>` via gnutls and `<sys/socket.h>` — is out of scope for TASK-004 and is the remit of TASK-007's header-hygiene CI gate.) Build: 20/20 tests pass under both default and `--enable-debug` (-Wall -Wextra -Werror -pedantic -O0). `grep -E '#include\s+<sys/uio\.h>' src/httpserver/*.hpp` returns no results. `make install` ships the new header at `$prefix/include/httpserver/iovec_entry.hpp`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a05f79c commit 74c1726

8 files changed

Lines changed: 269 additions & 7 deletions

File tree

src/Makefile.am

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ libhttpserver_la_SOURCES = string_utilities.cpp webserver.cpp http_utils.cpp fil
2424
# Detail headers (httpserver/details/*.hpp) live here so they cannot leak to
2525
# downstream consumers — the public surface comes in through <httpserver.hpp>.
2626
noinst_HEADERS = httpserver/string_utilities.hpp httpserver/details/modded_request.hpp httpserver/details/http_endpoint.hpp gettext.h
27-
nobase_include_HEADERS = httpserver.hpp httpserver/create_webserver.hpp httpserver/webserver.hpp httpserver/http_utils.hpp httpserver/file_info.hpp httpserver/http_request.hpp httpserver/http_response.hpp httpserver/http_resource.hpp httpserver/string_response.hpp httpserver/digest_auth_fail_response.hpp httpserver/deferred_response.hpp httpserver/file_response.hpp httpserver/pipe_response.hpp httpserver/empty_response.hpp httpserver/feature_unavailable.hpp httpserver/iovec_response.hpp httpserver/http_arg_value.hpp
27+
nobase_include_HEADERS = httpserver.hpp httpserver/create_webserver.hpp httpserver/webserver.hpp httpserver/http_utils.hpp httpserver/file_info.hpp httpserver/http_request.hpp httpserver/http_response.hpp httpserver/http_resource.hpp httpserver/string_response.hpp httpserver/digest_auth_fail_response.hpp httpserver/deferred_response.hpp httpserver/file_response.hpp httpserver/pipe_response.hpp httpserver/empty_response.hpp httpserver/feature_unavailable.hpp httpserver/iovec_entry.hpp httpserver/iovec_response.hpp httpserver/http_arg_value.hpp
2828

2929
if HAVE_BAUTH
3030
libhttpserver_la_SOURCES += basic_auth_fail_response.cpp

src/httpserver.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
#include "httpserver/http_resource.hpp"
4343
#include "httpserver/http_response.hpp"
4444
#include "httpserver/http_utils.hpp"
45+
#include "httpserver/iovec_entry.hpp"
4546
#include "httpserver/iovec_response.hpp"
4647
#include "httpserver/file_info.hpp"
4748
#include "httpserver/pipe_response.hpp"

src/httpserver/http_response.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#include <string>
3131
#include "httpserver/http_arg_value.hpp"
3232
#include "httpserver/http_utils.hpp"
33+
#include "httpserver/iovec_entry.hpp"
3334

3435
struct MHD_Connection;
3536
struct MHD_Response;

src/httpserver/iovec_entry.hpp

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
This file is part of libhttpserver
3+
Copyright (C) 2011-2019 Sebastiano Merlino
4+
5+
This library is free software; you can redistribute it and/or
6+
modify it under the terms of the GNU Lesser General Public
7+
License as published by the Free Software Foundation; either
8+
version 2.1 of the License, or (at your option) any later version.
9+
10+
This library is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
Lesser General Public License for more details.
14+
15+
You should have received a copy of the GNU Lesser General Public
16+
License along with this library; if not, write to the Free Software
17+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
18+
USA
19+
*/
20+
21+
#if !defined (_HTTPSERVER_HPP_INSIDE_) && !defined (HTTPSERVER_COMPILATION)
22+
#error "Only <httpserver.hpp> or <httpserverpp> can be included directly."
23+
#endif
24+
25+
#ifndef SRC_HTTPSERVER_IOVEC_ENTRY_HPP_
26+
#define SRC_HTTPSERVER_IOVEC_ENTRY_HPP_
27+
28+
#include <cstddef>
29+
30+
namespace httpserver {
31+
32+
// Library-defined POD describing a single scatter/gather buffer at the
33+
// public API surface. Replaces `struct iovec` from <sys/uio.h>, keeping
34+
// the public-header surface free of POSIX-only system headers.
35+
//
36+
// Layout is pinned to match POSIX `struct iovec` and libmicrohttpd's
37+
// `MHD_IoVec` so the dispatch path can `reinterpret_cast` a contiguous
38+
// array of iovec_entry into either C type at zero copy. The pinning
39+
// asserts live next to the cast site (currently `iovec_response.cpp`,
40+
// moving to `details/body.hpp` once TASK-009 lands).
41+
//
42+
// `base` is `const void*` because libhttpserver never writes through
43+
// these buffers on the response path.
44+
struct iovec_entry {
45+
const void* base;
46+
std::size_t len;
47+
};
48+
49+
} // namespace httpserver
50+
#endif // SRC_HTTPSERVER_IOVEC_ENTRY_HPP_

src/iovec_response.cpp

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,26 +19,72 @@
1919
*/
2020

2121
#include "httpserver/iovec_response.hpp"
22+
#include "httpserver/iovec_entry.hpp"
23+
24+
#include <cstddef>
2225
#include <microhttpd.h>
26+
#include <sys/uio.h>
2327
#include <vector>
2428

2529
struct MHD_Response;
2630

2731
namespace httpserver {
2832

33+
// ---------------------------------------------------------------------------
34+
// TASK-004: layout-pinning static_asserts.
35+
//
36+
// httpserver::iovec_entry is the public scatter/gather POD; libmicrohttpd's
37+
// MHD_IoVec is the actual cast target on the dispatch path. POSIX struct
38+
// iovec is asserted in parallel because the spec mandates it and because
39+
// every platform we ship to defines all three with identical layout
40+
// (glibc, musl, macOS, FreeBSD, NetBSD, OpenBSD, illumos).
41+
//
42+
// LIBHTTPSERVER_TODO_TASK004_MEMCPY_FALLBACK: if any of the asserts below
43+
// ever fires on a divergent-layout platform, the fix is to replace the
44+
// reinterpret_cast in the dispatch path with an element-by-element copy
45+
// into a stack/heap MHD_IoVec[]. Until such a platform appears the
46+
// asserts are the gate — a build failure on the divergent platform is
47+
// the desired outcome (loud, immediate, with the assert string naming
48+
// what diverged).
49+
// ---------------------------------------------------------------------------
50+
static_assert(sizeof(::httpserver::iovec_entry) == sizeof(struct iovec),
51+
"iovec_entry size must match POSIX struct iovec — divergent platform; "
52+
"implement memcpy fallback (see TASK-004)");
53+
static_assert(offsetof(::httpserver::iovec_entry, base) ==
54+
offsetof(struct iovec, iov_base),
55+
"iovec_entry::base offset must match struct iovec::iov_base");
56+
static_assert(offsetof(::httpserver::iovec_entry, len) ==
57+
offsetof(struct iovec, iov_len),
58+
"iovec_entry::len offset must match struct iovec::iov_len");
59+
60+
static_assert(sizeof(::httpserver::iovec_entry) == sizeof(MHD_IoVec),
61+
"iovec_entry size must match libmicrohttpd MHD_IoVec — MHD layout drift");
62+
static_assert(offsetof(::httpserver::iovec_entry, base) ==
63+
offsetof(MHD_IoVec, iov_base),
64+
"iovec_entry::base offset must match MHD_IoVec::iov_base");
65+
static_assert(offsetof(::httpserver::iovec_entry, len) ==
66+
offsetof(MHD_IoVec, iov_len),
67+
"iovec_entry::len offset must match MHD_IoVec::iov_len");
68+
2969
MHD_Response* iovec_response::get_raw_response() {
3070
// MHD_create_response_from_iovec makes an internal copy of the iov array,
3171
// so the local vector is safe. The buffer data pointed to by iov_base must
3272
// remain valid until the response is destroyed — this is guaranteed because
3373
// the buffers are owned by this iovec_response object.
34-
std::vector<MHD_IoVec> iov(buffers.size());
74+
//
75+
// The dispatch path builds a contiguous std::vector<iovec_entry> from the
76+
// owned std::strings, then reinterpret_casts it to const MHD_IoVec* when
77+
// calling MHD. The cast is well-defined because the layout-pinning
78+
// static_asserts above guarantee identical size and field offsets. This
79+
// same cast bridge will move into details/body.hpp when TASK-009 lands.
80+
std::vector<iovec_entry> entries(buffers.size());
3581
for (size_t i = 0; i < buffers.size(); ++i) {
36-
iov[i].iov_base = buffers[i].data();
37-
iov[i].iov_len = buffers[i].size();
82+
entries[i].base = buffers[i].data();
83+
entries[i].len = buffers[i].size();
3884
}
3985
return MHD_create_response_from_iovec(
40-
iov.data(),
41-
static_cast<unsigned int>(iov.size()),
86+
reinterpret_cast<const MHD_IoVec*>(entries.data()),
87+
static_cast<unsigned int>(entries.size()),
4288
nullptr,
4389
nullptr);
4490
}

test/Makefile.am

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ LDADD += -lcurl
2626

2727
AM_CPPFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/httpserver/ -DHTTPSERVER_COMPILATION
2828
METASOURCES = AUTO
29-
check_PROGRAMS = basic file_upload http_utils threaded nodelay string_utilities http_endpoint ban_system ws_start_stop authentication deferred http_resource http_response create_webserver new_response_types daemon_info uri_log feature_unavailable
29+
check_PROGRAMS = basic file_upload http_utils threaded nodelay string_utilities http_endpoint ban_system ws_start_stop authentication deferred http_resource http_response create_webserver new_response_types daemon_info uri_log feature_unavailable header_hygiene_iovec iovec_entry
3030

3131
MOSTLYCLEANFILES = *.gcda *.gcno *.gcov
3232

@@ -52,6 +52,8 @@ uri_log_SOURCES = unit/uri_log_test.cpp
5252
# LDADD (modern ld enforces --no-copy-dt-needed-entries).
5353
uri_log_LDADD = $(LDADD) -lmicrohttpd
5454
feature_unavailable_SOURCES = unit/feature_unavailable_test.cpp
55+
header_hygiene_iovec_SOURCES = unit/header_hygiene_iovec_test.cpp
56+
iovec_entry_SOURCES = unit/iovec_entry_test.cpp
5557

5658
noinst_HEADERS = littletest.hpp
5759
AM_CXXFLAGS += -Wall -fPIC -Wno-overloaded-virtual
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
This file is part of libhttpserver
3+
Copyright (C) 2011-2019 Sebastiano Merlino
4+
5+
This library is free software; you can redistribute it and/or
6+
modify it under the terms of the GNU Lesser General Public
7+
License as published by the Free Software Foundation; either
8+
version 2.1 of the License, or (at your option) any later version.
9+
10+
This library is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
Lesser General Public License for more details.
14+
15+
You should have received a copy of the GNU Lesser General Public
16+
License along with this library; if not, write to the Free Software
17+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
18+
USA
19+
*/
20+
21+
// Header-hygiene sentinel for TASK-004:
22+
//
23+
// AC #4 of TASK-004 ("public header must not include <sys/uio.h>") is
24+
// scoped to the new iovec_entry header itself; the broader umbrella-leak
25+
// concern (current umbrella transitively pulls <sys/uio.h> via gnutls/
26+
// <sys/socket.h>) is the remit of TASK-007's header-hygiene CI gate.
27+
//
28+
// To enforce the local guarantee, this TU declares a colliding
29+
// `struct iovec` BEFORE including iovec_entry.hpp directly. If the
30+
// header (or anything it pulls in) pulls <sys/uio.h>, the system
31+
// definition collides with this sentinel and the build fails with a
32+
// redefinition error. The TU compiling at all is the assertion.
33+
struct iovec {
34+
int libhttpserver_hygiene_sentinel;
35+
};
36+
37+
// Include the new POD header in isolation to verify it pulls no
38+
// surprise dependencies. HTTPSERVER_COMPILATION is already defined by
39+
// AM_CPPFLAGS in test/Makefile.am, so the gate is satisfied.
40+
#include "httpserver/iovec_entry.hpp"
41+
42+
#include "./littletest.hpp"
43+
44+
LT_BEGIN_SUITE(header_hygiene_iovec_suite)
45+
void set_up() {
46+
}
47+
48+
void tear_down() {
49+
}
50+
LT_END_SUITE(header_hygiene_iovec_suite)
51+
52+
LT_BEGIN_AUTO_TEST(header_hygiene_iovec_suite, iovec_entry_visible_without_sys_uio)
53+
httpserver::iovec_entry e{nullptr, 0};
54+
LT_CHECK_EQ(e.base, nullptr);
55+
LT_CHECK_EQ(e.len, 0u);
56+
LT_END_AUTO_TEST(iovec_entry_visible_without_sys_uio)
57+
58+
LT_BEGIN_AUTO_TEST_ENV()
59+
AUTORUN_TESTS()
60+
LT_END_AUTO_TEST_ENV()

test/unit/iovec_entry_test.cpp

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
This file is part of libhttpserver
3+
Copyright (C) 2011-2019 Sebastiano Merlino
4+
5+
This library is free software; you can redistribute it and/or
6+
modify it under the terms of the GNU Lesser General Public
7+
License as published by the Free Software Foundation; either
8+
version 2.1 of the License, or (at your option) any later version.
9+
10+
This library is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
Lesser General Public License for more details.
14+
15+
You should have received a copy of the GNU Lesser General Public
16+
License along with this library; if not, write to the Free Software
17+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
18+
USA
19+
*/
20+
21+
// Layout / POD-trait verification for `httpserver::iovec_entry`.
22+
// This TU is allowed to include <sys/uio.h> directly — it is an internal
23+
// test, not a header-hygiene sentinel. The library-side guarantee that
24+
// downstream code does NOT see <sys/uio.h> via the umbrella is asserted
25+
// separately by `header_hygiene_iovec_test.cpp`.
26+
27+
#include <cstddef>
28+
#include <sys/uio.h>
29+
#include <type_traits>
30+
31+
#include "./httpserver.hpp"
32+
#include "./littletest.hpp"
33+
34+
// AC: trivially copyable + standard layout — required for the
35+
// reinterpret_cast bridge to libmicrohttpd's MHD_IoVec / POSIX struct iovec.
36+
static_assert(std::is_standard_layout_v<httpserver::iovec_entry>,
37+
"iovec_entry must be standard layout");
38+
static_assert(std::is_trivially_copyable_v<httpserver::iovec_entry>,
39+
"iovec_entry must be trivially copyable");
40+
41+
// Member types as declared by the spec.
42+
static_assert(std::is_same_v<decltype(httpserver::iovec_entry::base),
43+
const void*>,
44+
"iovec_entry::base must be const void*");
45+
static_assert(std::is_same_v<decltype(httpserver::iovec_entry::len),
46+
std::size_t>,
47+
"iovec_entry::len must be std::size_t");
48+
49+
// Layout pinning duplicated from the consumer perspective: defense in depth
50+
// against a future change to <sys/uio.h> on a divergent platform.
51+
static_assert(sizeof(httpserver::iovec_entry) == sizeof(struct iovec),
52+
"iovec_entry size must match POSIX struct iovec");
53+
static_assert(offsetof(httpserver::iovec_entry, base) ==
54+
offsetof(struct iovec, iov_base),
55+
"iovec_entry::base offset must match struct iovec::iov_base");
56+
static_assert(offsetof(httpserver::iovec_entry, len) ==
57+
offsetof(struct iovec, iov_len),
58+
"iovec_entry::len offset must match struct iovec::iov_len");
59+
60+
LT_BEGIN_SUITE(iovec_entry_suite)
61+
void set_up() {
62+
}
63+
64+
void tear_down() {
65+
}
66+
LT_END_SUITE(iovec_entry_suite)
67+
68+
LT_BEGIN_AUTO_TEST(iovec_entry_suite, default_constructed_pod_holds_values)
69+
httpserver::iovec_entry e{};
70+
LT_CHECK_EQ(e.base, nullptr);
71+
LT_CHECK_EQ(e.len, 0u);
72+
LT_END_AUTO_TEST(default_constructed_pod_holds_values)
73+
74+
LT_BEGIN_AUTO_TEST(iovec_entry_suite, brace_init_assigns_members)
75+
const char* payload = "hello";
76+
httpserver::iovec_entry e{payload, 5};
77+
LT_CHECK_EQ(e.base, static_cast<const void*>(payload));
78+
LT_CHECK_EQ(e.len, 5u);
79+
LT_END_AUTO_TEST(brace_init_assigns_members)
80+
81+
// Reinterpret-cast bridge from a contiguous range of iovec_entry to
82+
// POSIX struct iovec. This is the cast the library performs when feeding
83+
// libmicrohttpd, and what TASK-010 will rely on when it lands the
84+
// std::span<const iovec_entry> factory.
85+
LT_BEGIN_AUTO_TEST(iovec_entry_suite, reinterpret_cast_to_struct_iovec_preserves_data)
86+
const char* a = "abc";
87+
const char* b = "wxyz";
88+
httpserver::iovec_entry entries[2] = {
89+
{a, 3},
90+
{b, 4},
91+
};
92+
const struct iovec* posix =
93+
reinterpret_cast<const struct iovec*>(&entries[0]);
94+
LT_CHECK_EQ(posix[0].iov_base, const_cast<void*>(static_cast<const void*>(a)));
95+
LT_CHECK_EQ(posix[0].iov_len, 3u);
96+
LT_CHECK_EQ(posix[1].iov_base, const_cast<void*>(static_cast<const void*>(b)));
97+
LT_CHECK_EQ(posix[1].iov_len, 4u);
98+
LT_END_AUTO_TEST(reinterpret_cast_to_struct_iovec_preserves_data)
99+
100+
LT_BEGIN_AUTO_TEST_ENV()
101+
AUTORUN_TESTS()
102+
LT_END_AUTO_TEST_ENV()

0 commit comments

Comments
 (0)