use std::collections::BTreeSet;
const ANDROID_BACKEND: &str = include_str!("android.rs");
const ANDROID_HELPERS: &str = include_str!("android/java.rs");
struct PlatformMethod {
class: &'static str,
name: &'static str,
parameters: &'static [&'static str],
result: Option<&'static str>,
}
const fn method(
class: &'static str,
name: &'static str,
parameters: &'static [&'static str],
result: Option<&'static str>,
) -> PlatformMethod {
PlatformMethod {
class,
name,
parameters,
result,
}
}
const ANDROID_API: &[PlatformMethod] = &[
method(
"java.security.KeyStore",
"getInstance",
&["java.lang.String"],
Some("java.security.KeyStore"),
),
method(
"java.security.KeyStore",
"load",
&["java.security.KeyStore$LoadStoreParameter"],
None,
),
method(
"java.security.KeyStore",
"containsAlias",
&["java.lang.String"],
Some("boolean"),
),
method(
"java.security.KeyStore",
"getCertificate",
&["java.lang.String"],
Some("java.security.cert.Certificate"),
),
method(
"java.security.KeyStore",
"getKey",
&["java.lang.String", "char[]"],
Some("java.security.Key"),
),
method(
"java.security.KeyStore",
"setEntry",
&[
"java.lang.String",
"java.security.KeyStore$Entry",
"java.security.KeyStore$ProtectionParameter",
],
None,
),
method(
"java.security.KeyStore",
"deleteEntry",
&["java.lang.String"],
None,
),
method(
"java.security.KeyStore$SecretKeyEntry",
"<init>",
&["javax.crypto.SecretKey"],
None,
),
method(
"java.security.cert.Certificate",
"getPublicKey",
&[],
Some("java.security.PublicKey"),
),
method(
"java.security.interfaces.ECPublicKey",
"getW",
&[],
Some("java.security.spec.ECPoint"),
),
method(
"java.security.spec.ECPoint",
"getAffineX",
&[],
Some("java.math.BigInteger"),
),
method(
"java.security.spec.ECPoint",
"getAffineY",
&[],
Some("java.math.BigInteger"),
),
method("java.math.BigInteger", "toByteArray", &[], Some("byte[]")),
method(
"java.security.Key",
"getAlgorithm",
&[],
Some("java.lang.String"),
),
method("java.security.Key", "getEncoded", &[], Some("byte[]")),
method(
"java.security.KeyFactory",
"getInstance",
&["java.lang.String", "java.lang.String"],
Some("java.security.KeyFactory"),
),
method(
"java.security.KeyFactory",
"getKeySpec",
&["java.security.Key", "java.lang.Class"],
Some("java.security.spec.KeySpec"),
),
method(
"javax.crypto.SecretKeyFactory",
"getInstance",
&["java.lang.String", "java.lang.String"],
Some("javax.crypto.SecretKeyFactory"),
),
method(
"javax.crypto.SecretKeyFactory",
"getKeySpec",
&["javax.crypto.SecretKey", "java.lang.Class"],
Some("java.security.spec.KeySpec"),
),
method(
"android.security.keystore.KeyInfo",
"isInsideSecureHardware",
&[],
Some("boolean"),
),
method(
"java.security.KeyPairGenerator",
"getInstance",
&["java.lang.String", "java.lang.String"],
Some("java.security.KeyPairGenerator"),
),
method(
"java.security.KeyPairGenerator",
"initialize",
&["java.security.spec.AlgorithmParameterSpec"],
None,
),
method(
"java.security.KeyPairGenerator",
"generateKeyPair",
&[],
Some("java.security.KeyPair"),
),
method(
"java.security.spec.ECGenParameterSpec",
"<init>",
&["java.lang.String"],
None,
),
method(
"android.security.keystore.KeyGenParameterSpec$Builder",
"<init>",
&["java.lang.String", "int"],
None,
),
method(
"android.security.keystore.KeyGenParameterSpec$Builder",
"setAlgorithmParameterSpec",
&["java.security.spec.AlgorithmParameterSpec"],
Some("android.security.keystore.KeyGenParameterSpec$Builder"),
),
method(
"android.security.keystore.KeyGenParameterSpec$Builder",
"setDigests",
&["java.lang.String[]"],
Some("android.security.keystore.KeyGenParameterSpec$Builder"),
),
method(
"android.security.keystore.KeyGenParameterSpec$Builder",
"setBlockModes",
&["java.lang.String[]"],
Some("android.security.keystore.KeyGenParameterSpec$Builder"),
),
method(
"android.security.keystore.KeyGenParameterSpec$Builder",
"setEncryptionPaddings",
&["java.lang.String[]"],
Some("android.security.keystore.KeyGenParameterSpec$Builder"),
),
method(
"android.security.keystore.KeyGenParameterSpec$Builder",
"setKeySize",
&["int"],
Some("android.security.keystore.KeyGenParameterSpec$Builder"),
),
method(
"android.security.keystore.KeyGenParameterSpec$Builder",
"setRandomizedEncryptionRequired",
&["boolean"],
Some("android.security.keystore.KeyGenParameterSpec$Builder"),
),
method(
"android.security.keystore.KeyGenParameterSpec$Builder",
"setIsStrongBoxBacked",
&["boolean"],
Some("android.security.keystore.KeyGenParameterSpec$Builder"),
),
method(
"android.security.keystore.KeyGenParameterSpec$Builder",
"build",
&[],
Some("android.security.keystore.KeyGenParameterSpec"),
),
method(
"java.security.Signature",
"getInstance",
&["java.lang.String"],
Some("java.security.Signature"),
),
method(
"java.security.Signature",
"initSign",
&["java.security.PrivateKey"],
None,
),
method("java.security.Signature", "update", &["byte[]"], None),
method("java.security.Signature", "sign", &[], Some("byte[]")),
method(
"javax.crypto.spec.SecretKeySpec",
"<init>",
&["byte[]", "java.lang.String"],
None,
),
method(
"android.security.keystore.KeyProtection$Builder",
"<init>",
&["int"],
None,
),
method(
"android.security.keystore.KeyProtection$Builder",
"setBlockModes",
&["java.lang.String[]"],
Some("android.security.keystore.KeyProtection$Builder"),
),
method(
"android.security.keystore.KeyProtection$Builder",
"setEncryptionPaddings",
&["java.lang.String[]"],
Some("android.security.keystore.KeyProtection$Builder"),
),
method(
"android.security.keystore.KeyProtection$Builder",
"build",
&[],
Some("android.security.keystore.KeyProtection"),
),
method(
"javax.crypto.KeyGenerator",
"getInstance",
&["java.lang.String", "java.lang.String"],
Some("javax.crypto.KeyGenerator"),
),
method(
"javax.crypto.KeyGenerator",
"init",
&["java.security.spec.AlgorithmParameterSpec"],
None,
),
method(
"javax.crypto.KeyGenerator",
"generateKey",
&[],
Some("javax.crypto.SecretKey"),
),
method(
"javax.crypto.Cipher",
"getInstance",
&["java.lang.String"],
Some("javax.crypto.Cipher"),
),
method(
"javax.crypto.Cipher",
"init",
&["int", "java.security.Key"],
None,
),
method(
"javax.crypto.Cipher",
"init",
&[
"int",
"java.security.Key",
"java.security.spec.AlgorithmParameterSpec",
],
None,
),
method("javax.crypto.Cipher", "getIV", &[], Some("byte[]")),
method(
"javax.crypto.Cipher",
"doFinal",
&["byte[]"],
Some("byte[]"),
),
method(
"javax.crypto.spec.GCMParameterSpec",
"<init>",
&["int", "byte[]"],
None,
),
method("java.lang.Class", "getName", &[], Some("java.lang.String")),
method(
"java.lang.Throwable",
"getMessage",
&[],
Some("java.lang.String"),
),
];
fn descriptor_of(method: &PlatformMethod) -> String {
let mut descriptor = String::from("(");
for parameter in method.parameters {
descriptor.push_str(&type_descriptor(parameter));
}
descriptor.push(')');
descriptor.push_str(&type_descriptor(method.result.unwrap_or("void")));
descriptor
}
fn type_descriptor(name: &str) -> String {
if let Some(element) = name.strip_suffix("[]") {
return format!("[{}", type_descriptor(element));
}
match name {
"void" => "V".to_owned(),
"boolean" => "Z".to_owned(),
"byte" => "B".to_owned(),
"char" => "C".to_owned(),
"short" => "S".to_owned(),
"int" => "I".to_owned(),
"long" => "J".to_owned(),
"float" => "F".to_owned(),
"double" => "D".to_owned(),
class => format!("L{};", class.replace('.', "/")),
}
}
fn type_length(text: &str) -> Option<usize> {
let mut index = 0;
while text[index..].starts_with('[') {
index += 1;
}
match text[index..].chars().next()? {
'V' | 'Z' | 'B' | 'C' | 'S' | 'I' | 'J' | 'F' | 'D' => Some(index + 1),
'L' => {
let end = text[index..].find(';')?;
Some(index + end + 1)
}
_ => None,
}
}
fn is_descriptor(literal: &str) -> bool {
let Some(body) = literal.strip_prefix('(') else {
return false;
};
let Some((parameters, result)) = body.split_once(')') else {
return false;
};
let mut rest = parameters;
while !rest.is_empty() {
match type_length(rest) {
Some(length) => rest = &rest[length..],
None => return false,
}
}
type_length(result) == Some(result.len())
}
fn string_literals(line: &str) -> Vec<&str> {
let mut literals = Vec::new();
let mut rest = line;
while let Some(start) = rest.find('"') {
let after = &rest[start + 1..];
match after.find('"') {
Some(end) => {
literals.push(&after[..end]);
rest = &after[end + 1..];
}
None => break,
}
}
literals
}
fn descriptors_in(source: &str) -> BTreeSet<String> {
let mut found = BTreeSet::new();
for line in source.lines() {
if line.trim_start().starts_with("//") {
continue;
}
for literal in string_literals(line) {
if is_descriptor(literal) {
found.insert(literal.to_owned());
}
}
}
found
}
fn declared_descriptors() -> BTreeSet<String> {
ANDROID_API.iter().map(descriptor_of).collect()
}
fn used_descriptors() -> BTreeSet<String> {
let mut used = descriptors_in(ANDROID_BACKEND);
used.extend(descriptors_in(ANDROID_HELPERS));
used
}
#[test]
fn the_backend_uses_the_platforms_descriptors_and_only_those() {
let declared = declared_descriptors();
let used = used_descriptors();
let undeclared: Vec<&String> = used.difference(&declared).collect();
assert!(
undeclared.is_empty(),
"the Android platform declares no such method: {undeclared:?}"
);
let unused: Vec<&String> = declared.difference(&used).collect();
assert!(
unused.is_empty(),
"the table declares methods this backend never calls: {unused:?}"
);
}
#[test]
fn the_scanner_finds_the_descriptors_that_are_in_the_sources() {
let used = used_descriptors();
assert!(
used.len() > 20,
"the Android backend calls far more than a handful of JNI methods, found {}",
used.len()
);
for expected in [
"(Ljava/lang/String;)Ljava/security/KeyStore;",
"([Ljava/lang/String;)Landroid/security/keystore/KeyGenParameterSpec$Builder;",
"(ILjava/security/Key;Ljava/security/spec/AlgorithmParameterSpec;)V",
"()Ljava/security/spec/ECPoint;",
] {
assert!(used.contains(expected), "the scanner missed {expected}");
}
}
#[test]
fn the_symmetric_key_description_takes_a_secret_key() {
let described = ANDROID_API
.iter()
.find(|method| {
method.class == "javax.crypto.SecretKeyFactory" && method.name == "getKeySpec"
})
.expect("the table carries the symmetric key description");
assert_eq!(
descriptor_of(described),
"(Ljavax/crypto/SecretKey;Ljava/lang/Class;)Ljava/security/spec/KeySpec;",
"Android takes a javax.crypto.SecretKey here, Java SE a java.security.Key"
);
assert!(
descriptors_in(ANDROID_BACKEND)
.contains("(Ljavax/crypto/SecretKey;Ljava/lang/Class;)Ljava/security/spec/KeySpec;"),
"the backend has to call it with the platform's descriptor"
);
}