struct Throwable {
class: &'static str,
message: String,
frames: &'static [&'static str],
elided: usize,
cause: Option<Box<Throwable>>,
}
impl Throwable {
fn render(&self, header: &str, out: &mut String) {
out.push_str(header);
out.push_str(self.class);
out.push_str(": ");
out.push_str(&self.message);
for frame in self.frames {
out.push_str("\n\tat ");
out.push_str(frame);
}
if self.elided != 0 {
out.push_str("\n\t... ");
out.push_str(&self.elided.to_string());
out.push_str(" more");
}
if let Some(cause) = &self.cause {
cause.render("\nCaused by: ", out);
}
}
}
const APK_FORMAT_EXCEPTION: &str = "com.android.apksig.apk.ApkFormatException";
const ZIP_FORMAT_EXCEPTION: &str = "com.android.apksig.zip.ZipFormatException";
const FRAMES_APK_VERIFIER_VERIFY: &[&str] = &[
"com.android.apksig.ApkVerifier.verify(ApkVerifier.java:191)",
"com.android.apksig.ApkVerifier.verify(ApkVerifier.java:164)",
"com.android.apksigner.ApkSignerTool.verify(ApkSignerTool.java:587)",
"com.android.apksigner.ApkSignerTool.main(ApkSignerTool.java:95)",
];
const FRAMES_FIND_ZIP_SECTIONS_NO_EOCD: &[&str] = &[
"com.android.apksig.apk.ApkUtilsLite.findZipSections(ApkUtilsLite.java:49)",
"com.android.apksig.apk.ApkUtils.findZipSections(ApkUtils.java:60)",
"com.android.apksig.ApkVerifier.verify(ApkVerifier.java:189)",
];
const FRAMES_FIND_ZIP_SECTIONS_OVERLAP: &[&str] = &[
"com.android.apksig.apk.ApkUtilsLite.findZipSections(ApkUtilsLite.java:65)",
"com.android.apksig.apk.ApkUtils.findZipSections(ApkUtils.java:60)",
"com.android.apksig.ApkVerifier.verify(ApkVerifier.java:189)",
];
const FRAMES_V1_VERIFY_MANIFEST: &[&str] = &[
"com.android.apksig.internal.apk.v1.V1SchemeVerifier$Signers.verify(V1SchemeVerifier.java:264)",
"com.android.apksig.internal.apk.v1.V1SchemeVerifier$Signers.access$100(V1SchemeVerifier.java:210)",
"com.android.apksig.internal.apk.v1.V1SchemeVerifier.verify(V1SchemeVerifier.java:132)",
"com.android.apksig.ApkVerifier.verify(ApkVerifier.java:346)",
"com.android.apksig.ApkVerifier.verify(ApkVerifier.java:164)",
"com.android.apksigner.ApkSignerTool.verify(ApkSignerTool.java:587)",
"com.android.apksigner.ApkSignerTool.main(ApkSignerTool.java:95)",
];
const FRAMES_LOCAL_FILE_RECORD: &[&str] = &[
"com.android.apksig.internal.zip.LocalFileRecord.getRecord(LocalFileRecord.java:191)",
"com.android.apksig.internal.zip.LocalFileRecord.outputUncompressedData(LocalFileRecord.java:427)",
"com.android.apksig.internal.zip.LocalFileRecord.getUncompressedData(LocalFileRecord.java:457)",
"com.android.apksig.internal.apk.v1.V1SchemeVerifier$Signers.verify(V1SchemeVerifier.java:262)",
];
const FRAMES_GET_ANDROID_MANIFEST: &[&str] = &[
"com.android.apksig.ApkSigner.getAndroidManifestFromApk(ApkSigner.java:970)",
"com.android.apksig.ApkVerifier.getAndroidManifestFromApk(ApkVerifier.java:1225)",
"com.android.apksig.ApkVerifier.verify(ApkVerifier.java:324)",
"com.android.apksig.ApkVerifier.verify(ApkVerifier.java:164)",
"com.android.apksigner.ApkSignerTool.verify(ApkSignerTool.java:587)",
"com.android.apksigner.ApkSignerTool.main(ApkSignerTool.java:95)",
];
const ZIP_CAUSE_SEPARATOR: &str = ": ZIP format error: ";
const MISSING_ANDROID_MANIFEST: &str = "Missing AndroidManifest.xml";
const NOT_A_ZIP_ARCHIVE: &str = "Malformed APK: not a ZIP archive";
const MALFORMED_ZIP_ENTRY: &str = "Malformed ZIP entry: ";
const MANIFEST_ENTRY: &str = "META-INF/MANIFEST.MF";
pub fn render_apk_format_exception(message: &str) -> Option<String> {
let mut out = String::new();
let throwable = classify(message)?;
throwable.render("Exception in thread \"main\" ", &mut out);
Some(out)
}
fn classify(message: &str) -> Option<Throwable> {
if message == MISSING_ANDROID_MANIFEST {
return Some(Throwable {
class: APK_FORMAT_EXCEPTION,
message: message.to_string(),
frames: FRAMES_GET_ANDROID_MANIFEST,
elided: 0,
cause: None,
});
}
let (top, cause_message) = message.split_once(ZIP_CAUSE_SEPARATOR)?;
if top == NOT_A_ZIP_ARCHIVE {
let frames = if cause_message.starts_with("ZIP End of Central Directory record not found") {
FRAMES_FIND_ZIP_SECTIONS_NO_EOCD
} else if cause_message
.starts_with("ZIP Central Directory overlaps with End of Central Directory.")
{
FRAMES_FIND_ZIP_SECTIONS_OVERLAP
} else {
return None;
};
return Some(Throwable {
class: APK_FORMAT_EXCEPTION,
message: top.to_string(),
frames: FRAMES_APK_VERIFIER_VERIFY,
elided: 0,
cause: Some(Box::new(Throwable {
class: ZIP_FORMAT_EXCEPTION,
message: cause_message.to_string(),
frames,
elided: 3,
cause: None,
})),
});
}
if top == format!("{MALFORMED_ZIP_ENTRY}{MANIFEST_ENTRY}")
&& cause_message.starts_with(
"CRC-32 mismatch between Local File Header and Central Directory for entry ",
)
{
return Some(Throwable {
class: APK_FORMAT_EXCEPTION,
message: top.to_string(),
frames: FRAMES_V1_VERIFY_MANIFEST,
elided: 0,
cause: Some(Box::new(Throwable {
class: ZIP_FORMAT_EXCEPTION,
message: cause_message.to_string(),
frames: FRAMES_LOCAL_FILE_RECORD,
elided: 6,
cause: None,
})),
});
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn missing_android_manifest_matches_the_oracle() {
let rendered = render_apk_format_exception("Missing AndroidManifest.xml").unwrap();
assert_eq!(
rendered,
"Exception in thread \"main\" com.android.apksig.apk.ApkFormatException: Missing AndroidManifest.xml\n\
\tat com.android.apksig.ApkSigner.getAndroidManifestFromApk(ApkSigner.java:970)\n\
\tat com.android.apksig.ApkVerifier.getAndroidManifestFromApk(ApkVerifier.java:1225)\n\
\tat com.android.apksig.ApkVerifier.verify(ApkVerifier.java:324)\n\
\tat com.android.apksig.ApkVerifier.verify(ApkVerifier.java:164)\n\
\tat com.android.apksigner.ApkSignerTool.verify(ApkSignerTool.java:587)\n\
\tat com.android.apksigner.ApkSignerTool.main(ApkSignerTool.java:95)"
);
}
#[test]
fn missing_end_of_central_directory_matches_the_oracle() {
let rendered = render_apk_format_exception(
"Malformed APK: not a ZIP archive: ZIP format error: \
ZIP End of Central Directory record not found",
)
.unwrap();
assert_eq!(
rendered,
"Exception in thread \"main\" com.android.apksig.apk.ApkFormatException: Malformed APK: not a ZIP archive\n\
\tat com.android.apksig.ApkVerifier.verify(ApkVerifier.java:191)\n\
\tat com.android.apksig.ApkVerifier.verify(ApkVerifier.java:164)\n\
\tat com.android.apksigner.ApkSignerTool.verify(ApkSignerTool.java:587)\n\
\tat com.android.apksigner.ApkSignerTool.main(ApkSignerTool.java:95)\n\
Caused by: com.android.apksig.zip.ZipFormatException: ZIP End of Central Directory record not found\n\
\tat com.android.apksig.apk.ApkUtilsLite.findZipSections(ApkUtilsLite.java:49)\n\
\tat com.android.apksig.apk.ApkUtils.findZipSections(ApkUtils.java:60)\n\
\tat com.android.apksig.ApkVerifier.verify(ApkVerifier.java:189)\n\
\t... 3 more"
);
}
#[test]
fn overlapping_central_directory_matches_the_oracle() {
let rendered = render_apk_format_exception(
"Malformed APK: not a ZIP archive: ZIP format error: ZIP Central Directory overlaps \
with End of Central Directory. CD end: 4112, EoCD start: 4111",
)
.unwrap();
assert_eq!(
rendered,
"Exception in thread \"main\" com.android.apksig.apk.ApkFormatException: Malformed APK: not a ZIP archive\n\
\tat com.android.apksig.ApkVerifier.verify(ApkVerifier.java:191)\n\
\tat com.android.apksig.ApkVerifier.verify(ApkVerifier.java:164)\n\
\tat com.android.apksigner.ApkSignerTool.verify(ApkSignerTool.java:587)\n\
\tat com.android.apksigner.ApkSignerTool.main(ApkSignerTool.java:95)\n\
Caused by: com.android.apksig.zip.ZipFormatException: ZIP Central Directory overlaps \
with End of Central Directory. CD end: 4112, EoCD start: 4111\n\
\tat com.android.apksig.apk.ApkUtilsLite.findZipSections(ApkUtilsLite.java:65)\n\
\tat com.android.apksig.apk.ApkUtils.findZipSections(ApkUtils.java:60)\n\
\tat com.android.apksig.ApkVerifier.verify(ApkVerifier.java:189)\n\
\t... 3 more"
);
}
#[test]
fn malformed_manifest_entry_matches_the_oracle() {
let rendered = render_apk_format_exception(
"Malformed ZIP entry: META-INF/MANIFEST.MF: ZIP format error: CRC-32 mismatch between \
Local File Header and Central Directory for entry META-INF/MANIFEST.MF. \
LFH: 3496857924, CD: 1634586948",
)
.unwrap();
assert_eq!(
rendered,
"Exception in thread \"main\" com.android.apksig.apk.ApkFormatException: Malformed ZIP entry: META-INF/MANIFEST.MF\n\
\tat com.android.apksig.internal.apk.v1.V1SchemeVerifier$Signers.verify(V1SchemeVerifier.java:264)\n\
\tat com.android.apksig.internal.apk.v1.V1SchemeVerifier$Signers.access$100(V1SchemeVerifier.java:210)\n\
\tat com.android.apksig.internal.apk.v1.V1SchemeVerifier.verify(V1SchemeVerifier.java:132)\n\
\tat com.android.apksig.ApkVerifier.verify(ApkVerifier.java:346)\n\
\tat com.android.apksig.ApkVerifier.verify(ApkVerifier.java:164)\n\
\tat com.android.apksigner.ApkSignerTool.verify(ApkSignerTool.java:587)\n\
\tat com.android.apksigner.ApkSignerTool.main(ApkSignerTool.java:95)\n\
Caused by: com.android.apksig.zip.ZipFormatException: CRC-32 mismatch between Local File Header and Central Directory for entry META-INF/MANIFEST.MF. LFH: 3496857924, CD: 1634586948\n\
\tat com.android.apksig.internal.zip.LocalFileRecord.getRecord(LocalFileRecord.java:191)\n\
\tat com.android.apksig.internal.zip.LocalFileRecord.outputUncompressedData(LocalFileRecord.java:427)\n\
\tat com.android.apksig.internal.zip.LocalFileRecord.getUncompressedData(LocalFileRecord.java:457)\n\
\tat com.android.apksig.internal.apk.v1.V1SchemeVerifier$Signers.verify(V1SchemeVerifier.java:262)\n\
\t... 6 more"
);
}
#[test]
fn unobserved_failures_fall_back_to_the_plain_message() {
assert!(
render_apk_format_exception(
"Malformed APK: not a ZIP archive: ZIP format error: something else entirely"
)
.is_none()
);
assert!(
render_apk_format_exception(
"Malformed ZIP entry: META-INF/CERT.SF: ZIP format error: CRC-32 mismatch between \
Local File Header and Central Directory for entry META-INF/CERT.SF. LFH: 1, CD: 2"
)
.is_none()
);
assert!(render_apk_format_exception("APK not provided").is_none());
assert!(render_apk_format_exception("Failed to open APK /nope.apk").is_none());
}
}