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

endoze / valheim-mod-manager / 23714700391

29 Mar 2026 05:22PM UTC coverage: 92.604% (+0.2%) from 92.43%
23714700391

push

github

web-flow
Merge pull request #15 from endoze/add-string-interning

Implement string interning for package manifest

500 of 531 new or added lines in 3 files covered. (94.16%)

9 existing lines in 3 files now uncovered.

889 of 960 relevant lines covered (92.6%)

3.72 hits per line

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

82.05
/src/zip.rs
1
use fs_extra::copy_items;
2
use fs_extra::dir::CopyOptions;
3
use std::fs;
4
use std::io::{self, Read, Write};
5
use std::path::{Path, PathBuf};
6
use tracing::{debug, error, info, warn};
7

8
use crate::config::APP_CONFIG;
9
use crate::error::{AppError, AppResult};
10
use crate::manifest::Manifest;
11

12
/// Unzips all downloaded mod files into directories named after the mods.
13
///
14
/// For each zip file in the downloads directory, this function:
15
/// 1. Extracts the mod name and version from the filename
16
/// 2. Checks if a directory for the mod already exists and has a manifest.json
17
/// 3. If manifest.json exists, compares its version with the zip version
18
/// 4. Only extracts the zip if versions don't match or the directory doesn't exist
19
///
20
/// # Returns
21
///
22
/// `Ok(())` if all files are processed successfully, or an error if reading the directory fails.
23
pub fn unzip_downloaded_mods(cache_dir: &str) -> AppResult<()> {
2✔
24
  let expanded_path = shellexpand::tilde(cache_dir);
4✔
25
  let mut cache_path = PathBuf::from(expanded_path.as_ref());
8✔
26
  cache_path.push("downloads");
4✔
27
  let downloads_dir = cache_path;
4✔
28
  let entries = fs::read_dir(&downloads_dir)?;
8✔
29

30
  info!("Processing mod files for extraction");
10✔
31

32
  for entry in entries.flatten() {
12✔
33
    let file_path = entry.path();
3✔
34

35
    if file_path.extension().and_then(|ext| ext.to_str()) != Some("zip") {
12✔
36
      continue;
37
    }
38

39
    process_zip_file(&downloads_dir, &file_path)?;
10✔
40
  }
41

42
  info!("Finished processing all mod files");
6✔
43

44
  Ok(())
2✔
45
}
46

47
/// Extracts a zip archive to the specified output path with CRC32 verification.
48
///
49
/// # Parameters
50
///
51
/// * `file` - The zip file to extract
52
/// * `outpath` - The directory to extract the archive to
53
///
54
/// # Returns
55
///
56
/// `Ok(())` if extraction succeeds, or a `ZipError` if it fails
57
///
58
/// # Errors
59
///
60
/// Returns an error if:
61
/// - The file is not a valid zip archive
62
/// - There are permission issues with the output path
63
/// - There is insufficient disk space
64
/// - Files in the archive have invalid names or paths
65
/// - CRC32 verification fails for any extracted file
66
fn unzip<T: AsRef<std::path::Path>>(mut file: std::fs::File, outpath: T) -> AppResult<()> {
2✔
67
  let mut archive = zip::ZipArchive::new(&mut file)?;
8✔
68
  let outpath = outpath.as_ref();
4✔
69

70
  fs::create_dir_all(outpath)?;
2✔
71

72
  for i in 0..archive.len() {
6✔
73
    let mut file_in_archive = archive.by_index(i)?;
4✔
74
    let outpath = match file_in_archive.enclosed_name() {
5✔
75
      Some(path) => outpath.join(path),
2✔
76
      None => {
×
77
        warn!("Invalid file path in zip: {}", file_in_archive.name());
×
78
        continue;
×
79
      }
80
    };
81

82
    let comment = file_in_archive.comment();
4✔
83

84
    if !comment.is_empty() {
2✔
85
      info!("File {} comment: {}", i, comment);
×
86
    }
87

88
    if file_in_archive.name().ends_with('/') {
4✔
89
      info!("Creating directory {}", outpath.display());
6✔
90
      fs::create_dir_all(&outpath)?;
4✔
91
    } else {
92
      info!("Extracting file {} to {}", i, outpath.display());
6✔
93
      if let Some(parent) = outpath.parent() {
6✔
94
        if !parent.exists() {
6✔
95
          fs::create_dir_all(parent)?;
2✔
96
        }
97
      }
98

99
      let expected_crc32 = file_in_archive.crc32();
8✔
100
      let mut outfile = fs::File::create(&outpath)?;
5✔
101

102
      let mut buffer = Vec::new();
4✔
103
      file_in_archive.read_to_end(&mut buffer)?;
7✔
104

105
      let actual_crc32 = calculate_crc32(&buffer);
7✔
106
      if actual_crc32 != expected_crc32 {
5✔
107
        error!(
×
108
          "CRC32 verification failed for {}: expected {:08x}, got {:08x}",
×
109
          outpath.display(),
×
110
          expected_crc32,
×
111
          actual_crc32
×
112
        );
113

114
        return Err(AppError::Zip(zip::result::ZipError::Io(io::Error::new(
×
115
          io::ErrorKind::InvalidData,
×
116
          format!("CRC32 verification failed for {}", outpath.display()),
×
117
        ))));
118
      }
119

120
      outfile.write_all(&buffer)?;
7✔
121

122
      debug!("CRC32 verification successful for {}", outpath.display());
8✔
123
    }
124
  }
125

126
  Ok(())
4✔
127
}
128

