Split from hpk crate

This commit is contained in:
Nathan Fisher 2023-04-02 18:51:12 -04:00
commit 1e7d73bc28
19 changed files with 2069 additions and 0 deletions

3
.gitignore vendored Normal file
View File

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

30
Cargo.toml Normal file
View File

@ -0,0 +1,30 @@
[package]
name = "hpk-package"
version = "0.1.0"
edition = "2021"
license = "GPL-3.0-only"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
deku = "0.16"
rayon = "1.7"
ron = "0.8"
sha2 = "0.10"
walkdir = "2.3"
zstd = "0.12"
thiserror = "1.0"
libc = "0.2"
[dependencies.chrono]
version = "0.4"
features = ["serde"]
[dependencies.serde]
version = "1.0"
features = ["derive"]
[profile.release]
codegen-units = 1
lto = true
strip = true

143
src/creator/mod.rs Normal file
View File

@ -0,0 +1,143 @@
use {
crate::{Entry, Item, Package, Plist, Specs},
rayon::prelude::{IntoParallelRefIterator, ParallelIterator},
std::{
borrow::BorrowMut,
env,
error::Error,
fs::{self, File},
io::{self, Write},
path::{Path, PathBuf},
sync::{
atomic::{AtomicUsize, Ordering},
mpsc::Sender,
Mutex,
},
},
walkdir::WalkDir,
zstd::Encoder,
};
pub enum Message {
MemberAdded(String),
Success(String),
Failure(String),
}
pub struct Creator {
path: PathBuf,
entries: Vec<PathBuf>,
specs: Specs,
}
impl Creator {
pub fn new(path: &Path, specs: Specs) -> Result<Self, io::Error> {
let d = env::current_dir()?;
env::set_current_dir(path)?;
let path = path.to_path_buf();
let entries = WalkDir::new(".")
.into_iter()
.filter(Result::is_ok)
.map(|x| x.unwrap().path().to_path_buf())
.collect::<Vec<_>>();
env::set_current_dir(d)?;
Ok(Self {
path,
entries,
specs,
})
}
pub fn from_list(list: &[&str], specs: Specs) -> Result<Self, io::Error> {
let entries = list.iter().map(|x| Path::new(x).to_path_buf()).collect();
Ok(Self {
path: env::current_dir().unwrap_or(Path::new("/").to_path_buf()),
entries,
specs,
})
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn create(self, outdir: &Path, sender: Sender<Message>) -> Result<(), Box<dyn Error>> {
let d = env::current_dir()?;
let plist = Mutex::new(Plist::default());
let totalsize: AtomicUsize = 0.into();
let fullname = format!(
"{}-{}_{}",
&self.specs.name, self.specs.version, self.specs.release
);
if !outdir.exists() {
fs::create_dir_all(outdir)?;
}
let outdir = outdir.canonicalize()?;
let mut archive = outdir.clone();
archive.push(&fullname);
archive.set_extension("tar.zst");
let fd = File::create(&archive)?;
let writer = Mutex::new(Encoder::new(fd, 0)?);
let sender = Mutex::new(sender);
env::set_current_dir(&self.path)?;
self.entries
.par_iter()
.filter(|x| x.as_path() != Path::new("."))
.for_each(|x| {
let sender = sender.lock().unwrap().clone();
if let Ok(item) = Item::try_create(x.as_path()) {
if let Entry::File {
path: _,
sha256sum: _,
mode: _,
size,
} = &item.entry
{
totalsize.fetch_add(*size, Ordering::Release);
}
let path = match item.entry.clone() {
Entry::File {
path,
sha256sum: _,
mode: _,
size: _,
}
| Entry::Link { path, target: _ }
| Entry::Directory { path, mode: _ } => path,
};
plist.lock().unwrap().borrow_mut().entries.push(item.entry);
match writer.lock().unwrap().borrow_mut().write_all(&item.data) {
Ok(_) => sender
.send(Message::MemberAdded(format!("{}", path.display())))
.expect("couldn't send message"),
Err(e) => sender
.send(Message::Failure(format!("{e}")))
.expect("couldn't send message"),
}
} else {
sender
.send(Message::Failure(format!(
"Could not process DirEntry for {}",
x.display()
)))
.expect("could not send message");
}
});
let mut package: Package = self.specs.into();
package.size = totalsize.into_inner();
let plist = plist.into_inner()?;
package.plist = plist;
let node = package.save_ron_and_create_tar_node(&outdir)?;
let mut writer = writer.into_inner()?;
writer.write_all(&node.to_vec()?)?;
let _fd = writer.finish()?;
let sender = sender.into_inner()?;
sender.send(Message::Success(format!("{} saved", archive.display())))?;
env::set_current_dir(d)?;
Ok(())
}
}

91
src/item/mod.rs Normal file
View File

@ -0,0 +1,91 @@
use crate::Entry;
use sha2::{Digest, Sha256};
use std::{
error::Error,
ffi::OsStr,
fmt::Write,
fs,
io::Read,
os::unix::fs::MetadataExt,
path::{Path, PathBuf},
};
use crate::tar::{Node, Owner};
#[derive(Clone, Debug)]
pub struct Item {
pub entry: Entry,
pub data: Vec<u8>,
}
impl Item {
pub fn try_create(path: &Path) -> Result<Self, Box<dyn Error>> {
let path = fix_path(path);
let meta = fs::metadata(&path)?;
let filename = format!("{}", path.display());
let owner = Some(Owner::default());
if meta.is_file() {
let mut data = vec![];
let mut fd = fs::File::open(path)?;
let path = PathBuf::from(&filename);
let size = fd.read_to_end(&mut data)?;
let mut sha256sum = String::new();
let mut hasher = Sha256::new();
hasher.update(&data);
let res = hasher.finalize();
for c in res {
write!(sha256sum, "{c:02x}")?;
}
let mode = meta.mode();
Ok(Self {
entry: Entry::File {
path,
sha256sum,
mode,
size,
},
data: Node::read_data_to_tar(&data, &filename, &meta, owner)?.to_vec()?,
})
} else if meta.is_dir() {
let mode = meta.mode();
let path = PathBuf::from(&filename);
Ok(Self {
entry: Entry::Directory { path, mode },
data: Node::read_data_to_tar(&[0; 0], &filename, &meta, owner)?.to_vec()?,
})
} else if meta.is_symlink() {
let target = fs::read_link(path)?;
let path = PathBuf::from(&filename);
Ok(Self {
entry: Entry::Link { path, target },
data: Node::read_data_to_tar(&[0; 0], &filename, &meta, owner)?.to_vec()?,
})
} else {
unreachable!();
}
}
}
fn fix_path(path: &Path) -> PathBuf {
let path = if let Ok(p) = path.strip_prefix("./") {
p
} else {
path
};
if path.is_file() || path.is_symlink() {
match path.ancestors().last().and_then(Path::to_str) {
Some("etc" | "var") => {
let ext = if let Some(x) = path.extension().and_then(OsStr::to_str) {
format!("{x}.new")
} else {
"new".to_string()
};
let mut path = path.to_path_buf();
path.set_extension(ext);
path
}
_ => path.to_path_buf(),
}
} else {
path.to_path_buf()
}
}

14
src/lib.rs Normal file
View File

@ -0,0 +1,14 @@
pub mod tar;
mod creator;
mod item;
mod package;
mod plist;
mod version;
pub use {
creator::{Creator, Message},
item::Item,
package::{Dependency, Package, Specs},
plist::*,
version::*,
};

28
src/package/dependency.rs Normal file
View File

@ -0,0 +1,28 @@
use {
super::Package,
crate::Version,
serde::{Deserialize, Serialize},
};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Dependency {
pub name: String,
pub version: (Option<Version>, Option<Version>),
}
impl Dependency {
#[allow(clippy::must_use_candidate)]
pub fn satisfied(&self, package: &Package) -> bool {
if self.name.as_str() == package.name.as_str() {
match &self.version {
(Some(low), Some(high)) => &package.version >= low && &package.version < high,
(Some(low), None) => &package.version >= low,
(None, Some(high)) => &package.version < high,
// no version requirements
_ => true,
}
} else {
false
}
}
}

122
src/package/mod.rs Normal file
View File

@ -0,0 +1,122 @@
mod dependency;
mod specs;
use {
crate::{Plist, Version},
ron::ser::{to_string_pretty, PrettyConfig},
serde::{Deserialize, Serialize},
std::{
error::Error,
fs,
fs::File,
io::{BufWriter, Write},
path::Path,
},
crate::tar::{Node, Owner},
};
pub use {dependency::Dependency, specs::Specs};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct User {
pub name: String,
pub uid: Option<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 release number for this package
pub release: u8,
/// a single line description of the package
pub description: String,
/// a more verbose description of the package
pub long_description: String,
/// an optional link to an
/// [AppStream](https://www.freedesktop.org/wiki/Distributions/AppStream/)
/// metadata file
pub appstream_data: Option<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,
appstream_data: value.appstream_data,
dependencies: value.dependencies,
users: value.users,
groups: value.groups,
post_install: value.post_install,
..Default::default()
}
}
}
impl Package {
fn as_ron(&self) -> Result<String, ron::Error> {
let cfg = PrettyConfig::new().struct_names(true);
to_string_pretty(self, cfg)
}
pub(crate) fn save_ron_and_create_tar_node(
&self,
outdir: &Path,
) -> Result<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)
}
/// Tests whether this package is an update for another
pub fn is_upgrade(&self, other: &Self) -> bool {
self.name == other.name
&& (self.version > other.version
|| (self.version == other.version && self.release > other.release))
}
}

