use std::collections::BTreeMap;
use std::io::Write;
use watto::{Pod, StringTable};
use crate::builder::{self, ParsedProguardMapping};
use crate::ProguardMapping;
use super::{CacheError, CacheErrorKind};
const PRGCACHE_MAGIC_BYTES: [u8; 4] = *b"PRGC";
pub(crate) const PRGCACHE_MAGIC: u32 = u32::from_le_bytes(PRGCACHE_MAGIC_BYTES);
pub(crate) const PRGCACHE_MAGIC_FLIPPED: u32 = PRGCACHE_MAGIC.swap_bytes();
pub const PRGCACHE_VERSION: u32 = 4;
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C)]
pub(crate) struct Header {
pub(crate) magic: u32,
pub(crate) version: u32,
pub(crate) num_classes: u32,
pub(crate) num_members: u32,
pub(crate) num_members_by_params: u32,
pub(crate) num_outline_pairs: u32,
pub(crate) num_rewrite_rule_entries: u32,
pub(crate) num_rewrite_rule_components: u32,
pub(crate) string_bytes: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(C)]
pub(crate) struct Class {
pub(crate) obfuscated_name_offset: u32,
pub(crate) original_name_offset: u32,
pub(crate) file_name_offset: u32,
pub(crate) members_offset: u32,
pub(crate) members_len: u32,
pub(crate) members_by_params_offset: u32,
pub(crate) members_by_params_len: u32,
pub(crate) is_synthesized: u8,
pub(crate) _reserved: [u8; 3],
}
impl Class {
pub(crate) fn is_synthesized(&self) -> bool {
self.is_synthesized != 0
}
}
impl Default for Class {
fn default() -> Self {
Self {
obfuscated_name_offset: u32::MAX,
original_name_offset: u32::MAX,
file_name_offset: u32::MAX,
members_offset: u32::MAX,
members_len: 0,
members_by_params_offset: u32::MAX,
members_by_params_len: 0,
is_synthesized: 0,
_reserved: [0; 3],
}
}
}
const NONE_VALUE: u32 = u32::MAX;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[repr(C)]
pub(crate) struct Member {
pub(crate) obfuscated_name_offset: u32,
pub(crate) startline: u32,
pub(crate) endline: u32,
pub(crate) original_class_offset: u32,
pub(crate) original_file_offset: u32,
pub(crate) original_name_offset: u32,
pub(crate) original_startline: u32,
pub(crate) original_endline: u32,
pub(crate) params_offset: u32,
pub(crate) outline_pairs_offset: u32,
pub(crate) outline_pairs_len: u32,
pub(crate) rewrite_rules_offset: u32,
pub(crate) rewrite_rules_len: u32,
pub(crate) is_synthesized: u8,
pub(crate) is_outline: u8,
pub(crate) _reserved: [u8; 2],
}
impl Member {
pub(crate) fn is_synthesized(&self) -> bool {
self.is_synthesized != 0
}
pub(crate) fn is_outline(&self) -> bool {
self.is_outline != 0
}
pub(crate) fn startline(&self) -> Option<u32> {
if self.startline == NONE_VALUE {
None
} else {
Some(self.startline)
}
}
pub(crate) fn endline(&self) -> Option<u32> {
if self.endline == NONE_VALUE {
None
} else {
Some(self.endline)
}
}
pub(crate) fn original_startline(&self) -> Option<u32> {
if self.original_startline == NONE_VALUE {
None
} else {
Some(self.original_startline)
}
}
}
unsafe impl Pod for Header {}
unsafe impl Pod for Class {}
unsafe impl Pod for Member {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
pub(crate) struct OutlinePair {
pub(crate) outline_pos: u32,
pub(crate) callsite_line: u32,
}
unsafe impl Pod for OutlinePair {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
pub(crate) struct RewriteRuleEntry {
pub(crate) conditions_offset: u32,
pub(crate) conditions_len: u32,
pub(crate) actions_offset: u32,
pub(crate) actions_len: u32,
}
unsafe impl Pod for RewriteRuleEntry {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
pub(crate) struct RewriteComponent {
pub(crate) kind: u32,
pub(crate) value: u32,
}
unsafe impl Pod for RewriteComponent {}
pub(crate) const REWRITE_CONDITION_THROWS: u32 = 0;
pub(crate) const REWRITE_CONDITION_UNKNOWN: u32 = u32::MAX;
pub(crate) const REWRITE_ACTION_REMOVE_INNER_FRAMES: u32 = 0;
pub(crate) const REWRITE_ACTION_UNKNOWN: u32 = u32::MAX;
#[derive(Clone, PartialEq, Eq)]
pub struct ProguardCache<'data> {
pub(crate) header: &'data Header,
pub(crate) classes: &'data [Class],
pub(crate) members: &'data [Member],
pub(crate) members_by_params: &'data [Member],
pub(crate) outline_pairs: &'data [OutlinePair],
pub(crate) rewrite_rule_entries: &'data [RewriteRuleEntry],
pub(crate) rewrite_rule_components: &'data [RewriteComponent],
pub(crate) string_bytes: &'data [u8],
}
impl std::fmt::Debug for ProguardCache<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ProguardCache")
.field("version", &self.header.version)
.field("classes", &self.header.num_classes)
.field("members", &self.header.num_members)
.field("members_by_params", &self.header.num_members_by_params)
.field("string_bytes", &self.header.string_bytes)
.finish()
}
}
impl<'data> ProguardCache<'data> {
pub fn parse(buf: &'data [u8]) -> Result<Self, CacheError> {
let (header, rest) = Header::ref_from_prefix(buf).ok_or(CacheErrorKind::InvalidHeader)?;
if header.magic == PRGCACHE_MAGIC_FLIPPED {
return Err(CacheErrorKind::WrongEndianness.into());
}
if header.magic != PRGCACHE_MAGIC {
return Err(CacheErrorKind::WrongFormat.into());
}
if header.version != PRGCACHE_VERSION {
return Err(CacheErrorKind::WrongVersion.into());
}
let (_, rest) = watto::align_to(rest, 8).ok_or(CacheErrorKind::InvalidClasses)?;
let (classes, rest) = Class::slice_from_prefix(rest, header.num_classes as usize)
.ok_or(CacheErrorKind::InvalidClasses)?;
let (_, rest) = watto::align_to(rest, 8).ok_or(CacheErrorKind::InvalidMembers)?;
let (members, rest) = Member::slice_from_prefix(rest, header.num_members as usize)
.ok_or(CacheErrorKind::InvalidMembers)?;
let (_, rest) = watto::align_to(rest, 8).ok_or(CacheErrorKind::InvalidMembers)?;
let (members_by_params, rest) =
Member::slice_from_prefix(rest, header.num_members_by_params as usize)
.ok_or(CacheErrorKind::InvalidMembers)?;
let (_, rest) = watto::align_to(rest, 8).ok_or(CacheErrorKind::InvalidMembers)?;
let (outline_pairs, rest) =
OutlinePair::slice_from_prefix(rest, header.num_outline_pairs as usize)
.ok_or(CacheErrorKind::InvalidMembers)?;
let (_, rest) = watto::align_to(rest, 8).ok_or(CacheErrorKind::InvalidMembers)?;
let (rewrite_rule_entries, rest) =
RewriteRuleEntry::slice_from_prefix(rest, header.num_rewrite_rule_entries as usize)
.ok_or(CacheErrorKind::InvalidMembers)?;
let (_, rest) = watto::align_to(rest, 8).ok_or(CacheErrorKind::InvalidMembers)?;
let (rewrite_rule_components, rest) =
RewriteComponent::slice_from_prefix(rest, header.num_rewrite_rule_components as usize)
.ok_or(CacheErrorKind::InvalidMembers)?;
let (_, string_bytes) =
watto::align_to(rest, 8).ok_or(CacheErrorKind::UnexpectedStringBytes {
expected: header.string_bytes as usize,
found: 0,
})?;
if string_bytes.len() < header.string_bytes as usize {
return Err(CacheErrorKind::UnexpectedStringBytes {
expected: header.string_bytes as usize,
found: string_bytes.len(),
}
.into());
}
Ok(Self {
header,
classes,
members,
members_by_params,
outline_pairs,
rewrite_rule_entries,
rewrite_rule_components,
string_bytes,
})
}
pub fn write<W: Write>(mapping: &ProguardMapping, writer: &mut W) -> std::io::Result<()> {
let mut string_table = StringTable::new();
let parsed = ParsedProguardMapping::parse(*mapping, true);
let mut classes: BTreeMap<&str, ClassInProgress> = parsed
.class_names
.iter()
.map(|(obfuscated, original)| {
let obfuscated_name_offset = string_table.insert(obfuscated.as_str()) as u32;
let original_name_offset = string_table.insert(original.as_str()) as u32;
let class_info = parsed.class_infos.get(original);
let is_synthesized = class_info.map(|ci| ci.is_synthesized).unwrap_or_default();
let file_name_offset = class_info
.and_then(|ci| ci.source_file)
.map_or(u32::MAX, |s| string_table.insert(s) as u32);
let class = ClassInProgress {
class: Class {
original_name_offset,
obfuscated_name_offset,
file_name_offset,
is_synthesized: is_synthesized as u8,
..Default::default()
},
..Default::default()
};
(obfuscated.as_str(), class)
})
.collect();
for ((obfuscated_class, obfuscated_method), members) in &parsed.members {
let current_class = classes.entry(obfuscated_class.as_str()).or_default();
let obfuscated_method_offset = string_table.insert(obfuscated_method.as_str()) as u32;
let method_mappings = current_class
.members
.entry(obfuscated_method.as_str())
.or_default();
for member in members.all.iter() {
method_mappings.push(Self::resolve_mapping(
&mut string_table,
&parsed,
obfuscated_method_offset,
member,
));
current_class.class.members_len += 1;
}
for (args, param_members) in members.by_params.iter() {
let param_mappings = current_class
.members_by_params
.entry((obfuscated_method.as_str(), args))
.or_default();
for member in param_members.iter() {
param_mappings.push(Self::resolve_mapping(
&mut string_table,
&parsed,
obfuscated_method_offset,
member,
));
current_class.class.members_by_params_len += 1;
}
}
}
let string_bytes = string_table.into_bytes();
let num_members = classes.values().map(|c| c.class.members_len).sum::<u32>();
let num_members_by_params = classes
.values()
.map(|c| c.class.members_by_params_len)
.sum::<u32>();
let mut out_classes: Vec<Class> = Vec::with_capacity(classes.len());
let mut members: Vec<Member> = Vec::with_capacity(num_members as usize);
let mut members_by_params: Vec<Member> = Vec::with_capacity(num_members_by_params as usize);
let mut outline_pairs: Vec<OutlinePair> = Vec::new();
let mut rewrite_rule_entries: Vec<RewriteRuleEntry> = Vec::new();
let mut rewrite_rule_components: Vec<RewriteComponent> = Vec::new();
for mut c in classes.into_values() {
c.class.members_offset = members.len() as u32;
c.class.members_by_params_offset = members_by_params.len() as u32;
for (_method, ms) in c.members {
for mut mp in ms {
let start = outline_pairs.len() as u32;
if !mp.outline_pairs.is_empty() {
mp.member.outline_pairs_offset = start;
mp.member.outline_pairs_len = mp.outline_pairs.len() as u32;
outline_pairs.extend(mp.outline_pairs);
} else {
mp.member.outline_pairs_offset = start;
mp.member.outline_pairs_len = 0;
}
let rule_start = rewrite_rule_entries.len() as u32;
let mut rule_count = 0;
for rule in mp.rewrite_rules {
let cond_start = rewrite_rule_components.len() as u32;
rewrite_rule_components.extend(rule.conditions);
let cond_len = rewrite_rule_components.len() as u32 - cond_start;
let action_start = rewrite_rule_components.len() as u32;
rewrite_rule_components.extend(rule.actions);
let action_len = rewrite_rule_components.len() as u32 - action_start;
rewrite_rule_entries.push(RewriteRuleEntry {
conditions_offset: cond_start,
conditions_len: cond_len,
actions_offset: action_start,
actions_len: action_len,
});
rule_count += 1;
}
mp.member.rewrite_rules_offset = rule_start;
mp.member.rewrite_rules_len = rule_count;
members.push(mp.member);
}
}
for (_key, ms) in c.members_by_params {
for mut mp in ms {
let start = outline_pairs.len() as u32;
if !mp.outline_pairs.is_empty() {
mp.member.outline_pairs_offset = start;
mp.member.outline_pairs_len = mp.outline_pairs.len() as u32;
outline_pairs.extend(mp.outline_pairs);
} else {
mp.member.outline_pairs_offset = start;
mp.member.outline_pairs_len = 0;
}
let rule_start = rewrite_rule_entries.len() as u32;
let mut rule_count = 0;
for rule in mp.rewrite_rules {
let cond_start = rewrite_rule_components.len() as u32;
rewrite_rule_components.extend(rule.conditions);
let cond_len = rewrite_rule_components.len() as u32 - cond_start;
let action_start = rewrite_rule_components.len() as u32;
rewrite_rule_components.extend(rule.actions);
let action_len = rewrite_rule_components.len() as u32 - action_start;
rewrite_rule_entries.push(RewriteRuleEntry {
conditions_offset: cond_start,
conditions_len: cond_len,
actions_offset: action_start,
actions_len: action_len,
});
rule_count += 1;
}
mp.member.rewrite_rules_offset = rule_start;
mp.member.rewrite_rules_len = rule_count;
members_by_params.push(mp.member);
}
}
out_classes.push(c.class);
}
let num_outline_pairs = outline_pairs.len() as u32;
let num_rewrite_rule_entries = rewrite_rule_entries.len() as u32;
let num_rewrite_rule_components = rewrite_rule_components.len() as u32;
let header = Header {
magic: PRGCACHE_MAGIC,
version: PRGCACHE_VERSION,
num_classes: out_classes.len() as u32,
num_members,
num_members_by_params,
num_outline_pairs,
num_rewrite_rule_entries,
num_rewrite_rule_components,
string_bytes: string_bytes.len() as u32,
};
let mut writer = watto::Writer::new(writer);
writer.write_all(header.as_bytes())?;
writer.align_to(8)?;
for c in out_classes.iter() {
writer.write_all(c.as_bytes())?;
}
writer.align_to(8)?;
writer.write_all(members.as_bytes())?;
writer.align_to(8)?;
writer.write_all(members_by_params.as_bytes())?;
writer.align_to(8)?;
writer.write_all(outline_pairs.as_bytes())?;
writer.align_to(8)?;
writer.write_all(rewrite_rule_entries.as_bytes())?;
writer.align_to(8)?;
writer.write_all(rewrite_rule_components.as_bytes())?;
writer.align_to(8)?;
writer.write_all(&string_bytes)?;
Ok(())
}
fn resolve_mapping(
string_table: &mut StringTable,
parsed: &ParsedProguardMapping<'_>,
obfuscated_name_offset: u32,
member: &builder::Member,
) -> MemberInProgress {
let original_file = parsed
.class_infos
.get(&member.method.receiver.name())
.and_then(|class| class.source_file);
let original_file_offset =
original_file.map_or(u32::MAX, |s| string_table.insert(s) as u32);
let original_name_offset = string_table.insert(member.method.name.as_str()) as u32;
let original_class_offset = match member.method.receiver {
builder::MethodReceiver::ThisClass(_) => u32::MAX,
builder::MethodReceiver::OtherClass(name) => string_table.insert(name.as_str()) as u32,
};
let params_offset = string_table.insert(member.method.arguments) as u32;
let method_info = parsed
.method_infos
.get(&member.method)
.copied()
.unwrap_or_default();
let class_synthesized = parsed
.class_infos
.get(&member.method.receiver.name())
.is_some_and(|ci| ci.is_synthesized);
let is_synthesized = (method_info.is_synthesized || class_synthesized) as u8;
let is_outline = method_info.is_outline as u8;
let outline_pairs: Vec<OutlinePair> = member
.outline_callsite_positions
.as_ref()
.map(|m| {
m.iter()
.map(|(k, v)| OutlinePair {
outline_pos: *k as u32,
callsite_line: *v as u32,
})
.collect()
})
.unwrap_or_default();
let rewrite_rules = member
.rewrite_rules
.iter()
.map(|rule| {
let mut conditions = Vec::new();
for condition in &rule.conditions {
match condition {
builder::RewriteCondition::Throws(descriptor) => {
let offset = string_table.insert(descriptor) as u32;
conditions.push(RewriteComponent {
kind: REWRITE_CONDITION_THROWS,
value: offset,
});
}
builder::RewriteCondition::Unknown(value) => {
let offset = string_table.insert(value) as u32;
conditions.push(RewriteComponent {
kind: REWRITE_CONDITION_UNKNOWN,
value: offset,
});
}
}
}
let mut actions = Vec::new();
for action in &rule.actions {
match action {
builder::RewriteAction::RemoveInnerFrames(count) => {
actions.push(RewriteComponent {
kind: REWRITE_ACTION_REMOVE_INNER_FRAMES,
value: *count as u32,
});
}
builder::RewriteAction::Unknown(value) => {
let offset = string_table.insert(value) as u32;
actions.push(RewriteComponent {
kind: REWRITE_ACTION_UNKNOWN,
value: offset,
});
}
}
}
RewriteRuleInProgress {
conditions,
actions,
}
})
.collect();
let member: Member = Member {
startline: member.startline.map_or(NONE_VALUE, |v| v as u32),
endline: member.endline.map_or(NONE_VALUE, |v| v as u32),
original_class_offset,
original_file_offset,
original_name_offset,
original_startline: member.original_startline.map_or(NONE_VALUE, |v| v as u32),
original_endline: member.original_endline.map_or(NONE_VALUE, |l| l as u32),
obfuscated_name_offset,
params_offset,
is_synthesized,
is_outline,
_reserved: [0; 2],
outline_pairs_offset: 0,
outline_pairs_len: 0,
rewrite_rules_offset: 0,
rewrite_rules_len: 0,
};
MemberInProgress {
member,
outline_pairs,
rewrite_rules,
}
}
pub fn test(&self) {
let mut prev_end = 0;
for class in self.classes {
assert!(self.read_string(class.obfuscated_name_offset).is_ok());
assert!(self.read_string(class.original_name_offset).is_ok());
assert!(class.is_synthesized == 0 || class.is_synthesized == 1);
if class.file_name_offset != u32::MAX {
assert!(self.read_string(class.file_name_offset).is_ok());
}
assert_eq!(class.members_offset, prev_end);
prev_end += class.members_len;
assert!(prev_end as usize <= self.members.len());
let Some(members) = self.get_class_members(class) else {
continue;
};
for member in members {
assert!(self.read_string(member.obfuscated_name_offset).is_ok());
assert!(self.read_string(member.original_name_offset).is_ok());
assert!(member.is_synthesized == 0 || member.is_synthesized == 1);
assert!(member.is_outline == 0 || member.is_outline == 1);
let start = member.outline_pairs_offset as usize;
let len = member.outline_pairs_len as usize;
let end = start.saturating_add(len);
assert!(end <= self.outline_pairs.len());
let rule_start = member.rewrite_rules_offset as usize;
let rule_len = member.rewrite_rules_len as usize;
let rule_end = rule_start.saturating_add(rule_len);
assert!(rule_end <= self.rewrite_rule_entries.len());
for entry in &self.rewrite_rule_entries[rule_start..rule_end] {
let cond_start = entry.conditions_offset as usize;
let cond_len = entry.conditions_len as usize;
let cond_end = cond_start.saturating_add(cond_len);
assert!(cond_end <= self.rewrite_rule_components.len());
let action_start = entry.actions_offset as usize;
let action_len = entry.actions_len as usize;
let action_end = action_start.saturating_add(action_len);
assert!(action_end <= self.rewrite_rule_components.len());
}
if member.params_offset != u32::MAX {
assert!(self.read_string(member.params_offset).is_ok());
}
if member.original_class_offset != u32::MAX {
assert!(self.read_string(member.original_class_offset).is_ok());
}
if member.original_file_offset != u32::MAX {
assert!(self.read_string(member.original_file_offset).is_ok());
}
}
}
}
pub(crate) fn read_string(&self, offset: u32) -> Result<&'data str, watto::ReadStringError> {
StringTable::read(self.string_bytes, offset as usize)
}
}
#[derive(Debug, Clone, Default)]
struct ClassInProgress<'data> {
class: Class,
members: BTreeMap<&'data str, Vec<MemberInProgress>>,
members_by_params: BTreeMap<(&'data str, &'data str), Vec<MemberInProgress>>,
}
#[derive(Debug, Clone, Default)]
struct MemberInProgress {
member: Member,
outline_pairs: Vec<OutlinePair>,
rewrite_rules: Vec<RewriteRuleInProgress>,
}
#[derive(Debug, Clone, Default)]
struct RewriteRuleInProgress {
conditions: Vec<RewriteComponent>,
actions: Vec<RewriteComponent>,
}