129
/// Calculates the CRC32 checksum for a byte buffer.
130
///
131
/// This function is used for verifying file integrity during extraction.
132
///
133
/// # Parameters
134
///
135
/// * `data` - The byte slice to calculate the checksum for
136
///
137
/// # Returns
138
///
139
/// The 32-bit CRC32 checksum value
140
fn calculate_crc32(data: &[u8]) -> u32 {
2✔
141
  let mut hasher = crc32fast::Hasher::new();
2✔
142
  hasher.update(data);
2✔
143
  hasher.finalize()
2✔
144
}
145

146
/// Processes a single zip file by extracting mod name and version from the filename,
147
/// then extracts the contents if needed and copies to install directory if configured.
148
///
149
/// # Parameters
150
///
151
/// * `downloads_dir` - Path to the downloads directory
152
/// * `file_path` - Path to the zip file being processed
153
///
154
/// # Returns
155
///
156
/// `Ok(())` if the file is processed successfully, or an error if:
157
/// - The filename doesn't match the expected format
158
/// - Directory creation fails
159
/// - The zip file cannot be opened
160
/// - Extraction fails
161
/// - Copying to install directory fails
162
fn process_zip_file(downloads_dir: &Path, file_path: &PathBuf) -> AppResult<()> {
3✔
163
  let file_name = match file_path.file_name().and_then(|n| n.to_str()) {
12✔
164
    Some(name) => name,
4✔
165
    None => {
166
      return Err(AppError::Other(format!(
×
167
        "Failed to get filename for {:?}",
168
        file_path
169
      )));
170
    }
171
  };
172

173
  let parts: Vec<&str> = file_name.split('-').collect();
4✔
174

175
  if parts.len() < 3 {
8✔
176
    return Err(AppError::Other(format!(
12✔
177
      "Invalid filename format: {}",
178
      file_name
179
    )));
180
  }
181

182
  let mod_name = format!("{}-{}", parts[0], parts[1]);
7✔
183
  let zip_version = parts[2].trim_end_matches(".zip");
7✔
184
  let mod_dir = downloads_dir.join(&mod_name);
3✔
185
  let should_extract = should_extract_mod(&mod_dir, &mod_name, zip_version)?;
6✔
186

187
  fs::create_dir_all(&mod_dir).map_err(|e| {
6✔
188
    AppError::Io(io::Error::other(format!(
×
189
      "Failed to create directory for {}: {}",
190
      mod_name, e
191
    )))
192
  })?;
193

194
  if should_extract {
4✔
195
    debug!("Extracting {} to {}", file_name, mod_dir.display());
7✔
196

197
    let file = fs::File::open(file_path).map_err(|e| {
6✔
198
      AppError::Io(io::Error::other(format!(
×
199
        "Failed to open {} for unzipping: {}",
200
        file_name, e
201
      )))
202
    })?;
203

204
    unzip(file, &mod_dir)?;
8✔
205
  }
206

207
  if let Some(install_dir) = &APP_CONFIG.install_dir {
4✔
208
    copy_mod_to_install_dir(&mod_dir, install_dir, &mod_name)?;
×
209
  }
210

211
  Ok(())
2✔
212
}
213

