use super::{
Error, PropertyType, RecordIdentifier, RecordType, RecordWriter, Result, SegmentSink,
compare_utf16_strings,
};
pub(crate) const TEMPLATE_DEDUP_BUDGET_BYTES: usize = 32 * 1024 * 1024;
pub struct PropertyToWrite {
pub name: String,
pub property_type: PropertyType,
pub values: PropertyValuesToWrite,
}
pub enum PropertyValuesToWrite {
Single(RecordIdentifier),
Multiple(Vec<RecordIdentifier>),
PreservedSlot {
value_slot: RecordIdentifier,
is_multiple: bool,
},
}
pub enum ChildNodesToWrite {
Zero,
One {
name: String,
node: RecordIdentifier,
},
Many(Vec<(String, RecordIdentifier)>),
ManyExistingMap(RecordIdentifier),
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub(crate) struct TemplateKey {
pub(crate) primary_type: Option<String>,
pub(crate) mixin_types: Vec<String>,
pub(crate) child_arity: u8,
pub(crate) single_child_name: Option<String>,
pub(crate) properties: Vec<(String, u8)>,
}
impl TemplateKey {
pub(in crate::writer) fn of(
primary_type: Option<&str>,
mixin_types: &[String],
child_nodes: &ChildNodesToWrite,
properties: &[PropertyToWrite],
) -> Self {
let (child_arity, single_child_name) = match child_nodes {
ChildNodesToWrite::Zero => (0u8, None),
ChildNodesToWrite::One { name, .. } => (1u8, Some(name.clone())),
ChildNodesToWrite::Many(_) | ChildNodesToWrite::ManyExistingMap(_) => (2u8, None),
};
Self {
primary_type: primary_type.map(str::to_owned),
mixin_types: mixin_types.to_vec(),
child_arity,
single_child_name,
properties: properties
.iter()
.map(|property| {
(
property.name.clone(),
property_slot_tag(property.property_type, &property.values),
)
})
.collect(),
}
}
}
pub(crate) fn property_slot_tag(property_type: PropertyType, values: &PropertyValuesToWrite) -> u8 {
let multiple = match values {
PropertyValuesToWrite::Single(_) => false,
PropertyValuesToWrite::Multiple(_) => true,
PropertyValuesToWrite::PreservedSlot { is_multiple, .. } => *is_multiple,
};
let tag = property_type as i8;
(if multiple { -tag } else { tag }) as u8
}
pub(crate) fn stable_identifier_names(
stable_identifier: Option<[u8; 20]>,
record: RecordIdentifier,
) -> bool {
let Some(bytes) = stable_identifier else {
return false;
};
let most = u64::from_be_bytes(bytes[0..8].try_into().expect("8 bytes"));
let least = u64::from_be_bytes(bytes[8..16].try_into().expect("8 bytes"));
let number = u32::from_be_bytes(bytes[16..20].try_into().expect("4 bytes"));
most == record.segment.most_significant_bits
&& least == record.segment.least_significant_bits
&& number == record.record_number
}
pub fn sort_properties_for_template(properties: &mut [PropertyToWrite]) {
properties.sort_by(|first, second| {
crate::hashing::utf16_string_hash(&first.name)
.cmp(&crate::hashing::utf16_string_hash(&second.name))
.then_with(|| compare_utf16_strings(&first.name, &second.name))
.then_with(|| (first.property_type as u8).cmp(&(second.property_type as u8)))
});
}
impl<Sink: SegmentSink> RecordWriter<Sink> {
#[allow(
clippy::missing_panics_doc,
reason = "record slice indexing is in-bounds by construction of the allocation"
)]
pub fn write_template(
&mut self,
primary_type: Option<&str>,
mixin_types: &[String],
child_nodes: &ChildNodesToWrite,
properties: &[PropertyToWrite],
) -> Result<RecordIdentifier> {
let key = TemplateKey::of(primary_type, mixin_types, child_nodes, properties);
if let Some(existing) = self.template_cache.get(&key) {
return Ok(existing);
}
let written =
self.write_template_record(primary_type, mixin_types, child_nodes, properties)?;
self.template_cache.insert(key, written);
Ok(written)
}
pub(super) fn write_template_record(
&mut self,
primary_type: Option<&str>,
mixin_types: &[String],
child_nodes: &ChildNodesToWrite,
properties: &[PropertyToWrite],
) -> Result<RecordIdentifier> {
if mixin_types.len() >= 1 << 10 {
return Err(Error::InvalidFormat {
details: format!(
"{} mixin types exceed the template limit of 1023",
mixin_types.len()
),
});
}
if properties.len() >= 1 << 18 {
return Err(Error::InvalidFormat {
details: format!(
"{} properties exceed the template limit of 262143",
properties.len()
),
});
}
let mut head = 0u32;
let mut trailing_identifiers: Vec<RecordIdentifier> = Vec::new();
let primary_type_identifier = match primary_type {
Some(name) => {
head |= 1 << 31;
Some(self.write_string(name)?)
}
None => None,
};
let mut mixin_identifiers = Vec::with_capacity(mixin_types.len());
if !mixin_types.is_empty() {
head |= 1 << 30;
head |= (mixin_types.len() as u32) << 18;
for mixin in mixin_types {
mixin_identifiers.push(self.write_string(mixin)?);
}
}
let single_child_name_identifier = match child_nodes {
ChildNodesToWrite::Zero => {
head |= 1 << 29;
None
}
ChildNodesToWrite::Many(_) | ChildNodesToWrite::ManyExistingMap(_) => {
head |= 1 << 28;
None
}
ChildNodesToWrite::One { name, .. } => Some(self.write_string(name)?),
};
head |= properties.len() as u32;
let property_names_identifier = if properties.is_empty() {
None
} else {
let mut name_identifiers = Vec::with_capacity(properties.len());
for property in properties {
name_identifiers.push(self.write_string(&property.name)?);
}
Some(
self.write_list_body(&name_identifiers)?
.expect("non-empty list"),
)
};
trailing_identifiers.extend(primary_type_identifier);
trailing_identifiers.extend(mixin_identifiers.iter().copied());
trailing_identifiers.extend(single_child_name_identifier);
trailing_identifiers.extend(property_names_identifier);
let size = 4 + trailing_identifiers.len() * 6 + properties.len();
let record = self.allocate(RecordType::Template, size, &trailing_identifiers)?;
self.current.record_bytes_mut(record)[0..4].copy_from_slice(&head.to_be_bytes());
let mut cursor = 4;
for identifier in &trailing_identifiers {
self.write_identifier_at(record, cursor, *identifier);
cursor += 6;
}
for property in properties {
self.current.record_bytes_mut(record)[cursor] =
property_slot_tag(property.property_type, &property.values);
cursor += 1;
}
Ok(self.identifier_of(record))
}
#[allow(
clippy::missing_panics_doc,
reason = "record slice indexing is in-bounds by construction of the allocation"
)]
pub fn write_node(
&mut self,
primary_type: Option<&str>,
mixin_types: &[String],
child_nodes: &ChildNodesToWrite,
properties: &[PropertyToWrite],
) -> Result<RecordIdentifier> {
self.write_node_with_stable_identifier(
primary_type,
mixin_types,
child_nodes,
properties,
None,
)
}
#[allow(
clippy::missing_panics_doc,
reason = "record slice indexing is in-bounds by construction of the allocation"
)]
pub fn write_node_with_stable_identifier(
&mut self,
primary_type: Option<&str>,
mixin_types: &[String],
child_nodes: &ChildNodesToWrite,
properties: &[PropertyToWrite],
stable_identifier: Option<[u8; 20]>,
) -> Result<RecordIdentifier> {
let template_identifier =
self.write_template(primary_type, mixin_types, child_nodes, properties)?;
let child_identifier = match child_nodes {
ChildNodesToWrite::Zero => None,
ChildNodesToWrite::One { node, .. } => Some(*node),
ChildNodesToWrite::Many(entries) => Some(self.write_map(entries)?),
ChildNodesToWrite::ManyExistingMap(map) => Some(*map),
};
let property_list_identifier = if properties.is_empty() {
None
} else {
let mut value_identifiers = Vec::with_capacity(properties.len());
for property in properties {
let identifier = match &property.values {
PropertyValuesToWrite::Single(value) => *value,
PropertyValuesToWrite::Multiple(values) => self.write_counted_list(values)?,
PropertyValuesToWrite::PreservedSlot { value_slot, .. } => *value_slot,
};
value_identifiers.push(identifier);
}
Some(
self.write_list_body(&value_identifiers)?
.expect("non-empty list"),
)
};
let stable_block = match stable_identifier {
Some(bytes) => {
let record = self.allocate(RecordType::Block, 20, &[])?;
self.current.record_bytes_mut(record)[..20].copy_from_slice(&bytes);
Some(self.identifier_of(record))
}
None => None,
};
let mut slots: Vec<RecordIdentifier> = vec![template_identifier];
slots.extend(child_identifier);
slots.extend(property_list_identifier);
let mut referenced = slots.clone();
referenced.extend(stable_block);
let size = 6 + slots.len() * 6;
let record = self.allocate(RecordType::Node, size, &referenced)?;
let own_identifier = self.identifier_of(record);
let slot_zero = match stable_block {
Some(block) => {
if stable_identifier_names(stable_identifier, own_identifier) {
own_identifier
} else {
block
}
}
None => own_identifier,
};
self.write_identifier_at(record, 0, slot_zero);
for (position, identifier) in slots.iter().enumerate() {
self.write_identifier_at(record, 6 + position * 6, *identifier);
}
Ok(own_identifier)
}
}
#[cfg(test)]
mod tests {
use super::{
ChildNodesToWrite, PropertyToWrite, PropertyValuesToWrite, sort_properties_for_template,
};
use crate::content::node::{NodeState, PropertyValues};
use crate::content::property::{PropertyType, PropertyValue};
use crate::writer::record_writer::test_support::new_writer;
#[test]
fn nodes_round_trip_with_properties_and_children() {
let mut writer = new_writer();
let leaf = writer
.write_node(Some("nt:unstructured"), &[], &ChildNodesToWrite::Zero, &[])
.expect("leaf");
let title_value = writer.write_string("Hello").expect("value");
let first_tag = writer.write_string("alpha").expect("value");
let second_tag = writer.write_string("beta").expect("value");
let count_value = writer.write_string("42").expect("value");
let mut properties = vec![
PropertyToWrite {
name: "title".to_owned(),
property_type: PropertyType::String,
values: PropertyValuesToWrite::Single(title_value),
},
PropertyToWrite {
name: "tags".to_owned(),
property_type: PropertyType::String,
values: PropertyValuesToWrite::Multiple(vec![first_tag, second_tag]),
},
PropertyToWrite {
name: "count".to_owned(),
property_type: PropertyType::Long,
values: PropertyValuesToWrite::Single(count_value),
},
];
sort_properties_for_template(&mut properties);
let parent = writer
.write_node(
Some("nt:unstructured"),
&["mix:versionable".to_owned()],
&ChildNodesToWrite::Many(vec![
("first".to_owned(), leaf),
("second".to_owned(), leaf),
]),
&properties,
)
.expect("parent");
let store = writer.finish().expect("finish");
let node = NodeState::new(&store, parent);
let template = node.template().expect("template");
assert_eq!(template.primary_type.as_deref(), Some("nt:unstructured"));
assert_eq!(template.mixin_types, vec!["mix:versionable"]);
let title = node.property("title").expect("read").expect("present");
assert_eq!(
title.values,
PropertyValues::Single(PropertyValue::String("Hello".to_owned()))
);
let count = node.property("count").expect("read").expect("present");
assert_eq!(
count.values,
PropertyValues::Single(PropertyValue::Long(42))
);
let tags = node.property("tags").expect("read").expect("present");
assert_eq!(
tags.values,
PropertyValues::Multiple(vec![
PropertyValue::String("alpha".to_owned()),
PropertyValue::String("beta".to_owned()),
])
);
assert_eq!(node.child_node_count().expect("count"), 2);
let first = node.child_node("first").expect("lookup").expect("present");
assert_eq!(first.record_identifier(), leaf);
assert_eq!(
node.stable_identifier().expect("stable"),
format!("{}:{}", parent.segment, parent.record_number as i32)
);
}
#[test]
fn a_preserved_multi_valued_slot_never_shares_a_template_with_a_single_valued_one() {
let mut writer = new_writer();
let first_tag = writer.write_string("alpha").expect("value");
let second_tag = writer.write_string("beta").expect("value");
let preserved_list = writer
.write_counted_list(&[first_tag, second_tag])
.expect("counted list");
let lone_value = writer.write_string("solo").expect("value");
let multi_valued = writer
.write_node(
Some("nt:unstructured"),
&[],
&ChildNodesToWrite::Zero,
&[PropertyToWrite {
name: "tags".to_owned(),
property_type: PropertyType::String,
values: PropertyValuesToWrite::PreservedSlot {
value_slot: preserved_list,
is_multiple: true,
},
}],
)
.expect("multi-valued node");
let single_valued = writer
.write_node(
Some("nt:unstructured"),
&[],
&ChildNodesToWrite::Zero,
&[PropertyToWrite {
name: "tags".to_owned(),
property_type: PropertyType::String,
values: PropertyValuesToWrite::Single(lone_value),
}],
)
.expect("single-valued node");
let store = writer.finish().expect("finish");
let multi_template = NodeState::new(&store, multi_valued)
.template()
.expect("multi template");
let single_template = NodeState::new(&store, single_valued)
.template()
.expect("single template");
assert!(
multi_template.properties[0].is_multiple,
"the preserved slot must serialize as multi-valued"
);
assert!(
!single_template.properties[0].is_multiple,
"the single-valued slot must not inherit the multi-valued template"
);
let multi_tags = NodeState::new(&store, multi_valued)
.property("tags")
.expect("read")
.expect("present");
assert_eq!(
multi_tags.values,
PropertyValues::Multiple(vec![
PropertyValue::String("alpha".to_owned()),
PropertyValue::String("beta".to_owned()),
])
);
let single_tags = NodeState::new(&store, single_valued)
.property("tags")
.expect("read")
.expect("present");
assert_eq!(
single_tags.values,
PropertyValues::Single(PropertyValue::String("solo".to_owned()))
);
}
#[test]
fn template_property_sort_orders_by_signed_hash_then_name_then_type() {
use crate::content::property::PropertyType;
use crate::segment::identifier::SegmentIdentifier;
use crate::segment::record::RecordIdentifier;
use crate::writer::record_writer::sort_properties_for_template;
let property = |name: &str, property_type: PropertyType| PropertyToWrite {
name: name.to_owned(),
property_type,
values: PropertyValuesToWrite::Single(RecordIdentifier::new(
SegmentIdentifier::new(0, 0xA000_0000_0000_0001),
0,
)),
};
let mut properties = vec![
property("title", PropertyType::String),
property("BB", PropertyType::Long),
property("count", PropertyType::Long),
property("count", PropertyType::String),
property("Aa", PropertyType::String),
property("active", PropertyType::Boolean),
];
sort_properties_for_template(&mut properties);
let names_and_types: Vec<(&str, PropertyType)> = properties
.iter()
.map(|property| (property.name.as_str(), property.property_type))
.collect();
assert_eq!(
names_and_types,
[
("active", PropertyType::Boolean),
("Aa", PropertyType::String),
("BB", PropertyType::Long),
("count", PropertyType::String),
("count", PropertyType::Long),
("title", PropertyType::String),
]
);
}
}