31
src/package/specs.rs Normal file
View File

@ -0,0 +1,31 @@
use {
super::{Group, User},
crate::{Dependency, Version},
serde::{Deserialize, Serialize},
};
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct Specs {
/// The name of the package minus all version information
pub name: String,
/// The `Version` of the package
pub version: Version,
/// The release number for this package
pub release: u8,
/// a single line description of the package
pub description: String,
/// a more verbose description of the package
pub long_description: String,
/// an optional link to an
/// [AppStream](https://www.freedesktop.org/wiki/Distributions/AppStream/)
/// metadata file
pub appstream_data: Option<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!();
}
}
}

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

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

@ -0,0 +1,441 @@
use crate::tar::Error;
use deku::prelude::*;
use std::{
env,
ffi::CStr,
fmt::{self, Write},
fs::{self, Metadata},
io,
ops::Deref,
os::{linux::fs::MetadataExt, unix::fs::FileTypeExt},
path::PathBuf,
};
#[repr(u8)]
pub enum FileType {
Normal = 0x30,
Hardlink = 0x31,
Symlink = 0x32,
Char = 0x33,
Block = 0x34,
Dir = 0x35,
FIFO = 0x36,
Unknown = 0x00,
}
impl<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 {
pub fn filename(&self) -> Result<String, fmt::Error> {
let mut s = String::new();
for c in self.fname {
if c != b'\0' {
write!(s, "{c}")?;
} else {
break;
}
}
Ok(s)
}
pub fn mode(&self) -> Result<u32, Error> {
let mut s = String::new();
for c in self.mode {
if c != b'\0' {
write!(s, "{c}")?;
} else {
break;
}
}
let mode = u32::from_str_radix(&s, 8)?;
Ok(mode)
}
fn uid(&self) -> Result<u32, Error> {
let mut s = String::new();
for c in self.mode {
if c != b'\0' {
write!(s, "{c}")?;
} else {
break;
}
}
let uid = u32::from_str_radix(&s, 8)?;
Ok(uid)
}
fn gid(&self) -> Result<u32, Error> {
let mut s = String::new();
for c in self.mode {
if c != b'\0' {
write!(s, "{c}")?;
} else {
break;
}
}
let gid = u32::from_str_radix(&s, 8)?;
Ok(gid)
}
fn username(&self) -> Result<String, fmt::Error> {
let mut s = String::new();
for c in self.username {
if c != b'\0' {
write!(s, "{c}")?;
} else {
break;
}
}
Ok(s)
}
fn groupname(&self) -> Result<String, fmt::Error> {
let mut s = String::new();
for c in self.groupname {
if c != b'\0' {
write!(s, "{c}")?;
} else {
break;
}
}
Ok(s)
}
pub fn owner(&self) -> Result<Owner, Error> {
let uid = self.uid()?;
let gid = self.gid()?;
let username = self.username()?;
let groupname = self.groupname()?;
Ok(Owner {
uid,
gid,
username,
groupname,
})
}
pub fn prefix(&self) -> Result<String, fmt::Error> {
let mut s = String::new();
for c in self.file_prefix {
if c != b'\0' {
write!(s, "{c}")?;
} else {
break;
}
}
Ok(s)
}
pub fn new(filename: &str) -> Result<Self, Error> {
let mut header = Header::default();
let meta = fs::symlink_metadata(filename)?;
let (filename, prefix) = if filename.len() < 100 {
(filename.to_string(), None)
} else {
// Deal with file names longer than 100 bytes
let path = PathBuf::from(&filename);
let name = match path.file_name().and_then(|n| n.to_str()) {
Some(n) => n.to_string(),
None => {
return Err(Error::Io(io::Error::new(
io::ErrorKind::Other,
"Cannot get file name",
)))
}
};
let dir = match path.parent() {
Some(d) => d,
None => {
return Err(Error::Io(io::Error::new(
io::ErrorKind::Other,
"Cannot get path prefix",
)))
}
};
(name, Some(format!("{}", dir.display())))
};
/* Fill in metadata */
header.fname[..filename.len()].copy_from_slice(filename.as_bytes());
let mode = format!("{:07o}", (meta.st_mode() & 0o777));
header.mode[..mode.len()].copy_from_slice(mode.as_bytes());
let user = format!("{:07o}", meta.st_uid());
header.uid[..user.len()].copy_from_slice(user.as_bytes());
let group = format!("{:07o}", meta.st_gid());
header.gid[..group.len()].copy_from_slice(group.as_bytes());
let size = format!("{:011o}", meta.st_size());
header.size[..size.len()].copy_from_slice(size.as_bytes());
let mtime = format!("{:011o}", meta.st_mtime());
header.mtime[..mtime.len()].copy_from_slice(mtime.as_bytes());
if let Some(prefix) = prefix {
header.file_prefix[..prefix.len()].copy_from_slice(prefix.as_bytes());
}
/* Get the file type and conditional metadata */
header.link_indicator[0] = FileType::from(&meta) as u8;
if header.link_indicator[0] == FileType::Symlink as u8 {
let link = fs::read_link(filename)?.to_str().unwrap().to_string();
header.link_name[..link.len()].copy_from_slice(link.as_bytes());
} else if header.link_indicator[0] == FileType::Block as u8 {
let major = format!("{:07o}", meta.st_dev());
header.device_major[..major.len()].copy_from_slice(major.as_bytes());
let minor = format!("{:07o}", meta.st_rdev());
header.device_minor[..minor.len()].copy_from_slice(minor.as_bytes());
}
/* TODO: Find better way to get username */
let key = "USER";
if let Ok(val) = env::var(key) {
header.username[..val.len()].copy_from_slice(val.as_bytes())
}
/* TODO: Find way to get groupname */
/* Update the header checksum value */
header.update_checksum()?;
Ok(header)
}
pub fn new_from_meta(
filename: &str,
meta: &Metadata,
owner: Option<Owner>,
) -> Result<Self, Error> {
let mut header = Header::default();
let (filename, prefix) = if filename.len() < 100 {
(filename.to_string(), None)
} else {
// Deal with file names longer than 100 bytes
let path = PathBuf::from(&filename);
let name = match path.file_name().and_then(|n| n.to_str()) {
Some(n) => n.to_string(),
None => {
return Err(Error::Io(io::Error::new(
io::ErrorKind::Other,
"Cannot get file name",
)))
}
};
let dir = match path.parent() {
Some(d) => d,
None => {
return Err(Error::Io(io::Error::new(
io::ErrorKind::Other,
"Cannot get path prefix",
)))
}
};
(name, Some(format!("{}", dir.display())))
};
header.fname[..filename.len()].copy_from_slice(filename.as_bytes());
let mode = format!("{:07o}", meta.st_mode());
header.mode[..mode.len()].copy_from_slice(mode.as_bytes());
let owner = match owner {
Some(o) => o,
None => Owner {
uid: meta.st_uid(),
gid: meta.st_gid(),
username: get_username_for_uid(meta.st_uid())?.into(),
groupname: get_groupname_for_gid(meta.st_gid())?.into(),
},
};
let uid = format!("{:07o}", owner.uid);
header.uid[..uid.len()].copy_from_slice(uid.as_bytes());
let gid = format!("{:07o}", owner.gid);
header.gid[..gid.len()].copy_from_slice(gid.as_bytes());
let size = format!("{:011o}", meta.len());
header.size[..size.len()].copy_from_slice(size.as_bytes());
let mtime = format!("{:011o}", meta.st_mtime());
header.mtime[..mtime.len()].copy_from_slice(mtime.as_bytes());
if let Some(prefix) = prefix {
header.file_prefix[..prefix.len()].copy_from_slice(prefix.as_bytes());
}
header.link_indicator[0] = FileType::from(meta) as u8;
if header.link_indicator[0] == FileType::Symlink as u8 {
let link = fs::read_link(filename)?.to_str().unwrap().to_string();
header.link_name[..link.len()].copy_from_slice(link.as_bytes());
} else if header.link_indicator[0] == FileType::Block as u8 {
let major = format!("{:07o}", meta.st_dev());
header.device_major[..major.len()].copy_from_slice(major.as_bytes());
let minor = format!("{:07o}", meta.st_rdev());
header.device_minor[..minor.len()].copy_from_slice(minor.as_bytes());
}
header.username[..owner.username.len()].copy_from_slice(owner.username.as_bytes());
header.groupname[..owner.groupname.len()].copy_from_slice(owner.groupname.as_bytes());
header.update_checksum()?;
Ok(header)
}
/// Validates that the magic value received matches the magic value required in the Tar specification.
///
/// # Example
///
/// ```
/// use hpk_package::tar::Header;
/// let header = Header::default();
/// if !header.validate_magic() {
/// println!("Magic value is invalid");
/// }
/// ```
pub fn validate_magic(self) -> bool {
self.ustar_magic == "ustar ".as_bytes()
}
/// Validates the header checksum computes to the expected value.
///
/// # Example
///
/// ```
/// use hpk_package::tar::Header;
/// let header = Header::default();
/// if header.validate_checksum().unwrap() {
/// println!("Checksum is valid");
/// }
/// ```
pub fn validate_checksum(self) -> Result<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()
}
pub fn get_groupname_for_gid<'a>(gid: u32) -> Result<&'a str, std::str::Utf8Error> {
let group = unsafe {
let gr = libc::getgrgid(gid);
let name = (*gr).gr_name;
CStr::from_ptr(name)
};
group.to_str()
}

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

