easy_archive/archive/
zip.rs

1use crate::{Decode, Encode, File, tool::clean};
2use std::io::{Cursor, Read, Seek, SeekFrom, Write};
3use time::OffsetDateTime;
4use zip::DateTime;
5
6pub struct Zip;
7
8#[cfg(feature = "zip")]
9fn decode_zip(buffer: &[u8]) -> Option<Vec<File>> {
10    let mut c = Cursor::new(Vec::new());
11    c.write_all(buffer).ok()?;
12    c.seek(SeekFrom::Start(0)).ok()?;
13    let mut files = Vec::new();
14    let mut archive = zip::ZipArchive::new(c).ok()?;
15    for i in 0..archive.len() {
16        let mut file = archive.by_index(i).ok()?;
17        if file.is_file() {
18            let mut buffer = vec![];
19            file.read_to_end(&mut buffer).ok()?;
20            let path = file.name();
21            let is_dir = file.is_dir() || path.ends_with("/");
22            let path = clean(path);
23            let last_modified = file
24                .last_modified()
25                .and_then(|i| OffsetDateTime::try_from(i).ok())
26                .map(|i| i.unix_timestamp() as u64);
27
28            files.push(File::new(path, buffer.clone(), None, is_dir, last_modified));
29        }
30    }
31    Some(files)
32}
33
34#[cfg(feature = "rc-zip")]
35fn decode_rc_zip(buffer: &[u8]) -> Option<Vec<File>> {
36    use rc_zip_sync::ReadZip;
37    let reader = buffer.read_zip().ok()?;
38    let mut files = Vec::new();
39    for entry in reader.entries() {
40        let path = entry.name.clone();
41        let buffer = entry.bytes().ok()?;
42        let mode = entry.mode.0;
43        let is_dir = matches!(entry.kind(), rc_zip::parse::EntryKind::Directory);
44        let path = clean(&path);
45        files.push(File {
46            buffer,
47            path,
48            mode: Some(mode),
49            is_dir,
50        });
51    }
52    Some(files)
53}
54
55impl Decode for Zip {
56    fn decode<T: AsRef<[u8]>>(buffer: T) -> Option<Vec<File>> {
57        let buffer = buffer.as_ref();
58        #[cfg(feature = "zip")]
59        return decode_zip(buffer);
60        #[cfg(feature = "rc-zip")]
61        return decode_rc_zip(&buffer);
62    }
63}
64
65impl Encode for Zip {
66    fn encode(files: Vec<File>) -> Option<Vec<u8>> {
67        use std::collections::HashSet;
68        use std::io::prelude::*;
69        use zip::write::FullFileOptions;
70
71        let mut v = vec![];
72        let mut c = std::io::Cursor::new(&mut v);
73        let mut zip = zip::ZipWriter::new(&mut c);
74        let mut dir_set = HashSet::new();
75
76        for i in files.iter().filter(|i| i.is_dir) {
77            if dir_set.contains(&i.path) {
78                continue;
79            }
80            dir_set.insert(i.path.clone());
81            let mut options = FullFileOptions::default();
82            if let Some(last) = i.last_modified {
83                let mod_time = OffsetDateTime::from_unix_timestamp(last as i64)
84                    .ok()
85                    .and_then(|i| DateTime::try_from(i).ok());
86                if let Some(offset) = mod_time {
87                    options = options.last_modified_time(offset);
88                }
89            }
90            zip.add_directory(i.path.as_str(), options).ok()?;
91        }
92
93        for i in files.iter().filter(|i| !i.is_dir) {
94            if !i.path.contains("/") {
95                continue;
96            }
97            if let Some(p) = std::path::Path::new(&i.path).parent() {
98                let path = p.to_string_lossy().to_string();
99                if dir_set.contains(&path) || path.is_empty() {
100                    continue;
101                }
102
103                let mut options = FullFileOptions::default();
104                if let Some(last) = i.last_modified {
105                    let mod_time = OffsetDateTime::from_unix_timestamp(last as i64)
106                        .ok()
107                        .and_then(|i| DateTime::try_from(i).ok());
108                    if let Some(offset) = mod_time {
109                        options = options.last_modified_time(offset);
110                    }
111                }
112                zip.add_directory(path.clone(), options).ok()?;
113                dir_set.insert(path);
114            }
115        }
116
117        for i in &files {
118            if i.is_dir {
119                continue;
120            }
121            let mode = i.mode.unwrap_or(0o755);
122            let mut options = FullFileOptions::default()
123                // XZ is too slow in wasm
124                .compression_method(zip::CompressionMethod::Zstd)
125                .unix_permissions(mode);
126
127            if let Some(last) = i.last_modified {
128                let mod_time = OffsetDateTime::from_unix_timestamp(last as i64)
129                    .ok()
130                    .and_then(|i| DateTime::try_from(i).ok());
131                if let Some(offset) = mod_time {
132                    options = options.last_modified_time(offset);
133                }
134            }
135
136            zip.start_file(i.path.as_str(), options).ok()?;
137            zip.write_all(&i.buffer).ok()?;
138        }
139        zip.finish().ok()?;
140        Some(v)
141    }
142}