Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow a database to be specified in both rbx_xml and rbx_binary #375

Merged
merged 13 commits into from
Nov 10, 2023
Merged
3 changes: 3 additions & 0 deletions rbx_binary/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# rbx_binary Changelog

## Unreleased
* Add the ability to specify a `ReflectionDatabase` to use for serializing and deserializing. This takes the form of `Deserializer::reflection_database` and `Serializer::reflection_database`. ([#375])

[#375]: https://github.com/rojo-rbx/rbx-dom/pull/375

## 0.7.3 (2023-10-23)
* Fixed missing fallback default for `SecurityCapabilities` ([#371]).
Expand Down
22 changes: 11 additions & 11 deletions rbx_binary/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,18 +336,18 @@ pub fn untransform_i64(value: i64) -> i64 {
((value as u64) >> 1) as i64 ^ -(value & 1)
}

pub struct PropertyDescriptors<'a> {
pub canonical: &'a PropertyDescriptor<'a>,
pub serialized: Option<&'a PropertyDescriptor<'a>>,
pub struct PropertyDescriptors<'db> {
pub canonical: &'db PropertyDescriptor<'db>,
pub serialized: Option<&'db PropertyDescriptor<'db>>,
}

/// Find both the canonical and serialized property descriptors for a given
/// class and property name pair. These might be the same descriptor!
pub fn find_property_descriptors<'a>(
database: &'a ReflectionDatabase<'a>,
pub fn find_property_descriptors<'db>(
database: &'db ReflectionDatabase<'db>,
class_name: &str,
property_name: &str,
) -> Option<PropertyDescriptors<'a>> {
) -> Option<PropertyDescriptors<'db>> {
let mut class_descriptor = database.classes.get(class_name)?;

// We need to find the canonical property descriptor associated with
Expand Down Expand Up @@ -437,11 +437,11 @@ pub fn find_property_descriptors<'a>(
/// Given the canonical property descriptor for a logical property along with
/// its serialization, returns the serialized form of the logical property if
/// this property is serializable.
fn find_serialized_from_canonical<'a>(
class: &'a ClassDescriptor<'a>,
canonical: &'a PropertyDescriptor<'a>,
serialization: &'a PropertySerialization<'a>,
) -> Option<&'a PropertyDescriptor<'a>> {
fn find_serialized_from_canonical<'db>(
class: &'db ClassDescriptor<'db>,
canonical: &'db PropertyDescriptor<'db>,
serialization: &'db PropertySerialization<'db>,
) -> Option<&'db PropertyDescriptor<'db>> {
match serialization {
// This property serializes as-is. This is the happiest path: both the
// canonical and serialized descriptors are the same!
Expand Down
24 changes: 19 additions & 5 deletions rbx_binary/src/deserializer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,32 @@ pub use self::error::Error;
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
pub struct Deserializer<'a> {
database: Option<&'a ReflectionDatabase<'a>>,
///
/// ## Configuration
///
/// A custom [`ReflectionDatabase`][ReflectionDatabase] can be specified via
/// [`reflection_database`][reflection_database].
///
/// [ReflectionDatabase]: rbx_reflection::ReflectionDatabase
/// [reflection_database]: Deserializer#method.reflection_database
pub struct Deserializer<'db> {
database: &'db ReflectionDatabase<'db>,
}

impl<'a> Deserializer<'a> {
impl<'db> Deserializer<'db> {
/// Create a new `Deserializer` with the default settings.
pub fn new() -> Self {
Self {
database: Some(rbx_reflection_database::get()),
database: rbx_reflection_database::get(),
}
}

/// Sets what reflection database for the deserializer to use.
#[inline]
pub fn reflection_database(self, database: &'db ReflectionDatabase<'db>) -> Self {
Self { database }
}

/// Deserialize a Roblox binary model or place from the given stream using
/// this deserializer.
pub fn deserialize<R: Read>(&self, reader: R) -> Result<WeakDom, Error> {
Expand Down Expand Up @@ -81,7 +95,7 @@ impl<'a> Deserializer<'a> {
}
}

impl<'a> Default for Deserializer<'a> {
impl<'db> Default for Deserializer<'db> {
fn default() -> Self {
Self::new()
}
Expand Down
16 changes: 8 additions & 8 deletions rbx_binary/src/deserializer/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ use crate::{

use super::{error::InnerError, header::FileHeader, Deserializer};

pub(super) struct DeserializerState<'a, R> {
pub(super) struct DeserializerState<'db, R> {
/// The user-provided configuration that we should use.
deserializer: &'a Deserializer<'a>,
deserializer: &'db Deserializer<'db>,

/// The input data encoded as a binary model.
input: R,
Expand Down Expand Up @@ -90,10 +90,10 @@ struct Instance {
/// contains a migration for some properties Roblox has replaced with
/// others (like Font, which has been superceded by FontFace).
#[derive(Debug)]
struct CanonicalProperty<'a> {
name: &'a str,
struct CanonicalProperty<'db> {
name: &'db str,
ty: VariantType,
migration: Option<&'a PropertySerialization<'a>>,
migration: Option<&'db PropertySerialization<'db>>,
}

fn find_canonical_property<'de>(
Expand Down Expand Up @@ -210,9 +210,9 @@ fn add_property(instance: &mut Instance, canonical_property: &CanonicalProperty,
}
}

impl<'a, R: Read> DeserializerState<'a, R> {
impl<'db, R: Read> DeserializerState<'db, R> {
pub(super) fn new(
deserializer: &'a Deserializer<'a>,
deserializer: &'db Deserializer<'db>,
mut input: R,
) -> Result<Self, InnerError> {
let tree = WeakDom::new(InstanceBuilder::new("DataModel"));
Expand Down Expand Up @@ -382,7 +382,7 @@ impl<'a, R: Read> DeserializerState<'a, R> {
}

let property = if let Some(property) = find_canonical_property(
self.deserializer.database.unwrap(),
self.deserializer.database,
binary_type,
&type_info.type_name,
&prop_name,
Expand Down
31 changes: 25 additions & 6 deletions rbx_binary/src/serializer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod state;
use std::io::Write;

use rbx_dom_weak::{types::Ref, WeakDom};
use rbx_reflection::ReflectionDatabase;

use self::state::SerializerState;

Expand All @@ -27,24 +28,42 @@ pub use self::error::Error;
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// ## Configuration
///
/// A custom [`ReflectionDatabase`][ReflectionDatabase] can be specified via
/// [`reflection_database`][reflection_database].
///
/// [ReflectionDatabase]: rbx_reflection::ReflectionDatabase
/// [reflection_database]: Serializer#method.reflection_database
//
// future settings:
// * reflection_database: Option<ReflectionDatabase> = default
// * recursive: bool = true
#[non_exhaustive]
pub struct Serializer {}
pub struct Serializer<'db> {
database: &'db ReflectionDatabase<'db>,
}

impl Serializer {
impl<'db> Serializer<'db> {
/// Create a new `Serializer` with the default settings.
pub fn new() -> Self {
Serializer {}
Serializer {
database: rbx_reflection_database::get(),
}
}

/// Sets what reflection database for the serializer to use.
#[inline]
pub fn reflection_database(self, database: &'db ReflectionDatabase<'db>) -> Self {
Self { database }
}

/// Serialize a Roblox binary model or place into the given stream using
/// this serializer.
pub fn serialize<W: Write>(&self, writer: W, dom: &WeakDom, refs: &[Ref]) -> Result<(), Error> {
profiling::scope!("rbx_binary::seserialize");

let mut serializer = SerializerState::new(dom, writer);
let mut serializer = SerializerState::new(self, dom, writer);

serializer.add_instances(refs)?;
serializer.generate_referents();
Expand All @@ -60,7 +79,7 @@ impl Serializer {
}
}

impl Default for Serializer {
impl<'db> Default for Serializer<'db> {
fn default() -> Self {
Self::new()
}
Expand Down
47 changes: 27 additions & 20 deletions rbx_binary/src/serializer/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use rbx_dom_weak::{

use rbx_reflection::{
ClassDescriptor, ClassTag, DataType, PropertyKind, PropertyMigration, PropertySerialization,
ReflectionDatabase,
};

use crate::{
Expand All @@ -27,6 +28,7 @@ use crate::{
find_property_descriptors, RbxWriteExt, FILE_MAGIC_HEADER, FILE_SIGNATURE, FILE_VERSION,
},
types::Type,
Serializer,
};

use super::error::InnerError;
Expand All @@ -36,7 +38,9 @@ static FILE_FOOTER: &[u8] = b"</roblox>";
/// Represents all of the state during a single serialization session. A new
/// `BinarySerializer` object should be created every time we want to serialize
/// a binary model file.
pub(super) struct SerializerState<'dom, W> {
pub(super) struct SerializerState<'dom, 'db, W> {
serializer: &'db Serializer<'db>,

/// The dom containing all of the instances that we're serializing.
dom: &'dom WeakDom,

Expand All @@ -53,7 +57,7 @@ pub(super) struct SerializerState<'dom, W> {

/// All of the types of instance discovered by our serializer that we'll be
/// writing into the output.
type_infos: TypeInfos<'dom>,
type_infos: TypeInfos<'dom, 'db>,

/// All of the SharedStrings in the DOM, in the order they'll be written
// in.
Expand All @@ -67,7 +71,7 @@ pub(super) struct SerializerState<'dom, W> {
/// An instance class that our serializer knows about. We should have one struct
/// per unique ClassName.
#[derive(Debug)]
struct TypeInfo<'dom> {
struct TypeInfo<'dom, 'db> {
/// The ID that this serializer will use to refer to this type of instance.
type_id: u32,

Expand All @@ -85,16 +89,16 @@ struct TypeInfo<'dom> {
///
/// Stored in a sorted map to try to ensure that we write out properties in
/// a deterministic order.
properties: BTreeMap<Cow<'static, str>, PropInfo>,
properties: BTreeMap<Cow<'db, str>, PropInfo<'db>>,

/// A reference to the type's class descriptor from rbx_reflection, if this
/// is a known class.
class_descriptor: Option<&'static ClassDescriptor<'static>>,
class_descriptor: Option<&'db ClassDescriptor<'db>>,

/// A set containing the properties that we have seen so far in the file and
/// processed. This helps us avoid traversing the reflection database
/// multiple times if there are many copies of the same kind of instance.
properties_visited: HashSet<(Cow<'static, str>, VariantType)>,
properties_visited: HashSet<(Cow<'db, str>, VariantType)>,
}

/// A property on a specific class that our serializer knows about.
Expand All @@ -104,7 +108,7 @@ struct TypeInfo<'dom> {
/// `BasePart.size` are present in the same document, they should share a
/// `PropInfo` as they are the same logical property.
#[derive(Debug)]
struct PropInfo {
struct PropInfo<'db> {
/// The binary format type ID that will be use to serialize this property.
/// This type is related to the type of the serialized form of the logical
/// property, but is not 1:1.
Expand All @@ -117,7 +121,7 @@ struct PropInfo {
/// The serialized name for this property. This is the name that is actually
/// written as part of the PROP chunk and may not line up with the canonical
/// name for the property.
serialized_name: Cow<'static, str>,
serialized_name: Cow<'db, str>,

/// A set containing the names of all aliases discovered while preparing to
/// serialize this property. Ideally, this set will remain empty (and not
Expand All @@ -136,47 +140,49 @@ struct PropInfo {
///
/// Default values are first populated from the reflection database, if
/// present, followed by an educated guess based on the type of the value.
default_value: Cow<'static, Variant>,
default_value: Cow<'db, Variant>,

/// If a logical property has a migration associated with it (i.e. BrickColor ->
/// Color, Font -> FontFace), this field contains Some(PropertyMigration). Otherwise,
/// it is None.
migration: Option<&'static PropertyMigration>,
migration: Option<&'db PropertyMigration>,
}

/// Contains all of the `TypeInfo` objects known to the serializer so far. This
/// struct was broken out to help encapsulate the behavior here and to ease
/// self-borrowing issues from BinarySerializer getting too large.
#[derive(Debug)]
struct TypeInfos<'dom> {
struct TypeInfos<'dom, 'db> {
database: &'db ReflectionDatabase<'db>,
/// A map containing one entry for each unique ClassName discovered in the
/// DOM.
///
/// These are stored sorted so that we naturally iterate over them in order
/// and improve our chances of being deterministic.
values: BTreeMap<String, TypeInfo<'dom>>,
values: BTreeMap<String, TypeInfo<'dom, 'db>>,

/// The next type ID that should be assigned if a type is discovered and
/// added to the serializer.
next_type_id: u32,
}

impl<'dom> TypeInfos<'dom> {
fn new() -> Self {
impl<'dom, 'db> TypeInfos<'dom, 'db> {
fn new(database: &'db ReflectionDatabase<'db>) -> Self {
Self {
database,
values: BTreeMap::new(),
next_type_id: 0,
}
}

/// Finds the type info from the given ClassName if it exists, or creates
/// one and returns a reference to it if not.
fn get_or_create(&mut self, class: &str) -> &mut TypeInfo<'dom> {
fn get_or_create(&mut self, class: &str) -> &mut TypeInfo<'dom, 'db> {
if !self.values.contains_key(class) {
let type_id = self.next_type_id;
self.next_type_id += 1;

let class_descriptor = rbx_reflection_database::get().classes.get(class);
let class_descriptor = self.database.classes.get(class);

let is_service = if let Some(descriptor) = &class_descriptor {
descriptor.tags.contains(&ClassTag::Service)
Expand Down Expand Up @@ -224,14 +230,15 @@ impl<'dom> TypeInfos<'dom> {
}
}

impl<'dom, W: Write> SerializerState<'dom, W> {
pub fn new(dom: &'dom WeakDom, output: W) -> Self {
impl<'dom, 'db, W: Write> SerializerState<'dom, 'db, W> {
pub fn new(serializer: &'db Serializer<'db>, dom: &'dom WeakDom, output: W) -> Self {
SerializerState {
serializer,
dom,
output,
relevant_instances: Vec::new(),
id_to_referent: HashMap::new(),
type_infos: TypeInfos::new(),
type_infos: TypeInfos::new(serializer.database),
shared_strings: Vec::new(),
shared_string_ids: HashMap::new(),
}
Expand Down Expand Up @@ -311,7 +318,7 @@ impl<'dom, W: Write> SerializerState<'dom, W> {
let serialized_ty;
let mut migration = None;

let database = rbx_reflection_database::get();
let database = self.serializer.database;
match find_property_descriptors(database, &instance.class, prop_name) {
Some(descriptors) => {
// For any properties that do not serialize, we can skip
Expand Down
3 changes: 3 additions & 0 deletions rbx_xml/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# rbx_xml Changelog

## Unreleased
* Add the ability to specify a `ReflectionDatabase` to use for serializing and deserializing. This takes the form of `DecodeOptions::reflection_database` and `EncodeOptions::reflection_database`. ([#375])

[#375]: https://github.com/rojo-rbx/rbx-dom/pull/375

## 0.13.2 (2023-10-03)
* Added support for `SecurityCapabilities` values. ([#359])
Expand Down
Loading