| // Copyright 2023 The ChromiumOS Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "dlcservice/metadata/metadata.h" | |
| #include <cstddef> | |
| #include <memory> | |
| #include <optional> | |
| #include <string> | |
| #include <string_view> | |
| #include <utility> | |
| #include <base/files/file_enumerator.h> | |
| #include <base/files/file_path.h> | |
| #include <base/files/file_util.h> | |
| #include <base/json/json_reader.h> | |
| #include <base/json/json_writer.h> | |
| #include <base/logging.h> | |
| #include <base/strings/stringprintf.h> | |
| #include <base/strings/string_split.h> | |
| #include <base/strings/string_util.h> | |
| #include <base/values.h> | |
| #include <brillo/compression/compressor_interface.h> | |
| #include <brillo/compression/zlib_compressor.h> | |
| #include <brillo/strings/string_utils.h> | |
| #include "dlcservice/metadata/metadata_interface.h" | |
| namespace dlcservice::metadata { | |
| namespace { | |
| constexpr char kMetadataFilePattern[] = "_metadata_*"; | |
| constexpr char kManifest[] = "manifest"; | |
| constexpr char kTable[] = "table"; | |
| constexpr char kKeyStringFactoryInstall[] = "factory-install"; | |
| constexpr char kKeyStringPowerwashSafe[] = "powerwash-safe"; | |
| constexpr char kKeyStringPreloadAllowed[] = "preload-allowed"; | |
| } // namespace | |
| const size_t kMaxMetadataFileSize = 4096; | |
| const std::string_view kMetadataPrefix("_metadata_"); | |
| Metadata::Metadata(const base::FilePath& metadata_path, | |
| size_t max_file_size, | |
| std::unique_ptr<brillo::CompressorInterface> compressor, | |
| std::unique_ptr<brillo::CompressorInterface> decompressor) | |
| : metadata_path_(metadata_path), | |
| max_file_size_(max_file_size), | |
| compressor_(std::move(compressor)), | |
| decompressor_(std::move(decompressor)) { | |
| if (!compressor_) { | |
| compressor_ = std::make_unique<brillo::ZlibCompressor>( | |
| brillo::ZlibCompressor::DeflateFormat::Raw); | |
| } | |
| if (!decompressor_) { | |
| decompressor_ = std::make_unique<brillo::ZlibDecompressor>( | |
| brillo::ZlibDecompressor::InflateFormat::Raw); | |
| } | |
| compressed_metadata_.reserve(max_file_size_); | |
| } | |
| bool Metadata::Initialize() { | |
| UpdateFileIds(); | |
| return compressor_->Initialize() && decompressor_->Initialize(); | |
| } | |
| std::optional<Metadata::Entry> Metadata::Get(const DlcId& id) { | |
| if (!LoadMetadata(id)) { | |
| LOG(ERROR) << "Failed to load the metadata data file for DLC=" << id; | |
| return std::nullopt; | |
| } | |
| auto* metadata_val = cache_.FindDict(id); | |
| if (!metadata_val) { | |
| LOG(ERROR) << "Unable to find DLC=" << id << " in the metadata."; | |
| return std::nullopt; | |
| } | |
| Metadata::Entry entry; | |
| auto* manifest_val = metadata_val->FindDict(kManifest); | |
| if (!manifest_val) { | |
| LOG(ERROR) << "Could not get manifest for DLC=" << id; | |
| return std::nullopt; | |
| } | |
| entry.manifest = manifest_val->Clone(); | |
| auto* table_str = metadata_val->FindString(kTable); | |
| if (!table_str) { | |
| LOG(ERROR) << "Could not get table for DLC=" << id; | |
| return std::nullopt; | |
| } | |
| entry.table = *table_str; | |
| return entry; | |
| } | |
| bool Metadata::Set(const DlcId& id, const Metadata::Entry& entry) { | |
| // Load, modify and save the metadata file that contains the target DLC. | |
| if (!LoadMetadata(id)) { | |
| cache_.clear(); | |
| } | |
| cache_.Set(id, base::Value::Dict() | |
| .Set(kManifest, entry.manifest.Clone()) | |
| .Set(kTable, entry.table)); | |
| // Update the `file_ids_` since new file may be created after modification. | |
| if (FlushCache()) { | |
| UpdateFileIds(); | |
| return true; | |
| } | |
| return false; | |
| } | |
| int Metadata::CompressionSize(const std::string& metadata) { | |
| auto compressor_copy = compressor_->Clone(); | |
| if (!compressor_copy) { | |
| return -1; | |
| } | |
| auto data_out = compressor_copy->Process( | |
| brillo::string_utils::GetStringAsBytes(metadata), /*flush=*/true); | |
| if (!data_out) { | |
| return -1; | |
| } | |
| return data_out->size(); | |
| } | |
| bool Metadata::FlushCache() { | |
| // The first of ascending DLC IDs added to current compressed metadata file | |
| // buffer, it will be used as the `file_id` to name the metadata file. | |
| DlcId min_id; | |
| compressor_->Reset(); | |
| compressed_metadata_.clear(); | |
| for (const auto& [id, metadata] : cache_) { | |
| auto metadata_str = base::WriteJson(metadata); | |
| if (!metadata_str) { | |
| LOG(ERROR) << "Failed to convert metadata to JSON for DLC=" << id; | |
| return false; | |
| } | |
| std::string metadata_entry = | |
| base::StringPrintf("\"%s\":%s,", id.c_str(), metadata_str->c_str()); | |
| int comp_size = CompressionSize(metadata_entry); | |
| if (comp_size < 0) { | |
| LOG(ERROR) << "Unable to estimate metadata compression size, flushing " | |
| "metadata failed."; | |
| return false; | |
| } | |
| if (compressed_metadata_.size() + comp_size > max_file_size_) { | |
| // If unable to fit into current file, flush current compressed metadata | |
| // to file and start a new output stream to re-process the metadata. | |
| if (!FlushBuffer(min_id)) { | |
| return false; | |
| } | |
| min_id.clear(); | |
| comp_size = CompressionSize(metadata_entry); | |
| if (comp_size < 0 || comp_size > max_file_size_) { | |
| LOG(ERROR) << "Unable to save metadata for DLC=" << id | |
| << " due to compression size error, size=" << comp_size | |
| << " max_file_size=" << max_file_size_; | |
| return false; | |
| } | |
| } | |
| auto buffer = compressor_->Process( | |
| brillo::string_utils::GetStringAsBytes(metadata_entry), | |
| /*flush=*/false); | |
| if (!buffer) { | |
| LOG(ERROR) << "Unable to compress metadata for DLC=" << id; | |
| return false; | |
| } | |
| compressed_metadata_.append(buffer->begin(), | |
| buffer->begin() + buffer->size()); | |
| if (min_id.empty()) | |
| min_id = id; | |
| } | |
| return FlushBuffer(min_id); | |
| } | |
| bool Metadata::FlushBuffer(const DlcId& file_id) { | |
| // Save to file. | |
| bool ret = true; | |
| if (file_id.size()) { | |
| // Flush all the data to output buffer. | |
| auto buffer = compressor_->Process( | |
| /*data_in=*/brillo::string_utils::GetStringAsBytes(""), /*flush=*/true); | |
| if (buffer) { | |
| compressed_metadata_.append(buffer->begin(), | |
| buffer->begin() + buffer->size()); | |
| } else { | |
| LOG(ERROR) << "Unable to flush the compressed metadata"; | |
| ret = false; | |
| } | |
| ret = | |
| ret && compressed_metadata_.size() && | |
| base::WriteFile( | |
| metadata_path_.Append(std::string(kMetadataPrefix).append(file_id)), | |
| compressed_metadata_); | |
| } | |
| if (!ret) { | |
| LOG(ERROR) << "Failed to save the metadata file=" << file_id; | |
| } | |
| compressor_->Reset(); | |
| compressed_metadata_.clear(); | |
| return ret; | |
| } | |
| void Metadata::UpdateFileIds() { | |
| file_ids_.clear(); | |
| base::FileEnumerator file_enumerator( | |
| metadata_path_, false, base::FileEnumerator::FILES, | |
| base::FilePath::StringType(kMetadataFilePattern)); | |
| for (base::FilePath name = file_enumerator.Next(); !name.empty(); | |
| name = file_enumerator.Next()) { | |
| // Skip `_metadata_`. | |
| if (name.BaseName().value().size() == kMetadataPrefix.size()) | |
| continue; | |
| file_ids_.emplace(name.BaseName().value(), kMetadataPrefix.size()); | |
| } | |
| } | |
| bool Metadata::LoadMetadata(const DlcId& id) { | |
| if (cache_.FindDict(id)) | |
| return true; | |
| // Locate the metadata file by binary search the `file_id`. | |
| auto file_id = file_ids_.upper_bound(id); | |
| if (file_id == file_ids_.begin()) { | |
| LOG(ERROR) << "Unable to find metadata for DLC=" << id; | |
| return false; | |
| } | |
| // Read and decompress metadata. | |
| base::FilePath fp = | |
| metadata_path_.Append(std::string(kMetadataPrefix).append(*(--file_id))); | |
| if (!base::ReadFileToString(fp, &compressed_metadata_)) { | |
| LOG(ERROR) << "Failed to read DLC metadata file=" << fp.value(); | |
| return false; | |
| } | |
| if (!decompressor_->Reset()) { | |
| LOG(ERROR) << "Failed to reset decompressor."; | |
| return false; | |
| } | |
| auto decompressed_metadata = decompressor_->Process( | |
| brillo::string_utils::GetStringAsBytes(compressed_metadata_), | |
| /*flush=*/true); | |
| compressed_metadata_.clear(); | |
| if (!decompressed_metadata) { | |
| return false; | |
| } | |
| // Parse decompressed metadata json. | |
| auto metadata_val = base::JSONReader::ReadAndReturnValueWithError( | |
| std::string("{") | |
| .append( | |
| decompressed_metadata->begin(), | |
| decompressed_metadata->begin() + decompressed_metadata->size()) | |
| .append("}"), | |
| base::JSON_ALLOW_TRAILING_COMMAS); | |
| if (!metadata_val.has_value()) { | |
| LOG(ERROR) << "Could not parse the DLC metadata as JSON. Error: " | |
| << metadata_val.error().message; | |
| return false; | |
| } | |
| if (!metadata_val->is_dict()) { | |
| LOG(ERROR) << "DLC metadata content is not dictionary."; | |
| return false; | |
| } | |
| cache_ = std::move(metadata_val->GetDict()); | |
| return true; | |
| } | |
| DlcIdList Metadata::ListDlcIds(const FilterKey& filter_key, | |
| const base::Value& filter_val) { | |
| auto key_str = FilterKeyToString(filter_key); | |
| if (!key_str) | |
| return {}; | |
| if (const auto& idx = GetIndex(*key_str)) { | |
| return *idx; | |
| } | |
| LOG(WARNING) << "Unable to read the index file for key=" << *key_str | |
| << ", looking up in the DLC metadata."; | |
| // Lookup in metadata. | |
| DlcIdList ids; | |
| for (const auto& file_id : GetFileIds()) { | |
| if (!LoadMetadata(file_id)) { | |
| LOG(WARNING) << "Failed to Load DLC metadata file=" << file_id | |
| << ", skip looking up."; | |
| continue; | |
| } | |
| for (const auto& [id, val] : GetCache()) { | |
| if (filter_key != FilterKey::kNone) { | |
| const auto* manifest_dict = val.GetDict().FindDict(kManifest); | |
| if (!manifest_dict) | |
| continue; | |
| const auto* manifest_val = manifest_dict->Find(*key_str); | |
| if (!manifest_val || *manifest_val != filter_val) | |
| continue; | |
| } | |
| ids.push_back(id); | |
| } | |
| } | |
| return ids; | |
| } | |
| std::optional<DlcIdList> Metadata::GetIndex(const std::string& key) { | |
| if (key.empty()) | |
| return std::nullopt; | |
| // TODO(b/303259102): Better/stricter index file naming to prevent collision. | |
| auto idx_file = base::StringPrintf("_%s_", key.c_str()); | |
| std::replace(idx_file.begin(), idx_file.end(), '-', '_'); | |
| auto idx_path = metadata_path_.Append(idx_file); | |
| std::string idx_str; | |
| if (!base::ReadFileToString(idx_path, &idx_str)) { | |
| return std::nullopt; | |
| } | |
| return base::SplitString(idx_str, base::kWhitespaceASCII, | |
| base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); | |
| } | |
| std::optional<std::string> Metadata::FilterKeyToString( | |
| const Metadata::FilterKey& key_enum) { | |
| switch (key_enum) { | |
| case FilterKey::kNone: | |
| return ""; | |
| case FilterKey::kFactoryInstall: | |
| return kKeyStringFactoryInstall; | |
| case FilterKey::kPowerwashSafe: | |
| return kKeyStringPowerwashSafe; | |
| case FilterKey::kPreloadAllowed: | |
| return kKeyStringPreloadAllowed; | |
| default: | |
| LOG(ERROR) << "Unsupported filter key."; | |
| return std::nullopt; | |
| } | |
| } | |
| } // namespace dlcservice::metadata |