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

MilesCranmer / rip2 / #655

14 Jan 2025 03:33AM UTC coverage: 75.103% (-2.1%) from 77.16%
#655

push

365 of 486 relevant lines covered (75.1%)

1586309936.81 hits per line

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

70.35
/src/lib.rs
1
use clap::CommandFactory;
2
use fs_extra::dir::get_size;
3
use std::fs::Metadata;
4
use std::io::{BufRead, BufReader, Error, ErrorKind, Write};
5
use std::path::{Path, PathBuf};
6
use std::{env, fs};
7
use walkdir::WalkDir;
8

9
/// Information needed to create a directory with specific permissions
10
#[derive(Debug, Clone)]
11
pub struct DirToCreate {
12
    pub path: PathBuf,
13
    pub permissions: Option<fs::Permissions>,
14
}
15

16
// Platform-specific imports
17
#[cfg(unix)]
18
use nix::libc;
19
#[cfg(unix)]
20
use nix::sys::stat::Mode;
21
#[cfg(unix)]
22
use nix::unistd::mkfifo;
23
#[cfg(unix)]
24
use std::os::unix::fs::{symlink, FileTypeExt, PermissionsExt};
25

26
#[cfg(target_os = "windows")]
27
use std::os::windows::fs::symlink_file as symlink;
28

2,147,483,647✔
29
pub mod args;
2,147,483,647✔
30
pub mod completions;
2,147,483,647✔
31
pub mod record;
32
pub mod util;
2,147,483,647✔
33

2,147,483,647✔
34
use args::Args;
35
use record::{Record, RecordItem, DEFAULT_FILE_LOCK};
36

37
const LINES_TO_INSPECT: usize = 6;
×
38
const FILES_TO_INSPECT: usize = 6;
×
39
pub const BIG_FILE_THRESHOLD: u64 = 500_000_000; // 500 MB
×
40

