Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 32306084 authored by Daniel Rosenberg's avatar Daniel Rosenberg Committed by Gerrit Code Review
Browse files

Merge changes I3339d552,I126e1583,Iccc6580a

* changes:
  Add GetLastLabel and InitializeAppend
  Switch up Cow Format to be resumable
  reland: Rename Flush to Finalize
parents 7d55df28 3d17cb9a
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -149,6 +149,7 @@ cc_defaults {
        "cow_decompress.cpp",
        "cow_reader.cpp",
        "cow_writer.cpp",
        "cow_format.cpp",
    ],
}

+227 −10
Original line number Diff line number Diff line
@@ -70,19 +70,21 @@ TEST_F(CowTest, ReadWrite) {
    ASSERT_TRUE(writer.AddCopy(10, 20));
    ASSERT_TRUE(writer.AddRawBlocks(50, data.data(), data.size()));
    ASSERT_TRUE(writer.AddZeroBlocks(51, 2));
    ASSERT_TRUE(writer.Flush());
    ASSERT_TRUE(writer.Finalize());

    ASSERT_EQ(lseek(cow_->fd, 0, SEEK_SET), 0);

    CowReader reader;
    CowHeader header;
    CowFooter footer;
    ASSERT_TRUE(reader.Parse(cow_->fd));
    ASSERT_TRUE(reader.GetHeader(&header));
    ASSERT_TRUE(reader.GetFooter(&footer));
    ASSERT_EQ(header.magic, kCowMagicNumber);
    ASSERT_EQ(header.major_version, kCowVersionMajor);
    ASSERT_EQ(header.minor_version, kCowVersionMinor);
    ASSERT_EQ(header.block_size, options.block_size);
    ASSERT_EQ(header.num_ops, 4);
    ASSERT_EQ(footer.op.num_ops, 4);

    auto iter = reader.GetOpIter();
    ASSERT_NE(iter, nullptr);
@@ -105,7 +107,6 @@ TEST_F(CowTest, ReadWrite) {
    ASSERT_EQ(op->compression, kCowCompressNone);
    ASSERT_EQ(op->data_length, 4096);
    ASSERT_EQ(op->new_block, 50);
    ASSERT_EQ(op->source, 106);
    ASSERT_TRUE(reader.ReadData(*op, &sink));
    ASSERT_EQ(sink.stream(), data);

@@ -145,7 +146,7 @@ TEST_F(CowTest, CompressGz) {
    data.resize(options.block_size, '\0');

    ASSERT_TRUE(writer.AddRawBlocks(50, data.data(), data.size()));
    ASSERT_TRUE(writer.Flush());
    ASSERT_TRUE(writer.Finalize());

    ASSERT_EQ(lseek(cow_->fd, 0, SEEK_SET), 0);

@@ -163,7 +164,6 @@ TEST_F(CowTest, CompressGz) {
    ASSERT_EQ(op->compression, kCowCompressGz);
    ASSERT_EQ(op->data_length, 56);  // compressed!
    ASSERT_EQ(op->new_block, 50);
    ASSERT_EQ(op->source, 106);
    ASSERT_TRUE(reader.ReadData(*op, &sink));
    ASSERT_EQ(sink.stream(), data);

@@ -182,7 +182,7 @@ TEST_F(CowTest, CompressTwoBlocks) {
    data.resize(options.block_size * 2, '\0');

    ASSERT_TRUE(writer.AddRawBlocks(50, data.data(), data.size()));
    ASSERT_TRUE(writer.Flush());
    ASSERT_TRUE(writer.Finalize());

    ASSERT_EQ(lseek(cow_->fd, 0, SEEK_SET), 0);

@@ -224,7 +224,7 @@ TEST_P(CompressionTest, HorribleSink) {
    data.resize(options.block_size, '\0');

    ASSERT_TRUE(writer.AddRawBlocks(50, data.data(), data.size()));
    ASSERT_TRUE(writer.Flush());
    ASSERT_TRUE(writer.Finalize());

    ASSERT_EQ(lseek(cow_->fd, 0, SEEK_SET), 0);

@@ -259,7 +259,7 @@ TEST_F(CowTest, GetSize) {
    ASSERT_TRUE(writer.AddRawBlocks(50, data.data(), data.size()));
    ASSERT_TRUE(writer.AddZeroBlocks(51, 2));
    auto size_before = writer.GetCowSize();
    ASSERT_TRUE(writer.Flush());
    ASSERT_TRUE(writer.Finalize());
    auto size_after = writer.GetCowSize();
    ASSERT_EQ(size_before, size_after);
    struct stat buf;
@@ -272,6 +272,7 @@ TEST_F(CowTest, GetSize) {
}

TEST_F(CowTest, Append) {
    cow_->DoNotRemove();
    CowOptions options;
    auto writer = std::make_unique<CowWriter>(options);
    ASSERT_TRUE(writer->Initialize(cow_->fd));
@@ -279,7 +280,7 @@ TEST_F(CowTest, Append) {
    std::string data = "This is some data, believe it";
    data.resize(options.block_size, '\0');
    ASSERT_TRUE(writer->AddRawBlocks(50, data.data(), data.size()));
    ASSERT_TRUE(writer->Flush());
    ASSERT_TRUE(writer->Finalize());

    ASSERT_EQ(lseek(cow_->fd, 0, SEEK_SET), 0);

@@ -289,7 +290,7 @@ TEST_F(CowTest, Append) {
    std::string data2 = "More data!";
    data2.resize(options.block_size, '\0');
    ASSERT_TRUE(writer->AddRawBlocks(51, data2.data(), data2.size()));
    ASSERT_TRUE(writer->Flush());
    ASSERT_TRUE(writer->Finalize());

    ASSERT_EQ(lseek(cow_->fd, 0, SEEK_SET), 0);

@@ -299,7 +300,9 @@ TEST_F(CowTest, Append) {

    // Read back both operations.
    CowReader reader;
    uint64_t label;
    ASSERT_TRUE(reader.Parse(cow_->fd));
    ASSERT_FALSE(reader.GetLastLabel(&label));

    StringSink sink;

@@ -325,6 +328,220 @@ TEST_F(CowTest, Append) {
    ASSERT_TRUE(iter->Done());
}

TEST_F(CowTest, AppendCorrupted) {
    CowOptions options;
    auto writer = std::make_unique<CowWriter>(options);
    ASSERT_TRUE(writer->Initialize(cow_->fd));

    std::string data = "This is some data, believe it";
    data.resize(options.block_size, '\0');
    ASSERT_TRUE(writer->AddLabel(0));
    ASSERT_TRUE(writer->AddRawBlocks(50, data.data(), data.size()));
    ASSERT_TRUE(writer->AddLabel(1));
    ASSERT_TRUE(writer->AddZeroBlocks(50, 1));
    ASSERT_TRUE(writer->Finalize());
    // Drop the tail end of the header. Last entry may be corrupted.
    ftruncate(cow_->fd, writer->GetCowSize() - 5);

    ASSERT_EQ(lseek(cow_->fd, 0, SEEK_SET), 0);

    writer = std::make_unique<CowWriter>(options);
    ASSERT_TRUE(writer->Initialize(cow_->fd, CowWriter::OpenMode::APPEND));

    ASSERT_TRUE(writer->AddLabel(2));
    ASSERT_TRUE(writer->AddZeroBlocks(50, 1));

    std::string data2 = "More data!";
    data2.resize(options.block_size, '\0');
    ASSERT_TRUE(writer->AddLabel(3));
    ASSERT_TRUE(writer->AddRawBlocks(51, data2.data(), data2.size()));
    ASSERT_TRUE(writer->Finalize());

    ASSERT_EQ(lseek(cow_->fd, 0, SEEK_SET), 0);

    struct stat buf;
    ASSERT_EQ(fstat(cow_->fd, &buf), 0);
    ASSERT_EQ(buf.st_size, writer->GetCowSize());

    // Read back all three operations.
    CowReader reader;
    ASSERT_TRUE(reader.Parse(cow_->fd));

    StringSink sink;

    auto iter = reader.GetOpIter();
    ASSERT_NE(iter, nullptr);

    ASSERT_FALSE(iter->Done());
    auto op = &iter->Get();
    ASSERT_EQ(op->type, kCowLabelOp);
    ASSERT_EQ(op->source, 0);

    iter->Next();

    ASSERT_FALSE(iter->Done());
    op = &iter->Get();
    ASSERT_EQ(op->type, kCowReplaceOp);
    ASSERT_TRUE(reader.ReadData(*op, &sink));
    ASSERT_EQ(sink.stream(), data);

    iter->Next();
    sink.Reset();

    ASSERT_FALSE(iter->Done());
    op = &iter->Get();
    ASSERT_EQ(op->type, kCowLabelOp);
    ASSERT_EQ(op->source, 2);

    iter->Next();

    ASSERT_FALSE(iter->Done());
    op = &iter->Get();
    ASSERT_EQ(op->type, kCowZeroOp);

    iter->Next();

    ASSERT_FALSE(iter->Done());
    op = &iter->Get();
    ASSERT_EQ(op->type, kCowLabelOp);
    ASSERT_EQ(op->source, 3);

    iter->Next();

    ASSERT_FALSE(iter->Done());
    op = &iter->Get();
    ASSERT_EQ(op->type, kCowReplaceOp);
    ASSERT_TRUE(reader.ReadData(*op, &sink));
    ASSERT_EQ(sink.stream(), data2);

    iter->Next();
    ASSERT_TRUE(iter->Done());
}

TEST_F(CowTest, AppendExtendedCorrupted) {
    CowOptions options;
    auto writer = std::make_unique<CowWriter>(options);
    ASSERT_TRUE(writer->Initialize(cow_->fd));

    ASSERT_TRUE(writer->AddLabel(5));
    ASSERT_TRUE(writer->AddLabel(6));

    std::string data = "This is some data, believe it";
    data.resize(options.block_size * 2, '\0');
    ASSERT_TRUE(writer->AddRawBlocks(50, data.data(), data.size()));

    // fail to write the footer

    ASSERT_EQ(lseek(cow_->fd, 0, SEEK_SET), 0);

    // Get the last known good label
    CowReader label_reader;
    uint64_t label;
    ASSERT_TRUE(label_reader.Parse(cow_->fd));
    ASSERT_TRUE(label_reader.GetLastLabel(&label));
    ASSERT_EQ(label, 5);

    ASSERT_EQ(lseek(cow_->fd, 0, SEEK_SET), 0);

    writer = std::make_unique<CowWriter>(options);
    ASSERT_TRUE(writer->Initialize(cow_->fd, CowWriter::OpenMode::APPEND));

    ASSERT_TRUE(writer->Finalize());

    struct stat buf;
    ASSERT_EQ(fstat(cow_->fd, &buf), 0);
    ASSERT_EQ(buf.st_size, writer->GetCowSize());

    // Read back all three operations.
    CowReader reader;
    ASSERT_TRUE(reader.Parse(cow_->fd));

    StringSink sink;

    auto iter = reader.GetOpIter();
    ASSERT_NE(iter, nullptr);

    ASSERT_FALSE(iter->Done());
    auto op = &iter->Get();
    ASSERT_EQ(op->type, kCowLabelOp);
    ASSERT_EQ(op->source, 5);

    iter->Next();
    ASSERT_TRUE(iter->Done());
}

TEST_F(CowTest, AppendbyLabel) {
    CowOptions options;
    auto writer = std::make_unique<CowWriter>(options);
    ASSERT_TRUE(writer->Initialize(cow_->fd));

    ASSERT_TRUE(writer->AddLabel(4));

    ASSERT_TRUE(writer->AddLabel(5));
    std::string data = "This is some data, believe it";
    data.resize(options.block_size * 2, '\0');
    ASSERT_TRUE(writer->AddRawBlocks(50, data.data(), data.size()));

    ASSERT_TRUE(writer->AddLabel(6));
    ASSERT_TRUE(writer->AddZeroBlocks(50, 2));

    ASSERT_EQ(lseek(cow_->fd, 0, SEEK_SET), 0);

    writer = std::make_unique<CowWriter>(options);
    ASSERT_FALSE(writer->InitializeAppend(cow_->fd, 12));
    ASSERT_TRUE(writer->InitializeAppend(cow_->fd, 5));

    // This should drop label 6
    ASSERT_TRUE(writer->Finalize());

    struct stat buf;
    ASSERT_EQ(fstat(cow_->fd, &buf), 0);
    ASSERT_EQ(buf.st_size, writer->GetCowSize());

    // Read back all ops
    CowReader reader;
    ASSERT_TRUE(reader.Parse(cow_->fd));

    StringSink sink;

    auto iter = reader.GetOpIter();
    ASSERT_NE(iter, nullptr);

    ASSERT_FALSE(iter->Done());
    auto op = &iter->Get();
    ASSERT_EQ(op->type, kCowLabelOp);
    ASSERT_EQ(op->source, 4);

    iter->Next();

    ASSERT_FALSE(iter->Done());
    op = &iter->Get();
    ASSERT_EQ(op->type, kCowLabelOp);
    ASSERT_EQ(op->source, 5);

    iter->Next();

    ASSERT_FALSE(iter->Done());
    op = &iter->Get();
    ASSERT_EQ(op->type, kCowReplaceOp);
    ASSERT_TRUE(reader.ReadData(*op, &sink));
    ASSERT_EQ(sink.stream(), data.substr(0, options.block_size));

    iter->Next();
    sink.Reset();

    ASSERT_FALSE(iter->Done());
    op = &iter->Get();
    ASSERT_EQ(op->type, kCowReplaceOp);
    ASSERT_TRUE(reader.ReadData(*op, &sink));
    ASSERT_EQ(sink.stream(), data.substr(options.block_size, 2 * options.block_size));

    iter->Next();
    sink.Reset();

    ASSERT_TRUE(iter->Done());
}

}  // namespace snapshot
}  // namespace android

+63 −0
Original line number Diff line number Diff line
//
// Copyright (C) 2020 The Android Open Source Project
//
// 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
//
//      http://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 <libsnapshot/cow_format.h>
#include <sys/types.h>
#include <unistd.h>

#include <android-base/logging.h>

namespace android {
namespace snapshot {

std::ostream& operator<<(std::ostream& os, CowOperation const& op) {
    os << "CowOperation(type:";
    if (op.type == kCowCopyOp)
        os << "kCowCopyOp,    ";
    else if (op.type == kCowReplaceOp)
        os << "kCowReplaceOp, ";
    else if (op.type == kCowZeroOp)
        os << "kZeroOp,       ";
    else if (op.type == kCowFooterOp)
        os << "kCowFooterOp,  ";
    else if (op.type == kCowLabelOp)
        os << "kCowLabelOp,   ";
    else
        os << (int)op.type << "?,";
    os << "compression:";
    if (op.compression == kCowCompressNone)
        os << "kCowCompressNone,   ";
    else if (op.compression == kCowCompressGz)
        os << "kCowCompressGz,     ";
    else if (op.compression == kCowCompressBrotli)
        os << "kCowCompressBrotli, ";
    else
        os << (int)op.compression << "?, ";
    os << "data_length:" << op.data_length << ",\t";
    os << "new_block:" << op.new_block << ",\t";
    os << "source:" << op.source << ")";
    return os;
}

int64_t GetNextOpOffset(const CowOperation& op) {
    if (op.type == kCowReplaceOp)
        return op.data_length;
    else
        return 0;
}

}  // namespace snapshot
}  // namespace android
+105 −61
Original line number Diff line number Diff line
@@ -18,17 +18,26 @@
#include <unistd.h>

#include <limits>
#include <vector>

#include <android-base/file.h>
#include <android-base/logging.h>
#include <libsnapshot/cow_reader.h>
#include <zlib.h>

#include "cow_decompress.h"

namespace android {
namespace snapshot {

CowReader::CowReader() : fd_(-1), header_(), fd_size_(0) {}
CowReader::CowReader()
    : fd_(-1),
      header_(),
      footer_(),
      fd_size_(0),
      has_footer_(false),
      last_label_(0),
      has_last_label_(false) {}

static void SHA256(const void*, size_t, uint8_t[]) {
#if 0
@@ -63,16 +72,6 @@ bool CowReader::Parse(android::base::borrowed_fd fd) {
        return false;
    }

    // Validity check the ops range.
    if (header_.ops_offset >= fd_size_) {
        LOG(ERROR) << "ops offset " << header_.ops_offset << " larger than fd size " << fd_size_;
        return false;
    }
    if (fd_size_ - header_.ops_offset < header_.ops_size) {
        LOG(ERROR) << "ops size " << header_.ops_size << " is too large";
        return false;
    }

    if (header_.magic != kCowMagicNumber) {
        LOG(ERROR) << "Header Magic corrupted. Magic: " << header_.magic
                   << "Expected: " << kCowMagicNumber;
@@ -83,6 +82,11 @@ bool CowReader::Parse(android::base::borrowed_fd fd) {
                   << sizeof(CowHeader);
        return false;
    }
    if (header_.footer_size != sizeof(CowFooter)) {
        LOG(ERROR) << "Footer size unknown, read " << header_.footer_size << ", expected "
                   << sizeof(CowFooter);
        return false;
    }

    if ((header_.major_version != kCowVersionMajor) ||
        (header_.minor_version != kCowVersionMinor)) {
@@ -94,19 +98,75 @@ bool CowReader::Parse(android::base::borrowed_fd fd) {
        return false;
    }

    uint8_t header_csum[32];
    {
        CowHeader tmp = header_;
        memset(&tmp.header_checksum, 0, sizeof(tmp.header_checksum));
        memset(header_csum, 0, sizeof(uint8_t) * 32);
    auto footer_pos = lseek(fd_.get(), -header_.footer_size, SEEK_END);
    if (footer_pos != fd_size_ - header_.footer_size) {
        LOG(ERROR) << "Failed to read full footer!";
        return false;
    }
    if (!android::base::ReadFully(fd_, &footer_, sizeof(footer_))) {
        PLOG(ERROR) << "read footer failed";
        return false;
    }
    has_footer_ = (footer_.op.type == kCowFooterOp);
    return ParseOps();
}

        SHA256(&tmp, sizeof(tmp), header_csum);
bool CowReader::ParseOps() {
    uint64_t pos = lseek(fd_.get(), sizeof(header_), SEEK_SET);
    if (pos != sizeof(header_)) {
        PLOG(ERROR) << "lseek ops failed";
        return false;
    }
    uint64_t next_last_label = 0;
    bool has_next = false;
    auto ops_buffer = std::make_shared<std::vector<CowOperation>>();
    if (has_footer_) ops_buffer->reserve(footer_.op.num_ops);
    uint64_t current_op_num = 0;
    // Look until we reach the last possible non-footer position.
    uint64_t last_pos = fd_size_ - (has_footer_ ? sizeof(footer_) : sizeof(CowOperation));

    // Alternating op and data
    while (pos < last_pos) {
        ops_buffer->resize(current_op_num + 1);
        if (!android::base::ReadFully(fd_, ops_buffer->data() + current_op_num,
                                      sizeof(CowOperation))) {
            PLOG(ERROR) << "read op failed";
            return false;
        }
    if (memcmp(header_csum, header_.header_checksum, sizeof(header_csum)) != 0) {
        LOG(ERROR) << "header checksum is invalid";
        auto& current_op = ops_buffer->data()[current_op_num];
        pos = lseek(fd_.get(), GetNextOpOffset(current_op), SEEK_CUR);
        if (pos < 0) {
            PLOG(ERROR) << "lseek next op failed";
            return false;
        }
        current_op_num++;
        if (current_op.type == kCowLabelOp) {
            // If we don't have a footer, the last label may be incomplete
            if (has_footer_) {
                has_last_label_ = true;
                last_label_ = current_op.source;
            } else {
                last_label_ = next_last_label;
                if (has_next) has_last_label_ = true;
                next_last_label = current_op.source;
                has_next = true;
            }
        }
    }

    uint8_t csum[32];
    memset(csum, 0, sizeof(uint8_t) * 32);

    if (has_footer_) {
        SHA256(ops_buffer.get()->data(), footer_.op.ops_size, csum);
        if (memcmp(csum, footer_.data.ops_checksum, sizeof(csum)) != 0) {
            LOG(ERROR) << "ops checksum does not match";
            return false;
        }
    } else {
        LOG(INFO) << "No Footer, recovered data";
    }
    ops_ = ops_buffer;
    return true;
}

@@ -115,74 +175,58 @@ bool CowReader::GetHeader(CowHeader* header) {
    return true;
}

bool CowReader::GetFooter(CowFooter* footer) {
    if (!has_footer_) return false;
    *footer = footer_;
    return true;
}

bool CowReader::GetLastLabel(uint64_t* label) {
    if (!has_last_label_) return false;
    *label = last_label_;
    return true;
}

class CowOpIter final : public ICowOpIter {
  public:
    CowOpIter(std::unique_ptr<uint8_t[]>&& ops, size_t len);
    CowOpIter(std::shared_ptr<std::vector<CowOperation>>& ops);

    bool Done() override;
    const CowOperation& Get() override;
    void Next() override;

  private:
    bool HasNext();

    std::unique_ptr<uint8_t[]> ops_;
    const uint8_t* pos_;
    const uint8_t* end_;
    bool done_;
    std::shared_ptr<std::vector<CowOperation>> ops_;
    std::vector<CowOperation>::iterator op_iter_;
};

CowOpIter::CowOpIter(std::unique_ptr<uint8_t[]>&& ops, size_t len)
    : ops_(std::move(ops)), pos_(ops_.get()), end_(pos_ + len), done_(!HasNext()) {}

bool CowOpIter::Done() {
    return done_;
CowOpIter::CowOpIter(std::shared_ptr<std::vector<CowOperation>>& ops) {
    ops_ = ops;
    op_iter_ = ops_.get()->begin();
}

bool CowOpIter::HasNext() {
    return pos_ < end_ && size_t(end_ - pos_) >= sizeof(CowOperation);
bool CowOpIter::Done() {
    return op_iter_ == ops_.get()->end();
}

void CowOpIter::Next() {
    CHECK(!Done());

    pos_ += sizeof(CowOperation);
    if (!HasNext()) done_ = true;
    op_iter_++;
}

const CowOperation& CowOpIter::Get() {
    CHECK(!Done());
    CHECK(HasNext());
    return *reinterpret_cast<const CowOperation*>(pos_);
    return (*op_iter_);
}

std::unique_ptr<ICowOpIter> CowReader::GetOpIter() {
    if (lseek(fd_.get(), header_.ops_offset, SEEK_SET) < 0) {
        PLOG(ERROR) << "lseek ops failed";
        return nullptr;
    }
    auto ops_buffer = std::make_unique<uint8_t[]>(header_.ops_size);
    if (!android::base::ReadFully(fd_, ops_buffer.get(), header_.ops_size)) {
        PLOG(ERROR) << "read ops failed";
        return nullptr;
    }

    uint8_t csum[32];
    memset(csum, 0, sizeof(uint8_t) * 32);

    SHA256(ops_buffer.get(), header_.ops_size, csum);
    if (memcmp(csum, header_.ops_checksum, sizeof(csum)) != 0) {
        LOG(ERROR) << "ops checksum does not match";
        return nullptr;
    }

    return std::make_unique<CowOpIter>(std::move(ops_buffer), header_.ops_size);
    return std::make_unique<CowOpIter>(ops_);
}

bool CowReader::GetRawBytes(uint64_t offset, void* buffer, size_t len, size_t* read) {
    // Validate the offset, taking care to acknowledge possible overflow of offset+len.
    if (offset < sizeof(header_) || offset >= header_.ops_offset || len >= fd_size_ ||
        offset + len > header_.ops_offset) {
    if (offset < sizeof(header_) || offset >= fd_size_ - sizeof(footer_) || len >= fd_size_ ||
        offset + len > fd_size_ - sizeof(footer_)) {
        LOG(ERROR) << "invalid data offset: " << offset << ", " << len << " bytes";
        return false;
    }
+1 −1
Original line number Diff line number Diff line
@@ -227,7 +227,7 @@ void SnapuserdTest::CreateCowDevice(std::unique_ptr<TemporaryFile>& cow) {
    ASSERT_TRUE(writer.AddRawBlocks(blk_random2_replace_start, random_buffer_2_.get(), size_));

    // Flush operations
    ASSERT_TRUE(writer.Flush());
    ASSERT_TRUE(writer.Finalize());

    ASSERT_EQ(lseek(cow->fd, 0, SEEK_SET), 0);
}
Loading