From 84058561bd7ca7ef18cb18f52b4f3b7dc625dd96 Mon Sep 17 00:00:00 2001 From: u9g Date: Tue, 12 Nov 2024 18:42:15 -0500 Subject: [PATCH 1/2] Add check for fields not existing when writing container --- src/datatypes/compiler-structures.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/datatypes/compiler-structures.js b/src/datatypes/compiler-structures.js index d1d1a84..67f4709 100644 --- a/src/datatypes/compiler-structures.js +++ b/src/datatypes/compiler-structures.js @@ -96,6 +96,7 @@ module.exports = { for (const { name } of type[1]) { const trueName = compiler.getField(name) code += `const ${trueName} = value.${name}\n` + code += `if (${trueName} === undefined) throw new Error("Missing bitfield field '${trueName}'")\n` if (name === trueName) names.push(name) else names.push(`${name}: ${trueName}`) } @@ -104,6 +105,7 @@ module.exports = { trueName = compiler.getField(name) if (_shouldBeInlined) code += `let ${name} = value\n` else code += `let ${trueName} = value.${name}\n` + code += `if (${trueName} === undefined) throw new Error("Missing field '${trueName}'")\n` } code += 'offset = ' + compiler.callType(trueName, type) + '\n' } From d567055742d303cefcf9831996fe6d864c34689d Mon Sep 17 00:00:00 2001 From: u9g Date: Tue, 8 Sep 2026 18:41:20 -0400 Subject: [PATCH 2/2] Skip the missing-field check for types that encode absence A field typed void, switch, option, or a type protodef does not define may legitimately be undefined; only the builtin value types are checked. --- src/datatypes/compiler-structures.js | 13 ++++++- test/missingFields.js | 57 ++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 test/missingFields.js diff --git a/src/datatypes/compiler-structures.js b/src/datatypes/compiler-structures.js index 67f4709..115b7d6 100644 --- a/src/datatypes/compiler-structures.js +++ b/src/datatypes/compiler-structures.js @@ -96,7 +96,7 @@ module.exports = { for (const { name } of type[1]) { const trueName = compiler.getField(name) code += `const ${trueName} = value.${name}\n` - code += `if (${trueName} === undefined) throw new Error("Missing bitfield field '${trueName}'")\n` + code += `if (${trueName} === undefined) throw new Error("Missing bitfield field '${name}'")\n` if (name === trueName) names.push(name) else names.push(`${name}: ${trueName}`) } @@ -105,7 +105,7 @@ module.exports = { trueName = compiler.getField(name) if (_shouldBeInlined) code += `let ${name} = value\n` else code += `let ${trueName} = value.${name}\n` - code += `if (${trueName} === undefined) throw new Error("Missing field '${trueName}'")\n` + if (requiresValue(compiler, type)) code += `if (${trueName} === undefined) throw new Error("Missing field '${name}'")\n` } code += 'offset = ' + compiler.callType(trueName, type) + '\n' } @@ -166,6 +166,15 @@ module.exports = { } } +// Builtin types that cannot encode an absent value; a type the compiler does not +// define may accept undefined, and void, switch and option do. +const valueTypes = new Set([...Object.keys(require('./numeric')), 'varint', 'bool', 'pstring', 'cstring', 'buffer', 'bitfield', 'mapper', 'array', 'count', 'container']) + +function requiresValue (compiler, type) { + while (typeof type === 'string' && compiler.types[type] && compiler.types[type] !== 'native') type = compiler.types[type] + return valueTypes.has(Array.isArray(type) ? type[0] : type) +} + function uniqueId () { return '_' + Math.random().toString(36).substr(2, 9) } diff --git a/test/missingFields.js b/test/missingFields.js new file mode 100644 index 0000000..27132cd --- /dev/null +++ b/test/missingFields.js @@ -0,0 +1,57 @@ +/* eslint-env mocha */ + +const expect = require('chai').expect +const { ProtoDefCompiler } = require('protodef').Compiler + +const compiler = new ProtoDefCompiler() +compiler.addTypes({ + Read: { + maybe: ['native', (buffer, offset) => ({ value: buffer[offset] || undefined, size: 1 })], + maybeType: ['parametrizable', (compiler) => compiler.wrapCode('return { value: buffer[offset] || undefined, size: 1 }')] + }, + Write: { + maybe: ['native', (value, buffer, offset) => { buffer[offset] = value === undefined ? 0 : value; return offset + 1 }], + maybeType: ['parametrizable', (compiler) => compiler.wrapCode('buffer[offset] = value === undefined ? 0 : value\nreturn offset + 1')] + }, + SizeOf: { + maybe: ['native', () => 1], + maybeType: ['parametrizable', (compiler) => compiler.wrapCode('return 1')] + } +}) +compiler.addTypesToCompile({ + maybe: 'native', + anonMaybe: ['maybeType', {}], + count: 'u8', + hit: ['option', 'u8'], + packet: ['container', [ + { name: 'mouse', type: 'u8' }, + { name: 'x', type: ['switch', { compareTo: 'mouse', fields: { 2: 'u8' }, default: 'void' }] }, + { name: 'y', type: 'hit' }, + { name: 'extra', type: ['option', 'u8'] }, + { name: 'nbt', type: 'maybe' }, + { name: 'anonNbt', type: 'anonMaybe' }, + { name: 'nothing', type: 'void' } + ]], + pair: ['container', [ + { name: 'a', type: 'u8' }, + { name: 'b', type: 'count' }, + { name: 'flags', anon: true, type: ['bitfield', [{ name: 'c', size: 4, signed: false }, { name: 'd', size: 4, signed: false }]] } + ]] +}) +const proto = compiler.compileProtoDefSync() + +describe('compiled container write', () => { + it('rejects a missing field', () => { + expect(() => proto.createPacketBuffer('pair', { a: 1, c: 1, d: 1 })).to.throw("Missing field 'b'") + }) + it('rejects an undefined field behind a type alias', () => { + expect(() => proto.createPacketBuffer('pair', { a: 1, b: undefined, c: 1, d: 1 })).to.throw("Missing field 'b'") + }) + it('rejects a missing bitfield field', () => { + expect(() => proto.createPacketBuffer('pair', { a: 1, b: 2, c: 1 })).to.throw("Missing bitfield field 'd'") + }) + it('accepts absent switch, option, void and native fields', () => { + expect(proto.createPacketBuffer('packet', { mouse: 0 })).to.deep.equal(Buffer.from([0, 0, 0, 0, 0])) + expect(proto.createPacketBuffer('packet', { mouse: 2, x: 7, y: 8, extra: 9, nbt: 5, anonNbt: 6 })).to.deep.equal(Buffer.from([2, 7, 1, 8, 1, 9, 5, 6])) + }) +})