Skip to main content

easy_archive/
types.rs

1/// Core types for archive operations
2pub use strum::IntoEnumIterator;
3use strum_macros::EnumIter;
4
5use crate::error::Result;
6
7#[cfg(feature = "tar")]
8use crate::archive::Tar;
9#[cfg(feature = "tar-bz")]
10use crate::archive::TarBz;
11#[cfg(feature = "tar-gz")]
12use crate::archive::TarGz;
13#[cfg(feature = "tar-xz")]
14use crate::archive::TarXz;
15#[cfg(feature = "tar-zstd")]
16use crate::archive::TarZstd;
17#[cfg(feature = "zip")]
18use crate::archive::Zip;
19#[cfg(feature = "7z")]
20use crate::archive::seven_zip::SevenZip;
21
22#[cfg(feature = "decode")]
23use crate::traits::Decode;
24#[cfg(feature = "encode")]
25use crate::traits::Encode;
26
27/// Archive format enumeration
28///
29/// Represents the supported archive formats. Each variant is conditionally
30/// compiled based on the corresponding feature flag.
31#[cfg_attr(feature = "wasm", wasm_bindgen::prelude::wasm_bindgen)]
32#[derive(EnumIter, Debug, Clone, Copy, PartialEq, Eq, Hash)]
33pub enum Fmt {
34    /// Plain tar archive format
35    #[cfg(feature = "tar")]
36    Tar,
37    /// Gzip-compressed tar archive (.tar.gz, .tgz)
38    #[cfg(feature = "tar-gz")]
39    TarGz,
40    /// XZ-compressed tar archive (.tar.xz, .txz)
41    #[cfg(feature = "tar-xz")]
42    TarXz,
43    /// Bzip2-compressed tar archive (.tar.bz2, .tbz2)
44    #[cfg(feature = "tar-bz")]
45    TarBz,
46    /// Zstd-compressed tar archive (.tar.zst, .tzst, .tzstd)
47    #[cfg(feature = "tar-zstd")]
48    TarZstd,
49    /// ZIP archive format
50    #[cfg(feature = "zip")]
51    Zip,
52    /// 7z archive format
53    #[cfg(feature = "7z")]
54    SevenZip,
55}
56
57impl Fmt {
58    /// Decode an archive from bytes
59    ///
60    /// # Arguments
61    /// * `buffer` - The archive data as bytes
62    ///
63    /// # Returns
64    /// * `Ok(Vec<File>)` - List of files extracted from the archive
65    /// * `Err(ArchiveError)` - If decoding fails
66    ///
67    /// # Example
68    /// ```no_run
69    /// use easy_archive::Fmt;
70    /// let data = std::fs::read("archive.tar.gz")?;
71    /// let files = Fmt::TarGz.decode(data)?;
72    /// # Ok::<(), Box<dyn std::error::Error>>(())
73    /// ```
74    #[cfg(feature = "decode")]
75    pub fn decode(&self, buffer: Vec<u8>) -> Result<Vec<File>> {
76        match self {
77            #[cfg(feature = "zip")]
78            Fmt::Zip => Zip::decode(buffer),
79            #[cfg(feature = "tar")]
80            Fmt::Tar => Tar::decode(buffer),
81            #[cfg(feature = "tar-gz")]
82            Fmt::TarGz => TarGz::decode(buffer),
83            #[cfg(feature = "tar-xz")]
84            Fmt::TarXz => TarXz::decode(buffer),
85            #[cfg(feature = "tar-bz")]
86            Fmt::TarBz => TarBz::decode(buffer),
87            #[cfg(feature = "tar-zstd")]
88            Fmt::TarZstd => TarZstd::decode(buffer),
89            #[cfg(feature = "7z")]
90            Fmt::SevenZip => SevenZip::decode(buffer),
91        }
92    }
93
94    /// Encode files into an archive
95    ///
96    /// # Arguments
97    /// * `files` - List of files to include in the archive
98    ///
99    /// # Returns
100    /// * `Ok(Vec<u8>)` - The encoded archive as bytes
101    /// * `Err(ArchiveError)` - If encoding fails or duplicate files are detected
102    ///
103    /// # Example
104    /// ```no_run
105    /// use easy_archive::{Fmt, File};
106    /// let files = vec![
107    ///     File {
108    ///         path: "hello.txt".to_string(),
109    ///         buffer: b"Hello, world!".to_vec(),
110    ///         ..Default::default()
111    ///     }
112    /// ];
113    /// let archive = Fmt::TarGz.encode(files)?;
114    /// std::fs::write("archive.tar.gz", archive)?;
115    /// # Ok::<(), Box<dyn std::error::Error>>(())
116    /// ```
117    #[cfg(feature = "encode")]
118    pub fn encode(&self, files: Vec<File>) -> Result<Vec<u8>> {
119        match self {
120            #[cfg(feature = "zip")]
121            Fmt::Zip => Zip::encode(files),
122            #[cfg(feature = "tar")]
123            Fmt::Tar => Tar::encode(files),
124            #[cfg(feature = "tar-gz")]
125            Fmt::TarGz => TarGz::encode(files),
126            #[cfg(feature = "tar-xz")]
127            Fmt::TarXz => TarXz::encode(files),
128            #[cfg(feature = "tar-bz")]
129            Fmt::TarBz => TarBz::encode(files),
130            #[cfg(feature = "tar-zstd")]
131            Fmt::TarZstd => TarZstd::encode(files),
132            #[cfg(feature = "7z")]
133            Fmt::SevenZip => SevenZip::encode(files),
134        }
135    }
136
137    /// Guess the archive format from a filename
138    ///
139    /// # Arguments
140    /// * `name` - The filename to analyze
141    ///
142    /// # Returns
143    /// * `Some(Fmt)` - The detected format
144    /// * `None` - If the format cannot be determined
145    ///
146    /// # Example
147    /// ```
148    /// use easy_archive::Fmt;
149    /// assert_eq!(Fmt::guess("archive.tar.gz"), Some(Fmt::TarGz));
150    /// assert_eq!(Fmt::guess("file.zip"), Some(Fmt::Zip));
151    /// assert_eq!(Fmt::guess("unknown.txt"), None);
152    /// ```
153    pub fn guess(name: &str) -> Option<Self> {
154        for fmt in Fmt::iter() {
155            for ext in fmt.extensions() {
156                if name.ends_with(ext) {
157                    return Some(fmt);
158                }
159            }
160        }
161        None
162    }
163
164    /// Get the file extensions for this format
165    ///
166    /// # Returns
167    /// A slice of file extension strings (including the leading dot)
168    ///
169    /// # Example
170    /// ```
171    /// use easy_archive::Fmt;
172    /// assert_eq!(Fmt::TarGz.extensions(), &[".tar.gz", ".tgz"]);
173    /// ```
174    pub fn extensions(&self) -> &[&'static str] {
175        match self {
176            #[cfg(feature = "tar")]
177            Fmt::Tar => &[".tar"],
178            #[cfg(feature = "tar-gz")]
179            Fmt::TarGz => &[".tar.gz", ".tgz"],
180            #[cfg(feature = "tar-xz")]
181            Fmt::TarXz => &[".tar.xz", ".txz"],
182            #[cfg(feature = "tar-bz")]
183            Fmt::TarBz => &[".tar.bz2", ".tbz2", ".tbz"],
184            #[cfg(feature = "tar-zstd")]
185            Fmt::TarZstd => &[".tzstd", ".tzst", ".tar.zst"],
186            #[cfg(feature = "zip")]
187            Fmt::Zip => &[".zip"],
188            #[cfg(feature = "7z")]
189            Fmt::SevenZip => &[".7z"],
190        }
191    }
192}
193
194/// Represents a file or directory entry in an archive
195///
196/// This structure holds all the metadata and content for a single entry
197/// in an archive file.
198#[cfg_attr(feature = "wasm", wasm_bindgen::prelude::wasm_bindgen)]
199#[derive(Debug, Clone, Default)]
200pub struct File {
201    /// The file content as raw bytes
202    #[cfg_attr(feature = "wasm", wasm_bindgen(skip))]
203    pub buffer: Vec<u8>,
204
205    /// The relative path of the file within the archive
206    #[cfg_attr(feature = "wasm", wasm_bindgen(skip))]
207    pub path: String,
208
209    /// Unix file permissions (e.g., 0o755 for rwxr-xr-x)
210    pub mode: Option<u32>,
211
212    /// Whether this entry represents a directory
213    #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "isDir"))]
214    pub is_dir: bool,
215
216    /// Last modification time as Unix timestamp (seconds since epoch)
217    #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "lastModified"))]
218    pub last_modified: Option<u64>,
219}
220
221#[cfg(feature = "wasm")]
222use wasm_bindgen::prelude::*;
223
224#[cfg_attr(feature = "wasm", wasm_bindgen::prelude::wasm_bindgen)]
225impl File {
226    /// Create a new File entry
227    ///
228    /// # Arguments
229    /// * `path` - The relative path within the archive
230    /// * `buffer` - The file content
231    /// * `mode` - Optional Unix permissions
232    /// * `is_dir` - Whether this is a directory
233    /// * `last_modified` - Optional modification timestamp
234    #[cfg_attr(feature = "wasm", wasm_bindgen(constructor))]
235    pub fn new(
236        path: String,
237        buffer: Vec<u8>,
238        mode: Option<u32>,
239        is_dir: bool,
240        last_modified: Option<u64>,
241    ) -> Self {
242        File {
243            path,
244            buffer,
245            mode,
246            is_dir,
247            last_modified,
248        }
249    }
250}
251
252#[cfg(feature = "wasm")]
253#[wasm_bindgen]
254impl File {
255    /// Get the file buffer (WASM only)
256    ///
257    /// Note: This consumes the File to reduce memory consumption
258    #[wasm_bindgen(getter = buffer)]
259    pub fn get_buffer(self) -> Vec<u8> {
260        self.buffer
261    }
262
263    /// Set the file buffer (WASM only)
264    #[wasm_bindgen(setter = buffer)]
265    pub fn set_buffer(&mut self, buffer: Vec<u8>) {
266        self.buffer = buffer;
267    }
268
269    /// Get the file path (WASM only)
270    #[wasm_bindgen(getter = path)]
271    pub fn get_path(&self) -> String {
272        self.path.clone()
273    }
274
275    /// Set the file path (WASM only)
276    #[wasm_bindgen(setter = path)]
277    pub fn set_path(&mut self, path: String) {
278        self.path = path;
279    }
280
281    /// Get the buffer size in bytes (WASM only)
282    #[wasm_bindgen(getter = bufferSize)]
283    pub fn buffer_size(&self) -> usize {
284        self.buffer.len()
285    }
286
287    /// Clone the File (WASM only)
288    #[wasm_bindgen]
289    #[allow(clippy::should_implement_trait)]
290    pub fn clone(&self) -> Self {
291        Clone::clone(self)
292    }
293}
294
295#[cfg(test)]
296mod test {
297    use super::Fmt;
298
299    #[test]
300    fn test_guess() {
301        let test_cases = vec![
302            #[cfg(feature = "zip")]
303            ("a.zip", Fmt::Zip),
304            #[cfg(feature = "tar")]
305            ("a.tar", Fmt::Tar),
306            #[cfg(feature = "tar-gz")]
307            ("a.tar.gz", Fmt::TarGz),
308            #[cfg(feature = "tar-xz")]
309            ("a.tar.xz", Fmt::TarXz),
310            #[cfg(feature = "tar-bz")]
311            ("a.tar.bz2", Fmt::TarBz),
312        ];
313
314        for (name, fmt) in test_cases {
315            assert_eq!(
316                Fmt::guess(name),
317                Some(fmt),
318                "Failed to guess format for {}",
319                name
320            );
321        }
322    }
323}