fix: validate Quick Connect and ~/.ssh/config imports; unknown enum values no longer reset a profile's config

- Quick Connect runs the same validation as saved profiles, so an SSH
  connection without a username fails up front with a clear message (F5).
- ~/.ssh/config imports fill a missing User (and jump-host user) with the
  local user name, as ssh does, and skip blocks that still don't validate,
  reporting how many (F6).
- An unrecognised hostKeyPolicy/lineEnding/parity/flowControl/direction in
  a stored config falls back to that field's default instead of failing
  the whole JSON column and silently resetting every other setting; parse
  failures that remain are logged (F7).
This commit is contained in:
Andy Dixon
2026-10-12 09:19:03 +01:00
parent 3b490fa6c3
commit bd1dc54e20
7 changed files with 90 additions and 14 deletions
+28
View File
@@ -37,6 +37,12 @@ fn nd<'de, D: Deserializer<'de>, T: Default + Deserialize<'de>>(d: D) -> Result<
Ok(Option::<T>::deserialize(d)?.unwrap_or_default())
}
/// For enum fields: an unknown value (e.g. written by a newer build) falls back to the default
/// for that field instead of failing the whole config, which would reset every other setting.
fn lenient<'de, D: Deserializer<'de>, T: Default + serde::de::DeserializeOwned>(d: D) -> Result<T, D::Error> {
Ok(Option::<serde_json::Value>::deserialize(d)?.and_then(|v| T::deserialize(v).ok()).unwrap_or_default())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Protocol {
@@ -244,6 +250,7 @@ pub struct SshConfig {
pub secret_mode: SecretMode,
pub private_key_path: String,
pub key_secret_mode: SecretMode,
#[serde(deserialize_with = "lenient")]
pub host_key_policy: HostKeyPolicy,
pub terminal_type: String,
pub keepalive_interval: i64,
@@ -303,6 +310,7 @@ pub enum ForwardDirection {
#[serde(rename_all = "camelCase", default)]
pub struct PortForward {
pub id: String,
#[serde(deserialize_with = "lenient")]
pub direction: ForwardDirection,
pub bind_host: String,
pub bind_port: i64,
@@ -316,6 +324,7 @@ pub struct PortForward {
#[serde(rename_all = "camelCase", default)]
pub struct TelnetConfig {
pub local_echo: bool,
#[serde(deserialize_with = "lenient")]
pub line_ending: LineEnding,
pub terminal_type: String,
pub encoding: String,
@@ -331,6 +340,7 @@ pub struct TelnetConfig {
#[serde(rename_all = "camelCase", default)]
pub struct RawConfig {
pub auto_reconnect: bool,
#[serde(deserialize_with = "lenient")]
pub line_ending: LineEnding,
pub encoding: String,
pub show_timestamps: bool,
@@ -431,9 +441,12 @@ pub enum FlowControl {
pub struct SerialConfig {
pub baud_rate: u32,
pub data_bits: u8,
#[serde(deserialize_with = "lenient")]
pub parity: Parity,
pub stop_bits: u8,
#[serde(deserialize_with = "lenient")]
pub flow_control: FlowControl,
#[serde(deserialize_with = "lenient")]
pub line_ending: LineEnding,
pub local_echo: bool,
pub dtr_on_connect: bool,
@@ -919,3 +932,18 @@ impl IrcConfig {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unknown_enum_values_fall_back_per_field() {
let c: SshConfig = serde_json::from_str(r#"{"hostKeyPolicy":"from-the-future","keepaliveInterval":42,"agentForwarding":true}"#).unwrap();
assert_eq!((c.host_key_policy, c.keepalive_interval, c.agent_forwarding), (HostKeyPolicy::Ask, 42, true));
let c: SshConfig = serde_json::from_str(r#"{"hostKeyPolicy":"strict"}"#).unwrap();
assert_eq!(c.host_key_policy, HostKeyPolicy::Strict);
let t: TelnetConfig = serde_json::from_str(r#"{"lineEnding":"weird","localEcho":true}"#).unwrap();
assert_eq!((t.line_ending, t.local_echo), (LineEnding::default(), true));
}
}