chromium/chrome/browser/ash/printing/printer_configurer.cc

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

#include "chrome/browser/ash/printing/printer_configurer.h"

#include <map>
#include <set>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "base/check.h"
#include "base/containers/flat_map.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/hash/md5.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/browser_process_platform_part.h"
#include "chrome/browser/component_updater/cros_component_installer_chromeos.h"
#include "chrome/common/webui_url_constants.h"
#include "chromeos/ash/components/dbus/debug_daemon/debug_daemon_client.h"
#include "chromeos/ash/components/dbus/dlcservice/dlcservice_client.h"
#include "chromeos/ash/components/dbus/printscanmgr/printscanmgr_client.h"
#include "chromeos/dbus/common/dbus_library_error.h"
#include "chromeos/printing/ppd_line_reader.h"
#include "chromeos/printing/ppd_provider.h"
#include "chromeos/printing/printer_configuration.h"
#include "components/device_event_log/device_event_log.h"
#include "content/public/browser/browser_thread.h"
#include "printing/printing_features.h"
#include "third_party/cros_system_api/dbus/debugd/dbus-constants.h"

namespace ash {

namespace {

using ::chromeos::PpdProvider;
using ::chromeos::Printer;

const char kEbuildWithHplipPlugins[] = "hplip-plugin";

PrinterSetupResult PrinterSetupResultFromDbusResultCode(const Printer& printer,
                                                        int result_code) {
  DCHECK_GE(result_code, 0);
  const std::string prefix = printer.make_and_model() + " setup result: ";
  switch (result_code) {
    case debugd::CupsResult::CUPS_SUCCESS:
      PRINTER_LOG(EVENT) << prefix << "Printer setup successful";
      return PrinterSetupResult::kSuccess;
    case debugd::CupsResult::CUPS_INVALID_PPD:
      PRINTER_LOG(EVENT) << prefix << "PPD Invalid";
      return PrinterSetupResult::kInvalidPpd;
    case debugd::CupsResult::CUPS_LPADMIN_FAILURE:
      PRINTER_LOG(ERROR) << prefix << "lpadmin-manual failed";
      return PrinterSetupResult::kFatalError;
    case debugd::CupsResult::CUPS_AUTOCONF_FAILURE:
      PRINTER_LOG(ERROR) << prefix << "lpadmin-autoconf failed";
      return PrinterSetupResult::kFatalError;
    case debugd::CupsResult::CUPS_BAD_URI:
      PRINTER_LOG(EVENT) << prefix << "Bad URI";
      return PrinterSetupResult::kBadUri;
    case debugd::CupsResult::CUPS_IO_ERROR:
      PRINTER_LOG(ERROR) << prefix << "I/O error";
      return PrinterSetupResult::kIoError;
    case debugd::CupsResult::CUPS_MEMORY_ALLOC_ERROR:
      PRINTER_LOG(EVENT) << prefix << "Memory allocation error";
      return PrinterSetupResult::kMemoryAllocationError;
    case debugd::CupsResult::CUPS_PRINTER_UNREACHABLE:
      PRINTER_LOG(EVENT) << prefix << "Printer is unreachable";
      return PrinterSetupResult::kPrinterUnreachable;
    case debugd::CupsResult::CUPS_PRINTER_WRONG_RESPONSE:
      PRINTER_LOG(EVENT) << prefix << "Unexpected response from printer";
      return PrinterSetupResult::kPrinterSentWrongResponse;
    case debugd::CupsResult::CUPS_PRINTER_NOT_AUTOCONF:
      PRINTER_LOG(EVENT) << prefix << "Printer is not autoconfigurable";
      return PrinterSetupResult::kPrinterIsNotAutoconfigurable;
    case debugd::CupsResult::CUPS_FATAL:
    default:
      // We have no idea.  It must be fatal.
      PRINTER_LOG(ERROR) << prefix << "Unrecognized error: " << result_code;
      return PrinterSetupResult::kFatalError;
  }
}

// Map D-Bus errors from the debug daemon client to D-Bus errors enumerated
// in PrinterSetupResult.
PrinterSetupResult PrinterSetupResultFromDbusErrorCode(
    const Printer& printer,
    chromeos::DBusLibraryError dbus_error) {
  DCHECK_LT(dbus_error, 0);
  const std::string prefix = printer.make_and_model() + " setup result: ";
  switch (dbus_error) {
    case chromeos::DBusLibraryError::kNoReply:
      PRINTER_LOG(ERROR) << prefix << "D-Bus error - no reply";
      return PrinterSetupResult::kDbusNoReply;
    case chromeos::DBusLibraryError::kTimeout:
      PRINTER_LOG(ERROR) << prefix << "D-Bus error - timeout";
      return PrinterSetupResult::kDbusTimeout;
    default:
      PRINTER_LOG(ERROR) << prefix << "Unknown D-Bus error";
      return PrinterSetupResult::kDbusError;
  }
}

PrinterSetupResult PrinterSetupResultFromAddPrinterResult(
    const Printer& printer,
    printscanmgr::AddPrinterResult result) {
  const std::string prefix = printer.make_and_model() + " setup result: ";
  switch (result) {
    case printscanmgr::AddPrinterResult::ADD_PRINTER_RESULT_SUCCESS:
      PRINTER_LOG(EVENT) << prefix << "Printer setup successful";
      return PrinterSetupResult::kSuccess;
    case printscanmgr::AddPrinterResult::ADD_PRINTER_RESULT_CUPS_INVALID_PPD:
      PRINTER_LOG(EVENT) << prefix << "PPD Invalid";
      return PrinterSetupResult::kInvalidPpd;
    case printscanmgr::AddPrinterResult::
        ADD_PRINTER_RESULT_CUPS_LPADMIN_FAILURE:
      PRINTER_LOG(ERROR) << prefix << "lpadmin-manual failed";
      return PrinterSetupResult::kFatalError;
    case printscanmgr::AddPrinterResult::
        ADD_PRINTER_RESULT_CUPS_AUTOCONF_FAILURE:
      PRINTER_LOG(ERROR) << prefix << "lpadmin-autoconf failed";
      return PrinterSetupResult::kFatalError;
    case printscanmgr::AddPrinterResult::ADD_PRINTER_RESULT_CUPS_BAD_URI:
      PRINTER_LOG(EVENT) << prefix << "Bad URI";
      return PrinterSetupResult::kBadUri;
    case printscanmgr::AddPrinterResult::ADD_PRINTER_RESULT_CUPS_IO_ERROR:
      PRINTER_LOG(ERROR) << prefix << "I/O error";
      return PrinterSetupResult::kIoError;
    case printscanmgr::AddPrinterResult::
        ADD_PRINTER_RESULT_CUPS_MEMORY_ALLOC_ERROR:
      PRINTER_LOG(EVENT) << prefix << "Memory allocation error";
      return PrinterSetupResult::kMemoryAllocationError;
    case printscanmgr::AddPrinterResult::
        ADD_PRINTER_RESULT_CUPS_PRINTER_UNREACHABLE:
      PRINTER_LOG(EVENT) << prefix << "Printer is unreachable";
      return PrinterSetupResult::kPrinterUnreachable;
    case printscanmgr::AddPrinterResult::
        ADD_PRINTER_RESULT_CUPS_PRINTER_WRONG_RESPONSE:
      PRINTER_LOG(EVENT) << prefix << "Unexpected response from printer";
      return PrinterSetupResult::kPrinterSentWrongResponse;
    case printscanmgr::AddPrinterResult::
        ADD_PRINTER_RESULT_CUPS_PRINTER_NOT_AUTOCONF:
      PRINTER_LOG(EVENT) << prefix << "Printer is not autoconfigurable";
      return PrinterSetupResult::kPrinterIsNotAutoconfigurable;
    case printscanmgr::AddPrinterResult::ADD_PRINTER_RESULT_DBUS_GENERIC:
      PRINTER_LOG(ERROR) << prefix << "Unknown D-Bus error";
      return PrinterSetupResult::kDbusError;
    case printscanmgr::AddPrinterResult::ADD_PRINTER_RESULT_DBUS_NO_REPLY:
      PRINTER_LOG(ERROR) << prefix << "D-Bus error - no reply";
      return PrinterSetupResult::kDbusNoReply;
    case printscanmgr::AddPrinterResult::ADD_PRINTER_RESULT_DBUS_TIMEOUT:
      PRINTER_LOG(ERROR) << prefix << "D-Bus error - timeout";
      return PrinterSetupResult::kDbusTimeout;
    // TODO(pmoy): handle new D-Bus encoding error here.
    case printscanmgr::AddPrinterResult::
        ADD_PRINTER_RESULT_UNSPECIFIED:  // FALLTHROUGH
    case printscanmgr::AddPrinterResult::
        ADD_PRINTER_RESULT_CUPS_FATAL:  // FALLTHROUGH
    default:
      // We have no idea.  It must be fatal.
      PRINTER_LOG(ERROR) << prefix << "Unrecognized error: "
                         << printscanmgr::AddPrinterResult_Name(result);
      return PrinterSetupResult::kFatalError;
  }
}

// Searches in `ppd` for a command setting HP printer language. If the keyword
// is found, the function inserts after the command a line containing a path to
// Hplip plugin provided in `path` and returns true.
bool AddHplipPluginPathToPpdContent(std::string_view path, std::string& ppd) {
  constexpr char kHpPrinterLanguageKeyword[] = "*hpPrinterLanguage:";
  size_t pos = ppd.find(kHpPrinterLanguageKeyword);
  if (pos != std::string::npos) {
    pos = ppd.find('\n', pos + sizeof(kHpPrinterLanguageKeyword));
  }
  if (pos == std::string::npos) {
    return false;
  }
  ppd.insert(++pos,
             base::StrCat({"*chromeOSHplipPluginPath: \"", path, "\"\n"}));
  return true;
}

// Configures printers by downloading PPDs then adding them to CUPS through
// debugd.  This class must be used on the UI thread.
class PrinterConfigurerImpl : public PrinterConfigurer {
 public:
  PrinterConfigurerImpl(scoped_refptr<PpdProvider> ppd_provider,
                        DlcserviceClient* dlc_service_client)
      : ppd_provider_(ppd_provider), dlc_service_client_(dlc_service_client) {
    DCHECK(ppd_provider_);
    DCHECK(dlc_service_client_);
  }