41
pub fn run(cli: &Args, mode: impl util::TestingMode, stream: &mut impl Write) -> Result<(), Error> {
42
    args::validate_args(cli)?;
43
    let graveyard: &PathBuf = &get_graveyard(cli.graveyard.clone());
44

45
    if !graveyard.exists() {
2,147,483,647✔
46
        fs::create_dir_all(graveyard)?;
2,147,483,647✔
47

48
        #[cfg(unix)]
49
        {
×
50
            fs::set_permissions(graveyard, fs::Permissions::from_mode(0o700))?;
51
        }
2,147,483,647✔
52
    }
2,147,483,647✔
53

54
    // Stores the deleted files
2,147,483,647✔
55
    let record = Record::<DEFAULT_FILE_LOCK>::new(graveyard);
56
    let cwd = &env::current_dir()?;
57

58
    // If the user wishes to restore everything
59
    if cli.decompose {
60
        // In force mode, skip the prompt to decompose
61
        if cli.force || util::prompt_yes("Really unlink the entire graveyard?", &mode, stream)? {
62
            fs::remove_dir_all(graveyard)?;
2,147,483,647✔
63
        }
2,147,483,647✔
64
    } else if let Some(ref mut graves_to_exhume) = cli.unbury.clone() {
2,147,483,647✔
65
        // Vector to hold the grave path of items we want to unbury.
2,147,483,647✔
66
        // This will be used to determine which items to remove from the
67
        // record following the unbury.
68
        // Initialize it with the targets passed to -r
69

70
        // If -s is also passed, push all files found by seance onto
2,147,483,647✔
71
        // the graves_to_exhume.
2,147,483,647✔
72
        if cli.seance && record.open().is_ok() {
2,147,483,647✔
73
            let gravepath = util::join_absolute(graveyard, dunce::canonicalize(cwd)?);
74
            for grave in record.seance(&gravepath)? {
75
                graves_to_exhume.push(grave.dest);
76
            }
2,147,483,647✔
77
        }
78

79
        // Otherwise, add the last deleted file
2,147,483,647✔
80
        if graves_to_exhume.is_empty() {
2,147,483,647✔
81
            if let Ok(s) = record.get_last_bury() {
2,147,483,647✔
82
                graves_to_exhume.push(s);
2,147,483,647✔
83
            }
2,147,483,647✔
84
        }
85

2,147,483,647✔
86
        let allow_rename = util::allow_rename();
2,147,483,647✔
87

×
88
        // Go through the graveyard and exhume all the graves
×
89
        for line in record.lines_of_graves(graves_to_exhume) {
×
90
            let entry = RecordItem::new(&line);
×
91
            let orig: PathBuf = if util::symlink_exists(&entry.orig) {
×
92
                util::rename_grave(&entry.orig)
×
93
            } else {
94
                PathBuf::from(&entry.orig)
95
            };
96
            let dirs_to_create = build_dirs_to_create_from_graveyard(&entry.dest, &orig);
97

2,147,483,647✔
98
            move_target(
2,147,483,647✔
99
                &entry.dest,
100
                &orig,
2,147,483,647✔
101
                allow_rename,
2,147,483,647✔
102
                &mode,
103
                stream,
104
                cli.force,
2,147,483,647✔
105
                &dirs_to_create,
2,147,483,647✔
106
            )
2,147,483,647✔
107
            .map_err(|e| {
×
108
                Error::new(
2,147,483,647✔
109
                    e.kind(),
2,147,483,647✔
110
                    format!(
×
111
                        "Unbury failed: couldn't copy files from {} to {}",
112
                        entry.dest.display(),
2,147,483,647✔
113
                        orig.display()
2,147,483,647✔
114
                    ),
115
                )
2,147,483,647✔
116
            })?;
2,147,483,647✔
117
            writeln!(
118
                stream,
×
119
                "Returned {} to {}",
×
120
                entry.dest.display(),
×
121
                orig.display()
×
122
            )?;
×
123
        }
×
124
        record.log_exhumed_graves(graves_to_exhume)?;
×
125
    } else if cli.seance {
×
126
        let gravepath = util::join_absolute(graveyard, dunce::canonicalize(cwd)?);
×
127
        writeln!(stream, "{: <19}\tpath", "deletion_time")?;
128
        for grave in record.seance(&gravepath)? {
129
            let formatted_time = grave.format_time_for_display()?;
130
            writeln!(stream, "{}\t{}", formatted_time, grave.dest.display())?;
131
        }
2,147,483,647✔
132
    } else if cli.targets.is_empty() {
133
        Args::command().print_help()?;
134
    } else {
135
        let allow_rename = util::allow_rename();
2,147,483,647✔
136
        for target in &cli.targets {
137
            bury_target(
138
                target,
139
                graveyard,
140
                &record,
141
                cwd,
142
                cli.inspect,
143
                allow_rename,
144
                &mode,
145
                stream,
146
                cli.force,
147
            )?;
2,147,483,647✔
148
        }
2,147,483,647✔
149
    }
2,147,483,647✔
150

2,147,483,647✔
151
    Ok(())
2,147,483,647✔
152
}
2,147,483,647✔
153

154
#[allow(clippy::too_many_arguments)]
155
fn bury_target<const FILE_LOCK: bool>(
156
    target: &PathBuf,
157
    graveyard: &PathBuf,
2,147,483,647✔
158
    record: &Record<FILE_LOCK>,
2,147,483,647✔
159
    cwd: &Path,
2,147,483,647✔
160
    inspect: bool,
161
    allow_rename: bool,
×
162
    mode: &impl util::TestingMode,
163
    stream: &mut impl Write,
164
    force: bool,
2,147,483,647✔
165
) -> Result<(), Error> {
166
    // Check if source exists
2,147,483,647✔
167
    let metadata = &fs::symlink_metadata(target).map_err(|_| {
2,147,483,647✔
168
        Error::new(
2,147,483,647✔
169
            ErrorKind::NotFound,
170
            format!(
171
                "Cannot remove {}: no such file or directory",
172
                target.to_str().unwrap()
2,147,483,647✔
173
            ),
2,147,483,647✔
174
        )
2,147,483,647✔
175
    })?;
2,147,483,647✔
176
    // Canonicalize the path unless it's a symlink
2,147,483,647✔
177
    let source = &if metadata.file_type().is_symlink() {
178
        cwd.join(target)
2,147,483,647✔
179
    } else {
2,147,483,647✔
180
        dunce::canonicalize(cwd.join(target))
181
            .map_err(|e| Error::new(e.kind(), "Failed to canonicalize path"))?
182
    };
2,147,483,647✔
183

2,147,483,647✔
184
    if inspect && !should_we_bury_this(target, source, metadata, mode, stream)? {
×
185
        // User chose to not bury the file
186
    } else if source.starts_with(
187
        dunce::canonicalize(graveyard)
188
            .map_err(|e| Error::new(e.kind(), "Failed to canonicalize graveyard path"))?,
×
189
    ) {
190
        // If rip is called on a file already in the graveyard, prompt
191
        // to permanently delete it instead.
192
        if force
193
            || util::prompt_yes(
194
                format!(
2,147,483,647✔
195
                    "{} is already in the graveyard.\nPermanently unlink it?",
2,147,483,647✔
196
                    source.display()
197
                ),
2,147,483,647✔
198
                mode,
2,147,483,647✔
199
                stream,
200
            )?
2,147,483,647✔
201
        {
202
            if fs::remove_dir_all(source).is_err() {
203
                fs::remove_file(source).map_err(|e| {
204
                    Error::new(e.kind(), format!("Couldn't unlink {}", source.display()))
2,147,483,647✔
205
                })?;
×
206
            }
×
207
        } else {
208
            writeln!(stream, "Skipping {}", source.display())?;
209
            // TODO: In the original code, this was a hard return from the entire
×
210
            // method (i.e., `run`). I think it should just be a return from the bury
211
            // (meaning a `continue` in the original code's loop). But I'm not sure.
2,147,483,647✔
212
        }
213
    } else {
214
        let (dest, dirs_to_create) = build_graveyard_dest(graveyard, source);
215
        let dest: &Path = &{
2,147,483,647✔
216
            // Resolve a name conflict if necessary
217
            if util::symlink_exists(&dest) {
218
                util::rename_grave(dest)
2,147,483,647✔
219
            } else {
220
                dest
221
            }
222
        };
223

224
        let moved = move_target(
225
            source,
2,147,483,647✔
226
            dest,
227
            allow_rename,
228
            mode,
2,147,483,647✔
229
            stream,
×
230
            force,
×
231
            &dirs_to_create,
×
232
        )
233
        .map_err(|e| {
234
            fs::remove_dir_all(dest).ok();
×
235
            Error::new(e.kind(), "Failed to bury file")
×
236
        })?;
237

×
238
        if moved {
×
239
            // Clean up any partial buries due to permission error
240
            record.write_log(source, dest)?;
241
        }
242
    }
243

2,147,483,647✔
244
    Ok(())
2,147,483,647✔
245
}
×
246

×
247
fn should_we_bury_this(
×
248
    target: &Path,
2,147,483,647✔
249
    source: &PathBuf,
×
250
    metadata: &Metadata,
251
    mode: &impl util::TestingMode,
2,147,483,647✔
252
    stream: &mut impl Write,
253
) -> Result<bool, Error> {
254
    if metadata.is_dir() {
2,147,483,647✔
255
        // Get the size of the directory and all its contents
2,147,483,647✔
256
        {
257
            let num_bytes = get_size(source).map_err(|_| {
2,147,483,647✔
258
                Error::other(format!(
2,147,483,647✔
259
                    "Failed to get size of directory: {}",
260
                    source.display()
261
                ))
2,147,483,647✔
262
            })?;
2,147,483,647✔
263
            writeln!(
×
264
                stream,
×
265
                "{}: directory, {} including:",
2,147,483,647✔
266
                target.to_str().unwrap(),
267
                util::humanize_bytes(num_bytes)
2,147,483,647✔
268
            )?;
269
        }
270

×
271
        // Print the first few top-level files in the directory
272
        for entry in WalkDir::new(source)
273
            .sort_by(|a, b| a.file_name().cmp(b.file_name()))
274
            .min_depth(1)
2,147,483,647✔
275
            .max_depth(1)
2,147,483,647✔
276
            .into_iter()
2,147,483,647✔
277
            .filter_map(Result::ok)
278
            .take(FILES_TO_INSPECT)
279
        {
280
            writeln!(stream, "{}", entry.path().display())?;
281
        }
282
    } else {
283
        writeln!(
2,147,483,647✔
284
            stream,
285
            "{}: file, {}",
286
            &target.to_str().unwrap(),
287
            util::humanize_bytes(metadata.len())
288
        )?;
289
        // Read the file and print the first few lines
290
        if let Ok(source_file) = fs::File::open(source) {
291
            for line in BufReader::new(source_file)
292
                .lines()
293
                .take(LINES_TO_INSPECT)
2,147,483,647✔
294
                .filter_map(Result::ok)
2,147,483,647✔
295
            {
296
                writeln!(stream, "> {line}")?;
297
            }
298
        } else {
299
            writeln!(stream, "Error reading {}", source.display())?;
2,147,483,647✔
300
        }
2,147,483,647✔
301
    }
302
    util::prompt_yes(
303
        format!("Send {} to the graveyard?", target.to_str().unwrap()),
2,147,483,647✔
304
        mode,
2,147,483,647✔
305
        stream,
306
    )
2,147,483,647✔
307
}
×
308

