pybcsv 1.5.10.dev0__tar.gz → 1.5.11.dev0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/PKG-INFO +1 -1
  2. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/bitset.hpp +6 -0
  3. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_file/file_codec_dispatch.h +52 -0
  4. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_file/file_codec_packet001.h +23 -0
  5. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_file/file_codec_packet_lz4_001.h +6 -0
  6. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_file/file_codec_packet_lz4_batch001.h +91 -17
  7. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_row/row_codec_delta002.h +25 -1
  8. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_row/row_codec_delta002.hpp +25 -2
  9. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_row/row_codec_flat001.hpp +8 -3
  10. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_row/row_codec_zoh001.hpp +4 -1
  11. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/definitions.h +31 -2
  12. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/file_footer.h +12 -0
  13. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/file_header.hpp +18 -2
  14. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/packet_header.h +2 -2
  15. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/reader.hpp +628 -604
  16. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/row.hpp +5 -2
  17. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/writer.h +1 -0
  18. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/writer.hpp +65 -8
  19. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/pybcsv/_version.py +3 -3
  20. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/pybcsv/pandas_utils.py +59 -19
  21. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_cycle4_python_hardening.py +20 -5
  22. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_pybcsv_pandas.py +83 -5
  23. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/BUILD_NOTES.md +0 -0
  24. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/CMakeLists.txt +0 -0
  25. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/LICENSE +0 -0
  26. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/MANIFEST.in +0 -0
  27. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/README.md +0 -0
  28. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/VERSION.txt +0 -0
  29. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/benchmarks/README.md +0 -0
  30. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/benchmarks/run_pybcsv_benchmarks.py +0 -0
  31. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/examples/README.md +0 -0
  32. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/examples/advanced_usage.py +0 -0
  33. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/examples/basic_usage.py +0 -0
  34. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/examples/pandas_integration.py +0 -0
  35. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/examples/performance_benchmark.py +0 -0
  36. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/bcsv.h +0 -0
  37. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/bcsv.hpp +0 -0
  38. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/bcsv_c_api.h +0 -0
  39. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/bitset.h +0 -0
  40. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/byte_buffer.h +0 -0
  41. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/checksum.hpp +0 -0
  42. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_file/file_codec_concept.h +0 -0
  43. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_file/file_codec_stream001.h +0 -0
  44. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_file/file_codec_stream_lz4_001.h +0 -0
  45. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_row/row_codec_dispatch.h +0 -0
  46. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_row/row_codec_flat001.h +0 -0
  47. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_row/row_codec_zoh001.h +0 -0
  48. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/column_name_index.h +0 -0
  49. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/column_name_index.hpp +0 -0
  50. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/csv_reader.h +0 -0
  51. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/csv_reader.hpp +0 -0
  52. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/csv_writer.h +0 -0
  53. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/csv_writer.hpp +0 -0
  54. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/file_header.h +0 -0
  55. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/layout.h +0 -0
  56. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/layout.hpp +0 -0
  57. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/layout_guard.h +0 -0
  58. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/lz4_block.hpp +0 -0
  59. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/lz4_stream.hpp +0 -0
  60. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/reader.h +0 -0
  61. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/reader_concept.h +0 -0
  62. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/row.h +0 -0
  63. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/row_visitors.h +0 -0
  64. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/sampler/sampler.h +0 -0
  65. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/sampler/sampler.hpp +0 -0
  66. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/sampler/sampler_ast.h +0 -0
  67. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/sampler/sampler_compiler.h +0 -0
  68. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/sampler/sampler_parser.h +0 -0
  69. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/sampler/sampler_tokenizer.h +0 -0
  70. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/sampler/sampler_types.h +0 -0
  71. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/sampler/sampler_vm.h +0 -0
  72. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/sampler/sampler_vm.hpp +0 -0
  73. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/sampler/sampler_window.h +0 -0
  74. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/std_charconv_compat.h +0 -0
  75. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/vle.hpp +0 -0
  76. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/writer_concept.h +0 -0
  77. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/lz4-1.10.0/lz4.c +0 -0
  78. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/lz4-1.10.0/lz4.h +0 -0
  79. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/lz4-1.10.0/lz4file.c +0 -0
  80. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/lz4-1.10.0/lz4file.h +0 -0
  81. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/lz4-1.10.0/lz4frame.c +0 -0
  82. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/lz4-1.10.0/lz4frame.h +0 -0
  83. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/lz4-1.10.0/lz4frame_static.h +0 -0
  84. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/lz4-1.10.0/lz4hc.c +0 -0
  85. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/lz4-1.10.0/lz4hc.h +0 -0
  86. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/lz4-1.10.0/xxhash.c +0 -0
  87. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/lz4-1.10.0/xxhash.h +0 -0
  88. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/xxHash-0.8.3/xxhash.c +0 -0
  89. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/xxHash-0.8.3/xxhash.h +0 -0
  90. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/pybcsv/__init__.py +0 -0
  91. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/pybcsv/__init__.pyi +0 -0
  92. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/pybcsv/__version__.py +0 -0
  93. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/pybcsv/_bcsv.pyi +0 -0
  94. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/pybcsv/bindings.cpp +0 -0
  95. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/pybcsv/parquet_utils.py +0 -0
  96. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/pybcsv/polars_utils.py +0 -0
  97. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/pybcsv/py.typed +0 -0
  98. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/pyproject.toml +0 -0
  99. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/sync_headers.py +0 -0
  100. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/README.md +0 -0
  101. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/conftest.py +0 -0
  102. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_basic_functionality.py +0 -0
  103. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_count_rows.py +0 -0
  104. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_csv_writer_reader.py +0 -0
  105. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_data_types.py +0 -0
  106. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_interoperability.py +0 -0
  107. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_minimal_string.py +0 -0
  108. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_parquet_conversion.py +0 -0
  109. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_performance_edge_cases.py +0 -0
  110. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_pybcsv_arrow.py +0 -0
  111. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_pybcsv_codecs.py +0 -0
  112. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_pybcsv_columnar.py +0 -0
  113. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_pybcsv_core.py +0 -0
  114. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_pybcsv_direct_access.py +0 -0
  115. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_pybcsv_errors.py +0 -0
  116. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_pybcsv_performance.py +0 -0
  117. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_pybcsv_polars.py +0 -0
  118. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_pybcsv_sampler.py +0 -0
  119. {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_streaming_bindings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pybcsv
3
- Version: 1.5.10.dev0
3
+ Version: 1.5.11.dev0
4
4
  Summary: High-performance Python bindings for the BCSV (Binary CSV) library with pandas integration
5
5
  Keywords: csv,binary,data-science,compression,performance,pandas,numpy,big-data,data-processing
6
6
  Author-Email: Tobias Weber <weber.tobias.md@gmail.com>
@@ -1606,6 +1606,9 @@ void Bitset<N>::readFrom(const void* src, size_t available) {
1606
1606
  if (available < byteCount()) {
1607
1607
  throw std::out_of_range("Bitset::readFrom: insufficient data");
1608
1608
  }
1609
+ if (byteCount() == 0) {
1610
+ return; // memcpy with a null src is UB even for n=0
1611
+ }
1609
1612
  std::memcpy(data(), src, byteCount());
1610
1613
  clearUnusedBits();
1611
1614
  }
@@ -1615,6 +1618,9 @@ void Bitset<N>::writeTo(void* dst, size_t capacity) const {
1615
1618
  if (capacity < byteCount()) {
1616
1619
  throw std::out_of_range("Bitset::writeTo: insufficient capacity");
1617
1620
  }
1621
+ if (byteCount() == 0) {
1622
+ return; // memcpy with a null dst is UB even for n=0
1623
+ }
1618
1624
  // Ensure padding bits in the last word are zero before export
1619
1625
  const_cast<Bitset*>(this)->clearUnusedBits();
1620
1626
  std::memcpy(dst, data(), byteCount());
@@ -59,6 +59,8 @@ public:
59
59
  using FinalizeFn = void (*)(void* ctx, std::ostream& os, uint64_t totalRows);
60
60
  using WriteBufferFn = ByteBuffer& (*)(void* ctx);
61
61
  using ReadRowFn = std::span<const std::byte> (*)(void* ctx, std::istream& is);
62
+ using ReadGoodFn = bool (*)(const void* ctx, std::istream& is);
63
+ using FinishPacketReadFn = void (*)(void* ctx, std::istream& is);
62
64
  using PacketBoundaryCrossedFn = bool (*)(const void* ctx);
63
65
  using ResetFn = void (*)(void* ctx);
64
66
  using SeekToPacketFn = bool (*)(void* ctx, std::istream& is, std::streamoff offset);
@@ -172,6 +174,25 @@ public:
172
174
  return read_row_fn_(ctx_, is);
173
175
  }
174
176
 
177
+ /// True while more rows may be readable. Synchronous codecs report the
178
+ /// stream state; codecs with an internal I/O thread (batch) answer from
179
+ /// their own state because the main thread must not poll a stream the
180
+ /// background thread may be using concurrently.
181
+ bool readGood(std::istream& is) const {
182
+ assert(ctx_ && read_good_fn_);
183
+ return read_good_fn_(ctx_, is);
184
+ }
185
+
186
+ /// Consume + validate the current packet's terminator/checksum without
187
+ /// touching the next packet. No-op for codecs that validate the whole
188
+ /// packet up front (batch) or have no packet framing (stream codecs).
189
+ /// Throws on checksum mismatch.
190
+ void finishPacketRead(std::istream& is) {
191
+ if (ctx_ && finish_packet_read_fn_) {
192
+ finish_packet_read_fn_(ctx_, is);
193
+ }
194
+ }
195
+
175
196
  bool packetBoundaryCrossed() const {
176
197
  assert(ctx_ && packet_boundary_crossed_fn_);
177
198
  return packet_boundary_crossed_fn_(ctx_);
@@ -218,6 +239,8 @@ private:
218
239
  FinalizeFn finalize_fn_{nullptr};
219
240
  WriteBufferFn write_buffer_fn_{nullptr};
220
241
  ReadRowFn read_row_fn_{nullptr};
242
+ ReadGoodFn read_good_fn_{nullptr};
243
+ FinishPacketReadFn finish_packet_read_fn_{nullptr};
221
244
  PacketBoundaryCrossedFn packet_boundary_crossed_fn_{nullptr};
222
245
  ResetFn reset_fn_{nullptr};
223
246
  SeekToPacketFn seek_to_packet_fn_{nullptr};
@@ -232,6 +255,8 @@ private:
232
255
  finalize_fn_ = nullptr;
233
256
  write_buffer_fn_ = nullptr;
234
257
  read_row_fn_ = nullptr;
258
+ read_good_fn_ = nullptr;
259
+ finish_packet_read_fn_ = nullptr;
235
260
  packet_boundary_crossed_fn_ = nullptr;
236
261
  reset_fn_ = nullptr;
237
262
  seek_to_packet_fn_ = nullptr;
@@ -249,6 +274,8 @@ private:
249
274
  finalize_fn_ = other.finalize_fn_;
250
275
  write_buffer_fn_ = other.write_buffer_fn_;
251
276
  read_row_fn_ = other.read_row_fn_;
277
+ read_good_fn_ = other.read_good_fn_;
278
+ finish_packet_read_fn_ = other.finish_packet_read_fn_;
252
279
  packet_boundary_crossed_fn_ = other.packet_boundary_crossed_fn_;
253
280
  reset_fn_ = other.reset_fn_;
254
281
  seek_to_packet_fn_ = other.seek_to_packet_fn_;
@@ -291,6 +318,31 @@ private:
291
318
  packet_boundary_crossed_fn_ = [](const void* ctx) -> bool {
292
319
  return static_cast<const ConcreteCodec*>(ctx)->packetBoundaryCrossed();
293
320
  };
321
+ // Codecs with an internal I/O thread provide readGood(); synchronous
322
+ // codecs fall back to plain stream-state polling.
323
+ if constexpr (requires(const ConcreteCodec& c, std::istream& is) {
324
+ { c.readGood(is) } -> std::convertible_to<bool>;
325
+ }) {
326
+ read_good_fn_ = [](const void* ctx, std::istream& is) -> bool {
327
+ return static_cast<const ConcreteCodec*>(ctx)->readGood(is);
328
+ };
329
+ } else {
330
+ read_good_fn_ = [](const void* /*ctx*/, std::istream& is) -> bool {
331
+ return is.good();
332
+ };
333
+ }
334
+ // Synchronous packet codecs provide finishPacketRead(); codecs that
335
+ // validate whole packets up front (batch) or have no packet framing
336
+ // (stream) don't need it — the dispatch no-ops for them.
337
+ if constexpr (requires(ConcreteCodec& c, std::istream& is) {
338
+ c.finishPacketRead(is);
339
+ }) {
340
+ finish_packet_read_fn_ = [](void* ctx, std::istream& is) {
341
+ static_cast<ConcreteCodec*>(ctx)->finishPacketRead(is);
342
+ };
343
+ } else {
344
+ finish_packet_read_fn_ = nullptr;
345
+ }
294
346
  reset_fn_ = [](void* ctx) {
295
347
  static_cast<ConcreteCodec*>(ctx)->reset();
296
348
  };
@@ -108,6 +108,9 @@ public:
108
108
  }
109
109
  FileFooter footer(packet_index_, totalRows);
110
110
  footer.write(os);
111
+ if (!os.good()) {
112
+ throw std::runtime_error("FileCodecPacket001: footer write failed");
113
+ }
111
114
  }
