/* * 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. */ @Override 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. */ @Override 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. */ @Override 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. */ @Override 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 */ @Override 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; } }