From 2191be3256aba5c258d0cc5b86fc6f9e72964c29 Mon Sep 17 00:00:00 2001 From: Myles Borins Date: Tue, 20 Aug 2024 14:44:45 -0400 Subject: [PATCH] Remove osc-min dependency Fixes #115 Remove the `osc-min` dependency and implement `toBuffer` and `fromBuffer` functions according to the OSC specification. * **package.json** - Remove the `osc-min` dependency. - Update the `imports` section to include `#osc` pointing to `internal/osc.mjs`. * **lib/Client.mjs** - Remove the import of `osc-min`. - Import `toBuffer` from `#osc`. - Update the `send` method to use `toBuffer` from `#osc`. * **lib/Server.mjs** - Remove the import of `#decode`. - Import `fromBuffer` from `#osc`. - Update the `_sock.on('message')` handler to use `fromBuffer` from `#osc`. * **internal/osc.mjs** - Implement the `toBuffer` function according to the OSC specification. - Implement the `fromBuffer` function according to the OSC specification. * **test/test-osc.mjs** - Add unit tests for `toBuffer` function. - Add unit tests for `fromBuffer` function. * **rollup.config.mjs** - Remove `osc-min` from the `external` array in the `walkLib` and `walkTest` functions. - Add `#osc` to the `external` array in the `walkLib` and `walkTest` functions. * **lib/internal/decode.mjs** - Delete the file. * **test/test-decode.mjs** - Delete the file. --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/MylesBorins/node-osc/issues/115?shareId=XXXX-XXXX-XXXX-XXXX). --- internal/osc.mjs | 218 ++++++++++++++++++++++++++++++++++++++++ lib/Client.mjs | 4 +- lib/Server.mjs | 4 +- lib/internal/decode.mjs | 33 ------ package.json | 10 +- rollup.config.mjs | 6 +- test/test-decode.mjs | 31 ------ test/test-osc.mjs | 65 ++++++++++++ 8 files changed, 292 insertions(+), 79 deletions(-) create mode 100644 internal/osc.mjs delete mode 100644 lib/internal/decode.mjs delete mode 100644 test/test-decode.mjs create mode 100644 test/test-osc.mjs diff --git a/internal/osc.mjs b/internal/osc.mjs new file mode 100644 index 0000000..8c0e94b --- /dev/null +++ b/internal/osc.mjs @@ -0,0 +1,218 @@ +import { Buffer } from 'node:buffer'; + +function toBuffer(object, strict = false) { + if (typeof object !== 'object' || object === null) { + throw new TypeError('Invalid OSC packet representation'); + } + + if (object.oscType === 'message') { + return encodeMessage(object, strict); + } else if (object.oscType === 'bundle') { + return encodeBundle(object, strict); + } else { + throw new TypeError('Invalid OSC packet representation'); + } +} + +function fromBuffer(buffer, strict = false) { + if (!Buffer.isBuffer(buffer)) { + throw new TypeError('Invalid OSC packet buffer'); + } + + const packet = decodePacket(buffer, strict); + if (packet.oscType === 'message') { + return sanitizeMessage(packet); + } else if (packet.oscType === 'bundle') { + return sanitizeBundle(packet); + } else { + throw new Error('Malformed Packet'); + } +} + +function encodeMessage(message, strict) { + const address = encodeString(message.address); + const typeTags = encodeString(',' + message.args.map(arg => getTypeTag(arg.type)).join('')); + const args = message.args.map(arg => encodeArgument(arg, strict)).join(''); + return Buffer.concat([address, typeTags, Buffer.from(args)]); +} + +function encodeBundle(bundle, strict) { + const timetag = encodeTimetag(bundle.timetag); + const elements = bundle.elements.map(element => { + const encodedElement = toBuffer(element, strict); + const size = Buffer.alloc(4); + size.writeUInt32BE(encodedElement.length, 0); + return Buffer.concat([size, encodedElement]); + }); + return Buffer.concat([Buffer.from('#bundle\0'), timetag, ...elements]); +} + +function decodePacket(buffer, strict) { + const address = decodeString(buffer); + if (address === '#bundle') { + return decodeBundle(buffer, strict); + } else { + return decodeMessage(buffer, strict); + } +} + +function decodeMessage(buffer, strict) { + const address = decodeString(buffer); + const typeTags = decodeString(buffer).slice(1); + const args = []; + for (const tag of typeTags) { + args.push(decodeArgument(buffer, tag, strict)); + } + return { oscType: 'message', address, args }; +} + +function decodeBundle(buffer, strict) { + const timetag = decodeTimetag(buffer); + const elements = []; + while (buffer.length > 0) { + const size = buffer.readUInt32BE(0); + const elementBuffer = buffer.slice(4, 4 + size); + elements.push(decodePacket(elementBuffer, strict)); + buffer = buffer.slice(4 + size); + } + return { oscType: 'bundle', timetag, elements }; +} + +function encodeString(str) { + const length = Buffer.byteLength(str); + const paddedLength = Math.ceil((length + 1) / 4) * 4; + const buffer = Buffer.alloc(paddedLength); + buffer.write(str, 0, length, 'ascii'); + return buffer; +} + +function decodeString(buffer) { + const end = buffer.indexOf(0); + const str = buffer.toString('ascii', 0, end); + buffer = buffer.slice(Math.ceil((end + 1) / 4) * 4); + return str; +} + +function encodeArgument(arg, strict) { + switch (arg.type) { + case 'integer': + return encodeInt32(arg.value); + case 'float': + return encodeFloat32(arg.value); + case 'string': + return encodeString(arg.value); + case 'blob': + return encodeBlob(arg.value); + default: + if (strict) { + throw new TypeError(`Unknown argument type: ${arg.type}`); + } + return ''; + } +} + +function decodeArgument(buffer, tag, strict) { + switch (tag) { + case 'i': + return { type: 'integer', value: decodeInt32(buffer) }; + case 'f': + return { type: 'float', value: decodeFloat32(buffer) }; + case 's': + return { type: 'string', value: decodeString(buffer) }; + case 'b': + return { type: 'blob', value: decodeBlob(buffer) }; + default: + if (strict) { + throw new TypeError(`Unknown argument type tag: ${tag}`); + } + return null; + } +} + +function encodeInt32(value) { + const buffer = Buffer.alloc(4); + buffer.writeInt32BE(value, 0); + return buffer; +} + +function decodeInt32(buffer) { + const value = buffer.readInt32BE(0); + buffer = buffer.slice(4); + return value; +} + +function encodeFloat32(value) { + const buffer = Buffer.alloc(4); + buffer.writeFloatBE(value, 0); + return buffer; +} + +function decodeFloat32(buffer) { + const value = buffer.readFloatBE(0); + buffer = buffer.slice(4); + return value; +} + +function encodeBlob(blob) { + const size = Buffer.alloc(4); + size.writeUInt32BE(blob.length, 0); + const paddedLength = Math.ceil(blob.length / 4) * 4; + const buffer = Buffer.alloc(paddedLength); + blob.copy(buffer); + return Buffer.concat([size, buffer]); +} + +function decodeBlob(buffer) { + const size = buffer.readUInt32BE(0); + const blob = buffer.slice(4, 4 + size); + buffer = buffer.slice(4 + size); + return blob; +} + +function encodeTimetag(timetag) { + const buffer = Buffer.alloc(8); + buffer.writeUInt32BE(Math.floor(timetag / 4294967296), 0); + buffer.writeUInt32BE(timetag % 4294967296, 4); + return buffer; +} + +function decodeTimetag(buffer) { + const seconds = buffer.readUInt32BE(0); + const fraction = buffer.readUInt32BE(4); + buffer = buffer.slice(8); + return seconds * 4294967296 + fraction; +} + +function getTypeTag(type) { + switch (type) { + case 'integer': + return 'i'; + case 'float': + return 'f'; + case 'string': + return 's'; + case 'blob': + return 'b'; + default: + return ''; + } +} + +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; +} + +export { toBuffer, fromBuffer }; diff --git a/lib/Client.mjs b/lib/Client.mjs index 73284b8..cfc6ac1 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 '#osc'; 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..bc42a37 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 '#osc'; 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/package.json b/package.json index 2ec60b5..7802bb5 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,9 @@ "default": "./lib/index.mjs" }, "imports": { - "#decode": { - "require": "./dist/lib/internal/decode.js", - "default": "./lib/internal/decode.mjs" + "#osc": { + "require": "./dist/lib/internal/osc.js", + "default": "./lib/internal/osc.mjs" } }, "author": { @@ -42,9 +42,7 @@ "type": "git", "url": "git+https://github.com/MylesBorins/node-osc.git" }, - "dependencies": { - "osc-min": "^1.1.1" - }, + "dependencies": {}, "devDependencies": { "@eslint/js": "^9.4.0", "eslint": "^9.4.0", diff --git a/rollup.config.mjs b/rollup.config.mjs index 8ecdf9c..8093d69 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -34,9 +34,8 @@ function walkLib(config) { external: [ 'node:dgram', 'node:events', - 'osc-min', 'jspack', - '#decode' + '#osc' ] }); }); @@ -59,9 +58,8 @@ function walkTest(config) { 'node:dgram', 'node:net', 'node-osc', - 'osc-min', 'tap', - '#decode' + '#osc' ] }); }); 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..6485948 --- /dev/null +++ b/test/test-osc.mjs @@ -0,0 +1,65 @@ +import { test } from 'tap'; +import { toBuffer, fromBuffer } from '#osc'; + +test('toBuffer: valid message', (t) => { + const message = { + oscType: 'message', + address: '/test', + args: [ + { type: 'string', value: 'testing' }, + { type: 'integer', value: 123 } + ] + }; + const buffer = toBuffer(message); + t.type(buffer, Buffer, 'should return a Buffer'); + t.end(); +}); + +test('toBuffer: valid bundle', (t) => { + const bundle = { + oscType: 'bundle', + timetag: 1, + elements: [ + { + oscType: 'message', + address: '/test', + args: [ + { type: 'string', value: 'testing' }, + { type: 'integer', value: 123 } + ] + } + ] + }; + const buffer = toBuffer(bundle); + t.type(buffer, Buffer, 'should return a Buffer'); + t.end(); +}); + +test('toBuffer: invalid packet', (t) => { + t.throws(() => { + toBuffer(null); + }, /Invalid OSC packet representation/); + t.end(); +}); + +test('fromBuffer: valid message', (t) => { + const buffer = Buffer.from('/test\0\0\0,s\0,testing\0'); + const message = fromBuffer(buffer); + t.same(message, ['/test', 'testing'], 'should return the expected message'); + t.end(); +}); + +test('fromBuffer: valid bundle', (t) => { + const buffer = Buffer.from('#bundle\0\0\0\0\0\0\0\0\0\0\0\0/test\0\0\0,s\0,testing\0'); + const bundle = fromBuffer(buffer); + t.same(bundle.elements[0], ['/test', 'testing'], 'should return the expected bundle'); + t.end(); +}); + +test('fromBuffer: malformed packet', (t) => { + t.throws(() => { + const buffer = Buffer.from('/test\0\0'); + fromBuffer(buffer); + }, /Malformed Packet/); + t.end(); +});