├── .gitignore ├── AUTHORS ├── benchmark ├── test.torrent ├── buffer-vs-string.js ├── compare-encode.js ├── encoding-length.js ├── compare-decode.js └── bencode.js ├── .npmignore ├── test ├── data │ └── announce-compacted-peers.bin ├── index.html ├── data.js ├── null-values.test.js ├── specifications.md ├── BEP-0023.test.js ├── abstract-encoding.test.js ├── encoding-length.test.js ├── decode.utf8.test.js ├── decode.buffer.test.js └── encode.test.js ├── index.js ├── .editorconfig ├── CONTRIBUTORS ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── lib ├── util.js ├── encoding-length.js ├── encode.js └── decode.js ├── LICENSE.md ├── package.json ├── README.md └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | package-lock.json 4 | pnpm-lock.yaml 5 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Mark Schmale 2 | Jonas Hermsmeier 3 | -------------------------------------------------------------------------------- /benchmark/test.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webtorrent/node-bencode/HEAD/benchmark/test.torrent -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.log 3 | .github/ 4 | AUTHORS 5 | Makefile 6 | benchmark 7 | dist 8 | test 9 | .editorconfig 10 | -------------------------------------------------------------------------------- /test/data/announce-compacted-peers.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webtorrent/node-bencode/HEAD/test/data/announce-compacted-peers.bin -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Bencode 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/data.js: -------------------------------------------------------------------------------- 1 | export default { 2 | binKeyData: Buffer.from('ZDU6ZmlsZXNkMzY6N++/vVXvv73go5rvv71L77+9z6fXlu+/ve+/ve+/ve+/vSR3ZDg6Y29tcGxldGVpMGUxMDpkb3dubG9hZGVkaTEwZTEwOmluY29tcGxldGVpMGVlZWU=', 'base64'), 3 | binKeyName: Buffer.from('N++/vVXvv73go5rvv71L77+9z6fXlu+/ve+/ve+/ve+/vSR3', 'base64'), 4 | binResultData: Buffer.from('NzrDtsKxc2Rm', 'base64'), 5 | binStringData: Buffer.from('w7bCsXNkZg==', 'base64') 6 | } 7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import encode from './lib/encode.js' 2 | import decode from './lib/decode.js' 3 | import byteLength from './lib/encoding-length.js' 4 | /** 5 | * Determines the amount of bytes 6 | * needed to encode the given value 7 | * @param {Object|Array|Uint8Array|String|Number|Boolean} value 8 | * @return {Number} byteCount 9 | */ 10 | const encodingLength = byteLength 11 | export default { encode, decode, byteLength, encodingLength } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # See http://editorconfig.org 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | indent_style = space 11 | indent_size = 2 12 | charset = utf-8 13 | 14 | [*.{md,mkd,markdown}] 15 | trim_trailing_whitespace = false 16 | 17 | [Makefile] 18 | indent_style = tab 19 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # Just a plain list of people who contributed to this project. 2 | # People who are already listed in AUTHORS are not listed again. 3 | 4 | Conrad Pankoff 5 | Patrick Williams 6 | Jeffrey Hwang 7 | Sean Lang 8 | Nicolas Gotchac 9 | Feross Aboukhadijeh 10 | Nazar Mokrynskyi 11 | Jimmy Wärting 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: [push,pull_request] 3 | jobs: 4 | test: 5 | name: Node ${{ matrix.node }} / ${{ matrix.os }} 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | os: 11 | - ubuntu-latest 12 | node: 13 | - '12' 14 | - '14' 15 | - '16' 16 | - '18' 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node }} 23 | - run: npm install 24 | - run: npm run build --if-present 25 | - run: npm test 26 | -------------------------------------------------------------------------------- /benchmark/buffer-vs-string.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import bencode from '../index.js' 4 | import bench from 'nanobench' 5 | 6 | const buffer = fs.readFileSync(path.join(__dirname, 'test.torrent')) 7 | const str = buffer.toString('ascii') 8 | 9 | const ITERATIONS = 10000 10 | 11 | bench(`decode buffer ⨉ ${ITERATIONS}`, function (run) { 12 | let result = null 13 | 14 | run.start() 15 | for (let i = 0; i < ITERATIONS; i++) { 16 | result = bencode.decode(buffer) 17 | } 18 | run.end() 19 | 20 | return result 21 | }) 22 | 23 | bench(`decode string ⨉ ${ITERATIONS}`, function (run) { 24 | let result = null 25 | 26 | run.start() 27 | for (let i = 0; i < ITERATIONS; i++) { 28 | result = bencode.decode(str) 29 | } 30 | run.end() 31 | 32 | return result 33 | }) 34 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | export function digitCount (value) { 2 | // Add a digit for negative numbers, as the sign will be prefixed 3 | const sign = value < 0 ? 1 : 0 4 | // Guard against negative numbers & zero going into log10(), 5 | // as that would return -Infinity 6 | value = Math.abs(Number(value || 1)) 7 | return Math.floor(Math.log10(value)) + 1 + sign 8 | } 9 | 10 | export function getType (value) { 11 | if (ArrayBuffer.isView(value)) return 'arraybufferview' 12 | if (Array.isArray(value)) return 'array' 13 | if (value instanceof Number) return 'number' 14 | if (value instanceof Boolean) return 'boolean' 15 | if (value instanceof Set) return 'set' 16 | if (value instanceof Map) return 'map' 17 | if (value instanceof String) return 'string' 18 | if (value instanceof ArrayBuffer) return 'arraybuffer' 19 | return typeof value 20 | } 21 | -------------------------------------------------------------------------------- /test/null-values.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import bencode from '../index.js' 3 | 4 | test('Data with null values', function (t) { 5 | t.test('should return an empty value when encoding either null or undefined', function (t) { 6 | t.plan(2) 7 | t.deepEqual(bencode.encode(null), new Uint8Array()) 8 | t.deepEqual(bencode.encode(undefined), new Uint8Array()) 9 | }) 10 | 11 | t.test('should return null when decoding an empty value', function (t) { 12 | t.plan(2) 13 | t.deepEqual(bencode.decode(Buffer.allocUnsafe(0)), null) 14 | t.deepEqual(bencode.decode(''), null) 15 | }) 16 | 17 | t.test('should omit null values when encoding', function (t) { 18 | const data = [{ empty: null }, { notset: undefined }, null, undefined, 0] 19 | const result = bencode.decode(bencode.encode(data)) 20 | const expected = [{}, {}, 0] 21 | t.plan(1) 22 | t.deepEqual(result, expected) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /test/specifications.md: -------------------------------------------------------------------------------- 1 | _Strings_ are length-prefixed base ten followed by a colon and the string. For example 4:spam corresponds to 'spam'. 2 | 3 | _Integers_ are represented by an 'i' followed by the number in base 10 followed by an 'e'. For example i3e corresponds to 3 and i-3e corresponds to -3. Integers have no size limitation. i-0e is invalid. All encodings with a leading zero, such as i03e, are invalid, other than i0e, which of course corresponds to 0. 4 | 5 | _Lists_ are encoded as an 'l' followed by their elements (also bencoded) followed by an 'e'. For example l4:spam4:eggse corresponds to ['spam', 'eggs']. 6 | 7 | _Dictionaries_ are encoded as a 'd' followed by a list of alternating keys and their corresponding values followed by an 'e'. For example, d3:cow3:moo4:spam4:eggse corresponds to {'cow': 'moo', 'spam': 'eggs'} and d4:spaml1:a1:bee corresponds to {'spam': ['a', 'b']}. 8 | Keys must be strings and appear in sorted order (sorted as raw strings, not alphanumerics). 9 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | with: 16 | persist-credentials: false 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 18 21 | - name: Cache 22 | uses: actions/cache@v3 23 | with: 24 | path: ~/.npm 25 | key: ${{ runner.os }}-npm-${{ hashFiles('**/package.json') }} 26 | restore-keys: | 27 | ${{ runner.os }}-npm- 28 | - name: Install dependencies 29 | run: npm i 30 | env: 31 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | - name: Release 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 35 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | run: npx semantic-release 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright (c) 2010 Mark Schmale 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a 7 | copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included 15 | in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 18 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /test/BEP-0023.test.js: -------------------------------------------------------------------------------- 1 | import path, { dirname } from 'path' 2 | import fs from 'fs' 3 | import test from 'tape' 4 | import bencode from '../index.js' 5 | import { fileURLToPath } from 'url' 6 | 7 | const __filename = fileURLToPath(import.meta.url) 8 | const __dirname = dirname(__filename) 9 | 10 | // @see http://www.bittorrent.org/beps/bep_0023.html 11 | test('BEP 0023', function (t) { 12 | t.test('should be able to handle an compacted peer announce', function (t) { 13 | const filename = path.join(__dirname, 'data', 'announce-compacted-peers.bin') 14 | const announce = fs.readFileSync(filename) 15 | const data = bencode.decode(announce) 16 | 17 | t.plan(1) 18 | t.deepEqual(data, { 19 | complete: 4, 20 | incomplete: 3, 21 | interval: 1800, 22 | 'min interval': 1800, 23 | peers: new Uint8Array(Buffer.from('2ebd1b641a1f51d54c0546cc342190401a1f626ee9c6c8d5cb0d92131a1fac4e689a3c6b180f3d5746db', 'hex')) 24 | }) 25 | }) 26 | 27 | t.test('should be able to handle an compacted peer announce when decoding strings', function (t) { 28 | const filename = path.join(__dirname, 'data', 'announce-compacted-peers.bin') 29 | const announce = fs.readFileSync(filename) 30 | const data = bencode.decode(announce, 'utf8') 31 | 32 | t.plan(1) 33 | t.deepEqual(data, { 34 | complete: 4, 35 | incomplete: 3, 36 | interval: 1800, 37 | 'min interval': 1800, 38 | peers: '.�\u001bd\u001a\u001fQ�L\u0005F�4!�@\u001a\u001fbn�����\r�\u0013\u001a\u001f�Nh�)', function (run) { 60 | const result = null 61 | 62 | run.start() 63 | for (let i = 0; i < ITERATIONS; i++) { 64 | bencode.encodingLength([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) 65 | } 66 | run.end() 67 | 68 | return result 69 | }) 70 | 71 | bench('bencode.encodingLength(small object)', function (run) { 72 | const result = null 73 | 74 | run.start() 75 | for (let i = 0; i < ITERATIONS; i++) { 76 | bencode.encodingLength({ a: 1, b: 'c', d: 'abcdefg', e: [1, 2, 3] }) 77 | } 78 | run.end() 79 | 80 | return result 81 | }) 82 | -------------------------------------------------------------------------------- /benchmark/compare-decode.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import bench from 'nanobench' 4 | 5 | import bencode from '../index.js' 6 | import bencoding from 'bencoding' 7 | import bncode from 'bncode' 8 | import btparse from 'btparse' 9 | import dht from 'dht.js/lib/dht/bencode' 10 | import dhtBencode from 'dht-bencode' 11 | 12 | const buffer = fs.readFileSync(path.join(__dirname, 'test.torrent')) 13 | 14 | const ITERATIONS = 10000 15 | 16 | bench(`bencode.decode() ⨉ ${ITERATIONS}`, function (run) { 17 | let result = null 18 | 19 | run.start() 20 | for (let i = 0; i < ITERATIONS; i++) { 21 | result = bencode.decode(buffer) 22 | } 23 | run.end() 24 | 25 | return result 26 | }) 27 | 28 | bench(`bencoding.decode() ⨉ ${ITERATIONS}`, function (run) { 29 | let result = null 30 | 31 | run.start() 32 | for (let i = 0; i < ITERATIONS; i++) { 33 | result = bencoding.decode(buffer) 34 | } 35 | run.end() 36 | 37 | return result 38 | }) 39 | 40 | bench(`bncode.decode() ⨉ ${ITERATIONS}`, function (run) { 41 | let result = null 42 | 43 | run.start() 44 | for (let i = 0; i < ITERATIONS; i++) { 45 | result = bncode.decode(buffer) 46 | } 47 | run.end() 48 | 49 | return result 50 | }) 51 | 52 | bench(`btparse() ⨉ ${ITERATIONS}`, function (run) { 53 | let result = null 54 | 55 | run.start() 56 | for (let i = 0; i < ITERATIONS; i++) { 57 | result = btparse(buffer) 58 | } 59 | run.end() 60 | 61 | return result 62 | }) 63 | 64 | bench(`dht.decode() ⨉ ${ITERATIONS}`, function (run) { 65 | let result = null 66 | 67 | run.start() 68 | for (let i = 0; i < ITERATIONS; i++) { 69 | result = dht.decode(buffer) 70 | } 71 | run.end() 72 | 73 | return result 74 | }) 75 | 76 | bench(`dhtBencode.decode() ⨉ ${ITERATIONS}`, function (run) { 77 | let result = null 78 | 79 | run.start() 80 | for (let i = 0; i < ITERATIONS; i++) { 81 | result = dhtBencode.bdecode(buffer) 82 | } 83 | run.end() 84 | 85 | return result 86 | }) 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bencode", 3 | "description": "Bencode de/encoder", 4 | "version": "4.0.0", 5 | "bugs": { 6 | "url": "https://github.com/webtorrent/node-bencode/issues" 7 | }, 8 | "type": "module", 9 | "contributors": [ 10 | { 11 | "name": "Mark Schmale", 12 | "email": "masch@masch.it", 13 | "url": "http://masch.it/" 14 | }, 15 | { 16 | "name": "Jonas Hermsmeier", 17 | "email": "jhermsmeier@gmail.com", 18 | "url": "https://jhermsmeier.de/" 19 | } 20 | ], 21 | "devDependencies": { 22 | "@webtorrent/semantic-release-config": "1.0.10", 23 | "bencoding": "latest", 24 | "bncode": "latest", 25 | "browserify": "^17.0.0", 26 | "btparse": "latest", 27 | "dht-bencode": "latest", 28 | "dht.js": "latest", 29 | "nanobench": "3.0.0", 30 | "semantic-release": "21.1.2", 31 | "standard": "17.1.0", 32 | "tap-spec": "5.0.0", 33 | "tape": "5.9.0" 34 | }, 35 | "keywords": [ 36 | "bdecode", 37 | "bencode", 38 | "bencoding", 39 | "bittorrent", 40 | "torrent" 41 | ], 42 | "license": "MIT", 43 | "engines": { 44 | "node": ">=12.20.0" 45 | }, 46 | "exports": { 47 | "import": "./index.js" 48 | }, 49 | "repository": { 50 | "type": "git", 51 | "url": "git://github.com/webtorrent/node-bencode.git" 52 | }, 53 | "scripts": { 54 | "benchmark": "nanobench benchmark/*.js", 55 | "bundle": "mkdir -p dist && npm run bundle:lib && npm run bundle:test", 56 | "bundle:lib": "browserify lib/index.js -s bencode -o dist/bencode.js", 57 | "bundle:test": "browserify test/*.test.js -o dist/tests.js", 58 | "style": "standard --fix", 59 | "test": "standard && tape test/*.test.js | tap-spec" 60 | }, 61 | "renovate": { 62 | "extends": [ 63 | "github>webtorrent/renovate-config" 64 | ] 65 | }, 66 | "release": { 67 | "extends": "@webtorrent/semantic-release-config" 68 | }, 69 | "dependencies": { 70 | "uint8-util": "^2.2.2" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/encoding-length.js: -------------------------------------------------------------------------------- 1 | import { text2arr } from 'uint8-util' 2 | import { digitCount, getType } from './util.js' 3 | 4 | function listLength (list) { 5 | let length = 1 + 1 // type marker + end-of-type marker 6 | 7 | for (const value of list) { 8 | length += encodingLength(value) 9 | } 10 | 11 | return length 12 | } 13 | 14 | function mapLength (map) { 15 | let length = 1 + 1 // type marker + end-of-type marker 16 | 17 | for (const [key, value] of map) { 18 | const keyLength = text2arr(key).byteLength 19 | length += digitCount(keyLength) + 1 + keyLength 20 | length += encodingLength(value) 21 | } 22 | 23 | return length 24 | } 25 | 26 | function objectLength (value) { 27 | let length = 1 + 1 // type marker + end-of-type marker 28 | const keys = Object.keys(value) 29 | 30 | for (let i = 0; i < keys.length; i++) { 31 | const keyLength = text2arr(keys[i]).byteLength 32 | length += digitCount(keyLength) + 1 + keyLength 33 | length += encodingLength(value[keys[i]]) 34 | } 35 | 36 | return length 37 | } 38 | 39 | function stringLength (value) { 40 | const length = text2arr(value).byteLength 41 | return digitCount(length) + 1 + length 42 | } 43 | 44 | function arrayBufferLength (value) { 45 | const length = value.byteLength - value.byteOffset 46 | return digitCount(length) + 1 + length 47 | } 48 | 49 | function encodingLength (value) { 50 | const length = 0 51 | 52 | if (value == null) return length 53 | 54 | const type = getType(value) 55 | 56 | switch (type) { 57 | case 'arraybufferview': return arrayBufferLength(value) 58 | case 'string': return stringLength(value) 59 | case 'array': case 'set': return listLength(value) 60 | case 'number': return 1 + digitCount(Math.floor(value)) + 1 61 | case 'bigint': return 1 + value.toString().length + 1 62 | case 'object': return objectLength(value) 63 | case 'map': return mapLength(value) 64 | default: 65 | throw new TypeError(`Unsupported value of type "${type}"`) 66 | } 67 | } 68 | 69 | export default encodingLength 70 | -------------------------------------------------------------------------------- /test/abstract-encoding.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import bencode from '../index.js' 3 | 4 | test('abstract encoding', function (t) { 5 | t.test('encodingLength( value )', function (t) { 6 | const input = { string: 'Hello World', integer: 12345 } 7 | const output = Buffer.from('d7:integeri12345e6:string11:Hello Worlde') 8 | t.plan(1) 9 | t.equal(bencode.encodingLength(input), output.length) 10 | }) 11 | 12 | t.test('encode.bytes', function (t) { 13 | const output = bencode.encode({ string: 'Hello World', integer: 12345 }) 14 | t.plan(1) 15 | t.equal(output.length, bencode.encode.bytes) 16 | }) 17 | 18 | t.test('encode into an existing buffer', function (t) { 19 | const input = { string: 'Hello World', integer: 12345 } 20 | const output = Buffer.from('d7:integeri12345e6:string11:Hello Worlde') 21 | const target = Buffer.allocUnsafe(output.length) 22 | bencode.encode(input, target) 23 | t.plan(1) 24 | t.deepEqual(target, output) 25 | }) 26 | 27 | t.test('encode into a buffer with an offset', function (t) { 28 | const input = { string: 'Hello World', integer: 12345 } 29 | const output = Buffer.from('d7:integeri12345e6:string11:Hello Worlde') 30 | const target = Buffer.allocUnsafe(64 + output.length) // Pad with 64 bytes 31 | const offset = 48 32 | bencode.encode(input, target, offset) 33 | t.plan(1) 34 | t.deepEqual(target.slice(offset, offset + output.length), output) 35 | }) 36 | 37 | t.test('decode.bytes', function (t) { 38 | const input = Buffer.from('d7:integeri12345e6:string11:Hello Worlde') 39 | bencode.decode(input) 40 | t.plan(1) 41 | t.equal(bencode.decode.bytes, input.length) 42 | }) 43 | 44 | t.test('decode from an offset', function (t) { 45 | const pad = '_______________________________' 46 | const input = Buffer.from(pad + 'd7:integeri12345e6:string11:Hello Worlde') 47 | const output = bencode.decode(input, pad.length, 'utf8') 48 | t.plan(1) 49 | t.deepEqual(output, { string: 'Hello World', integer: 12345 }) 50 | }) 51 | 52 | t.test('decode between an offset and end', function (t) { 53 | const pad = '_______________________________' 54 | const data = 'd7:integeri12345e6:string11:Hello Worlde' 55 | const input = Buffer.from(pad + data + pad) 56 | const output = bencode.decode(input, pad.length, pad.length + data.length, 'utf8') 57 | t.plan(1) 58 | t.deepEqual(output, { string: 'Hello World', integer: 12345 }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /benchmark/bencode.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import bench from 'nanobench' 4 | 5 | import bencode from '../index.js' 6 | 7 | const buffer = fs.readFileSync(path.join(__dirname, 'test.torrent')) 8 | const object = bencode.decode(buffer) 9 | const objectUtf8 = bencode.decode(buffer, 'utf8') 10 | const objectAscii = bencode.decode(buffer, 'ascii') 11 | const objectBinary = bencode.decode(buffer, 'binary') 12 | 13 | const ITERATIONS = 10000 14 | 15 | bench(`bencode.encode() [buffer] ⨉ ${ITERATIONS}`, function (run) { 16 | let result = null 17 | 18 | run.start() 19 | for (let i = 0; i < ITERATIONS; i++) { 20 | result = bencode.encode(object) 21 | } 22 | run.end() 23 | 24 | return result 25 | }) 26 | 27 | bench(`bencode.encode() [utf8] ⨉ ${ITERATIONS}`, function (run) { 28 | let result = null 29 | 30 | run.start() 31 | for (let i = 0; i < ITERATIONS; i++) { 32 | result = bencode.encode(objectUtf8) 33 | } 34 | run.end() 35 | 36 | return result 37 | }) 38 | 39 | bench(`bencode.encode() [ascii] ⨉ ${ITERATIONS}`, function (run) { 40 | let result = null 41 | 42 | run.start() 43 | for (let i = 0; i < ITERATIONS; i++) { 44 | result = bencode.encode(objectAscii) 45 | } 46 | run.end() 47 | 48 | return result 49 | }) 50 | 51 | bench(`bencode.encode() [binary] ⨉ ${ITERATIONS}`, function (run) { 52 | let result = null 53 | 54 | run.start() 55 | for (let i = 0; i < ITERATIONS; i++) { 56 | result = bencode.encode(objectBinary) 57 | } 58 | run.end() 59 | 60 | return result 61 | }) 62 | 63 | bench(`bencode.decode() [buffer] ⨉ ${ITERATIONS}`, function (run) { 64 | let result = null 65 | 66 | run.start() 67 | for (let i = 0; i < ITERATIONS; i++) { 68 | result = bencode.decode(buffer) 69 | } 70 | run.end() 71 | 72 | return result 73 | }) 74 | 75 | bench(`bencode.decode() [utf8] ⨉ ${ITERATIONS}`, function (run) { 76 | let result = null 77 | 78 | run.start() 79 | for (let i = 0; i < ITERATIONS; i++) { 80 | result = bencode.decode(buffer, 'utf8') 81 | } 82 | run.end() 83 | 84 | return result 85 | }) 86 | 87 | bench(`bencode.decode() [ascii] ⨉ ${ITERATIONS}`, function (run) { 88 | let result = null 89 | 90 | run.start() 91 | for (let i = 0; i < ITERATIONS; i++) { 92 | result = bencode.decode(buffer, 'ascii') 93 | } 94 | run.end() 95 | 96 | return result 97 | }) 98 | 99 | bench(`bencode.decode() [binary] ⨉ ${ITERATIONS}`, function (run) { 100 | let result = null 101 | 102 | run.start() 103 | for (let i = 0; i < ITERATIONS; i++) { 104 | result = bencode.decode(buffer, 'binary') 105 | } 106 | run.end() 107 | 108 | return result 109 | }) 110 | -------------------------------------------------------------------------------- /test/encoding-length.test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path, { dirname } from 'path' 3 | import test from 'tape' 4 | import bencode from '../index.js' 5 | import { fileURLToPath } from 'url' 6 | 7 | const __filename = fileURLToPath(import.meta.url) 8 | const __dirname = dirname(__filename) 9 | 10 | const torrent = fs.readFileSync( 11 | path.join(__dirname, '..', 'benchmark', 'test.torrent') 12 | ) 13 | 14 | test('encoding-length', function (t) { 15 | t.test('torrent', function (t) { 16 | const value = bencode.decode(torrent) 17 | const length = bencode.encodingLength(value) 18 | t.plan(1) 19 | t.equal(length, torrent.length) 20 | }) 21 | 22 | t.test('returns correct length for empty dictionaries', function (t) { 23 | t.plan(2) 24 | t.equal(bencode.encodingLength({}), 2) // de 25 | t.equal(bencode.encodingLength(new Map()), 2) // de 26 | }) 27 | 28 | t.test('returns correct length for dictionaries', function (t) { 29 | t.plan(2) 30 | const obj = { a: 1, b: 'str', c: { de: 'f' } } 31 | const map = new Map([ 32 | ['a', 1], 33 | ['b', 'str'], 34 | ['c', { de: 'f' }] 35 | ]) 36 | t.equal(bencode.encodingLength(obj), 28) // d1:ai1e1:b3:str1:cd2:de1:fee 37 | t.equal(bencode.encodingLength(map), 28) // d1:ai1e1:b3:str1:cd2:de1:fee 38 | }) 39 | 40 | t.test('returns correct length for empty lists', function (t) { 41 | t.plan(2) 42 | t.equal(bencode.encodingLength([]), 2) // le 43 | t.equal(bencode.encodingLength(new Set()), 2) // le 44 | }) 45 | 46 | t.test('returns correct length for lists', function (t) { 47 | t.plan(3) 48 | t.equal(bencode.encodingLength([1, 2, 3]), 11) // li1ei2ei3ee 49 | t.equal(bencode.encodingLength([1, 'string', [{ a: 1, b: 2 }]]), 29) // li1e6:stringld1:ai1e1:bi2eeee 50 | t.equal(bencode.encodingLength(new Set([1, 'string', [{ a: 1, b: 2 }]])), 29) // li1e6:stringld1:ai1e1:bi2eeee 51 | }) 52 | 53 | t.test('returns correct length for integers', function (t) { 54 | t.plan(2) 55 | t.equal(bencode.encodingLength(-0), 3) // i0e 56 | t.equal(bencode.encodingLength(-1), 4) // i-1e 57 | }) 58 | 59 | t.test('returns integer part length for floating point numbers', function (t) { 60 | t.plan(1) 61 | t.equal(bencode.encodingLength(100.25), 1 + 1 + 3) 62 | }) 63 | 64 | t.test('returns correct length for BigInts', function (t) { 65 | t.plan(1) 66 | // 2n ** 128n == 340282366920938463463374607431768211456 67 | t.equal(bencode.encodingLength(340282366920938463463374607431768211456), 1 + 1 + 39) 68 | }) 69 | 70 | t.test('returns zero for undefined or null values', function (t) { 71 | t.plan(2) 72 | t.equal(bencode.encodingLength(null), 0) 73 | t.equal(bencode.encodingLength(undefined), 0) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /test/decode.utf8.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import data from './data.js' 3 | import bencode from '../index.js' 4 | 5 | test("bencode#decode(x, 'uft8')", function (t) { 6 | t.test('should be able to decode an integer', function (t) { 7 | t.plan(2) 8 | t.equal(bencode.decode('i123e', 'utf8'), 123) 9 | t.equal(bencode.decode('i-123e', 'utf8'), -123) 10 | }) 11 | t.test('should be throw an error when trying to decode a broken integer', function (t) { 12 | t.plan(2) 13 | t.throws(function () { 14 | bencode.decode('i12+3e', 'utf8') 15 | }, /not a number/) 16 | t.throws(function () { 17 | bencode.decode('i-1+23e', 'utf8') 18 | }, /not a number/) 19 | }) 20 | t.test('should be able to decode a float (as int)', function (t) { 21 | t.plan(2) 22 | t.equal(bencode.decode('i12.3e', 'utf8'), 12) 23 | t.equal(bencode.decode('i-12.3e', 'utf8'), -12) 24 | }) 25 | t.test('should be throw an error when trying to decode a broken float', function (t) { 26 | t.plan(2) 27 | t.throws(function () { 28 | bencode.decode('i1+2.3e', 'utf8') 29 | }, /not a number/) 30 | t.throws(function () { 31 | bencode.decode('i-1+2.3e', 'utf8') 32 | }, /not a number/) 33 | }) 34 | t.test('should be able to decode a string', function (t) { 35 | t.plan(2) 36 | t.equal(bencode.decode('5:asdfe', 'utf8'), 'asdfe') 37 | t.deepEqual(bencode.decode(data.binResultData.toString(), 'utf8'), data.binStringData.toString()) 38 | }) 39 | // these tests weren't actually correctly testing values, just mangling values and checking if they are mangled, TODO: fix 40 | // t.test('should be able to decode "binary keys"', function (t) { 41 | // t.plan(1) 42 | // const decoded = bencode.decode(data.binKeyData, 'utf8') 43 | // t.ok(Object.prototype.hasOwnProperty.call(decoded.files, data.binKeyName.toString('utf8'))) 44 | // }) 45 | 46 | t.test('should be able to decode a dictionary', function (t) { 47 | t.plan(3) 48 | t.deepEqual( 49 | bencode.decode('d3:cow3:moo4:spam4:eggse', 'utf8'), 50 | { 51 | cow: 'moo', 52 | spam: 'eggs' 53 | } 54 | ) 55 | t.deepEqual( 56 | bencode.decode('d4:spaml1:a1:bee', 'utf8'), 57 | { spam: ['a', 'b'] } 58 | ) 59 | t.deepEqual( 60 | bencode.decode('d9:publisher3:bob17:publisher-webpage15:www.example.com18:publisher.location4:homee', 'utf8'), 61 | { 62 | publisher: 'bob', 63 | 'publisher-webpage': 'www.example.com', 64 | 'publisher.location': 'home' 65 | } 66 | ) 67 | }) 68 | 69 | t.test('should be able to decode a list', function (t) { 70 | t.plan(1) 71 | t.deepEqual( 72 | bencode.decode('l4:spam4:eggse', 'utf8'), 73 | ['spam', 'eggs'] 74 | ) 75 | }) 76 | t.test('should return the correct type', function (t) { 77 | t.plan(1) 78 | t.ok(typeof (bencode.decode('4:öö', 'utf8')) === 'string') 79 | }) 80 | 81 | t.test('should be able to decode stuff in dicts (issue #12)', function (t) { 82 | t.plan(4) 83 | const someData = { 84 | string: 'Hello World', 85 | integer: 12345, 86 | dict: { 87 | key: 'This is a string within a dictionary' 88 | }, 89 | list: [1, 2, 3, 4, 'string', 5, {}] 90 | } 91 | const result = bencode.encode(someData) 92 | const dat = bencode.decode(result, 'utf8') 93 | t.equal(dat.integer, 12345) 94 | t.deepEqual(dat.string, 'Hello World') 95 | t.deepEqual(dat.dict.key, 'This is a string within a dictionary') 96 | t.deepEqual(dat.list, [1, 2, 3, 4, 'string', 5, {}]) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /test/decode.buffer.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import data from './data.js' 3 | import bencode from '../index.js' 4 | 5 | test('bencode#decode(x)', function (t) { 6 | t.test('should be able to decode an integer', function (t) { 7 | t.plan(2) 8 | t.equal(bencode.decode('i123e'), 123) 9 | t.equal(bencode.decode('i-123e'), -123) 10 | }) 11 | t.test('should be throw an error when trying to decode a broken integer', function (t) { 12 | t.plan(2) 13 | t.throws(function () { 14 | bencode.decode('i12+3e') 15 | }, /not a number/) 16 | t.throws(function () { 17 | bencode.decode('i-1+23e') 18 | }, /not a number/) 19 | }) 20 | t.test('should be able to decode a float (as int)', function (t) { 21 | t.plan(2) 22 | t.equal(bencode.decode('i12.3e'), 12) 23 | t.equal(bencode.decode('i-12.3e'), -12) 24 | }) 25 | t.test('should be throw an error when trying to decode a broken float', function (t) { 26 | t.plan(2) 27 | t.throws(function () { 28 | bencode.decode('i1+2.3e') 29 | }, /not a number/) 30 | t.throws(function () { 31 | bencode.decode('i-1+2.3e') 32 | }, /not a number/) 33 | }) 34 | 35 | t.test('should be able to decode a string', function (t) { 36 | t.plan(2) 37 | t.deepEqual(bencode.decode('5:asdfe'), new Uint8Array(Buffer.from('asdfe'))) 38 | t.deepEqual(bencode.decode(data.binResultData.toString()), new Uint8Array(data.binStringData)) 39 | }) 40 | // these tests weren't actually correctly testing values, just mangling values and checking if they are mangled, TODO: fix 41 | // t.test('should be able to decode "binary keys"', function (t) { 42 | // t.plan(1) 43 | // t.ok(Object.prototype.hasOwnProperty.call(bencode.decode(data.binKeyData).files, data.binKeyName)) 44 | // }) 45 | 46 | t.test('should be able to decode a dictionary', function (t) { 47 | t.plan(3) 48 | t.deepEqual( 49 | bencode.decode('d3:cow3:moo4:spam4:eggse'), 50 | { 51 | cow: new Uint8Array(Buffer.from('moo')), 52 | spam: new Uint8Array(Buffer.from('eggs')) 53 | } 54 | ) 55 | t.deepEqual( 56 | bencode.decode('d4:spaml1:a1:bee'), 57 | { 58 | spam: [ 59 | new Uint8Array(Buffer.from('a')), 60 | new Uint8Array(Buffer.from('b')) 61 | ] 62 | } 63 | ) 64 | t.deepEqual( 65 | bencode.decode('d9:publisher3:bob17:publisher-webpage15:www.example.com18:publisher.location4:homee'), 66 | { 67 | publisher: new Uint8Array(Buffer.from('bob')), 68 | 'publisher-webpage': new Uint8Array(Buffer.from('www.example.com')), 69 | 'publisher.location': new Uint8Array(Buffer.from('home')) 70 | } 71 | ) 72 | }) 73 | 74 | t.test('should be able to decode a list', function (t) { 75 | t.plan(1) 76 | t.deepEqual( 77 | bencode.decode('l4:spam4:eggse'), 78 | [new Uint8Array(Buffer.from('spam')), 79 | new Uint8Array(Buffer.from('eggs'))] 80 | ) 81 | }) 82 | t.test('should return the correct type', function (t) { 83 | t.plan(1) 84 | t.ok(ArrayBuffer.isView(bencode.decode('4:öö'))) 85 | }) 86 | t.test('should be able to decode stuff in dicts (issue #12)', function (t) { 87 | t.plan(4) 88 | const someData = { 89 | string: 'Hello World', 90 | integer: 12345, 91 | dict: { 92 | key: 'This is a string within a dictionary' 93 | }, 94 | list: [1, 2, 3, 4, 'string', 5, {}] 95 | } 96 | const result = bencode.encode(someData) 97 | const dat = bencode.decode(result) 98 | t.equal(dat.integer, 12345) 99 | t.deepEqual(dat.string, new Uint8Array(Buffer.from('Hello World'))) 100 | t.deepEqual(dat.dict.key, new Uint8Array(Buffer.from('This is a string within a dictionary'))) 101 | t.deepEqual(dat.list, [1, 2, 3, 4, new Uint8Array(Buffer.from('string')), 5, {}]) 102 | }) 103 | }) 104 | -------------------------------------------------------------------------------- /lib/encode.js: -------------------------------------------------------------------------------- 1 | import { concat, text2arr } from 'uint8-util' 2 | import { getType } from './util.js' 3 | 4 | /** 5 | * Encodes data in bencode. 6 | * 7 | * @param {Uint8Array|Array|String|Object|Number|Boolean} data 8 | * @return {Uint8Array} 9 | */ 10 | function encode (data, buffer, offset) { 11 | const buffers = [] 12 | let result = null 13 | 14 | encode._encode(buffers, data) 15 | result = concat(buffers) 16 | encode.bytes = result.length 17 | 18 | if (ArrayBuffer.isView(buffer)) { 19 | buffer.set(result, offset) 20 | return buffer 21 | } 22 | 23 | return result 24 | } 25 | 26 | encode.bytes = -1 27 | encode._floatConversionDetected = false 28 | 29 | encode._encode = function (buffers, data) { 30 | if (data == null) { return } 31 | 32 | switch (getType(data)) { 33 | case 'object': encode.dict(buffers, data); break 34 | case 'map': encode.dictMap(buffers, data); break 35 | case 'array': encode.list(buffers, data); break 36 | case 'set': encode.listSet(buffers, data); break 37 | case 'string': encode.string(buffers, data); break 38 | case 'number': encode.number(buffers, data); break 39 | case 'boolean': encode.number(buffers, data); break 40 | case 'arraybufferview': encode.buffer(buffers, new Uint8Array(data.buffer, data.byteOffset, data.byteLength)); break 41 | case 'arraybuffer': encode.buffer(buffers, new Uint8Array(data)); break 42 | } 43 | } 44 | 45 | const buffE = new Uint8Array([0x65]) 46 | const buffD = new Uint8Array([0x64]) 47 | const buffL = new Uint8Array([0x6C]) 48 | 49 | encode.buffer = function (buffers, data) { 50 | buffers.push(text2arr(data.length + ':'), data) 51 | } 52 | 53 | encode.string = function (buffers, data) { 54 | buffers.push(text2arr(text2arr(data).byteLength + ':' + data)) 55 | } 56 | 57 | encode.number = function (buffers, data) { 58 | if (Number.isInteger(data)) return buffers.push(text2arr('i' + BigInt(data) + 'e')) 59 | 60 | const maxLo = 0x80000000 61 | const hi = (data / maxLo) << 0 62 | const lo = (data % maxLo) << 0 63 | const val = hi * maxLo + lo 64 | 65 | buffers.push(text2arr('i' + val + 'e')) 66 | 67 | if (val !== data && !encode._floatConversionDetected) { 68 | encode._floatConversionDetected = true 69 | console.warn( 70 | 'WARNING: Possible data corruption detected with value "' + data + '":', 71 | 'Bencoding only defines support for integers, value was converted to "' + val + '"' 72 | ) 73 | console.trace() 74 | } 75 | } 76 | 77 | encode.dict = function (buffers, data) { 78 | buffers.push(buffD) 79 | 80 | let j = 0 81 | let k 82 | // fix for issue #13 - sorted dicts 83 | const keys = Object.keys(data).sort() 84 | const kl = keys.length 85 | 86 | for (; j < kl; j++) { 87 | k = keys[j] 88 | if (data[k] == null) continue 89 | encode.string(buffers, k) 90 | encode._encode(buffers, data[k]) 91 | } 92 | 93 | buffers.push(buffE) 94 | } 95 | 96 | encode.dictMap = function (buffers, data) { 97 | buffers.push(buffD) 98 | 99 | const keys = Array.from(data.keys()).sort() 100 | 101 | for (const key of keys) { 102 | if (data.get(key) == null) continue 103 | ArrayBuffer.isView(key) 104 | ? encode._encode(buffers, key) 105 | : encode.string(buffers, String(key)) 106 | encode._encode(buffers, data.get(key)) 107 | } 108 | 109 | buffers.push(buffE) 110 | } 111 | 112 | encode.list = function (buffers, data) { 113 | let i = 0 114 | const c = data.length 115 | buffers.push(buffL) 116 | 117 | for (; i < c; i++) { 118 | if (data[i] == null) continue 119 | encode._encode(buffers, data[i]) 120 | } 121 | 122 | buffers.push(buffE) 123 | } 124 | 125 | encode.listSet = function (buffers, data) { 126 | buffers.push(buffL) 127 | 128 | for (const item of data) { 129 | if (item == null) continue 130 | encode._encode(buffers, item) 131 | } 132 | 133 | buffers.push(buffE) 134 | } 135 | 136 | export default encode 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bencode 2 | [![npm](https://img.shields.io/npm/v/bencode.svg)](https://npmjs.com/bencode) 3 | [![npm downloads](https://img.shields.io/npm/dm/bencode.svg)](https://npmjs.com/bencode) 4 | [![ci](https://github.com/webtorrent/node-bencode/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/webtorrent/node-bencode/actions/workflows/ci.yml) 5 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fwebtorrent%2Fnode-bencode.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fwebtorrent%2Fnode-bencode?ref=badge_shield) 6 | 7 | A node library for encoding and decoding bencoded data, 8 | according to the [BitTorrent specification](http://www.bittorrent.org/beps/bep_0003.html). 9 | 10 | ## Index 11 | 12 | - [About BEncoding](#about-bencoding) 13 | - [Installation](#install-with-npm) 14 | - [Usage](#usage) 15 | - [API](#api) 16 | 17 | ## About BEncoding 18 | 19 | from [Wikipedia](https://en.wikipedia.org/wiki/Bencoding): 20 | 21 | Bencode (pronounced like B encode) is the encoding used by the peer-to-peer 22 | file sharing system BitTorrent for storing and transmitting loosely structured data. 23 | 24 | It supports four different types of values: 25 | - byte strings 26 | - integers 27 | - lists 28 | - dictionaries 29 | 30 | Bencoding is most commonly used in torrent files. 31 | These metadata files are simply bencoded dictionaries. 32 | 33 | ## Install with [npm](https://npmjs.org) 34 | 35 | ``` 36 | npm install bencode 37 | ``` 38 | 39 | ## Usage 40 | 41 | ```javascript 42 | import bencode from 'bencode' 43 | ``` 44 | 45 | You can also use node-bencode with browserify to be able to use it in a lot of modern browsers. 46 | 47 | ### Encoding 48 | 49 | ```javascript 50 | 51 | var data = { 52 | string: 'Hello World', 53 | integer: 12345, 54 | dict: { 55 | key: 'This is a string within a dictionary' 56 | }, 57 | list: [ 1, 2, 3, 4, 'string', 5, {} ] 58 | } 59 | 60 | var result = bencode.encode( data ) 61 | 62 | ``` 63 | 64 | **NOTE** As of `bencode@0.8.0`, boolean values will be cast to integers (false -> 0, true -> 1). 65 | 66 | #### Output 67 | 68 | ``` 69 | d4:dictd3:key36:This is a string within a dictionarye7:integeri12345e4:listli1ei2ei3ei4e6:stringi5edee6:string11:Hello Worlde 70 | ``` 71 | 72 | ### Decoding 73 | 74 | ```javascript 75 | var data = Buffer.from('d6:string11:Hello World7:integeri12345e4:dictd3:key36:This is a string within a dictionarye4:listli1ei2ei3ei4e6:stringi5edeee') 76 | var result = bencode.decode( data ) 77 | ``` 78 | 79 | #### Output 80 | 81 | ```javascript 82 | { 83 | string: , 84 | integer: 12345, 85 | dict: { 86 | key: 87 | }, 88 | list: [ 1, 2, 3, 4, , 5, {} ] 89 | } 90 | ``` 91 | 92 | Automagically convert bytestrings to strings: 93 | 94 | ```javascript 95 | var result = bencode.decode( data, 'utf8' ) 96 | ``` 97 | 98 | #### Output 99 | 100 | ```javascript 101 | { 102 | string: 'Hello World', 103 | integer: 12345, 104 | dict: { 105 | key: 'This is a string within a dictionary' 106 | }, 107 | list: [ 1, 2, 3, 4, 'string', 5, {} ] 108 | } 109 | ``` 110 | 111 | ## API 112 | 113 | The API is compatible with the [`abstract-encoding`](https://github.com/mafintosh/abstract-encoding) specification. 114 | 115 | ### bencode.encode( *data*, *[buffer]*, *[offset]* ) 116 | 117 | > `Buffer` | `Array` | `String` | `Object` | `Number` | `Boolean` __data__ 118 | > `Buffer` __buffer__ 119 | > `Number` __offset__ 120 | 121 | Returns `Buffer` 122 | 123 | ### bencode.decode( *data*, *[start]*, *[end]*, *[encoding]* ) 124 | 125 | > `Buffer` __data__ 126 | > `Number` __start__ 127 | > `Number` __end__ 128 | > `String` __encoding__ 129 | 130 | If `encoding` is set, bytestrings are 131 | automatically converted to strings. 132 | 133 | Returns `Object` | `Array` | `Buffer` | `String` | `Number` 134 | 135 | ### bencode.byteLength( *value* ) or bencode.encodingLength( *value* ) 136 | 137 | > `Buffer` | `Array` | `String` | `Object` | `Number` | `Boolean` __value__ 138 | -------------------------------------------------------------------------------- /lib/decode.js: -------------------------------------------------------------------------------- 1 | import { arr2text, text2arr, arr2hex } from 'uint8-util' 2 | 3 | const INTEGER_START = 0x69 // 'i' 4 | const STRING_DELIM = 0x3A // ':' 5 | const DICTIONARY_START = 0x64 // 'd' 6 | const LIST_START = 0x6C // 'l' 7 | const END_OF_TYPE = 0x65 // 'e' 8 | 9 | /** 10 | * replaces parseInt(buffer.toString('ascii', start, end)). 11 | * For strings with less then ~30 charachters, this is actually a lot faster. 12 | * 13 | * @param {Uint8Array} data 14 | * @param {Number} start 15 | * @param {Number} end 16 | * @return {Number} calculated number 17 | */ 18 | function getIntFromBuffer (buffer, start, end) { 19 | let sum = 0 20 | let sign = 1 21 | 22 | for (let i = start; i < end; i++) { 23 | const num = buffer[i] 24 | 25 | if (num < 58 && num >= 48) { 26 | sum = sum * 10 + (num - 48) 27 | continue 28 | } 29 | 30 | if (i === start && num === 43) { // + 31 | continue 32 | } 33 | 34 | if (i === start && num === 45) { // - 35 | sign = -1 36 | continue 37 | } 38 | 39 | if (num === 46) { // . 40 | // its a float. break here. 41 | break 42 | } 43 | 44 | throw new Error('not a number: buffer[' + i + '] = ' + num) 45 | } 46 | 47 | return sum * sign 48 | } 49 | 50 | /** 51 | * Decodes bencoded data. 52 | * 53 | * @param {Uint8Array} data 54 | * @param {Number} start (optional) 55 | * @param {Number} end (optional) 56 | * @param {String} encoding (optional) 57 | * @return {Object|Array|Uint8Array|String|Number} 58 | */ 59 | function decode (data, start, end, encoding) { 60 | if (data == null || data.length === 0) { 61 | return null 62 | } 63 | 64 | if (typeof start !== 'number' && encoding == null) { 65 | encoding = start 66 | start = undefined 67 | } 68 | 69 | if (typeof end !== 'number' && encoding == null) { 70 | encoding = end 71 | end = undefined 72 | } 73 | 74 | decode.position = 0 75 | decode.encoding = encoding || null 76 | 77 | decode.data = !(ArrayBuffer.isView(data)) 78 | ? text2arr(data) 79 | : new Uint8Array(data.slice(start, end)) 80 | 81 | decode.bytes = decode.data.length 82 | 83 | return decode.next() 84 | } 85 | 86 | decode.bytes = 0 87 | decode.position = 0 88 | decode.data = null 89 | decode.encoding = null 90 | 91 | decode.next = function () { 92 | switch (decode.data[decode.position]) { 93 | case DICTIONARY_START: 94 | return decode.dictionary() 95 | case LIST_START: 96 | return decode.list() 97 | case INTEGER_START: 98 | return decode.integer() 99 | default: 100 | return decode.buffer() 101 | } 102 | } 103 | 104 | decode.find = function (chr) { 105 | let i = decode.position 106 | const c = decode.data.length 107 | const d = decode.data 108 | 109 | while (i < c) { 110 | if (d[i] === chr) return i 111 | i++ 112 | } 113 | 114 | throw new Error( 115 | 'Invalid data: Missing delimiter "' + 116 | String.fromCharCode(chr) + '" [0x' + 117 | chr.toString(16) + ']' 118 | ) 119 | } 120 | 121 | decode.dictionary = function () { 122 | decode.position++ 123 | 124 | const dict = {} 125 | 126 | while (decode.data[decode.position] !== END_OF_TYPE) { 127 | const buffer = decode.buffer() 128 | let key = arr2text(buffer) 129 | if (key.includes('\uFFFD')) key = arr2hex(buffer) 130 | dict[key] = decode.next() 131 | } 132 | 133 | decode.position++ 134 | 135 | return dict 136 | } 137 | 138 | decode.list = function () { 139 | decode.position++ 140 | 141 | const lst = [] 142 | 143 | while (decode.data[decode.position] !== END_OF_TYPE) { 144 | lst.push(decode.next()) 145 | } 146 | 147 | decode.position++ 148 | 149 | return lst 150 | } 151 | 152 | decode.integer = function () { 153 | const end = decode.find(END_OF_TYPE) 154 | const number = getIntFromBuffer(decode.data, decode.position + 1, end) 155 | 156 | decode.position += end + 1 - decode.position 157 | 158 | return number 159 | } 160 | 161 | decode.buffer = function () { 162 | let sep = decode.find(STRING_DELIM) 163 | const length = getIntFromBuffer(decode.data, decode.position, sep) 164 | const end = ++sep + length 165 | 166 | decode.position = end 167 | 168 | return decode.encoding 169 | ? arr2text(decode.data.slice(sep, end)) 170 | : decode.data.slice(sep, end) 171 | } 172 | 173 | export default decode 174 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [4.0.0](https://github.com/webtorrent/node-bencode/compare/v3.1.0...v4.0.0) (2023-08-09) 2 | 3 | 4 | ### Features 5 | 6 | * breaking, feat, fix: bigint support, don't mangle directory keys ([#150](https://github.com/webtorrent/node-bencode/pull/150)) ([e95475a](https://github.com/webtorrent/node-bencode/commit/e95475ac554fda8c756669ad31d8af2fae3d64f6)) 7 | 8 | ### BREAKING CHANGES 9 | 10 | * fix: bigint support, don't mangle directory keys 11 | 12 | ## [3.1.1](https://github.com/webtorrent/node-bencode/compare/v3.0.3...v3.1.1) (2023-07-31) 13 | 14 | ### Features 15 | 16 | * update uint8-util ([#153](https://github.com/webtorrent/node-bencode/issues/153)) ([7941736](https://github.com/webtorrent/node-bencode/commit/79417361876a5e5b6b9b17260a5ede8042cfa3e6)) 17 | 18 | ## [3.0.3](https://github.com/webtorrent/node-bencode/compare/v3.0.2...v3.0.3) (2023-01-31) 19 | 20 | 21 | ### Bug Fixes 22 | 23 | * update dependency nanobench to v3 ([#130](https://github.com/webtorrent/node-bencode/issues/130)) ([f7027c4](https://github.com/webtorrent/node-bencode/commit/f7027c46f9cf86017f388fa6d811417b13e03e8e)) 24 | 25 | ## [3.0.2](https://github.com/webtorrent/node-bencode/compare/v3.0.1...v3.0.2) (2023-01-31) 26 | 27 | 28 | ### Bug Fixes 29 | 30 | * package.json for exporting lib ([#140](https://github.com/webtorrent/node-bencode/issues/140)) ([f63c09a](https://github.com/webtorrent/node-bencode/commit/f63c09a8a525e67b00cc0e7619eb84bd159855b2)) 31 | 32 | ## [3.0.1](https://github.com/webtorrent/node-bencode/compare/v3.0.0...v3.0.1) (2023-01-31) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * imports ([#138](https://github.com/webtorrent/node-bencode/issues/138)) ([abe29c3](https://github.com/webtorrent/node-bencode/commit/abe29c32ab327fafb323b05f17eda0aa9ca32478)) 38 | 39 | # [3.0.0](https://github.com/webtorrent/node-bencode/compare/v2.0.3...v3.0.0) (2022-11-28) 40 | 41 | 42 | ### Features 43 | 44 | * esm ([#131](https://github.com/webtorrent/node-bencode/issues/131)) ([b111818](https://github.com/webtorrent/node-bencode/commit/b111818695c8e85e1268fa771fc49c7c6687167f)) 45 | 46 | 47 | ### BREAKING CHANGES 48 | 49 | * ESM only 50 | 51 | ## [2.0.3](https://github.com/webtorrent/node-bencode/compare/v2.0.2...v2.0.3) (2022-05-13) 52 | 53 | ## [2.0.2](https://github.com/webtorrent/node-bencode/compare/v2.0.1...v2.0.2) (2021-07-28) 54 | 55 | 56 | ### Bug Fixes 57 | 58 | * Patch release to drop a dependecy to safe-buffer ([#99](https://github.com/webtorrent/node-bencode/issues/99)) ([a661715](https://github.com/webtorrent/node-bencode/commit/a6617150c53c3c00d0cd12c685c5f2ee47db30c0)) 59 | 60 | ## 2.0.1 61 | 62 | - fix deprecation warning on Buffer() constructor (@jhermsmeier) 63 | - update dev depedencies (@jhermsmeier) 64 | 65 | ## 2.0.0 66 | 67 | - Drop support for Node 0.10, 0.12., add support for Node 8 & 9 (@jhermsmeier) 68 | - Support for typed arrays (@jhermsmeier, @nazar-pc) 69 | 70 | ## 1.0.0 71 | 72 | - Support Node 0.10, 0.12, and early Node 4 (@feross) 73 | 74 | ## 0.12.0 75 | 76 | - Add `btparse` to benchmarks (@themasch) 77 | - Use `Buffer.from()` & `Buffer.allocUnsafe()` (@slang800) 78 | - Use constants for character codes (@slang800) 79 | - Fix Makefile (@zunsthy) 80 | 81 | ## 0.11.0 82 | 83 | - Ignore null-values when encoding (@jhermsmeier) 84 | - Add test/BEP-0023: Test correct handling of compacted peer lists (@jhermsmeier) 85 | - Implement a faster way to parse intergers from buffers (@themasch) 86 | - Fix string to be decoded in README (@ngotchac) 87 | 88 | ## 0.10.0 89 | 90 | - Add `standard` code style (@slang800) 91 | - Update benchmarks (@slang800) 92 | - Remove `lib/dict.js` (@slang800) 93 | - Move `main` entrypoint into ./lib (@slang800) 94 | - Clean up `package.json` (@slang800) 95 | - Remove extra files from being published to npm (@slang800) 96 | 97 | ## 0.9.0 98 | 99 | - Implement the `abstract-encoding` API (@jhermsmeier) 100 | 101 | ## 0.8.0 102 | 103 | - Add support for encoding `Boolean` values (@kaelar) 104 | 105 | ## 0.7.0 106 | 107 | - Add binary key support (@deoxxa) 108 | - Improve test output format (@jhermsmeier) 109 | - Removed node v0.8 from CI tests 110 | 111 | ## 0.6.0 112 | 113 | - Fixed invalid test data (@themasch) 114 | - Added `Makefile` for browser tests (@themasch) 115 | - Fixed Browserify compatibility (@themasch) 116 | 117 | ## 0.5.2 118 | 119 | - Thorough fix for 64 bit and 53 bit numbers (@pwmckenna) 120 | 121 | ## 0.5.1 122 | 123 | - Added warning on float conversion during encoding (@jhermsmeier) 124 | 125 | ## 0.5.0 126 | 127 | - Added support for 64 bit number values (@pwmckenna) 128 | - Switched benchmark lib to `matcha` (@themasch) 129 | - Fixed npm scripts to work on Windows (@jhermsmeier) 130 | 131 | ## 0.4.3 132 | * improved performance a lot 133 | * dropped support for de- and encoding floats to respect the spec 134 | 135 | *note:* node-bencode will still decodes stuff like "i42.23e" but will cast the 136 | result to an interger 137 | 138 | ## 0.4.2 139 | * bugfix: sort dictionary keys to follow the spec 140 | 141 | ## 0.4.1 142 | * bugfix: number decoding was kinda broken 143 | 144 | ## 0.4.0 145 | * fixed problems with multibyte strings 146 | * some performance improvements 147 | * improved code quality 148 | 149 | ## 0.3.0 150 | * #decode() accepts a encoding as its second paramtere 151 | 152 | ## 0.2.0 153 | * complete rewrite, @jhermsmeier joins the team 154 | 155 | ## 0.1.0 156 | * added encoding 157 | 158 | ## 0.0.1 159 | First version, decoding only 160 | -------------------------------------------------------------------------------- /test/encode.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import data from './data.js' 3 | import bencode from '../index.js' 4 | import { arr2text } from 'uint8-util' 5 | 6 | test('bencode#encode()', function (t) { 7 | // prevent the warning showing up in the test 8 | bencode.encode._floatConversionDetected = true 9 | 10 | t.test('should always return a Buffer', function (t) { 11 | t.plan(5) 12 | t.ok(ArrayBuffer.isView(bencode.encode({})), 'its a buffer for empty dicts') 13 | t.ok(ArrayBuffer.isView(bencode.encode('test')), 'its a buffer for strings') 14 | t.ok(ArrayBuffer.isView(bencode.encode([3, 2])), 'its a buffer for lists') 15 | t.ok(ArrayBuffer.isView(bencode.encode({ a: 'b', 3: 6 })), 'its a buffer for big dicts') 16 | t.ok(ArrayBuffer.isView(bencode.encode(123)), 'its a buffer for numbers') 17 | }) 18 | 19 | t.test('should sort dictionaries', function (t) { 20 | t.plan(1) 21 | const data = { string: 'Hello World', integer: 12345 } 22 | t.equal(Buffer.from(bencode.encode(data)).toString(), 'd7:integeri12345e6:string11:Hello Worlde') 23 | }) 24 | 25 | t.test('should force keys to be strings', function (t) { 26 | t.plan(1) 27 | const data = { 28 | 12: 'Hello World', 29 | 34: 12345 30 | } 31 | t.equal(Buffer.from(bencode.encode(data)).toString(), 'd2:1211:Hello World2:34i12345ee') 32 | }) 33 | 34 | t.test('should encode a Map as dictionary', function (t) { 35 | t.plan(1) 36 | const data = new Map([ 37 | [12, 'Hello World'], 38 | ['34', 12345], 39 | [Buffer.from('buffer key'), Buffer.from('buffer value')] 40 | ]) 41 | t.equal(Buffer.from(bencode.encode(data)).toString(), 'd2:1211:Hello World2:34i12345e10:buffer key12:buffer valuee') 42 | }) 43 | 44 | t.test('should be able to encode a positive integer', function (t) { 45 | t.plan(1) 46 | t.equal(Buffer.from(bencode.encode(123)).toString(), 'i123e') 47 | }) 48 | t.test('should be able to encode a negative integer', function (t) { 49 | t.plan(1) 50 | t.equal(Buffer.from(bencode.encode(-123)).toString(), 'i-123e') 51 | }) 52 | t.test('should be able to encode a positive float (as int)', function (t) { 53 | t.plan(1) 54 | t.equal(Buffer.from(bencode.encode(123.5)).toString(), 'i123e') 55 | }) 56 | t.test('should be able to encode a negative float (as int)', function (t) { 57 | t.plan(1) 58 | t.equal(Buffer.from(bencode.encode(-123.5)).toString(), 'i-123e') 59 | }) 60 | 61 | t.test('should be able to safely encode numbers between -/+ 2 ^ 53 (as ints)', function (t) { 62 | const JAVASCRIPT_INT_BITS = 53 63 | const MAX_JAVASCRIPT_INT = Math.pow(2, JAVASCRIPT_INT_BITS) 64 | 65 | t.plan((JAVASCRIPT_INT_BITS - 1) * 6 + 3) 66 | t.equal(Buffer.from(bencode.encode(0)).toString(), 'i' + 0 + 'e') 67 | 68 | for (let exp = 1; exp < JAVASCRIPT_INT_BITS; ++exp) { 69 | const val = Math.pow(2, exp) 70 | // try the positive and negative 71 | t.equal(Buffer.from(bencode.encode(val)).toString(), 'i' + val + 'e') 72 | t.equal(Buffer.from(bencode.encode(-val)).toString(), 'i-' + val + 'e') 73 | 74 | // try the value, one above and one below, both positive and negative 75 | const above = val + 1 76 | const below = val - 1 77 | 78 | t.equal(Buffer.from(bencode.encode(above)).toString(), 'i' + above + 'e') 79 | t.equal(Buffer.from(bencode.encode(-above)).toString(), 'i-' + above + 'e') 80 | 81 | t.equal(Buffer.from(bencode.encode(below)).toString(), 'i' + below + 'e') 82 | t.equal(Buffer.from(bencode.encode(-below)).toString(), 'i-' + below + 'e') 83 | } 84 | t.equal(Buffer.from(bencode.encode(MAX_JAVASCRIPT_INT)).toString(), 'i' + MAX_JAVASCRIPT_INT + 'e') 85 | t.equal(Buffer.from(bencode.encode(-MAX_JAVASCRIPT_INT)).toString(), 'i-' + MAX_JAVASCRIPT_INT + 'e') 86 | }) 87 | t.test('should be able to encode a previously problematice 64 bit int', function (t) { 88 | t.plan(1) 89 | t.equal(Buffer.from(bencode.encode(2433088826)).toString(), 'i' + 2433088826 + 'e') 90 | }) 91 | t.test('should be able to encode a negative 64 bit int', function (t) { 92 | t.plan(1) 93 | t.equal(Buffer.from(bencode.encode(-0xffffffff)).toString(), 'i-' + 0xffffffff + 'e') 94 | }) 95 | t.test('should be able to encode a positive 64 bit float (as int)', function (t) { 96 | t.plan(1) 97 | t.equal(Buffer.from(bencode.encode(0xffffffff + 0.5)).toString(), 'i' + 0xffffffff + 'e') 98 | }) 99 | t.test('should be able to encode a negative 64 bit float (as int)', function (t) { 100 | t.plan(1) 101 | t.equal(Buffer.from(bencode.encode(-0xffffffff - 0.5)).toString(), 'i-' + 0xffffffff + 'e') 102 | }) 103 | t.test('should be able to encode a string', function (t) { 104 | t.plan(2) 105 | t.equal(Buffer.from(bencode.encode('asdf')).toString(), '4:asdf') 106 | t.equal(Buffer.from(bencode.encode(':asdf:')).toString(), '6::asdf:') 107 | }) 108 | t.test('should be able to encode a unicode string', function (t) { 109 | t.plan(2) 110 | t.deepEqual(bencode.encode(data.binStringData.toString()), new Uint8Array(data.binResultData)) 111 | t.deepEqual(bencode.encode(data.binStringData.toString()), new Uint8Array(data.binResultData)) 112 | }) 113 | t.test('should be able to encode a buffer', function (t) { 114 | t.plan(2) 115 | t.equal(Buffer.from(bencode.encode(Buffer.from('asdf'))).toString(), '4:asdf') 116 | t.equal(Buffer.from(bencode.encode(Buffer.from(':asdf:'))).toString(), '6::asdf:') 117 | }) 118 | t.test('should be able to encode an array', function (t) { 119 | t.plan(2) 120 | t.equal(Buffer.from(bencode.encode([32, 12])).toString(), 'li32ei12ee') 121 | t.equal(Buffer.from(bencode.encode([':asdf:'])).toString(), 'l6::asdf:e') 122 | }) 123 | t.test('should be able to encode a Set as a list', function (t) { 124 | t.plan(2) 125 | t.equal(Buffer.from(bencode.encode(new Set([32, 12]))).toString(), 'li32ei12ee') 126 | t.equal(Buffer.from(bencode.encode(new Set([':asdf:']))).toString(), 'l6::asdf:e') 127 | }) 128 | t.test('should be able to encode an object', function (t) { 129 | t.plan(3) 130 | t.equal(Buffer.from(bencode.encode({ a: 'bc' })).toString(), 'd1:a2:bce') 131 | t.equal(Buffer.from(bencode.encode({ a: '45', b: 45 })).toString(), 'd1:a2:451:bi45ee') 132 | t.equal(Buffer.from(bencode.encode({ a: Buffer.from('bc') })).toString(), 'd1:a2:bce') 133 | }) 134 | 135 | t.test('should encode new Number(1) as number', function (t) { 136 | var data = new Number(1) // eslint-disable-line 137 | const result = bencode.decode(bencode.encode(data)) 138 | const expected = 1 139 | t.plan(1) 140 | t.strictEqual(result, expected) 141 | }) 142 | 143 | t.test('should encode new Boolean(true) as number', function (t) { 144 | var data = new Boolean(true) // eslint-disable-line 145 | const result = bencode.decode(bencode.encode(data)) 146 | const expected = 1 147 | t.plan(1) 148 | t.strictEqual(result, expected) 149 | }) 150 | 151 | t.test('should encode Uint8Array as buffer', function (t) { 152 | const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]) 153 | const result = bencode.decode(bencode.encode(data)) 154 | const expected = new Uint8Array(data.buffer) 155 | t.plan(1) 156 | t.deepEqual(result, expected) 157 | }) 158 | 159 | t.test('should encode Uint32Array as buffer', function (t) { 160 | const data = new Uint32Array([0xF, 0xFF, 0xFFF, 0xFFFF, 0xFFFFF, 0xFFFFFF, 0xFFFFFFF, 0xFFFFFFFF]) 161 | const result = bencode.decode(bencode.encode(data)) 162 | const expected = new Uint8Array(data.buffer) 163 | t.plan(1) 164 | t.deepEqual(result, expected) 165 | }) 166 | 167 | t.test('should encode ArrayBuffer as buffer', function (t) { 168 | const data = new Uint32Array([0xF, 0xFF, 0xFFF, 0xFFFF, 0xFFFFF, 0xFFFFFF, 0xFFFFFFF, 0xFFFFFFFF]) 169 | const result = bencode.decode(bencode.encode(data.buffer)) 170 | const expected = new Uint8Array(data.buffer) 171 | t.plan(1) 172 | t.deepEqual(result, expected) 173 | }) 174 | 175 | t.test('should encode Float32Array as buffer', function (t) { 176 | const data = new Float32Array([1.2, 2.3, 3.4, 4.5, 5.6, 6.7, 7.8, 8.9, 9.0]) 177 | const result = bencode.decode(bencode.encode(data)) 178 | const expected = new Uint8Array(data.buffer) 179 | t.plan(1) 180 | t.deepEqual(result, expected) 181 | }) 182 | 183 | t.test('should encode DataView as buffer', function (t) { 184 | const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]) 185 | const view = new DataView(data.buffer) 186 | const result = bencode.decode(bencode.encode(view)) 187 | const expected = new Uint8Array(data.buffer) 188 | t.plan(1) 189 | t.deepEqual(result, expected) 190 | }) 191 | 192 | t.test('should encode Uint8Array subarray properly', function (t) { 193 | const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]) 194 | const subData = data.subarray(5) 195 | const result = bencode.decode(bencode.encode(subData)) 196 | const expected = new Uint8Array(subData.buffer, subData.byteOffset, subData.byteLength) 197 | t.plan(1) 198 | t.deepEqual(result, expected) 199 | }) 200 | t.test('should encode large numbers with full digits', function (t) { 201 | t.plan(1) 202 | const data = 340282366920938463463374607431768211456 203 | t.deepEqual(arr2text(bencode.encode(data)), 'i340282366920938463463374607431768211456e') 204 | }) 205 | }) 206 | --------------------------------------------------------------------------------