rust_apt/
util.rs

1//! Contains miscellaneous helper utilities.
2use std::cmp::Ordering;
3use std::env;
4
5use crate::error::AptErrors;
6use crate::{Cache, DepFlags, Package, config};
7
8fn env_usize_nonzero(key: &str) -> Option<usize> {
9	env::var(key).ok()?.parse().ok().filter(|v: &usize| *v > 0)
10}
11
12fn ioctl_terminal_size() -> Option<(usize, usize)> {
13	use std::ffi::{c_int, c_ulong};
14	use std::io::{stderr, stdin, stdout};
15	use std::mem::MaybeUninit;
16	use std::os::unix::io::AsRawFd;
17
18	#[repr(C)]
19	struct Winsize {
20		row: u16,
21		col: u16,
22		x_pixel: u16,
23		y_pixel: u16,
24	}
25
26	extern "C" {
27		fn ioctl(fd: c_int, request: c_ulong, ...) -> c_int;
28	}
29
30	const TIOCGWINSZ: c_ulong = 0x5413;
31
32	fn query(fd: c_int) -> Option<(usize, usize)> {
33		let mut win_size = MaybeUninit::<Winsize>::uninit();
34		let result = unsafe { ioctl(fd, TIOCGWINSZ, win_size.as_mut_ptr()) };
35		if result != 0 {
36			return None;
37		}
38		let win_size = unsafe { win_size.assume_init() };
39		let cols = usize::from(win_size.col);
40		let rows = usize::from(win_size.row);
41		(cols != 0 && rows != 0).then_some((cols, rows))
42	}
43
44	query(stdout().as_raw_fd())
45		.or_else(|| query(stderr().as_raw_fd()))
46		.or_else(|| query(stdin().as_raw_fd()))
47}
48
49/// Get the terminal's height, i.e. the number of rows it has.
50///
51/// # Returns:
52/// * The terminal height, or `24` if it cannot be determined.
53pub fn terminal_height() -> usize {
54	ioctl_terminal_size()
55		.map(|(_, rows)| rows)
56		.or_else(|| env_usize_nonzero("LINES"))
57		.unwrap_or(24)
58}
59
60/// Get the terminal's width, i.e. the number of columns it has.
61///
62/// # Returns:
63/// * The terminal width, or `80` if it cannot be determined.
64pub fn terminal_width() -> usize {
65	ioctl_terminal_size()
66		.map(|(cols, _)| cols)
67		.or_else(|| env_usize_nonzero("COLUMNS"))
68		.unwrap_or(80)
69}
70
71/// Compares two package versions, `ver1` and `ver2`. The returned enum variant
72/// applies to the first version passed in.
73///
74/// # Examples
75/// ```
76/// use rust_apt::util::cmp_versions;
77/// use std::cmp::Ordering;
78///
79/// let ver1 = "5.0";
80/// let ver2 = "6.0";
81/// let result = cmp_versions(ver1, ver2);
82///
83/// assert_eq!(Ordering::Less, result);
84/// ```
85pub fn cmp_versions(ver1: &str, ver2: &str) -> Ordering {
86	let result = raw::cmp_versions(ver1, ver2);
87	match result {
88		_ if result < 0 => Ordering::Less,
89		_ if result == 0 => Ordering::Equal,
90		_ => Ordering::Greater,
91	}
92}
93
94/// Disk Space that `apt` will use for a transaction.
95pub enum DiskSpace {
96	/// Additional Disk Space required.
97	Require(u64),
98	/// Disk Space that will be freed
99	Free(u64),
100}
101
102/// Numeral System for unit conversion.
103pub enum NumSys {
104	/// Base 2 | 1024 | KibiByte (KiB)
105	Binary,
106	/// Base 10 | 1000 | KiloByte (KB)
107	Decimal,
108}
109
110/// Converts bytes into human readable output.
111///
112/// ```
113/// use rust_apt::new_cache;
114/// use rust_apt::util::{unit_str, NumSys};
115/// let cache = new_cache!().unwrap();
116/// let pkg = cache.get("apt").unwrap();
117/// let version = pkg.candidate().unwrap();
118///
119/// println!("{}", unit_str(version.size(), NumSys::Decimal));
120/// ```
121pub fn unit_str(val: u64, base: NumSys) -> String {
122	let val = val as f64;
123	let (num, tera, giga, mega, kilo) = match base {
124		NumSys::Binary => (1024.0_f64, "TiB", "GiB", "MiB", "KiB"),
125		NumSys::Decimal => (1000.0_f64, "TB", "GB", "MB", "KB"),
126	};
127
128	let powers = [
129		(num.powi(4), tera),
130		(num.powi(3), giga),
131		(num.powi(2), mega),
132		(num, kilo),
133	];
134
135	for (divisor, unit) in powers {
136		if val > divisor {
137			return format!("{:.2} {unit}", val / divisor);
138		}
139	}
140	format!("{val} B")
141}
142
143/// Converts seconds into a human readable time string.
144pub fn time_str(seconds: u64) -> String {
145	if seconds > 60 * 60 * 24 {
146		return format!(
147			"{}d {}h {}min {}s",
148			seconds / 60 / 60 / 24,
149			(seconds / 60 / 60) % 24,
150			(seconds / 60) % 60,
151			seconds % 60,
152		);
153	}
154	if seconds > 60 * 60 {
155		return format!(
156			"{}h {}min {}s",
157			(seconds / 60 / 60) % 24,
158			(seconds / 60) % 60,
159			seconds % 60,
160		);
161	}
162	if seconds > 60 {
163		return format!("{}min {}s", (seconds / 60) % 60, seconds % 60,);
164	}
165	format!("{seconds}s")
166}
167
168/// Get an APT-styled progress bar.
169///
170/// # Returns:
171/// * [`String`] representing the progress bar.
172///
173/// # Example:
174/// ```
175/// use rust_apt::util::get_apt_progress_string;
176/// let progress = get_apt_progress_string(0.5, 10);
177/// assert_eq!(progress, "[####....]");
178/// ```
179pub fn get_apt_progress_string(percent: f32, output_width: u32) -> String {
180	raw::get_apt_progress_string(percent, output_width)
181}
182
183/// Lock the APT lockfile.
184/// This should be done before modifying any APT files
185/// such as with [`crate::cache::Cache::update`]
186/// and then [`apt_unlock`] should be called after.
187///
188/// This Function Requires root
189///
190/// If [`apt_lock`] is called `n` times, [`apt_unlock`] must also be called `n`
191/// times to release all acquired locks.
192///
193/// # Known Error Messages:
194/// * `E:Could not open lock file /var/lib/dpkg/lock-frontend - open (13:
195///   Permission denied)`
196/// * `E:Unable to acquire the dpkg frontend lock (/var/lib/dpkg/lock-frontend),
197///   are you root?`
198pub fn apt_lock() -> Result<(), AptErrors> {
199	config::init_config_system();
200	Ok(raw::apt_lock()?)
201}
202
203/// Unlock the APT lockfile.
204pub fn apt_unlock() {
205	config::init_config_system();
206	raw::apt_unlock()
207}
208
209/// Unlock the Dpkg lockfile.
210/// This should be done before manually running
211/// [`crate::cache::Cache::do_install`]
212/// and then [`apt_unlock_inner`] should be called after.
213///
214/// This Function Requires root
215pub fn apt_lock_inner() -> Result<(), AptErrors> {
216	config::init_config_system();
217	Ok(raw::apt_lock_inner()?)
218}
219
220/// Unlock the Dpkg lockfile.
221pub fn apt_unlock_inner() {
222	config::init_config_system();
223	raw::apt_unlock_inner()
224}
225
226/// Checks if any locks are currently active for the lockfile. Note that this
227/// will only return [`true`] if the current process has an active lock, while
228/// calls to [`apt_lock`] will return an [`AptErrors`] if another process has an
229/// active lock.
230pub fn apt_is_locked() -> bool {
231	config::init_config_system();
232	raw::apt_is_locked()
233}
234
235/// Reference implementation to print broken packages just like apt does.
236///
237/// ## Returns [`None`] if the package is not considered broken
238///
239/// ## now:
240///   * [true] = When checking broken packages before modifying the cache.
241///   * [false] = When checking broken packages after modifying the cache.
242pub fn show_broken_pkg(cache: &Cache, pkg: &Package, now: bool) -> Option<String> {
243	// If the package isn't broken for the state Return None
244	if (now && !pkg.is_now_broken()) || (!now && !pkg.is_inst_broken()) {
245		return None;
246	};
247
248	let mut broken_string = String::new();
249
250	broken_string += &format!(" {pkg} :");
251
252	// Pick the proper version based on now status.
253	// else Return with just the package name like Apt does.
254	let Some(ver) = (match now {
255		true => pkg.installed(),
256		false => pkg.install_version(),
257	}) else {
258		broken_string += "\n";
259		return Some(broken_string);
260	};
261
262	let indent = pkg.name().len() + 3;
263	let mut first = true;
264
265	// ShowBrokenDeps
266	for dep in ver.depends_map().values().flatten() {
267		for (i, base_dep) in dep.iter().enumerate() {
268			if !cache.depcache().is_important_dep(base_dep) {
269				continue;
270			}
271
272			let dep_flag = if now { DepFlags::DepGNow } else { DepFlags::DepInstall };
273
274			if cache.depcache().dep_state(base_dep) & dep_flag == dep_flag {
275				continue;
276			}
277
278			if !first {
279				broken_string += &" ".repeat(indent);
280			}
281			first = false;
282
283			// If it's the first or Dep
284			if i > 0 {
285				broken_string += &" ".repeat(base_dep.dep_type().as_ref().len() + 3);
286			} else {
287				broken_string += &format!(" {}: ", base_dep.dep_type())
288			}
289
290			broken_string += base_dep.target_package().name();
291
292			if let (Ok(ver_str), Some(comp)) = (base_dep.target_ver(), base_dep.comp_type()) {
293				broken_string += &format!(" ({comp} {ver_str})");
294			}
295
296			let target = base_dep.target_package();
297			if !target.has_provides() {
298				if let Some(target_ver) = target.install_version() {
299					broken_string += &format!(" but {target_ver} is to be installed")
300				} else if target.candidate().is_some() {
301					broken_string += " but it is not going to be installed";
302				} else if target.has_provides() {
303					broken_string += " but it is a virtual package";
304				} else {
305					broken_string += " but it is not installable";
306				}
307			}
308
309			if i + 1 != dep.len() {
310				broken_string += " or"
311			}
312			broken_string += "\n";
313		}
314	}
315	Some(broken_string)
316}
317
318#[cxx::bridge]
319pub(crate) mod raw {
320	unsafe extern "C++" {
321		include!("rust-apt/apt-pkg-c/util.h");
322
323		/// Compares two package versions, `ver1` and `ver2`. The returned
324		/// integer's value is mapped to one of the following integers:
325		/// - Less than 0: `ver1` is less than `ver2`.
326		/// - Equal to 0: `ver1` is equal to `ver2`.
327		/// - Greater than 0: `ver1` is greater than `ver2`.
328		///
329		/// Unless you have a specific need for otherwise, you should probably
330		/// use [`crate::util::cmp_versions`] instead.
331		pub fn cmp_versions(ver1: &str, ver2: &str) -> i32;
332
333		pub fn quote_string(string: &str, bad: String) -> String;
334
335		/// Return an APT-styled progress bar (`[####..]`).
336		pub fn get_apt_progress_string(percent: f32, output_width: u32) -> String;
337
338		/// Lock the lockfile.
339		pub fn apt_lock() -> Result<()>;
340
341		/// Unock the lockfile.
342		pub fn apt_unlock();
343
344		/// Lock the Dpkg lockfile.
345		pub fn apt_lock_inner() -> Result<()>;
346
347		/// Unlock the Dpkg lockfile.
348		pub fn apt_unlock_inner();
349
350		/// Check if the lockfile is locked.
351		pub fn apt_is_locked() -> bool;
352	}
353}