message.rs
331 lines
| 9.9 KiB
| application/rls-services+xml
|
RustLexer
Yuya Nishihara
|
r40007 | // 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. | ||||
Yuya Nishihara
|
r45164 | use bytes::{BufMut, Bytes, BytesMut}; | ||
Yuya Nishihara
|
r40007 | use std::error; | ||
use std::ffi::{OsStr, OsString}; | ||||
use std::io; | ||||
use std::os::unix::ffi::OsStrExt; | ||||
Yuya Nishihara
|
r45170 | use std::path::PathBuf; | ||
Yuya Nishihara
|
r40007 | |||
Gregory Szorc
|
r44270 | pub use tokio_hglib::message::*; // re-exports | ||
Yuya Nishihara
|
r40007 | |||
/// 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. | ||||
Martin von Zweigbergk
|
r46195 | pub fn parse_command_spec( | ||
data: Bytes, | ||||
) -> io::Result<(CommandType, CommandSpec)> { | ||||
Yuya Nishihara
|
r40007 | let mut split = data.split(|&c| c == b'\0'); | ||
Martin von Zweigbergk
|
r46195 | let ctype = parse_command_type( | ||
split.next().ok_or(new_parse_error("missing type"))?, | ||||
)?; | ||||
Yuya Nishihara
|
r40007 | let command = split.next().ok_or(new_parse_error("missing command"))?; | ||
Martin von Zweigbergk
|
r46195 | let current_dir = | ||
split.next().ok_or(new_parse_error("missing current dir"))?; | ||||
Yuya Nishihara
|
r40007 | |||
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"))?; | ||||
Gregory Szorc
|
r44270 | envs.push(( | ||
OsStr::from_bytes(k).to_owned(), | ||||
OsStr::from_bytes(v).to_owned(), | ||||
)); | ||||
Yuya Nishihara
|
r40007 | } | ||
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), | ||||
Gregory Szorc
|
r44270 | _ => Err(new_parse_error(format!( | ||
"unknown command type: {}", | ||||
decode_latin1(value) | ||||
))), | ||||
Yuya Nishihara
|
r40007 | } | ||
} | ||||
Yuya Nishihara
|
r45170 | /// Client-side instruction requested by the server. | ||
#[derive(Clone, Debug, Eq, PartialEq)] | ||||
pub enum Instruction { | ||||
Exit(i32), | ||||
Reconnect, | ||||
Redirect(PathBuf), | ||||
Unlink(PathBuf), | ||||
} | ||||
/// Parses validation result into instructions. | ||||
pub fn parse_instructions(data: Bytes) -> io::Result<Vec<Instruction>> { | ||||
let mut instructions = Vec::new(); | ||||
for l in data.split(|&c| c == b'\0') { | ||||
if l.is_empty() { | ||||
continue; | ||||
} | ||||
let mut s = l.splitn(2, |&c| c == b' '); | ||||
let inst = match (s.next().unwrap(), s.next()) { | ||||
(b"exit", Some(arg)) => decode_latin1(arg) | ||||
.parse() | ||||
.map(Instruction::Exit) | ||||
Martin von Zweigbergk
|
r46195 | .map_err(|_| { | ||
new_parse_error(format!("invalid exit code: {:?}", arg)) | ||||
})?, | ||||
Yuya Nishihara
|
r45170 | (b"reconnect", None) => Instruction::Reconnect, | ||
(b"redirect", Some(arg)) => { | ||||
Instruction::Redirect(OsStr::from_bytes(arg).to_owned().into()) | ||||
} | ||||
Martin von Zweigbergk
|
r46195 | (b"unlink", Some(arg)) => { | ||
Instruction::Unlink(OsStr::from_bytes(arg).to_owned().into()) | ||||
} | ||||
Yuya Nishihara
|
r45170 | _ => { | ||
Martin von Zweigbergk
|
r46195 | return Err(new_parse_error(format!( | ||
"unknown command: {:?}", | ||||
l | ||||
))); | ||||
Yuya Nishihara
|
r45170 | } | ||
}; | ||||
instructions.push(inst); | ||||
} | ||||
Ok(instructions) | ||||
} | ||||
Yuya Nishihara
|
r45164 | // allocate large buffer as environment variables can be quite long | ||
const INITIAL_PACKED_ENV_VARS_CAPACITY: usize = 4096; | ||||
/// Packs environment variables of platform encoding into bytes. | ||||
/// | ||||
/// # Panics | ||||
/// | ||||
/// Panics if key or value contains `\0` character, or key contains '=' | ||||
/// character. | ||||
Yuya Nishihara
|
r45183 | pub fn pack_env_vars_os( | ||
vars: impl IntoIterator<Item = (impl AsRef<OsStr>, impl AsRef<OsStr>)>, | ||||
) -> Bytes { | ||||
Yuya Nishihara
|
r45164 | let mut vars_iter = vars.into_iter(); | ||
if let Some((k, v)) = vars_iter.next() { | ||||
Martin von Zweigbergk
|
r46195 | let mut dst = | ||
BytesMut::with_capacity(INITIAL_PACKED_ENV_VARS_CAPACITY); | ||||
Yuya Nishihara
|
r45164 | pack_env_into(&mut dst, k.as_ref(), v.as_ref()); | ||
for (k, v) in vars_iter { | ||||
dst.reserve(1); | ||||
dst.put_u8(b'\0'); | ||||
pack_env_into(&mut dst, k.as_ref(), v.as_ref()); | ||||
} | ||||
dst.freeze() | ||||
} else { | ||||
Bytes::new() | ||||
} | ||||
} | ||||
fn pack_env_into(dst: &mut BytesMut, k: &OsStr, v: &OsStr) { | ||||
assert!(!k.as_bytes().contains(&0), "key shouldn't contain NUL"); | ||||
assert!(!k.as_bytes().contains(&b'='), "key shouldn't contain '='"); | ||||
assert!(!v.as_bytes().contains(&0), "value shouldn't contain NUL"); | ||||
dst.reserve(k.as_bytes().len() + 1 + v.as_bytes().len()); | ||||
dst.put_slice(k.as_bytes()); | ||||
dst.put_u8(b'='); | ||||
dst.put_slice(v.as_bytes()); | ||||
} | ||||
Yuya Nishihara
|
r45183 | fn decode_latin1(s: impl AsRef<[u8]>) -> String { | ||
Yuya Nishihara
|
r40007 | s.as_ref().iter().map(|&c| c as char).collect() | ||
} | ||||
Martin von Zweigbergk
|
r46195 | fn new_parse_error( | ||
error: impl Into<Box<dyn error::Error + Send + Sync>>, | ||||
) -> io::Error { | ||||
Yuya Nishihara
|
r40007 | io::Error::new(io::ErrorKind::InvalidData, error) | ||
} | ||||
#[cfg(test)] | ||||
mod tests { | ||||
Gregory Szorc
|
r44270 | use super::*; | ||
Yuya Nishihara
|
r40007 | use std::os::unix::ffi::OsStringExt; | ||
Yuya Nishihara
|
r45164 | use std::panic; | ||
Yuya Nishihara
|
r40007 | |||
#[test] | ||||
fn parse_command_spec_good() { | ||||
Gregory Szorc
|
r44270 | 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); | ||||
Yuya Nishihara
|
r40007 | let spec = CommandSpec { | ||
command: os_string_from(b"less -FRX"), | ||||
current_dir: os_string_from(b"/tmp"), | ||||
Gregory Szorc
|
r44270 | envs: vec![ | ||
(os_string_from(b"LANG"), os_string_from(b"C")), | ||||
(os_string_from(b"HGPLAIN"), os_string_from(b"")), | ||||
], | ||||
Yuya Nishihara
|
r40007 | }; | ||
Gregory Szorc
|
r44270 | assert_eq!( | ||
parse_command_spec(Bytes::from(src)).unwrap(), | ||||
(CommandType::Pager, spec) | ||||
); | ||||
Yuya Nishihara
|
r40007 | } | ||
#[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()); | ||||
Martin von Zweigbergk
|
r46195 | assert!( | ||
parse_command_spec(Bytes::from_static(b"pager\0less")).is_err() | ||||
); | ||||
Yuya Nishihara
|
r40007 | } | ||
#[test] | ||||
fn parse_command_spec_malformed_env() { | ||||
Martin von Zweigbergk
|
r46195 | assert!(parse_command_spec(Bytes::from_static( | ||
b"pager\0less\0/tmp\0HOME" | ||||
)) | ||||
.is_err()); | ||||
Yuya Nishihara
|
r40007 | } | ||
#[test] | ||||
fn parse_command_spec_unknown_type() { | ||||
Martin von Zweigbergk
|
r46195 | assert!( | ||
parse_command_spec(Bytes::from_static(b"paper\0less")).is_err() | ||||
); | ||||
Yuya Nishihara
|
r40007 | } | ||
Yuya Nishihara
|
r45164 | #[test] | ||
Yuya Nishihara
|
r45170 | fn parse_instructions_good() { | ||
let src = [ | ||||
b"exit 123".as_ref(), | ||||
b"reconnect".as_ref(), | ||||
b"redirect /whatever".as_ref(), | ||||
b"unlink /someother".as_ref(), | ||||
] | ||||
.join(&0); | ||||
let insts = vec![ | ||||
Instruction::Exit(123), | ||||
Instruction::Reconnect, | ||||
Instruction::Redirect(path_buf_from(b"/whatever")), | ||||
Instruction::Unlink(path_buf_from(b"/someother")), | ||||
]; | ||||
assert_eq!(parse_instructions(Bytes::from(src)).unwrap(), insts); | ||||
} | ||||
#[test] | ||||
fn parse_instructions_empty() { | ||||
assert_eq!(parse_instructions(Bytes::new()).unwrap(), vec![]); | ||||
assert_eq!( | ||||
parse_instructions(Bytes::from_static(b"\0")).unwrap(), | ||||
vec![] | ||||
); | ||||
} | ||||
#[test] | ||||
fn parse_instructions_malformed_exit_code() { | ||||
assert!(parse_instructions(Bytes::from_static(b"exit foo")).is_err()); | ||||
} | ||||
#[test] | ||||
fn parse_instructions_missing_argument() { | ||||
assert!(parse_instructions(Bytes::from_static(b"exit")).is_err()); | ||||
assert!(parse_instructions(Bytes::from_static(b"redirect")).is_err()); | ||||
assert!(parse_instructions(Bytes::from_static(b"unlink")).is_err()); | ||||
} | ||||
#[test] | ||||
fn parse_instructions_unknown_command() { | ||||
assert!(parse_instructions(Bytes::from_static(b"quit 0")).is_err()); | ||||
} | ||||
#[test] | ||||
Yuya Nishihara
|
r45164 | fn pack_env_vars_os_good() { | ||
assert_eq!( | ||||
pack_env_vars_os(vec![] as Vec<(OsString, OsString)>), | ||||
Bytes::new() | ||||
); | ||||
assert_eq!( | ||||
pack_env_vars_os(vec![os_string_pair_from(b"FOO", b"bar")]), | ||||
Bytes::from_static(b"FOO=bar") | ||||
); | ||||
assert_eq!( | ||||
pack_env_vars_os(vec![ | ||||
os_string_pair_from(b"FOO", b""), | ||||
os_string_pair_from(b"BAR", b"baz") | ||||
]), | ||||
Bytes::from_static(b"FOO=\0BAR=baz") | ||||
); | ||||
} | ||||
#[test] | ||||
fn pack_env_vars_os_large_key() { | ||||
let mut buf = vec![b'A'; INITIAL_PACKED_ENV_VARS_CAPACITY]; | ||||
let envs = vec![os_string_pair_from(&buf, b"")]; | ||||
buf.push(b'='); | ||||
assert_eq!(pack_env_vars_os(envs), Bytes::from(buf)); | ||||
} | ||||
#[test] | ||||
fn pack_env_vars_os_large_value() { | ||||
let mut buf = vec![b'A', b'=']; | ||||
buf.resize(INITIAL_PACKED_ENV_VARS_CAPACITY + 1, b'a'); | ||||
let envs = vec![os_string_pair_from(&buf[..1], &buf[2..])]; | ||||
assert_eq!(pack_env_vars_os(envs), Bytes::from(buf)); | ||||
} | ||||
#[test] | ||||
fn pack_env_vars_os_nul_eq() { | ||||
assert!(panic::catch_unwind(|| { | ||||
pack_env_vars_os(vec![os_string_pair_from(b"\0", b"")]) | ||||
}) | ||||
.is_err()); | ||||
assert!(panic::catch_unwind(|| { | ||||
pack_env_vars_os(vec![os_string_pair_from(b"FOO", b"\0bar")]) | ||||
}) | ||||
.is_err()); | ||||
assert!(panic::catch_unwind(|| { | ||||
pack_env_vars_os(vec![os_string_pair_from(b"FO=", b"bar")]) | ||||
}) | ||||
.is_err()); | ||||
assert_eq!( | ||||
pack_env_vars_os(vec![os_string_pair_from(b"FOO", b"=ba")]), | ||||
Bytes::from_static(b"FOO==ba") | ||||
); | ||||
} | ||||
Yuya Nishihara
|
r40007 | fn os_string_from(s: &[u8]) -> OsString { | ||
OsString::from_vec(s.to_vec()) | ||||
} | ||||
Yuya Nishihara
|
r45164 | |||
fn os_string_pair_from(k: &[u8], v: &[u8]) -> (OsString, OsString) { | ||||
(os_string_from(k), os_string_from(v)) | ||||
} | ||||
Yuya Nishihara
|
r45170 | |||
fn path_buf_from(s: &[u8]) -> PathBuf { | ||||
os_string_from(s).into() | ||||
} | ||||
Yuya Nishihara
|
r40007 | } | ||