Files
termscp/src/utils/parser.rs
T
Christian Visintin afbc74113f
Deploy docs to GitHub Pages / deploy (push) Has been cancelled
Site / build-site (push) Has been cancelled
Install.sh / build (macos-latest) (push) Has been cancelled
Install.sh / build (ubuntu-latest) (push) Has been cancelled
CI / toolchain (push) Has been cancelled
CI / fmt (push) Has been cancelled
CI / install-scripts (push) Has been cancelled
CI / crates-ubuntu-latest (push) Has been cancelled
CI / crates-windows-latest (push) Has been cancelled
CI / doc (push) Has been cancelled
CI / deny (push) Has been cancelled
CI / crates-macos-latest (push) Has been cancelled
ci: migrate project automation to Just (#442)
* ci: migrate project automation to Just

Centralize build, test, release, dependency, hook, and website commands in Just recipes. Pin workflow tooling, use the repository toolchain, and run the complete validation set in CI.

* ci: codex being codex

* docs: update CLAUDE.md for just task runner migration

Reflect the switch to just recipes for build/test/clippy/fmt, note
dprint replacing raw rustfmt, and add a cross-platform code requirement.

* fix: resolve clippy warnings breaking CI on ubuntu and windows

Use clone() instead of implicit to_string() on already-owned String
values, gate the windows-only unused make_file_at import behind
cfg(posix), and fix unused mut / manual assign-op in the windows-only
localhost test.

* fix: fmt
2026-08-28 19:29:58 +02:00

611 lines
23 KiB
Rust

//! ## Parser
//!
//! `parser` is the module which provides utilities for parsing different kind of stuff
use std::str::FromStr;
use bytesize::ByteSize;
use lazy_regex::{Lazy, Regex};
use tuirealm::ratatui::style::Color;
use tuirealm::utils::parser as tuirealm_parser;
use crate::filetransfer::FileTransferParams;
#[path = "parser/credentials.rs"]
mod credentials;
#[path = "parser/ports.rs"]
mod ports;
#[path = "parser/protocol.rs"]
mod protocol;
#[path = "parser/remote.rs"]
mod remote;
/// This regex matches the protocol used as option.
pub(super) static REMOTE_OPT_PROTOCOL_REGEX: Lazy<Regex> =
lazy_regex!(r"(?:([a-z0-9]+)://)?(\\\\)?(?:(.+))");
/// Regex matches generic remote options.
pub(super) static REMOTE_GENERIC_OPT_REGEX: Lazy<Regex> = lazy_regex!(
r"(?:(.+[^@])@)?(?:([^:]+))(?::((?:[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])))?(?::([^:]+))?"
);
/// Regex matches WebDAV remote options.
pub(super) static REMOTE_WEBDAV_OPT_REGEX: Lazy<Regex> =
lazy_regex!(r"(?:([^:]+):)(?:(.+[^@])@)(?:([^/]+))(?:(.+))?");
/// Regex matches kube remote options.
pub(super) static REMOTE_KUBE_OPT_REGEX: Lazy<Regex> =
lazy_regex!(r"(?:([^@]+))(@(?:([^$]+)))?(\$(?:(.+)))?");
/// Regex matches s3 remote options.
pub(super) static REMOTE_S3_OPT_REGEX: Lazy<Regex> =
lazy_regex!(r"(?:(.+[^@])@)(?:([^:]+))(?::([a-zA-Z0-9][^:]+))?(?::([^:]+))?");
/// Regex matches SMB remote options on Unix platforms.
#[cfg(smb_unix)]
pub(super) static REMOTE_SMB_OPT_REGEX: Lazy<Regex> = lazy_regex!(
r"(?:(.+[^@])@)?(?:([^/:]+))(?::((?:[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])))?(?:/([^/]+))?(?:(/.+))?"
);
/// Regex matches SMB remote options on Windows.
#[cfg(smb_windows)]
pub(super) static REMOTE_SMB_OPT_REGEX: Lazy<Regex> =
lazy_regex!(r"(?:(.+[^@])@)?(?:([^:\\]+))(?:\\([^\\]+))?(?:(\\.+))?");
/// Regex matches semantic versions.
static SEMVER_REGEX: Lazy<Regex> = lazy_regex!(r"v?((0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*))");
/**
* Regex matches:
* - group 1: amount (number)
* - group 4: unit (K, M, G, T, P)
*/
static BYTESIZE_REGEX: Lazy<Regex> = lazy_regex!(r"(:?([0-9])+)( )*(:?[KMGTP])?B$");
/// 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
/// - ...
///
/// For s3:
///
/// `s3://<bucket-name>@<region>[:profile][:/wrkdir]`
///
/// For SMB:
///
/// on UNIX derived (macos, linux, ...)
///
/// `smb://[username@]<address>[:port]/<share>[/path]`
///
/// on Windows
///
/// `\\<address>\<share>[\path]`
///
pub fn parse_remote_opt(s: &str) -> Result<FileTransferParams, String> {
remote::parse_remote_opt(s)
}
/// 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,
}
}
/// Parse color from string into a `Color` enum.
///
/// Color may be in different format:
///
/// 1. color name:
/// - Black,
/// - Blue,
/// - Cyan,
/// - DarkGray,
/// - Gray,
/// - Green,
/// - LightBlue,
/// - LightCyan,
/// - LightGreen,
/// - LightMagenta,
/// - LightRed,
/// - LightYellow,
/// - Magenta,
/// - Red,
/// - Reset,
/// - White,
/// - Yellow,
/// 2. Hex format:
/// - #f0ab05
/// - #AA33BC
/// 3. Rgb format:
/// - rgb(255, 64, 32)
/// - rgb(255,64,32)
/// - 255, 64, 32
pub fn parse_color(color: &str) -> Option<Color> {
tuirealm_parser::parse_color(color)
}
#[derive(Debug, PartialEq)]
enum ByteUnit {
Byte,
Kilobyte,
Megabyte,
Gigabyte,
Terabyte,
Petabyte,
}
impl FromStr for ByteUnit {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"B" => Ok(Self::Byte),
"KB" => Ok(Self::Kilobyte),
"MB" => Ok(Self::Megabyte),
"GB" => Ok(Self::Gigabyte),
"TB" => Ok(Self::Terabyte),
"PB" => Ok(Self::Petabyte),
_ => Err("Invalid unit"),
}
}
}
/// Parse bytes repr (e.g. `24 MB`) into `ByteSize`
pub fn parse_bytesize<S: AsRef<str>>(bytes: S) -> Option<ByteSize> {
match BYTESIZE_REGEX.captures(bytes.as_ref()) {
None => None,
Some(groups) => {
let amount = groups
.get(1)
.map(|x| x.as_str().parse::<u64>().unwrap_or(0))?;
let unit = groups.get(4).map(|x| x.as_str().to_string());
let unit = format!("{}B", unit.unwrap_or_default());
let unit = ByteUnit::from_str(unit.as_str()).ok()?;
Some(match unit {
ByteUnit::Byte => ByteSize::b(amount),
ByteUnit::Gigabyte => ByteSize::gib(amount),
ByteUnit::Kilobyte => ByteSize::kib(amount),
ByteUnit::Megabyte => ByteSize::mib(amount),
ByteUnit::Petabyte => ByteSize::pib(amount),
ByteUnit::Terabyte => ByteSize::tib(amount),
})
}
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use pretty_assertions::assert_eq;
use super::*;
use crate::filetransfer::FileTransferProtocol;
#[test]
fn test_utils_parse_remote_opt() {
// Base case
let result: FileTransferParams = parse_remote_opt(&String::from("172.26.104.1"))
.ok()
.unwrap();
let params = result.params.generic_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert_eq!(params.address, String::from("172.26.104.1"));
assert_eq!(params.port, 22);
assert!(params.username.is_none());
// User case
let result: FileTransferParams = parse_remote_opt(&String::from("root@172.26.104.1"))
.ok()
.unwrap();
let params = result.params.generic_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert_eq!(params.address, String::from("172.26.104.1"));
assert_eq!(params.port, 22);
assert_eq!(
params.username.as_deref().unwrap().to_string(),
String::from("root")
);
assert!(result.remote_path.is_none());
// User + port
let result: FileTransferParams = parse_remote_opt(&String::from("root@172.26.104.1:8022"))
.ok()
.unwrap();
let params = result.params.generic_params().unwrap();
assert_eq!(params.address, String::from("172.26.104.1"));
assert_eq!(params.port, 8022);
assert_eq!(
params.username.as_deref().unwrap().to_string(),
String::from("root")
);
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert!(result.remote_path.is_none());
// Port only
let result: FileTransferParams = parse_remote_opt(&String::from("172.26.104.1:4022"))
.ok()
.unwrap();
let params = result.params.generic_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert_eq!(params.address, String::from("172.26.104.1"));
assert_eq!(params.port, 4022);
assert!(params.username.is_none());
assert!(result.remote_path.is_none());
// Protocol
let result: FileTransferParams = parse_remote_opt(&String::from("ftp://172.26.104.1"))
.ok()
.unwrap();
let params = result.params.generic_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::Ftp(false));
assert_eq!(params.address, String::from("172.26.104.1"));
assert_eq!(params.port, 21); // Fallback to ftp default
assert!(params.username.is_none()); // Doesn't fall back
assert!(result.remote_path.is_none());
// Protocol
let result: FileTransferParams = parse_remote_opt(&String::from("sftp://172.26.104.1"))
.ok()
.unwrap();
let params = result.params.generic_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert_eq!(params.address, String::from("172.26.104.1"));
assert_eq!(params.port, 22); // Fallback to sftp default
assert!(params.username.is_none()); // Doesn't fall back
assert!(result.remote_path.is_none());
let result: FileTransferParams = parse_remote_opt(&String::from("scp://172.26.104.1"))
.ok()
.unwrap();
let params = result.params.generic_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::Scp);
assert_eq!(params.address, String::from("172.26.104.1"));
assert_eq!(params.port, 22); // Fallback to scp default
assert!(params.username.is_none()); // Doesn't fall back
assert!(result.remote_path.is_none());
// Protocol + user
let result: FileTransferParams =
parse_remote_opt(&String::from("ftps://anon@172.26.104.1"))
.ok()
.unwrap();
let params = result.params.generic_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::Ftp(true));
assert_eq!(params.address, String::from("172.26.104.1"));
assert_eq!(params.port, 21); // Fallback to ftp default
assert_eq!(
params.username.as_deref().unwrap().to_string(),
String::from("anon")
);
assert!(result.remote_path.is_none());
// Path
let result: FileTransferParams =
parse_remote_opt(&String::from("root@172.26.104.1:8022:/var"))
.ok()
.unwrap();
let params = result.params.generic_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert_eq!(params.address, String::from("172.26.104.1"));
assert_eq!(params.port, 8022);
assert_eq!(
params.username.as_deref().unwrap().to_string(),
String::from("root")
);
assert_eq!(result.remote_path.unwrap(), PathBuf::from("/var"));
// Port only
let result: FileTransferParams = parse_remote_opt(&String::from("172.26.104.1:home"))
.ok()
.unwrap();
let params = result.params.generic_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert_eq!(params.address, String::from("172.26.104.1"));
assert_eq!(params.port, 22);
assert!(params.username.is_none());
assert_eq!(result.remote_path.unwrap(), PathBuf::from("home"));
// All together now
let result: FileTransferParams =
parse_remote_opt(&String::from("ftp://anon@172.26.104.1:8021:/tmp"))
.ok()
.unwrap();
let params = result.params.generic_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::Ftp(false));
assert_eq!(params.address, String::from("172.26.104.1"));
assert_eq!(params.port, 8021); // Fallback to ftp default
assert_eq!(
params.username.as_deref().unwrap().to_string(),
String::from("anon")
);
assert_eq!(result.remote_path.unwrap(), PathBuf::from("/tmp"));
// bad syntax
// Bad protocol
assert!(parse_remote_opt(&String::from("omar://172.26.104.1")).is_err());
// Bad port
assert!(parse_remote_opt(&String::from("scp://172.26.104.1:650000")).is_err());
// with @ in username
let result: FileTransferParams =
parse_remote_opt(&String::from("dummy@veeso.dev@172.26.104.1:8022"))
.ok()
.unwrap();
let params = result.params.generic_params().unwrap();
assert_eq!(params.address, String::from("172.26.104.1"));
assert_eq!(params.port, 8022);
assert_eq!(
params.username.as_deref().unwrap().to_string(),
String::from("dummy@veeso.dev")
);
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert!(result.remote_path.is_none());
}
#[test]
fn test_should_parse_webdav_opt() {
let result =
parse_remote_opt("https://omar:password@myserver:4445/myshare/dir/subdir").unwrap();
let params = result.params.webdav_params().unwrap();
assert_eq!(params.uri.as_str(), "https://myserver:4445");
assert_eq!(params.username.as_str(), "omar");
assert_eq!(params.password.as_str(), "password");
let result =
parse_remote_opt("http://omar:password@myserver:4445/myshare/dir/subdir").unwrap();
let params = result.params.webdav_params().unwrap();
assert_eq!(params.uri.as_str(), "http://myserver:4445");
assert_eq!(params.username.as_str(), "omar");
assert_eq!(params.password.as_str(), "password");
// remote path
assert_eq!(
result.remote_path.unwrap(),
PathBuf::from("/myshare/dir/subdir")
);
}
#[test]
fn test_should_parse_webdav_opt_with_at() {
let result =
parse_remote_opt("https://omar@veeso.dev:password@myserver:4445/myshare/dir/subdir")
.unwrap();
let params = result.params.webdav_params().unwrap();
assert_eq!(params.uri.as_str(), "https://myserver:4445");
assert_eq!(params.username.as_str(), "omar@veeso.dev");
assert_eq!(params.password.as_str(), "password");
}
#[test]
fn test_should_parse_webdav_opt_with_at_in_password() {
let result =
parse_remote_opt("https://omar:p@ssword@myserver:4445/myshare/dir/subdir").unwrap();
let params = result.params.webdav_params().unwrap();
assert_eq!(params.uri.as_str(), "https://myserver:4445");
assert_eq!(params.username.as_str(), "omar");
assert_eq!(params.password.as_str(), "p@ssword");
assert_eq!(
result.remote_path.as_deref().unwrap(),
std::path::Path::new("/myshare/dir/subdir")
);
}
#[test]
fn should_reject_malformed_webdav_options() {
let result = parse_remote_opt("https://omar@myserver:4445/myshare");
assert!(result.is_err());
}
#[test]
fn should_reject_webdav_options_with_missing_credentials() {
assert!(parse_remote_opt("https://:password@myserver:4445/myshare").is_err());
assert!(parse_remote_opt("https://omar:@myserver:4445/myshare").is_err());
}
#[test]
fn parse_aws_s3_opt() {
// Simple
let result: FileTransferParams =
parse_remote_opt(&String::from("s3://mybucket@eu-central-1"))
.ok()
.unwrap();
let params = result.params.s3_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::AwsS3);
assert_eq!(result.remote_path, None);
assert_eq!(params.bucket_name.as_str(), "mybucket");
assert_eq!(params.region.as_deref().unwrap(), "eu-central-1");
assert_eq!(params.profile, None);
// With profile
let result: FileTransferParams =
parse_remote_opt(&String::from("s3://mybucket@eu-central-1:default"))
.ok()
.unwrap();
let params = result.params.s3_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::AwsS3);
assert_eq!(result.remote_path, None);
assert_eq!(params.bucket_name.as_str(), "mybucket");
assert_eq!(params.region.as_deref().unwrap(), "eu-central-1");
assert_eq!(params.profile.as_deref(), Some("default"));
// With wrkdir only
let result: FileTransferParams =
parse_remote_opt(&String::from("s3://mybucket@eu-central-1:/foobar"))
.ok()
.unwrap();
let params = result.params.s3_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::AwsS3);
assert_eq!(result.remote_path, Some(PathBuf::from("/foobar")));
assert_eq!(params.bucket_name.as_str(), "mybucket");
assert_eq!(params.region.as_deref().unwrap(), "eu-central-1");
assert_eq!(params.profile, None);
// With all arguments
let result: FileTransferParams =
parse_remote_opt(&String::from("s3://mybucket@eu-central-1:default:/foobar"))
.ok()
.unwrap();
let params = result.params.s3_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::AwsS3);
assert_eq!(result.remote_path, Some(PathBuf::from("/foobar")));
assert_eq!(params.bucket_name.as_str(), "mybucket");
assert_eq!(params.region.as_deref().unwrap(), "eu-central-1");
assert_eq!(params.profile.as_deref(), Some("default"));
// -- bad args
assert!(parse_remote_opt(&String::from("s3://mybucket:default:/foobar")).is_err());
// with @
let result: FileTransferParams = parse_remote_opt(&String::from(
"s3://omar@mybucket@eu-central-1:default:/foobar",
))
.ok()
.unwrap();
let params = result.params.s3_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::AwsS3);
assert_eq!(result.remote_path, Some(PathBuf::from("/foobar")));
assert_eq!(params.bucket_name.as_str(), "omar@mybucket");
assert_eq!(params.region.as_deref().unwrap(), "eu-central-1");
}
#[test]
fn should_parse_kube_address() {
let result = parse_remote_opt("kube://my-namespace@http://localhost:1234$/tmp")
.ok()
.unwrap();
let params = result.params.kube_params().unwrap();
assert_eq!(params.namespace, Some("my-namespace".to_string()));
assert_eq!(params.cluster_url.as_deref(), Some("http://localhost:1234"));
assert_eq!(params.username, None);
assert_eq!(params.client_cert, None);
assert_eq!(params.client_key, None);
assert_eq!(
result.remote_path.as_deref().unwrap(),
std::path::Path::new("/tmp")
);
}
#[test]
#[cfg(smb_unix)]
fn should_parse_smb_address() {
let result = parse_remote_opt("smb://myserver/myshare").ok().unwrap();
let params = result.params.smb_params().unwrap();
assert_eq!(params.address.as_str(), "myserver");
assert_eq!(params.port, 445);
assert_eq!(params.share.as_str(), "myshare");
assert!(params.username.is_some());
assert!(params.password.is_none());
assert!(params.workgroup.is_none());
assert!(result.remote_path.is_none());
}
#[test]
#[cfg(smb_unix)]
fn should_parse_smb_address_with_opts() {
let result = parse_remote_opt("smb://omar@myserver:4445/myshare/dir/subdir")
.ok()
.unwrap();
let params = result.params.smb_params().unwrap();
assert_eq!(params.address.as_str(), "myserver");
assert_eq!(params.port, 4445);
assert_eq!(params.username.as_deref().unwrap(), "omar");
assert!(params.password.is_none());
assert!(params.workgroup.is_none());
assert_eq!(params.share.as_str(), "myshare");
assert_eq!(
result.remote_path.as_deref().unwrap(),
std::path::Path::new("/dir/subdir")
);
}
#[test]
#[cfg(smb_windows)]
fn should_parse_smb_address() {
let result = parse_remote_opt(&String::from("\\\\myserver\\myshare"))
.ok()
.unwrap();
let params = result.params.smb_params().unwrap();
assert_eq!(params.address.as_str(), "myserver");
assert_eq!(params.share.as_str(), "myshare");
assert!(result.remote_path.is_none());
}
#[test]
#[cfg(smb_windows)]
fn should_parse_smb_address_with_opts() {
let result = parse_remote_opt(&String::from("\\\\omar@myserver\\myshare\\path"))
.ok()
.unwrap();
let params = result.params.smb_params().unwrap();
assert_eq!(params.address.as_str(), "myserver");
assert_eq!(params.share.as_str(), "myshare");
assert_eq!(params.username.as_deref().unwrap(), "omar");
assert_eq!(
result.remote_path.as_deref().unwrap(),
std::path::Path::new("\\path")
);
}
#[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());
assert_eq!(parse_semver("10.15.10"), Some("10.15.10".to_string()));
}
#[test]
fn test_utils_parse_color() {
assert_eq!(parse_color("Black").unwrap(), Color::Black);
assert_eq!(parse_color("#f0f0f0").unwrap(), Color::Rgb(240, 240, 240));
// -- css colors
assert_eq!(parse_color("aliceblue"), Some(Color::Rgb(240, 248, 255)));
// -- hex and rgb
assert_eq!(
parse_color("rgb(255, 64, 32)").unwrap(),
Color::Rgb(255, 64, 32)
);
// bad
assert!(parse_color("redd").is_none());
}
#[test]
fn parse_byteunit() {
assert_eq!(ByteUnit::from_str("B").ok().unwrap(), ByteUnit::Byte);
assert_eq!(ByteUnit::from_str("KB").ok().unwrap(), ByteUnit::Kilobyte);
assert_eq!(ByteUnit::from_str("MB").ok().unwrap(), ByteUnit::Megabyte);
assert_eq!(ByteUnit::from_str("GB").ok().unwrap(), ByteUnit::Gigabyte);
assert_eq!(ByteUnit::from_str("TB").ok().unwrap(), ByteUnit::Terabyte);
assert_eq!(ByteUnit::from_str("PB").ok().unwrap(), ByteUnit::Petabyte);
assert!(ByteUnit::from_str("uB").is_err());
}
#[test]
fn parse_str_as_bytesize() {
assert_eq!(parse_bytesize("1024 B").unwrap().as_u64(), 1024);
assert_eq!(parse_bytesize("1024B").unwrap().as_u64(), 1024);
assert_eq!(parse_bytesize("10240 KB").unwrap().as_u64(), 10485760);
assert_eq!(parse_bytesize("2 GB").unwrap().as_u64(), 2147483648);
assert_eq!(parse_bytesize("1 TB").unwrap().as_u64(), 1099511627776);
assert!(parse_bytesize("1 XB").is_none());
assert!(parse_bytesize("10 YB").is_none());
assert!(parse_bytesize("1 GB aaaaa").is_none());
assert!(parse_bytesize("1 GBaaaaa").is_none());
assert!(parse_bytesize("1MBaaaaa").is_none());
}
}