This commit is contained in:
Quaternions 2025-04-11 16:29:42 -07:00
parent 81e7c7afdf
commit 582693fc8f
Signed by: Quaternions
GPG Key ID: D0DF5964F79AC131

@ -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})
}