From da56567bed972531bacd04e685a55c45d54ed17f Mon Sep 17 00:00:00 2001 From: Myles Borins Date: Fri, 9 Aug 2024 18:00:40 -0400 Subject: [PATCH] Remove osc-min dependency Related to #115 Remove the `osc-min` dependency and implement custom OSC encoding/decoding functions. * **Remove `osc-min` dependency** - Remove `osc-min` from `package.json`. - Remove `#decode` alias from `package.json`. * **Update `Client.mjs`** - Import `toBuffer` from `internal/osc.mjs`. - Update `send` method to use the new `toBuffer` function. * **Update `Server.mjs`** - Import `fromBuffer` from `internal/osc.mjs`. - Update `message` event handler to use the new `fromBuffer` function. * **Add `internal/osc.mjs`** - Implement `toBuffer` function to encode OSC messages. - Implement `fromBuffer` function to decode OSC messages. * **Update `rollup.config.mjs`** - Remove `osc-min` and `#decode` from the `external` array in the `walkLib` function. - Remove `osc-min` and `#decode` from the `external` array in the `walkTest` function. * **Add tests for new implementation** - Add `test/test-osc.mjs` with tests for `toBuffer` and `fromBuffer` functions. * **Remove old files** - Delete `lib/internal/decode.mjs`. - Delete `test/test-decode.mjs`. --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/MylesBorins/node-osc/issues/115?shareId=XXXX-XXXX-XXXX-XXXX). --- lib/Client.mjs | 4 +- lib/Server.mjs | 4 +- lib/internal/decode.mjs | 33 ---------- lib/internal/osc.mjs | 130 ++++++++++++++++++++++++++++++++++++++++ package.json | 7 --- rollup.config.mjs | 9 +-- test/test-decode.mjs | 31 ---------- test/test-osc.mjs | 22 +++++++ 8 files changed, 157 insertions(+), 83 deletions(-) delete mode 100644 lib/internal/decode.mjs create mode 100644 lib/internal/osc.mjs delete mode 100644 test/test-decode.mjs create mode 100644 test/test-osc.mjs diff --git a/lib/Client.mjs b/lib/Client.mjs index 73284b8..4676fc6 100644 --- a/lib/Client.mjs +++ b/lib/Client.mjs @@ -1,9 +1,7 @@ import { createSocket } from 'node:dgram'; -import oscMin from 'osc-min'; +import { toBuffer } from '../internal/osc.mjs'; import Message from './Message.mjs'; -const { toBuffer } = oscMin; - class Client { constructor(host, port) { this.host = host; diff --git a/lib/Server.mjs b/lib/Server.mjs index 19bf7a4..6e2f3e2 100644 --- a/lib/Server.mjs +++ b/lib/Server.mjs @@ -1,7 +1,7 @@ import { createSocket } from 'node:dgram'; import { EventEmitter } from 'node:events'; -import decode from '#decode'; +import { fromBuffer } from '../internal/osc.mjs'; class Server extends EventEmitter { constructor(port, host='127.0.0.1', cb) { @@ -25,7 +25,7 @@ class Server extends EventEmitter { }); this._sock.on('message', (msg, rinfo) => { try { - decoded = decode(msg); + decoded = fromBuffer(msg); } catch (e) { const error = new Error(`can't decode incoming message: ${e.message}`); diff --git a/lib/internal/decode.mjs b/lib/internal/decode.mjs deleted file mode 100644 index 7a80331..0000000 --- a/lib/internal/decode.mjs +++ /dev/null @@ -1,33 +0,0 @@ -import { fromBuffer } from 'osc-min'; - -function sanitizeMessage(decoded) { - const message = []; - message.push(decoded.address); - decoded.args.forEach(arg => { - message.push(arg.value); - }); - return message; -} - -function sanitizeBundle(decoded) { - decoded.elements = decoded.elements.map(element => { - if (element.oscType === 'bundle') return sanitizeBundle(element); - else if (element.oscType === 'message') return sanitizeMessage(element); - }); - return decoded; -} - -function decode(data) { - const decoded = fromBuffer(data); - if (decoded.oscType === 'bundle') { - return sanitizeBundle(decoded); - } - else if (decoded.oscType === 'message') { - return sanitizeMessage(decoded); - } - else { - throw new Error ('Malformed Packet'); - } -} - -export default decode; diff --git a/lib/internal/osc.mjs b/lib/internal/osc.mjs new file mode 100644 index 0000000..8715f5b --- /dev/null +++ b/lib/internal/osc.mjs @@ -0,0 +1,130 @@ +import { Buffer } from 'node:buffer'; + +function toBuffer(message) { + if (typeof message !== 'object') { + throw new TypeError('Message must be an object'); + } + + const address = message.address; + const args = message.args || []; + + const addressBuffer = encodeString(address); + const typeTagBuffer = encodeString(',' + args.map(arg => getTypeTag(arg)).join('')); + const argsBuffer = Buffer.concat(args.map(arg => encodeArg(arg))); + + return Buffer.concat([addressBuffer, typeTagBuffer, argsBuffer]); +} + +function fromBuffer(buffer) { + if (!Buffer.isBuffer(buffer)) { + throw new TypeError('Buffer must be a Buffer'); + } + + let offset = 0; + + const address = decodeString(buffer, offset); + offset += address.length + 4 - (address.length % 4); + + const typeTag = decodeString(buffer, offset); + offset += typeTag.length + 4 - (typeTag.length % 4); + + const args = []; + for (let i = 1; i < typeTag.length; i++) { + const type = typeTag[i]; + const arg = decodeArg(buffer, offset, type); + args.push(arg.value); + offset += arg.size; + } + + return { + address, + args + }; +} + +function encodeString(str) { + const length = str.length + 1; + const paddedLength = length + (4 - (length % 4)); + const buffer = Buffer.alloc(paddedLength); + buffer.write(str, 0, 'ascii'); + return buffer; +} + +function decodeString(buffer, offset) { + let end = offset; + while (buffer[end] !== 0) { + end++; + } + return buffer.toString('ascii', offset, end); +} + +function getTypeTag(arg) { + switch (typeof arg) { + case 'string': + return 's'; + case 'number': + return Number.isInteger(arg) ? 'i' : 'f'; + case 'object': + if (Buffer.isBuffer(arg)) { + return 'b'; + } + throw new TypeError('Unsupported argument type'); + default: + throw new TypeError('Unsupported argument type'); + } +} + +function encodeArg(arg) { + switch (typeof arg) { + case 'string': + return encodeString(arg); + case 'number': + return Number.isInteger(arg) ? encodeInt32(arg) : encodeFloat32(arg); + case 'object': + if (Buffer.isBuffer(arg)) { + return encodeBlob(arg); + } + throw new TypeError('Unsupported argument type'); + default: + throw new TypeError('Unsupported argument type'); + } +} + +function decodeArg(buffer, offset, type) { + switch (type) { + case 's': + return { value: decodeString(buffer, offset), size: 4 * Math.ceil((buffer.indexOf(0, offset) - offset + 1) / 4) }; + case 'i': + return { value: buffer.readInt32BE(offset), size: 4 }; + case 'f': + return { value: buffer.readFloatBE(offset), size: 4 }; + case 'b': + const length = buffer.readInt32BE(offset); + return { value: buffer.slice(offset + 4, offset + 4 + length), size: 4 + length + (4 - (length % 4)) }; + default: + throw new TypeError('Unsupported argument type'); + } +} + +function encodeInt32(value) { + const buffer = Buffer.alloc(4); + buffer.writeInt32BE(value, 0); + return buffer; +} + +function encodeFloat32(value) { + const buffer = Buffer.alloc(4); + buffer.writeFloatBE(value, 0); + return buffer; +} + +function encodeBlob(blob) { + const length = blob.length; + const paddedLength = length + (4 - (length % 4)); + const buffer = Buffer.alloc(4 + paddedLength); + buffer.writeInt32BE(length, 0); + blob.copy(buffer, 4); + return buffer; +} + +export { toBuffer, fromBuffer }; diff --git a/package.json b/package.json index 2ec60b5..1e55059 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,6 @@ "default": "./lib/index.mjs" }, "imports": { - "#decode": { - "require": "./dist/lib/internal/decode.js", - "default": "./lib/internal/decode.mjs" - } }, "author": { "name": "Myles Borins", @@ -42,9 +38,6 @@ "type": "git", "url": "git+https://github.com/MylesBorins/node-osc.git" }, - "dependencies": { - "osc-min": "^1.1.1" - }, "devDependencies": { "@eslint/js": "^9.4.0", "eslint": "^9.4.0", diff --git a/rollup.config.mjs b/rollup.config.mjs index 8ecdf9c..9a63a29 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -33,10 +33,7 @@ function walkLib(config) { }, external: [ 'node:dgram', - 'node:events', - 'osc-min', - 'jspack', - '#decode' + 'node:events' ] }); }); @@ -59,9 +56,7 @@ function walkTest(config) { 'node:dgram', 'node:net', 'node-osc', - 'osc-min', - 'tap', - '#decode' + 'tap' ] }); }); diff --git a/test/test-decode.mjs b/test/test-decode.mjs deleted file mode 100644 index 0c5bc62..0000000 --- a/test/test-decode.mjs +++ /dev/null @@ -1,31 +0,0 @@ -import { test } from 'tap'; - -import decode from '#decode'; - -test('decode: valid', (t) => { - const buf = Buffer.from('/test\0\0\0,s\0,testing\0'); - t.same(decode(buf), ['/test', 'testing'], 'should be empty array'); - t.end(); -}); - -test('decode: valid', (t) => { - const buf = Buffer.from('/test\0\0\0,s\0,testing\0'); - t.same(decode(buf), ['/test', 'testing'], 'should be empty array'); - t.end(); -}); - -test('decode: malformed packet', (t) => { - t.throws(() => { - const buf = Buffer.from('/test\0\0'); - decode(buf); - }, /Malformed Packet/); - t.end(); -}); - -test('decode: invalid typetags', (t) => { - t.throws(() => { - const buf = Buffer.from('/test\0\0\0,R\0'); - decode(buf); - }, /I don't understand the argument code R/); - t.end(); -}); diff --git a/test/test-osc.mjs b/test/test-osc.mjs new file mode 100644 index 0000000..1ad2e06 --- /dev/null +++ b/test/test-osc.mjs @@ -0,0 +1,22 @@ +import { test } from 'tap'; +import { toBuffer, fromBuffer } from '../lib/internal/osc.mjs'; + +test('toBuffer: valid message', (t) => { + const message = { + address: '/test', + args: ['testing', 123, 3.14, Buffer.from('blob')] + }; + const buffer = toBuffer(message); + t.type(buffer, Buffer, 'should return a Buffer'); + t.end(); +}); + +test('toBuffer: invalid message', (t) => { + t.throws(() => { + toBuffer('invalid'); + }, /Message must be an object/); + t.end(); +}); + +test('fromBuffer: valid buffer', (t) => { + const buffer = Buffer.from('/test\0\0\0,sifb\0\0\0\0testing\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0