diff --git a/validation/src/check.rs b/validation/src/check.rs index da861f3..41fb341 100644 --- a/validation/src/check.rs +++ b/validation/src/check.rs @@ -12,6 +12,7 @@ pub enum Error{ CreatorTypeMustBeUser, Download(crate::download::Error), ModelFileDecode(ReadDomError), + GetRootInstance(GetRootInstanceError), } impl std::fmt::Display for Error{ fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ @@ -155,38 +156,29 @@ pub fn get_model_info(dom:&rbx_dom_weak::WeakDom)->Result<ModelInfo,GetRootInsta }) } - - -pub enum Check<Context>{ - Pass, - Fail(Context), -} - // check if an observed string matches and expected string -pub struct StringCheck<'a>(Check<StringCheckContext<'a>>); +pub struct StringCheck<'a,T>(Result<T,StringCheckContext<'a>>); pub struct StringCheckContext<'a>{ observed:&'a str, expected:Cow<'a,str>, } impl<'a> StringCheckContext<'a>{ - fn check(self)->StringCheck<'a>{ + fn check<T>(self,value:T)->StringCheck<'a,T>{ if self.observed==self.expected{ - StringCheck(Check::Pass) + StringCheck(Ok(value)) }else{ - StringCheck(Check::Fail(self)) + StringCheck(Err(self)) } } } // check if a string is empty -pub enum StringEmptyCheck<Context>{ - Empty, - Passed(Context), -} +pub struct StringEmpty; +pub struct StringEmptyCheck<Context>(Result<Context,StringEmpty>); // check for duplicate objects pub struct DuplicateCheckContext<ID>(HashMap<ID,u64>); -pub struct DuplicateCheck<ID>(Check<DuplicateCheckContext<ID>>); +pub struct DuplicateCheck<ID>(Result<(),DuplicateCheckContext<ID>>); impl<ID> DuplicateCheckContext<ID>{ fn check(self)->DuplicateCheck<ID>{ let Self(mut set)=self; @@ -194,9 +186,9 @@ impl<ID> DuplicateCheckContext<ID>{ set.retain(|_,&mut c|c!=1); // if any entries remain, they are incorrect if set.is_empty(){ - DuplicateCheck(Check::Pass) + DuplicateCheck(Ok(())) }else{ - DuplicateCheck(Check::Fail(Self(set))) + DuplicateCheck(Err(Self(set))) } } } @@ -206,7 +198,7 @@ pub struct AtLeastOneMatchingAndNoExtraCheckContext<ID>{ set:HashMap<ID,u64>, missing:HashSet<ID>, } -pub struct AtLeastOneMatchingAndNoExtraCheck<ID>(Check<AtLeastOneMatchingAndNoExtraCheckContext<ID>>); +pub struct AtLeastOneMatchingAndNoExtraCheck<ID>(Result<(),AtLeastOneMatchingAndNoExtraCheckContext<ID>>); impl<ID> AtLeastOneMatchingAndNoExtraCheckContext<ID>{ fn new(set:HashMap<ID,u64>)->Self{ Self{ @@ -227,9 +219,9 @@ impl<ID:Copy+Eq+std::hash::Hash> AtLeastOneMatchingAndNoExtraCheckContext<ID>{ } // if any entries remain, they are incorrect if set.is_empty()&&missing.is_empty(){ - AtLeastOneMatchingAndNoExtraCheck(Check::Pass) + AtLeastOneMatchingAndNoExtraCheck(Ok(())) }else{ - AtLeastOneMatchingAndNoExtraCheck(Check::Fail(Self{set,missing})) + AtLeastOneMatchingAndNoExtraCheck(Err(Self{set,missing})) } } } @@ -242,15 +234,15 @@ pub struct MapInfoOwned{ // crazy! pub struct MapCheck<'a>{ - model_class:StringCheck<'a>, - model_name:StringCheck<'a>, - display_name:Result<StringEmptyCheck<StringCheck<'a>>,StringValueError>, - creator:Result<StringEmptyCheck<()>,StringValueError>, + model_class:StringCheck<'a,()>, + model_name:StringCheck<'a,()>, + display_name:Result<StringEmptyCheck<StringCheck<'a,&'a str>>,StringValueError>, + creator:Result<StringEmptyCheck<&'a str>,StringValueError>, game_id:Result<GameID,ParseGameIDError>, - mapstart:Check<()>, + mapstart:Result<(),()>, mode_start_counts:DuplicateCheck<ModeID>, mode_finish_counts:AtLeastOneMatchingAndNoExtraCheck<ModeID>, - spawn1:Check<()>, + spawn1:Result<(),()>, spawn_counts:DuplicateCheck<SpawnID>, wormhole_out_counts:DuplicateCheck<WormholeOutID>, } @@ -260,32 +252,32 @@ impl<'a> ModelInfo<'a>{ let model_class=StringCheckContext{ observed:self.model_class, expected:Cow::Borrowed("Model"), - }.check(); + }.check(()); let model_name=StringCheckContext{ observed:self.model_name, expected:Cow::Owned(self.model_name.to_snake_case()), - }.check(); + }.check(()); // check display name let display_name=self.map_info.display_name.map(|display_name|{ if display_name.is_empty(){ - StringEmptyCheck::Empty + StringEmptyCheck(Err(StringEmpty)) }else{ let display_name=StringCheckContext{ observed:display_name, expected:Cow::Owned(display_name.to_title_case()), - }.check(); - StringEmptyCheck::Passed(display_name) + }.check(display_name); + StringEmptyCheck(Ok(display_name)) } }); // check Creator let creator=self.map_info.creator.map(|creator|{ if creator.is_empty(){ - StringEmptyCheck::Empty + StringEmptyCheck(Err(StringEmpty)) }else{ - StringEmptyCheck::Passed(()) + StringEmptyCheck(Ok(creator)) } }); @@ -294,16 +286,16 @@ impl<'a> ModelInfo<'a>{ // MapStart must exist let mapstart=if self.counts.mode_start_counts.get(&ModeID::MAIN).is_some(){ - Check::Pass + Ok(()) }else{ - Check::Fail(()) + Err(()) }; // Spawn1 must exist let spawn1=if self.counts.spawn_counts.get(&SpawnID::FIRST).is_some(){ - Check::Pass + Ok(()) }else{ - Check::Fail(()) + Err(()) }; // check that at least one end zone exists for each start zone. @@ -338,17 +330,17 @@ impl<'a> MapCheck<'a>{ fn pass(self)->Result<MapInfoOwned,Self>{ match self{ MapCheck{ - model_class:StringCheck(Check::Pass), - model_name:StringCheck(Check::Pass), - display_name:Ok(StringEmptyCheck::Passed(StringCheck(Check::Pass))), - creator:Ok(StringEmptyCheck::Passed(())), + model_class:StringCheck(Ok(())), + model_name:StringCheck(Ok(())), + display_name:Ok(StringEmptyCheck(Ok(StringCheck(Ok(display_name))))), + creator:Ok(StringEmptyCheck(Ok(creator))), game_id:Ok(game_id), - mapstart, - mode_start_counts, - mode_finish_counts, - spawn1, - spawn_counts, - wormhole_out_counts, + mapstart:Ok(()), + mode_start_counts:DuplicateCheck(Ok(())), + mode_finish_counts:AtLeastOneMatchingAndNoExtraCheck(Ok(())), + spawn1:Ok(()), + spawn_counts:DuplicateCheck(Ok(())), + wormhole_out_counts:DuplicateCheck(Ok(())), }=>{ Ok(MapInfoOwned{ display_name:display_name.to_owned(), @@ -362,7 +354,7 @@ impl<'a> MapCheck<'a>{ } pub struct CheckReportAndVersion{ - pub status:CheckStatus, + pub status:Result<MapInfoOwned,String>, pub version:u64, } @@ -389,7 +381,14 @@ impl crate::message_handler::MessageHandler{ // decode dom (slow!) let dom=maybe_gzip.read_with(read_dom,read_dom).map_err(Error::ModelFileDecode)?; - let status=check(&dom); + // extract information from the model + let model_info=get_model_info(&dom).map_err(Error::GetRootInstance)?; + + // convert the model information into a structured report + let map_check=model_info.check(); + + // check the report, generate an error message if it fails the check + let status=map_check.pass().map_err(|e|e.to_string()); Ok(CheckReportAndVersion{status,version}) }