Skip to content

Commit dbba0ee

Browse files
committed
💥 Replace CipherMode enum with byte-backed newtype
1 parent 176a25a commit dbba0ee

3 files changed

Lines changed: 134 additions & 30 deletions

File tree

lib/src/entry/header.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,7 @@ impl EntryHeader {
148148
data_kind: DataKind::from_byte(bytes[2]),
149149
compression: Compression::from_byte(bytes[3]),
150150
encryption: Encryption::from_byte(bytes[4]),
151-
cipher_mode: CipherMode::try_from(bytes[5])
152-
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?,
151+
cipher_mode: CipherMode::from_byte(bytes[5]),
153152
sanitized_path: OnceLock::new(),
154153
name: path,
155154
};
@@ -284,8 +283,7 @@ impl SolidHeader {
284283
minor: bytes[1],
285284
compression: Compression::from_byte(bytes[2]),
286285
encryption: Encryption::from_byte(bytes[3]),
287-
cipher_mode: CipherMode::try_from(bytes[4])
288-
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?,
286+
cipher_mode: CipherMode::from_byte(bytes[4]),
289287
})
290288
}
291289
}

lib/src/entry/options.rs

Lines changed: 131 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -579,53 +579,99 @@ impl TryFrom<u8> for Encryption {
579579
}
580580

581581
/// Cipher mode of encryption algorithm.
582-
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
583-
pub enum CipherMode {
582+
///
583+
/// Values without an associated constant are either reserved for future
584+
/// PNA specification (raw value < 128) or application-specific private
585+
/// values (raw value >= 128).
586+
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
587+
pub struct CipherMode(u8);
588+
589+
impl CipherMode {
584590
/// Cipher Block Chaining mode.
585-
CBC,
591+
pub const CBC: Self = Self(0);
586592
/// Counter mode.
587-
CTR,
588-
/// Value reserved for future PNA specification (raw value < 128).
589-
Reserved(u8),
590-
/// Application-specific private value (raw value >= 128).
591-
Private(u8),
592-
}
593+
pub const CTR: Self = Self(1);
593594

594-
impl CipherMode {
595-
/// Serialize this cipher mode to its u8 representation.
595+
/// Deserializes a cipher mode from its u8 representation.
596+
///
597+
/// Every byte value is a valid cipher mode value, so this conversion
598+
/// never fails.
599+
#[inline]
600+
pub const fn from_byte(value: u8) -> Self {
601+
Self(value)
602+
}
603+
604+
/// Serializes this cipher mode to its u8 representation.
596605
#[inline]
597606
pub const fn to_byte(self) -> u8 {
598-
match self {
599-
Self::CBC => 0,
600-
Self::CTR => 1,
601-
Self::Reserved(v) | Self::Private(v) => v,
607+
self.0
608+
}
609+
610+
/// Creates an application-specific private value.
611+
///
612+
/// Returns `Some` if `value` is in the private range (`128..=255`),
613+
/// otherwise `None`.
614+
#[inline]
615+
pub const fn new_private(value: u8) -> Option<Self> {
616+
if value >= 128 {
617+
Some(Self(value))
618+
} else {
619+
None
602620
}
603621
}
604622

605-
/// Returns `true` if this is a reserved value.
623+
/// Converts a `u8` into a [`CipherMode`]. Never fails.
624+
///
625+
/// Shadows `<CipherMode as TryFrom<u8>>::try_from` so existing
626+
/// `CipherMode::try_from(..)` call sites receive a deprecation warning;
627+
/// both will be removed together in a future release.
628+
///
629+
/// # Errors
630+
///
631+
/// Never returns `Err`; every byte value is a valid cipher mode.
632+
#[deprecated(since = "0.36.0", note = "use `CipherMode::from_byte`")]
633+
#[allow(clippy::should_implement_trait)]
634+
#[inline]
635+
pub const fn try_from(value: u8) -> Result<Self, UnknownValueError> {
636+
Ok(Self::from_byte(value))
637+
}
638+
639+
/// Returns `true` if this value is reserved for future PNA specification
640+
/// (unassigned and raw value < 128).
606641
#[inline]
607642
pub const fn is_reserved(self) -> bool {
608-
matches!(self, Self::Reserved(_))
643+
!matches!(self.0, 0..=1) && self.0 < 128
609644
}
610645

611-
/// Returns `true` if this is a private value.
646+
/// Returns `true` if this is an application-specific private value
647+
/// (raw value >= 128).
612648
#[inline]
613649
pub const fn is_private(self) -> bool {
614-
matches!(self, Self::Private(_))
650+
self.0 >= 128
651+
}
652+
}
653+
654+
impl fmt::Debug for CipherMode {
655+
#[inline]
656+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
657+
match *self {
658+
Self::CBC => f.write_str("CBC"),
659+
Self::CTR => f.write_str("CTR"),
660+
Self(v) if v < 128 => f.debug_tuple("Reserved").field(&v).finish(),
661+
Self(v) => f.debug_tuple("Private").field(&v).finish(),
662+
}
615663
}
616664
}
617665

666+
/// Infallible; kept for backward compatibility with the former enum-based
667+
/// API and scheduled for removal in a future release. Use
668+
/// [`CipherMode::from_byte`] instead.
618669
impl TryFrom<u8> for CipherMode {
619670
type Error = UnknownValueError;
620671

621672
#[inline]
622673
fn try_from(value: u8) -> Result<Self, Self::Error> {
623-
match value {
624-
0 => Ok(Self::CBC),
625-
1 => Ok(Self::CTR),
626-
v if v < 128 => Ok(Self::Reserved(v)),
627-
v => Ok(Self::Private(v)),
628-
}
674+
Ok(Self::from_byte(value))
629675
}
630676
}
631677

@@ -1665,4 +1711,64 @@ mod tests {
16651711
.unwrap_err();
16661712
assert_eq!(err.kind(), io::ErrorKind::Unsupported);
16671713
}
1714+
1715+
#[test]
1716+
fn cipher_mode_round_trips_all_byte_values() {
1717+
for v in 0..=u8::MAX {
1718+
assert_eq!(CipherMode::from_byte(v).to_byte(), v);
1719+
}
1720+
}
1721+
1722+
#[test]
1723+
fn cipher_mode_known_constants_map_to_spec_bytes() {
1724+
assert_eq!(CipherMode::CBC.to_byte(), 0);
1725+
assert_eq!(CipherMode::CTR.to_byte(), 1);
1726+
}
1727+
1728+
#[test]
1729+
fn cipher_mode_new_private_boundary() {
1730+
assert_eq!(CipherMode::new_private(0), None);
1731+
assert_eq!(CipherMode::new_private(127), None);
1732+
assert_eq!(
1733+
CipherMode::new_private(128),
1734+
Some(CipherMode::from_byte(128))
1735+
);
1736+
assert_eq!(
1737+
CipherMode::new_private(255),
1738+
Some(CipherMode::from_byte(255))
1739+
);
1740+
}
1741+
1742+
#[test]
1743+
fn cipher_mode_predicates() {
1744+
assert!(!CipherMode::CBC.is_reserved());
1745+
assert!(!CipherMode::CTR.is_reserved());
1746+
assert!(!CipherMode::CTR.is_private());
1747+
assert!(CipherMode::from_byte(2).is_reserved());
1748+
assert!(CipherMode::from_byte(127).is_reserved());
1749+
assert!(!CipherMode::from_byte(127).is_private());
1750+
assert!(!CipherMode::from_byte(128).is_reserved());
1751+
assert!(CipherMode::from_byte(128).is_private());
1752+
assert!(CipherMode::from_byte(255).is_private());
1753+
}
1754+
1755+
#[test]
1756+
fn cipher_mode_debug_matches_former_enum_output() {
1757+
assert_eq!(format!("{:?}", CipherMode::CBC), "CBC");
1758+
assert_eq!(format!("{:?}", CipherMode::CTR), "CTR");
1759+
assert_eq!(format!("{:?}", CipherMode::from_byte(2)), "Reserved(2)");
1760+
assert_eq!(format!("{:?}", CipherMode::from_byte(200)), "Private(200)");
1761+
}
1762+
1763+
#[allow(deprecated)]
1764+
#[test]
1765+
fn cipher_mode_try_from_is_infallible_and_matches_from_byte() {
1766+
for v in 0..=u8::MAX {
1767+
assert_eq!(CipherMode::try_from(v).unwrap(), CipherMode::from_byte(v));
1768+
assert_eq!(
1769+
<CipherMode as TryFrom<u8>>::try_from(v).unwrap(),
1770+
CipherMode::from_byte(v)
1771+
);
1772+
}
1773+
}
16681774
}

lib/src/entry/read.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ mod tests {
138138
let result = decrypt_reader(
139139
io::Cursor::new(Vec::<u8>::new()),
140140
Encryption::CAMELLIA,
141-
CipherMode::Reserved(3),
141+
CipherMode::from_byte(3),
142142
Some(phsf),
143143
Some(b"password"),
144144
None,

0 commit comments

Comments
 (0)