coreutils_rs/shred/
core.rs1use std::fs;
2use std::io::{self, Seek, Write};
3use std::path::Path;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum RemoveMode {
8 Unlink,
10 Wipe,
12 WipeSync,
14}
15
16#[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
42struct FastRng {
46 s0: u64,
47 s1: u64,
48}
49
50impl FastRng {
51 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 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 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 fn fill(&mut self, buf: &mut [u8]) {
88 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 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
106pub fn fill_random(buf: &mut [u8]) {
108 let mut rng = FastRng::new();
109 rng.fill(buf);
110}
111
112pub fn shred_file(path: &Path, config: &ShredConfig) -> io::Result<()> {
114 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 let block = 512u64;
149 (file_size + block - 1) / block * block
150 };
151
152 let mut file = fs::OpenOptions::new().write(true).open(path)?;
153 let buf_size = 1024 * 1024usize;
155 let mut rng_buf = vec![0u8; buf_size];
156
157 let mut rng = FastRng::new();
159
160 let total_passes = config.iterations + if config.zero_pass { 1 } else { 0 };
161
162 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 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 if let Some(ref mode) = config.remove {
208 match mode {
209 RemoveMode::Wipe | RemoveMode::WipeSync => {
210 if let Some(parent) = path.parent() {
212 let name_len = path.file_name().map(|n| n.len()).unwrap_or(1);
213 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(¤t, &new_path).is_ok() {
220 if *mode == RemoveMode::WipeSync {
221 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(¤t)?;
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
253pub 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 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
287pub 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}