Skip to content

Commit

Permalink
changes
Browse files Browse the repository at this point in the history
  • Loading branch information
baileympearson committed Nov 14, 2024
1 parent 8c40b08 commit 5bac6ab
Show file tree
Hide file tree
Showing 12 changed files with 380 additions and 66 deletions.
12 changes: 10 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,16 @@ jobs:
cache: 'npm'
registry-url: 'https://registry.npmjs.org'

- name: Build with Node.js ${{ matrix.node }} on ${{ matrix.os }}
run: npm install && npm run compile
- name: Install zstd
run: npm run install-zstd
shell: bash

- name: install dependencies
run: npm install --loglevel verbose --ignore-scripts && npm i --ignore-scripts @mongodb-js/zstd --no-save
shell: bash

- name: Compile addon
run: npm run compile
shell: bash

- name: Test ${{ matrix.os }}
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ node_modules
build

npm-debug.log
deps
98 changes: 98 additions & 0 deletions addon/compression_worker.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#include <napi.h>

#include <optional>

using namespace Napi;

/**
* @brief A class that represents the result of a compression operation. Once the
* MACOS_DEPLOYMENT_TARGET can be raised to 10.13 and use a c++17, we can remove this class and use
* a std::optional<std::variant<std::vector<uint8_t>, std::string>>> instead.
*/
struct CompressionResult {
CompressionResult(std::string error,
std::vector<uint8_t> result,
bool hasError,
bool hasResult,
bool initialized)
: error(error),
result(result),
hasError(hasError),
hasResult(hasResult),
initialized(true) {}

public:
static CompressionResult Error(std::string error) {
return CompressionResult(error, std::vector<uint8_t>(), true, false, true);
}

static CompressionResult Ok(std::vector<uint8_t> result) {
return CompressionResult(std::string(""), result, false, true, true);
}

static CompressionResult Empty() {
return CompressionResult(std::string(""), std::vector<uint8_t>(), false, false, false);
}

std::string error;
std::vector<uint8_t> result;

bool hasError;
bool hasResult;
bool initialized;
};

/**
* @brief An asynchronous Napi::Worker that can be with any functor that produces
* CompressionResults.
*
* @tparam TWorker - The functor to call asynchronously.
*/
template <typename TWorker>
class Worker : public Napi::AsyncWorker {
public:
Worker(const Napi::Env& env, TWorker worker)
: Napi::AsyncWorker{env, "Worker"},
m_deferred{env},
worker(worker),
result(CompressionResult::Empty()) {}

Napi::Promise GetPromise() {
return m_deferred.Promise();
}

protected:
void Execute() {
result = worker();
}

void OnOK() {
if (!result.initialized) {
m_deferred.Reject(Napi::Error::New(Env(),
"zstd runtime error - async worker finished without "
"a compression or decompression result.")
.Value());
} else if (result.hasError) {
m_deferred.Reject(Napi::Error::New(Env(), result.error).Value());
} else if (result.hasResult) {
Buffer<uint8_t> output =
Buffer<uint8_t>::Copy(m_deferred.Env(), result.result.data(), result.result.size());

m_deferred.Resolve(output);
} else {
m_deferred.Reject(Napi::Error::New(Env(),
"zstd runtime error - async worker finished without "
"a compression or decompression result.")
.Value());
}
}

void OnError(const Napi::Error& err) {
m_deferred.Reject(err.Value());
}

private:
Napi::Promise::Deferred m_deferred;
TWorker worker;
CompressionResult result;
};
44 changes: 44 additions & 0 deletions addon/compressor.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@

#include <napi.h>

#include <vector>

#include "zstd.h"

using namespace Napi;

struct Compressor {
std::vector<uint8_t> data;
size_t compression_level;

Compressor(std::vector<uint8_t> data, size_t compression_level)
: data(data), compression_level(compression_level) {}

CompressionResult operator()() {
size_t output_buffer_size = ZSTD_compressBound(data.size());
std::vector<uint8_t> output(output_buffer_size);

size_t result_code = ZSTD_compress(
output.data(), output.size(), data.data(), data.size(), compression_level);

if (ZSTD_isError(result_code)) {
std::string error(ZSTD_getErrorName(result_code));
return CompressionResult::Error(error);
}

output.resize(result_code);

return CompressionResult::Ok(output);
}

static Compressor fromUint8Array(const Uint8Array& to_compress, size_t compression_level) {
const uint8_t* input_data = to_compress.Data();
size_t total = to_compress.ElementLength();

std::vector<uint8_t> data(to_compress.ElementLength());

std::copy(input_data, input_data + total, data.data());

return Compressor(std::move(data), compression_level);
}
};
41 changes: 41 additions & 0 deletions addon/decompressor.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#include <napi.h>

#include <vector>

#include "zstd.h"

using namespace Napi;

struct Decompressor {
std::vector<uint8_t> data;
size_t buffer_size;

Decompressor(std::vector<uint8_t> data, size_t buffer_size)
: data(data), buffer_size(buffer_size) {}

CompressionResult operator()() {
std::vector<uint8_t> decompressed(buffer_size);

size_t _result =
ZSTD_decompress(decompressed.data(), decompressed.size(), data.data(), data.size());

if (ZSTD_isError(_result)) {
std::string error(ZSTD_getErrorName(_result));
return CompressionResult::Error(error);
}

decompressed.resize(_result);

return CompressionResult::Ok(decompressed);
}

static Decompressor fromUint8Array(const Uint8Array& compressed_data) {
const uint8_t* input_data = compressed_data.Data();
size_t total = compressed_data.ElementLength();

std::vector<uint8_t> data(total);
std::copy(input_data, input_data + total, data.data());

return Decompressor(data, total * 1000);
}
};
31 changes: 31 additions & 0 deletions addon/napi_utils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#include <napi.h>