  PrinterConfigurerImpl(const PrinterConfigurerImpl&) = delete;
  PrinterConfigurerImpl& operator=(const PrinterConfigurerImpl&) = delete;

  ~PrinterConfigurerImpl() override {}

  void SetUpPrinterInCups(const Printer& printer,
                          PrinterSetupCallback callback) override {
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    DCHECK(!printer.id().empty());
    DCHECK(printer.HasUri());
    PRINTER_LOG(USER) << printer.make_and_model()
                      << " Printer setup requested as " << printer.id();

    if (!printer.IsIppEverywhere()) {
      if (!printer.ppd_reference().user_supplied_ppd_url.empty()) {
        // The PPD was provided by the user.
        ResolvePpd(printer, /*hplip_plugin_path=*/"", std::move(callback));
      } else {
        // The PPD was selected from our PPD Index. We have to check its license
        // to make sure it doesn't need any additional plugins before setup.
        PRINTER_LOG(DEBUG) << printer.make_and_model() << " Check license";
        ppd_provider_->ResolvePpdLicense(
            printer.ppd_reference().effective_make_and_model,
            base::BindOnce(&PrinterConfigurerImpl::ResolveLicenseDone,
                           weak_factory_.GetWeakPtr(), printer,
                           std::move(callback)));
      }
      return;
    }

    PRINTER_LOG(DEBUG) << printer.make_and_model()
                       << " Attempting driverless setup at "
                       << printer.uri().GetNormalized();
    if (base::FeatureList::IsEnabled(
            printing::features::kAddPrinterViaPrintscanmgr)) {
      printscanmgr::CupsAddAutoConfiguredPrinterRequest request;
      request.set_name(printer.id());
      request.set_uri(printer.uri().GetNormalized(/*always_print_port=*/true));
      request.set_language(g_browser_process->GetApplicationLocale());
      PrintscanmgrClient::Get()->CupsAddAutoConfiguredPrinter(
          std::move(request),
          base::BindOnce(
              &PrinterConfigurerImpl::OnAddedPrinter<
                  printscanmgr::CupsAddAutoConfiguredPrinterResponse>,
              weak_factory_.GetWeakPtr(), printer, std::move(callback)));
    } else {
      DebugDaemonClient::Get()->CupsAddAutoConfiguredPrinter(
          printer.id(), printer.uri().GetNormalized(true /*always_print_port*/),
          g_browser_process->GetApplicationLocale(),
          base::BindOnce(&PrinterConfigurerImpl::OnAddedPrinterDebugd,
                         weak_factory_.GetWeakPtr(), printer,
                         std::move(callback)));
    }
  }

