diff --git a/rust/chg/src/lib.rs b/rust/chg/src/lib.rs --- a/rust/chg/src/lib.rs +++ b/rust/chg/src/lib.rs @@ -3,6 +3,9 @@ // This software may be used and distributed according to the terms of the // GNU General Public License version 2 or any later version. +extern crate bytes; extern crate libc; +extern crate tokio_hglib; +pub mod message; pub mod procutil; diff --git a/rust/chg/src/message.rs b/rust/chg/src/message.rs new file mode 100644 --- /dev/null +++ b/rust/chg/src/message.rs @@ -0,0 +1,117 @@ +// Copyright 2018 Yuya Nishihara +// +// This software may be used and distributed according to the terms of the +// GNU General Public License version 2 or any later version. + +//! Utility for parsing and building command-server messages. + +use bytes::Bytes; +use std::error; +use std::ffi::{OsStr, OsString}; +use std::io; +use std::os::unix::ffi::OsStrExt; + +pub use tokio_hglib::message::*; // re-exports + +/// Shell command type requested by the server. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CommandType { + /// Pager should be spawned. + Pager, + /// Shell command should be executed to send back the result code. + System, +} + +/// Shell command requested by the server. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CommandSpec { + pub command: OsString, + pub current_dir: OsString, + pub envs: Vec<(OsString, OsString)>, +} + +/// Parses "S" channel request into command type and spec. +pub fn parse_command_spec(data: Bytes) -> io::Result<(CommandType, CommandSpec)> { + let mut split = data.split(|&c| c == b'\0'); + let ctype = parse_command_type(split.next().ok_or(new_parse_error("missing type"))?)?; + let command = split.next().ok_or(new_parse_error("missing command"))?; + let current_dir = split.next().ok_or(new_parse_error("missing current dir"))?; + + let mut envs = Vec::new(); + for l in split { + let mut s = l.splitn(2, |&c| c == b'='); + let k = s.next().unwrap(); + let v = s.next().ok_or(new_parse_error("malformed env"))?; + envs.push((OsStr::from_bytes(k).to_owned(), OsStr::from_bytes(v).to_owned())); + } + + let spec = CommandSpec { + command: OsStr::from_bytes(command).to_owned(), + current_dir: OsStr::from_bytes(current_dir).to_owned(), + envs: envs, + }; + Ok((ctype, spec)) +} + +fn parse_command_type(value: &[u8]) -> io::Result { + match value { + b"pager" => Ok(CommandType::Pager), + b"system" => Ok(CommandType::System), + _ => Err(new_parse_error(format!("unknown command type: {}", decode_latin1(value)))), + } +} + +fn decode_latin1(s: S) -> String + where S: AsRef<[u8]>, +{ + s.as_ref().iter().map(|&c| c as char).collect() +} + +fn new_parse_error(error: E) -> io::Error + where E: Into>, +{ + io::Error::new(io::ErrorKind::InvalidData, error) +} + +#[cfg(test)] +mod tests { + use std::os::unix::ffi::OsStringExt; + use super::*; + + #[test] + fn parse_command_spec_good() { + let src = [b"pager".as_ref(), + b"less -FRX".as_ref(), + b"/tmp".as_ref(), + b"LANG=C".as_ref(), + b"HGPLAIN=".as_ref()].join(&0); + let spec = CommandSpec { + command: os_string_from(b"less -FRX"), + current_dir: os_string_from(b"/tmp"), + envs: vec![(os_string_from(b"LANG"), os_string_from(b"C")), + (os_string_from(b"HGPLAIN"), os_string_from(b""))], + }; + assert_eq!(parse_command_spec(Bytes::from(src)).unwrap(), (CommandType::Pager, spec)); + } + + #[test] + fn parse_command_spec_too_short() { + assert!(parse_command_spec(Bytes::from_static(b"")).is_err()); + assert!(parse_command_spec(Bytes::from_static(b"pager")).is_err()); + assert!(parse_command_spec(Bytes::from_static(b"pager\0less")).is_err()); + } + + #[test] + fn parse_command_spec_malformed_env() { + assert!(parse_command_spec(Bytes::from_static(b"pager\0less\0/tmp\0HOME")).is_err()); + } + + #[test] + fn parse_command_spec_unknown_type() { + assert!(parse_command_spec(Bytes::from_static(b"paper\0less")).is_err()); + } + + fn os_string_from(s: &[u8]) -> OsString { + OsString::from_vec(s.to_vec()) + } +}