├── package.json ├── LICENSE ├── struct.mjs ├── test └── test.mjs └── README.md /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@aksel/structjs", 3 | "version": "2.0.0", 4 | "description": "Python struct for javascript", 5 | "main": "struct.mjs", 6 | "type": "module", 7 | "directories": { 8 | "test": "test" 9 | }, 10 | "scripts": { 11 | "test": "mocha" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/TheRealAksel/structjs.git" 16 | }, 17 | "keywords": [ 18 | "python", 19 | "javascript", 20 | "struct", 21 | "structure", 22 | "pack", 23 | "unpack", 24 | "pack_into", 25 | "unpack_from", 26 | "calcsize", 27 | "size", 28 | "format" 29 | ], 30 | "author": "Aksel Jensen", 31 | "license": "ISC", 32 | "bugs": { 33 | "url": "https://github.com/TheRealAksel/structjs/issues" 34 | }, 35 | "homepage": "https://github.com/TheRealAksel/structjs#readme", 36 | "eslintConfig": { 37 | "ecmaFeatures": { 38 | "modules": true 39 | } 40 | }, 41 | "devDependencies": { 42 | "mocha": "^7.1.2", 43 | "should": "^13.2.3" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Aksel Jensen (TheRealAksel at github) 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 | -------------------------------------------------------------------------------- /struct.mjs: -------------------------------------------------------------------------------- 1 | /*eslint-env es6*/ 2 | const rechk = /^([<>])?(([1-9]\d*)?([xcbB?hHiIfdsp]))*$/ 3 | const refmt = /([1-9]\d*)?([xcbB?hHiIfdsp])/g 4 | const str = (v,o,c) => String.fromCharCode( 5 | ...new Uint8Array(v.buffer, v.byteOffset + o, c)) 6 | const rts = (v,o,c,s) => new Uint8Array(v.buffer, v.byteOffset + o, c) 7 | .set(s.split('').map(str => str.charCodeAt(0))) 8 | const pst = (v,o,c) => str(v, o + 1, Math.min(v.getUint8(o), c - 1)) 9 | const tsp = (v,o,c,s) => { v.setUint8(o, s.length); rts(v, o + 1, c - 1, s) } 10 | const lut = le => ({ 11 | x: c=>[1,c,0], 12 | c: c=>[c,1,o=>({u:v=>str(v, o, 1) , p:(v,c)=>rts(v, o, 1, c) })], 13 | '?': c=>[c,1,o=>({u:v=>Boolean(v.getUint8(o)),p:(v,B)=>v.setUint8(o,B)})], 14 | b: c=>[c,1,o=>({u:v=>v.getInt8( o ), p:(v,b)=>v.setInt8( o,b )})], 15 | B: c=>[c,1,o=>({u:v=>v.getUint8( o ), p:(v,B)=>v.setUint8( o,B )})], 16 | h: c=>[c,2,o=>({u:v=>v.getInt16( o,le), p:(v,h)=>v.setInt16( o,h,le)})], 17 | H: c=>[c,2,o=>({u:v=>v.getUint16( o,le), p:(v,H)=>v.setUint16( o,H,le)})], 18 | i: c=>[c,4,o=>({u:v=>v.getInt32( o,le), p:(v,i)=>v.setInt32( o,i,le)})], 19 | I: c=>[c,4,o=>({u:v=>v.getUint32( o,le), p:(v,I)=>v.setUint32( o,I,le)})], 20 | f: c=>[c,4,o=>({u:v=>v.getFloat32(o,le), p:(v,f)=>v.setFloat32(o,f,le)})], 21 | d: c=>[c,8,o=>({u:v=>v.getFloat64(o,le), p:(v,d)=>v.setFloat64(o,d,le)})], 22 | s: c=>[1,c,o=>({u:v=>str(v,o,c), p:(v,s)=>rts(v,o,c,s.slice(0,c ) )})], 23 | p: c=>[1,c,o=>({u:v=>pst(v,o,c), p:(v,s)=>tsp(v,o,c,s.slice(0,c - 1) )})] 24 | }) 25 | const errbuf = new RangeError("Structure larger than remaining buffer") 26 | const errval = new RangeError("Not enough values for structure") 27 | export default function struct(format) { 28 | let fns = [], size = 0, m = rechk.exec(format) 29 | if (!m) { throw new RangeError("Invalid format string") } 30 | const t = lut('<' === m[1]), lu = (n, c) => t[c](n ? parseInt(n, 10) : 1) 31 | while ((m = refmt.exec(format))) { ((r, s, f) => { 32 | for (let i = 0; i < r; ++i, size += s) { if (f) {fns.push(f(size))} } 33 | })(...lu(...m.slice(1)))} 34 | const unpack_from = (arrb, offs) => { 35 | if (arrb.byteLength < (offs|0) + size) { throw errbuf } 36 | let v = new DataView(arrb, offs|0) 37 | return fns.map(f => f.u(v)) 38 | } 39 | const pack_into = (arrb, offs, ...values) => { 40 | if (values.length < fns.length) { throw errval } 41 | if (arrb.byteLength < offs + size) { throw errbuf } 42 | const v = new DataView(arrb, offs) 43 | new Uint8Array(arrb, offs, size).fill(0) 44 | fns.forEach((f, i) => f.p(v, values[i])) 45 | } 46 | const pack = (...values) => { 47 | let b = new ArrayBuffer(size) 48 | pack_into(b, 0, ...values) 49 | return b 50 | } 51 | const unpack = arrb => unpack_from(arrb, 0) 52 | function* iter_unpack(arrb) { 53 | for (let offs = 0; offs + size <= arrb.byteLength; offs += size) { 54 | yield unpack_from(arrb, offs); 55 | } 56 | } 57 | return Object.freeze({ 58 | unpack, pack, unpack_from, pack_into, iter_unpack, format, size}) 59 | } 60 | /* 61 | const pack = (format, ...values) => struct(format).pack(...values) 62 | const unpack = (format, buffer) => struct(format).unpack(buffer) 63 | const pack_into = (format, arrb, offs, ...values) => 64 | struct(format).pack_into(arrb, offs, ...values) 65 | const unpack_from = (format, arrb, offset) => 66 | struct(format).unpack_from(arrb, offset) 67 | const iter_unpack = (format, arrb) => struct(format).iter_unpack(arrb) 68 | const calcsize = format => struct(format).size 69 | module.exports = { 70 | struct, pack, unpack, pack_into, unpack_from, iter_unpack, calcsize } 71 | */ 72 | -------------------------------------------------------------------------------- /test/test.mjs: -------------------------------------------------------------------------------- 1 | /*eslint-env es6, mocha*/ 2 | import should from "should"; 3 | should(); 4 | 5 | import struct from "../struct.mjs"; 6 | describe('struct', () => { 7 | let ab = new ArrayBuffer(100) 8 | let u8a = new Uint8Array(ab) 9 | it('packs and unpacks using pack and unpack functions', () => { 10 | let s = struct('b'), b = s.pack(-1) 11 | new Uint8Array(b).should.deepEqual(new Uint8Array([0xFF])) 12 | s.unpack(b).should.deepEqual([-1]) 13 | }) 14 | it('iterates', () => { 15 | Array.from(struct('b').iter_unpack(struct('bb').pack(1, 2))).should.deepEqual([[1], [2]]) 16 | }) 17 | it('packs and unpacks signed bytes', () => { 18 | struct('b').pack_into(ab, 5, -1) 19 | u8a.slice(5,6).should.deepEqual(new Uint8Array([0xFF])) 20 | struct('b').unpack_from(ab, 5).should.deepEqual([-1]) 21 | }) 22 | it('packs and unpacks unsigned bytes', () => { 23 | struct('B').pack_into(ab, 5, -1) 24 | u8a.slice(5,6).should.deepEqual(new Uint8Array([0xFF])) 25 | struct('B').unpack_from(ab, 5).should.deepEqual([0xFF]) 26 | }) 27 | it('packs and unpacks signed words', () => { 28 | struct('h').pack_into(ab, 5, -1) 29 | u8a.slice(5,7).should.deepEqual(new Uint8Array([0xFF, 0xFF])) 30 | struct('h').unpack_from(ab, 5).should.deepEqual([-1]) 31 | }) 32 | it('packs and unpacks unsigned words', () => { 33 | struct('H').pack_into(ab, 0, -1) 34 | u8a.slice(0,2).should.deepEqual(new Uint8Array([0xFF, 0xFF])) 35 | struct('H').unpack_from(ab, 5).should.deepEqual([0xFFFF]) 36 | }) 37 | it('packs and unpacks signed longs', () => { 38 | struct('i').pack_into(ab, 0, -1) 39 | u8a.slice(0,4).should.deepEqual(new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF])) 40 | struct('i').unpack_from(ab).should.deepEqual([-1]) 41 | }) 42 | it('packs and unpacks unsigned longs', () => { 43 | struct('I').pack_into(ab, 0, -1) 44 | u8a.slice(0,4).should.deepEqual(new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF])) 45 | struct('I').unpack_from(ab).should.deepEqual([0xFFFFFFFF]) 46 | }) 47 | it('packs and unpacks strings', () => { 48 | struct('10s').pack_into(ab, 0, "foobar") 49 | struct('10s').unpack_from(ab).should.deepEqual(["foobar\x00\x00\x00\x00"]) 50 | }) 51 | it('packs and unpacks pascal strings', () => { 52 | struct('3x10p3x').pack_into(ab, 0, "foobar") 53 | u8a.slice(0,3).should.deepEqual(new Uint8Array([0, 0, 0])) // pad 54 | u8a[3].should.equal(6) // String length 55 | u8a.slice(10,13).should.deepEqual(new Uint8Array([0, 0, 0])) // unused 56 | u8a.slice(13,16).should.deepEqual(new Uint8Array([0, 0, 0])) // pad 57 | struct('3x10p3x').unpack_from(ab).should.deepEqual(["foobar"]) 58 | }) 59 | it('packs and unpacks floats', () => { 60 | struct('f').pack_into(ab, 0, 1.2345) 61 | struct('f').unpack_from(ab)[0].should.be.approximately(1.2345, 0.00001) 62 | }) 63 | it('packs and unpacks doubles', () => { 64 | struct('d').pack_into(ab, 0, 1.23456789) 65 | struct('d').unpack_from(ab)[0].should.be.approximately(1.23456789, 0.00000000000001) 66 | }) 67 | it('skips pad bytes', () => { 68 | struct('bbxxxbb').pack_into(ab, 0, 1, 2, 3, 4) 69 | u8a.slice(0,7).should.deepEqual(new Uint8Array([1, 2, 0, 0, 0, 3, 4])) 70 | struct('bbxxxbb').unpack_from(ab).should.deepEqual([1, 2, 3, 4]) 71 | }) 72 | it('takes repeat counts', () => { 73 | struct('2b3x2b').pack_into(ab, 0, 1, 2, 3, 4) 74 | u8a.slice(0,7).should.deepEqual(new Uint8Array([1, 2, 0, 0, 0, 3, 4])) 75 | struct('2b3x2b').unpack_from(ab).should.deepEqual([1, 2, 3, 4]) 76 | }) 77 | it('packs and unpacks characters', () => { 78 | struct('3c').pack_into(ab, 0, "f", "o", "o") 79 | struct('3c').unpack_from(ab).should.deepEqual(["f", "o", "o"]) 80 | }) 81 | it('packs and unpacks booleans', () => { 82 | struct('??').pack_into(ab, 0, true, false) 83 | u8a.slice(0,2).should.deepEqual(new Uint8Array([1, 0])) 84 | struct('??').unpack_from(ab).should.deepEqual([true, false]) 85 | }) 86 | it('can do little-endian words and longs', () => { 87 | struct('h').unpack_from(ab, 5).should.deepEqual([0x0100]) 91 | }) 92 | it('handles more complicated structures', () => { 93 | let s = struct('<2h4x3H7i2x8s') 94 | s.pack_into(ab, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, -12, "hah") 95 | s.unpack_from(ab).should.deepEqual( 96 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, -12, 97 | "hah\x00\x00\x00\x00\x00"]) 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # structjs - Python-style struct module in javascript 2 | This module performs conversions between javascript values and C structs represented as javascript [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) objects. This can be used in handling binary data stored in files or from network connections, among other sources. It uses [Format Strings](#format-strings) as compact descriptions of the layout of the C structs and the intended conversion to/from javascript values. 3 | 4 | > **Note:** Unlike Python struct, this module does not support native size and alignment (that wouldn't make much sense in a javascript). Instead, specify byte order and emit pad bytes explicitly. 5 | 6 | Several methods of [Struct](#object) take a buffer argument. This refers to [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) objects. 7 | 8 | > **Note:** In Python struct the buffer argument refers to an object that implements the Buffer Protocol. 9 | 10 | ## Functions 11 | The module defines the following function: 12 | 13 | 14 | **struct**(*format*) 15 | Return a new object which writes and reads binary data according to the [format string](#format-strings) *format*. 16 | 17 | > **Note:** This is not a constructor, don't use new. In Python this is a constructor. Python has functions for packing and unpacking without the intermediate step of creating an object using the struct function. However I decided against including such functions as they are redundant, and I did not want to clutter the interface unnecessarily. They are currently in the code but inside a comment. 18 | 19 | ## Objects 20 | 21 | 22 | The compiled Struct objects returned by [struct](#struct) support the following methods and attributes: 23 | 24 | 25 | **pack**(*v1, v2, ...*) 26 | Return an [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) object containing the values *v1, v2, ...* packed according to [format](#format). The arguments must match the values required by the format exactly (`result.byteLength` will equal [size](#size)). 27 | 28 | 29 | **pack_into**(*buffer, offset, v1, v2, ...*) 30 | Pack the values *v1, v2, ...* according to [format](#format) and write the packed bytes into the [ ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) *buffer* starting at position *offset*. Note that *offset* is a required argument. 31 | 32 | 33 | **unpack**(*buffer*) 34 | Unpack from the [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) *buffer* (presumably packed by `pack()`) according to [format](#format). The result is a tuple even if it contains exactly one item. The buffer’s size in bytes must match the size required by the format, as reflected by [size](#size). 35 | 36 | 37 | **unpack_from**(*buffer, offset=0*) 38 | Unpack from [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) *buffer* starting at position *offset*, according to [format](#format). The result is a tuple even if it contains exactly one item. The buffer’s size in bytes, minus *offset*, must be at least the size required by the format, as reflected by [size](#size). 39 | 40 | 41 | **iter_unpack**(*buffer*) 42 | Iteratively unpack from the buffer buffer according to [format](#format). This function returns an iterator which will read equally-sized chunks from the buffer until all its contents have been consumed. The buffer’s size in bytes must be a multiple of the size required by the [format](#format), as reflected by [size](#size) (this is not enforced, remaining bytes are ignored silently). 43 | 44 | Each iteration yields a tuple as specified by the [format string](#format-strings). 45 | 46 | 47 | **format** 48 | The [format string](#format-strings) used to construct this object. 49 | 50 | 51 | **size** 52 | The calculated size of the struct (and hence of the [ArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) object produced by the [pack()](#pack) method) corresponding to [format](#format). 53 | 54 | 55 | ## Format Strings 56 | Format strings are the mechanism used to specify the expected layout when packing and unpacking data. They are built up from [Format Characters](#format-characters), which specify the type of data being packed/unpacked. In addition, there are special characters for controlling the [Byte Order](#byte-order). 57 | 58 | > **Note:** Unlike Python struct, this module does not have special format characters for indicating native size and alignment. 59 | 60 | ### Byte Order 61 | By default, C types are represented in the standard format and byte order, and not aligned in any way (no pad bytes are inserted). 62 | 63 | > **Note:** This is different from Python struct which uses native format. 64 | 65 | The first character of the format string can be used to indicate the byte order, according to the following table: 66 | 67 | | Character | Byte order | 68 | |-----------|---------------| 69 | | < | little-endian | 70 | | > | big-endian | 71 | If the first character is not one of these, '>' is assumed. 72 | 73 | > **Differences from Python struct:** 74 | > Python struct has more options that don't make as much sense for javascript. 75 | > No '@', '=', '!'. Use '>' for '!'. 76 | > No native support. 77 | > No alignment. Use 'x' for padding. 78 | 79 | ### Format Characters 80 | Format characters have the following meaning; the conversion between C and ES values should be obvious given their types. The ‘Size’ column refers to the size of the packed value in bytes: 81 | 82 | |Format|C Type |ES Type |Size| 83 | |---|--------------|--------|---| 84 | | x |pad byte | | 1 | 85 | | c |char | String of length 1| 1 | 86 | | b |signed char | Number | 1 | 87 | | B |unsigned char | Number | 1 | 88 | | ? |_Bool | Boolean| 1 | 89 | | h |short | Number | 2 | 90 | | H |unsigned short| Number | 2 | 91 | | i |int | Number | 4 | 92 | | I |unsigned int | Number | 4 | 93 | | f |float | Number | 4 | 94 | | d |double | Number | 8 | 95 | | s |char[] | String | | 96 | | p |char[] | String | | 97 | 98 | > **Differences from Python:** 99 | > No 'l', 'L', 'q', 'Q', 'P', no integers, no floats, no doubles, only numbers. 100 | > For 'l' and 'L' use 'i' and 'I'. For 'P' use 'H' or 'I' as appropriate. 101 | > 'q' and 'Q' cannot be fully represented in javascript. Use 'i' or 'I' instead. 102 | > No 'n' or 'N'. 103 | 104 | A format character may be preceded by an integral repeat count. For example, the format string `'4h'` means exactly the same as `'hhhh'`. 105 | 106 | Whitespace characters between formats are not accepted. 107 | 108 | > **Note:** Python struct ignores whitespace characters (a count and its format must not contain whitespace though). 109 | 110 | For the 's' format character, the count is interpreted as the size of the string, not a repeat count like for the other format characters; for example, `'10s'` means a single 10-byte string, while `'10c'` means 10 characters. If a count is not given, it defaults to 1. For packing, the string is truncated or padded with null bytes as appropriate to make it fit. For unpacking, the resulting string always has exactly the specified number of bytes. 111 | 112 | > **Note**: Python struct accepts a special case; '0s' means a single, empty string (while '0c' means 0 characters). 113 | 114 | > **Note:** Python guarantees that when packing a value x using one of the integer formats ('b', 'B', 'h', 'H', 'i', 'I', 'l', 'L', 'q', 'Q'), if x is outside the valid range for that format then struct.error is raised. 115 | 116 | The 'p' format character encodes a “Pascal string”, meaning a short variable-length string stored in a *fixed number of bytes*, given by the count. The first byte stored is the length of the string, or 255, whichever is smaller. The bytes of the string follow. If the string passed in to [pack()](#pack) is too long (longer than the count minus 1), only the leading `count-1` bytes of the string are stored. If the string is shorter than `count-1`, it is padded with null bytes so that exactly count bytes in all are used. Note that for [unpack()](#unpack), the `'p'` format character consumes `count` bytes, but that the string returned can never contain more than 255 characters. 117 | 118 | For the `'?'` format character, the return value is either [true](link-to-es-true) or [false](link-to-es-false). When packing, the truth value of the argument object is used. Either 0 or 1 in the native or standard bool representation will be packed, and any non-zero value will be `true` when unpacking. 119 | 120 | ### Examples: 121 | A basic example of packing/unpacking three integers: 122 | ```javascript 123 | import struct from "struct"; 124 | let s = struct('hhi'), b = new ArrayBuffer(s.size) 125 | s.pack(1, 2, 3) // ArrayBuffer {} 126 | new Uint8Array(s.pack(1, 2, 3)) // Uint8Array { '0': 0, '1': 1, '2': 0, '3': 2, '4': 0, '5': 0, '6': 0, '7': 3 } 127 | s.unpack(new Uint8Array([0, 1, 0, 2, 0, 0, 0, 3]).buffer) // [ 1, 2, 3 ] 128 | s.size // 8 129 | s.pack_into(b, 0, 1, 2, 3) 130 | new Uint8Array(b) // Uint8Array { '0': 0, '1': 1, '2': 0, '3': 2, '4': 0, '5': 0, '6': 0, '7': 3 } 131 | ``` 132 | Unpacked fields can be named by assigning them to variables: 133 | ```javascript 134 | import struct from "struct"; 135 | let s = struct("<10sHHb") 136 | let record = s.pack("Raymond ", 4658, 264, 8) 137 | let [name, serialnum, school, gradelevel] = s.unpack(record) 138 | ``` 139 | --------------------------------------------------------------------------------