Skip to main content

linuxutils_system/
losetup.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::{self, File},
9    io,
10    os::unix::io::AsRawFd,
11    path::Path,
12    process::ExitCode,
13};
14
15const LOOP_SET_FD: libc::c_ulong = 0x4C00;
16const LOOP_CLR_FD: libc::c_ulong = 0x4C01;
17const LOOP_SET_STATUS64: libc::c_ulong = 0x4C04;
18const LOOP_GET_STATUS64: libc::c_ulong = 0x4C05;
19const LOOP_SET_CAPACITY: libc::c_ulong = 0x4C07;
20const LOOP_CTL_GET_FREE: libc::c_ulong = 0x4C82;
21
22const LO_FLAGS_READ_ONLY: u32 = 1;
23const _LO_FLAGS_AUTOCLEAR: u32 = 4;
24const LO_FLAGS_PARTSCAN: u32 = 8;
25
26#[repr(C)]
27struct LoopInfo64 {
28    lo_device: u64,
29    lo_inode: u64,
30    lo_rdevice: u64,
31    lo_offset: u64,
32    lo_sizelimit: u64,
33    lo_number: u32,
34    lo_encrypt_type: u32,
35    lo_encrypt_key_size: u32,
36    lo_flags: u32,
37    lo_file_name: [u8; 64],
38    lo_crypt_name: [u8; 64],
39    lo_encrypt_key: [u8; 32],
40    lo_init: [u64; 2],
41}
42
43#[derive(Parser)]
44#[command(name = "losetup", about = "Set up and control loop devices")]
45pub struct Args {
46    /// List all used devices
47    #[arg(short = 'a', long)]
48    all: bool,
49
50    /// Detach one or more devices
51    #[arg(short = 'd', long)]
52    detach: bool,
53
54    /// Detach all used devices
55    #[arg(short = 'D', long = "detach-all")]
56    detach_all: bool,
57
58    /// Find first unused device
59    #[arg(short = 'f', long)]
60    find: bool,
61
62    /// Resize loop device (reread backing file size)
63    #[arg(short = 'c', long = "set-capacity")]
64    set_capacity: bool,
65
66    /// List devices associated with a backing file
67    #[arg(short = 'j', long)]
68    associated: Option<String>,
69
70    /// Byte offset into backing file
71    #[arg(short = 'o', long)]
72    offset: Option<u64>,
73
74    /// Limit device size in bytes
75    #[arg(long)]
76    sizelimit: Option<u64>,
77
78    /// Force kernel partition table scan
79    #[arg(short = 'P', long)]
80    partscan: bool,
81
82    /// Set up read-only
83    #[arg(short = 'r', long = "read-only")]
84    read_only: bool,
85
86    /// Print device name after setup (with -f)
87    #[arg(long)]
88    show: bool,
89
90    /// Tabular list format
91    #[arg(short = 'l', long)]
92    list: bool,
93
94    /// Suppress headers in list output
95    #[arg(short = 'n', long)]
96    noheadings: bool,
97
98    /// Verbose mode
99    #[arg(short = 'v', long)]
100    verbose: bool,
101
102    /// Device and/or file arguments
103    #[arg(trailing_var_arg = true)]
104    positional: Vec<String>,
105}
106
107#[derive(Cols)]
108struct LoopDeviceInfo {
109    #[column(header = "NAME")]
110    name: String,
111
112    #[column(right, header = "SIZELIMIT")]
113    sizelimit: u64,
114
115    #[column(right, header = "OFFSET")]
116    offset: u64,
117
118    #[column(right, header = "AUTOCLEAR")]
119    autoclear: u8,
120
121    #[column(right, header = "PARTSCAN")]
122    partscan: u8,
123
124    #[column(right, header = "RO")]
125    read_only: u8,
126
127    #[column(header = "BACK-FILE")]
128    backing_file: String,
129
130    #[column(right, header = "DIO")]
131    dio: u8,
132}
133
134fn sysfs_read(path: &str) -> Option<String> {
135    fs::read_to_string(path).ok().map(|s| s.trim().to_string())
136}
137
138fn list_loop_devices() -> Vec<LoopDeviceInfo> {
139    let mut devices = Vec::new();
140
141    let Ok(entries) = fs::read_dir("/sys/block") else {
142        return devices;
143    };
144
145    for entry in entries.flatten() {
146        let name = entry.file_name();
147        let name_str = name.to_string_lossy();
148        if !name_str.starts_with("loop") {
149            continue;
150        }
151
152        let loop_dir = format!("/sys/block/{name_str}/loop");
153        if !Path::new(&loop_dir).exists() {
154            continue;
155        }
156
157        let Some(backing_file) =
158            sysfs_read(&format!("{loop_dir}/backing_file"))
159        else {
160            continue;
161        };
162
163        let bool_val = |path: &str| -> u8 {
164            sysfs_read(path)
165                .map(|s| if s == "1" { 1 } else { 0 })
166                .unwrap_or(0)
167        };
168
169        devices.push(LoopDeviceInfo {
170            name: format!("/dev/{name_str}"),
171            sizelimit: sysfs_read(&format!("{loop_dir}/sizelimit"))
172                .and_then(|s| s.parse().ok())
173                .unwrap_or(0),
174            offset: sysfs_read(&format!("{loop_dir}/offset"))
175                .and_then(|s| s.parse().ok())
176                .unwrap_or(0),
177            autoclear: bool_val(&format!("{loop_dir}/autoclear")),
178            partscan: bool_val(&format!("{loop_dir}/partscan")),
179            read_only: bool_val(&format!("/sys/block/{name_str}/ro")),
180            backing_file,
181            dio: bool_val(&format!("{loop_dir}/dio")),
182        });
183    }
184
185    devices.sort_by(|a, b| a.name.cmp(&b.name));
186    devices
187}
188
189fn print_devices(devices: &[LoopDeviceInfo], noheadings: bool) {
190    let mut table = LoopDeviceInfo::to_table(devices);
191    table.headings_set(!noheadings);
192    let _ = print_table(&table, &mut io::stdout().lock());
193}
194
195fn print_old_style(devices: &[LoopDeviceInfo]) {
196    for d in devices {
197        let mut extra = String::new();
198        if d.offset > 0 {
199            extra.push_str(&format!(", offset {}", d.offset));
200        }
201        if d.sizelimit > 0 {
202            extra.push_str(&format!(", sizelimit {}", d.sizelimit));
203        }
204        println!("{}: ({}){extra}", d.name, d.backing_file);
205    }
206}
207
208fn find_free_device() -> io::Result<String> {
209    let ctl = File::open("/dev/loop-control")?;
210    let nr = unsafe { libc::ioctl(ctl.as_raw_fd(), LOOP_CTL_GET_FREE) };
211    if nr < 0 {
212        return Err(io::Error::last_os_error());
213    }
214    Ok(format!("/dev/loop{nr}"))
215}
216
217fn setup_loop(
218    device: &str,
219    file: &str,
220    offset: u64,
221    sizelimit: u64,
222    flags: u32,
223) -> io::Result<()> {
224    let backing = if flags & LO_FLAGS_READ_ONLY != 0 {
225        File::open(file)?
226    } else {
227        File::options().read(true).write(true).open(file)?
228    };
229
230    let loop_dev = File::options().read(true).write(true).open(device)?;
231
232    let ret = unsafe {
233        libc::ioctl(loop_dev.as_raw_fd(), LOOP_SET_FD, backing.as_raw_fd())
234    };
235    if ret < 0 {
236        return Err(io::Error::last_os_error());
237    }
238
239    let mut info = unsafe { std::mem::zeroed::<LoopInfo64>() };
240    info.lo_offset = offset;
241    info.lo_sizelimit = sizelimit;
242    info.lo_flags = flags;
243
244    let file_bytes = file.as_bytes();
245    let copy_len = file_bytes.len().min(63);
246    info.lo_file_name[..copy_len].copy_from_slice(&file_bytes[..copy_len]);
247
248    let ret =
249        unsafe { libc::ioctl(loop_dev.as_raw_fd(), LOOP_SET_STATUS64, &info) };
250    if ret < 0 {
251        let err = io::Error::last_os_error();
252        unsafe { libc::ioctl(loop_dev.as_raw_fd(), LOOP_CLR_FD, 0) };
253        return Err(err);
254    }
255
256    Ok(())
257}
258
259fn detach_loop(device: &str) -> io::Result<()> {
260    let f = File::open(device)?;
261    let ret = unsafe { libc::ioctl(f.as_raw_fd(), LOOP_CLR_FD, 0) };
262    if ret < 0 {
263        Err(io::Error::last_os_error())
264    } else {
265        Ok(())
266    }
267}
268
269fn set_capacity(device: &str) -> io::Result<()> {
270    let f = File::open(device)?;
271    let ret = unsafe { libc::ioctl(f.as_raw_fd(), LOOP_SET_CAPACITY, 0) };
272    if ret < 0 {
273        Err(io::Error::last_os_error())
274    } else {
275        Ok(())
276    }
277}
278
279fn show_device(device: &str) -> ExitCode {
280    let Ok(f) = File::open(device) else {
281        eprintln!("losetup: {device}: failed to open");
282        return ExitCode::from(2);
283    };
284
285    let mut info = unsafe { std::mem::zeroed::<LoopInfo64>() };
286    let ret =
287        unsafe { libc::ioctl(f.as_raw_fd(), LOOP_GET_STATUS64, &mut info) };
288    if ret < 0 {
289        let e = io::Error::last_os_error();
290        if e.raw_os_error() == Some(libc::ENXIO) {
291            eprintln!("losetup: {device}: not a configured loop device");
292            return ExitCode::FAILURE;
293        }
294        eprintln!("losetup: {device}: {e}");
295        return ExitCode::from(2);
296    }
297
298    let file_name = extract_string(&info.lo_file_name);
299    let mut extra = String::new();
300    if info.lo_offset > 0 {
301        extra.push_str(&format!(", offset {}", info.lo_offset));
302    }
303    if info.lo_sizelimit > 0 {
304        extra.push_str(&format!(", sizelimit {}", info.lo_sizelimit));
305    }
306    println!("{device}: ({file_name}){extra}");
307    ExitCode::SUCCESS
308}
309
310fn extract_string(bytes: &[u8]) -> String {
311    let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
312    String::from_utf8_lossy(&bytes[..end]).to_string()
313}
314
315pub fn run(args: Args) -> ExitCode {
316    if args.detach_all {
317        let devices = list_loop_devices();
318        let mut failed = false;
319        for d in &devices {
320            if let Err(e) = detach_loop(&d.name) {
321                eprintln!("losetup: {}: {e}", d.name);
322                failed = true;
323            }
324        }
325        return if failed {
326            ExitCode::FAILURE
327        } else {
328            ExitCode::SUCCESS
329        };
330    }
331
332    if args.detach {
333        if args.positional.is_empty() {
334            eprintln!("losetup: --detach requires at least one device");
335            return ExitCode::FAILURE;
336        }
337        let mut failed = false;
338        for dev in &args.positional {
339            if let Err(e) = detach_loop(dev) {
340                eprintln!("losetup: {dev}: {e}");
341                failed = true;
342            }
343        }
344        return if failed {
345            ExitCode::FAILURE
346        } else {
347            ExitCode::SUCCESS
348        };
349    }
350
351    if args.set_capacity {
352        let dev = match args.positional.first() {
353            Some(d) => d,
354            None => {
355                eprintln!("losetup: --set-capacity requires a device");
356                return ExitCode::FAILURE;
357            }
358        };
359        return match set_capacity(dev) {
360            Ok(()) => ExitCode::SUCCESS,
361            Err(e) => {
362                eprintln!("losetup: {dev}: {e}");
363                ExitCode::FAILURE
364            }
365        };
366    }
367
368    if let Some(ref file) = args.associated {
369        let canonical = fs::canonicalize(file)
370            .ok()
371            .and_then(|p| p.to_str().map(|s| s.to_string()))
372            .unwrap_or_else(|| file.clone());
373
374        let devices: Vec<_> = list_loop_devices()
375            .into_iter()
376            .filter(|d| {
377                d.backing_file == canonical
378                    || d.backing_file == *file
379                    || d.backing_file.trim_end() == canonical
380            })
381            .collect();
382
383        print_old_style(&devices);
384        return ExitCode::SUCCESS;
385    }
386
387    if args.find {
388        let dev = match find_free_device() {
389            Ok(d) => d,
390            Err(e) => {
391                eprintln!("losetup: failed to find a free loop device: {e}");
392                return ExitCode::FAILURE;
393            }
394        };
395
396        if let Some(file) = args.positional.first() {
397            let mut flags = 0u32;
398            if args.read_only {
399                flags |= LO_FLAGS_READ_ONLY;
400            }
401            if args.partscan {
402                flags |= LO_FLAGS_PARTSCAN;
403            }
404
405            if let Err(e) = setup_loop(
406                &dev,
407                file,
408                args.offset.unwrap_or(0),
409                args.sizelimit.unwrap_or(0),
410                flags,
411            ) {
412                eprintln!("losetup: {dev}: {e}");
413                return ExitCode::FAILURE;
414            }
415
416            if args.show {
417                println!("{dev}");
418            }
419        } else {
420            println!("{dev}");
421        }
422
423        return ExitCode::SUCCESS;
424    }
425
426    // List mode: no args = tabular, -a alone = old-style, -l or -a -l = tabular
427    if args.positional.is_empty()
428        && (args.all
429            || args.list
430            || (!args.detach && !args.find && !args.set_capacity))
431    {
432        let devices = list_loop_devices();
433        if args.all && !args.list {
434            print_old_style(&devices);
435        } else {
436            print_devices(&devices, args.noheadings);
437        }
438        return ExitCode::SUCCESS;
439    }
440
441    // Show or setup a specific device
442    if args.positional.len() == 1 {
443        return show_device(&args.positional[0]);
444    }
445
446    if args.positional.len() == 2 {
447        let dev = &args.positional[0];
448        let file = &args.positional[1];
449
450        let mut flags = 0u32;
451        if args.read_only {
452            flags |= LO_FLAGS_READ_ONLY;
453        }
454        if args.partscan {
455            flags |= LO_FLAGS_PARTSCAN;
456        }
457
458        return match setup_loop(
459            dev,
460            file,
461            args.offset.unwrap_or(0),
462            args.sizelimit.unwrap_or(0),
463            flags,
464        ) {
465            Ok(()) => {
466                if args.verbose {
467                    println!("{dev}: set up with {file}");
468                }
469                ExitCode::SUCCESS
470            }
471            Err(e) => {
472                eprintln!("losetup: {dev}: {e}");
473                ExitCode::FAILURE
474            }
475        };
476    }
477
478    eprintln!("losetup: bad usage, try 'losetup --help'");
479    ExitCode::FAILURE
480}