use crate::{compress, entry::write::derive_key_material, error::UnknownValueError};
use password_hash::Output;
pub(crate) use private::*;
use std::{
collections::HashMap,
fmt, io,
str::FromStr,
sync::{Arc, Mutex, MutexGuard, PoisonError},
};
mod private {
use super::*;
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub enum Compress {
No,
Deflate(compress::deflate::DeflateCompressionLevel),
ZStandard(compress::zstandard::ZstdCompressionLevel),
XZ(compress::xz::XZCompressionLevel),
}
#[derive(Clone, Debug)]
pub struct Cipher {
pub(crate) password: Password,
pub(crate) derived: DerivedKeyMaterial,
pub(crate) hash_algorithm: HashAlgorithm,
pub(crate) cipher_algorithm: CipherAlgorithm,
pub(crate) mode: CipherMode,
}
impl Cipher {
#[inline]
pub(crate) const fn new(
password: Password,
derived: DerivedKeyMaterial,
hash_algorithm: HashAlgorithm,
cipher_algorithm: CipherAlgorithm,
mode: CipherMode,
) -> Self {
Self {
password,
derived,
hash_algorithm,
cipher_algorithm,
mode,
}
}
}
const KEY_CACHE_CAP: usize = 16;
#[derive(Clone)]
pub struct KeyCache {
inner: Arc<Mutex<HashMap<String, Output>>>,
}
impl KeyCache {
pub(crate) fn new() -> Self {
Self {
inner: Arc::new(Mutex::new(HashMap::new())),
}
}
pub(crate) fn get(&self, phsf: &str) -> Option<Output> {
self.lock().get(phsf).copied()
}
pub(crate) fn insert(&self, phsf: &str, key: Output) {
let mut map = self.lock();
if map.len() >= KEY_CACHE_CAP {
map.clear();
}
map.insert(phsf.into(), key);
}
fn lock(&self) -> MutexGuard<'_, HashMap<String, Output>> {
self.inner.lock().unwrap_or_else(PoisonError::into_inner)
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.lock().len()
}
}
impl fmt::Debug for KeyCache {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("KeyCache")
.field("entries", &self.lock().len())
.finish()
}
}
#[derive(Clone, Debug)]
pub struct DerivedKeyMaterial {
pub(crate) phsf: String,
pub(crate) key: Output,
}
pub trait WriteOption {
fn compress(&self) -> Compress;
fn cipher(&self) -> Option<&Cipher>;
#[inline]
fn compression(&self) -> Compression {
match self.compress() {
Compress::No => Compression::NO,
Compress::Deflate(_) => Compression::DEFLATE,
Compress::ZStandard(_) => Compression::ZSTANDARD,
Compress::XZ(_) => Compression::XZ,
}
}
#[inline]
fn encryption(&self) -> Encryption {
self.cipher()
.map_or(Encryption::NO, |it| match it.cipher_algorithm {
CipherAlgorithm::Aes => Encryption::AES,
CipherAlgorithm::Camellia => Encryption::CAMELLIA,
})
}
#[inline]
fn cipher_mode(&self) -> CipherMode {
self.cipher().map_or(CipherMode::CTR, |it| it.mode)
}
#[inline]
fn hash_algorithm(&self) -> HashAlgorithm {
self.cipher()
.map_or_else(HashAlgorithm::argon2id, |it| it.hash_algorithm)
}
#[inline]
fn password(&self) -> Option<&[u8]> {
self.cipher().map(|it| it.password.as_bytes())
}
}
impl WriteOption for WriteOptions {
#[inline]
fn compress(&self) -> Compress {
self.compress
}
#[inline]
fn cipher(&self) -> Option<&Cipher> {
self.cipher.as_ref()
}
}
impl<T> WriteOption for &T
where
T: WriteOption,
{
#[inline]
fn compress(&self) -> Compress {
T::compress(self)
}
#[inline]
fn cipher(&self) -> Option<&Cipher> {
T::cipher(self)
}
}
pub trait ReadOption {
fn password(&self) -> Option<&[u8]>;
fn key_cache(&self) -> Option<&KeyCache>;
}
impl<T: ReadOption> ReadOption for &T {
#[inline]
fn password(&self) -> Option<&[u8]> {
T::password(self)
}
#[inline]
fn key_cache(&self) -> Option<&KeyCache> {
T::key_cache(self)
}
}
impl ReadOption for ReadOptions {
#[inline]
fn password(&self) -> Option<&[u8]> {
self.password.as_deref()
}
#[inline]
fn key_cache(&self) -> Option<&KeyCache> {
Some(&self.key_cache)
}
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Compression(u8);
impl Compression {
pub const NO: Self = Self(0);
pub const DEFLATE: Self = Self(1);
pub const ZSTANDARD: Self = Self(2);
pub const XZ: Self = Self(4);
#[deprecated(since = "0.36.0", note = "renamed to `Compression::NO`")]
#[allow(non_upper_case_globals)]
pub const No: Self = Self::NO;
#[deprecated(since = "0.36.0", note = "renamed to `Compression::DEFLATE`")]
#[allow(non_upper_case_globals)]
pub const Deflate: Self = Self::DEFLATE;
#[deprecated(since = "0.36.0", note = "renamed to `Compression::ZSTANDARD`")]
#[allow(non_upper_case_globals)]
pub const ZStandard: Self = Self::ZSTANDARD;
#[inline]
pub const fn from_byte(value: u8) -> Self {
Self(value)
}
#[inline]
pub const fn to_byte(self) -> u8 {
self.0
}
#[inline]
pub const fn new_private(value: u8) -> Option<Self> {
if value >= 128 {
Some(Self(value))
} else {
None
}
}
#[deprecated(since = "0.36.0", note = "use `Compression::from_byte`")]
#[allow(clippy::should_implement_trait)]
#[inline]
pub const fn try_from(value: u8) -> Result<Self, UnknownValueError> {
Ok(Self::from_byte(value))
}
#[inline]
pub const fn is_reserved(self) -> bool {
!matches!(self.0, 0..=2 | 4) && self.0 < 128
}
#[inline]
pub const fn is_private(self) -> bool {
self.0 >= 128
}
}
impl fmt::Debug for Compression {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::NO => f.write_str("No"),
Self::DEFLATE => f.write_str("Deflate"),
Self::ZSTANDARD => f.write_str("ZStandard"),
Self::XZ => f.write_str("XZ"),
Self(v) if v < 128 => f.debug_tuple("Reserved").field(&v).finish(),
Self(v) => f.debug_tuple("Private").field(&v).finish(),
}
}
}
impl TryFrom<u8> for Compression {
type Error = UnknownValueError;
#[inline]
fn try_from(value: u8) -> Result<Self, Self::Error> {
Ok(Self::from_byte(value))
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub(crate) enum CompressionLevelImpl {
Min,
Max,
Default,
Custom(i64),
}
impl FromStr for CompressionLevelImpl {
type Err = core::num::ParseIntError;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.eq_ignore_ascii_case("min") {
Ok(Self::Min)
} else if s.eq_ignore_ascii_case("max") {
Ok(Self::Max)
} else if s.eq_ignore_ascii_case("default") {
Ok(Self::Default)
} else {
Ok(Self::Custom(i64::from_str(s)?))
}
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub struct CompressionLevel(pub(crate) CompressionLevelImpl);
impl CompressionLevel {
pub(crate) const DEFAULT: Self = Self(CompressionLevelImpl::Default);
#[inline]
pub const fn min() -> Self {
Self(CompressionLevelImpl::Min)
}
#[inline]
pub const fn max() -> Self {
Self(CompressionLevelImpl::Max)
}
}
impl Default for CompressionLevel {
#[inline]
fn default() -> Self {
Self::DEFAULT
}
}
impl<T: Into<i64>> From<T> for CompressionLevel {
#[inline]
fn from(value: T) -> Self {
Self(CompressionLevelImpl::Custom(value.into()))
}
}
impl FromStr for CompressionLevel {
type Err = core::num::ParseIntError;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(CompressionLevelImpl::from_str(s)?))
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub enum CipherAlgorithm {
Aes,
Camellia,
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub(crate) struct Password(Vec<u8>);
impl Password {
#[inline]
pub(crate) const fn as_bytes(&self) -> &[u8] {
self.0.as_slice()
}
}
impl<T: AsRef<[u8]>> From<T> for Password {
#[inline]
fn from(value: T) -> Self {
Self(value.as_ref().to_vec())
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Encryption(u8);
impl Encryption {
pub const NO: Self = Self(0);
pub const AES: Self = Self(1);
pub const CAMELLIA: Self = Self(2);
#[deprecated(since = "0.36.0", note = "renamed to `Encryption::NO`")]
#[allow(non_upper_case_globals)]
pub const No: Self = Self::NO;
#[deprecated(since = "0.36.0", note = "renamed to `Encryption::AES`")]
#[allow(non_upper_case_globals)]
pub const Aes: Self = Self::AES;
#[deprecated(since = "0.36.0", note = "renamed to `Encryption::CAMELLIA`")]
#[allow(non_upper_case_globals)]
pub const Camellia: Self = Self::CAMELLIA;
#[inline]
pub const fn from_byte(value: u8) -> Self {
Self(value)
}
#[inline]
pub const fn to_byte(self) -> u8 {
self.0
}
#[inline]
pub const fn new_private(value: u8) -> Option<Self> {
if value >= 128 {
Some(Self(value))
} else {
None
}
}
#[deprecated(since = "0.36.0", note = "use `Encryption::from_byte`")]
#[allow(clippy::should_implement_trait)]
#[inline]
pub const fn try_from(value: u8) -> Result<Self, UnknownValueError> {
Ok(Self::from_byte(value))
}
#[inline]
pub const fn is_reserved(self) -> bool {
!matches!(self.0, 0..=2) && self.0 < 128
}
#[inline]
pub const fn is_private(self) -> bool {
self.0 >= 128
}
}
impl fmt::Debug for Encryption {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::NO => f.write_str("No"),
Self::AES => f.write_str("Aes"),
Self::CAMELLIA => f.write_str("Camellia"),
Self(v) if v < 128 => f.debug_tuple("Reserved").field(&v).finish(),
Self(v) => f.debug_tuple("Private").field(&v).finish(),
}
}
}
impl TryFrom<u8> for Encryption {
type Error = UnknownValueError;
#[inline]
fn try_from(value: u8) -> Result<Self, Self::Error> {
Ok(Self::from_byte(value))
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct CipherMode(u8);
impl CipherMode {
pub const CBC: Self = Self(0);
pub const CTR: Self = Self(1);
#[inline]
pub const fn from_byte(value: u8) -> Self {
Self(value)
}
#[inline]
pub const fn to_byte(self) -> u8 {
self.0
}
#[inline]
pub const fn new_private(value: u8) -> Option<Self> {
if value >= 128 {
Some(Self(value))
} else {
None
}
}
#[deprecated(since = "0.36.0", note = "use `CipherMode::from_byte`")]
#[allow(clippy::should_implement_trait)]
#[inline]
pub const fn try_from(value: u8) -> Result<Self, UnknownValueError> {
Ok(Self::from_byte(value))
}
#[inline]
pub const fn is_reserved(self) -> bool {
!matches!(self.0, 0..=1) && self.0 < 128
}
#[inline]
pub const fn is_private(self) -> bool {
self.0 >= 128
}
}
impl fmt::Debug for CipherMode {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::CBC => f.write_str("CBC"),
Self::CTR => f.write_str("CTR"),
Self(v) if v < 128 => f.debug_tuple("Reserved").field(&v).finish(),
Self(v) => f.debug_tuple("Private").field(&v).finish(),
}
}
}
impl TryFrom<u8> for CipherMode {
type Error = UnknownValueError;
#[inline]
fn try_from(value: u8) -> Result<Self, Self::Error> {
Ok(Self::from_byte(value))
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub(crate) enum HashAlgorithmParams {
Pbkdf2Sha256 {
rounds: Option<u32>,
},
Argon2Id {
time_cost: Option<u32>,
memory_cost: Option<u32>,
parallelism_cost: Option<u32>,
},
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub struct HashAlgorithm(pub(crate) HashAlgorithmParams);
impl HashAlgorithm {
#[inline]
pub const fn pbkdf2_sha256() -> Self {
Self::pbkdf2_sha256_with(None)
}
#[inline]
pub const fn pbkdf2_sha256_with(rounds: Option<u32>) -> Self {
Self(HashAlgorithmParams::Pbkdf2Sha256 { rounds })
}
#[inline]
pub const fn argon2id() -> Self {
Self::argon2id_with(None, None, None)
}
#[inline]
pub const fn argon2id_with(
time_cost: Option<u32>,
memory_cost: Option<u32>,
parallelism_cost: Option<u32>,
) -> Self {
Self(HashAlgorithmParams::Argon2Id {
time_cost,
memory_cost,
parallelism_cost,
})
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct DataKind(u8);
impl DataKind {
pub const FILE: Self = Self(0);
pub const DIRECTORY: Self = Self(1);
pub const SYMBOLIC_LINK: Self = Self(2);
pub const HARD_LINK: Self = Self(3);
#[deprecated(since = "0.36.0", note = "renamed to `DataKind::FILE`")]
#[allow(non_upper_case_globals)]
pub const File: Self = Self::FILE;
#[deprecated(since = "0.36.0", note = "renamed to `DataKind::DIRECTORY`")]
#[allow(non_upper_case_globals)]
pub const Directory: Self = Self::DIRECTORY;
#[deprecated(since = "0.36.0", note = "renamed to `DataKind::SYMBOLIC_LINK`")]
#[allow(non_upper_case_globals)]
pub const SymbolicLink: Self = Self::SYMBOLIC_LINK;
#[deprecated(since = "0.36.0", note = "renamed to `DataKind::HARD_LINK`")]
#[allow(non_upper_case_globals)]
pub const HardLink: Self = Self::HARD_LINK;
#[inline]
pub const fn from_byte(value: u8) -> Self {
Self(value)
}
#[inline]
pub const fn to_byte(self) -> u8 {
self.0
}
#[inline]
pub const fn new_private(value: u8) -> Option<Self> {
if value >= 128 {
Some(Self(value))
} else {
None
}
}
#[deprecated(since = "0.36.0", note = "use `DataKind::from_byte`")]
#[allow(clippy::should_implement_trait)]
#[inline]
pub const fn try_from(value: u8) -> Result<Self, UnknownValueError> {
Ok(Self::from_byte(value))
}
#[inline]
pub const fn is_reserved(self) -> bool {
!matches!(self.0, 0..=3) && self.0 < 128
}
#[inline]
pub const fn is_private(self) -> bool {
self.0 >= 128
}
}
impl fmt::Debug for DataKind {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::FILE => f.write_str("File"),
Self::DIRECTORY => f.write_str("Directory"),
Self::SYMBOLIC_LINK => f.write_str("SymbolicLink"),
Self::HARD_LINK => f.write_str("HardLink"),
Self(v) if v < 128 => f.debug_tuple("Reserved").field(&v).finish(),
Self(v) => f.debug_tuple("Private").field(&v).finish(),
}
}
}
impl TryFrom<u8> for DataKind {
type Error = UnknownValueError;
#[inline]
fn try_from(value: u8) -> Result<Self, Self::Error> {
Ok(Self::from_byte(value))
}
}
#[derive(Clone, Debug)]
pub struct WriteOptions {
compress: Compress,
cipher: Option<Cipher>,
}
impl WriteOptions {
#[inline]
pub const fn store() -> Self {
Self {
compress: Compress::No,
cipher: None,
}
}
#[inline]
pub const fn builder() -> WriteOptionsBuilder {
WriteOptionsBuilder::new()
}
#[inline]
pub fn into_builder(self) -> WriteOptionsBuilder {
self.into()
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub struct WriteOptionsBuilder {
compression: Compression,
compression_level: CompressionLevel,
encryption: Encryption,
cipher_mode: CipherMode,
hash_algorithm: HashAlgorithm,
password: Option<Vec<u8>>,
}
impl Default for WriteOptionsBuilder {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl From<WriteOptions> for WriteOptionsBuilder {
#[inline]
fn from(value: WriteOptions) -> Self {
let (compression, compression_level) = match value.compress {
Compress::No => (Compression::NO, CompressionLevel::DEFAULT),
Compress::Deflate(level) => (Compression::DEFLATE, level.into()),
Compress::ZStandard(level) => (Compression::ZSTANDARD, level.into()),
Compress::XZ(level) => (Compression::XZ, level.into()),
};
Self {
compression,
compression_level,
encryption: value.encryption(),
cipher_mode: value.cipher_mode(),
hash_algorithm: value.hash_algorithm(),
password: value.password().map(|p| p.to_vec()),
}
}
}
impl WriteOptionsBuilder {
const fn new() -> Self {
Self {
compression: Compression::NO,
compression_level: CompressionLevel::DEFAULT,
encryption: Encryption::NO,
cipher_mode: CipherMode::CTR,
hash_algorithm: HashAlgorithm::argon2id(),
password: None,
}
}
#[inline]
pub fn compression(&mut self, compression: Compression) -> &mut Self {
self.compression = compression;
self
}
#[inline]
pub fn compression_level(&mut self, compression_level: CompressionLevel) -> &mut Self {
self.compression_level = compression_level;
self
}
#[inline]
pub fn encryption(&mut self, encryption: Encryption) -> &mut Self {
self.encryption = encryption;
self
}
#[inline]
pub fn cipher_mode(&mut self, cipher_mode: CipherMode) -> &mut Self {
self.cipher_mode = cipher_mode;
self
}
#[inline]
pub fn hash_algorithm(&mut self, algorithm: HashAlgorithm) -> &mut Self {
self.hash_algorithm = algorithm;
self
}
#[inline]
pub fn password<B: AsRef<[u8]>>(&mut self, password: Option<B>) -> &mut Self {
self.password = password.map(|it| it.as_ref().to_vec());
self
}
#[inline]
#[must_use = "building options without using them is wasteful"]
pub fn try_build(&self) -> io::Result<WriteOptions> {
let cipher = if self.encryption != Encryption::NO {
let cipher_algorithm = match self.encryption {
Encryption::AES => CipherAlgorithm::Aes,
Encryption::CAMELLIA => CipherAlgorithm::Camellia,
other => {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
format!(
"unsupported encryption method for writing: byte={}",
other.to_byte()
),
));
}
};
let password = self.password.as_deref().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "Password was not provided.")
})?;
let derived = derive_key_material(cipher_algorithm, self.hash_algorithm, password)?;
Some(Cipher::new(
password.into(),
derived,
self.hash_algorithm,
cipher_algorithm,
self.cipher_mode,
))
} else {
None
};
Ok(WriteOptions {
compress: match self.compression {
Compression::NO => Compress::No,
Compression::DEFLATE => Compress::Deflate(self.compression_level.into()),
Compression::ZSTANDARD => Compress::ZStandard(self.compression_level.into()),
Compression::XZ => Compress::XZ(self.compression_level.into()),
other => {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
format!(
"unsupported compression method for writing: byte={}",
other.to_byte()
),
));
}
},
cipher,
})
}
#[inline]
#[must_use = "building options without using them is wasteful"]
pub fn build(&self) -> WriteOptions {
match self.try_build() {
Ok(options) => options,
Err(e) => panic!("{e}"),
}
}
}
#[derive(Clone, Debug)]
pub struct ReadOptions {
password: Option<Vec<u8>>,
key_cache: KeyCache,
}
impl ReadOptions {
#[inline]
pub fn with_password<B: AsRef<[u8]>>(password: Option<B>) -> Self {
Self {
password: password.map(|p| p.as_ref().to_vec()),
key_cache: KeyCache::new(),
}
}
#[inline]
pub const fn builder() -> ReadOptionsBuilder {
ReadOptionsBuilder::new()
}
#[inline]
pub fn into_builder(self) -> ReadOptionsBuilder {
self.into()
}
#[cfg(test)]
pub(crate) fn cached_key_count(&self) -> usize {
self.key_cache.len()
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub struct ReadOptionsBuilder {
password: Option<Vec<u8>>,
}
impl From<ReadOptions> for ReadOptionsBuilder {
#[inline]
fn from(value: ReadOptions) -> Self {
Self {
password: value.password,
}
}
}
impl ReadOptionsBuilder {
#[inline]
const fn new() -> Self {
Self { password: None }
}
#[inline]
#[must_use = "building options without using them is wasteful"]
pub fn build(&self) -> ReadOptions {
ReadOptions {
password: self.password.clone(),
key_cache: KeyCache::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
use wasm_bindgen_test::wasm_bindgen_test as test;
#[test]
fn try_build_derives_key_at_build() {
let options = WriteOptions::builder()
.encryption(Encryption::AES)
.password(Some("password"))
.try_build()
.unwrap();
let cipher = options.cipher.unwrap();
assert!(!cipher.derived.phsf.is_empty());
assert_eq!(cipher.derived.key.len(), 32);
}
#[test]
fn each_build_generates_fresh_salt() {
let mut builder = WriteOptions::builder();
builder
.encryption(Encryption::AES)
.password(Some("password"));
let first = builder.try_build().unwrap();
let second = builder.try_build().unwrap();
assert_ne!(
first.cipher.unwrap().derived.phsf,
second.cipher.unwrap().derived.phsf,
);
}
#[test]
fn try_build_without_password_returns_error() {
let err = WriteOptions::builder()
.encryption(Encryption::AES)
.try_build()
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
}
#[test]
fn try_build_with_invalid_kdf_params_returns_error() {
let result = WriteOptions::builder()
.encryption(Encryption::AES)
.hash_algorithm(HashAlgorithm::argon2id_with(Some(0), None, None))
.password(Some("password"))
.try_build();
assert!(result.is_err());
}
#[test]
#[should_panic(expected = "Password was not provided.")]
fn build_without_password_panics() {
let _ = WriteOptions::builder().encryption(Encryption::AES).build();
}
fn test_output(byte: u8) -> Output {
Output::new(&[byte; 32]).unwrap()
}
#[test]
fn key_cache_returns_inserted_key() {
let cache = KeyCache::new();
assert!(cache.get("phsf-a").is_none());
cache.insert("phsf-a", test_output(1));
assert_eq!(cache.get("phsf-a").unwrap(), test_output(1));
}
#[test]
fn key_cache_clears_when_full() {
let cache = KeyCache::new();
for i in 0..16 {
cache.insert(&format!("phsf-{i}"), test_output(i as u8));
}
assert_eq!(cache.len(), 16);
cache.insert("phsf-16", test_output(16));
assert_eq!(cache.len(), 1);
assert!(cache.get("phsf-0").is_none());
assert_eq!(cache.get("phsf-16").unwrap(), test_output(16));
}
#[test]
fn read_options_clone_shares_key_cache() {
let options = ReadOptions::with_password(Some("password"));
let cloned = options.clone();
cloned.key_cache.insert("phsf-a", test_output(1));
assert_eq!(options.cached_key_count(), 1);
}
#[test]
fn rebuilt_read_options_starts_with_empty_cache() {
let options = ReadOptions::with_password(Some("password"));
options.key_cache.insert("phsf-a", test_output(1));
let rebuilt = options.clone().into_builder().build();
assert_eq!(rebuilt.cached_key_count(), 0);
}
#[test]
fn data_kind_round_trips_all_byte_values() {
for v in 0..=u8::MAX {
assert_eq!(DataKind::from_byte(v).to_byte(), v);
}
}
#[test]
fn data_kind_known_constants_map_to_spec_bytes() {
assert_eq!(DataKind::FILE.to_byte(), 0);
assert_eq!(DataKind::DIRECTORY.to_byte(), 1);
assert_eq!(DataKind::SYMBOLIC_LINK.to_byte(), 2);
assert_eq!(DataKind::HARD_LINK.to_byte(), 3);
}
#[test]
fn data_kind_new_private_boundary() {
assert_eq!(DataKind::new_private(0), None);
assert_eq!(DataKind::new_private(127), None);
assert_eq!(DataKind::new_private(128), Some(DataKind::from_byte(128)));
assert_eq!(DataKind::new_private(255), Some(DataKind::from_byte(255)));
}
#[test]
fn data_kind_predicates() {
assert!(!DataKind::FILE.is_reserved());
assert!(!DataKind::FILE.is_private());
assert!(!DataKind::HARD_LINK.is_reserved());
assert!(DataKind::from_byte(4).is_reserved());
assert!(DataKind::from_byte(127).is_reserved());
assert!(!DataKind::from_byte(127).is_private());
assert!(!DataKind::from_byte(128).is_reserved());
assert!(DataKind::from_byte(128).is_private());
assert!(DataKind::from_byte(255).is_private());
}
#[test]
fn data_kind_debug_matches_former_enum_output() {
assert_eq!(format!("{:?}", DataKind::FILE), "File");
assert_eq!(format!("{:?}", DataKind::DIRECTORY), "Directory");
assert_eq!(format!("{:?}", DataKind::SYMBOLIC_LINK), "SymbolicLink");
assert_eq!(format!("{:?}", DataKind::HARD_LINK), "HardLink");
assert_eq!(format!("{:?}", DataKind::from_byte(5)), "Reserved(5)");
assert_eq!(format!("{:?}", DataKind::from_byte(200)), "Private(200)");
}
#[allow(deprecated)]
#[test]
fn data_kind_deprecated_aliases_equal_new_constants() {
assert_eq!(DataKind::File, DataKind::FILE);
assert_eq!(DataKind::Directory, DataKind::DIRECTORY);
assert_eq!(DataKind::SymbolicLink, DataKind::SYMBOLIC_LINK);
assert_eq!(DataKind::HardLink, DataKind::HARD_LINK);
}
#[allow(deprecated)]
#[test]
fn data_kind_try_from_is_infallible_and_matches_from_byte() {
for v in 0..=u8::MAX {
assert_eq!(DataKind::try_from(v).unwrap(), DataKind::from_byte(v));
assert_eq!(
<DataKind as TryFrom<u8>>::try_from(v).unwrap(),
DataKind::from_byte(v)
);
}
}
#[test]
fn compression_round_trips_all_byte_values() {
for v in 0..=u8::MAX {
assert_eq!(Compression::from_byte(v).to_byte(), v);
}
}
#[test]
fn compression_known_constants_map_to_spec_bytes() {
assert_eq!(Compression::NO.to_byte(), 0);
assert_eq!(Compression::DEFLATE.to_byte(), 1);
assert_eq!(Compression::ZSTANDARD.to_byte(), 2);
assert_eq!(Compression::XZ.to_byte(), 4);
}
#[test]
fn compression_new_private_boundary() {
assert_eq!(Compression::new_private(0), None);
assert_eq!(Compression::new_private(127), None);
assert_eq!(
Compression::new_private(128),
Some(Compression::from_byte(128))
);
assert_eq!(
Compression::new_private(255),
Some(Compression::from_byte(255))
);
}
#[test]
fn compression_predicates() {
assert!(!Compression::NO.is_reserved());
assert!(!Compression::XZ.is_reserved());
assert!(!Compression::XZ.is_private());
assert!(Compression::from_byte(3).is_reserved());
assert!(Compression::from_byte(127).is_reserved());
assert!(!Compression::from_byte(127).is_private());
assert!(!Compression::from_byte(128).is_reserved());
assert!(Compression::from_byte(128).is_private());
assert!(Compression::from_byte(255).is_private());
}
#[test]
fn compression_debug_matches_former_enum_output() {
assert_eq!(format!("{:?}", Compression::NO), "No");
assert_eq!(format!("{:?}", Compression::DEFLATE), "Deflate");
assert_eq!(format!("{:?}", Compression::ZSTANDARD), "ZStandard");
assert_eq!(format!("{:?}", Compression::XZ), "XZ");
assert_eq!(format!("{:?}", Compression::from_byte(3)), "Reserved(3)");
assert_eq!(format!("{:?}", Compression::from_byte(200)), "Private(200)");
}
#[allow(deprecated)]
#[test]
fn compression_deprecated_aliases_equal_new_constants() {
assert_eq!(Compression::No, Compression::NO);
assert_eq!(Compression::Deflate, Compression::DEFLATE);
assert_eq!(Compression::ZStandard, Compression::ZSTANDARD);
}
#[allow(deprecated)]
#[test]
fn compression_try_from_is_infallible_and_matches_from_byte() {
for v in 0..=u8::MAX {
assert_eq!(Compression::try_from(v).unwrap(), Compression::from_byte(v));
assert_eq!(
<Compression as TryFrom<u8>>::try_from(v).unwrap(),
Compression::from_byte(v)
);
}
}
#[test]
fn try_build_with_unknown_compression_returns_unsupported() {
let err = WriteOptions::builder()
.compression(Compression::from_byte(5))
.try_build()
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::Unsupported);
}
#[test]
fn encryption_round_trips_all_byte_values() {
for v in 0..=u8::MAX {
assert_eq!(Encryption::from_byte(v).to_byte(), v);
}
}
#[test]
fn encryption_known_constants_map_to_spec_bytes() {
assert_eq!(Encryption::NO.to_byte(), 0);
assert_eq!(Encryption::AES.to_byte(), 1);
assert_eq!(Encryption::CAMELLIA.to_byte(), 2);
}
#[test]
fn encryption_new_private_boundary() {
assert_eq!(Encryption::new_private(0), None);
assert_eq!(Encryption::new_private(127), None);
assert_eq!(
Encryption::new_private(128),
Some(Encryption::from_byte(128))
);
assert_eq!(
Encryption::new_private(255),
Some(Encryption::from_byte(255))
);
}
#[test]
fn encryption_predicates() {
assert!(!Encryption::NO.is_reserved());
assert!(!Encryption::CAMELLIA.is_reserved());
assert!(!Encryption::CAMELLIA.is_private());
assert!(Encryption::from_byte(3).is_reserved());
assert!(Encryption::from_byte(127).is_reserved());
assert!(!Encryption::from_byte(127).is_private());
assert!(!Encryption::from_byte(128).is_reserved());
assert!(Encryption::from_byte(128).is_private());
assert!(Encryption::from_byte(255).is_private());
}
#[test]
fn encryption_debug_matches_former_enum_output() {
assert_eq!(format!("{:?}", Encryption::NO), "No");
assert_eq!(format!("{:?}", Encryption::AES), "Aes");
assert_eq!(format!("{:?}", Encryption::CAMELLIA), "Camellia");
assert_eq!(format!("{:?}", Encryption::from_byte(3)), "Reserved(3)");
assert_eq!(format!("{:?}", Encryption::from_byte(200)), "Private(200)");
}
#[allow(deprecated)]
#[test]
fn encryption_deprecated_aliases_equal_new_constants() {
assert_eq!(Encryption::No, Encryption::NO);
assert_eq!(Encryption::Aes, Encryption::AES);
assert_eq!(Encryption::Camellia, Encryption::CAMELLIA);
}
#[allow(deprecated)]
#[test]
fn encryption_try_from_is_infallible_and_matches_from_byte() {
for v in 0..=u8::MAX {
assert_eq!(Encryption::try_from(v).unwrap(), Encryption::from_byte(v));
assert_eq!(
<Encryption as TryFrom<u8>>::try_from(v).unwrap(),
Encryption::from_byte(v)
);
}
}
#[test]
fn try_build_with_unknown_encryption_returns_unsupported() {
let err = WriteOptions::builder()
.encryption(Encryption::from_byte(5))
.password(Some("password"))
.try_build()
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::Unsupported);
}
#[test]
fn cipher_mode_round_trips_all_byte_values() {
for v in 0..=u8::MAX {
assert_eq!(CipherMode::from_byte(v).to_byte(), v);
}
}
#[test]
fn cipher_mode_known_constants_map_to_spec_bytes() {
assert_eq!(CipherMode::CBC.to_byte(), 0);
assert_eq!(CipherMode::CTR.to_byte(), 1);
}
#[test]
fn cipher_mode_new_private_boundary() {
assert_eq!(CipherMode::new_private(0), None);
assert_eq!(CipherMode::new_private(127), None);
assert_eq!(
CipherMode::new_private(128),
Some(CipherMode::from_byte(128))
);
assert_eq!(
CipherMode::new_private(255),
Some(CipherMode::from_byte(255))
);
}
#[test]
fn cipher_mode_predicates() {
assert!(!CipherMode::CBC.is_reserved());
assert!(!CipherMode::CTR.is_reserved());
assert!(!CipherMode::CTR.is_private());
assert!(CipherMode::from_byte(2).is_reserved());
assert!(CipherMode::from_byte(127).is_reserved());
assert!(!CipherMode::from_byte(127).is_private());
assert!(!CipherMode::from_byte(128).is_reserved());
assert!(CipherMode::from_byte(128).is_private());
assert!(CipherMode::from_byte(255).is_private());
}
#[test]
fn cipher_mode_debug_matches_former_enum_output() {
assert_eq!(format!("{:?}", CipherMode::CBC), "CBC");
assert_eq!(format!("{:?}", CipherMode::CTR), "CTR");
assert_eq!(format!("{:?}", CipherMode::from_byte(2)), "Reserved(2)");
assert_eq!(format!("{:?}", CipherMode::from_byte(200)), "Private(200)");
}
#[allow(deprecated)]
#[test]
fn cipher_mode_try_from_is_infallible_and_matches_from_byte() {
for v in 0..=u8::MAX {
assert_eq!(CipherMode::try_from(v).unwrap(), CipherMode::from_byte(v));
assert_eq!(
<CipherMode as TryFrom<u8>>::try_from(v).unwrap(),
CipherMode::from_byte(v)
);
}
}
}