fix(config): reject ambiguous router settings and declare Rust 1.88
Build / build (pull_request) Successful in 55s
DevSkim / DevSkim (pull_request) Failing after 1m22s
rust-clippy analyze / Run rust-clippy analyzing (pull_request) Failing after 2m2s

This commit is contained in:
Nils Andreas Svee committed 2026-09-24 16:41:35 +07:00
1 parent 8fb051d48f
commit e5c63dc250
6 files changed
+126 -31

No files matched your search

+2 -1
View File
@@ -2,6 +2,7 @@
name = "sqm-autorate-rust" name = "sqm-autorate-rust"
version = "0.4.1" version = "0.4.1"
edition = "2024" edition = "2024"
rust-version = "1.88"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -40,7 +41,7 @@ netlink-bindings = { version = "0.3.3", features = ["rt-link", "tc"] }
netlink-socket2 = { version = "0.3.3" } netlink-socket2 = { version = "0.3.3" }
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
rust-uci = { git = "https://github.com/Lochnair/rust-uci.git", rev = "fcfedd204fb76928ef617bf9631b041c816e03d4", optional = true, default-features = false, } rust-uci = { git = "https://github.com/Lochnair/rust-uci.git", rev = "fcfedd204fb76928ef617bf9631b041c816e03d4", optional = true, default-features = false }
[features] [features]
default = ["toml", "uci-vendored"] default = ["toml", "uci-vendored"]
+1 -1
View File
@@ -16,7 +16,7 @@ You can install **sqm‑autorate‑rust** in two ways:
### 1. Native Rust build ### 1. Native Rust build
1. Ensure you have the Rust toolchain (1.70+), Cargo, and `libnl` headers installed on your Linux host. 1. Ensure you have Rust 1.88 or newer, Cargo, and a C toolchain for libuci on your Linux host.
2. Clone and build: 2. Clone and build:
```bash ```bash
+1
View File
@@ -2,6 +2,7 @@
name = "uci-derive" name = "uci-derive"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
rust-version = "1.88"
publish = false publish = false
[lib] [lib]
+106 -26
View File
@@ -114,30 +114,12 @@ impl UciSource {
} }
} }
fn mapped_config_key( fn mapped_config_key(section: &UciSectionMapping, uci_option: &str) -> Option<String> {
&self,
selector: &SectionSelector<'_>,
uci_option: &str,
) -> Option<String> {
let section_type = match selector {
SectionSelector::Anonymous {
section_type,
index,
} if *index == 0 => *section_type,
SectionSelector::Anonymous { .. } | SectionSelector::Named(_) => return None,
};
self.schema
.iter()
.filter(|section| section.uci_section == section_type)
.find_map(|section| {
section section
.options .options
.iter() .iter()
.find(|option| option.uci_option == uci_option) .find(|option| option.uci_option == uci_option)
.map(|option| format!("{}.{}", section.config_section, option.config_field,)) .map(|option| format!("{}.{}", section.config_section, option.config_field))
})
} }
fn collect_package( fn collect_package(
@@ -146,14 +128,35 @@ impl UciSource {
package: &UciPackage, package: &UciPackage,
values: &mut Map<String, Value>, values: &mut Map<String, Value>,
) -> Result<(), SourceError> { ) -> Result<(), SourceError> {
let sections = package.sections().map_err(|e| { let sections: Vec<_> = package
.sections()
.map_err(|e| {
SourceError::Message(format!( SourceError::Message(format!(
"failed to retrieve sections from UCI package \ "failed to retrieve sections from UCI package \
{package_name:?}: {e}" {package_name:?}: {e}"
)) ))
})?; })?
.collect();
for mapping in self.schema {
if sections
.iter()
.filter(|section| section.type_() == mapping.uci_section)
.count()
> 1
{
return Err(SourceError::Message(format!(
"UCI package {package_name:?} declares multiple `{}` sections; exactly one is expected",
mapping.uci_section
)));
}
}
for section in sections { for section in sections {
let mapping = self
.schema
.iter()
.find(|entry| entry.uci_section == section.type_());
let selector = section.selector().ok_or_else(|| { let selector = section.selector().ok_or_else(|| {
SourceError::Message(format!( SourceError::Message(format!(
"loaded UCI section of type {:?} in package \ "loaded UCI section of type {:?} in package \
@@ -181,9 +184,14 @@ impl UciSource {
let source_key = format!("{package_name}.{config_relative_key}"); let source_key = format!("{package_name}.{config_relative_key}");
let destination_key = self let destination_key = mapping
.mapped_config_key(&selector, option_name) .and_then(|entry| Self::mapped_config_key(entry, option_name))
.unwrap_or(source_key); .unwrap_or_else(|| {
if mapping.is_some() {
eprintln!("warning: {uci_key}: unknown option; it will be ignored");
}
source_key
});
let Some(value) = option.get().map_err(|e| { let Some(value) = option.get().map_err(|e| {
SourceError::Message(format!( SourceError::Message(format!(
@@ -262,7 +270,7 @@ impl Source for UciSource {
let (_uci, packages) = match resolved { let (_uci, packages) = match resolved {
Ok(resolved) => resolved, Ok(resolved) => resolved,
Err(_) if !self.required => { Err(SourceError::NotFound(_)) if !self.required => {
return Ok(Map::new()); return Ok(Map::new());
} }
@@ -285,3 +293,75 @@ impl Source for UciSource {
Ok(values) Ok(values)
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::settings::Settings;
use std::fs;
use std::sync::atomic::{AtomicUsize, Ordering};
static NEXT: AtomicUsize = AtomicUsize::new(0);
struct Fixture(PathBuf);
impl Fixture {
fn new(contents: &str) -> Self {
let root = std::env::temp_dir().join(format!(
"sqma-uci-{}-{}",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
));
fs::create_dir_all(root.join("config")).unwrap();
fs::create_dir_all(root.join("save")).unwrap();
fs::write(root.join("config/sqm-autorate-rust"), contents).unwrap();
Self(root)
}
fn collect(&self) -> Result<Map<String, Value>, SourceError> {
Source::collect(
&UciSource::from_schema::<Settings>()
.with_directories(self.0.join("config"), self.0.join("save"))
.required(false),
)
}
}
impl Drop for Fixture {
fn drop(&mut self) {
fs::remove_dir_all(&self.0).unwrap();
}
}
#[test]
fn malformed_uci_is_not_treated_as_absent() {
let fixture = Fixture::new("config network 'unterminated\n");
assert!(fixture.collect().is_err());
}
#[test]
fn named_sections_are_mapped_and_mixed_sections_are_rejected() {
let named = Fixture::new("config network 'primary'\n option download_interface 'ifb0'\n");
let values = named.collect().unwrap();
assert_eq!(
values
.get("network.download_interface")
.unwrap()
.clone()
.into_string()
.unwrap(),
"ifb0"
);
let mixed = Fixture::new(
"config network 'primary'\n option download_interface 'ifb0'\nconfig network\n option upload_interface 'eth0'\n",
);
assert!(
mixed
.collect()
.unwrap_err()
.to_string()
.contains("multiple `network` sections")
);
}
}
+4 -1
View File
@@ -192,7 +192,10 @@ impl Settings {
})?; })?;
match (record.ip_version, record.reflector_ip) { match (record.ip_version, record.reflector_ip) {
(4, IpAddr::V4(_)) | (6, IpAddr::V6(_)) => {} (4, IpAddr::V4(_)) => {}
(6, address @ IpAddr::V6(_)) => {
bail!("reflector {address} is IPv6; only IPv4 reflectors are supported");
}
(version, address) => { (version, address) => {
bail!("reflector {address} has inconsistent IP version {version}"); bail!("reflector {address} has inconsistent IP version {version}");
} }
+12 -2
View File
@@ -153,7 +153,12 @@ impl NetworkSettings {
if download_base_valid && download_percent_valid { if download_base_valid && download_percent_valid {
let minimum = self.download_min_kbits(); let minimum = self.download_min_kbits();
if minimum < WARN_DOWNLOAD_MIN_KBITS { if minimum < 1.0 {
report.error(
"network.download_min_percent",
"minimum download rate floors to zero",
);
} else if minimum < WARN_DOWNLOAD_MIN_KBITS {
report.warning( report.warning(
"network.download_min_percent", "network.download_min_percent",
format!( format!(
@@ -168,7 +173,12 @@ impl NetworkSettings {
if upload_base_valid && upload_percent_valid { if upload_base_valid && upload_percent_valid {
let minimum = self.upload_min_kbits(); let minimum = self.upload_min_kbits();
if minimum < WARN_UPLOAD_MIN_KBITS { if minimum < 1.0 {
report.error(
"network.upload_min_percent",
"minimum upload rate floors to zero",
);
} else if minimum < WARN_UPLOAD_MIN_KBITS {
report.warning( report.warning(
"network.upload_min_percent", "network.upload_min_percent",
format!( format!(