use std::fmt;
use std::ops::Range;
use std::str;
use serde::Deserialize;
#[cfg(feature = "uuid")]
use uuid::Uuid;
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ParseError<'s> {
line: &'s [u8],
kind: ParseErrorKind,
}
impl ParseError<'_> {
pub fn line(&self) -> &[u8] {
self.line
}
pub fn kind(&self) -> ParseErrorKind {
self.kind
}
}
impl fmt::Display for ParseError<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.kind {
ParseErrorKind::Utf8Error(e) => e.fmt(f),
ParseErrorKind::ParseError(d) => d.fmt(f),
}
}
}
impl std::error::Error for ParseError<'_> {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self.kind {
ParseErrorKind::Utf8Error(ref e) => Some(e),
ParseErrorKind::ParseError(_) => None,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum ParseErrorKind {
Utf8Error(str::Utf8Error),
ParseError(&'static str),
}
pub struct MappingSummary<'s> {
compiler: Option<&'s str>,
compiler_version: Option<&'s str>,
min_api: Option<u32>,
class_count: usize,
method_count: usize,
}
impl<'s> MappingSummary<'s> {
fn new(mapping: &'s ProguardMapping<'s>) -> MappingSummary<'s> {
let mut compiler = None;
let mut compiler_version = None;
let mut min_api = None;
let mut class_count = 0;
let mut method_count = 0;
for record in mapping.iter() {
match record {
Ok(ProguardRecord::Header { key, value }) => match key {
"compiler" => {
compiler = value;
}
"compiler_version" => {
compiler_version = value;
}
"min_api" => {
min_api = value.and_then(|x| x.parse().ok());
}
_ => {}
},
Ok(ProguardRecord::Class { .. }) => class_count += 1,
Ok(ProguardRecord::Method { .. }) => method_count += 1,
_ => {}
}
}
MappingSummary {
compiler,
compiler_version,
min_api,
class_count,
method_count,
}
}
pub fn compiler(&self) -> Option<&str> {
self.compiler
}
pub fn compiler_version(&self) -> Option<&str> {
self.compiler_version
}
pub fn min_api(&self) -> Option<u32> {
self.min_api
}
pub fn class_count(&self) -> usize {
self.class_count
}
pub fn method_count(&self) -> usize {
self.method_count
}
}
#[derive(Clone, Copy, Default)]
pub struct ProguardMapping<'s> {
source: &'s [u8],
}
impl fmt::Debug for ProguardMapping<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ProguardMapping").finish()
}
}
impl<'s> ProguardMapping<'s> {
pub fn new(source: &'s [u8]) -> Self {
Self { source }
}
pub fn is_valid(&self) -> bool {
let mut has_class_line = false;
for record in self.iter().take(50) {
match record {
Ok(ProguardRecord::Class { .. }) => {
has_class_line = true;
}
Ok(ProguardRecord::Field { .. }) | Ok(ProguardRecord::Method { .. })
if has_class_line =>
{
return true;
}
_ => {}
}
}
false
}
pub fn summary(&self) -> MappingSummary<'_> {
MappingSummary::new(self)
}
pub fn has_line_info(&self) -> bool {
#[allow(clippy::manual_flatten)]
for record in self.iter() {
if let Ok(ProguardRecord::Method { line_mapping, .. }) = record {
if line_mapping.is_some() {
return true;
}
}
}
false
}
#[cfg(feature = "uuid")]
pub fn uuid(&self) -> Uuid {
use std::sync::LazyLock;
static NAMESPACE: LazyLock<Uuid> =
LazyLock::new(|| Uuid::new_v5(&Uuid::NAMESPACE_DNS, b"guardsquare.com"));
Uuid::new_v5(&NAMESPACE, self.source)
}
pub fn iter(&self) -> ProguardRecordIter<'s> {
ProguardRecordIter { slice: self.source }
}
pub fn section(&self, range: Range<usize>) -> Self {
Self {
source: &self.source[range],
}
}
}
#[derive(Clone, Default)]
pub struct ProguardRecordIter<'s> {
slice: &'s [u8],
}
impl fmt::Debug for ProguardRecordIter<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ProguardRecordIter").finish()
}
}
impl<'s> Iterator for ProguardRecordIter<'s> {
type Item = Result<ProguardRecord<'s>, ParseError<'s>>;
fn next(&mut self) -> Option<Self::Item> {
if self.slice.is_empty() {
return None;
}
let (result, slice) = parse_proguard_record(self.slice);
self.slice = slice;
Some(result)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LineMapping {
pub startline: Option<usize>,
pub endline: Option<usize>,
pub original_startline: Option<usize>,
pub original_endline: Option<usize>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(tag = "id", rename_all = "camelCase")]
pub enum R8Header<'s> {
#[serde(rename_all = "camelCase")]
SourceFile { file_name: &'s str },
#[serde(rename = "com.android.tools.r8.synthesized")]
Synthesized,
#[serde(rename = "com.android.tools.r8.outline")]
Outline,
#[serde(rename_all = "camelCase")]
#[serde(rename = "com.android.tools.r8.outlineCallsite")]
OutlineCallsite {
positions: std::collections::HashMap<&'s str, usize>,
#[serde(default)]
outline: Option<&'s str>,
},
#[serde(rename_all = "camelCase")]
#[serde(rename = "com.android.tools.r8.rewriteFrame")]
RewriteFrame {
conditions: Vec<&'s str>,
actions: Vec<&'s str>,
},
#[serde(other)]
Other,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ProguardRecord<'s> {
Header {
key: &'s str,
value: Option<&'s str>,
},
R8Header(R8Header<'s>),
Class {
original: &'s str,
obfuscated: &'s str,
},
Field {
ty: &'s str,
original: &'s str,
obfuscated: &'s str,
},
Method {
ty: &'s str,
original: &'s str,
obfuscated: &'s str,
arguments: &'s str,
original_class: Option<&'s str>,
line_mapping: Option<LineMapping>,
},
}
impl<'s> ProguardRecord<'s> {
pub fn try_parse(line: &'s [u8]) -> Result<Self, ParseError<'s>> {
match parse_proguard_record(line) {
(Err(err), _) => Err(err),
(Ok(_), slice) if !slice.is_empty() => Err(ParseError {
line,
kind: ParseErrorKind::ParseError("line is not a valid proguard record"),
}),
(Ok(record), _) => Ok(record),
}
}
}
fn parse_proguard_record(bytes: &[u8]) -> (Result<ProguardRecord<'_>, ParseError<'_>>, &[u8]) {
let bytes = consume_leading_newlines(bytes);
let result = if let Some(bytes) = bytes.trim_ascii_start().strip_prefix(b"#") {
let bytes = bytes.trim_ascii_start();
if bytes.starts_with(b"{") {
parse_r8_header(bytes)
} else {
parse_proguard_header(bytes)
}
} else if matches!(bytes.first(), Some(b' ' | b'\t')) {
parse_proguard_field_or_method(bytes)
} else {
parse_proguard_class(bytes)
};
match result {
Ok((record, bytes)) => (Ok(record), bytes),
Err(_) => {
let (line, bytes) = split_line(bytes);
(
Err(ParseError {
line,
kind: ParseErrorKind::ParseError("line is not a valid proguard record"),
}),
bytes,
)
}
}
}
fn parse_proguard_header(bytes: &[u8]) -> Result<(ProguardRecord<'_>, &[u8]), ParseError<'_>> {
let (key, bytes) = parse_until(bytes, |c| *c == b':' || is_newline(c))?;
let (value, bytes) = match parse_prefix(bytes, b":") {
Ok(bytes) => parse_until(bytes, is_newline).map(|(v, bytes)| (Some(v), bytes)),
Err(_) => Ok((None, bytes)),
}?;
let record = ProguardRecord::Header {
key: key.trim(),
value: value.map(|v| v.trim()),
};
Ok((record, consume_leading_newlines(bytes)))
}
fn parse_r8_header(bytes: &[u8]) -> Result<(ProguardRecord<'_>, &[u8]), ParseError<'_>> {
let (header, rest) = parse_until(bytes, is_newline)?;
let header = serde_json::from_str(header).map_err(|_| ParseError {
line: bytes,
kind: ParseErrorKind::ParseError("invalid r8 header"),
})?;
Ok((
ProguardRecord::R8Header(header),
consume_leading_newlines(rest),
))
}
fn parse_proguard_field_or_method(
bytes: &[u8],
) -> Result<(ProguardRecord<'_>, &[u8]), ParseError<'_>> {
let bytes = bytes.trim_ascii_start();
let (startline, bytes) = match parse_usize(bytes) {
Ok((startline, bytes)) => (Some(startline), bytes),
Err(_) => (None, bytes),
};
let (endline, bytes) = match startline {
Some(_) => {
let bytes = parse_prefix(bytes, b":")?;
let (endline, bytes) = parse_usize(bytes)?;
let bytes = parse_prefix(bytes, b":")?;
(Some(endline), bytes)
}
None => (None, bytes),
};
let (ty, bytes) = parse_until_no_newline(bytes, |c| *c == b' ')?;
let bytes = parse_prefix(bytes, b" ")?;
let (original, bytes) = parse_until_no_newline(bytes, |c| *c == b' ' || *c == b'(')?;
let (arguments, bytes) = match parse_prefix(bytes, b"(") {
Ok(bytes) => {
let (arguments, bytes) = parse_until_no_newline(bytes, |c| *c == b')')?;
let bytes = parse_prefix(bytes, b")")?;
(Some(arguments), bytes)
}
Err(_) => (None, bytes),
};
let (original_startline, bytes) = match arguments {
Some(_) => match parse_prefix(bytes, b":") {
Ok(bytes) => {
let (original_startline, bytes) = parse_usize(bytes)?;
(Some(original_startline), bytes)
}
Err(_) => (None, bytes),
},
None => (None, bytes),
};
let (original_endline, bytes) = match original_startline {
Some(_) => match parse_prefix(bytes, b":") {
Ok(bytes) => {
let (original_endline, bytes) = parse_usize(bytes)?;
(Some(original_endline), bytes)
}
Err(_) => (None, bytes),
},
None => (None, bytes),
};
let bytes = parse_prefix(bytes, b" -> ")?;
let (obfuscated, bytes) = parse_until(bytes, is_newline)?;
let record = match arguments {
Some(arguments) => {
let mut split_class = original.rsplitn(2, '.');
let original = split_class.next().ok_or(ParseError {
line: bytes,
kind: ParseErrorKind::ParseError("line is not a valid proguard record"),
})?;
let original_class = split_class.next();
let line_mapping = match (startline, endline, original_startline) {
(Some(startline), Some(endline), _) => Some(LineMapping {
startline: Some(startline),
endline: Some(endline),
original_startline,
original_endline,
}),
(None, None, Some(original_startline)) => Some(LineMapping {
startline: None,
endline: None,
original_startline: Some(original_startline),
original_endline,
}),
_ => None,
};
ProguardRecord::Method {
ty,
original,
obfuscated,
arguments,
original_class,
line_mapping,
}
}
None => ProguardRecord::Field {
ty,
original,
obfuscated,
},
};
Ok((record, consume_leading_newlines(bytes)))
}
fn parse_proguard_class(bytes: &[u8]) -> Result<(ProguardRecord<'_>, &[u8]), ParseError<'_>> {
let (original, bytes) = parse_until_no_newline(bytes, |c| *c == b' ')?;
let bytes = parse_prefix(bytes, b" -> ")?;
let (obfuscated, bytes) = parse_until_no_newline(bytes, |c| *c == b':')?;
let bytes = parse_prefix(bytes, b":")?;
let record = ProguardRecord::Class {
original,
obfuscated,
};
Ok((record, consume_leading_newlines(bytes)))
}
fn parse_usize(bytes: &[u8]) -> Result<(usize, &[u8]), ParseError<'_>> {
let (slice, rest) = match bytes.iter().position(|c| !(*c as char).is_numeric()) {
Some(pos) => bytes.split_at(pos),
None => (bytes, &[] as &[u8]),
};
match std::str::from_utf8(slice) {
Ok(s) => match s.parse() {
Ok(value) => Ok((value, rest)),
Err(_) => Err(ParseError {
line: slice,
kind: ParseErrorKind::ParseError("line is not a valid proguard record"),
}),
},
Err(err) => Err(ParseError {
line: slice,
kind: ParseErrorKind::Utf8Error(err),
}),
}
}
fn parse_prefix<'s>(bytes: &'s [u8], prefix: &'s [u8]) -> Result<&'s [u8], ParseError<'s>> {
bytes.strip_prefix(prefix).ok_or(ParseError {
line: bytes,
kind: ParseErrorKind::ParseError("line is not a valid proguard record"),
})
}
fn parse_until<P>(bytes: &[u8], predicate: P) -> Result<(&str, &[u8]), ParseError<'_>>
where
P: Fn(&u8) -> bool,
{
let (slice, rest) = match bytes.iter().position(predicate) {
Some(pos) => bytes.split_at(pos),
None => (bytes, &[] as &[u8]),
};
match std::str::from_utf8(slice) {
Ok(s) => Ok((s, rest)),
Err(err) => Err(ParseError {
line: slice,
kind: ParseErrorKind::Utf8Error(err),
}),
}
}
fn parse_until_no_newline<P>(bytes: &[u8], predicate: P) -> Result<(&str, &[u8]), ParseError<'_>>
where
P: Fn(&u8) -> bool,
{
match parse_until(bytes, |byte| is_newline(byte) || predicate(byte)) {
Ok((slice, bytes)) => {
if !bytes.is_empty() && is_newline(&bytes[0]) {
Err(ParseError {
line: slice.as_bytes(),
kind: ParseErrorKind::ParseError("line is not a valid proguard record"),
})
} else {
Ok((slice, bytes))
}
}
Err(err) => Err(err),
}
}
fn consume_leading_newlines(bytes: &[u8]) -> &[u8] {
match bytes.iter().position(|c| !is_newline(c)) {
Some(pos) => &bytes[pos..],
None => b"",
}
}
fn split_line(bytes: &[u8]) -> (&[u8], &[u8]) {
let pos = match bytes.iter().position(is_newline) {
Some(pos) => pos + 1,
None => bytes.len(),
};
bytes.split_at(pos)
}
fn is_newline(byte: &u8) -> bool {
*byte == b'\r' || *byte == b'\n'
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn try_parse_header_with_value() {
let bytes = b"# compiler: R8";
let parsed = ProguardRecord::try_parse(bytes);
assert_eq!(
parsed,
Ok(ProguardRecord::Header {
key: "compiler",
value: Some("R8")
})
);
}
#[test]
fn try_parse_header_without_value() {
let bytes = b"# common_typos_disable";
let parsed = ProguardRecord::try_parse(bytes);
assert_eq!(
parsed,
Ok(ProguardRecord::Header {
key: "common_typos_disable",
value: None,
})
);
}
#[test]
fn try_parse_header_trims_whitespace() {
let bytes = b"# compiler : R8 ";
let parsed = ProguardRecord::try_parse(bytes);
assert_eq!(
parsed,
Ok(ProguardRecord::Header {
key: "compiler",
value: Some("R8")
})
);
}
#[test]
fn try_parse_header_consumes_trailing_newlines() {
let bytes = b"# compiler: R8\r\n\r\n";
let parsed = ProguardRecord::try_parse(bytes);
assert_eq!(
parsed,
Ok(ProguardRecord::Header {
key: "compiler",
value: Some("R8")
})
);
}
#[test]
fn try_parse_header_source_file() {
let bytes = br#"# {"id":"sourceFile","fileName":"Foobar.kt"}"#;
let parsed = ProguardRecord::try_parse(bytes);
assert_eq!(
parsed,
Ok(ProguardRecord::R8Header(R8Header::SourceFile {
file_name: "Foobar.kt",
}))
);
}
#[test]
fn try_parse_r8_headers_invalid() {
let bytes = br#"# {123:"foobar"}"#;
assert!(ProguardRecord::try_parse(bytes).is_err(),);
}
#[test]
fn try_parse_r8_headers() {
let bytes = br#"# {"id":"foobar"}"#;
assert_eq!(
ProguardRecord::try_parse(bytes).unwrap(),
ProguardRecord::R8Header(R8Header::Other),
);
let bytes = br#" #{"id":"foobar"}"#;
assert_eq!(
ProguardRecord::try_parse(bytes).unwrap(),
ProguardRecord::R8Header(R8Header::Other),
);
let bytes = br#"# {"id":"foobar"}"#;
assert_eq!(
ProguardRecord::try_parse(bytes).unwrap(),
ProguardRecord::R8Header(R8Header::Other),
);
}
#[test]
fn try_parse_header_synthesized() {
let bytes = br#"# {"id":"com.android.tools.r8.synthesized"}"#;
assert_eq!(
ProguardRecord::try_parse(bytes).unwrap(),
ProguardRecord::R8Header(R8Header::Synthesized)
);
}
#[test]
fn try_parse_class() {
let bytes = b"android.support.v4.app.RemoteActionCompatParcelizer -> android.support.v4.app.RemoteActionCompatParcelizer:";
let parsed = ProguardRecord::try_parse(bytes);
assert_eq!(
parsed,
Ok(ProguardRecord::Class {
original: "android.support.v4.app.RemoteActionCompatParcelizer",
obfuscated: "android.support.v4.app.RemoteActionCompatParcelizer"
})
);
}
#[test]
fn try_parse_class_consumes_trailing_newlines() {
let bytes = b"android.support.v4.app.RemoteActionCompatParcelizer -> android.support.v4.app.RemoteActionCompatParcelizer:\r\n\r\n";
let parsed = ProguardRecord::try_parse(bytes);
assert_eq!(
parsed,
Ok(ProguardRecord::Class {
original: "android.support.v4.app.RemoteActionCompatParcelizer",
obfuscated: "android.support.v4.app.RemoteActionCompatParcelizer"
})
);
}
#[test]
fn try_parse_field() {
let bytes = b" android.app.Activity mActivity -> a";
let parsed = ProguardRecord::try_parse(bytes);
assert_eq!(
parsed,
Ok(ProguardRecord::Field {
ty: "android.app.Activity",
original: "mActivity",
obfuscated: "a",
}),
);
}
#[test]
fn try_parse_field_consumes_trailing_newlines() {
let bytes = b" android.app.Activity mActivity -> a\r\n\r\n";
let parsed = ProguardRecord::try_parse(bytes);
assert_eq!(
parsed,
Ok(ProguardRecord::Field {
ty: "android.app.Activity",
original: "mActivity",
obfuscated: "a",
}),
);
}
#[test]
fn try_parse_method_simple() {
let bytes = b" boolean equals(java.lang.Object,java.lang.Object) -> a";
let parsed = ProguardRecord::try_parse(bytes);
assert_eq!(
parsed,
Ok(ProguardRecord::Method {
ty: "boolean",
original: "equals",
obfuscated: "a",
arguments: "java.lang.Object,java.lang.Object",
original_class: None,
line_mapping: None,
}),
);
}
#[test]
fn try_parse_method_with_class() {
let bytes = b" void androidx.appcompat.app.AppCompatDelegateImpl.setSupportActionBar(androidx.appcompat.widget.Toolbar) -> onCreate";
let parsed = ProguardRecord::try_parse(bytes);
assert_eq!(
parsed,
Ok(ProguardRecord::Method {
ty: "void",
original: "setSupportActionBar",
obfuscated: "onCreate",
arguments: "androidx.appcompat.widget.Toolbar",
original_class: Some("androidx.appcompat.app.AppCompatDelegateImpl"),
line_mapping: None,
}),
);
}
#[test]
fn try_parse_method_with_start_end_lines() {
let bytes = b" 14:15:void androidx.appcompat.app.AppCompatDelegateImpl.setSupportActionBar(androidx.appcompat.widget.Toolbar) -> onCreate";
let parsed = ProguardRecord::try_parse(bytes);
assert_eq!(
parsed,
Ok(ProguardRecord::Method {
ty: "void",
original: "setSupportActionBar",
obfuscated: "onCreate",
arguments: "androidx.appcompat.widget.Toolbar",
original_class: Some("androidx.appcompat.app.AppCompatDelegateImpl"),
line_mapping: Some(LineMapping {
startline: Some(14),
endline: Some(15),
original_startline: None,
original_endline: None,
}),
}),
);
}
#[test]
fn try_parse_method_with_start_end_original_start_lines() {
let bytes = b" 14:15:void androidx.appcompat.app.AppCompatDelegateImpl.setSupportActionBar(androidx.appcompat.widget.Toolbar):436 -> onCreate";
let parsed = ProguardRecord::try_parse(bytes);
assert_eq!(
parsed,
Ok(ProguardRecord::Method {
ty: "void",
original: "setSupportActionBar",
obfuscated: "onCreate",
arguments: "androidx.appcompat.widget.Toolbar",
original_class: Some("androidx.appcompat.app.AppCompatDelegateImpl"),
line_mapping: Some(LineMapping {
startline: Some(14),
endline: Some(15),
original_startline: Some(436),
original_endline: None,
}),
}),
);
}
#[test]
fn try_parse_method_with_start_end_original_start_original_end_lines() {
let bytes = b" 14:15:void androidx.appcompat.app.AppCompatDelegateImpl.setSupportActionBar(androidx.appcompat.widget.Toolbar):436:437 -> onCreate";
let parsed = ProguardRecord::try_parse(bytes);
assert_eq!(
parsed,
Ok(ProguardRecord::Method {
ty: "void",
original: "setSupportActionBar",
obfuscated: "onCreate",
arguments: "androidx.appcompat.widget.Toolbar",
original_class: Some("androidx.appcompat.app.AppCompatDelegateImpl"),
line_mapping: Some(LineMapping {
startline: Some(14),
endline: Some(15),
original_startline: Some(436),
original_endline: Some(437),
}),
}),
);
}
#[test]
fn try_parse_class_with_bad_delimiter() {
let bytes = b"android.support.v4.app.RemoteActionCompatParcelizer->android.support.v4.app.RemoteActionCompatParcelizer:";
let parsed = ProguardRecord::try_parse(bytes);
assert_eq!(
parsed,
Err(ParseError {
line: bytes,
kind: ParseErrorKind::ParseError("line is not a valid proguard record"),
}),
);
}
#[test]
fn try_parse_class_without_trailing_colon() {
let bytes = b"android.support.v4.app.RemoteActionCompatParcelizer -> android.support.v4.app.RemoteActionCompatParcelizer";
let parsed = ProguardRecord::try_parse(bytes);
assert_eq!(
parsed,
Err(ParseError {
line: bytes,
kind: ParseErrorKind::ParseError("line is not a valid proguard record"),
}),
);
}
#[test]
fn try_parse_field_with_two_space_indentation() {
let bytes = b" android.app.Activity mActivity -> a";
let parsed = ProguardRecord::try_parse(bytes);
assert_eq!(
parsed,
Ok(ProguardRecord::Field {
ty: "android.app.Activity",
original: "mActivity",
obfuscated: "a",
}),
);
}
#[test]
fn try_parse_method_with_only_startline_no_endline() {
let bytes = b" 14:void androidx.appcompat.app.AppCompatDelegateImpl.setSupportActionBar(androidx.appcompat.widget.Toolbar) -> onCreate";
let parsed = ProguardRecord::try_parse(bytes);
assert_eq!(
parsed,
Err(ParseError {
line: bytes,
kind: ParseErrorKind::ParseError("line is not a valid proguard record"),
}),
);
}
#[test]
fn try_parse_method_without_type() {
let bytes = b" 14:15:androidx.appcompat.app.AppCompatDelegateImpl.setSupportActionBar(androidx.appcompat.widget.Toolbar) -> onCreate";
let parsed = ProguardRecord::try_parse(bytes);
assert_eq!(
parsed,
Err(ParseError {
line: bytes,
kind: ParseErrorKind::ParseError("line is not a valid proguard record"),
}),
);
}
#[test]
fn try_parse_iter() {
let bytes = b"\
# compiler: R8
# common_typos_disable
androidx.activity.OnBackPressedCallback->c.a.b:
androidx.activity.OnBackPressedCallback -> c.a.b:
boolean mEnabled -> a
boolean mEnabled -> a
java.util.ArrayDeque mOnBackPressedCallbacks -> b
1:4:void onBackPressed():184:187 -> c
# {\"id\":\"com.android.tools.r8.synthesized\"}
androidx.activity.OnBackPressedCallback
-> c.a.b:
";
let mapping: Vec<Result<ProguardRecord, ParseError>> =
ProguardMapping::new(bytes).iter().collect();
assert_eq!(
mapping,
vec![
Ok(ProguardRecord::Header {
key: "compiler",
value: Some("R8"),
}),
Ok(ProguardRecord::Header {
key: "common_typos_disable",
value: None,
}),
Err(ParseError {
line: b"androidx.activity.OnBackPressedCallback->c.a.b:\n",
kind: ParseErrorKind::ParseError("line is not a valid proguard record"),
}),
Ok(ProguardRecord::Class {
original: "androidx.activity.OnBackPressedCallback",
obfuscated: "c.a.b",
}),
Ok(ProguardRecord::Field {
ty: "boolean",
original: "mEnabled",
obfuscated: "a",
}),
Ok(ProguardRecord::Field {
ty: "boolean",
original: "mEnabled",
obfuscated: "a",
}),
Ok(ProguardRecord::Field {
ty: "java.util.ArrayDeque",
original: "mOnBackPressedCallbacks",
obfuscated: "b",
}),
Ok(ProguardRecord::Method {
ty: "void",
original: "onBackPressed",
obfuscated: "c",
arguments: "",
original_class: None,
line_mapping: Some(LineMapping {
startline: Some(1),
endline: Some(4),
original_startline: Some(184),
original_endline: Some(187),
}),
}),
Ok(ProguardRecord::R8Header(R8Header::Synthesized)),
Err(ParseError {
line: b"androidx.activity.OnBackPressedCallback \n",
kind: ParseErrorKind::ParseError("line is not a valid proguard record"),
}),
Err(ParseError {
line: b"-> c.a.b:\n",
kind: ParseErrorKind::ParseError("line is not a valid proguard record"),
}),
Err(ParseError {
line: b" ",
kind: ParseErrorKind::ParseError("line is not a valid proguard record"),
}),
],
);
}
#[test]
fn try_parse_header_outline() {
let bytes = br#"# {"id":"com.android.tools.r8.outline"}"#;
assert_eq!(
ProguardRecord::try_parse(bytes).unwrap(),
ProguardRecord::R8Header(R8Header::Outline)
);
}
#[test]
fn try_parse_header_rewrite_frame() {
let bytes = br#"# {"id":"com.android.tools.r8.rewriteFrame","conditions":["throws(Ljava/lang/NullPointerException;)"],"actions":["removeInnerFrames(1)"]}"#;
assert_eq!(
ProguardRecord::try_parse(bytes).unwrap(),
ProguardRecord::R8Header(R8Header::RewriteFrame {
conditions: vec!["throws(Ljava/lang/NullPointerException;)"],
actions: vec!["removeInnerFrames(1)"],
})
);
}
#[test]
fn try_parse_header_rewrite_frame_multiple_entries() {
let bytes = br#"# {"id":"com.android.tools.r8.rewriteFrame","conditions":["throws(Ljava/lang/NullPointerException;)","throws(Ljava/lang/IllegalStateException;)"],"actions":["removeInnerFrames(2)"]}"#;
assert_eq!(
ProguardRecord::try_parse(bytes).unwrap(),
ProguardRecord::R8Header(R8Header::RewriteFrame {
conditions: vec![
"throws(Ljava/lang/NullPointerException;)",
"throws(Ljava/lang/IllegalStateException;)"
],
actions: vec!["removeInnerFrames(2)"],
})
);
}
#[test]
fn try_parse_header_outline_callsite_without_outline_key() {
let bytes =
br#"# {"id":"com.android.tools.r8.outlineCallsite","positions":{"1:1":10,"2:3":42}}"#;
let parsed = ProguardRecord::try_parse(bytes).unwrap();
let mut expected_positions: std::collections::HashMap<&str, usize> =
std::collections::HashMap::new();
expected_positions.insert("1:1", 10);
expected_positions.insert("2:3", 42);
assert_eq!(
parsed,
ProguardRecord::R8Header(R8Header::OutlineCallsite {
positions: expected_positions,
outline: None,
})
);
}
#[test]
fn try_parse_header_outline_callsite_with_outline_key() {
let bytes = br#"# {"id":"com.android.tools.r8.outlineCallsite","positions":{"5:7":13},"outline":"Lcom/example/Outline;->m()V"}"#;
let parsed = ProguardRecord::try_parse(bytes).unwrap();
let mut expected_positions: std::collections::HashMap<&str, usize> =
std::collections::HashMap::new();
expected_positions.insert("5:7", 13);
assert_eq!(
parsed,
ProguardRecord::R8Header(R8Header::OutlineCallsite {
positions: expected_positions,
outline: Some("Lcom/example/Outline;->m()V"),
})
);
}
}