using namespace Napi;

/**
* @brief Given a T* source and a T* destination, copies count
* elements from source into destination.
*/
template <typename T>
void copy_buffer_data(T* source, T* dest, size_t count) {
for (size_t i = 0; i < count; ++i) {
dest[i] = source[i];
}
}

/**
* @brief Given an Napi;:Value, this function returns the value as a Uint8Array, if the
* Value is a Uint8Array. Otherwise, this function throws.
*
* @param v - An Napi::Value
* @param argument_name - the name of the value, to use when constructing an error message.
* @return Napi::Uint8Array
*/
Uint8Array Uint8ArrayFromValue(Value v, std::string argument_name) {
if (!v.IsTypedArray() || v.As<TypedArray>().TypedArrayType() != napi_uint8_array) {
std::string error_message = "Parameter `" + argument_name + "` must be a Uint8Array.";
throw TypeError::New(v.Env(), error_message);
}

return v.As<Uint8Array>();
}
47 changes: 41 additions & 6 deletions addon/zstd.cpp
Original file line number Diff line number Diff line change
@@ -1,14 +1,49 @@
#include "zstd.h"

#include <napi.h>

#include <string>
#include <vector>

#include "compression_worker.h"
#include "compressor.h"
#include "decompressor.h"
#include "napi_utils.h"

using namespace Napi;

Napi::String Compress(const Napi::CallbackInfo& info) {
auto string = Napi::String::New(info.Env(), "compress()");
return string;
Napi::Promise Compress(const Napi::CallbackInfo& info) {
// Argument handling happens in JS
if (info.Length() != 2) {
std::string error_message = "Expected two arguments.";
throw TypeError::New(info.Env(), error_message);
}

Uint8Array to_compress = Uint8ArrayFromValue(info[0], "buffer");
size_t compression_level = (size_t)info[1].ToNumber().Int32Value();

Compressor compressor = Compressor::fromUint8Array(to_compress, compression_level);
Worker<Compressor>* worker = new Worker<Compressor>(info.Env(), std::move(compressor));

worker->Queue();

return worker->GetPromise();
}
Napi::String Decompress(const Napi::CallbackInfo& info) {
auto string = Napi::String::New(info.Env(), "decompress()");
return string;

Napi::Promise Decompress(const CallbackInfo& info) {
// Argument handling happens in JS
if (info.Length() != 1) {
std::string error_message = "Expected one argument.";
throw TypeError::New(info.Env(), error_message);
}

Napi::Uint8Array compressed_data = Uint8ArrayFromValue(info[0], "buffer");
Decompressor decompressor = Decompressor::fromUint8Array(compressed_data);
Worker<Decompressor>* worker = new Worker<Decompressor>(info.Env(), decompressor);

worker->Queue();

return worker->GetPromise();
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
Expand Down
16 changes: 13 additions & 3 deletions binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@
'type': 'loadable_module',
'defines': ['ZSTD_STATIC_LINKING_ONLY'],
'include_dirs': [
"<!(node -p \"require('node-addon-api').include_dir\")"
"<!(node -p \"require('node-addon-api').include_dir\")",
"<(module_root_dir)/deps/zstd/lib",
],
'variables': {
'ARCH': '<(host_arch)',
'built_with_electron%': 0
},
'sources': [
'addon/zstd.cpp'
'addon/zstd.cpp',
'addon/compression_worker.h',
'addon/compressor.h',
'addon/decompressor.h',
'addon/napi_utils.h',
],
'xcode_settings': {
'GCC_ENABLE_CPP_EXCEPTIONS': 'YES',
Expand All @@ -23,6 +28,11 @@
'cflags_cc!': [ '-fno-exceptions' ],
'msvs_settings': {
'VCCLCompilerTool': { 'ExceptionHandling': 1 },
}
},
'link_settings': {
'libraries': [
'<(module_root_dir)/deps/zstd/build/cmake/lib/libzstd.a',
]
},
}]
}
26 changes: 26 additions & 0 deletions etc/install-zstd.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

set -o xtrace

clean_deps() {
rm -rf deps
}

download_zstd() {
rm -rf deps
mkdir -p deps/zstd

curl -L "https://github.com/facebook/zstd/releases/download/v1.5.6/zstd-1.5.6.tar.gz" \
| tar -zxf - -C deps/zstd --strip-components 1
}

build_zstd() {
export MACOSX_DEPLOYMENT_TARGET=10.12
cd deps/zstd/build/cmake

cmake .
make
}

clean_deps
download_zstd
build_zstd
15 changes: 13 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,25 @@ const { compress: _compress, decompress: _decompress } = require('bindings')('zs
// Error objects created via napi don't have JS stacks; wrap them so .stack is present
// https://github.com/nodejs/node/issues/25318#issuecomment-451068073

exports.compress = async function compress(data) {
exports.compress = async function compress(data, compressionLevel) {
if (!Buffer.isBuffer(data)) {
throw new TypeError(`parameter 'data' must be a Buffer.`);
}

if (compressionLevel != null && typeof compressionLevel !== 'number') {
throw new TypeError(`parameter 'compressionLevel' must be a number.`);
}

try {
return await _compress(data);
return await _compress(data, compressionLevel ?? 3);
} catch (e) {
throw new Error(`zstd: ${e.message}`);
}
};
exports.decompress = async function decompress(data) {
if (!Buffer.isBuffer(data)) {
throw new TypeError(`parameter 'data' must be a Buffer.`);
}
try {
return await _decompress(data);
} catch (e) {
Expand Down
Loading

0 comments on commit 5bac6ab

Please sign in to comment.