@ -0,0 +1,134 @@
use std::{
fs::File,
io::{self, BufReader, Write},
};
mod error;
mod header;
mod node;
pub use {
error::Error,
header::{FileType, Header, Owner},
node::Node,
};
#[derive(Default)]
pub struct Archive {
pub nodes: Vec<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".to_string()).unwrap();
/// ```
pub fn open(filename: String) -> Result<Self, Error> {
let file = File::open(&filename)?;
let mut reader = BufReader::new(file);
let mut out = Self {
nodes: Vec::<Node>::new(),
};
while let Ok(t) = Node::read(&mut reader) {
out.nodes.push(t);
}
Ok(out)
}
/// Remove the first file from the Tar that matches the filename and path.
///
/// # Example
///
/// ```
/// use hpk_package::tar::Archive;
///
/// let mut data = Archive::new("test/1.tar").unwrap();
/// data.remove("test/1.tar".to_string()).unwrap();
/// ```
pub fn remove(&mut self, filename: String) -> Result<bool, Error> {
let mut name = [0u8; 100];
name[..filename.len()].copy_from_slice(filename.as_bytes());
if let Some(i) = &self.nodes.iter().position(|x| x.header.fname == name) {
self.nodes.remove(*i);
return Ok(true);
}
Ok(false)
}
}

