Compare commits

...

22 Commits

Author SHA1 Message Date
Nathan Fisher 6037836f6b Merge branch 'odin' of git.hitchhiker-linux.org:jeang3nie/hpk into odin 2023-04-17 00:29:37 -04:00
Nathan Fisher 1e7a02e33d Moved hpk-package all the way back into tree, fixed some clippy lints 2023-04-16 10:36:43 -04:00
Nathan Fisher b1fcbd1f9f Merge hpk-package back in 2023-04-16 09:59:04 -04:00
Nathan Fisher e6f392bd7a Merge branch 'odin' of git.hitchhiker-linux.org:jeang3nie/hpk-package into odin 2023-04-11 22:04:54 -04:00
Nathan Fisher e0f497cb89 Writer for tar nodes takes `&self` instead of `self` 2023-04-11 22:03:09 -04:00
Nathan Fisher 00683722d6 Make Archive::remove parallel; Add Archive::pop method; 2023-04-10 19:17:26 -04:00
Nathan Fisher 43d8c82c08 Remove appstream from Specs and Package structs 2023-04-10 18:41:23 -04:00
Nathan Fisher 89d36281e3 Added some testing for version comparison and implemented PartialEq for
Version manually
2023-04-09 19:14:16 -04:00
Nathan Fisher 407d12a711 Remove stale backup of version::mod.rs 2023-04-09 01:38:39 -04:00
Nathan Fisher 4cede60a80 Merge branch 'odin' of git.hitchhiker-linux.org:jeang3nie/hpk-package into odin 2023-04-09 01:36:26 -04:00
Nathan Fisher 32b4f80715 Fix some issues with getting tar header fields; Add some doc tests in
tar module; Add some tests for version checks;
2023-04-08 19:10:30 -04:00
Nathan Fisher ae099d94aa Fix Header::filename() method;
TODO: fix other related methods, fix Archive::get() method.
2023-04-08 11:44:09 -04:00
Nathan Fisher acbdf2d992 Cargo fmt 2023-04-07 19:11:35 -04:00
Nathan Fisher 0ab84def2e Make `package::User` and `package::Group` public 2023-04-07 19:08:23 -04:00
Nathan Fisher 723d697e90 Export `Arch` enum 2023-04-06 18:31:52 -04:00
Nathan Fisher 9cf9d469f6 Add `any` arch; Add `archive_name` method for `Package`; Ensure
architecture matches when checking if a package is an upgrade;
2023-04-04 22:02:58 -04:00
Nathan Fisher 7d6376d5c4 Store target arch as `HOST_ARCH` const, use this as default value for
`Package` struct
2023-04-04 21:52:29 -04:00
Nathan Fisher a58150cc9c Add `arch` field to `Package` 2023-04-04 20:45:43 -04:00
Nathan Fisher 90b163eb1b Add deku, ron and sha2 to re-exports 2023-04-03 18:43:58 -04:00
Nathan Fisher 218abdaa87 Adjust re-exports 2023-04-03 18:36:54 -04:00
Nathan Fisher ee52bfb462 Add minitar test files; Remove creator and item modules; 2023-04-03 18:29:11 -04:00
Nathan Fisher 1e7d73bc28 Split from hpk crate 2023-04-02 18:51:12 -04:00
27 changed files with 1959 additions and 50 deletions

24
Cargo.lock generated
View File

@ -543,34 +543,22 @@ checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
[[package]]
name = "hpk"
version = "0.1.0"
dependencies = [
"clap",
"hpk-package",
"indicatif",
"package-bootstrap",
"rayon",
"ron",
"serde",
"ureq",
"url",
"walkdir",
"zstd",
]
[[package]]
name = "hpk-package"
version = "0.1.0"
source = "git+https://git.hitchhiker-linux.org/jeang3nie/hpk-package.git#e6f392bd7a91ffe3f0b080627e915ea458eade5a"
dependencies = [
"chrono",
"clap",
"deku",
"indicatif",
"libc",
"package-bootstrap",
"rayon",
"ron",
"serde",
"sha2",
"thiserror",
"ureq",
"url",
"walkdir",
"zstd",
]
[[package]]

View File

@ -21,12 +21,19 @@ path = "src/bootstrap.rs"
required-features = ["bootstrap"]
[dependencies]
hpk-package = { git = "https://git.hitchhiker-linux.org/jeang3nie/hpk-package.git" }
deku = "0.16"
libc = "0.2"
rayon = "1.7"
ron = "0.8"
sha2 = "0.10"
thiserror = "1.0"
walkdir = "2.3"
zstd = "0.12"
[dependencies.chrono]
version = "0.4"
features = ["serde"]
[dependencies.clap]
version = "4.2"
optional = true

View File

@ -1,6 +1,6 @@
use {
crate::{Item, ItemError, Package, Plist, Specs},
hpk_package::{deku::DekuError, tar, Entry},
crate::{tar, Entry, Item, ItemError, Package, Plist, Specs},
deku::DekuError,
rayon::prelude::{IntoParallelRefIterator, ParallelIterator},
std::{
borrow::BorrowMut,

View File

@ -1,7 +1,7 @@
use {
crate::{Arch, Package, Repository, Version},
hpk_package::ron::{self, ser::PrettyConfig},
rayon::prelude::*,
ron::{self, ser::PrettyConfig},
serde::{Deserialize, Serialize},
std::{
collections::HashMap,

View File

@ -1,7 +1,5 @@
use hpk_package::{Group, User};
use {
crate::InstallError,
crate::{Group, InstallError, User},
std::{
path::PathBuf,
process::{Command, Output},

View File

@ -1,4 +1,5 @@
#![warn(clippy::all, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
mod cli;
use {
@ -135,10 +136,11 @@ fn install_local<P: AsRef<OsStr> + fmt::Display>(
}
}
});
installer.install(sender)?;
let mut hooks = vec![];
installer.install(&mut hooks, sender)?;
match handle.join() {
Ok(hooks) => {
println!("hooks: {hooks:?}");
Ok(package) => {
println!("hooks: {package:?}");
Ok(())
}
Err(_) => Err(io::Error::new(ErrorKind::Other, "Unknown thread error").into()),

View File

@ -2,13 +2,12 @@
pub use error::InstallError;
use {
crate::{Hooks, Pinstall},
hpk_package::{
sha2::{Digest, Sha256},
crate::{
tar::{Archive, Node},
Entry, Group, Package, User,
Entry, Group, Hooks, Package, Pinstall, User,
},
rayon::prelude::{IntoParallelRefIterator, ParallelIterator},
sha2::{Digest, Sha256},
std::{
ffi::OsStr,
fmt::Write as _,
@ -261,8 +260,7 @@ fn pop_pinstall(
mod error {
use super::InstallMessage;
use crate::Hooks;
use hpk_package::tar;
use crate::{tar, Hooks};
use ron::error::SpannedError;
use std::{
error::Error,
@ -348,8 +346,8 @@ mod error {
}
}
impl From<hpk_package::tar::Error> for InstallError {
fn from(value: hpk_package::tar::Error) -> Self {
impl From<crate::tar::Error> for InstallError {
fn from(value: crate::tar::Error) -> Self {
Self::Tar(value)
}
}

View File

@ -1,17 +1,19 @@
use hpk_package::{
use {
crate::{
tar::{Error as TarError, Node, Owner},
Entry,
},
deku::DekuError,
sha2::{Digest, Sha256},
tar::{Error as TarError, Node, Owner},
Entry,
};
use std::{
error::Error,
ffi::OsStr,
fmt::{self, Write},
fs,
io::{self, Read},
os::unix::fs::MetadataExt,
path::{Path, PathBuf},
std::{
error::Error,
ffi::OsStr,
fmt::{self, Write},
fs,
io::{self, Read},
os::unix::fs::MetadataExt,
path::{Path, PathBuf},
},
};
#[derive(Clone, Debug)]

View File

@ -1,10 +1,15 @@
#![warn(clippy::all, clippy::pedantic)]
#![allow(clippy::must_use_candidate, clippy::missing_errors_doc)]
mod creator;
mod db;
mod hooks;
mod installer;
mod item;
mod package;
mod plist;
mod repository;
pub mod tar;
mod version;
use std::path::PathBuf;
@ -12,10 +17,12 @@ pub use {
creator::{CreationError, Creator, Message},
db::Database,
hooks::{Hooks, Pinstall},
hpk_package::{tar, Arch, Dependency, GitRev, Package, Plist, Rapid, SemVer, Specs, Version},
installer::{InstallError, InstallMessage, Installer},
item::{Item, ItemError},
package::{Arch, Dependency, Group, Package, Specs, User},
plist::{Entry, Plist},
repository::Repository,
version::{GitRev, Rapid, SemVer, Version},
};
const DB: [&str; 4] = ["var", "db", "hpk", "db.ron.zstd"];

79
src/package/arch.rs Normal file
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),
}
}
}

35
src/package/dependency.rs Normal file
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
}
}
}

129
src/package/mod.rs Normal file
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))
}
}

27
src/package/specs.rs Normal file
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>,
}

91
src/plist/mod.rs Normal file
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

@ -1,6 +1,5 @@
use {
crate::Package,
hpk_package::ron,
rayon::prelude::*,
serde::{Deserialize, Serialize},
std::{

8
src/tar/README.md Normal file
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.

22
src/tar/error.rs Normal file
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),
}

482
src/tar/header.rs Normal file
View File

@ -0,0 +1,482 @@
use crate::tar::Error;
use deku::prelude::*;
use std::{
env,
ffi::{CStr, OsStr},
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_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::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::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::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 {
break;
} else {
write!(s, "{}", char::from(c)).ok()?;
}
}
if s.is_empty() {
None
} else {
Some(s)
}
}
/// Gets the full file path to this archive member.
///
/// # Example
///
/// ```
/// use hpk::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)
}
#[allow(clippy::missing_panics_doc)]
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(OsStr::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)
}
#[allow(clippy::missing_panics_doc)]
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(OsStr::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::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::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::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()
}

199
src/tar/mod.rs Normal file
View File

@ -0,0 +1,199 @@
use std::{
fs::File,
io::{self, BufReader, Write},
path::PathBuf,
};
use rayon::prelude::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
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::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::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::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::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::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::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::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
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 `Node` 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 `Node`.
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)?)
}

101
src/version/gitrev.rs Normal file
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);
}
}

177
src/version/mod.rs Normal file
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);
}
}

214
src/version/rapid.rs Normal file
View File

@ -0,0 +1,214 @@
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);
}
}

205
src/version/semver.rs Normal file
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
test/1.tar Normal file

Binary file not shown.

1
test/1.txt Normal file
View File

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

BIN
test/2.tar Normal file

Binary file not shown.