• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

veeso / termscp / 4959928621

pending completion
4959928621

Pull #184

github

GitHub
Merge 79d6a6c1a into 79dd9e230
Pull Request #184: SMB support

238 of 238 new or added lines in 6 files covered. (100.0%)

5222 of 5604 relevant lines covered (93.18%)

14.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.77
/src/utils/parser.rs
1
//! ## Parser
×
2
//!
3
//! `parser` is the module which provides utilities for parsing different kind of stuff
4

5
// Locals
6
use std::path::PathBuf;
7
use std::str::FromStr;
8

9
// Ext
10
use bytesize::ByteSize;
11
use lazy_regex::{Lazy, Regex};
12
use tuirealm::tui::style::Color;
13
use tuirealm::utils::parser as tuirealm_parser;
14

15
#[cfg(smb)]
16
use crate::filetransfer::params::SmbParams;
17
use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams};
18
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
19
#[cfg(not(test))] // NOTE: don't use configuration during tests
20
use crate::system::config_client::ConfigClient;
21
#[cfg(not(test))] // NOTE: don't use configuration during tests
22
use crate::system::environment;
23

24
// Regex
25

26
/**
27
 * This regex matches the protocol used as option
28
 * Regex matches:
29
 * - group 1: Some(protocol) | None
30
 * - group 2: SMB windows prefix
31
 * - group 3: Some(other args)
32
 */
33
static REMOTE_OPT_PROTOCOL_REGEX: Lazy<Regex> = lazy_regex!(r"(?:([a-z0-9]+)://)?(\\)?(?:(.+))");
4✔
34

35
/**
36
 * Regex matches:
37
 *  - group 1: Some(user) | None
38
 *  - group 2: Address
39
 *  - group 3: Some(port) | None
40
 *  - group 4: Some(path) | None
41
 */
42
static REMOTE_GENERIC_OPT_REGEX: Lazy<Regex> = lazy_regex!(
1✔
43
    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])))?(?::([^:]+))?"
44
);
1✔
45

46
/**
47
 * Regex matches:
48
 * - group 1: Bucket
49
 * - group 2: Region
50
 * - group 3: Some(profile) | None
51
 * - group 4: Some(path) | None
52
 */
53
static REMOTE_S3_OPT_REGEX: Lazy<Regex> =
54
    lazy_regex!(r"(?:([^@]+)@)(?:([^:]+))(?::([a-zA-Z0-9][^:]+))?(?::([^:]+))?");
1✔
55

56
/**
57
 * Regex matches:
58
 * - group 1: username
59
 * - group 2: address
60
 * - group 3: port?
61
 * - group 4: share?
62
 * - group 5: remote-dir?
63
 */
64
#[cfg(smb_unix)]
65
static REMOTE_SMB_OPT_REGEX: Lazy<Regex> = lazy_regex!(
2✔
66
    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])))?(?:/([^/]+))?(?:(/.+))?"
67
);
2✔
68

69
/**
70
 * Regex matches:
71
 * - group 1: address
72
 * - group 2: port?
73
 * - group 3: share
74
 * - group 4: remote-dir?
75
 */
76
#[cfg(windows)]
77
static REMOTE_SMB_OPT_REGEX: Lazy<Regex> = lazy_regex!(
78
    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])))?(?:\\([^\\]+))?(?:(\\.+))?"
79
);
80

81
/**
82
 * Regex matches:
83
 * - group 1: Version
84
 * E.g. termscp-0.3.2 => 0.3.2
85
 *      v0.4.0 => 0.4.0
86
 */
87
static SEMVER_REGEX: Lazy<Regex> = lazy_regex!(r".*(:?[0-9]\.[0-9]\.[0-9])");
2✔
88

89
/**
90
 * Regex matches:
91
 * - group 1: amount (number)
92
 * - group 4: unit (K, M, G, T, P)
93
 */
94
static BYTESIZE_REGEX: Lazy<Regex> = lazy_regex!(r"(:?([0-9])+)( )*(:?[KMGTP])?B$");
1✔
95

96
// -- remote opts
97

