1 /* 2 * Copyright (c) 2023-2023 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16 package com.ohos.hapsigntool.codesigning.utils; 17 18 import com.google.gson.JsonArray; 19 import com.google.gson.JsonElement; 20 import com.google.gson.JsonObject; 21 import com.google.gson.JsonParseException; 22 import com.google.gson.JsonParser; 23 import com.google.gson.JsonPrimitive; 24 import com.google.gson.JsonSyntaxException; 25 import com.google.gson.stream.JsonReader; 26 import com.ohos.hapsigntool.entity.Pair; 27 import com.ohos.hapsigntool.error.ProfileException; 28 import org.apache.logging.log4j.LogManager; 29 import org.apache.logging.log4j.Logger; 30 31 import java.io.File; 32 import java.io.IOException; 33 import java.io.InputStream; 34 import java.io.InputStreamReader; 35 import java.nio.charset.StandardCharsets; 36 import java.util.ArrayList; 37 import java.util.Arrays; 38 import java.util.HashMap; 39 import java.util.LinkedList; 40 import java.util.List; 41 import java.util.Map; 42 import java.util.Queue; 43 import java.util.jar.JarEntry; 44 import java.util.jar.JarFile; 45 46 /** 47 * utility for check hap configs 48 * 49 * @since 2023/06/05 50 */ 51 public class HapUtils { 52 /** 53 * DEBUG_LIB_ID 54 */ 55 public static final String HAP_DEBUG_OWNER_ID = "DEBUG_LIB_ID"; 56 57 /** 58 * SHARED_LIB_ID 59 */ 60 public static final String HAP_SHARED_OWNER_ID = "SHARED_LIB_ID"; 61 62 private static final Logger LOGGER = LogManager.getLogger(HapUtils.class); 63 64 private static final String COMPRESS_NATIVE_LIBS_OPTION = "compressNativeLibs"; 65 66 private static final List<String> HAP_CONFIG_FILES = new ArrayList<>(); 67 68 private static final String HAP_FA_CONFIG_JSON_FILE = "config.json"; 69 70 private static final String HAP_STAGE_MODULE_JSON_FILE = "module.json"; 71 72 private static final int MAX_APP_ID_LEN = 32; // max app-identifier in profile 73 74 static { 75 HAP_CONFIG_FILES.add(HAP_FA_CONFIG_JSON_FILE); 76 HAP_CONFIG_FILES.add(HAP_STAGE_MODULE_JSON_FILE); 77 } 78 HapUtils()79 private HapUtils() { 80 } 81 82 /** 83 * Check configuration in hap to find out whether the native libs are compressed 84 * 85 * @param hapFile the given hap 86 * @return boolean value of parsing result 87 * @throws IOException io error 88 */ checkCompressNativeLibs(File hapFile)89 public static boolean checkCompressNativeLibs(File hapFile) throws IOException { 90 try (JarFile inputJar = new JarFile(hapFile, false)) { 91 for (String configFile : HAP_CONFIG_FILES) { 92 JarEntry entry = inputJar.getJarEntry(configFile); 93 if (entry == null) { 94 continue; 95 } 96 try (InputStream data = inputJar.getInputStream(entry)) { 97 String jsonString = new String(InputStreamUtils.toByteArray(data, (int) entry.getSize()), 98 StandardCharsets.UTF_8); 99 return checkCompressNativeLibs(jsonString); 100 } 101 } 102 } 103 return true; 104 } 105 106 /** 107 * Check whether the native libs are compressed by parsing config json 108 * 109 * @param jsonString the config json string 110 * @return boolean value of parsing result 111 */ checkCompressNativeLibs(String jsonString)112 public static boolean checkCompressNativeLibs(String jsonString) { 113 JsonObject jsonObject = JsonParser.parseString(jsonString).getAsJsonObject(); 114 Queue<JsonObject> queue = new LinkedList<>(); 115 queue.offer(jsonObject); 116 while (queue.size() > 0) { 117 JsonObject curJsonObject = queue.poll(); 118 JsonElement jsonElement = curJsonObject.get(COMPRESS_NATIVE_LIBS_OPTION); 119 if (jsonElement != null) { 120 return jsonElement.getAsBoolean(); 121 } 122 for (Map.Entry<String, JsonElement> entry : curJsonObject.entrySet()) { 123 if (entry.getValue().isJsonObject()) { 124 queue.offer(entry.getValue().getAsJsonObject()); 125 } 126 } 127 } 128 // default to compress native libs 129 return true; 130 } 131 132 /** 133 * get app-id from profile 134 * 135 * @param profileContent the content of profile 136 * @return string value of app-id 137 * @throws ProfileException profile is invalid 138 */ getAppIdentifier(String profileContent)139 public static String getAppIdentifier(String profileContent) throws ProfileException { 140 Pair<String, String> resultPair = parseAppIdentifier(profileContent); 141 String ownerID = resultPair.getFirst(); 142 String profileType = resultPair.getSecond(); 143 if ("debug".equals(profileType)) { 144 return HAP_DEBUG_OWNER_ID; 145 } else if ("release".equals(profileType)) { 146 return ownerID; 147 } else { 148 throw new ProfileException("unsupported profile type"); 149 } 150 } 151 152 /** 153 * parse app-id and profileType from profile 154 * 155 * @param profileContent the content of profile 156 * @return Pair value of app-id and profileType 157 * @throws ProfileException profile is invalid 158 */ parseAppIdentifier(String profileContent)159 public static Pair<String, String> parseAppIdentifier(String profileContent) throws ProfileException { 160 String ownerID = null; 161 String profileType = null; 162 try { 163 JsonElement parser = JsonParser.parseString(profileContent); 164 JsonObject profileJson = parser.getAsJsonObject(); 165 String profileTypeKey = "type"; 166 if (!profileJson.has(profileTypeKey)) { 167 throw new ProfileException("profile has no type key"); 168 } 169 170 profileType = profileJson.get(profileTypeKey).getAsString(); 171 if (profileType == null || profileType.length() == 0) { 172 throw new ProfileException("Get profile type error"); 173 } 174 175 String appIdentifier = "app-identifier"; 176 String buildInfoMember = "bundle-info"; 177 JsonObject buildInfoObject = profileJson.getAsJsonObject(buildInfoMember); 178 if (buildInfoObject == null) { 179 throw new ProfileException("can not find bundle-info"); 180 } 181 if (buildInfoObject.has(appIdentifier)) { 182 JsonElement ownerIDElement = buildInfoObject.get(appIdentifier); 183 if (!ownerIDElement.getAsJsonPrimitive().isString()) { 184 throw new ProfileException("value of app-identifier is not string"); 185 } 186 ownerID = ownerIDElement.getAsString(); 187 if (ownerID.isEmpty() || ownerID.length() > MAX_APP_ID_LEN) { 188 throw new ProfileException("app-id length in profile is invalid"); 189 } 190 191 } 192 } catch (JsonSyntaxException | UnsupportedOperationException e) { 193 LOGGER.error(e.getMessage()); 194 throw new ProfileException("profile json is invalid"); 195 } 196 LOGGER.info("profile type is: {}", profileType); 197 return Pair.create(ownerID, profileType); 198 } 199 200 /** 201 * get hnp app-id from profile when type is public 202 * 203 * @param profileContent the content of profile 204 * @return ownerid 205 */ getPublicHnpOwnerId(String profileContent)206 public static String getPublicHnpOwnerId(String profileContent) { 207 // property type 208 String publicOwnerID = ""; 209 JsonElement parser = JsonParser.parseString(profileContent); 210 JsonObject profileJson = parser.getAsJsonObject(); 211 String profileTypeKey = "type"; 212 JsonPrimitive profileType = profileJson.getAsJsonPrimitive(profileTypeKey); 213 if (profileType != null) { 214 if ("debug".equals(profileType.getAsString())) { 215 publicOwnerID = HAP_DEBUG_OWNER_ID; 216 } else if ("release".equals(profileType.getAsString())) { 217 publicOwnerID = HAP_SHARED_OWNER_ID; 218 } 219 } 220 return publicOwnerID; 221 } 222 223 /** 224 * get hnp path behind "hnp/abi/" 225 * 226 * @param path filepath 227 * @return hnp path behind "hnp/abi/" 228 */ parseHnpPath(String path)229 public static String parseHnpPath(String path) { 230 if (path == null || path.isEmpty()) { 231 return ""; 232 } 233 String[] strings = path.split("/"); 234 if (strings.length < 3) { 235 return ""; 236 } 237 // get hnp path behind "hnp/abi/" 238 strings = Arrays.copyOfRange(strings, 2, strings.length); 239 return String.join("/", strings); 240 } 241 242 /** 243 * get map of hnp name and type from module.json 244 * 245 * @param inputJar hap file 246 * @return packageName-type map 247 * @throws IOException when IO error occurred 248 * @throws ProfileException profile is invalid 249 */ getHnpsFromJson(JarFile inputJar)250 public static Map<String, String> getHnpsFromJson(JarFile inputJar) throws IOException, ProfileException { 251 // get module.json 252 Map<String, String> hnpNameMap = new HashMap<>(); 253 JarEntry moduleEntry = inputJar.getJarEntry("module.json"); 254 if (moduleEntry == null) { 255 return hnpNameMap; 256 } 257 try (JsonReader reader = new JsonReader( 258 new InputStreamReader(inputJar.getInputStream(moduleEntry), StandardCharsets.UTF_8))) { 259 JsonElement jsonElement = JsonParser.parseReader(reader); 260 JsonObject jsonObject = jsonElement.getAsJsonObject(); 261 JsonObject moduleObject = jsonObject.getAsJsonObject("module"); 262 JsonArray hnpPackageArr = moduleObject.getAsJsonArray("hnpPackages"); 263 if (hnpPackageArr == null || hnpPackageArr.isEmpty()) { 264 LOGGER.debug("profile has no hnpPackages key or hnpPackages value is empty"); 265 return hnpNameMap; 266 } 267 hnpPackageArr.iterator().forEachRemaining((element) -> { 268 JsonObject hnpPackage = element.getAsJsonObject(); 269 JsonPrimitive hnpName = hnpPackage.getAsJsonPrimitive("package"); 270 if (hnpName == null || hnpName.getAsString().isEmpty()) { 271 return; 272 } 273 hnpNameMap.put(hnpName.getAsString(), "private"); 274 JsonPrimitive type = hnpPackage.getAsJsonPrimitive("type"); 275 if (type != null && !type.getAsString().isEmpty()) { 276 hnpNameMap.put(hnpName.getAsString(), type.getAsString()); 277 } 278 }); 279 } catch (JsonParseException e) { 280 LOGGER.error(e.getMessage()); 281 throw new ProfileException("profile json is invalid"); 282 } 283 return hnpNameMap; 284 } 285 286 } 287