hid-types 0.1.1

Rust types for working with USB HID report descriptors
Documentation
//! Details of descriptor item encoding.

use bitfield_struct::bitfield;
use num_enum::TryFromPrimitive;

use crate::id::tag::IntoTagType;

/// A one-byte tag+type+size value
#[bitfield(u8, debug = false)]
pub struct TagTypeSize {
    /// The bSize field.
    #[bits(2)]
    pub encoded_size: SizeBits,
    /// The bType field.
    #[bits(2)]
    pub ty: TypeBits,
    /// the bTag field.
    #[bits(4)]
    pub tag: u8,
}

impl TagTypeSize {
    /// Create a `TagTypeSize` from a tag and data length.
    pub fn from_tag<T: IntoTagType>(tag: T, data_len: usize) -> Self {
        let size = SizeBits::from_size(data_len);
        tag.encode_tag().with_encoded_size(size)
    }

    /// Decode the data size.
    pub fn size(self) -> Size {
        if self.ty() == TypeBits::Reserved
            && self.tag() == 0xF
            && self.encoded_size() == SizeBits::Two
        {
            // This is a "long item format" with a different size encoding
            Size::Long
        } else {
            Size::Short(self.encoded_size().size_bytes())
        }
    }
}

/// An integer value, in little-endian variable length form.
///
/// Drops any trailing zero bytes, but always returns a value of at least one byte.
pub fn encode_unsigned(data: &[u8]) -> &[u8] {
    if data.is_empty() {
        return data;
    }
    assert!(data.len() <= 4 && data.len() != 3);
    let count_zero = data.iter().rev().take_while(|&&b| b == 0).count();
    let mut truncated_len = data.len() - count_zero;
    // We choose to encode zero as [0] rather than [].
    if truncated_len == 0 {
        truncated_len = 1;
    }
    // We can't truncate to 3 bytes, since that length can't be encoded.
    if truncated_len == 3 {
        truncated_len = 4;
    }
    &data[..truncated_len]
}

/// An integer value, in little-endian variable length form.
///
/// Drops any trailing zero bytes, if that leaves the last byte with its
/// most-significant bit 0.
///
/// Drops any trailing `0xFF` bytes, if that leaves the last byte with the
/// most-significant bit set.
///
/// Because this is a signed value, leading zeros can only be dropped if
/// the following byte doesn't have the sign bit (MSB) set.
pub fn encode_signed(data: &[u8]) -> &[u8] {
    // look at a window of 2 bytes and determine if we can drop the MSB.
    fn can_drop(&&[lsb, msb]: &&[u8; 2]) -> bool {
        // A positive value that can be shortened
        (msb == 0 && ((lsb & 0x80) == 0)) ||
        // A negative value that can be shortened
        (msb == 0xFF) && ((lsb & 0x80) != 0)
    }

    if data.is_empty() {
        return data;
    }

    let count_zero = data.array_windows::<2>().rev().take_while(can_drop).count();
    let mut truncated_len = data.len() - count_zero;
    // We can't truncate to 3 bytes, since that length can't be encoded.
    if truncated_len == 3 {
        truncated_len = 4;
    }
    &data[..truncated_len]
}

/// A value indicating the encoded data size.
pub enum Size {
    /// A short payload, 0-4 bytes.
    Short(usize),
    /// A long payload.
    Long,
}

/// The bits specifying the data size.
#[expect(missing_docs)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum SizeBits {
    Zero = 0,
    One = 1,
    Two = 2,
    Four = 3,
}

impl SizeBits {
    /// The size, encoded for a binary report descriptor.
    pub const fn into_bits(self) -> u8 {
        self as _
    }

    /// Decode the size from an encoded byte value.
    ///
    /// The input values allowed are the same as those in the binary report descriptor (0-3).
    ///
    /// # Panics
    /// This function will panic if the input is out of the range 0-3.
    // Note: this needs to be a `const fn` for compatibility with the `bitfield` macro.
    pub const fn from_bits(value: u8) -> Self {
        match value {
            0 => Self::Zero,
            1 => Self::One,
            2 => Self::Two,
            3 => Self::Four,
            _ => panic!("SizeBits value out of range"),
        }
    }