98
/// Parse remote option string. Returns in case of success a RemoteOptions struct
99
/// For ssh if username is not provided, current user will be used.
100
/// In case of error, message is returned
101
/// If port is missing default port will be used for each protocol
102
///     SFTP => 22
103
///     FTP => 21
104
/// The option string has the following syntax
105
/// [protocol://][username@]{address}[:port][:path]
106
/// The only argument which is mandatory is address
107
/// NOTE: possible strings
108
/// - 172.26.104.1
109
/// - root@172.26.104.1
110
/// - sftp://root@172.26.104.1
111
/// - sftp://172.26.104.1:4022
112
/// - sftp://172.26.104.1
113
/// - ...
114
///
115
/// For s3:
116
///
117
/// s3://<bucket-name>@<region>[:profile][:/wrkdir]
118
///
119
/// For SMB:
120
///
121
/// on UNIX derived (macos, linux, ...)
122
///
123
/// smb://[username@]<address>[:port]/<share>[/path]
124
///
125
/// on Windows
126
///
127
/// \\<address>\<share>[\path]
128
///
129
pub fn parse_remote_opt(s: &str) -> Result<FileTransferParams, String> {
20✔
130
    // Set protocol to default protocol
131
    #[cfg(not(test))] // NOTE: don't use configuration during tests
132
    let default_protocol: FileTransferProtocol = match environment::init_config_dir() {
133
        Ok(p) => match p {
134
            Some(p) => {
135
                // Create config client
136
                let (config_path, ssh_key_path) = environment::get_config_paths(p.as_path());
137
                match ConfigClient::new(config_path.as_path(), ssh_key_path.as_path()) {
138
                    Ok(cli) => cli.get_default_protocol(),
139
                    Err(_) => FileTransferProtocol::Sftp,
140
                }
141
            }
142
            None => FileTransferProtocol::Sftp,
143
        },
144
        Err(_) => FileTransferProtocol::Sftp,
145
    };
146
    #[cfg(test)] // NOTE: during test set protocol just to Sftp
147
    let default_protocol: FileTransferProtocol = FileTransferProtocol::Sftp;
20✔
148
    // Get protocol
149
    let (protocol, s): (FileTransferProtocol, String) =
19✔
150
        parse_remote_opt_protocol(s, default_protocol)?;
20✔
151
    // Match against regex for protocol type
152
    match protocol {
19✔
153
        FileTransferProtocol::AwsS3 => parse_s3_remote_opt(s.as_str()),
5✔
154
        #[cfg(smb)]
155
        FileTransferProtocol::Smb => parse_smb_remote_opts(s.as_str()),
2✔
156
        protocol => parse_generic_remote_opt(s.as_str(), protocol),
12✔
157
    }
158
}
20✔
159

160
/// Parse protocol from CLI option. In case of success, return the protocol to be used and the remaining arguments
161
fn parse_remote_opt_protocol(
20✔
162
    s: &str,
163
    default: FileTransferProtocol,
164
) -> Result<(FileTransferProtocol, String), String> {
165
    match REMOTE_OPT_PROTOCOL_REGEX.captures(s) {
20✔
166
        Some(groups) => {
20✔
167
            // Parse protocol or use default
168
            let protocol = groups.get(1).map(|x| {
34✔
169
                FileTransferProtocol::from_str(x.as_str())
28✔
170
                    .map_err(|_| format!("Unknown protocol \"{}\"", x.as_str()))
15✔
171
            });
14✔
172
            let protocol = match protocol {
20✔
173
                Some(Ok(protocol)) => protocol,
13✔
174
                Some(Err(err)) => return Err(err),
1✔
175
                #[cfg(windows)]
176
                None if groups.get(2).is_some() => FileTransferProtocol::Smb,
177
                None => default,
6✔
178
            };
179
            // Return protocol and remaining arguments
180
            Ok((
19✔
181
                protocol,
19✔
182
                groups
19✔
183
                    .get(3)
184
                    .map(|x| x.as_str().to_string())
19✔
185
                    .unwrap_or_default(),
186
            ))
187
        }
20✔
188
        None => Err("Invalid args".to_string()),
×
189
    }
190
}
20✔
191

