Skip to main content

linuxutils_system/
fallocate.rs

1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use rustix::{
7    fd::AsFd,
8    fs::{FallocateFlags, Mode, OFlags},
9};
10use std::{
11    io::{Read, Seek, SeekFrom},
12    process::ExitCode,
13};
14
15#[derive(Parser)]
16#[command(
17    name = "fallocate",
18    about = "Preallocate or deallocate space to a file",
19    override_usage = "fallocate [options] <filename>"
20)]
21pub struct Args {
22    /// Length of the range in bytes (required unless --dig-holes)
23    #[arg(short, long, value_name = "length")]
24    length: Option<String>,
25
26    /// Offset of the range in bytes
27    #[arg(short, long, value_name = "offset", default_value = "0")]
28    offset: String,
29
30    /// Do not modify the apparent length of the file
31    #[arg(short = 'n', long)]
32    keep_size: bool,
33
34    /// Remove a byte range from the file without leaving a hole
35    #[arg(short = 'c', long, conflicts_with_all = ["punch_hole", "zero_range", "dig_holes", "insert_range", "posix"])]
36    collapse_range: bool,
37
38    /// Detect zeroes and replace with holes
39    #[arg(short = 'd', long, conflicts_with_all = ["collapse_range", "punch_hole", "zero_range", "insert_range", "posix"])]
40    dig_holes: bool,
41
42    /// Insert a hole at the specified offset, shifting existing data
43    #[arg(short = 'i', long, conflicts_with_all = ["collapse_range", "punch_hole", "zero_range", "dig_holes", "posix"])]
44    insert_range: bool,
45
46    /// Deallocate space (punch a hole) in the byte range
47    #[arg(short = 'p', long, conflicts_with_all = ["collapse_range", "zero_range", "dig_holes", "insert_range", "posix"])]
48    punch_hole: bool,
49
50    /// Zero a byte range in the file
51    #[arg(short = 'z', long, conflicts_with_all = ["collapse_range", "punch_hole", "dig_holes", "insert_range", "posix"])]
52    zero_range: bool,
53
54    /// Use posix_fallocate(3) compatible operation
55    #[arg(short = 'x', long, conflicts_with_all = ["collapse_range", "punch_hole", "zero_range", "dig_holes", "insert_range"])]
56    posix: bool,
57
58    /// Verbose mode
59    #[arg(short, long)]
60    verbose: bool,
61
62    /// The file to operate on
63    #[arg(required = true)]
64    filename: String,
65}
66
67fn parse_size(s: &str) -> Result<u64, String> {
68    let s = s.trim();
69    if s.is_empty() {
70        return Err("empty size string".to_string());
71    }
72
73    // Find where digits end and suffix begins
74    let num_end = s
75        .find(|c: char| !c.is_ascii_digit() && c != '.')
76        .unwrap_or(s.len());
77    let (num_str, suffix) = s.split_at(num_end);
78    let base: u64 = num_str
79        .parse()
80        .map_err(|e| format!("invalid number '{num_str}': {e}"))?;
81
82    let multiplier: u64 = match suffix {
83        "" | "B" => 1,
84        "K" | "KiB" => 1024,
85        "M" | "MiB" => 1024 * 1024,
86        "G" | "GiB" => 1024 * 1024 * 1024,
87        "T" | "TiB" => 1024u64 * 1024 * 1024 * 1024,
88        "P" | "PiB" => 1024u64 * 1024 * 1024 * 1024 * 1024,
89        "E" | "EiB" => 1024u64 * 1024 * 1024 * 1024 * 1024 * 1024,
90        "KB" => 1000,
91        "MB" => 1000 * 1000,
92        "GB" => 1000 * 1000 * 1000,
93        "TB" => 1000u64 * 1000 * 1000 * 1000,
94        "PB" => 1000u64 * 1000 * 1000 * 1000 * 1000,
95        "EB" => 1000u64 * 1000 * 1000 * 1000 * 1000 * 1000,
96        other => return Err(format!("unknown size suffix '{other}'")),
97    };
98
99    base.checked_mul(multiplier)
100        .ok_or_else(|| format!("size overflow: {s}"))
101}
102
103fn dig_holes(
104    fd: &std::fs::File,
105    offset: u64,
106    length: u64,
107    verbose: bool,
108) -> Result<(), String> {
109    let file_size = fd
110        .metadata()
111        .map_err(|e| format!("failed to get file metadata: {e}"))?
112        .len();
113    let end = if length == 0 {
114        file_size
115    } else {
116        offset.saturating_add(length).min(file_size)
117    };
118    let mut pos = offset;
119
120    // Use a buffered reader approach: read chunks, find zero regions, punch them
121    let mut buf = vec![0u8; 64 * 1024];
122    let mut reader = std::io::BufReader::new(fd);
123    reader
124        .seek(SeekFrom::Start(pos))
125        .map_err(|e| format!("seek failed: {e}"))?;
126
127    while pos < end {
128        let to_read = ((end - pos) as usize).min(buf.len());
129        let n = reader
130            .read(&mut buf[..to_read])
131            .map_err(|e| format!("read failed: {e}"))?;
132        if n == 0 {
133            break;
134        }
135
136        // Find zero runs in this chunk and punch them
137        let chunk = &buf[..n];
138        let mut i = 0;
139        while i < chunk.len() {
140            if chunk[i] == 0 {
141                let zero_start = i;
142                while i < chunk.len() && chunk[i] == 0 {
143                    i += 1;
144                }
145                let zero_len = i - zero_start;
146                // Only punch if the zero run is at least 4K (filesystem block aligned)
147                if zero_len >= 4096 {
148                    let hole_offset = pos + zero_start as u64;
149                    let hole_len = zero_len as u64;
150                    let flags =
151                        FallocateFlags::PUNCH_HOLE | FallocateFlags::KEEP_SIZE;
152                    if let Err(e) = rustix::fs::fallocate(
153                        fd.as_fd(),
154                        flags,
155                        hole_offset,
156                        hole_len,
157                    ) {
158                        if verbose {
159                            eprintln!(
160                                "fallocate: punch hole at offset {hole_offset}, length {hole_len}: {e}"
161                            );
162                        }
163                    } else if verbose {
164                        eprintln!(
165                            "fallocate: punched hole at offset {hole_offset}, length {hole_len}"
166                        );
167                    }
168                }
169            } else {
170                i += 1;
171            }
172        }
173
174        pos += n as u64;
175    }
176
177    Ok(())
178}
179
180pub fn run(args: Args) -> ExitCode {
181    let offset = match parse_size(&args.offset) {
182        Ok(v) => v,
183        Err(e) => {
184            eprintln!("fallocate: invalid offset: {e}");
185            return ExitCode::FAILURE;
186        }
187    };
188
189    let length = if args.dig_holes {
190        match &args.length {
191            Some(s) => match parse_size(s) {
192                Ok(v) => v,
193                Err(e) => {
194                    eprintln!("fallocate: invalid length: {e}");
195                    return ExitCode::FAILURE;
196                }
197            },
198            None => 0, // dig-holes can work without explicit length (uses file size)
199        }
200    } else {
201        match &args.length {
202            Some(s) => match parse_size(s) {
203                Ok(v) => v,
204                Err(e) => {
205                    eprintln!("fallocate: invalid length: {e}");
206                    return ExitCode::FAILURE;
207                }
208            },
209            None => {
210                eprintln!("fallocate: required argument --length not provided");
211                return ExitCode::FAILURE;
212            }
213        }
214    };
215
216    // Open the file
217    let oflags = if args.dig_holes {
218        OFlags::RDWR
219    } else {
220        OFlags::RDWR | OFlags::CREATE
221    };
222
223    let fd = match rustix::fs::open(
224        &args.filename,
225        oflags,
226        Mode::RUSR | Mode::WUSR | Mode::RGRP | Mode::ROTH,
227    ) {
228        Ok(fd) => fd,
229        Err(e) => {
230            eprintln!("fallocate: {}: {e}", args.filename);
231            return ExitCode::FAILURE;
232        }
233    };
234
235    if args.dig_holes {
236        let file = std::fs::File::from(fd);
237        if let Err(e) = dig_holes(&file, offset, length, args.verbose) {
238            eprintln!("fallocate: {e}");
239            return ExitCode::FAILURE;
240        }
241        if args.verbose {
242            eprintln!("fallocate: {}: dig holes complete", args.filename);
243        }
244        return ExitCode::SUCCESS;
245    }
246
247    let flags = if args.punch_hole {
248        FallocateFlags::PUNCH_HOLE | FallocateFlags::KEEP_SIZE
249    } else if args.collapse_range {
250        FallocateFlags::COLLAPSE_RANGE
251    } else if args.insert_range {
252        FallocateFlags::INSERT_RANGE
253    } else if args.zero_range {
254        if args.keep_size {
255            FallocateFlags::ZERO_RANGE | FallocateFlags::KEEP_SIZE
256        } else {
257            FallocateFlags::ZERO_RANGE
258        }
259    } else if args.keep_size {
260        FallocateFlags::KEEP_SIZE
261    } else {
262        FallocateFlags::empty()
263    };
264
265    if args.verbose {
266        let mode = if args.punch_hole {
267            "punch hole"
268        } else if args.collapse_range {
269            "collapse range"
270        } else if args.insert_range {
271            "insert range"
272        } else if args.zero_range {
273            "zero range"
274        } else if args.posix {
275            "posix allocate"
276        } else {
277            "allocate"
278        };
279        eprintln!(
280            "fallocate: {}: {mode} offset {offset}, length {length}",
281            args.filename,
282        );
283    }
284
285    if let Err(e) = rustix::fs::fallocate(fd.as_fd(), flags, offset, length) {
286        eprintln!("fallocate: fallocate failed: {e}");
287        return ExitCode::FAILURE;
288    }
289
290    ExitCode::SUCCESS
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296
297    #[test]
298    fn parse_size_plain() {
299        assert_eq!(parse_size("1024").unwrap(), 1024);
300    }
301
302    #[test]
303    fn parse_size_k() {
304        assert_eq!(parse_size("1K").unwrap(), 1024);
305    }
306
307    #[test]
308    fn parse_size_kib() {
309        assert_eq!(parse_size("1KiB").unwrap(), 1024);
310    }
311
312    #[test]
313    fn parse_size_kb() {
314        assert_eq!(parse_size("1KB").unwrap(), 1000);
315    }
316
317    #[test]
318    fn parse_size_m() {
319        assert_eq!(parse_size("1M").unwrap(), 1024 * 1024);
320    }
321
322    #[test]
323    fn parse_size_mib() {
324        assert_eq!(parse_size("1MiB").unwrap(), 1024 * 1024);
325    }
326
327    #[test]
328    fn parse_size_mb() {
329        assert_eq!(parse_size("1MB").unwrap(), 1_000_000);
330    }
331
332    #[test]
333    fn parse_size_g() {
334        assert_eq!(parse_size("1G").unwrap(), 1024 * 1024 * 1024);
335    }
336
337    #[test]
338    fn parse_size_invalid_suffix() {
339        assert!(parse_size("1X").is_err());
340    }
341
342    #[test]
343    fn parse_size_empty() {
344        assert!(parse_size("").is_err());
345    }
346}