From 7d0c254874f4747dc3df26935990275f9c94605e Mon Sep 17 00:00:00 2001 From: Ryan Johnson Date: Wed, 12 Oct 2022 03:53:59 -0700 Subject: [PATCH] Rewrite ident module (again) (#111) Ident is now a wrapper around any string type `S`. --- Cargo.toml | 3 +- src/biome.rs | 12 +- src/client.rs | 2 +- src/dimension.rs | 4 +- src/ident.rs | 374 +++++++++++++++++------------------- src/protocol.rs | 2 +- src/protocol/packets/c2s.rs | 14 +- src/protocol/packets/s2c.rs | 20 +- src/text.rs | 7 +- 9 files changed, 207 insertions(+), 231 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c7a2415..8019ed0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,6 @@ aes = "0.7.5" anyhow = "1.0.65" approx = "0.5.1" arrayvec = "0.7.2" -ascii = "1.1.0" async-trait = "0.1.57" base64 = "0.13.0" bitfield-struct = "0.1.7" @@ -36,12 +35,12 @@ rsa = "0.6.1" rsa-der = "0.3.0" serde = { version = "1.0.145", features = ["derive"] } serde_json = "1.0.85" -valence_nbt = "0.2.0" sha1 = "0.10.5" sha2 = "0.10.6" thiserror = "1.0.35" url = { version = "2.2.2", features = ["serde"] } uuid = { version = "1.1.2", features = ["serde"] } +valence_nbt = "0.2.0" vek = "0.15.8" [dependencies.tokio] diff --git a/src/biome.rs b/src/biome.rs index db3deae..882aa9a 100644 --- a/src/biome.rs +++ b/src/biome.rs @@ -26,7 +26,7 @@ pub struct BiomeId(pub(crate) u16); pub struct Biome { /// The unique name for this biome. The name can be /// seen in the F3 debug menu. - pub name: Ident<'static>, + pub name: Ident, pub precipitation: BiomePrecipitation, pub sky_color: u32, pub water_fog_color: u32, @@ -36,7 +36,7 @@ pub struct Biome { pub grass_color: Option, pub grass_color_modifier: BiomeGrassColorModifier, pub music: Option, - pub ambient_sound: Option>, + pub ambient_sound: Option>, pub additions_sound: Option, pub mood_sound: Option, pub particle: Option, @@ -202,20 +202,20 @@ pub enum BiomeGrassColorModifier { #[derive(Clone, Debug)] pub struct BiomeMusic { pub replace_current_music: bool, - pub sound: Ident<'static>, + pub sound: Ident, pub min_delay: i32, pub max_delay: i32, } #[derive(Clone, Debug)] pub struct BiomeAdditionsSound { - pub sound: Ident<'static>, + pub sound: Ident, pub tick_chance: f64, } #[derive(Clone, Debug)] pub struct BiomeMoodSound { - pub sound: Ident<'static>, + pub sound: Ident, pub tick_delay: i32, pub offset: f64, pub block_search_extent: i32, @@ -224,5 +224,5 @@ pub struct BiomeMoodSound { #[derive(Clone, Debug)] pub struct BiomeParticle { pub probability: f32, - pub kind: Ident<'static>, + pub kind: Ident, } diff --git a/src/client.rs b/src/client.rs index ffa148c..6ef5f38 100644 --- a/src/client.rs +++ b/src/client.rs @@ -505,7 +505,7 @@ impl Client { /// Plays a sound to the client at a given position. pub fn play_sound( &mut self, - name: Ident<'static>, + name: Ident, category: SoundCategory, pos: Vec3, volume: f32, diff --git a/src/dimension.rs b/src/dimension.rs index 68c1c2f..135ab71 100644 --- a/src/dimension.rs +++ b/src/dimension.rs @@ -17,11 +17,11 @@ use crate::{ident, LIBRARY_NAMESPACE}; pub struct DimensionId(pub(crate) u16); impl DimensionId { - pub(crate) fn dimension_type_name(self) -> Ident<'static> { + pub(crate) fn dimension_type_name(self) -> Ident { ident!("{LIBRARY_NAMESPACE}:dimension_type_{}", self.0) } - pub(crate) fn dimension_name(self) -> Ident<'static> { + pub(crate) fn dimension_name(self) -> Ident { ident!("{LIBRARY_NAMESPACE}:dimension_{}", self.0) } } diff --git a/src/ident.rs b/src/ident.rs index 765bded..ca16413 100644 --- a/src/ident.rs +++ b/src/ident.rs @@ -1,20 +1,23 @@ //! Resource identifiers. -use std::borrow::Cow; +use std::borrow::Borrow; use std::cmp::Ordering; +use std::error::Error; +use std::fmt; +use std::fmt::Formatter; +use std::hash::{Hash, Hasher}; use std::io::Write; use std::str::FromStr; -use std::{fmt, hash}; -use ascii::{AsAsciiStr, AsciiChar, AsciiStr, IntoAsciiString}; -use hash::Hash; -use serde::de::Visitor; -use serde::{de, Deserialize, Deserializer, Serialize}; -use thiserror::Error; +use serde::de::Error as _; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::nbt; use crate::protocol::{Decode, Encode}; +/// A wrapper around a string type `S` which guarantees the wrapped string is a +/// valid resource identifier. +/// /// A resource identifier is a string divided into a "namespace" part and a /// "path" part. For instance `minecraft:apple` and `valence:frobnicator` are /// both valid identifiers. @@ -25,270 +28,267 @@ use crate::protocol::{Decode, Encode}; /// /// A string must match the regex `^([a-z0-9_-]+:)?[a-z0-9_\/.-]+$` to be a /// valid identifier. -#[derive(Clone, Eq)] -pub struct Ident<'a> { - string: Cow<'a, AsciiStr>, - /// The index of the first character of the path part in the string. +#[derive(Copy, Clone, Debug)] +pub struct Ident { + string: S, path_start: usize, } -/// The error type created when an [`Ident`] cannot be parsed from a -/// string. Contains the offending string. -#[derive(Clone, Debug, Error)] -#[error("invalid resource identifier \"{0}\"")] -pub struct IdentParseError<'a>(pub Cow<'a, str>); - -impl<'a> Ident<'a> { - /// Parses a new identifier from a string. - /// - /// An error is returned containing the input string if it is not a valid - /// resource identifier. - pub fn new(string: impl Into>) -> Result, IdentParseError<'a>> { - #![allow(bindings_with_variant_name)] - - let cow = match string.into() { - Cow::Borrowed(s) => { - Cow::Borrowed(s.as_ascii_str().map_err(|_| IdentParseError(s.into()))?) - } - Cow::Owned(s) => Cow::Owned( - s.into_ascii_string() - .map_err(|e| IdentParseError(e.into_source().into()))?, - ), - }; - - let str = cow.as_ref(); - - let check_namespace = |s: &AsciiStr| { +impl> Ident { + pub fn new(string: S) -> Result> { + let check_namespace = |s: &str| { !s.is_empty() && s.chars() - .all(|c| matches!(c.as_char(), 'a'..='z' | '0'..='9' | '_' | '-')) + .all(|c| matches!(c, 'a'..='z' | '0'..='9' | '_' | '-')) }; - let check_path = |s: &AsciiStr| { + let check_path = |s: &str| { !s.is_empty() && s.chars() - .all(|c| matches!(c.as_char(), 'a'..='z' | '0'..='9' | '_' | '/' | '.' | '-')) + .all(|c| matches!(c, 'a'..='z' | '0'..='9' | '_' | '/' | '.' | '-')) }; - match str.chars().position(|c| c == AsciiChar::Colon) { - Some(colon_idx) - if check_namespace(&str[..colon_idx]) && check_path(&str[colon_idx + 1..]) => - { - Ok(Self { - string: cow, - path_start: colon_idx + 1, - }) + let str = string.borrow(); + + match str.split_once(':') { + Some((namespace, path)) if check_namespace(namespace) && check_path(path) => { + let path_start = namespace.len() + 1; + Ok(Self { string, path_start }) } None if check_path(str) => Ok(Self { - string: cow, + string, path_start: 0, }), - _ => Err(IdentParseError(ascii_cow_to_str_cow(cow))), + _ => Err(IdentError(string)), } } /// Returns the namespace part of this resource identifier. /// - /// If this identifier was constructed from a string without a namespace, - /// then "minecraft" is returned. + /// If the underlying string does not contain a namespace followed by a + /// ':' character, `"minecraft"` is returned. pub fn namespace(&self) -> &str { if self.path_start == 0 { "minecraft" } else { - self.string[..self.path_start - 1].as_str() + &self.string.borrow()[..self.path_start - 1] } } - /// Returns the path part of this resource identifier. pub fn path(&self) -> &str { - self.string[self.path_start..].as_str() + &self.string.borrow()[self.path_start..] } /// Returns the underlying string as a `str`. pub fn as_str(&self) -> &str { - self.string.as_str() + self.string.borrow() + } + + /// Borrows the underlying string and returns it as an `Ident`. This + /// operation is infallible and no checks need to be performed. + pub fn as_str_ident(&self) -> Ident<&str> { + Ident { + string: self.string.borrow(), + path_start: self.path_start, + } + } + + /// Converts the underlying string to its owned representation and returns + /// it as an `Ident`. This operation is infallible and no checks need to be + /// performed. + pub fn to_owned_ident(&self) -> Ident + where + S: ToOwned, + S::Owned: Borrow, + { + Ident { + string: self.string.to_owned(), + path_start: self.path_start, + } } /// Consumes the identifier and returns the underlying string. - pub fn into_inner(self) -> Cow<'a, str> { - ascii_cow_to_str_cow(self.string) - } - - /// Used as the argument to `#[serde(deserialize_with = "...")]` when you - /// don't want to borrow data from the `'de` lifetime. - pub fn deserialize_to_owned<'de, D>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - Ident::new(String::deserialize(deserializer)?).map_err(de::Error::custom) + pub fn into_inner(self) -> S { + self.string } } -fn ascii_cow_to_str_cow(cow: Cow) -> Cow { - match cow { - Cow::Borrowed(s) => Cow::Borrowed(s.as_str()), - Cow::Owned(s) => Cow::Owned(s.into()), +/// The error type created when an [`Ident`] cannot be parsed from a +/// string. Contains the offending string. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct IdentError(pub S); + +impl fmt::Debug for IdentError +where + S: Borrow, +{ + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.debug_tuple("IdentError").field(&self.0.borrow()).finish() } } -impl<'a> fmt::Debug for Ident<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("Ident").field(&self.as_str()).finish() +impl fmt::Display for IdentError +where + S: Borrow, +{ + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "invalid resource identifier \"{}\"", self.0.borrow()) } } -impl<'a> FromStr for Ident<'a> { - type Err = IdentParseError<'a>; +impl Error for IdentError where S: Borrow {} + +impl FromStr for Ident { + type Err = IdentError; fn from_str(s: &str) -> Result { Ident::new(s.to_owned()) } } -impl<'a> From> for String { - fn from(id: Ident) -> Self { - id.string.into_owned().into() - } -} - -impl<'a> From> for Cow<'a, str> { - fn from(id: Ident<'a>) -> Self { - id.into_inner() - } -} - -impl<'a> AsRef for Ident<'a> { - fn as_ref(&self) -> &str { - self.as_str() - } -} - -impl<'a, 'b> PartialEq> for Ident<'a> { - fn eq(&self, other: &Ident<'b>) -> bool { - (self.namespace(), self.path()) == (other.namespace(), other.path()) - } -} - -impl<'a, 'b> PartialOrd> for Ident<'a> { - fn partial_cmp(&self, other: &Ident<'b>) -> Option { - (self.namespace(), self.path()).partial_cmp(&(other.namespace(), other.path())) - } -} - -impl<'a> Hash for Ident<'a> { - fn hash(&self, state: &mut H) { - self.namespace().hash(state); - self.path().hash(state); - } -} - -impl<'a> fmt::Display for Ident<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}:{}", self.namespace(), self.path()) - } -} - -impl<'a> TryFrom for Ident<'a> { - type Error = IdentParseError<'a>; +impl TryFrom for Ident { + type Error = IdentError; fn try_from(value: String) -> Result { Ident::new(value) } } -impl<'a> TryFrom<&'a str> for Ident<'a> { - type Error = IdentParseError<'a>; - - fn try_from(value: &'a str) -> Result { - Ident::new(value) +impl From> for String +where + S: Into + Borrow, +{ + fn from(id: Ident) -> Self { + if id.path_start == 0 { + format!("minecraft:{}", id.string.borrow()) + } else { + id.string.into() + } } } -impl<'a> From> for nbt::Value { - fn from(id: Ident<'a>) -> Self { - String::from(id).into() +impl> fmt::Display for Ident { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}", self.namespace(), self.path()) } } -impl<'a> Encode for Ident<'a> { - fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { - self.as_str().encode(w) +impl PartialEq> for Ident +where + S: Borrow, + T: Borrow, +{ + fn eq(&self, other: &Ident) -> bool { + self.namespace() == other.namespace() && self.path() == other.path() } } -impl<'a> Decode for Ident<'a> { - fn decode(r: &mut &[u8]) -> anyhow::Result { - Ok(Ident::new(String::decode(r)?)?) +impl Eq for Ident where S: Borrow {} + +impl PartialOrd> for Ident +where + S: Borrow, + T: Borrow, +{ + fn partial_cmp(&self, other: &Ident) -> Option { + (self.namespace(), self.path()).partial_cmp(&(other.namespace(), other.path())) } } -impl<'a> Serialize for Ident<'a> { - fn serialize(&self, serializer: S) -> Result { - self.as_str().serialize(serializer) +impl Ord for Ident +where + S: Borrow, +{ + fn cmp(&self, other: &Self) -> Ordering { + (self.namespace(), self.path()).cmp(&(other.namespace(), other.path())) } } -/// This uses borrowed data from the `'de` lifetime. If you just want owned -/// data, see [`Ident::deserialize_to_owned`]. -impl<'de> Deserialize<'de> for Ident<'de> { - fn deserialize>(deserializer: D) -> Result { - deserializer.deserialize_string(IdentVisitor) +impl Hash for Ident +where + S: Borrow, +{ + fn hash(&self, state: &mut H) { + (self.namespace(), self.path()).hash(state); } } -struct IdentVisitor; - -impl<'de> Visitor<'de> for IdentVisitor { - type Value = Ident<'de>; - - fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "a valid Minecraft resource identifier") - } - - fn visit_str(self, s: &str) -> Result { - Ident::from_str(s).map_err(E::custom) - } - - fn visit_borrowed_str(self, v: &'de str) -> Result +impl Serialize for Ident +where + T: Serialize, +{ + fn serialize(&self, serializer: S) -> Result where - E: de::Error, + S: Serializer, { - Ident::new(v).map_err(E::custom) - } - - fn visit_string(self, s: String) -> Result { - Ident::new(s).map_err(E::custom) + self.string.serialize(serializer) } } -/// Convenience macro for constructing an [`Ident`] from a format string. +impl<'de, T> Deserialize<'de> for Ident +where + T: Deserialize<'de> + Borrow, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ident::new(T::deserialize(deserializer)?).map_err(D::Error::custom) + } +} + +impl Encode for Ident { + fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { + self.string.encode(w) + } +} + +impl Decode for Ident +where + S: Decode + Borrow + Send + Sync + 'static, +{ + fn decode(r: &mut &[u8]) -> anyhow::Result { + Ok(Ident::new(S::decode(r)?)?) + } +} + +impl From> for nbt::Value +where + S: Into, +{ + fn from(id: Ident) -> Self { + id.string.into() + } +} + +/// Convenience macro for constructing an [`Ident`] from a format +/// string. /// -/// The arguments to this macro are forwarded to [`std::format_args`]. +/// The arguments to this macro are forwarded to [`std::format`]. /// /// # Panics /// -/// The macro will cause a panic if the formatted string is not a valid +/// The macro will cause a panic if the formatted string is not a valid resource /// identifier. See [`Ident`] for more information. /// +/// [`Ident`]: [Ident] +/// /// # Examples /// /// ``` /// use valence::ident; /// /// let namespace = "my_namespace"; -/// let path = ident!("{namespace}:my_path"); +/// let path = "my_path"; /// -/// assert_eq!(path.namespace(), "my_namespace"); -/// assert_eq!(path.path(), "my_path"); +/// let id = ident!("{namespace}:{path}"); +/// +/// assert_eq!(id.namespace(), "my_namespace"); +/// assert_eq!(id.path(), "my_path"); /// ``` #[macro_export] macro_rules! ident { ($($arg:tt)*) => {{ - let errmsg = "invalid resource identifier in `ident` macro"; - #[allow(clippy::redundant_closure_call)] - (|args: ::std::fmt::Arguments| match args.as_str() { - Some(s) => $crate::ident::Ident::new(s).expect(errmsg), - None => $crate::ident::Ident::new(args.to_string()).expect(errmsg), - })(format_args!($($arg)*)) + $crate::ident::Ident::new(::std::format!($($arg)*)).unwrap() }} } @@ -346,26 +346,4 @@ mod tests { assert_eq!(h1.finish(), h2.finish()); } - - fn check_borrowed(id: Ident) { - if let Cow::Owned(_) = id.into_inner() { - panic!("not borrowed!"); - } - } - - #[test] - fn literal_is_borrowed() { - check_borrowed(ident!("akjghsjkhebf")); - } - - #[test] - fn visit_borrowed_str_borrows() { - let data = String::from("valence:frobnicator"); - - check_borrowed( - IdentVisitor - .visit_borrowed_str::(data.as_ref()) - .unwrap(), - ); - } } diff --git a/src/protocol.rs b/src/protocol.rs index 0c7a323..565ab8c 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -52,7 +52,7 @@ pub trait Decode: Sized { } /// The maximum number of bytes in a single packet. -pub const MAX_PACKET_SIZE: i32 = 2097151; +pub const MAX_PACKET_SIZE: i32 = 2097152; impl Encode for () { fn encode(&self, _w: &mut impl Write) -> anyhow::Result<()> { diff --git a/src/protocol/packets/c2s.rs b/src/protocol/packets/c2s.rs index fcce0aa..202d75f 100644 --- a/src/protocol/packets/c2s.rs +++ b/src/protocol/packets/c2s.rs @@ -281,7 +281,7 @@ pub mod play { def_struct! { PluginMessageC2s { - channel: Ident<'static>, + channel: Ident, data: RawBytes, } } @@ -407,7 +407,7 @@ pub mod play { def_struct! { PlaceRecipe { window_id: i8, - recipe: Ident<'static>, + recipe: Ident, make_all: bool, } } @@ -520,7 +520,7 @@ pub mod play { def_struct! { SetSeenRecipe { - recipe_id: Ident<'static>, + recipe_id: Ident, } } @@ -542,7 +542,7 @@ pub mod play { def_enum! { SeenAdvancements: VarInt { - OpenedTab: Ident<'static> = 0, + OpenedTab: Ident = 0, ClosedScreen = 1, } } @@ -610,9 +610,9 @@ pub mod play { def_struct! { ProgramJigsawBlock { location: BlockPos, - name: Ident<'static>, - target: Ident<'static>, - pool: Ident<'static>, + name: Ident, + target: Ident, + pool: Ident, final_state: String, joint_type: String, } diff --git a/src/protocol/packets/s2c.rs b/src/protocol/packets/s2c.rs index b6ea24a..b5edad6 100644 --- a/src/protocol/packets/s2c.rs +++ b/src/protocol/packets/s2c.rs @@ -62,7 +62,7 @@ pub mod login { def_struct! { LoginPluginRequest { message_id: VarInt, - channel: Ident<'static>, + channel: Ident, data: RawBytes, } } @@ -277,7 +277,7 @@ pub mod play { def_struct! { CustomSoundEffect { - name: Ident<'static>, + name: Ident, category: SoundCategory, position: Vec3, volume: f32, @@ -388,13 +388,13 @@ pub mod play { is_hardcore: bool, gamemode: GameMode, previous_gamemode: GameMode, - dimension_names: Vec>, + dimension_names: Vec>, /// Contains information about dimensions, biomes, and chats. registry_codec: Compound, /// The name of the dimension type being spawned into. - dimension_type_name: Ident<'static>, + dimension_type_name: Ident, /// The name of the dimension being spawned into. - dimension_name: Ident<'static>, + dimension_name: Ident, /// Hash of the world's seed used for client biome noise. hashed_seed: i64, /// No longer used by the client. @@ -409,7 +409,7 @@ pub mod play { /// If this is a superflat world. /// Superflat worlds have different void fog and horizon levels. is_flat: bool, - last_death_location: Option<(Ident<'static>, BlockPos)>, + last_death_location: Option<(Ident, BlockPos)>, } } @@ -529,15 +529,15 @@ pub mod play { def_struct! { Respawn { - dimension_type_name: Ident<'static>, - dimension_name: Ident<'static>, + dimension_type_name: Ident, + dimension_name: Ident, hashed_seed: u64, game_mode: GameMode, previous_game_mode: GameMode, is_debug: bool, is_flat: bool, copy_metadata: bool, - last_death_location: Option<(Ident<'static>, BlockPos)>, + last_death_location: Option<(Ident, BlockPos)>, } } @@ -708,7 +708,7 @@ pub mod play { def_struct! { EntityAttributesProperty { - key: Ident<'static>, + key: Ident, value: f64, modifiers: Vec } diff --git a/src/text.rs b/src/text.rs index d05fda5..61d5723 100644 --- a/src/text.rs +++ b/src/text.rs @@ -378,15 +378,14 @@ enum ClickEvent { enum HoverEvent { ShowText(Box), ShowItem { - #[serde(deserialize_with = "Ident::deserialize_to_owned")] - id: Ident<'static>, + id: Ident, count: Option, // TODO: tag }, ShowEntity { name: Box, - #[serde(rename = "type", deserialize_with = "Ident::deserialize_to_owned")] - kind: Ident<'static>, + #[serde(rename = "type")] + kind: Ident, // TODO: id (hyphenated entity UUID as a string) }, }