Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion benchmark/bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import { Bench, hrtimeNow } from 'tinybench'
import { compress as legacyCompress, uncompress as legacyUncompress } from 'legacy-snappy'

import { compress, uncompress, compressSync } from '../index.js'
import { compress, uncompress, compressSync, uncompressSync } from '../index.js'
import { fileURLToPath } from 'node:url'

const gzipAsync = promisify(gzip)
Expand All @@ -42,6 +42,10 @@ b.add('snappy-compress', () => {
return compress(FIXTURE)
})

b.add('snappy-compress-sync', () => {
return compressSync(FIXTURE)
})

b.add('snappy-v6-compress', () => {
return compressV6(FIXTURE)
})
Expand All @@ -65,10 +69,22 @@ console.table(b.table())
const bUncompress = new Bench({
now: hrtimeNow,
})
const output = new Uint8Array(FIXTURE.length)

bUncompress.add('snappy-uncompress', () => {
return uncompress(SNAPPY_COMPRESSED_FIXTURE)
})
bUncompress.add('snappy-alloc-uncompress', () => {
return uncompress(SNAPPY_COMPRESSED_FIXTURE, { output: output })
})
bUncompress.add('snappy-sync-uncompress', () => {
return uncompressSync(SNAPPY_COMPRESSED_FIXTURE)
})
const output2 = new Uint8Array(FIXTURE.length)

bUncompress.add('snappy-sync-alloc-uncompress', () => {
Comment thread
Brooooooklyn marked this conversation as resolved.
Outdated
return uncompressSync(SNAPPY_COMPRESSED_FIXTURE, { output: output2 })
})

