commit 1e7d73bc28ec53e72bbe4fb357b171367fef158c Author: Nathan Fisher Date: Sun Apr 2 18:51:12 2023 -0400 Split from hpk crate diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..83a0302 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +/Cargo.lock +test diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..19b8704 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,30 @@ +[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" +zstd = "0.12" +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/src/creator/mod.rs b/src/creator/mod.rs new file mode 100644 index 0000000..e143601 --- /dev/null +++ b/src/creator/mod.rs @@ -0,0 +1,143 @@ +use { + crate::{Entry, Item, Package, Plist, Specs}, + rayon::prelude::{IntoParallelRefIterator, ParallelIterator}, + std::{ + borrow::BorrowMut, + env, + error::Error, + fs::{self, File}, + io::{self, Write}, + path::{Path, PathBuf}, + sync::{ + atomic::{AtomicUsize, Ordering}, + mpsc::Sender, + Mutex, + }, + }, + walkdir::WalkDir, + zstd::Encoder, +}; + +pub enum Message { + MemberAdded(String), + Success(String), + Failure(String), +} + +pub struct Creator { + path: PathBuf, + entries: Vec, + specs: Specs, +} + +impl Creator { + pub fn new(path: &Path, specs: Specs) -> Result { + let d = env::current_dir()?; + env::set_current_dir(path)?; + let path = path.to_path_buf(); + let entries = WalkDir::new(".") + .into_iter() + .filter(Result::is_ok) + .map(|x| x.unwrap().path().to_path_buf()) + .collect::>(); + env::set_current_dir(d)?; + Ok(Self { + path, + entries, + specs, + }) + } + + pub fn from_list(list: &[&str], specs: Specs) -> Result { + let entries = list.iter().map(|x| Path::new(x).to_path_buf()).collect(); + Ok(Self { + path: env::current_dir().unwrap_or(Path::new("/").to_path_buf()), + entries, + specs, + }) + } + + pub fn len(&self) -> usize { + self.entries.len() + } + + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } + + pub fn create(self, outdir: &Path, sender: Sender) -> Result<(), Box> { + let d = env::current_dir()?; + let plist = Mutex::new(Plist::default()); + let totalsize: AtomicUsize = 0.into(); + let fullname = format!( + "{}-{}_{}", + &self.specs.name, self.specs.version, self.specs.release + ); + if !outdir.exists() { + fs::create_dir_all(outdir)?; + } + let outdir = outdir.canonicalize()?; + let mut archive = outdir.clone(); + archive.push(&fullname); + archive.set_extension("tar.zst"); + let fd = File::create(&archive)?; + let writer = Mutex::new(Encoder::new(fd, 0)?); + let sender = Mutex::new(sender); + env::set_current_dir(&self.path)?; + self.entries + .par_iter() + .filter(|x| x.as_path() != Path::new(".")) + .for_each(|x| { + let sender = sender.lock().unwrap().clone(); + if let Ok(item) = Item::try_create(x.as_path()) { + if let Entry::File { + path: _, + sha256sum: _, + mode: _, + size, + } = &item.entry + { + totalsize.fetch_add(*size, Ordering::Release); + } + let path = match item.entry.clone() { + Entry::File { + path, + sha256sum: _, + mode: _, + size: _, + } + | Entry::Link { path, target: _ } + | Entry::Directory { path, mode: _ } => path, + }; + plist.lock().unwrap().borrow_mut().entries.push(item.entry); + match writer.lock().unwrap().borrow_mut().write_all(&item.data) { + Ok(_) => sender + .send(Message::MemberAdded(format!("{}", path.display()))) + .expect("couldn't send message"), + Err(e) => sender + .send(Message::Failure(format!("{e}"))) + .expect("couldn't send message"), + } + } else { + sender + .send(Message::Failure(format!( + "Could not process DirEntry for {}", + x.display() + ))) + .expect("could not send message"); + } + }); + let mut package: Package = self.specs.into(); + package.size = totalsize.into_inner(); + let plist = plist.into_inner()?; + package.plist = plist; + let node = package.save_ron_and_create_tar_node(&outdir)?; + let mut writer = writer.into_inner()?; + writer.write_all(&node.to_vec()?)?; + let _fd = writer.finish()?; + let sender = sender.into_inner()?; + sender.send(Message::Success(format!("{} saved", archive.display())))?; + env::set_current_dir(d)?; + Ok(()) + } +} diff --git a/src/item/mod.rs b/src/item/mod.rs new file mode 100644 index 0000000..6e2c60c --- /dev/null +++ b/src/item/mod.rs @@ -0,0 +1,91 @@ +use crate::Entry; +use sha2::{Digest, Sha256}; +use std::{ + error::Error, + ffi::OsStr, + fmt::Write, + fs, + io::Read, + os::unix::fs::MetadataExt, + path::{Path, PathBuf}, +}; +use crate::tar::{Node, Owner}; + +#[derive(Clone, Debug)] +pub struct Item { + pub entry: Entry, + pub data: Vec, +} + +impl Item { + pub fn try_create(path: &Path) -> Result> { + let path = fix_path(path); + let meta = fs::metadata(&path)?; + let filename = format!("{}", path.display()); + let owner = Some(Owner::default()); + if meta.is_file() { + let mut data = vec![]; + let mut fd = fs::File::open(path)?; + let path = PathBuf::from(&filename); + let size = fd.read_to_end(&mut data)?; + let mut sha256sum = String::new(); + let mut hasher = Sha256::new(); + hasher.update(&data); + let res = hasher.finalize(); + for c in res { + write!(sha256sum, "{c:02x}")?; + } + let mode = meta.mode(); + Ok(Self { + entry: Entry::File { + path, + sha256sum, + mode, + size, + }, + data: Node::read_data_to_tar(&data, &filename, &meta, owner)?.to_vec()?, + }) + } else if meta.is_dir() { + let mode = meta.mode(); + let path = PathBuf::from(&filename); + Ok(Self { + entry: Entry::Directory { path, mode }, + data: Node::read_data_to_tar(&[0; 0], &filename, &meta, owner)?.to_vec()?, + }) + } else if meta.is_symlink() { + let target = fs::read_link(path)?; + let path = PathBuf::from(&filename); + Ok(Self { + entry: Entry::Link { path, target }, + data: Node::read_data_to_tar(&[0; 0], &filename, &meta, owner)?.to_vec()?, + }) + } else { + unreachable!(); + } + } +} + +fn fix_path(path: &Path) -> PathBuf { + let path = if let Ok(p) = path.strip_prefix("./") { + p + } else { + path + }; + if path.is_file() || path.is_symlink() { + match path.ancestors().last().and_then(Path::to_str) { + Some("etc" | "var") => { + let ext = if let Some(x) = path.extension().and_then(OsStr::to_str) { + format!("{x}.new") + } else { + "new".to_string() + }; + let mut path = path.to_path_buf(); + path.set_extension(ext); + path + } + _ => path.to_path_buf(), + } + } else { + path.to_path_buf() + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ea6d0ff --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,14 @@ +pub mod tar; +mod creator; +mod item; +mod package; +mod plist; +mod version; + +pub use { + creator::{Creator, Message}, + item::Item, + package::{Dependency, Package, Specs}, + plist::*, + version::*, +}; diff --git a/src/package/dependency.rs b/src/package/dependency.rs new file mode 100644 index 0000000..64cbc29 --- /dev/null +++ b/src/package/dependency.rs @@ -0,0 +1,28 @@ +use { + super::Package, + crate::Version, + serde::{Deserialize, Serialize}, +}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct Dependency { + pub name: String, + pub version: (Option, Option), +} + +impl Dependency { + #[allow(clippy::must_use_candidate)] + 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/src/package/mod.rs b/src/package/mod.rs new file mode 100644 index 0000000..13bde79 --- /dev/null +++ b/src/package/mod.rs @@ -0,0 +1,122 @@ +mod dependency; +mod specs; + +use { + crate::{Plist, Version}, + ron::ser::{to_string_pretty, PrettyConfig}, + serde::{Deserialize, Serialize}, + std::{ + error::Error, + fs, + fs::File, + io::{BufWriter, Write}, + path::Path, + }, + crate::tar::{Node, Owner}, +}; + +pub use {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 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, + /// an optional link to an + /// [AppStream](https://www.freedesktop.org/wiki/Distributions/AppStream/) + /// metadata file + pub appstream_data: Option, + /// 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, + appstream_data: value.appstream_data, + 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(crate) 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) + } + + /// Tests whether this package is an update for another + pub fn is_upgrade(&self, other: &Self) -> bool { + self.name == other.name + && (self.version > other.version + || (self.version == other.version && self.release > other.release)) + } +} diff --git a/src/package/specs.rs b/src/package/specs.rs new file mode 100644 index 0000000..1cfcc83 --- /dev/null +++ b/src/package/specs.rs @@ -0,0 +1,31 @@ +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, + /// an optional link to an + /// [AppStream](https://www.freedesktop.org/wiki/Distributions/AppStream/) + /// metadata file + pub appstream_data: Option, + /// 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/src/plist/mod.rs b/src/plist/mod.rs new file mode 100644 index 0000000..3551aca --- /dev/null +++ b/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/src/tar/README.md b/src/tar/README.md new file mode 100644 index 0000000..062958e --- /dev/null +++ b/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/src/tar/error.rs b/src/tar/error.rs new file mode 100644 index 0000000..52f9f3a --- /dev/null +++ b/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/src/tar/header.rs b/src/tar/header.rs new file mode 100644 index 0000000..6e22b78 --- /dev/null +++ b/src/tar/header.rs @@ -0,0 +1,441 @@ +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 { + pub fn filename(&self) -> Result { + let mut s = String::new(); + for c in self.fname { + if c != b'\0' { + write!(s, "{c}")?; + } else { + break; + } + } + Ok(s) + } + + pub fn mode(&self) -> Result { + let mut s = String::new(); + for c in self.mode { + if c != b'\0' { + write!(s, "{c}")?; + } else { + break; + } + } + 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 != b'\0' { + write!(s, "{c}")?; + } else { + break; + } + } + 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 != b'\0' { + write!(s, "{c}")?; + } else { + break; + } + } + 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 != b'\0' { + write!(s, "{c}")?; + } else { + break; + } + } + Ok(s) + } + + fn groupname(&self) -> Result { + let mut s = String::new(); + for c in self.groupname { + if c != b'\0' { + write!(s, "{c}")?; + } else { + break; + } + } + 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, + }) + } + + pub fn prefix(&self) -> Result { + let mut s = String::new(); + for c in self.file_prefix { + if c != b'\0' { + write!(s, "{c}")?; + } else { + break; + } + } + Ok(s) + } + + pub fn new(filename: &str) -> Result { + let mut header = Header::default(); + let meta = fs::symlink_metadata(filename)?; + let (filename, prefix) = if filename.len() < 100 { + (filename.to_string(), None) + } else { + // Deal with file names longer than 100 bytes + 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 = match path.parent() { + Some(d) => d, + None => { + return Err(Error::Io(io::Error::new( + io::ErrorKind::Other, + "Cannot get path prefix", + ))) + } + }; + (name, Some(format!("{}", dir.display()))) + }; + + /* 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) = if filename.len() < 100 { + (filename.to_string(), None) + } else { + // Deal with file names longer than 100 bytes + 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 = match path.parent() { + Some(d) => d, + None => { + return Err(Error::Io(io::Error::new( + io::ErrorKind::Other, + "Cannot get path prefix", + ))) + } + }; + (name, Some(format!("{}", dir.display()))) + }; + 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() +} + +pub 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/src/tar/mod.rs b/src/tar/mod.rs new file mode 100644 index 0000000..c85010c --- /dev/null +++ b/src/tar/mod.rs @@ -0,0 +1,134 @@ +use std::{ + fs::File, + io::{self, BufReader, Write}, +}; + +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".to_string()).unwrap(); + /// ``` + pub fn open(filename: String) -> Result { + let file = File::open(&filename)?; + let mut reader = BufReader::new(file); + let mut out = Self { + nodes: Vec::::new(), + }; + + while let Ok(t) = Node::read(&mut reader) { + 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".to_string()).unwrap(); + /// ``` + pub fn remove(&mut self, filename: String) -> Result { + let mut name = [0u8; 100]; + name[..filename.len()].copy_from_slice(filename.as_bytes()); + if let Some(i) = &self.nodes.iter().position(|x| x.header.fname == name) { + self.nodes.remove(*i); + return Ok(true); + } + + Ok(false) + } +} diff --git a/src/tar/node.rs b/src/tar/node.rs new file mode 100644 index 0000000..eac092c --- /dev/null +++ b/src/tar/node.rs @@ -0,0 +1,137 @@ +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/src/version/gitrev.rs b/src/version/gitrev.rs new file mode 100644 index 0000000..4b6ef38 --- /dev/null +++ b/src/version/gitrev.rs @@ -0,0 +1,79 @@ +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)] +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), + } + } +} diff --git a/src/version/mod.rs b/src/version/mod.rs new file mode 100644 index 0000000..06b8057 --- /dev/null +++ b/src/version/mod.rs @@ -0,0 +1,130 @@ +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, PartialEq)] +pub enum Version { + Number(u32), + Rapid(Rapid), + SemVer(SemVer), + 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, + } + } +} + +#[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); + } +} diff --git a/src/version/mod.rs.bak b/src/version/mod.rs.bak new file mode 100644 index 0000000..9208624 --- /dev/null +++ b/src/version/mod.rs.bak @@ -0,0 +1,206 @@ +use { + chrono::{offset::Utc, DateTime}, + 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, PartialEq)] +pub enum Version { + Number(u32), + Rapid { + major: u32, + minor: u32, + }, + SemVer { + major: u32, + minor: u32, + patch: u32, + }, + Git { + hash: String, + datetime: DateTime, + }, +} + +impl Default for Version { + fn default() -> Self { + Self::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 { + major, + minor, + patch, + } => { + let v = SemVer { + major: *major, + minor: *minor, + patch: *patch, + }; + write!(f, "{v}") + } + Self::Rapid { major, minor } => { + let v = Rapid { + major: *major, + minor: *minor, + }; + write!(f, "{v}") + } + Self::Git { hash, datetime } => { + let v = GitRev { + hash: hash.clone(), + datetime: *datetime, + }; + write!(f, "{v}") + } + } + } +} + +impl From for Version { + fn from(value: SemVer) -> Self { + Self::SemVer { + major: value.major, + minor: value.minor, + patch: value.patch, + } + } +} + +impl From for Version { + fn from(value: Rapid) -> Self { + Self::Rapid { + major: value.major, + minor: value.minor, + } + } +} + +impl From for Version { + fn from(value: GitRev) -> Self { + Self::Git { + hash: value.hash, + datetime: value.datetime, + } + } +} + +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::SemVer { + major, + minor, + patch, + }, + Self::SemVer { + major: a, + minor: b, + patch: c, + }, + ) => (major, minor, patch).partial_cmp(&(a, b, c)), + (Self::Rapid { major, minor }, Self::Rapid { major: a, minor: b }) => { + (major, minor).partial_cmp(&(a, b)) + } + ( + Self::Git { + hash: _a, + datetime: b, + }, + Self::Git { + hash: _c, + datetime: d, + }, + ) => b.partial_cmp(&d), + ( + Self::SemVer { + major, + minor, + patch, + }, + Self::Rapid { major: a, minor: b }, + ) => SemVer { + major: *major, + minor: *minor, + patch: *patch, + } + .partial_cmp(&Rapid { + major: *a, + minor: *b, + }), + _ => None, + } + } +} + +#[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); + } +} diff --git a/src/version/rapid.rs b/src/version/rapid.rs new file mode 100644 index 0000000..0be2946 --- /dev/null +++ b/src/version/rapid.rs @@ -0,0 +1,168 @@ +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, + }) + ); + } +} diff --git a/src/version/semver.rs b/src/version/semver.rs new file mode 100644 index 0000000..cc9d3da --- /dev/null +++ b/src/version/semver.rs @@ -0,0 +1,191 @@ +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 + }) + ); + } +}