rust/chg/src/message.rs
changeset 39971 b1d8acd82d60
child 43818 ce088b38f92b
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rust/chg/src/message.rs	Mon Sep 24 16:33:24 2018 +0900
@@ -0,0 +1,117 @@
+// Copyright 2018 Yuya Nishihara <yuya@tcha.org>
+//
+// 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<CommandType> {
+    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: S) -> String
+    where S: AsRef<[u8]>,
+{
+    s.as_ref().iter().map(|&c| c as char).collect()
+}
+
+fn new_parse_error<E>(error: E) -> io::Error
+    where E: Into<Box<error::Error + Send + Sync>>,
+{
+    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())
+    }
+}