Conversation
["hash", { alg, type, body }] writes the value serialized as `body`,
hashed with `alg`, as `type`; reading yields the digest. crc32 and
crc32c are built in, anything else goes through node's crypto and is a
Buffer. A CRC written into a signed type takes its two's complement.
The compiled writer needs the size of the body before it can serialize
it, which no writer could get at until now: WriteCompiler.callTypeSize
and SizeOfCompiler.callTypeWrite generate code with the sibling compiler
in the current scope and run it against that compiler's context.
SizeOfCompiler now remembers fixed-size natives so hashes into a
fixed-width type are sized without hashing.
| 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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Done, the only supported hashes are crc32c for now until a different one is requested.
| @@ -0,0 +1,39 @@ | |||
| const crypto = require('crypto') | |||
There was a problem hiding this comment.
I don't think this belongs in the lib, we should either import lib (like crc) or at least move the hashing code to src/datatypes/hash.js which would contain the interpreter code plus the hashing code
The latter + importing the crc lib could be best ; you already have to import crypto as we don't do inline SHA hashing or anything like that
There was a problem hiding this comment.
src/hash.js is gone, the code lives in src/datatypes/hash.js next to readHash/writeHash/sizeOfHash, and crypto is no longer imported anywhere.
I didn't import crc because it has no CRC32C, it only supports CRC-32.
| /** | ||
| * 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Agreed, and callTypeIn is deleted. compileProtoDefSync already generated sizeOf → write → read. The only trade-off I took here, is that body must be a named type, which the spec now requires.
| this.readCtx = readCtx | ||
| // Code from callTypeSize / callTypeWrite runs against the other context | ||
| writeCtx.sizeOfCtx = sizeOfCtx | ||
| sizeOfCtx.writeCtx = writeCtx |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Right, and that direction is gone entirely, SizeOfCompiler.callTypeWrite and the sizeOfCtx.writeCtx back-reference are both removed.
| // 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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Agreed, that line is gone, and nothing is injected into compile()'s scope any more. I went with the context-type option you listed: _crc32c: ['context', crc32c] in Write, which copies the function's source into the generated output, so the compiled code is self-contained and generated code calls ctx._crc32c(bodyBuffer).
With the spec defining an explicit list of algorithms, a digest's width is known before hashing: the compiled sizer is a constant and type has to be of constant size, checked when compiling. That was the only caller of SizeOfCompiler.callTypeWrite, so it and the sizeOf -> write back-reference are gone; a sizer never writes. The hashing code moves to src/datatypes/hash.js beside the interpreter functions, leaving no general-purpose hashing module in the lib, and no crypto import. The digest function is copied into the compiled context as ctx._crc32c rather than injected into compile()'s eval scope, so generated code is self-contained and an unknown algorithm is caught when compiling. A named body type is sized with a direct ctx.sizeOfCtx.<type> call; only an anonymous one, which has no function to call, still has its sizer generated in place.
No schema change, so nothing here reads differently.
…degen callTypeSize now emits a call to the sizer the sizeOf context already holds, ctx.sizeOfCtx.<type>(value), and throws when the body is not a named type. That leaves nothing generating code with another compiler, so callTypeIn and its scope-stack swap are gone: what the write compiler borrows from the sizeOf compiler is one function call, resolved by name. The capability this gives up, a body whose type is selected by a field of the enclosing container, was reachable only from an inline type. A named type cannot reference a parent field at all -- getField throws while the type is being generated -- so hash was alone in being able to do it, and the scope sharing was the whole reason it could. The interpreter rejects an inline body too, rather than accepting schemas the compiler refuses.
Implements the
hashdatatype proposed in PrismarineJS/prismarine-item#184 (PrismarineJS/prismarine-item#184 (comment)):Write serializes the value as
body, hashes the bytes and writes the digest astype; read yields the digest.crc32andcrc32care implemented insrc/hash.js(thecrcpackage has no CRC32C, which is what Minecraft uses); any otheralggoes through node'scryptoand produces a Buffer. A CRC written into a signed type such asi32is written in two's complement, so it reads back as Java would produce it.Compiler
A compiled writer had no way to size a nested value, so it couldn't serialize the body into a scratch buffer (#169 is about the same gap). This adds
WriteCompiler.callTypeSize(value, type)andSizeOfCompiler.callTypeWrite(value, type, offsetExpr), which generate code with the sibling compiler in the current scope (socompareToreferences resolve to the same variables) and run it against that compiler's context, exposed asctx.sizeOfCtx/ctx.writeCtxlike #169 does. Documented indoc/compiler.md.SizeOfCompileralso remembers fixed-size natives, so a hash intoi32/lu32is sized without hashing.Tests
test/misc.js, interpreted and compiled: the CRC check values, signed output, avarintdigest, sha256, a body that switches on a field of the enclosing container, and a hash over a list of hashes.Schema and docs are in the ProtoDef submodule: ProtoDef-io/ProtoDef#65 (the submodule pointer here targets that branch).
Consumer: the HashOps encoding in PrismarineJS/prismarine-item#184 is built on this type.