Skip to main content

linuxutils_system/
blkdiscard.rs

1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use std::{fs::File, io, os::unix::io::AsRawFd, process::ExitCode};
7
8const BLKGETSIZE64: libc::c_ulong = 0x80081272;
9const BLKSSZGET: libc::c_ulong = 0x1268;
10const BLKDISCARD: libc::c_ulong = 0x1277;
11const BLKSECDISCARD: libc::c_ulong = 0x127d;
12const BLKZEROOUT: libc::c_ulong = 0x127f;
13
14/// Discard sectors on a block device.
15///
16/// Sends BLKDISCARD, BLKSECDISCARD, or BLKZEROOUT ioctls to tell the
17/// storage device that a range of blocks is no longer in use.
18#[derive(Parser)]
19#[command(name = "blkdiscard", about = "Discard sectors on a block device")]
20pub struct Args {
21    /// Disable exclusive open mode (O_EXCL)
22    #[arg(short = 'f', long)]
23    force: bool,
24
25    /// Byte offset to start discarding (default: 0)
26    #[arg(short = 'o', long, default_value = "0")]
27    offset: u64,
28
29    /// Number of bytes to discard (default: to end of device)
30    #[arg(short = 'l', long)]
31    length: Option<u64>,
32
33    /// Bytes to discard per ioctl call (default: all at once)
34    #[arg(short = 'p', long)]
35    step: Option<u64>,
36
37    /// Suppress warnings
38    #[arg(short = 'q', long)]
39    quiet: bool,
40
41    /// Secure discard (BLKSECDISCARD)
42    #[arg(short = 's', long)]
43    secure: bool,
44
45    /// Zero-fill instead of discard (BLKZEROOUT)
46    #[arg(short = 'z', long)]
47    zeroout: bool,
48
49    /// Show progress
50    #[arg(short = 'v', long)]
51    verbose: bool,
52
53    /// Block device to operate on
54    device: String,
55}
56
57fn get_device_size(fd: i32) -> io::Result<u64> {
58    let mut size: u64 = 0;
59    if unsafe { libc::ioctl(fd, BLKGETSIZE64, &mut size) } < 0 {
60        Err(io::Error::last_os_error())
61    } else {
62        Ok(size)
63    }
64}
65
66fn get_sector_size(fd: i32) -> io::Result<u32> {
67    let mut ss: libc::c_int = 0;
68    if unsafe { libc::ioctl(fd, BLKSSZGET, &mut ss) } < 0 {
69        Err(io::Error::last_os_error())
70    } else {
71        Ok(ss as u32)
72    }
73}
74
75fn do_discard(
76    fd: i32,
77    ioctl_nr: libc::c_ulong,
78    offset: u64,
79    length: u64,
80) -> io::Result<()> {
81    let range: [u64; 2] = [offset, length];
82    if unsafe { libc::ioctl(fd, ioctl_nr, &range) } < 0 {
83        Err(io::Error::last_os_error())
84    } else {
85        Ok(())
86    }
87}
88
89pub fn run(args: Args) -> ExitCode {
90    if args.secure && args.zeroout {
91        eprintln!("blkdiscard: --secure and --zeroout are mutually exclusive");
92        return ExitCode::FAILURE;
93    }
94
95    let oflags = if args.force {
96        libc::O_RDWR
97    } else {
98        libc::O_RDWR | libc::O_EXCL
99    };
100
101    let dev_cstr = match std::ffi::CString::new(args.device.as_str()) {
102        Ok(c) => c,
103        Err(_) => {
104            eprintln!("blkdiscard: invalid device path");
105            return ExitCode::FAILURE;
106        }
107    };
108
109    let fd = unsafe { libc::open(dev_cstr.as_ptr(), oflags) };
110    if fd < 0 {
111        eprintln!(
112            "blkdiscard: cannot open '{}': {}",
113            args.device,
114            io::Error::last_os_error()
115        );
116        return ExitCode::FAILURE;
117    }
118
119    // Wrap fd for RAII close (via File)
120    let file = unsafe { File::from_raw_fd(fd) };
121    let fd = file.as_raw_fd();
122
123    let dev_size = match get_device_size(fd) {
124        Ok(s) => s,
125        Err(e) => {
126            eprintln!("blkdiscard: cannot get device size: {e}");
127            return ExitCode::FAILURE;
128        }
129    };
130
131    let sector_size = get_sector_size(fd).unwrap_or(512) as u64;
132
133    let offset = args.offset;
134    let length = args.length.unwrap_or(dev_size.saturating_sub(offset));
135
136    if !offset.is_multiple_of(sector_size) {
137        eprintln!(
138            "blkdiscard: offset {offset} is not aligned to sector size {sector_size}"
139        );
140        return ExitCode::FAILURE;
141    }
142    if !length.is_multiple_of(sector_size) {
143        eprintln!(
144            "blkdiscard: length {length} is not aligned to sector size {sector_size}"
145        );
146        return ExitCode::FAILURE;
147    }
148
149    let ioctl_nr = if args.secure {
150        BLKSECDISCARD
151    } else if args.zeroout {
152        BLKZEROOUT
153    } else {
154        BLKDISCARD
155    };
156
157    let action = if args.secure {
158        "secure discard"
159    } else if args.zeroout {
160        "zeroout"
161    } else {
162        "discard"
163    };
164
165    if args.verbose {
166        eprintln!(
167            "blkdiscard: {action} offset={offset} length={length} device={}",
168            args.device
169        );
170    }
171
172    if let Some(step) = args.step {
173        let mut current = offset;
174        let end = offset + length;
175        while current < end {
176            let chunk = step.min(end - current);
177            if let Err(e) = do_discard(fd, ioctl_nr, current, chunk) {
178                eprintln!(
179                    "blkdiscard: {action} failed at offset {current}: {e}"
180                );
181                return if e.raw_os_error() == Some(libc::EOPNOTSUPP) {
182                    ExitCode::from(2)
183                } else {
184                    ExitCode::FAILURE
185                };
186            }
187            current += chunk;
188        }
189    } else if let Err(e) = do_discard(fd, ioctl_nr, offset, length) {
190        eprintln!("blkdiscard: {action} failed: {e}");
191        return if e.raw_os_error() == Some(libc::EOPNOTSUPP) {
192            ExitCode::from(2)
193        } else {
194            ExitCode::FAILURE
195        };
196    }
197
198    ExitCode::SUCCESS
199}
200
201use std::os::unix::io::FromRawFd;