Skip to content

Instantly share code, notes, and snippets.

@JavaCS3
Created March 9, 2020 12:55
Show Gist options
  • Save JavaCS3/c940bedbdbd84367dbb48cceb57a6b3a to your computer and use it in GitHub Desktop.
Save JavaCS3/c940bedbdbd84367dbb48cceb57a6b3a to your computer and use it in GitHub Desktop.
const assert = require('assert')
const ENCODERS = {
raw(len) {
return function(v) {
assert(v instanceof Buffer)
const buffer = Buffer.alloc(len)
v.copy(buffer)
return buffer
}
},
u8(v) {
return Buffer.from([v])
},
u16(v) {
const buffer = Buffer.alloc(2)
buffer.writeInt16LE(v)
return buffer
},
u32(v) {
const buffer = Buffer.alloc(4)
buffer.writeInt32LE(v)
return buffer
},
charn(len) {
return function(v) {
const buffer = Buffer.alloc(len || v.length)
buffer.write(v)
return buffer
}
}
}
const DECODERS = {
raw(len) {
return function(buffer, offset) {
assert(buffer instanceof Buffer)
assert(buffer.length >= offset + len)
return {
value: buffer.slice(offset, offset + len),
size: len
}
}
},
u8(buffer, offset) {
return {
value: buffer.readUInt8(offset),
size: 1
}
},
u16(buffer, offset) {
return {
value: buffer.readUInt16LE(offset),
size: 2
}
},
u32(buffer, offset) {
return {
value: buffer.readUInt32LE(offset),
size: 4
}
},
charn(len) {
return function(buffer, offset) {
return {
value: buffer.toString('utf8', offset, offset + len),
size: len
}
}
}
}
class Rule {
constructor(name, encoder, decoder) {
assert(name, 'Invalid rule name')
assert(typeof encoder === 'function', 'Encoder must be a function')
assert(typeof decoder === 'function', 'Decoder must be a function')
this.name = name
this.encoder = encoder
this.decoder = decoder
}
encode(value) {
return this.encoder(value)
}
decode(buffer, offset) {
return this.decoder(buffer, offset)
}
}
class Rules {
constructor() {
this.rules = []
}
size() {
return this.rules.length
}
*[Symbol.iterator]() {
for (let i = 0; i < this.rules.length; i++) {
yield this.rules[i]
}
}
getRuleNames() {
return this.rules.map((r) => r.name)
}
add(rule) {
this.rules.push(rule)
}
get(index) {
return this.rules[index]
}
encode(...args) {
assert(args.length === this.size(), 'Encoding arguments mismatch with struct schema rules')
let buffer = Buffer.alloc(0)
for (let i = 0; i < args.length; i++) {
const value = args[i]
const rule = this.rules[i]
buffer = Buffer.concat([buffer, rule.encoder(value)])
}
return buffer
}
decode(buffer, clazz) {
let offset = 0
let keys = []
this.rules.forEach((rule) => {
const { value, size } = rule.decode(buffer, offset)
keys.push({
name: rule.name,
value: value
})
offset += size
})
return new clazz(...keys.map((f) => f.value))
}
}
function CStruct(name) {
let rules = new Rules()
class Struct {
constructor(...args) {
const len = Math.min(args.length, rules.size())
for (let i = 0; i < len; i++) {
const value = args[i]
const rule = rules.get(i)
this[rule.name] = value
}
}
buffer() {
const keys = rules.getRuleNames()
const values = keys.map((k) => this[k])
return rules.encode(...values)
}
static decode(buffer) {
return rules.decode(buffer, Struct)
}
static from(buffer) {
return rules.decode(buffer, Struct)
}
static raw(name, len) {
rules.add(new Rule(name, ENCODERS.raw(len), DECODERS.raw(len)))
return Struct
}
static u8(name) {
rules.add(new Rule(name, ENCODERS.u8, DECODERS.u8))
return Struct
}
static u16(name) {
rules.add(new Rule(name, ENCODERS.u16, DECODERS.u16))
return Struct
}
static u32(name) {
rules.add(new Rule(name, ENCODERS.u32, DECODERS.u32))
return Struct
}
static charn(name, len) {
rules.add(new Rule(name, ENCODERS.charn(len), DECODERS.charn(len)))
return Struct
}
}
Object.defineProperty(Struct, 'name', {
value: name || 'Struct'
})
return Struct
}
module.exports = {
CStruct
}
const { CStruct } = require('../cstruct')
test('Test define struct', () => {
const Point = CStruct()
.u8('x')
.u8('y')
const p = new Point(1, 2)
expect(p.x).toBe(1)
expect(p.y).toBe(2)
})
test('Test define struct with name', () => {
const Point = CStruct('Point')
.u8('x')
.u8('y')
const p = new Point(1, 2)
expect(p.constructor.name).toBe('Point')
})
test('Test define struct and change properties', () => {
const Point = CStruct()
.u8('x')
.u8('y')
const p = new Point(1, 2)
expect(p.x).toBe(1)
expect(p.y).toBe(2)
p.x = 100
p.y = 200
expect(p.x).toBe(100)
expect(p.y).toBe(200)
})
test('Test get struct buffer', () => {
const Point = CStruct()
.u8('x')
.u8('y')
const p = new Point(1, 2)
expect(p.buffer()).toEqual(Buffer.from([0x01, 0x02]))
})
test('Test get struct buffer when propety changed', () => {
const Point = CStruct()
.u8('x')
.u8('y')
const p = new Point(1, 2)
expect(p.buffer()).toEqual(Buffer.from([0x01, 0x02]))
p.x = 100
p.y = 200
expect(p.buffer()).toEqual(Buffer.from([100, 200]))
})
test('Test struct encode and decode', () => {
const Point = CStruct()
.u8('x')
.u8('y')
const p = new Point(1, 2)
const p1 = Point.decode(p.buffer())
expect(p1.x).toBe(1)
expect(p1.y).toBe(2)
})
test('Test struct encode/decode strn', () => {
const Person = CStruct()
.charn('name', 10)
.charn('sex', 10)
const p = new Person('James', 'Male')
const buffer = Buffer.alloc(20)
buffer.write('James')
buffer.write('Male', 10)
expect(p.buffer()).toEqual(buffer)
const p1 = Person.from(buffer)
expect(p1.name.replace(/\0/g, '')).toBe('James') // https://github.com/nodejs/node/issues/4775
expect(p1.sex.replace(/\0/g, '')).toBe('Male')
})
test('Test struct encode/decode raw', () => {
const Product = CStruct()
.charn('name', 10)
.raw('description', 100)
const p = new Product('MacBook', Buffer('A laptop produced by Apple Inc.'))
const buffer = Buffer.alloc(110)
buffer.write('MacBook')
buffer.write('A laptop produced by Apple Inc.', 10, 100)
expect(p.buffer()).toEqual(buffer)
const p1 = Product.from(buffer)
expect(p1.name.replace(/\0/g, '')).toBe('MacBook') // https://github.com/nodejs/node/issues/4775
expect(p1.description.toString().replace(/\0/g, '')).toBe('A laptop produced by Apple Inc.')
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment