diff --git a/.gitignore b/.gitignore index ab951f8..1bab0ab 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,7 @@ Cargo.lock # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# Used for debugging +/index/ +/core/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bc9f46f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "rust-analyzer.check.command": "clippy", +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..14e9942 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "pacwoman" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "pacwoman-bin" +path = "src/main.rs" + +[lib] +name = "pacwoman" +path = "src/lib/lib.rs" + +[dependencies] +base64 = "0.22.1" +bytes = "1.10.1" +flate2 = "1.1.0" +reqwest = { version = "0.12.12", features = ["blocking"] } +sha256 = "1.6.0" +tar = "0.4.44" +time = { version = "0.3.39", features = ["formatting"] } +url = "2.5.4" +users = "0.11.0" diff --git a/src/lib/lib.rs b/src/lib/lib.rs new file mode 100644 index 0000000..c3ab88a --- /dev/null +++ b/src/lib/lib.rs @@ -0,0 +1,839 @@ +//! Pacwoman library. +#![warn( + missing_docs, + clippy::missing_docs_in_private_items, + clippy::empty_docs, + clippy::missing_panics_doc +)] + +use std::{ + ffi::OsString, + io::{Read, Write}, + path::PathBuf, +}; + +use base64::Engine; +use bytes::Buf; + +/// 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. + pub fn format(&self) -> String { + if self.hash().is_some() { + format!("{:x?}-{}-{}", 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. +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() + } + } +} + +/// 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 { + PathBuf::from("/pacwoman/config") + } else { + PathBuf::from(format!( + "/pacwoman/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 /pacwomand/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 { + PathBuf::from("/home/arthur/pacwoman/store") + } else { + PathBuf::from(format!( + "/home/arthur/pacwoman/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. +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 { + PathBuf::from("/home/arthur/pacwoman/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) +} + +/// 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 = vec![]; + for ele in strings { + out.push(index_directory()?.join(PathBuf::from(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() + .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: Vec, + /// SHA256 checksum of the package. + /// Stored as hex in the desc file. + pub sha256sum: Vec, + /// 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].as_bytes().to_vec(); + } + + 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].as_bytes().to_vec(); + } + + 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", + )) + } +} + +pub fn receive_package( + mirror: Mirror, + repo: RepoDescriptor, + package: String, + system: bool, +) -> std::io::Result { + if repo.hash().is_none() { + return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "need hash in repo descriptor")); + } + + 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, repo)?; + + let url = mirror.substitute(&repo.repo(), &repo.arch(), &desc.filename); + + let dir = repo_store_directory(system, repo)?.join(format!("{}-{}", desc_hash, package)); + +} + +/// 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" { + store_directory(false)?; + config_directory(false)?; + index_directory()?; + } else { + store_directory(true)?; + config_directory(true)?; + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..4adf86e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,17 @@ +//! Main binary entrypoint. +#![warn( + missing_docs, + clippy::missing_docs_in_private_items, + clippy::empty_docs +)] + +fn main() { + 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(), + ).unwrap(); +}