diff --git a/client/src/agent.rs b/client/src/agent.rs index 22bfdb3..ab45788 100644 --- a/client/src/agent.rs +++ b/client/src/agent.rs @@ -85,15 +85,17 @@ pub fn sign_request(key: &PublicKey, data: &[u8]) -> Result { frame(body) } -/// Parse an identities-answer payload into its keys. Comments are -/// consumed and dropped. Key IDs derive from the key material alone. +/// Parse an identities-answer payload into its keys, comments +/// attached. pub fn identities_answer(mut payload: Bytes) -> Result, AgentError> { let count = payload.try_get_u32()?; let mut keys = Vec::new(); for _ in 0..count { let blob = get_string(&mut payload)?; - let _comment = get_string(&mut payload)?; - keys.push(PublicKey::from_bytes(&blob)?); + let comment = get_string(&mut payload)?; + let mut key = PublicKey::from_bytes(&blob)?; + key.set_comment(String::from_utf8_lossy(&comment)); + keys.push(key); } done(payload)?; Ok(keys) @@ -180,7 +182,7 @@ mod test { done(frame).unwrap(); } - /// An identities-answer parses to its keys, dropping comments. + /// An identities-answer parses to its keys, comments attached. #[test] fn identities_answer_roundtrip() { let mut payload = BytesMut::new(); @@ -190,6 +192,7 @@ mod test { let keys = identities_answer(payload.freeze()).unwrap(); assert_eq!(keys.len(), 1); assert_eq!(keys[0].key_data(), key().key_data()); + assert_eq!(keys[0].comment(), "a comment"); } /// Truncated and oversized payloads are rejected, not misread. diff --git a/client/src/main.rs b/client/src/main.rs index c64db03..23a7d99 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -4,20 +4,50 @@ //! Command-line interface to the Oxide Support Shell. +use std::io::stdout; use std::process::ExitCode; use clap::Parser as _; +use rustix::termios::{OptionalActions, OutputModes, Termios, isatty, tcgetattr, tcsetattr}; use sush_client::cli::Cli; use sush_client::commands::ClientArgs; +/// The kernel tab-expansion bits, which rustix does not expose on +/// illumos: TABDLY from illumos sys/termios.h. +#[cfg(target_os = "illumos")] +const TABDLY: OutputModes = OutputModes::from_bits_retain(0o014000); + +#[cfg(not(target_os = "illumos"))] +const TABDLY: OutputModes = OutputModes::TABDLY; + +/// Expand tabs in the terminal, not the kernel. illumos ttys default +/// to tab3, whose column accounting is emoji-blind and misaligns our +/// tabbed output. Returns the modes to restore at exit. +fn no_expand_tabs() -> Option { + let out = stdout(); + if !isatty(&out) { + return None; + } + let old = tcgetattr(&out).ok()?; + let mut new = old.clone(); + new.output_modes &= !TABDLY; + tcsetattr(&out, OptionalActions::Drain, &new).ok()?; + Some(old) +} + #[tokio::main] async fn main() -> ExitCode { - match ClientArgs::parse().execute(&mut Cli::default()).await { + let saved = no_expand_tabs(); + let code = match ClientArgs::parse().execute(&mut Cli::default()).await { Ok(()) => ExitCode::SUCCESS, Err(error) => { eprintln!("{error}"); ExitCode::FAILURE } + }; + if let Some(old) = saved { + let _ = tcsetattr(stdout(), OptionalActions::Drain, &old); } + code }