112
115
 
113
116
  /// Flush: close the current packet (terminator + checksum), flush the
@@ -193,6 +196,26 @@ public:
193
196
  packet_size_ = 0;
194
197
  }
195
198
 
199
+ /// Consume the current packet's terminator + checksum and validate.
200
+ /// Used by direct access after reading a known number of rows: unlike a
201
+ /// readRow() that crosses the boundary, this STOPS after the checksum —
202
+ /// it never touches the next packet, so corruption there cannot poison
203
+ /// reads of this (valid) packet. Throws on checksum mismatch or a
204
+ /// malformed packet tail.
205
+ void finishPacketRead(std::istream& is) {
206
+ if (!packet_open_) {
207
+ return;
208
+ }
209
+ uint64_t rowLen = 0;
210
+ vleDecode<uint64_t, true>(is, rowLen, &packet_hash_);
211
+ if (rowLen != PCKT_TERMINATOR) {
212
+ throw std::runtime_error(
213
+ "FileCodecPacket001::finishPacketRead: expected packet terminator");
214
+ }
215
+ closePacketRead(is);
216
+ packet_open_ = false;
217
+ }
218
+
196
219
  /// Seek to a specific packet by absolute file offset and prepare for reading.
197
220
  /// Resets all per-packet state and opens the packet at the given offset.
