1use std::path::{Path, PathBuf};
6
7use crate::error::{Error, ErrorKind};
8
9const BLK_SIZE: nc::blksize_t = 512;
11
12#[derive(Debug, Clone)]
13pub struct Options {
14 pub no_create: bool,
16
17 pub io_blocks: bool,
19
20 pub reference: Option<PathBuf>,
22
23 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
50pub 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 } 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 new_size = old_size + num;
150 } else if prefix == "-" {
151 new_size = old_size - num;
153 } else if prefix == "<" {
154 new_size = u64::min(old_size, num);
156 } else if prefix == ">" {
157 new_size = u64::max(old_size, num);
159 } else if prefix == "/" {
160 let rem = old_size.rem_euclid(num);
162 println!("rem: {}", rem);
163 new_size = old_size - rem;
164 } else if prefix == "%" {
165 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}