Use derive_classpath in VS for has_classpath_jar

Previously we checked the environment variables directly, but that
doesn't handle an update to derive_classpath that adds new variables.

Bug: 210472252
Test: atest virtualizationservice_device_test
Test: composd_cmd test-compile
Change-Id: I98980b631e534a14a9434745fa64fab308257a1d
diff --git a/virtualizationservice/Android.bp b/virtualizationservice/Android.bp
index 277432b..c3ab39a 100644
--- a/virtualizationservice/Android.bp
+++ b/virtualizationservice/Android.bp
@@ -36,6 +36,7 @@
         "libmicrodroid_metadata",
         "libmicrodroid_payload_config",
         "libonce_cell",
+        "libregex",
         "librustutils",
         "libselinux_bindgen",
         "libserde",
diff --git a/virtualizationservice/src/payload.rs b/virtualizationservice/src/payload.rs
index bbd7fec..a8c22cd 100644
--- a/virtualizationservice/src/payload.rs
+++ b/virtualizationservice/src/payload.rs
@@ -22,16 +22,18 @@
 use android_system_virtualizationservice::binder::ParcelFileDescriptor;
 use anyhow::{anyhow, bail, Context, Result};
 use binder::wait_for_interface;
-use log::{error, info};
+use log::{info, warn};
 use microdroid_metadata::{ApexPayload, ApkPayload, Metadata};
 use microdroid_payload_config::{ApexConfig, VmPayloadConfig};
 use once_cell::sync::OnceCell;
 use packagemanager_aidl::aidl::android::content::pm::IPackageManagerNative::IPackageManagerNative;
+use regex::Regex;
 use serde::Deserialize;
 use serde_xml_rs::from_reader;
-use std::env;
+use std::collections::HashSet;
 use std::fs::{File, OpenOptions};
 use std::path::{Path, PathBuf};
+use std::process::Command;
 use vmconfig::open_parcel_file;
 
 /// The list of APEXes which microdroid requires.
@@ -71,21 +73,16 @@
             let mut apex_info_list: ApexInfoList = from_reader(apex_info_list)
                 .context(format!("Failed to parse {}", APEX_INFO_LIST_PATH))?;
 
-            // For active APEXes, we refer env variables to see if it contributes to classpath
-            // TODO(b/210472252): Don't hard code the env variable names
-            let boot_classpath_apexes = find_apex_names_in_classpath_env("BOOTCLASSPATH");
-            let systemserver_classpath_apexes =
-                find_apex_names_in_classpath_env("SYSTEMSERVERCLASSPATH");
-            let dex2oatboot_classpath_apexes =
-                find_apex_names_in_classpath_env("DEX2OATBOOTCLASSPATH");
-            let standalone_jar_apexes =
-                find_apex_names_in_classpath_env("STANDALONE_SYSTEMSERVER_JARS");
+            // For active APEXes, we run derive_classpath and parse its output to see if it
+            // contributes to the classpath(s). (This allows us to handle any new classpath env
+            // vars seamlessly.)
+            let classpath_vars = run_derive_classpath()?;
+            let classpath_apexes = find_apex_names_in_classpath(&classpath_vars)?;
+
             for apex_info in apex_info_list.list.iter_mut() {
-                apex_info.has_classpath_jar = boot_classpath_apexes.contains(&apex_info.name)
-                    || systemserver_classpath_apexes.contains(&apex_info.name)
-                    || dex2oatboot_classpath_apexes.contains(&apex_info.name)
-                    || standalone_jar_apexes.contains(&apex_info.name);
+                apex_info.has_classpath_jar = classpath_apexes.contains(&apex_info.name);
             }
+
             Ok(apex_info_list)
         })
     }
@@ -263,22 +260,41 @@
     Ok(DiskImage { image: None, partitions, writable: false })
 }
 
-fn find_apex_names_in_classpath_env(classpath_env_var: &str) -> Vec<String> {
-    let val = env::var(classpath_env_var).unwrap_or_else(|e| {
-        error!("Reading {} failed: {}", classpath_env_var, e);
-        String::from("")
-    });
-    val.split(':')
-        .filter_map(|path| {
-            Path::new(path)
-                .strip_prefix("/apex/")
-                .map(|stripped| {
-                    let first = stripped.iter().next().unwrap();
-                    first.to_str().unwrap().to_string()
-                })
-                .ok()
-        })
-        .collect()
+fn run_derive_classpath() -> Result<String> {
+    let result = Command::new("/apex/com.android.sdkext/bin/derive_classpath")
+        .arg("/proc/self/fd/1")
+        .output()
+        .context("Failed to run derive_classpath")?;
+
+    if !result.status.success() {
+        bail!("derive_classpath returned {}", result.status);
+    }
+
+    String::from_utf8(result.stdout).context("Converting derive_classpath output")
+}
+
+fn find_apex_names_in_classpath(classpath_vars: &str) -> Result<HashSet<String>> {
+    // Each line should be in the format "export <var name> <paths>", where <paths> is a
+    // colon-separated list of paths to JARs. We don't care about the var names, and we're only
+    // interested in paths that look like "/apex/<apex name>/<anything>" so we know which APEXes
+    // contribute to at least one var.
+    let mut apexes = HashSet::new();
+
+    let pattern = Regex::new(r"^export [^ ]+ ([^ ]+)$").context("Failed to construct Regex")?;
+    for line in classpath_vars.lines() {
+        if let Some(captures) = pattern.captures(line) {
+            if let Some(paths) = captures.get(1) {
+                apexes.extend(paths.as_str().split(':').filter_map(|path| {
+                    let path = path.strip_prefix("/apex/")?;
+                    Some(path[..path.find('/')?].to_owned())
+                }));
+                continue;
+            }
+        }
+        warn!("Malformed line from derive_classpath: {}", line);
+    }
+
+    Ok(apexes)
 }
 
 // Collect APEX names from config
@@ -359,13 +375,16 @@
 mod tests {
     use super::*;
     #[test]
-    fn test_find_apex_names_in_classpath_env() {
-        let key = "TEST_BOOTCLASSPATH";
-        let classpath = "/apex/com.android.foo/javalib/foo.jar:/system/framework/framework.jar:/apex/com.android.bar/javalib/bar.jar";
-        env::set_var(key, classpath);
-        assert_eq!(
-            find_apex_names_in_classpath_env(key),
-            vec!["com.android.foo".to_owned(), "com.android.bar".to_owned()]
-        );
+    fn test_find_apex_names_in_classpath() {
+        let vars = r#"
+export FOO /apex/unterminated
+export BAR /apex/valid.apex/something
+wrong
+export EMPTY
+export OTHER /foo/bar:/baz:/apex/second.valid.apex/:gibberish:"#;
+        let expected = vec!["valid.apex", "second.valid.apex"];
+        let expected: HashSet<_> = expected.into_iter().map(ToString::to_string).collect();
+
+        assert_eq!(find_apex_names_in_classpath(vars).unwrap(), expected);
     }
 }