-
Notifications
You must be signed in to change notification settings - Fork 25
Add the hash datatype #177
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| +1 −0 | doc/datatypes.md | |
| +24 −0 | doc/datatypes/utils.md | |
| +2 −1 | schemas/datatype.json | |
| +26 −0 | schemas/utils.json |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,8 @@ class ProtoDefCompiler { | |
| this.readCompiler = new ReadCompiler() | ||
| this.writeCompiler = new WriteCompiler() | ||
| this.sizeOfCompiler = new SizeOfCompiler() | ||
| this.writeCompiler.sizeOfCompiler = this.sizeOfCompiler | ||
| this.sizeOfCompiler.writeCompiler = this.writeCompiler | ||
| } | ||
|
|
||
| addTypes (types) { | ||
|
|
@@ -62,6 +64,9 @@ class CompiledProtodef { | |
| this.sizeOfCtx = sizeOfCtx | ||
| this.writeCtx = writeCtx | ||
| this.readCtx = readCtx | ||
| // Code from callTypeSize / callTypeWrite runs against the other context | ||
| writeCtx.sizeOfCtx = sizeOfCtx | ||
| sizeOfCtx.writeCtx = writeCtx | ||
| } | ||
|
|
||
| read (buffer, cursor, type) { | ||
|
|
@@ -174,6 +179,24 @@ class Compiler { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Generates code with another compiler inside this compiler's scope, so that | ||
| * field references resolve to the same variables, and binds it to that | ||
| * compiler's context. Natives are reachable through the context as well. | ||
| */ | ||
| callTypeIn (other, ctxName, generate) { | ||
| if (!other) throw new Error(`${ctxName} is only available when compiling with ProtoDefCompiler`) | ||
| const scopeStack = other.scopeStack | ||
| other.scopeStack = this.scopeStack | ||
| try { | ||
| const code = generate(other) | ||
| if (!isNaN(code)) return code | ||
| return `((ctx, native) => ${code})(ctx.${ctxName}, ctx.${ctxName})` | ||
| } finally { | ||
| other.scopeStack = scopeStack | ||
| } | ||
| } | ||
|
Comment on lines
+182
to
+198
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should not be this complicated, no need to generate anything at call time Compile order should be fixed to something like sizeOf=>write=>read so write can always call sizeOf |
||
|
|
||
| addTypesToCompile (types) { | ||
| for (const [type, json] of Object.entries(types)) { | ||
| // Replace native type, otherwise first in wins | ||
|
|
@@ -259,6 +282,7 @@ class Compiler { | |
| // Local variable to provide some context to eval() | ||
| const native = this.native // eslint-disable-line | ||
| const { PartialReadError } = require('./utils') // eslint-disable-line | ||
| const hashDigest = require('./hash').digest // eslint-disable-line | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is another codesmell, specific data types should not require injecting stuff like this into the pre compile step. parameterizable types do create duplication but all the types have the same issue, so we shouldn't inject just for this So that other JS code should be directly copied into the codegen step Or figure out way to use native/context type, but that would require looking at making them parameterizable or split the code between a 'parameterizable' type with a parameterizable part that calls some native/context function |
||
| return eval(code)() // eslint-disable-line | ||
| } | ||
| } | ||
|
|
@@ -361,11 +385,20 @@ class WriteCompiler extends Compiler { | |
| if (args.length > 0) return '(' + code + `)(${value}, buffer, ${offsetExpr}, ` + args.map(name => this.getField(name)).join(', ') + ')' | ||
| return '(' + code + `)(${value}, buffer, ${offsetExpr})` | ||
| } | ||
|
|
||
| /** | ||
| * Code computing the size of `value` as `type`, for writers that need to | ||
| * serialize part of a value before they can write it | ||
| */ | ||
| callTypeSize (value, type, args = []) { | ||
| return this.callTypeIn(this.sizeOfCompiler, 'sizeOfCtx', compiler => compiler.callType(value, type, args)) | ||
| } | ||
| } | ||
|
|
||
| class SizeOfCompiler extends Compiler { | ||
| constructor () { | ||
| super() | ||
| this.constants = {} | ||
|
|
||
| this.addTypes(conditionalDatatypes.SizeOf) | ||
| this.addTypes(structuresDatatypes.SizeOf) | ||
|
|
@@ -390,12 +423,22 @@ class SizeOfCompiler extends Compiler { | |
| this.primitiveTypes[type] = `native.${type}` | ||
| if (!isNaN(fn)) { | ||
| this.native[type] = (value) => { return fn } | ||
| this.constants[type] = fn | ||
| } else { | ||
| this.native[type] = fn | ||
| } | ||
| this.types[type] = 'native' | ||
| } | ||
|
|
||
| /** | ||
| * The size of `type` when it doesn't depend on the value, following | ||
| * aliases down to a fixed-size native; undefined otherwise | ||
| */ | ||
| constantSize (type) { | ||
| while (typeof type === 'string' && typeof this.types[type] === 'string' && this.types[type] !== 'native') type = this.types[type] | ||
| return this.constants[type] | ||
| } | ||
|
|
||
| compileType (type) { | ||
| if (type instanceof Array) { | ||
| if (this.parameterizableTypes[type[0]]) { return this.parameterizableTypes[type[0]](this, type[1]) } | ||
|
|
@@ -429,6 +472,14 @@ class SizeOfCompiler extends Compiler { | |
| if (args.length > 0) return '(' + code + `)(${value}, ` + args.map(name => this.getField(name)).join(', ') + ')' | ||
| return '(' + code + `)(${value})` | ||
| } | ||
|
|
||
| /** | ||
| * Code writing `value` as `type` into `buffer` at `offsetExpr`, for sizers | ||
| * whose result depends on the serialized form of a value | ||
| */ | ||
| callTypeWrite (value, type, offsetExpr = 'offset', args = []) { | ||
| return this.callTypeIn(this.writeCompiler, 'writeCtx', compiler => compiler.callType(value, type, offsetExpr, args)) | ||
| } | ||
| } | ||
|
|
||
| module.exports = { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -83,6 +83,9 @@ return { value, size } | |
| let code = 'const { value, size } = ' + compiler.callType(mapper.type) + '\n' | ||
| code += 'return { value: ' + JSON.stringify(sanitizeMappings(mapper.mappings)) + '[value] || value, size }' | ||
| return compiler.wrapCode(code) | ||
| }], | ||
| hash: ['parametrizable', (compiler, { type }) => { | ||
| return compiler.wrapCode('return ' + compiler.callType(type)) | ||
| }] | ||
| }, | ||
|
|
||
|
|
@@ -163,6 +166,18 @@ return (ctx.${type})(val, buffer, offset) | |
| code += 'if (mapped === undefined) throw new Error(value + \' is not in the mappings value\')\n' | ||
| code += 'return ' + compiler.callType('mapped', mapper.type) | ||
| return compiler.wrapCode(code) | ||
| }], | ||
| hash: ['parametrizable', (compiler, { alg, type, body }) => { | ||
| let code = `const bodyBuffer = Buffer.alloc(${compiler.callTypeSize('value', body)})\n` | ||
| code += `;((buffer) => ${compiler.callType('value', body, '0')})(bodyBuffer)\n` | ||
| code += `const hash = hashDigest(${JSON.stringify(alg)}, bodyBuffer)\n` | ||
| code += 'try {\n' | ||
| code += ' return ' + compiler.callType('hash', type) + '\n' | ||
| code += '} catch (e) {\n' | ||
| code += ' if (!(e instanceof RangeError) || typeof hash !== "number") throw e\n' | ||
| code += ' return ' + compiler.callType('hash | 0', type) + '\n' | ||
| code += '}' | ||
| return compiler.wrapCode(code) | ||
| }] | ||
| }, | ||
|
|
||
|
|
@@ -217,6 +232,17 @@ return (ctx.${type})(val) | |
| code += 'if (mapped === undefined) throw new Error(value + \' is not in the mappings value\')\n' | ||
| code += 'return ' + compiler.callType('mapped', mapper.type) | ||
| return compiler.wrapCode(code) | ||
| }], | ||
| hash: ['parametrizable', (compiler, { alg, type, body }) => { | ||
| const constant = compiler.constantSize(type) | ||
| if (constant !== undefined) return String(constant) | ||
| const size = compiler.callType('hash', type) | ||
| if (!isNaN(size)) return size | ||
| let code = `const bodyBuffer = Buffer.alloc(${compiler.callType('value', body)})\n` | ||
| code += `;((buffer) => ${compiler.callTypeWrite('value', body, '0')})(bodyBuffer)\n` | ||
| code += `const hash = hashDigest(${JSON.stringify(alg)}, bodyBuffer)\n` | ||
| code += 'return ' + size | ||
| return compiler.wrapCode(code) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hashes are pretty much always fixed in size. May be nicer to have ProtoDef spec support an explicit list of hashes (SHA1, SHA256, etc) rather than relying on whatever Node.js standard lib exposes. That will allow hard codeing the hash byte length into a map without having to fake hash first and allows other non-JS ProtoDef implementations to support an explicit list to be spec complaint |
||
| }] | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| const crypto = require('crypto') | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this belongs in the lib, we should either import lib (like The latter + importing the |
||
|
|
||
| // Reflected table-driven CRC with all-ones init and final xor; `poly` is the | ||
| // reversed polynomial. | ||
| const tables = {} | ||
| function table (poly) { | ||
| if (!tables[poly]) { | ||
| const t = new Int32Array(256) | ||
| for (let n = 0; n < 256; n++) { | ||
| let c = n | ||
| for (let k = 0; k < 8; k++) c = c & 1 ? poly ^ (c >>> 1) : c >>> 1 | ||
| t[n] = c | ||
| } | ||
| tables[poly] = t | ||
| } | ||
| return tables[poly] | ||
| } | ||
|
|
||
| function crc (poly, buffer) { | ||
| const t = table(poly) | ||
| let c = -1 | ||
| for (let i = 0; i < buffer.length; i++) c = t[(c ^ buffer[i]) & 0xff] ^ (c >>> 8) | ||
| return (c ^ -1) >>> 0 | ||
| } | ||
|
|
||
| const algorithms = { | ||
| crc32: buffer => crc(0xEDB88320, buffer), | ||
| crc32c: buffer => crc(0x82F63B78, buffer) | ||
| } | ||
|
|
||
| // CRC digests are unsigned integers; every other algorithm is delegated to | ||
| // node's crypto and yields a Buffer. | ||
| function digest (alg, buffer) { | ||
| const algorithm = algorithms[alg] | ||
| if (algorithm) return algorithm(buffer) | ||
| return crypto.createHash(alg).update(buffer).digest() | ||
| } | ||
|
|
||
| module.exports = { digest, algorithms } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SizeOfCompiler shouldn't need to write to figure out the size. That creates a potential cyclic dependency loop.
But it is useful for the WriteCompiler to know size of type such as for writing length prefixes for strings/array/buffer, hash digest, etc. Only reason looks like we didn't have this already is you can size a string/buffer in JS stdlib instead of needing ProtoDef (Buffer.byteLength vs needing to call our own sizeOf functions)