fix(config): reject ambiguous router settings and declare Rust 1.88
This commit is contained in:
6 files changed
+132
-37
No files matched your search
+2
-1
@@ -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"]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
+112
-32
@@ -114,30 +114,12 @@ impl UciSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mapped_config_key(
|
fn mapped_config_key(section: &UciSectionMapping, uci_option: &str) -> Option<String> {
|
||||||
&self,
|
section
|
||||||
selector: &SectionSelector<'_>,
|
.options
|
||||||
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()
|
.iter()
|
||||||
.filter(|section| section.uci_section == section_type)
|
.find(|option| option.uci_option == uci_option)
|
||||||
.find_map(|section| {
|
.map(|option| format!("{}.{}", section.config_section, option.config_field))
|
||||||
section
|
|
||||||
.options
|
|
||||||
.iter()
|
|
||||||
.find(|option| option.uci_option == uci_option)
|
|
||||||
.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
|
||||||
SourceError::Message(format!(
|
.sections()
|
||||||
"failed to retrieve sections from UCI package \
|
.map_err(|e| {
|
||||||
|
SourceError::Message(format!(
|
||||||
|
"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
@@ -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}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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!(
|
||||||
|
|||||||
Reference in new issue
Block a user