/* * $Id$ * * Copyright (C) 2003-2015 JNode.org * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published * by the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library; If not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package org.jnode.command.archive; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.io.InputStream; import java.io.LineNumberReader; import java.io.OutputStream; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import org.apache.tools.zip.ZipEntry; import org.apache.tools.zip.ZipExtraField; import org.apache.tools.zip.ZipFile; import org.apache.tools.zip.ZipOutputStream; import org.jnode.command.util.AbstractDirectoryWalker; import org.jnode.command.util.AbstractDirectoryWalker.ModTimeFilter; import org.jnode.command.util.AbstractDirectoryWalker.PathnamePatternFilter; import org.jnode.shell.PathnamePattern; import org.jnode.shell.syntax.Argument; import org.jnode.shell.syntax.FileArgument; import org.jnode.shell.syntax.FlagArgument; import org.jnode.shell.syntax.StringArgument; /** * TODO test return codes * TODO make sure no archive is created if the creation fails * TODO implement delete * TODO implement update * TODO implement freshen * @author chris boeriten */ public class Zip extends ArchiveCommand { private static final boolean DEBUG = true; private static final String help_delete = "remove the list of entries from the archive"; private static final String help_freshen = "[zip] Replaces entries in the archive with files from the file " + "system if they exist and are newer than the entry.\n[unzip] Replaces " + "files on the file system with entries from the archive if they exist " + "and are newer than the file."; private static final String help_update = "Like freshen, except it will also add files if they do not exist"; private static final String help_test = "[zip] Tests the archive before finishing. If the archive is corrupt " + "then the original archive is restored, if any. This will also skip " + "deleting files in a move operation.\n[unzip] Tests the archive, " + "reporting wether the archive is corrupt or not."; private static final String help_move = "add the list of files to the archive, removing them from the file system"; private static final String help_list = "list the contents of the archive"; private static final String help_no_path = "store/extract the file with only its file name and no path prefix"; private static final String help_archive = "the zip archive to use"; private static final String help_patterns = "file matching patterns(wildcards)"; private static final String fmt_extract = "%11s: %s"; private static final String fmt_footer = " %8d %8d %d files"; private static final String fmt_entry = " %8d %8d %d %s"; private static final String fmt_warn_dup = "%s: %s: %s"; private static final String fatal_create_arch = "Could not create archive: "; private static final String fatal_req_arch = "Archive required but not found: "; private static final String fatal_create_zfile = "Unable to open archive as ZipFile: "; private static final String fatal_inv_args = "zip error: Invalid arguments (cannot repeat names in zip file)"; private static final String fatal_walking = "Exception while walking."; private static final String fatal_read_stdin = "Exception while reading stdin."; private static final String str_header_1 = " Size CSize Date Time M Name"; private static final String str_header_2 = " -------- -------- -------- ----- - ----"; private static final String str_footer = " -------- -------- -------"; private static final String str_archive = "Archive: "; private static final String str_creating = "creating"; private static final String str_inflating = "inflating"; private static final String str_adding = "adding"; private static final String str_zip_warn = "zip warning"; private static final String str_fullname_1 = " first full name"; private static final String str_fullname_2 = "second full name"; private static final String str_name_repeat = "name in zip file repeated"; protected final StringArgument Patterns; protected final FileArgument Archive; protected final FlagArgument Delete; protected final FlagArgument Freshen; protected final FlagArgument Update; protected final FlagArgument Test; protected final FlagArgument Move; protected final FlagArgument List; protected final FlagArgument NoPath; private static final int ZIP_ADD = 0x01; private static final int ZIP_MOVE = 0x02; private static final int ZIP_EXTRACT = 0x04; private static final int ZIP_DELETE = 0x08; private static final int ZIP_LIST = 0x10; private static final int ZIP_TEST = 0x20; private static final int ZIP_FRESHEN = 0x40; private static final int ZIP_UPDATE = 0x80; private static final int ZIP_ALL = 0x3F; private static final int ZIP_INSERT = ZIP_ADD | ZIP_MOVE; @SuppressWarnings("unused") private static final int ZIP_REQ_ARCH = ZIP_ALL & ~ZIP_INSERT; /* Populated in ZipCommand and UnzipCommand */ protected List<String> includes; protected List<String> excludes; protected List<String> excludeDirs; private List<File> files; private List<ZipEntry> fileEntries; private List<ZipEntry> dirEntries; protected long newer; protected long older; private File archive; private ZipFile zarchive; protected File tmpDir; protected String[] noCompress; protected int mode; protected boolean ignore_case; protected boolean keep; protected boolean overwrite; protected boolean backup; protected boolean noDirEntry; protected boolean noPath; protected boolean recurse; protected boolean filesStdin; protected boolean useStdout; public Zip(String s) { super(s); Delete = new FlagArgument("delete", Argument.OPTIONAL, help_delete); Freshen = new FlagArgument("freshen", Argument.OPTIONAL, help_freshen); Update = new FlagArgument("update", Argument.OPTIONAL, help_update); Test = new FlagArgument("test", Argument.OPTIONAL, help_test); Move = new FlagArgument("move", Argument.OPTIONAL, help_move); List = new FlagArgument("list", Argument.OPTIONAL, help_list); NoPath = new FlagArgument("no-path", Argument.OPTIONAL, help_no_path); Patterns = new StringArgument("patterns", Argument.OPTIONAL | Argument.MULTIPLE, help_patterns); Archive = new FileArgument("archive", Argument.MANDATORY, help_archive); } public void execute(String command) { super.execute("zcat"); parseOptions(command); try { if ((mode & ZIP_ADD) != 0) { insert(); if ((mode & ZIP_MOVE) != 0) { for (File file : files) { file.delete(); } } return; } if (mode == ZIP_EXTRACT) { extract(); return; } if (mode == ZIP_LIST) { list(); return; } } catch (Exception e) { e.printStackTrace(); } finally { close(zarchive); exit(0); } } private void insert() throws IOException { ZipOutputStream zout = null; ZipEntry entry; InputStream in; try { zout = new ZipOutputStream(archive); for (File file : files) { in = null; entry = createEntry(file); out(String.format(fmt_extract, str_adding, entry.getName())); try { if (!file.isDirectory()) { if ((in = openFileRead(file)) == null) { continue; } zout.putNextEntry(entry); processStream(in, zout); } else { zout.putNextEntry(entry); } zout.closeEntry(); } catch (IOException e) { debug(e.getMessage()); } finally { if (in != null) { try { in.close(); } catch (IOException e) { // ignore } } } } } finally { if (zout != null) { try { zout.finish(); } catch (IOException e) { // ignore } } } } private void list() throws IOException { int size = 0; int csize = 0; int count = 0; printListHeader(); for (ZipEntry entry : dirEntries) { printListEntry(entry); count++; } for (ZipEntry entry : fileEntries) { printListEntry(entry); count++; size += entry.getSize(); csize += entry.getCompressedSize(); } printListFooter(size, csize, count); } private void extract() throws IOException { InputStream in = null; OutputStream out = null; File file; out(str_archive + archive.getName()); for (ZipEntry entry : dirEntries) { out(String.format(fmt_extract, str_creating, entry.getName())); file = new File(entry.getName()); file.mkdirs(); } for (ZipEntry entry : fileEntries) { out(String.format(fmt_extract, str_inflating, entry.getName())); file = new File(entry.getName()); try { File parent = file.getParentFile(); if (parent != null && !parent.exists()) { parent.mkdirs(); } file.createNewFile(); in = zarchive.getInputStream(entry); if ((out = openFileWrite(file, false, false)) == null) { continue; } processStream(in, out); } catch (IOException e) { debug(e.getMessage()); } finally { close(in); close(out); } } } /** * Creates a ZipEntry for the specified file. * * If the file is a directory it will have a trailing slash * appended to its name. This is used to distinguish files * from directories in the archive. * * If the -j option is given, then only the file name is stored, * not its full pathname. Conflicts are dealt with in parseFiles() */ private ZipEntry createEntry(File file) { String name = file.getPath(); ZipEntry entry; if (file.isDirectory()) { if (!name.endsWith(File.separator)) { name = name + File.separator; } entry = new ZipEntry(name); entry.setMethod(ZipEntry.STORED); } else { if (noPath) { name = file.getName(); } entry = new ZipEntry(name); entry.setMethod(ZipEntry.DEFLATED); if (noCompress != null && noCompress.length > 0) { for (String suf : noCompress) { if (name.endsWith(suf)) { entry.setMethod(ZipEntry.STORED); break; } } } } return entry; } private void parseOptions(String command) { if (DEBUG || Debug.isSet()) { outMode |= OUT_DEBUG; } if (Verbose.isSet()) { outMode |= OUT_NOTICE; } if (Quiet.isSet()) { outMode = 0; } if (command.equals("zip")) { if (Delete.isSet()) { mode = ZIP_DELETE; } else if (Freshen.isSet()) { mode = ZIP_FRESHEN | ZIP_ADD; } else if (Update.isSet()) { mode = ZIP_UPDATE | ZIP_ADD; } else if (Move.isSet()) { mode = ZIP_MOVE | ZIP_ADD; } else { mode = ZIP_ADD; } } else if (command.equals("unzip")) { if (Freshen.isSet()) { mode = ZIP_FRESHEN | ZIP_EXTRACT; } else if (Update.isSet()) { mode = ZIP_UPDATE | ZIP_EXTRACT; } else if (List.isSet()) { mode = ZIP_LIST; } else if (Test.isSet()) { mode = ZIP_TEST; } else { mode = ZIP_EXTRACT; } } noPath = NoPath.isSet(); switch (mode & (ZIP_ADD | ZIP_EXTRACT | ZIP_LIST)) { case ZIP_ADD : parseFiles(); getArchive(true, false); break; case ZIP_EXTRACT : getArchive(false, true); parseEntries(); break; case ZIP_LIST : getArchive(false, true); parseEntries(); break; default : throw new UnsupportedOperationException("This mode is not implemented."); } } /** * Instantiates the archive. * * If zipfile is true, than a ZipFile object for the archive is also created. * * This will exit with an error if: * - The archive does not exist, create is true, but an exception was thrown on creation. * - The archive does not exist, and create is false. * - The archive exists and create is true. (FIXME) * - A ZipFile was requested and there was an exception during instantiation. */ private void getArchive(boolean create, boolean zipfile) { archive = Archive.getValue(); if (archive.getName().equals("-")) { // pipe to stdout } if (!archive.exists()) { if (create) { try { archive.createNewFile(); } catch (IOException e) { fatal(fatal_create_arch + archive, 1); } } else { fatal(fatal_req_arch + archive, 1); } } else { if (create) { fatal("Archive exists, refused to overwrite: " + archive, 1); } } if (zipfile) { try { zarchive = new ZipFile(archive); } catch (IOException e) { debug(e.getMessage()); fatal(fatal_create_zfile + archive, 1); } } } private class Walker extends AbstractDirectoryWalker { @Override public void handleFile(final File file) throws IOException { addFile(file); } @Override public void handleDir(final File file) throws IOException { assert !(noDirEntry || noPath) : "handleDir called when noDirEntry || noPath"; addFile(file); } } /** * Creates a list of files based on the files and filename patterns given * on the command line. If we're using recursion, then a directory walker * is used with the given include/exclude filters if any are in use. * * The -D/-j options prevent directories from being listed. * The -x/-i options are used to add exclude/include patterns. * The -r option turns on recursion * * This will exit with an error if there was an exception while walking. */ private void parseFiles() { files = new ArrayList<File>(); List<File> dirs = new ArrayList<File>(); if (filesStdin) { parseFilesStdin(); } if (Patterns.isSet()) { for (String pattern : Patterns.getValues()) { if (!PathnamePattern.isPattern(pattern)) { File file = new File(pattern); if (!file.exists()) { debug("File or Directory does not exist: " + file); continue; } if (file.isDirectory()) { dirs.add(file); } else { addFile(file); } } else { PathnamePattern pat = PathnamePattern.compilePathPattern(pattern); List<String> list = pat.expand(new File(".")); for (String name : list) { File file = new File(name); if (file.isDirectory()) { dirs.add(file); } else { addFile(file); } } } } } if (recurse && dirs.size() > 0) { Walker walker = new Walker(); if (noDirEntry || noPath) { walker.addFilter(new FileFilter() { @Override public boolean accept(File file) { return !file.isDirectory(); } }); } if (excludes != null && excludes.size() > 0) { for (String pattern : excludes) { walker.addFilter(new PathnamePatternFilter(pattern, true)); } } if (includes != null && includes.size() > 0) { for (String pattern : includes) { walker.addFilter(new PathnamePatternFilter(pattern, false)); } } if (newer > 0) { walker.addFilter(new ModTimeFilter(newer, true)); } if (older > 0) { walker.addFilter(new ModTimeFilter(older, false)); } try { walker.walk(dirs); } catch (IOException e) { debug(e.getMessage()); fatal(fatal_walking, 1); } } } /** * Parses files from stdin, one file per line. * * If the file does not exist, it is ignored and omitted. * * This will exit with an error if there is an exception while reading stdin. */ private void parseFilesStdin() { LineNumberReader reader = new LineNumberReader(stdinReader); String line; File file; try { while ((line = reader.readLine()) != null) { file = new File(line); if (file.exists()) { addFile(file); } } } catch (IOException e) { fatal(fatal_read_stdin, 1); } } /** * Adds a file to the list of files. * * If the -j option is used, then the list is scanned for the file name. * This is done because -j strips the path from the pathname, which can cause * collisions if two files from separate directories with the same name are added. */ private void addFile(File file) { if (noPath) { // this isn't effecient by any means, but this is not an often-used // case, and when it is, its not likely that files.size() is going // to grow very large. for (File f : files) { if (f.getName().equals(file.getName())) { printDuplicateError(file, f); fatal(fatal_inv_args, 1); } } } files.add(file); } @SuppressWarnings("unchecked") private void parseEntries() { int count = 0; ZipEntry entry; Enumeration<ZipEntry> entries = zarchive.getEntries(); fileEntries = new ArrayList<ZipEntry>(); dirEntries = new ArrayList<ZipEntry>(); while (entries.hasMoreElements()) { count++; entry = entries.nextElement(); if (entry.isDirectory()) { dirEntries.add(entry); } else { fileEntries.add(entry); } } } private void printListHeader() { out(str_header_1); out(str_header_2); } private void printListEntry(ZipEntry entry) { out(String.format(fmt_entry, entry.getSize(), entry.getCompressedSize(), entry.getMethod(), entry.getName())); } private void printListFooter(int size, int csize, int numFiles) { out(str_footer); out(String.format(fmt_footer, size, csize, numFiles)); } private void printDuplicateError(File A, File B) { error(String.format(fmt_warn_dup, str_zip_warn, str_fullname_1, A.getPath())); error(String.format(fmt_warn_dup, str_zip_warn, str_fullname_2, B.getPath())); error(String.format(fmt_warn_dup, str_zip_warn, str_name_repeat, A.getName())); } @SuppressWarnings("unused") private void printName(String s) { if (outMode != 0) { out(s); } } @SuppressWarnings("unused") private void debug(ZipEntry entry) { debug("Name: " + entry.getName()); debug("Directory: " + entry.isDirectory()); debug("Platform: " + entry.getPlatform()); debug("Mode: " + entry.getUnixMode()); debug("IAttr: " + entry.getInternalAttributes()); debug("EAttr: " + entry.getExternalAttributes()); debug("CSize: " + entry.getCompressedSize()); debug("Size: " + entry.getSize()); debug("MTime: " + entry.getTime()); debug("Method: " + entry.getMethod()); debug("CRC: " + entry.getCrc()); debug("Comment: " + entry.getComment()); ZipExtraField[] extra = entry.getExtraFields(); if (extra != null && extra.length > 0) { debug("--Extra--"); for (ZipExtraField field : extra) { debug("CDL: " + field.getCentralDirectoryLength().getValue() + " FDL: " + field.getLocalFileDataLength().getValue()); } } } }