 private:
  // Receive the callback from the printscanmgr daemon client once we attempt to
  // add the printer.
  template <typename T>
  void OnAddedPrinter(const Printer& printer,
                      PrinterSetupCallback cb,
                      std::optional<T> response) {
    // It's expected that the printscanmgr daemon posts callbacks on the UI
    // thread.
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

    if (!response) {
      PRINTER_LOG(ERROR) << "Null response to OnAddedPrinter";
      std::move(cb).Run(PrinterSetupResult::kFatalError);
      return;
    }

    PrinterSetupResult setup_result =
        PrinterSetupResultFromAddPrinterResult(printer, response->result());
    std::move(cb).Run(setup_result);
  }

  // Receive the callback from the debug daemon client once we attempt to
  // add the printer.
  void OnAddedPrinterDebugd(const Printer& printer,
                            PrinterSetupCallback cb,
                            int32_t result_code) {
    // It's expected that debug daemon posts callbacks on the UI thread.
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

    PrinterSetupResult setup_result =
        result_code < 0
            ? PrinterSetupResultFromDbusErrorCode(
                  printer, static_cast<chromeos::DBusLibraryError>(result_code))
            : PrinterSetupResultFromDbusResultCode(printer, result_code);
    std::move(cb).Run(setup_result);
  }

