Files
termscp/src/utils/parser.rs
veeso 1c58f1d623 Use containers to test file transfers
Use containers to test file transfers

Container setup

Container setup

tests with docker-compose

these tests won't work with containers

ftp tests with containers; removed crap servers; tests only lib

hostname for github

booooooh

fixed recursive remove FTP

Use containers to test file transfers

Container setup

Container setup

tests with docker-compose

these tests won't work with containers

ftp tests with containers; removed crap servers; tests only lib

hostname for github

booooooh

fixed recursive remove FTP

fixed rename

changelog

Desperate attempt

Fixed ftp tests; migrated sftp tests to containers; use env for services

github actions are just broken imo

github actions are just broken imo

Don't use services, since they just don't fuckin work...

docker compose not supported yet?

deprecated typo

Now explain this: github actions have services (which don't work) and then you find out docker-compose is already installed *BLOWMIND*

Fixed hostname for tests

scp tests

maybe

tests

wtf

Restored host tests

Changelog

Improving coverage

Improving coverage

Restored coverage task

More tests for file transfers; test ssh keys too

typo

tests; code improvements

Use tests helpers

fixed tempdir

fixed tempdir
2021-06-15 17:51:06 +02:00

414 lines
16 KiB
Rust

//! ## Parser
//!
//! `parser` is the module which provides utilities for parsing different kind of stuff
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Dependencies
extern crate chrono;
extern crate regex;
extern crate whoami;
// Locals
use crate::filetransfer::FileTransferProtocol;
#[cfg(not(test))] // NOTE: don't use configuration during tests
use crate::system::config_client::ConfigClient;
#[cfg(not(test))] // NOTE: don't use configuration during tests
use crate::system::environment;
// Ext
use chrono::format::ParseError;
use chrono::prelude::*;
use regex::Regex;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::{Duration, SystemTime};
// Regex
lazy_static! {
/**
* Regex matches:
* - group 1: Some(protocol) | None
* - group 2: Some(user) | None
* - group 3: Address
* - group 4: Some(port) | None
* - group 5: Some(path) | None
*/
static ref REMOTE_OPT_REGEX: Regex = Regex::new(r"(?:([a-z]+)://)?(?:([^@]+)@)?(?:([^:]+))(?::((?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])))?(?::([^:]+))?").ok().unwrap();
/**
* Regex matches:
* - group 1: Version
* E.g. termscp-0.3.2 => 0.3.2
* v0.4.0 => 0.4.0
*/
static ref SEMVER_REGEX: Regex = Regex::new(r".*(:?[0-9]\.[0-9]\.[0-9])").unwrap();
}
pub struct RemoteOptions {
pub hostname: String,
pub port: u16,
pub protocol: FileTransferProtocol,
pub username: Option<String>,
pub wrkdir: Option<PathBuf>,
}
/// ### parse_remote_opt
///
/// Parse remote option string. Returns in case of success a RemoteOptions struct
/// For ssh if username is not provided, current user will be used.
/// In case of error, message is returned
/// If port is missing default port will be used for each protocol
/// SFTP => 22
/// FTP => 21
/// The option string has the following syntax
/// [protocol://][username@]{address}[:port][:path]
/// The only argument which is mandatory is address
/// NOTE: possible strings
/// - 172.26.104.1
/// - root@172.26.104.1
/// - sftp://root@172.26.104.1
/// - sftp://172.26.104.1:4022
/// - sftp://172.26.104.1
/// - ...
///
pub fn parse_remote_opt(remote: &str) -> Result<RemoteOptions, String> {
// Set protocol to default protocol
#[cfg(not(test))] // NOTE: don't use configuration during tests
let mut protocol: FileTransferProtocol = match environment::init_config_dir() {
Ok(p) => match p {
Some(p) => {
// Create config client
let (config_path, ssh_key_path) = environment::get_config_paths(p.as_path());
match ConfigClient::new(config_path.as_path(), ssh_key_path.as_path()) {
Ok(cli) => cli.get_default_protocol(),
Err(_) => FileTransferProtocol::Sftp,
}
}
None => FileTransferProtocol::Sftp,
},
Err(_) => FileTransferProtocol::Sftp,
};
#[cfg(test)] // NOTE: during test set protocol just to Sftp
let mut protocol: FileTransferProtocol = FileTransferProtocol::Sftp;
// Match against regex
match REMOTE_OPT_REGEX.captures(remote) {
Some(groups) => {
// Match protocol
let mut port: u16 = 22;
if let Some(group) = groups.get(1) {
// Set protocol from group
let (m_protocol, m_port) = match FileTransferProtocol::from_str(group.as_str()) {
Ok(proto) => match proto {
FileTransferProtocol::Ftp(_) => (proto, 21),
FileTransferProtocol::Scp => (proto, 22),
FileTransferProtocol::Sftp => (proto, 22),
},
Err(_) => return Err(format!("Unknown protocol \"{}\"", group.as_str())),
};
// NOTE: tuple destructuring assignment is not supported yet :(
protocol = m_protocol;
port = m_port;
}
// Match user
let username: Option<String> = match groups.get(2) {
Some(group) => Some(group.as_str().to_string()),
None => match protocol {
// If group is empty, set to current user
FileTransferProtocol::Scp | FileTransferProtocol::Sftp => {
Some(whoami::username())
}
_ => None,
},
};
// Get address
let hostname: String = match groups.get(3) {
Some(group) => group.as_str().to_string(),
None => return Err(String::from("Missing address")),
};
// Get port
if let Some(group) = groups.get(4) {
port = match group.as_str().parse::<u16>() {
Ok(p) => p,
Err(err) => return Err(format!("Bad port \"{}\": {}", group.as_str(), err)),
};
}
// Get workdir
let wrkdir: Option<PathBuf> = groups.get(5).map(|group| PathBuf::from(group.as_str()));
Ok(RemoteOptions {
hostname,
port,
protocol,
username,
wrkdir,
})
}
None => Err(String::from("Bad remote host syntax!")),
}
}
/// ### parse_lstime
///
/// Convert ls syntax time to System Time
/// ls time has two possible syntax:
/// 1. if year is current: %b %d %H:%M (e.g. Nov 5 13:46)
/// 2. else: %b %d %Y (e.g. Nov 5 2019)
pub fn parse_lstime(tm: &str, fmt_year: &str, fmt_hours: &str) -> Result<SystemTime, ParseError> {
let datetime: NaiveDateTime = match NaiveDate::parse_from_str(tm, fmt_year) {
Ok(date) => {
// Case 2.
// Return NaiveDateTime from NaiveDate with time 00:00:00
date.and_hms(0, 0, 0)
}
Err(_) => {
// Might be case 1.
// We need to add Current Year at the end of the string
let this_year: i32 = Utc::now().year();
let date_time_str: String = format!("{} {}", tm, this_year);
// Now parse
NaiveDateTime::parse_from_str(
date_time_str.as_ref(),
format!("{} %Y", fmt_hours).as_ref(),
)?
}
};
// Convert datetime to system time
let sys_time: SystemTime = SystemTime::UNIX_EPOCH;
Ok(sys_time
.checked_add(Duration::from_secs(datetime.timestamp() as u64))
.unwrap_or(SystemTime::UNIX_EPOCH))
}
/// ### parse_datetime
///
/// Parse date time string representation and transform it into `SystemTime`
pub fn parse_datetime(tm: &str, fmt: &str) -> Result<SystemTime, ParseError> {
match NaiveDateTime::parse_from_str(tm, fmt) {
Ok(dt) => {
let sys_time: SystemTime = SystemTime::UNIX_EPOCH;
Ok(sys_time
.checked_add(Duration::from_secs(dt.timestamp() as u64))
.unwrap_or(SystemTime::UNIX_EPOCH))
}
Err(err) => Err(err),
}
}
/// ### parse_semver
///
/// Parse semver string
pub fn parse_semver(haystack: &str) -> Option<String> {
match SEMVER_REGEX.captures(haystack) {
Some(groups) => groups.get(1).map(|version| version.as_str().to_string()),
None => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utils::fmt::fmt_time;
use pretty_assertions::assert_eq;
#[test]
fn test_utils_parse_remote_opt() {
// Base case
let result: RemoteOptions = parse_remote_opt(&String::from("172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.hostname, String::from("172.26.104.1"));
assert_eq!(result.port, 22);
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert!(result.username.is_some());
// User case
let result: RemoteOptions = parse_remote_opt(&String::from("root@172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.hostname, String::from("172.26.104.1"));
assert_eq!(result.port, 22);
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert_eq!(result.username.unwrap(), String::from("root"));
assert!(result.wrkdir.is_none());
// User + port
let result: RemoteOptions = parse_remote_opt(&String::from("root@172.26.104.1:8022"))
.ok()
.unwrap();
assert_eq!(result.hostname, String::from("172.26.104.1"));
assert_eq!(result.port, 8022);
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert_eq!(result.username.unwrap(), String::from("root"));
assert!(result.wrkdir.is_none());
// Port only
let result: RemoteOptions = parse_remote_opt(&String::from("172.26.104.1:4022"))
.ok()
.unwrap();
assert_eq!(result.hostname, String::from("172.26.104.1"));
assert_eq!(result.port, 4022);
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert!(result.username.is_some());
assert!(result.wrkdir.is_none());
// Protocol
let result: RemoteOptions = parse_remote_opt(&String::from("ftp://172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.hostname, String::from("172.26.104.1"));
assert_eq!(result.port, 21); // Fallback to ftp default
assert_eq!(result.protocol, FileTransferProtocol::Ftp(false));
assert!(result.username.is_none()); // Doesn't fall back
assert!(result.wrkdir.is_none());
// Protocol
let result: RemoteOptions = parse_remote_opt(&String::from("sftp://172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.hostname, String::from("172.26.104.1"));
assert_eq!(result.port, 22); // Fallback to sftp default
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert!(result.username.is_some()); // Doesn't fall back
assert!(result.wrkdir.is_none());
let result: RemoteOptions = parse_remote_opt(&String::from("scp://172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.hostname, String::from("172.26.104.1"));
assert_eq!(result.port, 22); // Fallback to scp default
assert_eq!(result.protocol, FileTransferProtocol::Scp);
assert!(result.username.is_some()); // Doesn't fall back
assert!(result.wrkdir.is_none());
// Protocol + user
let result: RemoteOptions = parse_remote_opt(&String::from("ftps://anon@172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.hostname, String::from("172.26.104.1"));
assert_eq!(result.port, 21); // Fallback to ftp default
assert_eq!(result.protocol, FileTransferProtocol::Ftp(true));
assert_eq!(result.username.unwrap(), String::from("anon"));
assert!(result.wrkdir.is_none());
// Path
let result: RemoteOptions = parse_remote_opt(&String::from("root@172.26.104.1:8022:/var"))
.ok()
.unwrap();
assert_eq!(result.hostname, String::from("172.26.104.1"));
assert_eq!(result.port, 8022);
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert_eq!(result.username.unwrap(), String::from("root"));
assert_eq!(result.wrkdir.unwrap(), PathBuf::from("/var"));
// Port only
let result: RemoteOptions = parse_remote_opt(&String::from("172.26.104.1:home"))
.ok()
.unwrap();
assert_eq!(result.hostname, String::from("172.26.104.1"));
assert_eq!(result.port, 22);
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert!(result.username.is_some());
assert_eq!(result.wrkdir.unwrap(), PathBuf::from("home"));
// All together now
let result: RemoteOptions =
parse_remote_opt(&String::from("ftp://anon@172.26.104.1:8021:/tmp"))
.ok()
.unwrap();
assert_eq!(result.hostname, String::from("172.26.104.1"));
assert_eq!(result.port, 8021); // Fallback to ftp default
assert_eq!(result.protocol, FileTransferProtocol::Ftp(false));
assert_eq!(result.username.unwrap(), String::from("anon"));
assert_eq!(result.wrkdir.unwrap(), PathBuf::from("/tmp"));
// bad syntax
assert!(parse_remote_opt(&String::from("omar://172.26.104.1")).is_err()); // Bad protocol
assert!(parse_remote_opt(&String::from("omar://172.26.104.1:650000")).is_err());
// Bad port
}
#[test]
fn test_utils_parse_lstime() {
// Good cases
assert_eq!(
fmt_time(
parse_lstime("Nov 5 16:32", "%b %d %Y", "%b %d %H:%M")
.ok()
.unwrap(),
"%m %d %M"
)
.as_str(),
"11 05 32"
);
assert_eq!(
fmt_time(
parse_lstime("Dec 2 21:32", "%b %d %Y", "%b %d %H:%M")
.ok()
.unwrap(),
"%m %d %M"
)
.as_str(),
"12 02 32"
);
assert_eq!(
parse_lstime("Nov 5 2018", "%b %d %Y", "%b %d %H:%M")
.ok()
.unwrap()
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1541376000)
);
assert_eq!(
parse_lstime("Mar 18 2018", "%b %d %Y", "%b %d %H:%M")
.ok()
.unwrap()
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1521331200)
);
// bad cases
assert!(parse_lstime("Oma 31 2018", "%b %d %Y", "%b %d %H:%M").is_err());
assert!(parse_lstime("Feb 31 2018", "%b %d %Y", "%b %d %H:%M").is_err());
assert!(parse_lstime("Feb 15 25:32", "%b %d %Y", "%b %d %H:%M").is_err());
}
#[test]
fn test_utils_parse_datetime() {
assert_eq!(
parse_datetime("04-08-14 03:09PM", "%d-%m-%y %I:%M%p")
.ok()
.unwrap()
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1407164940)
);
// Not enough argument for datetime
assert!(parse_datetime("04-08-14", "%d-%m-%y").is_err());
}
#[test]
fn test_utils_parse_semver() {
assert_eq!(
parse_semver("termscp-0.3.2").unwrap(),
String::from("0.3.2")
);
assert_eq!(parse_semver("v0.4.1").unwrap(), String::from("0.4.1"),);
assert_eq!(parse_semver("1.0.0").unwrap(), String::from("1.0.0"),);
assert!(parse_semver("v1.1").is_none());
}
}