blob: e7c32535c16e39f3d5833395ec94a6e7e1faeac3 [file] [log] [blame]
// 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/usb_printer_util.h"
#include <ctype.h>
#include <stdint.h>
#include <algorithm>
#include <string>
#include <utility>
#include <vector>
#include "base/big_endian.h"
#include "base/callback_helpers.h"
#include "base/containers/span.h"
#include "base/hash/md5.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/printing/printer_configuration.h"
#include "chromeos/printing/usb_printer_id.h"
#include "services/device/public/cpp/usb/usb_utils.h"
#include "services/device/public/mojom/usb_device.mojom.h"
#include "services/device/public/mojom/usb_enumeration_options.mojom.h"
#include "services/device/public/mojom/usb_manager.mojom.h"
#include "ui/base/l10n/l10n_util.h"
namespace ash {
namespace {
using device::mojom::UsbDeviceInfo;
// Base class used for printer USB interfaces
// (https://www.usb.org/developers/defined_class).
constexpr uint8_t kPrinterInterfaceClass = 7;
// Subclass used for printers
// (http://www.usb.org/developers/docs/devclass_docs/usbprint11a021811.pdf).
constexpr uint8_t kPrinterInterfaceSubclass = 1;
// Protocol for ippusb printing.
// (http://www.usb.org/developers/docs/devclass_docs/IPP.zip).
constexpr uint8_t kPrinterIppusbProtocol = 4;
// Configuration for a GET_DEVICE_ID Printer Class-Specific Request.
const int kGetDeviceIdRequest = 0;
const int kDefaultInterface = 0;
const int kDefaultConfiguration = 0;
// Callback for device.mojom.UsbDevice.ControlTransferIn.
// Expects |data| to hold a newly queried Device ID.
void OnControlTransfer(mojo::Remote<device::mojom::UsbDevice> device,
GetDeviceIdCallback cb,
device::mojom::UsbTransferStatus status,
base::span<const uint8_t> data) {
if (status != device::mojom::UsbTransferStatus::COMPLETED || data.empty()) {
return std::move(cb).Run({});
}
// Cleanup device.
device->ReleaseInterface(kDefaultInterface, base::DoNothing());
device->Close(base::DoNothing());
return std::move(cb).Run(chromeos::UsbPrinterId(data));
}
// Callback for device.mojom.UsbDevice.ClaimInterface.
// If interface was claimed successfully, attempts to query printer for a
// Device ID.
void OnClaimInterface(mojo::Remote<device::mojom::UsbDevice> device,
GetDeviceIdCallback cb,
device::mojom::UsbClaimInterfaceResult result) {
if (result != device::mojom::UsbClaimInterfaceResult::kSuccess) {
return std::move(cb).Run({});
}
auto params = device::mojom::UsbControlTransferParams::New();
params->type = device::mojom::UsbControlTransferType::CLASS;
params->recipient = device::mojom::UsbControlTransferRecipient::INTERFACE;
params->request = kGetDeviceIdRequest;
params->value = kDefaultConfiguration; // default config index
params->index = kDefaultInterface; // default interface index
// Query for IEEE1284 string.
auto* device_raw = device.get();
device_raw->ControlTransferIn(
std::move(params), 255 /* max size */, 2000 /* 2 second timeout */,
base::BindOnce(OnControlTransfer, std::move(device), std::move(cb)));
}
// Callback for device.mojom.UsbDevice.Open.
// If device was opened successfully, attempts to claim printer's default
// interface.
void OnDeviceOpen(mojo::Remote<device::mojom::UsbDevice> device,
GetDeviceIdCallback cb,
device::mojom::UsbOpenDeviceError error) {
if (error != device::mojom::UsbOpenDeviceError::OK || !device) {
return std::move(cb).Run({});
}
// Claim interface.
auto* device_raw = device.get();
device_raw->ClaimInterface(
kDefaultInterface,
base::BindOnce(OnClaimInterface, std::move(device), std::move(cb)));
}
// Incorporate the bytes of |val| into the incremental hash carried in |ctx| in
// big-endian order. |val| must be a simple integer type
template <typename T>
void MD5UpdateBigEndian(base::MD5Context* ctx, T val) {
static_assert(std::is_integral<T>::value, "Value must be an integer");
char buf[sizeof(T)];
base::WriteBigEndian(buf, val);
base::MD5Update(ctx, base::StringPiece(buf, sizeof(T)));
}
// Update the hash with the contents of |str|.
//
// UTF-16 strings are a bit fraught for consistency in memory representation;
// endianness is an issue, but more importantly, there are *optional* prefix
// codepoints to identify the endianness of the string.
//
// This is a long way to say "UTF-16 is hard to hash, let's just convert
// to UTF-8 and hash that", which avoids all of these issues.
void MD5UpdateString16(base::MD5Context* ctx, const std::u16string& str) {
std::string tmp = base::UTF16ToUTF8(str);
base::MD5Update(ctx, base::StringPiece(tmp.data(), tmp.size()));
}
// Get the usb printer id for |device|. This is used both as the identifier for
// the printer in the user's PrintersManager and as the name of the printer in
// CUPS, so it has to satisfy the naming restrictions of both. CUPS in
// particular is intolerant of much more than [a-z0-9_-], so we use that
// character set. This needs to be stable for a given device, but as unique as
// possible for that device. So we basically toss every bit of stable
// information from the device into an MD5 hash, and then hexify the hash value
// as a suffix to "usb-" as the final printer id.
std::string CreateUsbPrinterId(const UsbDeviceInfo& device_info) {
// Paranoid checks; in the unlikely event someone messes with the USB device
// definition, our (supposedly stable) hashes will change.
static_assert(sizeof(device_info.class_code) == 1, "Class size changed");
static_assert(sizeof(device_info.subclass_code) == 1,
"Subclass size changed");
static_assert(sizeof(device_info.protocol_code) == 1,
"Protocol size changed");
static_assert(sizeof(device_info.vendor_id) == 2, "Vendor id size changed");
static_assert(sizeof(device_info.product_id) == 2, "Product id size changed");
static_assert(sizeof(device::GetDeviceVersion(device_info)) == 2,
"Version size changed");
base::MD5Context ctx;
base::MD5Init(&ctx);
MD5UpdateBigEndian(&ctx, device_info.class_code);
MD5UpdateBigEndian(&ctx, device_info.subclass_code);
MD5UpdateBigEndian(&ctx, device_info.protocol_code);
MD5UpdateBigEndian(&ctx, device_info.vendor_id);
MD5UpdateBigEndian(&ctx, device_info.product_id);
MD5UpdateBigEndian(&ctx, device::GetDeviceVersion(device_info));
MD5UpdateString16(&ctx, GetManufacturerName(device_info));
MD5UpdateString16(&ctx, GetProductName(device_info));
MD5UpdateString16(&ctx, GetSerialNumber(device_info));
base::MD5Digest digest;
base::MD5Final(&digest, &ctx);
return base::StringPrintf("usb-%s", base::MD5DigestToBase16(digest).c_str());
}
// Creates a mojom filter which can be used to identify a basic USB printer.
mojo::StructPtr<device::mojom::UsbDeviceFilter> CreatePrinterFilter() {
auto printer_filter = device::mojom::UsbDeviceFilter::New();
printer_filter->has_class_code = true;
printer_filter->class_code = kPrinterInterfaceClass;
printer_filter->has_subclass_code = true;
printer_filter->subclass_code = kPrinterInterfaceSubclass;
return printer_filter;
}
bool UsbDeviceSupportsIppusb(const UsbDeviceInfo& device_info) {
auto printer_filter = CreatePrinterFilter();
printer_filter->has_protocol_code = true;
printer_filter->protocol_code = kPrinterIppusbProtocol;
return device::UsbDeviceFilterMatches(*printer_filter, device_info);
}
// Convert the interesting details of a device to a string, for
// logging/debugging.
std::string UsbPrinterDeviceDetailsAsString(const UsbDeviceInfo& device_info) {
return base::StringPrintf(
" guid: %s\n"
" usb version: %d\n"
" device class: %d\n"
" device subclass: %d\n"
" device protocol: %d\n"
" vendor id: %04x\n"
" product id: %04x\n"
" device version: %d\n"
" manufacturer string: %s\n"
" product string: %s\n"
" serial number: %s",
device_info.guid.c_str(), device::GetUsbVersion(device_info),
device_info.class_code, device_info.subclass_code,
device_info.protocol_code, device_info.vendor_id, device_info.product_id,
device::GetDeviceVersion(device_info),
base::UTF16ToUTF8(GetManufacturerName(device_info)).c_str(),
base::UTF16ToUTF8(GetProductName(device_info)).c_str(),
base::UTF16ToUTF8(GetSerialNumber(device_info)).c_str());
}
// Gets the URI CUPS would use to refer to this USB device. Assumes device
// is a printer.
chromeos::Uri UsbPrinterUri(const UsbDeviceInfo& device_info) {
// Note that serial may, for some devices, be empty or bogus (all zeros, non
// unique, or otherwise not really a serial number), but having a non-unique
// or empty serial field in the URI still lets us print, it just means we
// don't have a way to uniquely identify a printer if there are multiple ones
// plugged in with the same VID/PID, so we may print to the *wrong* printer.
// There doesn't seem to be a robust solution to this problem; if printers
// don't supply a serial number, we don't have any reliable way to do that
// differentiation.
std::string serial = base::UTF16ToUTF8(GetSerialNumber(device_info));
chromeos::Uri uri;
uri.SetScheme("usb");
uri.SetHost(base::StringPrintf("%04x", device_info.vendor_id));
uri.SetPath({base::StringPrintf("%04x", device_info.product_id)});
uri.SetQuery({{"serial", serial}});
return uri;
}
} // namespace
std::u16string GetManufacturerName(const UsbDeviceInfo& device_info) {
return device_info.manufacturer_name.value_or(std::u16string());
}
std::u16string GetProductName(const UsbDeviceInfo& device_info) {
return device_info.product_name.value_or(std::u16string());
}
std::u16string GetSerialNumber(const UsbDeviceInfo& device_info) {
return device_info.serial_number.value_or(std::u16string());
}
bool UsbDeviceIsPrinter(const UsbDeviceInfo& device_info) {
auto printer_filter = CreatePrinterFilter();
return device::UsbDeviceFilterMatches(*printer_filter, device_info);
}
// Attempt to gather all the information we need to work with this printer by
// querying the USB device. This should only be called using devices we believe
// are printers, not arbitrary USB devices, as we may get weird partial results
// from arbitrary devices. The results are saved in the second parameter.
bool UsbDeviceToPrinter(const UsbDeviceInfo& device_info,
PrinterDetector::DetectedPrinter* entry) {
DCHECK(entry);
// Preflight all required fields and log errors if we find something wrong.
if (device_info.vendor_id == 0 || device_info.product_id == 0) {
LOG(ERROR) << "Failed to convert USB device to printer. Fields were:\n"
<< UsbPrinterDeviceDetailsAsString(device_info);
return false;
}
entry->ppd_search_data.usb_manufacturer =
base::UTF16ToUTF8(GetManufacturerName(device_info));
entry->ppd_search_data.usb_model =
base::UTF16ToUTF8(GetProductName(device_info));
const std::string& make = entry->ppd_search_data.usb_manufacturer;
const std::string& model = entry->ppd_search_data.usb_model;
// Synthesize make-and-model string for printer identification.
entry->printer.set_make_and_model(base::JoinString({make, model}, " "));
// Construct the display name by however much of the manufacturer/model
// information that we have available.
if (make.empty() && model.empty()) {
entry->printer.set_display_name(
l10n_util::GetStringUTF8(IDS_USB_PRINTER_UNKNOWN_DISPLAY_NAME));
} else if (!make.empty() && !model.empty()) {
entry->printer.set_display_name(l10n_util::GetStringFUTF8(
IDS_USB_PRINTER_DISPLAY_NAME, base::UTF8ToUTF16(make),
base::UTF8ToUTF16(model)));
} else {
// Exactly one string is present.
DCHECK_NE(make.empty(), model.empty());
entry->printer.set_display_name(
l10n_util::GetStringFUTF8(IDS_USB_PRINTER_DISPLAY_NAME_MAKE_OR_MODEL,
base::UTF8ToUTF16(make + model)));
}
entry->printer.set_description(entry->printer.display_name());
entry->printer.SetUri(UsbPrinterUri(device_info));
entry->printer.set_id(CreateUsbPrinterId(device_info));
entry->printer.set_supports_ippusb(UsbDeviceSupportsIppusb(device_info));
return true;
}
void GetDeviceId(mojo::Remote<device::mojom::UsbDevice> device,
GetDeviceIdCallback cb) {
// Open device.
auto* device_raw = device.get();
device_raw->Open(
base::BindOnce(OnDeviceOpen, std::move(device), std::move(cb)));
}
} // namespace ash