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
42pub 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 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 seed ^= seed << 13;
57 seed ^= seed >> 7;
58 seed ^= seed << 17;
59 *byte = seed as u8;
60 }
61 }
62}
63
64pub fn shred_file(path: &Path, config: &ShredConfig) -> io::Result<()> {
66 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 let block = 512u64;
101 (file_size + block - 1) / block * block
102 };
103
104 let mut file = fs::OpenOptions::new().write(true).open(path)?;
105 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 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 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 if let Some(ref mode) = config.remove {
157 match mode {
158 RemoveMode::Wipe | RemoveMode::WipeSync => {
159 if let Some(parent) = path.parent() {
161 let name_len = path.file_name().map(|n| n.len()).unwrap_or(1);
162 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(¤t, &new_path).is_ok() {
169 if *mode == RemoveMode::WipeSync {
170 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(¤t)?;
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
202pub 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 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
236pub 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}