shell_rs/
truncate.rs

1// Copyright (c) 2021 Xu Shaohua <shaohua@biofan.org>. All rights reserved.
2// Use of this source is governed by General Public License that can be found
3// in the LICENSE file.
4
5use std::path::{Path, PathBuf};
6
7use crate::error::{Error, ErrorKind};
8
9// TODO(Shaohua): Read block size info from system.
10const BLK_SIZE: nc::blksize_t = 512;
11
12#[derive(Debug, Clone)]
13pub struct Options {
14    /// Do not create any files.
15    pub no_create: bool,
16
17    /// Treat `size` as number of IO blocks instead of bytes.
18    pub io_blocks: bool,
19
20    /// Base size on another file.
21    pub reference: Option<PathBuf>,
22
23    /// Set or adjust the file size by `size` bytes.
24    ///
25    /// The `size` argument is an integer and optional unit (example: 10K is 10*1024).
26    /// Units are K,M,G,T,P,E, (powers of  1024) or KB,MB,... (powers of 1000).
27    /// Binary prefixes can be used, too: KiB=K, MiB=M, and so on.
28    ///
29    /// `size` may also be prefixed by one of the following modifying characters:
30    ///   * '+' extend by
31    ///   * '-' reduce by
32    ///   * '<' at most
33    ///   * '>' at least
34    ///   * '/' round down to multiple of
35    ///   * '%' round up to multiple of
36    pub size: Option<String>,
37}
38
39impl Default for Options {
40    fn default() -> Self {
41        Self {
42            no_create: false,
43            io_blocks: false,
44            reference: None,
45            size: None,
46        }
47    }
48}
49
50/// Shrink or extend the size of a file to the specified size.
51///
52/// Shrink or extend the size of file to the specified size.
53/// `file` argument that does not exist is created.
54/// If `file` is larger than the specified size, the extra data is lost.
55/// If `file` is shorter, it is extended and the sparse extended part (hole) reads as zero bytes.
56/// Mandatory arguments to long options are mandatory for short options too.
57pub fn truncate<P: AsRef<Path>>(file: P, options: &Options) -> Result<u64, Error> {
58    let new_size: u64 = if let Some(size) = &options.size {
59        parse_size(file.as_ref(), size, options.io_blocks)?
60    } else if let Some(ref_file) = &options.reference {
61        let fd = unsafe { nc::openat(nc::AT_FDCWD, ref_file, nc::O_RDONLY, 0)? };
62        let mut statbuf = nc::stat_t::default();
63        unsafe { nc::fstat(fd, &mut statbuf)? };
64        unsafe { nc::close(fd)? };
65        statbuf.st_size as u64
66    } else {
67        return Err(Error::new(
68            ErrorKind::ParameterError,
69            "Please specify either`size` or `reference`",
70        ));
71    };
72
73    let file = file.as_ref();
74    if let Err(err) = unsafe { nc::access(file, nc::R_OK | nc::W_OK) } {
75        if options.no_create {
76            return Err(err.into());
77        }
78    }
79
80    let fd = unsafe { nc::openat(nc::AT_FDCWD, file, nc::O_CREAT | nc::O_WRONLY, 0o644)? };
81    unsafe { nc::ftruncate(fd, new_size as nc::off_t)? };
82    unsafe { nc::close(fd)? };
83    Ok(new_size as u64)
84}
85
86fn parse_size<P: AsRef<Path>>(file: P, size: &str, io_blocks: bool) -> Result<u64, Error> {
87    let pattern =
88        regex::Regex::new(r"^(?P<prefix>[+\-<>/%]*)(?P<num>\d+)(?P<suffix>\w*)$").unwrap();
89    let matches = pattern.captures(size).unwrap();
90    let size_error = Error::from_string(
91        ErrorKind::ParameterError,
92        format!("Invalid size: {:?}", size),
93    );
94
95    let mut num: u64 = matches
96        .name("num")
97        .ok_or_else(|| size_error.clone())?
98        .as_str()
99        .parse()?;
100    if let Some(suffix) = matches.name("suffix") {
101        let suffix = suffix.as_str();
102        if suffix.is_empty() {
103            // Do nothing.
104        } else if suffix == "K" {
105            num *= 1024;
106        } else if suffix == "KB" {
107            num *= 1000;
108        } else if suffix == "M" {
109            num *= 1024 * 1024;
110        } else if suffix == "MB" {
111            num *= 1000 * 1000;
112        } else if suffix == "G" {
113            num *= 1024 * 1024 * 1024;
114        } else if suffix == "GB" {
115            num *= 1000 * 1000 * 1000;
116        } else if suffix == "T" {
117            num *= 1024 * 1024 * 1024 * 1024;
118        } else if suffix == "TB" {
119            num *= 1000 * 1000 * 1000 * 1000;
120        } else if suffix == "P" {
121            num *= 1024 * 1024 * 1024 * 1024 * 1024;
122        } else if suffix == "PB" {
123            num *= 1000 * 1000 * 1000 * 1000 * 1000;
124        } else if suffix == "E" {
125            num *= 1024 * 1024 * 1024 * 1024 * 1024 * 1024;
126        } else if suffix == "EB" {
127            num *= 1000 * 1000 * 1000 * 1000 * 1000 * 1000;
128        } else {
129            return Err(size_error);
130        }
131    }
132
133    let num = if io_blocks {
134        num * BLK_SIZE as u64
135    } else {
136        num
137    };
138    let mut new_size = num;
139    if let Some(prefix) = matches.name("prefix") {
140        let prefix = prefix.as_str();
141        if !prefix.is_empty() {
142            let fd = unsafe { nc::openat(nc::AT_FDCWD, file.as_ref(), nc::O_RDONLY, 0)? };
143            let mut statbuf = nc::stat_t::default();
144            unsafe { nc::fstat(fd, &mut statbuf)? };
145            unsafe { nc::close(fd)? };
146            let old_size = statbuf.st_size as u64;
147            if prefix == "+" {
148                // Extend
149                new_size = old_size + num;
150            } else if prefix == "-" {
151                // Shrink
152                new_size = old_size - num;
153            } else if prefix == "<" {
154                // At most
155                new_size = u64::min(old_size, num);
156            } else if prefix == ">" {
157                // At least
158                new_size = u64::max(old_size, num);
159            } else if prefix == "/" {
160                // Round down
161                let rem = old_size.rem_euclid(num);
162                println!("rem: {}", rem);
163                new_size = old_size - rem;
164            } else if prefix == "%" {
165                // Round up
166                let rem = old_size.rem_euclid(num);
167                println!("rem: {}", rem);
168                new_size = old_size - rem + num;
169            } else {
170                return Err(size_error);
171            }
172        }
173    }
174
175    Ok(new_size)
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn test_parse_size() {
184        let file = "tests/Rust_Wikipedia.pdf";
185        const OLD_SIZE: u64 = 455977;
186
187        assert_eq!(parse_size(file, "1024", false), Ok(1024));
188        assert_eq!(parse_size(file, "1K", false), Ok(1024));
189        assert_eq!(parse_size(file, "1M", false), Ok(1024 * 1024));
190        assert_eq!(parse_size(file, "1G", false), Ok(1024 * 1024 * 1024));
191        assert_eq!(parse_size(file, "1T", false), Ok(1024 * 1024 * 1024 * 1024));
192        assert_eq!(parse_size(file, "1KB", false), Ok(1000));
193        assert_eq!(parse_size(file, "1MB", false), Ok(1000 * 1000));
194        assert_eq!(parse_size(file, "1GB", false), Ok(1000 * 1000 * 1000));
195        assert_eq!(
196            parse_size(file, "1TB", false),
197            Ok(1000 * 1000 * 1000 * 1000)
198        );
199
200        assert_eq!(parse_size(file, "+1024", false), Ok(OLD_SIZE + 1024));
201        assert_eq!(parse_size(file, "-1024", false), Ok(OLD_SIZE - 1024));
202        assert_eq!(parse_size(file, "<1024", false), Ok(1024));
203        assert_eq!(parse_size(file, ">1024", false), Ok(OLD_SIZE));
204        assert_eq!(parse_size(file, "/1024", false), Ok(455680));
205        assert_eq!(parse_size(file, "%1024", false), Ok(456704));
206    }
207
208    #[test]
209    fn test_truncate() {
210        let file = "/tmp/truncate.shell-rs";
211        assert!(truncate(file, &Options::default()).is_err());
212
213        assert_eq!(
214            truncate(
215                file,
216                &Options {
217                    size: Some("1M".to_string()),
218                    ..Options::default()
219                },
220            ),
221            Ok(1024 * 1024)
222        );
223
224        assert_eq!(
225            truncate(
226                file,
227                &Options {
228                    size: Some("1K".to_string()),
229                    ..Options::default()
230                },
231            ),
232            Ok(1024)
233        );
234
235        let _ = nc::unlink(file);
236    }
237}