192
/// Parse generic remote options
193
fn parse_generic_remote_opt(
12✔
194
    s: &str,
195
    protocol: FileTransferProtocol,
196
) -> Result<FileTransferParams, String> {
197
    match REMOTE_GENERIC_OPT_REGEX.captures(s) {
12✔
198
        Some(groups) => {
12✔
199
            // Match user
200
            let username: Option<String> = match groups.get(1) {
12✔
201
                Some(group) => Some(group.as_str().to_string()),
5✔
202
                None => match protocol {
7✔
203
                    // If group is empty, set to current user
204
                    FileTransferProtocol::Scp | FileTransferProtocol::Sftp => {
205
                        Some(whoami::username())
6✔
206
                    }
207
                    _ => None,
1✔
208
                },
209
            };
210
            // Get address
211
            let address: String = match groups.get(2) {
12✔
212
                Some(group) => group.as_str().to_string(),
12✔
213
                None => return Err(String::from("Missing address")),
×
214
            };
215
            // Get port
216
            let port: u16 = match groups.get(3) {
12✔
217
                Some(port) => match port.as_str().parse::<u16>() {
5✔
218
                    // Try to parse port
219
                    Ok(p) => p,
4✔
220
                    Err(err) => return Err(format!("Bad port \"{}\": {}", port.as_str(), err)),
1✔
221
                },
4✔
222
                None => match protocol {
7✔
223
                    // Set port based on protocol
224
                    FileTransferProtocol::Ftp(_) => 21,
2✔
225
                    FileTransferProtocol::Scp => 22,
1✔
226
                    FileTransferProtocol::Sftp => 22,
4✔
227
                    _ => 22, // Doesn't matter
×
228
                },
229
            };
230
            // Get workdir
231
            let entry_directory: Option<PathBuf> =
232
                groups.get(4).map(|group| PathBuf::from(group.as_str()));
14✔
233
            let params: ProtocolParams = ProtocolParams::Generic(
11✔
234
                GenericProtocolParams::default()
44✔
235
                    .address(address)
11✔
236
                    .port(port)
11✔
237
                    .username(username),
11✔
238
            );
239
            Ok(FileTransferParams::new(protocol, params).entry_directory(entry_directory))
11✔
240
        }
12✔
241
        None => Err(String::from("Bad remote host syntax!")),
×
242
    }
243
}
12✔
244

245
/// Parse remote options for s3 protocol
246
fn parse_s3_remote_opt(s: &str) -> Result<FileTransferParams, String> {
5✔
247
    match REMOTE_S3_OPT_REGEX.captures(s) {
5✔
248
        Some(groups) => {
4✔
249
            let bucket: String = groups
4✔
250
                .get(1)
251
                .map(|x| x.as_str().to_string())
4✔
252
                .unwrap_or_default();
253
            let region: String = groups
4✔
254
                .get(2)
255
                .map(|x| x.as_str().to_string())
4✔
256
                .unwrap_or_default();
257
            let profile: Option<String> = groups.get(3).map(|x| x.as_str().to_string());
6✔
258
            let entry_directory: Option<PathBuf> =
259
                groups.get(4).map(|group| PathBuf::from(group.as_str()));
6✔
260
            Ok(FileTransferParams::new(
8✔
261
                FileTransferProtocol::AwsS3,
4✔
262
                ProtocolParams::AwsS3(AwsS3Params::new(bucket, Some(region), profile)),
4✔
263
            )
264
            .entry_directory(entry_directory))
4✔
265
        }
4✔
266
        None => Err(String::from("Bad remote host syntax!")),
1✔
267
    }
268
}
5✔
269

270
/// Parse remote options for smb protocol
271
#[cfg(smb_unix)]
272
fn parse_smb_remote_opts(s: &str) -> Result<FileTransferParams, String> {
2✔
273
    match REMOTE_SMB_OPT_REGEX.captures(s) {
2✔
274
        Some(groups) => {
2✔
275
            let username: Option<String> = match groups.get(1) {
2✔
276
                Some(group) => Some(group.as_str().to_string()),
1✔
277
                None => Some(whoami::username()),
1✔
278
            };
279
            let address = match groups.get(2) {
2✔
280
                Some(group) => group.as_str().to_string(),
2✔
281
                None => return Err(String::from("Missing address")),
×
282
            };
283
            let port = match groups.get(3) {
2✔
284
                Some(port) => match port.as_str().parse::<u16>() {
1✔
285
                    // Try to parse port
286
                    Ok(p) => p,
1✔
287
                    Err(err) => return Err(format!("Bad port \"{}\": {}", port.as_str(), err)),
×
288
                },
1✔
289
                None => 445,
1✔
290
            };
291
            let share = match groups.get(4) {
2✔
292
                Some(group) => group.as_str().to_string(),
2✔
293
                None => return Err(String::from("Missing address")),
×
294
            };
295
            let entry_directory: Option<PathBuf> =
296
                groups.get(5).map(|group| PathBuf::from(group.as_str()));
3✔
297

298
            Ok(FileTransferParams::new(
4✔
299
                FileTransferProtocol::Smb,
2✔
300
                ProtocolParams::Smb(SmbParams::new(address, port, share).username(username)),
2✔
301
            )
302
            .entry_directory(entry_directory))
2✔
303
        }
2✔
304
        None => Err(String::from("Bad remote host syntax!")),
×
305
    }
306
}
2✔
307

