diff --git a/hpk-package/.gitignore b/hpk-package/.gitignore new file mode 100644 index 0000000..985a402 --- /dev/null +++ b/hpk-package/.gitignore @@ -0,0 +1,3 @@ +/target +/Cargo.lock +tags diff --git a/hpk-package/Cargo.toml b/hpk-package/Cargo.toml new file mode 100644 index 0000000..648bf57 --- /dev/null +++ b/hpk-package/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "hpk-package" +version = "0.1.0" +edition = "2021" +license = "GPL-3.0-only" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +deku = "0.16" +rayon = "1.7" +ron = "0.8" +sha2 = "0.10" +walkdir = "2.3" +thiserror = "1.0" +libc = "0.2" + +[dependencies.chrono] +version = "0.4" +features = ["serde"] + +[dependencies.serde] +version = "1.0" +features = ["derive"] + +[profile.release] +codegen-units = 1 +lto = true +strip = true diff --git a/hpk-package/src/lib.rs b/hpk-package/src/lib.rs new file mode 100644 index 0000000..fb56ef0 --- /dev/null +++ b/hpk-package/src/lib.rs @@ -0,0 +1,12 @@ +mod package; +mod plist; +pub mod tar; +mod version; + +pub use { + deku, + package::{Arch, Dependency, Group, Package, Specs, User}, + plist::*, + ron, sha2, + version::*, +}; diff --git a/hpk-package/src/package/arch.rs b/hpk-package/src/package/arch.rs new file mode 100644 index 0000000..59f1c01 --- /dev/null +++ b/hpk-package/src/package/arch.rs @@ -0,0 +1,79 @@ +use serde::{Deserialize, Serialize}; +use std::{error::Error, fmt, str::FromStr}; + +#[cfg(target_arch = "arm")] +pub const HOST_ARCH: Arch = Arch::armv7l; +#[cfg(target_arch = "aarch64")] +pub const HOST_ARCH: Arch = Arch::aarch64; +#[cfg(target_arch = "x86")] +pub const HOST_ARCH: Arch = Arch::i486; +#[cfg(target_arch = "riscv64")] +pub const HOST_ARCH: Arch = Arch::riscv64; +#[cfg(target_arch = "x86_64")] +pub const HOST_ARCH: Arch = Arch::x86_64; + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)] +#[allow(non_camel_case_types)] +pub enum Arch { + armv7l, + aarch64, + i486, + i586, + i686, + riscv64, + x86_64, + any, +} + +impl Default for Arch { + fn default() -> Self { + HOST_ARCH + } +} + +impl fmt::Display for Arch { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Self::armv7l => "armv7l", + Self::aarch64 => "aarch64", + Self::i486 => "i486", + Self::i586 => "i586", + Self::i686 => "i686", + Self::riscv64 => "riscv64", + Self::x86_64 => "x86_64", + Self::any => "any", + } + ) + } +} + +#[derive(Debug)] +pub struct ParseArchError; + +impl fmt::Display for ParseArchError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "error parsing architecture") + } +} + +impl Error for ParseArchError {} + +impl FromStr for Arch { + type Err = ParseArchError; + + fn from_str(s: &str) -> Result { + match s { + "i486" | "x86" => Ok(Self::i486), + "i586" => Ok(Self::i586), + "i686" => Ok(Self::i686), + "armv7l" | "arm" => Ok(Self::armv7l), + "arm64" | "aarch64" | "armv8" => Ok(Self::aarch64), + "riscv" | "riscv64" => Ok(Self::riscv64), + "any" => Ok(Self::any), + _ => Err(ParseArchError), + } + } +} diff --git a/hpk-package/src/package/dependency.rs b/hpk-package/src/package/dependency.rs new file mode 100644 index 0000000..a9e99a8 --- /dev/null +++ b/hpk-package/src/package/dependency.rs @@ -0,0 +1,35 @@ +use { + super::Package, + crate::Version, + serde::{Deserialize, Serialize}, +}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +/// Specifies a dependency requirement +pub struct Dependency { + /// The name of the dependency. All packages must have a unique name. + pub name: String, + /// The version requirements for this dependency. If the low + /// version is `Some`, then the version must be equal to or + /// greater than this version. If the high version is `Some`, + /// then the version must be less than this version. + pub version: (Option, Option), +} + +impl Dependency { + #[allow(clippy::must_use_candidate)] + /// Checks whether a package satisfies a given dependency + pub fn satisfied(&self, package: &Package) -> bool { + if self.name.as_str() == package.name.as_str() { + match &self.version { + (Some(low), Some(high)) => &package.version >= low && &package.version < high, + (Some(low), None) => &package.version >= low, + (None, Some(high)) => &package.version < high, + // no version requirements + _ => true, + } + } else { + false + } + } +} diff --git a/hpk-package/src/package/mod.rs b/hpk-package/src/package/mod.rs new file mode 100644 index 0000000..bcef93f --- /dev/null +++ b/hpk-package/src/package/mod.rs @@ -0,0 +1,129 @@ +mod arch; +mod dependency; +mod specs; + +use { + crate::tar::{Node, Owner}, + crate::{Plist, Version}, + ron::ser::{to_string_pretty, PrettyConfig}, + serde::{Deserialize, Serialize}, + std::{ + error::Error, + fs, + fs::File, + io::{BufWriter, Write}, + path::Path, + }, +}; + +pub use {arch::Arch, dependency::Dependency, specs::Specs}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct User { + pub name: String, + pub uid: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct Group { + pub name: String, + pub gid: Option, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +/// the metadata associated with a package +pub struct Package { + /// The name of the package minus all version information + pub name: String, + /// The `Version` of the package + pub version: Version, + /// The architecture the package is built for + pub arch: Arch, + /// The release number for this package + pub release: u8, + /// a single line description of the package + pub description: String, + /// a more verbose description of the package + pub long_description: String, + /// a listing of all files, directories and symlinks which are a part of + /// this package + pub plist: Plist, + /// the total size of this package + pub size: usize, + /// all of this package's runtime dependencies + pub dependencies: Vec, + /// an optional list of users to be created upon installation + pub users: Option>, + /// an optional list of groups to be created upon installation + pub groups: Option>, + /// an optional post installation shell script to be run + pub post_install: Option, +} + +impl From for Package { + fn from(value: Specs) -> Self { + Package { + name: value.name, + version: value.version, + release: value.release, + description: value.description, + long_description: value.long_description, + dependencies: value.dependencies, + users: value.users, + groups: value.groups, + post_install: value.post_install, + ..Default::default() + } + } +} + +impl Package { + fn as_ron(&self) -> Result { + let cfg = PrettyConfig::new().struct_names(true); + to_string_pretty(self, cfg) + } + + pub fn save_ron_and_create_tar_node(&self, outdir: &Path) -> Result> { + if !outdir.exists() { + fs::create_dir_all(outdir)?; + } + let mut outfile = outdir.to_path_buf(); + outfile.push("package.ron"); + let fd = File::create(&outfile)?; + let s = self.as_ron()?; + let mut writer = BufWriter::new(&fd); + writer.write_all(s.as_bytes())?; + writer.flush()?; + let node = Node::read_data_to_tar( + s.as_bytes(), + "package.ron", + &outfile.metadata()?, + Some(Owner::default()), + )?; + Ok(node) + } + + /// Returns the formatted full package name including version and release strings + pub fn fullname(&self) -> String { + format!( + "{}-{}_{}_{}", + self.name, self.version, self.release, self.arch + ) + } + + /// Returns the name of the package archive + pub fn archive_name(&self) -> String { + format!( + "{}-{}_{}_{}.tar.zstd", + self.name, self.version, self.release, self.arch + ) + } + + /// Tests whether this package is an update for another + pub fn is_upgrade(&self, other: &Self) -> bool { + self.arch == other.arch + && self.name == other.name + && (self.version > other.version + || (self.version == other.version && self.release > other.release)) + } +} diff --git a/hpk-package/src/package/specs.rs b/hpk-package/src/package/specs.rs new file mode 100644 index 0000000..abe182b --- /dev/null +++ b/hpk-package/src/package/specs.rs @@ -0,0 +1,27 @@ +use { + super::{Group, User}, + crate::{Dependency, Version}, + serde::{Deserialize, Serialize}, +}; + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct Specs { + /// The name of the package minus all version information + pub name: String, + /// The `Version` of the package + pub version: Version, + /// The release number for this package + pub release: u8, + /// a single line description of the package + pub description: String, + /// a more verbose description of the package + pub long_description: String, + /// all of this package's runtime dependencies + pub dependencies: Vec, + /// an optional list of users to be created upon installation + pub users: Option>, + /// an optional list of groups to be created upon installation + pub groups: Option>, + /// an optional post installation shell script to be run + pub post_install: Option, +} diff --git a/hpk-package/src/plist/mod.rs b/hpk-package/src/plist/mod.rs new file mode 100644 index 0000000..3551aca --- /dev/null +++ b/hpk-package/src/plist/mod.rs @@ -0,0 +1,91 @@ +use { + rayon::prelude::*, + serde::{Deserialize, Serialize}, + sha2::{digest::Digest, Sha256}, + std::{ + error::Error, + fmt::Write, + fs, + io::Read, + os::unix::fs::MetadataExt, + path::{Path, PathBuf}, + }, + walkdir::WalkDir, +}; + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct Plist { + pub entries: Vec, +} + +impl TryFrom<&Path> for Plist { + type Error = Box; + + fn try_from(value: &Path) -> Result { + let entries = WalkDir::new(value) + .into_iter() + .collect::>() + .par_iter() + .filter(|x| x.is_ok()) + .filter_map(|x| { + Entry::try_from(x.as_ref().unwrap().path().to_path_buf().as_path()).ok() + }) + .collect(); + Ok(Self { entries }) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum Entry { + File { + path: PathBuf, + sha256sum: String, + mode: u32, + size: usize, + }, + Directory { + path: PathBuf, + mode: u32, + }, + Link { + path: PathBuf, + target: PathBuf, + }, +} + +impl TryFrom<&Path> for Entry { + type Error = Box; + + fn try_from(value: &Path) -> Result { + let mut path = PathBuf::from("/"); + path.push(value); + let meta = fs::metadata(value)?; + if meta.is_file() { + let mut buf = vec![]; + let mut fd = fs::File::open(value)?; + let size = fd.read_to_end(&mut buf)?; + let mut sha256sum = String::new(); + let mut hasher = Sha256::new(); + hasher.update(&buf); + let res = hasher.finalize(); + for c in res { + write!(sha256sum, "{c:02x}")?; + } + let mode = meta.mode(); + Ok(Self::File { + path, + sha256sum, + mode, + size, + }) + } else if meta.is_dir() { + let mode = meta.mode(); + Ok(Self::Directory { path, mode }) + } else if meta.is_symlink() { + let target = fs::read_link(value)?; + Ok(Self::Link { path, target }) + } else { + unreachable!(); + } + } +} diff --git a/hpk-package/src/tar/README.md b/hpk-package/src/tar/README.md new file mode 100644 index 0000000..062958e --- /dev/null +++ b/hpk-package/src/tar/README.md @@ -0,0 +1,8 @@ +## About +Derived originally from [minitar](https://github.com/genonullfree/minitar), this +crate implements basic functionality for creating and extracting tar archives. It +has been adapted to allow creation of a Node (Tar header + 512byte blocks of data) +from the raw data plus file metadata. This allows for better efficiency when it +is embedded into another application (such as a package manager), as the raw data +and metadata about each file can be extracted once and reused for purposes such +as generating checksums, getting file sizes and creating packing lists. diff --git a/hpk-package/src/tar/error.rs b/hpk-package/src/tar/error.rs new file mode 100644 index 0000000..52f9f3a --- /dev/null +++ b/hpk-package/src/tar/error.rs @@ -0,0 +1,22 @@ +use std::{fmt, io, num::ParseIntError, str::Utf8Error}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("DekuError: {0}")] + Deku(#[from] deku::DekuError), + #[error("IoError: {0}")] + Io(#[from] io::Error), + #[error("Error in conversion of oct_to_dev")] + Utf8Error(#[from] Utf8Error), + #[error("Error in conversion of oct_to_dev")] + ParseIntError(#[from] ParseIntError), + #[error("End of tar")] + EndOfTar, + #[error("Invalid magic")] + InvalidMagic, + #[error("Invalid Checksum")] + InvalidChecksum, + #[error("Parse int failed")] + Parse(#[from] fmt::Error), +} diff --git a/hpk-package/src/tar/header.rs b/hpk-package/src/tar/header.rs new file mode 100644 index 0000000..697f974 --- /dev/null +++ b/hpk-package/src/tar/header.rs @@ -0,0 +1,482 @@ +use crate::tar::Error; +use deku::prelude::*; +use std::{ + env, + ffi::CStr, + fmt::{self, Write}, + fs::{self, Metadata}, + io, + ops::Deref, + os::{linux::fs::MetadataExt, unix::fs::FileTypeExt}, + path::PathBuf, +}; + +#[repr(u8)] +pub enum FileType { + Normal = 0x30, + Hardlink = 0x31, + Symlink = 0x32, + Char = 0x33, + Block = 0x34, + Dir = 0x35, + FIFO = 0x36, + Unknown = 0x00, +} + +impl From for FileType +where + T: Deref, +{ + fn from(meta: T) -> Self { + if meta.is_dir() { + return FileType::Dir; + } + + let file_type = meta.file_type(); + if file_type.is_fifo() { + return FileType::FIFO; + } else if file_type.is_char_device() { + return FileType::Char; + } else if file_type.is_block_device() { + return FileType::Block; + } else if file_type.is_fifo() { + return FileType::FIFO; + } else if file_type.is_symlink() { + return FileType::Symlink; + } else if file_type.is_file() { + return FileType::Normal; + } + + FileType::Unknown + } +} + +#[derive(Clone)] +pub struct Owner { + pub uid: u32, + pub gid: u32, + pub username: String, + pub groupname: String, +} + +impl Default for Owner { + fn default() -> Self { + Self { + uid: 0, + gid: 0, + username: "root".into(), + groupname: "root".into(), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, DekuRead, DekuWrite)] +#[deku(endian = "little")] +pub struct Header { + pub(crate) fname: [u8; 100], + pub(crate) mode: [u8; 8], + pub(crate) uid: [u8; 8], + pub(crate) gid: [u8; 8], + pub(crate) size: [u8; 12], + pub(crate) mtime: [u8; 12], + pub(crate) header_checksum: [u8; 8], + pub(crate) link_indicator: [u8; 1], + pub(crate) link_name: [u8; 100], + pub(crate) ustar_magic: [u8; 6], + pub(crate) ustar_version: [u8; 2], + pub(crate) username: [u8; 32], + pub(crate) groupname: [u8; 32], + pub(crate) device_major: [u8; 8], + pub(crate) device_minor: [u8; 8], + pub(crate) file_prefix: [u8; 155], + pub(crate) reserved: [u8; 12], +} + +impl Default for Header { + fn default() -> Self { + Self { + fname: [0; 100], + mode: [0; 8], + uid: [0; 8], + gid: [0; 8], + size: [0; 12], + mtime: [0; 12], + header_checksum: [0x20; 8], + link_indicator: [0; 1], + link_name: [0; 100], + ustar_magic: [0x75, 0x73, 0x74, 0x61, 0x72, 0x20], + ustar_version: [0x20, 0x00], + username: [0; 32], + groupname: [0; 32], + device_major: [0; 8], + device_minor: [0; 8], + file_prefix: [0; 155], + reserved: [0; 12], + } + } +} + +impl Header { + /// Get the filename of this archive member + /// + /// # Example + /// + /// ``` + /// use hpk_package::tar::Header; + /// + /// let header = Header::new("test/1.txt").unwrap(); + /// let filename = header.filename().unwrap(); + /// assert_eq!(filename.as_str(), "1.txt"); + /// ``` + pub fn filename(&self) -> Result { + let mut s = String::new(); + for c in self.fname { + if c == 0 { + break; + } else { + write!(s, "{}", char::from(c))?; + } + } + Ok(s) + } + + /// Gets the Unix mode of this archive member + /// + /// # Example + /// ``` + /// use hpk_package::tar::Header; + /// + /// let header = Header::new("test/1.txt").unwrap(); + /// let mode = header.mode().unwrap(); + /// assert_eq!(mode, 420); + /// ``` + pub fn mode(&self) -> Result { + let mut s = String::new(); + for c in self.mode { + if c == 0 { + break; + } else { + write!(s, "{}", char::from(c))?; + } + } + let mode = u32::from_str_radix(&s, 8)?; + Ok(mode) + } + + fn uid(&self) -> Result { + let mut s = String::new(); + for c in self.mode { + if c == 0 { + break; + } else { + write!(s, "{}", char::from(c))?; + } + } + let uid = u32::from_str_radix(&s, 8)?; + Ok(uid) + } + + fn gid(&self) -> Result { + let mut s = String::new(); + for c in self.mode { + if c == 0 { + break; + } else { + write!(s, "{}", char::from(c))?; + } + } + let gid = u32::from_str_radix(&s, 8)?; + Ok(gid) + } + + fn username(&self) -> Result { + let mut s = String::new(); + for c in self.username { + if c == 0 { + break; + } else { + write!(s, "{}", char::from(c))?; + } + } + Ok(s) + } + + fn groupname(&self) -> Result { + let mut s = String::new(); + for c in self.groupname { + if c == 0 { + break; + } else { + write!(s, "{}", char::from(c))?; + } + } + Ok(s) + } + + pub fn owner(&self) -> Result { + let uid = self.uid()?; + let gid = self.gid()?; + let username = self.username()?; + let groupname = self.groupname()?; + Ok(Owner { + uid, + gid, + username, + groupname, + }) + } + + /// Gets the path to the file minus it's final component + /// + /// # Example + /// ``` + /// use hpk_package::tar::Header; + /// + /// let header = Header::new("test/1.txt").unwrap(); + /// let prefix = header.prefix().unwrap(); + /// assert_eq!(prefix.as_str(), "test"); + /// ``` + pub fn prefix(&self) -> Option { + let mut s = String::new(); + for c in self.file_prefix { + if c != 0 { + write!(s, "{}", char::from(c)).ok()?; + } else { + break; + } + } + if s.is_empty() { + None + } else { + Some(s) + } + } + + /// Gets the full file path to this archive member. + /// + /// # Example + /// + /// ``` + /// use hpk_package::tar::Header; + /// use std::path::PathBuf; + /// + /// let header = Header::new("test/1.txt").unwrap(); + /// let path = header.file_path().unwrap(); + /// assert_eq!(PathBuf::from("test/1.txt"), path); + /// ``` + pub fn file_path(&self) -> Result { + let mut path = match self.prefix() { + Some(p) => PathBuf::from(&p), + None => PathBuf::new(), + }; + let name = self.filename()?; + path.push(&name); + Ok(path) + } + + pub fn new(filename: &str) -> Result { + let mut header = Header::default(); + let meta = fs::symlink_metadata(filename)?; + let (filename, prefix) = { + // Original tar has a maximum file name length of 100 bytes. The ustar + // revision allows storing the path prefix separately, with 100 bytes + // reserved for the file name and 150 bytes for the rest of the path. + let path = PathBuf::from(&filename); + let name = match path.file_name().and_then(|n| n.to_str()) { + Some(n) => n.to_string(), + None => { + return Err(Error::Io(io::Error::new( + io::ErrorKind::Other, + "Cannot get file name", + ))) + } + }; + let dir = path.parent().map(|x| format!("{}", x.display())); + (name, dir) + }; + + /* Fill in metadata */ + header.fname[..filename.len()].copy_from_slice(filename.as_bytes()); + let mode = format!("{:07o}", (meta.st_mode() & 0o777)); + header.mode[..mode.len()].copy_from_slice(mode.as_bytes()); + let user = format!("{:07o}", meta.st_uid()); + header.uid[..user.len()].copy_from_slice(user.as_bytes()); + let group = format!("{:07o}", meta.st_gid()); + header.gid[..group.len()].copy_from_slice(group.as_bytes()); + let size = format!("{:011o}", meta.st_size()); + header.size[..size.len()].copy_from_slice(size.as_bytes()); + let mtime = format!("{:011o}", meta.st_mtime()); + header.mtime[..mtime.len()].copy_from_slice(mtime.as_bytes()); + if let Some(prefix) = prefix { + header.file_prefix[..prefix.len()].copy_from_slice(prefix.as_bytes()); + } + + /* Get the file type and conditional metadata */ + header.link_indicator[0] = FileType::from(&meta) as u8; + if header.link_indicator[0] == FileType::Symlink as u8 { + let link = fs::read_link(filename)?.to_str().unwrap().to_string(); + header.link_name[..link.len()].copy_from_slice(link.as_bytes()); + } else if header.link_indicator[0] == FileType::Block as u8 { + let major = format!("{:07o}", meta.st_dev()); + header.device_major[..major.len()].copy_from_slice(major.as_bytes()); + let minor = format!("{:07o}", meta.st_rdev()); + header.device_minor[..minor.len()].copy_from_slice(minor.as_bytes()); + } + + /* TODO: Find better way to get username */ + let key = "USER"; + if let Ok(val) = env::var(key) { + header.username[..val.len()].copy_from_slice(val.as_bytes()) + } + /* TODO: Find way to get groupname */ + + /* Update the header checksum value */ + header.update_checksum()?; + + Ok(header) + } + + pub fn new_from_meta( + filename: &str, + meta: &Metadata, + owner: Option, + ) -> Result { + let mut header = Header::default(); + let (filename, prefix) = { + // Original tar has a maximum file name length of 100 bytes. The ustar + // revision allows storing the path prefix separately, with 100 bytes + // reserved for the file name and 150 bytes for the rest of the path. + let path = PathBuf::from(&filename); + let name = match path.file_name().and_then(|n| n.to_str()) { + Some(n) => n.to_string(), + None => { + return Err(Error::Io(io::Error::new( + io::ErrorKind::Other, + "Cannot get file name", + ))) + } + }; + let dir = path.parent().map(|x| format!("{}", x.display())); + (name, dir) + }; + header.fname[..filename.len()].copy_from_slice(filename.as_bytes()); + let mode = format!("{:07o}", meta.st_mode()); + header.mode[..mode.len()].copy_from_slice(mode.as_bytes()); + let owner = match owner { + Some(o) => o, + None => Owner { + uid: meta.st_uid(), + gid: meta.st_gid(), + username: get_username_for_uid(meta.st_uid())?.into(), + groupname: get_groupname_for_gid(meta.st_gid())?.into(), + }, + }; + let uid = format!("{:07o}", owner.uid); + header.uid[..uid.len()].copy_from_slice(uid.as_bytes()); + let gid = format!("{:07o}", owner.gid); + header.gid[..gid.len()].copy_from_slice(gid.as_bytes()); + let size = format!("{:011o}", meta.len()); + header.size[..size.len()].copy_from_slice(size.as_bytes()); + let mtime = format!("{:011o}", meta.st_mtime()); + header.mtime[..mtime.len()].copy_from_slice(mtime.as_bytes()); + if let Some(prefix) = prefix { + header.file_prefix[..prefix.len()].copy_from_slice(prefix.as_bytes()); + } + header.link_indicator[0] = FileType::from(meta) as u8; + if header.link_indicator[0] == FileType::Symlink as u8 { + let link = fs::read_link(filename)?.to_str().unwrap().to_string(); + header.link_name[..link.len()].copy_from_slice(link.as_bytes()); + } else if header.link_indicator[0] == FileType::Block as u8 { + let major = format!("{:07o}", meta.st_dev()); + header.device_major[..major.len()].copy_from_slice(major.as_bytes()); + let minor = format!("{:07o}", meta.st_rdev()); + header.device_minor[..minor.len()].copy_from_slice(minor.as_bytes()); + } + header.username[..owner.username.len()].copy_from_slice(owner.username.as_bytes()); + header.groupname[..owner.groupname.len()].copy_from_slice(owner.groupname.as_bytes()); + header.update_checksum()?; + Ok(header) + } + + /// Validates that the magic value received matches the magic value required in the Tar specification. + /// + /// # Example + /// + /// ``` + /// use hpk_package::tar::Header; + /// let header = Header::default(); + /// if !header.validate_magic() { + /// println!("Magic value is invalid"); + /// } + /// ``` + pub fn validate_magic(self) -> bool { + self.ustar_magic == "ustar ".as_bytes() + } + + /// Validates the header checksum computes to the expected value. + /// + /// # Example + /// + /// ``` + /// use hpk_package::tar::Header; + /// let header = Header::default(); + /// if header.validate_checksum().unwrap() { + /// println!("Checksum is valid"); + /// } + /// ``` + pub fn validate_checksum(self) -> Result { + let mut test = self; + let mut new = [0x20u8; 8]; + test.header_checksum.copy_from_slice(&[0x20; 8]); + + let tmp = format!("{:06o}\x00", test.calc_checksum()?); + new[..tmp.len()].copy_from_slice(tmp.as_bytes()); + + Ok(self.header_checksum == new) + } + + /// Updates the header checksum value. + /// + /// # Example + /// + /// ``` + /// use hpk_package::tar::Header; + /// let mut header = Header::default(); + /// + /// /* Fill in header information */ + /// + /// header.update_checksum(); + /// ``` + pub fn update_checksum(&mut self) -> Result<(), Error> { + let checksum = format!("{:06o}\x00", self.calc_checksum()?); + self.header_checksum[..checksum.len()].copy_from_slice(checksum.as_bytes()); + Ok(()) + } + + fn calc_checksum(self) -> Result { + let out = self.to_bytes()?; + let mut checksum = 0; + for i in out { + checksum += i as usize; + } + Ok(checksum) + } +} + +fn get_username_for_uid<'a>(uid: u32) -> Result<&'a str, std::str::Utf8Error> { + let user = unsafe { + let pw = libc::getpwuid(uid); + let name = (*pw).pw_name; + CStr::from_ptr(name) + }; + user.to_str() +} + +fn get_groupname_for_gid<'a>(gid: u32) -> Result<&'a str, std::str::Utf8Error> { + let group = unsafe { + let gr = libc::getgrgid(gid); + let name = (*gr).gr_name; + CStr::from_ptr(name) + }; + group.to_str() +} diff --git a/hpk-package/src/tar/mod.rs b/hpk-package/src/tar/mod.rs new file mode 100644 index 0000000..f94798d --- /dev/null +++ b/hpk-package/src/tar/mod.rs @@ -0,0 +1,192 @@ +use std::{ + fs::File, + io::{self, BufReader, Write}, + path::PathBuf, +}; + +use rayon::prelude::{IntoParallelRefIterator, ParallelIterator, IndexedParallelIterator}; + +mod error; +mod header; +mod node; +pub use { + error::Error, + header::{FileType, Header, Owner}, + node::Node, +}; + +#[derive(Default)] +pub struct Archive { + pub nodes: Vec, +} + +impl Archive { + pub fn to_vec(self) -> Result, Error> { + let mut buf = vec![]; + for node in self.nodes { + buf.extend(node.to_vec()?); + } + buf.write_all(&[0; 9216])?; + Ok(buf) + } + + /// Write out a vector of `TarNodes` to a file or something that implements + /// ``std::io::Write`` and ``std::io::Copy``. + /// + /// # Example + /// + /// ``` + /// use std::fs::File; + /// use hpk_package::tar::Archive; + /// + /// let data = Archive::new("test/1.txt").unwrap(); + /// + /// let out = File::create("test/2.tar".to_string()).unwrap(); + /// data.write(&out).unwrap(); + /// ``` + pub fn write(self, mut input: T) -> Result { + let mut written = 0; + for f in self.nodes.clone() { + written += f.write(input)?; + } + + /* Complete the write with 18 blocks of 512 ``0x00`` bytes per the specification */ + if !self.nodes.is_empty() { + input.write_all(&[0; 9216])?; + written += 9216; + } + + Ok(written) + } + + /// Create a new `TarFile` struct and initialize it with a `filename` file. + /// This will read in the file to the `TarFile` struct as a `TarNode`. + /// + /// # Example + /// + /// ``` + /// use hpk_package::tar::Archive; + /// + /// let data = Archive::new("test/1.txt").unwrap(); + /// ``` + pub fn new(filename: &str) -> Result { + Ok(Self { + nodes: vec![Node::read_file_to_tar(filename)?], + }) + } + + /// Append another file to the `TarFile.file` vector. This adds a file to the + /// internal representation of the tar file. + /// + /// # Example + /// + /// ``` + /// use hpk_package::tar::Archive; + /// + /// let mut data = Archive::new("test/1.txt").unwrap(); + /// data.append("test/1.txt").unwrap(); + /// ``` + pub fn append(&mut self, filename: &str) -> Result<(), Error> { + self.nodes.push(Node::read_file_to_tar(filename)?); + + Ok(()) + } + + /// Open and load an external tar file into the internal `TarFile` struct. This + /// parses and loads up all the files contained within the external tar file. + /// + /// # Example + /// + /// ``` + /// use hpk_package::tar::Archive; + /// + /// Archive::open("test/1.tar").unwrap(); + /// ``` + pub fn open(filename: &str) -> Result { + let file = File::open(filename)?; + let mut reader = BufReader::new(file); + Self::read(&mut reader) + } + + /// Read a tar archive from anything which implements `io::Read`. + /// + /// # Example + /// + /// ``` + /// use hpk_package::tar::Archive; + /// use std::{fs::File, io::BufReader}; + /// + /// let file = File::open("test/1.tar").unwrap(); + /// let mut reader = BufReader::new(file); + /// Archive::read(&mut reader).unwrap(); + /// ``` + pub fn read(mut input: T) -> Result { + let mut out = Self { + nodes: Vec::::new(), + }; + while let Ok(t) = Node::read(&mut input) { + out.nodes.push(t); + } + Ok(out) + } + + /// Remove the first file from the Tar that matches the filename and path. + /// + /// # Example + /// + /// ``` + /// use hpk_package::tar::Archive; + /// + /// let mut data = Archive::new("test/1.tar").unwrap(); + /// data.remove("test/1.tar").unwrap(); + /// ``` + pub fn remove(&mut self, filename: &str) -> Result { + let mut name = [0u8; 100]; + name[..filename.len()].copy_from_slice(filename.as_bytes()); + if let Some(i) = &self.nodes.par_iter().position_any(|x| x.header.fname == name) { + self.nodes.remove(*i); + return Ok(true); + } + + Ok(false) + } + + /// Get the first node from the archive that matches the given filename. + /// + /// # Example + /// + /// ``` + /// use hpk_package::tar::Archive; + /// let archive = Archive::new("test/1.txt").unwrap(); + /// let node = archive.get("test/1.txt"); + /// assert!(node.is_some()); + /// ``` + pub fn get(&self, filename: &str) -> Option { + self.nodes.par_iter().find_any(|x| { + x.header.file_path() == Ok(PathBuf::from(filename)) + }).cloned() + } + + pub fn pop(&mut self, filename: &str) -> Option { + if let Some(i) = self.nodes.par_iter().position_any(|x| { + x.header.file_path() == Ok(PathBuf::from(filename)) + }) { + let node = self.nodes.get(i).cloned(); + self.nodes.remove(i); + return node; + } + None + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn open_get() { + let archive = Archive::open("test/1.tar").unwrap(); + let node = archive.get("1.txt"); + assert!(node.is_some()); + } +} diff --git a/hpk-package/src/tar/node.rs b/hpk-package/src/tar/node.rs new file mode 100644 index 0000000..e618910 --- /dev/null +++ b/hpk-package/src/tar/node.rs @@ -0,0 +1,138 @@ +use crate::tar::{header::Owner, Error, FileType, Header}; +use deku::prelude::*; +use std::{ + fs::{File, Metadata}, + io::{self, BufReader}, + str, +}; + +#[derive(Clone, Debug, Default)] +pub struct Node { + pub header: Header, + pub data: Vec<[u8; 512]>, +} + +impl Node { + pub fn to_vec(self) -> Result, DekuError> { + let mut buf = self.header.to_bytes()?; + for block in self.data { + buf.extend(block.to_vec()); + } + Ok(buf) + } + + /// Write out a single file within the tar to a file or something with a + /// ``std::io::Write`` trait. + pub fn write(&self, mut input: T) -> Result { + input.write_all(&self.header.to_bytes()?)?; + let mut written = 512; + for d in &self.data { + input.write_all(d)?; + written += d.len(); + } + + Ok(written) + } + + /// Read a TarNode in from a file or something with a ``std::io::Read`` trait. + pub fn read(mut input: T) -> Result { + let mut h = vec![0u8; 512]; + input.read_exact(&mut h)?; + + let (_, header) = Header::from_bytes((&h, 0))?; + if header == Header::default() { + return Err(Error::EndOfTar); + } + if !header.validate_magic() { + return Err(Error::InvalidMagic); + } + if !header.validate_checksum()? { + return Err(Error::InvalidChecksum); + } + + let chunks = (oct_to_dec(&header.size)? / 512) + 1; + Ok(Node { + header, + data: Node::chunk_file(&mut input, Some(chunks))?, + }) + } + + /// Open and read a file from the ``filename`` argument to a TarNode. + pub fn read_file_to_tar(filename: &str) -> Result { + let header = Header::new(filename)?; + if header.link_indicator[0] != FileType::Normal as u8 { + return Ok(Node { + header, + data: Vec::<[u8; 512]>::new(), + }); + } + + let file = File::open(filename)?; + let mut reader = BufReader::new(file); + Ok(Node { + header, + data: Node::chunk_file(&mut reader, None)?, + }) + } + + /// Create a Node from in memory data, given the filename and metadata + pub fn read_data_to_tar( + data: &[u8], + filename: &str, + meta: &Metadata, + owner: Option, + ) -> Result { + let header = Header::new_from_meta(filename, meta, owner)?; + if header.link_indicator[0] != FileType::Normal as u8 { + return Ok(Node { + header, + data: Vec::<[u8; 512]>::new(), + }); + } + let mut reader = BufReader::new(data); + Ok(Node { + header, + data: Node::chunk_file(&mut reader, None)?, + }) + } + + /// Read in and split a file into ``512`` byte chunks. + fn chunk_file( + file: &mut T, + max_chunks: Option, + ) -> Result, Error> { + /* Extract the file data from the tar file */ + let mut out = Vec::<[u8; 512]>::new(); + let mut n = if let Some(max) = max_chunks { + max + } else { + usize::MAX + }; + + /* Carve out 512 bytes at a time */ + let mut buf: [u8; 512] = [0; 512]; + loop { + let len = file.read(&mut buf)?; + + n -= 1; + + /* If read len == 0, we've hit the EOF */ + if len == 0 || n == 0 { + break; + } + + /* Save this chunk */ + out.push(buf); + } + Ok(out) + } +} + +fn oct_to_dec(input: &[u8]) -> Result { + /* Convert the &[u8] to string and remove the null byte */ + let mut s = str::from_utf8(input)?.to_string(); + s.pop(); + + /* Convert to usize from octal */ + Ok(usize::from_str_radix(&s, 8)?) +} diff --git a/hpk-package/src/version/gitrev.rs b/hpk-package/src/version/gitrev.rs new file mode 100644 index 0000000..b40331f --- /dev/null +++ b/hpk-package/src/version/gitrev.rs @@ -0,0 +1,101 @@ +use { + crate::Version, + chrono::{offset::Utc, DateTime}, + serde::{Deserialize, Serialize}, + std::{cmp::Ordering, error::Error, fmt, str::FromStr}, +}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +/// Represents a Git revision +pub struct GitRev { + /// the short revision hash + pub hash: String, + /// the time of the revision commit + pub datetime: DateTime, +} + +impl fmt::Display for GitRev { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "git_{}.{}", self.hash, self.datetime.format("%Y%m%d")) + } +} + +impl PartialOrd for GitRev { + fn partial_cmp(&self, other: &Self) -> Option { + self.datetime.partial_cmp(&other.datetime) + } +} + +impl Ord for GitRev { + fn cmp(&self, other: &Self) -> Ordering { + self.datetime.cmp(&other.datetime) + } +} + +#[derive(Debug)] +pub struct ParseGitRevError; + +impl fmt::Display for ParseGitRevError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Error parsing git version") + } +} + +impl Error for ParseGitRevError {} + +impl From for ParseGitRevError { + fn from(_value: chrono::ParseError) -> Self { + Self + } +} + +impl FromStr for GitRev { + type Err = ParseGitRevError; + + fn from_str(s: &str) -> Result { + if let Some(gitrev) = s.strip_prefix("git_") { + if let Some((hash, date)) = gitrev.split_once('_') { + if hash.len() == 7 { + let datetime = DateTime::parse_from_str(date, "%Y%m%d")?; + return Ok(Self { + hash: hash.to_string(), + datetime: datetime.into(), + }); + } + } + } + Err(ParseGitRevError) + } +} + +impl TryFrom for GitRev { + type Error = ParseGitRevError; + + fn try_from(value: Version) -> Result { + match value { + Version::Git(g) => Ok(g), + _ => Err(ParseGitRevError), + } + } +} + +#[cfg(test)] +mod test { + use std::{thread, time::Duration}; + + use super::*; + + #[test] + fn ord() { + let a = GitRev { + hash: "aaab".to_string(), + datetime: Utc::now(), + }; + thread::sleep(Duration::from_millis(10)); + let b = GitRev { + hash: "aaaa".to_string(), + datetime: Utc::now(), + }; + assert!(a < b); + } +} diff --git a/hpk-package/src/version/mod.rs b/hpk-package/src/version/mod.rs new file mode 100644 index 0000000..903b117 --- /dev/null +++ b/hpk-package/src/version/mod.rs @@ -0,0 +1,177 @@ +use { + serde::{Deserialize, Serialize}, + std::{error::Error, fmt, str::FromStr}, +}; + +mod gitrev; +mod rapid; +mod semver; + +pub use {gitrev::GitRev, rapid::Rapid, semver::SemVer}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +/// An enum representing the most common versioning schemes. +/// Each scheme must implement Eq and Ord with itself, and +/// may optionally implement PartialEq and PartialOrd with +/// other schemes. Number, Rapid, and SemVer do this, while +/// Git can only compare with itself. +pub enum Version { + /// A single replease number, as in Firefox 102 + Number(u32), + /// Rapid versioning consists of two numbers separated by + /// a dot (.) character, and is used notably by Gnome + Rapid(Rapid), + /// SemVer is the most common versioning scheme and consists + /// of three dot (.) separated numbers. The numbers are major, + /// minor, and patch respctively. + SemVer(SemVer), + /// A git revision. Use of this scheme is to be avoided in + /// official packaging as it cannot be readily compared with + /// other versioning schemes for dependency resolution. + Git(GitRev), +} + +impl Default for Version { + fn default() -> Self { + Self::SemVer(SemVer { + major: 0, + minor: 1, + patch: 0, + }) + } +} + +impl fmt::Display for Version { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Number(n) => write!(f, "{n}"), + Self::SemVer(s) => write!(f, "{s}"), + Self::Rapid(r) => write!(f, "{r}"), + Self::Git(g) => write!(f, "{g}"), + } + } +} + +impl From for Version { + fn from(value: SemVer) -> Self { + Self::SemVer(value) + } +} + +impl From for Version { + fn from(value: Rapid) -> Self { + Self::Rapid(value) + } +} + +impl From for Version { + fn from(value: GitRev) -> Self { + Self::Git(value) + } +} + +impl From for Version { + fn from(value: u32) -> Self { + Self::Number(value) + } +} + +impl PartialOrd for Version { + #[allow(clippy::many_single_char_names)] + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (Self::Number(s), Self::Number(o)) => s.partial_cmp(o), + (Self::Number(s), Self::Rapid(o)) => s.partial_cmp(o), + (Self::Number(s), Self::SemVer(o)) => s.partial_cmp(o), + (Self::Rapid(s), Self::Number(o)) => s.partial_cmp(o), + (Self::Rapid(s), Self::Rapid(o)) => s.partial_cmp(o), + (Self::Rapid(s), Self::SemVer(o)) => s.partial_cmp(o), + (Self::SemVer(s), Self::Number(o)) => s.partial_cmp(o), + (Self::SemVer(s), Self::Rapid(o)) => s.partial_cmp(o), + (Self::SemVer(s), Self::SemVer(o)) => s.partial_cmp(o), + (Self::Git(s), Self::Git(o)) => s.partial_cmp(o), + _ => None, + } + } +} + +impl PartialEq for Version { + #[allow(clippy::many_single_char_names)] + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Number(s), Self::Number(o)) => s.eq(o), + (Self::Number(s), Self::Rapid(o)) => s.eq(o), + (Self::Number(s), Self::SemVer(o)) => s.eq(o), + (Self::Rapid(s), Self::Number(o)) => s.eq(o), + (Self::Rapid(s), Self::Rapid(o)) => s.eq(o), + (Self::Rapid(s), Self::SemVer(o)) => s.eq(o), + (Self::SemVer(s), Self::Number(o)) => s.eq(o), + (Self::SemVer(s), Self::Rapid(o)) => s.eq(o), + (Self::SemVer(s), Self::SemVer(o)) => s.eq(o), + (Self::Git(s), Self::Git(o)) => s.eq(o), + _ => false, + } + } +} + +#[derive(Debug)] +pub struct ParseVersionError; + +impl fmt::Display for ParseVersionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "error parsing version") + } +} + +impl Error for ParseVersionError {} + +impl FromStr for Version { + type Err = ParseVersionError; + + fn from_str(s: &str) -> Result { + if let Ok(v) = s.parse::() { + Ok(v.into()) + } else if let Ok(v) = s.parse::() { + Ok(v.into()) + } else if let Ok(v) = s.parse::() { + Ok(v.into()) + } else if let Ok(v) = s.parse::() { + Ok(v.into()) + } else { + Err(ParseVersionError) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn cmp_semver_rapid_gt() { + let sem = "1.42.1".parse::().unwrap(); + let rpd = "1.42".parse::().unwrap(); + assert!(sem > rpd); + } + + #[test] + fn cmp_semver_rapid_eq() { + let sem = "1.42.0".parse::().unwrap(); + let rpd = "1.42".parse::().unwrap(); + assert!(sem == rpd); + } + + #[test] + fn cmp_semver_num_gt() { + let sem = Version::SemVer("42.69.0".parse().unwrap()); + let num = Version::Number(42); + assert!(sem > num); + } + + #[test] + fn cmp_semver_num_eq() { + let sem = Version::SemVer("42.0.0".parse().unwrap()); + let num = Version::Number(42); + assert_eq!(sem, num); + } +} diff --git a/hpk-package/src/version/rapid.rs b/hpk-package/src/version/rapid.rs new file mode 100644 index 0000000..e7842fa --- /dev/null +++ b/hpk-package/src/version/rapid.rs @@ -0,0 +1,194 @@ +use crate::SemVer; + +use { + crate::Version, + serde::{Deserialize, Serialize}, + std::{cmp::Ordering, error::Error, fmt, num::ParseIntError, str::FromStr}, +}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct Rapid { + pub major: u32, + pub minor: u32, +} + +impl fmt::Display for Rapid { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}.{}", self.major, self.minor) + } +} + +impl PartialOrd for Rapid { + fn partial_cmp(&self, other: &Self) -> Option { + match self.major.partial_cmp(&other.major) { + Some(Ordering::Greater) => Some(Ordering::Greater), + Some(Ordering::Less) => Some(Ordering::Less), + Some(Ordering::Equal) => self.minor.partial_cmp(&other.minor), + None => None, + } + } +} + +impl Ord for Rapid { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match self.major.cmp(&other.major) { + Ordering::Greater => Ordering::Greater, + Ordering::Less => Ordering::Less, + Ordering::Equal => self.minor.cmp(&other.minor), + } + } +} + +impl PartialEq for Rapid { + fn eq(&self, other: &u32) -> bool { + self.major == *other && self.minor == 0 + } +} + +impl PartialOrd for Rapid { + fn partial_cmp(&self, other: &u32) -> Option { + match self.major.partial_cmp(other) { + Some(Ordering::Greater) => Some(Ordering::Greater), + Some(Ordering::Less) => Some(Ordering::Less), + None => None, + Some(Ordering::Equal) => { + if self.minor == 0 { + Some(Ordering::Equal) + } else { + Some(Ordering::Greater) + } + } + } + } +} + +impl PartialEq for Rapid { + fn eq(&self, other: &SemVer) -> bool { + other.eq(self) + } +} + +impl PartialOrd for Rapid { + fn partial_cmp(&self, other: &SemVer) -> Option { + match other.partial_cmp(self) { + Some(Ordering::Less) => Some(Ordering::Greater), + Some(Ordering::Greater) => Some(Ordering::Less), + Some(Ordering::Equal) => Some(Ordering::Equal), + None => None, + } + } +} + +impl PartialEq for u32 { + fn eq(&self, other: &Rapid) -> bool { + other.eq(self) + } +} + +impl PartialOrd for u32 { + fn partial_cmp(&self, other: &Rapid) -> Option { + match other.partial_cmp(self) { + Some(Ordering::Equal) => Some(Ordering::Equal), + Some(Ordering::Less) => Some(Ordering::Greater), + Some(Ordering::Greater) => Some(Ordering::Less), + None => None, + } + } +} + +#[derive(Debug, PartialEq)] +pub struct ParseRapidError; + +impl fmt::Display for ParseRapidError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Error parsing Rapid version") + } +} + +impl Error for ParseRapidError {} + +impl From for ParseRapidError { + fn from(_value: ParseIntError) -> Self { + Self + } +} + +impl FromStr for Rapid { + type Err = ParseRapidError; + + fn from_str(s: &str) -> Result { + let split = s.split('.').collect::>(); + match split.len() { + 2 => { + let major = split.first().unwrap().parse::()?; + let minor = split.get(1).unwrap().parse::()?; + Ok(Self { major, minor }) + } + _ => Err(ParseRapidError), + } + } +} + +impl TryFrom for Rapid { + type Error = ParseRapidError; + + fn try_from(value: Version) -> Result { + match value { + Version::SemVer(s) => { + if s.patch == 0 { + Ok(Self { + major: s.major, + minor: s.minor, + }) + } else { + Err(ParseRapidError) + } + } + Version::Rapid(s) => Ok(s), + Version::Number(major) => Ok(Self { major, minor: 0 }), + Version::Git(_) => Err(ParseRapidError), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn parse_semver() { + assert_eq!( + "93.0".parse::(), + Ok(Rapid { + major: 93, + minor: 0, + }) + ); + } + + #[test] + fn rapid_num_eq() { + let rapid = Rapid { major: 42, minor: 0 }; + assert_eq!(rapid, 42); + } + + #[test] + fn rapid_num_gt() { + let rapid = Rapid { major: 1, minor: 42 }; + assert!(rapid > 1); + } + + #[test] + fn rapid_semver_eq() { + let rapid = Rapid { major: 42, minor: 69 }; + let semver = SemVer { major: 42, minor: 69, patch: 0 }; + assert_eq!(rapid, semver); + } + + #[test] + fn rapid_semver_lt() { + let rapid = Rapid { major: 42, minor: 69 }; + let semver = SemVer { major: 42, minor: 69, patch: 1 }; + assert!(rapid < semver); + } +} diff --git a/hpk-package/src/version/semver.rs b/hpk-package/src/version/semver.rs new file mode 100644 index 0000000..89758ec --- /dev/null +++ b/hpk-package/src/version/semver.rs @@ -0,0 +1,205 @@ +use { + crate::{Rapid, Version}, + serde::{Deserialize, Serialize}, + std::{cmp::Ordering, error::Error, fmt, num::ParseIntError, str::FromStr}, +}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct SemVer { + pub major: u32, + pub minor: u32, + pub patch: u32, +} + +impl fmt::Display for SemVer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch) + } +} + +impl PartialOrd for SemVer { + fn partial_cmp(&self, other: &Self) -> Option { + match self.major.partial_cmp(&other.major) { + Some(Ordering::Greater) => Some(Ordering::Greater), + Some(Ordering::Less) => Some(Ordering::Less), + None => None, + Some(Ordering::Equal) => match self.minor.partial_cmp(&other.minor) { + Some(Ordering::Greater) => Some(Ordering::Greater), + Some(Ordering::Less) => Some(Ordering::Less), + None => None, + Some(Ordering::Equal) => self.patch.partial_cmp(&other.patch), + }, + } + } +} + +impl Ord for SemVer { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match self.major.cmp(&other.major) { + Ordering::Greater => Ordering::Greater, + Ordering::Less => Ordering::Less, + Ordering::Equal => match self.minor.cmp(&other.minor) { + Ordering::Greater => Ordering::Greater, + Ordering::Less => Ordering::Less, + Ordering::Equal => self.patch.cmp(&other.patch), + }, + } + } +} + +impl PartialEq for SemVer { + fn eq(&self, other: &Rapid) -> bool { + self.major == other.major && self.minor == other.minor && self.patch == 0 + } +} + +impl PartialOrd for SemVer { + fn partial_cmp(&self, other: &Rapid) -> Option { + match self.major.partial_cmp(&other.major) { + Some(Ordering::Greater) => Some(Ordering::Greater), + Some(Ordering::Less) => Some(Ordering::Less), + None => None, + Some(Ordering::Equal) => match self.minor.partial_cmp(&other.minor) { + Some(Ordering::Greater) => Some(Ordering::Greater), + Some(Ordering::Less) => Some(Ordering::Less), + None => None, + Some(Ordering::Equal) => match self.patch { + 0 => Some(Ordering::Equal), + _ => Some(Ordering::Greater), + }, + }, + } + } +} + +impl PartialEq for SemVer { + fn eq(&self, other: &u32) -> bool { + self.major == *other && self.minor == 0 && self.patch == 0 + } +} + +impl PartialOrd for SemVer { + fn partial_cmp(&self, other: &u32) -> Option { + match self.major.cmp(other) { + Ordering::Greater => Some(Ordering::Greater), + Ordering::Less => Some(Ordering::Less), + Ordering::Equal => { + if self.minor == 0 && self.patch == 0 { + Some(Ordering::Equal) + } else { + Some(Ordering::Greater) + } + } + } + } +} + +impl PartialEq for u32 { + fn eq(&self, other: &SemVer) -> bool { + other.eq(self) + } +} + +impl PartialOrd for u32 { + fn partial_cmp(&self, other: &SemVer) -> Option { + match other.partial_cmp(self) { + Some(Ordering::Less) => Some(Ordering::Greater), + Some(Ordering::Greater) => Some(Ordering::Less), + Some(Ordering::Equal) => Some(Ordering::Equal), + None => None, + } + } +} + +#[derive(Debug, PartialEq)] +pub struct ParseSemVerError; + +impl fmt::Display for ParseSemVerError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Error parsing SemVer") + } +} + +impl Error for ParseSemVerError {} + +impl From for ParseSemVerError { + fn from(_value: ParseIntError) -> Self { + Self + } +} + +impl FromStr for SemVer { + type Err = ParseSemVerError; + + fn from_str(s: &str) -> Result { + let split = s.split('.').collect::>(); + match split.len() { + 3 => { + let major = split.first().unwrap().parse::()?; + let minor = split.get(1).unwrap().parse::()?; + let patch = split.get(2).unwrap().parse::()?; + Ok(Self { + major, + minor, + patch, + }) + } + _ => Err(ParseSemVerError), + } + } +} + +impl TryFrom for SemVer { + type Error = ParseSemVerError; + fn try_from(value: Version) -> Result { + match value { + Version::SemVer(s) => Ok(SemVer { + major: s.major, + minor: s.minor, + patch: s.patch, + }), + Version::Rapid(r) => Ok(SemVer { + major: r.major, + minor: r.minor, + patch: 0, + }), + Version::Number(n) => Ok(Self { + major: n, + minor: 0, + patch: 0, + }), + Version::Git(_) => Err(ParseSemVerError), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn parse_semver() { + assert_eq!( + "1.0.3".parse::(), + Ok(SemVer { + major: 1, + minor: 0, + patch: 3 + }) + ); + } + + #[test] + fn cmp_semver_num_gt() { + let sem = "42.69.0".parse::().unwrap(); + let num = 42; + assert!(sem > num); + } + + #[test] + fn cmp_semver_num_eq() { + let sem = "42.0.0".parse::().unwrap(); + let num = 42; + assert_eq!(sem, num); + } +} diff --git a/hpk-package/test/1.tar b/hpk-package/test/1.tar new file mode 100644 index 0000000..ef16843 Binary files /dev/null and b/hpk-package/test/1.tar differ diff --git a/hpk-package/test/1.txt b/hpk-package/test/1.txt new file mode 100644 index 0000000..6de7b8c --- /dev/null +++ b/hpk-package/test/1.txt @@ -0,0 +1 @@ +This is a test file. diff --git a/hpk-package/test/2.tar b/hpk-package/test/2.tar new file mode 100644 index 0000000..d37f5f8 Binary files /dev/null and b/hpk-package/test/2.tar differ