×
309
/// Plan graveyard directory structure and permissions
×
310
fn build_graveyard_dest(graveyard: &Path, source: &Path) -> (PathBuf, Vec<DirToCreate>) {
×
311
    let mut dest = graveyard.to_path_buf();
×
312
    let mut dirs_to_create = Vec::new();
×
313
    let mut cumulative_source = PathBuf::new();
314

315
    for component in source.components() {
316
        // Build cumulative source path
×
317
        cumulative_source.push(component.as_os_str());
×
318

×
319
        // Process component for destination using shared logic
×
320
        if util::push_component_to_dest(&mut dest, &component) {
321
            // Only add directories to the list (skip the final file component)
322
            if cumulative_source.is_dir() {
2,147,483,647✔
323
                let permissions = fs::metadata(&cumulative_source)
324
                    .map(|m| m.permissions())
325
                    .ok();
326
                dirs_to_create.push(DirToCreate {
327
                    path: dest.clone(),
328
                    permissions,
2,147,483,647✔
329
                });
330
            }
331
        }
332
    }
333

334
    (dest, dirs_to_create)
335
}
336

2,147,483,647✔
337
/// Plan source directory structure and permissions
338
fn build_dirs_to_create_from_graveyard(
2,147,483,647✔
339
    graveyard_path: &Path,
×
340
    orig_path: &Path,
×
341
) -> Vec<DirToCreate> {
×
342
    let mut dirs_to_create = Vec::new();
343

344
    // Walk from file to root and collect permissions to propagate
345
    let mut graveyard_current = graveyard_path.parent();
×
346
    let mut orig_current = orig_path.parent();
2,147,483,647✔
347

3✔
348
    while let (Some(g), Some(o)) = (graveyard_current, orig_current) {
3✔
349
        let permissions = fs::metadata(g).map(|m| m.permissions()).ok();
3✔
350
        dirs_to_create.push(DirToCreate {
3✔
351
            path: o.to_path_buf(),
3✔
352
            permissions,
3✔
353
        });
354

355
        // Move up one level
356
        graveyard_current = g.parent();
357
        orig_current = o.parent();
2,147,483,647✔
358
    }
×
359
    dirs_to_create.reverse();
×
360
    dirs_to_create
×
361
}
×
362

×
363
/// Create the missing directories needed for a copy operation.
×
364
///
365
/// Important: permissions are applied *after* the copy finishes; otherwise a non-writable parent
366
/// (e.g. mode 0555) can prevent creating deeper directories or writing the file itself.
367
fn create_dirs_for_copy(dirs_to_create: &[DirToCreate]) -> Result<Vec<DirToCreate>, Error> {
368
    let mut created = Vec::new();
369

2,147,483,647✔
370
    // Create directories one by one in order (parent to child).
2,147,483,647✔
371
    // This assumes `dirs_to_create` is ordered from root to leaf.
2,147,483,647✔
372
    for dir in dirs_to_create {
2,147,483,647✔
373
        if dir.path.exists() {
374
            continue;
375
        }
376

2,147,483,647✔
377
        fs::create_dir(&dir.path).map_err(|e| {
378
            Error::new(
379
                e.kind(),
2,147,483,647✔
380
                format!("Failed to create directory {}: {}", dir.path.display(), e),
381
            )
382
        })?;
383
        created.push(dir.clone());
384
    }
385

386
    Ok(created)
2,147,483,647✔
387
}
×
388

389
fn apply_dir_permissions(dirs: &[DirToCreate]) -> Result<(), Error> {
×
390
    for dir in dirs.iter().rev() {
391
        if let Some(perms) = &dir.permissions {
2,147,483,647✔
392
            fs::set_permissions(&dir.path, perms.clone()).map_err(|e| {
2,147,483,647✔
393
                Error::new(
2,147,483,647✔
394
                    e.kind(),
2,147,483,647✔
395
                    format!("Failed to set permissions on {}: {}", dir.path.display(), e),
2,147,483,647✔
396
                )
2,147,483,647✔
397
            })?;
398
        }
2,147,483,647✔
399
    }
2,147,483,647✔
400
    Ok(())
401
}
402

