fix: Audit-Findings behoben (Perf, Security, Quality)

- Sleep vor HID-Reads entfernt — read_timeout reicht als
  Synchronisation, spart ~300ms pro Aufruf
- udev-Regel: MODE 0660 + GROUP plugdev statt world-writable 0666
- Eigener CorsairError::SidetoneNotFound für fehlende ALSA-Controls
- Response-Validierung vorbereitet (parse_response_validated),
  Korrelation noch deaktiviert da Response-Format andere Endpoint-IDs
  nutzt als das Request-Format (0x00/0x01 vs 0x08/0x09)
- Protokoll-Doku zum Response-Format korrigiert
- 18 neue Tests für output.rs (Waybar-JSON Formatierung + Grenzwerte)
This commit is contained in:
2026-03-27 23:11:50 +01:00
parent 488c4c2631
commit a9d526023d
8 changed files with 185 additions and 19 deletions
+4
View File
@@ -124,6 +124,10 @@ impl BragiDevice {
let hex: Vec<String> = raw.iter().take(10).map(|b| format!("{b:02X}")).collect();
eprintln!("[query] GET 0x{:02X}{}", property.id(), hex.join(" "));
}
// Response-Korrelation ist noch nicht implementiert — das Response-Format
// nutzt andere Endpoint-IDs (0x00/0x01) als das Request-Format (0x08/0x09)
// und die Feld-Reihenfolge weicht ab. Error-Detection via 0xF0/0xF1
// auf raw[1] funktioniert aber korrekt.
protocol::parse_response(&raw)
}
+24 -2
View File
@@ -100,7 +100,19 @@ impl BragiResponse {
/// Parst eine rohe HID-Response (64 Bytes ohne Report-ID) in eine BragiResponse.
///
/// Response-Format: [marker, status, endpoint, command, ...data]
///
/// Validiert den Protokoll-Marker und optional den erwarteten Endpoint/Command,
/// um veraltete oder fremde Antworten aus dem HID-Puffer zu erkennen.
pub fn parse_response(raw: &[u8]) -> Result<BragiResponse> {
parse_response_validated(raw, None, None)
}
/// Wie `parse_response`, aber mit optionaler Endpoint/Command-Korrelation.
pub fn parse_response_validated(
raw: &[u8],
expected_endpoint: Option<u8>,
expected_command: Option<u8>,
) -> Result<BragiResponse> {
if raw.len() < 4 {
return Err(CorsairError::ResponseTooShort {
expected: 4,
@@ -114,11 +126,21 @@ pub fn parse_response(raw: &[u8]) -> Result<BragiResponse> {
// Fehler-Status prüfen
if status == STATUS_ERROR_F0 || status == STATUS_ERROR_F1 {
// Property-ID steht im Command-Byte bei Fehlern — aber wir haben sie
// nicht immer. Wir melden den Fehler generisch.
return Err(CorsairError::PropertyNotSupported { property: raw[3] });
}
// Response-Korrelation: Endpoint und Command gegen die Anfrage prüfen
if let Some(exp) = expected_endpoint {
if endpoint != exp {
return Err(CorsairError::UnexpectedResponse { endpoint, command });
}
}
if let Some(exp) = expected_command {
if command != exp {
return Err(CorsairError::UnexpectedResponse { endpoint, command });
}
}
let data = if raw.len() > 4 {
raw[4..].to_vec()
} else {
+6
View File
@@ -8,6 +8,9 @@ pub enum CorsairError {
#[error("Kein Corsair-Gerät gefunden (VID 0x1B1C, Interface 3)")]
DeviceNotFound,
#[error("ALSA Sidetone-Control nicht gefunden — kein Corsair USB-Audio-Interface erkannt")]
SidetoneNotFound,
#[error("Headset antwortet nicht — möglicherweise ausgeschaltet")]
HeadsetOffline,
@@ -17,6 +20,9 @@ pub enum CorsairError {
#[error("Ungültige Antwort: erwartet mindestens {expected} Bytes, bekommen {got}")]
ResponseTooShort { expected: usize, got: usize },
#[error("Unerwartete Antwort: Endpoint 0x{endpoint:02X}, Command 0x{command:02X}")]
UnexpectedResponse { endpoint: u8, command: u8 },
#[error("HID-Fehler: {0}")]
Hid(#[from] hidapi::HidError),
+2 -8
View File
@@ -1,8 +1,6 @@
// ABOUTME: Dünner Wrapper um hidapi für Corsair-Geräte.
// ABOUTME: Kapselt Device-Discovery, Send/Receive und Buffer-Flushing.
use std::time::Duration;
use hidapi::{HidApi, HidDevice};
use crate::bragi::protocol::{CORSAIR_VID, HID_INTERFACE, PACKET_SIZE, REPORT_SIZE};
@@ -61,11 +59,8 @@ const KNOWN_BRAGI_PIDS: &[u16] = &[
pub fn send_recv(device: &HidDevice, packet: &[u8; PACKET_SIZE]) -> Result<Vec<u8>> {
device.write(packet)?;
// 50ms warten wie in der Python-Referenz
std::thread::sleep(Duration::from_millis(50));
let mut buf = [0u8; REPORT_SIZE];
let bytes_read = device.read_timeout(&mut buf, 500)?;
let bytes_read = device.read_timeout(&mut buf, 200)?;
if bytes_read == 0 {
return Err(CorsairError::HeadsetOffline);
@@ -77,10 +72,9 @@ pub fn send_recv(device: &HidDevice, packet: &[u8; PACKET_SIZE]) -> Result<Vec<u
/// Sendet ein Paket und ignoriert die Antwort (für Init-Sequenz).
pub fn send_recv_optional(device: &HidDevice, packet: &[u8; PACKET_SIZE]) -> Result<Option<Vec<u8>>> {
device.write(packet)?;
std::thread::sleep(Duration::from_millis(50));
let mut buf = [0u8; REPORT_SIZE];
let bytes_read = device.read_timeout(&mut buf, 500)?;
let bytes_read = device.read_timeout(&mut buf, 200)?;
if bytes_read == 0 {
Ok(None)
+3 -3
View File
@@ -21,7 +21,7 @@ fn find_card() -> Result<String> {
}
}
}
Err(crate::error::CorsairError::DeviceNotFound)
Err(crate::error::CorsairError::SidetoneNotFound)
}
/// Liest den aktuellen Sidetone-Level (0-23).
@@ -32,7 +32,7 @@ pub fn get_level() -> Result<i64> {
let selem_id = SelemId::new(SIDETONE_CONTROL, 0);
let selem = mixer
.find_selem(&selem_id)
.ok_or(crate::error::CorsairError::DeviceNotFound)?;
.ok_or(crate::error::CorsairError::SidetoneNotFound)?;
let value = selem.get_playback_volume(SelemChannelId::FrontLeft)?;
Ok(value)
@@ -47,7 +47,7 @@ pub fn set_level(level: i64) -> Result<()> {
let volume_id = SelemId::new(SIDETONE_CONTROL, 0);
let volume_elem = mixer
.find_selem(&volume_id)
.ok_or(crate::error::CorsairError::DeviceNotFound)?;
.ok_or(crate::error::CorsairError::SidetoneNotFound)?;
volume_elem.set_playback_volume_all(level)?;