├── .codeclimate.yml ├── .editorconfig ├── .eslintrc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── babel.config.json ├── dist ├── byte-buffer.js └── byte-buffer.min.js ├── jest.config.js ├── lib ├── ByteBuffer.mjs └── utils.mjs ├── package-lock.json ├── package.json ├── rollup.config.js └── spec ├── .eslintrc ├── ByteBuffer.spec.mjs └── spec-helper.mjs /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | languages: 2 | JavaScript: true 3 | exclude_paths: 4 | - 'dist/*' 5 | - 'spec/*' 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@timkurvers", 3 | "rules": { 4 | "func-names": "off", 5 | "no-bitwise": "off", 6 | "no-plusplus": "off", 7 | "no-underscore-dangle": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: [push, pull_request] 3 | jobs: 4 | lint-test: 5 | name: lint and test 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | node-version: [10.x] 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Node.js ${{ matrix.node-version }} 13 | uses: actions/setup-node@v1 14 | with: 15 | node-version: ${{ matrix.node-version }} 16 | - run: npm install 17 | - run: npm run lint 18 | - run: npm test 19 | lint-test-coverage: 20 | name: lint and test with coverage (12.x) 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v2 24 | - uses: actions/setup-node@v1 25 | with: 26 | node-version: 12.x 27 | - run: npm install 28 | - run: npm run lint 29 | - uses: paambaati/codeclimate-action@v2.5.4 30 | env: 31 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 32 | with: 33 | coverageCommand: npm run test:coverage 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage/ 2 | /node_modules/ 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### v2.0.0 - March 16, 2020 4 | 5 | - Dropped support for deprecated Node 8. 6 | - Uses `@timkurvers/eslint-config` for linting. 7 | - Removed `ByteBuffer.prototype.toString`. 8 | - Usable as an ECMAScript module using `import`. 9 | 10 | ### v1.0.3 - October 27, 2014 11 | 12 | - Configures `attr-accessor` as runtime dependency. 13 | 14 | ### v1.0.2 - October 25, 2014 15 | 16 | - Node support. 17 | 18 | ### v1.0.1 - January 4, 2014 19 | 20 | - Work around `Uint8Array`'s constructor behaviour when given strings. 21 | 22 | ### v1.0.0 - December 20, 2012 23 | 24 | - Stable release. 25 | - Byte order is now maintained when reading, slicing and cloning buffers. 26 | - Implicit growth strategy is now maintained when cloning a buffer. 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2012-2020 Tim Kurvers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ByteBuffer 2 | 3 | [![Version](https://badgen.net/npm/v/byte-buffer)](https://www.npmjs.org/package/byte-buffer) 4 | ![Node Version](https://badgen.net/badge/node/10+/green) 5 | [![MIT License](https://badgen.net/github/license/timkurvers/byte-buffer)](LICENSE.md) 6 | ![Checks](https://badgen.net/github/checks/timkurvers/byte-buffer) 7 | [![Maintainability](https://badgen.net/codeclimate/maintainability/timkurvers/byte-buffer)](https://codeclimate.com/github/timkurvers/byte-buffer) 8 | [![Test Coverage](https://badgen.net/codeclimate/coverage/timkurvers/byte-buffer)](https://codeclimate.com/github/timkurvers/byte-buffer) 9 | 10 | Wrapper for JavaScript's ArrayBuffer/DataView maintaining index and default 11 | endianness. Supports arbitrary reading/writing, implicit growth, clipping, 12 | cloning and reversing as well as UTF-8 characters and NULL-terminated C-strings. 13 | 14 | ## Installation 15 | 16 | ByteBuffer is available via [npm]: 17 | 18 | ```shell 19 | npm install byte-buffer 20 | ``` 21 | 22 | Or for usage in the browser: 23 | 24 | - `dist/byte-buffer.js` 25 | - `dist/byte-buffer.min.js` 26 | 27 | ## Usage 28 | 29 | As an ECMAScript module: 30 | 31 | ```javascript 32 | import ByteBuffer from 'byte-buffer'; 33 | 34 | const b = new ByteBuffer(); 35 | ``` 36 | 37 | In CommonJS environments: 38 | 39 | ```javascript 40 | const ByteBuffer = require('byte-buffer'); 41 | 42 | const b = new ByteBuffer(); 43 | ``` 44 | 45 | Available in the global scope when included in browser environments: 46 | 47 | ```javascript 48 | const b = new ByteBuffer(); 49 | ``` 50 | 51 | ## API 52 | 53 | ByteBuffer's API borrows heavily from Adobe's [IDataInput] and [IDataOutput] as 54 | well as David Flanagan's [BufferView]. 55 | 56 | The concept of separate buffers and views - as outlined in MDN's [JavaScript 57 | typed arrays] - is *not* used. ByteBuffer handles this separation for you. 58 | 59 | ### Constants 60 | 61 | Use the following constants to indicate endianness: 62 | 63 | ```javascript 64 | ByteBuffer.BIG_ENDIAN 65 | ByteBuffer.LITTLE_ENDIAN 66 | ``` 67 | 68 | ### Construction 69 | 70 | ```javascript 71 | new ByteBuffer(1) // Buffer of one byte with big-endian byte order 72 | new ByteBuffer(1, ByteBuffer.LITTLE_ENDIAN) // Little-endian byte order instead 73 | ``` 74 | 75 | ByteBuffers may also be constructed from other byte-aware sources: 76 | 77 | ```javascript 78 | new ByteBuffer(new ArrayBuffer(2)) 79 | new ByteBuffer(new Uint8Array(3)) 80 | new ByteBuffer(new DataView(new ArrayBuffer(4))) 81 | new ByteBuffer(new ByteBuffer(5)) 82 | ``` 83 | 84 | Or from generic sequences: 85 | 86 | ```javascript 87 | new ByteBuffer([0, 1, 2, 3]) 88 | ``` 89 | 90 | After construction a ByteBuffer's read/write index is always at the front of the 91 | buffer. Hereafter ```b``` is assumed to be an instance of ByteBuffer. 92 | 93 | ### Properties 94 | 95 | ```javascript 96 | b.buffer // Reference to internal ArrayBuffer 97 | b.buffer = new ArrayBuffer(3) // Sets new buffer 98 | ``` 99 | 100 | ```javascript 101 | b.raw // Reference to raw buffer (read-only) 102 | ``` 103 | 104 | ```javascript 105 | b.view // Reference to internal DataView (read-only) 106 | ``` 107 | 108 | ```javascript 109 | b.length // Number of bytes in the buffer (read-only) 110 | b.byteLength // Alias 111 | ``` 112 | 113 | ```javascript 114 | b.order // Buffer's current default byte order 115 | b.order = ByteBuffer.BIG_ENDIAN // Sets byte order 116 | ``` 117 | 118 | ```javascript 119 | b.available // Number of available bytes (read-only) 120 | ``` 121 | 122 | ### Index Manipulation 123 | 124 | ByteBuffer maintains a read/write index to simplify usage. 125 | 126 | ```javascript 127 | b.index // Current read/write index 128 | b.index = 4 // Sets index 129 | ``` 130 | 131 | If the index is out of bounds, a RangeError will be thrown. 132 | 133 | ```javascript 134 | b.front() // Sets index to front of the buffer 135 | ``` 136 | 137 | ```javascript 138 | b.end() // Sets index to end of the buffer 139 | ``` 140 | 141 | ```javascript 142 | b.seek(10) // Forwards ten bytes 143 | b.seek(-2) // Backs two bytes 144 | ``` 145 | 146 | These methods may be chained: 147 | 148 | ```javascript 149 | b.front().seek(2) 150 | ``` 151 | 152 | ### Read API 153 | 154 | All read methods default to the ByteBuffer's byte order if not given. 155 | 156 | ```javascript 157 | b.readByte() 158 | ``` 159 | 160 | ```javascript 161 | b.readUnsignedByte() 162 | ``` 163 | 164 | ```javascript 165 | b.readShort() // Buffer's default byte order 166 | b.readShort(ByteBuffer.LITTLE_ENDIAN) // Explicit byte order 167 | ``` 168 | 169 | ```javascript 170 | b.readUnsignedShort() 171 | ``` 172 | 173 | ```javascript 174 | b.readInt() 175 | ``` 176 | 177 | ```javascript 178 | b.readUnsignedInt() 179 | ``` 180 | 181 | ```javascript 182 | b.readFloat() 183 | ``` 184 | 185 | ```javascript 186 | b.readDouble() 187 | ``` 188 | 189 | ```javascript 190 | b.read(6) // Reads 6 bytes 191 | b.read() // Reads all remaining bytes 192 | ``` 193 | 194 | ```javascript 195 | b.readString(5) // Reads 5 bytes as a string 196 | b.readString() // Reads all remaining bytes as a string 197 | b.readUTFChars() // Alias 198 | ``` 199 | 200 | ```javascript 201 | b.readCString() // Reads string up to NULL-byte or end of buffer 202 | ``` 203 | 204 | ### Write API 205 | 206 | All write methods default to the ByteBuffer's byte order if not given. 207 | 208 | ```javascript 209 | b.writeByte(10) 210 | ``` 211 | 212 | ```javascript 213 | b.writeUnsignedByte(-10) 214 | ``` 215 | 216 | ```javascript 217 | b.writeShort(-2048) 218 | b.writeShort(-2048, ByteBuffer.LITTLE_ENDIAN) // Explicit byte order 219 | ``` 220 | 221 | ```javascript 222 | b.writeUnsignedShort(4096) 223 | ``` 224 | 225 | ```javascript 226 | b.writeInt(-524288) 227 | ``` 228 | 229 | ```javascript 230 | b.writeUnsignedInt(1048576) 231 | ``` 232 | 233 | ```javascript 234 | b.writeFloat(13.37) 235 | ``` 236 | 237 | ```javascript 238 | b.writeDouble(1048576.89) 239 | ``` 240 | 241 | ```javascript 242 | b.write([1, 2, 3]) 243 | b.write(new ArrayBuffer(2)) 244 | b.write(new Uint8Array(3)) 245 | b.write(new ByteBuffer(5)) 246 | ``` 247 | 248 | Additionally, all the above write methods may be chained: 249 | 250 | ```javascript 251 | b.writeShort(0x2020).write([1, 2, 3]) 252 | ``` 253 | 254 | The following string related methods do not return the buffer itself, but rather 255 | provide the number of bytes that were written to it. More on this under implicit 256 | growth strategy a bit further down. 257 | 258 | ```javascript 259 | b.writeString('ByteBuffer') // Writes given string and returns number of bytes 260 | b.writeUTFChars('ByteBuffer') // Alias 261 | ``` 262 | 263 | ```javascript 264 | b.writeCString('ByteBuffer') // Writes given string and returns number of bytes (including NULL-byte) 265 | ``` 266 | 267 | ### Size Manipulation 268 | 269 | #### Growth 270 | 271 | The buffer may be grown at the front or at the end. When prepending, the buffer's 272 | index is adjusted accordingly. 273 | 274 | ```javascript 275 | b.prepend(2) // Prepends given number of bytes 276 | ``` 277 | 278 | ```javascript 279 | b.append(2) // Appends given number of bytes 280 | ``` 281 | 282 | #### Implicit Growth 283 | 284 | This feature allows a ByteBuffer to grow implicitly when writing arbitrary data. 285 | Since every implicit growth requires the buffer to be rebuilt from scratch, care 286 | must be taken when using this feature. Writing low byte-length pieces of data in 287 | rapid succession is not recommended. 288 | 289 | To protect the unaware from harm, this feature needs to be explicitly enabled: 290 | 291 | ```javascript 292 | b = new ByteBuffer(2, ByteBuffer.BIG_ENDIAN, true) // Last argument indicates implicit growth strategy 293 | b.writeUnsignedInt(2345102) // Implicitly makes room for 4 bytes - by growing with 2 - prior to writing 294 | ``` 295 | 296 | The implicit growth strategy can also be enabled and disabled after construction: 297 | 298 | ```javascript 299 | b.implicitGrowth = true/false 300 | ``` 301 | 302 | Implicit growth is a must when dealing with UTF-8 encoded strings, as dealing 303 | with arbitrary user data - e.g. names or addresses - *may* include various 304 | characters that require to be encoded in multiple bytes, which would be 305 | relatively verbose to calculate beforehand. 306 | 307 | #### Clipping 308 | 309 | The buffer may be truncated at the front, end or both. Both arguments are 310 | optional and may be negative in which case the offsets are calculated from the 311 | respective boundaries of the buffer. The `begin`-argument defaults to the current 312 | index, allowing efficient clipping in various scenarios, e.g. when used in 313 | combination with network sockets to shift off read data. The `end`-argument 314 | defaults to the end of the buffer. 315 | 316 | ```javascript 317 | b.clip(2, -2) 318 | b.clip(-2, 4) 319 | ``` 320 | 321 | ### Miscellaneous 322 | 323 | ```javascript 324 | b.slice(2, 4) // Independent clone of given slice of the buffer 325 | ``` 326 | 327 | ```javascript 328 | b.clone() // Independent clone of the entire buffer 329 | ``` 330 | 331 | ```javascript 332 | b.reverse() // Reverses buffer in place 333 | ``` 334 | 335 | ```javascript 336 | b.toArray() // Changes to this array are not backed 337 | ``` 338 | 339 | ```javascript 340 | b.toHex() // Hexadecimal representation of this buffer, e.g: 42 79 74 65 42 75 66 66 65 72 341 | ``` 342 | 343 | ```javascript 344 | b.toASCII() // ASCII representation of this buffer, e.g: B y t e B u f f e r 345 | ``` 346 | 347 | ## Development & Contribution 348 | 349 | ByteBuffer is written in [ES2015+], modularized using [ECMAScript Modules], 350 | compiled by [Babel], bundled with [rollup] and tested through [Jest]. 351 | 352 | Getting this toolchain up and running, is easy and straight-forward: 353 | 354 | 1. Get the code: 355 | 356 | ```shell 357 | git clone git://github.com/timkurvers/byte-buffer.git 358 | ``` 359 | 360 | 2. Download and install [Node.js] – including `npm` – for your platform. 361 | 362 | 3. Install dependencies: 363 | 364 | ```shell 365 | npm install 366 | ``` 367 | 368 | 4. Run `npm test:watch` which will run tests when source files change. 369 | 370 | When contributing, please: 371 | 372 | - Fork the repository 373 | - Accompany each logical unit of operation with at least one test 374 | - Open a pull request 375 | - Do *not* include any distribution files (such as `dist/byte-buffer.js`) 376 | 377 | ## Alternative Comparisons 378 | 379 | ### Christopher Chedeau's [jDataView] 380 | 381 | - Maintains read-index and supports seeking 382 | - Various string/char utilities (may support UTF-8) 383 | - Does *not* support writing values 384 | - Does *not* support NULL-terminated C-strings 385 | - Does *not* support growing, clipping, cloning and reversing 386 | - Supports a wide range of browsers/setups 387 | 388 | ### David Flanagan's [BufferView] 389 | 390 | - Supports reading/writing values 391 | - Maintains index and supports seeking 392 | - Supports UTF-8 characters 393 | - Does *not* support NULL-terminated C-strings 394 | - Does *not* support growing, clipping, cloning and reversing as view and buffer 395 | are immutable 396 | 397 | [Babel]: https://babeljs.io/ 398 | [BufferView]: https://github.com/davidflanagan/BufferView 399 | [ECMAScript Modules]: https://nodejs.org/api/esm.html#esm_ecmascript_modules 400 | [ES2015+]: https://babeljs.io/docs/learn-es2015/ 401 | [IDataInput]: http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/utils/IDataInput.html 402 | [IDataOutput]: http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/utils/IDataOutput.html 403 | [JavaScript typed arrays]: https://developer.mozilla.org/en/JavaScript_typed_arrays 404 | [Jest]: https://jestjs.io/ 405 | [Node.js]: http://nodejs.org/#download 406 | [jDataView]: https://github.com/vjeux/jDataView/ 407 | [npm]: https://www.npmjs.com 408 | [rollup]: https://rollupjs.org 409 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": "> 0.25%, not dead" 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /dist/byte-buffer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * byte-buffer v2.0.0 3 | * Copyright (c) 2012-2020 Tim Kurvers 4 | * @license MIT 5 | * 6 | * Wrapper for JavaScript's ArrayBuffer/DataView. 7 | * 8 | * https://github.com/timkurvers/byte-buffer 9 | */ 10 | (function (global, factory) { 11 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 12 | typeof define === 'function' && define.amd ? define(factory) : 13 | (global = global || self, global.ByteBuffer = factory()); 14 | }(this, (function () { 'use strict'; 15 | 16 | function _classCallCheck(instance, Constructor) { 17 | if (!(instance instanceof Constructor)) { 18 | throw new TypeError("Cannot call a class as a function"); 19 | } 20 | } 21 | 22 | function _defineProperties(target, props) { 23 | for (var i = 0; i < props.length; i++) { 24 | var descriptor = props[i]; 25 | descriptor.enumerable = descriptor.enumerable || false; 26 | descriptor.configurable = true; 27 | if ("value" in descriptor) descriptor.writable = true; 28 | Object.defineProperty(target, descriptor.key, descriptor); 29 | } 30 | } 31 | 32 | function _createClass(Constructor, protoProps, staticProps) { 33 | if (protoProps) _defineProperties(Constructor.prototype, protoProps); 34 | if (staticProps) _defineProperties(Constructor, staticProps); 35 | return Constructor; 36 | } 37 | 38 | /* eslint-disable import/prefer-default-export */ 39 | // Extracts buffer from given source and optionally clones it 40 | var extractBuffer = function extractBuffer(source) { 41 | var clone = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; 42 | 43 | // Source is a byte-aware object 44 | if (source && typeof source.byteLength !== 'undefined') { 45 | // Determine whether source is a view or a raw buffer 46 | if (typeof source.buffer !== 'undefined') { 47 | return clone ? source.buffer.slice(0) : source.buffer; 48 | } 49 | 50 | return clone ? source.slice(0) : source; 51 | } // Source is a sequence of bytes 52 | 53 | 54 | if (source && typeof source.length !== 'undefined') { 55 | // Although Uint8Array's constructor succeeds when given strings, 56 | // it does not correctly instantiate the buffer 57 | if (source.constructor === String) { 58 | return null; 59 | } 60 | 61 | try { 62 | return new Uint8Array(source).buffer; 63 | } catch (error) { 64 | return null; 65 | } 66 | } // No buffer found 67 | 68 | 69 | return null; 70 | }; 71 | 72 | var ByteBuffer = /*#__PURE__*/function () { 73 | // Creates a new ByteBuffer 74 | // - from given source (assumed to be number of bytes when numeric) 75 | // - with given byte order (defaults to big-endian) 76 | // - with given implicit growth strategy (defaults to false) 77 | function ByteBuffer() { 78 | var source = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; 79 | var order = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.constructor.BIG_ENDIAN; 80 | var implicitGrowth = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; 81 | 82 | _classCallCheck(this, ByteBuffer); 83 | 84 | // Holds buffer 85 | this._buffer = null; // Holds raw buffer 86 | 87 | this._raw = null; // Holds internal view for reading/writing 88 | 89 | this._view = null; // Holds byte order 90 | 91 | this._order = !!order; // Holds implicit growth strategy 92 | 93 | this._implicitGrowth = !!implicitGrowth; // Holds read/write index 94 | 95 | this._index = 0; // Attempt to extract a buffer from given source 96 | 97 | var buffer = extractBuffer(source, true); // On failure, assume source is a primitive indicating the number of bytes 98 | 99 | if (!buffer) { 100 | buffer = new ArrayBuffer(source); 101 | } // Assign new buffer 102 | 103 | 104 | this.buffer = buffer; 105 | } // Sanitizes read/write index 106 | 107 | 108 | _createClass(ByteBuffer, [{ 109 | key: "_sanitizeIndex", 110 | value: function _sanitizeIndex() { 111 | if (this._index < 0) { 112 | this._index = 0; 113 | } 114 | 115 | if (this._index > this.length) { 116 | this._index = this.length; 117 | } 118 | } // Retrieves buffer 119 | 120 | }, { 121 | key: "front", 122 | // Sets index to front of the buffer 123 | value: function front() { 124 | this._index = 0; 125 | return this; 126 | } // Sets index to end of the buffer 127 | 128 | }, { 129 | key: "end", 130 | value: function end() { 131 | this._index = this.length; 132 | return this; 133 | } // Seeks given number of bytes 134 | // Note: Backwards seeking is supported 135 | 136 | }, { 137 | key: "seek", 138 | value: function seek() { 139 | var bytes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1; 140 | this.index += bytes; 141 | return this; 142 | } // Reads sequence of given number of bytes (defaults to number of bytes available) 143 | 144 | }, { 145 | key: "read", 146 | value: function read() { 147 | var bytes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.available; 148 | 149 | if (bytes > this.available) { 150 | throw new Error("Cannot read ".concat(bytes, " byte(s), ").concat(this.available, " available")); 151 | } 152 | 153 | if (bytes <= 0) { 154 | throw new RangeError("Invalid number of bytes ".concat(bytes)); 155 | } 156 | 157 | var value = new ByteBuffer(this._buffer.slice(this._index, this._index + bytes), this.order); 158 | this._index += bytes; 159 | return value; 160 | } // Writes sequence of bytes 161 | 162 | }, { 163 | key: "write", 164 | value: function write(sequence) { 165 | var view; // Ensure we're dealing with a Uint8Array view 166 | 167 | if (!(sequence instanceof Uint8Array)) { 168 | // Extract the buffer from the sequence 169 | var buffer = extractBuffer(sequence); 170 | 171 | if (!buffer) { 172 | throw new TypeError("Cannot write ".concat(sequence, ", not a sequence")); 173 | } // And create a new Uint8Array view for it 174 | 175 | 176 | view = new Uint8Array(buffer); 177 | } else { 178 | view = sequence; 179 | } 180 | 181 | var available = this.available; 182 | 183 | if (view.byteLength > available) { 184 | if (this._implicitGrowth) { 185 | this.append(view.byteLength - available); 186 | } else { 187 | throw new Error("Cannot write ".concat(sequence, " using ").concat(view.byteLength, " byte(s), ").concat(this.available, " available")); 188 | } 189 | } 190 | 191 | this._raw.set(view, this._index); 192 | 193 | this._index += view.byteLength; 194 | return this; 195 | } // Reads UTF-8 encoded string of given number of bytes (defaults to number of bytes available) 196 | // 197 | // Based on David Flanagan's BufferView (https://github.com/davidflanagan/BufferView/blob/master/BufferView.js//L195) 198 | 199 | }, { 200 | key: "readString", 201 | value: function readString() { 202 | var bytes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.available; 203 | 204 | if (bytes > this.available) { 205 | throw new Error("Cannot read ".concat(bytes, " byte(s), ").concat(this.available, " available")); 206 | } 207 | 208 | if (bytes <= 0) { 209 | throw new RangeError("Invalid number of bytes ".concat(bytes)); 210 | } // Local reference 211 | 212 | 213 | var raw = this._raw; // Holds decoded characters 214 | 215 | var codepoints = []; // Index into codepoints 216 | 217 | var c = 0; // Bytes 218 | 219 | var b1 = null; 220 | var b2 = null; 221 | var b3 = null; 222 | var b4 = null; // Target index 223 | 224 | var target = this._index + bytes; 225 | 226 | while (this._index < target) { 227 | b1 = raw[this._index]; 228 | 229 | if (b1 < 128) { 230 | // One byte sequence 231 | codepoints[c++] = b1; 232 | this._index++; 233 | } else if (b1 < 194) { 234 | throw new Error('Unexpected continuation byte'); 235 | } else if (b1 < 224) { 236 | // Two byte sequence 237 | b2 = raw[this._index + 1]; 238 | 239 | if (b2 < 128 || b2 > 191) { 240 | throw new Error('Bad continuation byte'); 241 | } 242 | 243 | codepoints[c++] = ((b1 & 0x1F) << 6) + (b2 & 0x3F); 244 | this._index += 2; 245 | } else if (b1 < 240) { 246 | // Three byte sequence 247 | b2 = raw[this._index + 1]; 248 | 249 | if (b2 < 128 || b2 > 191) { 250 | throw new Error('Bad continuation byte'); 251 | } 252 | 253 | b3 = raw[this._index + 2]; 254 | 255 | if (b3 < 128 || b3 > 191) { 256 | throw new Error('Bad continuation byte'); 257 | } 258 | 259 | codepoints[c++] = ((b1 & 0x0F) << 12) + ((b2 & 0x3F) << 6) + (b3 & 0x3F); 260 | this._index += 3; 261 | } else if (b1 < 245) { 262 | // Four byte sequence 263 | b2 = raw[this._index + 1]; 264 | 265 | if (b2 < 128 || b2 > 191) { 266 | throw new Error('Bad continuation byte'); 267 | } 268 | 269 | b3 = raw[this._index + 2]; 270 | 271 | if (b3 < 128 || b3 > 191) { 272 | throw new Error('Bad continuation byte'); 273 | } 274 | 275 | b4 = raw[this._index + 3]; 276 | 277 | if (b4 < 128 || b4 > 191) { 278 | throw new Error('Bad continuation byte'); 279 | } 280 | 281 | var cp = ((b1 & 0x07) << 18) + ((b2 & 0x3F) << 12) + ((b3 & 0x3F) << 6) + (b4 & 0x3F); 282 | cp -= 0x10000; // Turn code point into two surrogate pairs 283 | 284 | codepoints[c++] = 0xD800 + ((cp & 0x0FFC00) >>> 10); 285 | codepoints[c++] = 0xDC00 + (cp & 0x0003FF); 286 | this._index += 4; 287 | } else { 288 | throw new Error('Illegal byte'); 289 | } 290 | } // Browsers may have hardcoded or implicit limits on the array length when applying a function 291 | // See: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/apply//apply_and_built-in_functions 292 | 293 | 294 | var limit = 1 << 16; 295 | var length = codepoints.length; 296 | 297 | if (length < limit) { 298 | return String.fromCharCode.apply(String, codepoints); 299 | } 300 | 301 | var chars = []; 302 | var i = 0; 303 | 304 | while (i < length) { 305 | chars.push(String.fromCharCode.apply(String, codepoints.slice(i, i + limit))); 306 | i += limit; 307 | } 308 | 309 | return chars.join(''); 310 | } // Writes UTF-8 encoded string 311 | // Note: Does not write string length or terminator 312 | // 313 | // Based on David Flanagan's BufferView (https://github.com/davidflanagan/BufferView/blob/master/BufferView.js//L264) 314 | 315 | }, { 316 | key: "writeString", 317 | value: function writeString(string) { 318 | // Encoded UTF-8 bytes 319 | var bytes = []; // String length, offset and byte offset 320 | 321 | var length = string.length; 322 | var i = 0; 323 | var b = 0; 324 | 325 | while (i < length) { 326 | var c = string.charCodeAt(i); 327 | 328 | if (c <= 0x7F) { 329 | // One byte sequence 330 | bytes[b++] = c; 331 | } else if (c <= 0x7FF) { 332 | // Two byte sequence 333 | bytes[b++] = 0xC0 | (c & 0x7C0) >>> 6; 334 | bytes[b++] = 0x80 | c & 0x3F; 335 | } else if (c <= 0xD7FF || c >= 0xE000 && c <= 0xFFFF) { 336 | // Three byte sequence 337 | // Source character is not a UTF-16 surrogate 338 | bytes[b++] = 0xE0 | (c & 0xF000) >>> 12; 339 | bytes[b++] = 0x80 | (c & 0x0FC0) >>> 6; 340 | bytes[b++] = 0x80 | c & 0x3F; 341 | } else { 342 | // Four byte sequence 343 | if (i === length - 1) { 344 | throw new Error("Unpaired surrogate ".concat(string[i], " (index ").concat(i, ")")); 345 | } // Retrieve surrogate 346 | 347 | 348 | var d = string.charCodeAt(++i); 349 | 350 | if (c < 0xD800 || c > 0xDBFF || d < 0xDC00 || d > 0xDFFF) { 351 | throw new Error("Unpaired surrogate ".concat(string[i], " (index ").concat(i, ")")); 352 | } 353 | 354 | var cp = ((c & 0x03FF) << 10) + (d & 0x03FF) + 0x10000; 355 | bytes[b++] = 0xF0 | (cp & 0x1C0000) >>> 18; 356 | bytes[b++] = 0x80 | (cp & 0x03F000) >>> 12; 357 | bytes[b++] = 0x80 | (cp & 0x000FC0) >>> 6; 358 | bytes[b++] = 0x80 | cp & 0x3F; 359 | } 360 | 361 | ++i; 362 | } 363 | 364 | this.write(bytes); 365 | return bytes.length; 366 | } // Aliases for reading/writing UTF-8 encoded strings 367 | // readUTFChars: this.::readString 368 | // writeUTFChars: this.::writeString 369 | // Reads UTF-8 encoded C-string (excluding the actual NULL-byte) 370 | 371 | }, { 372 | key: "readCString", 373 | value: function readCString() { 374 | var bytes = this._raw; 375 | var length = bytes.length; 376 | var i = this._index; 377 | 378 | while (bytes[i] !== 0x00 && i < length) { 379 | ++i; 380 | } 381 | 382 | length = i - this._index; 383 | 384 | if (length > 0) { 385 | var string = this.readString(length); 386 | this.readByte(); 387 | return string; 388 | } 389 | 390 | return null; 391 | } // Writes UTF-8 encoded C-string (NULL-terminated) 392 | 393 | }, { 394 | key: "writeCString", 395 | value: function writeCString(string) { 396 | var bytes = this.writeString(string); 397 | this.writeByte(0x00); 398 | return ++bytes; 399 | } // Prepends given number of bytes 400 | 401 | }, { 402 | key: "prepend", 403 | value: function prepend(bytes) { 404 | if (bytes <= 0) { 405 | throw new RangeError("Invalid number of bytes ".concat(bytes)); 406 | } 407 | 408 | var view = new Uint8Array(this.length + bytes); 409 | view.set(this._raw, bytes); 410 | this._index += bytes; 411 | this.buffer = view.buffer; 412 | return this; 413 | } // Appends given number of bytes 414 | 415 | }, { 416 | key: "append", 417 | value: function append(bytes) { 418 | if (bytes <= 0) { 419 | throw new RangeError("Invalid number of bytes ".concat(bytes)); 420 | } 421 | 422 | var view = new Uint8Array(this.length + bytes); 423 | view.set(this._raw, 0); 424 | this.buffer = view.buffer; 425 | return this; 426 | } // Clips this buffer 427 | 428 | }, { 429 | key: "clip", 430 | value: function clip() { 431 | var begin = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this._index; 432 | var end = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.length; 433 | 434 | if (begin < 0) { 435 | begin = this.length + begin; 436 | } 437 | 438 | var buffer = this._buffer.slice(begin, end); 439 | 440 | this._index -= begin; 441 | this.buffer = buffer; 442 | return this; 443 | } // Slices this buffer 444 | 445 | }, { 446 | key: "slice", 447 | value: function slice() { 448 | var begin = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; 449 | var end = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.length; 450 | var slice = new ByteBuffer(this._buffer.slice(begin, end), this.order); 451 | return slice; 452 | } // Clones this buffer 453 | 454 | }, { 455 | key: "clone", 456 | value: function clone() { 457 | var clone = new ByteBuffer(this._buffer.slice(0), this.order, this.implicitGrowth); 458 | clone.index = this._index; 459 | return clone; 460 | } // Reverses this buffer 461 | 462 | }, { 463 | key: "reverse", 464 | value: function reverse() { 465 | Array.prototype.reverse.call(this._raw); 466 | this._index = 0; 467 | return this; 468 | } // Array of bytes in this buffer 469 | 470 | }, { 471 | key: "toArray", 472 | value: function toArray() { 473 | return Array.prototype.slice.call(this._raw, 0); 474 | } // Hex representation of this buffer with given spacer 475 | 476 | }, { 477 | key: "toHex", 478 | value: function toHex() { 479 | var spacer = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ' '; 480 | return Array.prototype.map.call(this._raw, function (byte) { 481 | return "00".concat(byte.toString(16).toUpperCase()).slice(-2); 482 | }).join(spacer); 483 | } // ASCII representation of this buffer with given spacer and optional byte alignment 484 | 485 | }, { 486 | key: "toASCII", 487 | value: function toASCII() { 488 | var spacer = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ' '; 489 | var align = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; 490 | var unknown = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "\uFFFD"; 491 | var prefix = align ? ' ' : ''; 492 | return Array.prototype.map.call(this._raw, function (byte) { 493 | return byte < 0x20 || byte > 0x7E ? prefix + unknown : prefix + String.fromCharCode(byte); 494 | }).join(spacer); 495 | } 496 | }, { 497 | key: "buffer", 498 | get: function get() { 499 | return this._buffer; 500 | } // Sets new buffer and sanitizes read/write index 501 | , 502 | set: function set(buffer) { 503 | this._buffer = buffer; 504 | this._raw = new Uint8Array(this._buffer); 505 | this._view = new DataView(this._buffer); 506 | 507 | this._sanitizeIndex(); 508 | } // Retrieves raw buffer 509 | 510 | }, { 511 | key: "raw", 512 | get: function get() { 513 | return this._raw; 514 | } // Retrieves view 515 | 516 | }, { 517 | key: "view", 518 | get: function get() { 519 | return this._view; 520 | } // Retrieves number of bytes 521 | 522 | }, { 523 | key: "length", 524 | get: function get() { 525 | return this._buffer.byteLength; 526 | } // Retrieves number of bytes 527 | // Note: This allows for ByteBuffer to be detected as a proper source by its own constructor 528 | 529 | }, { 530 | key: "byteLength", 531 | get: function get() { 532 | return this.length; 533 | } // Retrieves byte order 534 | 535 | }, { 536 | key: "order", 537 | get: function get() { 538 | return this._order; 539 | } // Sets byte order 540 | , 541 | set: function set(order) { 542 | this._order = !!order; 543 | } // Retrieves implicit growth strategy 544 | 545 | }, { 546 | key: "implicitGrowth", 547 | get: function get() { 548 | return this._implicitGrowth; 549 | } // Sets implicit growth strategy 550 | , 551 | set: function set(implicitGrowth) { 552 | this._implicitGrowth = !!implicitGrowth; 553 | } // Retrieves read/write index 554 | 555 | }, { 556 | key: "index", 557 | get: function get() { 558 | return this._index; 559 | } // Sets read/write index 560 | , 561 | set: function set(index) { 562 | if (index < 0 || index > this.length) { 563 | throw new RangeError("Invalid index ".concat(index, ", should be between 0 and ").concat(this.length)); 564 | } 565 | 566 | this._index = index; 567 | } // Retrieves number of available bytes 568 | 569 | }, { 570 | key: "available", 571 | get: function get() { 572 | return this.length - this._index; 573 | } 574 | }]); 575 | 576 | return ByteBuffer; 577 | }(); // Generic reader 578 | 579 | 580 | var reader = function reader(method, bytes) { 581 | return function () { 582 | var order = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this._order; 583 | 584 | if (bytes > this.available) { 585 | throw new Error("Cannot read ".concat(bytes, " byte(s), ").concat(this.available, " available")); 586 | } 587 | 588 | var value = this._view[method](this._index, order); 589 | 590 | this._index += bytes; 591 | return value; 592 | }; 593 | }; // Generic writer 594 | 595 | 596 | var writer = function writer(method, bytes) { 597 | return function (value) { 598 | var order = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this._order; 599 | var available = this.available; 600 | 601 | if (bytes > available) { 602 | if (this._implicitGrowth) { 603 | this.append(bytes - available); 604 | } else { 605 | throw new Error("Cannot write ".concat(value, " using ").concat(bytes, " byte(s), ").concat(available, " available")); 606 | } 607 | } 608 | 609 | this._view[method](this._index, value, order); 610 | 611 | this._index += bytes; 612 | return this; 613 | }; 614 | }; // Byte order constants 615 | 616 | 617 | ByteBuffer.LITTLE_ENDIAN = true; 618 | ByteBuffer.BIG_ENDIAN = false; // Readers for bytes, shorts, integers, floats and doubles 619 | 620 | ByteBuffer.prototype.readByte = reader('getInt8', 1); 621 | ByteBuffer.prototype.readUnsignedByte = reader('getUint8', 1); 622 | ByteBuffer.prototype.readShort = reader('getInt16', 2); 623 | ByteBuffer.prototype.readUnsignedShort = reader('getUint16', 2); 624 | ByteBuffer.prototype.readInt = reader('getInt32', 4); 625 | ByteBuffer.prototype.readUnsignedInt = reader('getUint32', 4); 626 | ByteBuffer.prototype.readFloat = reader('getFloat32', 4); 627 | ByteBuffer.prototype.readDouble = reader('getFloat64', 8); // Writers for bytes, shorts, integers, floats and doubles 628 | 629 | ByteBuffer.prototype.writeByte = writer('setInt8', 1); 630 | ByteBuffer.prototype.writeUnsignedByte = writer('setUint8', 1); 631 | ByteBuffer.prototype.writeShort = writer('setInt16', 2); 632 | ByteBuffer.prototype.writeUnsignedShort = writer('setUint16', 2); 633 | ByteBuffer.prototype.writeInt = writer('setInt32', 4); 634 | ByteBuffer.prototype.writeUnsignedInt = writer('setUint32', 4); 635 | ByteBuffer.prototype.writeFloat = writer('setFloat32', 4); 636 | ByteBuffer.prototype.writeDouble = writer('setFloat64', 8); 637 | 638 | return ByteBuffer; 639 | 640 | }))); 641 | -------------------------------------------------------------------------------- /dist/byte-buffer.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * byte-buffer v2.0.0 3 | * Copyright (c) 2012-2020 Tim Kurvers 4 | * @license MIT 5 | * 6 | * Wrapper for JavaScript's ArrayBuffer/DataView. 7 | * 8 | * https://github.com/timkurvers/byte-buffer 9 | */ 10 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).ByteBuffer=e()}(this,(function(){"use strict";function t(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function e(t,e){for(var n=0;n1&&void 0!==arguments[1]&&arguments[1];if(t&&void 0!==t.byteLength)return void 0!==t.buffer?e?t.buffer.slice(0):t.buffer:e?t.slice(0):t;if(t&&void 0!==t.length){if(t.constructor===String)return null;try{return new Uint8Array(t).buffer}catch(t){return null}}return null},i=function(){function i(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.constructor.BIG_ENDIAN,o=arguments.length>2&&void 0!==arguments[2]&&arguments[2];t(this,i),this._buffer=null,this._raw=null,this._view=null,this._order=!!r,this._implicitGrowth=!!o,this._index=0;var a=n(e,!0);a||(a=new ArrayBuffer(e)),this.buffer=a}var r,o,a;return r=i,(o=[{key:"_sanitizeIndex",value:function(){this._index<0&&(this._index=0),this._index>this.length&&(this._index=this.length)}},{key:"front",value:function(){return this._index=0,this}},{key:"end",value:function(){return this._index=this.length,this}},{key:"seek",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1;return this.index+=t,this}},{key:"read",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.available;if(t>this.available)throw new Error("Cannot read ".concat(t," byte(s), ").concat(this.available," available"));if(t<=0)throw new RangeError("Invalid number of bytes ".concat(t));var e=new i(this._buffer.slice(this._index,this._index+t),this.order);return this._index+=t,e}},{key:"write",value:function(t){var e;if(t instanceof Uint8Array)e=t;else{var i=n(t);if(!i)throw new TypeError("Cannot write ".concat(t,", not a sequence"));e=new Uint8Array(i)}var r=this.available;if(e.byteLength>r){if(!this._implicitGrowth)throw new Error("Cannot write ".concat(t," using ").concat(e.byteLength," byte(s), ").concat(this.available," available"));this.append(e.byteLength-r)}return this._raw.set(e,this._index),this._index+=e.byteLength,this}},{key:"readString",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.available;if(t>this.available)throw new Error("Cannot read ".concat(t," byte(s), ").concat(this.available," available"));if(t<=0)throw new RangeError("Invalid number of bytes ".concat(t));for(var e=this._raw,n=[],i=0,r=null,o=null,a=null,h=null,s=this._index+t;this._index191)throw new Error("Bad continuation byte");n[i++]=((31&r)<<6)+(63&o),this._index+=2}else if(r<240){if((o=e[this._index+1])<128||o>191)throw new Error("Bad continuation byte");if((a=e[this._index+2])<128||a>191)throw new Error("Bad continuation byte");n[i++]=((15&r)<<12)+((63&o)<<6)+(63&a),this._index+=3}else{if(!(r<245))throw new Error("Illegal byte");if((o=e[this._index+1])<128||o>191)throw new Error("Bad continuation byte");if((a=e[this._index+2])<128||a>191)throw new Error("Bad continuation byte");if((h=e[this._index+3])<128||h>191)throw new Error("Bad continuation byte");var l=((7&r)<<18)+((63&o)<<12)+((63&a)<<6)+(63&h);l-=65536,n[i++]=55296+((1047552&l)>>>10),n[i++]=56320+(1023&l),this._index+=4}}var u=65536,f=n.length;if(f>>6,e[r++]=128|63&o;else if(o<=55295||o>=57344&&o<=65535)e[r++]=224|(61440&o)>>>12,e[r++]=128|(4032&o)>>>6,e[r++]=128|63&o;else{if(i===n-1)throw new Error("Unpaired surrogate ".concat(t[i]," (index ").concat(i,")"));var a=t.charCodeAt(++i);if(o<55296||o>56319||a<56320||a>57343)throw new Error("Unpaired surrogate ".concat(t[i]," (index ").concat(i,")"));var h=((1023&o)<<10)+(1023&a)+65536;e[r++]=240|(1835008&h)>>>18,e[r++]=128|(258048&h)>>>12,e[r++]=128|(4032&h)>>>6,e[r++]=128|63&h}++i}return this.write(e),e.length}},{key:"readCString",value:function(){for(var t=this._raw,e=t.length,n=this._index;0!==t[n]&&n0){var i=this.readString(e);return this.readByte(),i}return null}},{key:"writeCString",value:function(t){var e=this.writeString(t);return this.writeByte(0),++e}},{key:"prepend",value:function(t){if(t<=0)throw new RangeError("Invalid number of bytes ".concat(t));var e=new Uint8Array(this.length+t);return e.set(this._raw,t),this._index+=t,this.buffer=e.buffer,this}},{key:"append",value:function(t){if(t<=0)throw new RangeError("Invalid number of bytes ".concat(t));var e=new Uint8Array(this.length+t);return e.set(this._raw,0),this.buffer=e.buffer,this}},{key:"clip",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this._index,e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.length;t<0&&(t=this.length+t);var n=this._buffer.slice(t,e);return this._index-=t,this.buffer=n,this}},{key:"slice",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.length,n=new i(this._buffer.slice(t,e),this.order);return n}},{key:"clone",value:function(){var t=new i(this._buffer.slice(0),this.order,this.implicitGrowth);return t.index=this._index,t}},{key:"reverse",value:function(){return Array.prototype.reverse.call(this._raw),this._index=0,this}},{key:"toArray",value:function(){return Array.prototype.slice.call(this._raw,0)}},{key:"toHex",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:" ";return Array.prototype.map.call(this._raw,(function(t){return"00".concat(t.toString(16).toUpperCase()).slice(-2)})).join(t)}},{key:"toASCII",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:" ",e=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"�",i=e?" ":"";return Array.prototype.map.call(this._raw,(function(t){return t<32||t>126?i+n:i+String.fromCharCode(t)})).join(t)}},{key:"buffer",get:function(){return this._buffer},set:function(t){this._buffer=t,this._raw=new Uint8Array(this._buffer),this._view=new DataView(this._buffer),this._sanitizeIndex()}},{key:"raw",get:function(){return this._raw}},{key:"view",get:function(){return this._view}},{key:"length",get:function(){return this._buffer.byteLength}},{key:"byteLength",get:function(){return this.length}},{key:"order",get:function(){return this._order},set:function(t){this._order=!!t}},{key:"implicitGrowth",get:function(){return this._implicitGrowth},set:function(t){this._implicitGrowth=!!t}},{key:"index",get:function(){return this._index},set:function(t){if(t<0||t>this.length)throw new RangeError("Invalid index ".concat(t,", should be between 0 and ").concat(this.length));this._index=t}},{key:"available",get:function(){return this.length-this._index}}])&&e(r.prototype,o),a&&e(r,a),i}(),r=function(t,e){return function(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this._order;if(e>this.available)throw new Error("Cannot read ".concat(e," byte(s), ").concat(this.available," available"));var i=this._view[t](this._index,n);return this._index+=e,i}},o=function(t,e){return function(n){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this._order,r=this.available;if(e>r){if(!this._implicitGrowth)throw new Error("Cannot write ".concat(n," using ").concat(e," byte(s), ").concat(r," available"));this.append(e-r)}return this._view[t](this._index,n,i),this._index+=e,this}};return i.LITTLE_ENDIAN=!0,i.BIG_ENDIAN=!1,i.prototype.readByte=r("getInt8",1),i.prototype.readUnsignedByte=r("getUint8",1),i.prototype.readShort=r("getInt16",2),i.prototype.readUnsignedShort=r("getUint16",2),i.prototype.readInt=r("getInt32",4),i.prototype.readUnsignedInt=r("getUint32",4),i.prototype.readFloat=r("getFloat32",4),i.prototype.readDouble=r("getFloat64",8),i.prototype.writeByte=o("setInt8",1),i.prototype.writeUnsignedByte=o("setUint8",1),i.prototype.writeShort=o("setInt16",2),i.prototype.writeUnsignedShort=o("setUint16",2),i.prototype.writeInt=o("setInt32",4),i.prototype.writeUnsignedInt=o("setUint32",4),i.prototype.writeFloat=o("setFloat32",4),i.prototype.writeDouble=o("setFloat64",8),i})); 11 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { defaults } = require('jest-config'); 2 | 3 | module.exports = { 4 | collectCoverageFrom: [ 5 | 'lib/**/*.{js,mjs}', 6 | ], 7 | coverageDirectory: './coverage/', 8 | moduleFileExtensions: [ 9 | ...defaults.moduleFileExtensions, 10 | 'mjs', 11 | ], 12 | testEnvironment: 'node', 13 | testMatch: [ 14 | '**/spec/**/*.spec.mjs', 15 | ], 16 | transform: { 17 | '^.+\\.mjs$': 'babel-jest', 18 | }, 19 | verbose: true, 20 | }; 21 | -------------------------------------------------------------------------------- /lib/ByteBuffer.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable lines-between-class-members, no-param-reassign, prefer-spread */ 2 | 3 | import { extractBuffer } from './utils'; 4 | 5 | class ByteBuffer { 6 | // Creates a new ByteBuffer 7 | // - from given source (assumed to be number of bytes when numeric) 8 | // - with given byte order (defaults to big-endian) 9 | // - with given implicit growth strategy (defaults to false) 10 | constructor(source = 0, order = this.constructor.BIG_ENDIAN, implicitGrowth = false) { 11 | // Holds buffer 12 | this._buffer = null; 13 | 14 | // Holds raw buffer 15 | this._raw = null; 16 | 17 | // Holds internal view for reading/writing 18 | this._view = null; 19 | 20 | // Holds byte order 21 | this._order = !!order; 22 | 23 | // Holds implicit growth strategy 24 | this._implicitGrowth = !!implicitGrowth; 25 | 26 | // Holds read/write index 27 | this._index = 0; 28 | 29 | // Attempt to extract a buffer from given source 30 | let buffer = extractBuffer(source, true); 31 | 32 | // On failure, assume source is a primitive indicating the number of bytes 33 | if (!buffer) { 34 | buffer = new ArrayBuffer(source); 35 | } 36 | 37 | // Assign new buffer 38 | this.buffer = buffer; 39 | } 40 | 41 | // Sanitizes read/write index 42 | _sanitizeIndex() { 43 | if (this._index < 0) { 44 | this._index = 0; 45 | } 46 | if (this._index > this.length) { 47 | this._index = this.length; 48 | } 49 | } 50 | 51 | // Retrieves buffer 52 | get buffer() { 53 | return this._buffer; 54 | } 55 | 56 | // Sets new buffer and sanitizes read/write index 57 | set buffer(buffer) { 58 | this._buffer = buffer; 59 | this._raw = new Uint8Array(this._buffer); 60 | this._view = new DataView(this._buffer); 61 | this._sanitizeIndex(); 62 | } 63 | 64 | // Retrieves raw buffer 65 | get raw() { 66 | return this._raw; 67 | } 68 | 69 | // Retrieves view 70 | get view() { 71 | return this._view; 72 | } 73 | 74 | // Retrieves number of bytes 75 | get length() { 76 | return this._buffer.byteLength; 77 | } 78 | 79 | // Retrieves number of bytes 80 | // Note: This allows for ByteBuffer to be detected as a proper source by its own constructor 81 | get byteLength() { 82 | return this.length; 83 | } 84 | 85 | // Retrieves byte order 86 | get order() { 87 | return this._order; 88 | } 89 | 90 | // Sets byte order 91 | set order(order) { 92 | this._order = !!order; 93 | } 94 | 95 | // Retrieves implicit growth strategy 96 | get implicitGrowth() { 97 | return this._implicitGrowth; 98 | } 99 | 100 | // Sets implicit growth strategy 101 | set implicitGrowth(implicitGrowth) { 102 | this._implicitGrowth = !!implicitGrowth; 103 | } 104 | 105 | // Retrieves read/write index 106 | get index() { 107 | return this._index; 108 | } 109 | 110 | // Sets read/write index 111 | set index(index) { 112 | if (index < 0 || index > this.length) { 113 | throw new RangeError(`Invalid index ${index}, should be between 0 and ${this.length}`); 114 | } 115 | 116 | this._index = index; 117 | } 118 | 119 | // Retrieves number of available bytes 120 | get available() { 121 | return this.length - this._index; 122 | } 123 | 124 | // Sets index to front of the buffer 125 | front() { 126 | this._index = 0; 127 | return this; 128 | } 129 | 130 | // Sets index to end of the buffer 131 | end() { 132 | this._index = this.length; 133 | return this; 134 | } 135 | 136 | // Seeks given number of bytes 137 | // Note: Backwards seeking is supported 138 | seek(bytes = 1) { 139 | this.index += bytes; 140 | return this; 141 | } 142 | 143 | // Reads sequence of given number of bytes (defaults to number of bytes available) 144 | read(bytes = this.available) { 145 | if (bytes > this.available) { 146 | throw new Error(`Cannot read ${bytes} byte(s), ${this.available} available`); 147 | } 148 | 149 | if (bytes <= 0) { 150 | throw new RangeError(`Invalid number of bytes ${bytes}`); 151 | } 152 | 153 | const value = new ByteBuffer(this._buffer.slice(this._index, this._index + bytes), this.order); 154 | this._index += bytes; 155 | return value; 156 | } 157 | 158 | // Writes sequence of bytes 159 | write(sequence) { 160 | let view; 161 | 162 | // Ensure we're dealing with a Uint8Array view 163 | if (!(sequence instanceof Uint8Array)) { 164 | // Extract the buffer from the sequence 165 | const buffer = extractBuffer(sequence); 166 | if (!buffer) { 167 | throw new TypeError(`Cannot write ${sequence}, not a sequence`); 168 | } 169 | 170 | // And create a new Uint8Array view for it 171 | view = new Uint8Array(buffer); 172 | } else { 173 | view = sequence; 174 | } 175 | 176 | const { available } = this; 177 | if (view.byteLength > available) { 178 | if (this._implicitGrowth) { 179 | this.append(view.byteLength - available); 180 | } else { 181 | throw new Error(`Cannot write ${sequence} using ${view.byteLength} byte(s), ${this.available} available`); 182 | } 183 | } 184 | 185 | this._raw.set(view, this._index); 186 | this._index += view.byteLength; 187 | return this; 188 | } 189 | 190 | // Reads UTF-8 encoded string of given number of bytes (defaults to number of bytes available) 191 | // 192 | // Based on David Flanagan's BufferView (https://github.com/davidflanagan/BufferView/blob/master/BufferView.js//L195) 193 | readString(bytes = this.available) { 194 | if (bytes > this.available) { 195 | throw new Error(`Cannot read ${bytes} byte(s), ${this.available} available`); 196 | } 197 | 198 | if (bytes <= 0) { 199 | throw new RangeError(`Invalid number of bytes ${bytes}`); 200 | } 201 | 202 | // Local reference 203 | const raw = this._raw; 204 | 205 | // Holds decoded characters 206 | const codepoints = []; 207 | 208 | // Index into codepoints 209 | let c = 0; 210 | 211 | // Bytes 212 | let b1 = null; 213 | let b2 = null; 214 | let b3 = null; 215 | let b4 = null; 216 | 217 | // Target index 218 | const target = this._index + bytes; 219 | 220 | while (this._index < target) { 221 | b1 = raw[this._index]; 222 | 223 | if (b1 < 128) { 224 | // One byte sequence 225 | codepoints[c++] = b1; 226 | this._index++; 227 | } else if (b1 < 194) { 228 | throw new Error('Unexpected continuation byte'); 229 | } else if (b1 < 224) { 230 | // Two byte sequence 231 | b2 = raw[this._index + 1]; 232 | 233 | if (b2 < 128 || b2 > 191) { 234 | throw new Error('Bad continuation byte'); 235 | } 236 | 237 | codepoints[c++] = ((b1 & 0x1F) << 6) + (b2 & 0x3F); 238 | 239 | this._index += 2; 240 | } else if (b1 < 240) { 241 | // Three byte sequence 242 | b2 = raw[this._index + 1]; 243 | 244 | if (b2 < 128 || b2 > 191) { 245 | throw new Error('Bad continuation byte'); 246 | } 247 | 248 | b3 = raw[this._index + 2]; 249 | 250 | if (b3 < 128 || b3 > 191) { 251 | throw new Error('Bad continuation byte'); 252 | } 253 | 254 | codepoints[c++] = ((b1 & 0x0F) << 12) + ((b2 & 0x3F) << 6) + (b3 & 0x3F); 255 | 256 | this._index += 3; 257 | } else if (b1 < 245) { 258 | // Four byte sequence 259 | b2 = raw[this._index + 1]; 260 | 261 | if (b2 < 128 || b2 > 191) { 262 | throw new Error('Bad continuation byte'); 263 | } 264 | 265 | b3 = raw[this._index + 2]; 266 | 267 | if (b3 < 128 || b3 > 191) { 268 | throw new Error('Bad continuation byte'); 269 | } 270 | 271 | b4 = raw[this._index + 3]; 272 | 273 | if (b4 < 128 || b4 > 191) { 274 | throw new Error('Bad continuation byte'); 275 | } 276 | 277 | let cp = ((b1 & 0x07) << 18) + ((b2 & 0x3F) << 12) + ((b3 & 0x3F) << 6) + (b4 & 0x3F); 278 | cp -= 0x10000; 279 | 280 | // Turn code point into two surrogate pairs 281 | codepoints[c++] = 0xD800 + ((cp & 0x0FFC00) >>> 10); 282 | codepoints[c++] = 0xDC00 + (cp & 0x0003FF); 283 | 284 | this._index += 4; 285 | } else { 286 | throw new Error('Illegal byte'); 287 | } 288 | } 289 | 290 | // Browsers may have hardcoded or implicit limits on the array length when applying a function 291 | // See: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/apply//apply_and_built-in_functions 292 | const limit = 1 << 16; 293 | const { length } = codepoints; 294 | if (length < limit) { 295 | return String.fromCharCode.apply(String, codepoints); 296 | } 297 | const chars = []; 298 | let i = 0; 299 | while (i < length) { 300 | chars.push(String.fromCharCode.apply(String, codepoints.slice(i, i + limit))); 301 | i += limit; 302 | } 303 | return chars.join(''); 304 | } 305 | 306 | // Writes UTF-8 encoded string 307 | // Note: Does not write string length or terminator 308 | // 309 | // Based on David Flanagan's BufferView (https://github.com/davidflanagan/BufferView/blob/master/BufferView.js//L264) 310 | writeString(string) { 311 | // Encoded UTF-8 bytes 312 | const bytes = []; 313 | 314 | // String length, offset and byte offset 315 | const { length } = string; 316 | let i = 0; 317 | let b = 0; 318 | 319 | while (i < length) { 320 | const c = string.charCodeAt(i); 321 | 322 | if (c <= 0x7F) { 323 | // One byte sequence 324 | bytes[b++] = c; 325 | } else if (c <= 0x7FF) { 326 | // Two byte sequence 327 | bytes[b++] = 0xC0 | ((c & 0x7C0) >>> 6); 328 | bytes[b++] = 0x80 | (c & 0x3F); 329 | } else if (c <= 0xD7FF || (c >= 0xE000 && c <= 0xFFFF)) { 330 | // Three byte sequence 331 | // Source character is not a UTF-16 surrogate 332 | bytes[b++] = 0xE0 | ((c & 0xF000) >>> 12); 333 | bytes[b++] = 0x80 | ((c & 0x0FC0) >>> 6); 334 | bytes[b++] = 0x80 | (c & 0x3F); 335 | } else { 336 | // Four byte sequence 337 | if (i === length - 1) { 338 | throw new Error(`Unpaired surrogate ${string[i]} (index ${i})`); 339 | } 340 | 341 | // Retrieve surrogate 342 | const d = string.charCodeAt(++i); 343 | if (c < 0xD800 || c > 0xDBFF || d < 0xDC00 || d > 0xDFFF) { 344 | throw new Error(`Unpaired surrogate ${string[i]} (index ${i})`); 345 | } 346 | 347 | const cp = ((c & 0x03FF) << 10) + (d & 0x03FF) + 0x10000; 348 | 349 | bytes[b++] = 0xF0 | ((cp & 0x1C0000) >>> 18); 350 | bytes[b++] = 0x80 | ((cp & 0x03F000) >>> 12); 351 | bytes[b++] = 0x80 | ((cp & 0x000FC0) >>> 6); 352 | bytes[b++] = 0x80 | (cp & 0x3F); 353 | } 354 | 355 | ++i; 356 | } 357 | 358 | this.write(bytes); 359 | 360 | return bytes.length; 361 | } 362 | 363 | // Aliases for reading/writing UTF-8 encoded strings 364 | // readUTFChars: this.::readString 365 | // writeUTFChars: this.::writeString 366 | 367 | // Reads UTF-8 encoded C-string (excluding the actual NULL-byte) 368 | readCString() { 369 | const bytes = this._raw; 370 | let { length } = bytes; 371 | let i = this._index; 372 | while (bytes[i] !== 0x00 && i < length) { 373 | ++i; 374 | } 375 | 376 | length = i - this._index; 377 | if (length > 0) { 378 | const string = this.readString(length); 379 | this.readByte(); 380 | return string; 381 | } 382 | 383 | return null; 384 | } 385 | 386 | // Writes UTF-8 encoded C-string (NULL-terminated) 387 | writeCString(string) { 388 | let bytes = this.writeString(string); 389 | this.writeByte(0x00); 390 | return ++bytes; 391 | } 392 | 393 | // Prepends given number of bytes 394 | prepend(bytes) { 395 | if (bytes <= 0) { 396 | throw new RangeError(`Invalid number of bytes ${bytes}`); 397 | } 398 | 399 | const view = new Uint8Array(this.length + bytes); 400 | view.set(this._raw, bytes); 401 | this._index += bytes; 402 | this.buffer = view.buffer; 403 | return this; 404 | } 405 | 406 | // Appends given number of bytes 407 | append(bytes) { 408 | if (bytes <= 0) { 409 | throw new RangeError(`Invalid number of bytes ${bytes}`); 410 | } 411 | 412 | const view = new Uint8Array(this.length + bytes); 413 | view.set(this._raw, 0); 414 | this.buffer = view.buffer; 415 | return this; 416 | } 417 | 418 | // Clips this buffer 419 | clip(begin = this._index, end = this.length) { 420 | if (begin < 0) { 421 | begin = this.length + begin; 422 | } 423 | const buffer = this._buffer.slice(begin, end); 424 | this._index -= begin; 425 | this.buffer = buffer; 426 | return this; 427 | } 428 | 429 | // Slices this buffer 430 | slice(begin = 0, end = this.length) { 431 | const slice = new ByteBuffer(this._buffer.slice(begin, end), this.order); 432 | return slice; 433 | } 434 | 435 | // Clones this buffer 436 | clone() { 437 | const clone = new ByteBuffer(this._buffer.slice(0), this.order, this.implicitGrowth); 438 | clone.index = this._index; 439 | return clone; 440 | } 441 | 442 | // Reverses this buffer 443 | reverse() { 444 | Array.prototype.reverse.call(this._raw); 445 | this._index = 0; 446 | return this; 447 | } 448 | 449 | // Array of bytes in this buffer 450 | toArray() { 451 | return Array.prototype.slice.call(this._raw, 0); 452 | } 453 | 454 | // Hex representation of this buffer with given spacer 455 | toHex(spacer = ' ') { 456 | return Array.prototype.map.call(this._raw, (byte) => ( 457 | `00${byte.toString(16).toUpperCase()}`.slice(-2) 458 | )).join(spacer); 459 | } 460 | 461 | // ASCII representation of this buffer with given spacer and optional byte alignment 462 | toASCII(spacer = ' ', align = true, unknown = '\uFFFD') { 463 | const prefix = (align) ? ' ' : ''; 464 | return Array.prototype.map.call(this._raw, (byte) => ( 465 | (byte < 0x20 || byte > 0x7E) ? prefix + unknown : prefix + String.fromCharCode(byte) 466 | )).join(spacer); 467 | } 468 | } 469 | 470 | // Generic reader 471 | const reader = function (method, bytes) { 472 | return function (order = this._order) { 473 | if (bytes > this.available) { 474 | throw new Error(`Cannot read ${bytes} byte(s), ${this.available} available`); 475 | } 476 | 477 | const value = this._view[method](this._index, order); 478 | this._index += bytes; 479 | return value; 480 | }; 481 | }; 482 | 483 | // Generic writer 484 | const writer = function (method, bytes) { 485 | return function (value, order = this._order) { 486 | const { available } = this; 487 | if (bytes > available) { 488 | if (this._implicitGrowth) { 489 | this.append(bytes - available); 490 | } else { 491 | throw new Error(`Cannot write ${value} using ${bytes} byte(s), ${available} available`); 492 | } 493 | } 494 | 495 | this._view[method](this._index, value, order); 496 | this._index += bytes; 497 | return this; 498 | }; 499 | }; 500 | 501 | // Byte order constants 502 | ByteBuffer.LITTLE_ENDIAN = true; 503 | ByteBuffer.BIG_ENDIAN = false; 504 | 505 | // Readers for bytes, shorts, integers, floats and doubles 506 | ByteBuffer.prototype.readByte = reader('getInt8', 1); 507 | ByteBuffer.prototype.readUnsignedByte = reader('getUint8', 1); 508 | ByteBuffer.prototype.readShort = reader('getInt16', 2); 509 | ByteBuffer.prototype.readUnsignedShort = reader('getUint16', 2); 510 | ByteBuffer.prototype.readInt = reader('getInt32', 4); 511 | ByteBuffer.prototype.readUnsignedInt = reader('getUint32', 4); 512 | ByteBuffer.prototype.readFloat = reader('getFloat32', 4); 513 | ByteBuffer.prototype.readDouble = reader('getFloat64', 8); 514 | 515 | // Writers for bytes, shorts, integers, floats and doubles 516 | ByteBuffer.prototype.writeByte = writer('setInt8', 1); 517 | ByteBuffer.prototype.writeUnsignedByte = writer('setUint8', 1); 518 | ByteBuffer.prototype.writeShort = writer('setInt16', 2); 519 | ByteBuffer.prototype.writeUnsignedShort = writer('setUint16', 2); 520 | ByteBuffer.prototype.writeInt = writer('setInt32', 4); 521 | ByteBuffer.prototype.writeUnsignedInt = writer('setUint32', 4); 522 | ByteBuffer.prototype.writeFloat = writer('setFloat32', 4); 523 | ByteBuffer.prototype.writeDouble = writer('setFloat64', 8); 524 | 525 | export default ByteBuffer; 526 | -------------------------------------------------------------------------------- /lib/utils.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | 3 | // Extracts buffer from given source and optionally clones it 4 | export const extractBuffer = (source, clone = false) => { 5 | // Source is a byte-aware object 6 | if (source && typeof source.byteLength !== 'undefined') { 7 | // Determine whether source is a view or a raw buffer 8 | if (typeof source.buffer !== 'undefined') { 9 | return clone ? source.buffer.slice(0) : source.buffer; 10 | } 11 | return clone ? source.slice(0) : source; 12 | } 13 | 14 | // Source is a sequence of bytes 15 | if (source && typeof source.length !== 'undefined') { 16 | // Although Uint8Array's constructor succeeds when given strings, 17 | // it does not correctly instantiate the buffer 18 | if (source.constructor === String) { 19 | return null; 20 | } 21 | 22 | try { 23 | return (new Uint8Array(source)).buffer; 24 | } catch (error) { 25 | return null; 26 | } 27 | } 28 | 29 | // No buffer found 30 | return null; 31 | }; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "byte-buffer", 3 | "version": "2.0.0", 4 | "description": "Wrapper for JavaScript's ArrayBuffer/DataView", 5 | "author": "Tim Kurvers ", 6 | "repository": "timkurvers/byte-buffer", 7 | "license": "MIT", 8 | "keywords": [ 9 | "byte", 10 | "byte buffer", 11 | "bytebuffer" 12 | ], 13 | "main": "dist/byte-buffer.js", 14 | "module": "lib/byte-buffer.mjs", 15 | "files": [ 16 | "dist", 17 | "lib", 18 | "CHANGELOG.md", 19 | "LICENSE.md", 20 | "README.md" 21 | ], 22 | "scripts": { 23 | "build": "rollup -c", 24 | "lint": "eslint . --ignore-pattern dist --ext js,mjs", 25 | "test": "jest", 26 | "test:coverage": "jest --coverage", 27 | "test:watch": "jest --watch" 28 | }, 29 | "dependencies": {}, 30 | "devDependencies": { 31 | "@babel/core": "^7.8.7", 32 | "@babel/preset-env": "^7.14.2", 33 | "@timkurvers/eslint-config": "^2.0.0", 34 | "babel-eslint": "^10.1.0", 35 | "babel-jest": "^25.1.0", 36 | "eslint": "^6.8.0", 37 | "eslint-config-airbnb": "^18.1.0", 38 | "eslint-plugin-import": "^2.20.1", 39 | "jest": "^26.6.3", 40 | "jest-config": "^26.6.3", 41 | "rollup": "^2.0.6", 42 | "rollup-plugin-babel": "^4.4.0", 43 | "rollup-plugin-terser": "^7.0.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import { terser } from 'rollup-plugin-terser'; 3 | 4 | import pkg from './package.json'; 5 | 6 | const banner = `/** 7 | * ${pkg.name} v${pkg.version} 8 | * Copyright (c) 2012-2020 ${pkg.author} 9 | * @license ${pkg.license} 10 | * 11 | * ${pkg.description}. 12 | * 13 | * https://github.com/${pkg.repository} 14 | */`; 15 | 16 | export default { 17 | input: 'lib/ByteBuffer.mjs', 18 | plugins: [babel()], 19 | output: [ 20 | { 21 | banner, 22 | file: 'dist/byte-buffer.js', 23 | format: 'umd', 24 | name: 'ByteBuffer', 25 | }, 26 | { 27 | banner, 28 | file: 'dist/byte-buffer.min.js', 29 | format: 'umd', 30 | name: 'ByteBuffer', 31 | plugins: [terser()], 32 | }, 33 | ], 34 | }; 35 | -------------------------------------------------------------------------------- /spec/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /spec/ByteBuffer.spec.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable guard-for-in, no-restricted-syntax */ 2 | 3 | import { ByteBuffer } from './spec-helper'; 4 | 5 | describe('ByteBuffer', () => { 6 | describe('constructor', () => { 7 | it('initializes by length', () => { 8 | const b = new ByteBuffer(1); 9 | expect(b.length).toEqual(1); 10 | }); 11 | 12 | it('initializes from an ArrayBuffer', () => { 13 | const b = new ByteBuffer(new ArrayBuffer(2)); 14 | expect(b.length).toBe(2); 15 | }); 16 | 17 | it('initializes from an Uint8Array', () => { 18 | const b = new ByteBuffer(new Uint8Array(3)); 19 | expect(b.length).toBe(3); 20 | }); 21 | 22 | it('initializes from an Uint16Array', () => { 23 | const b = new ByteBuffer(new Uint16Array(1)); 24 | expect(b.length).toBe(2); 25 | }); 26 | 27 | it('initializes from an array', () => { 28 | const b = new ByteBuffer([0, 1, 2, 3]); 29 | expect(b.length).toBe(4); 30 | }); 31 | 32 | it('initializes from another ByteBuffer', () => { 33 | const b = new ByteBuffer(new ByteBuffer(4)); 34 | expect(b.length).toBe(4); 35 | }); 36 | 37 | it('initializes without crashing when given odd objects', () => { 38 | const b = new ByteBuffer({ length: Infinity }); 39 | expect(b.length).toBe(0); 40 | }); 41 | }); 42 | 43 | describe('get/set buffer', () => { 44 | const original = new ByteBuffer([1, 2, 3]); 45 | const { buffer } = original; 46 | 47 | it('sets and returns raw buffer', () => { 48 | const b = new ByteBuffer(buffer); 49 | expect(b.buffer).not.toBe(buffer); 50 | expect(b.buffer).toBeInstanceOf(ArrayBuffer); 51 | }); 52 | 53 | it('does not mutate given buffer', () => { 54 | const b = new ByteBuffer(buffer); 55 | b.write([4, 5, 6]); 56 | expect(original.toArray()).toEqual([1, 2, 3]); 57 | }); 58 | 59 | it('ensures index is within bounds', () => { 60 | const b = new ByteBuffer(); 61 | 62 | b._index = -1; 63 | b.buffer = buffer; 64 | expect(b.index).toBe(0); 65 | 66 | b._index = 4; 67 | b.buffer = buffer; 68 | expect(b.index).toBe(3); 69 | }); 70 | }); 71 | 72 | describe('get raw', () => { 73 | it('returns raw Uint8Array', () => { 74 | const b = new ByteBuffer([1, 2]); 75 | expect(b.raw).toBeInstanceOf(Uint8Array); 76 | }); 77 | }); 78 | 79 | describe('get view', () => { 80 | it('returns underlying view', () => { 81 | const b = new ByteBuffer(1); 82 | expect(b.view).toBeInstanceOf(DataView); 83 | }); 84 | }); 85 | 86 | describe('get length / byteLength', () => { 87 | it('returns number of bytes in buffer', () => { 88 | const b = new ByteBuffer(42); 89 | expect(b.length).toBe(42); 90 | expect(b.byteLength).toBe(42); 91 | }); 92 | }); 93 | 94 | describe('get/set order', () => { 95 | it('sets and gets byte order', () => { 96 | const b = new ByteBuffer(1, ByteBuffer.LITTLE_ENDIAN); 97 | expect(b.order).toBe(ByteBuffer.LITTLE_ENDIAN); 98 | b.order = ByteBuffer.BIG_ENDIAN; 99 | expect(b.order).toBe(ByteBuffer.BIG_ENDIAN); 100 | }); 101 | 102 | it('exposes byte order constants', () => { 103 | expect(ByteBuffer.BIG_ENDIAN).toBe(false); 104 | expect(ByteBuffer.LITTLE_ENDIAN).toBe(true); 105 | }); 106 | 107 | it('defaults to big endian', () => { 108 | const b = new ByteBuffer(1); 109 | expect(b.order).toBe(ByteBuffer.BIG_ENDIAN); 110 | }); 111 | 112 | it('may be provided during initialization', () => { 113 | const b = new ByteBuffer(1, ByteBuffer.BIG_ENDIAN); 114 | expect(b.order).toBe(ByteBuffer.BIG_ENDIAN); 115 | }); 116 | 117 | it('maintains byte order when reading, slicing and cloning', () => { 118 | let b = new ByteBuffer(1); 119 | expect(b.read().order).toBe(ByteBuffer.BIG_ENDIAN); 120 | expect(b.slice().order).toBe(ByteBuffer.BIG_ENDIAN); 121 | expect(b.clone().order).toBe(ByteBuffer.BIG_ENDIAN); 122 | 123 | b = new ByteBuffer(1, ByteBuffer.LITTLE_ENDIAN); 124 | expect(b.read().order).toBe(ByteBuffer.LITTLE_ENDIAN); 125 | expect(b.slice().order).toBe(ByteBuffer.LITTLE_ENDIAN); 126 | expect(b.clone().order).toBe(ByteBuffer.LITTLE_ENDIAN); 127 | }); 128 | }); 129 | 130 | describe('implicitGrowth', () => { 131 | describe('when disabled', () => { 132 | it('throws Error when writing', () => { 133 | const b = new ByteBuffer(1); 134 | expect(b.implicitGrowth).toBe(false); 135 | expect(() => { 136 | b.writeDouble(0); 137 | }).toThrow(Error); 138 | }); 139 | }); 140 | 141 | describe('when enabled', () => { 142 | it('grows implicitly when writing', () => { 143 | const b = new ByteBuffer(2, ByteBuffer.BIG_ENDIAN, true); 144 | expect(b.implicitGrowth).toBe(true); 145 | expect(b.writeUnsignedInt(0).length).toBe(4); 146 | expect(b.writeString('Byte $\u00A2\u20AC\uD834\uDDC7 Buffer')).toBe(22); 147 | expect(b.length).toBe(26); 148 | }); 149 | }); 150 | 151 | it('maintains implicit growth strategy when cloning', () => { 152 | const b = new ByteBuffer(1); 153 | expect(b.clone().implicitGrowth).toBe(false); 154 | b.implicitGrowth = true; 155 | expect(b.clone().implicitGrowth).toBe(true); 156 | }); 157 | }); 158 | 159 | describe('get/set index', () => { 160 | describe('when within valid range', () => { 161 | it('returns read/write index', () => { 162 | const b = new ByteBuffer(8); 163 | expect(b.index).toBe(0); 164 | }); 165 | }); 166 | 167 | describe('when outside of valid range', () => { 168 | it('throws RangeError', () => { 169 | const b = new ByteBuffer(8); 170 | expect(() => { 171 | b.index = -1; 172 | }).toThrow(RangeError); 173 | 174 | expect(() => { 175 | b.index = 9; 176 | }).toThrow(RangeError); 177 | }); 178 | }); 179 | }); 180 | 181 | describe('get available', () => { 182 | it('returns number of bytes available', () => { 183 | const b = new ByteBuffer(8); 184 | expect(b.available).toBe(8); 185 | 186 | b.index = 4; 187 | expect(b.available).toBe(4); 188 | 189 | expect(b.end().available).toBe(0); 190 | }); 191 | }); 192 | 193 | describe('front()', () => { 194 | it('sets read/write index to front of buffer', () => { 195 | const b = new ByteBuffer(8); 196 | expect(b.front().index).toBe(0); 197 | }); 198 | }); 199 | 200 | describe('end()', () => { 201 | it('sets read/write index to end of buffer', () => { 202 | const b = new ByteBuffer(8); 203 | expect(b.end().index).toBe(8); 204 | }); 205 | }); 206 | 207 | describe('seek()', () => { 208 | describe('when within valid range', () => { 209 | it('seeks by relative offset', () => { 210 | const b = new ByteBuffer(4); 211 | expect(b.seek().index).toBe(1); 212 | expect(b.seek(2).index).toBe(3); 213 | expect(b.seek(-1).index).toBe(2); 214 | }); 215 | }); 216 | 217 | describe('when outside of valid range', () => { 218 | it('throws RangeError', () => { 219 | const b = new ByteBuffer(2); 220 | expect(() => { 221 | b.seek(3); 222 | }).toThrow(RangeError); 223 | }); 224 | }); 225 | }); 226 | 227 | const types = { /* eslint-disable key-spacing */ 228 | Byte: -1 << 7, 229 | UnsignedByte: 1 << 7, 230 | Short: -1 << 15, 231 | UnsignedShort: 1 << 15, 232 | Int: -1 << 30, 233 | UnsignedInt: 1 << 30, 234 | Float: Math.PI, 235 | Double: Math.PI, 236 | }; 237 | 238 | for (const type in types) { 239 | const value = types[type]; 240 | const writer = `write${type}`; 241 | const reader = `read${type}`; 242 | 243 | describe(`write${type}() / read${type}()`, () => { 244 | it('writes value, returns buffer and reads value', () => { 245 | const b = new ByteBuffer(8); 246 | const result = b[writer](value); 247 | expect(result).toBe(b); 248 | 249 | if (['Float', 'Double'].indexOf(type) !== -1) { 250 | expect(b.front()[reader]()).toBeCloseTo(value, 0.0000001); 251 | } else { 252 | expect(b.front()[reader]()).toBe(value); 253 | } 254 | }); 255 | 256 | describe('when writing and no bytes available', () => { 257 | it('throws Error', () => { 258 | const b = new ByteBuffer(1); 259 | b.end(); 260 | expect(() => { 261 | b[writer](value); 262 | }).toThrow(Error); 263 | }); 264 | }); 265 | 266 | describe('when reading and no bytes available', () => { 267 | it('throws Error', () => { 268 | const b = new ByteBuffer(1); 269 | b.end(); 270 | expect(() => { 271 | b[reader](); 272 | }).toThrow(Error); 273 | }); 274 | }); 275 | }); 276 | } 277 | 278 | describe('write() / read()', () => { 279 | it('writes Uint8Arrays, returns buffer and reads sequence', () => { 280 | const b = new ByteBuffer(2); 281 | const result = b.write(new Uint8Array([1, 2])); 282 | expect(result).toBe(b); 283 | expect(b.front().read(2).toArray()).toEqual([1, 2]); 284 | }); 285 | 286 | it('writes Uint16Arrays, returns buffer and reads sequence', () => { 287 | const b = new ByteBuffer(2); 288 | const result = b.write(new Uint16Array([(1 << 16) - 1])); 289 | expect(result).toBe(b); 290 | expect(b.front().read(2).toArray()).toEqual([255, 255]); 291 | }); 292 | 293 | it('writes another ByteBuffer, returns buffer and reads sequence', () => { 294 | const b = new ByteBuffer(2); 295 | const result = b.write(new ByteBuffer([3, 4])); 296 | expect(result).toBe(b); 297 | expect(b.front().read(2).toArray()).toEqual([3, 4]); 298 | }); 299 | 300 | it('writes arrays, returns buffer and reads sequence', () => { 301 | const b = new ByteBuffer(2); 302 | const result = b.write([13, 37]); 303 | expect(result).toBe(b); 304 | expect(b.front().read(2).toArray()).toEqual([13, 37]); 305 | }); 306 | 307 | describe('when writing non-sequences', () => { 308 | it('throws TypeError', () => { 309 | const b = new ByteBuffer(8); 310 | 311 | expect(() => { 312 | b.write(666); 313 | }).toThrow(TypeError); 314 | 315 | expect(() => { 316 | b.write('unwritable'); 317 | }).toThrow(TypeError); 318 | }); 319 | }); 320 | 321 | describe('when writing and no bytes available', () => { 322 | it('throws Error', () => { 323 | const b = new ByteBuffer(1); 324 | b.end(); 325 | expect(() => { 326 | b.write([1]); 327 | }).toThrow(Error); 328 | }); 329 | }); 330 | 331 | describe('when reading and no bytes available', () => { 332 | it('throws Error', () => { 333 | const b = new ByteBuffer(1); 334 | b.end(); 335 | expect(() => { 336 | b.read(1); 337 | }).toThrow(Error); 338 | }); 339 | }); 340 | 341 | describe('when reading outside of valid range', () => { 342 | it('throws RangeError', () => { 343 | const b = new ByteBuffer(1); 344 | expect(() => { 345 | b.read(-1); 346 | }).toThrow(RangeError); 347 | }); 348 | }); 349 | }); 350 | 351 | describe('writeString() / readString()', () => { 352 | it('writes utf-8 strings, returns bytes used and reads strings', () => { 353 | let b = new ByteBuffer(22); 354 | 355 | expect(b.writeString('Byte $\u00A2\u20AC\uD834\uDDC7 Buffer')).toBe(22); 356 | expect(b.index).toBe(22); 357 | expect(b.toHex()).toBe('42 79 74 65 20 24 C2 A2 E2 82 AC F0 9D 87 87 20 42 75 66 66 65 72'); 358 | 359 | b.front(); 360 | 361 | expect(b.readString()).toBe('Byte $\u00A2\u20AC\uD834\uDDC7 Buffer'); 362 | 363 | b = new ByteBuffer(262140); 364 | const long = (new Array(1 << 16)).join('\uD834\uDDC7'); 365 | expect(b.writeString(long)).toBe(262140); 366 | 367 | b.front(); 368 | 369 | expect(b.readString()).toBe(long); 370 | }); 371 | 372 | describe('when writing and no bytes available', () => { 373 | it('throws Error', () => { 374 | const b = new ByteBuffer(1); 375 | b.end(); 376 | expect(() => { 377 | b.writeString('foo'); 378 | }).toThrow(Error); 379 | }); 380 | }); 381 | 382 | describe('when reading and no bytes available', () => { 383 | it('throws Error', () => { 384 | const b = new ByteBuffer(1); 385 | b.end(); 386 | expect(() => { 387 | b.readString(1); 388 | }).toThrow(Error); 389 | }); 390 | }); 391 | 392 | describe('when reading outside of valid range', () => { 393 | it('throws RangeError', () => { 394 | const b = new ByteBuffer(1); 395 | expect(() => { 396 | b.readString(-1); 397 | }).toThrow(RangeError); 398 | }); 399 | }); 400 | }); 401 | 402 | describe('writeCString() / readCString()', () => { 403 | it('writes NULL-terminated C-strings, returns bytes used and reads strings', () => { 404 | const b = new ByteBuffer(27); 405 | 406 | expect(b.writeCString('Byte $\u00A2\u20AC\uD834\uDDC7 Buffer')).toBe(23); 407 | b.writeUnsignedInt(10); 408 | 409 | b.front(); 410 | 411 | expect(b.readCString()).toBe('Byte $\u00A2\u20AC\uD834\uDDC7 Buffer'); 412 | expect(b.available).toBe(4); 413 | }); 414 | 415 | describe('when writing and no bytes available', () => { 416 | it('throws Error', () => { 417 | const b = new ByteBuffer(1); 418 | b.end(); 419 | expect(() => { 420 | b.writeCString('foo'); 421 | }).toThrow(Error); 422 | }); 423 | }); 424 | 425 | describe('when reading and null byte immediately encountered', () => { 426 | it('returns null', () => { 427 | const b = new ByteBuffer([0]); 428 | expect(b.readCString()).toBeNull(); 429 | }); 430 | }); 431 | }); 432 | 433 | describe('prepend()', () => { 434 | it('grows buffer at the front', () => { 435 | const b = new ByteBuffer([1, 2]); 436 | expect(b.prepend(2).toArray()).toEqual([0, 0, 1, 2]); 437 | expect(b.index).toBe(2); 438 | }); 439 | 440 | describe('when given invalid number of bytes', () => { 441 | it('throws Error', () => { 442 | const b = new ByteBuffer(); 443 | expect(() => { 444 | b.prepend(-1); 445 | }).toThrow(Error); 446 | }); 447 | }); 448 | }); 449 | 450 | describe('append()', () => { 451 | it('grows buffer at the end', () => { 452 | const b = new ByteBuffer([1, 2]); 453 | expect(b.append(2).toArray()).toEqual([1, 2, 0, 0]); 454 | expect(b.index).toBe(0); 455 | }); 456 | 457 | describe('when given invalid number of bytes', () => { 458 | it('throws Error', () => { 459 | const b = new ByteBuffer(); 460 | expect(() => { 461 | b.append(-1); 462 | }).toThrow(Error); 463 | }); 464 | }); 465 | }); 466 | 467 | describe('clip()', () => { 468 | it('clips buffer from current index until end', () => { 469 | const b = new ByteBuffer([1, 2, 3, 4, 5, 6]); 470 | b.index = 3; 471 | expect(b.clip().toArray()).toEqual([4, 5, 6]); 472 | expect(b.index).toBe(0); 473 | }); 474 | 475 | it('clips buffer from given index until end', () => { 476 | const b = new ByteBuffer([1, 2, 3, 4, 5, 6]); 477 | b.index = 3; 478 | expect(b.clip(2).toArray()).toEqual([3, 4, 5, 6]); 479 | expect(b.index).toBe(1); 480 | }); 481 | 482 | it('clips buffer from given negative index until end', () => { 483 | const b = new ByteBuffer([1, 2, 3, 4, 5, 6]); 484 | b.index = 4; 485 | expect(b.clip(-4).toArray()).toEqual([3, 4, 5, 6]); 486 | expect(b.index).toBe(2); 487 | }); 488 | 489 | it('clips buffer from given index until given negative end', () => { 490 | const b = new ByteBuffer([1, 2, 3, 4, 5, 6]); 491 | b.index = 3; 492 | expect(b.clip(2, -2).toArray()).toEqual([3, 4]); 493 | expect(b.index).toBe(1); 494 | }); 495 | }); 496 | 497 | describe('slice()', () => { 498 | it('slices buffer returning a new copy', () => { 499 | const b = new ByteBuffer([1, 2, 3, 4, 5, 6]); 500 | 501 | expect(b.slice().toArray()).toEqual([1, 2, 3, 4, 5, 6]); 502 | expect(b.slice(1).toArray()).toEqual([2, 3, 4, 5, 6]); 503 | expect(b.slice(2, -2).toArray()).toEqual([3, 4]); 504 | 505 | expect(b.slice()).not.toBe(b); 506 | }); 507 | }); 508 | 509 | describe('clone()', () => { 510 | it('clones buffer', () => { 511 | const b = new ByteBuffer(3); 512 | b.end(); 513 | 514 | const clone = b.clone(); 515 | expect(clone).toEqual(b); 516 | expect(clone).not.toBe(b); 517 | }); 518 | }); 519 | 520 | describe('reverse()', () => { 521 | it('reverses/flips buffer', () => { 522 | const b = new ByteBuffer([1, 2, 3]); 523 | b.end(); 524 | 525 | expect(b.reverse().toArray()).toEqual([3, 2, 1]); 526 | expect(b.index).toBe(0); 527 | }); 528 | }); 529 | 530 | describe('representations', () => { 531 | const b = new ByteBuffer([245, 66, 121, 116, 101, 215, 66, 117, 102, 102, 101, 114, 0]); 532 | 533 | describe('toArray()', () => { 534 | it('returns buffer contents as an array', () => { 535 | expect(b.toArray()).toEqual([245, 66, 121, 116, 101, 215, 66, 117, 102, 102, 101, 114, 0]); 536 | }); 537 | }); 538 | 539 | describe('toHex()', () => { 540 | it('returns hex string representation of buffer', () => { 541 | expect(b.toHex()).toBe('F5 42 79 74 65 D7 42 75 66 66 65 72 00'); 542 | expect(b.toHex('')).toBe('F542797465D742756666657200'); 543 | }); 544 | }); 545 | 546 | describe('toASCII()', () => { 547 | it('returns ASCII string representation of buffer', () => { 548 | expect(b.toASCII()).toBe(' \uFFFD B y t e \uFFFD B u f f e r \uFFFD'); 549 | expect(b.toASCII('')).toBe(' \uFFFD B y t e \uFFFD B u f f e r \uFFFD'); 550 | expect(b.toASCII('', false)).toBe('\uFFFDByte\uFFFDBuffer\uFFFD'); 551 | }); 552 | }); 553 | }); 554 | 555 | describe('class inheritance', () => { 556 | class NetworkPacket extends ByteBuffer {} 557 | const p = new NetworkPacket(1); 558 | 559 | it('maintains byte order', () => { 560 | expect(p.order).toBe(ByteBuffer.BIG_ENDIAN); 561 | }); 562 | 563 | it('returns ByteBuffer when reading', () => { 564 | expect(p.read().constructor).toBe(ByteBuffer); 565 | }); 566 | 567 | it('returns ByteBuffer when cloning', () => { 568 | expect(p.clone().constructor).toBe(ByteBuffer); 569 | }); 570 | }); 571 | }); 572 | -------------------------------------------------------------------------------- /spec/spec-helper.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | 3 | import ByteBuffer from '../lib/ByteBuffer'; 4 | 5 | export { ByteBuffer }; 6 | --------------------------------------------------------------------------------