Compare commits
No commits in common. "6037836f6b4485435affae1cbb97a62df5cc524a" and "13a97ff1d8f6b4abed95f21c138783a9426c4f6e" have entirely different histories.
6037836f6b
...
13a97ff1d8
24
Cargo.lock
generated
24
Cargo.lock
generated
@ -544,21 +544,33 @@ checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
|
||||
name = "hpk"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"deku",
|
||||
"hpk-package",
|
||||
"indicatif",
|
||||
"libc",
|
||||
"package-bootstrap",
|
||||
"rayon",
|
||||
"ron",
|
||||
"serde",
|
||||
"ureq",
|
||||
"url",
|
||||
"walkdir",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hpk-package"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.hitchhiker-linux.org/jeang3nie/hpk-package.git#e6f392bd7a91ffe3f0b080627e915ea458eade5a"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"deku",
|
||||
"libc",
|
||||
"rayon",
|
||||
"ron",
|
||||
"serde",
|
||||
"sha2",
|
||||
"thiserror",
|
||||
"ureq",
|
||||
"url",
|
||||
"walkdir",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -21,19 +21,12 @@ path = "src/bootstrap.rs"
|
||||
required-features = ["bootstrap"]
|
||||
|
||||
[dependencies]
|
||||
deku = "0.16"
|
||||
libc = "0.2"
|
||||
hpk-package = { git = "https://git.hitchhiker-linux.org/jeang3nie/hpk-package.git" }
|
||||
rayon = "1.7"
|
||||
ron = "0.8"
|
||||
sha2 = "0.10"
|
||||
thiserror = "1.0"
|
||||
walkdir = "2.3"
|
||||
zstd = "0.12"
|
||||
|
||||
[dependencies.chrono]
|
||||
version = "0.4"
|
||||
features = ["serde"]
|
||||
|
||||
[dependencies.clap]
|
||||
version = "4.2"
|
||||
optional = true
|
||||
|
@ -1,6 +1,6 @@
|
||||
use {
|
||||
crate::{tar, Entry, Item, ItemError, Package, Plist, Specs},
|
||||
deku::DekuError,
|
||||
crate::{Item, ItemError, Package, Plist, Specs},
|
||||
hpk_package::{deku::DekuError, tar, Entry},
|
||||
rayon::prelude::{IntoParallelRefIterator, ParallelIterator},
|
||||
std::{
|
||||
borrow::BorrowMut,
|
||||
|
@ -1,7 +1,7 @@
|
||||
use {
|
||||
crate::{Arch, Package, Repository, Version},
|
||||
hpk_package::ron::{self, ser::PrettyConfig},
|
||||
rayon::prelude::*,
|
||||
ron::{self, ser::PrettyConfig},
|
||||
serde::{Deserialize, Serialize},
|
||||
std::{
|
||||
collections::HashMap,
|
||||
|
@ -1,5 +1,7 @@
|
||||
use hpk_package::{Group, User};
|
||||
|
||||
use {
|
||||
crate::{Group, InstallError, User},
|
||||
crate::InstallError,
|
||||
std::{
|
||||
path::PathBuf,
|
||||
process::{Command, Output},
|
||||
|
@ -1,5 +1,4 @@
|
||||
#![warn(clippy::all, clippy::pedantic)]
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
mod cli;
|
||||
use {
|
||||
@ -136,11 +135,10 @@ fn install_local<P: AsRef<OsStr> + fmt::Display>(
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut hooks = vec![];
|
||||
installer.install(&mut hooks, sender)?;
|
||||
installer.install(sender)?;
|
||||
match handle.join() {
|
||||
Ok(package) => {
|
||||
println!("hooks: {package:?}");
|
||||
Ok(hooks) => {
|
||||
println!("hooks: {hooks:?}");
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => Err(io::Error::new(ErrorKind::Other, "Unknown thread error").into()),
|
||||
|
@ -2,12 +2,13 @@
|
||||
pub use error::InstallError;
|
||||
|
||||
use {
|
||||
crate::{
|
||||
crate::{Hooks, Pinstall},
|
||||
hpk_package::{
|
||||
sha2::{Digest, Sha256},
|
||||
tar::{Archive, Node},
|
||||
Entry, Group, Hooks, Package, Pinstall, User,
|
||||
Entry, Group, Package, User,
|
||||
},
|
||||
rayon::prelude::{IntoParallelRefIterator, ParallelIterator},
|
||||
sha2::{Digest, Sha256},
|
||||
std::{
|
||||
ffi::OsStr,
|
||||
fmt::Write as _,
|
||||
@ -260,7 +261,8 @@ fn pop_pinstall(
|
||||
|
||||
mod error {
|
||||
use super::InstallMessage;
|
||||
use crate::{tar, Hooks};
|
||||
use crate::Hooks;
|
||||
use hpk_package::tar;
|
||||
use ron::error::SpannedError;
|
||||
use std::{
|
||||
error::Error,
|
||||
@ -346,8 +348,8 @@ mod error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::tar::Error> for InstallError {
|
||||
fn from(value: crate::tar::Error) -> Self {
|
||||
impl From<hpk_package::tar::Error> for InstallError {
|
||||
fn from(value: hpk_package::tar::Error) -> Self {
|
||||
Self::Tar(value)
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,17 @@
|
||||
use {
|
||||
crate::{
|
||||
tar::{Error as TarError, Node, Owner},
|
||||
Entry,
|
||||
},
|
||||
use hpk_package::{
|
||||
deku::DekuError,
|
||||
sha2::{Digest, Sha256},
|
||||
std::{
|
||||
error::Error,
|
||||
ffi::OsStr,
|
||||
fmt::{self, Write},
|
||||
fs,
|
||||
io::{self, Read},
|
||||
os::unix::fs::MetadataExt,
|
||||
path::{Path, PathBuf},
|
||||
},
|
||||
tar::{Error as TarError, Node, Owner},
|
||||
Entry,
|
||||
};
|
||||
use std::{
|
||||
error::Error,
|
||||
ffi::OsStr,
|
||||
fmt::{self, Write},
|
||||
fs,
|
||||
io::{self, Read},
|
||||
os::unix::fs::MetadataExt,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -1,15 +1,10 @@
|
||||
#![warn(clippy::all, clippy::pedantic)]
|
||||
#![allow(clippy::must_use_candidate, clippy::missing_errors_doc)]
|
||||
mod creator;
|
||||
mod db;
|
||||
mod hooks;
|
||||
mod installer;
|
||||
mod item;
|
||||
mod package;
|
||||
mod plist;
|
||||
mod repository;
|
||||
pub mod tar;
|
||||
mod version;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
@ -17,12 +12,10 @@ pub use {
|
||||
creator::{CreationError, Creator, Message},
|
||||
db::Database,
|
||||
hooks::{Hooks, Pinstall},
|
||||
hpk_package::{tar, Arch, Dependency, GitRev, Package, Plist, Rapid, SemVer, Specs, Version},
|
||||
installer::{InstallError, InstallMessage, Installer},
|
||||
item::{Item, ItemError},
|
||||
package::{Arch, Dependency, Group, Package, Specs, User},
|
||||
plist::{Entry, Plist},
|
||||
repository::Repository,
|
||||
version::{GitRev, Rapid, SemVer, Version},
|
||||
};
|
||||
|
||||
const DB: [&str; 4] = ["var", "db", "hpk", "db.ron.zstd"];
|
||||
|
@ -1,79 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{error::Error, fmt, str::FromStr};
|
||||
|
||||
#[cfg(target_arch = "arm")]
|
||||
pub const HOST_ARCH: Arch = Arch::armv7l;
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
pub const HOST_ARCH: Arch = Arch::aarch64;
|
||||
#[cfg(target_arch = "x86")]
|
||||
pub const HOST_ARCH: Arch = Arch::i486;
|
||||
#[cfg(target_arch = "riscv64")]
|
||||
pub const HOST_ARCH: Arch = Arch::riscv64;
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
pub const HOST_ARCH: Arch = Arch::x86_64;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum Arch {
|
||||
armv7l,
|
||||
aarch64,
|
||||
i486,
|
||||
i586,
|
||||
i686,
|
||||
riscv64,
|
||||
x86_64,
|
||||
any,
|
||||
}
|
||||
|
||||
impl Default for Arch {
|
||||
fn default() -> Self {
|
||||
HOST_ARCH
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Arch {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::armv7l => "armv7l",
|
||||
Self::aarch64 => "aarch64",
|
||||
Self::i486 => "i486",
|
||||
Self::i586 => "i586",
|
||||
Self::i686 => "i686",
|
||||
Self::riscv64 => "riscv64",
|
||||
Self::x86_64 => "x86_64",
|
||||
Self::any => "any",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParseArchError;
|
||||
|
||||
impl fmt::Display for ParseArchError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "error parsing architecture")
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ParseArchError {}
|
||||
|
||||
impl FromStr for Arch {
|
||||
type Err = ParseArchError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"i486" | "x86" => Ok(Self::i486),
|
||||
"i586" => Ok(Self::i586),
|
||||
"i686" => Ok(Self::i686),
|
||||
"armv7l" | "arm" => Ok(Self::armv7l),
|
||||
"arm64" | "aarch64" | "armv8" => Ok(Self::aarch64),
|
||||
"riscv" | "riscv64" => Ok(Self::riscv64),
|
||||
"any" => Ok(Self::any),
|
||||
_ => Err(ParseArchError),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
use {
|
||||
super::Package,
|
||||
crate::Version,
|
||||
serde::{Deserialize, Serialize},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
/// Specifies a dependency requirement
|
||||
pub struct Dependency {
|
||||
/// The name of the dependency. All packages must have a unique name.
|
||||
pub name: String,
|
||||
/// The version requirements for this dependency. If the low
|
||||
/// version is `Some`, then the version must be equal to or
|
||||
/// greater than this version. If the high version is `Some`,
|
||||
/// then the version must be less than this version.
|
||||
pub version: (Option<Version>, Option<Version>),
|
||||
}
|
||||
|
||||
impl Dependency {
|
||||
#[allow(clippy::must_use_candidate)]
|
||||
/// Checks whether a package satisfies a given dependency
|
||||
pub fn satisfied(&self, package: &Package) -> bool {
|
||||
if self.name.as_str() == package.name.as_str() {
|
||||
match &self.version {
|
||||
(Some(low), Some(high)) => &package.version >= low && &package.version < high,
|
||||
(Some(low), None) => &package.version >= low,
|
||||
(None, Some(high)) => &package.version < high,
|
||||
// no version requirements
|
||||
_ => true,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
@ -1,129 +0,0 @@
|
||||
mod arch;
|
||||
mod dependency;
|
||||
mod specs;
|
||||
|
||||
use {
|
||||
crate::tar::{Node, Owner},
|
||||
crate::{Plist, Version},
|
||||
ron::ser::{to_string_pretty, PrettyConfig},
|
||||
serde::{Deserialize, Serialize},
|
||||
std::{
|
||||
error::Error,
|
||||
fs,
|
||||
fs::File,
|
||||
io::{BufWriter, Write},
|
||||
path::Path,
|
||||
},
|
||||
};
|
||||
|
||||
pub use {arch::Arch, dependency::Dependency, specs::Specs};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct User {
|
||||
pub name: String,
|
||||
pub uid: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Group {
|
||||
pub name: String,
|
||||
pub gid: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
/// the metadata associated with a package
|
||||
pub struct Package {
|
||||
/// The name of the package minus all version information
|
||||
pub name: String,
|
||||
/// The `Version` of the package
|
||||
pub version: Version,
|
||||
/// The architecture the package is built for
|
||||
pub arch: Arch,
|
||||
/// The release number for this package
|
||||
pub release: u8,
|
||||
/// a single line description of the package
|
||||
pub description: String,
|
||||
/// a more verbose description of the package
|
||||
pub long_description: String,
|
||||
/// a listing of all files, directories and symlinks which are a part of
|
||||
/// this package
|
||||
pub plist: Plist,
|
||||
/// the total size of this package
|
||||
pub size: usize,
|
||||
/// all of this package's runtime dependencies
|
||||
pub dependencies: Vec<Dependency>,
|
||||
/// an optional list of users to be created upon installation
|
||||
pub users: Option<Vec<User>>,
|
||||
/// an optional list of groups to be created upon installation
|
||||
pub groups: Option<Vec<Group>>,
|
||||
/// an optional post installation shell script to be run
|
||||
pub post_install: Option<String>,
|
||||
}
|
||||
|
||||
impl From<Specs> for Package {
|
||||
fn from(value: Specs) -> Self {
|
||||
Package {
|
||||
name: value.name,
|
||||
version: value.version,
|
||||
release: value.release,
|
||||
description: value.description,
|
||||
long_description: value.long_description,
|
||||
dependencies: value.dependencies,
|
||||
users: value.users,
|
||||
groups: value.groups,
|
||||
post_install: value.post_install,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Package {
|
||||
fn as_ron(&self) -> Result<String, ron::Error> {
|
||||
let cfg = PrettyConfig::new().struct_names(true);
|
||||
to_string_pretty(self, cfg)
|
||||
}
|
||||
|
||||
pub fn save_ron_and_create_tar_node(&self, outdir: &Path) -> Result<Node, Box<dyn Error>> {
|
||||
if !outdir.exists() {
|
||||
fs::create_dir_all(outdir)?;
|
||||
}
|
||||
let mut outfile = outdir.to_path_buf();
|
||||
outfile.push("package.ron");
|
||||
let fd = File::create(&outfile)?;
|
||||
let s = self.as_ron()?;
|
||||
let mut writer = BufWriter::new(&fd);
|
||||
writer.write_all(s.as_bytes())?;
|
||||
writer.flush()?;
|
||||
let node = Node::read_data_to_tar(
|
||||
s.as_bytes(),
|
||||
"package.ron",
|
||||
&outfile.metadata()?,
|
||||
Some(Owner::default()),
|
||||
)?;
|
||||
Ok(node)
|
||||
}
|
||||
|
||||
/// Returns the formatted full package name including version and release strings
|
||||
pub fn fullname(&self) -> String {
|
||||
format!(
|
||||
"{}-{}_{}_{}",
|
||||
self.name, self.version, self.release, self.arch
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the name of the package archive
|
||||
pub fn archive_name(&self) -> String {
|
||||
format!(
|
||||
"{}-{}_{}_{}.tar.zstd",
|
||||
self.name, self.version, self.release, self.arch
|
||||
)
|
||||
}
|
||||
|
||||
/// Tests whether this package is an update for another
|
||||
pub fn is_upgrade(&self, other: &Self) -> bool {
|
||||
self.arch == other.arch
|
||||
&& self.name == other.name
|
||||
&& (self.version > other.version
|
||||
|| (self.version == other.version && self.release > other.release))
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
use {
|
||||
super::{Group, User},
|
||||
crate::{Dependency, Version},
|
||||
serde::{Deserialize, Serialize},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Specs {
|
||||
/// The name of the package minus all version information
|
||||
pub name: String,
|
||||
/// The `Version` of the package
|
||||
pub version: Version,
|
||||
/// The release number for this package
|
||||
pub release: u8,
|
||||
/// a single line description of the package
|
||||
pub description: String,
|
||||
/// a more verbose description of the package
|
||||
pub long_description: String,
|
||||
/// all of this package's runtime dependencies
|
||||
pub dependencies: Vec<Dependency>,
|
||||
/// an optional list of users to be created upon installation
|
||||
pub users: Option<Vec<User>>,
|
||||
/// an optional list of groups to be created upon installation
|
||||
pub groups: Option<Vec<Group>>,
|
||||
/// an optional post installation shell script to be run
|
||||
pub post_install: Option<String>,
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
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!();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
use {
|
||||
crate::Package,
|
||||
hpk_package::ron,
|
||||
rayon::prelude::*,
|
||||
serde::{Deserialize, Serialize},
|
||||
std::{
|
||||
|
@ -1,8 +0,0 @@
|
||||
## 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.
|
@ -1,22 +0,0 @@
|
||||
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),
|
||||
}
|
@ -1,482 +0,0 @@
|
||||
use crate::tar::Error;
|
||||
use deku::prelude::*;
|
||||
use std::{
|
||||
env,
|
||||
ffi::{CStr, OsStr},
|
||||
fmt::{self, Write},
|
||||
fs::{self, Metadata},
|
||||
io,
|
||||
ops::Deref,
|
||||
os::{linux::fs::MetadataExt, unix::fs::FileTypeExt},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
#[repr(u8)]
|
||||
pub enum FileType {
|
||||
Normal = 0x30,
|
||||
Hardlink = 0x31,
|
||||
Symlink = 0x32,
|
||||
Char = 0x33,
|
||||
Block = 0x34,
|
||||
Dir = 0x35,
|
||||
FIFO = 0x36,
|
||||
Unknown = 0x00,
|
||||
}
|
||||
|
||||
impl<T> From<T> for FileType
|
||||
where
|
||||
T: Deref<Target = Metadata>,
|
||||
{
|
||||
fn from(meta: T) -> Self {
|
||||
if meta.is_dir() {
|
||||
return FileType::Dir;
|
||||
}
|
||||
|
||||
let file_type = meta.file_type();
|
||||
if file_type.is_fifo() {
|
||||
return FileType::FIFO;
|
||||
} else if file_type.is_char_device() {
|
||||
return FileType::Char;
|
||||
} else if file_type.is_block_device() {
|
||||
return FileType::Block;
|
||||
} else if file_type.is_symlink() {
|
||||
return FileType::Symlink;
|
||||
} else if file_type.is_file() {
|
||||
return FileType::Normal;
|
||||
}
|
||||
|
||||
FileType::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Owner {
|
||||
pub uid: u32,
|
||||
pub gid: u32,
|
||||
pub username: String,
|
||||
pub groupname: String,
|
||||
}
|
||||
|
||||
impl Default for Owner {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
uid: 0,
|
||||
gid: 0,
|
||||
username: "root".into(),
|
||||
groupname: "root".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, DekuRead, DekuWrite)]
|
||||
#[deku(endian = "little")]
|
||||
pub struct Header {
|
||||
pub(crate) fname: [u8; 100],
|
||||
pub(crate) mode: [u8; 8],
|
||||
pub(crate) uid: [u8; 8],
|
||||
pub(crate) gid: [u8; 8],
|
||||
pub(crate) size: [u8; 12],
|
||||
pub(crate) mtime: [u8; 12],
|
||||
pub(crate) header_checksum: [u8; 8],
|
||||
pub(crate) link_indicator: [u8; 1],
|
||||
pub(crate) link_name: [u8; 100],
|
||||
pub(crate) ustar_magic: [u8; 6],
|
||||
pub(crate) ustar_version: [u8; 2],
|
||||
pub(crate) username: [u8; 32],
|
||||
pub(crate) groupname: [u8; 32],
|
||||
pub(crate) device_major: [u8; 8],
|
||||
pub(crate) device_minor: [u8; 8],
|
||||
pub(crate) file_prefix: [u8; 155],
|
||||
pub(crate) reserved: [u8; 12],
|
||||
}
|
||||
|
||||
impl Default for Header {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
fname: [0; 100],
|
||||
mode: [0; 8],
|
||||
uid: [0; 8],
|
||||
gid: [0; 8],
|
||||
size: [0; 12],
|
||||
mtime: [0; 12],
|
||||
header_checksum: [0x20; 8],
|
||||
link_indicator: [0; 1],
|
||||
link_name: [0; 100],
|
||||
ustar_magic: [0x75, 0x73, 0x74, 0x61, 0x72, 0x20],
|
||||
ustar_version: [0x20, 0x00],
|
||||
username: [0; 32],
|
||||
groupname: [0; 32],
|
||||
device_major: [0; 8],
|
||||
device_minor: [0; 8],
|
||||
file_prefix: [0; 155],
|
||||
reserved: [0; 12],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Header {
|
||||
/// Get the filename of this archive member
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use hpk::tar::Header;
|
||||
///
|
||||
/// let header = Header::new("test/1.txt").unwrap();
|
||||
/// let filename = header.filename().unwrap();
|
||||
/// assert_eq!(filename.as_str(), "1.txt");
|
||||
/// ```
|
||||
pub fn filename(&self) -> Result<String, fmt::Error> {
|
||||
let mut s = String::new();
|
||||
for c in self.fname {
|
||||
if c == 0 {
|
||||
break;
|
||||
} else {
|
||||
write!(s, "{}", char::from(c))?;
|
||||
}
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
/// Gets the Unix mode of this archive member
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use hpk::tar::Header;
|
||||
///
|
||||
/// let header = Header::new("test/1.txt").unwrap();
|
||||
/// let mode = header.mode().unwrap();
|
||||
/// assert_eq!(mode, 420);
|
||||
/// ```
|
||||
pub fn mode(&self) -> Result<u32, Error> {
|
||||
let mut s = String::new();
|
||||
for c in self.mode {
|
||||
if c == 0 {
|
||||
break;
|
||||
} else {
|
||||
write!(s, "{}", char::from(c))?;
|
||||
}
|
||||
}
|
||||
let mode = u32::from_str_radix(&s, 8)?;
|
||||
Ok(mode)
|
||||
}
|
||||
|
||||
fn uid(&self) -> Result<u32, Error> {
|
||||
let mut s = String::new();
|
||||
for c in self.mode {
|
||||
if c == 0 {
|
||||
break;
|
||||
} else {
|
||||
write!(s, "{}", char::from(c))?;
|
||||
}
|
||||
}
|
||||
let uid = u32::from_str_radix(&s, 8)?;
|
||||
Ok(uid)
|
||||
}
|
||||
|
||||
fn gid(&self) -> Result<u32, Error> {
|
||||
let mut s = String::new();
|
||||
for c in self.mode {
|
||||
if c == 0 {
|
||||
break;
|
||||
} else {
|
||||
write!(s, "{}", char::from(c))?;
|
||||
}
|
||||
}
|
||||
let gid = u32::from_str_radix(&s, 8)?;
|
||||
Ok(gid)
|
||||
}
|
||||
|
||||
fn username(&self) -> Result<String, fmt::Error> {
|
||||
let mut s = String::new();
|
||||
for c in self.username {
|
||||
if c == 0 {
|
||||
break;
|
||||
} else {
|
||||
write!(s, "{}", char::from(c))?;
|
||||
}
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn groupname(&self) -> Result<String, fmt::Error> {
|
||||
let mut s = String::new();
|
||||
for c in self.groupname {
|
||||
if c == 0 {
|
||||
break;
|
||||
} else {
|
||||
write!(s, "{}", char::from(c))?;
|
||||
}
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
pub fn owner(&self) -> Result<Owner, Error> {
|
||||
let uid = self.uid()?;
|
||||
let gid = self.gid()?;
|
||||
let username = self.username()?;
|
||||
let groupname = self.groupname()?;
|
||||
Ok(Owner {
|
||||
uid,
|
||||
gid,
|
||||
username,
|
||||
groupname,
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets the path to the file minus it's final component
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use hpk::tar::Header;
|
||||
///
|
||||
/// let header = Header::new("test/1.txt").unwrap();
|
||||
/// let prefix = header.prefix().unwrap();
|
||||
/// assert_eq!(prefix.as_str(), "test");
|
||||
/// ```
|
||||
pub fn prefix(&self) -> Option<String> {
|
||||
let mut s = String::new();
|
||||
for c in self.file_prefix {
|
||||
if c == 0 {
|
||||
break;
|
||||
} else {
|
||||
write!(s, "{}", char::from(c)).ok()?;
|
||||
}
|
||||
}
|
||||
if s.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(s)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the full file path to this archive member.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use hpk::tar::Header;
|
||||
/// use std::path::PathBuf;
|
||||
///
|
||||
/// let header = Header::new("test/1.txt").unwrap();
|
||||
/// let path = header.file_path().unwrap();
|
||||
/// assert_eq!(PathBuf::from("test/1.txt"), path);
|
||||
/// ```
|
||||
pub fn file_path(&self) -> Result<PathBuf, fmt::Error> {
|
||||
let mut path = match self.prefix() {
|
||||
Some(p) => PathBuf::from(&p),
|
||||
None => PathBuf::new(),
|
||||
};
|
||||
let name = self.filename()?;
|
||||
path.push(&name);
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
pub fn new(filename: &str) -> Result<Self, Error> {
|
||||
let mut header = Header::default();
|
||||
let meta = fs::symlink_metadata(filename)?;
|
||||
let (filename, prefix) = {
|
||||
// Original tar has a maximum file name length of 100 bytes. The ustar
|
||||
// revision allows storing the path prefix separately, with 100 bytes
|
||||
// reserved for the file name and 150 bytes for the rest of the path.
|
||||
let path = PathBuf::from(&filename);
|
||||
let name = match path.file_name().and_then(OsStr::to_str) {
|
||||
Some(n) => n.to_string(),
|
||||
None => {
|
||||
return Err(Error::Io(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Cannot get file name",
|
||||
)))
|
||||
}
|
||||
};
|
||||
let dir = path.parent().map(|x| format!("{}", x.display()));
|
||||
(name, dir)
|
||||
};
|
||||
|
||||
/* Fill in metadata */
|
||||
header.fname[..filename.len()].copy_from_slice(filename.as_bytes());
|
||||
let mode = format!("{:07o}", (meta.st_mode() & 0o777));
|
||||
header.mode[..mode.len()].copy_from_slice(mode.as_bytes());
|
||||
let user = format!("{:07o}", meta.st_uid());
|
||||
header.uid[..user.len()].copy_from_slice(user.as_bytes());
|
||||
let group = format!("{:07o}", meta.st_gid());
|
||||
header.gid[..group.len()].copy_from_slice(group.as_bytes());
|
||||
let size = format!("{:011o}", meta.st_size());
|
||||
header.size[..size.len()].copy_from_slice(size.as_bytes());
|
||||
let mtime = format!("{:011o}", meta.st_mtime());
|
||||
header.mtime[..mtime.len()].copy_from_slice(mtime.as_bytes());
|
||||
if let Some(prefix) = prefix {
|
||||
header.file_prefix[..prefix.len()].copy_from_slice(prefix.as_bytes());
|
||||
}
|
||||
|
||||
/* Get the file type and conditional metadata */
|
||||
header.link_indicator[0] = FileType::from(&meta) as u8;
|
||||
if header.link_indicator[0] == FileType::Symlink as u8 {
|
||||
let link = fs::read_link(filename)?.to_str().unwrap().to_string();
|
||||
header.link_name[..link.len()].copy_from_slice(link.as_bytes());
|
||||
} else if header.link_indicator[0] == FileType::Block as u8 {
|
||||
let major = format!("{:07o}", meta.st_dev());
|
||||
header.device_major[..major.len()].copy_from_slice(major.as_bytes());
|
||||
let minor = format!("{:07o}", meta.st_rdev());
|
||||
header.device_minor[..minor.len()].copy_from_slice(minor.as_bytes());
|
||||
}
|
||||
|
||||
/* TODO: Find better way to get username */
|
||||
let key = "USER";
|
||||
if let Ok(val) = env::var(key) {
|
||||
header.username[..val.len()].copy_from_slice(val.as_bytes());
|
||||
}
|
||||
/* TODO: Find way to get groupname */
|
||||
|
||||
/* Update the header checksum value */
|
||||
header.update_checksum()?;
|
||||
|
||||
Ok(header)
|
||||
}
|
||||
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
pub fn new_from_meta(
|
||||
filename: &str,
|
||||
meta: &Metadata,
|
||||
owner: Option<Owner>,
|
||||
) -> Result<Self, Error> {
|
||||
let mut header = Header::default();
|
||||
let (filename, prefix) = {
|
||||
// Original tar has a maximum file name length of 100 bytes. The ustar
|
||||
// revision allows storing the path prefix separately, with 100 bytes
|
||||
// reserved for the file name and 150 bytes for the rest of the path.
|
||||
let path = PathBuf::from(&filename);
|
||||
let name = match path.file_name().and_then(OsStr::to_str) {
|
||||
Some(n) => n.to_string(),
|
||||
None => {
|
||||
return Err(Error::Io(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Cannot get file name",
|
||||
)))
|
||||
}
|
||||
};
|
||||
let dir = path.parent().map(|x| format!("{}", x.display()));
|
||||
(name, dir)
|
||||
};
|
||||
header.fname[..filename.len()].copy_from_slice(filename.as_bytes());
|
||||
let mode = format!("{:07o}", meta.st_mode());
|
||||
header.mode[..mode.len()].copy_from_slice(mode.as_bytes());
|
||||
let owner = match owner {
|
||||
Some(o) => o,
|
||||
None => Owner {
|
||||
uid: meta.st_uid(),
|
||||
gid: meta.st_gid(),
|
||||
username: get_username_for_uid(meta.st_uid())?.into(),
|
||||
groupname: get_groupname_for_gid(meta.st_gid())?.into(),
|
||||
},
|
||||
};
|
||||
let uid = format!("{:07o}", owner.uid);
|
||||
header.uid[..uid.len()].copy_from_slice(uid.as_bytes());
|
||||
let gid = format!("{:07o}", owner.gid);
|
||||
header.gid[..gid.len()].copy_from_slice(gid.as_bytes());
|
||||
let size = format!("{:011o}", meta.len());
|
||||
header.size[..size.len()].copy_from_slice(size.as_bytes());
|
||||
let mtime = format!("{:011o}", meta.st_mtime());
|
||||
header.mtime[..mtime.len()].copy_from_slice(mtime.as_bytes());
|
||||
if let Some(prefix) = prefix {
|
||||
header.file_prefix[..prefix.len()].copy_from_slice(prefix.as_bytes());
|
||||
}
|
||||
header.link_indicator[0] = FileType::from(meta) as u8;
|
||||
if header.link_indicator[0] == FileType::Symlink as u8 {
|
||||
let link = fs::read_link(filename)?.to_str().unwrap().to_string();
|
||||
header.link_name[..link.len()].copy_from_slice(link.as_bytes());
|
||||
} else if header.link_indicator[0] == FileType::Block as u8 {
|
||||
let major = format!("{:07o}", meta.st_dev());
|
||||
header.device_major[..major.len()].copy_from_slice(major.as_bytes());
|
||||
let minor = format!("{:07o}", meta.st_rdev());
|
||||
header.device_minor[..minor.len()].copy_from_slice(minor.as_bytes());
|
||||
}
|
||||
header.username[..owner.username.len()].copy_from_slice(owner.username.as_bytes());
|
||||
header.groupname[..owner.groupname.len()].copy_from_slice(owner.groupname.as_bytes());
|
||||
header.update_checksum()?;
|
||||
Ok(header)
|
||||
}
|
||||
|
||||
/// Validates that the magic value received matches the magic value required in the Tar specification.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use hpk::tar::Header;
|
||||
/// let header = Header::default();
|
||||
/// if !header.validate_magic() {
|
||||
/// println!("Magic value is invalid");
|
||||
/// }
|
||||
/// ```
|
||||
pub fn validate_magic(self) -> bool {
|
||||
self.ustar_magic == "ustar ".as_bytes()
|
||||
}
|
||||
|
||||
/// Validates the header checksum computes to the expected value.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use hpk::tar::Header;
|
||||
/// let header = Header::default();
|
||||
/// if header.validate_checksum().unwrap() {
|
||||
/// println!("Checksum is valid");
|
||||
/// }
|
||||
/// ```
|
||||
pub fn validate_checksum(self) -> Result<bool, Error> {
|
||||
let mut test = self;
|
||||
let mut new = [0x20u8; 8];
|
||||
test.header_checksum.copy_from_slice(&[0x20; 8]);
|
||||
|
||||
let tmp = format!("{:06o}\x00", test.calc_checksum()?);
|
||||
new[..tmp.len()].copy_from_slice(tmp.as_bytes());
|
||||
|
||||
Ok(self.header_checksum == new)
|
||||
}
|
||||
|
||||
/// Updates the header checksum value.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use hpk::tar::Header;
|
||||
/// let mut header = Header::default();
|
||||
///
|
||||
/// /* Fill in header information */
|
||||
///
|
||||
/// header.update_checksum();
|
||||
/// ```
|
||||
pub fn update_checksum(&mut self) -> Result<(), Error> {
|
||||
let checksum = format!("{:06o}\x00", self.calc_checksum()?);
|
||||
self.header_checksum[..checksum.len()].copy_from_slice(checksum.as_bytes());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn calc_checksum(self) -> Result<usize, Error> {
|
||||
let out = self.to_bytes()?;
|
||||
let mut checksum = 0;
|
||||
for i in out {
|
||||
checksum += i as usize;
|
||||
}
|
||||
Ok(checksum)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_username_for_uid<'a>(uid: u32) -> Result<&'a str, std::str::Utf8Error> {
|
||||
let user = unsafe {
|
||||
let pw = libc::getpwuid(uid);
|
||||
let name = (*pw).pw_name;
|
||||
CStr::from_ptr(name)
|
||||
};
|
||||
user.to_str()
|
||||
}
|
||||
|
||||
fn get_groupname_for_gid<'a>(gid: u32) -> Result<&'a str, std::str::Utf8Error> {
|
||||
let group = unsafe {
|
||||
let gr = libc::getgrgid(gid);
|
||||
let name = (*gr).gr_name;
|
||||
CStr::from_ptr(name)
|
||||
};
|
||||
group.to_str()
|
||||
}
|
199
src/tar/mod.rs
199
src/tar/mod.rs
@ -1,199 +0,0 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, BufReader, Write},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use rayon::prelude::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
|
||||
|
||||
mod error;
|
||||
mod header;
|
||||
mod node;
|
||||
pub use {
|
||||
error::Error,
|
||||
header::{FileType, Header, Owner},
|
||||
node::Node,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Archive {
|
||||
pub nodes: Vec<Node>,
|
||||
}
|
||||
|
||||
impl Archive {
|
||||
pub fn to_vec(self) -> Result<Vec<u8>, Error> {
|
||||
let mut buf = vec![];
|
||||
for node in self.nodes {
|
||||
buf.extend(node.to_vec()?);
|
||||
}
|
||||
buf.write_all(&[0; 9216])?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Write out a vector of `TarNodes` to a file or something that implements
|
||||
/// ``std::io::Write`` and ``std::io::Copy``.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use std::fs::File;
|
||||
/// use hpk::tar::Archive;
|
||||
///
|
||||
/// let data = Archive::new("test/1.txt").unwrap();
|
||||
///
|
||||
/// let out = File::create("test/2.tar".to_string()).unwrap();
|
||||
/// data.write(&out).unwrap();
|
||||
/// ```
|
||||
pub fn write<T: io::Write + Copy>(self, mut input: T) -> Result<usize, Error> {
|
||||
let mut written = 0;
|
||||
for f in self.nodes.clone() {
|
||||
written += f.write(input)?;
|
||||
}
|
||||
|
||||
/* Complete the write with 18 blocks of 512 ``0x00`` bytes per the specification */
|
||||
if !self.nodes.is_empty() {
|
||||
input.write_all(&[0; 9216])?;
|
||||
written += 9216;
|
||||
}
|
||||
|
||||
Ok(written)
|
||||
}
|
||||
|
||||
/// Create a new `TarFile` struct and initialize it with a `filename` file.
|
||||
/// This will read in the file to the `TarFile` struct as a `TarNode`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use hpk::tar::Archive;
|
||||
///
|
||||
/// let data = Archive::new("test/1.txt").unwrap();
|
||||
/// ```
|
||||
pub fn new(filename: &str) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
nodes: vec![Node::read_file_to_tar(filename)?],
|
||||
})
|
||||
}
|
||||
|
||||
/// Append another file to the `TarFile.file` vector. This adds a file to the
|
||||
/// internal representation of the tar file.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use hpk::tar::Archive;
|
||||
///
|
||||
/// let mut data = Archive::new("test/1.txt").unwrap();
|
||||
/// data.append("test/1.txt").unwrap();
|
||||
/// ```
|
||||
pub fn append(&mut self, filename: &str) -> Result<(), Error> {
|
||||
self.nodes.push(Node::read_file_to_tar(filename)?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Open and load an external tar file into the internal `TarFile` struct. This
|
||||
/// parses and loads up all the files contained within the external tar file.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use hpk::tar::Archive;
|
||||
///
|
||||
/// Archive::open("test/1.tar").unwrap();
|
||||
/// ```
|
||||
pub fn open(filename: &str) -> Result<Self, Error> {
|
||||
let file = File::open(filename)?;
|
||||
let mut reader = BufReader::new(file);
|
||||
Self::read(&mut reader)
|
||||
}
|
||||
|
||||
/// Read a tar archive from anything which implements `io::Read`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use hpk::tar::Archive;
|
||||
/// use std::{fs::File, io::BufReader};
|
||||
///
|
||||
/// let file = File::open("test/1.tar").unwrap();
|
||||
/// let mut reader = BufReader::new(file);
|
||||
/// Archive::read(&mut reader).unwrap();
|
||||
/// ```
|
||||
pub fn read<T: io::Read>(mut input: T) -> Result<Self, Error> {
|
||||
let mut out = Self {
|
||||
nodes: Vec::<Node>::new(),
|
||||
};
|
||||
while let Ok(t) = Node::read(&mut input) {
|
||||
out.nodes.push(t);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Remove the first file from the Tar that matches the filename and path.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use hpk::tar::Archive;
|
||||
///
|
||||
/// let mut data = Archive::new("test/1.tar").unwrap();
|
||||
/// data.remove("test/1.tar").unwrap();
|
||||
/// ```
|
||||
pub fn remove(&mut self, filename: &str) -> Result<bool, Error> {
|
||||
let mut name = [0u8; 100];
|
||||
name[..filename.len()].copy_from_slice(filename.as_bytes());
|
||||
if let Some(i) = &self
|
||||
.nodes
|
||||
.par_iter()
|
||||
.position_any(|x| x.header.fname == name)
|
||||
{
|
||||
self.nodes.remove(*i);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Get the first node from the archive that matches the given filename.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use hpk::tar::Archive;
|
||||
/// let archive = Archive::new("test/1.txt").unwrap();
|
||||
/// let node = archive.get("test/1.txt");
|
||||
/// assert!(node.is_some());
|
||||
/// ```
|
||||
pub fn get(&self, filename: &str) -> Option<Node> {
|
||||
self.nodes
|
||||
.par_iter()
|
||||
.find_any(|x| x.header.file_path() == Ok(PathBuf::from(filename)))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn pop(&mut self, filename: &str) -> Option<Node> {
|
||||
if let Some(i) = self
|
||||
.nodes
|
||||
.par_iter()
|
||||
.position_any(|x| x.header.file_path() == Ok(PathBuf::from(filename)))
|
||||
{
|
||||
let node = self.nodes.get(i).cloned();
|
||||
self.nodes.remove(i);
|
||||
return node;
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn open_get() {
|
||||
let archive = Archive::open("test/1.tar").unwrap();
|
||||
let node = archive.get("1.txt");
|
||||
assert!(node.is_some());
|
||||
}
|
||||
}
|
138
src/tar/node.rs
138
src/tar/node.rs
@ -1,138 +0,0 @@
|
||||
use crate::tar::{header::Owner, Error, FileType, Header};
|
||||
use deku::prelude::*;
|
||||
use std::{
|
||||
fs::{File, Metadata},
|
||||
io::{self, BufReader},
|
||||
str,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Node {
|
||||
pub header: Header,
|
||||
pub data: Vec<[u8; 512]>,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
pub fn to_vec(self) -> Result<Vec<u8>, DekuError> {
|
||||
let mut buf = self.header.to_bytes()?;
|
||||
for block in self.data {
|
||||
buf.extend(block.to_vec());
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Write out a single file within the tar to a file or something with a
|
||||
/// ``std::io::Write`` trait.
|
||||
pub fn write<T: io::Write>(&self, mut input: T) -> Result<usize, Error> {
|
||||
input.write_all(&self.header.to_bytes()?)?;
|
||||
let mut written = 512;
|
||||
for d in &self.data {
|
||||
input.write_all(d)?;
|
||||
written += d.len();
|
||||
}
|
||||
|
||||
Ok(written)
|
||||
}
|
||||
|
||||
/// Read a `Node` in from a file or something with a ``std::io::Read`` trait.
|
||||
pub fn read<T: io::Read>(mut input: T) -> Result<Self, Error> {
|
||||
let mut h = vec![0u8; 512];
|
||||
input.read_exact(&mut h)?;
|
||||
|
||||
let (_, header) = Header::from_bytes((&h, 0))?;
|
||||
if header == Header::default() {
|
||||
return Err(Error::EndOfTar);
|
||||
}
|
||||
if !header.validate_magic() {
|
||||
return Err(Error::InvalidMagic);
|
||||
}
|
||||
if !header.validate_checksum()? {
|
||||
return Err(Error::InvalidChecksum);
|
||||
}
|
||||
|
||||
let chunks = (oct_to_dec(&header.size)? / 512) + 1;
|
||||
Ok(Node {
|
||||
header,
|
||||
data: Node::chunk_file(&mut input, Some(chunks))?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Open and read a file from the ``filename`` argument to a `Node`.
|
||||
pub fn read_file_to_tar(filename: &str) -> Result<Self, Error> {
|
||||
let header = Header::new(filename)?;
|
||||
if header.link_indicator[0] != FileType::Normal as u8 {
|
||||
return Ok(Node {
|
||||
header,
|
||||
data: Vec::<[u8; 512]>::new(),
|
||||
});
|
||||
}
|
||||
|
||||
let file = File::open(filename)?;
|
||||
let mut reader = BufReader::new(file);
|
||||
Ok(Node {
|
||||
header,
|
||||
data: Node::chunk_file(&mut reader, None)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a Node from in memory data, given the filename and metadata
|
||||
pub fn read_data_to_tar(
|
||||
data: &[u8],
|
||||
filename: &str,
|
||||
meta: &Metadata,
|
||||
owner: Option<Owner>,
|
||||
) -> Result<Self, Error> {
|
||||
let header = Header::new_from_meta(filename, meta, owner)?;
|
||||
if header.link_indicator[0] != FileType::Normal as u8 {
|
||||
return Ok(Node {
|
||||
header,
|
||||
data: Vec::<[u8; 512]>::new(),
|
||||
});
|
||||
}
|
||||
let mut reader = BufReader::new(data);
|
||||
Ok(Node {
|
||||
header,
|
||||
data: Node::chunk_file(&mut reader, None)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Read in and split a file into ``512`` byte chunks.
|
||||
fn chunk_file<T: std::io::Read>(
|
||||
file: &mut T,
|
||||
max_chunks: Option<usize>,
|
||||
) -> Result<Vec<[u8; 512]>, Error> {
|
||||
/* Extract the file data from the tar file */
|
||||
let mut out = Vec::<[u8; 512]>::new();
|
||||
let mut n = if let Some(max) = max_chunks {
|
||||
max
|
||||
} else {
|
||||
usize::MAX
|
||||
};
|
||||
|
||||
/* Carve out 512 bytes at a time */
|
||||
let mut buf: [u8; 512] = [0; 512];
|
||||
loop {
|
||||
let len = file.read(&mut buf)?;
|
||||
|
||||
n -= 1;
|
||||
|
||||
/* If read len == 0, we've hit the EOF */
|
||||
if len == 0 || n == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
/* Save this chunk */
|
||||
out.push(buf);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
fn oct_to_dec(input: &[u8]) -> Result<usize, Error> {
|
||||
/* Convert the &[u8] to string and remove the null byte */
|
||||
let mut s = str::from_utf8(input)?.to_string();
|
||||
s.pop();
|
||||
|
||||
/* Convert to usize from octal */
|
||||
Ok(usize::from_str_radix(&s, 8)?)
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
use {
|
||||
crate::Version,
|
||||
chrono::{offset::Utc, DateTime},
|
||||
serde::{Deserialize, Serialize},
|
||||
std::{cmp::Ordering, error::Error, fmt, str::FromStr},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
/// Represents a Git revision
|
||||
pub struct GitRev {
|
||||
/// the short revision hash
|
||||
pub hash: String,
|
||||
/// the time of the revision commit
|
||||
pub datetime: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl fmt::Display for GitRev {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "git_{}.{}", self.hash, self.datetime.format("%Y%m%d"))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for GitRev {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
self.datetime.partial_cmp(&other.datetime)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for GitRev {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.datetime.cmp(&other.datetime)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParseGitRevError;
|
||||
|
||||
impl fmt::Display for ParseGitRevError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Error parsing git version")
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ParseGitRevError {}
|
||||
|
||||
impl From<chrono::ParseError> for ParseGitRevError {
|
||||
fn from(_value: chrono::ParseError) -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for GitRev {
|
||||
type Err = ParseGitRevError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if let Some(gitrev) = s.strip_prefix("git_") {
|
||||
if let Some((hash, date)) = gitrev.split_once('_') {
|
||||
if hash.len() == 7 {
|
||||
let datetime = DateTime::parse_from_str(date, "%Y%m%d")?;
|
||||
return Ok(Self {
|
||||
hash: hash.to_string(),
|
||||
datetime: datetime.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(ParseGitRevError)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Version> for GitRev {
|
||||
type Error = ParseGitRevError;
|
||||
|
||||
fn try_from(value: Version) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Version::Git(g) => Ok(g),
|
||||
_ => Err(ParseGitRevError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{thread, time::Duration};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ord() {
|
||||
let a = GitRev {
|
||||
hash: "aaab".to_string(),
|
||||
datetime: Utc::now(),
|
||||
};
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
let b = GitRev {
|
||||
hash: "aaaa".to_string(),
|
||||
datetime: Utc::now(),
|
||||
};
|
||||
assert!(a < b);
|
||||
}
|
||||
}
|
@ -1,177 +0,0 @@
|
||||
use {
|
||||
serde::{Deserialize, Serialize},
|
||||
std::{error::Error, fmt, str::FromStr},
|
||||
};
|
||||
|
||||
mod gitrev;
|
||||
mod rapid;
|
||||
mod semver;
|
||||
|
||||
pub use {gitrev::GitRev, rapid::Rapid, semver::SemVer};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
/// An enum representing the most common versioning schemes.
|
||||
/// Each scheme must implement `Eq` and `Ord` with itself, and
|
||||
/// may optionally implement `PartialEq` and `PartialOrd` with
|
||||
/// other schemes. `Number`, `Rapid`, and `SemVer` do this, while
|
||||
/// Git can only compare with itself.
|
||||
pub enum Version {
|
||||
/// A single replease number, as in Firefox 102
|
||||
Number(u32),
|
||||
/// Rapid versioning consists of two numbers separated by
|
||||
/// a dot (.) character, and is used notably by Gnome
|
||||
Rapid(Rapid),
|
||||
/// SemVer is the most common versioning scheme and consists
|
||||
/// of three dot (.) separated numbers. The numbers are major,
|
||||
/// minor, and patch respctively.
|
||||
SemVer(SemVer),
|
||||
/// A git revision. Use of this scheme is to be avoided in
|
||||
/// official packaging as it cannot be readily compared with
|
||||
/// other versioning schemes for dependency resolution.
|
||||
Git(GitRev),
|
||||
}
|
||||
|
||||
impl Default for Version {
|
||||
fn default() -> Self {
|
||||
Self::SemVer(SemVer {
|
||||
major: 0,
|
||||
minor: 1,
|
||||
patch: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Version {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Number(n) => write!(f, "{n}"),
|
||||
Self::SemVer(s) => write!(f, "{s}"),
|
||||
Self::Rapid(r) => write!(f, "{r}"),
|
||||
Self::Git(g) => write!(f, "{g}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SemVer> for Version {
|
||||
fn from(value: SemVer) -> Self {
|
||||
Self::SemVer(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rapid> for Version {
|
||||
fn from(value: Rapid) -> Self {
|
||||
Self::Rapid(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GitRev> for Version {
|
||||
fn from(value: GitRev) -> Self {
|
||||
Self::Git(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for Version {
|
||||
fn from(value: u32) -> Self {
|
||||
Self::Number(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Version {
|
||||
#[allow(clippy::many_single_char_names)]
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
match (self, other) {
|
||||
(Self::Number(s), Self::Number(o)) => s.partial_cmp(o),
|
||||
(Self::Number(s), Self::Rapid(o)) => s.partial_cmp(o),
|
||||
(Self::Number(s), Self::SemVer(o)) => s.partial_cmp(o),
|
||||
(Self::Rapid(s), Self::Number(o)) => s.partial_cmp(o),
|
||||
(Self::Rapid(s), Self::Rapid(o)) => s.partial_cmp(o),
|
||||
(Self::Rapid(s), Self::SemVer(o)) => s.partial_cmp(o),
|
||||
(Self::SemVer(s), Self::Number(o)) => s.partial_cmp(o),
|
||||
(Self::SemVer(s), Self::Rapid(o)) => s.partial_cmp(o),
|
||||
(Self::SemVer(s), Self::SemVer(o)) => s.partial_cmp(o),
|
||||
(Self::Git(s), Self::Git(o)) => s.partial_cmp(o),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Version {
|
||||
#[allow(clippy::many_single_char_names)]
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Number(s), Self::Number(o)) => s.eq(o),
|
||||
(Self::Number(s), Self::Rapid(o)) => s.eq(o),
|
||||
(Self::Number(s), Self::SemVer(o)) => s.eq(o),
|
||||
(Self::Rapid(s), Self::Number(o)) => s.eq(o),
|
||||
(Self::Rapid(s), Self::Rapid(o)) => s.eq(o),
|
||||
(Self::Rapid(s), Self::SemVer(o)) => s.eq(o),
|
||||
(Self::SemVer(s), Self::Number(o)) => s.eq(o),
|
||||
(Self::SemVer(s), Self::Rapid(o)) => s.eq(o),
|
||||
(Self::SemVer(s), Self::SemVer(o)) => s.eq(o),
|
||||
(Self::Git(s), Self::Git(o)) => s.eq(o),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParseVersionError;
|
||||
|
||||
impl fmt::Display for ParseVersionError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "error parsing version")
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ParseVersionError {}
|
||||
|
||||
impl FromStr for Version {
|
||||
type Err = ParseVersionError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if let Ok(v) = s.parse::<SemVer>() {
|
||||
Ok(v.into())
|
||||
} else if let Ok(v) = s.parse::<Rapid>() {
|
||||
Ok(v.into())
|
||||
} else if let Ok(v) = s.parse::<GitRev>() {
|
||||
Ok(v.into())
|
||||
} else if let Ok(v) = s.parse::<u32>() {
|
||||
Ok(v.into())
|
||||
} else {
|
||||
Err(ParseVersionError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cmp_semver_rapid_gt() {
|
||||
let sem = "1.42.1".parse::<SemVer>().unwrap();
|
||||
let rpd = "1.42".parse::<Rapid>().unwrap();
|
||||
assert!(sem > rpd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp_semver_rapid_eq() {
|
||||
let sem = "1.42.0".parse::<SemVer>().unwrap();
|
||||
let rpd = "1.42".parse::<Rapid>().unwrap();
|
||||
assert!(sem == rpd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp_semver_num_gt() {
|
||||
let sem = Version::SemVer("42.69.0".parse().unwrap());
|
||||
let num = Version::Number(42);
|
||||
assert!(sem > num);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp_semver_num_eq() {
|
||||
let sem = Version::SemVer("42.0.0".parse().unwrap());
|
||||
let num = Version::Number(42);
|
||||
assert_eq!(sem, num);
|
||||
}
|
||||
}
|
@ -1,214 +0,0 @@
|
||||
use crate::SemVer;
|
||||
|
||||
use {
|
||||
crate::Version,
|
||||
serde::{Deserialize, Serialize},
|
||||
std::{cmp::Ordering, error::Error, fmt, num::ParseIntError, str::FromStr},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct Rapid {
|
||||
pub major: u32,
|
||||
pub minor: u32,
|
||||
}
|
||||
|
||||
impl fmt::Display for Rapid {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}.{}", self.major, self.minor)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Rapid {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
match self.major.partial_cmp(&other.major) {
|
||||
Some(Ordering::Greater) => Some(Ordering::Greater),
|
||||
Some(Ordering::Less) => Some(Ordering::Less),
|
||||
Some(Ordering::Equal) => self.minor.partial_cmp(&other.minor),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Rapid {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
match self.major.cmp(&other.major) {
|
||||
Ordering::Greater => Ordering::Greater,
|
||||
Ordering::Less => Ordering::Less,
|
||||
Ordering::Equal => self.minor.cmp(&other.minor),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<u32> for Rapid {
|
||||
fn eq(&self, other: &u32) -> bool {
|
||||
self.major == *other && self.minor == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd<u32> for Rapid {
|
||||
fn partial_cmp(&self, other: &u32) -> Option<Ordering> {
|
||||
match self.major.partial_cmp(other) {
|
||||
Some(Ordering::Greater) => Some(Ordering::Greater),
|
||||
Some(Ordering::Less) => Some(Ordering::Less),
|
||||
None => None,
|
||||
Some(Ordering::Equal) => {
|
||||
if self.minor == 0 {
|
||||
Some(Ordering::Equal)
|
||||
} else {
|
||||
Some(Ordering::Greater)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<SemVer> for Rapid {
|
||||
fn eq(&self, other: &SemVer) -> bool {
|
||||
other.eq(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd<SemVer> for Rapid {
|
||||
fn partial_cmp(&self, other: &SemVer) -> Option<Ordering> {
|
||||
match other.partial_cmp(self) {
|
||||
Some(Ordering::Less) => Some(Ordering::Greater),
|
||||
Some(Ordering::Greater) => Some(Ordering::Less),
|
||||
Some(Ordering::Equal) => Some(Ordering::Equal),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Rapid> for u32 {
|
||||
fn eq(&self, other: &Rapid) -> bool {
|
||||
other.eq(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd<Rapid> for u32 {
|
||||
fn partial_cmp(&self, other: &Rapid) -> Option<Ordering> {
|
||||
match other.partial_cmp(self) {
|
||||
Some(Ordering::Equal) => Some(Ordering::Equal),
|
||||
Some(Ordering::Less) => Some(Ordering::Greater),
|
||||
Some(Ordering::Greater) => Some(Ordering::Less),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct ParseRapidError;
|
||||
|
||||
impl fmt::Display for ParseRapidError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Error parsing Rapid version")
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ParseRapidError {}
|
||||
|
||||
impl From<ParseIntError> for ParseRapidError {
|
||||
fn from(_value: ParseIntError) -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Rapid {
|
||||
type Err = ParseRapidError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let split = s.split('.').collect::<Vec<_>>();
|
||||
match split.len() {
|
||||
2 => {
|
||||
let major = split.first().unwrap().parse::<u32>()?;
|
||||
let minor = split.get(1).unwrap().parse::<u32>()?;
|
||||
Ok(Self { major, minor })
|
||||
}
|
||||
_ => Err(ParseRapidError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Version> for Rapid {
|
||||
type Error = ParseRapidError;
|
||||
|
||||
fn try_from(value: Version) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Version::SemVer(s) => {
|
||||
if s.patch == 0 {
|
||||
Ok(Self {
|
||||
major: s.major,
|
||||
minor: s.minor,
|
||||
})
|
||||
} else {
|
||||
Err(ParseRapidError)
|
||||
}
|
||||
}
|
||||
Version::Rapid(s) => Ok(s),
|
||||
Version::Number(major) => Ok(Self { major, minor: 0 }),
|
||||
Version::Git(_) => Err(ParseRapidError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_semver() {
|
||||
assert_eq!(
|
||||
"93.0".parse::<Rapid>(),
|
||||
Ok(Rapid {
|
||||
major: 93,
|
||||
minor: 0,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rapid_num_eq() {
|
||||
let rapid = Rapid {
|
||||
major: 42,
|
||||
minor: 0,
|
||||
};
|
||||
assert_eq!(rapid, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rapid_num_gt() {
|
||||
let rapid = Rapid {
|
||||
major: 1,
|
||||
minor: 42,
|
||||
};
|
||||
assert!(rapid > 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rapid_semver_eq() {
|
||||
let rapid = Rapid {
|
||||
major: 42,
|
||||
minor: 69,
|
||||
};
|
||||
let semver = SemVer {
|
||||
major: 42,
|
||||
minor: 69,
|
||||
patch: 0,
|
||||
};
|
||||
assert_eq!(rapid, semver);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rapid_semver_lt() {
|
||||
let rapid = Rapid {
|
||||
major: 42,
|
||||
minor: 69,
|
||||
};
|
||||
let semver = SemVer {
|
||||
major: 42,
|
||||
minor: 69,
|
||||
patch: 1,
|
||||
};
|
||||
assert!(rapid < semver);
|
||||
}
|
||||
}
|
@ -1,205 +0,0 @@
|
||||
use {
|
||||
crate::{Rapid, Version},
|
||||
serde::{Deserialize, Serialize},
|
||||
std::{cmp::Ordering, error::Error, fmt, num::ParseIntError, str::FromStr},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct SemVer {
|
||||
pub major: u32,
|
||||
pub minor: u32,
|
||||
pub patch: u32,
|
||||
}
|
||||
|
||||
impl fmt::Display for SemVer {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for SemVer {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
match self.major.partial_cmp(&other.major) {
|
||||
Some(Ordering::Greater) => Some(Ordering::Greater),
|
||||
Some(Ordering::Less) => Some(Ordering::Less),
|
||||
None => None,
|
||||
Some(Ordering::Equal) => match self.minor.partial_cmp(&other.minor) {
|
||||
Some(Ordering::Greater) => Some(Ordering::Greater),
|
||||
Some(Ordering::Less) => Some(Ordering::Less),
|
||||
None => None,
|
||||
Some(Ordering::Equal) => self.patch.partial_cmp(&other.patch),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for SemVer {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
match self.major.cmp(&other.major) {
|
||||
Ordering::Greater => Ordering::Greater,
|
||||
Ordering::Less => Ordering::Less,
|
||||
Ordering::Equal => match self.minor.cmp(&other.minor) {
|
||||
Ordering::Greater => Ordering::Greater,
|
||||
Ordering::Less => Ordering::Less,
|
||||
Ordering::Equal => self.patch.cmp(&other.patch),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Rapid> for SemVer {
|
||||
fn eq(&self, other: &Rapid) -> bool {
|
||||
self.major == other.major && self.minor == other.minor && self.patch == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd<Rapid> for SemVer {
|
||||
fn partial_cmp(&self, other: &Rapid) -> Option<std::cmp::Ordering> {
|
||||
match self.major.partial_cmp(&other.major) {
|
||||
Some(Ordering::Greater) => Some(Ordering::Greater),
|
||||
Some(Ordering::Less) => Some(Ordering::Less),
|
||||
None => None,
|
||||
Some(Ordering::Equal) => match self.minor.partial_cmp(&other.minor) {
|
||||
Some(Ordering::Greater) => Some(Ordering::Greater),
|
||||
Some(Ordering::Less) => Some(Ordering::Less),
|
||||
None => None,
|
||||
Some(Ordering::Equal) => match self.patch {
|
||||
0 => Some(Ordering::Equal),
|
||||
_ => Some(Ordering::Greater),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<u32> for SemVer {
|
||||
fn eq(&self, other: &u32) -> bool {
|
||||
self.major == *other && self.minor == 0 && self.patch == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd<u32> for SemVer {
|
||||
fn partial_cmp(&self, other: &u32) -> Option<Ordering> {
|
||||
match self.major.cmp(other) {
|
||||
Ordering::Greater => Some(Ordering::Greater),
|
||||
Ordering::Less => Some(Ordering::Less),
|
||||
Ordering::Equal => {
|
||||
if self.minor == 0 && self.patch == 0 {
|
||||
Some(Ordering::Equal)
|
||||
} else {
|
||||
Some(Ordering::Greater)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<SemVer> for u32 {
|
||||
fn eq(&self, other: &SemVer) -> bool {
|
||||
other.eq(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd<SemVer> for u32 {
|
||||
fn partial_cmp(&self, other: &SemVer) -> Option<Ordering> {
|
||||
match other.partial_cmp(self) {
|
||||
Some(Ordering::Less) => Some(Ordering::Greater),
|
||||
Some(Ordering::Greater) => Some(Ordering::Less),
|
||||
Some(Ordering::Equal) => Some(Ordering::Equal),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct ParseSemVerError;
|
||||
|
||||
impl fmt::Display for ParseSemVerError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Error parsing SemVer")
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ParseSemVerError {}
|
||||
|
||||
impl From<ParseIntError> for ParseSemVerError {
|
||||
fn from(_value: ParseIntError) -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SemVer {
|
||||
type Err = ParseSemVerError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let split = s.split('.').collect::<Vec<_>>();
|
||||
match split.len() {
|
||||
3 => {
|
||||
let major = split.first().unwrap().parse::<u32>()?;
|
||||
let minor = split.get(1).unwrap().parse::<u32>()?;
|
||||
let patch = split.get(2).unwrap().parse::<u32>()?;
|
||||
Ok(Self {
|
||||
major,
|
||||
minor,
|
||||
patch,
|
||||
})
|
||||
}
|
||||
_ => Err(ParseSemVerError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Version> for SemVer {
|
||||
type Error = ParseSemVerError;
|
||||
fn try_from(value: Version) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Version::SemVer(s) => Ok(SemVer {
|
||||
major: s.major,
|
||||
minor: s.minor,
|
||||
patch: s.patch,
|
||||
}),
|
||||
Version::Rapid(r) => Ok(SemVer {
|
||||
major: r.major,
|
||||
minor: r.minor,
|
||||
patch: 0,
|
||||
}),
|
||||
Version::Number(n) => Ok(Self {
|
||||
major: n,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
}),
|
||||
Version::Git(_) => Err(ParseSemVerError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_semver() {
|
||||
assert_eq!(
|
||||
"1.0.3".parse::<SemVer>(),
|
||||
Ok(SemVer {
|
||||
major: 1,
|
||||
minor: 0,
|
||||
patch: 3
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp_semver_num_gt() {
|
||||
let sem = "42.69.0".parse::<SemVer>().unwrap();
|
||||
let num = 42;
|
||||
assert!(sem > num);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmp_semver_num_eq() {
|
||||
let sem = "42.0.0".parse::<SemVer>().unwrap();
|
||||
let num = 42;
|
||||
assert_eq!(sem, num);
|
||||
}
|
||||
}
|
BIN
test/1.tar
BIN
test/1.tar
Binary file not shown.
@ -1 +0,0 @@
|
||||
This is a test file.
|
BIN
test/2.tar
BIN
test/2.tar
Binary file not shown.
Loading…
Reference in New Issue
Block a user