chromium/chromeos/printing/ppd_provider.cc

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/printing/ppd_provider.h"

#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/ranges/algorithm.h"
#include "base/strings/strcat.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/epson_driver_matching.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 {

// The exact queue length at which PpdProvider will begin to post
// failure callbacks in response to its queue-able public methods.
// Arbitrarily chosen.
// See also: struct MethodDeferralContext
constexpr size_t kMethodDeferralLimit = 20;

// Age limit for time-sensitive API calls. Typically denotes "Please
// respond with data no older than kMaxDataAge." Arbitrarily chosen.
constexpr base::TimeDelta kMaxDataAge = base::Minutes(30LL);

// Effective-make-and-model string that describes a printer capable of
// using the generic Epson PPD.
const char kEpsonGenericEmm[] = "epson generic escpr printer";

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;
  }

  // All effective-make-and-model strings should be lowercased, since v2.
  // Since make-and-model strings could include non-Latin chars, only checking
  // that it excludes all upper-case chars A-Z.
  if (!base::ranges::all_of(reference.effective_make_and_model,
                            [](char c) { return !base::IsAsciiUpper(c); })) {
    return false;
  }
  // Should have exactly one non-empty field.
  return filled_fields == 1;
}

std::string PpdPathInServingRoot(std::string_view ppd_basename) {
  return base::StrCat({"ppds_for_metadata_v3/", ppd_basename});
}

// Zebra printers that support ZPL contain "Zebra" (or "Zebra Technologies") and
// "ZPL" in the IEEE 1284 device id make and model.
bool SupportsGenericZebraPPD(const PrinterSearchData& search_data) {
  return search_data.printer_id.make().starts_with("Zebra") &&
         base::Contains(search_data.printer_id.model(), "ZPL");
}

// Helper struct for PpdProviderImpl. Allows PpdProviderImpl to defer
// its public method calls, which PpdProviderImpl will do when the
// PpdMetadataManager is not ready to deal with locale-sensitive PPD
// metadata.
//
// Note that the semantics of this struct demand two things of the
// deferable public methods of PpdProviderImpl:
// 1. that they check for its presence and
// 2. that they check its |current_method_is_being_failed| member to
//    prevent infinite re-enqueueing of public methods once the queue
//    is full.
struct MethodDeferralContext {
  MethodDeferralContext() = default;
  ~MethodDeferralContext() = default;

  // This struct is not copyable.
  MethodDeferralContext(const MethodDeferralContext&) = delete;
  MethodDeferralContext& operator=(const MethodDeferralContext&) = delete;

  // Pops the first entry from |deferred_methods| and synchronously runs
  // it with the intent to fail it.
  void FailOneEnqueuedMethod() {
    DCHECK(!current_method_is_being_failed);

    // Explicitly activates the failure codepath for whatever public
    // method of PpdProviderImpl that we'll now Run().
    current_method_is_being_failed = true;

    std::move(deferred_methods.front()).Run();
    deferred_methods.pop();
    current_method_is_being_failed = false;
  }

  // Fails all |deferred_methods| synchronously.
  void FailAllEnqueuedMethods() {
    while (!deferred_methods.empty()) {
      FailOneEnqueuedMethod();
    }
  }

  // Dequeues and posts all |deferred_methods| onto our sequence.
  void FlushAndPostAll() {
    while (!deferred_methods.empty()) {
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, std::move(deferred_methods.front()));
      deferred_methods.pop();
    }
  }

  bool IsFull() { return deferred_methods.size() >= kMethodDeferralLimit; }

  // Whether an attempt to get the metadata locale is ongoing.
  bool metadata_locale_fetch_is_ongoing = false;

  // This bool is checked during execution of a queue-able public method
  // of PpdProviderImpl. If it is true, then
  // 1. the current queue-able public method was previously enqueued,
  // 2. the deferral queue is full, and so
  // 3. the current queue-able public method was posted for the sole
  //    purpose of being _failed_, and should not be re-enqueued.
  bool current_method_is_being_failed = false;

  base::queue<base::OnceCallback<void()>> deferred_methods;
};

