node-osc 11.2.0 → 11.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.github/workflows/bump-version.yml +3 -3
  2. package/.github/workflows/create-release.yml +4 -4
  3. package/.github/workflows/nodejs.yml +4 -4
  4. package/README.md +3 -2
  5. package/agent.md +330 -0
  6. package/dist/lib/Client.js +1 -1
  7. package/dist/lib/Message.js +3 -1
  8. package/dist/lib/Server.js +7 -12
  9. package/dist/lib/internal/decode.js +3 -1
  10. package/dist/lib/osc.js +32 -3
  11. package/dist/test/lib/osc.js +32 -3
  12. package/dist/test/test-bundle.js +13 -12
  13. package/dist/test/test-client.js +68 -37
  14. package/dist/test/test-decode.js +35 -0
  15. package/dist/test/test-e2e.js +9 -9
  16. package/dist/test/test-encode-decode.js +455 -0
  17. package/dist/test/test-error-handling.js +14 -13
  18. package/dist/test/test-message.js +261 -64
  19. package/dist/test/test-osc-internal.js +151 -0
  20. package/dist/test/test-promises.js +90 -26
  21. package/dist/test/test-server.js +19 -16
  22. package/docs/README.md +81 -0
  23. package/examples/README.md +3 -1
  24. package/lib/Client.mjs +1 -1
  25. package/lib/Message.mjs +3 -1
  26. package/lib/Server.mjs +7 -12
  27. package/lib/internal/decode.mjs +3 -1
  28. package/lib/osc.mjs +32 -3
  29. package/package.json +2 -2
  30. package/rollup.config.mjs +1 -0
  31. package/test/test-bundle.mjs +14 -13
  32. package/test/test-client.mjs +69 -38
  33. package/test/test-decode.mjs +35 -0
  34. package/test/test-e2e.mjs +10 -10
  35. package/test/test-encode-decode.mjs +455 -0
  36. package/test/test-error-handling.mjs +15 -14
  37. package/test/test-message.mjs +262 -66
  38. package/test/test-osc-internal.mjs +151 -0
  39. package/test/test-promises.mjs +91 -27
  40. package/test/test-server.mjs +20 -17
  41. package/types/Message.d.mts.map +1 -1
  42. package/types/Server.d.mts.map +1 -1
  43. package/types/internal/decode.d.mts.map +1 -1
  44. package/types/osc.d.mts.map +1 -1
  45. package/dist/test/test-getPort.js +0 -20
  46. package/dist/test/util.js +0 -34
  47. package/test/test-getPort.mjs +0 -18
  48. package/test/util.mjs +0 -34
@@ -35,14 +35,14 @@ jobs:
35
35
  # run that runs on: tag. (Using the GitHub token would
36
36
  # not run the workflow to prevent infinite recursion.)
37
37
  - name: Check out source
38
- uses: actions/checkout@v4
38
+ uses: actions/checkout@v6
39
39
  with:
40
40
  ssh-key: ${{ secrets.DEPLOY_KEY }}
41
41
 
42
42
  - name: Setup Node.js
43
- uses: actions/setup-node@v4
43
+ uses: actions/setup-node@v6
44
44
  with:
45
- node-version: 22
45
+ node-version: 24
46
46
  cache: 'npm'
47
47
 
48
48
  - name: Install npm packages
@@ -17,11 +17,11 @@ jobs:
17
17
  id-token: write
18
18
  steps:
19
19
  - name: Checkout source
20
- uses: actions/checkout@v4
20
+ uses: actions/checkout@v6
21
21
  - name: Setup node
22
- uses: actions/setup-node@v4
22
+ uses: actions/setup-node@v6
23
23
  with:
24
- node-version: 22
24
+ node-version: 24
25
25
  registry-url: 'https://registry.npmjs.org'
26
26
  cache: npm
27
27
  - name: Install latest npm
@@ -39,7 +39,7 @@ jobs:
39
39
  contents: write
40
40
  steps:
41
41
  - name: Checkout code
