5.0 KiB · text History aca7335
//! Patching AndroidManifest.xml to point at the exported icon.

use icon_studio::manifest::{self, Change};

const DIOGRAMOS: &str = r#"<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-permission android:name="android.permission.INTERNET"/>

    <application
        android:label="@string/app_name"
        android:allowBackup="false"
        android:theme="@style/Theme.Diogramos"
        android:usesCleartextTraffic="true">

        <activity android:name=".MainActivity" android:exported="true"/>
    </application>
</manifest>
"#;

#[test]
fn adds_the_icon_when_the_manifest_has_none() {
    // The case that started this: files exported correctly, no icon shown,
    // because nothing referenced them.
    let (out, change) = manifest::patch_source(DIOGRAMOS, "ic_icon", true).unwrap();
    assert_eq!(change, Change::Added);
    assert!(out.contains(r#"android:icon="@mipmap/ic_icon""#), "{out}");
    assert!(
        out.contains(r#"android:roundIcon="@mipmap/ic_icon""#),
        "{out}"
    );

    // Everything else survives.
    assert!(out.contains(r#"android:theme="@style/Theme.Diogramos""#));
    assert!(out.contains("android.permission.INTERNET"));
    assert!(out.contains(".MainActivity"));
}

#[test]
fn inserted_attributes_match_the_surrounding_indentation() {
    let (out, _) = manifest::patch_source(DIOGRAMOS, "ic_icon", true).unwrap();
    let line = out
        .lines()
        .find(|l| l.contains("android:icon="))
        .expect("icon line");
    assert!(
        line.starts_with("        android:icon="),
        "should line up with the other attributes, got {line:?}"
    );
}

#[test]
fn patching_twice_changes_nothing_the_second_time() {
    let (once, first) = manifest::patch_source(DIOGRAMOS, "ic_icon", true).unwrap();
    assert_eq!(first, Change::Added);
    let (twice, second) = manifest::patch_source(&once, "ic_icon", true).unwrap();
    assert_eq!(second, Change::AlreadyCorrect);
    assert_eq!(once, twice, "idempotent");
}

#[test]
fn repoints_an_icon_that_names_something_else() {
    let src = DIOGRAMOS.replace(
        r#"        android:label="@string/app_name""#,
        "        android:label=\"@string/app_name\"\n        android:icon=\"@mipmap/ic_launcher\"",
    );
    let (out, change) = manifest::patch_source(&src, "ic_icon", false).unwrap();
    assert_eq!(change, Change::Updated);
    assert!(out.contains(r#"android:icon="@mipmap/ic_icon""#));
    assert!(!out.contains("ic_launcher"));
}

#[test]
fn icon_and_round_icon_are_told_apart() {
    // `android:icon` is a substring of `android:roundIcon`; a naive search
    // would rewrite the wrong one.
    let src = DIOGRAMOS.replace(
        r#"        android:label="@string/app_name""#,
        "        android:label=\"@string/app_name\"\n        android:roundIcon=\"@mipmap/old_round\"",
    );
    let (out, _) = manifest::patch_source(&src, "ic_icon", true).unwrap();
    assert!(
        out.contains(r#"android:roundIcon="@mipmap/ic_icon""#),
        "{out}"
    );
    assert!(out.contains(r#"android:icon="@mipmap/ic_icon""#), "{out}");
    assert!(!out.contains("old_round"));
    assert_eq!(out.matches("android:roundIcon=").count(), 1);
    assert_eq!(out.matches(r#"android:icon="#).count(), 1);
}

#[test]
fn a_greater_than_inside_an_attribute_does_not_end_the_tag() {
    let src = r#"<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application android:label="a > b" android:theme="@style/T">
    </application>
</manifest>
"#;
    let (out, change) = manifest::patch_source(src, "ic_icon", false).unwrap();
    assert_eq!(change, Change::Added);
    assert!(out.contains(r#"android:label="a > b""#), "{out}");
    assert!(out.contains(r#"android:icon="@mipmap/ic_icon""#), "{out}");
}

#[test]
fn a_single_line_application_tag_is_handled() {
    let src = r#"<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application android:label="@string/app_name">
    </application>
</manifest>
"#;
    let (out, change) = manifest::patch_source(src, "ic_icon", false).unwrap();
    assert_eq!(change, Change::Added);
    assert!(out.contains(r#"android:icon="@mipmap/ic_icon""#), "{out}");
}

#[test]
fn a_manifest_without_an_application_element_is_an_error_not_a_guess() {
    let src = r#"<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
"#;
    assert!(manifest::patch_source(src, "ic_icon", false).is_err());
}

#[test]
fn the_real_cbzauth_manifest_is_already_correct() {
    // Ground truth: a manifest that works. Patching it must be a no-op.
    let p = "/home/gmorell/Development/projects/cbz_clients_decompilation/recreated/cbzauth/app/src/main/AndroidManifest.xml";
    let Ok(src) = std::fs::read_to_string(p) else {
        return; // not checked out; nothing to assert
    };
    let (out, change) = manifest::patch_source(&src, "ic_icon", true).unwrap();
    assert_eq!(change, Change::AlreadyCorrect);
    assert_eq!(out, src, "a correct manifest must not be rewritten");
}