137
src/tar/node.rs Normal file
View File

@ -0,0 +1,137 @@
use crate::tar::{header::Owner, Error, FileType, Header};
use deku::prelude::*;
use std::{
fs::{File, Metadata},
io::{self, BufReader},
str,
};
#[derive(Clone, Debug, Default)]
pub struct Node {
pub header: Header,
pub data: Vec<[u8; 512]>,
}
impl Node {
pub fn to_vec(self) -> Result<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)?)
}

79
src/version/gitrev.rs Normal file
View File

@ -0,0 +1,79 @@
use {
crate::Version,
chrono::{offset::Utc, DateTime},
serde::{Deserialize, Serialize},
std::{cmp::Ordering, error::Error, fmt, str::FromStr},
};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct GitRev {
/// the short revision hash
pub hash: String,
/// the time of the revision commit
pub datetime: DateTime<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),
}
}
}

130
src/version/mod.rs Normal file
View File

@ -0,0 +1,130 @@
use {
serde::{Deserialize, Serialize},
std::{error::Error, fmt, str::FromStr},
};
mod gitrev;
mod rapid;
mod semver;
pub use {gitrev::GitRev, rapid::Rapid, semver::SemVer};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum Version {
Number(u32),
Rapid(Rapid),
SemVer(SemVer),
Git(GitRev),
}
impl Default for Version {
fn default() -> Self {
Self::SemVer(SemVer {
major: 0,
minor: 1,
patch: 0,
})
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Number(n) => write!(f, "{n}"),
Self::SemVer(s) => write!(f, "{s}"),
Self::Rapid(r) => write!(f, "{r}"),
Self::Git(g) => write!(f, "{g}"),
}
}
}
impl From<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,
}
}
}
#[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);
}
}

