Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions crates/flutterdec-serwalker/src/cluster/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -559,3 +559,146 @@ impl Cluster for _StringCluster {
Ok(self.end_of_fill - self.start_of_fill)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::constants::UNSIGNED_M;

fn leb_unsigned(mut value: u64) -> Vec<u8> {
let mut out = Vec::new();
loop {
let low = (value & 0x7f) as u8;
let rest = value >> 7;
if rest == 0 && low <= (0xff - UNSIGNED_M) {
out.push(low + UNSIGNED_M);
return out;
}
out.push(low);
value = rest;
}
}

/// StringDeserializationCluster writes `length << 1 | is_two_byte` in the
/// alloc section and again in the fill section, then the payload.
fn encode_string_cluster(strings: &[(&str, bool)]) -> Vec<u8> {
let encoded = |s: &str, two: bool| -> u64 {
let len = if two {
s.chars().map(|c| c.len_utf16()).sum()
} else {
s.chars().count()
};
((len as u64) << 1) | two as u64
};
let mut alloc = leb_unsigned(strings.len() as u64);
for (s, two) in strings {
alloc.extend_from_slice(&leb_unsigned(encoded(s, *two)));
}
let mut fill = Vec::new();
for (s, two) in strings {
fill.extend_from_slice(&leb_unsigned(encoded(s, *two)));
if *two {
for u in s.encode_utf16() {
fill.extend_from_slice(&u.to_le_bytes());
}
} else {
fill.extend(s.chars().map(|c| c as u32 as u8));
}
}
alloc.extend(fill);
alloc
}

fn read_strings(bytes: &[u8], count: usize) -> Vec<String> {
let mut cluster = _StringCluster::default();
let mut stream = Stream::new(bytes);
let mut next_ref = 1u64;
cluster.read_alloc(&mut next_ref, &mut stream).unwrap();
cluster.read_fill(&mut stream).unwrap();
assert_eq!(
next_ref,
1 + count as u64,
"alloc must claim one ref per string"
);
cluster
.objs
.iter()
.map(|o| o.internal_str.clone())
.collect()
}

#[test]
fn one_byte_strings_round_trip() {
let input = [
("main", false),
("package:app/main.dart", false),
("", false),
];
let bytes = encode_string_cluster(&input);
assert_eq!(
read_strings(&bytes, input.len()),
vec!["main", "package:app/main.dart", ""]
);
}

/// The payload is Latin-1, so bytes above 0x7F are ordinary characters.
/// Decoding as UTF-8 used to panic the whole parse here.
#[test]
fn one_byte_strings_carry_latin1_payloads() {
let input = [("caf\u{e9}", false), ("\u{ff}\u{80}", false)];
let bytes = encode_string_cluster(&input);
assert_eq!(
read_strings(&bytes, input.len()),
vec!["caf\u{e9}", "\u{ff}\u{80}"]
);
}

#[test]
fn two_byte_strings_round_trip() {
let input = [("\u{4f60}\u{597d}", true), ("mixed \u{2713}", true)];
let bytes = encode_string_cluster(&input);
assert_eq!(
read_strings(&bytes, input.len()),
vec!["\u{4f60}\u{597d}", "mixed \u{2713}"]
);
}

/// Both widths in one cluster: the per-object type comes from the low bit
/// of the encoded word, so mixing them catches a mis-shifted length.
#[test]
fn mixed_width_cluster_stays_in_sync() {
let input = [
("ascii", false),
("\u{4f60}", true),
("caf\u{e9}", false),
("\u{2713}", true),
];
let bytes = encode_string_cluster(&input);
assert_eq!(
read_strings(&bytes, input.len()),
vec!["ascii", "\u{4f60}", "caf\u{e9}", "\u{2713}"]
);
}

/// Dart strings may hold unpaired surrogates, which are not valid UTF-16.
/// Those must decode lossily rather than panic.
#[test]
fn unpaired_surrogates_do_not_panic() {
let mut bytes = leb_unsigned(1);
bytes.extend_from_slice(&leb_unsigned((1 << 1) | 1)); // one two-byte unit
bytes.extend_from_slice(&leb_unsigned((1 << 1) | 1)); // repeated in the fill section
bytes.extend_from_slice(&0xd800u16.to_le_bytes()); // lone high surrogate
assert_eq!(read_strings(&bytes, 1), vec!["\u{fffd}"]);
}

#[test]
fn a_truncated_payload_errors_instead_of_panicking() {
let mut bytes = encode_string_cluster(&[("hello", false)]);
bytes.truncate(bytes.len() - 3);
let mut cluster = _StringCluster::default();
let mut stream = Stream::new(&bytes);
let mut next_ref = 1u64;
cluster.read_alloc(&mut next_ref, &mut stream).unwrap();
assert!(cluster.read_fill(&mut stream).is_err());
}
}
28 changes: 7 additions & 21 deletions crates/flutterdec-serwalker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,12 @@ mod stream;

