lesavka/common/src/hid.rs

135 lines
4.8 KiB
Rust

//! Shared HID mapping helpers used by both the client and server crates.
pub type KeyboardHidReport = [u8; 8];
/// Map a printable character to a USB HID usage plus modifier byte.
///
/// Inputs: a Unicode scalar value that should be typed through the HID gadget.
/// Outputs: `Some((usage, modifiers))` for supported ASCII characters, or
/// `None` when the character cannot be represented by the current keyboard map.
/// Why: server-side paste injection and client-side keymap tests must agree on
/// the exact HID encoding so they do not drift apart over time.
#[must_use]
pub fn char_to_usage(c: char) -> Option<(u8, u8)> {
let shift = 0x02; // left shift in HID modifier byte
match c {
'a'..='z' => Some((0x04 + (c as u8 - b'a'), 0)),
'A'..='Z' => Some((0x04 + (c as u8 - b'A'), shift)),
'1'..='9' => Some((0x1E + (c as u8 - b'1'), 0)),
'0' => Some((0x27, 0)),
'!' => Some((0x1E, shift)),
'@' => Some((0x1F, shift)),
'#' => Some((0x20, shift)),
'$' => Some((0x21, shift)),
'%' => Some((0x22, shift)),
'^' => Some((0x23, shift)),
'&' => Some((0x24, shift)),
'*' => Some((0x25, shift)),
'(' => Some((0x26, shift)),
')' => Some((0x27, shift)),
'-' => Some((0x2D, 0)),
'_' => Some((0x2D, shift)),
'=' => Some((0x2E, 0)),
'+' => Some((0x2E, shift)),
'[' => Some((0x2F, 0)),
'{' => Some((0x2F, shift)),
']' => Some((0x30, 0)),
'}' => Some((0x30, shift)),
'\\' => Some((0x31, 0)),
'|' => Some((0x31, shift)),
';' => Some((0x33, 0)),
':' => Some((0x33, shift)),
'\'' => Some((0x34, 0)),
'"' => Some((0x34, shift)),
'`' => Some((0x35, 0)),
'~' => Some((0x35, shift)),
',' => Some((0x36, 0)),
'<' => Some((0x36, shift)),
'.' => Some((0x37, 0)),
'>' => Some((0x37, shift)),
'/' => Some((0x38, 0)),
'?' => Some((0x38, shift)),
' ' => Some((0x2C, 0)),
'\n' | '\r' => Some((0x28, 0)),
'\t' => Some((0x2B, 0)),
_ => None,
}
}
/// Append the HID report sequence needed to type a character.
///
/// Inputs: an output buffer plus the character that should be typed.
/// Outputs: `true` when the character is supported and reports were appended.
/// Why: some targets miss uppercase or shifted characters when they arrive as a
/// single modifier+usage pulse, so we emit a more human-like modifier press,
/// key press, key release, and modifier release sequence.
pub fn append_char_reports(out: &mut Vec<KeyboardHidReport>, c: char) -> bool {
let Some((usage, mods)) = char_to_usage(c) else {
return false;
};
if mods != 0 {
out.push([mods, 0, 0, 0, 0, 0, 0, 0]);
out.push([mods, 0, usage, 0, 0, 0, 0, 0]);
out.push([mods, 0, 0, 0, 0, 0, 0, 0]);
out.push([0; 8]);
} else {
out.push([0, 0, usage, 0, 0, 0, 0, 0]);
out.push([0; 8]);
}
true
}
#[cfg(test)]
mod tests {
use super::{append_char_reports, char_to_usage};
#[test]
fn char_to_usage_maps_letters_numbers_and_shifted_symbols() {
assert_eq!(char_to_usage('a'), Some((0x04, 0)));
assert_eq!(char_to_usage('Z'), Some((0x1D, 0x02)));
assert_eq!(char_to_usage('0'), Some((0x27, 0)));
assert_eq!(char_to_usage('9'), Some((0x26, 0)));
assert_eq!(char_to_usage(' '), Some((0x2C, 0)));
assert_eq!(char_to_usage('\n'), Some((0x28, 0)));
assert_eq!(char_to_usage('\t'), Some((0x2B, 0)));
assert_eq!(char_to_usage('{'), Some((0x2F, 0x02)));
assert_eq!(char_to_usage('~'), Some((0x35, 0x02)));
assert_eq!(char_to_usage('?'), Some((0x38, 0x02)));
}
#[test]
fn char_to_usage_rejects_unsupported_chars() {
assert_eq!(char_to_usage('é'), None);
assert_eq!(char_to_usage('\u{2603}'), None);
}
#[test]
fn append_char_reports_expands_shifted_chars_into_four_steps() {
let mut reports = Vec::new();
assert!(append_char_reports(&mut reports, 'A'));
assert_eq!(reports.len(), 4);
assert_eq!(reports[0], [0x02, 0, 0, 0, 0, 0, 0, 0]);
assert_eq!(reports[1], [0x02, 0, 0x04, 0, 0, 0, 0, 0]);
assert_eq!(reports[2], [0x02, 0, 0, 0, 0, 0, 0, 0]);
assert_eq!(reports[3], [0; 8]);
}
#[test]
fn append_char_reports_keeps_unshifted_chars_as_press_and_release() {
let mut reports = Vec::new();
assert!(append_char_reports(&mut reports, 'a'));
assert_eq!(reports.len(), 2);
assert_eq!(reports[0], [0, 0, 0x04, 0, 0, 0, 0, 0]);
assert_eq!(reports[1], [0; 8]);
}
#[test]
fn append_char_reports_rejects_unsupported_chars() {
let mut reports = Vec::new();
assert!(!append_char_reports(&mut reports, '🙂'));
assert!(reports.is_empty());
}
}