maps-service/validation/src/validator.rs

304 lines
9.3 KiB
Rust
Raw Normal View History

use futures::TryStreamExt;
2024-12-03 06:52:01 +00:00
use crate::nats_types::ValidateRequest;
2024-12-18 02:33:02 +00:00
use submissions_api::internal::ScriptPolicyResponse;
2024-12-14 08:15:05 +00:00
2024-12-07 02:57:26 +00:00
const SCRIPT_CONCURRENCY:usize=16;
enum Policy{
2024-12-14 05:50:19 +00:00
None,
Allowed,
Blocked,
Delete,
Replace(String),
}
2024-12-14 20:31:31 +00:00
struct NamePolicy{
name:String,
policy:Policy,
}
2024-12-14 08:15:05 +00:00
// this is awful
fn interpret_get_script_policy_response(reponse:Result<ScriptPolicyResponse,submissions_api::Error>)->Result<Option<ScriptPolicyResponse>,submissions_api::Error>{
match reponse{
Ok(script_policy)=>Ok(Some(script_policy)),
Err(e)=>{
if let submissions_api::Error::Response(submissions_api::ResponseError::StatusCodeWithUrlAndBody(s))=&e{
if s.status_code.as_u16()==404{
// wew we figured out that the resource does not exist
return Ok(None);
2024-12-14 08:15:05 +00:00
}
}
return Err(e);
}
}
}
2024-12-03 06:52:01 +00:00
#[allow(dead_code)]
#[derive(Debug)]
pub enum ValidateError{
Blocked,
NotAllowed,
2024-12-03 06:52:01 +00:00
Get(rbx_asset::cookie::GetError),
ReadDom(ReadDomError),
2024-12-14 11:31:19 +00:00
ApiGetScriptPolicy(submissions_api::Error),
ApiGetScript(submissions_api::Error),
2024-12-14 08:15:05 +00:00
ApiCreateScript(submissions_api::Error),
ApiCreateScriptPolicy(submissions_api::Error),
2024-12-14 11:31:19 +00:00
ApiUpdateSubmissionModel(submissions_api::Error),
ApiActionSubmissionValidate(submissions_api::Error),
2024-12-04 02:54:01 +00:00
WriteDom(rbx_binary::EncodeError),
Upload(rbx_asset::cookie::UploadError),
Create(rbx_asset::cookie::CreateError),
2024-12-03 06:09:01 +00:00
}
2024-12-03 06:52:01 +00:00
impl std::fmt::Display for ValidateError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for ValidateError{}
2024-12-03 06:09:01 +00:00
pub struct Validator{
2024-12-03 06:52:01 +00:00
roblox_cookie:rbx_asset::cookie::CookieContext,
2024-12-18 02:33:02 +00:00
api:submissions_api::internal::Context,
2024-12-03 06:09:01 +00:00
}
2024-12-03 06:52:01 +00:00
2024-12-03 06:09:01 +00:00
impl Validator{
pub const fn new(
2024-12-03 06:52:01 +00:00
roblox_cookie:rbx_asset::cookie::CookieContext,
2024-12-18 02:33:02 +00:00
api:submissions_api::internal::Context,
)->Self{
Self{
2024-12-03 06:52:01 +00:00
roblox_cookie,
api,
2024-12-03 06:09:01 +00:00
}
}
2024-12-18 04:30:31 +00:00
pub async fn validate(&self,validate_info:ValidateRequest)->Result<(),ValidateError>{
let submission_id=validate_info.SubmissionID;
let validate_result=self.validate_inner(validate_info).await;
2024-12-03 06:52:01 +00:00
2024-12-18 04:30:31 +00:00
// update the submission depending on the result
match &validate_result{
Ok(())=>{
// update the submission model status to validated
self.api.action_submission_validated(
submissions_api::internal::SubmissionID(submission_id)
).await.map_err(ValidateError::ApiActionSubmissionValidate)?;
},
Err(_)=>{
// update the submission model status to accepted
self.api.action_submission_accepted(
submissions_api::internal::SubmissionID(submission_id)
).await.map_err(ValidateError::ApiActionSubmissionValidate)?;
},
}
validate_result
}
pub async fn validate_inner(&self,validate_info:ValidateRequest)->Result<(),ValidateError>{
2024-12-03 06:09:01 +00:00
// download map
2024-12-03 06:52:01 +00:00
let data=self.roblox_cookie.get_asset(rbx_asset::cookie::GetAssetRequest{
asset_id:validate_info.ModelID,
version:Some(validate_info.ModelVersion),
2024-12-03 06:52:01 +00:00
}).await.map_err(ValidateError::Get)?;
// decode dom (slow!)
let mut dom=read_dom(&mut std::io::Cursor::new(data)).map_err(ValidateError::ReadDom)?;
2024-12-04 02:54:01 +00:00
/* VALIDATE MAP */
// collect unique scripts
let script_refs=get_script_refs(&dom);
2024-12-14 20:31:31 +00:00
let mut script_map=std::collections::HashMap::<String,NamePolicy>::new();
2024-12-04 02:54:01 +00:00
for &script_ref in &script_refs{
if let Some(script)=dom.get_by_ref(script_ref){
if let Some(rbx_dom_weak::types::Variant::String(source))=script.properties.get("Source"){
2024-12-14 20:31:31 +00:00
script_map.insert(source.clone(),NamePolicy{
name:script.name.clone(),
policy:Policy::None,
});
2024-12-04 02:54:01 +00:00
}
}
}
2024-12-07 02:57:26 +00:00
// send all script hashes to REST endpoint and retrieve the replacements
2024-12-07 02:57:26 +00:00
futures::stream::iter(script_map.iter_mut().map(Ok))
2024-12-14 20:31:31 +00:00
.try_for_each_concurrent(Some(SCRIPT_CONCURRENCY),|(source,NamePolicy{policy,name})|async{
2024-12-07 02:57:26 +00:00
// get the hash
2024-12-07 02:57:11 +00:00
let mut hasher=siphasher::sip::SipHasher::new();
std::hash::Hasher::write(&mut hasher,source.as_bytes());
let hash=std::hash::Hasher::finish(&hasher);
2024-12-07 02:57:26 +00:00
// fetch the script policy
2024-12-14 08:15:05 +00:00
let script_policy=interpret_get_script_policy_response(
2024-12-18 02:33:02 +00:00
self.api.get_script_policy_from_hash(submissions_api::internal::ScriptPolicyHashRequest{
2024-12-17 23:57:37 +00:00
hash:format!("{:016x}",hash).as_str(),
2024-12-14 08:15:05 +00:00
}).await
).map_err(ValidateError::ApiGetScriptPolicy)?;
2024-12-07 02:57:26 +00:00
// write the policy to the script_map, fetching the replacement code if necessary
2024-12-14 08:15:05 +00:00
if let Some(script_policy)=script_policy{
2024-12-14 20:31:31 +00:00
*policy=match script_policy.Policy{
2024-12-18 02:33:02 +00:00
submissions_api::internal::Policy::None=>Policy::None,
submissions_api::internal::Policy::Allowed=>Policy::Allowed,
submissions_api::internal::Policy::Blocked=>Policy::Blocked,
submissions_api::internal::Policy::Delete=>Policy::Delete,
submissions_api::internal::Policy::Replace=>{
let script=self.api.get_script(submissions_api::internal::GetScriptRequest{
2024-12-14 08:15:05 +00:00
ScriptID:script_policy.ToScriptID,
}).await.map_err(ValidateError::ApiGetScript)?;
Policy::Replace(script.Source)
},
};
}else{
// upload the script
2024-12-18 02:33:02 +00:00
let script=self.api.create_script(submissions_api::internal::CreateScriptRequest{
2024-12-14 20:31:31 +00:00
Name:name.as_str(),
Source:source.as_str(),
2024-12-14 08:15:05 +00:00
SubmissionID:Some(validate_info.SubmissionID),
}).await.map_err(ValidateError::ApiCreateScript)?;
// create a None policy (pending review by yours truly)
2024-12-18 02:33:02 +00:00
self.api.create_script_policy(submissions_api::internal::CreateScriptPolicyRequest{
2024-12-14 08:15:05 +00:00
ToScriptID:script.ID,
FromScriptID:script.ID,
2024-12-18 02:33:02 +00:00
Policy:submissions_api::internal::Policy::None,
2024-12-14 08:15:05 +00:00
}).await.map_err(ValidateError::ApiCreateScriptPolicy)?;
}
2024-12-07 02:57:26 +00:00
Ok(())
})
.await?;
2024-12-04 02:54:01 +00:00
// make the replacements
let mut modified=false;
2024-12-04 02:54:01 +00:00
for &script_ref in &script_refs{
if let Some(script)=dom.get_by_ref_mut(script_ref){
if let Some(rbx_dom_weak::types::Variant::String(source))=script.properties.get_mut("Source"){
2024-12-14 20:31:31 +00:00
match script_map.get(source.as_str()).map(|p|&p.policy){
Some(Policy::Blocked)=>return Err(ValidateError::Blocked),
2024-12-14 05:50:19 +00:00
None
|Some(Policy::None)
=>return Err(ValidateError::NotAllowed),
Some(Policy::Allowed)=>(),
Some(Policy::Delete)=>{
modified=true;
// delete script
dom.destroy(script_ref);
},
Some(Policy::Replace(replacement))=>{
modified=true;
*source=replacement.clone();
},
2024-12-04 02:54:01 +00:00
}
}
}
}
2024-12-03 06:52:01 +00:00
2024-12-07 03:45:44 +00:00
// if the model was validated, the submission must be changed to use the modified model
if modified{
2024-12-04 02:54:01 +00:00
// serialize model (slow!)
let mut data=Vec::new();
2024-12-14 03:58:16 +00:00
rbx_binary::to_writer(&mut data,&dom,dom.root().children()).map_err(ValidateError::WriteDom)?;
2024-12-04 02:54:01 +00:00
// upload a model lol
2024-12-14 03:57:51 +00:00
let model_id=if let Some(model_id)=validate_info.ValidatedModelID{
2024-12-04 02:54:01 +00:00
// upload to existing id
let response=self.roblox_cookie.upload(rbx_asset::cookie::UploadRequest{
assetid:model_id,
name:None,
description:None,
ispublic:None,
allowComments:None,
groupId:None,
},data).await.map_err(ValidateError::Upload)?;
2024-12-14 03:57:51 +00:00
response.AssetId
2024-12-04 02:54:01 +00:00
}else{
// create new model
let response=self.roblox_cookie.create(rbx_asset::cookie::CreateRequest{
name:dom.root().name.clone(),
2024-12-04 02:54:01 +00:00
description:"".to_owned(),
ispublic:true,
allowComments:true,
groupId:None,
},data).await.map_err(ValidateError::Create)?;
2024-12-14 03:57:51 +00:00
response.AssetId
2024-12-04 02:54:01 +00:00
};
2024-12-07 03:45:44 +00:00
// update the submission to use the validated model
2024-12-18 02:33:02 +00:00
self.api.update_submission_model(submissions_api::internal::UpdateSubmissionModelRequest{
2024-12-14 20:44:56 +00:00
SubmissionID:validate_info.SubmissionID,
2024-12-07 03:45:44 +00:00
ModelID:model_id,
2024-12-14 03:57:51 +00:00
ModelVersion:1, //TODO
2024-12-07 03:45:44 +00:00
}).await.map_err(ValidateError::ApiUpdateSubmissionModel)?;
};
Ok(())
2024-12-03 06:52:01 +00:00
}
}
#[allow(dead_code)]
#[derive(Debug)]
pub enum ReadDomError{
2024-12-03 06:52:01 +00:00
Binary(rbx_binary::DecodeError),
Xml(rbx_xml::DecodeError),
Read(std::io::Error),
Seek(std::io::Error),
UnknownFormat([u8;8]),
}
impl std::fmt::Display for ReadDomError{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
write!(f,"{self:?}")
}
}
impl std::error::Error for ReadDomError{}
fn read_dom<R:std::io::Read+std::io::Seek>(input:&mut R)->Result<rbx_dom_weak::WeakDom,ReadDomError>{
let mut first_8=[0u8;8];
std::io::Read::read_exact(input,&mut first_8).map_err(ReadDomError::Read)?;
std::io::Seek::rewind(input).map_err(ReadDomError::Seek)?;
match &first_8[0..4]{
b"<rob"=>{
match &first_8[4..8]{
b"lox!"=>rbx_binary::from_reader(input).map_err(ReadDomError::Binary),
b"lox "=>rbx_xml::from_reader(input,rbx_xml::DecodeOptions::default()).map_err(ReadDomError::Xml),
_=>Err(ReadDomError::UnknownFormat(first_8)),
}
},
_=>Err(ReadDomError::UnknownFormat(first_8)),
2024-12-03 06:09:01 +00:00
}
}
2024-12-04 02:54:01 +00:00
fn class_is_a(class:&str,superclass:&str)->bool{
if class==superclass{
return true
}
let class_descriptor=rbx_reflection_database::get().classes.get(class);
if let Some(descriptor)=&class_descriptor{
if let Some(class_super)=&descriptor.superclass{
return class_is_a(&class_super,superclass)
}
}
false
}
fn recursive_collect_superclass(objects:&mut std::vec::Vec<rbx_dom_weak::types::Ref>,dom:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance,superclass:&str){
for &referent in instance.children(){
if let Some(c)=dom.get_by_ref(referent){
if class_is_a(c.class.as_str(),superclass){
objects.push(c.referent());//copy ref
}
recursive_collect_superclass(objects,dom,c,superclass);
}
}
}
fn get_script_refs(dom:&rbx_dom_weak::WeakDom)->Vec<rbx_dom_weak::types::Ref>{
let mut scripts=std::vec::Vec::new();
recursive_collect_superclass(&mut scripts,dom,dom.root(),"LuaSourceContainer");
scripts
}