disk_types/
usage.rs

1use crate::fs::FileSystem;
2use std::{
3    io::{self, BufRead, Cursor},
4    path::Path,
5    process::{Command, Stdio},
6};
7
8/// Executes a given file system's dump command to obtain the minimum shrink
9/// size
10pub 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                // If a failure occurred, try to correct any fixable errors.
36                Command::new("fsck.fat")
37                    .arg("-fy")
38                    .arg(part.as_ref())
39                    .stdout(Stdio::piped())
40                    .stderr(Stdio::null())
41                    .output()?;
42
43                // Then re-run the fsck command to get the status again.
44                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}