Skip to main content

linuxutils_disk/
blockdev.rs

1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use cols::{Cols, print_table};
7use std::{
8    fs::File,
9    io::{self, BufRead},
10    os::unix::io::AsRawFd,
11    process::ExitCode,
12};
13
14// Block device ioctls
15const BLKROSET: libc::c_ulong = 0x125d;
16const BLKROGET: libc::c_ulong = 0x125e;
17const BLKRRPART: libc::c_ulong = 0x125f;
18const BLKGETSIZE: libc::c_ulong = 0x1260;
19const BLKFLSBUF: libc::c_ulong = 0x1261;
20const BLKRASET: libc::c_ulong = 0x1262;
21const BLKRAGET: libc::c_ulong = 0x1263;
22const BLKFRASET: libc::c_ulong = 0x1264;
23const BLKFRAGET: libc::c_ulong = 0x1265;
24const BLKSECTGET: libc::c_ulong = 0x1267;
25const BLKSSZGET: libc::c_ulong = 0x1268;
26const BLKBSZGET: libc::c_ulong = 0x80081270;
27const BLKBSZSET: libc::c_ulong = 0x40081271;
28const BLKGETSIZE64: libc::c_ulong = 0x80081272;
29const BLKIOMIN: libc::c_ulong = 0x1278;
30const BLKIOOPT: libc::c_ulong = 0x1279;
31const BLKALIGNOFF: libc::c_ulong = 0x127a;
32const BLKPBSZGET: libc::c_ulong = 0x127b;
33const BLKDISCARDZEROES: libc::c_ulong = 0x127c;
34
35/// Call block device ioctls from the command line.
36///
37/// Each `--getXXX` or `--setXXX` command performs a single ioctl on
38/// the specified block device(s). Multiple commands and devices can be
39/// given in a single invocation.
40#[derive(Parser)]
41#[command(
42    name = "blockdev",
43    about = "Call block device ioctls from the command line",
44    override_usage = "blockdev [-q] [-v] command [command...] device [device...]\n       \
45                      blockdev --report [device...]"
46)]
47pub struct Args {
48    /// Commands, arguments, and device paths
49    #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
50    args: Vec<String>,
51}
52
53enum Cmd {
54    GetInt(libc::c_ulong, &'static str),
55    GetUint(libc::c_ulong, &'static str),
56    GetU64(libc::c_ulong, &'static str),
57    GetUlong(libc::c_ulong, &'static str),
58    GetSz,
59    SetInt(libc::c_ulong, libc::c_int, &'static str),
60    SetUlong(libc::c_ulong, libc::c_ulong, &'static str),
61    Action(libc::c_ulong, &'static str),
62}
63
64fn ioctl_get_int(fd: i32, nr: libc::c_ulong) -> io::Result<i64> {
65    let mut val: libc::c_int = 0;
66    if unsafe { libc::ioctl(fd, nr, &mut val) } < 0 {
67        Err(io::Error::last_os_error())
68    } else {
69        Ok(val as i64)
70    }
71}
72
73fn ioctl_get_uint(fd: i32, nr: libc::c_ulong) -> io::Result<i64> {
74    let mut val: libc::c_uint = 0;
75    if unsafe { libc::ioctl(fd, nr, &mut val) } < 0 {
76        Err(io::Error::last_os_error())
77    } else {
78        Ok(val as i64)
79    }
80}
81
82fn ioctl_get_u64(fd: i32, nr: libc::c_ulong) -> io::Result<u64> {
83    let mut val: u64 = 0;
84    if unsafe { libc::ioctl(fd, nr, &mut val) } < 0 {
85        Err(io::Error::last_os_error())
86    } else {
87        Ok(val)
88    }
89}
90
91fn ioctl_get_ulong(fd: i32, nr: libc::c_ulong) -> io::Result<i64> {
92    let mut val: libc::c_ulong = 0;
93    if unsafe { libc::ioctl(fd, nr, &mut val) } < 0 {
94        Err(io::Error::last_os_error())
95    } else {
96        Ok(val as i64)
97    }
98}
99
100fn ioctl_set_int(
101    fd: i32,
102    nr: libc::c_ulong,
103    val: libc::c_int,
104) -> io::Result<()> {
105    if unsafe { libc::ioctl(fd, nr, &val) } < 0 {
106        Err(io::Error::last_os_error())
107    } else {
108        Ok(())
109    }
110}
111
112fn ioctl_set_ulong(
113    fd: i32,
114    nr: libc::c_ulong,
115    val: libc::c_ulong,
116) -> io::Result<()> {
117    if unsafe { libc::ioctl(fd, nr, val) } < 0 {
118        Err(io::Error::last_os_error())
119    } else {
120        Ok(())
121    }
122}
123
124fn ioctl_action(fd: i32, nr: libc::c_ulong) -> io::Result<()> {
125    if unsafe { libc::ioctl(fd, nr, 0) } < 0 {
126        Err(io::Error::last_os_error())
127    } else {
128        Ok(())
129    }
130}
131
132fn execute_cmd(
133    fd: i32,
134    cmd: &Cmd,
135    verbose: bool,
136    quiet: bool,
137) -> io::Result<()> {
138    match cmd {
139        Cmd::GetInt(nr, name) => {
140            let val = ioctl_get_int(fd, *nr)?;
141            if verbose {
142                print!("{name}: ");
143            }
144            println!("{val}");
145        }
146        Cmd::GetUint(nr, name) => {
147            let val = ioctl_get_uint(fd, *nr)?;
148            if verbose {
149                print!("{name}: ");
150            }
151            println!("{val}");
152        }
153        Cmd::GetU64(nr, name) => {
154            let val = ioctl_get_u64(fd, *nr)?;
155            if verbose {
156                print!("{name}: ");
157            }
158            println!("{val}");
159        }
160        Cmd::GetUlong(nr, name) => {
161            let val = ioctl_get_ulong(fd, *nr)?;
162            if verbose {
163                print!("{name}: ");
164            }
165            println!("{val}");
166        }
167        Cmd::GetSz => {
168            let bytes = ioctl_get_u64(fd, BLKGETSIZE64)?;
169            if verbose {
170                print!("getsz: ");
171            }
172            println!("{}", bytes / 512);
173        }
174        Cmd::SetInt(nr, val, name) => {
175            ioctl_set_int(fd, *nr, *val)?;
176            if !quiet {
177                eprintln!("{name} succeeded.");
178            }
179        }
180        Cmd::SetUlong(nr, val, name) => {
181            ioctl_set_ulong(fd, *nr, *val)?;
182            if !quiet {
183                eprintln!("{name} succeeded.");
184            }
185        }
186        Cmd::Action(nr, name) => {
187            ioctl_action(fd, *nr)?;
188            if !quiet {
189                eprintln!("{name} succeeded.");
190            }
191        }
192    }
193    Ok(())
194}
195
196fn get_start_sector(device: &str) -> u64 {
197    let Ok(f) = File::open(device) else {
198        return 0;
199    };
200    let mut stat: libc::stat = unsafe { std::mem::zeroed() };
201    if unsafe { libc::fstat(f.as_raw_fd(), &mut stat) } < 0 {
202        return 0;
203    }
204    let major = libc::major(stat.st_rdev as libc::dev_t);
205    let minor = libc::minor(stat.st_rdev as libc::dev_t);
206    let path = format!("/sys/dev/block/{major}:{minor}/start");
207    std::fs::read_to_string(path)
208        .ok()
209        .and_then(|s| s.trim().parse().ok())
210        .unwrap_or(0)
211}
212
213fn devices_from_proc_partitions() -> Vec<String> {
214    let mut devices = Vec::new();
215    let Ok(file) = File::open("/proc/partitions") else {
216        return devices;
217    };
218    for line in io::BufReader::new(file)
219        .lines()
220        .map_while(Result::ok)
221        .skip(2)
222    {
223        let fields: Vec<&str> = line.split_whitespace().collect();
224        if fields.len() >= 4 {
225            devices.push(format!("/dev/{}", fields[3]));
226        }
227    }
228    devices
229}
230
231#[derive(Cols)]
232struct ReportRow {
233    #[column(right, header = "RO")]
234    ro: i64,
235
236    #[column(right, header = "RA")]
237    ra: i64,
238
239    #[column(right, header = "SSZ")]
240    ssz: i64,
241
242    #[column(right, header = "BSZ")]
243    bsz: i64,
244
245    #[column(right, header = "StartSec")]
246    start_sec: u64,
247
248    #[column(right, header = "Size")]
249    size: u64,
250
251    #[column(header = "Device")]
252    device: String,
253}
254
255fn print_report(devices: &[String]) {
256    let mut rows = Vec::new();
257    for device in devices {
258        let Ok(f) = File::options().read(true).open(device) else {
259            continue;
260        };
261        let fd = f.as_raw_fd();
262        rows.push(ReportRow {
263            ro: ioctl_get_int(fd, BLKROGET).unwrap_or(-1),
264            ra: ioctl_get_ulong(fd, BLKRAGET).unwrap_or(-1),
265            ssz: ioctl_get_int(fd, BLKSSZGET).unwrap_or(-1),
266            bsz: ioctl_get_int(fd, BLKBSZGET).unwrap_or(-1),
267            start_sec: get_start_sector(device),
268            size: ioctl_get_u64(fd, BLKGETSIZE64).unwrap_or(0),
269            device: device.clone(),
270        });
271    }
272    let table = ReportRow::to_table(&rows);
273    let _ = print_table(&table, &mut io::stdout().lock());
274}
275
276struct ParsedArgs {
277    commands: Vec<Cmd>,
278    devices: Vec<String>,
279    verbose: bool,
280    quiet: bool,
281    report: bool,
282}
283
284fn parse_commands(raw_args: &[String]) -> Result<ParsedArgs, String> {
285    let mut commands: Vec<Cmd> = Vec::new();
286    let mut devices: Vec<String> = Vec::new();
287    let mut verbose = false;
288    let mut quiet = false;
289    let mut report = false;
290    let mut i = 0;
291
292    while i < raw_args.len() {
293        let arg = &raw_args[i];
294        match arg.as_str() {
295            "-v" | "--verbose" => verbose = true,
296            "-q" => quiet = true,
297            "--report" => report = true,
298
299            "--getro" => commands.push(Cmd::GetInt(BLKROGET, "getro")),
300            "--getbsz" => commands.push(Cmd::GetInt(BLKBSZGET, "getbsz")),
301            "--getss" => commands.push(Cmd::GetInt(BLKSSZGET, "getss")),
302            "--getpbsz" => commands.push(Cmd::GetUint(BLKPBSZGET, "getpbsz")),
303            "--getsize" => commands.push(Cmd::GetUlong(BLKGETSIZE, "getsize")),
304            "--getsize64" => {
305                commands.push(Cmd::GetU64(BLKGETSIZE64, "getsize64"))
306            }
307            "--getsz" => commands.push(Cmd::GetSz),
308            "--getra" => commands.push(Cmd::GetUlong(BLKRAGET, "getra")),
309            "--getfra" => commands.push(Cmd::GetUlong(BLKFRAGET, "getfra")),
310            "--getiomin" => commands.push(Cmd::GetUint(BLKIOMIN, "getiomin")),
311            "--getioopt" => commands.push(Cmd::GetUint(BLKIOOPT, "getioopt")),
312            "--getalignoff" => {
313                commands.push(Cmd::GetUint(BLKALIGNOFF, "getalignoff"))
314            }
315            "--getmaxsect" => {
316                commands.push(Cmd::GetUint(BLKSECTGET, "getmaxsect"))
317            }
318            "--getdiscardzeroes" => {
319                commands
320                    .push(Cmd::GetUint(BLKDISCARDZEROES, "getdiscardzeroes"));
321            }
322
323            "--setro" => commands.push(Cmd::SetInt(BLKROSET, 1, "setro")),
324            "--setrw" => commands.push(Cmd::SetInt(BLKROSET, 0, "setrw")),
325            "--setbsz" => {
326                i += 1;
327                let val: libc::c_int = raw_args
328                    .get(i)
329                    .ok_or("--setbsz requires an argument")?
330                    .parse()
331                    .map_err(|_| "--setbsz: invalid number")?;
332                commands.push(Cmd::SetInt(BLKBSZSET, val, "setbsz"));
333            }
334            "--setra" => {
335                i += 1;
336                let val: libc::c_ulong = raw_args
337                    .get(i)
338                    .ok_or("--setra requires an argument")?
339                    .parse()
340                    .map_err(|_| "--setra: invalid number")?;
341                commands.push(Cmd::SetUlong(BLKRASET, val, "setra"));
342            }
343            "--setfra" => {
344                i += 1;
345                let val: libc::c_ulong = raw_args
346                    .get(i)
347                    .ok_or("--setfra requires an argument")?
348                    .parse()
349                    .map_err(|_| "--setfra: invalid number")?;
350                commands.push(Cmd::SetUlong(BLKFRASET, val, "setfra"));
351            }
352            "--flushbufs" => commands.push(Cmd::Action(BLKFLSBUF, "flushbufs")),
353            "--rereadpt" => commands.push(Cmd::Action(BLKRRPART, "rereadpt")),
354
355            other if other.starts_with('-') => {
356                return Err(format!("unknown command: {other}"));
357            }
358            _ => devices.push(arg.clone()),
359        }
360        i += 1;
361    }
362
363    Ok(ParsedArgs {
364        commands,
365        devices,
366        verbose,
367        quiet,
368        report,
369    })
370}
371
372pub fn run(args: Args) -> ExitCode {
373    let ParsedArgs {
374        commands,
375        devices,
376        verbose,
377        quiet,
378        report,
379    } = match parse_commands(&args.args) {
380        Ok(parsed) => parsed,
381        Err(e) => {
382            eprintln!("blockdev: {e}");
383            return ExitCode::FAILURE;
384        }
385    };
386
387    if report {
388        let devs = if devices.is_empty() {
389            devices_from_proc_partitions()
390        } else {
391            devices
392        };
393        print_report(&devs);
394        return ExitCode::SUCCESS;
395    }
396
397    if commands.is_empty() {
398        eprintln!("blockdev: no command given, try 'blockdev --help'");
399        return ExitCode::FAILURE;
400    }
401
402    if devices.is_empty() {
403        eprintln!("blockdev: no device specified");
404        return ExitCode::FAILURE;
405    }
406
407    let mut failed = false;
408    for device in &devices {
409        let f = match File::options().read(true).write(true).open(device) {
410            Ok(f) => f,
411            Err(e) => {
412                eprintln!("blockdev: cannot open {device}: {e}");
413                failed = true;
414                continue;
415            }
416        };
417        let fd = f.as_raw_fd();
418        for cmd in &commands {
419            if let Err(e) = execute_cmd(fd, cmd, verbose, quiet) {
420                eprintln!("blockdev: {device}: {e}");
421                failed = true;
422            }
423        }
424    }
425
426    if failed {
427        ExitCode::FAILURE
428    } else {
429        ExitCode::SUCCESS
430    }
431}