| // 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 <stdint.h> |
| |
| #include <algorithm> |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/containers/contains.h" |
| #include "base/containers/fixed_flat_set.h" |
| #include "base/containers/span.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/numerics/byte_conversions.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.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 "components/device_event_log/device_event_log.h" |
| #include "crypto/obsolete/md5.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 printing { |
| crypto::obsolete::Md5 MakeMd5HasherForUsbPrinterUtil() { |
| return {}; |
| } |
| } // namespace printing |
| |
| 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; |
| |
| // Generic USB make_and_model strings that are reused across device. |
| bool IsGenericUsbDescription(const std::string& make_and_model) { |
| static constexpr auto kGenericUsbModels = |
| base::MakeFixedFlatSet<std::string_view>({ |
| "canon canon capt usb device", |
| "epson usb1.1 mfp(hi-speed)", |
| "epson usb2.0 mfp(hi-speed)", |
| "epson usb2.0 printer (hi-speed)", |
| "epson usb mfp", |
| "epson usb printer", |
| "seiko epson usb mfp", |
| "oki data corp usb device", |
| }); |
| return base::Contains(kGenericUsbModels, make_and_model); |
| } |
| |
| // Callback for device.mojom.UsbDevice.ControlTransferIn. |
| // Expects |data| to hold a newly queried Device ID. |
| void OnControlTransfer(mojo::Remote<device::mojom::UsbDevice> device, |
| uint8_t interface_num, |
| 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(interface_num, base::DoNothing()); |
| device->Close(base::DoNothing()); |
| |
| return std::move(cb).Run(chromeos::UsbPrinterId(data)); |
| } |
| |
| // Callback for device.mojom.UsbDevice.SetInterfaceAlternateSetting. |
| // If interface was set successfully, attempts to query printer for a |
| // Device ID. |
| void OnAlternateSelected(mojo::Remote<device::mojom::UsbDevice> device, |
| const internal::PrinterInterfaceTarget& target, |
| GetDeviceIdCallback cb, |
| bool result) { |
| if (!result) { |
| PRINTER_LOG(ERROR) << "USB printer " << target.vidpid |
| << " failed to set interface " |
| << target.interface << " to alternate " |
| << target.alternate; |
| 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 = target.config; |
| params->index = (uint16_t(target.interface) << 8) | (target.alternate); |
| |
| // 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), target.interface, |
| std::move(cb))); |
| } |
| |
| // Callback for device.mojom.UsbDevice.ClaimInterface. |
| // If interface was claimed successfully, either sets the target alternate or |
| // continues directly to querying Device ID. |
| void OnClaimInterface(mojo::Remote<device::mojom::UsbDevice> device, |
| const internal::PrinterInterfaceTarget& target, |
| GetDeviceIdCallback cb, |
| device::mojom::UsbClaimInterfaceResult result) { |
| if (result != device::mojom::UsbClaimInterfaceResult::kSuccess) { |
| PRINTER_LOG(ERROR) << "USB printer " << target.vidpid |
| << " failed to claim interface " |
| << target.interface << ": " << result; |
| return std::move(cb).Run({}); |
| } |
| |
| // Set alternate or directly proceed to next step if the interface only has |
| // one alternate. |
| if (target.set_alternate) { |
| auto* device_raw = device.get(); |
| device_raw->SetInterfaceAlternateSetting( |
| target.interface, target.alternate, |
| base::BindOnce(OnAlternateSelected, std::move(device), |
| std::move(target), std::move(cb))); |
| } else { |
| OnAlternateSelected(std::move(device), std::move(target), std::move(cb), |
| true); |
| } |
| } |
| |
| // 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, |
| const internal::PrinterInterfaceTarget& target, |
| GetDeviceIdCallback cb, |
| device::mojom::UsbOpenDeviceResultPtr result) { |
| if (result->is_error() || !device) { |
| return std::move(cb).Run({}); |
| } |
| |
| // Claim target interface. |
| auto* device_raw = device.get(); |
| device_raw->ClaimInterface(target.interface, |
| base::BindOnce(OnClaimInterface, std::move(device), |
| target, std::move(cb))); |
| } |
| |
| // Incorporate the byte |val| into the incremental hash carried in |md5|. |
| void Md5UpdateU8(crypto::obsolete::Md5& md5, base::StrictNumeric<uint8_t> val) { |
| uint8_t tmp = val; |
| md5.Update(base::span_from_ref(tmp)); |
| } |
| |
| // Incorporate |val| into |md5| as a big-endian value. |
| void Md5UpdateU16BigEndian(crypto::obsolete::Md5& md5, |
| base::StrictNumeric<uint16_t> val) { |
| md5.Update(base::U16ToBigEndian(val)); |
| } |
| |
| // 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(crypto::obsolete::Md5& md5, const std::u16string& str) { |
| md5.Update(base::UTF16ToUTF8(str)); |
| } |
| |
| // 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"); |
| |
| auto md5 = ash::printing::MakeMd5HasherForUsbPrinterUtil(); |
| Md5UpdateU8(md5, device_info.class_code); |
| Md5UpdateU8(md5, device_info.subclass_code); |
| Md5UpdateU8(md5, device_info.protocol_code); |
| Md5UpdateU16BigEndian(md5, device_info.vendor_id); |
| Md5UpdateU16BigEndian(md5, device_info.product_id); |
| Md5UpdateU16BigEndian(md5, device::GetDeviceVersion(device_info)); |
| md5.Update(GetManufacturerName(device_info)); |
| md5.Update(GetProductName(device_info)); |
| Md5UpdateString16(md5, GetSerialNumber(device_info)); |
| return base::StringPrintf("usb-%s", |
| base::ToLowerASCII(base::HexEncode(md5.Finish()))); |
| } |
| |
| // 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), |
| GetManufacturerName(device_info).c_str(), |
| 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 |
| |
| namespace internal { |
| |
| // Find the best interface to send a printer-specific GET_DEVICE_ID request to. |
| // Not every printer has a printer class interface at alternate 0 of interface |
| // 0. First, try to find the first interface that does have printer class |
| // without any alternates. If that fails, select the first interface that has |
| // printer class on any of the alternates and mark that SET_INTERFACE will need |
| // to be called. |
| std::optional<PrinterInterfaceTarget> FindPrinterInterfaceTarget( |
| const device::mojom::UsbDeviceInfo& device_info) { |
| std::optional<PrinterInterfaceTarget> candidate; |
| for (uint8_t config_idx = 0; |
| const auto& config : device_info.configurations) { |
| for (uint8_t iface_idx = 0; const auto& iface : config->interfaces) { |
| for (uint8_t alt_idx = 0; const auto& alt : iface->alternates) { |
| if (alt->class_code == kPrinterInterfaceClass && |
| alt->subclass_code == kPrinterInterfaceSubclass) { |
| std::string vidpid = base::StringPrintf( |
| "%04x:%04x", device_info.vendor_id, device_info.product_id); |
| if (iface->alternates.size() == 1) { |
| return PrinterInterfaceTarget{ |
| std::move(vidpid), config_idx, iface_idx, alt_idx, false, |
| }; |
| } else if (!candidate.has_value()) { |
| candidate.emplace(std::move(vidpid), config_idx, iface_idx, alt_idx, |
| true); |
| } |
| } |
| alt_idx++; |
| } |
| iface_idx++; |
| } |
| config_idx++; |
| } |
| |
| if (candidate.has_value()) { |
| return candidate; |
| } |
| |
| return std::nullopt; |
| } |
| |
| } // namespace internal |
| |
| std::string GuessEffectiveMakeAndModel( |
| const device::mojom::UsbDeviceInfo& device_info) { |
| return base::StrCat( |
| {GetManufacturerName(device_info), " ", GetProductName(device_info)}); |
| } |
| |
| std::string GetManufacturerName(const UsbDeviceInfo& device_info) { |
| return base::UTF16ToUTF8( |
| device_info.manufacturer_name.value_or(std::u16string())); |
| } |
| |
| std::string GetProductName(const UsbDeviceInfo& device_info) { |
| const std::string manufacturer = |
| base::StrCat({GetManufacturerName(device_info), " "}); |
| std::string model = |
| base::UTF16ToUTF8(device_info.product_name.value_or(std::u16string())); |
| |
| // Some devices have the manufacturer duplicated in the product |
| // string. This needs to be removed to remain consistent with the make |
| // and model strings in the ppd index. |
| if (base::StartsWith(model, manufacturer, |
| base::CompareCase::INSENSITIVE_ASCII)) { |
| model.erase(0, manufacturer.size()); |
| } |
| return model; |
| } |
| |
| std::u16string GetSerialNumber(const UsbDeviceInfo& device_info) { |
| // If the device does not have a serial number or has an empty serial number, |
| // use '?' so this matches the convention that CUPS uses for 'no serial |
| // number'. |
| if (!device_info.serial_number.has_value() || |
| device_info.serial_number.value().empty()) { |
| return u"?"; |
| } |
| return device_info.serial_number.value(); |
| } |
| |
| 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 = GetManufacturerName(device_info); |
| entry->ppd_search_data.usb_model = 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(GuessEffectiveMakeAndModel(device_info)); |
| |
| entry->printer.set_display_name(MakeDisplayName(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(const device::mojom::UsbDeviceInfo& device_info, |
| mojo::Remote<device::mojom::UsbDevice> device, |
| GetDeviceIdCallback cb) { |
| // Find printer class interface so the request can be directed to it. |
| auto target = internal::FindPrinterInterfaceTarget(device_info); |
| if (!target.has_value()) { |
| PRINTER_LOG(ERROR) << "No printer interface found for device: " |
| << UsbPrinterDeviceDetailsAsString(device_info); |
| return std::move(cb).Run({}); |
| } |
| |
| // Open device. |
| auto* device_raw = device.get(); |
| device_raw->Open(base::BindOnce(OnDeviceOpen, std::move(device), |
| std::move(target.value()), std::move(cb))); |
| } |
| |
| std::string MakeDisplayName(const std::string& make, const std::string& model) { |
| // Construct the display name by however much of the manufacturer/model |
| // information that we have available. |
| if (make.empty() && model.empty()) { |
| return l10n_util::GetStringUTF8(IDS_USB_PRINTER_UNKNOWN_DISPLAY_NAME); |
| } else if (!make.empty() && !model.empty()) { |
| return 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()); |
| return l10n_util::GetStringFUTF8(IDS_USB_PRINTER_DISPLAY_NAME_MAKE_OR_MODEL, |
| base::UTF8ToUTF16(make + model)); |
| } |
| } |
| |
| void UpdateSearchDataFromDeviceId(const chromeos::UsbPrinterId& device_id, |
| PrinterDetector::DetectedPrinter* printer) { |
| // If the IEEE1284 device info looks complete and doesn't match the USB |
| // string descriptors, add an additional PPD search string. In addition, if |
| // the USB make_and_model is empty or matches known generic strings, replace |
| // the entire device description with the values from the IEEE1284 info. |
| const std::string& usb_make = device_id.make(); |
| const std::string& usb_model = device_id.model(); |
| if (usb_make.empty() || usb_model.empty()) { |
| return; |
| } |
| std::string usb_make_and_model = base::StrCat({usb_make, " ", usb_model}); |
| if (base::CompareCaseInsensitiveASCII(printer->printer.make_and_model(), |
| usb_make_and_model) == 0) { |
| return; |
| } |
| |
| if (base::TrimWhitespaceASCII(printer->printer.make_and_model(), |
| base::TRIM_ALL) |
| .empty() || |
| IsGenericUsbDescription( |
| base::ToLowerASCII(printer->printer.make_and_model()))) { |
| PRINTER_LOG(EVENT) << printer->printer.make_and_model() |
| << " replaced with USB device info: " |
| << usb_make_and_model; |
| printer->printer.set_display_name(MakeDisplayName(usb_make, usb_model)); |
| printer->printer.set_description(printer->printer.display_name()); |
| printer->printer.set_make_and_model(usb_make_and_model); |
| printer->ppd_search_data.make_and_model.front() = |
| base::ToLowerASCII(usb_make_and_model); |
| } else { |
| // Not a generic string, but still add the IEEE 1284 ID as an additional |
| // possible PPD match. |
| printer->ppd_search_data.make_and_model.push_back( |
| base::ToLowerASCII(usb_make_and_model)); |
| } |
| } |
| |
| } // namespace ash |