blob: 67fbfb9111c4ba8de725cfa0dd5fd7774eb6c233 [file] [log] [blame]
// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/storage/fvm/host/fvm_container.h"
#include <errno.h>
#include <inttypes.h>
#include <lib/fit/defer.h>
#include <sys/ioctl.h>
#include <iostream>
#include <memory>
#include <utility>
#include <safemath/checked_math.h>
#include "src/storage/fvm/format.h"
#include "src/storage/fvm/host/format.h"
#include "src/storage/fvm/host/internal_snapshot_meta_format.h"
#include "src/storage/fvm/snapshot_metadata_format.h"
#include "src/storage/fvm/sparse_reader.h"
#if defined(__APPLE__)
#include <sys/disk.h>
#define IOCTL_GET_BLOCK_COUNT DKIOCGETBLOCKCOUNT
#endif
#if defined(__linux__)
#include <linux/fs.h>
#define IOCTL_GET_BLOCK_COUNT BLKGETSIZE
#endif
constexpr int DEFAULT_OPEN_MODE = 0644;
zx_status_t FvmContainer::CreateNew(const char* path, size_t slice_size, off_t offset, off_t length,
std::unique_ptr<FvmContainer>* out) {
std::unique_ptr<FvmContainer> fvmContainer(new FvmContainer(path, slice_size, offset, length));
zx_status_t status;
if ((status = fvmContainer->InitNew()) != ZX_OK) {
return status;
}
*out = std::move(fvmContainer);
return ZX_OK;
}
zx_status_t FvmContainer::CreateExisting(const char* path, off_t offset,
std::unique_ptr<FvmContainer>* out) {
std::unique_ptr<FvmContainer> fvmContainer(new FvmContainer(path, 0, offset, 0));
zx_status_t status;
if ((status = fvmContainer->InitExisting()) != ZX_OK) {
return status;
}
*out = std::move(fvmContainer);
return ZX_OK;
}
zx_status_t FvmContainer::Verify(const char* path, off_t offset) {
std::unique_ptr<FvmContainer> fvmContainer(new FvmContainer(path, 0, offset, 0));
return fvmContainer->InitExisting(InitExistingMode::kCheckOnly);
}
FvmContainer::FvmContainer(const char* path, size_t slice_size, off_t offset, off_t length)
: Container(path, slice_size, 0), disk_offset_(offset), disk_size_(length) {}
FvmContainer::~FvmContainer() = default;
zx_status_t FvmContainer::InitNew() {
fd_.reset(open(path_.data(), O_RDWR, DEFAULT_OPEN_MODE));
if (!fd_) {
if (errno != ENOENT) {
fprintf(stderr, "Failed to open path %s: %s\n", path_.data(), strerror(errno));
return ZX_ERR_IO;
}
if (disk_offset_ > 0 || disk_size_ > 0) {
fprintf(stderr, "Invalid disk size for path %s", path_.data());
return ZX_ERR_INVALID_ARGS;
}
fd_.reset(open(path_.data(), O_RDWR | O_CREAT | O_EXCL, DEFAULT_OPEN_MODE));
if (!fd_) {
fprintf(stderr, "Failed to create path %s\n", path_.data());
return ZX_ERR_IO;
}
xprintf("Created path %s\n", path_.data());
} else {
// If the file already exists, check its size and make sure it is valid given the user
// provided disk size and offset (if any).
uint64_t size;
zx_status_t status = VerifyFileSize(&size);
if (status != ZX_OK) {
return status;
}
if (disk_size_ == 0) {
disk_size_ = size;
}
}
return info_.Reset(disk_size_, slice_size_);
}
zx_status_t FvmContainer::VerifyFileSize(uint64_t* size_out, bool allow_resize) {
struct stat stats;
if (fstat(fd_.get(), &stats) < 0) {
fprintf(stderr, "Failed to stat %s\n", path_.data());
return ZX_ERR_IO;
}
uint64_t size = stats.st_size;
if (S_ISBLK(stats.st_mode)) {
uint64_t block_count;
if (ioctl(fd_.get(), IOCTL_GET_BLOCK_COUNT, &block_count) >= 0) {
size = block_count * 512;
}
}
if (allow_resize) {
uint64_t minimum_disk_size = CalculateDiskSize();
if (size < minimum_disk_size) {
fprintf(stderr, "Invalid file size %" PRIu64 " for minimum disk size %" PRIu64 "\n", size,
minimum_disk_size);
return ZX_ERR_INVALID_ARGS;
}
} else if (disk_size_ > 0 && size < disk_offset_ + disk_size_) {
fprintf(stderr, "Invalid file size %" PRIu64 " for specified offset+length\n", size);
return ZX_ERR_INVALID_ARGS;
}
if (size_out != nullptr) {
*size_out = size;
}
return ZX_OK;
}
zx_status_t FvmContainer::InitExisting(InitExistingMode mode) {
int flag = mode == InitExistingMode::kAllowModification ? O_RDWR : O_RDONLY;
fd_.reset(open(path_.data(), flag, DEFAULT_OPEN_MODE));
if (!fd_) {
fprintf(stderr, "Failed to open path %s: %s\n", path_.data(), strerror(errno));
return ZX_ERR_IO;
}
fvm::Header fvm_superblock;
if (pread(fd_.get(), &fvm_superblock, sizeof(fvm::Header), disk_offset_) != sizeof(fvm::Header)) {
fprintf(stderr, "Failed to read FVM metadata from disk\n");
return ZX_ERR_IO;
}
if (fvm_superblock.magic != fvm::kMagic) {
fprintf(stderr, "Found invalid FVM container\n");
return ZX_ERR_INVALID_ARGS;
}
disk_size_ = fvm_superblock.fvm_partition_size;
// Attempt to load metadata from disk
fvm::host::FdWrapper wrapper = fvm::host::FdWrapper(fd_.get());
zx_status_t status = info_.Load(&wrapper, disk_offset_, disk_size_);
if (status != ZX_OK) {
fprintf(stderr, "Failed to load FVM image: %d\n", status);
return status;
}
if (!info_.Validate()) {
fprintf(stderr, "Found invalid FVM container\n");
return ZX_ERR_INVALID_ARGS;
}
slice_size_ = info_.SliceSize();
// For an existing file, ensure the metadata is internally consistent.
// This includes accounting for the possibility the FVM may resize even if the
// container is smaller than we expect.
status = VerifyFileSize(nullptr, true /* allow_resize */);
if (status != ZX_OK) {
return status;
}
return ZX_OK;
}
zx_status_t FvmContainer::Verify() const {
info_.CheckValid();
if (!info_.Validate()) {
return ZX_ERR_IO_DATA_INTEGRITY;
}
const fvm::Header& sb = info_.SuperBlock();
xprintf("Total size is %zu\n", disk_size_);
xprintf("Metadata size is %zu\n", info_.MetadataSize());
xprintf("Slice size is %" PRIu64 "\n", info_.SliceSize());
xprintf("Slice count is %" PRIu64 "\n", info_.SuperBlock().pslice_count);
// |end| keeps track of where each partition ends. Initialize it to the first byte of the first
// partition (as if there was a partition before this first one).
off_t end = disk_offset_ + info_.SuperBlock().GetDataStartOffset();
size_t slice_index = 1;
for (size_t vpart_index = 1; vpart_index <= sb.GetPartitionTableEntryCount(); ++vpart_index) {
fvm::VPartitionEntry* vpart = nullptr;
// The next partition starts where the last ended.
off_t start = end;
zx_status_t status;
if ((status = info_.GetPartition(vpart_index, &vpart)) != ZX_OK) {
return status;
}
if (vpart->IsFree()) {
break;
}
if (vpart->IsInternalReservationPartition()) {
// Reserve partitions need no verification.
continue;
}
fbl::Vector<size_t> extent_lengths;
size_t last_vslice = 0;
size_t slice_count = 0;
for (; slice_index <= sb.pslice_count; ++slice_index) {
fvm::SliceEntry* slice = nullptr;
if ((status = info_.GetSlice(slice_index, &slice)) != ZX_OK) {
return status;
}
if (slice->VPartition() != vpart_index) {
break;
}
end += slice_size_;
slice_count++;
if (slice->VSlice() == last_vslice + 1) {
extent_lengths[extent_lengths.size() - 1] += slice_size_;
} else {
extent_lengths.push_back(slice_size_);
}
last_vslice = slice->VSlice();
}
if (vpart->slices != slice_count) {
fprintf(stderr, "Detected slices for partition %lu (%lu) do not match expected (%u)\n",
vpart_index, slice_count, vpart->slices);
return ZX_ERR_BAD_STATE;
}
disk_format_t part;
if ((status = Format::Detect(fd_.get(), start, &part)) != ZX_OK) {
return status;
}
fbl::unique_fd dupfd(dup(fd_.get()));
if (!dupfd) {
fprintf(stderr, "Failed to duplicate fd\n");
return ZX_ERR_INTERNAL;
}
if ((status = Format::Check(std::move(dupfd), start, end, extent_lengths, part)) != ZX_OK) {
std::cerr << vpart->name() << " fsck returned an error." << std::endl;
return status;
}
xprintf("Found valid %s partition\n", vpart->name);
}
return ZX_OK;
}
zx_status_t FvmContainer::Extend(size_t new_disk_size) {
if (disk_offset_) {
fprintf(stderr, "Cannot extend FVM within another container\n");
return ZX_ERR_BAD_STATE;
}
if (new_disk_size <= disk_size_) {
if (extend_length_type_ == ExtendLengthType::LOWER_BOUND) {
return ResizeImageFileToDiskSize();
}
fprintf(stderr, "Cannot extend to disk size %zu smaller than current size %" PRIu64 "\n",
new_disk_size, disk_size_);
return ZX_ERR_INVALID_ARGS;
}
const char* temp = ".tmp";
if (path_.length() >= PATH_MAX - strlen(temp) - 1) {
fprintf(stderr, "Path name exceeds maximum length\n");
return ZX_ERR_INVALID_ARGS;
}
fbl::StringBuffer<PATH_MAX> path;
path.AppendPrintf("%s%s", path_.c_str(), temp);
fbl::unique_fd fd(open(path.c_str(), O_RDWR | O_CREAT, DEFAULT_OPEN_MODE));
if (!fd) {
fprintf(stderr, "Unable to open temp file %s\n", path.c_str());
return ZX_ERR_IO;
}
auto cleanup = fit::defer([path]() {
if (unlink(path.c_str()) < 0) {
fprintf(stderr, "Failed to unlink path %s\n", path.c_str());
}
});
if (ftruncate(fd.get(), new_disk_size) != 0) {
fprintf(stderr, "Failed to truncate fvm container");
return ZX_ERR_IO;
}
// Since the size and location of both metadata in an FVM is dependent on the size of
// the FVM partition, we must relocate any data that already exists within the volume
// manager.
//
// First, we read all old slices from the original device, and write them to their
// new locations.
//
// Then, we update the on-disk metadata to reflect the new size of the disk.
// To avoid collision between relocated slices, this is done on a temporary file.
fvm::Header source_header = info_.SuperBlock();
fvm::Header target_header =
fvm::Header::FromDiskSize(fvm::kMaxUsablePartitions, new_disk_size, slice_size_);
std::vector<uint8_t> data(slice_size_);
for (uint32_t index = 1; index <= info_.SuperBlock().GetAllocationTableUsedEntryCount();
index++) {
zx_status_t status;
fvm::SliceEntry* slice = nullptr;
if ((status = info_.GetSlice(index, &slice)) != ZX_OK) {
fprintf(stderr, "Failed to retrieve slice %u: %d\n", index, status);
return status;
}
if (slice->IsFree()) {
continue;
}
ssize_t r = pread(fd_.get(), data.data(), slice_size_, source_header.GetSliceDataOffset(index));
if (r < 0 || static_cast<size_t>(r) != slice_size_) {
fprintf(stderr, "Failed to read slice %u from FVM: %ld\n", index, r);
return ZX_ERR_BAD_STATE;
}
r = pwrite(fd.get(), data.data(), slice_size_, target_header.GetSliceDataOffset(index));
if (r < 0 || static_cast<size_t>(r) != slice_size_) {
fprintf(stderr, "Failed to write data to FVM: %ld\n", r);
return ZX_ERR_BAD_STATE;
}
}
if (zx_status_t status = info_.Grow(target_header); status != ZX_OK) {
return status;
}
fvm::host::FdWrapper wrapper = fvm::host::FdWrapper(fd.get());
if (zx_status_t status = info_.Write(&wrapper, 0, new_disk_size); status != ZX_OK) {
return status;
}
fd_.reset(fd.release());
disk_size_ = new_disk_size;
if (rename(path.c_str(), path_.c_str()) < 0) {
fprintf(stderr, "Failed to copy over temp file\n");
return ZX_ERR_IO;
}
cleanup.cancel();
return ZX_OK;
}
zx_status_t FvmContainer::Commit() {
if (!info_.IsDirty()) {
fprintf(stderr, "Commit: Nothing to write\n");
return ZX_OK;
}
// If the FVM container has just been created, truncate it to an appropriate size
if (disk_size_ == 0) {
if (partitions_.is_empty()) {
fprintf(stderr, "Cannot create new FVM container with 0 partitions\n");
return ZX_ERR_INVALID_ARGS;
}
fvm::Header header =
fvm::Header::FromSliceCount(fvm::kMaxUsablePartitions, CountAddedSlices(), slice_size_);
if (zx_status_t status = info_.Grow(header); status != ZX_OK) {
return status;
}
uint64_t total_size = header.fvm_partition_size;
if (ftruncate(fd_.get(), total_size) != 0) {
fprintf(stderr, "Failed to truncate fvm container");
return ZX_ERR_IO;
}
struct stat s;
if (fstat(fd_.get(), &s) < 0) {
fprintf(stderr, "Failed to stat container\n");
return ZX_ERR_IO;
}
disk_size_ = s.st_size;
if (disk_size_ != total_size) {
fprintf(stderr, "Truncated to incorrect size\n");
return ZX_ERR_IO;
}
}
fvm::host::FdWrapper wrapper = fvm::host::FdWrapper(fd_.get());
zx_status_t status = info_.Write(&wrapper, disk_offset_, disk_size_);
if (status != ZX_OK) {
return status;
}
size_t primary_offset = info_.SuperBlock().GetSuperblockOffset(fvm::SuperblockType::kPrimary);
size_t secondary_offset = info_.SuperBlock().GetSuperblockOffset(fvm::SuperblockType::kSecondary);
non_empty_segments_ = {
{disk_offset_ + primary_offset, disk_offset_ + primary_offset + info_.MetadataSize()},
{disk_offset_ + secondary_offset, disk_offset_ + secondary_offset + info_.MetadataSize()},
};
for (unsigned i = 0; i < partitions_.size(); i++) {
if ((status = WritePartition(i)) != ZX_OK) {
return status;
}
}
xprintf("Successfully wrote FVM data to disk\n");
return ZX_OK;
}
zx_status_t FvmContainer::ResizeImageFileToFit() {
// Resize the image file to just fit the header and added partitions. Disk size specified in
// the metadata header remains the same. Metadatasize and slice offset stay consistent with the
// the specified disk size.
size_t required_data_size = CountAddedSlices() * slice_size_;
size_t minimal_size = disk_offset_ + info_.SuperBlock().GetDataStartOffset() + required_data_size;
if (ftruncate(fd_.get(), minimal_size) != 0) {
fprintf(stderr, "Failed to truncate fvm container");
return ZX_ERR_IO;
}
return ZX_OK;
}
zx_status_t FvmContainer::ResizeImageFileToDiskSize() {
if (ftruncate(fd_.get(), disk_size_ + disk_offset_) != 0) {
fprintf(stderr, "Failed to truncate fvm container");
return ZX_ERR_IO;
}
return ZX_OK;
}
size_t FvmContainer::SliceSize() const {
info_.CheckValid();
return slice_size_;
}
zx_status_t FvmContainer::AddPartition(const char* path, const char* type_name,
FvmReservation* reserve) {
info_.CheckValid();
std::unique_ptr<Format> format;
zx_status_t status;
if ((status = Format::Create(path, type_name, &format)) != ZX_OK) {
fprintf(stderr, "Failed to initialize partition\n");
return status;
}
uint32_t vpart_index;
uint8_t guid[fvm::kGuidSize];
format->Guid(guid);
fvm::PartitionDescriptor descriptor;
format->GetPartitionInfo(&descriptor);
if ((status = info_.AllocatePartition(descriptor, guid, &vpart_index)) != ZX_OK) {
fprintf(stderr, "Failed to allocate partition: %d\n", status);
return status;
}
if ((status = format->MakeFvmReady(slice_size_, vpart_index, reserve)) != ZX_OK) {
fprintf(stderr, "Failed to prepare partition for FVM: %d\n", status);
return status;
}
uint32_t slice_count = 0;
if ((status = format->GetSliceCount(&slice_count)) != ZX_OK) {
return status;
}
// If allocated metadata is too small, grow it to an appropriate size
if ((status = info_.GrowForSlices(slice_count)) != ZX_OK) {
fprintf(stderr, "Failed to resize metadata buffer: %d\n", status);
return status;
}
// Allocate all slices for each extent in this partition
uint32_t pslice_start = 0;
uint32_t pslice_total = 0;
unsigned extent_index = 0;
while (true) {
zx::status<ExtentInfo> extent_or = format->GetExtent(extent_index);
if (extent_or.is_error()) {
if (extent_or.status_value() == ZX_ERR_OUT_OF_RANGE) {
break;
}
return extent_or.status_value();
}
const ExtentInfo& extent = extent_or.value();
zx::status<uint32_t> pslice_or = info_.AllocateSlicesContiguous(format->VpartIndex(), extent);
if (pslice_or.is_error()) {
return pslice_or.status_value();
}
if (pslice_start == 0) {
pslice_start = pslice_or.value();
}
pslice_total += extent.PslicesNeeded();
++extent_index;
}
fvm::VPartitionEntry* entry;
if ((status = info_.GetPartition(format->VpartIndex(), &entry)) != ZX_OK) {
return status;
}
ZX_ASSERT(entry->slices == pslice_total);
FvmPartitionInfo partition;
partition.vpart_index = format->VpartIndex();
partition.pslice_start = pslice_start;
partition.pslice_count = pslice_total;
partition.format = std::move(format);
partitions_.push_back(std::move(partition));
return ZX_OK;
}
zx_status_t FvmContainer::AddSnapshotMetadataPartition(size_t reserved_slices) {
info_.CheckValid();
auto vpart_or = info_.AllocatePartition(fvm::VPartitionEntry::CreateSnapshotMetadataPartition());
if (vpart_or.is_error()) {
return vpart_or.status_value();
}
uint32_t vpart_index = vpart_or.value();
// TODO(fxbug.dev/59567): Add partition/extent entries describing blobfs.
std::vector<fvm::PartitionSnapshotState> partition_states{};
std::vector<fvm::SnapshotExtentType> extent_types{};
auto format = std::make_unique<InternalSnapshotMetaFormat>(reserved_slices, slice_size_,
partition_states, extent_types);
// Find out the actual number of slices we need by asking |format|.
uint32_t final_slices;
zx_status_t status = format->GetSliceCount(&final_slices);
if (status != ZX_OK) {
return status;
}
if ((status = info_.GrowForSlices(final_slices)) != ZX_OK) {
fprintf(stderr, "Failed to resize metadata buffer: %d\n", status);
return status;
}
// Allocate all slices for this partition.
// This assumes there is only one extent in |format|.
auto pslice_start_or = info_.AllocateSlicesContiguous(vpart_index, format->GetExtent(0).value());
if (pslice_start_or.is_error()) {
fprintf(stderr, "Failed to allocate slices: %d\n", pslice_start_or.status_value());
return pslice_start_or.status_value();
}
fvm::VPartitionEntry* entry;
if ((status = info_.GetPartition(vpart_index, &entry)) != ZX_OK) {
return status;
}
ZX_ASSERT(entry->slices == final_slices);
FvmPartitionInfo partition;
partition.format = std::move(format);
partition.vpart_index = vpart_index;
partition.pslice_start = pslice_start_or.value();
partition.vslice_count = final_slices;
partitions_.push_back(std::move(partition));
return ZX_OK;
}
size_t FvmContainer::CountAddedSlices() const {
size_t required_slices = 0;
for (size_t index = 1; index <= info_.SuperBlock().GetPartitionTableEntryCount(); ++index) {
fvm::VPartitionEntry* vpart;
ZX_ASSERT(info_.GetPartition(index, &vpart) == ZX_OK);
if (vpart->IsFree()) {
continue;
}
required_slices += vpart->slices;
}
return required_slices;
}
uint64_t FvmContainer::CalculateDiskSize() const {
info_.CheckValid();
return CalculateDiskSizeForSlices(CountAddedSlices());
}
uint64_t FvmContainer::GetDiskSize() const { return disk_size_; }
zx_status_t FvmContainer::WritePartition(unsigned part_index) {
info_.CheckValid();
if (part_index > partitions_.size()) {
fprintf(stderr, "Error: Tried to access partition %u / %zu\n", part_index, partitions_.size());
return ZX_ERR_OUT_OF_RANGE;
}
unsigned extent_index = 0;
FvmPartitionInfo* partition = &partitions_[part_index];
if (!partition->format) {
// No data to write. This leaves the partition's data uninitialized.
// The underlying file was already extended to the appropriate size with ftruncate(2), which
// means that the byte value will be zeroes.
return ZX_OK;
}
Format* format = partition->format.get();
uint32_t pslice_start = partition->pslice_start;
while (true) {
zx_status_t status;
if ((status = WriteExtent(extent_index++, format, &pslice_start)) != ZX_OK) {
if (status != ZX_ERR_OUT_OF_RANGE) {
return status;
}
return ZX_OK;
}
}
}
zx_status_t FvmContainer::WriteExtent(unsigned extent_index, Format* format, uint32_t* pslice) {
auto extent_or = format->GetExtent(extent_index);
if (extent_or.is_error()) {
return extent_or.status_value();
}
const ExtentInfo& extent = extent_or.value();
auto slice_start = GetBlockStart(*pslice, 0, format->BlockSize());
auto slice_end =
safemath::CheckMul(safemath::CheckAdd(slice_start, extent.vslice_count), slice_size_)
.ValueOrDie();
AddNonEmptySegment(slice_start, slice_end);
zx_status_t status;
// Write each slice in the given extent
uint32_t current_block = 0;
uint32_t current_pslice = *pslice;
for (unsigned i = 0; i < extent.vslice_count; i++) {
// Write each block in this slice
for (uint32_t j = 0; j < format->BlocksPerSlice(); j++) {
// If we have gone beyond the blocks written to partition file, write empty block
if (current_block >= extent.block_count) {
if (!extent.zero_fill) {
break;
}
format->EmptyBlock();
} else {
if ((status = format->FillBlock(extent.block_offset + current_block)) != ZX_OK) {
fprintf(stderr, "Failed to read block from filesystem\n");
return status;
}
++current_block;
}
if ((status = WriteData(current_pslice, j, format->BlockSize(), format->Data())) != ZX_OK) {
fprintf(stderr, "Failed to write data to FVM\n");
return status;
}
}
++current_pslice;
}
*pslice += extent.PslicesNeeded();
return ZX_OK;
}
zx_status_t FvmContainer::WriteData(uint32_t pslice, uint32_t block_offset, size_t block_size,
void* data) {
info_.CheckValid();
if (block_offset * block_size > slice_size_) {
fprintf(stderr, "Not enough space in slice\n");
return ZX_ERR_OUT_OF_RANGE;
}
if (lseek(fd_.get(), GetBlockStart(pslice, block_offset, block_size), SEEK_SET) < 0) {
return ZX_ERR_BAD_STATE;
}
ssize_t r = write(fd_.get(), data, block_size);
if (r < 0 || static_cast<size_t>(r) != block_size) {
fprintf(stderr, "Failed to write data to FVM\n");
return ZX_ERR_BAD_STATE;
}
return ZX_OK;
}
size_t FvmContainer::GetBlockStart(uint32_t pslice, uint32_t block_offset,
size_t block_size) const {
return disk_offset_ +
fvm::Header::FromDiskSize(fvm::kMaxUsablePartitions, disk_size_, slice_size_)
.GetSliceDataOffset(pslice) +
block_offset * block_size;
}
AndroidSparseChunkType FvmContainer::DetermineAndroidSparseChunkType(const uint32_t* buffer,
size_t block_size,
size_t block_start) {
// Check whether it can be dont-care block.
// If it intersects with any non-empty segment, it cannot be dont-care.
for (const auto& segment : non_empty_segments_) {
if (segment.start >= block_start + block_size) {
break;
}
if (segment.end > block_start) {
if (std::all_of(buffer, &buffer[block_size / sizeof(buffer[0])],
[&](uint32_t val) { return val == buffer[0]; })) {
return kChunkTypeFill;
}
return kChunkTypeRaw;
}
}
return kChunkTypeDontCare;
}
namespace {
bool CanAppendBlockToChunk(const uint32_t* buffer, uint16_t block_type,
const AndroidSparseChunkHeader& chunk, uint32_t fill_val) {
// Check whether we can just append the block to current chunk.
// 1. The block has to be of the same type as the current chunk.
// 2. If it is kChunkTypeFill, the fill value should be the same as well.
if (block_type == chunk.chunk_type) {
return block_type == kChunkTypeFill ? buffer[0] == fill_val : true;
}
return false;
}
} // namespace
zx_status_t FvmContainer::ConvertToAndroidSparseImage() {
char path[] = "/tmp/block.XXXXXX";
fbl::unique_fd fd(mkstemp(path));
if (!fd) {
fprintf(stderr, "Failed to create temporary file\n");
return ZX_ERR_IO;
}
auto cleanup = fit::defer([path]() {
if (unlink(path) < 0) {
fprintf(stderr, "Failed to unlink path %s\n", path);
}
});
// The block size is recommended to be always 4096.
constexpr size_t block_size = 4096;
// Defined as uint32_t instead of uint8_t because kChunkTypeFill is based
// on uint32_t granularity instead of byte.
uint32_t buffer[block_size / sizeof(uint32_t)];
if (lseek(fd_.get(), 0, SEEK_SET) != 0) {
fprintf(stderr, "Failed to seek to the beginning of the file.\n");
return ZX_ERR_IO;
}
uint64_t file_size = fvm::host::FdWrapper(fd_.get()).Size();
FinalizeNonEmptySegmentsInfo();
// Scan the file to determine all chunks.
std::vector<AndroidSparseChunkHeader> chunks;
size_t total_bytes = 0, total_blocks = 0;
uint32_t fill_val = 0;
while (total_bytes < file_size) {
ssize_t read_bytes = read(fd_.get(), buffer, block_size);
if (read_bytes != block_size) {
fprintf(stderr, "Failed to read data @ %zu\n", total_bytes);
return ZX_ERR_IO;
}
AndroidSparseChunkType block_type =
DetermineAndroidSparseChunkType(buffer, block_size, total_bytes);
if (!chunks.empty() && CanAppendBlockToChunk(buffer, block_type, chunks.back(), fill_val)) {
chunks.back().chunk_blocks++;
chunks.back().total_size += block_type == kChunkTypeRaw ? block_size : 0;
} else {
// Start a new chunk.
chunks.push_back({block_type, 0, 1, sizeof(AndroidSparseChunkHeader)});
if (block_type == kChunkTypeFill) {
chunks.back().total_size += sizeof(uint32_t);
fill_val = buffer[0];
} else if (block_type == kChunkTypeRaw) {
chunks.back().total_size += block_size;
}
}
total_bytes += read_bytes;
total_blocks++;
}
// Construct android sparse image header.
AndroidSparseHeader sparse_header{
.file_header_size = sizeof(AndroidSparseHeader),
.chunk_header_size = sizeof(AndroidSparseChunkHeader),
.block_size = static_cast<uint32_t>(block_size),
.total_blocks = static_cast<uint32_t>(total_blocks),
.total_chunks = static_cast<unsigned>(chunks.size()),
.image_checksum = 0,
};
if (write(fd.get(), &sparse_header, sizeof(sparse_header)) != sizeof(sparse_header)) {
fprintf(stderr, "Failed to write sparse header\n");
return ZX_ERR_IO;
}
// Write chunks to file.
size_t read_offset = 0;
for (auto& chunk : chunks) {
// Write chunk header.
if (write(fd.get(), &chunk, sizeof(chunk)) != sizeof(chunk)) {
fprintf(stderr, "Failed to write chunk header\n");
return ZX_ERR_IO;
}
// Write chunk data.
if (chunk.chunk_type == kChunkTypeRaw) {
for (size_t i = 0; i < chunk.chunk_blocks; i++) {
ssize_t read_bytes = pread(fd_.get(), buffer, block_size, read_offset + i * block_size);
if (read_bytes != block_size) {
fprintf(stderr, "Failed to read raw block data @ %zu\n", read_offset + i * block_size);
return ZX_ERR_IO;
}
ssize_t write_bytes = write(fd.get(), buffer, block_size);
if (write_bytes != block_size) {
fprintf(stderr, "Failed to write raw block data\n");
return ZX_ERR_IO;
}
}
} else if (chunk.chunk_type == kChunkTypeFill) {
uint32_t fill_val;
if (pread(fd_.get(), &fill_val, sizeof(fill_val), read_offset) != sizeof(fill_val)) {
fprintf(stderr, "Failed to read fill value @ %zu\n", read_offset);
return ZX_ERR_IO;
}
if (write(fd.get(), &fill_val, sizeof(fill_val)) != sizeof(fill_val)) {
fprintf(stderr, "Failed to write fill value for fill chunk\n");
return ZX_ERR_IO;
}
}
read_offset += chunk.chunk_blocks * block_size;
}
fd_.reset(fd.release());
if (rename(path, path_.c_str()) < 0) {
fprintf(stderr, "Failed to copy over temp file\n");
return ZX_ERR_IO;
}
cleanup.cancel();
return ZX_OK;
}
namespace {
zx_status_t CreateCompressionContext(CompressionContext* out, size_t size) {
auto result = CompressionContext::Create();
if (!result.is_ok()) {
fprintf(stderr, "%s", result.take_error_result().error.c_str());
return ZX_ERR_INTERNAL;
}
*out = std::move(result.take_ok_result().value);
out->Setup(size);
return ZX_OK;
}
} // namespace
zx_status_t FvmContainer::CompressWithLZ4() {
constexpr size_t kBufferLength = 1024 * 1024;
std::vector<uint8_t> buffer(kBufferLength);
if (lseek(fd_.get(), 0, SEEK_SET) != 0) {
fprintf(stderr, "Failed to seek to beginning of the file.\n");
return ZX_ERR_IO;
}
uint64_t file_size = fvm::host::FdWrapper(fd_.get()).Size();
CompressionContext compression;
if (auto status = CreateCompressionContext(&compression, file_size); status != ZX_OK) {
return status;
}
while (true) {
ssize_t read_bytes = read(fd_.get(), buffer.data(), kBufferLength);
if (read_bytes < 0) {
fprintf(stderr, "Failed to read data from image file\n");
return ZX_ERR_IO;
} else if (read_bytes == 0) {
break;
}
if (auto status = compression.Compress(buffer.data(), read_bytes); status != ZX_OK) {
fprintf(stderr, "Failed to compress data.\n");
return status;
}
}
if (auto status = compression.Finish(); status != ZX_OK) {
return status;
}
if (lseek(fd_.get(), 0, SEEK_SET) != 0) {
fprintf(stderr, "Failed to seek to beginning of the file.\n");
return ZX_ERR_IO;
}
// Write compressed data to file;
const uint8_t* start = static_cast<const uint8_t*>(compression.GetData());
const uint8_t* end = start + compression.GetLength();
while (start < end) {
ssize_t result = write(fd_.get(), start, end - start);
if (result <= 0) {
fprintf(stderr, "Failed to write compressed data to output file.\n");
return ZX_ERR_IO;
}
start += result;
}
if (ftruncate(fd_.get(), compression.GetLength())) {
fprintf(stderr, "Failed to truncate file\n");
return ZX_ERR_IO;
}
return ZX_OK;
}
void FvmContainer::AddNonEmptySegment(size_t start, size_t end) {
non_empty_segments_.push_back({start, end});
}
void FvmContainer::FinalizeNonEmptySegmentsInfo() {
// 1. Sort segments
// 2. Make sure segments are disjoint.
std::sort(non_empty_segments_.begin(), non_empty_segments_.end(),
[](auto& l, auto& r) { return l.start < r.start; });
std::vector<Segment> disjoint;
for (auto& seg : non_empty_segments_) {
if (disjoint.empty() || disjoint.back().end < seg.start) {
disjoint.push_back(seg);
} else {
disjoint.back().end = seg.end;
}
}
non_empty_segments_ = disjoint;
}