linux_info/
util.rs

1
2use std::{fmt, io, mem};
3use std::io::Read;
4use std::fs::File;
5use std::path::Path;
6use std::ffi::CString;
7use std::os::unix::ffi::OsStrExt;
8use std::os::unix::prelude::AsRawFd;
9use std::convert::TryInto;
10
11use byte_parser::{StrParser, ParseIterator};
12
13use libc::c_int;
14
15const DEF_PRECISION: usize = 2;
16
17/// Represents a size, for example `1024 kB`.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct DataSize {
20	bytes: u128
21}
22
23impl DataSize {
24
25	// not implemeting FromStr because this is private.
26	pub(crate) fn from_str(s: &str) -> Option<Self> {
27		let mut iter = StrParser::new(s);
28		let float = parse_f64(&mut iter)?;
29		// now we need to parse the unit
30		let unit = iter.record()
31			.consume_to_str()
32			.trim();
33
34		let unit = DataSizeUnit::from_str(unit)?;
35		Some(Self {
36			bytes: unit.to_byte(float)
37		})
38	}
39
40	pub(crate) fn from_size_bytes(bytes: impl TryInto<u128>) -> Option<Self> {
41		bytes.try_into().ok()
42			.map(|bytes| Self {bytes})
43	}
44
45	/// Convert the data unit into a specific unit.
46	pub fn to(self, unit: &DataSizeUnit) -> f64 {
47		DataSizeUnit::convert(self.bytes, unit)
48	}
49
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum DataSizeUnit {
54	/// Byte
55	B,
56	/// Kilobyte
57	Kb,// 1_000
58	/// Megabyte
59	Mb,// 1_000_000
60	/// Gigabyte
61	Gb,// 1_000_000_000
62	/// Terabyte
63	Tb // 1_000_000_000_000
64}
65
66impl DataSizeUnit {
67
68	const fn val(&self) -> u128 {
69		match self {
70			Self::B => 1,
71			Self::Kb => 1_024,
72			Self::Mb => 1_024 * 1_024,
73			Self::Gb => 1_024 * 1_024 * 1_024,
74			Self::Tb => 1_024 * 1_024 * 1_024 * 1_024
75		}
76	}
77
78	fn from_str(s: &str) -> Option<Self> {
79		Some(match s {
80			"" => Self::B,
81			s if eqs(s, "b") => Self::B,
82			s if eqs(s, "kb") => Self::Kb,
83			s if eqs(s, "mb") => Self::Mb,
84			s if eqs(s, "gb") => Self::Gb,
85			s if eqs(s, "tb") => Self::Tb,
86			_ => return None
87		})
88	}
89
90	fn to_byte(&self, val: f64) -> u128 {
91		// TODO probably need fix this overflowing
92		(val * self.val() as f64) as u128
93	}
94
95	fn adjust_to(byte: u128) -> Self {
96		match byte {
97			b if b < Self::Kb.val() => Self::B,
98			b if b < Self::Mb.val() => Self::Kb,
99			b if b < Self::Gb.val() => Self::Mb,
100			b if b < Self::Tb.val() => Self::Gb,
101			_ => Self::Tb
102		}
103	}
104
105	fn convert(byte: u128, to: &Self) -> f64 {
106		byte as f64 / to.val() as f64
107	}
108
109	const fn as_str(&self) -> &'static str {
110		match self {
111			Self::B => "b",
112			Self::Kb => "kb",
113			Self::Mb => "mb",
114			Self::Gb => "gb",
115			Self::Tb => "tb"
116		}
117	}
118
119	// val needs to be already adjusted to self
120	fn fmt_val(&self, val: f64, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121		if val == 0f64 {
122			return write!(f, "{}", val)
123		}
124
125		// calculate the precision we wan't to use
126		let fract = val.fract();
127		let precision = if fract == 0f64 {
128			0
129		} else {
130
131			let max = f.precision().unwrap_or(DEF_PRECISION);
132
133			if max <= 1 {
134				max
135			} else {
136				calculate_precision(fract, max)
137			}
138		};
139
140		write!(f, "{:.*} {}", precision, val, self.as_str())
141	}
142
143}
144
145fn calculate_precision(mut fract: f64, max: usize) -> usize {
146	let max_number = 10usize.pow(max as u32) as f64;
147
148	// round to the max position
149	fract *= max_number;
150	fract = fract.round();
151
152	// then go slowly back until we hit a fraction
153	for m in (1..=max).rev() {
154		fract /= 10f64;
155		if fract.fract() != 0f64 {
156			return m;
157		}
158	}
159
160	0
161}
162
163#[inline(always)]
164fn eqs(a: &str, b: &str) -> bool {
165	a.eq_ignore_ascii_case(b)
166}
167
168impl fmt::Display for DataSize {
169	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170		let unit = DataSizeUnit::adjust_to(self.bytes);
171		let val = DataSizeUnit::convert(self.bytes, &unit);
172		unit.fmt_val(val, f)
173	}
174}
175
176// parses a part of a slice
177// Panics if Iterator contains not valid utf8
178fn parse_f64<'s, I>(iter: &mut I) -> Option<f64>
179where I: ParseIterator<'s> {
180
181	let mut iter = iter.record();
182
183	// consume first digits
184	iter.while_byte_fn(u8::is_ascii_digit)
185		.consume_at_least(1)
186		.ok()?;
187
188	// dot
189	let has_dot = iter
190		.next_if(|&b| b == b'.')
191		.is_some();
192
193	if has_dot {
194		// consume next digits
195		iter.consume_while_byte_fn(u8::is_ascii_digit);
196	}
197
198	iter.to_str()
199		.parse().ok()
200}
201
202/// Clears the string the writes the entire file to the string.  
203/// Does not allocate in advance like std::fs::read_to_string.
204pub fn read_to_string_mut(path: impl AsRef<Path>, s: &mut String) -> io::Result<()> {
205	s.clear();
206	let mut file = File::open(path)?;
207	file.read_to_string(s)
208		.map(|_| ())
209}
210
211
212fn cstr(path: impl AsRef<Path>) -> io::Result<CString> {
213	CString::new(path.as_ref().as_os_str().as_bytes())
214		.map_err(From::from)
215}
216
217// see https://man7.org/linux/man-pages/man2/fstatfs.2.html
218pub fn statfs(path: impl AsRef<Path>) -> io::Result<libc::statfs> {
219	unsafe {
220		let mut stat = mem::MaybeUninit::<libc::statfs>::uninit();
221		let c = cstr(path)?;
222		let r = libc::statfs(c.as_ptr(), stat.as_mut_ptr());
223		match r {
224			0 => Ok(stat.assume_init()),
225			-1 => Err(io::Error::last_os_error()),
226			_ => panic!("unexpected return value from statfs {:?}", r)
227		}
228	}
229}
230
231// BLKSSZGET
232
233pub fn blkdev_sector_size(fd: impl AsRawFd) -> io::Result<u64> {
234	let s = unsafe {
235		let mut size: c_int = 0;// todo set default to 512
236		match blksszget(fd.as_raw_fd(), &mut size) {
237			-1 => return Err(io::Error::last_os_error()),
238			_ => size
239		}
240	};
241
242	s.try_into()
243		.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
244}
245
246#[cfg(any(
247	target_arch = "x86",
248	target_arch = "arm",
249	target_arch = "x86_64",
250	target_arch = "aarch64"
251))]
252unsafe fn blksszget(fd: c_int, data: *mut c_int) -> c_int {
253	let nr = (0x12 << 8) | (104 << 0);
254	libc::ioctl(fd, nr, data)
255}
256
257#[cfg(test)]
258mod tests {
259	use super::*;
260
261	#[test]
262	fn test_size() {
263		let size = DataSize::from_str("24576 kB").unwrap();
264		assert_eq!(size.to(&DataSizeUnit::Kb), 24576.0);
265	}
266
267	#[test]
268	fn size_str() {
269		// TODO update the formatter
270		let s = DataSize::from_str("1024").unwrap();
271		assert_eq!(s.to_string(), "1 kb");
272		let s = DataSize::from_str("10 kb").unwrap();
273		assert_eq!(s.to_string(), "10 kb");
274		let s = DataSize::from_str("42.1 mB").unwrap();
275		assert_eq!(s.to_string(), "42.1 mb");
276		let s = DataSize::from_str("4.22 Gb").unwrap();
277		assert_eq!(s.to_string(), "4.22 gb");
278		let s = DataSize::from_str("2000 Tb").unwrap();
279		assert_eq!(s.to_string(), "2000 tb");
280		// and precision
281		assert_eq!(format!("{:.0}", DataSize::from_str("1.2 kb").unwrap()), "1 kb");
282	}
283
284	#[test]
285	fn test_precision() {
286		assert_eq!(calculate_precision(0.00005, 4), 4);
287		assert_eq!(calculate_precision(0.00001, 4), 0);
288		assert_eq!(calculate_precision(0.0001, 4), 4);
289		assert_eq!(calculate_precision(0.001, 4), 3);
290		assert_eq!(calculate_precision(0.01, 4), 2);
291		assert_eq!(calculate_precision(0.1, 4), 1);
292		assert_eq!(calculate_precision(0.0, 4), 0);
293	}
294
295	#[test]
296	fn run_statfs() {
297		statfs("/").unwrap();
298	}
299
300}