use std::{borrow::Cow, collections::BTreeMap, io::Write};
use ahash::{HashMap, HashMapExt};
use rbx_dom_weak::{
types::{Ref, SharedString, SharedStringHash, Variant},
WeakDom,
};
use rbx_reflection::{PropertyKind, PropertySerialization, ReflectionDatabase};
use crate::{
conversion::ConvertVariant,
core::find_serialized_property_descriptor,
error::{EncodeError as NewEncodeError, EncodeErrorKind},
types::write_value_xml,
};
use crate::serializer_core::{XmlEventWriter, XmlWriteEvent};
pub fn encode_internal<W: Write>(
output: W,
tree: &WeakDom,
ids: &[Ref],
options: EncodeOptions,
) -> Result<(), NewEncodeError> {
let mut writer = XmlEventWriter::from_output(output);
let mut state = EmitState::new(options);
writer.write(XmlWriteEvent::start_element("roblox").attr("version", "4"))?;
let mut property_buffer = Vec::new();
for id in ids {
serialize_instance(&mut writer, &mut state, tree, *id, &mut property_buffer)?;
}
serialize_shared_strings(&mut writer, &mut state)?;
writer.write(XmlWriteEvent::end_element())?;
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum EncodePropertyBehavior {
IgnoreUnknown,
WriteUnknown,
ErrorOnUnknown,
NoReflection,
}
#[derive(Debug, Clone)]
pub struct EncodeOptions<'db> {
property_behavior: EncodePropertyBehavior,
database: &'db ReflectionDatabase<'db>,
}
impl<'db> EncodeOptions<'db> {
#[inline]
pub fn new() -> Self {
EncodeOptions {
property_behavior: EncodePropertyBehavior::IgnoreUnknown,
database: rbx_reflection_database::get().unwrap(),
}
}
#[inline]
pub fn property_behavior(self, property_behavior: EncodePropertyBehavior) -> Self {
EncodeOptions {
property_behavior,
..self
}
}
#[inline]
pub fn reflection_database(self, database: &'db ReflectionDatabase<'db>) -> Self {
EncodeOptions { database, ..self }
}
pub(crate) fn use_reflection(&self) -> bool {
self.property_behavior != EncodePropertyBehavior::NoReflection
}
}
impl<'db> Default for EncodeOptions<'db> {
fn default() -> EncodeOptions<'db> {
EncodeOptions::new()
}
}
pub struct EmitState<'db> {
options: EncodeOptions<'db>,
referent_map: HashMap<Ref, u32>,
next_referent: u32,
shared_strings_to_emit: BTreeMap<SharedStringHash, SharedString>,
}
impl<'db> EmitState<'db> {
pub fn new(options: EncodeOptions<'db>) -> EmitState<'db> {
EmitState {
options,
referent_map: HashMap::new(),
next_referent: 0,
shared_strings_to_emit: BTreeMap::new(),
}
}
pub fn map_id(&mut self, id: Ref) -> u32 {
match self.referent_map.get(&id) {
Some(&value) => value,
None => {
let referent = self.next_referent;
self.referent_map.insert(id, referent);
self.next_referent += 1;
referent
}
}
}
pub fn add_shared_string(&mut self, value: SharedString) {
self.shared_strings_to_emit.insert(value.hash(), value);
}
}
fn serialize_instance<'dom, W: Write>(
writer: &mut XmlEventWriter<W>,
state: &mut EmitState,
tree: &'dom WeakDom,
id: Ref,
property_buffer: &mut Vec<(&'dom str, &'dom Variant)>,
) -> Result<(), NewEncodeError> {
let instance = tree.get_by_ref(id).unwrap();
let mapped_id = state.map_id(id);
writer.write(
XmlWriteEvent::start_element("Item")
.attr("class", &instance.class)
.attr("referent", &mapped_id.to_string()),
)?;
writer.write(XmlWriteEvent::start_element("Properties"))?;
write_value_xml(
writer,
state,
"Name",
&Variant::String(instance.name.clone()),
)?;
property_buffer.extend(instance.properties.iter().map(|(k, v)| (k.as_str(), v)));
property_buffer.sort_unstable_by_key(|(key, _)| *key);
for (property_name, value) in property_buffer.drain(..) {
let maybe_serialized_descriptor = if state.options.use_reflection() {
find_serialized_property_descriptor(
&instance.class,
property_name,
state.options.database,
)
} else {
None
};
if let Some(serialized_descriptor) = maybe_serialized_descriptor {
let data_type = serialized_descriptor.data_type.ty();
let mut serialized_name = serialized_descriptor.name.as_ref();
let mut converted_value = match value.try_convert_ref(instance.class, data_type) {
Ok(value) => value,
Err(message) => {
return Err(
writer.error(EncodeErrorKind::UnsupportedPropertyConversion {
class_name: instance.class.to_string(),
property_name: property_name.to_string(),
expected_type: data_type,
actual_type: value.ty(),
message,
}),
)
}
};
if let PropertyKind::Canonical {
serialization: PropertySerialization::Migrate(migration),
} = &serialized_descriptor.kind
{
if let Ok(new_value) = migration.perform(&converted_value) {
converted_value = Cow::Owned(new_value);
serialized_name = &migration.new_property_name
}
}
write_value_xml(writer, state, serialized_name, &converted_value)?;
} else {
match state.options.property_behavior {
EncodePropertyBehavior::IgnoreUnknown => {}
EncodePropertyBehavior::WriteUnknown | EncodePropertyBehavior::NoReflection => {
write_value_xml(writer, state, property_name, value)?;
}
EncodePropertyBehavior::ErrorOnUnknown => {
return Err(writer.error(EncodeErrorKind::UnknownProperty {
class_name: instance.class.to_string(),
property_name: property_name.to_string(),
}));
}
}
}
}
writer.write(XmlWriteEvent::end_element())?;
for child_id in instance.children() {
serialize_instance(writer, state, tree, *child_id, property_buffer)?;
}
writer.write(XmlWriteEvent::end_element())?;
Ok(())
}
fn serialize_shared_strings<W: Write>(
writer: &mut XmlEventWriter<W>,
state: &mut EmitState,
) -> Result<(), NewEncodeError> {
if state.shared_strings_to_emit.is_empty() {
return Ok(());
}
writer.write(XmlWriteEvent::start_element("SharedStrings"))?;
for value in state.shared_strings_to_emit.values() {
let full_hash = value.hash();
let truncated_hash = &full_hash.as_bytes()[..16];
writer.write(
XmlWriteEvent::start_element("SharedString")
.attr("md5", &base64::encode(truncated_hash)),
)?;
writer.write_string(&base64::encode(value.data()))?;
writer.end_element()?;
}
writer.end_element()?;
Ok(())
}