|
| 1 | +# buffer-codec |
| 2 | + |
| 3 | +This package provides a set of functions for encoding and decoding typed arrays |
| 4 | +to/from various base encodings (c.f. [RFC |
| 5 | +4648](https://datatracker.ietf.org/doc/html/rfc4648)), and for slicing and |
| 6 | +combining the bits of these arrays into values of different sizes and |
| 7 | +endiannesses. It principally exists to support the |
| 8 | +[@escodb/buffer](https://github.com/escodb/buffer) package but also supports |
| 9 | +encodings that are not part of the `Buffer` interface. |
| 10 | + |
| 11 | +All the functions for converting binary data are constant-time. The same set of |
| 12 | +bitwise and arithmetic operations are applied to every unit of input, and the |
| 13 | +values in the input are not used to drive any branching decisions or table |
| 14 | +lookups. The only thing that should affect how long a decoding function takes to |
| 15 | +run is the length of the input string. |
| 16 | + |
| 17 | + |
| 18 | +## Installation |
| 19 | + |
| 20 | + npm install @escodb/buffer-codec |
| 21 | + |
| 22 | + |
| 23 | +## Usage |
| 24 | + |
| 25 | +The package's functionality breaks down into three main areas: |
| 26 | + |
| 27 | +- Converting strings to/from their UTF-8 representation via the `utf8` module |
| 28 | +- Encoding and decoding binary data to/from base 16, 32, or 64 via the `hex`, |
| 29 | + `base32` and `base64` modules |
| 30 | +- Slicing/combining the bits of array values into arbitrary-sized chunks via the |
| 31 | + `transcodeBE()` and `transcodeLE()` functions |
| 32 | + |
| 33 | + |
| 34 | +### Encoding and decoding text |
| 35 | + |
| 36 | +The `utf8` codec converts a `String` into a `Uint8Array` containing the bytes of |
| 37 | +the string's UTF-8 representation and back again. Just as for the binary data |
| 38 | +modules, the `encode()` function converts a `Uint8Array` into a `String` and |
| 39 | +`decode()` does the opposite. |
| 40 | + |
| 41 | +```js |
| 42 | +const { utf8 } = require('@escodb/buffer-codec') |
| 43 | + |
| 44 | +let bytes = utf8.decode('¿Qué vas a hacer mañana?') |
| 45 | +// -> Uint8Array(27) [0xc2, 0xbf, 0x51, 0x75, 0xc3, 0xa9, 0x20, 0x76, ...] |
| 46 | + |
| 47 | +utf8.encode(bytes) |
| 48 | +// -> '¿Qué vas a hacer mañana?' |
| 49 | +``` |
| 50 | + |
| 51 | + |
| 52 | +### Encoding binary data |
| 53 | + |
| 54 | +The `hex` (also aliased as `base16`), `base32` and `base64` modules provide an |
| 55 | +`encode()` function that takes a `Uint8Array` of arbitrary length and content |
| 56 | +and returns a `String` representing the input. |
| 57 | + |
| 58 | +```js |
| 59 | +const { hex, base32, base64 } = require('@escodb/buffer-codec') |
| 60 | + |
| 61 | +let bytes = new Uint8Array([ |
| 62 | + 0xa6, 0x2d, 0xd5, 0xb1, 0x5e, 0x34, 0x49, 0xac, 0x26, 0xfa, 0x3d, 0x7c, 0x1c |
| 63 | +]) |
| 64 | + |
| 65 | +hex.encode(bytes) |
| 66 | +// -> 'a62dd5b15e3449ac26fa3d7c1c' |
| 67 | + |
| 68 | +base32.encode(bytes) |
| 69 | +// -> 'UYW5LMK6GRE2YJX2HV6BY===' |
| 70 | + |
| 71 | +base64.encode(bytes) |
| 72 | +// -> 'pi3VsV40Sawm+j18HA==' |
| 73 | +``` |
| 74 | + |
| 75 | +The `base32` and `base64` modules add `=` padding characters where necessary to |
| 76 | +make the output length an integer multiple of the block size. The `hex` module |
| 77 | +returns lowercase output for compatibility with the Node.js `Buffer.toString()` |
| 78 | +method, but the `base32` module returns uppercase. The `base64` module uses the |
| 79 | +`+` and `/` characters, not the URL/filename-safe `-` and `_` characters for the |
| 80 | +last two values in the output alphabet. |
| 81 | + |
| 82 | + |
| 83 | +### Decoding binary data |
| 84 | + |
| 85 | +The `hex`, `base32` and `base64` modules provide a `decode()` function that |
| 86 | +takes a `String` and returns a `Uint8Array` containing the result of decoding |
| 87 | +the string. |
| 88 | + |
| 89 | +```js |
| 90 | +const { hex, base32, base64 } = require('@escodb/buffer-codec') |
| 91 | + |
| 92 | +hex.decode('a62dd5b15e3449ac26fa3d7c1c') |
| 93 | +// -> Uint8Array(13) [166, 45, 213, 177, 94, 52, 73, 172, 38, 250, 61, 124, 28] |
| 94 | + |
| 95 | +base32.decode('UYW5LMK6GRE2YJX2HV6BY===') |
| 96 | +// -> Uint8Array(13) [166, 45, 213, 177, 94, 52, 73, 172, 38, 250, 61, 124, 28] |
| 97 | + |
| 98 | +base64.decode('pi3VsV40Sawm+j18HA==') |
| 99 | +// -> Uint8Array(13) [166, 45, 213, 177, 94, 52, 73, 172, 38, 250, 61, 124, 28] |
| 100 | +``` |
| 101 | + |
| 102 | +The `hex` and `base32` modules accept alphabetic characters in either upper or |
| 103 | +lower case. The `base64` module _only_ accepts `+` and `/` as the final |
| 104 | +characters in the input space; it does not support the URL/filename-safe |
| 105 | +character set. |
| 106 | + |
| 107 | +All modules will accept inputs without `=` padding characters at the end, and |
| 108 | +only consider the input before the padding when determining the length of the |
| 109 | +output. |
| 110 | + |
| 111 | +For reasons of performance and constant-time behaviour, these functions do not |
| 112 | +check that the input only contains valid characters. Characters outside the |
| 113 | +accepted alphabet are interpreted as a block of zero bits of the appropriate |
| 114 | +size (4 bits for `hex`, 5 bits for `base32`, 6 bits for `base64`). If your |
| 115 | +application needs to reject invalid characters, you must implement this check |
| 116 | +yourself. |
| 117 | + |
| 118 | + |
| 119 | +### Converting between arbitrary bit sizes |
| 120 | + |
| 121 | +The `transcodeBE()` and `transcodeLE()` functions treat an array as a sequence |
| 122 | +of bits and let you slice and combine those bits into differently sized chunks. |
| 123 | +They both perform the same task, except that `transcodeBE()` uses big-endian |
| 124 | +bit/byte ordering and `transcodeLE()` is little-endian. Their general form is: |
| 125 | + |
| 126 | +```js |
| 127 | +transcode{BE,LE}(inBits, outBits, source, target, targetStart, sourceStart, sourceEnd) |
| 128 | +``` |
| 129 | + |
| 130 | +- `inBits`: the number of bits in each input value in the `source` array |
| 131 | +- `outBits`: the number of bits in each output value in the `target` array |
| 132 | +- `source`: an `Array` or typed array that can fit values of at least `inBits` |
| 133 | + in size; values will be read from this array |
| 134 | +- `target`: an `Array` or typed array that can fit values of at least `outBits` |
| 135 | + in size; values will be written to this array |
| 136 | +- `targetStart` (default `0`): the offset at which to start writing in `target` |
| 137 | +- `sourceStart` (default `0`): the offset at which to start reading in `source` |
| 138 | +- `sourceEnd` (default: `source.length`): the offset at which to stop reading in |
| 139 | + `source` |
| 140 | + |
| 141 | +For example, say we have a `Uint8Array` containing the single byte `0xb6`. |
| 142 | +Calling `transcodeBE(8, 1, source, target)` will read each value in `source` as |
| 143 | +an 8-bit value, and write this stream of bits as a sequence of 1-bit values into |
| 144 | +`target`. In other words, it places the individual bits of `0xb6` into `target`. |
| 145 | + |
| 146 | +```js |
| 147 | +const { transcodeBE } = require('@escodb/buffer-codec') |
| 148 | + |
| 149 | +let source = new Uint8Array([0xb6]) |
| 150 | +let target = new Uint8Array(8) |
| 151 | + |
| 152 | +transcodeBE(8, 1, source, target) |
| 153 | + |
| 154 | +// -> target = Uint8Array(8) [1, 0, 1, 1, 0, 1, 1, 0] |
| 155 | +``` |
| 156 | + |
| 157 | +Calling `transcodeBE(8, 2, source, target)` means we write 2-bit values into |
| 158 | +target. Looking at the bits listed above, this means writing `10` (2), `11` (3), |
| 159 | +`01` (1), `10` (2). |
| 160 | + |
| 161 | +```js |
| 162 | +target = new Uint8Array(4) |
| 163 | +transcodeBE(8, 2, source, target) |
| 164 | + |
| 165 | +// -> target = Uint8Array(4) [2, 3, 1, 2] |
| 166 | +``` |
| 167 | + |
| 168 | +Similarly, using an output size of 4 bits writes `1011` (11), `0110` (6). |
| 169 | + |
| 170 | +```js |
| 171 | +target = new Uint8Array(2) |
| 172 | +transcodeBE(8, 4, bytes) |
| 173 | + |
| 174 | +// -> target = Uint8Array(2) [11, 6] |
| 175 | +``` |
| 176 | + |
| 177 | +If the total number of input bits available is not an integer multiple of the |
| 178 | +output bit size, `transcodeBE()` behaves as though the final chunk of bits is |
| 179 | +padded on the right with `0` bits. For example, with an output bit size of 3, |
| 180 | +the bits of `0xb6` split into `101`, `101`, `10`. Since `10` is less than 3 bits |
| 181 | +long, it is padded with `0` at the end to make it fill 3 bits, so it becomes |
| 182 | +`100` (4). |
| 183 | + |
| 184 | +```js |
| 185 | +target = new Uint8Array(3) |
| 186 | +transcodeBE(8, 3, bytes) |
| 187 | + |
| 188 | +// -> target = Uint8Array(2) [5, 5, 4] |
| 189 | +``` |
| 190 | + |
| 191 | +In general, the input and output bit sizes can take any value from `1` to `32`, |
| 192 | +as long as they don't exceed the bit size of the relevant array. For example, if |
| 193 | +`target` is a `Uint8Array` then it cannot hold values larger than 8 bits, so any |
| 194 | +values larger than that will be subject to truncation or wrapping. |
| 195 | + |
| 196 | +As well as slicing numbers into smaller chunks, we can also combine them into |
| 197 | +larger values. For example, we can group 8-bit bytes into 16-bit or 32-bit |
| 198 | +values, as long as the `target` array is the appropriate type. |
| 199 | + |
| 200 | +```js |
| 201 | +const { transcodeBE } = require('@escodb/buffer-codec') |
| 202 | + |
| 203 | +let bytes = new Uint8Array([ |
| 204 | + 0xa6, 0x2d, 0xd5, 0xb1, 0x5e, 0x34, 0x49, 0xac, 0x26, 0xfa, 0x3d, 0x7c |
| 205 | +]) |
| 206 | + |
| 207 | +let out16 = new Uint16Array(6) |
| 208 | +transcodeBE(8, 16, bytes, out16) |
| 209 | +// out16 = Uint16Array([0xa62d, 0xd5b1, 0x5e34, 0x49ac, 0x26fa, 0x3d7c]) |
| 210 | + |
| 211 | +let out32 = new Uint32(4) |
| 212 | +transcodeBE(8, 32, bytes, out32) |
| 213 | +// out32 = Uint32Array([0xa62dd5b1, 0x5e3449ac, 0x26fa3d7c]) |
| 214 | +``` |
| 215 | + |
| 216 | +Using `transcodeLE()` does the same thing as `transcodeBE()`, except that it |
| 217 | +uses little-endian bit/byte order. |
| 218 | + |
| 219 | +```js |
| 220 | +const { transcodeLE } = require('@escodb/buffer-codec') |
| 221 | + |
| 222 | +let bytes = new Uint8Array([ |
| 223 | + 0xa6, 0x2d, 0xd5, 0xb1, 0x5e, 0x34, 0x49, 0xac, 0x26, 0xfa, 0x3d, 0x7c |
| 224 | +]) |
| 225 | + |
| 226 | +let out16 = new Uint16Array(6) |
| 227 | +transcodeLE(8, 16, bytes, out16) |
| 228 | +// out16 = Uint16Array([0x2da6, 0xb1d5, 0x345e, 0xac49, 0xfa26, 0x7c3d]) |
| 229 | + |
| 230 | +let out32 = new Uint32(4) |
| 231 | +transcodeLE(8, 32, bytes, out32) |
| 232 | +// out32 = Uint32Array([0xb1d52da6, 0xac49345e, 0x7c3dfa26]) |
| 233 | +``` |
| 234 | + |
| 235 | +Finally, there are some optional arguments that control where to begin writing |
| 236 | +in the `target` array, and which region in the `source` array to read from. For |
| 237 | +example, setting `targetStart` to `3` produces: |
| 238 | + |
| 239 | +```js |
| 240 | +let out16 = new Uint16Array(6) |
| 241 | +transcodeBE(8, 16, bytes, out16, 3) |
| 242 | +// out16 = Uint16Array([0, 0, 0, 0xa62d, 0xd5b1, 0x5e34]) |
| 243 | +``` |
| 244 | + |
| 245 | +Setting `sourceStart` to `7` and `sourceEnd` to `9` means we read just the |
| 246 | +values `0xac`, `0x26` from the `source` array: |
| 247 | + |
| 248 | +```js |
| 249 | +let out16 = new Uint16Array(6) |
| 250 | +transcodeBE(8, 16, bytes, out16, 3, 7, 9) |
| 251 | +// out16 = Uint16Array([0, 0, 0, 0xac26, 0, 0]) |
| 252 | +``` |
0 commit comments