Implement auto detection of document kind for roblox lib

This commit is contained in:
Filip Tibell 2023-03-09 17:31:44 +01:00
parent 5bdc968ffe
commit 0f4c36219f
No known key found for this signature in database
6 changed files with 168 additions and 14 deletions

2
Cargo.lock generated
View file

@ -858,6 +858,8 @@ dependencies = [
"mlua",
"rbx_binary",
"rbx_dom_weak",
"rbx_reflection",
"rbx_reflection_database",
"rbx_xml",
"thiserror",
]

View file

@ -21,6 +21,8 @@ thiserror = "1.0"
rbx_binary = { git = "https://github.com/rojo-rbx/rbx-dom", rev = "ce4c5bf7b18c813417ad14cc37e5abe281dfb51a" }
rbx_dom_weak = { git = "https://github.com/rojo-rbx/rbx-dom", rev = "ce4c5bf7b18c813417ad14cc37e5abe281dfb51a" }
rbx_reflection = { git = "https://github.com/rojo-rbx/rbx-dom", rev = "ce4c5bf7b18c813417ad14cc37e5abe281dfb51a" }
rbx_reflection_database = { git = "https://github.com/rojo-rbx/rbx-dom", rev = "ce4c5bf7b18c813417ad14cc37e5abe281dfb51a" }
rbx_xml = { git = "https://github.com/rojo-rbx/rbx-dom", rev = "ce4c5bf7b18c813417ad14cc37e5abe281dfb51a" }
# TODO: Split lune lib out into something like lune-core so

View file

@ -1,5 +1,9 @@
use std::path::Path;
use rbx_dom_weak::WeakDom;
use crate::instance::instance_is_a_service;
/**
A document kind specifier.
@ -51,13 +55,29 @@ impl DocumentKind {
}
/**
Try to detect a document kind specifier from file contents.
Try to detect a document kind specifier from a weak dom.
Returns `None` if the file contents do not seem to be from a valid roblox file.
Returns `None` if the given dom is empty and as such can not have its kind inferred.
*/
pub fn from_bytes(_bytes: impl AsRef<[u8]>) -> Option<Self> {
// TODO: Implement this, read comment below
todo!("Investigate if it is possible to detect document kind from contents")
pub fn from_weak_dom(dom: &WeakDom) -> Option<Self> {
let mut has_top_level_child = false;
let mut has_top_level_service = false;
for child_ref in dom.root().children() {
if let Some(child_inst) = dom.get_by_ref(*child_ref) {
has_top_level_child = true;
if instance_is_a_service(&child_inst.class).unwrap_or(false) {
has_top_level_service = true;
break;
}
}
}
if has_top_level_service {
Some(Self::Place)
} else if has_top_level_child {
Some(Self::Model)
} else {
None
}
}
}
@ -65,6 +85,8 @@ impl DocumentKind {
mod tests {
use std::path::PathBuf;
use rbx_dom_weak::InstanceBuilder;
use super::*;
#[test]
@ -149,5 +171,39 @@ mod tests {
);
}
// TODO: Add tests here for the from_bytes implementation
#[test]
fn from_weak_dom() {
let empty = WeakDom::new(InstanceBuilder::new("Instance"));
assert_eq!(DocumentKind::from_weak_dom(&empty), None);
let with_services = WeakDom::new(
InstanceBuilder::new("Instance")
.with_child(InstanceBuilder::new("Workspace"))
.with_child(InstanceBuilder::new("ReplicatedStorage")),
);
assert_eq!(
DocumentKind::from_weak_dom(&with_services),
Some(DocumentKind::Place)
);
let with_children = WeakDom::new(
InstanceBuilder::new("Instance")
.with_child(InstanceBuilder::new("Model"))
.with_child(InstanceBuilder::new("Part")),
);
assert_eq!(
DocumentKind::from_weak_dom(&with_children),
Some(DocumentKind::Model)
);
let with_mixed = WeakDom::new(
InstanceBuilder::new("Instance")
.with_child(InstanceBuilder::new("Workspace"))
.with_child(InstanceBuilder::new("Part")),
);
assert_eq!(
DocumentKind::from_weak_dom(&with_mixed),
Some(DocumentKind::Place)
);
}
}

