135 lines
4.8 KiB
Rust
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());
|
|
}
|
|
}
|