Skip to main content

anyreader/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::io;
4use std::io::Read;
5use std::path::{Path, PathBuf};
6
7mod container;
8mod peekable;
9mod stream;
10
11pub use crate::container::{ArchiveKind, Container, ContainerKind, Items};
12pub use crate::stream::CompressionKind;
13pub use crate::stream::StreamKind;
14
15#[derive(Debug, strum::EnumIs)]
16pub enum FileKind {
17    File,
18    Directory,
19    Other,
20}
21
22/// Represents what is known about the size of a file or entry.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub enum SizeHint {
25    /// The exact uncompressed size is known (e.g., from archive entry headers).
26    Exact(u64),
27    /// Only the compressed size is known (after transparent decompression).
28    CompressedSize(u64),
29    /// No size information is available.
30    #[default]
31    Unknown,
32}
33
34impl SizeHint {
35    /// Returns the exact size if known.
36    pub fn exact(&self) -> Option<u64> {
37        match self {
38            SizeHint::Exact(n) => Some(*n),
39            _ => None,
40        }
41    }
42
43    /// Returns the compressed size if that's what is known.
44    pub fn compressed_size(&self) -> Option<u64> {
45        match self {
46            SizeHint::CompressedSize(n) => Some(*n),
47            _ => None,
48        }
49    }
50
51    /// Returns any known size (exact or compressed).
52    pub fn any_known(&self) -> Option<u64> {
53        match self {
54            SizeHint::Exact(n) | SizeHint::CompressedSize(n) => Some(*n),
55            SizeHint::Unknown => None,
56        }
57    }
58
59    /// Returns true if the size is exactly known.
60    pub fn is_exact(&self) -> bool {
61        matches!(self, SizeHint::Exact(_))
62    }
63
64    /// Returns true if the size is unknown.
65    pub fn is_unknown(&self) -> bool {
66        matches!(self, SizeHint::Unknown)
67    }
68}
69
70#[derive(Debug)]
71pub struct FileItem<T: Read> {
72    pub path: PathBuf,
73    pub reader: T,
74    pub kind: FileKind,
75    pub size_hint: SizeHint,
76}
77
78pub fn recursive_read<F>(path: &Path, mut reader: impl Read, callback: &mut F) -> io::Result<()>
79where
80    F: FnMut(FileItem<&mut dyn Read>) -> io::Result<()>,
81{
82    read_recursive_inner(
83        path,
84        FileKind::File,
85        SizeHint::Unknown,
86        &mut reader as &mut dyn Read,
87        callback,
88    )?;
89    Ok(())
90}
91
92fn handle_container<F>(path: &Path, mut archive: impl Container, callback: &mut F) -> io::Result<()>
93where
94    F: FnMut(FileItem<&mut dyn Read>) -> io::Result<()>,
95{
96    let mut items = archive.items()?;
97    while let Some(x) = items.next_item() {
98        let mut x = x?;
99        let reader = &mut x.reader as &mut dyn Read;
100        read_recursive_inner(
101            path.join(x.path).as_path(),
102            x.kind,
103            x.size_hint,
104            reader,
105            callback,
106        )?;
107    }
108    Ok(())
109}
110
111fn read_recursive_inner<F>(
112    path: &Path,
113    kind: FileKind,
114    size_hint: SizeHint,
115    reader: &mut dyn Read,
116    callback: &mut F,
117) -> io::Result<()>
118where
119    F: FnMut(FileItem<&mut dyn Read>) -> io::Result<()>,
120{
121    let container = ContainerKind::from_reader(reader)?;
122    match container {
123        ContainerKind::Stream(StreamKind::Raw(mut r)) => callback(FileItem {
124            path: path.to_path_buf(),
125            reader: &mut r as &mut dyn Read,
126            kind,
127            size_hint,
128        }),
129        ContainerKind::Stream(StreamKind::Compressed(mut c)) => {
130            // When decompressing, convert Exact to CompressedSize
131            let new_hint = match size_hint {
132                SizeHint::Exact(n) => SizeHint::CompressedSize(n),
133                other => other,
134            };
135            read_recursive_inner(path, kind, new_hint, &mut c as &mut dyn Read, callback)
136        }
137        ContainerKind::Archive(ArchiveKind::Tar(r)) => handle_container(path, r, callback),
138        ContainerKind::Archive(ArchiveKind::Zip(r)) => handle_container(path, r, callback),
139    }
140}
141
142/// Unwraps compression layers and iterates archive entries without recursion.
143///
144/// This function decompresses outer layers (gzip, zstd, bzip2, xz) to reach the archive,
145/// then iterates archive entries (tar or zip) once. Unlike [`recursive_read`], it does NOT
146/// recurse into nested archives or decompress entry contents - entries are returned with
147/// their raw bytes.
148///
149/// Returns an error if the input is not an archive after decompression.
150pub fn iterate_archive<R, F>(mut reader: R, mut callback: F) -> io::Result<()>
151where
152    R: Read,
153    F: FnMut(FileItem<&mut dyn Read>) -> io::Result<()>,
154{
155    iterate_archive_inner(&mut reader as &mut dyn Read, &mut callback)
156}
157
158fn iterate_archive_inner<F>(reader: &mut dyn Read, callback: &mut F) -> io::Result<()>
159where
160    F: FnMut(FileItem<&mut dyn Read>) -> io::Result<()>,
161{
162    let container = ContainerKind::from_reader(reader)?;
163    match container {
164        // Raw data after decompression - not an archive
165        ContainerKind::Stream(StreamKind::Raw(_)) => Err(io::Error::new(
166            io::ErrorKind::InvalidData,
167            "input is not an archive",
168        )),
169        // Compressed - recurse through compression layer
170        ContainerKind::Stream(StreamKind::Compressed(mut c)) => {
171            iterate_archive_inner(&mut c as &mut dyn Read, callback)
172        }
173        // Archive found - iterate entries without recursively decompressing
174        ContainerKind::Archive(ArchiveKind::Tar(mut r)) => iterate_entries(&mut r, callback),
175        ContainerKind::Archive(ArchiveKind::Zip(mut r)) => iterate_entries(&mut r, callback),
176    }
177}
178
179fn iterate_entries<F>(archive: &mut impl Container, callback: &mut F) -> io::Result<()>
180where
181    F: FnMut(FileItem<&mut dyn Read>) -> io::Result<()>,
182{
183    let mut items = archive.items()?;
184    while let Some(item) = items.next_item() {
185        let mut item = item?;
186        callback(FileItem {
187            path: item.path,
188            reader: &mut item.reader as &mut dyn Read,
189            kind: item.kind,
190            size_hint: item.size_hint,
191        })?;
192    }
193    Ok(())
194}