fix: Audit-Befunde in Protokoll-Parsing, Error-Handling und Eingabe-Validierung

BragiResponse-Felder korrekt zugeordnet (endpoint=raw[1], command=raw[2],
status=raw[3]) gemäß Protokoll-Doku. PropertyNotSupported durch DeviceError
ersetzt, parse_response_validated in device.rs aktiviert, flush() mit
Iterationslimit gegen Endlosschleifen, Sidetone-Range per clap validiert
statt clamp, JSON-Escaping im hidpp-battery-waybar.sh, udev auf uaccess
umgestellt. 52 Tests grün.
This commit is contained in:
2026-03-28 00:37:36 +01:00
parent 05d138e922
commit 25eacfc02d
15 changed files with 208 additions and 48 deletions
+6 -3
View File
@@ -4,7 +4,10 @@
use hidapi::{HidApi, HidDevice};
use crate::bragi::properties::{self, BatteryStatus, Property};
use crate::bragi::protocol::{self, BragiResponse, ENDPOINT_HEADSET, ENDPOINT_RECEIVER};
use crate::bragi::protocol::{
self, BragiResponse, ENDPOINT_HEADSET, ENDPOINT_RECEIVER,
RESPONSE_ENDPOINT_HEADSET, CMD_GET, CMD_SET,
};
use crate::error::{CorsairError, Result};
use crate::hid;
@@ -128,7 +131,7 @@ 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(" "));
}
protocol::parse_response(&raw)
protocol::parse_response_validated(&raw, Some(RESPONSE_ENDPOINT_HEADSET), Some(CMD_GET))
}
/// Setzt eine Property auf dem Headset.
@@ -138,7 +141,7 @@ impl BragiDevice {
hid::flush(&self.device)?;
let packet = protocol::build_set_packet(ENDPOINT_HEADSET, property.id(), data);
let raw = hid::send_recv(&self.device, &packet)?;
protocol::parse_response(&raw)
protocol::parse_response_validated(&raw, Some(RESPONSE_ENDPOINT_HEADSET), Some(CMD_SET))
}
/// Batterie-Level in Prozent (0.0 - 100.0).
+1
View File
@@ -9,4 +9,5 @@ pub use device::BragiDevice;
pub use properties::{BatteryStatus, Property};
pub use protocol::{
BragiResponse, CORSAIR_VID, ENDPOINT_HEADSET, ENDPOINT_RECEIVER, HID_INTERFACE,
RESPONSE_ENDPOINT_HEADSET, RESPONSE_ENDPOINT_RECEIVER,
};
+19 -11
View File
@@ -33,6 +33,10 @@ pub const STATUS_OK: u8 = 0x00;
pub const STATUS_ERROR_F0: u8 = 0xF0;
pub const STATUS_ERROR_F1: u8 = 0xF1;
// Response-Endpoints (weichen von Request-Endpoints ab!)
pub const RESPONSE_ENDPOINT_RECEIVER: u8 = 0x00;
pub const RESPONSE_ENDPOINT_HEADSET: u8 = 0x01;
/// Baut ein Bragi-Paket (65 Bytes) für den HID-Versand.
///
/// Format: [0x00, 0x02, endpoint, command, property, ...data, 0x00-padding]
@@ -65,11 +69,14 @@ pub fn build_set_packet(endpoint: u8, property: u8, data: &[u8]) -> [u8; PACKET_
}
/// Geparste Bragi-Antwort.
///
/// Byte-Zuordnung laut Protokoll-Doku:
/// raw[0] = Report-Typ (0x01), raw[1] = Endpoint, raw[2] = Command, raw[3] = Status
#[derive(Debug, Clone)]
pub struct BragiResponse {
pub status: u8,
pub endpoint: u8,
pub command: u8,
pub status: u8,
pub data: Vec<u8>,
}
@@ -97,11 +104,11 @@ impl BragiResponse {
}
}
/// Parst eine rohe HID-Response (64 Bytes ohne Report-ID) in eine BragiResponse.
/// Parst eine rohe HID-Response (64 Bytes) in eine BragiResponse.
///
/// Response-Format: [marker, status, endpoint, command, ...data]
/// Response-Format: [report_type, endpoint, command, status, ...data]
///
/// Validiert den Protokoll-Marker und optional den erwarteten Endpoint/Command,
/// Validiert 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)
@@ -120,13 +127,14 @@ pub fn parse_response_validated(
});
}
let status = raw[1];
let endpoint = raw[2];
let command = raw[3];
// raw[0] = Report-Typ (0x01)
let endpoint = raw[1];
let command = raw[2];
let status = raw[3];
// Fehler-Status prüfen
if status == STATUS_ERROR_F0 || status == STATUS_ERROR_F1 {
return Err(CorsairError::PropertyNotSupported { property: raw[3] });
// Fehler-Status prüfen: Endpoint-Byte 0xF0/0xF1 signalisiert Fehler
if endpoint == STATUS_ERROR_F0 || endpoint == STATUS_ERROR_F1 {
return Err(CorsairError::DeviceError { status: endpoint });
}
// Response-Korrelation: Endpoint und Command gegen die Anfrage prüfen
@@ -148,9 +156,9 @@ pub fn parse_response_validated(
};
Ok(BragiResponse {
status,
endpoint,
command,
status,
data,
})
}
+2 -1
View File
@@ -22,7 +22,8 @@ pub enum Command {
/// Sidetone-Level lesen oder setzen (0-23, ALSA-Mixer)
Sidetone {
/// Sidetone-Level (0-23). Ohne Angabe wird der aktuelle Wert gelesen.
level: Option<i64>,
#[arg(value_parser = clap::value_parser!(u8).range(0..=23))]
level: Option<u8>,
},
/// LED-Helligkeit lesen oder setzen (0-1000)
+2 -2
View File
@@ -14,8 +14,8 @@ pub enum CorsairError {
#[error("Headset antwortet nicht — möglicherweise ausgeschaltet")]
HeadsetOffline,
#[error("Gerät-Fehler: Property 0x{property:02X} nicht unterstützt")]
PropertyNotSupported { property: u8 },
#[error("Gerät-Fehler: Status 0x{status:02X} (Property nicht unterstützt)")]
DeviceError { status: u8 },
#[error("Ungültige Antwort: erwartet mindestens {expected} Bytes, bekommen {got}")]
ResponseTooShort { expected: usize, got: usize },
+4 -1
View File
@@ -89,10 +89,13 @@ pub fn send_recv_optional(device: &HidDevice, packet: &[u8; PACKET_SIZE]) -> Res
}
}
/// Maximale Iterationen beim Flushen, um Endlosschleifen zu verhindern.
const MAX_FLUSH_ITERATIONS: usize = 64;
/// Leert den HID-Empfangspuffer (nonblocking reads bis leer).
pub fn flush(device: &HidDevice) -> Result<()> {
let mut buf = [0u8; REPORT_SIZE];
loop {
for _ in 0..MAX_FLUSH_ITERATIONS {
// read_timeout mit 0ms = nonblocking
let bytes_read = device.read_timeout(&mut buf, 0)?;
if bytes_read == 0 {
+1
View File
@@ -2,6 +2,7 @@
// ABOUTME: Re-exportiert alle Module für externe Nutzung.
pub mod bragi;
pub mod cli;
pub mod error;
pub mod hid;
pub mod output;
+2 -3
View File
@@ -25,9 +25,8 @@ fn run() -> error::Result<()> {
Command::Sidetone { level } => {
if let Some(value) = level {
let clamped = value.clamp(0, 23);
sidetone::set_level(clamped)?;
println!("{}", output::format_sidetone(clamped));
sidetone::set_level(value as i64)?;
println!("{}", output::format_sidetone(value as i64));
} else {
let current = sidetone::get_level()?;
println!("{}", output::format_sidetone(current));