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

veeso / termscp / 4967029585

pending completion
4967029585

push

github

GitHub
SMB support (#184)

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

5226 of 5606 relevant lines covered (93.22%)

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

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

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

95
// -- remote opts
96

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

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

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

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

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

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

307
#[cfg(windows)]
308
fn parse_smb_remote_opts(s: &str) -> Result<FileTransferParams, String> {
309
    match REMOTE_SMB_OPT_REGEX.captures(s) {
310
        Some(groups) => {
311
            let username = groups.get(1).map(|x| x.as_str().to_string());
312
            let address = match groups.get(2) {
313
                Some(group) => group.as_str().to_string(),
314
                None => return Err(String::from("Missing address")),
315
            };
316
            let share = match groups.get(3) {
317
                Some(group) => group.as_str().to_string(),
318
                None => return Err(String::from("Missing address")),
319
            };
320
            let entry_directory: Option<PathBuf> =
321
                groups.get(4).map(|group| PathBuf::from(group.as_str()));
322

323
            Ok(FileTransferParams::new(
324
                FileTransferProtocol::Smb,
325
                ProtocolParams::Smb(SmbParams::new(address, share).username(username)),
326
            )
327
            .entry_directory(entry_directory))
328
        }
329
        None => Err(String::from("Bad remote host syntax!")),
330
    }
331
}
332

333
/// Parse semver string
334
pub fn parse_semver(haystack: &str) -> Option<String> {
6✔
335
    match SEMVER_REGEX.captures(haystack) {
6✔
336
        Some(groups) => groups.get(1).map(|version| version.as_str().to_string()),
6✔
337
        None => None,
3✔
338
    }
339
}
6✔
340

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

374
#[derive(Debug, PartialEq)]
6✔
375
enum ByteUnit {
376
    Byte,
377
    Kilobyte,
378
    Megabyte,
379
    Gigabyte,
380
    Terabyte,
381
    Petabyte,
382
}
383

384
impl FromStr for ByteUnit {
385
    type Err = &'static str;
386

387
    fn from_str(s: &str) -> Result<Self, Self::Err> {
12✔
388
        match s {
389
            "B" => Ok(Self::Byte),
12✔
390
            "KB" => Ok(Self::Kilobyte),
9✔
391
            "MB" => Ok(Self::Megabyte),
7✔
392
            "GB" => Ok(Self::Gigabyte),
6✔
393
            "TB" => Ok(Self::Terabyte),
4✔
394
            "PB" => Ok(Self::Petabyte),
2✔
395
            _ => Err("Invalid unit"),
1✔
396
        }
397
    }
12✔
398
}
399

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

423
#[cfg(test)]
424
mod tests {
425

426
    use pretty_assertions::assert_eq;
427

428
    use super::*;
429

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

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

615
    #[test]
616
    #[cfg(smb_unix)]
617
    fn should_parse_smb_address() {
2✔
618
        let result = parse_remote_opt("smb://myserver/myshare").ok().unwrap();
1✔
619
        let params = result.params.smb_params().unwrap();
1✔
620

621
        assert_eq!(params.address.as_str(), "myserver");
1✔
622
        assert_eq!(params.port, 445);
1✔
623
        assert_eq!(params.share.as_str(), "myshare");
1✔
624
        assert!(params.username.is_some());
1✔
625
        assert!(params.password.is_none());
1✔
626
        assert!(params.workgroup.is_none());
1✔
627
        assert!(result.entry_directory.is_none());
1✔
628
    }
2✔
629

630
    #[test]
631
    #[cfg(smb_unix)]
632
    fn should_parse_smb_address_with_opts() {
2✔
633
        let result = parse_remote_opt("smb://omar@myserver:4445/myshare/dir/subdir")
1✔
634
            .ok()
635
            .unwrap();
636
        let params = result.params.smb_params().unwrap();
1✔
637

638
        assert_eq!(params.address.as_str(), "myserver");
1✔
639
        assert_eq!(params.port, 4445);
1✔
640
        assert_eq!(params.username.as_deref().unwrap(), "omar");
1✔
641
        assert!(params.password.is_none());
1✔
642
        assert!(params.workgroup.is_none());
1✔
643
        assert_eq!(params.share.as_str(), "myshare");
1✔
644
        assert_eq!(
1✔
645
            result.entry_directory.as_deref().unwrap(),
1✔
646
            std::path::Path::new("/dir/subdir")
1✔
647
        );
648
    }
2✔
649

650
    #[test]
651
    #[cfg(windows)]
652
    fn should_parse_smb_address() {
653
        let result = parse_remote_opt(&String::from("\\\\myserver\\myshare"))
654
            .ok()
655
            .unwrap();
656
        let params = result.params.smb_params().unwrap();
657

658
        assert_eq!(params.address.as_str(), "myserver");
659
        assert_eq!(params.share.as_str(), "myshare");
660
        assert!(result.entry_directory.is_none());
661
    }
662

663
    #[test]
664
    #[cfg(windows)]
665
    fn should_parse_smb_address_with_opts() {
666
        let result = parse_remote_opt(&String::from("\\\\omar@myserver\\myshare\\path"))
667
            .ok()
668
            .unwrap();
669
        let params = result.params.smb_params().unwrap();
670

671
        assert_eq!(params.address.as_str(), "myserver");
672
        assert_eq!(params.share.as_str(), "myshare");
673
        assert_eq!(params.username.as_deref().unwrap(), "omar");
674
        assert_eq!(
675
            result.entry_directory.as_deref().unwrap(),
676
            std::path::Path::new("\\path")
677
        );
678
    }
679

680
    #[test]
681
    fn test_utils_parse_semver() {
2✔
682
        assert_eq!(
1✔
683
            parse_semver("termscp-0.3.2").unwrap(),
1✔
684
            String::from("0.3.2")
1✔
685
        );
686
        assert_eq!(parse_semver("v0.4.1").unwrap(), String::from("0.4.1"),);
1✔
687
        assert_eq!(parse_semver("1.0.0").unwrap(), String::from("1.0.0"),);
1✔
688
        assert!(parse_semver("v1.1").is_none());
1✔
689
    }
2✔
690

691
    #[test]
692
    fn test_utils_parse_color() {
2✔
693
        assert_eq!(parse_color("Black").unwrap(), Color::Black);
1✔
694
        assert_eq!(parse_color("#f0f0f0").unwrap(), Color::Rgb(240, 240, 240));
1✔
695
        // -- css colors
696
        assert_eq!(parse_color("aliceblue"), Some(Color::Rgb(240, 248, 255)));
1✔
697
        // -- hex and rgb
698
        assert_eq!(
1✔
699
            parse_color("rgb(255, 64, 32)").unwrap(),
1✔
700
            Color::Rgb(255, 64, 32)
701
        );
702
        // bad
703
        assert!(parse_color("redd").is_none());
1✔
704
    }
2✔
705

706
    #[test]
707
    fn parse_byteunit() {
2✔
708
        assert_eq!(ByteUnit::from_str("B").ok().unwrap(), ByteUnit::Byte);
1✔
709
        assert_eq!(ByteUnit::from_str("KB").ok().unwrap(), ByteUnit::Kilobyte);
1✔
710
        assert_eq!(ByteUnit::from_str("MB").ok().unwrap(), ByteUnit::Megabyte);
1✔
711
        assert_eq!(ByteUnit::from_str("GB").ok().unwrap(), ByteUnit::Gigabyte);
1✔
712
        assert_eq!(ByteUnit::from_str("TB").ok().unwrap(), ByteUnit::Terabyte);
1✔
713
        assert_eq!(ByteUnit::from_str("PB").ok().unwrap(), ByteUnit::Petabyte);
1✔
714
        assert!(ByteUnit::from_str("uB").is_err());
1✔
715
    }
2✔
716

717
    #[test]
718
    fn parse_str_as_bytesize() {
2✔
719
        assert_eq!(parse_bytesize("1024 B").unwrap().as_u64(), 1024);
1✔
720
        assert_eq!(parse_bytesize("1024B").unwrap().as_u64(), 1024);
1✔
721
        assert_eq!(parse_bytesize("10240 KB").unwrap().as_u64(), 10485760);
1✔
722
        assert_eq!(parse_bytesize("2 GB").unwrap().as_u64(), 2147483648);
1✔
723
        assert_eq!(parse_bytesize("1 TB").unwrap().as_u64(), 1099511627776);
1✔
724
        assert!(parse_bytesize("1 XB").is_none());
1✔
725
        assert!(parse_bytesize("1 GB aaaaa").is_none());
1✔
726
        assert!(parse_bytesize("1 GBaaaaa").is_none());
1✔
727
        assert!(parse_bytesize("1MBaaaaa").is_none());
1✔
728
    }
2✔
729
}
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