From 16c3671054ec3d94cded9181f30062ab5a2e2349 Mon Sep 17 00:00:00 2001 From: Matt <47545907+SoundMatt@users.noreply.github.com> Date: Sat, 22 Aug 2026 03:52:12 -0700 Subject: [PATCH] =?UTF-8?q?rewrite(phase6):=20batch=202=20=E2=80=94=20FRAG?= =?UTF-8?q?/RESPQUEUE-slice/LOAN=20catalog=20re-derivation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17), branch rewrite/v3-from-c-rcp. Phase 6 (requirement catalog), batch 2 of 13. ## Scope fragment.hpp's REQ-FRAG-* prefix (18 c-RCP entries, all already orphan-cited in cpp-RCP's own code), the 7-entry REQ-RMAP-* slice that respqueue.hpp's code already cites (059/061-065/085 -- a real subset of c-RCP's combined register-map+response-queue module, specific to response-queue behavior; the rest of REQ-RMAP-*/REQ-REGMAP-* is deliberately deferred to batch 11's regmap.hpp rename), and loan.hpp's REQ-LOAN-* prefix (9 c-RCP entries). ## New catalog entries (26) - REQ-FRAG-{001-015,017,018} (17) -- fragment.hpp's plan_count()/ plan()/Reassembler, all verified against real code. REQ-FRAG-010 needed one new test (destruction-safety, RAII) that didn't exist yet. - REQ-LOAN-{008,009} (2) -- BufferPool destructor free-list release, new_buffer_pool(). LOAN-001-007 needed no action: already had correct catalog entries and code tags from prior work (LOAN-007's tag lives in rcp.hpp/test_rcp.cpp, not loan.hpp -- confirmed found-and-covered, not a gap). - REQ-RMAP-{059,061,062,063,064,065,085} (7) -- respqueue.hpp's RespQueue class. REQ-RMAP-065 copied honestly as c-RCP has it: scope="tc18-gap", status="partial" -- c-RCP's own text states the heartbeat-composition primitives are all present and proven composable, but driving them against a real clock/transport is left to the integrator; cpp-RCP is at the identical boundary. ## Judgment call, not filed as a gap (documented in-file, not the pending file) REQ-FRAG-016 (rcp_fragment_reasm_result_t's RCP_FRAGMENT_REASM_ERR_ALLOC, a distinct allocation-failure return value): no C++ analog -- Reassembler uses a fixed std::array member with no heap growth and therefore no allocation-failure path to distinguish. c-RCP's own REQ-FRAG-016 also carries no TC18 citation, matching the same "architecture divergence, no spec basis to force a gap entry" precedent as batch 1's REQ-ACF-037/REQ-AVTP-016/027. The orphaned tag lines were removed from fragment.hpp/test_fragment.cpp and replaced with an explanatory comment (prose mentions of the id remain for context, not real //fusa:req/test tags -- confirmed these don't register as references to cpfusa's trace tool). ## No gaps filed this batch Every c-RCP requirement in scope was either genuinely implemented, already covered by a pre-existing entry, or the one documented judgment call above -- .fusa-reqs-pending.json is unchanged. ## Verification Independently re-verified (not just the porting agent's own report): semantic id-keyed diff confirms zero pre-existing catalog entries modified/deleted, exactly the 26 new ids added. Zero real orphaned FRAG/LOAN/RMAP tags remain in the six files in scope (the one apparent match, REQ-FRAG-016, is plain prose in a regular comment, not a //fusa:req/test tag -- confirmed by direct grep). Confirmed no new entry carries "severity": "cybersecurity" (would have diluted the sec-tested absolute-count floor fixed in batch 1/#155) -- sec count holds exactly at 72/474, matching the floor. Full tree rebuilt from scratch: 0 errors, 0 warnings, 58/58 ctest suites pass (100%). .fusa-reqs.json: 448 -> 474 entries (+26, 4092 -> 4378 lines). include/rcp/fragment.hpp, tests/test_fragment.cpp: net changes for the REQ-FRAG-016 tag removal/explanatory-comment swap and the new REQ-FRAG-010 test. respqueue.hpp/loan.hpp and their test files: unchanged (already correctly tagged and tested). Co-Authored-By: Claude Sonnet 5 Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com> --- .fusa-reqs.json | 286 +++++++++++++++++++++++++++++++++++++++ include/rcp/fragment.hpp | 15 +- tests/test_fragment.cpp | 31 ++++- 3 files changed, 324 insertions(+), 8 deletions(-) diff --git a/.fusa-reqs.json b/.fusa-reqs.json index 5799011..4c942c9 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -4087,6 +4087,292 @@ "status": "implemented", "tc18": "§12.9.6 Table 30, TC18.txt L3804-3844", "tc18_master_id": "TC18-12.9.6-004" + }, + { + "id": "REQ-FRAG-001", + "title": "fragment_category() gives every FragmentErrc value a distinct, non-empty message", + "text": "rcp::fragment::fragment_category().message() shall return a non-empty string for every rcp::fragment::FragmentErrc value, including unrecognized ones, with each defined error code mapped to a distinct message.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented" + }, + { + "id": "REQ-FRAG-002", + "title": "plan_count() always plans a payload that already fits in one fragment (including an empty payload) to exactly one segment", + "text": "rcp::fragment::plan_count() shall return 1 for payload_len == 0 regardless of max_fragment_payload, and shall return 1 whenever payload_len <= max_fragment_payload.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.7.9 (\"In case an AVTPDU containing a single ACF_type would exceed the 'Max_AVTPDUsize' fragmentation as supported in ACF_ABB and ACF_GBB by the ms-bit will be performed.\"), TC18.txt L3408-3409", + "tc18_master_id": "TC18-12.7.9-003" + }, + { + "id": "REQ-FRAG-003", + "title": "plan_count() returns 0 when fragmentation is disabled or the split exceeds the representable segment count", + "text": "rcp::fragment::plan_count() shall return 0 when max_fragment_payload == 0 and payload_len exceeds it, and shall return 0 when the resulting split would need more than kMaxIntermediateSegments intermediate segments to represent.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented" + }, + { + "id": "REQ-FRAG-004", + "title": "plan() plans a single ms=false segment for a payload that fits in one fragment", + "text": "rcp::fragment::plan() shall, when plan_count() reports 1, write exactly one Segment spanning the whole payload with ms == false.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§11.2.1 Table 4 (\"ms 0b – the fragment sequence is completed with this ABB message\"), TC18.txt L1233; §12.7.9 (Max_AVTPDUsize fragmentation trigger), TC18.txt L3408-3409", + "tc18_master_id": "TC18-11.2.1-005" + }, + { + "id": "REQ-FRAG-005", + "title": "plan() contiguously covers a multi-segment payload with correctly numbered intermediate segments", + "text": "rcp::fragment::plan() shall, when plan_count() reports more than 1, produce segments whose [offset, offset+len) ranges contiguously cover the whole payload, with every segment but the last carrying ms == true and a segment_num equal to its zero-based index, and the last segment carrying ms == false.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§11.2.1 Table 4 (\"ms 0b – the fragment sequence is completed with this ABB message / 1b - more data to follow in one or more subsequent ABB messages\"; \"read_size/segment_num if op = 0 this is read_size, else segment_num\"), TC18.txt L1233-1235", + "tc18_master_id": "TC18-11.2.1-005" + }, + { + "id": "REQ-FRAG-006", + "title": "plan() returns a distinct error code for each of its own precondition failures", + "text": "rcp::fragment::plan() shall return ErrDisabled or ErrTooManySegments under the same conditions plan_count() returns 0 for, and shall return ErrBadSegmentCount when segment_count does not match plan_count()'s answer for the same payload_len/max_fragment_payload.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented" + }, + { + "id": "REQ-FRAG-007", + "title": "to_string(ReasmResult) never returns null and gives every result value a distinct message", + "text": "rcp::fragment::to_string() shall return a non-null string for every rcp::fragment::ReasmResult value, including unrecognized ones, with each defined value mapped to a distinct message.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented" + }, + { + "id": "REQ-FRAG-008", + "title": "A freshly-constructed Reassembler starts empty and not collecting", + "text": "rcp::fragment::Reassembler's constructor shall leave the reassembler not collecting, with size() == 0 and data() == nullptr, and max_total_len set to the caller-supplied value.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented" + }, + { + "id": "REQ-FRAG-009", + "title": "Reassembler::reset() discards in-progress state while preserving max_total_len", + "text": "rcp::fragment::Reassembler::reset() shall discard any in-progress reassembly and any previously reassembled payload, leaving the reassembler in the same state a freshly-constructed Reassembler with the same max_total_len would be in.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented" + }, + { + "id": "REQ-FRAG-010", + "title": "A Reassembler's destruction releases its internal storage without leaking", + "text": "rcp::fragment::Reassembler shall release its internal storage when destroyed without leaking, whether or not a fragment sequence was in progress at the time -- satisfied here by RAII over its own fixed-capacity std::array member (this class holds no heap-allocated buffer of its own to explicitly free; see fragment.hpp's own \"Fixed-capacity from day one\" comment), unlike c-RCP's rcp_fragment_reassembler_destroy(), which frees a realloc()-grown heap buffer.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented" + }, + { + "id": "REQ-FRAG-011", + "title": "Reassembler::feed() completes immediately for a never-fragmented (ms=false) first fragment", + "text": "rcp::fragment::Reassembler::feed() shall, when fed a single ms=false fragment on a freshly-constructed-or-reset Reassembler, return ReasmResult::kComplete with that fragment's payload as the reassembled result, without entering the collecting state.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§11.2.1 Table 4 (\"ms 0b – the fragment sequence is completed with this ABB message\"), TC18.txt L1233", + "tc18_master_id": "TC18-11.2.1-005" + }, + { + "id": "REQ-FRAG-012", + "title": "Reassembler::feed() requires the first ms=true fragment of a sequence to carry segment_num 0", + "text": "rcp::fragment::Reassembler::feed() shall return ReasmResult::kErrOutOfOrder, without appending the fragment, when the first ms=true fragment fed to a freshly-constructed-or-reset Reassembler carries a segment_num other than 0.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented" + }, + { + "id": "REQ-FRAG-013", + "title": "Reassembler::feed() requires each subsequent ms=true fragment's segment_num to be exactly one more than the previous", + "text": "rcp::fragment::Reassembler::feed() shall return ReasmResult::kErrOutOfOrder, without appending the fragment and without disturbing already-collected state, when an ms=true fragment's segment_num is not exactly one more than the previously accepted fragment's segment_num.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented" + }, + { + "id": "REQ-FRAG-014", + "title": "Reassembler::feed() completes a sequence on any ms=false fragment regardless of its own segment_num field value", + "text": "rcp::fragment::Reassembler::feed() shall return ReasmResult::kComplete and append the fragment's payload when fed an ms=false fragment while collecting, without validating or consulting that fragment's segment_num argument.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§11.2.1 Table 4 (\"ms 0b – the fragment sequence is completed with this ABB message\"), TC18.txt L1233", + "tc18_master_id": "TC18-11.2.1-005" + }, + { + "id": "REQ-FRAG-015", + "title": "Reassembler::feed() rejects a fragment that would exceed max_total_len without mutating state", + "text": "rcp::fragment::Reassembler::feed() shall return ReasmResult::kErrTooLarge, leaving the reassembler's already-collected state unchanged, when appending the fed fragment's payload would cause the reassembled total to exceed max_total_len (further capped, always, by this Reassembler's own fixed kDefaultReassemblyCapacity).", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.7.7 Table 24 (\"rx_stream_max_request_size ... Max size of a request in bytes (longer requests will be rejected)\"), TC18.txt L3230-3232", + "tc18_master_id": "TC18-12.7.7-001" + }, + { + "id": "REQ-FRAG-017", + "title": "Reassembler::is_collecting() reports whether a fragment sequence is currently in progress", + "text": "rcp::fragment::Reassembler::is_collecting() shall return true iff the reassembler has accepted at least one ms=true fragment since the last completed reassembly or reset, and false otherwise, without mutating the reassembler.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented" + }, + { + "id": "REQ-FRAG-018", + "title": "Reassembler::data()/size() return the reassembled payload and its length after a completed reassembly", + "text": "rcp::fragment::Reassembler::data()/size(), called immediately after feed() has returned ReasmResult::kComplete, shall report a payload pointer and length equal to the concatenation, in segment_num order, of every fragment's payload accepted for that sequence.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§11.2.1 Table 4 (\"ms\"/\"read_size/segment_num\" fields – the mechanism by which a split message's fragments are numbered so they can be reassembled), TC18.txt L1233-1235", + "tc18_master_id": "TC18-11.2.1-005" + }, + { + "id": "REQ-LOAN-008", + "title": "BufferPool destruction frees every buffer still in the free list", + "text": "rcp::loan::BufferPool's destructor shall free every buffer currently in the pool's free list, then release the pool itself, without leaking.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented" + }, + { + "id": "REQ-LOAN-009", + "title": "new_buffer_pool() allocates an empty, open pool", + "text": "rcp::loan::new_buffer_pool() shall return a non-null, open (ok()==true) rcp::loan::BufferPool with an empty free list (pooled_count() == 0).", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented" + }, + { + "id": "REQ-RMAP-059", + "title": "per-response/acknowledge-stream transmit queue enforces an octet-budgeted capacity", + "text": "rcp::respqueue::RespQueue shall provide an octet-budgeted FIFO of framed response/acknowledge messages (push()/push_seq()/pop()), with capacity enforced against a caller-supplied capacity_octets (regmap.hpp's ResponseQueueConfig::queue_size, quadlets already converted to octets by the caller), refusing a push that would exceed capacity_octets outright and leaving the queue unchanged when frame_len exceeds the entire configured capacity, unfixable by any amount of eviction. capacity_octets == 0 means unbounded.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.7.9, TC18.txt L3403-L3404", + "tc18_master_id": "TC18-12.7.9-007" + }, + { + "id": "REQ-RMAP-061", + "title": "Max_AVTPDUsize bounds every transmitted AVTPDU and is MTU-consistency-checkable", + "text": "rcp::respqueue::RespQueue::push()/push_seq() shall refuse (queue unchanged) any single frame whose own length exceeds the caller-supplied max_avtpdu_size_octets (regmap.hpp's ResponseQueueConfig::max_avtpdu_size, quadlets already converted to octets); RespQueue::max_avtpdu_size_within_mtu() shall report whether a candidate max_avtpdu_size_octets value fits within a caller-supplied mtu_budget_octets, for use before ever constructing a RespQueue.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.7.9, TC18.txt L3405-L3407 (and Table 27 row 0x0002, L3427-3428)" + }, + { + "id": "REQ-RMAP-062", + "title": "A single ACF message exceeding Max_AVTPDUsize is fragmented via the ms bit", + "text": "rcp::respqueue::RespQueue::max_fragment_payload() shall derive, from max_avtpdu_size_octets and a caller-supplied ACF header length, the max_fragment_payload budget rcp/fragment.hpp's plan_count()/plan() need to split a payload too large for one AVTPDU, conservatively reserving the fixed ACF header plus the worst-case 3 octets of trailing pad so the resulting budget is always safe.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.7.9, TC18.txt L3408-L3409", + "tc18_master_id": "TC18-12.7.9-003" + }, + { + "id": "REQ-RMAP-063", + "title": "flush_on_count triggers transmission of the whole queue packed into Max_AVTPDUsize-bounded AVTPDUs", + "text": "rcp::respqueue::RespQueue::should_flush() shall return true once the queue's own running octet total reaches or exceeds a caller-supplied flush_on_count_octets threshold; RespQueue::plan_batch() shall report how many of the queue's FIFO-ordered entries fit together within one max_avtpdu_size_octets-bounded AVTPDU, so a caller drains exactly that many via pop() and repeats until the queue is empty.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.7.9, TC18.txt L3410-L3413 (and Table 27 row 0x0006, L3432-3436)", + "tc18_master_id": "TC18-12.7.9-004" + }, + { + "id": "REQ-RMAP-064", + "title": "Flush_time forces transmission once the configured interval since the last transmission elapses", + "text": "rcp::respqueue::RespQueue::should_flush_by_time() shall return false when flush_time_us == 0 (\"flush only by count\"), and otherwise return true once a caller-tracked elapsed_since_last_transmit_us reaches flush_time_us, independently of flush_on_count.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.7.9, TC18.txt L3414-L3415 (and Table 27 row 0x0008, L3437-3438)", + "tc18_master_id": "TC18-12.7.9-005" + }, + { + "id": "REQ-RMAP-065", + "title": "an empty response queue still emits a heartbeat AVTPDU on Flush_time expiry", + "text": "PARTIAL: on Flush_time expiry the RC Server shall emit an AVTPDU from the queue even when that queue is empty, so the RC Client can observe that the server is alive. rcp::respqueue::RespQueue::should_flush_by_time() already fires identically for an empty queue, and RespQueue::plan_batch() already reports 0 for an empty queue -- so the empty heartbeat AVTPDU is fully constructible from existing primitives. What remains open: this is a protocol-primitives library, not a scheduler, and ships no emitter that actually drives this composition against a real clock and a real transport -- that composition is left to the integrator.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18-gap", + "status": "partial", + "tc18": "§12.7.9, TC18.txt L3416-L3417", + "tc18_master_id": "TC18-12.7.9-006" + }, + { + "id": "REQ-RMAP-085", + "title": "TC18 §12.9.4/§12.9.5: a completely-full response/acknowledge queue evicts the lowest-sequence_num entry and sets the overflow bit", + "text": "rcp::respqueue::RespQueue::push()/push_seq() shall, once accepting an incoming frame would exceed capacity_octets or the queue's own fixed kMaxEntries slot bound, repeatedly evict the currently-queued entry with the numerically lowest sequence_num (not merely the FIFO-oldest) until enough capacity is freed, latching overflow() true on every eviction; a frame whose own length exceeds capacity_octets in its entirety shall be refused outright (queue unchanged, no eviction attempted).", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.9.4/§12.9.5 (queue-full eviction and overflow-bit rule), see REQ-RMAP-059/061 for the byte-budget and per-message-ceiling siblings this rule is independent of" } ] } diff --git a/include/rcp/fragment.hpp b/include/rcp/fragment.hpp index 9fea87d..4d4ca46 100644 --- a/include/rcp/fragment.hpp +++ b/include/rcp/fragment.hpp @@ -13,9 +13,22 @@ // fusa:req REQ-FRAG-013 // fusa:req REQ-FRAG-014 // fusa:req REQ-FRAG-015 -// fusa:req REQ-FRAG-016 // fusa:req REQ-FRAG-017 // fusa:req REQ-FRAG-018 +// +// REQ-FRAG-016 (rcp_fragment_reasm_result_t's own RCP_FRAGMENT_REASM_ERR_ +// ALLOC, "reports an allocation failure distinctly from every other failure +// mode") deliberately has no catalog entry or tag here: it is structurally +// inapplicable, not missing behavior. Reassembler is backed by a fixed +// std::array member (see "Fixed-capacity from day one" below), not +// c-RCP's own realloc()-grown heap buffer, so there is no allocation on the +// reassembly path at all to ever fail -- any attempt to grow past the fixed +// capacity is already covered by ReasmResult::kErrTooLarge (REQ-FRAG-015). +// c-RCP's own REQ-FRAG-016 carries no TC18 citation of its own either, so +// this is a documented judgment call (matching this codebase's established +// convention -- see e.g. Phase 6 batch 1's REQ-ACF-037/REQ-AVTP-016/027 -- +// for an architecture divergence with nothing TC18-scoped to file as a gap), +// not a gap filed to .fusa-reqs-pending.json. // Multi-AVTPDU message fragmentation and reassembly for the TC18 Remote // Control Protocol wire layer (TC18 §13.7.11.3) — brand new to cpp-RCP. diff --git a/tests/test_fragment.cpp b/tests/test_fragment.cpp index 090c81b..c359543 100644 --- a/tests/test_fragment.cpp +++ b/tests/test_fragment.cpp @@ -12,6 +12,7 @@ // fusa:test REQ-FRAG-012 // fusa:test REQ-FRAG-013 // fusa:test REQ-FRAG-014 +// fusa:test REQ-FRAG-010 // fusa:test REQ-FRAG-015 // fusa:test REQ-FRAG-017 // fusa:test REQ-FRAG-018 @@ -20,16 +21,19 @@ // generic primitive underlying TC18 §13.7.11.3 (Phase 17, cpp-RCP issue // #129). Ported from c-RCP's tests/test_fragment.c. // -// REQ-FRAG-010/016 (rcp_fragment_reassembler_destroy() / -// RCP_FRAGMENT_REASM_ERR_ALLOC) have no direct C++ analog here: this +// REQ-FRAG-016 (RCP_FRAGMENT_REASM_ERR_ALLOC) has no direct C++ analog +// here and no catalog entry -- see fragment.hpp's own header comment for +// why this is a documented judgment call, not a gap. REQ-FRAG-010 +// (rcp_fragment_reassembler_destroy()) DOES have a catalog entry: this // module's Reassembler is backed by a fixed std::array member (see // fragment.hpp's own "Fixed-capacity from day one" comment) rather than // c-RCP's realloc()-grown heap buffer, so there is no separate -// destroy()/free step (RAII handles it) and no allocation-failure outcome -// to report -- any attempt to grow past the fixed capacity is folded into -// kErrTooLarge instead, exercised below by -// "feed() rejects a payload exceeding this Reassembler's own fixed -// capacity". +// destroy()/free step of its own -- RAII satisfies the "releases internal +// storage without leaking" requirement instead, exercised below by +// "a Reassembler can be destroyed mid-collection without leaking (RAII)". +// Any attempt to grow past the fixed capacity is folded into kErrTooLarge +// instead, exercised below by "feed() rejects a payload exceeding this +// Reassembler's own fixed capacity". #include #include @@ -218,6 +222,19 @@ TEST_CASE("Reassembler starts empty and not collecting", "[fragment][REQ-FRAG-00 REQUIRE(r.data() == nullptr); } +// ── Reassembler: destruction releases internal storage without leaking ───── + +TEST_CASE("a Reassembler can be destroyed mid-collection without leaking (RAII)", + "[fragment][REQ-FRAG-010]") { + const uint8_t seg0[4] = {1, 2, 3, 4}; + { + Reassembler r(1024); + REQUIRE(r.feed(true, 0, seg0, sizeof(seg0)) == ReasmResult::kContinue); + REQUIRE(r.is_collecting()); + } // destructor runs here, mid-sequence; ASan-checked in CI + SUCCEED(); +} + // ── Reassembler: single-segment (never fragmented) messages ──────────────── TEST_CASE("Reassembler: a single ms=false fragment completes immediately",