"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 SIGNATURE_WORDS = 128;
const CDYLIB_NAMES = ["pith_text.dll", "libpith_text.so", "libpith_text.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-text 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 fingerprint = lib.func("pith_text_fingerprint", "int32_t", [
"const uint8_t *",
"size_t",
koffi.out(koffi.pointer("void *")),
koffi.out(koffi.pointer("size_t")),
]);
const jaccard = lib.func("pith_text_jaccard", "int32_t", [
"const uint8_t *",
"size_t",
"const uint8_t *",
"size_t",
koffi.out(koffi.pointer("uint64_t")),
]);
const free = lib.func("void pith_text_free(void *ptr, size_t len)");
cached = { fingerprint, jaccard, free };
return cached;
}
function wordsBlob(words) {
if (Buffer.isBuffer(words)) return words;
const out = Buffer.alloc(words.length * 8);
for (let i = 0; i < words.length; i++) {
out.writeBigUInt64LE(BigInt(words[i]), i * 8);
}
return out;
}
function fingerprintStream(data) {
const blob = Buffer.isBuffer(data) ? data : Buffer.from(data, "utf8");
const { fingerprint, free } = loadLibrary();
const out = [null];
const outLen = [0];
const status = fingerprint(blob, blob.length, out, outLen);
if (status !== STATUS_OK) {
throw new FfiError("pith_text_fingerprint", status);
}
try {
return Buffer.from(koffi.decode(out[0], "uint8_t", Number(outLen[0])));
} finally {
free(out[0], Number(outLen[0]));
}
}
function parseFingerprint(raw) {
if (!Buffer.isBuffer(raw) || raw.length < 8 + SIGNATURE_WORDS * 8) {
throw new TypeError("canonical stream is shorter than the header plus signature tail");
}
const canonicalLen = raw.length - 8 - SIGNATURE_WORDS * 8;
const signature = [];
for (let i = 0; i < SIGNATURE_WORDS; i++) {
signature.push(raw.readBigUInt64LE(8 + canonicalLen + i * 8));
}
return {
wordCount: raw.readUInt32BE(0),
shingleCount: raw.readUInt32BE(4),
canonical: raw.subarray(8, 8 + canonicalLen),
signature,
raw,
};
}
function jaccardBits(a, b) {
const aBlob = wordsBlob(a);
const bBlob = wordsBlob(b);
const { jaccard } = loadLibrary();
const bits = [null];
const status = jaccard(aBlob, aBlob.length / 8, bBlob, bBlob.length / 8, bits);
if (status !== STATUS_OK) {
throw new FfiError("pith_text_jaccard", status);
}
return BigInt(bits[0]);
}
function jaccard(a, b) {
const buf = Buffer.alloc(8);
buf.writeBigUInt64LE(jaccardBits(a, b), 0);
return buf.readDoubleLE(0);
}
module.exports = {
STATUS_OK,
STATUS_INVALID,
STATUS_REJECTED,
SIGNATURE_WORDS,
CDYLIB_NAMES,
FfiError,
findCdylib,
fingerprintStream,
parseFingerprint,
jaccardBits,
jaccard,
};