Merge hpk-package back in

This commit is contained in:
Nathan Fisher 2023-04-16 09:59:04 -04:00
commit b1fcbd1f9f
20 changed files with 1925 additions and 0 deletions

3
hpk-package/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
/Cargo.lock
tags

29
hpk-package/Cargo.toml Normal file
View File

@ -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

12
hpk-package/src/lib.rs Normal file
View File

@ -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::*,
};

View File

@ -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<Self, Self::Err> {
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),
}
}
}

View File

@ -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<Version>, Option<Version>),
}
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
}
}
}

View File

@ -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<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Group {
pub name: String,
pub gid: Option<u32>,
}
#[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<Dependency>,
/// an optional list of users to be created upon installation
pub users: Option<Vec<User>>,
/// an optional list of groups to be created upon installation
pub groups: Option<Vec<Group>>,
/// an optional post installation shell script to be run
pub post_install: Option<String>,
}
impl From<Specs> 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<String, ron::Error> {
let cfg = PrettyConfig::new().struct_names(true);
to_string_pretty(self, cfg)
}
pub fn save_ron_and_create_tar_node(&self, outdir: &Path) -> Result<Node, Box<dyn Error>> {
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))
}
}

View File

@ -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<Dependency>,
/// an optional list of users to be created upon installation
pub users: Option<Vec<User>>,
/// an optional list of groups to be created upon installation
pub groups: Option<Vec<Group>>,
/// an optional post installation shell script to be run
pub post_install: Option<String>,
}

View File

@ -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<Entry>,
}
impl TryFrom<&Path> for Plist {
type Error = Box<dyn Error>;
fn try_from(value: &Path) -> Result<Self, Self::Error> {
let entries = WalkDir::new(value)
.into_iter()
.collect::<Vec<_>>()
.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<dyn Error>;
fn try_from(value: &Path) -> Result<Self, Self::Error> {
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!();
}
}
}

View File

@ -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.

View File

@ -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),
}

View File

