extern crate std;
extern crate bytes;
use failure::*;
use std::{fmt, result};
use self::bytes::{Bytes};
#[derive(Debug, PartialEq, Clone)]
pub enum StruParam {
File,
Record,
Page,
}
#[derive(Debug, PartialEq, Clone)]
pub enum ModeParam {
Stream,
Block,
Compressed,
}
#[derive(Debug, PartialEq, Clone)]
pub enum Opt {
UTF8,
}
#[derive(Debug, PartialEq, Clone)]
pub enum Command {
User {
username: Bytes,
},
Pass {
password: Bytes,
},
Acct {
account: Bytes,
},
Syst,
Stat {
path: Option<Bytes>,
},
Type,
Stru {
structure: StruParam,
},
Mode {
mode: ModeParam,
},
Help,
Noop,
Pasv,
Port,
Retr {
path: String,
},
Stor {
path: String
},
List {
path: Option<String>,
},
Nlst {
path: Option<String>
},
Feat,
Pwd,
Cwd {
path: std::path::PathBuf,
},
Cdup,
Opts {
option: Opt
},
Dele {
path: String,
},
Quit,
Mkd {
path: std::path::PathBuf,
},
}
impl Command {
pub fn parse<T: AsRef<[u8]> + Into<Bytes>>(buf: T) -> Result<Command> {
let vec = buf.into().to_vec();
let mut iter = vec.splitn(2, |&b| b == b' ' || b == b'\r' || b == b'\n');
let cmd_token = iter.next().unwrap();
let cmd_params = iter.next().unwrap_or(&[]);
let cmd = match cmd_token {
b"USER" | b"user" => {
let username = parse_to_eol(cmd_params)?;
Command::User{
username,
}
},
b"PASS" | b"pass" => {
let password = parse_to_eol(cmd_params)?;
Command::Pass{
password,
}
}
b"ACCT" | b"acct" => {
let account = parse_to_eol(cmd_params)?;
Command::Acct{
account,
}
}
b"SYST" | b"syst" => Command::Syst,
b"STAT" => {
let params = parse_to_eol(cmd_params)?;
let path = if !params.is_empty() { Some(params) } else { None };
Command::Stat{path}
},
b"TYPE" | b"type" => {
Command::Type
},
b"STRU" | b"stru" => {
let params = parse_to_eol(cmd_params)?;
if params.len() > 1 {
return Err(ParseErrorKind::InvalidCommand)?;
}
match params.first() {
Some(b'F') => Command::Stru{structure: StruParam::File},
Some(b'R') => Command::Stru{structure: StruParam::Record},
Some(b'P') => Command::Stru{structure: StruParam::Page},
_ => return Err(ParseErrorKind::InvalidCommand)?,
}
},
b"MODE" | b"mode" => {
let params = parse_to_eol(cmd_params)?;
if params.len() > 1 {
return Err(ParseErrorKind::InvalidCommand)?;
}
match params.first() {
Some(b'S') => Command::Mode{mode: ModeParam::Stream},
Some(b'B') => Command::Mode{mode: ModeParam::Block},
Some(b'C') => Command::Mode{mode: ModeParam::Compressed},
_ => return Err(ParseErrorKind::InvalidCommand)?,
}
},
b"HELP" | b"help" => Command::Help,
b"NOOP" | b"noop" => {
let params = parse_to_eol(cmd_params)?;
if !params.is_empty() {
return Err(ParseErrorKind::InvalidCommand)?;
}
Command::Noop
},
b"PASV" | b"pasv" => {
let params = parse_to_eol(cmd_params)?;
if !params.is_empty() {
return Err(ParseErrorKind::InvalidCommand)?;
}
Command::Pasv
},
b"PORT" | b"port" => {
let params = parse_to_eol(cmd_params)?;
if params.is_empty() {
return Err(ParseErrorKind::InvalidCommand)?;
}
Command::Port
},
b"RETR" | b"retr" => {
let path = parse_to_eol(cmd_params)?;
if path.is_empty() {
return Err(ParseErrorKind::InvalidCommand)?;
}
let path = String::from_utf8_lossy(&path);
Command::Retr{path: path.to_string()}
},
b"STOR" | b"stor" => {
let path = parse_to_eol(cmd_params)?;
if path.is_empty() {
return Err(ParseErrorKind::InvalidCommand)?;
}
let path = String::from_utf8_lossy(&path);
Command::Stor{path: path.to_string()}
},
b"LIST" | b"list" => {
let path = parse_to_eol(cmd_params)?;
let path = if path.is_empty() { None } else { Some(String::from_utf8_lossy(&path).to_string()) };
Command::List{path: path}
},
b"NLST" | b"nlst" => {
let path = parse_to_eol(cmd_params)?;
let path = if path.is_empty() { None } else { Some(String::from_utf8_lossy(&path).to_string()) };
Command::Nlst{path: path}
},
b"FEAT" |b"feat" => {
let params = parse_to_eol(cmd_params)?;
if !params.is_empty() {
return Err(ParseErrorKind::InvalidCommand)?;
}
Command::Feat
},
b"PWD" | b"XPWD" | b"pwd" | b"xpwd" => {
let params = parse_to_eol(cmd_params)?;
if !params.is_empty() {
return Err(ParseErrorKind::InvalidCommand)?;
}
Command::Pwd
},
b"CWD" | b"XCWD" | b"cwd" | b"xcwd" => {
let path = parse_to_eol(cmd_params)?;
if path.is_empty() {
return Err(ParseErrorKind::InvalidCommand)?;
}
let path = String::from_utf8_lossy(&path).to_string();
let path = path.into();
Command::Cwd{path}
},
b"CDUP" | b"cdup" => {
let params = parse_to_eol(cmd_params)?;
if !params.is_empty() {
return Err(ParseErrorKind::InvalidCommand)?;
}
Command::Cdup
},
b"OPTS" | b"opts" => {
let params = parse_to_eol(cmd_params)?;
if params.is_empty() {
return Err(ParseErrorKind::InvalidCommand)?
}
match ¶ms[..] {
b"UTF8" => Command::Opts{option: Opt::UTF8},
_ => return Err(ParseErrorKind::InvalidCommand)?,
}
},
b"DELE" | b"dele" => {
let path = parse_to_eol(cmd_params)?;
if path.is_empty() {
return Err(ParseErrorKind::InvalidCommand)?
}
let path = String::from_utf8_lossy(&path).to_string();
let path = path.into();
Command::Dele{path}
},
b"QUIT" | b"quit" => {
let params = parse_to_eol(cmd_params)?;
if !params.is_empty() {
return Err(ParseErrorKind::InvalidCommand)?
}
Command::Quit
},
b"MKD" | b"XMKD" => {
let params = parse_to_eol(cmd_params)?;
if params.is_empty() {
return Err(ParseErrorKind::InvalidCommand)?
}
let path = String::from_utf8_lossy(¶ms).to_string();
let path = path.into();
Command::Mkd{path}
},
_ => return Err(ParseErrorKind::UnknownCommand{command: std::str::from_utf8(cmd_token).context(ParseErrorKind::InvalidUTF8)?.to_string()})?,
};
Ok(cmd)
}
}
fn parse_to_eol<T: AsRef<[u8]> + Into<Bytes>>(bytes: T) -> Result<Bytes> {
let mut pos: usize = 0;
let mut bytes: Bytes = bytes.into();
let copy = bytes.clone();
let mut iter = copy.as_ref().iter();
loop {
let b = match iter.next() {
Some(b) => b,
_ => return Err(ParseErrorKind::InvalidEOL)?,
};
if *b == b'\r' {
match iter.next() {
Some(b'\n') => return Ok(bytes.split_to(pos)),
_ => return Err(ParseErrorKind::InvalidEOL)?,
}
}
if *b == b'\n' {
return Ok(bytes.split_to(pos));
}
if !is_valid_token_char(*b) {
return Err(ParseErrorKind::InvalidToken{token: *b})?;
}
pos += 1;
}
}
fn is_valid_token_char(b: u8) -> bool {
b > 0x1F && b < 0x7F
}
#[derive(Debug)]
pub struct ParseError {
inner: Context<ParseErrorKind>,
}
impl PartialEq for ParseError {
#[inline]
fn eq(&self, other: &ParseError) -> bool {
self.kind() == other.kind()
}
}
#[derive(Clone, Eq, PartialEq, Debug, Fail)]
pub enum ParseErrorKind {
#[fail(display = "Unknown command: {}", command)]
UnknownCommand {
command: String,
},
#[fail(display = "Invalid command")]
InvalidCommand,
#[fail(display = "Invalid token while parsing: {}", token)]
InvalidToken{
token: u8,
},
#[fail(display = "Non-UTF8 character while parsing")]
InvalidUTF8,
#[fail(display = "Invalid end-of-line")]
InvalidEOL,
}
impl Fail for ParseError {
fn cause(&self) -> Option<&Fail> {
self.inner.cause()
}
fn backtrace(&self) -> Option<&Backtrace> {
self.inner.backtrace()
}
}
impl ParseError {
pub fn kind(&self) -> &ParseErrorKind {
self.inner.get_context()
}
}
impl From<ParseErrorKind> for ParseError {
fn from(kind: ParseErrorKind) -> ParseError {
ParseError { inner: Context::new(kind) }
}
}
impl From<Context<ParseErrorKind>> for ParseError {
fn from(inner: Context<ParseErrorKind>) -> ParseError {
ParseError { inner: inner }
}
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Display::fmt(&self.inner, f)
}
}
pub type Result<T> = result::Result<T, ParseError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_user_cmd_crnl() {
let input = "USER Dolores\r\n";
assert_eq!(Command::parse(input).unwrap(), Command::User{username: "Dolores".into()});
}
#[test]
fn parse_user_cmd_mixed_case() {
let input = "uSeR Dolores\r\n";
assert_eq!(Command::parse(input), Err(ParseError{inner: Context::new(ParseErrorKind::UnknownCommand{command: "uSeR".into() })}));
}
#[test]
fn parse_user_lowercase() {
let input = "user Dolores\r\n";
assert_eq!(Command::parse(input).unwrap(), Command::User{username: "Dolores".into()});
}
#[test]
fn parse_user_cmd_nl(){
let input = "USER Dolores\n";
assert_eq!(Command::parse(input).unwrap(), Command::User{username: "Dolores".into()});
}
#[test]
fn parse_user_cmd_cr() {
let input = "USER Dolores\r";
assert_eq!(Command::parse(input), Err(ParseError{inner: Context::new(ParseErrorKind::InvalidEOL)}));
}
#[test]
fn parse_user_cmd_no_eol() {
let input = "USER Dolores";
assert_eq!(Command::parse(input), Err(ParseError{inner: Context::new(ParseErrorKind::InvalidEOL)}));
}
#[test]
fn parse_user_cmd_double_space(){
let input = "USER Dolores\r\n";
assert_eq!(Command::parse(input).unwrap(), Command::User{username: " Dolores".into()});
}
#[test]
fn parse_user_cmd_whitespace() {
let input = "USER Dolores Abernathy\r\n";
assert_eq!(Command::parse(input).unwrap(), Command::User{username: "Dolores Abernathy".into()});
}
#[test]
fn parse_pass_cmd_crnl() {
let input = "PASS s3cr3t\r\n";
assert_eq!(Command::parse(input).unwrap(), Command::Pass{password: "s3cr3t".into()});
}
#[test]
fn parse_pass_cmd_whitespace() {
let input = "PASS s3cr#t p@S$w0rd\r\n";
assert_eq!(Command::parse(input).unwrap(), Command::Pass{password: "s3cr#t p@S$w0rd".into()});
}
#[test]
fn parse_acct() {
let input = "ACCT Teddy\r\n";
assert_eq!(Command::parse(input).unwrap(), Command::Acct{account: "Teddy".into()});
}
#[test]
fn parse_stru_no_params() {
let input = "STRU\r\n";
assert_eq!(Command::parse(input), Err(ParseError{inner: Context::new(ParseErrorKind::InvalidCommand)}));
}
#[test]
fn parse_stru_f() {
let input = "STRU F\r\n";
assert_eq!(Command::parse(input).unwrap(), Command::Stru{structure: StruParam::File});
}
#[test]
fn parse_stru_r() {
let input = "STRU R\r\n";
assert_eq!(Command::parse(input).unwrap(), Command::Stru{structure: StruParam::Record});
}
#[test]
fn parse_stru_p() {
let input = "STRU P\r\n";
assert_eq!(Command::parse(input).unwrap(), Command::Stru{structure: StruParam::Page});
}
#[test]
fn parse_stru_garbage() {
let input = "STRU FSK\r\n";
assert_eq!(Command::parse(input), Err(ParseError{inner: Context::new(ParseErrorKind::InvalidCommand)}));
let input = "STRU F lskdjf\r\n";
assert_eq!(Command::parse(input), Err(ParseError{inner: Context::new(ParseErrorKind::InvalidCommand)}));
let input = "STRU\r\n";
assert_eq!(Command::parse(input), Err(ParseError{inner: Context::new(ParseErrorKind::InvalidCommand)}));
}
#[test]
fn parse_mode_s() {
let input = "MODE S\r\n";
assert_eq!(Command::parse(input).unwrap(), Command::Mode{mode: ModeParam::Stream});
}
#[test]
fn parse_mode_b() {
let input = "MODE B\r\n";
assert_eq!(Command::parse(input).unwrap(), Command::Mode{mode: ModeParam::Block});
}
#[test]
fn parse_mode_c() {
let input = "MODE C\r\n";
assert_eq!(Command::parse(input).unwrap(), Command::Mode{mode: ModeParam::Compressed});
}
#[test]
fn parse_mode_garbage() {
let input = "MODE SKDJF\r\n";
assert_eq!(Command::parse(input), Err(ParseError{inner: Context::new(ParseErrorKind::InvalidCommand)}));
let input = "MODE\r\n";
assert_eq!(Command::parse(input), Err(ParseError{inner: Context::new(ParseErrorKind::InvalidCommand)}));
let input = "MODE S D\r\n";
assert_eq!(Command::parse(input), Err(ParseError{inner: Context::new(ParseErrorKind::InvalidCommand)}));
}
#[test]
fn parse_help() {
let input = "HELP\r\n";
assert_eq!(Command::parse(input).unwrap(), Command::Help);
let input = "HELP bla\r\n";
assert_eq!(Command::parse(input).unwrap(), Command::Help);
}
#[test]
fn parse_noop() {
let input = "NOOP\r\n";
assert_eq!(Command::parse(input).unwrap(), Command::Noop);
let input = "NOOP bla\r\n";
assert_eq!(Command::parse(input), Err(ParseError{inner: Context::new(ParseErrorKind::InvalidCommand)}));
}
#[test]
fn parse_pasv() {
let input = "PASV\r\n";
assert_eq!(Command::parse(input).unwrap(), Command::Pasv);
let input = "PASV bla\r\n";
assert_eq!(Command::parse(input), Err(ParseError{inner: Context::new(ParseErrorKind::InvalidCommand)}));
}
#[test]
fn parse_port() {
let input = "PORT\r\n";
assert_eq!(Command::parse(input), Err(ParseError{inner: Context::new(ParseErrorKind::InvalidCommand)}));
let input = "PORT a1,a2,a3,a4,p1,p2\r\n";
assert_eq!(Command::parse(input).unwrap(), Command::Port);
}
#[test]
fn parse_list() {
let input = "LIST\r\n";
assert_eq!(Command::parse(input), Ok(Command::List{path: None}));
let input = "LIST tmp\r\n";
let expected_path = Some("tmp".to_string());
assert_eq!(Command::parse(input), Ok(Command::List{path: expected_path}));
}
#[test]
fn parse_feat() {
let input = "FEAT\r\n";
assert_eq!(Command::parse(input), Ok(Command::Feat));
let input = "FEAT bla\r\n";
assert_eq!(Command::parse(input), Err(ParseError{inner: Context::new(ParseErrorKind::InvalidCommand)}));
}
#[test]
fn parse_pwd() {
let input = "PWD\r\n";
assert_eq!(Command::parse(input), Ok(Command::Pwd));
let input = "PWD bla\r\n";
assert_eq!(Command::parse(input), Err(ParseError{inner: Context::new(ParseErrorKind::InvalidCommand)}));
}
#[test]
fn parse_cwd() {
let input = "CWD\r\n";
assert_eq!(Command::parse(input), Err(ParseError{inner: Context::new(ParseErrorKind::InvalidCommand)}));
let input = "CWD /tmp\r\n";
assert_eq!(Command::parse(input), Ok(Command::Cwd{path: "/tmp".into()}));
let input = "CWD public\r\n";
assert_eq!(Command::parse(input), Ok(Command::Cwd{path: "public".into()}));
}
#[test]
fn parse_cdup() {
let input = "CDUP\r\n";
assert_eq!(Command::parse(input), Ok(Command::Cdup));
let input = "CDUP bla\r\n";
assert_eq!(Command::parse(input), Err(ParseError{inner: Context::new(ParseErrorKind::InvalidCommand)}));
}
#[test]
fn parse_opts() {
let input = "OPTS\r\n";
assert_eq!(Command::parse(input), Err(ParseError{inner: Context::new(ParseErrorKind::InvalidCommand)}));
let input = "OPTS bla\r\n";
assert_eq!(Command::parse(input), Err(ParseError{inner: Context::new(ParseErrorKind::InvalidCommand)}));
let input = "OPTS UTF8\r\n";
assert_eq!(Command::parse(input), Ok(Command::Opts{option: Opt::UTF8}));
}
#[test]
fn parse_dele() {
let input = "DELE\r\n";
assert_eq!(Command::parse(input), Err(ParseError{inner: Context::new(ParseErrorKind::InvalidCommand)}));
let input = "DELE some_file\r\n";
assert_eq!(Command::parse(input), Ok(Command::Dele{path: "some_file".into()}));
}
#[test]
fn parse_quit() {
let input = "QUIT\r\n";
assert_eq!(Command::parse(input), Ok(Command::Quit));
let input = "QUIT NOW\r\n";
assert_eq!(Command::parse(input), Err(ParseError{inner: Context::new(ParseErrorKind::InvalidCommand)}));
}
#[test]
fn parse_mkd() {
let input = "MKD\r\n";
assert_eq!(Command::parse(input), Err(ParseError{inner: Context::new(ParseErrorKind::InvalidCommand)}));
let input = "MKD bla\r\n";
assert_eq!(Command::parse(input), Ok(Command::Mkd{path: "bla".into()}));
}
}