  void AddPrinter(const Printer& printer,
                  const std::string& ppd_contents,
                  PrinterSetupCallback cb) {
    PRINTER_LOG(EVENT) << printer.make_and_model()
                       << " Attempting setup with PPD at "
                       << printer.uri().GetNormalized();
    if (base::FeatureList::IsEnabled(
            printing::features::kAddPrinterViaPrintscanmgr)) {
      printscanmgr::CupsAddManuallyConfiguredPrinterRequest request;
      request.set_name(printer.id());
      request.set_uri(printer.uri().GetNormalized(/*always_print_port=*/true));
      request.set_ppd_contents(ppd_contents);
      request.set_language(g_browser_process->GetApplicationLocale());
      PrintscanmgrClient::Get()->CupsAddManuallyConfiguredPrinter(
          std::move(request),
          base::BindOnce(
              &PrinterConfigurerImpl::OnAddedPrinter<
                  printscanmgr::CupsAddManuallyConfiguredPrinterResponse>,
              weak_factory_.GetWeakPtr(), printer, std::move(cb)));
    } else {
      DebugDaemonClient::Get()->CupsAddManuallyConfiguredPrinter(
          printer.id(), printer.uri().GetNormalized(true /*always_print_port*/),
          g_browser_process->GetApplicationLocale(), ppd_contents,
          base::BindOnce(&PrinterConfigurerImpl::OnAddedPrinterDebugd,
                         weak_factory_.GetWeakPtr(), printer, std::move(cb)));
    }
  }

