13 Commits

Author SHA1 Message Date
jwetzell 5449c8ec5b bump @jwetzell/osc to v1.4.0 2024-10-15 21:57:18 -05:00
jwetzell bf355706bb more bundle tests 2024-10-15 21:55:51 -05:00
jwetzell 31e0cb7d23 Merge pull request #26 from jwetzell/bundle-min-size
fix bundle minimum size check
2024-10-15 21:48:08 -05:00
jwetzell af6dd46303 fix bundle minimum size check 2024-10-15 21:47:33 -05:00
jwetzell 2ea1b928ba more tests 2024-10-15 21:41:47 -05:00
jwetzell 7bffb53691 split up passing and failing tests 2024-10-15 21:38:53 -05:00
jwetzell abc9445b2c npm run format:write 2024-10-15 21:36:53 -05:00
jwetzell 69a3eeec9f Merge pull request #25 from jwetzell/osc-address-only
allow no type string
2024-10-15 21:36:16 -05:00
jwetzell 99ae9d8a3b allow no type string 2024-10-15 21:35:53 -05:00
jwetzell b3dc125df3 npm run format:write 2024-10-14 21:35:16 -05:00
jwetzell 713fdc8959 Merge pull request #24 from jwetzell/message-error-handling
rework message testing for easier addition of failing tests
2024-10-14 21:34:39 -05:00
jwetzell 1a9decfa30 rework tests to have easily add tests that should fail 2024-10-14 21:33:35 -05:00
jwetzell 374bf0d39f messageToBuffer should error on invalid address 2024-10-14 21:15:29 -05:00
7 changed files with 291 additions and 151 deletions
+1 -1
View File
@@ -3651,7 +3651,7 @@
}, },
"packages/osc": { "packages/osc": {
"name": "@jwetzell/osc", "name": "@jwetzell/osc",
"version": "1.3.0", "version": "1.4.0",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "22.7.5", "@types/node": "22.7.5",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@jwetzell/osc", "name": "@jwetzell/osc",
"version": "1.3.0", "version": "1.4.0",
"description": "", "description": "",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
+1 -1
View File
@@ -3,7 +3,7 @@ import { OSCBundle } from './models';
import { oscTypeConverterMap } from './osc-types'; import { oscTypeConverterMap } from './osc-types';
export function bundleFromBuffer(bytes: Uint8Array): [OSCBundle | undefined, Uint8Array | undefined] { export function bundleFromBuffer(bytes: Uint8Array): [OSCBundle | undefined, Uint8Array | undefined] {
if (bytes.length < 8) { if (bytes.length < 20) {
throw new Error('bundle has to be at least 20 bytes'); throw new Error('bundle has to be at least 20 bytes');
} }
+38 -23
View File
@@ -28,28 +28,32 @@ function argsToBuffer(args: OSCArg[]) {
} }
export function messageToBuffer(message: OSCMessage): Uint8Array { export function messageToBuffer(message: OSCMessage): Uint8Array {
if (!message.address.startsWith('/')) {
throw new Error('osc message must start with a /');
}
const addressBuffer = oscTypeConverterMap.s.toBuffer(message.address); const addressBuffer = oscTypeConverterMap.s.toBuffer(message.address);
if (addressBuffer === undefined) { if (addressBuffer === undefined) {
throw new Error('problem encoding address'); throw new Error('problem encoding address');
} }
let typeString = ',' let typeString = ',';
const flatOSCArgs: OSCArg[] = [] const flatOSCArgs: OSCArg[] = [];
message.args.forEach((oscArg)=> { message.args.forEach((oscArg) => {
if(Array.isArray(oscArg)){ if (Array.isArray(oscArg)) {
typeString += '[' typeString += '[';
oscArg.forEach((oscArg)=>{ oscArg.forEach((oscArg) => {
typeString += oscArg.type typeString += oscArg.type;
flatOSCArgs.push(oscArg) flatOSCArgs.push(oscArg);
}) });
typeString += ']' typeString += ']';
}else { } else {
typeString += oscArg.type typeString += oscArg.type;
flatOSCArgs.push(oscArg) flatOSCArgs.push(oscArg);
} }
}) });
const typesBuffer = oscTypeConverterMap.s.toBuffer(typeString); const typesBuffer = oscTypeConverterMap.s.toBuffer(typeString);
if (typesBuffer === undefined) { if (typesBuffer === undefined) {
@@ -72,6 +76,17 @@ export function messageFromBuffer(bytes: Uint8Array): [OSCMessage | undefined, U
const [address, bytesAfterAddress] = oscTypeConverterMap.s.fromBuffer(bytes); const [address, bytesAfterAddress] = oscTypeConverterMap.s.fromBuffer(bytes);
if (typeof address === 'string') { if (typeof address === 'string') {
if (bytesAfterAddress.length === 0) {
// NOTE(jwetzell): OSC 1.0 spec says that messages without a type string should be allowed
return [
{
address,
args: [],
},
bytesAfterAddress,
];
}
let [typeString, bytesAfterType] = oscTypeConverterMap.s.fromBuffer(bytesAfterAddress); let [typeString, bytesAfterType] = oscTypeConverterMap.s.fromBuffer(bytesAfterAddress);
if (typeof typeString === 'string') { if (typeof typeString === 'string') {
if (!typeString.startsWith(',')) { if (!typeString.startsWith(',')) {
@@ -83,18 +98,18 @@ export function messageFromBuffer(bytes: Uint8Array): [OSCMessage | undefined, U
for (let index = 1; index < typeString.length; index++) { for (let index = 1; index < typeString.length; index++) {
const argType = typeString.charAt(index) as OSCType; const argType = typeString.charAt(index) as OSCType;
if (argType === '['){ if (argType === '[') {
if (insideArgArray){ if (insideArgArray) {
throw new Error('osc arg array opened without closing previous arg array') throw new Error('osc arg array opened without closing previous arg array');
} }
oscArrayArg = []; oscArrayArg = [];
insideArgArray = true; insideArgArray = true;
continue; continue;
} }
if (argType === ']'){ if (argType === ']') {
if (!insideArgArray){ if (!insideArgArray) {
throw new Error('osc arg array closed without opening arg array') throw new Error('osc arg array closed without opening arg array');
} }
oscArgs.push(oscArrayArg); oscArgs.push(oscArrayArg);
oscArrayArg = []; oscArrayArg = [];
@@ -112,9 +127,9 @@ export function messageFromBuffer(bytes: Uint8Array): [OSCMessage | undefined, U
type: argType, type: argType,
value: value, value: value,
}; };
if(insideArgArray){ if (insideArgArray) {
oscArrayArg.push(arg) oscArrayArg.push(arg);
}else { } else {
oscArgs.push(arg); oscArgs.push(arg);
} }
} }
+105 -4
View File
@@ -1,8 +1,8 @@
const { deepEqual, equal } = require('assert'); const { deepEqual, equal, throws } = require('assert');
const { describe, it } = require('node:test'); const { describe, it } = require('node:test');
const osc = require('../dist/index'); const osc = require('../dist/index');
const tests = [ const goodTests = [
{ {
description: 'simple contents single message', description: 'simple contents single message',
bytes: new Uint8Array([ bytes: new Uint8Array([
@@ -20,6 +20,34 @@ const tests = [
contents: [{ address: '/oscillator/4/frequency', args: [{ type: 'f', value: 440 }] }], contents: [{ address: '/oscillator/4/frequency', args: [{ type: 'f', value: 440 }] }],
}, },
}, },
{
description: 'simple contents single bundle',
bytes: new Uint8Array([
...new TextEncoder().encode('#bundle'),
...new Uint8Array([0x00]),
...new Uint8Array([0, 0, 0, 32, 0, 0, 0, 0]),
...new Uint8Array([0x00, 0x00, 0x00, 0x34]),
...new Uint8Array([
...new TextEncoder().encode('#bundle'),
...new Uint8Array([0x00]),
...new Uint8Array([0, 0, 0, 32, 0, 0, 0, 0]),
...new Uint8Array([0x00, 0x00, 0x00, 0x20]),
...new Uint8Array([
0x2f, 0x6f, 0x73, 0x63, 0x69, 0x6c, 0x6c, 0x61, 0x74, 0x6f, 0x72, 0x2f, 0x34, 0x2f, 0x66, 0x72, 0x65, 0x71,
0x75, 0x65, 0x6e, 0x63, 0x79, 0x00, 0x2c, 0x66, 0x00, 0x00, 0x43, 0xdc, 0x00, 0x00,
]),
]),
]),
expected: {
timeTag: [32, 0],
contents: [
{
timeTag: [32, 0],
contents: [{ address: '/oscillator/4/frequency', args: [{ type: 'f', value: 440 }] }],
},
],
},
},
{ {
description: 'resolume bundle example', description: 'resolume bundle example',
bytes: new Uint8Array([ bytes: new Uint8Array([
@@ -88,8 +116,71 @@ const tests = [
}, },
]; ];
describe('OSC Bundle Decoding', () => { const badTests = [
tests.forEach((bundleTest) => { {
description: 'bad bundle header',
bytes: new Uint8Array([
...new TextEncoder().encode('bundle'),
...new Uint8Array([0x00]),
...new Uint8Array([0, 0, 0, 32, 0, 0, 0, 0]),
...new Uint8Array([0x00, 0x00, 0x00, 0x20]),
...new Uint8Array([
0x2f, 0x6f, 0x73, 0x63, 0x69, 0x6c, 0x6c, 0x61, 0x74, 0x6f, 0x72, 0x2f, 0x34, 0x2f, 0x66, 0x72, 0x65, 0x71,
0x75, 0x65, 0x6e, 0x63, 0x79, 0x00, 0x2c, 0x66, 0x00, 0x00, 0x43, 0xdc, 0x00, 0x00,
]),
]),
throwsMessage: {
name: /^Error$/,
message: 'bundle must start with #bundle',
},
},
{
description: 'incomplete bundle',
bytes: new Uint8Array([
...new TextEncoder().encode('#bundle'),
...new Uint8Array([0x00]),
...new Uint8Array([0, 0, 0, 32, 0, 0, 0, 0]),
]),
throwsMessage: {
name: /^Error$/,
message: 'bundle has to be at least 20 bytes',
},
},
{
description: 'bundle not enough bytes',
bytes: new Uint8Array([
...new TextEncoder().encode('#bundle'),
...new Uint8Array([0x00]),
...new Uint8Array([0, 0, 0, 32, 0, 0, 0, 0]),
...new Uint8Array([0x00, 0x00, 0x00, 0x20]),
...new Uint8Array([0x2f, 0x6f, 0x73, 0x63, 0x69, 0x6c, 0x6c, 0x61]),
]),
throwsMessage: {
name: /^Error$/,
message: 'bundle does not contain enough data',
},
},
{
description: 'bad bundle contents',
bytes: new Uint8Array([
...new TextEncoder().encode('#bundle'),
...new Uint8Array([0x00]),
...new Uint8Array([0, 0, 0, 32, 0, 0, 0, 0]),
...new Uint8Array([0x00, 0x00, 0x00, 0x20]),
...new Uint8Array([
0x2a, 0x6f, 0x73, 0x63, 0x69, 0x6c, 0x6c, 0x61, 0x74, 0x6f, 0x72, 0x2f, 0x34, 0x2f, 0x66, 0x72, 0x65, 0x71,
0x75, 0x65, 0x6e, 0x63, 0x79, 0x00, 0x2c, 0x66, 0x00, 0x00, 0x43, 0xdc, 0x00, 0x00,
]),
]),
throwsMessage: {
name: /^Error$/,
message: 'bundle contents does not look like a OSC message or bundle',
},
},
];
describe('OSC Bundle Decoding Pass', () => {
goodTests.forEach((bundleTest) => {
it(bundleTest.description, () => { it(bundleTest.description, () => {
const [encoded, remainingBytes] = osc.bundleFromBuffer(bundleTest.bytes); const [encoded, remainingBytes] = osc.bundleFromBuffer(bundleTest.bytes);
equal(remainingBytes.length, 0); equal(remainingBytes.length, 0);
@@ -97,3 +188,13 @@ describe('OSC Bundle Decoding', () => {
}); });
}); });
}); });
describe('OSC Bundle Decoding Throws', () => {
badTests.forEach((bundleTest) => {
it(bundleTest.description, () => {
throws(() => {
osc.bundleFromBuffer(bundleTest.bytes);
}, bundleTest.throwsMessage);
});
});
});
+86 -74
View File
@@ -2,7 +2,7 @@ const { deepEqual, throws, equal } = require('assert');
const { describe, it } = require('node:test'); const { describe, it } = require('node:test');
const osc = require('../dist/index'); const osc = require('../dist/index');
const tests = [ const goodTests = [
{ {
description: 'simple address no args', description: 'simple address no args',
bytes: new Uint8Array([47, 104, 101, 108, 108, 111, 0, 0, 44, 0, 0, 0]), bytes: new Uint8Array([47, 104, 101, 108, 108, 111, 0, 0, 44, 0, 0, 0]),
@@ -68,9 +68,26 @@ const tests = [
{ {
description: 'simple address array arg', description: 'simple address array arg',
bytes: new Uint8Array([ bytes: new Uint8Array([
47, 104, 101, 108, 108, 111, 0, 0, 44, 91, 100, 105, 93, 0, 0, 0, 0x40, 0x29, 0x87, 0xec, 0x82, 0x74, 0xb9, 0xe6, 0, 0, 3, 232 47, 104, 101, 108, 108, 111, 0, 0, 44, 91, 100, 105, 93, 0, 0, 0, 0x40, 0x29, 0x87, 0xec, 0x82, 0x74, 0xb9, 0xe6,
0, 0, 3, 232,
]), ]),
expected: { address: '/hello', args: [[{ type: 'd', value: 12.7654763 }, { type: 'i', value: 1000}]] }, expected: {
address: '/hello',
args: [
[
{ type: 'd', value: 12.7654763 },
{ type: 'i', value: 1000 },
],
],
},
},
{
description: 'simple address no type string',
bytes: new Uint8Array([47, 104, 101, 108, 108, 111, 0, 0]),
expected: {
address: '/hello',
args: [],
},
}, },
{ {
description: 'osc 1.0 spec example 1', description: 'osc 1.0 spec example 1',
@@ -100,83 +117,78 @@ const tests = [
}, },
]; ];
describe('OSC Message Decoding', () => { const badTests = [
tests.forEach((messageTest) => { {
description: 'bad address',
bytes: new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x2c, 0x66, 0x00, 0x00, 0x42, 0x0a, 0x00, 0x00]),
throwsMessage: { name: /^Error$/, message: /must start with/ },
},
{
description: 'bad type string',
bytes: new Uint8Array([0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x42, 0x0a, 0x00]),
throwsMessage: { name: /^Error$/, message: /type string must start with/ },
},
{
description: 'unknown type',
bytes: new Uint8Array([
0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x2c, 0x7a, 0x00, 0x00, 0x42, 0x0a, 0x00, 0x00,
]),
throwsMessage: { name: /^Error$/, message: /unknown/ },
},
{
description: 'float arg missing bytes',
bytes: new Uint8Array([0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x2c, 0x66, 0x00, 0x00, 0x42, 0x0a, 0x00]),
throwsMessage: { name: /^Error$/, message: /not enough bytes/ },
},
{
description: 'int arg missing bytes',
bytes: new Uint8Array([0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x2c, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00]),
throwsMessage: { name: /^Error$/, message: /not enough bytes/ },
},
{
description: 'blob bytes too small',
bytes: new Uint8Array([
0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x2c, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x62, 0x6c, 0x6f,
]),
throwsMessage: { name: /^Error$/, message: /not enough bytes/ },
},
{
description: 'float64 missing bytes',
bytes: new Uint8Array([
47, 104, 101, 108, 108, 111, 0, 0, 44, 100, 0, 0, 0x40, 0x29, 0x87, 0xec, 0x82, 0x74, 0xb9,
]),
throwsMessage: { name: /^Error$/, message: /not enough bytes/ },
},
{
description: 'int64 missing bytes',
bytes: new Uint8Array([47, 104, 101, 108, 108, 111, 0, 0, 44, 104, 0, 0, 0, 0, 255, 255, 255, 255, 255]),
throwsMessage: { name: /^Error$/, message: /must be at least/ },
},
{
description: 'color missing bytes',
bytes: new Uint8Array([47, 104, 101, 108, 108, 111, 0, 0, 44, 114, 0, 0, 20, 21, 22]),
throwsMessage: { name: /^Error$/, message: /must be at least/ },
},
];
describe('OSC Message Decoding Pass', () => {
goodTests.forEach((messageTest) => {
it(messageTest.description, () => { it(messageTest.description, () => {
const [decoded, remainingBytes] = osc.messageFromBuffer(messageTest.bytes); const [decoded, remainingBytes] = osc.messageFromBuffer(messageTest.bytes);
equal(remainingBytes.length, 0); equal(remainingBytes.length, 0);
deepEqual(decoded, messageTest.expected); deepEqual(decoded, messageTest.expected);
}); });
}); });
});
it('bad address', () => { describe('OSC Message Decoding Throws', () => {
throws( badTests.forEach((messageTest) => {
() => { it(messageTest.description, () => {
osc.messageFromBuffer( throws(() => {
new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x2c, 0x66, 0x00, 0x00, 0x42, 0x0a, 0x00, 0x00]) osc.messageFromBuffer(messageTest.bytes);
); }, messageTest.throwsMessage);
}, });
{ name: /^Error$/, message: /must start with/ }
);
});
it('bad type string', () => {
throws(
() => {
osc.messageFromBuffer(
new Uint8Array([0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x42, 0x0a, 0x00])
);
},
{ name: /^Error$/, message: /type string must start with/ }
);
});
it('unknown type', () => {
throws(
() => {
osc.messageFromBuffer(
new Uint8Array([
0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x2c, 0x7a, 0x00, 0x00, 0x42, 0x0a, 0x00, 0x00,
])
);
},
{ name: /^Error$/, message: /unknown/ }
);
});
it('float arg missing bytes', () => {
throws(
() => {
osc.messageFromBuffer(
new Uint8Array([0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x2c, 0x66, 0x00, 0x00, 0x42, 0x0a, 0x00])
);
},
{ name: /^Error$/, message: /not enough bytes/ }
);
});
it('int arg missing bytes', () => {
throws(
() => {
osc.messageFromBuffer(
new Uint8Array([0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x2c, 0x69, 0x00, 0x00, 0x00, 0x00, 0x00])
);
},
{ name: /^Error$/, message: /not enough bytes/ }
);
});
it('blob bytes too small', () => {
throws(
() => {
osc.messageFromBuffer(
new Uint8Array([
0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x2c, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x62, 0x6c,
0x6f,
])
);
},
{ name: /^Error$/, message: /not enough bytes/ }
);
}); });
}); });
+59 -47
View File
@@ -2,7 +2,7 @@ const { deepEqual, throws } = require('assert');
const { describe, it } = require('node:test'); const { describe, it } = require('node:test');
const osc = require('../dist/index'); const osc = require('../dist/index');
const tests = [ const goodTests = [
{ {
description: 'simple address no args', description: 'simple address no args',
message: { address: '/hello', args: [] }, message: { address: '/hello', args: [] },
@@ -62,9 +62,18 @@ const tests = [
}, },
{ {
description: 'simple address array arg', description: 'simple address array arg',
message: { address: '/hello', args: [[{ type: 'd', value: 12.7654763 }, { type: 'i', value: 1000}]] }, message: {
address: '/hello',
args: [
[
{ type: 'd', value: 12.7654763 },
{ type: 'i', value: 1000 },
],
],
},
expected: new Uint8Array([ expected: new Uint8Array([
47, 104, 101, 108, 108, 111, 0, 0, 44, 91, 100, 105, 93, 0, 0, 0, 0x40, 0x29, 0x87, 0xec, 0x82, 0x74, 0xb9, 0xe6, 0, 0, 3, 232 47, 104, 101, 108, 108, 111, 0, 0, 44, 91, 100, 105, 93, 0, 0, 0, 0x40, 0x29, 0x87, 0xec, 0x82, 0x74, 0xb9, 0xe6,
0, 0, 3, 232,
]), ]),
}, },
{ {
@@ -95,55 +104,58 @@ const tests = [
}, },
]; ];
describe('OSC Message Encoding', () => { const badTests = [
tests.forEach((messageTest) => { {
description: 'bad string arg',
message: { address: '/address', args: [{ type: 's', value: 123 }] },
throwsMessage: { name: /^TypeError$/, message: /non string/ },
},
{
description: 'bad integer arg',
message: { address: '/address', args: [{ type: 'i', value: 'hi' }] },
throwsMessage: { name: /^TypeError$/, message: /non number/ },
},
{
description: 'bad float arg',
message: { address: '/address', args: [{ type: 'f', value: 'hi' }] },
throwsMessage: { name: /^TypeError$/, message: /non number/ },
},
{
description: 'bad blob arg',
message: { address: '/address', args: [{ type: 'b', value: 123 }] },
throwsMessage: { name: /^TypeError$/, message: /non Uint8Array/ },
},
{
description: 'unknown arg type',
message: { address: '/address', args: [{ type: 'z', value: 123 }] },
throwsMessage: { name: /^TypeError$/, message: /unknown type z/ },
},
{
description: 'address that does not start with / should throw',
message: { address: 'address', args: [] },
throwsMessage: {
name: /^Error$/,
message: 'osc message must start with a /',
},
},
];
describe('OSC Message Encoding Pass', () => {
goodTests.forEach((messageTest) => {
it(messageTest.description, () => { it(messageTest.description, () => {
const encoded = osc.messageToBuffer(messageTest.message); const encoded = osc.messageToBuffer(messageTest.message);
deepEqual(encoded, messageTest.expected); deepEqual(encoded, messageTest.expected);
}); });
}); });
it('bad string arg', () => { });
throws(
() => {
osc.messageToBuffer({ address: '/address', args: [{ type: 's', value: 123 }] });
},
{ name: /^TypeError$/ }
);
});
it('bad integer arg', () => { describe('OSC Message Encoding Throws', () => {
throws( badTests.forEach((messageTest) => {
() => { it(messageTest.description, () => {
osc.messageToBuffer({ address: '/address', args: [{ type: 'i', value: 'hi' }] }); throws(() => {
}, osc.messageToBuffer(messageTest.message);
{ name: /^TypeError$/ } }, messageTest.throwsMessage);
); });
});
it('bad float arg', () => {
throws(
() => {
osc.messageToBuffer({ address: '/address', args: [{ type: 'f', value: 'hi' }] });
},
{ name: /^TypeError$/ }
);
});
it('bad blob arg', () => {
throws(
() => {
osc.messageToBuffer({ address: '/address', args: [{ type: 'b', value: 123 }] });
},
{ name: /^TypeError$/ }
);
});
it('unknown arg type', () => {
throws(
() => {
osc.messageToBuffer({ address: '/address', args: [{ type: 'z', value: 123 }] });
},
{ name: /^TypeError$/ }
);
}); });
}); });