#include "chromeos/printing/ppd_provider.h"
#include <algorithm>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "base/containers/contains.h"
#include "base/containers/queue.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/memory/scoped_refptr.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/scoped_blocking_call.h"
#include "base/time/time.h"
#include "chromeos/printing/ppd_cache.h"
#include "chromeos/printing/ppd_metadata_manager.h"
#include "chromeos/printing/printer_config_cache.h"
#include "chromeos/printing/printer_configuration.h"
#include "chromeos/printing/printing_constants.h"
#include "chromeos/printing/remote_ppd_fetcher.h"
#include "components/device_event_log/device_event_log.h"
#include "net/base/filename_util.h"
namespace chromeos {
namespace {
constexpr base::TimeDelta kMaxDataAge = base::Minutes(30LL);
bool PpdReferenceIsWellFormed(const Printer::PpdReference& reference) {
int filled_fields = 0;
if (!reference.user_supplied_ppd_url.empty()) {
++filled_fields;
GURL tmp_url(reference.user_supplied_ppd_url);
const bool is_http = tmp_url.SchemeIsHTTPOrHTTPS();
const bool is_file = tmp_url.SchemeIs("file");
const bool has_supported_scheme = is_http || is_file;
if (!tmp_url.is_valid() || !has_supported_scheme) {
LOG(ERROR) << "Invalid url for a user-supplied ppd: "
<< reference.user_supplied_ppd_url;
return false;
}
}
if (!reference.effective_make_and_model.empty()) {
++filled_fields;
}
if (!std::ranges::all_of(reference.effective_make_and_model,
[](char c) { return !base::IsAsciiUpper(c); })) {
return false;
}
return filled_fields == 1;
}
std::string PpdPathInServingRoot(std::string_view ppd_basename) {
return base::StrCat({"ppds_for_metadata_v3/", ppd_basename});
}
bool SupportsGenericZebraPPD(const PrinterSearchData& search_data) {
return search_data.printer_id.make().starts_with("Zebra") &&
base::Contains(search_data.printer_id.model(), "ZPL");
}
class PpdProviderImpl : public PpdProvider {
public:
PpdProviderImpl(const base::Version& current_version,
scoped_refptr<PpdCache> cache,
std::unique_ptr<PpdMetadataManager> metadata_manager,
std::unique_ptr<PrinterConfigCache> config_cache,
std::unique_ptr<RemotePpdFetcher> remote_ppd_fetcher)
: version_(current_version),
ppd_cache_(cache),
metadata_manager_(std::move(metadata_manager)),
config_cache_(std::move(config_cache)),
remote_ppd_fetcher_(std::move(remote_ppd_fetcher)),
file_task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
{base::TaskPriority::USER_VISIBLE, base::MayBlock(),
base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})) {}
void ResolveManufacturers(ResolveManufacturersCallback cb) override {
metadata_manager_->GetManufacturers(kMaxDataAge, std::move(cb));
}
void ResolvePrinters(const std::string& manufacturer,
ResolvePrintersCallback cb) override {
PpdMetadataManager::GetPrintersCallback manager_callback =
base::BindOnce(&PpdProviderImpl::OnPrintersGotten,
weak_factory_.GetWeakPtr(), std::move(cb));
metadata_manager_->GetPrinters(manufacturer, kMaxDataAge,
std::move(manager_callback));
}
void ResolvePpdReference(const PrinterSearchData& search_data,
ResolvePpdReferenceCallback cb) override {
PrinterSearchData lowercased_search_data(search_data);
for (std::string& emm : lowercased_search_data.make_and_model) {
emm = base::ToLowerASCII(emm);
}
if (SupportsGenericZebraPPD(lowercased_search_data)) {
lowercased_search_data.make_and_model.clear();
lowercased_search_data.make_and_model.push_back(
"zebra zpl label printer");
}
ResolvePpdReferenceContext context(lowercased_search_data, std::move(cb));
if (!lowercased_search_data.make_and_model.empty()) {
auto callback = base::BindOnce(
&PpdProviderImpl::TryToResolvePpdReferenceFromForwardIndices,
weak_factory_.GetWeakPtr(), std::move(context));
metadata_manager_->FindAllEmmsAvailableInIndex(
lowercased_search_data.make_and_model, kMaxDataAge,
std::move(callback));
return;
}
TryToResolvePpdReferenceFromUsbIndices(std::move(context));
}
void ResolvePpd(const Printer::PpdReference& reference,
ResolvePpdCallback cb) override {
Printer::PpdReference lowercased_reference(reference);
lowercased_reference.effective_make_and_model =
base::ToLowerASCII(lowercased_reference.effective_make_and_model);
if (!PpdReferenceIsWellFormed(lowercased_reference)) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(cb),
CallbackResultCode::INTERNAL_ERROR, ""));
return;
}
if (!lowercased_reference.user_supplied_ppd_url.empty()) {
ResolveUserSuppliedPpd(lowercased_reference, std::move(cb));
return;
}
std::vector<std::string> target_emm = {
lowercased_reference.effective_make_and_model};
auto callback =
base::BindOnce(&PpdProviderImpl::OnPpdBasenameSoughtFromForwardIndex,
weak_factory_.GetWeakPtr(),
std::move(lowercased_reference), std::move(cb));
metadata_manager_->FindAllEmmsAvailableInIndex(target_emm, kMaxDataAge,
std::move(callback));
}
void ReverseLookup(const std::string& effective_make_and_model,
ReverseLookupCallback cb) override {
std::string lowercased_effective_make_and_model =
base::ToLowerASCII(effective_make_and_model);
metadata_manager_->SplitMakeAndModel(lowercased_effective_make_and_model,
kMaxDataAge, std::move(cb));
}
void ResolvePpdLicense(std::string_view effective_make_and_model,
ResolvePpdLicenseCallback cb) override {
const std::string lowercased_effective_make_and_model =
base::ToLowerASCII(effective_make_and_model);
auto callback = base::BindOnce(
&PpdProviderImpl::FindLicenseForEmm, weak_factory_.GetWeakPtr(),
lowercased_effective_make_and_model, std::move(cb));
metadata_manager_->FindAllEmmsAvailableInIndex(
{lowercased_effective_make_and_model}, kMaxDataAge,
std::move(callback));
}
protected:
~PpdProviderImpl() override = default;
private:
struct ResolvePpdReferenceContext {
ResolvePpdReferenceContext(const PrinterSearchData& search_data_arg,
ResolvePpdReferenceCallback cb_arg)
: search_data(search_data_arg), cb(std::move(cb_arg)) {}
~ResolvePpdReferenceContext() = default;
ResolvePpdReferenceContext(ResolvePpdReferenceContext&& other) = default;
ResolvePpdReferenceContext& operator=(ResolvePpdReferenceContext&& other) =
default;
PrinterSearchData search_data;
ResolvePpdReferenceCallback cb;
};
enum class ResolvedPpdOrigin {
kFromServingRoot,
kFromUserSuppliedUrl,
kFromPpdCache,
};
static std::string FetchFile(const GURL& ppd_url) {
DCHECK(ppd_url.is_valid());
DCHECK(ppd_url.SchemeIs("file"));
base::ScopedBlockingCall scoped_blocking_call(
FROM_HERE, base::BlockingType::MAY_BLOCK);
base::FilePath path;
if (!net::FileURLToFilePath(ppd_url, &path)) {
LOG(ERROR) << "Not a valid file URL.";
return "";
}
std::string file_contents;
if (!base::ReadFileToString(path, &file_contents)) {
return "";
}
return file_contents;
}
bool CurrentVersionSatisfiesRestrictions(
const Restrictions& restrictions) const {
if ((restrictions.min_milestone.has_value() &&
restrictions.min_milestone.value().IsValid() &&
version_ < restrictions.min_milestone) ||
(restrictions.max_milestone.has_value() &&
restrictions.max_milestone.value().IsValid() &&
version_ > restrictions.max_milestone)) {
return false;
}
return true;
}
void OnPrintersGotten(ResolvePrintersCallback cb,
bool succeeded,
const ParsedPrinters& printers) {
if (!succeeded) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(cb), CallbackResultCode::SERVER_ERROR,
ResolvedPrintersList()));
return;
}
ResolvedPrintersList printers_available_to_our_version;
for (const ParsedPrinter& printer : printers) {
if (CurrentVersionSatisfiesRestrictions(printer.restrictions)) {
Printer::PpdReference ppd_reference;
ppd_reference.effective_make_and_model =
printer.effective_make_and_model;
printers_available_to_our_version.push_back(ResolvedPpdReference{
printer.user_visible_printer_name, ppd_reference});
}
}
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(cb), CallbackResultCode::SUCCESS,
printers_available_to_our_version));
}
const ParsedIndexLeaf* FirstAllowableParsedIndexLeaf(
std::string_view effective_make_and_model,
const base::flat_map<std::string, ParsedIndexValues>&
forward_index_subset) const {
const auto& iter = forward_index_subset.find(effective_make_and_model);
if (iter == forward_index_subset.end()) {
return nullptr;
}
for (const ParsedIndexLeaf& index_leaf : iter->second.values) {
if (CurrentVersionSatisfiesRestrictions(index_leaf.restrictions)) {
return &index_leaf;
}
}
return nullptr;
}
static void SuccessfullyResolvePpdReferenceWithEmm(
std::string_view effective_make_and_model,
ResolvePpdReferenceCallback cb) {
Printer::PpdReference reference;
reference.effective_make_and_model = std::string(effective_make_and_model);
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(cb), CallbackResultCode::SUCCESS,
std::move(reference), ""));
}
static void FailToResolvePpdReferenceWithUsbManufacturer(
ResolvePpdReferenceCallback cb,
const std::string& usb_manufacturer) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(cb), CallbackResultCode::NOT_FOUND,
Printer::PpdReference(), usb_manufacturer));
}
void FailToResolvePpdReference(ResolvePpdReferenceContext context) {
if (context.search_data.discovery_type ==
PrinterSearchData::PrinterDiscoveryType::kUsb) {
auto callback = base::BindOnce(
&PpdProviderImpl::FailToResolvePpdReferenceWithUsbManufacturer,
std::move(context.cb));
metadata_manager_->GetUsbManufacturerName(
context.search_data.usb_vendor_id, kMaxDataAge, std::move(callback));
return;
}
FailToResolvePpdReferenceWithUsbManufacturer(std::move(context.cb),
"");
}
void OnForwardIndicesSearchedForUsbEmm(
ResolvePpdReferenceContext context,
const std::string& effective_make_and_model_from_usb_index,
const base::flat_map<std::string, ParsedIndexValues>&
forward_index_results) {
const ParsedIndexLeaf* const index_leaf = FirstAllowableParsedIndexLeaf(
effective_make_and_model_from_usb_index, forward_index_results);
if (index_leaf) {
SuccessfullyResolvePpdReferenceWithEmm(
effective_make_and_model_from_usb_index, std::move(context.cb));
return;
}
FailToResolvePpdReference(std::move(context));
}
void OnUsbIndicesSearched(ResolvePpdReferenceContext context,
const std::string& effective_make_and_model) {
if (!effective_make_and_model.empty()) {
auto callback =
base::BindOnce(&PpdProviderImpl::OnForwardIndicesSearchedForUsbEmm,
weak_factory_.GetWeakPtr(), std::move(context),
effective_make_and_model);
metadata_manager_->FindAllEmmsAvailableInIndex(
{effective_make_and_model}, kMaxDataAge, std::move(callback));
return;
}
FailToResolvePpdReference(std::move(context));
}
void TryToResolvePpdReferenceFromUsbIndices(
ResolvePpdReferenceContext context) {
const int vendor_id = context.search_data.usb_vendor_id;
const int product_id = context.search_data.usb_product_id;
if (vendor_id && product_id) {
auto callback =
base::BindOnce(&PpdProviderImpl::OnUsbIndicesSearched,
weak_factory_.GetWeakPtr(), std::move(context));
metadata_manager_->FindDeviceInUsbIndex(vendor_id, product_id,
kMaxDataAge, std::move(callback));
return;
}
FailToResolvePpdReference(std::move(context));
}
void TryToResolvePpdReferenceFromForwardIndices(
ResolvePpdReferenceContext context,
const base::flat_map<std::string, ParsedIndexValues>&
forward_index_results) {
for (std::string_view effective_make_and_model :
context.search_data.make_and_model) {
const ParsedIndexLeaf* const index_leaf = FirstAllowableParsedIndexLeaf(
effective_make_and_model, forward_index_results);
if (!index_leaf) {
continue;
}
SuccessfullyResolvePpdReferenceWithEmm(effective_make_and_model,
std::move(context.cb));
return;
}
TryToResolvePpdReferenceFromUsbIndices(std::move(context));
}
void StorePpdWithContents(const std::string& ppd_contents,
std::optional<std::string> ppd_basename,
ResolvedPpdOrigin ppd_origin,
Printer::PpdReference reference) {
switch (ppd_origin) {
case ResolvedPpdOrigin::kFromPpdCache:
return;
case ResolvedPpdOrigin::kFromServingRoot:
DCHECK(ppd_basename.has_value());
DCHECK(!ppd_basename->empty());
DCHECK(!reference.effective_make_and_model.empty());
ppd_cache_->Store(PpdBasenameToCacheKey(ppd_basename.value()),
ppd_contents);
ppd_cache_->Store(PpdReferenceToCacheKey(reference),
ppd_basename.value());
break;
case ResolvedPpdOrigin::kFromUserSuppliedUrl:
DCHECK(!reference.user_supplied_ppd_url.empty());
ppd_cache_->Store(PpdReferenceToCacheKey(reference), ppd_contents);
break;
}
}
void ResolvePpdWithContents(ResolvedPpdOrigin ppd_origin,
std::optional<std::string> ppd_basename,
std::string ppd_contents,
Printer::PpdReference reference,
ResolvePpdCallback cb) {
DCHECK(!ppd_contents.empty());
if (ppd_contents.size() > kMaxPpdSizeBytes) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(cb), CallbackResultCode::PPD_TOO_LARGE, ""));
return;
}
StorePpdWithContents(ppd_contents, std::move(ppd_basename), ppd_origin,
std::move(reference));
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(cb), CallbackResultCode::SUCCESS,
std::move(ppd_contents)));
}
void OnPpdFetchedFromServingRoot(
Printer::PpdReference reference,
ResolvePpdCallback cb,
const PrinterConfigCache::FetchResult& result) {
if (!result.succeeded || result.contents.empty()) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(cb), CallbackResultCode::SERVER_ERROR, ""));
return;
}
ResolvePpdWithContents(ResolvedPpdOrigin::kFromServingRoot,
result.key,
result.contents,
std::move(reference), std::move(cb));
}
void OnPpdBasenameSoughtInPpdCache(Printer::PpdReference reference,
ResolvePpdCallback cb,
const PpdCache::FindResult& result) {
if (!result.success || result.contents.empty()) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(cb), CallbackResultCode::NOT_FOUND, ""));
return;
}
std::string cache_key = PpdBasenameToCacheKey(result.contents);
ppd_cache_->Find(
cache_key,
base::BindOnce(&PpdProviderImpl::OnPpdFromServingRootSoughtInPpdCache,
weak_factory_.GetWeakPtr(),
result.contents, std::move(reference),
std::move(cb)));
}
void OnPpdFromServingRootSoughtInPpdCache(
const std::string& ppd_basename,
Printer::PpdReference reference,
ResolvePpdCallback cb,
const PpdCache::FindResult& result) {
if (!result.success || result.contents.empty()) {
auto callback = base::BindOnce(
&PpdProviderImpl::OnPpdFetchedFromServingRoot,
weak_factory_.GetWeakPtr(), std::move(reference), std::move(cb));
config_cache_->Fetch(PpdPathInServingRoot(ppd_basename), kMaxDataAge,
std::move(callback));
return;
}
ResolvePpdWithContents(ResolvedPpdOrigin::kFromPpdCache, ppd_basename,
result.contents, std::move(reference),
std::move(cb));
}
void OnPpdBasenameSoughtFromForwardIndex(
Printer::PpdReference reference,
ResolvePpdCallback cb,
const base::flat_map<std::string, ParsedIndexValues>&
forward_index_subset) {
const ParsedIndexLeaf* const leaf = FirstAllowableParsedIndexLeaf(
reference.effective_make_and_model, forward_index_subset);
if (!leaf || leaf->ppd_basename.empty()) {
std::string cache_key = PpdReferenceToCacheKey(reference);
ppd_cache_->Find(
cache_key,
base::BindOnce(&PpdProviderImpl::OnPpdBasenameSoughtInPpdCache,
weak_factory_.GetWeakPtr(), std::move(reference),
std::move(cb)));
return;
}
PRINTER_LOG(DEBUG) << reference.effective_make_and_model << " mapped to "
<< leaf->ppd_basename;
ppd_cache_->Find(
PpdBasenameToCacheKey(leaf->ppd_basename),
base::BindOnce(&PpdProviderImpl::OnPpdFromServingRootSoughtInPpdCache,
weak_factory_.GetWeakPtr(), leaf->ppd_basename,
std::move(reference), std::move(cb)));
}
void OnUserSuppliedPpdSoughtInPpdCache(
Printer::PpdReference reference,
CallbackResultCode result_if_unsuccessful,
ResolvePpdCallback cb,
const PpdCache::FindResult& result) {
if (!result.success) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(cb), result_if_unsuccessful, ""));
return;
}
ResolvePpdWithContents(ResolvedPpdOrigin::kFromPpdCache,
std::nullopt, result.contents,
std::move(reference), std::move(cb));
}
void OnUserSuppliedPpdFetchedFromLocalFile(Printer::PpdReference reference,
ResolvePpdCallback cb,
const std::string& result) {
if (result.empty()) {
std::string cache_key = PpdReferenceToCacheKey(reference);
ppd_cache_->Find(
cache_key,
base::BindOnce(&PpdProviderImpl::OnUserSuppliedPpdSoughtInPpdCache,
weak_factory_.GetWeakPtr(), std::move(reference),
CallbackResultCode::NOT_FOUND, std::move(cb)));
return;
}
ResolvePpdWithContents(ResolvedPpdOrigin::kFromUserSuppliedUrl,
std::nullopt, result,
std::move(reference), std::move(cb));
}
void OnUserSuppliedPpdFetchedFromRemoteUrl(
Printer::PpdReference reference,
ResolvePpdCallback cb,
RemotePpdFetcher::FetchResultCode code,
std::string result) {
if (code != RemotePpdFetcher::FetchResultCode::kSuccess) {
std::string cache_key = PpdReferenceToCacheKey(reference);
ppd_cache_->Find(
cache_key,
base::BindOnce(&PpdProviderImpl::OnUserSuppliedPpdSoughtInPpdCache,
weak_factory_.GetWeakPtr(), std::move(reference),
CallbackResultCode::SERVER_ERROR, std::move(cb)));
return;
}
ResolvePpdWithContents(ResolvedPpdOrigin::kFromUserSuppliedUrl,
std::nullopt, std::move(result),
std::move(reference), std::move(cb));
}
void ResolveUserSuppliedPpd(Printer::PpdReference reference,
ResolvePpdCallback cb) {
DCHECK(!reference.user_supplied_ppd_url.empty());
GURL url(reference.user_supplied_ppd_url);
if (url.SchemeIsHTTPOrHTTPS()) {
ResolveUserSuppliedPpdFromRemoteUrl(url, std::move(reference),
std::move(cb));
} else {
ResolveUserSuppliedPpdFromLocalFile(url, std::move(reference),
std::move(cb));
}
}
void ResolveUserSuppliedPpdFromLocalFile(GURL file_url,
Printer::PpdReference reference,
ResolvePpdCallback cb) {
file_task_runner_->PostTaskAndReplyWithResult(
FROM_HERE, base::BindOnce(&FetchFile, file_url),
base::BindOnce(&PpdProviderImpl::OnUserSuppliedPpdFetchedFromLocalFile,
weak_factory_.GetWeakPtr(), std::move(reference),
std::move(cb)));
}
void ResolveUserSuppliedPpdFromRemoteUrl(GURL url,
Printer::PpdReference reference,
ResolvePpdCallback cb) {
remote_ppd_fetcher_->Fetch(
url,
base::BindOnce(&PpdProviderImpl::OnUserSuppliedPpdFetchedFromRemoteUrl,
weak_factory_.GetWeakPtr(), std::move(reference),
std::move(cb)));
}
void FindLicenseForEmm(const std::string& effective_make_and_model,
ResolvePpdLicenseCallback cb,
const base::flat_map<std::string, ParsedIndexValues>&
forward_index_results) {
const ParsedIndexLeaf* const index_leaf = FirstAllowableParsedIndexLeaf(
effective_make_and_model, forward_index_results);
if (!index_leaf) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(std::move(cb), CallbackResultCode::NOT_FOUND,
""));
return;
}
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(cb), CallbackResultCode::SUCCESS,
index_leaf->license));
}
const base::Version version_;
scoped_refptr<PpdCache> ppd_cache_;
std::unique_ptr<PpdMetadataManager> metadata_manager_;
std::unique_ptr<PrinterConfigCache> config_cache_;
std::unique_ptr<RemotePpdFetcher> remote_ppd_fetcher_;
const scoped_refptr<base::SequencedTaskRunner> file_task_runner_;
base::WeakPtrFactory<PpdProviderImpl> weak_factory_{this};
};
}
PrinterSearchData::PrinterSearchData() = default;
PrinterSearchData::PrinterSearchData(const PrinterSearchData& other) = default;
PrinterSearchData::~PrinterSearchData() = default;
std::string PpdProvider::PpdReferenceToCacheKey(
const Printer::PpdReference& reference) {
DCHECK(PpdReferenceIsWellFormed(reference));
if (!reference.effective_make_and_model.empty()) {
return base::StrCat(
{"emm_for_metadata_v3:", reference.effective_make_and_model});
} else {
return base::StrCat({"up:", reference.user_supplied_ppd_url});
}
}
std::string PpdProvider::PpdBasenameToCacheKey(std::string_view ppd_basename) {
return base::StrCat({"ppd_basename_for_metadata_v3:", ppd_basename});
}
scoped_refptr<PpdProvider> PpdProvider::Create(
const base::Version& current_version,
scoped_refptr<PpdCache> cache,
std::unique_ptr<PpdMetadataManager> metadata_manager,
std::unique_ptr<PrinterConfigCache> config_cache,
std::unique_ptr<RemotePpdFetcher> remote_ppd_fetcher) {
return base::MakeRefCounted<PpdProviderImpl>(
current_version, cache, std::move(metadata_manager),
std::move(config_cache), std::move(remote_ppd_fetcher));
}
std::string_view PpdProvider::CallbackResultCodeName(CallbackResultCode code) {
switch (code) {
case SUCCESS:
return "SUCCESS";
case NOT_FOUND:
return "NOT_FOUND";
case SERVER_ERROR:
return "SERVER_ERROR";
case INTERNAL_ERROR:
return "INTERNAL_ERROR";
case PPD_TOO_LARGE:
return "PPD_TOO_LARGE";
}
}
}