SDDS ToolKit Programs and Libraries for C and Python
Loading...
Searching...
No Matches
SDDSModern.cc
Go to the documentation of this file.
1/**
2 * @file SDDSModern.cc
3 * @brief Standalone C++17 implementation of the serial SDDS protocol.
4 *
5 * @details Implements header parsing and serialization, ASCII and binary page
6 * codecs, projection pushdown, compression, page indexing, append, update,
7 * locking, reconnect, and live-file reading.
8 *
9 * @copyright
10 * - (c) 2026 The University of Chicago
11 *
12 * @license
13 * This file is distributed under the terms of the Software License Agreement
14 * found in the file LICENSE included with this distribution.
15 */
16
17#include "SDDS.hpp"
18
19#include <algorithm>
20#include <array>
21#include <cerrno>
22#include <cfloat>
23#include <charconv>
24#include <cmath>
25#include <cstdio>
26#include <cstring>
27#include <iomanip>
28#include <limits>
29#include <locale>
30#include <sstream>
31#include <system_error>
32#include <unordered_set>
33
34#include <lzma.h>
35#include <zlib.h>
36
37#if defined(_WIN32)
38# if !defined(NOMINMAX)
39# define NOMINMAX
40# endif
41# include <fcntl.h>
42# include <io.h>
43# include <sys/stat.h>
44# include <windows.h>
45#else
46# include <fcntl.h>
47# include <sys/file.h>
48# include <sys/stat.h>
49# include <unistd.h>
50#endif
51
52namespace sdds {
53namespace {
54
55constexpr std::int32_t kInt64RowCount = INT32_MIN;
56
57[[noreturn]] void throwIo(const std::string &message, const std::filesystem::path &path = {}) {
58 throw IoError(ErrorKind::Io, message, path);
59}
60
61[[noreturn]] void throwFormat(const std::string &message, const std::filesystem::path &path = {},
62 std::int64_t page = 0,
63 std::optional<std::string> field = std::nullopt,
64 std::optional<std::uint64_t> offset = std::nullopt,
65 std::optional<std::int64_t> row = std::nullopt) {
66 throw FormatError(ErrorKind::Format, message, path, page, std::move(field), offset, row);
67}
68
69[[noreturn]] void throwType(const std::string &message,
70 std::optional<std::string> field = std::nullopt) {
71 throw TypeError(ErrorKind::Type, message, {}, 0, std::move(field));
72}
73
74[[noreturn]] void throwState(const std::string &message) {
75 throw StateError(ErrorKind::State, message);
76}
77
78[[noreturn]] void throwLimit(const std::string &message, const std::filesystem::path &path = {},
79 std::int64_t page = 0,
80 std::optional<std::string> field = std::nullopt,
81 std::optional<std::int64_t> row = std::nullopt) {
82 throw LimitError(ErrorKind::Limit, message, path, page, std::move(field), std::nullopt, row);
83}
84
85[[noreturn]] void throwWithContext(const Error &error,
86 const std::filesystem::path &path,
87 std::int64_t page,
88 std::optional<std::uint64_t> offset = std::nullopt,
89 std::optional<std::int64_t> row = std::nullopt,
90 std::optional<std::string> field = std::nullopt) {
91 const std::filesystem::path effectivePath = error.path().empty() ? path : error.path();
92 const std::int64_t effectivePage = error.page() ? error.page() : page;
93 const auto effectiveOffset = error.offset() ? error.offset() : offset;
94 const auto effectiveRow = error.row() ? error.row() : row;
95 const auto effectiveField = error.field() ? error.field() : field;
96 switch (error.kind()) {
97 case ErrorKind::Io:
98 throw IoError(ErrorKind::Io, error.what(), effectivePath, effectivePage, effectiveField,
99 effectiveOffset, effectiveRow);
100 case ErrorKind::Format:
101 throw FormatError(ErrorKind::Format, error.what(), effectivePath, effectivePage,
102 effectiveField, effectiveOffset, effectiveRow);
103 case ErrorKind::Type:
104 throw TypeError(ErrorKind::Type, error.what(), effectivePath, effectivePage, effectiveField,
105 effectiveOffset, effectiveRow);
106 case ErrorKind::State:
107 throw StateError(ErrorKind::State, error.what(), effectivePath, effectivePage, effectiveField,
108 effectiveOffset, effectiveRow);
109 case ErrorKind::Limit:
110 throw LimitError(ErrorKind::Limit, error.what(), effectivePath, effectivePage, effectiveField,
111 effectiveOffset, effectiveRow);
112 }
113 throw Error(error);
114}
115
116std::string trim(std::string value) {
117 const auto first = value.find_first_not_of(" \t\r\n");
118 if (first == std::string::npos)
119 return {};
120 const auto last = value.find_last_not_of(" \t\r\n");
121 return value.substr(first, last - first + 1);
122}
123
124std::string lower(std::string value) {
125 std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
126 return static_cast<char>(std::tolower(c));
127 });
128 return value;
129}
130
131bool parseBool(const std::string &value) {
132 const std::string text = lower(trim(value));
133 if (text == "1" || text == "true" || text == "yes")
134 return true;
135 if (text == "0" || text == "false" || text == "no")
136 return false;
137 throwFormat("invalid boolean value: " + value);
138}
139
140template <class T>
141T parseInteger(const std::string &text, const char *what) {
142 T result{};
143 const std::string value = trim(text);
144 const char *begin = value.data();
145 const char *end = begin + value.size();
146 const auto converted = std::from_chars(begin, end, result, 10);
147 if (converted.ec != std::errc() || converted.ptr != end)
148 throwFormat(std::string("invalid ") + what + ": " + text);
149 return result;
150}
151
152std::string unescape(std::string_view input) {
153 std::string result;
154 result.reserve(input.size());
155 for (std::size_t i = 0; i < input.size(); ++i) {
156 if (input[i] != '\\' || i + 1 >= input.size()) {
157 result.push_back(input[i]);
158 continue;
159 }
160 const char next = input[++i];
161 if (next >= '0' && next <= '7') {
162 unsigned value = static_cast<unsigned>(next - '0');
163 unsigned digits = 1;
164 while (digits < 3 && i + 1 < input.size() && input[i + 1] >= '0' &&
165 input[i + 1] <= '7') {
166 value = value * 8U + static_cast<unsigned>(input[++i] - '0');
167 ++digits;
168 }
169 result.push_back(static_cast<char>(value));
170 } else if (next == 'n') {
171 result.push_back('\n');
172 } else if (next == 'r') {
173 result.push_back('\r');
174 } else if (next == 't') {
175 result.push_back('\t');
176 } else {
177 result.push_back(next);
178 }
179 }
180 return result;
181}
182
183std::string quote(const std::optional<std::string> &value) {
184 if (!value)
185 return {};
186 std::string result = "\"";
187 for (unsigned char c : *value) {
188 if (c == '\\' || c == '"' || c == '!') {
189 result.push_back('\\');
190 result.push_back(static_cast<char>(c));
191 } else if (!std::isprint(c)) {
192 char buffer[5];
193 std::snprintf(buffer, sizeof(buffer), "\\%03o", c);
194 result += buffer;
195 } else {
196 result.push_back(static_cast<char>(c));
197 }
198 }
199 result.push_back('"');
200 return result;
201}
202
203std::string quoteRequired(const std::string &value) {
204 return quote(std::optional<std::string>(value));
205}
206
207Type parseType(const std::string &name) {
208 const std::string value = lower(trim(name));
209 if (value == "longdouble") return Type::LongDouble;
210 if (value == "double") return Type::Double;
211 if (value == "float") return Type::Float;
212 if (value == "long64") return Type::Int64;
213 if (value == "ulong64") return Type::UInt64;
214 if (value == "long") return Type::Int32;
215 if (value == "ulong") return Type::UInt32;
216 if (value == "short") return Type::Int16;
217 if (value == "ushort") return Type::UInt16;
218 if (value == "string") return Type::String;
219 if (value == "character") return Type::Character;
220 throwFormat("unknown SDDS type: " + name);
221}
222
223Values emptyValues(Type type) {
224 switch (type) {
225 case Type::LongDouble: return std::vector<long double>{};
226 case Type::Double: return std::vector<double>{};
227 case Type::Float: return std::vector<float>{};
228 case Type::Int64: return std::vector<std::int64_t>{};
229 case Type::UInt64: return std::vector<std::uint64_t>{};
230 case Type::Int32: return std::vector<std::int32_t>{};
231 case Type::UInt32: return std::vector<std::uint32_t>{};
232 case Type::Int16: return std::vector<std::int16_t>{};
233 case Type::UInt16: return std::vector<std::uint16_t>{};
234 case Type::String: return std::vector<std::string>{};
235 case Type::Character: return std::vector<char>{};
236 }
237 throwType("unknown SDDS type");
238}
239
240Scalar defaultScalar(Type type) {
241 switch (type) {
242 case Type::LongDouble: return static_cast<long double>(0);
243 case Type::Double: return 0.0;
244 case Type::Float: return 0.0F;
245 case Type::Int64: return static_cast<std::int64_t>(0);
246 case Type::UInt64: return static_cast<std::uint64_t>(0);
247 case Type::Int32: return static_cast<std::int32_t>(0);
248 case Type::UInt32: return static_cast<std::uint32_t>(0);
249 case Type::Int16: return static_cast<std::int16_t>(0);
250 case Type::UInt16: return static_cast<std::uint16_t>(0);
251 case Type::String: return std::string{};
252 case Type::Character: return static_cast<char>(0);
253 }
254 throwType("unknown SDDS type");
255}
256
257std::size_t valuesSize(const Values &values) {
258 return std::visit([](const auto &v) { return v.size(); }, values);
259}
260
261class Stream {
262 public:
263 virtual ~Stream() = default;
264 virtual std::size_t read(void *data, std::size_t size) = 0;
265 virtual void write(const void *data, std::size_t size) = 0;
266 virtual bool eof() const = 0;
267 virtual bool seekable() const noexcept { return false; }
268 virtual std::optional<std::uint64_t> size() const { return std::nullopt; }
269 virtual std::uint64_t tell() const { throwState("stream is not seekable"); }
270 virtual void seek(std::uint64_t) { throwState("stream is not seekable"); }
271 virtual void truncate(std::uint64_t) { throwState("stream is not seekable"); }
272 virtual void flush() = 0;
273 virtual void sync() { throwState("stream does not support sync"); }
274 virtual void close() = 0;
275};
276
277class InputSourceStream final : public Stream {
278 public:
279 explicit InputSourceStream(std::unique_ptr<InputSource> source) : source_(std::move(source)) {
280 if (!source_) throwState("input source cannot be null");
281 }
282 std::size_t read(void *data, std::size_t size) override { return source_->read(data, size); }
283 void write(const void *, std::size_t) override { throwState("stream is not writable"); }
284 bool eof() const override { return source_->eof(); }
285 bool seekable() const noexcept override { return source_->capabilities().seek; }
286 std::uint64_t tell() const override { return source_->tell(); }
287 void seek(std::uint64_t offset) override { source_->seek(offset); }
288 void flush() override {}
289 void close() override { source_->close(); }
290
291 private:
292 std::unique_ptr<InputSource> source_;
293};
294
295class OutputSinkStream final : public Stream {
296 public:
297 explicit OutputSinkStream(std::unique_ptr<OutputSink> sink) : sink_(std::move(sink)) {
298 if (!sink_) throwState("output sink cannot be null");
299 }
300 std::size_t read(void *, std::size_t) override { throwState("stream is not readable"); }
301 void write(const void *data, std::size_t size) override { sink_->write(data, size); }
302 bool eof() const override { return false; }
303 bool seekable() const noexcept override { return sink_->capabilities().seek; }
304 std::uint64_t tell() const override { return sink_->tell(); }
305 void seek(std::uint64_t offset) override { sink_->seek(offset); }
306 void truncate(std::uint64_t length) override { sink_->truncate(length); }
307 void flush() override { sink_->flush(); }
308 void sync() override { sink_->sync(); }
309 void close() override { sink_->close(); }
310
311 private:
312 std::unique_ptr<OutputSink> sink_;
313};
314
315class FileStream final : public Stream {
316 public:
317 FileStream(FILE *file, std::filesystem::path path, bool owned, bool writable,
318 std::size_t bufferBytes = 256U * 1024U)
319 : file_(file), path_(std::move(path)), owned_(owned), writable_(writable) {
320 if (!file_)
321 throwIo("null file stream", path_);
322 if (bufferBytes) {
323 buffer_.resize(bufferBytes);
324 if (setvbuf(file_, reinterpret_cast<char *>(buffer_.data()), _IOFBF, buffer_.size()) != 0) {
325 if (owned_) std::fclose(file_);
326 file_ = nullptr;
327 throwIo("unable to configure file buffer", path_);
328 }
329 }
330 }
331
332 ~FileStream() override {
333 try { close(); } catch (...) {}
334 }
335
336 std::size_t read(void *data, std::size_t size) override {
337 const std::size_t count = std::fread(data, 1, size, file_);
338 if (count < size && std::ferror(file_))
339 throwIo("read failure: " + std::string(std::strerror(errno)), path_);
340 return count;
341 }
342
343 void write(const void *data, std::size_t size) override {
344 if (!writable_)
345 throwState("stream is not writable");
346 if (size && std::fwrite(data, 1, size, file_) != size)
347 throwIo("write failure: " + std::string(std::strerror(errno)), path_);
348 }
349
350 bool eof() const override { return std::feof(file_) != 0; }
351 bool seekable() const noexcept override { return owned_; }
352
353 std::optional<std::uint64_t> size() const override {
354#if defined(_WIN32)
355 struct _stat64 status{};
356 if (_fstat64(_fileno(file_), &status) != 0 || status.st_size < 0)
357 return std::nullopt;
358#else
359 struct stat status{};
360 if (fstat(fileno(file_), &status) != 0 || status.st_size < 0)
361 return std::nullopt;
362#endif
363 return static_cast<std::uint64_t>(status.st_size);
364 }
365
366 std::uint64_t tell() const override {
367#if defined(_WIN32)
368 const auto offset = _ftelli64(file_);
369#else
370 const auto offset = ftello(file_);
371#endif
372 if (offset < 0)
373 throwIo("unable to query file position", path_);
374 return static_cast<std::uint64_t>(offset);
375 }
376
377 void seek(std::uint64_t offset) override {
378#if defined(_WIN32)
379 const int status = _fseeki64(file_, static_cast<__int64>(offset), SEEK_SET);
380#else
381 if (offset > static_cast<std::uint64_t>(std::numeric_limits<off_t>::max()))
382 throwLimit("file offset exceeds platform limit", path_);
383 const int status = fseeko(file_, static_cast<off_t>(offset), SEEK_SET);
384#endif
385 if (status != 0)
386 throwIo("seek failure", path_);
387 std::clearerr(file_);
388 }
389
390 void truncate(std::uint64_t length) override {
391 flush();
392#if defined(_WIN32)
393 if (_chsize_s(_fileno(file_), length) != 0)
394#else
395 if (length > static_cast<std::uint64_t>(std::numeric_limits<off_t>::max()) ||
396 ftruncate(fileno(file_), static_cast<off_t>(length)) != 0)
397#endif
398 throwIo("unable to truncate output file", path_);
399 }
400
401 void flush() override {
402 if (writable_ && std::fflush(file_) != 0)
403 throwIo("flush failure: " + std::string(std::strerror(errno)), path_);
404 }
405
406 void sync() override {
407 flush();
408#if defined(_WIN32)
409 if (_commit(_fileno(file_)) != 0)
410#else
411 if (::fsync(fileno(file_)) != 0)
412#endif
413 throwIo("fsync failure", path_);
414 }
415
416 FILE *file() const noexcept { return file_; }
417
418 void close() override {
419 if (!file_)
420 return;
421 if (owned_) {
422 FILE *closing = file_;
423 file_ = nullptr;
424 if (std::fclose(closing) != 0)
425 throwIo("close failure", path_);
426 } else if (writable_) {
427 flush();
428 file_ = nullptr;
429 } else {
430 file_ = nullptr;
431 }
432 }
433
434 private:
435 FILE *file_ = nullptr;
436 std::filesystem::path path_;
437 bool owned_ = false;
438 bool writable_ = false;
439 std::vector<unsigned char> buffer_;
440};
441
442class GzipStream final : public Stream {
443 public:
444 GzipStream(gzFile file, std::filesystem::path path, bool writable,
445 std::size_t bufferBytes = 256U * 1024U)
446 : file_(file), path_(std::move(path)), writable_(writable) {
447 if (!file_)
448 throwIo("unable to open gzip stream", path_);
449 if (bufferBytes && gzbuffer(file_, static_cast<unsigned>(
450 std::min<std::size_t>(bufferBytes, UINT_MAX))) != 0) {
451 gzclose(file_);
452 file_ = nullptr;
453 throwIo("unable to configure gzip buffer", path_);
454 }
455 }
456
457 ~GzipStream() override {
458 try { close(); } catch (...) {}
459 }
460
461 std::size_t read(void *data, std::size_t size) override {
462 std::size_t total = 0;
463 while (total < size) {
464 const unsigned amount = static_cast<unsigned>(std::min<std::size_t>(size - total, UINT_MAX));
465 const int result = gzread(file_, static_cast<char *>(data) + total, amount);
466 if (result < 0) {
467 int code = 0;
468 const char *message = gzerror(file_, &code);
469 throwIo(std::string("gzip read failure: ") + (message ? message : "unknown"), path_);
470 }
471 if (result == 0)
472 break;
473 total += static_cast<std::size_t>(result);
474 }
475 return total;
476 }
477
478 void write(const void *data, std::size_t size) override {
479 if (!writable_)
480 throwState("gzip stream is not writable");
481 std::size_t total = 0;
482 while (total < size) {
483 const unsigned amount = static_cast<unsigned>(std::min<std::size_t>(size - total, UINT_MAX));
484 const int result = gzwrite(file_, static_cast<const char *>(data) + total, amount);
485 if (result <= 0)
486 throwIo("gzip write failure", path_);
487 total += static_cast<std::size_t>(result);
488 }
489 }
490
491 bool eof() const override { return gzeof(file_) != 0; }
492 void flush() override {
493 if (writable_ && gzflush(file_, Z_SYNC_FLUSH) != Z_OK)
494 throwIo("gzip flush failure", path_);
495 }
496 void close() override {
497 if (!file_)
498 return;
499 gzFile closing = file_;
500 file_ = nullptr;
501 if (gzclose(closing) != Z_OK)
502 throwIo("gzip close failure", path_);
503 }
504
505 private:
506 gzFile file_ = nullptr;
507 std::filesystem::path path_;
508 bool writable_ = false;
509};
510
511class LzmaStream final : public Stream {
512 public:
513 LzmaStream(FILE *file, std::filesystem::path path, bool writable,
514 std::size_t bufferBytes = 256U * 1024U, std::uint32_t preset = 6,
515 LzmaCheck check = LzmaCheck::Crc64, bool alone = false)
516 : file_(file), path_(std::move(path)), writable_(writable),
517 alone_(alone),
518 input_(std::max<std::size_t>(bufferBytes, 4096U)),
519 output_(std::max<std::size_t>(bufferBytes, 4096U)) {
520 stream_ = LZMA_STREAM_INIT;
521 lzma_ret result = LZMA_OK;
522 if (!writable_) {
523 result = lzma_auto_decoder(&stream_, UINT64_MAX, 0);
524 } else if (alone) {
525 lzma_options_lzma filters{};
526 if (lzma_lzma_preset(&filters, preset)) result = LZMA_OPTIONS_ERROR;
527 else result = lzma_alone_encoder(&stream_, &filters);
528 } else {
529 lzma_check selected = LZMA_CHECK_CRC64;
530 switch (check) {
531 case LzmaCheck::None: selected = LZMA_CHECK_NONE; break;
532 case LzmaCheck::Crc32: selected = LZMA_CHECK_CRC32; break;
533 case LzmaCheck::Crc64: selected = LZMA_CHECK_CRC64; break;
534 case LzmaCheck::Sha256: selected = LZMA_CHECK_SHA256; break;
535 }
536 result = lzma_easy_encoder(&stream_, preset, selected);
537 }
538 if (result != LZMA_OK) {
539 std::fclose(file_);
540 file_ = nullptr;
541 throwIo("unable to initialize LZMA codec", path_);
542 }
543 }
544
545 ~LzmaStream() override {
546 try { close(); } catch (...) {}
547 }
548
549 std::size_t read(void *data, std::size_t size) override {
550 if (writable_)
551 throwState("LZMA stream is not readable");
552 stream_.next_out = static_cast<std::uint8_t *>(data);
553 stream_.avail_out = size;
554 while (stream_.avail_out && !finished_) {
555 if (!stream_.avail_in) {
556 const std::size_t count = std::fread(input_.data(), 1, input_.size(), file_);
557 if (!count && std::ferror(file_))
558 throwIo("LZMA backing-file read failure", path_);
559 stream_.next_in = input_.data();
560 stream_.avail_in = count;
561 inputEof_ = count == 0;
562 }
563 const lzma_ret result = lzma_code(&stream_, inputEof_ ? LZMA_FINISH : LZMA_RUN);
564 if (result == LZMA_STREAM_END) {
565 finished_ = true;
566 } else if (result != LZMA_OK) {
567 throwFormat("invalid LZMA/XZ stream", path_);
568 } else if (inputEof_ && !stream_.avail_in && stream_.avail_out == size) {
569 throwFormat("truncated LZMA/XZ stream", path_);
570 }
571 }
572 return size - stream_.avail_out;
573 }
574
575 void write(const void *data, std::size_t size) override {
576 if (!writable_)
577 throwState("LZMA stream is not writable");
578 stream_.next_in = static_cast<const std::uint8_t *>(data);
579 stream_.avail_in = size;
580 encode(LZMA_RUN);
581 }
582
583 bool eof() const override { return finished_; }
584 void flush() override {
585 if (writable_ && !alone_)
586 encode(LZMA_SYNC_FLUSH);
587 if (std::fflush(file_) != 0)
588 throwIo("LZMA flush failure", path_);
589 }
590 void close() override {
591 if (!file_)
592 return;
593 if (writable_ && !finished_) {
594 encode(LZMA_FINISH);
595 finished_ = true;
596 }
597 lzma_end(&stream_);
598 FILE *closing = file_;
599 file_ = nullptr;
600 if (std::fclose(closing) != 0)
601 throwIo("LZMA close failure", path_);
602 }
603
604 private:
605 void encode(lzma_action action) {
606 do {
607 stream_.next_out = output_.data();
608 stream_.avail_out = output_.size();
609 const lzma_ret result = lzma_code(&stream_, action);
610 const std::size_t produced = output_.size() - stream_.avail_out;
611 if (produced && std::fwrite(output_.data(), 1, produced, file_) != produced)
612 throwIo("LZMA backing-file write failure", path_);
613 if (result == LZMA_STREAM_END) {
614 if (action == LZMA_FINISH)
615 finished_ = true;
616 break;
617 }
618 if (result != LZMA_OK)
619 throwIo("LZMA encoding failure", path_);
620 if (action == LZMA_RUN && stream_.avail_in == 0)
621 break;
622 if (action == LZMA_SYNC_FLUSH && stream_.avail_out != 0)
623 break;
624 } while (true);
625 }
626
627 FILE *file_ = nullptr;
628 std::filesystem::path path_;
629 bool writable_ = false;
630 bool alone_ = false;
631 bool finished_ = false;
632 bool inputEof_ = false;
633 lzma_stream stream_ = LZMA_STREAM_INIT;
634 std::vector<std::uint8_t> input_;
635 std::vector<std::uint8_t> output_;
636};
637
638class GzipCodecStream final : public Stream {
639 public:
640 GzipCodecStream(std::unique_ptr<Stream> backing, bool writable, std::size_t bufferBytes,
641 int level = -1)
642 : backing_(std::move(backing)), writable_(writable),
643 input_(std::max<std::size_t>(bufferBytes, 4096U)),
644 output_(std::max<std::size_t>(bufferBytes, 4096U)) {
645 if (!backing_) throwState("gzip codec requires a backing stream");
646 std::memset(&codec_, 0, sizeof(codec_));
647 const int result = writable_
648 ? deflateInit2(&codec_, level < 0 ? Z_DEFAULT_COMPRESSION : level,
649 Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY)
650 : inflateInit2(&codec_, 15 + 32);
651 if (result != Z_OK) throwIo("unable to initialize gzip codec");
652 }
653 ~GzipCodecStream() override { try { close(); } catch (...) {} }
654
655 std::size_t read(void *data, std::size_t size) override {
656 if (writable_) throwState("gzip stream is not readable");
657 std::size_t total = 0;
658 while (total < size && !finished_) {
659 const std::size_t chunk = std::min<std::size_t>(size - total, UINT_MAX);
660 codec_.next_out = reinterpret_cast<Bytef *>(static_cast<char *>(data) + total);
661 codec_.avail_out = static_cast<uInt>(chunk);
662 while (codec_.avail_out && !finished_) {
663 if (!codec_.avail_in && !inputEof_) {
664 const std::size_t count = backing_->read(input_.data(), input_.size());
665 codec_.next_in = input_.data();
666 codec_.avail_in = static_cast<uInt>(count);
667 inputEof_ = count == 0;
668 }
669 const uInt before = codec_.avail_out;
670 const int result = inflate(&codec_, Z_NO_FLUSH);
671 if (result == Z_STREAM_END) {
672 finished_ = true;
673 } else if (result != Z_OK && result != Z_BUF_ERROR) {
674 throwFormat("invalid gzip stream");
675 } else if (inputEof_ && !codec_.avail_in && codec_.avail_out == before) {
676 throwFormat("truncated gzip stream");
677 }
678 }
679 total += chunk - codec_.avail_out;
680 if (codec_.avail_out) break;
681 }
682 return total;
683 }
684 void write(const void *data, std::size_t size) override {
685 if (!writable_) throwState("gzip stream is not writable");
686 const auto *bytes = static_cast<const unsigned char *>(data);
687 while (size) {
688 const std::size_t chunk = std::min<std::size_t>(size, UINT_MAX);
689 codec_.next_in = const_cast<Bytef *>(reinterpret_cast<const Bytef *>(bytes));
690 codec_.avail_in = static_cast<uInt>(chunk);
691 encode(Z_NO_FLUSH);
692 bytes += chunk;
693 size -= chunk;
694 }
695 }
696 bool eof() const override { return finished_; }
697 void flush() override {
698 if (writable_) encode(Z_SYNC_FLUSH);
699 backing_->flush();
700 }
701 void close() override {
702 if (!backing_) return;
703 if (writable_ && !finished_) {
704 encode(Z_FINISH);
705 finished_ = true;
706 }
707 writable_ ? deflateEnd(&codec_) : inflateEnd(&codec_);
708 backing_->close();
709 backing_.reset();
710 }
711
712 private:
713 void encode(int flushMode) {
714 do {
715 codec_.next_out = output_.data();
716 codec_.avail_out = static_cast<uInt>(output_.size());
717 const int result = deflate(&codec_, flushMode);
718 if (result != Z_OK && result != Z_STREAM_END &&
719 !(flushMode == Z_FINISH && result == Z_BUF_ERROR))
720 throwIo("gzip encoding failure");
721 const std::size_t produced = output_.size() - codec_.avail_out;
722 if (produced) backing_->write(output_.data(), produced);
723 if (result == Z_STREAM_END) { finished_ = true; break; }
724 if (flushMode == Z_NO_FLUSH && codec_.avail_in == 0) break;
725 if (flushMode == Z_SYNC_FLUSH && codec_.avail_out != 0) break;
726 } while (true);
727 }
728 std::unique_ptr<Stream> backing_;
729 bool writable_ = false;
730 bool finished_ = false;
731 bool inputEof_ = false;
732 z_stream codec_{};
733 std::vector<unsigned char> input_;
734 std::vector<unsigned char> output_;
735};
736
737class LzmaCodecStream final : public Stream {
738 public:
739 LzmaCodecStream(std::unique_ptr<Stream> backing, bool writable, std::size_t bufferBytes,
740 std::uint32_t preset = 6, LzmaCheck check = LzmaCheck::Crc64,
741 bool alone = false)
742 : backing_(std::move(backing)), writable_(writable),
743 alone_(alone),
744 input_(std::max<std::size_t>(bufferBytes, 4096U)),
745 output_(std::max<std::size_t>(bufferBytes, 4096U)) {
746 if (!backing_) throwState("LZMA codec requires a backing stream");
747 codec_ = LZMA_STREAM_INIT;
748 lzma_ret result = LZMA_OK;
749 if (!writable_) {
750 result = lzma_auto_decoder(&codec_, UINT64_MAX, 0);
751 } else if (alone) {
752 lzma_options_lzma options{};
753 if (lzma_lzma_preset(&options, preset)) result = LZMA_OPTIONS_ERROR;
754 else result = lzma_alone_encoder(&codec_, &options);
755 } else {
756 lzma_check selected = LZMA_CHECK_CRC64;
757 switch (check) {
758 case LzmaCheck::None: selected = LZMA_CHECK_NONE; break;
759 case LzmaCheck::Crc32: selected = LZMA_CHECK_CRC32; break;
760 case LzmaCheck::Crc64: selected = LZMA_CHECK_CRC64; break;
761 case LzmaCheck::Sha256: selected = LZMA_CHECK_SHA256; break;
762 }
763 result = lzma_easy_encoder(&codec_, preset, selected);
764 }
765 if (result != LZMA_OK) throwIo("unable to initialize LZMA codec");
766 }
767 ~LzmaCodecStream() override { try { close(); } catch (...) {} }
768
769 std::size_t read(void *data, std::size_t size) override {
770 if (writable_) throwState("LZMA stream is not readable");
771 codec_.next_out = static_cast<std::uint8_t *>(data);
772 codec_.avail_out = size;
773 while (codec_.avail_out && !finished_) {
774 if (!codec_.avail_in) {
775 const std::size_t count = backing_->read(input_.data(), input_.size());
776 codec_.next_in = input_.data();
777 codec_.avail_in = count;
778 inputEof_ = count == 0;
779 }
780 const std::size_t before = codec_.avail_out;
781 const lzma_ret result = lzma_code(&codec_, inputEof_ ? LZMA_FINISH : LZMA_RUN);
782 if (result == LZMA_STREAM_END) finished_ = true;
783 else if (result != LZMA_OK) throwFormat("invalid LZMA/XZ stream");
784 else if (inputEof_ && !codec_.avail_in && codec_.avail_out == before)
785 throwFormat("truncated LZMA/XZ stream");
786 }
787 return size - codec_.avail_out;
788 }
789 void write(const void *data, std::size_t size) override {
790 if (!writable_) throwState("LZMA stream is not writable");
791 codec_.next_in = static_cast<const std::uint8_t *>(data);
792 codec_.avail_in = size;
793 encode(LZMA_RUN);
794 }
795 bool eof() const override { return finished_; }
796 void flush() override {
797 if (writable_ && !alone_) encode(LZMA_SYNC_FLUSH);
798 backing_->flush();
799 }
800 void close() override {
801 if (!backing_) return;
802 if (writable_ && !finished_) { encode(LZMA_FINISH); finished_ = true; }
803 lzma_end(&codec_);
804 backing_->close();
805 backing_.reset();
806 }
807
808 private:
809 void encode(lzma_action action) {
810 do {
811 codec_.next_out = output_.data();
812 codec_.avail_out = output_.size();
813 const lzma_ret result = lzma_code(&codec_, action);
814 const std::size_t produced = output_.size() - codec_.avail_out;
815 if (produced) backing_->write(output_.data(), produced);
816 if (result == LZMA_STREAM_END) { if (action == LZMA_FINISH) finished_ = true; break; }
817 if (result != LZMA_OK) throwIo("LZMA encoding failure");
818 if (action == LZMA_RUN && codec_.avail_in == 0) break;
819 if (action == LZMA_SYNC_FLUSH && codec_.avail_out != 0) break;
820 } while (true);
821 }
822 std::unique_ptr<Stream> backing_;
823 bool writable_ = false;
824 bool alone_ = false;
825 bool finished_ = false;
826 bool inputEof_ = false;
827 lzma_stream codec_ = LZMA_STREAM_INIT;
828 std::vector<std::uint8_t> input_;
829 std::vector<std::uint8_t> output_;
830};
831
832std::unique_ptr<Stream> wrapInputCodec(std::unique_ptr<Stream> stream,
833 const ReaderOptions &options) {
834 switch (options.compression) {
835 case Compression::Auto:
836 case Compression::None: return stream;
837 case Compression::Gzip:
838 return std::make_unique<GzipCodecStream>(std::move(stream), false, options.bufferBytes);
839 case Compression::Xz:
840 case Compression::Lzma:
841 return std::make_unique<LzmaCodecStream>(std::move(stream), false, options.bufferBytes);
842 }
843 throwState("unknown compression mode");
844}
845
846std::unique_ptr<Stream> wrapOutputCodec(std::unique_ptr<Stream> stream,
847 const WriterOptions &options) {
848 switch (options.compression) {
849 case Compression::Auto:
850 case Compression::None: return stream;
851 case Compression::Gzip:
852 return std::make_unique<GzipCodecStream>(std::move(stream), true, options.bufferBytes,
853 options.gzipLevel);
854 case Compression::Xz:
855 return std::make_unique<LzmaCodecStream>(std::move(stream), true, options.bufferBytes,
856 options.lzmaPreset, options.lzmaCheck, false);
857 case Compression::Lzma:
858 return std::make_unique<LzmaCodecStream>(std::move(stream), true, options.bufferBytes,
859 options.lzmaPreset, options.lzmaCheck, true);
860 }
861 throwState("unknown compression mode");
862}
863
864Compression compressionFor(const std::filesystem::path &path, Compression requested) {
865 if (requested != Compression::Auto)
866 return requested;
867 const std::string extension = lower(path.extension().string());
868 if (extension == ".gz") return Compression::Gzip;
869 if (extension == ".xz") return Compression::Xz;
870 if (extension == ".lzma") return Compression::Lzma;
871 return Compression::None;
872}
873
874struct FileIdentity {
875 std::uint64_t device = 0;
876 std::uint64_t inode = 0;
877 bool valid = false;
878};
879
880FileIdentity fileIdentity(const std::filesystem::path &path) {
881 if (path.empty()) return {};
882#if defined(_WIN32)
883 const HANDLE handle = CreateFileW(path.wstring().c_str(), FILE_READ_ATTRIBUTES,
884 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
885 nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
886 if (handle == INVALID_HANDLE_VALUE) return {};
887 BY_HANDLE_FILE_INFORMATION information{};
888 const bool found = GetFileInformationByHandle(handle, &information) != 0;
889 CloseHandle(handle);
890 if (!found) return {};
891 return {static_cast<std::uint64_t>(information.dwVolumeSerialNumber),
892 (static_cast<std::uint64_t>(information.nFileIndexHigh) << 32U) |
893 information.nFileIndexLow,
894 true};
895#else
896 struct stat status{};
897 if (stat(path.c_str(), &status) != 0) return {};
898 return {static_cast<std::uint64_t>(status.st_dev),
899 static_cast<std::uint64_t>(status.st_ino), true};
900#endif
901}
902
903bool sameFile(const FileIdentity &left, const FileIdentity &right) {
904 return left.valid && right.valid && left.device == right.device && left.inode == right.inode;
905}
906
907void replaceFile(const std::filesystem::path &source,
908 const std::filesystem::path &destination) {
909#if defined(_WIN32)
910 if (!MoveFileExW(source.wstring().c_str(), destination.wstring().c_str(),
911 MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
912 throwIo("unable to replace output after last-page update", destination);
913#else
914 std::error_code error;
915 std::filesystem::rename(source, destination, error);
916 if (error)
917 throwIo("unable to replace output after last-page update: " + error.message(),
918 destination);
919#endif
920}
921
922class PathLock {
923 public:
924 PathLock(const std::filesystem::path &path, LockMode mode, bool create) : path_(path) {
925 if (mode == LockMode::None || path.empty()) return;
926#if defined(_WIN32)
927 const DWORD access = mode == LockMode::Exclusive ? GENERIC_READ | GENERIC_WRITE : GENERIC_READ;
928 const DWORD disposition = create ? OPEN_ALWAYS : OPEN_EXISTING;
929 handle_ = CreateFileW(path.wstring().c_str(), access,
930 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
931 nullptr, disposition, FILE_ATTRIBUTE_NORMAL, nullptr);
932 if (handle_ == INVALID_HANDLE_VALUE)
933 throwIo("unable to open file for advisory locking", path);
934 OVERLAPPED overlapped{};
935 overlapped.Offset = MAXDWORD;
936 overlapped.OffsetHigh = 0x7FFFFFFFU;
937 const DWORD flags = LOCKFILE_FAIL_IMMEDIATELY |
938 (mode == LockMode::Exclusive ? LOCKFILE_EXCLUSIVE_LOCK : 0U);
939 if (!LockFileEx(handle_, flags, 0, 1, 0, &overlapped)) {
940 CloseHandle(handle_);
941 handle_ = INVALID_HANDLE_VALUE;
942 throwIo("unable to acquire advisory file lock", path);
943 }
944#else
945 const int flags = mode == LockMode::Exclusive ? O_RDWR | (create ? O_CREAT : 0) : O_RDONLY;
946 descriptor_ = ::open(path.c_str(), flags, 0666);
947 if (descriptor_ < 0) throwIo("unable to open file for advisory locking", path);
948 const int operation = (mode == LockMode::Exclusive ? LOCK_EX : LOCK_SH) | LOCK_NB;
949 if (flock(descriptor_, operation) != 0) {
950 ::close(descriptor_);
951 descriptor_ = -1;
952 throwIo("unable to acquire advisory file lock", path);
953 }
954#endif
955 }
956
957 PathLock(const PathLock &) = delete;
958 PathLock &operator=(const PathLock &) = delete;
959 ~PathLock() {
960#if defined(_WIN32)
961 if (handle_ != INVALID_HANDLE_VALUE) {
962 OVERLAPPED overlapped{};
963 overlapped.Offset = MAXDWORD;
964 overlapped.OffsetHigh = 0x7FFFFFFFU;
965 UnlockFileEx(handle_, 0, 1, 0, &overlapped);
966 CloseHandle(handle_);
967 }
968#else
969 if (descriptor_ >= 0) {
970 flock(descriptor_, LOCK_UN);
971 ::close(descriptor_);
972 }
973#endif
974 }
975
976 private:
977 std::filesystem::path path_;
978#if defined(_WIN32)
979 HANDLE handle_ = INVALID_HANDLE_VALUE;
980#else
981 int descriptor_ = -1;
982#endif
983};
984
985std::unique_ptr<PathLock> acquirePathLock(const std::filesystem::path &path, LockMode mode,
986 bool create) {
987 return mode == LockMode::None ? nullptr : std::make_unique<PathLock>(path, mode, create);
988}
989
990void setBinaryMode(FILE *file) {
991#if defined(_WIN32)
992 if (_setmode(_fileno(file), _O_BINARY) == -1)
993 throwIo("unable to set standard stream to binary mode");
994#else
995 (void)file;
996#endif
997}
998
999std::unique_ptr<Stream> openInput(const std::filesystem::path &path, Compression requested,
1000 std::size_t bufferBytes = 256U * 1024U) {
1001 const Compression compression = compressionFor(path, requested);
1002 if (compression == Compression::Gzip) {
1003 return std::make_unique<GzipStream>(gzopen(path.string().c_str(), "rb"), path, false,
1004 bufferBytes);
1005 }
1006 FILE *file = std::fopen(path.string().c_str(), "rb");
1007 if (!file)
1008 throwIo("unable to open input: " + std::string(std::strerror(errno)), path);
1009 if (compression == Compression::Xz || compression == Compression::Lzma)
1010 return std::make_unique<LzmaStream>(file, path, false, bufferBytes);
1011 return std::make_unique<FileStream>(file, path, true, false, bufferBytes);
1012}
1013
1014std::unique_ptr<Stream> openOutput(const std::filesystem::path &path, Compression requested,
1015 const char *mode = "wb",
1016 std::size_t bufferBytes = 256U * 1024U,
1017 int gzipLevel = -1, std::uint32_t lzmaPreset = 6,
1018 LzmaCheck lzmaCheck = LzmaCheck::Crc64) {
1019 const Compression compression = compressionFor(path, requested);
1020 if (gzipLevel < -1 || gzipLevel > 9)
1021 throwState("gzip compression level must be between 0 and 9, or -1 for default");
1022 if (lzmaPreset > 9)
1023 throwState("LZMA preset must be between 0 and 9");
1024 if (compression == Compression::Gzip) {
1025 std::string gzipMode = "wb";
1026 if (gzipLevel >= 0) gzipMode.push_back(static_cast<char>('0' + gzipLevel));
1027 return std::make_unique<GzipStream>(gzopen(path.string().c_str(), gzipMode.c_str()),
1028 path, true, bufferBytes);
1029 }
1030 FILE *file = std::fopen(path.string().c_str(), mode);
1031 if (!file)
1032 throwIo("unable to open output: " + std::string(std::strerror(errno)), path);
1033 if (compression == Compression::Xz || compression == Compression::Lzma) {
1034 if (std::strcmp(mode, "wb") != 0) {
1035 std::fclose(file);
1036 throwState("compressed append/update is not supported");
1037 }
1038 return std::make_unique<LzmaStream>(file, path, true, bufferBytes, lzmaPreset,
1039 lzmaCheck, compression == Compression::Lzma);
1040 }
1041 return std::make_unique<FileStream>(file, path, true, true, bufferBytes);
1042}
1043
1044class BufferedStream {
1045 public:
1046 explicit BufferedStream(std::unique_ptr<Stream> stream,
1047 std::uint64_t maxReadBytes = UINT64_MAX,
1048 std::filesystem::path path = {})
1049 : stream_(std::move(stream)), maxReadBytes_(maxReadBytes), path_(std::move(path)) {}
1050
1051 std::size_t read(void *data, std::size_t size) {
1052 std::size_t total = 0;
1053 if (pushback_ && size) {
1054 *static_cast<unsigned char *>(data) = *pushback_;
1055 pushback_.reset();
1056 total = 1;
1057 ++offset_;
1058 }
1059 while (total < size) {
1060 if (offset_ >= maxReadBytes_)
1061 throwLimit("decompressed input exceeds configured limit", path_);
1062 if (readPosition_ < readSize_) {
1063 const std::size_t count = std::min(size - total, readSize_ - readPosition_);
1064 std::memcpy(static_cast<unsigned char *>(data) + total,
1065 readBuffer_.data() + readPosition_, count);
1066 readPosition_ += count;
1067 offset_ += count;
1068 total += count;
1069 continue;
1070 }
1071 const std::size_t allowed = static_cast<std::size_t>(std::min<std::uint64_t>(
1072 size - total, maxReadBytes_ - offset_));
1073 if (allowed < readBuffer_.size()) {
1074 readPosition_ = 0;
1075 readSize_ = stream_->read(readBuffer_.data(), static_cast<std::size_t>(
1076 std::min<std::uint64_t>(readBuffer_.size(), maxReadBytes_ - offset_)));
1077 if (!readSize_)
1078 break;
1079 continue;
1080 }
1081 const std::size_t count = stream_->read(static_cast<unsigned char *>(data) + total,
1082 allowed);
1083 offset_ += count;
1084 if (!count)
1085 break;
1086 total += count;
1087 }
1088 return total;
1089 }
1090
1091 void readExact(void *data, std::size_t size, const std::filesystem::path &path,
1092 std::int64_t page = 0) {
1093 if (read(data, size) != size)
1094 throwFormat("unexpected end of file", path, page, std::nullopt, offset_);
1095 }
1096
1097 std::optional<unsigned char> get() {
1098 unsigned char value = 0;
1099 if (read(&value, 1) != 1)
1100 return std::nullopt;
1101 return value;
1102 }
1103
1104 void unget(unsigned char value) {
1105 if (pushback_)
1106 throwState("only one byte of pushback is supported");
1107 pushback_ = value;
1108 if (offset_)
1109 --offset_;
1110 }
1111
1112 void skip(std::uint64_t size, const std::filesystem::path &path,
1113 std::int64_t page = 0) {
1114 if (!size) return;
1115 constexpr std::uint64_t seekThreshold = 64U * 1024U;
1116 if (size >= seekThreshold && !pushback_ && stream_->seekable()) {
1117 if (size > UINT64_MAX - tell()) throwLimit("file offset overflow", path, page);
1118 const std::uint64_t destination = tell() + size;
1119 if (destination > maxReadBytes_)
1120 throwLimit("decompressed input exceeds configured limit", path, page);
1121 const auto streamSize = stream_->size();
1122 if (streamSize && destination > *streamSize) {
1123 seek(*streamSize);
1124 throwFormat("unexpected end of file", path, page, std::nullopt, offset_);
1125 }
1126 if (streamSize) {
1127 seek(destination);
1128 return;
1129 }
1130 }
1131 std::array<unsigned char, 65536> discard;
1132 while (size) {
1133 const std::size_t amount = static_cast<std::size_t>(
1134 std::min<std::uint64_t>(size, discard.size()));
1135 if (read(discard.data(), amount) != amount)
1136 throwFormat("unexpected end of file", path, page, std::nullopt, offset_);
1137 size -= amount;
1138 }
1139 }
1140
1141 std::optional<std::string> line(std::uint64_t maxBytes,
1142 const std::filesystem::path &path) {
1143 std::string result;
1144 while (true) {
1145 const auto byte = get();
1146 if (!byte)
1147 return result.empty() ? std::nullopt : std::optional<std::string>(std::move(result));
1148 if (*byte == '\n')
1149 break;
1150 if (*byte != '\r')
1151 result.push_back(static_cast<char>(*byte));
1152 if (result.size() > maxBytes)
1153 throwLimit("line exceeds configured limit", path);
1154 }
1155 return result;
1156 }
1157
1158 void write(const void *data, std::size_t size) {
1159 if (readPosition_ < readSize_ || pushback_) {
1160 if (!stream_->seekable())
1161 throwState("cannot write after buffered input on a sequential stream");
1162 stream_->seek(offset_);
1163 readPosition_ = readSize_ = 0;
1164 pushback_.reset();
1165 }
1166 stream_->write(data, size);
1167 offset_ += size;
1168 }
1169 void write(std::string_view text) { write(text.data(), text.size()); }
1170 bool eof() const { return !pushback_ && readPosition_ == readSize_ && stream_->eof(); }
1171 bool seekable() const noexcept { return stream_->seekable(); }
1172 std::uint64_t tell() const noexcept { return offset_; }
1173 void seek(std::uint64_t offset) {
1174 if (offset > maxReadBytes_)
1175 throwLimit("decompressed input exceeds configured limit", path_);
1176 pushback_.reset();
1177 readPosition_ = readSize_ = 0;
1178 stream_->seek(offset);
1179 offset_ = offset;
1180 }
1181 void flush() { stream_->flush(); }
1182 void truncate(std::uint64_t length) { stream_->truncate(length); }
1183 void close() { stream_->close(); }
1184 void replace(std::unique_ptr<Stream> stream, std::uint64_t offset = 0) {
1185 stream_ = std::move(stream);
1186 pushback_.reset();
1187 readPosition_ = readSize_ = 0;
1188 offset_ = 0;
1189 if (offset) seek(offset);
1190 }
1191 Stream &raw() { return *stream_; }
1192 std::uint64_t offset() const noexcept { return offset_; }
1193
1194 private:
1195 std::unique_ptr<Stream> stream_;
1196 std::optional<unsigned char> pushback_;
1197 std::array<unsigned char, 64U * 1024U> readBuffer_;
1198 std::size_t readPosition_ = 0;
1199 std::size_t readSize_ = 0;
1200 std::uint64_t offset_ = 0;
1201 std::uint64_t maxReadBytes_ = UINT64_MAX;
1202 std::filesystem::path path_;
1203};
1204
1205using Tags = std::vector<std::pair<std::string, std::string>>;
1206
1207Tags parseTags(std::string_view text) {
1208 Tags tags;
1209 std::size_t position = 0;
1210 while (position < text.size()) {
1211 while (position < text.size() &&
1212 (std::isspace(static_cast<unsigned char>(text[position])) || text[position] == ','))
1213 ++position;
1214 if (position >= text.size())
1215 break;
1216 const std::size_t keyStart = position;
1217 while (position < text.size() && text[position] != '=' && text[position] != ',' &&
1218 !std::isspace(static_cast<unsigned char>(text[position])))
1219 ++position;
1220 std::string key(text.substr(keyStart, position - keyStart));
1221 while (position < text.size() && std::isspace(static_cast<unsigned char>(text[position])))
1222 ++position;
1223 if (position >= text.size() || text[position] != '=')
1224 throwFormat("missing '=' after namelist tag " + key);
1225 ++position;
1226 while (position < text.size() && std::isspace(static_cast<unsigned char>(text[position])))
1227 ++position;
1228 std::string value;
1229 if (position < text.size() && text[position] == '"') {
1230 ++position;
1231 bool escaped = false;
1232 while (position < text.size()) {
1233 const char c = text[position++];
1234 if (c == '"' && !escaped)
1235 break;
1236 value.push_back(c);
1237 if (c == '\\' && !escaped)
1238 escaped = true;
1239 else
1240 escaped = false;
1241 }
1242 value = unescape(value);
1243 } else {
1244 const std::size_t valueStart = position;
1245 while (position < text.size() && text[position] != ',' &&
1246 !std::isspace(static_cast<unsigned char>(text[position])))
1247 ++position;
1248 value = unescape(text.substr(valueStart, position - valueStart));
1249 }
1250 tags.emplace_back(lower(std::move(key)), std::move(value));
1251 }
1252 return tags;
1253}
1254
1255std::optional<std::string> tag(const Tags &tags, std::string_view name) {
1256 for (const auto &entry : tags)
1257 if (entry.first == name)
1258 return entry.second;
1259 return std::nullopt;
1260}
1261
1262std::string requiredTag(const Tags &tags, std::string_view name) {
1263 auto value = tag(tags, name);
1264 if (!value)
1265 throwFormat("missing required namelist tag " + std::string(name));
1266 return *value;
1267}
1268
1269struct Namelist {
1270 std::string name;
1271 Tags tags;
1272};
1273
1274std::optional<Namelist> readNamelist(BufferedStream &stream, const ReaderOptions &options,
1275 const std::filesystem::path &path,
1276 std::optional<ByteOrder> &commentOrder,
1277 bool &fixedRowComment) {
1278 std::string command;
1279 bool started = false;
1280 while (true) {
1281 auto line = stream.line(options.limits.maxLayoutCommandBytes, path);
1282 if (!line)
1283 return std::nullopt;
1284 const std::string stripped = trim(*line);
1285 if (!started && !stripped.empty() && stripped.front() == '!') {
1286 const std::string special = lower(stripped);
1287 if (special.find("big-endian") != std::string::npos)
1288 commentOrder = ByteOrder::Big;
1289 if (special.find("little-endian") != std::string::npos)
1290 commentOrder = ByteOrder::Little;
1291 if (special.find("fixed-rowcount") != std::string::npos ||
1292 special.find("fixed-row-count") != std::string::npos)
1293 fixedRowComment = true;
1294 continue;
1295 }
1296 std::size_t start = 0;
1297 if (!started) {
1298 start = line->find('&');
1299 if (start == std::string::npos)
1300 continue;
1301 started = true;
1302 }
1303 if (!command.empty())
1304 command.push_back(' ');
1305 command.append(line->substr(start));
1306 if (command.size() > options.limits.maxLayoutCommandBytes)
1307 throwLimit("layout command exceeds configured limit", path);
1308 bool quoted = false;
1309 bool escaped = false;
1310 for (std::size_t i = 0; i + 3 < command.size(); ++i) {
1311 const char c = command[i];
1312 if (c == '"' && !escaped)
1313 quoted = !quoted;
1314 if (!quoted && c == '&' && lower(command.substr(i, 4)) == "&end") {
1315 const std::string body = command.substr(1, i - 1);
1316 const std::size_t separator = body.find_first_of(" \t\r\n");
1317 Namelist result;
1318 result.name = lower(separator == std::string::npos ? body : body.substr(0, separator));
1319 result.tags = parseTags(separator == std::string::npos ? std::string_view{} :
1320 std::string_view(body).substr(separator + 1));
1321 return result;
1322 }
1323 if (c == '\\' && !escaped)
1324 escaped = true;
1325 else
1326 escaped = false;
1327 }
1328 }
1329}
1330
1331void validateLayout(Layout &layout) {
1332 std::unordered_set<std::string> names;
1333 auto validateFields = [&](auto &definitions, const char *kind) {
1334 names.clear();
1335 for (auto &definition : definitions) {
1336 if (definition.name.empty())
1337 throwFormat(std::string(kind) + " name is empty");
1338 if (!names.insert(definition.name).second)
1339 throwFormat(std::string("duplicate ") + kind + " name: " + definition.name);
1340 }
1341 };
1342 validateFields(layout.parameters, "parameter");
1343 validateFields(layout.arrays, "array");
1344 validateFields(layout.columns, "column");
1345 names.clear();
1346 for (const auto &associate : layout.associates) {
1347 if (associate.name.empty())
1348 throwFormat("associate name is empty");
1349 if (!names.insert(associate.name).second)
1350 throwFormat("duplicate associate name: " + associate.name);
1351 }
1352 for (const auto &array : layout.arrays)
1353 if (array.dimensions < 1)
1354 throwFormat("array dimensions must be positive", {}, 0, array.name);
1355 if (layout.data.linesPerRow < 0 || layout.data.additionalHeaderLines < 0)
1356 throwFormat("negative data-mode count");
1357 if (layout.data.rowCountMode == RowCountMode::Fixed && layout.data.fixedRowIncrement < 1)
1358 throwFormat("fixed row increment must be positive");
1359
1360 std::int32_t version = 1;
1361 auto consider = [&](Type type) {
1362 if (type == Type::UInt16 || type == Type::UInt32) version = std::max(version, 2);
1363 if (type == Type::LongDouble) version = std::max(version, 4);
1364 if (type == Type::Int64 || type == Type::UInt64) version = std::max(version, 5);
1365 };
1366 for (const auto &definition : layout.parameters) consider(definition.type);
1367 for (const auto &definition : layout.arrays) consider(definition.type);
1368 for (const auto &definition : layout.columns) consider(definition.type);
1369 if (layout.data.mode == DataMode::Binary && layout.data.majorOrder == MajorOrder::Column)
1370 version = std::max(version, 3);
1371 layout.version = version;
1372}
1373
1374FieldMetadata fieldFrom(const Tags &tags) {
1375 FieldMetadata field;
1376 field.name = requiredTag(tags, "name");
1377 field.type = parseType(requiredTag(tags, "type"));
1378 field.symbol = tag(tags, "symbol");
1379 field.units = tag(tags, "units");
1380 field.description = tag(tags, "description");
1381 field.format = tag(tags, "format_string");
1382 return field;
1383}
1384
1385void parseLayoutStream(BufferedStream &stream, Layout &layout, const ReaderOptions &options,
1386 const std::filesystem::path &path, std::uint32_t depth,
1387 std::unordered_set<std::string> &includeStack,
1388 std::optional<ByteOrder> &commentOrder, bool &fixedRowComment,
1389 bool topLevel) {
1390 if (depth > options.limits.maxIncludeDepth)
1391 throwLimit("maximum include depth exceeded", path);
1392 if (topLevel) {
1393 auto header = stream.line(options.limits.maxLayoutCommandBytes, path);
1394 if (!header || header->rfind("SDDS", 0) != 0)
1395 throwFormat("missing SDDS version header", path);
1396 const std::int32_t version = parseInteger<std::int32_t>(header->substr(4), "SDDS version");
1397 if (version < 1 || version > 5)
1398 throwFormat("unsupported SDDS version " + std::to_string(version), path);
1399 layout.version = version;
1400 }
1401
1402 while (auto command = readNamelist(stream, options, path, commentOrder, fixedRowComment)) {
1403 if (command->name == "description") {
1404 layout.description = tag(command->tags, "text");
1405 layout.contents = tag(command->tags, "contents");
1406 } else if (command->name == "parameter") {
1407 ParameterDefinition definition;
1408 static_cast<FieldMetadata &>(definition) = fieldFrom(command->tags);
1409 definition.fixedValue = tag(command->tags, "fixed_value");
1410 layout.parameters.push_back(std::move(definition));
1411 } else if (command->name == "column") {
1412 ColumnDefinition definition;
1413 static_cast<FieldMetadata &>(definition) = fieldFrom(command->tags);
1414 if (auto value = tag(command->tags, "field_length"))
1415 definition.fieldLength = parseInteger<std::int32_t>(*value, "field_length");
1416 layout.columns.push_back(std::move(definition));
1417 } else if (command->name == "array") {
1418 ArrayDefinition definition;
1419 static_cast<FieldMetadata &>(definition) = fieldFrom(command->tags);
1420 if (auto value = tag(command->tags, "field_length"))
1421 definition.fieldLength = parseInteger<std::int32_t>(*value, "field_length");
1422 if (auto value = tag(command->tags, "dimensions"))
1423 definition.dimensions = parseInteger<std::int32_t>(*value, "array dimensions");
1424 else
1425 definition.dimensions = 1;
1426 definition.groupName = tag(command->tags, "group_name");
1427 layout.arrays.push_back(std::move(definition));
1428 } else if (command->name == "associate") {
1429 AssociateDefinition definition;
1430 definition.name = requiredTag(command->tags, "name");
1431 definition.filename = tag(command->tags, "filename");
1432 definition.path = tag(command->tags, "path");
1433 definition.description = tag(command->tags, "description");
1434 definition.contents = tag(command->tags, "contents");
1435 if (auto value = tag(command->tags, "sdds"))
1436 definition.isSdds = parseBool(*value);
1437 layout.associates.push_back(std::move(definition));
1438 } else if (command->name == "include") {
1439 const std::filesystem::path includePath(requiredTag(command->tags, "filename"));
1440 const std::string key = std::filesystem::absolute(includePath).lexically_normal().string();
1441 if (!includeStack.insert(key).second)
1442 throwFormat("cyclic SDDS include: " + includePath.string(), path);
1443 auto includeInput = openInput(includePath, Compression::Auto, options.bufferBytes);
1444 BufferedStream included(std::move(includeInput), options.limits.maxDecompressedBytes,
1445 includePath);
1446 parseLayoutStream(included, layout, options, includePath, depth + 1, includeStack,
1447 commentOrder, fixedRowComment, false);
1448 includeStack.erase(key);
1449 } else if (command->name == "data") {
1450 const std::string mode = lower(requiredTag(command->tags, "mode"));
1451 if (mode == "binary") layout.data.mode = DataMode::Binary;
1452 else if (mode == "ascii") layout.data.mode = DataMode::Ascii;
1453 else throwFormat("invalid SDDS data mode: " + mode, path);
1454 if (auto value = tag(command->tags, "lines_per_row"))
1455 layout.data.linesPerRow = parseInteger<std::int32_t>(*value, "lines_per_row");
1456 if (auto value = tag(command->tags, "additional_header_lines"))
1457 layout.data.additionalHeaderLines = parseInteger<std::int32_t>(*value, "additional_header_lines");
1458 if (auto value = tag(command->tags, "column_major_order"))
1459 layout.data.majorOrder = parseBool(*value) ? MajorOrder::Column : MajorOrder::Row;
1460 if (auto value = tag(command->tags, "endian")) {
1461 const std::string order = lower(*value);
1462 if (order == "big") layout.data.byteOrder = ByteOrder::Big;
1463 else if (order == "little") layout.data.byteOrder = ByteOrder::Little;
1464 else throwFormat("invalid endian value: " + *value, path);
1465 }
1466 if (auto value = tag(command->tags, "no_row_counts"))
1467 layout.data.rowCountMode = parseBool(*value) ? RowCountMode::None : RowCountMode::Variable;
1468 if (auto value = tag(command->tags, "fixed_row_count"))
1469 if (parseBool(*value)) layout.data.rowCountMode = RowCountMode::Fixed;
1470 if (fixedRowComment)
1471 layout.data.rowCountMode = RowCountMode::Fixed;
1472 if (commentOrder)
1473 layout.data.byteOrder = *commentOrder;
1474 for (std::int32_t i = 0; i < layout.data.additionalHeaderLines; ++i)
1475 if (!stream.line(options.limits.maxLayoutCommandBytes, path))
1476 throwFormat("unexpected EOF in additional header lines", path);
1477 return;
1478 } else {
1479 throwFormat("unknown SDDS layout command: " + command->name, path);
1480 }
1481 }
1482 if (topLevel)
1483 throwFormat("missing SDDS data command", path);
1484}
1485
1486} // namespace
1487
1488LayoutBuilder::LayoutBuilder(Layout layout) : layout_(std::move(layout)) {}
1489
1490LayoutBuilder &LayoutBuilder::setDescription(std::optional<std::string> text,
1491 std::optional<std::string> contents) {
1492 layout_.description = std::move(text);
1493 layout_.contents = std::move(contents);
1494 return *this;
1495}
1496
1497LayoutBuilder &LayoutBuilder::setDataOptions(DataOptions options) {
1498 layout_.data = options;
1499 return *this;
1500}
1501
1502LayoutBuilder &LayoutBuilder::addParameter(ParameterDefinition definition) {
1503 layout_.parameters.push_back(std::move(definition));
1504 return *this;
1505}
1506
1507LayoutBuilder &LayoutBuilder::addArray(ArrayDefinition definition) {
1508 layout_.arrays.push_back(std::move(definition));
1509 return *this;
1510}
1511
1512LayoutBuilder &LayoutBuilder::addColumn(ColumnDefinition definition) {
1513 layout_.columns.push_back(std::move(definition));
1514 return *this;
1515}
1516
1517LayoutBuilder &LayoutBuilder::addAssociate(AssociateDefinition definition) {
1518 layout_.associates.push_back(std::move(definition));
1519 return *this;
1520}
1521
1522Layout LayoutBuilder::build() const {
1523 Layout result = layout_;
1524 validateLayout(result);
1525 return result;
1526}
1527
1528Page::Page(std::shared_ptr<const Layout> layout, LoadMode load) : layout_(std::move(layout)) {
1529 if (!layout_)
1530 throwState("page requires a layout");
1531 parameters_.reserve(layout_->parameters.size());
1532 for (const auto &definition : layout_->parameters)
1533 parameters_.push_back(defaultScalar(definition.type));
1534 arrays_.reserve(layout_->arrays.size());
1535 for (const auto &definition : layout_->arrays)
1536 arrays_.push_back({std::vector<std::int32_t>(static_cast<std::size_t>(definition.dimensions), 0),
1537 emptyValues(definition.type)});
1538 columns_.reserve(layout_->columns.size());
1539 for (const auto &definition : layout_->columns)
1540 columns_.push_back(emptyValues(definition.type));
1541 const bool loaded = load == LoadMode::All;
1542 parametersLoaded_.assign(parameters_.size(), loaded);
1543 arraysLoaded_.assign(arrays_.size(), loaded);
1544 columnsLoaded_.assign(columns_.size(), loaded);
1545}
1546
1547const Layout &Page::layout() const {
1548 if (!layout_) throwState("page has no layout");
1549 return *layout_;
1550}
1551
1552bool Page::parameterLoaded(std::size_t index) const { return parametersLoaded_.at(index); }
1553bool Page::parameterLoaded(std::string_view name) const {
1554 return parameterLoaded(layout().parameterIndex(name));
1555}
1556bool Page::arrayLoaded(std::size_t index) const { return arraysLoaded_.at(index); }
1557bool Page::arrayLoaded(std::string_view name) const { return arrayLoaded(layout().arrayIndex(name)); }
1558bool Page::columnLoaded(std::size_t index) const { return columnsLoaded_.at(index); }
1559bool Page::columnLoaded(std::string_view name) const {
1560 return columnLoaded(layout().columnIndex(name));
1561}
1562bool Page::allFieldsLoaded() const noexcept {
1563 const auto all = [](const std::vector<bool> &loaded) {
1564 return std::all_of(loaded.begin(), loaded.end(), [](bool value) { return value; });
1565 };
1566 return all(parametersLoaded_) && all(arraysLoaded_) && all(columnsLoaded_);
1567}
1568
1569const Scalar &Page::parameter(std::size_t index) const {
1570 if (!parameterLoaded(index))
1571 throwState("parameter was not requested: " + layout().parameters.at(index).name);
1572 return parameters_.at(index);
1573}
1574const Scalar &Page::parameter(std::string_view name) const { return parameter(layout().parameterIndex(name)); }
1575const ArrayData &Page::array(std::size_t index) const {
1576 if (!arrayLoaded(index))
1577 throwState("array was not requested: " + layout().arrays.at(index).name);
1578 return arrays_.at(index);
1579}
1580const ArrayData &Page::array(std::string_view name) const { return array(layout().arrayIndex(name)); }
1581const Values &Page::column(std::size_t index) const {
1582 if (!columnLoaded(index))
1583 throwState("column was not requested: " + layout().columns.at(index).name);
1584 return columns_.at(index);
1585}
1586const Values &Page::column(std::string_view name) const { return column(layout().columnIndex(name)); }
1587
1588void Page::setParameter(std::size_t index, Scalar value) {
1589 if (typeOf(value) != layout().parameters.at(index).type)
1590 throwType("parameter type mismatch", layout().parameters.at(index).name);
1591 parameters_.at(index) = std::move(value);
1592 parametersLoaded_.at(index) = true;
1593}
1594void Page::setParameter(std::string_view name, Scalar value) {
1595 setParameter(layout().parameterIndex(name), std::move(value));
1596}
1597void Page::setArray(std::size_t index, ArrayData value) {
1598 const auto &definition = layout().arrays.at(index);
1599 if (typeOf(value.values) != definition.type)
1600 throwType("array type mismatch", definition.name);
1601 if (value.dimensions.size() != static_cast<std::size_t>(definition.dimensions))
1602 throwType("array dimension count mismatch", definition.name);
1603 std::uint64_t elements = 1;
1604 for (const auto dimension : value.dimensions) {
1605 if (dimension < 0)
1606 throwType("negative array dimension", definition.name);
1607 if (dimension && elements > UINT64_MAX / static_cast<std::uint64_t>(dimension))
1608 throwLimit("array dimension product overflow", {}, 0, definition.name);
1609 elements *= static_cast<std::uint64_t>(dimension);
1610 }
1611 if (elements != valuesSize(value.values))
1612 throwType("array element count does not match dimensions", definition.name);
1613 arrays_.at(index) = std::move(value);
1614 arraysLoaded_.at(index) = true;
1615}
1616void Page::setArray(std::string_view name, ArrayData value) {
1617 setArray(layout().arrayIndex(name), std::move(value));
1618}
1619void Page::setColumn(std::size_t index, Values value) {
1620 const auto &definition = layout().columns.at(index);
1621 if (typeOf(value) != definition.type)
1622 throwType("column type mismatch", definition.name);
1623 columns_.at(index) = std::move(value);
1624 columnsLoaded_.at(index) = true;
1625 rowCount_ = 0;
1626 for (std::size_t column = 0; column < columns_.size(); ++column)
1627 if (columnsLoaded_[column])
1628 rowCount_ = std::max(rowCount_, static_cast<std::int64_t>(valuesSize(columns_[column])));
1629}
1630void Page::setColumn(std::string_view name, Values value) {
1631 setColumn(layout().columnIndex(name), std::move(value));
1632}
1633
1634namespace {
1635
1636bool nativeBigEndian() noexcept {
1637 const std::uint16_t value = 0x0102;
1638 return *reinterpret_cast<const unsigned char *>(&value) == 0x01;
1639}
1640
1641ByteOrder resolvedOrder(ByteOrder order) noexcept {
1642 if (order != ByteOrder::Native)
1643 return order;
1644 return nativeBigEndian() ? ByteOrder::Big : ByteOrder::Little;
1645}
1646
1647bool mustSwap(ByteOrder order) noexcept {
1648 return (resolvedOrder(order) == ByteOrder::Big) != nativeBigEndian();
1649}
1650
1651template <class T>
1652T readPod(BufferedStream &stream, ByteOrder order, const std::filesystem::path &path,
1653 std::int64_t page) {
1654 T value{};
1655 stream.readExact(&value, sizeof(value), path, page);
1656 if (mustSwap(order)) {
1657 auto *bytes = reinterpret_cast<unsigned char *>(&value);
1658 std::reverse(bytes, bytes + sizeof(value));
1659 }
1660 return value;
1661}
1662
1663template <class T>
1664void writePod(BufferedStream &stream, T value, ByteOrder order) {
1665 if (mustSwap(order)) {
1666 auto *bytes = reinterpret_cast<unsigned char *>(&value);
1667 std::reverse(bytes, bytes + sizeof(value));
1668 }
1669 stream.write(&value, sizeof(value));
1670}
1671
1672long double decodeExtended80(std::array<unsigned char, 16> bytes, ByteOrder order) {
1673 if (resolvedOrder(order) == ByteOrder::Big)
1674 std::reverse(bytes.begin(), bytes.begin() + 12);
1675 std::uint64_t significand = 0;
1676 for (int i = 7; i >= 0; --i)
1677 significand = (significand << 8U) | bytes[static_cast<std::size_t>(i)];
1678 const std::uint16_t signExponent =
1679 static_cast<std::uint16_t>(bytes[8]) |
1680 static_cast<std::uint16_t>(static_cast<std::uint16_t>(bytes[9]) << 8U);
1681 const bool negative = (signExponent & 0x8000U) != 0;
1682 const std::uint16_t exponent = signExponent & 0x7fffU;
1683 if (exponent == 0 && significand == 0)
1684 return negative ? -0.0L : 0.0L;
1685 if (exponent == 0x7fffU) {
1686 if ((significand & UINT64_C(0x7fffffffffffffff)) == 0)
1687 return negative ? -std::numeric_limits<long double>::infinity()
1688 : std::numeric_limits<long double>::infinity();
1689 return std::numeric_limits<long double>::quiet_NaN();
1690 }
1691 const long double fraction = std::ldexp(static_cast<long double>(significand), -63);
1692 const long double value = std::ldexp(fraction, static_cast<int>(exponent) - 16383);
1693 return negative ? -value : value;
1694}
1695
1696std::array<unsigned char, 16> encodeExtended80(long double value, ByteOrder order) {
1697 std::array<unsigned char, 16> bytes{};
1698 const bool negative = std::signbit(value);
1699 const long double magnitude = std::fabs(value);
1700 std::uint16_t exponent = 0;
1701 std::uint64_t significand = 0;
1702 if (std::isnan(magnitude)) {
1703 exponent = 0x7fffU;
1704 significand = UINT64_C(0xc000000000000000);
1705 } else if (std::isinf(magnitude)) {
1706 exponent = 0x7fffU;
1707 significand = UINT64_C(0x8000000000000000);
1708 } else if (magnitude != 0) {
1709 int binaryExponent = 0;
1710 long double fraction = std::frexp(magnitude, &binaryExponent);
1711 fraction *= 2;
1712 --binaryExponent;
1713 const int biased = binaryExponent + 16383;
1714 if (biased <= 0) {
1715 exponent = 0;
1716 significand = static_cast<std::uint64_t>(
1717 std::ldexp(magnitude, 63 + 16382));
1718 } else if (biased >= 0x7fff) {
1719 exponent = 0x7fffU;
1720 significand = UINT64_C(0x8000000000000000);
1721 } else {
1722 exponent = static_cast<std::uint16_t>(biased);
1723 const long double scaled = std::ldexp(fraction, 63);
1724 significand = scaled >= std::ldexp(1.0L, 64)
1725 ? UINT64_MAX
1726 : static_cast<std::uint64_t>(scaled);
1727 }
1728 }
1729 for (std::size_t i = 0; i < 8; ++i) {
1730 bytes[i] = static_cast<unsigned char>(significand & 0xffU);
1731 significand >>= 8U;
1732 }
1733 const std::uint16_t signExponent = exponent | (negative ? 0x8000U : 0U);
1734 bytes[8] = static_cast<unsigned char>(signExponent & 0xffU);
1735 bytes[9] = static_cast<unsigned char>(signExponent >> 8U);
1736 if (resolvedOrder(order) == ByteOrder::Big)
1737 std::reverse(bytes.begin(), bytes.begin() + 12);
1738 return bytes;
1739}
1740
1741Scalar parseAsciiScalar(Type type, const std::string &text) {
1742 if (type == Type::String)
1743 return text;
1744 if (type == Type::Character) {
1745 if (text.empty()) throwFormat("empty character value");
1746 return text.front();
1747 }
1748 const std::string value = trim(text);
1749 char *end = nullptr;
1750 errno = 0;
1751 switch (type) {
1752 case Type::LongDouble: {
1753 const long double result = std::strtold(value.c_str(), &end);
1754 if (errno == ERANGE || end != value.c_str() + value.size())
1755 throwFormat("invalid longdouble value: " + text);
1756 return result;
1757 }
1758 case Type::Double: {
1759 const double result = std::strtod(value.c_str(), &end);
1760 if (errno == ERANGE || end != value.c_str() + value.size())
1761 throwFormat("invalid double value: " + text);
1762 return result;
1763 }
1764 case Type::Float: {
1765 const float result = std::strtof(value.c_str(), &end);
1766 if (errno == ERANGE || end != value.c_str() + value.size())
1767 throwFormat("invalid float value: " + text);
1768 return result;
1769 }
1770 case Type::Int64: return parseInteger<std::int64_t>(value, "long64 value");
1771 case Type::UInt64: return parseInteger<std::uint64_t>(value, "ulong64 value");
1772 case Type::Int32: return parseInteger<std::int32_t>(value, "long value");
1773 case Type::UInt32: return parseInteger<std::uint32_t>(value, "ulong value");
1774 case Type::Int16: return parseInteger<std::int16_t>(value, "short value");
1775 case Type::UInt16: return parseInteger<std::uint16_t>(value, "ushort value");
1776 case Type::String:
1777 case Type::Character: break;
1778 }
1779 throwType("unknown ASCII scalar type");
1780}
1781
1782std::string formatAsciiScalar(const Scalar &value) {
1783 return std::visit([](const auto &item) -> std::string {
1784 using T = std::decay_t<decltype(item)>;
1785 if constexpr (std::is_same_v<T, std::string>) {
1786 return quoteRequired(item);
1787 } else if constexpr (std::is_same_v<T, char>) {
1788 return quoteRequired(std::string(1, item));
1789 } else if constexpr (std::is_floating_point_v<T>) {
1790 std::ostringstream output;
1791 output.imbue(std::locale::classic());
1792 output << std::setprecision(std::numeric_limits<T>::max_digits10) << item;
1793 return output.str();
1794 } else if constexpr (std::is_signed_v<T>) {
1795 return std::to_string(static_cast<long long>(item));
1796 } else {
1797 return std::to_string(static_cast<unsigned long long>(item));
1798 }
1799 }, value);
1800}
1801
1802Scalar readBinaryScalar(BufferedStream &stream, Type type, ByteOrder order,
1803 LongDoubleEncoding longDoubleEncoding,
1804 const ReaderLimits &limits, const std::filesystem::path &path,
1805 std::int64_t page) {
1806 switch (type) {
1807 case Type::LongDouble:
1808 if (longDoubleEncoding == LongDoubleEncoding::LegacyFloat64)
1809 return static_cast<long double>(readPod<double>(stream, order, path, page));
1810 else {
1811 std::array<unsigned char, 16> bytes{};
1812 stream.readExact(bytes.data(), bytes.size(), path, page);
1813 return decodeExtended80(bytes, order);
1814 }
1815 case Type::Double: return readPod<double>(stream, order, path, page);
1816 case Type::Float: return readPod<float>(stream, order, path, page);
1817 case Type::Int64: return readPod<std::int64_t>(stream, order, path, page);
1818 case Type::UInt64: return readPod<std::uint64_t>(stream, order, path, page);
1819 case Type::Int32: return readPod<std::int32_t>(stream, order, path, page);
1820 case Type::UInt32: return readPod<std::uint32_t>(stream, order, path, page);
1821 case Type::Int16: return readPod<std::int16_t>(stream, order, path, page);
1822 case Type::UInt16: return readPod<std::uint16_t>(stream, order, path, page);
1823 case Type::Character: {
1824 char value = 0;
1825 stream.readExact(&value, 1, path, page);
1826 return value;
1827 }
1828 case Type::String: {
1829 const std::int32_t length = readPod<std::int32_t>(stream, order, path, page);
1830 if (length < 0)
1831 throwFormat("negative binary string length", path, page, std::nullopt, stream.offset());
1832 if (static_cast<std::uint64_t>(length) > limits.maxStringBytes)
1833 throwLimit("binary string exceeds configured limit", path, page);
1834 std::string value(static_cast<std::size_t>(length), '\0');
1835 if (length)
1836 stream.readExact(value.data(), value.size(), path, page);
1837 return value;
1838 }
1839 }
1840 throwType("unknown binary scalar type");
1841}
1842
1843std::uint64_t binaryScalarBytes(Type type, LongDoubleEncoding longDoubleEncoding) {
1844 switch (type) {
1845 case Type::LongDouble:
1846 return longDoubleEncoding == LongDoubleEncoding::LegacyFloat64 ? sizeof(double) : 16U;
1847 case Type::Double: return sizeof(double);
1848 case Type::Float: return sizeof(float);
1849 case Type::Int64: return sizeof(std::int64_t);
1850 case Type::UInt64: return sizeof(std::uint64_t);
1851 case Type::Int32: return sizeof(std::int32_t);
1852 case Type::UInt32: return sizeof(std::uint32_t);
1853 case Type::Int16: return sizeof(std::int16_t);
1854 case Type::UInt16: return sizeof(std::uint16_t);
1855 case Type::Character: return 1U;
1856 case Type::String: return 0U;
1857 }
1858 throwType("unknown binary scalar type");
1859}
1860
1861void skipBinaryScalar(BufferedStream &stream, Type type, ByteOrder order,
1862 LongDoubleEncoding longDoubleEncoding, const ReaderLimits &limits,
1863 const std::filesystem::path &path, std::int64_t page) {
1864 if (type != Type::String) {
1865 stream.skip(binaryScalarBytes(type, longDoubleEncoding), path, page);
1866 return;
1867 }
1868 const std::int32_t length = readPod<std::int32_t>(stream, order, path, page);
1869 if (length < 0)
1870 throwFormat("negative binary string length", path, page, std::nullopt, stream.offset());
1871 if (static_cast<std::uint64_t>(length) > limits.maxStringBytes)
1872 throwLimit("binary string exceeds configured limit", path, page);
1873 stream.skip(static_cast<std::uint64_t>(length), path, page);
1874}
1875
1876void reserveValues(Values &values, std::size_t count) {
1877 std::visit([&](auto &items) { items.reserve(count); }, values);
1878}
1879
1880template <class T>
1881T fixedValue(const unsigned char *data, ByteOrder order) {
1882 T value{};
1883 std::memcpy(&value, data, sizeof(value));
1884 if (mustSwap(order)) {
1885 auto *bytes = reinterpret_cast<unsigned char *>(&value);
1886 std::reverse(bytes, bytes + sizeof(value));
1887 }
1888 return value;
1889}
1890
1891void appendFixedBinaryValue(Values &values, Type type, const unsigned char *data,
1892 ByteOrder order, LongDoubleEncoding longDoubleEncoding) {
1893 switch (type) {
1894 case Type::LongDouble:
1895 if (longDoubleEncoding == LongDoubleEncoding::LegacyFloat64)
1896 std::get<std::vector<long double>>(values).push_back(fixedValue<double>(data, order));
1897 else {
1898 std::array<unsigned char, 16> bytes{};
1899 std::memcpy(bytes.data(), data, bytes.size());
1900 std::get<std::vector<long double>>(values).push_back(decodeExtended80(bytes, order));
1901 }
1902 return;
1903 case Type::Double:
1904 std::get<std::vector<double>>(values).push_back(fixedValue<double>(data, order)); return;
1905 case Type::Float:
1906 std::get<std::vector<float>>(values).push_back(fixedValue<float>(data, order)); return;
1907 case Type::Int64:
1908 std::get<std::vector<std::int64_t>>(values).push_back(
1909 fixedValue<std::int64_t>(data, order)); return;
1910 case Type::UInt64:
1911 std::get<std::vector<std::uint64_t>>(values).push_back(
1912 fixedValue<std::uint64_t>(data, order)); return;
1913 case Type::Int32:
1914 std::get<std::vector<std::int32_t>>(values).push_back(
1915 fixedValue<std::int32_t>(data, order)); return;
1916 case Type::UInt32:
1917 std::get<std::vector<std::uint32_t>>(values).push_back(
1918 fixedValue<std::uint32_t>(data, order)); return;
1919 case Type::Int16:
1920 std::get<std::vector<std::int16_t>>(values).push_back(
1921 fixedValue<std::int16_t>(data, order)); return;
1922 case Type::UInt16:
1923 std::get<std::vector<std::uint16_t>>(values).push_back(
1924 fixedValue<std::uint16_t>(data, order)); return;
1925 case Type::Character:
1926 std::get<std::vector<char>>(values).push_back(static_cast<char>(*data)); return;
1927 case Type::String: throwState("string is not a fixed-width binary value");
1928 }
1929 throwType("unknown binary scalar type");
1930}
1931
1932void writeBinaryScalar(BufferedStream &stream, const Scalar &value, ByteOrder order,
1933 LongDoubleEncoding longDoubleEncoding) {
1934 std::visit([&](const auto &item) {
1935 using T = std::decay_t<decltype(item)>;
1936 if constexpr (std::is_same_v<T, long double>) {
1937 if (longDoubleEncoding == LongDoubleEncoding::LegacyFloat64)
1938 writePod(stream, static_cast<double>(item), order);
1939 else {
1940 const auto bytes = encodeExtended80(item, order);
1941 stream.write(bytes.data(), bytes.size());
1942 }
1943 } else if constexpr (std::is_same_v<T, std::string>) {
1944 if (item.size() > static_cast<std::size_t>(INT32_MAX))
1945 throwLimit("string is too large for SDDS binary encoding");
1946 writePod(stream, static_cast<std::int32_t>(item.size()), order);
1947 stream.write(item.data(), item.size());
1948 } else if constexpr (std::is_same_v<T, char>) {
1949 stream.write(&item, 1);
1950 } else {
1951 writePod(stream, item, order);
1952 }
1953 }, value);
1954}
1955
1956template <class T>
1957void storeFixedValue(unsigned char *destination, T value, ByteOrder order) {
1958 if (mustSwap(order)) {
1959 auto *bytes = reinterpret_cast<unsigned char *>(&value);
1960 std::reverse(bytes, bytes + sizeof(value));
1961 }
1962 std::memcpy(destination, &value, sizeof(value));
1963}
1964
1965void storeFixedBinaryValue(unsigned char *destination, const Values &values,
1966 std::size_t index, Type type, ByteOrder order,
1967 LongDoubleEncoding longDoubleEncoding) {
1968 switch (type) {
1969 case Type::LongDouble: {
1970 const long double value = std::get<std::vector<long double>>(values).at(index);
1971 if (longDoubleEncoding == LongDoubleEncoding::LegacyFloat64)
1972 storeFixedValue(destination, static_cast<double>(value), order);
1973 else {
1974 const auto bytes = encodeExtended80(value, order);
1975 std::memcpy(destination, bytes.data(), bytes.size());
1976 }
1977 return;
1978 }
1979 case Type::Double:
1980 storeFixedValue(destination, std::get<std::vector<double>>(values).at(index), order); return;
1981 case Type::Float:
1982 storeFixedValue(destination, std::get<std::vector<float>>(values).at(index), order); return;
1983 case Type::Int64:
1984 storeFixedValue(destination, std::get<std::vector<std::int64_t>>(values).at(index), order);
1985 return;
1986 case Type::UInt64:
1987 storeFixedValue(destination, std::get<std::vector<std::uint64_t>>(values).at(index), order);
1988 return;
1989 case Type::Int32:
1990 storeFixedValue(destination, std::get<std::vector<std::int32_t>>(values).at(index), order);
1991 return;
1992 case Type::UInt32:
1993 storeFixedValue(destination, std::get<std::vector<std::uint32_t>>(values).at(index), order);
1994 return;
1995 case Type::Int16:
1996 storeFixedValue(destination, std::get<std::vector<std::int16_t>>(values).at(index), order);
1997 return;
1998 case Type::UInt16:
1999 storeFixedValue(destination, std::get<std::vector<std::uint16_t>>(values).at(index), order);
2000 return;
2001 case Type::Character:
2002 *destination = static_cast<unsigned char>(std::get<std::vector<char>>(values).at(index));
2003 return;
2004 case Type::String: throwState("string is not a fixed-width binary value");
2005 }
2006 throwType("unknown binary scalar type");
2007}
2008
2009void appendScalar(Values &values, Scalar value) {
2010 if (typeOf(values) != typeOf(value))
2011 throwType("internal scalar/vector type mismatch");
2012 std::visit([&](auto &vector) {
2013 using VectorType = std::decay_t<decltype(vector)>;
2014 using T = typename VectorType::value_type;
2015 vector.push_back(std::get<T>(std::move(value)));
2016 }, values);
2017}
2018
2019Scalar scalarAt(const Values &values, std::size_t index) {
2020 return std::visit([&](const auto &vector) -> Scalar { return vector.at(index); }, values);
2021}
2022
2023std::vector<std::string> splitAsciiTokens(std::string_view line) {
2024 std::vector<std::string> tokens;
2025 std::size_t position = 0;
2026 while (position < line.size()) {
2027 while (position < line.size() && std::isspace(static_cast<unsigned char>(line[position])))
2028 ++position;
2029 if (position >= line.size()) break;
2030 if (line[position] == '!') break;
2031 std::string token;
2032 if (line[position] == '"') {
2033 ++position;
2034 bool escaped = false;
2035 while (position < line.size()) {
2036 const char c = line[position++];
2037 if (c == '"' && !escaped) break;
2038 token.push_back(c);
2039 if (c == '\\' && !escaped) escaped = true;
2040 else escaped = false;
2041 }
2042 token = unescape(token);
2043 } else {
2044 const std::size_t start = position;
2045 while (position < line.size() && !std::isspace(static_cast<unsigned char>(line[position])))
2046 ++position;
2047 token = unescape(line.substr(start, position - start));
2048 }
2049 tokens.push_back(std::move(token));
2050 }
2051 return tokens;
2052}
2053
2054class AsciiCursor {
2055 public:
2056 AsciiCursor(BufferedStream &stream, const ReaderOptions &options,
2057 std::filesystem::path path, std::optional<std::string> pending = std::nullopt)
2058 : stream_(stream), options_(options), path_(std::move(path)), pending_(std::move(pending)) {}
2059
2060 std::optional<std::string> nextLine(bool keepBlank = true) {
2061 while (true) {
2062 std::optional<std::string> line;
2063 if (pending_) {
2064 line = std::move(pending_);
2065 pending_.reset();
2066 } else {
2067 line = stream_.line(options_.limits.maxLayoutCommandBytes, path_);
2068 }
2069 if (!line) return std::nullopt;
2070 const std::string stripped = trim(*line);
2071 if (!stripped.empty() && stripped.front() == '!') continue;
2072 if (!keepBlank && stripped.empty()) continue;
2073 return line;
2074 }
2075 }
2076
2077 std::vector<std::string> requiredTokens(const std::string &what) {
2078 auto line = nextLine(false);
2079 if (!line) throwFormat("unexpected EOF reading " + what, path_);
2080 auto tokens = splitAsciiTokens(*line);
2081 if (tokens.empty()) throwFormat("missing " + what, path_);
2082 return tokens;
2083 }
2084
2085 private:
2086 BufferedStream &stream_;
2087 const ReaderOptions &options_;
2088 std::filesystem::path path_;
2089 std::optional<std::string> pending_;
2090};
2091
2092std::uint64_t checkedArrayElements(const std::vector<std::int32_t> &dimensions,
2093 const ReaderLimits &limits,
2094 const std::filesystem::path &path,
2095 std::int64_t page, const std::string &name) {
2096 std::uint64_t elements = 1;
2097 for (const auto dimension : dimensions) {
2098 if (dimension < 0)
2099 throwFormat("negative array dimension", path, page, name);
2100 if (dimension && elements > UINT64_MAX / static_cast<std::uint64_t>(dimension))
2101 throwLimit("array dimension product overflow", path, page, name);
2102 elements *= static_cast<std::uint64_t>(dimension);
2103 }
2104 if (elements > limits.maxElements || elements > static_cast<std::uint64_t>(SIZE_MAX))
2105 throwLimit("array exceeds configured element limit", path, page, name);
2106 return elements;
2107}
2108
2109std::string fieldTags(const FieldMetadata &field) {
2110 std::string result = "name=" + quoteRequired(field.name) + ", ";
2111 if (field.symbol) result += "symbol=" + quote(field.symbol) + ", ";
2112 if (field.units) result += "units=" + quote(field.units) + ", ";
2113 if (field.description) result += "description=" + quote(field.description) + ", ";
2114 if (field.format) result += "format_string=" + quote(field.format) + ", ";
2115 result += "type=" + std::string(typeName(field.type)) + ", ";
2116 return result;
2117}
2118
2119void writeLayout(BufferedStream &stream, Layout &layout, const WriterOptions &options) {
2120 validateLayout(layout);
2121 layout.version = std::max(layout.version, options.minimumVersion);
2122 if (layout.version < 1 || layout.version > 5)
2123 throwState("minimum SDDS version must be in the range 1 through 5");
2124 stream.write("SDDS" + std::to_string(layout.version) + "\n");
2125 if (layout.data.mode == DataMode::Binary) {
2126 stream.write(resolvedOrder(layout.data.byteOrder) == ByteOrder::Big
2127 ? "!# big-endian\n" : "!# little-endian\n");
2128 }
2129 if (layout.data.rowCountMode == RowCountMode::Fixed)
2130 stream.write("!# fixed-rowcount\n");
2131 if (layout.description || layout.contents) {
2132 std::string command = "&description ";
2133 if (layout.description) command += "text=" + quote(layout.description) + ", ";
2134 if (layout.contents) command += "contents=" + quote(layout.contents) + ", ";
2135 stream.write(command + "&end\n");
2136 }
2137 for (const auto &definition : layout.parameters) {
2138 std::string command = "&parameter " + fieldTags(definition);
2139 if (definition.fixedValue) command += "fixed_value=" + quote(definition.fixedValue) + ", ";
2140 stream.write(command + "&end\n");
2141 }
2142 for (const auto &definition : layout.arrays) {
2143 std::string command = "&array " + fieldTags(definition);
2144 if (definition.groupName) command += "group_name=" + quote(definition.groupName) + ", ";
2145 if (definition.fieldLength) command += "field_length=" + std::to_string(definition.fieldLength) + ", ";
2146 command += "dimensions=" + std::to_string(definition.dimensions) + ", &end\n";
2147 stream.write(command);
2148 }
2149 for (const auto &definition : layout.columns) {
2150 std::string command = "&column " + fieldTags(definition);
2151 if (definition.fieldLength) command += "field_length=" + std::to_string(definition.fieldLength) + ", ";
2152 stream.write(command + "&end\n");
2153 }
2154 for (const auto &definition : layout.associates) {
2155 std::string command = "&associate name=" + quoteRequired(definition.name) + ", ";
2156 if (definition.filename) command += "filename=" + quote(definition.filename) + ", ";
2157 if (definition.path) command += "path=" + quote(definition.path) + ", ";
2158 if (definition.description) command += "description=" + quote(definition.description) + ", ";
2159 if (definition.contents) command += "contents=" + quote(definition.contents) + ", ";
2160 command += std::string("sdds=") + (definition.isSdds ? "1" : "0") + ", &end\n";
2161 stream.write(command);
2162 }
2163 std::string data = "&data mode=";
2164 data += layout.data.mode == DataMode::Binary ? "binary, " : "ascii, ";
2165 if (layout.data.mode == DataMode::Ascii && layout.data.linesPerRow != 1)
2166 data += "lines_per_row=" + std::to_string(layout.data.linesPerRow) + ", ";
2167 if (layout.data.rowCountMode == RowCountMode::None) data += "no_row_counts=1, ";
2168 if (layout.data.rowCountMode == RowCountMode::Fixed) data += "fixed_row_count=1, ";
2169 if (layout.data.mode == DataMode::Binary) {
2170 data += resolvedOrder(layout.data.byteOrder) == ByteOrder::Big ? "endian=big, " : "endian=little, ";
2171 if (layout.data.majorOrder == MajorOrder::Column) data += "column_major_order=1, ";
2172 }
2173 if (layout.data.additionalHeaderLines)
2174 data += "additional_header_lines=" + std::to_string(layout.data.additionalHeaderLines) + ", ";
2175 stream.write(data + "&end\n");
2176 for (std::int32_t i = 0; i < layout.data.additionalHeaderLines; ++i)
2177 stream.write("! additional SDDS header line\n");
2178}
2179
2180void validatePage(const Page &page, const Layout &layout) {
2181 if (page.parameters().size() != layout.parameters.size() ||
2182 page.arrays().size() != layout.arrays.size() || page.columns().size() != layout.columns.size())
2183 throwType("page does not match writer layout");
2184 if (!page.allFieldsLoaded())
2185 throwState("a projected page must be completed before it can be written");
2186 std::optional<std::size_t> rows;
2187 for (std::size_t i = 0; i < layout.parameters.size(); ++i)
2188 if (typeOf(page.parameters()[i]) != layout.parameters[i].type)
2189 throwType("parameter type mismatch", layout.parameters[i].name);
2190 for (std::size_t i = 0; i < layout.arrays.size(); ++i) {
2191 if (typeOf(page.arrays()[i].values) != layout.arrays[i].type)
2192 throwType("array type mismatch", layout.arrays[i].name);
2193 if (page.arrays()[i].dimensions.size() != static_cast<std::size_t>(layout.arrays[i].dimensions))
2194 throwType("array dimension count mismatch", layout.arrays[i].name);
2195 ReaderLimits unlimited;
2196 const auto count = checkedArrayElements(page.arrays()[i].dimensions, unlimited, {}, 0,
2197 layout.arrays[i].name);
2198 if (count != valuesSize(page.arrays()[i].values))
2199 throwType("array element count mismatch", layout.arrays[i].name);
2200 }
2201 for (std::size_t i = 0; i < layout.columns.size(); ++i) {
2202 if (typeOf(page.columns()[i]) != layout.columns[i].type)
2203 throwType("column type mismatch", layout.columns[i].name);
2204 if (!rows) rows = valuesSize(page.columns()[i]);
2205 else if (*rows != valuesSize(page.columns()[i]))
2206 throwType("columns have inconsistent row counts", layout.columns[i].name);
2207 }
2208}
2209
2210struct DecodedPage {
2211 Page page;
2212 std::int64_t rows = 0;
2213 std::int64_t rawRows = 0;
2214 bool recovered = false;
2215};
2216
2217bool recoveryEnabled(const Layout &layout, const ReaderOptions &options) {
2218 return options.recovery == RecoveryMode::Recover ||
2219 (options.recovery == RecoveryMode::Automatic &&
2220 layout.data.rowCountMode == RowCountMode::Fixed);
2221}
2222
2223bool endOfStreamError(const FormatError &error) {
2224 const std::string message(error.what());
2225 return message.find("end of file") != std::string::npos ||
2226 message.find("truncated LZMA/XZ stream") != std::string::npos;
2227}
2228
2229struct ResolvedReadRequest {
2230 std::vector<bool> parameters;
2231 std::vector<bool> arrays;
2232 std::vector<bool> columns;
2233 RowSlice rows;
2234};
2235
2236template <class Definition>
2237std::vector<bool> resolveFields(const FieldSelection &selection,
2238 const std::vector<Definition> &definitions,
2239 std::uint32_t &projected, const ReaderOptions &options) {
2240 if (selection.all && !selection.names.empty())
2241 throwState("all-fields projection cannot also contain names");
2242 std::vector<bool> result(definitions.size(), selection.all);
2243 if (!selection.all) {
2244 for (const auto &name : selection.names) {
2245 const auto found = std::find_if(definitions.begin(), definitions.end(),
2246 [&](const auto &definition) {
2247 return definition.name == name;
2248 });
2249 if (found == definitions.end())
2250 throwType("unknown projected field: " + name, name);
2251 result[static_cast<std::size_t>(found - definitions.begin())] = true;
2252 }
2253 }
2254 for (const bool selected : result) {
2255 if (!selected) continue;
2256 if (projected == options.limits.maxProjectedFields)
2257 throwLimit("projection exceeds configured field limit");
2258 ++projected;
2259 }
2260 return result;
2261}
2262
2263ResolvedReadRequest resolveRequest(const Layout &layout, const ReadRequest &request,
2264 const ReaderOptions &options) {
2265 if (request.rows.first < 0 || request.rows.stride < 1 ||
2266 (request.rows.count && *request.rows.count < 0) ||
2267 (request.rows.last && *request.rows.last < 0) ||
2268 (request.rows.last && (request.rows.first || request.rows.count)))
2269 throwState("invalid row slice");
2270 std::uint32_t projected = 0;
2271 ResolvedReadRequest result;
2272 result.parameters = resolveFields(request.parameters, layout.parameters, projected, options);
2273 result.arrays = resolveFields(request.arrays, layout.arrays, projected, options);
2274 result.columns = resolveFields(request.columns, layout.columns, projected, options);
2275 result.rows = request.rows;
2276 return result;
2277}
2278
2279bool selectKnownRow(const RowSlice &slice, std::int64_t row, std::int64_t total) {
2280 const std::int64_t begin = slice.last ? std::max<std::int64_t>(0, total - *slice.last)
2281 : std::min(slice.first, total);
2282 if (row < begin || (row - begin) % slice.stride)
2283 return false;
2284 return !slice.count || (row - begin) / slice.stride < *slice.count;
2285}
2286
2287void appendRingScalar(Values &values, Scalar value, std::size_t limit,
2288 std::size_t position) {
2289 if (!limit) return;
2290 std::visit([&](auto &items) {
2291 using Vector = std::decay_t<decltype(items)>;
2292 using Value = typename Vector::value_type;
2293 if (items.size() < limit)
2294 items.push_back(std::get<Value>(std::move(value)));
2295 else
2296 items.at(position) = std::get<Value>(std::move(value));
2297 }, values);
2298}
2299
2300void rotateRing(Values &values, std::size_t first) {
2301 if (!first) return;
2302 std::visit([&](auto &items) {
2303 if (first < items.size()) std::rotate(items.begin(), items.begin() + first, items.end());
2304 }, values);
2305}
2306
2307std::vector<std::string> readAsciiTokens(AsciiCursor &cursor, std::size_t count,
2308 const std::string &what) {
2309 std::vector<std::string> result;
2310 result.reserve(count);
2311 while (result.size() < count) {
2312 auto line = cursor.nextLine(false);
2313 if (!line)
2314 throwFormat("unexpected EOF reading " + what);
2315 auto tokens = splitAsciiTokens(*line);
2316 result.insert(result.end(), std::make_move_iterator(tokens.begin()),
2317 std::make_move_iterator(tokens.end()));
2318 }
2319 if (result.size() != count)
2320 throwFormat("too many values while reading " + what);
2321 return result;
2322}
2323
2324void discardAsciiTokens(AsciiCursor &cursor, std::size_t count, const std::string &what,
2325 Type type, const ReaderOptions &options,
2326 const std::filesystem::path &path, std::int64_t page,
2327 const std::string &field) {
2328 std::size_t consumed = 0;
2329 while (consumed < count) {
2330 auto line = cursor.nextLine(false);
2331 if (!line) throwFormat("unexpected EOF reading " + what, path, page, field);
2332 auto tokens = splitAsciiTokens(*line);
2333 if (tokens.size() > count - consumed)
2334 throwFormat("too many values while reading " + what, path, page, field);
2335 if (type == Type::String)
2336 for (const auto &token : tokens)
2337 if (token.size() > options.limits.maxStringBytes)
2338 throwLimit("ASCII string exceeds configured limit", path, page, field);
2339 consumed += tokens.size();
2340 }
2341}
2342
2343void retainStride(Values &values, std::int64_t stride) {
2344 if (stride == 1) return;
2345 std::visit([&](auto &items) {
2346 using Vector = std::decay_t<decltype(items)>;
2347 Vector retained;
2348 retained.reserve((items.size() + static_cast<std::size_t>(stride) - 1) /
2349 static_cast<std::size_t>(stride));
2350 for (std::size_t index = 0; index < items.size(); index += static_cast<std::size_t>(stride))
2351 retained.push_back(std::move(items[index]));
2352 items = std::move(retained);
2353 }, values);
2354}
2355
2356std::optional<std::string> findAsciiPage(BufferedStream &stream, const ReaderOptions &options,
2357 const std::filesystem::path &path, bool &foundMarker) {
2358 foundMarker = false;
2359 while (auto line = stream.line(options.limits.maxLayoutCommandBytes, path)) {
2360 const std::string stripped = trim(*line);
2361 if (stripped.empty())
2362 continue;
2363 if (stripped.front() == '!') {
2364 if (lower(stripped).find("page number") != std::string::npos)
2365 foundMarker = true;
2366 continue;
2367 }
2368 return line;
2369 }
2370 return std::nullopt;
2371}
2372
2373DecodedPage readAsciiPage(BufferedStream &stream, const std::shared_ptr<const Layout> &layout,
2374 const ReaderOptions &options, const std::filesystem::path &path,
2375 std::int64_t pageNumber, std::optional<std::string> firstLine,
2376 const ResolvedReadRequest &request) {
2377 AsciiCursor cursor(stream, options, path, std::move(firstLine));
2378 Page page(layout, LoadMode::None);
2379 for (std::size_t i = 0; i < layout->parameters.size(); ++i) {
2380 const auto &definition = layout->parameters[i];
2381 std::string value;
2382 if (definition.fixedValue) {
2383 value = *definition.fixedValue;
2384 } else {
2385 auto line = cursor.nextLine(false);
2386 if (!line)
2387 throwFormat("unexpected EOF reading parameter", path, pageNumber, definition.name);
2388 auto tokens = splitAsciiTokens(*line);
2389 if (tokens.empty())
2390 throwFormat("missing parameter value", path, pageNumber, definition.name);
2391 value = definition.type == Type::String ? tokens.front() : tokens.front();
2392 }
2393 if (definition.type == Type::String && value.size() > options.limits.maxStringBytes)
2394 throwLimit("ASCII string exceeds configured limit", path, pageNumber, definition.name);
2395 if (request.parameters[i]) {
2396 try {
2397 page.setParameter(i, parseAsciiScalar(definition.type, value));
2398 } catch (const Error &error) {
2399 throwWithContext(error, path, pageNumber, stream.offset(), std::nullopt,
2400 definition.name);
2401 }
2402 }
2403 }
2404
2405 for (std::size_t i = 0; i < layout->arrays.size(); ++i) {
2406 const auto &definition = layout->arrays[i];
2407 const auto dimensionTokens = readAsciiTokens(
2408 cursor, static_cast<std::size_t>(definition.dimensions), "array dimensions");
2409 ArrayData array;
2410 array.dimensions.reserve(static_cast<std::size_t>(definition.dimensions));
2411 for (const auto &token : dimensionTokens) {
2412 try {
2413 array.dimensions.push_back(parseInteger<std::int32_t>(token, "array dimension"));
2414 } catch (const Error &error) {
2415 throwWithContext(error, path, pageNumber, stream.offset(), std::nullopt,
2416 definition.name);
2417 }
2418 }
2419 const std::uint64_t elements = checkedArrayElements(array.dimensions, options.limits, path,
2420 pageNumber, definition.name);
2421 array.values = emptyValues(definition.type);
2422 if (elements && request.arrays[i]) {
2423 const auto values = readAsciiTokens(cursor, static_cast<std::size_t>(elements),
2424 "array " + definition.name);
2425 for (const auto &value : values) {
2426 if (definition.type == Type::String && value.size() > options.limits.maxStringBytes)
2427 throwLimit("ASCII string exceeds configured limit", path, pageNumber, definition.name);
2428 try {
2429 appendScalar(array.values, parseAsciiScalar(definition.type, value));
2430 } catch (const Error &error) {
2431 throwWithContext(error, path, pageNumber, stream.offset(), std::nullopt,
2432 definition.name);
2433 }
2434 }
2435 } else if (elements) {
2436 discardAsciiTokens(cursor, static_cast<std::size_t>(elements),
2437 "array " + definition.name, definition.type, options,
2438 path, pageNumber, definition.name);
2439 }
2440 if (request.arrays[i]) page.setArray(i, std::move(array));
2441 }
2442
2443 std::int64_t expectedRows = 0;
2444 if (!layout->columns.empty() && layout->data.rowCountMode != RowCountMode::None) {
2445 auto line = cursor.nextLine(false);
2446 if (!line)
2447 throwFormat("unexpected EOF reading row count", path, pageNumber);
2448 const auto tokens = splitAsciiTokens(*line);
2449 if (tokens.empty())
2450 throwFormat("missing row count", path, pageNumber);
2451 expectedRows = parseInteger<std::int64_t>(tokens.front(), "row count");
2452 if (expectedRows < 0)
2453 throwFormat("negative row count", path, pageNumber);
2454 if (expectedRows > options.limits.maxRows)
2455 throwLimit("page exceeds configured row limit", path, pageNumber);
2456 }
2457
2458 std::vector<Values> columns;
2459 columns.reserve(layout->columns.size());
2460 for (const auto &definition : layout->columns)
2461 columns.push_back(emptyValues(definition.type));
2462
2463 bool recovered = false;
2464 std::int64_t rows = 0;
2465 if (!layout->columns.empty()) {
2466 std::vector<std::string> rowTokens;
2467 const bool counted = layout->data.rowCountMode != RowCountMode::None;
2468 while (!counted || rows < expectedRows) {
2469 while (rowTokens.size() < layout->columns.size()) {
2470 auto line = cursor.nextLine(true);
2471 if (!line || trim(*line).empty()) {
2472 if (counted && rows < expectedRows) {
2473 if (!recoveryEnabled(*layout, options))
2474 throwFormat("unexpected EOF in ASCII row data", path, pageNumber,
2475 std::nullopt, stream.offset(), rows);
2476 recovered = true;
2477 } else if (!rowTokens.empty() && !recoveryEnabled(*layout, options)) {
2478 throwFormat("incomplete ASCII row", path, pageNumber, std::nullopt,
2479 stream.offset(), rows);
2480 } else if (!rowTokens.empty()) {
2481 recovered = true;
2482 }
2483 rowTokens.clear();
2484 goto ascii_rows_done;
2485 }
2486 auto tokens = splitAsciiTokens(*line);
2487 rowTokens.insert(rowTokens.end(), std::make_move_iterator(tokens.begin()),
2488 std::make_move_iterator(tokens.end()));
2489 }
2490 if (rowTokens.size() != layout->columns.size())
2491 throwFormat("ASCII row has the wrong number of fields", path, pageNumber,
2492 std::nullopt, stream.offset(), rows);
2493 try {
2494 for (std::size_t i = 0; i < layout->columns.size(); ++i) {
2495 if (layout->columns[i].type == Type::String &&
2496 rowTokens[i].size() > options.limits.maxStringBytes)
2497 throwLimit("ASCII string exceeds configured limit", path, pageNumber,
2498 layout->columns[i].name);
2499 bool selected = false;
2500 if (layout->data.rowCountMode == RowCountMode::None && request.rows.last) {
2501 selected = *request.rows.last > 0;
2502 } else if (layout->data.rowCountMode == RowCountMode::None) {
2503 const auto &slice = request.rows;
2504 selected = rows >= slice.first && (rows - slice.first) % slice.stride == 0 &&
2505 (!slice.count || (rows - slice.first) / slice.stride < *slice.count);
2506 } else {
2507 selected = selectKnownRow(request.rows, rows, expectedRows);
2508 }
2509 if (request.columns[i] && selected) {
2510 Scalar value;
2511 try {
2512 value = parseAsciiScalar(layout->columns[i].type, rowTokens[i]);
2513 } catch (const Error &error) {
2514 throwWithContext(error, path, pageNumber, stream.offset(), rows,
2515 layout->columns[i].name);
2516 }
2517 if (layout->data.rowCountMode == RowCountMode::None && request.rows.last) {
2518 const auto limit = static_cast<std::size_t>(*request.rows.last);
2519 appendRingScalar(columns[i], std::move(value), limit,
2520 limit ? static_cast<std::size_t>(rows) % limit : 0);
2521 } else {
2522 appendScalar(columns[i], std::move(value));
2523 }
2524 }
2525 }
2526 } catch (const Error &error) {
2527 throwWithContext(error, path, pageNumber, stream.offset(), rows);
2528 }
2529 rowTokens.clear();
2530 ++rows;
2531 if (rows > options.limits.maxRows)
2532 throwLimit("page exceeds configured row limit", path, pageNumber,
2533 std::nullopt, rows);
2534 }
2535 }
2536ascii_rows_done:
2537 if (layout->data.rowCountMode == RowCountMode::None && request.rows.last) {
2538 const auto limit = static_cast<std::size_t>(*request.rows.last);
2539 const std::size_t first = limit && rows > static_cast<std::int64_t>(limit)
2540 ? static_cast<std::size_t>(rows) % limit : 0;
2541 for (std::size_t i = 0; i < columns.size(); ++i)
2542 if (request.columns[i]) {
2543 rotateRing(columns[i], first);
2544 retainStride(columns[i], request.rows.stride);
2545 }
2546 }
2547 std::int64_t selectedRows = 0;
2548 for (std::size_t i = 0; i < columns.size(); ++i) {
2549 if (!request.columns[i]) continue;
2550 selectedRows = static_cast<std::int64_t>(valuesSize(columns[i]));
2551 page.setColumn(i, std::move(columns[i]));
2552 }
2553 if (layout->columns.empty() ||
2554 std::none_of(request.columns.begin(), request.columns.end(), [](bool value) { return value; })) {
2555 if (layout->data.rowCountMode == RowCountMode::None && request.rows.last) {
2556 const std::int64_t retained = std::min(rows, *request.rows.last);
2557 selectedRows = (retained + request.rows.stride - 1) / request.rows.stride;
2558 } else {
2559 for (std::int64_t row = 0; row < rows; ++row)
2560 if (layout->data.rowCountMode == RowCountMode::None
2561 ? (row >= request.rows.first && (row - request.rows.first) % request.rows.stride == 0 &&
2562 (!request.rows.count ||
2563 (row - request.rows.first) / request.rows.stride < *request.rows.count))
2564 : selectKnownRow(request.rows, row, expectedRows))
2565 ++selectedRows;
2566 }
2567 }
2568 return {std::move(page), selectedRows, rows, recovered};
2569}
2570
2571DecodedPage readBinaryPage(BufferedStream &stream, const std::shared_ptr<const Layout> &layout,
2572 const ReaderOptions &options, const std::filesystem::path &path,
2573 std::int64_t pageNumber, const ResolvedReadRequest &request) {
2574 const ByteOrder order = layout->data.byteOrder;
2575 const std::int32_t count32 = readPod<std::int32_t>(stream, order, path, pageNumber);
2576 const std::int64_t expectedRows = count32 == kInt64RowCount
2577 ? readPod<std::int64_t>(stream, order, path, pageNumber) : count32;
2578 if (expectedRows < 0)
2579 throwFormat("negative row count", path, pageNumber);
2580 if (expectedRows > options.limits.maxRows)
2581 throwLimit("page exceeds configured row limit", path, pageNumber);
2582
2583 Page page(layout, LoadMode::None);
2584 for (std::size_t i = 0; i < layout->parameters.size(); ++i) {
2585 const auto &definition = layout->parameters[i];
2586 try {
2587 if (definition.fixedValue) {
2588 if (request.parameters[i])
2589 page.setParameter(i, parseAsciiScalar(definition.type, *definition.fixedValue));
2590 } else if (request.parameters[i]) {
2591 page.setParameter(i, readBinaryScalar(stream, definition.type, order,
2592 options.longDoubleEncoding, options.limits,
2593 path, pageNumber));
2594 } else {
2595 skipBinaryScalar(stream, definition.type, order, options.longDoubleEncoding,
2596 options.limits, path, pageNumber);
2597 }
2598 } catch (const Error &error) {
2599 throwWithContext(error, path, pageNumber, stream.offset(), std::nullopt,
2600 definition.name);
2601 }
2602 }
2603 for (std::size_t i = 0; i < layout->arrays.size(); ++i) {
2604 const auto &definition = layout->arrays[i];
2605 try {
2606 ArrayData array;
2607 for (std::int32_t dimension = 0; dimension < definition.dimensions; ++dimension)
2608 array.dimensions.push_back(readPod<std::int32_t>(stream, order, path, pageNumber));
2609 const std::uint64_t elements = checkedArrayElements(array.dimensions, options.limits, path,
2610 pageNumber, definition.name);
2611 array.values = emptyValues(definition.type);
2612 for (std::uint64_t element = 0; element < elements; ++element) {
2613 if (request.arrays[i])
2614 appendScalar(array.values,
2615 readBinaryScalar(stream, definition.type, order,
2616 options.longDoubleEncoding, options.limits,
2617 path, pageNumber));
2618 else
2619 skipBinaryScalar(stream, definition.type, order, options.longDoubleEncoding,
2620 options.limits, path, pageNumber);
2621 }
2622 if (request.arrays[i]) page.setArray(i, std::move(array));
2623 } catch (const Error &error) {
2624 throwWithContext(error, path, pageNumber, stream.offset(), std::nullopt,
2625 definition.name);
2626 }
2627 }
2628
2629 std::vector<Values> columns;
2630 columns.reserve(layout->columns.size());
2631 for (const auto &definition : layout->columns)
2632 columns.push_back(emptyValues(definition.type));
2633 bool recovered = false;
2634 std::int64_t rows = 0;
2635 const bool fixedWidthColumns = std::none_of(
2636 layout->columns.begin(), layout->columns.end(),
2637 [](const auto &definition) { return definition.type == Type::String; });
2638 if (fixedWidthColumns && layout->data.majorOrder == MajorOrder::Row) {
2639 std::vector<std::uint64_t> offsets(layout->columns.size());
2640 std::uint64_t rowBytes = 0;
2641 bool anySelected = false;
2642 std::size_t selectedCapacity = 0;
2643 for (std::int64_t row = 0; row < expectedRows; ++row)
2644 if (selectKnownRow(request.rows, row, expectedRows)) ++selectedCapacity;
2645 for (std::size_t i = 0; i < layout->columns.size(); ++i) {
2646 offsets[i] = rowBytes;
2647 const std::uint64_t width = binaryScalarBytes(layout->columns[i].type,
2648 options.longDoubleEncoding);
2649 if (width > UINT64_MAX - rowBytes)
2650 throwLimit("binary row width overflow", path, pageNumber);
2651 rowBytes += width;
2652 if (request.columns[i]) {
2653 anySelected = true;
2654 reserveValues(columns[i], selectedCapacity);
2655 }
2656 }
2657 if (rowBytes && static_cast<std::uint64_t>(expectedRows) > UINT64_MAX / rowBytes)
2658 throwLimit("binary page size overflow", path, pageNumber);
2659 if (!anySelected && !recoveryEnabled(*layout, options)) {
2660 const std::uint64_t startOffset = stream.offset();
2661 try {
2662 stream.skip(rowBytes * static_cast<std::uint64_t>(expectedRows), path, pageNumber);
2663 } catch (const Error &error) {
2664 const std::uint64_t consumed = stream.offset() - startOffset;
2665 const std::uint64_t withinRow = consumed % rowBytes;
2666 std::optional<std::string> field;
2667 for (std::size_t i = 0; i < layout->columns.size(); ++i)
2668 if (withinRow >= offsets[i]) field = layout->columns[i].name;
2669 throwWithContext(error, path, pageNumber, stream.offset(),
2670 static_cast<std::int64_t>(consumed / rowBytes), field);
2671 }
2672 rows = expectedRows;
2673 } else if (!rowBytes) {
2674 rows = expectedRows;
2675 } else {
2676 const std::uint64_t chunkRows = std::max<std::uint64_t>(
2677 1, (1024U * 1024U) / rowBytes);
2678 std::vector<unsigned char> buffer(static_cast<std::size_t>(
2679 std::min<std::uint64_t>(static_cast<std::uint64_t>(expectedRows), chunkRows) *
2680 rowBytes));
2681 while (rows < expectedRows) {
2682 const std::uint64_t requestedRows = std::min<std::uint64_t>(
2683 static_cast<std::uint64_t>(expectedRows - rows), chunkRows);
2684 const std::size_t requestedBytes = static_cast<std::size_t>(requestedRows * rowBytes);
2685 const std::size_t bytesRead = stream.read(buffer.data(), requestedBytes);
2686 const std::int64_t completeRows = static_cast<std::int64_t>(bytesRead / rowBytes);
2687 for (std::int64_t localRow = 0; localRow < completeRows; ++localRow) {
2688 const std::int64_t sourceRow = rows + localRow;
2689 if (!selectKnownRow(request.rows, sourceRow, expectedRows)) continue;
2690 const auto *rowData = buffer.data() + static_cast<std::size_t>(localRow * rowBytes);
2691 for (std::size_t i = 0; i < layout->columns.size(); ++i)
2692 if (request.columns[i])
2693 appendFixedBinaryValue(columns[i], layout->columns[i].type,
2694 rowData + offsets[i], order,
2695 options.longDoubleEncoding);
2696 }
2697 rows += completeRows;
2698 if (bytesRead != requestedBytes) {
2699 if (!recoveryEnabled(*layout, options)) {
2700 const std::uint64_t withinRow = bytesRead % rowBytes;
2701 std::optional<std::string> field;
2702 for (std::size_t i = 0; i < layout->columns.size(); ++i)
2703 if (withinRow >= offsets[i]) field = layout->columns[i].name;
2704 throwFormat("unexpected end of file", path, pageNumber, field,
2705 stream.offset(), rows);
2706 }
2707 recovered = true;
2708 break;
2709 }
2710 }
2711 }
2712 } else if (fixedWidthColumns) {
2713 std::vector<std::int64_t> columnRows(layout->columns.size(), 0);
2714 std::size_t selectedCapacity = 0;
2715 for (std::int64_t row = 0; row < expectedRows; ++row)
2716 if (selectKnownRow(request.rows, row, expectedRows)) ++selectedCapacity;
2717 for (std::size_t i = 0; i < layout->columns.size(); ++i) {
2718 const std::uint64_t width = binaryScalarBytes(layout->columns[i].type,
2719 options.longDoubleEncoding);
2720 if (width && static_cast<std::uint64_t>(expectedRows) > UINT64_MAX / width)
2721 throwLimit("binary column size overflow", path, pageNumber);
2722 if (!request.columns[i] && !recoveryEnabled(*layout, options)) {
2723 const std::uint64_t startOffset = stream.offset();
2724 try {
2725 stream.skip(width * static_cast<std::uint64_t>(expectedRows), path, pageNumber);
2726 } catch (const Error &error) {
2727 const std::uint64_t consumed = stream.offset() - startOffset;
2728 throwWithContext(error, path, pageNumber, stream.offset(),
2729 static_cast<std::int64_t>(consumed / width),
2730 layout->columns[i].name);
2731 }
2732 columnRows[i] = expectedRows;
2733 continue;
2734 }
2735 if (request.columns[i]) reserveValues(columns[i], selectedCapacity);
2736 const std::uint64_t chunkRows = std::max<std::uint64_t>(1, (1024U * 1024U) / width);
2737 std::vector<unsigned char> buffer(static_cast<std::size_t>(
2738 std::min<std::uint64_t>(static_cast<std::uint64_t>(expectedRows), chunkRows) * width));
2739 while (columnRows[i] < expectedRows) {
2740 const std::uint64_t requestedRows = std::min<std::uint64_t>(
2741 static_cast<std::uint64_t>(expectedRows - columnRows[i]), chunkRows);
2742 const std::size_t requestedBytes = static_cast<std::size_t>(requestedRows * width);
2743 const std::size_t bytesRead = stream.read(buffer.data(), requestedBytes);
2744 const std::int64_t completeRows = static_cast<std::int64_t>(bytesRead / width);
2745 if (request.columns[i])
2746 for (std::int64_t localRow = 0; localRow < completeRows; ++localRow) {
2747 const std::int64_t sourceRow = columnRows[i] + localRow;
2748 if (selectKnownRow(request.rows, sourceRow, expectedRows))
2749 appendFixedBinaryValue(columns[i], layout->columns[i].type,
2750 buffer.data() + static_cast<std::size_t>(localRow * width),
2751 order, options.longDoubleEncoding);
2752 }
2753 columnRows[i] += completeRows;
2754 if (bytesRead != requestedBytes) {
2755 if (!recoveryEnabled(*layout, options))
2756 throwFormat("unexpected end of file", path, pageNumber, layout->columns[i].name,
2757 stream.offset(), columnRows[i]);
2758 recovered = true;
2759 break;
2760 }
2761 }
2762 if (recovered) break;
2763 }
2764 rows = columnRows.empty() ? expectedRows
2765 : *std::min_element(columnRows.begin(), columnRows.end());
2766 if (recovered)
2767 for (std::size_t i = 0; i < columns.size(); ++i) {
2768 if (!request.columns[i]) continue;
2769 std::size_t selected = 0;
2770 for (std::int64_t row = 0; row < rows; ++row)
2771 if (selectKnownRow(request.rows, row, expectedRows)) ++selected;
2772 std::visit([&](auto &values) { values.resize(std::min(values.size(), selected)); },
2773 columns[i]);
2774 }
2775 } else if (layout->data.majorOrder == MajorOrder::Row) {
2776 for (; rows < expectedRows; ++rows) {
2777 std::size_t currentColumn = 0;
2778 try {
2779 const bool selected = selectKnownRow(request.rows, rows, expectedRows);
2780 for (; currentColumn < layout->columns.size(); ++currentColumn) {
2781 const auto &definition = layout->columns[currentColumn];
2782 if (request.columns[currentColumn] && selected)
2783 appendScalar(columns[currentColumn], readBinaryScalar(
2784 stream, definition.type, order, options.longDoubleEncoding,
2785 options.limits, path, pageNumber));
2786 else
2787 skipBinaryScalar(stream, definition.type, order, options.longDoubleEncoding,
2788 options.limits, path, pageNumber);
2789 }
2790 } catch (const FormatError &error) {
2791 if (!recoveryEnabled(*layout, options) || !endOfStreamError(error))
2792 throwWithContext(error, path, pageNumber, stream.offset(), rows,
2793 currentColumn < layout->columns.size()
2794 ? std::optional<std::string>(layout->columns[currentColumn].name)
2795 : std::nullopt);
2796 recovered = true;
2797 break;
2798 }
2799 }
2800 } else {
2801 std::vector<std::int64_t> columnRows(layout->columns.size(), 0);
2802 for (std::size_t i = 0; i < layout->columns.size(); ++i) {
2803 try {
2804 for (; columnRows[i] < expectedRows; ++columnRows[i]) {
2805 if (request.columns[i] && selectKnownRow(request.rows, columnRows[i], expectedRows))
2806 appendScalar(columns[i], readBinaryScalar(stream, layout->columns[i].type, order,
2807 options.longDoubleEncoding, options.limits,
2808 path, pageNumber));
2809 else
2810 skipBinaryScalar(stream, layout->columns[i].type, order,
2811 options.longDoubleEncoding, options.limits, path, pageNumber);
2812 }
2813 } catch (const FormatError &error) {
2814 if (!recoveryEnabled(*layout, options) || !endOfStreamError(error))
2815 throwWithContext(error, path, pageNumber, stream.offset(), columnRows[i],
2816 layout->columns[i].name);
2817 recovered = true;
2818 break;
2819 }
2820 }
2821 rows = columnRows.empty() ? expectedRows
2822 : *std::min_element(columnRows.begin(), columnRows.end());
2823 if (recovered)
2824 for (std::size_t i = 0; i < columns.size(); ++i) {
2825 if (!request.columns[i]) continue;
2826 std::size_t selected = 0;
2827 for (std::int64_t row = 0; row < rows; ++row)
2828 if (selectKnownRow(request.rows, row, expectedRows)) ++selected;
2829 std::visit([&](auto &values) { values.resize(std::min(values.size(), selected)); },
2830 columns[i]);
2831 }
2832 }
2833 std::int64_t selectedRows = 0;
2834 for (std::int64_t row = 0; row < rows; ++row)
2835 if (selectKnownRow(request.rows, row, expectedRows)) ++selectedRows;
2836 for (std::size_t i = 0; i < columns.size(); ++i)
2837 if (request.columns[i]) page.setColumn(i, std::move(columns[i]));
2838 return {std::move(page), selectedRows, rows, recovered};
2839}
2840
2841void writeRowCount(BufferedStream &stream, std::int64_t rows, ByteOrder order) {
2842 if (rows > INT32_MAX) {
2843 writePod(stream, kInt64RowCount, order);
2844 writePod(stream, rows, order);
2845 } else {
2846 writePod(stream, static_cast<std::int32_t>(rows), order);
2847 }
2848}
2849
2850void writeAsciiPage(BufferedStream &stream, const Page &page, const Layout &layout,
2851 std::int64_t pageNumber) {
2852 if (layout.data.rowCountMode == RowCountMode::None && pageNumber > 1)
2853 stream.write("\n");
2854 stream.write("! page number " + std::to_string(pageNumber) + "\n");
2855 for (std::size_t i = 0; i < layout.parameters.size(); ++i) {
2856 if (layout.parameters[i].fixedValue)
2857 continue;
2858 stream.write(formatAsciiScalar(page.parameter(i)) + "\n");
2859 }
2860 for (std::size_t i = 0; i < layout.arrays.size(); ++i) {
2861 const auto &definition = layout.arrays[i];
2862 const auto &array = page.array(i);
2863 for (const auto dimension : array.dimensions)
2864 stream.write(std::to_string(dimension) + " ");
2865 stream.write(" ! " + std::to_string(definition.dimensions) +
2866 "-dimensional array " + definition.name + ":\n");
2867 for (std::size_t element = 0; element < valuesSize(array.values); ++element) {
2868 stream.write(formatAsciiScalar(scalarAt(array.values, element)));
2869 if ((element + 1) % 6 == 0 || element + 1 == valuesSize(array.values))
2870 stream.write("\n");
2871 else
2872 stream.write(" ");
2873 }
2874 }
2875 if (!layout.columns.empty() && layout.data.rowCountMode != RowCountMode::None)
2876 stream.write(std::to_string(page.rowCount()) + "\n");
2877 for (std::int64_t row = 0; row < page.rowCount(); ++row) {
2878 for (std::size_t column = 0; column < layout.columns.size(); ++column) {
2879 if (column)
2880 stream.write(" ");
2881 stream.write(formatAsciiScalar(scalarAt(page.column(column),
2882 static_cast<std::size_t>(row))));
2883 }
2884 stream.write("\n");
2885 }
2886}
2887
2888void writeBinaryPage(BufferedStream &stream, const Page &page, const Layout &layout,
2889 const WriterOptions &options) {
2890 const ByteOrder order = layout.data.byteOrder;
2891 writeRowCount(stream, page.rowCount(), order);
2892 for (std::size_t i = 0; i < layout.parameters.size(); ++i)
2893 if (!layout.parameters[i].fixedValue)
2894 writeBinaryScalar(stream, page.parameter(i), order, options.longDoubleEncoding);
2895 for (std::size_t i = 0; i < layout.arrays.size(); ++i) {
2896 const auto &array = page.array(i);
2897 for (const auto dimension : array.dimensions)
2898 writePod(stream, dimension, order);
2899 for (std::size_t element = 0; element < valuesSize(array.values); ++element)
2900 writeBinaryScalar(stream, scalarAt(array.values, element), order,
2901 options.longDoubleEncoding);
2902 }
2903 const bool fixedWidthColumns = std::none_of(
2904 layout.columns.begin(), layout.columns.end(),
2905 [](const auto &definition) { return definition.type == Type::String; });
2906 if (fixedWidthColumns && layout.data.majorOrder == MajorOrder::Row &&
2907 !layout.columns.empty()) {
2908 std::vector<std::uint64_t> offsets(layout.columns.size());
2909 std::vector<const Values *> columnValues;
2910 columnValues.reserve(layout.columns.size());
2911 bool allDouble = true;
2912 std::uint64_t rowBytes = 0;
2913 for (std::size_t column = 0; column < layout.columns.size(); ++column) {
2914 columnValues.push_back(&page.column(column));
2915 allDouble = allDouble && layout.columns[column].type == Type::Double;
2916 offsets[column] = rowBytes;
2917 const std::uint64_t width = binaryScalarBytes(layout.columns[column].type,
2918 options.longDoubleEncoding);
2919 if (width > UINT64_MAX - rowBytes) throwLimit("binary row width overflow");
2920 rowBytes += width;
2921 }
2922 const bool directDouble = allDouble && !mustSwap(order);
2923 const std::uint64_t chunkRows = std::max<std::uint64_t>(1, (1024U * 1024U) / rowBytes);
2924 std::vector<unsigned char> buffer(static_cast<std::size_t>(
2925 std::min<std::uint64_t>(static_cast<std::uint64_t>(page.rowCount()), chunkRows) *
2926 rowBytes));
2927 std::int64_t firstRow = 0;
2928 while (firstRow < page.rowCount()) {
2929 const std::uint64_t rows = std::min<std::uint64_t>(
2930 static_cast<std::uint64_t>(page.rowCount() - firstRow), chunkRows);
2931 for (std::uint64_t row = 0; row < rows; ++row) {
2932 auto *rowData = buffer.data() + static_cast<std::size_t>(row * rowBytes);
2933 if (allDouble) {
2934 for (std::size_t column = 0; column < layout.columns.size(); ++column) {
2935 const double value = std::get<std::vector<double>>(*columnValues[column])[
2936 static_cast<std::size_t>(firstRow + row)];
2937 if (directDouble)
2938 std::memcpy(rowData + offsets[column], &value, sizeof(value));
2939 else
2940 storeFixedValue(rowData + offsets[column], value, order);
2941 }
2942 } else {
2943 for (std::size_t column = 0; column < layout.columns.size(); ++column)
2944 storeFixedBinaryValue(rowData + offsets[column], *columnValues[column],
2945 static_cast<std::size_t>(firstRow + row),
2946 layout.columns[column].type, order,
2947 options.longDoubleEncoding);
2948 }
2949 }
2950 stream.write(buffer.data(), static_cast<std::size_t>(rows * rowBytes));
2951 firstRow += static_cast<std::int64_t>(rows);
2952 }
2953 } else if (fixedWidthColumns && layout.data.majorOrder == MajorOrder::Column) {
2954 for (std::size_t column = 0; column < layout.columns.size(); ++column) {
2955 const std::uint64_t width = binaryScalarBytes(layout.columns[column].type,
2956 options.longDoubleEncoding);
2957 const std::uint64_t chunkRows = std::max<std::uint64_t>(1, (1024U * 1024U) / width);
2958 std::vector<unsigned char> buffer(static_cast<std::size_t>(
2959 std::min<std::uint64_t>(static_cast<std::uint64_t>(page.rowCount()), chunkRows) *
2960 width));
2961 std::int64_t firstRow = 0;
2962 while (firstRow < page.rowCount()) {
2963 const std::uint64_t rows = std::min<std::uint64_t>(
2964 static_cast<std::uint64_t>(page.rowCount() - firstRow), chunkRows);
2965 for (std::uint64_t row = 0; row < rows; ++row)
2966 storeFixedBinaryValue(buffer.data() + static_cast<std::size_t>(row * width),
2967 page.column(column),
2968 static_cast<std::size_t>(firstRow + row),
2969 layout.columns[column].type, order,
2970 options.longDoubleEncoding);
2971 stream.write(buffer.data(), static_cast<std::size_t>(rows * width));
2972 firstRow += static_cast<std::int64_t>(rows);
2973 }
2974 }
2975 } else if (layout.data.majorOrder == MajorOrder::Column) {
2976 for (std::size_t column = 0; column < layout.columns.size(); ++column)
2977 for (std::int64_t row = 0; row < page.rowCount(); ++row)
2978 writeBinaryScalar(stream, scalarAt(page.column(column),
2979 static_cast<std::size_t>(row)), order,
2980 options.longDoubleEncoding);
2981 } else {
2982 for (std::int64_t row = 0; row < page.rowCount(); ++row)
2983 for (std::size_t column = 0; column < layout.columns.size(); ++column)
2984 writeBinaryScalar(stream, scalarAt(page.column(column),
2985 static_cast<std::size_t>(row)), order,
2986 options.longDoubleEncoding);
2987 }
2988}
2989
2990void writePageData(BufferedStream &stream, const Page &page, const Layout &layout,
2991 const WriterOptions &options, std::int64_t pageNumber) {
2992 validatePage(page, layout);
2993 if (layout.data.mode == DataMode::Ascii)
2994 writeAsciiPage(stream, page, layout, pageNumber);
2995 else
2996 writeBinaryPage(stream, page, layout, options);
2997}
2998
2999std::unique_ptr<Stream> standardInput(const ReaderOptions &options) {
3000 const Compression compression = options.compression;
3001 setBinaryMode(stdin);
3002 if (compression == Compression::Auto || compression == Compression::None)
3003 return std::make_unique<FileStream>(stdin, std::filesystem::path{}, false, false,
3004 options.bufferBytes);
3005#if defined(_WIN32)
3006 const int descriptor = _dup(_fileno(stdin));
3007#else
3008 const int descriptor = dup(fileno(stdin));
3009#endif
3010 if (descriptor < 0)
3011 throwIo("unable to duplicate standard input");
3012 if (compression == Compression::Gzip)
3013 return std::make_unique<GzipStream>(gzdopen(descriptor, "rb"),
3014 std::filesystem::path{}, false, options.bufferBytes);
3015#if defined(_WIN32)
3016 FILE *file = _fdopen(descriptor, "rb");
3017#else
3018 FILE *file = fdopen(descriptor, "rb");
3019#endif
3020 if (!file)
3021 throwIo("unable to open duplicate standard input");
3022 return std::make_unique<LzmaStream>(file, std::filesystem::path{}, false,
3023 options.bufferBytes);
3024}
3025
3026std::unique_ptr<Stream> standardOutput(const WriterOptions &options) {
3027 const Compression compression = options.compression;
3028 if (options.gzipLevel < -1 || options.gzipLevel > 9)
3029 throwState("gzip compression level must be between 0 and 9, or -1 for default");
3030 if (options.lzmaPreset > 9)
3031 throwState("LZMA preset must be between 0 and 9");
3032 setBinaryMode(stdout);
3033 if (compression == Compression::Auto || compression == Compression::None)
3034 return std::make_unique<FileStream>(stdout, std::filesystem::path{}, false, true,
3035 options.bufferBytes);
3036#if defined(_WIN32)
3037 const int descriptor = _dup(_fileno(stdout));
3038#else
3039 const int descriptor = dup(fileno(stdout));
3040#endif
3041 if (descriptor < 0)
3042 throwIo("unable to duplicate standard output");
3043 if (compression == Compression::Gzip) {
3044 std::string mode = "wb";
3045 if (options.gzipLevel >= 0) mode.push_back(static_cast<char>('0' + options.gzipLevel));
3046 return std::make_unique<GzipStream>(gzdopen(descriptor, mode.c_str()),
3047 std::filesystem::path{}, true, options.bufferBytes);
3048 }
3049#if defined(_WIN32)
3050 FILE *file = _fdopen(descriptor, "wb");
3051#else
3052 FILE *file = fdopen(descriptor, "wb");
3053#endif
3054 if (!file)
3055 throwIo("unable to open duplicate standard output");
3056 return std::make_unique<LzmaStream>(file, std::filesystem::path{}, true,
3057 options.bufferBytes, options.lzmaPreset,
3058 options.lzmaCheck, compression == Compression::Lzma);
3059}
3060
3061template <class T>
3062void mergeValues(std::vector<T> &destination, std::vector<T> &&source,
3063 std::size_t startRow) {
3064 if (destination.size() < startRow)
3065 destination.resize(startRow);
3066 if (destination.size() < startRow + source.size())
3067 destination.resize(startRow + source.size());
3068 std::move(source.begin(), source.end(), destination.begin() + startRow);
3069}
3070
3071void mergeValuesInPlace(Values &existing, Values incoming, std::int64_t startRow) {
3072 if (startRow < 0)
3073 throwState("column start row cannot be negative");
3074 if (existing.index() != incoming.index())
3075 throwType("column type mismatch");
3076 std::visit([&](auto &destination) {
3077 using Vector = std::decay_t<decltype(destination)>;
3078 mergeValues(destination, std::get<Vector>(std::move(incoming)),
3079 static_cast<std::size_t>(startRow));
3080 }, existing);
3081}
3082
3083} // namespace
3084
3086 Impl(std::filesystem::path sourcePath, ReaderOptions readerOptions,
3087 std::unique_ptr<Stream> input, std::optional<Layout> suppliedLayout = std::nullopt,
3088 bool isPathBacked = false)
3089 : path(std::move(sourcePath)), options(std::move(readerOptions)),
3090 stream(std::move(input), options.limits.maxDecompressedBytes, path),
3091 pathBacked(isPathBacked) {
3092 if (pathBacked) identity = fileIdentity(path);
3093 if (suppliedLayout) {
3094 validateLayout(*suppliedLayout);
3095 layout = std::make_shared<Layout>(std::move(*suppliedLayout));
3096 } else {
3097 Layout parsed;
3098 std::unordered_set<std::string> includeStack;
3099 std::optional<ByteOrder> commentOrder;
3100 bool fixedRowComment = false;
3101 parseLayoutStream(stream, parsed, options, path, 0, includeStack, commentOrder,
3102 fixedRowComment, true);
3103 const std::int32_t declaredVersion = parsed.version;
3104 validateLayout(parsed);
3105 if (declaredVersion < parsed.version)
3106 throwFormat("SDDS version is too old for its layout", path);
3107 parsed.version = declaredVersion;
3108 layout = std::make_shared<Layout>(std::move(parsed));
3109 }
3110 if (stream.seekable()) {
3111 dataStart = stream.tell();
3112 rememberOffset(0, *dataStart);
3113 }
3114 }
3115
3116 void rememberOffset(std::size_t index, std::uint64_t offset) {
3117 if (index >= options.limits.maxPageIndexEntries)
3118 throwLimit("page index exceeds configured entry limit", path);
3119 if (pageOffsets.size() <= index) pageOffsets.resize(index + 1);
3120 pageOffsets[index] = offset;
3121 }
3122
3123 std::optional<DecodedPage> next(const ReadRequest &readRequest) {
3124 if (closed)
3125 throwState("reader is closed");
3126 if (disconnected)
3127 throwState("reader is disconnected");
3128 const ResolvedReadRequest request = resolveRequest(*layout, readRequest, options);
3129 if (stream.seekable()) rememberOffset(static_cast<std::size_t>(pageNumber), stream.tell());
3130 std::optional<DecodedPage> result;
3131 if (layout->data.mode == DataMode::Ascii) {
3132 bool marker = false;
3133 auto first = findAsciiPage(stream, options, path, marker);
3134 if (first || marker)
3135 result = readAsciiPage(stream, layout, options, path, pageNumber + 1,
3136 std::move(first), request);
3137 } else {
3138 const auto first = stream.get();
3139 if (first) {
3140 stream.unget(*first);
3141 result = readBinaryPage(stream, layout, options, path, pageNumber + 1, request);
3142 }
3143 }
3144 if (!result) {
3145 indexComplete = true;
3146 indexedPages = pageNumber;
3147 if (!tailInitialized) {
3148 tailPageNumber = pageNumber;
3149 tailRows = lastRawRows;
3150 tailInitialized = true;
3151 }
3152 return std::nullopt;
3153 }
3154 if (stream.seekable()) rememberOffset(static_cast<std::size_t>(pageNumber + 1), stream.tell());
3155 return result;
3156 }
3157
3158 std::filesystem::path path;
3159 ReaderOptions options;
3160 BufferedStream stream;
3161 std::shared_ptr<Layout> layout;
3162 std::optional<std::uint64_t> dataStart;
3163 std::vector<std::uint64_t> pageOffsets;
3164 std::optional<std::int64_t> indexedPages;
3165 std::int64_t pageNumber = 0;
3166 std::optional<std::uint64_t> disconnectedOffset;
3167 std::unique_ptr<PathLock> lock;
3168 FileIdentity identity;
3169 std::int64_t lastRawRows = 0;
3170 std::int64_t tailPageNumber = 0;
3171 std::int64_t tailRows = 0;
3172 bool tailInitialized = false;
3173 bool headerless = false;
3174 bool pathBacked = false;
3175 bool indexComplete = false;
3176 bool disconnected = false;
3177 bool closed = false;
3178};
3179
3180Reader::Reader(std::unique_ptr<Impl> impl) : impl_(std::move(impl)) {}
3181Reader::Reader(Reader &&) noexcept = default;
3182Reader &Reader::operator=(Reader &&) noexcept = default;
3183Reader::~Reader() = default;
3184
3185Reader Reader::open(const std::filesystem::path &path, ReaderOptions options) {
3186 try {
3187 auto lock = acquirePathLock(path, options.lockMode, false);
3188 auto impl = std::make_unique<Impl>(path, options,
3189 openInput(path, options.compression, options.bufferBytes),
3190 std::nullopt, true);
3191 impl->lock = std::move(lock);
3192 return Reader(std::move(impl));
3193 } catch (const Error &error) {
3194 throwWithContext(error, path, 0);
3195 }
3196}
3197
3198Reader Reader::openHeaderless(const std::filesystem::path &path, Layout layout,
3199 ReaderOptions options) {
3200 try {
3201 auto lock = acquirePathLock(path, options.lockMode, false);
3202 auto impl = std::make_unique<Impl>(path, options,
3203 openInput(path, options.compression, options.bufferBytes),
3204 std::move(layout), true);
3205 impl->lock = std::move(lock);
3206 impl->headerless = true;
3207 return Reader(std::move(impl));
3208 } catch (const Error &error) {
3209 throwWithContext(error, path, 0);
3210 }
3211}
3212
3213Reader Reader::fromStdin(ReaderOptions options) {
3214 return Reader(std::make_unique<Impl>(std::filesystem::path{}, options,
3215 standardInput(options)));
3216}
3217
3218Reader Reader::fromSource(std::unique_ptr<InputSource> source, std::string sourceName,
3219 ReaderOptions options) {
3220 const std::filesystem::path path(sourceName);
3221 try {
3222 auto stream = wrapInputCodec(std::make_unique<InputSourceStream>(std::move(source)), options);
3223 return Reader(std::make_unique<Impl>(path, options, std::move(stream)));
3224 } catch (const Error &error) {
3225 throwWithContext(error, path, 0);
3226 }
3227}
3228
3229Reader Reader::fromHeaderlessSource(std::unique_ptr<InputSource> source, Layout layout,
3230 std::string sourceName, ReaderOptions options) {
3231 const std::filesystem::path path(sourceName);
3232 try {
3233 auto stream = wrapInputCodec(std::make_unique<InputSourceStream>(std::move(source)), options);
3234 auto impl = std::make_unique<Impl>(path, options, std::move(stream), std::move(layout));
3235 impl->headerless = true;
3236 return Reader(std::move(impl));
3237 } catch (const Error &error) {
3238 throwWithContext(error, path, 0);
3239 }
3240}
3241
3242const Layout &Reader::layout() const {
3243 if (!impl_ || impl_->closed)
3244 throwState("reader is closed");
3245 return *impl_->layout;
3246}
3247
3248std::optional<Page> Reader::next(ReadRequest request) {
3249 if (!impl_)
3250 throwState("reader has no implementation");
3251 std::optional<DecodedPage> decoded;
3252 try {
3253 decoded = impl_->next(request);
3254 } catch (const Error &error) {
3255 throwWithContext(error, impl_->path, impl_->pageNumber + 1, impl_->stream.offset());
3256 }
3257 if (!decoded)
3258 return std::nullopt;
3259 ++impl_->pageNumber;
3260 impl_->lastRawRows = decoded->rawRows;
3261 decoded->page.number_ = impl_->pageNumber;
3262 decoded->page.rowCount_ = decoded->rows;
3263 decoded->page.recovered_ = decoded->recovered;
3264 decoded->page.maxTransformationElements_ = impl_->options.limits.maxTransformationElements;
3265 return std::move(decoded->page);
3266}
3267
3268std::optional<Page> Reader::next(ReadSelection selection) {
3269 ReadRequest request;
3270 request.rows.first = selection.sparseOffset;
3271 request.rows.stride = selection.sparseInterval;
3272 request.rows.last = selection.lastRows;
3273 return next(std::move(request));
3274}
3275
3276void Reader::gotoPage(std::int64_t pageNumber) {
3277 if (!impl_ || impl_->closed)
3278 throwState("reader is closed");
3279 if (pageNumber < 1)
3280 throwState("page numbers start at one");
3281 if (!impl_->dataStart)
3282 throwState("gotoPage requires a seekable, uncompressed input");
3283 std::size_t startIndex = 0;
3284 if (static_cast<std::size_t>(pageNumber - 1) < impl_->pageOffsets.size()) {
3285 startIndex = static_cast<std::size_t>(pageNumber - 1);
3286 } else if (!impl_->pageOffsets.empty()) {
3287 startIndex = impl_->pageOffsets.size() - 1;
3288 }
3289 impl_->stream.seek(impl_->pageOffsets.at(startIndex));
3290 impl_->pageNumber = static_cast<std::int64_t>(startIndex);
3291 ReadRequest skip;
3292 skip.parameters = FieldSelection::noFields();
3293 skip.arrays = FieldSelection::noFields();
3294 skip.columns = FieldSelection::noFields();
3295 while (impl_->pageNumber + 1 < pageNumber)
3296 if (!next(skip))
3297 throwState("requested page is beyond end of file");
3298}
3299
3300void Reader::buildPageIndex() {
3301 if (!impl_ || impl_->closed) throwState("reader is closed");
3302 if (!impl_->dataStart) throwState("page indexing requires a seekable, uncompressed input");
3303 const std::uint64_t savedOffset = impl_->stream.tell();
3304 const std::int64_t savedPage = impl_->pageNumber;
3305 impl_->stream.seek(*impl_->dataStart);
3306 impl_->pageNumber = 0;
3307 ReadRequest skip;
3308 skip.parameters = FieldSelection::noFields();
3309 skip.arrays = FieldSelection::noFields();
3310 skip.columns = FieldSelection::noFields();
3311 while (next(skip)) {}
3312 impl_->indexedPages = impl_->pageNumber;
3313 impl_->indexComplete = true;
3314 impl_->stream.seek(savedOffset);
3315 impl_->pageNumber = savedPage;
3316}
3317
3318std::optional<std::int64_t> Reader::indexedPageCount() const noexcept {
3319 return impl_ && impl_->indexComplete ? impl_->indexedPages : std::nullopt;
3320}
3321
3322void Reader::disconnect() {
3323 if (!impl_ || impl_->closed) throwState("reader is closed");
3324 if (impl_->disconnected) return;
3325 if (!impl_->pathBacked || compressionFor(impl_->path, impl_->options.compression) != Compression::None ||
3326 !impl_->stream.seekable())
3327 throwState("disconnect requires a path-backed, uncompressed, seekable input");
3328 impl_->disconnectedOffset = impl_->stream.tell();
3329 impl_->stream.close();
3330 impl_->disconnected = true;
3331}
3332
3333void Reader::reconnect() {
3334 if (!impl_ || impl_->closed) throwState("reader is closed");
3335 if (!impl_->disconnected) return;
3336 const FileIdentity current = fileIdentity(impl_->path);
3337 if (!sameFile(impl_->identity, current))
3338 throwState("input file was replaced while disconnected");
3339 auto input = openInput(impl_->path, Compression::None, impl_->options.bufferBytes);
3340 impl_->stream.replace(std::move(input), *impl_->disconnectedOffset);
3341 impl_->disconnected = false;
3342}
3343
3344std::optional<PageDelta> Reader::readNewRows(ReadRequest request) {
3345 if (!impl_ || impl_->closed) throwState("reader is closed");
3346 if (!impl_->pathBacked || compressionFor(impl_->path, impl_->options.compression) != Compression::None ||
3347 impl_->layout->data.mode != DataMode::Binary ||
3348 impl_->layout->data.majorOrder != MajorOrder::Row)
3349 throwState("reading newly appended rows requires a path-backed, uncompressed "
3350 "row-major binary input");
3351 (void)resolveRequest(*impl_->layout, request, impl_->options);
3352 if (!sameFile(impl_->identity, fileIdentity(impl_->path)))
3353 throwState("input file was replaced while following it");
3354 ReaderOptions pollOptions = impl_->options;
3355 pollOptions.lockMode = LockMode::None;
3356 Reader poll = Reader::open(impl_->path, pollOptions);
3357 poll.buildPageIndex();
3358 const std::int64_t pageNumber = poll.indexedPageCount().value_or(0);
3359 if (!pageNumber) {
3360 poll.close();
3361 return std::nullopt;
3362 }
3363 ReadRequest probe;
3364 probe.parameters = FieldSelection::noFields();
3365 probe.arrays = FieldSelection::noFields();
3366 probe.columns = FieldSelection::noFields();
3367 poll.gotoPage(pageNumber);
3368 if (!poll.next(probe)) throwState("indexed final page could not be read");
3369 const std::int64_t observedRows = poll.impl_->lastRawRows;
3370 if (!impl_->tailInitialized) {
3371 impl_->tailPageNumber = pageNumber;
3372 impl_->tailRows = observedRows;
3373 impl_->tailInitialized = true;
3374 poll.close();
3375 return std::nullopt;
3376 }
3377 if (pageNumber != impl_->tailPageNumber)
3378 throwState("readNewRows only follows growth of the existing final page");
3379 if (observedRows < impl_->tailRows)
3380 throwState("the followed page was truncated");
3381 if (observedRows == impl_->tailRows) {
3382 poll.close();
3383 return std::nullopt;
3384 }
3385 const std::int64_t firstRow = impl_->tailRows;
3386 const std::int64_t newRows = observedRows - firstRow;
3387 if (request.rows.last) {
3388 request.rows.first = firstRow + std::max<std::int64_t>(0, newRows - *request.rows.last);
3389 request.rows.last.reset();
3390 } else {
3391 request.rows.first = firstRow + std::min(request.rows.first, newRows);
3392 }
3393 poll.gotoPage(pageNumber);
3394 auto delta = poll.next(std::move(request));
3395 poll.close();
3396 if (!delta) throwState("final page disappeared while reading newly appended rows");
3397 impl_->tailRows = observedRows;
3398 return PageDelta{pageNumber, firstRow, std::move(*delta)};
3399}
3400
3401MaterializedDataset Reader::readAll(ReadRequest request) {
3402 MaterializedDataset result{layout(), {}};
3403 while (auto page = next(request))
3404 result.pages.push_back(std::move(*page));
3405 return result;
3406}
3407
3408void Reader::close() {
3409 if (!impl_ || impl_->closed)
3410 return;
3411 if (!impl_->disconnected) impl_->stream.close();
3412 impl_->lock.reset();
3413 impl_->closed = true;
3414}
3415
3417 std::filesystem::path path;
3418 WriterOptions options;
3419 std::shared_ptr<Layout> layout;
3420 std::unique_ptr<BufferedStream> stream;
3421 std::optional<Page> current;
3422 std::int64_t pageNumber = 0;
3423 std::int64_t expectedRows = 0;
3424 std::int64_t updateInterval = 0;
3425 std::optional<std::uint64_t> pageStart;
3426 std::optional<std::uint64_t> disconnectedOffset;
3427 std::unique_ptr<PathLock> lock;
3428 FileIdentity identity;
3429 bool snapshotWritten = false;
3430 bool rewriteLastPage = false;
3431 bool pathBacked = false;
3432 bool disconnected = false;
3433 bool closed = false;
3434
3435 void writeCurrent(bool keepOpen) {
3436 if (!current)
3437 throwState("no page has been started");
3438 if (disconnected)
3439 throwState("writer is disconnected");
3440 validatePage(*current, *layout);
3441 if (rewriteLastPage) {
3442 const std::filesystem::path temporary(path.string() + ".sddspp.tmp");
3443 if (std::filesystem::exists(temporary))
3444 throwState("temporary rewrite file already exists: " + temporary.string());
3445 try {
3446 Layout outputLayout = *layout;
3447 BufferedStream output(openOutput(temporary,
3448 compressionFor(path, options.compression), "wb",
3449 options.bufferBytes, options.gzipLevel,
3450 options.lzmaPreset, options.lzmaCheck));
3451 writeLayout(output, outputLayout, options);
3452 std::int64_t number = 0;
3453 Reader source = Reader::open(path);
3454 while (number < pageNumber) {
3455 auto page = source.next();
3456 if (!page)
3457 throwState("source file lost a page during atomic last-page update");
3458 writePageData(output, *page, outputLayout, options, ++number);
3459 }
3460 source.close();
3461 writePageData(output, *current, outputLayout, options, ++number);
3462 output.close();
3463#if defined(_WIN32)
3464 lock.reset();
3465#endif
3466 replaceFile(temporary, path);
3467 identity = fileIdentity(path);
3468 if (options.lockMode != LockMode::None)
3469 lock = acquirePathLock(path, options.lockMode, false);
3470 } catch (...) {
3471 std::error_code ignored;
3472 std::filesystem::remove(temporary, ignored);
3473 throw;
3474 }
3475 snapshotWritten = true;
3476 } else {
3477 if (!stream)
3478 throwState("writer has no output stream");
3479 if (snapshotWritten) {
3480 if (!pageStart || !stream->seekable())
3481 throwState("page update requires a seekable, uncompressed output");
3482 stream->seek(*pageStart);
3483 } else {
3484 pageStart = stream->seekable() ? std::optional<std::uint64_t>(stream->tell())
3485 : std::nullopt;
3486 }
3487 writePageData(*stream, *current, *layout, options, pageNumber + 1);
3488 if (snapshotWritten)
3489 stream->truncate(stream->tell());
3490 snapshotWritten = true;
3491 stream->flush();
3492 }
3493 if (!keepOpen) {
3494 ++pageNumber;
3495 current.reset();
3496 pageStart.reset();
3497 snapshotWritten = false;
3498 }
3499 }
3500};
3501
3502Writer::Writer(std::unique_ptr<Impl> impl) : impl_(std::move(impl)) {}
3503Writer::Writer(Writer &&) noexcept = default;
3504Writer &Writer::operator=(Writer &&) noexcept = default;
3505Writer::~Writer() = default;
3506
3507Writer Writer::create(const std::filesystem::path &path, Layout layout,
3508 WriterOptions options) {
3509 const Compression compression = compressionFor(path, options.compression);
3510 if (compression == Compression::Xz || compression == Compression::Lzma)
3511 layout.data.mode = DataMode::Binary;
3512 validateLayout(layout);
3513 auto lock = acquirePathLock(path, options.lockMode, true);
3514 auto impl = std::make_unique<Impl>();
3515 impl->path = path;
3516 impl->pathBacked = true;
3517 impl->options = options;
3518 impl->layout = std::make_shared<Layout>(std::move(layout));
3519 impl->stream = std::make_unique<BufferedStream>(
3520 openOutput(path, options.compression, "wb", options.bufferBytes,
3521 options.gzipLevel, options.lzmaPreset, options.lzmaCheck));
3522 impl->identity = fileIdentity(path);
3523 impl->lock = std::move(lock);
3524 writeLayout(*impl->stream, *impl->layout, options);
3525 return Writer(std::move(impl));
3526}
3527
3528Writer Writer::toStdout(Layout layout, WriterOptions options) {
3529 if (options.compression == Compression::Xz || options.compression == Compression::Lzma)
3530 layout.data.mode = DataMode::Binary;
3531 validateLayout(layout);
3532 auto impl = std::make_unique<Impl>();
3533 impl->options = options;
3534 impl->layout = std::make_shared<Layout>(std::move(layout));
3535 impl->stream = std::make_unique<BufferedStream>(standardOutput(options));
3536 writeLayout(*impl->stream, *impl->layout, options);
3537 return Writer(std::move(impl));
3538}
3539
3540Writer Writer::toSink(std::unique_ptr<OutputSink> sink, Layout layout,
3541 std::string sinkName, WriterOptions options) {
3542 if (options.compression == Compression::Xz || options.compression == Compression::Lzma)
3543 layout.data.mode = DataMode::Binary;
3544 validateLayout(layout);
3545 auto impl = std::make_unique<Impl>();
3546 impl->path = std::filesystem::path(std::move(sinkName));
3547 impl->options = options;
3548 impl->layout = std::make_shared<Layout>(std::move(layout));
3549 auto stream = wrapOutputCodec(std::make_unique<OutputSinkStream>(std::move(sink)), options);
3550 impl->stream = std::make_unique<BufferedStream>(std::move(stream));
3551 writeLayout(*impl->stream, *impl->layout, options);
3552 return Writer(std::move(impl));
3553}
3554
3555Writer Writer::append(const std::filesystem::path &path, WriterOptions options) {
3556 if (compressionFor(path, options.compression) != Compression::None)
3557 throwState("appending new pages to compressed SDDS files is not supported");
3558 auto lock = acquirePathLock(path, options.lockMode, false);
3559 Reader reader = Reader::open(path);
3560 Layout layout = reader.layout();
3561 std::int64_t pages = 0;
3562 if (layout.data.mode == DataMode::Ascii) {
3563 reader.buildPageIndex();
3564 pages = reader.indexedPageCount().value_or(0);
3565 }
3566 reader.close();
3567 auto impl = std::make_unique<Impl>();
3568 impl->path = path;
3569 impl->pathBacked = true;
3570 impl->options = options;
3571 impl->layout = std::make_shared<Layout>(std::move(layout));
3572 impl->pageNumber = pages;
3573 impl->stream = std::make_unique<BufferedStream>(
3574 openOutput(path, Compression::None, "r+b", options.bufferBytes));
3575 impl->identity = fileIdentity(path);
3576 impl->lock = std::move(lock);
3577 impl->stream->seek(std::filesystem::file_size(path));
3578 return Writer(std::move(impl));
3579}
3580
3581Writer Writer::appendToLastPage(const std::filesystem::path &path,
3582 std::int64_t updateInterval, WriterOptions options) {
3583 if (updateInterval < 1)
3584 throwState("update interval must be positive");
3585 auto lock = acquirePathLock(path, options.lockMode, false);
3586 Reader reader = Reader::open(path);
3587 Layout layout = reader.layout();
3588 std::optional<std::uint64_t> lastPageOffset;
3589 std::optional<Page> lastPage;
3590 std::int64_t totalPages = 0;
3591 if (reader.impl_->dataStart) {
3592 reader.buildPageIndex();
3593 totalPages = reader.indexedPageCount().value_or(0);
3594 if (totalPages) {
3595 lastPageOffset = reader.impl_->pageOffsets.at(static_cast<std::size_t>(totalPages - 1));
3596 reader.gotoPage(totalPages);
3597 lastPage = reader.next();
3598 }
3599 } else {
3600 while (auto page = reader.next()) {
3601 lastPage = std::move(*page);
3602 ++totalPages;
3603 }
3604 }
3605 if (!lastPage)
3606 throwState("cannot append to the last page of an empty SDDS file");
3607 reader.close();
3608 auto impl = std::make_unique<Impl>();
3609 impl->path = path;
3610 impl->pathBacked = true;
3611 impl->options = options;
3612 impl->layout = std::make_shared<Layout>(std::move(layout));
3613 impl->current = std::move(*lastPage);
3614 impl->pageNumber = totalPages - 1;
3615 impl->updateInterval = updateInterval;
3616 const bool canUpdateInPlace = compressionFor(path, options.compression) == Compression::None &&
3617 impl->layout->data.mode == DataMode::Binary &&
3618 impl->layout->data.majorOrder == MajorOrder::Row;
3619 if (options.updateStrategy == UpdateStrategy::InPlaceOnly && !canUpdateInPlace)
3620 throwState("in-place last-page updates require uncompressed row-major binary data");
3621 impl->rewriteLastPage = options.updateStrategy == UpdateStrategy::AtomicRewrite ||
3622 !canUpdateInPlace;
3623 if (!impl->rewriteLastPage) {
3624 if (!lastPageOffset)
3625 throwState("in-place last-page update requires a seekable input");
3626 impl->stream = std::make_unique<BufferedStream>(
3627 openOutput(path, Compression::None, "r+b", options.bufferBytes));
3628 impl->pageStart = *lastPageOffset;
3629 impl->snapshotWritten = true;
3630 }
3631 impl->identity = fileIdentity(path);
3632 impl->lock = std::move(lock);
3633 return Writer(std::move(impl));
3634}
3635
3636const Layout &Writer::layout() const {
3637 if (!impl_ || impl_->closed)
3638 throwState("writer is closed");
3639 return *impl_->layout;
3640}
3641
3642std::int64_t Writer::rowsPresent() const noexcept {
3643 return impl_ && impl_->current ? impl_->current->rowCount() : 0;
3644}
3645
3646void Writer::write(const Page &page) {
3647 if (!impl_ || impl_->closed)
3648 throwState("writer is closed");
3649 if (impl_->current)
3650 throwState("commit the current page before writing another page");
3651 impl_->current = page;
3652 try {
3653 impl_->writeCurrent(false);
3654 } catch (const Error &error) {
3655 throwWithContext(error, impl_->path, impl_->pageNumber + 1,
3656 impl_->stream ? std::optional<std::uint64_t>(impl_->stream->offset())
3657 : std::nullopt);
3658 }
3659}
3660
3661void Writer::write(Page &&page) {
3662 if (!impl_ || impl_->closed) throwState("writer is closed");
3663 if (impl_->disconnected) throwState("writer is disconnected");
3664 if (impl_->current) throwState("commit the current page before writing another page");
3665 impl_->current.emplace(std::move(page));
3666 try {
3667 impl_->writeCurrent(false);
3668 } catch (const Error &error) {
3669 throwWithContext(error, impl_->path, impl_->pageNumber + 1,
3670 impl_->stream ? std::optional<std::uint64_t>(impl_->stream->offset())
3671 : std::nullopt);
3672 }
3673}
3674
3675void Writer::beginPage(std::int64_t expectedRows) {
3676 if (!impl_ || impl_->closed)
3677 throwState("writer is closed");
3678 if (impl_->current)
3679 throwState("a page is already active");
3680 if (expectedRows < 0)
3681 throwState("expected row count cannot be negative");
3682 impl_->expectedRows = expectedRows;
3683 impl_->current.emplace(impl_->layout);
3684}
3685
3686void Writer::setParameter(std::string_view name, Scalar value) {
3687 if (!impl_ || !impl_->current)
3688 throwState("no page has been started");
3689 impl_->current->setParameter(name, std::move(value));
3690}
3691
3692void Writer::setArray(std::string_view name, ArrayData value) {
3693 if (!impl_ || !impl_->current)
3694 throwState("no page has been started");
3695 impl_->current->setArray(name, std::move(value));
3696}
3697
3698void Writer::setColumn(std::string_view name, Values value, std::int64_t startRow) {
3699 if (!impl_ || !impl_->current)
3700 throwState("no page has been started");
3701 const std::size_t index = impl_->layout->columnIndex(name);
3702 if (startRow == 0)
3703 impl_->current->setColumn(index, std::move(value));
3704 else {
3705 mergeValuesInPlace(impl_->current->columns_.at(index), std::move(value), startRow);
3706 impl_->current->columnsLoaded_.at(index) = true;
3707 impl_->current->rowCount_ = 0;
3708 for (std::size_t column = 0; column < impl_->current->columns_.size(); ++column)
3709 if (impl_->current->columnsLoaded_[column])
3710 impl_->current->rowCount_ = std::max(
3711 impl_->current->rowCount_,
3712 static_cast<std::int64_t>(valuesSize(impl_->current->columns_[column])));
3713 }
3714}
3715
3716void Writer::commitPage() {
3717 if (!impl_ || impl_->closed)
3718 throwState("writer is closed");
3719 try {
3720 impl_->writeCurrent(false);
3721 } catch (const Error &error) {
3722 throwWithContext(error, impl_->path, impl_->pageNumber + 1,
3723 impl_->stream ? std::optional<std::uint64_t>(impl_->stream->offset())
3724 : std::nullopt);
3725 }
3726}
3727
3728void Writer::updatePage(bool flushRows) {
3729 if (!impl_ || impl_->closed)
3730 throwState("writer is closed");
3731 if (!impl_->rewriteLastPage && impl_->stream && !impl_->stream->seekable() &&
3732 impl_->snapshotWritten)
3733 throwState("page updates require a seekable output or atomic-rewrite mode");
3734 try {
3735 impl_->writeCurrent(true);
3736 } catch (const Error &error) {
3737 throwWithContext(error, impl_->path, impl_->pageNumber + 1,
3738 impl_->stream ? std::optional<std::uint64_t>(impl_->stream->offset())
3739 : std::nullopt);
3740 }
3741 if (flushRows)
3742 sync();
3743}
3744
3745void Writer::sync() {
3746 if (!impl_ || impl_->closed)
3747 throwState("writer is closed");
3748 if (impl_->stream) {
3749 impl_->stream->flush();
3750 if (impl_->layout->data.fsync) {
3751 impl_->stream->raw().sync();
3752 }
3753 }
3754}
3755
3756void Writer::disconnect() {
3757 if (!impl_ || impl_->closed) throwState("writer is closed");
3758 if (impl_->disconnected) return;
3759 if (!impl_->pathBacked || compressionFor(impl_->path, impl_->options.compression) != Compression::None ||
3760 !impl_->stream || !impl_->stream->seekable())
3761 throwState("disconnect requires a path-backed, uncompressed, seekable output");
3762 impl_->disconnectedOffset = impl_->stream->tell();
3763 impl_->stream->close();
3764 impl_->disconnected = true;
3765}
3766
3767void Writer::reconnect() {
3768 if (!impl_ || impl_->closed) throwState("writer is closed");
3769 if (!impl_->disconnected) return;
3770 if (!sameFile(impl_->identity, fileIdentity(impl_->path)))
3771 throwState("output file was replaced while disconnected");
3772 impl_->stream->replace(openOutput(impl_->path, Compression::None, "r+b",
3773 impl_->options.bufferBytes),
3774 *impl_->disconnectedOffset);
3775 impl_->disconnected = false;
3776}
3777
3778void Writer::close() {
3779 if (!impl_ || impl_->closed)
3780 return;
3781 try {
3782 if (impl_->current)
3783 impl_->writeCurrent(false);
3784 if (impl_->stream && !impl_->disconnected)
3785 impl_->stream->close();
3786 } catch (const Error &error) {
3787 throwWithContext(error, impl_->path, impl_->pageNumber + 1,
3788 impl_->stream ? std::optional<std::uint64_t>(impl_->stream->offset())
3789 : std::nullopt);
3790 }
3791 impl_->lock.reset();
3792 impl_->closed = true;
3793}
3794
3795} // namespace sdds
write(SddsFile sdds_file, output_file)
Mostly backward compatible with the PyLHC sdds module write() function.
Definition sdds.py:1967
SddsFile read(input_file)
Mostly backward compatible with the PyLHC sdds module read() function.
Definition sdds.py:1870