Match NBT path quoted escapes and index tokens

This commit is contained in:
voidsip committed 2026-11-06 05:53:36 -06:00
1 parent f4dcf8a2fb
commit db25d1d092
4 files changed
+193 -3

No files matched your search

@@ -0,0 +1,22 @@
//! NBT path parser output and byte cursor for the pinned Java oracle.
use horizon_client_core::nbt_path::NbtPath;
use std::io::{BufRead, Write};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1);
let input = std::io::BufReader::new(std::fs::File::open(args.next().unwrap())?);
let mut output = std::io::BufWriter::new(std::fs::File::create(args.next().unwrap())?);
for line in input.lines() {
let text: String = serde_json::from_str(&line?)?;
match NbtPath::parse_prefix(&text) {
Ok((path, cursor)) => {
write!(output, "OK {cursor} ")?;
for value in path.as_str().as_bytes() {
write!(output, "{value:02x}")?;
}
writeln!(output)?;
}
Err(error) => writeln!(output, "ERR {} {}", error.cursor, error.key)?,
}
}
Ok(())
}
+49 -3
View File
@@ -726,11 +726,16 @@ fn parse_node(text: &str, i: usize, first: bool) -> Result<(Node, usize), PathEr
Some(b']') => Ok((Node::AllElements, j + 1)),
_ => {
let start = j;
if b.get(j) == Some(&b'-') {
// StringReader.readInt scans all allowed number characters
// before parsing, including malformed signs and decimal points.
while b
.get(j)
.is_some_and(|c| c.is_ascii_digit() || matches!(c, b'-' | b'.'))
{
j += 1;
}
while b.get(j).is_some_and(u8::is_ascii_digit) {
j += 1;
if start == j {
return Err(PathError::new("parsing.int.expected", vec![], start));
}
let n: i32 = text[start..j].parse().map_err(|_| {
PathError::new(
@@ -792,6 +797,13 @@ fn read_quoted(text: &str, i: usize) -> Result<(String, usize), PathError> {
let mut escaped = false;
for (o, c) in text[i + 1..].char_indices() {
if escaped {
if c != quote && c != '\\' {
return Err(PathError::new(
"parsing.quote.escape",
vec![c.to_string()],
i + 1 + o,
));
}
out.push(c);
escaped = false;
} else if c == '\\' {
@@ -840,6 +852,40 @@ fn parse_compound(text: &str, i: usize) -> Result<(Tag, usize), PathError> {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quoted_escapes_and_index_tokens_match_brigadier() {
let cases = [
(r#""a\q""#, "parsing.quote.escape", vec!["q"], 3),
(r#"'a\x'"#, "parsing.quote.escape", vec!["x"], 3),
("[1.0]", "parsing.int.invalid", vec!["1.0"], 1),
("[.]", "parsing.int.invalid", vec!["."], 1),
("[--1]", "parsing.int.invalid", vec!["--1"], 1),
("[2147483648]", "parsing.int.invalid", vec!["2147483648"], 1),
("[+1]", "parsing.int.expected", vec![], 1),
(r#""é\q""#, "parsing.quote.escape", vec!["q"], 4),
(r#""é"[1.0]"#, "parsing.int.invalid", vec!["1.0"], 5),
];
let mut failures = Vec::new();
for (text, key, args, cursor) in cases {
match NbtPath::parse_prefix(text) {
Err(error) if error.key == key && error.args == args && error.cursor == cursor => {}
other => failures.push(format!("{text:?}: {other:?}")),
}
}
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[test]
fn valid_quoted_escapes_still_select_the_exact_field() {
let root = Tag::compound([("a\"b", Tag::Int(7)), ("a\\b", Tag::Int(8))]);
for (path, value) in [(r#""a\"b""#, 7), (r#""a\\b""#, 8)] {
assert_eq!(
NbtPath::parse(path).unwrap().get(&root).unwrap(),
vec![Tag::Int(value)]
);
}
}
#[test]
fn field_provenance_handles_quotes_and_filters_without_matching_lists() {
for text in [
@@ -0,0 +1,44 @@
package dev.horizonlab.parity;
import com.google.gson.JsonParser;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import net.minecraft.commands.arguments.NbtPathArgument;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.contents.TranslatableContents;
/** Real NbtPathArgument parsing only: no game bootstrap, resources or server. */
public final class VanillaNbtPathReaderOracle {
public static void main(String[] args) throws Exception {
try (var out = Files.newBufferedWriter(Path.of(args[1]), StandardCharsets.UTF_8)) {
for (String line : Files.readAllLines(Path.of(args[0]), StandardCharsets.UTF_8)) {
String text = JsonParser.parseString(line).getAsString();
StringReader reader = new StringReader(text);
try {
var path = NbtPathArgument.nbtPath().parse(reader);
out.write("OK " + utf8Cursor(text, reader.getCursor()) + " ");
for (byte value : path.toString().getBytes(StandardCharsets.UTF_8))
out.write(String.format("%02x", value & 255));
} catch (CommandSyntaxException error) {
var built = CommandSyntaxException.BUILT_IN_EXCEPTIONS;
String key = error.getType() == built.readerInvalidEscape() ? "parsing.quote.escape"
: error.getType() == built.readerExpectedEndOfQuote() ? "parsing.quote.expected.end"
: error.getType() == built.readerInvalidInt() ? "parsing.int.invalid"
: error.getType() == built.readerExpectedInt() ? "parsing.int.expected"
: error.getType() == built.readerExpectedSymbol() ? "parsing.expected"
: error.getRawMessage() instanceof Component component
&& component.getContents() instanceof TranslatableContents contents
? contents.getKey() : "unknown";
out.write("ERR " + utf8Cursor(text, reader.getCursor()) + " " + key);
}
out.newLine();
}
}
}
private static int utf8Cursor(String text, int cursor) {
return text.substring(0, cursor).getBytes(StandardCharsets.UTF_8).length;
}
}
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""NBT path quote/index lexical rules, result and byte cursor against vanilla.
Uses the real NbtPathArgument/StringReader without a server or prepared resources.
Covers quoted escapes, Unicode keys and prefixes, numeric index tokens, empty and
unterminated controls. Error categories/cursors are compared, not localized text
or invalid-escape argument UTF-16 representation. SNBT filters and path operations
and the vanilla unchecked bare-bracket exception are outside this parser-only corpus.
"""
import itertools
import json
import os
from pathlib import Path
import random
try:
from . import lab, oracle_harness as h, test_rust_parity as base
except ImportError:
import lab, oracle_harness as h, test_rust_parity as base
def fixtures():
values = ['', ' ', 'a', 'a.b', 'a.', 'a..b', '[]', '[0]', '[-1]',
'a[2147483647]', 'a[-2147483648]', 'a[2147483648]',
'a[-2147483649]', 'a[1.0]', 'a[.]', 'a[ ]', 'a[+1]']
for quote, prefix in itertools.product(('"', "'"), ('', 'a', '\u00e9', '\U0001f600')):
for char in [chr(i) for i in range(128)] + ['\u00e9', '\u4e2d', '\U0001f600']:
values.append(quote + prefix + '\\' + char + quote)
for key in ('', 'plain', 'a.b', 'a[b]', 'a b', '\u00e9', '\U0001f600', 'a\u4e2d\U0001f600b'):
values.extend([quote + key + quote, quote + key, quote + key + '\\'])
rng = random.Random(0x11039)
for _ in range(2500):
token = ''.join(rng.choice('0129-.') for _ in range(rng.randrange(1, 18)))
for prefix in ('', 'a', '"\u00e9"', '"\U0001f600"'):
values.append(prefix + '[' + token + ']')
for prefix, token, suffix in itertools.product(('', 'list'),
('', '-', '.', '--1', '1.2', '1', '-2', '0'), ('', ']', '+]', 'x]', ' ]')):
values.append(prefix + '[' + token + suffix)
# Vanilla parseNode peeks past a bare terminal '[' and throws an unchecked
# exception. This corpus checks command syntax errors, not that crash.
return [value for value in dict.fromkeys(values) if not value.endswith('[')]
def main():
lock, jars, libraries = base.verify_artifacts()
build = lab.LOCAL / 'nbt-path-reader-parity'
build.mkdir(parents=True, exist_ok=True)
report = build / 'report.json'
report.unlink(missing_ok=True)
paths = ['crates/horizon-client-core/src/nbt_path.rs',
'crates/horizon-client-core/examples/nbt_path_reader_probe.rs',
'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaNbtPathReaderOracle.java',
'tools/test_nbt_path_reader_parity.py']
sources = [lab.ROOT / path for path in paths]
hashes = base.source_hashes(sources)
cases = fixtures()
inputs = build / 'inputs.jsonl'
inputs.write_text(''.join(json.dumps(text) + '\n' for text in cases), encoding='utf-8')
classes, dependencies, _ = h.compile_oracle(build, 'VanillaNbtPathReaderOracle',
{'client': jars['server']}, libraries, lock['minecraft']['java_major'])
h.java_run(build, classes, dependencies, 'VanillaNbtPathReaderOracle',
[inputs, build / 'java.txt'], 'java')
h.run_logged(['cargo', 'build', '--offline', '--locked', '-p', 'horizon-client-core',
'--example', 'nbt_path_reader_probe'], build, 'compile-rust',
env=dict(os.environ, CARGO_BUILD_JOBS=os.environ.get('CARGO_BUILD_JOBS', '2')))
binary = lab.cargo_target() / 'debug/examples' / (
'nbt_path_reader_probe.exe' if os.name == 'nt' else 'nbt_path_reader_probe')
h.run_logged([binary, inputs, build / 'rust.txt'], build, 'rust')
matched = h.compare(build / 'java.txt', build / 'rust.txt', len(cases))
base.ensure_sources_unchanged(sources, hashes)
lab.write_json(report, dict(schema=1, minecraft=lock['minecraft']['version'],
cases=len(cases), matched_output_lines=matched, sources_sha256=hashes,
input_sha256=lab.digest(inputs, 'sha256'),
server_jar_sha256=lab.digest(jars['server'], 'sha256'), scope=__doc__))
print(f'{matched} NBT path reader cases match the pinned server: {report}', flush=True)
if __name__ == '__main__':
main()