42
- uses: actions/checkout@v4
42
+ uses: actions/checkout@v6
43
43
  - name: Create Release
44
44
  run: gh release create ${{ github.ref }} --generate-notes
45
45
  env:
@@ -14,19 +14,19 @@ jobs:
14
14
  run-tests:
15
15
  strategy:
16
16
  matrix:
17
- node-version: ['24', '22', '20']
17
+ node-version: ['25', '24', '22', '20']
18
18
  os: [ubuntu-latest, macos-latest, windows-latest]
19
19
 
20
20
  runs-on: ${{ matrix.os }}
21
21
 
22
22
  steps:
23
- - uses: actions/checkout@v4
23
+ - uses: actions/checkout@v6
24
24
 
25
25
  - name: Use Node.js ${{ matrix.node-version }}
26
- uses: actions/setup-node@v4
26
+ uses: actions/setup-node@v6
27
27
  with:
28
28
  node-version: ${{ matrix.node-version }}
29
- cache: 'npm'
29
+ cache: ${{ runner.os != 'Windows' && 'npm' || '' }}
30
30
 
31
31
  - name: Install Dependencies
32
32
  run: npm ci
package/README.md CHANGED
@@ -47,8 +47,9 @@ server.on('message', (msg) => {
47
47
 
48
48
  ## Documentation
49
49
 
50
- - 📚 **[API Documentation](./docs/API.md)** - Complete API reference generated from source code
51
- - 📘 **[Guide](./docs/GUIDE.md)** - Best practices, error handling, and troubleshooting
50
+ - 📂 **[Documentation Hub](./docs/)** - Complete documentation with navigation guide
51
+ - 📚 **[API Reference](./docs/API.md)** - Complete API reference generated from source code
52
+ - 📘 **[Usage Guide](./docs/GUIDE.md)** - Best practices, error handling, and troubleshooting
52
53
  - 📖 **[Examples](./examples/)** - Working examples for various use cases
53
54
 
54
55
  ## Compatibility
package/agent.md ADDED
@@ -0,0 +1,330 @@
1
+ # Agent Instructions for node-osc
2
+
3
+ This document provides context and instructions for AI agents (GitHub Copilot, Cursor, and other agentic platforms) working on the node-osc project.
4
+
5
+ ## Project Overview
6
+
7
+ **node-osc** is a Node.js library for sending and receiving [Open Sound Control (OSC)](http://opensoundcontrol.org) messages over UDP. It provides a simple, no-frills API inspired by pyOSC.
8
+
9
+ ### Key Features
10
+ - Send and receive OSC messages and bundles
11
+ - Dual module support (ESM and CommonJS)
12
+ - Both callback and async/await APIs
13
+ - TypeScript type definitions generated from JSDoc
14
+ - Well-tested with comprehensive test coverage
15
+ - Supports Node.js 20, 22, and 24
16
+
17
+ ## Architecture
18
+
19
+ ### Core Components
20
+
21
+ 1. **Server** (`lib/Server.mjs`) - EventEmitter-based OSC server for receiving messages
22
+ - Listens on UDP socket
23
+ - Emits events: `listening`, `message`, `bundle`, `error`, and address-specific events
24
+
25
+ 2. **Client** (`lib/Client.mjs`) - OSC client for sending messages
26
+ - Sends messages over UDP
27
+ - Supports both callbacks and async/await
28
+
29
+ 3. **Message** (`lib/Message.mjs`) - Represents a single OSC message
30
+ - Contains address (string) and arguments (array)
31
+ - Can append additional arguments
32
+
33
+ 4. **Bundle** (`lib/Bundle.mjs`) - Represents a collection of OSC messages
34
+ - Contains timetag and array of elements (messages or nested bundles)
35
+ - Used for sending multiple messages together
36
+
37
+ 5. **Low-level encoding/decoding** (`lib/osc.mjs`, `lib/internal/`) - Binary OSC protocol implementation
38
+ - `encode()` - Converts Message/Bundle objects to binary Buffer
39
+ - `decode()` - Parses binary Buffer into Message/Bundle objects
40
+
41
+ ### Module System
42
+
43
+ The project uses **ESM as the source format** but provides **dual ESM/CommonJS support**:
44
+ - Source files: `lib/**/*.mjs` (ESM)
45
+ - Built CommonJS files: `dist/lib/**/*.js` (transpiled via Rollup)
46
+ - TypeScript definitions: `types/index.d.mts` (generated from JSDoc)
47
+
48
+ **Important:** The single `.d.mts` type definition file works for both ESM and CommonJS consumers.
49
+
50
+ ### Package Exports
51
+
52
+ ```json
53
+ {
54
+ "exports": {
55
+ "types": "./types/index.d.mts",
56
+ "require": "./dist/lib/index.js",
57
+ "import": "./lib/index.mjs",
58
+ "default": "./lib/index.mjs"
59
+ }
60
+ }
61
+ ```
62
+
63
+ ## Development Workflow
64
+
65
+ ### Essential Commands
66
+
67
+ ```bash
68
+ # Install dependencies
69
+ npm install
70
+
71
+ # Run linter (ESLint)
72
+ npm run lint
73
+
74
+ # Build the project (clean, transpile to CJS, generate types)
75
+ npm run build
76
+
77
+ # Run all tests (lint + build + ESM tests + CJS tests)
78
+ npm test
79
+
80
+ # Run only ESM tests
81
+ npm run test:esm
82
+
83
+ # Run only CJS tests
84
+ npm run test:cjs
85
+
86
+ # Generate API documentation from JSDoc
87
+ npm run docs
88
+
89
+ # Clean build artifacts
90
+ npm run clean
91
+ ```
92
+
93
+ ### Testing Strategy
94
+
95
+ - Tests are written in ESM format in `test/test-*.mjs`
96
+ - Tests are run against both ESM source (`lib/`) and transpiled CJS (`dist/`)
97
+ - Uses `tap` test framework
98
+ - Test utilities in `test/util.mjs` provide helpers like `getPort()` for getting available ports
99
+ - Always run `npm run build` before running CJS tests
100
+ - **100% test coverage is required** - All lines, branches, functions, and statements must be covered
101
+
102
+ ### Build Process
103
+
104
+ 1. **Clean**: Removes `dist/` and `types/` directories
105
+ 2. **Rollup**: Transpiles ESM to CommonJS in `dist/` directory
106
+ 3. **TypeScript**: Generates type definitions from JSDoc in `types/` directory
107
+
108
+ The build is automatically run before publishing (`prepublishOnly` script).
109
+
110
+ ## Coding Standards
111
+
112
+ ### JavaScript Style
113
+
114
+ - **ES Modules**: Use ESM syntax (`import`/`export`)
115
+ - **File extension**: Use `.mjs` for ESM files
116
+ - **Linting**: Follow ESLint rules in `eslint.config.mjs`
117
+ - **Modern JavaScript**: Use async/await, arrow functions, destructuring
118
+ - **Error handling**: Always handle errors in async operations
119
+
120
+ ### Documentation
121
+
122
+ - **JSDoc comments**: All public APIs must have JSDoc comments
123
+ - **Type annotations**: Use JSDoc types for TypeScript generation
124
+ - **Examples**: Include code examples in JSDoc comments
125
+ - **Auto-generated docs**: Run `npm run docs` after changing JSDoc comments
126
+
127
+ Example JSDoc pattern:
128
+ ```javascript
129
+ /**
130
+ * Sends an OSC message or bundle.
131
+ *
132
+ * @param {Message|Bundle|string} msg - The message, bundle, or address to send.
133
+ * @param {...*} args - Additional arguments (used when first param is a string address).
134
+ * @returns {Promise<void>}
135
+ *
136
+ * @example
137
+ * await client.send('/test', 123);
138
+ *
139
+ * @example
140
+ * const message = new Message('/test', 123);
141
+ * await client.send(message);
142
+ */
143
+ async send(msg, ...args) { ... }
144
+ ```
145
+
146
+ ### Type System
147
+
148
+ - TypeScript definitions are **generated** from JSDoc comments
149
+ - Do not manually edit `types/*.d.mts` files
150
+ - Update JSDoc comments in source files instead
151
+ - Run `npm run build:types` to regenerate types
152
+
153
+ ### Naming Conventions
154
+
155
+ - **Classes**: PascalCase (e.g., `Client`, `Server`, `Message`, `Bundle`)
156
+ - **Functions**: camelCase (e.g., `encode`, `decode`, `toBuffer`)
157
+ - **Private functions**: Prefix with underscore (e.g., `_oscType`)
158
+ - **Constants**: UPPER_SNAKE_CASE for true constants
159
+ - **Files**: Match class names or use descriptive kebab-case
160
+
161
+ ### Dual Module Support Patterns
162
+
163
+ When writing code that needs to work in both ESM and CJS:
164
+
165
+ 1. **Imports**: Use ESM imports in source (Rollup handles conversion)
166
+ 2. **Exports**: Use named exports for all public APIs
167
+ 3. **Testing**: Test both ESM and CJS builds
168
+ 4. **Package imports**: Use `#decode` subpath import for internal modules (defined in `package.json` imports field)
169
+
170
+ ## Important Files and Directories
171
+
172
+ ### Source Files
173
+ - `lib/` - ESM source code (the canonical source)
174
+ - `lib/index.mjs` - Main entry point, exports all public APIs
175
+ - `lib/internal/` - Internal utilities (decode, encode, helpers)
176
+ - `lib/osc.mjs` - Low-level encode/decode functions
177
+
178
+ ### Build Artifacts
179
+ - `dist/` - Transpiled CommonJS files (generated, do not edit)
180
+ - `types/` - TypeScript type definitions (generated, do not edit)
181
+
182
+ ### Tests
183
+ - `test/test-*.mjs` - Test files using tap framework
184
+ - `test/util.mjs` - Test utilities and helpers
185
+ - `test/fixtures/` - Test data and fixtures
186
+
187
+ ### Documentation
188
+ - `README.md` - Main documentation with quick start guide
189
+ - `docs/API.md` - Auto-generated API reference (do not edit manually)
190
+ - `docs/GUIDE.md` - Best practices, error handling, troubleshooting
191
+ - `examples/` - Working example code for various use cases
192
+
193
+ ### Configuration
194
+ - `package.json` - Package configuration, scripts, exports
195
+ - `eslint.config.mjs` - ESLint configuration
196
+ - `rollup.config.mjs` - Rollup build configuration (ESM to CJS)
197
+ - `tsconfig.json` - TypeScript compiler options for type generation
198
+ - `jsdoc.json` - JSDoc configuration for documentation generation
199
+
200
+ ## Making Changes
201
+
202
+ ### Adding a New Feature
203
+
204
+ 1. **Write ESM source** in `lib/`
205
+ 2. **Add JSDoc comments** with types and examples
206
+ 3. **Export** from `lib/index.mjs` if it's a public API
207
+ 4. **Write tests** in `test/test-*.mjs` - **must achieve 100% coverage** (lines, branches, functions, statements)
208
+ 5. **Run tests**: `npm test` (tests both ESM and CJS)
209
+ 6. **Update docs**: `npm run docs` to regenerate API.md
210
+ 7. **Update README.md** if adding user-facing functionality
211
+
212
+ ### Fixing a Bug
213
+
214
+ 1. **Write a failing test** that demonstrates the bug
215
+ 2. **Fix the bug** in the ESM source files
216
+ 3. **Run tests**: `npm test` to verify fix works in both ESM and CJS
217
+ 4. **Verify coverage**: Ensure 100% test coverage is maintained
218
+ 5. **Check no regressions**: Ensure all tests pass
219
+
220
+ ### Modifying the API
221
+
222
+ 1. **Update JSDoc** in source files
223
+ 2. **Regenerate types**: `npm run build:types`
224
+ 3. **Update tests** to cover new behavior - **must maintain 100% coverage**
225
+ 4. **Regenerate docs**: `npm run docs`
226
+ 5. **Update README.md** and `docs/GUIDE.md` as appropriate
227
+
228
+ ## Common Patterns
229
+
230
+ ### Creating a Server
231
+ ```javascript
232
+ import { Server } from 'node-osc';
233
+
234
+ const server = new Server(3333, '0.0.0.0');
235
+ server.on('message', (msg, rinfo) => {
236
+ console.log('Message:', msg);
237
+ });
238
+ ```
239
+
240
+ ### Creating a Client
241
+ ```javascript
242
+ import { Client } from 'node-osc';
243
+
244
+ const client = new Client('127.0.0.1', 3333);
245
+ await client.send('/test', 123);
246
+ await client.close();
247
+ ```
248
+
249
+ ### Working with Bundles
250
+ ```javascript
251
+ import { Bundle } from 'node-osc';
252
+
253
+ const bundle = new Bundle(['/one', 1], ['/two', 2]);
254
+ await client.send(bundle);
255
+ ```
256
+
257
+ ### Low-level Encoding/Decoding
258
+ ```javascript
259
+ import { Message, encode, decode } from 'node-osc';
260
+
261
+ const message = new Message('/test', 123);
262
+ const buffer = encode(message);
263
+ const decoded = decode(buffer);
264
+ ```
265
+
266
+ ## Troubleshooting
267
+
268
+ ### Build Issues
269
+
270
+ - **"Cannot find module"**: Run `npm install` to install dependencies
271
+ - **Type generation fails**: Check JSDoc syntax in source files
272
+ - **CJS tests fail but ESM pass**: Run `npm run build` before testing
273
+
274
+ ### Test Issues
275
+
276
+ - **Port conflicts**: Tests use dynamic port allocation via `getPort()` utility
277
+ - **Timing issues**: Use async/await and proper event handling
278
+ - **ESM/CJS differences**: Ensure code works in both environments
279
+
280
+ ### Module Resolution
281
+
282
+ - **Dual package hazard**: The package exports both ESM and CJS - don't mix them
283
+ - **Type imports**: TypeScript consumers get types automatically from `types/index.d.mts`
284
+ - **Internal imports**: Use `#decode` subpath for internal modules
285
+
286
+ ## Dependencies
287
+
288
+ ### Runtime Dependencies
289
+ - **None** - This is a zero-dependency library for production use
290
+
291
+ ### Development Dependencies
292
+ - **eslint** - Code linting
293
+ - **tap** - Test framework
294
+ - **rollup** - Module bundler for ESM → CJS transpilation
295
+ - **typescript** - Type definition generation from JSDoc
296
+ - **jsdoc** - Documentation generation
297
+ - **globals** - ESLint globals configuration
298
+
299
+ ## OSC Protocol Knowledge
300
+
301
+ When working with OSC message encoding/decoding:
302
+
303
+ - OSC addresses start with `/` (e.g., `/oscillator/frequency`)
304
+ - OSC types: integer (i), float (f), string (s), blob (b), time tag (t)
305
+ - Messages are null-padded to 4-byte boundaries
306
+ - Bundles have time tags (when to execute) and can contain nested bundles
307
+ - See [OSC Specification](http://opensoundcontrol.org/spec-1_0) for protocol details
308
+
309
+ ## Security Considerations
310
+
311
+ - Always validate input data when decoding OSC messages
312
+ - Be careful with buffer operations to avoid out-of-bounds access
313
+ - Limit message and bundle sizes to prevent DoS attacks
314
+ - Sanitize OSC addresses before using them as event names
315
+ - Handle malformed OSC data gracefully (emit errors, don't crash)
316
+
317
+ ## License
318
+
319
+ This project uses the Apache-2.0 license. When contributing code:
320
+ - Ensure all new code is compatible with Apache-2.0
321
+ - Do not introduce dependencies with incompatible licenses
322
+ - Include proper attribution for any third-party code
323
+
324
+ ## Getting Help
325
+
326
+ - **API Documentation**: See `docs/API.md`
327
+ - **Usage Guide**: See `docs/GUIDE.md`
328
+ - **Examples**: See `examples/` directory
329
+ - **Issues**: Check existing GitHub issues for similar problems
330
+ - **OSC Protocol**: Refer to http://opensoundcontrol.org for protocol details
@@ -156,7 +156,7 @@ class Client extends node_events.EventEmitter {
156
156
  if (message instanceof Array) {
157
157
  message = {
158
158
  address: message[0],
159
- args: message.splice(1)
159
+ args: message.slice(1)
160
160
  };
161
161
  }
162
162
 
@@ -111,7 +111,9 @@ class Message {
111
111
  let argOut;
112
112
  switch (typeof arg) {
113
113
  case 'object':
114
- if (arg instanceof Array) {
114
+ if (Buffer.isBuffer(arg)) {
115
+ this.args.push(arg);
116
+ } else if (arg instanceof Array) {
115
117
  arg.forEach(a => this.append(a));
116
118
  } else if (arg.type) {
117
119
  if (typeTags[arg.type]) arg.type = typeTags[arg.type];
@@ -91,18 +91,13 @@ class Server extends node_events.EventEmitter {
91
91
  });
92
92
  this._sock.bind(port, host);
93
93
 
94
- // Support both callback and promise-based listening
95
- if (cb) {
96
- this._sock.on('listening', () => {
97
- this.emit('listening');
98
- cb();
99
- });
100
- } else {
101
- // For promise support, still emit the event but don't require a callback
102
- this._sock.on('listening', () => {
103
- this.emit('listening');
104
- });
105
- }
94
+ // Update port and emit listening event when socket is ready
95
+ this._sock.on('listening', () => {
96
+ // Update port with actual bound port (important when using port 0)
97
+ this.port = this._sock.address().port;
98
+ this.emit('listening');
99
+ if (cb) cb();
100
+ });
106
101
 
107
102
  this._sock.on('message', (msg, rinfo) => {
108
103
  try {
@@ -5,7 +5,8 @@ var osc = require('../osc.js');
5
5
  function sanitizeMessage(decoded) {
6
6
  const message = [];
7
7
  message.push(decoded.address);
8
- decoded.args.forEach(arg => {
8
+ const args = decoded.args ?? [];
9
+ args.forEach(arg => {
9
10
  message.push(arg.value);
10
11
  });
11
12
  return message;
@@ -15,6 +16,7 @@ function sanitizeBundle(decoded) {
15
16
  decoded.elements = decoded.elements.map(element => {
16
17
  if (element.oscType === 'bundle') return sanitizeBundle(element);
17
18
  else if (element.oscType === 'message') return sanitizeMessage(element);
19
+ throw new Error('Malformed Packet');
18
20
  });
19
21
  return decoded;
20
22
  }
package/dist/lib/osc.js CHANGED
@@ -8,8 +8,9 @@ var node_buffer = require('node:buffer');
8
8
 
9
9
  function padString(str) {
10
10
  const nullTerminated = str + '\0';
11
- const padding = 4 - (nullTerminated.length % 4);
12
- return nullTerminated + '\0'.repeat(padding === 4 ? 0 : padding);
11
+ const byteLength = node_buffer.Buffer.byteLength(nullTerminated);
12
+ const padding = (4 - (byteLength % 4)) % 4;
13
+ return nullTerminated + '\0'.repeat(padding);
13
14
  }
14
15
 
15
16
  function readString(buffer, offset) {
@@ -17,6 +18,9 @@ function readString(buffer, offset) {
17
18
  while (end < buffer.length && buffer[end] !== 0) {
18
19
  end++;
19
20
  }
21
+ if (end >= buffer.length) {
22
+ throw new Error('Malformed Packet: Missing null terminator for string');
23
+ }
20
24
  const str = buffer.subarray(offset, end).toString('utf8');
21
25
  // Find next 4-byte boundary
22
26
  const paddedLength = Math.ceil((end - offset + 1) / 4) * 4;
@@ -30,6 +34,9 @@ function writeInt32(value) {
30
34
  }
31
35
 
32
36
  function readInt32(buffer, offset) {
37
+ if (offset + 4 > buffer.length) {
38
+ throw new Error('Malformed Packet: Not enough bytes for int32');
39
+ }
33
40
  const value = buffer.readInt32BE(offset);
34
41
  return { value, offset: offset + 4 };
35
42
  }
@@ -41,6 +48,9 @@ function writeFloat32(value) {
41
48
  }
42
49
 
43
50
  function readFloat32(buffer, offset) {
51
+ if (offset + 4 > buffer.length) {
52
+ throw new Error('Malformed Packet: Not enough bytes for float32');
53
+ }
44
54
  const value = buffer.readFloatBE(offset);
45
55
  return { value, offset: offset + 4 };
46
56
  }
@@ -56,9 +66,18 @@ function writeBlob(value) {
56
66
  function readBlob(buffer, offset) {
57
67
  const lengthResult = readInt32(buffer, offset);
58
68
  const length = lengthResult.value;
69
+ if (length < 0) {
70
+ throw new Error('Malformed Packet: Invalid blob length');
71
+ }
72
+ if (lengthResult.offset + length > buffer.length) {
73
+ throw new Error('Malformed Packet: Not enough bytes for blob');
74
+ }
59
75
  const data = buffer.subarray(lengthResult.offset, lengthResult.offset + length);
60
76
  const padding = 4 - (length % 4);
61
77
  const nextOffset = lengthResult.offset + length + (padding === 4 ? 0 : padding);
78
+ if (nextOffset > buffer.length) {
79
+ throw new Error('Malformed Packet: Not enough bytes for blob padding');
80
+ }
62
81
  return { value: data, offset: nextOffset };
63
82
  }
64
83
 
@@ -66,7 +85,11 @@ function writeTimeTag(value) {
66
85
  // For now, treat timetag as a double (8 bytes)
67
86
  // OSC timetag is 64-bit: 32-bit seconds since 1900, 32-bit fractional
68
87
  const buffer = node_buffer.Buffer.alloc(8);
69
- if (typeof value === 'number') {
88
+ if (value === 0 || value === null || value === undefined) {
89
+ // Immediate execution
90
+ buffer.writeUInt32BE(0, 0);
91
+ buffer.writeUInt32BE(1, 4);
92
+ } else if (typeof value === 'number') {
70
93
  // Convert to OSC timetag format
71
94
  const seconds = Math.floor(value);
72
95
  const fraction = Math.floor((value - seconds) * 0x100000000);
@@ -81,6 +104,9 @@ function writeTimeTag(value) {
81
104
  }
82
105
 
83
106
  function readTimeTag(buffer, offset) {
107
+ if (offset + 8 > buffer.length) {
108
+ throw new Error('Malformed Packet: Not enough bytes for timetag');
109
+ }
84
110
  const seconds = buffer.readUInt32BE(offset);
85
111
  const fraction = buffer.readUInt32BE(offset + 4);
86
112
 
@@ -376,6 +402,9 @@ function decodeBundleFromBuffer(buffer) {
376
402
  const sizeResult = readInt32(buffer, offset);
377
403
  const size = sizeResult.value;
378
404
  offset = sizeResult.offset;
405
+ if (size <= 0 || offset + size > buffer.length) {
406
+ throw new Error('Malformed Packet');
407
+ }
379
408
 
380
409
  // Read element data
381
410
  const elementBuffer = buffer.subarray(offset, offset + size);
@@ -8,8 +8,9 @@ var node_buffer = require('node:buffer');
8
8
 
9
9
  function padString(str) {
10
10
  const nullTerminated = str + '\0';
11
- const padding = 4 - (nullTerminated.length % 4);
12
- return nullTerminated + '\0'.repeat(padding === 4 ? 0 : padding);
11
+ const byteLength = node_buffer.Buffer.byteLength(nullTerminated);
12
+ const padding = (4 - (byteLength % 4)) % 4;
13
+ return nullTerminated + '\0'.repeat(padding);
13
14
  }
14
15
 
15
16
  function readString(buffer, offset) {
@@ -17,6 +18,9 @@ function readString(buffer, offset) {
17
18
  while (end < buffer.length && buffer[end] !== 0) {
18
19
  end++;
19
20
  }
21
+ if (end >= buffer.length) {
22
+ throw new Error('Malformed Packet: Missing null terminator for string');
23
+ }
20
24
  const str = buffer.subarray(offset, end).toString('utf8');
21
25
  // Find next 4-byte boundary
22
26
  const paddedLength = Math.ceil((end - offset + 1) / 4) * 4;
@@ -30,6 +34,9 @@ function writeInt32(value) {
30
34
  }
31
35
 
32
36
  function readInt32(buffer, offset) {
37
+ if (offset + 4 > buffer.length) {
38
+ throw new Error('Malformed Packet: Not enough bytes for int32');
39
+ }
33
40
  const value = buffer.readInt32BE(offset);
34
41
  return { value, offset: offset + 4 };
35
42
  }
@@ -41,6 +48,9 @@ function writeFloat32(value) {
41
48
  }
42
49
 
43
50
  function readFloat32(buffer, offset) {
51
+ if (offset + 4 > buffer.length) {
52
+ throw new Error('Malformed Packet: Not enough bytes for float32');
53
+ }
44
54
  const value = buffer.readFloatBE(offset);
45
55
  return { value, offset: offset + 4 };
46
56
  }
@@ -56,9 +66,18 @@ function writeBlob(value) {
56
66
  function readBlob(buffer, offset) {
57
67
  const lengthResult = readInt32(buffer, offset);
58
68
  const length = lengthResult.value;
69
+ if (length < 0) {
70
+ throw new Error('Malformed Packet: Invalid blob length');
71
+ }
72
+ if (lengthResult.offset + length > buffer.length) {
73
+ throw new Error('Malformed Packet: Not enough bytes for blob');
74
+ }
59
75
  const data = buffer.subarray(lengthResult.offset, lengthResult.offset + length);
60
76
  const padding = 4 - (length % 4);
61
77
  const nextOffset = lengthResult.offset + length + (padding === 4 ? 0 : padding);
78
+ if (nextOffset > buffer.length) {
79
+ throw new Error('Malformed Packet: Not enough bytes for blob padding');
80
+ }
62
81
  return { value: data, offset: nextOffset };
63
82
  }
64
83
 
@@ -66,7 +85,11 @@ function writeTimeTag(value) {
66
85
  // For now, treat timetag as a double (8 bytes)
67
86
  // OSC timetag is 64-bit: 32-bit seconds since 1900, 32-bit fractional
68
87
  const buffer = node_buffer.Buffer.alloc(8);
69
- if (typeof value === 'number') {
88
+ if (value === 0 || value === null || value === undefined) {
89
+ // Immediate execution
90
+ buffer.writeUInt32BE(0, 0);
91
+ buffer.writeUInt32BE(1, 4);
92
+ } else if (typeof value === 'number') {
70
93
  // Convert to OSC timetag format
71
94
  const seconds = Math.floor(value);
72
95
  const fraction = Math.floor((value - seconds) * 0x100000000);
@@ -81,6 +104,9 @@ function writeTimeTag(value) {
81
104
  }
82
105
 
83
106
  function readTimeTag(buffer, offset) {
107
+ if (offset + 8 > buffer.length) {
108
+ throw new Error('Malformed Packet: Not enough bytes for timetag');
109
+ }
84
110
  const seconds = buffer.readUInt32BE(offset);
85
111
  const fraction = buffer.readUInt32BE(offset + 4);
86
112
 
@@ -376,6 +402,9 @@ function decodeBundleFromBuffer(buffer) {
376
402
  const sizeResult = readInt32(buffer, offset);
377
403
  const size = sizeResult.value;
378
404
  offset = sizeResult.offset;
405
+ if (size <= 0 || offset + size > buffer.length) {
406
+ throw new Error('Malformed Packet');
407
+ }
379
408
 
380
409
  // Read element data
381
410
  const elementBuffer = buffer.subarray(offset, offset + size);