Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/rust/src/files.rs
2885 views
1
// Licensed to the Software Freedom Conservancy (SFC) under one
2
// or more contributor license agreements. See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership. The SFC licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License. You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied. See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
use crate::config::OS;
19
use crate::config::OS::WINDOWS;
20
use crate::{
21
format_one_arg, format_three_args, run_shell_command_by_os, Command, Logger, CP_VOLUME_COMMAND,
22
HDIUTIL_ATTACH_COMMAND, HDIUTIL_DETACH_COMMAND, MACOS, MSIEXEC_INSTALL_COMMAND,
23
};
24
use anyhow::anyhow;
25
use anyhow::Error;
26
use apple_flat_package::PkgReader;
27
use bzip2::read::BzDecoder;
28
use directories::BaseDirs;
29
use flate2::read::GzDecoder;
30
use fs_extra::dir::{move_dir, CopyOptions};
31
use regex::Regex;
32
#[cfg(windows)]
33
use std::ffi::OsStr;
34
use std::fs;
35
use std::fs::File;
36
use std::io;
37
use std::io::{BufReader, Cursor, Read};
38
#[cfg(windows)]
39
use std::os::windows::ffi::OsStrExt;
40
use std::path::{Path, PathBuf};
41
#[cfg(windows)]
42
use std::ptr;
43
use tar::Archive;
44
use walkdir::{DirEntry, WalkDir};
45
#[cfg(windows)]
46
use winapi::shared::minwindef::LPVOID;
47
#[cfg(windows)]
48
use winapi::um::winver::{GetFileVersionInfoSizeW, GetFileVersionInfoW, VerQueryValueW};
49
use xz2::read::XzDecoder;
50
use zip::ZipArchive;
51
52
pub const PARSE_ERROR: &str = "Wrong browser/driver version";
53
const CACHE_FOLDER: &str = ".cache/selenium";
54
const ZIP: &str = "zip";
55
const GZ: &str = "gz";
56
const XML: &str = "xml";
57
const HTML: &str = "html";
58
const BZ2: &str = "bz2";
59
const PKG: &str = "pkg";
60
const DMG: &str = "dmg";
61
const EXE: &str = "exe";
62
const DEB: &str = "deb";
63
const MSI: &str = "msi";
64
const XZ: &str = "xz";
65
const SEVEN_ZIP_HEADER: &[u8; 6] = b"7z\xBC\xAF\x27\x1C";
66
const UNCOMPRESS_MACOS_ERR_MSG: &str = "{} files are only supported in macOS";
67
68
#[derive(Hash, Eq, PartialEq, Debug)]
69
pub struct BrowserPath {
70
os: OS,
71
channel: String,
72
}
73
74
impl BrowserPath {
75
pub fn new(os: OS, channel: &str) -> BrowserPath {
76
BrowserPath {
77
os,
78
channel: channel.to_string(),
79
}
80
}
81
}
82
83
pub fn create_parent_path_if_not_exists(path: &Path) -> Result<(), Error> {
84
if let Some(p) = path.parent() {
85
create_path_if_not_exists(p)?;
86
}
87
Ok(())
88
}
89
90
pub fn create_path_if_not_exists(path: &Path) -> Result<(), Error> {
91
if !path.exists() {
92
fs::create_dir_all(path)?;
93
}
94
Ok(())
95
}
96
97
pub fn uncompress(
98
compressed_file: &str,
99
target: &Path,
100
log: &Logger,
101
os: &str,
102
single_file: Option<String>,
103
volume: Option<&str>,
104
) -> Result<(), Error> {
105
let mut extension = match infer::get_from_path(compressed_file)? {
106
Some(kind) => kind.extension(),
107
_ => {
108
if compressed_file.ends_with(PKG) || compressed_file.ends_with(DMG) {
109
if MACOS.is(os) {
110
PKG
111
} else {
112
return Err(anyhow!(format_one_arg(UNCOMPRESS_MACOS_ERR_MSG, PKG)));
113
}
114
} else {
115
return Err(anyhow!(format!(
116
"Format for file {} cannot be inferred",
117
compressed_file
118
)));
119
}
120
}
121
};
122
if compressed_file.ends_with(DMG) {
123
if MACOS.is(os) {
124
extension = DMG;
125
} else {
126
return Err(anyhow!(format_one_arg(UNCOMPRESS_MACOS_ERR_MSG, DMG)));
127
}
128
}
129
log.trace(format!(
130
"The detected extension of the compressed file is {}",
131
extension
132
));
133
134
if extension.eq_ignore_ascii_case(ZIP) {
135
unzip(compressed_file, target, log, single_file)?
136
} else if extension.eq_ignore_ascii_case(GZ) {
137
untargz(compressed_file, target, log)?
138
} else if extension.eq_ignore_ascii_case(BZ2) {
139
uncompress_tar(
140
&mut BzDecoder::new(File::open(compressed_file)?),
141
target,
142
log,
143
)?
144
} else if extension.eq_ignore_ascii_case(XZ) {
145
uncompress_tar(
146
&mut XzDecoder::new(File::open(compressed_file)?),
147
target,
148
log,
149
)?
150
} else if extension.eq_ignore_ascii_case(PKG) {
151
uncompress_pkg(compressed_file, target, log)?
152
} else if extension.eq_ignore_ascii_case(DMG) {
153
uncompress_dmg(compressed_file, target, log, os, volume.unwrap_or_default())?
154
} else if extension.eq_ignore_ascii_case(EXE) {
155
uncompress_sfx(compressed_file, target, log)?
156
} else if extension.eq_ignore_ascii_case(DEB) {
157
uncompress_deb(compressed_file, target, log, volume.unwrap_or_default())?
158
} else if extension.eq_ignore_ascii_case(MSI) {
159
install_msi(compressed_file, log, os)?
160
} else if extension.eq_ignore_ascii_case(XML) || extension.eq_ignore_ascii_case(HTML) {
161
log.debug(format!(
162
"Wrong downloaded driver: {}",
163
fs::read_to_string(compressed_file).unwrap_or_default()
164
));
165
return Err(anyhow!(PARSE_ERROR));
166
} else {
167
return Err(anyhow!(format!(
168
"Downloaded file cannot be uncompressed ({} extension)",
169
extension
170
)));
171
}
172
173
Ok(())
174
}
175
176
pub fn uncompress_sfx(compressed_file: &str, target: &Path, log: &Logger) -> Result<(), Error> {
177
let zip_parent = Path::new(compressed_file).parent().unwrap();
178
log.trace(format!(
179
"Decompressing {} to {}",
180
compressed_file,
181
zip_parent.display()
182
));
183
184
let file_bytes = read_bytes_from_file(compressed_file)?;
185
let header = find_bytes(&file_bytes, SEVEN_ZIP_HEADER);
186
let index_7z = header.ok_or(anyhow!("Incorrect SFX (self extracting exe) file"))?;
187
let file_reader = Cursor::new(&file_bytes[index_7z..]);
188
sevenz_rust::decompress(file_reader, zip_parent).unwrap();
189
190
let zip_parent_str = path_to_string(zip_parent);
191
let core_str = format!(r"{}\core", zip_parent_str);
192
move_folder_content(&core_str, target, log)?;
193
194
Ok(())
195
}
196
197
pub fn move_folder_content(source: &str, target: &Path, log: &Logger) -> Result<(), Error> {
198
log.trace(format!(
199
"Moving files and folders from {} to {}",
200
source,
201
target.display()
202
));
203
create_parent_path_if_not_exists(target)?;
204
let mut options = CopyOptions::new();
205
options.content_only = true;
206
options.skip_exist = true;
207
move_dir(source, target, &options)?;
208
Ok(())
209
}
210
211
pub fn uncompress_pkg(compressed_file: &str, target: &Path, log: &Logger) -> Result<(), Error> {
212
let target_path = Path::new(target);
213
let mut reader = PkgReader::new(File::open(compressed_file)?)?;
214
let packages = reader.component_packages()?;
215
let package = packages.first().ok_or(anyhow!("Unable to extract PKG"))?;
216
if let Some(mut cpio_reader) = package.payload_reader()? {
217
while let Some(next) = cpio_reader.next() {
218
let entry = next?;
219
let name = entry.name();
220
let mut file = Vec::new();
221
cpio_reader.read_to_end(&mut file)?;
222
let target_path_buf = target_path.join(name);
223
log.trace(format!("Extracting {}", target_path_buf.display()));
224
if entry.file_size() != 0 {
225
let target_path = target_path_buf.as_path();
226
fs::create_dir_all(target_path.parent().unwrap())?;
227
fs::write(&target_path_buf, file)?;
228
229
// Set permissions
230
#[cfg(unix)]
231
{
232
use std::os::unix::fs::PermissionsExt;
233
234
let mode = entry.mode();
235
fs::set_permissions(target_path, fs::Permissions::from_mode(mode))?;
236
}
237
}
238
}
239
}
240
Ok(())
241
}
242
243
pub fn uncompress_dmg(
244
compressed_file: &str,
245
target: &Path,
246
log: &Logger,
247
os: &str,
248
volume: &str,
249
) -> Result<(), Error> {
250
let dmg_file_name = Path::new(compressed_file)
251
.file_name()
252
.unwrap_or_default()
253
.to_os_string();
254
log.debug(format!(
255
"Mounting {} and copying content to cache",
256
dmg_file_name.to_str().unwrap_or_default()
257
));
258
let mut command = Command::new_single(format_one_arg(HDIUTIL_ATTACH_COMMAND, compressed_file));
259
log.trace(format!("Running command: {}", command.display()));
260
run_shell_command_by_os(os, command)?;
261
262
fs::create_dir_all(target)?;
263
let target_folder = path_to_string(target);
264
command = Command::new_single(format_three_args(
265
CP_VOLUME_COMMAND,
266
volume,
267
volume,
268
&target_folder,
269
));
270
log.trace(format!("Running command: {}", command.display()));
271
run_shell_command_by_os(os, command)?;
272
273
command = Command::new_single(format_one_arg(HDIUTIL_DETACH_COMMAND, volume));
274
log.trace(format!("Running command: {}", command.display()));
275
run_shell_command_by_os(os, command)?;
276
277
Ok(())
278
}
279
280
pub fn uncompress_deb(
281
compressed_file: &str,
282
target: &Path,
283
log: &Logger,
284
label: &str,
285
) -> Result<(), Error> {
286
let zip_parent = Path::new(compressed_file).parent().unwrap();
287
log.trace(format!(
288
"Extracting from {} to {}",
289
compressed_file,
290
zip_parent.display()
291
));
292
293
let deb_file = File::open(compressed_file)?;
294
let mut deb_pkg = debpkg::DebPkg::parse(deb_file)?;
295
deb_pkg.data()?.unpack(zip_parent)?;
296
297
let zip_parent_str = path_to_string(zip_parent);
298
let opt_edge_str = format!("{}/opt/microsoft/{}", zip_parent_str, label);
299
300
// Exception due to bad symbolic link in unstable distributions. For example:
301
// microsoft-edge -> /opt/microsoft/msedge-beta/microsoft-edge-beta
302
if !label.eq("msedge") {
303
let link = format!("{}/microsoft-edge", opt_edge_str);
304
fs::remove_file(Path::new(&link)).unwrap_or_default();
305
}
306
307
move_folder_content(&opt_edge_str, target, log)?;
308
309
Ok(())
310
}
311
312
pub fn install_msi(msi_file: &str, log: &Logger, os: &str) -> Result<(), Error> {
313
let msi_file_name = Path::new(msi_file)
314
.file_name()
315
.unwrap_or_default()
316
.to_os_string();
317
log.debug(format!(
318
"Installing {}",
319
msi_file_name.to_str().unwrap_or_default()
320
));
321
322
let command = Command::new_single(format_one_arg(MSIEXEC_INSTALL_COMMAND, msi_file));
323
log.trace(format!("Running command: {}", command.display()));
324
run_shell_command_by_os(os, command)?;
325
326
Ok(())
327
}
328
329
pub fn untargz(compressed_file: &str, target: &Path, log: &Logger) -> Result<(), Error> {
330
log.trace(format!(
331
"Untargz {} to {}",
332
compressed_file,
333
target.display()
334
));
335
let file = File::open(compressed_file)?;
336
let tar = GzDecoder::new(&file);
337
let mut archive = Archive::new(tar);
338
let parent_path = target
339
.parent()
340
.ok_or(anyhow!(format!("Error getting parent of {:?}", file)))?;
341
if !target.exists() {
342
archive.unpack(parent_path)?;
343
}
344
Ok(())
345
}
346
347
pub fn uncompress_tar(decoder: &mut dyn Read, target: &Path, log: &Logger) -> Result<(), Error> {
348
log.trace(format!(
349
"Uncompress compressed tarball to {}",
350
target.display()
351
));
352
let mut buffer: Vec<u8> = Vec::new();
353
decoder.read_to_end(&mut buffer)?;
354
let mut archive = Archive::new(Cursor::new(buffer));
355
for entry in archive.entries()? {
356
let mut entry_decoder = entry?;
357
let entry_path: PathBuf = entry_decoder.path()?.iter().skip(1).collect();
358
let entry_target = target.join(entry_path);
359
fs::create_dir_all(entry_target.parent().unwrap())?;
360
entry_decoder.unpack(entry_target)?;
361
}
362
Ok(())
363
}
364
365
pub fn unzip(
366
compressed_file: &str,
367
target: &Path,
368
log: &Logger,
369
single_file: Option<String>,
370
) -> Result<(), Error> {
371
let file = File::open(compressed_file)?;
372
let compressed_path = Path::new(compressed_file);
373
let tmp_path = compressed_path
374
.parent()
375
.unwrap_or(compressed_path)
376
.to_path_buf();
377
let final_path = if single_file.is_some() {
378
target.parent().unwrap_or(target).to_path_buf()
379
} else {
380
target.to_path_buf()
381
};
382
log.trace(format!(
383
"Unzipping {} to {}",
384
compressed_file,
385
final_path.display()
386
));
387
let mut zip_archive = ZipArchive::new(file)?;
388
let mut unzipped_files = 0;
389
390
for i in 0..zip_archive.len() {
391
let mut file = zip_archive.by_index(i)?;
392
let path: PathBuf = match file.enclosed_name() {
393
// This logic is required since some zip files (e.g. chromedriver 115+)
394
// are zipped with a parent folder, while others (e.g. chromedriver 114-)
395
// are zipped without a parent folder
396
Some(p) => {
397
let iter = p.iter();
398
if iter.to_owned().count() > 1 {
399
iter.skip(1).collect()
400
} else {
401
iter.collect()
402
}
403
}
404
None => continue,
405
};
406
if file.name().ends_with('/') {
407
log.trace(format!("File extracted to {}", tmp_path.display()));
408
fs::create_dir_all(&tmp_path)?;
409
} else {
410
let target_path = tmp_path.join(path.clone());
411
create_parent_path_if_not_exists(target_path.as_path())?;
412
let mut outfile = File::create(&target_path)?;
413
414
// Set permissions in Unix-like systems
415
#[cfg(unix)]
416
{
417
use std::os::unix::fs::PermissionsExt;
418
419
if single_file.is_some() {
420
fs::set_permissions(&target_path, fs::Permissions::from_mode(0o755))?;
421
} else if let Some(mode) = file.unix_mode() {
422
fs::set_permissions(&target_path, fs::Permissions::from_mode(mode))?;
423
}
424
}
425
426
io::copy(&mut file, &mut outfile)?;
427
unzipped_files += 1;
428
log.trace(format!(
429
"File extracted to {} ({} bytes)",
430
target_path.display(),
431
file.size()
432
));
433
}
434
}
435
if unzipped_files == 0 {
436
return Err(anyhow!(format!(
437
"Problem uncompressing zip ({} files extracted)",
438
unzipped_files
439
)));
440
}
441
442
fs::remove_file(compressed_path)?;
443
copy_folder_content(
444
tmp_path,
445
final_path,
446
single_file,
447
&compressed_path.to_path_buf(),
448
log,
449
)?;
450
451
Ok(())
452
}
453
454
pub fn copy_folder_content(
455
source: impl AsRef<Path>,
456
destination: impl AsRef<Path>,
457
single_file: Option<String>,
458
avoid_path: &PathBuf,
459
log: &Logger,
460
) -> io::Result<()> {
461
fs::create_dir_all(&destination)?;
462
for dir_entry in fs::read_dir(source)? {
463
let entry = dir_entry?;
464
let file_type = entry.file_type()?;
465
let destination_path = destination.as_ref().join(entry.file_name());
466
if file_type.is_file() {
467
if entry.path().eq(avoid_path) {
468
continue;
469
}
470
let target_file_name = entry
471
.file_name()
472
.to_os_string()
473
.into_string()
474
.unwrap_or_default();
475
if single_file.is_none()
476
|| (single_file.is_some() && single_file.clone().unwrap().eq(&target_file_name))
477
{
478
log.trace(format!(
479
"Copying {} to {}",
480
entry.path().display(),
481
destination_path.display()
482
));
483
if !destination_path.exists() {
484
fs::copy(entry.path(), destination_path)?;
485
}
486
}
487
} else if single_file.is_none() {
488
copy_folder_content(
489
entry.path(),
490
destination_path,
491
single_file.clone(),
492
avoid_path,
493
log,
494
)?;
495
}
496
}
497
Ok(())
498
}
499
500
pub fn default_cache_folder() -> PathBuf {
501
if let Some(base_dirs) = BaseDirs::new() {
502
return Path::new(base_dirs.home_dir())
503
.join(String::from(CACHE_FOLDER).replace('/', std::path::MAIN_SEPARATOR_STR));
504
}
505
PathBuf::new()
506
}
507
508
pub fn compose_driver_path_in_cache(
509
driver_path: PathBuf,
510
driver_name: &str,
511
os: &str,
512
arch_folder: &str,
513
driver_version: &str,
514
) -> PathBuf {
515
driver_path
516
.join(driver_name)
517
.join(arch_folder)
518
.join(driver_version)
519
.join(get_driver_filename(driver_name, os))
520
}
521
522
pub fn get_driver_filename(driver_name: &str, os: &str) -> String {
523
format!("{}{}", driver_name, get_binary_extension(os))
524
}
525
526
pub fn get_binary_extension(os: &str) -> &str {
527
if WINDOWS.is(os) {
528
".exe"
529
} else {
530
""
531
}
532
}
533
534
pub fn parse_version(version_text: String, log: &Logger) -> Result<String, Error> {
535
if version_text.to_ascii_lowercase().contains("error") {
536
log.debug(format!("Error parsing version: {}", version_text));
537
return Err(anyhow!(PARSE_ERROR));
538
}
539
let mut parsed_version = "".to_string();
540
let re_numbers_dots = Regex::new(r"[^\d^.]")?;
541
let re_versions = Regex::new(r"(?:(\d+)\.)?(?:(\d+)\.)?(?:(\d+)\.\d+)")?;
542
for token in version_text.split(' ') {
543
parsed_version = re_numbers_dots.replace_all(token, "").to_string();
544
if re_versions.is_match(parsed_version.as_str()) {
545
break;
546
}
547
}
548
if parsed_version.ends_with('.') {
549
parsed_version = parsed_version[0..parsed_version.len() - 1].to_string();
550
}
551
Ok(parsed_version)
552
}
553
554
pub fn path_to_string(path: &Path) -> String {
555
path.to_path_buf()
556
.into_os_string()
557
.into_string()
558
.unwrap_or_default()
559
}
560
561
pub fn read_bytes_from_file(file_path: &str) -> Result<Vec<u8>, Error> {
562
let file = File::open(file_path)?;
563
let mut reader = BufReader::new(file);
564
let mut buffer = Vec::new();
565
reader.read_to_end(&mut buffer)?;
566
Ok(buffer)
567
}
568
569
pub fn find_bytes(buffer: &[u8], bytes: &[u8]) -> Option<usize> {
570
buffer
571
.windows(bytes.len())
572
.position(|window| window == bytes)
573
}
574
575
pub fn collect_files_from_cache<F: Fn(&DirEntry) -> bool>(
576
cache_path: &PathBuf,
577
filter: F,
578
) -> Vec<PathBuf> {
579
WalkDir::new(cache_path)
580
.sort_by_file_name()
581
.into_iter()
582
.filter_map(|entry| entry.ok())
583
.filter(|entry| filter(entry))
584
.map(|entry| entry.path().to_owned())
585
.collect()
586
}
587
588
pub fn find_latest_from_cache<F: Fn(&DirEntry) -> bool>(
589
cache_path: &PathBuf,
590
filter: F,
591
) -> Result<Option<PathBuf>, Error> {
592
let files_in_cache = collect_files_from_cache(cache_path, filter);
593
if !files_in_cache.is_empty() {
594
Ok(Some(files_in_cache.iter().last().unwrap().to_owned()))
595
} else {
596
Ok(None)
597
}
598
}
599
600
pub fn capitalize(s: &str) -> String {
601
let mut chars = s.chars();
602
match chars.next() {
603
None => String::new(),
604
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
605
}
606
}
607
608
#[cfg(not(windows))]
609
pub fn get_win_file_version(_file_path: &str) -> Option<String> {
610
None
611
}
612
613
#[cfg(windows)]
614
pub fn get_win_file_version(file_path: &str) -> Option<String> {
615
unsafe {
616
let wide_path: Vec<u16> = OsStr::new(file_path).encode_wide().chain(Some(0)).collect();
617
618
let mut dummy = 0;
619
let size = GetFileVersionInfoSizeW(wide_path.as_ptr(), &mut dummy);
620
if size == 0 {
621
return None;
622
}
623
624
let mut buffer: Vec<u8> = Vec::with_capacity(size as usize);
625
if GetFileVersionInfoW(wide_path.as_ptr(), 0, size, buffer.as_mut_ptr() as LPVOID) == 0 {
626
return None;
627
}
628
buffer.set_len(size as usize);
629
630
let mut lang_and_codepage_ptr: LPVOID = ptr::null_mut();
631
let mut lang_and_codepage_len: u32 = 0;
632
633
if VerQueryValueW(
634
buffer.as_ptr() as LPVOID,
635
OsStr::new("\\VarFileInfo\\Translation")
636
.encode_wide()
637
.chain(Some(0))
638
.collect::<Vec<u16>>()
639
.as_ptr(),
640
&mut lang_and_codepage_ptr,
641
&mut lang_and_codepage_len,
642
) == 0
643
{
644
return None;
645
}
646
647
if lang_and_codepage_len == 0 {
648
return None;
649
}
650
651
let lang_and_codepage_slice = std::slice::from_raw_parts(
652
lang_and_codepage_ptr as *const u16,
653
lang_and_codepage_len as usize / 2,
654
);
655
let lang = lang_and_codepage_slice[0];
656
let codepage = lang_and_codepage_slice[1];
657
658
let query = format!(
659
"\\StringFileInfo\\{:04x}{:04x}\\ProductVersion",
660
lang, codepage
661
);
662
let query_wide: Vec<u16> = OsStr::new(&query).encode_wide().chain(Some(0)).collect();
663
664
let mut product_version_ptr: LPVOID = ptr::null_mut();
665
let mut product_version_len: u32 = 0;
666
667
if VerQueryValueW(
668
buffer.as_ptr() as LPVOID,
669
query_wide.as_ptr(),
670
&mut product_version_ptr,
671
&mut product_version_len,
672
) == 0
673
{
674
return None;
675
}
676
677
if product_version_ptr.is_null() {
678
return None;
679
}
680
681
let product_version_slice = std::slice::from_raw_parts(
682
product_version_ptr as *const u16,
683
product_version_len as usize,
684
);
685
let product_version = String::from_utf16_lossy(product_version_slice);
686
687
Some(product_version.trim_end_matches('\0').to_string())
688
}
689
}
690
691