use crate::models::*;
use crate::mmio;
use crate::models::PACKAGE_CACHE;
use crate::io::load_installed_packages;
use crate::utils::format_size;
use color_eyre::Result;
use memchr::{memchr, memmem::Finder};
use glob::Pattern;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
static HEADERS_PRINTED: AtomicBool = AtomicBool::new(false);
static ACCUM_TOTAL_SIZE: AtomicU64 = AtomicU64::new(0);
static ACCUM_TOTAL_INSTALLED_SIZE: AtomicU64 = AtomicU64::new(0);
static ACCUM_PACKAGE_COUNT: AtomicU64 = AtomicU64::new(0);
fn reset_display_state() {
HEADERS_PRINTED.store(false, Ordering::SeqCst);
ACCUM_TOTAL_SIZE.store(0, Ordering::SeqCst);
ACCUM_TOTAL_INSTALLED_SIZE.store(0, Ordering::SeqCst);
ACCUM_PACKAGE_COUNT.store(0, Ordering::SeqCst);
}
#[derive(Debug, Clone, PartialEq)]
pub enum ListScope {
Installed,
Available,
Upgradable,
All,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ListType {
Installed,
Upgradable,
Available,
}
#[derive(Debug, Clone)]
pub struct PackageListItem {
pub pkgname: String,
pub version: String,
pub arch: String,
pub repodata_name: String,
pub summary: String,
pub status: String,
pub depth: u16,
pub size: u32,
pub installed_size: u32,
#[allow(dead_code)]
pub pkgkey: String,
#[allow(dead_code)]
pub installed_info: Option<InstalledPackageInfo>,
}
pub fn list_packages_with_scope(scope: ListScope, pattern: &str) -> Result<()> {
reset_display_state();
crate::repo::sync_channel_metadata()?;
load_installed_packages()?;
let mut packages_found_overall = 0;
match scope {
ListScope::Installed => {
packages_found_overall += process_installed_packages(pattern, ListType::Installed)?;
},
ListScope::Upgradable => {
packages_found_overall += process_installed_packages(pattern, ListType::Upgradable)?;
},
ListScope::Available => {
packages_found_overall += process_available_packages(pattern)?;
},
ListScope::All => {
let installed_count = process_installed_packages(pattern, ListType::Installed)?;
packages_found_overall += installed_count;
PACKAGE_CACHE.pkgkey2package.write().unwrap().clear();
let available_count = process_available_packages(pattern)?;
packages_found_overall += available_count;
}
}
if packages_found_overall > 0 {
let total_packages = ACCUM_PACKAGE_COUNT.load(Ordering::SeqCst);
let total_size = ACCUM_TOTAL_SIZE.load(Ordering::SeqCst);
let total_installed_size = ACCUM_TOTAL_INSTALLED_SIZE.load(Ordering::SeqCst);
println!("\nTotal: {} packages, {}, {} if installed", total_packages, format_size(total_size), format_size(total_installed_size));
} else {
if pattern.is_empty() {
println!("No packages found in scope {:?}.", scope);
} else {
println!("No packages found matching pattern '{}' in scope {:?}.", pattern, scope);
}
}
Ok(())
}
fn process_installed_packages(pattern: &str, list_type: ListType) -> Result<usize> {
let mut local_items = Vec::new();
let upgradable_only = matches!(list_type, ListType::Upgradable);
let pattern_glob = if pattern.is_empty() || pattern == "*" {
None
} else {
match Pattern::new(pattern) {
Ok(p) => Some(p),
Err(_) => return Ok(0),
}
};
let installed_data: Vec<(String, Arc<InstalledPackageInfo>)> = PACKAGE_CACHE.installed_packages
.read()
.unwrap()
.iter()
.map(|(k, v)| (k.clone(), Arc::clone(v)))
.collect();
for (pkgkey, installed_info) in installed_data {
let pkgname = match crate::package::pkgkey2pkgname(&pkgkey) {
Ok(name) => name,
Err(_) => {
log::debug!("Skipping invalid pkgkey: {}", pkgkey);
continue;
}
};
if let Some(ref pat) = pattern_glob {
if !pat.matches(&pkgname) {
continue;
}
}
if upgradable_only {
let is_upgradable = is_package_upgradable(&pkgname, &installed_info).unwrap_or(false);
if !is_upgradable {
continue;
}
}
match create_installed_package_item(&pkgname, &pkgkey, &installed_info) {
Ok(item) => local_items.push(item),
Err(e) => log::warn!("Failed to create item for installed package {}: {}", pkgname, e),
}
}
let count = local_items.len();
sort_and_display_packages(&mut local_items, list_type)?;
Ok(count)
}
fn process_available_packages(pattern: &str) -> Result<usize> {
if pattern.is_empty() || pattern == "*" {
return process_all_available_packages(ListType::Available);
} else {
return process_few_available_packages(pattern, ListType::Available);
};
}
fn process_few_available_packages(pattern: &str, list_type: ListType) -> Result<usize> {
let mut local_items = Vec::new();
let matching_pkgnames = collect_matching_pkgnames(pattern)?;
for pkgname in matching_pkgnames {
match mmio::map_pkgname2packages(&pkgname) {
Ok(packages) => {
for pkg in packages {
if PACKAGE_CACHE.installed_packages.read().unwrap().contains_key(&pkg.pkgkey) {
continue;
}
match create_available_package_item(&pkg) {
Ok(item) => local_items.push(item),
Err(e) => log::warn!("Failed to create item for available package {}: {}", pkgname, e),
}
}
},
Err(e) => log::warn!("Failed to get package details for {}: {}", pkgname, e),
}
}
let count = local_items.len();
sort_and_display_packages(&mut local_items, list_type)?;
Ok(count)
}
fn collect_matching_pkgnames(pattern: &str) -> Result<Vec<String>> {
let mut repodata_indice = crate::models::repodata_indice_mut();
let mut matching_pkgnames = Vec::new();
if !pattern.contains('*') {
for repo_index in repodata_indice.values_mut() {
for shard in repo_index.repo_shards.values_mut() {
mmio::ensure_pkgname2ranges_loaded(shard)?;
if shard.pkgname2ranges.contains_key(pattern) {
matching_pkgnames.push(pattern.to_string());
return Ok(matching_pkgnames);
}
}
}
return Ok(matching_pkgnames);
}
if pattern.ends_with('*') && !pattern[..pattern.len()-1].contains('*') {
let prefix = &pattern[..pattern.len()-1];
for repo_index in repodata_indice.values_mut() {
for shard in repo_index.repo_shards.values_mut() {
mmio::ensure_pkgname2ranges_loaded(shard)?;
let range = shard.pkgname2ranges.range(prefix.to_string()..);
for (pkgname, _) in range {
if pkgname.starts_with(prefix) {
matching_pkgnames.push(pkgname.clone());
} else {
break;
}
}
}
}
return Ok(matching_pkgnames);
}
let mut handles = Vec::new();
let pattern = pattern.to_string();
for repo_index in repodata_indice.values_mut() {
for shard in repo_index.repo_shards.values_mut() {
mmio::ensure_pkgname2ranges_loaded(shard)?;
let shard_pkgnames: Vec<String> = shard.pkgname2ranges.keys().cloned().collect();
let pattern_clone = pattern.clone();
let handle = std::thread::spawn(move || {
let mut local_matches = Vec::new();
let pat = match Pattern::new(&pattern_clone) {
Ok(p) => p,
Err(_) => return local_matches,
};
for pkgname in shard_pkgnames {
if pat.matches(&pkgname) {
local_matches.push(pkgname);
}
}
local_matches
});
handles.push(handle);
}
}
for handle in handles {
match handle.join() {
Ok(thread_matches) => matching_pkgnames.extend(thread_matches),
Err(_) => log::warn!("Thread failed to complete"),
}
}
Ok(matching_pkgnames)
}
fn process_all_available_packages(list_type: ListType) -> Result<usize> {
let mut repodata_indice = crate::models::repodata_indice_mut();
let mut count = 0;
let mut estimated_total_packages = 0;
for repo_index in repodata_indice.values_mut() {
for shard in repo_index.repo_shards.values_mut() {
estimated_total_packages += shard.packages.nr_packages;
shard.pkgname2ranges.clear();
}
}
let mut local_items = Vec::with_capacity(estimated_total_packages);
for repo_index in repodata_indice.values_mut() {
for shard in repo_index.repo_shards.values_mut() {
if let Some(mmap) = &shard.packages_mmap {
count += scan_packages_mmap(
mmap,
&repo_index.repodata_name,
&mut local_items,
)?;
}
}
}
sort_and_display_packages(&mut local_items, list_type)?;
Ok(count)
}
fn scan_packages_mmap(
file_mapper: &crate::mmio::FileMapper,
repodata_name: &str,
local_items: &mut Vec<PackageListItem>,
) -> Result<usize> {
#[cfg(target_os = "linux")]
use libc::{madvise, c_void, MADV_DONTNEED};
let data = file_mapper.data();
let mut count = 0;
let mut pos = 0;
let finder = Finder::new(b"pkgname: ");
let mut pkgname: &[u8] = &[];
let mut version: &[u8] = &[];
let mut arch: &[u8] = &[];
let mut summary: &[u8] = &[];
let mut size: u32 = 0;
let mut installed_size: u32 = 0;
let mut nr_found_fields = 0;
#[cfg(target_os = "linux")]
let mut last_advised = 0;
#[cfg(target_os = "linux")]
const MMAP_DROP_GRANULARITY: usize = 2 * 1024 * 1024;
while pos < data.len() {
if nr_found_fields == 0 {
if let Some(found) = finder.find(&data[pos..]) {
pos += found;
let line_end = memchr(b'\n', &data[pos..]).map(|i| pos + i).unwrap_or(data.len());
let line = &data[pos..line_end];
if line.starts_with(b"pkgname: ") {
pkgname = &line[b"pkgname: ".len()..];
nr_found_fields = 1;
pos = line_end + 1;
} else {
pos = line_end + 1;
continue;
}
} else {
break;
}
} else {
let line_end = memchr(b'\n', &data[pos..]).map(|i| pos + i).unwrap_or(data.len());
let line = &data[pos..line_end];
if line.is_empty() {
nr_found_fields = 6;
}
if line.starts_with(b"version: ") {
version = &line[b"version: ".len()..];
nr_found_fields += 1;
} else if line.starts_with(b"arch: ") {
arch = &line[b"arch: ".len()..];
nr_found_fields += 1;
} else if line.starts_with(b"summary: ") {
summary = &line[b"summary: ".len()..];
nr_found_fields += 1;
} else if line.starts_with(b"size: ") {
if let Ok(parsed) = std::str::from_utf8(&line[b"size: ".len()..]).unwrap_or("0").trim().parse() {
size = parsed;
nr_found_fields += 1;
}
} else if line.starts_with(b"installedSize: ") {
if let Ok(parsed) = std::str::from_utf8(&line[b"installedSize: ".len()..]).unwrap_or("0").trim().parse() {
installed_size = parsed;
nr_found_fields += 1;
}
}
pos = line_end + 1;
if nr_found_fields >= 6 {
count += handle_completed_package_bytes(
pkgname,
version,
arch,
summary,
size,
installed_size,
repodata_name,
local_items,
)?;
pkgname = &[];
version = &[];
arch = &[];
summary = &[];
size = 0;
installed_size = 0;
nr_found_fields = 0;
}
}
#[cfg(target_os = "linux")]
{
let next_advisable = (pos / MMAP_DROP_GRANULARITY) * MMAP_DROP_GRANULARITY;
if next_advisable > last_advised {
let advise_ptr = unsafe { data.as_ptr().add(last_advised) as *mut c_void };
let advise_len = next_advisable - last_advised;
unsafe {
madvise(advise_ptr, advise_len, MADV_DONTNEED);
}
last_advised = next_advisable;
}
}
}
Ok(count)
}
fn handle_completed_package_bytes(
pkgname: &[u8],
version: &[u8],
arch: &[u8],
summary: &[u8],
size: u32,
installed_size: u32,
repodata_name: &str,
local_items: &mut Vec<PackageListItem>,
) -> Result<usize> {
if !pkgname.is_empty() {
let pkgname_str = std::str::from_utf8(pkgname)?.trim();
let version_str = std::str::from_utf8(version)?.trim();
let arch_str = std::str::from_utf8(arch)?.trim();
let summary_str = std::str::from_utf8(summary).unwrap_or("").trim();
let pkgkey = crate::package::format_pkgkey(pkgname_str, version_str, arch_str);
if !PACKAGE_CACHE.installed_packages.read().unwrap().contains_key(&pkgkey) {
let status = determine_status_for_available(pkgname_str)?;
let item = PackageListItem {
pkgname: pkgname_str.to_owned(),
version: version_str.to_owned(),
arch: arch_str.to_owned(),
repodata_name: repodata_name.to_owned(),
summary: summary_str.to_owned(),
status,
depth: 0,
size,
installed_size,
pkgkey: pkgkey.to_owned(),
installed_info: None,
};
local_items.push(item);
return Ok(1);
}
}
Ok(0)
}
fn sort_and_display_packages(package_items: &mut Vec<PackageListItem>, list_type: ListType) -> Result<()> {
match list_type {
ListType::Available => {
package_items.sort_by(|a, b| a.pkgname.cmp(&b.pkgname));
}
ListType::Installed | ListType::Upgradable => {
package_items.sort_by(|a, b| {
match a.depth.cmp(&b.depth) {
std::cmp::Ordering::Equal => a.pkgname.cmp(&b.pkgname),
other => other,
}
});
}
}
display_package_list(package_items)?;
Ok(())
}
fn create_installed_package_item(pkgname: &str, pkgkey: &str, installed_info: &InstalledPackageInfo) -> Result<PackageListItem> {
let (version, arch, summary, repodata_name, size, installed_size) = match mmio::map_pkgkey2package(pkgkey) {
Ok(pkg) => (
pkg.version.clone(),
pkg.arch.clone(),
pkg.summary.clone(),
pkg.repodata_name.clone(),
pkg.size,
pkg.installed_size,
),
Err(_) => {
match crate::package_cache::map_pkgline2package(&installed_info.pkgline) {
Ok(local_pkg) => (
local_pkg.version.clone(),
local_pkg.arch.clone(),
local_pkg.summary.clone(),
"local".to_string(),
local_pkg.size,
local_pkg.installed_size,
),
Err(_) => {
(
"unknown".to_string(),
config().common.arch.clone(),
"Package not found in repositories or local store".to_string(),
"orphaned".to_string(),
0,
0,
)
}
}
}
};
let (status, depth) = determine_status_for_installed(pkgname, installed_info)?;
Ok(PackageListItem {
pkgname: pkgname.to_string(),
version,
arch,
repodata_name,
summary,
status,
depth,
size,
installed_size,
pkgkey: installed_info.pkgline.clone(),
installed_info: Some(installed_info.clone()),
})
}
fn create_available_package_item(pkg: &Package) -> Result<PackageListItem> {
let status = determine_status_for_available(&pkg.pkgname)?;
Ok(PackageListItem {
pkgname: pkg.pkgname.clone(),
version: pkg.version.clone(),
arch: pkg.arch.clone(),
repodata_name: pkg.repodata_name.clone(),
summary: pkg.summary.clone(),
status,
depth: 0,
size: pkg.size,
installed_size: pkg.installed_size,
pkgkey: pkg.pkgkey.clone(),
installed_info: None,
})
}
fn determine_status_for_installed(pkgname: &str, installed_info: &InstalledPackageInfo) -> Result<(String, u16)> {
let pos1 = if installed_info.ebin_exposure { 'E' } else { 'I' };
let pos3 = if is_package_upgradable(pkgname, installed_info).unwrap_or(false) {
'U'
} else {
' '
};
let status = format!("{}{}", pos1, pos3);
let depth = installed_info.depend_depth;
Ok((status, depth))
}
fn determine_status_for_available(_pkgname: &str) -> Result<String> {
let pos1 = 'A';
let pos3 = ' ';
Ok(format!("{}{}", pos1, pos3))
}
fn is_package_upgradable(pkgname: &str, installed_info: &InstalledPackageInfo) -> Result<bool> {
let installed_version = extract_version_from_installed_info(installed_info)?;
let available_packages = crate::package_cache::map_pkgname2packages(pkgname)?;
for pkg in available_packages {
if pkg.arch == installed_info.arch {
if is_version_newer(&pkg.version, &installed_version) {
return Ok(true);
}
}
}
Ok(false)
}
fn extract_version_from_installed_info(installed_info: &InstalledPackageInfo) -> Result<String> {
match crate::package::parse_pkgline(&installed_info.pkgline) {
Ok(package_line) => Ok(package_line.version),
Err(e) => {
log::warn!("Failed to parse package line '{}': {}", installed_info.pkgline, e);
Ok("unknown".to_string())
}
}
}
fn is_version_newer(new_version: &str, current_version: &str) -> bool {
crate::version_compare::is_version_newer(new_version, current_version)
}
fn print_headers_if_needed(headers: &[&str], col_widths: &[usize]) {
if !HEADERS_PRINTED.load(Ordering::SeqCst) {
println!("Exposed/Installed/Available");
println!("| Upgradable");
}
if !HEADERS_PRINTED.load(Ordering::SeqCst) {
for (i, header) in headers.iter().enumerate() {
if i == 1 || i == 2 {
print!("{:>width$}", header, width = col_widths[i]);
} else {
print!("{:<width$}", header, width = col_widths[i]);
}
if i < headers.len() - 1 {
print!(" ");
}
}
println!();
for (i, &width) in col_widths.iter().enumerate() {
print!("{}", "=".repeat(width));
if i < col_widths.len() - 1 {
print!("=-");
}
}
println!();
HEADERS_PRINTED.store(true, Ordering::SeqCst);
}
}
fn compute_table_config() -> ([usize; 8], [usize; 8], [bool; 8]) {
let col_widths = [2, 5, 9, 36, 30, 11, 18, 60];
let mut starts = [0; 8];
let mut pos = 0;
for i in 0..8 {
starts[i] = pos;
pos += col_widths[i];
if i < 7 {
pos += 2;
}
}
let right_aligned = [false, true, true, false, false, false, false, false];
(col_widths, starts, right_aligned)
}
fn print_row_with_shift_absorption(
row_cells: &[&str],
col_widths: &[usize; 8],
starts: &[usize; 8],
right_aligned: &[bool; 8],
) {
let mut shift = 0;
let mut pos = 0;
for i in 0..8 {
let col_width = col_widths[i];
let cell = row_cells[i];
let width = cell.len();
let spare = col_width.saturating_sub(width);
let absorbed = if spare > 0 { spare.min(shift) } else { 0 };
shift -= absorbed;
let content_start = starts[i] + absorbed;
if pos < content_start {
print!("{:>width$}", "", width = content_start - pos);
pos = content_start;
}
if right_aligned[i] {
if width <= col_width {
let left_padding = col_width - width;
print!("{:>width$}", "", width = left_padding);
print!("{}", cell);
pos += col_width;
} else {
print!("{}", cell);
pos += width;
}
} else {
print!("{}", cell);
pos += width;
}
let effective_width = col_width - absorbed;
let overflow = width.saturating_sub(effective_width);
if overflow > 0 {
shift += overflow;
}
if i < 7 {
let spaces_after = 2_usize;
print!("{: <width$}", "", width = spaces_after);
pos += spaces_after;
}
}
println!();
}
fn display_package_list(items: &[PackageListItem]) -> Result<()> {
let has_items = !items.is_empty();
let headers = vec!["|/", "Depth", "Size", "Name", "Version", "Arch", "Repo", "Description"];
let (col_widths, starts, right_aligned) = compute_table_config();
if has_items {
print_headers_if_needed(&headers, &col_widths);
}
let mut prev_pkgkey = "";
let mut batch_package_count = 0;
if has_items {
for item in items {
if item.pkgkey == prev_pkgkey {
continue;
}
prev_pkgkey = &item.pkgkey;
batch_package_count += 1;
let depth_str = item.depth.to_string();
let size_str = format_size(item.size as u64);
let description = item.summary.clone();
ACCUM_TOTAL_SIZE.fetch_add(item.size as u64, Ordering::SeqCst);
ACCUM_TOTAL_INSTALLED_SIZE.fetch_add(item.installed_size as u64, Ordering::SeqCst);
let row_cells = [
item.status.as_str(),
depth_str.as_str(),
size_str.as_str(),
item.pkgname.as_str(),
item.version.as_str(),
item.arch.as_str(),
item.repodata_name.as_str(),
description.as_str(),
];
print_row_with_shift_absorption(&row_cells, &col_widths, &starts, &right_aligned);
}
}
ACCUM_PACKAGE_COUNT.fetch_add(batch_package_count, Ordering::SeqCst);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dynamic_padding() {
let items = vec![
PackageListItem {
pkgname: "short".to_string(),
version: "1.0".to_string(),
arch: "x86_64".to_string(),
repodata_name: "main".to_string(),
summary: "Test package".to_string(),
status: "A ".to_string(),
depth: 0,
size: 1024,
installed_size: 2048,
pkgkey: "short-1.0-x86_64".to_string(),
installed_info: None,
},
PackageListItem {
pkgname: "a-very-long-package-name-that-exceeds-column-width".to_string(),
version: "20250814.1-r0".to_string(),
arch: "x86_64".to_string(),
repodata_name: "main".to_string(),
summary: "Long package".to_string(),
status: "A ".to_string(),
depth: 0,
size: 2048,
installed_size: 4096,
pkgkey: "long-20250814.1-r0-x86_64".to_string(),
installed_info: None,
},
];
let result = display_package_list(&items);
assert!(result.is_ok());
}
}