// This class implements the PpdProvider interface for the v3 metadata
// (https://crbug.com/888189).
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),
        deferral_context_(std::make_unique<MethodDeferralContext>()),
        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 {
    // Do we need
    // 1. to defer this method?
    // 2. to fail this method (which was already previously deferred)?
    if (deferral_context_) {
      if (deferral_context_->current_method_is_being_failed) {
        auto failure_cb = base::BindOnce(
            std::move(cb), PpdProvider::CallbackResultCode::SERVER_ERROR,
            std::vector<std::string>());
        base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE, std::move(failure_cb));
        return;
      }

      if (deferral_context_->IsFull()) {
        deferral_context_->FailOneEnqueuedMethod();
        DCHECK(!deferral_context_->IsFull());
      }
      TryToGetMetadataManagerLocale();

      base::OnceCallback<void()> this_method =
          base::BindOnce(&PpdProviderImpl::ResolveManufacturers,
                         weak_factory_.GetWeakPtr(), std::move(cb));
      deferral_context_->deferred_methods.push(std::move(this_method));
      return;
    }

    metadata_manager_->GetManufacturers(kMaxDataAge, std::move(cb));
  }

  void ResolvePrinters(const std::string& manufacturer,
                       ResolvePrintersCallback cb) override {
    // Caller must not call ResolvePrinters() before a successful reply
    // from ResolveManufacturers(). ResolveManufacturers() cannot have
    // been successful if the |deferral_context_| still exists.
    if (deferral_context_) {
      auto failure_cb = base::BindOnce(
          std::move(cb), PpdProvider::CallbackResultCode::INTERNAL_ERROR,
          ResolvedPrintersList());
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, std::move(failure_cb));
      return;
    }

    PpdMetadataManager::GetPrintersCallback manager_callback =
        base::BindOnce(&PpdProviderImpl::OnPrintersGotten,
                       weak_factory_.GetWeakPtr(), std::move(cb));
    metadata_manager_->GetPrinters(manufacturer, kMaxDataAge,
                                   std::move(manager_callback));
  }

  // This method examines the members of |search_data| in turn and seeks
  // out an appropriate PPD from the serving root. The order is
  // 1. |search_data|::make_and_model - we seek out
  //    effective-make-and-model strings from forward index metadata.
  // 2. |search_data|::usb_*_id - we seek out a device with a matching
  //    ID from USB index metadata.
  // 3. |search_data|::make_and_model - we check if any among these
  //    effective-make-and-model strings describe a printer for which
  //    we can use the generic Epson PPD.
  //
  // *  This method observes and honors PPD restrictions (furnished by
  //    forward index metadata) and will ignore PPDs that are not
  //    advertised to run with the current |version_|.
  // *  This method is not locale-sensitive.
  void ResolvePpdReference(const PrinterSearchData& search_data,
                           ResolvePpdReferenceCallback cb) override {
    // In v3 metadata, effective-make-and-model strings are only
    // expressed in lowercased ASCII.
    PrinterSearchData lowercased_search_data(search_data);
    for (std::string& emm : lowercased_search_data.make_and_model) {
      emm = base::ToLowerASCII(emm);
    }

    // Any Zebra printer that supports ZPL uses the same PPD file, which is
    // kept in the PPD index with the key "zebra zpl label printer".
    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));

    // Initiate step 1 if possible.
    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;
    }

    // Otherwise, jump straight to step 2.
    TryToResolvePpdReferenceFromUsbIndices(std::move(context));
  }

  // This method invokes |cb| with the contents of a successfully
  // retrieved PPD appropriate for |reference|.
  //
  // As a side effect, this method may attempt
  // *  to read a PPD from the user's files (if the PPD is a
  //    user-supplied local file) or
  // *  to download a PPD from an http(s) URL (if the PPD is specified by a
  //    user-supplied remote URL
  // *  to download a PPD from the serving root (if the PPD is specified by
  //    effective-make-and-model).
  void ResolvePpd(const Printer::PpdReference& reference,
                  ResolvePpdCallback cb) override {
    // In v3 metadata, effective-make-and-model strings are only
    // expressed in lowercased ASCII.
    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 {
    // Do we need
    // 1. to defer this method?
    // 2. to fail this method (which was already previously deferred)?
    if (deferral_context_) {
      if (deferral_context_->current_method_is_being_failed) {
        auto failure_cb = base::BindOnce(
            std::move(cb), PpdProvider::CallbackResultCode::SERVER_ERROR, "",
            "");
        base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE, std::move(failure_cb));
        return;
      }

      if (deferral_context_->IsFull()) {
        deferral_context_->FailOneEnqueuedMethod();
        DCHECK(!deferral_context_->IsFull());
      }
      TryToGetMetadataManagerLocale();

      base::OnceCallback<void()> this_method = base::BindOnce(
          &PpdProviderImpl::ReverseLookup, weak_factory_.GetWeakPtr(),
          effective_make_and_model, std::move(cb));
      deferral_context_->deferred_methods.push(std::move(this_method));
      return;
    }

    // In v3 metadata, effective-make-and-model strings are only
    // expressed in lowercased ASCII.
    std::string lowercased_effective_make_and_model =
        base::ToLowerASCII(effective_make_and_model);

    // Delegates directly to the PpdMetadataManager.
    metadata_manager_->SplitMakeAndModel(lowercased_effective_make_and_model,
                                         kMaxDataAge, std::move(cb));
  }

  // This method depends on forward indices, which are not
  // locale-sensitive.
  void ResolvePpdLicense(std::string_view effective_make_and_model,
                         ResolvePpdLicenseCallback cb) override {
    // In v3 metadata, effective-make-and-model strings are only
    // expressed in lowercased ASCII.
    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:
  // Convenience container used throughout ResolvePpdReference().
  struct ResolvePpdReferenceContext {
    ResolvePpdReferenceContext(const PrinterSearchData& search_data_arg,
                               ResolvePpdReferenceCallback cb_arg)
        : search_data(search_data_arg), cb(std::move(cb_arg)) {}
    ~ResolvePpdReferenceContext() = default;

    // This container is not copyable and is move-only.
    ResolvePpdReferenceContext(ResolvePpdReferenceContext&& other) = default;
    ResolvePpdReferenceContext& operator=(ResolvePpdReferenceContext&& other) =
        default;

    PrinterSearchData search_data;
    ResolvePpdReferenceCallback cb;
  };

  // Used internally in ResolvePpd(). Describes the physical, bitwise
  // origin of a PPD.
  //
  // Example: a PPD previously downloaded from the serving root is saved
  // into the local PpdCache. A subsequent call to ResolvePpd() searches
  // the local PpdCache and returns this PPD. Internally, the methods
  // that comprise ResolvePpd() treat this as kFromPpdCache.
  enum class ResolvedPpdOrigin {
    kFromServingRoot,
    kFromUserSuppliedUrl,
    kFromPpdCache,
  };

  // Returns an empty string on failure.
  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;
  }

  // Requests that |metadata_manager_| obtain a metadata locale so that
  // |this| can call its locale-sensitive methods.
  //
  // |this| is largely useless if its |metadata_manager_| is not ready
  // to traffick in locale-sensitive PPD metadata, so we want this
  // method to eventually succeed.
  void TryToGetMetadataManagerLocale() {
    if (deferral_context_->metadata_locale_fetch_is_ongoing) {
      return;
    }
    auto callback =
        base::BindOnce(&PpdProviderImpl::OnMetadataManagerLocaleGotten,
                       weak_factory_.GetWeakPtr());
    metadata_manager_->GetLocale(std::move(callback));
    deferral_context_->metadata_locale_fetch_is_ongoing = true;
  }

  // Evaluates true if our |version_| falls within the bounds set by
  // |restrictions|.
  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;
  }

  // Callback fed to PpdMetadataManager::GetLocale().
  void OnMetadataManagerLocaleGotten(bool succeeded) {
    deferral_context_->metadata_locale_fetch_is_ongoing = false;
    if (!succeeded) {
      // Uh-oh, we concretely failed to get a metadata locale. We should
      // fail all outstanding deferred methods and let callers retry as
      // they see fit.
      deferral_context_->FailAllEnqueuedMethods();
      return;
    }
    deferral_context_->FlushAndPostAll();

    // It is no longer necessary to defer public method calls.
    deferral_context_.reset();
  }

  // Callback fed to PpdMetadataManager::GetPrinters().
  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));
  }

  // Finds the first ParsedIndexLeaf keyed on |effective_make_and_model|
  // from |forward_index_subset| (a slice of forward index metadata)
  // that is allowed for use in our current |version_|.
  //
  // Note that |forward_index_subset| has the type returned by
  // PpdMetadataManager::FindAllEmmsAvailableInIndexCallback.
  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), /*manufacturer=*/""));
  }

  // Fails a prior call to ResolvePpdReference().
  // |usb_manufacturer| may be empty
  // *  if we didn't find a manufacturer name for the given vendor ID or
  // *  if we invoked this method directly with an empty manufacturer
  //    name: "this wasn't a USB printer in the first place, so there's
  //    no USB manufacturer to speak of."
  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));
  }

  // Entry point to fail a prior call to ResolvePpdReference().
  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;
    }

    // If |search_data| does not describe a USB printer, the |cb| is
    // posted in the same way, but with an empty USB manufacturer name.
    FailToResolvePpdReferenceWithUsbManufacturer(std::move(context.cb),
                                                 /*manufacturer=*/"");
  }

  // Continues a prior call to ResolvePpdReference() (step 3).
  // This callback is fed to
  // PpdMetadataManager::FindAllEmmsAvailableInIndex(), as we treat the
  // hardcoded generic Epson effective-make-and-model string like any
  // other emm, duly verifying its presence in forward index metadata
  // and that it is not restricted from running in this |version_|.
  void OnForwardIndicesSearchedForGenericEpsonEmm(
      ResolvePpdReferenceContext context,
      const base::flat_map<std::string, ParsedIndexValues>&
          forward_index_results) {
    const ParsedIndexLeaf* const index_leaf =
        FirstAllowableParsedIndexLeaf(kEpsonGenericEmm, forward_index_results);
    if (index_leaf) {
      SuccessfullyResolvePpdReferenceWithEmm(kEpsonGenericEmm,
                                             std::move(context.cb));
      return;
    }

    // This really shouldn't happen, but we couldn't build a
    // PpdReference that would point to the generic Epson PPD.
    // (This might mean that the serving root is badly messed up in a
    // way that escaped the attention of the Chrome OS printing team.)
    FailToResolvePpdReference(std::move(context));
  }

  // Continues a prior call to ResolvePpdReference() (step 3).
  void TryToResolvePpdReferenceWithGenericEpsonPpd(
      ResolvePpdReferenceContext context) {
    if (CanUseEpsonGenericPPD(context.search_data)) {
      auto callback = base::BindOnce(
          &PpdProviderImpl::OnForwardIndicesSearchedForGenericEpsonEmm,
          weak_factory_.GetWeakPtr(), std::move(context));
      metadata_manager_->FindAllEmmsAvailableInIndex(
          {kEpsonGenericEmm}, kMaxDataAge, std::move(callback));
      return;
    }

    // At this point, we couldn't build a PpdReference using the
    // generic Epson PPD. ResolvePpdReference() can only fail now.
    FailToResolvePpdReference(std::move(context));
  }

  // Continues a prior call to ResolvePpdReference() (step 2).
  // This callback is fed to
  // PpdMetadataManager::FindAllEmmsAvailableInIndex().
  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;
    }

    // At this point, we couldn't build a PpdReference from the
    // effective-make-and-model string sourced from the USB index.
    // ResolvePpdReference() continues to its next step.
    TryToResolvePpdReferenceWithGenericEpsonPpd(std::move(context));
  }

  // Continues a prior call to ResolvePpdReference() (step 2).
  // This callback is fed to PpdMetadataManager::FindDeviceInUsbIndex().
  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;
    }

    // At this point, we couldn't build a PpdReference from a USB index
    // search. ResolvePpdReference() continues to its next step.
    TryToResolvePpdReferenceWithGenericEpsonPpd(std::move(context));
  }

  // Continues a prior call to ResolvePpdReference() (step 2).
  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;
    }

    // At this point, we couldn't use |search_data| to search USB indices.
    // ResolvePpdReference() continues to its next step.
    TryToResolvePpdReferenceWithGenericEpsonPpd(std::move(context));
  }

  // Continues a prior call to ResolvePpdReference() (step 1).
  // This callback is fed to
  // PpdMetadataManager::FindAllEmmsAvailableInIndexCallback().
  void TryToResolvePpdReferenceFromForwardIndices(
      ResolvePpdReferenceContext context,
      const base::flat_map<std::string, ParsedIndexValues>&
          forward_index_results) {
    // Sweeps through the results of the forward index metadata search.
    // If any effective-make-and-model string advertises an available
    // PPD, we use that result to post |cb|.
    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;
    }

    // At this point, we couldn't build a PpdReference directly from a
    // forward index search. ResolvePpdReference() continues to step 2.
    TryToResolvePpdReferenceFromUsbIndices(std::move(context));
  }

  // Continues a prior call to ResolvePpd().
  //
  // Stores a PPD with |ppd_contents| in the PPD Cache.
  // Caller must provide nonempty |ppd_basename| when |ppd_origin|
  // identifies the PPD as coming from the the serving root.
  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:
        // This very PPD was retrieved from the local PpdCache; there's no
        // point in storing it again.
        return;

      case ResolvedPpdOrigin::kFromServingRoot:
        // To service the two-step "dereference" of resolving a PPD from
        // the serving root, we need to Store() the basename of this PPD
        // in the local PpdCache.
        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:
        // No special considerations for a user-supplied PPD; we can
        // Store() it directly by mapping the user-supplied URI to the
        // PPD contents.
        DCHECK(!reference.user_supplied_ppd_url.empty());

        ppd_cache_->Store(PpdReferenceToCacheKey(reference), ppd_contents);
        break;
    }
  }

  // Continues a prior call to ResolvePpd().
  //
  // Called when we have the contents of the PPD being resolved; we are
  // on the cusp of being able to invoke the |cb|.
  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)));
  }

  // Continues a prior call to ResolvePpd().
  //
  // Called back by PrinterConfigCache::Fetch() when we've fetched
  // a PPD from the serving root.
  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,
                           /*ppd_basename=*/result.key,
                           /*ppd_contents=*/result.contents,
                           std::move(reference), std::move(cb));
  }

  // Continues a prior call to ResolvePpd().
  //
  // Called when we seek a mapping from an effective-make-and-model
  // string to a PPD basename by querying the local PpdCache.
  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(),
                       /*ppd_basename=*/result.contents, std::move(reference),
                       std::move(cb)));
  }

  // Continues a prior call to ResolvePpd().
  //
  // Called when we have a PPD basename already and seek its contents
  // in the local PpdCache.
  void OnPpdFromServingRootSoughtInPpdCache(
      const std::string& ppd_basename,
      Printer::PpdReference reference,
      ResolvePpdCallback cb,
      const PpdCache::FindResult& result) {
    if (!result.success || result.contents.empty()) {
      // We have the PPD basename, but not the contents of the PPD
      // itself in our local PpdCache. We must seek out the contents
      // from the serving root.
      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));
  }

  // Continues a prior call to ResolvePpd().
  //
  // Called back by PpdMetadataManager::FindAllEmmsAvailableInIndex().
  //
  // 1. Maps |reference|::effective_make_and_model to a PPD basename.
  //    a.  Attempts to do so with fresh forward index metadata if
  //        possible, searching |forward_index_subset| for the best
  //        available PPD.
  //    b.  Falls back to directly querying the local PpdCache instance,
  //        e.g. if the network is unreachable.
  // 2. Uses basename derived in previous step to retrieve the
  //    appropriate PPD from the local PpdCache instance.
  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()) {
      // The forward index doesn't advise what the best fit PPD is for
      // |reference|::effective_make_and_model. We can look toward the
      // local PpdCache to see if we saved it previously.
      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;
    }

    // The forward index does advertise a best-fit PPD basename. We
    // check the local PpdCache to see if we already have it.
    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)));
  }

  // Continues a prior call to ResolvePpd().
  //
  // Called when we finish searching the PpdCache for a user-supplied
  // PPD. This contrasts with the slightly more involved two-step
  // "dereference" process in searching the PpdCache for a PPD retrieved
  // from the serving root.
  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,
                           /*ppd_basename=*/std::nullopt, result.contents,
                           std::move(reference), std::move(cb));
  }

  // Continues a prior call to ResolvePpd().
  //
  // Called when we finish fetching a PPD file from device-local storage
  // (e.g. from the user's home directory, not from the PpdCache).
  void OnUserSuppliedPpdFetchedFromLocalFile(Printer::PpdReference reference,
                                             ResolvePpdCallback cb,
                                             const std::string& result) {
    if (result.empty()) {
      // We didn't find a nonempty PPD at the location specified by the
      // user. Try searching the PpdCache and fail with NOT_FOUND if not found
      // in PpdCache.
      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,
                           /*ppd_basename=*/std::nullopt, result,
                           std::move(reference), std::move(cb));
  }

  // Continues a prior call to ResolvePpd().
  //
  // Called when we finish fetching the contents of a PPD file from a remote
  // URL.
  void OnUserSuppliedPpdFetchedFromRemoteUrl(
      Printer::PpdReference reference,
      ResolvePpdCallback cb,
      RemotePpdFetcher::FetchResultCode code,
      std::string result) {
    if (code != RemotePpdFetcher::FetchResultCode::kSuccess) {
      // Fetching the PPD from remote URL was unsuccessful. Try searching the
      // PpdCache and fail with SERVER_ERROR if not found in PpdCache.
      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,
                           /*ppd_basename=*/std::nullopt, std::move(result),
                           std::move(reference), std::move(cb));
  }

  // Continues a prior call to ResolvePpd().
  //
  // 1. Attempts to invoke |cb| with the file named by
  //    |reference|::user_suplied_ppd_url - i.e. a live fetch from
  //    local disk or an http:// url.
  // 2. Attempts to search the local PpdCache instance for the file
  //    whose cache key was built from
  //    |reference|::user_supplied_ppd_url.
  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)));
  }

  // Continues a prior call to ResolvePpdLicense().
  // This callback is fed to
  // PpdMetadataManager::FindAllEmmsAvailableInIndexCallback().
  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) {
      // This particular |effective_make_and_model| is invisible to the
      // current |version_|; either it is restricted or it is missing
      // entirely from the forward indices.
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE,
          base::BindOnce(std::move(cb), CallbackResultCode::NOT_FOUND,
                         /*license_name=*/""));
      return;
    }

    // Note that the license can also be empty; this denotes that
    // no license is associated with this particular
    // |effective_make_and_model| in this |version_|.
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(cb), CallbackResultCode::SUCCESS,
                                  index_leaf->license));
  }

  // Current version used to filter restricted ppds
  const base::Version version_;

  // Provides PPD storage on-device.
  scoped_refptr<PpdCache> ppd_cache_;

  // Used to
  // 1. to determine if |this| should defer locale-sensitive public
  //    method calls and
  // 2. to defer those method calls, if necessary.
  // These deferrals are only necessary before the |metadata_manager_|
  // is ready to deal with locale-sensitive PPD metadata. This member is
  // reset once deferrals are unnecessary.
  std::unique_ptr<MethodDeferralContext> deferral_context_;

  // Interacts with and controls PPD metadata.
  std::unique_ptr<PpdMetadataManager> metadata_manager_;

  // Fetches PPDs from the Chrome OS Printing team's serving root.
  std::unique_ptr<PrinterConfigCache> config_cache_;

  // Fetches PPDs from remote http:// or https:// URLs.
  std::unique_ptr<RemotePpdFetcher> remote_ppd_fetcher_;

  // Where to run disk operations.
  const scoped_refptr<base::SequencedTaskRunner> file_task_runner_;

  base::WeakPtrFactory<PpdProviderImpl> weak_factory_{this};
};

}  // namespace

PrinterSearchData::PrinterSearchData() = default;
PrinterSearchData::PrinterSearchData(const PrinterSearchData& other) = default;
PrinterSearchData::~PrinterSearchData() = default;

// static
//
// Used in production but also exposed for testing.
std::string PpdProvider::PpdReferenceToCacheKey(
    const Printer::PpdReference& reference) {
  DCHECK(PpdReferenceIsWellFormed(reference));
  // The key prefixes here are arbitrary, but ensure we can't have an (unhashed)
  // collision between keys generated from different PpdReference fields.
  if (!reference.effective_make_and_model.empty()) {
    return base::StrCat(
        {"emm_for_metadata_v3:", reference.effective_make_and_model});
  } else {
    // Retains the legacy salt from the v2 PpdProvider. This is done
    // to minimize user breakage when we roll out the v3 PpdProvider.
    return base::StrCat({"up:", reference.user_supplied_ppd_url});
  }
}

// static
//
// Used in production but also exposed for testing.
std::string PpdProvider::PpdBasenameToCacheKey(std::string_view ppd_basename) {
  return base::StrCat({"ppd_basename_for_metadata_v3:", ppd_basename});
}

// static
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));
}

// static
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";
  }
}

}  // namespace chromeos