Loading core/java/com/android/internal/content/om/OverlayConfigParser.java +98 −3 Original line number Diff line number Diff line Loading @@ -24,6 +24,8 @@ import android.content.pm.PackagePartitions; import android.content.pm.PackagePartitions.SystemPartition; import android.os.Build; import android.os.FileUtils; import android.os.SystemProperties; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; import android.util.Xml; Loading Loading @@ -241,6 +243,18 @@ public final class OverlayConfigParser { } } @FunctionalInterface public interface SysPropWrapper{ /** * Get system property * * @param property the key to look up. * * @return The property value if found, empty string otherwise. */ String get(String property); } /** * Retrieves overlays configured within the partition in increasing priority order. * Loading Loading @@ -319,6 +333,76 @@ public final class OverlayConfigParser { } } /** * Expand the property inside a rro configuration path. * * A RRO configuration can contain a property, this method expands * the property to its value. * * Only read only properties allowed, prefixed with ro. Other * properties will raise exception. * * Only a single property in the path is allowed. * * Example "${ro.boot.hardware.sku}/config.xml" would expand to * "G020N/config.xml" * * @param configPath path to expand * @param sysPropWrapper method used for reading properties * * @return The expanded path. Returns null if configPath is null. */ @VisibleForTesting public static String expandProperty(String configPath, SysPropWrapper sysPropWrapper) { if (configPath == null) { return null; } int propStartPos = configPath.indexOf("${"); if (propStartPos == -1) { // No properties inside the string, return as is return configPath; } final StringBuilder sb = new StringBuilder(); sb.append(configPath.substring(0, propStartPos)); // Read out the end position int propEndPos = configPath.indexOf("}", propStartPos); if (propEndPos == -1) { throw new IllegalStateException("Malformed property, unmatched braces, in: " + configPath); } // Confirm that there is only one property inside the string if (configPath.indexOf("${", propStartPos + 2) != -1) { throw new IllegalStateException("Only a single property supported in path: " + configPath); } final String propertyName = configPath.substring(propStartPos + 2, propEndPos); if (!propertyName.startsWith("ro.")) { throw new IllegalStateException("Only read only properties can be used when " + "merging RRO config files: " + propertyName); } final String propertyValue = sysPropWrapper.get(propertyName); if (TextUtils.isEmpty(propertyValue)) { throw new IllegalStateException("Property is empty or doesn't exist: " + propertyName); } Log.d(TAG, String.format("Using property in overlay config path: \"%s\"", propertyName)); sb.append(propertyValue); // propEndPos points to '}', need to step to next character, might be outside of string propEndPos = propEndPos + 1; // Append the remainder, if exists if (propEndPos < configPath.length()) { sb.append(configPath.substring(propEndPos)); } return sb.toString(); } /** * Parses a <merge> tag within an overlay configuration file. * Loading @@ -331,10 +415,21 @@ public final class OverlayConfigParser { @Nullable OverlayScanner scanner, @Nullable Map<String, ParsedOverlayInfo> packageManagerOverlayInfos, @NonNull ParsingContext parsingContext) { final String path = parser.getAttributeValue(null, "path"); final String path; try { SysPropWrapper sysPropWrapper = p -> { return SystemProperties.get(p, ""); }; path = expandProperty(parser.getAttributeValue(null, "path"), sysPropWrapper); } catch (IllegalStateException e) { throw new IllegalStateException(String.format("<merge> path expand error in %s at %s", configFile, parser.getPositionDescription()), e); } if (path == null) { throw new IllegalStateException(String.format("<merge> without path in %s at %s" + configFile, parser.getPositionDescription())); throw new IllegalStateException(String.format("<merge> without path in %s at %s", configFile, parser.getPositionDescription())); } if (path.startsWith("/")) { Loading core/tests/coretests/src/com/android/internal/content/res/OverlayConfigParserTest.java 0 → 100644 +119 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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. */ package com.android.internal.content.res; import static com.android.internal.content.om.OverlayConfigParser.SysPropWrapper; import static org.junit.Assert.assertEquals; import android.platform.test.annotations.Presubmit; import androidx.test.runner.AndroidJUnit4; import com.android.internal.content.om.OverlayConfigParser; import org.junit.Test; import org.junit.runner.RunWith; @Presubmit @RunWith(AndroidJUnit4.class) public class OverlayConfigParserTest { @Test(expected = IllegalStateException.class) public void testMergePropNotRoProp() { SysPropWrapper sysProp = p -> { return "dummy_value"; }; OverlayConfigParser.expandProperty("${persist.value}/path", sysProp); } @Test(expected = IllegalStateException.class) public void testMergePropMissingEndBracket() { SysPropWrapper sysProp = p -> { return "dummy_value"; }; OverlayConfigParser.expandProperty("${ro.value/path", sysProp); } @Test(expected = IllegalStateException.class) public void testMergeOnlyPropStart() { SysPropWrapper sysProp = p -> { return "dummy_value"; }; OverlayConfigParser.expandProperty("path/${", sysProp); } @Test(expected = IllegalStateException.class) public void testMergePropInProp() { SysPropWrapper sysProp = p -> { return "dummy_value"; }; OverlayConfigParser.expandProperty("path/${${ro.value}}", sysProp); } /** * The path is only allowed to contain one property. */ @Test(expected = IllegalStateException.class) public void testMergePropMultipleProps() { SysPropWrapper sysProp = p -> { return "dummy_value"; }; OverlayConfigParser.expandProperty("${ro.value}/path${ro.value2}/path", sysProp); } @Test public void testMergePropOneProp() { final SysPropWrapper sysProp = p -> { if ("ro.value".equals(p)) { return "dummy_value"; } else { return "invalid"; } }; // Property in the beginnig of the string String result = OverlayConfigParser.expandProperty("${ro.value}/path", sysProp); assertEquals("dummy_value/path", result); // Property in the middle of the string result = OverlayConfigParser.expandProperty("path/${ro.value}/file", sysProp); assertEquals("path/dummy_value/file", result); // Property at the of the string result = OverlayConfigParser.expandProperty("path/${ro.value}", sysProp); assertEquals("path/dummy_value", result); // Property is the entire string result = OverlayConfigParser.expandProperty("${ro.value}", sysProp); assertEquals("dummy_value", result); } @Test public void testMergePropNoProp() { final SysPropWrapper sysProp = p -> { return "dummy_value"; }; final String path = "no_props/path"; String result = OverlayConfigParser.expandProperty(path, sysProp); assertEquals(path, result); } } Loading
core/java/com/android/internal/content/om/OverlayConfigParser.java +98 −3 Original line number Diff line number Diff line Loading @@ -24,6 +24,8 @@ import android.content.pm.PackagePartitions; import android.content.pm.PackagePartitions.SystemPartition; import android.os.Build; import android.os.FileUtils; import android.os.SystemProperties; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; import android.util.Xml; Loading Loading @@ -241,6 +243,18 @@ public final class OverlayConfigParser { } } @FunctionalInterface public interface SysPropWrapper{ /** * Get system property * * @param property the key to look up. * * @return The property value if found, empty string otherwise. */ String get(String property); } /** * Retrieves overlays configured within the partition in increasing priority order. * Loading Loading @@ -319,6 +333,76 @@ public final class OverlayConfigParser { } } /** * Expand the property inside a rro configuration path. * * A RRO configuration can contain a property, this method expands * the property to its value. * * Only read only properties allowed, prefixed with ro. Other * properties will raise exception. * * Only a single property in the path is allowed. * * Example "${ro.boot.hardware.sku}/config.xml" would expand to * "G020N/config.xml" * * @param configPath path to expand * @param sysPropWrapper method used for reading properties * * @return The expanded path. Returns null if configPath is null. */ @VisibleForTesting public static String expandProperty(String configPath, SysPropWrapper sysPropWrapper) { if (configPath == null) { return null; } int propStartPos = configPath.indexOf("${"); if (propStartPos == -1) { // No properties inside the string, return as is return configPath; } final StringBuilder sb = new StringBuilder(); sb.append(configPath.substring(0, propStartPos)); // Read out the end position int propEndPos = configPath.indexOf("}", propStartPos); if (propEndPos == -1) { throw new IllegalStateException("Malformed property, unmatched braces, in: " + configPath); } // Confirm that there is only one property inside the string if (configPath.indexOf("${", propStartPos + 2) != -1) { throw new IllegalStateException("Only a single property supported in path: " + configPath); } final String propertyName = configPath.substring(propStartPos + 2, propEndPos); if (!propertyName.startsWith("ro.")) { throw new IllegalStateException("Only read only properties can be used when " + "merging RRO config files: " + propertyName); } final String propertyValue = sysPropWrapper.get(propertyName); if (TextUtils.isEmpty(propertyValue)) { throw new IllegalStateException("Property is empty or doesn't exist: " + propertyName); } Log.d(TAG, String.format("Using property in overlay config path: \"%s\"", propertyName)); sb.append(propertyValue); // propEndPos points to '}', need to step to next character, might be outside of string propEndPos = propEndPos + 1; // Append the remainder, if exists if (propEndPos < configPath.length()) { sb.append(configPath.substring(propEndPos)); } return sb.toString(); } /** * Parses a <merge> tag within an overlay configuration file. * Loading @@ -331,10 +415,21 @@ public final class OverlayConfigParser { @Nullable OverlayScanner scanner, @Nullable Map<String, ParsedOverlayInfo> packageManagerOverlayInfos, @NonNull ParsingContext parsingContext) { final String path = parser.getAttributeValue(null, "path"); final String path; try { SysPropWrapper sysPropWrapper = p -> { return SystemProperties.get(p, ""); }; path = expandProperty(parser.getAttributeValue(null, "path"), sysPropWrapper); } catch (IllegalStateException e) { throw new IllegalStateException(String.format("<merge> path expand error in %s at %s", configFile, parser.getPositionDescription()), e); } if (path == null) { throw new IllegalStateException(String.format("<merge> without path in %s at %s" + configFile, parser.getPositionDescription())); throw new IllegalStateException(String.format("<merge> without path in %s at %s", configFile, parser.getPositionDescription())); } if (path.startsWith("/")) { Loading
core/tests/coretests/src/com/android/internal/content/res/OverlayConfigParserTest.java 0 → 100644 +119 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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. */ package com.android.internal.content.res; import static com.android.internal.content.om.OverlayConfigParser.SysPropWrapper; import static org.junit.Assert.assertEquals; import android.platform.test.annotations.Presubmit; import androidx.test.runner.AndroidJUnit4; import com.android.internal.content.om.OverlayConfigParser; import org.junit.Test; import org.junit.runner.RunWith; @Presubmit @RunWith(AndroidJUnit4.class) public class OverlayConfigParserTest { @Test(expected = IllegalStateException.class) public void testMergePropNotRoProp() { SysPropWrapper sysProp = p -> { return "dummy_value"; }; OverlayConfigParser.expandProperty("${persist.value}/path", sysProp); } @Test(expected = IllegalStateException.class) public void testMergePropMissingEndBracket() { SysPropWrapper sysProp = p -> { return "dummy_value"; }; OverlayConfigParser.expandProperty("${ro.value/path", sysProp); } @Test(expected = IllegalStateException.class) public void testMergeOnlyPropStart() { SysPropWrapper sysProp = p -> { return "dummy_value"; }; OverlayConfigParser.expandProperty("path/${", sysProp); } @Test(expected = IllegalStateException.class) public void testMergePropInProp() { SysPropWrapper sysProp = p -> { return "dummy_value"; }; OverlayConfigParser.expandProperty("path/${${ro.value}}", sysProp); } /** * The path is only allowed to contain one property. */ @Test(expected = IllegalStateException.class) public void testMergePropMultipleProps() { SysPropWrapper sysProp = p -> { return "dummy_value"; }; OverlayConfigParser.expandProperty("${ro.value}/path${ro.value2}/path", sysProp); } @Test public void testMergePropOneProp() { final SysPropWrapper sysProp = p -> { if ("ro.value".equals(p)) { return "dummy_value"; } else { return "invalid"; } }; // Property in the beginnig of the string String result = OverlayConfigParser.expandProperty("${ro.value}/path", sysProp); assertEquals("dummy_value/path", result); // Property in the middle of the string result = OverlayConfigParser.expandProperty("path/${ro.value}/file", sysProp); assertEquals("path/dummy_value/file", result); // Property at the of the string result = OverlayConfigParser.expandProperty("path/${ro.value}", sysProp); assertEquals("path/dummy_value", result); // Property is the entire string result = OverlayConfigParser.expandProperty("${ro.value}", sysProp); assertEquals("dummy_value", result); } @Test public void testMergePropNoProp() { final SysPropWrapper sysProp = p -> { return "dummy_value"; }; final String path = "no_props/path"; String result = OverlayConfigParser.expandProperty(path, sysProp); assertEquals(path, result); } }