use std::{io::{Read, Seek}, path::PathBuf}; use clap::{Args, Parser, Subcommand}; use anyhow::Result as AResult; #[derive(Parser)] #[command(author, version, about, long_about = None)] #[command(propagate_version = true)] struct Cli { #[arg(long)] path:Option, #[command(subcommand)] command: Commands, } #[derive(Subcommand)] enum Commands { ExtractScripts(PathBufList), Interactive, Replace, Scan, Upload, } #[derive(Args)] struct PathBufList { paths:Vec } #[derive(Args)] struct MapList { maps: Vec, } fn main() -> AResult<()> { let cli = Cli::parse(); match cli.command { Commands::ExtractScripts(pathlist)=>extract_scripts(pathlist.paths), Commands::Interactive=>interactive(), Commands::Replace=>replace(), Commands::Scan=>scan(), Commands::Upload=>upload(), } } 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,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_full_name(dom:&rbx_dom_weak::WeakDom,instance:&rbx_dom_weak::Instance) -> String{ let mut full_name=instance.name.clone(); let mut pref=instance.parent(); while let Some(parent)=dom.get_by_ref(pref){ full_name.insert(0, '.'); full_name.insert_str(0, &parent.name); pref=parent.parent(); } full_name } //scan (scripts) //iter maps/unprocessed //passing moves to maps/verified //failing moves to maps/blocked //replace (edits & deletions) //iter maps/blocked //replace scripts and put in maps/unprocessed //interactive //iter maps/unprocessed //for each unique script, load it into the file current.lua and have it open in sublime text //I can edit the file and it will edit it in place //I pass/fail(with comment)/allow each script fn get_script_refs(dom:&rbx_dom_weak::WeakDom) -> Vec{ let mut scripts = std::vec::Vec::new(); recursive_collect_superclass(&mut scripts, dom, dom.root(),"LuaSourceContainer"); scripts } enum ReaderType<'a, R:Read+Seek>{ GZip(flate2::read::GzDecoder<&'a mut R>), Raw(&'a mut R), } fn maybe_gzip_decode(input:&mut R)->AResult>{ let mut first_2=[0u8;2]; if let (Ok(()),Ok(()))=(std::io::Read::read_exact(input, &mut first_2),std::io::Seek::rewind(input)){ match &first_2{ b"\x1f\x8b"=>Ok(ReaderType::GZip(flate2::read::GzDecoder::new(input))), _=>Ok(ReaderType::Raw(input)), } }else{ Err(anyhow::Error::msg("failed to peek")) } } fn load_dom(input:&mut R)->AResult{ let mut first_8=[0u8;8]; if let (Ok(()),Ok(()))=(std::io::Read::read_exact(input, &mut first_8),std::io::Seek::rewind(input)){ match &first_8[0..4]{ b"{ match &first_8[4..8]{ b"lox!"=>rbx_binary::from_reader(input).map_err(anyhow::Error::msg), b"lox "=>rbx_xml::from_reader(input,rbx_xml::DecodeOptions::default()).map_err(anyhow::Error::msg), other=>Err(anyhow::Error::msg(format!("Unknown Roblox file type {:?}",other))), } }, _=>Err(anyhow::Error::msg("unsupported file type")), } }else{ Err(anyhow::Error::msg("peek failed")) } } fn get_dom(input:&mut R)->AResult{ match maybe_gzip_decode(input){ Ok(ReaderType::GZip(mut readable)) => { //gzip let mut extracted:Vec=Vec::new(); readable.read_to_end(&mut extracted)?; Ok(load_dom(&mut std::io::Cursor::new(extracted))?) }, Ok(ReaderType::Raw(readable)) => Ok(load_dom(readable)?), Err(e) => Err(e)?, } } fn get_id() -> AResult{ match std::fs::read_to_string("id"){ Ok(id_file)=>Ok(id_file.parse::()?), Err(e) => match e.kind() { std::io::ErrorKind::NotFound => Ok(0),//implicitly take on id=0 _ => Err(e)?, } } } fn get_set_from_file(file:&str) -> AResult>{ let mut set=std::collections::HashSet::::new(); for entry in std::fs::read_dir(file)? { set.insert(std::fs::read_to_string(entry?.path())?); } Ok(set) } fn get_allowed_set() -> AResult>{ get_set_from_file("scripts/allowed") } fn get_blocked() -> AResult>{ get_set_from_file("scripts/blocked") } fn get_allowed_map() -> AResult>{ let mut allowed_map = std::collections::HashMap::::new(); for entry in std::fs::read_dir("scripts/allowed")? { let entry=entry?; allowed_map.insert(entry.path().file_stem().unwrap().to_str().unwrap().parse::()?,std::fs::read_to_string(entry.path())?); } Ok(allowed_map) } fn get_replace_map() -> AResult>{ let mut replace = std::collections::HashMap::::new(); for entry in std::fs::read_dir("scripts/replace")? { let entry=entry?; replace.insert(std::fs::read_to_string(entry.path())?,entry.path().file_stem().unwrap().to_str().unwrap().parse::()?); } Ok(replace) } fn check_source_illegal_keywords(source:&String)->bool{ source.find("getfenv").is_some()||source.find("require").is_some() } fn find_first_child_class<'a>(dom:&'a rbx_dom_weak::WeakDom,instance:&'a rbx_dom_weak::Instance,name:&'a str,class:&'a str) -> Option<&'a rbx_dom_weak::Instance> { for &referent in instance.children() { if let Some(c) = dom.get_by_ref(referent) { if c.name==name&&class_is_a(c.class.as_str(),class) { return Some(c); } } } None } fn get_mapinfo(dom:&rbx_dom_weak::WeakDom) -> AResult<(String,String,String,rbx_dom_weak::types::Ref)>{ let workspace_children=dom.root().children(); if workspace_children.len()!=1{ return Err(anyhow::Error::msg("there can only be one model")); } if let Some(model_instance) = dom.get_by_ref(workspace_children[0]) { if let (Some(creator),Some(displayname))=(find_first_child_class(dom, model_instance, "Creator", "StringValue"),find_first_child_class(dom, model_instance, "DisplayName", "StringValue")){ if let ( Some(rbx_dom_weak::types::Variant::String(creator_string)), Some(rbx_dom_weak::types::Variant::String(displayname_string)) )=( creator.properties.get("Value"), displayname.properties.get("Value") ){ return Ok((model_instance.name.clone(),creator_string.clone(),displayname_string.clone(),displayname.referent())); } } } Err(anyhow::Error::msg("no stuff in map")) } struct RobloxAssetId(u64); struct RobloxAssetIdParseErr; impl std::str::FromStr for RobloxAssetId { type Err=RobloxAssetIdParseErr; fn from_str(s: &str) -> Result{ let regman=lazy_regex::regex!(r"(\d+)$"); if let Some(captures) = regman.captures(s) { if captures.len()==2{//captures[0] is all captures concatenated, and then each individual capture if let Ok(id) = captures[0].parse::() { return Ok(Self(id)); } } } Err(RobloxAssetIdParseErr) } } enum Scan{ Passed, Blocked, Flagged, } fn scan() -> AResult<()>{ let mut id = get_id()?; //Construct allowed scripts let allowed_set = get_allowed_set()?; let mut blocked = get_blocked()?; for entry in std::fs::read_dir("maps/unprocessed")? { let file_thing=entry?; let mut input = std::io::BufReader::new(std::fs::File::open(file_thing.path())?); let dom = get_dom(&mut input)?; let script_refs = get_script_refs(&dom); //check scribb let mut fail_count=0; let mut fail_type=Scan::Passed; for &script_ref in script_refs.iter() { if let Some(script)=dom.get_by_ref(script_ref){ if let Some(rbx_dom_weak::types::Variant::String(s)) = script.properties.get("Source") { //flag keywords and instantly fail if check_source_illegal_keywords(s){ println!("{:?} - flagged.",file_thing.file_name()); fail_type=Scan::Flagged; break; } if allowed_set.contains(s) { continue; }else{ fail_type=Scan::Blocked;//no need to check for Flagged, it breaks the loop. fail_count+=1; if !blocked.contains(s) { blocked.insert(s.clone());//all fixed! just clone! std::fs::write(format!("scripts/blocked/{}.lua",id),s)?; id+=1; } } }else{ panic!("FATAL: failed to get source for {:?}",file_thing.file_name()); } }else{ panic!("FATAL: failed to get_by_ref {:?}",script_ref); } } let mut dest=match fail_type { Scan::Passed => PathBuf::from("maps/processed"), Scan::Blocked => { println!("{:?} - {} {} not allowed.",file_thing.file_name(),fail_count,if fail_count==1 {"script"}else{"scripts"}); PathBuf::from("maps/blocked") } Scan::Flagged => PathBuf::from("maps/flagged") }; dest.push(file_thing.file_name()); std::fs::rename(file_thing.path(), dest)?; } std::fs::write("id",id.to_string())?; Ok(()) } fn extract_scripts(paths: Vec) -> AResult<()>{ let mut id = 0; //Construct allowed scripts let mut script_set = std::collections::HashSet::::new(); for path in paths { let file_name=path.file_name(); let mut input = std::io::BufReader::new(std::fs::File::open(&path)?); let dom = get_dom(&mut input)?; let script_refs = get_script_refs(&dom); //extract scribb for &script_ref in script_refs.iter() { if let Some(script)=dom.get_by_ref(script_ref){ if let Some(rbx_dom_weak::types::Variant::String(s)) = script.properties.get("Source") { if script_set.contains(s) { continue; }else{ script_set.insert(s.clone()); std::fs::write(format!("scripts/extracted/{:?}_{}_{}.lua",file_name,id,script.name),s)?; id+=1; } }else{ panic!("FATAL: failed to get source for {:?}",file_name); } }else{ panic!("FATAL: failed to get_by_ref {:?}",script_ref); } } } println!("extracted {} {}",id,if id==1 {"script"}else{"scripts"}); Ok(()) } fn replace() -> AResult<()>{ let allowed_map=get_allowed_map()?; let replace_map=get_replace_map()?; for entry in std::fs::read_dir("maps/blocked")? { let file_thing=entry?; let mut input = std::io::BufReader::new(std::fs::File::open(file_thing.path())?); let mut dom = get_dom(&mut input)?; let script_refs = get_script_refs(&dom); //check scribb let mut any_failed=false; for &script_ref in script_refs.iter() { if let Some(script)=dom.get_by_ref(script_ref){ if let Some(rbx_dom_weak::types::Variant::String(source)) = script.properties.get("Source") { if let (Some(replace_id),Some(replace_script))=(replace_map.get(source),dom.get_by_ref_mut(script.referent())) { println!("replace {}",replace_id); //replace the source if let Some(replace_source)=allowed_map.get(replace_id){ replace_script.properties.insert("Source".to_string(), rbx_dom_weak::types::Variant::String(replace_source.clone())); }else{ println!("failed to get replacement source {}",replace_id); any_failed=true; } }else{ println!("failed to failed to get replace_id and replace_script"); any_failed=true; } }else{ panic!("FATAL: failed to get source for {:?}",file_thing.file_name()); } }else{ panic!("FATAL: failed to get_by_ref {:?}",script_ref); } } if any_failed { println!("One or more scripts failed to replace."); }else{ let mut dest=PathBuf::from("maps/unprocessed"); dest.push(file_thing.file_name()); let output = std::io::BufWriter::new(std::fs::File::open(dest)?); //write workspace:GetChildren()[1] let workspace_children=dom.root().children(); if workspace_children.len()!=1{ return Err(anyhow::Error::msg("there can only be one model")); } rbx_binary::to_writer(output, &dom, &[workspace_children[0]])?; } } Ok(()) } enum UploadAction { Upload(u64), Skip, New, Delete, } struct ParseUploadActionErr; impl std::str::FromStr for UploadAction { type Err=ParseUploadActionErr; fn from_str(s: &str) -> Result{ if s=="skip\n"{ Ok(Self::Skip) }else if s=="new\n"{ Ok(Self::New) }else if s=="delete\n"{ Ok(Self::Delete) }else if let Ok(asset_id)=s[..s.len()-1].parse::(){ Ok(Self::Upload(asset_id)) }else{ Err(ParseUploadActionErr) } } } fn upload() -> AResult<()>{ //interactive prompt per upload: for entry in std::fs::read_dir("maps/passed")? { let file_thing=entry?; println!("map file: {:?}",file_thing.file_name()); let mut input = std::io::BufReader::new(std::fs::File::open(file_thing.path())?); let dom = get_dom(&mut input)?; let (modelname,creator,displayname,_) = get_mapinfo(&dom)?; //Creator: [auto fill creator] //DisplayName: [auto fill DisplayName] //id: ["New" for blank because of my double enter key] print!("Model name: {}\nCreator: {}\nDisplayName: {}\n",modelname,creator,displayname); let upload_action; loop{ print!("Action or Upload Asset Id: "); std::io::Write::flush(&mut std::io::stdout())?; let mut upload_action_string = String::new(); std::io::stdin().read_line(&mut upload_action_string)?; if let Ok(parsed_upload_action)=upload_action_string.parse::(){ upload_action=parsed_upload_action; break; } } match upload_action { UploadAction::Upload(asset_id) => { let status=std::process::Command::new("asset-tool") .args([ "upload-asset", "--cookie-envvar","RBXCOOKIE", "--group-id","6980477" ]) .arg("--asset-id").arg(asset_id.to_string()) .arg("--input-file").arg(file_thing.path().into_os_string().into_string().unwrap()) .status()?; match status.code() { Some(0)=>{ //move file let mut dest=PathBuf::from("maps/uploaded"); dest.push(file_thing.file_name()); std::fs::rename(file_thing.path(), dest)?; } Some(code)=>println!("upload failed! code={}",code), None => println!("no status code!"), } } UploadAction::Skip => continue, UploadAction::New => { let output=std::process::Command::new("asset-tool") .args([ "create-asset", "--cookie-envvar","RBXCOOKIE", "--group-id","6980477" ]) .arg("--model-name").arg(modelname.as_str()) .arg("--input-file").arg(file_thing.path().into_os_string().into_string().unwrap()) .output()?; match output.status.code() { Some(0)=>{ //print output println!("{}", std::str::from_utf8(output.stdout.as_slice())?); //move file let mut dest=PathBuf::from("maps/uploaded"); dest.push(file_thing.file_name()); std::fs::rename(file_thing.path(), dest)?; } Some(code)=>println!("upload failed! code={}",code), None => println!("no status code!"), } } UploadAction::Delete => std::fs::remove_file(file_thing.path())?, } } Ok(()) } enum Interactive{ Passed, Blocked, Flagged, } enum ScriptAction { Pass, Replace(u32), Flag, Block, Delete, } enum ScriptActionParseResult { Pass, Block, Exit, Delete, } struct ParseScriptActionErr; impl std::str::FromStr for ScriptActionParseResult { type Err=ParseScriptActionErr; fn from_str(s: &str) -> Result{ if s=="pass\n"||s=="1\n"{ Ok(Self::Pass) }else if s=="block\n"{ Ok(Self::Block) }else if s=="exit\n"{ Ok(Self::Exit) }else if s=="delete\n"{ Ok(Self::Delete) }else{ Err(ParseScriptActionErr) } } } fn interactive() -> AResult<()>{ let mut id=get_id()?; //Construct allowed scripts let mut allowed_set=get_allowed_set()?; let mut allowed_map=get_allowed_map()?; let mut replace_map=get_replace_map()?; let mut blocked = get_blocked()?; let model_name_pattern=lazy_regex::regex!(r"^[a-z0-9_]+$"); let display_name_pattern=lazy_regex::regex!(r"\b[a-z]"); 'map_loop: for entry in std::fs::read_dir("maps/unprocessed")? { let file_thing=entry?; println!("processing map={:?}",file_thing.file_name()); let mut input = std::io::BufReader::new(std::fs::File::open(file_thing.path())?); let mut dom = get_dom(&mut input)?; let (modelname,creator,displayname,displayname_ref)=get_mapinfo(&dom)?; let mut script_count=0; let mut replace_count=0; let mut block_count=0; //if model name is illegal prompt for new name print!("Model name: {}\nCreator: {}\nDisplayName: {}\n",modelname,creator,displayname); if !model_name_pattern.is_match(modelname.as_str()){ //illegal let new_model_name; loop{ print!("Enter new model name: "); std::io::Write::flush(&mut std::io::stdout())?; let mut input_string=String::new(); std::io::stdin().read_line(&mut input_string)?; if model_name_pattern.is_match(input_string.trim()){ new_model_name=input_string.trim().to_owned(); break; } } let model_instance=dom.get_by_ref_mut(dom.root().children()[0]).unwrap(); model_instance.name=new_model_name; //mark file as edited so a new file is generated replace_count+=1; } if displayname.len()==0||display_name_pattern.is_match(displayname.as_str()){ //illegal let new_display_name; loop{ print!("Enter new display name: "); std::io::Write::flush(&mut std::io::stdout())?; let mut input_string=String::new(); std::io::stdin().read_line(&mut input_string)?; if displayname.len()!=0||!display_name_pattern.is_match(input_string.trim()){ new_display_name=input_string.trim().to_owned(); break; } } let displayname_instance=dom.get_by_ref_mut(displayname_ref).unwrap(); assert!(displayname_instance.properties.insert("Value".to_owned(),new_display_name.into()).is_some(),"StringValue we have a problem"); //mark file as edited so a new file is generated replace_count+=1; } let script_refs = get_script_refs(&dom); //check scribb let mut fail_type=Interactive::Passed; for &script_ref in script_refs.iter() { if let Some(script)=dom.get_by_ref(script_ref){ if let Some(rbx_dom_weak::types::Variant::String(source)) = script.properties.get("Source") { script_count+=1; let source_action=if check_source_illegal_keywords(source) { ScriptAction::Flag//script triggers flagging -> Flag } else if blocked.contains(source) { ScriptAction::Block//script is blocked -> Block } else if allowed_set.contains(source) { ScriptAction::Pass//script is allowed -> Pass }else if let Some(replace_id)=replace_map.get(source) { ScriptAction::Replace(*replace_id) }else{ //interactive logic goes here print!("unresolved source location={}\n",get_full_name(&dom, script)); //load source into current.lua std::fs::write("current.lua",source)?; //prompt action in terminal //wait for input let script_action; loop{ print!("action: "); std::io::Write::flush(&mut std::io::stdout())?; let mut action_string = String::new(); std::io::stdin().read_line(&mut action_string)?; if let Ok(parsed_script_action)=action_string.parse::(){ script_action=parsed_script_action; break; } } //update allowed/replace/blocked match script_action{ ScriptActionParseResult::Pass => { //if current.lua was updated, create an allowed and replace file and set script_action to replace(new_id) let modified_source=std::fs::read_to_string("current.lua")?; if &modified_source==source{ //it's always new. //insert allowed_set allowed_set.insert(modified_source.clone()); //insert allowed_map allowed_map.insert(id,modified_source.clone()); //write allowed/id.lua std::fs::write(format!("scripts/allowed/{}.lua",id),modified_source)?; id+=1; ScriptAction::Pass }else{ //insert allowed_set allowed_set.insert(modified_source.clone()); //insert allowed_map allowed_map.insert(id,modified_source.clone()); //insert replace_map replace_map.insert(source.clone(),id);//this cannot be reached if it already exists //write allowed/id.lua std::fs::write(format!("scripts/allowed/{}.lua",id),modified_source)?; //write replace/id.lua std::fs::write(format!("scripts/replace/{}.lua",id),source)?; let ret=ScriptAction::Replace(id); id+=1; ret } }, ScriptActionParseResult::Block => { blocked.insert(source.clone()); std::fs::write(format!("scripts/blocked/{}.lua",id),source)?; id+=1; ScriptAction::Block }, ScriptActionParseResult::Exit => break 'map_loop, ScriptActionParseResult::Delete => ScriptAction::Delete, } }; let location=get_full_name(&dom, script); match source_action{ ScriptAction::Pass => println!("passed source location={}",location), ScriptAction::Replace(replace_id)=>{ //replace the source if let (Some(replace_source),Some(replace_script))=(allowed_map.get(&replace_id),dom.get_by_ref_mut(script.referent())){ replace_count+=1; println!("replaced source id={} location={}",replace_id,location); replace_script.properties.insert("Source".to_string(), rbx_dom_weak::types::Variant::String(replace_source.clone())); }else{ panic!("failed to get replacement source id={} location={}",replace_id,location); } }, ScriptAction::Delete => { println!("deleted source location={}",location); replace_count+=1;//trigger a new file generation dom.destroy(script.referent()); }, ScriptAction::Flag => { println!("flagged source location={}",location); fail_type=Interactive::Flagged; }, ScriptAction::Block => { block_count+=1; println!("blocked source location={}",location); match fail_type{ Interactive::Passed => fail_type=Interactive::Blocked, _=>(), } }, } }else{ panic!("FATAL: failed to get source for {:?}",file_thing.file_name()); } }else{ panic!("FATAL: failed to get_by_ref {:?}",script_ref); } } let mut dest=match fail_type{ Interactive::Passed => { println!("map={:?} passed with {} {}",file_thing.file_name(),script_count,if script_count==1 {"script"}else{"scripts"}); if replace_count==0{ PathBuf::from("maps/passed") }else{ //create new file println!("{} {} replaced - generating new file...",replace_count,if replace_count==1 {"script was"}else{"scripts were"}); let mut dest=PathBuf::from("maps/passed"); dest.push(file_thing.file_name()); let output = std::io::BufWriter::new(std::fs::File::create(dest)?); //write workspace:GetChildren()[1] let workspace_children=dom.root().children(); if workspace_children.len()!=1{ return Err(anyhow::Error::msg("there can only be one model")); } rbx_binary::to_writer(output, &dom, &[workspace_children[0]])?; //move original to processed folder PathBuf::from("maps/unaltered") } },//write map into maps/processed Interactive::Blocked => { println!("map={:?} blocked with {}/{} {} blocked",file_thing.file_name(),block_count,script_count,if script_count==1 {"script"}else{"scripts"}); PathBuf::from("maps/blocked") },//write map into maps/blocked Interactive::Flagged => { println!("map={:?} flagged",file_thing.file_name()); PathBuf::from("maps/flagged") },//write map into maps/flagged }; dest.push(file_thing.file_name()); std::fs::rename(file_thing.path(), dest)?; } std::fs::write("id",id.to_string())?; Ok(()) }