"use strict";
const koffi = require("koffi");
const fs = require("node:fs");
const path = require("node:path");
const STATUS_OK = 0;
const STATUS_INVALID = -1;
const STATUS_REJECTED = -2;
const CDYLIB_NAMES = ["pith_digest.dll", "libpith_digest.so", "libpith_digest.dylib"];
const PKG_ROOT = path.join(__dirname);
const REPO_ROOT = path.resolve(__dirname, "..", "..");
class FfiError extends Error {
constructor(op, status) {
const kind = { [STATUS_INVALID]: "invalid argument", [STATUS_REJECTED]: "input rejected" }[status] ?? "unknown failure";
super(`${op} failed: ${kind} (status ${status})`);
this.name = "FfiError";
this.status = status;
}
}
function findCdylib() {
const explicit = process.env.PITH_CDYLIB;
if (explicit && fs.statSync(explicit, { throwIfNoEntry: false })?.isFile()) {
return path.resolve(explicit);
}
const dirs = [];
const envDir = process.env.PITH_CDYLIB_DIR;
if (envDir) {
dirs.push(envDir);
if (!path.isAbsolute(envDir)) {
dirs.push(path.join(REPO_ROOT, envDir));
}
}
const osArch = `${process.platform}-${process.arch}`;
dirs.push(path.join(PKG_ROOT, "prebuilds", osArch));
dirs.push(path.join(PKG_ROOT, "prebuilds"));
dirs.push(path.join(REPO_ROOT, "target", "release"));
for (const dir of dirs) {
for (const name of CDYLIB_NAMES) {
const p = path.join(dir, name);
if (fs.statSync(p, { throwIfNoEntry: false })?.isFile()) return p;
}
}
throw new Error(
"no pith-digest cdylib found (searched PITH_CDYLIB, PITH_CDYLIB_DIR, prebuilds/ and <repo>/target/release); " +
"run `cargo build --release` first",
);
}
let cached = undefined;
function loadLibrary() {
if (cached) return cached;
const lib = koffi.load(findCdylib());
const sha256 = lib.func("pith_digest_sha256", "int32_t", [
"const uint8_t *",
"size_t",
"uint8_t *",
]);
const crc32 = lib.func("pith_digest_crc32", "int32_t", [
"const uint8_t *",
"size_t",
koffi.out(koffi.pointer("uint32_t")),
]);
const adler32 = lib.func("pith_digest_adler32", "int32_t", [
"const uint8_t *",
"size_t",
koffi.out(koffi.pointer("uint32_t")),
]);
const fnv1a64 = lib.func("pith_digest_fnv1a64", "int32_t", [
"const uint8_t *",
"size_t",
koffi.out(koffi.pointer("uint64_t")),
]);
const splitmix64Fill = lib.func("pith_digest_splitmix64_fill", "int32_t", [
"uint64_t",
"uint64_t *",
"size_t",
]);
const sha1 = lib.func("pith_digest_sha1", "int32_t", [
"const uint8_t *",
"size_t",
"uint8_t *",
]);
const sha512 = lib.func("pith_digest_sha512", "int32_t", [
"const uint8_t *",
"size_t",
"uint8_t *",
]);
const hmacSha1 = lib.func("pith_digest_hmac_sha1", "int32_t", [
"const uint8_t *",
"size_t",
"const uint8_t *",
"size_t",
"uint8_t *",
]);
const hmacSha256 = lib.func("pith_digest_hmac_sha256", "int32_t", [
"const uint8_t *",
"size_t",
"const uint8_t *",
"size_t",
"uint8_t *",
]);
const hmacSha512 = lib.func("pith_digest_hmac_sha512", "int32_t", [
"const uint8_t *",
"size_t",
"const uint8_t *",
"size_t",
"uint8_t *",
]);
const xxh64 = lib.func("pith_digest_xxh64", "int32_t", [
"const uint8_t *",
"size_t",
"uint64_t",
koffi.out(koffi.pointer("uint64_t")),
]);
const murmur3X64128 = lib.func("pith_digest_murmur3_x64_128", "int32_t", [
"const uint8_t *",
"size_t",
"uint32_t",
"uint8_t *",
]);
const crc32c = lib.func("pith_digest_crc32c", "int32_t", [
"const uint8_t *",
"size_t",
koffi.out(koffi.pointer("uint32_t")),
]);
const base64Encode = lib.func("pith_digest_base64_encode", "int32_t", [
"const uint8_t *",
"size_t",
"uint8_t *",
"size_t",
koffi.out(koffi.pointer("size_t")),
]);
const base64Decode = lib.func("pith_digest_base64_decode", "int32_t", [
"const uint8_t *",
"size_t",
"uint8_t *",
"size_t",
koffi.out(koffi.pointer("size_t")),
]);
const xoshiro256Fill = lib.func("pith_digest_xoshiro256_fill", "int32_t", [
"uint64_t",
"uint64_t *",
"size_t",
]);
cached = {
sha256, crc32, adler32, fnv1a64, splitmix64Fill,
sha1, sha512, hmacSha1, hmacSha256, hmacSha512,
xxh64, murmur3X64128, crc32c, base64Encode, base64Decode,
xoshiro256Fill,
};
return cached;
}
function assertData(data, op) {
if (!Buffer.isBuffer(data)) {
throw new TypeError(`${op}: data must be a Buffer`);
}
}
function sha256(data) {
assertData(data, "sha256");
const { sha256: op } = loadLibrary();
const out = Buffer.alloc(32);
const status = op(data, data.length, out);
if (status !== STATUS_OK) {
throw new FfiError("pith_digest_sha256", status);
}
return out;
}
function hex64(value) {
return value.toString(16).padStart(16, "0");
}
function crc32(data) {
assertData(data, "crc32");
const { crc32: op } = loadLibrary();
const out = [0];
const status = op(data, data.length, out);
if (status !== STATUS_OK) {
throw new FfiError("pith_digest_crc32", status);
}
return out[0] >>> 0;
}
function adler32(data) {
assertData(data, "adler32");
const { adler32: op } = loadLibrary();
const out = [0];
const status = op(data, data.length, out);
if (status !== STATUS_OK) {
throw new FfiError("pith_digest_adler32", status);
}
return out[0] >>> 0;
}
function fnv1a64(data) {
assertData(data, "fnv1a64");
const { fnv1a64: op } = loadLibrary();
const out = [0n];
const status = op(data, data.length, out);
if (status !== STATUS_OK) {
throw new FfiError("pith_digest_fnv1a64", status);
}
return out[0];
}
function splitmix64Fill(seed, count) {
if (!Number.isInteger(count) || count < 0) {
throw new TypeError("count must be a non-negative integer");
}
const { splitmix64Fill: op } = loadLibrary();
const out = Buffer.alloc(8 * count);
const status = op(BigInt(seed), out, count);
if (status !== STATUS_OK) {
throw new FfiError("pith_digest_splitmix64_fill", status);
}
const values = [];
for (let i = 0; i < count; i++) {
values.push(out.readBigUInt64LE(8 * i));
}
return values;
}
function sha1(data) {
assertData(data, "sha1");
const { sha1: op } = loadLibrary();
const out = Buffer.alloc(20);
const status = op(data, data.length, out);
if (status !== STATUS_OK) {
throw new FfiError("pith_digest_sha1", status);
}
return out;
}
function sha512(data) {
assertData(data, "sha512");
const { sha512: op } = loadLibrary();
const out = Buffer.alloc(64);
const status = op(data, data.length, out);
if (status !== STATUS_OK) {
throw new FfiError("pith_digest_sha512", status);
}
return out;
}
function hmac(opName, op, key, data, digestLen) {
if (!Buffer.isBuffer(key)) {
throw new TypeError(`${opName}: key must be a Buffer`);
}
assertData(data, opName);
const out = Buffer.alloc(digestLen);
const status = op(key, key.length, data, data.length, out);
if (status !== STATUS_OK) {
throw new FfiError(opName, status);
}
return out;
}
function hmacSha1(key, data) {
return hmac("pith_digest_hmac_sha1", loadLibrary().hmacSha1, key, data, 20);
}
function hmacSha256(key, data) {
return hmac("pith_digest_hmac_sha256", loadLibrary().hmacSha256, key, data, 32);
}
function hmacSha512(key, data) {
return hmac("pith_digest_hmac_sha512", loadLibrary().hmacSha512, key, data, 64);
}
function xxh64(data, seed) {
assertData(data, "xxh64");
const { xxh64: op } = loadLibrary();
const out = [0n];
const status = op(data, data.length, BigInt(seed), out);
if (status !== STATUS_OK) {
throw new FfiError("pith_digest_xxh64", status);
}
return out[0];
}
function murmur3X64128(data, seed) {
assertData(data, "murmur3_x64_128");
const { murmur3X64128: op } = loadLibrary();
const out = Buffer.alloc(16);
const status = op(data, data.length, seed >>> 0, out);
if (status !== STATUS_OK) {
throw new FfiError("pith_digest_murmur3_x64_128", status);
}
return out;
}
function crc32c(data) {
assertData(data, "crc32c");
const { crc32c: op } = loadLibrary();
const out = [0];
const status = op(data, data.length, out);
if (status !== STATUS_OK) {
throw new FfiError("pith_digest_crc32c", status);
}
return out[0] >>> 0;
}
function base64Encode(data, out) {
assertData(data, "base64_encode");
const { base64Encode: op } = loadLibrary();
const dest = out ?? Buffer.alloc(Math.max(1, Math.ceil(data.length / 3) * 4));
const outLen = [0];
const status = op(data, data.length, dest, dest.length, outLen);
if (status !== STATUS_OK) {
throw new FfiError("pith_digest_base64_encode", status);
}
return dest.toString("ascii", 0, outLen[0]);
}
function base64Decode(data, out) {
assertData(data, "base64_decode");
const { base64Decode: op } = loadLibrary();
const dest = out ?? Buffer.alloc(Math.max(1, Math.floor(data.length / 4) * 3));
const outLen = [0];
const status = op(data, data.length, dest, dest.length, outLen);
if (status !== STATUS_OK) {
throw new FfiError("pith_digest_base64_decode", status);
}
return dest.subarray(0, outLen[0]);
}
function xoshiro256Fill(seed, count) {
if (!Number.isInteger(count) || count < 0) {
throw new TypeError("count must be a non-negative integer");
}
const { xoshiro256Fill: op } = loadLibrary();
const out = Buffer.alloc(8 * count);
const status = op(BigInt(seed), out, count);
if (status !== STATUS_OK) {
throw new FfiError("pith_digest_xoshiro256_fill", status);
}
const values = [];
for (let i = 0; i < count; i++) {
values.push(out.readBigUInt64LE(8 * i));
}
return values;
}
module.exports = {
STATUS_OK,
STATUS_INVALID,
STATUS_REJECTED,
CDYLIB_NAMES,
FfiError,
findCdylib,
loadLibrary,
sha256,
sha1,
sha512,
hmacSha1,
hmacSha256,
hmacSha512,
crc32,
crc32c,
adler32,
fnv1a64,
xxh64,
murmur3X64128,
base64Encode,
base64Decode,
splitmix64Fill,
xoshiro256Fill,
hex64,
};