Split from hpk crate
This commit is contained in:
commit
1e7d73bc28
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/target
|
||||||
|
/Cargo.lock
|
||||||
|
test
|
30
Cargo.toml
Normal file
30
Cargo.toml
Normal 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
143
src/creator/mod.rs
Normal 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
91
src/item/mod.rs
Normal 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
14
src/lib.rs
Normal 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
28
src/package/dependency.rs
Normal 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
122
src/package/mod.rs
Normal 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
31
src/package/specs.rs
Normal 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
91
src/plist/mod.rs
Normal 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
8
src/tar/README.md
Normal 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
22
src/tar/error.rs
Normal 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
441
src/tar/header.rs
Normal 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
134
src/tar/mod.rs
Normal 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
137
src/tar/node.rs
Normal 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
79
src/version/gitrev.rs
Normal 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
130
src/version/mod.rs
Normal 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
206
src/version/mod.rs.bak
Normal 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
168
src/version/rapid.rs
Normal 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
191
src/version/semver.rs
Normal 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
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user