fix(pair): clean up imports, Frame API warnings, and add clippy clean formatting
This commit is contained in:
parent
2929a35261
commit
5c905a5c26
7 changed files with 110 additions and 41 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
31
src/cli.rs
31
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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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('=') {
|
||||
|
|
|
|||
12
src/qr.rs
12
src/qr.rs
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue