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/// Fast userspace PRNG (xorshift128+) for shred data generation.
43/// Seeded from /dev/urandom once, then generates all random data in userspace.
44/// This is sufficient for shred's purpose (overwriting data to prevent recovery).
45struct FastRng {
46    s0: u64,
47    s1: u64,
48}
49
50impl FastRng {
51    /// Create a new PRNG seeded from /dev/urandom.
52    fn new() -> Self {
53        use std::io::Read;
54        let mut seed = [0u8; 16];
55        if let Ok(mut f) = std::fs::File::open("/dev/urandom") {
56            let _ = f.read_exact(&mut seed);
57        } else {
58            // Fallback: seed from clock
59            let t = std::time::SystemTime::now()
60                .duration_since(std::time::UNIX_EPOCH)
61                .map(|d| d.as_nanos() as u64)
62                .unwrap_or(0x12345678);
63            seed[..8].copy_from_slice(&t.to_le_bytes());
64            seed[8..].copy_from_slice(&(t.wrapping_mul(0x9E3779B97F4A7C15)).to_le_bytes());
65        }
66        let s0 = u64::from_le_bytes(seed[..8].try_into().unwrap());
67        let s1 = u64::from_le_bytes(seed[8..].try_into().unwrap());
68        // Ensure not all-zero state
69        Self {
70            s0: if s0 == 0 { 0x12345678 } else { s0 },
71            s1: if s1 == 0 { 0x87654321 } else { s1 },
72        }
73    }
74
75    #[inline]
76    fn next_u64(&mut self) -> u64 {
77        let mut s1 = self.s0;
78        let s0 = self.s1;
79        let result = s0.wrapping_add(s1);
80        self.s0 = s0;
81        s1 ^= s1 << 23;
82        self.s1 = s1 ^ s0 ^ (s1 >> 18) ^ (s0 >> 5);
83        result
84    }
85
86    /// Fill a buffer with random bytes entirely in userspace.
87    fn fill(&mut self, buf: &mut [u8]) {
88        // Fill 8 bytes at a time
89        let chunks = buf.len() / 8;
90        let ptr = buf.as_mut_ptr() as *mut u64;
91        for i in 0..chunks {
92            unsafe { ptr.add(i).write_unaligned(self.next_u64()) };
93        }
94        // Fill remaining bytes
95        let remaining = buf.len() % 8;
96        if remaining > 0 {
97            let val = self.next_u64();
98            let start = chunks * 8;
99            for j in 0..remaining {
100                buf[start + j] = (val >> (j * 8)) as u8;
101            }
102        }
103    }
104}
105
106/// Fill a buffer with random bytes using a fast userspace PRNG.
107pub fn fill_random(buf: &mut [u8]) {
108    let mut rng = FastRng::new();
109    rng.fill(buf);
110}
111
112/// Shred a single file according to the given configuration.
113pub fn shred_file(path: &Path, config: &ShredConfig) -> io::Result<()> {
114    // If force, make writable if needed
115    if config.force {
116        if let Ok(meta) = fs::metadata(path) {
117            let mut perms = meta.permissions();
118            #[cfg(unix)]
119            {
120                use std::os::unix::fs::PermissionsExt;
121                let mode = perms.mode();
122                if mode & 0o200 == 0 {
123                    perms.set_mode(mode | 0o200);
124                    let _ = fs::set_permissions(path, perms);
125                }
126            }
127            #[cfg(not(unix))]
128            {
129                #[allow(clippy::permissions_set_readonly_false)]
130                if perms.readonly() {
131                    perms.set_readonly(false);
132                    let _ = fs::set_permissions(path, perms);
133                }
134            }
135        }
136    }
137
138    let file_size = if let Some(s) = config.size {
139        s
140    } else {
141        fs::metadata(path)?.len()
142    };
143
144    let write_size = if config.exact {
145        file_size
146    } else {
147        // Round up to 512-byte block boundary
148        let block = 512u64;
149        (file_size + block - 1) / block * block
150    };
151
152    let mut file = fs::OpenOptions::new().write(true).open(path)?;
153    // Use 1MB buffer for fewer read/write syscalls
154    let buf_size = 1024 * 1024usize;
155    let mut rng_buf = vec![0u8; buf_size];
156
157    // Create PRNG once and reuse across all passes (seeded from /dev/urandom)
158    let mut rng = FastRng::new();
159
160    let total_passes = config.iterations + if config.zero_pass { 1 } else { 0 };
161
162    // Random passes
163    for pass in 0..config.iterations {
164        if config.verbose {
165            eprintln!(
166                "shred: {}: pass {}/{} (random)...",
167                path.display(),
168                pass + 1,
169                total_passes
170            );
171        }
172        file.seek(io::SeekFrom::Start(0))?;
173        let mut remaining = write_size;
174        while remaining > 0 {
175            let chunk = remaining.min(rng_buf.len() as u64) as usize;
176            rng.fill(&mut rng_buf[..chunk]);
177            file.write_all(&rng_buf[..chunk])?;
178            remaining -= chunk as u64;
179        }
180        file.sync_data()?;
181    }
182
183    // Zero pass
184    if config.zero_pass {
185        if config.verbose {
186            eprintln!(
187                "shred: {}: pass {}/{} (000000)...",
188                path.display(),
189                total_passes,
190                total_passes
191            );
192        }
193        file.seek(io::SeekFrom::Start(0))?;
194        let zeros = vec![0u8; buf_size];
195        let mut remaining = write_size;
196        while remaining > 0 {
197            let chunk = remaining.min(zeros.len() as u64) as usize;
198            file.write_all(&zeros[..chunk])?;
199            remaining -= chunk as u64;
200        }
201        file.sync_data()?;
202    }
203
204    drop(file);
205
206    // Remove file if requested
207    if let Some(ref mode) = config.remove {
208        match mode {
209            RemoveMode::Wipe | RemoveMode::WipeSync => {
210                // Try to rename the file to obscure the name before removing
211                if let Some(parent) = path.parent() {
212                    let name_len = path.file_name().map(|n| n.len()).unwrap_or(1);
213                    // Rename to progressively shorter names
214                    let mut current = path.to_path_buf();
215                    let mut len = name_len;
216                    while len > 0 {
217                        let new_name: String = std::iter::repeat_n('0', len).collect();
218                        let new_path = parent.join(&new_name);
219                        if fs::rename(&current, &new_path).is_ok() {
220                            if *mode == RemoveMode::WipeSync {
221                                // Sync the directory
222                                if let Ok(dir) = fs::File::open(parent) {
223                                    let _ = dir.sync_all();
224                                }
225                            }
226                            current = new_path;
227                        }
228                        len /= 2;
229                    }
230                    if config.verbose {
231                        eprintln!("shred: {}: removed", path.display());
232                    }
233                    fs::remove_file(&current)?;
234                } else {
235                    if config.verbose {
236                        eprintln!("shred: {}: removed", path.display());
237                    }
238                    fs::remove_file(path)?;
239                }
240            }
241            RemoveMode::Unlink => {
242                if config.verbose {
243                    eprintln!("shred: {}: removed", path.display());
244                }
245                fs::remove_file(path)?;
246            }
247        }
248    }
249
250    Ok(())
251}
252
253/// Parse a size string with optional suffix (K, M, G, etc.).
254pub fn parse_size(s: &str) -> Result<u64, String> {
255    if s.is_empty() {
256        return Err("invalid size: ''".to_string());
257    }
258
259    let s = s.trim();
260
261    // Check for suffix
262    let (num_str, multiplier) = if s.ends_with("GB") || s.ends_with("gB") {
263        (&s[..s.len() - 2], 1_000_000_000u64)
264    } else if s.ends_with("MB") {
265        (&s[..s.len() - 2], 1_000_000u64)
266    } else if s.ends_with("KB") {
267        (&s[..s.len() - 2], 1_000u64)
268    } else if s.ends_with('G') || s.ends_with('g') {
269        (&s[..s.len() - 1], 1_073_741_824u64)
270    } else if s.ends_with('M') || s.ends_with('m') {
271        (&s[..s.len() - 1], 1_048_576u64)
272    } else if s.ends_with('K') || s.ends_with('k') {
273        (&s[..s.len() - 1], 1_024u64)
274    } else {
275        (s, 1u64)
276    };
277
278    let value: u64 = num_str
279        .parse()
280        .map_err(|_| format!("invalid size: '{}'", s))?;
281
282    value
283        .checked_mul(multiplier)
284        .ok_or_else(|| format!("size too large: '{}'", s))
285}
286
287/// Parse a --remove[=HOW] argument.
288pub fn parse_remove_mode(arg: &str) -> Result<RemoveMode, String> {
289    if arg == "--remove" || arg == "-u" {
290        Ok(RemoveMode::WipeSync)
291    } else if let Some(how) = arg.strip_prefix("--remove=") {
292        match how {
293            "unlink" => Ok(RemoveMode::Unlink),
294            "wipe" => Ok(RemoveMode::Wipe),
295            "wipesync" => Ok(RemoveMode::WipeSync),
296            _ => Err(format!(
297                "invalid argument '{}' for '--remove'\nValid arguments are:\n  - 'unlink'\n  - 'wipe'\n  - 'wipesync'",
298                how
299            )),
300        }
301    } else {
302        Err(format!("unrecognized option '{}'", arg))
303    }
304}