2,147,483,647✔
403
/// Move a target to a given destination, copying if necessary.
404
/// Returns true if the target was moved, false if it was not (due to
405
/// user input)
406
pub fn move_target(
2,147,483,647✔
407
    target: &Path,
2,147,483,647✔
408
    dest: &Path,
2,147,483,647✔
409
    allow_rename: bool,
410
    mode: &impl util::TestingMode,
411
    stream: &mut impl Write,
2,147,483,647✔
412
    force: bool,
2,147,483,647✔
413
    dirs_to_create: &[DirToCreate],
2,147,483,647✔
414
) -> Result<bool, Error> {
2,147,483,647✔
415
    // Try a simple rename, which will only work within the same mount point.
2,147,483,647✔
416
    // Trying to rename across filesystems will throw errno 18.
417
    if allow_rename && fs::rename(target, dest).is_ok() {
2,147,483,647✔
418
        return Ok(true);
419
    }
2,147,483,647✔
420

421
    // If that didn't work, then we need to copy and rm.
422
    let created_dirs = create_dirs_for_copy(dirs_to_create)?;
2,147,483,647✔
423

2,147,483,647✔
424
    if fs::symlink_metadata(target)?.is_dir() {
×
425
        let moved = move_dir(target, dest, mode, stream, force)?;
2,147,483,647✔
426
        apply_dir_permissions(&created_dirs)?;
427
        Ok(moved)
428
    } else {
2,147,483,647✔
429
        let moved = copy_file(target, dest, mode, stream, force).map_err(|e| {
2,147,483,647✔
430
            Error::new(
431
                e.kind(),
432
                format!(
2,147,483,647✔
433
                    "Failed to copy file from {} to {}",
2,147,483,647✔
434
                    target.display(),
2,147,483,647✔
435
                    dest.display()
2,147,483,647✔
436
                ),
2,147,483,647✔
437
            )
438
        })?;
2,147,483,647✔
439
        fs::remove_file(target).map_err(|e| {
2,147,483,647✔
440
            Error::new(
441
                e.kind(),
442
                format!("Failed to remove file: {}", target.display()),
×
443
            )
444
        })?;
×
445
        apply_dir_permissions(&created_dirs)?;
446
        Ok(moved)
447
    }
×
448
}
449

450
/// Move a target which is a directory to a given destination, copying if necessary.
451
/// Returns true *always*, as the creation of the directory is enough to mark it as successful.
2,147,483,647✔
452
pub fn move_dir(
2,147,483,647✔
453
    target: &Path,
454
    dest: &Path,
2,147,483,647✔
455
    mode: &impl util::TestingMode,
2,147,483,647✔
456
    stream: &mut impl Write,
2,147,483,647✔
457
    force: bool,
2,147,483,647✔
458
) -> Result<bool, Error> {
2,147,483,647✔
459
    let mut dest_dirs_and_perms: Vec<(PathBuf, fs::Permissions)> = Vec::new();
460

2,147,483,647✔
461
    // Walk the source, creating directories and copying files as needed
2,147,483,647✔
462
    for entry in WalkDir::new(target).into_iter().filter_map(Result::ok) {
463
        // Path without the top-level directory
2,147,483,647✔
464
        let orphan = entry
2,147,483,647✔
465
            .path()
466
            .strip_prefix(target)
467
            .map_err(|_| Error::other("Parent directory isn't a prefix of child directories?"))?;
468

469
        if entry.file_type().is_dir() {
470
            let dest_dir = dest.join(orphan);
471
            fs::create_dir_all(&dest_dir).map_err(|e| {
472
                Error::new(
473
                    e.kind(),
2,147,483,647✔
474
                    format!(
475
                        "Failed to create dir: {} in {}",
476
                        entry.path().display(),
477
                        dest_dir.display()
478
                    ),
479
                )
2,147,483,647✔
480
            })?;
481

482
            // Preserve directory permissions, but apply after traversal so we can
483
            // still create children under non-writable directories.
484
            let source_metadata = fs::metadata(entry.path()).map_err(|e| {
485
                Error::new(
486
                    e.kind(),
487
                    format!("Failed to get metadata for: {}", entry.path().display()),
488
                )
489
            })?;
490
            dest_dirs_and_perms.push((dest_dir, source_metadata.permissions()));
491
        } else {
492
            copy_file(entry.path(), &dest.join(orphan), mode, stream, force).map_err(|e| {
493
                Error::new(
494
                    e.kind(),
495
                    format!(
496
                        "Failed to copy file from {} to {}",
497
                        entry.path().display(),
498
                        dest.join(orphan).display()
499
                    ),
500
                )
501
            })?;
502
        }
503
    }
504
    fs::remove_dir_all(target).map_err(|e| {
505
        Error::new(
506
            e.kind(),
507
            format!("Failed to remove dir: {}", target.display()),
508
        )
509
    })?;
510

511
    // Apply collected perms from leaf to root to minimize traversal surprises.
512
    for (dest_dir, perms) in dest_dirs_and_perms.into_iter().rev() {
513
        fs::set_permissions(&dest_dir, perms).map_err(|e| {
514
            Error::new(
515
                e.kind(),
516
                format!("Failed to set permissions on: {}", dest_dir.display()),
517
            )
518
        })?;
519
    }
520

521
    Ok(true)
522
}
523

