diff --git a/Cargo.toml b/Cargo.toml index a586f21..7ddd396 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,10 @@ path = "src/lib/lib.rs" [dependencies] base64 = "0.22.1" bytes = "1.10.1" +clap = { version = "4.5.32", features = ["derive"] } flate2 = "1.1.0" gpgme = { version = "0.11.0", features = ["v1_18"] } +regex = "1.11.1" reqwest = { version = "0.12.12", features = ["blocking"] } ruzstd = "0.8.0" sha256 = "1.6.0" diff --git a/src/default_mirrorlist b/src/default_mirrorlist new file mode 100644 index 0000000..548006c --- /dev/null +++ b/src/default_mirrorlist @@ -0,0 +1,6 @@ +# Default mirror list. Only contains global mirrors. +# Format is identical to pacman's mirrorlist except that there is no `Server = ` prefix. + +https://geo.mirror.pkgbuild.com/$repo/os/$arch +https://ftpmirror.infania.net/mirror/archlinux/$repo/os/$arch +https://mirror.rackspace.com/archlinux/$repo/os/$arch diff --git a/src/lib/lib.rs b/src/lib/lib.rs index 0eaa0e0..83c496a 100644 --- a/src/lib/lib.rs +++ b/src/lib/lib.rs @@ -12,6 +12,7 @@ use std::{ ffi::OsString, io::{Read, Write}, path::PathBuf, + os::unix::fs::PermissionsExt }; use base64::Engine; @@ -197,7 +198,7 @@ pub fn get_current_user() -> OsString { /// /// This directory is not guaranteed to exist! If you need it to, use [base_dir]! pub fn get_base_dir() -> PathBuf { - PathBuf::from("/home/arthur/pacwoman/debug") + PathBuf::from("/pacwoman") } /// Same as [get_base_dir]; however, this function will create the directory if it doesn't @@ -897,6 +898,8 @@ pub fn receive_package( package: String, system: bool, ) -> std::io::Result { + print!("Attempting to recieve package {}...", package); + let path = index_directory()? .join(repo.format()) .join(&package) @@ -1005,6 +1008,8 @@ pub fn receive_package( packages.write_fmt(format_args!("{}-{}\n", desc_hash, package))?; drop(packages); + println!(" Success."); + Ok(dir) } @@ -1016,6 +1021,7 @@ pub fn recieve_package_and_dependencies( system: bool, ) -> std::io::Result> { let mut out: Vec = vec![]; + out.push(receive_package( mirror.clone(), repo.clone(), @@ -1039,7 +1045,6 @@ pub fn recieve_package_and_dependencies( let desc = read_desc(package.clone(), repo.clone())?; for package in desc.depends { - println!("{}", package); out.push(receive_package( mirror.clone(), repo.clone(), @@ -1047,7 +1052,6 @@ pub fn recieve_package_and_dependencies( system, )?); } - println!("recieved all dependencies"); Ok(out) } @@ -1061,9 +1065,6 @@ pub fn install_package( ) -> std::io::Result<()> { let out = bin_dir(system)?; for path in recieve_package_and_dependencies(mirror, repo, package, system)? { - println!("{}", path.to_string_lossy()); - println!("{}", out.to_string_lossy()); - if std::fs::exists(index_directory()?.join("INSTALLED"))? { let mut packages = std::fs::OpenOptions::new() .read(true) @@ -1105,8 +1106,6 @@ pub fn install_package( .unwrap_or(&entry.path().to_string_lossy()), ); - println!("{}", entry_path.clone().to_string_lossy()); - if entry.file_name() == *".BUILDINFO" || entry.file_name() == *".MTREE" || entry.file_name() == *".PKGINFO" @@ -1143,18 +1142,18 @@ pub fn install_package( } if std::fs::exists(out.join(".INSTALL"))? { - if users::get_effective_uid() == 0 { - std::os::unix::fs::chroot(&out)?; - } else { - println!("[WARN] .INSTALL script for package {} is being run without a chroot.", path.file_name().unwrap().to_string_lossy()); + if users::get_effective_uid() != 0 { + println!( + "[WARN] .INSTALL script for package {} is being run without root.", + path.file_name().unwrap().to_string_lossy() + ); println!("[WARN] The script may give permission errors or ask for authentication."); - println!(" [TIP] To run in a chroot, run the program as root."); } if !std::process::Command::new("/usr/bin/bash") .arg("-c") .arg("source ./.INSTALL && post_install && post_upgrade") - .current_dir(&out) + .current_dir("/") .spawn()? .wait()? .success() @@ -1165,10 +1164,6 @@ pub fn install_package( ); } - if users::get_effective_uid() == 0 { - std::os::unix::fs::chroot(".")?; - } - std::fs::remove_file(out.join(".INSTALL"))?; } @@ -1188,17 +1183,47 @@ pub fn install_package( Ok(()) } -/// Creates all of the necessary directories, for the system if root or the pacwoman user, and +/// Remove a package from the store. This WILL lead to dangling symlinks because I don't +/// feel like fixing that! +pub fn remove_package(package: String, system: bool, repo: RepoDescriptor) -> std::io::Result<()> { + let path = index_directory()? + .join(repo.format()) + .join(&package) + .join("desc"); + + let mut desc = std::fs::OpenOptions::new().read(true).open(path)?; + + let mut desc_data = String::new(); + + desc.read_to_string(&mut desc_data)?; + + drop(desc); + + let desc_hash = sha256::digest(desc_data); + + std::fs::remove_dir_all(store_directory(system)?.join(format!("{}-{}", desc_hash, package)))?; + + Ok(()) +} + +/// Creates all of the necessary directories, for the system if running as root, and /// for the current user if not. If [std::fs::create_dir_all] returns an error, it will be /// propagated. -pub fn create_directories() -> Result<(), std::io::Error> { - if users::get_effective_uid() == 0 || get_current_user() == "pacwoman" { +pub fn create_directories(system: bool) -> Result<(), std::io::Error> { + if system { store_directory(true)?; config_directory(true)?; index_directory()?; gpg_dir()?; bin_dir(true)?; + let user_dir = base_dir()?.join("user"); + std::fs::create_dir_all(&user_dir)?; + std::fs::set_permissions(user_dir, std::fs::Permissions::from_mode(0o777))?; } else { + let user_dir = base_dir()?.join("user").join(get_current_user()); + std::fs::create_dir_all(&user_dir)?; + std::fs::set_permissions(user_dir, std::fs::Permissions::from_mode(0o700))?; + store_directory(false)?; config_directory(false)?; bin_dir(false)?; diff --git a/src/main.rs b/src/main.rs index fb6cf14..10bf2b2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,28 +1,185 @@ -//! Main binary entrypoint. +//! An alternate client for the arch linux package repositories. Stores files in a +//! separate directory from everything else and keeps everything hashed. #![warn( missing_docs, clippy::missing_docs_in_private_items, clippy::empty_docs )] +use clap::Parser; + +/// Parser for command line arguments +#[derive(Parser)] +#[command(version, about, long_about = None, name = "pacwoman")] +struct Cli { + /// Whether to operate as a user or not. In most cases, running as system will + /// require running as root. + #[arg(short, long, default_value_t = false)] + user: bool, + /// Initalize the GPG keyring and create skeleton directories. This will also add + /// a default configuration for the mirrors. + #[arg(long, default_value_t = false)] + initalize: bool, + /// Download one or more packages. This WILL NOT install them to your bin directory! + #[arg(id = "packages to download", short = 'D')] + download: Vec, + /// Download and install one or more packages. This WILL NOT update the repositories if needed! + #[arg(id = "packages to sync", short = 'S')] + sync: Vec, + /// Remove a package. Because I'm lazy, this will only remove the store entry, leaving a bunch + /// of dangling symlinks. + #[arg(id = "packages to remove", short = 'R')] + remove: Vec, + /// Sync repository indexes from mirrors. + #[arg(id = "repositories to sync", short = 'Y')] + sync_repos: Vec, +} + +/// Default mirrorlist configuration. +const DEFAULT_MIRRORLIST: &[u8] = include_bytes!("default_mirrorlist"); + fn main() -> std::io::Result<()> { - pacwoman::create_directories()?; - //pacwoman::init_gpg()?; + let args = Cli::parse(); - pacwoman::populate_index( - pacwoman::Mirror::new(url::Url::parse( - "https://geo.mirror.pkgbuild.com/$repo/os/$arch", - ).unwrap()), - pacwoman::RepoDescriptor::new() - .set_repo("core".to_string()) - .set_arch("x86_64".to_string()).clone(), - )?; + if args.initalize { + pacwoman::create_directories(!args.user)?; + pacwoman::init_gpg()?; - for package in pacwoman::locate_package("pacman".to_string())? { - println!("found package"); - pacwoman::install_package(pacwoman::Mirror::new(url::Url::parse( - "https://geo.mirror.pkgbuild.com/$repo/os/$arch", - ).unwrap()), package.1, package.0, false)?; + std::fs::write( + pacwoman::config_directory(!args.user)?.join("mirrors"), + DEFAULT_MIRRORLIST, + )?; + } + + let regex = regex::Regex::new(r"(?m)#.*$").unwrap(); + + let mirrors: Vec = regex + .replace_all( + &(std::fs::read_to_string(pacwoman::config_directory(!args.user)?.join("mirrors"))?), + "", + ) + .split("\n") + .filter(|item| item.trim() != "") + .map(|item| pacwoman::Mirror::new(url::Url::parse(item).unwrap())) + .collect(); + + if !args.sync_repos.is_empty() { + for repo in &args.sync_repos { + let mut ok = false; + for mirror in &mirrors { + if pacwoman::populate_index( + mirror.clone(), + pacwoman::RepoDescriptor::new() + .set_repo(repo.clone()) + .set_arch(std::env::consts::ARCH.to_string()) + .clone(), + ).is_ok() { + ok = true; + break; + } + } + if !ok { + println!(" [ERR] Cannot download repo {repo}!"); + println!(" [TIP] Check the spelling and your mirrors."); + + drop(mirrors); + drop(args); + std::process::exit(1); + } + } + } + + if !args.download.is_empty() { + for package in &args.download { + let results = pacwoman::locate_package(package.clone())?; + if results.is_empty() { + println!(" [ERR] Cannot locate packages for {package}!"); + println!(" [TIP] Try adding a repository to the index."); + + drop(mirrors); + drop(args); + std::process::exit(1); + } + for result in results { + let mut ok = false; + for mirror in &mirrors { + if pacwoman::recieve_package_and_dependencies( + mirror.clone(), + result.1.clone(), + result.0.clone(), + !args.user, + ) + .is_ok() + { + ok = true; + break; + } + } + if !ok { + println!(" [ERR] Cannot download package {}!", result.0); + println!(" [TIP] Check the spelling and try syncing repos."); + + drop(mirrors); + drop(args); + std::process::exit(1); + } + } + } + } + + if !args.sync.is_empty() { + for package in &args.sync { + let results = pacwoman::locate_package(package.clone())?; + if results.is_empty() { + println!(" [ERR] Cannot locate packages for {package}!"); + println!(" [TIP] Try adding a repository to the index."); + + drop(mirrors); + drop(args); + std::process::exit(1); + } + for result in results { + let mut ok = false; + for mirror in &mirrors { + if pacwoman::install_package( + mirror.clone(), + result.1.clone(), + result.0.clone(), + !args.user, + ) + .is_ok() + { + ok = true; + break; + } + } + if !ok { + println!(" [ERR] Cannot sync package {}!", result.0); + println!(" [TIP] Check the spelling and try syncing repos."); + + drop(mirrors); + drop(args); + std::process::exit(1); + } + } + } + } + + if !args.remove.is_empty() { + for package in &args.remove { + let results = pacwoman::locate_package(package.clone())?; + if results.is_empty() { + println!(" [ERR] Cannot locate packages for {package}!"); + println!(" [TIP] Try adding a repository to the index."); + + drop(mirrors); + drop(args); + std::process::exit(1); + } + for result in results { + pacwoman::remove_package(result.0, !args.user, result.1)?; + } + } } Ok(())