feat(command): implement SNBT parser (#2088)

* initial commit

* split snbt into modules

* formatted

* clippy warnings fixed

* added more rules

* applied clippy fixes

* added even more rules

* fixed clippy errors

* made all the required rules

* added some integer tests

* fixed clippy errors part 2

* finish up the last integer tests

* added float tests and some basic string tests

* quoted string literals

* `bool` operation tests done

* operation and maps

* fixed conflicts

# Conflicts:
#	pumpkin-nbt/src/tag.rs

* fixed clippy errors

* added message to `todo!()`

* fixed `indexmap` dependency for `pumpkin-codegen`

* fixed `typos` errors

* removed usage of `peek_byte()` for consistency

* removed useless macro

* moved some parser functions into a trait

* new translation changes
This commit is contained in:
Laptop59
2026-05-05 14:11:27 +05:30
committed by GitHub
parent b44ba0fbce
commit 04874d4420
24 changed files with 2401 additions and 123 deletions

1
Cargo.lock generated
View File

@@ -2805,6 +2805,7 @@ dependencies = [
"num-bigint",
"ordered-float",
"postcard",
"pumpkin-codecs",
"pumpkin-config",
"pumpkin-data",
"pumpkin-inventory",

View File

@@ -145,41 +145,10 @@ macro_rules! stream_struct {
};
}
stream_struct!(ByteBuffer, i8, create_byte_list, get_byte_list);
stream_struct!(IntStream, i32, create_int_list, get_int_list);
stream_struct!(LongStream, i64, create_long_list, get_long_list);
/// A [`Box<[u8]>`] wrapper that has built-in DFU support for encoding and decoding.
#[derive(Debug, Clone)]
pub struct ByteBuffer(pub Box<[u8]>);
impl From<Box<[u8]>> for ByteBuffer {
fn from(value: Box<[u8]>) -> Self {
Self(value)
}
}
impl<const N: usize> From<[u8; N]> for ByteBuffer {
fn from(value: [u8; N]) -> Self {
Self(Box::from(value))
}
}
impl From<ByteBuffer> for Box<[u8]> {
fn from(value: ByteBuffer) -> Self {
value.0
}
}
impl Primitive for ByteBuffer {
fn primitive_encode<O: DynamicOps>(&self, ops: &'static O) -> O::Value {
ops.create_byte_buffer(self.0.to_vec())
}
fn primitive_decode<O: DynamicOps>(ops: &'static O, input: O::Value) -> DataResult<Self> {
ops.get_byte_buffer(input).map(From::from)
}
}
#[cfg(test)]
mod test {
use crate::json_ops::JsonOps;
@@ -193,7 +162,7 @@ mod test {
assert_encode_success!(-913813743, JsonOps, json!(-913813743));
assert_encode_success!("Hello, world!".to_string(), JsonOps, json!("Hello, world!"));
assert_encode_success!(String::new(), JsonOps, json!(""));
assert_encode_success!(ByteBuffer::from([1u8, 2u8, 3u8]), JsonOps, json!([1, 2, 3]));
assert_encode_success!(ByteBuffer::from(vec![1, 2, 3]), JsonOps, json!([1, 2, 3]));
assert_encode_success!(
IntStream::from(vec![3, 6, 9, 11, 15]),
JsonOps,

View File

@@ -27,17 +27,6 @@ macro_rules! create_number_impl {
/// - [`DynamicOps::get_long_list`]
#[macro_export]
macro_rules! impl_get_list {
(box $target:expr, $input:expr, $ty:literal) => {
$target.get_iter($input).flat_map(|iter| {
// We want all elements in the iterator to be numbers.
iter.map(|e| $target.get_number(&e).into_result().map(Into::into))
.collect::<Option<Vec<_>>>()
.map_or_else(
|| DataResult::new_error(concat!("Some elements are not ", $ty)),
|v| DataResult::new_success(v.into_boxed_slice()),
)
})
};
($target:expr, $input:expr, $ty:literal) => {
$target.get_iter($input).flat_map(|iter| {
// We want all elements in the iterator to be numbers.
@@ -128,13 +117,13 @@ pub trait DynamicOps {
/// Gets a `Box<[u8]>` (byte buffer) from a generic value represented by this `DynamicOps`.
/// This is the equivalent of DFU's `getByteBuffer()` function.
fn get_byte_buffer(&self, input: Self::Value) -> DataResult<Box<[u8]>> {
impl_get_list!(box self, input, "bytes")
fn get_byte_list(&self, input: Self::Value) -> DataResult<Vec<i8>> {
impl_get_list!(self, input, "bytes")
}
/// Creates a byte buffer that can be represented by this `DynamicOps` using a [`Vec<u8>`].
fn create_byte_buffer(&self, buffer: Vec<u8>) -> Self::Value {
self.create_list(buffer.iter().map(|b| self.create_byte(*b as i8)))
fn create_byte_list(&self, vec: Vec<i8>) -> Self::Value {
self.create_list(vec.into_iter().map(|b| self.create_byte(b)))
}
/// Gets a [`Vec<i32>`] (`int` list) from a generic value represented by this `DynamicOps`.

View File

@@ -1,29 +1,44 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use crate::deserializer::NbtReadHelper;
use crate::serializer::WriteAdaptor;
use crate::tag::NbtTag;
use crate::{END_ID, Error, Nbt, get_nbt_string};
use std::collections::hash_map::IntoIter;
use std::io::{ErrorKind, Read, Seek, Write};
use std::vec::IntoIter;
/// Represents a Compound NBT tag, effectively a Key-Value map.
#[macro_export]
macro_rules! nbt_compound_tag {
{ $($key:literal : $tag:expr),+ $(,)* } => {
{
let mut compound = NbtCompound::new();
$( compound.put($key, $tag); )+
NbtTag::Compound(compound)
}
};
// For empty compounds
{} => {
NbtTag::Compound(NbtCompound::new())
};
}
/// Represents a Compound NBT tag, effectively a hash map.
///
/// Internally, this uses a `Vec<(String, NbtTag)>` to preserve insertion order,
/// which is often preferred in NBT serialization, though lookups are O(n).
/// Internally, this uses a `HashMap<String, NbtTag>`, which does not preserve insertion order,
/// just like Minecraft: Java Edition, but it does mean lookups are O(1).
///
///
#[derive(Clone, Debug, Default, PartialEq, PartialOrd)]
#[derive(Clone, Debug, Default, PartialEq)]
pub struct NbtCompound {
pub child_tags: Vec<(String, NbtTag)>,
pub child_tags: HashMap<String, NbtTag>,
}
impl NbtCompound {
#[must_use]
pub const fn new() -> Self {
pub fn new() -> Self {
Self {
child_tags: Vec::new(),
child_tags: HashMap::new(),
}
}
@@ -68,7 +83,7 @@ impl NbtCompound {
let name = get_nbt_string(reader)?;
let tag = NbtTag::deserialize_data(reader, tag_id)?;
compound.child_tags.push((name, tag));
compound.child_tags.insert(name, tag);
}
Ok(compound)
@@ -85,13 +100,13 @@ impl NbtCompound {
}
#[must_use]
pub const fn is_empty(&self) -> bool {
pub fn is_empty(&self) -> bool {
self.child_tags.is_empty()
}
pub fn put(&mut self, name: &str, value: impl Into<NbtTag>) {
if !self.child_tags.iter().any(|(key, _)| key == name) {
self.child_tags.push((name.to_string(), value.into()));
self.child_tags.insert(name.to_string(), value.into());
}
}
@@ -142,10 +157,13 @@ impl NbtCompound {
#[inline]
#[must_use]
pub fn get(&self, name: &str) -> Option<&NbtTag> {
self.child_tags
.iter()
.find(|k| k.0.as_str() == name)
.map(|r| &r.1)
self.child_tags.get(name)
}
#[inline]
#[must_use]
pub fn has(&self, name: &str) -> bool {
self.child_tags.contains_key(name)
}
#[must_use]
@@ -222,7 +240,7 @@ impl FromIterator<(String, NbtTag)> for NbtCompound {
impl IntoIterator for NbtCompound {
type Item = (String, NbtTag);
type IntoIter = IntoIter<(String, NbtTag)>;
type IntoIter = IntoIter<String, NbtTag>;
fn into_iter(self) -> Self::IntoIter {
self.child_tags.into_iter()

View File

@@ -72,7 +72,7 @@ impl de::Error for Error {
}
}
#[derive(Clone, Debug, Default, PartialEq, PartialOrd)]
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Nbt {
pub name: String,
pub root_tag: NbtCompound,

View File

@@ -151,7 +151,7 @@ impl DynamicOps for NbtOps {
}
NbtTag::ByteArray(b) => DataResult::new_success(NbtIter::ByteArray(
b.into_iter().map(|b| Self.create_byte(b as i8)),
b.into_iter().map(|b| Self.create_byte(b)),
)),
NbtTag::IntArray(i) => DataResult::new_success(NbtIter::IntArray(
i.into_iter().map(|i| Self.create_int(i)),
@@ -164,16 +164,16 @@ impl DynamicOps for NbtOps {
}
}
fn get_byte_buffer(&self, input: Self::Value) -> DataResult<Box<[u8]>> {
fn get_byte_list(&self, input: Self::Value) -> DataResult<Vec<i8>> {
if let NbtTag::ByteArray(b) = input {
DataResult::new_success(b)
} else {
impl_get_list!(box self, input, "bytes")
impl_get_list!(self, input, "bytes")
}
}
fn create_byte_buffer(&self, buffer: Vec<u8>) -> Self::Value {
NbtTag::ByteArray(buffer.into_boxed_slice())
fn create_byte_list(&self, buffer: Vec<i8>) -> Self::Value {
NbtTag::ByteArray(buffer)
}
fn get_int_list(&self, input: Self::Value) -> DataResult<Vec<i32>> {
@@ -301,7 +301,7 @@ impl DynamicOps for NbtOps {
NbtTag::Long(l) => out_ops.create_long(l),
NbtTag::Float(f) => out_ops.create_float(f),
NbtTag::Double(d) => out_ops.create_double(d),
NbtTag::ByteArray(b) => out_ops.create_byte_buffer(b.to_vec()),
NbtTag::ByteArray(b) => out_ops.create_byte_list(b),
NbtTag::String(s) => out_ops.create_string(&s),
NbtTag::List(_) => self.convert_list(out_ops, input),
NbtTag::Compound(_) => self.convert_map(out_ops, input),
@@ -326,11 +326,9 @@ impl NbtOps {
/// If `compound` only has one element with an empty key (`""`), it returns that element.
/// Otherwise, this simply returns a new [`NbtTag::Compound`] with `compound`.
fn try_unwrap(mut compound: NbtCompound) -> NbtTag {
if compound.child_tags.len() == 1
&& let Some(_) = compound.get("")
{
if compound.child_tags.len() == 1 && compound.has("") {
// Remove the element to own the contained tag.
compound.child_tags.remove(0).1
compound.child_tags.remove("").unwrap()
} else {
NbtTag::from(compound)
}
@@ -341,7 +339,7 @@ impl NbtOps {
enum NbtIter {
List(IntoIter<NbtTag>),
CompoundList(Map<IntoIter<NbtTag>, fn(NbtTag) -> NbtTag>),
ByteArray(Map<IntoIter<u8>, fn(u8) -> NbtTag>),
ByteArray(Map<IntoIter<i8>, fn(i8) -> NbtTag>),
IntArray(Map<IntoIter<i32>, fn(i32) -> NbtTag>),
LongArray(Map<IntoIter<i64>, fn(i64) -> NbtTag>),
}
@@ -595,21 +593,13 @@ impl InnerListCollector for InnerByteListCollector {
}
fn result(self) -> NbtTag {
NbtTag::ByteArray(
self.list
.into_iter()
.map(|i| i as u8)
.collect::<Vec<_>>()
.into_boxed_slice(),
)
NbtTag::ByteArray(self.list)
}
}
impl InnerByteListCollector {
fn new(list: Box<[u8]>) -> Self {
Self {
list: list.into_iter().map(|i| i as i8).collect(),
}
const fn new(list: Vec<i8>) -> Self {
Self { list }
}
}
@@ -680,13 +670,13 @@ mod test {
);
// Byte list collector
let tag = NbtTag::ByteArray(Box::new([255, 45, 100]));
let tag = NbtTag::ByteArray(vec![-1, 45, 100]);
assert_eq!(
ListCollector::new(tag)
.expect("List collector should exist")
.result(),
NbtTag::ByteArray(Box::new([255, 45, 100]))
NbtTag::ByteArray(vec![-1, 45, 100])
);
// Long list

View File

@@ -10,7 +10,7 @@ use crate::{
get_nbt_string, io, nbt_byte_array, nbt_int_array, nbt_long_array, serializer,
};
#[derive(Clone, Debug, PartialEq, PartialOrd)]
#[derive(Clone, Debug, PartialEq)]
#[repr(u8)]
pub enum NbtTag {
End = END_ID,
@@ -20,7 +20,7 @@ pub enum NbtTag {
Long(i64) = LONG_ID,
Float(f32) = FLOAT_ID,
Double(f64) = DOUBLE_ID,
ByteArray(Box<[u8]>) = BYTE_ARRAY_ID,
ByteArray(Vec<i8>) = BYTE_ARRAY_ID,
String(String) = STRING_ID,
List(Vec<Self>) = LIST_ID,
Compound(NbtCompound) = COMPOUND_ID,
@@ -80,7 +80,7 @@ impl NbtTag {
if let Self::Compound(mut compound) = tag {
// Try to get the wrapped tag, stored by "".
if Self::is_wrapper_compound(&compound) {
compound.child_tags.remove(0).1
compound.child_tags.remove("").unwrap()
} else {
Self::Compound(compound)
}
@@ -94,7 +94,7 @@ impl NbtTag {
/// A *wrapper compound* is a compound that stores exactly one
/// key-value pair, an empty string key (`""`) and an `NbtTag`.
fn is_wrapper_compound(compound: &NbtCompound) -> bool {
compound.child_tags.len() == 1 && compound.child_tags[0].0.is_empty()
compound.child_tags.len() == 1 && compound.child_tags.contains_key("")
}
/// Wraps the provided tag if needed with the provided element type of list
@@ -135,7 +135,9 @@ impl NbtTag {
}
w.write_i32_be(len as i32)?;
w.write_slice(&byte_array)?;
for int in byte_array {
w.write_i8_be(int)?;
}
}
Self::String(string) => {
Self::write_string(&string, w)?;
@@ -283,7 +285,12 @@ impl NbtTag {
return Err(Error::NegativeLength(len));
}
let byte_array = reader.read_boxed_slice(len as usize)?;
let len = len as usize;
let mut byte_array = Vec::with_capacity(len);
for _ in 0..len {
let byte = reader.get_i8_be()?;
byte_array.push(byte);
}
Ok(Self::ByteArray(byte_array))
}
STRING_ID => Ok(Self::String(get_nbt_string(reader)?)),
@@ -393,7 +400,7 @@ impl NbtTag {
}
#[must_use]
pub fn extract_byte_array(&self) -> Option<&[u8]> {
pub fn extract_byte_array(&self) -> Option<&[i8]> {
match self {
Self::ByteArray(byte_array) => Some(byte_array),
_ => None,
@@ -447,9 +454,9 @@ impl From<&str> for NbtTag {
}
}
impl From<&[u8]> for NbtTag {
fn from(value: &[u8]) -> Self {
Self::ByteArray(value.into())
impl From<&[i8]> for NbtTag {
fn from(value: &[i8]) -> Self {
Self::ByteArray(value.to_vec())
}
}
@@ -567,7 +574,7 @@ impl<'de> Deserialize<'de> for NbtTag {
while let Some(value) = seq.next_element()? {
vec.push(value);
}
Ok(NbtTag::ByteArray(vec.into_boxed_slice()))
Ok(NbtTag::ByteArray(vec))
}
_ => {
let mut vec = Vec::new();

View File

@@ -106,7 +106,7 @@ pub fn place_template(
if key != "x" && key != "y" && key != "z" && key != "id" {
block_entity_nbt
.child_tags
.push((key.clone(), value.clone()));
.insert(key.clone(), value.clone());
}
}
}

View File

@@ -28,6 +28,7 @@ pumpkin-world.workspace = true
pumpkin-data.workspace = true
pumpkin-protocol.workspace = true
pumpkin-macros.workspace = true
pumpkin-codecs.workspace = true
dashmap.workspace = true
crossbeam.workspace = true

View File

@@ -23,7 +23,7 @@ impl ArgumentType for StringArgumentType {
fn parse(&self, reader: &mut StringReader) -> Result<String, CommandSyntaxError> {
match self {
Self::SingleWord => reader.read_unquoted_string(),
Self::SingleWord => Ok(reader.read_unquoted_string()),
Self::QuotablePhrase => reader.read_string(),
Self::GreedyPhrase => {
let text = reader.remaining_part().to_owned();

View File

@@ -20,7 +20,7 @@ impl ArgumentType for EntityAnchorArgumentType {
fn parse(&self, reader: &mut StringReader) -> Result<Self::Item, CommandSyntaxError> {
let i = reader.cursor();
let anchor = reader.read_unquoted_string()?;
let anchor = reader.read_unquoted_string();
EntityAnchor::from_id(anchor.as_str()).map_or_else(
|| {
reader.set_cursor(i);

View File

@@ -207,7 +207,7 @@ impl EntitySelectorOption {
}
}
Self::Sort => {
let string = parser.reader.read_unquoted_string()?;
let string = parser.reader.read_unquoted_string();
parser.order = match string.as_str() {
"nearest" => Ok(Order::Nearest),
"furthest" => Ok(Order::Furthest),
@@ -227,7 +227,7 @@ impl EntitySelectorOption {
if parser.has_flag(Flags::GAMEMODE_NOT_EQUALS_SET) && !invert {
return Err(self.inapplicable_error(parser.reader));
}
let string = parser.reader.read_unquoted_string()?;
let string = parser.reader.read_unquoted_string();
if let Ok(gamemode) = GameMode::from_str(&string) {
parser.set_includes_entities(false);
parser.add_predicate(EntitySelectorPredicate::GameMode(gamemode, invert));

View File

@@ -47,7 +47,7 @@ impl ArgumentType for TimeArgumentType {
fn parse(&self, reader: &mut StringReader) -> Result<Self::Item, CommandSyntaxError> {
let value = reader.read_float()?;
let unit = reader.read_unquoted_string()?;
let unit = reader.read_unquoted_string();
// Find our unit's translation to ticks.
let ticks_per_unit = match unit.as_str() {
"t" | "" => 1,

View File

@@ -33,6 +33,8 @@ pub mod context;
pub mod dispatcher;
pub mod errors;
pub mod node;
pub mod parser;
pub mod snbt;
pub mod string_reader;
pub mod suggestion;
pub mod tree;

View File

@@ -0,0 +1,179 @@
use std::borrow::Cow;
use crate::command::{
errors::error_types::{AnyCommandErrorType, CommandErrorType},
string_reader::StringReader,
};
/// A delayed version of [`CommandSyntaxError`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DelayedCommandSyntaxError {
pub error_type: &'static dyn AnyCommandErrorType,
pub java_translation_key: &'static str,
pub bedrock_translation_key: &'static str,
pub arguments: Vec<Cow<'static, str>>,
}
#[derive(Debug, Default)]
pub struct ParserErrors {
pub cursor: usize,
pub command_error: Option<DelayedCommandSyntaxError>,
pub suggestions: Vec<Cow<'static, str>>,
}
/// A trait so that a parser specializing
/// to keep track of errors don't need to keep track
/// of suggestions, and vice versa.
impl ParserErrors {
pub fn simple_static(
&mut self,
reader: &StringReader,
error_type: &'static CommandErrorType<0>,
suggestions: &[&'static str],
) {
self.store(
reader,
|| DelayedCommandSyntaxError {
error_type,
java_translation_key: error_type.java_translation_key,
bedrock_translation_key: error_type.bedrock_translation_key,
arguments: vec![],
},
|entries| {
for suggestion in suggestions {
entries.push(Cow::Borrowed(*suggestion));
}
},
);
}
pub fn dynamic_static<A: Into<Cow<'static, str>>>(
&mut self,
reader: &StringReader,
error_type: &'static CommandErrorType<1>,
arg1: A,
suggestions: &[&'static str],
) {
self.store(
reader,
|| DelayedCommandSyntaxError {
error_type,
java_translation_key: error_type.java_translation_key,
bedrock_translation_key: error_type.bedrock_translation_key,
arguments: vec![arg1.into()],
},
|entries| {
for suggestion in suggestions {
entries.push(Cow::Borrowed(*suggestion));
}
},
);
}
pub fn simple(
&mut self,
reader: &StringReader,
error_type: &'static CommandErrorType<0>,
suggestions: Vec<String>,
) {
self.store(
reader,
|| DelayedCommandSyntaxError {
error_type,
java_translation_key: error_type.java_translation_key,
bedrock_translation_key: error_type.bedrock_translation_key,
arguments: vec![],
},
|entries| {
for suggestion in suggestions {
entries.push(Cow::Owned(suggestion));
}
},
);
}
pub fn dynamic<A: Into<Cow<'static, str>>>(
&mut self,
reader: &StringReader,
error_type: &'static CommandErrorType<1>,
arg1: A,
suggestions: Vec<String>,
) {
self.store(
reader,
|| DelayedCommandSyntaxError {
error_type,
java_translation_key: error_type.java_translation_key,
bedrock_translation_key: error_type.bedrock_translation_key,
arguments: vec![arg1.into()],
},
|entries| {
for suggestion in suggestions {
entries.push(Cow::Owned(suggestion));
}
},
);
}
#[inline]
fn store(
&mut self,
reader: &StringReader,
error: impl FnOnce() -> DelayedCommandSyntaxError,
suggestions: impl FnOnce(&mut Vec<Cow<'static, str>>),
) {
let current = self.cursor;
let new = reader.cursor();
if self.command_error.is_none() || new > current {
self.command_error = Some(error());
self.cursor = new;
self.suggestions.clear();
suggestions(&mut self.suggestions);
} else if new == current {
suggestions(&mut self.suggestions);
}
}
}
/// A trait that rule-like parsers like `SnbtParser` implement.
pub trait Parser<'r, 's> {
fn state_mut(&mut self) -> (&mut StringReader<'s>, &mut ParserErrors);
/// Records that a simple error occurred while parsing, and adds suggestions to counteract it.
fn store_simple_error_and_suggest(
&mut self,
error_type: &'static CommandErrorType<0>,
suggestions: &[&'static str],
) {
let (reader, errors) = self.state_mut();
errors.simple_static(reader, error_type, suggestions);
}
/// Records that a dynamic error occurred while parsing, and adds suggestions to counteract it.
fn store_dynamic_error_and_suggest(
&mut self,
error_type: &'static CommandErrorType<1>,
arg1: impl Into<Cow<'static, str>>,
suggestions: &[&'static str],
) {
let (reader, errors) = self.state_mut();
errors.dynamic_static(reader, error_type, arg1, suggestions);
}
/// Records that a simple error occurred while parsing.
fn store_simple_error(&mut self, error_type: &'static CommandErrorType<0>) {
let (reader, errors) = self.state_mut();
errors.simple(reader, error_type, Vec::new());
}
/// Records that a dynamic error occurred while parsing.
fn store_dynamic_error(
&mut self,
error_type: &'static CommandErrorType<1>,
arg1: impl Into<Cow<'static, str>>,
) {
let (reader, errors) = self.state_mut();
errors.dynamic(reader, error_type, arg1, Vec::new());
}
}

View File

@@ -0,0 +1,136 @@
use crate::command::{
errors::error_types::CommandErrorType,
snbt::rules::{EXPECTED_BINARY_NUMERAL, EXPECTED_DECIMAL_NUMERAL, EXPECTED_HEX_NUMERAL},
};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Sign {
Plus = 0,
Minus = 1,
}
impl Sign {
/// Returns the minimum number of characters required to express this sign to be parsed.
///
/// For example, between `5.0` and `+5.0`, the former takes no space for the `+` symbol,
/// while for `-5.0`, there must be a `-` symbol, so the minimum size there is `1` instead of `0`.
#[must_use]
#[inline]
pub const fn minimum_size_parsable(self) -> usize {
self as usize
}
/// Appends the slice containing the minimum characters required to express this sign to be parsed to a String
/// referred by the given mutable reference.
///
/// This is a no-op for [`Sign::Plus`], while for [`Sign::Minus`], a `-` is appended.
#[inline]
pub fn append_minimum_str_parsable(self, buffer: &mut String) {
if self == Self::Minus {
buffer.push('-');
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum SignedPrefix {
None,
Unsigned,
Signed,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum TypeSuffix {
None,
Byte,
Short,
Int,
Long,
Float,
Double,
}
impl TypeSuffix {
#[must_use]
/// Returns the `default` suffix if this suffix is [`TypeSuffix::None`], otherwise
/// it returns itself.
pub fn or(self, default: Self) -> Self {
if self == Self::None { default } else { self }
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct IntegerSuffix(pub SignedPrefix, pub TypeSuffix);
impl IntegerSuffix {
pub const EMPTY: Self = Self(SignedPrefix::None, TypeSuffix::None);
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Base {
Binary,
Decimal,
Hexadecimal,
}
impl Base {
#[must_use]
pub const fn should_allow(self, c: char) -> bool {
matches!(
(self, c),
(_, '_') |
(Self::Binary, '0' | '1') |
(Self::Decimal, '0'..='9') |
(Self::Hexadecimal, '0'..='9' | 'A'..='F' | 'a'..='f')
)
}
#[must_use]
pub const fn no_value_error_type(self) -> &'static CommandErrorType<0> {
match self {
Self::Binary => &EXPECTED_BINARY_NUMERAL,
Self::Decimal => &EXPECTED_DECIMAL_NUMERAL,
Self::Hexadecimal => &EXPECTED_HEX_NUMERAL,
}
}
#[must_use]
pub const fn radix(self) -> u32 {
match self {
Self::Binary => 2,
Self::Decimal => 10,
Self::Hexadecimal => 16,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IntegerLiteral {
pub sign: Sign,
pub base: Base,
pub digits: String,
pub suffix: IntegerSuffix,
}
impl IntegerLiteral {
pub const fn get_signed_prefix_or_default(&self) -> SignedPrefix {
match (self.suffix.0, self.base) {
(SignedPrefix::None, Base::Binary | Base::Hexadecimal) => SignedPrefix::Unsigned,
(SignedPrefix::None, Base::Decimal) => SignedPrefix::Signed,
(prefix, _) => prefix,
}
}
}
pub struct Signed<T> {
pub sign: Sign,
pub value: T,
}
/// Represents a explicit prefix set for an array.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ArrayPrefix {
Byte,
Long,
Int,
}

View File

@@ -0,0 +1,478 @@
mod markers;
mod operations;
mod rules;
#[cfg(test)]
mod tests;
use crate::command::errors::command_syntax_error::{CommandSyntaxError, CommandSyntaxErrorContext};
use crate::command::errors::error_types::{CommandErrorType, LITERAL_INCORRECT};
use crate::command::parser::{Parser, ParserErrors};
use crate::command::snbt::markers::{
ArrayPrefix, Base, IntegerLiteral, Sign, SignedPrefix, TypeSuffix,
};
use crate::command::string_reader::StringReader;
use crate::command::suggestion::suggestions::{Suggestions, SuggestionsBuilder};
use pumpkin_codecs::Number;
use pumpkin_data::translation;
use pumpkin_nbt::tag::NbtTag;
use pumpkin_util::text::TextComponent;
pub const NUMBER_PARSE_FAILURE: CommandErrorType<1> = CommandErrorType::new(
translation::java::SNBT_PARSER_NUMBER_PARSE_FAILURE,
translation::java::SNBT_PARSER_NUMBER_PARSE_FAILURE,
);
pub const UNDERSCORE_NOT_ALLOWED: CommandErrorType<0> = CommandErrorType::new(
translation::java::SNBT_PARSER_UNDESCORE_NOT_ALLOWED,
translation::java::SNBT_PARSER_UNDESCORE_NOT_ALLOWED,
);
pub const EXPECTED_HEX_ESCAPE: CommandErrorType<1> = CommandErrorType::new(
translation::java::SNBT_PARSER_EXPECTED_HEX_ESCAPE,
translation::java::SNBT_PARSER_EXPECTED_HEX_ESCAPE,
);
pub const EXPECTED_NON_NEGATIVE_NUMBER: CommandErrorType<0> = CommandErrorType::new(
translation::java::SNBT_PARSER_EXPECTED_NON_NEGATIVE_NUMBER,
translation::java::SNBT_PARSER_EXPECTED_NON_NEGATIVE_NUMBER,
);
pub const INVALID_ARRAY_ELEMENT_TYPE: CommandErrorType<0> = CommandErrorType::new(
translation::java::SNBT_PARSER_INVALID_ARRAY_ELEMENT_TYPE,
translation::java::SNBT_PARSER_INVALID_ARRAY_ELEMENT_TYPE,
);
pub const EXPECTED_INTEGER_TYPE: CommandErrorType<0> = CommandErrorType::new(
translation::java::SNBT_PARSER_EXPECTED_INTEGER_TYPE,
translation::java::SNBT_PARSER_EXPECTED_INTEGER_TYPE,
);
/// A structure that parses SNBT.
///
/// This stores a reader and gives the furthest error, or suggestions
/// to fix errors that have ever occurred while parsing.
pub struct SnbtParser<'r, 's> {
reader: &'r mut StringReader<'s>,
errors: ParserErrors,
}
//
// USAGE
//
impl SnbtParser<'_, '_> {
/// Parses SNBT with a given [`StringReader`], giving the result or error from parsing.
pub fn parse_for_commands(reader: &mut StringReader) -> Result<NbtTag, CommandSyntaxError> {
let (result, errors) = {
let mut parser = SnbtParser {
reader,
errors: ParserErrors::default(),
};
let literal = parser.parse();
let errors = parser.errors;
(literal, errors)
};
result.ok_or_else(|| {
if let Some(error) = errors.command_error {
CommandSyntaxError {
error_type: error.error_type,
message: TextComponent::translate_cross(
error.java_translation_key,
error.bedrock_translation_key,
error
.arguments
.into_iter()
.map(TextComponent::text)
.collect::<Vec<_>>(),
),
context: Some(CommandSyntaxErrorContext { input: reader.string().to_string(), cursor: errors.cursor }),
}
} else {
// This shouldn't happen... If it didn't parse successfully, there should be an error to supplement it.
// Hacky way to report an error:
const PARSING_FAILED_WITHOUT_ERRORS: CommandErrorType<0> = CommandErrorType::new(
translation::java::COMMAND_FAILED,
translation::java::COMMAND_FAILED
);
tracing::error!("Failed to parse SNBT, while having zero errors to report (report this to Pumpkin): {}", reader.string());
PARSING_FAILED_WITHOUT_ERRORS.create(reader)
}
})
}
// Parses SNBT with a given [`StringReader`], giving the suggestions to fix errors from parsing.
pub fn parse_for_suggestions(
reader: &mut StringReader,
mut builder: SuggestionsBuilder,
) -> Suggestions {
let mut parser = SnbtParser {
reader,
errors: ParserErrors::default(),
};
let _ = parser.parse();
if !parser.errors.suggestions.is_empty() {
builder = builder.create_offset(parser.errors.cursor);
for suggestion in &parser.errors.suggestions {
builder = builder.suggest(suggestion.to_string());
}
}
builder.build()
}
}
//
// HELPER FUNCTIONS
//
impl SnbtParser<'_, '_> {
/// Utility method that parses a type suffix of an integer.
fn integer_type_suffix(&mut self) -> Option<TypeSuffix> {
self.reader.skip_whitespace();
match self.reader.peek() {
Some('b' | 'B') => {
self.reader.skip();
Some(TypeSuffix::Byte)
}
Some('s' | 'S') => {
self.reader.skip();
Some(TypeSuffix::Short)
}
Some('i' | 'I') => {
self.reader.skip();
Some(TypeSuffix::Int)
}
Some('l' | 'L') => {
self.reader.skip();
Some(TypeSuffix::Long)
}
_ => {
// Only b|B is given as the error, being the first errored choice.
self.store_dynamic_error_and_suggest(
&LITERAL_INCORRECT,
"b|B",
&["b", "B", "s", "S", "i", "I", "l", "L"],
);
None
}
}
}
/// General method that parses an integer of a specific base.
fn parse_numeral(&mut self, base: Base) -> Option<String> {
self.parse_or_revert(|parser| {
parser.reader.skip_whitespace();
let slice = parser.reader.string();
let start = parser.reader.cursor();
let mut end = start;
for (i, c) in slice[start..].char_indices() {
if !base.should_allow(c) {
break;
}
end = start + i + c.len_utf8();
}
if start == end {
parser.store_simple_error(base.no_value_error_type());
None
} else if slice.as_bytes()[start] == b'_' || slice.as_bytes()[end - 1] == b'_' {
parser.store_simple_error(&UNDERSCORE_NOT_ALLOWED);
None
} else {
parser.reader.set_cursor(end);
Some(parser.reader.string()[start..end].to_string())
}
})
}
/// Parses a value, and if unsuccessful, reverts back to what the state initially was.
#[inline]
fn parse_or_revert<T>(&mut self, closure: impl FnOnce(&mut Self) -> Option<T>) -> Option<T> {
let start = self.reader.cursor();
let result = closure(self);
if result.is_none() {
self.reader.set_cursor(start);
}
result
}
/// Appends every character given in the `reference` slice except `_` in the provided `buffer`.
fn clean_and_append(buffer: &mut String, reference: &str) {
// This could really be optimized further
// with bytes instead of chars, but that
// probably requires unsafe code. Is that worth it?
// TODO
for c in reference.chars() {
if c != '_' {
buffer.push(c);
}
}
}
/// General method to parse a specific number of hexadecimal digits greedily (no underscores are allowed).
fn hex_literal(&mut self, digits: usize) -> Option<String> {
self.parse_or_revert(|parser| {
parser.reader.skip_whitespace();
let slice = parser.reader.string();
let start = parser.reader.cursor();
let mut end = start;
for (count, (i, c)) in slice[start..].char_indices().enumerate() {
if count == digits || !c.is_ascii_hexdigit() {
break;
}
end = start + i + c.len_utf8();
}
if end - start < digits {
parser.store_dynamic_error(&EXPECTED_HEX_ESCAPE, digits.to_string());
None
} else {
parser.reader.set_cursor(end);
Some(parser.reader.string()[start..end].to_string())
}
})
}
fn repeated_with_trailing_comma<T, S>(
&mut self,
rule: impl Fn(&mut Self) -> Option<T>,
new: S,
insert: impl Fn(&mut S, T),
) -> S {
let mut elements = new;
let mut first = true;
loop {
if !first {
let parse_comma = self.parse_or_revert(|parser| {
parser.reader.skip_whitespace();
if parser.reader.peek() == Some(',') {
parser.reader.skip();
Some(())
} else {
parser.store_dynamic_error_and_suggest(&LITERAL_INCORRECT, ",", &[","]);
None
}
});
if parse_comma.is_none() {
break;
}
}
if let Some(parsed) = self.parse_or_revert(&rule) {
insert(&mut elements, parsed);
} else {
break;
}
first = false;
}
elements
}
fn repeated_with_trailing_comma_vec<T>(
&mut self,
rule: impl Fn(&mut Self) -> Option<T>,
) -> Vec<T> {
self.repeated_with_trailing_comma(rule, Vec::new(), Vec::push)
}
fn parse_integer_literal(
&mut self,
literal: &IntegerLiteral,
suffix: TypeSuffix,
) -> Option<Number> {
let signed = literal.get_signed_prefix_or_default() == SignedPrefix::Signed;
if !signed && literal.sign == Sign::Minus {
self.store_simple_error(&EXPECTED_NON_NEGATIVE_NUMBER);
return None;
}
let mut number =
String::with_capacity(literal.digits.len() + literal.sign.minimum_size_parsable());
literal.sign.append_minimum_str_parsable(&mut number);
Self::clean_and_append(&mut number, &literal.digits);
let radix = literal.base.radix();
// The error messages vary by a lot to match the error messages in Java.
match (signed, suffix) {
(true, TypeSuffix::Byte) => {
let integer = self.parse_int_or_error(&number, radix)?;
integer.try_into().map_or_else(
|_| {
self.store_dynamic_error(
&NUMBER_PARSE_FAILURE,
format!("Value out of range. Value:\"{number}\" Radix:{radix}"),
);
None
},
|byte| Some(Number::Byte(byte)),
)
}
(true, TypeSuffix::Short) => {
let integer = self.parse_int_or_error(&number, radix)?;
integer.try_into().map_or_else(
|_| {
self.store_dynamic_error(
&NUMBER_PARSE_FAILURE,
format!("Value out of range. Value:\"{number}\" Radix:{radix}"),
);
None
},
|short| Some(Number::Short(short)),
)
}
(true, TypeSuffix::Int) => Some(Number::Int(self.parse_int_or_error(&number, radix)?)),
(true, TypeSuffix::Long) => i64::from_str_radix(&number, radix).map_or_else(
|_| {
self.store_dynamic_error(
&NUMBER_PARSE_FAILURE,
format!("For input string: \"{number}\""),
);
None
},
|long| Some(Number::Long(long)),
),
(false, TypeSuffix::Byte) => {
let integer = self.parse_int_or_error(&number, radix)?;
TryInto::<u8>::try_into(integer).map_or_else(
|_| {
self.store_dynamic_error(
&NUMBER_PARSE_FAILURE,
format!("out of range: {number}"),
);
None
},
|byte| Some(Number::Byte(byte as i8)),
)
}
(false, TypeSuffix::Short) => {
let integer = self.parse_int_or_error(&number, radix)?;
TryInto::<u16>::try_into(integer).map_or_else(
|_| {
self.store_dynamic_error(
&NUMBER_PARSE_FAILURE,
format!("out of range: {number}"),
);
None
},
|short| Some(Number::Short(short as i16)),
)
}
(false, TypeSuffix::Int) => u32::from_str_radix(&number, radix).map_or_else(
|_| {
self.store_dynamic_error(
&NUMBER_PARSE_FAILURE,
format!("String value {number} exceeds range of unsigned int."),
);
None
},
|int| Some(Number::Int(int as i32)),
),
(false, TypeSuffix::Long) => u64::from_str_radix(&number, radix).map_or_else(
|_| {
self.store_dynamic_error(
&NUMBER_PARSE_FAILURE,
format!("String value {number} exceeds range of unsigned long."),
);
None
},
|long| Some(Number::Long(long as i64)),
),
_ => {
self.store_simple_error(&EXPECTED_INTEGER_TYPE);
None
}
}
}
fn parse_int_or_error(&mut self, number: &str, radix: u32) -> Option<i32> {
i32::from_str_radix(number, radix).map_or_else(
|_| {
self.store_dynamic_error(
&NUMBER_PARSE_FAILURE,
format!("For input string: \"{number}\""),
);
None
},
Some,
)
}
fn create_prefixed_array(
&mut self,
values: &[IntegerLiteral],
prefix: ArrayPrefix,
) -> Option<NbtTag> {
match prefix {
ArrayPrefix::Byte => self.create_byte_array(values),
ArrayPrefix::Int => self.create_int_array(values),
ArrayPrefix::Long => self.create_long_array(values),
}
}
fn create_byte_array(&mut self, values: &[IntegerLiteral]) -> Option<NbtTag> {
let mut bytes = Vec::with_capacity(values.len());
for value in values {
if !matches!(value.suffix.1, TypeSuffix::None | TypeSuffix::Byte) {
self.store_simple_error(&INVALID_ARRAY_ELEMENT_TYPE);
return None;
}
bytes.push(self.parse_integer_literal(value, TypeSuffix::Byte)?.into());
}
Some(NbtTag::ByteArray(bytes))
}
fn create_int_array(&mut self, values: &[IntegerLiteral]) -> Option<NbtTag> {
let mut ints = Vec::with_capacity(values.len());
for value in values {
let suffix = value.suffix.1.or(TypeSuffix::Int);
if !matches!(
suffix,
TypeSuffix::Byte | TypeSuffix::Short | TypeSuffix::Int
) {
self.store_simple_error(&INVALID_ARRAY_ELEMENT_TYPE);
return None;
}
ints.push(self.parse_integer_literal(value, suffix)?.into());
}
Some(NbtTag::IntArray(ints))
}
fn create_long_array(&mut self, values: &[IntegerLiteral]) -> Option<NbtTag> {
let mut longs = Vec::with_capacity(values.len());
for value in values {
let suffix = value.suffix.1.or(TypeSuffix::Long);
if !matches!(
suffix,
TypeSuffix::Byte | TypeSuffix::Short | TypeSuffix::Int | TypeSuffix::Long
) {
self.store_simple_error(&INVALID_ARRAY_ELEMENT_TYPE);
return None;
}
longs.push(self.parse_integer_literal(value, suffix)?.into());
}
Some(NbtTag::LongArray(longs))
}
}
impl<'r, 's> Parser<'r, 's> for SnbtParser<'r, 's> {
fn state_mut(&mut self) -> (&mut StringReader<'s>, &mut ParserErrors) {
(self.reader, &mut self.errors)
}
}

View File

@@ -0,0 +1,137 @@
use pumpkin_data::translation;
use pumpkin_nbt::{nbt_ops::NbtOps, tag::NbtTag};
use crate::command::{errors::error_types::CommandErrorType, parser::Parser, snbt::SnbtParser};
use pumpkin_codecs::DynamicOps;
pub const EXPECTED_NUMBER_OR_BOOLEAN: CommandErrorType<0> = CommandErrorType::new(
translation::java::SNBT_PARSER_EXPECTED_NUMBER_OR_BOOLEAN,
translation::java::SNBT_PARSER_EXPECTED_NUMBER_OR_BOOLEAN,
);
pub const EXPECTED_STRING_UUID: CommandErrorType<0> = CommandErrorType::new(
translation::java::SNBT_PARSER_EXPECTED_STRING_UUID,
translation::java::SNBT_PARSER_EXPECTED_STRING_UUID,
);
/// Represents an *operation* that can take *operands* and return a required *result*.
pub type SnbtOperation = fn(parser: &mut SnbtParser, args: &[NbtTag]) -> Option<NbtTag>;
/// A manager for SNBT operations baked at compile-time.
pub struct SnbtOperations;
impl SnbtOperations {
pub const BUILTIN_IDS: &[&str] = &["true", "false", "bool", "uuid"];
/// Searches for an operation to be run from the
/// given identifier and argument count.
pub fn search(id: &str, arg_count: usize) -> Option<SnbtOperation> {
match (id, arg_count) {
("bool", 1) => Some(Self::bool),
("uuid", 1) => Some(Self::uuid),
_ => None,
}
}
/// Represents the `bool` unary operator in SNBT.
///
/// Acts like an identity operation for booleans,
/// and returns `true` for non-zero numbers.
fn bool(parser: &mut SnbtParser, args: &[NbtTag]) -> Option<NbtTag> {
NbtOps.get_bool(&args[0]).into_result().map_or_else(
|| {
parser.store_simple_error(&EXPECTED_NUMBER_OR_BOOLEAN);
None
},
|result| Some(NbtTag::Byte(result as i8)),
)
}
/// Represents the `uuid` unary operator in SNBT.
///
/// Parses a UUID in a string to an array of 4 integers.
fn uuid(parser: &mut SnbtParser, args: &[NbtTag]) -> Option<NbtTag> {
if let NbtTag::String(string) = &args[0]
&& let Some(ints) = Self::parse_uuid(string)
{
Some(NbtTag::IntArray(ints))
} else {
parser.store_simple_error(&EXPECTED_STRING_UUID);
None
}
}
}
impl SnbtOperations {
/// Parses UUIDs the 'Java' way.
#[inline]
#[must_use]
fn parse_uuid(uuid: &str) -> Option<Vec<i32>> {
// We can't directly use the uuid crate to parse UUIDs, as it parses them
// in a different way from Java.
if uuid.len() > 36 {
// UUID string is too large.
return None;
}
// Split by hyphen. (5 segments)
let mut parts = uuid.split('-');
let mut parsed_parts: [i64; 5] = [0; 5];
for part in &mut parsed_parts {
// If a part is empty, the parsing functions will error anyway - this is what we want.
*part = i64::from_str_radix(parts.next()?, 16).ok()?;
}
if parts.next().is_some() {
// UUIDs must have exactly 5 parts.
return None;
}
let bits = [
(parsed_parts[0] & 0xFFFFFFFF) << 32
| (parsed_parts[1] & 0xFFFF) << 16
| (parsed_parts[2] & 0xFFFF),
(parsed_parts[3] & 0xFFFF) << 48 | (parsed_parts[4] & 0xFFFFFFFFFFFF),
];
Some(vec![
(bits[0] >> 32) as i32,
bits[0] as i32,
(bits[1] >> 32) as i32,
bits[1] as i32,
])
}
}
#[cfg(test)]
mod test {
use crate::command::snbt::operations::SnbtOperations;
#[test]
fn parse_uuids() {
assert_eq!(
SnbtOperations::parse_uuid("3d569d3a-93ef-44a0-9f1c-f69db9d37a56"),
Some(vec![1029086522, -1813035872, -1625491811, -1177322922])
);
assert_eq!(
SnbtOperations::parse_uuid("3d53a-f-40-c-f69db9d37a56"),
Some(vec![251194, 983104, 849565, -1177322922])
);
assert_eq!(SnbtOperations::parse_uuid("3d53a-f40-c-f69db9d37a56"), None);
assert_eq!(
SnbtOperations::parse_uuid("fffffffffffffff-0-0-0-0"),
Some(vec![-1, 0, 0, 0])
);
assert_eq!(SnbtOperations::parse_uuid("ffffffffffffffff-0-0-0-0"), None);
assert_eq!(
SnbtOperations::parse_uuid("+1-+2-+3-+4-+5"),
Some(vec![1, 131075, 262144, 5])
);
assert_eq!(
SnbtOperations::parse_uuid("aaaaaaaaaaaaaaa-bbbbbbbbbbbbbb-c-d-e"),
Some(vec![-1431655766, -1145372660, 851968, 14])
);
}
}

View File

@@ -0,0 +1,854 @@
use std::collections::HashMap;
use crate::command::errors::error_types::{CommandErrorType, LITERAL_INCORRECT};
use crate::command::parser::Parser;
use crate::command::snbt::markers::{
ArrayPrefix, Base, IntegerLiteral, IntegerSuffix, Sign, Signed, SignedPrefix, TypeSuffix,
};
use crate::command::snbt::operations::SnbtOperations;
use crate::command::snbt::{NUMBER_PARSE_FAILURE, SnbtParser};
use pumpkin_codecs::{DynamicOps, Number};
use pumpkin_data::translation;
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_nbt::nbt_ops::NbtOps;
use pumpkin_nbt::tag::NbtTag;
pub const INVALID_CODEPOINT: CommandErrorType<1> = CommandErrorType::new(
translation::java::SNBT_PARSER_INVALID_CODEPOINT,
translation::java::SNBT_PARSER_INVALID_CODEPOINT,
);
pub const NO_SUCH_OPERATION: CommandErrorType<1> = CommandErrorType::new(
translation::java::SNBT_PARSER_NO_SUCH_OPERATION,
translation::java::SNBT_PARSER_NO_SUCH_OPERATION,
);
pub const EXPECTED_FLOAT_TYPE: CommandErrorType<0> = CommandErrorType::new(
translation::java::SNBT_PARSER_EXPECTED_FLOAT_TYPE,
translation::java::SNBT_PARSER_EXPECTED_FLOAT_TYPE,
);
pub const INVALID_CHARACTER_NAME: CommandErrorType<0> = CommandErrorType::new(
translation::java::SNBT_PARSER_INVALID_CHARACTER_NAME,
translation::java::SNBT_PARSER_INVALID_CHARACTER_NAME,
);
pub const INVALID_UNQUOTED_START: CommandErrorType<0> = CommandErrorType::new(
translation::java::SNBT_PARSER_INVALID_UNQUOTED_START,
translation::java::SNBT_PARSER_INVALID_UNQUOTED_START,
);
pub const EXPECTED_UNQUOTED_STRING: CommandErrorType<0> = CommandErrorType::new(
translation::java::SNBT_PARSER_EXPECTED_UNQUOTED_STRING,
translation::java::SNBT_PARSER_EXPECTED_UNQUOTED_STRING,
);
pub const INVALID_STRING_CONTENTS: CommandErrorType<0> = CommandErrorType::new(
translation::java::SNBT_PARSER_INVALID_STRING_CONTENTS,
translation::java::SNBT_PARSER_INVALID_STRING_CONTENTS,
);
pub const EXPECTED_BINARY_NUMERAL: CommandErrorType<0> = CommandErrorType::new(
translation::java::SNBT_PARSER_EXPECTED_BINARY_NUMERAL,
translation::java::SNBT_PARSER_EXPECTED_BINARY_NUMERAL,
);
pub const EXPECTED_DECIMAL_NUMERAL: CommandErrorType<0> = CommandErrorType::new(
translation::java::SNBT_PARSER_EXPECTED_DECIMAL_NUMERAL,
translation::java::SNBT_PARSER_EXPECTED_DECIMAL_NUMERAL,
);
pub const EXPECTED_HEX_NUMERAL: CommandErrorType<0> = CommandErrorType::new(
translation::java::SNBT_PARSER_EXPECTED_HEX_NUMERAL,
translation::java::SNBT_PARSER_EXPECTED_HEX_NUMERAL,
);
pub const EMPTY_KEY: CommandErrorType<0> = CommandErrorType::new(
translation::java::SNBT_PARSER_EMPTY_KEY,
translation::java::SNBT_PARSER_EMPTY_KEY,
);
pub const LEADING_ZERO_NOT_ALLOWED: CommandErrorType<0> = CommandErrorType::new(
translation::java::SNBT_PARSER_LEADING_ZERO_NOT_ALLOWED,
translation::java::SNBT_PARSER_LEADING_ZERO_NOT_ALLOWED,
);
pub const INFINITY_NOT_ALLOWED: CommandErrorType<0> = CommandErrorType::new(
translation::java::SNBT_PARSER_INFINITY_NOT_ALLOWED,
translation::java::SNBT_PARSER_INFINITY_NOT_ALLOWED,
);
/// Parses a string literal.
macro_rules! parse_string_literal {
($parser:expr, $quote:literal) => {{
let mut buffer = String::new();
let mut high_surrogate_queue: u32 = 0;
loop {
match $parser.reader.read() {
Some($quote) => break Some(buffer),
Some('\\') => {
let i = $parser.escape_sequence()?;
if let Some(c) = char::from_u32(i) {
buffer.push(c);
} else if high_surrogate_queue == 0 && matches!(i, 0xD800..=0xDBFF) {
// High surrogate incoming.
high_surrogate_queue = i;
} else if high_surrogate_queue != 0 && matches!(i, 0xDC00..=0xDFFF) {
// Low surrogate incoming.
let high_bits = high_surrogate_queue - 0xD800;
let low_bits = i - 0xDC00;
let bits = high_bits << 10 | low_bits;
let i = bits + 0x10000;
// This really shouldn't fail though.
if let Some(c) = char::from_u32(i) {
buffer.push(c);
} else {
buffer.push('\u{FFFD}');
}
high_surrogate_queue = 0;
} else {
// Add replacement character.
buffer.push('\u{FFFD}');
if high_surrogate_queue != 0 {
buffer.push('\u{FFFD}');
}
high_surrogate_queue = 0;
}
}
Some(ch) => {
if high_surrogate_queue != 0 {
// Add replacement character.
buffer.push('\u{FFFD}');
high_surrogate_queue = 0;
}
buffer.push(ch);
}
None => {
// reached EOL
$parser.store_simple_error_and_suggest(
&INVALID_STRING_CONTENTS,
&["'", "\"", "\\"],
);
break None;
}
}
}
}};
}
impl SnbtParser<'_, '_> {
fn sign(&mut self) -> Option<Sign> {
self.parse_or_revert(|parser| {
parser.reader.skip_whitespace();
match parser.reader.peek() {
Some('+') => {
parser.reader.skip();
Some(Sign::Plus)
}
Some('-') => {
parser.reader.skip();
Some(Sign::Minus)
}
_ => {
parser.store_dynamic_error_and_suggest(&LITERAL_INCORRECT, "+", &["+", "-"]);
None
}
}
})
}
fn integer_suffix(&mut self) -> Option<IntegerSuffix> {
self.parse_or_revert(|parser| {
parser.reader.skip_whitespace();
match parser.reader.peek() {
Some('u' | 'U') => {
parser.reader.skip();
Some(IntegerSuffix(
SignedPrefix::Unsigned,
parser.integer_type_suffix()?,
))
}
Some('s' | 'S') => {
// Can mean signed or short.
Some(
if let Some(suffix) = parser.parse_or_revert(|parser| {
parser.reader.skip();
parser.integer_type_suffix()
}) {
IntegerSuffix(SignedPrefix::Signed, suffix)
} else {
IntegerSuffix(SignedPrefix::None, parser.integer_type_suffix()?)
},
)
}
Some('b' | 'B' | 'i' | 'I' | 'l' | 'L') => Some(IntegerSuffix(
SignedPrefix::None,
parser.integer_type_suffix()?,
)),
_ => {
parser.store_dynamic_error_and_suggest(
&LITERAL_INCORRECT,
"u|U",
&["u", "U", "s", "S", "b", "B", "s", "S", "i", "I", "l", "L"],
);
None
}
}
})
}
fn binary_numeral(&mut self) -> Option<String> {
self.parse_numeral(Base::Binary)
}
fn decimal_numeral(&mut self) -> Option<String> {
self.parse_numeral(Base::Decimal)
}
fn hexadecimal_numeral(&mut self) -> Option<String> {
self.parse_numeral(Base::Hexadecimal)
}
/// Parses an integer literal.
fn integer_literal(&mut self) -> Option<IntegerLiteral> {
let mut result = self.parse_or_revert(|parser| {
let sign = parser.parse_or_revert(Self::sign).unwrap_or(Sign::Plus);
parser.reader.skip_whitespace();
// We need to be careful to make sure that
// `0b` parses as a byte literal and NOT a prefix.
let after_sign_cursor = parser.reader.cursor();
if parser.reader.peek() == Some('0') {
parser.reader.skip();
parser.reader.skip_whitespace();
match parser.reader.peek() {
Some('x' | 'X') => {
parser.reader.skip();
return parser.hexadecimal_numeral().map(|number| IntegerLiteral {
sign,
base: Base::Hexadecimal,
suffix: IntegerSuffix::EMPTY,
digits: number,
});
}
Some('b' | 'B') => {
parser.reader.skip();
if let Some(number) = parser.binary_numeral() {
return Some(IntegerLiteral {
sign,
base: Base::Binary,
suffix: IntegerSuffix::EMPTY,
digits: number,
});
}
parser.reader.set_cursor(after_sign_cursor);
}
_ => {
return if parser.decimal_numeral().is_none() {
Some(IntegerLiteral {
sign,
base: Base::Decimal,
suffix: IntegerSuffix::EMPTY,
digits: "0".to_string(),
})
} else {
parser.store_simple_error(&LEADING_ZERO_NOT_ALLOWED);
None
};
}
}
}
if let Some(number) = parser.decimal_numeral() {
return Some(IntegerLiteral {
sign,
base: Base::Decimal,
suffix: IntegerSuffix::EMPTY,
digits: number,
});
}
parser.store_dynamic_error_and_suggest(&LITERAL_INCORRECT, "0", &["0"]);
None
})?;
result.suffix = self
.parse_or_revert(Self::integer_suffix)
.unwrap_or(IntegerSuffix::EMPTY);
Some(result)
}
fn float_type_suffix(&mut self) -> Option<TypeSuffix> {
self.parse_or_revert(|parser| {
parser.reader.skip_whitespace();
match parser.reader.peek() {
Some('f' | 'F') => {
parser.reader.skip();
Some(TypeSuffix::Float)
}
Some('d' | 'D') => {
parser.reader.skip();
Some(TypeSuffix::Double)
}
_ => {
parser.store_dynamic_error_and_suggest(
&LITERAL_INCORRECT,
"f|F",
&["f", "F", "d", "D"],
);
None
}
}
})
}
fn float_exponent_part(&mut self) -> Option<Signed<String>> {
self.parse_or_revert(|parser| {
parser.reader.skip_whitespace();
if matches!(parser.reader.peek(), Some('e' | 'E')) {
parser.reader.skip();
let sign = parser.parse_or_revert(Self::sign).unwrap_or(Sign::Plus);
let value = parser.decimal_numeral()?;
Some(Signed { sign, value })
} else {
parser.store_dynamic_error_and_suggest(&LITERAL_INCORRECT, "e|E", &["e", "E"]);
None
}
})
}
fn float_literal(&mut self) -> Option<NbtTag> {
struct FloatingPointIntermediate {
whole_part: String,
fraction_part: Option<String>,
exponent_part: Option<Signed<String>>,
type_suffix: Option<TypeSuffix>,
}
// Paths:
// A --- XXX.[yyy][eZZZ][suffix]
// B --- .yyy[eZZZ][suffix]
// C --- XXXeZZZ[suffix]
// D --- XXX[eZZZ]suffix
//
// where [a] means 'optionally parse a',
// XXX is the whole part, yyy is the decimal part,
// eZZZ is the float exponent path, and
// suffix is float type suffix.
//
// Ruleset:
// If we encounter a digit, we must parse a decimal number. Then:
// If we encounter a decimal point, we must choose path A.
// Try to parse [eZZZ] AND [suffix]:
// if [eZZZ] parses, then irrespective of [suffix], choose path D.
// if ONLY [suffix] parses, choose path C.
// if none parse, FAIL.
// If we encounter a decimal point, we must choose path B.
// FAIL if nether a period or a digit
let sign = self.parse_or_revert(Self::sign).unwrap_or(Sign::Plus);
let intermediate = self.parse_or_revert(|parser| {
parser.reader.skip_whitespace();
if let Some(whole_part) = parser.parse_or_revert(Self::decimal_numeral) {
// Must be pathway A, C, or D.
parser.reader.skip_whitespace();
if parser.reader.peek() == Some('.') {
// We choose pathway A.
parser.reader.skip();
let fraction_part = parser.decimal_numeral();
let exponent_part = parser.float_exponent_part();
let type_suffix = parser.float_type_suffix();
Some(FloatingPointIntermediate {
whole_part,
fraction_part,
exponent_part,
type_suffix,
})
} else {
// This error won't actually matter if the following part
// parses successfully.
parser.store_dynamic_error_and_suggest(&LITERAL_INCORRECT, ".", &["."]);
// Must be pathway C or D.
let exponent_part = parser.float_exponent_part();
let type_suffix = parser.float_type_suffix();
(exponent_part.is_some() || type_suffix.is_some()).then_some(
FloatingPointIntermediate {
whole_part,
fraction_part: None,
exponent_part,
type_suffix,
},
)
}
} else {
// We must parse a decimal point.
parser.reader.skip_whitespace();
if parser.reader.peek() == Some('.') {
parser.reader.skip();
// We choose pathway B.
let fraction_part = parser.decimal_numeral()?;
let exponent_part = parser.float_exponent_part();
let type_suffix = parser.float_type_suffix();
Some(FloatingPointIntermediate {
whole_part: String::new(),
fraction_part: Some(fraction_part),
exponent_part,
type_suffix,
})
} else {
// We cannot choose a pathway.
parser.store_dynamic_error_and_suggest(&LITERAL_INCORRECT, ".", &["."]);
None
}
}
})?;
// Parsing the float:
let mut buffer = String::with_capacity(
sign.minimum_size_parsable()
+ intermediate.whole_part.len()
+ intermediate
.fraction_part
.as_ref()
.map_or(0, |s| 1 + s.len())
+ intermediate
.exponent_part
.as_ref()
.map_or(0, |s| 1 + s.sign.minimum_size_parsable() + s.value.len()),
);
sign.append_minimum_str_parsable(&mut buffer);
Self::clean_and_append(&mut buffer, &intermediate.whole_part);
if let Some(fraction) = &intermediate.fraction_part {
buffer.push('.');
Self::clean_and_append(&mut buffer, fraction);
}
if let Some(exponent) = &intermediate.exponent_part {
buffer.push('e');
exponent.sign.append_minimum_str_parsable(&mut buffer);
Self::clean_and_append(&mut buffer, &exponent.value);
}
match intermediate.type_suffix {
None | Some(TypeSuffix::Double) => match buffer.parse::<f64>() {
Err(_) => self.store_dynamic_error(&NUMBER_PARSE_FAILURE, "Invalid float literal"),
Ok(value) if value.is_finite() => {
return Some(NbtTag::Double(value));
}
Ok(_) => self.store_simple_error(&INFINITY_NOT_ALLOWED),
},
Some(TypeSuffix::Float) => match buffer.parse::<f32>() {
Err(_) => {
self.store_dynamic_error(&NUMBER_PARSE_FAILURE, "Invalid float literal");
}
Ok(value) if value.is_finite() => {
return Some(NbtTag::Float(value));
}
Ok(_) => self.store_simple_error(&INFINITY_NOT_ALLOWED),
},
_ => self.store_simple_error(&EXPECTED_FLOAT_TYPE),
}
None
}
fn string_hex_2(&mut self) -> Option<String> {
self.hex_literal(2)
}
fn string_hex_4(&mut self) -> Option<String> {
self.hex_literal(4)
}
fn string_hex_8(&mut self) -> Option<String> {
self.hex_literal(8)
}
/// Parses a unicode name pattern.
fn string_unicode_name(&mut self) -> Option<String> {
self.parse_or_revert(|parser| {
let start = parser.reader.cursor();
let mut end = start;
// Since the only characters allowed are all ASCII, it should
// be fine to go byte by byte.
let bytes = parser.reader.string().as_bytes();
while end < bytes.len() {
let b = bytes[end];
if matches!(b, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b' ' | b'-') {
end += 1;
} else {
break;
}
}
if start == end {
parser.store_simple_error(&INVALID_CHARACTER_NAME);
None
} else {
parser.reader.set_cursor(end);
Some(parser.reader.string()[start..end].to_string())
}
})
}
/// Parses an escape sequence (without the \)
/// The returned character will be expressed as a `u32`
/// due to Rust's strictness on `char` of surrogate codepoints.
fn escape_sequence(&mut self) -> Option<u32> {
enum EscapeSequenceBranch {
Return(char),
CheckValidity(u32),
UnicodeName(String),
}
let cursor_at_escaping_char = self.reader.cursor();
let branch = match self.reader.read() {
Some('b') => Some(EscapeSequenceBranch::Return('\x08')),
Some('s') => Some(EscapeSequenceBranch::Return(' ')),
Some('t') => Some(EscapeSequenceBranch::Return('\t')),
Some('n') => Some(EscapeSequenceBranch::Return('\n')),
Some('f') => Some(EscapeSequenceBranch::Return('\x0C')),
Some('r') => Some(EscapeSequenceBranch::Return('\r')),
Some('\\') => Some(EscapeSequenceBranch::Return('\\')),
Some('\'') => Some(EscapeSequenceBranch::Return('\'')),
Some('"') => Some(EscapeSequenceBranch::Return('"')),
Some('x') => Some(EscapeSequenceBranch::CheckValidity(
u32::from_str_radix(&self.string_hex_2()?, 16)
.expect("Hexadecimal parsed should have been valid"),
)),
Some('u') => Some(EscapeSequenceBranch::CheckValidity(
u32::from_str_radix(&self.string_hex_4()?, 16)
.expect("Hexadecimal parsed should have been valid"),
)),
Some('U') => Some(EscapeSequenceBranch::CheckValidity(
u32::from_str_radix(&self.string_hex_8()?, 16)
.expect("Hexadecimal parsed should have been valid"),
)),
Some('N') => {
if self.reader.peek() != Some('{') {
self.store_dynamic_error_and_suggest(&LITERAL_INCORRECT, "{", &["}"]);
return None;
}
self.reader.skip();
let string_unicode_name = self.string_unicode_name()?;
if self.reader.peek() != Some('}') {
self.store_dynamic_error_and_suggest(&LITERAL_INCORRECT, "}", &["}"]);
return None;
}
self.reader.skip();
Some(EscapeSequenceBranch::UnicodeName(string_unicode_name))
}
_ => {
self.reader.set_cursor(cursor_at_escaping_char);
self.store_dynamic_error_and_suggest(
&LITERAL_INCORRECT,
"b",
&[
"b", "s", "t", "n", "f", "r", "\\", "'", "\"", "x", "u", "U", "N",
],
);
None
}
}?;
match branch {
EscapeSequenceBranch::Return(ch) => Some(ch as u32),
EscapeSequenceBranch::CheckValidity(value) => {
// Value must be <= 0x10FFFF to be a valid codepoint.
// (Surrogates are handled outside this function)
if value <= 0x10FFFF {
Some(value)
} else {
self.store_dynamic_error(&INVALID_CODEPOINT, format!("U+{value:08X}"));
None
}
}
EscapeSequenceBranch::UnicodeName(_name) => {
todo!("Unicode Name functionality has not been implemented yet")
}
}
}
fn quoted_string_literal(&mut self) -> Option<String> {
self.parse_or_revert(|parser| {
parser.reader.skip_whitespace();
match parser.reader.peek() {
Some('\'') => {
parser.reader.skip();
parse_string_literal!(parser, '\'')
}
Some('"') => {
parser.reader.skip();
parse_string_literal!(parser, '"')
}
_ => {
parser.store_dynamic_error_and_suggest(&LITERAL_INCORRECT, "\"", &["'", "\""]);
None
}
}
})
}
fn unquoted_string_literal(&mut self) -> Option<String> {
self.parse_or_revert(|parser| {
parser.reader.skip_whitespace();
let value = parser.reader.read_unquoted_string();
if value.is_empty() {
parser.store_simple_error(&EXPECTED_UNQUOTED_STRING);
None
} else {
Some(value)
}
})
}
fn arguments(&mut self) -> Vec<NbtTag> {
self.repeated_with_trailing_comma_vec(Self::literal)
}
fn unquoted_string_or_built_in(&mut self) -> Option<NbtTag> {
let literal = self.unquoted_string_literal()?;
// Trying to match the same behaviour of storing arguments
// in the scope even if the right bracket failed to parse:
let mut arguments = None;
let _ = self.parse_or_revert(|parser| {
parser.reader.skip_whitespace();
if parser.reader.peek() != Some('(') {
parser.store_dynamic_error_and_suggest(&LITERAL_INCORRECT, "(", &["("]);
return None;
}
parser.reader.skip();
arguments = Some(parser.arguments());
parser.reader.skip_whitespace();
if parser.reader.peek() == Some(')') {
parser.reader.skip();
Some(())
} else {
parser.store_dynamic_error_and_suggest(&LITERAL_INCORRECT, ")", &[")"]);
None
}
});
// This should be fine as the characters in the predicate are all ASCII.
if literal.is_empty() || matches!(literal.as_bytes()[0], b'0'..=b'9' | b'+' | b'-' | b'.') {
self.store_simple_error_and_suggest(
&INVALID_UNQUOTED_START,
SnbtOperations::BUILTIN_IDS,
);
return None;
}
if let Some(arguments) = arguments {
let count = arguments.len();
if let Some(operation) = SnbtOperations::search(&literal, count) {
operation(self, &arguments[..])
} else {
self.store_dynamic_error(&NO_SUCH_OPERATION, format!("{literal}/{count}"));
None
}
} else if literal.eq_ignore_ascii_case("true") {
Some(NbtTag::Byte(1))
} else if literal.eq_ignore_ascii_case("false") {
Some(NbtTag::Byte(0))
} else {
Some(NbtTag::String(literal))
}
}
fn map_key(&mut self) -> Option<String> {
self.parse_or_revert(Self::quoted_string_literal)
.map_or_else(|| self.unquoted_string_literal(), Some)
}
fn map_entry(&mut self) -> Option<(String, NbtTag)> {
let entry = self.parse_or_revert(|parser| {
let key = parser.map_key()?;
parser.reader.skip_whitespace();
if parser.reader.peek() == Some(':') {
parser.reader.skip();
Some((key, parser.literal()?))
} else {
parser.store_dynamic_error_and_suggest(&LITERAL_INCORRECT, ":", &[":"]);
None
}
})?;
if entry.0.is_empty() {
self.store_simple_error(&EMPTY_KEY);
None
} else {
Some(entry)
}
}
fn map_entries(&mut self) -> HashMap<String, NbtTag> {
self.repeated_with_trailing_comma(Self::map_entry, HashMap::new(), |map, element| {
map.insert(element.0, element.1);
})
}
fn map_literal(&mut self) -> Option<NbtTag> {
let entries = self.parse_or_revert(|parser| {
parser.reader.skip_whitespace();
if parser.reader.peek() != Some('{') {
parser.store_dynamic_error_and_suggest(&LITERAL_INCORRECT, "{", &["}"]);
return None;
}
parser.reader.skip();
let entries = parser.map_entries();
parser.reader.skip_whitespace();
if parser.reader.peek() != Some('}') {
parser.store_dynamic_error_and_suggest(&LITERAL_INCORRECT, "}", &["}"]);
return None;
}
parser.reader.skip();
Some(entries)
})?;
Some(NbtTag::Compound(NbtCompound {
child_tags: entries,
}))
}
fn list_entries(&mut self) -> Vec<NbtTag> {
self.repeated_with_trailing_comma_vec(Self::literal)
}
fn array_prefix(&mut self) -> Option<ArrayPrefix> {
self.parse_or_revert(|parser| {
parser.reader.skip_whitespace();
match parser.reader.peek() {
Some('B') => {
parser.reader.skip();
Some(ArrayPrefix::Byte)
}
Some('I') => {
parser.reader.skip();
Some(ArrayPrefix::Int)
}
Some('L') => {
parser.reader.skip();
Some(ArrayPrefix::Long)
}
_ => {
parser.store_dynamic_error_and_suggest(
&LITERAL_INCORRECT,
"B",
&["B", "I", "L"],
);
None
}
}
})
}
fn int_array_entries(&mut self) -> Vec<IntegerLiteral> {
self.repeated_with_trailing_comma_vec(Self::integer_literal)
}
fn list_literal(&mut self) -> Option<NbtTag> {
self.parse_or_revert(|parser| {
parser.reader.skip_whitespace();
if parser.reader.peek() != Some('[') {
parser.store_dynamic_error_and_suggest(&LITERAL_INCORRECT, "[", &["["]);
return None;
}
parser.reader.skip();
if let Some((prefix, literals)) = parser.parse_or_revert(|parser| {
let prefix = parser.array_prefix()?;
parser.reader.skip_whitespace();
if parser.reader.peek() == Some(';') {
parser.reader.skip();
let entries = parser.int_array_entries();
parser.reader.skip_whitespace();
if parser.reader.peek() != Some(']') {
parser.store_dynamic_error_and_suggest(&LITERAL_INCORRECT, "]", &["]"]);
return None;
}
parser.reader.skip();
Some((prefix, entries))
} else {
parser.store_dynamic_error_and_suggest(&LITERAL_INCORRECT, ";", &[";"]);
None
}
}) {
parser.create_prefixed_array(&literals[..], prefix)
} else {
let entries = parser.list_entries();
parser.reader.skip_whitespace();
if parser.reader.peek() != Some(']') {
parser.store_dynamic_error_and_suggest(&LITERAL_INCORRECT, "]", &["]"]);
return None;
}
parser.reader.skip();
Some(NbtOps.create_list(entries))
}
})
}
fn literal(&mut self) -> Option<NbtTag> {
enum Literal {
Tag(NbtTag),
Integer(IntegerLiteral),
String(String),
}
self.reader.skip_whitespace();
// This has to match the actual rules, so this code is pretty awkward.
let mut result = None;
if matches!(self.reader.peek(), Some('0'..='9' | '+' | '-' | '.')) {
if let Some(tag) = self.parse_or_revert(Self::float_literal) {
result = Some(Literal::Tag(tag));
} else if let Some(literal) = self.parse_or_revert(Self::integer_literal) {
result = Some(Literal::Integer(literal));
}
}
let result = if let Some(result) = result {
result
} else {
match self.reader.peek() {
Some('"' | '\'') => Literal::String(self.quoted_string_literal()?),
Some('{') => Literal::Tag(self.map_literal()?),
Some('[') => Literal::Tag(self.list_literal()?),
_ => Literal::Tag(self.unquoted_string_or_built_in()?),
}
};
Some(match result {
Literal::Tag(tag) => tag,
Literal::Integer(int) => {
match self.parse_integer_literal(&int, int.suffix.1.or(TypeSuffix::Int))? {
Number::Byte(byte) => NbtTag::Byte(byte),
Number::Short(short) => NbtTag::Short(short),
Number::Int(int) => NbtTag::Int(int),
Number::Long(long) => NbtTag::Long(long),
_ => unreachable!(
"Got a floating-point number when only integers should be returned"
),
}
}
Literal::String(string) => NbtTag::String(string),
})
}
pub(super) fn parse(&mut self) -> Option<NbtTag> {
self.literal()
}
}

View File

@@ -0,0 +1,508 @@
use std::collections::HashMap;
use pumpkin_nbt::{compound::NbtCompound, nbt_compound_tag, tag::NbtTag};
const BUILT_IN_LIKE_SUGGESTIONS: &[&str] = &["(", "bool", "false", "true", "uuid"];
use crate::command::{
errors::command_syntax_error::CommandSyntaxError, snbt::SnbtParser,
string_reader::StringReader, suggestion::suggestions::SuggestionsBuilder,
};
fn parse(snbt: &str) -> Result<NbtTag, CommandSyntaxError> {
SnbtParser::parse_for_commands(&mut StringReader::new(snbt))
}
fn suggestions(snbt: &str) -> Vec<String> {
let builder = SuggestionsBuilder::new(snbt, 0);
let suggestions = SnbtParser::parse_for_suggestions(&mut StringReader::new(snbt), builder);
suggestions
.suggestions
.into_iter()
.map(|suggestion| suggestion.text_as_string())
.collect()
}
macro_rules! assert_parse_ok {
($snbt:expr, $tag:expr) => {
let mut reader = StringReader::new($snbt);
match SnbtParser::parse_for_commands(&mut reader) {
Err(error) => {
panic!("Expected a successful parse, but instead got error: {error:?}")
}
Ok(tag_parsed) => {
assert_eq!(
tag_parsed, $tag,
"Parsed tag does not match the required one"
);
assert!(
reader.cursor() == reader.string().len(),
"Expected everything to get parsed, but found trailing data: {}",
&reader.string()[reader.cursor()..]
);
}
}
};
}
macro_rules! assert_parse_ok_but_trailing {
($snbt:expr, $trailing_data:expr) => {
let mut reader = StringReader::new($snbt);
if let Err(error) = SnbtParser::parse_for_commands(&mut reader) {
panic!("Expected a successful parse, but instead got error: {error:?}")
}
assert!(
reader.cursor() < reader.string().len(),
"Expected trailing data, but everything was parsed successfully"
);
assert_eq!(
&reader.string()[reader.cursor()..],
$trailing_data,
"Trailing data don't match"
)
};
}
macro_rules! assert_parse_err {
($snbt:expr, $error_message:expr, $cursor:expr) => {
let parsed = parse($snbt);
match parsed {
Ok(tag) => panic!("Expected command error, but instead got result: {tag:#?}"),
Err(error) => {
assert_eq!(
error.message.get_text(),
$error_message,
"Error messages don't match"
);
// There should always be a context in SNBT parsing.
assert_eq!(
error.context.unwrap().cursor,
$cursor,
"Cursor positions for error don't match"
);
}
}
};
// Without this, we keep getting the error message: type annotations needed
($snbt:expr, $error_message:expr, $cursor:expr, []) => {
assert_parse_err!($snbt, $error_message, $cursor);
let suggestions = suggestions($snbt);
assert!(
suggestions.is_empty(),
"Expected no suggestions, but got one or more: {suggestions:?}"
);
};
($snbt:expr, $error_message:expr, $cursor:expr, $suggestions:expr) => {
assert_parse_err!($snbt, $error_message, $cursor);
let suggestions = suggestions($snbt);
assert_eq!(suggestions, $suggestions, "Suggestions don't match");
};
}
#[test]
fn integers() {
assert_parse_ok!("9", NbtTag::Int(9));
assert_parse_ok!("5_0_0_0", NbtTag::Int(5000));
assert_parse_err!(
"5_0_0_0_",
"Expected literal (",
8,
BUILT_IN_LIKE_SUGGESTIONS
);
assert_parse_err!(
"5_0_0_0_",
"Expected literal (",
8,
BUILT_IN_LIKE_SUGGESTIONS
);
assert_parse_ok!("3ub", NbtTag::Byte(3));
assert_parse_ok!("-7s", NbtTag::Short(-7));
assert_parse_ok!("255uB", NbtTag::Byte(-1));
assert_parse_err!("256ub", "Failed to parse number: out of range: 256", 5, []);
assert_parse_ok!("256ss", NbtTag::Short(256));
assert_parse_ok!("256 s s", NbtTag::Short(256));
assert_parse_err!(
"3_000_000_000",
"Expected literal .",
13,
[
".", "b", "B", "d", "D", "e", "E", "f", "F", "i", "I", "l", "L", "s", "S", "u", "U"
]
);
assert_parse_ok!("+3_000_000_000uI", NbtTag::Int(-1_294_967_296));
assert_parse_ok!("+3_000_000_000s L", NbtTag::Long(3_000_000_000));
assert_parse_ok!("-3_000_000_000 sL", NbtTag::Long(-3_000_000_000));
assert_parse_err!(
"-3_000_000_000i",
"Failed to parse number: For input string: \"-3000000000\"",
15,
[]
);
assert_parse_err!("-3_000_000_000UI", "Expected a non-negative number", 16, []);
assert_parse_err!(
"00",
"Expected literal .",
2,
[
"(", ".", "bool", "d", "D", "e", "E", "f", "F", "false", "true", "uuid"
]
);
assert_parse_err!(
"0x",
"Expected a hexadecimal number",
2,
BUILT_IN_LIKE_SUGGESTIONS
);
assert_parse_ok!("0b", NbtTag::Byte(0));
assert_parse_ok!("0b10101", NbtTag::Int(21));
assert_parse_ok!("0X111", NbtTag::Int(273));
assert_parse_err!("0x_111", "Expected literal (", 6, BUILT_IN_LIKE_SUGGESTIONS);
assert_parse_err!(
"0xAbCdEfs",
"Expected literal b|B",
9,
["b", "B", "i", "I", "l", "L", "s", "S"]
);
assert_parse_ok_but_trailing!("0xABCDEFG", "G");
assert_parse_ok!("0xABCDUS", NbtTag::Short(-21555));
// Should not parse as byte of 0xAB
assert_parse_ok!("0xABB", NbtTag::Int(2747));
}
#[test]
fn floats() {
assert_parse_ok!("0.", NbtTag::Double(0.0));
assert_parse_ok!("0.f", NbtTag::Float(0.0));
assert_parse_ok!("0.D", NbtTag::Double(0.0));
assert_parse_ok!(".0", NbtTag::Double(0.0));
assert_parse_ok!(".0F", NbtTag::Float(0.0));
assert_parse_ok!(".0d", NbtTag::Double(0.0));
assert_parse_ok!("1.024", NbtTag::Double(1.024));
assert_parse_err!("1_.024", "Expected literal (", 6, BUILT_IN_LIKE_SUGGESTIONS);
assert_parse_ok_but_trailing!("1._024", "_024");
assert_parse_ok!("1.0_2_4", NbtTag::Double(1.024));
assert_parse_ok!("1e1", NbtTag::Double(10.0));
assert_parse_ok!("2e+2", NbtTag::Double(200.0));
assert_parse_ok!("4e-2", NbtTag::Double(0.04));
assert_parse_ok!("4e-2", NbtTag::Double(0.04));
assert_parse_ok!("0E100_000_000", NbtTag::Double(0.0));
assert_parse_ok_but_trailing!("0.1e100_000_000", ".1e100_000_000");
assert_parse_ok!("0.1e-100_000_000", NbtTag::Double(0.0));
assert_parse_ok!("1e38f", NbtTag::Float(1e38));
assert_parse_ok_but_trailing!("1e39f", "e39f");
assert_parse_ok!("1e39", NbtTag::Double(1e39));
assert_parse_ok!("0.001e41f", NbtTag::Float(1e38));
assert_parse_ok_but_trailing!("0.01E41f", ".01E41f");
assert_parse_ok!("1.28E308", NbtTag::Double(1.28E308));
assert_parse_ok_but_trailing!("1.8e308", ".8e308");
assert_parse_ok_but_trailing!("1.E", "E");
assert_parse_ok!("2000f", NbtTag::Float(2000.0));
assert_parse_ok!("70d", NbtTag::Double(70.0));
assert_parse_ok!("03f", NbtTag::Float(3.0));
assert_parse_ok!("03.70", NbtTag::Double(3.7));
assert_parse_ok!("+1e-1", NbtTag::Double(0.1));
}
#[test]
fn quoted_string_literals() {
assert_parse_ok!("''", NbtTag::String(String::new()));
assert_parse_ok!("\"\"", NbtTag::String(String::new()));
assert_parse_ok!("\"'hello'\"", NbtTag::String("'hello'".to_string()));
assert_parse_ok!("'\"hello\"'", NbtTag::String("\"hello\"".to_string()));
assert_parse_ok!("'\\\\'", NbtTag::String("\\".to_string()));
assert_parse_ok_but_trailing!("'\"'\"", "\"");
assert_parse_err!("'\\'", "Invalid string contents", 3, ["\"", "'", "\\"]);
assert_parse_ok!("'\\b'", NbtTag::String("\u{8}".to_string()));
assert_parse_ok!("'hello\\sword'", NbtTag::String("hello word".to_string()));
assert_parse_ok!(
"'hello\\tword\n'",
NbtTag::String("hello\tword\n".to_string())
);
assert_parse_ok!("'\\f\\r'", NbtTag::String("\u{c}\r".to_string()));
assert_parse_ok!("'hello \\x65!'", NbtTag::String("hello e!".to_string()));
assert_parse_ok!(
"'\\x53\\x65\\u0063\\U00000072\\x65\\x74\\x21'",
NbtTag::String("Secret!".to_string())
);
assert_parse_err!(
"'\\U1234567'",
"Expected a character literal of length 8",
3,
[]
);
assert_parse_ok!(
"'\\uD83C\\uDF83 or \\U0001F383'",
NbtTag::String("🎃 or 🎃".to_string())
);
// TODO: make tests for when \N is implemented
}
#[test]
fn unquoted_string_literals() {
assert_parse_ok!("abc", NbtTag::String("abc".to_string()));
assert_parse_ok!(
"abc-def_ghi+jkl.mno",
NbtTag::String("abc-def_ghi+jkl.mno".to_string())
);
assert_parse_ok!("_1234", NbtTag::String("_1234".to_string()));
assert_parse_ok!("x+1", NbtTag::String("x+1".to_string()));
assert_parse_ok_but_trailing!("x*1", "*1");
assert_parse_ok!("true", NbtTag::Byte(1));
assert_parse_ok!("false", NbtTag::Byte(0));
assert_parse_ok!("maybe", NbtTag::String("maybe".to_string()));
assert_parse_ok!("bool", NbtTag::String("bool".to_string()));
}
#[test]
fn operations() {
assert_parse_ok!("bool( true)", NbtTag::Byte(1));
assert_parse_ok!("bool (false )", NbtTag::Byte(0));
assert_parse_ok!("bool(0)", NbtTag::Byte(0));
assert_parse_ok!("bool( 1 )", NbtTag::Byte(1));
assert_parse_ok!("bool (2.5 )", NbtTag::Byte(1));
assert_parse_ok!("bool ( -4.3412e+12 )", NbtTag::Byte(0));
assert_parse_err!("bool(", "Expected a valid unquoted string", 5, [")"]);
assert_parse_err!("bool()", "No such operation: bool/0", 6, []);
assert_parse_err!("bool(1, 2)", "No such operation: bool/2", 10, []);
assert_parse_err!(
"bool (1,2,3",
"Expected literal .",
11,
[
")", ",", ".", "b", "B", "d", "D", "e", "E", "f", "F", "i", "I", "l", "L", "s", "S",
"u", "U"
]
);
assert_parse_ok!(
"uuid('3d569d3a-93ef-44a0-9f1c-f69db9d37a56')",
NbtTag::IntArray(vec![1029086522, -1813035872, -1625491811, -1177322922])
);
assert_parse_ok!(
"uuid(ad569d3a-93ef-44a0-9f1c-f69db9d37a56)",
NbtTag::IntArray(vec![-1386832582, -1813035872, -1625491811, -1177322922])
);
assert_parse_err!(
"uuid(3d53a-f40-c-f69db9d37a56)",
"Expected literal ,",
7,
[")", ","]
);
assert_parse_ok!(
"uuid(fffffffffffffff-0-0-0-0)",
NbtTag::IntArray(vec![-1, 0, 0, 0])
);
assert_parse_ok!(
"uuid(AaaAaaAaaAaaAaA-BBbBbbBbBbbbBB-c-D-e)",
NbtTag::IntArray(vec![-1431655766, -1145372660, 851968, 14])
);
assert_parse_ok!(
"uuid(a1-+2-+3-+4-+5)",
NbtTag::IntArray(vec![161, 131075, 262144, 5])
);
assert_parse_err!(
"uuid(x)",
"Expected a string representing a valid UUID",
7,
[]
);
}
#[test]
fn maps() {
assert_parse_ok!(
"{x:5}",
NbtTag::Compound(NbtCompound {
child_tags: HashMap::from([("x".to_string(), NbtTag::Int(5))])
})
);
assert_parse_ok!(
"{ a:1b,B :2uS , c:3L }",
NbtTag::Compound(NbtCompound {
child_tags: HashMap::from([
("a".to_string(), NbtTag::Byte(1)),
("B".to_string(), NbtTag::Short(2)),
("c".to_string(), NbtTag::Long(3))
])
})
);
assert_parse_ok!(
"{ a:1b, \"a\":2b, 'a':\"hi\" }",
nbt_compound_tag! {
"a": NbtTag::String("hi".to_string())
}
);
assert_parse_ok!(
"{ elem: 'this', next: { elem: 'is', next: { elem: 'a', next: { elem: 'linked', next: 'list' } } } }",
nbt_compound_tag! {
"elem": NbtTag::String("this".to_string()),
"next": nbt_compound_tag! {
"elem": NbtTag::String("is".to_string()),
"next": nbt_compound_tag! {
"elem": NbtTag::String("a".to_string()),
"next": nbt_compound_tag! {
"elem": NbtTag::String("linked".to_string()),
"next": NbtTag::String("list".to_string()),
}
}
}
}
);
assert_parse_err!("{'x': 5f", "Expected literal ,", 8, [",", "}"]);
assert_parse_err!(
"{text:\"cool\",\"color:dark_red}",
"Invalid string contents",
29,
["\"", "'", "\\"]
);
assert_parse_ok!(
"{9._+._+foo:1}",
nbt_compound_tag! {
"9._+._+foo": NbtTag::Int(1)
}
);
assert_parse_err!("{9._+._+=foo:1}", "Expected literal :", 8, [":"]);
assert_parse_err!("{\"a\":b", "Expected literal (", 6, ["(", ",", "}"]);
assert_parse_err!(
"{\"a\":25",
"Expected literal .",
7,
[
",", ".", "b", "B", "d", "D", "e", "E", "f", "F", "i", "I", "l", "L", "s", "S", "u",
"U", "}"
]
);
assert_parse_err!("{\"a\":25,", "Expected literal \"", 8, ["\"", "'", "}"]);
assert_parse_err!("{,}", "Expected literal \"", 1, ["\"", "'", "}"]);
assert_parse_err!("{{}}", "Expected literal \"", 1, ["\"", "'", "}"]);
assert_parse_ok!(
"{1:1}",
nbt_compound_tag! {
"1": NbtTag::Int(1)
}
);
}
#[test]
fn lists() {
assert_parse_ok!("[ ]", NbtTag::List(Vec::new()));
assert_parse_ok!(
"[5, _]",
NbtTag::List(vec![NbtTag::Int(5), NbtTag::String("_".to_string())])
);
assert_parse_ok!(
"[a, [true, c], [4s, uuid(f-0-0-0-0), f, [[], 7f, [\"\\n\", [200Ub, {x: 1e1}]]]]]",
NbtTag::List(vec![
NbtTag::String("a".to_string()),
NbtTag::List(vec![NbtTag::Byte(1), NbtTag::String("c".to_string())]),
NbtTag::List(vec![
NbtTag::Short(4),
NbtTag::IntArray(vec![15, 0, 0, 0]),
NbtTag::String("f".to_string()),
NbtTag::List(vec![
NbtTag::List(Vec::new()),
NbtTag::Float(7.0),
NbtTag::List(vec![
NbtTag::String("\n".to_string()),
NbtTag::List(vec![
NbtTag::Byte(-56),
nbt_compound_tag! {
"x": NbtTag::Double(10.0)
}
]),
]),
]),
])
])
);
assert_parse_err!(
"[1;1]",
"Expected literal .",
2,
[
",", ".", "]", "b", "B", "d", "D", "e", "E", "f", "F", "i", "I", "l", "L", "s", "S",
"u", "U"
]
);
assert_parse_err!("[{]}", "Expected literal \"", 2, ["\"", "'", "}"]);
assert_parse_err!("{[}]", "Expected literal \"", 1, ["\"", "'", "}"]);
assert_parse_err!("[,]", "Expected literal B", 1, ["]", "B", "I", "L"]);
assert_parse_err!("[Z;9]", "Expected literal (", 2, ["(", ",", "]"]);
}
#[test]
fn arrays() {
assert_parse_ok!("[B;]", NbtTag::ByteArray(vec![]));
assert_parse_ok!("[I ;1 ,2 , 3,]", NbtTag::IntArray(vec![1, 2, 3]));
assert_parse_ok!("[L;1 ,2 , 3, 4]", NbtTag::LongArray(vec![1, 2, 3, 4]));
assert_parse_err!("[B;1i]", "Invalid array element type", 6, []);
assert_parse_err!("[I;1L]", "Invalid array element type", 6, []);
assert_parse_err!(
"[B;128]",
"Failed to parse number: Value out of range. Value:\"128\" Radix:10",
7,
[]
);
assert_parse_err!(
"[B;3000000000]",
"Failed to parse number: For input string: \"3000000000\"",
14,
[]
);
assert_parse_err!(
"[I;3000000000]",
"Failed to parse number: For input string: \"3000000000\"",
14,
[]
);
assert_parse_err!(
"[I; 1.0]",
"Expected literal u|U",
5,
[",", "]", "b", "B", "i", "I", "l", "L", "s", "S", "u", "U"]
);
assert_parse_err!("[I;{}]", "Expected literal +", 3, ["+", "-", "0", "]"]);
assert_parse_err!("[i;4]", "Expected literal (", 2, ["(", ",", "]"]);
assert_parse_ok!("[B; 0b11111111]", NbtTag::ByteArray(vec![-1]));
assert_parse_ok!("[L; 0xFFFFFFFFFFFFFFFF]", NbtTag::LongArray(vec![-1]));
assert_parse_err!(
"[L; 0xFFFFFFFFFFFFFFFFF]",
"Failed to parse number: String value FFFFFFFFFFFFFFFFF exceeds range of unsigned long.",
24,
[]
);
}

View File

@@ -11,7 +11,7 @@ use std::str::FromStr;
/// It internally uses a cursor to read them, which is
/// very important to determine the location of the cause
/// of a syntax error arising from this parser.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Default)]
pub struct StringReader<'a> {
string: Cow<'a, str>,
byte_cursor: usize,
@@ -251,7 +251,7 @@ impl<'a> StringReader<'a> {
}
/// Reads an unquoted string (not enclosed in quotes)
pub fn read_unquoted_string(&mut self) -> Result<String, CommandSyntaxError> {
pub fn read_unquoted_string(&mut self) -> String {
let start = self.byte_cursor;
while let Some(c) = self.peek() {
if Self::is_allowed_in_unquoted_string(c) {
@@ -260,7 +260,7 @@ impl<'a> StringReader<'a> {
break;
}
}
Ok(self.string[start..self.byte_cursor].to_string())
self.string[start..self.byte_cursor].to_string()
}
/// Reads any string, whether it be quoted or unquoted.
@@ -272,7 +272,7 @@ impl<'a> StringReader<'a> {
self.skip();
self.read_string_until(next)
} else {
self.read_unquoted_string()
Ok(self.read_unquoted_string())
}
}
@@ -433,7 +433,7 @@ mod test {
assert_eq!(reader.read_quoted_string(), Ok("apple".to_string()));
reader.skip_whitespace();
assert_eq!(reader.read_unquoted_string(), Ok("banana".to_string()));
assert_eq!(reader.read_unquoted_string(), "banana".to_string());
reader.skip_whitespace();
assert_eq!(reader.read_string(), Ok("orange".to_string()));

View File

@@ -382,8 +382,8 @@ mod test {
assert_eq!(
&merged.suggestions,
&[
Suggestion::without_tooltip(StringRange::between(4, 5), "BAR"),
Suggestion::without_tooltip(StringRange::between(4, 5), "bar"),
Suggestion::without_tooltip(StringRange::between(4, 5), "BAR"),
Suggestion::without_tooltip(StringRange::between(4, 5), "bars"),
Suggestion::without_tooltip(StringRange::between(4, 5), "baz"),
Suggestion::without_tooltip(StringRange::between(4, 5), "foo"),

View File

@@ -3,7 +3,6 @@ use crate::command::suggestion::{Suggestion, SuggestionText};
use pumpkin_util::text::TextComponent;
use std::borrow::Borrow;
use std::cmp::Ordering;
use std::collections::HashSet;
/// Represents a builder of [`Suggestion`]s.
pub struct SuggestionsBuilder {
@@ -148,11 +147,13 @@ impl Suggestions {
return input[0].borrow().clone();
}
let mut texts = HashSet::new();
let mut texts = Vec::new();
for suggestions in &input {
for suggestion in &suggestions.borrow().suggestions {
texts.insert(suggestion);
if !texts.contains(&suggestion) {
texts.push(suggestion);
}
}
}
@@ -180,9 +181,12 @@ impl Suggestions {
.reduce(StringRange::encompass)
.unwrap();
let mut texts: HashSet<Suggestion> = HashSet::new();
let mut texts = Vec::new();
for suggestion in &suggestions {
texts.insert(suggestion.borrow().expand(command, range));
let suggestion = suggestion.borrow().expand(command, range);
if !texts.contains(&suggestion) {
texts.push(suggestion);
}
}
Self::new(range, Self::sort(texts))
@@ -192,7 +196,7 @@ impl Suggestions {
///
/// 1. If both suggestions are integers, their integral value is compared.
/// 2. Otherwise, compare their text lexicographically.
fn sort(suggestions: HashSet<Suggestion>) -> Vec<Suggestion> {
fn sort(suggestions: Vec<Suggestion>) -> Vec<Suggestion> {
enum PushSide {
Text,
Integer,
@@ -207,7 +211,13 @@ impl Suggestions {
for suggestion in suggestions {
match suggestion.text {
SuggestionText::Text(text) => {
text_suggestions.push((text, suggestion.tooltip, suggestion.range));
let text_lowercase = text.to_lowercase();
text_suggestions.push((
text,
suggestion.tooltip,
suggestion.range,
text_lowercase,
));
}
SuggestionText::Integer { cached_text, value } => integer_suggestions.push((
cached_text,
@@ -218,9 +228,7 @@ impl Suggestions {
}
}
// We need not preserve the original order as
// there cannot be two or more equivalent suggestions in a set.
text_suggestions.sort_unstable_by(|a, b| a.0.cmp(&b.0));
text_suggestions.sort_by(|a, b| a.3.cmp(&b.3));
integer_suggestions.sort_unstable_by_key(|x| x.1);
let mut text_iter = text_suggestions.into_iter().peekable();

View File

@@ -6,3 +6,4 @@ handled = "handled"
received = "received"
ba = "ba"
fo = "fo"
undescore = "undescore"