From 04874d44209f5de829c0ddf73e9aa956075c9a0c Mon Sep 17 00:00:00 2001 From: Laptop59 <90901102+Laptop59@users.noreply.github.com> Date: Tue, 5 May 2026 14:11:27 +0530 Subject: [PATCH] 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 --- Cargo.lock | 1 + pumpkin-codecs/src/codec/primitive.rs | 35 +- pumpkin-codecs/src/dynamic_ops.rs | 19 +- pumpkin-nbt/src/compound.rs | 52 +- pumpkin-nbt/src/lib.rs | 2 +- pumpkin-nbt/src/nbt_ops.rs | 38 +- pumpkin-nbt/src/tag.rs | 29 +- .../src/generation/structure/template/mod.rs | 2 +- pumpkin/Cargo.toml | 1 + .../src/command/argument_types/core/string.rs | 2 +- .../command/argument_types/entity_anchor.rs | 2 +- .../argument_types/entity_selector/option.rs | 4 +- pumpkin/src/command/argument_types/time.rs | 2 +- pumpkin/src/command/mod.rs | 2 + pumpkin/src/command/parser.rs | 179 ++++ pumpkin/src/command/snbt/markers.rs | 136 +++ pumpkin/src/command/snbt/mod.rs | 478 ++++++++++ pumpkin/src/command/snbt/operations.rs | 137 +++ pumpkin/src/command/snbt/rules.rs | 854 ++++++++++++++++++ pumpkin/src/command/snbt/tests.rs | 508 +++++++++++ pumpkin/src/command/string_reader.rs | 10 +- pumpkin/src/command/suggestion/mod.rs | 2 +- pumpkin/src/command/suggestion/suggestions.rs | 28 +- typos.toml | 1 + 24 files changed, 2401 insertions(+), 123 deletions(-) create mode 100644 pumpkin/src/command/parser.rs create mode 100644 pumpkin/src/command/snbt/markers.rs create mode 100644 pumpkin/src/command/snbt/mod.rs create mode 100644 pumpkin/src/command/snbt/operations.rs create mode 100644 pumpkin/src/command/snbt/rules.rs create mode 100644 pumpkin/src/command/snbt/tests.rs diff --git a/Cargo.lock b/Cargo.lock index ca70e06b8..df495e283 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2805,6 +2805,7 @@ dependencies = [ "num-bigint", "ordered-float", "postcard", + "pumpkin-codecs", "pumpkin-config", "pumpkin-data", "pumpkin-inventory", diff --git a/pumpkin-codecs/src/codec/primitive.rs b/pumpkin-codecs/src/codec/primitive.rs index 6a33cbce3..f8e323801 100644 --- a/pumpkin-codecs/src/codec/primitive.rs +++ b/pumpkin-codecs/src/codec/primitive.rs @@ -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> for ByteBuffer { - fn from(value: Box<[u8]>) -> Self { - Self(value) - } -} - -impl From<[u8; N]> for ByteBuffer { - fn from(value: [u8; N]) -> Self { - Self(Box::from(value)) - } -} - -impl From for Box<[u8]> { - fn from(value: ByteBuffer) -> Self { - value.0 - } -} - -impl Primitive for ByteBuffer { - fn primitive_encode(&self, ops: &'static O) -> O::Value { - ops.create_byte_buffer(self.0.to_vec()) - } - - fn primitive_decode(ops: &'static O, input: O::Value) -> DataResult { - 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, diff --git a/pumpkin-codecs/src/dynamic_ops.rs b/pumpkin-codecs/src/dynamic_ops.rs index d62671065..8cf054044 100644 --- a/pumpkin-codecs/src/dynamic_ops.rs +++ b/pumpkin-codecs/src/dynamic_ops.rs @@ -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::>>() - .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> { - impl_get_list!(box self, input, "bytes") + fn get_byte_list(&self, input: Self::Value) -> DataResult> { + impl_get_list!(self, input, "bytes") } /// Creates a byte buffer that can be represented by this `DynamicOps` using a [`Vec`]. - fn create_byte_buffer(&self, buffer: Vec) -> Self::Value { - self.create_list(buffer.iter().map(|b| self.create_byte(*b as i8))) + fn create_byte_list(&self, vec: Vec) -> Self::Value { + self.create_list(vec.into_iter().map(|b| self.create_byte(b))) } /// Gets a [`Vec`] (`int` list) from a generic value represented by this `DynamicOps`. diff --git a/pumpkin-nbt/src/compound.rs b/pumpkin-nbt/src/compound.rs index 2d11448d7..874e0cfe9 100644 --- a/pumpkin-nbt/src/compound.rs +++ b/pumpkin-nbt/src/compound.rs @@ -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`, 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, } 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) { 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; fn into_iter(self) -> Self::IntoIter { self.child_tags.into_iter() diff --git a/pumpkin-nbt/src/lib.rs b/pumpkin-nbt/src/lib.rs index 87a17329e..9b6571239 100644 --- a/pumpkin-nbt/src/lib.rs +++ b/pumpkin-nbt/src/lib.rs @@ -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, diff --git a/pumpkin-nbt/src/nbt_ops.rs b/pumpkin-nbt/src/nbt_ops.rs index 9b20f1de7..cf2b9af73 100644 --- a/pumpkin-nbt/src/nbt_ops.rs +++ b/pumpkin-nbt/src/nbt_ops.rs @@ -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> { + fn get_byte_list(&self, input: Self::Value) -> DataResult> { 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) -> Self::Value { - NbtTag::ByteArray(buffer.into_boxed_slice()) + fn create_byte_list(&self, buffer: Vec) -> Self::Value { + NbtTag::ByteArray(buffer) } fn get_int_list(&self, input: Self::Value) -> DataResult> { @@ -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), CompoundList(Map, fn(NbtTag) -> NbtTag>), - ByteArray(Map, fn(u8) -> NbtTag>), + ByteArray(Map, fn(i8) -> NbtTag>), IntArray(Map, fn(i32) -> NbtTag>), LongArray(Map, 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::>() - .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) -> 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 diff --git a/pumpkin-nbt/src/tag.rs b/pumpkin-nbt/src/tag.rs index e38700bd6..a211f46af 100644 --- a/pumpkin-nbt/src/tag.rs +++ b/pumpkin-nbt/src/tag.rs @@ -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) = BYTE_ARRAY_ID, String(String) = STRING_ID, List(Vec) = 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(); diff --git a/pumpkin-world/src/generation/structure/template/mod.rs b/pumpkin-world/src/generation/structure/template/mod.rs index 4b4ffa6ec..b06842367 100644 --- a/pumpkin-world/src/generation/structure/template/mod.rs +++ b/pumpkin-world/src/generation/structure/template/mod.rs @@ -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()); } } } diff --git a/pumpkin/Cargo.toml b/pumpkin/Cargo.toml index 9ac3a314e..3ddc1a853 100644 --- a/pumpkin/Cargo.toml +++ b/pumpkin/Cargo.toml @@ -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 diff --git a/pumpkin/src/command/argument_types/core/string.rs b/pumpkin/src/command/argument_types/core/string.rs index 06946e531..018a67892 100644 --- a/pumpkin/src/command/argument_types/core/string.rs +++ b/pumpkin/src/command/argument_types/core/string.rs @@ -23,7 +23,7 @@ impl ArgumentType for StringArgumentType { fn parse(&self, reader: &mut StringReader) -> Result { 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(); diff --git a/pumpkin/src/command/argument_types/entity_anchor.rs b/pumpkin/src/command/argument_types/entity_anchor.rs index ceb25c564..a1ce32941 100644 --- a/pumpkin/src/command/argument_types/entity_anchor.rs +++ b/pumpkin/src/command/argument_types/entity_anchor.rs @@ -20,7 +20,7 @@ impl ArgumentType for EntityAnchorArgumentType { fn parse(&self, reader: &mut StringReader) -> Result { 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); diff --git a/pumpkin/src/command/argument_types/entity_selector/option.rs b/pumpkin/src/command/argument_types/entity_selector/option.rs index 278083c78..d854ff5a2 100644 --- a/pumpkin/src/command/argument_types/entity_selector/option.rs +++ b/pumpkin/src/command/argument_types/entity_selector/option.rs @@ -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)); diff --git a/pumpkin/src/command/argument_types/time.rs b/pumpkin/src/command/argument_types/time.rs index 801c812b8..5c97279a8 100644 --- a/pumpkin/src/command/argument_types/time.rs +++ b/pumpkin/src/command/argument_types/time.rs @@ -47,7 +47,7 @@ impl ArgumentType for TimeArgumentType { fn parse(&self, reader: &mut StringReader) -> Result { 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, diff --git a/pumpkin/src/command/mod.rs b/pumpkin/src/command/mod.rs index 78197c3b6..999ef72b6 100644 --- a/pumpkin/src/command/mod.rs +++ b/pumpkin/src/command/mod.rs @@ -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; diff --git a/pumpkin/src/command/parser.rs b/pumpkin/src/command/parser.rs new file mode 100644 index 000000000..fc56cf07d --- /dev/null +++ b/pumpkin/src/command/parser.rs @@ -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>, +} + +#[derive(Debug, Default)] +pub struct ParserErrors { + pub cursor: usize, + pub command_error: Option, + pub suggestions: Vec>, +} + +/// 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>>( + &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, + ) { + 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>>( + &mut self, + reader: &StringReader, + error_type: &'static CommandErrorType<1>, + arg1: A, + suggestions: Vec, + ) { + 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>), + ) { + 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>, + 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>, + ) { + let (reader, errors) = self.state_mut(); + errors.dynamic(reader, error_type, arg1, Vec::new()); + } +} diff --git a/pumpkin/src/command/snbt/markers.rs b/pumpkin/src/command/snbt/markers.rs new file mode 100644 index 000000000..3717e514e --- /dev/null +++ b/pumpkin/src/command/snbt/markers.rs @@ -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 { + 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, +} diff --git a/pumpkin/src/command/snbt/mod.rs b/pumpkin/src/command/snbt/mod.rs new file mode 100644 index 000000000..1117abdd8 --- /dev/null +++ b/pumpkin/src/command/snbt/mod.rs @@ -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 { + 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::>(), + ), + 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 { + 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 { + 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(&mut self, closure: impl FnOnce(&mut Self) -> Option) -> Option { + 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 { + 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( + &mut self, + rule: impl Fn(&mut Self) -> Option, + 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( + &mut self, + rule: impl Fn(&mut Self) -> Option, + ) -> Vec { + self.repeated_with_trailing_comma(rule, Vec::new(), Vec::push) + } + + fn parse_integer_literal( + &mut self, + literal: &IntegerLiteral, + suffix: TypeSuffix, + ) -> Option { + 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::::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::::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::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 { + 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 { + 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 { + 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 { + 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) + } +} diff --git a/pumpkin/src/command/snbt/operations.rs b/pumpkin/src/command/snbt/operations.rs new file mode 100644 index 000000000..3e251eb55 --- /dev/null +++ b/pumpkin/src/command/snbt/operations.rs @@ -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; + +/// 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 { + 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 { + 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 { + 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> { + // 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]) + ); + } +} diff --git a/pumpkin/src/command/snbt/rules.rs b/pumpkin/src/command/snbt/rules.rs new file mode 100644 index 000000000..2d0480ec5 --- /dev/null +++ b/pumpkin/src/command/snbt/rules.rs @@ -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 { + 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 { + 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 { + self.parse_numeral(Base::Binary) + } + + fn decimal_numeral(&mut self) -> Option { + self.parse_numeral(Base::Decimal) + } + + fn hexadecimal_numeral(&mut self) -> Option { + self.parse_numeral(Base::Hexadecimal) + } + + /// Parses an integer literal. + fn integer_literal(&mut self) -> Option { + 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 { + 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> { + 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 { + struct FloatingPointIntermediate { + whole_part: String, + fraction_part: Option, + exponent_part: Option>, + type_suffix: Option, + } + + // 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::() { + 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::() { + 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 { + self.hex_literal(2) + } + + fn string_hex_4(&mut self) -> Option { + self.hex_literal(4) + } + + fn string_hex_8(&mut self) -> Option { + self.hex_literal(8) + } + + /// Parses a unicode name pattern. + fn string_unicode_name(&mut self) -> Option { + 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 { + 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 { + 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 { + 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 { + self.repeated_with_trailing_comma_vec(Self::literal) + } + + fn unquoted_string_or_built_in(&mut self) -> Option { + 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 { + 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 { + self.repeated_with_trailing_comma(Self::map_entry, HashMap::new(), |map, element| { + map.insert(element.0, element.1); + }) + } + + fn map_literal(&mut self) -> Option { + 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 { + self.repeated_with_trailing_comma_vec(Self::literal) + } + + fn array_prefix(&mut self) -> Option { + 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 { + self.repeated_with_trailing_comma_vec(Self::integer_literal) + } + + fn list_literal(&mut self) -> Option { + 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 { + 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 { + self.literal() + } +} diff --git a/pumpkin/src/command/snbt/tests.rs b/pumpkin/src/command/snbt/tests.rs new file mode 100644 index 000000000..eeea9b7f4 --- /dev/null +++ b/pumpkin/src/command/snbt/tests.rs @@ -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 { + SnbtParser::parse_for_commands(&mut StringReader::new(snbt)) +} + +fn suggestions(snbt: &str) -> Vec { + 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, + [] + ); +} diff --git a/pumpkin/src/command/string_reader.rs b/pumpkin/src/command/string_reader.rs index 0171ca9b8..9e666ac95 100644 --- a/pumpkin/src/command/string_reader.rs +++ b/pumpkin/src/command/string_reader.rs @@ -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 { + 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())); diff --git a/pumpkin/src/command/suggestion/mod.rs b/pumpkin/src/command/suggestion/mod.rs index c4c46a387..66939b5d7 100644 --- a/pumpkin/src/command/suggestion/mod.rs +++ b/pumpkin/src/command/suggestion/mod.rs @@ -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"), diff --git a/pumpkin/src/command/suggestion/suggestions.rs b/pumpkin/src/command/suggestion/suggestions.rs index 7775a0249..943231cb6 100644 --- a/pumpkin/src/command/suggestion/suggestions.rs +++ b/pumpkin/src/command/suggestion/suggestions.rs @@ -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 = 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) -> Vec { + fn sort(suggestions: Vec) -> Vec { 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(); diff --git a/typos.toml b/typos.toml index cc44350a6..529cb9a69 100644 --- a/typos.toml +++ b/typos.toml @@ -6,3 +6,4 @@ handled = "handled" received = "received" ba = "ba" fo = "fo" +undescore = "undescore" \ No newline at end of file