/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "derive_classpath.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "android-base/unique_fd.h" #include "packages/modules/common/proto/classpaths.pb.h" namespace android { namespace derive_classpath { namespace { static const std::string kFrameworkJarFilepath = "/system/framework/framework.jar"; static const std::string kLibartJarFilepath = "/apex/com.android.art/javalib/core-libart.jar"; static const std::string kSdkExtensionsJarFilepath = "/apex/com.android.sdkext/javalib/framework-sdkextensions.jar"; static const std::string kServicesJarFilepath = "/system/framework/services.jar"; // The fixture for testing derive_classpath. class DeriveClasspathTest : public ::testing::Test { protected: ~DeriveClasspathTest() override { // Not really needed, as a test device will re-generate a proper classpath on reboot, // but it's better to leave it in a clean state after a test. GenerateClasspathExports(default_args_); } const std::string working_dir() { return std::string(temp_dir_.path); } // Parses the generated classpath exports file and returns each line individually. std::vector ParseExportsFile(const char* file = "/data/system/environ/classpath") { std::string contents; EXPECT_TRUE(android::base::ReadFileToString(file, &contents, /*follow_symlinks=*/true)); return android::base::Split(contents, "\n"); } std::vector SplitClasspathExportLine(const std::string& line) { std::vector contents = android::base::Split(line, " "); // Export lines are expected to be structured as `export `. EXPECT_EQ(3, contents.size()); EXPECT_EQ("export", contents[0]); return contents; } // Checks the order of the jars in a given classpath. // Instead of doing a full order check, it assumes the jars are grouped by partition and checks // that partitions come in order of the `prefixes` that is given. void CheckClasspathGroupOrder(const std::string classpath, const std::vector prefixes) { ASSERT_NE(0, prefixes.size()); ASSERT_NE(0, classpath.size()); auto jars = android::base::Split(classpath, ":"); auto prefix = prefixes.begin(); auto jar = jars.begin(); for (; jar != jars.end() && prefix != prefixes.end(); ++jar) { if (*jar == "/apex/com.android.i18n/javalib/core-icu4j.jar") { // core-icu4j.jar is special and is out of order in BOOTCLASSPATH; // ignore it when checking for general order continue; } if (!android::base::StartsWith(*jar, *prefix)) { ++prefix; } } EXPECT_NE(prefix, prefixes.end()); // All jars have been iterated over, thus they all have valid prefixes EXPECT_EQ(jar, jars.end()); } void WriteConfig(const ExportedClasspathsJars& exported_jars, const std::string& path) { std::string fragment_path = working_dir() + path; std::string buf; exported_jars.SerializeToString(&buf); std::string cmd("mkdir -p " + android::base::Dirname(fragment_path)); ASSERT_EQ(0, system(cmd.c_str())); ASSERT_TRUE(android::base::WriteStringToFile(buf, fragment_path, true)); } void AddJarToClasspath(const std::string& partition, const std::string& jar_filepath, Classpath classpath) { ExportedClasspathsJars exported_jars; Jar* jar = exported_jars.add_jars(); jar->set_path(jar_filepath); jar->set_classpath(classpath); std::string basename = Classpath_Name(classpath) + ".pb"; std::transform(basename.begin(), basename.end(), basename.begin(), [](unsigned char c) { return std::tolower(c); }); WriteConfig(exported_jars, partition + "/etc/classpaths/" + basename); } const TemporaryDir temp_dir_; const Args default_args_ = { .output_path = kGeneratedClasspathExportsFilepath, }; const Args default_args_with_test_dir_ = { .output_path = kGeneratedClasspathExportsFilepath, .glob_pattern_prefix = temp_dir_.path, }; }; using DeriveClasspathDeathTest = DeriveClasspathTest; // Check only known *CLASSPATH variables are exported. TEST_F(DeriveClasspathTest, DefaultNoUnknownClasspaths) { // Re-generate default on device classpaths GenerateClasspathExports(default_args_); const std::vector exportLines = ParseExportsFile(); // The first four lines are tested below. for (int i = 4; i < exportLines.size(); i++) { EXPECT_EQ(exportLines[i], ""); } } // Test that all variables are properly generated. TEST_F(DeriveClasspathTest, AllVariables) { ExportedClasspathsJars exported_jars; Jar* jar = exported_jars.add_jars(); jar->set_path("/apex/com.android.foo/javalib/foo"); jar->set_classpath(BOOTCLASSPATH); jar = exported_jars.add_jars(); jar->set_path("/apex/com.android.bar/javalib/bar"); jar->set_classpath(DEX2OATBOOTCLASSPATH); WriteConfig(exported_jars, "/system/etc/classpaths/bootclasspath.pb"); exported_jars.clear_jars(); jar = exported_jars.add_jars(); jar->set_path("/apex/com.android.baz/javalib/baz"); jar->set_classpath(SYSTEMSERVERCLASSPATH); jar = exported_jars.add_jars(); jar->set_path("/apex/com.android.qux/javalib/qux"); jar->set_classpath(STANDALONE_SYSTEMSERVER_JARS); WriteConfig(exported_jars, "/system/etc/classpaths/systemserverclasspath.pb"); ASSERT_TRUE(GenerateClasspathExports(default_args_with_test_dir_)); const std::vector exportLines = ParseExportsFile(); std::vector splitExportLine; splitExportLine = SplitClasspathExportLine(exportLines[0]); EXPECT_EQ("BOOTCLASSPATH", splitExportLine[1]); EXPECT_EQ("/apex/com.android.foo/javalib/foo", splitExportLine[2]); splitExportLine = SplitClasspathExportLine(exportLines[1]); EXPECT_EQ("DEX2OATBOOTCLASSPATH", splitExportLine[1]); EXPECT_EQ("/apex/com.android.bar/javalib/bar", splitExportLine[2]); splitExportLine = SplitClasspathExportLine(exportLines[2]); EXPECT_EQ("SYSTEMSERVERCLASSPATH", splitExportLine[1]); EXPECT_EQ("/apex/com.android.baz/javalib/baz", splitExportLine[2]); splitExportLine = SplitClasspathExportLine(exportLines[3]); EXPECT_EQ("STANDALONE_SYSTEMSERVER_JARS", splitExportLine[1]); EXPECT_EQ("/apex/com.android.qux/javalib/qux", splitExportLine[2]); } // Test that temp directory does not pick up actual jars. TEST_F(DeriveClasspathTest, TempConfig) { AddJarToClasspath("/apex/com.android.foo", "/apex/com.android.foo/javalib/foo", BOOTCLASSPATH); AddJarToClasspath("/apex/com.android.baz", "/apex/com.android.baz/javalib/baz", SYSTEMSERVERCLASSPATH); ASSERT_TRUE(GenerateClasspathExports(default_args_with_test_dir_)); const std::vector exportLines = ParseExportsFile(); std::vector splitExportLine; splitExportLine = SplitClasspathExportLine(exportLines[0]); EXPECT_EQ("BOOTCLASSPATH", splitExportLine[1]); EXPECT_EQ("/apex/com.android.foo/javalib/foo", splitExportLine[2]); splitExportLine = SplitClasspathExportLine(exportLines[2]); EXPECT_EQ("SYSTEMSERVERCLASSPATH", splitExportLine[1]); EXPECT_EQ("/apex/com.android.baz/javalib/baz", splitExportLine[2]); } // Test individual modules are sorted by pathnames. TEST_F(DeriveClasspathTest, ModulesAreSorted) { AddJarToClasspath("/apex/com.android.art", "/apex/com.android.art/javalib/art", BOOTCLASSPATH); AddJarToClasspath("/system", "/system/framework/jar", BOOTCLASSPATH); AddJarToClasspath("/apex/com.android.foo", "/apex/com.android.foo/javalib/foo", BOOTCLASSPATH); AddJarToClasspath("/apex/com.android.bar", "/apex/com.android.bar/javalib/bar", BOOTCLASSPATH); AddJarToClasspath("/apex/com.android.baz", "/apex/com.android.baz/javalib/baz", BOOTCLASSPATH); ASSERT_TRUE(GenerateClasspathExports(default_args_with_test_dir_)); const std::vector exportLines = ParseExportsFile(); const std::vector splitExportLine = SplitClasspathExportLine(exportLines[0]); const std::string exportValue = splitExportLine[2]; const std::string expectedJars( "/apex/com.android.art/javalib/art" ":/system/framework/jar" ":/apex/com.android.bar/javalib/bar" ":/apex/com.android.baz/javalib/baz" ":/apex/com.android.foo/javalib/foo"); EXPECT_EQ(expectedJars, exportValue); } // Test we can output to custom files. TEST_F(DeriveClasspathTest, CustomOutputLocation) { AddJarToClasspath("/apex/com.android.art", "/apex/com.android.art/javalib/art", BOOTCLASSPATH); AddJarToClasspath("/system", "/system/framework/jar", BOOTCLASSPATH); AddJarToClasspath("/apex/com.android.foo", "/apex/com.android.foo/javalib/foo", BOOTCLASSPATH); AddJarToClasspath("/apex/com.android.bar", "/apex/com.android.bar/javalib/bar", BOOTCLASSPATH); AddJarToClasspath("/apex/com.android.baz", "/apex/com.android.baz/javalib/baz", BOOTCLASSPATH); android::base::unique_fd fd(memfd_create("temp_file", MFD_CLOEXEC)); ASSERT_TRUE(fd.ok()) << "Unable to open temp-file"; const std::string file_name = android::base::StringPrintf("/proc/self/fd/%d", fd.get()); Args args = { .output_path = file_name, .glob_pattern_prefix = working_dir(), }; ASSERT_TRUE(GenerateClasspathExports(args)); const std::vector exportLines = ParseExportsFile(file_name.c_str()); const std::vector splitExportLine = SplitClasspathExportLine(exportLines[0]); const std::string exportValue = splitExportLine[2]; const std::string expectedJars( "/apex/com.android.art/javalib/art" ":/system/framework/jar" ":/apex/com.android.bar/javalib/bar" ":/apex/com.android.baz/javalib/baz" ":/apex/com.android.foo/javalib/foo"); EXPECT_EQ(expectedJars, exportValue); } // Test alternative .pb for bootclasspath and systemclasspath. TEST_F(DeriveClasspathTest, CustomInputLocation) { AddJarToClasspath("/other", "/other/bcp-jar", BOOTCLASSPATH); AddJarToClasspath("/other", "/other/systemserver-jar", SYSTEMSERVERCLASSPATH); AddJarToClasspath("/apex/com.android.art", "/apex/com.android.art/javalib/art", BOOTCLASSPATH); AddJarToClasspath("/apex/com.android.foo", "/apex/com.android.foo/javalib/foo", BOOTCLASSPATH); AddJarToClasspath("/apex/com.android.baz", "/apex/com.android.baz/javalib/baz", SYSTEMSERVERCLASSPATH); Args args = default_args_with_test_dir_; args.system_bootclasspath_fragment = "/other/etc/classpaths/bootclasspath.pb"; args.system_systemserverclasspath_fragment = "/other/etc/classpaths/systemserverclasspath.pb"; ASSERT_TRUE(GenerateClasspathExports(args)); const std::vector exportLines = ParseExportsFile(); std::vector splitExportLine; splitExportLine = SplitClasspathExportLine(exportLines[0]); EXPECT_EQ("BOOTCLASSPATH", splitExportLine[1]); const std::string expectedBcpJars( "/apex/com.android.art/javalib/art" ":/other/bcp-jar" ":/apex/com.android.foo/javalib/foo"); EXPECT_EQ(expectedBcpJars, splitExportLine[2]); splitExportLine = SplitClasspathExportLine(exportLines[2]); EXPECT_EQ("SYSTEMSERVERCLASSPATH", splitExportLine[1]); const std::string expectedSystemServerJars( "/other/systemserver-jar" ":/apex/com.android.baz/javalib/baz"); EXPECT_EQ(expectedSystemServerJars, splitExportLine[2]); } // Test output location that can't be written to. TEST_F(DeriveClasspathTest, NonWriteableOutputLocation) { AddJarToClasspath("/apex/com.android.art", "/apex/com.android.art/javalib/art", BOOTCLASSPATH); AddJarToClasspath("/system", "/system/framework/jar", BOOTCLASSPATH); Args args = { .output_path = "/system/non_writable_path", .glob_pattern_prefix = working_dir(), }; ASSERT_FALSE(GenerateClasspathExports(args)); } TEST_F(DeriveClasspathTest, ScanOnlySpecificDirectories) { AddJarToClasspath("/system", "/system/framework/jar", BOOTCLASSPATH); AddJarToClasspath("/apex/com.android.foo", "/apex/com.android.foo/javalib/foo", BOOTCLASSPATH); AddJarToClasspath("/apex/com.android.foo", "/apex/com.android.foo/javalib/sys", SYSTEMSERVERCLASSPATH); AddJarToClasspath("/apex/com.android.bar", "/apex/com.android.bar/javalib/bar", BOOTCLASSPATH); AddJarToClasspath("/apex/com.android.baz", "/apex/com.android.baz/javalib/baz", BOOTCLASSPATH); auto args_with_scan_dirs = default_args_with_test_dir_; args_with_scan_dirs.scan_dirs.push_back("/apex/com.android.foo"); args_with_scan_dirs.scan_dirs.push_back("/apex/com.android.bar"); ASSERT_TRUE(GenerateClasspathExports(args_with_scan_dirs)); const std::vector exportLines = ParseExportsFile(); std::vector splitExportLine; splitExportLine = SplitClasspathExportLine(exportLines[0]); EXPECT_EQ("BOOTCLASSPATH", splitExportLine[1]); // Not sorted. Maintains the ordering provided in scan_dirs const std::string expectedJars( "/apex/com.android.foo/javalib/foo" ":/apex/com.android.bar/javalib/bar"); EXPECT_EQ(expectedJars, splitExportLine[2]); splitExportLine = SplitClasspathExportLine(exportLines[2]); EXPECT_EQ("SYSTEMSERVERCLASSPATH", splitExportLine[1]); EXPECT_EQ("/apex/com.android.foo/javalib/sys", splitExportLine[2]); } // Test apexes only export their own jars. TEST_F(DeriveClasspathDeathTest, ApexJarsBelongToApex) { // EXPECT_DEATH expects error messages in stderr, log there android::base::SetLogger(android::base::StderrLogger); AddJarToClasspath("/system", "/system/framework/jar", BOOTCLASSPATH); ASSERT_TRUE(GenerateClasspathExports(default_args_with_test_dir_)); AddJarToClasspath("/apex/com.android.foo", "/apex/com.android.foo/javalib/foo", BOOTCLASSPATH); ASSERT_TRUE(GenerateClasspathExports(default_args_with_test_dir_)); AddJarToClasspath("/apex/com.android.bar@12345.tmp", "/apex/com.android.bar/javalib/bar", BOOTCLASSPATH); ASSERT_TRUE(GenerateClasspathExports(default_args_with_test_dir_)); AddJarToClasspath("/apex/com.android.baz@12345", "/apex/this/path/is/skipped", BOOTCLASSPATH); ASSERT_TRUE(GenerateClasspathExports(default_args_with_test_dir_)); AddJarToClasspath("/apex/com.android.bar", "/apex/wrong/path/bar", BOOTCLASSPATH); EXPECT_DEATH(GenerateClasspathExports(default_args_with_test_dir_), "must not export a jar.*wrong/path/bar"); } // Test only bind mounted apexes are skipped TEST_F(DeriveClasspathTest, OnlyBindMountedApexIsSkipped) { AddJarToClasspath("/system", "/system/framework/jar", BOOTCLASSPATH); // Normal APEX with format: /apex//* AddJarToClasspath("/apex/com.android.foo", "/apex/com.android.foo/javalib/foo", BOOTCLASSPATH); // Bind mounted APEX with format: /apex/@/* AddJarToClasspath("/apex/com.android.bar@123", "/apex/com.android.bar/javalib/bar", BOOTCLASSPATH); // Temp mounted APEX with format: /apex/@.tmp/* AddJarToClasspath("/apex/com.android.baz@123.tmp", "/apex/com.android.baz/javalib/baz", BOOTCLASSPATH); ASSERT_TRUE(GenerateClasspathExports(default_args_with_test_dir_)); const std::vector exportLines = ParseExportsFile(); std::vector splitExportLine; splitExportLine = SplitClasspathExportLine(exportLines[0]); EXPECT_EQ("BOOTCLASSPATH", splitExportLine[1]); const std::string expectedJars( "/system/framework/jar" ":/apex/com.android.baz/javalib/baz" ":/apex/com.android.foo/javalib/foo"); EXPECT_EQ(expectedJars, splitExportLine[2]); } // Test classpath fragments export jars for themselves. TEST_F(DeriveClasspathDeathTest, WrongClasspathInFragments) { // Valid configs AddJarToClasspath("/system", "/system/framework/framework-jar", BOOTCLASSPATH); AddJarToClasspath("/system", "/system/framework/service-jar", SYSTEMSERVERCLASSPATH); // Manually create an invalid config with both BCP and SSCP jars... ExportedClasspathsJars exported_jars; Jar* jar = exported_jars.add_jars(); jar->set_path("/apex/com.android.foo/javalib/foo"); jar->set_classpath(BOOTCLASSPATH); // note that DEX2OATBOOTCLASSPATH and BOOTCLASSPATH jars are expected to be in the same config jar = exported_jars.add_jars(); jar->set_path("/apex/com.android.foo/javalib/foo"); jar->set_classpath(DEX2OATBOOTCLASSPATH); jar = exported_jars.add_jars(); jar->set_path("/apex/com.android.foo/javalib/service-foo"); jar->set_classpath(SYSTEMSERVERCLASSPATH); // ...and write this config to bootclasspath.pb WriteConfig(exported_jars, "/apex/com.android.foo/etc/classpaths/bootclasspath.pb"); EXPECT_DEATH(GenerateClasspathExports(default_args_with_test_dir_), "must not export a jar for SYSTEMSERVERCLASSPATH"); } TEST_F(DeriveClasspathDeathTest, CurrentSdkVersion) { if (android_get_device_api_level() < __ANDROID_API_S__) { GTEST_SKIP(); } ExportedClasspathsJars exported_jars; Jar* jar = exported_jars.add_jars(); jar->set_path("/apex/com.android.foo/javalib/minsdkcurrent"); jar->set_min_sdk_version("current"); jar->set_classpath(SYSTEMSERVERCLASSPATH); WriteConfig(exported_jars, "/apex/com.android.foo/etc/classpaths/systemserverclasspath.pb"); EXPECT_DEATH(GenerateClasspathExports(default_args_with_test_dir_), "no conversion"); } // Test jars with different sdk versions. TEST_F(DeriveClasspathTest, SdkVersionsAreRespected) { if (android_get_device_api_level() < __ANDROID_API_S__) { GTEST_SKIP(); } // List of jars expected to be in SYSTEMSERVERCLASSPATH std::vector expected_jars; // Add an unbounded jar AddJarToClasspath("/system", "/system/framework/unbounded", SYSTEMSERVERCLASSPATH); expected_jars.push_back("/system/framework/unbounded"); // Manually create a config with jars that set sdk versions... ExportedClasspathsJars exported_jars; // known released versions: Jar* jar = exported_jars.add_jars(); jar->set_path("/apex/com.android.foo/javalib/minsdk30"); jar->set_min_sdk_version(std::to_string(__ANDROID_API_R__)); jar->set_classpath(SYSTEMSERVERCLASSPATH); expected_jars.push_back("/apex/com.android.foo/javalib/minsdk30"); jar = exported_jars.add_jars(); jar->set_path("/apex/com.android.foo/javalib/maxsdk30"); jar->set_max_sdk_version(std::to_string(__ANDROID_API_R__)); jar->set_classpath(SYSTEMSERVERCLASSPATH); // Device's reported version: jar = exported_jars.add_jars(); jar->set_path("/apex/com.android.foo/javalib/minsdklatest"); jar->set_min_sdk_version(std::to_string(android_get_device_api_level())); jar->set_classpath(SYSTEMSERVERCLASSPATH); expected_jars.push_back("/apex/com.android.foo/javalib/minsdklatest"); jar = exported_jars.add_jars(); jar->set_path("/apex/com.android.foo/javalib/maxsdklatest"); jar->set_max_sdk_version(std::to_string(android_get_device_api_level())); jar->set_classpath(SYSTEMSERVERCLASSPATH); if ("REL" == android::base::GetProperty("ro.build.version.codename", "")) { expected_jars.push_back("/apex/com.android.foo/javalib/maxsdklatest"); } // unknown SDK_INT+1 version jar = exported_jars.add_jars(); jar->set_path("/apex/com.android.foo/javalib/minsdk_plus1"); jar->set_min_sdk_version(std::to_string(android_get_device_api_level() + 1)); jar->set_classpath(SYSTEMSERVERCLASSPATH); jar = exported_jars.add_jars(); jar->set_path("/apex/com.android.foo/javalib/maxsdk_plus1"); jar->set_max_sdk_version(std::to_string(android_get_device_api_level() + 1)); jar->set_classpath(SYSTEMSERVERCLASSPATH); expected_jars.push_back("/apex/com.android.foo/javalib/maxsdk_plus1"); // known min_sdk_version and future max_sdk_version jar = exported_jars.add_jars(); jar->set_path("/apex/com.android.foo/javalib/minsdk30maxsdk10000"); jar->set_min_sdk_version(std::to_string(__ANDROID_API_R__)); jar->set_max_sdk_version(std::to_string(android_get_device_api_level() + 1)); jar->set_classpath(SYSTEMSERVERCLASSPATH); expected_jars.push_back("/apex/com.android.foo/javalib/minsdk30maxsdk10000"); // codename if ("REL" != android::base::GetProperty("ro.build.version.codename", "")) { jar = exported_jars.add_jars(); jar->set_path("/apex/com.android.foo/javalib/minsdkS"); jar->set_min_sdk_version("S"); jar->set_classpath(SYSTEMSERVERCLASSPATH); expected_jars.push_back("/apex/com.android.foo/javalib/minsdkS"); jar = exported_jars.add_jars(); jar->set_path("/apex/com.android.foo/javalib/minsdkSv2"); jar->set_min_sdk_version("Sv2"); jar->set_classpath(SYSTEMSERVERCLASSPATH); expected_jars.push_back("/apex/com.android.foo/javalib/minsdkSv2"); jar = exported_jars.add_jars(); jar->set_path("/apex/com.android.foo/javalib/minsdkTiramisu"); jar->set_min_sdk_version("Tiramisu"); jar->set_classpath(SYSTEMSERVERCLASSPATH); expected_jars.push_back("/apex/com.android.foo/javalib/minsdkTiramisu"); jar = exported_jars.add_jars(); jar->set_path("/apex/com.android.foo/javalib/maxsdkS"); jar->set_max_sdk_version("S"); jar->set_classpath(SYSTEMSERVERCLASSPATH); jar = exported_jars.add_jars(); jar->set_path("/apex/com.android.foo/javalib/maxsdkSv2"); jar->set_max_sdk_version("Sv2"); jar->set_classpath(SYSTEMSERVERCLASSPATH); jar = exported_jars.add_jars(); jar->set_path("/apex/com.android.foo/javalib/maxsdkZFutureSdkVersion"); jar->set_max_sdk_version("ZFutureSdkVersion"); jar->set_classpath(SYSTEMSERVERCLASSPATH); expected_jars.push_back("/apex/com.android.foo/javalib/maxsdkZFutureSdkVersion"); } // ...and write this config to systemserverclasspath.pb WriteConfig(exported_jars, "/apex/com.android.foo/etc/classpaths/systemserverclasspath.pb"); // Generate and parse SYSTEMSERVERCLASSPATH GenerateClasspathExports(default_args_with_test_dir_); const std::vector exportLines = ParseExportsFile(); const std::vector splitExportLine = SplitClasspathExportLine(exportLines[2]); const std::string exportValue = splitExportLine[2]; EXPECT_EQ(android::base::Join(expected_jars, ":"), exportValue); } } // namespace } // namespace derive_classpath } // namespace android int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); // Required for EXPECT_DEATH to work correctly android::base::SetLogger(android::base::StderrLogger); return RUN_ALL_TESTS(); }