/* * Copyright 2018 Mordechai Meisels * * 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 org.update4j.util; import java.io.BufferedReader; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.UncheckedIOException; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.lang.module.FindException; import java.lang.module.InvalidModuleDescriptorException; import java.lang.module.ModuleDescriptor; import java.lang.module.ModuleFinder; import java.lang.module.ModuleReference; import java.net.URI; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.AccessDeniedException; import java.nio.file.DirectoryStream; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.Signature; import java.security.SignatureException; import java.util.ArrayList; import java.util.Base64; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.jar.Attributes; import java.util.jar.Manifest; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.zip.Adler32; import java.util.zip.ZipFile; import org.update4j.OS; public class FileUtils { private FileUtils() { } public static long getChecksum(Path path) throws IOException { try (InputStream input = Files.newInputStream(path)) { Adler32 checksum = new Adler32(); byte[] buf = new byte[1024 * 8]; int read; while ((read = input.read(buf, 0, buf.length)) > -1) checksum.update(buf, 0, read); return checksum.getValue(); } } public static String getChecksumString(Path path) throws IOException { return Long.toHexString(getChecksum(path)); } public static boolean isJarFile(Path path) throws IOException { if (!isZipFile(path)) { return false; } try (ZipFile zip = new ZipFile(path.toFile())) { return zip.getEntry("META-INF/MANIFEST.MF") != null; } } public static boolean isZipFile(Path path) throws IOException { if (Files.isDirectory(path)) { return false; } if (!Files.isReadable(path)) { throw new IOException("Cannot read file " + path.toAbsolutePath()); } if (Files.size(path) < 4) { return false; } try (DataInputStream in = new DataInputStream(Files.newInputStream(path))) { int test = in.readInt(); return test == 0x504b0304; } } public static byte[] sign(Path path, PrivateKey key) throws IOException { try { String alg = key.getAlgorithm().equals("EC") ? "ECDSA" : key.getAlgorithm(); Signature sign = Signature.getInstance("SHA256with" + alg); sign.initSign(key); try (InputStream input = Files.newInputStream(path)) { byte[] buf = new byte[1024 * 8]; int len; while ((len = input.read(buf, 0, buf.length)) > 0) sign.update(buf, 0, len); } return sign.sign(); } catch (InvalidKeyException | SignatureException e) { throw new IOException(e); } catch (NoSuchAlgorithmException e) { throw new AssertionError(e); } } public static String signAndEncode(Path path, PrivateKey key) throws IOException { return Base64.getEncoder().encodeToString(sign(path, key)); } public static Path fromUri(URI uri) { String path = uri.getPath(); if (uri.isAbsolute()) { path = path.substring(path.lastIndexOf("/") + 1); } return Paths.get(path); } public static URI fromPath(Path path) { if (path.isAbsolute()) { Path filename = path.getFileName(); return fromPath(filename); } try { String uri = URLEncoder.encode(path.toString().replace("\\", "/"), "UTF-8"); uri = uri.replace("%2F", "/") // We still need directory structure .replace("+", "%20"); // "+" only means space in queries, not in paths return URI.create(uri); } catch (UnsupportedEncodingException e) { throw new AssertionError(e); } } public static URI relativize(URI base, URI other) { if (base == null || other == null) return other; return base.relativize(other); } public static Path relativize(Path base, Path other) { if (base == null || other == null) return other; try { return base.relativize(other); } catch (IllegalArgumentException e) { } return other; } public static OS fromFilename(String filename) { Pattern osPattern = Pattern.compile(".+-(linux|win|mac)\\.[^.]+"); Matcher osMatcher = osPattern.matcher(filename); if (osMatcher.matches()) { return OS.fromShortName(osMatcher.group(1)); } return null; } public static boolean isEmptyDirectory(Path path) throws IOException { if (Files.isDirectory(path)) { try (DirectoryStream<Path> dir = Files.newDirectoryStream(path)) { return !dir.iterator().hasNext(); } } return false; } public static void windowsHidden(Path file, boolean hidden) { if (OS.CURRENT != OS.WINDOWS) return; try { Files.setAttribute(file, "dos:hidden", hidden); } catch (Exception e) { } } public static void verifyAccessible(Path path) throws IOException { boolean exists = Files.exists(path); if (exists && !Files.isWritable(path)) throw new AccessDeniedException(path.toString()); try (Writer out = Files.newBufferedWriter(path, exists ? StandardOpenOption.APPEND : StandardOpenOption.CREATE)) { } finally { if (!exists) Files.deleteIfExists(path); } } public static void secureMoveFile(Path source, Path target) throws IOException { // for windows we can't go wrong because the OS manages locking if (OS.CURRENT == OS.WINDOWS || Files.notExists(target)) { Files.move(source, target, StandardCopyOption.REPLACE_EXISTING); return; } // At this point we are on non-windows and exists // Lets unlink file first so we don't run into file-busy errors. Path temp = Files.createTempFile(target.getParent(), null, null); Files.move(target, temp, StandardCopyOption.REPLACE_EXISTING); try { Files.move(source, target); } catch (IOException e) { Files.move(temp, target); throw e; } finally { Files.deleteIfExists(temp); } } public static void delayedDelete(Collection<Path> files, int secondsDelay) { secondsDelay = Math.max(secondsDelay, 1); List<String> commands = new ArrayList<>(); String filenames = files.stream() .map(Path::toString) .map(f -> "\"" + f.replace("\"", "\\\"") + "\"") .collect(Collectors.joining(" ")); if (OS.CURRENT == OS.WINDOWS) { commands.addAll(List.of("cmd", "/c")); commands.add("ping localhost -n " + (secondsDelay + 1) + " & del " + filenames); } else { commands.addAll(List.of("sh", "-c")); commands.add("sleep " + secondsDelay + " ; rm " + filenames); } ProcessBuilder pb = new ProcessBuilder(commands); Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { pb.start(); } catch (IOException e) { e.printStackTrace(); } })); } public static ModuleDescriptor deriveModuleDescriptor(Path jar, String filename, boolean readZip) throws IOException { if (!readZip) return primitiveModuleDescriptor(jar, filename); try (FileSystem zip = FileSystems.newFileSystem(jar, ClassLoader.getSystemClassLoader())) { Path moduleInfo = zip.getPath("/module-info.class"); if (Files.exists(moduleInfo)) { try (InputStream in = Files.newInputStream(moduleInfo)) { return ModuleDescriptor.read(in, () -> { try { Path root = zip.getPath("/"); return Files.walk(root) .filter(f -> !Files.isDirectory(f)) .map(f -> root.relativize(f)) .map(Path::toString) .map(FileUtils::toPackageName) .flatMap(Optional::stream) .collect(Collectors.toSet()); } catch (IOException e) { throw new UncheckedIOException(e); } }); } } return automaticModule(zip, filename); } } private static ModuleDescriptor primitiveModuleDescriptor(Path jar, String filename) throws IOException { String tempName = "a" + jar.getFileName(); Path temp = Paths.get(System.getProperty("user.home"), tempName + ".jar"); ModuleDescriptor mod; try { Files.copy(jar, temp); mod = ModuleFinder.of(temp) // .findAll() .stream() .map(ModuleReference::descriptor) .findAny() .orElseThrow(IllegalStateException::new); } finally { Files.deleteIfExists(temp); } if (tempName.equals(mod.name())) { String newModuleName = StringUtils.deriveModuleName(filename); if (!StringUtils.isModuleName(newModuleName)) { Warning.illegalModule(jar.getFileName().toString()); throw new IllegalStateException("Automatic module name '" + newModuleName + "' for file '" + jar.getFileName() + "' is not valid."); } return ModuleDescriptor.newAutomaticModule(newModuleName).packages(mod.packages()).build(); } return mod; } /* * https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/jdk/internal/module/ModulePath.java#L459 */ private static final String SERVICES_PREFIX = "META-INF/services/"; private static final Attributes.Name AUTOMATIC_MODULE_NAME = new Attributes.Name("Automatic-Module-Name"); private static ModuleDescriptor automaticModule(FileSystem zip, String filename) throws IOException { // I stripped elements that I don't currently care, as main class and version Manifest man = null; try (InputStream in = Files.newInputStream(zip.getPath("/META-INF/MANIFEST.MF"))) { man = new Manifest(in); } Attributes attrs = null; String moduleName = null; if (man != null) { attrs = man.getMainAttributes(); if (attrs != null) { moduleName = attrs.getValue(AUTOMATIC_MODULE_NAME); } } // Create builder, using the name derived from file name when // Automatic-Module-Name not present ModuleDescriptor.Builder builder; if (moduleName != null) { try { builder = ModuleDescriptor.newAutomaticModule(moduleName); } catch (IllegalArgumentException e) { throw new FindException(AUTOMATIC_MODULE_NAME + ": " + e.getMessage()); } } else { builder = ModuleDescriptor.newAutomaticModule(StringUtils.deriveModuleName(filename)); } // scan the names of the entries in the JAR file Map<Boolean, Set<String>> map = Files.walk(zip.getPath("/")) .filter(e -> !Files.isDirectory(e)) .map(Path::toString) .filter(e -> (e.endsWith(".class") ^ e.startsWith(SERVICES_PREFIX))) .collect(Collectors.partitioningBy(e -> e.startsWith(SERVICES_PREFIX), Collectors.toSet())); Set<String> classFiles = map.get(Boolean.FALSE); Set<String> configFiles = map.get(Boolean.TRUE); // the packages containing class files Set<String> packages = classFiles.stream() .map(FileUtils::toPackageName) .flatMap(Optional::stream) .distinct() .collect(Collectors.toSet()); // all packages are exported and open builder.packages(packages); // map names of service configuration files to service names Set<String> serviceNames = configFiles.stream() .map(FileUtils::toServiceName) .flatMap(Optional::stream) .collect(Collectors.toSet()); // parse each service configuration file for (String sn : serviceNames) { Path entry = zip.getPath(SERVICES_PREFIX + sn); List<String> providerClasses = new ArrayList<>(); try (InputStream in = Files.newInputStream(entry)) { BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); String cn; while ((cn = nextLine(reader)) != null) { if (!cn.isEmpty()) { String pn = packageName(cn); if (!packages.contains(pn)) { String msg = "Provider class " + cn + " not in module"; throw new InvalidModuleDescriptorException(msg); } providerClasses.add(cn); } } } if (!providerClasses.isEmpty()) builder.provides(sn, providerClasses); } return builder.build(); } /** * Reads the next line from the given reader and trims it of comments and * leading/trailing white space. * * Returns null if the reader is at EOF. */ private static String nextLine(BufferedReader reader) throws IOException { String ln = reader.readLine(); if (ln != null) { int ci = ln.indexOf('#'); if (ci >= 0) ln = ln.substring(0, ci); ln = ln.trim(); } return ln; } /** * Maps a type name to its package name. */ private static String packageName(String cn) { int index = cn.lastIndexOf('.'); return (index == -1) ? "" : cn.substring(0, index); } /** * Maps the name of an entry in a JAR or ZIP file to a package name. * * @throws InvalidModuleDescriptorException * if the name is a class file in the top-level directory of the * JAR/ZIP file (and it's not module-info.class) */ private static Optional<String> toPackageName(String name) { int index = name.lastIndexOf("/"); if (index == -1) { if (name.endsWith(".class") && !name.equals("module-info.class")) { String msg = name + " found in top-level directory" + " (unnamed package not allowed in module)"; throw new InvalidModuleDescriptorException(msg); } return Optional.empty(); } String pn = name.substring(0, index).replace('/', '.'); if (StringUtils.isClassName(pn)) { return Optional.of(pn); } else { // not a valid package name return Optional.empty(); } } /** * Returns the service type corresponding to the name of a services * configuration file if it is a legal type name. * * For example, if called with "META-INF/services/p.S" then this method returns * a container with the value "p.S". */ private static Optional<String> toServiceName(String cf) { int index = cf.lastIndexOf("/") + 1; if (index < cf.length()) { String prefix = cf.substring(0, index); if (prefix.equals(SERVICES_PREFIX)) { String sn = cf.substring(index); if (StringUtils.isClassName(sn)) return Optional.of(sn); } } return Optional.empty(); } }