3 Commits

Author SHA1 Message Date
e219ea13fb fix: file-extension based archive matching 2025-11-21 22:04:34 +11:00
efab43720f fix: 7z and streams 2025-11-20 13:39:05 +11:00
894f2b354a fix: 7z 2025-11-20 11:38:11 +11:00
7 changed files with 115 additions and 90 deletions

View File

@ -67,7 +67,7 @@ test("read file", async (t) => {
let finalString = ""; let finalString = "";
for await (const chunk of stream.getStream()) { for await (const chunk of stream) {
// Do something with each 'chunk' // Do something with each 'chunk'
finalString += String.fromCharCode.apply(null, chunk); finalString += String.fromCharCode.apply(null, chunk);
} }
@ -94,7 +94,7 @@ test("read file offset", async (t) => {
let finalString = ""; let finalString = "";
for await (const chunk of stream.getStream()) { for await (const chunk of stream) {
// Do something with each 'chunk' // Do something with each 'chunk'
finalString += String.fromCharCode.apply(null, chunk); finalString += String.fromCharCode.apply(null, chunk);
} }
@ -121,7 +121,7 @@ test.skip("zip speed test", async (t) => {
const timeThreshold = BigInt(1_000_000_000); const timeThreshold = BigInt(1_000_000_000);
let runningTotal = 0; let runningTotal = 0;
let runningTime = BigInt(0); let runningTime = BigInt(0);
for await (const chunk of stream.getStream()) { for await (const chunk of stream) {
// Do something with each 'chunk' // Do something with each 'chunk'
const currentTime = process.hrtime.bigint(); const currentTime = process.hrtime.bigint();
const timeDiff = currentTime - lastTime; const timeDiff = currentTime - lastTime;
@ -146,13 +146,17 @@ test.skip("zip speed test", async (t) => {
t.pass(); t.pass();
}); });
test.skip("zip manifest test", async (t) => { test("zip manifest test", async (t) => {
const zipFiles = fs.readdirSync("./assets").filter((v) => v.endsWith(".zip"));
const dropletHandler = new DropletHandler(); const dropletHandler = new DropletHandler();
for (const zipFile of zipFiles) {
console.log("generating manifest for " + zipFile);
const manifest = JSON.parse( const manifest = JSON.parse(
await new Promise((r, e) => await new Promise((r, e) =>
generateManifest( generateManifest(
dropletHandler, dropletHandler,
"./assets/TheGame.zip", "./assets/" + zipFile,
(_, __) => {}, (_, __) => {},
(_, __) => {}, (_, __) => {},
(err, manifest) => (err ? e(err) : r(manifest)) (err, manifest) => (err ? e(err) : r(manifest))
@ -166,12 +170,13 @@ test.skip("zip manifest test", async (t) => {
const hash = createHash("md5"); const hash = createHash("md5");
const stream = ( const stream = (
await dropletHandler.readFile( await dropletHandler.readFile(
"./assets/TheGame.zip", "./assets/" + zipFile,
filename, filename,
BigInt(start), BigInt(start),
BigInt(start + length) BigInt(start + length)
) )
).getStream(); );
console.log(stream);
let streamLength = 0; let streamLength = 0;
await stream.pipeTo( await stream.pipeTo(
@ -197,6 +202,7 @@ test.skip("zip manifest test", async (t) => {
start += length; start += length;
} }
} }
}
t.pass(); t.pass();
}); });

View File

@ -1,4 +1,4 @@
# yes "droplet is awesome" | dd of=./setup.exe bs=1024 count=1000000 # yes "droplet is awesome" | dd of=./setup.exe bs=1024 count=1000000
dd if=/dev/random of=./setup.exe bs=1024 count=1000000 dd if=/dev/random of=./setup.exe bs=1024 count=1000000
zip TheGame.zip setup.exe zip TheGame.zip setup.exe "test file.txt"
rm setup.exe rm setup.exe

6
index.d.ts vendored
View File

@ -8,11 +8,7 @@ export declare class DropletHandler {
hasBackendForPath(path: string): boolean hasBackendForPath(path: string): boolean
listFiles(path: string): Array<string> listFiles(path: string): Array<string>
peekFile(path: string, subPath: string): bigint peekFile(path: string, subPath: string): bigint
readFile(path: string, subPath: string, start?: bigint | undefined | null, end?: bigint | undefined | null): JsDropStreamable readFile(path: string, subPath: string, start?: bigint | undefined | null, end?: bigint | undefined | null): ReadableStream
}
export declare class JsDropStreamable {
getStream(): any
} }
export declare class Script { export declare class Script {

View File

@ -377,7 +377,6 @@ if (!nativeBinding) {
module.exports = nativeBinding module.exports = nativeBinding
module.exports.DropletHandler = nativeBinding.DropletHandler module.exports.DropletHandler = nativeBinding.DropletHandler
module.exports.JsDropStreamable = nativeBinding.JsDropStreamable
module.exports.Script = nativeBinding.Script module.exports.Script = nativeBinding.Script
module.exports.ScriptEngine = nativeBinding.ScriptEngine module.exports.ScriptEngine = nativeBinding.ScriptEngine
module.exports.callAltThreadFunc = nativeBinding.callAltThreadFunc module.exports.callAltThreadFunc = nativeBinding.callAltThreadFunc

View File

@ -1,6 +1,6 @@
{ {
"name": "@drop-oss/droplet", "name": "@drop-oss/droplet",
"version": "3.2.2", "version": "3.5.0",
"main": "index.js", "main": "index.js",
"types": "index.d.ts", "types": "index.d.ts",
"napi": { "napi": {

View File

@ -111,6 +111,17 @@ impl VersionBackend for PathVersionBackend {
pub static SEVEN_ZIP_INSTALLED: LazyLock<bool> = pub static SEVEN_ZIP_INSTALLED: LazyLock<bool> =
LazyLock::new(|| Command::new("7z").output().is_ok()); LazyLock::new(|| Command::new("7z").output().is_ok());
// https://7-zip.opensource.jp/chm/general/formats.htm
// Intentionally repeated some because it's a trivial cost and it's easier to directly copy from the docs above
pub const SUPPORTED_FILE_EXTENSIONS: [&str; 89] = [
"7z", "bz2", "bzip2", "tbz2", "tbz", "gz", "gzip", "tgz", "tar", "wim", "swm", "esd", "xz",
"txz", "zip", "zipx", "jar", "xpi", "odt", "ods", "docx", "xlsx", "epub", "apm", "ar", "a",
"deb", "lib", "arj", "cab", "chm", "chw", "chi", "chq", "msi", "msp", "doc", "xls", "ppt",
"cpio", "cramfs", "dmg", "ext", "ext2", "ext3", "ext4", "img", "fat", "img", "hfs", "hfsx",
"hxs", "hxr", "hxq", "hxw", "lit", "ihex", "iso", "img", "lzh", "lha", "lzma", "mbr", "mslz",
"mub", "nsis", "ntfs", "img", "mbr", "rar", "r00", "rpm", "ppmd", "qcow", "qcow2", "qcow2c",
"squashfs", "udf", "iso", "img", "scap", "uefif", "vdi", "vhd", "vmdk", "xar", "pkg", "z", "taz",
];
#[derive(Clone)] #[derive(Clone)]
pub struct ZipVersionBackend { pub struct ZipVersionBackend {
@ -126,12 +137,15 @@ impl ZipVersionBackend {
pub struct ZipFileWrapper { pub struct ZipFileWrapper {
command: Child, command: Child,
reader: BufReader<ChildStdout> reader: BufReader<ChildStdout>,
} }
impl ZipFileWrapper { impl ZipFileWrapper {
pub fn new(mut command: Child) -> Self { pub fn new(mut command: Child) -> Self {
let stdout = command.stdout.take().expect("failed to access stdout of 7z"); let stdout = command
.stdout
.take()
.expect("failed to access stdout of 7z");
let reader = BufReader::new(stdout); let reader = BufReader::new(stdout);
ZipFileWrapper { command, reader } ZipFileWrapper { command, reader }
} }
@ -165,24 +179,33 @@ impl VersionBackend for ZipVersionBackend {
)); ));
} }
let raw_result = String::from_utf8(result.stdout)?; let raw_result = String::from_utf8(result.stdout)?;
let files = raw_result.split("\n").filter(|v| v.len() > 0).map(|v| v.split(" ").filter(|v| v.len() > 0)); let files = raw_result
.split("\n")
.filter(|v| v.len() > 0)
.map(|v| v.split(" ").filter(|v| v.len() > 0));
let mut results = Vec::new(); let mut results = Vec::new();
for file in files { for file in files {
let mut values = file.collect::<Vec<&str>>(); let values = file.collect::<Vec<&str>>();
values.reverse();
let mut iter = values.iter(); let mut iter = values.iter();
let (name, compress, size, attrs) = ( let (date, time, attrs, size, compress, name) = (
iter.next().expect("failed to fetch name"), iter.next().expect("failed to read date"),
iter.next().expect("failed to read compressed size"), iter.next().expect("failed to read time"),
iter.next().expect("failed to read file size"), iter.next().expect("failed to read attrs"),
iter.next().expect("failed to fetch attrs") iter.next().expect("failed to read size"),
iter.next().expect("failed to read compress"),
iter.collect::<Vec<&&str>>(),
); );
if attrs.starts_with("D") { if attrs.starts_with("D") {
continue; continue;
} }
results.push(VersionFile { results.push(VersionFile {
relative_filename: name.to_owned().to_owned(), relative_filename: name
.into_iter()
.map(|v| *v)
.fold(String::new(), |a, b| a + b + " ")
.trim_end()
.to_owned(),
permission: 0o744, // owner r/w/x, everyone else, read permission: 0o744, // owner r/w/x, everyone else, read
size: size.parse().unwrap(), size: size.parse().unwrap(),
}); });
@ -199,7 +222,10 @@ impl VersionBackend for ZipVersionBackend {
) -> anyhow::Result<Box<dyn MinimumFileObject + '_>> { ) -> anyhow::Result<Box<dyn MinimumFileObject + '_>> {
let mut read_command = Command::new("7z"); let mut read_command = Command::new("7z");
read_command.args(vec!["e", "-so", &self.path, &file.relative_filename]); read_command.args(vec!["e", "-so", &self.path, &file.relative_filename]);
let output = read_command.stdout(Stdio::piped()).spawn().expect("failed to spawn 7z"); let output = read_command
.stdout(Stdio::piped())
.spawn()
.expect("failed to spawn 7z");
Ok(Box::new(ZipFileWrapper::new(output))) Ok(Box::new(ZipFileWrapper::new(output)))
} }

View File

@ -10,7 +10,9 @@ use napi::{bindgen_prelude::*, sys::napi_value__, tokio_stream::StreamExt};
use tokio_util::codec::{BytesCodec, FramedRead}; use tokio_util::codec::{BytesCodec, FramedRead};
use crate::version::{ use crate::version::{
backends::{PathVersionBackend, ZipVersionBackend, SEVEN_ZIP_INSTALLED}, backends::{
PathVersionBackend, ZipVersionBackend, SEVEN_ZIP_INSTALLED, SUPPORTED_FILE_EXTENSIONS,
},
types::{ReadToAsyncRead, VersionBackend, VersionFile}, types::{ReadToAsyncRead, VersionBackend, VersionFile},
}; };
@ -33,14 +35,26 @@ pub fn create_backend_constructor<'a>(
}; };
if *SEVEN_ZIP_INSTALLED { if *SEVEN_ZIP_INSTALLED {
/*
Slow 7zip integrity test
let mut test = Command::new("7z"); let mut test = Command::new("7z");
test.args(vec!["t", path.to_str().expect("invalid utf path")]); test.args(vec!["t", path.to_str().expect("invalid utf path")]);
let status = test.status().ok()?; let status = test.status().ok()?;
if status.code().unwrap_or(1) == 0 { if status.code().unwrap_or(1) == 0 {
let buf = path.to_path_buf(); let buf = path.to_path_buf();
return Some(Box::new(move || { return Some(Box::new(move || Ok(Box::new(ZipVersionBackend::new(buf)?))));
Ok(Box::new(ZipVersionBackend::new(buf)?)) }
})); */
// Fast filename-based test
if let Some(extension) = path.extension().and_then(|v| v.to_str()) {
let supported = SUPPORTED_FILE_EXTENSIONS
.iter()
.find(|v| ***v == *extension)
.is_some();
if supported {
let buf = path.to_path_buf();
return Some(Box::new(move || Ok(Box::new(ZipVersionBackend::new(buf)?))));
}
} }
} }
@ -111,7 +125,7 @@ impl<'a> DropletHandler<'a> {
Ok(file.size) Ok(file.size)
} }
#[napi] #[napi(ts_return_type = "ReadableStream")]
pub fn read_file( pub fn read_file(
&mut self, &mut self,
reference: Reference<DropletHandler<'static>>, reference: Reference<DropletHandler<'static>>,
@ -120,7 +134,7 @@ impl<'a> DropletHandler<'a> {
env: Env, env: Env,
start: Option<BigInt>, start: Option<BigInt>,
end: Option<BigInt>, end: Option<BigInt>,
) -> anyhow::Result<JsDropStreamable> { ) -> anyhow::Result<*mut napi_value__> {
let stream = reference.share_with(env, |handler| { let stream = reference.share_with(env, |handler| {
let backend = handler let backend = handler
.create_backend_for_path(path) .create_backend_for_path(path)
@ -149,25 +163,9 @@ impl<'a> DropletHandler<'a> {
// Apply Result::map_err to transform Err(std::io::Error) to Err(napi::Error) // Apply Result::map_err to transform Err(std::io::Error) to Err(napi::Error)
.map_err(napi::Error::from) // napi::Error implements From<tokio::io::Error> .map_err(napi::Error::from) // napi::Error implements From<tokio::io::Error>
}); });
// Create the napi-rs ReadableStream from the tokio_stream::Stream
// The unwrap() here means if stream creation fails, it will panic.
// For a production system, consider returning Result<Option<...>> and handling this.
ReadableStream::create_with_stream_bytes(&env, stream) ReadableStream::create_with_stream_bytes(&env, stream)
})?; })?;
Ok(JsDropStreamable { inner: stream }) Ok(stream.raw())
}
}
#[napi]
pub struct JsDropStreamable {
inner: SharedReference<DropletHandler<'static>, ReadableStream<'static, BufferSlice<'static>>>,
}
#[napi]
impl JsDropStreamable {
#[napi]
pub fn get_stream(&self) -> *mut napi_value__ {
self.inner.raw()
} }
} }