blob: b65d2ac008ea2c7cea7a7e944aace1b5c1fc40de [file] [log] [blame] [edit]
// 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