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

MilesCranmer / rip2 / #805

03 Sep 2025 06:42AM UTC coverage: 68.603% (+5.6%) from 62.996%
#805

push

378 of 551 relevant lines covered (68.6%)

188.79 hits per line

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

76.19
/src/util.rs
1
use std::collections::hash_map::DefaultHasher;
2
use std::env;
3
use std::fs;
4
use std::hash::{Hash, Hasher};
5
use std::io::{self, BufReader, Error, Read, Write};
6
use std::path::Prefix::Disk;
7
use std::path::{Component, Path, PathBuf};
8
use std::str::from_utf8;
9

10
fn hash_component(c: &Component) -> String {
×
11
    let mut hasher = DefaultHasher::new();
×
12
    c.hash(&mut hasher);
×
13
    format!("{:x}", hasher.finish())
×
14
}
15
pub fn str_component(c: &Component) -> String {
×
16
    match c {
×
17
        Component::Prefix(prefix) => match prefix.kind() {
×
18
            // C:\\ is the most common, so we just make a readable name for it.
19
            Disk(disk) => format!("DISK_{}", from_utf8(&[disk]).unwrap()),
×
20
            _ => hash_component(c),
×
21
        },
22
        _ => hash_component(c),
×
23
    }
24
}
25

26
/// Process a path component and push it to the destination if needed
27
/// Returns true if the component was pushed, false if it was skipped
28
pub fn push_component_to_dest(dest: &mut PathBuf, component: &Component) -> bool {
1,008✔
29
    match component {
1,008✔
30
        Component::RootDir => false, // Skip root dir
192✔
31
        Component::Prefix(_) => {
32
            // Hash the prefix component.
33
            // We do this because there are many ways to get prefix components
34
            // on Windows, so its safer to simply hash it.
35
            dest.push(str_component(component));
×
36
            true
×
37
        }
38
        _ => {
39
            dest.push(component);
2,448✔
40
            true
816✔
41
        }
42
    }
43
}
44

45
/// Concatenate two paths, even if the right argument is an absolute path.
46
pub fn join_absolute<A: AsRef<Path>, B: AsRef<Path>>(left: A, right: B) -> PathBuf {
48✔
47
    let (left, right) = (left.as_ref(), right.as_ref());
240✔
48
    let mut result = left.to_path_buf();
144✔
49
    for c in right.components() {
343✔
50
        push_component_to_dest(&mut result, &c);
×
51
    }
52
    result
48✔
53
}
54

55
pub fn symlink_exists<P: AsRef<Path>>(path: P) -> bool {
90✔
56
    fs::symlink_metadata(path).is_ok()
180✔
57
}
58

59
pub fn get_user() -> String {
14✔
60
    #[cfg(unix)]
61
    {
62
        env::var("USER").unwrap_or_else(|_| String::from("unknown"))
28✔
63
    }
64
    #[cfg(target_os = "windows")]
65
    {
66
        env::var("USERNAME").unwrap_or_else(|_| String::from("unknown"))
67
    }
68
}
69

70
// Allows injection of test-specific behavior
71
pub trait TestingMode {
72
    fn is_test(&self) -> bool;
73
}
74

75
pub struct ProductionMode;
76
pub struct TestMode;
77

78
impl TestingMode for ProductionMode {
79
    fn is_test(&self) -> bool {
5✔
80
        false
5✔
81
    }
82
}
83
impl TestingMode for TestMode {
84
    fn is_test(&self) -> bool {
16✔
85
        true
16✔
86
    }
87
}
88

89
pub fn allow_rename() -> bool {
126✔
90
    // Test behavior to skip simple rename
91
    env::var_os("__RIP_ALLOW_RENAME").is_none_or(|v| v != "false")
304✔
92
}
93

94
/// Prompt for user input, returning True if the first character is 'y' or 'Y'
95
/// Will create an error if given a 'q' or 'Q', equivalent to if the user
96
/// had passed a SIGINT.
97
pub fn prompt_yes(
21✔
98
    prompt: impl AsRef<str>,
99
    source: &impl TestingMode,
100
    stream: &mut impl Write,
101
) -> Result<bool, Error> {
102
    write!(stream, "{} (y/N) ", prompt.as_ref())?;
105✔
103
    if stream.flush().is_err() {
21✔
104
        // If stdout wasn't flushed properly, fallback to println
105
        writeln!(stream, "{} (y/N)", prompt.as_ref())?;
×
106
    }
107

108
    if source.is_test() {
21✔
109
        return Ok(true);
16✔
110
    }
111

112
    yes_no_quit(io::stdin())
×
113
}
114

115
pub fn yes_no_quit(in_stream: impl Read) -> Result<bool, Error> {
14✔
116
    let buffered = BufReader::new(in_stream);
42✔
117
    let char_result = buffered
28✔
118
        .bytes()
119
        .next()
120
        .and_then(Result::ok)
14✔
121
        .map(|c| c as char);
27✔
122

123
    match char_result {
14✔
124
        Some('y' | 'Y') => Ok(true),
4✔
125
        Some('n' | 'N' | '\n') | None => Ok(false),
6✔
126
        Some('q' | 'Q') => Err(Error::new(
3✔
127
            io::ErrorKind::Interrupted,
3✔
128
            "User requested to quit",
3✔
129
        )),
130
        _ => Err(Error::new(io::ErrorKind::InvalidInput, "Invalid input")),
1✔
131
    }
132
}
133

134
/// Add a numbered extension to duplicate filenames to avoid overwriting files.
135
pub fn rename_grave(grave: impl AsRef<Path>) -> PathBuf {
8✔
136
    let grave = grave.as_ref();
24✔
137
    let name = grave.to_str().expect("Filename must be valid unicode.");
40✔
138
    (1_u64..)
8✔
139
        .map(|i| PathBuf::from(format!("{name}~{i}")))
32✔
140
        .find(|p| !symlink_exists(p))
24✔
141
        .expect("Failed to rename duplicate file or directory")
142
}
143

144
const UNITS: [(&str, u64); 4] = [
145
    ("KiB", 1_u64 << 10),
146
    ("MiB", 1_u64 << 20),
147
    ("GiB", 1_u64 << 30),
148
    ("TiB", 1_u64 << 40),
149
];
150

151
pub fn humanize_bytes(bytes: u64) -> String {
50✔
152
    for (unit, size) in UNITS.iter().rev() {
502✔
153
        if bytes >= *size {
176✔
154
            #[allow(clippy::cast_precision_loss)]
155
            return format!("{:.1} {}", bytes as f64 / *size as f64, unit);
34✔
156
        }
157
    }
158
    format!("{bytes} B")
16✔
159
}
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