Skip to main content

coreutils_rs/shred/
core.rs

1use std::fs;
2use std::io::{self, Seek, Write};
3use std::path::Path;
4
5/// How to remove files after shredding.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum RemoveMode {
8    /// Just unlink the file.
9    Unlink,
10    /// Wipe the filename by renaming before unlinking.
11    Wipe,
12    /// Wipe and sync before unlinking.
13    WipeSync,
14}
15
16/// Configuration for the shred operation.
17#[derive(Debug, Clone)]
18pub struct ShredConfig {
19    pub iterations: usize,
20    pub zero_pass: bool,
21    pub remove: Option<RemoveMode>,
22    pub force: bool,
23    pub verbose: bool,
24    pub exact: bool,
25    pub size: Option<u64>,
26}
27
28impl Default for ShredConfig {
29    fn default() -> Self {
30        Self {
31            iterations: 3,
32            zero_pass: false,
33            remove: None,
34            force: false,
35            verbose: false,
36            exact: false,
37            size: None,
38        }
39    }
40}
41
42/// Fill a buffer with random bytes from /dev/urandom.
43pub fn fill_random(buf: &mut [u8]) {
44    use std::fs::File;
45    use std::io::Read;
46    if let Ok(mut f) = File::open("/dev/urandom") {
47        let _ = f.read_exact(buf);
48    } else {
49        // Fallback: simple PRNG seeded from the clock
50        let mut seed: u64 = std::time::SystemTime::now()
51            .duration_since(std::time::UNIX_EPOCH)
52            .map(|d| d.as_nanos() as u64)
53            .unwrap_or(0x12345678);
54        for byte in buf.iter_mut() {
55            // xorshift64
56            seed ^= seed << 13;
57            seed ^= seed >> 7;
58            seed ^= seed << 17;
59            *byte = seed as u8;
60        }
61    }
62}
63
64/// Shred a single file according to the given configuration.
65pub fn shred_file(path: &Path, config: &ShredConfig) -> io::Result<()> {
66    // If force, make writable if needed
67    if config.force {
68        if let Ok(meta) = fs::metadata(path) {
69            let mut perms = meta.permissions();
70            #[cfg(unix)]
71            {
72                use std::os::unix::fs::PermissionsExt;
73                let mode = perms.mode();
74                if mode & 0o200 == 0 {
75                    perms.set_mode(mode | 0o200);
76                    let _ = fs::set_permissions(path, perms);
77                }
78            }
79            #[cfg(not(unix))]
80            {
81                #[allow(clippy::permissions_set_readonly_false)]
82                if perms.readonly() {
83                    perms.set_readonly(false);
84                    let _ = fs::set_permissions(path, perms);
85                }
86            }
87        }
88    }
89
90    let file_size = if let Some(s) = config.size {
91        s
92    } else {
93        fs::metadata(path)?.len()
94    };
95
96    let write_size = if config.exact {
97        file_size
98    } else {
99        // Round up to 512-byte block boundary
100        let block = 512u64;
101        (file_size + block - 1) / block * block
102    };
103
104    let mut file = fs::OpenOptions::new().write(true).open(path)?;
105    // Use 1MB buffer for fewer read/write syscalls (~16x fewer than 64KB)
106    let buf_size = 1024 * 1024usize;
107    let mut rng_buf = vec![0u8; buf_size];
108
109    let total_passes = config.iterations + if config.zero_pass { 1 } else { 0 };
110
111    // Random passes
112    for pass in 0..config.iterations {
113        if config.verbose {
114            eprintln!(
115                "shred: {}: pass {}/{} (random)...",
116                path.display(),
117                pass + 1,
118                total_passes
119            );
120        }
121        file.seek(io::SeekFrom::Start(0))?;
122        let mut remaining = write_size;
123        while remaining > 0 {
124            let chunk = remaining.min(rng_buf.len() as u64) as usize;
125            fill_random(&mut rng_buf[..chunk]);
126            file.write_all(&rng_buf[..chunk])?;
127            remaining -= chunk as u64;
128        }
129        file.sync_all()?;
130    }
131
132    // Zero pass
133    if config.zero_pass {
134        if config.verbose {
135            eprintln!(
136                "shred: {}: pass {}/{} (000000)...",
137                path.display(),
138                total_passes,
139                total_passes
140            );
141        }
142        file.seek(io::SeekFrom::Start(0))?;
143        let zeros = vec![0u8; buf_size];
144        let mut remaining = write_size;
145        while remaining > 0 {
146            let chunk = remaining.min(zeros.len() as u64) as usize;
147            file.write_all(&zeros[..chunk])?;
148            remaining -= chunk as u64;
149        }
150        file.sync_all()?;
151    }
152
153    drop(file);
154
155    // Remove file if requested
156    if let Some(ref mode) = config.remove {
157        match mode {
158            RemoveMode::Wipe | RemoveMode::WipeSync => {
159                // Try to rename the file to obscure the name before removing
160                if let Some(parent) = path.parent() {
161                    let name_len = path.file_name().map(|n| n.len()).unwrap_or(1);
162                    // Rename to progressively shorter names
163                    let mut current = path.to_path_buf();
164                    let mut len = name_len;
165                    while len > 0 {
166                        let new_name: String = std::iter::repeat_n('0', len).collect();
167                        let new_path = parent.join(&new_name);
168                        if fs::rename(&current, &new_path).is_ok() {
169                            if *mode == RemoveMode::WipeSync {
170                                // Sync the directory
171                                if let Ok(dir) = fs::File::open(parent) {
172                                    let _ = dir.sync_all();
173                                }
174                            }
175                            current = new_path;
176                        }
177                        len /= 2;
178                    }
179                    if config.verbose {
180                        eprintln!("shred: {}: removed", path.display());
181                    }
182                    fs::remove_file(&current)?;
183                } else {
184                    if config.verbose {
185                        eprintln!("shred: {}: removed", path.display());
186                    }
187                    fs::remove_file(path)?;
188                }
189            }
190            RemoveMode::Unlink => {
191                if config.verbose {
192                    eprintln!("shred: {}: removed", path.display());
193                }
194                fs::remove_file(path)?;
195            }
196        }
197    }
198
199    Ok(())
200}
201
202/// Parse a size string with optional suffix (K, M, G, etc.).
203pub fn parse_size(s: &str) -> Result<u64, String> {
204    if s.is_empty() {
205        return Err("invalid size: ''".to_string());
206    }
207
208    let s = s.trim();
209
210    // Check for suffix
211    let (num_str, multiplier) = if s.ends_with("GB") || s.ends_with("gB") {
212        (&s[..s.len() - 2], 1_000_000_000u64)
213    } else if s.ends_with("MB") {
214        (&s[..s.len() - 2], 1_000_000u64)
215    } else if s.ends_with("KB") {
216        (&s[..s.len() - 2], 1_000u64)
217    } else if s.ends_with('G') || s.ends_with('g') {
218        (&s[..s.len() - 1], 1_073_741_824u64)
219    } else if s.ends_with('M') || s.ends_with('m') {
220        (&s[..s.len() - 1], 1_048_576u64)
221    } else if s.ends_with('K') || s.ends_with('k') {
222        (&s[..s.len() - 1], 1_024u64)
223    } else {
224        (s, 1u64)
225    };
226
227    let value: u64 = num_str
228        .parse()
229        .map_err(|_| format!("invalid size: '{}'", s))?;
230
231    value
232        .checked_mul(multiplier)
233        .ok_or_else(|| format!("size too large: '{}'", s))
234}
235
236/// Parse a --remove[=HOW] argument.
237pub fn parse_remove_mode(arg: &str) -> Result<RemoveMode, String> {
238    if arg == "--remove" || arg == "-u" {
239        Ok(RemoveMode::WipeSync)
240    } else if let Some(how) = arg.strip_prefix("--remove=") {
241        match how {
242            "unlink" => Ok(RemoveMode::Unlink),
243            "wipe" => Ok(RemoveMode::Wipe),
244            "wipesync" => Ok(RemoveMode::WipeSync),
245            _ => Err(format!(
246                "invalid argument '{}' for '--remove'\nValid arguments are:\n  - 'unlink'\n  - 'wipe'\n  - 'wipesync'",
247                how
248            )),
249        }
250    } else {
251        Err(format!("unrecognized option '{}'", arg))
252    }
253}