| // Copyright 2022 The Centipede Authors. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // https://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| #include "./common/blob_file.h" | |
| #include <algorithm> | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <cstring> | |
| #include <iterator> | |
| #include <memory> | |
| #include <string> | |
| #include <string_view> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/base/nullability.h" | |
| #include "absl/base/optimization.h" | |
| #include "absl/status/status.h" | |
| #include "absl/strings/str_format.h" | |
| #include "absl/strings/string_view.h" | |
| #include "absl/time/clock.h" | |
| #include "absl/time/time.h" | |
| #include "./common/defs.h" | |
| #include "./common/hash.h" | |
| #include "./common/logging.h" | |
| #include "./common/remote_file.h" | |
| #include "./common/status_macros.h" | |
| #ifndef CENTIPEDE_DISABLE_RIEGELI | |
| #include "riegeli/base/object.h" | |
| #include "riegeli/base/types.h" | |
| #include "riegeli/bytes/reader.h" | |
| #include "riegeli/bytes/writer.h" | |
| #include "riegeli/records/record_reader.h" | |
| #include "riegeli/records/record_writer.h" | |
| #endif // CENTIPEDE_DISABLE_RIEGELI | |
| namespace fuzztest::internal { | |
| namespace { | |
| constexpr size_t kMagicLen = 11; | |
| constexpr uint8_t kPackBegMagic[] = "-Centipede-"; | |
| constexpr uint8_t kPackEndMagic[] = "-edepitneC-"; | |
| static_assert(sizeof(kPackBegMagic) == kMagicLen + 1); | |
| static_assert(sizeof(kPackEndMagic) == kMagicLen + 1); | |
| // TODO(ussuri): Return more informative statuses, at least with the file path | |
| // included. That will require adjustments in the test: use | |
| // `testing::status::StatusIs` instead of direct `absl::Status` comparisons). | |
| // Simple implementations of `BlobFileReader` / `BlobFileWriter` based on | |
| // `PackBytesForAppendFile()` / `UnpackBytesFromAppendFile()`. | |
| // We expect to eventually replace this code with something more robust, | |
| // and efficient, e.g. possibly https://github.com/google/riegeli. | |
| // But the current implementation is fully functional. | |
| class SimpleBlobFileReader : public BlobFileReader { | |
| public: | |
| ~SimpleBlobFileReader() override { | |
| if (file_ && !closed_) { | |
| // Virtual resolution is off in dtors, so use a specific Close(). | |
| FUZZTEST_CHECK_OK(SimpleBlobFileReader::Close()); | |
| } | |
| } | |
| absl::Status Open(std::string_view path) override { | |
| if (closed_) return absl::FailedPreconditionError("already closed"); | |
| if (file_) return absl::FailedPreconditionError("already open"); | |
| ASSIGN_OR_RETURN_IF_NOT_OK(file_, RemoteFileOpen(path, "r")); | |
| if (file_ == nullptr) return absl::UnknownError("can't open file"); | |
| // Read the entire file at once. | |
| // It may be useful to read the file in chunks, but if we are going | |
| // to migrate to something else, it's not important here. | |
| ByteArray raw_bytes; | |
| RETURN_IF_NOT_OK(RemoteFileRead(file_, raw_bytes)); | |
| RETURN_IF_NOT_OK( | |
| RemoteFileClose(file_)); // close the file here, we won't need it. | |
| UnpackBytesFromAppendFile(raw_bytes, &unpacked_blobs_); | |
| return absl::OkStatus(); | |
| } | |
| absl::Status Read(ByteSpan &blob) override { | |
| if (closed_) return absl::FailedPreconditionError("already closed"); | |
| if (!file_) return absl::FailedPreconditionError("was not open"); | |
| if (next_to_read_blob_index_ == unpacked_blobs_.size()) | |
| return absl::OutOfRangeError("no more blobs"); | |
| if (next_to_read_blob_index_ != 0) // Clear the previous blob to save RAM. | |
| unpacked_blobs_[next_to_read_blob_index_ - 1].clear(); | |
| blob = ByteSpan(unpacked_blobs_[next_to_read_blob_index_]); | |
| ++next_to_read_blob_index_; | |
| return absl::OkStatus(); | |
| } | |
| // Closes the file (it must be open). | |
| absl::Status Close() override { | |
| if (closed_) return absl::FailedPreconditionError("already closed"); | |
| if (!file_) return absl::FailedPreconditionError("was not open"); | |
| closed_ = true; | |
| // Nothing to do here, we've already closed the underlying file (in Open()). | |
| return absl::OkStatus(); | |
| } | |
| private: | |
| RemoteFile *file_ = nullptr; | |
| bool closed_ = false; | |
| std::vector<ByteArray> unpacked_blobs_; | |
| size_t next_to_read_blob_index_ = 0; | |
| }; | |
| // See SimpleBlobFileReader. | |
| class SimpleBlobFileWriter : public BlobFileWriter { | |
| public: | |
| ~SimpleBlobFileWriter() override { | |
| if (file_ && !closed_) { | |
| // Virtual resolution is off in dtors, so use a specific Close(). | |
| FUZZTEST_CHECK_OK(SimpleBlobFileWriter::Close()); | |
| } | |
| } | |
| absl::Status Open(std::string_view path, std::string_view mode) override { | |
| FUZZTEST_CHECK(mode == "w" || mode == "a") << VV(mode); | |
| if (closed_) return absl::FailedPreconditionError("already closed"); | |
| if (file_) return absl::FailedPreconditionError("already open"); | |
| ASSIGN_OR_RETURN_IF_NOT_OK(file_, RemoteFileOpen(path, mode.data())); | |
| if (file_ == nullptr) return absl::UnknownError("can't open file"); | |
| RETURN_IF_NOT_OK(RemoteFileSetWriteBufferSize(file_, kMaxBufferedBytes)); | |
| return absl::OkStatus(); | |
| } | |
| absl::Status Write(ByteSpan blob) override { | |
| if (closed_) return absl::FailedPreconditionError("already closed"); | |
| if (!file_) return absl::FailedPreconditionError("was not open"); | |
| ByteArray packed = PackBytesForAppendFile(blob); | |
| RETURN_IF_NOT_OK(RemoteFileAppend(file_, packed)); | |
| RETURN_IF_NOT_OK(RemoteFileFlush(file_)); | |
| return absl::OkStatus(); | |
| } | |
| absl::Status Close() override { | |
| if (closed_) return absl::FailedPreconditionError("already closed"); | |
| if (!file_) return absl::FailedPreconditionError("was not open"); | |
| closed_ = true; | |
| RETURN_IF_NOT_OK(RemoteFileClose(file_)); | |
| return absl::OkStatus(); | |
| } | |
| private: | |
| static constexpr uint64_t kMB = 1024UL * 1024UL; | |
| static constexpr uint64_t kMaxBufferedBytes = 100 * kMB; | |
| RemoteFile *file_ = nullptr; | |
| bool closed_ = false; | |
| }; | |
| // Implementation of `BlobFileReader` that can read files written in legacy or | |
| // Riegeli (https://github.com/google/riegeli) format. | |
| class DefaultBlobFileReader : public BlobFileReader { | |
| public: | |
| ~DefaultBlobFileReader() override { | |
| // Virtual resolution is off in dtors, so use a specific Close(). | |
| FUZZTEST_CHECK_OK(DefaultBlobFileReader::Close()); | |
| } | |
| absl::Status Open(std::string_view path) override { | |
| if (absl::Status s = Close(); !s.ok()) return s; | |
| #ifndef CENTIPEDE_DISABLE_RIEGELI | |
| ASSIGN_OR_RETURN_IF_NOT_OK(std::unique_ptr<riegeli::Reader> new_reader, | |
| CreateRiegeliFileReader(path)); | |
| riegeli_reader_.Reset(std::move(new_reader)); | |
| if (ABSL_PREDICT_TRUE(riegeli_reader_.CheckFileFormat())) { | |
| // File could be opened and is in the Riegeli format. | |
| return absl::OkStatus(); | |
| } | |
| if (ABSL_PREDICT_FALSE(!riegeli_reader_.src()->ok())) { | |
| // File could not be opened. | |
| return riegeli_reader_.src()->status(); | |
| } | |
| // File could be opened but is not in the Riegeli format. | |
| riegeli_reader_.Reset(riegeli::kClosed); | |
| #endif // CENTIPEDE_DISABLE_RIEGELI | |
| // Because we fall back to a legacy reader, we can't distinguish between | |
| // an empty blob file and a file that is not a blob file. Once this behavior | |
| // changes (e.g., we get rid of the legacy reader), consider b/349115475 and | |
| // track down places that would benefit from this distinction. | |
| legacy_reader_ = std::make_unique<SimpleBlobFileReader>(); | |
| if (absl::Status s = legacy_reader_->Open(path); !s.ok()) { | |
| legacy_reader_ = nullptr; | |
| return s; | |
| } | |
| return absl::OkStatus(); | |
| } | |
| absl::Status Read(ByteSpan &blob) override { | |
| #ifdef CENTIPEDE_DISABLE_RIEGELI | |
| if (legacy_reader_) | |
| return legacy_reader_->Read(blob); | |
| else | |
| return absl::FailedPreconditionError("no reader open"); | |
| #else | |
| if (ABSL_PREDICT_FALSE(legacy_reader_)) return legacy_reader_->Read(blob); | |
| absl::string_view record; | |
| if (!riegeli_reader_.ReadRecord(record)) { | |
| if (riegeli_reader_.ok()) | |
| return absl::OutOfRangeError("no more blobs"); | |
| else | |
| return riegeli_reader_.status(); | |
| } | |
| blob = AsByteSpan(record); | |
| return absl::OkStatus(); | |
| #endif // CENTIPEDE_DISABLE_RIEGELI | |
| } | |
| absl::Status Close() override { | |
| #ifdef CENTIPEDE_DISABLE_RIEGELI | |
| legacy_reader_ = nullptr; | |
| return absl::OkStatus(); | |
| #else | |
| if (ABSL_PREDICT_FALSE(legacy_reader_)) { | |
| legacy_reader_ = nullptr; | |
| return absl::OkStatus(); | |
| } | |
| // `riegeli_reader_` not being ok will result in `Close()` failing, but its | |
| // non-ok status stems from a previously failed operation in an `Open()` or | |
| // `Read()` call whose errors were already propagated there - these are | |
| // therefore filtered out here. | |
| // `Close()` failing on an ok reader is due to the file being in an invalid | |
| // state that primarily arises from an incomplete concurrent write (which | |
| // can happen even with every write being flushed - see comment in | |
| // `RiegeliWriter::Write()`) - these are therefore logged but not propagated | |
| // as failures. | |
| // TODO(b/313706444): Reconsider error handling after experiments. | |
| // TODO(b/310701588): Try adding a test for this. | |
| if (riegeli_reader_.ok() && !riegeli_reader_.Close()) { | |
| FUZZTEST_LOG(WARNING) << "Ignoring errors while closing Riegeli file: " | |
| << riegeli_reader_.status(); | |
| } | |
| // Any non-ok status of `riegeli_reader_` persists for subsequent | |
| // operations; therefore, re-initialize it to a closed ok state. | |
| riegeli_reader_.Reset(riegeli::kClosed); | |
| return absl::OkStatus(); | |
| #endif // CENTIPEDE_DISABLE_RIEGELI | |
| } | |
| private: | |
| std::unique_ptr<SimpleBlobFileReader> legacy_reader_ = nullptr; | |
| #ifndef CENTIPEDE_DISABLE_RIEGELI | |
| riegeli::RecordReader<std::unique_ptr<riegeli::Reader>> riegeli_reader_{ | |
| riegeli::kClosed}; | |
| #endif // CENTIPEDE_DISABLE_RIEGELI | |
| }; | |
| #ifndef CENTIPEDE_DISABLE_RIEGELI | |
| // Implementation of `BlobFileWriter` using Riegeli | |
| // (https://github.com/google/riegeli). | |
| class RiegeliWriter : public BlobFileWriter { | |
| public: | |
| ~RiegeliWriter() override { | |
| // Virtual resolution is off in dtors, so use a specific Close(). | |
| FUZZTEST_CHECK_OK(RiegeliWriter::Close()); | |
| } | |
| absl::Status Open(std::string_view path, std::string_view mode) override { | |
| FUZZTEST_CHECK(mode == "w" || mode == "a") << VV(mode); | |
| if (absl::Status s = Close(); !s.ok()) return s; | |
| const auto kWriterOpts = riegeli::RecordWriterBase::Options{} | |
| .set_chunk_size(kMaxBufferedBytes) | |
| .set_zstd(); | |
| ASSIGN_OR_RETURN_IF_NOT_OK(std::unique_ptr<riegeli::Writer> new_writer, | |
| CreateRiegeliFileWriter(path, mode == "a")); | |
| writer_.Reset(std::move(new_writer), kWriterOpts); | |
| RETURN_IF_NOT_OK(writer_.status()); | |
| path_ = path; | |
| opened_at_ = absl::Now(); | |
| flushed_at_ = absl::Now(); | |
| written_blobs_ = 0; | |
| written_bytes_ = 0; | |
| buffered_blobs_ = 0; | |
| buffered_bytes_ = 0; | |
| return absl::OkStatus(); | |
| } | |
| absl::Status Write(ByteSpan blob) override { | |
| std::string_view blob_view = AsStringView(blob); | |
| const auto now = absl::Now(); | |
| if (!PreWriteFlush(blob.size())) return writer_.status(); | |
| if (!writer_.WriteRecord( | |
| absl::string_view{blob_view.data(), blob_view.size()})) { | |
| return writer_.status(); | |
| } | |
| if (!PostWriteFlush(blob.size())) return writer_.status(); | |
| write_duration_ += absl::Now() - now; | |
| if (written_blobs_ + buffered_blobs_ % 10000 == 0) | |
| FUZZTEST_VLOG(10) << "Current stats: " << StatsString(); | |
| return absl::OkStatus(); | |
| } | |
| absl::Status Close() override { | |
| // Writer already being in a bad state will result in close failure but | |
| // those errors have already been reported. | |
| if (!writer_.ok()) { | |
| writer_.Reset(riegeli::kClosed); | |
| return absl::OkStatus(); | |
| } | |
| if (!writer_.Close()) return writer_.status(); | |
| flushed_at_ = absl::Now(); | |
| written_blobs_ += buffered_blobs_; | |
| written_bytes_ += buffered_bytes_; | |
| buffered_blobs_ = 0; | |
| buffered_bytes_ = 0; | |
| FUZZTEST_VLOG(1) << "Final stats: " << StatsString(); | |
| return absl::OkStatus(); | |
| } | |
| private: | |
| static constexpr uint64_t kMB = 1024UL * 1024UL; | |
| // Buffering/flushing control settings. The defaults were chosen based on | |
| // experimental runs and intuition with the idea to balance good buffering | |
| // performance and a steady stream of blobs being committed to the file, so | |
| // external readers see updates frequently enough. | |
| // TODO(ussuri): Once Riegeli is the sole blob writer, maybe expose these | |
| // as Centipede flags and plumb them through `DefaultBlobFileWriterFactory()`. | |
| static constexpr uint64_t kMaxBufferedBlobs = 10000; | |
| // Riegeli's default is 1 MB. | |
| static constexpr uint64_t kMaxBufferedBytes = 100 * kMB; | |
| static constexpr absl::Duration kMaxFlushInterval = absl::Minutes(1); | |
| // For each record, Riegeli also writes its offset in the stream to the file. | |
| static constexpr size_t kRiegeliPerRecordMetadataSize = sizeof(uint64_t); | |
| // Riegeli's automatic flushing occurs when it accumulates over | |
| // `Options::chunk_size()` of data, not on record boundaries. Our outputs | |
| // are continuously consumed by external readers, so we can't tolerate | |
| // partially written records at the end of a file. Therefore, we explicitly | |
| // flush when we're just about to cross the chunk size boundary, or if the | |
| // client writes infrequently, or if the size of records is small relative | |
| // to the chunk size. The latter two cases are to make the data visible to | |
| // readers earlier; however, note that the compression performance may | |
| // suffer. | |
| bool PreWriteFlush(size_t blob_size) { | |
| const auto record_size = blob_size + kRiegeliPerRecordMetadataSize; | |
| const std::string_view flush_reason = | |
| (buffered_blobs_ > kMaxBufferedBlobs) ? "blobs" | |
| : (buffered_bytes_ + record_size > kMaxBufferedBytes) ? "bytes" | |
| : (absl::Now() - flushed_at_ > kMaxFlushInterval) ? "time" | |
| : ""; | |
| if (!flush_reason.empty()) { | |
| FUZZTEST_VLOG(20) << "Flushing b/c " << flush_reason << ": " | |
| << StatsString(); | |
| if (!writer_.Flush(riegeli::FlushType::kFromMachine)) return false; | |
| flushed_at_ = absl::Now(); | |
| written_blobs_ += buffered_blobs_; | |
| written_bytes_ += buffered_bytes_; | |
| buffered_blobs_ = 0; | |
| buffered_bytes_ = 0; | |
| } | |
| return true; | |
| } | |
| // In the rare case where the current blob itself exceeds the chunk size, | |
| // `Write()` will auto-flush a portion of it to the file, but the remainder | |
| // will remain in the buffer, so we need to force-flush it to maintain file | |
| // completeness. | |
| bool PostWriteFlush(size_t blob_size) { | |
| const auto record_size = blob_size + kRiegeliPerRecordMetadataSize; | |
| if (record_size >= kMaxBufferedBytes) { | |
| FUZZTEST_VLOG(20) << "Post-write flushing b/c blob size: " | |
| << StatsString(); | |
| if (!writer_.Flush(riegeli::FlushType::kFromMachine)) return false; | |
| flushed_at_ = absl::Now(); | |
| written_blobs_ += 1; | |
| written_bytes_ += record_size; | |
| buffered_blobs_ = 0; | |
| buffered_bytes_ = 0; | |
| } else { | |
| buffered_blobs_ += 1; | |
| buffered_bytes_ += record_size; | |
| } | |
| return true; | |
| } | |
| // Returns a debug string with the effective writing rate for the current file | |
| // path. The effective rate is measured as a ratio of the total bytes passed | |
| // to `Write()` and the elapsed time from the file opening till now. | |
| std::string StatsString() const { | |
| const auto opened_secs = absl::ToDoubleSeconds(absl::Now() - opened_at_); | |
| const auto write_secs = absl::ToDoubleSeconds(write_duration_); | |
| const auto total_bytes = written_bytes_ + buffered_bytes_; | |
| const auto throughput = | |
| write_secs > 0.0 ? (1.0 * total_bytes / write_secs) : 0; | |
| const auto file_size = writer_.EstimatedSize(); | |
| const auto compression = | |
| file_size > 0 ? (1.0 * written_bytes_ / file_size) : 0; | |
| std::string stats = absl::StrFormat( | |
| "written/buffered blobs: %llu/%llu, written/buffered bytes: %llu/%llu, " | |
| "opened: %f sec, writing: %f sec, throughput: %.0f B/sec, " | |
| "file size: %llu, compression: %.1f, path: %s", | |
| written_blobs_, buffered_blobs_, written_bytes_, buffered_bytes_, | |
| opened_secs, write_secs, throughput, file_size, compression, path_); | |
| return stats; | |
| } | |
| // The underlying Riegeli writer. | |
| riegeli::RecordWriter<std::unique_ptr<riegeli::Writer>> writer_{ | |
| riegeli::kClosed}; | |
| // Buffering/flushing control. | |
| absl::Time flushed_at_ = absl::InfiniteFuture(); | |
| uint64_t buffered_blobs_ = 0; | |
| uint64_t buffered_bytes_ = 0; | |
| // Telemetry. | |
| std::string path_; | |
| absl::Time opened_at_ = absl::InfiniteFuture(); | |
| absl::Duration write_duration_ = absl::ZeroDuration(); | |
| uint64_t written_blobs_ = 0; | |
| uint64_t written_bytes_ = 0; | |
| }; | |
| #endif // CENTIPEDE_DISABLE_RIEGELI | |
| } // namespace | |
| // Pack 'data' such that it can be appended to a file and later extracted: | |
| // * kPackBegMagic | |
| // * hash(data) | |
| // * data.size() (8 bytes) | |
| // * data itself | |
| // * kPackEndMagic | |
| // Storing the magics and the hash is a precaution against partial writes. | |
| // UnpackBytesFromAppendFile looks for the kPackBegMagic and so | |
| // it will ignore any partially-written data. | |
| // | |
| // This is simple and efficient, but I wonder if there is a ready-to-use | |
| // standard open-source alternative. Or should we just use tar? | |
| ByteArray PackBytesForAppendFile(ByteSpan blob) { | |
| ByteArray res; | |
| auto hash = Hash(blob); | |
| FUZZTEST_CHECK_EQ(hash.size(), kHashLen); | |
| size_t size = blob.size(); | |
| uint8_t size_bytes[sizeof(size)]; | |
| std::memcpy(size_bytes, &size, sizeof(size)); | |
| res.insert(res.end(), std::begin(kPackBegMagic), | |
| std::begin(kPackBegMagic) + kMagicLen); | |
| res.insert(res.end(), hash.begin(), hash.end()); | |
| res.insert(res.end(), std::begin(size_bytes), std::end(size_bytes)); | |
| res.insert(res.end(), blob.begin(), blob.end()); | |
| res.insert(res.end(), std::begin(kPackEndMagic), | |
| std::begin(kPackEndMagic) + kMagicLen); | |
| return res; | |
| } | |
| // Reverse to a sequence of PackBytesForAppendFile() appended to each other. | |
| void UnpackBytesFromAppendFile(const ByteArray &packed_data, | |
| std::vector<ByteArray> *absl_nullable unpacked, | |
| std::vector<std::string> *absl_nullable hashes) { | |
| auto pos = packed_data.cbegin(); | |
| while (true) { | |
| pos = std::search(pos, packed_data.end(), &kPackBegMagic[0], | |
| &kPackBegMagic[kMagicLen]); | |
| if (pos == packed_data.end()) return; | |
| pos += kMagicLen; | |
| if (packed_data.end() - pos < kHashLen) return; | |
| std::string hash(pos, pos + kHashLen); | |
| pos += kHashLen; | |
| size_t size = 0; | |
| if (packed_data.end() - pos < sizeof(size)) return; | |
| std::memcpy(&size, &*pos, sizeof(size)); | |
| pos += sizeof(size); | |
| if (packed_data.end() - pos < size) return; | |
| ByteArray ba(pos, pos + size); | |
| pos += size; | |
| if (packed_data.end() - pos < kMagicLen) return; | |
| if (std::memcmp(&*pos, kPackEndMagic, kMagicLen) != 0) continue; | |
| pos += kMagicLen; | |
| if (hash != Hash(ba)) continue; | |
| if (unpacked) unpacked->push_back(std::move(ba)); | |
| if (hashes) hashes->push_back(std::move(hash)); | |
| } | |
| } | |
| std::unique_ptr<BlobFileReader> DefaultBlobFileReaderFactory() { | |
| return std::make_unique<DefaultBlobFileReader>(); | |
| } | |
| std::unique_ptr<BlobFileWriter> DefaultBlobFileWriterFactory(bool riegeli) { | |
| if (riegeli) | |
| #ifdef CENTIPEDE_DISABLE_RIEGELI | |
| FUZZTEST_LOG(FATAL) | |
| << "Riegeli unavailable: built with --use_riegeli set to false."; | |
| #else | |
| return std::make_unique<RiegeliWriter>(); | |
| #endif // CENTIPEDE_DISABLE_RIEGELI | |
| else | |
| return std::make_unique<SimpleBlobFileWriter>(); | |
| } | |
| } // namespace fuzztest::internal |