diff --git a/Cargo.lock b/Cargo.lock index 6551b5a..b73c523 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -361,6 +361,15 @@ dependencies = [ "zip", ] +[[package]] +name = "flutterdec-serwalker" +version = "0.1.0-alpha.2" +dependencies = [ + "anyhow", + "flutterdec-adapter", + "goblin", +] + [[package]] name = "foldhash" version = "0.1.5" diff --git a/Cargo.toml b/Cargo.toml index ce04ace..a529bc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "crates/flutterdec-disasm-arm64", "crates/flutterdec-ir", "crates/flutterdec-decompiler", + "crates/flutterdec-serwalker", ] resolver = "2" diff --git a/crates/flutterdec-adapter/src/lib.rs b/crates/flutterdec-adapter/src/lib.rs index 76f1e63..31630f3 100644 --- a/crates/flutterdec-adapter/src/lib.rs +++ b/crates/flutterdec-adapter/src/lib.rs @@ -2,6 +2,7 @@ use anyhow::{anyhow, bail, Context, Result}; use serde::{Deserialize, Serialize}; use std::fs; use std::io::Write; +#[cfg(unix)] use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; use std::process::Command; @@ -171,9 +172,12 @@ pub fn install_adapter(repo_root: &Path, dart_hash: &str) -> Result { ); fs::write(&out, script).with_context(|| format!("write adapter script: {}", out.display()))?; - let mut perms = fs::metadata(&out)?.permissions(); - perms.set_mode(0o755); - fs::set_permissions(&out, perms)?; + #[cfg(unix)] + { + let mut perms = fs::metadata(&out)?.permissions(); + perms.set_mode(0o755); + fs::set_permissions(&out, perms)?; + } Ok(out) } diff --git a/crates/flutterdec-serwalker/Cargo.toml b/crates/flutterdec-serwalker/Cargo.toml new file mode 100644 index 0000000..442749f --- /dev/null +++ b/crates/flutterdec-serwalker/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "flutterdec-serwalker" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow.workspace = true +goblin.workspace = true +flutterdec-adapter = { path = "../flutterdec-adapter" } \ No newline at end of file diff --git a/crates/flutterdec-serwalker/src/cluster/mod.rs b/crates/flutterdec-serwalker/src/cluster/mod.rs new file mode 100644 index 0000000..c0cc69e --- /dev/null +++ b/crates/flutterdec-serwalker/src/cluster/mod.rs @@ -0,0 +1,1041 @@ +use crate::constants::{ClassId, ClassId::*}; +use crate::raw_object::*; +use crate::stream::Stream; +use crate::DECLARE_FIXED_LENGTH_CLUSTER; +use crate::DECLARE_VARIABLE_LENGTH_CLUSTER; +use crate::FFI_TYPES_LIST; + +pub trait Cluster { + fn set_metadata(&mut self, tags: u32, cid: ClassId, is_immutable: bool, is_canonical: bool); + fn is_fixed_len(&self) -> bool; + fn read_alloc(&mut self, last_ref_id: &mut u64, stream: &mut Stream) -> anyhow::Result; + fn read_fill(&mut self, stream: &mut Stream) -> anyhow::Result; +} + +pub fn read_smi(stream: &mut Stream) -> anyhow::Result { + let raw_smi = stream.read()?; // smis are always written as signed numbers + + Ok(raw_smi as Smi) +} + +macro_rules! FFI_CASE_PATTERN { + ( $( $ffi_type:ident ),* ) => { + $( $ffi_type )|* + }; +} + +pub fn decide_cluster(class_id: ClassId) -> Result, &'static str> { + match class_id { + // we assume compressed pointers, it supports only Android for now... + IllegalCid => Err("Not a supported class (illegal class)..."), + FFI_TYPES_LIST!(FFI_CASE_PATTERN) => Err("To do..."), + _ => Err("Not a supported class..."), + } +} + +// These are the objects that call ReadAllocFixedSize during deserialization, +// whose fill cluster size is uniquely determined by sizeof(Object) * num_of_objects +// and alloc cluster size is tags (MULEB128) + num_of_objects (MULEB128) + +DECLARE_FIXED_LENGTH_CLUSTER!(TypeParameters, TypeParametersCluster, |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.names = stream.read_ref_id()?; + obj.flags = stream.read_ref_id()?; + obj.bounds = stream.read_ref_id()?; + obj.defaults = stream.read_ref_id()?; + } +}); +DECLARE_FIXED_LENGTH_CLUSTER!(PatchClass, PatchClassCluster, |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.wrapped_class = stream.read_ref_id()?; + obj.script = stream.read_ref_id()?; + obj.kernel_program_info = stream.read_ref_id()?; + // obj.kernel_library_index = stream.read_unsigned()? as u32; [[NOT PRESENT IN FullAOT]] + } +}); +DECLARE_FIXED_LENGTH_CLUSTER!(Function, FunctionCluster, |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.name = stream.read_ref_id()?; + obj.owner = stream.read_ref_id()?; + obj.signature = stream.read_ref_id()?; + obj.data = stream.read_ref_id()?; + // obj.ic_data_array_or_bytecode = stream.read_ref_id()?; [[NOT PRESENT IN FullAOT]] + // obj.code = stream.read_ref_id()?; [[SKIPPED BY WriteFromTo]] + // obj.positional_parameter_names = stream.read_ref_id()?; [[NOT PRESENT IN FullAOT]] + // obj.unoptimized_code = stream.read_ref_id()?; [[NOT PRESENT IN FullAOT]] + // obj.bitmap = stream.read_unsigned()? as u64; [[NOT PRESENT IN FullAOT]] + obj.code_index = stream.read_unsigned()? as u32; + // obj.token_pos = stream.read()? as i32; [[NOT PRESENT IN release builds (called prouct in Flutter)]] + // obj.kernel_offset = stream.read_unsigned()? as u32; [[NOT PRESENT IN FullAOT]] + obj.kind_tag = stream.read()? as u32; + } +}); +DECLARE_FIXED_LENGTH_CLUSTER!(ClosureData, ClosureDataCluster, |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.context_scope = stream.read_ref_id()?; + obj.parent_function = stream.read_ref_id()?; + obj.closure = stream.read_ref_id()?; + obj.packed_fields = stream.read_unsigned()? as u32; + } +}); +DECLARE_FIXED_LENGTH_CLUSTER!( + FfiTrampolineData, + FfiTrampolineDataCluster, + |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.signature_type = stream.read_ref_id()?; + obj.c_signature = stream.read_ref_id()?; + obj.callback_target = stream.read_ref_id()?; + obj.callback_exceptional_return = stream.read_ref_id()?; + obj.ffi_function_kind = stream.read_byte()? as u8; + obj.callback_id = stream.read()? as i32; + } + } +); +DECLARE_FIXED_LENGTH_CLUSTER!(Field, FieldCluster, |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.name = stream.read_ref_id()?; + obj.owner = stream.read_ref_id()?; + obj.type_field = stream.read_ref_id()?; + obj.initializer_function = stream.read_ref_id()?; + obj.host_offset_or_field_id = stream.read_ref_id()?; + // obj.guarded_list_length = stream.read_ref_id()?; [[NOT PRESENT IN FullAOT]] + // obj.exact_type = stream.read_ref_id()?; [[NOT PRESENT IN FullAOT]] + // obj.dependent_code = stream.read_ref_id()?; [[NOT PRESENT IN FullAOT]] + obj.token_pos = stream.read()? as i32; + obj.end_token_pos = stream.read()? as i32; + obj.guarded_cid = stream.read_unsigned()? as u32; + obj.is_nullable = stream.read_unsigned()? as u32; + // obj.kernel_offset = stream.read_unsigned()? as u32; [[NOT PRESENT IN FullAOT]] + // obj.guarded_list_length_in_object_offset = stream.read()? as i8; [[NOT PRESENT IN FullAOT]] + // obj.static_type_exactness_state = stream.read()? as i8; [[NOT PRESENT IN FullAOT]] + // obj.target_offset = stream.read()? as i32; [[NOT PRESENT IN FullAOT]] + obj.kind_bits = stream.read_unsigned()? as u32; + } +}); +DECLARE_FIXED_LENGTH_CLUSTER!(Script, ScriptCluster, |_self, stream| { + for _ in 0.._self.obj_count as usize {} +}); +DECLARE_FIXED_LENGTH_CLUSTER!(Library, LibraryCluster, |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.name = stream.read_ref_id()?; + obj.url = stream.read_ref_id()?; + obj.private_key = stream.read_ref_id()?; + obj.dictionary = stream.read_ref_id()?; + obj.metadata = stream.read_ref_id()?; + obj.toplevel_class = stream.read_ref_id()?; + obj.used_scripts = stream.read_ref_id()?; + obj.loading_unit = stream.read_ref_id()?; + obj.imports = stream.read_ref_id()?; + obj.exports = stream.read_ref_id()?; + // obj.dependencies = stream.read_ref_id()?; [[NOT PRESENT IN FullAOT]] + // obj.kernel_program_info = stream.read_ref_id()?; [[NOT PRESENT IN FullAOT]] + // obj.loaded_scripts = stream.read_ref_id()?; [[NOT PRESENT IN FullAOT]] + obj.index = stream.read()? as i32; + obj.num_imports = stream.read()? as u16; + obj.load_state = stream.read_byte()? as i8; + obj.flags = stream.read_byte()? as u8; + // obj.kernel_library_index = stream.read_unsigned()? as u32; [[NOT PRESENT IN FullAOT]] + } +}); +DECLARE_FIXED_LENGTH_CLUSTER!(Namespace, NamespaceCluster, |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.target = stream.read_ref_id()?; + obj.show_names = stream.read_ref_id()?; + obj.hide_names = stream.read_ref_id()?; + obj.owner = stream.read_ref_id()?; + } +}); +DECLARE_FIXED_LENGTH_CLUSTER!( + KernelProgramInfo, + KernelProgramInfoCluster, + |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.kernel_component = stream.read_ref_id()?; + obj.string_offsets = stream.read_ref_id()?; + obj.string_data = stream.read_ref_id()?; + obj.canonical_names = stream.read_ref_id()?; + obj.metadata_payloads = stream.read_ref_id()?; + obj.metadata_mappings = stream.read_ref_id()?; + obj.scripts = stream.read_ref_id()?; + obj.constants = stream.read_ref_id()?; + obj.constants_table = stream.read_ref_id()?; + obj.libraries_cache = stream.read_ref_id()?; + obj.classes_cache = stream.read_ref_id()?; + } + } +); +DECLARE_FIXED_LENGTH_CLUSTER!(UnlinkedCall, UnlinkedCallCluster, |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.can_patch_to_monomorphic = stream.read_byte()? != 0; + } +}); +DECLARE_FIXED_LENGTH_CLUSTER!(ICData, ICDataCluster, |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.target_name = stream.read_ref_id()?; + obj.args_descriptor = stream.read_ref_id()?; + obj.entries = stream.read_ref_id()?; + obj.state_bits = stream.read_unsigned()? as u32; + } +}); +DECLARE_FIXED_LENGTH_CLUSTER!( + MegamorphicCache, + MegamorphicCacheCluster, + |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.target_name = stream.read_ref_id()?; + obj.args_descriptor = stream.read_ref_id()?; + obj.buckets = stream.read_ref_id()?; + obj.mask = stream.read_ref_id()? as i32; + obj.filled_entry_count = stream.read()? as i32; + } + } +); +DECLARE_FIXED_LENGTH_CLUSTER!( + SubtypeTestCache, + SubtypeTestCacheCluster, + |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.cache = stream.read_ref_id()?; + obj.num_inputs = stream.read_unsigned()? as u32; + obj.num_occupied = stream.read_unsigned()? as u32; + } + } +); +DECLARE_FIXED_LENGTH_CLUSTER!(LoadingUnit, LoadingUnitCluster, |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.parent = stream.read_ref_id()?; + obj.base_objects = stream.read_ref_id()?; + obj.packed_fields = stream.read()? as i64; + } +}); +DECLARE_FIXED_LENGTH_CLUSTER!(LanguageError, LanguageErrorCluster, |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.previous_error = stream.read_ref_id()?; + obj.script = stream.read_ref_id()?; + obj.message = stream.read_ref_id()?; + obj.formatted_message = stream.read_ref_id()?; + obj.token_pos = stream.read()? as i32; + obj.report_after_token = stream.read_byte()? != 0; + obj.kind = stream.read_byte()? as i8; + } +}); +DECLARE_FIXED_LENGTH_CLUSTER!( + UnhandledException, + UnhandledExceptionCluster, + |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.exception = stream.read_ref_id()?; + obj.stacktrace = stream.read_ref_id()?; + } + } +); +DECLARE_FIXED_LENGTH_CLUSTER!(LibraryPrefix, LibraryPrefixCluster, |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.name = stream.read_ref_id()?; + obj.imports = stream.read_ref_id()?; + obj.importer = stream.read_ref_id()?; + obj.num_imports = stream.read_unsigned()? as u16; + obj.is_deferred_load = stream.read_byte()? != 0; + } +}); +DECLARE_FIXED_LENGTH_CLUSTER!(Type, TypeCluster, |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.type_test_stub = stream.read_ref_id()?; + obj.hash = stream.read_ref_id()?; + obj.arguments = stream.read_ref_id()?; + obj.flags = stream.read_unsigned()? as u8; + } +}); +DECLARE_FIXED_LENGTH_CLUSTER!(FunctionType, FunctionTypeCluster, |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.type_test_stub = stream.read_ref_id()?; + obj.hash = stream.read_ref_id()?; + obj.type_parameters = stream.read_ref_id()?; + obj.result_type = stream.read_ref_id()?; + obj.parameter_types = stream.read_ref_id()?; + obj.named_parameter_names = stream.read_ref_id()?; + obj.flags = stream.read_byte()? as u8; + obj.packed_parameter_counts = stream.read_unsigned()? as u32; + obj.packed_type_parameter_counts = stream.read_unsigned()? as u16; + } +}); +DECLARE_FIXED_LENGTH_CLUSTER!(RecordType, RecordTypeCluster, |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.type_test_stub = stream.read_ref_id()?; + obj.hash = stream.read_ref_id()?; + obj.shape = stream.read_ref_id()? as i32; + obj.field_types = stream.read_ref_id()?; + obj.flags = stream.read_byte()? as u8; + // obj.shape = stream.read_ref_id()?; as i32; + } +}); +DECLARE_FIXED_LENGTH_CLUSTER!(TypeParameter, TypeParameterCluster, |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.type_test_stub = stream.read_ref_id()?; + obj.hash = stream.read_ref_id()?; + obj.owner = stream.read_ref_id()?; + obj.base = stream.read()? as u16; + obj.index = stream.read()? as u16; + obj.flags = stream.read_byte()? as u8; + } +}); +DECLARE_FIXED_LENGTH_CLUSTER!(Closure, ClosureCluster, |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.instantiator_type_arguments = stream.read_ref_id()?; + obj.function_type_arguments = stream.read_ref_id()?; + obj.delayed_type_arguments = stream.read_ref_id()?; + obj.function = stream.read_ref_id()?; + obj.context = stream.read_ref_id()?; + obj.hash = stream.read_ref_id()?; + } +}); +DECLARE_FIXED_LENGTH_CLUSTER!(Double, DoubleCluster, |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.value = f64::from_bits(stream.read_raw_u64()?); + } +}); +DECLARE_FIXED_LENGTH_CLUSTER!(Int32x4, Int32x4Cluster, |_self, stream| { + for _ in 0.._self.obj_count as usize {} +}); +DECLARE_FIXED_LENGTH_CLUSTER!( + GrowableObjectArray, + GrowableObjectArrayCluster, + |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.type_arguments = stream.read_ref_id()?; + obj.data = stream.read_ref_id()?; + obj.length = stream.read_ref_id()? as i32; + } + } +); +DECLARE_FIXED_LENGTH_CLUSTER!(TypedDataView, TypedDataViewCluster, |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.typed_data = stream.read_ref_id()?; + obj.offset_in_bytes = stream.read_ref_id()? as i32; + } +}); +DECLARE_FIXED_LENGTH_CLUSTER!( + ExternalTypedData, + ExternalTypedDataCluster, + |_self, stream| { for _ in 0.._self.obj_count as usize {} } +); +DECLARE_FIXED_LENGTH_CLUSTER!(StackTrace, StackTraceCluster, |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.async_link = stream.read_ref_id()?; + obj.code_array = stream.read_ref_id()?; + obj.pc_offset_array = stream.read_ref_id()?; + // obj.expand_inlined = stream.read_unsigned()? != 0; [[NOT PRESENT IN FullAOT]] + } +}); +DECLARE_FIXED_LENGTH_CLUSTER!(RegExp, RegExpCluster, |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.capture_name_map = stream.read_ref_id()?; + obj.pattern = stream.read_ref_id()?; + obj.one_byte = stream.read_ref_id()?; + obj.two_byte = stream.read_ref_id()?; + obj.one_byte_sticky = stream.read_ref_id()?; + obj.two_byte_sticky = stream.read_ref_id()?; + obj.num_one_byte_registers = stream.read()? as i32; + obj.num_two_byte_registers = stream.read()? as i32; + // Deserializer::Read() dispatches to ReadStream::Raw<1, T>. + obj.type_flags = stream.read_byte()? as i8; + } +}); +DECLARE_FIXED_LENGTH_CLUSTER!(WeakProperty, WeakPropertyCluster, |_self, stream| { + for obj_idx in 0.._self.obj_count as usize { + let obj = &mut *_self.objs[obj_idx]; + obj.key = stream.read_ref_id()?; + obj.value = stream.read_ref_id()?; + // obj.next_seen_by_gc = stream.read_ref_id()?; [[NOT PRESENT IN FullAOT]] + } +}); + +DECLARE_VARIABLE_LENGTH_CLUSTER!(Code, CodeCluster); +DECLARE_VARIABLE_LENGTH_CLUSTER!(ObjectPool, ObjectPoolCluster); + +macro_rules! IMPLEMENT_VARIABLE_LENGTH_CLUSTER { + ( + $cluster_name:ident, + |$alloc_self:ident, $alloc_stream:ident| $alloc_impl:block, + |$fill_self:ident, $fill_stream:ident| $fill_impl:block + ) => { + impl Cluster for $cluster_name { + fn set_metadata( + &mut self, + tags: u32, + cid: ClassId, + is_immutable: bool, + is_canonical: bool, + ) { + self.tags = tags; + self.cid = cid; + self.is_immutable = is_immutable; + self.is_canonical = is_canonical; + } + + fn is_fixed_len(&self) -> bool { + false + } + + fn read_alloc( + &mut self, + last_ref_id: &mut u64, + stream: &mut Stream, + ) -> anyhow::Result { + self.start_of_alloc = stream.get_current_pos(); + self.first_ref_id = *last_ref_id as u32; + + let $alloc_self = &mut *self; + let $alloc_stream = &mut *stream; + $alloc_impl + + *last_ref_id += $alloc_self.obj_count; + $alloc_self.end_of_alloc = $alloc_stream.get_current_pos(); + Ok($alloc_self.end_of_alloc - $alloc_self.start_of_alloc) + } + + fn read_fill(&mut self, stream: &mut Stream) -> anyhow::Result { + self.start_of_fill = stream.get_current_pos(); + + let $fill_self = &mut *self; + let $fill_stream = &mut *stream; + $fill_impl + + $fill_self.end_of_fill = $fill_stream.get_current_pos(); + Ok($fill_self.end_of_fill - $fill_self.start_of_fill) + } + } + }; +} + +fn typed_data_element_size(cid: ClassId) -> anyhow::Result { + let size = match cid { + TypedDataInt8ArrayCid | TypedDataUint8ArrayCid | TypedDataUint8ClampedArrayCid => 1, + TypedDataInt16ArrayCid | TypedDataUint16ArrayCid => 2, + TypedDataInt32ArrayCid | TypedDataUint32ArrayCid | TypedDataFloat32ArrayCid => 4, + TypedDataInt64ArrayCid | TypedDataUint64ArrayCid | TypedDataFloat64ArrayCid => 8, + TypedDataFloat32x4ArrayCid | TypedDataInt32x4ArrayCid | TypedDataFloat64x2ArrayCid => 16, + _ => anyhow::bail!("class {:?} is not an internal TypedData class", cid), + }; + Ok(size) +} + +DECLARE_VARIABLE_LENGTH_CLUSTER!(Map, MapCluster); +IMPLEMENT_VARIABLE_LENGTH_CLUSTER!( + MapCluster, + |cluster, stream| { + cluster.obj_count = stream.read_unsigned()?; + for _ in 0..cluster.obj_count { + cluster.objs.push(Box::::default()); + } + }, + |cluster, stream| { + for obj in &mut cluster.objs { + obj.type_arguments = stream.read_ref_id()?; + obj.hash_mask = stream.read_ref_id()?; + obj.data = stream.read_ref_id()?; + obj.used_data = stream.read_ref_id()?; + obj.deleted_keys = stream.read_ref_id()?; + // UntaggedLinkedHashBase::to_snapshot excludes the rebuilt index_. + } + } +); + +DECLARE_VARIABLE_LENGTH_CLUSTER!(Set, SetCluster); +IMPLEMENT_VARIABLE_LENGTH_CLUSTER!( + SetCluster, + |cluster, stream| { + cluster.obj_count = stream.read_unsigned()?; + for _ in 0..cluster.obj_count { + cluster.objs.push(Box::::default()); + } + }, + |cluster, stream| { + for obj in &mut cluster.objs { + obj.type_arguments = stream.read_ref_id()?; + obj.hash_mask = stream.read_ref_id()?; + obj.data = stream.read_ref_id()?; + obj.used_data = stream.read_ref_id()?; + obj.deleted_keys = stream.read_ref_id()?; + } + } +); + +DECLARE_VARIABLE_LENGTH_CLUSTER!(Instance, InstanceCluster); +IMPLEMENT_VARIABLE_LENGTH_CLUSTER!( + InstanceCluster, + |cluster, stream| { + cluster.obj_count = stream.read_unsigned()?; + let next_field_offset_in_words = stream.read()? as i32; + let instance_size_in_words = stream.read()? as i32; + for _ in 0..cluster.obj_count { + cluster.objs.push(Box::new(Instance { + next_field_offset_in_words, + instance_size_in_words, + ..Instance::default() + })); + } + }, + |cluster, stream| { + const FIRST_INSTANCE_FIELD_WORD: i32 = 2; + + let bitmap = stream.read_unsigned()?; + for obj in &mut cluster.objs { + obj.unboxed_fields_bitmap = bitmap; + if obj.next_field_offset_in_words < FIRST_INSTANCE_FIELD_WORD { + anyhow::bail!( + "invalid instance next-field offset {}", + obj.next_field_offset_in_words + ); + } + + for word_index in FIRST_INSTANCE_FIELD_WORD..obj.next_field_offset_in_words { + let mask = 1_u64.checked_shl(word_index as u32).unwrap_or(0); + if bitmap & mask != 0 { + // ReadWordWith32BitReads reads two encoded 32-bit chunks + // for a 64-bit target word. + let low = stream.read()? as u32 as u64; + let high = stream.read()? as u32 as u64; + obj.fields.push(InstanceField::Unboxed(low | (high << 32))); + } else { + obj.fields + .push(InstanceField::Reference(stream.read_ref_id()?)); + } + } + } + } +); + +DECLARE_VARIABLE_LENGTH_CLUSTER!(TypedData, TypedDataCluster); +IMPLEMENT_VARIABLE_LENGTH_CLUSTER!( + TypedDataCluster, + |cluster, stream| { + cluster.obj_count = stream.read_unsigned()?; + for _ in 0..cluster.obj_count { + let length = stream.read_unsigned()? as usize; + cluster.objs.push(Box::new(TypedData { + length, + ..TypedData::default() + })); + } + }, + |cluster, stream| { + let element_size = typed_data_element_size(cluster.cid)?; + for obj in &mut cluster.objs { + let length = stream.read_unsigned()? as usize; + let byte_length = length + .checked_mul(element_size) + .ok_or_else(|| anyhow::anyhow!("TypedData byte length overflow"))?; + obj.length = length; + obj.data = stream.read_bytes(byte_length)?; + } + } +); + +DECLARE_VARIABLE_LENGTH_CLUSTER!(Class, ClassCluster); +IMPLEMENT_VARIABLE_LENGTH_CLUSTER!( + ClassCluster, + |cluster, stream| { + let predefined_count = stream.read_unsigned()?; + for _ in 0..predefined_count { + cluster.objs.push(Box::new(Class { + id: stream.read()? as i32, + is_predefined: true, + ..Class::default() + })); + } + + let regular_count = stream.read_unsigned()?; + for _ in 0..regular_count { + cluster.objs.push(Box::::default()); + } + cluster.obj_count = predefined_count + regular_count; + }, + |cluster, stream| { + const TOP_LEVEL_CID_OFFSET: i32 = 1 << 20; + + for obj in &mut cluster.objs { + obj.name = stream.read_ref_id()?; + obj.functions = stream.read_ref_id()?; + obj.functions_hash_table = stream.read_ref_id()?; + obj.fields = stream.read_ref_id()?; + obj.offset_in_words_to_field = stream.read_ref_id()?; + obj.interfaces = stream.read_ref_id()?; + obj.script = stream.read_ref_id()?; + obj.library = stream.read_ref_id()?; + obj.type_parameters = stream.read_ref_id()?; + obj.super_type = stream.read_ref_id()?; + obj.constants = stream.read_ref_id()?; + obj.declaration_type = stream.read_ref_id()?; + obj.invocation_dispatcher_cache = stream.read_ref_id()?; + + obj.id = stream.read()? as i32; + obj.target_instance_size_in_words = stream.read()? as i32; + obj.target_next_field_offset_in_words = stream.read()? as i32; + obj.target_type_arguments_field_offset_in_words = stream.read()? as i32; + obj.num_type_arguments = stream.read()? as i16; + obj.num_native_fields = stream.read()? as u16; + obj.state_bits = stream.read()? as u32; + if obj.id < TOP_LEVEL_CID_OFFSET { + obj.unboxed_fields_bitmap = Some(stream.read_unsigned()?); + } + } + } +); + +DECLARE_VARIABLE_LENGTH_CLUSTER!(TypeArguments, TypeArgumentsCluster); +IMPLEMENT_VARIABLE_LENGTH_CLUSTER!( + TypeArgumentsCluster, + |cluster, stream| { + cluster.obj_count = stream.read_unsigned()?; + for _ in 0..cluster.obj_count { + let length = stream.read_unsigned()? as i32; + cluster.objs.push(Box::new(TypeArguments { + length, + ..TypeArguments::default() + })); + } + + // a canonical cluster in the root loading unit carries the canonical + // hash-set layout after its ordinary allocation records + + // so this is just for the canonical TypeArguments cluster (if any) + if cluster.is_canonical { + let _table_length = stream.read_unsigned()?; + let first_element = stream.read_unsigned()?; + if first_element > cluster.obj_count { + anyhow::bail!( + "canonical TypeArguments first element {first_element} exceeds count {}", + cluster.obj_count + ); + } + for _ in first_element..cluster.obj_count { + let _gap = stream.read_unsigned()?; + } + } + }, + |cluster, stream| { + for obj in &mut cluster.objs { + let length = stream.read_unsigned()? as i32; + obj.length = length; + obj.hash = stream.read()? as i32; + obj.nullability = stream.read_unsigned()? as i32; + obj.instantiations = stream.read_ref_id()?; + for _ in 0..length { + obj.types.push(stream.read_ref_id()?); + } + } + } +); + +DECLARE_VARIABLE_LENGTH_CLUSTER!(ExceptionHandlers, ExceptionHandlersCluster); +IMPLEMENT_VARIABLE_LENGTH_CLUSTER!( + ExceptionHandlersCluster, + |cluster, stream| { + cluster.obj_count = stream.read_unsigned()?; + for _ in 0..cluster.obj_count { + let num_entries = stream.read_unsigned()? as usize; + cluster.objs.push(Box::new(ExceptionHandlers { + num_entries, + ..ExceptionHandlers::default() + })); + } + }, + |cluster, stream| { + for obj in &mut cluster.objs { + obj.packed_fields = stream.read_unsigned()? as u32; + obj.handled_types_data = stream.read_ref_id()?; + for _ in 0..obj.num_entries { + obj.entries.push(ExceptionHandlerInfo { + handler_pc_offset: stream.read()? as u32, + outer_try_index: stream.read()? as i16, + needs_stacktrace: stream.read_byte()? as i8, + has_catch_all: stream.read_byte()? as i8, + is_generated: stream.read_byte()? as i8, + }); + } + } + } +); + +DECLARE_VARIABLE_LENGTH_CLUSTER!(Context, ContextCluster); +IMPLEMENT_VARIABLE_LENGTH_CLUSTER!( + ContextCluster, + |cluster, stream| { + cluster.obj_count = stream.read_unsigned()?; + for _ in 0..cluster.obj_count { + let num_variables = stream.read_unsigned()? as i32; + cluster.objs.push(Box::new(Context { + num_variables, + ..Context::default() + })); + } + }, + |cluster, stream| { + for obj in &mut cluster.objs { + let num_variables = stream.read_unsigned()? as i32; + obj.num_variables = num_variables; + obj.parent = stream.read_ref_id()?; + for _ in 0..num_variables { + obj.variables.push(stream.read_ref_id()?); + } + } + } +); + +DECLARE_VARIABLE_LENGTH_CLUSTER!(ContextScope, ContextScopeCluster); +IMPLEMENT_VARIABLE_LENGTH_CLUSTER!( + ContextScopeCluster, + |cluster, stream| { + cluster.obj_count = stream.read_unsigned()?; + for _ in 0..cluster.obj_count { + let num_variables = stream.read_unsigned()? as i32; + cluster.objs.push(Box::new(ContextScope { + num_variables, + ..ContextScope::default() + })); + } + }, + |cluster, stream| { + const VARIABLE_DESC_REF_COUNT: i32 = 10; + + for obj in &mut cluster.objs { + let num_variables = stream.read_unsigned()? as i32; + obj.num_variables = num_variables; + obj.is_implicit = stream.read_byte()? != 0; + for _ in 0..num_variables * VARIABLE_DESC_REF_COUNT { + obj.variables.push(stream.read_ref_id()?); + } + } + } +); + +DECLARE_VARIABLE_LENGTH_CLUSTER!(Mint, MintCluster); +IMPLEMENT_VARIABLE_LENGTH_CLUSTER!( + MintCluster, + |cluster, stream| { + cluster.obj_count = stream.read_unsigned()?; + for _ in 0..cluster.obj_count { + cluster.objs.push(Box::new(Mint { + value: stream.read()? as i64, + })); + } + }, + |_cluster, _stream| {} +); + +DECLARE_VARIABLE_LENGTH_CLUSTER!(Float32x4, Float32x4Cluster); +IMPLEMENT_VARIABLE_LENGTH_CLUSTER!( + Float32x4Cluster, + |cluster, stream| { + cluster.obj_count = stream.read_unsigned()?; + for _ in 0..cluster.obj_count { + cluster.objs.push(Box::::default()); + } + }, + |cluster, stream| { + for obj in &mut cluster.objs { + obj.value = stream.read_bytes(16)?; + } + } +); + +DECLARE_VARIABLE_LENGTH_CLUSTER!(Float64x2, Float64x2Cluster); +IMPLEMENT_VARIABLE_LENGTH_CLUSTER!( + Float64x2Cluster, + |cluster, stream| { + cluster.obj_count = stream.read_unsigned()?; + for _ in 0..cluster.obj_count { + cluster.objs.push(Box::::default()); + } + }, + |cluster, stream| { + for obj in &mut cluster.objs { + obj.value = stream.read_bytes(16)?; + } + } +); + +DECLARE_VARIABLE_LENGTH_CLUSTER!(Record, RecordCluster); +IMPLEMENT_VARIABLE_LENGTH_CLUSTER!( + RecordCluster, + |cluster, stream| { + cluster.obj_count = stream.read_unsigned()?; + for _ in 0..cluster.obj_count { + let num_fields = stream.read_unsigned()? as usize; + cluster.objs.push(Box::new(Record { + num_fields, + ..Record::default() + })); + } + }, + |cluster, stream| { + for obj in &mut cluster.objs { + obj.shape = stream.read_unsigned()? as i32; + for _ in 0..obj.num_fields { + obj.fields.push(stream.read_ref_id()?); + } + } + } +); + +DECLARE_VARIABLE_LENGTH_CLUSTER!(Array, ArrayCluster); +IMPLEMENT_VARIABLE_LENGTH_CLUSTER!( + ArrayCluster, + |cluster, stream| { + cluster.obj_count = stream.read_unsigned()?; + for _ in 0..cluster.obj_count { + let length = stream.read_unsigned()? as i32; + cluster.objs.push(Box::new(Array { + length, + ..Array::default() + })); + } + }, + |cluster, stream| { + for obj in &mut cluster.objs { + let length = stream.read_unsigned()? as i32; + obj.length = length; + obj.type_arguments = stream.read_ref_id()?; + for _ in 0..length { + obj.elements.push(stream.read_ref_id()?); + } + } + } +); + +DECLARE_VARIABLE_LENGTH_CLUSTER!(WeakArray, WeakArrayCluster); +IMPLEMENT_VARIABLE_LENGTH_CLUSTER!( + WeakArrayCluster, + |cluster, stream| { + cluster.obj_count = stream.read_unsigned()?; + for _ in 0..cluster.obj_count { + let length = stream.read_unsigned()? as i32; + cluster.objs.push(Box::new(WeakArray { + length, + ..WeakArray::default() + })); + } + }, + |cluster, stream| { + for obj in &mut cluster.objs { + let length = stream.read_unsigned()? as i32; + obj.length = length; + for _ in 0..length { + obj.elements.push(stream.read_ref_id()?); + } + } + } +); + +DECLARE_VARIABLE_LENGTH_CLUSTER!(ImmutableArray, ImmutableArrayCluster); +IMPLEMENT_VARIABLE_LENGTH_CLUSTER!( + ImmutableArrayCluster, + |cluster, stream| { + cluster.obj_count = stream.read_unsigned()?; + for _ in 0..cluster.obj_count { + let length = stream.read_unsigned()? as i32; + cluster.objs.push(Box::new(ImmutableArray { + length, + ..ImmutableArray::default() + })); + } + }, + |cluster, stream| { + for obj in &mut cluster.objs { + let length = stream.read_unsigned()? as i32; + obj.length = length; + obj.type_arguments = stream.read_ref_id()?; + for _ in 0..length { + obj.elements.push(stream.read_ref_id()?); + } + } + } +); + +DECLARE_VARIABLE_LENGTH_CLUSTER!(ConstMap, ConstMapCluster); +IMPLEMENT_VARIABLE_LENGTH_CLUSTER!( + ConstMapCluster, + |cluster, stream| { + cluster.obj_count = stream.read_unsigned()?; + for _ in 0..cluster.obj_count { + cluster.objs.push(Box::::default()); + } + }, + |cluster, stream| { + for obj in &mut cluster.objs { + obj.type_arguments = stream.read_ref_id()?; + obj.hash_mask = stream.read_ref_id()?; + obj.data = stream.read_ref_id()?; + obj.used_data = stream.read_ref_id()?; + obj.deleted_keys = stream.read_ref_id()?; + } + } +); + +DECLARE_VARIABLE_LENGTH_CLUSTER!(ConstSet, ConstSetCluster); +IMPLEMENT_VARIABLE_LENGTH_CLUSTER!( + ConstSetCluster, + |cluster, stream| { + cluster.obj_count = stream.read_unsigned()?; + for _ in 0..cluster.obj_count { + cluster.objs.push(Box::::default()); + } + }, + |cluster, stream| { + for obj in &mut cluster.objs { + obj.type_arguments = stream.read_ref_id()?; + obj.hash_mask = stream.read_ref_id()?; + obj.data = stream.read_ref_id()?; + obj.used_data = stream.read_ref_id()?; + obj.deleted_keys = stream.read_ref_id()?; + } + } +); + +DECLARE_VARIABLE_LENGTH_CLUSTER!(CodeSourceMap, CodeSourceMapCluster); +IMPLEMENT_VARIABLE_LENGTH_CLUSTER!( + CodeSourceMapCluster, + |cluster, stream| { + cluster.obj_count = stream.read_unsigned()?; + for _ in 0..cluster.obj_count { + let length = stream.read_unsigned()? as u32; + cluster.objs.push(Box::new(CodeSourceMap { + length, + ..CodeSourceMap::default() + })); + } + }, + |cluster, stream| { + for obj in &mut cluster.objs { + let length = stream.read_unsigned()? as u32; + obj.length = length; + obj.data = stream.read_bytes(length as usize)?; + } + } +); + +DECLARE_VARIABLE_LENGTH_CLUSTER!(CompressedStackMaps, CompressedStackMapsCluster); +IMPLEMENT_VARIABLE_LENGTH_CLUSTER!( + CompressedStackMapsCluster, + |cluster, stream| { + cluster.obj_count = stream.read_unsigned()?; + for _ in 0..cluster.obj_count { + let length = stream.read_unsigned()? as u32; + cluster.objs.push(Box::new(CompressedStackMaps { + length, + ..CompressedStackMaps::default() + })); + } + }, + |cluster, stream| { + for obj in &mut cluster.objs { + obj.flags_and_size = stream.read_unsigned()? as u32; + obj.data = stream.read_bytes(obj.length as usize)?; + } + } +); + +DECLARE_VARIABLE_LENGTH_CLUSTER!(PcDescriptors, PcDescriptorsCluster); +IMPLEMENT_VARIABLE_LENGTH_CLUSTER!( + PcDescriptorsCluster, + |cluster, stream| { + cluster.obj_count = stream.read_unsigned()?; + for _ in 0..cluster.obj_count { + let length = stream.read_unsigned()? as u32; + cluster.objs.push(Box::new(PcDescriptors { + length, + ..PcDescriptors::default() + })); + } + }, + |cluster, stream| { + for obj in &mut cluster.objs { + let length = stream.read_unsigned()?; // its saved twice + obj.data = stream.read_bytes(length as usize)?; + } + } +); + +//DECLARE_VARIABLE_LENGTH_CLUSTER!(OneByteString, OneByteStringCluster); These only exist when NO COMPRESSED_POINTERS +//DECLARE_VARIABLE_LENGTH_CLUSTER!(TwoByteString, TwoByteStringCluster); +DECLARE_VARIABLE_LENGTH_CLUSTER!(_String, _StringCluster); +IMPLEMENT_VARIABLE_LENGTH_CLUSTER!( + _StringCluster, + |cluster, stream| { + cluster.obj_count = stream.read_unsigned()?; + for _ in 0..cluster.obj_count { + let encoded = stream.read_unsigned()?; + let string_type = if encoded & 1 == 1 { + StrType::TwoByte + } else { + StrType::OneByte + }; + cluster.objs.push(Box::new(_String { + string_type, + length: (encoded >> 1) as i32, + .._String::default() + })); + } + }, + |cluster, stream| { + for obj in &mut cluster.objs { + let _encoded = stream.read_unsigned()?; // why is this here twice? Huh? + match obj.string_type { + StrType::OneByte => { + // OneByteString payload is Latin-1, not UTF-8. Bytes 0x80..=0xFF + // are valid and map to U+0080..U+00FF; from_utf8 rejects them. + let mut decoded = String::with_capacity(obj.length as usize); + for _ in 0..obj.length { + decoded.push(stream.read_byte()? as char); + } + obj.internal_str = decoded; + } + StrType::TwoByte => { + // a TwoByteString has exactly `length` 16-bit code units + let mut code_units = Vec::with_capacity(obj.length as usize); + + for _ in 0..obj.length { + // read 2 bytes (little-endian) + let b1 = stream.read_byte()? as u16; + let b2 = stream.read_byte()? as u16; + let code_unit = b1 | (b2 << 8); + + code_units.push(code_unit); + } + + // Dart strings may hold unpaired surrogates; never panic on them. + obj.internal_str = String::from_utf16_lossy(&code_units); + } + } + } + } +); diff --git a/crates/flutterdec-serwalker/src/constants.rs b/crates/flutterdec-serwalker/src/constants.rs new file mode 100644 index 0000000..adb4137 --- /dev/null +++ b/crates/flutterdec-serwalker/src/constants.rs @@ -0,0 +1,275 @@ +use std::mem::size_of; + +pub const MAGIC_BYTES: u32 = 0xdcdcf5f5; + +pub const SNAPSHOT_MAGIC_NUMBER_SZ: usize = size_of::(); +pub const SNAPSHOT_LEN_SZ: usize = size_of::(); +pub const SNAPSHOT_KIND_SZ: usize = size_of::(); + +pub const SNAPSHOT_HEADER_SZ: usize = SNAPSHOT_MAGIC_NUMBER_SZ // 20 bytes of header + + SNAPSHOT_LEN_SZ + + SNAPSHOT_KIND_SZ; + +pub const MAX_CLUSTER_NUM: usize = 67usize; + +pub const UNSIGNED_END_OF_DATA_BYTE: u8 = 0x80u8; // last byte +pub const UNSIGNED_MAX_DATA_PER_BYTE: u8 = 0x7fu8; // more bytes to follow (for both) + +pub const SIGNED_END_OF_DATA_BYTE: u8 = 0xc0u8; // last byte + +pub const SIGNED_M: u8 = SIGNED_END_OF_DATA_BYTE; +pub const UNSIGNED_M: u8 = UNSIGNED_END_OF_DATA_BYTE; + +pub const DATA_BITS_PER_BYTE: usize = 7usize; + +pub const SMI_SHIFT: usize = 1usize; + +pub const VERSION_HASH_LENGTH: usize = 32usize; + +// Version::SnapshotString() for the Dart SDK 3.11.1 tag (e927f58e327a). +// The current parser has a hardcoded VM schema, so reject other layouts until +// schema information can be supplied dynamically. +pub const DART_3_11_1_SNAPSHOT_HASH: &str = "78da37fed6bf1489361a312568249f3f"; + +pub const HEADER_SIZE: usize = 64; + +macro_rules! DEFINE_CLASS_ID { + ( $( $name:ident = $val:expr ),* ) => { + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] + #[repr(u32)] + pub enum ClassId { + #[default] + IllegalCid = 0, + $( $name = $val, )* + } + + impl TryFrom for ClassId { + type Error = &'static str; + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(ClassId::IllegalCid), + $( $val => Ok(ClassId::$name), )* + _ => Err("Invalid ClassId"), + } + } + } + }; +} + +DEFINE_CLASS_ID! { + NativePointer = 1, + FreeListElement = 2, + ForwardingCorpse = 3, + ObjectCid = 4, + ClassCid = 5, + PatchClassCid = 6, + FunctionCid = 7, + TypeParametersCid = 8, + ClosureDataCid = 9, + FfiTrampolineDataCid = 10, + FieldCid = 11, + ScriptCid = 12, + LibraryCid = 13, + NamespaceCid = 14, + KernelProgramInfoCid = 15, + WeakSerializationReferenceCid = 16, + WeakArrayCid = 17, + CodeCid = 18, + BytecodeCid = 19, + InstructionsCid = 20, + InstructionsSectionCid = 21, + InstructionsTableCid = 22, + ObjectPoolCid = 23, + PcDescriptorsCid = 24, + CodeSourceMapCid = 25, + CompressedStackMapsCid = 26, + LocalVarDescriptorsCid = 27, + ExceptionHandlersCid = 28, + ContextCid = 29, + ContextScopeCid = 30, + SentinelCid = 31, + SingleTargetCacheCid = 32, + MonomorphicSmiableCallCid = 33, + CallSiteDataCid = 34, + UnlinkedCallCid = 35, + ICDataCid = 36, + MegamorphicCacheCid = 37, + SubtypeTestCacheCid = 38, + LoadingUnitCid = 39, + ErrorCid = 40, + ApiErrorCid = 41, + LanguageErrorCid = 42, + UnhandledExceptionCid = 43, + UnwindErrorCid = 44, + InstanceCid = 45, + LibraryPrefixCid = 46, + TypeArgumentsCid = 47, + AbstractTypeCid = 48, + TypeCid = 49, + FunctionTypeCid = 50, + RecordTypeCid = 51, + TypeParameterCid = 52, + FinalizerBaseCid = 53, + FinalizerCid = 54, + NativeFinalizerCid = 55, + FinalizerEntryCid = 56, + ClosureCid = 57, + NumberCid = 58, + IntegerCid = 59, + SmiCid = 60, + MintCid = 61, + DoubleCid = 62, + BoolCid = 63, + Float32x4Cid = 64, + Int32x4Cid = 65, + Float64x2Cid = 66, + RecordCid = 67, + TypedDataBaseCid = 68, + TypedDataCid = 69, + ExternalTypedDataCid = 70, + TypedDataViewCid = 71, + PointerCid = 72, + DynamicLibraryCid = 73, + CapabilityCid = 74, + ReceivePortCid = 75, + SendPortCid = 76, + StackTraceCid = 77, + SuspendStateCid = 78, + RegExpCid = 79, + WeakPropertyCid = 80, + WeakReferenceCid = 81, + MirrorReferenceCid = 82, + FutureOrCid = 83, + UserTagCid = 84, + TransferableTypedDataCid = 85, + MapCid = 86, + ConstMapCid = 87, + SetCid = 88, + ConstSetCid = 89, + ArrayCid = 90, + ImmutableArrayCid = 91, + GrowableObjectArrayCid = 92, + _StringCid = 93, + OneByteStringCid = 94, + TwoByteStringCid = 95, + FfiNativeFunctionCid = 96, + FfiInt8Cid = 97, + FfiInt16Cid = 98, + FfiInt32Cid = 99, + FfiInt64Cid = 100, + FfiUint8Cid = 101, + FfiUint16Cid = 102, + FfiUint32Cid = 103, + FfiUint64Cid = 104, + FfiFloatCid = 105, + FfiDoubleCid = 106, + FfiVoidCid = 107, + FfiHandleCid = 108, + FfiBoolCid = 109, + FfiNativeTypeCid = 110, + FfiStructCid = 111, + TypedDataInt8ArrayCid = 112, + TypedDataInt8ArrayViewCid = 113, + ExternalTypedDataInt8ArrayCid = 114, + UnmodifiableTypedDataInt8ArrayViewCid = 115, + TypedDataUint8ArrayCid = 116, + TypedDataUint8ArrayViewCid = 117, + ExternalTypedDataUint8ArrayCid = 118, + UnmodifiableTypedDataUint8ArrayViewCid = 119, + TypedDataUint8ClampedArrayCid = 120, + TypedDataUint8ClampedArrayViewCid = 121, + ExternalTypedDataUint8ClampedArrayCid = 122, + UnmodifiableTypedDataUint8ClampedArrayViewCid = 123, + TypedDataInt16ArrayCid = 124, + TypedDataInt16ArrayViewCid = 125, + ExternalTypedDataInt16ArrayCid = 126, + UnmodifiableTypedDataInt16ArrayViewCid = 127, + TypedDataUint16ArrayCid = 128, + TypedDataUint16ArrayViewCid = 129, + ExternalTypedDataUint16ArrayCid = 130, + UnmodifiableTypedDataUint16ArrayViewCid = 131, + TypedDataInt32ArrayCid = 132, + TypedDataInt32ArrayViewCid = 133, + ExternalTypedDataInt32ArrayCid = 134, + UnmodifiableTypedDataInt32ArrayViewCid = 135, + TypedDataUint32ArrayCid = 136, + TypedDataUint32ArrayViewCid = 137, + ExternalTypedDataUint32ArrayCid = 138, + UnmodifiableTypedDataUint32ArrayViewCid = 139, + TypedDataInt64ArrayCid = 140, + TypedDataInt64ArrayViewCid = 141, + ExternalTypedDataInt64ArrayCid = 142, + UnmodifiableTypedDataInt64ArrayViewCid = 143, + TypedDataUint64ArrayCid = 144, + TypedDataUint64ArrayViewCid = 145, + ExternalTypedDataUint64ArrayCid = 146, + UnmodifiableTypedDataUint64ArrayViewCid = 147, + TypedDataFloat32ArrayCid = 148, + TypedDataFloat32ArrayViewCid = 149, + ExternalTypedDataFloat32ArrayCid = 150, + UnmodifiableTypedDataFloat32ArrayViewCid = 151, + TypedDataFloat64ArrayCid = 152, + TypedDataFloat64ArrayViewCid = 153, + ExternalTypedDataFloat64ArrayCid = 154, + UnmodifiableTypedDataFloat64ArrayViewCid = 155, + TypedDataFloat32x4ArrayCid = 156, + TypedDataFloat32x4ArrayViewCid = 157, + ExternalTypedDataFloat32x4ArrayCid = 158, + UnmodifiableTypedDataFloat32x4ArrayViewCid = 159, + TypedDataInt32x4ArrayCid = 160, + TypedDataInt32x4ArrayViewCid = 161, + ExternalTypedDataInt32x4ArrayCid = 162, + UnmodifiableTypedDataInt32x4ArrayViewCid = 163, + TypedDataFloat64x2ArrayCid = 164, + TypedDataFloat64x2ArrayViewCid = 165, + ExternalTypedDataFloat64x2ArrayCid = 166, + UnmodifiableTypedDataFloat64x2ArrayViewCid = 167, + ByteDataViewCid = 168, + UnmodifiableByteDataViewCid = 169, + ByteBufferCid = 170, + NullCid = 171, + DynamicCid = 172, + VoidCid = 173, + NeverCid = 174, + NumPredefinedCids = 175 +} + +#[macro_export] +macro_rules! FFI_TYPES_LIST { + ($callback:ident) => { + $callback! { + FfiNativeFunctionCid, + FfiInt8Cid, + FfiInt16Cid, + FfiInt32Cid, + FfiInt64Cid, + FfiUint8Cid, + FfiUint16Cid, + FfiUint32Cid, + FfiUint64Cid, + FfiFloatCid, + FfiDoubleCid, + FfiVoidCid, + FfiHandleCid, + FfiBoolCid, + FfiNativeTypeCid, + FfiStructCid + } + }; +} + +/* + + +pub const NUM_BASE_OBJECTS_SZ: usize = size_of::(); +pub const NUM_OBJECTS_SZ: usize = size_of::(); +pub const NUM_CLUSTERS_SZ: usize = size_of::(); + +pub const INSTR_TABLE_LEN_SZ: usize = size_of::(); +pub const INSTR_TABLE_OFFSET_SZ: usize = size_of::(); + +pub const CLUSTER_TAGS_SZ: usize = size_of::(); +pub const CLUSTER_OBJ_COUNT_SZ: usize = size_of::(); + +pub const OBJECT_STORE_ENTRY_SIZE: usize = size_of::(); +*/ diff --git a/crates/flutterdec-serwalker/src/info_producer/classes_info.rs b/crates/flutterdec-serwalker/src/info_producer/classes_info.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/crates/flutterdec-serwalker/src/info_producer/classes_info.rs @@ -0,0 +1 @@ + diff --git a/crates/flutterdec-serwalker/src/info_producer/functions_info.rs b/crates/flutterdec-serwalker/src/info_producer/functions_info.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/crates/flutterdec-serwalker/src/info_producer/functions_info.rs @@ -0,0 +1 @@ + diff --git a/crates/flutterdec-serwalker/src/info_producer/libraries_info.rs b/crates/flutterdec-serwalker/src/info_producer/libraries_info.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/crates/flutterdec-serwalker/src/info_producer/libraries_info.rs @@ -0,0 +1 @@ + diff --git a/crates/flutterdec-serwalker/src/info_producer/mod.rs b/crates/flutterdec-serwalker/src/info_producer/mod.rs new file mode 100644 index 0000000..4761c24 --- /dev/null +++ b/crates/flutterdec-serwalker/src/info_producer/mod.rs @@ -0,0 +1,30 @@ +mod classes_info; +mod functions_info; +mod libraries_info; +mod object_pool_info; + +use anyhow::Error; +use flutterdec_adapter::ProgramModel; + +use crate::snapshot::DataSnapshot; + +pub fn produce_model_headers( + model: &mut ProgramModel, + snapshot: &DataSnapshot, +) -> anyhow::Result<()> { + Ok(()) +} + +pub fn produce_model_object_info( + model: &mut ProgramModel, + snapshot: &DataSnapshot, +) -> anyhow::Result<()> { + Ok(()) +} + +pub fn produce_model_object_pool( + model: &mut ProgramModel, + snapshot: &DataSnapshot, +) -> anyhow::Result<()> { + Ok(()) +} diff --git a/crates/flutterdec-serwalker/src/info_producer/object_pool_info.rs b/crates/flutterdec-serwalker/src/info_producer/object_pool_info.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/crates/flutterdec-serwalker/src/info_producer/object_pool_info.rs @@ -0,0 +1 @@ + diff --git a/crates/flutterdec-serwalker/src/instruction_table.rs b/crates/flutterdec-serwalker/src/instruction_table.rs new file mode 100644 index 0000000..a8986c7 --- /dev/null +++ b/crates/flutterdec-serwalker/src/instruction_table.rs @@ -0,0 +1,66 @@ +use crate::stream::Stream; + +#[derive(Default)] +pub struct InstructionTable +// in reality this is the representation of InstructionTable::Data of the C++ code +{ + canonical_stack_map_entries_offset: usize, + length: usize, + first_entry_with_code: usize, + padding: usize, + data: Vec, +} + +// in AOT mode, the instruction table is used to resolve the entry points +// for Function, Code, Closure, etc... Objects +struct DataEntry { + pc_offset: usize, + stack_map_offset: usize, +} + +pub fn parse_instr_table_from_rodata(stream: &mut Stream) -> anyhow::Result { + // ROData objects are wrapped inside OneByteString objects, so we need to read the syntetic fields first. + let _tags = stream.read_raw_u64()?; + let _data_byte_size = stream.read_raw_u64()?; + // i wont do anything with them for now, for a normal snapshot, the tags should be a OneByteString cid + // and data_byte_size should be the size of the contained InstructionsTable::Data + // i.e 4 * sizeof(u32) + (2*sizeof(u32)) * length + // corresponding to the four fields below + the size of each entry + + let mut instruction_table = InstructionTable { + canonical_stack_map_entries_offset: stream.read_raw_u32()? as usize, + length: stream.read_raw_u32()? as usize, + first_entry_with_code: stream.read_raw_u32()? as usize, + padding: stream.read_raw_u32()? as usize, + data: Vec::default(), + }; + + // knowing what i explained in the comment above, we have that necessarily + // 4 * sizeof(u32) + (2*sizeof(u32)) * length <= data_byte_size + + for _idx in 0..instruction_table.length { + let entry = DataEntry { + pc_offset: stream.read_raw_u32()? as usize, + stack_map_offset: stream.read_raw_u32()? as usize, + }; + + instruction_table.data.push(entry); + } + + Ok(instruction_table) +} + +pub fn resolve_entry_points( + code_index: u32, + instr_table: &InstructionTable, +) -> anyhow::Result { + let abs_index = instr_table.first_entry_with_code + code_index as usize; + let entry = instr_table.data.get(abs_index).ok_or_else(|| { + anyhow::anyhow!( + "instruction-table index {abs_index} is out of bounds for length {}", + instr_table.data.len() + ) + })?; + + Ok(entry.pc_offset) +} diff --git a/crates/flutterdec-serwalker/src/lib.rs b/crates/flutterdec-serwalker/src/lib.rs new file mode 100644 index 0000000..7a79fbc --- /dev/null +++ b/crates/flutterdec-serwalker/src/lib.rs @@ -0,0 +1,46 @@ +mod constants; +mod utils; + +mod cluster; +mod instruction_table; +mod program_roots; + +mod raw_object; +mod snapshot; + +mod info_producer; +mod stream; + +use flutterdec_adapter::{AdapterInput, ProgramModel}; + +use crate::{ + constants::DART_3_11_1_SNAPSHOT_HASH, + info_producer::{produce_model_headers, produce_model_object_info, produce_model_object_pool}, + snapshot::parse_snapshot, + stream::Stream, +}; + +pub fn walk_snapshot_and_produce_model( + adapter_input: &AdapterInput, +) -> anyhow::Result { + let mut program_model = ProgramModel { + schema_version: 3, + adapter_kind: "serwalker".to_owned(), + dart_version: "3.11.1".to_owned(), + snapshot_hash: DART_3_11_1_SNAPSHOT_HASH.to_owned(), + arch: "unknown".to_owned(), + libraries: Vec::new(), + classes: Vec::new(), + functions: Vec::new(), + object_pool: Vec::new(), + }; + + let mut isolate_data_stream = Stream::new(adapter_input.isolate_data); + let isolate_data_snapshot = parse_snapshot(&mut isolate_data_stream)?; + + produce_model_headers(&mut program_model, &isolate_data_snapshot)?; + produce_model_object_info(&mut program_model, &isolate_data_snapshot)?; + produce_model_object_pool(&mut program_model, &isolate_data_snapshot)?; + + Ok(program_model) +} diff --git a/crates/flutterdec-serwalker/src/program_roots/mod.rs b/crates/flutterdec-serwalker/src/program_roots/mod.rs new file mode 100644 index 0000000..49261ad --- /dev/null +++ b/crates/flutterdec-serwalker/src/program_roots/mod.rs @@ -0,0 +1,103 @@ +pub mod structs; +use structs::{DispatchTable, DispatchTableEntry, FieldTable, ObjectStore}; + +use crate::stream::Stream; + +pub fn parse_object_store(stream: &mut Stream) -> anyhow::Result { + ObjectStore::read(stream) +} + +pub fn parse_field_table(stream: &mut Stream) -> anyhow::Result { + let mut field_table = FieldTable::default(); + let table_length = stream.read_unsigned()?; + + // extremely unlikely but one never knows + field_table.length = table_length.try_into().map_err(|_| { + anyhow::anyhow!( + "field table of length {} does not fit in usize", + table_length + ) + })?; + + // field_table.field_refs = Vec::with_capacity(table_length as usize); // passed the try_into above, safe to raw cast + + for _ in 0..table_length { + let refid = stream.read_ref_id()?; + field_table.field_refs.push(refid); + } + + Ok(field_table) +} + +pub fn parse_dispatch_table(stream: &mut Stream) -> anyhow::Result { + const RECENT_COUNT: usize = 1 << 6; + const RECENT_MASK: usize = RECENT_COUNT - 1; + const MAX_REPEAT: i64 = RECENT_COUNT as i64 - 1; + const RECENT_MIN: i64 = -MAX_REPEAT; + const INDEX_BASE: i64 = MAX_REPEAT + 1; + + let encoded_length = stream.read_unsigned()?; + let length: usize = encoded_length.try_into().map_err(|_| { + anyhow::anyhow!("dispatch table of length {encoded_length} does not fit in usize") + })?; + + // for an empty table, the serializer writes only its length. + if length == 0 { + return Ok(DispatchTable::default()); + } + + let reference = stream.read_unsigned()?; + let first_code_ref = reference.try_into().map_err(|_| { + anyhow::anyhow!("first Code-cluster reference {reference} does not fit in u32") + })?; + + let mut table = DispatchTable { + first_code_ref: Some(first_code_ref), + // dont reserve from an untrusted claimed length before consuming + // entries, same thing as FieldTable parsing. + entries: Vec::new(), + }; + let mut previous = DispatchTableEntry::Invalid; + let mut recent: [Option; RECENT_COUNT] = [None; RECENT_COUNT]; + let mut recent_index = 0; + let mut repeat_remaining = 0usize; + + while table.entries.len() < length { + if repeat_remaining != 0 { + table.entries.push(previous); + repeat_remaining -= 1; + continue; + } + + let encoded = stream.read()? as i64; + match encoded { + 0 => previous = DispatchTableEntry::Invalid, + RECENT_MIN..=-1 => { + let slot = (!encoded) as usize; + previous = recent[slot].ok_or_else(|| { + anyhow::anyhow!( + "dispatch table recent-entry reference {slot} appears before it is defined" + ) + })?; + } + 1..=MAX_REPEAT => { + repeat_remaining = (encoded - 1) as usize; + } + _ => { + let code_index = (encoded - INDEX_BASE) as u64; + previous = DispatchTableEntry::CodeIndex(code_index); + recent[recent_index] = Some(previous); + recent_index = (recent_index + 1) & RECENT_MASK; + } + } + table.entries.push(previous); + } + + if repeat_remaining != 0 { + anyhow::bail!( + "dispatch table repeat encoding exceeds its declared length by {repeat_remaining} entries" + ); + } + + Ok(table) +} diff --git a/crates/flutterdec-serwalker/src/program_roots/structs.rs b/crates/flutterdec-serwalker/src/program_roots/structs.rs new file mode 100644 index 0000000..6e7ea07 --- /dev/null +++ b/crates/flutterdec-serwalker/src/program_roots/structs.rs @@ -0,0 +1,324 @@ +use crate::stream::Stream; + +macro_rules! object_store_aot_fields { + ($emit:ident) => { + $emit! { + // Layout for the 3.11.X Dart version, and as early as 3.11.0 + list_class: u32, // ClassPtr + map_class: u32, // ClassPtr + set_class: u32, // ClassPtr + non_nullable_list_rare_type: u32, // TypePtr + non_nullable_map_rare_type: u32, // TypePtr + enum_index_field: u32, // FieldPtr + enum_name_field: u32, // FieldPtr + _object_equals_function: u32, // FunctionPtr + _object_hash_code_function: u32, // FunctionPtr + _object_to_string_function: u32, // FunctionPtr + symbol_class: u32, // ClassPtr + symbol_name_field: u32, // FieldPtr + ffi_array_class: u32, // ClassPtr + ffi_compound_class: u32, // ClassPtr + ffi_struct_class: u32, // ClassPtr + ffi_union_class: u32, // ClassPtr + ffi_varargs_class: u32, // ClassPtr + compound_offset_in_bytes_field: u32, // FieldPtr + compound_typed_data_base_field: u32, // FieldPtr + ffi_resolver_function: u32, // FunctionPtr + handle_finalizer_message_function: u32, // FunctionPtr + handle_native_finalizer_message_function: u32, // FunctionPtr + non_nullable_future_never_type: u32, // TypePtr + nullable_future_null_type: u32, // TypePtr + send_port_class: u32, // ClassPtr + capability_class: u32, // ClassPtr + transferable_class: u32, // ClassPtr + lookup_port_handler: u32, // FunctionPtr + lookup_open_ports: u32, // FunctionPtr + handle_message_function: u32, // FunctionPtr + object_class: u32, // ClassPtr + object_type: u32, // TypePtr + non_nullable_object_type: u32, // TypePtr + nullable_object_type: u32, // TypePtr + null_class: u32, // ClassPtr + null_type: u32, // TypePtr + never_class: u32, // ClassPtr + never_type: u32, // TypePtr + function_type: u32, // TypePtr + type_type: u32, // TypePtr + closure_class: u32, // ClassPtr + record_class: u32, // ClassPtr + number_type: u32, // TypePtr + nullable_number_type: u32, // TypePtr + int_type: u32, // TypePtr + non_nullable_int_type: u32, // TypePtr + nullable_int_type: u32, // TypePtr + integer_implementation_class: u32, // ClassPtr + int64_type: u32, // TypePtr + smi_class: u32, // ClassPtr + smi_type: u32, // TypePtr + mint_class: u32, // ClassPtr + mint_type: u32, // TypePtr + double_class: u32, // ClassPtr + double_type: u32, // TypePtr + nullable_double_type: u32, // TypePtr + float32x4_type: u32, // TypePtr + int32x4_type: u32, // TypePtr + float64x2_type: u32, // TypePtr + string_type: u32, // TypePtr + type_argument_int: u32, // TypeArgumentsPtr + type_argument_double: u32, // TypeArgumentsPtr + type_argument_never: u32, // TypeArgumentsPtr + type_argument_string: u32, // TypeArgumentsPtr + type_argument_string_dynamic: u32, // TypeArgumentsPtr + type_argument_string_string: u32, // TypeArgumentsPtr + compiletime_error_class: u32, // ClassPtr + pragma_class: u32, // ClassPtr + pragma_name: u32, // FieldPtr + pragma_options: u32, // FieldPtr + future_class: u32, // ClassPtr + future_or_class: u32, // ClassPtr + one_byte_string_class: u32, // ClassPtr + two_byte_string_class: u32, // ClassPtr + bool_type: u32, // TypePtr + bool_class: u32, // ClassPtr + array_class: u32, // ClassPtr + array_type: u32, // TypePtr + immutable_array_class: u32, // ClassPtr + growable_object_array_class: u32, // ClassPtr + map_impl_class: u32, // ClassPtr + const_map_impl_class: u32, // ClassPtr + set_impl_class: u32, // ClassPtr + const_set_impl_class: u32, // ClassPtr + float32x4_class: u32, // ClassPtr + int32x4_class: u32, // ClassPtr + float64x2_class: u32, // ClassPtr + error_class: u32, // ClassPtr + expando_class: u32, // ClassPtr + iterable_class: u32, // ClassPtr + weak_property_class: u32, // ClassPtr + weak_reference_class: u32, // ClassPtr + finalizer_class: u32, // ClassPtr + finalizer_entry_class: u32, // ClassPtr + native_finalizer_class: u32, // ClassPtr + dart_condition_variable_class: u32, // ClassPtr + dart_mutex_class: u32, // ClassPtr + symbol_table: u32, // WeakArrayPtr + regexp_table: u32, // WeakArrayPtr + canonical_types: u32, // ArrayPtr + canonical_function_types: u32, // ArrayPtr + canonical_record_types: u32, // ArrayPtr + canonical_type_parameters: u32, // ArrayPtr + canonical_type_arguments: u32, // ArrayPtr + async_library: u32, // LibraryPtr + core_library: u32, // LibraryPtr + _compact_hash_library: u32, // LibraryPtr + collection_library: u32, // LibraryPtr + concurrent_library: u32, // LibraryPtr + convert_library: u32, // LibraryPtr + developer_library: u32, // LibraryPtr + ffi_library: u32, // LibraryPtr + _internal_library: u32, // LibraryPtr + isolate_library: u32, // LibraryPtr + math_library: u32, // LibraryPtr + mirrors_library: u32, // LibraryPtr + native_wrappers_library: u32, // LibraryPtr + root_library: u32, // LibraryPtr + typed_data_library: u32, // LibraryPtr + _vm_library: u32, // LibraryPtr + _vmservice_library: u32, // LibraryPtr + native_assets_library: u32, // LibraryPtr + native_assets_map: u32, // ArrayPtr + libraries: u32, // GrowableObjectArrayPtr + libraries_map: u32, // ArrayPtr + uri_to_resolved_uri_map: u32, // ArrayPtr + resolved_uri_to_uri_map: u32, // ArrayPtr + last_libraries_count: u32, // SmiPtr + loading_units: u32, // ArrayPtr + closure_functions: u32, // GrowableObjectArrayPtr + closure_functions_table: u32, // ArrayPtr + pending_classes: u32, // GrowableObjectArrayPtr + record_field_names_map: u32, // ArrayPtr + record_field_names: u32, // ArrayPtr + stack_overflow: u32, // InstancePtr + out_of_memory: u32, // InstancePtr + growable_list_factory: u32, // FunctionPtr + simple_instance_of_function: u32, // FunctionPtr + simple_instance_of_true_function: u32, // FunctionPtr + simple_instance_of_false_function: u32, // FunctionPtr + async_star_stream_controller_add: u32, // FunctionPtr + async_star_stream_controller_add_stream: u32, // FunctionPtr + suspend_state_init_async: u32, // FunctionPtr + suspend_state_await: u32, // FunctionPtr + suspend_state_await_with_type_check: u32, // FunctionPtr + suspend_state_return_async: u32, // FunctionPtr + suspend_state_return_async_not_future: u32, // FunctionPtr + suspend_state_init_async_star: u32, // FunctionPtr + suspend_state_yield_async_star: u32, // FunctionPtr + suspend_state_return_async_star: u32, // FunctionPtr + suspend_state_init_sync_star: u32, // FunctionPtr + suspend_state_suspend_sync_star_at_start: u32, // FunctionPtr + suspend_state_handle_exception: u32, // FunctionPtr + async_star_stream_controller: u32, // ClassPtr + stream_class: u32, // ClassPtr + sync_star_iterator_class: u32, // ClassPtr + async_star_stream_controller_async_star_body: u32, // FieldPtr + sync_star_iterator_current: u32, // FieldPtr + sync_star_iterator_state: u32, // FieldPtr + sync_star_iterator_yield_star_iterable: u32, // FieldPtr + canonicalized_stack_map_entries: u32, // CompressedStackMapsPtr + global_object_pool: u32, // ObjectPoolPtr + unique_dynamic_targets: u32, // ArrayPtr + megamorphic_cache_table: u32, // GrowableObjectArrayPtr + ffi_callback_code: u32, // GrowableObjectArrayPtr + dispatch_table_null_error_stub: u32, // CodePtr + late_initialization_error_stub_with_fpu_regs_stub: u32, // CodePtr + late_initialization_error_stub_without_fpu_regs_stub: u32, // CodePtr + null_error_stub_with_fpu_regs_stub: u32, // CodePtr + null_error_stub_without_fpu_regs_stub: u32, // CodePtr + null_arg_error_stub_with_fpu_regs_stub: u32, // CodePtr + null_arg_error_stub_without_fpu_regs_stub: u32, // CodePtr + null_cast_error_stub_with_fpu_regs_stub: u32, // CodePtr + null_cast_error_stub_without_fpu_regs_stub: u32, // CodePtr + range_error_stub_with_fpu_regs_stub: u32, // CodePtr + range_error_stub_without_fpu_regs_stub: u32, // CodePtr + write_error_stub_with_fpu_regs_stub: u32, // CodePtr + write_error_stub_without_fpu_regs_stub: u32, // CodePtr + field_access_error_stub_with_fpu_regs_stub: u32, // CodePtr + field_access_error_stub_without_fpu_regs_stub: u32, // CodePtr + allocate_mint_with_fpu_regs_stub: u32, // CodePtr + allocate_mint_without_fpu_regs_stub: u32, // CodePtr + stack_overflow_stub_with_fpu_regs_stub: u32, // CodePtr + stack_overflow_stub_without_fpu_regs_stub: u32, // CodePtr + allocate_array_stub: u32, // CodePtr + allocate_mint_stub: u32, // CodePtr + allocate_double_stub: u32, // CodePtr + allocate_float32x4_stub: u32, // CodePtr + allocate_float64x2_stub: u32, // CodePtr + allocate_int32x4_stub: u32, // CodePtr + allocate_int8_array_stub: u32, // CodePtr + allocate_uint8_array_stub: u32, // CodePtr + allocate_uint8_clamped_array_stub: u32, // CodePtr + allocate_int16_array_stub: u32, // CodePtr + allocate_uint16_array_stub: u32, // CodePtr + allocate_int32_array_stub: u32, // CodePtr + allocate_uint32_array_stub: u32, // CodePtr + allocate_int64_array_stub: u32, // CodePtr + allocate_uint64_array_stub: u32, // CodePtr + allocate_float32_array_stub: u32, // CodePtr + allocate_float64_array_stub: u32, // CodePtr + allocate_float32x4_array_stub: u32, // CodePtr + allocate_int32x4_array_stub: u32, // CodePtr + allocate_float64x2_array_stub: u32, // CodePtr + allocate_closure_stub: u32, // CodePtr + allocate_closure_generic_stub: u32, // CodePtr + allocate_closure_ta_stub: u32, // CodePtr + allocate_closure_ta_generic_stub: u32, // CodePtr + allocate_context_stub: u32, // CodePtr + allocate_growable_array_stub: u32, // CodePtr + allocate_object_stub: u32, // CodePtr + allocate_object_parametrized_stub: u32, // CodePtr + allocate_record_stub: u32, // CodePtr + allocate_record2_stub: u32, // CodePtr + allocate_record2_named_stub: u32, // CodePtr + allocate_record3_stub: u32, // CodePtr + allocate_record3_named_stub: u32, // CodePtr + allocate_unhandled_exception_stub: u32, // CodePtr + check_isolate_field_access_stub: u32, // CodePtr + clone_context_stub: u32, // CodePtr + write_barrier_wrappers_stub: u32, // CodePtr + array_write_barrier_stub: u32, // CodePtr + throw_stub: u32, // CodePtr + re_throw_stub: u32, // CodePtr + instance_of_stub: u32, // CodePtr + init_static_field_stub: u32, // CodePtr + init_late_static_field_stub: u32, // CodePtr + init_late_final_static_field_stub: u32, // CodePtr + init_instance_field_stub: u32, // CodePtr + init_late_instance_field_stub: u32, // CodePtr + init_late_final_instance_field_stub: u32, // CodePtr + init_shared_late_static_field_stub: u32, // CodePtr + call_closure_no_such_method_stub: u32, // CodePtr + default_tts_stub: u32, // CodePtr + default_nullable_tts_stub: u32, // CodePtr + top_type_tts_stub: u32, // CodePtr + nullable_type_parameter_tts_stub: u32, // CodePtr + type_parameter_tts_stub: u32, // CodePtr + unreachable_tts_stub: u32, // CodePtr + ffi_callback_functions: u32, // ArrayPtr + resume_stub: u32, // CodePtr + slow_tts_stub: u32, // CodePtr (last field in FullAOT) + } + }; +} + +macro_rules! define_object_store { + ($( $field:ident: u32,)*) => { + #[derive(Default)] + #[repr(C)] + pub(crate) struct ObjectStore { + $( $field: u32, )* + } + + impl ObjectStore { + pub const REF_COUNT: usize = [$(stringify!($field)),*].len(); // this is probably better than sizeof(ObjectStore)/sizeof(u32) + + pub fn read(stream: &mut Stream) -> anyhow::Result { + Ok(Self { + $( $field: stream.read_ref_id()?, )* + }) + } + } + }; +} + +object_store_aot_fields!(define_object_store); + +pub(super) struct IsolateObjectStore +// small one, not too important, skip for now +{ + dart_args_1: u32, // ArrayPtr + dart_args_2: u32, // ArrayPtr + resume_capabilities: u32, // GrowableObjectArrayPtr + exit_listeners: u32, // GrowableObjectArrayPtr + error_listeners: u32, // GrowableObjectArrayPtr +} +#[derive(Default)] +pub(crate) struct FieldTable { + pub(super) length: usize, + pub(super) field_refs: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum DispatchTableEntry { + Invalid, + CodeIndex(u64), // this is an index INTO the instruction table +} + +#[derive(Debug, Default)] +pub(crate) struct DispatchTable { + pub(super) first_code_ref: Option, + pub(super) entries: Vec, +} + +#[derive(Default)] +pub struct ProgramRoots { + object_store: ObjectStore, + field_table: FieldTable, + shared_field_table: FieldTable, + dispatch_table: DispatchTable, +} + +impl ProgramRoots { + pub(crate) fn new( + object_store: ObjectStore, + field_table: FieldTable, + shared_field_table: FieldTable, + dispatch_table: DispatchTable, + ) -> Self { + Self { + object_store, + field_table, + shared_field_table, + dispatch_table, + } + } +} diff --git a/crates/flutterdec-serwalker/src/raw_object/mod.rs b/crates/flutterdec-serwalker/src/raw_object/mod.rs new file mode 100644 index 0000000..b61e737 --- /dev/null +++ b/crates/flutterdec-serwalker/src/raw_object/mod.rs @@ -0,0 +1,551 @@ +pub type Smi = i32; + +#[derive(Default)] +pub struct Object { + pub tags: u64, +} + +#[derive(Default)] +pub struct Class { + pub id: i32, + pub is_predefined: bool, + pub name: u32, // StringPtr + pub user_name: u32, // StringPtr + pub functions: u32, // ArrayPtr + pub functions_hash_table: u32, // ArrayPtr + pub fields: u32, // ArrayPtr + pub offset_in_words_to_field: u32, // ArrayPtr + pub interfaces: u32, // ArrayPtr + pub script: u32, // ScriptPtr + pub library: u32, // LibraryPtr + pub type_parameters: u32, // TypeParametersPtr + pub super_type: u32, // TypePtr + pub constants: u32, // ArrayPtr + pub declaration_type: u32, // TypePtr + pub invocation_dispatcher_cache: u32, // ArrayPtr + pub direct_implementors: u32, // GrowableObjectArrayPtr + pub direct_subclasses: u32, // GrowableObjectArrayPtr + pub declaration_instance_type_arguments: u32, // TypeArgumentsPtr + pub allocation_stub: u32, // CodePtr + pub dependent_code: u32, // WeakArrayPtr + pub num_native_fields: u16, + pub state_bits: u32, + pub kernel_offset: u32, + pub num_type_arguments: i16, + pub host_instance_size_in_words: i32, + pub host_type_arguments_field_offset_in_words: i32, + pub host_next_field_offset_in_words: i32, + pub target_instance_size_in_words: i32, + pub target_type_arguments_field_offset_in_words: i32, + pub target_next_field_offset_in_words: i32, + pub unboxed_fields_bitmap: Option, +} + +#[derive(Default)] +pub struct PatchClass { + pub wrapped_class: u32, // ClassPtr + pub script: u32, // ScriptPtr + pub kernel_program_info: u32, // KernelProgramInfoPtr +} + +#[derive(Default)] +pub struct Function { + pub name: u32, // StringPtr + pub owner: u32, // ObjectPtr + pub signature: u32, // FunctionTypePtr + pub data: u32, // ObjectPtr + // pub ic_data_array_or_bytecode: u32, // ObjectPtr [[NOT PRESENT IN FullAOT]] + pub code_index: u32, // unsigned integer index + // pub positional_parameter_names: u32, // ArrayPtr [[NOT PRESENT IN FullAOT]] + // pub unoptimized_code: u32, // CodePtr [[NOT PRESENT IN FullAOT]] + // pub bitmap: u64, [[NOT PRESENT IN FullAOT]] + pub token_pos: i32, + // pub kernel_offset: u32, [[NOT PRESENT IN FullAOT]] + pub kind_tag: u32, +} + +#[derive(Default)] +pub struct ClosureData { + pub context_scope: u32, // ContextScopePtr + pub parent_function: u32, // FunctionPtr + pub closure: u32, // ClosurePtr + pub packed_fields: u32, +} + +#[derive(Default)] +pub struct FfiTrampolineData { + pub signature_type: u32, // TypePtr + pub c_signature: u32, // FunctionTypePtr + pub callback_target: u32, // FunctionPtr + pub callback_exceptional_return: u32, // InstancePtr + pub ffi_function_kind: u8, + pub callback_id: i32, +} + +#[derive(Default)] +pub struct Field { + pub name: u32, // StringPtr + pub owner: u32, // ObjectPtr + pub type_field: u32, // AbstractTypePtr + pub initializer_function: u32, // FunctionPtr + pub host_offset_or_field_id: u32, // SmiPtr + // pub guarded_list_length: u32, // SmiPtr [[NOT PRESENT IN FullAOT]] + // pub exact_type: u32, // AbstractTypePtr [[NOT PRESENT IN FullAOT]] + // pub dependent_code: u32, // WeakArrayPtr [[NOT PRESENT IN FullAOT]] + pub token_pos: i32, + pub end_token_pos: i32, + pub guarded_cid: u32, + pub is_nullable: u32, + // pub kernel_offset: u32, [[NOT PRESENT IN FullAOT]] + // pub guarded_list_length_in_object_offset: i8, [[NOT PRESENT IN FullAOT]] + // pub static_type_exactness_state: i8, [[NOT PRESENT IN FullAOT]] + // pub target_offset: i32, [[NOT PRESENT IN FullAOT]] + pub kind_bits: u32, +} + +#[derive(Default)] +pub struct Script { + // Fieldless class +} + +#[derive(Default)] +pub struct Library { + pub name: u32, // StringPtr + pub url: u32, // StringPtr + pub private_key: u32, // StringPtr + pub dictionary: u32, // ArrayPtr + pub metadata: u32, // ArrayPtr + pub toplevel_class: u32, // ClassPtr + pub used_scripts: u32, // GrowableObjectArrayPtr + pub loading_unit: u32, // LoadingUnitPtr + pub imports: u32, // ArrayPtr + pub exports: u32, // ArrayPtr + // pub dependencies: u32, // ArrayPtr [[NOT PRESENT IN FullAOT]] + // pub kernel_program_info: u32, // KernelProgramInfoPtr [[NOT PRESENT IN FullAOT]] + // pub loaded_scripts: u32, // ArrayPtr [[NOT PRESENT IN FullAOT]] + pub index: i32, + pub num_imports: u16, + pub load_state: i8, + pub flags: u8, + // pub kernel_library_index: u32, [[NOT PRESENT IN FullAOT]] +} + +#[derive(Default)] +pub struct Namespace { + pub target: u32, // LibraryPtr + pub show_names: u32, // ArrayPtr + pub hide_names: u32, // ArrayPtr + pub owner: u32, // LibraryPtr +} + +#[derive(Default)] +pub struct KernelProgramInfo { + pub kernel_component: u32, // TypedDataBasePtr + pub string_offsets: u32, // TypedDataPtr + pub string_data: u32, // TypedDataViewPtr + pub canonical_names: u32, // TypedDataPtr + pub metadata_payloads: u32, // TypedDataViewPtr + pub metadata_mappings: u32, // TypedDataViewPtr + pub scripts: u32, // ArrayPtr + pub constants: u32, // ArrayPtr + pub constants_table: u32, // TypedDataViewPtr + pub libraries_cache: u32, // ArrayPtr + pub classes_cache: u32, // ArrayPtr +} + +#[derive(Default)] +pub struct CodeSourceMap { + pub length: u32, + pub data: Vec, +} + +#[derive(Default)] +pub struct CompressedStackMaps { + pub length: u32, + pub flags_and_size: u32, + pub data: Vec, +} + +#[derive(Default)] +pub struct PcDescriptors { + // not really + pub length: u32, + pub data: Vec, +} + +#[derive(Default)] +pub struct ExceptionHandlers { + pub handled_types_data: u32, // ArrayPtr + pub packed_fields: u32, + pub num_entries: usize, + pub entries: Vec, +} + +#[derive(Default)] +pub struct ExceptionHandlerInfo { + pub handler_pc_offset: u32, + pub outer_try_index: i16, + pub needs_stacktrace: i8, + pub has_catch_all: i8, + pub is_generated: i8, +} + +#[derive(Default)] +pub struct Context { + pub parent: u32, // ContextPtr + pub num_variables: i32, + pub variables: Vec, +} + +#[derive(Default)] +pub struct ContextScope { + pub num_variables: i32, + pub is_implicit: bool, + /// Flattened `VariableDesc` reference fields. There are ten per variable + /// in this SDK revision. + pub variables: Vec, +} + +#[derive(Default)] +pub struct UnlinkedCall { + pub can_patch_to_monomorphic: bool, +} + +#[derive(Default)] +pub struct ObjectPool { + data: Vec, // vector holding the array of reference ids making up the object pool +} + +#[derive(Default)] +pub struct Mint { + pub value: i64, // ALIGN8 +} + +#[derive(Default)] +pub struct Double { + pub value: f64, // ALIGN8 +} + +#[derive(Default)] +pub struct TypeArguments { + pub instantiations: u32, // ArrayPtr + pub length: Smi, // SmiPtr + pub hash: Smi, // SmiPtr + pub nullability: Smi, // SmiPtr + pub types: Vec, // AbstractTypePtr elements +} + +#[derive(Default)] +pub struct TypeParameter { + pub type_test_stub: u32, // CodePtr + pub hash: u32, // SmiPtr + pub owner: u32, // ObjectPtr + pub base: u16, + pub index: u16, + pub flags: u8, +} + +#[derive(Default)] +pub struct Type { + pub type_test_stub: u32, // CodePtr + pub hash: u32, // SmiPtr + pub arguments: u32, // TypeArgumentsPtr + pub flags: u8, +} + +#[derive(Default)] +pub struct TypeParameters { + pub names: u32, // ArrayPtr + pub flags: u32, // ArrayPtr + pub bounds: u32, // TypeArgumentsPtr + pub defaults: u32, // TypeArgumentsPtr +} + +/* + No need to make two separate structs here. Better to just + have the _String class and add an enum field to determine + the number of bytes "StrType". + +#[derive(Default)] +pub struct OneByteString { + // Fieldless class +} + +#[derive(Default)] +pub struct TwoByteString { + // Fieldless class +} +*/ + +#[derive(Default)] +pub enum StrType { + #[default] + OneByte, // assume the string is a one byte string. + TwoByte, +} + +#[derive(Default)] +pub struct _String { + pub string_type: StrType, + pub hash: Smi, // SmiPtr + pub length: Smi, // SmiPtr + pub internal_str: String, +} + +#[derive(Default)] +pub struct Array { + pub type_arguments: u32, // TypeArgumentsPtr + pub length: Smi, // SmiPtr + pub elements: Vec, // ObjectPtr elements +} + +#[derive(Default)] +pub struct AbstractType { + pub type_test_stub: u32, // CodePtr + pub hash: u32, // SmiPtr + pub padding: u32, + pub flags: u32, +} + +#[derive(Default)] +pub struct FunctionType { + pub type_test_stub: u32, // CodePtr + pub hash: u32, // SmiPtr + pub type_parameters: u32, // TypeParametersPtr + pub result_type: u32, // AbstractTypePtr + pub parameter_types: u32, // ArrayPtr + pub named_parameter_names: u32, // ArrayPtr + pub flags: u8, + pub packed_parameter_counts: u32, + pub packed_type_parameter_counts: u16, +} + +#[derive(Default)] +pub struct Closure { + pub instantiator_type_arguments: u32, // TypeArgumentsPtr + pub function_type_arguments: u32, // TypeArgumentsPtr + pub delayed_type_arguments: u32, // TypeArgumentsPtr + pub function: u32, // FunctionPtr + pub context: u32, // ObjectPtr + pub hash: u32, // SmiPtr +} + +#[derive(Default)] +pub struct Instance { + pub next_field_offset_in_words: i32, + pub instance_size_in_words: i32, + pub unboxed_fields_bitmap: u64, + pub fields: Vec, +} + +#[derive(Debug)] +pub enum InstanceField { + Reference(u32), + Unboxed(u64), +} + +#[derive(Default)] +pub struct WeakArray { + pub next_seen_by_gc: u32, // WeakArrayPtr + pub length: Smi, // SmiPtr + pub elements: Vec, // ObjectPtr elements +} + +#[derive(Default)] +pub struct TypedDataBase { + pub length: Smi, // SmiPtr + pub padding: u32, +} + +#[derive(Default)] +pub struct TypedData { + pub length: usize, + pub data: Vec, +} + +#[derive(Default)] +pub struct TypedDataView { + pub typed_data: u32, // TypedDataBasePtr + pub offset_in_bytes: Smi, // SmiPtr +} + +#[derive(Default)] +pub struct GrowableObjectArray { + pub type_arguments: u32, // TypeArgumentsPtr + pub data: u32, // ArrayPtr + pub length: Smi, // SmiPtr +} + +#[derive(Default)] +pub struct Code { + pub state_bits: i32, +} + +#[derive(Default)] +pub struct LoadingUnit { + pub parent: u32, // LoadingUnitPtr + pub base_objects: u32, // ArrayPtr + pub packed_fields: i64, +} + +#[derive(Default)] +pub struct ICData { + pub target_name: u32, + pub args_descriptor: u32, + pub entries: u32, + pub state_bits: u32, +} + +#[derive(Default)] +pub struct MegamorphicCache { + pub target_name: u32, + pub args_descriptor: u32, + pub buckets: u32, + pub mask: Smi, + pub filled_entry_count: i32, +} + +#[derive(Default)] +pub struct SubtypeTestCache { + pub cache: u32, + pub num_inputs: u32, + pub num_occupied: u32, +} + +#[derive(Default)] +pub struct LanguageError { + pub previous_error: u32, // ErrorPtr + pub script: u32, // ScriptPtr + pub message: u32, // StringPtr + pub formatted_message: u32, // StringPtr + pub token_pos: i32, + pub report_after_token: bool, + pub kind: i8, +} + +#[derive(Default)] +pub struct UnhandledException { + pub exception: u32, // InstancePtr + pub stacktrace: u32, // InstancePtr +} + +#[derive(Default)] +pub struct LibraryPrefix { + pub name: u32, // StringPtr + pub imports: u32, // ArrayPtr + pub importer: u32, // LibraryPtr + pub num_imports: u16, + pub is_deferred_load: bool, +} + +#[derive(Default)] +pub struct RecordType { + pub type_test_stub: u32, // CodePtr + pub hash: u32, // SmiPtr + pub shape: Smi, // SmiPtr + pub field_types: u32, // ArrayPtr + pub flags: u8, +} + +#[derive(Default)] +pub struct Int32x4 { + // Fieldless class +} + +#[derive(Default)] +pub struct ExternalTypedData { + // Fieldless class +} + +#[derive(Default)] +pub struct StackTrace { + pub async_link: u32, // StackTracePtr + pub code_array: u32, // ArrayPtr + pub pc_offset_array: u32, // TypedDataPtr + // pub expand_inlined: bool, [[NOT PRESENT IN FullAOT]] +} + +#[derive(Default)] +pub struct RegExp { + pub capture_name_map: u32, // ArrayPtr + pub pattern: u32, // StringPtr + pub one_byte: u32, // TypedDataPtr + pub two_byte: u32, // TypedDataPtr + pub one_byte_sticky: u32, // TypedDataPtr + pub two_byte_sticky: u32, // TypedDataPtr + pub num_one_byte_registers: i32, + pub num_two_byte_registers: i32, + pub type_flags: i8, +} + +#[derive(Default)] +pub struct WeakProperty { + pub key: u32, // ObjectPtr + pub value: u32, // ObjectPtr + // pub next_seen_by_gc: u32, // WeakPropertyPtr [[NOT PRESENT IN FullAOT]] +} + +#[derive(Default)] +pub struct Map { + pub type_arguments: u32, + pub hash_mask: u32, + pub data: u32, + pub used_data: u32, + pub deleted_keys: u32, + pub index: u32, +} + +#[derive(Default)] +pub struct Set { + pub type_arguments: u32, + pub hash_mask: u32, + pub data: u32, + pub used_data: u32, + pub deleted_keys: u32, + pub index: u32, +} + +#[derive(Default)] +pub struct Float32x4 { + pub value: Vec, +} + +#[derive(Default)] +pub struct Float64x2 { + pub value: Vec, +} + +#[derive(Default)] +pub struct ConstMap { + pub type_arguments: u32, + pub hash_mask: u32, + pub data: u32, + pub used_data: u32, + pub deleted_keys: u32, + pub index: u32, +} + +#[derive(Default)] +pub struct ConstSet { + pub type_arguments: u32, + pub hash_mask: u32, + pub data: u32, + pub used_data: u32, + pub deleted_keys: u32, + pub index: u32, +} + +#[derive(Default)] +pub struct Record { + pub shape: Smi, // SmiPtr + pub padding: u32, + pub num_fields: usize, + pub fields: Vec, +} + +#[derive(Default)] +pub struct ImmutableArray { + pub type_arguments: u32, + pub length: Smi, + pub elements: Vec, +} diff --git a/crates/flutterdec-serwalker/src/snapshot.rs b/crates/flutterdec-serwalker/src/snapshot.rs new file mode 100644 index 0000000..c64bb54 --- /dev/null +++ b/crates/flutterdec-serwalker/src/snapshot.rs @@ -0,0 +1,208 @@ +use std::collections::HashMap; +use std::mem::size_of; + +use crate::cluster::{decide_cluster, Cluster}; +use crate::constants::{ + self, ClassId, DART_3_11_1_SNAPSHOT_HASH, HEADER_SIZE, MAGIC_BYTES, UNSIGNED_M, +}; +use crate::instruction_table::{parse_instr_table_from_rodata, InstructionTable}; +use crate::program_roots::structs::ProgramRoots; +use crate::program_roots::{parse_dispatch_table, parse_field_table, parse_object_store}; +use crate::stream::Stream; +use crate::utils::{decode_tags, DecodedTags}; + +#[derive(Default)] +enum SnapshotKind +// Snapshot::Kind, snapshot.h:24. There is no kModule variant. +{ + Full, + FullCore, + FullJIT, + FullAOT, // Full + AOT code, this is the one we care about, as this is how flutter builds projects + #[default] + None, + Invalid, +} + +impl TryFrom for SnapshotKind { + type Error = &'static str; + + fn try_from(value: u64) -> Result { + match value { + 0 => Ok(SnapshotKind::Full), + 1 => Ok(SnapshotKind::FullCore), + 2 => Ok(SnapshotKind::FullJIT), + 3 => Ok(SnapshotKind::FullAOT), + 4 => Ok(SnapshotKind::None), + 5 => Ok(SnapshotKind::Invalid), + _ => Err("Invalid snapshot kind: header corrupt, or not a snapshot at all."), + } + } +} + +#[derive(Default)] +pub struct DataSnapshot { + clusters: HashMap>, + cluster_order: Vec, // used in the fill step to know which cluster's read_fill function to call + roots: ProgramRoots, + instruction_table: InstructionTable, + + magic_bytes: u32, + size: u64, + kind: SnapshotKind, + + version_hash: String, + features: String, + + num_base_objects: u64, + num_objects: u64, + num_clusters: u64, + + instr_table_len: usize, + instr_table_offset: usize, + + start_of_alloc_area: usize, + start_of_fill_area: usize, + + end_of_alloc_area: usize, + end_of_fill_area: usize, +} + +impl DataSnapshot { + fn parse_version_and_features(&mut self, stream: &mut Stream) -> anyhow::Result<()> { + let mut version_and_features = stream.read_c_string()?; + + self.features = version_and_features.split_off(constants::VERSION_HASH_LENGTH); // returns (str[hash_len..]) + self.version_hash = version_and_features; + Ok(()) + } + + fn parse_header(&mut self, stream: &mut Stream) -> anyhow::Result<()> { + self.magic_bytes = stream.read_raw_u32()?; + + if self.magic_bytes != MAGIC_BYTES { + anyhow::bail!("Not a snapshot...") + } + + self.size = stream.read_raw_u64()?; + self.kind = + SnapshotKind::try_from(stream.read_raw_u64()?).map_err(|e| anyhow::anyhow!(e))?; + + if !matches!(&self.kind, SnapshotKind::FullAOT) { + anyhow::bail!("Serwalker currently supports FullAOT snapshots only"); + } + + self.parse_version_and_features(stream)?; + + if self.version_hash != DART_3_11_1_SNAPSHOT_HASH { + anyhow::bail!( + "unsupported Dart snapshot hash {}; expected {} (Dart 3.11.1)", + self.version_hash, + DART_3_11_1_SNAPSHOT_HASH + ); + } + + if !self + .features + .split_ascii_whitespace() + .any(|feature| feature == "compressed-pointers") + { + anyhow::bail!("Serwalker currently requires a compressed-pointers snapshot"); + } + + self.num_base_objects = stream.read_unsigned()?; + self.num_objects = stream.read_unsigned()?; + self.num_clusters = stream.read_unsigned()?; + + self.instr_table_len = stream.read_unsigned()? as usize; + self.instr_table_offset = stream.read_unsigned()? as usize; + Ok(()) + } + + fn parse_clusters(&mut self, stream: &mut Stream) -> anyhow::Result<()> { + let mut curr_ref_id: u64 = self.num_base_objects + 1; // all objects are numbered starting from num_base_objects + 1 + + self.start_of_alloc_area = stream.get_current_pos(); + for _cluster_idx in 0..self.num_clusters { + let tags: u32 = stream.read()? as u32; + let decoded_tags: DecodedTags = decode_tags(tags)?; + let cid = decoded_tags.get_cid(); + + let mut cluster = decide_cluster(cid).map_err(|_| { + anyhow::anyhow!("Couldn't find cluster implementation for class {:?}", cid) + })?; + + cluster.set_metadata( + tags, + cid, + decoded_tags.is_immutable(), + decoded_tags.is_canonical(), + ); + cluster.read_alloc(&mut curr_ref_id, stream)?; + + // Composite key exactly as suggested to PR reviewer + let key = (cid as u32) << 2 + | ((decoded_tags.is_canonical() as u32) << 1) + | (decoded_tags.is_immutable() as u32); + self.clusters.insert(key, cluster); + self.cluster_order.push(key); + } + self.end_of_alloc_area = stream.get_current_pos(); + + // ASSERT_EQUAL(next_ref_index_ - kFirstReference, num_objects_) + // app_snapshot.cc:9591. Cheapest possible desync detector. + let allocated = curr_ref_id - 1; + if allocated != self.num_objects { + anyhow::bail!( + "alloc pass allocated {allocated} refs, header declares {}", + self.num_objects + ); + } + + self.start_of_fill_area = stream.get_current_pos(); + for key in self.cluster_order.iter() { + let cluster = self.clusters.get_mut(key).unwrap(); + (*cluster).read_fill(stream)?; + } + self.end_of_fill_area = stream.get_current_pos(); + Ok(()) + } + + fn parse_roots(&mut self, stream: &mut Stream) -> anyhow::Result<()> { + let object_store = parse_object_store(stream)?; + let field_table = parse_field_table(stream)?; + let shared_field_table = parse_field_table(stream)?; + let dispatch_table = parse_dispatch_table(stream)?; + + self.roots = ProgramRoots::new( + object_store, + field_table, + shared_field_table, + dispatch_table, + ); + Ok(()) + } + + fn parse_instruction_table(&mut self, stream: &mut Stream) -> anyhow::Result<()> { + self.instruction_table = parse_instr_table_from_rodata(stream)?; + Ok(()) + } +} + +pub fn parse_snapshot(stream: &mut Stream) -> anyhow::Result { + let mut snapshot = DataSnapshot::default(); + + println!("Now parsing the snapshot..."); + snapshot.parse_header(stream)?; + snapshot.parse_clusters(stream)?; + snapshot.parse_roots(stream)?; // right after we finish reading the roots we need to align + + stream.align_stream(HEADER_SIZE)?; + + // after that, we land right at the start of the ROData image + // where the instruction table is, at an offset we already know + stream.seek(stream.get_current_pos() + snapshot.instr_table_offset)?; + snapshot.parse_instruction_table(stream)?; + + Ok(snapshot) +} diff --git a/crates/flutterdec-serwalker/src/stream.rs b/crates/flutterdec-serwalker/src/stream.rs new file mode 100644 index 0000000..f1c7e2e --- /dev/null +++ b/crates/flutterdec-serwalker/src/stream.rs @@ -0,0 +1,158 @@ +use anyhow::{anyhow, bail, Result}; + +use crate::constants::{DATA_BITS_PER_BYTE, SIGNED_M, UNSIGNED_M, UNSIGNED_MAX_DATA_PER_BYTE}; + +pub struct Stream<'a> { + byte_stream: &'a [u8], + curr_stream_offset: usize, +} + +impl<'a> Stream<'a> { + pub fn new(byte_stream: &'a [u8]) -> Self { + Self { + byte_stream, + curr_stream_offset: 0, + } + } + + pub fn seek(&mut self, pos: usize) -> Result<()> { + if pos > self.byte_stream.len() { + bail!( + "seek to {pos} past end of snapshot ({})", + self.byte_stream.len() + ); + } + self.curr_stream_offset = pos; + Ok(()) + } + + pub fn align_stream(&mut self, alignment: usize) -> anyhow::Result<()> { + let mut next_pos = self.get_current_pos(); + if next_pos % alignment == 0 { + return Ok(()); + } + + next_pos = next_pos & !(alignment - 1); + next_pos += alignment; + + self.seek(next_pos) + } + + fn take(&mut self, n: usize) -> Result<&[u8]> { + let end = self + .curr_stream_offset + .checked_add(n) + .filter(|e| *e <= self.byte_stream.len()) + .ok_or_else(|| { + anyhow!( + "read of {n} bytes past end of snapshot at offset {}", + self.curr_stream_offset + ) + })?; + let slice = &self.byte_stream[self.curr_stream_offset..end]; + self.curr_stream_offset = end; + Ok(slice) + } + + pub fn get_current_pos(&self) -> usize { + self.curr_stream_offset + } + + /// Dart's modified LEB128: little endian 7 bit groups, continuation bit 0, + /// final byte has its MSb set. ReadStream::Read, datastream.h:231 + fn read_leb128(&mut self, end_byte_marker: u8) -> Result { + let mut value: u64 = 0; + let mut shift: usize = 0; + loop { + let byte = self.read_byte()?; + if byte > UNSIGNED_MAX_DATA_PER_BYTE { + // Final byte. wrapping_sub mimics C++ unsigned underflow, so the + // signed variant sign extends and narrowing casts stay congruent. + let tail = (byte as u64).wrapping_sub(end_byte_marker as u64); + return Ok(value | (tail << shift)); + } + value |= (byte as u64) << shift; + shift += DATA_BITS_PER_BYTE; + if shift >= 64 { + bail!( + "LEB128 value exceeds 64 bits at offset {}", + self.curr_stream_offset + ); + } + } + } + + /// ReadStream::Read(), datastream.h:153. kEndByteMarker == 0xC0. + pub fn read(&mut self) -> Result { + self.read_leb128(SIGNED_M) + } + + /// ReadStream::ReadUnsigned(), datastream.h:99. kEndUnsignedByteMarker == 0x80. + pub fn read_unsigned(&mut self) -> Result { + self.read_leb128(UNSIGNED_M) + } + + /// Raw fixed width little endian, header fields only. Mostly everything else inside the + /// clustered body is LEB128. Renamed so the two can't get mixed up. + pub fn read_raw_u64(&mut self) -> Result { + Ok(u64::from_le_bytes( + self.take(8)?.try_into().expect("take(8) is 8 bytes"), + )) + } + + pub fn read_raw_u32(&mut self) -> Result { + Ok(u32::from_le_bytes( + self.take(4)?.try_into().expect("take(4) is 4 bytes"), + )) + } + + pub fn read_byte(&mut self) -> Result { + Ok(self.take(1)?[0]) + } + + pub fn read_c_string(&mut self) -> Result { + let nul = self.byte_stream[self.curr_stream_offset..] + .iter() + .position(|&b| b == 0) + .ok_or_else(|| { + anyhow!( + "unterminated C string at offset {}", + self.curr_stream_offset + ) + })?; + let raw = self.take(nul + 1)?; + Ok(String::from_utf8(raw[..nul].to_vec())?) + } + + /// Dart OneByteString payload is Latin-1, not UTF-8. See the string cluster comment. + pub fn read_latin1(&mut self, len: usize) -> Result { + Ok(self.take(len)?.iter().map(|&b| b as char).collect()) + } + + /// ReadStream::ReadRefId(), datastream.h:103. + /// Big endian VLI, not LEB128. Dart caps it at 5 stages (28 bits). + pub fn read_ref_id(&mut self) -> Result { + let mut result: i64 = 0; + for _ in 0..5 { + let byte = self.read_byte()? as i8; + result = (result << 7) + byte as i64; + if byte < 0 { + return Ok((result + 128) as u32); + } + } + bail!( + "ref id longer than 5 bytes at offset {}", + self.curr_stream_offset + ) + } + + /// Reads a block of bytes and returns a newly allocated Vec (Creates a copy) + pub fn read_bytes(&mut self, len: usize) -> Result> { + Ok(self.take(len)?.to_vec()) + } + + /// Reads a block of bytes and returns a reference to the slice (Zero-copy, highly recommended for large payloads) + pub fn read_bytes_zero_copy(&mut self, len: usize) -> Result<&[u8]> { + self.take(len) + } +} diff --git a/crates/flutterdec-serwalker/src/utils.rs b/crates/flutterdec-serwalker/src/utils.rs new file mode 100644 index 0000000..7a36584 --- /dev/null +++ b/crates/flutterdec-serwalker/src/utils.rs @@ -0,0 +1,159 @@ +use crate::constants::ClassId; + +#[macro_export] +macro_rules! DECLARE_FIXED_LENGTH_CLUSTER { + ($name:ident, $cluster_name:ident, |$_self:ident, $stream:ident| $fill_impl:block) => { + pub struct $cluster_name { + tags: u32, + cid: ClassId, + is_immutable: bool, + is_canonical: bool, + obj_count: u64, + + start_of_fill: usize, + start_of_alloc: usize, + + end_of_fill: usize, + end_of_alloc: usize, + + first_ref_id: u32, + + objs: Vec>, + } + + impl Cluster for $cluster_name { + fn set_metadata( + &mut self, + tags: u32, + cid: ClassId, + is_immutable: bool, + is_canonical: bool, + ) { + self.tags = tags; + self.cid = cid; + self.is_immutable = is_immutable; + self.is_canonical = is_canonical; + } + + fn read_alloc( + &mut self, + last_ref_id: &mut u64, + stream: &mut Stream, + ) -> anyhow::Result { + self.start_of_alloc = stream.get_current_pos(); + self.first_ref_id = *last_ref_id as u32; + + self.obj_count = stream.read_unsigned()?; + + for _obj_idx in 0..self.obj_count { + self.objs.push(Box::<$name>::default()); + } + + *last_ref_id += self.obj_count; + self.end_of_alloc = stream.get_current_pos(); + + Ok(self.end_of_alloc - self.start_of_alloc) + } + + fn read_fill(&mut self, stream: &mut Stream) -> anyhow::Result { + self.start_of_fill = stream.get_current_pos(); + + let $_self = self; + let $stream = stream; + + $fill_impl; + + $_self.end_of_fill = $stream.get_current_pos(); + + Ok($_self.end_of_fill - $_self.start_of_fill) + } + + fn is_fixed_len(&self) -> bool { + true + } + } + }; +} + +#[macro_export] +macro_rules! DECLARE_VARIABLE_LENGTH_CLUSTER { + ($name:ident, $cluster_name:ident) => { + pub struct $cluster_name { + tags: u32, + cid: ClassId, + is_immutable: bool, + is_canonical: bool, + obj_count: u64, + + start_of_fill: usize, + start_of_alloc: usize, + + end_of_fill: usize, + end_of_alloc: usize, + + first_ref_id: u32, + + objs: Vec>, + } + }; +} + +pub struct DecodedTags { + class_id: ClassId, + is_immutable: bool, + is_canonical: bool, +} + +impl DecodedTags { + pub fn new(cid: ClassId, immut: bool, canonical: bool) -> Self { + Self { + class_id: cid, + is_immutable: immut, + is_canonical: canonical, + } + } + + pub fn get_cid(&self) -> ClassId { + self.class_id + } + + pub fn is_immutable(&self) -> bool { + self.is_immutable + } + + pub fn is_canonical(&self) -> bool { + self.is_canonical + } +} + +macro_rules! DECODE_CID { + ($tags:expr) => { + ClassId::try_from(($tags >> 12) & 0xFFFFF) + }; +} +macro_rules! DECODE_IS_IMMUTABLE { + ($tags:expr) => { + // Dart 3.11.1 UntaggedObject::ImmutableBit is bit 6. Mainline later + // split this into ShallowImmutableBit (6) and DeeplyImmutableBit (7). + (($tags >> 6) & 0x1) == 1 + }; +} +macro_rules! DECODE_IS_CANONICAL { + ($tags:expr) => { + (($tags >> 1) & 0x1) == 1 + }; +} + +pub fn decode_tags(tags: u32) -> anyhow::Result { + let class_id = DECODE_CID!(tags).map_err(|_| { + anyhow::anyhow!( + "unknown class id {} in tags {tags:#x}", + (tags >> 12) & 0xFFFFF + ) + })?; + Ok(DecodedTags::new( + class_id, + DECODE_IS_IMMUTABLE!(tags), + DECODE_IS_CANONICAL!(tags), + )) +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 73cb934..3fe7418 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] channel = "stable" -components = ["rustfmt", "clippy"] +components = ["rustfmt", "clippy", "rust-analyzer"]