feat: publish-ready overhaul — lints, tests, docs, CI, fuzzing, mutant testing
- Enable clippy pedantic + nursery lints; fix all warnings - Add config validation (SF, CR, frequency range checks) - Zeroize passphrase intermediates after key derivation - Convert to lib+bin crate for fuzz target accessibility - Replace Vec with VecDeque for O(1) log/bucket rotation - Add TUI minimum terminal size guard Testing: - 145 unit + property tests (was 15) - 6 proptest groups (frame roundtrip, hash, SNR, air time, peer list, padding) - 2 cargo-fuzz targets (gossip frame decode, peer list decode) - cargo-mutants: 0 missed, 241 caught, 54 unviable Documentation: - README with prerequisites, quick start, config reference, architecture - Module-level and item-level rustdoc on all public items - LICENSE (MIT), rustfmt.toml, .gitignore Infrastructure: - GitHub Actions CI (fmt, clippy, test, audit) - cargo-mutants config (.cargo/mutants.toml) - justfile for common tasks
This commit is contained in:
24 files changed
+3048
-563
No files matched your search
+41
-24
@@ -17,10 +17,8 @@ const RESET: &str = "\x1b[0m";
|
||||
|
||||
/// Run the interactive setup wizard. If `existing` is Some, use those values as defaults.
|
||||
pub fn run_wizard(config_path: &Path, existing: Option<&Config>) -> Result<()> {
|
||||
let defaults = existing.map_or_else(
|
||||
|| (RadioSection::default(), BridgeSection::default()),
|
||||
|c| (c.radio.clone(), c.bridge.clone()),
|
||||
);
|
||||
let defaults = existing
|
||||
.map_or_else(|| (RadioSection::default(), BridgeSection::default()), |c| (c.radio.clone(), c.bridge.clone()));
|
||||
let (radio, bridge) = defaults;
|
||||
|
||||
println!();
|
||||
@@ -43,8 +41,9 @@ pub fn run_wizard(config_path: &Path, existing: Option<&Config>) -> Result<()> {
|
||||
|
||||
// ── Radio ────────────────────────────────────────────────────
|
||||
|
||||
let freq_default = format!("{:.3}", radio.frequency as f64 / 1_000_000.0);
|
||||
let freq_default = format!("{:.3}", f64::from(radio.frequency) / 1_000_000.0);
|
||||
let freq_mhz: f64 = prompt_parse("Frequency (MHz)", &freq_default)?;
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] // validated by prompt_parse
|
||||
let frequency = (freq_mhz * 1_000_000.0) as u32;
|
||||
|
||||
let bandwidth = prompt_default("Bandwidth", &radio.bandwidth)?;
|
||||
@@ -70,11 +69,7 @@ pub fn run_wizard(config_path: &Path, existing: Option<&Config>) -> Result<()> {
|
||||
|
||||
let port_default = radio.port.as_deref().unwrap_or("auto");
|
||||
let port_str = prompt_default("Serial Port", port_default)?;
|
||||
let port = if port_str == "auto" || port_str.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(port_str)
|
||||
};
|
||||
let port = if port_str == "auto" || port_str.is_empty() { None } else { Some(port_str) };
|
||||
|
||||
// ── Build and save ───────────────────────────────────────────
|
||||
|
||||
@@ -111,12 +106,10 @@ pub fn run_wizard(config_path: &Path, existing: Option<&Config>) -> Result<()> {
|
||||
|
||||
fn write_config(path: &Path, cfg: &Config) -> Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("creating directory: {}", parent.display()))?;
|
||||
std::fs::create_dir_all(parent).with_context(|| format!("creating directory: {}", parent.display()))?;
|
||||
}
|
||||
let content = toml::to_string_pretty(cfg).context("serializing config")?;
|
||||
std::fs::write(path, content)
|
||||
.with_context(|| format!("writing config: {}", path.display()))?;
|
||||
std::fs::write(path, content).with_context(|| format!("writing config: {}", path.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -128,20 +121,12 @@ fn prompt_default(label: &str, default: &str) -> Result<String> {
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(default.to_string())
|
||||
} else {
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
if trimmed.is_empty() { Ok(default.to_string()) } else { Ok(trimmed.to_string()) }
|
||||
}
|
||||
|
||||
fn prompt_required(label: &str, default: Option<&str>) -> Result<String> {
|
||||
loop {
|
||||
let prompt = if let Some(d) = default {
|
||||
format!(" {label} {DIM}[{d}]{RESET}: ")
|
||||
} else {
|
||||
format!(" {label}: ")
|
||||
};
|
||||
let prompt = default.map_or_else(|| format!(" {label}: "), |d| format!(" {label} {DIM}[{d}]{RESET}: "));
|
||||
print!("{prompt}");
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
@@ -175,3 +160,35 @@ fn parse_sync_word(s: &str) -> Result<u16> {
|
||||
let s = s.strip_prefix("0x").unwrap_or(&s);
|
||||
u16::from_str_radix(s, 16).with_context(|| format!("invalid sync word: {s}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_sync_word_hex_prefix() {
|
||||
assert_eq!(parse_sync_word("0x3444").unwrap(), 0x3444);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_sync_word_no_prefix() {
|
||||
assert_eq!(parse_sync_word("3444").unwrap(), 0x3444);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_sync_word_case_insensitive() {
|
||||
assert_eq!(parse_sync_word("0xABCD").unwrap(), 0xABCD);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_sync_word_whitespace() {
|
||||
assert_eq!(parse_sync_word(" 0x3444 ").unwrap(), 0x3444);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_sync_word_invalid() {
|
||||
assert!(parse_sync_word("ZZZZ").is_err());
|
||||
assert!(parse_sync_word("").is_err());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user