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.
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/PKG-INFO +1 -1
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/bitset.hpp +6 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_file/file_codec_dispatch.h +52 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_file/file_codec_packet001.h +23 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_file/file_codec_packet_lz4_001.h +6 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_file/file_codec_packet_lz4_batch001.h +91 -17
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_row/row_codec_delta002.h +25 -1
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_row/row_codec_delta002.hpp +25 -2
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_row/row_codec_flat001.hpp +8 -3
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_row/row_codec_zoh001.hpp +4 -1
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/definitions.h +31 -2
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/file_footer.h +12 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/file_header.hpp +18 -2
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/packet_header.h +2 -2
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/reader.hpp +628 -604
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/row.hpp +5 -2
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/writer.h +1 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/writer.hpp +65 -8
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/pybcsv/_version.py +3 -3
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/pybcsv/pandas_utils.py +59 -19
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_cycle4_python_hardening.py +20 -5
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_pybcsv_pandas.py +83 -5
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/BUILD_NOTES.md +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/CMakeLists.txt +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/LICENSE +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/MANIFEST.in +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/README.md +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/VERSION.txt +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/benchmarks/README.md +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/benchmarks/run_pybcsv_benchmarks.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/examples/README.md +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/examples/advanced_usage.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/examples/basic_usage.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/examples/pandas_integration.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/examples/performance_benchmark.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/bcsv.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/bcsv.hpp +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/bcsv_c_api.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/bitset.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/byte_buffer.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/checksum.hpp +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_file/file_codec_concept.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_file/file_codec_stream001.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_file/file_codec_stream_lz4_001.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_row/row_codec_dispatch.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_row/row_codec_flat001.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_row/row_codec_zoh001.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/column_name_index.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/column_name_index.hpp +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/csv_reader.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/csv_reader.hpp +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/csv_writer.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/csv_writer.hpp +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/file_header.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/layout.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/layout.hpp +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/layout_guard.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/lz4_block.hpp +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/lz4_stream.hpp +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/reader.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/reader_concept.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/row.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/row_visitors.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/sampler/sampler.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/sampler/sampler.hpp +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/sampler/sampler_ast.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/sampler/sampler_compiler.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/sampler/sampler_parser.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/sampler/sampler_tokenizer.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/sampler/sampler_types.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/sampler/sampler_vm.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/sampler/sampler_vm.hpp +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/sampler/sampler_window.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/std_charconv_compat.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/vle.hpp +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/writer_concept.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/lz4-1.10.0/lz4.c +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/lz4-1.10.0/lz4.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/lz4-1.10.0/lz4file.c +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/lz4-1.10.0/lz4file.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/lz4-1.10.0/lz4frame.c +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/lz4-1.10.0/lz4frame.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/lz4-1.10.0/lz4frame_static.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/lz4-1.10.0/lz4hc.c +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/lz4-1.10.0/lz4hc.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/lz4-1.10.0/xxhash.c +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/lz4-1.10.0/xxhash.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/xxHash-0.8.3/xxhash.c +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/xxHash-0.8.3/xxhash.h +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/pybcsv/__init__.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/pybcsv/__init__.pyi +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/pybcsv/__version__.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/pybcsv/_bcsv.pyi +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/pybcsv/bindings.cpp +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/pybcsv/parquet_utils.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/pybcsv/polars_utils.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/pybcsv/py.typed +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/pyproject.toml +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/sync_headers.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/README.md +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/conftest.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_basic_functionality.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_count_rows.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_csv_writer_reader.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_data_types.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_interoperability.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_minimal_string.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_parquet_conversion.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_performance_edge_cases.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_pybcsv_arrow.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_pybcsv_codecs.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_pybcsv_columnar.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_pybcsv_core.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_pybcsv_direct_access.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_pybcsv_errors.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_pybcsv_performance.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_pybcsv_polars.py +0 -0
- {pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/tests/test_pybcsv_sampler.py +0 -0
- {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.
|
|
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.
|
{pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_file/file_codec_packet_lz4_001.h
RENAMED
|
@@ -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
|
{pybcsv-1.5.10.dev0 → pybcsv-1.5.11.dev0}/include/bcsv/codec_file/file_codec_packet_lz4_batch001.h
RENAMED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
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
|
|
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_;///<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|