1use crate::fs::FileSystem;
2use std::{
3 io::{self, BufRead, Cursor},
4 path::Path,
5 process::{Command, Stdio},
6};
7
8pub fn sectors_used<P: AsRef<Path>>(part: P, fs: FileSystem) -> io::Result<u64> {
11 use self::FileSystem::*;
12 match fs {
13 Ext2 | Ext3 | Ext4 => {
14 let reader = Cursor::new(
15 Command::new("dumpe2fs")
16 .arg("-h")
17 .arg(part.as_ref())
18 .stdout(Stdio::piped())
19 .stderr(Stdio::null())
20 .output()?
21 .stdout,
22 );
23
24 get_ext4_usage(reader.lines().skip(1))
25 }
26 Fat16 | Fat32 => {
27 let mut cmd = Command::new("fsck.fat")
28 .arg("-nv")
29 .arg(part.as_ref())
30 .stdout(Stdio::piped())
31 .stderr(Stdio::null())
32 .output()?;
33
34 if !cmd.status.success() {
35 Command::new("fsck.fat")
37 .arg("-fy")
38 .arg(part.as_ref())
39 .stdout(Stdio::piped())
40 .stderr(Stdio::null())
41 .output()?;
42
43 cmd = Command::new("fsck.fat")
45 .arg("-nv")
46 .arg(part.as_ref())
47 .stdout(Stdio::piped())
48 .stderr(Stdio::null())
49 .output()?;
50 }
51
52 let reader = Cursor::new(cmd.stdout);
53 get_fat_usage(reader.lines().skip(1))
54 }
55 Ntfs => {
56 let cmd = Command::new("ntfsresize")
57 .arg("--info")
58 .arg("--force")
59 .arg("--no-progress-bar")
60 .arg(part.as_ref())
61 .stdout(Stdio::piped())
62 .stderr(Stdio::null())
63 .output()?;
64
65 let reader = Cursor::new(cmd.stdout).lines().skip(1);
66 if cmd.status.success() {
67 get_ntfs_usage(reader)
68 } else {
69 get_ntfs_size(reader)
70 }
71 }
72 Btrfs => {
73 let cmd = Command::new("btrfs")
74 .arg("filesystem")
75 .arg("show")
76 .arg(part.as_ref())
77 .stdout(Stdio::piped())
78 .stderr(Stdio::null())
79 .output()?;
80
81 let reader = Cursor::new(cmd.stdout).lines().skip(1);
82 get_btrfs_usage(reader)
83 }
84 _ => Err(io::Error::new(io::ErrorKind::NotFound, "unsupported file system")),
85 }
86}
87
88fn get_btrfs_usage<R: Iterator<Item = io::Result<String>>>(mut reader: R) -> io::Result<u64> {
89 parse_field_as_unit(&mut reader, "Total devices", 6).map(|used| used / 512)
90}
91
92fn get_ext4_usage<R: Iterator<Item = io::Result<String>>>(mut reader: R) -> io::Result<u64> {
93 let total_blocks = parse_field(&mut reader, "Block count:", 2)?;
94 let free_blocks = parse_field(&mut reader, "Free blocks:", 2)?;
95 let block_size = parse_field(&mut reader, "Block size:", 2)?;
96 Ok(((total_blocks - free_blocks) * block_size) / 512)
97}
98
99fn get_ntfs_usage<R: Iterator<Item = io::Result<String>>>(mut reader: R) -> io::Result<u64> {
100 parse_field(&mut reader, "You might resize at", 4)
101 .map(|bytes| (bytes + (2 * 1024 * 1024)) / 512)
102}
103
104fn get_ntfs_size<R: Iterator<Item = io::Result<String>>>(mut reader: R) -> io::Result<u64> {
105 parse_field(&mut reader, "Current volume size", 3).map(|bytes| bytes / 512)
106}
107
108fn get_fat_usage<R: Iterator<Item = io::Result<String>>>(mut reader: R) -> io::Result<u64> {
109 let cluster_size = parse_fsck_field(&mut reader, "bytes per cluster")?;
110 let (used, _) = parse_fsck_cluster_summary(&mut reader)?;
111 Ok((used * cluster_size) / 512)
112}
113
114fn parse_fsck_field<R: Iterator<Item = io::Result<String>>>(
115 reader: &mut R,
116 end: &str,
117) -> io::Result<u64> {
118 loop {
119 match reader.next() {
120 Some(line) => {
121 let line = line?;
122 let line = line.trim();
123 if line.ends_with(end) {
124 match line.split_whitespace().next().map(|v| v.parse::<u64>()) {
125 Some(Ok(value)) => break Ok(value),
126 _ => {
127 break Err(io::Error::new(io::ErrorKind::Other, "invalid dump output"))
128 }
129 }
130 }
131 }
132 None => {
133 break Err(io::Error::new(io::ErrorKind::Other, "invalid dump output: EOF"));
134 }
135 }
136 }
137}
138
139fn parse_fsck_cluster_summary<R: Iterator<Item = io::Result<String>>>(
140 reader: &mut R,
141) -> io::Result<(u64, u64)> {
142 loop {
143 match reader.next() {
144 Some(line) => {
145 let line = line?;
146 if line.split_whitespace().next().map_or(false, |word| word.ends_with(':')) {
147 if let Some(stats) = line.split_whitespace().nth(3) {
148 if let Some(id) = stats.find('/') {
149 if stats.len() > id + 1 {
150 if let Ok(used) = stats[..id].parse::<u64>() {
151 if let Ok(total) = stats[id + 1..].parse::<u64>() {
152 break Ok((used, total));
153 }
154 }
155 }
156 }
157 }
158
159 break Err(io::Error::new(io::ErrorKind::Other, "invalid dump output"));
160 }
161 }
162 None => {
163 break Err(io::Error::new(io::ErrorKind::Other, "invalid dump output: EOF"));
164 }
165 }
166 }
167}
168
169fn parse_field<R: Iterator<Item = io::Result<String>>>(
170 reader: &mut R,
171 field: &str,
172 value: usize,
173) -> io::Result<u64> {
174 for line in reader {
175 let line = line?;
176 if line.starts_with(field) {
177 match line.split_whitespace().nth(value).map(|v| v.parse::<u64>()) {
178 Some(Ok(value)) => return Ok(value),
179 _ => return Err(io::Error::new(io::ErrorKind::Other, "invalid usage field")),
180 }
181 }
182 }
183
184 Err(io::Error::new(io::ErrorKind::Other, "invalid usage output"))
185}
186
187fn parse_unit(unit: &str) -> io::Result<u64> {
188 let (value, unit) = unit.split_at(unit.len() - 3);
189 eprintln!("Value: {}, unit: {}", value, unit);
190 let value = match value.parse::<f64>() {
191 Ok(value) => value,
192 Err(why) => {
193 return Err(io::Error::new(
194 io::ErrorKind::Other,
195 format!("invalid unit value: {}", why),
196 ));
197 }
198 };
199
200 match unit {
201 "KiB" => Ok((value * 1024f64) as u64),
202 "MiB" => Ok((value * 1024f64 * 1024f64) as u64),
203 "GiB" => Ok((value * 1024f64 * 1024f64 * 1024f64) as u64),
204 "TiB" => Ok((value * 1024f64 * 1024f64 * 1024f64 * 1024f64) as u64),
205 _ => Err(io::Error::new(io::ErrorKind::Other, format!("invalid unit type: {}", unit))),
206 }
207}
208
209fn parse_field_as_unit<R: Iterator<Item = io::Result<String>>>(
210 reader: &mut R,
211 field: &str,
212 value: usize,
213) -> io::Result<u64> {
214 for line in reader {
215 let line = line?;
216 let line = line.trim_start();
217 if line.starts_with(field) {
218 match line.split_whitespace().nth(value) {
219 Some(value) => {
220 let value = parse_unit(value)?;
221 return Ok(value);
222 }
223 None => return Err(io::Error::new(io::ErrorKind::Other, "invalid usage field")),
224 }
225 }
226 }
227
228 Err(io::Error::new(io::ErrorKind::Other, "invalid usage output"))
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 const FAT_INPUT: &str = r#"fsck.fat 4.1 (2017-01-24)
236Checking we can access the last sector of the filesystem
237Boot sector contents:
238System ID "mkfs.fat"
239Media byte 0xf8 (hard disk)
240 512 bytes per logical sector
241 4096 bytes per cluster
242 32 reserved sectors
243First FAT starts at byte 16384 (sector 32)
244 2 FATs, 32 bit entries
245 1048576 bytes per FAT (= 2048 sectors)
246Root directory start at cluster 2 (arbitrary size)
247Data area starts at byte 2113536 (sector 4128)
248 261628 data clusters (1071628288 bytes)
24963 sectors/track, 255 heads
250 2048 hidden sectors
251 2097152 sectors total
252Checking for unused clusters.
253Checking free cluster summary.
254/dev/sdb1: 0 files, 1/261628 clusters"#;
255
256 const FAT2_INPUT: &str = r#"fsck.fat 4.1 (2017-01-24)
257Checking we can access the last sector of the filesystem
258Boot sector contents:
259System ID "mkfs.fat"
260Media byte 0xf8 (hard disk)
261 512 bytes per logical sector
262 4096 bytes per cluster
263 32 reserved sectors
264First FAT starts at byte 16384 (sector 32)
265 2 FATs, 32 bit entries
266 524288 bytes per FAT (= 1024 sectors)
267Root directory start at cluster 2 (arbitrary size)
268Data area starts at byte 1064960 (sector 2080)
269 130812 data clusters (535805952 bytes)
27063 sectors/track, 255 heads
271 2048 hidden sectors
272 1048576 sectors total
273Checking for unused clusters.
274Checking free cluster summary.
275/dev/sda1: 31 files, 66356/130812 clusters
276"#;
277
278 const FAT3_INPUT: &str = r#"fsck.fat 4.1 (2017-01-24)
279Checking we can access the last sector of the filesystem
280Boot sector contents:
281System ID "mkfs.fat"
282Media byte 0xf8 (hard disk)
283 512 bytes per logical sector
284 8192 bytes per cluster
285 16 reserved sectors
286First FAT starts at byte 8192 (sector 16)
287 2 FATs, 16 bit entries
288 131072 bytes per FAT (= 256 sectors)
289Root directory starts at byte 270336 (sector 528)
290 512 root directory entries
291Data area starts at byte 286720 (sector 560)
292 65501 data clusters (536584192 bytes)
29363 sectors/track, 255 heads
294 2048 hidden sectors
295 1048576 sectors total
296Checking for unused clusters.
297/dev/sda1: 35 files, 24176/65501 clusters
298"#;
299
300 #[test]
301 fn fat_parsing() {
302 let mut reader = FAT_INPUT.lines().map(|x| Ok(x.into()));
303 assert_eq!(parse_fsck_field(&mut reader, "bytes per cluster").unwrap(), 4096);
304 assert_eq!(parse_fsck_cluster_summary(&mut reader).unwrap(), (1, 261628));
305 }
306
307 #[test]
308 fn fat_parsing2() {
309 let mut reader = FAT2_INPUT.lines().map(|x| Ok(x.into()));
310 assert_eq!(parse_fsck_field(&mut reader, "bytes per cluster").unwrap(), 4096);
311 assert_eq!(parse_fsck_cluster_summary(&mut reader).unwrap(), (66356, 130812));
312 }
313
314 #[test]
315 fn fat_parsing3() {
316 let mut reader = FAT3_INPUT.lines().map(|x| Ok(x.into()));
317 assert_eq!(parse_fsck_field(&mut reader, "bytes per cluster").unwrap(), 8192);
318 assert_eq!(parse_fsck_cluster_summary(&mut reader).unwrap(), (24176, 65501));
319 }
320
321 #[test]
322 fn fat_usage() {
323 assert_eq!(get_fat_usage(FAT_INPUT.lines().map(|x| Ok(x.into()))).unwrap(), 8);
324 }
325
326 #[test]
327 fn fat_usage2() {
328 assert_eq!(get_fat_usage(FAT2_INPUT.lines().map(|x| Ok(x.into()))).unwrap(), 530848);
329 }
330
331 #[test]
332 fn fat_usage3() {
333 assert_eq!(get_fat_usage(FAT3_INPUT.lines().map(|x| Ok(x.into()))).unwrap(), 386816);
334 }
335
336 const EXT_INPUT: &str = r#"dumpe2fs 1.43.9 (8-Feb-2018)
337Filesystem volume name: <none>
338Last mounted on: <not available>
339Filesystem UUID: 5d9baf52-67c5-4ed2-ba13-ef20b2dfc0a7
340Filesystem magic number: 0xEF53
341Filesystem revision #: 1 (dynamic)
342Filesystem features: has_journal ext_attr resize_inode dir_index filetype extent flex_bg sparse_super large_file huge_file dir_nlink extra_isize metadata_csum
343Filesystem flags: signed_directory_hash
344Default mount options: user_xattr acl
345Filesystem state: clean
346Errors behavior: Continue
347Filesystem OS type: Linux
348Inode count: 1310720
349Block count: 5242880
350Reserved block count: 262144
351Free blocks: 5116591
352Free inodes: 1310709
353First block: 0
354Block size: 4096
355Fragment size: 4096
356Reserved GDT blocks: 1022
357Blocks per group: 32768
358Fragments per group: 32768
359Inodes per group: 8192
360Inode blocks per group: 512
361Flex block group size: 16
362Filesystem created: Tue Feb 27 13:35:37 2018
363Last mount time: n/a
364Last write time: Tue Feb 27 13:35:37 2018
365Mount count: 0
366Maximum mount count: -1
367Last checked: Tue Feb 27 13:35:37 2018
368Check interval: 0 (<none>)
369Lifetime writes: 132 MB
370Reserved blocks uid: 0 (user root)
371Reserved blocks gid: 0 (group root)
372First inode: 11
373Inode size: 256
374Required extra isize: 32
375Desired extra isize: 32
376Journal inode: 8
377Default directory hash: half_md4
378Directory Hash Seed: 05d9ad6e-d157-401f-be37-350a5017ddbf
379Journal backup: inode blocks
380Checksum type: crc32c
381Checksum: 0x9449cff8
382Journal features: (none)
383Journal size: 128M
384Journal length: 32768
385Journal sequence: 0x00000001
386Journal start: 0
387"#;
388
389 #[test]
390 fn ext_usage() {
391 assert_eq!(get_ext4_usage(EXT_INPUT.lines().map(|x| Ok(x.into()))).unwrap(), 1010312);
392 }
393
394 #[test]
395 fn ext_parsing() {
396 let mut reader = EXT_INPUT.lines().map(|x| Ok(x.into()));
397 assert_eq!(parse_field(&mut reader, "Block count:", 2).unwrap(), 5242880);
398
399 assert_eq!(parse_field(&mut reader, "Free blocks:", 2).unwrap(), 5116591);
400
401 assert_eq!(parse_field(&mut reader, "Block size:", 2).unwrap(), 4096);
402 }
403
404 const NTFS_INPUT: &str = r#"ntfsresize v2017.3.23 (libntfs-3g)
405Device name : /dev/sdb4
406NTFS volume version: 3.1
407Cluster size : 4096 bytes
408Current volume size: 21474832896 bytes (21475 MB)
409Current device size: 21474836480 bytes (21475 MB)
410Checking filesystem consistency ...
411Accounting clusters ...
412Space in use : 69 MB (0.3%)
413Collecting resizing constraints ...
414You might resize at 68227072 bytes or 69 MB (freeing 21406 MB).
415Please make a test run using both the -n and -s options before real resizing!"#;
416
417 #[test]
418 fn ntfs_usage() {
419 let reader = NTFS_INPUT.lines().map(|x| Ok(x.into()));
420 assert_eq!(get_ntfs_usage(reader).unwrap(), 133_256 + (2 * 1024 * 1024) / 512);
421 }
422
423 const BTRFS_INPUT: &str = r#"Label: none uuid: 8a69ba4c-6cf5-46cc-aff3-f0c23251a21b
424 Total devices 1 FS bytes used 112.00KiB
425 devid 1 size 20.00GiB used 2.02GiB path /dev/sdb2"#;
426
427 #[test]
428 fn btrfs_usage() {
429 let reader = BTRFS_INPUT.lines().map(|x| Ok(x.into()));
430 assert_eq!(get_btrfs_usage(reader).unwrap(), 224);
431 }
432}