cab-0.6.0/.cargo_vcs_info.json0000644000000001360000000000100115640ustar { "git": { "sha1": "1675e33776a38c3706b5b4679a178a16382295d9" }, "path_in_vcs": "" }cab-0.6.0/.github/workflows/tests.yml000064400000000000000000000016451046102023000156440ustar 00000000000000name: tests on: push: paths-ignore: - 'LICENSE-*' - '**.md' pull_request: paths-ignore: - 'LICENSE-*' - '**.md' jobs: linters: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install rust toolchain uses: dtolnay/rust-toolchain@master with: toolchain: stable components: rustfmt, clippy - name: Cargo fmt run: cargo fmt --all -- --check - name: Cargo clippy run: cargo clippy --all-features -- -D warnings tests: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] rust: [ stable ] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 - name: Install rust toolchain uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust }} - name: Test run: cargo test --verbose cab-0.6.0/.gitignore000064400000000000000000000000601046102023000123400ustar 00000000000000.idea *.rs.bk *~ /Cargo.lock /scratch/ /target/ cab-0.6.0/Cargo.lock0000644000000325500000000000100075440ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "anstream" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "anstyle-parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" dependencies = [ "anstyle", "windows-sys", ] [[package]] name = "anyhow" version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "bitflags" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cab" version = "0.6.0" dependencies = [ "anyhow", "byteorder", "clap", "flate2", "lipsum", "lzxd", "rand", "time", "winapi", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" version = "4.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2275f18819641850fa26c89acc84d465c1bf91ce57bc2748b28c420473352f64" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07cdf1b148b25c1e1f7a42225e30a0d99a615cd4637eae7365548dd4529b95bc" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", "terminal_size", "unicase", "unicode-width", ] [[package]] name = "clap_derive" version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "colorchoice" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "crc32fast" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ "cfg-if", ] [[package]] name = "deranged" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" dependencies = [ "powerfmt", ] [[package]] name = "errno" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" dependencies = [ "libc", "windows-sys", ] [[package]] name = "flate2" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "getrandom" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "libc" version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "linux-raw-sys" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" [[package]] name = "lipsum" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "636860251af8963cc40f6b4baadee105f02e21b28131d76eba8e40ce84ab8064" dependencies = [ "rand", "rand_chacha", ] [[package]] name = "lzxd" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5de7336a183103429ad66d11d56d8bdc9c4a2916f6b85a8f11e5b127bde12001" [[package]] name = "miniz_oxide" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", ] [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "rustix" version = "0.38.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys", ] [[package]] name = "serde" version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "terminal_size" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ "rustix", "windows-sys", ] [[package]] name = "time" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" dependencies = [ "deranged", "powerfmt", "serde", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" dependencies = [ "time-core", ] [[package]] name = "unicase" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-width" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "utf8parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" cab-0.6.0/Cargo.toml0000644000000026560000000000100075730ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "cab" version = "0.6.0" authors = ["Matthew D. Steele "] description = "Read/write Windows cabinet (CAB) files" readme = "README.md" keywords = [ "archive", "cab", "cabinet", "windows", ] license = "MIT" repository = "https://github.com/mdsteele/rust-cab" [dependencies.byteorder] version = "1" [dependencies.flate2] version = "1" features = ["rust_backend"] default-features = false [dependencies.lzxd] version = "0.2.5" [dependencies.time] version = "0.3" [dev-dependencies.anyhow] version = "1.0" [dev-dependencies.clap] version = "4.4" features = [ "color", "suggestions", "derive", "wrap_help", "unicode", ] [dev-dependencies.lipsum] version = "0.9" [dev-dependencies.rand] version = "0.8" features = ["small_rng"] [dev-dependencies.time] version = "0.3" features = ["macros"] [dev-dependencies.winapi] version = "0.3" features = [ "basetsd", "minwindef", "winnt", ] cab-0.6.0/Cargo.toml.orig000064400000000000000000000014121046102023000132410ustar 00000000000000[package] name = "cab" version = "0.6.0" edition = '2021' authors = ["Matthew D. Steele "] description = "Read/write Windows cabinet (CAB) files" repository = "https://github.com/mdsteele/rust-cab" keywords = ["archive", "cab", "cabinet", "windows"] license = "MIT" readme = "README.md" [dependencies] byteorder = "1" flate2 = { version = "1", features = ["rust_backend"], default-features = false } lzxd = "0.2.5" time = "0.3" [dev-dependencies] anyhow = "1.0" lipsum = "0.9" clap = { version = "4.4", features = ["color", "suggestions", "derive", "wrap_help", "unicode"] } rand = { version = "0.8", features = ["small_rng"] } time = { version = "0.3", features = ["macros"] } winapi = { version = "0.3", features = ["basetsd", "minwindef", "winnt"] } cab-0.6.0/LICENSE000064400000000000000000000020621046102023000113610ustar 00000000000000MIT License Copyright (c) 2017 Matthew D. Steele 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. cab-0.6.0/README.md000064400000000000000000000011161046102023000116320ustar 00000000000000# rust-cab [![Build Status](https://github.com/mdsteele/rust-cab/actions/workflows/tests.yml/badge.svg)](https://github.com/mdsteele/rust-cab/actions/workflows/tests.yml) [![Crates.io](https://img.shields.io/crates/v/cab.svg)](https://crates.io/crates/cab) [![Documentation](https://docs.rs/cab/badge.svg)](https://docs.rs/cab) A pure Rust library for reading/writing [Windows cabinet](https://en.wikipedia.org/wiki/Cabinet_(file_format)) (CAB) files. Documentation: https://docs.rs/cab ## License rust-cab is made available under the [MIT License](http://spdx.org/licenses/MIT.html). cab-0.6.0/examples/cabtool.rs000064400000000000000000000122471046102023000141710ustar 00000000000000use std::fs::{self, File}; use std::io; use std::path::PathBuf; use std::time::UNIX_EPOCH; use clap::{Parser, Subcommand}; use time::{OffsetDateTime, PrimitiveDateTime}; use cab::{Cabinet, CabinetBuilder, CompressionType, FileEntry, FolderEntry}; #[derive(Parser, Debug)] #[command(author, about, version)] struct Cli { #[command(subcommand)] command: Command, } #[derive(Subcommand, Debug)] enum Command { /// Concatenates and prints streams Cat { path: PathBuf, files: Vec }, /// Creates a new cabinet Create { /// Sets compression type #[clap(short, long, default_value_t = String::from("mszip"))] compress: String, /// Sets output path #[clap(short, long)] output: Option, files: Vec, }, /// Lists files in the cabinet Ls { /// Lists in long format #[clap(short, long)] long: bool, path: PathBuf, }, } fn main() { let cli = Cli::parse(); match cli.command { Command::Cat { path, files } => { let mut cabinet = Cabinet::new(File::open(path).unwrap()).unwrap(); for filename in files { let mut file_reader = cabinet.read_file(&filename).unwrap(); io::copy(&mut file_reader, &mut io::stdout()).unwrap(); } } Command::Create { compress, output, files } => { let compress = match compress.as_str() { "none" => CompressionType::None, "mszip" => CompressionType::MsZip, _ => panic!("Invalid compression type: {}", compress), }; let output = output.unwrap_or_else(|| { let mut path = PathBuf::from("out.cab"); let mut index: i32 = 0; while path.exists() { index += 1; path = PathBuf::from(format!("out{}.cab", index)); } path }); let mut builder = CabinetBuilder::new(); let mut file_index: usize = 0; while file_index < files.len() { let folder = builder.add_folder(compress); let mut folder_size: u64 = 0; while file_index < files.len() && folder_size < 0x8000 { let filename = files[file_index].as_str(); let metadata = fs::metadata(filename).unwrap(); folder_size += metadata.len(); let file = folder.add_file(filename); if let Ok(time) = metadata.modified() { if let Ok(dur) = time.duration_since(UNIX_EPOCH) { let dt = OffsetDateTime::from_unix_timestamp( dur.as_secs() as i64, ) .unwrap(); file.set_datetime(PrimitiveDateTime::new( dt.date(), dt.time(), )); } } file_index += 1; } } let file = File::create(&output).unwrap(); let mut cabinet = builder.build(file).unwrap(); while let Some(mut writer) = cabinet.next_file().unwrap() { let mut file = File::open(writer.file_name()).unwrap(); io::copy(&mut file, &mut writer).unwrap(); } cabinet.finish().unwrap(); } Command::Ls { path, long } => { let cabinet = Cabinet::new(File::open(path).unwrap()).unwrap(); for (index, folder) in cabinet.folder_entries().enumerate() { for file in folder.file_entries() { list_file(index, &folder, file, long); } } } } } fn list_file( folder_index: usize, folder: &FolderEntry, file: &FileEntry, long: bool, ) { if !long { println!("{}", file.name()); return; } let ctype = match folder.compression_type() { CompressionType::None => "None".to_string(), CompressionType::MsZip => "MsZip".to_string(), CompressionType::Quantum(v, m) => format!("Q{}/{}", v, m), CompressionType::Lzx(w) => format!("Lzx{:?}", w), }; let file_size = if file.uncompressed_size() >= 100_000_000 { format!("{} MB", file.uncompressed_size() / (1 << 20)) } else if file.uncompressed_size() >= 1_000_000 { format!("{} kB", file.uncompressed_size() / (1 << 10)) } else { format!("{} B ", file.uncompressed_size()) }; println!( "{}{}{}{}{}{} {:>2} {:<5} {:>10} {} {}", if file.is_read_only() { 'R' } else { '-' }, if file.is_hidden() { 'H' } else { '-' }, if file.is_system() { 'S' } else { '-' }, if file.is_archive() { 'A' } else { '-' }, if file.is_exec() { 'E' } else { '-' }, if file.is_name_utf() { 'U' } else { '-' }, folder_index, ctype, file_size, file.datetime() .map(|dt| dt.to_string()) .unwrap_or("invalid datetime".to_string()), file.name() ); } cab-0.6.0/examples/readcab.rs000064400000000000000000000017361046102023000141300ustar 00000000000000use std::fs::File; use std::path::PathBuf; use anyhow::Context; use clap::Parser; #[derive(Parser, Debug)] #[command(author, about)] struct Cli { path: PathBuf, } fn main() -> anyhow::Result<()> { let cli = Cli::parse(); let input_file = File::open(cli.path)?; let cabinet = cab::Cabinet::new(input_file) .context("Failed to open cabinet file")?; for (index, folder) in cabinet.folder_entries().enumerate() { println!("Folder #{}:", index); println!(" compression_type = {:?}", folder.compression_type()); println!(" reserve_data = {:?}", folder.reserve_data()); println!(" num_data_blocks = {}", folder.num_data_blocks()); let mut total_size = 0; for file in folder.file_entries() { let size = file.uncompressed_size(); println!(" {:?} ({} bytes)", file.name(), size); total_size += size; } println!(" {} bytes total", total_size); } Ok(()) } cab-0.6.0/rustfmt.toml000064400000000000000000000001431046102023000127530ustar 00000000000000max_width = 79 newline_style = "Unix" use_field_init_shorthand = true use_small_heuristics = "Max" cab-0.6.0/src/builder.rs000064400000000000000000000625721046102023000131530ustar 00000000000000use crate::checksum::Checksum; use crate::consts; use crate::ctype::CompressionType; use crate::datetime::datetime_to_bits; use crate::mszip::MsZipCompressor; use byteorder::{LittleEndian, WriteBytesExt}; use std::io::{self, Seek, SeekFrom, Write}; use std::mem; use time::PrimitiveDateTime; const MAX_UNCOMPRESSED_BLOCK_SIZE: usize = 0x8000; /// A structure for building a file within a new cabinet. pub struct FileBuilder { name: String, attributes: u16, datetime: PrimitiveDateTime, entry_offset: u64, uncompressed_size: u32, offset_within_folder: u32, } impl FileBuilder { fn new(name: String) -> FileBuilder { let name_is_utf = name.bytes().any(|byte| byte > 0x7f); let now = time::OffsetDateTime::now_utc(); let mut builder = FileBuilder { name, attributes: consts::ATTR_ARCH, datetime: time::PrimitiveDateTime::new(now.date(), now.time()), entry_offset: 0, // filled in later by CabinetWriter uncompressed_size: 0, // filled in later by FileWriter offset_within_folder: 0, // filled in later by CabinetWriter }; builder.set_attribute(consts::ATTR_NAME_IS_UTF, name_is_utf); builder } /// Sets the datetime for this file. According to the CAB spec, this "is /// typically considered the 'last modified' time in local time, but the /// actual definition is application-defined". /// /// The CAB file format only supports storing datetimes with years from /// 1980 to 2107 (inclusive), with a resolution of two seconds. If the /// given datetime is outside this range/resolution, it will be /// clamped/rounded to the nearest legal value. /// /// By default, the datetime of a new `FileBuilder` is the current UTC /// date/time. pub fn set_datetime(&mut self, datetime: PrimitiveDateTime) { self.datetime = datetime; } /// Sets whether this file has the "read-only" attribute set. This /// attribute is false by default. pub fn set_is_read_only(&mut self, is_read_only: bool) { self.set_attribute(consts::ATTR_READ_ONLY, is_read_only); } /// Sets whether this file has the "hidden" attribute set. This attribute /// is false by default. pub fn set_is_hidden(&mut self, is_hidden: bool) { self.set_attribute(consts::ATTR_HIDDEN, is_hidden); } /// Sets whether this file has the "system file" attribute set. This /// attribute is false by default. pub fn set_is_system(&mut self, is_system_file: bool) { self.set_attribute(consts::ATTR_SYSTEM, is_system_file); } /// Sets whether this file has the "archive" (modified since last backup) /// attribute set. This attribute is true by default. pub fn set_is_archive(&mut self, is_archive: bool) { self.set_attribute(consts::ATTR_ARCH, is_archive); } /// Returns true if this file has the "execute after extraction" attribute /// set. This attribute is false by default. pub fn set_is_exec(&mut self, is_exec: bool) { self.set_attribute(consts::ATTR_EXEC, is_exec); } fn set_attribute(&mut self, bit: u16, enable: bool) { if enable { self.attributes |= bit; } else { self.attributes &= !bit; } } } /// A structure for building a folder within a new cabinet. pub struct FolderBuilder { compression_type: CompressionType, files: Vec, reserve_data: Vec, entry_offset: u32, } impl FolderBuilder { fn new(ctype: CompressionType) -> FolderBuilder { FolderBuilder { compression_type: ctype, files: Vec::new(), reserve_data: Vec::new(), entry_offset: 0, // filled in later by CabinetWriter } } /// Adds a new file to the folder. You can optionally use the returned /// `FileBuilder` to change settings on the file. pub fn add_file>(&mut self, name: S) -> &mut FileBuilder { self.files.push(FileBuilder::new(name.into())); self.files.last_mut().unwrap() } /// Sets the folder's reserve data. The meaning of this data is /// application-defined. The data must be no more than 255 bytes long. pub fn set_reserve_data(&mut self, data: Vec) { self.reserve_data = data; } } /// A structure for building a new cabinet. pub struct CabinetBuilder { folders: Vec, reserve_data: Vec, } impl CabinetBuilder { /// Creates a new, empty `CabinetBuilder`. pub fn new() -> CabinetBuilder { CabinetBuilder { folders: Vec::new(), reserve_data: Vec::new() } } /// Adds a new folder to the cabinet. Use the returned `FolderBuilder` to /// add files to the folder or to change other settings on the folder. pub fn add_folder( &mut self, ctype: CompressionType, ) -> &mut FolderBuilder { self.folders.push(FolderBuilder::new(ctype)); self.folders.last_mut().unwrap() } /// Sets the cabinet file's header reserve data. The meaning of this data /// is application-defined. The data must be no more than 60,000 bytes /// long. pub fn set_reserve_data(&mut self, data: Vec) { self.reserve_data = data; } /// Locks in the cabinet settings and returns a `CabinetWriter` object that /// will write the cabinet file into the given writer. pub fn build( self, writer: W, ) -> io::Result> { CabinetWriter::start(writer, self) } } impl Default for CabinetBuilder { fn default() -> Self { CabinetBuilder::new() } } /// A structure for writing file data into a new cabinet file. pub struct CabinetWriter { writer: InnerCabinetWriter, builder: CabinetBuilder, current_folder_index: usize, next_file_index: usize, offset_within_folder: u64, } enum InnerCabinetWriter { Raw(W), Folder(FolderWriter), None, } impl InnerCabinetWriter { fn is_none(&self) -> bool { matches!(*self, InnerCabinetWriter::None) } fn take(&mut self) -> InnerCabinetWriter { mem::replace(self, InnerCabinetWriter::None) } } impl CabinetWriter { fn start( mut writer: W, mut builder: CabinetBuilder, ) -> io::Result> { let num_folders = builder.folders.len(); if num_folders > consts::MAX_NUM_FOLDERS { invalid_input!( "Cabinet has too many folders ({}; max is {})", num_folders, consts::MAX_NUM_FOLDERS ); } let num_files: usize = builder.folders.iter().map(|folder| folder.files.len()).sum(); if num_files > consts::MAX_NUM_FILES { invalid_input!( "Cabinet has too many files ({}; max is {})", num_files, consts::MAX_NUM_FILES ); } let header_reserve_size = builder.reserve_data.len(); if header_reserve_size > consts::MAX_HEADER_RESERVE_SIZE { invalid_input!( "Cabinet header reserve data is too large \ ({} bytes; max is {} bytes)", header_reserve_size, consts::MAX_HEADER_RESERVE_SIZE ); } let folder_reserve_size = builder .folders .iter() .map(|folder| folder.reserve_data.len()) .max() .unwrap_or(0); if folder_reserve_size > consts::MAX_FOLDER_RESERVE_SIZE { invalid_input!( "Cabinet folder reserve data is too large \ ({} bytes; max is {} bytes)", folder_reserve_size, consts::MAX_FOLDER_RESERVE_SIZE ); } let mut flags: u16 = 0; if header_reserve_size > 0 || folder_reserve_size > 0 { flags |= consts::FLAG_RESERVE_PRESENT; } let mut first_folder_offset = 36; if (flags & consts::FLAG_RESERVE_PRESENT) != 0 { first_folder_offset += 4 + header_reserve_size as u32; } let folder_entry_size = 8 + folder_reserve_size as u32; let first_file_offset = first_folder_offset + (num_folders as u32) * folder_entry_size; // Write cabinet header: writer.write_u32::(consts::FILE_SIGNATURE)?; writer.write_u32::(0)?; // reserved1 writer.write_u32::(0)?; // total size, filled later writer.write_u32::(0)?; // reserved2 writer.write_u32::(first_file_offset)?; writer.write_u32::(0)?; // reserved3 writer.write_u8(consts::VERSION_MINOR)?; writer.write_u8(consts::VERSION_MAJOR)?; writer.write_u16::(num_folders as u16)?; writer.write_u16::(num_files as u16)?; writer.write_u16::(flags)?; writer.write_u16::(0)?; // cabinet set ID writer.write_u16::(0)?; // cabinet set index if (flags & consts::FLAG_RESERVE_PRESENT) != 0 { writer.write_u16::(header_reserve_size as u16)?; writer.write_u8(folder_reserve_size as u8)?; writer.write_u8(0)?; // data reserve size writer.write_all(&builder.reserve_data)?; } if (flags & consts::FLAG_PREV_CABINET) != 0 { invalid_input!("Prev-cabinet feature not yet supported"); } if (flags & consts::FLAG_NEXT_CABINET) != 0 { invalid_input!("Next-cabinet feature not yet supported"); } // Write structs for folders: for (index, folder) in builder.folders.iter_mut().enumerate() { folder.entry_offset = first_folder_offset + (index as u32) * folder_entry_size; writer.write_u32::(0)?; // first data, filled later writer.write_u16::(0)?; // num data, filled later let ctype_bits = folder.compression_type.to_bitfield(); writer.write_u16::(ctype_bits)?; debug_assert!(folder.reserve_data.len() <= folder_reserve_size); if folder_reserve_size > 0 { writer.write_all(&folder.reserve_data)?; let padding = folder_reserve_size - folder.reserve_data.len(); if padding > 0 { writer.write_all(&vec![0; padding])?; } } } // Write structs for files: let mut current_offset = first_file_offset as u64; for (folder_index, folder) in builder.folders.iter_mut().enumerate() { for file in folder.files.iter_mut() { file.entry_offset = current_offset; writer.write_u32::(0)?; // size, filled later writer.write_u32::(0)?; // offset, filled later writer.write_u16::(folder_index as u16)?; let (date, time) = datetime_to_bits(file.datetime); writer.write_u16::(date)?; writer.write_u16::(time)?; writer.write_u16::(file.attributes)?; writer.write_all(file.name.as_bytes())?; writer.write_u8(0)?; current_offset += 17 + file.name.len() as u64; } } Ok(CabinetWriter { writer: InnerCabinetWriter::Raw(writer), builder, current_folder_index: 0, next_file_index: 0, offset_within_folder: 0, }) } /// Returns a `FileWriter` for the next file within that cabinet that needs /// data to be written, or `None` if all files are now complete. pub fn next_file(&mut self) -> io::Result>> { let num_folders = self.builder.folders.len(); while self.current_folder_index < num_folders { if self.next_file_index > 0 { // End previous file: let folder = &self.builder.folders[self.current_folder_index]; let file = &folder.files[self.next_file_index - 1]; self.offset_within_folder += file.uncompressed_size as u64; } let num_files = self.builder.folders[self.current_folder_index].files.len(); if self.next_file_index < num_files { let folder = &mut self.builder.folders[self.current_folder_index]; if self.next_file_index == 0 { // Begin folder: match self.writer.take() { InnerCabinetWriter::Raw(writer) => { let folder_writer = FolderWriter::new( writer, folder.compression_type, folder.entry_offset, )?; self.writer = InnerCabinetWriter::Folder(folder_writer); } _ => unreachable!(), } } // Begin next file: let file = &mut folder.files[self.next_file_index]; if self.offset_within_folder > (u32::MAX as u64) { invalid_data!( "Folder is overfull \ (file offset of {} bytes, max is {} bytes)", self.offset_within_folder, u32::MAX ); } file.offset_within_folder = self.offset_within_folder as u32; let file_writer = match self.writer { InnerCabinetWriter::Folder(ref mut folder_writer) => { FileWriter::new(folder_writer, file) } _ => unreachable!(), }; self.next_file_index += 1; return Ok(Some(file_writer)); } // End folder: match self.writer.take() { InnerCabinetWriter::Folder(folder_writer) => { let folder = &self.builder.folders[self.current_folder_index]; let writer = folder_writer.finish(&folder.files)?; self.writer = InnerCabinetWriter::Raw(writer); } _ => unreachable!(), } self.current_folder_index += 1; self.next_file_index = 0; self.offset_within_folder = 0; } Ok(None) } /// Finishes writing the cabinet file, and returns the underlying writer. pub fn finish(mut self) -> io::Result { self.shutdown()?; match self.writer.take() { InnerCabinetWriter::Raw(writer) => Ok(writer), _ => unreachable!(), } } fn shutdown(&mut self) -> io::Result<()> { while (self.next_file()?).is_some() {} match self.writer { InnerCabinetWriter::Raw(ref mut writer) => { let cabinet_file_size = writer.stream_position()?; if cabinet_file_size > (consts::MAX_TOTAL_CAB_SIZE as u64) { invalid_data!( "Cabinet file is too large \ ({} bytes; max is {} bytes)", cabinet_file_size, consts::MAX_TOTAL_CAB_SIZE ); } writer.seek(SeekFrom::Start(8))?; writer.write_u32::(cabinet_file_size as u32)?; writer.seek(SeekFrom::End(0))?; writer.flush()?; } _ => unreachable!(), }; Ok(()) } } impl Drop for CabinetWriter { fn drop(&mut self) { if !self.writer.is_none() { let _ = self.shutdown(); } } } /// Allows writing data for a single file within a new cabinet. pub struct FileWriter<'a, W: 'a + Write + Seek> { folder_writer: &'a mut FolderWriter, file_builder: &'a mut FileBuilder, } impl<'a, W: Write + Seek> FileWriter<'a, W> { fn new( folder_writer: &'a mut FolderWriter, file_builder: &'a mut FileBuilder, ) -> FileWriter<'a, W> { FileWriter { folder_writer, file_builder } } /// Returns the name of the file being written. pub fn file_name(&self) -> &str { &self.file_builder.name } } impl<'a, W: Write + Seek> Write for FileWriter<'a, W> { fn write(&mut self, buf: &[u8]) -> io::Result { if buf.is_empty() { return Ok(0); } if self.file_builder.uncompressed_size == consts::MAX_FILE_SIZE { invalid_input!( "File is already at maximum size of {} bytes", consts::MAX_FILE_SIZE ); } let remaining = consts::MAX_FILE_SIZE - self.file_builder.uncompressed_size; let max_bytes = (buf.len() as u64).min(remaining as u64) as usize; let bytes_written = self.folder_writer.write(&buf[0..max_bytes])?; self.file_builder.uncompressed_size += bytes_written as u32; Ok(bytes_written) } fn flush(&mut self) -> io::Result<()> { self.folder_writer.flush() } } /// A writer for writer data into a cabinet folder. struct FolderWriter { writer: W, compressor: FolderCompressor, folder_entry_offset: u32, first_data_block_offset: u32, next_data_block_offset: u64, num_data_blocks: u16, data_block_buffer: Vec, } enum FolderCompressor { Uncompressed, MsZip(MsZipCompressor), // TODO: add options for other compression types } impl FolderWriter { fn new( mut writer: W, compression_type: CompressionType, folder_entry_offset: u32, ) -> io::Result> { let current_offset = writer.stream_position()?; if current_offset > (consts::MAX_TOTAL_CAB_SIZE as u64) { invalid_data!( "Cabinet file is too large \ (already {} bytes; max is {} bytes)", current_offset, consts::MAX_TOTAL_CAB_SIZE ); } let compressor = match compression_type { CompressionType::None => FolderCompressor::Uncompressed, CompressionType::MsZip => { FolderCompressor::MsZip(MsZipCompressor::new()) } CompressionType::Quantum(_, _) => { invalid_data!("Quantum compression is not yet supported."); } CompressionType::Lzx(_) => { invalid_data!("LZX compression is not yet supported."); } }; Ok(FolderWriter { writer, compressor, folder_entry_offset, first_data_block_offset: current_offset as u32, next_data_block_offset: current_offset, num_data_blocks: 0, data_block_buffer: Vec::with_capacity(MAX_UNCOMPRESSED_BLOCK_SIZE), }) } fn finish(mut self, files: &[FileBuilder]) -> io::Result { if !self.data_block_buffer.is_empty() { self.write_data_block(true)?; } let mut writer = self.writer; let offset = writer.stream_position()?; writer.seek(SeekFrom::Start(self.folder_entry_offset as u64))?; writer.write_u32::(self.first_data_block_offset)?; writer.write_u16::(self.num_data_blocks)?; for file in files.iter() { writer.seek(SeekFrom::Start(file.entry_offset))?; writer.write_u32::(file.uncompressed_size)?; writer.write_u32::(file.offset_within_folder)?; } writer.seek(SeekFrom::Start(offset))?; Ok(writer) } fn write_data_block(&mut self, is_last_block: bool) -> io::Result<()> { debug_assert!(!self.data_block_buffer.is_empty()); let uncompressed_size = self.data_block_buffer.len() as u16; let compressed = match self.compressor { FolderCompressor::Uncompressed => { let empty = Vec::with_capacity(MAX_UNCOMPRESSED_BLOCK_SIZE); mem::replace(&mut self.data_block_buffer, empty) } FolderCompressor::MsZip(ref mut compressor) => { let compressed = compressor .compress_block(&self.data_block_buffer, is_last_block)?; self.data_block_buffer.clear(); compressed } }; let compressed_size = compressed.len() as u16; let mut checksum = Checksum::new(); checksum.update(&compressed); let checksum_value = checksum.value() ^ ((compressed_size as u32) | ((uncompressed_size as u32) << 16)); let total_data_block_size = 8 + compressed_size as u64; self.writer.seek(SeekFrom::Start(self.next_data_block_offset))?; self.writer.write_u32::(checksum_value)?; self.writer.write_u16::(compressed_size)?; self.writer.write_u16::(uncompressed_size)?; self.writer.write_all(&compressed)?; self.next_data_block_offset += total_data_block_size; self.num_data_blocks += 1; Ok(()) } } impl Write for FolderWriter { fn write(&mut self, buf: &[u8]) -> io::Result { let capacity = self.data_block_buffer.capacity(); debug_assert_eq!(capacity, MAX_UNCOMPRESSED_BLOCK_SIZE); if buf.is_empty() { return Ok(0); } if self.data_block_buffer.len() == capacity { self.write_data_block(false)?; } let max_bytes = buf.len().min(capacity - self.data_block_buffer.len()); debug_assert!(max_bytes > 0); self.data_block_buffer.extend_from_slice(&buf[..max_bytes]); debug_assert_eq!(self.data_block_buffer.capacity(), capacity); Ok(max_bytes) } fn flush(&mut self) -> io::Result<()> { self.writer.flush() } } #[cfg(test)] mod tests { use super::CabinetBuilder; use crate::ctype::CompressionType; use std::io::{Cursor, Write}; use time::macros::datetime; #[test] fn write_uncompressed_cabinet_with_one_file() { let mut builder = CabinetBuilder::new(); let dt = datetime!(1997-03-12 11:13:52); builder .add_folder(CompressionType::None) .add_file("hi.txt") .set_datetime(dt); let mut cab_writer = builder.build(Cursor::new(Vec::new())).unwrap(); while let Some(mut file_writer) = cab_writer.next_file().unwrap() { file_writer.write_all(b"Hello, world!\n").unwrap(); } let output = cab_writer.finish().unwrap().into_inner(); let expected: &[u8] = b"MSCF\0\0\0\0\x59\0\0\0\0\0\0\0\ \x2c\0\0\0\0\0\0\0\x03\x01\x01\0\x01\0\0\0\0\0\0\0\ \x43\0\0\0\x01\0\0\0\ \x0e\0\0\0\0\0\0\0\0\0\x6c\x22\xba\x59\x20\0hi.txt\0\ \x4c\x1a\x2e\x7f\x0e\0\x0e\0Hello, world!\n"; assert_eq!(output.as_slice(), expected); } #[test] fn write_uncompressed_cabinet_with_two_files() { let mut builder = CabinetBuilder::new(); let dt = datetime!(2018-01-06 15:19:42); { let folder_builder = builder.add_folder(CompressionType::None); folder_builder.add_file("hi.txt").set_datetime(dt); folder_builder.add_file("bye.txt").set_datetime(dt); } let mut cab_writer = builder.build(Cursor::new(Vec::new())).unwrap(); while let Some(mut file_writer) = cab_writer.next_file().unwrap() { let data = if file_writer.file_name() == "hi.txt" { "Hello, world!\n".as_bytes() } else { "See you later!\n".as_bytes() }; file_writer.write_all(data).unwrap(); } let output = cab_writer.finish().unwrap().into_inner(); let expected: &[u8] = b"MSCF\0\0\0\0\x80\0\0\0\0\0\0\0\ \x2c\0\0\0\0\0\0\0\x03\x01\x01\0\x02\0\0\0\0\0\0\0\ \x5b\0\0\0\x01\0\0\0\ \x0e\0\0\0\0\0\0\0\0\0\x26\x4c\x75\x7a\x20\0hi.txt\0\ \x0f\0\0\0\x0e\0\0\0\0\0\x26\x4c\x75\x7a\x20\0bye.txt\0\ \x1a\x54\x09\x35\x1d\0\x1d\0Hello, world!\nSee you later!\n"; assert_eq!(output.as_slice(), expected); } #[test] fn write_uncompressed_cabinet_with_non_ascii_filename() { let mut builder = CabinetBuilder::new(); let dt = datetime!(1997-03-12 11:13:52); builder .add_folder(CompressionType::None) .add_file("\u{2603}.txt") .set_datetime(dt); let mut cab_writer = builder.build(Cursor::new(Vec::new())).unwrap(); while let Some(mut file_writer) = cab_writer.next_file().unwrap() { file_writer.write_all(b"Snowman!\n").unwrap(); } let output = cab_writer.finish().unwrap().into_inner(); let expected: &[u8] = b"MSCF\0\0\0\0\x55\0\0\0\0\0\0\0\ \x2c\0\0\0\0\0\0\0\x03\x01\x01\0\x01\0\0\0\0\0\0\0\ \x44\0\0\0\x01\0\0\0\ \x09\0\0\0\0\0\0\0\0\0\x6c\x22\xba\x59\xa0\0\xe2\x98\x83.txt\0\ \x3d\x0f\x08\x56\x09\0\x09\0Snowman!\n"; assert_eq!(output.as_slice(), expected); } } cab-0.6.0/src/cabinet.rs000064400000000000000000000365271046102023000131330ustar 00000000000000use std::cell::RefCell; use std::io::{self, Read, Seek, SeekFrom}; use byteorder::{LittleEndian, ReadBytesExt}; use crate::consts; use crate::file::{parse_file_entry, FileEntry, FileReader}; use crate::folder::{ parse_folder_entry, FolderEntries, FolderEntry, FolderReader, }; use crate::string::read_null_terminated_string; pub(crate) trait ReadSeek: Read + Seek {} impl ReadSeek for R {} /// A structure for reading a cabinet file. pub struct Cabinet { pub(crate) inner: CabinetInner, } pub(crate) struct CabinetInner { cabinet_set_id: u16, cabinet_set_index: u16, data_reserve_size: u8, reserve_data: Vec, folders: Vec, files: Vec, reader: RefCell, } impl Cabinet { /// Open an existing cabinet file. pub fn new(mut reader: R) -> io::Result> { let signature = reader.read_u32::()?; if signature != consts::FILE_SIGNATURE { invalid_data!("Not a cabinet file (invalid file signature)"); } let _reserved1 = reader.read_u32::()?; let total_size = reader.read_u32::()?; if total_size > consts::MAX_TOTAL_CAB_SIZE { invalid_data!( "Cabinet total size field is too large \ ({} bytes; max is {} bytes)", total_size, consts::MAX_TOTAL_CAB_SIZE ); } let _reserved2 = reader.read_u32::()?; let first_file_offset = reader.read_u32::()?; let _reserved3 = reader.read_u32::()?; let minor_version = reader.read_u8()?; let major_version = reader.read_u8()?; if major_version > consts::VERSION_MAJOR || major_version == consts::VERSION_MAJOR && minor_version > consts::VERSION_MINOR { invalid_data!( "Version {}.{} cabinet files are not supported", major_version, minor_version ); } let num_folders = reader.read_u16::()? as usize; let num_files = reader.read_u16::()?; let flags = reader.read_u16::()?; let cabinet_set_id = reader.read_u16::()?; let cabinet_set_index = reader.read_u16::()?; let mut header_reserve_size = 0u16; let mut folder_reserve_size = 0u8; let mut data_reserve_size = 0u8; if (flags & consts::FLAG_RESERVE_PRESENT) != 0 { header_reserve_size = reader.read_u16::()?; folder_reserve_size = reader.read_u8()?; data_reserve_size = reader.read_u8()?; } let mut header_reserve_data = vec![0u8; header_reserve_size as usize]; if header_reserve_size > 0 { reader.read_exact(&mut header_reserve_data)?; } let _prev_cabinet = if (flags & consts::FLAG_PREV_CABINET) != 0 { let cab_name = read_null_terminated_string(&mut reader, false)?; let disk_name = read_null_terminated_string(&mut reader, false)?; Some((cab_name, disk_name)) } else { None }; let _next_cabinet = if (flags & consts::FLAG_NEXT_CABINET) != 0 { let cab_name = read_null_terminated_string(&mut reader, false)?; let disk_name = read_null_terminated_string(&mut reader, false)?; Some((cab_name, disk_name)) } else { None }; let mut folders = Vec::with_capacity(num_folders); for _ in 0..num_folders { let entry = parse_folder_entry(&mut reader, folder_reserve_size as usize)?; folders.push(entry); } reader.seek(SeekFrom::Start(first_file_offset as u64))?; let mut files = Vec::with_capacity(num_files as usize); for _ in 0..num_files { let entry = parse_file_entry(&mut reader)?; let folder_index = entry.folder_index as usize; if folder_index >= folders.len() { invalid_data!("File entry folder index out of bounds"); } let folder = &mut folders[folder_index]; folder.files.push(entry.clone()); files.push(entry); } Ok(Cabinet { inner: CabinetInner { cabinet_set_id, cabinet_set_index, data_reserve_size, reserve_data: header_reserve_data, folders, files, reader: RefCell::new(reader), }, }) } /// Returns the cabinet set ID for this cabinet (an arbitrary number used /// to group together a set of cabinets). pub fn cabinet_set_id(&self) -> u16 { self.inner.cabinet_set_id } /// Returns this cabinet's (zero-based) index within its cabinet set. pub fn cabinet_set_index(&self) -> u16 { self.inner.cabinet_set_index } /// Returns the application-defined reserve data stored in the cabinet /// header. pub fn reserve_data(&self) -> &[u8] { &self.inner.reserve_data } /// Returns an iterator over the folder entries in this cabinet. pub fn folder_entries(&self) -> FolderEntries { FolderEntries { iter: self.inner.folders.iter() } } /// Returns the entry for the file with the given name, if any.. pub fn get_file_entry(&self, name: &str) -> Option<&FileEntry> { self.inner.files.iter().find(|&file| file.name() == name) } /// Returns a reader over the decompressed data for the file in the cabinet /// with the given name. pub fn read_file(&mut self, name: &str) -> io::Result> { match self.get_file_entry(name) { Some(file_entry) => { let folder_index = file_entry.folder_index as usize; let file_start_in_folder = file_entry.uncompressed_offset as u64; let size = file_entry.uncompressed_size() as u64; let mut folder_reader = self.read_folder(folder_index)?; folder_reader .seek_to_uncompressed_offset(file_start_in_folder)?; Ok(FileReader { reader: folder_reader, file_start_in_folder, offset: 0, size, }) } None => not_found!("No such file in cabinet: {:?}", name), } } /// Returns a reader over the decompressed data in the specified folder. fn read_folder(&mut self, index: usize) -> io::Result> { if index >= self.inner.folders.len() { invalid_input!( "Folder index {} is out of range (cabinet has {} folders)", index, self.inner.folders.len() ); } let me: &Cabinet = self; FolderReader::new( me, &self.inner.folders[index], self.inner.data_reserve_size, ) } } impl<'a, R: ?Sized + Read> Read for &'a CabinetInner { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.reader.borrow_mut().read(buf) } } impl<'a, R: ?Sized + Seek> Seek for &'a CabinetInner { fn seek(&mut self, pos: SeekFrom) -> io::Result { self.reader.borrow_mut().seek(pos) } } #[cfg(test)] mod tests { use std::io::{Cursor, Read}; use super::Cabinet; #[test] fn read_uncompressed_cabinet_with_one_file() { let binary: &[u8] = b"MSCF\0\0\0\0\x59\0\0\0\0\0\0\0\ \x2c\0\0\0\0\0\0\0\x03\x01\x01\0\x01\0\0\0\x34\x12\0\0\ \x43\0\0\0\x01\0\0\0\ \x0e\0\0\0\0\0\0\0\0\0\x6c\x22\xba\x59\x01\0hi.txt\0\ \x4c\x1a\x2e\x7f\x0e\0\x0e\0Hello, world!\n"; assert_eq!(binary.len(), 0x59); let mut cabinet = Cabinet::new(Cursor::new(binary)).unwrap(); assert_eq!(cabinet.cabinet_set_id(), 0x1234); assert_eq!(cabinet.cabinet_set_index(), 0); assert_eq!(cabinet.reserve_data(), &[]); assert_eq!(cabinet.folder_entries().len(), 1); { let file = cabinet.get_file_entry("hi.txt").unwrap(); assert_eq!(file.name(), "hi.txt"); assert!(!file.is_name_utf()); let dt = file.datetime().unwrap(); assert_eq!(dt.year(), 1997); assert_eq!(dt.month(), time::Month::March); assert_eq!(dt.day(), 12); assert_eq!(dt.hour(), 11); assert_eq!(dt.minute(), 13); assert_eq!(dt.second(), 52); } let mut data = Vec::new(); cabinet.read_folder(0).unwrap().read_to_end(&mut data).unwrap(); assert_eq!(data, b"Hello, world!\n"); let mut data = Vec::new(); cabinet.read_file("hi.txt").unwrap().read_to_end(&mut data).unwrap(); assert_eq!(data, b"Hello, world!\n"); } #[test] fn read_uncompressed_cabinet_with_two_files() { let binary: &[u8] = b"MSCF\0\0\0\0\x80\0\0\0\0\0\0\0\ \x2c\0\0\0\0\0\0\0\x03\x01\x01\0\x02\0\0\0\x34\x12\0\0\ \x5b\0\0\0\x01\0\0\0\ \x0e\0\0\0\0\0\0\0\0\0\x6c\x22\xe7\x59\x01\0hi.txt\0\ \x0f\0\0\0\x0e\0\0\0\0\0\x6c\x22\xe7\x59\x01\0bye.txt\0\ \0\0\0\0\x1d\0\x1d\0Hello, world!\nSee you later!\n"; assert_eq!(binary.len(), 0x80); let mut cabinet = Cabinet::new(Cursor::new(binary)).unwrap(); let mut data = Vec::new(); cabinet.read_folder(0).unwrap().read_to_end(&mut data).unwrap(); assert_eq!(data, b"Hello, world!\nSee you later!\n"); let mut data = Vec::new(); cabinet.read_file("hi.txt").unwrap().read_to_end(&mut data).unwrap(); assert_eq!(data, b"Hello, world!\n"); let mut data = Vec::new(); cabinet.read_file("bye.txt").unwrap().read_to_end(&mut data).unwrap(); assert_eq!(data, b"See you later!\n"); } #[test] fn read_uncompressed_cabinet_with_two_data_blocks() { let binary: &[u8] = b"MSCF\0\0\0\0\x61\0\0\0\0\0\0\0\ \x2c\0\0\0\0\0\0\0\x03\x01\x01\0\x01\0\0\0\x34\x12\0\0\ \x43\0\0\0\x02\0\0\0\ \x0e\0\0\0\0\0\0\0\0\0\x6c\x22\xba\x59\x01\0hi.txt\0\ \0\0\0\0\x06\0\x06\0Hello,\ \0\0\0\0\x08\0\x08\0 world!\n"; assert_eq!(binary.len(), 0x61); let mut cabinet = Cabinet::new(Cursor::new(binary)).unwrap(); assert_eq!(cabinet.folder_entries().len(), 1); assert_eq!( cabinet.folder_entries().nth(0).unwrap().num_data_blocks(), 2 ); let mut data = Vec::new(); cabinet.read_folder(0).unwrap().read_to_end(&mut data).unwrap(); assert_eq!(data, b"Hello, world!\n"); let mut data = Vec::new(); cabinet.read_file("hi.txt").unwrap().read_to_end(&mut data).unwrap(); assert_eq!(data, b"Hello, world!\n"); } #[test] fn read_mszip_cabinet_with_one_file() { let binary: &[u8] = b"MSCF\0\0\0\0\x61\0\0\0\0\0\0\0\ \x2c\0\0\0\0\0\0\0\x03\x01\x01\0\x01\0\0\0\x34\x12\0\0\ \x43\0\0\0\x01\0\x01\0\ \x0e\0\0\0\0\0\0\0\0\0\x6c\x22\xe7\x59\x01\0hi.txt\0\ \0\0\0\0\x16\0\x0e\0\ CK\xf3H\xcd\xc9\xc9\xd7Q(\xcf/\xcaIQ\xe4\x02\x00$\xf2\x04\x94"; assert_eq!(binary.len(), 0x61); let mut cabinet = Cabinet::new(Cursor::new(binary)).unwrap(); assert_eq!(cabinet.cabinet_set_id(), 0x1234); assert_eq!(cabinet.cabinet_set_index(), 0); assert_eq!(cabinet.reserve_data(), &[]); assert_eq!(cabinet.folder_entries().len(), 1); let mut data = Vec::new(); cabinet.read_folder(0).unwrap().read_to_end(&mut data).unwrap(); assert_eq!(data, b"Hello, world!\n"); let mut data = Vec::new(); cabinet.read_file("hi.txt").unwrap().read_to_end(&mut data).unwrap(); assert_eq!(data, b"Hello, world!\n"); } #[test] fn read_mszip_cabinet_with_two_files() { let binary: &[u8] = b"MSCF\0\0\0\0\x88\0\0\0\0\0\0\0\ \x2c\0\0\0\0\0\0\0\x03\x01\x01\0\x02\0\0\0\x34\x12\0\0\ \x5b\0\0\0\x01\0\x01\0\ \x0e\0\0\0\0\0\0\0\0\0\x6c\x22\xe7\x59\x01\0hi.txt\0\ \x0f\0\0\0\x0e\0\0\0\0\0\x6c\x22\xe7\x59\x01\0bye.txt\0\ \0\0\0\0\x25\0\x1d\0CK\xf3H\xcd\xc9\xc9\xd7Q(\xcf/\xcaIQ\xe4\ \nNMU\xa8\xcc/U\xc8I,I-R\xe4\x02\x00\x93\xfc\t\x91"; assert_eq!(binary.len(), 0x88); let mut cabinet = Cabinet::new(Cursor::new(binary)).unwrap(); let mut data = Vec::new(); cabinet.read_folder(0).unwrap().read_to_end(&mut data).unwrap(); assert_eq!(data, b"Hello, world!\nSee you later!\n"); let mut data = Vec::new(); cabinet.read_file("hi.txt").unwrap().read_to_end(&mut data).unwrap(); assert_eq!(data, b"Hello, world!\n"); let mut data = Vec::new(); cabinet.read_file("bye.txt").unwrap().read_to_end(&mut data).unwrap(); assert_eq!(data, b"See you later!\n"); } #[test] fn read_lzx_cabinet_with_two_files() { let binary: &[u8] = b"\x4d\x53\x43\x46\x00\x00\x00\x00\x97\x00\x00\x00\x00\x00\x00\ \x00\x2c\x00\x00\x00\x00\x00\x00\x00\x03\x01\x01\x00\x02\x00\ \x00\x00\x2d\x05\x00\x00\x5b\x00\x00\x00\x01\x00\x03\x13\x0f\ \x00\x00\x00\x00\x00\x00\x00\x00\x00\x21\x53\x0d\xb2\x20\x00\ \x68\x69\x2e\x74\x78\x74\x00\x10\x00\x00\x00\x0f\x00\x00\x00\ \x00\x00\x21\x53\x0b\xb2\x20\x00\x62\x79\x65\x2e\x74\x78\x74\ \x00\x5c\xef\x2a\xc7\x34\x00\x1f\x00\x5b\x80\x80\x8d\x00\x30\ \xf0\x01\x10\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x48\ \x65\x6c\x6c\x6f\x2c\x20\x77\x6f\x72\x6c\x64\x21\x0d\x0a\x53\ \x65\x65\x20\x79\x6f\x75\x20\x6c\x61\x74\x65\x72\x21\x0d\x0a\ \x00"; assert_eq!(binary.len(), 0x97); let mut cabinet = Cabinet::new(Cursor::new(binary)).unwrap(); let mut data = Vec::new(); cabinet.read_folder(0).unwrap().read_to_end(&mut data).unwrap(); assert_eq!(data, b"Hello, world!\r\nSee you later!\r\n"); let mut data = Vec::new(); cabinet.read_file("hi.txt").unwrap().read_to_end(&mut data).unwrap(); assert_eq!(data, b"Hello, world!\r\n"); let mut data = Vec::new(); cabinet.read_file("bye.txt").unwrap().read_to_end(&mut data).unwrap(); assert_eq!(data, b"See you later!\r\n"); } #[test] fn read_uncompressed_cabinet_with_non_ascii_filename() { let binary: &[u8] = b"MSCF\0\0\0\0\x55\0\0\0\0\0\0\0\ \x2c\0\0\0\0\0\0\0\x03\x01\x01\0\x01\0\0\0\0\0\0\0\ \x44\0\0\0\x01\0\0\0\ \x09\0\0\0\0\0\0\0\0\0\x6c\x22\xba\x59\xa0\0\xe2\x98\x83.txt\0\ \x3d\x0f\x08\x56\x09\0\x09\0Snowman!\n"; assert_eq!(binary.len(), 0x55); let mut cabinet = Cabinet::new(Cursor::new(binary)).unwrap(); { let file_entry = cabinet.get_file_entry("\u{2603}.txt").unwrap(); assert_eq!(file_entry.name(), "\u{2603}.txt"); assert!(file_entry.is_name_utf()); } { let mut file_reader = cabinet.read_file("\u{2603}.txt").unwrap(); let mut data = Vec::new(); file_reader.read_to_end(&mut data).unwrap(); assert_eq!(data, b"Snowman!\n"); } } } cab-0.6.0/src/checksum.rs000064400000000000000000000042671046102023000133240ustar 00000000000000pub struct Checksum { value: u32, remainder: u32, remainder_shift: u32, } impl Checksum { pub fn new() -> Checksum { Checksum { value: 0, remainder: 0, remainder_shift: 0 } } pub fn value(&self) -> u32 { match self.remainder_shift { 0 => self.value, 8 => self.value ^ self.remainder, 16 => { self.value ^ (self.remainder >> 8) ^ ((self.remainder & 0xff) << 8) } 24 => { self.value ^ (self.remainder >> 16) ^ (self.remainder & 0xff00) ^ ((self.remainder & 0xff) << 16) } _ => unreachable!(), } } pub fn update(&mut self, buf: &[u8]) { for &byte in buf { self.remainder |= (byte as u32) << self.remainder_shift; if self.remainder_shift == 24 { self.value ^= self.remainder; self.remainder = 0; self.remainder_shift = 0; } else { self.remainder_shift += 8; } } } } #[cfg(test)] mod tests { use super::Checksum; #[test] fn empty_checksum() { assert_eq!(Checksum::new().value(), 0); } #[test] fn simple_checksums() { let mut checksum = Checksum::new(); checksum.update(b"\x0e\0\x0e\0Hello, world!\n"); assert_eq!(checksum.value(), 0x7f2e1a4c); let mut checksum = Checksum::new(); checksum.update(b"\x1d\0\x1d\0Hello, world!\nSee you later!\n"); assert_eq!(checksum.value(), 0x3509541a); } #[test] fn checksum_from_cab_spec() { // This comes from the example cabinet file found in the CAB spec. let mut checksum = Checksum::new(); checksum.update( b"\x97\0\x97\0#include \r\n\r\n\ void main(void)\r\n{\r\n \ printf(\"Hello, world!\\n\");\r\n}\r\n\ #include \r\n\r\n\ void main(void)\r\n{\r\n \ printf(\"Welcome!\\n\");\r\n}\r\n\r\n", ); assert_eq!(checksum.value(), 0x30a65abd); } } cab-0.6.0/src/consts.rs000064400000000000000000000015011046102023000130170ustar 00000000000000pub const FILE_SIGNATURE: u32 = 0x4643534d; // "MSCF" stored little-endian pub const VERSION_MAJOR: u8 = 1; pub const VERSION_MINOR: u8 = 3; pub const MAX_TOTAL_CAB_SIZE: u32 = 0x7fffffff; pub const MAX_HEADER_RESERVE_SIZE: usize = 60_000; pub const MAX_FOLDER_RESERVE_SIZE: usize = 255; pub const MAX_STRING_SIZE: usize = 255; pub const MAX_NUM_FILES: usize = 0xffff; pub const MAX_NUM_FOLDERS: usize = 0xffff; pub const MAX_FILE_SIZE: u32 = 0x7fff8000; // Header flags: pub const FLAG_PREV_CABINET: u16 = 0x1; pub const FLAG_NEXT_CABINET: u16 = 0x2; pub const FLAG_RESERVE_PRESENT: u16 = 0x4; // File attributes: pub const ATTR_READ_ONLY: u16 = 0x01; pub const ATTR_HIDDEN: u16 = 0x02; pub const ATTR_SYSTEM: u16 = 0x04; pub const ATTR_ARCH: u16 = 0x20; pub const ATTR_EXEC: u16 = 0x40; pub const ATTR_NAME_IS_UTF: u16 = 0x80; cab-0.6.0/src/ctype.rs000064400000000000000000000144651046102023000126470ustar 00000000000000use std::io; use lzxd::Lzxd; use crate::mszip::MsZipDecompressor; const CTYPE_NONE: u16 = 0; const CTYPE_MSZIP: u16 = 1; const CTYPE_QUANTUM: u16 = 2; const CTYPE_LZX: u16 = 3; const QUANTUM_LEVEL_MIN: u16 = 1; const QUANTUM_LEVEL_MAX: u16 = 7; const QUANTUM_MEMORY_MIN: u16 = 10; const QUANTUM_MEMORY_MAX: u16 = 21; /// A scheme for compressing data within the cabinet. #[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)] pub enum CompressionType { /// No compression. None, /// MSZIP compression. MSZIP is described further in /// [MS-MCI](https://msdn.microsoft.com/en-us/library/cc483131.aspx). MsZip, /// Quantum compression with the given level and memory. Quantum(u16, u16), /// LZX compression with the given window size. The LZX compression scheme /// is described further in /// [MS-PATCH](https://msdn.microsoft.com/en-us/library/cc483133.aspx). Lzx(lzxd::WindowSize), } impl CompressionType { pub(crate) fn from_bitfield(bits: u16) -> io::Result { let ctype = bits & 0x000f; if ctype == CTYPE_NONE { Ok(CompressionType::None) } else if ctype == CTYPE_MSZIP { Ok(CompressionType::MsZip) } else if ctype == CTYPE_QUANTUM { let level = (bits & 0x00f0) >> 4; if !(QUANTUM_LEVEL_MIN..=QUANTUM_LEVEL_MAX).contains(&level) { invalid_data!("Invalid Quantum level: 0x{:02x}", level); } let memory = (bits & 0x1f00) >> 8; if !(QUANTUM_MEMORY_MIN..=QUANTUM_MEMORY_MAX).contains(&memory) { invalid_data!("Invalid Quantum memory: 0x{:02x}", memory); } Ok(CompressionType::Quantum(level, memory)) } else if ctype == CTYPE_LZX { let window = (bits & 0x1f00) >> 8; let window = match window { 15 => lzxd::WindowSize::KB32, 16 => lzxd::WindowSize::KB64, 17 => lzxd::WindowSize::KB128, 18 => lzxd::WindowSize::KB256, 19 => lzxd::WindowSize::KB512, 20 => lzxd::WindowSize::MB1, 21 => lzxd::WindowSize::MB2, 22 => lzxd::WindowSize::MB4, 23 => lzxd::WindowSize::MB8, 24 => lzxd::WindowSize::MB16, 25 => lzxd::WindowSize::MB32, _ => invalid_data!("Invalid LZX window: 0x{:02x}", window), }; Ok(CompressionType::Lzx(window)) } else { invalid_data!("Invalid compression type: 0x{:04x}", bits); } } pub(crate) fn to_bitfield(self) -> u16 { match self { CompressionType::None => CTYPE_NONE, CompressionType::MsZip => CTYPE_MSZIP, CompressionType::Quantum(level, memory) => { CTYPE_QUANTUM | (level.max(QUANTUM_LEVEL_MIN).min(QUANTUM_LEVEL_MAX) << 4) | (memory.max(QUANTUM_MEMORY_MIN).min(QUANTUM_MEMORY_MAX) << 8) } CompressionType::Lzx(window_size) => { let window = match window_size { lzxd::WindowSize::KB32 => 15, lzxd::WindowSize::KB64 => 16, lzxd::WindowSize::KB128 => 17, lzxd::WindowSize::KB256 => 18, lzxd::WindowSize::KB512 => 19, lzxd::WindowSize::MB1 => 20, lzxd::WindowSize::MB2 => 21, lzxd::WindowSize::MB4 => 22, lzxd::WindowSize::MB8 => 23, lzxd::WindowSize::MB16 => 24, lzxd::WindowSize::MB32 => 25, }; CTYPE_LZX | (window << 8) } } } pub(crate) fn into_decompressor(self) -> io::Result { match self { CompressionType::None => Ok(Decompressor::Uncompressed), CompressionType::MsZip => { Ok(Decompressor::MsZip(Box::new(MsZipDecompressor::new()))) } CompressionType::Quantum(_, _) => { invalid_data!("Quantum decompression is not yet supported.") } CompressionType::Lzx(window_size) => { Ok(Decompressor::Lzx(Box::new(Lzxd::new(window_size)))) } } } } pub enum Decompressor { Uncompressed, MsZip(Box), Lzx(Box), } impl Decompressor { pub(crate) fn reset(&mut self) { match self { Self::Uncompressed => {} Self::MsZip(d) => d.reset(), Self::Lzx(d) => d.reset(), } } pub(crate) fn decompress( &mut self, data: Vec, uncompressed_size: usize, ) -> io::Result> { let data = match self { Decompressor::Uncompressed => data, Decompressor::MsZip(decompressor) => decompressor .decompress_block(&data, uncompressed_size) .map_err(|e| io::Error::new(io::ErrorKind::Other, e))? .to_vec(), Decompressor::Lzx(decompressor) => decompressor .decompress_next(&data, uncompressed_size) .map_err(|e| io::Error::new(io::ErrorKind::Other, e))? .to_vec(), }; Ok(data) } } #[cfg(test)] mod tests { use super::CompressionType; #[test] fn compression_type_to_bitfield() { assert_eq!(CompressionType::None.to_bitfield(), 0x0); assert_eq!(CompressionType::MsZip.to_bitfield(), 0x1); assert_eq!(CompressionType::Quantum(7, 20).to_bitfield(), 0x1472); assert_eq!( CompressionType::Lzx(lzxd::WindowSize::MB2).to_bitfield(), 0x1503 ); } #[test] fn compression_type_from_bitfield() { assert_eq!( CompressionType::from_bitfield(0x0).unwrap(), CompressionType::None ); assert_eq!( CompressionType::from_bitfield(0x1).unwrap(), CompressionType::MsZip ); assert_eq!( CompressionType::from_bitfield(0x1472).unwrap(), CompressionType::Quantum(7, 20) ); assert_eq!( CompressionType::from_bitfield(0x1503).unwrap(), CompressionType::Lzx(lzxd::WindowSize::MB2) ); } } cab-0.6.0/src/datetime.rs000064400000000000000000000054771046102023000133220ustar 00000000000000use std::convert::TryInto; use time::PrimitiveDateTime; pub fn datetime_from_bits(date: u16, time: u16) -> Option { let year = (date >> 9) as i32 + 1980; let month = (((date >> 5) & 0xf) as u8).try_into().ok()?; let day = (date & 0x1f) as u8; let date = time::Date::from_calendar_date(year, month, day).ok()?; let hour = (time >> 11) as u8; let minute = ((time >> 5) & 0x3f) as u8; let second = 2 * (time & 0x1f) as u8; let time = time::Time::from_hms(hour, minute, second).ok()?; Some(PrimitiveDateTime::new(date, time)) } pub fn datetime_to_bits(mut datetime: PrimitiveDateTime) -> (u16, u16) { // Clamp to legal range: if datetime.year() < 1980 { return (0x21, 0); // 1980-01-01 00:00:00 } else if datetime.year() > 2107 { return (0xff9f, 0xbf7d); // 2107-12-31 23:59:59 } // Round to nearest two seconds: if datetime.second() % 2 != 0 { datetime += time::Duration::seconds(1); } let year = datetime.year() as u16; let month = datetime.month() as u16; let day = datetime.day() as u16; let date = ((year - 1980) << 9) | (month << 5) | day; let hour = datetime.hour() as u16; let minute = datetime.minute() as u16; let second = datetime.second() as u16; let time = (hour << 11) | (minute << 5) | (second / 2); (date, time) } #[cfg(test)] mod tests { use time::macros::datetime; use super::{datetime_from_bits, datetime_to_bits}; #[test] fn valid_datetime_bits() { let dt = datetime!(2018-01-06 15:19:42); assert_eq!(datetime_to_bits(dt), (0x4c26, 0x7a75)); assert_eq!(datetime_from_bits(0x4c26, 0x7a75), Some(dt)); } #[test] fn datetime_outside_range() { let dt = datetime!(1977-02-03 4:05:06); let bits = datetime_to_bits(dt); let dt = datetime!(1980-01-01 0:00:00); assert_eq!(datetime_from_bits(bits.0, bits.1), Some(dt)); assert_eq!(bits, (0x0021, 0x0000)); let dt = datetime!(2110-02-03 4:05:06); let bits = datetime_to_bits(dt); let dt = datetime!(2107-12-31 23:59:58); assert_eq!(datetime_from_bits(bits.0, bits.1), Some(dt)); assert_eq!(bits, (0xff9f, 0xbf7d)); } #[test] fn datetime_round_to_nearest_two_seconds() { // Round down: let dt = datetime!(2012-03-04 1:02:06.900); let bits = datetime_to_bits(dt); let dt = datetime!(2012-03-04 1:02:06); assert_eq!(datetime_from_bits(bits.0, bits.1), Some(dt)); assert_eq!(bits, (0x4064, 0x0843)); // Round up: let dt = datetime!(2012-03-04 5:06:59.3); let bits = datetime_to_bits(dt); let dt = datetime!(2012-03-04 5:07:00); assert_eq!(datetime_from_bits(bits.0, bits.1), Some(dt)); assert_eq!(bits, (0x4064, 0x28e0)); } } cab-0.6.0/src/file.rs000064400000000000000000000117441046102023000124370ustar 00000000000000use std::io::{self, Read, Seek, SeekFrom}; use std::slice; use byteorder::{LittleEndian, ReadBytesExt}; use time::PrimitiveDateTime; use crate::consts; use crate::datetime::datetime_from_bits; use crate::folder::FolderReader; use crate::string::read_null_terminated_string; /// An iterator over the file entries in a folder. #[derive(Clone)] pub struct FileEntries<'a> { pub(crate) iter: slice::Iter<'a, FileEntry>, } /// Metadata about one file stored in a cabinet. #[derive(Debug, Clone)] pub struct FileEntry { name: String, datetime: Option, uncompressed_size: u32, attributes: u16, pub(crate) folder_index: u16, pub(crate) uncompressed_offset: u32, } /// A reader for reading decompressed data from a cabinet file. pub struct FileReader<'a, R: 'a> { pub(crate) reader: FolderReader<'a, R>, pub(crate) file_start_in_folder: u64, pub(crate) offset: u64, pub(crate) size: u64, } impl<'a> Iterator for FileEntries<'a> { type Item = &'a FileEntry; fn next(&mut self) -> Option<&'a FileEntry> { self.iter.next() } fn size_hint(&self) -> (usize, Option) { self.iter.size_hint() } } impl<'a> ExactSizeIterator for FileEntries<'a> {} impl FileEntry { /// Returns the name of file. pub fn name(&self) -> &str { &self.name } /// Returns the datetime for this file. According to the CAB spec, this /// "is typically considered the 'last modified' time in local time, but /// the actual definition is application-defined." /// /// Note that this will return [`None`] if the datetime in the cabinet file /// was not a valid date/time. pub fn datetime(&self) -> Option { self.datetime } /// Returns the total size of the file when decompressed, in bytes. pub fn uncompressed_size(&self) -> u32 { self.uncompressed_size } /// Returns true if this file has the "read-only" attribute set. pub fn is_read_only(&self) -> bool { (self.attributes & consts::ATTR_READ_ONLY) != 0 } /// Returns true if this file has the "hidden" attribute set. pub fn is_hidden(&self) -> bool { (self.attributes & consts::ATTR_HIDDEN) != 0 } /// Returns true if this file has the "system file" attribute set. pub fn is_system(&self) -> bool { (self.attributes & consts::ATTR_SYSTEM) != 0 } /// Returns true if this file has the "archive" (modified since last /// backup) attribute set. pub fn is_archive(&self) -> bool { (self.attributes & consts::ATTR_ARCH) != 0 } /// Returns true if this file has the "execute after extraction" attribute /// set. pub fn is_exec(&self) -> bool { (self.attributes & consts::ATTR_EXEC) != 0 } /// Returns true if this file has the "name is UTF" attribute set. pub fn is_name_utf(&self) -> bool { (self.attributes & consts::ATTR_NAME_IS_UTF) != 0 } } impl<'a, R: Read + Seek> Read for FileReader<'a, R> { fn read(&mut self, buf: &mut [u8]) -> io::Result { debug_assert!(self.offset <= self.size); let bytes_remaining = self.size - self.offset; let max_bytes = bytes_remaining.min(buf.len() as u64) as usize; if max_bytes == 0 { return Ok(0); } let bytes_read = self.reader.read(&mut buf[..max_bytes])?; self.offset += bytes_read as u64; Ok(bytes_read) } } impl<'a, R: Read + Seek> Seek for FileReader<'a, R> { fn seek(&mut self, pos: SeekFrom) -> io::Result { let new_offset = match pos { SeekFrom::Start(offset) => offset as i64, SeekFrom::Current(delta) => self.offset as i64 + delta, SeekFrom::End(delta) => self.size as i64 + delta, }; if new_offset < 0 || (new_offset as u64) > self.size { invalid_input!( "Cannot seek to {}, file length is {}", new_offset, self.size ); } let new_offset = new_offset as u64; self.reader.seek_to_uncompressed_offset( self.file_start_in_folder + new_offset, )?; self.offset = new_offset; Ok(new_offset) } } pub(crate) fn parse_file_entry( mut reader: R, ) -> io::Result { let uncompressed_size = reader.read_u32::()?; let uncompressed_offset = reader.read_u32::()?; let folder_index = reader.read_u16::()?; let date = reader.read_u16::()?; let time = reader.read_u16::()?; let datetime = datetime_from_bits(date, time); let attributes = reader.read_u16::()?; let is_utf8 = (attributes & consts::ATTR_NAME_IS_UTF) != 0; let name = read_null_terminated_string(&mut reader, is_utf8)?; let entry = FileEntry { name, folder_index, datetime, uncompressed_size, uncompressed_offset, attributes, }; Ok(entry) } cab-0.6.0/src/folder.rs000064400000000000000000000233501046102023000127670ustar 00000000000000use std::io::{self, Read, Seek, SeekFrom}; use std::marker::PhantomData; use std::slice; use byteorder::{LittleEndian, ReadBytesExt}; use crate::cabinet::{Cabinet, ReadSeek}; use crate::checksum::Checksum; use crate::ctype::{CompressionType, Decompressor}; use crate::file::{FileEntries, FileEntry}; /// An iterator over the folder entries in a cabinet. #[derive(Clone)] pub struct FolderEntries<'a> { pub(crate) iter: slice::Iter<'a, FolderEntry>, } /// Metadata about one folder in a cabinet. pub struct FolderEntry { first_data_block_offset: u32, num_data_blocks: u16, compression_type: CompressionType, reserve_data: Vec, pub(crate) files: Vec, } #[derive(Debug, Clone)] struct DataBlockEntry { checksum: u32, compressed_size: u16, uncompressed_size: u16, reserve_data: Vec, data_offset: u64, cumulative_size: u64, } /// A reader for reading decompressed data from a cabinet folder. pub(crate) struct FolderReader<'a, R> { reader: &'a Cabinet, num_data_blocks: usize, data_reserve_size: u8, decompressor: Decompressor, /// The data blocks we've read so far. /// This always has len() <= num_data_blocks and grows once we encounter /// a new block in load_block(). data_blocks: Vec, current_block_index: usize, current_block_data: Vec, current_offset_within_block: usize, current_offset_within_folder: u64, _p: PhantomData, } impl<'a> Iterator for FolderEntries<'a> { type Item = &'a FolderEntry; fn next(&mut self) -> Option { self.iter.next() } fn size_hint(&self) -> (usize, Option) { self.iter.size_hint() } } impl<'a> ExactSizeIterator for FolderEntries<'a> {} impl FolderEntry { /// Returns the scheme used to compress this folder's data. pub fn compression_type(&self) -> CompressionType { self.compression_type } /// Returns the number of data blocks used to store this folder's data. pub fn num_data_blocks(&self) -> u16 { self.num_data_blocks } /// Returns the application-defined reserve data for this folder. pub fn reserve_data(&self) -> &[u8] { &self.reserve_data } /// Returns an iterator over the file entries in this folder. pub fn file_entries(&self) -> FileEntries { FileEntries { iter: self.files.iter() } } } impl<'a, R: Read + Seek> FolderReader<'a, R> { pub(crate) fn new( reader: &'a Cabinet, entry: &FolderEntry, data_reserve_size: u8, ) -> io::Result> { let num_data_blocks = entry.num_data_blocks as usize; let mut data_blocks = Vec::with_capacity(num_data_blocks); let r = &mut &reader.inner; r.seek(SeekFrom::Start(entry.first_data_block_offset as u64))?; if num_data_blocks != 0 { let first_block = parse_block_entry(*r, 0, data_reserve_size as usize)?; data_blocks.push(first_block); } let decompressor = entry.compression_type.into_decompressor()?; let mut folder_reader = FolderReader { reader, num_data_blocks, data_reserve_size, decompressor, data_blocks, current_block_index: 0, current_block_data: Vec::new(), current_offset_within_block: 0, current_offset_within_folder: 0, _p: PhantomData, }; folder_reader.load_block()?; Ok(folder_reader) } pub fn seek_to_uncompressed_offset( &mut self, new_offset: u64, ) -> io::Result<()> { if new_offset < self.current_block_start() { self.rewind()?; } if new_offset > 0 { // TODO: If folder is uncompressed, we should just jump straight to // the correct block without "decompressing" those in between. while self.data_blocks[self.current_block_index].cumulative_size < new_offset { self.current_block_index += 1; self.load_block()?; } } debug_assert!(new_offset >= self.current_block_start()); self.current_offset_within_block = (new_offset - self.current_block_start()) as usize; self.current_offset_within_folder = new_offset; Ok(()) } fn current_block_start(&self) -> u64 { if self.current_block_index == 0 { 0 } else { self.data_blocks[self.current_block_index - 1].cumulative_size } } fn rewind(&mut self) -> io::Result<()> { self.current_offset_within_block = 0; self.current_offset_within_folder = 0; if self.current_block_index != 0 { self.current_block_index = 0; self.decompressor.reset(); self.load_block()?; } Ok(()) } fn load_block(&mut self) -> io::Result<()> { if self.current_block_index >= self.num_data_blocks { self.current_block_data = Vec::new(); return Ok(()); } debug_assert!(self.current_block_index <= self.data_blocks.len()); let block = if self.current_block_index == self.data_blocks.len() { let previous_block = self.data_blocks.last().unwrap(); let reader = &mut &self.reader.inner; reader.seek(SeekFrom::Start( previous_block.data_offset + previous_block.compressed_size as u64, ))?; let block = parse_block_entry( reader, previous_block.cumulative_size, self.data_reserve_size as usize, )?; self.data_blocks.push(block); &self.data_blocks[self.current_block_index] } else { let block = &self.data_blocks[self.current_block_index]; let reader = &mut &self.reader.inner; reader.seek(SeekFrom::Start(block.data_offset))?; block }; let mut compressed_data = vec![0u8; block.compressed_size as usize]; let reader = &mut &self.reader.inner; reader.read_exact(&mut compressed_data)?; if block.checksum != 0 { let mut checksum = Checksum::new(); checksum.update(&block.reserve_data); checksum.update(&compressed_data); let actual_checksum = checksum.value() ^ ((block.compressed_size as u32) | ((block.uncompressed_size as u32) << 16)); if actual_checksum != block.checksum { invalid_data!( "Checksum error in data block {} \ (expected {:08x}, actual {:08x})", self.current_block_index, block.checksum, actual_checksum ); } } self.current_block_data = self .decompressor .decompress(compressed_data, block.uncompressed_size as usize)?; Ok(()) } } impl<'a, R: Read + Seek + 'a> Read for FolderReader<'a, R> { fn read(&mut self, buf: &mut [u8]) -> io::Result { if buf.is_empty() || self.current_block_index >= self.num_data_blocks { return Ok(0); } if self.current_offset_within_block == self.current_block_data.len() { self.current_block_index += 1; self.current_offset_within_block = 0; self.load_block()?; } let max_bytes = buf.len().min( self.current_block_data.len() - self.current_offset_within_block, ); buf[..max_bytes].copy_from_slice( &self.current_block_data[self.current_offset_within_block..] [..max_bytes], ); self.current_offset_within_block += max_bytes; self.current_offset_within_folder += max_bytes as u64; Ok(max_bytes) } } pub(crate) fn parse_folder_entry( mut reader: R, reserve_size: usize, ) -> io::Result { let first_data_offset = reader.read_u32::()?; let num_data_blocks = reader.read_u16::()?; let compression_bits = reader.read_u16::()?; let compression_type = CompressionType::from_bitfield(compression_bits)?; let mut folder_reserve_data = vec![0u8; reserve_size]; if reserve_size > 0 { reader.read_exact(&mut folder_reserve_data)?; } let entry = FolderEntry { first_data_block_offset: first_data_offset, num_data_blocks, compression_type, reserve_data: folder_reserve_data, files: vec![], }; Ok(entry) } /// Parse a data block entry from a reader. /// /// The reader must be positioned at the start of the data block, /// which is either at the start first data block or immediately after /// the previous data block, i.e. at position /// `previous_data_block.data_offset + previous_data_block.compressed_size`. /// /// Once this function returns, the reader will be positioned at the current /// block's `data_offset`. fn parse_block_entry( mut reader: R, cumulative_size: u64, data_reserve_size: usize, ) -> io::Result { let checksum = reader.read_u32::()?; let compressed_size = reader.read_u16::()?; let uncompressed_size = reader.read_u16::()?; let mut reserve_data = vec![0u8; data_reserve_size]; reader.read_exact(&mut reserve_data)?; let data_offset = reader.stream_position()?; let cumulative_size = cumulative_size + uncompressed_size as u64; Ok(DataBlockEntry { checksum, compressed_size, uncompressed_size, reserve_data, cumulative_size, data_offset, }) } cab-0.6.0/src/lib.rs000064400000000000000000000106371046102023000122660ustar 00000000000000//! A library for reading/writing [Windows //! cabinet](https://en.wikipedia.org/wiki/Cabinet_(file_format)) (CAB) files. //! //! # Overview //! //! CAB is an archive file format used by Windows. A cabinet file can contain //! multiple compressed files, which are divided into "folders" (no relation to //! filesystem folders/directories); files in the same folder are compressed //! together, and each folder in the cabinet can potentially use a different //! compression scheme. The CAB file format supports multiple different //! compression schemes; this library can recognize all of them when reading //! metadata for an existing cabinet file, but currently only supports //! encoding/decoding some of them, as shown: //! //! | Compression | Supported | //! |----------------------------|-------------------| //! | Uncompressed | Yes | //! | MSZIP ([Deflate][deflate]) | Yes | //! | [Quantum][quantum] | No | //! | [LZX][lzx] | Yes (decode only) | //! //! [deflate]: https://en.wikipedia.org/wiki/DEFLATE //! [quantum]: https://en.wikipedia.org/wiki/Quantum_compression //! [lzx]: https://en.wikipedia.org/wiki/LZX_(algorithm) //! //! # Example usage //! //! Use the `Cabinet` type to read an existing cabinet file: //! //! ```no_run //! use cab; //! use std::fs; //! use std::io; //! //! let cab_file = fs::File::open("path/to/cabinet.cab").unwrap(); //! let mut cabinet = cab::Cabinet::new(cab_file).unwrap(); //! // List all files in the cabinet, with file sizes and compression types: //! for folder in cabinet.folder_entries() { //! for file in folder.file_entries() { //! println!("File {} ({} B) is compressed with {:?}", //! file.name(), //! file.uncompressed_size(), //! folder.compression_type()); //! } //! } //! // Decompress a particular file in the cabinet and save it to disk: //! let mut reader = cabinet.read_file("images/example.png").unwrap(); //! let mut writer = fs::File::create("out/example.png").unwrap(); //! io::copy(&mut reader, &mut writer).unwrap(); //! ``` //! //! Creating a new cabinet file is a little more involved. Because of how the //! cabinet file is structured on disk, the library has to know the names of //! all the files that will be in the cabinet up front, before it can start //! writing anything to disk. However, we don't want to have to hold all the //! file **contents** in memory at once. Therefore, cabinet creation happens //! in two steps: first, create a `CabinetBuilder` and specify all filenames //! and other metadata, and then second, stream each file's data into a //! `CabinetWriter`, one at a time: //! //! ```no_run //! use cab; //! use std::fs; //! use std::io; //! //! let mut cab_builder = cab::CabinetBuilder::new(); //! // Add a single file in its own folder: //! cab_builder.add_folder(cab::CompressionType::None).add_file("img/foo.jpg"); //! // Add several more files, compressed together in a second folder: //! { //! let folder = cab_builder.add_folder(cab::CompressionType::MsZip); //! folder.add_file("documents/README.txt"); //! folder.add_file("documents/license.txt"); //! // We can also specify metadata on individual files: //! { //! let file = folder.add_file("documents/hidden.txt"); //! file.set_is_hidden(true); //! file.set_is_read_only(true); //! } //! } //! // Now, we'll actually construct the cabinet file on disk: //! let cab_file = fs::File::create("path/to/cabinet.cab").unwrap(); //! let mut cab_writer = cab_builder.build(cab_file).unwrap(); //! while let Some(mut writer) = cab_writer.next_file().unwrap() { //! let mut reader = fs::File::open(writer.file_name()).unwrap(); //! io::copy(&mut reader, &mut writer).unwrap(); //! } //! // Print the file size of the cabinet file we just created: //! let mut cab_file = cab_writer.finish().unwrap(); //! println!("Cabinet size: {} B", cab_file.metadata().unwrap().len()); //! ``` #![warn(missing_docs)] pub use lzxd::WindowSize; pub use builder::{ CabinetBuilder, CabinetWriter, FileBuilder, FileWriter, FolderBuilder, }; pub use cabinet::Cabinet; pub use ctype::CompressionType; pub use file::{FileEntries, FileEntry, FileReader}; pub use folder::{FolderEntries, FolderEntry}; #[macro_use] mod macros; mod builder; mod cabinet; mod checksum; mod consts; mod ctype; mod datetime; mod file; mod folder; mod mszip; mod string; cab-0.6.0/src/macros.rs000064400000000000000000000020211046102023000127700ustar 00000000000000macro_rules! invalid_data { ($e:expr) => { return Err(::std::io::Error::new(::std::io::ErrorKind::InvalidData, $e)) }; ($fmt:expr, $($arg:tt)+) => { return Err(::std::io::Error::new(::std::io::ErrorKind::InvalidData, format!($fmt, $($arg)+))) }; } macro_rules! invalid_input { ($e:expr) => { return Err(::std::io::Error::new(::std::io::ErrorKind::InvalidInput, $e)) }; ($fmt:expr, $($arg:tt)+) => { return Err(::std::io::Error::new(::std::io::ErrorKind::InvalidInput, format!($fmt, $($arg)+))) }; } macro_rules! not_found { ($e:expr) => { return Err(::std::io::Error::new(::std::io::ErrorKind::NotFound, $e)) }; ($fmt:expr, $($arg:tt)+) => { return Err(::std::io::Error::new(::std::io::ErrorKind::NotFound, format!($fmt, $($arg)+))) }; } cab-0.6.0/src/mszip.rs000064400000000000000000000354761046102023000126720ustar 00000000000000use std::io; use byteorder::{LittleEndian, WriteBytesExt}; use flate2::Compression; const MSZIP_SIGNATURE: u16 = 0x4B43; // "CK" stored little-endian const MSZIP_SIGNATURE_LEN: usize = 2; const MSZIP_BLOCK_TERMINATOR: u16 = 0x0003; const DEFLATE_MAX_DICT_LEN: usize = 0x8000; pub struct MsZipCompressor { compressor: flate2::Compress, } impl MsZipCompressor { pub fn new() -> MsZipCompressor { MsZipCompressor { compressor: flate2::Compress::new(Compression::best(), false), } } pub fn compress_block( &mut self, data: &[u8], is_last_block: bool, ) -> io::Result> { debug_assert!(data.len() <= 0x8000); let mut out = Vec::::with_capacity(0xffff); out.write_u16::(MSZIP_SIGNATURE)?; let flush = if is_last_block { flate2::FlushCompress::Finish } else { flate2::FlushCompress::Sync }; match self.compressor.compress_vec(data, &mut out, flush) { Ok(_) => {} Err(error) => invalid_data!("MSZIP compression failed: {}", error), } if !is_last_block { out.write_u16::(MSZIP_BLOCK_TERMINATOR)?; } let max_out_len = data.len() + 7; if out.len() > max_out_len { out = Vec::with_capacity(max_out_len); out.write_u16::(MSZIP_SIGNATURE)?; out.push(1); out.write_u16::(data.len() as u16)?; out.write_u16::(!(data.len() as u16))?; out.extend_from_slice(data); debug_assert_eq!(out.len(), max_out_len); debug_assert_eq!(out.capacity(), max_out_len); } Ok(out) } } pub struct MsZipDecompressor { decompressor: flate2::Decompress, dictionary: Vec, } impl MsZipDecompressor { pub fn new() -> MsZipDecompressor { MsZipDecompressor { decompressor: flate2::Decompress::new(false), dictionary: Vec::with_capacity(DEFLATE_MAX_DICT_LEN), } } pub fn reset(&mut self) { self.decompressor.reset(true); self.dictionary = Vec::with_capacity(DEFLATE_MAX_DICT_LEN); } pub fn decompress_block( &mut self, data: &[u8], uncompressed_size: usize, ) -> io::Result> { // Check signature: if data.len() < MSZIP_SIGNATURE_LEN || ((data[0] as u16) | ((data[1] as u16) << 8)) != MSZIP_SIGNATURE { invalid_data!( "MSZIP decompression failed: Invalid block signature" ); } let data = &data[MSZIP_SIGNATURE_LEN..]; // Reset decompressor with appropriate dictionary: self.decompressor.reset(false); if !self.dictionary.is_empty() { // TODO: Avoid doing extra allocations/copies here. debug_assert!(self.dictionary.len() <= DEFLATE_MAX_DICT_LEN); let length = self.dictionary.len() as u16; let mut chunk: Vec = vec![0]; chunk.write_u16::(length)?; chunk.write_u16::(!length)?; chunk.extend_from_slice(&self.dictionary); let mut out = Vec::with_capacity(self.dictionary.len()); let flush = flate2::FlushDecompress::Sync; match self.decompressor.decompress_vec(&chunk, &mut out, flush) { Ok(flate2::Status::Ok) => {} _ => unreachable!(), } } // Decompress data: let mut out = Vec::::with_capacity(uncompressed_size); let flush = flate2::FlushDecompress::Finish; match self.decompressor.decompress_vec(data, &mut out, flush) { Ok(_) => {} Err(error) => { invalid_data!("MSZIP decompression failed: {}", error); } } if out.len() != uncompressed_size { invalid_data!( "MSZIP decompression failed: Incorrect uncompressed size \ (expected {}, was actually {})", uncompressed_size, out.len() ); } // Update dictionary for next block: if out.len() >= DEFLATE_MAX_DICT_LEN { let start = out.len() - DEFLATE_MAX_DICT_LEN; self.dictionary = out[start..].to_vec(); } else { let total = self.dictionary.len() + out.len(); if total > DEFLATE_MAX_DICT_LEN { self.dictionary.drain(..(total - DEFLATE_MAX_DICT_LEN)); } self.dictionary.extend_from_slice(&out); } debug_assert_eq!(self.dictionary.capacity(), DEFLATE_MAX_DICT_LEN); Ok(out) } } #[cfg(test)] mod tests { use rand::RngCore; use super::{MsZipCompressor, MsZipDecompressor, DEFLATE_MAX_DICT_LEN}; #[test] fn read_compressed_data() { let input: &[u8] = b"CK%\xcc\xd1\t\x031\x0c\x04\xd1V\xb6\x80#\x95\xa4\ \t\xc5\x12\xc7\x82e\xfb,\xa9\xff\x18\xee{x\xf3\x9d\xdb\x1c\\Q\ \x0e\x9d}n\x04\x13\xe2\x96\x17\xda\x1ca--kC\x94\x8b\xd18nX\xe7\ \x89az\x00\x8c\x15>\x15i\xbe\x0e\xe6hTj\x8dD%\xba\xfc\xce\x1e\ \x96\xef\xda\xe0r\x0f\x81t>%\x9f?\x12]-\x87"; let expected: &[u8] = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed \ do eiusmod tempor incididunt ut labore et dolore magna aliqua."; assert!(input.len() < expected.len()); let mut decompressor = MsZipDecompressor::new(); let output = decompressor.decompress_block(&input, expected.len()).unwrap(); assert_eq!(output, expected); } fn repeating_data(size: usize) -> Vec { let modulus = 251; // a prime number no bigger than u8::MAX (0..size).map(|index| (index % modulus) as u8).collect::>() } fn random_data(size: usize) -> Vec { use rand::SeedableRng; let mut rd = vec![0; size]; rand::rngs::SmallRng::from_entropy().fill_bytes(&mut rd); rd } #[cfg(target_env = "msvc")] /// Wrappers for the Microsoft compression API so that on Windows we can /// test interop with the system implementation. This code comes from /// https://github.com/luser/rust-makecab; thanks to Ted Mielczarek for /// sharing it. mod sys { #![allow(non_camel_case_types)] use std::mem; use std::ptr; use winapi::shared::basetsd::{PSIZE_T, SIZE_T}; use winapi::shared::minwindef::{BOOL, DWORD, FALSE, LPVOID, TRUE}; use winapi::um::winnt::{HANDLE, PVOID}; use super::super::DEFLATE_MAX_DICT_LEN; const COMPRESS_ALGORITHM_MSZIP: DWORD = 2; const COMPRESS_RAW: DWORD = 1 << 29; type PCOMPRESS_ALLOCATION_ROUTINES = LPVOID; type COMPRESSOR_HANDLE = HANDLE; type DECOMPRESSOR_HANDLE = HANDLE; type PCOMPRESSOR_HANDLE = *mut COMPRESSOR_HANDLE; type PDECOMPRESSOR_HANDLE = *mut DECOMPRESSOR_HANDLE; #[rustfmt::skip] #[link(name = "cabinet")] extern "system" { fn CreateCompressor( Algorithm: DWORD, AllocationRoutines: LPVOID, CompressorHandle: PCOMPRESSOR_HANDLE) -> BOOL; fn CloseCompressor( CompressorHandle: COMPRESSOR_HANDLE) -> BOOL; fn Compress( CompressorHandle: COMPRESSOR_HANDLE, UncompressedData: PVOID, UncompressedDataSize: SIZE_T, CompressedBuffer: PVOID, CompressedBufferSize: SIZE_T, CompressedDataSize: PSIZE_T) -> BOOL; fn CreateDecompressor( Algorithm: DWORD, AllocationRoutines: PCOMPRESS_ALLOCATION_ROUTINES, DecompressorHandle: PDECOMPRESSOR_HANDLE) -> BOOL; fn CloseDecompressor( DecompressorHandle: DECOMPRESSOR_HANDLE) -> BOOL; fn Decompress( DecompressorHandle: DECOMPRESSOR_HANDLE, CompressedData: PVOID, CompressedDataSize: SIZE_T, UncompressedBuffer: PVOID, UncompressedBufferSize: SIZE_T, UncompressedDataSize: PSIZE_T) -> BOOL; } /// Compress `data` with the Microsoft compression API. pub fn do_system_compress(data: &[u8]) -> Vec<(usize, Vec)> { let handle = unsafe { let mut handle = mem::MaybeUninit::uninit(); if CreateCompressor( COMPRESS_ALGORITHM_MSZIP | COMPRESS_RAW, ptr::null_mut(), handle.as_mut_ptr(), ) != TRUE { panic!("CreateCompressor failed"); } handle.assume_init() }; let mut blocks = Vec::<(usize, Vec)>::new(); for slice in data.chunks(DEFLATE_MAX_DICT_LEN) { let mut buffer = vec![0; 0xffff]; unsafe { let mut compressed_size = mem::MaybeUninit::uninit(); if Compress( handle, slice.as_ptr() as PVOID, slice.len() as SIZE_T, buffer.as_ptr() as PVOID, buffer.len() as SIZE_T, compressed_size.as_mut_ptr(), ) == FALSE { panic!("Compress failed"); } buffer.resize(compressed_size.assume_init() as usize, 0); } blocks.push((slice.len(), buffer)); } unsafe { CloseCompressor(handle); } blocks } pub fn do_system_decompress(blocks: Vec<(usize, Vec)>) -> Vec { let handle = unsafe { let mut handle = mem::MaybeUninit::uninit(); if CreateDecompressor( COMPRESS_ALGORITHM_MSZIP | COMPRESS_RAW, ptr::null_mut(), handle.as_mut_ptr(), ) != TRUE { panic!("CreateDecompressor failed"); } handle.assume_init() }; let mut buffer = Vec::::new(); // Decompress each chunk in turn. for (original_size, ref block) in blocks.into_iter() { assert!(original_size <= DEFLATE_MAX_DICT_LEN); // Make space in the output buffer. let last = buffer.len(); buffer.resize(last + original_size, 0); unsafe { if Decompress( handle, block.as_ptr() as PVOID, block.len() as SIZE_T, buffer[last..].as_mut_ptr() as PVOID, original_size as SIZE_T, ptr::null_mut(), ) == FALSE { panic!("Decompress failed"); } } } unsafe { CloseDecompressor(handle); } buffer } } fn do_lib_compress(mut data: &[u8]) -> Vec<(usize, Vec)> { let mut blocks = Vec::<(usize, Vec)>::new(); let mut compressor = MsZipCompressor::new(); while data.len() > DEFLATE_MAX_DICT_LEN { let slice = &data[0..DEFLATE_MAX_DICT_LEN]; let compressed = compressor.compress_block(slice, false).unwrap(); blocks.push((slice.len(), compressed)); data = &data[slice.len()..]; } let compressed = compressor.compress_block(data, true).unwrap(); blocks.push((data.len(), compressed)); blocks } fn do_lib_decompress(blocks: Vec<(usize, Vec)>) -> Vec { let mut output = Vec::::new(); let mut decompressor = MsZipDecompressor::new(); for (size, compressed) in blocks.into_iter() { output.append( &mut decompressor.decompress_block(&compressed, size).unwrap(), ); } output } macro_rules! round_trip_tests { ($name:ident, $data:expr) => { mod $name { use super::*; #[test] fn lib_to_lib() { let original: &[u8] = $data; let compressed = do_lib_compress(original); assert_eq!( do_lib_decompress(compressed).as_slice(), original ); } #[cfg(target_env = "msvc")] #[test] fn lib_to_sys() { let original: &[u8] = $data; let compressed = do_lib_compress(original); assert_eq!( sys::do_system_decompress(compressed).as_slice(), original ); } #[cfg(target_env = "msvc")] #[test] fn sys_to_lib() { let original: &[u8] = $data; let compressed = sys::do_system_compress(original); assert_eq!( do_lib_decompress(compressed).as_slice(), original ); } } }; } round_trip_tests!( lorem_ipsum, b"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed \ do eiusmod tempor incididunt ut labore et dolore magna aliqua." ); round_trip_tests!( one_block_exactly, &repeating_data(DEFLATE_MAX_DICT_LEN) ); round_trip_tests!( one_block_less_a_byte, &repeating_data(DEFLATE_MAX_DICT_LEN - 1) ); round_trip_tests!( one_block_plus_a_byte, &repeating_data(DEFLATE_MAX_DICT_LEN + 1) ); round_trip_tests!(zeros_one_block, &[0u8; 1000]); round_trip_tests!(zeros_two_blocks, &[0u8; DEFLATE_MAX_DICT_LEN + 1000]); round_trip_tests!(zeros_many_blocks, &[0u8; DEFLATE_MAX_DICT_LEN * 10]); round_trip_tests!(repeating_one_block, &repeating_data(1000)); round_trip_tests!( repeating_two_blocks, &repeating_data(DEFLATE_MAX_DICT_LEN + 1000) ); round_trip_tests!( repeating_many_blocks, &repeating_data(DEFLATE_MAX_DICT_LEN * 10) ); round_trip_tests!(random_one_block, &random_data(1000)); round_trip_tests!( random_two_blocks, &random_data(DEFLATE_MAX_DICT_LEN + 1000) ); round_trip_tests!( random_many_blocks, &random_data(DEFLATE_MAX_DICT_LEN * 10) ); } cab-0.6.0/src/string.rs000064400000000000000000000013171046102023000130210ustar 00000000000000use std::io::{self, Read}; use byteorder::ReadBytesExt; use crate::consts; pub(crate) fn read_null_terminated_string( reader: &mut R, _is_utf8: bool, ) -> io::Result { let mut bytes = Vec::::with_capacity(consts::MAX_STRING_SIZE); loop { let byte = reader.read_u8()?; if byte == 0 { break; } else if bytes.len() == consts::MAX_STRING_SIZE { invalid_data!( "String longer than maximum of {} bytes", consts::MAX_STRING_SIZE ); } bytes.push(byte); } // TODO: Handle decoding differently depending on `_is_utf8`. Ok(String::from_utf8_lossy(&bytes).to_string()) } cab-0.6.0/tests/roundtrip.rs000064400000000000000000000154101046102023000141130ustar 00000000000000use std::io::{Cursor, Read, Write}; use time::macros::datetime; // ========================================================================= // #[test] fn cabinet_with_one_small_uncompressed_text_file() { let original = lipsum::lipsum(500); let datetime = datetime!(2063-04-05 23:14:38); let mut cab_builder = cab::CabinetBuilder::new(); { let folder_builder = cab_builder.add_folder(cab::CompressionType::None); let file_builder = folder_builder.add_file("lorem_ipsum.txt"); file_builder.set_datetime(datetime); file_builder.set_is_read_only(true); file_builder.set_is_system(true); file_builder.set_is_archive(false); } let mut cab_writer = cab_builder.build(Cursor::new(Vec::new())).unwrap(); while let Some(mut file_writer) = cab_writer.next_file().unwrap() { file_writer.write_all(original.as_bytes()).unwrap(); } let cab_file = cab_writer.finish().unwrap().into_inner(); let mut cabinet = cab::Cabinet::new(Cursor::new(cab_file)).unwrap(); { let file_entry = cabinet.get_file_entry("lorem_ipsum.txt").unwrap(); assert_eq!(file_entry.datetime(), Some(datetime)); assert!(file_entry.is_read_only()); assert!(!file_entry.is_hidden()); assert!(file_entry.is_system()); assert!(!file_entry.is_archive()); } let mut output = Vec::new(); let mut file_reader = cabinet.read_file("lorem_ipsum.txt").unwrap(); file_reader.read_to_end(&mut output).unwrap(); assert_eq!(String::from_utf8_lossy(&output), original); } #[test] fn cabinet_with_one_small_mszipped_text_file() { let original = lipsum::lipsum(500); let mut cab_builder = cab::CabinetBuilder::new(); cab_builder .add_folder(cab::CompressionType::MsZip) .add_file("lorem_ipsum.txt"); let mut cab_writer = cab_builder.build(Cursor::new(Vec::new())).unwrap(); while let Some(mut file_writer) = cab_writer.next_file().unwrap() { file_writer.write_all(original.as_bytes()).unwrap(); } let cab_file = cab_writer.finish().unwrap().into_inner(); let mut cabinet = cab::Cabinet::new(Cursor::new(cab_file)).unwrap(); assert_eq!( cabinet.folder_entries().nth(0).unwrap().compression_type(), cab::CompressionType::MsZip ); let mut output = Vec::new(); let mut file_reader = cabinet.read_file("lorem_ipsum.txt").unwrap(); file_reader.read_to_end(&mut output).unwrap(); assert_eq!(String::from_utf8_lossy(&output), original); } #[test] fn cabinet_with_one_big_uncompressed_text_file() { let original = lipsum::lipsum(30000); let mut cab_builder = cab::CabinetBuilder::new(); cab_builder .add_folder(cab::CompressionType::None) .add_file("lorem_ipsum.txt"); let mut cab_writer = cab_builder.build(Cursor::new(Vec::new())).unwrap(); while let Some(mut file_writer) = cab_writer.next_file().unwrap() { file_writer.write_all(original.as_bytes()).unwrap(); } let cab_file = cab_writer.finish().unwrap().into_inner(); assert!(cab_file.len() > original.len()); let mut cabinet = cab::Cabinet::new(Cursor::new(cab_file)).unwrap(); { let folder = cabinet.folder_entries().nth(0).unwrap(); assert_eq!(folder.compression_type(), cab::CompressionType::None); assert!(folder.num_data_blocks() > 1); let file = folder.file_entries().nth(0).unwrap(); assert_eq!(file.uncompressed_size() as usize, original.len()); } let mut output = Vec::new(); let mut file_reader = cabinet.read_file("lorem_ipsum.txt").unwrap(); file_reader.read_to_end(&mut output).unwrap(); assert_eq!(output.len(), original.len()); assert_eq!(String::from_utf8_lossy(&output), original); } #[test] fn cabinet_with_one_big_mszipped_text_file() { let original = lipsum::lipsum(30000); let mut cab_builder = cab::CabinetBuilder::new(); cab_builder .add_folder(cab::CompressionType::MsZip) .add_file("lorem_ipsum.txt"); let mut cab_writer = cab_builder.build(Cursor::new(Vec::new())).unwrap(); while let Some(mut file_writer) = cab_writer.next_file().unwrap() { file_writer.write_all(original.as_bytes()).unwrap(); } let cab_file = cab_writer.finish().unwrap().into_inner(); assert!(cab_file.len() < original.len()); let mut cabinet = cab::Cabinet::new(Cursor::new(cab_file)).unwrap(); { let folder = cabinet.folder_entries().nth(0).unwrap(); assert_eq!(folder.compression_type(), cab::CompressionType::MsZip); let file = folder.file_entries().nth(0).unwrap(); assert_eq!(file.uncompressed_size() as usize, original.len()); } let mut output = Vec::new(); let mut file_reader = cabinet.read_file("lorem_ipsum.txt").unwrap(); file_reader.read_to_end(&mut output).unwrap(); assert_eq!(output.len(), original.len()); assert_eq!(String::from_utf8_lossy(&output), original); } // ========================================================================= // fn random_data_roundtrip(num_bytes: usize, ctype: cab::CompressionType) { use rand::{RngCore, SeedableRng}; let mut original = vec![0; num_bytes]; rand::rngs::SmallRng::from_entropy().fill_bytes(&mut original); let mut cab_builder = cab::CabinetBuilder::new(); cab_builder.add_folder(ctype).add_file("binary"); let mut cab_writer = cab_builder.build(Cursor::new(Vec::new())).unwrap(); while let Some(mut file_writer) = cab_writer.next_file().unwrap() { file_writer.write_all(&original).unwrap(); } let cab_file = cab_writer.finish().unwrap().into_inner(); let mut cabinet = cab::Cabinet::new(Cursor::new(cab_file)).unwrap(); { let folder = cabinet.folder_entries().nth(0).unwrap(); assert_eq!(folder.compression_type(), ctype); assert!((folder.num_data_blocks() as usize) >= (num_bytes / 0x8000)); let file = folder.file_entries().nth(0).unwrap(); assert_eq!(file.name(), "binary"); assert_eq!(file.uncompressed_size() as usize, original.len()); } let mut output = Vec::::new(); let mut file_reader = cabinet.read_file("binary").unwrap(); file_reader.read_to_end(&mut output).unwrap(); assert_eq!(output, original); } #[test] fn cabinet_with_one_small_uncompressed_binary_file() { random_data_roundtrip(10_000, cab::CompressionType::None); } #[test] fn cabinet_with_one_small_mszipped_binary_file() { random_data_roundtrip(10_000, cab::CompressionType::MsZip); } #[test] fn cabinet_with_one_big_uncompressed_binary_file() { random_data_roundtrip(1_000_000, cab::CompressionType::None); } #[test] fn cabinet_with_one_big_mszipped_binary_file() { random_data_roundtrip(1_000_000, cab::CompressionType::MsZip); } // ========================================================================= // cab-0.6.0/tests/seek.rs000064400000000000000000000067131046102023000130220ustar 00000000000000extern crate cab; extern crate lipsum; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; // ========================================================================= // #[test] fn seek_within_big_uncompressed_file() { let original_string = lipsum::lipsum(30000); let original_bytes = original_string.as_bytes(); let mut cab_builder = cab::CabinetBuilder::new(); cab_builder .add_folder(cab::CompressionType::None) .add_file("lorem_ipsum.txt"); let mut cab_writer = cab_builder.build(Cursor::new(Vec::new())).unwrap(); while let Some(mut file_writer) = cab_writer.next_file().unwrap() { file_writer.write_all(original_bytes).unwrap(); } let cab_file = cab_writer.finish().unwrap().into_inner(); assert!(cab_file.len() > original_bytes.len()); let mut cabinet = cab::Cabinet::new(Cursor::new(cab_file)).unwrap(); let mut file_reader = cabinet.read_file("lorem_ipsum.txt").unwrap(); let mut offset: usize = 1000; while offset < original_bytes.len() { let start = file_reader.seek(SeekFrom::End(-(offset as i64))).unwrap(); let mut output = vec![0u8; 1000]; file_reader.read_exact(&mut output).unwrap(); assert_eq!( &output as &[u8], &original_bytes[(start as usize)..][..1000] ); offset += 1000; } } #[test] fn seek_within_big_mszipped_file() { let original_string = lipsum::lipsum(30000); let original_bytes = original_string.as_bytes(); let mut cab_builder = cab::CabinetBuilder::new(); cab_builder .add_folder(cab::CompressionType::MsZip) .add_file("lorem_ipsum.txt"); let mut cab_writer = cab_builder.build(Cursor::new(Vec::new())).unwrap(); while let Some(mut file_writer) = cab_writer.next_file().unwrap() { file_writer.write_all(original_bytes).unwrap(); } let cab_file = cab_writer.finish().unwrap().into_inner(); assert!(cab_file.len() < original_bytes.len()); let mut cabinet = cab::Cabinet::new(Cursor::new(cab_file)).unwrap(); let mut file_reader = cabinet.read_file("lorem_ipsum.txt").unwrap(); let mut offset: usize = 1000; while offset < original_bytes.len() { let start = file_reader.seek(SeekFrom::End(-(offset as i64))).unwrap(); let mut output = vec![0u8; 1000]; file_reader.read_exact(&mut output).unwrap(); assert_eq!( &output as &[u8], &original_bytes[(start as usize)..][..1000] ); offset += 1000; } } // Regression test for https://github.com/mdsteele/rust-cab/issues/15 #[test] fn seek_within_empty_file() { let mut cab_builder = cab::CabinetBuilder::new(); cab_builder.add_folder(cab::CompressionType::None).add_file("empty.txt"); let mut cab_writer = cab_builder.build(Cursor::new(Vec::new())).unwrap(); while let Some(mut file_writer) = cab_writer.next_file().unwrap() { file_writer.write_all(b"").unwrap(); } let cab_file = cab_writer.finish().unwrap().into_inner(); let mut cabinet = cab::Cabinet::new(Cursor::new(cab_file)).unwrap(); for folder in cabinet.folder_entries() { assert_eq!(folder.num_data_blocks(), 0); } let mut file_reader = cabinet.read_file("empty.txt").unwrap(); file_reader.seek(SeekFrom::Start(0)).unwrap(); let mut data = Vec::::new(); file_reader.read_to_end(&mut data).unwrap(); assert!(data.is_empty()); } // ========================================================================= //