Skip to content

Add the hash datatype - #177

Open
u9g wants to merge 1 commit into
masterfrom
hash-type
Open

Add the hash datatype#177
u9g wants to merge 1 commit into
masterfrom
hash-type

Conversation

@u9g

@u9g u9g commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Implements the hash datatype proposed in PrismarineJS/prismarine-item#184 (PrismarineJS/prismarine-item#184 (comment)):

["hash", { "alg": "crc32c", "type": "i32", "body": "ItemComponent" }]

Write serializes the value as body, hashes the bytes and writes the digest as type; read yields the digest. crc32 and crc32c are implemented in src/hash.js (the crc package has no CRC32C, which is what Minecraft uses); any other alg goes through node's crypto and produces a Buffer. A CRC written into a signed type such as i32 is 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) and SizeOfCompiler.callTypeWrite(value, type, offsetExpr), which generate code with the sibling compiler in the current scope (so compareTo references resolve to the same variables) and run it against that compiler's context, exposed as ctx.sizeOfCtx / ctx.writeCtx like #169 does. Documented in doc/compiler.md. SizeOfCompiler also remembers fixed-size natives, so a hash into i32/lu32 is sized without hashing.

Tests

test/misc.js, interpreted and compiled: the CRC check values, signed output, a varint digest, 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.

["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)

@extremeheat extremeheat Sep 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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

Comment thread src/hash.js
@@ -0,0 +1,39 @@
const crypto = require('crypto')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 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

Comment thread src/compiler.js
Comment on lines +182 to +198
/**
* 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
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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

Comment thread src/compiler.js
this.readCtx = readCtx
// Code from callTypeSize / callTypeWrite runs against the other context
writeCtx.sizeOfCtx = sizeOfCtx
sizeOfCtx.writeCtx = writeCtx

Copy link
Copy Markdown
Contributor

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)

Comment thread src/compiler.js
// 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

@extremeheat extremeheat Sep 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants