From 7fdd72ffddcd8cc7ca08e81077cebc5bbd15c9a7 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Wed, 7 Jan 2026 10:12:09 -0800 Subject: [PATCH 1/3] validation: refactor string checks --- validation/src/check.rs | 107 +++++++++++++++++++++++++--------------- 1 file changed, 68 insertions(+), 39 deletions(-) diff --git a/validation/src/check.rs b/validation/src/check.rs index 98952b0..44758c6 100644 --- a/validation/src/check.rs +++ b/validation/src/check.rs @@ -324,25 +324,24 @@ pub fn get_model_info<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&'a rbx_d } // check if an observed string matches an expected string -pub struct StringCheck<'a,T,Str>(Result>); -pub struct StringCheckContext<'a,Str>{ +pub struct StringEquality<'a,Str>{ observed:&'a str, expected:Str, } -impl<'a,Str> StringCheckContext<'a,Str> +impl<'a,Str> StringEquality<'a,Str> where &'a str:PartialEq, { /// Compute the StringCheck, passing through the provided value on success. - fn check(self,value:T)->StringCheck<'a,T,Str>{ + fn check(self,value:T)->Result{ if self.observed==self.expected{ - StringCheck(Ok(value)) + Ok(value) }else{ - StringCheck(Err(self)) + Err(self) } } } -impl std::fmt::Display for StringCheckContext<'_,Str>{ +impl std::fmt::Display for StringEquality<'_,Str>{ fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ write!(f,"expected: {}, observed: {}",self.expected,self.observed) } @@ -464,19 +463,54 @@ impl TryFrom> for MapInfoOwned{ struct Exists; struct Absent; +enum DisplayNameError<'a>{ + TitleCase(StringEquality<'a,String>), + Empty(StringEmpty), + StringValue(StringValueError), +} +fn check_display_name<'a>(display_name:Result<&'a str,StringValueError>)->Result<&'a str,DisplayNameError<'a>>{ + // DisplayName StringValue can be missing or whatever + let display_name=display_name.map_err(DisplayNameError::StringValue)?; + + // DisplayName cannot be "" + let display_name=check_empty(display_name).map_err(DisplayNameError::Empty)?; + + // Check title case + let display_name=StringEquality{ + observed:display_name, + expected:display_name.to_title_case(), + }.check(display_name).map_err(DisplayNameError::TitleCase)?; + + Ok(display_name) +} + +enum CreatorError{ + Empty(StringEmpty), + StringValue(StringValueError), +} +fn check_creator<'a>(creator:Result<&'a str,StringValueError>)->Result<&'a str,CreatorError>{ + // Creator StringValue can be missing or whatever + let creator=creator.map_err(CreatorError::StringValue)?; + + // Creator cannot be "" + let creator=check_empty(creator).map_err(CreatorError::Empty)?; + + Ok(creator) +} + /// The result of every map check. struct MapCheck<'a>{ // === METADATA CHECKS === // The root must be of class Model - model_class:StringCheck<'a,(),&'static str>, + model_class:Result<(),StringEquality<'a,&'static str>>, // Model's name must be in snake case - model_name:StringCheck<'a,(),String>, + model_name:Result<(),StringEquality<'a,String>>, // Map must have a StringValue named DisplayName. // Value must not be empty, must be in title case. - display_name:Result,StringEmpty>,StringValueError>, + display_name:Result<&'a str,DisplayNameError<'a>>, // Map must have a StringValue named Creator. // Value must not be empty. - creator:Result,StringValueError>, + creator:Result<&'a str,CreatorError>, // The prefix of the model's name must match the game it was submitted for. // bhop_ for bhop, and surf_ for surf game_id:Result, @@ -511,27 +545,22 @@ struct MapCheck<'a>{ impl<'a> ModelInfo<'a>{ fn check(self)->MapCheck<'a>{ // Check class is exactly "Model" - let model_class=StringCheckContext{ + let model_class=StringEquality{ observed:self.model_class, expected:"Model", }.check(()); // Check model name is snake case - let model_name=StringCheckContext{ + let model_name=StringEquality{ observed:self.model_name, expected:self.model_name.to_snake_case(), }.check(()); // Check display name is not empty and has title case - let display_name=self.map_info.display_name.map(|display_name|{ - check_empty(display_name).map(|display_name|StringCheckContext{ - observed:display_name, - expected:display_name.to_title_case(), - }.check(display_name)) - }); + let display_name=check_display_name(self.map_info.display_name); // Check Creator is not empty - let creator=self.map_info.creator.map(check_empty); + let creator=check_creator(self.map_info.creator); // Check GameID (model name was prefixed with bhop_ surf_ etc) let game_id=self.map_info.game_id; @@ -630,10 +659,10 @@ impl MapCheck<'_>{ fn result(self)->Result>{ match self{ MapCheck{ - model_class:StringCheck(Ok(())), - model_name:StringCheck(Ok(())), - display_name:Ok(Ok(StringCheck(Ok(display_name)))), - creator:Ok(Ok(creator)), + model_class:Ok(()), + model_name:Ok(()), + display_name:Ok(display_name), + creator:Ok(creator), game_id:Ok(game_id), mapstart:Ok(Exists), mode_start_counts:DuplicateCheck(Ok(())), @@ -737,27 +766,27 @@ macro_rules! summary_format{ impl MapCheck<'_>{ fn itemize(&self)->Result{ let model_class=match &self.model_class{ - StringCheck(Ok(()))=>passed!("ModelClass"), - StringCheck(Err(context))=>summary_format!("ModelClass","Invalid model class: {context}"), + Ok(())=>passed!("ModelClass"), + Err(context)=>summary_format!("ModelClass","Invalid model class: {context}"), }; let model_name=match &self.model_name{ - StringCheck(Ok(()))=>passed!("ModelName"), - StringCheck(Err(context))=>summary_format!("ModelName","Model name must have snake_case: {context}"), + Ok(())=>passed!("ModelName"), + Err(context)=>summary_format!("ModelName","Model name must have snake_case: {context}"), }; let display_name=match &self.display_name{ - Ok(Ok(StringCheck(Ok(_))))=>passed!("DisplayName"), - Ok(Ok(StringCheck(Err(context))))=>summary_format!("DisplayName","DisplayName must have Title Case: {context}"), - Ok(Err(context))=>summary_format!("DisplayName","Invalid DisplayName: {context}"), - Err(StringValueError::ObjectNotFound)=>summary!("DisplayName","Missing DisplayName StringValue".to_owned()), - Err(StringValueError::ValueNotSet)=>summary!("DisplayName","DisplayName Value not set".to_owned()), - Err(StringValueError::NonStringValue)=>summary!("DisplayName","DisplayName Value is not a String".to_owned()), + Ok(_)=>passed!("DisplayName"), + Err(DisplayNameError::TitleCase(context))=>summary_format!("DisplayName","DisplayName must have Title Case: {context}"), + Err(DisplayNameError::Empty(context))=>summary_format!("DisplayName","Invalid DisplayName: {context}"), + Err(DisplayNameError::StringValue(StringValueError::ObjectNotFound))=>summary!("DisplayName","Missing DisplayName StringValue".to_owned()), + Err(DisplayNameError::StringValue(StringValueError::ValueNotSet))=>summary!("DisplayName","DisplayName Value not set".to_owned()), + Err(DisplayNameError::StringValue(StringValueError::NonStringValue))=>summary!("DisplayName","DisplayName Value is not a String".to_owned()), }; let creator=match &self.creator{ - Ok(Ok(_))=>passed!("Creator"), - Ok(Err(context))=>summary_format!("Creator","Invalid Creator: {context}"), - Err(StringValueError::ObjectNotFound)=>summary!("Creator","Missing Creator StringValue".to_owned()), - Err(StringValueError::ValueNotSet)=>summary!("Creator","Creator Value not set".to_owned()), - Err(StringValueError::NonStringValue)=>summary!("Creator","Creator Value is not a String".to_owned()), + Ok(_)=>passed!("Creator"), + Err(CreatorError::Empty(context))=>summary_format!("Creator","Invalid Creator: {context}"), + Err(CreatorError::StringValue(StringValueError::ObjectNotFound))=>summary!("Creator","Missing Creator StringValue".to_owned()), + Err(CreatorError::StringValue(StringValueError::ValueNotSet))=>summary!("Creator","Creator Value not set".to_owned()), + Err(CreatorError::StringValue(StringValueError::NonStringValue))=>summary!("Creator","Creator Value is not a String".to_owned()), }; let game_id=match &self.game_id{ Ok(_)=>passed!("GameID"), -- 2.49.1 From f00b2b8473fab83633095b8deb634e45fdd84665 Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Wed, 7 Jan 2026 10:16:43 -0800 Subject: [PATCH 2/3] validation: impl Display for StringValueError --- validation/src/check.rs | 8 ++------ validation/src/rbx_util.rs | 9 +++++++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/validation/src/check.rs b/validation/src/check.rs index 44758c6..11279c0 100644 --- a/validation/src/check.rs +++ b/validation/src/check.rs @@ -777,16 +777,12 @@ impl MapCheck<'_>{ Ok(_)=>passed!("DisplayName"), Err(DisplayNameError::TitleCase(context))=>summary_format!("DisplayName","DisplayName must have Title Case: {context}"), Err(DisplayNameError::Empty(context))=>summary_format!("DisplayName","Invalid DisplayName: {context}"), - Err(DisplayNameError::StringValue(StringValueError::ObjectNotFound))=>summary!("DisplayName","Missing DisplayName StringValue".to_owned()), - Err(DisplayNameError::StringValue(StringValueError::ValueNotSet))=>summary!("DisplayName","DisplayName Value not set".to_owned()), - Err(DisplayNameError::StringValue(StringValueError::NonStringValue))=>summary!("DisplayName","DisplayName Value is not a String".to_owned()), + Err(DisplayNameError::StringValue(context))=>summary_format!("DisplayName","DisplayName StringValue: {context}"), }; let creator=match &self.creator{ Ok(_)=>passed!("Creator"), Err(CreatorError::Empty(context))=>summary_format!("Creator","Invalid Creator: {context}"), - Err(CreatorError::StringValue(StringValueError::ObjectNotFound))=>summary!("Creator","Missing Creator StringValue".to_owned()), - Err(CreatorError::StringValue(StringValueError::ValueNotSet))=>summary!("Creator","Creator Value not set".to_owned()), - Err(CreatorError::StringValue(StringValueError::NonStringValue))=>summary!("Creator","Creator Value is not a String".to_owned()), + Err(CreatorError::StringValue(context))=>summary_format!("Creator","Creator StringValue: {context}"), }; let game_id=match &self.game_id{ Ok(_)=>passed!("GameID"), diff --git a/validation/src/rbx_util.rs b/validation/src/rbx_util.rs index dbfc086..39287f3 100644 --- a/validation/src/rbx_util.rs +++ b/validation/src/rbx_util.rs @@ -79,6 +79,15 @@ pub enum StringValueError{ ValueNotSet, NonStringValue, } +impl std::fmt::Display for StringValueError{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + match self{ + StringValueError::ObjectNotFound=>write!(f,"Missing StringValue"), + StringValueError::ValueNotSet=>write!(f,"Value not set"), + StringValueError::NonStringValue=>write!(f,"Value is not a String"), + } + } +} fn string_value(instance:Option<&rbx_dom_weak::Instance>)->Result<&str,StringValueError>{ let instance=instance.ok_or(StringValueError::ObjectNotFound)?; -- 2.49.1 From 19a6b0304cc0a56874bec06ece8b5d8dd6fc5d1f Mon Sep 17 00:00:00 2001 From: Rhys Lloyd Date: Wed, 7 Jan 2026 10:29:16 -0800 Subject: [PATCH 3/3] validation: limit DisplayName and Creator to 50 characters --- validation/src/check.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/validation/src/check.rs b/validation/src/check.rs index 11279c0..afcc810 100644 --- a/validation/src/check.rs +++ b/validation/src/check.rs @@ -466,6 +466,7 @@ struct Absent; enum DisplayNameError<'a>{ TitleCase(StringEquality<'a,String>), Empty(StringEmpty), + TooLong(usize), StringValue(StringValueError), } fn check_display_name<'a>(display_name:Result<&'a str,StringValueError>)->Result<&'a str,DisplayNameError<'a>>{ @@ -475,6 +476,11 @@ fn check_display_name<'a>(display_name:Result<&'a str,StringValueError>)->Result // DisplayName cannot be "" let display_name=check_empty(display_name).map_err(DisplayNameError::Empty)?; + // DisplayName cannot exceed 50 characters + if 50(display_name:Result<&'a str,StringValueError>)->Result enum CreatorError{ Empty(StringEmpty), + TooLong(usize), StringValue(StringValueError), } fn check_creator<'a>(creator:Result<&'a str,StringValueError>)->Result<&'a str,CreatorError>{ @@ -495,6 +502,11 @@ fn check_creator<'a>(creator:Result<&'a str,StringValueError>)->Result<&'a str,C // Creator cannot be "" let creator=check_empty(creator).map_err(CreatorError::Empty)?; + // Creator cannot exceed 50 characters + if 50{ Ok(_)=>passed!("DisplayName"), Err(DisplayNameError::TitleCase(context))=>summary_format!("DisplayName","DisplayName must have Title Case: {context}"), Err(DisplayNameError::Empty(context))=>summary_format!("DisplayName","Invalid DisplayName: {context}"), + Err(DisplayNameError::TooLong(context))=>summary_format!("DisplayName","DisplayName is too long: {context} characters (50 characters max)"), Err(DisplayNameError::StringValue(context))=>summary_format!("DisplayName","DisplayName StringValue: {context}"), }; let creator=match &self.creator{ Ok(_)=>passed!("Creator"), Err(CreatorError::Empty(context))=>summary_format!("Creator","Invalid Creator: {context}"), + Err(CreatorError::TooLong(context))=>summary_format!("Creator","Creator is too long: {context} characters (50 characters max)"), Err(CreatorError::StringValue(context))=>summary_format!("Creator","Creator StringValue: {context}"), }; let game_id=match &self.game_id{ -- 2.49.1