9#include <pixie/detail/serialization.h>
10#include <pixie/storage/read_only_view.h>
11#include <pixie/wavelet_tree/index.h>
37 std::vector<std::byte> content;
39 std::uint32_t mode = 0;
46 std::uint32_t mode = 0;
51 std::string_view path;
52 std::size_t content_offset = 0;
53 std::size_t content_size = 0;
54 std::size_t newline_rank_base = 0;
55 std::size_t line_count = 0;
56 std::uint32_t mode = 0;
61namespace file_archive_detail {
63inline constexpr std::array<std::uint8_t, 8> kMagic = {
'P',
'I',
'X',
'A',
65inline constexpr std::uint32_t kVersion = 5;
66inline constexpr std::size_t kHeaderBytes = 3 *
sizeof(std::uint64_t);
67inline constexpr std::size_t kMetadataHeaderBytes =
68 kHeaderBytes + 2 *
sizeof(std::uint32_t) + 5 *
sizeof(std::uint64_t);
69inline constexpr std::size_t kRecordBytes =
70 6 *
sizeof(std::uint64_t) +
sizeof(std::uint32_t) + 4;
71inline constexpr std::size_t kByteAlphabetSize = 256;
74 std::size_t path_offset = 0;
75 std::size_t path_size = 0;
76 std::size_t content_offset = 0;
77 std::size_t content_size = 0;
78 std::size_t newline_rank_base = 0;
79 std::size_t line_count = 0;
80 std::uint32_t mode = 0;
92 void Consume(std::span<const std::byte> bytes) {
96 for (
const std::byte raw : bytes) {
97 const std::uint8_t
byte = std::to_integer<std::uint8_t>(raw);
98 if (remaining_ == 0) {
99 if (
byte == 0 && reject_nul_) {
106 if (
byte >= 0xc2U &&
byte <= 0xdfU) {
107 code_point_ =
byte & 0x1fU;
110 }
else if (
byte >= 0xe0U &&
byte <= 0xefU) {
111 code_point_ =
byte & 0x0fU;
114 }
else if (
byte >= 0xf0U &&
byte <= 0xf4U) {
115 code_point_ =
byte & 0x07U;
124 if ((
byte & 0xc0U) != 0x80U) {
128 code_point_ = (code_point_ << 6U) | (
byte & 0x3fU);
130 if (remaining_ == 0 &&
131 (code_point_ < minimum_ || code_point_ > 0x10ffffU ||
132 (code_point_ >= 0xd800U && code_point_ <= 0xdfffU))) {
140 bool valid()
const {
return valid_ && remaining_ == 0; }
145 std::uint32_t code_point_ = 0;
146 std::uint32_t minimum_ = 0;
147 std::uint8_t remaining_ = 0;
150inline bool IsUtf8(std::span<const std::byte> bytes,
bool reject_nul) {
151 Utf8Validator validator(reject_nul);
152 validator.Consume(bytes);
153 return validator.valid();
156inline bool IsUtf8(std::string_view value,
bool reject_nul) {
157 return IsUtf8(std::as_bytes(std::span(value.data(), value.size())),
161inline std::size_t CheckedSize(std::uint64_t value) {
162 if (value > std::numeric_limits<std::size_t>::max()) {
163 throw std::length_error(
"File-archive size does not fit in size_t");
165 return static_cast<std::size_t
>(value);
168inline void RequireZeroBytes(std::span<const std::byte> bytes,
169 std::size_t offset) {
170 for (
const std::byte
byte : bytes) {
171 if (
byte != std::byte{0}) {
172 throw SerializationError(
"Non-zero file-archive padding", offset);
195 return file_archive_detail::kVersion;
199 std::size_t
size()
const {
return impl().records_impl().size(); }
212 return size() * file_archive_detail::kRecordBytes;
217 return impl().path_storage_size_impl();
222 const std::size_t unaligned = file_archive_detail::kMetadataHeaderBytes +
225 (
alignof(std::uint64_t) - unaligned %
alignof(std::uint64_t)) %
226 alignof(std::uint64_t);
233 std::optional<std::size_t>
find(std::string_view path)
const {
234 const auto records = impl().records_impl();
235 const auto position =
236 std::lower_bound(records.begin(), records.end(), path,
238 std::string_view wanted) {
239 return impl().path_impl(record) < wanted;
241 if (position == records.end() || impl().path_impl(*position) != path) {
244 return static_cast<std::size_t
>(position - records.begin());
252 const auto records = impl().records_impl();
253 if (index >= records.size()) {
254 throw std::out_of_range(
"File-archive entry index is out of range");
256 const auto& record = records[index];
257 return {.path = impl().path_impl(record),
258 .content_offset = record.content_offset,
259 .content_size = record.content_size,
260 .newline_rank_base = record.newline_rank_base,
261 .line_count = record.line_count,
264 .is_text = record.is_text};
271 std::vector<std::byte>
extract(std::size_t index)
const {
273 return extract_range(metadata.content_offset,
274 metadata.content_offset + metadata.content_size);
286 std::size_t right)
const {
288 if (metadata.type != FileArchiveEntryType::kRegular) {
289 throw std::invalid_argument(
"Line extraction requires a regular file");
291 if (!metadata.is_text) {
292 throw std::invalid_argument(
"Line extraction requires a text file");
294 if (left > right || right > metadata.line_count) {
295 throw std::out_of_range(
"File-archive line range is out of range");
297 const auto& tree = impl().tree_impl();
298 const auto boundary = [&](std::size_t line) {
300 return metadata.content_offset;
302 if (line == metadata.line_count) {
303 return metadata.content_offset + metadata.content_size;
305 return tree.select(
'\n', metadata.newline_rank_base + line) + 1;
307 const std::size_t begin = boundary(left);
308 const std::size_t end = left == right ? begin : boundary(right);
309 return extract_range(begin, end);
313 const Impl& impl()
const {
return static_cast<const Impl&
>(*this); }
315 std::vector<std::byte> extract_range(std::size_t begin,
316 std::size_t end)
const {
317 const std::vector<std::uint8_t> symbols =
318 impl().tree_impl().get_segment(begin, end);
319 std::vector<std::byte> bytes;
320 bytes.reserve(symbols.size());
321 for (
const std::uint8_t symbol : symbols) {
322 bytes.push_back(
static_cast<std::byte
>(symbol));
332template <StorageImplementation Storage>
337 requires(std::same_as<Storage, ReadOnlyStorageView>)
347 requires(std::same_as<Storage, ReadOnlyStorageView>)
349 FileArchiveIndex& operator=(
const FileArchiveIndex&)
350 requires(std::same_as<Storage, ReadOnlyStorageView>)
352 FileArchiveIndex(
const FileArchiveIndex&)
353 requires(std::same_as<Storage, AlignedStorage>)
355 FileArchiveIndex& operator=(
const FileArchiveIndex&)
356 requires(std::same_as<Storage, AlignedStorage>)
358 FileArchiveIndex(FileArchiveIndex&&) noexcept = default;
359 FileArchiveIndex& operator=(FileArchiveIndex&&) noexcept = default;
366 explicit FileArchiveIndex(
372 consume(std::span<const std::byte>(source.content));
387 template <
class ReadSource>
389 std::vector<FileArchiveSourceMetadata> sources,
390 ReadSource&& read_source,
392 requires(std::same_as<Storage, AlignedStorage>)
394 build_sources(sources, read_source);
399 if (writer.
size_bytes() %
alignof(std::uint64_t) != 0) {
400 throw std::invalid_argument(
401 "File-archive serialization requires an aligned writer offset");
403 const std::size_t begin = writer.
size_bytes();
404 detail::write_magic(writer, file_archive_detail::kMagic);
405 writer.
write_u32(file_archive_detail::kVersion);
408 writer.
write_u32(
static_cast<std::uint32_t
>(build_type_));
412 writer.
write_size(records_.size() * file_archive_detail::kRecordBytes);
415 for (
const auto& record : records_) {
423 writer.
write_u8(
static_cast<std::uint8_t
>(record.type));
424 writer.
write_u8(
static_cast<std::uint8_t
>(record.is_text));
428 writer.
align_to(
alignof(std::uint64_t));
429 if (!tree_.has_value()) {
430 throw std::logic_error(
"Cannot serialize an uninitialized file archive");
432 tree_->serialize(writer);
433 writer.
align_to(
alignof(std::uint64_t));
435 static_cast<std::uint64_t
>(writer.
size_bytes() - begin));
446 requires(std::same_as<Storage, AlignedStorage> ||
447 std::same_as<Storage, ReadOnlyStorageView>)
449 if constexpr (std::same_as<Storage, ReadOnlyStorageView>) {
450 if (
reinterpret_cast<std::uintptr_t
>(reader.remaining_bytes().data()) %
451 alignof(std::uint64_t) !=
453 throw std::invalid_argument(
454 "Serialized file archive is not word aligned");
457 const std::size_t available = reader.remaining();
458 detail::require_magic(reader, file_archive_detail::kMagic);
459 if (reader.read_u32() != file_archive_detail::kVersion ||
460 reader.read_u32() != 0) {
461 throw std::invalid_argument(
"Incompatible serialized file archive");
463 const std::size_t artifact_size = detail::checked_artifact_size(
464 reader.read_u64(), file_archive_detail::kHeaderBytes, available);
466 artifact_size - file_archive_detail::kHeaderBytes);
468 FileArchiveIndex result(LoadTag{});
471 static_cast<std::uint32_t
>(WaveletTreeBuildType::Huffman)) {
472 throw std::invalid_argument(
"Invalid file-archive build type");
476 throw std::invalid_argument(
"Invalid file-archive reserved field");
478 const std::size_t count = payload.
read_size();
479 result.logical_size_ = payload.
read_size();
480 const std::size_t records_bytes = payload.
read_size();
481 const std::size_t paths_bytes = payload.
read_size();
483 count > std::numeric_limits<std::size_t>::max() /
484 file_archive_detail::kRecordBytes ||
485 records_bytes != count * file_archive_detail::kRecordBytes) {
486 throw std::invalid_argument(
"Invalid file-archive section sizes");
490 result.records_.reserve(count);
491 for (std::size_t index = 0; index < count; ++index) {
493 record.path_offset = records_reader.
read_size();
494 record.path_size = records_reader.
read_size();
495 record.content_offset = records_reader.
read_size();
496 record.content_size = records_reader.
read_size();
497 record.newline_rank_base = records_reader.
read_size();
498 record.line_count = records_reader.
read_size();
499 record.mode = records_reader.
read_u32();
500 const std::uint8_t type = records_reader.
read_u8();
501 const std::uint8_t is_text = records_reader.
read_u8();
502 if (type >
static_cast<std::uint8_t
>(FileArchiveEntryType::kSymlink) ||
503 is_text > 1 || records_reader.
read_u16() != 0) {
504 throw std::invalid_argument(
"Invalid file-archive entry flags");
507 record.is_text = is_text != 0;
508 result.records_.push_back(record);
510 if (!records_reader.
empty()) {
511 throw std::invalid_argument(
"Invalid file-archive record section");
513 result.paths_ = MakeStorage(payload.
read_bytes(paths_bytes));
514 const std::size_t padding =
515 (
alignof(std::uint64_t) -
517 alignof(std::uint64_t);
518 const std::size_t padding_offset = payload.
byte_offset();
519 file_archive_detail::RequireZeroBytes(payload.
read_bytes(padding),
522 payload, validation));
531 explicit FileArchiveIndex(LoadTag) {}
533 static Storage MakeStorage(std::span<const std::byte> bytes) {
534 if constexpr (std::same_as<Storage, ReadOnlyStorageView>) {
535 return ReadOnlyStorageView(bytes);
537 if (bytes.size() > std::numeric_limits<std::size_t>::max() / 8) {
538 throw std::length_error(
"File-archive path storage is too large");
540 AlignedStorage result(bytes.size() * 8);
541 std::ranges::copy(bytes, result.writable_bytes().begin());
546 template <
class Source,
class ReadSource>
547 void build_sources(std::vector<Source>& sources, ReadSource&& read_source) {
548 std::sort(sources.begin(), sources.end(),
549 [](
const auto& left,
const auto& right) {
550 return left.path < right.path;
553 std::vector<std::uint64_t> content_hashes;
554 content_hashes.reserve(sources.size());
556 std::size_t newline_rank = 0;
557 for (std::size_t source_index = 0; source_index < sources.size();
559 const Source& source = sources[source_index];
560 if (source.path.empty() ||
561 !file_archive_detail::IsUtf8(source.path,
true)) {
562 throw std::invalid_argument(
563 "File-archive paths must be non-empty UTF-8 strings");
565 if (source_index != 0 && sources[source_index - 1].path == source.path) {
566 throw std::invalid_argument(
"File-archive paths must be unique");
568 if (source.type != FileArchiveEntryType::kRegular &&
569 source.type != FileArchiveEntryType::kSymlink) {
570 throw std::invalid_argument(
"Invalid file-archive entry type");
574 record.path_offset = paths.size();
575 record.path_size = source.path.size();
576 record.content_offset = logical_size_;
577 record.newline_rank_base = newline_rank;
578 record.mode = source.mode;
579 record.type = source.type;
580 paths.append(source.path);
583 std::size_t newlines = 0;
584 bool has_content =
false;
585 std::uint8_t last_byte = 0;
586 std::uint64_t content_hash = 14695981039346656037ULL;
587 read_source(source, [&](std::span<const std::byte> chunk) {
589 std::numeric_limits<std::size_t>::max() - logical_size_) {
590 throw std::length_error(
"File-archive content is too large");
592 logical_size_ += chunk.size();
594 histogram.
add(chunk);
595 for (
const std::byte
byte : chunk) {
596 const std::uint8_t value = std::to_integer<std::uint8_t>(
byte);
597 newlines += value ==
'\n' ? 1U : 0U;
600 content_hash ^= value;
601 content_hash *= 1099511628211ULL;
604 record.content_size = logical_size_ - record.content_offset;
605 record.is_text = utf8.
valid();
606 if (source.type == FileArchiveEntryType::kRegular && has_content) {
608 newlines +
static_cast<std::size_t
>(last_byte !=
'\n');
610 newline_rank += newlines;
611 records_.push_back(record);
612 content_hashes.push_back(content_hash);
615 paths_ = MakeStorage(
616 std::as_bytes(std::span<const char>(paths.data(), paths.size())));
618 const auto symbol_counts = histogram.
counts();
621 file_archive_detail::kByteAlphabetSize, symbol_counts,
623 for (std::size_t index = 0; index < sources.size(); ++index) {
624 std::size_t content_size = 0;
625 std::uint64_t content_hash = 14695981039346656037ULL;
626 read_source(sources[index], [&](std::span<const std::byte> chunk) {
627 if (content_size > records_[index].content_size ||
628 chunk.size() > records_[index].content_size - content_size) {
629 throw std::invalid_argument(
630 "File-archive source changed between build passes");
632 content_size += chunk.size();
633 const auto symbols = std::span<const std::uint8_t>(
634 reinterpret_cast<const std::uint8_t*
>(chunk.data()),
636 for (
const std::byte
byte : chunk) {
637 const std::uint8_t value = std::to_integer<std::uint8_t>(
byte);
638 content_hash ^= value;
639 content_hash *= 1099511628211ULL;
643 if (content_size != records_[index].content_size ||
644 content_hash != content_hashes[index]) {
645 throw std::invalid_argument(
646 "File-archive source changed between build passes");
656 void validate(
bool full)
const {
657 if (tree_->size() != logical_size_) {
658 throw std::invalid_argument(
"Invalid file-archive content size");
660 const std::size_t newline_count = tree_->rank(
'\n', logical_size_);
661 std::size_t expected_content_offset = 0;
662 std::size_t expected_newline_rank = 0;
663 std::string_view previous_path;
664 for (std::size_t index = 0; index < records_.size(); ++index) {
665 const auto& record = records_[index];
666 if (record.path_offset > paths_.size_bytes() ||
667 record.path_size > paths_.size_bytes() - record.path_offset ||
668 record.content_offset > logical_size_ ||
669 record.content_size > logical_size_ - record.content_offset ||
670 record.newline_rank_base > newline_count ||
671 record.line_count > newline_count - record.newline_rank_base + 1 ||
672 (record.type == FileArchiveEntryType::kSymlink &&
673 record.line_count != 0)) {
674 throw std::invalid_argument(
"Invalid file-archive entry bounds");
679 const std::string_view path = path_impl(record);
680 if (path.empty() || !file_archive_detail::IsUtf8(path,
true) ||
681 (index != 0 && previous_path >= path) ||
682 record.content_offset != expected_content_offset ||
683 record.newline_rank_base != expected_newline_rank) {
684 throw std::invalid_argument(
"Invalid file-archive entry metadata");
686 const std::vector<std::byte> content = this->
extract(index);
687 const bool is_text = file_archive_detail::IsUtf8(content,
true);
688 std::size_t newlines = 0;
689 for (
const std::byte
byte : content) {
690 newlines +=
byte == std::byte{
'\n'} ? 1U : 0U;
692 std::size_t line_count = 0;
693 if (record.type == FileArchiveEntryType::kRegular && !content.empty()) {
694 line_count = newlines +
static_cast<std::size_t
>(content.back() !=
697 if (record.is_text != is_text || record.line_count != line_count) {
698 throw std::invalid_argument(
"Invalid file-archive derived metadata");
700 previous_path = path;
701 expected_content_offset += content.size();
702 expected_newline_rank += newlines;
704 if (full && expected_content_offset != logical_size_) {
705 throw std::invalid_argument(
"Invalid file-archive content layout");
710 std::span<const file_archive_detail::FileRecord> records_impl()
const {
714 std::string_view path_impl(
715 const file_archive_detail::FileRecord& record)
const {
716 const std::span<const std::byte> bytes = paths_.as_bytes();
717 const std::string_view paths =
720 : std::string_view(
reinterpret_cast<const char*
>(bytes.data()),
722 return paths.substr(record.path_offset, record.path_size);
725 const WaveletTreeIndex<std::uint8_t, Storage>& tree_impl()
const {
729 std::size_t logical_size_impl()
const {
return logical_size_; }
733 std::size_t path_storage_size_impl()
const {
return paths_.size_bytes(); }
735 std::vector<file_archive_detail::FileRecord> records_;
737 std::optional<WaveletTreeIndex<std::uint8_t, Storage>> tree_;
738 std::size_t logical_size_ = 0;
Dependency-reduced histogram construction for byte streams.
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:191
std::size_t size() const
Return the number of archived entries.
Definition file_archive.h:199
std::size_t path_storage_bytes() const
Return serialized path-blob bytes.
Definition file_archive.h:216
std::optional< std::size_t > find(std::string_view path) const
Find an exact archive-relative path.
Definition file_archive.h:233
WaveletTreeBuildType build_type() const
Return the wavelet-tree construction strategy.
Definition file_archive.h:208
std::size_t metadata_bytes() const
Return framing, file-table, path, and alignment bytes.
Definition file_archive.h:221
FileArchiveEntry entry(std::size_t index) const
Return metadata for an entry.
Definition file_archive.h:251
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:284
std::size_t logical_size_bytes() const
Return the number of logical content bytes, excluding framing.
Definition file_archive.h:205
std::size_t file_table_bytes() const
Return serialized fixed-record bytes.
Definition file_archive.h:211
bool empty() const
Return whether the archive has no entries.
Definition file_archive.h:202
std::vector< std::byte > extract(std::size_t index) const
Reconstruct the complete byte content of one entry.
Definition file_archive.h:271
static constexpr std::uint32_t format_version()
Return the serialized file-archive format version.
Definition file_archive.h:194
Storage-backed byte-oriented file archive.
Definition file_archive.h:334
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:388
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:443
void serialize_impl(BinaryWriter &writer) const
Serialize one native, framed Pixie file archive.
Definition file_archive.h:398
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
Incremental 256-bin byte histogram with four update lanes.
Definition byte_histogram.h:16
std::array< std::size_t, 256 > counts() const
Return the combined symbol counts.
Definition byte_histogram.h:34
void add(std::span< const std::byte > bytes)
Add one contiguous chunk to the histogram.
Definition byte_histogram.h:19
Incrementally validate a byte stream as UTF-8.
Definition file_archive.h:86
void Consume(std::span< const std::byte > bytes)
Consume the next contiguous chunk of the byte stream.
Definition file_archive.h:92
bool valid() const
Return whether all consumed bytes form complete valid UTF-8.
Definition file_archive.h:140
Utf8Validator(bool reject_nul)
Construct a validator, optionally rejecting embedded NUL bytes.
Definition file_archive.h:89
FileArchiveEntryType
Kind of filesystem entry stored in a file archive.
Definition file_archive.h:29
FileArchiveIndex< ReadOnlyStorageView > FileArchiveView
Read-only file archive retaining serialized storage views.
Definition file_archive.h:746
FileArchiveIndex< AlignedStorage > FileArchive
Owning aligned-storage file archive.
Definition file_archive.h:743
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:50
Source entry used to construct an owning file archive.
Definition file_archive.h:35
Definition file_archive.h:73
WaveletTreeBuildType
Construction strategy for a wavelet-tree implementation.
Definition wavelet_tree.h:19