Skip to main content

libpna/entry/
options.rs

1use crate::{compress, error::UnknownValueError};
2pub(crate) use private::*;
3use std::str::FromStr;
4
5mod private {
6    use super::*;
7
8    /// Compression options.
9    #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
10    pub enum Compress {
11        No,
12        Deflate(compress::deflate::DeflateCompressionLevel),
13        ZStandard(compress::zstandard::ZstdCompressionLevel),
14        XZ(compress::xz::XZCompressionLevel),
15    }
16
17    /// Cipher options.
18    #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
19    pub struct Cipher {
20        pub(crate) password: Password,
21        pub(crate) hash_algorithm: HashAlgorithm,
22        pub(crate) cipher_algorithm: CipherAlgorithm,
23        pub(crate) mode: CipherMode,
24    }
25
26    impl Cipher {
27        /// Creates a new [Cipher]
28        #[inline]
29        pub(crate) const fn new(
30            password: Password,
31            hash_algorithm: HashAlgorithm,
32            cipher_algorithm: CipherAlgorithm,
33            mode: CipherMode,
34        ) -> Self {
35            Self {
36                password,
37                hash_algorithm,
38                cipher_algorithm,
39                mode,
40            }
41        }
42    }
43
44    /// Accessors for write options.
45    pub trait WriteOption {
46        fn compress(&self) -> Compress;
47        fn cipher(&self) -> Option<&Cipher>;
48        #[inline]
49        fn compression(&self) -> Compression {
50            match self.compress() {
51                Compress::No => Compression::No,
52                Compress::Deflate(_) => Compression::Deflate,
53                Compress::ZStandard(_) => Compression::ZStandard,
54                Compress::XZ(_) => Compression::XZ,
55            }
56        }
57
58        #[inline]
59        fn encryption(&self) -> Encryption {
60            self.cipher()
61                .map_or(Encryption::No, |it| match it.cipher_algorithm {
62                    CipherAlgorithm::Aes => Encryption::Aes,
63                    CipherAlgorithm::Camellia => Encryption::Camellia,
64                })
65        }
66
67        #[inline]
68        fn cipher_mode(&self) -> CipherMode {
69            self.cipher().map_or(CipherMode::CTR, |it| it.mode)
70        }
71
72        #[inline]
73        fn hash_algorithm(&self) -> HashAlgorithm {
74            self.cipher()
75                .map_or_else(HashAlgorithm::argon2id, |it| it.hash_algorithm)
76        }
77
78        #[inline]
79        fn password(&self) -> Option<&[u8]> {
80            self.cipher().map(|it| it.password.as_bytes())
81        }
82    }
83
84    impl WriteOption for WriteOptions {
85        #[inline]
86        fn compress(&self) -> Compress {
87            self.compress
88        }
89
90        #[inline]
91        fn cipher(&self) -> Option<&Cipher> {
92            self.cipher.as_ref()
93        }
94    }
95
96    impl<T> WriteOption for &T
97    where
98        T: WriteOption,
99    {
100        #[inline]
101        fn compress(&self) -> Compress {
102            T::compress(self)
103        }
104
105        #[inline]
106        fn cipher(&self) -> Option<&Cipher> {
107            T::cipher(self)
108        }
109    }
110
111    /// Entry read option getter trait.
112    pub trait ReadOption {
113        fn password(&self) -> Option<&[u8]>;
114    }
115
116    impl<T: ReadOption> ReadOption for &T {
117        #[inline]
118        fn password(&self) -> Option<&[u8]> {
119            T::password(self)
120        }
121    }
122
123    impl ReadOption for ReadOptions {
124        #[inline]
125        fn password(&self) -> Option<&[u8]> {
126            self.password.as_deref()
127        }
128    }
129}
130
131/// Compression method.
132#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
133#[repr(u8)]
134pub enum Compression {
135    /// Do not apply any compression.
136    No = 0,
137    /// Zlib format.
138    Deflate = 1,
139    /// ZStandard format.
140    ZStandard = 2,
141    /// Xz format.
142    XZ = 4,
143}
144
145impl TryFrom<u8> for Compression {
146    type Error = UnknownValueError;
147
148    #[inline]
149    fn try_from(value: u8) -> Result<Self, Self::Error> {
150        match value {
151            0 => Ok(Self::No),
152            1 => Ok(Self::Deflate),
153            2 => Ok(Self::ZStandard),
154            4 => Ok(Self::XZ),
155            value => Err(UnknownValueError(value)),
156        }
157    }
158}
159
160#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
161pub(crate) enum CompressionLevelImpl {
162    /// Minimum compression level.
163    Min,
164    /// Maximum compression level.
165    Max,
166    /// Default compression level.
167    Default,
168    /// Custom compression level.
169    Custom(i64),
170}
171
172impl FromStr for CompressionLevelImpl {
173    type Err = core::num::ParseIntError;
174
175    #[inline]
176    fn from_str(s: &str) -> Result<Self, Self::Err> {
177        if s.eq_ignore_ascii_case("min") {
178            Ok(Self::Min)
179        } else if s.eq_ignore_ascii_case("max") {
180            Ok(Self::Max)
181        } else if s.eq_ignore_ascii_case("default") {
182            Ok(Self::Default)
183        } else {
184            Ok(Self::Custom(i64::from_str(s)?))
185        }
186    }
187}
188
189/// Compression level of each algorithm.
190#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
191pub struct CompressionLevel(pub(crate) CompressionLevelImpl);
192
193impl CompressionLevel {
194    pub(crate) const DEFAULT: Self = Self(CompressionLevelImpl::Default);
195
196    /// Minimum compression level.
197    /// This value will be replaced with the minimum level for each algorithm.
198    #[inline]
199    pub const fn min() -> Self {
200        Self(CompressionLevelImpl::Min)
201    }
202
203    /// Maximum compression level.
204    /// This value will be replaced with the maximum level for each algorithm.
205    #[inline]
206    pub const fn max() -> Self {
207        Self(CompressionLevelImpl::Max)
208    }
209}
210
211impl Default for CompressionLevel {
212    #[inline]
213    fn default() -> Self {
214        Self::DEFAULT
215    }
216}
217
218impl<T: Into<i64>> From<T> for CompressionLevel {
219    #[inline]
220    fn from(value: T) -> Self {
221        Self(CompressionLevelImpl::Custom(value.into()))
222    }
223}
224
225impl FromStr for CompressionLevel {
226    type Err = core::num::ParseIntError;
227
228    /// Parses a string `s` to return a value of this type.
229    ///
230    /// If parsing succeeds, return the value inside [`Ok`], otherwise
231    /// when the string is ill-formatted return an error specific to the
232    /// inside [`Err`]. The error type is specific to the implementation of the trait.
233    ///
234    /// # Examples
235    ///
236    /// ```rust
237    /// use libpna::CompressionLevel;
238    /// use std::str::FromStr;
239    ///
240    /// assert_eq!(
241    ///     CompressionLevel::min(),
242    ///     CompressionLevel::from_str("min").unwrap()
243    /// );
244    /// assert_eq!(
245    ///     CompressionLevel::max(),
246    ///     CompressionLevel::from_str("max").unwrap()
247    /// );
248    /// assert_eq!(
249    ///     CompressionLevel::default(),
250    ///     CompressionLevel::from_str("default").unwrap()
251    /// );
252    /// assert_eq!(
253    ///     CompressionLevel::from(3),
254    ///     CompressionLevel::from_str("3").unwrap()
255    /// );
256    /// ```
257    #[inline]
258    fn from_str(s: &str) -> Result<Self, Self::Err> {
259        Ok(Self(CompressionLevelImpl::from_str(s)?))
260    }
261}
262
263/// Cipher algorithm.
264#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
265pub enum CipherAlgorithm {
266    /// Aes algorithm.
267    Aes,
268    /// Camellia algorithm.
269    Camellia,
270}
271
272/// Password.
273#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
274pub(crate) struct Password(Vec<u8>);
275
276impl Password {
277    #[inline]
278    pub(crate) const fn as_bytes(&self) -> &[u8] {
279        self.0.as_slice()
280    }
281}
282
283impl<T: AsRef<[u8]>> From<T> for Password {
284    #[inline]
285    fn from(value: T) -> Self {
286        Self(value.as_ref().to_vec())
287    }
288}
289
290/// Encryption algorithm.
291#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
292#[repr(u8)]
293pub enum Encryption {
294    /// Do not apply any encryption.
295    No = 0,
296    /// Aes algorithm.
297    Aes = 1,
298    /// Camellia algorithm.
299    Camellia = 2,
300}
301
302impl TryFrom<u8> for Encryption {
303    type Error = UnknownValueError;
304
305    #[inline]
306    fn try_from(value: u8) -> Result<Self, Self::Error> {
307        match value {
308            0 => Ok(Self::No),
309            1 => Ok(Self::Aes),
310            2 => Ok(Self::Camellia),
311            value => Err(UnknownValueError(value)),
312        }
313    }
314}
315
316/// Cipher mode of encryption algorithm.
317#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
318#[repr(u8)]
319pub enum CipherMode {
320    /// Cipher Block Chaining Mode
321    CBC = 0,
322    /// Counter Mode
323    CTR = 1,
324}
325
326impl TryFrom<u8> for CipherMode {
327    type Error = UnknownValueError;
328
329    #[inline]
330    fn try_from(value: u8) -> Result<Self, Self::Error> {
331        match value {
332            0 => Ok(Self::CBC),
333            1 => Ok(Self::CTR),
334            value => Err(UnknownValueError(value)),
335        }
336    }
337}
338
339/// Password hash algorithm parameters.
340#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
341pub(crate) enum HashAlgorithmParams {
342    /// Pbkdf2 with sha256
343    Pbkdf2Sha256 {
344        /// Pbkdf2 rounds, if `None` use default rounds.
345        rounds: Option<u32>,
346    },
347    /// Argon2Id
348    Argon2Id {
349        /// Argon2Id time_cost, if `None` use default time_cost.
350        time_cost: Option<u32>,
351        /// Argon2Id memory_cost, if `None` use default memory_cost.
352        memory_cost: Option<u32>,
353        /// Argon2Id parallelism_cost, if `None` use default parallelism_cost.
354        parallelism_cost: Option<u32>,
355    },
356}
357
358/// Password hash algorithm.
359#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
360pub struct HashAlgorithm(pub(crate) HashAlgorithmParams);
361
362impl HashAlgorithm {
363    /// Creates a PBKDF2-SHA256 password hasher with default iterations.
364    ///
365    /// **Note:** Prefer [`argon2id()`](Self::argon2id) for new archives.
366    /// PBKDF2 is provided for compatibility with systems where Argon2 is unavailable.
367    ///
368    /// # Example
369    ///
370    /// ```rust
371    /// use libpna::{WriteOptions, Encryption, HashAlgorithm};
372    ///
373    /// let opts = WriteOptions::builder()
374    ///     .encryption(Encryption::Aes)
375    ///     .hash_algorithm(HashAlgorithm::pbkdf2_sha256())
376    ///     .password(Some("password"))
377    ///     .build();
378    /// ```
379    #[inline]
380    pub const fn pbkdf2_sha256() -> Self {
381        Self::pbkdf2_sha256_with(None)
382    }
383
384    /// Creates a PBKDF2-SHA256 password hasher with custom iteration count.
385    ///
386    /// Higher iteration counts increase security but also increase key derivation time.
387    /// If `rounds` is `None`, the default iteration count is used.
388    ///
389    /// **Note:** Prefer [`argon2id_with()`](Self::argon2id_with) for new archives.
390    ///
391    /// # Example
392    ///
393    /// ```rust
394    /// use libpna::{WriteOptions, Encryption, HashAlgorithm};
395    ///
396    /// let opts = WriteOptions::builder()
397    ///     .encryption(Encryption::Aes)
398    ///     .hash_algorithm(HashAlgorithm::pbkdf2_sha256_with(Some(100_000)))
399    ///     .password(Some("password"))
400    ///     .build();
401    /// ```
402    #[inline]
403    pub const fn pbkdf2_sha256_with(rounds: Option<u32>) -> Self {
404        Self(HashAlgorithmParams::Pbkdf2Sha256 { rounds })
405    }
406
407    /// Creates an Argon2id password hasher with default parameters.
408    ///
409    /// **Recommended** for all new archives. Argon2id is memory-hard, providing
410    /// better resistance against GPU/ASIC brute-force attacks compared to PBKDF2.
411    ///
412    /// # Example
413    ///
414    /// ```rust
415    /// use libpna::{WriteOptions, Encryption, HashAlgorithm};
416    ///
417    /// let opts = WriteOptions::builder()
418    ///     .encryption(Encryption::Aes)
419    ///     .hash_algorithm(HashAlgorithm::argon2id())
420    ///     .password(Some("secure_password"))
421    ///     .build();
422    /// ```
423    #[inline]
424    pub const fn argon2id() -> Self {
425        Self::argon2id_with(None, None, None)
426    }
427
428    /// Creates an Argon2id password hasher with custom parameters.
429    ///
430    /// - `time_cost`: Number of iterations (higher = slower, more secure)
431    /// - `memory_cost`: Memory usage in KiB (higher = more memory-hard)
432    /// - `parallelism_cost`: Degree of parallelism (threads)
433    ///
434    /// If any parameter is `None`, the default value is used.
435    ///
436    /// **Recommended** for all new archives when custom tuning is needed.
437    ///
438    /// # Example
439    ///
440    /// ```rust
441    /// use libpna::{WriteOptions, Encryption, HashAlgorithm};
442    ///
443    /// // Custom Argon2id with higher security parameters
444    /// let opts = WriteOptions::builder()
445    ///     .encryption(Encryption::Aes)
446    ///     .hash_algorithm(HashAlgorithm::argon2id_with(
447    ///         Some(4),       // time_cost: 4 iterations
448    ///         Some(65536),   // memory_cost: 64 MiB
449    ///         Some(2),       // parallelism: 2 threads
450    ///     ))
451    ///     .password(Some("secure_password"))
452    ///     .build();
453    /// ```
454    #[inline]
455    pub const fn argon2id_with(
456        time_cost: Option<u32>,
457        memory_cost: Option<u32>,
458        parallelism_cost: Option<u32>,
459    ) -> Self {
460        Self(HashAlgorithmParams::Argon2Id {
461            time_cost,
462            memory_cost,
463            parallelism_cost,
464        })
465    }
466}
467
468/// Type of filesystem object represented by an entry.
469///
470/// Each variant determines how the entry's data should be interpreted
471/// and how the entry should be extracted to the filesystem.
472#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
473#[repr(u8)]
474pub enum DataKind {
475    /// Regular file. Entry data contains the file contents.
476    File = 0,
477    /// Directory. Entry has no data content.
478    Directory = 1,
479    /// Symbolic link. Entry data contains the UTF-8 encoded link target path.
480    SymbolicLink = 2,
481    /// Hard link. Entry data contains the UTF-8 encoded path of the target entry
482    /// within the same archive.
483    HardLink = 3,
484}
485
486impl TryFrom<u8> for DataKind {
487    type Error = UnknownValueError;
488
489    #[inline]
490    fn try_from(value: u8) -> Result<Self, Self::Error> {
491        match value {
492            0 => Ok(Self::File),
493            1 => Ok(Self::Directory),
494            2 => Ok(Self::SymbolicLink),
495            3 => Ok(Self::HardLink),
496            value => Err(UnknownValueError(value)),
497        }
498    }
499}
500
501/// Options for writing entries to a PNA archive.
502///
503/// This type configures compression, encryption, and password hashing for archive entries.
504/// Options are created using the builder pattern via [`WriteOptions::builder()`] or by
505/// using the convenience constructor [`WriteOptions::store()`] for uncompressed entries.
506///
507/// # Compression and Encryption Order
508///
509/// When both compression and encryption are enabled, data is **compressed first, then encrypted**.
510/// This order maximizes compression efficiency since encrypted data is essentially random
511/// and cannot be compressed effectively.
512///
513/// Data flow: `Original → Compress → Encrypt → Write to archive`
514///
515/// # Security Considerations
516///
517/// - **Hash Algorithm**: Always use [`HashAlgorithm::argon2id()`] in production for password-based
518///   encryption. [`HashAlgorithm::pbkdf2_sha256()`] is primarily for compatibility with older
519///   systems or when Argon2 is not available.
520/// - **Cipher Mode**: CTR mode ([`CipherMode::CTR`]) is recommended over CBC for most use cases
521///   as it allows parallel processing and has simpler security requirements.
522/// - **IV Generation**: Initialization vectors (IVs) are automatically generated using
523///   cryptographically secure random number generation. You do not need to provide IVs.
524/// - **Password Strength**: Use strong passwords (12+ characters, mixed case, numbers, symbols)
525///   as the encryption key is derived from the password.
526///
527/// # Examples
528///
529/// Store without compression or encryption:
530/// ```rust
531/// use libpna::WriteOptions;
532///
533/// let opts = WriteOptions::store();
534/// ```
535///
536/// Compress only (no encryption):
537/// ```rust
538/// use libpna::{WriteOptions, Compression, CompressionLevel};
539///
540/// let opts = WriteOptions::builder()
541///     .compression(Compression::ZStandard)
542///     .compression_level(CompressionLevel::max())
543///     .build();
544/// ```
545///
546/// Encrypt only (no compression):
547/// ```rust
548/// use libpna::{WriteOptions, Encryption, CipherMode, HashAlgorithm};
549///
550/// let opts = WriteOptions::builder()
551///     .encryption(Encryption::Aes)
552///     .cipher_mode(CipherMode::CTR)
553///     .hash_algorithm(HashAlgorithm::argon2id())
554///     .password(Some("secure_password"))
555///     .build();
556/// ```
557///
558/// Both compression and encryption (recommended for sensitive data):
559/// ```rust
560/// use libpna::{WriteOptions, Compression, Encryption, CipherMode, HashAlgorithm};
561///
562/// let opts = WriteOptions::builder()
563///     .compression(Compression::ZStandard)
564///     .encryption(Encryption::Aes)
565///     .cipher_mode(CipherMode::CTR)
566///     .hash_algorithm(HashAlgorithm::argon2id())
567///     .password(Some("secure_password"))
568///     .build();
569/// ```
570///
571/// # Relationship to ReadOptions
572///
573/// When reading an archive, use [`ReadOptions`] to provide the password for decryption.
574/// The compression algorithm and cipher mode are stored in the archive metadata, so you
575/// only need to provide the password.
576#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
577pub struct WriteOptions {
578    compress: Compress,
579    cipher: Option<Cipher>,
580}
581
582impl WriteOptions {
583    /// A new [WriteOptions] to simply store.
584    ///
585    /// # Examples
586    ///
587    /// ```rust
588    /// use libpna::{EntryBuilder, WriteOptions};
589    ///
590    /// EntryBuilder::new_file("example.txt".into(), WriteOptions::store()).unwrap();
591    /// ```
592    ///
593    /// [Entry]: crate::Entry
594    #[inline]
595    pub const fn store() -> Self {
596        Self {
597            compress: Compress::No,
598            cipher: None,
599        }
600    }
601
602    /// Returns a builder for [WriteOptions].
603    ///
604    /// # Returns
605    ///
606    /// [WriteOptionsBuilder] Builder object for [WriteOptions].
607    ///
608    /// # Examples
609    ///
610    /// ```rust
611    /// use libpna::WriteOptions;
612    ///
613    /// let builder = WriteOptions::builder();
614    /// ```
615    #[inline]
616    pub const fn builder() -> WriteOptionsBuilder {
617        WriteOptionsBuilder::new()
618    }
619
620    /// Converts [WriteOptions] into a [WriteOptionsBuilder].
621    ///
622    /// # Returns
623    ///
624    /// [WriteOptionsBuilder]: Builder object for [WriteOptions].
625    ///
626    /// # Examples
627    /// ```rust
628    /// use libpna::WriteOptions;
629    ///
630    /// let write_option = WriteOptions::builder().build();
631    /// let builder = write_option.into_builder();
632    /// ```
633    #[inline]
634    pub fn into_builder(self) -> WriteOptionsBuilder {
635        self.into()
636    }
637}
638
639/// Builder for [`WriteOptions`].
640#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
641pub struct WriteOptionsBuilder {
642    compression: Compression,
643    compression_level: CompressionLevel,
644    encryption: Encryption,
645    cipher_mode: CipherMode,
646    hash_algorithm: HashAlgorithm,
647    password: Option<Vec<u8>>,
648}
649
650impl Default for WriteOptionsBuilder {
651    #[inline]
652    fn default() -> Self {
653        Self::new()
654    }
655}
656
657impl From<WriteOptions> for WriteOptionsBuilder {
658    #[inline]
659    fn from(value: WriteOptions) -> Self {
660        let (compression, compression_level) = match value.compress {
661            Compress::No => (Compression::No, CompressionLevel::DEFAULT),
662            Compress::Deflate(level) => (Compression::Deflate, level.into()),
663            Compress::ZStandard(level) => (Compression::ZStandard, level.into()),
664            Compress::XZ(level) => (Compression::XZ, level.into()),
665        };
666        Self {
667            compression,
668            compression_level,
669            encryption: value.encryption(),
670            cipher_mode: value.cipher_mode(),
671            hash_algorithm: value.hash_algorithm(),
672            password: value.password().map(|p| p.to_vec()),
673        }
674    }
675}
676
677impl WriteOptionsBuilder {
678    const fn new() -> Self {
679        Self {
680            compression: Compression::No,
681            compression_level: CompressionLevel::DEFAULT,
682            encryption: Encryption::No,
683            cipher_mode: CipherMode::CTR,
684            hash_algorithm: HashAlgorithm::argon2id(),
685            password: None,
686        }
687    }
688
689    /// Sets the [`Compression`].
690    #[inline]
691    pub fn compression(&mut self, compression: Compression) -> &mut Self {
692        self.compression = compression;
693        self
694    }
695
696    /// Sets the [`CompressionLevel`].
697    #[inline]
698    pub fn compression_level(&mut self, compression_level: CompressionLevel) -> &mut Self {
699        self.compression_level = compression_level;
700        self
701    }
702
703    /// Sets the [`Encryption`].
704    #[inline]
705    pub fn encryption(&mut self, encryption: Encryption) -> &mut Self {
706        self.encryption = encryption;
707        self
708    }
709
710    /// Sets the [`CipherMode`].
711    #[inline]
712    pub fn cipher_mode(&mut self, cipher_mode: CipherMode) -> &mut Self {
713        self.cipher_mode = cipher_mode;
714        self
715    }
716
717    /// Sets the [`HashAlgorithm`].
718    #[inline]
719    pub fn hash_algorithm(&mut self, algorithm: HashAlgorithm) -> &mut Self {
720        self.hash_algorithm = algorithm;
721        self
722    }
723
724    /// Sets the password.
725    ///
726    /// Accepts both UTF-8 strings and arbitrary byte slices.
727    ///
728    /// # Examples
729    /// ```rust
730    /// use libpna::WriteOptions;
731    ///
732    /// // String password
733    /// WriteOptions::builder().password(Some("my_password"));
734    ///
735    /// // Byte slice password
736    /// WriteOptions::builder().password(Some(b"binary_password"));
737    /// WriteOptions::builder().password(Some(&[0x01, 0x02, 0x03, 0x04]));
738    /// ```
739    #[inline]
740    pub fn password<B: AsRef<[u8]>>(&mut self, password: Option<B>) -> &mut Self {
741        self.password = password.map(|it| it.as_ref().to_vec());
742        self
743    }
744
745    /// Creates a new [`WriteOptions`] from this builder.
746    ///
747    /// This finalizes the builder configuration and creates an immutable [`WriteOptions`]
748    /// that can be used when creating entries.
749    ///
750    /// # Panics
751    ///
752    /// Panics if [`encryption()`](Self::encryption) was set to [`Encryption::Aes`] or
753    /// [`Encryption::Camellia`] but [`password()`](Self::password) was not called with
754    /// a password.
755    ///
756    /// **Always provide a password when enabling encryption.** The following code will panic:
757    /// ```no_run
758    /// use libpna::{WriteOptions, Encryption};
759    ///
760    /// let opts = WriteOptions::builder()
761    ///     .encryption(Encryption::Aes)
762    ///     .build();  // PANICS: "Password was not provided."
763    /// ```
764    ///
765    /// **Correct usage:**
766    /// ```rust
767    /// use libpna::{WriteOptions, Encryption};
768    ///
769    /// let opts = WriteOptions::builder()
770    ///     .encryption(Encryption::Aes)
771    ///     .password(Some("secure_password"))
772    ///     .build();  // OK
773    /// ```
774    #[inline]
775    #[must_use = "building options without using them is wasteful"]
776    pub fn build(&self) -> WriteOptions {
777        let cipher = if self.encryption != Encryption::No {
778            Some(Cipher::new(
779                self.password
780                    .as_deref()
781                    .expect("Password was not provided.")
782                    .into(),
783                self.hash_algorithm,
784                match self.encryption {
785                    Encryption::Aes => CipherAlgorithm::Aes,
786                    Encryption::Camellia => CipherAlgorithm::Camellia,
787                    Encryption::No => unreachable!(),
788                },
789                self.cipher_mode,
790            ))
791        } else {
792            None
793        };
794        WriteOptions {
795            compress: match self.compression {
796                Compression::No => Compress::No,
797                Compression::Deflate => Compress::Deflate(self.compression_level.into()),
798                Compression::ZStandard => Compress::ZStandard(self.compression_level.into()),
799                Compression::XZ => Compress::XZ(self.compression_level.into()),
800            },
801            cipher,
802        }
803    }
804}
805
806/// Options for reading an entry.
807#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
808pub struct ReadOptions {
809    password: Option<Vec<u8>>,
810}
811
812impl ReadOptions {
813    /// Creates a new [`ReadOptions`] with an optional password.
814    ///
815    /// Accepts both UTF-8 strings and arbitrary byte slices.
816    ///
817    /// # Examples
818    /// ```rust
819    /// use libpna::ReadOptions;
820    ///
821    /// // String password
822    /// let read_option = ReadOptions::with_password(Some("password"));
823    ///
824    /// // Byte slice password
825    /// let read_option = ReadOptions::with_password(Some(b"password"));
826    /// let read_option = ReadOptions::with_password(Some(&[0x01, 0x02, 0x03]));
827    /// ```
828    #[inline]
829    pub fn with_password<B: AsRef<[u8]>>(password: Option<B>) -> Self {
830        Self {
831            password: password.map(|p| p.as_ref().to_vec()),
832        }
833    }
834
835    /// Returns a builder for [ReadOptions].
836    ///
837    /// # Returns
838    ///
839    /// [ReadOptionsBuilder]: Builder object for [ReadOptions].
840    ///
841    /// # Examples
842    /// ```rust
843    /// use libpna::ReadOptions;
844    ///
845    /// let builder = ReadOptions::builder();
846    /// ```
847    #[inline]
848    pub const fn builder() -> ReadOptionsBuilder {
849        ReadOptionsBuilder::new()
850    }
851
852    /// Converts [ReadOptions] into a [ReadOptionsBuilder].
853    ///
854    /// # Returns
855    ///
856    /// [ReadOptionsBuilder]: Builder object for [ReadOptions].
857    ///
858    /// # Examples
859    /// ```rust
860    /// use libpna::ReadOptions;
861    ///
862    /// let read_option = ReadOptions::builder().build();
863    /// let builder = read_option.into_builder();
864    /// ```
865    #[inline]
866    pub fn into_builder(self) -> ReadOptionsBuilder {
867        self.into()
868    }
869}
870
871/// Builder for [`ReadOptions`].
872#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
873pub struct ReadOptionsBuilder {
874    password: Option<Vec<u8>>,
875}
876
877impl From<ReadOptions> for ReadOptionsBuilder {
878    #[inline]
879    fn from(value: ReadOptions) -> Self {
880        Self {
881            password: value.password,
882        }
883    }
884}
885
886impl ReadOptionsBuilder {
887    #[inline]
888    const fn new() -> Self {
889        Self { password: None }
890    }
891
892    /// Creates a new [`ReadOptions`].
893    #[inline]
894    #[must_use = "building options without using them is wasteful"]
895    pub fn build(&self) -> ReadOptions {
896        ReadOptions {
897            password: self.password.clone(),
898        }
899    }
900}