  void ResolvePpdDone(const Printer& printer,
                      const std::string& hplip_plugin_path,
                      PrinterSetupCallback cb,
                      PpdProvider::CallbackResultCode result,
                      const std::string& ppd_contents) {
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    PRINTER_LOG(EVENT) << printer.make_and_model() << " PPD Resolution Result: "
                       << PpdProvider::CallbackResultCodeName(result);
    switch (result) {
      case PpdProvider::SUCCESS:
        DCHECK(!ppd_contents.empty());
        {
          // Use PpdLineReader to ungzip the content (if gzipped).
          auto reader = chromeos::PpdLineReader::Create(ppd_contents);
          std::string ppd = reader->RemainingContent();
          if (reader->Error()) {
            PRINTER_LOG(ERROR) << printer.make_and_model()
                               << " Error when reading/decompressing PPD";
          }
          if (!hplip_plugin_path.empty()) {
            if (!AddHplipPluginPathToPpdContent(hplip_plugin_path, ppd)) {
              PRINTER_LOG(ERROR) << printer.make_and_model()
                                 << " Missing HP printer language in PPD file";
            }
          }
          AddPrinter(printer, ppd, std::move(cb));
        }
        break;
      case PpdProvider::CallbackResultCode::NOT_FOUND:
        std::move(cb).Run(PrinterSetupResult::kPpdNotFound);
        break;
      case PpdProvider::CallbackResultCode::SERVER_ERROR:
        std::move(cb).Run(PrinterSetupResult::kPpdUnretrievable);
        break;
      case PpdProvider::CallbackResultCode::INTERNAL_ERROR:
        std::move(cb).Run(PrinterSetupResult::kFatalError);
        break;
      case PpdProvider::CallbackResultCode::PPD_TOO_LARGE:
        std::move(cb).Run(PrinterSetupResult::kPpdTooLarge);
        break;
    }
  }

  void ResolvePpd(const Printer& printer,
                  const std::string& hplip_plugin_path,
                  PrinterSetupCallback cb) {
    PRINTER_LOG(DEBUG) << printer.make_and_model() << " Lookup PPD";
    ppd_provider_->ResolvePpd(
        printer.ppd_reference(),
        base::BindOnce(&PrinterConfigurerImpl::ResolvePpdDone,
                       weak_factory_.GetWeakPtr(), printer, hplip_plugin_path,
                       std::move(cb)));
  }

  void ResolveLicenseDone(const Printer& printer,
                          PrinterSetupCallback cb,
                          PpdProvider::CallbackResultCode result,
                          const std::string& license_name) {
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    PRINTER_LOG(EVENT) << printer.make_and_model()
                       << " License resolution Result: "
                       << PpdProvider::CallbackResultCodeName(result);
    switch (result) {
      case PpdProvider::SUCCESS:
        break;
      case PpdProvider::CallbackResultCode::NOT_FOUND:
        std::move(cb).Run(PrinterSetupResult::kPpdNotFound);
        return;
      case PpdProvider::CallbackResultCode::SERVER_ERROR:
        std::move(cb).Run(PrinterSetupResult::kPpdUnretrievable);
        return;
      case PpdProvider::CallbackResultCode::INTERNAL_ERROR:
        std::move(cb).Run(PrinterSetupResult::kFatalError);
        return;
      case PpdProvider::CallbackResultCode::PPD_TOO_LARGE:
        std::move(cb).Run(PrinterSetupResult::kPpdTooLarge);
        return;
    }

    if (license_name == kEbuildWithHplipPlugins) {
      // Printers with this license require special plugin. We have to install
      // it before proceeding.
      PRINTER_LOG(DEBUG) << "Attempting installation of hplip-plugin";
      dlcservice::InstallRequest install_request;
      install_request.set_id(kEbuildWithHplipPlugins);
      dlc_service_client_->Install(
          install_request,
          base::BindOnce(&PrinterConfigurerImpl::OnPluginInstallationComplete,
                         weak_factory_.GetWeakPtr(), printer, std::move(cb)),
          base::DoNothing());
    } else {
      // Proceed with PPD resolution.
      ResolvePpd(printer, /*hplip_plugin_path=*/"", std::move(cb));
    }
  }

  void OnPluginInstallationComplete(
      const Printer& printer,
      PrinterSetupCallback cb,
      const DlcserviceClient::InstallResult& result) {
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

    if (result.root_path.empty()) {
      // Empty path of the plugin location means failure.
      PRINTER_LOG(ERROR) << "Cannot install plugin " << result.dlc_id << ": "
                         << result.error;
      std::move(cb).Run(PrinterSetupResult::kComponentUnavailable);
    } else {
      // Plugin installed. We can proceed with PPD resolution.
      ResolvePpd(printer, result.root_path, std::move(cb));
    }
  }

