//! Pacwoman library. #![warn( missing_docs, clippy::missing_docs_in_private_items, clippy::empty_docs, clippy::missing_panics_doc )] #![feature(str_as_str)] use std::{ ffi::OsString, io::{Read, Write}, path::PathBuf, }; use base64::Engine; use bytes::Buf; use gpgme::SignatureSummary; /// A descriptor of a repository. #[derive(Clone, Default, Debug)] pub struct RepoDescriptor { /// The repo(i.e. core, extra, multilib) repo: String, /// The architecture to use. arch: String, /// The hash of the (repo).db file. hash: Option, } impl RepoDescriptor { /// Creates a new [RepoDescriptor]. pub fn new() -> Self { Self::default() } /// The repo(i.e. core, extra, multilib) pub fn repo(&self) -> String { self.repo.clone() } /// Sets the repo. pub fn set_repo(&mut self, repo: String) -> &mut Self { self.repo = repo; self } /// The architecture to use. pub fn arch(&self) -> String { self.arch.clone() } /// Sets [`arch`]. /// [`arch`]: [RepoDescriptor::arch] pub fn set_arch(&mut self, arch: String) -> &mut Self { self.arch = arch; self } /// The hash of the (repo).db file. pub fn hash(&self) -> Option { self.hash.clone() } /// Sets [`hash`]. /// [`hash`]: [RepoDescriptor::hash] pub fn set_hash(&mut self, hash: Option) -> &mut Self { self.hash = hash; self } /// Formats the repo descriptor into a string. #[allow(clippy::missing_panics_doc)] // function won't ever actually panic pub fn format(&self) -> String { if self.hash().is_some() { format!("{}-{}-{}", self.hash().unwrap(), self.repo(), self.arch()) } else { format!("{}-{}", self.repo(), self.arch()) } } } /// The identifier for a package. Used to locate the package. #[derive(Clone)] pub struct Package { /// The repo(i.e. core, extra, multilib) repo: RepoDescriptor, /// The filename of the package. filename: String, } impl Default for Package { fn default() -> Self { Self::new() } } impl Package { /// Creates a new [Package]. pub fn new() -> Self { Self { repo: RepoDescriptor::new(), filename: String::new(), } } /// The repo(i.e. core, extra, multilib) pub fn repo(&self) -> RepoDescriptor { self.repo.clone() } /// Sets the repo. pub fn set_repo(&mut self, repo: RepoDescriptor) -> &mut Self { self.repo = repo; self } /// The filename of the package. pub fn filename(&self) -> String { self.filename.clone() } /// Sets [`filename`]. /// [`filename`]: [Package::filename] pub fn set_filename(&mut self, filename: String) -> &mut Self { self.filename = filename; self } } /// A mirror to use. #[derive(Clone, Debug)] pub struct Mirror(url::Url); impl Mirror { /// Creates a new [Mirror]. pub fn new(base_url: url::Url) -> Self { Self(base_url) } /// Returns the base URL of the mirror and, similar to pacman, replacements for the repo and arch. /// (i.e. https://geo.mirror.pkgbuild.com/$repo/os/$arch). pub fn base(&self) -> url::Url { self.0.clone() } /// Sets the base URL of the mirror. pub fn set_base(&mut self, url: url::Url) -> &mut Self { self.0 = url; self } /// Substitutes a repo and architecture into the base url. #[allow(clippy::missing_panics_doc)] // never will panic pub fn substitute(&self, repo: &str, arch: &str, path: &str) -> url::Url { url::Url::parse( &(self .base() .as_str() .replace("$repo", repo) .replace("$arch", arch) + "/" + path), ) .unwrap() } /// Makes an HTTP request to see if the server is reachable. Specifically, it attempts to reach /// the core.db file in the core repository. #[allow(clippy::missing_panics_doc)] // never will panic pub fn is_reachable(&self, arch: &str) -> bool { let url = self.substitute("core", arch, "core.db"); if let Ok(res) = reqwest::blocking::get(url) { res.status().is_success() } else { false } } } /// Get the current user. It will first try the effective username. If the effective user has been /// deleted, then it will try the current user. If the current user has also been deleted, then /// it will as a last resort create a unique string in the format `deleted_user_e{e}_c{c}` /// where {e} is the effective user uid and {c} is the current user uid. pub fn get_current_user() -> OsString { let username = users::get_effective_username(); if let Some(user) = username { user } else { let username = users::get_current_username(); if let Some(user) = username { user } else { format!( "deleted_user_e{}_c{}", users::get_effective_uid(), users::get_current_uid() ) .into() } } } /// Returns the base dir for pacwoman. Use this instead of hard-coding /pacwoman for the case /// of debugging in a different directory. /// /// 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") } /// Same as [get_base_dir]; however, this function will create the directory if it doesn't /// exist. If [std::fs::create_dir_all] returns an error, it will be propagated. pub fn base_dir() -> std::io::Result { let dir = get_base_dir(); std::fs::create_dir_all(&dir)?; Ok(dir) } /// Get the config directory. If for the entire system, then it will be /pacwoman/config, otherwise /// it will be /pacwomand/user/{user name from [get_current_user]}/config. /// /// This directory is not guaranteed to exist! If you need it to, use [config_directory]! pub fn get_config_directory(system: bool) -> PathBuf { if system { get_base_dir().join("config") } else { get_base_dir().join(format!( "user/{}/config", get_current_user().to_string_lossy() )) } } /// Same as [get_config_directory]; however, this function will create the directory if it doesn't /// exist. If [std::fs::create_dir_all] returns an error, it will be propagated. pub fn config_directory(system: bool) -> std::io::Result { let dir = get_config_directory(system); std::fs::create_dir_all(&dir)?; Ok(dir) } /// Get the store directory. If for the entire system, then it will be /pacwoman/store, otherwise /// it will be /pacwoman/user/{user name from [get_current_user]}/store. /// /// This directory is not guaranteed to exist! If you need it to, use [store_directory]! pub fn get_store_directory(system: bool) -> PathBuf { if system { get_base_dir().join("store") } else { get_base_dir().join(format!( "user/{}/store", get_current_user().to_string_lossy() )) } } /// Same as [get_store_directory]; however, this function will create the directory if it doesn't /// exist. If [std::fs::create_dir_all] returns an error, it will be propagated. pub fn store_directory(system: bool) -> std::io::Result { let dir = get_store_directory(system); std::fs::create_dir_all(&dir)?; Ok(dir) } /// Gets the store directory for a repo. /// /// This directory is not guaranteed to exist! See [repo_store_directory] if you need it too. pub fn get_repo_store_directory(system: bool, repo: RepoDescriptor) -> PathBuf { get_store_directory(system).join(repo.format()) } /// Same as [get_repo_store_directory] but creates the directory(s) if they don't exist. pub fn repo_store_directory(system: bool, repo: RepoDescriptor) -> std::io::Result { let dir = get_repo_store_directory(system, repo); std::fs::create_dir_all(&dir)?; Ok(dir) } /// Get the index directory. This is always a system-wide directory of /pacwoman/index. /// This directory is subject to change; because of this, use this method instead of /// hard-coding the directory. /// /// This directory is not guaranteed to exist! If you need it to, use [index_directory]! pub fn get_index_directory() -> PathBuf { get_base_dir().join("index") } /// Same as [get_index_directory]; however, this function will create the directory if it doesn't /// exist. If [std::fs::create_dir_all] returns an error, it will be propagated. pub fn index_directory() -> std::io::Result { let dir = get_index_directory(); std::fs::create_dir_all(&dir)?; Ok(dir) } /// Returns the index directory of a repository. This does not include the hash; this should be /// assumed to symlink to the correct, hashed directory. /// /// This directory is not guaranteed to exist! See [repo_index_dir] if you need it too. pub fn get_repo_index_dir(repo: RepoDescriptor) -> PathBuf { get_index_directory().join(repo.format()) } /// Same as [get_repo_index_dir], except it will create the directory if it doesn't exist. /// If [std::fs::create_dir_all] returns an error, it will be propagated. pub fn repo_index_dir(repo: RepoDescriptor) -> std::io::Result { let dir = get_repo_index_dir(repo); std::fs::create_dir_all(&dir)?; Ok(dir) } /// Directory for storing GPG keys and other miscellaneous GPG stuff. /// /// This directory is not guaranteed to exist! See [gpg_dir] if you need it too. pub fn get_gpg_dir() -> PathBuf { get_base_dir().join("keys") } /// Same as [get_gpg_dir], except it will create the directory if it doesn't exist. /// If [std::fs::create_dir_all] returns an error, it will be propagated. pub fn gpg_dir() -> std::io::Result { let dir = get_gpg_dir(); std::fs::create_dir_all(&dir)?; Ok(dir) } /// Directory for storing binaries. Equivalent to the root directory. Contains directories such /// as etc, usr, var, and others. /// /// This directory is not guaranteed to exist! See [bin_dir] if you need it too. pub fn get_bin_dir(system: bool) -> PathBuf { if system { get_base_dir().join("bin") } else { get_base_dir().join(format!("user/{}/bin", get_current_user().to_string_lossy())) } } /// Same as [get_bin_dir], except it will create the directory if it doesn't exist. /// If [std::fs::create_dir_all] returns an error, it will be propagated. pub fn bin_dir(system: bool) -> std::io::Result { let dir = get_bin_dir(system); std::fs::create_dir_all(&dir)?; Ok(dir) } /// Populates the index with information for a certain repo from a certain mirror. Will /// overwrite any symlink for the repo. /// /// # Panics /// Should never panic as .unwrap() is only used on things that should always be /// Some/Ok. pub fn populate_index(mirror: Mirror, repo: RepoDescriptor) -> std::io::Result { if !mirror.is_reachable(&repo.arch) { return Err(std::io::Error::new( std::io::ErrorKind::HostUnreachable, "mirror is unreachable", )); } let url = mirror.substitute(&repo.repo(), &repo.arch(), &format!("{}.db", repo.repo())); if let Ok(res) = reqwest::blocking::get(url) { let bytes = res.bytes().unwrap().to_vec(); let bytes_reader_bytes = bytes.clone(); let bytes_reader = bytes_reader_bytes.reader(); let tar = flate2::read::GzDecoder::new(bytes_reader); let mut archive = tar::Archive::new(tar); let digest = sha256::digest(bytes); let index_dir = index_directory()?.join(format!("{}-{}-{}", digest, repo.repo(), repo.arch())); if std::fs::exists(&index_dir)? { let _ = std::fs::remove_dir_all(get_repo_index_dir(repo.clone())); std::os::unix::fs::symlink(index_dir, get_repo_index_dir(repo.clone()))?; return repo_index_dir(repo); } std::fs::create_dir_all(&index_dir)?; archive.unpack(&index_dir)?; let mut package_list: Vec = vec![]; for item in std::fs::read_dir(&index_dir)? { if item.is_err() { continue; } let item = item.unwrap(); if item.file_type().is_err() { continue; } if item.file_type().unwrap().is_dir() { package_list.push(item.file_name()); } } let mut packages = std::fs::OpenOptions::new() .create(true) .append(true) .truncate(false) .open(index_directory()?.join("PACKAGES"))?; packages.write_fmt(format_args!( "{}-{}-{}: {}", digest, repo.repo(), repo.arch(), package_list.join(&OsString::from(" ")).to_string_lossy() ))?; drop(packages); std::fs::OpenOptions::new() .create(true) .write(true) .truncate(true) .open(index_dir.join("SOURCE"))? .write_fmt(format_args!( "{}\n{}\n{}", mirror.substitute(&repo.repo(), &repo.arch(), ""), time::OffsetDateTime::now_utc() .format(&time::format_description::well_known::Iso8601::DATE_TIME_OFFSET) .unwrap(), digest ))?; if std::fs::exists(index_directory()?.join("REPOS"))? { let mut repos = std::fs::OpenOptions::new() .read(true) .open(index_directory()?.join("REPOS"))?; let mut repos_data = String::new(); let mut repos_data_out = String::new(); repos.read_to_string(&mut repos_data)?; drop(repos); let mut need_append = true; for line in repos_data.split("\n") { if line.starts_with(&format!("{}-{}: ", repo.repo(), repo.arch())) { need_append = false; repos_data_out += &format!("{} {}\n", line, digest); } else { repos_data_out += &(line.to_owned() + "\n"); } } if need_append { repos_data_out += &format!("{}-{}: {}\n", repo.repo(), repo.arch(), digest); } let mut repos = std::fs::OpenOptions::new() .create(true) .write(true) .truncate(true) .open(index_directory()?.join("REPOS"))?; repos.write_all(repos_data_out.as_bytes())?; drop(repos); } else { std::fs::write( index_directory()?.join("REPOS"), format!("{}-{}: {}\n", repo.repo(), repo.arch(), digest), )?; } let _ = std::fs::remove_dir_all(get_repo_index_dir(repo.clone())); std::os::unix::fs::symlink(index_dir, get_repo_index_dir(repo.clone()))?; Ok(repo_index_dir(repo)?) } else { Err(std::io::Error::new( std::io::ErrorKind::HostUnreachable, "mirror does not have repo", )) } } /// Get a list of hashes provided by the repo. #[allow(clippy::missing_panics_doc)] // Shouldn't ever panic, but could be wrong. pub fn read_repos(repo: RepoDescriptor) -> std::io::Result> { let mut repos = std::fs::OpenOptions::new() .read(true) .open(index_directory()?.join("REPOS"))?; let mut repos_data = String::new(); repos.read_to_string(&mut repos_data)?; drop(repos); let repos = repos_data; for line in repos.split("\n") { if line.starts_with(&format!("{}-{}: ", repo.repo(), repo.arch())) { let mut strings: Vec = vec![]; for hash in line.split_once(": ").unwrap().1.split(" ") { strings.push(hash.to_owned() + &repo.format()); } let mut out: Vec<(PathBuf, String)> = vec![]; for ele in strings { out.push((index_directory()?.join(PathBuf::from(&ele)), ele)); } return Ok(out); } } Err(std::io::Error::new( std::io::ErrorKind::NotFound, "repo not found in REPOS file", )) } /// Get a list of (Package name and version, repo, path to index directory with the package) /// for a certain package name. This function uses [read_desc] internally after performing /// preliminary checks to make sure that only packages that have the exact name of /// package_name will be returned. If this was not used, then this would create false /// positives of any package that is in the format `(package_name)-.*` in a psuedo-regex. #[allow(clippy::missing_panics_doc)] // will never panic pub fn locate_package( package_name: String, ) -> std::io::Result> { let mut packages = std::fs::OpenOptions::new() .read(true) .open(index_directory()?.join("PACKAGES"))?; let mut packages_data = String::new(); packages.read_to_string(&mut packages_data)?; drop(packages); let packages = packages_data.split("\n"); let mut out: Vec<(String, RepoDescriptor, PathBuf)> = vec![]; for line in packages { let segments = line.split_once(": ").unwrap(); let prefix = segments.0; let suffix = segments.1; if suffix.contains(&(" ".to_string() + &package_name + "-")) { let arch = prefix.split("-").last().unwrap().to_string(); let repo = prefix .strip_suffix(&("-".to_string() + &arch)) .unwrap() .split_once("-") .unwrap() .1 .to_string(); let descriptor = RepoDescriptor::new() .set_arch(arch.clone()) .set_repo(repo.clone()) .clone(); let mut package = String::new(); let start = suffix .find(&(" ".to_string() + &package_name + "-")) .unwrap() + 1; // add one because otherwise that would be the space for char in suffix[start..].chars() { if char == ' ' { break; } package += &char.to_string(); } if read_desc(package.clone(), descriptor.clone())?.name != package_name { continue; } out.push(( package.clone(), descriptor, index_directory()? .join(format!("{}-{}", repo, arch)) .join(package), )); } } if !out.is_empty() { return Ok(out); } Err(std::io::Error::new( std::io::ErrorKind::NotFound, "cannot find package", )) } /// Information provided by a packages desc file. #[derive(Clone, Default, Debug)] pub struct PackageDesc { /// The filename of the package in the repo. pub filename: String, /// The name of the package. pub name: String, /// Base name for split packages. Unknown how common? pub base: Option, /// The version of the package. Generally semver, unknown if must be? pub version: String, /// Description of the package. pub description: Option, /// Download size. pub csize: Option, /// Installed size. pub isize: Option, /// MD5 checksum of the package. Recommended not to use and to instead use [`sha256sum`]. /// Stored as hex in the desc file. /// [`sha256sum`]: [PackageDesc::sha256sum] pub md5sum: String, /// SHA256 checksum of the package. /// Stored as hex in the desc file. pub sha256sum: String, /// PGP signature of the package. /// Stored as base64 in the desc file. pub pgpsig: Vec, /// URL to a website for the package. pub url: Option, /// An array of licenses for the package. pub license: Vec, /// The architecture of the package. Not always the same as the repos; if not, generally /// something like `any`. pub arch: Option, /// The build date of the package as a unix timestamp. pub builddate: Option, /// The packager of the package. Generally in the format `(name) <(email)>` (may be enforced? /// unclear). pub packager: Option, /// Any packages the package depends on. pub depends: Vec, /// Packages required for the package's testing suite. Unsure if tests can be performed even? pub checkdepends: Vec, /// Packages optionally required. The first field is the package name, and the second is /// the human-readable reason. pub optdepends: Vec<(String, String)>, /// Packages required to build the package. May be useless. pub makedepends: Vec, } /// Reads the desc file of the package into a [PackageDesc]. Will return an error if required /// fields don't exist. If they have an empty value, it will work. An error will also be /// returned if any calls such as opening a file fail. #[allow(clippy::missing_panics_doc)] // as far as i know shouldn't ever panic pub fn read_desc(package: String, 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_data = desc_data.split("\n").collect::>(); let mut out = PackageDesc::default(); if let Some(loc) = desc_data.iter().position(|item| (*item) == "%FILENAME%") { let loc = loc + 1; // value is on next line out.filename = desc_data[loc].to_string(); if let Some(loc) = desc_data.iter().position(|item| (*item) == "%NAME%") { let loc = loc + 1; // value is on next line out.name = desc_data[loc].to_string(); if let Some(loc) = desc_data.iter().position(|item| (*item) == "%VERSION%") { let loc = loc + 1; // value is on next line out.version = desc_data[loc].to_string(); if let Some(loc) = desc_data.iter().position(|item| (*item) == "%BASE%") { let loc = loc + 1; // value is on next line out.base = Some(desc_data[loc].to_string()); } if let Some(loc) = desc_data.iter().position(|item| (*item) == "%DESC%") { let loc = loc + 1; // value is on next line out.description = Some(desc_data[loc].to_string()); } if let Some(loc) = desc_data.iter().position(|item| (*item) == "%CSIZE%") { let loc = loc + 1; // value is on next line let val = desc_data[loc].parse(); if val.is_err() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "csize is invalid value", )); } out.csize = Some(val.unwrap()); } if let Some(loc) = desc_data.iter().position(|item| (*item) == "%ISIZE%") { let loc = loc + 1; // value is on next line let val = desc_data[loc].parse(); if val.is_err() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "isize is invalid value", )); } out.isize = Some(val.unwrap()); } if let Some(loc) = desc_data.iter().position(|item| (*item) == "%MD5SUM%") { let loc = loc + 1; // value is on next line out.md5sum = desc_data[loc].to_string(); } if let Some(loc) = desc_data.iter().position(|item| (*item) == "%SHA256SUM%") { let loc = loc + 1; // value is on next line out.sha256sum = desc_data[loc].to_string(); } if let Some(loc) = desc_data.iter().position(|item| (*item) == "%PGPSIG%") { let loc = loc + 1; // value is on next line let val = base64::engine::general_purpose::STANDARD.decode(desc_data[loc].as_bytes()); if val.is_err() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "pgpsig is invalid value", )); } out.pgpsig = val.unwrap(); } if let Some(loc) = desc_data.iter().position(|item| (*item) == "%URL%") { let loc = loc + 1; // value is on next line out.url = Some(desc_data[loc].to_string()); } if let Some(loc) = desc_data.iter().position(|item| (*item) == "%LICENSE%") { let loc = loc + 1; // value is on next line let mut val: Vec = vec![]; for line in &desc_data[loc..] { let line = *line; if line.is_empty() { break; } val.push(line.to_string()); } out.license = val; } if let Some(loc) = desc_data.iter().position(|item| (*item) == "%ARCH%") { let loc = loc + 1; // value is on next line out.arch = Some(desc_data[loc].to_string()); } if let Some(loc) = desc_data.iter().position(|item| (*item) == "%BUILDDATE%") { let loc = loc + 1; // value is on next line let val = desc_data[loc].parse(); if val.is_err() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "builddate is invalid value", )); } out.builddate = Some(val.unwrap()); } if let Some(loc) = desc_data.iter().position(|item| (*item) == "%PACKAGER%") { let loc = loc + 1; // value is on next line out.packager = Some(desc_data[loc].to_string()); } if let Some(loc) = desc_data.iter().position(|item| (*item) == "%DEPENDS%") { let loc = loc + 1; // value is on next line let mut val: Vec = vec![]; for line in &desc_data[loc..] { let line = *line; if line.is_empty() { break; } val.push(line.to_string()); } out.depends = val; } if let Some(loc) = desc_data .iter() .position(|item| (*item) == "%CHECKDEPENDS%") { let loc = loc + 1; // value is on next line let mut val: Vec = vec![]; for line in &desc_data[loc..] { let line = *line; if line.is_empty() { break; } val.push(line.to_string()); } out.checkdepends = val; } if let Some(loc) = desc_data.iter().position(|item| (*item) == "%MAKEDEPENDS%") { let loc = loc + 1; // value is on next line let mut val: Vec = vec![]; for line in &desc_data[loc..] { let line = *line; if line.is_empty() { break; } val.push(line.to_string()); } out.makedepends = val; } if let Some(loc) = desc_data.iter().position(|item| (*item) == "%OPTDEPENDS%") { let loc = loc + 1; // value is on next line let mut val: Vec<(String, String)> = vec![]; for line in &desc_data[loc..] { let line = line.to_string(); if line.is_empty() { break; } let l = line.split_once(": ").unwrap(); let l = (l.0.to_string(), l.1.to_string()); val.push(l); } out.optdepends = val; } Ok(out) } else { Err(std::io::Error::new( std::io::ErrorKind::NotFound, "cannot find version in desc file of package", )) } } else { Err(std::io::Error::new( std::io::ErrorKind::NotFound, "cannot find name in desc file of package", )) } } else { Err(std::io::Error::new( std::io::ErrorKind::NotFound, "cannot find filename in desc file of package", )) } } /// Gets the [gpgme] context and sets the appropriate properties. pub fn get_gpg_ctx() -> std::io::Result { let mut gpg_ctx = gpgme::Context::from_protocol(gpgme::Protocol::OpenPgp)?; gpg_ctx.set_engine_home_dir(gpg_dir()?.to_string_lossy().into_owned())?; Ok(gpg_ctx) } /// The PGP public key file for arch linux signing keys. const ARCHLINUX_PGP_PUBLIC_KEY: &[u8] = include_bytes!("archlinux.gpg"); /// Initalizes the GPG keyring and other GPG stuff. pub fn init_gpg() -> std::io::Result<()> { let mut gpg_ctx = get_gpg_ctx()?; print!("Generating master key..."); gpg_ctx.generate_key::, Vec>( r#" Key-Type: RSA Key-Length: 4096 Key-Usage: sign Name-Real: Pacwoman Keyring Master Key Name-Email: pacwoman@localhost Expire-Date: 0 %no-protection "#, None, None, )?; println!(" Done."); gpg_ctx.import(ARCHLINUX_PGP_PUBLIC_KEY)?; Ok(()) } /// Downloads, verifies, and unpacks a package from the provided mirror. The package value /// provided should be the file name provided in a [PackageDesc]. /// /// # Panics /// Panics if there are any errors with downloading because right now I don't feel like /// implementing it to fail gently. pub fn receive_package( mirror: Mirror, repo: RepoDescriptor, package: String, system: bool, ) -> 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); let desc = read_desc(package.clone(), repo.clone())?; let sig_url = mirror.substitute( &repo.clone().repo(), &repo.arch(), &(desc.filename.clone() + ".sig"), ); let url = mirror.substitute(&repo.repo(), &repo.arch(), &desc.filename.clone()); let dir = repo_store_directory(system, repo)?.join(format!("{}-{}", desc_hash, package)); if std::fs::exists(index_directory()?.join("RECIEVED"))? { let mut packages = std::fs::OpenOptions::new() .read(true) .open(index_directory()?.join("RECIEVED"))?; let mut packages_data = String::new(); packages.read_to_string(&mut packages_data)?; drop(packages); let packages: Vec<&str> = packages_data.split("\n").collect(); if packages.contains(&format!("{}-{}", desc_hash, package).as_str()) { return Ok(dir); } } std::fs::create_dir_all(&dir)?; let sig_data = reqwest::blocking::get(sig_url) .unwrap() .bytes() .unwrap() .to_vec(); let package_data = reqwest::blocking::get(url) .unwrap() .bytes() .unwrap() .to_vec(); let mut gpg_ctx = get_gpg_ctx()?; let res = gpg_ctx .verify_detached(&sig_data, &package_data)? .signatures() .next() .unwrap() .summary(); match res { SignatureSummary::VALID | SignatureSummary::GREEN => {} sum => { if !sum.is_empty() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, format!("signature invalid, error {:#?}", sum), )); } } } let sum = sha256::digest(&package_data); if desc.sha256sum != sum { return Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "SHA256 checksum does not match provided sum in desc, try updating the index or try receiving the package again", )); } let mut reader = package_data.reader(); let tar = ruzstd::decoding::StreamingDecoder::new(&mut reader).unwrap(); let mut archive = tar::Archive::new(tar); archive.unpack(&dir)?; let mut packages = std::fs::OpenOptions::new() .create(true) .append(true) .truncate(false) .open(index_directory()?.join("RECIEVED"))?; packages.write_fmt(format_args!("{}-{}\n", desc_hash, package))?; drop(packages); Ok(dir) } /// Recieves a package and all of it's dependencies. Returns a list of paths to each package. pub fn recieve_package_and_dependencies( mirror: Mirror, repo: RepoDescriptor, package: String, system: bool, ) -> std::io::Result> { let mut out: Vec = vec![]; out.push(receive_package( mirror.clone(), repo.clone(), package.clone(), system, )?); let path = index_directory()? .join(repo.clone().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 = read_desc(package.clone(), repo.clone())?; for package in desc.depends { println!("{}", package); out.push(receive_package( mirror.clone(), repo.clone(), locate_package(package)?[0].0.clone(), system, )?); } println!("recieved all dependencies"); Ok(out) } /// Recieves a package and it's dependencies and installs it. #[allow(clippy::missing_panics_doc)] // won't ever actually panic pub fn install_package( mirror: Mirror, repo: RepoDescriptor, package: String, system: bool, ) -> 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) .open(index_directory()?.join("INSTALLED"))?; let mut packages_data = String::new(); packages.read_to_string(&mut packages_data)?; drop(packages); let packages: Vec<&str> = packages_data.split("\n").collect(); if packages.contains(&(path.file_name().unwrap().to_string_lossy().as_str())) { continue; } } for entry in std::fs::read_dir(&path)? { println!("entry"); let entry = entry?; if entry.file_name() == *".BUILDINFO" || entry.file_name() == *".MTREE" || entry.file_name() == *".PKGINFO" { continue; } println!("{}", entry.file_name().to_string_lossy()); if entry.file_type()?.is_dir() { std::fs::create_dir_all(out.join(entry.path()))?; } else { std::fs::create_dir_all(out.join(entry.path().parent().unwrap()))?; std::fs::remove_file(out.join(entry.path()))?; std::os::unix::fs::symlink(path.join(entry.path()), out.join(entry.path()))?; } } std::os::unix::fs::chroot(&out)?; if !std::process::Command::new("/usr/bin/bash") .arg("-c") .arg("source /.INSTALL && post_install && post_upgrade") .current_dir(&out) .spawn()? .wait()? .success() { println!( "[WARN] .INSTALL script for package {} failed!", path.file_name().unwrap().to_string_lossy() ); } std::os::unix::fs::chroot(".")?; std::fs::remove_file(out.join(".INSTALL"))?; let mut packages = std::fs::OpenOptions::new() .create(true) .append(true) .truncate(false) .open(index_directory()?.join("INSTALLED"))?; packages.write_fmt(format_args!( "{}\n", path.file_name().unwrap().to_string_lossy() ))?; drop(packages); } Ok(()) } /// Creates all of the necessary directories, for the system if root or the pacwoman user, 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" || get_current_user() == "arthur" { store_directory(true)?; config_directory(true)?; index_directory()?; gpg_dir()?; bin_dir(true)?; } else { store_directory(false)?; config_directory(false)?; bin_dir(false)?; } Ok(()) }