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