roxify 1.13.4 → 1.13.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +2 -1
- package/README.md +30 -20
- package/dist/cli.js +36 -194
- package/dist/rox-macos-universal +0 -0
- package/dist/roxify_native +0 -0
- package/dist/roxify_native-macos-arm64 +0 -0
- package/dist/roxify_native-macos-x64 +0 -0
- package/dist/roxify_native.exe +0 -0
- package/dist/utils/decoder.d.ts +1 -21
- package/dist/utils/decoder.js +42 -1241
- package/dist/utils/encoder.d.ts +1 -13
- package/dist/utils/encoder.js +36 -559
- package/dist/utils/rust-cli-wrapper.d.ts +2 -2
- package/dist/utils/rust-cli-wrapper.js +8 -2
- package/native/encoder.rs +10 -22
- package/native/io_advice.rs +43 -0
- package/native/io_ntfs_optimized.rs +99 -0
- package/native/lib.rs +2 -0
- package/native/main.rs +329 -57
- package/native/packer.rs +188 -82
- package/native/png_chunk_writer.rs +146 -0
- package/native/png_utils.rs +70 -54
- package/native/streaming.rs +16 -39
- package/native/streaming_decode.rs +263 -110
- package/native/streaming_encode.rs +36 -59
- package/package.json +1 -1
- package/roxify_native-aarch64-apple-darwin.node +0 -0
- package/roxify_native-aarch64-pc-windows-msvc.node +0 -0
- package/roxify_native-aarch64-unknown-linux-gnu.node +0 -0
- package/roxify_native-i686-pc-windows-msvc.node +0 -0
- package/roxify_native-i686-unknown-linux-gnu.node +0 -0
- package/roxify_native-x86_64-apple-darwin.node +0 -0
- package/roxify_native-x86_64-pc-windows-msvc.node +0 -0
- package/roxify_native-x86_64-unknown-linux-gnu.node +0 -0
|
@@ -233,7 +233,7 @@ function spawnRustCLI(args, options) {
|
|
|
233
233
|
runSpawn(cliPath);
|
|
234
234
|
});
|
|
235
235
|
}
|
|
236
|
-
export async function encodeWithRustCLI(inputPath, outputPath, compressionLevel = 3, passphrase, encryptType = 'aes', name, onProgress) {
|
|
236
|
+
export async function encodeWithRustCLI(inputPath, outputPath, compressionLevel = 3, passphrase, encryptType = 'aes', name, ramBudgetMb, onProgress) {
|
|
237
237
|
const cliPath = findRustBinary();
|
|
238
238
|
if (!cliPath)
|
|
239
239
|
throw new Error('Rust CLI binary not found');
|
|
@@ -250,10 +250,13 @@ export async function encodeWithRustCLI(inputPath, outputPath, compressionLevel
|
|
|
250
250
|
args.push('--passphrase', passphrase);
|
|
251
251
|
args.push('--encrypt', encryptType);
|
|
252
252
|
}
|
|
253
|
+
if (typeof ramBudgetMb === 'number' && Number.isFinite(ramBudgetMb)) {
|
|
254
|
+
args.push('--ram-budget-mb', String(Math.max(1, Math.floor(ramBudgetMb))));
|
|
255
|
+
}
|
|
253
256
|
args.push(inputPath, outputPath);
|
|
254
257
|
await spawnRustCLI(args, { onProgress });
|
|
255
258
|
}
|
|
256
|
-
export async function decodeWithRustCLI(inputPath, outputPath, passphrase, files, dict, onProgress) {
|
|
259
|
+
export async function decodeWithRustCLI(inputPath, outputPath, passphrase, files, dict, ramBudgetMb, onProgress) {
|
|
257
260
|
const args = ['decompress', inputPath, outputPath];
|
|
258
261
|
if (passphrase)
|
|
259
262
|
args.push('--passphrase', passphrase);
|
|
@@ -261,6 +264,9 @@ export async function decodeWithRustCLI(inputPath, outputPath, passphrase, files
|
|
|
261
264
|
args.push('--files', JSON.stringify(files));
|
|
262
265
|
if (dict)
|
|
263
266
|
args.push('--dict', dict);
|
|
267
|
+
if (typeof ramBudgetMb === 'number' && Number.isFinite(ramBudgetMb)) {
|
|
268
|
+
args.push('--ram-budget-mb', String(Math.max(1, Math.floor(ramBudgetMb))));
|
|
269
|
+
}
|
|
264
270
|
await spawnRustCLI(args, { onProgress });
|
|
265
271
|
}
|
|
266
272
|
export async function listWithRustCLI(inputPath) {
|
package/native/encoder.rs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
use anyhow::Result;
|
|
2
2
|
|
|
3
|
+
use crate::png_chunk_writer::{write_chunked_idat_bytes, write_png_chunk};
|
|
4
|
+
|
|
3
5
|
const MAGIC: &[u8] = b"ROX1";
|
|
4
6
|
const PIXEL_MAGIC: &[u8] = b"PXL1";
|
|
5
7
|
const PNG_HEADER: &[u8] = &[137, 80, 78, 71, 13, 10, 26, 10];
|
|
8
|
+
const HEADER_VERSION_V2: u8 = 2;
|
|
6
9
|
|
|
7
10
|
const MARKER_START: [(u8, u8, u8); 3] = [(255, 0, 0), (0, 255, 0), (0, 0, 255)];
|
|
8
11
|
const MARKER_END: [(u8, u8, u8); 3] = [(0, 0, 255), (0, 255, 0), (255, 0, 0)];
|
|
@@ -204,12 +207,12 @@ fn build_flat_pixel_buffer(
|
|
|
204
207
|
}
|
|
205
208
|
|
|
206
209
|
fn build_meta_pixel_with_name_and_filelist(payload: &[u8], name: Option<&str>, file_list: Option<&str>) -> Result<Vec<u8>> {
|
|
207
|
-
let version =
|
|
210
|
+
let version = HEADER_VERSION_V2;
|
|
208
211
|
let name_bytes = name.map(|n| n.as_bytes()).unwrap_or(&[]);
|
|
209
212
|
let name_len = name_bytes.len().min(255) as u8;
|
|
210
|
-
let payload_len_bytes = (payload.len() as
|
|
213
|
+
let payload_len_bytes = (payload.len() as u64).to_be_bytes();
|
|
211
214
|
|
|
212
|
-
let mut result = Vec::with_capacity(1 + 1 + name_len as usize +
|
|
215
|
+
let mut result = Vec::with_capacity(1 + 1 + name_len as usize + 8 + payload.len() + 256);
|
|
213
216
|
result.push(version);
|
|
214
217
|
result.push(name_len);
|
|
215
218
|
|
|
@@ -245,33 +248,18 @@ fn build_png(width: usize, height: usize, idat_data: &[u8], file_list: Option<&s
|
|
|
245
248
|
ihdr_data[11] = 0;
|
|
246
249
|
ihdr_data[12] = 0;
|
|
247
250
|
|
|
248
|
-
|
|
249
|
-
|
|
251
|
+
write_png_chunk(&mut png, b"IHDR", &ihdr_data)?;
|
|
252
|
+
write_chunked_idat_bytes(&mut png, idat_data)?;
|
|
250
253
|
|
|
251
254
|
if let Some(file_list_json) = file_list {
|
|
252
|
-
|
|
255
|
+
write_png_chunk(&mut png, b"rXFL", file_list_json.as_bytes())?;
|
|
253
256
|
}
|
|
254
257
|
|
|
255
|
-
|
|
258
|
+
write_png_chunk(&mut png, b"IEND", &[])?;
|
|
256
259
|
|
|
257
260
|
Ok(png)
|
|
258
261
|
}
|
|
259
262
|
|
|
260
|
-
fn write_chunk(out: &mut Vec<u8>, chunk_type: &[u8; 4], data: &[u8]) -> Result<()> {
|
|
261
|
-
let len = data.len() as u32;
|
|
262
|
-
out.extend_from_slice(&len.to_be_bytes());
|
|
263
|
-
out.extend_from_slice(chunk_type);
|
|
264
|
-
out.extend_from_slice(data);
|
|
265
|
-
|
|
266
|
-
let mut hasher = crc32fast::Hasher::new();
|
|
267
|
-
hasher.update(chunk_type);
|
|
268
|
-
hasher.update(data);
|
|
269
|
-
let crc = hasher.finalize();
|
|
270
|
-
|
|
271
|
-
out.extend_from_slice(&crc.to_be_bytes());
|
|
272
|
-
Ok(())
|
|
273
|
-
}
|
|
274
|
-
|
|
275
263
|
fn create_raw_deflate_from_rows(flat: &[u8], row_bytes: usize, height: usize) -> Vec<u8> {
|
|
276
264
|
let stride = row_bytes + 1;
|
|
277
265
|
let scanlines_total = height * stride;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
use std::fs::File;
|
|
2
|
+
#[cfg(target_os = "linux")]
|
|
3
|
+
use std::os::fd::AsRawFd;
|
|
4
|
+
|
|
5
|
+
pub const INPUT_DROP_GRANULARITY: u64 = 8 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
pub fn advise_file_sequential(file: &File) {
|
|
8
|
+
#[cfg(target_os = "linux")]
|
|
9
|
+
unsafe {
|
|
10
|
+
let _ = libc::posix_fadvise(file.as_raw_fd(), 0, 0, libc::POSIX_FADV_SEQUENTIAL);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#[cfg(not(target_os = "linux"))]
|
|
14
|
+
let _ = file;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
pub fn advise_drop(file: &File, offset: u64, len: u64) {
|
|
18
|
+
if len == 0 {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#[cfg(target_os = "linux")]
|
|
23
|
+
unsafe {
|
|
24
|
+
let _ = libc::posix_fadvise(
|
|
25
|
+
file.as_raw_fd(),
|
|
26
|
+
offset as libc::off_t,
|
|
27
|
+
len as libc::off_t,
|
|
28
|
+
libc::POSIX_FADV_DONTNEED,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[cfg(not(target_os = "linux"))]
|
|
33
|
+
let _ = (file, offset, len);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pub fn sync_and_drop(file: &File, len: u64) {
|
|
37
|
+
if len < INPUT_DROP_GRANULARITY {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let _ = file.sync_data();
|
|
42
|
+
advise_drop(file, 0, len);
|
|
43
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
use std::fs::{File, OpenOptions};
|
|
2
|
+
use std::io::{BufWriter, Write};
|
|
3
|
+
use std::path::Path;
|
|
4
|
+
|
|
5
|
+
/// Buffer size optimized for NTFS (larger = fewer syscalls)
|
|
6
|
+
#[cfg(windows)]
|
|
7
|
+
const NTFS_WRITE_BUFFER: usize = 4 * 1024 * 1024; // 4MB for Windows/NTFS
|
|
8
|
+
#[cfg(not(windows))]
|
|
9
|
+
const NTFS_WRITE_BUFFER: usize = 64 * 1024; // 64KB for Unix
|
|
10
|
+
|
|
11
|
+
/// Optimized file writer with large buffer for NTFS
|
|
12
|
+
pub struct OptimizedFileWriter {
|
|
13
|
+
writer: BufWriter<File>,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
impl OptimizedFileWriter {
|
|
17
|
+
pub fn create(path: &Path) -> std::io::Result<Self> {
|
|
18
|
+
let file = OpenOptions::new()
|
|
19
|
+
.write(true)
|
|
20
|
+
.create(true)
|
|
21
|
+
.truncate(true)
|
|
22
|
+
.open(path)?;
|
|
23
|
+
|
|
24
|
+
Ok(Self {
|
|
25
|
+
writer: BufWriter::with_capacity(NTFS_WRITE_BUFFER, file),
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
pub fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
|
|
30
|
+
self.writer.write_all(buf)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pub fn flush(&mut self) -> std::io::Result<()> {
|
|
34
|
+
self.writer.flush()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Write file with optimized buffering for target filesystem
|
|
39
|
+
pub fn write_file_optimized(path: &Path, content: &[u8]) -> std::io::Result<()> {
|
|
40
|
+
let mut writer = OptimizedFileWriter::create(path)?;
|
|
41
|
+
writer.write_all(content)?;
|
|
42
|
+
writer.flush()?;
|
|
43
|
+
Ok(())
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Batch write multiple files - keeps files open for better NTFS performance
|
|
47
|
+
pub fn write_files_batch(
|
|
48
|
+
base_dir: &Path,
|
|
49
|
+
files: &[(String, &[u8])],
|
|
50
|
+
) -> Result<Vec<String>, String> {
|
|
51
|
+
let mut written = Vec::with_capacity(files.len());
|
|
52
|
+
|
|
53
|
+
for (rel_path, content) in files {
|
|
54
|
+
let safe_path = sanitize_path(rel_path);
|
|
55
|
+
let dest = base_dir.join(&safe_path);
|
|
56
|
+
|
|
57
|
+
if let Some(parent) = dest.parent() {
|
|
58
|
+
std::fs::create_dir_all(parent)
|
|
59
|
+
.map_err(|e| format!("Cannot create parent dir {:?}: {}", parent, e))?;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
write_file_optimized(&dest, content)
|
|
63
|
+
.map_err(|e| format!("Cannot write {:?}: {}", dest, e))?;
|
|
64
|
+
|
|
65
|
+
written.push(safe_path.to_string_lossy().to_string());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
Ok(written)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fn sanitize_path(path: &str) -> std::path::PathBuf {
|
|
72
|
+
let mut safe = std::path::PathBuf::new();
|
|
73
|
+
for comp in std::path::Path::new(path).components() {
|
|
74
|
+
if let std::path::Component::Normal(osstr) = comp {
|
|
75
|
+
safe.push(osstr);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
safe
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Pre-allocate file space on NTFS to reduce fragmentation
|
|
82
|
+
#[cfg(windows)]
|
|
83
|
+
pub fn preallocate_file(path: &Path, size: u64) -> std::io::Result<()> {
|
|
84
|
+
use std::os::windows::fs::FileExt;
|
|
85
|
+
|
|
86
|
+
let file = OpenOptions::new()
|
|
87
|
+
.write(true)
|
|
88
|
+
.create(true)
|
|
89
|
+
.open(path)?;
|
|
90
|
+
|
|
91
|
+
// Pre-allocate space
|
|
92
|
+
file.set_len(size)?;
|
|
93
|
+
Ok(())
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#[cfg(not(windows))]
|
|
97
|
+
pub fn preallocate_file(_path: &Path, _size: u64) -> std::io::Result<()> {
|
|
98
|
+
Ok(())
|
|
99
|
+
}
|