content: /*
   *  Licensed to the Apache Software Foundation (ASF) under one or more
   *  contributor license agreements.  See the NOTICE file distributed with
   *  this work for additional information regarding copyright ownership.
   *  The ASF licenses this file to You 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
   *
   *      https://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.
   *
   */
  /**
   * jlink.java links together multiple .jar files Original code by Patrick
   * Beard. Modifications to work with ANT by Matthew Kuperus Heun.
   *
   */
  package org.apache.tools.ant.taskdefs.optional.jlink;
  
  import java.io.BufferedInputStream;
  import java.io.File;
  import java.io.IOException;
  import java.io.InputStream;
  import java.nio.file.Files;
  import java.nio.file.Paths;
  import java.util.Enumeration;
  import java.util.List;
  import java.util.Vector;
  import java.util.zip.CRC32;
  import java.util.zip.Deflater;
  import java.util.zip.ZipEntry;
  import java.util.zip.ZipException;
  import java.util.zip.ZipFile;
  import java.util.zip.ZipOutputStream;
  
  // CheckStyle:TypeNameCheck OFF - bc
  /**
   * jlink links together multiple .jar files.
   */
  public class jlink {
      private static final int BUFFER_SIZE = 8192;
      private static final int VECTOR_INIT_SIZE = 10;
  
      private String outfile = null;
  
      private List<String> mergefiles = new Vector<>(VECTOR_INIT_SIZE);
  
      private List<String> addfiles = new Vector<>(VECTOR_INIT_SIZE);
  
      private boolean compression = false;
  
      // CheckStyle:VisibilityModifier OFF - bc
  
      byte[] buffer = new byte[BUFFER_SIZE];
  
      // CheckStyle:VisibilityModifier OFF - bc
  
      /** The file that will be created by this instance of jlink.
       * @param outfile the file to create.
       */
      public void setOutfile(String outfile) {
          if (outfile == null) {
              return;
          }
          this.outfile = outfile;
      }
  
      /**
       * Adds a file to be merged into the output.
       * @param fileToMerge the file to merge into the output.
       */
      public void addMergeFile(String fileToMerge) {
          if (fileToMerge == null) {
              return;
          }
          mergefiles.add(fileToMerge);
      }
  
      /** Adds a file to be added into the output.
       * @param fileToAdd the file to add to the output.
       */
      public void addAddFile(String fileToAdd) {
          if (fileToAdd == null) {
              return;
          }
          addfiles.add(fileToAdd);
      }
  
      /**
       * Adds several files to be merged into the output.
       * @param filesToMerge an array of files to merge into the output.
       */
      public void addMergeFiles(String... filesToMerge) {
          if (filesToMerge == null) {
              return;
          }
          for (String element : filesToMerge) {
              addMergeFile(element);
          }
      }
  
      /**
       * Adds several files to be added into the output.
       * @param filesToAdd an array of files to add to the output.
       */
      public void addAddFiles(String... filesToAdd) {
          if (filesToAdd == null) {
              return;
          }
          for (String element : filesToAdd) {
              addAddFile(element);
          }
      }
  
      /**
       * Determines whether output will be compressed.
       * @param compress if true use compression.
       */
      public void setCompression(boolean compress) {
          this.compression = compress;
      }
  
      /**
       * Performs the linking of files. Addfiles are added to the output as-is.
       * For example, a jar file is added to the output as a jar file. However,
       * mergefiles are first examined for their type. If it is a jar or zip
       * file, the contents will be extracted from the mergefile and entered
       * into the output. If a zip or jar file is encountered in a subdirectory
       * it will be added, not merged. If a directory is encountered, it becomes
       * the root entry of all the files below it. Thus, you can provide
       * multiple, disjoint directories, as addfiles: they will all be added in
       * a rational manner to outfile.
       * @throws Exception on error.
       */
      public void link() throws Exception { //NOSONAR
          try (ZipOutputStream output =
              new ZipOutputStream(Files.newOutputStream(Paths.get(outfile)))) {
  
              if (compression) {
                  output.setMethod(ZipOutputStream.DEFLATED);
                  output.setLevel(Deflater.DEFAULT_COMPRESSION);
              } else {
                  output.setMethod(ZipOutputStream.STORED);
              }
              for (String path : mergefiles) {
                  File f = new File(path);
  
                  if (f.getName().endsWith(".jar")
                      || f.getName().endsWith(".zip")) {
                      //Do the merge
                      mergeZipJarContents(output, f);
                  } else {
                      //Add this file to the addfiles Vector and add it
                      //later at the top level of the output file.
                      addAddFile(path);
                  }
              }
              for (String name : addfiles) {
                  File f = new File(name);
  
                  if (f.isDirectory()) {
                      addDirContents(output, f, f.getName() + '/', compression);
                  } else {
                      addFile(output, f, "", compression);
                  }
              }
          }
      }
  
      /**
       * The command line entry point for jlink.
       * @param args an array of arguments
       */
      public static void main(String[] args) {
          // jlink output input1 ... inputN
          if (args.length < 2) {
              System.out.println("usage: jlink output input1 ... inputN");
              System.exit(1);
          }
          jlink linker = new jlink();
  
          linker.setOutfile(args[0]);
          // To maintain compatibility with the command-line version,
          // we will only add files to be merged.
          for (int i = 1; i < args.length; i++) {
              linker.addMergeFile(args[i]);
          }
          try {
              linker.link();
          } catch (Exception ex) {
              System.err.print(ex.getMessage());
          }
      }
  
      /*
       * Actually performs the merging of f into the output.
       * f should be a zip or jar file.
       */
      private void mergeZipJarContents(ZipOutputStream output, File f) throws IOException {
          //Check to see that the file with name "name" exists.
          if (!f.exists()) {
              return;
          }
          try (ZipFile zipf = new ZipFile(f)) {
              Enumeration<? extends ZipEntry> entries = zipf.entries();
              while (entries.hasMoreElements()) {
                  ZipEntry inputEntry = entries.nextElement();
                  //Ignore manifest entries.  They're bound to cause conflicts between
                  //files that are being merged.  User should supply their own
                  //manifest file when doing the merge.
                  String inputEntryName = inputEntry.getName();
                  int index = inputEntryName.indexOf("META-INF");
  
                  if (index < 0) {
                      //META-INF not found in the name of the entry. Go ahead and process it.
                      try {
                          output.putNextEntry(processEntry(zipf, inputEntry));
                      } catch (ZipException ex) {
                          //If we get here, it could be because we are trying to put a
                          //directory entry that already exists.
                          //For example, we're trying to write "com", but a previous
                          //entry from another mergefile was called "com".
                          //In that case, just ignore the error and go on to the
                          //next entry.
                          if (ex.getMessage().contains("duplicate")) {
                              //It was the duplicate entry.
                              continue;
                          }
                          // I hate to admit it, but we don't know what happened
                          // here.  Throw the Exception.
                          throw ex;
                      }
  
                      try (InputStream in = zipf.getInputStream(inputEntry)) {
                          int len = buffer.length;
                          int count = -1;
  
                          while ((count = in.read(buffer, 0, len)) > 0) {
                              output.write(buffer, 0, count);
                          }
                          output.closeEntry();
                      }
                  }
              }
          }
      }
  
      /*
       * Adds contents of a directory to the output.
       */
      private void addDirContents(ZipOutputStream output, File dir, String prefix,
                                  boolean compress) throws IOException {
          String[] names = dir.list();
          if (names == null || names.length == 0) {
              return;
          }
          for (String name : names) {
              File file = new File(dir, name);
  
              if (file.isDirectory()) {
                  addDirContents(output, file, prefix + name + '/', compress);
              } else {
                  addFile(output, file, prefix, compress);
              }
          }
      }
  
      /*
       * Gets the name of an entry in the file.  This is the real name
       * which for a class is the name of the package with the class
       * name appended.
       */
      private String getEntryName(File file, String prefix) {
          String name = file.getName();
  
          if (!name.endsWith(".class")) {
              // see if the file is in fact a .class file, and determine its actual name.
              try (InputStream input = Files.newInputStream(file.toPath())) {
                  String className = ClassNameReader.getClassName(input);
  
                  if (className != null) {
                      return className.replace('.', '/') + ".class";
                  }
              } catch (IOException ioe) {
                  //do nothing
              }
          }
          System.out.printf(
              "From %1$s and prefix %2$s, creating entry %2$s%3$s%n",
              file.getPath(), prefix, name);
          return prefix + name;
      }
  
  
      /*
       * Adds a file to the output stream.
       */
      private void addFile(ZipOutputStream output, File file, String prefix,
                           boolean compress) throws IOException {
          //Make sure file exists
          if (!file.exists()) {
              return;
          }
          ZipEntry entry = new ZipEntry(getEntryName(file, prefix));
  
          entry.setTime(file.lastModified());
          entry.setSize(file.length());
          if (!compress) {
              entry.setCrc(calcChecksum(file));
          }
          addToOutputStream(output, Files.newInputStream(file.toPath()), entry);
      }
  
      /*
       * A convenience method that several other methods might call.
       */
      private void addToOutputStream(ZipOutputStream output, InputStream input,
                                     ZipEntry ze) throws IOException {
          try {
              output.putNextEntry(ze);
          } catch (ZipException zipEx) {
              //This entry already exists. So, go with the first one.
              input.close();
              return;
          }
  
          int numBytes;
  
          while ((numBytes = input.read(buffer)) > 0) {
              output.write(buffer, 0, numBytes);
          }
          output.closeEntry();
          input.close();
      }
  
      /*
       * A method that does the work on a given entry in a mergefile.
       * The big deal is to set the right parameters in the ZipEntry
       * on the output stream.
       */
      private ZipEntry processEntry(ZipFile zip, ZipEntry inputEntry) {
          /*
            First, some notes.
            On MRJ 2.2.2, getting the size, compressed size, and CRC32 from the
            ZipInputStream does not work for compressed (deflated) files.  Those calls return -1.
            For uncompressed (stored) files, those calls do work.
            However, using ZipFile.getEntries() works for both compressed and
            uncompressed files.
  
            Now, from some simple testing I did, it seems that the value of CRC-32 is
            independent of the compression setting. So, it should be easy to pass this
            information on to the output entry.
          */
          String name = inputEntry.getName();
  
          if (!inputEntry.isDirectory() && !name.endsWith(".class")) {
              try (InputStream input = zip.getInputStream(zip.getEntry(name))) {
                  String className = ClassNameReader.getClassName(input);
  
                  if (className != null) {
                      name = className.replace('.', '/') + ".class";
                  }
              } catch (IOException ioe) {
                  //do nothing
              }
          }
          ZipEntry outputEntry = new ZipEntry(name);
  
          outputEntry.setTime(inputEntry.getTime());
          outputEntry.setExtra(inputEntry.getExtra());
          outputEntry.setComment(inputEntry.getComment());
          outputEntry.setTime(inputEntry.getTime());
          if (compression) {
              outputEntry.setMethod(ZipEntry.DEFLATED);
              //Note, don't need to specify size or crc for compressed files.
          } else {
              outputEntry.setMethod(ZipEntry.STORED);
              outputEntry.setCrc(inputEntry.getCrc());
              outputEntry.setSize(inputEntry.getSize());
          }
          return outputEntry;
      }
  
      /*
       * Necessary in the case where you add a entry that
       * is not compressed.
       */
      private long calcChecksum(File f) throws IOException {
          return calcChecksum(
              new BufferedInputStream(Files.newInputStream(f.toPath())));
      }
  
      /*
       * Necessary in the case where you add a entry that
       * is not compressed.
       */
      private long calcChecksum(InputStream in) throws IOException {
          CRC32 crc = new CRC32();
          int len = buffer.length;
          int count;
  
          while ((count = in.read(buffer, 0, len)) > 0) {
              crc.update(buffer, 0, count);
          }
          in.close();
          return crc.getValue();
      }
  
  }
  
  
  