diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..f4174523 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +node_modules/ +coverage/ +lib/ diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..863a250e --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,76 @@ +{ + "extends": ["eslint:recommended", "plugin:node/recommended", "prettier"], + "env": { + "node": true, + "es6": true + }, + "rules": { + "eqeqeq": [2, "smart"], + "no-caller": 2, + "dot-notation": 2, + "no-var": 2, + "prefer-const": 2, + "prefer-arrow-callback": [2, { "allowNamedFunctions": true }], + "arrow-body-style": [2, "as-needed"], + "object-shorthand": 2, + "prefer-template": 2, + "one-var": [2, "never"], + "prefer-destructuring": [2, { "object": true }], + "capitalized-comments": 2, + "multiline-comment-style": [2, "starred-block"], + "spaced-comment": 2, + "yoda": [2, "never"], + "curly": [2, "multi-line"], + "no-else-return": 2, + + "node/no-unsupported-features/es-syntax": [ + 2, + { "ignores": ["modules"] } + ] + }, + "overrides": [ + { + "files": "*.spec.*", + "env": { "jest": true } + }, + { + "files": "*.ts", + "extends": [ + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "prettier/@typescript-eslint" + ], + "parserOptions": { + "sourceType": "module", + "project": "./tsconfig.eslint.json" + }, + "settings": { + "node": { + "tryExtensions": [".js", ".json", ".node", ".ts"] + } + }, + "rules": { + "@typescript-eslint/prefer-for-of": 0, + "@typescript-eslint/member-ordering": 0, + "@typescript-eslint/explicit-function-return-type": 0, + "@typescript-eslint/no-unused-vars": 0, + "@typescript-eslint/no-use-before-define": [ + 2, + { "functions": false } + ], + "@typescript-eslint/consistent-type-definitions": [ + 2, + "interface" + ], + "@typescript-eslint/prefer-function-type": 2, + "@typescript-eslint/no-unnecessary-type-arguments": 2, + "@typescript-eslint/prefer-string-starts-ends-with": 2, + "@typescript-eslint/prefer-readonly": 2, + "@typescript-eslint/prefer-includes": 2, + "@typescript-eslint/no-unnecessary-condition": 2, + "@typescript-eslint/switch-exhaustiveness-check": 2, + "@typescript-eslint/prefer-nullish-coalescing": 2 + } + } + ] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f4174523 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +coverage/ +lib/ diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 39edf469..00000000 --- a/.npmignore +++ /dev/null @@ -1,2 +0,0 @@ -.travis.yml -tests.js diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..51f1879a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +node_modules/ +coverage/ +lib/ +src/maps/ diff --git a/.travis.yml b/.travis.yml index 8ef2696f..02026fc8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ language: node_js node_js: - - lts/* -sudo: false + - lts/* +after_success: npm run coverage diff --git a/LICENSE b/LICENSE index 46586fb9..cf9c1ffa 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013 Felix Böhm +Copyright (c) 2013-2020 Felix Böhm Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/index.js b/index.js deleted file mode 100644 index 1c8d5c56..00000000 --- a/index.js +++ /dev/null @@ -1,40 +0,0 @@ -function getByteSize (num) { - let out = num >> 3 - if (num % 8 !== 0) out++ - return out -} - -class BitField { - constructor (data = 0, opts) { - const grow = opts != null && opts.grow - this.grow = (grow && isFinite(grow) && getByteSize(grow)) || grow || 0 - this.buffer = typeof data === 'number' ? new Uint8Array(getByteSize(data)) : data - } - - get (i) { - const j = i >> 3 - return (j < this.buffer.length) && - !!(this.buffer[j] & (128 >> (i % 8))) - } - - set (i, b = true) { - const j = i >> 3 - if (b) { - if (this.buffer.length < j + 1) { - const length = Math.max(j + 1, Math.min(2 * this.buffer.length, this.grow)) - if (length <= this.grow) { - const newBuffer = new Uint8Array(length) - newBuffer.set(this.buffer) - this.buffer = newBuffer - } - } - // Set - this.buffer[j] |= 128 >> (i % 8) - } else if (j < this.buffer.length) { - // Clear - this.buffer[j] &= ~(128 >> (i % 8)) - } - } -} - -if (typeof module !== 'undefined') module.exports = BitField diff --git a/package.json b/package.json index d7d76bd7..85547338 100644 --- a/package.json +++ b/package.json @@ -1,29 +1,66 @@ { - "name": "bitfield", - "description": "a very simple bitfield implementation using buffers", - "version": "3.0.0", - "author": "Felix Boehm ", - "bugs": { - "url": "https://github.com/fb55/bitfield/issues" - }, - "devDependencies": { - "standard": "^15.0.0", - "tape": "^5.0.1" - }, - "engines": { - "node": ">=8" - }, - "keywords": [ - "bitfield", - "buffer" - ], - "license": "MIT", - "main": "index.js", - "repository": { - "type": "git", - "url": "https://github.com/fb55/bitfield" - }, - "scripts": { - "test": "standard && tape tests.js" - } + "name": "bitfield", + "description": "a simple bitfield, compliant with the BitTorrent spec", + "version": "3.0.0", + "author": "Felix Boehm ", + "funding": "https://github.com/sponsors/fb55", + "sideEffects": false, + "main": "lib/index.js", + "types": "lib/index.d.ts", + "directories": { + "lib": "lib/" + }, + "files": [ + "lib/**/*" + ], + "bugs": { + "url": "https://github.com/fb55/bitfield/issues" + }, + "devDependencies": { + "@types/jest": "^26.0.0", + "@types/node": "^14.11.8", + "@typescript-eslint/eslint-plugin": "^4.4.1", + "@typescript-eslint/parser": "^4.4.1", + "coveralls": "*", + "eslint": "^7.11.0", + "eslint-config-prettier": "^6.0.0", + "eslint-plugin-node": "^11.1.0", + "jest": "^26.5.3", + "prettier": "^2.0.5", + "ts-jest": "^26.1.0", + "typescript": "^4.0.2" + }, + "engines": { + "node": ">=8" + }, + "keywords": [ + "bitfield", + "buffer", + "bittorrent" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/fb55/bitfield" + }, + "scripts": { + "test": "jest --coverage && npm run lint", + "coverage": "cat coverage/lcov.info | coveralls", + "lint": "npm run lint:es && npm run lint:prettier", + "lint:es": "eslint .", + "lint:prettier": "npm run prettier -- --check", + "format": "npm run format:es && npm run format:prettier", + "format:es": "npm run lint:es -- --fix", + "format:prettier": "npm run prettier -- --write", + "prettier": "prettier '**/*.{js,ts,md,json,yml}'", + "build": "tsc", + "prepare": "npm run build" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "node" + }, + "prettier": { + "tabWidth": 4 + } } diff --git a/readme.md b/readme.md index 9a673185..538fdea5 100644 --- a/readme.md +++ b/readme.md @@ -1,29 +1,30 @@ # bitfield -a very simple bitfield, compliant with the Bittorrent spec +A simple bitfield, compliant with the BitTorrent spec. npm install bitfield #### Example ```js -var Bitfield = require("bitfield"); +import Bitfield from "bitfield"; -var field = new Bitfield(256); //create a bitfield with 256 bits +const field = new Bitfield(256); // Create a bitfield with 256 bits. -field.set(128); //set the 128th bit -field.set(128, true); //same as above +field.set(128); // Set the 128th bit. +field.set(128, true); // Same as above. -field.get(128); //true -field.get(200); //false (all values are initialised to `false`) -field.get(1e3); //false (out-of-bounds is also false) +field.get(128); // `true` +field.get(200); // `false` (all values are initialised to `false`) +field.get(1e3); // `false` (out-of-bounds is also false) -field.set(128, false); //set the 128th bit to 0 again +field.set(128, false); // Set the 128th bit to 0 again. -field.buffer; //the buffer used by bitfield +field.buffer; // The buffer used by the bitfield. ``` #### Methods + `Bitfield(data)`: `data` can be either a node.js buffer, WebGL Int8Array or numeric array, or a number representing the maximum number of supported bytes. `Bitfield#get(index)`: Returns a boolean indicating whether the bit is set. @@ -31,15 +32,13 @@ field.buffer; //the buffer used by bitfield `Bitfield#set(index[, value])`: `value` defaults to true. Sets the bit to `1` for a value of `true` or `0` for `false`. ##### Auto-grow mode -`Bitfield(data, { grow: size })`: If you `set` an index that is out-of-bounds, the Bitfield will automatically grow so that the bitfield is big enough to contain the given index, up to the given `size` (in bit). If you want the Bitfield to grow indefinitely, pass `Infinity` as the size. +`Bitfield(data, { grow: size })`: If you `set` an index that is out-of-bounds, the Bitfield will automatically grow so that the bitfield is big enough to contain the given index, up to the given `size` (in bit). If you want the Bitfield to grow indefinitely, pass `Infinity` as the size. #### Properties `Bitfield#buffer`: The contents of the bitfield. -`Bitfield#grow`: The passed growth option (defaults to `0`). - ## License MIT diff --git a/src/index.spec.ts b/src/index.spec.ts new file mode 100644 index 00000000..942feb40 --- /dev/null +++ b/src/index.spec.ts @@ -0,0 +1,155 @@ +import BitField from "./"; + +const data = "011011100110111".split("").map(Number).map(Boolean); + +describe("Bitfield", () => { + it("should be empty when initialized", () => { + const field = new BitField(data.length); + + for (let i = 0; i < data.length; i++) { + expect(field.get(i)).toBe(false); + } + }); + + // Write data + it("should reproduce written data", () => { + const field = new BitField(data.length); + + for (let i = 0; i < data.length; i++) { + field.set(i, data[i]); + } + + for (let i = 0; i < data.length; i++) { + expect(field.get(i)).toBe(data[i]); + } + }); + + it("out-of-bounds should simply be false", () => { + const field = new BitField(data.length); + + for (let i = data.length; i < 1e3; i++) { + expect(field.get(i)).toBe(false); + } + }); + + it("should not grow by default", () => { + const field = new BitField(data.length); + let index = 25; + + for (let i = 0; i < 100; i++) { + index += 8 + Math.floor(32 * Math.random()); + + const oldLength = field.buffer.length; + expect(field.get(index)).toBe(false); + + // Should not have grown for get() + expect(field.buffer).toHaveLength(oldLength); + + field.set(index, true); + + // Should not have grown for set() + expect(field.buffer).toHaveLength(oldLength); + expect(field.get(index)).toBe(false); + } + }); + + it("should be able to grow to infinity", () => { + const growField = new BitField(data.length, { grow: Infinity }); + let index = 25; + + for (let i = 0; i < 100; i++) { + index += 8 + Math.floor(32 * Math.random()); + + const oldLength = growField.buffer.length; + expect(growField.get(index)).toBe(false); + // Should not have grown for get() + expect(growField.buffer).toHaveLength(oldLength); + growField.set(index, true); + // Should have grown for set() + expect(growField.buffer.length).toBeGreaterThanOrEqual( + Math.ceil((index + 1) / 8) + ), + expect(growField.get(index)).toBe(true); + } + }); + + it("should restrict growth to growth option", () => { + const smallGrowField = new BitField(0, { grow: 50 }); + + for (let i = 0; i < 100; i++) { + const oldLength = smallGrowField.buffer.length; + smallGrowField.set(i, true); + if (i <= 55) { + // Should have grown for set() + expect(smallGrowField.buffer.length).toBeGreaterThanOrEqual( + (i >> 3) + 1 + ); + expect(smallGrowField.get(i)).toBe(true); + } else { + // Should not have grown for set() + expect(smallGrowField.buffer).toHaveLength(oldLength); + expect(smallGrowField.get(i)).toBe(false); + } + } + }); + + it("should assume size 0 if no data or size passed in", () => { + const field2 = new BitField(); + expect(field2.buffer).not.toBeNull(); + }); + + it("should accept a typed array as input", () => { + const original = new BitField(0, { grow: 100 }); + original.set(15); + const copy = new BitField(original.buffer); + expect(copy.get(15)).toBe(true); + }); + + it("should support disabling a field", () => { + const field = new BitField(0, { grow: 100 }); + field.set(3, true); + expect(field.get(3)).toBe(true); + field.set(3, false); + + // Check the first 10 indices, to ensure we only mutated a single field + for (let i = 0; i < 10; i++) { + expect(field.get(i)).toBe(false); + } + + // Set the first 10 fields, then disable one + for (let i = 0; i < 10; i++) { + field.set(i); + } + + field.set(5, false); + for (let i = 0; i < 10; i++) { + expect(field.get(i)).toBe(i !== 5); + } + }); + + it("should ignore disables out of bounds", () => { + const field = new BitField(0, { grow: 100 }); + field.set(3, false); + expect(field.buffer).toHaveLength(0); + }); + + it("correct size bitfield", () => { + expect(new BitField(1).buffer).toHaveLength(1); + expect(new BitField(2).buffer).toHaveLength(1); + expect(new BitField(3).buffer).toHaveLength(1); + expect(new BitField(4).buffer).toHaveLength(1); + expect(new BitField(5).buffer).toHaveLength(1); + expect(new BitField(6).buffer).toHaveLength(1); + expect(new BitField(7).buffer).toHaveLength(1); + expect(new BitField(8).buffer).toHaveLength(1); + expect(new BitField(9).buffer).toHaveLength(2); + expect(new BitField(10).buffer).toHaveLength(2); + expect(new BitField(11).buffer).toHaveLength(2); + expect(new BitField(12).buffer).toHaveLength(2); + expect(new BitField(13).buffer).toHaveLength(2); + expect(new BitField(14).buffer).toHaveLength(2); + expect(new BitField(15).buffer).toHaveLength(2); + expect(new BitField(16).buffer).toHaveLength(2); + expect(new BitField(17).buffer).toHaveLength(3); + }); +}); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..e0a099be --- /dev/null +++ b/src/index.ts @@ -0,0 +1,82 @@ +function getByteSize(num: number): number { + let out = num >> 3; + if (num % 8 !== 0) out++; + return out; +} + +interface BitFieldOptions { + /** + * If you `set` an index that is out-of-bounds, the bitfield + * will automatically grow so that the bitfield is big enough + * to contain the given index, up to the given size (in bit). + * + * If you want the Bitfield to grow indefinitely, pass `Infinity`. + * + * @default 0. + */ + grow?: number; +} + +export default class BitField { + /** + * Grow the bitfield up to this number of entries. + * @default 0. + */ + private readonly grow: number; + /** The internal storage of the bitfield. */ + public buffer: Uint8Array; + + /** + * + * + * @param data Either a number representing the maximum number of supported bytes, or an Int8Array. + * @param opts Options for the bitfield. + */ + constructor(data: number | Uint8Array = 0, opts?: BitFieldOptions) { + const grow = opts?.grow; + this.grow = (grow && isFinite(grow) && getByteSize(grow)) || grow || 0; + this.buffer = + typeof data === "number" ? new Uint8Array(getByteSize(data)) : data; + } + + /** + * Get a particular bit. + * + * @param i Bit index to retrieve. + * @returns A boolean indicating whether the `i`th bit is set. + */ + get(i: number): boolean { + const j = i >> 3; + return j < this.buffer.length && !!(this.buffer[j] & (128 >> i % 8)); + } + + /** + * Set a particular bit. + * + * Will grow the underlying array if the bit is out of bounds and the `grow` option is set. + * + * @param i Bit index to set. + * @param value Value to set the bit to. Defaults to `true`. + */ + set(i: number, value = true): void { + const j = i >> 3; + if (value) { + if (this.buffer.length < j + 1) { + const length = Math.max( + j + 1, + Math.min(2 * this.buffer.length, this.grow) + ); + if (length <= this.grow) { + const newBuffer = new Uint8Array(length); + newBuffer.set(this.buffer); + this.buffer = newBuffer; + } + } + // Set + this.buffer[j] |= 128 >> i % 8; + } else if (j < this.buffer.length) { + // Clear + this.buffer[j] &= ~(128 >> i % 8); + } + } +} diff --git a/tests.js b/tests.js deleted file mode 100644 index 271c7dcf..00000000 --- a/tests.js +++ /dev/null @@ -1,119 +0,0 @@ -var test = require('tape') -var BitField = require('./') - -var data = '011011100110111'.split('').map(Number).map(Boolean) -var field = new BitField(data.length) - -test('bitfield should be empty when initialized', function (t) { - for (var i = 0; i < data.length; i++) { - t.strictEqual(field.get(i), false) - } - - t.end() -}) - -// Write data - -test('should reproduce written data', function (t) { - var i - for (i = 0; i < data.length; i++) { - field.set(i, data[i]) - } - - for (i = 0; i < data.length; i++) { - t.strictEqual(field.get(i), data[i]) - } - - t.end() -}) - -test('out-of-bounds should simply be false', function (t) { - for (var i = data.length; i < 1e3; i++) { - t.strictEqual(field.get(i), false) - } - - t.end() -}) - -test('should not grow by default', function (t) { - var index = 25 - - for (var i = 0; i < 100; i++) { - index += 8 + Math.floor(32 * Math.random()) - - var oldLength = field.buffer.length - t.strictEqual(field.get(index), false) - t.equal(field.buffer.length, oldLength, 'should not have grown for get()') - field.set(index, true) - - t.equal(field.buffer.length, oldLength, 'should not have grown for set()') - t.strictEqual(field.get(index), false) - } - - t.end() -}) - -test('should be able to grow to infinity', function (t) { - var growField = new BitField(data.length, { grow: Infinity }) - var index = 25 - - for (var i = 0; i < 100; i++) { - index += 8 + Math.floor(32 * Math.random()) - - var oldLength = growField.buffer.length - t.strictEqual(growField.get(index), false) - t.equal(growField.buffer.length, oldLength, 'should not have grown for get()') - growField.set(index, true) - var newLength = Math.ceil((index + 1) / 8) - t.ok(growField.buffer.length >= newLength, 'should have grown for set()') - t.strictEqual(growField.get(index), true) - } - - t.end() -}) - -test('should restrict growth to growth option', function (t) { - var smallGrowField = new BitField(0, { grow: 50 }) - - for (var i = 0; i < 100; i++) { - var oldLength = smallGrowField.buffer.length - smallGrowField.set(i, true) - if (i <= 55) { - t.ok(smallGrowField.buffer.length >= (i >> 3) + 1, 'should have grown for set()') - t.strictEqual(smallGrowField.get(i), true) - } else { - t.equal(smallGrowField.buffer.length, oldLength, 'should not have grown for set()') - t.strictEqual(smallGrowField.get(i), false, i + ' bitfield ' + smallGrowField.buffer.length) - } - } - - t.end() -}) - -test('if no data or size passed in, should assume size 0', function (t) { - var field2 = new BitField() - t.ok(field2.buffer) - - t.end() -}) - -test('correct size bitfield', function (t) { - t.equal(new BitField(1).buffer.length, 1) - t.equal(new BitField(2).buffer.length, 1) - t.equal(new BitField(3).buffer.length, 1) - t.equal(new BitField(4).buffer.length, 1) - t.equal(new BitField(5).buffer.length, 1) - t.equal(new BitField(6).buffer.length, 1) - t.equal(new BitField(7).buffer.length, 1) - t.equal(new BitField(8).buffer.length, 1) - t.equal(new BitField(9).buffer.length, 2) - t.equal(new BitField(10).buffer.length, 2) - t.equal(new BitField(11).buffer.length, 2) - t.equal(new BitField(12).buffer.length, 2) - t.equal(new BitField(13).buffer.length, 2) - t.equal(new BitField(14).buffer.length, 2) - t.equal(new BitField(15).buffer.length, 2) - t.equal(new BitField(16).buffer.length, 2) - t.equal(new BitField(17).buffer.length, 3) - t.end() -}) diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 00000000..c1ec5444 --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src", "scripts"], + "exclude": [] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..5d16dc75 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + /* Basic Options */ + "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, + // "lib": [], /* Specify library files to be included in the compilation. */ + "declaration": true /* Generates corresponding '.d.ts' file. */, + "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */, + // "sourceMap": true, /* Generates corresponding '.map' file. */ + "outDir": "lib" /* Redirect output structure to the directory. */, + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + + /* Additional Checks */ + "noUnusedLocals": true /* Report errors on unused locals. */, + "noUnusedParameters": true /* Report errors on unused parameters. */, + "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, + "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, + + /* Module Resolution Options */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + "resolveJsonModule": true + }, + "include": ["src"], + "exclude": [ + "**/*.spec.ts", + "**/__fixtures__/*", + "**/__tests__/*", + "**/__snapshots__/*" + ] +}