use flutterdec_adapter::ProgramModel;

use crate::{
info_producer::{enrich_model_headers, enrich_model_object_info},
snapshot::parse_snapshot,
stream::Stream,
};

#[allow(dead_code)]
fn walk_snapshot_and_enrich_model(
isolate_data: &[u8],
isolate_instr: &[u8],
vm_data: Option<&[u8]>,
vm_instr: Option<&[u8]>,
) -> ProgramModel {
let program_model: ProgramModel;

let mut isolate_data_stream = Stream::new(isolate_data);
let isolate_data_snapshot = parse_snapshot(&mut isolate_data_stream)?;

enrich_model_headers(&mut program_model, &isolate_data_snapshot);
enrich_model_object_info(&mut program_model, &isolate_data_snapshot);
enrich_model_object_pool(&mut program_model, &isolate_data_snapshot);

program_model
_isolate_data: &[u8],
_isolate_instr: &[u8],
_vm_data: Option<&[u8]>,
_vm_instr: Option<&[u8]>,
) -> anyhow::Result<ProgramModel> {
todo!("wire parse_snapshot into the info_producer passes")
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use flutterdec_adapter::LibraryInfo;

#[allow(dead_code)]
pub fn resolve_root_library() -> anyhow::Result<LibraryInfo> {
let root_lib: LibraryInfo;

Ok(root_lib)
todo!("resolve the root library from the object store")
}
174 changes: 172 additions & 2 deletions crates/flutterdec-serwalker/src/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ impl TryFrom<u64> for SnapshotKind {
pub struct DataSnapshot {
clusters: HashMap<u32, Box<dyn Cluster>>,
cluster_order: Vec<u32>, // used in the fill step to know which cluster's read_fill function to call
roots: ProgramRoots,

// roots: ProgramRoots, // TODO: define ProgramRoots in object_store
magic_bytes: u32,
size: u64,
kind: SnapshotKind,
Expand Down Expand Up @@ -151,3 +150,174 @@ pub fn parse_snapshot(stream: &mut Stream) -> anyhow::Result<DataSnapshot> {

Ok(snapshot)
}

#[cfg(test)]
mod tests {
use super::*;
use crate::constants::{SIGNED_M, UNSIGNED_M};

fn leb(mut value: u64, marker: u8) -> Vec<u8> {
let mut out = Vec::new();
loop {
let low = (value & 0x7f) as u8;
let rest = value >> 7;
if rest == 0 && low <= (0xff - marker) {
out.push(low + marker);
return out;
}
out.push(low);
value = rest;
}
}

/// Snapshot header, snapshot.h:36. magic(u32) + length(i64) + kind(i64) as
/// raw little endian, then 32 version chars and a NUL terminated feature
/// string, then five LEB128 counts.
struct Header {
kind: u64,
version: String,
features: String,
num_base_objects: u64,
num_objects: u64,
num_clusters: u64,
}

impl Default for Header {
fn default() -> Self {
Self {
kind: 3, // kFullAOT
version: "b3d0d9d1c1e8b57e9b31f8e0e4a5c7d2".into(),
features: "product no-code_comments arm64".into(),
num_base_objects: 42,
num_objects: 1000,
num_clusters: 7,
}
}
}

impl Header {
fn encode(&self, magic: u32) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&magic.to_le_bytes());
b.extend_from_slice(&0u64.to_le_bytes()); // length, unused by the parser
b.extend_from_slice(&self.kind.to_le_bytes());
b.extend_from_slice(self.version.as_bytes());
b.extend_from_slice(self.features.as_bytes());
b.push(0);
for v in [
self.num_base_objects,
self.num_objects,
self.num_clusters,
0, // instructions table length
0, // instructions table offset
] {
b.extend_from_slice(&leb(v, UNSIGNED_M));
}
b
}
}

fn parse(bytes: &[u8]) -> anyhow::Result<DataSnapshot> {
let mut snap = DataSnapshot::default();
let mut stream = Stream::new(bytes);
snap.parse_header(&mut stream)?;
Ok(snap)
}

#[test]
fn parses_a_well_formed_header() {
let h = Header::default();
let snap = parse(&h.encode(MAGIC_BYTES)).unwrap();
assert_eq!(snap.magic_bytes, MAGIC_BYTES);
assert_eq!(snap.num_base_objects, 42);
assert_eq!(snap.num_objects, 1000);
assert_eq!(snap.num_clusters, 7);
}

/// The version hash is 32 raw chars with no terminator and the features
/// string runs to the NUL, so the split is positional. Getting the length
/// wrong silently corrupts both fields rather than failing.
#[test]
fn splits_version_from_features_at_thirty_two_chars() {
let h = Header::default();
let snap = parse(&h.encode(MAGIC_BYTES)).unwrap();
assert_eq!(snap.version_hash, h.version);
assert_eq!(
snap.version_hash.len(),
crate::constants::VERSION_HASH_LENGTH
);
assert_eq!(snap.features, h.features);
}

#[test]
fn rejects_a_bad_magic() {
let h = Header::default();
assert!(parse(&h.encode(0xdeadbeef)).is_err());
}

#[test]
fn rejects_an_out_of_range_snapshot_kind() {
let h = Header {
kind: 99,
..Default::default()
};
assert!(parse(&h.encode(MAGIC_BYTES)).is_err());
}

/// kFull=0, kFullCore=1, kFullJIT=2, kFullAOT=3, kNone=4, kInvalid=5.
/// There is no kModule, so 5 must be the last valid value.
#[test]
fn snapshot_kind_numbering_matches_dart() {
for k in 0..=5u64 {
assert!(
SnapshotKind::try_from(k).is_ok(),
"kind {k} should be valid"
);
}
assert!(
SnapshotKind::try_from(6).is_err(),
"there is no seventh kind"
);
assert!(matches!(
SnapshotKind::try_from(3),
Ok(SnapshotKind::FullAOT)
));
assert!(matches!(SnapshotKind::try_from(4), Ok(SnapshotKind::None)));
}

/// The counts are ReadUnsigned (0x80), not Read (0xC0). Encoding them with
/// the other marker must not silently produce plausible numbers.
#[test]
fn header_counts_use_the_unsigned_marker() {
let h = Header {
num_objects: 1000,
..Default::default()
};
let good = parse(&h.encode(MAGIC_BYTES)).unwrap();
assert_eq!(good.num_objects, 1000);

// Re-encode just the counts with the signed marker.
let mut b = h.encode(MAGIC_BYTES);
let prefix = 20 + h.version.len() + h.features.len() + 1;
b.truncate(prefix);
for v in [h.num_base_objects, h.num_objects, h.num_clusters, 0, 0] {
b.extend_from_slice(&leb(v, SIGNED_M));
}
let wrong = parse(&b).unwrap();
assert_ne!(
wrong.num_objects, 1000,
"wrong marker must not decode cleanly"
);
}

#[test]
fn truncated_headers_error_instead_of_panicking() {
let full = Header::default().encode(MAGIC_BYTES);
for cut in [0, 4, 12, 20, 30, full.len() - 1] {
assert!(
parse(&full[..cut]).is_err(),
"truncation at {cut} should error"
);
}
}
}
Loading
Loading