use std::collections::HashMap;
use crate::v2;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProjectionError {
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TableLayout {
pub slots: Vec<FieldSlot>,
}
impl TableLayout {
#[must_use]
pub fn vtable_slots(&self) -> u64 {
self.slots
.iter()
.map(|slot| u64::from(slot.id) + 1)
.max()
.unwrap_or(0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldSlot {
pub id: u32,
pub source: SlotSource,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SlotSource {
Field { name: String, ordinal: u32 },
Retired { ordinal: u32 },
TupleField { position: u32 },
Key,
Value,
Wrapped,
}
pub fn struct_table(owner: &str, def: &v2::StructDef) -> Result<TableLayout, ProjectionError> {
let mut slots: Vec<FieldSlot> = Vec::with_capacity(def.members.len());
for member in &def.members {
match &member.member {
Some(v2::struct_member::Member::Field(field)) => slots.push(FieldSlot {
id: slot_id(owner, field.ordinal)?,
source: SlotSource::Field {
name: field.name.clone(),
ordinal: field.ordinal,
},
}),
Some(v2::struct_member::Member::Reserved(reserved)) => slots.push(FieldSlot {
id: slot_id(owner, reserved.ordinal)?,
source: SlotSource::Retired {
ordinal: reserved.ordinal,
},
}),
None => {}
}
if let Some(last) = slots.last()
&& slots[..slots.len() - 1]
.iter()
.any(|slot| slot.id == last.id)
{
return Err(ProjectionError {
message: format!(
"`{owner}` carries two struct members with ordinal {}, which FlatBuffers \
cannot represent — one id names one field (typl §7.4).",
last.id + 1
),
});
}
}
Ok(TableLayout { slots })
}
pub fn tuple_table(name: &str, tuple: &v2::TupleType) -> Result<TableLayout, ProjectionError> {
let mut slots = Vec::with_capacity(tuple.fields.len());
for index in 0..tuple.fields.len() {
let position = u32::try_from(index + 1).map_err(|_| ProjectionError {
message: format!("{name} has more tuple fields than a FlatBuffers id can carry."),
})?;
slots.push(FieldSlot {
id: position - 1,
source: SlotSource::TupleField { position },
});
}
Ok(TableLayout { slots })
}
pub const MAP_ENTRY_KEY_ID: u32 = 0;
pub const MAP_ENTRY_VALUE_ID: u32 = 1;
pub const UNION_WRAPPER_VALUE_ID: u32 = 1;
pub const UNION_ARM_BOX_VALUE_ID: u32 = 0;
#[must_use]
pub fn map_entry_table() -> TableLayout {
TableLayout {
slots: vec![
FieldSlot {
id: MAP_ENTRY_KEY_ID,
source: SlotSource::Key,
},
FieldSlot {
id: MAP_ENTRY_VALUE_ID,
source: SlotSource::Value,
},
],
}
}
#[must_use]
pub fn union_wrapper_table() -> TableLayout {
TableLayout {
slots: vec![FieldSlot {
id: UNION_WRAPPER_VALUE_ID,
source: SlotSource::Wrapped,
}],
}
}
#[must_use]
pub fn union_arm_box_table() -> TableLayout {
TableLayout {
slots: vec![FieldSlot {
id: UNION_ARM_BOX_VALUE_ID,
source: SlotSource::Wrapped,
}],
}
}
#[must_use]
pub fn root_box_table() -> TableLayout {
union_arm_box_table()
}
#[must_use]
pub fn union_arm_discriminant(arm: &v2::UnionArm) -> u32 {
arm.ordinal
}
#[must_use]
pub fn enum_field_needs_null_default(def: &v2::EnumDef) -> bool {
!def.values.iter().any(|value| value.value == 0)
}
fn slot_id(owner: &str, ordinal: u32) -> Result<u32, ProjectionError> {
ordinal.checked_sub(1).ok_or_else(|| ProjectionError {
message: format!(
"`{owner}` carries a struct member with ordinal 0, which FlatBuffers ids cannot \
represent — typl ordinals start at 1 (typl §7.4)."
),
})
}
#[derive(Clone, Copy)]
pub struct Packages<'a> {
pub package: &'a v2::Package,
pub others: &'a [&'a v2::Package],
}
#[must_use]
pub fn root_table(decl: &v2::Decl) -> Option<RootTable> {
match decl.kind {
Some(v2::decl::Kind::StructDef(_)) => Some(RootTable::Own),
Some(v2::decl::Kind::UnionDef(_)) => Some(RootTable::UnionWrapper),
Some(v2::decl::Kind::TypeDef(_))
| Some(v2::decl::Kind::EnumDef(_))
| Some(v2::decl::Kind::EnumSetDef(_)) => Some(RootTable::Box),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RootTable {
Own,
UnionWrapper,
Box,
}
#[must_use]
pub fn max_size(packages: Packages<'_>, decl: &v2::Decl) -> Option<u64> {
let mut sizer = Sizer {
packages,
visiting: Vec::new(),
computed: HashMap::new(),
};
let body = match &decl.kind {
Some(v2::decl::Kind::StructDef(def)) => {
sizer.struct_table_bound(packages.package, &decl.name, def)?
}
Some(v2::decl::Kind::UnionDef(def)) => {
sizer.union_wrapper_bound(packages.package, &decl.name, def)?
}
Some(v2::decl::Kind::TypeDef(_))
| Some(v2::decl::Kind::EnumDef(_))
| Some(v2::decl::Kind::EnumSetDef(_)) => sizer.root_box_bound(packages.package, decl)?,
_ => return None,
};
match ROOT.checked_add(body) {
Some(bound) if bound <= MAX_ENCODABLE => Some(bound),
_ => None,
}
}
pub const MAX_ENCODABLE: u64 = u32::MAX as u64;
pub const ALIGN_SLACK: u64 = 7;
const OFFSET: u64 = 4;
const VTABLE_HEADER: u64 = 4;
const VTABLE_SLOT: u64 = 2;
const ROOT: u64 = OFFSET + ALIGN_SLACK;
#[derive(Clone, Copy)]
struct Charge {
inline: u64,
out_of_line: u64,
}
impl Charge {
const fn inline_only(inline: u64) -> Self {
Self {
inline,
out_of_line: 0,
}
}
}
struct Sizer<'a> {
packages: Packages<'a>,
visiting: Vec<String>,
computed: HashMap<String, u64>,
}
impl<'a> Sizer<'a> {
fn resolve(
&self,
home: &'a v2::Package,
reference: &str,
) -> Option<(&'a v2::Decl, &'a v2::Package)> {
match reference.rsplit_once('.') {
Some((referenced_package, member)) => std::iter::once(self.packages.package)
.chain(self.packages.others.iter().copied())
.find(|candidate| candidate.name == referenced_package)
.and_then(|candidate| {
candidate
.decls
.iter()
.find(|decl| decl.name == member)
.map(|decl| (decl, candidate))
}),
None => home
.decls
.iter()
.find(|decl| decl.name == reference)
.map(|decl| (decl, home)),
}
}
fn guarded(&mut self, key: String, body: impl FnOnce(&mut Self) -> Option<u64>) -> Option<u64> {
if let Some(bound) = self.computed.get(&key) {
return Some(*bound);
}
if self.visiting.contains(&key) {
return None;
}
self.visiting.push(key.clone());
let out = body(self);
self.visiting.pop();
if let Some(bound) = out {
self.computed.insert(key, bound);
}
out
}
fn table_bound(&self, layout: &TableLayout, inline: u64, out_of_line: u64) -> Option<u64> {
let slots = layout.vtable_slots();
let vtable = VTABLE_HEADER
.checked_add(VTABLE_SLOT.checked_mul(slots)?)?
.checked_add(ALIGN_SLACK)?;
let padding = ALIGN_SLACK.checked_mul(slots.checked_add(1)?)?;
OFFSET
.checked_add(inline)?
.checked_add(padding)?
.checked_add(vtable)?
.checked_add(out_of_line)
}
fn struct_table_bound(
&mut self,
home: &'a v2::Package,
owner: &str,
def: &v2::StructDef,
) -> Option<u64> {
let layout = struct_table(owner, def).ok()?;
self.guarded(format!("{}.{owner}", home.name), |sizer| {
let mut inline = 0u64;
let mut out_of_line = 0u64;
for member in &def.members {
let charge = match &member.member {
Some(v2::struct_member::Member::Field(field)) => {
sizer.field_charge(home, field.r#type.as_ref()?)?
}
Some(v2::struct_member::Member::Reserved(_)) => Charge::inline_only(1),
None => continue,
};
inline = inline.checked_add(charge.inline)?;
out_of_line = out_of_line.checked_add(charge.out_of_line)?;
}
sizer.table_bound(&layout, inline, out_of_line)
})
}
fn union_wrapper_bound(
&mut self,
home: &'a v2::Package,
owner: &str,
def: &v2::UnionDef,
) -> Option<u64> {
let layout = union_wrapper_table();
self.guarded(format!("{}.{owner}", home.name), |sizer| {
let mut largest_arm = 0u64;
for arm in &def.arms {
largest_arm = largest_arm.max(sizer.union_arm_bound(home, arm)?);
}
sizer.table_bound(&layout, 1 + OFFSET, largest_arm)
})
}
fn union_arm_bound(&mut self, home: &'a v2::Package, arm: &v2::UnionArm) -> Option<u64> {
let (decl, declaring) = self.resolve(home, &arm.type_ref)?;
match &decl.kind {
Some(v2::decl::Kind::StructDef(def)) => {
self.struct_table_bound(declaring, &decl.name, def)
}
Some(v2::decl::Kind::UnionDef(def)) => {
self.union_wrapper_bound(declaring, &decl.name, def)
}
Some(v2::decl::Kind::TypeDef(_))
| Some(v2::decl::Kind::EnumDef(_))
| Some(v2::decl::Kind::EnumSetDef(_)) => {
let charge = self.named_charge(home, &arm.type_ref)?;
self.table_bound(&union_arm_box_table(), charge.inline, charge.out_of_line)
}
_ => None,
}
}
fn root_box_bound(&mut self, home: &'a v2::Package, decl: &v2::Decl) -> Option<u64> {
let charge = self.named_charge(home, &decl.name)?;
self.table_bound(&root_box_table(), charge.inline, charge.out_of_line)
}
fn tuple_table_bound(&mut self, home: &'a v2::Package, tuple: &v2::TupleType) -> Option<u64> {
let layout = tuple_table("", tuple).ok()?;
let mut inline = 0u64;
let mut out_of_line = 0u64;
for field in &tuple.fields {
let charge = self.field_charge(home, field.r#type.as_ref()?)?;
inline = inline.checked_add(charge.inline)?;
out_of_line = out_of_line.checked_add(charge.out_of_line)?;
}
self.table_bound(&layout, inline, out_of_line)
}
fn map_entry_bound(&mut self, home: &'a v2::Package, map: &v2::MapType) -> Option<u64> {
let key = self.field_charge(home, map.key.as_ref()?)?;
let value = self.field_charge(home, map.value.as_ref()?)?;
self.table_bound(
&map_entry_table(),
key.inline.checked_add(value.inline)?,
key.out_of_line.checked_add(value.out_of_line)?,
)
}
fn vector_charge(count: u64, element: Charge) -> Option<Charge> {
let out_of_line = OFFSET
.checked_add(count.checked_mul(element.inline)?)?
.checked_add(ALIGN_SLACK)?
.checked_add(count.checked_mul(element.out_of_line)?)?;
Some(Charge {
inline: OFFSET,
out_of_line,
})
}
fn field_charge(&mut self, home: &'a v2::Package, ty: &v2::FieldType) -> Option<Charge> {
match ty.kind.as_ref()? {
v2::field_type::Kind::Primitive(primitive) => {
match v2::PrimitiveType::try_from(*primitive).ok()? {
v2::PrimitiveType::Boolean => Some(Charge::inline_only(1)),
v2::PrimitiveType::Integer | v2::PrimitiveType::Float => {
Some(Charge::inline_only(8))
}
_ => None,
}
}
v2::field_type::Kind::InlineScalar(td) => self.scalar_charge(td),
v2::field_type::Kind::Named(reference) => self.named_charge(home, reference),
v2::field_type::Kind::Array(array) => {
let element = self.field_charge(home, array.element.as_ref()?)?;
Self::vector_charge(array.max, element)
}
v2::field_type::Kind::Map(map) => {
let entry = self.map_entry_bound(home, map)?;
Self::vector_charge(
map.max,
Charge {
inline: OFFSET,
out_of_line: entry,
},
)
}
v2::field_type::Kind::Tuple(tuple) => Some(Charge {
inline: OFFSET,
out_of_line: self.tuple_table_bound(home, tuple)?,
}),
_ => None,
}
}
fn named_charge(&mut self, home: &'a v2::Package, reference: &str) -> Option<Charge> {
let (decl, declaring) = self.resolve(home, reference)?;
match &decl.kind {
Some(v2::decl::Kind::TypeDef(td)) => self.scalar_charge(td),
Some(v2::decl::Kind::StructDef(def)) => Some(Charge {
inline: OFFSET,
out_of_line: self.struct_table_bound(declaring, &decl.name, def)?,
}),
Some(v2::decl::Kind::EnumDef(_)) => Some(Charge::inline_only(8)),
Some(v2::decl::Kind::EnumSetDef(esd)) => {
Some(Charge::inline_only(int_width_bytes(esd.width)?))
}
Some(v2::decl::Kind::UnionDef(def)) => Some(Charge {
inline: OFFSET,
out_of_line: self.union_wrapper_bound(declaring, &decl.name, def)?,
}),
_ => None,
}
}
fn scalar_charge(&self, td: &v2::TypeDef) -> Option<Charge> {
match &td.width {
Some(v2::type_def::Width::IntWidth(width)) => {
Some(Charge::inline_only(int_width_bytes(*width)?))
}
Some(v2::type_def::Width::FloatWidth(width)) => {
match v2::FloatWidth::try_from(*width).ok()? {
v2::FloatWidth::F32 => Some(Charge::inline_only(4)),
v2::FloatWidth::F64 => Some(Charge::inline_only(8)),
v2::FloatWidth::Unspecified => None,
}
}
None => match td.backing.as_ref()?.kind.as_ref()? {
v2::backing::Kind::Primitive(primitive) => {
match v2::PrimitiveType::try_from(*primitive).ok()? {
v2::PrimitiveType::Boolean => Some(Charge::inline_only(1)),
v2::PrimitiveType::Bytes => {
let length = td.constraint.as_ref()?.len_max?;
Some(Charge {
inline: OFFSET,
out_of_line: OFFSET
.checked_add(length)?
.checked_add(ALIGN_SLACK)?,
})
}
v2::PrimitiveType::String => {
let characters = td.constraint.as_ref()?.len_max?;
Some(Charge {
inline: OFFSET,
out_of_line: OFFSET
.checked_add(characters.checked_mul(4)?)?
.checked_add(1)?
.checked_add(ALIGN_SLACK)?,
})
}
_ => None,
}
}
_ => None,
},
}
}
}
fn int_width_bytes(width: i32) -> Option<u64> {
match v2::IntWidth::try_from(width).ok()? {
v2::IntWidth::U8 | v2::IntWidth::I8 => Some(1),
v2::IntWidth::U16 | v2::IntWidth::I16 => Some(2),
v2::IntWidth::U32 | v2::IntWidth::I32 => Some(4),
v2::IntWidth::U64 | v2::IntWidth::I64 => Some(8),
v2::IntWidth::Unspecified => None,
}
}
#[cfg(test)]
mod tests;