198
221
  /// Returns true if the packet was successfully opened.
@@ -155,6 +155,12 @@ public:
155
155
  return packet_.seekToPacket(is, offset);
156
156
  }
157
157
 
158
+ /// Consume terminator + checksum of the current packet (framing lives in
159
+ /// the inner packet codec — see FileCodecPacket001::finishPacketRead).
160
+ void finishPacketRead(std::istream& is) {
161
+ packet_.finishPacketRead(is);
162
+ }
163
+
158
164
  private:
159
165
  ByteBuffer write_buffer_; // Owned write buffer for RowCodec serialization
160
166
  FileCodecPacket001 packet_; // Handles framing, checksums, packet lifecycle
@@ -37,7 +37,18 @@
37
37
  * BLE(PCKT_TERMINATOR)
38
38
  *
39
39
  * Exceptions that occur on the background thread are captured via
40
- * std::exception_ptr and re-thrown on the next main-thread call.
40
+ * std::exception_ptr (under mutex_) and re-thrown on the main thread at the
41
+ * next synchronization point: packet boundary, flushPacket(), or finalize().
42
+ *
43
+ * Threading contract (main thread ↔ one internal BG thread):
44
+ * - While a BG task is in flight, the BG thread is the only owner of the
45
+ * stream. The main thread must not touch the stream — not even to poll
46
+ * rdstate(); it queries readGood() instead, which answers from
47
+ * main-thread-owned state (packet_open_).
48
+ * - The BG thread leaves the stream in a defined state on every task exit
49
+ * (clears eof/fail bits it caused; EOF is reported exclusively via
50
+ * bg_has_next_packet_).
51
+ * - bg_exception_ is written and read only under mutex_.
41
52
  */
