mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
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:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2805,6 +2805,7 @@ dependencies = [
|
||||
"num-bigint",
|
||||
"ordered-float",
|
||||
"postcard",
|
||||
"pumpkin-codecs",
|
||||
"pumpkin-config",
|
||||
"pumpkin-data",
|
||||
"pumpkin-inventory",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
179
pumpkin/src/command/parser.rs
Normal file
179
pumpkin/src/command/parser.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
136
pumpkin/src/command/snbt/markers.rs
Normal file
136
pumpkin/src/command/snbt/markers.rs
Normal 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,
|
||||
}
|
||||
478
pumpkin/src/command/snbt/mod.rs
Normal file
478
pumpkin/src/command/snbt/mod.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
137
pumpkin/src/command/snbt/operations.rs
Normal file
137
pumpkin/src/command/snbt/operations.rs
Normal 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])
|
||||
);
|
||||
}
|
||||
}
|
||||
854
pumpkin/src/command/snbt/rules.rs
Normal file
854
pumpkin/src/command/snbt/rules.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
508
pumpkin/src/command/snbt/tests.rs
Normal file
508
pumpkin/src/command/snbt/tests.rs
Normal 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,
|
||||
[]
|
||||
);
|
||||
}
|
||||
@@ -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()));
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -6,3 +6,4 @@ handled = "handled"
|
||||
received = "received"
|
||||
ba = "ba"
|
||||
fo = "fo"
|
||||
undescore = "undescore"
|
||||
Reference in New Issue
Block a user