214
/// Determines whether a mod should be extracted based on version comparison.
215
///
216
/// # Parameters
217
///
218
/// * `mod_dir` - Path to the mod directory
219
/// * `mod_name` - Name of the mod
220
/// * `zip_version` - Version of the mod in the zip file
221
///
222
/// # Returns
223
///
224
/// * `Ok(true)` if:
225
///   - The mod directory doesn't exist
226
///   - No manifest.json exists
227
///   - The manifest exists but has a different version
228
///   - The manifest can't be parsed
229
/// * `Ok(false)` if the mod already exists with the same version
230
fn should_extract_mod(mod_dir: &Path, mod_name: &str, zip_version: &str) -> AppResult<bool> {
2✔
231
  let manifest_path = mod_dir.join("manifest.json");
2✔
232

233
  if !manifest_path.exists() {
4✔
234
    debug!(
6✔
235
      "Mod {} not found or no manifest, extracting version {}",
236
      mod_name, zip_version
237
    );
238

239
    return Ok(true);
2✔
240
  }
241

242
  match Manifest::from_file(&manifest_path) {
4✔
243
    Ok(manifest) => {
2✔
244
      if manifest.version_number == zip_version {
8✔
245
        debug!(
6✔
246
          "Mod {} already has version {}, skipping",
247
          mod_name, zip_version
248
        );
249

250
        Ok(false)
2✔
251
      } else {
252
        debug!(
6✔
253
          "Mod {} version changed from {} to {}, extracting",
254
          mod_name, manifest.version_number, zip_version
255
        );
256

257
        Ok(true)
2✔
258
      }
259
    }
260
    Err(err) => {
2✔
261
      warn!("Failed to parse manifest for {}: {}", mod_name, err);
6✔
262

263
      Ok(true)
2✔
264
    }
265
  }
266
}
267

268
/// Copies a mod to the configured install directory using optimized copy options.
269
///
270
/// This function will:
271
/// - Create install and target directories if they don't exist
272
/// - Copy all mod files to the target directory
273
/// - Overwrite existing files if they exist
274
/// - Copy directory contents inside the target directory
275
/// - Use optimized buffer size for faster copying
276
///
277
/// # Parameters
278
///
279
/// * `mod_dir` - Path to the mod directory
280
/// * `install_dir` - Path to the installation directory
281
/// * `mod_name` - Name of the mod
282
///
283
/// # Returns
284
///
285
/// `Ok(())` if the copy succeeds, or an error if:
286
/// - The install directory doesn't exist or can't be created
287
/// - Files can't be copied due to permission issues
288
/// - Other I/O errors occur
289
fn copy_mod_to_install_dir(mod_dir: &Path, install_dir: &str, mod_name: &str) -> AppResult<()> {
2✔
290
  let expanded_install_dir = shellexpand::tilde(install_dir);
2✔
291
  let install_path = PathBuf::from(expanded_install_dir.as_ref());
4✔
292

293
  if !install_path.exists() {
4✔
294
    debug!(
6✔
295
      "Install directory {} does not exist, creating it",
296
      install_dir
297
    );
298

299
    fs::create_dir_all(&install_path).map_err(|e| {
4✔
300
      AppError::Io(io::Error::other(format!(
×
301
        "Failed to create install directory {}: {}",
302
        install_dir, e
303
      )))
304
    })?;
305
  }
306

307
  info!(
12✔
308
    "Copying mod {} to install directory {}",
309
    mod_name, install_dir
310
  );
311

312
  let target_dir = install_path.join(mod_name);
8✔
313

314
  if !target_dir.exists() {
10✔
315
    fs::create_dir_all(&target_dir).map_err(|e| {
12✔
316
      AppError::Io(io::Error::other(format!(
×
317
        "Failed to create target directory {}: {}",
318
        target_dir.display(),
×
319
        e
320
      )))
321
    })?;
322
  }
323

324
  let mut options = CopyOptions::new();
6✔
325
  options.overwrite = true;
6✔
326
  options.skip_exist = false;
6✔
327
  options.copy_inside = true;
6✔
328
  options.buffer_size = 64000;
6✔
329

330
  let entries = fs::read_dir(mod_dir)?
6✔
331
    .filter_map(Result::ok)
332
    .map(|entry| entry.path())
7✔
333
    .collect::<Vec<_>>();
334

335
  if !entries.is_empty() {
7✔
UNCOV
336
    debug!(
×
337
      "Copying {} items to {}",
338
      entries.len(),
339
      target_dir.display()
340
    );
341

342
    copy_items(&entries, &target_dir, &options)
6✔
343
      .map_err(|e| AppError::Io(io::Error::other(format!("Failed to copy mod files: {}", e))))?;
×
344
  }
345

346
  debug!(
3✔
347
    "Successfully copied mod {} to {}",
348
    mod_name,
349
    target_dir.display()
350
  );
351

352
  Ok(())
3✔
353
}
354

