diff --git a/src/app.rs b/src/app.rs index 077555c..4158071 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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; } } diff --git a/src/cli.rs b/src/cli.rs index 9cd5c97..e4c0898 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -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); diff --git a/src/config.rs b/src/config.rs index c401d0d..50ce6a7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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); } } diff --git a/src/network.rs b/src/network.rs index 869c5a7..04e7698 100644 --- a/src/network.rs +++ b/src/network.rs @@ -36,12 +36,12 @@ fn interface_priority(info: &NetworkInterfaceInfo) -> u32 { let is_ts = is_tailscale_ip(&info.ip); match (info.is_virtual, is_priv, is_ts) { - (false, true, _) => 100, // Physical LAN (192.168.x.x, 10.x.x.x, 172.16-31.x.x) - (false, false, true) => 80, // Tailscale / Overlay - (false, false, false) => 60, // Other physical (e.g. public or custom) - (true, true, _) => 40, // Virtual LAN (e.g. WSL, Hyper-V virtual switch) - (true, false, true) => 30, // Virtual Tailscale - (true, false, false) => 20, // Other virtual + (false, true, _) => 100, // Physical LAN (192.168.x.x, 10.x.x.x, 172.16-31.x.x) + (false, false, true) => 80, // Tailscale / Overlay + (false, false, false) => 60, // Other physical (e.g. public or custom) + (true, true, _) => 40, // Virtual LAN (e.g. WSL, Hyper-V virtual switch) + (true, false, true) => 30, // Virtual Tailscale + (true, false, false) => 20, // Other virtual } } diff --git a/src/pairing.rs b/src/pairing.rs index a17c35b..bb16604 100644 --- a/src/pairing.rs +++ b/src/pairing.rs @@ -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 { +pub fn decode_pairing_uri_at_time( + uri: &str, + current_time: u64, +) -> Result { 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 Result { // 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= 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 diff --git a/tests/unit_and_contract_tests.rs b/tests/unit_and_contract_tests.rs index e57714f..fe4924b 100644 --- a/tests/unit_and_contract_tests.rs +++ b/tests/unit_and_contract_tests.rs @@ -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()));