308
#[cfg(windows)]
309
fn parse_smb_remote_opts(s: &str) -> Result<FileTransferParams, String> {
310
    match REMOTE_SMB_OPT_REGEX.captures(s) {
311
        Some(groups) => {
312
            let address = match groups.get(1) {
313
                Some(group) => group.as_str().to_string(),
314
                None => return Err(String::from("Missing address")),
315
            };
316
            let port = match groups.get(2) {
317
                Some(port) => match port.as_str().parse::<u16>() {
318
                    // Try to parse port
319
                    Ok(p) => p,
320
                    Err(err) => return Err(format!("Bad port \"{}\": {}", port.as_str(), err)),
321
                },
322
                None => 445,
323
            };
324
            let share = match groups.get(3) {
325
                Some(group) => group.as_str().to_string(),
326
                None => return Err(String::from("Missing address")),
327
            };
328
            let entry_directory: Option<PathBuf> =
329
                groups.get(4).map(|group| PathBuf::from(group.as_str()));
330

331
            Ok(FileTransferParams::new(
332
                FileTransferProtocol::Smb,
333
                ProtocolParams::Smb(SmbParams::new(address, port, share)),
334
            )
335
            .entry_directory(entry_directory))
336
        }
337
        None => Err(String::from("Bad remote host syntax!")),
338
    }
339
}
340

341
/// Parse semver string
342
pub fn parse_semver(haystack: &str) -> Option<String> {
6✔
343
    match SEMVER_REGEX.captures(haystack) {
6✔
344
        Some(groups) => groups.get(1).map(|version| version.as_str().to_string()),
6✔
345
        None => None,
3✔
346
    }
347
}
6✔
348

349
/// Parse color from string into a `Color` enum.
350
///
351
/// Color may be in different format:
352
///
353
/// 1. color name:
354
///     - Black,
355
///     - Blue,
356
///     - Cyan,
357
///     - DarkGray,
358
///     - Gray,
359
///     - Green,
360
///     - LightBlue,
361
///     - LightCyan,
362
///     - LightGreen,
363
///     - LightMagenta,
364
///     - LightRed,
365
///     - LightYellow,
366
///     - Magenta,
367
///     - Red,
368
///     - Reset,
369
///     - White,
370
///     - Yellow,
371
/// 2. Hex format:
372
///     - #f0ab05
373
///     - #AA33BC
374
/// 3. Rgb format:
375
///     - rgb(255, 64, 32)
376
///     - rgb(255,64,32)
377
///     - 255, 64, 32
378
pub fn parse_color(color: &str) -> Option<Color> {
129✔
379
    tuirealm_parser::parse_color(color)
129✔
380
}
129✔
381

382
#[derive(Debug, PartialEq)]
6✔
383
enum ByteUnit {
384
    Byte,
385
    Kilobyte,
386
    Megabyte,
387
    Gigabyte,
388
    Terabyte,
389
    Petabyte,
390
}
391

392
impl FromStr for ByteUnit {
393
    type Err = &'static str;
394

395
    fn from_str(s: &str) -> Result<Self, Self::Err> {
12✔
396
        match s {
397
            "B" => Ok(Self::Byte),
12✔
398
            "KB" => Ok(Self::Kilobyte),
9✔
399
            "MB" => Ok(Self::Megabyte),
7✔
400
            "GB" => Ok(Self::Gigabyte),
6✔
401
            "TB" => Ok(Self::Terabyte),
4✔
402
            "PB" => Ok(Self::Petabyte),
2✔
403
            _ => Err("Invalid unit"),
1✔
404
        }
405
    }
12✔
406
}
407

