use std::{ io::Write, path::{Path, PathBuf}, process::{Command, Stdio}, }; use based::get_pg; use serde::Serialize; use sqlx::FromRow; use super::{Repository, arch::Architecture}; /// General Package #[derive(Debug, Clone)] pub struct Package { /// Repository of the package pub repo: String, /// `Architecture` of the package pub arch: Architecture, /// Name of the package pub name: String, pub rel: String, /// Version of the package pub version: Option, /// Compression used pub compression: Compression, } pub struct PackageFile { pub path: PathBuf, pub compression: Compression, } impl PackageFile { pub fn new(path: PathBuf) -> Self { Self { path: path.clone(), compression: Compression::from_filename(path.to_str().unwrap()).unwrap(), } } pub fn pkginfo(&self) -> PackageInfo { let content = read_file_tar(&self.path, ".PKGINFO", self.compression.clone()).unwrap(); let keys = Package::pkginfo_from_str(&content); PackageInfo::new(keys) } pub fn pkginfo_raw(&self) -> Vec<(String, String)> { let content = read_file_tar(&self.path, ".PKGINFO", self.compression.clone()).unwrap(); Package::pkginfo_from_str(&content) } } impl Package { /// Create a new package pub fn new( repo: &str, arch: Architecture, pkg_name: &str, version: &str, rel: String, compression: Compression, ) -> Self { let pkg = Package { repo: repo.to_string(), arch, name: pkg_name.to_string(), rel: rel, version: Some(version.to_string()), compression, }; std::fs::create_dir_all(pkg.base_path()).unwrap(); pkg } pub fn version(ver: &str) -> (String, String) { let mut splitted = ver.split('-').collect::>(); let rel = splitted.pop().unwrap(); let ver = splitted.join("-"); (ver, rel.parse().unwrap()) } pub fn install_script(&self) -> Option { let pkg = self.base_path().join(self.file_name()); read_file_tar(&pkg, ".INSTALL", self.compression.clone()) } pub fn file_list(&self) -> Vec { list_tar_file(&self.base_path().join(self.file_name())) .unwrap() .into_iter() .filter(|x| !x.ends_with("/")) .collect() } pub fn binaries(&self) -> Vec { list_tar_file(&self.base_path().join(self.file_name())) .unwrap_or_default() .into_iter() .filter_map(|x| { let mut paths: Vec<_> = x.split("/").collect(); paths.reverse(); let parent = paths.get(1)?; if (*parent == "bin" || *parent == "sbin") && !x.ends_with("/") { return Some(x); } None }) .collect() } pub fn pkginfo_from_str(content: &str) -> Vec<(String, String)> { let mut ret: Vec<(String, Vec)> = Vec::new(); for line in content.split("\n") { if line.starts_with('#') || line.is_empty() { continue; } let (key, val) = line.split_once(" = ").unwrap(); if let Some(e) = ret.iter_mut().find(|x| x.0 == key) { e.1.push(val.trim().to_string()); } else { ret.push((key.to_string(), vec![val.trim().to_string()])); } } let mut ret: Vec<_> = ret.into_iter().map(|x| (x.0, x.1.join(";"))).collect(); ret.sort_by(|a, b| a.0.cmp(&b.0)); ret } pub fn pkginfo(&self) -> PackageInfo { PackageFile::new(self.base_path().join(self.file_name())).pkginfo() } pub fn pkginfo_raw(&self) -> Vec<(String, String)> { PackageFile::new(self.base_path().join(self.file_name())).pkginfo_raw() } pub fn arch(&self) -> Vec { let mut ret = Vec::new(); for a in [ Architecture::x86_64, Architecture::aarch64, Architecture::any, ] { let check_pkg = self.switch_arch(a.clone()); if check_pkg.exists() { ret.push(a); } } ret } /// Extract values from a package filename /// /// # Example /// /// ``` /// use pacco::pkg::Package; /// /// let (name, version, rel, arch, compress) = Package::extract_pkg_name("pkg-ver-rel-x86_64.pkg.tar.zst").unwrap(); /// assert_eq!(name, "pkg"); /// assert_eq!(version, "ver"); /// assert_eq!(rel, "rel"); /// assert_eq!(arch, pacco::pkg::arch::Architecture::x86_64); /// assert_eq!(compress, pacco::pkg::package::Compression::Zstd); /// /// let (name, version, rel, arch, compress) = Package::extract_pkg_name("my-pkg-ver-rel-x86_64.pkg.tar.xz").unwrap(); /// assert_eq!(name, "my-pkg"); /// assert_eq!(version, "ver"); /// assert_eq!(rel, "rel"); /// assert_eq!(arch, pacco::pkg::arch::Architecture::x86_64); /// assert_eq!(compress, pacco::pkg::package::Compression::Xz); /// ``` pub fn extract_pkg_name( file_name: &str, ) -> Option<(String, String, String, Architecture, Compression)> { // Extract (assuming the filename is "---.pkg.tar.zst") let file_name = file_name.trim_end_matches(".sig").to_string(); let mut splitted = file_name.split('-').collect::>(); let arch = splitted.pop()?; let compression = Compression::from_filename(arch); assert!(compression.is_some()); let compression = compression.unwrap(); let arch = match compression { Compression::Zstd => arch.trim_end_matches(".pkg.tar.zst"), Compression::Xz => arch.trim_end_matches(".pkg.tar.xz"), }; let relation = splitted.pop()?; let version = splitted.pop()?; let pkg_name = splitted.join("-"); Some(( pkg_name, version.to_string(), relation.to_string(), Architecture::parse(arch)?, compression, )) } /// Parse a pkg filename pub fn from_filename(repo: &str, file_name: &str) -> Package { let (pkg_name, version, rel, arch, compression) = Package::extract_pkg_name(file_name).unwrap(); Self { repo: repo.to_string(), arch, name: pkg_name, rel: rel.parse().unwrap(), version: Some(version.to_string()), compression, } } /// Find a package with latest version pub fn find(repo: &str, arch: Architecture, pkg_name: &str) -> Option { if let Some(pkg) = Self::find_compressed(repo, arch.clone(), pkg_name, Compression::Zstd) { return Some(pkg); } if let Some(pkg) = Self::find_compressed(repo, arch, pkg_name, Compression::Xz) { return Some(pkg); } None } /// Find a package with latest version fn find_compressed( repo: &str, arch: Architecture, pkg_name: &str, compression: Compression, ) -> Option { let mut base = Package { repo: repo.to_string(), arch, name: pkg_name.to_string(), rel: 1.to_string(), version: None, compression, }; let versions = base.versions(); let ver = versions.first()?; let (ver, rel) = Package::version(&ver); base.version = Some(ver.clone()); base.rel = rel; Some(base) } pub fn man_entries(&self) -> Vec { let files = self.file_list(); files .into_iter() .filter(|x| x.starts_with("usr/share/man")) .map(|x| { x.trim_start_matches("usr/share/man/") .trim_end_matches(".gz") .to_string() }) .collect() } pub fn kernel_modules(&self) -> Vec { let files = self.file_list(); files .into_iter() .filter(|x| x.starts_with("usr/lib/modules")) .map(|x| { x.trim_start_matches("usr/lib/modules/") .trim_end_matches(".zst") .to_string() }) .collect() } pub fn fonts(&self) -> Vec { let files = self.file_list(); files .into_iter() .filter(|x| x.starts_with("usr/share/fonts")) .map(|x| x.trim_start_matches("usr/share/fonts/").to_string()) .collect() } pub fn firmware(&self) -> Vec { let files = self.file_list(); files .into_iter() .filter(|x| x.starts_with("usr/lib/firmware")) .map(|x| { x.trim_start_matches("usr/lib/firmware/") .trim_end_matches(".zst") .to_string() }) .collect() } pub fn keyrings(&self) -> Vec { let files = self.file_list(); files .into_iter() .filter(|x| x.starts_with("usr/share/pacman/keyrings")) .map(|x| { x.trim_start_matches("usr/share/pacman/keyrings/") .to_string() }) .collect() } pub fn shared_objects(&self) -> Vec { list_tar_file(&self.base_path().join(self.file_name())) .unwrap_or_default() .into_iter() .filter(|x| { let file_name = x.split("/").last().unwrap(); file_name.contains(".so.") || file_name.ends_with(".so") }) .collect() } pub fn icons(&self) -> Vec { let files = self.file_list(); files .into_iter() .filter(|x| x.starts_with("usr/share/icons")) .map(|x| x.trim_start_matches("usr/share/icons/").to_string()) .collect() } pub fn etc_entries(&self) -> Vec { let files = self.file_list(); files.into_iter().filter(|x| x.starts_with("etc")).collect() } pub fn systemd_units(&self) -> Vec { // TODO : Extract unit infos list_tar_file(&self.base_path().join(self.file_name())) .unwrap_or_default() .into_iter() .filter(|x| { let ext = x.split(".").last().unwrap(); ext == "service" || ext == "timer" || ext == "mount" || ext == "socket" }) .collect() } pub fn readmes(&self) -> Vec<(String, String)> { let pkg_file = self.base_path().join(self.file_name()); let files = self.file_list(); files .into_iter() .filter(|x| { let file_name = x.split("/").last().unwrap(); let cleaned = file_name.trim_end_matches(".md").trim_end_matches(".txt"); cleaned == "README" }) .map(|x| { let content = read_file_tar(&pkg_file, &x, self.compression.clone()).unwrap(); (x, content) }) .collect() } pub fn pacman_hooks(&self) -> Vec<(String, String)> { let pkg_file = self.base_path().join(self.file_name()); let files = self.file_list(); files .into_iter() .filter(|x| { x.starts_with("etc/pacman.d/hooks/") || x.starts_with("usr/share/libalpm/hooks/") }) .map(|x| { let content = read_file_tar(&pkg_file, &x, self.compression.clone()).unwrap(); (x, content) }) .collect() } /// Save a new package to repository pub fn save(&self, pkg: Vec, sig: Option>) { let pkg_file = self.base_path().join(self.file_name()); let sig_file = self.base_path().join(format!("{}.sig", self.file_name())); std::fs::write(&pkg_file, pkg).unwrap(); if let Some(sig) = sig { std::fs::write(sig_file, sig).unwrap(); } let db_file = PathBuf::from("./data") .join(&self.repo) .join(self.arch.to_string()) .join(format!("{}.db.tar.gz", self.repo)); repo_add(db_file.to_str().unwrap(), pkg_file.to_str().unwrap()); // Add to any arch repo db if `arch=any` if self.arch == Architecture::any { let archs = Repository::new(&self.repo).unwrap().arch(); for arch in archs { if arch == Architecture::any { continue; } let db_file = PathBuf::from("./data") .join(&self.repo) .join(arch.to_string()) .join(format!("{}.db.tar.gz", self.repo)); repo_add(db_file.to_str().unwrap(), pkg_file.to_str().unwrap()); } } } fn base_path(&self) -> PathBuf { // /// Path::new("./data") .join(&self.repo) .join(self.arch.to_string()) .join(&self.name) } /// Switch the `Architecture` of the package pub fn switch_arch(&self, arch: Architecture) -> Self { let mut new = self.clone(); new.arch = arch; new } /// Switch to a specific `version` of the package pub fn get_version(&self, version: &str) -> Self { let mut new_pkg = self.clone(); new_pkg.version = Some(version.to_string()); new_pkg } /// Check if the specific package exists pub fn exists(&self) -> bool { let pkg_file = self.base_path().join(self.file_name()); pkg_file.exists() } /// Checks if the package has a signature pub fn is_signed(&self) -> bool { let signed_file = self.base_path().join(format!("{}.sig", self.file_name())); signed_file.exists() } /// Build a pkg filename from the packages values pub fn file_name(&self) -> String { format!( "{}-{}-{}-{}.pkg.tar.{}", self.name, if let Some(ver) = &self.version { ver.to_string() } else { let versions = self.versions(); Package::version(&versions.first().unwrap().clone()).0 }, self.rel, self.arch.to_string(), self.compression.extension() ) } pub fn has_pkg_ext(filename: &str) -> bool { filename.ends_with(".pkg.tar.zst") || filename.ends_with(".pkg.tar.xz") } /// Get all versions of the package pub fn versions(&self) -> Vec { let dir_path = self.base_path(); let mut versions: Vec = vec![]; if let Ok(entries) = std::fs::read_dir(dir_path) { for entry in entries.filter_map(Result::ok) { let file_name = entry.file_name().into_string().unwrap_or_default(); if file_name.starts_with(&self.name) && Package::has_pkg_ext(&file_name) { let (_, version, rel, _, _) = Package::extract_pkg_name(&file_name).unwrap(); versions.push(format!("{version}-{rel}")); } } } // Sort versions in descending order (most recent version first) versions.sort_by(|a, b| b.cmp(a)); versions } pub fn pkg_content_path(&self) -> Option { if self.exists() { return Some( self.base_path() .join(self.file_name()) .to_str() .unwrap() .to_string(), ); } None } /// Get the content of the `.pkg.tar.zst` pub fn pkg_content(&self) -> Option> { if self.exists() { return std::fs::read(self.base_path().join(self.file_name())).ok(); } None } /// Get the content of the `.pkg.tar.zst.sig` pub fn sig_content(&self) -> Option> { if self.exists() { return std::fs::read(self.base_path().join(format!("{}.sig", &self.file_name()))).ok(); } None } } pub fn run_command(cmd: Vec<&str>) { std::process::Command::new(cmd.first().unwrap()) .args(cmd.into_iter().skip(1).collect::>()) .spawn() .unwrap() .wait() .unwrap(); } /// Add a package file to a repo db pub fn repo_add(db_file: &str, pkg_file: &str) { run_command(vec!["repo-add", db_file, pkg_file]); } pub fn read_file_tar(tar: &Path, file_path: &str, compression: Compression) -> Option { let mut output = Command::new("tar"); let output = match compression { Compression::Zstd => output.arg("-xO").arg("--zstd"), Compression::Xz => output.arg("-xOJ"), }; let output = output.arg("-f").arg(tar).arg(file_path).output().ok()?; if output.status.success() { Some(String::from_utf8(output.stdout).ok()?.to_string()) } else { None } } pub fn read_file_tar_raw( tar_data: &[u8], file_path: &str, compression: Compression, ) -> Option { let mut output = Command::new("tar"); let output = match compression { Compression::Zstd => output.arg("-xO").arg("--zstd"), Compression::Xz => output.arg("xOJ"), }; let mut output = output .arg("-f") .arg("-") // Indicate that the file input comes from stdin .arg(file_path) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() .ok()?; if let Some(stdin) = output.stdin.as_mut() { stdin.write_all(tar_data).ok()?; stdin.flush().ok()?; } let output = output.wait_with_output().ok()?; if output.status.success() { Some(String::from_utf8(output.stdout).ok()?.to_string()) } else { None } } pub fn list_tar_file(tar: &Path) -> Option> { let output = Command::new("tar") .arg("-tf") // List the contents of the tar file .arg(tar) .output() .ok()?; if output.status.success() { let output_str = String::from_utf8(output.stdout).ok()?; let paths = output_str .lines() .map(|line| line.to_string()) .collect::>(); Some(paths) } else { None } } #[derive(Debug, Clone, FromRow)] pub struct PackageMeta { pub repo: String, pub name: String, pub arch: String, pub version: String, pub rel: String, pub download_count: i32, } pub trait PackageMetaInfo { fn download_amount(&self) -> impl std::future::Future; fn increase_download_count(&self) -> impl std::future::Future; } impl PackageMetaInfo for Package { async fn download_amount(&self) -> i32 { let res: Option<(i32,)> = sqlx::query_as("SELECT download_count FROM package_meta WHERE repo = $1 AND name = $2 AND version = $3 AND arch = $4 AND rel = $5") .bind(&self.repo) .bind(&self.name) .bind(self.version.as_ref().unwrap()) .bind(&self.arch.to_string()) .bind(&self.rel) .fetch_optional(get_pg!()).await.unwrap(); res.map(|x| x.0).unwrap_or(0) } async fn increase_download_count(&self) { sqlx::query("INSERT INTO package_meta (repo, name, arch, version, rel) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (repo, name, arch, version, rel) DO UPDATE SET download_count = package_meta.download_count + 1") .bind(&self.repo) .bind(&self.name) .bind(&self.arch.to_string()) .bind(&self.version.as_ref().map(|x| x.to_string()).unwrap_or_default()) .bind(&self.rel) .execute(get_pg!()).await.unwrap(); } } #[derive(Debug, Clone, PartialEq)] pub enum Compression { Zstd, Xz, } impl Compression { pub fn from_filename(name: &str) -> Option { if name.ends_with("zst") { Some(Self::Zstd) } else if name.ends_with("xz") { Some(Self::Xz) } else { None } } pub fn extension(&self) -> &str { match self { Compression::Zstd => "zst", Compression::Xz => "xz", } } } #[derive(Debug, Default, Serialize)] pub struct PackageInfo { /// Architecture pub architectures: Vec, /// Build Timestamp pub build_date: i64, /// Licenses pub licenses: Vec, /// Make Dependencies pub makedepends: Vec, /// Packager pub packager: Option, /// Package Base pub pkgbase: String, /// Description pub description: String, /// Package Name pub name: String, /// Version pub version: String, /// Size pub size: i64, /// Website pub website: Option, /// XDATA pub xdata: Option, /// Dependencies pub dependencies: Vec, /// Optional Dependencies pub optdepends: Vec, /// Provides pub provides: Vec, // Replaces pub replaces: Vec, /// Backup pub backup: Vec, /// Conflicts pub conflicts: Vec, /// Check Dependencies pub checkdepends: Vec, /// Groups pub groups: Vec, } #[derive(Debug, Default, Serialize)] pub struct OptionalDependency { pub pkg: String, pub reason: Option, } impl OptionalDependency { pub fn as_str(&self) -> String { if let Some(reason) = &self.reason { format!("{}: {}", self.pkg, reason) } else { self.pkg.clone() } } } impl PackageInfo { pub fn new(keys: Vec<(String, String)>) -> Self { let mut info = Self::default(); for (key, val) in keys { match key.as_str() { "arch" => { info.architectures.push(Architecture::parse(&val).unwrap()); } "builddate" => { info.build_date = val.parse().unwrap(); } "license" => { let elements: Vec<_> = val.split(';').collect(); for e in elements { let licenses: Vec<_> = e.split(" OR ").collect(); for e in licenses { info.licenses.push(e.to_string()); } } } "makedepend" => { let pkgs: Vec<_> = val.split(';').collect(); for pkg in pkgs { info.makedepends.push(pkg.to_string()); } } "packager" => { info.packager = Some(val); } "pkgbase" => { info.pkgbase = val; } "pkgdesc" => { info.description = val; } "pkgname" => { info.name = val; } "pkgver" => { info.version = val; } "size" => { info.size = val.parse().unwrap(); } "url" => { info.website = Some(val); } "xdata" => { info.xdata = Some(val); } "depend" => { let pkgs: Vec<_> = val.split(';').collect(); for pkg in pkgs { info.dependencies.push(pkg.to_string()); } } "optdepend" => { let pkgs: Vec<_> = val.split(';').collect(); for pkg in pkgs { let (pkg, reason) = if let Some((pkg, reason)) = pkg.split_once(':') { let reason = if !reason.is_empty() { Some(reason.trim().to_string()) } else { None }; (pkg, reason) } else { (pkg, None) }; info.optdepends.push(OptionalDependency { pkg: pkg.to_string(), reason: reason, }); } } "provides" => { let provides: Vec<_> = val.split(';').collect(); for p in provides { info.provides.push(p.to_string()); } } "replaces" => { let replaces: Vec<_> = val.split(';').collect(); for r in replaces { info.replaces.push(r.to_string()); } } "backup" => { let backups: Vec<_> = val.split(';').collect(); for b in backups { info.backup.push(b.to_string()); } } "conflict" => { let conflicts: Vec<_> = val.split(';').collect(); for e in conflicts { info.conflicts.push(e.to_string()); } } "checkdepend" => { let pkgs: Vec<_> = val.split(';').collect(); for pkg in pkgs { info.checkdepends.push(pkg.to_string()); } } "group" => { let groups: Vec<_> = val.split(';').collect(); for group in groups { info.groups.push(group.to_string()); } } _ => { println!("Unrecognized key {key} -> {val}"); std::process::exit(1); } } } info } pub fn gen_array(key: &str, vals: &[String]) -> String { if vals.is_empty() { return String::new(); } let mut ret = format!("{key}=("); for (index, val) in vals.iter().enumerate() { if index == vals.len() - 1 { ret.push_str(&format!("'{val}'")); } else { ret.push_str(&format!("'{val}' ")); } } ret.push_str(")\n"); ret } /// Generate a basic `PKGBUILD` from `PKGINFO` pub fn pkgbuild(&self) -> String { #[allow(non_snake_case)] let mut PKGBUILD = String::new(); if let Some(packager) = &self.packager { PKGBUILD.push_str(&format!("# Packager: {packager}\n\n")); } PKGBUILD.push_str(&format!("pkgbase={}\n", self.pkgbase)); PKGBUILD.push_str(&format!("pkgname={}\n", self.name)); PKGBUILD.push_str(&format!("pkgdesc={}\n", self.description)); PKGBUILD.push_str(&format!("pkgver={}\n", self.version)); let arch: Vec<_> = self.architectures.iter().map(|x| x.to_string()).collect(); PKGBUILD.push_str(&PackageInfo::gen_array("arch", &arch)); PKGBUILD.push_str(&PackageInfo::gen_array("license", &self.licenses)); PKGBUILD.push_str(&PackageInfo::gen_array("makedepends", &self.makedepends)); PKGBUILD.push_str(&PackageInfo::gen_array("depends", &self.dependencies)); PKGBUILD.push_str(&PackageInfo::gen_array("checkdepends", &self.checkdepends)); PKGBUILD.push_str(&PackageInfo::gen_array( "optdepends", &self .optdepends .iter() .map(|x| x.as_str()) .collect::>(), )); if let Some(website) = &self.website { PKGBUILD.push_str(&format!("url={}\n", website)); } PKGBUILD.push_str(&PackageInfo::gen_array("provides", &self.provides)); PKGBUILD.push_str(&PackageInfo::gen_array("replaces", &self.replaces)); PKGBUILD.push_str(&PackageInfo::gen_array("backup", &self.backup)); PKGBUILD.push_str(&PackageInfo::gen_array("conflicts", &self.conflicts)); PKGBUILD.push_str(&PackageInfo::gen_array("groups", &self.groups)); PKGBUILD.push_str("\npackage() {\n\trsync -avzhruP ../root/ \"$pkdir/\"\n}"); PKGBUILD } }