mirror of
https://github.com/Drop-OSS/droplet.git
synced 2025-11-10 04:22:16 +10:00
feat: zip file reading
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@ -202,3 +202,6 @@ manifest.json
|
|||||||
|
|
||||||
# JetBrains
|
# JetBrains
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
assets/*
|
||||||
|
!assets/generate.sh
|
||||||
@ -24,7 +24,13 @@ webpki = "0.22.4"
|
|||||||
ring = "0.17.14"
|
ring = "0.17.14"
|
||||||
tokio = { version = "1.45.1", features = ["fs", "io-util"] }
|
tokio = { version = "1.45.1", features = ["fs", "io-util"] }
|
||||||
tokio-util = { version = "0.7.15", features = ["codec"] }
|
tokio-util = { version = "0.7.15", features = ["codec"] }
|
||||||
zip = "4.2.0"
|
rawzip = "0.2.0"
|
||||||
|
|
||||||
|
[package.metadata.patch]
|
||||||
|
crates = ["rawzip"]
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
rawzip = { path="./target/patch/rawzip-0.2.0" }
|
||||||
|
|
||||||
[dependencies.x509-parser]
|
[dependencies.x509-parser]
|
||||||
version = "0.17.0"
|
version = "0.17.0"
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import test from "ava";
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
import droplet from "../index.js";
|
import droplet, { generateManifest } from "../index.js";
|
||||||
|
|
||||||
test("check alt thread util", async (t) => {
|
test("check alt thread util", async (t) => {
|
||||||
let endtime1, endtime2;
|
let endtime1, endtime2;
|
||||||
@ -45,7 +45,6 @@ test("read file", async (t) => {
|
|||||||
fs.rmSync(dirName, { recursive: true });
|
fs.rmSync(dirName, { recursive: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
test("read file offset", async (t) => {
|
test("read file offset", async (t) => {
|
||||||
const dirName = "./.test3";
|
const dirName = "./.test3";
|
||||||
if (fs.existsSync(dirName)) fs.rmSync(dirName, { recursive: true });
|
if (fs.existsSync(dirName)) fs.rmSync(dirName, { recursive: true });
|
||||||
@ -65,6 +64,36 @@ test("read file offset", async (t) => {
|
|||||||
|
|
||||||
const expectedString = testString.slice(1, 4);
|
const expectedString = testString.slice(1, 4);
|
||||||
|
|
||||||
t.assert(finalString == expectedString, "file strings don't match");
|
t.assert(
|
||||||
|
finalString == expectedString,
|
||||||
|
`file strings don't match: ${finalString} vs ${expectedString}`
|
||||||
|
);
|
||||||
fs.rmSync(dirName, { recursive: true });
|
fs.rmSync(dirName, { recursive: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("zip file reader", async (t) => {
|
||||||
|
const manifest = JSON.parse(
|
||||||
|
await new Promise((r, e) =>
|
||||||
|
generateManifest(
|
||||||
|
"./assets/TheGame.zip",
|
||||||
|
(_, __) => {},
|
||||||
|
(_, __) => {},
|
||||||
|
(err, manifest) => (err ? e(err) : r(manifest))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(manifest);
|
||||||
|
|
||||||
|
return t.pass();
|
||||||
|
const stream = droplet.readFile("./assets/TheGame.zip", "TheGame/setup.exe");
|
||||||
|
|
||||||
|
let finalString;
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
console.log(`read chunk ${chunk}`);
|
||||||
|
// Do something with each 'chunk'
|
||||||
|
finalString += String.fromCharCode.apply(null, chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(finalString);
|
||||||
|
});
|
||||||
|
|||||||
3
assets/generate.sh
Executable file
3
assets/generate.sh
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
dd if=/dev/random of=./setup.exe bs=1024 count=1000000
|
||||||
|
zip TheGame.zip setup.exe
|
||||||
|
rm setup.exe
|
||||||
26
patches/rawzip+0.2.0.patch
Normal file
26
patches/rawzip+0.2.0.patch
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
diff --git a/src/archive.rs b/src/archive.rs
|
||||||
|
index 1203015..837c405 100644
|
||||||
|
--- a/src/archive.rs
|
||||||
|
+++ b/src/archive.rs
|
||||||
|
@@ -275,7 +275,7 @@ impl<'data> Iterator for ZipSliceEntries<'data> {
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ZipArchive<R> {
|
||||||
|
- pub(crate) reader: R,
|
||||||
|
+ pub reader: R,
|
||||||
|
pub(crate) comment: ZipString,
|
||||||
|
pub(crate) eocd: EndOfCentralDirectory,
|
||||||
|
}
|
||||||
|
@@ -431,9 +431,9 @@ where
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ZipEntry<'archive, R> {
|
||||||
|
archive: &'archive ZipArchive<R>,
|
||||||
|
- body_offset: u64,
|
||||||
|
- body_end_offset: u64,
|
||||||
|
- entry: ZipArchiveEntryWayfinder,
|
||||||
|
+ pub body_offset: u64,
|
||||||
|
+ pub body_end_offset: u64,
|
||||||
|
+ pub entry: ZipArchiveEntryWayfinder,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'archive, R> ZipEntry<'archive, R>
|
||||||
@ -1,11 +1,18 @@
|
|||||||
|
use core::arch;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::{
|
use std::{
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{self, Read},
|
io::{self, Read},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
pin::Pin,
|
||||||
|
rc::Rc,
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use rawzip::{
|
||||||
|
FileReader, ReaderAt, ZipArchive, ZipArchiveEntryWayfinder, ZipEntry, RECOMMENDED_BUFFER_SIZE,
|
||||||
};
|
};
|
||||||
use zip::{read::ZipFile, ZipArchive};
|
|
||||||
|
|
||||||
use crate::version::{
|
use crate::version::{
|
||||||
types::{MinimumFileObject, Skippable, VersionBackend, VersionFile},
|
types::{MinimumFileObject, Skippable, VersionBackend, VersionFile},
|
||||||
@ -57,51 +64,96 @@ impl VersionBackend for PathVersionBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct ZipVersionBackend {
|
pub struct ZipVersionBackend {
|
||||||
archive: ZipArchive<File>,
|
archive: Arc<ZipArchive<FileReader>>,
|
||||||
}
|
}
|
||||||
impl ZipVersionBackend {
|
impl ZipVersionBackend {
|
||||||
pub fn new(archive: PathBuf) -> Self {
|
pub fn new(archive: File) -> Self {
|
||||||
let handle = File::open(archive).unwrap();
|
let archive = ZipArchive::from_file(archive, &mut [0u8; RECOMMENDED_BUFFER_SIZE]).unwrap();
|
||||||
Self {
|
Self {
|
||||||
archive: ZipArchive::new(handle).unwrap(),
|
archive: Arc::new(archive),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_entry(&self, entry: ZipEntry<'_, FileReader>) -> ZipFileWrapper {
|
||||||
|
ZipFileWrapper {
|
||||||
|
archive: self.archive.clone(),
|
||||||
|
wayfinder: entry.entry,
|
||||||
|
offset: entry.body_offset,
|
||||||
|
end_offset: entry.body_end_offset,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl Drop for ZipVersionBackend {
|
||||||
struct ZipFileWrapper<'a> {
|
fn drop(&mut self) {
|
||||||
inner: ZipFile<'a, File>,
|
println!("dropping archive");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Read for ZipFileWrapper<'_> {
|
struct ZipFileWrapper {
|
||||||
|
pub archive: Arc<ZipArchive<FileReader>>,
|
||||||
|
wayfinder: ZipArchiveEntryWayfinder,
|
||||||
|
offset: u64,
|
||||||
|
end_offset: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Read for ZipFileWrapper {
|
||||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||||
self.inner.read(buf)
|
let read_size = buf.len().min((self.end_offset - self.offset) as usize);
|
||||||
|
let read = self
|
||||||
|
.archive
|
||||||
|
.reader
|
||||||
|
.read_at(&mut buf[..read_size], self.offset)?;
|
||||||
|
self.offset += read as u64;
|
||||||
|
Ok(read)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Skippable for ZipFileWrapper<'_> {
|
impl Skippable for ZipFileWrapper {
|
||||||
fn skip(&mut self, amount: u64) {
|
fn skip(&mut self, amount: u64) {
|
||||||
io::copy(&mut self.inner.by_ref().take(amount), &mut io::sink()).unwrap();
|
/*io::copy(
|
||||||
|
&mut self.inner.reader().by_ref().take(amount),
|
||||||
|
&mut io::sink(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl MinimumFileObject for ZipFileWrapper<'_> {}
|
impl MinimumFileObject for ZipFileWrapper {}
|
||||||
|
|
||||||
impl VersionBackend for ZipVersionBackend {
|
impl VersionBackend for ZipVersionBackend {
|
||||||
fn list_files(&mut self) -> Vec<VersionFile> {
|
fn list_files(&mut self) -> Vec<VersionFile> {
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
for i in 0..self.archive.len() {
|
let read_buffer = &mut [0u8; RECOMMENDED_BUFFER_SIZE];
|
||||||
let entry = self.archive.by_index(i).unwrap();
|
let mut budget_iterator = self.archive.entries(read_buffer);
|
||||||
|
while let Some(entry) = budget_iterator.next_entry().unwrap() {
|
||||||
|
if entry.is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
results.push(VersionFile {
|
results.push(VersionFile {
|
||||||
relative_filename: entry.name().to_owned(),
|
relative_filename: entry.file_safe_path().unwrap().to_string(),
|
||||||
permission: entry.unix_mode().or(Some(0)).unwrap(),
|
permission: 744, // apparently ZIPs with permissions are not supported by this library, so we let the owner do anything
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
results
|
results
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reader(&mut self, file: &VersionFile) -> Option<Box<(dyn MinimumFileObject)>> {
|
fn reader(&mut self, file: &VersionFile) -> Option<Box<(dyn MinimumFileObject)>> {
|
||||||
let file = self.archive.by_name(&file.relative_filename).ok()?;
|
let read_buffer = &mut [0u8; RECOMMENDED_BUFFER_SIZE];
|
||||||
let zip_file_wrapper = ZipFileWrapper { inner: file };
|
let mut entries = self.archive.entries(read_buffer);
|
||||||
|
let entry = loop {
|
||||||
|
if let Some(v) = entries.next_entry().unwrap() {
|
||||||
|
if v.file_safe_path().unwrap().to_string() == file.relative_filename {
|
||||||
|
break Some(v);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break None;
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
|
||||||
//Some(Box::new(zip_file_wrapper))
|
let wayfinder = entry.wayfinder();
|
||||||
None
|
let local_entry = self.archive.get_entry(wayfinder).unwrap();
|
||||||
|
|
||||||
|
let wrapper = self.new_entry(local_entry);
|
||||||
|
|
||||||
|
Some(Box::new(wrapper))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
use std::io::{Read, Seek, SeekFrom};
|
use std::{
|
||||||
|
fmt::Debug, io::{Read, Seek, SeekFrom}
|
||||||
|
};
|
||||||
|
|
||||||
use tokio::io::{self, AsyncRead};
|
use tokio::io::{self, AsyncRead};
|
||||||
|
|
||||||
@ -26,6 +28,7 @@ impl<T: Read + Send + Seek> MinimumFileObject for T {}
|
|||||||
// Intentionally not a generic, because of types in read_file
|
// Intentionally not a generic, because of types in read_file
|
||||||
pub struct ReadToAsyncRead {
|
pub struct ReadToAsyncRead {
|
||||||
pub inner: Box<(dyn Read + Send)>,
|
pub inner: Box<(dyn Read + Send)>,
|
||||||
|
pub backend: Box<(dyn VersionBackend + Send)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsyncRead for ReadToAsyncRead {
|
impl AsyncRead for ReadToAsyncRead {
|
||||||
@ -35,7 +38,8 @@ impl AsyncRead for ReadToAsyncRead {
|
|||||||
buf: &mut tokio::io::ReadBuf<'_>,
|
buf: &mut tokio::io::ReadBuf<'_>,
|
||||||
) -> std::task::Poll<io::Result<()>> {
|
) -> std::task::Poll<io::Result<()>> {
|
||||||
let mut read_buf = [0u8; 8192];
|
let mut read_buf = [0u8; 8192];
|
||||||
let amount = self.inner.read(&mut read_buf).unwrap();
|
let var_name = self.inner.read(&mut read_buf).unwrap();
|
||||||
|
let amount = var_name;
|
||||||
buf.put_slice(&read_buf[0..amount]);
|
buf.put_slice(&read_buf[0..amount]);
|
||||||
std::task::Poll::Ready(Ok(()))
|
std::task::Poll::Ready(Ok(()))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,11 +6,10 @@ use std::{
|
|||||||
|
|
||||||
use napi::{bindgen_prelude::*, tokio_stream::StreamExt};
|
use napi::{bindgen_prelude::*, tokio_stream::StreamExt};
|
||||||
use tokio_util::codec::{BytesCodec, FramedRead};
|
use tokio_util::codec::{BytesCodec, FramedRead};
|
||||||
use zip::ZipArchive;
|
|
||||||
|
|
||||||
use crate::version::{
|
use crate::version::{
|
||||||
backends::{PathVersionBackend, ZipVersionBackend},
|
backends::{PathVersionBackend, ZipVersionBackend},
|
||||||
types::{MinimumFileObject, ReadToAsyncRead, VersionBackend, VersionFile},
|
types::{ReadToAsyncRead, VersionBackend, VersionFile},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn _list_files(vec: &mut Vec<PathBuf>, path: &Path) {
|
pub fn _list_files(vec: &mut Vec<PathBuf>, path: &Path) {
|
||||||
@ -27,7 +26,7 @@ pub fn _list_files(vec: &mut Vec<PathBuf>, path: &Path) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_backend_for_path(path: &Path) -> Option<Box<(dyn VersionBackend + Send)>> {
|
pub fn create_backend_for_path<'a>(path: &Path) -> Option<Box<(dyn VersionBackend + Send + 'a)>> {
|
||||||
let is_directory = path.is_dir();
|
let is_directory = path.is_dir();
|
||||||
if is_directory {
|
if is_directory {
|
||||||
return Some(Box::new(PathVersionBackend {
|
return Some(Box::new(PathVersionBackend {
|
||||||
@ -35,12 +34,9 @@ pub fn create_backend_for_path(path: &Path) -> Option<Box<(dyn VersionBackend +
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
if path.to_string_lossy().ends_with(".zip") {
|
||||||
Insert checks for whatever backend you like
|
let f = File::open(path.to_path_buf()).unwrap();
|
||||||
*/
|
return Some(Box::new(ZipVersionBackend::new(f)));
|
||||||
|
|
||||||
if path.ends_with(".zip") {
|
|
||||||
return Some(Box::new(ZipVersionBackend::new(path.to_path_buf())));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
@ -56,11 +52,12 @@ pub fn has_backend_for_path(path: String) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[napi]
|
#[napi]
|
||||||
pub fn list_files(path: String) -> Vec<String> {
|
pub fn list_files(path: String) -> Result<Vec<String>> {
|
||||||
let path = Path::new(&path);
|
let path = Path::new(&path);
|
||||||
let mut backend = create_backend_for_path(path).unwrap();
|
let mut backend =
|
||||||
|
create_backend_for_path(path).ok_or(napi::Error::from_reason("No backend for path"))?;
|
||||||
let files = backend.list_files();
|
let files = backend.list_files();
|
||||||
files.into_iter().map(|e| e.relative_filename).collect()
|
Ok(files.into_iter().map(|e| e.relative_filename).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[napi]
|
#[napi]
|
||||||
@ -70,7 +67,7 @@ pub fn read_file(
|
|||||||
env: &Env,
|
env: &Env,
|
||||||
start: Option<u32>,
|
start: Option<u32>,
|
||||||
end: Option<u32>,
|
end: Option<u32>,
|
||||||
) -> Option<ReadableStream<'static, BufferSlice<'static>>> {
|
) -> Option<ReadableStream<'_, BufferSlice<'_>>> {
|
||||||
let path = Path::new(&path);
|
let path = Path::new(&path);
|
||||||
let mut backend = create_backend_for_path(path).unwrap();
|
let mut backend = create_backend_for_path(path).unwrap();
|
||||||
let version_file = VersionFile {
|
let version_file = VersionFile {
|
||||||
@ -90,9 +87,10 @@ pub fn read_file(
|
|||||||
let amount = limit - start.or(Some(0)).unwrap();
|
let amount = limit - start.or(Some(0)).unwrap();
|
||||||
ReadToAsyncRead {
|
ReadToAsyncRead {
|
||||||
inner: Box::new(reader.take(amount.into())),
|
inner: Box::new(reader.take(amount.into())),
|
||||||
|
backend
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ReadToAsyncRead { inner: reader }
|
ReadToAsyncRead { inner: reader, backend }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create a FramedRead stream with BytesCodec for chunking
|
// Create a FramedRead stream with BytesCodec for chunking
|
||||||
|
|||||||
Reference in New Issue
Block a user