diff --git a/buffer.js b/buffer.js index 57eded4..3d50186 100644 --- a/buffer.js +++ b/buffer.js @@ -113,3 +113,20 @@ export const encodeAny = data => { * @return {any} */ export const decodeAny = buf => decoding.readAny(decoding.createDecoder(buf)) + +/** + * Shift Byte Array {N} bits to the left. Does not expand byte array. + * + * @param {Uint8Array} bs + * @param {number} N should be in the range of [0-7] + */ +export const shiftNBitsLeft = (bs, N) => { + if (N === 0) return bs + bs = new Uint8Array(bs) + bs[0] <<= N + for (let i = 1; i < bs.length; i++) { + bs[i - 1] |= bs[i] >>> (8 - N) + bs[i] <<= N + } + return bs +} diff --git a/checksum.js b/checksum.js new file mode 100644 index 0000000..22cd38d --- /dev/null +++ b/checksum.js @@ -0,0 +1,28 @@ +import * as buffer from './buffer.js' + +/** + * Little endian table + * @type {Uint8Array | null} + */ +let _crc32Table = null +const _computeCrc32Table = () => { + if (_crc32Table == null) { + _crc32Table = buffer.createUint8ArrayFromLen(32) + } + let i = 128 + let crc = 1 + do { + if ((crc & 1) > 0) { // @todo this could be optimized + crc = (crc >>> 1) ^ 0x8408 + } else { + crc >>>= 1 + } + for (let j = 0; j < 256; j = j * i) { + _crc32Table[i + j] = crc ^ _crc32Table[j] + } + i >>>= 1 + } while (i > 0) + return _crc32Table +} + +console.log(_computeCrc32Table()) diff --git a/hash/rabin-gf2-polynomial.js b/hash/rabin-gf2-polynomial.js new file mode 100644 index 0000000..6ae87b2 --- /dev/null +++ b/hash/rabin-gf2-polynomial.js @@ -0,0 +1,379 @@ +/** + * The idea of the Rabin fingerprint algorithm is to represent the binary as a polynomial in a + * finite field (Galois Field G(2)). The polynomial will then be taken "modulo" by an irreducible + * polynomial of the desired size. + * + * This implementation is inefficient and is solely used to verify the actually performant + * implementation in `./rabin.js`. + * + * @module rabin-gf2-polynomial + */ + +import * as math from '../math.js' +import * as webcrypto from 'lib0/webcrypto' +import * as array from '../array.js' +import * as buffer from '../buffer.js' + +/** + * @param {number} degree + */ +const _degreeToMinByteLength = degree => math.floor(degree / 8) + 1 + +/** + * This is a GF2 Polynomial abstraction that is not meant for production! + * + * It is easy to understand and it's correctness is as obvious as possible. It can be used to verify + * efficient implementations of algorithms on GF2. + */ +export class GF2Polynomial { + constructor () { + /** + * @type {Set} + */ + this.degrees = new Set() + } +} + +/** + * From Uint8Array (MSB). + * + * @param {Uint8Array} bytes + */ +export const createFromBytes = bytes => { + const p = new GF2Polynomial() + for (let bsi = bytes.length - 1, currDegree = 0; bsi >= 0; bsi--) { + const currByte = bytes[bsi] + for (let i = 0; i < 8; i++) { + if (((currByte >>> i) & 1) === 1) { + p.degrees.add(currDegree) + } + currDegree++ + } + } + return p +} + +/** + * Transform to Uint8Array (MSB). + * + * @param {GF2Polynomial} p + * @param {number} byteLength + */ +export const toUint8Array = (p, byteLength = _degreeToMinByteLength(getHighestDegree(p))) => { + const buf = buffer.createUint8ArrayFromLen(byteLength) + /** + * @param {number} i + */ + const setBit = i => { + const bi = math.floor(i / 8) + buf[buf.length - 1 - bi] |= (1 << (i % 8)) + } + p.degrees.forEach(setBit) + return buf +} + +/** + * Create from unsigned integer (max 32bit uint) - read most-significant-byte first. + * + * @param {number} uint + */ +export const createFromUint = uint => { + const buf = new Uint8Array(4) + for (let i = 0; i < 4; i++) { + buf[i] = uint >>> 8 * (3 - i) + } + return createFromBytes(buf) +} + +/** + * Create a random polynomial of a specified degree. + * + * @param {number} degree + */ +export const createRandom = degree => { + const bs = new Uint8Array(_degreeToMinByteLength(degree)) + webcrypto.getRandomValues(bs) + // Get first byte and explicitly set the bit of "degree" to 1 (the result must have the specified + // degree). + const firstByte = bs[0] | 1 << (degree % 8) + // Find out how many bits of the first byte need to be filled with zeros because they are >degree. + const zeros = 7 - (degree % 8) + bs[0] = ((firstByte << zeros) & 0xff) >>> zeros + return createFromBytes(bs) +} + +/** + * @param {GF2Polynomial} p + * @return number + */ +export const getHighestDegree = p => array.fold(array.from(p.degrees), 0, math.max) + +/** + * Add (+) p2 int the p1 polynomial. + * + * Addition is defined as xor in F2. Substraction is equivalent to addition in F2. + * + * @param {GF2Polynomial} p1 + * @param {GF2Polynomial} p2 + */ +export const addInto = (p1, p2) => { + p2.degrees.forEach(degree => { + if (p1.degrees.has(degree)) { + p1.degrees.delete(degree) + } else { + p1.degrees.add(degree) + } + }) +} + +/** + * Or (|) p2 into the p1 polynomial. + * + * Addition is defined as xor in F2. Substraction is equivalent to addition in F2. + * + * @param {GF2Polynomial} p1 + * @param {GF2Polynomial} p2 + */ +export const orInto = (p1, p2) => { + p2.degrees.forEach(degree => { + p1.degrees.add(degree) + }) +} + +/** + * Add (+) p2 to the p1 polynomial. + * + * Addition is defined as xor in F2. Substraction is equivalent to addition in F2. + * + * @param {GF2Polynomial} p1 + * @param {GF2Polynomial} p2 + */ +export const add = (p1, p2) => { + const result = new GF2Polynomial() + p2.degrees.forEach(degree => { + if (!p1.degrees.has(degree)) { + result.degrees.add(degree) + } + }) + p1.degrees.forEach(degree => { + if (!p2.degrees.has(degree)) { + result.degrees.add(degree) + } + }) + return result +} + +/** + * Add (+) p2 to the p1 polynomial. + * + * Addition is defined as xor in F2. Substraction is equivalent to addition in F2. + * + * @param {GF2Polynomial} p + */ +export const clone = (p) => { + const result = new GF2Polynomial() + p.degrees.forEach(d => result.degrees.add(d)) + return result +} + +/** + * Add (+) p2 to the p1 polynomial. + * + * Addition is defined as xor in F2. Substraction is equivalent to addition in F2. + * + * @param {GF2Polynomial} p + * @param {number} degree + */ +export const addDegreeInto = (p, degree) => { + if (p.degrees.has(degree)) { + p.degrees.delete(degree) + } else { + p.degrees.add(degree) + } +} + +/** + * Multiply (•) p1 with p2 and store the result in p1. + * + * @param {GF2Polynomial} p1 + * @param {GF2Polynomial} p2 + */ +export const multiply = (p1, p2) => { + const result = new GF2Polynomial() + p1.degrees.forEach(degree1 => { + p2.degrees.forEach(degree2 => { + addDegreeInto(result, degree1 + degree2) + }) + }) + return result +} + +/** + * Multiply (•) p1 with p2 and store the result in p1. + * + * @param {GF2Polynomial} p + * @param {number} shift + */ +export const shiftLeft = (p, shift) => { + const result = new GF2Polynomial() + p.degrees.forEach(degree => { + const r = degree + shift + r >= 0 && result.degrees.add(r) + }) + return result +} + +/** + * Computes p1 % p2. I.e. the remainder of p1/p2. + * + * @param {GF2Polynomial} p1 + * @param {GF2Polynomial} p2 + */ +export const mod = (p1, p2) => { + const maxDeg1 = getHighestDegree(p1) + const maxDeg2 = getHighestDegree(p2) + const result = clone(p1) + for (let i = maxDeg1 - maxDeg2; i >= 0; i--) { + if (result.degrees.has(maxDeg2 + i)) { + const shifted = shiftLeft(p2, i) + addInto(result, shifted) + } + } + return result +} + +/** + * Computes (p^e mod m). + * + * http://en.wikipedia.org/wiki/Modular_exponentiation + * + * @param {GF2Polynomial} p + * @param {number} e + * @param {GF2Polynomial} m + */ +export const modPow = (p, e, m) => { + let result = ONE + while (true) { + if ((e & 1) === 1) { + result = mod(multiply(result, p), m) + } + e >>>= 1 + if (e === 0) { + return result + } + p = mod(multiply(p, p), m) + } +} + +/** + * Find the greatest common divisor using Euclid's Algorithm. + * + * @param {GF2Polynomial} p1 + * @param {GF2Polynomial} p2 + */ +export const gcd = (p1, p2) => { + while (p2.degrees.size > 0) { + const modded = mod(p1, p2) + p1 = p2 + p2 = modded + } + return p1 +} + +/** + * true iff p1 equals p2 + * + * @param {GF2Polynomial} p1 + * @param {GF2Polynomial} p2 + */ +export const equals = (p1, p2) => { + if (p1.degrees.size !== p2.degrees.size) return false + for (const d of p1.degrees) { + if (!p2.degrees.has(d)) return false + } + return true +} + +const X = createFromBytes(new Uint8Array([2])) +const ONE = createFromBytes(new Uint8Array([1])) + +/** + * Computes ( x^(2^p) - x ) mod f + * + * (shamelessly copied from + * https://github.com/opendedup/rabinfingerprint/blob/master/src/org/rabinfingerprint/polynomial/Polynomial.java) + * + * @param {GF2Polynomial} f + * @param {number} p + */ +const reduceExponent = (f, p) => { + // compute (x^q^p mod f) + const q2p = math.pow(2, p) + const x2q2p = modPow(X, q2p, f) + // subtract (x mod f) + return mod(add(x2q2p, X), f) +} + +/** + * BenOr Reducibility Test + * + * Tests and Constructions of Irreducible Polynomials over Finite Fields + * (1997) Shuhong Gao, Daniel Panario + * + * http://citeseer.ist.psu.edu/cache/papers/cs/27167/http:zSzzSzwww.math.clemson.eduzSzfacultyzSzGaozSzpaperszSzGP97a.pdf/gao97tests.pdf + * + * @param {GF2Polynomial} p + */ +export const isIrreducibleBenOr = p => { + const degree = getHighestDegree(p) + for (let i = 1; i < degree / 2; i++) { + const b = reduceExponent(p, i) + const g = gcd(p, b) + if (!equals(g, ONE)) { + return false + } + } + return true +} + +/** + * @param {number} degree + */ +export const createIrreducible = degree => { + while (true) { + const p = createRandom(degree) + if (isIrreducibleBenOr(p)) return p + } +} + +/** + * Create a fingerprint of buf using the irreducible polynomial m. + * + * @param {Uint8Array} buf + * @param {GF2Polynomial} m + */ +export const fingerprint = (buf, m) => toUint8Array(mod(createFromBytes(buf), m), _degreeToMinByteLength(getHighestDegree(m) - 1)) + +export class RabinPolynomialEncoder { + /** + * @param {GF2Polynomial} m The irreducible polynomial + */ + constructor (m) { + this.fingerprint = new GF2Polynomial() + this.m = m + } + + /** + * @param {number} b + */ + write (b) { + const bp = createFromBytes(new Uint8Array([b])) + const fingerprint = shiftLeft(this.fingerprint, 8) + orInto(fingerprint, bp) + this.fingerprint = mod(fingerprint, this.m) + } + + getFingerprint () { + return toUint8Array(this.fingerprint, _degreeToMinByteLength(getHighestDegree(this.m) - 1)) + } +} diff --git a/hash/rabin-uncached.js b/hash/rabin-uncached.js new file mode 100644 index 0000000..f40da97 --- /dev/null +++ b/hash/rabin-uncached.js @@ -0,0 +1,68 @@ +/** + * It is not recommended to use this package. This is the uncached implementation of the rabin + * fingerprint algorithm. However, it can be used to verify the `rabin.js` implementation. + * + * @module rabin-uncached + */ + +import * as math from '../math.js' +import * as buffer from '../buffer.js' + +export class RabinUncachedEncoder { + /** + * @param {Uint8Array} m assert(m[0] === 1) + */ + constructor (m) { + this.m = m + this.blen = m.byteLength + this.bs = new Uint8Array(this.blen) + /** + * This describes the position of the most significant byte (starts with 0 and increases with + * shift) + */ + this.bpos = 0 + } + + /** + * Add/Xor/Substract bytes. + * + * Discards bytes that are out of range. + * @todo put this in function or inline + * + * @param {Uint8Array} cs + */ + add (cs) { + const copyLen = math.min(this.blen, cs.byteLength) + // copy from right to left until max is reached + for (let i = 0; i < copyLen; i++) { + this.bs[(this.bpos + this.blen - i - 1) % this.blen] ^= cs[cs.byteLength - i - 1] + } + } + + /** + * @param {number} byte + */ + write (byte) { + // [0,m1,m2,b] + // x <- bpos + // Shift one byte to the left, add b + this.bs[this.bpos] = byte + this.bpos = (this.bpos + 1) % this.blen + // mod + for (let i = 7; i >= 0; i--) { + if (((this.bs[this.bpos] >>> i) & 1) === 1) { + this.add(buffer.shiftNBitsLeft(this.m, i)) + } + } + // if (this.bs[this.bpos] !== 0) { error.unexpectedCase() } + // assert(this.bs[this.bpos] === 0) + } + + getFingerprint () { + const result = new Uint8Array(this.blen - 1) + for (let i = 0; i < result.byteLength; i++) { + result[i] = this.bs[(this.bpos + i + 1) % this.blen] + } + return result + } +} diff --git a/hash/rabin.js b/hash/rabin.js new file mode 100644 index 0000000..d5690f1 --- /dev/null +++ b/hash/rabin.js @@ -0,0 +1,100 @@ +/** + * @module rabin + * + * Very efficient & versatile fingerprint/hashing algorithm. However, it is not cryptographically + * secure. Well suited for fingerprinting. + */ + +import * as buffer from '../buffer.js' +import * as map from '../map.js' + +export const StandardIrreducible8 = new Uint8Array([1, 221]) +export const StandardIrreducible16 = new Uint8Array([1, 244, 157]) +export const StandardIrreducible32 = new Uint8Array([1, 149, 183, 205, 191]) +export const StandardIrreducible64 = new Uint8Array([1, 133, 250, 114, 193, 250, 28, 193, 231]) +export const StandardIrreducible128 = new Uint8Array([1, 94, 109, 166, 228, 6, 222, 102, 239, 27, 128, 184, 13, 50, 112, 169, 199]) + +/** + * Maps from a modulo to the precomputed values. + * + * @type {Map} + */ +const _precomputedFingerprintCache = new Map() + +/** + * @param {Uint8Array} m + */ +const ensureCache = m => map.setIfUndefined(_precomputedFingerprintCache, buffer.toBase64(m), () => { + const byteLen = m.byteLength + const cache = new Uint8Array(256 * byteLen) + // Use dynamic computing to compute the cached results. + // Starting values: cache(0) = 0; cache(1) = m + cache.set(m, byteLen) + for (let bit = 1; bit < 8; bit++) { + const mBitShifted = buffer.shiftNBitsLeft(m, bit) + const bitShifted = 1 << bit + for (let j = 0; j < bitShifted; j++) { + // apply the shifted result (reducing the degree of the polynomial) + const msb = bitShifted | j + const rest = msb ^ mBitShifted[0] + for (let i = 0; i < byteLen; i++) { + // rest is already precomputed in the cache + cache[msb * byteLen + i] = cache[rest * byteLen + i] ^ mBitShifted[i] + } + // if (cache[(bitShifted | j) * byteLen] !== (bitShifted | j)) { error.unexpectedCase() } + } + } + return cache +}) + +export class RabinEncoder { + /** + * @param {Uint8Array} m assert(m[0] === 1) + */ + constructor (m) { + this.m = m + this.blen = m.byteLength + this.bs = new Uint8Array(this.blen) + this.cache = ensureCache(m) + /** + * This describes the position of the most significant byte (starts with 0 and increases with + * shift) + */ + this.bpos = 0 + } + + /** + * @param {number} byte + */ + write (byte) { + // assert(this.bs[0] === 0) + // Shift one byte to the left, add b + this.bs[this.bpos] = byte + this.bpos = (this.bpos + 1) % this.blen + const msb = this.bs[this.bpos] + for (let i = 0; i < this.blen; i++) { + this.bs[(this.bpos + i) % this.blen] ^= this.cache[msb * this.blen + i] + } + // assert(this.bs[this.bpos] === 0) + } + + getFingerprint () { + const result = new Uint8Array(this.blen - 1) + for (let i = 0; i < result.byteLength; i++) { + result[i] = this.bs[(this.bpos + i + 1) % this.blen] + } + return result + } +} + +/** + * @param {Uint8Array} irreducible + * @param {Uint8Array} data + */ +export const fingerprint = (irreducible, data) => { + const encoder = new RabinEncoder(irreducible) + for (let i = 0; i < data.length; i++) { + encoder.write(data[i]) + } + return encoder.getFingerprint() +} diff --git a/hash/rabin.test.js b/hash/rabin.test.js new file mode 100644 index 0000000..37a82ff --- /dev/null +++ b/hash/rabin.test.js @@ -0,0 +1,232 @@ +import * as t from '../testing.js' +import * as gf2 from './rabin-gf2-polynomial.js' +import { RabinUncachedEncoder } from './rabin-uncached.js' +import * as rabin from './rabin.js' +import * as math from '../math.js' +import * as array from '../array.js' +import * as prng from '../prng.js' +import * as buffer from '../buffer.js' +import * as map from '../map.js' + +/** + * @param {t.TestCase} _tc + */ +export const testPolynomialBasics = _tc => { + const bs = new Uint8Array([1, 11]) + const p = gf2.createFromBytes(bs) + t.assert(p.degrees.has(3)) + t.assert(p.degrees.has(1)) + t.assert(p.degrees.has(0)) + t.assert(p.degrees.has(8)) +} + +/** + * @param {t.TestCase} _tc + */ +export const testIrreducibleInput = _tc => { + const pa = gf2.createFromUint(0x53) + const pb = gf2.createFromUint(0xCA) + const pm = gf2.createFromUint(0x11B) + const px = gf2.multiply(pa, pb) + t.compare(new Uint8Array([0x53]), gf2.toUint8Array(pa)) + t.compare(new Uint8Array([0xCA]), gf2.toUint8Array(pb)) + t.assert(gf2.equals(gf2.createFromUint(0x3F7E), px)) + t.compare(new Uint8Array([0x3F, 0x7E]), gf2.toUint8Array(px)) + const pabm = gf2.mod(px, pm) + t.compare(new Uint8Array([0x1]), gf2.toUint8Array(pabm)) +} + +/** + * @param {t.TestCase} _tc + */ +export const testIrreducibleSpread = _tc => { + const degree = 32 + const N = 1000 + const avgSpread = getSpreadAverage(degree, N) + const diffSpread = math.abs(avgSpread - degree) + t.info(`Average spread for degree ${degree} at ${N} repetitions: ${avgSpread}`) + t.assert(diffSpread < 3, 'Spread of irreducible polynomials is within expected range') +} + +/** + * @param {number} degree + * @param {number} tests + */ +const getSpreadAverage = (degree, tests) => { + const spreads = [] + for (let i = 0, test = 0, lastI = 0; test < tests; i++) { + const f = gf2.createRandom(degree) + t.assert(gf2.getHighestDegree(f) === degree) + if (gf2.isIrreducibleBenOr(f)) { + const spread = i - lastI + spreads.push(spread) + lastI = i + test++ + } + } + return array.fold(spreads, 0, math.add) / tests +} + +/** + * @param {t.TestCase} _tc + */ +export const testGenerateIrreducibles = _tc => { + /** + * @param {number} byteLen + */ + const testIrreducibleGen = byteLen => { + const K = byteLen * 8 + const irr = gf2.createIrreducible(K) + t.assert(gf2.getHighestDegree(irr) === K, 'degree equals K') + const irrBs = gf2.toUint8Array(irr) + console.log(`K = ${K}`, irrBs) + t.assert(irrBs[0] === 1) + t.assert(irrBs.byteLength === byteLen + 1) + } + testIrreducibleGen(1) + testIrreducibleGen(2) + testIrreducibleGen(4) + testIrreducibleGen(8) + testIrreducibleGen(16) + gf2.isIrreducibleBenOr(gf2.createFromBytes(rabin.StandardIrreducible8)) + gf2.isIrreducibleBenOr(gf2.createFromBytes(rabin.StandardIrreducible16)) + gf2.isIrreducibleBenOr(gf2.createFromBytes(rabin.StandardIrreducible32)) + gf2.isIrreducibleBenOr(gf2.createFromBytes(rabin.StandardIrreducible64)) + gf2.isIrreducibleBenOr(gf2.createFromBytes(rabin.StandardIrreducible128)) +} + +/** + * @param {t.TestCase} tc + * @param {number} K + */ +const _testFingerprintCompatiblityK = (tc, K) => { + /** + * @type {Array} + */ + const dataObjects = [] + const N = 300 + const MSIZE = 130 + t.info(`N=${N} K=${K} MSIZE=${MSIZE}`) + /** + * @type {gf2.GF2Polynomial} + */ + let irreducible + /** + * @type {Uint8Array} + */ + let irreducibleBuffer + t.measureTime(`find irreducible of ${K}`, () => { + irreducible = gf2.createIrreducible(K) + irreducibleBuffer = gf2.toUint8Array(irreducible) + }) + for (let i = 0; i < N; i++) { + dataObjects.push(prng.uint8Array(tc.prng, MSIZE)) + } + /** + * @type {Array} + */ + let fingerprints1 = [] + t.measureTime('polynomial direct', () => { + fingerprints1 = dataObjects.map((o, _index) => gf2.fingerprint(o, irreducible)) + }) + const testSet = new Set(fingerprints1.map(buffer.toBase64)) + t.assert(K < 32 || testSet.size === N) + /** + * @type {Array} + */ + let fingerprints2 = [] + t.measureTime('polynomial incremental', () => { + fingerprints2 = dataObjects.map((o, _index) => { + const encoder = new gf2.RabinPolynomialEncoder(irreducible) + for (let i = 0; i < o.byteLength; i++) { + encoder.write(o[i]) + } + return encoder.getFingerprint() + }) + }) + t.compare(fingerprints1, fingerprints2) + /** + * @type {Array} + */ + let fingerprints3 = [] + t.measureTime('polynomial incremental (efficent))', () => { + fingerprints3 = dataObjects.map((o, _index) => { + const encoder = new RabinUncachedEncoder(irreducibleBuffer) + for (let i = 0; i < o.byteLength; i++) { + encoder.write(o[i]) + } + return encoder.getFingerprint() + }) + }) + t.compare(fingerprints1, fingerprints3) + // ensuring that the cache is already populated + // @ts-ignore + // eslint-disable-next-line + new rabin.RabinEncoder(irreducibleBuffer) + /** + * @type {Array} + */ + let fingerprints4 = [] + t.measureTime('polynomial incremental (efficent & cached)) using encoder', () => { + fingerprints4 = dataObjects.map((o, _index) => { + const encoder = new rabin.RabinEncoder(irreducibleBuffer) + for (let i = 0; i < o.byteLength; i++) { + encoder.write(o[i]) + } + return encoder.getFingerprint() + }) + }) + t.compare(fingerprints1, fingerprints4) + /** + * @type {Array} + */ + let fingerprints5 = [] + t.measureTime('polynomial incremental (efficent & cached))', () => { + fingerprints5 = dataObjects.map((o, _index) => { + return rabin.fingerprint(irreducibleBuffer, o) + }) + }) + t.compare(fingerprints1, fingerprints5) +} + +/** + * @param {t.TestCase} tc + */ +export const testFingerprintCompatiblity = tc => { + _testFingerprintCompatiblityK(tc, 8) + _testFingerprintCompatiblityK(tc, 16) + _testFingerprintCompatiblityK(tc, 32) + _testFingerprintCompatiblityK(tc, 64) + _testFingerprintCompatiblityK(tc, 128) +} + +/** + * @param {t.TestCase} tc + */ +export const testConflicts = tc => { + /** + * @type {Array} + */ + const data = [] + const N = 100 + const Irr = rabin.StandardIrreducible8 + t.measureTime(`generate ${N} items`, () => { + for (let i = 0; i < N; i++) { + data.push(prng.uint8Array(tc.prng, prng.uint32(tc.prng, 5, 50))) + } + }) + /** + * @type {Map>} + */ + const results = new Map() + t.measureTime(`fingerprint ${N} items`, () => { + data.forEach(d => { + const f = buffer.toBase64(rabin.fingerprint(Irr, d)) + map.setIfUndefined(results, f, () => new Set()).add(buffer.toBase64(d)) + }) + }) + const conflicts = array.fold(map.map(results, (ds) => ds.size - 1), 0, math.add) + const usedFields = results.size + const unusedFieds = math.pow(2, (Irr.length - 1) * 8) - results.size + console.log({ conflicts, usedFields, unusedFieds }) +} diff --git a/number.js b/number.js index 575967e..bbc297a 100644 --- a/number.js +++ b/number.js @@ -18,3 +18,20 @@ export const HIGHEST_UINT32 = binary.BITS32 export const isInteger = Number.isInteger || (num => typeof num === 'number' && isFinite(num) && math.floor(num) === num) export const isNaN = Number.isNaN export const parseInt = Number.parseInt + +/** + * Count the number of "1" bits in an unsigned 32bit number. + * + * Super fun bitcount algorithm by Brian Kernighan. + * + * @param {number} n + */ +export const countBits = n => { + n &= binary.BITS32 + let count = 0 + while (n) { + n &= (n - 1) + count++ + } + return count +} diff --git a/number.test.js b/number.test.js index 0f6bdd2..bf8c42b 100644 --- a/number.test.js +++ b/number.test.js @@ -4,9 +4,9 @@ import * as random from './random.js' import * as math from './math.js' /** - * @param {t.TestCase} tc + * @param {t.TestCase} _tc */ -export const testNumber = tc => { +export const testNumber = _tc => { t.describe('isNaN') t.assert(number.isNaN(NaN)) t.assert(!number.isNaN(1 / 0)) @@ -18,6 +18,9 @@ export const testNumber = tc => { t.assert(!number.isInteger(NaN)) t.assert(number.isInteger(0)) t.assert(number.isInteger(-1)) + t.assert(number.countBits(1) === 1) + t.assert(number.countBits(3) === 2) + t.assert(number.countBits(128 + 3) === 3) } /** diff --git a/package.json b/package.json index 5570fef..c4be3d7 100644 --- a/package.json +++ b/package.json @@ -83,19 +83,25 @@ "types": "./crypto/aes-gcm.d.ts", "module": "./crypto/aes-gcm.js", "import": "./crypto/aes-gcm.js", - "require": "./dist/crypto/aes-gcm.cjs" + "require": "./dist/aes-gcm.cjs" }, "./crypto/ecdsa": { "types": "./crypto/ecdsa.d.ts", "module": "./crypto/ecdsa.js", "import": "./crypto/ecdsa.js", - "require": "./dist/crypto/ecdsa.cjs" + "require": "./dist/ecdsa.cjs" }, "./crypto/rsa-oaep": { "types": "./crypto/rsa-oaep.d.ts", "module": "./crypto/rsa-oaep.js", "import": "./crypto/rsa-oaep.js", - "require": "./dist/crypto/rsa-oaep.cjs" + "require": "./dist/rsa-oaep.cjs" + }, + "./hash/rabin": { + "types": "./hash/rabin.d.ts", + "module": "./hash/rabin.js", + "import": "./hash/rabin.js", + "require": "./dist/rabin.cjs" }, "./decoding.js": "./decoding.js", "./dist/decoding.cjs": "./dist/decoding.cjs", diff --git a/rollup.config.js b/rollup.config.js index 3fa4f52..6dea38c 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,6 +1,9 @@ import * as fs from 'fs' -const files = fs.readdirSync('./').filter(file => /(? fs.readdirSync(root).map(f => root + f)).flat().filter(file => /(?