View file

@ -51,15 +51,10 @@ impl Document {
}
}
/**
Decodes and creates a new document from a byte buffer.
This will automatically handle and detect if the document should be decoded
using a roblox binary or roblox xml format, and if it is a model or place file.
*/
pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self, DocumentError> {
fn from_bytes_inner(
bytes: impl AsRef<[u8]>,
) -> Result<(DocumentFormat, WeakDom), DocumentError> {
let bytes = bytes.as_ref();
let kind = DocumentKind::from_bytes(bytes).ok_or(DocumentError::UnknownKind)?;
let format = DocumentFormat::from_bytes(bytes).ok_or(DocumentError::UnknownFormat)?;
let dom = match format {
DocumentFormat::InternalRoot => Err(DocumentError::InternalRootReadWrite),
@ -72,6 +67,37 @@ impl Document {
.map_err(|err| DocumentError::ReadError(err.to_string()))
}
}?;
Ok((format, dom))
}
/**
Decodes and creates a new document from a byte buffer.
This will automatically handle and detect if the document should be decoded
using a roblox binary or roblox xml format, and if it is a model or place file.
Note that detection of model vs place file is heavily dependent on the structure
of the file, and a model file with services in it will detect as a place file, so
if possible using [`Document::from_bytes`] with an explicit kind should be preferred.
*/
pub fn from_bytes_auto(bytes: impl AsRef<[u8]>) -> Result<Self, DocumentError> {
let (format, dom) = Self::from_bytes_inner(bytes)?;
let kind = DocumentKind::from_weak_dom(&dom).ok_or(DocumentError::UnknownKind)?;
Ok(Self {
kind,
format,
dom: Arc::new(RwLock::new(dom)),
})
}
/**
Decodes and creates a new document from a byte buffer.
This will automatically handle and detect if the document
should be decoded using a roblox binary or roblox xml format.
*/
pub fn from_bytes(bytes: impl AsRef<[u8]>, kind: DocumentKind) -> Result<Self, DocumentError> {
let (format, dom) = Self::from_bytes_inner(bytes)?;
Ok(Self {
kind,
format,

View file

@ -0,0 +1,66 @@
use std::borrow::Borrow;
use rbx_reflection::ClassTag;
/**
Checks if an instance class matches a given class or superclass, similar to
[Instance::IsA](https://create.roblox.com/docs/reference/engine/classes/Instance#IsA)
from the Roblox standard library.
Note that this function may return `None` if it encounters a class or superclass
that does not exist in the currently known class reflection database.
*/
#[allow(dead_code)]
pub fn instance_is_a(
instance_class_name: impl AsRef<str>,
class_name: impl AsRef<str>,
) -> Option<bool> {
let instance_class_name = instance_class_name.as_ref();
let class_name = class_name.as_ref();
if class_name == "Instance" || instance_class_name == class_name {
Some(true)
} else {
let db = rbx_reflection_database::get();
let mut super_class_name = instance_class_name;
while super_class_name != class_name {
let class_descriptor = db.classes.get(super_class_name)?;
if let Some(sup) = &class_descriptor.superclass {
super_class_name = sup.borrow();
} else {
return Some(false);
}
}
Some(true)
}
}
/**
Checks if an instance class is a service.
This is separate from [`instance_is_a`] since services do not share a
common base class, and are instead determined through reflection tags.
Note that this function may return `None` if it encounters a class or superclass
that does not exist in the currently known class reflection database.
*/
pub fn instance_is_a_service(class_name: impl AsRef<str>) -> Option<bool> {
let mut class_name = class_name.as_ref();
let db = rbx_reflection_database::get();
loop {
let class_descriptor = db.classes.get(class_name)?;
if class_descriptor.tags.contains(&ClassTag::Service) {
return Some(true);
} else if let Some(sup) = &class_descriptor.superclass {
class_name = sup.borrow();
} else {
break;
}
}
Some(false)
}

View file

@ -1,5 +1,7 @@
use mlua::prelude::*;
mod instance;
pub mod document;
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {