libpna/
archive.rs

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