1 /*
   2  * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 package jdk.test.lib.util;
  25 
  26 import java.io.ByteArrayOutputStream;
  27 import java.io.File;
  28 import java.io.FileInputStream;
  29 import java.io.FileNotFoundException;
  30 import java.io.FileOutputStream;
  31 import java.io.IOException;
  32 import java.io.OutputStream;
  33 import java.nio.file.Files;
  34 import java.nio.file.InvalidPathException;
  35 import java.nio.file.Path;
  36 import java.nio.file.Paths;
  37 import java.nio.file.StandardCopyOption;
  38 import java.util.ArrayList;
  39 import java.util.Enumeration;
  40 import java.util.HashMap;
  41 import java.util.List;
  42 import java.util.Map;
  43 import java.util.Set;
  44 import java.util.jar.JarEntry;
  45 import java.util.jar.JarFile;
  46 import java.util.jar.JarOutputStream;
  47 import java.util.jar.Manifest;
  48 import java.util.stream.Collectors;
  49 import java.util.stream.Stream;
  50 
  51 /**
  52  * This class consists exclusively of static utility methods that are useful
  53  * for creating and manipulating JAR files.
  54  */
  55 public final class JarUtils {
  56     private JarUtils() { }
  57 
  58     /**
  59      * Creates a JAR file.
  60      *
  61      * Equivalent to {@code jar cfm <jarfile> <manifest> -C <dir> file...}
  62      *
  63      * The input files are resolved against the given directory. Any input
  64      * files that are directories are processed recursively.
  65      */
  66     public static void createJarFile(Path jarfile, Manifest man, Path dir, Path... files)
  67             throws IOException
  68     {
  69         // create the target directory
  70         Path parent = jarfile.getParent();
  71         if (parent != null) {
  72             Files.createDirectories(parent);
  73         }
  74 
  75         List<Path> entries = findAllRegularFiles(dir, files);
  76 
  77         try (OutputStream out = Files.newOutputStream(jarfile);
  78              JarOutputStream jos = new JarOutputStream(out)) {
  79             if (man != null) {
  80                 JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
  81                 jos.putNextEntry(je);
  82                 man.write(jos);
  83                 jos.closeEntry();
  84             }
  85 
  86             for (Path entry : entries) {
  87                 String name = toJarEntryName(entry);
  88                 jos.putNextEntry(new JarEntry(name));
  89                 Files.copy(dir.resolve(entry), jos);
  90                 jos.closeEntry();
  91             }
  92         }
  93     }
  94 
  95     /**
  96      * Creates a JAR file.
  97      *
  98      * Equivalent to {@code jar cf <jarfile>  -C <dir> file...}
  99      *
 100      * The input files are resolved against the given directory. Any input
 101      * files that are directories are processed recursively.
 102      */
 103     public static void createJarFile(Path jarfile, Path dir, Path... files)
 104             throws IOException
 105     {
 106         createJarFile(jarfile, null, dir, files);
 107     }
 108 
 109     /**
 110      * Creates a JAR file from the contents of a directory.
 111      *
 112      * Equivalent to {@code jar cf <jarfile> -C <dir> .}
 113      */
 114     public static void createJarFile(Path jarfile, Path dir) throws IOException {
 115         createJarFile(jarfile, dir, Paths.get("."));
 116     }
 117 
 118 
 119     /**
 120      * Creates a JAR file.
 121      *
 122      * Equivalent to {@code jar cf <jarfile> -C <dir> file...}
 123      *
 124      * The input files are resolved against the given directory. Any input
 125      * files that are directories are processed recursively.
 126      */
 127     public static void createJarFile(Path jarfile, Path dir, String... input)
 128             throws IOException
 129     {
 130         Path[] paths = Stream.of(input).map(Paths::get).toArray(Path[]::new);
 131         createJarFile(jarfile, dir, paths);
 132     }
 133 
 134     /**
 135      * Updates a JAR file.
 136      *
 137      * Equivalent to {@code jar uf <jarfile> -C <dir> file...}
 138      *
 139      * The input files are resolved against the given directory. Any input
 140      * files that are directories are processed recursively.
 141      */
 142     public static void updateJarFile(Path jarfile, Path dir, Path... files)
 143             throws IOException
 144     {
 145         List<Path> entries = findAllRegularFiles(dir, files);
 146 
 147         Set<String> names = entries.stream()
 148                                    .map(JarUtils::toJarEntryName)
 149                                    .collect(Collectors.toSet());
 150 
 151         Path tmpfile = Files.createTempFile("jar", "jar");
 152 
 153         try (OutputStream out = Files.newOutputStream(tmpfile);
 154              JarOutputStream jos = new JarOutputStream(out)) {
 155             // copy existing entries from the original JAR file
 156             try (JarFile jf = new JarFile(jarfile.toString())) {
 157                 Enumeration<JarEntry> jentries = jf.entries();
 158                 while (jentries.hasMoreElements()) {
 159                     JarEntry jentry = jentries.nextElement();
 160                     if (!names.contains(jentry.getName())) {
 161                         jos.putNextEntry(copyEntry(jentry));
 162                         jf.getInputStream(jentry).transferTo(jos);
 163                     }
 164                 }
 165             }
 166 
 167             // add the new entries
 168             for (Path entry : entries) {
 169                 String name = toJarEntryName(entry);
 170                 jos.putNextEntry(new JarEntry(name));
 171                 Files.copy(dir.resolve(entry), jos);
 172             }
 173         }
 174 
 175         // replace the original JAR file
 176         Files.move(tmpfile, jarfile, StandardCopyOption.REPLACE_EXISTING);
 177     }
 178 
 179     /**
 180      * Updates a JAR file.
 181      *
 182      * Equivalent to {@code jar uf <jarfile> -C <dir> .}
 183      */
 184     public static void updateJarFile(Path jarfile, Path dir) throws IOException {
 185         updateJarFile(jarfile, dir, Paths.get("."));
 186     }
 187 
 188 
 189     /**
 190      * Create jar file with specified files. If a specified file does not exist,
 191      * a new jar entry will be created with the file name itself as the content.
 192      */
 193     @Deprecated
 194     public static void createJar(String dest, String... files)
 195             throws IOException {
 196         try (JarOutputStream jos = new JarOutputStream(
 197                 new FileOutputStream(dest), new Manifest())) {
 198             for (String file : files) {
 199                 System.out.println(String.format("Adding %s to %s",
 200                         file, dest));
 201 
 202                 // add an archive entry, and write a file
 203                 jos.putNextEntry(new JarEntry(file));
 204                 try (FileInputStream fis = new FileInputStream(file)) {
 205                     fis.transferTo(jos);
 206                 } catch (FileNotFoundException e) {
 207                     jos.write(file.getBytes());
 208                 }
 209             }
 210         }
 211         System.out.println();
 212     }
 213 
 214     /**
 215      * Add or remove specified files to existing jar file. If a specified file
 216      * to be updated or added does not exist, the jar entry will be created
 217      * with the file name itself as the content.
 218      *
 219      * @param src the original jar file name
 220      * @param dest the new jar file name
 221      * @param files the files to update. The list is broken into 2 groups
 222      *              by a "-" string. The files before in the 1st group will
 223      *              be either updated or added. The files in the 2nd group
 224      *              will be removed. If no "-" exists, all files belong to
 225      *              the 1st group.
 226      * @throws IOException if there is an error
 227      */
 228     @Deprecated
 229     public static void updateJar(String src, String dest, String... files)
 230             throws IOException {
 231         Map<String,Object> changes = new HashMap<>();
 232         boolean update = true;
 233         for (String file : files) {
 234             if (file.equals("-")) {
 235                 update = false;
 236             } else if (update) {
 237                 try {
 238                     Path p = Paths.get(file);
 239                     if (Files.exists(p)) {
 240                         changes.put(file, p);
 241                     } else {
 242                         changes.put(file, file);
 243                     }
 244                 } catch (InvalidPathException e) {
 245                     // Fallback if file not a valid Path.
 246                     changes.put(file, file);
 247                 }
 248             } else {
 249                 changes.put(file, Boolean.FALSE);
 250             }
 251         }
 252         updateJar(src, dest, changes);
 253     }
 254 
 255     /**
 256      * Update content of a jar file.
 257      *
 258      * @param src the original jar file name
 259      * @param dest the new jar file name
 260      * @param changes a map of changes, key is jar entry name, value is content.
 261      *                Value can be Path, byte[] or String. If key exists in
 262      *                src but value is Boolean FALSE. The entry is removed.
 263      *                Existing entries in src not a key is unmodified.
 264      * @throws IOException if there is an error
 265      */
 266     @Deprecated
 267     public static void updateJar(String src, String dest,
 268                                  Map<String,Object> changes)
 269             throws IOException {
 270 
 271         // What if input changes is immutable?
 272         changes = new HashMap<>(changes);
 273 
 274         System.out.printf("Creating %s from %s...\n", dest, src);
 275 
 276         if (dest.equals(src)) {
 277             throw new IOException("src and dest cannot be the same");
 278         }
 279 
 280         try (JarOutputStream jos = new JarOutputStream(
 281                 new FileOutputStream(dest))) {
 282 
 283             try (JarFile srcJarFile = new JarFile(src)) {
 284                 Enumeration<JarEntry> entries = srcJarFile.entries();
 285                 while (entries.hasMoreElements()) {
 286                     JarEntry entry = entries.nextElement();
 287                     String name = entry.getName();
 288                     if (changes.containsKey(name)) {
 289                         System.out.println(String.format("- Update %s", name));
 290                         updateEntry(jos, name, changes.get(name));
 291                         changes.remove(name);
 292                     } else {
 293                         System.out.println(String.format("- Copy %s", name));
 294                         jos.putNextEntry(copyEntry(entry));
 295                         srcJarFile.getInputStream(entry).transferTo(jos);
 296                     }
 297                 }
 298             }
 299             for (Map.Entry<String, Object> e : changes.entrySet()) {
 300                 System.out.println(String.format("- Add %s", e.getKey()));
 301                 updateEntry(jos, e.getKey(), e.getValue());
 302             }
 303         }
 304         System.out.println();
 305     }
 306 
 307     /**
 308      * Update the Manifest inside a jar.
 309      *
 310      * @param src the original jar file name
 311      * @param dest the new jar file name
 312      * @param man the Manifest
 313      *
 314      * @throws IOException
 315      */
 316     public static void updateManifest(String src, String dest, Manifest man)
 317             throws IOException {
 318         ByteArrayOutputStream bout = new ByteArrayOutputStream();
 319         man.write(bout);
 320         updateJar(src, dest, Map.of(JarFile.MANIFEST_NAME, bout.toByteArray()));
 321     }
 322 
 323     private static void updateEntry(JarOutputStream jos, String name, Object content)
 324            throws IOException {
 325         if (content instanceof Boolean) {
 326             if (((Boolean) content).booleanValue()) {
 327                 throw new RuntimeException("Boolean value must be FALSE");
 328             }
 329         } else {
 330             jos.putNextEntry(new JarEntry(name));
 331             if (content instanceof Path) {
 332                 Files.newInputStream((Path) content).transferTo(jos);
 333             } else if (content instanceof byte[]) {
 334                 jos.write((byte[]) content);
 335             } else if (content instanceof String) {
 336                 jos.write(((String) content).getBytes());
 337             } else {
 338                 throw new RuntimeException("Unknown type " + content.getClass());
 339             }
 340         }
 341     }
 342 
 343     /**
 344      * Maps a file path to the equivalent name in a JAR file
 345      */
 346     private static String toJarEntryName(Path file) {
 347         Path normalized = file.normalize();
 348         return normalized.subpath(0, normalized.getNameCount())  // drop root
 349                          .toString()
 350                          .replace(File.separatorChar, '/');
 351     }
 352 
 353     private static List<Path> findAllRegularFiles(Path dir, Path[] files) throws IOException {
 354         List<Path> entries = new ArrayList<>();
 355         for (Path file : files) {
 356             try (Stream<Path> stream = Files.find(dir.resolve(file), Integer.MAX_VALUE,
 357                     (p, attrs) -> attrs.isRegularFile())) {
 358                 stream.map(dir::relativize)
 359                       .forEach(entries::add);
 360             }
 361         }
 362         return entries;
 363     }
 364 
 365     private static JarEntry copyEntry(JarEntry e1) {
 366         JarEntry e2 = new JarEntry(e1.getName());
 367         e2.setMethod(e1.getMethod());
 368         e2.setTime(e1.getTime());
 369         e2.setComment(e1.getComment());
 370         e2.setExtra(e1.getExtra());
 371         if (e1.getMethod() == JarEntry.STORED) {
 372             e2.setSize(e1.getSize());
 373             e2.setCrc(e1.getCrc());
 374         }
 375         return e2;
 376     }
 377 }