@ -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<T> From<T> for FileType
where
T: Deref<Target = Metadata>,
{
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<String, fmt::Error> {
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<u32, Error> {
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<u32, Error> {
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<u32, Error> {
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<String, fmt::Error> {
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<String, fmt::Error> {
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<Owner, Error> {
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<String> {
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<PathBuf, fmt::Error> {
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<Self, Error> {
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<Owner>,
) -> Result<Self, Error> {
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<bool, Error> {
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<usize, Error> {
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()
}

192
hpk-package/src/tar/mod.rs Normal file
View File

@ -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<Node>,
}
impl Archive {
pub fn to_vec(self) -> Result<Vec<u8>, 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<T: io::Write + Copy>(self, mut input: T) -> Result<usize, Error> {
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<Self, Error> {
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<Self, Error> {
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<T: io::Read>(mut input: T) -> Result<Self, Error> {
let mut out = Self {
nodes: Vec::<Node>::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<bool, Error> {
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<Node> {
self.nodes.par_iter().find_any(|x| {
x.header.file_path() == Ok(PathBuf::from(filename))
}).cloned()
}
pub fn pop(&mut self, filename: &str) -> Option<Node> {
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());
}
}

138
hpk-package/src/tar/node.rs Normal file
View File

@ -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<Vec<u8>, 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<T: io::Write>(&self, mut input: T) -> Result<usize, Error> {
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<T: io::Read>(mut input: T) -> Result<Self, Error> {
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<Self, Error> {
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<Owner>,
) -> Result<Self, Error> {
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<T: std::io::Read>(
file: &mut T,
max_chunks: Option<usize>,
) -> Result<Vec<[u8; 512]>, 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<usize, Error> {
/* 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)?)
}

View File

@ -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<Utc>,
}
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<Ordering> {
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<chrono::ParseError> for ParseGitRevError {
fn from(_value: chrono::ParseError) -> Self {
Self
}
}
impl FromStr for GitRev {
type Err = ParseGitRevError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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<Version> for GitRev {
type Error = ParseGitRevError;
fn try_from(value: Version) -> Result<Self, Self::Error> {
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);
}
}

View File

@ -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<SemVer> for Version {
fn from(value: SemVer) -> Self {
Self::SemVer(value)
}
}
impl From<Rapid> for Version {
fn from(value: Rapid) -> Self {
Self::Rapid(value)
}
}
impl From<GitRev> for Version {
fn from(value: GitRev) -> Self {
Self::Git(value)
}
}
impl From<u32> 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<std::cmp::Ordering> {
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<Self, Self::Err> {
if let Ok(v) = s.parse::<SemVer>() {
Ok(v.into())
} else if let Ok(v) = s.parse::<Rapid>() {
Ok(v.into())
} else if let Ok(v) = s.parse::<GitRev>() {
Ok(v.into())
} else if let Ok(v) = s.parse::<u32>() {
Ok(v.into())
} else {
Err(ParseVersionError)
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn cmp_semver_rapid_gt() {
let sem = "1.42.1".parse::<SemVer>().unwrap();
let rpd = "1.42".parse::<Rapid>().unwrap();
assert!(sem > rpd);
}
#[test]
fn cmp_semver_rapid_eq() {
let sem = "1.42.0".parse::<SemVer>().unwrap();
let rpd = "1.42".parse::<Rapid>().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);
}
}

View File

@ -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<Ordering> {
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<u32> for Rapid {
fn eq(&self, other: &u32) -> bool {
self.major == *other && self.minor == 0
}
}
impl PartialOrd<u32> for Rapid {
fn partial_cmp(&self, other: &u32) -> Option<Ordering> {
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<SemVer> for Rapid {
fn eq(&self, other: &SemVer) -> bool {
other.eq(self)
}
}
impl PartialOrd<SemVer> for Rapid {
fn partial_cmp(&self, other: &SemVer) -> Option<Ordering> {
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<Rapid> for u32 {
fn eq(&self, other: &Rapid) -> bool {
other.eq(self)
}
}
impl PartialOrd<Rapid> for u32 {
fn partial_cmp(&self, other: &Rapid) -> Option<Ordering> {
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<ParseIntError> for ParseRapidError {
fn from(_value: ParseIntError) -> Self {
Self
}
}
impl FromStr for Rapid {
type Err = ParseRapidError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let split = s.split('.').collect::<Vec<_>>();
match split.len() {
2 => {
let major = split.first().unwrap().parse::<u32>()?;
let minor = split.get(1).unwrap().parse::<u32>()?;
Ok(Self { major, minor })
}
_ => Err(ParseRapidError),
}
}
}
impl TryFrom<Version> for Rapid {
type Error = ParseRapidError;
fn try_from(value: Version) -> Result<Self, Self::Error> {
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::<Rapid>(),
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);
}
}

View File

@ -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<Ordering> {
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<Rapid> for SemVer {
fn eq(&self, other: &Rapid) -> bool {
self.major == other.major && self.minor == other.minor && self.patch == 0
}
}
impl PartialOrd<Rapid> for SemVer {
fn partial_cmp(&self, other: &Rapid) -> Option<std::cmp::Ordering> {
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<u32> for SemVer {
fn eq(&self, other: &u32) -> bool {
self.major == *other && self.minor == 0 && self.patch == 0
}
}
impl PartialOrd<u32> for SemVer {
fn partial_cmp(&self, other: &u32) -> Option<Ordering> {
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<SemVer> for u32 {
fn eq(&self, other: &SemVer) -> bool {
other.eq(self)
}
}
impl PartialOrd<SemVer> for u32 {
fn partial_cmp(&self, other: &SemVer) -> Option<Ordering> {
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<ParseIntError> for ParseSemVerError {
fn from(_value: ParseIntError) -> Self {
Self
}
}
impl FromStr for SemVer {
type Err = ParseSemVerError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let split = s.split('.').collect::<Vec<_>>();
match split.len() {
3 => {
let major = split.first().unwrap().parse::<u32>()?;
let minor = split.get(1).unwrap().parse::<u32>()?;
let patch = split.get(2).unwrap().parse::<u32>()?;
Ok(Self {
major,
minor,
patch,
})
}
_ => Err(ParseSemVerError),
}
}
}
impl TryFrom<Version> for SemVer {
type Error = ParseSemVerError;
fn try_from(value: Version) -> Result<Self, Self::Error> {
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::<SemVer>(),
Ok(SemVer {
major: 1,
minor: 0,
patch: 3
})
);
}
#[test]
fn cmp_semver_num_gt() {
let sem = "42.69.0".parse::<SemVer>().unwrap();
let num = 42;
assert!(sem > num);
}
#[test]
fn cmp_semver_num_eq() {
let sem = "42.0.0".parse::<SemVer>().unwrap();
let num = 42;
assert_eq!(sem, num);
}
}

BIN
hpk-package/test/1.tar Normal file

Binary file not shown.

1
hpk-package/test/1.txt Normal file
View File

@ -0,0 +1 @@
This is a test file.

BIN
hpk-package/test/2.tar Normal file

Binary file not shown.