  scoped_refptr<PpdProvider> ppd_provider_;
  raw_ptr<DlcserviceClient> dlc_service_client_;
  base::WeakPtrFactory<PrinterConfigurerImpl> weak_factory_{this};
};

}  // namespace

// static
std::string PrinterConfigurer::SetupFingerprint(const Printer& printer) {
  base::MD5Context ctx;
  base::MD5Init(&ctx);
  base::MD5Update(&ctx, printer.id());
  base::MD5Update(&ctx, printer.uri().GetNormalized(false));
  base::MD5Update(&ctx, printer.ppd_reference().user_supplied_ppd_url);
  base::MD5Update(&ctx, printer.ppd_reference().effective_make_and_model);
  char autoconf = printer.ppd_reference().autoconf ? 1 : 0;
  base::MD5Update(&ctx, std::string(&autoconf, sizeof(autoconf)));
  base::MD5Digest digest;
  base::MD5Final(&digest, &ctx);
  return std::string(reinterpret_cast<char*>(&digest.a[0]), sizeof(digest.a));
}

// static
void PrinterConfigurer::RecordUsbPrinterSetupSource(
    UsbPrinterSetupSource source) {
  base::UmaHistogramEnumeration("Printing.CUPS.UsbSetupSource", source);
}

// static
std::unique_ptr<PrinterConfigurer> PrinterConfigurer::Create(
    scoped_refptr<PpdProvider> ppd_provider,
    DlcserviceClient* dlc_service_client) {
  return std::make_unique<PrinterConfigurerImpl>(ppd_provider,
                                                 dlc_service_client);
}

// static
GURL PrinterConfigurer::GeneratePrinterEulaUrl(const std::string& license) {
  GURL eula_url(chrome::kChromeUIOSCreditsURL);
  // Construct the URL with proper reference fragment.
  GURL::Replacements replacements;
  replacements.SetRefStr(license);
  return eula_url.ReplaceComponents(replacements);
}

std::string ResultCodeToMessage(const PrinterSetupResult result) {
  switch (result) {
    // Success.
    case PrinterSetupResult::kSuccess:
      return "Printer successfully configured.";
    case PrinterSetupResult::kEditSuccess:
      return "Printer successfully updated.";
    // Invalid configuration.
    case PrinterSetupResult::kNativePrintersNotAllowed:
      return "Unable to add or edit printer due to enterprise policy.";
    case PrinterSetupResult::kBadUri:
      return "Invalid URI.";
    case PrinterSetupResult::kInvalidPrinterUpdate:
      return "Requested printer changes would make printer unusable.";
    // Problem with a printer.
    case PrinterSetupResult::kPrinterUnreachable:
      return "Could not contact printer for configuration.";
    case PrinterSetupResult::kPrinterSentWrongResponse:
      return "Printer sent unexpected response.";
    case PrinterSetupResult::kPrinterIsNotAutoconfigurable:
      return "Printer is not autoconfigurable.";
    // Problem with a PPD file.
    case PrinterSetupResult::kPpdTooLarge:
      return "PPD is too large.";
    case PrinterSetupResult::kInvalidPpd:
      return "Provided PPD is invalid.";
    case PrinterSetupResult::kPpdNotFound:
      return "Could not locate requested PPD. Check printer configuration.";
    case PrinterSetupResult::kPpdUnretrievable:
      return "Could not retrieve PPD from server. Check Internet connection.";
    // Cannot load a required compomonent.
    case PrinterSetupResult::kComponentUnavailable:
      return "Could not install component.";
    // Problem with D-Bus.
    case PrinterSetupResult::kDbusError:
      return "D-Bus error occurred. Reboot required.";
    case PrinterSetupResult::kDbusNoReply:
      return "Couldn't talk to printscanmgr over D-Bus.";
    case PrinterSetupResult::kDbusTimeout:
      return "Timed out trying to reach printscanmgr over D-Bus.";
    // Problem reported by OS.
    case PrinterSetupResult::kIoError:
      return "I/O error occurred.";
    case PrinterSetupResult::kMemoryAllocationError:
      return "Memory allocation error occurred.";
    // Unknown problem.
    case PrinterSetupResult::kFatalError:
      return "Unknown error occurred.";
    // Printer requires manual setup.
    case PrinterSetupResult::kManualSetupRequired:
      return "Printer requires manual setup.";
    case PrinterSetupResult::kPrinterRemoved:
      return "Printer was removed during the setup.";
  }
}

}  // namespace ash