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 #[arg(short, long, value_name = "length")]
24 length: Option<String>,
25
26 #[arg(short, long, value_name = "offset", default_value = "0")]
28 offset: String,
29
30 #[arg(short = 'n', long)]
32 keep_size: bool,
33
34 #[arg(short = 'c', long, conflicts_with_all = ["punch_hole", "zero_range", "dig_holes", "insert_range", "posix"])]
36 collapse_range: bool,
37
38 #[arg(short = 'd', long, conflicts_with_all = ["collapse_range", "punch_hole", "zero_range", "insert_range", "posix"])]
40 dig_holes: bool,
41
42 #[arg(short = 'i', long, conflicts_with_all = ["collapse_range", "punch_hole", "zero_range", "dig_holes", "posix"])]
44 insert_range: bool,
45
46 #[arg(short = 'p', long, conflicts_with_all = ["collapse_range", "zero_range", "dig_holes", "insert_range", "posix"])]
48 punch_hole: bool,
49
50 #[arg(short = 'z', long, conflicts_with_all = ["collapse_range", "punch_hole", "dig_holes", "insert_range", "posix"])]
52 zero_range: bool,
53
54 #[arg(short = 'x', long, conflicts_with_all = ["collapse_range", "punch_hole", "zero_range", "dig_holes", "insert_range"])]
56 posix: bool,
57
58 #[arg(short, long)]
60 verbose: bool,
61
62 #[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 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 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 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 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, }
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 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}