# @taichunmin/buffer
> A cross platform alternative of Node buffer base on UInt8Array.
## Install
### Package manager
```shell
# use npm
npm install @taichunmin/buffer
# use yarn
yarn add @taichunmin/buffer
```
And then import the library using `import` or `require`:
```js
// import
import { Buffer } from '@taichunmin/buffer'
// require
const { Buffer } = require('@taichunmin/buffer')
```
### CDN
Powered by jsDelivr CDN:
```html
```
## Quick start
`Buffer` extends `Uint8Array`, so every `Uint8Array` method is available. On top of that it implements the Node.js `Buffer` API — `Buffer.from()`, `buf.toString(encoding)`, `buf.readUInt16BE()` and friends — plus the `pack` / `unpack` family described below.
```js
import { Buffer } from '@taichunmin/buffer'
const buf1 = Buffer.from('01020304', 'hex')
console.log(buf1.toString('hex')) // '01020304'
console.log(buf1.readUInt16BE(0)) // 258
const buf2 = Buffer.from('hello', 'utf8')
console.log(buf2.toString('base64url')) // 'aGVsbG8'
console.log(Buffer.concat([buf1, buf2]).length) // 9
console.log(Buffer.isBuffer(buf1)) // true, and false for a plain Uint8Array
```
## API
- [Buffer](classes/Buffer.md): Complete API reference for the Buffer class, a cross platform alternative of Node buffer based on Uint8Array. Covers construction and allocation (from, of, alloc, allocUnsafe, allocUnsafeSlow, concat, copyBytesFrom, fromView); encoding conversion between hex, base64, base64url, utf8, latin1, ascii, binary and ucs2/utf16le via toString, write and the fromHexString/toHexString style helpers; typed reads and writes for every integer width in both byte orders (readUInt8, readUInt16LE, readInt32BE, readUIntBE, readBigUInt64LE, writeInt16LE, writeBigInt64BE and friends), IEEE 754 float16/float32/float64 (readFloatLE, readDoubleBE, readFloat16LE), and single bits (readBitLSB, writeBitMSB); DataView style aliases (getUint16, setFloat32, getBigInt64); Python struct compatible pack, unpack, iterUnpack, packCalcSize and packParseFormat; bitwise and, or, xor, not with their toAnded/toOred/toXored/toNoted copies; byte order swaps (swap16, swap32, swap64); plus slicing, copying, comparison and search (subarray, slice, copy, chunk, equals, compare, indexOf, lastIndexOf, includes) and the isBuffer/isEncoding/byteLength static helpers.
## `pack` / `unpack` format strings
`Buffer.pack()`, `Buffer.unpack()`, `Buffer.iterUnpack()` and `Buffer.packCalcSize()` borrow their format strings from [Python's `struct` module](https://docs.python.org/3/library/struct.html). The format syntax is the same, but **the runtime behaviour is not** — this library validates far less than Python does, and several cases Python rejects loudly are silently accepted here.
If you are porting `struct` code or generating calls from `struct` knowledge, read [Differences from Python's `struct`](#differences-from-pythons-struct) first. The four that bite hardest:
1. **Integers are never range-checked.** `Buffer.pack('>B', 300)` returns `2c`, not an error.
2. **Extra values are silently ignored.** `Buffer.pack('>b', 1, 2, 3)` returns `01`.
3. **`c` / `s` / `p` unpack to views that share memory with the source buffer**, not to copies.
4. **`unpack` accepts an over-long buffer** and ignores the trailing bytes, where Python demands an exact length.
### API surface
Every operation has a static form and, except for `packCalcSize`, an instance form:
| Static | Instance | Python equivalent |
| ------ | -------- | ----------------- |
| `Buffer.pack(format, ...vals)` | — | `struct.pack` |
| `Buffer.pack(buf, format, ...vals)` | `buf.pack(format, ...vals)` | `struct.pack_into` |
| `Buffer.unpack(buf, format)` | `buf.unpack(format)` | `struct.unpack` |
| `Buffer.iterUnpack(buf, format)` | `buf.iterUnpack(format)` | `struct.iter_unpack` |
| `Buffer.packCalcSize(formatOrItems)` | — | `struct.calcsize` |
| `Buffer.packParseFormat(format)` | — | (no equivalent) |
`buf` must be a real `Buffer`. A plain `Uint8Array` throws `TypeError: Invalid type of buf` — wrap it first with `Buffer.fromView(u8)`, which shares the same memory, or `Buffer.from(u8)`, which copies. Use `fromView` when packing into a `Uint8Array` you need to see the writes in.
`unpack` always returns an array, even for a single value: `Buffer.unpack(buf, '>b')` is `[1]`, not `1`.
`Buffer.packParseFormat(format)` returns `{ littleEndian, items }`, where `items` is an array of `[repeat, type]` pairs. Passing that `items` array back to `Buffer.packCalcSize()` skips re-parsing, which is worth doing in a hot loop:
```js
const { items } = Buffer.packParseFormat('h'` throws.
| Prefix | Byte order |
| ------ | ---------- |
| `<` | little-endian |
| `>` | big-endian |
| `!` | big-endian (network order) |
| `@` | native (whatever the host is) |
| `=` | native |
| (omitted) | same as `@` |
Native order follows the host, so on the x86-64 and arm64 machines almost everything runs on it means little-endian. **Prefer `<`, `>` or `!` for any bytes that leave the process** — a file, a socket, a device protocol — so the encoding does not change with the machine.
### Format characters
| Format | C type | Value when packing | Value when unpacking | Size |
| ------ | ------ | ------------------ | -------------------- | ---- |
| `x` | pad byte | takes no value | yields no value | 1 |
| `c` | `char` | anything `Buffer.from()` accepts; first byte used | `Buffer` of length 1 | 1 |
| `b` | `signed char` | `number` | `number` | 1 |
| `B` | `unsigned char` | `number` | `number` | 1 |
| `?` | `_Bool` | any value, by JS truthiness | `boolean` | 1 |
| `h` | `short` | `number` | `number` | 2 |
| `H` | `unsigned short` | `number` | `number` | 2 |
| `i` | `int` | `number` | `number` | 4 |
| `I` | `unsigned int` | `number` | `number` | 4 |
| `l` | `long` | `number` | `number` | 4 |
| `L` | `unsigned long` | `number` | `number` | 4 |
| `q` | `long long` | `bigint` (or `number`) | `bigint` | 8 |
| `Q` | `unsigned long long` | `bigint` (or `number`) | `bigint` | 8 |
| `e` | half precision float | `number` | `number` | 2 |
| `f` | `float` | `number` | `number` | 4 |
| `d` | `double` | `number` | `number` | 8 |
| `s` | `char[]` | `Buffer`/string/array | `Buffer` (a view) | count |
| `p` | Pascal `char[]` | `Buffer`/string/array | `Buffer` (a view) | count |
`i`/`I` and `l`/`L` are identical here — both are 4 bytes, since there is no native-size mode.
`n`, `N` and `P` are **not supported** and throw `TypeError: Invalid format: …`.
### Packing values: coercion, not validation
This is the largest behavioural gap from Python. Producing a value the format cannot hold is not an error; the value is coerced and then truncated.
**Integer formats (`b B h H i I l L`)** run the value through lodash `toSafeInteger` and then write it with a `DataView` setter, so it wraps modulo the field width. Nothing throws:
| Call | Result | Python |
| ---- | ------ | ------ |
| `Buffer.pack('>B', 300)` | `2c` (300 mod 256) | `struct.error` |
| `Buffer.pack('>B', -1)` | `ff` | `struct.error` |
| `Buffer.pack('>h', 1.9)` | `0001` (truncated toward zero) | `struct.error` |
| `Buffer.pack('>h', '258')` | `0102` | `struct.error` |
| `Buffer.pack('>h', null)` | `0000` | `struct.error` |
| `Buffer.pack('>h', NaN)` | `0000` | `struct.error` |
| `Buffer.pack('>h', 'abc')` | `0000` | `struct.error` |
| `Buffer.pack('>i', 1e20)` | `ffffffff` (clamped to `Number.MAX_SAFE_INTEGER`, then wrapped) | `struct.error` |
**Validate ranges yourself** before packing anything that comes from user input or the network.
**`q` / `Q`** always unpack to `bigint`, never `number`. When packing, the value goes through `BigInt()`, which is stricter than the integer path — but the result still wraps:
```js
Buffer.pack('>Q', 9007199254740993n) // '0020000000000001'
Buffer.pack('>Q', 9007199254740993) // '0020000000000000' — precision lost before BigInt() sees it
Buffer.pack('>Q', 2n ** 64n + 5n) // '0000000000000005' — wraps mod 2^64
Buffer.pack('>Q', '12') // '000000000000000c' — numeric strings are accepted
Buffer.pack('>Q', 1.5) // RangeError (not an integer)
Buffer.pack('>Q', 'abc') // SyntaxError
Buffer.pack('>Q', null) // TypeError
```
Pass a `bigint` literal for anything that might exceed 2^53.
**`?`** uses JavaScript truthiness, which differs from Python's for several common values:
```js
Buffer.pack('>?', []) // '01' — truthy in JS; Python packs an empty list as 0
Buffer.pack('>?', {}) // '01'
Buffer.pack('>?', 'false') // '01' — any non-empty string is truthy
Buffer.pack('>?', '') // '00'
Buffer.pack('>?', 0) // '00'
```
Unpacking yields `true` for any non-zero byte.
**`c`, `s` and `p`** pass the value to `Buffer.from()`, so a `Buffer`, string (UTF-8) or byte array all work, but a **number throws** `TypeError: Invalid type of value: number` — `Buffer.pack('>c', 65)` is an error; use `Buffer.pack('>c', 'A')` or `Buffer.pack('>B', 65)`. `c` keeps only the first byte of whatever it is given (`Buffer.pack('>c', 'xy')` is `78`), and an empty value packs as `00`.
**`e`, `f` and `d`** always use IEEE 754 binary16 / binary32 / binary64 regardless of the host, rounding half to even. Values too large for the format saturate to `±Infinity` rather than raising, which is where `e` differs most from Python — `Buffer.pack('>e', 1e5)` is `7c00` (`Infinity`), while Python's `struct.pack('>e', 1e5)` raises `OverflowError`. Anything above `65519.996…` rounds up to `Infinity`; the largest finite binary16 is `65504`. Small magnitudes degrade into subnormals down to `2^-24` and then to `±0`.
### Argument count
Too few values throws `TypeError: Not enough vals`. **Too many are silently ignored** — Python raises `struct.error` in both directions:
```js
Buffer.pack('>bb', 1) // TypeError: Not enough vals
Buffer.pack('>b', 1, 2, 3) // '01' — no error, 2 and 3 are dropped
```
### Repeat counts
A format character may be preceded by a decimal repeat count. For most characters the count is a repetition: `4h` means exactly `hhhh`, and consumes four values. A count of `0` is legal and consumes nothing (`Buffer.pack('>0hb', 7)` is `07`).
`s`, `p` and `x` treat the count as a **length**, not a repetition:
- `10s` is one 10-byte `Buffer` taking one value; `10c` is ten separate one-byte `Buffer`s taking ten values. When packing, the value is truncated or NUL-padded to exactly the count. Without a count, `s` means `1s`; `0s` is a single empty `Buffer`.
- `5p` is a Pascal string in exactly 5 bytes: a length byte followed by up to `count - 1` bytes of content, NUL-padded to the count. Content longer than `count - 1` is truncated, and the stored length byte reflects the truncated length (`Buffer.pack('>5p', Buffer.from('abcdefgh'))` is `0461626364`). When unpacking, a length byte larger than `count - 1` is clamped.
- `2x` is two pad bytes and takes no value. When packing it writes that many NUL bytes (`0x00`); when unpacking it skips them and yields nothing. `2x` and `xx` are equivalent, in both directions.
### Format string validation
`packParseFormat` accepts only `/^([@=<>!]?)((?:\d*[xcbB?hHiIlLqQefdsp])+)$/`. Anything else throws `TypeError: Invalid format: …`. In particular, and unlike Python:
- **Whitespace is not allowed.** `'> b h'` throws; Python ignores whitespace between format characters.
- **The empty string is not a valid format.** `''` throws; Python's `calcsize('')` is `0`.
- A prefix character anywhere but position 0 throws.
### Unpacked `c` / `s` / `p` are views, not copies
`unpack` and `iterUnpack` return `subarray()` views into the source buffer for `c`, `s` and `p`. Mutating a result mutates the source, and holding one alive retains the whole underlying `ArrayBuffer`. Python's `struct` always returns independent `bytes`.
```js
const src = Buffer.from('0161626300', 'hex')
const [str] = Buffer.unpack(src, '>x4s')
str[0] = 0x5a
console.log(src.toString('hex')) // '015a626300' — the source changed
```
Call `.slice()` (or `Buffer.from(view)`) when you need to detach the result from the source.
### `pack` has two overloads
`pack` is both `struct.pack` and `struct.pack_into`, chosen by whether the first argument is a `Buffer` or a format string:
```js
// struct.pack — allocates a new Buffer of exactly packCalcSize(format) bytes
const buf1 = Buffer.pack('>h', 0x0102)
console.log(buf1.toString('hex')) // '0102'
// struct.pack_into — writes into an existing Buffer, starting at index 0, and returns it
const buf2 = Buffer.alloc(3)
Buffer.pack(buf2, '>h', 0x0102)
console.log(buf2.toString('hex')) // '010200'
// the instance form of the same thing, returning `this`
const buf3 = Buffer.alloc(3)
buf3.pack('>h', 0x0102)
console.log(buf3.toString('hex')) // '010200'
// to write at an offset, pack into a subarray — it shares memory with the parent
const buf4 = Buffer.alloc(3)
buf4.subarray(1).pack('>h', 0x0102)
console.log(buf4.toString('hex')) // '000102'
```
There is no `offset` parameter; `subarray()` is how you write at an offset. The `pack_into` forms only write the bytes the format covers, and they throw a `RangeError` if the target is shorter than `packCalcSize(format)`. A longer target is fine; the trailing bytes are left as they were (`Buffer.alloc(4).fill(0xEE)` packed with `'>h'` becomes `0102eeee`).
### `iterUnpack`
Reads fixed-size records until fewer than `packCalcSize(format)` bytes remain; a trailing partial record is silently dropped. Python's `iter_unpack` instead raises unless the length is an exact multiple.
```js
[...Buffer.iterUnpack(Buffer.from('0102030405', 'hex'), '>2b')] // [[1, 2], [3, 4]] — the 5th byte is dropped
[...Buffer.iterUnpack(Buffer.from('01', 'hex'), '>2b')] // RangeError — not even one record
```
A zero-size format such as `'0b'` or `'0x'` throws `RangeError: Cannot iterate over 0-length format: …`, since it could never consume the buffer. Python raises `ValueError: cannot iterate over 0-length struct` for the same reason.
### Errors
Python raises `struct.error` for everything. This library uses ordinary JS error types, so catch accordingly:
| Error | Cause |
| ----- | ----- |
| `TypeError: Invalid format: …` | format string does not match the grammar |
| `TypeError: Invalid type of format` | format is not a string |
| `TypeError: Invalid type of buf` | `buf` is not a `Buffer` (a plain `Uint8Array` lands here) |
| `TypeError: Not enough vals` | fewer values than the format requires |
| `TypeError: Invalid type of value: number` | a number passed to `c`, `s` or `p` |
| `RangeError: buf.length = N, lenRequired = M` | buffer shorter than `packCalcSize(format)` |
| `RangeError: Cannot iterate over 0-length format: …` | `iterUnpack` with a format of size 0 |
| `RangeError` / `SyntaxError` from `BigInt()` | bad value for `q` or `Q` |
### Worked examples
```js
console.log(Buffer.packCalcSize('bhl', 1, 2, 3).toString('hex')) // '01000200000003'
console.log(Buffer.unpack(Buffer.from('01fe01fe', 'hex'), '!BBbb')) // [1, 254, 1, -2]
console.log([...Buffer.iterUnpack(Buffer.from('01fe01fe', 'hex'), '!BB')]) // [[1, 254], [1, 254]]
console.log(Buffer.pack('!10s', Buffer.from('abc')).toString('hex')) // '61626300000000000000'
console.log(Buffer.pack('>5p', Buffer.from('ab')).toString('hex')) // '0261620000'
console.log(Buffer.unpack(Buffer.from('01020304', 'hex'), '>b2xb')) // [1, 4]
```
### Differences from Python's `struct`
| # | Behaviour | This library | Python |
| - | --------- | ------------ | ------ |
| 1 | Out-of-range integers | coerced and wrapped silently | `struct.error` |
| 2 | Non-numeric values for integer formats | coerced (`'abc'` → `0`) | `struct.error` |
| 3 | Extra values passed to `pack` | ignored | `struct.error` |
| 4 | `unpack` buffer length | must be **at least** `packCalcSize`; extra bytes ignored | must be exactly `calcsize` |
| 5 | `iter_unpack` trailing bytes | partial record dropped | `struct.error` |
| 6 | `c` / `s` / `p` unpack results | views sharing the source's memory | independent `bytes` copies |
| 7 | Native size and alignment | not supported; no padding is ever inserted for any prefix | `@` inserts C alignment padding |
| 8 | `p` count above 255 | clamped to 255 (`packCalcSize('300p')` is `255`) | `300` |
| 9 | Whitespace in the format string | `TypeError` | ignored |
| 10 | Empty format string | `TypeError` | valid, `calcsize('')` is `0` |
| 11 | `n`, `N`, `P` formats | `TypeError` | supported in native mode |
| 12 | `?` truthiness | JS rules (`[]` and `{}` are true) | Python rules (`[]` and `{}` are false) |
| 13 | Float overflow | saturates to `±Infinity` | `OverflowError` for `e`, `f` |
| 14 | Error type | `TypeError` / `RangeError` / `SyntaxError` | `struct.error` |
| 15 | `q` / `Q` results | always `bigint` | `int` |
Rows 7 and 8 are deliberate design choices. The rest follow from this library validating less than Python does, so **validate ranges and argument counts yourself** at the boundary where untrusted data enters.