ippusb 0.5.0

HTTP proxy for IPP-over-USB devices
Documentation
// Copyright 2025 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use log::info;
use rusb::{Direction, TransferType, UsbContext};

use crate::error::Error;
use crate::error::Result;

pub(crate) fn is_ippusb_interface(descriptor: &rusb::InterfaceDescriptor) -> bool {
    descriptor.class_code() == 0x07
        && descriptor.sub_class_code() == 0x01
        && descriptor.protocol_code() == 0x04
}

/// Check if the given device supports IPP-USB.
///
/// Given a rusb Device, search through the device's configurations to see if there is a
/// particular configuration that supports IPP-over-USB (aka IPP-USB).  If such a configuration
/// is found, return `Ok(true)`.  If there were no errors, but the device does not have such a
/// configuration, return `Ok(false)`.  If there was an error reading descriptors, return `Err`.
///
/// A device is considered to support IPP-USB if it has a configuration with at least two
/// IPP-USB interfaces.
///
/// An interface is considered an IPP-USB interface if all of the following are true:
///
/// *  The USB class is Printer (7).
/// *  The USB subclass is Printer (1).
/// *  The USB protocol is IPP-USB (4).
/// *  The interface contains a bulk-in and a bulk-out endpoint.
///
/// The device's configuration is not changed by this function.
pub fn device_supports_ippusb<T: UsbContext>(device: &rusb::Device<T>) -> Result<bool> {
    match IppusbDeviceInfo::new(device) {
        Ok(_) => Ok(true),
        Err(Error::NotIppUsb) => Ok(false),
        Err(e) => Err(e),
    }
}

/// The information for an interface descriptor that supports IPP-USB.
///
/// Bulk transfers can be read/written to the in/out endpoints, respectively.
#[derive(Copy, Clone, Eq, Ord, PartialEq, PartialOrd)]
pub(crate) struct IppusbDescriptor {
    pub interface_number: u8,
    pub alternate_setting: u8,
    pub in_endpoint: u8,
    pub out_endpoint: u8,
}

/// The configuration and interfaces that support IPP-USB for a USB device.
///
///  A valid IPP-USB device will have at least two interfaces.
pub(crate) struct IppusbDeviceInfo {
    pub config: u8,
    pub interfaces: Vec<IppusbDescriptor>,
}

impl IppusbDeviceInfo {
    /// Given a rusb Device, search through the device's configurations to see if there is a
    /// particular configuration that supports IPP-over-USB (aka IPP-USB).  If such a configuration
    /// is found, return an `IppusbDeviceInfo`, which specifies the configuration as well as the
    /// IPP-USB interfaces within that configuration.
    ///
    /// See the documentation for `device_supports_ippusb()` for the details of what IPP-USB
    /// support requires.
    ///
    /// If the given device does not support IPP-USB or the descriptors cannot be read, return
    /// `Err`.
    pub(crate) fn new<T: UsbContext>(device: &rusb::Device<T>) -> Result<Self> {
        let desc = device
            .device_descriptor()
            .map_err(Error::ReadDeviceDescriptor)?;
        for i in 0..desc.num_configurations() {
            let config = device
                .config_descriptor(i)
                .map_err(Error::ReadConfigDescriptor)?;

            let mut interfaces = Vec::new();
            for interface in config.interfaces() {
                'alternates: for alternate in interface.descriptors() {
                    if !is_ippusb_interface(&alternate) {
                        continue;
                    }
                    info!(
                        concat!(
                            "Device {}:{} - Found IPP-USB interface. ",
                            "config {}, interface {}, alternate {}"
                        ),
                        device.bus_number(),
                        device.address(),
                        config.number(),
                        interface.number(),
                        alternate.setting_number()
                    );

                    // Find the bulk in and out endpoints for this interface.
                    let mut in_endpoint: Option<u8> = None;
                    let mut out_endpoint: Option<u8> = None;
                    for endpoint in alternate.endpoint_descriptors() {
                        match (endpoint.direction(), endpoint.transfer_type()) {
                            (Direction::In, TransferType::Bulk) => {
                                in_endpoint.get_or_insert(endpoint.address());
                            }
                            (Direction::Out, TransferType::Bulk) => {
                                out_endpoint.get_or_insert(endpoint.address());
                            }
                            _ => {}
                        };

                        if in_endpoint.is_some() && out_endpoint.is_some() {
                            break;
                        }
                    }

                    if let (Some(in_endpoint), Some(out_endpoint)) = (in_endpoint, out_endpoint) {
                        interfaces.push(IppusbDescriptor {
                            interface_number: interface.number(),
                            alternate_setting: alternate.setting_number(),
                            in_endpoint,
                            out_endpoint,
                        });
                        // We must consider at most one alternate setting when detecting IPP-USB
                        // interfaces.
                        break 'alternates;
                    }
                }
            }

            // A device must have at least two IPP-USB interfaces in order to be considered an
            // IPP-USB device.
            if interfaces.len() >= 2 {
                return Ok(Self {
                    config: config.number(),
                    interfaces,
                });
            }
        }

        Err(Error::NotIppUsb)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // The IppusbDescriptor tests just restate what the generated Ord implementation will do.  They
    // are useful to capture the expectation that interface_number and alternate_setting are the
    // most important sort fields even if somebody reorders the struct members.

    #[test]
    fn compare_lowest_interface_first() {
        let lhs = IppusbDescriptor {
            interface_number: 1,
            alternate_setting: 1,
            in_endpoint: 1,
            out_endpoint: 2,
        };
        let rhs = IppusbDescriptor {
            interface_number: 0,
            alternate_setting: 2,
            in_endpoint: 3,
            out_endpoint: 4,
        };
        assert!(rhs < lhs);
    }

    #[test]
    fn compare_lowest_alternate_first() {
        let lhs = IppusbDescriptor {
            interface_number: 1,
            alternate_setting: 2,
            in_endpoint: 1,
            out_endpoint: 2,
        };
        let rhs = IppusbDescriptor {
            interface_number: 1,
            alternate_setting: 1,
            in_endpoint: 3,
            out_endpoint: 4,
        };
        assert!(rhs < lhs);
    }
}