001/**
002 * Please feel free to use any fragment of the code in this file that you need in your own
003 * work. As far as I am concerned, it's in the public domain. No permission is necessary
004 * or required. Credit is always appreciated if you use a large chunk or base a
005 * significant product on one of my examples, but that's not required either.
006 * 
007 * This code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
008 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
009 * PURPOSE.
010 * 
011 * --- Joseph A. Huwaldt
012 */
013package jahuwaldt.io;
014
015import java.io.*;
016import static java.util.Objects.requireNonNull;
017import java.util.zip.ZipEntry;
018import java.util.zip.ZipInputStream;
019import java.util.zip.ZipOutputStream;
020
021/**
022 * This is a utility class of static methods for working with ZIP archive files.
023 * 
024 * <p> Modified by: Joseph A. Huwaldt </p>
025 * 
026 * @author Behrouz Fallahi, Date: April 28, 2000
027 * @version February 23, 2025
028 */
029public final class ZipUtils {
030    //  This class now requires Java 1.7 or later!
031    
032    /**
033     * A list of characters that are illegal on some of the file systems supported by this
034     * program. The characters in this list include:
035     * \n,\r,\t,\0,\f,',?,*,&lt;,&gt;,|,",:,~,@,!,#,[,],=,+,;, and ','.
036     */
037    public static final char[] ILLEGAL_CHARACTERS
038            = {'\n', '\r', '\t', '\0', '\f', '`', '?', '*', '<', '>', '|', '\"', ':', '~', '@', '!', '#',
039                '[', ']', '=', '+', ';', ','};
040
041    private static final int BUFFER_SIZE = 2048;
042
043    /**
044     * Prevent instantiation of this utility class.
045     */
046    private ZipUtils() {
047    }
048    
049    /**
050     * Replace any potentially illegal characters from a file name with '_'.
051     *
052     * @param name The file name to be cleaned of potentially illegal characters. May not
053     *             be null.
054     * @return The input file name with potentially illegal characters replaced with "_".
055     */
056    public static String cleanFileName(String name) {
057        if (name.length() == 0)
058            return name;
059        for (char c : ILLEGAL_CHARACTERS) {
060            name = name.replace(c, '_');
061        }
062        return name;
063    }
064
065    /**
066     * Returns true if the supplied file name contains characters that are illegal on some
067     * file systems.
068     *
069     * @param name The file name to be checked for potentially illegal characters. May not
070     *             be null.
071     * @return true if the file name contains potentially illegal characters, false if it
072     *         is safe.
073     */
074    public static boolean hasIllegalChars(String name) {
075        if (name.length() == 0)
076            return false;
077        for (char c : ILLEGAL_CHARACTERS) {
078            if (name.indexOf(c) >= 0)
079                return true;
080        }
081        return false;
082    }
083
084    /**
085     * Write a ZIP archive to the specified output stream made up of all the contents of
086     * the specified file or directory, using the specified compression method. If any of
087     * the files in the specified directory contain characters that are illegal on some
088     * file systems, then those files are skipped and not included in the archive. The
089     * output stream is closed by this method.
090     *
091     * @param fileOrDir The file or directory to be compressed into a ZIP archive. May not
092     *                  be null.
093     * @param out       The output stream to write the ZIP archive data to. May not be
094     *                  null.
095     * @param zipLevel  The compression level (0-9) or java.util.zip.Deflater.DEFAULT_COMPRESSION.
096     * @throws java.io.IOException If there is any problem writing out the ZIP stream.
097     * @see java.util.zip.Deflater
098     */
099    public static void writeZip(File fileOrDir, OutputStream out, int zipLevel) throws IOException {
100        requireNonNull(fileOrDir, "fileOrDir == null");
101        requireNonNull(out, "out == null");
102
103        //  Create a ZIP output stream to the specified output file.
104        try (ZipOutputStream zos = new ZipOutputStream(out)) {
105            zos.setLevel(zipLevel);
106
107            if (fileOrDir.isDirectory())
108                addDir(null, fileOrDir, zos, new byte[BUFFER_SIZE]);
109            else
110                addFile(null, fileOrDir, zos, new byte[BUFFER_SIZE]);
111        }
112    }
113
114    /**
115     * Create a ZIP archive file made up of all the contents of the specified file or
116     * directory, using the specified compression method. If any of the files in the
117     * specified directory contain characters that are illegal on some file systems, then
118     * those files are skipped and not included in the archive.
119     *
120     * @param fileOrDir The file or directory to be compressed into a ZIP archive. May not
121     *                  be null.
122     * @param zipFile   The ZIP file to be written out. May not be null.
123     * @param zipLevel  The compression level (0-9) or
124     *                  java.util.zip.ZipOutputStream.DEFAULT_COMPRESSION.
125     * @throws java.io.IOException If there is any problem writing out the ZIP file.
126     * @see java.util.zip.ZipOutputStream
127     */
128    public static void writeZip(File fileOrDir, File zipFile, int zipLevel) throws IOException {
129        requireNonNull(fileOrDir, "fileOrDir == null");
130        requireNonNull(zipFile, "zipFile == null");
131        try (FileOutputStream out = new FileOutputStream(zipFile)) {
132            writeZip(fileOrDir, out, zipLevel);
133        }
134    }
135
136    /**
137     * Create a ZIP archive file made up of all the contents of the specified file or
138     * directory. If any of the files in the specified directory contain characters that
139     * are illegal on some file systems, then those files are skipped and not included in
140     * the archive.
141     *
142     * @param fileOrDir The file or directory to be compressed into a ZIP archive. May not
143     *                  be null.
144     * @param zipFile   The ZIP file to be written out. May not be null.
145     * @throws java.io.IOException If there is any problem writing out the ZIP file.
146     */
147    public static void writeZip(File fileOrDir, File zipFile) throws IOException {
148        requireNonNull(fileOrDir, "fileOrDir == null");
149        requireNonNull(zipFile, "zipFile == null");
150        writeZip(fileOrDir, zipFile, java.util.zip.Deflater.DEFAULT_COMPRESSION);
151    }
152
153    /**
154     * Extracts a ZIP archive file to the specified directory. If the ZIP archive contains
155     * files with characters that might be illegal on some file systems, those characters
156     * are replaced with underline characters, '_'.
157     *
158     * @param input  An InputStream from a ZIP archive. May not be null.
159     * @param outDir The directory to extract the ZIP file into. May not be null.
160     * @throws java.io.IOException If there is any problem extracting from the ZIP stream.
161     */
162    public static void extractZip(InputStream input, File outDir) throws IOException {
163        requireNonNull(input, "input == null");
164        requireNonNull(outDir, "outDir == null");
165
166        try (ZipInputStream zis = new ZipInputStream(input)) {
167            ZipEntry entry;
168            while ((entry = zis.getNextEntry()) != null) {
169                //  Sanitize the file name to remove any potentially illegal characters.
170                String name = cleanFileName(entry.getName());
171
172                if (entry.isDirectory()) {
173                    //  Create the directory.
174                    File d = new File(outDir, name);
175                    if (!d.exists())
176                        d.mkdir();
177
178                } else {
179                    //  Make sure that the directory for the file exists.
180                    File file = new File(outDir, name);
181                    File dir = file.getParentFile();
182                    if (dir != null && !dir.exists())
183                        dir.mkdir();
184
185                    //  Write out the file.
186                    try (FileOutputStream fos = new FileOutputStream(file)) {
187                        byte buffer[] = new byte[BUFFER_SIZE];
188                        int count;
189                        while ((count = zis.read(buffer)) != -1)
190                            fos.write(buffer, 0, count);
191                    }
192                }
193            }
194        }
195
196    }
197
198    /**
199     * Extracts a ZIP archive file to the specified directory. If the ZIP archive contains
200     * files with characters that might be illegal on some file systems, those characters
201     * are replaced with underline characters, '_'.
202     *
203     *
204     * @param zipFile The ZIP file to be extracted. May not be null.
205     * @param outDir  The directory to extract the ZIP file into. May not be null.
206     * @throws java.io.IOException If there is any problem extracting the ZIP file.
207     */
208    public static void extractZip(File zipFile, File outDir) throws IOException {
209        requireNonNull(zipFile, "zipFile == null");
210        requireNonNull(outDir, "outDir == null");
211
212        try (FileInputStream fis = new FileInputStream(zipFile)) {
213            extractZip(fis, outDir);
214        }
215    }
216
217    /**
218     * Add the specified directory, and all of it's contents, to the specified ZIP archive
219     * file.
220     *
221     * @param rootPath The path, in the ZIP archive, of the directory containing this
222     *                 directory (including the trailing "/").  <code>null</code> or ""
223     *                 will place this directory at the top of the ZIP archive.
224     * @param dir      The directory to be added to the ZIP archive.
225     * @param zos      An output stream pointing to the ZIP archive file.
226     * @param buffer   A byte buffer used as temporary storage to read/write the data.
227     */
228    private static void addDir(String rootPath, File dir, ZipOutputStream zos, byte[] buffer) throws IOException {
229        if (rootPath == null)
230            rootPath = "";
231
232        // Get a listing of the directory contents
233        File[] dirList = dir.listFiles();
234
235        // Loop through the directory listing, and zip all the files.
236        for (File f : dirList) {
237            if (f.isDirectory()) {
238                // If the File object is a directory, call this
239                // function again to add its content recursively.
240
241                // Create a new zip entry for a directory (they end in "/").
242                rootPath += cleanFileName(f.getName()) + "/";
243                ZipEntry anEntry = new ZipEntry(rootPath);
244
245                // Place the zip entry in the ZipOutputStream object
246                zos.putNextEntry(anEntry);
247
248                //  Call this method again on the sub-directory.
249                addDir(rootPath, f, zos, buffer);
250
251            } else {
252                // If we reached here, the File object f was not a directory
253                addFile(rootPath, f, zos, buffer);
254            }
255        }
256
257    }
258
259    /**
260     * Add the specified file to the specified ZIP archive. Files with character names
261     * that may be illegal on some file systems will be skipped.
262     *
263     * @param rootPath The path, in the ZIP archive, of the directory containing this file
264     *                 (including the trailing "/").  <code>null</code> or "" will place
265     *                 this file at the top of the ZIP archive.
266     *
267     * @param file     The file to be added to the ZIP archive.
268     * @param zos      An output stream pointing to the ZIP archive file.
269     * @param buffer   A byte buffer used as temporary storage to read/write the data.
270     */
271    private static void addFile(String rootPath, File file, ZipOutputStream zos, byte[] buffer) throws IOException {
272
273        // If the file name contains illegal characters, just skip it.
274        if (hasIllegalChars(file.getName()))
275            return;
276
277        // Create an input stream from the file.
278        try (FileInputStream fis = new FileInputStream(file)) {
279            // Create a new zip entry
280            if (rootPath == null)
281                rootPath = "";
282            ZipEntry anEntry = new ZipEntry(rootPath + file.getName());
283
284            // Place the zip entry in the ZipOutputStream object
285            zos.putNextEntry(anEntry);
286
287            // Now write the content of the file to the ZipOutputStream
288            int bytesIn;
289            while ((bytesIn = fis.read(buffer)) != -1)
290                zos.write(buffer, 0, bytesIn);
291        }
292
293    }
294
295}