Skip to content

Commit 309d4c7

Browse files
authored
ref: Robustly parse R8 headers (#50)
R8 headers have a different format from "normal" ProGuard headers. This splits the parsing of the two header formats apart and uses `serde` for R8 headers (they are specified to be in JSON format). See https://r8.googlesource.com/r8/+/refs/heads/main/doc/retrace.md#additional-information-appended-as-comments-to-the-file for R8 headers.
1 parent 8a1f717 commit 309d4c7

File tree

6 files changed

+104
-53
lines changed

6 files changed

+104
-53
lines changed

Cargo.lock

Lines changed: 11 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ edition = "2021"
1414
uuid = ["dep:uuid"]
1515

1616
[dependencies]
17+
serde = { version = "1.0.219", features = ["derive"] }
18+
serde_json = "1.0.140"
1719
thiserror = "1.0.61"
1820
uuid = { version = "1.0.0", features = ["v5"], optional = true }
1921
watto = { version = "0.1.0", features = ["writer", "strings"] }

src/cache/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ pub struct CacheError {
9292
}
9393

9494
impl CacheError {
95-
/// Returns the corresponding [`ErrorKind`] for this error.
95+
/// Returns the corresponding [`CacheErrorKind`] for this error.
9696
pub fn kind(&self) -> CacheErrorKind {
9797
self.kind
9898
}

src/cache/raw.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::io::Write;
33

44
use watto::{Pod, StringTable};
55

6+
use crate::mapping::R8Header;
67
use crate::{ProguardMapping, ProguardRecord};
78

89
use super::{CacheError, CacheErrorKind};
@@ -194,15 +195,11 @@ impl<'data> ProguardCache<'data> {
194195
let mut records = mapping.iter().filter_map(Result::ok).peekable();
195196
while let Some(record) = records.next() {
196197
match record {
197-
ProguardRecord::Header {
198-
key,
199-
value: Some(file_name),
200-
} => {
201-
if key == "sourceFile" {
202-
current_class.class.file_name_offset =
203-
string_table.insert(file_name) as u32;
204-
}
198+
ProguardRecord::R8Header(R8Header::SourceFile { file_name }) => {
199+
current_class.class.file_name_offset = string_table.insert(file_name) as u32;
205200
}
201+
ProguardRecord::Header { .. } => {}
202+
ProguardRecord::R8Header(R8Header::Other) => {}
206203
ProguardRecord::Class {
207204
original,
208205
obfuscated,

src/mapper.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::fmt::{Error as FmtError, Write};
55
use std::iter::FusedIterator;
66

77
use crate::java;
8+
use crate::mapping::R8Header;
89
use crate::mapping::{ProguardMapping, ProguardRecord};
910
use crate::stacktrace::{self, StackFrame, StackTrace, Throwable};
1011

@@ -236,11 +237,11 @@ impl<'s> ProguardMapper<'s> {
236237
let mut records = mapping.iter().filter_map(Result::ok).peekable();
237238
while let Some(record) = records.next() {
238239
match record {
239-
ProguardRecord::Header { key, value } => {
240-
if key == "sourceFile" {
241-
class.file_name = value;
242-
}
240+
ProguardRecord::R8Header(R8Header::SourceFile { file_name }) => {
241+
class.file_name = Some(file_name);
243242
}
243+
ProguardRecord::Header { .. } => {}
244+
ProguardRecord::R8Header(R8Header::Other) => {}
244245
ProguardRecord::Class {
245246
original,
246247
obfuscated,

src/mapping.rs

Lines changed: 80 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use std::fmt;
77
use std::ops::Range;
88
use std::str;
99

10+
use serde::Deserialize;
11+
1012
#[cfg(feature = "uuid")]
1113
use uuid::Uuid;
1214

@@ -283,7 +285,7 @@ impl<'s> Iterator for ProguardRecordIter<'s> {
283285
/// Maps start/end lines of a minified file to original start/end lines.
284286
///
285287
/// All line mappings are 1-based and inclusive.
286-
#[derive(Clone, Copy, Debug, PartialEq)]
288+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
287289
pub struct LineMapping {
288290
/// Start Line, 1-based.
289291
pub startline: usize,
@@ -295,8 +297,26 @@ pub struct LineMapping {
295297
pub original_endline: Option<usize>,
296298
}
297299

300+
/// An R8 header, as described in
301+
/// <https://r8.googlesource.com/r8/+/refs/heads/main/doc/retrace.md#additional-information-appended-as-comments-to-the-file>.
302+
///
303+
/// The format is a line starting with `#` and followed by a JSON object.
304+
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
305+
#[serde(tag = "id", rename_all = "camelCase")]
306+
pub enum R8Header<'s> {
307+
/// A source file header, stating what source file a class originated from.
308+
///
309+
/// See <https://r8.googlesource.com/r8/+/refs/heads/main/doc/retrace.md#source-file>.
310+
#[serde(rename_all = "camelCase")]
311+
SourceFile { file_name: &'s str },
312+
313+
/// Catchall variant for headers we don't support.
314+
#[serde(other)]
315+
Other,
316+
}
317+
298318
/// A Proguard Mapping Record.
299-
#[derive(Clone, Debug, PartialEq)]
319+
#[derive(Clone, Debug, PartialEq, Eq)]
300320
pub enum ProguardRecord<'s> {
301321
/// A Proguard Header.
302322
Header {
@@ -305,6 +325,8 @@ pub enum ProguardRecord<'s> {
305325
/// Optional value if the Header is a KV pair.
306326
value: Option<&'s str>,
307327
},
328+
/// An R8 Header.
329+
R8Header(R8Header<'s>),
308330
/// A Class Mapping.
309331
Class {
310332
/// Original name of the class.
@@ -437,8 +459,15 @@ impl<'s> ProguardRecord<'s> {
437459
fn parse_proguard_record(bytes: &[u8]) -> (Result<ProguardRecord, ParseError>, &[u8]) {
438460
let bytes = consume_leading_newlines(bytes);
439461

440-
let result = if bytes.starts_with(b"#") {
441-
parse_proguard_header(bytes)
462+
let result = if let Some(bytes) = bytes.trim_ascii_start().strip_prefix(b"#") {
463+
// ProGuard / R8 headers
464+
465+
let bytes = bytes.trim_ascii_start();
466+
if bytes.starts_with(b"{") {
467+
parse_r8_header(bytes)
468+
} else {
469+
parse_proguard_header(bytes)
470+
}
442471
} else if bytes.starts_with(b" ") {
443472
parse_proguard_field_or_method(bytes)
444473
} else {
@@ -460,38 +489,36 @@ fn parse_proguard_record(bytes: &[u8]) -> (Result<ProguardRecord, ParseError>, &
460489
}
461490
}
462491

463-
const SOURCE_FILE_PREFIX: &[u8; 32] = br#" {"id":"sourceFile","fileName":""#;
464-
465492
/// Parses a single Proguard Header from a Proguard File.
466493
fn parse_proguard_header(bytes: &[u8]) -> Result<(ProguardRecord, &[u8]), ParseError> {
467-
let bytes = parse_prefix(bytes, b"#")?;
494+
// Note: the leading `#` has already been parsed.
468495

469-
if let Ok(bytes) = parse_prefix(bytes, SOURCE_FILE_PREFIX) {
470-
let (value, bytes) = parse_until(bytes, |c| *c == b'"')?;
471-
let bytes = parse_prefix(bytes, br#""}"#)?;
496+
// Existing logic for `key: value` format
497+
let (key, bytes) = parse_until(bytes, |c| *c == b':' || is_newline(c))?;
472498

473-
let record = ProguardRecord::Header {
474-
key: "sourceFile",
475-
value: Some(value),
476-
};
499+
let (value, bytes) = match parse_prefix(bytes, b":") {
500+
Ok(bytes) => parse_until(bytes, is_newline).map(|(v, bytes)| (Some(v), bytes)),
501+
Err(_) => Ok((None, bytes)),
502+
}?;
477503

478-
Ok((record, consume_leading_newlines(bytes)))
479-
} else {
480-
// Existing logic for `key: value` format
481-
let (key, bytes) = parse_until(bytes, |c| *c == b':' || is_newline(c))?;
504+
let record = ProguardRecord::Header {
505+
key: key.trim(),
506+
value: value.map(|v| v.trim()),
507+
};
482508

483-
let (value, bytes) = match parse_prefix(bytes, b":") {
484-
Ok(bytes) => parse_until(bytes, is_newline).map(|(v, bytes)| (Some(v), bytes)),
485-
Err(_) => Ok((None, bytes)),
486-
}?;
509+
Ok((record, consume_leading_newlines(bytes)))
510+
}
487511

488-
let record = ProguardRecord::Header {
489-
key: key.trim(),
490-
value: value.map(|v| v.trim()),
491-
};
512+
fn parse_r8_header(bytes: &[u8]) -> Result<(ProguardRecord, &[u8]), ParseError> {
513+
// Note: the leading `#` has already been parsed.
492514

493-
Ok((record, consume_leading_newlines(bytes)))
494-
}
515+
let (header, rest) = parse_until(bytes, is_newline)?;
516+
517+
let header = serde_json::from_str(header).unwrap();
518+
Ok((
519+
ProguardRecord::R8Header(header),
520+
consume_leading_newlines(rest),
521+
))
495522
}
496523

497524
/// Parses a single Proguard Field or Method from a Proguard File.
@@ -764,10 +791,29 @@ mod tests {
764791
let parsed = ProguardRecord::try_parse(bytes);
765792
assert_eq!(
766793
parsed,
767-
Ok(ProguardRecord::Header {
768-
key: "sourceFile",
769-
value: Some("Foobar.kt")
770-
})
794+
Ok(ProguardRecord::R8Header(R8Header::SourceFile {
795+
file_name: "Foobar.kt",
796+
}))
797+
);
798+
}
799+
800+
#[test]
801+
fn try_parse_r8_headers() {
802+
let bytes = br#"# {"id":"foobar"}"#;
803+
assert_eq!(
804+
ProguardRecord::try_parse(bytes).unwrap(),
805+
ProguardRecord::R8Header(R8Header::Other),
806+
);
807+
808+
let bytes = br#" #{"id":"foobar"}"#;
809+
assert_eq!(
810+
ProguardRecord::try_parse(bytes).unwrap(),
811+
ProguardRecord::R8Header(R8Header::Other),
812+
);
813+
let bytes = br#"# {"id":"foobar"}"#;
814+
assert_eq!(
815+
ProguardRecord::try_parse(bytes).unwrap(),
816+
ProguardRecord::R8Header(R8Header::Other),
771817
);
772818
}
773819

@@ -1005,6 +1051,7 @@ androidx.activity.OnBackPressedCallback -> c.a.b:
10051051
boolean mEnabled -> a
10061052
java.util.ArrayDeque mOnBackPressedCallbacks -> b
10071053
1:4:void onBackPressed():184:187 -> c
1054+
# {\"id\":\"com.android.tools.r8.synthesized\"}
10081055
androidx.activity.OnBackPressedCallback
10091056
-> c.a.b:
10101057
";
@@ -1057,6 +1104,7 @@ androidx.activity.OnBackPressedCallback
10571104
original_endline: Some(187),
10581105
}),
10591106
}),
1107+
Ok(ProguardRecord::R8Header(R8Header::Other)),
10601108
Err(ParseError {
10611109
line: b"androidx.activity.OnBackPressedCallback \n",
10621110
kind: ParseErrorKind::ParseError("line is not a valid proguard record"),

0 commit comments

Comments
 (0)