| // 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 |