8#include <pixie/detail/serialization.h>
9#include <pixie/storage/read_only_view.h>
36 std::vector<std::byte> content;
38 std::uint32_t mode = 0;
45 std::uint32_t mode = 0;
50 std::string_view path;
51 std::size_t content_offset = 0;
52 std::size_t content_size = 0;
53 std::size_t newline_rank_base = 0;
54 std::size_t line_count = 0;
55 std::uint32_t mode = 0;
60namespace file_archive_detail {
62inline constexpr std::array<std::uint8_t, 8> kMagic = {
'P',
'I',
'X',
'A',
64inline constexpr std::uint32_t kVersion = 5;
65inline constexpr std::size_t kHeaderBytes = 3 *
sizeof(std::uint64_t);
66inline constexpr std::size_t kMetadataHeaderBytes =
67 kHeaderBytes + 2 *
sizeof(std::uint32_t) + 5 *
sizeof(std::uint64_t);
68inline constexpr std::size_t kRecordBytes =
69 6 *
sizeof(std::uint64_t) +
sizeof(std::uint32_t) + 4;
70inline constexpr std::size_t kByteAlphabetSize = 256;
73 std::size_t path_offset = 0;
74 std::size_t path_size = 0;
75 std::size_t content_offset = 0;
76 std::size_t content_size = 0;
77 std::size_t newline_rank_base = 0;
78 std::size_t line_count = 0;
79 std::uint32_t mode = 0;
91 void Consume(std::span<const std::byte> bytes) {
95 for (
const std::byte raw : bytes) {
96 const std::uint8_t
byte = std::to_integer<std::uint8_t>(raw);
97 if (remaining_ == 0) {
98 if (
byte == 0 && reject_nul_) {
105 if (
byte >= 0xc2U &&
byte <= 0xdfU) {
106 code_point_ =
byte & 0x1fU;
109 }
else if (
byte >= 0xe0U &&
byte <= 0xefU) {
110 code_point_ =
byte & 0x0fU;
113 }
else if (
byte >= 0xf0U &&
byte <= 0xf4U) {
114 code_point_ =
byte & 0x07U;
123 if ((
byte & 0xc0U) != 0x80U) {
127 code_point_ = (code_point_ << 6U) | (
byte & 0x3fU);
129 if (remaining_ == 0 &&
130 (code_point_ < minimum_ || code_point_ > 0x10ffffU ||
131 (code_point_ >= 0xd800U && code_point_ <= 0xdfffU))) {
139 bool valid()
const {
return valid_ && remaining_ == 0; }
144 std::uint32_t code_point_ = 0;
145 std::uint32_t minimum_ = 0;
146 std::uint8_t remaining_ = 0;
149inline bool IsUtf8(std::span<const std::byte> bytes,
bool reject_nul) {
150 Utf8Validator validator(reject_nul);
151 validator.Consume(bytes);
152 return validator.valid();
155inline bool IsUtf8(std::string_view value,
bool reject_nul) {
156 return IsUtf8(std::as_bytes(std::span(value.data(), value.size())),
160inline std::size_t CheckedSize(std::uint64_t value) {
161 if (value > std::numeric_limits<std::size_t>::max()) {
162 throw std::length_error(
"File-archive size does not fit in size_t");
164 return static_cast<std::size_t
>(value);
167inline void RequireZeroBytes(std::span<const std::byte> bytes,
168 std::size_t offset) {
169 for (
const std::byte
byte : bytes) {
170 if (
byte != std::byte{0}) {
171 throw SerializationError(
"Non-zero file-archive padding", offset);
194 return file_archive_detail::kVersion;
198 std::size_t
size()
const {
return impl().records_impl().size(); }
211 return size() * file_archive_detail::kRecordBytes;
216 return impl().path_storage_size_impl();
221 const std::size_t unaligned = file_archive_detail::kMetadataHeaderBytes +
224 (
alignof(std::uint64_t) - unaligned %
alignof(std::uint64_t)) %
225 alignof(std::uint64_t);
232 std::optional<std::size_t>
find(std::string_view path)
const {
233 const auto records = impl().records_impl();
234 const auto position =
235 std::lower_bound(records.begin(), records.end(), path,
237 std::string_view wanted) {
238 return impl().path_impl(record) < wanted;
240 if (position == records.end() || impl().path_impl(*position) != path) {
243 return static_cast<std::size_t
>(position - records.begin());
251 const auto records = impl().records_impl();
252 if (index >= records.size()) {
253 throw std::out_of_range(
"File-archive entry index is out of range");
255 const auto& record = records[index];
256 return {.path = impl().path_impl(record),
257 .content_offset = record.content_offset,
258 .content_size = record.content_size,
259 .newline_rank_base = record.newline_rank_base,
260 .line_count = record.line_count,
263 .is_text = record.is_text};
270 std::vector<std::byte>
extract(std::size_t index)
const {
272 return extract_range(metadata.content_offset,
273 metadata.content_offset + metadata.content_size);
285 std::size_t right)
const {
287 if (metadata.type != FileArchiveEntryType::kRegular) {
288 throw std::invalid_argument(
"Line extraction requires a regular file");
290 if (!metadata.is_text) {
291 throw std::invalid_argument(
"Line extraction requires a text file");
293 if (left > right || right > metadata.line_count) {
294 throw std::out_of_range(
"File-archive line range is out of range");
296 const auto& tree = impl().tree_impl();
297 const auto boundary = [&](std::size_t line) {
299 return metadata.content_offset;
301 if (line == metadata.line_count) {
302 return metadata.content_offset + metadata.content_size;
304 return tree.select(
'\n', metadata.newline_rank_base + line) + 1;
306 const std::size_t begin = boundary(left);
307 const std::size_t end = left == right ? begin : boundary(right);
308 return extract_range(begin, end);
312 const Impl& impl()
const {
return static_cast<const Impl&
>(*this); }
314 std::vector<std::byte> extract_range(std::size_t begin,
315 std::size_t end)
const {
316 const std::vector<std::uint8_t> symbols =
317 impl().tree_impl().get_segment(begin, end);
318 std::vector<std::byte> bytes;
319 bytes.reserve(symbols.size());
320 for (
const std::uint8_t symbol : symbols) {
321 bytes.push_back(
static_cast<std::byte
>(symbol));
331template <StorageImplementation Storage>
336 requires(std::same_as<Storage, ReadOnlyStorageView>)
346 requires(std::same_as<Storage, ReadOnlyStorageView>)
348 FileArchiveIndex& operator=(
const FileArchiveIndex&)
349 requires(std::same_as<Storage, ReadOnlyStorageView>)
351 FileArchiveIndex(
const FileArchiveIndex&)
352 requires(std::same_as<Storage, AlignedStorage>)
354 FileArchiveIndex& operator=(
const FileArchiveIndex&)
355 requires(std::same_as<Storage, AlignedStorage>)
357 FileArchiveIndex(FileArchiveIndex&&) noexcept = default;
358 FileArchiveIndex& operator=(FileArchiveIndex&&) noexcept = default;
365 explicit FileArchiveIndex(
371 consume(std::span<const std::byte>(source.content));
384 template <
class ReadSource>
386 std::vector<FileArchiveSourceMetadata> sources,
387 ReadSource&& read_source,
389 requires(std::same_as<Storage, AlignedStorage>)
391 build_sources(sources, read_source);
396 if (writer.
size_bytes() %
alignof(std::uint64_t) != 0) {
397 throw std::invalid_argument(
398 "File-archive serialization requires an aligned writer offset");
400 const std::size_t begin = writer.
size_bytes();
401 detail::write_magic(writer, file_archive_detail::kMagic);
402 writer.
write_u32(file_archive_detail::kVersion);
405 writer.
write_u32(
static_cast<std::uint32_t
>(build_type_));
409 writer.
write_size(records_.size() * file_archive_detail::kRecordBytes);
412 for (
const auto& record : records_) {
420 writer.
write_u8(
static_cast<std::uint8_t
>(record.type));
421 writer.
write_u8(
static_cast<std::uint8_t
>(record.is_text));
425 writer.
align_to(
alignof(std::uint64_t));
426 if (!tree_.has_value()) {
427 throw std::logic_error(
"Cannot serialize an uninitialized file archive");
429 tree_->serialize(writer);
430 writer.
align_to(
alignof(std::uint64_t));
432 static_cast<std::uint64_t
>(writer.
size_bytes() - begin));
443 requires(std::same_as<Storage, AlignedStorage> ||
444 std::same_as<Storage, ReadOnlyStorageView>)
446 if constexpr (std::same_as<Storage, ReadOnlyStorageView>) {
447 if (
reinterpret_cast<std::uintptr_t
>(reader.remaining_bytes().data()) %
448 alignof(std::uint64_t) !=
450 throw std::invalid_argument(
451 "Serialized file archive is not word aligned");
454 const std::size_t available = reader.remaining();
455 detail::require_magic(reader, file_archive_detail::kMagic);
456 if (reader.read_u32() != file_archive_detail::kVersion ||
457 reader.read_u32() != 0) {
458 throw std::invalid_argument(
"Incompatible serialized file archive");
460 const std::size_t artifact_size = detail::checked_artifact_size(
461 reader.read_u64(), file_archive_detail::kHeaderBytes, available);
463 artifact_size - file_archive_detail::kHeaderBytes);
465 FileArchiveIndex result(LoadTag{});
468 static_cast<std::uint32_t
>(WaveletTreeBuildType::Huffman)) {
469 throw std::invalid_argument(
"Invalid file-archive build type");
473 throw std::invalid_argument(
"Invalid file-archive reserved field");
475 const std::size_t count = payload.
read_size();
476 result.logical_size_ = payload.
read_size();
477 const std::size_t records_bytes = payload.
read_size();
478 const std::size_t paths_bytes = payload.
read_size();
480 count > std::numeric_limits<std::size_t>::max() /
481 file_archive_detail::kRecordBytes ||
482 records_bytes != count * file_archive_detail::kRecordBytes) {
483 throw std::invalid_argument(
"Invalid file-archive section sizes");
487 result.records_.reserve(count);
488 for (std::size_t index = 0; index < count; ++index) {
490 record.path_offset = records_reader.
read_size();
491 record.path_size = records_reader.
read_size();
492 record.content_offset = records_reader.
read_size();
493 record.content_size = records_reader.
read_size();
494 record.newline_rank_base = records_reader.
read_size();
495 record.line_count = records_reader.
read_size();
496 record.mode = records_reader.
read_u32();
497 const std::uint8_t type = records_reader.
read_u8();
498 const std::uint8_t is_text = records_reader.
read_u8();
499 if (type >
static_cast<std::uint8_t
>(FileArchiveEntryType::kSymlink) ||
500 is_text > 1 || records_reader.
read_u16() != 0) {
501 throw std::invalid_argument(
"Invalid file-archive entry flags");
504 record.is_text = is_text != 0;
505 result.records_.push_back(record);
507 if (!records_reader.
empty()) {
508 throw std::invalid_argument(
"Invalid file-archive record section");
510 result.paths_ = MakeStorage(payload.
read_bytes(paths_bytes));
511 const std::size_t padding =
512 (
alignof(std::uint64_t) -
514 alignof(std::uint64_t);
515 const std::size_t padding_offset = payload.
byte_offset();
516 file_archive_detail::RequireZeroBytes(payload.
read_bytes(padding),
519 payload, validation));
528 explicit FileArchiveIndex(LoadTag) {}
530 static Storage MakeStorage(std::span<const std::byte> bytes) {
531 if constexpr (std::same_as<Storage, ReadOnlyStorageView>) {
532 return ReadOnlyStorageView(bytes);
534 if (bytes.size() > std::numeric_limits<std::size_t>::max() / 8) {
535 throw std::length_error(
"File-archive path storage is too large");
537 AlignedStorage result(bytes.size() * 8);
538 std::ranges::copy(bytes, result.writable_bytes().begin());
543 template <
class Source,
class ReadSource>
544 void build_sources(std::vector<Source>& sources, ReadSource&& read_source) {
545 std::sort(sources.begin(), sources.end(),
546 [](
const auto& left,
const auto& right) {
547 return left.path < right.path;
549 std::array<std::size_t, file_archive_detail::kByteAlphabetSize>
551 std::vector<std::uint64_t> content_hashes;
552 content_hashes.reserve(sources.size());
554 std::size_t newline_rank = 0;
555 for (std::size_t source_index = 0; source_index < sources.size();
557 const Source& source = sources[source_index];
558 if (source.path.empty() ||
559 !file_archive_detail::IsUtf8(source.path,
true)) {
560 throw std::invalid_argument(
561 "File-archive paths must be non-empty UTF-8 strings");
563 if (source_index != 0 && sources[source_index - 1].path == source.path) {
564 throw std::invalid_argument(
"File-archive paths must be unique");
566 if (source.type != FileArchiveEntryType::kRegular &&
567 source.type != FileArchiveEntryType::kSymlink) {
568 throw std::invalid_argument(
"Invalid file-archive entry type");
572 record.path_offset = paths.size();
573 record.path_size = source.path.size();
574 record.content_offset = logical_size_;
575 record.newline_rank_base = newline_rank;
576 record.mode = source.mode;
577 record.type = source.type;
578 paths.append(source.path);
581 std::size_t newlines = 0;
582 bool has_content =
false;
583 std::uint8_t last_byte = 0;
584 std::uint64_t content_hash = 14695981039346656037ULL;
585 read_source(source, [&](std::span<const std::byte> chunk) {
587 std::numeric_limits<std::size_t>::max() - logical_size_) {
588 throw std::length_error(
"File-archive content is too large");
590 logical_size_ += chunk.size();
592 for (
const std::byte
byte : chunk) {
593 const std::uint8_t value = std::to_integer<std::uint8_t>(
byte);
594 ++symbol_counts[value];
595 newlines += value ==
'\n' ? 1U : 0U;
598 content_hash ^= value;
599 content_hash *= 1099511628211ULL;
602 record.content_size = logical_size_ - record.content_offset;
603 record.is_text = utf8.
valid();
604 if (source.type == FileArchiveEntryType::kRegular && has_content) {
606 newlines +
static_cast<std::size_t
>(last_byte !=
'\n');
608 newline_rank += newlines;
609 records_.push_back(record);
610 content_hashes.push_back(content_hash);
613 paths_ = MakeStorage(
614 std::as_bytes(std::span<const char>(paths.data(), paths.size())));
617 file_archive_detail::kByteAlphabetSize, symbol_counts,
619 for (std::size_t index = 0; index < sources.size(); ++index) {
620 std::size_t content_size = 0;
621 std::uint64_t content_hash = 14695981039346656037ULL;
622 read_source(sources[index], [&](std::span<const std::byte> chunk) {
623 if (content_size > records_[index].content_size ||
624 chunk.size() > records_[index].content_size - content_size) {
625 throw std::invalid_argument(
626 "File-archive source changed between build passes");
628 content_size += chunk.size();
629 for (
const std::byte
byte : chunk) {
630 const std::uint8_t value = std::to_integer<std::uint8_t>(
byte);
631 content_hash ^= value;
632 content_hash *= 1099511628211ULL;
636 if (content_size != records_[index].content_size ||
637 content_hash != content_hashes[index]) {
638 throw std::invalid_argument(
639 "File-archive source changed between build passes");
649 void validate(
bool full)
const {
650 if (tree_->size() != logical_size_) {
651 throw std::invalid_argument(
"Invalid file-archive content size");
653 const std::size_t newline_count = tree_->rank(
'\n', logical_size_);
654 std::size_t expected_content_offset = 0;
655 std::size_t expected_newline_rank = 0;
656 std::string_view previous_path;
657 for (std::size_t index = 0; index < records_.size(); ++index) {
658 const auto& record = records_[index];
659 if (record.path_offset > paths_.size_bytes() ||
660 record.path_size > paths_.size_bytes() - record.path_offset ||
661 record.content_offset > logical_size_ ||
662 record.content_size > logical_size_ - record.content_offset ||
663 record.newline_rank_base > newline_count ||
664 record.line_count > newline_count - record.newline_rank_base + 1 ||
665 (record.type == FileArchiveEntryType::kSymlink &&
666 record.line_count != 0)) {
667 throw std::invalid_argument(
"Invalid file-archive entry bounds");
672 const std::string_view path = path_impl(record);
673 if (path.empty() || !file_archive_detail::IsUtf8(path,
true) ||
674 (index != 0 && previous_path >= path) ||
675 record.content_offset != expected_content_offset ||
676 record.newline_rank_base != expected_newline_rank) {
677 throw std::invalid_argument(
"Invalid file-archive entry metadata");
679 const std::vector<std::byte> content = this->
extract(index);
680 const bool is_text = file_archive_detail::IsUtf8(content,
true);
681 std::size_t newlines = 0;
682 for (
const std::byte
byte : content) {
683 newlines +=
byte == std::byte{
'\n'} ? 1U : 0U;
685 std::size_t line_count = 0;
686 if (record.type == FileArchiveEntryType::kRegular && !content.empty()) {
687 line_count = newlines +
static_cast<std::size_t
>(content.back() !=
690 if (record.is_text != is_text || record.line_count != line_count) {
691 throw std::invalid_argument(
"Invalid file-archive derived metadata");
693 previous_path = path;
694 expected_content_offset += content.size();
695 expected_newline_rank += newlines;
697 if (full && expected_content_offset != logical_size_) {
698 throw std::invalid_argument(
"Invalid file-archive content layout");
703 std::span<const file_archive_detail::FileRecord> records_impl()
const {
707 std::string_view path_impl(
708 const file_archive_detail::FileRecord& record)
const {
709 const std::span<const std::byte> bytes = paths_.as_bytes();
710 const std::string_view paths =
713 : std::string_view(
reinterpret_cast<const char*
>(bytes.data()),
715 return paths.substr(record.path_offset, record.path_size);
718 const WaveletTreeIndex<std::uint8_t, Storage>& tree_impl()
const {
722 std::size_t logical_size_impl()
const {
return logical_size_; }
726 std::size_t path_storage_size_impl()
const {
return paths_.size_bytes(); }
728 std::vector<file_archive_detail::FileRecord> records_;
730 std::optional<WaveletTreeIndex<std::uint8_t, Storage>> tree_;
731 std::size_t logical_size_ = 0;
Owning storage with a logical byte size and 64-byte-aligned backing.
Definition aligned.h:41
Bounds-checked reader for canonical little-endian binary data.
Definition serialization.h:526
std::uint16_t read_u16()
Read an unsigned little-endian 16-bit integer.
Definition serialization.h:555
std::uint8_t read_u8()
Read an unsigned eight-bit integer.
Definition serialization.h:552
BinaryReader read_subreader(std::size_t count)
Read a bounded region as an independent child reader.
Definition serialization.h:615
std::span< const std::byte > read_bytes(std::size_t count)
Read exactly count uninterpreted bytes.
Definition serialization.h:604
void require_zero_padding(std::size_t maximum)
Consume at most maximum trailing zero-padding bytes.
Definition serialization.h:633
std::uint32_t read_u32()
Read an unsigned little-endian 32-bit integer.
Definition serialization.h:558
bool empty() const noexcept
Return whether all input bytes have been consumed.
Definition serialization.h:532
std::size_t byte_offset() const noexcept
Return the current byte offset in the outermost input.
Definition serialization.h:541
std::size_t read_size()
Read an unsigned 64-bit size and convert it to size_t.
Definition serialization.h:587
Bounded-buffer writer for canonical little-endian binary data.
Definition serialization.h:198
void write_bytes(std::span< const std::byte > bytes)
Append bytes without interpretation.
Definition serialization.h:297
void write_u32(std::uint32_t value)
Write an unsigned 32-bit integer in little-endian order.
Definition serialization.h:258
void write_u8(std::uint8_t value)
Write an unsigned eight-bit integer.
Definition serialization.h:252
std::size_t size_bytes() const noexcept
Return the logical number of bytes written.
Definition serialization.h:244
void align_to(std::size_t alignment)
Pad with zero bytes to the next multiple of alignment.
Definition serialization.h:347
void write_u16(std::uint16_t value)
Write an unsigned 16-bit integer in little-endian order.
Definition serialization.h:255
void patch_u64(std::size_t position, std::uint64_t value)
Replace an existing 64-bit field with value.
Definition serialization.h:373
std::size_t write_u64_placeholder()
Write a zero 64-bit field and return its byte position.
Definition serialization.h:363
void write_size(std::size_t value)
Write a platform size as an unsigned 64-bit integer.
Definition serialization.h:287
CRTP facade for file-archive lookup and extraction.
Definition file_archive.h:190
std::size_t size() const
Return the number of archived entries.
Definition file_archive.h:198
std::size_t path_storage_bytes() const
Return serialized path-blob bytes.
Definition file_archive.h:215
std::optional< std::size_t > find(std::string_view path) const
Find an exact archive-relative path.
Definition file_archive.h:232
WaveletTreeBuildType build_type() const
Return the wavelet-tree construction strategy.
Definition file_archive.h:207
std::size_t metadata_bytes() const
Return framing, file-table, path, and alignment bytes.
Definition file_archive.h:220
FileArchiveEntry entry(std::size_t index) const
Return metadata for an entry.
Definition file_archive.h:250
std::vector< std::byte > extract_lines(std::size_t index, std::size_t left, std::size_t right) const
Reconstruct zero-based half-open line range [left, right).
Definition file_archive.h:283
std::size_t logical_size_bytes() const
Return the number of logical content bytes, excluding framing.
Definition file_archive.h:204
std::size_t file_table_bytes() const
Return serialized fixed-record bytes.
Definition file_archive.h:210
bool empty() const
Return whether the archive has no entries.
Definition file_archive.h:201
std::vector< std::byte > extract(std::size_t index) const
Reconstruct the complete byte content of one entry.
Definition file_archive.h:270
static constexpr std::uint32_t format_version()
Return the serialized file-archive format version.
Definition file_archive.h:193
Storage-backed byte-oriented file archive.
Definition file_archive.h:333
FileArchiveIndex(std::vector< FileArchiveSourceMetadata > sources, ReadSource &&read_source, WaveletTreeBuildType build_type=WaveletTreeBuildType::Huffman)
Construct from metadata and a replayable chunk reader.
Definition file_archive.h:385
FileArchiveIndex(const FileArchiveIndex &)=default
Owning archives are move-only; archive views are copyable.
static FileArchiveIndex deserialize_impl(BinaryReader &reader, DeserializationValidation validation=DeserializationValidation::kQuick)
Deserialize one framed archive using this specialization's storage.
Definition file_archive.h:440
void serialize_impl(BinaryWriter &writer) const
Serialize one native, framed Pixie file archive.
Definition file_archive.h:395
CRTP facade for optional binary serialization and deserialization.
Definition serialization.h:693
static WaveletTreeIndex< Symbol, AlignedStorage > deserialize(BinaryReader &reader, Context &&... context)
Definition serialization.h:710
Incrementally validate a byte stream as UTF-8.
Definition file_archive.h:85
void Consume(std::span< const std::byte > bytes)
Consume the next contiguous chunk of the byte stream.
Definition file_archive.h:91
bool valid() const
Return whether all consumed bytes form complete valid UTF-8.
Definition file_archive.h:139
Utf8Validator(bool reject_nul)
Construct a validator, optionally rejecting embedded NUL bytes.
Definition file_archive.h:88
FileArchiveEntryType
Kind of filesystem entry stored in a file archive.
Definition file_archive.h:28
FileArchiveIndex< ReadOnlyStorageView > FileArchiveView
Read-only file archive retaining serialized storage views.
Definition file_archive.h:739
FileArchiveIndex< AlignedStorage > FileArchive
Owning aligned-storage file archive.
Definition file_archive.h:736
DeserializationValidation
Validation strength used while restoring serialized indexes.
Definition serialization.h:28
@ kQuick
Check framing, dimensions, references, and other conditions needed for memory-safe terminating querie...
Definition serialization.h:33
@ kFull
Additionally authenticate all source-derived metadata against the supplied source contents.
Definition serialization.h:39
Public metadata for one archive entry.
Definition file_archive.h:49
Source entry used to construct an owning file archive.
Definition file_archive.h:34
Definition file_archive.h:72
All wavelet-tree implementations provided by Pixie.
WaveletTreeBuildType
Construction strategy for a wavelet-tree implementation.
Definition wavelet_tree.h:19