blob: 8f7d9d63e350b2347387e807ba6f5703f7b2c853 [file] [edit]
// Copyright 2026 Google LLC
//
// 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 "formats/zip/embedder.h"
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
#include "absl/base/nullability.h"
#include "absl/status/status.h"
#include "absl/status/status_macros.h"
#include "absl/status/statusor.h"
#include "absl/strings/string_view.h"
#include "absl/types/span.h"
#include "formats/zip/constants.h"
#include "formats/zip/content_type_stream_util.h"
#include "formats/zip/crc.h"
#include "formats/zip/reader.h"
#include "riegeli/base/byte_fill.h"
#include "riegeli/base/types.h"
#include "riegeli/bytes/reader.h"
#include "riegeli/bytes/writer.h"
#include "riegeli/endian/endian_writing.h"
namespace credentio {
namespace {
// A file entry that has been copied from the source to the destination.
struct FileCopied {
ZipReader::FileEntry entry;
riegeli::Position offset_in_destination;
};
// Copies a file entry from the source to the destination.
absl::Status CopyFileEntry(riegeli::Reader* absl_nonnull source,
const ZipReader::FileEntry& entry,
riegeli::Writer* absl_nonnull destination) {
int64_t file_end = entry.file_range.offset + entry.file_range.length;
int64_t data_descriptor_length = 0;
if (entry.has_data_descriptor) {
source->Seek(file_end);
ABSL_RETURN_IF_ERROR(source->status());
std::string buffer;
source->Read(kZipDataDescriptorSignature.size(), buffer);
ABSL_RETURN_IF_ERROR(source->status());
if (buffer == kZipDataDescriptorSignature) {
data_descriptor_length = kDataDescriptorWithSignatureSize;
} else {
data_descriptor_length = kDataDescriptorSize;
}
}
int64_t length =
file_end + data_descriptor_length - entry.local_file_header_offset;
source->Seek(entry.local_file_header_offset);
ABSL_RETURN_IF_ERROR(source->status());
source->Copy(length, *destination);
ABSL_RETURN_IF_ERROR(source->status());
ABSL_RETURN_IF_ERROR(destination->status());
return absl::OkStatus();
}
// Reads the content types stream from the source, writes the updated stream to
// the destination, including the local file header and the data descriptor (if
// used). Returns a `ContentTypesStreamMetadata` containing the updated metadata
// of the content types stream.
absl::StatusOr<ContentTypesStreamMetadata> WriteUpdatedContentTypesStream(
riegeli::Reader* absl_nonnull source,
const ZipReader::FileEntry& content_types_file_entry,
riegeli::Writer* absl_nonnull destination) {
if (content_types_file_entry.has_data_descriptor) {
// Copy the original local file header since they don't need to be updated.
source->Seek(content_types_file_entry.local_file_header_offset);
ABSL_RETURN_IF_ERROR(source->status());
source->Copy(kLocalFileHeaderFixedSize + kOoxmlContentTypesFileName.size() +
content_types_file_entry.extra_field_length,
*destination);
ABSL_RETURN_IF_ERROR(source->status());
ABSL_RETURN_IF_ERROR(destination->status());
// Read and update the original content types stream from the source.
ABSL_ASSIGN_OR_RETURN(
ContentTypesStream content_types_stream,
ReadContentTypesStream(source, content_types_file_entry));
ABSL_RETURN_IF_ERROR(UpdateContentTypesStream(
content_types_stream, kZipC2paManifestMimeType, kZipManifestFileName));
return WriteContentTypesStream(content_types_stream,
content_types_file_entry.has_data_descriptor,
destination);
} else {
return absl::UnimplementedError(
"Content types stream without a data descriptor is not supported.");
}
}
// Writes the central directory header for the updated content types stream to
// the destination.
absl::Status WriteUpdatedContentTypesStreamCentralDirectoryHeader(
riegeli::Reader* absl_nonnull source,
const ZipReader::FileEntry& original_file_entry,
const ContentTypesStreamMetadata& content_types_stream_metadata,
int64_t updated_local_file_header_offset,
riegeli::Writer* absl_nonnull destination) {
// Copy the following fields from the original central directory header:
// - Central directory file header signature (4 bytes)
// - Version made by (2 bytes)
// - Version needed to extract (2 bytes)
// - General purpose bit flag (2 bytes)
// - Compression method (2 bytes)
// - File modification time (2 bytes)
// - File modification date (2 bytes)
source->Seek(original_file_entry.central_directory_header_offset);
ABSL_RETURN_IF_ERROR(source->status());
source->Copy(16, *destination);
ABSL_RETURN_IF_ERROR(source->status());
ABSL_RETURN_IF_ERROR(destination->status());
// CRC-32 (4 bytes, little-endian)
riegeli::WriteLittleEndian<uint32_t>(content_types_stream_metadata.crc32,
*destination);
ABSL_RETURN_IF_ERROR(destination->status());
// Compressed size (4 bytes, little-endian)
riegeli::WriteLittleEndian<uint32_t>(
content_types_stream_metadata.compressed_size, *destination);
ABSL_RETURN_IF_ERROR(destination->status());
// Uncompressed size (4 bytes, little-endian)
riegeli::WriteLittleEndian<uint32_t>(
content_types_stream_metadata.uncompressed_size, *destination);
ABSL_RETURN_IF_ERROR(destination->status());
// Copy the remaining fields from the original central directory header:
// - File name length (2 bytes)
// - Extra field length (2 bytes)
// - File comment length (2 bytes)
// - Disk number start (2 bytes)
// - Internal file attributes (2 bytes)
// - External file attributes (4 bytes)
source->Seek(original_file_entry.central_directory_header_offset + 28);
ABSL_RETURN_IF_ERROR(source->status());
source->Copy(14, *destination);
ABSL_RETURN_IF_ERROR(source->status());
ABSL_RETURN_IF_ERROR(destination->status());
// Local file header offset (4 bytes, little-endian)
riegeli::WriteLittleEndian<uint32_t>(updated_local_file_header_offset,
*destination);
ABSL_RETURN_IF_ERROR(destination->status());
// File name
destination->Write(kOoxmlContentTypesFileName);
ABSL_RETURN_IF_ERROR(destination->status());
// Extra field and File comment data
int64_t extra_and_comment_length = original_file_entry.extra_field_length +
original_file_entry.file_comment_length;
if (extra_and_comment_length > 0) {
source->Seek(original_file_entry.central_directory_header_offset +
kCentralDirectoryHeaderFixedSize +
original_file_entry.file_name.size());
ABSL_RETURN_IF_ERROR(source->status());
source->Copy(extra_and_comment_length, *destination);
ABSL_RETURN_IF_ERROR(source->status());
ABSL_RETURN_IF_ERROR(destination->status());
}
return absl::OkStatus();
}
// Writes the local file header for the manifest store to the destination.
absl::Status WriteManifestLocalFileHeader(
int64_t manifest_store_size, riegeli::Writer* absl_nonnull destination) {
// Local file header signature (4 bytes)
destination->Write(kZipLocalFileHeaderSignature);
ABSL_RETURN_IF_ERROR(destination->status());
// Version needed to extract (2 bytes)
riegeli::WriteLittleEndian<uint16_t>(10, *destination);
ABSL_RETURN_IF_ERROR(destination->status());
// Zero-filled fields:
// - General purpose bit flag (2 bytes)
// - Compression method (2 bytes, 0 means no compression)
// - File modification time (2 bytes)
// - File modification date (2 bytes)
// - CRC-32 (4 bytes, placeholder)
destination->Write(riegeli::ByteFill(12));
ABSL_RETURN_IF_ERROR(destination->status());
// Compressed size (4 bytes, little-endian)
riegeli::WriteLittleEndian<uint32_t>(manifest_store_size, *destination);
ABSL_RETURN_IF_ERROR(destination->status());
// Uncompressed size (4 bytes, little-endian)
riegeli::WriteLittleEndian<uint32_t>(manifest_store_size, *destination);
ABSL_RETURN_IF_ERROR(destination->status());
// File name length (2 bytes, little-endian)
riegeli::WriteLittleEndian<uint16_t>(kZipManifestFileName.size(),
*destination);
ABSL_RETURN_IF_ERROR(destination->status());
// Zero-filled extra field length (2 bytes)
destination->Write(riegeli::ByteFill(2));
ABSL_RETURN_IF_ERROR(destination->status());
// File name
destination->Write(kZipManifestFileName);
ABSL_RETURN_IF_ERROR(destination->status());
return absl::OkStatus();
}
// Writes the central directory header for the manifest store to the
// destination.
absl::Status WriteManifestCentralDirectoryHeader(
uint32_t manifest_store_size,
uint32_t manifest_store_local_file_header_offset,
riegeli::Writer* absl_nonnull destination) {
// Central directory file header signature (4 bytes)
destination->Write(kZipCentralDirectorySignature);
ABSL_RETURN_IF_ERROR(destination->status());
// Version made by (2 bytes)
riegeli::WriteLittleEndian<uint16_t>(20, *destination); // Version 2.0
ABSL_RETURN_IF_ERROR(destination->status());
// Version needed to extract (2 bytes)
riegeli::WriteLittleEndian<uint16_t>(10, *destination); // Version 1.0
ABSL_RETURN_IF_ERROR(destination->status());
// Zero-filled fields:
// - General purpose bit flag (2 bytes)
// - Compression method (2 bytes, 0 means no compression)
// - File modification time (2 bytes)
// - File modification date (2 bytes)
// - CRC-32 (4 bytes, placeholder)
destination->Write(riegeli::ByteFill(12));
ABSL_RETURN_IF_ERROR(destination->status());
// Compressed size (4 bytes, little-endian)
riegeli::WriteLittleEndian<uint32_t>(manifest_store_size, *destination);
ABSL_RETURN_IF_ERROR(destination->status());
// Uncompressed size (4 bytes, little-endian)
riegeli::WriteLittleEndian<uint32_t>(manifest_store_size, *destination);
ABSL_RETURN_IF_ERROR(destination->status());
// File name length (2 bytes, little-endian)
riegeli::WriteLittleEndian<uint16_t>(kZipManifestFileName.size(),
*destination);
ABSL_RETURN_IF_ERROR(destination->status());
// Zero-filled fields:
// - Extra field length (2 bytes)
// - File comment length (2 bytes)
// - Disk number start (2 bytes)
// - Internal file attributes (2 bytes)
// - External file attributes (4 bytes)
destination->Write(riegeli::ByteFill(12));
ABSL_RETURN_IF_ERROR(destination->status());
// Local file header offset (4 bytes, little-endian)
riegeli::WriteLittleEndian<uint32_t>(manifest_store_local_file_header_offset,
*destination);
ABSL_RETURN_IF_ERROR(destination->status());
// File name
destination->Write(kZipManifestFileName);
ABSL_RETURN_IF_ERROR(destination->status());
return absl::OkStatus();
}
// Writes the central directory header for a copied file entry to the
// destination.
absl::Status WriteCentralDirectoryHeader(
riegeli::Reader* absl_nonnull source, const FileCopied& file_copied,
riegeli::Writer* absl_nonnull destination) {
source->Seek(file_copied.entry.central_directory_header_offset);
ABSL_RETURN_IF_ERROR(source->status());
int64_t total_cd_length = kCentralDirectoryHeaderFixedSize +
file_copied.entry.file_name.size() +
file_copied.entry.extra_field_length +
file_copied.entry.file_comment_length;
if (file_copied.offset_in_destination ==
file_copied.entry.local_file_header_offset) {
source->Copy(total_cd_length, *destination);
ABSL_RETURN_IF_ERROR(source->status());
ABSL_RETURN_IF_ERROR(destination->status());
} else {
source->Copy(kCentralDirectoryHeaderFixedSize - 4, *destination);
ABSL_RETURN_IF_ERROR(source->status());
ABSL_RETURN_IF_ERROR(destination->status());
riegeli::WriteLittleEndian<uint32_t>(file_copied.offset_in_destination,
*destination);
ABSL_RETURN_IF_ERROR(destination->status());
source->Skip(4);
ABSL_RETURN_IF_ERROR(source->status());
int64_t remaining_length = file_copied.entry.file_name.size() +
file_copied.entry.extra_field_length +
file_copied.entry.file_comment_length;
source->Copy(remaining_length, *destination);
ABSL_RETURN_IF_ERROR(source->status());
ABSL_RETURN_IF_ERROR(destination->status());
}
return absl::OkStatus();
}
// Updates the End of Central Directory (EOCD) record with the new central
// directory offset, length and entry count.
absl::Status UpdateEndOfCentralDirectory(
riegeli::Reader* absl_nonnull source, uint32_t central_directory_offset,
uint32_t central_directory_length, uint16_t entry_count,
riegeli::Writer* absl_nonnull destination) {
// The first 8 bytes of the EOCD don't need to be updated.
auto source_size = source->Size();
if (!source_size.has_value()) return source->status();
source->Seek(*source_size - kEocdRecordSize);
ABSL_RETURN_IF_ERROR(source->status());
source->Copy(8, *destination);
ABSL_RETURN_IF_ERROR(source->status());
ABSL_RETURN_IF_ERROR(destination->status());
// Total number of entries in the central directory on this disk (2 bytes,
// little-endian)
riegeli::WriteLittleEndian<uint16_t>(entry_count, *destination);
ABSL_RETURN_IF_ERROR(destination->status());
// Total number of entries in the central directory (2 bytes, little-endian)
riegeli::WriteLittleEndian<uint16_t>(entry_count, *destination);
ABSL_RETURN_IF_ERROR(destination->status());
// Size of the central directory (4 bytes, little-endian)
riegeli::WriteLittleEndian<uint32_t>(central_directory_length, *destination);
ABSL_RETURN_IF_ERROR(destination->status());
// Offset of the start of the central directory (4 bytes, little-endian)
riegeli::WriteLittleEndian<uint32_t>(central_directory_offset, *destination);
ABSL_RETURN_IF_ERROR(destination->status());
// Zero-filled comment length (2 bytes)
destination->Write(riegeli::ByteFill(2));
ABSL_RETURN_IF_ERROR(destination->status());
return absl::OkStatus();
}
} // namespace
absl::StatusOr<ZipEmbedder::CopyAndAllocateResult> ZipEmbedder::CopyAndAllocate(
riegeli::Reader* absl_nonnull source,
absl::Span<const ZipReader::FileEntry> file_entries,
int64_t manifest_store_size,
riegeli::Writer* absl_nonnull destination) const {
ABSL_ASSIGN_OR_RETURN(std::vector<ZipReader::FileEntry> sorted_entries,
ZipReader::SortFileEntries(file_entries));
// Step 1: Copy all the files in the ZIP archive from the source to the
// destination, except the content types stream.
std::vector<FileCopied> files_copied;
std::optional<ZipReader::FileEntry> content_types_file_entry;
for (const auto& entry : sorted_entries) {
if (entry.file_name == kOoxmlContentTypesFileName) {
content_types_file_entry = entry;
continue;
}
if (entry.file_name == kZipManifestFileName) {
return absl::AlreadyExistsError("Manifest store already exists.");
}
files_copied.push_back(
{.entry = entry, .offset_in_destination = destination->pos()});
ABSL_RETURN_IF_ERROR(CopyFileEntry(source, entry, destination));
}
// Step 2: Write the updated content types stream to the destination.
if (!content_types_file_entry.has_value()) {
return absl::NotFoundError("Content types stream not found.");
}
auto content_types_stream_output_offset = destination->pos();
ABSL_ASSIGN_OR_RETURN(ContentTypesStreamMetadata content_types_metadata,
WriteUpdatedContentTypesStream(
source, *content_types_file_entry, destination));
// Step 3: Write the zero-filled manifest store to the destination.
auto manifest_store_local_file_header_offset = destination->pos();
ABSL_RETURN_IF_ERROR(
WriteManifestLocalFileHeader(manifest_store_size, destination));
auto manifest_store_offset = destination->pos();
destination->Write(riegeli::ByteFill(manifest_store_size));
ABSL_RETURN_IF_ERROR(destination->status());
// Step 4: Write the central directory headers.
auto central_directory_offset = destination->pos();
for (const auto& file : files_copied) {
ABSL_RETURN_IF_ERROR(
WriteCentralDirectoryHeader(source, file, destination));
}
ABSL_RETURN_IF_ERROR(WriteUpdatedContentTypesStreamCentralDirectoryHeader(
source, *content_types_file_entry, content_types_metadata,
content_types_stream_output_offset, destination));
auto manifest_store_central_directory_header_offset = destination->pos();
ABSL_RETURN_IF_ERROR(WriteManifestCentralDirectoryHeader(
manifest_store_size, manifest_store_local_file_header_offset,
destination));
// Step 5: Update the End of Central Directory (EOCD).
ABSL_RETURN_IF_ERROR(UpdateEndOfCentralDirectory(
source, central_directory_offset,
/*central_directory_length=*/destination->pos() -
central_directory_offset,
files_copied.size() + 2, destination));
destination->Flush();
ABSL_RETURN_IF_ERROR(destination->status());
return CopyAndAllocateResult{
.manifest_store_offset = manifest_store_offset,
.manifest_store_central_directory_header_offset =
manifest_store_central_directory_header_offset,
.central_directory_offset = central_directory_offset,
};
}
absl::Status ZipEmbedder::Embed(
absl::string_view manifest_store,
const CopyAndAllocateResult& copy_and_allocate_result,
riegeli::Writer* absl_nonnull destination) const {
if (!destination->SupportsRandomAccess()) {
return absl::InvalidArgumentError(
"ZipEmbedder::Embed requires a riegeli::Writer that supports random "
"access.");
}
uint32_t crc32 = ZipFileCrc(manifest_store);
int64_t local_file_header_crc_offset =
copy_and_allocate_result.manifest_store_offset -
kZipManifestFileName.size() - 16;
int64_t central_directory_header_crc_offset =
copy_and_allocate_result.manifest_store_central_directory_header_offset +
16;
// Replace the zero-filled manifest store placeholder with the actual manifest
// store. Also update the CRC-32 fields in both the local file header and the
// central directory header with the actual CRC-32 of the manifest store.
destination->Seek(local_file_header_crc_offset);
ABSL_RETURN_IF_ERROR(destination->status());
riegeli::WriteLittleEndian<uint32_t>(crc32, *destination);
ABSL_RETURN_IF_ERROR(destination->status());
destination->Seek(copy_and_allocate_result.manifest_store_offset);
ABSL_RETURN_IF_ERROR(destination->status());
destination->Write(manifest_store);
ABSL_RETURN_IF_ERROR(destination->status());
destination->Seek(central_directory_header_crc_offset);
ABSL_RETURN_IF_ERROR(destination->status());
riegeli::WriteLittleEndian<uint32_t>(crc32, *destination);
return destination->status();
}
} // namespace credentio