chore: harden CI and shipped script, tidy internals (audit remediation)
CI / test (push) Successful in 5s
Update PKGBUILD version / update-pkgver (push) Successful in 0s

A four-axis audit found no HIGH/MEDIUM issues. This addresses the
remaining low-risk items and protects the release path:

- ci: add cargo test + clippy -D warnings gate on master/tags
- ci: reject malformed version tags before the pkgver sed substitution
- fix: move the shipped waybar wrapper's cache/lock to $XDG_RUNTIME_DIR
  (mode-700, no cross-user symlink) instead of predictable /tmp paths
- refactor: find_card() returns the open ALSA mixer, dropping a second
  open; collapse two if-let blocks into edition-2024 let-chains
- docs: record in DECISIONS.md why the audit's "dead code" stays (it
  mirrors the Bragi spec) and why the 5ms HID sleep is left as-is

Bump 0.1.4 -> 0.1.5: the shipped waybar script changes behavior.
This commit is contained in:
nevaforget committed 2026-06-23 16:03:35 +02:00
1 parent 6ec56a7968
commit 717c54c9c1
8 files changed
+61 -19

No files matched your search

+31
View File
@@ -0,0 +1,31 @@
# ABOUTME: Test + lint gate on push to master and on version tags.
# ABOUTME: Stops a broken build or regressed test from reaching a tagged release.
name: CI
on:
push:
branches:
- master
tags:
- 'v*'
jobs:
test:
runs-on: moonarch
steps:
- name: Checkout pushed commit
run: |
git clone http://gitea:3000/nevaforget/corsairctl.git repo
cd repo
git checkout "$GITHUB_SHA"
- name: cargo test
run: |
cd repo
cargo test --locked
- name: cargo clippy
run: |
cd repo
cargo clippy --all-targets -- -D warnings
+4
View File
@@ -17,6 +17,10 @@ jobs:
git clone --bare http://gitea:3000/nevaforget/corsairctl.git source.git
cd source.git
PKGVER=$(git describe --tags --abbrev=0 | sed 's/^v//')
if ! echo "$PKGVER" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "Invalid pkgver: $PKGVER"
exit 1
fi
echo "New pkgver: $PKGVER"
echo "$PKGVER" > /tmp/pkgver
Generated
+1 -1
View File
@@ -144,7 +144,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "corsairctl"
version = "0.1.4"
version = "0.1.5"
dependencies = [
"alsa",
"clap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "corsairctl"
version = "0.1.4"
version = "0.1.5"
edition = "2024"
description = "CLI tool for Corsair Bragi-protocol devices (HS80, etc.)"
license = "MIT"
+6
View File
@@ -23,3 +23,9 @@
- **Why**: The open-source publish plan (rename away from "Corsair", multi-protocol backend layer, crates.io, upstream to HeadsetControl) was dropped. The tool is for personal use across Dom's own machines, distributed as an Arch package via the private moonarch repo (Gitea registry at gitea.moonarch.de), used primarily as a Waybar widget.
- **Tradeoffs**: GPLv3 and the rename were only justified by public distribution and porting HeadsetControl (GPLv3) code. With no public release and no GPL-derived code in the tree, copyleft and the trademark rename buy nothing. MIT is simpler and reversible — switch to GPLv3 only if GPL code is later pulled in.
- **How**: Revert license to MIT (Cargo.toml + MIT LICENSE file). Keep the name `corsairctl`. PLAN.md retained as a record of the considered direction, no longer driving work.
## 2026-06-23 – Audit remediation: CI gate, script hardening; keep the protocol-spec model
- **Who**: Dom, F.R.I.D.A.Y.
- **Why**: A four-axis audit (quality/performance/security/hygiene) found no HIGH/MEDIUM issues; only a few low-risk items remained. Acted on the real ones, recorded what was deliberately left alone so a future audit does not re-flag it.
- **Tradeoffs**: The audit marked unused `Property` variants, the response endpoint constant, and `STATUS_OK`/`BragiResponse.status` as "dead code". Kept on purpose — the `Property` enum mirrors the full Bragi spec (`docs/bragi-protocol.md`) and `status` captures a real device byte; deleting would lose protocol fidelity for no functional gain. The 5 ms HID pre-read sleep was also left as-is — it is a deliberate fix against observed response mix-ups (`f3a4bf8`), not arbitrary latency.
- **How**: Added `ci.yaml` (`cargo test` + `clippy -D warnings` on master pushes and version tags) and a tag-format guard in `update-pkgver.yaml`. Moved the shipped Waybar wrapper's cache/lock from `/tmp` to `$XDG_RUNTIME_DIR` (mode-700, no cross-user symlink). `find_card()` now returns the already-open ALSA mixer instead of a card name, dropping a redundant second open. Collapsed two `if let` blocks into edition-2024 let-chains.
+3 -2
View File
@@ -2,8 +2,9 @@
# ABOUTME: Waybar-Wrapper für corsairctl — cached JSON-Output.
# ABOUTME: Verhindert dass mehrere Waybar-Bars gleichzeitig das HID-Gerät ansprechen.
CACHE="/tmp/corsairctl-waybar.json"
LOCK="/tmp/corsairctl-waybar.lock"
RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
CACHE="$RUNTIME_DIR/corsairctl-waybar.json"
LOCK="$RUNTIME_DIR/corsairctl-waybar.lock"
MAX_AGE=300
FALLBACK='{"text":"","tooltip":"","class":"offline","percentage":0}'
+8 -8
View File
@@ -138,15 +138,15 @@ pub fn parse_response_validated(
}
// 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_endpoint
&& endpoint != exp
{
return Err(CorsairError::UnexpectedResponse { endpoint, command });
}
if let Some(exp) = expected_command {
if command != exp {
return Err(CorsairError::UnexpectedResponse { endpoint, command });
}
if let Some(exp) = expected_command
&& command != exp
{
return Err(CorsairError::UnexpectedResponse { endpoint, command });
}
let data = if raw.len() > 4 {
+7 -7
View File
@@ -7,17 +7,19 @@ use crate::error::Result;
const SIDETONE_CONTROL: &str = "Sidetone";
/// Findet die ALSA-Card mit einem Sidetone-Control.
/// Findet die ALSA-Card mit einem Sidetone-Control und gibt den offenen Mixer zurück.
///
/// Sucht nach der Karte, die ein "Sidetone" Control hat,
/// statt sich auf den Kartennamen zu verlassen (der kann abgeschnitten sein).
fn find_card() -> Result<String> {
/// Gibt den bereits geöffneten Mixer zurück, damit der Aufrufer ihn nicht
/// ein zweites Mal öffnen muss.
fn find_card() -> Result<Mixer> {
for card in alsa::card::Iter::new().flatten() {
let name = format!("hw:{}", card.get_index());
if let Ok(mixer) = Mixer::new(&name, false) {
let selem_id = SelemId::new(SIDETONE_CONTROL, 0);
if mixer.find_selem(&selem_id).is_some() {
return Ok(name);
return Ok(mixer);
}
}
}
@@ -26,8 +28,7 @@ fn find_card() -> Result<String> {
/// Liest den aktuellen Sidetone-Level (0-23).
pub fn get_level() -> Result<i64> {
let card = find_card()?;
let mixer = Mixer::new(&card, false)?;
let mixer = find_card()?;
let selem_id = SelemId::new(SIDETONE_CONTROL, 0);
let selem = mixer
@@ -45,8 +46,7 @@ pub fn get_level() -> Result<i64> {
/// Setzt den Sidetone-Level (0-23).
pub fn set_level(level: i64) -> Result<()> {
let card = find_card()?;
let mixer = Mixer::new(&card, false)?;
let mixer = find_card()?;
// Volume setzen
let volume_id = SelemId::new(SIDETONE_CONTROL, 0);