Skip to main content

libpna/
archive.rs

1mod header;
2mod read;
3mod write;
4
5use crate::{
6    chunk::{ChunkStreamWriter, RawChunk},
7    cipher::CipherWriter,
8    compress::CompressionWriter,
9};
10use core::num::NonZeroU32;
11pub use header::*;
12use std::io::prelude::*;
13pub(crate) use {read::*, write::*};
14
15/// An object providing access to a PNA file.
16/// An instance of an [Archive] can be read and/or written.
17///
18/// The [Archive] struct provides two main modes of operation:
19/// - Read mode: Allows reading entries from an existing PNA file
20/// - Write mode: Enables creating new entries and writing data to the archive
21///
22/// The archive supports various features including:
23/// - Multiple compression algorithms
24/// - Encryption options
25/// - Solid and non-solid modes
26/// - Chunk-based storage
27///
28/// # Examples
29/// Creates a new PNA file and adds an entry to it.
30/// ```no_run
31/// # use libpna::{Archive, EntryBuilder, WriteOptions};
32/// # use std::fs::File;
33/// # use std::io::{self, prelude::*};
34///
35/// # fn main() -> io::Result<()> {
36/// let file = File::create("foo.pna")?;
37/// let mut archive = Archive::write_header(file)?;
38/// let mut entry_builder =
39///     EntryBuilder::new_file("bar.txt".into(), WriteOptions::builder().build())?;
40/// entry_builder.write_all(b"content")?;
41/// let entry = entry_builder.build()?;
42/// archive.add_entry(entry)?;
43/// archive.finalize()?;
44/// #     Ok(())
45/// # }
46/// ```
47///
48/// Reads the entries of a PNA file.
49/// ```no_run
50/// # use libpna::{Archive, ReadOptions};
51/// # use std::fs::File;
52/// # use std::io::{self, copy, prelude::*};
53///
54/// # fn main() -> io::Result<()> {
55/// let file = File::open("foo.pna")?;
56/// let mut archive = Archive::read_header(file)?;
57/// for entry in archive.entries().skip_solid() {
58///     let entry = entry?;
59///     let mut file = File::create(entry.header().path().as_path())?;
60///     let mut reader = entry.reader(ReadOptions::builder().build())?;
61///     copy(&mut reader, &mut file)?;
62/// }
63/// #     Ok(())
64/// # }
65/// ```
66pub struct Archive<T> {
67    inner: T,
68    header: ArchiveHeader,
69    max_chunk_size: Option<NonZeroU32>,
70    // following fields are only use in reader mode
71    next_archive: bool,
72    buf: Vec<RawChunk>,
73}
74
75impl<T> Archive<T> {
76    const fn new(inner: T, header: ArchiveHeader) -> Self {
77        Self::with_buffer(inner, header, Vec::new())
78    }
79
80    const fn with_buffer(inner: T, header: ArchiveHeader, buf: Vec<RawChunk>) -> Self {
81        Self {
82            inner,
83            header,
84            max_chunk_size: None,
85            next_archive: false,
86            buf,
87        }
88    }
89
90    /// Sets the maximum chunk size limit.
91    ///
92    /// When set, this limit affects both reading and writing:
93    /// - **Reading**: Chunks larger than this size will be rejected with an error,
94    ///   protecting against maliciously crafted archives with extremely large chunks.
95    /// - **Writing**: Data written via [`write_file()`](Archive::write_file) will be
96    ///   split into chunks no larger than this size.
97    ///
98    /// **Note**: This setting only affects the streaming write path
99    /// ([`write_file()`](Archive::write_file)). Pre-built entries added via
100    /// [`add_entry()`](Archive::add_entry) use their own chunk size configured
101    /// through [`EntryBuilder::max_chunk_size()`](crate::EntryBuilder::max_chunk_size).
102    ///
103    #[inline]
104    pub fn set_max_chunk_size(&mut self, size: NonZeroU32) {
105        self.max_chunk_size = Some(size);
106    }
107
108    /// Returns `true` if an [ANXT] chunk has appeared before calling this method.
109    ///
110    /// # Returns
111    ///
112    /// `true` if the next archive in the series is available, otherwise `false`.
113    ///
114    /// [ANXT]: crate::chunk::ChunkType::ANXT
115    #[inline]
116    pub const fn has_next_archive(&self) -> bool {
117        self.next_archive
118    }
119
120    /// Consumes the archive and returns the underlying reader or writer.
121    ///
122    /// # Warning
123    ///
124    /// This method does not finalize the archive. If you are writing to an
125    /// archive, call [`Archive::finalize`] first to ensure the end-of-archive
126    /// marker is written. Using `into_inner` on a writer without finalizing
127    /// leaves the archive incomplete.
128    ///
129    /// # Examples
130    ///
131    /// For normal archive completion, prefer [`Archive::finalize`] which writes
132    /// the end-of-archive marker and returns the inner writer:
133    ///
134    /// ```
135    /// # use libpna::Archive;
136    /// # use std::io;
137    /// # fn main() -> io::Result<()> {
138    /// let archive = Archive::write_header(Vec::new())?;
139    /// let writer = archive.finalize()?; // Preferred: archive is properly closed
140    /// # Ok(())
141    /// # }
142    /// ```
143    ///
144    /// Use `into_inner` when you need to abandon an archive or access the
145    /// underlying reader:
146    ///
147    /// ```
148    /// # use libpna::Archive;
149    /// # use std::io;
150    /// # fn main() -> io::Result<()> {
151    /// let file = std::io::Cursor::new(include_bytes!("../../resources/test/empty.pna").to_vec());
152    /// let archive = Archive::read_header(file)?;
153    /// let _reader = archive.into_inner(); // Safe for readers
154    /// # Ok(())
155    /// # }
156    /// ```
157    #[must_use = "call `finalize` instead if you don't need the inner value"]
158    #[inline]
159    pub fn into_inner(self) -> T {
160        self.inner
161    }
162}
163
164/// An object that provides write access to solid mode PNA files.
165///
166/// In solid mode, all entries are compressed together as a single unit,
167/// which typically results in better compression ratios compared to
168/// non-solid mode. However, this means that individual entries cannot
169/// be accessed randomly - they must be read sequentially.
170///
171/// Key features of solid mode:
172/// - Improved compression ratio
173/// - Sequential access only
174/// - Single compression/encryption context for all entries
175///
176/// # Examples
177/// Creates a new solid mode PNA file and adds an entry to it.
178/// ```no_run
179/// use libpna::{Archive, EntryBuilder, WriteOptions};
180/// use std::fs::File;
181/// # use std::io::{self, prelude::*};
182///
183/// # fn main() -> io::Result<()> {
184/// let option = WriteOptions::builder().build();
185/// let file = File::create("foo.pna")?;
186/// let mut archive = Archive::write_solid_header(file, option)?;
187/// let mut entry_builder = EntryBuilder::new_file("bar.txt".into(), WriteOptions::store())?;
188/// entry_builder.write_all(b"content")?;
189/// let entry = entry_builder.build()?;
190/// archive.add_entry(entry)?;
191/// archive.finalize()?;
192/// #     Ok(())
193/// # }
194/// ```
195pub struct SolidArchive<T: Write> {
196    archive_header: ArchiveHeader,
197    inner: CompressionWriter<CipherWriter<ChunkStreamWriter<T>>>,
198    max_chunk_size: Option<NonZeroU32>,
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use crate::{Duration, entry::*};
205    use std::io::{self, Cursor};
206    #[cfg(all(target_family = "wasm", target_os = "unknown"))]
207    use wasm_bindgen_test::wasm_bindgen_test as test;
208
209    #[test]
210    fn store_archive() {
211        archive(
212            b"src data bytes",
213            WriteOptions::builder().compression(Compression::No).build(),
214        )
215        .unwrap()
216    }
217
218    #[test]
219    fn deflate_archive() {
220        archive(
221            b"src data bytes",
222            WriteOptions::builder()
223                .compression(Compression::Deflate)
224                .build(),
225        )
226        .unwrap()
227    }
228
229    #[test]
230    fn zstd_archive() {
231        archive(
232            b"src data bytes",
233            WriteOptions::builder()
234                .compression(Compression::ZStandard)
235                .build(),
236        )
237        .unwrap()
238    }
239
240    #[test]
241    fn xz_archive() {
242        archive(
243            b"src data bytes",
244            WriteOptions::builder().compression(Compression::XZ).build(),
245        )
246        .unwrap();
247    }
248
249    #[test]
250    fn store_with_aes_cbc_archive() {
251        archive(
252            b"plain text",
253            WriteOptions::builder()
254                .compression(Compression::No)
255                .encryption(Encryption::Aes)
256                .cipher_mode(CipherMode::CBC)
257                .hash_algorithm(HashAlgorithm::pbkdf2_sha256_with(Some(1)))
258                .password(Some("password"))
259                .build(),
260        )
261        .unwrap();
262    }
263
264    #[test]
265    fn zstd_with_aes_ctr_archive() {
266        archive(
267            b"plain text",
268            WriteOptions::builder()
269                .compression(Compression::ZStandard)
270                .encryption(Encryption::Aes)
271                .cipher_mode(CipherMode::CTR)
272                .hash_algorithm(HashAlgorithm::pbkdf2_sha256_with(Some(1)))
273                .password(Some("password"))
274                .build(),
275        )
276        .unwrap();
277    }
278
279    #[test]
280    fn zstd_with_aes_cbc_archive() {
281        archive(
282            b"plain text",
283            WriteOptions::builder()
284                .compression(Compression::ZStandard)
285                .encryption(Encryption::Aes)
286                .cipher_mode(CipherMode::CBC)
287                .hash_algorithm(HashAlgorithm::pbkdf2_sha256_with(Some(1)))
288                .password(Some("password"))
289                .build(),
290        )
291        .unwrap();
292    }
293
294    #[test]
295    fn zstd_with_camellia_ctr_archive() {
296        archive(
297            b"plain text",
298            WriteOptions::builder()
299                .compression(Compression::ZStandard)
300                .encryption(Encryption::Camellia)
301                .cipher_mode(CipherMode::CTR)
302                .hash_algorithm(HashAlgorithm::pbkdf2_sha256_with(Some(1)))
303                .password(Some("password"))
304                .build(),
305        )
306        .unwrap();
307    }
308
309    #[test]
310    fn zstd_with_camellia_cbc_archive() {
311        archive(
312            b"plain text",
313            WriteOptions::builder()
314                .compression(Compression::ZStandard)
315                .encryption(Encryption::Camellia)
316                .cipher_mode(CipherMode::CBC)
317                .hash_algorithm(HashAlgorithm::pbkdf2_sha256_with(Some(1)))
318                .password(Some("password"))
319                .build(),
320        )
321        .unwrap();
322    }
323
324    #[test]
325    fn xz_with_aes_cbc_archive() {
326        archive(
327            b"plain text",
328            WriteOptions::builder()
329                .compression(Compression::XZ)
330                .encryption(Encryption::Aes)
331                .cipher_mode(CipherMode::CBC)
332                .hash_algorithm(HashAlgorithm::pbkdf2_sha256_with(Some(1)))
333                .password(Some("password"))
334                .build(),
335        )
336        .unwrap()
337    }
338
339    #[test]
340    fn xz_with_camellia_cbc_archive() {
341        archive(
342            b"plain text",
343            WriteOptions::builder()
344                .compression(Compression::XZ)
345                .encryption(Encryption::Camellia)
346                .cipher_mode(CipherMode::CBC)
347                .hash_algorithm(HashAlgorithm::pbkdf2_sha256_with(Some(1)))
348                .password(Some("password"))
349                .build(),
350        )
351        .unwrap()
352    }
353
354    fn create_archive(src: &[u8], options: WriteOptions) -> io::Result<Vec<u8>> {
355        let mut writer = Archive::write_header(Vec::with_capacity(src.len()))?;
356        writer.add_entry({
357            let mut builder = EntryBuilder::new_file("test/text".into(), options)?;
358            builder.write_all(src)?;
359            builder.build()?
360        })?;
361        writer.finalize()
362    }
363
364    fn archive(src: &[u8], options: WriteOptions) -> io::Result<()> {
365        let read_options = ReadOptions::with_password(options.password());
366        let archive = create_archive(src, options)?;
367        let mut archive_reader = Archive::read_header(archive.as_slice())?;
368        let item = archive_reader.entries().skip_solid().next().unwrap()?;
369        let mut reader = item.reader(read_options)?;
370        let mut dist = Vec::new();
371        io::copy(&mut reader, &mut dist)?;
372        assert_eq!(src, dist.as_slice());
373        Ok(())
374    }
375
376    fn solid_archive(write_option: WriteOptions) {
377        let password = write_option.password().map(|it| it.to_vec());
378        let mut archive = Archive::write_solid_header(Vec::new(), write_option).unwrap();
379        for i in 0..200 {
380            archive
381                .add_entry({
382                    let mut builder = EntryBuilder::new_file(
383                        format!("test/text{i}").into(),
384                        WriteOptions::store(),
385                    )
386                    .unwrap();
387                    builder
388                        .write_all(format!("text{i}").repeat(i).as_bytes())
389                        .unwrap();
390                    builder.build().unwrap()
391                })
392                .unwrap();
393        }
394        let buf = archive.finalize().unwrap();
395        let mut archive = Archive::read_header(&buf[..]).unwrap();
396        let mut entries = archive.entries();
397        let entry = entries.next().unwrap().unwrap();
398        if let ReadEntry::Solid(entry) = entry {
399            let mut entries = entry.entries(password.as_deref()).unwrap();
400            for i in 0..200 {
401                let entry = entries.next().unwrap().unwrap();
402                let mut reader = entry.reader(ReadOptions::builder().build()).unwrap();
403                let mut body = Vec::new();
404                reader.read_to_end(&mut body).unwrap();
405                assert_eq!(format!("text{i}").repeat(i).as_bytes(), &body[..]);
406            }
407        } else {
408            panic!()
409        }
410    }
411
412    #[test]
413    fn solid_store_camellia_cbc() {
414        solid_archive(
415            WriteOptions::builder()
416                .compression(Compression::No)
417                .encryption(Encryption::Camellia)
418                .cipher_mode(CipherMode::CBC)
419                .hash_algorithm(HashAlgorithm::pbkdf2_sha256_with(Some(1)))
420                .password(Some("PASSWORD"))
421                .build(),
422        );
423    }
424
425    #[test]
426    fn solid_entry() {
427        let archive = {
428            let mut writer = Archive::write_header(Vec::new()).unwrap();
429            let dir_entry = {
430                let builder = EntryBuilder::new_dir("test".into());
431                builder.build().unwrap()
432            };
433            let file_entry = {
434                let options = WriteOptions::store();
435                let mut builder = EntryBuilder::new_file("test/text".into(), options).unwrap();
436                builder.write_all(b"text").unwrap();
437                builder.build().unwrap()
438            };
439            writer
440                .add_entry({
441                    let mut builder = SolidEntryBuilder::new(WriteOptions::store()).unwrap();
442                    builder.add_entry(dir_entry).unwrap();
443                    builder.add_entry(file_entry).unwrap();
444                    builder.build().unwrap()
445                })
446                .unwrap();
447            writer.finalize().unwrap()
448        };
449
450        let mut archive_reader = Archive::read_header(archive.as_slice()).unwrap();
451        let mut entries = archive_reader.entries_with_password(Some(b"password"));
452        entries.next().unwrap().expect("failed to read entry");
453        entries.next().unwrap().expect("failed to read entry");
454        assert!(entries.next().is_none());
455    }
456
457    #[test]
458    fn copy_entry() {
459        let archive = create_archive(b"archive text", WriteOptions::builder().build())
460            .expect("failed to create archive");
461        let mut reader =
462            Archive::read_header(archive.as_slice()).expect("failed to read archive header");
463
464        let mut writer = Archive::write_header(Vec::new()).expect("failed to write archive header");
465
466        for entry in reader.raw_entries() {
467            writer
468                .add_entry(entry.expect("failed to read entry"))
469                .expect("failed to add entry");
470        }
471        assert_eq!(
472            archive,
473            writer.finalize().expect("failed to finish archive")
474        )
475    }
476
477    #[test]
478    fn append() {
479        let mut writer = Archive::write_header(Vec::new()).unwrap();
480        writer
481            .add_entry({
482                let builder =
483                    EntryBuilder::new_file("text1.txt".into(), WriteOptions::builder().build())
484                        .unwrap();
485                builder.build().unwrap()
486            })
487            .unwrap();
488        let result = writer.finalize().unwrap();
489
490        let mut appender = Archive::read_header(Cursor::new(result)).unwrap();
491        appender.seek_to_end().unwrap();
492        appender
493            .add_entry({
494                let builder =
495                    EntryBuilder::new_file("text2.txt".into(), WriteOptions::builder().build())
496                        .unwrap();
497                builder.build().unwrap()
498            })
499            .unwrap();
500        let appended = appender.finalize().unwrap().into_inner();
501
502        let mut reader = Archive::read_header(appended.as_slice()).unwrap();
503
504        let mut entries = reader.entries();
505        assert!(entries.next().is_some());
506        assert!(entries.next().is_some());
507        assert!(entries.next().is_none());
508    }
509
510    #[test]
511    fn metadata() {
512        let original_entry = {
513            let mut builder =
514                EntryBuilder::new_file("name".into(), WriteOptions::builder().build()).unwrap();
515            builder.created(Duration::seconds(31));
516            builder.modified(Duration::seconds(32));
517            builder.accessed(Duration::seconds(33));
518            builder.permission(Permission::new(1, "uname".into(), 2, "gname".into(), 0o775));
519            builder.write_all(b"entry data").unwrap();
520            builder.build().unwrap()
521        };
522
523        let mut archive = Archive::write_header(Vec::new()).unwrap();
524        archive.add_entry(original_entry.clone()).unwrap();
525
526        let buf = archive.finalize().unwrap();
527
528        let mut archive = Archive::read_header(buf.as_slice()).unwrap();
529
530        let mut entries = archive.entries_with_password(None);
531        let read_entry = entries.next().unwrap().unwrap();
532
533        assert_eq!(
534            original_entry.metadata().created(),
535            read_entry.metadata().created()
536        );
537        assert_eq!(
538            original_entry.metadata().modified(),
539            read_entry.metadata().modified()
540        );
541        assert_eq!(
542            original_entry.metadata().accessed(),
543            read_entry.metadata().accessed()
544        );
545        assert_eq!(
546            original_entry.metadata().permission(),
547            read_entry.metadata().permission()
548        );
549        assert_eq!(
550            original_entry.metadata().compressed_size(),
551            read_entry.metadata().compressed_size()
552        );
553        assert_eq!(
554            original_entry.metadata().raw_file_size(),
555            read_entry.metadata().raw_file_size()
556        );
557    }
558}