bUncompress.add('snappy-v6-uncompress', () => {
// @ts-expect-error
Expand Down
15 changes: 13 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface DecOptions {
* see https://www.electronjs.org/blog/v8-memory-cage and https://github.com/electron/electron/issues/35801#issuecomment-1261206333
*/
copyOutputData?: boolean
output?: Uint8Array
}

export interface EncOptions {
Expand All @@ -31,6 +32,16 @@ export interface EncOptions {
copyOutputData?: boolean
}

export declare function uncompress(input: string | Uint8Array, options?: DecOptions | undefined | null, signal?: AbortSignal | undefined | null): Promise<string | Buffer>
export declare function uncompress(input: string | Uint8Array, options?: DecOptions | undefined | null, signal?: AbortSignal | undefined | null): Promise<Uint8Array>
export declare function uncompress(input: string | Uint8Array, options: { asBuffer: false }): Promise<string>;
export declare function uncompress(input: string | Uint8Array, options: { output: Uint8Array }): Promise<number>;
export declare function uncompress(input: string | Uint8Array, options?: { asBuffer?: true }): Promise<Uint8Array>;
export declare function uncompress(input: string | Uint8Array, options?: DecOptions): Promise<string | Uint8Array | number>;


export declare function uncompressSync(input: undefined, options?: DecOptions | undefined | null): Buffer
export declare function uncompressSync(input: string | Uint8Array, options: { asBuffer: false }): string;
export declare function uncompressSync(input: string | Uint8Array, options: { output: Uint8Array }): number;
export declare function uncompressSync(input: string | Uint8Array, options?: { asBuffer?: true }): Buffer;
export declare function uncompressSync(input: string | Uint8Array, options?: DecOptions): string | Buffer | number;

export declare function uncompressSync(input: string | Uint8Array, options?: DecOptions | undefined | null): string | Buffer
112 changes: 77 additions & 35 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub struct DecOptions {
/// for compatibility with electron >= 21 \n
/// see https://www.electronjs.org/blog/v8-memory-cage and https://github.com/electron/electron/issues/35801#issuecomment-1261206333
pub copy_output_data: Option<bool>,
pub output: Option<Uint8Array>,
}

#[napi(object)]
Expand Down Expand Up @@ -74,31 +75,49 @@ pub struct Dec {

#[napi]
impl<'env> ScopedTask<'env> for Dec {
type Output = Vec<u8>;
type JsValue = Either<String, BufferSlice<'env>>;
type Output = Either<Vec<u8>, u32>;
type JsValue = Either3<String, BufferSlice<'env>, u32>;

fn compute(&mut self) -> Result<Self::Output> {
self
let input_data = match &self.data {
Either::A(ref s) => s.as_bytes(),
Either::B(b) => b.as_ref(),
};

if let Some(ref mut opts) = self.options {
if let Some(ref mut output_buffer) = opts.output {
let decompressed_len = self
.inner

Copilot AI Aug 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of unsafe to get a mutable reference to the output buffer is potentially dangerous. Consider using safe alternatives or adding proper safety documentation explaining why this unsafe block is necessary and what invariants must be maintained.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Brooooooklyn a question from me - I see no other way than using the unsafe in this small part. Would adding:

          // SAFETY: We know the buffer is valid for the lifetime of this function
          // and we're not extending beyond its bounds

comment be sufficient enough? I might be missing something, as Im pretty new to Rust.

.decompress(input_data, unsafe { output_buffer.as_mut() })
.map_err(|err| Error::new(Status::GenericFailure, format!("{err}")))?;

return Ok(Either::B(decompressed_len as u32));
}
}
return self
.inner
.decompress_vec(match self.data {
Either::A(ref s) => s.as_bytes(),
Either::B(ref b) => b.as_ref(),
})
.map_err(|e| Error::new(Status::GenericFailure, format!("{e}")))
.decompress_vec(input_data)
.map(|out| Either::A(out))
.map_err(|e| Error::new(Status::GenericFailure, format!("{e}")));
}

fn resolve(&mut self, env: &'env Env, output: Self::Output) -> Result<Self::JsValue> {
let opt_ref = self.options.as_ref();
if opt_ref.and_then(|o| o.as_buffer).unwrap_or(true) {
if opt_ref.and_then(|o| o.copy_output_data).unwrap_or(false) {
BufferSlice::copy_from(env, output).map(Either::B)
} else {
BufferSlice::from_data(env, output).map(Either::B)
match output {
Either::B(length) => return Ok(Either3::C(length)),
Either::A(output) => {
let opt_ref = self.options.as_ref();
if opt_ref.and_then(|o| o.as_buffer).unwrap_or(true) {
if opt_ref.and_then(|o| o.copy_output_data).unwrap_or(false) {
BufferSlice::copy_from(env, output).map(Either3::B)
} else {
BufferSlice::from_data(env, output).map(Either3::B)
}
} else {
Ok(Either3::A(String::from_utf8(output).map_err(|e| {
Error::new(Status::GenericFailure, format!("{e}"))
})?))
}
}
} else {
Ok(Either::A(String::from_utf8(output).map_err(|e| {
Error::new(Status::GenericFailure, format!("{e}"))
})?))
}
}
}
Expand Down Expand Up @@ -144,39 +163,62 @@ pub fn compress(
AsyncTask::with_optional_signal(encoder, signal)
}

#[napi]
#[napi(ts_return_type = r#"Buffer
export declare function uncompressSync(input: string | Uint8Array, options: { asBuffer: false }): string;
export declare function uncompressSync(input: string | Uint8Array, options: { output: Uint8Array }): number;
export declare function uncompressSync(input: string | Uint8Array, options?: { asBuffer?: true }): Buffer;
export declare function uncompressSync(input: string | Uint8Array, options?: DecOptions): string | Buffer | number;
"#)]
pub fn uncompress_sync<'env>(
env: &'env Env,
input: Either<String, &'env [u8]>,
#[napi(ts_arg_type = "undefined")] input: Either<String, &'env [u8]>,
options: Option<DecOptions>,
) -> Result<Either<String, BufferSlice<'env>>> {
) -> Result<Either3<String, BufferSlice<'env>, u32>> {
let mut dec = Decoder::new();
let input_data = match input {
Either::A(ref s) => s.as_bytes(),
Either::B(b) => b,
};

let as_buffer = options.as_ref().and_then(|o| o.as_buffer).unwrap_or(true);
let copy_output_data = options
.as_ref()
.and_then(|o| o.copy_output_data)
.unwrap_or(false);

if let Some(mut opts) = options {
if let Some(ref mut output_buffer) = opts.output {
Comment thread
osztenkurden marked this conversation as resolved.
let decompressed_len = dec
.decompress(input_data, unsafe { output_buffer.as_mut() })
.map_err(|err| Error::new(Status::GenericFailure, format!("{err}")))?;

return Ok(Either3::C(decompressed_len as u32));
}
}
dec
.decompress_vec(match input {
Either::A(ref s) => s.as_bytes(),
Either::B(b) => b,
})
.decompress_vec(input_data)
.map_err(|err| Error::new(Status::GenericFailure, format!("{err}")))
.and_then(|output| {
if options.as_ref().and_then(|o| o.as_buffer).unwrap_or(true) {
if options
.as_ref()
.and_then(|o| o.copy_output_data)
.unwrap_or(false)
{
BufferSlice::copy_from(env, output).map(Either::B)
if as_buffer {
if copy_output_data {
BufferSlice::copy_from(env, output).map(Either3::B)
} else {
BufferSlice::from_data(env, output).map(Either::B)
BufferSlice::from_data(env, output).map(Either3::B)
}
} else {
Ok(Either::A(String::from_utf8(output).map_err(|e| {
Ok(Either3::A(String::from_utf8(output).map_err(|e| {
Error::new(Status::GenericFailure, format!("{e}"))
})?))
}
})
}

#[napi]
#[napi(ts_return_type = r#"Promise<Buffer>
export declare function uncompress(input: string | Uint8Array, options: { asBuffer: false }): Promise<string>;
export declare function uncompress(input: string | Uint8Array, options: { output: Uint8Array }): Promise<number>;
export declare function uncompress(input: string | Uint8Array, options?: { asBuffer?: true }): Promise<Buffer>;
export declare function uncompress(input: string | Uint8Array, options?: DecOptions): Promise<string | Buffer | number>;
"#)]
pub fn uncompress(
input: Either<String, Uint8Array>,
options: Option<DecOptions>,
Expand Down
Loading