diff --git a/.gitignore b/.gitignore index eded420b..05844fbc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,8 @@ target \.idea/ /fuzz_read/out/ /fuzz_write/out/ + +## wasm start ## +pkg/ +extracted/ +## wasm end ## diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..45673969 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "wasynth"] + path = wasynth + url = https://github.com/Rerumu/Wasynth.git diff --git a/Cargo.toml b/Cargo.toml index 173f0a23..14caf7a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,9 @@ edition = "2021" exclude = ["tests/**", "examples/**", ".github/**", "fuzz_read/**", "fuzz_write/**"] build = "src/build.rs" +[lib] +crate-type = ["cdylib"] + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] @@ -26,8 +29,16 @@ rustdoc-args = ["--cfg", "docsrs"] time = { version = "0.3.1", default-features = false } [dependencies] +## wasm dependencies start ## +wasm-bindgen = { version = "0.2" } +js-sys = { version = "0.3" } +getrandom = { version = "0.2", features = ["js"] } +bzip2 = { path = "./bzip2-rs", optional = true, default-features = false, features = ["libbz2-rs-sys"] } +zstd = { version = "0.13", optional = true, default-features = false, features = ["wasm"] } +## wasm dependencies end ## + aes = { version = "0.8", optional = true } -bzip2 = { version = "0.4.3", optional = true } +# bzip2 = { version = "0.4.3", optional = true } chrono = { version = "0.4", optional = true } constant_time_eq = { version = "0.3", optional = true } crc32fast = "1.4" @@ -44,7 +55,7 @@ time = { workspace = true, optional = true, features = [ "std", ] } zeroize = { version = "1.8", optional = true, features = ["zeroize_derive"] } -zstd = { version = "0.13", optional = true, default-features = false } +# zstd = { version = "0.13", optional = true, default-features = false } zopfli = { version = "0.8", optional = true } deflate64 = { version = "0.1.9", optional = true } lzma-rs = { version = "0.3", default-features = false, optional = true } @@ -101,3 +112,4 @@ harness = false [[bench]] name = "merge_archive" harness = false + diff --git a/bzip2-rs b/bzip2-rs new file mode 160000 index 00000000..15258feb --- /dev/null +++ b/bzip2-rs @@ -0,0 +1 @@ +Subproject commit 15258feb0cdc1114d6fc6b936254c4f4e1d5730c diff --git a/src/lib.rs b/src/lib.rs index 9937d72a..d7dbe8d8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,6 +31,7 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![warn(missing_docs)] #![allow(unexpected_cfgs)] // Needed for cfg(fuzzing) on nightly as of 2024-05-06 + pub use crate::compression::{CompressionMethod, SUPPORTED_COMPRESSION_METHODS}; pub use crate::read::HasZipMetadata; pub use crate::read::ZipArchive; @@ -67,3 +68,74 @@ zip = \"="] #[doc = "\"\n\ ```"] pub mod unstable; + +// ====== wasm start ====== // + +use std::io::{Cursor, Read}; + +use result::{ZipResult, ZipError}; + +use wasm_bindgen::{prelude::*, JsValue}; +use js_sys::{Array, Object, Error, Reflect::set}; + +// +// Utilities +// + +impl From for JsValue { + fn from(err: ZipError) -> JsValue { + match err { + ZipError::Io(e) => JsValue::from(Error::new(&format!("IO Error: {}", e))), + ZipError::InvalidArchive(msg) => JsValue::from(Error::new(&format!("Invalid Archive: {}", msg))), + ZipError::UnsupportedArchive(msg) => JsValue::from(Error::new(&format!("Unsupported Archive: {}", msg))), + ZipError::FileNotFound => JsValue::from(Error::new("File Not Found")), + ZipError::InvalidPassword => JsValue::from(Error::new("Invalid Password")), + } + } +} + + +/// Converts a Rust `Vec` to a JS Array. +fn vec_to_js_array>(vec: Vec) -> Array { + let js_array = Array::new(); + for item in vec { + js_array.push(&item.into()); + } + + js_array +} + +// +// Exports +// + +/// Unzips a file provided as a JS array, returning a mapping of filenames to decompressed +/// contents. +#[wasm_bindgen] +pub fn unzip(bytes: &[u8]) -> ZipResult { + // TODO: Accept Uint8Array instead of array + + let cursor = Cursor::new(bytes); + let mut archive = ZipArchive::new(cursor)?; + let extracted_files = Object::new(); + + for i in 0..archive.len() { + let mut file = archive.by_index(i)?; + let file_name = file.name().to_string(); + + // Read the file contents into a Vec + let mut contents = Vec::new(); + file.read_to_end(&mut contents)?; + + // Convert to JS values and insert into object + let js_file_name = JsValue::from(file_name); + let js_contents = vec_to_js_array(contents); + + set(&extracted_files, &js_file_name, &js_contents).unwrap(); + } + + Ok(JsValue::from(&extracted_files)) +} + + +// ====== wasm end ====== // diff --git a/test.ts b/test.ts new file mode 100644 index 00000000..291e425e --- /dev/null +++ b/test.ts @@ -0,0 +1,19 @@ +import * as path from "jsr:@std/path"; +import { ensureDir } from "jsr:@std/fs"; +import { unzip } from "./pkg/zip.js"; + +const EXTRACTED_DIR = "./extracted"; +await ensureDir(EXTRACTED_DIR); + +const TARGET_PATH = Deno.args[0]; +if (!TARGET_PATH) { + console.error("usage: test.js "); + Deno.exit(1); +} + +const zipFile = Deno.readFileSync(TARGET_PATH); +for (const [file, contents] of Object.entries(unzip([...zipFile]))) { + // TODO: handle directories & more in the future + const contentBytes = new TextEncoder().encode(contents); + Deno.writeFileSync(path.join(EXTRACTED_DIR, file), contentBytes); +} diff --git a/wasynth b/wasynth new file mode 160000 index 00000000..27f34987 --- /dev/null +++ b/wasynth @@ -0,0 +1 @@ +Subproject commit 27f34987341ec1837d3ff911f3e583647920af16