42
53
 
43
54
  #include "file_codec_concept.h"
@@ -64,6 +75,7 @@
64
75
  #include <stop_token>
65
76
  #include <string>
66
77
  #include <thread>
78
+ #include <utility>
67
79
 
68
80
  namespace bcsv {
69
81
 
@@ -127,9 +139,9 @@ public:
127
139
 
128
140
  /// Called before each writeRow(). Returns true if a packet boundary
129
141
  /// was crossed (caller resets RowCodec).
142
+ /// Note: BG failures surface at the next packet boundary, flushPacket()
143
+ /// or finalize() — never on the per-row fast path (no locking here).
130
144
  bool beginWrite(std::ostream& /*os*/, uint64_t rowCnt) {
131
- rethrowBgException();
132
-
133
145
  if (raw_active_->size() >= packet_size_limit_) {
134
146
  // Close the current packet payload.
135
147
  vleEncode<uint64_t, true>(static_cast<uint64_t>(PCKT_TERMINATOR), *raw_active_);
@@ -169,6 +181,9 @@ public:
169
181
  }
170
182
 
171
183
  /// Finalize: flush remaining data, shut down BG thread, write footer.
184
+ /// Any pending BG failure is rethrown unconditionally BEFORE the footer
185
+ /// is written — a file with a corrupt/missing packet must never get a
186
+ /// clean footer.
172
187
  void finalize(std::ostream& /*os*/, uint64_t totalRows) {
173
188
  if (!raw_active_->empty()) {
174
189
  // Close the last packet payload.
@@ -184,16 +199,18 @@ public:
184
199
  bg_task_ = BgTask::COMPRESS_WRITE;
185
200
  }
186
201
  cv_.notify_one();
187
-
188
- waitForBgIdle();
189
- rethrowBgException();
190
202
  }
191
203
 
192
- shutdownBgThread();
204
+ shutdownBgThread(); // waits for any in-flight task, then joins
205
+ rethrowBgException(); // unconditional — surfaces failures from the last packet
193
206
 
194
207
  // Write file footer on main thread (BG is stopped).
195
208
  FileFooter footer(packet_index_, totalRows);
196
209
  footer.write(*os_ptr_);
210
+ if (!os_ptr_->good()) {
211
+ throw std::runtime_error(
212
+ "FileCodecPacketLZ4Batch001: footer write failed");
213
+ }
197
214
  }
198
215
 
199
216
  ByteBuffer& writeBuffer() { return write_buffer_; }
@@ -233,8 +250,10 @@ public:
233
250
 
234
251
  // ── Read lifecycle ──────────────────────────────────────────────────
235
252
 
253
+ /// Note: BG failures (e.g. a corrupt pre-read packet) surface at the
254
+ /// packet boundary inside decodeNextRow() — remaining rows of the
255
+ /// current (validated) packet are served first; no locking per row.
236
256
  std::span<const std::byte> readRow(std::istream& /*is*/) {
237
- rethrowBgException();
238
257
  packet_boundary_crossed_ = false;
239
258
 
240
259
  if (!packet_open_) {
@@ -246,6 +265,14 @@ public:
246
265
 
247
266
  // ── Boundary / state signals ────────────────────────────────────────
248
267
 
268
+ /// Reader liveness query — replaces direct stream polling. The BG
269
+ /// thread may be touching the stream concurrently, so the main thread
270
+ /// must not inspect stream state; packet_open_ is main-thread-owned
271
+ /// and reflects whether more rows may be available.
272
+ bool readGood(std::istream& /*is*/) const noexcept {
273
+ return packet_open_;
274
+ }
275
+
249
276
  bool packetBoundaryCrossed() const noexcept {
250
277
  return packet_boundary_crossed_;
251
278
  }
@@ -290,6 +317,8 @@ private:
290
317
  void startBgThread() {
291
318
  if (bg_thread_.joinable()) return; // Already running
292
319
 
320
+ // No BG thread exists here, so plain writes are race-free; the
321
+ // jthread constructor below provides the happens-before edge.
293
322
  bg_task_ = BgTask::IDLE;
294
323
  bg_exception_ = nullptr;
295
324
  bg_thread_ = std::jthread([this](std::stop_token stoken) { bgLoop(stoken); });
@@ -317,7 +346,19 @@ private:
317
346
  // fires cv_.notify_all() when request_stop() is called, ensuring
318
347
  // the BG thread exits its idle wait without a manual CV notification
319
348
  // from the main thread.
320
- std::stop_callback wake_on_stop(stoken, [this] { cv_.notify_all(); });
349
+ //
350
+ // The lock_guard is essential: request_stop() sets the stop flag
351
+ // OUTSIDE mutex_, so an unlocked notify could land in the window
352
+ // where this thread holds the mutex between a (pre-stop) predicate
353
+ // check and going to sleep — a lost wakeup that leaves the CV wait
354
+ // sleeping and shutdownBgThread()'s join() blocked forever. Taking
355
+ // the mutex serializes the notify against the predicate check.
356
+ // (Reproduced readily by open()+close() cycles under load: the
357
+ // shutdown races the just-started thread's first predicate check.)
358
+ std::stop_callback wake_on_stop(stoken, [this] {
359
+ std::lock_guard<std::mutex> lk(mutex_);
360
+ cv_.notify_all();
361
+ });
321
362
 
322
363
  while (!stoken.stop_requested()) {
323
364
  std::unique_lock<std::mutex> lk(mutex_);
@@ -328,6 +369,7 @@ private:
328
369
  BgTask task = bg_task_;
329
370
  lk.unlock();
330
371
 
372
+ std::exception_ptr ex;
331
373
  try {
332
374
  if (task == BgTask::COMPRESS_WRITE) {
333
375
  bgCompressAndWrite();
@@ -335,11 +377,14 @@ private:
335
377
  bgReadAndDecompress();
336
378
  }
337
379
  } catch (...) {
338
- bg_exception_ = std::current_exception();
380
+ ex = std::current_exception();
339
381
  }
340
382
 
341
383
  {
342
384
  std::lock_guard<std::mutex> lk2(mutex_);
385
+ if (ex) {
386
+ bg_exception_ = ex; // published under mutex_ only
387
+ }
343
388
  bg_task_ = BgTask::IDLE;
344
389
  }
345
390
  cv_.notify_one();
@@ -351,10 +396,16 @@ private:
351
396
  cv_.wait(lk, [this] { return bg_task_ == BgTask::IDLE; });
352
397
  }
353
398
 
399
+ /// Take and rethrow a captured BG exception, if any. Safe to call at
400
+ /// any time (slot is accessed under mutex_), but only used at packet
401
+ /// boundaries / flush / finalize — never on the per-row fast path.
354
402
  void rethrowBgException() {
355
- if (bg_exception_) {
356
- std::exception_ptr ex = bg_exception_;
357
- bg_exception_ = nullptr;
403
+ std::exception_ptr ex;
404
+ {
405
+ std::lock_guard<std::mutex> lk(mutex_);
406
+ ex = std::exchange(bg_exception_, nullptr);
407
+ }
408
+ if (ex) {
358
409
  std::rethrow_exception(ex);
359
410
  }
360
411
  }
@@ -425,6 +476,12 @@ private:
425
476
  is.seekg(pos);
426
477
  return false; // Footer reached
427
478
  } else if (is.eof()) {
479
+ // Raw EOF (footer-less / crashed file): restore a defined
480
+ // stream state. End-of-data is reported to the main thread
481
+ // exclusively via the return value (bg_has_next_packet_) —
482
+ // never via stream flags, which the main thread must not
483
+ // poll while this may run on the BG thread.
484
+ is.clear();
428
485
  return false;
429
486
  } else {
430
487
  is.clear();
@@ -448,9 +505,19 @@ private:
448
505
  "FileCodecPacketLZ4Batch001: failed to read compressed_size");
449
506
  }
450
507
 
451
- if (uncompressed_size > MAX_PACKET_SIZE || compressed_size > MAX_PACKET_SIZE) {
508
+ // Bound declared sizes by what this file's header allows, not by the
509
+ // absolute 1 GiB format limit: a packet payload can exceed the
510
+ // header packet size by at most one row (+ VLE framing + terminator).
511
+ // This stops tiny corrupt/hostile files from triggering huge
512
+ // allocations before checksum validation gets a chance to reject them.
513
+ const uint64_t max_uncompressed =
514
+ static_cast<uint64_t>(packet_size_limit_) + MAX_ROW_LENGTH + 16;
515
+ // LZ4 worst-case bound (LZ4_COMPRESSBOUND formula) inlined in 64-bit:
516
+ // the macro/API take int and would overflow for large packet sizes.
517
+ const uint64_t max_compressed = max_uncompressed + max_uncompressed / 255 + 16;
518
+ if (uncompressed_size > max_uncompressed || compressed_size > max_compressed) {
452
519
  throw std::runtime_error(
453
- "FileCodecPacketLZ4Batch001: packet size exceeds MAX_PACKET_SIZE");
520
+ "FileCodecPacketLZ4Batch001: declared packet size exceeds header packet size limit");
454
521
  }
455
522
 
456
523
  // 3. Read compressed block.
@@ -599,9 +666,16 @@ private:
599
666
  // mutex + cv_.notify) and read by main AFTER waitForBgIdle():
600
667
  // bg_has_next_packet_ — whether read_next_ contains valid data
601
668
  // packet_index_ — BG appends entries; main reads after join()
602
- // bg_exception_ — captured exception; main reads after wait
603
669
  // compressed_buf_ — BG writes here; main never reads it directly
604
670
  //
671
+ // bg_exception_ is written and read ONLY under mutex_ (BG publishes in
672
+ // the same critical section as the IDLE transition; main takes it via
673
+ // rethrowBgException() at packet boundaries / flush / finalize).
674
+ //
675
+ // The stream (is_ptr_/os_ptr_) is owned by the BG thread while a task
676
+ // is in flight. The main thread must not touch it then — not even
677
+ // rdstate(); Reader queries readGood() instead (main-thread state).
678
+ //
605
679
  // All other state is exclusively owned by one thread (documented below).
606
680
 
607
681
  // Main-thread only: RowCodec serialization buffer.
@@ -650,7 +724,7 @@ private:
650
724
  std::mutex mutex_; ///< Protects bg_task_ and provides happens-before edges.
651
725
  std::condition_variable cv_; ///< Signals bg_task_ transitions.
652
726
  BgTask bg_task_{BgTask::IDLE};
653
- std::exception_ptr bg_exception_;///< BG→main via sync protocol.
727
+ std::exception_ptr bg_exception_;///< Guarded by mutex_ (see protocol above).
654
728
  };
655
729
 
656
730
  static_assert(FileCodecConcept<FileCodecPacketLZ4Batch001>,
@@ -69,6 +69,27 @@
69
69
 
70
70
  namespace bcsv {
71
71
 
72
+ /// Cold path of delta002ValidateLengthCode — kept out of line so the string
73
+ /// construction does not bloat the decode hot loop.
74
+ [[noreturn]] inline void delta002ThrowInvalidLengthCode(size_t deltaBytes,
75
+ size_t typeSize) {
76
+ throw std::runtime_error(
77
+ "RowCodecDelta002::deserialize() failed! Invalid delta length code ("
78
+ + std::to_string(deltaBytes) + " bytes for a "
79
+ + std::to_string(typeSize) + "-byte column)");
80
+ }
81
+
82
+ /// Reject a delta header code that implies more payload bytes than the
83
+ /// column type holds. Reachable only from corrupt/hostile input (the
84
+ /// encoder clamps to sizeof(T)); without this check decodeDelta() would
85
+ /// shift past the 64-bit width (UB) and desync all following column
86
+ /// offsets. Shared by the dynamic and static Delta002 decoders.
87
+ inline void delta002ValidateLengthCode(size_t deltaBytes, size_t typeSize) {
88
+ if (deltaBytes > typeSize) [[unlikely]] {
89
+ delta002ThrowInvalidLengthCode(deltaBytes, typeSize);
90
+ }
91
+ }
92
+
72
93
  // ────────────────────────────────────────────────────────────────────────────
73
94
  // RowCodecDelta002 for dynamic Layout
74
95
  // ────────────────────────────────────────────────────────────────────────────
@@ -425,7 +446,10 @@ private:
425
446
 
426
447
  static uint64_t decodeDelta(const std::byte* src, size_t byteCount) {
427
448
  uint64_t result = 0;
428
- for (size_t i = 0; i < byteCount; ++i)
449
+ // Clamped loop bound: total for all inputs + full unrolling.
450
+ // See the dynamic-layout decodeDelta in row_codec_delta002.hpp.
451
+ const size_t n = byteCount <= sizeof(uint64_t) ? byteCount : sizeof(uint64_t);
452
+ for (size_t i = 0; i < n; ++i)
429
453
  result |= static_cast<uint64_t>(static_cast<uint8_t>(src[i])) << (i * 8);
430
454
  return result;
431
455
  }
@@ -24,6 +24,7 @@
24
24
  #include <algorithm>
25
25
  #include <bit>
26
26
  #include <cassert>
27
+ #include <cmath>
27
28
  #include <cstring>
28
29
  #include <type_traits>
29
30
 
@@ -50,7 +51,14 @@ size_t RowCodecDelta002<LayoutType>::encodeDelta(std::byte* dst, uint64_t value,
50
51
  template<typename LayoutType>
51
52
  uint64_t RowCodecDelta002<LayoutType>::decodeDelta(const std::byte* src, size_t byteCount) {
52
53
  uint64_t result = 0;
53
- for (size_t i = 0; i < byteCount; ++i)
54
+ // Clamp the loop bound: makes the function total (no shift past the
55
+ // 64-bit width for any input) and, importantly, gives the compiler a
56
+ // provable max trip count — the byte-assembly loop unrolls fully,
57
+ // which measures FASTER than the unclamped loop (see docs/archive/
58
+ // B2_VALIDATION_COST_INVESTIGATION.md). For valid inputs (encoder
59
+ // clamps to sizeof(T) <= 8) the clamp is an identity.
60
+ const size_t n = byteCount <= sizeof(uint64_t) ? byteCount : sizeof(uint64_t);
61
+ for (size_t i = 0; i < n; ++i)
54
62
  result |= static_cast<uint64_t>(static_cast<uint8_t>(src[i])) << (i * 8);
55
63
  return result;
56
64
  }
@@ -155,6 +163,13 @@ bool RowCodecDelta002<LayoutType>::checkFloatFoC(const std::byte* curr, const st
155
163
  std::memcpy(&p, prev, sizeof(T));
156
164
  std::memcpy(&g, grad, sizeof(T));
157
165
  T predicted = p + g;
166
+ // Never emit FoC for a NaN prediction: the decoder reconstructs the
167
+ // value as prev + grad, and NaN *payload* propagation through
168
+ // arithmetic is implementation-defined — the XOR-delta path is
169
+ // bit-exact on every platform. (Encoder-only guard; wire unchanged.)
170
+ if (std::isnan(predicted)) {
171
+ return false;
172
+ }
158
173
  return std::memcmp(&c, &predicted, sizeof(T)) == 0;
159
174
  }
160
175
 
@@ -453,6 +468,7 @@ void RowCodecDelta002<LayoutType>::deserialize(
453
468
 
454
469
  // ── code ≥ 2: delta with (code-1) bytes ─────────────────────
455
470
  const size_t deltaBytes = code - 1;
471
+ delta002ValidateLengthCode(deltaBytes, sizeof(T));
456
472
 
457
473
  if (dataOff + deltaBytes > buffer.size())
458
474
  throw std::runtime_error(
@@ -627,6 +643,10 @@ bool RowCodecDelta002<LayoutStatic<ColumnTypes...>>::checkFloatFoC(
627
643
  const T& curr, const T& prev, const T& grad)
628
644
  {
629
645
  T predicted = prev + grad;
646
+ // See the dynamic-layout checkFloatFoC: no FoC for NaN predictions.
647
+ if (std::isnan(predicted)) {
648
+ return false;
649
+ }
630
650
  return std::memcmp(&curr, &predicted, sizeof(T)) == 0;
631
651
  }
632
652
 
@@ -694,7 +714,9 @@ RowCodecDelta002<LayoutStatic<ColumnTypes...>>::serialize(
694
714
  T& grad = std::get<ColIdx>(grad_data_);
695
715
 
696
716
  // ── ZoH check ────────────────────────────────────────────
697
- if (curr == prev) {
717
+ // bitEqual: NaN holds (same bits) and -0.0 vs +0.0 counts
718
+ // as a change (matches the dynamic layout's memcmp check).
719
+ if (bitEqual(curr, prev)) {
698
720
  row_header_.encode(ROW_HEADER_BIT_INDEX[ColIdx], HB, 0);
699
721
  grad = T{};
700
722
  return;
@@ -846,6 +868,7 @@ void RowCodecDelta002<LayoutStatic<ColumnTypes...>>::deserialize(
846
868
 
847
869
  // ── code ≥ 2: delta with (code-1) bytes ─────────────────
848
870
  const size_t deltaBytes = code - 1;
871
+ delta002ValidateLengthCode(deltaBytes, sizeof(T));
849
872
 
850
873
  if (dataOff + deltaBytes > buffer.size())
851
874
  throw std::runtime_error(
@@ -75,10 +75,13 @@ inline std::span<std::byte> RowCodecFlat001<LayoutType>::serialize(
75
75
  const uint32_t* offsets = layout_->columnOffsets().data();
76
76
 
77
77
  // ── Pre-scan: sum string payload sizes for a single buffer.resize() ──
78
+ // Clamp to MAX_STRING_LENGTH — the write loop below truncates to that
79
+ // limit, and the buffer size must match the bytes actually written
80
+ // (an unclamped size would leave uninitialized bytes in the row span).
78
81
  size_t strg_payload = 0;
79
82
  for (size_t i = 0; i < count; ++i) {
80
83
  if (types[i] == ColumnType::STRING) {
81
- strg_payload += row.strg_[offsets[i]].size();
84
+ strg_payload += std::min(row.strg_[offsets[i]].size(), MAX_STRING_LENGTH);
82
85
  }
83
86
  }
84
87
 
@@ -195,12 +198,14 @@ inline std::span<std::byte> RowCodecFlat001<LayoutStatic<ColumnTypes...>>::seria
195
198
 
196
199
  const size_t offRow = buffer.size();
197
200
 
198
- // Compute total string payload via fold expression
201
+ // Compute total string payload via fold expression. Clamped to
202
+ // MAX_STRING_LENGTH to match the truncating write in serializeElements
203
+ // (see the dynamic-layout pre-scan above).
199
204
  size_t strg_payload = 0;
200
205
  [&]<size_t... I>(std::index_sequence<I...>) {
201
206
  (([&] {
202
207
  if constexpr (std::is_same_v<typename RowType::template column_type<I>, std::string>) {
203
- strg_payload += std::get<I>(row.data_).size();
208
+ strg_payload += std::min(std::get<I>(row.data_).size(), MAX_STRING_LENGTH);
204
209
  }
205
210
  }()), ...);
206
211
  }(std::index_sequence_for<ColumnTypes...>{});
@@ -446,7 +446,10 @@ RowCodecZoH001<LayoutStatic<ColumnTypes...>>::serialize(
446
446
  std::get<I>(prev_data_) = std::get<I>(row.data_);
447
447
  }
448
448
  } else {
449
- if (std::get<I>(row.data_) != std::get<I>(prev_data_)) {
449
+ // bitEqual: NaN holds (same bits) and -0.0 vs +0.0 counts as
450
+ // a change — IEEE operator== would silently turn -0.0 into
451
+ // the held +0.0 on the decoder side.
452
+ if (!bitEqual(std::get<I>(row.data_), std::get<I>(prev_data_))) {
450
453
  row_header_.set(ROW_HEADER_BIT_INDEX[I], true);
451
454
  hasAnyChange = true;
452
455
  std::get<I>(prev_data_) = std::get<I>(row.data_);
@@ -41,7 +41,35 @@
41
41
  #endif
42
42
 
43
43
  namespace bcsv {
44
-
44
+
45
+ // The wire format is written by memcpy of native integers and documented
46
+ // as little-endian. Until explicit byte-order conversion exists, refuse
47
+ // to compile on big-endian targets rather than silently producing files
48
+ // no other platform can read.
49
+ static_assert(std::endian::native == std::endian::little,
50
+ "BCSV wire format requires a little-endian target");
51
+
52
+ /// Value equality with bit-exact semantics for floating-point types:
53
+ /// NaN equals NaN (same bit pattern) and -0.0 does NOT equal +0.0.
54
+ /// Codec change detection (ZoH/Delta) must use this instead of
55
+ /// operator== so signed zeros round-trip exactly and repeated NaNs
56
+ /// hold — IEEE operator== breaks both (NaN != NaN; -0.0 == +0.0).
57
+ /// Non-floating types fall back to operator==.
58
+ /// BCSV_ALWAYS_INLINE: without it, the extra call in the templated
59
+ /// serialize fold-expressions perturbs GCC's TU-wide inlining budget and
60
+ /// measurably degrades unrelated (decode) hot loops — see
61
+ /// docs/archive/B2_VALIDATION_COST_INVESTIGATION.md for the same effect class.
62
+ template<typename T>
63
+ [[nodiscard]] BCSV_ALWAYS_INLINE bool bitEqual(const T& a, const T& b) {
64
+ if constexpr (std::is_same_v<T, float>) {
65
+ return std::bit_cast<uint32_t>(a) == std::bit_cast<uint32_t>(b);
66
+ } else if constexpr (std::is_same_v<T, double>) {
67
+ return std::bit_cast<uint64_t>(a) == std::bit_cast<uint64_t>(b);
68
+ } else {
69
+ return a == b;
70
+ }
71
+ }
72
+
45
73
  // Configuration
46
74
  constexpr bool RANGE_CHECKING = true;
47
75
  constexpr bool STRING_OVERFLOW_THROWS = true; // true = throw std::length_error on string > MAX_STRING_LENGTH, false = silently truncate (legacy behavior)
@@ -80,7 +108,8 @@ namespace bcsv {
80
108
  constexpr size_t MAX_COLUMN_COUNT = std::numeric_limits<uint16_t>::max(); // Maximum number of columns
81
109
  constexpr size_t MAX_COLUMN_LENGTH = std::numeric_limits<uint16_t>::max(); // Maximum width of column content
82
110
  constexpr size_t MAX_STRING_LENGTH = std::numeric_limits<uint16_t>::max(); // Maximum length of string data (wire format uses uint16_t lengths)
83
- constexpr size_t MAX_ROW_LENGTH = (1ULL << 24) - 2 ; // ~16 MB maximum row size, 4b BLE encoding (2 bits for length), reserve 0xFFFF for terminator.
111
+ constexpr size_t MAX_ROW_LENGTH = (1ULL << 24) - 2 ; // ~16 MB maximum row size, 4b BLE encoding (2 bits for length); safely below PCKT_TERMINATOR (0x3FFFFFFF) so a row length can never collide with the terminator marker.
112
+ constexpr size_t MAX_HEADER_NAME_BYTES = 16 * 1024 * 1024; // Cumulative cap on column-name bytes in a file header (hostile-input guard)
84
113
  constexpr size_t MIN_PACKET_SIZE = 64 * 1024; // 64KB minimum packet size
85
114
  constexpr size_t DEFAULT_PACKET_SIZE_KB = 8192; // 8MB default packet size (in KB, for Writer::open blockSizeKB param)
86
115
  constexpr size_t MAX_PACKET_SIZE = 1024 * 1024 * 1024; // 1GB maximum packet size
@@ -246,6 +246,18 @@ namespace bcsv {
246
246
  return false;
247
247
  }
248
248
 
249
+ // Validate start_offset before any arithmetic on it: it must
250
+ // cover at least magic + ConstSection (or `indexSize` below
251
+ // underflows size_t), and it cannot reach past the start of the
252
+ // file (this also bounds the index allocation by the file size).
253
+ stream.seekg(0, std::ios::end);
254
+ const std::streamoff fileSize = stream.tellg();
255
+ if (const_section_.start_offset < sizeof(ConstSection) + 4 ||
256
+ static_cast<std::streamoff>(const_section_.start_offset) > fileSize) {
257
+ stream.seekg(originalPos);
258
+ return false;
259
+ }
260
+
249
261
  // Seek to index start
250
262
  stream.seekg(-static_cast<int64_t>(const_section_.start_offset), std::ios::end);
251
263