355
#[cfg(test)]
356
mod tests {
357
  use super::*;
358
  use std::fs::File;
359
  use std::io::Write;
360
  use tempfile::tempdir;
361
  use zip::write::{FileOptions, ZipWriter};
362

363
  #[test]
364
  fn test_unzip_downloaded_mods() {
365
    let temp_dir = tempdir().unwrap();
366
    let cache_dir = temp_dir.path().to_str().unwrap();
367
    let downloads_dir = PathBuf::from(cache_dir).join("downloads");
368
    fs::create_dir_all(&downloads_dir).unwrap();
369

370
    let zip_path = downloads_dir.join("Owner-ModName-1.0.0.zip");
371
    let mut file = File::create(&zip_path).unwrap();
372
    file.write_all(b"This is not a valid zip file").unwrap();
373

374
    let result = unzip_downloaded_mods(cache_dir);
375

376
    assert!(result.is_err());
377
  }
378

379
  #[test]
380
  fn test_unzip_downloaded_mods_with_valid_zip() {
381
    let temp_dir = tempdir().unwrap();
382
    let cache_dir = temp_dir.path().to_str().unwrap();
383
    let downloads_dir = PathBuf::from(cache_dir).join("downloads");
384
    fs::create_dir_all(&downloads_dir).unwrap();
385

386
    let manifest_content = r#"{
387
      "version_number": "1.0.0",
388
      "name": "TestMod",
389
      "description": "Test mod description"
390
    }"#;
391

392
    let _zip_path = create_test_zip(&downloads_dir, "Owner-ModName-1.0.0.zip", manifest_content);
393

394
    let non_zip_path = downloads_dir.join("not-a-zipfile.txt");
395
    let mut non_zip_file = File::create(&non_zip_path).unwrap();
396
    non_zip_file.write_all(b"This is not a zip file").unwrap();
397

398
    let _result = unzip_downloaded_mods(cache_dir);
399
  }
400

401
  fn create_test_zip(dir: &Path, filename: &str, manifest_content: &str) -> PathBuf {
402
    let zip_path = dir.join(filename);
403
    let file = File::create(&zip_path).unwrap();
404
    let mut zip = ZipWriter::new(file);
405

406
    let options: FileOptions<'_, ()> =
407
      FileOptions::default().compression_method(zip::CompressionMethod::Stored);
408

409
    zip.start_file("manifest.json", options).unwrap();
410
    zip.write_all(manifest_content.as_bytes()).unwrap();
411

412
    zip.start_file("README.md", options).unwrap();
413
    zip.write_all(b"# Test Mod\nThis is a test mod.").unwrap();
414

415
    zip.add_directory("test_directory/", options).unwrap();
416

417
    zip
418
      .start_file("test_directory/test_file.txt", options)
419
      .unwrap();
420
    zip.write_all(b"Test file inside directory").unwrap();
421

422
    zip.finish().unwrap();
423

424
    zip_path
425
  }
426

427
  #[test]
428
  fn test_calculate_crc32() {
429
    let data = b"test data for crc32 calculation";
430
    let crc = calculate_crc32(data);
431

432
    let expected_crc: u32 = 1707861357;
433

434
    assert_eq!(crc, expected_crc);
435
  }
436

437
  #[test]
438
  fn test_unzip_with_invalid_paths() {
439
    let temp_dir = tempdir().unwrap();
440
    let output_dir = temp_dir.path().join("output");
441
    fs::create_dir_all(&output_dir).unwrap();
442

443
    let zip_path = temp_dir.path().join("test_invalid_paths.zip");
444
    let file = File::create(&zip_path).unwrap();
445
    let mut zip = ZipWriter::new(file);
446

447
    let options: FileOptions<'_, ()> =
448
      FileOptions::default().compression_method(zip::CompressionMethod::Stored);
449

450
    zip.start_file("valid_file.txt", options).unwrap();
451
    zip.write_all(b"Valid file content").unwrap();
452

453
    zip.finish().unwrap();
454

455
    let file = File::open(&zip_path).unwrap();
456
    let result = unzip(file, &output_dir);
457

458
    assert!(result.is_ok());
459

460
    let valid_file = output_dir.join("valid_file.txt");
461
    assert!(valid_file.exists());
462
  }
