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 #[arg(short = 'a', long)]
48 all: bool,
49
50 #[arg(short = 'd', long)]
52 detach: bool,
53
54 #[arg(short = 'D', long = "detach-all")]
56 detach_all: bool,
57
58 #[arg(short = 'f', long)]
60 find: bool,
61
62 #[arg(short = 'c', long = "set-capacity")]
64 set_capacity: bool,
65
66 #[arg(short = 'j', long)]
68 associated: Option<String>,
69
70 #[arg(short = 'o', long)]
72 offset: Option<u64>,
73
74 #[arg(long)]
76 sizelimit: Option<u64>,
77
78 #[arg(short = 'P', long)]
80 partscan: bool,
81
82 #[arg(short = 'r', long = "read-only")]
84 read_only: bool,
85
86 #[arg(long)]
88 show: bool,
89
90 #[arg(short = 'l', long)]
92 list: bool,
93
94 #[arg(short = 'n', long)]
96 noheadings: bool,
97
98 #[arg(short = 'v', long)]
100 verbose: bool,
101
102 #[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 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 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}