diff --git a/adapter/ohos/BundleException.java b/adapter/ohos/BundleException.java index ea3911857c1de5e20c0e61c027c66cf9a64c70d2..cc332c43e19a7f5486c383978b2e9db81f15b2ac 100644 --- a/adapter/ohos/BundleException.java +++ b/adapter/ohos/BundleException.java @@ -19,7 +19,7 @@ package ohos; * bundle tool exception class. * */ -class BundleException extends Exception { +public class BundleException extends Exception { private static final long serialVersionUID = 1813070042705457755L; /** diff --git a/adapter/ohos/JsonUtil.java b/adapter/ohos/JsonUtil.java index 74ba0f653652ab34b988f3d7a6376656c41e607b..f786da9b2040baa995bedde8cb3d93948de56b69 100644 --- a/adapter/ohos/JsonUtil.java +++ b/adapter/ohos/JsonUtil.java @@ -25,6 +25,8 @@ import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONException; +import ohos.restool.ResourcesParserFactory; +import ohos.restool.ResourcesParserV2; /** * Json Util. @@ -392,7 +394,7 @@ public class JsonUtil { String labelRes = ""; if (appJson.containsKey("labelId")) { int labelId = appJson.getIntValue("labelId"); - labelRes = ResourcesParser.getBaseResourceById(labelId, data); + labelRes = ResourcesParserFactory.createParser(data).getBaseResourceById(labelId, data); } if (labelRes != null && !labelRes.isEmpty()) { appInfo.appName = labelRes; @@ -818,7 +820,7 @@ public class JsonUtil { ability.name = getJsonString(abilityJson, "name"); if (abilityJson.containsKey("iconId")) { int iconId = abilityJson.getIntValue("iconId"); - String iconPath = ResourcesParser.getResourceById(iconId, data); + String iconPath = ResourcesParserFactory.createParser(data).getResourceById(iconId, data); if (iconPath != null && !iconPath.isEmpty()) { ability.iconPath = ASSETS_DIR_NAME + iconPath; } @@ -828,7 +830,7 @@ public class JsonUtil { if (abilityJson.containsKey("descriptionId")) { int descriptionId = abilityJson.getIntValue("descriptionId"); - ability.descriptionRes = ResourcesParser.getBaseResourceById(descriptionId, data); + ability.descriptionRes = ResourcesParserFactory.createParser(data).getBaseResourceById(descriptionId, data); } ability.description = ability.descriptionRes != null && !ability.descriptionRes.isEmpty() ? ability.descriptionRes : getJsonString(abilityJson, "description"); @@ -836,7 +838,7 @@ public class JsonUtil { if (abilityJson.containsKey("labelId")) { int labelId = abilityJson.getIntValue("labelId"); - ability.labelRes = ResourcesParser.getBaseResourceById(labelId, data); + ability.labelRes = ResourcesParserFactory.createParser(data).getBaseResourceById(labelId, data); } if (ability.labelRes != null && !ability.labelRes.isEmpty()) { ability.label = ability.labelRes; @@ -1782,7 +1784,7 @@ public class JsonUtil { String descriptionId = descriptionStr.substring(len); try { int id = Integer.parseInt(descriptionId); - descriptions = ResourcesParser.getResourceMapById(id, data); + descriptions = ResourcesParserFactory.createParser(data).getResourceMapById(id, data); } catch (NumberFormatException e) { LOG.error("parseFormDescriptions failed: invalid descriptionId: " + descriptionId); } @@ -1935,7 +1937,7 @@ public class JsonUtil { } try { int finalId = Integer.parseInt(id.substring(index)); - res = ResourcesParser.getResourceStringById(finalId, data); + res = ResourcesParserFactory.createParser(data).getResourceStringById(finalId, data); } catch (NumberFormatException e) { LOG.error("parseResourceByStringID failed: input invalid of " + id + "."); } @@ -1955,7 +1957,7 @@ public class JsonUtil { String res = ""; if (jsonObject.containsKey(keyId)) { int resId = jsonObject.getIntValue(keyId); - res = ResourcesParser.getResourceStringById(resId, data); + res = ResourcesParserFactory.createParser(data).getResourceStringById(resId, data); } if (res != null && !res.isEmpty()) { return res; @@ -1970,7 +1972,7 @@ public class JsonUtil { HashMap map = new HashMap<>(); if (jsonObject.containsKey(keyId)) { int resId = jsonObject.getIntValue(keyId); - map = ResourcesParser.getResourceMapById(resId, data); + map = ResourcesParserFactory.createParser(data).getResourceMapById(resId, data); } return map; } @@ -1987,9 +1989,9 @@ public class JsonUtil { String iconPath = ""; if (jsonObject.containsKey("iconId")) { int resId = jsonObject.getIntValue("iconId"); - iconPath = ResourcesParser.getBaseResourceById(resId, data); + iconPath = ResourcesParserFactory.createParser(data).getBaseResourceById(resId, data); if (iconPath.isEmpty()) { - iconPath = ResourcesParser.getResourceById(resId, data); + iconPath = ResourcesParserFactory.createParser(data).getResourceById(resId, data); } if (iconPath.contains("resources")) { iconPath = iconPath.substring(iconPath.lastIndexOf("resources")); diff --git a/adapter/ohos/Uncompress.java b/adapter/ohos/Uncompress.java index 129d343b37f7a25114a67e0ca633170f24e1e459..5dd352a0105f5f47be5ba94b7dbcb5a62843c5b1 100644 --- a/adapter/ohos/Uncompress.java +++ b/adapter/ohos/Uncompress.java @@ -15,6 +15,9 @@ package ohos; +import ohos.restool.ResourcesParserFactory; +import ohos.restool.ResourcesParserV2; + import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; @@ -1803,7 +1806,7 @@ public class Uncompress { File srcFile = new File(srcPath); zipFile = new ZipFile(srcFile); byte[] data = getResourceDataFromHap(zipFile); - return ResourcesParser.getAllDataItem(data); + return ResourcesParserFactory.createParser(data).getAllDataItem(data); } finally { Utility.closeStream(zipFile); } diff --git a/adapter/ohos/restool/ResourcesParser.java b/adapter/ohos/restool/ResourcesParser.java new file mode 100644 index 0000000000000000000000000000000000000000..41f7c3019d8a0fed70cd4055c1082c6a77993f33 --- /dev/null +++ b/adapter/ohos/restool/ResourcesParser.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * 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 ohos.restool; + +import ohos.BundleException; +import ohos.ResourceIndexResult; + +import java.util.HashMap; +import java.util.List; + +/** + * Resources Parser Interface. + * + * @since 2025-06-06 + */ +public interface ResourcesParser { + /** + * Get resource value by resource id. + * + * @param resourceId resource id + * @param data resource index data + * @return the resource value + * @throws BundleException IOException. + */ + String getResourceById(int resourceId, byte[] data) throws BundleException; + + /** + * Get base resource value by resource id. + * + * @param resourceId resource id + * @param data resource index data + * @return the resource value + * @throws BundleException IOException. + */ + String getBaseResourceById(int resourceId, byte[] data) throws BundleException; + + /** + * Read all config item. + * + * @param data config byte buffer + * @return the item info. + */ + List getAllDataItem(byte[] data); + + /** + * Read resource map by id. + * + * @param resId The resource ID to query + * @param data config byte buffer + * @return the resource map of id. + */ + HashMap getResourceMapById(int resId, byte[] data); + + /** + * Gets the resource string value by ID + * + * @param resId The resource ID to query + * @param data The resource data buffer + * @return The string value of the resource + */ + String getResourceStringById(int resId, byte[] data); +} diff --git a/adapter/ohos/restool/ResourcesParserFactory.java b/adapter/ohos/restool/ResourcesParserFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..5ea2e50785a34e736436df1e6fc1f83f20e96fea --- /dev/null +++ b/adapter/ohos/restool/ResourcesParserFactory.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * 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 ohos.restool; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; + +/** + * ResourcesParserFactory + * + * @since 2025-06-06 + */ +public class ResourcesParserFactory { + private static final int VERSION_BYTE_LENGTH = 128; + private static final String RESOURCE_PROTOCOL_VERSION_TAG = "RestoolV2"; + + /** + * createParser. + * + * @param data resource index data + * @return ResourcesParser + */ + public static ResourcesParser createParser(byte[] data) { + if (isV2Protocol(data)) { + return new ResourcesParserV2(data); + } + return new ResourcesParserV1(); + } + + /** + * Is V2 protocol. + * + * @param data resource index data + * @return is V2 + */ + private static boolean isV2Protocol(byte[] data) { + ByteBuffer byteBuf = ByteBuffer.wrap(data); + byteBuf.order(ByteOrder.LITTLE_ENDIAN); + byte[] version = new byte[VERSION_BYTE_LENGTH]; + byteBuf.get(version); + String versionStr = new String(version, StandardCharsets.UTF_8); + return versionStr.contains(RESOURCE_PROTOCOL_VERSION_TAG); + } +} diff --git a/adapter/ohos/ResourcesParser.java b/adapter/ohos/restool/ResourcesParserV1.java similarity index 85% rename from adapter/ohos/ResourcesParser.java rename to adapter/ohos/restool/ResourcesParserV1.java index 825458568573125e79fbb2bb4b6541dcc4b9678e..3a1208cdef971d55c900d59337c3ace671ef9d6d 100644 --- a/adapter/ohos/ResourcesParser.java +++ b/adapter/ohos/restool/ResourcesParserV1.java @@ -13,26 +13,32 @@ * limitations under the License. */ -package ohos; +package ohos.restool; + +import ohos.BundleException; +import ohos.Log; +import ohos.ResourceIndexResult; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.List; import java.util.HashMap; +import java.util.List; import java.util.Optional; /** * Resources Parser. * + * @since 2021-03-11 */ -public class ResourcesParser { +public class ResourcesParserV1 implements ResourcesParser { /** * Parses resources default id. */ public static final int RESOURCE_DEFAULT_ID = -1; + private static final Log LOG = new Log(ResourcesParserV1.class.toString()); private static final int VERSION_BYTE_LENGTH = 128; private static final int TAG_BYTE_LENGTH = 4; private static final String LEFT_BRACKET = "<"; @@ -48,30 +54,31 @@ public class ResourcesParser { private static final String BASE = "base"; private static final int CHAR_LENGTH = 1; private static final String EMPTY_STRING = ""; + private enum ResType { - Values(0, "Values"), - Animator(1, "Animator"), - Drawable(2, "Drawable"), - Layout(3, "Layout"), - Menu(4, "Menu"), - Mipmap(5, "Mipmap"), - Raw(6, "Raw"), - Xml(7, "Xml"), - Integer(8, "Integer"), - String(9, "String"), - StrArray(10, "StrArray"), - IntArray(11, "IntArray"), - Boolean(12, "Boolean"), - Dimen(13, "Dimen"), - Color(14, "Color"), - Id(15, "Id"), - Theme(16, "Theme"), - Plurals(17, "Plurals"), - Float(18, "Float"), - Media(19, "Media"), - Prof(20, "Prof"), - Svg(21, "Svg"), - Pattern(22, "Pattern"); + VALUES(0, "Values"), + ANIMATOR(1, "Animator"), + DRAWABLE(2, "Drawable"), + LAYOUT(3, "Layout"), + MENU(4, "Menu"), + MIPMAP(5, "Mipmap"), + RAW(6, "Raw"), + XML(7, "Xml"), + INTEGER(8, "Integer"), + STRING(9, "String"), + STR_ARRAY(10, "StrArray"), + INT_ARRAY(11, "IntArray"), + BOOLEAN(12, "Boolean"), + DIMEN(13, "Dimen"), + COLOR(14, "Color"), + ID(15, "Id"), + THEME(16, "Theme"), + PLURALS(17, "Plurals"), + FLOAT(18, "Float"), + MEDIA(19, "Media"), + PROF(20, "Prof"), + SVG(21, "Svg"), + PATTERN(22, "Pattern"); private final int index; private final String type; @@ -99,15 +106,15 @@ public class ResourcesParser { } private enum DeviceType { - Phone(0, "phone"), - Tablet(1, "tablet"), - Car(2, "car"), - Pc(3, "pc"), - Tv(4, "tv"), - Speaker(5, "speaker"), - Wearable(6, "wearable"), - Glasses(7, "glasses"), - Headset(8, "headset"); + PHONE(0, "phone"), + TABLET(1, "tablet"), + CAR(2, "car"), + PC(3, "pc"), + TV(4, "tv"), + SPEAKER(5, "speaker"), + WEARABLE(6, "wearable"), + GLASSES(7, "glasses"), + HEADSET(8, "headset"); private final int index; private final String type; @@ -134,15 +141,15 @@ public class ResourcesParser { } private enum Resolution { - Nodpi(-2, "nodpi"), - Anydpi(-1, "anydpi"), - Sdpi(120, "sdpi"), - Mdpi(160, "mdpi"), - Tvdpi(213, "tvdpi"), - Ldpi(240, "ldpi"), - Xldpi(320, "xldpi"), - Xxldpi(480, "xxldpi"), - Xxxldpi(640, "xxxldpi"); + NODPI(-2, "nodpi"), + ANYDPI(-1, "anydpi"), + SDPI(120, "sdpi"), + MDPI(160, "mdpi"), + TVDPI(213, "tvdpi"), + LDPI(240, "ldpi"), + XLDPI(320, "xldpi"), + XXLDPI(480, "xxldpi"), + XXXLDPI(640, "xxxldpi"); private final int index; private final String type; @@ -168,11 +175,17 @@ public class ResourcesParser { } private enum ConfigType { - Language, Region, Resolution, Direction, DeviceType, Script, LightMode, MCC, MNC + LANGUAGE, + REGION, + RESOLUTION, + DIRECTION, + DEVICE_TYPE, + SCRIPT, + LIGHT_MODE, + MCC, + MNC } - private static final Log LOG = new Log(ResourcesParser.class.toString()); - /** * Key Param. */ @@ -210,7 +223,7 @@ public class ResourcesParser { * @return the resourceId value * @throws BundleException IOException. */ - static String getResourceById(int resourceId, byte[] data) throws BundleException { + public String getResourceById(int resourceId, byte[] data) throws BundleException { String resourceIdValue = ""; if (data == null || data.length <= 0 || resourceId == RESOURCE_DEFAULT_ID) { LOG.error("ResourcesParser::getIconPath data byte or ResourceId is null"); @@ -232,7 +245,7 @@ public class ResourcesParser { * @return the resource value * @throws BundleException IOException. */ - static String getBaseResourceById(int resourceId, byte[] data) throws BundleException { + public String getBaseResourceById(int resourceId, byte[] data) throws BundleException { String resourceIdValue = ""; if (data == null || data.length <= 0 || resourceId == RESOURCE_DEFAULT_ID) { LOG.error("ResourcesParser::getBaseResourceById data byte or ResourceId is null"); @@ -276,7 +289,7 @@ public class ResourcesParser { * @return the base config index * @throws BundleException IOException. */ - static Optional loadBaseConfig(ByteBuffer bufBuf, int count) { + static Optional loadBaseConfig(ByteBuffer bufBuf, int count) { for (int i = 0; i < count; i++) { ConfigIndex cfg = new ConfigIndex(); byte[] tag = new byte[TAG_BYTE_LENGTH]; @@ -433,7 +446,7 @@ public class ResourcesParser { * @param data config byte buffer * @return the item info. */ - static List getAllDataItem(byte[] data) { + public List getAllDataItem(byte[] data) { ByteBuffer byteBuf = ByteBuffer.wrap(data); byteBuf.order(ByteOrder.LITTLE_ENDIAN); byte[] version = new byte[VERSION_BYTE_LENGTH]; @@ -447,10 +460,11 @@ public class ResourcesParser { /** * Read resource map by id. * + * @param resId resource id * @param data config byte buffer * @return the resource map of id. */ - static HashMap getResourceMapById(int resId, byte[] data) { + public HashMap getResourceMapById(int resId, byte[] data) { List resources = getAllDataItem(data); HashMap resourceMap = new HashMap<>(); for (ResourceIndexResult indexResult : resources) { @@ -461,7 +475,14 @@ public class ResourcesParser { return resourceMap; } - static String getResourceStringById(int resId, byte[] data) { + /** + * Read resource by id. + * + * @param resId resource id + * @param data config byte buffer + * @return the resource. + */ + public String getResourceStringById(int resId, byte[] data) { List resources = getAllDataItem(data); for (ResourceIndexResult indexResult : resources) { if (indexResult.id == resId) { @@ -505,7 +526,8 @@ public class ResourcesParser { /** * convert DataItems to ResourceIndexResult. * - * @param item Indicates the DataItem. + * @param item Indicates the DataItem. + * @param configClass config info. * @return the final ResourceIndexResult */ static ResourceIndexResult parseDataItems(DataItem item, String configClass) { @@ -515,9 +537,7 @@ public class ResourcesParser { resourceIndexResult.type = ResType.getType(item.type); resourceIndexResult.id = item.id; resourceIndexResult.name = item.name.substring(0, item.name.length() - 1); - if (item.type == ResType.StrArray.getIndex() || item.type == ResType.IntArray.getIndex() - || item.type == ResType.Theme.getIndex() || item.type == ResType.Plurals.getIndex() - || item.type == ResType.Pattern.getIndex()) { + if (requireBytesConversion(item.type)) { byte[] bytes = item.value.substring(0, item.value.length() - 1).getBytes(StandardCharsets.UTF_8); resourceIndexResult.value = convertBytesToString(bytes); @@ -528,6 +548,14 @@ public class ResourcesParser { return resourceIndexResult; } + private static boolean requireBytesConversion(int resType) { + return resType == ResType.STR_ARRAY.getIndex() + || resType == ResType.INT_ARRAY.getIndex() + || resType == ResType.THEME.getIndex() + || resType == ResType.PLURALS.getIndex() + || resType == ResType.PATTERN.getIndex(); + } + /** * convert bytes to string. * @@ -538,7 +566,7 @@ public class ResourcesParser { StringBuilder result = new StringBuilder(); ByteBuffer byteBuf = ByteBuffer.wrap(data); byteBuf.order(ByteOrder.LITTLE_ENDIAN); - while(byteBuf.hasRemaining()) { + while (byteBuf.hasRemaining()) { result.append(LEFT_BRACKET); int len = byteBuf.getShort(); if (len <= 0) { @@ -566,38 +594,39 @@ public class ResourcesParser { int lastKeyType = -1; for (int i = 0; i < configIndex.keyCount; ++i) { KeyParam param = configIndex.params[i]; - if (param.keyType == ConfigType.Language.ordinal() || param.keyType == ConfigType.Region.ordinal() - || param.keyType == ConfigType.Script.ordinal()) { + if (param.keyType == ConfigType.LANGUAGE.ordinal() + || param.keyType == ConfigType.REGION.ordinal() + || param.keyType == ConfigType.SCRIPT.ordinal()) { if (EMPTY_STRING.equals(configClass.toString())) { configClass.append(parseAscii(param.value)); } else { - if (lastKeyType == ConfigType.Language.ordinal() || - lastKeyType == ConfigType.Region.ordinal() || lastKeyType == ConfigType.Script.ordinal()) { + if (lastKeyType == ConfigType.LANGUAGE.ordinal() || + lastKeyType == ConfigType.REGION.ordinal() || lastKeyType == ConfigType.SCRIPT.ordinal()) { configClass.append(MCC_CONJUNCTION).append(parseAscii(param.value)); } else { configClass.append(CONFIG_CONJUNCTION).append(parseAscii(param.value)); } } lastKeyType = param.keyType; - } else if (param.keyType == ConfigType.Resolution.ordinal()) { + } else if (param.keyType == ConfigType.RESOLUTION.ordinal()) { if (EMPTY_STRING.equals(configClass.toString())) { configClass.append(Resolution.getType(param.value)); } else { configClass.append(CONFIG_CONJUNCTION).append(Resolution.getType(param.value)); } - } else if (param.keyType == ConfigType.Direction.ordinal()) { + } else if (param.keyType == ConfigType.DIRECTION.ordinal()) { if (EMPTY_STRING.equals(configClass.toString())) { configClass.append(param.value == 0 ? VERTICAL : HORIZONTAL); } else { configClass.append(CONFIG_CONJUNCTION).append(param.value == 0 ? VERTICAL : HORIZONTAL); } - } else if (param.keyType == ConfigType.DeviceType.ordinal()) { + } else if (param.keyType == ConfigType.DEVICE_TYPE.ordinal()) { if (EMPTY_STRING.equals(configClass.toString())) { configClass.append(DeviceType.getType(param.value)); } else { configClass.append(CONFIG_CONJUNCTION).append(DeviceType.getType(param.value)); } - } else if (param.keyType == ConfigType.LightMode.ordinal()) { + } else if (param.keyType == ConfigType.LIGHT_MODE.ordinal()) { if (EMPTY_STRING.equals(configClass.toString())) { configClass.append(param.value == 0 ? DARK : LIGHT); } else { @@ -631,7 +660,7 @@ public class ResourcesParser { */ private static String parseAscii(Integer value) { StringBuilder result = new StringBuilder(); - while(value > 0) { + while (value > 0) { result.insert(0, (char) (value & 0xFF)); value = value >> 8; } @@ -649,7 +678,7 @@ public class ResourcesParser { return inputString; } StringBuilder result = new StringBuilder(); - while(result.length() < length - inputString.length()) { + while (result.length() < length - inputString.length()) { result.append('0'); } result.append(inputString); diff --git a/adapter/ohos/restool/ResourcesParserV2.java b/adapter/ohos/restool/ResourcesParserV2.java new file mode 100644 index 0000000000000000000000000000000000000000..19cf64bc6f3963c27790db3c767a8884fd0f3f79 --- /dev/null +++ b/adapter/ohos/restool/ResourcesParserV2.java @@ -0,0 +1,875 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * 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 ohos.restool; + +import ohos.BundleException; +import ohos.Log; +import ohos.ResourceIndexResult; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Resources Parser V2. + * + * @since 2025-06-06 + */ +public class ResourcesParserV2 implements ResourcesParser { + /** + * Parses resources default id. + */ + public static final int RESOURCE_DEFAULT_ID = -1; + + private static final Log LOG = new Log(ResourcesParserV2.class.toString()); + private static final int VERSION_BYTE_LENGTH = 128; + private static final int TAG_BYTE_LENGTH = 4; + private static final int TYPE_BYTE_LENGTH = 4; + private static final int VALUE_BYTE_LENGTH = 4; + private static final int IDSS_TYPE_BYTE_LENGTH = 4; + private static final int DATA_OFFSET_LENGTH = 4; + private static final String LEFT_BRACKET = "<"; + private static final String RIGHT_BRACKET = ">"; + private static final String VERTICAL = "vertical"; + private static final String HORIZONTAL = "horizontal"; + private static final String DARK = "dark"; + private static final String LIGHT = "light"; + private static final String MCC = "mcc"; + private static final String MNC = "mnc"; + private static final String CONFIG_CONJUNCTION = "-"; + private static final String MCC_CONJUNCTION = "_"; + private static final String BASE = "base"; + private static final int CHAR_LENGTH = 1; + private static final String EMPTY_STRING = ""; + + private static int idssOffset; + private static Map> keysMap = new HashMap<>(); + private static Map> idssMap = new HashMap<>(); + private static Map> dataMap = new HashMap<>(); + + /** + * ResourcesParserV2 + * + * @param data resource index data + */ + public ResourcesParserV2(byte[] data) { + parseZone(data); + } + + private enum ResType { + VALUES(0, "Values"), + ANIMATOR(1, "Animator"), + DRAWABLE(2, "Drawable"), + LAYOUT(3, "Layout"), + MENU(4, "Menu"), + MIPMAP(5, "Mipmap"), + RAW(6, "Raw"), + XML(7, "Xml"), + INTEGER(8, "Integer"), + STRING(9, "String"), + STR_ARRAY(10, "StrArray"), + INT_ARRAY(11, "IntArray"), + BOOLEAN_TYPE(12, "Boolean"), + DIMEN(13, "Dimen"), + COLOR(14, "Color"), + ID(15, "Id"), + THEME(16, "Theme"), + PLURALS(17, "Plurals"), + FLOAT_TYPE(18, "Float"), + MEDIA(19, "Media"), + PROF(20, "Prof"), + SVG(21, "Svg"), + PATTERN(22, "Pattern"); + + private final int index; + private final String type; + + private ResType(int index, String type) { + this.index = index; + this.type = type; + } + + public static String getType(int index) { + for (ResType resType : ResType.values()) { + if (resType.getIndex() == index) { + return resType.type; + } + } + return ""; + } + + public int getIndex() { + return index; + } + public String getType() { + return type; + } + } + + private enum DeviceType { + PHONE(0, "phone"), + TABLET(1, "tablet"), + CAR(2, "car"), + PC(3, "pc"), + TV(4, "tv"), + SPEAKER(5, "speaker"), + WEARABLE(6, "wearable"), + GLASSES(7, "glasses"), + HEADSET(8, "headset"); + + private final int index; + private final String type; + private static final Map INDEX_MAP; + + static { + INDEX_MAP = new HashMap<>(); + for (DeviceType deviceType : values()) { + INDEX_MAP.put(deviceType.index, deviceType); + } + } + + private DeviceType(int index, String type) { + this.index = index; + this.type = type; + } + + /** + * get index + * + * @return index of DeviceType + */ + public int getIndex() { + return index; + } + + /** + * get type + * + * @return type of DeviceType + */ + public String getType() { + return type; + } + + /** + * get type by index + * + * @param index index of DeviceType + * @return type of DeviceType + */ + public static String getTypeByIndex(int index) { + DeviceType deviceType = INDEX_MAP.get(index); + return deviceType != null ? deviceType.type : ""; + } + } + + private enum Resolution { + NODPI(-2, "nodpi"), + ANYDPI(-1, "anydpi"), + SDPI(120, "sdpi"), + MDPI(160, "mdpi"), + TVDPI(213, "tvdpi"), + LDPI(240, "ldpi"), + XLDPI(320, "xldpi"), + XXLDPI(480, "xxldpi"), + XXXLDPI(640, "xxxldpi"); + + private final int index; + private final String type; + private static final Map INDEX_MAP; + + static { + INDEX_MAP = new HashMap<>(); + for (Resolution resolution : values()) { + INDEX_MAP.put(resolution.index, resolution); + } + } + + private Resolution(int index, String type) { + this.index = index; + this.type = type; + } + + /** + * get index + * + * @return index of Resolution + */ + public int getIndex() { + return index; + } + + /** + * get type + * + * @return type of Resolution + */ + public String getType() { + return type; + } + + /** + * get type by index + * + * @param index index of Resolution + * @return type of Resolution + */ + public static String getTypeByIndex(int index) { + Resolution resolution = INDEX_MAP.get(index); + return resolution != null ? resolution.type : ""; + } + } + + private enum ConfigType { + LANGUAGE, + REGION, + RESOLUTION, + DIRECTION, + DEVICE_TYPE, + SCRIPT, + LIGHT_MODE, + MCC, + MNC + } + + /** + * Key Param. + */ + static class KeyParamV2 { + int keyType; + int value; + } + + /** + * Config Index. + */ + static class ConfigIndexV2 { + String tag; + int resCfgId; + int keyCount; + KeyParamV2[] params; + } + + /** + * KEYS item. + */ + static class KeysItemV2 { + String tag; + int resCfgId; + String type; + String value; + } + + /** + * IDSS item. + */ + static class IdssItemV2 { + int type; + int resId; + int offset; + int nameLen; + String name; + } + + /** + * DATA item. + */ + static class DataItemV2 { + int type; + int resId; + int resCfgId; + String name; + String value; + } + + /** + * Get resource value by resource id. + * + * @param resourceId resource id + * @param data resource index data + * @return the resourceId value + * @throws BundleException IOException. + */ + public String getResourceById(int resourceId, byte[] data) throws BundleException { + String resourceIdValue = ""; + if (data == null || data.length <= 0 || resourceId == RESOURCE_DEFAULT_ID) { + LOG.error("ResourcesParserV2::getIconPath data byte or ResourceId is null"); + return resourceIdValue; + } + + List result = getResource(resourceId); + if (result != null && result.size() > 0 && result.get(0) != null && !EMPTY_STRING.equals(result.get(0))) { + resourceIdValue = result.get(0); + } + return resourceIdValue; + } + + /** + * Get base resource value by resource id. + * + * @param resourceId resource id + * @param data resource index data + * @return the resource value + * @throws BundleException IOException. + */ + public String getBaseResourceById(int resourceId, byte[] data) throws BundleException { + String resourceIdValue = ""; + if (data == null || data.length <= 0 || resourceId == RESOURCE_DEFAULT_ID) { + LOG.error("ResourcesParserV2::getBaseResourceById data byte or ResourceId is null"); + return resourceIdValue; + } + resourceIdValue = getBaseResource(resourceId, data); + return resourceIdValue; + } + + /** + * Get base resource. + * + * @param resId resource id + * @param data resource index data array + * @return the resource value + * @throws BundleException IOException. + */ + static String getBaseResource(int resId, byte[] data) { + ByteBuffer byteBuf = ByteBuffer.wrap(data); + byteBuf.order(ByteOrder.LITTLE_ENDIAN); + byte[] version = new byte[VERSION_BYTE_LENGTH]; + byteBuf.get(version); + byteBuf.getInt(); // length + int configCount = byteBuf.getInt(); + byteBuf.getInt(); // dataBrokeOffset + Optional optionalConfigIndex = loadBaseConfig(byteBuf, configCount); + if (!optionalConfigIndex.isPresent()) { + LOG.error("ResourcesParserV2::getBaseResource configIndex is null"); + return ""; + } + return readBaseItem(resId, optionalConfigIndex.get()); + } + + /** + * Load index config. + * + * @param bufBuf config byte buffer + * @param count config count + * @return the base config index + * @throws BundleException IOException. + */ + static Optional loadBaseConfig(ByteBuffer bufBuf, int count) { + for (int i = 0; i < count; i++) { + ConfigIndexV2 cfg = new ConfigIndexV2(); + byte[] tag = new byte[TAG_BYTE_LENGTH]; + bufBuf.get(tag); + cfg.tag = new String(tag, StandardCharsets.UTF_8); + cfg.resCfgId = bufBuf.getInt(); + cfg.keyCount = bufBuf.getInt(); + cfg.params = new KeyParamV2[cfg.keyCount]; + for (int j = 0; j < cfg.keyCount; j++) { + cfg.params[j] = new KeyParamV2(); + cfg.params[j].keyType = bufBuf.getInt(); + cfg.params[j].value = bufBuf.getInt(); + } + if (cfg.keyCount == 0) { + return Optional.of(cfg); + } + } + return Optional.empty(); + } + + /** + * Read base config item. + * + * @param resId resource id + * @param configIndex the config index + * @return the base item + * @throws BundleException IOException. + */ + static String readBaseItem(int resId, ConfigIndexV2 configIndex) { + Map map = dataMap.get(resId); + return map.get(configIndex.resCfgId).value; + } + + /** + * Get Icon resource. + * + * @param resId resource id + * @return the result + * @throws BundleException IOException. + */ + static List getResource(int resId) { + return readAllItem(resId); + } + + /** + * Load index config. + * + * @param bufBuf config byte buffer + * @param count config count + * @return the config list + * @throws BundleException IOException. + */ + static List loadConfig(ByteBuffer bufBuf, int count) { + List configList = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + ConfigIndexV2 cfg = new ConfigIndexV2(); + byte[] tag = new byte[TAG_BYTE_LENGTH]; + bufBuf.get(tag); + cfg.tag = new String(tag, StandardCharsets.UTF_8); + cfg.resCfgId = bufBuf.getInt(); + cfg.keyCount = bufBuf.getInt(); + cfg.params = new KeyParamV2[cfg.keyCount]; + for (int j = 0; j < cfg.keyCount; j++) { + cfg.params[j] = new KeyParamV2(); + cfg.params[j].keyType = bufBuf.getInt(); + cfg.params[j].value = bufBuf.getInt(); + } + configList.add(cfg); + } + return configList; + } + + /** + * Read all config item. + * + * @param resId resource id + * @return the item list + * @throws BundleException IOException. + */ + static List readAllItem(int resId) { + List result = new ArrayList<>(); + Map map = dataMap.get(resId); + for (DataItemV2 dataItemV2 : map.values()) { + result.add(dataItemV2.value); + } + return result; + } + + /** + * Read all config item. + * + * @param data config byte buffer + * @return the item info. + */ + public List getAllDataItem(byte[] data) { + ByteBuffer byteBuf = ByteBuffer.wrap(data); + byteBuf.order(ByteOrder.LITTLE_ENDIAN); + byte[] version = new byte[VERSION_BYTE_LENGTH]; + byteBuf.get(version); + byteBuf.getInt(); + int keyCount = byteBuf.getInt(); + byteBuf.getInt(); // dataBrokeOffset + List cfg = loadConfig(byteBuf, keyCount); + return readDataAllItem(cfg, byteBuf); + } + + /** + * Read resource map by id. + * + * @param resId resource id + * @param data config byte buffer + * @return the resource map of id. + */ + public HashMap getResourceMapById(int resId, byte[] data) { + List resources = getAllDataItem(data); + HashMap resourceMap = new HashMap<>(); + for (ResourceIndexResult indexResult : resources) { + if (indexResult.id == resId) { + resourceMap.put(indexResult.configClass, indexResult.value); + } + } + return resourceMap; + } + + /** + * Read resource by id. + * + * @param resId resource id + * @param data config byte buffer + * @return resource + */ + public String getResourceStringById(int resId, byte[] data) { + List resources = getAllDataItem(data); + for (ResourceIndexResult indexResult : resources) { + if (indexResult.id == resId) { + return indexResult.value; + } + } + return ""; + } + + /** + * Read all config item. + * + * @param configs the config list + * @param buf config byte buffer + * @return the item list + */ + static List readDataAllItem(List configs, ByteBuffer buf) { + List resourceIndexResults = new ArrayList<>(); + for (ConfigIndexV2 index : configs) { + String configClass = convertConfigIndexToString(index); + dataMap.values() + .forEach( + integerDataItemV2Map -> + integerDataItemV2Map + .values() + .forEach( + dataItemV2 -> { + if (dataItemV2.resCfgId == index.resCfgId) { + resourceIndexResults.add( + parseDataItems(dataItemV2, configClass)); + } + })); + } + return resourceIndexResults; + } + + /** + * convert DataItems to ResourceIndexResult. + * + * @param item Indicates the DataItem. + * @param configClass config info + * @return the final ResourceIndexResult + */ + static ResourceIndexResult parseDataItems(DataItemV2 item, String configClass) { + ResourceIndexResult resourceIndexResult = new ResourceIndexResult(); + resourceIndexResult.configClass = configClass; + if (item != null) { + resourceIndexResult.type = ResType.getType(item.type); + resourceIndexResult.id = item.resId; + resourceIndexResult.name = item.name; + if (requireBytesConversion(item.type)) { + byte[] bytes = item.value.getBytes(StandardCharsets.UTF_8); + resourceIndexResult.value = convertBytesToString(bytes); + } else { + resourceIndexResult.value = item.value; + } + } + return resourceIndexResult; + } + + private static boolean requireBytesConversion(int resType) { + return resType == ResType.STR_ARRAY.getIndex() + || resType == ResType.INT_ARRAY.getIndex() + || resType == ResType.THEME.getIndex() + || resType == ResType.PLURALS.getIndex() + || resType == ResType.PATTERN.getIndex(); + } + + /** + * convert bytes to string. + * + * @param data Indicates the bytes of data. + * @return the final string + */ + static String convertBytesToString(byte[] data) { + StringBuilder result = new StringBuilder(); + ByteBuffer byteBuf = ByteBuffer.wrap(data); + byteBuf.order(ByteOrder.LITTLE_ENDIAN); + while (byteBuf.hasRemaining()) { + result.append(LEFT_BRACKET); + int len = byteBuf.getShort(); + if (len <= 0) { + LOG.info("len less than 0, dismiss"); + result.append(RIGHT_BRACKET); + break; + } + byte[] value = new byte[len + CHAR_LENGTH]; + byteBuf.get(value); + String item = new String(value, StandardCharsets.UTF_8); + result.append(item, 0, item.length()); + result.append(RIGHT_BRACKET); + } + return result.toString(); + } + + /** + * convert config to string. + * + * @param configIndex Indicates the configIndex. + * @return the final string + */ + static String convertConfigIndexToString(ConfigIndexV2 configIndex) { + if (configIndex.keyCount == 0) { + return BASE; + } + + StringBuilder configClass = new StringBuilder(); + ConfigType lastKeyType = null; + + for (int i = 0; i < configIndex.keyCount; ++i) { + KeyParamV2 param = configIndex.params[i]; + ConfigType currentType = ConfigType.values()[param.keyType]; + Optional valuePart = processParamValue(param, currentType); + + if (valuePart.isPresent()) { + appendValue(configClass, lastKeyType, currentType, valuePart.get()); + lastKeyType = currentType; + } + } + + return configClass.length() == 0 ? BASE : configClass.toString(); + } + + private static Optional processParamValue(KeyParamV2 param, ConfigType type) { + switch (type) { + case LANGUAGE: + case REGION: + case SCRIPT: + return Optional.ofNullable(parseAscii(param.value)); + case RESOLUTION: + return Optional.ofNullable(Resolution.getTypeByIndex(param.value)); + case DIRECTION: + return Optional.ofNullable(param.value == 0 ? VERTICAL : HORIZONTAL); + case DEVICE_TYPE: + return Optional.ofNullable(DeviceType.getTypeByIndex(param.value)); + case LIGHT_MODE: + return Optional.ofNullable(param.value == 0 ? DARK : LIGHT); + case MCC: + return Optional.ofNullable(MCC + param.value); + case MNC: + return Optional.ofNullable(MNC + fillUpZero(String.valueOf(param.value), 3)); + default: + return Optional.empty(); + } + } + + private static void appendValue( + StringBuilder builder, ConfigType lastKeyType, ConfigType currentKeyType, String value) { + if (builder.length() == 0) { + builder.append(value); + return; + } + + String conjunction = shouldUseMccConjunction(lastKeyType, currentKeyType) + ? MCC_CONJUNCTION + : CONFIG_CONJUNCTION; + builder.append(conjunction).append(value); + } + + private static boolean shouldUseMccConjunction(ConfigType lastKeyType, ConfigType currentKeyType) { + boolean isCurrentMccMnc = currentKeyType == ConfigType.MCC || currentKeyType == ConfigType.MNC; + boolean isLastLanguageRegionScript = + lastKeyType != null + && (lastKeyType == ConfigType.LANGUAGE + || lastKeyType == ConfigType.REGION + || lastKeyType == ConfigType.SCRIPT); + + return isCurrentMccMnc || isLastLanguageRegionScript; + } + + /** + * convert integer to string. + * + * @param value Indicates the Integer. + * @return the final string + */ + private static String parseAscii(Integer value) { + if (value == null) { + return ""; + } + StringBuilder result = new StringBuilder(); + int tempValue = value; + while (tempValue > 0) { + result.insert(0, (char) (tempValue & 0xFF)); + tempValue = tempValue >> 8; + } + return result.toString(); + } + + /** + * fillup zero to string. + * + * @param inputString Indicates the string should to be filled. + * @param length Indicates the final length of String. + * @return the final string + */ + private static String fillUpZero(String inputString, int length) { + if (inputString.length() >= length) { + return inputString; + } + StringBuilder result = new StringBuilder(); + while (result.length() < length - inputString.length()) { + result.append('0'); + } + result.append(inputString); + return result.toString(); + } + + /** + * Parse KEYS,IDSS,DATA zone. + * + * @param data resource byte + */ + static void parseZone(byte[] data) { + if (!parseKEYSZone(data)) { + LOG.error("ResourcesParserV2 parseKEYSZone() failed"); + } + if (!parseDATAZone(data)) { + LOG.error("ResourcesParserV2 parseDATAZone() failed"); + } + if (!parseIDSSZone(data)) { + LOG.error("ResourcesParserV2 parseIDSSZone() failed"); + } + } + + /** + * Parse KEYS zone. + * + * @param data resource byte + * @return parse result + */ + static boolean parseKEYSZone(byte[] data) { + if (data == null || data.length == 0) { + LOG.error("ResourcesParserV2::parseKEYSZone data byte is null"); + return false; + } + ByteBuffer buf = ByteBuffer.wrap(data); + buf.order(ByteOrder.LITTLE_ENDIAN); + byte[] version = new byte[VERSION_BYTE_LENGTH]; + buf.get(version); + buf.getInt(); // length + int keyCount = buf.getInt(); + buf.getInt(); // dataBrokeOffset + // KEYS zone + for (int i = 0; i < keyCount; i++) { + Map map = new HashMap<>(); + byte[] tag = new byte[TAG_BYTE_LENGTH]; + buf.get(tag); + String tagStr = new String(tag, StandardCharsets.UTF_8); + int resCfgId = buf.getInt(); + int keyParamCount = buf.getInt(); + for (int j = 0; j < keyParamCount; j++) { + KeysItemV2 keysItemV2 = new KeysItemV2(); + keysItemV2.tag = tagStr; + keysItemV2.resCfgId = resCfgId; + byte[] type = new byte[TYPE_BYTE_LENGTH]; + buf.get(type); + keysItemV2.type = new String(type, StandardCharsets.UTF_8); + byte[] value = new byte[VALUE_BYTE_LENGTH]; + buf.get(value); + keysItemV2.value = new String(value, StandardCharsets.UTF_8); + map.put(keysItemV2.type, keysItemV2); + } + keysMap.put(tagStr, map); + } + idssOffset = buf.position(); + return true; + } + + /** + * Parse IDSS zone. + * + * @param data resource byte + * @return parse result + */ + static boolean parseIDSSZone(byte[] data) { + if (data == null || data.length == 0) { + LOG.error("ResourcesParserV2::parseIDSSZone data byte is null"); + return false; + } + if (idssOffset == 0) { + LOG.error("ResourcesParserV2::parseIDSSZone idssOffset has not parse"); + return false; + } + ByteBuffer buf = ByteBuffer.wrap(data); + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.position(idssOffset); + buf.getInt(); // tag + buf.getInt(); // len + int typeCount = buf.getInt(); + buf.getInt(); // idCount + // IDSS zone + for (int i = 0; i < typeCount; i++) { + Map map = new HashMap<>(); + int type = buf.getInt(); + buf.getInt(); // length + int count = buf.getInt(); + for (int j = 0; j < count; j++) { + IdssItemV2 idssItemV2 = new IdssItemV2(); + idssItemV2.type = type; + idssItemV2.resId = buf.getInt(); + idssItemV2.offset = buf.getInt(); + int nameLen = buf.getInt(); + byte[] name = new byte[nameLen]; + buf.get(name); + idssItemV2.nameLen = nameLen; + idssItemV2.name = new String(name, StandardCharsets.UTF_8); + dataMap.get(idssItemV2.resId) + .values() + .forEach( + dataItemV2 -> { + dataItemV2.type = idssItemV2.type; + dataItemV2.name = idssItemV2.name; + }); + map.put(idssItemV2.resId, idssItemV2); + } + idssMap.put(type, map); + } + return true; + } + + /** + * Parse DATA zone. + * + * @param data resource byte + * @return data zone + */ + static boolean parseDATAZone(byte[] data) { + if (data == null || data.length == 0) { + LOG.error("ResourcesParserV2::parseDATAZone data byte is null"); + return false; + } + ByteBuffer buf = ByteBuffer.wrap(data); + buf.order(ByteOrder.LITTLE_ENDIAN); + byte[] version = new byte[VERSION_BYTE_LENGTH]; + buf.get(version); + buf.getInt(); // length + buf.getInt(); + int dataZoneOffset = buf.getInt(); // dataBrokeOffset + buf.position(dataZoneOffset); + buf.getInt(); // DATA tag + buf.getInt(); // DATA length + int resIdCount = buf.getInt(); + for (int i = 0; i < resIdCount; i++) { + Map resCfgIdMap = new HashMap<>(); + int resId = buf.getInt(); + buf.getInt(); // length + int resCfgIdCount = buf.getInt(); + for (int j = 0; j < resCfgIdCount; j++) { + DataItemV2 dataItemV2 = new DataItemV2(); + dataItemV2.resId = resId; + dataItemV2.resCfgId = buf.getInt(); + int dataOffset = buf.getInt(); + int position = buf.position(); + buf.position(dataOffset); + short dataLen = buf.getShort(); + byte[] tag = new byte[dataLen]; + buf.get(tag); + dataItemV2.value = new String(tag, StandardCharsets.UTF_8); + buf.position(position); + resCfgIdMap.put(dataItemV2.resCfgId, dataItemV2); + } + dataMap.put(resId, resCfgIdMap); + } + return true; + } +} + diff --git a/build.py b/build.py index 1a59283ebc8e60ac6eade532427bff2e0aa01067..2ebe3682872506a9ecc035d23ec50fc70658ec32 100755 --- a/build.py +++ b/build.py @@ -84,7 +84,8 @@ def compile_unpacking_tool(root_path, src_path, jar_output, out_path, big_versio 'ModuleMetadataInfo.java', 'ModuleProfileInfo.java', 'ModuleResult.java', 'ModuleShortcut.java', 'PackFormatter.java', 'PackInfo.java', 'PackingToolErrMsg.java', 'PreloadItem.java', 'ProfileInfo.java', 'ReqPermission.java', - 'ResourceIndexResult.java', 'ResourcesParser.java', 'ScreenDensity.java', + 'ResourceIndexResult.java', 'restool/ResourcesParser.java', 'restool/ResourcesParserV1.java', + 'restool/ResourcesParserV2.java', 'restool/ResourcesParserFactory.java', 'ScreenDensity.java', 'ScreenShape.java', 'ScreenWindow.java', 'Shortcut.java', 'ShowHelp.java', 'SkillInfo.java', 'UncompressEntrance.java', 'Uncompress.java', 'UncompressResult.java', 'UncompressVerify.java',