| // 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/writer.h" | |
| #include <cstdint> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/base/nullability.h" | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_macros.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/string_view.h" | |
| #include "crypto/algorithms.h" | |
| #include "formats/zip/embedder.h" | |
| #include "formats/zip/hasher_data.h" | |
| #include "formats/zip/reader.h" | |
| #include "proto/generator.pb.h" | |
| #include "riegeli/bytes/reader.h" | |
| #include "riegeli/bytes/writer.h" | |
| namespace credentio { | |
| absl::Status ZipWriter::Write(GenerateManifestStoreRequest& request, | |
| riegeli::Reader* absl_nonnull source, | |
| riegeli::Writer* absl_nonnull destination) { | |
| if (request.has_hard_binding()) { | |
| return absl::InvalidArgumentError( | |
| "request must not include `hard_binding` (binding is computed by " | |
| "ZipWriter)"); | |
| } | |
| // Phase 2 below reads back and patches `destination`. Check this before | |
| // writing anything, so that an unsuitable `destination` is left untouched. | |
| if (!destination->SupportsReadMode()) { | |
| return absl::InvalidArgumentError( | |
| "ZipWriter::Write requires a riegeli::Writer that supports read mode."); | |
| } | |
| if (!destination->SupportsRandomAccess()) { | |
| return absl::InvalidArgumentError( | |
| "ZipWriter::Write requires a riegeli::Writer that supports random " | |
| "access."); | |
| } | |
| ABSL_ASSIGN_OR_RETURN(auto reader, ZipReader::Create(source)); | |
| std::vector<ZipReader::FileEntry> file_entries; | |
| while (reader->HasNext()) { | |
| ABSL_ASSIGN_OR_RETURN(auto entry, reader->Next()); | |
| file_entries.push_back(entry); | |
| } | |
| // The two-phase approach is required for ZIP-based assets: The sections to be | |
| // hashed can't be finalized until the manifest store size is known and | |
| // content types stream is updated. | |
| // Phase 1: Copy the asset to the destination, update the content types | |
| // stream, compute preliminary hard binding, produce a manifest store | |
| // template, and embed a zero-filled placeholder in the asset. | |
| HashAlgorithm alg = HashAlgorithm::kSha256; | |
| ABSL_ASSIGN_OR_RETURN( | |
| *request.mutable_hard_binding()->mutable_collection_data_hash(), | |
| CollectionDataHashTemplate(file_entries, alg)); | |
| ABSL_ASSIGN_OR_RETURN(ManifestStoreTemplate manifest_store_template, | |
| generator_.GenerateManifestStoreTemplate(request)); | |
| constexpr int64_t kMaxManifestStoreSizeBytes = 100 * 1024 * 1024; // 100 MiB | |
| if (manifest_store_template.manifest_store_size() <= 0 || | |
| manifest_store_template.manifest_store_size() > | |
| kMaxManifestStoreSizeBytes) { | |
| return absl::InvalidArgumentError( | |
| absl::StrCat("invalid manifest store template size: ", | |
| manifest_store_template.manifest_store_size())); | |
| } | |
| ZipEmbedder embedder; | |
| ABSL_ASSIGN_OR_RETURN( | |
| auto copy_and_allocate_result, | |
| embedder.CopyAndAllocate(source, file_entries, | |
| manifest_store_template.manifest_store_size(), | |
| destination)); | |
| // Phase 2: Compute final hard binding based on the updated content, finalize | |
| // the manifest store, and embed the final manifest store in the asset. | |
| riegeli::Reader* phase2_reader = destination->ReadMode(0); | |
| if (phase2_reader == nullptr) { | |
| return absl::InternalError("failed to switch to read mode"); | |
| } | |
| ABSL_ASSIGN_OR_RETURN(auto phase2_zip_reader, | |
| ZipReader::Create(phase2_reader)); | |
| std::vector<ZipReader::FileEntry> phase2_file_entries; | |
| while (phase2_zip_reader->HasNext()) { | |
| ABSL_ASSIGN_OR_RETURN(auto entry, phase2_zip_reader->Next()); | |
| phase2_file_entries.push_back(entry); | |
| } | |
| FinalizeManifestStoreRequest finalize_request; | |
| *finalize_request.mutable_manifest_store_template() = | |
| std::move(manifest_store_template); | |
| ABSL_ASSIGN_OR_RETURN( | |
| *finalize_request.mutable_hard_binding()->mutable_collection_data_hash(), | |
| DigestCollectionDataHash( | |
| phase2_reader, alg, phase2_file_entries, | |
| copy_and_allocate_result.central_directory_offset)); | |
| ABSL_ASSIGN_OR_RETURN(auto response, | |
| generator_.FinalizeManifestStore(finalize_request)); | |
| if (response.manifest_store().size() != | |
| finalize_request.manifest_store_template().manifest_store_size()) { | |
| return absl::InternalError("manifest store size does not match template"); | |
| } | |
| ABSL_RETURN_IF_ERROR(embedder.Embed(response.manifest_store(), | |
| copy_and_allocate_result, destination)); | |
| return absl::OkStatus(); | |
| } | |
| } // namespace credentio |