LCOV - code coverage report
Current view: top level - wirestead/wrapper - bounded_receive.hpp (source / functions) Coverage Total Hit
Test: Wirestead Coverage Report Lines: 92.2 % 154 142
Test Date: 2026-10-29 11:36:05 Functions: 88.9 % 63 56
Legend: Lines: hit not hit | Branches: + taken - not taken # not executed Branches: 72.2 % 72 52

             Branch data     Line data    Source code
       1                 :             : /*
       2                 :             :  * Copyright 2025 Jinwoo Sung
       3                 :             :  *
       4                 :             :  * Licensed under the Apache License, Version 2.0 (the "License");
       5                 :             :  * you may not use this file except in compliance with the License.
       6                 :             :  * You may obtain a copy of the License at
       7                 :             :  *
       8                 :             :  *     http://www.apache.org/licenses/LICENSE-2.0
       9                 :             :  *
      10                 :             :  * Unless required by applicable law or agreed to in writing, software
      11                 :             :  * distributed under the License is distributed on an "AS IS" BASIS,
      12                 :             :  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      13                 :             :  * See the License for the specific language governing permissions and
      14                 :             :  * limitations under the License.
      15                 :             :  */
      16                 :             : 
      17                 :             : #pragma once
      18                 :             : 
      19                 :             : #include <algorithm>
      20                 :             : #include <functional>
      21                 :             : #include <limits>
      22                 :             : #include <optional>
      23                 :             : #include <typeinfo>
      24                 :             : 
      25                 :             : #include "wirestead/framer/length_prefix_framer.hpp"
      26                 :             : #include "wirestead/framer/line_framer.hpp"
      27                 :             : #include "wirestead/framer/packet_framer.hpp"
      28                 :             : #include "wirestead/wrapper/context.hpp"
      29                 :             : #include "wirestead/wrapper/receive_budget.hpp"
      30                 :             : 
      31                 :             : namespace wirestead::wrapper::detail {
      32                 :             : struct ReceiveOverflow {
      33                 :             :   ReceiveOverflowReason reason;
      34                 :             : };
      35                 :        3740 : inline std::shared_ptr<ReceiveCharge> reserve_receive(const std::shared_ptr<ReceiveScope>& scope, size_t bytes) {
      36                 :        3740 :   auto charge = scope->reserve(bytes);
      37         [ +  + ]:        3740 :   if (!charge) throw ReceiveOverflow{ReceiveOverflowReason::ByteLimit};
      38                 :        3722 :   return charge;
      39                 :          18 : }
      40                 :             : 
      41                 :             : // Reservation precedes payload allocation and outlives the payload, including
      42                 :             : // a batch moved out of the queue into a user callback.
      43                 :             : struct ReceiveMessage {
      44                 :             :   std::shared_ptr<ReceiveCharge> charge;
      45                 :             :   MessageContext context;
      46                 :        1348 :   ReceiveMessage(std::shared_ptr<ReceiveCharge> c, ClientId id, memory::ConstByteSpan bytes)
      47                 :        4044 :       : charge(std::move(c)), context(id, memory::SafeDataBuffer(bytes)) {}
      48                 :        3231 :   ReceiveMessage(ReceiveMessage&&) noexcept = default;
      49                 :             :   ReceiveMessage& operator=(ReceiveMessage&&) noexcept = default;
      50                 :             : };
      51                 :             : inline thread_local ReceiveMessage* prepared_message = nullptr;
      52                 :             : class PreparedMessageGuard {
      53                 :             :  public:
      54                 :        1235 :   explicit PreparedMessageGuard(ReceiveMessage& message) : previous_(prepared_message) { prepared_message = &message; }
      55                 :        1235 :   ~PreparedMessageGuard() { prepared_message = previous_; }
      56                 :             : 
      57                 :             :  private:
      58                 :             :   ReceiveMessage* previous_;
      59                 :             : };
      60                 :             : // Consume staged ownership at the wrapper message boundary, before any user
      61                 :             : // callback can reenter another receive path. Raw input retention never adopts
      62                 :             : // another dispatch's storage, even when it happens to borrow the same address.
      63                 :        1227 : inline std::optional<ReceiveMessage> take_prepared_message(ClientId id, memory::ConstByteSpan bytes) {
      64   [ +  +  +  -  :        2453 :   if (prepared_message && prepared_message->charge && prepared_message->context.client_id() == id &&
                   +  - ]
      65   [ +  -  +  -  :        3680 :       prepared_message->context.safe_data().data() == bytes.data() &&
                   +  + ]
      66                 :        1226 :       prepared_message->context.safe_data().size() == bytes.size()) {
      67                 :        1226 :     return std::move(*prepared_message);
      68                 :             :   }
      69                 :           1 :   return std::nullopt;
      70                 :             : }
      71                 :        1366 : inline ReceiveMessage retain_received(const std::shared_ptr<ReceiveScope>& scope, ClientId id,
      72                 :             :                                       memory::ConstByteSpan bytes) {
      73                 :             :   // Conservative record allowance also covers staging and vector growth.
      74                 :        1366 :   constexpr size_t records = 4 * (sizeof(MessageContext) + sizeof(std::shared_ptr<ReceiveCharge>));
      75         [ -  + ]:        1366 :   if (bytes.size() > std::numeric_limits<size_t>::max() - records)
      76                 :           0 :     throw ReceiveOverflow{ReceiveOverflowReason::ByteLimit};
      77                 :        1366 :   return ReceiveMessage(reserve_receive(scope, bytes.size() + records), id, bytes);
      78                 :             : }
      79                 :             : 
      80                 :             : class ReceiveBatch {
      81                 :             :  public:
      82                 :        3812 :   ReceiveBatch() = default;
      83                 :         749 :   ReceiveBatch(ReceiveBatch&&) noexcept = default;
      84                 :          51 :   ReceiveBatch& operator=(ReceiveBatch&& other) noexcept {
      85         [ +  - ]:          51 :     if (this != &other) {
      86                 :          51 :       clear();
      87                 :          51 :       charges_ = std::move(other.charges_);
      88                 :          51 :       messages_ = std::move(other.messages_);
      89                 :             :     }
      90                 :          51 :     return *this;
      91                 :             :   }
      92                 :        1111 :   bool empty() const { return messages_.empty(); }
      93                 :         470 :   size_t size() const { return messages_.size(); }
      94                 :       10029 :   void clear() {
      95                 :       10029 :     std::vector<MessageContext>().swap(messages_);
      96                 :       10029 :     std::vector<std::shared_ptr<ReceiveCharge>>().swap(charges_);
      97                 :       10029 :   }
      98                 :         263 :   void emplace_back(ReceiveMessage message) {
      99                 :         263 :     charges_.push_back(message.charge);
     100                 :             :     try {
     101                 :         263 :       messages_.push_back(std::move(message.context));
     102                 :           0 :     } catch (...) {
     103                 :           0 :       charges_.pop_back();
     104                 :           0 :       throw;
     105                 :           0 :     }
     106                 :         263 :   }
     107                 :         110 :   operator const std::vector<MessageContext>&() const { return messages_; }
     108                 :             : 
     109                 :             :  private:
     110                 :             :   std::vector<std::shared_ptr<ReceiveCharge>> charges_;
     111                 :             :   std::vector<MessageContext> messages_;
     112                 :             : };
     113                 :             : 
     114                 :             : // Friend access changes neither existing public virtual tables nor layouts.
     115                 :             : // Exact type checks intentionally exclude custom subclasses with hidden state.
     116                 :             : struct ReceiveFramerAccess {
     117                 :             :   template <class F>
     118                 :        9886 :   static F* exact(framer::IFramer* f) {
     119   [ +  -  +  -  :        9886 :     return f && typeid(*f) == typeid(F) ? static_cast<F*>(f) : nullptr;
                   +  + ]
     120                 :             :   }
     121                 :             :   template <class Fn>
     122                 :        9808 :   static bool visit(framer::IFramer* f, Fn&& fn) {
     123         [ +  + ]:        9808 :     if (auto* p = exact<framer::LineFramer>(f)) {
     124                 :        9736 :       fn(*p);
     125                 :        9736 :       return true;
     126                 :             :     }
     127         [ +  + ]:          72 :     if (auto* p = exact<framer::LengthPrefixFramer>(f)) {
     128                 :          66 :       fn(*p);
     129                 :          66 :       return true;
     130                 :             :     }
     131         [ +  - ]:           6 :     if (auto* p = exact<framer::PacketFramer>(f)) {
     132                 :           6 :       fn(*p);
     133                 :           6 :       return true;
     134                 :             :     }
     135                 :           0 :     return false;
     136                 :             :   }
     137                 :        2375 :   static size_t capacity(framer::IFramer* f) {
     138                 :        2375 :     size_t n = 0;
     139                 :        4750 :     visit(f, [&](auto& p) { n = p.buffer_.capacity(); });
     140                 :        2375 :     return n;
     141                 :             :   }
     142                 :        1206 :   static size_t size(framer::IFramer* f) {
     143                 :        1206 :     size_t n = 0;
     144                 :        2412 :     visit(f, [&](auto& p) { n = p.buffer_.size(); });
     145                 :        1206 :     return n;
     146                 :             :   }
     147                 :        1179 :   static bool supported(framer::IFramer* f) {
     148                 :        2358 :     return visit(f, [](auto&) {});
     149                 :             :   }
     150                 :        1179 :   static std::unique_ptr<framer::IFramer> clone(framer::IFramer* f) {
     151                 :        1179 :     std::unique_ptr<framer::IFramer> result;
     152                 :        2358 :     visit(f, [&](auto& p) {
     153                 :             :       using F = std::decay_t<decltype(p)>;
     154                 :        1179 :       result = std::make_unique<F>(p);
     155                 :        1179 :     });
     156                 :        1179 :     return result;
     157                 :           0 :   }
     158                 :        1195 :   static void reserve_exact(framer::IFramer* f, size_t n) {
     159                 :        2390 :     visit(f, [&](auto& p) {
     160         [ -  + ]:        1195 :       if (n <= p.buffer_.capacity()) return;
     161                 :        1195 :       std::vector<uint8_t> replacement;
     162                 :        1195 :       replacement.reserve(n);
     163                 :        1195 :       replacement.insert(replacement.end(), p.buffer_.begin(), p.buffer_.end());
     164                 :        1195 :       p.buffer_.swap(replacement);
     165                 :        1195 :     });
     166                 :        1195 :   }
     167                 :        1170 :   static void commit(framer::IFramer* target, framer::IFramer* source) {
     168                 :        2340 :     visit(target, [&](auto& p) {
     169                 :             :       using F = std::decay_t<decltype(p)>;
     170                 :        1170 :       auto& q = *static_cast<F*>(source);
     171                 :        1170 :       p.buffer_.swap(q.buffer_);
     172                 :             :       if constexpr (std::is_same_v<F, framer::LineFramer>) {
     173                 :        1168 :         std::swap(p.scanned_idx_, q.scanned_idx_);
     174                 :        1168 :         std::swap(p.discarding_, q.discarding_);
     175                 :             :       } else if constexpr (std::is_same_v<F, framer::PacketFramer>) {
     176                 :           0 :         std::swap(p.scanned_idx_, q.scanned_idx_);
     177                 :           0 :         std::swap(p.state_, q.state_);
     178                 :             :       }
     179                 :        1170 :     });
     180                 :        1170 :   }
     181                 :        1168 :   static framer::IFramer::MessageCallback callback(framer::IFramer* f) {
     182                 :        1168 :     framer::IFramer::MessageCallback result;
     183                 :        2336 :     visit(f, [&](auto& p) { result = p.on_message_; });
     184                 :        1168 :     return result;
     185                 :           0 :   }
     186                 :        4640 :   static void release_storage(framer::IFramer* f) {
     187         [ +  + ]:        4640 :     if (!f) return;
     188                 :         336 :     f->reset();
     189                 :         672 :     visit(f, [](auto& p) { std::vector<uint8_t>().swap(p.buffer_); });
     190                 :             :   }
     191                 :             : };
     192                 :             : struct ReceiveState {
     193                 :        2032 :   explicit ReceiveState(std::shared_ptr<ReceiveScope> s) : scope(std::move(s)) {}
     194                 :             :   std::shared_ptr<ReceiveScope> scope;
     195                 :             :   std::shared_ptr<ReceiveCharge> frame_storage;
     196                 :        4640 :   void reset(framer::IFramer* f) {
     197                 :        4640 :     ReceiveFramerAccess::release_storage(f);
     198                 :        4640 :     frame_storage.reset();
     199                 :        4640 :   }
     200                 :             : };
     201                 :             : 
     202                 :             : struct PreparedReceive {
     203                 :             :   std::optional<ReceiveMessage> raw;
     204                 :             :   // MSVC debug vectors allocate an iterator proxy even when empty. Construct
     205                 :             :   // staging only for a built-in framer so raw scalar receive stays allocation-free.
     206                 :             :   std::optional<std::vector<ReceiveMessage>> messages;
     207                 :             :   std::shared_ptr<framer::IFramer> framer;
     208                 :             :   memory::ConstByteSpan input;
     209                 :             :   bool builtin = false;
     210                 :        3346 :   void deliver() {
     211         [ +  + ]:        3370 :     if (!framer) return;
     212         [ -  + ]:        1168 :     if (!builtin) {
     213                 :           0 :       framer->push_bytes(input);
     214                 :           0 :       return;
     215                 :             :     }
     216                 :        1168 :     auto callback = ReceiveFramerAccess::callback(framer.get());
     217         [ +  + ]:        1168 :     if (!callback) return;
     218         [ +  + ]:        2375 :     for (auto& message : *messages) {
     219                 :        1231 :       auto bytes = message.context.safe_data().as_span();
     220                 :        1231 :       PreparedMessageGuard guard(message);
     221                 :        1231 :       callback(bytes);
     222                 :        1231 :     }
     223                 :        1168 :   }
     224                 :             : };
     225                 :             : 
     226                 :             : // Stage a whole input before exposing callbacks. A rejected UDP datagram cannot
     227                 :             : // partially alter an existing built-in framer or leak some decoded messages.
     228                 :        3384 : inline PreparedReceive prepare_receive(ReceiveState& state, const std::shared_ptr<framer::IFramer>& f, ClientId id,
     229                 :             :                                        memory::ConstByteSpan input, bool raw_batch) {
     230                 :        3384 :   PreparedReceive result;
     231                 :        3384 :   result.framer = f;
     232                 :        3384 :   result.input = input;
     233   [ +  +  +  + ]:        3384 :   if (raw_batch) result.raw.emplace(retain_received(state.scope, id, input));
     234   [ +  +  +  -  :        3367 :   if (!f || !ReceiveFramerAccess::supported(f.get())) return result;
             -  +  +  + ]
     235                 :        1179 :   result.builtin = true;
     236                 :        1179 :   result.messages.emplace();
     237                 :        1179 :   const auto limit = state.scope->limits().max_frame_bytes;
     238                 :        1179 :   const auto capacity = ReceiveFramerAccess::capacity(f.get());
     239         [ -  + ]:        1179 :   if (capacity > limit) throw ReceiveOverflow{ReceiveOverflowReason::FrameLimit};
     240   [ +  +  -  +  :        1179 :   if (!state.frame_storage && capacity) state.frame_storage = reserve_receive(state.scope, capacity);
             -  +  -  - ]
     241                 :        1179 :   auto work_charge = reserve_receive(state.scope, capacity);
     242                 :        1179 :   auto work = ReceiveFramerAccess::clone(f.get());
     243                 :        2358 :   work->on_message(
     244                 :        2426 :       [&](memory::ConstByteSpan message) { result.messages->emplace_back(retain_received(state.scope, id, message)); });
     245                 :        1179 :   size_t offset = 0;
     246         [ +  + ]:        2373 :   while (offset < input.size()) {
     247                 :        1203 :     const auto used = ReceiveFramerAccess::size(work.get());
     248         [ +  + ]:        1203 :     if (used >= limit) throw ReceiveOverflow{ReceiveOverflowReason::FrameLimit};
     249                 :        1195 :     const auto n = std::min({input.size() - offset, limit - used, size_t(4096)});
     250                 :        1195 :     const auto needed = used + n;
     251   [ +  -  +  - ]:        1195 :     if (needed > ReceiveFramerAccess::capacity(work.get())) {
     252                 :             :       // Keep the old allocation charged until reserve_exact has destroyed it.
     253                 :        1195 :       auto next = reserve_receive(state.scope, needed);
     254                 :        1195 :       ReceiveFramerAccess::reserve_exact(work.get(), needed);
     255                 :        1195 :       work_charge = std::move(next);
     256                 :        1195 :     }
     257                 :        1195 :     work->push_bytes(input.subspan(offset, n));
     258                 :        1194 :     offset += n;
     259                 :             :   }
     260                 :        1170 :   ReceiveFramerAccess::commit(f.get(), work.get());
     261                 :        1170 :   work.reset();  // destroy old source storage before releasing its reservation
     262                 :        1170 :   state.frame_storage = std::move(work_charge);
     263                 :        1170 :   return result;
     264                 :        1214 : }
     265                 :             : }  // namespace wirestead::wrapper::detail
        

Generated by: LCOV version 2.0-1