206
src/version/mod.rs.bak Normal file
View File

@ -0,0 +1,206 @@
use {
chrono::{offset::Utc, DateTime},
serde::{Deserialize, Serialize},
std::{error::Error, fmt, str::FromStr},
};
mod gitrev;
mod rapid;
mod semver;
pub use {gitrev::GitRev, rapid::Rapid, semver::SemVer};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum Version {
Number(u32),
Rapid {
major: u32,
minor: u32,
},
SemVer {
major: u32,
minor: u32,
patch: u32,
},
Git {
hash: String,
datetime: DateTime<Utc>,
},
}
impl Default for Version {
fn default() -> Self {
Self::SemVer {
major: 0,
minor: 1,
patch: 0,
}
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Number(n) => write!(f, "{n}"),
Self::SemVer {
major,
minor,
patch,
} => {
let v = SemVer {
major: *major,
minor: *minor,
patch: *patch,
};
write!(f, "{v}")
}
Self::Rapid { major, minor } => {
let v = Rapid {
major: *major,
minor: *minor,
};
write!(f, "{v}")
}
Self::Git { hash, datetime } => {
let v = GitRev {
hash: hash.clone(),
datetime: *datetime,
};
write!(f, "{v}")
}
}
}
}
impl From<SemVer> for Version {
fn from(value: SemVer) -> Self {
Self::SemVer {
major: value.major,
minor: value.minor,
patch: value.patch,
}
}
}
impl From<Rapid> for Version {
fn from(value: Rapid) -> Self {
Self::Rapid {
major: value.major,
minor: value.minor,
}
}
}
impl From<GitRev> for Version {
fn from(value: GitRev) -> Self {
Self::Git {
hash: value.hash,
datetime: value.datetime,
}
}
}
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::SemVer {
major,
minor,
patch,
},
Self::SemVer {
major: a,
minor: b,
patch: c,
},
) => (major, minor, patch).partial_cmp(&(a, b, c)),
(Self::Rapid { major, minor }, Self::Rapid { major: a, minor: b }) => {
(major, minor).partial_cmp(&(a, b))
}
(
Self::Git {
hash: _a,
datetime: b,
},
Self::Git {
hash: _c,
datetime: d,
},
) => b.partial_cmp(&d),
(
Self::SemVer {
major,
minor,
patch,
},
Self::Rapid { major: a, minor: b },
) => SemVer {
major: *major,
minor: *minor,
patch: *patch,
}
.partial_cmp(&Rapid {
major: *a,
minor: *b,
}),
_ => None,
}
}
}
#[derive(Debug)]
pub struct ParseVersionError;
impl fmt::Display for ParseVersionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "error parsing version")
}
}
impl Error for ParseVersionError {}
impl FromStr for Version {
type Err = ParseVersionError;
fn from_str(s: &str) -> Result<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);
}
}

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

@ -0,0 +1,168 @@
use crate::SemVer;
use {
crate::Version,
serde::{Deserialize, Serialize},
std::{cmp::Ordering, error::Error, fmt, num::ParseIntError, str::FromStr},
};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Rapid {
pub major: u32,
pub minor: u32,
}
impl fmt::Display for Rapid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}", self.major, self.minor)
}
}
impl PartialOrd for Rapid {
fn partial_cmp(&self, other: &Self) -> Option<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,
})
);
}
}

191
src/version/semver.rs Normal file
View File

@ -0,0 +1,191 @@
use {
crate::{Rapid, Version},
serde::{Deserialize, Serialize},
std::{cmp::Ordering, error::Error, fmt, num::ParseIntError, str::FromStr},
};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct SemVer {
pub major: u32,
pub minor: u32,
pub patch: u32,
}
impl fmt::Display for SemVer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
impl PartialOrd for SemVer {
fn partial_cmp(&self, other: &Self) -> Option<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
})
);
}
}