├── .gitignore ├── zip-parser ├── Cargo.toml └── src │ ├── util.rs │ └── lib.rs ├── .github └── workflows │ └── ci.yml ├── README.md ├── Cargo.toml ├── LICENSE ├── src ├── main.rs └── util.rs ├── tests └── zip.rs └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /zip-parser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zip-parser" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | thiserror = "1" 10 | memchr = "2" 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | os: [ubuntu-latest, windows-latest, macos-latest] 10 | 11 | runs-on: ${{ matrix.os }} 12 | 13 | env: 14 | RUSTFLAGS: "-D warnings" 15 | 16 | steps: 17 | - uses: actions/checkout@master 18 | - name: minimum feature 19 | run: | 20 | cargo test --no-default-features 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unzrip 2 | 3 | Simple unzip implementation 4 | 5 | ## Automatic detection filename encoding 6 | 7 | If you have ever downloaded a zip file with 8 | an encoded filename using a character set such as GBK or SHIFT-JIS, 9 | then you know what that means. 10 | 11 | ## Parallel decompression 12 | 13 | All files in zip can be decompressed independently, 14 | which means that more files there are, 15 | the more significant the performance improvement. 16 | 17 | ## Safety 18 | 19 | If you know unzip has been inactive for over 10year 20 | and downstream requires 20+ of security patches. 21 | 22 | see https://infozip.sourceforge.net/UnZip.html 23 | and https://github.com/archlinux/svntogit-packages/blob/packages/unzip/trunk/PKGBUILD#L16 24 | 25 | # License 26 | 27 | MIT 28 | -------------------------------------------------------------------------------- /zip-parser/src/util.rs: -------------------------------------------------------------------------------- 1 | pub struct Eof; 2 | 3 | 4 | #[inline] 5 | pub fn take(input: &[u8], n: usize) -> Result<(&[u8], &[u8]), Eof> { 6 | if input.len() >= n { 7 | let (prefix, suffix) = input.split_at(n); 8 | Ok((suffix, prefix)) 9 | } else { 10 | Err(Eof) 11 | } 12 | } 13 | 14 | #[inline] 15 | pub fn read_u16(input: &[u8]) -> Result<(&[u8], u16), Eof> { 16 | let mut buf = [0; 2]; 17 | let (input, output) = take(input, buf.len())?; 18 | buf.copy_from_slice(output); 19 | let output = u16::from_le_bytes(buf); 20 | Ok((input, output)) 21 | } 22 | 23 | #[inline] 24 | pub fn read_u32(input: &[u8]) -> Result<(&[u8], u32), Eof> { 25 | let mut buf = [0; 4]; 26 | let (input, output) = take(input, buf.len())?; 27 | buf.copy_from_slice(output); 28 | let output = u32::from_le_bytes(buf); 29 | Ok((input, output)) 30 | } 31 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "unzrip" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [workspace] 10 | members = [ "zip-parser" ] 11 | 12 | [features] 13 | default = [ "zstd-sys" ] 14 | zstd-sys = [ "zstd" ] 15 | 16 | [dependencies] 17 | zip-parser = { path = "zip-parser" } 18 | 19 | # tools 20 | anyhow = "1" 21 | argh = "0.1" 22 | bstr = "1" 23 | 24 | # fast 25 | rayon = "1" 26 | memmap2 = "0.5" 27 | 28 | # check 29 | crc32fast = "1" 30 | 31 | # compress 32 | flate2 = "1" 33 | zstd = { version = "0.12", features = [ "pkg-config" ], optional = true } 34 | 35 | # encoding 36 | encoding_rs = "0.8" 37 | chardetng = "0.1" 38 | 39 | # time 40 | time = "0.3" 41 | filetime = "0.2" 42 | 43 | [dev-dependencies] 44 | zip = { version = "0.6", default-features = false, features = [ "deflate" ] } 45 | tempfile = "3" 46 | assert_cmd = "2" 47 | walkdir = "2" 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | Copyright (c) 2023 quininer@live.com 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | 3 | use std::{ cmp, env, fs }; 4 | use std::io::{ self, Read }; 5 | use std::path::{ Path, PathBuf }; 6 | use argh::FromArgs; 7 | use anyhow::Context; 8 | use bstr::ByteSlice; 9 | use encoding_rs::Encoding; 10 | use rayon::prelude::*; 11 | use memmap2::MmapOptions; 12 | use flate2::bufread::DeflateDecoder; 13 | use zip_parser::{ compress, ZipArchive, CentralFileHeader }; 14 | use util::{ 15 | Decoder, Crc32Checker, FilenameEncoding, 16 | dos2time, path_join, path_open 17 | }; 18 | 19 | #[cfg(feature = "zstd-sys")] 20 | use zstd::stream::read::Decoder as ZstdDecoder; 21 | 22 | /// unzrip - extract compressed files in a ZIP archive 23 | #[derive(FromArgs)] 24 | struct Options { 25 | /// path of the ZIP archive(s). 26 | #[argh(positional)] 27 | file: Vec, 28 | 29 | /// an optional directory to which to extract files. 30 | #[argh(option, short = 'd')] 31 | exdir: Option, 32 | 33 | /// specify character set used to decode filename, 34 | /// which will be automatically detected by default. 35 | #[argh(option, short = 'O')] 36 | charset: Option, 37 | 38 | /// try to keep the original filename, 39 | /// which will ignore the charset. 40 | #[argh(switch)] 41 | keep_origin_filename: bool 42 | } 43 | 44 | fn main() -> anyhow::Result<()> { 45 | let options: Options = argh::from_env(); 46 | 47 | let target_dir = if let Some(exdir) = options.exdir { 48 | exdir 49 | } else { 50 | env::current_dir()? 51 | }; 52 | let encoding = if options.keep_origin_filename { 53 | FilenameEncoding::Os 54 | } else if let Some(label) = options.charset { 55 | let encoding = Encoding::for_label(label.as_bytes()).context("invalid encoding label")?; 56 | FilenameEncoding::Charset(encoding) 57 | } else { 58 | FilenameEncoding::Auto 59 | }; 60 | 61 | for file in options.file.iter() { 62 | unzip(encoding, &target_dir, file) 63 | .with_context(|| file.display().to_string())?; 64 | } 65 | 66 | Ok(()) 67 | } 68 | 69 | fn unzip(encoding: FilenameEncoding, target_dir: &Path, path: &Path) -> anyhow::Result<()> { 70 | println!("Archive: {}", path.display()); 71 | 72 | let fd = fs::File::open(path)?; 73 | let buf = unsafe { 74 | MmapOptions::new().map_copy_read_only(&fd)? 75 | }; 76 | 77 | let zip = ZipArchive::parse(&buf)?; 78 | let len: usize = zip.eocdr().cd_entries.try_into()?; 79 | let len = cmp::min(len, 128); 80 | 81 | zip.entries()? 82 | .try_fold(Vec::with_capacity(len), |mut acc, e| e.map(|e| { 83 | acc.push(e); 84 | acc 85 | }))? 86 | .par_iter() 87 | .try_for_each(|cfh| do_entry(encoding, &zip, &cfh, target_dir))?; 88 | 89 | Ok(()) 90 | } 91 | 92 | fn do_entry( 93 | encoding: FilenameEncoding, 94 | zip: &ZipArchive<'_>, 95 | cfh: &CentralFileHeader<'_>, 96 | target_dir: &Path 97 | ) -> anyhow::Result<()> { 98 | let (_lfh, buf) = zip.read(cfh).context("read entry failed")?; 99 | 100 | if cfh.gp_flag & 1 != 0 { 101 | anyhow::bail!("encrypt is not supported"); 102 | } 103 | 104 | let name = cfh.name; 105 | 106 | if (name.ends_with_str("/") || name.ends_with_str("\\")) 107 | && cfh.method == compress::STORE 108 | && buf.is_empty() 109 | { 110 | #[cfg(unix)] 111 | let name = name.trim_end_with(|c| c == '\\'); 112 | let path = encoding.decode(name)?; 113 | do_dir(target_dir, &path)? 114 | } else { 115 | let path = encoding.decode(&name)?; 116 | do_file(cfh, target_dir, &path, buf)?; 117 | } 118 | 119 | Ok(()) 120 | } 121 | 122 | fn do_dir(target_dir: &Path, path: &Path) -> anyhow::Result<()> { 123 | let target = path_join(target_dir, path)?; 124 | 125 | fs::create_dir_all(target) 126 | .or_else(|err| if err.kind() == io::ErrorKind::AlreadyExists { 127 | Ok(()) 128 | } else { 129 | Err(err) 130 | }) 131 | .with_context(|| path.display().to_string())?; 132 | 133 | println!(" creating: {}", path.display()); 134 | 135 | Ok(()) 136 | } 137 | 138 | fn do_file( 139 | cfh: &CentralFileHeader, 140 | target_dir: &Path, 141 | path: &Path, 142 | buf: &[u8] 143 | ) -> anyhow::Result<()> { 144 | let target = path_join(target_dir, path)?; 145 | 146 | let reader = match cfh.method { 147 | compress::STORE => Decoder::None(buf), 148 | compress::DEFLATE => Decoder::Deflate(DeflateDecoder::new(buf)), 149 | #[cfg(feature = "zstd-sys")] 150 | compress::ZSTD => Decoder::Zstd(ZstdDecoder::with_buffer(buf)?), 151 | _ => anyhow::bail!("compress method is not supported: {}", cfh.method) 152 | }; 153 | // prevent zipbomb 154 | let reader = reader.take(cfh.uncomp_size.into()); 155 | let mut reader = Crc32Checker::new(reader, cfh.crc32); 156 | 157 | let mtime = { 158 | let time = dos2time(cfh.mod_date, cfh.mod_time)?.assume_utc(); 159 | let unix_timestamp = time.unix_timestamp(); 160 | let nanos = time.nanosecond(); 161 | filetime::FileTime::from_unix_time(unix_timestamp, nanos) 162 | }; 163 | 164 | let mut fd = path_open(&target).with_context(|| path.display().to_string())?; 165 | 166 | io::copy(&mut reader, &mut fd)?; 167 | 168 | filetime::set_file_handle_times(&fd, None, Some(mtime))?; 169 | 170 | #[cfg(unix)] 171 | if cfh.ext_attrs != 0 && cfh.made_by_ver >> 8 == zip_parser::system::UNIX { 172 | use std::os::unix::fs::PermissionsExt; 173 | 174 | let perm = fs::Permissions::from_mode(cfh.ext_attrs >> 16); 175 | fd.set_permissions(util::sanitize_setuid(perm))?; 176 | } 177 | 178 | println!(" inflating: {}", path.display()); 179 | 180 | Ok(()) 181 | } 182 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use std::{ io, fs }; 2 | use std::path::{ Path, PathBuf, Component }; 3 | use std::borrow::Cow; 4 | use anyhow::Context; 5 | use bstr::ByteSlice; 6 | use encoding_rs::Encoding; 7 | use flate2::bufread::DeflateDecoder; 8 | 9 | #[cfg(feature = "zstd-sys")] 10 | use zstd::stream::read::Decoder as ZstdDecoder; 11 | 12 | 13 | pub enum Decoder { 14 | None(R), 15 | Deflate(DeflateDecoder), 16 | #[cfg(feature = "zstd-sys")] 17 | Zstd(ZstdDecoder<'static, R>) 18 | } 19 | 20 | impl io::Read for Decoder { 21 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 22 | match self { 23 | Decoder::None(reader) => io::Read::read(reader, buf), 24 | Decoder::Deflate(reader) => io::Read::read(reader, buf), 25 | #[cfg(feature = "zstd-sys")] 26 | Decoder::Zstd(reader) => io::Read::read(reader, buf) 27 | } 28 | } 29 | } 30 | 31 | pub struct Crc32Checker { 32 | reader: R, 33 | expect: u32, 34 | hasher: crc32fast::Hasher, 35 | } 36 | 37 | impl Crc32Checker { 38 | pub fn new(reader: R, expect: u32) -> Crc32Checker { 39 | Crc32Checker { 40 | reader, expect, 41 | hasher: crc32fast::Hasher::new() 42 | } 43 | } 44 | } 45 | 46 | impl io::Read for Crc32Checker { 47 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 48 | let n = io::Read::read(&mut self.reader, buf)?; 49 | 50 | if n == 0 { 51 | let crc = self.hasher.clone().finalize(); 52 | if crc != self.expect { 53 | let msg = format!("crc32 check failed. expect: {}, got: {}", 54 | self.expect, 55 | crc 56 | ); 57 | return Err(io::Error::new(io::ErrorKind::InvalidData, msg)) 58 | } 59 | } else { 60 | self.hasher.update(&buf[..n]); 61 | } 62 | 63 | Ok(n) 64 | } 65 | } 66 | 67 | #[derive(Clone, Copy)] 68 | pub enum FilenameEncoding { 69 | Os, 70 | Charset(&'static Encoding), 71 | Auto 72 | } 73 | 74 | impl FilenameEncoding { 75 | pub fn decode<'a>(self, name: &'a [u8]) -> anyhow::Result> { 76 | fn cow_str_to_path<'a>(name: Cow<'a, str>) -> Cow<'a, Path> { 77 | match name { 78 | Cow::Borrowed(name) => Cow::Borrowed(Path::new(name)), 79 | Cow::Owned(name) => Cow::Owned(name.into()) 80 | } 81 | } 82 | 83 | match self { 84 | FilenameEncoding::Os => { 85 | name.to_path() 86 | .map(Cow::Borrowed) 87 | .context("Convert to os str failed") 88 | .with_context(|| String::from_utf8_lossy(name).into_owned()) 89 | }, 90 | FilenameEncoding::Charset(encoding) => { 91 | let (name, ..) = encoding.decode(name); 92 | Ok(cow_str_to_path(name)) 93 | }, 94 | FilenameEncoding::Auto => if let Ok(name) = std::str::from_utf8(name) { 95 | Ok(Path::new(name).into()) 96 | } else { 97 | let mut encoding_detector = chardetng::EncodingDetector::new(); 98 | encoding_detector.feed(name, true); 99 | let (name, ..) = encoding_detector.guess(None, false).decode(name); 100 | Ok(cow_str_to_path(name)) 101 | } 102 | } 103 | } 104 | } 105 | 106 | pub fn dos2time(dos_date: u16, dos_time: u16) 107 | -> anyhow::Result 108 | { 109 | let sec = (dos_time & 0x1f) * 2; 110 | let min = (dos_time >> 5) & 0x3f; 111 | let hour = dos_time >> 11; 112 | 113 | let day = dos_date & 0x1f; 114 | let mon = (dos_date >> 5) & 0xf; 115 | let year = (dos_date >> 9) + 1980; 116 | 117 | let mon: u8 = mon.try_into().context("mon cast")?; 118 | let mon: time::Month = mon.try_into()?; 119 | 120 | let time = time::Time::from_hms( 121 | hour.try_into().context("hour cast")?, 122 | min.try_into().context("min cast")?, 123 | sec.try_into().context("sec cast")? 124 | )?; 125 | let date = time::Date::from_calendar_date( 126 | year.try_into().context("year cast")?, 127 | mon, 128 | day.try_into().context("day cast")? 129 | )?; 130 | 131 | Ok(date.with_time(time)) 132 | } 133 | 134 | pub fn path_join(base: &Path, path: &Path) -> anyhow::Result { 135 | // check path 136 | path.components() 137 | .try_fold(0u32, |mut depth, next| { 138 | match next { 139 | Component::RootDir | Component::Prefix(_) => 140 | anyhow::bail!("must relative path: {:?}", path), 141 | Component::Normal(_) => depth += 1, 142 | Component::ParentDir => { 143 | depth = depth.checked_sub(1) 144 | .context("filename over the path limit") 145 | .with_context(|| path.display().to_string())?; 146 | }, 147 | Component::CurDir => () 148 | } 149 | 150 | Ok(depth) 151 | })?; 152 | 153 | Ok(base.join(path)) 154 | } 155 | 156 | pub fn path_open(path: &Path) -> io::Result { 157 | let mut open_options = fs::File::options(); 158 | open_options.write(true).append(true).create_new(true); 159 | 160 | match open_options.open(path) { 161 | Ok(fd) => Ok(fd), 162 | Err(err) => { 163 | // parent dir not found 164 | if err.kind() == io::ErrorKind::NotFound { 165 | if let Some(dir) = path.parent() { 166 | fs::create_dir_all(dir) 167 | .or_else(|err| if err.kind() == io::ErrorKind::AlreadyExists { 168 | Ok(()) 169 | } else { 170 | Err(err) 171 | })?; 172 | return open_options.open(path); 173 | } 174 | } 175 | 176 | Err(err) 177 | } 178 | } 179 | } 180 | 181 | #[cfg(unix)] 182 | pub fn sanitize_setuid(input: std::fs::Permissions) -> std::fs::Permissions { 183 | use std::os::unix::fs::PermissionsExt; 184 | 185 | const SETUID_AND_SETGID: u32 = 0b11 << 9; 186 | const MASK: u32 = !SETUID_AND_SETGID; 187 | 188 | let sanitized_mode = input.mode() & MASK; 189 | std::fs::Permissions::from_mode(sanitized_mode) 190 | } 191 | -------------------------------------------------------------------------------- /tests/zip.rs: -------------------------------------------------------------------------------- 1 | use std::{ fs, io }; 2 | use std::path::{ Path, PathBuf }; 3 | use bstr::ByteSlice; 4 | use tempfile::tempdir; 5 | use zip::ZipWriter; 6 | use assert_cmd::cmd::Command; 7 | 8 | 9 | fn hash_file(path: &Path) -> anyhow::Result { 10 | use std::hash::Hasher; 11 | use std::collections::hash_map::DefaultHasher; 12 | 13 | struct HashWriter(DefaultHasher); 14 | 15 | impl io::Write for HashWriter { 16 | fn write(&mut self, buf: &[u8]) -> io::Result { 17 | self.0.write(buf); 18 | Ok(buf.len()) 19 | } 20 | 21 | fn flush(&mut self) -> io::Result<()> { 22 | Ok(()) 23 | } 24 | } 25 | 26 | let mut fd = fs::File::open(path)?; 27 | let mut hasher = HashWriter(DefaultHasher::new()); 28 | 29 | io::copy(&mut fd, &mut hasher)?; 30 | 31 | Ok(hasher.0.finish()) 32 | } 33 | 34 | fn list_dir(path: &Path) -> anyhow::Result> { 35 | let mut list = Vec::new(); 36 | 37 | for entry in walkdir::WalkDir::new(path) 38 | .max_depth(2) 39 | { 40 | let entry = entry?; 41 | 42 | if entry.path() != path { 43 | let path = entry.path().strip_prefix(path)?; 44 | list.push(path.into()); 45 | } 46 | } 47 | 48 | Ok(list) 49 | } 50 | 51 | #[test] 52 | fn test_simple_zip_file() -> anyhow::Result<()> { 53 | let dir = tempdir()?; 54 | let dir = dir.path(); 55 | 56 | let path = dir.join("test1.zip"); 57 | 58 | // create zip 59 | { 60 | let fd = fs::File::create(&path)?; 61 | let mut writer = ZipWriter::new(fd); 62 | 63 | writer.start_file("Cargo.toml", Default::default())?; 64 | io::copy(&mut fs::File::open("Cargo.toml")?, &mut writer)?; 65 | 66 | writer.add_directory("lock/", Default::default())?; 67 | writer.start_file("lock/Cargo.lock", Default::default())?; 68 | io::copy(&mut fs::File::open("Cargo.lock")?, &mut writer)?; 69 | 70 | writer.add_symlink("lock/Cargo.toml", "Cargo.toml", Default::default())?; 71 | writer.add_directory("lock2\\", Default::default())?; 72 | 73 | writer.finish()?; 74 | } 75 | 76 | Command::cargo_bin("unzrip")? 77 | .arg(&path) 78 | .arg("-d") 79 | .arg(dir) 80 | .assert() 81 | .success(); 82 | 83 | assert_eq!(hash_file(Path::new("Cargo.toml"))?, hash_file(&dir.join("Cargo.toml"))?); 84 | assert_eq!(hash_file(Path::new("Cargo.lock"))?, hash_file(&dir.join("lock/Cargo.lock"))?); 85 | 86 | let mut list = list_dir(dir)?; 87 | list.sort(); 88 | 89 | assert_eq!(list, vec![ 90 | Path::new("Cargo.toml"), 91 | Path::new("lock"), 92 | Path::new("lock/Cargo.lock"), 93 | Path::new("lock/Cargo.toml"), 94 | Path::new("lock2"), 95 | Path::new("test1.zip"), 96 | ]); 97 | 98 | Ok(()) 99 | } 100 | 101 | 102 | #[test] 103 | fn test_encoding_filename() -> anyhow::Result<()> { 104 | let dir = tempdir()?; 105 | let dir = dir.path(); 106 | 107 | let path = dir.join("test2.zip"); 108 | 109 | // create zip 110 | { 111 | let fd = fs::File::create(&path)?; 112 | let mut writer = ZipWriter::new(fd); 113 | 114 | let name = "中文漢字"; 115 | let (name2, _, _) = encoding_rs::GBK.encode(name); 116 | let name2 = name2.into_owned(); 117 | assert_ne!(name.as_bytes(), &name2); 118 | 119 | // Just test :( 120 | let bad_name = unsafe { 121 | String::from_utf8_unchecked(name2) 122 | }; 123 | 124 | writer.start_file(bad_name, Default::default())?; 125 | 126 | let name = "かんじ"; 127 | let (name2, _, _) = encoding_rs::SHIFT_JIS.encode(name); 128 | let name2 = name2.into_owned(); 129 | assert_ne!(name.as_bytes(), &name2); 130 | 131 | // Just test :( 132 | let bad_name = unsafe { 133 | String::from_utf8_unchecked(name2) 134 | }; 135 | 136 | writer.start_file(bad_name, Default::default())?; 137 | 138 | writer.finish()?; 139 | } 140 | 141 | Command::cargo_bin("unzrip")? 142 | .arg(&path) 143 | .arg("-d") 144 | .arg(dir) 145 | .assert() 146 | .success(); 147 | 148 | let mut list = list_dir(dir)?; 149 | list.sort(); 150 | 151 | assert_eq!(list, vec![ 152 | Path::new("test2.zip"), 153 | Path::new("かんじ"), 154 | Path::new("中文漢字"), 155 | ]); 156 | 157 | Ok(()) 158 | } 159 | 160 | #[cfg(target_os = "linux")] 161 | #[test] 162 | fn test_unix_filename() -> anyhow::Result<()> { 163 | let dir = tempdir()?; 164 | let dir = dir.path(); 165 | 166 | let path = dir.join("test3.zip"); 167 | let name = vec![0x12, 0x23, 0x34, 0x45, 0x56, 0x67, 0x78, 0x89, 0x90]; 168 | 169 | // create zip 170 | { 171 | let fd = fs::File::create(&path)?; 172 | let mut writer = ZipWriter::new(fd); 173 | 174 | // Just test :( 175 | let bad_name = unsafe { 176 | String::from_utf8_unchecked(name.clone()) 177 | }; 178 | 179 | writer.start_file(bad_name, Default::default())?; 180 | 181 | writer.finish()?; 182 | } 183 | 184 | Command::cargo_bin("unzrip")? 185 | .arg(&path) 186 | .arg("--keep-origin-filename") 187 | .arg("-d") 188 | .arg(dir) 189 | .assert() 190 | .success(); 191 | 192 | let mut list = list_dir(dir)?; 193 | list.sort(); 194 | 195 | assert_eq!(list, vec![ 196 | name.to_path().unwrap(), 197 | Path::new("test3.zip"), 198 | ]); 199 | 200 | Ok(()) 201 | } 202 | 203 | #[test] 204 | fn test_evil_path() -> anyhow::Result<()> { 205 | let dir = tempdir()?; 206 | let dir = dir.path(); 207 | 208 | let path = dir.join("test4.zip"); 209 | 210 | // create zip 211 | { 212 | let fd = fs::File::create(&path)?; 213 | let mut writer = ZipWriter::new(fd); 214 | 215 | writer.start_file("/home/user/.bashrc", Default::default())?; 216 | writer.finish()?; 217 | } 218 | 219 | 220 | let assert = Command::cargo_bin("unzrip")? 221 | .arg(&path) 222 | .arg("-d") 223 | .arg(dir) 224 | .assert() 225 | .failure(); 226 | assert!(assert.get_output().stderr.contains_str("must relative path")); 227 | 228 | Ok(()) 229 | } 230 | 231 | 232 | #[test] 233 | fn test_evil_path2() -> anyhow::Result<()> { 234 | let dir = tempdir()?; 235 | let dir = dir.path(); 236 | 237 | let path = dir.join("test5.zip"); 238 | 239 | // create zip 240 | { 241 | let fd = fs::File::create(&path)?; 242 | let mut writer = ZipWriter::new(fd); 243 | 244 | writer.start_file("../../../../../../../../.bashrc", Default::default())?; 245 | writer.finish()?; 246 | } 247 | 248 | 249 | let assert = Command::cargo_bin("unzrip")? 250 | .arg(&path) 251 | .arg("-d") 252 | .arg(dir) 253 | .assert() 254 | .failure(); 255 | assert!(assert.get_output().stderr.contains_str("filename over the path limit")); 256 | 257 | Ok(()) 258 | } 259 | -------------------------------------------------------------------------------- /zip-parser/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! https://www.hanshq.net/zip.html#zip 2 | 3 | mod util; 4 | 5 | use thiserror::Error; 6 | use memchr::memmem::rfind; 7 | use util::{ Eof, take, read_u16, read_u32 }; 8 | 9 | 10 | pub mod compress { 11 | pub const STORE: u16 = 0; 12 | pub const DEFLATE: u16 = 8; 13 | pub const ZSTD: u16 = 93; 14 | } 15 | 16 | pub mod system { 17 | pub const DOS: u16 = 0; 18 | pub const UNIX: u16 = 3; 19 | } 20 | 21 | #[non_exhaustive] 22 | #[derive(Debug)] 23 | pub struct EocdRecord<'a> { 24 | pub disk_nbr: u16, 25 | pub cd_start_disk: u16, 26 | pub disk_cd_entries: u16, 27 | pub cd_entries: u16, 28 | pub cd_size: u32, 29 | pub cd_offset: u32, 30 | pub comment: &'a [u8] 31 | } 32 | 33 | #[derive(Error, Debug)] 34 | pub enum Error { 35 | #[error("eof")] 36 | Eof, 37 | #[error("bad eocdr magic number")] 38 | BadEocdr, 39 | #[error("bad cfh magic number")] 40 | BadCfh, 41 | #[error("bad lfh magic number")] 42 | BadLfh, 43 | #[error("not supported")] 44 | Unsupported, 45 | #[error("offset overflow")] 46 | OffsetOverflow 47 | } 48 | 49 | impl From for Error { 50 | #[inline] 51 | fn from(_err: Eof) -> Error { 52 | Error::Eof 53 | } 54 | } 55 | 56 | impl EocdRecord<'_> { 57 | fn find(buf: &[u8]) -> Result, Error> { 58 | const EOCDR_SIGNATURE: &[u8; 4] = &[b'P', b'K', 5, 6]; 59 | const MAX_BACK_OFFSET: usize = 1024 * 128; 60 | 61 | let eocdr_buf = { 62 | let max_back_buf = buf.len() 63 | .checked_sub(MAX_BACK_OFFSET) 64 | .map(|pos| &buf[pos..]) 65 | .unwrap_or(buf); 66 | 67 | let eocdr_offset = rfind(max_back_buf, EOCDR_SIGNATURE) 68 | .ok_or(Error::BadEocdr)?; 69 | &max_back_buf[eocdr_offset..] 70 | }; 71 | 72 | let input = eocdr_buf; 73 | let (input, _) = take(input, EOCDR_SIGNATURE.len())?; 74 | let (input, disk_nbr) = read_u16(input)?; 75 | let (input, cd_start_disk) = read_u16(input)?; 76 | let (input, disk_cd_entries) = read_u16(input)?; 77 | let (input, cd_entries) = read_u16(input)?; 78 | let (input, cd_size) = read_u32(input)?; 79 | let (input, cd_offset) = read_u32(input)?; 80 | let (input, comment_len) = read_u16(input)?; 81 | let (_input, comment) = take(input, comment_len.into())?; 82 | 83 | Ok(EocdRecord { 84 | disk_nbr, 85 | cd_start_disk, 86 | disk_cd_entries, 87 | cd_entries, 88 | cd_size, 89 | cd_offset, 90 | comment 91 | }) 92 | } 93 | } 94 | 95 | #[non_exhaustive] 96 | #[derive(Debug)] 97 | pub struct CentralFileHeader<'a> { 98 | pub made_by_ver: u16, 99 | pub extract_ver: u16, 100 | pub gp_flag: u16, 101 | pub method: u16, 102 | pub mod_time: u16, 103 | pub mod_date: u16, 104 | pub crc32: u32, 105 | pub comp_size: u32, 106 | pub uncomp_size: u32, 107 | pub disk_nbr_start: u16, 108 | pub int_attrs: u16, 109 | pub ext_attrs: u32, 110 | pub lfh_offset: u32, 111 | pub name: &'a [u8], 112 | pub extra: &'a [u8], 113 | pub comment: &'a [u8] 114 | } 115 | 116 | impl CentralFileHeader<'_> { 117 | fn parse(input: &[u8]) -> Result<(&[u8], CentralFileHeader<'_>), Error> { 118 | const CFH_SIGNATURE: &[u8; 4] = &[b'P', b'K', 1, 2]; 119 | 120 | let (input, expect_sig) = take(input, CFH_SIGNATURE.len())?; 121 | if expect_sig != CFH_SIGNATURE { 122 | return Err(Error::BadCfh); 123 | } 124 | 125 | let (input, made_by_ver) = read_u16(input)?; 126 | let (input, extract_ver) = read_u16(input)?; 127 | let (input, gp_flag) = read_u16(input)?; 128 | let (input, method) = read_u16(input)?; 129 | let (input, mod_time) = read_u16(input)?; 130 | let (input, mod_date) = read_u16(input)?; 131 | let (input, crc32) = read_u32(input)?; 132 | let (input, comp_size) = read_u32(input)?; 133 | let (input, uncomp_size) = read_u32(input)?; 134 | let (input, name_len) = read_u16(input)?; 135 | let (input, extra_len) = read_u16(input)?; 136 | let (input, comment_len) = read_u16(input)?; 137 | let (input, disk_nbr_start) = read_u16(input)?; 138 | let (input, int_attrs) = read_u16(input)?; 139 | let (input, ext_attrs) = read_u32(input)?; 140 | let (input, lfh_offset) = read_u32(input)?; 141 | let (input, name) = take(input, name_len.into())?; 142 | let (input, extra) = take(input, extra_len.into())?; 143 | let (input, comment) = take(input, comment_len.into())?; 144 | 145 | let header = CentralFileHeader { 146 | made_by_ver, 147 | extract_ver, 148 | gp_flag, 149 | method, 150 | mod_time, 151 | mod_date, 152 | crc32, 153 | comp_size, 154 | uncomp_size, 155 | disk_nbr_start, 156 | int_attrs, 157 | ext_attrs, 158 | lfh_offset, 159 | name, 160 | extra, 161 | comment 162 | }; 163 | 164 | Ok((input, header)) 165 | } 166 | } 167 | 168 | #[non_exhaustive] 169 | #[derive(Debug)] 170 | pub struct LocalFileHeader<'a> { 171 | pub extract_ver: u16, 172 | pub gp_flag: u16, 173 | pub method: u16, 174 | pub mod_time: u16, 175 | pub mod_date: u16, 176 | pub crc32: u32, 177 | pub comp_size: u32, 178 | pub uncomp_size: u32, 179 | pub name: &'a [u8], 180 | pub extra: &'a [u8] 181 | } 182 | 183 | impl LocalFileHeader<'_> { 184 | fn parse(input: &[u8]) -> Result<(&[u8], LocalFileHeader<'_>), Error> { 185 | const LFH_SIGNATURE: &[u8; 4] = &[b'P', b'K', 3, 4]; 186 | 187 | let (input, expect_sig) = take(input, LFH_SIGNATURE.len())?; 188 | if expect_sig != LFH_SIGNATURE { 189 | return Err(Error::BadLfh); 190 | } 191 | 192 | let (input, extract_ver) = read_u16(input)?; 193 | let (input, gp_flag) = read_u16(input)?; 194 | let (input, method) = read_u16(input)?; 195 | let (input, mod_time) = read_u16(input)?; 196 | let (input, mod_date) = read_u16(input)?; 197 | let (input, crc32) = read_u32(input)?; 198 | let (input, comp_size) = read_u32(input)?; 199 | let (input, uncomp_size) = read_u32(input)?; 200 | let (input, name_len) = read_u16(input)?; 201 | let (input, extra_len) = read_u16(input)?; 202 | let (input, name) = take(input, name_len.into())?; 203 | let (input, extra) = take(input, extra_len.into())?; 204 | 205 | let header = LocalFileHeader { 206 | extract_ver, 207 | gp_flag, 208 | method, 209 | mod_time, 210 | mod_date, 211 | crc32, 212 | comp_size, 213 | uncomp_size, 214 | name, 215 | extra 216 | }; 217 | 218 | Ok((input, header)) 219 | } 220 | } 221 | 222 | pub struct ZipArchive<'a> { 223 | buf: &'a [u8], 224 | eocdr: EocdRecord<'a> 225 | } 226 | 227 | impl ZipArchive<'_> { 228 | pub fn parse(buf: &[u8]) -> Result, Error> { 229 | let eocdr = EocdRecord::find(buf)?; 230 | 231 | if eocdr.disk_nbr != 0 232 | || eocdr.cd_start_disk != 0 233 | || eocdr.disk_cd_entries != eocdr.cd_entries 234 | { 235 | return Err(Error::Unsupported); 236 | } 237 | 238 | Ok(ZipArchive { buf, eocdr }) 239 | } 240 | 241 | pub fn eocdr(&self) -> &EocdRecord<'_> { 242 | &self.eocdr 243 | } 244 | 245 | pub fn entries(&self) -> Result, Error> { 246 | let offset: usize = self.eocdr.cd_offset.try_into() 247 | .map_err(|_| Error::OffsetOverflow)?; 248 | let buf = self.buf.get(offset..) 249 | .ok_or(Error::OffsetOverflow)?; 250 | let count = self.eocdr.cd_entries; 251 | 252 | Ok(ZipEntries { buf, count }) 253 | } 254 | 255 | pub fn read<'a>(&'a self, cfh: &CentralFileHeader) -> Result<(LocalFileHeader<'a>, &'a [u8]), Error> { 256 | let offset: usize = cfh.lfh_offset.try_into() 257 | .map_err(|_| Error::OffsetOverflow)?; 258 | let buf = self.buf.get(offset..) 259 | .ok_or(Error::OffsetOverflow)?; 260 | 261 | let (input, lfh) = LocalFileHeader::parse(buf)?; 262 | 263 | let size = cfh.comp_size.try_into() 264 | .map_err(|_| Error::OffsetOverflow)?; 265 | let (_, buf) = take(input, size)?; 266 | 267 | Ok((lfh, buf)) 268 | } 269 | } 270 | 271 | pub struct ZipEntries<'a> { 272 | buf: &'a [u8], 273 | count: u16 274 | } 275 | 276 | impl<'a> Iterator for ZipEntries<'a> { 277 | type Item = Result, Error>; 278 | 279 | fn next(&mut self) -> Option { 280 | let new_count = self.count.checked_sub(1)?; 281 | 282 | let input = self.buf; 283 | let (input, cfh) = match CentralFileHeader::parse(input) { 284 | Ok(output) => output, 285 | Err(err) => return Some(Err(err)) 286 | }; 287 | 288 | self.buf = input; 289 | self.count = new_count; 290 | 291 | Some(Ok(cfh)) 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "anstyle" 13 | version = "0.3.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "453bc2a7b261f8c4d1ce5b2c6c222d648d00988d30315e4911fbddc4ddf8983c" 16 | 17 | [[package]] 18 | name = "anyhow" 19 | version = "1.0.69" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" 22 | 23 | [[package]] 24 | name = "argh" 25 | version = "0.1.10" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "ab257697eb9496bf75526f0217b5ed64636a9cfafa78b8365c71bd283fcef93e" 28 | dependencies = [ 29 | "argh_derive", 30 | "argh_shared", 31 | ] 32 | 33 | [[package]] 34 | name = "argh_derive" 35 | version = "0.1.10" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "b382dbd3288e053331f03399e1db106c9fb0d8562ad62cb04859ae926f324fa6" 38 | dependencies = [ 39 | "argh_shared", 40 | "proc-macro2", 41 | "quote", 42 | "syn", 43 | ] 44 | 45 | [[package]] 46 | name = "argh_shared" 47 | version = "0.1.10" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "64cb94155d965e3d37ffbbe7cc5b82c3dd79dd33bd48e536f73d2cfb8d85506f" 50 | 51 | [[package]] 52 | name = "assert_cmd" 53 | version = "2.0.10" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "ec0b2340f55d9661d76793b2bfc2eb0e62689bd79d067a95707ea762afd5e9dd" 56 | dependencies = [ 57 | "anstyle", 58 | "bstr", 59 | "doc-comment", 60 | "predicates", 61 | "predicates-core", 62 | "predicates-tree", 63 | "wait-timeout", 64 | ] 65 | 66 | [[package]] 67 | name = "autocfg" 68 | version = "1.1.0" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 71 | 72 | [[package]] 73 | name = "bitflags" 74 | version = "1.3.2" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 77 | 78 | [[package]] 79 | name = "bstr" 80 | version = "1.3.0" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "5ffdb39cb703212f3c11973452c2861b972f757b021158f3516ba10f2fa8b2c1" 83 | dependencies = [ 84 | "memchr", 85 | "once_cell", 86 | "regex-automata", 87 | "serde", 88 | ] 89 | 90 | [[package]] 91 | name = "byteorder" 92 | version = "1.4.3" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 95 | 96 | [[package]] 97 | name = "cc" 98 | version = "1.0.79" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 101 | dependencies = [ 102 | "jobserver", 103 | ] 104 | 105 | [[package]] 106 | name = "cfg-if" 107 | version = "1.0.0" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 110 | 111 | [[package]] 112 | name = "chardetng" 113 | version = "0.1.17" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "14b8f0b65b7b08ae3c8187e8d77174de20cb6777864c6b832d8ad365999cf1ea" 116 | dependencies = [ 117 | "cfg-if", 118 | "encoding_rs", 119 | "memchr", 120 | ] 121 | 122 | [[package]] 123 | name = "crc32fast" 124 | version = "1.3.2" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" 127 | dependencies = [ 128 | "cfg-if", 129 | ] 130 | 131 | [[package]] 132 | name = "crossbeam-channel" 133 | version = "0.5.7" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "cf2b3e8478797446514c91ef04bafcb59faba183e621ad488df88983cc14128c" 136 | dependencies = [ 137 | "cfg-if", 138 | "crossbeam-utils", 139 | ] 140 | 141 | [[package]] 142 | name = "crossbeam-deque" 143 | version = "0.8.3" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" 146 | dependencies = [ 147 | "cfg-if", 148 | "crossbeam-epoch", 149 | "crossbeam-utils", 150 | ] 151 | 152 | [[package]] 153 | name = "crossbeam-epoch" 154 | version = "0.9.14" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" 157 | dependencies = [ 158 | "autocfg", 159 | "cfg-if", 160 | "crossbeam-utils", 161 | "memoffset", 162 | "scopeguard", 163 | ] 164 | 165 | [[package]] 166 | name = "crossbeam-utils" 167 | version = "0.8.15" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" 170 | dependencies = [ 171 | "cfg-if", 172 | ] 173 | 174 | [[package]] 175 | name = "difflib" 176 | version = "0.4.0" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 179 | 180 | [[package]] 181 | name = "doc-comment" 182 | version = "0.3.3" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 185 | 186 | [[package]] 187 | name = "either" 188 | version = "1.8.1" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" 191 | 192 | [[package]] 193 | name = "encoding_rs" 194 | version = "0.8.32" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" 197 | dependencies = [ 198 | "cfg-if", 199 | ] 200 | 201 | [[package]] 202 | name = "errno" 203 | version = "0.2.8" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 206 | dependencies = [ 207 | "errno-dragonfly", 208 | "libc", 209 | "winapi", 210 | ] 211 | 212 | [[package]] 213 | name = "errno-dragonfly" 214 | version = "0.1.2" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 217 | dependencies = [ 218 | "cc", 219 | "libc", 220 | ] 221 | 222 | [[package]] 223 | name = "fastrand" 224 | version = "1.9.0" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" 227 | dependencies = [ 228 | "instant", 229 | ] 230 | 231 | [[package]] 232 | name = "filetime" 233 | version = "0.2.20" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412" 236 | dependencies = [ 237 | "cfg-if", 238 | "libc", 239 | "redox_syscall", 240 | "windows-sys 0.45.0", 241 | ] 242 | 243 | [[package]] 244 | name = "flate2" 245 | version = "1.0.25" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" 248 | dependencies = [ 249 | "crc32fast", 250 | "miniz_oxide", 251 | ] 252 | 253 | [[package]] 254 | name = "hermit-abi" 255 | version = "0.2.6" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" 258 | dependencies = [ 259 | "libc", 260 | ] 261 | 262 | [[package]] 263 | name = "instant" 264 | version = "0.1.12" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 267 | dependencies = [ 268 | "cfg-if", 269 | ] 270 | 271 | [[package]] 272 | name = "io-lifetimes" 273 | version = "1.0.6" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "cfa919a82ea574332e2de6e74b4c36e74d41982b335080fa59d4ef31be20fdf3" 276 | dependencies = [ 277 | "libc", 278 | "windows-sys 0.45.0", 279 | ] 280 | 281 | [[package]] 282 | name = "itertools" 283 | version = "0.10.5" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 286 | dependencies = [ 287 | "either", 288 | ] 289 | 290 | [[package]] 291 | name = "jobserver" 292 | version = "0.1.26" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" 295 | dependencies = [ 296 | "libc", 297 | ] 298 | 299 | [[package]] 300 | name = "libc" 301 | version = "0.2.140" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" 304 | 305 | [[package]] 306 | name = "linux-raw-sys" 307 | version = "0.1.4" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" 310 | 311 | [[package]] 312 | name = "memchr" 313 | version = "2.5.0" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 316 | 317 | [[package]] 318 | name = "memmap2" 319 | version = "0.5.10" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" 322 | dependencies = [ 323 | "libc", 324 | ] 325 | 326 | [[package]] 327 | name = "memoffset" 328 | version = "0.8.0" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" 331 | dependencies = [ 332 | "autocfg", 333 | ] 334 | 335 | [[package]] 336 | name = "miniz_oxide" 337 | version = "0.6.2" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" 340 | dependencies = [ 341 | "adler", 342 | ] 343 | 344 | [[package]] 345 | name = "num_cpus" 346 | version = "1.15.0" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" 349 | dependencies = [ 350 | "hermit-abi", 351 | "libc", 352 | ] 353 | 354 | [[package]] 355 | name = "once_cell" 356 | version = "1.17.1" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" 359 | 360 | [[package]] 361 | name = "pkg-config" 362 | version = "0.3.26" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" 365 | 366 | [[package]] 367 | name = "predicates" 368 | version = "3.0.1" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "1ba7d6ead3e3966038f68caa9fc1f860185d95a793180bbcfe0d0da47b3961ed" 371 | dependencies = [ 372 | "anstyle", 373 | "difflib", 374 | "itertools", 375 | "predicates-core", 376 | ] 377 | 378 | [[package]] 379 | name = "predicates-core" 380 | version = "1.0.6" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" 383 | 384 | [[package]] 385 | name = "predicates-tree" 386 | version = "1.0.9" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" 389 | dependencies = [ 390 | "predicates-core", 391 | "termtree", 392 | ] 393 | 394 | [[package]] 395 | name = "proc-macro2" 396 | version = "1.0.52" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224" 399 | dependencies = [ 400 | "unicode-ident", 401 | ] 402 | 403 | [[package]] 404 | name = "quote" 405 | version = "1.0.26" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" 408 | dependencies = [ 409 | "proc-macro2", 410 | ] 411 | 412 | [[package]] 413 | name = "rayon" 414 | version = "1.7.0" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" 417 | dependencies = [ 418 | "either", 419 | "rayon-core", 420 | ] 421 | 422 | [[package]] 423 | name = "rayon-core" 424 | version = "1.11.0" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" 427 | dependencies = [ 428 | "crossbeam-channel", 429 | "crossbeam-deque", 430 | "crossbeam-utils", 431 | "num_cpus", 432 | ] 433 | 434 | [[package]] 435 | name = "redox_syscall" 436 | version = "0.2.16" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 439 | dependencies = [ 440 | "bitflags", 441 | ] 442 | 443 | [[package]] 444 | name = "regex-automata" 445 | version = "0.1.10" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 448 | 449 | [[package]] 450 | name = "rustix" 451 | version = "0.36.9" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "fd5c6ff11fecd55b40746d1995a02f2eb375bf8c00d192d521ee09f42bef37bc" 454 | dependencies = [ 455 | "bitflags", 456 | "errno", 457 | "io-lifetimes", 458 | "libc", 459 | "linux-raw-sys", 460 | "windows-sys 0.45.0", 461 | ] 462 | 463 | [[package]] 464 | name = "same-file" 465 | version = "1.0.6" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 468 | dependencies = [ 469 | "winapi-util", 470 | ] 471 | 472 | [[package]] 473 | name = "scopeguard" 474 | version = "1.1.0" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 477 | 478 | [[package]] 479 | name = "serde" 480 | version = "1.0.156" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "314b5b092c0ade17c00142951e50ced110ec27cea304b1037c6969246c2469a4" 483 | 484 | [[package]] 485 | name = "syn" 486 | version = "1.0.109" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 489 | dependencies = [ 490 | "proc-macro2", 491 | "quote", 492 | "unicode-ident", 493 | ] 494 | 495 | [[package]] 496 | name = "tempfile" 497 | version = "3.4.0" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" 500 | dependencies = [ 501 | "cfg-if", 502 | "fastrand", 503 | "redox_syscall", 504 | "rustix", 505 | "windows-sys 0.42.0", 506 | ] 507 | 508 | [[package]] 509 | name = "termtree" 510 | version = "0.4.1" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" 513 | 514 | [[package]] 515 | name = "thiserror" 516 | version = "1.0.39" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c" 519 | dependencies = [ 520 | "thiserror-impl", 521 | ] 522 | 523 | [[package]] 524 | name = "thiserror-impl" 525 | version = "1.0.39" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" 528 | dependencies = [ 529 | "proc-macro2", 530 | "quote", 531 | "syn", 532 | ] 533 | 534 | [[package]] 535 | name = "time" 536 | version = "0.3.20" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" 539 | dependencies = [ 540 | "serde", 541 | "time-core", 542 | ] 543 | 544 | [[package]] 545 | name = "time-core" 546 | version = "0.1.0" 547 | source = "registry+https://github.com/rust-lang/crates.io-index" 548 | checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" 549 | 550 | [[package]] 551 | name = "unicode-ident" 552 | version = "1.0.8" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" 555 | 556 | [[package]] 557 | name = "unzrip" 558 | version = "0.1.0" 559 | dependencies = [ 560 | "anyhow", 561 | "argh", 562 | "assert_cmd", 563 | "bstr", 564 | "chardetng", 565 | "crc32fast", 566 | "encoding_rs", 567 | "filetime", 568 | "flate2", 569 | "memmap2", 570 | "rayon", 571 | "tempfile", 572 | "time", 573 | "walkdir", 574 | "zip", 575 | "zip-parser", 576 | "zstd", 577 | ] 578 | 579 | [[package]] 580 | name = "wait-timeout" 581 | version = "0.2.0" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 584 | dependencies = [ 585 | "libc", 586 | ] 587 | 588 | [[package]] 589 | name = "walkdir" 590 | version = "2.3.3" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" 593 | dependencies = [ 594 | "same-file", 595 | "winapi-util", 596 | ] 597 | 598 | [[package]] 599 | name = "winapi" 600 | version = "0.3.9" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 603 | dependencies = [ 604 | "winapi-i686-pc-windows-gnu", 605 | "winapi-x86_64-pc-windows-gnu", 606 | ] 607 | 608 | [[package]] 609 | name = "winapi-i686-pc-windows-gnu" 610 | version = "0.4.0" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 613 | 614 | [[package]] 615 | name = "winapi-util" 616 | version = "0.1.5" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 619 | dependencies = [ 620 | "winapi", 621 | ] 622 | 623 | [[package]] 624 | name = "winapi-x86_64-pc-windows-gnu" 625 | version = "0.4.0" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 628 | 629 | [[package]] 630 | name = "windows-sys" 631 | version = "0.42.0" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 634 | dependencies = [ 635 | "windows_aarch64_gnullvm", 636 | "windows_aarch64_msvc", 637 | "windows_i686_gnu", 638 | "windows_i686_msvc", 639 | "windows_x86_64_gnu", 640 | "windows_x86_64_gnullvm", 641 | "windows_x86_64_msvc", 642 | ] 643 | 644 | [[package]] 645 | name = "windows-sys" 646 | version = "0.45.0" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 649 | dependencies = [ 650 | "windows-targets", 651 | ] 652 | 653 | [[package]] 654 | name = "windows-targets" 655 | version = "0.42.2" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 658 | dependencies = [ 659 | "windows_aarch64_gnullvm", 660 | "windows_aarch64_msvc", 661 | "windows_i686_gnu", 662 | "windows_i686_msvc", 663 | "windows_x86_64_gnu", 664 | "windows_x86_64_gnullvm", 665 | "windows_x86_64_msvc", 666 | ] 667 | 668 | [[package]] 669 | name = "windows_aarch64_gnullvm" 670 | version = "0.42.2" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 673 | 674 | [[package]] 675 | name = "windows_aarch64_msvc" 676 | version = "0.42.2" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 679 | 680 | [[package]] 681 | name = "windows_i686_gnu" 682 | version = "0.42.2" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 685 | 686 | [[package]] 687 | name = "windows_i686_msvc" 688 | version = "0.42.2" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 691 | 692 | [[package]] 693 | name = "windows_x86_64_gnu" 694 | version = "0.42.2" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 697 | 698 | [[package]] 699 | name = "windows_x86_64_gnullvm" 700 | version = "0.42.2" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 703 | 704 | [[package]] 705 | name = "windows_x86_64_msvc" 706 | version = "0.42.2" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 709 | 710 | [[package]] 711 | name = "zip" 712 | version = "0.6.4" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "0445d0fbc924bb93539b4316c11afb121ea39296f99a3c4c9edad09e3658cdef" 715 | dependencies = [ 716 | "byteorder", 717 | "crc32fast", 718 | "crossbeam-utils", 719 | "flate2", 720 | ] 721 | 722 | [[package]] 723 | name = "zip-parser" 724 | version = "0.1.0" 725 | dependencies = [ 726 | "memchr", 727 | "thiserror", 728 | ] 729 | 730 | [[package]] 731 | name = "zstd" 732 | version = "0.12.3+zstd.1.5.2" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806" 735 | dependencies = [ 736 | "zstd-safe", 737 | ] 738 | 739 | [[package]] 740 | name = "zstd-safe" 741 | version = "6.0.4+zstd.1.5.4" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "7afb4b54b8910cf5447638cb54bf4e8a65cbedd783af98b98c62ffe91f185543" 744 | dependencies = [ 745 | "libc", 746 | "zstd-sys", 747 | ] 748 | 749 | [[package]] 750 | name = "zstd-sys" 751 | version = "2.0.7+zstd.1.5.4" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "94509c3ba2fe55294d752b79842c530ccfab760192521df74a081a78d2b3c7f5" 754 | dependencies = [ 755 | "cc", 756 | "libc", 757 | "pkg-config", 758 | ] 759 | --------------------------------------------------------------------------------