blob: 2bade9b63ca0c3ec4c1af880d8e3568b9113244e [file]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::ast::*;
use anyhow::{bail, Context, Result};
use std::collections::BTreeMap;
use std::sync::Arc;
/// Wrapper type for the output of the deparser
pub struct DeparsedData {
bytes: Vec<u8>,
handles: Vec<UntypedHandle>,
}
impl Default for DeparsedData {
fn default() -> Self {
Self::new()
}
}
impl DeparsedData {
pub fn new() -> Self {
DeparsedData { bytes: vec![], handles: vec![] }
}
pub fn into_parts(self) -> (Vec<u8>, Vec<UntypedHandle>) {
(self.bytes, self.handles)
}
}
// Convenient implementations: In almost all circumstances we only care about
// the `bytes` field, so make that easily available (via the . operator).
// The fields can also be accessed directly, of course.
impl std::ops::Deref for DeparsedData {
type Target = Vec<u8>;
fn deref(&self) -> &Self::Target {
&self.bytes
}
}
impl std::ops::DerefMut for DeparsedData {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.bytes
}
}
/// Return a reference to the field at index `ordinal`
fn get_field_at_ordinal(field_values: &mut [MojomValue], ordinal: Ordinal) -> Result<&MojomValue> {
field_values.get(ordinal).with_context(|| {
anyhow::anyhow!(
"Wire type asked for field with ordinal {}, but there are only {} fields.",
ordinal,
field_values.len()
)
})
}
/// Take ownership of the field at index `ordinal` in the vector, replacing it
/// with an invalid value. Fails if the value was already taken.
fn take_field_at_ordinal(field_values: &mut [MojomValue], ordinal: Ordinal) -> Result<MojomValue> {
let Some(field_ref) = field_values.get_mut(ordinal) else {
anyhow::bail!(
"Wire type asked for field with ordinal {}, but there are only {} fields.",
ordinal,
field_values.len()
)
};
let field_value = std::mem::take(field_ref);
if field_value == MojomValue::Invalid {
anyhow::bail!(
"Wire type tried to retrieve the field with with ordinal {} multiple times!",
ordinal,
)
};
Ok(field_value)
}
/// Abstract over possibly-nullable MojomValues.
///
/// This function takes a MojomValue which _might_ be nullable, and returns an
/// option:
/// - If `value` is not nullable, it returns `Some(value)`.
/// - If `value` is Nullable(Some(inner_value)), it returns `Some(inner_value)`.
/// - If `value` is None, it returns `None`.
///
/// If the value is not of the expected nullability, it returns an error.
///
/// This function allows other code to not care about whether the value was
/// originally a nullable or not, since `value` and `Nullable(Some(value))`
/// map to the same thing.
///
/// Note that:
/// - If `None` is returned, it means the original value was _validly_ `None`.
/// - If `Some(value)` is returned, then `value` should not be a
/// `MojomValue::Nullable`, because Mojom does not permit nested nullable
/// types.
fn flatten_possibly_nullable_value(
maybe_nullable: MojomValue,
expect_nullable: bool,
) -> Result<Option<MojomValue>> {
match maybe_nullable {
MojomValue::Nullable(_) if !expect_nullable => {
bail!("Got nullable value for non-nullable type")
}
MojomValue::Nullable(None) => Ok(None),
MojomValue::Nullable(Some(inner_value)) => Ok(Some(*inner_value)),
_ if expect_nullable => bail!("Got non-nullable value for nullable type"),
_ => Ok(Some(maybe_nullable)),
}
}
/// If the contained value is (validly) null, then write num_bytes empty bytes
/// to the data vector; otherwise, return the contained value.
///
/// This is a macro instead of a function so we can `continue` inside it.
macro_rules! write_or_extract_nullable {
($data:expr, $val:expr, $is_nullable:expr, $num_bytes:expr, $none_indicator_byte:expr) => {
match flatten_possibly_nullable_value($val, $is_nullable)? {
None => {
// For a null value, we need only write a bunch of 0s
$data.extend(vec![$none_indicator_byte; $num_bytes]);
continue;
}
Some(v) => v,
}
};
($data:expr, $val:expr, $is_nullable:expr, $num_bytes:expr) => {
write_or_extract_nullable!($data, $val, $is_nullable, $num_bytes, 0x00)
};
}
// Standard error message for getting a value that doesn't match its type
macro_rules! wrong_type {
($expected_type:expr, $value:expr) => {
bail!("Expected to find type {:?}, but got value {:?}", $expected_type, $value)
};
}
fn pad_to_alignment(data: &mut DeparsedData, alignment: usize) {
let mismatch = data.len() % alignment;
if mismatch != 0 {
data.extend(vec![0; alignment - mismatch])
}
}
/// Write out the bytes for a leaf node
fn deparse_leaf_value(
data: &mut DeparsedData,
value: MojomValue,
leaf_type: &PackedLeafType,
) -> Result<()> {
match (value, leaf_type) {
(MojomValue::Bool(value), PackedLeafType::Bool) => {
data.extend(u8::from(value).to_le_bytes())
}
(MojomValue::Int8(value), PackedLeafType::Int8) => data.extend(value.to_le_bytes()),
(MojomValue::UInt8(value), PackedLeafType::UInt8) => data.extend(value.to_le_bytes()),
(MojomValue::Int16(value), PackedLeafType::Int16) => data.extend(value.to_le_bytes()),
(MojomValue::UInt16(value), PackedLeafType::UInt16) => data.extend(value.to_le_bytes()),
(MojomValue::Int32(value), PackedLeafType::Int32) => data.extend(value.to_le_bytes()),
(MojomValue::UInt32(value), PackedLeafType::UInt32) => data.extend(value.to_le_bytes()),
(MojomValue::Int64(value), PackedLeafType::Int64) => data.extend(value.to_le_bytes()),
(MojomValue::UInt64(value), PackedLeafType::UInt64) => data.extend(value.to_le_bytes()),
(MojomValue::Float32(value), PackedLeafType::Float32) => data.extend(value.to_le_bytes()),
(MojomValue::Float64(value), PackedLeafType::Float64) => data.extend(value.to_le_bytes()),
(MojomValue::Enum(value), PackedLeafType::Enum { .. }) => data.extend(value.to_le_bytes()),
(MojomValue::Handle(handle), PackedLeafType::Handle) => {
// Handles are represented on the wire as a 32-bit index into the
// attached handles array. So instead of writing the value directly
// to the wire, push it to the array and write its index instead.
let handle_idx = u32::try_from(data.handles.len()).unwrap();
data.handles.push(handle);
data.extend(handle_idx.to_le_bytes())
}
(value, _) => wrong_type!(leaf_type, value),
}
Ok(())
}
// Mojom structs and arrays are never nested inside each other. Instead, they
// are represented by a pointer with an offset to the actual data which appears
// later in the message.
enum NestedData<'a> {
Struct {
field_values: Vec<MojomValue>,
packed_fields: &'a [StructuredBodyElementOwned],
},
Array {
contents: MojomValue,
element_type: &'a Arc<MojomWireType>,
array_type: &'a PackedArrayType,
},
Union {
tag: i32,
value: MojomValue,
variants: &'a BTreeMap<i32, MojomWireType>,
},
Map {
values_map: BTreeMap<MojomValue, MojomValue>,
key_type: &'a Arc<MojomWireType>,
value_type: &'a Arc<MojomWireType>,
},
}
/// Information about a nested struct/array, which we will emit later
struct NestedDataInfo<'a> {
nested_data: NestedData<'a>,
// Logically this can be thought of as a &mut [u8; 8] pointing to the bytes
// in the data vector in which we'll store the pointer value. But since the
// borrow checker isn't that fine in granularity, we store its index instead.
ptr_loc: usize,
}
/// Overwrite data[start..start+len] with the contents of `value`.
/// Panics if the range doesn't exist or the `value` is too short, probably.
fn write_to_slice(data: &mut [u8], start: usize, len: usize, value: &[u8]) {
let struct_size_field: &mut [u8] = &mut data[start..start + len];
struct_size_field.copy_from_slice(&value[0..len]);
}
pub fn deparse_struct(
data: &mut DeparsedData,
field_values: Vec<MojomValue>,
packed_fields: &[StructuredBodyElementOwned],
) -> Result<()> {
// Write the struct's header
data.extend([0; 4]); // Size; we'll fill this in later
data.extend([0; 4]); // Version number; not supported yet
deparse_structured_body(
data,
None,
false,
field_values,
packed_fields.iter().map(StructuredBodyElementOwned::as_ref),
)
}
fn deparse_array(
data: &mut DeparsedData,
contents: MojomValue,
element_type: &Arc<MojomWireType>,
array_type: &PackedArrayType,
) -> Result<()> {
let element_values = match (array_type, contents) {
(PackedArrayType::String, MojomValue::String(string)) => {
return deparse_string(data, string);
}
(
PackedArrayType::SizedArray(_) | PackedArrayType::UnsizedArray,
MojomValue::Array(element_values),
) => element_values,
(array_type, contents) => {
bail!("deparse_array: Got mismatched type and value: {array_type:?} vs. {contents:?}")
}
};
let num_elements = element_values.len();
if let PackedArrayType::SizedArray(expected_num_elements) = array_type
&& *expected_num_elements != num_elements
{
bail!(
"Array type expected {expected_num_elements} elements but had {num_elements} elements."
)
};
data.extend([0; 4]); // Size field of the header; we'll fill this in later
// We don't support more than 2^32 elements.
let num_elements_u32: u32 = num_elements.try_into().unwrap();
data.extend(num_elements_u32.to_le_bytes());
let array_body = crate::pack::pack_array_body(element_type, num_elements);
deparse_structured_body(data, None, true, element_values, array_body.into_iter())
}
/// Serialize a union to the wire.
///
/// See the documentation of parse_union in parse_values.rs for an explanation
/// of the `enclosing_nested_data_list` argument
fn deparse_union<'a>(
data: &mut DeparsedData,
enclosing_nested_data_list: Option<&mut Vec<NestedDataInfo<'a>>>,
tag: i32,
contained_value: MojomValue,
variants: &'a BTreeMap<i32, MojomWireType>,
) -> Result<()> {
// Write the union's header
let expected_wire_type = variants
.get(&tag)
.context(format!("Tag value {tag} is not a valid discriminant for this union!"))?;
data.extend([0; 4]); // Size; we'll fill this in later
data.extend(tag.to_le_bytes()); // Union tag
let struct_ref_element: StructuredBodyElementMixed =
StructuredBodyElement::SingleValue(0, expected_wire_type);
deparse_structured_body(
data,
enclosing_nested_data_list,
false,
vec![contained_value],
std::iter::once(struct_ref_element),
)
}
fn deparse_map(
data: &mut DeparsedData,
values_map: BTreeMap<MojomValue, MojomValue>,
key_type: &Arc<MojomWireType>,
value_type: &Arc<MojomWireType>,
) -> Result<()> {
let (keys, values) = values_map.into_iter().unzip();
let field_values = vec![MojomValue::Array(keys), MojomValue::Array(values)];
let packed_fields = [
StructuredBodyElement::SingleValue(
0,
MojomWireType::Pointer {
nested_data_type: PackedStructuredType::Array {
// This clone is cheap because it's in an Arc
element_type: key_type.clone(),
array_type: PackedArrayType::UnsizedArray,
},
is_nullable: false,
},
),
StructuredBodyElement::SingleValue(
1,
MojomWireType::Pointer {
nested_data_type: PackedStructuredType::Array {
// This clone is cheap because it's in an Arc
element_type: value_type.clone(),
array_type: PackedArrayType::UnsizedArray,
},
is_nullable: false,
},
),
];
deparse_struct(data, field_values, &packed_fields)
}
fn deparse_string(data: &mut DeparsedData, value: String) -> Result<()> {
let bytes = value.as_bytes();
let num_bytes: u32 = bytes
.len()
.try_into()
.with_context(|| "Mojom cannot serialize strings of more than 2^32 bytes")?;
// Write header size (8 + num elements) and number of elements
data.extend(u32::to_le_bytes(8 + num_bytes));
data.extend(u32::to_le_bytes(num_bytes));
// Write the actual string, then pad to 8 byte alignment
data.extend(bytes);
pad_to_alignment(data, 8);
Ok(())
}
/// Deparse the fields of a struct (or union) after having parsed its header
///
/// See the documentation of parse_union in parse_values.rs for an explanation
/// of the `enclosing_nested_data_list` argument
///
/// The is_array argument controls whether we should write padding at the end
/// of the body before or after we record the size of the body in its header.
/// For arrays, the padding is not included in the header; for structs, it is.
// FOR_RELEASE: Try to take the value by value instead of by reference
fn deparse_structured_body<'a, 'b, IterT, BitfieldT>(
data: &mut DeparsedData,
enclosing_nested_data_list: Option<&mut Vec<NestedDataInfo<'a>>>,
is_array: bool,
mut field_values: Vec<MojomValue>,
packed_fields: IterT,
) -> Result<()>
where
BitfieldT: std::fmt::Debug + std::borrow::Borrow<BitfieldOrdinals>,
IterT: Iterator<Item = StructuredBodyElementRef<'a, BitfieldT>>,
{
// Start counting from the beginning of the header, which we already wrote
let initial_bytes = data.len() - 8;
let mut local_nested_data_list: Vec<NestedDataInfo> = vec![];
let nested_data_list = match enclosing_nested_data_list {
Some(list) => list,
None => &mut local_nested_data_list,
};
// Go through all the fields and either write them to the vector, or
// (for nested data) prepare for them to be written later, in order.
for packed_field in packed_fields {
match packed_field {
StructuredBodyElement::Bitfield(ref ordinals) => {
let mut iter = ordinals.borrow().iter().enumerate();
let mut bitfield: u8 = 0;
// Construct the bitfield bit-by-bit
while let Some((idx, Some((ordinal, is_tag_bit)))) = iter.next() {
let bit_mojom_value = get_field_at_ordinal(&mut field_values, *ordinal)?;
let bit_value = match bit_mojom_value {
MojomValue::Bool(bit) => *bit,
MojomValue::Nullable(None) => false,
MojomValue::Nullable(Some(_)) if *is_tag_bit => true,
MojomValue::Nullable(Some(inner_value)) => {
// If deref patterns are ever stabilized, we won't need
// a nested match here.
match &**inner_value {
MojomValue::Bool(bit) => *bit,
_ => bail!("Got non-bool value when deparsing a bitfield"),
}
}
_ => {
wrong_type!(&packed_field, bit_mojom_value);
}
};
bitfield |= (bit_value as u8) << idx;
}
// Now we've set all the bits, write it to the wire
data.push(bitfield)
}
StructuredBodyElement::SingleValue(ordinal, wire_type) => {
match wire_type {
MojomWireType::Leaf { leaf_type, is_nullable } => {
let num_bytes = wire_type.size();
let leaf_value = take_field_at_ordinal(&mut field_values, ordinal)?;
// Null handles are indicated with all `f`s, everything else is all `0`s.
let none_indicator_byte =
if leaf_type == &PackedLeafType::Handle { 0xff } else { 0x00 };
let leaf_value = write_or_extract_nullable!(
data,
leaf_value,
*is_nullable,
num_bytes,
none_indicator_byte
);
pad_to_alignment(data, packed_field.alignment());
deparse_leaf_value(data, leaf_value, leaf_type)?
}
MojomWireType::Pointer { nested_data_type, is_nullable } => {
let nested_data_value = take_field_at_ordinal(&mut field_values, ordinal)?;
let nested_data_value =
write_or_extract_nullable!(data, nested_data_value, *is_nullable, 8);
let nested_data = match (nested_data_value, nested_data_type) {
(
MojomValue::Struct(_field_names, nested_data_fields),
PackedStructuredType::Struct {
packed_field_types,
packed_field_names: _,
num_elements_in_value: _,
},
) => NestedData::Struct {
field_values: nested_data_fields,
packed_fields: packed_field_types,
},
(
nested_data_value @ (MojomValue::Array(_) | MojomValue::String(_)),
PackedStructuredType::Array { element_type, array_type },
) => NestedData::Array {
contents: nested_data_value,
element_type,
array_type,
},
(
MojomValue::Union(tag, value),
PackedStructuredType::Union { variants, .. },
) => NestedData::Union { tag, value: *value, variants },
(
MojomValue::Map(values_map),
PackedStructuredType::Map { key_type, value_type },
) => NestedData::Map { values_map, key_type, value_type },
(_, nested_data_value) => bail!(
"Unexpected type for nested data: Expected {:?}, got {:?}",
nested_data_type,
nested_data_value
),
};
nested_data_list.push(NestedDataInfo { nested_data, ptr_loc: data.len() });
// Allocate space for the pointer, we'll write to it later.
pad_to_alignment(data, 8);
data.extend([0; 8]);
}
MojomWireType::Union { variants, is_nullable } => {
let union_value = take_field_at_ordinal(&mut field_values, ordinal)?;
let union_value =
write_or_extract_nullable!(data, union_value, *is_nullable, 16);
let (tag, contained_value) = match union_value {
MojomValue::Union(tag, value) => (tag, value),
_ => {
wrong_type!(&packed_field, union_value);
}
};
pad_to_alignment(data, 8);
deparse_union(
data,
Some(nested_data_list),
tag,
*contained_value,
variants,
)?;
}
}
}
}
}
if !is_array {
// Non-array bodies must be a multiple of 8 bytes before we record
// their size in their header.
pad_to_alignment(data, 8);
}
let bytes_written = data.len() - initial_bytes;
// Write the length of the struct to the first 4 bytes of the header
// The usize->u32 cast should always work, because hopefully our message
// is less than 2^32 bytes long!
write_to_slice(data, initial_bytes, 4, &u32::to_le_bytes(bytes_written.try_into().unwrap()));
// This is a no_op unless is_array is true.
pad_to_alignment(data, 8);
for nested_data_info in local_nested_data_list {
// Write to this nested data's pointer.
let bytes_from_ptr = data.len() - nested_data_info.ptr_loc;
write_to_slice(
data,
nested_data_info.ptr_loc,
8,
&u64::to_le_bytes(bytes_from_ptr.try_into().unwrap()),
);
match nested_data_info.nested_data {
NestedData::Struct { field_values, packed_fields } => {
deparse_struct(data, field_values, packed_fields)?
}
NestedData::Array { contents, element_type, array_type } => {
deparse_array(data, contents, element_type, array_type)?
}
NestedData::Union { tag, value, variants } => {
deparse_union(data, None, tag, value, variants)?
}
NestedData::Map { values_map, key_type, value_type } => {
deparse_map(data, values_map, key_type, value_type)?
}
}
}
Ok(())
}
/// Serialize a single mojom value of the given type, outside the context of a
/// struct. This function is only useful for unit testing, since all mojom
/// values in practice are members of a struct. The function only works for
/// some mojom types, since e.g. booleans can't be parsed individually.
pub fn deparse_single_value_for_testing(
value: MojomValue,
wire_type: &MojomWireType,
) -> Result<DeparsedData> {
let mut data = DeparsedData::new();
match (wire_type, value) {
(MojomWireType::Leaf { leaf_type, .. }, value) => {
deparse_leaf_value(&mut data, value, leaf_type)?
}
(
MojomWireType::Pointer {
nested_data_type: PackedStructuredType::Struct { packed_field_types, .. },
..
},
MojomValue::Struct(_field_names, fields),
) => deparse_struct(&mut data, fields, packed_field_types)?,
(
MojomWireType::Pointer {
nested_data_type: PackedStructuredType::Array { element_type, array_type },
..
},
value @ (MojomValue::Array(_) | MojomValue::String(_)),
) => deparse_array(&mut data, value, element_type, array_type)?,
(
MojomWireType::Union { variants, .. }
| MojomWireType::Pointer {
nested_data_type: PackedStructuredType::Union { variants },
..
},
MojomValue::Union(tag, value),
) => deparse_union(&mut data, None, tag, *value, variants)?,
(
MojomWireType::Pointer {
nested_data_type: PackedStructuredType::Map { key_type, value_type },
..
},
MojomValue::Map(values_map),
) => deparse_map(&mut data, values_map, key_type, value_type)?,
_ if wire_type.is_nullable() => {
// Nullables only make sense in the context of an enclosing body
panic!("Cannot deparse single nullable values for testing")
}
(_, value) => {
wrong_type!(wire_type, value);
}
};
Ok(data)
}