Skip to main content

sys_shred/core/
overwrite.rs

1//! # Low-Level Overwrite Engine
2//!
3//! This module implements the core byte-level overwriting logic. It is designed
4//! to be hardware-aware, utilizing `sync_all` to bypass Operating System
5//! write buffers and ensure data reaches the physical storage medium.
6
7use crate::cli::args::ShredMethod;
8use crate::error::{ShredError, ShredResult};
9use rand::{rngs::StdRng, RngCore, SeedableRng};
10use std::fs::File;
11use std::io::{Read, Seek, SeekFrom, Write};
12#[cfg(unix)]
13use std::os::unix::io::AsRawFd;
14use std::sync::atomic::{AtomicBool, Ordering};
15use std::sync::Arc;
16
17/// A specialized engine for performing secure cryptographic overwrites on a file.
18pub struct Overwriter<'a> {
19    file: &'a mut File,
20    buffer: Vec<u8>,
21    verify: bool,
22    cancelled: Arc<AtomicBool>,
23}
24
25impl<'a> Overwriter<'a> {
26    /// Creates a new `Overwriter` for a specific file handle.
27    pub fn new(file: &'a mut File, verify: bool, cancelled: Arc<AtomicBool>) -> Self {
28        // Apply OS-specific hints for aggressive cache bypassing
29        #[cfg(target_os = "macos")]
30        {
31            let fd = file.as_raw_fd();
32            unsafe {
33                // F_NOCACHE: Turns off caching for this file descriptor.
34                libc::fcntl(fd, libc::F_NOCACHE, 1);
35            }
36        }
37
38        #[cfg(target_os = "linux")]
39        {
40            let fd = file.as_raw_fd();
41            unsafe {
42                // POSIX_FADV_DONTNEED: Hint to the kernel that we don't need this data in cache.
43                libc::posix_fadvise(fd, 0, 0, libc::POSIX_FADV_DONTNEED);
44            }
45        }
46
47        let buffer_size = 64 * 1024;
48        Self {
49            file,
50            buffer: vec![0u8; buffer_size],
51            verify,
52            cancelled,
53        }
54    }
55
56    fn is_cancelled(&self) -> bool {
57        self.cancelled.load(Ordering::Relaxed)
58    }
59
60    /// Executes the full destruction sequence based on the selected method.
61    pub fn execute(&mut self, method: ShredMethod, passes: u32) -> ShredResult<()> {
62        match method {
63            ShredMethod::Zero => self.run_zero_pass()?,
64            ShredMethod::Random => {
65                for _ in 0..passes {
66                    if self.is_cancelled() {
67                        break;
68                    }
69                    self.run_random_pass()?;
70                }
71            }
72            ShredMethod::Dod => {
73                if !self.is_cancelled() {
74                    self.run_fixed_pass(0x00)?;
75                }
76                if !self.is_cancelled() {
77                    self.run_fixed_pass(0xFF)?;
78                }
79                if !self.is_cancelled() {
80                    self.run_random_pass()?;
81                }
82            }
83            ShredMethod::Gutmann => {
84                // Gutmann 35-pass sequence
85                for _ in 0..4 {
86                    if self.is_cancelled() {
87                        return Ok(());
88                    }
89                    self.run_random_pass()?;
90                }
91                let patterns = [
92                    0x55, 0xAA, 0x92, 0x49, 0x24, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
93                    0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x92, 0x49, 0x24, 0x6D, 0xB6,
94                    0xDB,
95                ];
96                for &p in &patterns {
97                    if self.is_cancelled() {
98                        return Ok(());
99                    }
100                    self.run_fixed_pass(p)?;
101                }
102                for _ in 0..4 {
103                    if self.is_cancelled() {
104                        return Ok(());
105                    }
106                    self.run_random_pass()?;
107                }
108            }
109        }
110        Ok(())
111    }
112
113    fn run_zero_pass(&mut self) -> ShredResult<()> {
114        self.run_fixed_pass(0x00)
115    }
116
117    fn run_fixed_pass(&mut self, byte: u8) -> ShredResult<()> {
118        let file_size = self.file.metadata()?.len();
119        self.file.seek(SeekFrom::Start(0))?;
120
121        // Fill the reusable buffer with the fixed byte
122        self.buffer.fill(byte);
123        let mut total_written: u64 = 0;
124        let buffer_len = self.buffer.len();
125
126        while total_written < file_size {
127            if self.is_cancelled() {
128                return Ok(());
129            }
130            let remaining = file_size - total_written;
131            let current_chunk = std::cmp::min(buffer_len as u64, remaining) as usize;
132            self.file.write_all(&self.buffer[..current_chunk])?;
133            total_written += current_chunk as u64;
134        }
135
136        self.file.sync_all()?;
137
138        if self.verify {
139            self.verify_pass(byte)?;
140        }
141
142        Ok(())
143    }
144
145    fn run_random_pass(&mut self) -> ShredResult<()> {
146        let file_size = self.file.metadata()?.len();
147        self.file.seek(SeekFrom::Start(0))?;
148
149        let mut rng = StdRng::from_entropy();
150        let mut total_written: u64 = 0;
151        let buffer_len = self.buffer.len();
152
153        while total_written < file_size {
154            if self.is_cancelled() {
155                return Ok(());
156            }
157            let remaining = file_size - total_written;
158            let current_chunk = std::cmp::min(buffer_len as u64, remaining) as usize;
159            rng.fill_bytes(&mut self.buffer[..current_chunk]);
160            self.file.write_all(&self.buffer[..current_chunk])?;
161            total_written += current_chunk as u64;
162        }
163
164        self.file.sync_all()?;
165        Ok(())
166    }
167
168    fn verify_pass(&mut self, expected_byte: u8) -> ShredResult<()> {
169        let file_size = self.file.metadata()?.len();
170        self.file.seek(SeekFrom::Start(0))?;
171
172        let mut total_read: u64 = 0;
173        let buffer_len = self.buffer.len();
174
175        while total_read < file_size {
176            if self.is_cancelled() {
177                return Ok(());
178            }
179            let remaining = file_size - total_read;
180            let current_chunk = std::cmp::min(buffer_len as u64, remaining) as usize;
181            self.file.read_exact(&mut self.buffer[..current_chunk])?;
182
183            for &byte in &self.buffer[..current_chunk] {
184                if self.is_cancelled() {
185                    return Ok(());
186                }
187                if byte != expected_byte {
188                    return Err(ShredError::Obfuscation(format!(
189                        "Verification failed: Expected 0x{:02X}, found 0x{:02X} at offset {}",
190                        expected_byte, byte, total_read
191                    )));
192                }
193            }
194            total_read += current_chunk as u64;
195        }
196        Ok(())
197    }
198}