/* * Copyright (c) 2024 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; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.serializer.SerializerFeature; import org.apache.commons.compress.utils.IOUtils; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributes; import java.util.Enumeration; import java.util.zip.CRC32; import java.util.zip.Deflater; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; /** * PackageNormalize, normalize HSP bundleName and versionCode * * @since 2024-04-06 */ public class PackageNormalize { private static final Log LOG = new Log(PackageNormalize.class.toString()); private static final int BUFFER_SIZE = 10 * 1024; private static final String MODULE_JSON = "module.json"; private static final String PACK_INFO = "pack.info"; private static final String APP = "app"; private static final String SUMMARY = "summary"; private static final String VERSION = "version"; private static final String CODE = "code"; private static final String BUNDLE_NAME = "bundleName"; private static final String VERSION_CODE = "versionCode"; private static final String TMP = "tmp"; /** * normalize HSP bundleName and versionCode * * @param utility common data * @return true if normalize succeed */ public static boolean normalize(Utility utility) { Path outPath = Paths.get(utility.getOutPath()); for (String hspPath : utility.getFormattedHspPathList()) { try { normalize(Paths.get(hspPath), outPath, utility.getBundleName(), utility.getVersionCode()); } catch (BundleException ex) { LOG.error(PackingToolErrMsg.PACKAGE_NORMALIZE_FAILED.toString( "Package normalize exist BundleException: " + ex.getMessage())); return false; } catch (IOException ex) { LOG.error(PackingToolErrMsg.PACKAGE_NORMALIZE_FAILED.toString( "Package normalize exist IOException: " + ex.getMessage())); return false; } } return true; } private static void normalize(Path hspPath, Path outPath, String bundleName, int versionCode) throws BundleException, IOException { Path outHspPath = outPath.resolve(hspPath.getFileName()); Path tmpDir = Files.createTempDirectory(outPath, TMP); Path moduleJson = Files.createFile(tmpDir.resolve(MODULE_JSON)); Path packInfo = Files.createFile(tmpDir.resolve(PACK_INFO)); try (ZipFile hspFile = new ZipFile(hspPath.toFile()); OutputStream output = Files.newOutputStream(outHspPath); ZipOutputStream zipOut = new ZipOutputStream(output)) { // 1.unzip module.json and pack.info, copy to tmp ZipEntry moduleEntry = hspFile.getEntry(MODULE_JSON); if (moduleEntry != null) { try (OutputStream out = Files.newOutputStream(moduleJson)) { IOUtils.copy(hspFile.getInputStream(moduleEntry), out); } } ZipEntry packEntry = hspFile.getEntry(PACK_INFO); if (moduleEntry != null) { try (OutputStream out = Files.newOutputStream(packInfo)) { IOUtils.copy(hspFile.getInputStream(packEntry), out); } } // 2.update bundleName, versionCode to module.json, pack.info updateModuleJson(moduleJson, bundleName, versionCode); updatePackInfo(packInfo, bundleName, versionCode); // 3.zip hsp, module.json, pack.info to outPath copyHsp(hspFile, moduleJson, packInfo, zipOut); } finally { rmdir(tmpDir); } } private static void rmdir(Path dir) throws IOException { Files.walkFileTree(dir, new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.delete(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException ex) throws IOException { Files.delete(dir); return FileVisitResult.CONTINUE; } }); } private static void updateModuleJson(Path moduleJson, String bundleName, int versionCode) throws BundleException, IOException { try (FileInputStream input = new FileInputStream(moduleJson.toFile())) { JSONObject jsonObject = JSON.parseObject(input, JSONObject.class); if (jsonObject == null) { LOG.error(PackingToolErrMsg.UPDATE_MODULE_JSON_FAILED.toString( "Failed to parse module.json: jsonObject is null.")); throw new BundleException("updateModuleJson failed, parse json is null."); } if (!jsonObject.containsKey(APP)) { LOG.error(PackingToolErrMsg.UPDATE_MODULE_JSON_FAILED.toString( "The module.json file does not contain 'app'.")); throw new BundleException("updateModuleJson failed, json format invalid."); } JSONObject appObject = jsonObject.getJSONObject(APP); appObject.put(BUNDLE_NAME, bundleName); appObject.put(VERSION_CODE, versionCode); Files.write(moduleJson, JSON.toJSONBytes(jsonObject, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteDateUseDateFormat, SerializerFeature.SortField), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); } } private static void updatePackInfo(Path packInfo, String bundleName, int versionCode) throws BundleException, IOException { try (FileInputStream input = new FileInputStream(packInfo.toFile())) { JSONObject jsonObject = JSON.parseObject(input, JSONObject.class); if (jsonObject == null) { LOG.error(PackingToolErrMsg.UPDATE_PACKINFO_FAILED.toString( "Failed to parse pack.info: invalid json format.")); throw new BundleException("updatePackInfo failed, json format invalid."); } JSONObject summaryObject = jsonObject.getJSONObject(SUMMARY); if (summaryObject == null) { LOG.error(PackingToolErrMsg.UPDATE_PACKINFO_FAILED.toString( "The pack.info file does not contain 'summary'.")); throw new BundleException("updatePackInfo failed, json format invalid."); } JSONObject appObject = summaryObject.getJSONObject(APP); if (appObject == null) { LOG.error(PackingToolErrMsg.UPDATE_PACKINFO_FAILED.toString( "The pack.info file does not contain 'app'.")); throw new BundleException("updatePackInfo failed, json format invalid."); } appObject.put(BUNDLE_NAME, bundleName); JSONObject versionObject = appObject.getJSONObject(VERSION); if (versionObject == null) { LOG.error(PackingToolErrMsg.UPDATE_PACKINFO_FAILED.toString( "The pack.info file does not contain 'version'.")); throw new BundleException("updatePackInfo failed, json format invalid."); } versionObject.put(CODE, versionCode); Files.write(packInfo, JSON.toJSONBytes(jsonObject, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteDateUseDateFormat, SerializerFeature.SortField), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); } } private static void copyHsp(ZipFile hspFile, Path moduleJson, Path packInfo, ZipOutputStream zipOut) throws BundleException, IOException { zipOut.setLevel(Deflater.BEST_SPEED); boolean isStored = true; Enumeration entries = hspFile.entries(); while (entries.hasMoreElements()) { ZipEntry zipEntry = entries.nextElement(); if (MODULE_JSON.equals(zipEntry.getName()) || PACK_INFO.equals(zipEntry.getName())) { isStored = zipEntry.getMethod() == ZipEntry.STORED; continue; } ZipEntry newEntry = zipEntry.getMethod() == ZipEntry.STORED ? new ZipEntry(zipEntry) : new ZipEntry(zipEntry.getName()); zipOut.putNextEntry(newEntry); if (!zipEntry.isDirectory()) { IOUtils.copy(hspFile.getInputStream(zipEntry), zipOut); } zipOut.closeEntry(); } compressFile(moduleJson, isStored, MODULE_JSON, zipOut); compressFile(packInfo, isStored, PACK_INFO, zipOut); } private static void compressFile(Path file, boolean isStored, String entryName, ZipOutputStream zipOut) throws BundleException, IOException { try (InputStream input = Files.newInputStream(file)) { if (isStored) { ZipEntry newEntry = new ZipEntry(entryName); newEntry.setMethod(ZipEntry.STORED); File jsonFile = file.toFile(); newEntry.setCompressedSize(jsonFile.length()); CRC32 crc = getCrcFromFile(jsonFile); newEntry.setCrc(crc.getValue()); zipOut.putNextEntry(newEntry); } else { ZipEntry newEntry = new ZipEntry(entryName); newEntry.setMethod(ZipEntry.DEFLATED); zipOut.putNextEntry(newEntry); } IOUtils.copy(input, zipOut); zipOut.closeEntry(); } } /** * get crc32 from file * @param file input file * @return cac32 value * @throws BundleException bundle exception */ public static CRC32 getCrcFromFile(File file) throws BundleException { CRC32 crc = new CRC32(); try (FileInputStream fileInputStream = new FileInputStream(file)) { byte[] buffer = new byte[BUFFER_SIZE]; int count = fileInputStream.read(buffer); while (count > 0) { crc.update(buffer, 0, count); count = fileInputStream.read(buffer); } } catch (FileNotFoundException ignored) { LOG.error(PackingToolErrMsg.FILE_NOT_FOUND.toString( "Get Crc from file exist FileNotFoundException: " + ignored.getMessage())); throw new BundleException("Get Crc from file failed."); } catch (IOException exception) { LOG.error(PackingToolErrMsg.IO_EXCEPTION.toString( "Get Crc from file exist IOException: " + exception.getMessage())); throw new BundleException("Get Crc from file failed, io exception."); } return crc; } }