    /// Return the number of bytes this field represents.
    ///
    /// Note: this value is only correct for the "short item format".
    pub fn size_bytes(self) -> usize {
        match self {
            SizeBits::Zero => 0,
            SizeBits::One => 1,
            SizeBits::Two => 2,
            SizeBits::Four => 4,
        }
    }

    /// Create `SizeBits` from an integer size.
    pub fn from_size(size: usize) -> Self {
        match size {
            0 => SizeBits::Zero,
            1 => SizeBits::One,
            2 => SizeBits::Two,
            4 => SizeBits::Four,
            n => panic!("improper short item size ({n})"),
        }
    }
}

/// The bits specifying the type of an item.
#[expect(missing_docs)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive)]
#[repr(u8)]
pub enum TypeBits {
    Main = 0,
    Global = 1,
    Local = 2,
    Reserved = 3,
}

impl TypeBits {
    /// Convert the `TypeBits` into an integer in the encoded form.
    pub const fn into_bits(self) -> u8 {
        self as _
    }

    /// Decode encoded type bits.
    ///
    /// The input values allowed are the same as those in the binary report descriptor (0-3).
    ///
    /// # Panics
    /// This function will panic if the input is out of the range 0-3.
    ///
    // Note: this needs to be a `const fn` for compatibility with the `bitfield` macro.
    pub const fn from_bits(value: u8) -> Self {
        match value {
            0 => Self::Main,
            1 => Self::Global,
            2 => Self::Local,
            3 => Self::Reserved,
            _ => panic!("TypeBits value out of range"),
        }
    }
}

#[cfg(feature = "std")]
mod std_impls {
    use super::*;
    use std::fmt::{self, Debug};

    impl Debug for TagTypeSize {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            f.debug_struct("TagTypeSize")
                .field("raw", &format_args!("{:#04x}", self.into_bits()))
                .field("size", &self.encoded_size().size_bytes())
                .field("type", &self.ty())
                .field("tag", &format_args!("{:#x}", self.tag()))
                .finish()
        }
    }
}

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

    #[test]
    fn test_unsigned_encoding() {
        // little-endian variable length encoding of 1, 2, or 4 bytes.
        // trailing zeros are always removed.
        let unsigned_values = [
            (0u32, &[0x00][..]),
            (0x7F, &[0x7F]),
            (0x80, &[0x80]),
            (0xFF, &[0xFF]),
            (0x100, &[0x00, 0x01]),
            (0xFFFF, &[0xFF, 0xFF]),
            (0x123456, &[0x56, 0x34, 0x12, 0x00]),
        ];
        for (value, expected) in unsigned_values {
            assert_eq!(expected, encode_unsigned(&value.to_le_bytes()))
        }
    }

    #[test]
    fn test_signed_encoding() {
        // little-endian variable length encoding of 1, 2, or 4 bytes.
        // trailing zeros or 0xFF bytes may be removed, as long as
        // the sign bit is preserved.
        let signed_values = [
            (0i32, &[0x00][..]),
            (1i32, &[0x01]),
            (0x7F, &[0x7F]),
            (0x80, &[0x80, 0x00]),
            (0xFF, &[0xFF, 0x00]),
            (0x100, &[0x00, 0x01]),
            (0xFFFF, &[0xFF, 0xFF, 0x00, 0x00]),
            (0x123456, &[0x56, 0x34, 0x12, 0x00]),
            (-1, &[0xFF]),
            (-128, &[0x80]),
            (-129, &[0x7F, 0xFF]),
            (-32768, &[0x00, 0x80]),
            (-32769, &[0xFF, 0x7F, 0xFF, 0xFF]),
            (-2147483648, &[0x00, 0x00, 0x00, 0x80]),
        ];
        for (value, expected) in signed_values {
            let bytes = value.to_le_bytes();
            let encoded = encode_signed(&bytes);
            assert_eq!(
                expected, encoded,
                "expected {:X?}, got {:X?}",
                expected, encoded
            )
        }
    }
}