408
/// Parse bytes repr (e.g. `24 MB`) into `ByteSize`
409
pub fn parse_bytesize<S: AsRef<str>>(bytes: S) -> Option<ByteSize> {
9✔
410
    match BYTESIZE_REGEX.captures(bytes.as_ref()) {
9✔
411
        None => None,
4✔
412
        Some(groups) => {
5✔
413
            let amount = groups
5✔
414
                .get(1)
415
                .map(|x| x.as_str().parse::<u64>().unwrap_or(0))?;
14✔
416
            let unit = groups.get(4).map(|x| x.as_str().to_string());
8✔
417
            let unit = format!("{}B", unit.unwrap_or_default());
5✔
418
            let unit = ByteUnit::from_str(unit.as_str()).unwrap();
5✔
419
            Some(match unit {
10✔
420
                ByteUnit::Byte => ByteSize::b(amount),
2✔
421
                ByteUnit::Gigabyte => ByteSize::gib(amount),
1✔
422
                ByteUnit::Kilobyte => ByteSize::kib(amount),
1✔
423
                ByteUnit::Megabyte => ByteSize::mib(amount),
×
424
                ByteUnit::Petabyte => ByteSize::pib(amount),
×
425
                ByteUnit::Terabyte => ByteSize::tib(amount),
1✔
426
            })
427
        }
5✔
428
    }
429
}
9✔
430

