mirror of
https://github.com/veeso/termscp.git
synced 2026-07-02 12:44:12 +02:00
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
414 lines
16 KiB
Rust
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());
|
|
}
|
|
}
|