1use super::mappings::{MappingError, NameMappings, NameResolver, is_class_descriptor};
13use std::collections::HashMap;
14use std::collections::hash_map::Entry;
15use std::fmt;
16use std::io::{self, Read};
17use std::path::{Path, PathBuf};
18
19#[derive(Debug, thiserror::Error)]
21#[non_exhaustive]
22pub enum ProguardError {
23 #[error("reading mapping file: {0}")]
25 Io(#[from] io::Error),
26 #[error("reading mapping file {}: {source}", path.display())]
28 File {
29 path: PathBuf,
31 source: io::Error,
33 },
34 #[error("mapping file line {line}: {reason}")]
36 Syntax {
37 line: usize,
39 reason: &'static str,
41 },
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
46#[non_exhaustive]
47pub struct ProguardWarning {
48 pub line: usize,
50 pub kind: WarningKind,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
56#[non_exhaustive]
57pub enum WarningKind {
58 #[error("{key} mapped again to {ignored}; keeping {kept}")]
60 Conflict {
61 key: String,
63 kept: String,
65 ignored: String,
67 },
68 #[error("original class {original} mapped again from {ignored}; keeping {kept}")]
71 DuplicateOriginal {
72 original: String,
74 kept: String,
76 ignored: String,
78 },
79 #[error(transparent)]
81 Rejected(#[from] MappingError),
82}
83
84impl fmt::Display for ProguardWarning {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 write!(f, "mapping file line {}: {}", self.line, self.kind)
87 }
88}
89
90#[derive(Debug, Clone, Default, PartialEq, Eq)]
92#[non_exhaustive]
93pub struct ProguardMapping {
94 pub mappings: NameMappings,
96 pub warnings: Vec<ProguardWarning>,
98}
99
100const REMOVED_CLASS: &str = "R8$$REMOVED$$CLASS$$";
102const RESIDUAL_SIGNATURE: &str = "com.android.tools.r8.residualsignature";
103
104impl NameMappings {
105 pub fn from_proguard(text: &str) -> Result<ProguardMapping, ProguardError> {
115 let text = text.strip_prefix('\u{feff}').unwrap_or(text);
116 let mut parser = Parser::default();
117 let classes = parser.read_classes(text)?;
118 parser.read_members(text, &classes)?;
119 Ok(ProguardMapping { mappings: parser.mappings, warnings: parser.warnings })
120 }
121
122 pub fn from_proguard_reader(mut reader: impl Read) -> Result<ProguardMapping, ProguardError> {
129 let mut text = String::new();
130 reader.read_to_string(&mut text)?;
131 NameMappings::from_proguard(&text)
132 }
133
134 pub fn from_proguard_file(path: impl AsRef<Path>) -> Result<ProguardMapping, ProguardError> {
141 let path = path.as_ref();
142 let text = std::fs::read_to_string(path).map_err(|source| ProguardError::File { path: path.to_path_buf(), source })?;
143 NameMappings::from_proguard(&text)
144 }
145}
146
147fn lines(text: &str) -> impl Iterator<Item = (usize, &str)> {
149 text.lines().enumerate().map(|(i, l)| (i + 1, l.trim_end()))
150}
151
152fn is_class_line(line: &str) -> bool {
154 !line.is_empty() && !line.starts_with(|c: char| c.is_whitespace() || c == '#')
155}
156
157fn parse_class_line(line: &str) -> Option<(&str, &str)> {
159 let (original, obfuscated) = line.strip_suffix(':')?.split_once("->")?;
160 let (original, obfuscated) = (original.trim(), obfuscated.trim());
161 let word = |s: &str| !s.is_empty() && !s.contains(char::is_whitespace);
162 (word(original) && word(obfuscated)).then_some((original, obfuscated))
163}
164
165fn descriptor(binary_name: &str) -> String {
167 let mut out = String::with_capacity(binary_name.len() + 2);
168 push_descriptor(&mut out, binary_name);
169 out
170}
171
172fn push_descriptor(out: &mut String, binary_name: &str) {
173 out.push('L');
174 out.extend(binary_name.chars().map(|c| if c == '.' { '/' } else { c }));
175 out.push(';');
176}
177
178struct Class<'a> {
180 original: &'a str,
181 descriptor: String,
183}
184
185#[derive(Default)]
186struct Parser<'a> {
187 mappings: NameMappings,
188 warnings: Vec<ProguardWarning>,
189 obfuscated: HashMap<&'a str, &'a str>,
191}
192
193#[derive(Clone, Copy)]
194enum Section {
195 Class,
196 Field,
197 Method,
198}
199
200impl<'a> Parser<'a> {
201 fn read_classes(&mut self, text: &'a str) -> Result<Vec<Option<Class<'a>>>, ProguardError> {
204 let mut classes = Vec::new();
205 for (line, l) in lines(text).filter(|(_, l)| is_class_line(l)) {
206 let (original, obfuscated) =
207 parse_class_line(l).ok_or(ProguardError::Syntax { line, reason: "expected `original -> obfuscated:`" })?;
208 classes.push(self.class(line, original, obfuscated));
209 }
210 Ok(classes)
211 }
212
213 fn class(&mut self, line: usize, original: &'a str, obfuscated: &'a str) -> Option<Class<'a>> {
214 if obfuscated.starts_with(REMOVED_CLASS) {
215 return None;
216 }
217 let dex_descriptor = descriptor(obfuscated);
218 if !is_class_descriptor(&dex_descriptor) {
219 self.warn(line, MappingError::Class(dex_descriptor).into());
220 return None;
221 }
222 match self.obfuscated.entry(original) {
223 Entry::Vacant(e) => {
224 e.insert(obfuscated);
225 }
226 Entry::Occupied(e) if *e.get() != obfuscated => {
227 let kind = WarningKind::DuplicateOriginal {
228 original: original.to_string(),
229 kept: e.get().to_string(),
230 ignored: obfuscated.to_string(),
231 };
232 self.warn(line, kind);
233 }
234 Entry::Occupied(_) => {}
235 }
236 if original != obfuscated {
237 self.insert(line, Section::Class, &dex_descriptor, &descriptor(original));
238 }
239 Some(Class { original, descriptor: dex_descriptor })
240 }
241
242 fn read_members(&mut self, text: &'a str, classes: &[Option<Class<'a>>]) -> Result<(), ProguardError> {
244 let mut classes = classes.iter();
245 let mut class: Option<&Class<'a>> = None;
246 let mut seen_class = false;
247 let mut pending: Option<Pending<'a>> = None;
248 let mut previous: Option<Previous<'a>> = None;
249 for (line, l) in lines(text) {
250 let body = l.trim_start();
251 if body.is_empty() {
252 continue;
253 }
254 if let Some(comment) = body.strip_prefix('#') {
255 if let (Some(p), Some(residual)) = (pending.as_mut(), residual_signature(comment)) {
256 p.residual = Some(residual);
257 }
258 continue;
259 }
260 if is_class_line(l) {
261 self.flush(class, pending.take(), &mut previous);
262 previous = None;
263 class = classes.next().and_then(Option::as_ref);
264 seen_class = true;
265 continue;
266 }
267 if !seen_class {
268 return Err(ProguardError::Syntax { line, reason: "member line before any class line" });
269 }
270 let member = Member::parse(body).ok_or(ProguardError::Syntax { line, reason: "expected a field or method mapping" })?;
271 match (pending.as_mut(), member) {
272 (Some(p), Member::Method(m)) if p.member.is_inlined_into(&m) => {
273 p.member = Member::Method(m);
274 p.line = line;
275 }
276 (_, member) => {
277 self.flush(class, pending.take(), &mut previous);
278 pending = Some(Pending { member, line, residual: None });
279 }
280 }
281 }
282 self.flush(class, pending, &mut previous);
283 Ok(())
284 }
285
286 fn flush(&mut self, class: Option<&Class<'a>>, pending: Option<Pending<'a>>, previous: &mut Option<Previous<'a>>) {
288 let (Some(class), Some(Pending { member, line, residual })) = (class, pending) else { return };
289 match member {
290 Member::Field(f) => {
291 let Some(name) = own_name(class, f.name) else { return };
292 if name == f.obfuscated {
293 return;
294 }
295 let mut key = format!("{}->{}:", class.descriptor, f.obfuscated);
296 match &residual {
297 Some(ty) => key.push_str(ty),
298 None => self.push_type(&mut key, f.ty),
299 }
300 self.insert(line, Section::Field, &key, name);
301 }
302 Member::Method(m) => {
303 let residual = residual.or_else(|| previous.take().filter(|p| p.method.same_method(&m)).and_then(|p| p.residual));
306 *previous = Some(Previous { method: m, residual });
307 let residual = previous.as_ref().and_then(|p| p.residual.as_deref());
308 let Some(name) = own_name(class, m.name) else { return };
309 if name == m.obfuscated || m.obfuscated.starts_with('<') {
310 return;
311 }
312 let mut key = format!("{}->{}", class.descriptor, m.obfuscated);
313 match residual {
314 Some(proto) => key.push_str(proto),
315 None => {
316 key.push('(');
317 m.args.split(',').filter(|a| !a.trim().is_empty()).for_each(|a| self.push_type(&mut key, a));
318 key.push(')');
319 self.push_type(&mut key, m.ret);
320 }
321 }
322 self.insert(line, Section::Method, &key, name);
323 }
324 }
325 }
326
327 fn push_type(&self, out: &mut String, java: &str) {
330 let mut base = java.trim();
331 while let Some(element) = base.strip_suffix("[]") {
332 out.push('[');
333 base = element.trim_end();
334 }
335 let primitive = match base {
336 "void" => 'V',
337 "boolean" => 'Z',
338 "byte" => 'B',
339 "char" => 'C',
340 "short" => 'S',
341 "int" => 'I',
342 "long" => 'J',
343 "float" => 'F',
344 "double" => 'D',
345 _ => return push_descriptor(out, self.obfuscated.get(base).copied().unwrap_or(base)),
346 };
347 out.push(primitive);
348 }
349
350 fn insert(&mut self, line: usize, section: Section, key: &str, name: &str) {
353 let existing = match section {
354 Section::Class => self.mappings.class(key),
355 Section::Field => self.mappings.field(key),
356 Section::Method => self.mappings.method(key),
357 };
358 if let Some(kept) = existing {
359 if kept != name {
360 let kind = WarningKind::Conflict { key: key.to_string(), kept: kept.to_string(), ignored: name.to_string() };
361 self.warn(line, kind);
362 }
363 return;
364 }
365 let inserted = match section {
366 Section::Class => self.mappings.insert_class(key, name),
367 Section::Field => self.mappings.insert_field(key, name),
368 Section::Method => self.mappings.insert_method(key, name),
369 };
370 if let Err(e) = inserted {
371 self.warn(line, e.into());
372 }
373 }
374
375 fn warn(&mut self, line: usize, kind: WarningKind) {
376 self.warnings.push(ProguardWarning { line, kind });
377 }
378}
379
380fn own_name<'a>(class: &Class<'_>, name: &'a str) -> Option<&'a str> {
385 match name.rsplit_once('.') {
386 None => Some(name),
387 Some((owner, simple)) => (owner == class.original).then_some(simple),
388 }
389}
390
391fn residual_signature(comment: &str) -> Option<String> {
394 let json = comment.trim_start();
395 if !json.starts_with('{') || !json.contains(RESIDUAL_SIGNATURE) {
396 return None;
397 }
398 let value: serde_json::Value = serde_json::from_str(json).ok()?;
399 if value.get("id")?.as_str()? != RESIDUAL_SIGNATURE {
400 return None;
401 }
402 value.get("signature")?.as_str().map(str::to_string)
403}
404
405struct Pending<'a> {
407 member: Member<'a>,
408 line: usize,
410 residual: Option<String>,
411}
412
413struct Previous<'a> {
416 method: MethodLine<'a>,
417 residual: Option<String>,
418}
419
420enum Member<'a> {
421 Field(FieldLine<'a>),
422 Method(MethodLine<'a>),
423}
424
425struct FieldLine<'a> {
427 ty: &'a str,
428 name: &'a str,
429 obfuscated: &'a str,
430}
431
432#[derive(Clone, Copy)]
434struct MethodLine<'a> {
435 range: &'a str,
437 ret: &'a str,
438 name: &'a str,
440 args: &'a str,
441 call_site: bool,
444 obfuscated: &'a str,
445}
446
447impl<'a> Member<'a> {
448 fn parse(body: &'a str) -> Option<Member<'a>> {
449 let (lhs, obfuscated) = body.rsplit_once("->")?;
450 let (lhs, obfuscated) = (lhs.trim_end(), obfuscated.trim());
451 if obfuscated.is_empty() || obfuscated.contains(char::is_whitespace) {
452 return None;
453 }
454 let digits = lhs.find(|c: char| !(c.is_ascii_digit() || c == ':')).unwrap_or(lhs.len());
455 let (range, rest) = match lhs.split_at(digits) {
456 ("", rest) => ("", rest),
457 (prefix, rest) => (prefix.strip_suffix(':')?, rest),
458 };
459 let (ty, rest) = rest.split_once(' ')?;
460 let rest = rest.trim_start();
461 if ty.is_empty() || rest.is_empty() {
462 return None;
463 }
464 let Some((name, rest)) = rest.split_once('(') else {
465 return (range.is_empty() && !rest.contains(char::is_whitespace)).then_some(Member::Field(FieldLine {
466 ty,
467 name: rest,
468 obfuscated,
469 }));
470 };
471 let (args, position) = rest.split_once(')')?;
472 let numbers = match position {
473 "" => 0,
474 p => {
475 let p = p.strip_prefix(':')?;
476 if !p.split(':').all(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit())) {
477 return None;
478 }
479 p.split(':').count()
480 }
481 };
482 if name.is_empty() || numbers > 2 {
483 return None;
484 }
485 Some(Member::Method(MethodLine { range, ret: ty, name, args, call_site: numbers == 1, obfuscated }))
486 }
487
488 fn is_inlined_into(&self, next: &MethodLine<'_>) -> bool {
491 match self {
492 Member::Method(m) => !m.range.is_empty() && m.range == next.range && m.obfuscated == next.obfuscated && next.call_site,
493 Member::Field(_) => false,
494 }
495 }
496}
497
498impl MethodLine<'_> {
499 fn same_method(&self, other: &MethodLine<'_>) -> bool {
501 (self.obfuscated, self.ret, self.name, self.args) == (other.obfuscated, other.ret, other.name, other.args)
502 }
503}
504
505#[cfg(test)]
506mod tests;