fix(pair): clean up imports, Frame API warnings, and add clippy clean formatting

This commit is contained in:
Ochenstarik 2026-08-24 11:09:11 +07:00
parent 2929a35261
commit 5c905a5c26
7 changed files with 110 additions and 41 deletions

View file

@ -51,9 +51,10 @@ impl HermesPairApp {
let mut selected_iface_index = 0;
if let Some(ref target) = explicit_interface {
let lower = target.to_lowercase();
if let Some(idx) = interfaces.iter().position(|i| {
i.name.to_lowercase().contains(&lower) || i.ip.to_string() == *target
}) {
if let Some(idx) = interfaces
.iter()
.position(|i| i.name.to_lowercase().contains(&lower) || i.ip.to_string() == *target)
{
selected_iface_index = idx;
}
}

View file

@ -79,9 +79,10 @@ pub fn resolve_selected_ip(
}
let lower = target.to_lowercase();
if let Some(matched) = interfaces.iter().find(|i| {
i.name.to_lowercase().contains(&lower) || i.ip.to_string() == target
}) {
if let Some(matched) = interfaces
.iter()
.find(|i| i.name.to_lowercase().contains(&lower) || i.ip.to_string() == target)
{
return (matched.name.clone(), matched.ip);
}
}
@ -134,8 +135,13 @@ pub async fn run_once(
println!("Hermes: Running (v{}, Auth: {})", ver, auth);
}
ProbeState::LoopbackOnly { lan_error, .. } => {
println!("Hermes: Running locally (127.0.0.1), but LAN unreachable: {}", lan_error);
println!("⚠️ Warning: Hermes is bound to loopback only. Start Hermes with --host 0.0.0.0");
println!(
"Hermes: Running locally (127.0.0.1), but LAN unreachable: {}",
lan_error
);
println!(
"⚠️ Warning: Hermes is bound to loopback only. Start Hermes with --host 0.0.0.0"
);
}
ProbeState::Offline(err) => {
println!("Hermes: Offline ({})", err);
@ -221,8 +227,14 @@ pub async fn run_terminal_loop(
println!("Hermes: Running (v{}, Auth: {})", ver, auth);
}
ProbeState::LoopbackOnly { lan_error, .. } => {
println!("Hermes: Loopback Only (127.0.0.1) [LAN Error: {}]", lan_error);
println!("⚠️ Run Hermes with: hermes serve --host 0.0.0.0 --port {}", port);
println!(
"Hermes: Loopback Only (127.0.0.1) [LAN Error: {}]",
lan_error
);
println!(
"⚠️ Run Hermes with: hermes serve --host 0.0.0.0 --port {}",
port
);
}
ProbeState::Offline(err) => {
println!("Hermes: Offline ({})", err);
@ -237,7 +249,10 @@ pub async fn run_terminal_loop(
};
println!("Host: {}", current_payload.name);
println!("Address: http://{}:{}", current_payload.host, current_payload.port);
println!(
"Address: http://{}:{}",
current_payload.host, current_payload.port
);
println!("Host ID: {}", short_id);
println!("Expires in: {:02}:{:02}", remaining / 60, remaining % 60);
println!("\n{}", qr_rendered);

View file

@ -26,7 +26,9 @@ pub fn get_config_path() -> PathBuf {
#[cfg(target_os = "windows")]
{
if let Ok(appdata) = std::env::var("APPDATA") {
return PathBuf::from(appdata).join("HermesPair").join("config.json");
return PathBuf::from(appdata)
.join("HermesPair")
.join("config.json");
}
if let Some(config_dir) = dirs::config_dir() {
return config_dir.join("HermesPair").join("config.json");
@ -43,7 +45,10 @@ pub fn get_config_path() -> PathBuf {
return config_dir.join("hermes-pair").join("config.json");
}
if let Some(home_dir) = dirs::home_dir() {
return home_dir.join(".config").join("hermes-pair").join("config.json");
return home_dir
.join(".config")
.join("hermes-pair")
.join("config.json");
}
PathBuf::from("config.json")
}
@ -60,7 +65,9 @@ pub fn save_config_to_path(config: &AppConfig, path: &Path) -> Result<(), std::i
let tmp_file_name = format!(
"{}.tmp.{}",
path.file_name().and_then(|n| n.to_str()).unwrap_or("config"),
path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("config"),
Uuid::new_v4()
);
let tmp_path = match path.parent() {
@ -76,7 +83,7 @@ pub fn save_config_to_path(config: &AppConfig, path: &Path) -> Result<(), std::i
let _ = fs::remove_file(path);
if let Err(fallback_err) = fs::rename(&tmp_path, path) {
let _ = fs::remove_file(&tmp_path);
return Err(fallback_err.into());
return Err(fallback_err);
}
}

View file

@ -25,18 +25,32 @@ pub enum PairingError {
impl fmt::Display for PairingError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PairingError::InvalidUriScheme(s) => write!(f, "Invalid URI scheme '{}', expected 'hermes'", s),
PairingError::InvalidUriScheme(s) => {
write!(f, "Invalid URI scheme '{}', expected 'hermes'", s)
}
PairingError::InvalidUriFormat(s) => write!(f, "Invalid pairing URI format: {}", s),
PairingError::MissingDataParameter => write!(f, "Missing 'data' query parameter in pairing URI"),
PairingError::Base64DecodeError(e) => write!(f, "Failed to decode Base64URL payload: {}", e),
PairingError::MissingDataParameter => {
write!(f, "Missing 'data' query parameter in pairing URI")
}
PairingError::Base64DecodeError(e) => {
write!(f, "Failed to decode Base64URL payload: {}", e)
}
PairingError::JsonDecodeError(e) => write!(f, "Failed to parse JSON payload: {}", e),
PairingError::UnsupportedVersion(v) => write!(f, "Unsupported payload version {}, expected 1", v),
PairingError::InvalidPayloadType(t) => write!(f, "Invalid payload type '{}', expected 'hermes-pair'", t),
PairingError::UnsupportedVersion(v) => {
write!(f, "Unsupported payload version {}, expected 1", v)
}
PairingError::InvalidPayloadType(t) => {
write!(f, "Invalid payload type '{}', expected 'hermes-pair'", t)
}
PairingError::InvalidHostId(id) => write!(f, "Invalid host UUID: '{}'", id),
PairingError::EmptyHost => write!(f, "Host address cannot be empty"),
PairingError::InvalidPort(p) => write!(f, "Invalid port number: {}", p),
PairingError::PayloadExpired { expires_at, now } => {
write!(f, "Pairing payload expired at timestamp {} (current time: {})", expires_at, now)
write!(
f,
"Pairing payload expired at timestamp {} (current time: {})",
expires_at, now
)
}
}
}
@ -83,12 +97,16 @@ pub fn create_pairing_payload(
}
pub fn encode_pairing_uri(payload: &PairingPayloadV1) -> String {
let json = serde_json::to_string(payload).expect("Serialization of PairingPayloadV1 should never fail");
let json = serde_json::to_string(payload)
.expect("Serialization of PairingPayloadV1 should never fail");
let encoded = URL_SAFE_NO_PAD.encode(json.as_bytes());
format!("hermes://pair?data={}", encoded)
}
pub fn decode_pairing_uri_at_time(uri: &str, current_time: u64) -> Result<PairingPayloadV1, PairingError> {
pub fn decode_pairing_uri_at_time(
uri: &str,
current_time: u64,
) -> Result<PairingPayloadV1, PairingError> {
let data_str = if let Ok(url) = Url::parse(uri) {
if url.scheme() != "hermes" {
return Err(PairingError::InvalidUriScheme(url.scheme().to_string()));
@ -107,7 +125,10 @@ pub fn decode_pairing_uri_at_time(uri: &str, current_time: u64) -> Result<Pairin
if !uri.starts_with("hermes://") && !uri.starts_with("hermes:") {
return Err(PairingError::InvalidUriScheme("unknown".to_string()));
}
let query_part = uri.split_once('?').map(|x| x.1).ok_or(PairingError::MissingDataParameter)?;
let query_part = uri
.split_once('?')
.map(|x| x.1)
.ok_or(PairingError::MissingDataParameter)?;
let mut found = None;
for pair in query_part.split('&') {
if let Some((k, v)) = pair.split_once('=') {

View file

@ -37,7 +37,11 @@ pub fn render_terminal_qr(data: &str) -> Result<String, QrError> {
// Dark modules: "██", Light modules: " "
for y in 0..total_size {
for x in 0..total_size {
let is_dark = if x >= quiet_zone && x < quiet_zone + width && y >= quiet_zone && y < quiet_zone + width {
let is_dark = if x >= quiet_zone
&& x < quiet_zone + width
&& y >= quiet_zone
&& y < quiet_zone + width
{
let qx = x - quiet_zone;
let qy = y - quiet_zone;
colors[qy * width + qx] == Color::Dark
@ -74,7 +78,11 @@ pub fn render_egui_image(data: &str, scale: usize) -> Result<ColorImage, QrError
for my in 0..total_modules {
for _sy in 0..scale {
for mx in 0..total_modules {
let is_dark = if mx >= quiet_zone && mx < quiet_zone + width && my >= quiet_zone && my < quiet_zone + width {
let is_dark = if mx >= quiet_zone
&& mx < quiet_zone + width
&& my >= quiet_zone
&& my < quiet_zone + width
{
let qx = mx - quiet_zone;
let qy = my - quiet_zone;
colors[qy * width + qx] == Color::Dark

View file

@ -28,7 +28,8 @@ fn test_config_persistence() {
assert!(Uuid::parse_str(&config1.host_id).is_ok());
// Second load -> loads existing config and keeps same host_id
let config2 = load_or_create_config_from_path(&config_path).expect("Should load existing config");
let config2 =
load_or_create_config_from_path(&config_path).expect("Should load existing config");
assert_eq!(config1.host_id, config2.host_id);
// Clean up
@ -51,13 +52,17 @@ fn test_pairing_payload_serde() {
};
let json = serde_json::to_string(&payload).expect("Serialization failed");
let deserialized: PairingPayloadV1 = serde_json::from_str(&json).expect("Deserialization failed");
let deserialized: PairingPayloadV1 =
serde_json::from_str(&json).expect("Deserialization failed");
assert_eq!(payload, deserialized);
// Verify Base64URL round-trip
let b64 = URL_SAFE_NO_PAD.encode(json.as_bytes());
let decoded_bytes = URL_SAFE_NO_PAD.decode(b64.as_bytes()).expect("B64 decode failed");
let from_b64: PairingPayloadV1 = serde_json::from_slice(&decoded_bytes).expect("JSON from b64 failed");
let decoded_bytes = URL_SAFE_NO_PAD
.decode(b64.as_bytes())
.expect("B64 decode failed");
let from_b64: PairingPayloadV1 =
serde_json::from_slice(&decoded_bytes).expect("JSON from b64 failed");
assert_eq!(payload, from_b64);
}
@ -203,8 +208,12 @@ fn test_network_interface_filtering() {
let sorted = filter_and_sort_interfaces(test_interfaces);
// Loopback and link-local must be eliminated
assert!(!sorted.iter().any(|i| i.is_loopback || i.ip == Ipv4Addr::new(127, 0, 0, 1)));
assert!(!sorted.iter().any(|i| i.ip == Ipv4Addr::new(169, 254, 10, 20)));
assert!(!sorted
.iter()
.any(|i| i.is_loopback || i.ip == Ipv4Addr::new(127, 0, 0, 1)));
assert!(!sorted
.iter()
.any(|i| i.ip == Ipv4Addr::new(169, 254, 10, 20)));
// Order: Physical LAN (eth0 192.168.1.10) -> Tailscale (100.80.5.6) -> Virtual LAN (docker0 172.17.0.1)
assert_eq!(sorted.len(), 3);
@ -215,7 +224,9 @@ fn test_network_interface_filtering() {
#[tokio::test]
async fn test_mock_hermes_probe() {
let listener = TcpListener::bind("127.0.0.1:0").await.expect("Failed to bind mock listener");
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("Failed to bind mock listener");
let port = listener.local_addr().unwrap().port();
let server_task = tokio::spawn(async move {
@ -245,11 +256,17 @@ async fn test_mock_hermes_probe() {
let base_url = format!("http://127.0.0.1:{}", port);
let status_res = client.fetch_status(&base_url).await;
assert!(status_res.is_ok(), "Probe should succeed against mock server");
assert!(
status_res.is_ok(),
"Probe should succeed against mock server"
);
let status = status_res.unwrap();
assert_eq!(status.status, "running");
assert!(status.auth_required);
assert_eq!(status.auth_providers, vec!["bearer".to_string(), "oauth2".to_string()]);
assert_eq!(
status.auth_providers,
vec!["bearer".to_string(), "oauth2".to_string()]
);
assert_eq!(status.auth_flows, vec!["token".to_string()]);
assert_eq!(status.version, Some("1.2.0".to_string()));