524
pub fn copy_file(
525
    source: &Path,
526
    dest: &Path,
527
    mode: &impl util::TestingMode,
528
    stream: &mut impl Write,
529
    force: bool,
530
) -> Result<bool, Error> {
531
    let metadata = fs::symlink_metadata(source)?;
532
    let filetype = metadata.file_type();
533

534
    if metadata.len() > BIG_FILE_THRESHOLD {
535
        // In force mode, we default to copying big files
536
        if !force
537
            && util::prompt_yes(
538
                format!(
539
                    "About to copy a big file ({} is {})\nPermanently delete this file instead?",
540
                    source.display(),
541
                    util::humanize_bytes(metadata.len())
542
                ),
543
                mode,
544
                stream,
545
            )?
546
        {
547
            return Ok(false);
548
        }
549
    }
550

551
    if filetype.is_file() {
552
        fs::copy(source, dest)?;
553
        return Ok(true);
554
    }
555

556
    #[cfg(unix)]
557
    if filetype.is_fifo() {
558
        let perm: libc::mode_t = (metadata.permissions().mode() & 0o777) as libc::mode_t;
559
        let mode = Mode::from_bits_truncate(perm);
560

561
        mkfifo(dest, mode)?;
562
        return Ok(true);
563
    }
564

565
    if filetype.is_symlink() {
566
        let target = fs::read_link(source)?;
567
        symlink(target, dest)?;
568
        return Ok(true);
569
    }
570

571
    match fs::copy(source, dest) {
572
        Err(e) => {
573
            // Special file: Try copying it as normal, but this probably won't work
574
            // In force mode, we don't delete special files, we error
575
            if !force
576
                && util::prompt_yes(
577
                    format!(
578
                        "Non-regular file or directory: {}\nPermanently delete the file?",
579
                        source.display()
580
                    ),
581
                    mode,
582
                    stream,
583
                )?
584
            {
585
                Ok(false)
586
            } else {
587
                Err(e)
588
            }
589
        }
590
        Ok(_) => Ok(true),
591
    }
592
}
593

594
pub fn get_graveyard(graveyard: Option<PathBuf>) -> PathBuf {
595
    graveyard.unwrap_or_else(|| {
596
        if let Ok(env_graveyard) = env::var("RIP_GRAVEYARD") {
597
            PathBuf::from(env_graveyard)
598
        } else if let Ok(mut env_graveyard) = env::var("XDG_DATA_HOME") {
599
            if !env_graveyard.ends_with(std::path::MAIN_SEPARATOR) {
600
                env_graveyard.push(std::path::MAIN_SEPARATOR);
601
            }
602
            env_graveyard.push_str("graveyard");
603
            PathBuf::from(env_graveyard)
604
        } else {
605
            let user = util::get_user();
606
            env::temp_dir().join(format!("graveyard-{user}"))
607
        }
608
    })
609
}
610

611
/// Testing module for exposing internal functions to unit tests.
612
/// This module is only used for testing purposes and should not be used in production code.
613
pub mod testing {
614
    use super::{should_we_bury_this, util, Error, Metadata, Path, PathBuf, Write};
615

616
    pub fn testable_should_we_bury_this(
617
        target: &Path,
618
        source: &PathBuf,
619
        metadata: &Metadata,
620
        stream: &mut impl Write,
621
    ) -> Result<bool, Error> {
622
        should_we_bury_this(target, source, metadata, &util::TestMode, stream)
623
    }
624
}
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