rust/chg/src/message.rs
author Yuya Nishihara <yuya@tcha.org>
Mon, 24 Sep 2018 18:18:35 +0900
changeset 39974 a9c5fc436fd5
parent 39971 b1d8acd82d60
child 43818 ce088b38f92b
permissions -rw-r--r--
rust-chg: add callback to handle pager and shell command requests This could be inlined into the ChgRunCommand state to be introduced by the next patch, but it seemed good to separate any user interactions from the IPC code.

// 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())
    }
}