463

464
  #[test]
465
  fn test_process_zip_file_invalid_filename() {
466
    let temp_dir = tempdir().unwrap();
467
    let downloads_dir = temp_dir.path().join("downloads");
468
    fs::create_dir_all(&downloads_dir).unwrap();
469

470
    let invalid_zip_path = downloads_dir.join("InvalidFile.zip");
471
    let mut file = File::create(&invalid_zip_path).unwrap();
472
    file.write_all(b"This is not a valid zip file").unwrap();
473

474
    let result = process_zip_file(&downloads_dir, &invalid_zip_path);
475

476
    assert!(result.is_err());
477
    if let Err(err) = result {
478
      match err {
479
        AppError::Other(msg) => {
480
          assert!(msg.contains("Invalid filename format"));
481
        }
482
        _ => panic!("Expected 'Other' error for invalid filename format"),
483
      }
484
    }
485
  }
486

487
  #[test]
488
  fn test_process_zip_file_nonexistant_filename() {
489
    let temp_dir = tempdir().unwrap();
490
    let downloads_dir = temp_dir.path().join("downloads");
491
    fs::create_dir_all(&downloads_dir).unwrap();
492

493
    let nonexistent_path = PathBuf::from("/nonexistent/file/that/does/not/exist");
494

495
    let result = process_zip_file(&downloads_dir, &nonexistent_path);
496

497
    assert!(result.is_err());
498
  }
499

500
  #[test]
501
  fn test_should_extract_mod() {
502
    let temp_dir = tempdir().unwrap();
503
    let mod_dir = temp_dir.path().join("Owner-ModName");
504
    fs::create_dir_all(&mod_dir).unwrap();
505

506
    let result = should_extract_mod(&mod_dir, "Owner-ModName", "1.0.0");
507
    assert!(result.unwrap());
508
    let manifest_content = r#"{
509
      "version_number": "1.0.0",
510
      "name": "TestMod",
511
      "description": "Test mod description"
512
    }"#;
513

514
    let manifest_path = mod_dir.join("manifest.json");
515
    let mut file = File::create(&manifest_path).unwrap();
516
    file.write_all(manifest_content.as_bytes()).unwrap();
517

518
    let result = should_extract_mod(&mod_dir, "Owner-ModName", "1.0.0");
519
    assert!(!result.unwrap());
520

521
    let result = should_extract_mod(&mod_dir, "Owner-ModName", "2.0.0");
522
    assert!(result.unwrap());
523
    let mut file = File::create(&manifest_path).unwrap();
524
    file.write_all(b"invalid json content").unwrap();
525

526
    let result = should_extract_mod(&mod_dir, "Owner-ModName", "1.0.0");
527
    assert!(result.unwrap());
528
  }
529

530
  #[test]
531
  fn test_copy_mod_to_install_dir() {
532
    let src_dir = tempdir().unwrap();
533
    let install_dir = tempdir().unwrap();
534
    let mod_name = "TestMod";
535

536
    let test_file1 = src_dir.path().join("test1.txt");
537
    let mut file1 = File::create(&test_file1).unwrap();
538
    file1.write_all(b"test file 1 content").unwrap();
539

540
    let test_file2 = src_dir.path().join("test2.txt");
541
    let mut file2 = File::create(&test_file2).unwrap();
542
    file2.write_all(b"test file 2 content").unwrap();
543

544
    let subdir = src_dir.path().join("subdir");
545
    fs::create_dir(&subdir).unwrap();
546

547
    let test_file3 = subdir.join("test3.txt");
548
    let mut file3 = File::create(&test_file3).unwrap();
549
    file3.write_all(b"test file 3 content").unwrap();
550

551
    let result = copy_mod_to_install_dir(
552
      src_dir.path(),
553
      install_dir.path().to_str().unwrap(),
554
      mod_name,
555
    );
556

557
    assert!(result.is_ok());
558

559
    let target_dir = install_dir.path().join(mod_name);
560
    let dest_file1 = target_dir.join("test1.txt");
561
    let dest_file2 = target_dir.join("test2.txt");
562
    let dest_subdir = target_dir.join("subdir");
563
    let dest_file3 = dest_subdir.join("test3.txt");
564

565
    assert!(target_dir.exists());
566
    assert!(dest_file1.exists());
567
    assert!(dest_file2.exists());
568
    assert!(dest_subdir.exists());
569
    assert!(dest_file3.exists());
570

571
    let content1 = fs::read_to_string(&dest_file1).unwrap();
572
    let content3 = fs::read_to_string(&dest_file3).unwrap();
573

574
    assert_eq!(content1, "test file 1 content");
575
    assert_eq!(content3, "test file 3 content");
576
  }
577

578
  #[test]
579
  fn test_copy_mod_to_install_dir_nonexistent_dir() {
580
    let src_dir = tempdir().unwrap();
581
    let install_dir = tempdir().unwrap();
582
    let nonexistent_dir = install_dir.path().join("nonexistent");
583
    let mod_name = "TestMod";
584

585
    let test_file = src_dir.path().join("test.txt");
586
    let mut file = File::create(&test_file).unwrap();
587
    file.write_all(b"test content").unwrap();
588

589
    let result =
590
      copy_mod_to_install_dir(src_dir.path(), nonexistent_dir.to_str().unwrap(), mod_name);
591

592
    assert!(result.is_ok());
593
    assert!(nonexistent_dir.exists());
594
    assert!(nonexistent_dir.join(mod_name).exists());
595
    assert!(nonexistent_dir.join(mod_name).join("test.txt").exists());
596
  }
597

598
  #[test]
599
  fn test_copy_mod_to_install_dir_empty_src() {
600
    let src_dir = tempdir().unwrap();
601
    let install_dir = tempdir().unwrap();
602
    let mod_name = "EmptyMod";
603

604
    let result = copy_mod_to_install_dir(
605
      src_dir.path(),
606
      install_dir.path().to_str().unwrap(),
607
      mod_name,
608
    );
609

610
    assert!(result.is_ok());
611

612
    let target_dir = install_dir.path().join(mod_name);
613
    assert!(target_dir.exists());
614

615
    let entries = fs::read_dir(&target_dir).unwrap().count();
616
    assert_eq!(entries, 0);
617
  }
618

619
  #[test]
620
  fn test_unzip_with_directory_in_zip() {
621
    let temp_dir = tempdir().unwrap();
622
    let output_dir = temp_dir.path().join("output");
623
    fs::create_dir_all(&output_dir).unwrap();
624

625
    let zip_path = temp_dir.path().join("test_with_dir.zip");
626
    let file = File::create(&zip_path).unwrap();
627
    let mut zip = ZipWriter::new(file);
628

629
    let options: FileOptions<'_, ()> =
630
      FileOptions::default().compression_method(zip::CompressionMethod::Stored);
631

632
    zip.add_directory("test_dir/", options).unwrap();
633

634
    zip.start_file("test_dir/file_in_dir.txt", options).unwrap();
635
    zip.write_all(b"File in directory").unwrap();
636

637
    zip.finish().unwrap();
638

639
    let file = File::open(&zip_path).unwrap();
640
    let result = unzip(file, &output_dir);
641

642
    assert!(result.is_ok());
643

644
    let dir_path = output_dir.join("test_dir");
645
    assert!(dir_path.exists());
646
    assert!(dir_path.is_dir());
647

648
    let file_in_dir = dir_path.join("file_in_dir.txt");
649
    assert!(file_in_dir.exists());
650

651
    let content = fs::read_to_string(&file_in_dir).unwrap();
652
    assert_eq!(content, "File in directory");
653
  }
654

655
  #[test]
656
  fn test_unzip_with_nested_directories() {
657
    let temp_dir = tempdir().unwrap();
658
    let output_dir = temp_dir.path().join("output");
659

660
    let zip_path = temp_dir.path().join("test_nested.zip");
661
    let file = File::create(&zip_path).unwrap();
662
    let mut zip = ZipWriter::new(file);
663

664
    let options: FileOptions<'_, ()> =
665
      FileOptions::default().compression_method(zip::CompressionMethod::Stored);
666

667
    zip
668
      .start_file("deep/nested/path/file.txt", options)
669
      .unwrap();
670
    zip.write_all(b"Deep nested file").unwrap();
671

672
    zip.finish().unwrap();
673

674
    let file = File::open(&zip_path).unwrap();
675
    let result = unzip(file, &output_dir);
676

677
    assert!(result.is_ok());
678

679
    let nested_file = output_dir.join("deep/nested/path/file.txt");
680
    assert!(nested_file.exists());
681

682
    let content = fs::read_to_string(&nested_file).unwrap();
683
    assert_eq!(content, "Deep nested file");
684
  }
685
}
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