431
#[cfg(test)]
432
mod tests {
433

434
    use pretty_assertions::assert_eq;
435

436
    use super::*;
437

438
    #[test]
439
    fn test_utils_parse_remote_opt() {
2✔
440
        // Base case
441
        let result: FileTransferParams = parse_remote_opt(&String::from("172.26.104.1"))
1✔
442
            .ok()
443
            .unwrap();
1✔
444
        let params = result.params.generic_params().unwrap();
1✔
445
        assert_eq!(result.protocol, FileTransferProtocol::Sftp);
1✔
446
        assert_eq!(params.address, String::from("172.26.104.1"));
1✔
447
        assert_eq!(params.port, 22);
1✔
448
        assert!(params.username.is_some());
1✔
449
        // User case
450
        let result: FileTransferParams = parse_remote_opt(&String::from("root@172.26.104.1"))
1✔
451
            .ok()
452
            .unwrap();
1✔
453
        let params = result.params.generic_params().unwrap();
1✔
454
        assert_eq!(result.protocol, FileTransferProtocol::Sftp);
1✔
455
        assert_eq!(params.address, String::from("172.26.104.1"));
1✔
456
        assert_eq!(params.port, 22);
1✔
457
        assert_eq!(
1✔
458
            params.username.as_deref().unwrap().to_string(),
1✔
459
            String::from("root")
1✔
460
        );
461
        assert!(result.entry_directory.is_none());
1✔
462
        // User + port
463
        let result: FileTransferParams = parse_remote_opt(&String::from("root@172.26.104.1:8022"))
1✔
464
            .ok()
465
            .unwrap();
1✔
466
        let params = result.params.generic_params().unwrap();
1✔
467
        assert_eq!(params.address, String::from("172.26.104.1"));
1✔
468
        assert_eq!(params.port, 8022);
1✔
469
        assert_eq!(
1✔
470
            params.username.as_deref().unwrap().to_string(),
1✔
471
            String::from("root")
1✔
472
        );
473
        assert_eq!(result.protocol, FileTransferProtocol::Sftp);
1✔
474
        assert!(result.entry_directory.is_none());
1✔
475
        // Port only
476
        let result: FileTransferParams = parse_remote_opt(&String::from("172.26.104.1:4022"))
1✔
477
            .ok()
478
            .unwrap();
1✔
479
        let params = result.params.generic_params().unwrap();
1✔
480
        assert_eq!(result.protocol, FileTransferProtocol::Sftp);
1✔
481
        assert_eq!(params.address, String::from("172.26.104.1"));
1✔
482
        assert_eq!(params.port, 4022);
1✔
483
        assert!(params.username.is_some());
1✔
484
        assert!(result.entry_directory.is_none());
1✔
485
        // Protocol
486
        let result: FileTransferParams = parse_remote_opt(&String::from("ftp://172.26.104.1"))
1✔
487
            .ok()
488
            .unwrap();
1✔
489
        let params = result.params.generic_params().unwrap();
1✔
490
        assert_eq!(result.protocol, FileTransferProtocol::Ftp(false));
1✔
491
        assert_eq!(params.address, String::from("172.26.104.1"));
1✔
492
        assert_eq!(params.port, 21); // Fallback to ftp default
1✔
493
        assert!(params.username.is_none()); // Doesn't fall back
1✔
494
        assert!(result.entry_directory.is_none());
1✔
495
        // Protocol
496
        let result: FileTransferParams = parse_remote_opt(&String::from("sftp://172.26.104.1"))
1✔
497
            .ok()
498
            .unwrap();
1✔
499
        let params = result.params.generic_params().unwrap();
1✔
500
        assert_eq!(result.protocol, FileTransferProtocol::Sftp);
1✔
501
        assert_eq!(params.address, String::from("172.26.104.1"));
1✔
502
        assert_eq!(params.port, 22); // Fallback to sftp default
1✔
503
        assert!(params.username.is_some()); // Doesn't fall back
1✔
504
        assert!(result.entry_directory.is_none());
1✔
505
        let result: FileTransferParams = parse_remote_opt(&String::from("scp://172.26.104.1"))
1✔
506
            .ok()
507
            .unwrap();
1✔
508
        let params = result.params.generic_params().unwrap();
1✔
509
        assert_eq!(result.protocol, FileTransferProtocol::Scp);
1✔
510
        assert_eq!(params.address, String::from("172.26.104.1"));
1✔
511
        assert_eq!(params.port, 22); // Fallback to scp default
1✔
512
        assert!(params.username.is_some()); // Doesn't fall back
1✔
513
        assert!(result.entry_directory.is_none());
1✔
514
        // Protocol + user
515
        let result: FileTransferParams =
516
            parse_remote_opt(&String::from("ftps://anon@172.26.104.1"))
1✔
517
                .ok()
518
                .unwrap();
1✔
519
        let params = result.params.generic_params().unwrap();
1✔
520
        assert_eq!(result.protocol, FileTransferProtocol::Ftp(true));
1✔
521
        assert_eq!(params.address, String::from("172.26.104.1"));
1✔
522
        assert_eq!(params.port, 21); // Fallback to ftp default
1✔
523
        assert_eq!(
1✔
524
            params.username.as_deref().unwrap().to_string(),
1✔
525
            String::from("anon")
1✔
526
        );
527
        assert!(result.entry_directory.is_none());
1✔
528
        // Path
529
        let result: FileTransferParams =
530
            parse_remote_opt(&String::from("root@172.26.104.1:8022:/var"))
1✔
531
                .ok()
532
                .unwrap();
1✔
533
        let params = result.params.generic_params().unwrap();
1✔
534
        assert_eq!(result.protocol, FileTransferProtocol::Sftp);
1✔
535
        assert_eq!(params.address, String::from("172.26.104.1"));
1✔
536
        assert_eq!(params.port, 8022);
1✔
537
        assert_eq!(
1✔
538
            params.username.as_deref().unwrap().to_string(),
1✔
539
            String::from("root")
1✔
540
        );
541
        assert_eq!(result.entry_directory.unwrap(), PathBuf::from("/var"));
1✔
542
        // Port only
543
        let result: FileTransferParams = parse_remote_opt(&String::from("172.26.104.1:home"))
1✔
544
            .ok()
545
            .unwrap();
1✔
546
        let params = result.params.generic_params().unwrap();
1✔
547
        assert_eq!(result.protocol, FileTransferProtocol::Sftp);
1✔
548
        assert_eq!(params.address, String::from("172.26.104.1"));
1✔
549
        assert_eq!(params.port, 22);
1✔
550
        assert!(params.username.is_some());
1✔
551
        assert_eq!(result.entry_directory.unwrap(), PathBuf::from("home"));
1✔
552
        // All together now
553
        let result: FileTransferParams =
554
            parse_remote_opt(&String::from("ftp://anon@172.26.104.1:8021:/tmp"))
1✔
555
                .ok()
556
                .unwrap();
1✔
557
        let params = result.params.generic_params().unwrap();
1✔
558
        assert_eq!(result.protocol, FileTransferProtocol::Ftp(false));
1✔
559
        assert_eq!(params.address, String::from("172.26.104.1"));
1✔
560
        assert_eq!(params.port, 8021); // Fallback to ftp default
1✔
561
        assert_eq!(
1✔
562
            params.username.as_deref().unwrap().to_string(),
1✔
563
            String::from("anon")
1✔
564
        );
565
        assert_eq!(result.entry_directory.unwrap(), PathBuf::from("/tmp"));
1✔
566
        // bad syntax
567
        // Bad protocol
568
        assert!(parse_remote_opt(&String::from("omar://172.26.104.1")).is_err());
1✔
569
        // Bad port
570
        assert!(parse_remote_opt(&String::from("scp://172.26.104.1:650000")).is_err());
1✔
571
    }
2✔
572

573
    #[test]
574
    fn parse_aws_s3_opt() {
2✔
575
        // Simple
576
        let result: FileTransferParams =
577
            parse_remote_opt(&String::from("s3://mybucket@eu-central-1"))
1✔
578
                .ok()
579
                .unwrap();
1✔
580
        let params = result.params.s3_params().unwrap();
1✔
581
        assert_eq!(result.protocol, FileTransferProtocol::AwsS3);
1✔
582
        assert_eq!(result.entry_directory, None);
1✔
583
        assert_eq!(params.bucket_name.as_str(), "mybucket");
1✔
584
        assert_eq!(params.region.as_deref().unwrap(), "eu-central-1");
1✔
585
        assert_eq!(params.profile, None);
1✔
586
        // With profile
587
        let result: FileTransferParams =
588
            parse_remote_opt(&String::from("s3://mybucket@eu-central-1:default"))
1✔
589
                .ok()
590
                .unwrap();
1✔
591
        let params = result.params.s3_params().unwrap();
1✔
592
        assert_eq!(result.protocol, FileTransferProtocol::AwsS3);
1✔
593
        assert_eq!(result.entry_directory, None);
1✔
594
        assert_eq!(params.bucket_name.as_str(), "mybucket");
1✔
595
        assert_eq!(params.region.as_deref().unwrap(), "eu-central-1");
1✔
596
        assert_eq!(params.profile.as_deref(), Some("default"));
1✔
597
        // With wrkdir only
598
        let result: FileTransferParams =
599
            parse_remote_opt(&String::from("s3://mybucket@eu-central-1:/foobar"))
1✔
600
                .ok()
601
                .unwrap();
1✔
602
        let params = result.params.s3_params().unwrap();
1✔
603
        assert_eq!(result.protocol, FileTransferProtocol::AwsS3);
1✔
604
        assert_eq!(result.entry_directory, Some(PathBuf::from("/foobar")));
1✔
605
        assert_eq!(params.bucket_name.as_str(), "mybucket");
1✔
606
        assert_eq!(params.region.as_deref().unwrap(), "eu-central-1");
1✔
607
        assert_eq!(params.profile, None);
1✔
608
        // With all arguments
609
        let result: FileTransferParams =
610
            parse_remote_opt(&String::from("s3://mybucket@eu-central-1:default:/foobar"))
1✔
611
                .ok()
612
                .unwrap();
1✔
613
        let params = result.params.s3_params().unwrap();
1✔
614
        assert_eq!(result.protocol, FileTransferProtocol::AwsS3);
1✔
615
        assert_eq!(result.entry_directory, Some(PathBuf::from("/foobar")));
1✔
616
        assert_eq!(params.bucket_name.as_str(), "mybucket");
1✔
617
        assert_eq!(params.region.as_deref().unwrap(), "eu-central-1");
1✔
618
        assert_eq!(params.profile.as_deref(), Some("default"));
1✔
619
        // -- bad args
620
        assert!(parse_remote_opt(&String::from("s3://mybucket:default:/foobar")).is_err());
1✔
621
    }
2✔
622

623
    #[test]
624
    #[cfg(smb_unix)]
625
    fn should_parse_smb_address() {
2✔
626
        let result = parse_remote_opt("smb://myserver/myshare").ok().unwrap();
1✔
627
        let params = result.params.smb_params().unwrap();
1✔
628

629
        assert_eq!(params.address.as_str(), "myserver");
1✔
630
        assert_eq!(params.port, 445);
1✔
631
        assert_eq!(params.share.as_str(), "myshare");
1✔
632
        assert!(params.username.is_some());
1✔
633
        assert!(params.password.is_none());
1✔
634
        assert!(params.workgroup.is_none());
1✔
635
        assert!(result.entry_directory.is_none());
1✔
636
    }
2✔
637

638
    #[test]
639
    #[cfg(smb_unix)]
640
    fn should_parse_smb_address_with_opts() {
2✔
641
        let result = parse_remote_opt("smb://omar@myserver:4445/myshare/dir/subdir")
1✔
642
            .ok()
643
            .unwrap();
644
        let params = result.params.smb_params().unwrap();
1✔
645

646
        assert_eq!(params.address.as_str(), "myserver");
1✔
647
        assert_eq!(params.port, 4445);
1✔
648
        assert_eq!(params.username.as_deref().unwrap(), "omar");
1✔
649
        assert!(params.password.is_none());
1✔
650
        assert!(params.workgroup.is_none());
1✔
651
        assert_eq!(params.share.as_str(), "myshare");
1✔
652
        assert_eq!(
1✔
653
            result.entry_directory.as_deref().unwrap(),
1✔
654
            std::path::Path::new("/dir/subdir")
1✔
655
        );
656
    }
2✔
657

658
    #[test]
659
    #[cfg(windows)]
660
    fn should_parse_smb_address() {
661
        let result = parse_remote_opt(&String::from("\\\\myserver\\myshare"))
662
            .ok()
663
            .unwrap();
664
        let params = result.params.smb_params().unwrap();
665

666
        assert_eq!(params.address.as_str(), "myserver");
667
        assert_eq!(params.port, 445);
668
        assert_eq!(params.share.as_str(), "myshare");
669
        assert!(result.entry_directory.is_none());
670
    }
671

672
    #[test]
673
    #[cfg(windows)]
674
    fn should_parse_smb_address_with_opts() {
675
        let result = parse_remote_opt(&String::from("\\\\myserver:3445\\myshare\\path"))
676
            .ok()
677
            .unwrap();
678
        let params = result.params.smb_params().unwrap();
679

680
        assert_eq!(params.address.as_str(), "myserver");
681
        assert_eq!(params.port, 3445);
682
        assert_eq!(params.share.as_str(), "myshare");
683
        assert_eq!(
684
            result.entry_directory.as_deref().unwrap(),
685
            std::path::Path::new("\\path")
686
        );
687
    }
688

689
    #[test]
690
    fn test_utils_parse_semver() {
2✔
691
        assert_eq!(
1✔
692
            parse_semver("termscp-0.3.2").unwrap(),
1✔
693
            String::from("0.3.2")
1✔
694
        );
695
        assert_eq!(parse_semver("v0.4.1").unwrap(), String::from("0.4.1"),);
1✔
696
        assert_eq!(parse_semver("1.0.0").unwrap(), String::from("1.0.0"),);
1✔
697
        assert!(parse_semver("v1.1").is_none());
1✔
698
    }
2✔
699

700
    #[test]
701
    fn test_utils_parse_color() {
2✔
702
        assert_eq!(parse_color("Black").unwrap(), Color::Black);
1✔
703
        assert_eq!(parse_color("#f0f0f0").unwrap(), Color::Rgb(240, 240, 240));
1✔
704
        // -- css colors
705
        assert_eq!(parse_color("aliceblue"), Some(Color::Rgb(240, 248, 255)));
1✔
706
        // -- hex and rgb
707
        assert_eq!(
1✔
708
            parse_color("rgb(255, 64, 32)").unwrap(),
1✔
709
            Color::Rgb(255, 64, 32)
710
        );
711
        // bad
712
        assert!(parse_color("redd").is_none());
1✔
713
    }
2✔
714

715
    #[test]
716
    fn parse_byteunit() {
2✔
717
        assert_eq!(ByteUnit::from_str("B").ok().unwrap(), ByteUnit::Byte);
1✔
718
        assert_eq!(ByteUnit::from_str("KB").ok().unwrap(), ByteUnit::Kilobyte);
1✔
719
        assert_eq!(ByteUnit::from_str("MB").ok().unwrap(), ByteUnit::Megabyte);
1✔
720
        assert_eq!(ByteUnit::from_str("GB").ok().unwrap(), ByteUnit::Gigabyte);
1✔
721
        assert_eq!(ByteUnit::from_str("TB").ok().unwrap(), ByteUnit::Terabyte);
1✔
722
        assert_eq!(ByteUnit::from_str("PB").ok().unwrap(), ByteUnit::Petabyte);
1✔
723
        assert!(ByteUnit::from_str("uB").is_err());
1✔
724
    }
2✔
725

726
    #[test]
727
    fn parse_str_as_bytesize() {
2✔
728
        assert_eq!(parse_bytesize("1024 B").unwrap().as_u64(), 1024);
1✔
729
        assert_eq!(parse_bytesize("1024B").unwrap().as_u64(), 1024);
1✔
730
        assert_eq!(parse_bytesize("10240 KB").unwrap().as_u64(), 10485760);
1✔
731
        assert_eq!(parse_bytesize("2 GB").unwrap().as_u64(), 2147483648);
1✔
732
        assert_eq!(parse_bytesize("1 TB").unwrap().as_u64(), 1099511627776);
1✔
733
        assert!(parse_bytesize("1 XB").is_none());
1✔
734
        assert!(parse_bytesize("1 GB aaaaa").is_none());
1✔
735
        assert!(parse_bytesize("1 GBaaaaa").is_none());
1✔
736
        assert!(parse_bytesize("1MBaaaaa").is_none());
1✔
737
    }
2✔
738
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc