OSDN Git Service

Add new source files 1.0.0-beta1
authorargius <argius.net@gmail.com>
Wed, 20 Nov 2013 11:54:01 +0000 (20:54 +0900)
committerargius <argius.net@gmail.com>
Wed, 20 Nov 2013 11:54:01 +0000 (20:54 +0900)
86 files changed:
build.xml [new file with mode: 0644]
logging.properties [new file with mode: 0644]
src/MANIFEST.MF [new file with mode: 0644]
src/jp/sfjp/armadillo/ArmadilloCommands.java [new file with mode: 0644]
src/jp/sfjp/armadillo/Logger.java [new file with mode: 0644]
src/jp/sfjp/armadillo/ProgressNotifier.java [new file with mode: 0644]
src/jp/sfjp/armadillo/ResourceManager.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/ArchiveCreator.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/ArchiveEntry.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/ArchiveEntryInterface.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/ArchiveExtractor.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/ArchiveFile.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/ArchiveInputStream.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/ArchiveOutputStream.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/ArchiveType.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/DumpArchiveHeader.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/cab/CabArchiveCreator.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/cab/CabArchiveExtractor.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/cab/CabCfData.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/cab/CabCfFile.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/cab/CabCfFolder.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/cab/CabChecksum.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/cab/CabCompressionType.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/cab/CabEntry.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/cab/CabException.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/cab/CabHeader.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/cab/CabInputStream.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/cab/CabOutputStream.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/cab/DumpCabHeader.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/lzh/DumpLzhHeader.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/lzh/LzhArchiveCreator.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/lzh/LzhArchiveExtractor.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/lzh/LzhChecksum.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/lzh/LzhEntry.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/lzh/LzhException.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/lzh/LzhFile.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/lzh/LzhHeader.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/lzh/LzhInputStream.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/lzh/LzhMethod.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/lzh/LzhOutputStream.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/lzh/LzhQuit.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/tar/DumpTarHeader.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/tar/TarArchiveCreator.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/tar/TarArchiveExtractor.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/tar/TarEntry.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/tar/TarException.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/tar/TarFile.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/tar/TarHeader.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/tar/TarInputStream.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/tar/TarOutputStream.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/zip/DumpZipHeader.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/zip/ZipArchiveCreator.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/zip/ZipArchiveExtractor.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/zip/ZipEndEntry.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/zip/ZipEntry.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/zip/ZipFile.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/zip/ZipHeader.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/zip/ZipInputStream.java [new file with mode: 0644]
src/jp/sfjp/armadillo/archive/zip/ZipOutputStream.java [new file with mode: 0644]
src/jp/sfjp/armadillo/compression/lzhuf/LzhHuffmanDecoder.java [new file with mode: 0644]
src/jp/sfjp/armadillo/compression/lzhuf/LzhHuffmanEncoder.java [new file with mode: 0644]
src/jp/sfjp/armadillo/compression/lzhuf/LzhHuffmanTable.java [new file with mode: 0644]
src/jp/sfjp/armadillo/compression/lzhuf/LzhufException.java [new file with mode: 0644]
src/jp/sfjp/armadillo/compression/lzhuf/LzssDecoderReadable.java [new file with mode: 0644]
src/jp/sfjp/armadillo/compression/lzhuf/LzssEncoderWritable.java [new file with mode: 0644]
src/jp/sfjp/armadillo/compression/lzhuf/LzssInputStream.java [new file with mode: 0644]
src/jp/sfjp/armadillo/compression/lzhuf/LzssOutputStream.java [new file with mode: 0644]
src/jp/sfjp/armadillo/file/Find.java [new file with mode: 0644]
src/jp/sfjp/armadillo/file/FindAction.java [new file with mode: 0644]
src/jp/sfjp/armadillo/io/BitInputStream.java [new file with mode: 0644]
src/jp/sfjp/armadillo/io/BitOutputStream.java [new file with mode: 0644]
src/jp/sfjp/armadillo/io/ByteQueue.java [new file with mode: 0644]
src/jp/sfjp/armadillo/io/IOUtilities.java [new file with mode: 0644]
src/jp/sfjp/armadillo/io/InspectionOutputStream.java [new file with mode: 0644]
src/jp/sfjp/armadillo/io/RewindableInputStream.java [new file with mode: 0644]
src/jp/sfjp/armadillo/io/VolumetricOutputStream.java [new file with mode: 0644]
src/jp/sfjp/armadillo/time/FTime.java [new file with mode: 0644]
src/jp/sfjp/armadillo/time/FileTime.java [new file with mode: 0644]
src/jp/sfjp/armadillo/time/TimeConverter.java [new file with mode: 0644]
src/jp/sfjp/armadillo/time/TimeT.java [new file with mode: 0644]
src/jp/sfjp/armadillo/ui/console/CommandLine.java [new file with mode: 0644]
src/jp/sfjp/armadillo/ui/messages.u8p [new file with mode: 0644]
src/jp/sfjp/armadillo/ui/messages_ja.u8p [new file with mode: 0644]
src/jp/sfjp/armadillo/ui/window/ListDialog.java [new file with mode: 0644]
src/jp/sfjp/armadillo/ui/window/MainWindow.java [new file with mode: 0644]
src/jp/sfjp/armadillo/version [new file with mode: 0644]

diff --git a/build.xml b/build.xml
new file mode 100644 (file)
index 0000000..84ca13c
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- ====================================================================== 
+     Armadillo
+     armadillo - archiving utility
+     ====================================================================== -->
+<project name="armadillo1" default="build">
+
+    <!-- ENVIRONMENT VERSION INFO -->
+    <echo level="info" message="Ant  = ${ant.version}" />
+    <echo level="info" message="java = ${ant.java.version}" />
+
+    <!-- PROPERTIES -->
+    <property name="project.archive" value="armadillo.jar" />
+    <property name="project.directory.source" value="src" />
+    <property name="project.directory.binary" value="BLD" />
+
+    <!-- TARGET : clean -->
+    <target name="clean">
+        <mkdir dir="${project.directory.binary}" />
+        <delete includeEmptyDirs="yes">
+            <fileset dir="${project.directory.binary}" includes="**" />
+        </delete>
+    </target>
+
+    <!-- TARGET : compile -->
+    <target name="compile">
+        <mkdir dir="${project.directory.binary}" />
+        <javac srcdir="${project.directory.source}"
+               destdir="${project.directory.binary}"
+               target="1.6"
+               source="1.6"
+               optimize="yes"
+               deprecation="no"
+               debug="yes"
+               debuglevel="source,lines">
+        </javac>
+        <copy todir="${project.directory.binary}">
+            <fileset dir="${project.directory.source}">
+                <include name="**/*.u8p" />
+                <include name="**/*.png" />
+                <include name="**/version" />
+            </fileset>
+        </copy>
+    </target>
+
+    <!-- TARGET : archive -->
+    <target name="archive" depends="compile">
+        <jar destfile="${project.archive}"
+             manifest="${project.directory.source}/MANIFEST.MF">
+            <fileset dir="${project.directory.binary}">
+                <include name="**" />
+            </fileset>
+        </jar>
+    </target>
+
+    <!-- TARGET : build -->
+    <target name="build" depends="archive" />
+
+</project>
diff --git a/logging.properties b/logging.properties
new file mode 100644 (file)
index 0000000..495b555
--- /dev/null
@@ -0,0 +1,18 @@
+
+.level = OFF
+
+# ConsoleHandler
+java.util.logging.ConsoleHandler.level = ALL
+java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
+
+# FileHandler
+java.util.logging.FileHandler.level = ALL
+java.util.logging.FileHandler.pattern = armadillo.log
+java.util.logging.FileHandler.append = true
+java.util.logging.FileHandler.limit = 4194304
+java.util.logging.FileHandler.count = 2
+java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter
+
+# about logger
+jp.sfjp.armadillo.level = ALL
+jp.sfjp.armadillo.handlers=java.util.logging.ConsoleHandler,java.util.logging.FileHandler
diff --git a/src/MANIFEST.MF b/src/MANIFEST.MF
new file mode 100644 (file)
index 0000000..7f45300
--- /dev/null
@@ -0,0 +1,2 @@
+Manifest-Version: 1.0
+Main-Class: jp.sfjp.armadillo.ui.window.MainWindow
diff --git a/src/jp/sfjp/armadillo/ArmadilloCommands.java b/src/jp/sfjp/armadillo/ArmadilloCommands.java
new file mode 100644 (file)
index 0000000..5d353f1
--- /dev/null
@@ -0,0 +1,413 @@
+package jp.sfjp.armadillo;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.util.*;
+import java.util.zip.*;
+import jp.sfjp.armadillo.archive.*;
+import jp.sfjp.armadillo.archive.cab.*;
+import jp.sfjp.armadillo.archive.lzh.*;
+import jp.sfjp.armadillo.archive.tar.*;
+import jp.sfjp.armadillo.archive.zip.ZipFile;
+import jp.sfjp.armadillo.file.*;
+import jp.sfjp.armadillo.io.*;
+
+public final class ArmadilloCommands {
+
+    private static Logger log = Logger.getLogger(ArmadilloCommands.class);
+
+    private ArmadilloCommands() { // empty
+    }
+
+    public static String getVersionString() {
+        return getVersionString(false);
+    }
+
+    public static String getVersionString(boolean throwsError) {
+        try {
+            InputStream is = ArmadilloCommands.class.getResourceAsStream("version");
+            if (is != null)
+                try {
+                    final Scanner r = new Scanner(is);
+                    if (r.hasNextLine())
+                        return r.nextLine();
+                }
+                finally {
+                    is.close();
+                }
+        }
+        catch (Exception ex) {
+            if (throwsError)
+                throw new IllegalStateException(ex);
+            // ignore
+        }
+        return "";
+    }
+
+    public static void createArchive(File srcdir, File dst0) throws IOException {
+        createArchive(srcdir, dst0, ProgressNotifier.NullObject);
+    }
+
+    public static void createArchive(File srcdir, File dst0, ProgressNotifier notifier) throws IOException {
+        File dst = getAlternativeFileIfExists(dst0);
+        ArchiveType type = ArchiveType.of(dst.getName());
+        List<File> files = getAllChildrenFile(srcdir);
+        ArchiveCreator ac = getArchiveCreator(dst, type);
+        try {
+            for (final File file : files) {
+                final String pathForEntryName = getRelativePath(srcdir, file);
+                if (pathForEntryName.length() == 0)
+                    continue;
+                ArchiveEntry entry = ac.newEntry(pathForEntryName);
+                final long fileSize = file.length();
+                entry.setSize(fileSize);
+                entry.setLastModified(file.lastModified());
+                ac.addEntry(entry, file);
+                if (fileSize > 0)
+                    notifier.notifyProgress(fileSize);
+            }
+        }
+        finally {
+            ac.close();
+        }
+    }
+
+    public static void extractAll(File src) throws IOException {
+        extractAll(src, ProgressNotifier.NullObject);
+    }
+
+    public static void extractAll(File src, File dstdir) throws IOException {
+        extractAll(src, dstdir, ProgressNotifier.NullObject);
+    }
+
+    public static void extractAll(File src, ProgressNotifier notifier) throws IOException {
+        extractAll(src, getAlternativeFileIfExists(getDestinationDirectory(src)), notifier);
+    }
+
+    public static void extractAll(File src, File dstdir, ProgressNotifier notifier) throws IOException {
+        Set<String> targetSet = Collections.emptySet();
+        extract(src, dstdir, targetSet, notifier);
+    }
+
+    public static void extract(File src, Set<String> targetSet, ProgressNotifier notifier) throws IOException {
+        extract(src, getAlternativeFileIfExists(getDestinationDirectory(src)), targetSet, notifier);
+    }
+
+    public static void extract(File src,
+                               File dstdir,
+                               Set<String> targetSet,
+                               ProgressNotifier notifier) throws IOException {
+        if (dstdir.exists())
+            throw new IOException("the directory already exists: " + dstdir);
+        final boolean all = targetSet.isEmpty();
+        int targetCount = targetSet.size();
+        ArchiveType type = ArchiveType.of(src.getName());
+        Set<ArchiveEntry> dirEntrySet = new HashSet<ArchiveEntry>();
+        ArchiveExtractor ax = getArchiveExtractor(src, type);
+        try {
+            assert !dstdir.exists();
+            if (!dstdir.mkdirs())
+                throw new IllegalStateException("failed to mkdirs");
+            while (true) {
+                ArchiveEntry entry = ax.nextEntry();
+                if (entry == ArchiveEntry.NULL)
+                    break;
+                if (entry.isDirectory())
+                    dirEntrySet.add(entry);
+                if (!all && !targetSet.contains(entry.getName()))
+                    continue;
+                if (!all)
+                    --targetCount;
+                final File newfile = getRelativeFile(dstdir, entry);
+                if (entry.isDirectory()) {
+                    if (!newfile.exists())
+                        newfile.mkdirs();
+                    continue;
+                }
+                if (newfile.exists())
+                    throw new IOException("file '" + newfile + "' already exists");
+                final File parent = newfile.getParentFile();
+                if (!parent.exists())
+                    if (!parent.mkdirs())
+                        throw new IOException("can't mkdir '" + parent + "'");
+                final long inc = (entry.getCompressedSize() > 0)
+                        ? entry.getCompressedSize()
+                        : (long)(entry.getSize() * 0.18f);
+                final long inc1 = (long)(inc * 0.3f);
+                final long inc2 = inc - inc1;
+                assert inc1 + inc2 == inc;
+                notifier.notifyProgress(inc1);
+                final long writtenSize;
+                if (newfile.exists()) // second check
+                    throw new IOException("file '" + newfile + "' already exists");
+                OutputStream fos = new FileOutputStream(newfile);
+                try {
+                    writtenSize = ax.extract(fos);
+                }
+                catch (Exception ex) {
+                    throw new IllegalStateException("illegal state", ex);
+                }
+                finally {
+                    fos.close();
+                }
+                assert writtenSize == entry.getSize() : String.format("%s: written=%d, entry=%d",
+                                                                      entry.getName(),
+                                                                      writtenSize,
+                                                                      entry.getSize());
+                newfile.setLastModified(entry.getLastModified());
+                notifier.notifyProgress(inc2);
+                if (!all && targetCount <= 0)
+                    break;
+            }
+            notifier.notifyFinished();
+            /*
+             * Finally, set timestamp on all directories.
+             */
+            for (final ArchiveEntry entry : dirEntrySet) {
+                File dir = new File(dstdir, entry.getName());
+                if (dir.exists())
+                    dir.setLastModified(entry.getLastModified());
+            }
+        }
+        finally {
+            ax.close();
+        }
+    }
+
+    public static boolean validate(File src) throws IOException {
+        ArchiveType type = ArchiveType.of(src.getName());
+        ArchiveExtractor ax = getArchiveExtractor(src, type);
+        try {
+            while (true) {
+                ArchiveEntry entry = ax.nextEntry();
+                if (entry == ArchiveEntry.NULL)
+                    break;
+                assert entry != null : "return null entry: extractor="
+                                       + ax.getClass().getSimpleName();
+                if (entry.isDirectory())
+                    continue;
+                VolumetricOutputStream vos = new VolumetricOutputStream();
+                try {
+                    if (ax.extract(vos) != entry.getSize())
+                        return false;
+                }
+                finally {
+                    vos.close();
+                }
+            }
+        }
+        catch (IOException ex) {
+            throw ex;
+        }
+        catch (Exception ex) {
+            log.error(ex);
+            return false;
+        }
+        finally {
+            ax.close();
+        }
+        return true;
+    }
+
+    public static List<ArchiveEntry> extractArchiveEntries(File src, ArchiveType type) throws IOException {
+        List<ArchiveEntry> a = new ArrayList<ArchiveEntry>();
+        ArchiveExtractor ax = getArchiveExtractor(src, type);
+        try {
+            while (true) {
+                ArchiveEntry entry = ax.nextEntry();
+                if (entry == ArchiveEntry.NULL)
+                    break;
+                a.add(entry);
+            }
+        }
+        finally {
+            ax.close();
+        }
+        return a;
+    }
+
+    public static long transferAll(InputStream is, OutputStream os) throws IOException {
+        long totalSize = 0;
+        byte[] buffer = new byte[8192];
+        for (int readLength; (readLength = is.read(buffer)) >= 0;) {
+            assert readLength != 0 : "Read Zero";
+            os.write(buffer, 0, readLength);
+            totalSize += readLength;
+        }
+        os.flush();
+        return totalSize;
+    }
+
+    public static String getRelativePath(File dir, File f) throws IOException {
+        final String filePath = f.getCanonicalFile().getAbsolutePath();
+        final String dirPath = dir.getCanonicalFile().getAbsolutePath();
+        if (dirPath.equals(filePath))
+            return "";
+        if (!filePath.startsWith(dirPath))
+            throw new IllegalStateException(String.format("not relative: dir=%s, f=%s", dir, f));
+        final int dirLen = dirPath.length() + (dirPath.endsWith("/") ? 0 : 1);
+        final String s = filePath.substring(dirLen).replace('\\', '/');
+        if (f.isDirectory() && !filePath.endsWith("/"))
+            return s + "/";
+        else
+            return s;
+    }
+
+    public static File getRelativeFile(File dir, ArchiveEntry entry) {
+        final String name = entry.getName();
+        if (name.startsWith("/"))
+            return new File(dir, name);
+        else if (name.matches("(?i)[A-Z]\\:[\\\\/].*"))
+            return new File(dir, name.substring(2));
+        return new File(dir, name);
+    }
+
+    public static List<File> getAllChildrenFile(File dir) {
+        if (!dir.isDirectory())
+            throw new IllegalArgumentException("file is not dir: " + dir);
+        List<File> a = new ArrayList<File>();
+        getAllChildrenFile(a, dir);
+        return a;
+    }
+
+    private static void getAllChildrenFile(List<File> a, File f) {
+        a.add(f);
+        if (f.isDirectory()) {
+            File[] children = f.listFiles();
+            if (children != null)
+                for (File child : children)
+                    getAllChildrenFile(a, child);
+        }
+    }
+
+    public static long getTotalSize(File dir) {
+        TotalFileSizeFindAction action = new TotalFileSizeFindAction();
+        Find.find(dir, action);
+        return action.totalSize;
+    }
+
+    static final class TotalFileSizeFindAction implements FindAction {
+
+        long totalSize;
+
+        @Override
+        public void act(File file) {
+            totalSize += file.length();
+        }
+
+    }
+
+    public static File getDestinationDirectory(File f) {
+        return getAlternativeFileIfExists(new File(f.getPath() + ".tmp"));
+    }
+
+    public static File getAlternativeFileIfExists(File f) {
+        if (!f.exists())
+            return f;
+        File parent = f.getParentFile();
+        final String name = f.getName();
+        final String mainName;
+        final String ext;
+        if (f.isDirectory()) {
+            mainName = name;
+            ext = "";
+        }
+        else {
+            final int index = name.indexOf('.');
+            if (index == 0)
+                throw new IllegalArgumentException("dot file not supported at getAlternativeFileIfExists");
+            if (index > 0) {
+                mainName = name.substring(0, index);
+                ext = name.substring(index + 1);
+            }
+            else {
+                mainName = name;
+                ext = "";
+            }
+        }
+        for (int i = 1; i <= 1000; i++) {
+            final String newName = String.format("%s(%d).%s", mainName, i, ext);
+            File newFile = new File(parent, newName);
+            if (!newFile.exists())
+                return newFile;
+        }
+        throw new RuntimeException("over 100 times to try to resolve alternative file.");
+    }
+
+    public static ArchiveCreator getArchiveCreator(File dst, ArchiveType type) throws IOException {
+        switch (type) {
+            case TAR:
+                return new TarArchiveCreator(new BufferedOutputStream(new FileOutputStream(dst)));
+            case TARGZ:
+                return new TarArchiveCreator(new GZIPOutputStream(new BufferedOutputStream(new FileOutputStream(dst))));
+            case TARZ:
+            case TARBZ2:
+            case TARXZ:
+                return new TarArchiveCreator(getOutputStream(dst, type));
+            case ZIP:
+                return new ZipFile(dst, true);
+            case LZH:
+                return new LzhFile(dst, true);
+            case CAB:
+                return new CabArchiveCreator(new BufferedOutputStream(new FileOutputStream(dst)));
+            default:
+        }
+        throw new IllegalStateException("File: " + dst + ", ArchiverType: " + type);
+    }
+
+    private static OutputStream getOutputStream(File file, ArchiveType type) throws IOException {
+        try {
+            final String cname = System.getProperty("plugin.stream.o." + type, "");
+            if (cname.length() == 0)
+                throw new UnsupportedOperationException("File: " + file + ", ArchiverType: " + type);
+            final Class<?> c = Class.forName(cname);
+            Constructor<?> ctor = c.getConstructor(new Class<?>[]{InputStream.class});
+            return (OutputStream)ctor.newInstance(new BufferedOutputStream(new FileOutputStream(file)));
+        }
+        catch (IOException ex) {
+            throw ex;
+        }
+        catch (Exception ex) {
+            throw new UnsupportedOperationException("File: " + file + ", ArchiverType: " + type, ex);
+        }
+    }
+
+    public static ArchiveExtractor getArchiveExtractor(File src, ArchiveType type) throws IOException {
+        switch (type) {
+            case TAR:
+                return new TarArchiveExtractor(new BufferedInputStream(new FileInputStream(src)));
+            case TARGZ:
+                return new TarArchiveExtractor(new GZIPInputStream(new BufferedInputStream(new FileInputStream(src))));
+            case TARZ:
+            case TARBZ2:
+            case TARXZ:
+                return new TarArchiveExtractor(new BufferedInputStream(getInputStream(src, type)));
+            case ZIP:
+                return new ZipFile(src, true);
+            case LZH:
+                return new LzhFile(src, true);
+            case CAB:
+                return new CabArchiveExtractor(new BufferedInputStream(new FileInputStream(src)));
+            default:
+        }
+        throw new IllegalStateException("File: " + src + ", ArchiverType: " + type);
+    }
+
+    private static InputStream getInputStream(File file, ArchiveType type) throws IOException {
+        try {
+            final String cname = System.getProperty("plugin.stream.i." + type, "");
+            if (cname.length() == 0)
+                throw new UnsupportedOperationException("File: " + file + ", ArchiverType: " + type);
+            final Class<?> c = Class.forName(cname);
+            Constructor<?> ctor = c.getConstructor(new Class<?>[]{InputStream.class});
+            return (InputStream)ctor.newInstance(new FileInputStream(file));
+        }
+        catch (IOException ex) {
+            throw ex;
+        }
+        catch (Exception ex) {
+            throw new UnsupportedOperationException("File: " + file + ", ArchiverType: " + type, ex);
+        }
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/Logger.java b/src/jp/sfjp/armadillo/Logger.java
new file mode 100644 (file)
index 0000000..daf628a
--- /dev/null
@@ -0,0 +1,217 @@
+package jp.sfjp.armadillo;
+
+import java.util.logging.*;
+
+/**
+ * The Logger which has Apache Logging style interface.
+ *
+ * <p>The log levels map to core API's log level as below.</p><ul>
+ * <li> fatal: Level.SEVERE (and "fatal" message)
+ * <li> error: Level.SEVERE
+ * <li> warn: Level.WARNING
+ * <li> info: Level.INFO
+ * <li> debug: Level.FINE
+ * <li> trace: Level.FINER
+ * </ul>
+ * Level.CONFIG is not used.
+ */
+public final class Logger {
+
+    private final java.util.logging.Logger log;
+
+    private String enteredMethodName;
+
+    Logger(String name) {
+        this.log = java.util.logging.Logger.getLogger(name);
+        removeRootLoggerHandlers();
+    }
+
+    Logger(Class<?> c) {
+        this(c.getName());
+    }
+
+    static void removeRootLoggerHandlers() {
+        java.util.logging.Logger rootLogger = LogManager.getLogManager().getLogger("");
+        for (Handler handler : rootLogger.getHandlers())
+            rootLogger.removeHandler(handler);
+    }
+
+    public static Logger getLogger(Object o) {
+        final String name;
+        if (o instanceof Class) {
+            Class<?> c = (Class<?>)o;
+            name = c.getName();
+        }
+        else if (o instanceof String)
+            name = (String)o;
+        else
+            name = String.valueOf(o);
+        return new Logger(name);
+    }
+
+    /**
+     * The INFO level maps to logging.Level.INFO .
+     * @return info level is enabled
+     */
+    public boolean isInfoEnabled() {
+        return log.isLoggable(Level.INFO);
+    }
+
+    /**
+     * The DEBUG level maps to logging.Level.FINE .
+     * @return debug level is enabled
+     */
+    public boolean isDebugEnabled() {
+        return log.isLoggable(Level.FINE);
+    }
+
+    /**
+     * The TRACE level maps to logging.Level.FINER .
+     * @return debug level is enabled
+     */
+    public boolean isTraceEnabled() {
+        return log.isLoggable(Level.FINER);
+    }
+
+    public String getEnteredMethodName() {
+        return (enteredMethodName == null) ? "" : enteredMethodName;
+    }
+
+    public void setEnteredMethodName(String methodName) {
+        enteredMethodName = (methodName == null) ? "" : methodName;
+    }
+
+    public void log(Level level, Throwable th, String format, Object... args) {
+        if (log.isLoggable(level)) {
+            final String cn = log.getName();
+            final String mn = (enteredMethodName == null) ? "(unknown method)" : enteredMethodName;
+            final String msg = (args.length == 0) ? format : String.format(format, args);
+            if (th == null)
+                log.logp(level, cn, mn, msg);
+            else
+                log.logp(level, cn, mn, msg, th);
+        }
+    }
+
+    public void fatal(Throwable th) {
+        if (log.isLoggable(Level.SEVERE))
+            log(Level.SEVERE, th, "*FATAL*");
+    }
+
+    public void fatal(Throwable th, String format, Object... args) {
+        if (log.isLoggable(Level.SEVERE))
+            log(Level.SEVERE, th, "*FATAL* " + String.format(format, args));
+    }
+
+    public void error(Throwable th) {
+        if (log.isLoggable(Level.SEVERE))
+            log(Level.SEVERE, th, "");
+    }
+
+    public void error(Throwable th, Object o) {
+        if (log.isLoggable(Level.SEVERE))
+            log(Level.SEVERE, th, String.valueOf(o));
+    }
+
+    public void error(Throwable th, String format, Object arg) {
+        if (log.isLoggable(Level.SEVERE))
+            log(Level.SEVERE, th, format, arg);
+    }
+
+    public void warn(Throwable th) {
+        if (log.isLoggable(Level.WARNING))
+            log(Level.WARNING, th, "");
+    }
+
+    public void warn(Throwable th, Object o) {
+        if (log.isLoggable(Level.WARNING))
+            log(Level.WARNING, th, String.valueOf(o));
+    }
+
+    public void warn(String format, Object... args) {
+        if (log.isLoggable(Level.WARNING))
+            log(Level.WARNING, null, format, args);
+    }
+
+    public void info(Object o) {
+        if (isInfoEnabled())
+            log(Level.INFO, null, String.valueOf(o));
+    }
+
+    public void info(String format, Object arg) {
+        if (isInfoEnabled())
+            log(Level.INFO, null, format, arg);
+    }
+
+    public void info(String format, Object arg1, Object arg2) {
+        if (isInfoEnabled())
+            log(Level.INFO, null, format, arg1, arg2);
+    }
+
+    public void info(String format, Object... args) {
+        if (isInfoEnabled())
+            log(Level.INFO, null, format, args);
+    }
+
+    public void debug(Object o) {
+        if (isDebugEnabled())
+            log(Level.FINE, null, String.valueOf(o));
+    }
+
+    public void debug(String format, Object arg) {
+        if (isDebugEnabled())
+            log(Level.FINE, null, format, arg);
+    }
+
+    public void debug(String format, Object arg1, Object arg2) {
+        if (isDebugEnabled())
+            log(Level.FINE, null, format, arg1, arg2);
+    }
+
+    public void debug(String format, Object... args) {
+        if (isDebugEnabled())
+            log(Level.FINE, null, format, args);
+    }
+
+    public void trace(Throwable th) {
+        if (isTraceEnabled())
+            log(Level.FINER, th, "");
+    }
+
+    public void trace(Object o) {
+        if (isTraceEnabled())
+            log(Level.FINER, null, String.valueOf(o));
+    }
+
+    public void trace(String format, Object arg) {
+        if (isTraceEnabled())
+            log(Level.FINER, null, format, arg);
+    }
+
+    public void trace(String format, Object... args) {
+        if (isTraceEnabled())
+            log(Level.FINER, null, format, args);
+    }
+
+    public void atEnter(String method, Object... args) {
+        setEnteredMethodName(method);
+        log.entering(log.getName(), method, args);
+    }
+
+    public void atExit() {
+        log.exiting(log.getName(), enteredMethodName);
+        setEnteredMethodName("");
+    }
+
+    public void atExit(String method) {
+        log.exiting(log.getName(), method);
+        setEnteredMethodName("");
+    }
+
+    public <T> T atExit(String method, T returnValue) {
+        log.exiting(log.getName(), method, returnValue);
+        setEnteredMethodName("");
+        return returnValue;
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/ProgressNotifier.java b/src/jp/sfjp/armadillo/ProgressNotifier.java
new file mode 100644 (file)
index 0000000..e496eaa
--- /dev/null
@@ -0,0 +1,58 @@
+package jp.sfjp.armadillo;
+
+public abstract class ProgressNotifier {
+
+    public static final ProgressNotifier NullObject = new ProgressNotifier(1L) {
+        @Override
+        public void progressNotified(long part, long total) {
+            // do nothing
+        }
+    };
+
+    private final long total;
+    private long part;
+
+    protected ProgressNotifier(long total) {
+        assert total > 0;
+        this.total = total;
+    }
+
+    protected final int getRateAsIntPercent() {
+        if (part < 0)
+            return 0;
+        if (part >= total)
+            return 100;
+        return (int)(100.0f / total * part);
+    }
+
+    protected final int getRateAsIntPermill() {
+        if (part < 0)
+            return 0;
+        if (part >= total)
+            return 1000;
+        return (int)(1000.0f / total * part);
+    }
+
+    protected final double getRate() {
+        if (part < 0)
+            return 0.0f;
+        if (part >= total)
+            return 1.0f;
+        final long rate = part / total;
+        assert rate >= 0 && rate <= 1.0;
+        return rate;
+    }
+
+    public final void notifyFinished() {
+        part = total;
+        progressNotified(part, total);
+    }
+
+    public final void notifyProgress(long increasement) {
+        this.part += increasement;
+        progressNotified(part, total);
+    }
+
+    public abstract void progressNotified(long part, long total);
+
+}
diff --git a/src/jp/sfjp/armadillo/ResourceManager.java b/src/jp/sfjp/armadillo/ResourceManager.java
new file mode 100644 (file)
index 0000000..37b0724
--- /dev/null
@@ -0,0 +1,221 @@
+package jp.sfjp.armadillo;
+
+import java.io.*;
+import java.text.*;
+import java.util.*;
+import java.util.concurrent.*;
+
+/**
+ * ResourceManager provides a function like a ResourceBundle used UTF-8 instead Unicode escapes.
+ */
+public final class ResourceManager {
+
+    public static final ResourceManager Default = ResourceManager.getInstance(ResourceManager0.class);
+
+    private final List<Map<String, String>> list;
+
+    private ResourceManager(List<Map<String, String>> list) {
+        this.list = list;
+    }
+
+    /**
+     * Creates an instance.
+     * @param o
+     * It used as a bundle name.
+     * If String, it will use as a bundle name directly.
+     * Else if Package, it will use its package name + "messages".
+     * Otherwise, it will use as its FQCN.
+     * @return
+     */
+    public static ResourceManager getInstance(Object o) {
+        Locale loc = Locale.getDefault();
+        String[] suffixes = {"_" + loc, "_" + loc.getLanguage(), ""};
+        List<Map<String, String>> a = new ArrayList<Map<String, String>>();
+        for (final String name : getResourceNames(o))
+            for (final String suffix : suffixes) {
+                final String key = name + suffix;
+                Map<String, String> m = ResourceManager0.map.get(key);
+                if (m == null) {
+                    m = loadResource(key, "u8p", "utf-8");
+                    if (m == null)
+                        continue;
+                    ResourceManager0.map.putIfAbsent(key, m);
+                }
+                a.add(m);
+            }
+        return new ResourceManager(a);
+    }
+
+    private static Set<String> getResourceNames(Object o) {
+        Set<String> set = new LinkedHashSet<String>();
+        String cn = null;
+        String pn = null;
+        if (o instanceof String)
+            cn = (String)o;
+        else if (o instanceof Package)
+            pn = ((Package)o).getName();
+        else if (o != null) {
+            final Class<?> c = (o instanceof Class) ? (Class<?>)o : o.getClass();
+            cn = c.getName();
+            pn = c.getPackage().getName();
+        }
+        if (cn != null)
+            set.add(cn);
+        if (pn != null)
+            set.add(pn + ".messages");
+        set.add(ResourceManager0.getPackageName() + ".messages");
+        return set;
+    }
+
+    private static Map<String, String> loadResource(String name, String extension, String encname) {
+        final String path = "/" + name.replace('.', '/') + '.' + extension;
+        InputStream is = ResourceManager0.getResourceAsStream(path);
+        if (is == null)
+            return null;
+        List<String> lines = new ArrayList<String>();
+        Scanner r = new Scanner(is, encname);
+        try {
+            StringBuilder buffer = new StringBuilder();
+            while (r.hasNextLine()) {
+                final String s = r.nextLine();
+                if (s.matches("^\\s*#.*"))
+                    continue;
+                buffer.append(s.replace("\\t", "\t").replace("\\n", "\n").replace("\\=", "="));
+                if (s.endsWith("\\")) {
+                    buffer.setLength(buffer.length() - 1);
+                    continue;
+                }
+                lines.add(buffer.toString());
+                buffer.setLength(0);
+            }
+            if (buffer.length() > 0)
+                lines.add(buffer.toString());
+        } finally {
+            r.close();
+        }
+        Map<String, String> m = new HashMap<String, String>();
+        for (final String s : lines)
+            if (s.contains("=")) {
+                String[] a = s.split("=", 2);
+                m.put(a[0].trim(), a[1].trim().replaceFirst("\\\\$", " ").replace("\\ ", " "));
+            }
+            else
+                m.put(s.trim(), "");
+        return m;
+    }
+
+    /**
+     * @param path resource's path
+     * @param defaultValue defalut value if a resource not found
+     * @return
+     */
+    public String read(String path, String defaultValue) {
+        InputStream in = ResourceManager0.getResourceAsStream(path);
+        if (in == null)
+            return defaultValue;
+        StringBuilder buffer = new StringBuilder();
+        Scanner r = new Scanner(in);
+        try {
+            if (r.hasNextLine())
+                buffer.append(r.nextLine());
+            while (r.hasNextLine()) {
+                buffer.append(String.format("%n"));
+                buffer.append(r.nextLine());
+            }
+        } finally {
+            r.close();
+        }
+        return buffer.toString();
+    }
+
+    private String s(String key) {
+        for (final Map<String, String> m : this.list) {
+            final String s = m.get(key);
+            if (s != null)
+                return s;
+        }
+        return "";
+    }
+
+    /**
+     * Returns true if this resource contains a value specified by key.
+     * @param key
+     * @return
+     */
+    public boolean containsKey(String key) {
+        for (final Map<String, String> m : this.list)
+            if (m.containsKey(key))
+                return true;
+        return false;
+    }
+
+    /**
+     * Returns the value specified by key as a String.
+     * @param key
+     * @param args
+     * @return
+     */
+    public String get(String key, Object... args) {
+        final String s = s(key);
+        return (s.length() == 0) ? key : MessageFormat.format(s(key), args);
+    }
+
+    /**
+     * Returns the value specified by key as a boolean.
+     * @param key
+     * @return
+     */
+    public boolean isTrue(String key) {
+        return s(key).matches("(?i)true|on|yes");
+    }
+
+    /**
+     * Returns the (initial char) value specified by key as a char.
+     * @param key
+     * @return
+     */
+    public char getChar(String key) {
+        final String s = s(key);
+        return (s.length() == 0) ? ' ' : s.charAt(0);
+    }
+
+    /**
+     * Returns the value specified by key as a int.
+     * @param key
+     * @return
+     */
+    public int getInt(String key) {
+        return getInt(key, 0);
+    }
+
+    /**
+     * Returns the value specified by key as a int.
+     * @param key
+     * @param defaultValue
+     * @return
+     */
+    public int getInt(String key, int defaultValue) {
+        final String s = s(key);
+        try {
+            return Integer.parseInt(s);
+        } catch (NumberFormatException ex) {
+            // ignore
+        }
+        return defaultValue;
+    }
+
+}
+
+class ResourceManager0 {
+
+    static final ConcurrentHashMap<String, Map<String, String>> map = new ConcurrentHashMap<String, Map<String, String>>();
+
+    static String getPackageName() {
+        return ResourceManager0.class.getPackage().getName();
+    }
+
+    static InputStream getResourceAsStream(String path) {
+        return ResourceManager0.class.getResourceAsStream(path);
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/ArchiveCreator.java b/src/jp/sfjp/armadillo/archive/ArchiveCreator.java
new file mode 100644 (file)
index 0000000..26617b0
--- /dev/null
@@ -0,0 +1,13 @@
+package jp.sfjp.armadillo.archive;
+
+import java.io.*;
+
+public interface ArchiveCreator extends Closeable {
+
+    ArchiveEntry newEntry(String name);
+
+    void addEntry(ArchiveEntry entry, File file) throws IOException;
+
+    void addEntry(ArchiveEntry entry, InputStream is, long length) throws IOException;
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/ArchiveEntry.java b/src/jp/sfjp/armadillo/archive/ArchiveEntry.java
new file mode 100644 (file)
index 0000000..3a1cd36
--- /dev/null
@@ -0,0 +1,167 @@
+package jp.sfjp.armadillo.archive;
+
+import java.io.*;
+import java.nio.charset.*;
+import java.util.*;
+
+public abstract class ArchiveEntry implements ArchiveEntryInterface {
+
+    public static final ArchiveEntry NULL = new NullArchiveEntry();
+
+    private boolean added;
+    private String name;
+    private byte[] nameb;
+    private boolean initialized;
+
+    protected ArchiveEntry(boolean initialized) {
+        this.initialized = initialized;
+    }
+
+    public static ArchiveEntry orNull(ArchiveEntry entry) {
+        return (entry == null) ? NULL : entry;
+    }
+
+    public ArchiveEntry copyFrom(ArchiveEntry an) {
+        copyAtoB(an, this);
+        return an;
+    }
+
+    public ArchiveEntry copyTo(ArchiveEntry an) {
+        copyAtoB(this, an);
+        return an;
+    }
+
+    private static void copyAtoB(ArchiveEntry a, ArchiveEntry b) {
+        b.name = a.name;
+        b.nameb = a.nameb;
+        b.initialized = a.initialized;
+        b.setSize(a.getSize());
+        b.setCompressedSize(a.getCompressedSize());
+        b.setLastModified(a.getLastModified());
+    }
+
+    public boolean isNull() {
+        return this == NULL;
+    }
+
+    public boolean isAdded() {
+        return added;
+    }
+
+    @Override
+    public void setAdded(boolean added) {
+        this.added = added;
+    }
+
+    public final String name() {
+        if (name == null)
+            if (nameb == null)
+                throw new IllegalStateException("not initialized: " + getClass());
+            else
+                name = new String(nameb);
+        return name;
+    }
+
+    @Override
+    public final String getName() {
+        return name();
+    }
+
+    public final byte[] getNameAsBytes() {
+        if (nameb == null)
+            if (name == null)
+                throw new IllegalStateException("not initialized: " + getClass());
+            else
+                nameb = name.getBytes();
+        return Arrays.copyOf(nameb, nameb.length);
+    }
+
+    public final byte[] getNameAsBytes(Charset charset) {
+        if (nameb == null)
+            nameb = name.getBytes(charset);
+        return Arrays.copyOf(nameb, nameb.length);
+    }
+
+    public final void setName(String name) {
+        this.name = name;
+        this.nameb = null;
+        this.initialized = true;
+    }
+
+    public final void setName(String name, Charset charset) {
+        this.name = name;
+        this.nameb = name.getBytes(charset);
+        this.initialized = true;
+    }
+
+    public final void setName(byte[] nameb) {
+        this.name = null;
+        this.nameb = Arrays.copyOf(nameb, nameb.length);
+        this.initialized = true;
+    }
+
+    public final boolean equalsName(ArchiveEntry an) {
+        return Arrays.equals(getNameAsBytes(), an.getNameAsBytes());
+    }
+
+    public final void setFileInfo(File file) {
+        assert isDirectory() == file.isDirectory();
+        setSize(file.length());
+        setLastModified(file.lastModified());
+    }
+
+    @Override
+    public boolean isDirectory() {
+        if (initialized)
+            return getName().endsWith("/");
+        return false;
+    }
+
+    @Override
+    public long getSize() {
+        throw new UnsupportedOperationException("ArchiveEntry#getSize");
+    }
+
+    @Override
+    public void setSize(long size) {
+        throw new UnsupportedOperationException("ArchiveEntry#setSize");
+    }
+
+    @Override
+    public long getCompressedSize() {
+        throw new UnsupportedOperationException("ArchiveEntry#getCompressedSize");
+    }
+
+    @Override
+    public void setCompressedSize(long size) {
+        throw new UnsupportedOperationException("ArchiveEntry#setCompressedSize");
+    }
+
+    @Override
+    public long getLastModified() {
+        throw new UnsupportedOperationException("ArchiveEntry#getLastModified");
+    }
+
+    @Override
+    public void setLastModified(long time) {
+        throw new UnsupportedOperationException("ArchiveEntry#setLastModified");
+    }
+
+    public String getMethodName() {
+        return "";
+    }
+
+    @Override
+    public String toString() {
+        return String.format("%s:%s", getClass().getSimpleName(), getName());
+    }
+
+    private static final class NullArchiveEntry extends ArchiveEntry {
+
+        public NullArchiveEntry() {
+            super(false);
+        }
+
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/ArchiveEntryInterface.java b/src/jp/sfjp/armadillo/archive/ArchiveEntryInterface.java
new file mode 100644 (file)
index 0000000..c5672a3
--- /dev/null
@@ -0,0 +1,23 @@
+package jp.sfjp.armadillo.archive;
+
+interface ArchiveEntryInterface {
+
+    String getName();
+
+    long getSize();
+
+    void setSize(long size);
+
+    long getCompressedSize();
+
+    void setCompressedSize(long size);
+
+    long getLastModified();
+
+    void setLastModified(long time);
+
+    boolean isDirectory();
+
+    void setAdded(boolean b);
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/ArchiveExtractor.java b/src/jp/sfjp/armadillo/archive/ArchiveExtractor.java
new file mode 100644 (file)
index 0000000..9cff652
--- /dev/null
@@ -0,0 +1,11 @@
+package jp.sfjp.armadillo.archive;
+
+import java.io.*;
+
+public interface ArchiveExtractor extends Closeable {
+
+    ArchiveEntry nextEntry() throws IOException;
+
+    long extract(OutputStream os) throws IOException;
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/ArchiveFile.java b/src/jp/sfjp/armadillo/archive/ArchiveFile.java
new file mode 100644 (file)
index 0000000..e03f6f8
--- /dev/null
@@ -0,0 +1,149 @@
+package jp.sfjp.armadillo.archive;
+
+import java.io.*;
+import java.util.*;
+
+public abstract class ArchiveFile implements
+                                 Iterable<ArchiveEntry>,
+                                 ArchiveCreator,
+                                 ArchiveExtractor {
+
+    protected long currentPosition;
+    protected boolean opened;
+    private boolean closed;
+
+    protected ArchiveFile() {
+        this.currentPosition = 0L;
+        this.opened = false;
+        this.closed = false;
+    }
+
+    @Override
+    public Iterator<ArchiveEntry> iterator() {
+        return new Iterator<ArchiveEntry>() {
+            boolean first = true;
+            ArchiveEntry entry = null;
+
+            @Override
+            public boolean hasNext() {
+                try {
+                    if (first) {
+                        reset();
+                        first = false;
+                    }
+                    entry = nextEntry();
+                }
+                catch (IOException ex) {
+                    handleException(ex);
+                }
+                assert entry != null;
+                return entry != ArchiveEntry.NULL;
+            }
+
+            @Override
+            public ArchiveEntry next() {
+                return entry;
+            }
+
+            @Override
+            public void remove() {
+                try {
+                    removeEntry(entry);
+                }
+                catch (IOException ex) {
+                    handleException(ex);
+                }
+            }
+
+            void handleException(Exception ex) {
+                throw new RuntimeException(ex);
+            }
+        };
+    }
+
+    @Override
+    public void addEntry(ArchiveEntry entry, File file) throws IOException {
+        assert entry.isDirectory() == file.isDirectory();
+        if (entry.isDirectory())
+            addEntry(entry, null, 0L);
+        else {
+            FileInputStream fis = new FileInputStream(file);
+            try {
+                addEntry(entry, fis, file.length());
+            }
+            finally {
+                fis.close();
+            }
+        }
+    }
+
+    @Override
+    public void addEntry(ArchiveEntry entry, InputStream is, long length) throws IOException {
+        throw new UnsupportedOperationException("ArchiveFile#addEntry");
+    }
+
+    public void updateEntry(ArchiveEntry entry, File file) throws IOException {
+        assert entry.isDirectory() == file.isDirectory();
+        if (entry.isDirectory())
+            updateEntry(entry, null, 0);
+        else {
+            FileInputStream fis = new FileInputStream(file);
+            try {
+                updateEntry(entry, fis, file.length());
+            }
+            finally {
+                fis.close();
+            }
+        }
+    }
+
+    public void updateEntry(ArchiveEntry entry, InputStream is, long length) throws IOException {
+        removeEntry(entry);
+        addEntry(entry, is, length);
+    }
+
+    public void removeEntry(ArchiveEntry entry) throws IOException {
+        throw new UnsupportedOperationException("ArchiveFile#removeEntry");
+    }
+
+    public boolean seek(ArchiveEntry entry) throws IOException {
+        reset();
+        while (true) {
+            ArchiveEntry nextEntry = nextEntry();
+            if (nextEntry == null)
+                break;
+            if (nextEntry.equalsName(entry))
+                return true;
+        }
+        reset();
+        return false;
+    }
+
+    @Override
+    public ArchiveEntry nextEntry() throws IOException {
+        throw new UnsupportedOperationException("ArchiveFile#nextEntry");
+    }
+
+    @Override
+    public long extract(OutputStream os) throws IOException {
+        throw new UnsupportedOperationException("ArchiveFile#extract");
+    }
+
+    public abstract void open() throws IOException;
+
+    public abstract void reset() throws IOException;
+
+    protected final void ensureOpen() throws IOException {
+        if (!opened)
+            throw new IOException("file is not opened yet");
+        if (closed)
+            throw new IOException("file was closed");
+    }
+
+    @Override
+    public void close() throws IOException {
+        currentPosition = 0L;
+        closed = true;
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/ArchiveInputStream.java b/src/jp/sfjp/armadillo/archive/ArchiveInputStream.java
new file mode 100644 (file)
index 0000000..1681e55
--- /dev/null
@@ -0,0 +1,113 @@
+package jp.sfjp.armadillo.archive;
+
+import java.io.*;
+
+/**
+ * The InputStream for Archive.
+ */
+public abstract class ArchiveInputStream extends FilterInputStream {
+
+    protected InputStream frontStream;
+    protected long remaining;
+
+    private boolean closed;
+
+    public ArchiveInputStream(InputStream is) {
+        super(is);
+        this.closed = false;
+        this.remaining = 0;
+    }
+
+    protected final void ensureOpen() throws IOException {
+        if (closed)
+            throw new IOException("stream closed");
+    }
+
+    @Override
+    public int read() throws IOException {
+        ensureOpen();
+        if (frontStream == null)
+            return super.read();
+        else if (remaining <= 0) {
+            assert remaining == 0;
+            return -1;
+        }
+        else {
+            int read = frontStream.read();
+            if (read == -1)
+                return -1;
+            --remaining;
+            return read;
+        }
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+        ensureOpen();
+        if (frontStream == null)
+            return super.read(b, off, len);
+        else if (remaining <= 0) {
+            assert remaining == 0;
+            return -1;
+        }
+        else {
+            long requiredLength = Math.min(len, remaining);
+            assert requiredLength <= Integer.MAX_VALUE;
+            int readLength = frontStream.read(b, off, (int)requiredLength);
+            assert readLength != 0 : "Read Zero";
+            if (readLength >= 0)
+                remaining -= readLength;
+            return readLength;
+        }
+    }
+
+    @Override
+    public long skip(long n) throws IOException {
+        ensureOpen();
+        if (frontStream == null)
+            return super.skip(n);
+        else if (remaining <= 0) {
+            assert remaining == 0;
+            return -1;
+        }
+        else {
+            long skipped = frontStream.skip(n);
+            if (skipped > 0)
+                remaining -= skipped;
+            return skipped;
+        }
+    }
+
+    @Override
+    public int available() throws IOException {
+        return 0;
+    }
+
+    @Override
+    public boolean markSupported() {
+        return false;
+    }
+
+    @Override
+    public synchronized void mark(int limit) {
+        // not supported
+    }
+
+    @Override
+    public synchronized void reset() throws IOException {
+        throw new IOException("mark/reset not supported");
+    }
+
+    @Override
+    public void close() throws IOException {
+        ensureOpen();
+        try {
+            super.close();
+        }
+        finally {
+            frontStream = null;
+            closed = true;
+        }
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/ArchiveOutputStream.java b/src/jp/sfjp/armadillo/archive/ArchiveOutputStream.java
new file mode 100644 (file)
index 0000000..d9e8015
--- /dev/null
@@ -0,0 +1,65 @@
+package jp.sfjp.armadillo.archive;
+
+import java.io.*;
+
+/**
+ * The OutputStream for Archive.
+ */
+public abstract class ArchiveOutputStream extends FilterOutputStream {
+
+    protected OutputStream frontStream;
+    protected long written;
+
+    private boolean closed;
+
+    public ArchiveOutputStream(OutputStream out) {
+        super(out);
+    }
+
+    protected final void ensureOpen() throws IOException {
+        if (closed)
+            throw new IOException("stream closed");
+    }
+
+    @Override
+    public void write(int b) throws IOException {
+        ensureOpen();
+        if (frontStream == null)
+            super.write(b);
+        else
+            frontStream.write(b);
+        ++written;
+    }
+
+    @Override
+    public void write(byte[] b, int off, int len) throws IOException {
+        ensureOpen();
+        if (frontStream == null)
+            super.write(b, off, len);
+        else
+            frontStream.write(b, off, len);
+        written += len;
+    }
+
+    @Override
+    public void flush() throws IOException {
+        ensureOpen();
+        if (frontStream == null)
+            super.flush();
+        else
+            frontStream.flush();
+    }
+
+    @Override
+    public void close() throws IOException {
+        try {
+            flush();
+        }
+        finally {
+            closed = true;
+            frontStream = null;
+            super.close();
+        }
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/ArchiveType.java b/src/jp/sfjp/armadillo/archive/ArchiveType.java
new file mode 100644 (file)
index 0000000..4999de8
--- /dev/null
@@ -0,0 +1,45 @@
+package jp.sfjp.armadillo.archive;
+
+public enum ArchiveType {
+
+    TAR, TARZ, TARGZ, TARBZ2, TARXZ, ZIP, LZH, CAB, Unknown;
+
+    public static ArchiveType of(String fileName) {
+        final String ext = getExtension(fileName);
+        if (ext.equalsIgnoreCase("tar"))
+            return TAR;
+        if (ext.equalsIgnoreCase("zip") || ext.equalsIgnoreCase("jar"))
+            return ZIP;
+        if (ext.equalsIgnoreCase("lzh"))
+            return LZH;
+        if (ext.equalsIgnoreCase("cab"))
+            return CAB;
+        if (endsWithIgnoreCase(fileName, ".tar.z", ".tz", ".taz"))
+            return TARZ;
+        if (endsWithIgnoreCase(fileName, ".tar.gz", ".tgz"))
+            return TARGZ;
+        if (endsWithIgnoreCase(fileName, ".tar.bz2", ".tbz"))
+            return TARBZ2;
+        if (endsWithIgnoreCase(fileName, ".tar.xz", ".txz"))
+            return TARXZ;
+        return Unknown;
+    }
+
+    static boolean endsWithIgnoreCase(String s, String... suffixes) {
+        for (final String suffix : suffixes) {
+            final int sl = s.length();
+            final int suffixl = suffix.length();
+            if (sl >= suffixl && s.substring(sl - suffixl).equalsIgnoreCase(suffix))
+                return true;
+        }
+        return false;
+    }
+
+    static String getExtension(String fileName) {
+        final int index = fileName.lastIndexOf('.');
+        if (index < 0)
+            return "";
+        return fileName.substring(index + 1).toLowerCase();
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/DumpArchiveHeader.java b/src/jp/sfjp/armadillo/archive/DumpArchiveHeader.java
new file mode 100644 (file)
index 0000000..41bb511
--- /dev/null
@@ -0,0 +1,79 @@
+package jp.sfjp.armadillo.archive;
+
+import java.io.*;
+import jp.sfjp.armadillo.archive.cab.*;
+import jp.sfjp.armadillo.archive.lzh.*;
+import jp.sfjp.armadillo.archive.tar.*;
+import jp.sfjp.armadillo.archive.zip.*;
+
+public abstract class DumpArchiveHeader {
+
+    public abstract void dump(InputStream is, PrintWriter out) throws IOException;
+
+    protected void printOffset(PrintWriter out, long offset) {
+        out.printf("%n---------- offset %1$d ( 0x%1$08X )%n", offset);
+    }
+
+    protected void printEnd(PrintWriter out, String name, long offset) {
+        out.printf("%n---------- end of %1$s : position = %2$d ( 0x%2$08X )%n", name, offset);
+    }
+
+    protected void printHeaderName(PrintWriter out, String name) {
+        out.printf("%n%s%n", name);
+    }
+
+    protected static void warn(PrintWriter out, String fmt, Object... args) {
+        out.printf("warn: " + fmt + "%n", args);
+    }
+
+    public static DumpArchiveHeader getInstance(String filename) {
+        switch (ArchiveType.of(filename)) {
+            case CAB:
+                return new DumpCabHeader();
+            case LZH:
+                return new DumpLzhHeader();
+            case TAR:
+                return new DumpTarHeader();
+            case ZIP:
+                return new DumpZipHeader();
+            default:
+                throw new IllegalArgumentException("unknown type: " + filename);
+        }
+    }
+
+    public static void execute(File f) throws IOException {
+        execute(f, new PrintWriter(System.out, true));
+    }
+
+    public static void execute(File f, PrintWriter out) throws IOException {
+        FileInputStream is = new FileInputStream(f);
+        try {
+            getInstance(f.getName()).dump(is, out);
+        }
+        finally {
+            is.close();
+        }
+    }
+
+    public static void main(String... args) throws IOException {
+        if (args.length != 1) {
+            System.out.println("usage: DumpArchiveHeader file [ file2 ... fileN ]");
+            return;
+        }
+        for (final String arg : args) {
+            final File f = new File(arg);
+            if (!f.exists()) {
+                System.out.printf("warn: file [%s] not found %n", arg);
+                continue;
+            }
+            try {
+                execute(f);
+            }
+            catch (Exception ex) {
+                System.err.printf("%n--- at file [%s]%n", f.getAbsolutePath());
+                ex.printStackTrace();
+            }
+        }
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/cab/CabArchiveCreator.java b/src/jp/sfjp/armadillo/archive/cab/CabArchiveCreator.java
new file mode 100644 (file)
index 0000000..a8fc27c
--- /dev/null
@@ -0,0 +1,64 @@
+package jp.sfjp.armadillo.archive.cab;
+
+import java.io.*;
+import jp.sfjp.armadillo.archive.*;
+import jp.sfjp.armadillo.io.*;
+
+public final class CabArchiveCreator implements ArchiveCreator {
+
+    private CabOutputStream os;
+
+    public CabArchiveCreator(OutputStream os) {
+        this.os = new CabOutputStream(os);
+    }
+
+    @Override
+    public ArchiveEntry newEntry(String name) {
+        return CabEntry.getInstance(name);
+    }
+
+    @Override
+    public void addEntry(ArchiveEntry entry, File file) throws IOException {
+        if (file.isDirectory()) {
+            os.putNextEntry(toCabEntry(entry));
+            os.closeEntry();
+        }
+        else {
+            InputStream is = new FileInputStream(file);
+            try {
+                addEntry(entry, is, file.length());
+            }
+            finally {
+                is.close();
+            }
+        }
+        entry.setAdded(true);
+    }
+
+    @Override
+    public void addEntry(ArchiveEntry entry, InputStream is, long length) throws IOException {
+        CabEntry fileEntry = toCabEntry(entry);
+        fileEntry.setSize(length);
+        os.putNextEntry(fileEntry);
+        final long size = IOUtilities.transferAll(is, os);
+        os.closeEntry();
+        assert size == fileEntry.getSize() : "file size";
+        assert size == length : "file size";
+        entry.setSize(size);
+        entry.setAdded(true);
+    }
+
+    static CabEntry toCabEntry(ArchiveEntry entry) {
+        if (entry instanceof CabEntry)
+            return (CabEntry)entry;
+        CabEntry newEntry = CabEntry.getInstance(entry.getName());
+        entry.copyTo(newEntry);
+        return newEntry;
+    }
+
+    @Override
+    public void close() throws IOException {
+        os.close();
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/cab/CabArchiveExtractor.java b/src/jp/sfjp/armadillo/archive/cab/CabArchiveExtractor.java
new file mode 100644 (file)
index 0000000..0e502bb
--- /dev/null
@@ -0,0 +1,30 @@
+package jp.sfjp.armadillo.archive.cab;
+
+import java.io.*;
+import jp.sfjp.armadillo.archive.*;
+import jp.sfjp.armadillo.io.*;
+
+public final class CabArchiveExtractor implements ArchiveExtractor {
+
+    private CabInputStream is;
+
+    public CabArchiveExtractor(InputStream is) {
+        this.is = new CabInputStream(is);
+    }
+
+    @Override
+    public ArchiveEntry nextEntry() throws IOException {
+        return ArchiveEntry.orNull(is.getNextEntry());
+    }
+
+    @Override
+    public long extract(OutputStream os) throws IOException {
+        return IOUtilities.transferAll(is, os);
+    }
+
+    @Override
+    public void close() throws IOException {
+        is.close();
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/cab/CabCfData.java b/src/jp/sfjp/armadillo/archive/cab/CabCfData.java
new file mode 100644 (file)
index 0000000..02a86b1
--- /dev/null
@@ -0,0 +1,78 @@
+package jp.sfjp.armadillo.archive.cab;
+
+import java.io.*;
+import java.util.zip.*;
+
+public final class CabCfData extends FilterOutputStream {
+
+    ByteArrayOutputStream bos = new ByteArrayOutputStream();
+    OutputStream os; // front
+    private Deflater deflater;
+
+    int uncompsize = 0;
+    boolean finished;
+    private final CabCompressionType type;
+
+    public CabCfData(CabCompressionType type) {
+        super(null);
+        this.os = bos;
+        this.type = type;
+        switch (type) {
+            case No:
+                os = bos;
+                break;
+            case MSZIP:
+                bos.write('C');
+                bos.write('K');
+                this.deflater = new Deflater(Deflater.BEST_COMPRESSION, true);
+                deflater.reset();
+                os = new DeflaterOutputStream(bos, deflater);
+                break;
+            case Quantum:
+            case LZX:
+            case Unknown:
+                throw new UnsupportedOperationException("comptype: " + type);
+        }
+    }
+
+    @Override
+    public void write(int b) throws IOException {
+        if (finished)
+            throw new IOException("stream closed");
+        os.write(b);
+        ++uncompsize;
+    }
+
+    @Override
+    public void close() throws IOException {
+        finish();
+    }
+
+    public void finish() throws IOException {
+        if (finished)
+            return;
+        switch (type) {
+            case MSZIP:
+                if (os instanceof DeflaterOutputStream) {
+                    DeflaterOutputStream dos = (DeflaterOutputStream)os;
+                    dos.finish();
+                    dos.flush();
+                }
+                break;
+            case No:
+                os.flush();
+                break;
+            default:
+                // do nothing
+        }
+        os = bos;
+        finished = true;
+    }
+
+    public void writeInto(OutputStream out) throws IOException {
+        if (!finished)
+            throw new IOException("stream is not closed yet");
+        bos.writeTo(out);
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/cab/CabCfFile.java b/src/jp/sfjp/armadillo/archive/cab/CabCfFile.java
new file mode 100644 (file)
index 0000000..614355a
--- /dev/null
@@ -0,0 +1,84 @@
+package jp.sfjp.armadillo.archive.cab;
+
+import java.io.*;
+import jp.sfjp.armadillo.time.*;
+
+public final class CabCfFile extends CabEntry {
+
+    private static final FTime FTIME = new FTime();
+
+    CabCfFolder folder = new CabCfFolder("./");
+    int uncompressedSize;
+    int uncompressedOffset;
+    short folderIndex;
+    short date;
+    short time;
+    short attributes;
+
+    public CabCfFile() {
+        super();
+        this.uncompressedSize = 0;
+        this.uncompressedOffset = 0;
+        this.folderIndex = 0;
+        this.date = 0;
+        this.time = 0;
+        this.attributes = 0x20;
+    }
+
+    public CabCfFile(byte[] name) {
+        this();
+        setName(name);
+    }
+
+    public CabCfFile(String name, File file) {
+        this();
+        setName(name);
+        setSize(file.length());
+        setLastModified(file.lastModified());
+    }
+
+    @Override
+    public boolean isDirectory() {
+        return false;
+    }
+
+    @Override
+    public long getSize() {
+        return uncompressedSize;
+    }
+
+    @Override
+    public void setSize(long size) {
+        if (size > Integer.MAX_VALUE)
+            throw new IllegalArgumentException("limit=int_max, size=" + size);
+        this.uncompressedSize = (int)(size & 0xFFFFFFFF);
+    }
+
+    @Override
+    public long getCompressedSize() {
+        return -1L;
+    }
+
+    @Override
+    public void setCompressedSize(long size) {
+        // ignore
+    }
+
+    @Override
+    public long getLastModified() {
+        return FTIME.toMillisecond(date, time);
+    }
+
+    @Override
+    public void setLastModified(long lastModified) {
+        final int datetime = FTIME.int32From(lastModified);
+        this.date = (short)((datetime >> 16) & 0xFFFF);
+        this.time = (short)(datetime & 0xFFFF);
+    }
+
+    @Override
+    public String getMethodName() {
+        return CabCompressionType.of(folder.method).toString();
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/cab/CabCfFolder.java b/src/jp/sfjp/armadillo/archive/cab/CabCfFolder.java
new file mode 100644 (file)
index 0000000..b5445ba
--- /dev/null
@@ -0,0 +1,103 @@
+package jp.sfjp.armadillo.archive.cab;
+
+import java.io.*;
+import java.util.*;
+
+public final class CabCfFolder extends CabEntry {
+
+    final List<CabCfFile> files;
+    ByteArrayOutputStream bos;
+    CabCfData currentCabCfData;
+    short method;
+    int checksum;
+    int offset;
+    final List<CabCfData> arrayOfCfData;
+
+    public CabCfFolder() {
+        super();
+        this.files = new ArrayList<CabCfFile>();
+        this.arrayOfCfData = new ArrayList<CabCfData>();
+    }
+
+    public CabCfFolder(String name) {
+        this();
+        if (name == null)
+            throw new IllegalArgumentException("name is null");
+        setName(name);
+        this.bos = new ByteArrayOutputStream();
+    }
+
+    public void writeCfDataInto(OutputStream os, CabHeader header) throws IOException {
+        for (CabCfData data : arrayOfCfData) {
+            header.writeCfDataHeader(os, data);
+            data.writeInto(os);
+        }
+    }
+
+    @Override
+    public long getSize() {
+        return 0;
+    }
+
+    @Override
+    public void setSize(long size) {
+        // ignore
+    }
+
+    @Override
+    public long getCompressedSize() {
+        return 0;
+    }
+
+    @Override
+    public void setCompressedSize(long size) {
+        // ignore
+    }
+
+    @Override
+    public void setLastModified(long time) {
+        // ignore
+    }
+
+    public boolean add(CabCfFile f) {
+        return files.add(f);
+    }
+
+    public int getCompressedDataSize() {
+        int size = 0;
+        for (CabCfData data : arrayOfCfData)
+            size += data.bos.size();
+        return size;
+    }
+
+    public void close() throws IOException {
+        final int size = bos.size();
+        byte[] bytes = bos.toByteArray();
+        final int cfDataSize = 32768;
+        int len = size;
+        final int count;
+        if (size == 0)
+            count = 0;
+        else if (size < cfDataSize)
+            count = 1;
+        else
+            count = size / cfDataSize + 1;
+        int p = 0;
+        method = 1;
+        for (int i = 0; i < count; i++) {
+            CabCfData data = new CabCfData(CabCompressionType.of(method));
+            try {
+                final int writeLen = len > cfDataSize ? cfDataSize : len;
+                data.write(bytes, p, writeLen);
+                p += cfDataSize;
+                len -= cfDataSize;
+            }
+            finally {
+                data.close();
+            }
+            arrayOfCfData.add(data);
+        }
+        bos = null;
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/cab/CabChecksum.java b/src/jp/sfjp/armadillo/archive/cab/CabChecksum.java
new file mode 100644 (file)
index 0000000..3fadcae
--- /dev/null
@@ -0,0 +1,93 @@
+package jp.sfjp.armadillo.archive.cab;
+
+import java.util.zip.*;
+
+/**
+ * Checksum for Cab file.
+ * @see <a href="http://msdn.microsoft.com/en-us/library/bb267310.aspx">
+ *      Microsoft Cabinet Format (http://msdn.microsoft.com/en-us/library/bb267310.aspx)</a>
+ */
+public final class CabChecksum implements Checksum {
+
+    private final int seed;
+
+    private int value;
+    private int p;
+    private int tmp;
+
+    public CabChecksum() {
+        this(0);
+    }
+
+    public CabChecksum(int seed) {
+        this.seed = seed;
+        reset();
+    }
+
+    @Override
+    public void update(int b) {
+        switch (++p % 4) {
+            case 0:
+                tmp = b;
+                break;
+            case 1:
+                tmp |= b << 8;
+                break;
+            case 2:
+                tmp |= b << 16;
+                break;
+            case 3:
+                tmp |= b << 24;
+                value ^= tmp;
+                tmp = 0;
+                break;
+            default:
+        }
+    }
+
+    @Override
+    public void update(byte[] b, int off, int len) {
+        for (int i = off; i < len; i++)
+            update(b[i] & 0xFF);
+    }
+
+    @Override
+    public long getValue() {
+        final int tmpValue;
+        final int mod = (p + 1) % 4;
+        if (mod == 0)
+            tmpValue = value;
+        else {
+            int x = 0;
+            switch (mod) {
+                case 3:
+                    x |= (tmp << 16) & 0xFF0000;
+                    x |= tmp & 0x00FF00;
+                    x |= (tmp >> 16) & 0x0000FF;
+                    assert x == (x & 0xFFFFFF);
+                    break;
+                case 2:
+                    x |= (tmp << 8) & 0xFF00;
+                    x |= (tmp >> 8) & 0x00FF;
+                    assert x == (x & 0xFFFF);
+                    break;
+                case 1:
+                    x = tmp;
+                    assert x == (x & 0xFF);
+                    break;
+                default:
+                    break;
+            }
+            tmpValue = value ^ x;
+        }
+        assert tmpValue == (tmpValue & 0xFFFFFFFF);
+        return tmpValue;
+    }
+
+    @Override
+    public void reset() {
+        value = seed;
+        p = -1;
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/cab/CabCompressionType.java b/src/jp/sfjp/armadillo/archive/cab/CabCompressionType.java
new file mode 100644 (file)
index 0000000..ae9a0a8
--- /dev/null
@@ -0,0 +1,31 @@
+package jp.sfjp.armadillo.archive.cab;
+
+import static jp.sfjp.armadillo.archive.cab.CabCompressionType.Constants.*;
+
+public enum CabCompressionType {
+
+    No, MSZIP, Quantum, LZX, Unknown;
+
+    public static CabCompressionType of(short type) {
+        switch (type) {
+            case iNo:
+                return No;
+            case iMSZIP:
+                return MSZIP;
+            case iQuantum:
+                return Quantum;
+            case iLZX:
+                return LZX;
+            default:
+        }
+        return Unknown;
+    }
+
+    static final class Constants {
+        static final short iNo = 0x0000;
+        static final short iMSZIP = 0x0001;
+        static final short iQuantum = 0x0002;
+        static final short iLZX = 0x0003;
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/cab/CabEntry.java b/src/jp/sfjp/armadillo/archive/cab/CabEntry.java
new file mode 100644 (file)
index 0000000..2aef843
--- /dev/null
@@ -0,0 +1,32 @@
+package jp.sfjp.armadillo.archive.cab;
+
+import jp.sfjp.armadillo.archive.*;
+
+public abstract class CabEntry extends ArchiveEntry {
+
+    protected CabEntry() {
+        super(false);
+    }
+
+    protected CabEntry(String name) {
+        super(false);
+        setName(name);
+    }
+
+    public CabEntry(byte[] name) {
+        super(false);
+        setName(name);
+    }
+
+    public static CabEntry getInstance(String name) {
+        CabEntry entry = normalizePath(name).endsWith("/") ? new CabCfFolder() : new CabCfFile();
+        entry.setName(name);
+        return entry;
+    }
+
+    static String normalizePath(String path) {
+        String s = path.replace('\\', '/');
+        return s;
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/cab/CabException.java b/src/jp/sfjp/armadillo/archive/cab/CabException.java
new file mode 100644 (file)
index 0000000..d6545c3
--- /dev/null
@@ -0,0 +1,19 @@
+package jp.sfjp.armadillo.archive.cab;
+
+import java.io.*;
+
+public class CabException extends IOException {
+
+    public CabException(String message) {
+        super(message);
+    }
+
+    public CabException(String format, Object... args) {
+        super(String.format(format, args));
+    }
+
+    public CabException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/cab/CabHeader.java b/src/jp/sfjp/armadillo/archive/cab/CabHeader.java
new file mode 100644 (file)
index 0000000..7185817
--- /dev/null
@@ -0,0 +1,361 @@
+package jp.sfjp.armadillo.archive.cab;
+
+import java.io.*;
+import java.nio.*;
+import java.nio.channels.*;
+import java.util.*;
+import java.util.zip.*;
+
+/**
+ * Cab Header.
+ * @see <a href="http://msdn.microsoft.com/en-us/library/bb267310.aspx">
+ *      Microsoft Cabinet Format (http://msdn.microsoft.com/en-us/library/bb267310.aspx)</a>
+ */
+public final class CabHeader {
+
+    // signature 'MSCF'
+    static final int SIGNATURE = 0x4D534346;
+
+    private static final byte MAJOR_VERSION = 1;
+    private static final byte MINOR_VERSION = 3;
+
+    private final ByteBuffer buffer;
+    // on read
+    private boolean initialized;
+    private final List<CabCfFile> fileEntriesOnRead;
+    private int entryIndex;
+
+    private short[] folderCompressType;
+
+    public CabHeader() {
+        this.buffer = ByteBuffer.allocate(8192);
+        this.initialized = false;
+        this.fileEntriesOnRead = new ArrayList<CabCfFile>();
+    }
+
+    public boolean initialize(InputStream is) throws IOException {
+        if (initialized)
+            return false;
+        try {
+            readFileHeader(is);
+            return true;
+        }
+        catch (ArrayIndexOutOfBoundsException ex) {
+            throw new CabException("readFileHeader", ex);
+        }
+        catch (BufferUnderflowException ex) {
+            throw new CabException("readFileHeader", ex);
+        }
+    }
+
+    @SuppressWarnings("unused")
+    private void readFileHeader(InputStream is) throws IOException {
+        // Cabinet File header
+        final int signature; // cabinet file signature
+        final int reserved1; // reserved
+        final int cbCabinet; // size of this cabinet file in bytes
+        final int reserved2; // reserved
+        final int coffFiles; // offset of the first CFFILE entry
+        final int reserved3; // reserved
+        final byte versionMinor; // cabinet file format version, minor
+        final byte versionMajor; // cabinet file format version, major
+        final short cFolders; // number of CFFOLDER entries in this cabinet
+        final short cFiles; // number of CFFILE entries in this cabinet
+        final short flags; // cabinet file option indicators
+        final short setID; // must be the same for all cabinets in a set
+        final short iCabinet; // number of this cabinet file in a set
+        // (optional fields not supported)
+        // ---
+        buffer.clear();
+        buffer.limit(36);
+        Channels.newChannel(is).read(buffer);
+        if (buffer.position() == 0)
+            throw new CabException("reading archive header, but position is zero");
+        buffer.rewind();
+        buffer.order(ByteOrder.BIG_ENDIAN);
+        signature = buffer.getInt();
+        if (signature != SIGNATURE)
+            throw new CabException("bad signature: 0x%X", signature);
+        buffer.order(ByteOrder.LITTLE_ENDIAN);
+        reserved1 = buffer.getInt();
+        cbCabinet = buffer.getInt();
+        reserved2 = buffer.getInt();
+        coffFiles = buffer.getInt();
+        reserved3 = buffer.getInt();
+        versionMinor = buffer.get();
+        versionMajor = buffer.get();
+        cFolders = buffer.getShort();
+        cFiles = buffer.getShort();
+        flags = buffer.getShort();
+        setID = buffer.getShort();
+        iCabinet = buffer.getShort();
+        if (iCabinet != 0)
+            throw new CabException("iCabinet not supported: 0x%d", iCabinet);
+        folderCompressType = new short[cFolders];
+        // read CFFOLDERs
+        for (int i = 0; i < cFolders; i++) {
+            // CFFOLDER header
+            final int coffCabStart; // offset of the first CFDATA block in this folder
+            final short cCFData; // number of CFDATA blocks in this folder
+            final short typeCompress; // compression type indicator
+            final byte[] abReserve; // (optional) per-folder reserved area
+            // ---
+            buffer.clear();
+            buffer.limit(8);
+            Channels.newChannel(is).read(buffer);
+            if (buffer.position() == 0)
+                throw new CabException("reading CFFOLDER header, but position is zero");
+            buffer.rewind();
+            coffCabStart = buffer.getInt();
+            cCFData = buffer.getShort();
+            typeCompress = buffer.getShort();
+            abReserve = new byte[0];
+            folderCompressType[i] = typeCompress;
+        }
+        // read CFFILEs
+        for (int i = 0; i < cFiles; i++) {
+            // CFFILE header
+            final int cbFile; // uncompressed size of this file in bytes
+            final int uoffFolderStart; // uncompressed offset of this file in the folder
+            final short iFolder; // index into the CFFOLDER area
+            final short date; // date stamp for this file
+            final short time; // time stamp for this file
+            final short attribs; // attribute flags for this file
+            final byte[] szName; // name of this file
+            // ---
+            buffer.clear();
+            buffer.limit(16);
+            Channels.newChannel(is).read(buffer);
+            if (buffer.position() == 0)
+                throw new CabException("reading CFFILE header, but position is zero");
+            buffer.rewind();
+            cbFile = buffer.getInt();
+            uoffFolderStart = buffer.getInt();
+            iFolder = buffer.getShort();
+            date = buffer.getShort();
+            time = buffer.getShort();
+            attribs = buffer.getShort();
+            szName = readName(is, 256);
+            CabCfFile entry = new CabCfFile(szName);
+            entry.uncompressedSize = cbFile;
+            entry.uncompressedOffset = uoffFolderStart;
+            entry.folderIndex = iFolder;
+            entry.date = date;
+            entry.time = time;
+            entry.attributes = attribs;
+            entry.setName(szName);
+            fileEntriesOnRead.add(entry);
+        }
+        initialized = true;
+    }
+
+    private byte[] readName(InputStream is, int limit) throws IOException {
+        byte[] bytes = new byte[limit];
+        int readSize = 0;
+        for (int i = 0; i < limit; i++) {
+            int b = is.read();
+            if (b <= 0)
+                break;
+            bytes[i] = (byte)(b & 0xFF);
+            ++readSize;
+        }
+        if (readSize < limit) {
+            byte[] r = new byte[readSize];
+            System.arraycopy(bytes, 0, r, 0, readSize);
+            return r;
+        }
+        else
+            return bytes;
+    }
+
+    public CabEntry read(InputStream is) throws IOException {
+        if (!initialized)
+            initialize(is);
+        if (entryIndex >= fileEntriesOnRead.size())
+            return null;
+        return fileEntriesOnRead.get(entryIndex++);
+    }
+
+    @SuppressWarnings("unused")
+    public void write(OutputStream os, List<CabCfFolder> folders) throws IOException {
+        List<CabCfFile> files = new ArrayList<CabCfFile>();
+        for (CabCfFolder folder : folders)
+            files.addAll(folder.files);
+        // Cabinet File header
+        final int signature; // cabinet file signature
+        final int reserved1; // reserved
+        final int cbCabinet; // size of this cabinet file in bytes
+        final int reserved2; // reserved
+        final int coffFiles; // offset of the first CFFILE entry
+        final int reserved3; // reserved
+        final byte versionMinor; // cabinet file format version, minor
+        final byte versionMajor; // cabinet file format version, major
+        final short cFolders; // number of CFFOLDER entries in this cabinet
+        final short cFiles; // number of CFFILE entries in this cabinet
+        final short flags; // cabinet file option indicators
+        final short setID; // must be the same for all cabinets in a set
+        final short iCabinet;// number of this cabinet file in a set
+        // (optional fields not supported)
+        // ---
+        assert folders.size() <= Short.MAX_VALUE;
+        assert files.size() <= Short.MAX_VALUE;
+        signature = SIGNATURE;
+        reserved1 = 0;
+        cbCabinet = calculateFileSize(folders, files);
+        reserved2 = 0;
+        reserved3 = 0;
+        coffFiles = calculateOffsetOfFirstCffile(folders);
+        versionMinor = MINOR_VERSION;
+        versionMajor = MAJOR_VERSION;
+        cFolders = (short)folders.size();
+        cFiles = (short)files.size();
+        flags = 0;
+        setID = 0; // ignore
+        iCabinet = 0;
+        buffer.clear();
+        // Header
+        buffer.order(ByteOrder.BIG_ENDIAN);
+        buffer.putInt(SIGNATURE);
+        buffer.order(ByteOrder.LITTLE_ENDIAN);
+        buffer.putInt(reserved1);
+        buffer.putInt(cbCabinet);
+        buffer.putInt(reserved2);
+        buffer.putInt(coffFiles);
+        buffer.putInt(reserved3);
+        buffer.put(versionMinor);
+        buffer.put(versionMajor);
+        buffer.putShort(cFolders);
+        buffer.putShort(cFiles);
+        buffer.putShort(flags);
+        buffer.putShort(setID);
+        buffer.putShort(iCabinet);
+        buffer.flip();
+        Channels.newChannel(os).write(buffer);
+        // Folders
+        int folderOffset = calculateOffsetOfCabStart(folders);
+        for (CabCfFolder folder : folders) {
+            // CFFOLDER header
+            final int coffCabStart; // offset of the first CFDATA block in this folder
+            final short cCFData; // number of CFDATA blocks in this folder
+            final short typeCompress; // compression type indicator
+            final byte[] abReserve; // (optional) per-folder reserved area
+            // ---
+            assert folder.arrayOfCfData.size() <= Short.MAX_VALUE;
+            coffCabStart = folderOffset;
+            cCFData = (short)folder.arrayOfCfData.size();
+            typeCompress = folder.method;
+            buffer.clear();
+            buffer.putInt(coffCabStart);
+            buffer.putShort(cCFData);
+            buffer.putShort(typeCompress);
+            buffer.flip();
+            Channels.newChannel(os).write(buffer);
+            folderOffset += folder.getCompressedDataSize();
+        }
+        // Files
+        for (CabCfFile entry : files) {
+            // CFFOLDER header
+            final int cbFile; // uncompressed size of this file in bytes
+            final int uoffFolderStart; // uncompressed offset of this file in the folder
+            final short iFolder; // index into the CFFOLDER area
+            final short date; // date stamp for this file
+            final short time; // time stamp for this file
+            final short attribs; // attribute flags for this file
+            final byte[] szName; // name of this file
+            // ---
+            cbFile = entry.uncompressedSize;
+            uoffFolderStart = entry.uncompressedOffset;
+            iFolder = entry.folderIndex;
+            date = entry.date;
+            time = entry.time;
+            attribs = entry.attributes;
+            szName = entry.getNameAsBytes();
+            buffer.clear();
+            buffer.putInt(cbFile);
+            buffer.putInt(uoffFolderStart);
+            buffer.putShort(iFolder);
+            buffer.putShort(date);
+            buffer.putShort(time);
+            buffer.putShort(attribs);
+            buffer.put(szName);
+            buffer.put((byte)0); // end of szName
+            buffer.flip();
+            Channels.newChannel(os).write(buffer);
+        }
+    }
+
+    @SuppressWarnings("unused")
+    public void writeCfDataHeader(OutputStream os, CabCfData cfdata) throws IOException {
+        // CFDATA header
+        final int csum; // checksum of this CFDATA entry
+        final short cbData; // number of compressed bytes in this block
+        final short cbUncomp; // number of uncompressed bytes in this block
+        final byte[] abReserve; // (optional) per-datablock reserved area
+        // ---
+        if (!cfdata.finished)
+            throw new IOException("stream is not closed yet");
+        assert cfdata.uncompsize >= 0 && cfdata.uncompsize <= 32768;
+        byte[] bytes = cfdata.bos.toByteArray();
+        final short cbData0 = (short)(bytes.length & 0xFFFF);
+        final short cbUncomp0 = (cfdata.uncompsize == 32768)
+                ? Short.MIN_VALUE
+                : (short)cfdata.uncompsize;
+        buffer.order(ByteOrder.LITTLE_ENDIAN);
+        buffer.clear();
+        csum = calcucateChecksum(bytes, cbData0, cbUncomp0);
+        cbData = cbData0;
+        cbUncomp = cbUncomp0;
+        buffer.putInt(csum);
+        buffer.putShort(cbData);
+        buffer.putShort(cbUncomp);
+        buffer.flip();
+        Channels.newChannel(os).write(buffer);
+    }
+
+    private int calcucateChecksum(byte[] bytes, short compsize, short uncompsize) {
+        final int csum1 = calcucateChecksum0(bytes, compsize, 0);
+        ByteBuffer buffer = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN);
+        buffer.putShort(compsize);
+        buffer.putShort(uncompsize);
+        buffer.rewind();
+        byte[] bytes2 = new byte[4];
+        buffer.get(bytes2);
+        return calcucateChecksum0(bytes2, 4, csum1);
+    }
+
+    private static int calcucateChecksum0(byte[] bytes, int cb, int seed) {
+        Checksum cksum = new CabChecksum(seed);
+        cksum.update(bytes, 0, cb);
+        return (int)(cksum.getValue() & 0xFFFFFFFF);
+    }
+
+    static int calculateFileSize(List<CabCfFolder> folders, List<CabCfFile> files) {
+        int x = 0;
+        // HEADER
+        x += 36;
+        // CFFOLDER
+        x += folders.size() * 8;
+        // CFFILE
+        for (final CabCfFile f : files)
+            x += 16 + f.getName().getBytes().length + 1;
+        // CFDATA
+        for (CabCfFolder folder : folders)
+            x += folder.getCompressedDataSize() + 8;
+        return x;
+    }
+
+    static int calculateOffsetOfFirstCffile(List<CabCfFolder> folders) {
+        final int sizeOfCfheader = 36;
+        return sizeOfCfheader + folders.size() * 8;
+    }
+
+    static int calculateOffsetOfCabStart(List<CabCfFolder> folders) {
+        int size = 0;
+        size += calculateOffsetOfFirstCffile(folders);
+        for (CabCfFolder folder : folders)
+            for (CabEntry entry : folder.files)
+                size += 16 + entry.getName().getBytes().length + 1;
+        return size;
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/cab/CabInputStream.java b/src/jp/sfjp/armadillo/archive/cab/CabInputStream.java
new file mode 100644 (file)
index 0000000..201d55a
--- /dev/null
@@ -0,0 +1,160 @@
+package jp.sfjp.armadillo.archive.cab;
+
+import java.io.*;
+import java.nio.*;
+import java.nio.channels.*;
+import java.util.zip.*;
+import jp.sfjp.armadillo.archive.*;
+import jp.sfjp.armadillo.io.*;
+
+public final class CabInputStream extends ArchiveInputStream {
+
+    private final CabHeader header;
+
+    private int index = 0;
+
+    public CabInputStream(InputStream is) {
+        super(is);
+        this.header = new CabHeader();
+    }
+
+    public CabInputStream(InputStream is, boolean readHeaderFirst) throws IOException {
+        this(is);
+        if (readHeaderFirst)
+            header.initialize(is);
+    }
+
+    public CabEntry getNextEntry() throws IOException {
+        ensureOpen();
+        CabEntry entry = header.read(in);
+        if (entry == null)
+            return null;
+        final boolean first = index == 0;
+        ++index;
+        remaining = entry.getSize();
+        if (first) {
+            final short imethod = 1;
+            final CabCompressionType type = CabCompressionType.of(imethod);
+            switch (type) {
+                case No:
+                    frontStream = in;
+                    break;
+                case MSZIP:
+                    frontStream = new CfDataInflateInputStream(in);
+                    break;
+                default:
+                    throw new CabException("Unsupported compression type: %s(0x%X)", type, imethod);
+            }
+        }
+        return entry;
+    }
+
+    static final class CfDataInflateInputStream extends FilterInputStream {
+
+        private static final int BLOCK_SIZE = 32768;
+
+        private final Inflater inflater;
+        private byte[] inflaterBuffer;
+
+        CfDataInflateInputStream(InputStream is) {
+            this(is, new Inflater(true));
+        }
+
+        CfDataInflateInputStream(InputStream is, Inflater inflater) {
+            super(is);
+            this.inflater = inflater;
+        }
+
+        @Override
+        public int read() throws IOException {
+            byte[] bytes = new byte[1];
+            if (read(bytes, 0, 1) < 1)
+                return -1;
+            return bytes[0];
+        }
+
+        @Override
+        public int read(byte[] b, int off, int len) throws IOException {
+            if (inflaterBuffer == null)
+                refill();
+            int p = off;
+            int q = len;
+            while (q > 0) {
+                final int r;
+                try {
+                    r = inflater.inflate(b, p, q);
+                }
+                catch (DataFormatException ex) {
+                    throw new CabException("data format error", ex);
+                }
+                if (r < 0)
+                    throw new CabException("illegal state");
+                if (r == 0) {
+                    if (inflater.needsDictionary())
+                        throw new CabException("change dictionary not supported");
+                    if (inflater.finished())
+                        if (inflater.getTotalOut() == BLOCK_SIZE)
+                            refill();
+                        else
+                            break;
+                    if (inflater.needsInput())
+                        refill();
+                }
+                p += r;
+                q -= r;
+            }
+            assert q >= 0;
+            return len - q;
+        }
+
+        void refill() throws IOException {
+            byte[] tmp = new byte[BLOCK_SIZE];
+            final int r = readNextBlock(tmp, 0);
+            if (r < 1)
+                throw new CabException("illegal state");
+            final int remaining = inflater.getRemaining();
+            byte[] newBuffer = new byte[remaining + r];
+            if (remaining > 0) {
+                assert inflaterBuffer != null;
+                final int offset = inflaterBuffer.length - remaining;
+                System.arraycopy(inflaterBuffer, offset, newBuffer, 0, remaining);
+            }
+            System.arraycopy(tmp, 0, newBuffer, remaining, r);
+            inflater.reset();
+            inflater.setInput(newBuffer);
+            inflaterBuffer = newBuffer;
+        }
+
+        @SuppressWarnings("unused")
+        int readNextBlock(byte[] bytes, int offset) throws IOException {
+            // CFDATA header
+            final int csum; // checksum of this CFDATA entry
+            final short cbData; // number of compressed bytes in this block
+            final short cbUncomp; // number of uncompressed bytes in this block
+            final byte[] abReserve; // (optional) per-datablock reserved area
+            // ---
+            ByteBuffer buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);
+            buffer.clear();
+            Channels.newChannel(in).read(buffer);
+            if (buffer.position() == 0)
+                return -1; // EOF?
+            if (buffer.position() != 8)
+                throw new CabException("reading CFDATA header error: %d", buffer.position());
+            buffer.rewind();
+            csum = buffer.getInt();
+            cbData = buffer.getShort();
+            cbUncomp = buffer.getShort();
+            byte[] tmp = new byte[cbData];
+            final int r = IOUtilities.read(in, tmp, 0, cbData);
+            if (r != cbData)
+                throw new CabException("failed to inflate (fill)");
+            // skip CK
+            if (tmp[0] != 'C' || tmp[1] != 'K')
+                throw new CabException("bad CFDATA block (%02X%02X)", tmp[0], tmp[1]);
+            System.arraycopy(tmp, 2, bytes, offset, r - 2);
+            return r - 2;
+        }
+
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/cab/CabOutputStream.java b/src/jp/sfjp/armadillo/archive/cab/CabOutputStream.java
new file mode 100644 (file)
index 0000000..504f2b8
--- /dev/null
@@ -0,0 +1,91 @@
+package jp.sfjp.armadillo.archive.cab;
+
+import java.io.*;
+import java.util.*;
+import jp.sfjp.armadillo.archive.*;
+
+public final class CabOutputStream extends ArchiveOutputStream {
+
+    private final CabHeader header;
+    private final List<CabCfFolder> folders;
+    private CabCfFolder currentFolder;
+
+    public CabOutputStream(OutputStream out) {
+        super(out);
+        this.header = new CabHeader();
+        this.folders = new ArrayList<CabCfFolder>();
+        this.currentFolder = new CabCfFolder("*");
+    }
+
+    public void putNextEntry(CabEntry entry) throws IOException {
+        ensureOpen();
+        final String name = entry.getName();
+        if (name.length() > 255)
+            throw new IllegalArgumentException("too long name: " + name);
+        if (entry.isDirectory()) {
+            CabCfFolder folder = (CabCfFolder)entry;
+            if (folders.isEmpty() || folder.method != currentFolder.method)
+                currentFolder.method = folder.method;
+        }
+        else {
+            if (folders.isEmpty())
+                changeFolder(getParentPath(entry));
+            CabCfFile file = (CabCfFile)entry;
+            final int folderCount = folders.size() - 1;
+            assert folderCount <= Short.MAX_VALUE;
+            file.folderIndex = (short)folderCount;
+            file.uncompressedOffset = currentFolder.offset;
+            currentFolder.offset += file.uncompressedSize;
+            currentFolder.add(file);
+        }
+    }
+
+    void changeFolder(String name) throws IOException {
+        currentFolder.close();
+        CabCfFolder folder = new CabCfFolder(normalizePath(name, true));
+        folder.method = 1;
+        folders.add(folder);
+        currentFolder = folder;
+        frontStream = currentFolder.bos;
+    }
+
+    void closeFolder(CabCfFolder folder) throws IOException {
+        ensureOpen();
+        flush();
+        folder.close();
+        frontStream = out;
+    }
+
+    String getParentPath(CabEntry entry) {
+        final String path = normalizePath(entry.getName(), false);
+        return path.replaceFirst("/[^/]+$", "/");
+    }
+
+    static String normalizePath(String path, boolean isDirectory) {
+        String s = path.replace('\\', '/');
+        if (isDirectory && !s.endsWith("/"))
+            s += "/";
+        return s;
+    }
+
+    public void closeEntry() throws IOException {
+        // do nothing
+    }
+
+    @Override
+    public void close() throws IOException {
+        try {
+            currentFolder.close();
+            header.write(out, folders);
+            for (CabCfFolder folder : folders)
+                folder.writeCfDataInto(out, header);
+            out.flush();
+            flush();
+            folders.clear();
+        }
+        finally {
+            super.close();
+        }
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/cab/DumpCabHeader.java b/src/jp/sfjp/armadillo/archive/cab/DumpCabHeader.java
new file mode 100644 (file)
index 0000000..1511d74
--- /dev/null
@@ -0,0 +1,208 @@
+package jp.sfjp.armadillo.archive.cab;
+
+import java.io.*;
+import java.nio.*;
+import java.nio.channels.*;
+import java.util.*;
+import jp.sfjp.armadillo.archive.*;
+import jp.sfjp.armadillo.time.*;
+
+/**
+ * Dump CAB archive header.
+ */
+public final class DumpCabHeader extends DumpArchiveHeader {
+
+    // signature 'MSCF'
+    static final int SIGNATURE = 0x4D534346;
+
+    private static final FTime FTIME = new FTime();
+    private static final String fmt0 = "  -- %s[%d] --%n";
+    private static final String fmt1 = "  * %s = %s%n";
+    private static final String fmt2 = "  %1$-16s = [0x%2$08X] ( %2$d )%n";
+
+    private static final int BUFFER_SIZE = 1024;
+
+    private final ByteBuffer buffer;
+    private short[] folderCompressType;
+
+    public DumpCabHeader() {
+        this.buffer = ByteBuffer.allocate(BUFFER_SIZE).order(ByteOrder.LITTLE_ENDIAN);
+    }
+
+    @SuppressWarnings("unused")
+    @Override
+    public void dump(InputStream is, PrintWriter out) throws IOException {
+        // Cabinet File header
+        final int signature; // cabinet file signature
+        final int reserved1; // reserved
+        final int cbCabinet; // size of this cabinet file in bytes
+        final int reserved2; // reserved
+        final int coffFiles; // offset of the first CFFILE entry
+        final int reserved3; // reserved
+        final byte versionMinor; // cabinet file format version, minor
+        final byte versionMajor; // cabinet file format version, major
+        final short cFolders; // number of CFFOLDER entries in this cabinet
+        final short cFiles; // number of CFFILE entries in this cabinet
+        final short flags; // cabinet file option indicators
+        final short setID; // must be the same for all cabinets in a set
+        final short iCabinet;// number of this cabinet file in a set
+        buffer.clear();
+        buffer.limit(36);
+        Channels.newChannel(is).read(buffer);
+        if (buffer.position() == 0)
+            throw new CabException("reading archive header, but position is zero");
+        buffer.rewind();
+        buffer.order(ByteOrder.BIG_ENDIAN);
+        signature = buffer.getInt();
+        if (signature != SIGNATURE)
+            throw new CabException("bad signature: 0x%X", signature);
+        buffer.order(ByteOrder.LITTLE_ENDIAN);
+        reserved1 = buffer.getInt();
+        cbCabinet = buffer.getInt();
+        reserved2 = buffer.getInt();
+        coffFiles = buffer.getInt();
+        reserved3 = buffer.getInt();
+        versionMinor = buffer.get();
+        versionMajor = buffer.get();
+        cFolders = buffer.getShort();
+        cFiles = buffer.getShort();
+        flags = buffer.getShort();
+        setID = buffer.getShort();
+        iCabinet = buffer.getShort();
+        printHeaderName(out, "Cabinet File header");
+        out.printf(fmt2, "signature", signature);
+        out.printf(fmt2, "reserved1", reserved1);
+        out.printf(fmt2, "cbCabinet", cbCabinet);
+        out.printf(fmt2, "reserved2", reserved2);
+        out.printf(fmt2, "coffFiles", coffFiles);
+        out.printf(fmt2, "reserved3", reserved3);
+        out.printf(fmt2, "versionMinor", versionMinor);
+        out.printf(fmt2, "versionMajor", versionMajor);
+        out.printf(fmt2, "cFolders", cFolders);
+        out.printf(fmt2, "cFiles", cFiles);
+        out.printf(fmt2, "flags", flags);
+        out.printf(fmt2, "setID", setID);
+        out.printf(fmt2, "iCabinet", iCabinet);
+        if (iCabinet != 0) {
+            warn(out, "iCabinet not supported: 0x%d", iCabinet);
+            return;
+        }
+        // array size of folderCompressType
+        folderCompressType = new short[cFolders];
+        // read CFFOLDERs
+        printHeaderName(out, "CFFOLDER headers");
+        int cfdatacount = 0;
+        for (int i = 0; i < cFolders; i++) {
+            // CFFOLDER header
+            final int coffCabStart; // offset of the first CFDATA block in this folder
+            final short cCFData; // number of CFDATA blocks in this folder
+            final short typeCompress; // compression type indicator
+            final byte[] abReserve; // (optional) per-folder reserved area
+            buffer.clear();
+            buffer.limit(8);
+            Channels.newChannel(is).read(buffer);
+            if (buffer.position() == 0) {
+                warn(out, "reading CFFOLDER header, but position is zero");
+                return;
+            }
+            buffer.rewind();
+            coffCabStart = buffer.getInt();
+            cCFData = buffer.getShort();
+            typeCompress = buffer.getShort();
+            // ignore optional data
+            abReserve = new byte[0];
+            folderCompressType[i] = typeCompress;
+            out.printf(fmt0, "CFFOLDER", i);
+            out.printf(fmt2, "coffCabStart", coffCabStart);
+            out.printf(fmt2, "cCFData", cCFData);
+            out.printf(fmt2, "typeCompress", typeCompress);
+            //            out.printf(fmt2, "abReserve", abReserve);
+            cfdatacount += cCFData;
+        }
+        // read CFFILEs
+        printHeaderName(out, "CFFILE headers");
+        for (int i = 0; i < cFiles; i++) {
+            // CFFILE header
+            final int cbFile; // uncompressed size of this file in bytes
+            final int uoffFolderStart; // uncompressed offset of this file in the folder
+            final short iFolder; // index into the CFFOLDER area
+            final short date; // date stamp for this file
+            final short time; // time stamp for this file
+            final short attribs; // attribute flags for this file
+            final byte[] szName; // name of this file
+            buffer.clear();
+            buffer.limit(16);
+            Channels.newChannel(is).read(buffer);
+            if (buffer.position() == 0)
+                throw new CabException("reading CFFILE header, but position is zero");
+            buffer.rewind();
+            cbFile = buffer.getInt();
+            uoffFolderStart = buffer.getInt();
+            iFolder = buffer.getShort();
+            date = buffer.getShort();
+            time = buffer.getShort();
+            attribs = buffer.getShort();
+            szName = readName(is, 256);
+            final String name = new String(szName);
+            out.printf(fmt0, "CFFILE", i);
+            out.printf(fmt1, "name", name);
+            out.printf(fmt1, "mtime as date", toDate(date, time));
+            out.printf(fmt2, "cbFile", cbFile);
+            out.printf(fmt2, "uoffFolderStart", uoffFolderStart);
+            out.printf(fmt2, "iFolder", iFolder);
+            out.printf(fmt2, "date", date);
+            out.printf(fmt2, "time", time);
+            out.printf(fmt2, "attribs", attribs);
+            out.printf(fmt2, "szName.length", szName.length);
+        }
+        // read CFDATAs
+        printHeaderName(out, "CFDATA headers");
+        for (int i = 0; i < cfdatacount; i++) {
+            // CFDATA header
+            final int csum; // checksum of this CFDATA entry
+            final short cbData; // number of compressed bytes in this block
+            final short cbUncomp; // number of uncompressed bytes in this block
+            final byte[] abReserve; // (optional) per-datablock reserved area
+            final byte[] ab; // compressed data bytes
+            buffer.clear();
+            buffer.limit(8);
+            Channels.newChannel(is).read(buffer);
+            if (buffer.position() < 8)
+                break;
+            buffer.rewind();
+            csum = buffer.getInt();
+            cbData = buffer.getShort();
+            cbUncomp = buffer.getShort();
+            out.printf(fmt0, "CFDATA", i);
+            out.printf(fmt2, "csum", csum);
+            out.printf(fmt2, "cbData", cbData);
+            out.printf(fmt2, "cbUncomp", cbUncomp);
+            int skipSize = cbData;
+            while (skipSize > 0)
+                skipSize -= is.skip(skipSize);
+        }
+        printEnd(out, "CAB", 0);
+    }
+
+    static Date toDate(int mdate, int mtime) {
+        final int ftime = (mdate << 16) | mtime & 0xFFFF;
+        return new Date(FTIME.toMilliseconds(ftime));
+    }
+
+    private byte[] readName(InputStream is, int limit) throws IOException {
+        byte[] bytes = new byte[limit];
+        int readSize = 0;
+        for (int i = 0; i < limit; i++) {
+            int b = is.read();
+            if (b <= 0)
+                break;
+            bytes[i] = (byte)(b & 0xFF);
+            ++readSize;
+        }
+        if (readSize < limit)
+            return Arrays.copyOf(bytes, readSize);
+        else
+            return bytes;
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/lzh/DumpLzhHeader.java b/src/jp/sfjp/armadillo/archive/lzh/DumpLzhHeader.java
new file mode 100644 (file)
index 0000000..94702b9
--- /dev/null
@@ -0,0 +1,408 @@
+package jp.sfjp.armadillo.archive.lzh;
+
+import java.io.*;
+import java.nio.*;
+import java.nio.channels.*;
+import java.util.*;
+import jp.sfjp.armadillo.archive.*;
+import jp.sfjp.armadillo.io.*;
+import jp.sfjp.armadillo.time.*;
+
+/**
+ * Dump LZH archive header.
+ */
+public final class DumpLzhHeader extends DumpArchiveHeader {
+
+    private static final TimeT TIME_T = new TimeT();
+    private static final FTime FTIME = new FTime();
+    private static final FileTime FILETIME = new FileTime();
+    private static final int siglen = 5;
+    private static final int siglen_m1 = siglen - 1;
+
+    private static final int USHORT_MASK = 0xFFFF;
+    private static final int UBYTE_MASK = 0xFF;
+
+    private static final int BUFFER_SIZE = 1024;
+    private static final int LEVEL_OFFSET = 20;
+    private static final byte PATH_DELIMITER = (byte)0xFF;
+    private static final String ERROR_PREFIX = "invalid header: ";
+
+    private final ByteBuffer buffer;
+
+    public DumpLzhHeader() {
+        this.buffer = ByteBuffer.allocate(BUFFER_SIZE).order(ByteOrder.LITTLE_ENDIAN);
+    }
+
+    @Override
+    public void dump(InputStream is, PrintWriter out) throws IOException {
+        final int bufferSize = 65536;
+        byte[] bytes = new byte[bufferSize];
+        long p = 0;
+        RewindableInputStream pis = new RewindableInputStream(is, bufferSize);
+        while (true) {
+            Arrays.fill(bytes, (byte)0);
+            final int readSize = pis.read(bytes);
+            if (readSize <= 0)
+                break;
+            final int offset = findHeader(bytes);
+            if (offset < 0) {
+                if (readSize < siglen)
+                    break;
+                pis.rewind(siglen_m1);
+                p += readSize - siglen_m1;
+                continue;
+            }
+            final int head = offset - 2;
+            if (offset == 0 || offset < bufferSize - siglen)
+                pis.rewind(readSize - head);
+            else
+                throw new AssertionError("else");
+            p += head;
+            printOffset(out, p);
+            try {
+                p += dumpHeader(pis, out);
+            }
+            catch (Exception ex) {
+                warn(out, "unexpected error: %s", ex);
+                p += 7;
+            }
+        }
+        printEnd(out, "LZH", p);
+    }
+
+    static int findHeader(byte[] bytes) {
+        // pattern = "-l**-"
+        for (int i = 0; i < bytes.length - siglen_m1; i++)
+            if (bytes[i] == '-' && bytes[i + 1] == 'l' && bytes[i + 4] == '-')
+                return i;
+        return -1;
+    }
+
+    public long dumpHeader(InputStream is, PrintWriter out) throws IOException {
+        buffer.clear();
+        buffer.limit(LEVEL_OFFSET + 1);
+        Channels.newChannel(is).read(buffer);
+        int headerLength0 = buffer.position();
+        if (headerLength0 == 0
+            || (headerLength0 == 1 && buffer.get(0) == 0x00)
+            || (headerLength0 <= LEVEL_OFFSET + 1 && buffer.getShort(0) == 0x00))
+            return -1;
+        if (headerLength0 != LEVEL_OFFSET + 1)
+            return -1; // warn: next header length
+        final int level = buffer.get(LEVEL_OFFSET);
+        buffer.rewind();
+        int headerLength;
+        switch (level) {
+            case 0:
+            case 1:
+                headerLength = (buffer.get() & UBYTE_MASK) + 2;
+                break;
+            case 2:
+                headerLength = buffer.getShort() & USHORT_MASK;
+                break;
+            default:
+                throw new LzhException("unsupported header level (=" + level + ")");
+        }
+        if (headerLength == 0)
+            return -1;
+        assert headerLength >= 0 && headerLength < BUFFER_SIZE : "header length = " + headerLength;
+        buffer.limit(headerLength);
+        buffer.position(LEVEL_OFFSET + 1);
+        Channels.newChannel(is).read(buffer);
+        if (buffer.position() != headerLength)
+            throw new LzhException(ERROR_PREFIX + "header length = " + headerLength);
+        final long readLength;
+        switch (level) {
+            case 0:
+                readLength = dumpLevel0(is, out);
+                break;
+            case 1:
+                readLength = dumpLevel1(is, out);
+                break;
+            case 2:
+                readLength = dumpLevel2(is, out);
+                break;
+            default:
+                throw new IllegalStateException("unexpected state");
+        }
+        return readLength;
+    }
+
+    private long dumpLevel0(InputStream is, PrintWriter out) throws IOException {
+        long readLength = 0;
+        // Header Level 0
+        final byte headerLength; // length of this header
+        final byte checksum; // checksum of header (SUM)
+        final byte[] method; // compression method
+        final int skipSize; // skip size
+        final int size; // uncompressed size
+        final short mtime; // last modified time (MS-DOS time)
+        final short mdate; // last modified date (MS-DOS time)
+        final byte attribute; // file attribute
+        final byte level; // header level
+        final byte nameLength; // length of path name
+        final byte[] name; // path name
+        final short crc; // checksum of file (CRC16)
+        final short extend; // -
+        // ---
+        buffer.position(0);
+        headerLength = buffer.get();
+        checksum = buffer.get();
+        method = getBytes(5);
+        skipSize = buffer.getInt();
+        size = buffer.getInt();
+        mtime = buffer.getShort();
+        mdate = buffer.getShort();
+        attribute = buffer.get();
+        level = buffer.get();
+        nameLength = buffer.get();
+        name = getBytes(nameLength);
+        crc = buffer.getShort();
+        extend = buffer.getShort();
+        printHeaderName(out, "Level 0 Header");
+        p(out, "headerLength", headerLength);
+        p(out, "checksum", checksum);
+        p(out, "method", method);
+        p(out, "skipSize", skipSize);
+        p(out, "size", size);
+        p(out, "mtime", mtime);
+        p(out, "mdate", mdate);
+        p(out, "attribute", attribute);
+        p(out, "level", level);
+        p(out, "nameLength", nameLength);
+        p(out, "name", name);
+        p(out, "crc", crc);
+        p(out, "extend", extend);
+        readLength += buffer.position();
+        if (skipSize > 0)
+            readLength += is.skip(skipSize);
+        return readLength;
+    }
+
+    private long dumpLevel1(InputStream is, PrintWriter out) throws IOException {
+        long readLength = 0;
+        // Header Level 1
+        final byte headerLength; // length of this header
+        final byte checksum; // checksum of header (SUM)
+        final byte[] method; // compression method
+        final int skipSize; // skip size
+        final int size; // uncompressed size
+        final short mtime; // last modified time (MS-DOS time)
+        final short mdate; // last modified date (MS-DOS time)
+        final byte reserved; // reserved
+        final byte level; // header level
+        final byte nameLength; // length of path name
+        final byte[] name; // path name
+        final short crc; // checksum of file (CRC16)
+        final byte osIdentifier; // OS identifier which compressed this
+        final short extendHeaderSize; //
+        // ---
+        buffer.position(0);
+        headerLength = buffer.get();
+        checksum = buffer.get();
+        method = getBytes(5);
+        skipSize = buffer.getInt();
+        size = buffer.getInt();
+        mtime = buffer.getShort();
+        mdate = buffer.getShort();
+        reserved = buffer.get();
+        level = buffer.get();
+        nameLength = buffer.get();
+        name = getBytes(nameLength);
+        crc = buffer.getShort();
+        osIdentifier = buffer.get();
+        extendHeaderSize = buffer.getShort();
+        printHeaderName(out, "Level 1 Header");
+        p(out, "headerLength", headerLength);
+        p(out, "checksum", checksum);
+        p(out, "method", method);
+        p(out, "skipSize", skipSize);
+        p(out, "size", size);
+        p(out, "mtime", mtime);
+        p(out, "mdate", mdate);
+        p(out, "reserved", reserved);
+        p(out, "level", level);
+        p(out, "nameLength", nameLength);
+        p(out, "name", name);
+        p(out, "crc", crc & 0xFFFFFFFFL);
+        p(out, "osIdentifier", osIdentifier);
+        p(out, "extendHeaderSize", extendHeaderSize);
+        // p(out, "extendHeader", extendHeader);
+        readLength += buffer.position();
+        if (skipSize > 0)
+            readLength += is.skip(skipSize);
+        return readLength;
+    }
+
+    private long dumpLevel2(InputStream is, PrintWriter out) throws IOException {
+        long readLength = 0;
+        // Header Level 2
+        final short headerLength; // length of this header
+        final byte[] method; // compression method
+        final int compressedSize; // compressed size
+        final int size; // uncompressed size
+        final int mtime; // last modified timestamp (POSIX time)
+        final byte reserved; // reserved
+        final byte level; // header level
+        final short crc; // checksum of file (CRC16)
+        final byte osIdentifier; // OS identifier which compressed this
+        final short firstExHeaderLength; // length of next (first) extend header
+        // ---
+        buffer.position(0);
+        headerLength = buffer.getShort();
+        method = getBytes(5);
+        compressedSize = buffer.getInt();
+        size = buffer.getInt();
+        mtime = buffer.getInt();
+        reserved = buffer.get();
+        level = buffer.get();
+        crc = buffer.getShort();
+        osIdentifier = buffer.get();
+        firstExHeaderLength = buffer.getShort();
+        final String methodName = new String(method);
+        ByteArrayOutputStream x01 = new ByteArrayOutputStream();
+        ByteArrayOutputStream x02 = new ByteArrayOutputStream();
+        List<String> a = new ArrayList<String>();
+        int p = 0;
+        short nextHeaderLength = firstExHeaderLength;
+        while (nextHeaderLength > 0) {
+            final byte identifier = buffer.get();
+            final int length = nextHeaderLength - 3; // datalength = len - 1(id) - 2(nextlen)
+            buffer.mark();
+            a.add(String.format("ex: id=%02X, len=%d", identifier, length));
+            switch (identifier) {
+                case 0x00: // common
+                    final long hcrc = buffer.getShort();
+                    a.add(String.format("  hCRC=0x%04X", hcrc & USHORT_MASK));
+                    try {
+                        for (int i = 0, n = length - 2; i < n; i++)
+                            a.add(String.format("  [%d]=%02X", i, buffer.get() & UBYTE_MASK));
+                    }
+                    catch (Exception ex) {
+                        warn(out, ">>> %s", ex);
+                        return buffer.position();
+                    }
+                    break;
+                case 0x01: // file name
+                    x01.write(nextPath(length));
+                    break;
+                case 0x02: // dir name
+                    x02.write(nextPath(length));
+                    break;
+                case 0x41: // MS Windows timestamp
+                    final long wctime = buffer.getLong();
+                    final long wmtime = buffer.getLong();
+                    final long watime = buffer.getLong();
+                    Date wcd = new Date(FILETIME.toMilliseconds(wctime));
+                    Date wmd = new Date(FILETIME.toMilliseconds(wmtime));
+                    Date wad = new Date(FILETIME.toMilliseconds(watime));
+                    a.add(String.format("  Windows ctime = %s (%d)", wcd, wctime));
+                    a.add(String.format("  Windows mtime = %s (%d)", wmd, wmtime));
+                    a.add(String.format("  Windows atime = %s (%d)", wad, watime));
+                    break;
+                case 0x42: // MS filesize
+                    a.add("  MS compsize = " + buffer.getLong());
+                    a.add("  MS filesize = " + buffer.getLong());
+                    break;
+                case 0x54: // UNIX time_t
+                    final long umtime = buffer.getLong();
+                    Date umd = new Date(FILETIME.toMilliseconds(umtime));
+                    a.add(String.format("  UNIX mtime = %s (%d)", umd, umtime));
+                    break;
+                default:
+                    for (int i = 0; i < length; i++)
+                        a.add(String.format("  [%d]=%02X", i, buffer.get() & UBYTE_MASK));
+            }
+            buffer.reset();
+            buffer.position(buffer.position() + length);
+            nextHeaderLength = buffer.getShort();
+            p = buffer.position();
+        }
+        final int headerLength0 = p;
+        ByteArrayOutputStream nameb = new ByteArrayOutputStream();
+        if (x02.size() > 0) {
+            nameb.write(x02.toByteArray());
+            if (!nameb.toString().endsWith("/"))
+                nameb.write('/');
+        }
+        if (x01.size() > 0)
+            nameb.write(x01.toByteArray());
+        printHeaderName(out, "Level 2 Header");
+        p(out, "name", nameb.toByteArray());
+        p(out, "mtime as Date", new Date(TIME_T.toMilliseconds(mtime)));
+        p(out, "method", methodName);
+        p(out, "headerLength", headerLength);
+        p(out, "compressedSize", compressedSize);
+        p(out, "size", size);
+        p(out, "mtime", mtime);
+        p(out, "reserved", reserved);
+        p(out, "level", level);
+        p(out, "crc", crc);
+        p(out, "osIdentifier", (char)osIdentifier);
+        p(out, "firstExHdrLen", firstExHeaderLength);
+        for (final String x : a)
+            p(out, "  ", x);
+        if (headerLength != headerLength0)
+            warn(out, "header length is expected %d , but was %d%n", headerLength, p);
+        readLength += headerLength0;
+        if (compressedSize > 0)
+            readLength += is.skip(compressedSize);
+        return readLength;
+    }
+
+    private void p(PrintWriter out, String name, Object value) {
+        if (value instanceof Character) {
+            final char c = ((Character)value).charValue();
+            out.printf("  %-16s [%04X] ('%s')%n", name, (int)c, value);
+        }
+        else if (value instanceof Number) {
+            final long v;
+            final int w;
+            if (value instanceof Byte) {
+                v = ((Byte)value) & 0xFF;
+                w = 2;
+            }
+            else if (value instanceof Short) {
+                v = ((Short)value) & 0xFFFF;
+                w = 4;
+            }
+            else if (value instanceof Integer) {
+                v = ((Integer)value) & 0xFFFFFFFFL;
+                w = 8;
+            }
+            else {
+                v = ((Number)value).longValue();
+                w = 1;
+            }
+            out.printf("  %-16s [%0" + w + "X] (%d)%n", name, value, v);
+        }
+        else if (value instanceof byte[])
+            out.printf("  * %s = %s%n", name, new String((byte[])value));
+        else if (value instanceof Date)
+            out.printf("  * %s = %s%n", name, value);
+        else
+            out.printf("  %s %s%n", name, value);
+    }
+
+    private byte[] getBytes(int length) {
+        byte[] bytes = new byte[length];
+        for (int i = 0; i < bytes.length; i++)
+            bytes[i] = buffer.get();
+        return bytes;
+    }
+
+    private byte[] nextPath(int length) {
+        byte[] bytes = new byte[length];
+        buffer.get(bytes);
+        for (int i = 0; i < length; i++)
+            if ((bytes[i] & PATH_DELIMITER) == PATH_DELIMITER)
+                bytes[i] = '/';
+        return bytes;
+    }
+
+    static Date toDate(int mdate, int mtime) {
+        final int ftime = (mdate << 16) | mtime & 0xFFFF;
+        return new Date(FTIME.toMilliseconds(ftime));
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/lzh/LzhArchiveCreator.java b/src/jp/sfjp/armadillo/archive/lzh/LzhArchiveCreator.java
new file mode 100644 (file)
index 0000000..bd1807d
--- /dev/null
@@ -0,0 +1,73 @@
+package jp.sfjp.armadillo.archive.lzh;
+
+import static jp.sfjp.armadillo.archive.lzh.LzhHeader.HEADER_LEVEL_2;
+import java.io.*;
+import jp.sfjp.armadillo.archive.*;
+import jp.sfjp.armadillo.io.*;
+
+public final class LzhArchiveCreator implements ArchiveCreator {
+
+    private LzhOutputStream os;
+
+    public LzhArchiveCreator(OutputStream os) {
+        this.os = new LzhOutputStream(os);
+    }
+
+    @Override
+    public ArchiveEntry newEntry(String name) {
+        LzhEntry entry = newLzhEntry();
+        entry.setName(name);
+        return entry;
+    }
+
+    @Override
+    public void addEntry(ArchiveEntry entry, File file) throws IOException {
+        if (file.isDirectory()) {
+            final LzhEntry en = toLzhEntry(entry);
+            en.setMethod(LzhMethod.LHD);
+            os.putNextEntry(en);
+            os.closeEntry();
+        }
+        else {
+            InputStream is = new FileInputStream(file);
+            try {
+                addEntry(entry, is, file.length());
+            }
+            finally {
+                is.close();
+            }
+        }
+        entry.setAdded(true);
+    }
+
+    @Override
+    public void addEntry(ArchiveEntry entry, InputStream is, long length) throws IOException {
+        LzhEntry fileEntry = toLzhEntry(entry);
+        fileEntry.setSize(length);
+        os.putNextEntry(fileEntry);
+        final long size = IOUtilities.transferAll(is, os);
+        os.closeEntry();
+        assert size == fileEntry.getSize() : "file size";
+        assert size == length : "file size";
+        entry.setSize(size);
+        entry.setAdded(true);
+    }
+
+    static LzhEntry toLzhEntry(ArchiveEntry entry) {
+        if (entry instanceof LzhEntry)
+            return (LzhEntry)entry;
+        LzhEntry newEntry = newLzhEntry();
+        entry.copyTo(newEntry);
+        return newEntry;
+    }
+
+    private static LzhEntry newLzhEntry() {
+        return new LzhEntry(HEADER_LEVEL_2);
+    }
+
+    @Override
+    public void close() throws IOException {
+        os.close();
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/lzh/LzhArchiveExtractor.java b/src/jp/sfjp/armadillo/archive/lzh/LzhArchiveExtractor.java
new file mode 100644 (file)
index 0000000..16dee0f
--- /dev/null
@@ -0,0 +1,30 @@
+package jp.sfjp.armadillo.archive.lzh;
+
+import java.io.*;
+import jp.sfjp.armadillo.archive.*;
+import jp.sfjp.armadillo.io.*;
+
+public final class LzhArchiveExtractor implements ArchiveExtractor {
+
+    private LzhInputStream is;
+
+    public LzhArchiveExtractor(InputStream is) {
+        this.is = new LzhInputStream(is);
+    }
+
+    @Override
+    public ArchiveEntry nextEntry() throws IOException {
+        return ArchiveEntry.orNull(is.getNextEntry());
+    }
+
+    @Override
+    public long extract(OutputStream os) throws IOException {
+        return IOUtilities.transferAll(is, os);
+    }
+
+    @Override
+    public void close() throws IOException {
+        is.close();
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/lzh/LzhChecksum.java b/src/jp/sfjp/armadillo/archive/lzh/LzhChecksum.java
new file mode 100644 (file)
index 0000000..36cbfa2
--- /dev/null
@@ -0,0 +1,76 @@
+package jp.sfjp.armadillo.archive.lzh;
+
+import java.util.zip.*;
+
+/**
+ * Checksum(CRC16) for LZH.
+ */
+public final class LzhChecksum implements Checksum {
+
+    private final int[] table;
+    private final int initial;
+
+    private int value;
+
+    public LzhChecksum() {
+        this(0xA001, 0);
+    }
+
+    LzhChecksum(int exp, int initial) {
+        this.table = create(exp);
+        this.initial = initial;
+        this.value = initial;
+    }
+
+    /**
+     * Creates table.
+     * @param exp
+     * @return 
+     */
+    static int[] create(int exp) {
+        int[] table = new int[256];
+        for (int i = 0; i < table.length; i++) {
+            table[i] = i;
+            for (int j = 0; j < 8; j++) {
+                if ((table[i] & 0x01) != 0) {
+                    table[i] = (table[i] >> 1) ^ exp;
+                }
+                else {
+                    table[i] >>= 1;
+                }
+            }
+        }
+        return table;
+    }
+
+    public short getShortValue() {
+        return (short)(value & 0xFFFF);
+    }
+
+    @Override
+    public long getValue() {
+        return value;
+    }
+
+    @Override
+    public void reset() {
+        value = initial;
+    }
+
+    @Override
+    public void update(int b) {
+        int value = this.value;
+        value = (value >> 8) ^ table[(value ^ b) & 0xFF];
+        this.value = value;
+    }
+
+    @Override
+    public void update(byte[] bytes, int offset, int length) {
+        int value = this.value;
+        for (int i = offset; i < offset + length; i++) {
+            value = (value >> 8) ^ table[(value ^ bytes[i]) & 0xFF];
+        }
+        this.value = value;
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/lzh/LzhEntry.java b/src/jp/sfjp/armadillo/archive/lzh/LzhEntry.java
new file mode 100644 (file)
index 0000000..cc96ae6
--- /dev/null
@@ -0,0 +1,138 @@
+package jp.sfjp.armadillo.archive.lzh;
+
+import jp.sfjp.armadillo.archive.*;
+import jp.sfjp.armadillo.time.*;
+
+/**
+ * LZH archive entry.
+ */
+public final class LzhEntry extends ArchiveEntry {
+
+    private static final TimeT TIME_T = new TimeT();
+    private static final FTime FTIME = new FTime();
+    private static final FileTime FILETIME = new FileTime();
+
+    int headerLength;
+    String method;
+    long compressedSize;
+    long size;
+    private int ftime;
+    private int timeT;
+    private long fileTime;
+    byte type;
+    final byte headerLevel;
+    short crc;
+    int checksum;
+    int calculatedChecksum;
+    byte attribute;
+    byte osIdentifier;
+
+    public LzhEntry(int headerLevel) {
+        super(false);
+        this.headerLevel = (byte)headerLevel;
+        this.method = LzhMethod.LH5;
+        this.osIdentifier = 'J'; // not an official ?
+    }
+
+    @Override
+    public boolean isDirectory() {
+        return LzhMethod.LHD.equals(method);
+    }
+
+    public LzhMethod getMethod() throws LzhException {
+        if (method == null)
+            throw new IllegalStateException("method is null");
+        return new LzhMethod(method);
+    }
+
+    public void setMethod(String method) {
+        this.method = method;
+    }
+
+    @Override
+    public long getSize() {
+        return size;
+    }
+
+    @Override
+    public void setSize(long size) {
+        this.size = size;
+    }
+
+    @Override
+    public long getCompressedSize() {
+        return compressedSize;
+    }
+
+    @Override
+    public void setCompressedSize(long compressedSize) {
+        this.compressedSize = compressedSize;
+    }
+
+    @Override
+    public long getLastModified() {
+        TimeConverter tc = getTimeConverter(headerLevel);
+        if (tc == FTIME)
+            return FTIME.toMilliseconds(ftime);
+        else if (tc == FILETIME)
+            return tc.toMilliseconds(fileTime);
+        else
+            return tc.toMilliseconds(timeT);
+    }
+
+    @Override
+    public void setLastModified(long time) {
+        TimeConverter tc = getTimeConverter(headerLevel);
+        if (tc == FTIME)
+            ftime = FTIME.int32From(time);
+        else if (tc == FILETIME)
+            fileTime = tc.int64From(time);
+        else
+            timeT = tc.int32From(time);
+    }
+
+    @Override
+    public String getMethodName() {
+        return method;
+    }
+
+    private static TimeConverter getTimeConverter(int level) {
+        switch (level) {
+            case 0:
+            case 1:
+                return FTIME;
+            case 2:
+                return FILETIME;
+            default:
+                return TIME_T;
+        }
+    }
+
+    int getFTime() {
+        return FTIME.int32From(getLastModified());
+    }
+
+    void setFtime(short mdate, short mtime) {
+        this.ftime = (mdate << 16) | mtime & 0xFFFF;
+        setLastModified(FTIME.toMilliseconds(ftime));
+    }
+
+    long getFileTime() {
+        return FILETIME.int64From(getLastModified());
+    }
+
+    void setFileTime(long filetime) {
+        this.fileTime = filetime;
+        setLastModified(FILETIME.toMilliseconds(filetime));
+    }
+
+    int getTimeT() {
+        return TIME_T.int32From(getLastModified());
+    }
+
+    void setTimeT(int timet) {
+        this.timeT = timet;
+        setLastModified(TIME_T.toMilliseconds(timet));
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/lzh/LzhException.java b/src/jp/sfjp/armadillo/archive/lzh/LzhException.java
new file mode 100644 (file)
index 0000000..53c6b52
--- /dev/null
@@ -0,0 +1,16 @@
+package jp.sfjp.armadillo.archive.lzh;
+
+import java.io.*;
+
+final class LzhException extends IOException {
+
+    public LzhException(String s) {
+        super(s);
+    }
+
+    public LzhException(String s, Throwable th) {
+        super(s);
+        initCause(th);
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/lzh/LzhFile.java b/src/jp/sfjp/armadillo/archive/lzh/LzhFile.java
new file mode 100644 (file)
index 0000000..d0bb364
--- /dev/null
@@ -0,0 +1,167 @@
+package jp.sfjp.armadillo.archive.lzh;
+
+import java.io.*;
+import java.nio.channels.*;
+import jp.sfjp.armadillo.archive.*;
+import jp.sfjp.armadillo.io.*;
+
+public final class LzhFile extends ArchiveFile {
+
+    private File afile;
+    private RandomAccessFile raf;
+    private LzhHeader header;
+    private LzhEntry ongoingEntry;
+    private final LzhChecksum cksum;
+    private final byte[] buffer;
+
+    public LzhFile(File afile) {
+        this.afile = afile;
+        this.header = new LzhHeader();
+        this.cksum = new LzhChecksum();
+        this.buffer = new byte[8192];
+    }
+
+    public LzhFile(File afile, boolean withOpen) throws IOException {
+        this(afile);
+        open();
+    }
+
+    @Override
+    public ArchiveEntry newEntry(String name) {
+        LzhEntry entry = new LzhEntry(LzhHeader.HEADER_LEVEL_2);
+        entry.setName(name);
+        if (name.endsWith("/"))
+            entry.method = LzhMethod.LHD;
+        return entry;
+    }
+
+    @Override
+    public void open() throws IOException {
+        if (raf != null)
+            throw new IOException("the file has been already opened");
+        if (!afile.exists())
+            afile.createNewFile();
+        this.raf = new RandomAccessFile(afile, "rw");
+        this.opened = true;
+    }
+
+    @Override
+    public void reset() throws IOException {
+        ongoingEntry = null;
+        currentPosition = 0L;
+        raf.seek(0L);
+    }
+
+    @Override
+    public ArchiveEntry nextEntry() throws IOException {
+        ensureOpen();
+        if (ongoingEntry == null)
+            currentPosition = 0L;
+        else
+            currentPosition += ongoingEntry.compressedSize;
+        raf.seek(currentPosition);
+        ongoingEntry = readCurrentEntry();
+        currentPosition = raf.getFilePointer();
+        return ongoingEntry;
+    }
+
+    @Override
+    public void addEntry(ArchiveEntry entry, InputStream is, long length) throws IOException {
+        final long p;
+        if (raf.length() == 0)
+            p = 0;
+        else
+            raf.seek(p = raf.length() - 1);
+        OutputStream os = Channels.newOutputStream(raf.getChannel());
+        LzhEntry newEntry = LzhArchiveCreator.toLzhEntry(entry);
+        header.write(os, newEntry);
+        if (length > 0) {
+            final long p1 = raf.getFilePointer();
+            writeData(newEntry, is, os, length);
+            raf.seek(p);
+            header.write(os, newEntry);
+            assert raf.getFilePointer() == p1;
+            raf.seek(p1 + newEntry.compressedSize);
+        }
+        raf.write(0);
+        raf.seek(p);
+        currentPosition = p;
+    }
+
+    private void writeData(LzhEntry entry, InputStream is, OutputStream os, long length) throws LzhException, IOException {
+        final long p = raf.getFilePointer();
+        cksum.reset();
+        // XXX better way to flush remaining bits
+        OutputStream los = LzhOutputStream.openStream(new AntiCloseOutputStream(os),
+                                                      entry.getMethod());
+        OutputStream ios = new InspectionOutputStream(los, cksum);
+        final long written = IOUtilities.transfer(buffer, is, ios, length);
+        assert written == length;
+        los.close();
+        entry.compressedSize = raf.getFilePointer() - p;
+        entry.crc = cksum.getShortValue();
+    }
+
+    @Override
+    public void updateEntry(ArchiveEntry entry, InputStream is, long length) throws IOException {
+        removeEntry(entry);
+        addEntry(entry, is, length);
+    }
+
+    @Override
+    public void removeEntry(ArchiveEntry entry) throws IOException {
+        if (!seek(entry))
+            throw new LzhException("entry " + entry + " not found");
+        assert ongoingEntry != null;
+        final int headerLength = ongoingEntry.headerLength;
+        final long bodyLength = ongoingEntry.compressedSize;
+        truncate(currentPosition - headerLength, headerLength + bodyLength);
+    }
+
+    LzhEntry readCurrentEntry() throws IOException {
+        return header.read(Channels.newInputStream(raf.getChannel()));
+    }
+
+    void truncate(final long offset, final long length) throws IOException {
+        long p2 = offset;
+        long p1 = p2 + length;
+        while (true) {
+            raf.seek(p1);
+            final int r = raf.read(buffer);
+            if (r <= 0)
+                break;
+            raf.seek(p2);
+            raf.write(buffer, 0, r);
+            p2 += r;
+            p1 += r;
+        }
+        raf.setLength(raf.length() - length);
+        reset();
+    }
+
+    @Override
+    public void close() throws IOException {
+        try {
+            raf.close();
+        }
+        finally {
+            super.close();
+            afile = null;
+            header = null;
+        }
+    }
+
+    private static final class AntiCloseOutputStream extends BufferedOutputStream {
+
+        public AntiCloseOutputStream(OutputStream os) {
+            super(os, 32768);
+        }
+
+        @Override
+        public void close() throws IOException {
+            // ignore
+        }
+
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/lzh/LzhHeader.java b/src/jp/sfjp/armadillo/archive/lzh/LzhHeader.java
new file mode 100644 (file)
index 0000000..2b82263
--- /dev/null
@@ -0,0 +1,599 @@
+package jp.sfjp.armadillo.archive.lzh;
+
+import java.io.*;
+import java.nio.*;
+import java.nio.channels.*;
+import java.util.*;
+
+/**
+ * LZH archive header.
+ * This class supports header level 0, 1, 2.
+ */
+public final class LzhHeader {
+
+    /**
+     * <code>Header level 0.</code>
+     */
+    public static final byte HEADER_LEVEL_0 = 0;
+    /**
+     * <code>Header level 1.</code>
+     */
+    public static final byte HEADER_LEVEL_1 = 1;
+    /**
+     * <code>Header level 2.</code>
+     */
+    public static final byte HEADER_LEVEL_2 = 2;
+
+    private static final int BUFFER_SIZE = 1024;
+    private static final long MAX_DATA_SIZE = 0xFFFFFFFFL;
+    private static final long UINT_MASK = 0xFFFFFFFFL;
+    private static final int USHORT_MASK = 0xFFFF;
+    private static final int UBYTE_MASK = 0xFF;
+    private static final int LEVEL_OFFSET = 20;
+    private static final byte FILETYPE_FILE = 0x20;
+    private static final byte PLATFORM_JAVA = 'J';
+    private static final byte PATH_DELIMITER = (byte)0xFF;
+    private static final String ISO_8859_1 = "iso-8859-1";
+    private static final String ERROR_PREFIX = "invalid header: ";
+
+    private final ByteBuffer buffer;
+
+    public LzhHeader() {
+        this.buffer = ByteBuffer.allocate(BUFFER_SIZE).order(ByteOrder.LITTLE_ENDIAN);
+    }
+
+    public LzhEntry read(InputStream is) throws IOException {
+        buffer.clear();
+        buffer.limit(LEVEL_OFFSET + 1);
+        Channels.newChannel(is).read(buffer);
+        int readLength = buffer.position();
+        if (readLength == 0
+            || (readLength == 1 && buffer.get(0) == 0x00)
+            || (readLength <= LEVEL_OFFSET + 1 && buffer.getShort(0) == 0x00))
+            return null;
+        if (readLength != LEVEL_OFFSET + 1)
+            return null; // warn: next header length
+        int level = buffer.get(LEVEL_OFFSET);
+        buffer.rewind();
+        int headerLength;
+        switch (level) {
+            case 0:
+            case 1:
+                headerLength = (buffer.get() & UBYTE_MASK) + 2;
+                break;
+            case 2:
+                headerLength = buffer.getShort() & USHORT_MASK;
+                break;
+            default:
+                throw new LzhException("unsupported header level (=" + level + ")");
+        }
+        if (headerLength == 0)
+            return null;
+        assert headerLength >= 0 && headerLength < BUFFER_SIZE : "header length = " + headerLength;
+        buffer.limit(headerLength);
+        buffer.position(LEVEL_OFFSET + 1);
+        Channels.newChannel(is).read(buffer);
+        if (buffer.position() != headerLength)
+            throw new LzhException(ERROR_PREFIX + "header length = " + headerLength);
+        LzhEntry entry;
+        switch (level) {
+            case 0:
+                entry = readLevel0();
+                break;
+            case 1:
+                entry = readLevel1(is);
+                break;
+            case 2:
+                entry = readLevel2();
+                break;
+            default:
+                throw new IllegalStateException("unexpected state");
+        }
+        switch (level) {
+            case 0:
+            case 1:
+                entry.headerLength += 2;
+                break;
+            default:
+        }
+        assert entry.headerLength == headerLength;
+        return entry;
+    }
+
+    @SuppressWarnings("unused")
+    private LzhEntry readLevel0() throws LzhException {
+        // Header Level 0
+        final byte headerLength; // length of this header
+        final byte checksum; // checksum of header (SUM)
+        final byte[] method; // compression method
+        final int skipSize; // compressed size (skip size)
+        final int size; // uncompressed size
+        final short mtime; // last modified time (MS-DOS time)
+        final short mdate; // last modified date (MS-DOS time)
+        final byte attribute; // file attribute
+        final byte level; // header level
+        final byte nameLength; // length of path name
+        final byte[] name; // path name
+        final short crc; // checksum of file (CRC16)
+        final short extend; // -
+        // ---
+        buffer.position(0);
+        headerLength = buffer.get();
+        checksum = buffer.get();
+        method = getBytes(5);
+        skipSize = buffer.getInt();
+        size = buffer.getInt();
+        mtime = buffer.getShort();
+        mdate = buffer.getShort();
+        attribute = buffer.get();
+        level = buffer.get();
+        nameLength = buffer.get();
+        name = getBytes(nameLength);
+        crc = buffer.getShort();
+        assert level == HEADER_LEVEL_0;
+        LzhEntry entry = new LzhEntry(0);
+        entry.headerLength = headerLength;
+        entry.checksum = checksum & UBYTE_MASK;
+        assert (method[0] & 0x7F) == method[0];
+        assert (method[1] & 0x7F) == method[1];
+        assert (method[2] & 0x7F) == method[2];
+        assert (method[3] & 0x7F) == method[3];
+        assert (method[4] & 0x7F) == method[4];
+        entry.method = new String(method);
+        entry.compressedSize = skipSize;
+        entry.size = size;
+        entry.setFtime(mdate, mtime);
+        entry.attribute = attribute;
+        entry.setName(name);
+        entry.crc = crc;
+        entry.calculatedChecksum = calculateChecksum(2, buffer.limit());
+        return entry;
+    }
+
+    private LzhEntry readLevel1(InputStream is) throws IOException {
+        // Header Level 1
+        final byte headerLength; // length of this header
+        final byte checksum; // checksum of header (SUM)
+        final byte[] method; // compression method
+        final int skipSize; // skip size
+        final int size; // uncompressed size
+        final short mtime; // last modified time (MS-DOS time)
+        final short mdate; // last modified date (MS-DOS time)
+        final byte reserved; // reserved
+        final byte level; // header level
+        final byte nameLength; // length of path name
+        final byte[] name; // path name
+        final short crc; // checksum of file (CRC16)
+        final byte osIdentifier; // OS identifier which compressed this
+        // ---
+        buffer.position(0);
+        LzhEntry entry = new LzhEntry(1);
+        headerLength = buffer.get();
+        checksum = buffer.get();
+        method = getBytes(5);
+        skipSize = buffer.getInt();
+        size = buffer.getInt();
+        mtime = buffer.getShort();
+        mdate = buffer.getShort();
+        reserved = buffer.get();
+        level = buffer.get();
+        nameLength = buffer.get();
+        name = getBytes(nameLength);
+        crc = buffer.getShort();
+        osIdentifier = buffer.get();
+        assert level == HEADER_LEVEL_1;
+        assert reserved == FILETYPE_FILE;
+        entry.headerLength = headerLength;
+        entry.checksum = checksum;
+        assert (method[0] & 0x7F) == method[0];
+        assert (method[1] & 0x7F) == method[1];
+        assert (method[2] & 0x7F) == method[2];
+        assert (method[3] & 0x7F) == method[3];
+        assert (method[4] & 0x7F) == method[4];
+        entry.method = new String(method);
+        entry.size = size;
+        entry.setFtime(mdate, mtime);
+        entry.setName(name);
+        entry.crc = crc;
+        entry.osIdentifier = osIdentifier;
+        entry.calculatedChecksum = calculateChecksum(2, buffer.limit());
+        int extendedHeaderSize = 0;
+        entry.compressedSize = skipSize - extendedHeaderSize;
+        return entry;
+    }
+
+    private LzhEntry readLevel2() throws IOException {
+        // Header Level 2
+        final short headerLength; // length of this header
+        final byte[] method; // compression method
+        final int compressedSize; // compressed size
+        final int size; // uncompressed size
+        final int mtime; // last modified timestamp (POSIX time)
+        final byte reserved; // reserved
+        final byte level; // header level
+        final short crc; // checksum of file (CRC16)
+        final byte osIdentifier; // OS identifier which compressed this
+        final short firstExHeaderLength; // length of next (first) extend header
+        // ---
+        buffer.position(0);
+        headerLength = buffer.getShort();
+        method = getBytes(5);
+        compressedSize = buffer.getInt();
+        size = buffer.getInt();
+        mtime = buffer.getInt();
+        reserved = buffer.get();
+        level = buffer.get();
+        crc = buffer.getShort();
+        osIdentifier = buffer.get();
+        firstExHeaderLength = buffer.getShort();
+        assert level == HEADER_LEVEL_2;
+        assert reserved == FILETYPE_FILE;
+        LzhEntry entry = new LzhEntry(2);
+        entry.headerLength = headerLength;
+        assert (method[0] & 0x7F) == method[0];
+        assert (method[1] & 0x7F) == method[1];
+        assert (method[2] & 0x7F) == method[2];
+        assert (method[3] & 0x7F) == method[3];
+        assert (method[4] & 0x7F) == method[4];
+        entry.method = new String(method);
+        entry.compressedSize = compressedSize;
+        entry.size = size;
+        entry.setTimeT(mtime);
+        entry.crc = crc;
+        entry.osIdentifier = osIdentifier;
+        int warningCount = 0;
+        ByteArrayOutputStream x01 = new ByteArrayOutputStream();
+        ByteArrayOutputStream x02 = new ByteArrayOutputStream();
+        short nextHeaderLength = firstExHeaderLength;
+        while (nextHeaderLength > 0) {
+            final byte identifier = buffer.get();
+            final int length = nextHeaderLength - 3; // datalength = len - 1(id) - 2(nextlen)
+            buffer.mark();
+            switch (identifier) {
+                case 0x00: // common
+                    buffer.getShort(); // header CRC
+                    break;
+                case 0x01: // file name
+                    x01.write(nextPath(length));
+                    break;
+                case 0x02: // dir name
+                    x02.write(nextPath(length));
+                    break;
+                case 0x39: // plural disk
+                    ++warningCount;
+                    break;
+                case 0x41: // MS Windows timestamp
+                    buffer.getLong(); // ctime
+                    entry.setFileTime(buffer.getLong());
+                    buffer.getLong(); // atime
+                    break;
+                case 0x42: // MS filesize
+                    entry.compressedSize = buffer.getLong();
+                    entry.size = buffer.getLong();
+                    break;
+                case 0x54: // UNIX time_t
+                    entry.setTimeT(buffer.getInt());
+                    break;
+                case 0x40: // MS attribute
+                case 0x50: // UNIX permission
+                case 0x51: // UNIX uid gid
+                case 0x52: // UNIX group
+                case 0x53: // UNIX user
+                    break;
+                case 0x3F: // comment
+                default:
+                    // ignore
+            }
+            buffer.reset();
+            buffer.position(buffer.position() + length);
+            nextHeaderLength = buffer.getShort();
+        }
+        ByteArrayOutputStream name = new ByteArrayOutputStream();
+        if (x02.size() > 0) {
+            name.write(x02.toByteArray());
+            if (!name.toString().endsWith("/"))
+                name.write('/');
+        }
+        if (x01.size() > 0)
+            name.write(x01.toByteArray());
+        assert (x01.size() == 0) == entry.isDirectory();
+        entry.setName(name.toByteArray());
+        assert warningCount == 0;
+        return entry;
+    }
+
+    public void write(OutputStream os, LzhEntry entry) throws IOException {
+        if (!entry.method.matches("-l[hz][a-z0-9]-"))
+            throw new LzhException("invalid compression type: " + entry.method);
+        if (entry.compressedSize > Integer.MAX_VALUE)
+            throw new LzhException("too large compressed size: " + entry.compressedSize);
+        if (entry.size > Integer.MAX_VALUE)
+            throw new LzhException("too large size: " + entry.size);
+        if (entry.getNameAsBytes().length > Short.MAX_VALUE)
+            throw new LzhException("too long file name: length=" + entry.size);
+        if (entry.isDirectory())
+            entry.method = LzhMethod.LHD;
+        buffer.clear();
+        switch (entry.headerLevel) {
+            case HEADER_LEVEL_0:
+                writeLevel0(entry);
+                break;
+            case HEADER_LEVEL_1:
+                writeLevel1(entry);
+                break;
+            case HEADER_LEVEL_2:
+                writeLevel2(entry);
+                break;
+            default:
+                throw new IllegalArgumentException(ERROR_PREFIX
+                                                   + "header-level="
+                                                   + entry.headerLevel);
+        }
+        buffer.flip();
+        Channels.newChannel(os).write(buffer);
+    }
+
+    @SuppressWarnings("unused")
+    private void writeLevel0(LzhEntry entry) throws IOException {
+        // Header Level 0
+        final byte headerLength; // length of this header
+        final byte checksum; // checksum of header (SUM)
+        final byte[] method; // compression method
+        final int skipSize; // skip size
+        final int size; // uncompressed size
+        final short mtime; // last modified time (MS-DOS time)
+        final short mdate; // last modified date (MS-DOS time)
+        final byte attribute; // file attribute
+        final byte level; // header level
+        final byte nameLength; // length of path name
+        final byte[] name; // path name
+        final short crc; // checksum of file (CRC16)
+        final short extend; // -
+        // ---
+        assert entry.compressedSize >= 0 && entry.compressedSize <= MAX_DATA_SIZE;
+        assert entry.size >= 0 && entry.size <= MAX_DATA_SIZE;
+        assert entry.attribute == FILETYPE_FILE;
+        name = entry.getNameAsBytes();
+        assert name.length <= 0xFF;
+        method = entry.method.getBytes(ISO_8859_1);
+        assert method.length == 5;
+        size = (int)(entry.size & UINT_MASK);
+        final int ftime = entry.getFTime();
+        mtime = (short)(ftime & USHORT_MASK);
+        mdate = (short)((ftime << 16) & USHORT_MASK);
+        attribute = entry.attribute;
+        level = HEADER_LEVEL_0;
+        nameLength = (byte)(name.length & UBYTE_MASK);
+        crc = entry.crc;
+        assert buffer.position() == 0;
+        buffer.putShort((short)0); // skip
+        buffer.put(method);
+        buffer.position(11); // skip
+        buffer.putInt(size);
+        buffer.putShort(mtime);
+        buffer.putShort(mdate);
+        buffer.put(attribute);
+        buffer.put(level);
+        buffer.put(nameLength);
+        buffer.put(name);
+        buffer.putShort(crc);
+        final int extendedHeaderSize = 0; // not supported
+        final int endp = buffer.position();
+        if ((endp - 2) > 0xFF)
+            throw new LzhException("invalid header length: " + (endp - 2));
+        headerLength = (byte)((endp - 2) & UBYTE_MASK);
+        checksum = (byte)(calculateChecksum(2, endp) & UBYTE_MASK);
+        if (entry.compressedSize > 0)
+            skipSize = calculateSkipSize(entry, size, extendedHeaderSize);
+        else
+            skipSize = 0;
+        buffer.put(0, headerLength);
+        buffer.put(1, checksum);
+        buffer.putInt(7, skipSize);
+    }
+
+    @SuppressWarnings("unused")
+    private void writeLevel1(LzhEntry entry) throws IOException {
+        // Header Level 1
+        final byte headerLength; // length of this header
+        final byte checksum; // checksum of header (SUM)
+        final byte[] method; // compression method
+        final int skipSize; // skip size
+        final int size; // uncompressed size
+        final short mtime; // last modified time (MS-DOS time)
+        final short mdate; // last modified date (MS-DOS time)
+        final byte reserved; // reserved
+        final byte level; // header level
+        final byte nameLength; // length of path name
+        final byte[] name; // path name
+        final short crc; // checksum of file (CRC16)
+        final byte osIdentifier; // OS identifier which compressed this
+        // ---
+        assert entry.compressedSize >= 0 && entry.compressedSize <= Integer.MAX_VALUE;
+        assert entry.size >= 0 && entry.size <= Integer.MAX_VALUE;
+        name = entry.getNameAsBytes();
+        assert name.length <= 0xFF;
+        method = entry.method.getBytes(ISO_8859_1);
+        assert method.length == 5;
+        assert entry.size >= 0 && entry.size <= MAX_DATA_SIZE;
+        size = (int)(entry.size & UINT_MASK);
+        final int ftime = entry.getFTime();
+        mtime = (short)(ftime & USHORT_MASK);
+        mdate = (short)((ftime << 16) & USHORT_MASK);
+        reserved = FILETYPE_FILE;
+        level = HEADER_LEVEL_1;
+        nameLength = (byte)(name.length & UBYTE_MASK);
+        osIdentifier = PLATFORM_JAVA; // fixed value in writing
+        assert buffer.position() == 0;
+        buffer.putShort((short)0); // skip
+        buffer.put(method);
+        buffer.putInt(11); // skip
+        buffer.putInt(size);
+        buffer.putShort(mtime);
+        buffer.putShort(mdate);
+        buffer.put(reserved);
+        buffer.put(level);
+        buffer.put(nameLength);
+        buffer.put(name);
+        buffer.putShort((short)0); // skip
+        buffer.put(osIdentifier);
+        final short extendedHeaderSize = 0;
+        buffer.putShort(extendedHeaderSize);
+        buffer.put((byte)0);
+        final int endp = buffer.position();
+        if ((endp - 2) > 0xFF)
+            throw new LzhException("invalid header length: " + (endp - 2));
+        headerLength = (byte)((endp - 2) & UBYTE_MASK);
+        checksum = (byte)calculateChecksum(2, endp);
+        if (entry.compressedSize > 0)
+            skipSize = calculateSkipSize(entry, size, extendedHeaderSize);
+        else
+            skipSize = 0;
+        buffer.put(0, headerLength);
+        buffer.put(1, checksum);
+        buffer.putInt(7, skipSize);
+    }
+
+    @SuppressWarnings("unused")
+    private void writeLevel2(LzhEntry entry) throws IOException {
+        // Header Level 2
+        final short headerLength; // length of this header
+        final byte[] method; // compression method
+        final int compressedSize; // compressed size
+        final int size; // uncompressed size
+        final int mtime; // last modified timestamp (POSIX time)
+        final byte reserved; // reserved
+        final byte level; // header level
+        final short crc; // checksum of file (CRC16)
+        final byte osIdentifier; // OS identifier which compressed this
+        final short firstExHeaderLength; // length of next (first) extend header
+        // ---
+        assert entry.compressedSize >= 0 && entry.compressedSize <= MAX_DATA_SIZE;
+        assert entry.size >= 0 && entry.size <= MAX_DATA_SIZE;
+        byte[] nameb = entry.getNameAsBytes();
+        assert nameb.length <= 0xFF;
+        method = entry.method.getBytes(ISO_8859_1);
+        assert method.length == 5;
+        final boolean isDirectory = entry.isDirectory();
+        assert isDirectory == entry.method.equals(LzhMethod.LHD);
+        compressedSize = isDirectory ? 0 : (int)(entry.compressedSize & UINT_MASK);
+        size = isDirectory ? 0 : (int)(entry.size & UINT_MASK);
+        mtime = entry.getTimeT();
+        reserved = FILETYPE_FILE;
+        level = HEADER_LEVEL_2;
+        crc = entry.crc;
+        osIdentifier = PLATFORM_JAVA; // fixed value in writing
+        firstExHeaderLength = 0;
+        assert buffer.position() == 0;
+        buffer.putShort((short)0); // skip
+        buffer.put(method);
+        buffer.putInt(compressedSize);
+        buffer.putInt(size);
+        buffer.putInt(mtime);
+        buffer.put(reserved);
+        buffer.put(level);
+        buffer.putShort(crc);
+        buffer.put(osIdentifier);
+        /* extended */
+        // dividing dir and name
+        int lastDelimiterOffset = -1;
+        for (int i = 0; i < nameb.length; i++)
+            if (nameb[i] == '/' || nameb[i] == '\\') {
+                nameb[i] = PATH_DELIMITER;
+                lastDelimiterOffset = i;
+            }
+        final int nameOffset = (lastDelimiterOffset < 0) ? 0 : lastDelimiterOffset + 1;
+        // 0x01 file name
+        final int nameLength = (isDirectory) ? 0 : nameb.length - nameOffset;
+        buffer.putShort((short)(nameLength + 3));
+        buffer.put((byte)0x01);
+        if (nameLength > 0)
+            buffer.put(nameb, nameOffset, nameLength);
+        if (nameOffset > 0) {
+            // 0x02 dir name
+            byte[] dirb = Arrays.copyOf(nameb, nameb.length + 1);
+            final int dirLength;
+            if (isDirectory) {
+                if (nameb[nameb.length - 1] != PATH_DELIMITER) {
+                    dirLength = dirb.length;
+                    dirb[nameb.length - 1] = PATH_DELIMITER;
+                }
+                else
+                    dirLength = nameb.length;
+            }
+            else
+                dirLength = nameOffset;
+            buffer.putShort((short)(dirLength + 3));
+            buffer.put((byte)0x02);
+            buffer.put(dirb, 0, dirLength);
+        }
+        // 0x41 MS Windows timestamp
+        buffer.putShort((short)27);
+        buffer.put((byte)0x41);
+        buffer.putLong(entry.getFileTime());
+        buffer.putLong(entry.getFileTime());
+        buffer.putLong(entry.getFileTime());
+        // 0x00 common
+        buffer.putShort((short)6);
+        buffer.put((byte)0x00);
+        final int crcIndex = buffer.position();
+        buffer.putShort((short)0);
+        buffer.put((byte)0x00); // ??
+        // no next
+        buffer.putShort((short)0);
+        // header size
+        short headerLength0 = 0;
+        headerLength0 += buffer.position();
+        if (headerLength0 % 256 == 0) {
+            buffer.put((byte)0x00);
+            ++headerLength0;
+            assert headerLength0 >= 0;
+        }
+        headerLength = headerLength0;
+        LzhChecksum cksum = new LzhChecksum();
+        cksum.reset();
+        cksum.update(buffer.array(), 0, headerLength);
+        buffer.putShort(0, headerLength);
+        buffer.putShort(crcIndex, cksum.getShortValue());
+    }
+
+    private int calculateChecksum(int start, int end) {
+        int sum = 0;
+        for (int i = start; i < end; i++)
+            sum += buffer.get(i);
+        return sum;
+    }
+
+    private static int calculateSkipSize(LzhEntry entry, int headerSize, int extendedHeaderSize) throws LzhException {
+        assert entry.compressedSize >= 0 && entry.compressedSize <= Integer.MAX_VALUE;
+        assert entry.size >= 0 && entry.size <= Integer.MAX_VALUE;
+        assert headerSize >= 0 && extendedHeaderSize >= 0;
+        int skipSize = 0;
+        if (entry.getMethod().isCompressing()) {
+            skipSize += entry.compressedSize;
+            skipSize -= headerSize;
+        }
+        else
+            skipSize += entry.size;
+        skipSize += extendedHeaderSize;
+        assert skipSize >= 0 : "skip size = " + skipSize;
+        return skipSize;
+    }
+
+    private byte[] getBytes(int length) {
+        byte[] bytes = new byte[length];
+        for (int i = 0; i < bytes.length; i++)
+            bytes[i] = buffer.get();
+        return bytes;
+    }
+
+    private byte[] nextPath(int length) {
+        byte[] bytes = new byte[length];
+        buffer.get(bytes);
+        for (int i = 0; i < length; i++)
+            if ((bytes[i] & PATH_DELIMITER) == PATH_DELIMITER)
+                bytes[i] = '/';
+        return bytes;
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/lzh/LzhInputStream.java b/src/jp/sfjp/armadillo/archive/lzh/LzhInputStream.java
new file mode 100644 (file)
index 0000000..aa908a5
--- /dev/null
@@ -0,0 +1,56 @@
+package jp.sfjp.armadillo.archive.lzh;
+
+import java.io.*;
+import jp.sfjp.armadillo.archive.*;
+import jp.sfjp.armadillo.compression.lzhuf.*;
+
+public final class LzhInputStream extends ArchiveInputStream {
+
+    private LzhHeader header;
+    private LzhEntry nextEntry;
+
+    public LzhInputStream(InputStream is) {
+        super(is);
+        this.header = new LzhHeader();
+    }
+
+    public LzhEntry getNextEntry() throws IOException {
+        ensureOpen();
+        if (remaining > 0)
+            closeEntry();
+        LzhEntry entry = header.read(in);
+        if (entry == null)
+            return null;
+        LzhMethod method = new LzhMethod(entry.method);
+        if (method.isCompressing())
+            frontStream = new LzssInputStream(new LzhHuffmanDecoder(in, entry.compressedSize),
+                                              method.getDictionarySize(),
+                                              method.getMatchSize(),
+                                              method.getThreshold());
+        else
+            frontStream = in;
+        remaining = entry.size;
+        nextEntry = entry;
+        return entry;
+    }
+
+    public void closeEntry() throws IOException {
+        ensureOpen();
+        if (nextEntry != null && remaining == nextEntry.size) {
+            frontStream = in;
+            remaining = nextEntry.compressedSize;
+        }
+        while (remaining > 0)
+            skip(remaining);
+        assert remaining == 0 : "rest=" + remaining;
+        nextEntry = null;
+        frontStream = in;
+    }
+
+    @Override
+    public void close() throws IOException {
+        header = null;
+        super.close();
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/lzh/LzhMethod.java b/src/jp/sfjp/armadillo/archive/lzh/LzhMethod.java
new file mode 100644 (file)
index 0000000..690fbef
--- /dev/null
@@ -0,0 +1,76 @@
+package jp.sfjp.armadillo.archive.lzh;
+
+/**
+ * LZH compression method.
+ */
+public final class LzhMethod {
+
+    /** <code>LHD</code> */
+    public static final String LHD = "-lhd-";
+    /** <code>LH0</code> */
+    public static final String LH0 = "-lh0-";
+    /** <code>LH4</code> */
+    public static final String LH4 = "-lh4-";
+    /** <code>LH5</code> */
+    public static final String LH5 = "-lh5-";
+    /** <code>LH6</code> */
+    public static final String LH6 = "-lh6-";
+    /** <code>LH7</code> */
+    public static final String LH7 = "-lh7-";
+
+    private final String methodName;
+    private final int dictionarySize;
+    private final int matchSize;
+    private final int threshold;
+
+    public LzhMethod(String methodName) throws LzhException {
+        this.methodName = methodName;
+        this.dictionarySize = detectDictionarySize(methodName);
+        this.matchSize = (dictionarySize == 0) ? 0 : 256;
+        this.threshold = (dictionarySize == 0) ? 0 : 3;
+    }
+
+    private static int detectDictionarySize(String methodName) throws LzhException {
+        if (methodName.matches("-lh[d0]-"))
+            return 0;
+        else if (methodName.matches("-lh[4567]-"))
+            switch (methodName.charAt(3)) {
+                case '4':
+                    return 4096;
+                case '5':
+                default:
+                    return 8192;
+                case '6':
+                    return 32768;
+                case '7':
+                    return 65536;
+            }
+        else
+            throw new LzhException("unsupported method: " + methodName);
+    }
+
+    public String getMethodName() {
+        return methodName;
+    }
+
+    public int getDictionarySize() {
+        return dictionarySize;
+    }
+
+    public int getMatchSize() {
+        return matchSize;
+    }
+
+    public int getThreshold() {
+        return threshold;
+    }
+
+    public boolean isCompressing() {
+        return dictionarySize > 0;
+    }
+
+    public static boolean isCompressing(String methodName) throws LzhException {
+        return detectDictionarySize(methodName) > 0;
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/lzh/LzhOutputStream.java b/src/jp/sfjp/armadillo/archive/lzh/LzhOutputStream.java
new file mode 100644 (file)
index 0000000..dabde60
--- /dev/null
@@ -0,0 +1,90 @@
+package jp.sfjp.armadillo.archive.lzh;
+
+import java.io.*;
+import jp.sfjp.armadillo.archive.*;
+import jp.sfjp.armadillo.compression.lzhuf.*;
+import jp.sfjp.armadillo.io.*;
+
+public final class LzhOutputStream extends ArchiveOutputStream {
+
+    private LzhHeader header;
+    private LzhEntry ongoingEntry;
+    private InspectionOutputStream ios;
+    private LzhChecksum crc;
+    private ByteArrayOutputStream bos;
+
+    public LzhOutputStream(OutputStream os) {
+        super(os);
+        this.header = new LzhHeader();
+        this.crc = new LzhChecksum();
+        this.bos = new ByteArrayOutputStream();
+    }
+
+    public void putNextEntry(LzhEntry entry) throws IOException {
+        if (ongoingEntry != null)
+            closeEntry();
+        ongoingEntry = entry;
+        LzhMethod method = entry.getMethod();
+        crc.reset();
+        bos.reset();
+        frontStream = ios = new InspectionOutputStream(openStream(bos, method), crc);
+    }
+
+    static OutputStream openStream(OutputStream os, LzhMethod method) {
+        if (method.isCompressing())
+            return new LzssOutputStream(new LzhHuffmanEncoder(os, method.getThreshold()),
+                                        method.getDictionarySize(),
+                                        method.getMatchSize(),
+                                        method.getThreshold());
+        else
+            return os;
+    }
+
+    public void closeEntry() throws IOException {
+        frontStream.flush();
+        if (frontStream != out)
+            frontStream.close();
+        frontStream = out;
+        if (ios != null) {
+            ongoingEntry.compressedSize = bos.size();
+            ongoingEntry.crc = getCrc();
+        }
+        writeHeader(ongoingEntry);
+        if (ios != null)
+            bos.writeTo(out);
+        ongoingEntry = null;
+        ios = null;
+        crc.reset();
+        bos.reset();
+    }
+
+    public void writeHeader(LzhEntry entry) throws IOException {
+        header.write(out, entry);
+    }
+
+    public void writeHeader(LzhEntry entry, OutputStream os) throws IOException {
+        header.write(os, entry);
+    }
+
+    public short getCrc() {
+        return crc.getShortValue();
+    }
+
+    @Override
+    public void close() throws IOException {
+        try {
+            flush();
+            super.write(0); // end of archive
+            super.flush();
+        }
+        finally {
+            header = null;
+            ongoingEntry = null;
+            ios = null;
+            crc = null;
+            bos = null;
+            super.close();
+        }
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/lzh/LzhQuit.java b/src/jp/sfjp/armadillo/archive/lzh/LzhQuit.java
new file mode 100644 (file)
index 0000000..de9758a
--- /dev/null
@@ -0,0 +1,13 @@
+package jp.sfjp.armadillo.archive.lzh;
+
+public final class LzhQuit extends RuntimeException {
+
+    public LzhQuit(String message) {
+        super(message);
+    }
+
+    public LzhQuit(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/tar/DumpTarHeader.java b/src/jp/sfjp/armadillo/archive/tar/DumpTarHeader.java
new file mode 100644 (file)
index 0000000..9e51fc4
--- /dev/null
@@ -0,0 +1,290 @@
+package jp.sfjp.armadillo.archive.tar;
+
+import java.io.*;
+import java.nio.*;
+import java.nio.channels.*;
+import java.util.*;
+import java.util.zip.*;
+import jp.sfjp.armadillo.archive.*;
+import jp.sfjp.armadillo.time.*;
+
+/**
+ * Dump TAR archive header.
+ */
+public final class DumpTarHeader extends DumpArchiveHeader {
+
+    private static final int BLOCK_SIZE = 512;
+    private static final String MAGIC_USTAR = "ustar";
+    private static final TimeT TIME_T = new TimeT();
+
+    private static final String fmt1 = "  * %s = %s%n";
+    private static final String fmt2 = "  %1$-16s = [0x%2$08X] [0o%2$012o] ( %2$d )%n";
+    private static final String fmt3 = "  %-16s = \"%s\"%n";
+
+    private final ByteBuffer buffer;
+
+    public DumpTarHeader() {
+        this.buffer = ByteBuffer.allocate(BLOCK_SIZE);
+    }
+
+    @Override
+    public void dump(InputStream is, PrintWriter out) throws IOException {
+        int p = 0;
+        while (true) {
+            printOffset(out, p);
+            final int readLength = draw(is, buffer);
+            if (readLength == 0)
+                break;
+            p += readLength;
+            if (readLength != BLOCK_SIZE)
+                warn(out, "bad header: size=%d", readLength);
+            if (isEmptyBlock()) {
+                out.println("  ( EMPTY BLOCK )");
+                continue;
+            }
+            try {
+                if (isUstar())
+                    p += readUstar(is, out);
+                else
+                    p += readTar(is, out);
+            }
+            catch (Exception ex) {
+                throw new IOException(ex);
+            }
+        }
+        printEnd(out, "TAR", p);
+    }
+
+    private boolean isUstar() {
+        final String s = new String(buffer.array(), 257, 5);
+        return s.equals(MAGIC_USTAR);
+    }
+
+    private static int draw(InputStream is, ByteBuffer buffer) throws IOException {
+        buffer.clear();
+        buffer.limit(BLOCK_SIZE);
+        Channels.newChannel(is).read(buffer);
+        int length = buffer.position();
+        buffer.rewind();
+        return length;
+    }
+
+    private int readTar(InputStream is, PrintWriter out) throws IOException {
+        int readSize = 0;
+        // Header Block (TAR Format)
+        final String name; // name of file (100bytes)
+        final int mode; // file mode (8bytes)
+        final int uid; // owner user ID (8bytes)
+        final int gid; // owner group ID (8bytes)
+        final long size; // length of file in bytes (12bytes)
+        final long mtime; // modify time of file (12bytes)
+        final int chksum; // checksum for header (8bytes)
+        final int link; // indicator for links (1byte)
+        name = clip(100);
+        mode = clipAsInt(8);
+        uid = clipAsInt(8);
+        gid = clipAsInt(8);
+        size = clipAsLong(12);
+        mtime = clipAsLong(12);
+        chksum = clipAsInt(8);
+        link = clipAsInt(1);
+        clip(100);
+        printHeaderName(out, "TAR (old) format");
+        out.printf(fmt1, "name", name);
+        p(out, "mode", mode, 8);
+        p(out, "uid", uid, 8);
+        p(out, "gid", gid, 8);
+        p(out, "size", size, 12);
+        p(out, "mtime", mtime, 12);
+        p(out, "chksum", chksum, 8);
+        p(out, "link", link, 1);
+        if (size > 0)
+            readSize += skipBlock(is, size);
+        return readSize;
+    }
+
+    private int readUstar(InputStream is, PrintWriter out) throws IOException {
+        int readSize = 0;
+        // Header Block (USTAR Format)
+        final String name; // name of file (100bytes)
+        final int mode; // file mode (8bytes)
+        final int uid; // owner user ID (8bytes)
+        final int gid; // owner group ID (8bytes)
+        final long size; // length of file in bytes (12bytes)
+        final long mtime; // modify time of file (12bytes)
+        final int chksum; // checksum for header (8bytes)
+        final char typeflag; // type of file (1byte)
+        final String linkname; // name of linked file (100bytes)
+        final String magic; // USTAR indicator (6bytes)
+        final int version; // USTAR version (2bytes)
+        final String uname; // owner user name (32bytes)
+        final String gname; // owner group name (32bytes)
+        final int devmajor; // device major number (8bytes)
+        final int devminor; // device minor number (8bytes)
+        final String prefix; // prefix for file name (155bytes)
+        name = clip(100);
+        mode = clipAsInt(8);
+        uid = clipAsInt(8);
+        gid = clipAsInt(8);
+        size = clipAsLong(12);
+        mtime = clipAsLong(12);
+        chksum = clipAsInt(8);
+        typeflag = clipAsChar();
+        linkname = clip(100);
+        magic = clip(6);
+        version = clipAsInt(2);
+        uname = clip(32);
+        gname = clip(32);
+        devmajor = clipAsInt(8);
+        devminor = clipAsInt(8);
+        prefix = clip(155);
+        printHeaderName(out, "USTAR format");
+        out.printf(fmt1, "name", name);
+        out.printf(fmt1, "mtime as date", toDate(mtime));
+        p(out, "mode", mode, 8);
+        p(out, "uid", uid, 8);
+        p(out, "gid", gid, 8);
+        p(out, "size", size, 12);
+        p(out, "mtime", mtime, 12);
+        p(out, "chksum", chksum, 8);
+        out.printf(fmt3, "typeflag", typeflag);
+        out.printf(fmt3, "linkname", linkname);
+        out.printf(fmt3, "magic", magic);
+        out.printf(fmt2, "version", version);
+        out.printf(fmt3, "uname", uname);
+        out.printf(fmt3, "gname", gname);
+        out.printf(fmt2, "devmajor", devmajor);
+        out.printf(fmt2, "devminor", devminor);
+        out.printf(fmt3, "prefix", prefix);
+        if (typeflag == 'L') {
+            // LongLink
+            readSize += draw(is, buffer);
+            final String s = clip(512);
+            out.printf(fmt1, "LongLink", s);
+        }
+        else if (size > 0)
+            readSize += skipBlock(is, size);
+        return readSize;
+    }
+
+    private long skipBlock(InputStream is, long size) throws IOException {
+        long skippedSize = 0;
+        long skipCount = size;
+        while (skipCount > 0) {
+            final long skipped = is.skip(BLOCK_SIZE);
+            skipCount -= skipped;
+            if (skipped != BLOCK_SIZE && skipCount > 0)
+                throw new IllegalStateException("bad skip size: " + skipped);
+            skippedSize += skipped;
+        }
+        return skippedSize;
+    }
+
+    private String clip(int length) {
+        final int p = buffer.position();
+        int availableLength = 0;
+        for (int i = 0; i < length; i++) {
+            if (buffer.get() == 0x00)
+                break;
+            ++availableLength;
+        }
+        buffer.rewind();
+        buffer.position(p);
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < availableLength; i++) {
+            final int x = buffer.get() & 0xFF;
+            if (x < 0x20 || x > 0x7F)
+                sb.append(String.format("\\%o", x));
+            else
+                sb.append((char)x);
+        }
+        buffer.position(p + length);
+        return sb.toString();
+    }
+
+    private char clipAsChar() {
+        final String s = clip(1);
+        assert s.length() == 1 && s.matches("^[A-Za-z0-9]$");
+        return s.charAt(0);
+    }
+
+    private int clipAsInt(int length) {
+        final String s = clipAsNumberString(length);
+        return s.isEmpty() ? 0 : Integer.parseInt(s, 8);
+    }
+
+    private long clipAsLong(int length) {
+        final String s = clipAsNumberString(length);
+        return s.isEmpty() ? 0 : Long.parseLong(s, 8);
+    }
+
+    private String clipAsNumberString(int length) {
+        byte[] bytes = new byte[length];
+        buffer.get(bytes);
+        int i = 0;
+        for (; i < length; i++) {
+            byte b = bytes[i];
+            if (b == 0x00)
+                break;
+            assert b == 0x20 || b >= 0x30 && b <= 0x39;
+        }
+        return (new String(bytes, 0, i)).trim();
+    }
+
+    private boolean isEmptyBlock() {
+        byte[] bytes = buffer.array();
+        for (int i = 0; i < bytes.length; i++)
+            if (bytes[i] != 0x00)
+                return false;
+        return true;
+    }
+
+    static long getSkipSize(long size) {
+        if (size == 0 || size == BLOCK_SIZE || size % BLOCK_SIZE == 0)
+            return 0;
+        else
+            return BLOCK_SIZE - ((size > BLOCK_SIZE) ? size % BLOCK_SIZE : size);
+
+    }
+
+    static InputStream getInputStream(File file) throws IOException {
+        ArchiveType type = ArchiveType.of(file.getName());
+        switch (type) {
+            case TAR:
+                return new FileInputStream(file);
+            case TARGZ:
+                return new GZIPInputStream(new FileInputStream(file));
+            default:
+                throw new UnsupportedOperationException("not TAR ? : " + type);
+        }
+    }
+
+    static Date toDate(long timet) {
+        return new Date(TIME_T.toMilliseconds(timet));
+    }
+
+    static <T> void p(PrintWriter out, String name, T value, int width) {
+        final int wh;
+        final int wo;
+        switch (width) {
+            case 1:
+                wh = 2;
+                wo = 2;
+                break;
+            case 8:
+                wh = 6;
+                wo = 8;
+                break;
+            case 12:
+                wh = 6;
+                wo = 8;
+                break;
+            default:
+                wh = 8;
+                wo = width;
+        }
+        final String fmt = "  %1$-16s = [0x%2$0" + wh + "X] [0o%2$0" + wo + "o] ( %2$d )%n";
+        out.printf(fmt, name, value);
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/tar/TarArchiveCreator.java b/src/jp/sfjp/armadillo/archive/tar/TarArchiveCreator.java
new file mode 100644 (file)
index 0000000..638cd34
--- /dev/null
@@ -0,0 +1,64 @@
+package jp.sfjp.armadillo.archive.tar;
+
+import java.io.*;
+import jp.sfjp.armadillo.archive.*;
+import jp.sfjp.armadillo.io.*;
+
+public final class TarArchiveCreator implements ArchiveCreator {
+
+    private final TarOutputStream os;
+
+    public TarArchiveCreator(OutputStream os) {
+        this.os = new TarOutputStream(os);
+    }
+
+    @Override
+    public ArchiveEntry newEntry(String name) {
+        return new TarEntry(name);
+    }
+
+    @Override
+    public void addEntry(ArchiveEntry entry, File file) throws IOException {
+        if (file.isDirectory()) {
+            os.putNextEntry(toTarEntry(entry));
+            os.closeEntry();
+        }
+        else {
+            InputStream is = new FileInputStream(file);
+            try {
+                addEntry(entry, is, file.length());
+            }
+            finally {
+                is.close();
+            }
+        }
+        entry.setAdded(true);
+    }
+
+    @Override
+    public void addEntry(ArchiveEntry entry, InputStream is, long length) throws IOException {
+        TarEntry fileEntry = toTarEntry(entry);
+        fileEntry.setSize(length);
+        os.putNextEntry(fileEntry);
+        final long size = IOUtilities.transferAll(is, os);
+        os.closeEntry();
+        assert size == fileEntry.getSize() : "file size";
+        assert size == length : "file size";
+        entry.setSize(size);
+        entry.setAdded(true);
+    }
+
+    static TarEntry toTarEntry(ArchiveEntry entry) {
+        if (entry instanceof TarEntry)
+            return (TarEntry)entry;
+        TarEntry newEntry = new TarEntry(entry.getName());
+        entry.copyTo(newEntry);
+        return newEntry;
+    }
+
+    @Override
+    public void close() throws IOException {
+        os.close();
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/tar/TarArchiveExtractor.java b/src/jp/sfjp/armadillo/archive/tar/TarArchiveExtractor.java
new file mode 100644 (file)
index 0000000..42cb3eb
--- /dev/null
@@ -0,0 +1,30 @@
+package jp.sfjp.armadillo.archive.tar;
+
+import java.io.*;
+import jp.sfjp.armadillo.archive.*;
+import jp.sfjp.armadillo.io.*;
+
+public final class TarArchiveExtractor implements ArchiveExtractor {
+
+    private TarInputStream is;
+
+    public TarArchiveExtractor(InputStream is) {
+        this.is = new TarInputStream(is);
+    }
+
+    @Override
+    public ArchiveEntry nextEntry() throws IOException {
+        return ArchiveEntry.orNull(is.getNextEntry());
+    }
+
+    @Override
+    public long extract(OutputStream os) throws IOException {
+        return IOUtilities.transferAll(is, os);
+    }
+
+    @Override
+    public void close() throws IOException {
+        is.close();
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/tar/TarEntry.java b/src/jp/sfjp/armadillo/archive/tar/TarEntry.java
new file mode 100644 (file)
index 0000000..da2d91c
--- /dev/null
@@ -0,0 +1,216 @@
+package jp.sfjp.armadillo.archive.tar;
+
+import java.io.*;
+import jp.sfjp.armadillo.archive.*;
+import jp.sfjp.armadillo.time.*;
+
+/**
+ * TAR entry.
+ */
+public final class TarEntry extends ArchiveEntry {
+
+    /** default mode for dir */
+    public static final int DEFAULT_MODE_DIR = 0755;
+
+    /** default mode for file */
+    public static final int DEFAULT_MODE_FILE = 0666;
+
+    private static final TimeT TIME_T = new TimeT();
+
+    private int mode;
+    int uid;
+    int gid;
+    long size;
+    long mtime;
+    int chksum;
+    char typeflag;
+    String linkname;
+    String magic;
+    String version;
+    String uname;
+    String gname;
+    String devmajor;
+    String devminor;
+    String prefix;
+
+    TarEntry() {
+        super(false);
+        this.mode = 0;
+        this.uid = -1;
+        this.gid = -1;
+        this.size = 0L;
+        this.mtime = 0;
+        this.chksum = 0;
+        this.typeflag = 0;
+        this.linkname = "";
+        this.magic = "ustar"; // default format
+        this.version = "";
+        this.uname = "unknown";
+        this.gname = "unknown";
+        this.devmajor = "";
+        this.devminor = "";
+        this.prefix = "";
+    }
+
+    public TarEntry(String name) {
+        this();
+        setName(name);
+        this.mode = (isDirectory()) ? DEFAULT_MODE_DIR : DEFAULT_MODE_FILE;
+    }
+
+    public TarEntry(String name, File file) {
+        this(name);
+        setFileInfo(file);
+    }
+
+    @Override
+    public long getSize() {
+        return size;
+    }
+
+    @Override
+    public void setSize(long size) {
+        if (!isDirectory())
+            this.size = size;
+    }
+
+    @Override
+    public long getCompressedSize() {
+        return -1;
+    }
+
+    @Override
+    public void setCompressedSize(long size) {
+        // ignore
+    }
+
+    @Override
+    public long getLastModified() {
+        return TIME_T.toMilliseconds(mtime);
+    }
+
+    @Override
+    public void setLastModified(long time) {
+        this.mtime = TIME_T.int64From(time);
+    }
+
+    @Override
+    public String getMethodName() {
+        if (!this.magic.trim().isEmpty())
+            return this.magic.trim();
+        return "TAR";
+    }
+
+    public int getMode() {
+        return mode;
+    }
+
+    public void setMode(int mode) {
+        this.mode = mode;
+    }
+
+    public int getUid() {
+        return uid;
+    }
+
+    public void setUid(int uid) {
+        this.uid = uid;
+    }
+
+    public int getGid() {
+        return gid;
+    }
+
+    public void setGid(int gid) {
+        this.gid = gid;
+    }
+
+    public long getMtime() {
+        return mtime;
+    }
+
+    public void setMtime(long mtime) {
+        this.mtime = mtime;
+    }
+
+    public int getChksum() {
+        return chksum;
+    }
+
+    public void setChksum(int chksum) {
+        this.chksum = chksum;
+    }
+
+    public char getTypeflag() {
+        return typeflag;
+    }
+
+    public void setTypeflag(char typeflag) {
+        this.typeflag = typeflag;
+    }
+
+    public String getLinkname() {
+        return linkname;
+    }
+
+    public void setLinkname(String linkname) {
+        this.linkname = linkname;
+    }
+
+    public String getMagic() {
+        return magic;
+    }
+
+    public void setMagic(String magic) {
+        this.magic = magic;
+    }
+
+    public String getVersion() {
+        return version;
+    }
+
+    public void setVersion(String version) {
+        this.version = version;
+    }
+
+    public String getUname() {
+        return uname;
+    }
+
+    public void setUname(String uname) {
+        this.uname = uname;
+    }
+
+    public String getGname() {
+        return gname;
+    }
+
+    public void setGname(String gname) {
+        this.gname = gname;
+    }
+
+    public String getDevmajor() {
+        return devmajor;
+    }
+
+    public void setDevmajor(String devmajor) {
+        this.devmajor = devmajor;
+    }
+
+    public String getDevminor() {
+        return devminor;
+    }
+
+    public void setDevminor(String devminor) {
+        this.devminor = devminor;
+    }
+
+    public String getPrefix() {
+        return prefix;
+    }
+
+    public void setPrefix(String prefix) {
+        this.prefix = prefix;
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/tar/TarException.java b/src/jp/sfjp/armadillo/archive/tar/TarException.java
new file mode 100644 (file)
index 0000000..617d0d7
--- /dev/null
@@ -0,0 +1,16 @@
+package jp.sfjp.armadillo.archive.tar;
+
+import java.io.*;
+
+public final class TarException extends IOException {
+
+    public TarException(String s) {
+        super(s);
+    }
+
+    public TarException(String s, Throwable th) {
+        super(s);
+        initCause(th);
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/tar/TarFile.java b/src/jp/sfjp/armadillo/archive/tar/TarFile.java
new file mode 100644 (file)
index 0000000..52e240d
--- /dev/null
@@ -0,0 +1,223 @@
+package jp.sfjp.armadillo.archive.tar;
+
+import java.io.*;
+import java.nio.channels.*;
+import jp.sfjp.armadillo.archive.*;
+
+public final class TarFile extends ArchiveFile { //implements ArchiveCreator, ArchiveExtractor {
+
+    private File afile;
+    private RandomAccessFile raf;
+    private TarHeader header;
+    private TarEntry ongoingEntry;
+    private final byte[] buffer;
+
+    public TarFile(File afile) {
+        if (afile.isDirectory())
+            throw new IllegalArgumentException("not a file: " + afile.getPath());
+        this.afile = afile;
+        this.header = new TarHeader();
+        this.buffer = new byte[TarHeader.BLOCK_SIZE];
+    }
+
+    @Override
+    public void open() throws IOException {
+        if (raf != null)
+            throw new IOException("the file has been already opened");
+        if (!afile.exists())
+            afile.createNewFile();
+        this.raf = new RandomAccessFile(afile, "rw");
+        this.opened = true;
+    }
+
+    @Override
+    public void reset() throws IOException {
+        ongoingEntry = null;
+        currentPosition = 0L;
+        raf.seek(0L);
+    }
+
+    @Override
+    public ArchiveEntry nextEntry() throws IOException {
+        ensureOpen();
+        if (ongoingEntry == null)
+            currentPosition = 0L;
+        else {
+            final long size = ongoingEntry.size;
+            final long totalLength = TarHeader.BLOCK_SIZE + size + TarHeader.getSkipSize(size);
+            assert totalLength % TarHeader.BLOCK_SIZE == 0;
+            currentPosition += totalLength;
+        }
+        raf.seek(currentPosition);
+        ongoingEntry = readCurrentEntry();
+        raf.seek(currentPosition + TarHeader.BLOCK_SIZE);
+        return ongoingEntry;
+    }
+
+    @Override
+    public boolean seek(ArchiveEntry entry) throws IOException {
+        ensureOpen();
+        reset();
+        while (true) {
+            ArchiveEntry nextEntry = nextEntry();
+            if (nextEntry == null)
+                break;
+            if (nextEntry.getName().equals(entry.getName()))
+                return true;
+        }
+        reset();
+        return false;
+    }
+
+    public void seekEndOfLastEntry() throws IOException {
+        ensureOpen();
+        reset();
+        while (true)
+            if (nextEntry() == null)
+                break;
+    }
+
+    @Override
+    public void addEntry(ArchiveEntry entry, InputStream is, long length) throws IOException {
+        final long blockCount = calculateBlockCount(length);
+        FileChannel fc = raf.getChannel();
+        seekEndOfLastEntry();
+        raf.seek(currentPosition);
+        insertEmptyBlock(raf.getFilePointer(), blockCount + 1);
+        TarEntry newEntry = toTarEntry(entry);
+        header.write(Channels.newOutputStream(fc), newEntry);
+        if (blockCount > 0) {
+            final long p = currentPosition + TarHeader.BLOCK_SIZE;
+            long written = fc.transferFrom(Channels.newChannel(is), p, length);
+            assert written == length;
+        }
+        raf.seek(currentPosition);
+        ongoingEntry = newEntry;
+    }
+
+    @Override
+    public void updateEntry(ArchiveEntry entry, InputStream is, long length) throws IOException {
+        final int blockSize = TarHeader.BLOCK_SIZE;
+        if (!seek(entry))
+            throw new TarException("entry " + entry + " not found");
+        raf.seek(currentPosition);
+        header.write(Channels.newOutputStream(raf.getChannel()), toTarEntry(entry));
+        if (length > 0) {
+            final long oldCount = calculateBlockCount(ongoingEntry.size);
+            final long newCount = calculateBlockCount(length);
+            assert newCount > 0;
+            final long p = currentPosition + blockSize;
+            if (newCount < oldCount)
+                truncateBlock(p, oldCount - newCount);
+            if (newCount <= oldCount) {
+                raf.seek(p + (newCount - 1) * blockSize);
+                raf.write(buffer);
+            }
+            else
+                insertEmptyBlock(p, newCount - oldCount);
+            raf.getChannel().transferFrom(Channels.newChannel(is), p, length);
+        }
+        raf.seek(currentPosition);
+        ongoingEntry = TarArchiveCreator.toTarEntry(entry);
+    }
+
+    @Override
+    public void removeEntry(ArchiveEntry entry) throws IOException {
+        if (!seek(entry))
+            throw new TarException("entry " + entry + " not found");
+        raf.seek(currentPosition);
+        assert ongoingEntry != null;
+        final long size = ongoingEntry.size;
+        if (size == 0)
+            truncate(currentPosition, TarHeader.BLOCK_SIZE);
+        else {
+            final long totalLength = TarHeader.BLOCK_SIZE + size + TarHeader.getSkipSize(size);
+            assert totalLength >= 0 && totalLength <= Integer.MAX_VALUE;
+            assert totalLength % TarHeader.BLOCK_SIZE == 0;
+            truncate(currentPosition, totalLength);
+        }
+    }
+
+    static TarEntry toTarEntry(ArchiveEntry entry) {
+        if (entry instanceof TarEntry)
+            return (TarEntry)entry;
+        TarEntry newEntry = new TarEntry(entry.getName());
+        entry.copyTo(newEntry);
+        return newEntry;
+    }
+
+    @Override
+    public ArchiveEntry newEntry(String name) {
+        return new TarEntry(name);
+    }
+
+    public void insertEmptyBlock(long offset, long blockCount) throws IOException {
+        final int blockSize = TarHeader.BLOCK_SIZE;
+        final long insertLength = blockCount * blockSize;
+        raf.seek(raf.length() + insertLength - 1);
+        raf.write(0);
+        final long x = raf.getFilePointer();
+        long p2 = x - blockSize; // last block
+        long p1 = p2 - insertLength;
+        assert p1 > 0 && p2 > 0;
+        byte[] buffer = this.buffer;
+        for (int i = 0; i < blockCount && p1 >= offset; i++) {
+            raf.seek(p1);
+            raf.readFully(buffer);
+            raf.seek(p2);
+            raf.write(buffer);
+            p1 -= blockSize;
+            p2 -= blockSize;
+        }
+        raf.seek(currentPosition = offset);
+    }
+
+    void truncateBlock(long offset, long blockCount) throws IOException {
+        truncate(offset, blockCount * TarHeader.BLOCK_SIZE);
+    }
+
+    void truncate(final long offset, final long length) throws IOException {
+        final int blockSize = TarHeader.BLOCK_SIZE;
+        long p2 = offset;
+        long p1 = p2 + length;
+        final long restLength = raf.length() - p1;
+        assert restLength >= blockSize && restLength % blockSize == 0;
+        byte[] bytes = new byte[blockSize];
+        final long blockCount = calculateBlockCount(restLength);
+        for (int i = 0; i < blockCount; i++) {
+            raf.seek(p1);
+            raf.readFully(bytes);
+            raf.seek(p2);
+            raf.write(bytes);
+            p2 += blockSize;
+            p1 += blockSize;
+        }
+        raf.setLength(raf.length() - length);
+        reset();
+    }
+
+    TarEntry readCurrentEntry() throws IOException {
+        return header.read(Channels.newInputStream(raf.getChannel()));
+    }
+
+    static long calculateBlockCount(long size) {
+        if (size == 0L)
+            return 0L;
+        final long r = size / TarHeader.BLOCK_SIZE;
+        final long mod = size - r * TarHeader.BLOCK_SIZE;
+        return r + (mod > 0 ? 1 : 0);
+    }
+
+    @Override
+    public void close() throws IOException {
+        try {
+            raf.close();
+        }
+        finally {
+            super.close();
+            afile = null;
+            header = null;
+        }
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/tar/TarHeader.java b/src/jp/sfjp/armadillo/archive/tar/TarHeader.java
new file mode 100644 (file)
index 0000000..fad789d
--- /dev/null
@@ -0,0 +1,409 @@
+package jp.sfjp.armadillo.archive.tar;
+
+import java.io.*;
+import java.util.*;
+
+public final class TarHeader {
+
+    static final int BLOCK_SIZE = 512;
+
+    private static final String MAGIC_USTAR = "ustar";
+
+    private final byte[] buffer;
+    private int position;
+
+    public TarHeader() {
+        this.buffer = new byte[BLOCK_SIZE];
+        this.position = 0;
+    }
+
+    public TarEntry read(InputStream is) throws IOException {
+        reset();
+        final int readLength = draw(is, buffer);
+        if (readLength == 0)
+            return null;
+        if (readLength != BLOCK_SIZE)
+            throw new TarException("bad header: size=" + readLength);
+        if (buffer[0] == 0x00 && isEmptyBlock()) {
+            if (is.read(buffer) == BLOCK_SIZE && isEmptyBlock())
+                return null;
+            throw new TarException("bad end-of-archive");
+        }
+        try {
+            if (isUstar())
+                return readUstar(is);
+            else
+                return readTar(is);
+        }
+        catch (RuntimeException ex) {
+            throw new TarException("bad header at " + position, ex);
+        }
+    }
+
+    private boolean isUstar() {
+        final String s = new String(buffer, 257, 5);
+        return s.equals(MAGIC_USTAR);
+    }
+
+    @SuppressWarnings("unused")
+    private TarEntry readTar(InputStream is) throws IOException {
+        // Header Block (TAR Format)
+        final byte[] name; // name of file (100bytes)
+        final int mode; // file mode (8bytes)
+        final int uid; // owner user ID (8bytes)
+        final int gid; // owner group ID (8bytes)
+        final long size; // length of file in bytes (12bytes)
+        final long mtime; // modify time of file (12bytes)
+        final int chksum; // checksum for header (8bytes)
+        final int link; // indicator for links (1byte)
+        final String linkname; // name of linked file (100bytes)
+        // ---
+        name = clipNameField(100);
+        mode = clipAsInt(8);
+        uid = clipAsInt(8);
+        gid = clipAsInt(8);
+        size = clipAsLong(12);
+        mtime = clipAsLong(12);
+        chksum = clipAsInt(8);
+        link = clipAsInt(1);
+        linkname = clipAsString(100);
+        TarEntry entry = new TarEntry();
+        entry.setName(name);
+        entry.setMode(mode);
+        entry.uid = uid;
+        entry.gid = gid;
+        entry.size = size;
+        entry.mtime = mtime;
+        entry.chksum = chksum;
+        entry.linkname = linkname;
+        return entry;
+    }
+
+    private TarEntry readUstar(InputStream is) throws IOException {
+        TarEntry entry = new TarEntry();
+        readUstar(is, entry);
+        if (entry.typeflag == 'L') {
+            // LongLink
+            final int readLength = draw(is, buffer);
+            assert readLength == BLOCK_SIZE;
+            assert entry.size <= Integer.MAX_VALUE;
+            position = 0;
+            byte[] bytes = clipNameField((int)entry.size);
+            position = 0;
+            final int readLength2 = draw(is, buffer);
+            assert readLength2 == BLOCK_SIZE;
+            readUstar(is, entry);
+            entry.setName(bytes);
+        }
+        return entry;
+    }
+
+    private void readUstar(InputStream is, TarEntry entry) throws IOException {
+        // Header Block (USTAR Format)
+        final byte[] name; // name of file (100bytes)
+        final int mode; // file mode (8bytes)
+        final int uid; // owner user ID (8bytes)
+        final int gid; // owner group ID (8bytes)
+        final long size; // length of file in bytes (12bytes)
+        final long mtime; // modify time of file (12bytes)
+        final int chksum; // checksum for header (8bytes)
+        final char typeflag; // type of file (1byte)
+        final String linkname; // name of linked file (100bytes)
+        final String magic; // USTAR indicator (6bytes)
+        final String version; // USTAR version (2bytes)
+        final String uname; // owner user name (32bytes)
+        final String gname; // owner group name (32bytes)
+        final String devmajor; // device major number (8bytes)
+        final String devminor; // device minor number (8bytes)
+        final String prefix; // prefix for file name (155bytes)
+        // ---
+        name = clipNameField(100);
+        mode = clipAsInt(8);
+        uid = clipAsInt(8);
+        gid = clipAsInt(8);
+        size = clipAsLong(12);
+        mtime = clipAsLong(12);
+        chksum = clipAsInt(8);
+        typeflag = clipChar();
+        linkname = clipAsString(100);
+        magic = clipAsString(6);
+        version = clipAsString(2);
+        uname = clipAsString(32);
+        gname = clipAsString(32);
+        devmajor = clipAsString(8);
+        devminor = clipAsString(8);
+        prefix = clipAsString(155);
+        entry.setName(name);
+        entry.setMode(mode);
+        entry.uid = uid;
+        entry.gid = gid;
+        entry.size = size;
+        entry.mtime = mtime;
+        entry.chksum = chksum;
+        entry.typeflag = typeflag;
+        entry.linkname = linkname;
+        entry.magic = magic;
+        entry.version = version;
+        entry.uname = uname;
+        entry.gname = gname;
+        entry.devmajor = devmajor;
+        entry.devminor = devminor;
+        entry.prefix = prefix;
+    }
+
+    private char clipChar() {
+        final String s = clipAsString(1);
+        if (s.isEmpty())
+            return ' ';
+        return s.charAt(0);
+    }
+
+    private static int draw(InputStream is, byte[] bytes) throws IOException {
+        int readLength = 0;
+        int offset = 0;
+        while (readLength < BLOCK_SIZE) {
+            int read = is.read(bytes, offset, BLOCK_SIZE - readLength);
+            if (read <= 0)
+                break;
+            offset += read;
+            readLength += read;
+        }
+        return readLength;
+    }
+
+    public void write(OutputStream os, TarEntry entry) throws IOException {
+        try {
+            reset();
+            if (entry.magic.startsWith(MAGIC_USTAR))
+                writeUstar(os, entry);
+            else
+                writeTar(os, entry);
+        }
+        catch (RuntimeException ex) {
+            throw new TarException("bad header at " + position, ex);
+        }
+    }
+
+    private void writeTar(OutputStream os, TarEntry entry) throws IOException {
+        // Header Block (TAR Format)
+        final String name; // name of file (100bytes)
+        final int mode; // file mode (8bytes)
+        final int uid; // owner user ID (8bytes)
+        final int gid; // owner group ID (8bytes)
+        final long size; // length of file in bytes (12bytes)
+        final long mtime; // modify time of file (12bytes)
+        final int chksum; // checksum for header (8bytes)
+        final int link; // indicator for links (1byte)
+        final String linkname; // name of linked file (100bytes)
+        // ---
+        name = entry.name();
+        mode = entry.getMode();
+        uid = entry.uid;
+        gid = entry.gid;
+        size = entry.size;
+        mtime = entry.mtime;
+        chksum = entry.chksum;
+        link = 0;
+        linkname = entry.linkname;
+        patch(100, name);
+        patch(8, mode);
+        patch(8, uid);
+        patch(8, gid);
+        patch(12, size);
+        patch(12, mtime);
+        patch(8, chksum);
+        patch(1, link);
+        patch(100, linkname);
+        os.write(buffer);
+        throw new TarException("not impl yet (old Tar)");
+    }
+
+    @SuppressWarnings("unused")
+    private void writeUstar(OutputStream os, TarEntry entry) throws IOException {
+        // Header Block (USTAR Format)
+        final String name; // name of file (100bytes)
+        final int mode; // file mode (8bytes)
+        final int uid; // owner user ID (8bytes)
+        final int gid; // owner group ID (8bytes)
+        final long size; // length of file in bytes (12bytes)
+        final long mtime; // modify time of file (12bytes)
+        final int chksum; // checksum for header (8bytes)
+        final int typeflag; // type of file (1byte)
+        final String linkname; // name of linked file (100bytes)
+        final String magic; // USTAR indicator (6bytes)
+        final String version; // USTAR version (2bytes)
+        final String uname; // owner user name (32bytes)
+        final String gname; // owner group name (32bytes)
+        final String devmajor; // device major number (8bytes)
+        final String devminor; // device minor number (8bytes)
+        final String prefix; // prefix for file name (155bytes)
+        // ---
+        name = entry.name();
+        mode = entry.getMode();
+        uid = entry.uid;
+        gid = entry.gid;
+        size = entry.size;
+        mtime = entry.mtime;
+        chksum = entry.chksum;
+        typeflag = entry.typeflag;
+        linkname = entry.linkname;
+        magic = entry.magic;
+        version = entry.version;
+        uname = entry.uname;
+        gname = entry.gname;
+        devmajor = entry.devmajor;
+        devminor = entry.devminor;
+        prefix = entry.prefix;
+        patch(100, name);
+        patch(8, mode);
+        patch(8, uid);
+        patch(8, gid);
+        patch(12, size);
+        patch(12, mtime);
+        patch(8, "        "); // set after calculating checksum
+        patch(1, typeflag);
+        patch(100, linkname);
+        patch(6, MAGIC_USTAR + ' '); // ignore input
+        patch(2, version);
+        patch(32, uname);
+        patch(32, gname);
+        patch(8, devmajor);
+        patch(8, devminor);
+        patch(155, prefix);
+        // calculate checksum
+        int checksum = 0;
+        for (int i = 0; i < buffer.length; i++)
+            checksum += (buffer[i] & 0xFF);
+        position = 148;
+        patch(6, checksum);
+        entry.chksum = checksum;
+        os.write(buffer);
+    }
+
+    public void writeEndOfArchive(OutputStream os) throws IOException {
+        reset();
+        os.write(buffer);
+        os.write(buffer);
+        os.flush();
+    }
+
+    public void reset() {
+        position = 0;
+        Arrays.fill(buffer, (byte)0);
+    }
+
+    private byte[] clipNameField(int length) {
+        assert length <= BLOCK_SIZE - position;
+        final int p = position;
+        position += length;
+        int availableLength = 0;
+        for (int i = 0; i < length; i++) {
+            if (buffer[p + i] == 0x00)
+                break;
+            ++availableLength;
+        }
+        byte[] nameb = new byte[availableLength];
+        System.arraycopy(buffer, 0, nameb, 0, availableLength);
+        return nameb;
+    }
+
+    private String clipAsString(int length) {
+        assert length <= BLOCK_SIZE - position;
+        final int p = position;
+        position += length;
+        int availableLength = 0;
+        for (int i = 0; i < length; i++) {
+            if (buffer[p + i] == 0x00)
+                break;
+            ++availableLength;
+        }
+        StringBuilder s = new StringBuilder(length);
+        for (int i = 0; i < availableLength; i++) {
+            final byte b = buffer[p + i];
+            if (b < 0x20 || b > 0x7F)
+                s.append(String.format("\\%o", (b & 0xFF)));
+            else
+                s.append((char)b);
+        }
+        return s.toString();
+    }
+
+    private int clipAsInt(int length) {
+        final String s = clipAsNumberString(length);
+        return s.isEmpty() ? 0 : Integer.parseInt(s, 8);
+    }
+
+    private long clipAsLong(int length) {
+        final String s = clipAsNumberString(length);
+        return s.isEmpty() ? 0 : Long.parseLong(s, 8);
+    }
+
+    private String clipAsNumberString(int length) {
+        assert length <= BLOCK_SIZE - position;
+        final int p = position;
+        position += length;
+        int i = 0;
+        for (; i < length; i++) {
+            final byte b = buffer[p + i];
+            if (b == 0x00)
+                break;
+            assert b == 0x20 || b >= 0x30 && b <= 0x39;
+            boolean x = b == 0x20 || b >= 0x30 && b <= 0x39;
+            if (!x)
+                System.out.print("");
+        }
+        return (new String(buffer, p, i)).trim();
+    }
+
+    private int patch(int length, String value) {
+        final int p = position;
+        position += length;
+        byte[] data;
+        if (value == null || value.isEmpty())
+            data = new byte[0];
+        else
+            data = value.getBytes();
+        final int width = (data.length > length) ? length : data.length;
+        System.arraycopy(data, 0, buffer, p, width);
+        return data.length;
+    }
+
+    private int patch(int length, int value) {
+        final String s = padZero(Integer.toOctalString(value), length - 1);
+        return patch(length, s);
+    }
+
+    private int patch(int length, long value) {
+        final String s = padZero(Long.toOctalString(value), length - 1);
+        return patch(length, s);
+    }
+
+    private String padZero(String value, int length) {
+        final int valueLength = value.length();
+        if (valueLength >= length)
+            return value;
+        char[] buffer = new char[length];
+        int p = length - valueLength;
+        for (int i = 0; i < p; i++)
+            buffer[i] = '0';
+        for (int i = p; i < length; i++)
+            buffer[i] = value.charAt(i - p);
+        return String.valueOf(buffer);
+    }
+
+    private boolean isEmptyBlock() {
+        for (int i = 0; i < buffer.length; i++)
+            if (buffer[i] != 0x00)
+                return false;
+        return true;
+    }
+
+    static long getSkipSize(long size) {
+        if (size == 0 || size == BLOCK_SIZE || size % BLOCK_SIZE == 0)
+            return 0;
+        else
+            return BLOCK_SIZE - ((size > BLOCK_SIZE) ? size % BLOCK_SIZE : size);
+
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/tar/TarInputStream.java b/src/jp/sfjp/armadillo/archive/tar/TarInputStream.java
new file mode 100644 (file)
index 0000000..77976ca
--- /dev/null
@@ -0,0 +1,44 @@
+package jp.sfjp.armadillo.archive.tar;
+
+import java.io.*;
+import jp.sfjp.armadillo.archive.*;
+
+public final class TarInputStream extends ArchiveInputStream {
+
+    private TarHeader header;
+    private long skipSize;
+
+    public TarInputStream(InputStream is) {
+        super(is);
+        this.header = new TarHeader();
+        this.skipSize = 0;
+        frontStream = is;
+    }
+
+    public TarEntry getNextEntry() throws IOException {
+        ensureOpen();
+        if (remaining + skipSize > 0) {
+            remaining += skipSize;
+            while (remaining > 0) {
+                long skipped = in.skip(remaining);
+                remaining -= skipped;
+            }
+        }
+        remaining = 0;
+        skipSize = 0;
+        TarEntry entry = header.read(in);
+        if (entry == null)
+            return null;
+        long size = entry.getSize();
+        remaining = size;
+        skipSize = TarHeader.getSkipSize(size);
+        return entry;
+    }
+
+    @Override
+    public void close() throws IOException {
+        header = null;
+        super.close();
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/tar/TarOutputStream.java b/src/jp/sfjp/armadillo/archive/tar/TarOutputStream.java
new file mode 100644 (file)
index 0000000..d0c467e
--- /dev/null
@@ -0,0 +1,48 @@
+package jp.sfjp.armadillo.archive.tar;
+
+import java.io.*;
+import jp.sfjp.armadillo.archive.*;
+
+public final class TarOutputStream extends ArchiveOutputStream {
+
+    private TarHeader header;
+    private TarEntry nextEntry;
+
+    public TarOutputStream(OutputStream os) {
+        super(os);
+        this.header = new TarHeader();
+        frontStream = os;
+    }
+
+    public void putNextEntry(TarEntry entry) throws IOException {
+        ensureOpen();
+        nextEntry = entry;
+        header.write(this, entry);
+        written = 0;
+    }
+
+    public void closeEntry() throws IOException {
+        ensureOpen();
+        int skipSize = (int)TarHeader.getSkipSize(nextEntry.getSize());
+        write(new byte[skipSize]);
+        flush();
+        nextEntry = null;
+    }
+
+    @Override
+    public void close() throws IOException {
+        try {
+            header.writeEndOfArchive(this);
+        }
+        finally {
+            try {
+                super.close();
+            }
+            finally {
+                header = null;
+                nextEntry = null;
+            }
+        }
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/zip/DumpZipHeader.java b/src/jp/sfjp/armadillo/archive/zip/DumpZipHeader.java
new file mode 100644 (file)
index 0000000..07afe32
--- /dev/null
@@ -0,0 +1,334 @@
+package jp.sfjp.armadillo.archive.zip;
+
+import java.io.*;
+import java.nio.*;
+import java.nio.channels.*;
+import java.util.*;
+import java.util.zip.*;
+import jp.sfjp.armadillo.archive.*;
+import jp.sfjp.armadillo.io.*;
+
+/**
+ * Dump ZIP archive header.
+ */
+public final class DumpZipHeader extends DumpArchiveHeader {
+
+    private static final String fmt1 = "  * %s = %s%n";
+    private static final String fmt2 = "  %1$-16s = [0x%2$08X] ( %2$d )%n";
+    private static final int siglen = 4;
+    private static final int siglen_m1 = siglen - 1;
+
+    static final int SIGN_LOC = 0x04034B50;
+    static final int SIGN_CEN = 0x02014B50;
+    static final int SIGN_END = 0x06054B50;
+    static final int SIGN_EXT = 0x08074B50;
+    static final int LENGTH_LOC = 30;
+    static final int LENGTH_CEN = 46;
+    static final int LENGTH_END = 22;
+    static final int LENGTH_EXT = 16;
+
+    private final ByteBuffer buffer;
+
+    public DumpZipHeader() {
+        this.buffer = ByteBuffer.allocate(LENGTH_CEN).order(ByteOrder.LITTLE_ENDIAN);
+    }
+
+    @Override
+    public void dump(InputStream is, PrintWriter out) throws IOException {
+        final int bufferSize = 65536;
+        byte[] bytes = new byte[bufferSize];
+        int p = 0;
+        RewindableInputStream pis = new RewindableInputStream(is, bufferSize);
+        while (true) {
+            Arrays.fill(bytes, (byte)0);
+            final int readSize = pis.read(bytes);
+            if (readSize <= 0)
+                break;
+            final int offset = findPK(bytes);
+            if (offset < 0) {
+                if (readSize < siglen)
+                    break;
+                pis.rewind(siglen_m1);
+                p += readSize - siglen_m1;
+                continue;
+            }
+            if (offset == 0)
+                pis.rewind(readSize);
+            else if (offset < bufferSize - siglen)
+                pis.rewind(readSize - offset);
+            p += offset;
+            printOffset(out, p);
+            ByteBuffer buffer = ByteBuffer.wrap(bytes, offset, siglen)
+                                          .order(ByteOrder.LITTLE_ENDIAN);
+            final int len;
+            final int signature = buffer.getInt();
+            switch (signature) {
+                case SIGN_LOC:
+                    len = readLOC(pis, out);
+                    break;
+                case SIGN_CEN:
+                    len = readCEN(pis, out);
+                    break;
+                case SIGN_END:
+                    len = readEND(pis, out);
+                    break;
+                case SIGN_EXT:
+                    len = readEXT(pis, out);
+                    break;
+                default:
+                    warn(out, "[%08X] is not a signature", signature);
+                    len = 0;
+            }
+            assert len >= 0;
+            if (len > 0)
+                p += len;
+            else {
+                pis.read();
+                ++p;
+            }
+        }
+        printEnd(out, "ZIP", p);
+    }
+
+    static int findPK(byte[] bytes) {
+        // PK=0x504B
+        final int n = bytes.length - siglen_m1;
+        for (int i = 0; i < n; i++)
+            if (bytes[i] == 0x50)
+                if (bytes[i + 1] == 0x4B)
+                    return i;
+        return -1;
+    }
+
+    int readLOC(InputStream is, PrintWriter out) throws IOException {
+        int readSize = 0;
+        // Local file header (LOC)
+        final int signature; // local file header signature
+        final short version; // version needed to extract
+        final short flags; // general purpose bit flag
+        final short method; // compression method
+        final short mtime; // last mod file time
+        final short mdate; // last mod file date
+        final int crc; // crc-32
+        final int compsize; // compressed size
+        final int uncompsize; // uncompressed size
+        final short namelen; // file name length
+        final short extlen; // extra field length
+        buffer.clear();
+        buffer.limit(LENGTH_LOC);
+        Channels.newChannel(is).read(buffer);
+        readSize += buffer.position();
+        buffer.rewind();
+        signature = buffer.getInt();
+        version = buffer.getShort();
+        flags = buffer.getShort();
+        method = buffer.getShort();
+        mtime = buffer.getShort();
+        mdate = buffer.getShort();
+        crc = buffer.getInt();
+        compsize = buffer.getInt();
+        uncompsize = buffer.getInt();
+        namelen = buffer.getShort();
+        extlen = buffer.getShort();
+        final String name;
+        assert namelen >= 0;
+        if (namelen == 0)
+            name = "";
+        else { // if (namelen > 0)
+            byte[] nameBuffer = new byte[namelen];
+            final int read = is.read(nameBuffer);
+            name = new String(nameBuffer);
+            if (read != namelen)
+                warn(out, "namelen=%d, read=%d, name=%s", namelen, read, name);
+            readSize += read;
+        }
+        if (extlen > 0) {
+            final long read = is.skip(extlen);
+            if (read != extlen)
+                throw new ZipException("invalid LOC header (extra length)");
+            readSize += read;
+        }
+        printHeaderName(out, "LOC header");
+        out.printf(fmt1, "name", name);
+        out.printf(fmt1, "mtime as date", toDate(mdate, mtime));
+        out.printf(fmt2, "signature", signature);
+        out.printf(fmt2, "version", version);
+        out.printf(fmt2, "flags", flags);
+        out.printf(fmt2, "method", method);
+        out.printf(fmt2, "mtime", mtime);
+        out.printf(fmt2, "mdate", mdate);
+        out.printf(fmt2, "crc", crc);
+        out.printf(fmt2, "compsize", compsize);
+        out.printf(fmt2, "uncompsize", uncompsize);
+        out.printf(fmt2, "namelen", namelen);
+        out.printf(fmt2, "extlen", extlen);
+        return readSize;
+    }
+
+    int readCEN(InputStream is, PrintWriter out) throws IOException {
+        int readSize = 0;
+        // Central file header (CEN)
+        final int signature; // central file header signature
+        final short madever; // version made by
+        final short needver; // version needed to extract
+        final short flags; // general purpose bit flag
+        final short method; // compression method
+        final short mtime; // last mod file time
+        final short mdate; // last mod file date
+        final int crc; // crc-32
+        final int compsize; // compressed size
+        final int uncompsize; // uncompressed size
+        final short namelen; // file name length
+        final short extlen; // extra field length
+        final short fcmlen; // file comment length
+        final short dnum; // disk number start
+        final short inattr; // internal file attributes
+        final int exattr; // external file attributes
+        final int reloff; // relative offset of local header
+        buffer.clear();
+        buffer.limit(LENGTH_CEN);
+        Channels.newChannel(is).read(buffer);
+        readSize += buffer.position();
+        if (buffer.position() == 0)
+            return readSize;
+        buffer.rewind();
+        signature = buffer.getInt();
+        madever = buffer.getShort();
+        needver = buffer.getShort();
+        flags = buffer.getShort();
+        method = buffer.getShort();
+        mtime = buffer.getShort();
+        mdate = buffer.getShort();
+        crc = buffer.getInt();
+        compsize = buffer.getInt();
+        uncompsize = buffer.getInt();
+        namelen = buffer.getShort();
+        extlen = buffer.getShort();
+        fcmlen = buffer.getShort();
+        dnum = buffer.getShort();
+        inattr = buffer.getShort();
+        exattr = buffer.getInt();
+        reloff = buffer.getInt();
+        final String name;
+        if (namelen > 0) {
+            final int reallen = namelen;
+            byte[] nameBuffer = new byte[reallen];
+            final int read = is.read(nameBuffer);
+            name = new String(nameBuffer);
+            if (read != namelen)
+                warn(out, "namelen=%d, read=%d, name=%s", namelen, read, name);
+        }
+        else
+            name = "";
+        if (extlen > 0) {
+            final long read = is.skip(extlen);
+            if (read != extlen)
+                throw new ZipException("invalid CEN header (extra length)");
+            readSize += read;
+        }
+        if (fcmlen > 0)
+            System.out.println();
+        printHeaderName(out, "CEN header");
+        out.printf(fmt1, "name", name);
+        out.printf(fmt1, "mtime as date", toDate(mdate, mtime));
+        out.printf(fmt2, "signature", signature);
+        out.printf(fmt2, "madever", madever);
+        out.printf(fmt2, "needver", needver);
+        out.printf(fmt2, "flags", flags);
+        out.printf(fmt2, "method", method);
+        out.printf(fmt2, "mtime", mtime);
+        out.printf(fmt2, "mdate", mdate);
+        out.printf(fmt2, "crc", crc);
+        out.printf(fmt2, "compsize", compsize);
+        out.printf(fmt2, "uncompsize", uncompsize);
+        out.printf(fmt2, "namelen", namelen);
+        out.printf(fmt2, "extlen", extlen);
+        out.printf(fmt2, "fcmlen", fcmlen);
+        out.printf(fmt2, "dnum", dnum);
+        out.printf(fmt2, "infattr", inattr);
+        out.printf(fmt2, "exfattr", exattr);
+        out.printf(fmt2, "reloff", reloff);
+        return readSize;
+    }
+
+    int readEND(InputStream is, PrintWriter out) throws IOException {
+        int readSize = 0;
+        // End of central dir header (END)
+        final int signature; // end of central dir signature
+        final short ndisk; // number of this disk
+        final short ndiskCEN; // number of the disk with the start of the central directory
+        final short countDiskCENs; // total number of entries in the central directory on this disk
+        final short countCENs; // total number of entries in the central directory
+        final int sizeCENs; // size of the central directory
+        final int offsetCENs; // offset of start of central directory with respect to the starting disk number
+        final short commentlen; // .ZIP file comment length
+        final byte[] comment; // .ZIP file comment
+        buffer.clear();
+        buffer.limit(LENGTH_END);
+        Channels.newChannel(is).read(buffer);
+        readSize += buffer.position();
+        buffer.rewind();
+        signature = buffer.getInt();
+        ndisk = buffer.getShort();
+        ndiskCEN = buffer.getShort();
+        countDiskCENs = buffer.getShort();
+        countCENs = buffer.getShort();
+        sizeCENs = buffer.getInt();
+        offsetCENs = buffer.getInt();
+        commentlen = buffer.getShort();
+        final String commentString;
+        if (commentlen > 0) {
+            comment = new byte[commentlen];
+            final int read = is.read(comment);
+            if (read != commentlen)
+                throw new ZipException("invalid END header (comment length)");
+            readSize += read;
+            commentString = new String(comment);
+        }
+        else
+            commentString = "";
+        printHeaderName(out, "END header");
+        out.printf(fmt2, "signature", signature);
+        out.printf(fmt2, "ndisk", ndisk);
+        out.printf(fmt2, "ndiskCEN", ndiskCEN);
+        out.printf(fmt2, "countDiskCENs", countDiskCENs);
+        out.printf(fmt2, "countCENs", countCENs);
+        out.printf(fmt2, "sizeCENs", sizeCENs);
+        out.printf(fmt2, "offsetCENs", offsetCENs);
+        out.printf(fmt2, "commentlen", commentlen);
+        out.printf(fmt1, "comment", commentString);
+        return readSize;
+    }
+
+    int readEXT(InputStream is, PrintWriter out) throws IOException {
+        int readSize = 0;
+        // Extend header (EXT)
+        final int signature; // extend header signature
+        final int crc; // crc-32
+        final int compsize; // compressed size
+        final int uncompsize; // uncompressed size
+        buffer.clear();
+        buffer.limit(LENGTH_EXT);
+        Channels.newChannel(is).read(buffer);
+        readSize += buffer.position();
+        buffer.rewind();
+        signature = buffer.getInt();
+        crc = buffer.getInt();
+        compsize = buffer.getInt();
+        uncompsize = buffer.getInt();
+        printHeaderName(out, "EXT header");
+        out.printf(fmt2, "signature", signature);
+        out.printf(fmt2, "crc", crc);
+        out.printf(fmt2, "compsize", compsize);
+        out.printf(fmt2, "uncompsize", uncompsize);
+        return readSize;
+    }
+
+    static Date toDate(short mdate, short mtime) {
+        ZipEntry entry = new ZipEntry();
+        entry.mdate = mdate;
+        entry.mtime = mtime;
+        return new Date(entry.getLastModified());
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/zip/ZipArchiveCreator.java b/src/jp/sfjp/armadillo/archive/zip/ZipArchiveCreator.java
new file mode 100644 (file)
index 0000000..b999c67
--- /dev/null
@@ -0,0 +1,77 @@
+package jp.sfjp.armadillo.archive.zip;
+
+import java.io.*;
+import jp.sfjp.armadillo.archive.*;
+import jp.sfjp.armadillo.io.*;
+
+public final class ZipArchiveCreator implements ArchiveCreator {
+
+    private ZipOutputStream os;
+
+    public ZipArchiveCreator(OutputStream os) {
+        this.os = new ZipOutputStream(os);
+    }
+
+    @Override
+    public ArchiveEntry newEntry(String name) {
+        return new ZipEntry(name);
+    }
+
+    @Override
+    public void addEntry(ArchiveEntry entry, File file) throws IOException {
+        if (file.isDirectory()) {
+            os.putNextEntry(toZipEntry(entry));
+            os.closeEntry();
+            entry.setAdded(true);
+        }
+        else
+            addEntry(entry, file, null, file.length());
+    }
+
+    @Override
+    public void addEntry(ArchiveEntry entry, InputStream is, long length) throws IOException {
+        addEntry(entry, null, is, length);
+    }
+
+    private void addEntry(ArchiveEntry entry, File file, InputStream src, final long length) throws IOException {
+        ZipEntry fileEntry = toZipEntry(entry);
+        fileEntry.setSize(length);
+        if (length == 0) {
+            fileEntry.setCompressedSize(0L);
+            fileEntry.method = ZipEntry.STORED;
+        }
+        os.putNextEntry(fileEntry);
+        if (length > 0) {
+            final long size;
+            InputStream is = (src == null) ? new FileInputStream(file) : src;
+            try {
+                size = IOUtilities.transfer(is, os, length);
+            }
+            finally {
+                if (src == null)
+                    is.close();
+            }
+            assert size == fileEntry.getSize() : "file size";
+            assert size == length : "file size";
+            entry.setSize(size);
+        }
+        os.closeEntry();
+        entry.setAdded(true);
+    }
+
+    static ZipEntry toZipEntry(ArchiveEntry entry) {
+        if (entry instanceof ZipEntry)
+            return (ZipEntry)entry;
+        ZipEntry newEntry = new ZipEntry(entry.getName());
+        entry.copyTo(newEntry);
+        if (newEntry.isDirectory())
+            newEntry.method = ZipEntry.STORED;
+        return newEntry;
+    }
+
+    @Override
+    public void close() throws IOException {
+        os.close();
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/zip/ZipArchiveExtractor.java b/src/jp/sfjp/armadillo/archive/zip/ZipArchiveExtractor.java
new file mode 100644 (file)
index 0000000..0d2928f
--- /dev/null
@@ -0,0 +1,30 @@
+package jp.sfjp.armadillo.archive.zip;
+
+import java.io.*;
+import jp.sfjp.armadillo.archive.*;
+import jp.sfjp.armadillo.io.*;
+
+public final class ZipArchiveExtractor implements ArchiveExtractor {
+
+    private ZipInputStream is;
+
+    public ZipArchiveExtractor(InputStream is) {
+        this.is = new ZipInputStream(is);
+    }
+
+    @Override
+    public ArchiveEntry nextEntry() throws IOException {
+        return ArchiveEntry.orNull(is.getNextEntry());
+    }
+
+    @Override
+    public long extract(OutputStream os) throws IOException {
+        return IOUtilities.transferAll(is, os);
+    }
+
+    @Override
+    public void close() throws IOException {
+        is.close();
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/zip/ZipEndEntry.java b/src/jp/sfjp/armadillo/archive/zip/ZipEndEntry.java
new file mode 100644 (file)
index 0000000..22da8d0
--- /dev/null
@@ -0,0 +1,70 @@
+package jp.sfjp.armadillo.archive.zip;
+
+import java.io.*;
+import java.util.*;
+import java.util.zip.*;
+import jp.sfjp.armadillo.io.*;
+
+final class ZipEndEntry {
+
+    int signature;
+    short disknum;
+    short disknumCEN;
+    short countDiskCENs;
+    short countCENs;
+    int sizeCENs;
+    int offsetStartCEN;
+    short commentlen;
+    byte[] comment;
+
+    static ZipEndEntry create(long offset, List<ZipEntry> entries) throws IOException {
+        // End of central dir header (END)
+        final int signature; // end of central dir signature
+        final short disknum; // number of this disk
+        final short disknumCEN; // number of the disk with the start of the central directory
+        final short countDiskCENs; // total number of entries in the central directory on this disk
+        final short countCENs; // total number of entries in the central directory
+        final int sizeCENs; // size of the central directory
+        final int offsetStartCEN; // offset of start of central directory with respect to the starting disk number
+        final short commentlen; // .ZIP file comment length
+        final byte[] comment; // .ZIP file comment
+        // ---
+        final int iEntryCount = entries.size();
+        if (iEntryCount > 0xFFFF)
+            throw new ZipException("overflow: entry-count=" + iEntryCount);
+        final short entryCount = (short)(iEntryCount & 0xFFFF);
+        if (offset > 0xFFFFFFFFL)
+            throw new ZipException("overflow: offset=" + offset);
+        assert offset >= 0L;
+        final long p = offset;
+        ZipHeader header = new ZipHeader();
+        VolumetricOutputStream vos = new VolumetricOutputStream();
+        for (final ZipEntry entry : entries)
+            header.writeCEN(vos, entry);
+        final long lsizeCENs = vos.getSize();
+        if (lsizeCENs > 0xFFFFFFFFL)
+            throw new ZipException("overflow: size of CEN headers=" + lsizeCENs);
+        // ... prepared
+        signature = ZipHeader.SIGN_END;
+        disknum = 0; // not supported
+        disknumCEN = 0; // not supported
+        countDiskCENs = entryCount; // ignore disk number
+        countCENs = entryCount;
+        sizeCENs = (int)(lsizeCENs & 0xFFFFFFFFL);
+        offsetStartCEN = (int)(p & 0xFFFFFFFF);
+        commentlen = 0; // not supported
+        comment = new byte[0]; // not supported
+        ZipEndEntry entry = new ZipEndEntry();
+        entry.signature = signature;
+        entry.disknum = disknum;
+        entry.disknumCEN = disknumCEN;
+        entry.countDiskCENs = countDiskCENs;
+        entry.countCENs = countCENs;
+        entry.sizeCENs = sizeCENs;
+        entry.offsetStartCEN = offsetStartCEN;
+        entry.commentlen = commentlen;
+        entry.comment = comment;
+        return entry;
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/zip/ZipEntry.java b/src/jp/sfjp/armadillo/archive/zip/ZipEntry.java
new file mode 100644 (file)
index 0000000..b82cbfe
--- /dev/null
@@ -0,0 +1,127 @@
+package jp.sfjp.armadillo.archive.zip;
+
+import java.io.*;
+import jp.sfjp.armadillo.archive.*;
+import jp.sfjp.armadillo.time.*;
+
+/**
+ * ZIP archive entry.
+ */
+public final class ZipEntry extends ArchiveEntry {
+
+    /**
+     * <code>STORED</code>
+     */
+    public static final short STORED = 0;
+
+    /**
+     * <code>DEFLATED</code>
+     */
+    public static final short DEFLATED = 8;
+
+    private static final long UINT_MAX = 0xFFFFFFFFL;
+    private static final long USHORT_MAX = 0xFFFFL;
+    private static final FTime FTIME = new FTime();
+
+    // state
+    boolean extOverwritten;
+    int reloff;
+    // header
+    int signature;
+    short version;
+    short flags;
+    short method;
+    short mtime;
+    short mdate;
+    int crc;
+    int compsize;
+    int uncompsize;
+    short extlen;
+
+    public ZipEntry() {
+        super(false);
+        this.version = 10;
+        this.flags = 0;
+        this.method = DEFLATED;
+        this.mdate = 0;
+        this.mtime = 0;
+        this.crc = -1;
+        this.compsize = 0;
+        this.uncompsize = 0;
+    }
+
+    public ZipEntry(String name) {
+        this();
+        setName(name);
+    }
+
+    public ZipEntry(String name, File file) {
+        this(name);
+        setFileInfo(file);
+    }
+
+    public boolean hasEXT() {
+        return (flags & 8) == 8;
+    }
+
+    public boolean isOnePassMode() {
+        return crc == -1;
+    }
+
+    @Override
+    public long getSize() {
+        if (isDirectory())
+            return 0;
+        return uncompsize & UINT_MAX;
+    }
+
+    @Override
+    public void setSize(long size) {
+        if (size > UINT_MAX)
+            throw new IllegalArgumentException("max size is int.max: " + size);
+        this.uncompsize = (int)(size & UINT_MAX);
+    }
+
+    @Override
+    public long getCompressedSize() {
+        if (isDirectory())
+            return 0;
+        return compsize & UINT_MAX;
+    }
+
+    @Override
+    public void setCompressedSize(long compressedSize) {
+        if (compressedSize > UINT_MAX)
+            throw new IllegalArgumentException("max size is int-max: " + compressedSize);
+        this.compsize = (int)(compressedSize & UINT_MAX);
+    }
+
+    @Override
+    public long getLastModified() {
+        return FTIME.toMillisecond(mdate, mtime);
+    }
+
+    @Override
+    public void setLastModified(long lastModified) {
+        final int ftime = FTIME.int32From(lastModified);
+        this.mdate = (short)((ftime >> 16) & USHORT_MAX);
+        this.mtime = (short)(ftime & USHORT_MAX);
+    }
+
+    @Override
+    public String getMethodName() {
+        return getMethodName(method);
+    }
+
+    private static String getMethodName(int method) {
+        switch (method) {
+            case ZipEntry.DEFLATED:
+                return "DEFLATED";
+            case ZipEntry.STORED:
+                return "STORED";
+            default:
+                return String.valueOf(method);
+        }
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/zip/ZipFile.java b/src/jp/sfjp/armadillo/archive/zip/ZipFile.java
new file mode 100644 (file)
index 0000000..6e7cce2
--- /dev/null
@@ -0,0 +1,234 @@
+package jp.sfjp.armadillo.archive.zip;
+
+import java.io.*;
+import java.nio.channels.*;
+import java.util.*;
+import java.util.zip.*;
+import jp.sfjp.armadillo.archive.*;
+import jp.sfjp.armadillo.archive.tar.*;
+import jp.sfjp.armadillo.io.*;
+
+public final class ZipFile extends ArchiveFile {
+
+    private File afile;
+    private ZipHeader header;
+    private RandomAccessFile raf;
+    private ZipEntry ongoingEntry;
+    private byte[] buffer;
+    private ZipEndEntry endEntry;
+
+    public ZipFile(File afile) {
+        this.afile = afile;
+        this.header = new ZipHeader();
+    }
+
+    public ZipFile(File afile, boolean withOpen) throws IOException {
+        this(afile);
+        open();
+    }
+
+    @Override
+    public void open() throws IOException {
+        if (raf != null)
+            throw new IOException("the file has been already opened");
+        if (!afile.exists())
+            afile.createNewFile();
+        this.raf = new RandomAccessFile(afile, "rw");
+        this.opened = true;
+    }
+
+    @Override
+    public void reset() throws IOException {
+        if (raf.length() == 0)
+            endEntry = new ZipEndEntry();
+        else if ((endEntry = readEndHeader()) == null)
+            throw new IllegalStateException("failed to read END header");
+        ongoingEntry = null;
+        currentPosition = endEntry.offsetStartCEN;
+        raf.seek(currentPosition);
+    }
+
+    @Override
+    public ArchiveEntry nextEntry() throws IOException {
+        ensureOpen();
+        if (ongoingEntry == null)
+            reset();
+        else {
+            long cenLength = 0L;
+            cenLength += ZipHeader.LENGTH_CEN;
+            cenLength += ongoingEntry.getNameAsBytes().length;
+            cenLength += ongoingEntry.extlen;
+            currentPosition += cenLength;
+        }
+        raf.seek(currentPosition);
+        ongoingEntry = header.readCEN(Channels.newInputStream(raf.getChannel()));
+        return (ongoingEntry == null) ? ArchiveEntry.NULL : ongoingEntry;
+    }
+
+    @Override
+    public ArchiveEntry newEntry(String name) {
+        return new ZipEntry(name);
+    }
+
+    @Override
+    public void addEntry(ArchiveEntry entry, InputStream is, long length) throws IOException {
+        reset();
+        List<ZipEntry> entries = getEntries();
+        ZipEntry newEntry = new ZipEntry(entry.getName());
+        newEntry.copyFrom(entry);
+        // overwrite new entry, CEN and END
+        raf.seek(endEntry.offsetStartCEN);
+        ZipOutputStream zos = new ZipOutputStream(Channels.newOutputStream(raf.getChannel()));
+        zos.putNextEntry(newEntry);
+        if (length > 0) {
+            final long written = IOUtilities.transfer(is, zos, length);
+            assert written == length;
+            newEntry.setCompressedSize(written);
+            newEntry.setSize(length);
+        }
+        zos.closeEntry();
+        zos.flush();
+        final long p = raf.getFilePointer();
+        raf.seek(endEntry.offsetStartCEN);
+        newEntry.flags &= 0xFFF7;
+        zos.putNextEntry(newEntry);
+        newEntry.reloff = endEntry.offsetStartCEN;
+        entries.add(newEntry);
+        endEntry.countCENs = (short)entries.size();
+        raf.seek(p);
+        writeEnd(entries);
+    }
+
+    @Override
+    public void updateEntry(ArchiveEntry entry, InputStream is, long length) throws IOException {
+        super.updateEntry(entry, is, length);
+    }
+
+    @Override
+    public void removeEntry(ArchiveEntry entry) throws IOException {
+        if (!seek(entry))
+            throw new TarException("entry " + entry + " not found");
+        assert ongoingEntry != null;
+        ZipEntry target = this.ongoingEntry;
+        ZipEndEntry endEntry = this.endEntry;
+        final long offset = target.reloff;
+        List<ZipEntry> entries = getEntries(); // reset offset
+        raf.seek(offset);
+        ZipInputStream zis = new ZipInputStream(Channels.newInputStream(raf.getChannel()));
+        ZipEntry locEntry = zis.getNextEntry();
+        zis.skip(locEntry.uncompsize);
+        zis.closeEntry();
+        final long totalLength = getOffset() - offset;
+        endEntry.offsetStartCEN -= totalLength;
+        truncate(offset, totalLength);
+        for (ZipEntry zipEntry : entries)
+            if (zipEntry.equalsName(target)) {
+                entries.remove(zipEntry);
+                break;
+            }
+        for (ZipEntry zipEntry : entries)
+            if (zipEntry.reloff > offset)
+                zipEntry.reloff -= totalLength;
+        raf.seek(endEntry.offsetStartCEN);
+        this.endEntry = endEntry;
+        writeEnd(entries);
+    }
+
+    void writeEnd(List<ZipEntry> entries) throws IOException {
+        ZipEndEntry endEntry = ZipEndEntry.create(getOffset(), entries);
+        OutputStream os = Channels.newOutputStream(raf.getChannel());
+        for (ZipEntry entry : entries)
+            header.writeCEN(os, entry);
+        header.writeEND(os, endEntry);
+        if (raf.length() > raf.getFilePointer())
+            raf.setLength(raf.getFilePointer());
+    }
+
+    public void insertEmptySpace(long offset, long blockCount) throws IOException {
+        final int blockSize = 8192;
+        final long insertLength = blockCount * blockSize;
+        raf.seek(raf.length() + insertLength - 1);
+        raf.write(0);
+        final long x = raf.getFilePointer();
+        long p2 = x - blockSize; // last block
+        long p1 = p2 - insertLength;
+        assert p1 > 0 && p2 > 0;
+        byte[] buffer = this.buffer;
+        for (int i = 0; i < blockCount && p1 >= offset; i++) {
+            raf.seek(p1);
+            raf.readFully(buffer);
+            raf.seek(p2);
+            raf.write(buffer);
+            p1 -= blockSize;
+            p2 -= blockSize;
+        }
+        raf.seek(currentPosition = offset);
+    }
+
+    @Override
+    public long extract(OutputStream os) throws IOException {
+        raf.seek(ongoingEntry.reloff);
+        InputStream is = Channels.newInputStream(raf.getChannel());
+        ZipEntry entry = header.read(is);
+        assert entry != null;
+        entry.compsize = ongoingEntry.compsize;
+        entry.uncompsize = ongoingEntry.uncompsize;
+        entry.flags &= (8 ^ 0xFFFF);
+        ZipInputStream zis = new ZipInputStream(is);
+        zis.openEntry(entry);
+        return IOUtilities.transfer(zis, os, ongoingEntry.uncompsize);
+    }
+
+    int getOffset() throws IOException {
+        final long offset = raf.getFilePointer();
+        if (offset > Integer.MAX_VALUE)
+            throw new ZipException("size not supported: " + offset);
+        return (int)offset;
+    }
+
+    List<ZipEntry> getEntries() {
+        List<ZipEntry> entries = new ArrayList<ZipEntry>();
+        for (ArchiveEntry entry : this)
+            entries.add((ZipEntry)entry);
+        return entries;
+    }
+
+    ZipEndEntry readEndHeader() throws IOException {
+        final int commentlen = 0;
+        raf.seek(raf.length() - 22 - commentlen);
+        return header.readEND(Channels.newInputStream(raf.getChannel()));
+    }
+
+    void truncate(final long offset, final long length) throws IOException {
+        final int blockSize = 8192;
+        long p2 = offset;
+        long p1 = p2 + length;
+        byte[] bytes = new byte[blockSize];
+        while (true) {
+            raf.seek(p1);
+            final int r = raf.read(bytes);
+            if (r <= 0)
+                break;
+            raf.seek(p2);
+            raf.write(bytes, 0, r);
+            p2 += r;
+            p1 += r;
+        }
+        raf.setLength(raf.length() - length);
+        reset();
+    }
+
+    @Override
+    public void close() throws IOException {
+        try {
+            if (raf != null)
+                raf.close();
+        }
+        finally {
+            super.close();
+            afile = null;
+            header = null;
+        }
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/zip/ZipHeader.java b/src/jp/sfjp/armadillo/archive/zip/ZipHeader.java
new file mode 100644 (file)
index 0000000..55925bc
--- /dev/null
@@ -0,0 +1,442 @@
+package jp.sfjp.armadillo.archive.zip;
+
+import static java.lang.String.format;
+import java.io.*;
+import java.nio.*;
+import java.nio.channels.*;
+import java.util.*;
+import java.util.zip.*;
+
+/**
+ * ZIP archive header.
+ */
+public final class ZipHeader {
+
+    static final int SIGN_LOC = 0x04034B50;
+    static final int SIGN_CEN = 0x02014B50;
+    static final int SIGN_END = 0x06054B50;
+    static final int SIGN_EXT = 0x08074B50;
+    static final int LENGTH_LOC = 30;
+    static final int LENGTH_CEN = 46;
+    static final int LENGTH_END = 22;
+    static final int LENGTH_EXT = 16;
+
+    private final ByteBuffer buffer;
+    private final List<ZipEntry> entries;
+    private long offset;
+
+    public ZipHeader() {
+        this.buffer = ByteBuffer.allocate(LENGTH_CEN).order(ByteOrder.LITTLE_ENDIAN);
+        this.entries = new ArrayList<ZipEntry>();
+        this.offset = 0L;
+    }
+
+    public ZipEntry read(InputStream is) throws IOException {
+        return readLOC(is);
+    }
+
+    public ZipEntry readLOC(InputStream is) throws IOException {
+        // Local file header (LOC)
+        final int signature; // local file header signature
+        final short version; // version needed to extract
+        final short flags; // general purpose bit flag
+        final short method; // compression method
+        final short mtime; // last mod file time
+        final short mdate; // last mod file date
+        final int crc; // crc-32
+        final int compsize; // compressed size
+        final int uncompsize; // uncompressed size
+        final short namelen; // file name length
+        final short extlen; // extra field length
+        final byte[] nameb; // file name
+        // ---
+        buffer.clear();
+        buffer.limit(LENGTH_LOC);
+        Channels.newChannel(is).read(buffer);
+        if (buffer.position() == 0)
+            return null;
+        buffer.rewind();
+        signature = buffer.getInt();
+        if (signature == SIGN_CEN || signature == SIGN_END)
+            return null;
+        if (signature != SIGN_LOC)
+            throw new ZipException(format("invalid LOC header: signature=0x%X", signature));
+        version = buffer.getShort();
+        flags = buffer.getShort();
+        method = buffer.getShort();
+        mtime = buffer.getShort();
+        mdate = buffer.getShort();
+        crc = buffer.getInt();
+        compsize = buffer.getInt();
+        uncompsize = buffer.getInt();
+        namelen = buffer.getShort();
+        extlen = buffer.getShort();
+        nameb = new byte[namelen];
+        if (is.read(nameb) != namelen)
+            throw new ZipException("invalid LOC header (name length)");
+        if (extlen > 0)
+            if (is.skip(extlen) != extlen)
+                throw new ZipException("invalid LOC header (extra length)");
+        ZipEntry entry = new ZipEntry();
+        entry.signature = signature;
+        entry.version = version;
+        entry.flags = flags;
+        entry.method = method;
+        entry.mdate = mdate;
+        entry.mtime = mtime;
+        entry.crc = crc;
+        entry.compsize = compsize;
+        entry.uncompsize = uncompsize;
+        entry.setName(nameb);
+        return entry;
+    }
+
+    @SuppressWarnings("unused")
+    public ZipEntry readCEN(InputStream is) throws IOException {
+        // Central file header (CEN)
+        final int signature; // central file header signature
+        final short madever; // version made by
+        final short needver; // version needed to extract
+        final short flags; // general purpose bit flag
+        final short method; // compression method
+        final short mtime; // last mod file time
+        final short mdate; // last mod file date
+        final int crc; // crc-32
+        final int compsize; // compressed size
+        final int uncompsize; // uncompressed size
+        final short namelen; // file name length
+        final short extlen; // extra field length
+        final short fcmlen; // file comment length
+        final short dnum; // disk number start
+        final short infattr; // internal file attributes
+        final int exfattr; // external file attributes
+        final int reloff; // relative offset of local header
+        final byte[] nameb; // file name
+        // ---
+        buffer.clear();
+        buffer.limit(LENGTH_CEN);
+        Channels.newChannel(is).read(buffer);
+        if (buffer.position() == 0)
+            return null;
+        buffer.rewind();
+        signature = buffer.getInt();
+        if (signature == SIGN_END)
+            return null;
+        else if (signature != SIGN_CEN)
+            throw new ZipException(format("invalid CEN header: signature=0x%X", signature));
+        madever = buffer.getShort();
+        needver = buffer.getShort();
+        flags = buffer.getShort();
+        method = buffer.getShort();
+        mtime = buffer.getShort();
+        mdate = buffer.getShort();
+        crc = buffer.getInt();
+        compsize = buffer.getInt();
+        uncompsize = buffer.getInt();
+        namelen = buffer.getShort();
+        extlen = buffer.getShort();
+        fcmlen = buffer.getShort();
+        dnum = buffer.getShort();
+        infattr = buffer.getShort();
+        exfattr = buffer.getInt();
+        reloff = buffer.getInt();
+        nameb = new byte[namelen];
+        if (is.read(nameb) != namelen)
+            throw new ZipException("invalid LOC header (name length)");
+        if (extlen > 0)
+            if (is.skip(extlen) != extlen)
+                throw new ZipException("invalid LOC header (extra length)");
+        ZipEntry entry = new ZipEntry();
+        entry.signature = signature;
+        entry.flags = flags;
+        entry.method = method;
+        entry.mtime = mtime;
+        entry.mdate = mdate;
+        entry.crc = crc;
+        entry.compsize = compsize;
+        entry.uncompsize = uncompsize;
+        entry.extlen = extlen;
+        entry.reloff = reloff;
+        entry.setName(nameb);
+        return entry;
+    }
+
+    public ZipEndEntry readEND(InputStream is) throws IOException {
+        // End of central dir header (END)
+        final int signature; // end of central dir signature
+        final short disknum; // number of this disk
+        final short disknumCEN; // number of the disk with the start of the central directory
+        final short countDiskCENs; // total number of entries in the central directory on this disk
+        final short countCENs; // total number of entries in the central directory
+        final int sizeCENs; // size of the central directory
+        final int offsetStartCEN; // offset of start of central directory with respect to the starting disk number
+        final short commentlen; // .ZIP file comment length
+        final byte[] comment; // .ZIP file comment
+        // ---
+        buffer.clear();
+        buffer.limit(LENGTH_END);
+        Channels.newChannel(is).read(buffer);
+        if (buffer.position() == 0)
+            return null;
+        buffer.rewind();
+        signature = buffer.getInt();
+        if (signature != SIGN_END)
+            return null;
+        disknum = buffer.getShort();
+        disknumCEN = buffer.getShort();
+        countDiskCENs = buffer.getShort();
+        countCENs = buffer.getShort();
+        sizeCENs = buffer.getInt();
+        offsetStartCEN = buffer.getInt();
+        commentlen = buffer.getShort();
+        comment = new byte[commentlen];
+        if (commentlen > 0)
+            if (is.skip(commentlen) != commentlen)
+                throw new ZipException("invalid END header (comment length)");
+        ZipEndEntry entry = new ZipEndEntry();
+        entry.signature = signature;
+        entry.disknum = disknum;
+        entry.disknumCEN = disknumCEN;
+        entry.countDiskCENs = countDiskCENs;
+        entry.countCENs = countCENs;
+        entry.sizeCENs = sizeCENs;
+        entry.offsetStartCEN = offsetStartCEN;
+        entry.commentlen = commentlen;
+        entry.comment = comment;
+        return entry;
+    }
+
+    public ZipEntry readEXT(InputStream is) throws IOException {
+        ZipEntry entry = new ZipEntry();
+        readEXT(is, entry);
+        return entry;
+    }
+
+    public boolean readEXT(InputStream is, ZipEntry entry) throws IOException {
+        // Extend header (EXT)
+        final int signature; // extend header signature
+        final int crc; // crc-32
+        final int compsize; // compressed size
+        final int uncompsize; // uncompressed size
+        // ---
+        buffer.clear();
+        buffer.limit(LENGTH_EXT);
+        Channels.newChannel(is).read(buffer);
+        if (buffer.position() == 0)
+            return false;
+        buffer.rewind();
+        signature = buffer.getInt();
+        if (signature != SIGN_EXT)
+            throw new ZipException(format("invalid EXT header: signature=0x%X", signature));
+        crc = buffer.getInt();
+        compsize = buffer.getInt();
+        uncompsize = buffer.getInt();
+        entry.extOverwritten = true;
+        entry.crc = crc;
+        entry.compsize = compsize;
+        entry.uncompsize = uncompsize;
+        return true;
+    }
+
+    public void write(OutputStream os, ZipEntry entry) throws IOException {
+        writeLOC(os, entry);
+    }
+
+    public void writeLOC(OutputStream os, ZipEntry entry) throws IOException {
+        // Local file header (LOC)
+        final int signature; // local file header signature
+        final short version; // version needed to extract
+        final short flags; // general purpose bit flag
+        final short method; // compression method
+        final short mtime; // last mod file time
+        final short mdate; // last mod file date
+        final int crc; // crc-32
+        final int compsize; // compressed size
+        final int uncompsize; // uncompressed size
+        final short namelen; // file name length
+        final short extlen; // extra field length
+        final byte[] nameb; // file name
+        // ---
+        final boolean hasEXT = entry.hasEXT();
+        nameb = entry.getNameAsBytes();
+        if (nameb.length > Short.MAX_VALUE)
+            throw new ZipException("too long name: length=" + nameb.length);
+        entry.reloff = (int)(offset & 0xFFFFFFFFL);
+        signature = SIGN_LOC;
+        version = entry.version;
+        flags = entry.flags;
+        method = entry.method;
+        mtime = entry.mtime;
+        mdate = entry.mdate;
+        crc = hasEXT ? 0 : entry.crc;
+        compsize = hasEXT ? 0 : entry.compsize;
+        uncompsize = hasEXT ? 0 : entry.uncompsize;
+        namelen = (short)(nameb.length & 0xFFFF);
+        extlen = entry.extlen;
+        assert offset <= 0xFFFFFFFFL;
+        buffer.clear();
+        buffer.putInt(signature);
+        buffer.putShort(version);
+        buffer.putShort(flags);
+        buffer.putShort(method);
+        buffer.putShort(mtime);
+        buffer.putShort(mdate);
+        buffer.putInt(crc);
+        buffer.putInt(compsize);
+        buffer.putInt(uncompsize);
+        buffer.putShort(namelen);
+        buffer.putShort(extlen);
+        assert buffer.position() == LENGTH_LOC;
+        buffer.flip();
+        Channels.newChannel(os).write(buffer);
+        os.write(nameb);
+        os.flush();
+        entries.add(entry);
+        assert namelen > 0;
+        assert extlen >= 0;
+        assert compsize >= 0;
+        offset += LENGTH_LOC + namelen + extlen + compsize;
+    }
+
+    public void writeCEN(OutputStream os, ZipEntry entry) throws IOException {
+        // Central file header (CEN)
+        final int signature; // central file header signature
+        final short madever; // version made by
+        final short needver; // version needed to extract
+        final short flags; // general purpose bit flag
+        final short method; // compression method
+        final short mtime; // last mod file time
+        final short mdate; // last mod file date
+        final int crc; // crc-32
+        final int compsize; // compressed size
+        final int uncompsize; // uncompressed size
+        final short namelen; // file name length
+        final short extlen; // extra field length
+        final short commlen; // file comment length
+        final short disknum; // disk number start
+        final short inattr; // internal file attributes
+        final int exattr; // external file attributes
+        final int reloff; // relative offset of local header
+        final byte[] nameb; // file name
+        // ---
+        signature = SIGN_CEN;
+        madever = entry.version;
+        needver = entry.version;
+        flags = entry.flags;
+        method = entry.method;
+        mtime = entry.mtime;
+        mdate = entry.mdate;
+        crc = entry.crc;
+        compsize = entry.compsize;
+        uncompsize = entry.uncompsize;
+        nameb = entry.getNameAsBytes();
+        if (nameb.length > Short.MAX_VALUE)
+            throw new ZipException("too long name: length=" + nameb.length);
+        namelen = (short)nameb.length;
+        extlen = 0; // not support
+        commlen = 0; // not support
+        disknum = 0; // not support
+        inattr = 0; // not support
+        exattr = 0; // not support
+        reloff = entry.reloff;
+        buffer.clear();
+        buffer.putInt(signature);
+        buffer.putShort(madever);
+        buffer.putShort(needver);
+        buffer.putShort(flags);
+        buffer.putShort(method);
+        buffer.putShort(mtime);
+        buffer.putShort(mdate);
+        buffer.putInt(crc);
+        buffer.putInt(compsize);
+        buffer.putInt(uncompsize);
+        buffer.putShort(namelen);
+        buffer.putShort(extlen);
+        buffer.putShort(commlen);
+        buffer.putShort(disknum);
+        buffer.putShort(inattr);
+        buffer.putInt(exattr);
+        buffer.putInt(reloff);
+        assert buffer.position() == LENGTH_CEN;
+        buffer.flip();
+        Channels.newChannel(os).write(buffer);
+        os.write(nameb);
+        os.flush();
+        offset += LENGTH_CEN + namelen + extlen;
+    }
+
+    public void writeEND(OutputStream os) throws IOException {
+        for (final ZipEntry entry : entries)
+            writeCEN(os, entry);
+        writeEND(os, ZipEndEntry.create(offset, entries));
+    }
+
+    public void writeEND(OutputStream os, ZipEndEntry entry) throws IOException {
+        // End of central dir header (END)
+        final int signature; // end of central dir signature
+        final short disknum; // number of this disk
+        final short disknumCEN; // number of the disk with the start of the central directory
+        final short countDiskCENs; // total number of entries in the central directory on this disk
+        final short countCENs; // total number of entries in the central directory
+        final int sizeCENs; // size of the central directory
+        final int offsetStartCEN; // offset of start of central directory with respect to the starting disk number
+        final short commentlen; // .ZIP file comment length
+        final byte[] comment; // .ZIP file comment
+        // ---
+        signature = entry.signature;
+        disknum = entry.disknum;
+        disknumCEN = entry.disknumCEN;
+        countDiskCENs = entry.countDiskCENs;
+        countCENs = entry.countCENs;
+        sizeCENs = entry.sizeCENs;
+        offsetStartCEN = entry.offsetStartCEN;
+        commentlen = 0;
+        comment = new byte[0];
+        buffer.clear();
+        buffer.putInt(signature);
+        buffer.putShort(disknum);
+        buffer.putShort(disknumCEN);
+        buffer.putShort(countDiskCENs);
+        buffer.putShort(countCENs);
+        buffer.putInt(sizeCENs);
+        buffer.putInt(offsetStartCEN);
+        buffer.putShort(commentlen);
+        buffer.put(comment);
+        assert buffer.position() == LENGTH_END;
+        buffer.flip();
+        Channels.newChannel(os).write(buffer);
+        os.flush();
+        offset += LENGTH_END;
+    }
+
+    public void writeEXT(OutputStream os, ZipEntry entry) throws IOException {
+        // Extend header (EXT)
+        final int signature; // extend header signature
+        final int crc; // crc-32
+        final int compsize; // compressed size
+        final int uncompsize; // uncompressed size
+        // ---
+        signature = SIGN_EXT;
+        crc = entry.crc;
+        compsize = entry.compsize;
+        uncompsize = entry.uncompsize;
+        buffer.clear();
+        buffer.putInt(signature);
+        buffer.putInt(crc);
+        buffer.putInt(compsize);
+        buffer.putInt(uncompsize);
+        assert buffer.position() == LENGTH_EXT;
+        buffer.flip();
+        Channels.newChannel(os).write(buffer);
+        os.flush();
+        offset += LENGTH_EXT;
+        assert entry.compsize >= 0;
+        offset += entry.compsize;
+    }
+
+    public void clearEntries() {
+        entries.clear();
+        offset = 0;
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/zip/ZipInputStream.java b/src/jp/sfjp/armadillo/archive/zip/ZipInputStream.java
new file mode 100644 (file)
index 0000000..55404c6
--- /dev/null
@@ -0,0 +1,107 @@
+package jp.sfjp.armadillo.archive.zip;
+
+import java.io.*;
+import java.util.zip.*;
+import jp.sfjp.armadillo.archive.*;
+import jp.sfjp.armadillo.io.*;
+
+public final class ZipInputStream extends ArchiveInputStream {
+
+    private static final int BUFFER_SIZE = 16384;
+
+    private ZipHeader header;
+    private ZipEntry ongoingEntry;
+    private Inflater inflater;
+
+    public ZipInputStream(InputStream is) {
+        super(new RewindableInputStream(is, BUFFER_SIZE));
+        this.header = new ZipHeader();
+        this.inflater = new Inflater(true);
+    }
+
+    public ZipEntry getNextEntry() throws IOException {
+        ensureOpen();
+        if (ongoingEntry != null)
+            closeEntry();
+        ZipEntry entry = header.readLOC(in);
+        if (entry == null)
+            return null;
+        return openEntry(entry);
+    }
+
+    ZipEntry openEntry(ZipEntry entry) throws IOException {
+        assert entry != null;
+        if (entry.isDirectory() || entry.method == ZipEntry.STORED)
+            frontStream = in;
+        else if (entry.method == ZipEntry.DEFLATED)
+            frontStream = new InflaterInputStream(in, inflater, 512);
+        else {
+            System.out.printf("method=%d, name=%s%n", entry.method, entry.getName());
+            frontStream = in;
+        }
+        if (entry.hasEXT())
+            if (entry.isDirectory() && !header.readEXT(in, entry))
+                throw new ZipException("failed to read EXT header on stream mode (0)");
+            else {
+                byte[] buffer = new byte[BUFFER_SIZE];
+                final int readSize = in.read(buffer);
+                final int offset = findSIGEXT(buffer);
+                if (offset < 0)
+                    throw new ZipException("failed to read EXT header on stream mode (1)");
+                ((RewindableInputStream)in).rewind(readSize);
+                InputStream bis = new ByteArrayInputStream(buffer, offset, ZipHeader.LENGTH_EXT);
+                if (!header.readEXT(bis, entry))
+                    throw new ZipException("failed to read EXT header on stream mode (2)");
+            }
+        remaining = entry.getSize();
+        return ongoingEntry = entry;
+    }
+
+    public void closeEntry() throws IOException {
+        ZipEntry entry = ongoingEntry;
+        ensureOpen();
+        if (!entry.isDirectory()) {
+            // jump to the head of next header if need
+            if (remaining > 0) {
+                final long compsize = entry.compsize;
+                final long uncompsize = entry.uncompsize;
+                if (remaining == uncompsize) {
+                    long skip = compsize;
+                    while (skip > 0)
+                        skip -= in.skip(skip);
+                    remaining -= uncompsize;
+                    assert (skip != 0) : "skipped size should be zero, but was " + skip;
+                }
+            }
+            final int inflaterRemaining = inflater.getRemaining();
+            if (inflaterRemaining > 0) // overread
+                ((RewindableInputStream)in).rewind(inflaterRemaining);
+            assert remaining == 0 : "remaining should be zero, but was %d" + remaining;
+            if (entry.hasEXT() && !header.readEXT(in, entry))
+                throw new ZipException("failed to read EXT header on stream mode (3)");
+        }
+        // reset
+        ongoingEntry = null;
+        inflater.reset();
+        frontStream = in;
+    }
+
+    private static int findSIGEXT(byte[] bytes) {
+        // SIGEXT=0x504B0708
+        for (int i = 0; i < bytes.length - 3; i++)
+            if (bytes[i] == 0x50)
+                if (bytes[i + 1] == 0x4B)
+                    if (bytes[i + 2] == 0x07)
+                        if (bytes[i + 3] == 0x08)
+                            return i;
+        return -1;
+    }
+
+    @Override
+    public void close() throws IOException {
+        header = null;
+        inflater = null;
+        ongoingEntry = null;
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/archive/zip/ZipOutputStream.java b/src/jp/sfjp/armadillo/archive/zip/ZipOutputStream.java
new file mode 100644 (file)
index 0000000..5490d0a
--- /dev/null
@@ -0,0 +1,108 @@
+package jp.sfjp.armadillo.archive.zip;
+
+import java.io.*;
+import java.util.zip.*;
+import jp.sfjp.armadillo.archive.*;
+
+public final class ZipOutputStream extends ArchiveOutputStream {
+
+    private ZipHeader header;
+    private Deflater deflater;
+    private CRC32 crc;
+    private ZipEntry ongoingEntry;
+
+    public ZipOutputStream(OutputStream os) {
+        super(os);
+        this.header = new ZipHeader();
+        this.deflater = new Deflater(Deflater.DEFAULT_COMPRESSION, true);
+        this.crc = new CRC32();
+        frontStream = os;
+    }
+
+    public void putNextEntry(ZipEntry entry) throws IOException {
+        ensureOpen();
+        if (ongoingEntry != null)
+            closeEntry();
+        assert entry.method != -1;
+        assert entry.uncompsize >= 0;
+        if (entry.isDirectory()) {
+            entry.version = 10;
+            entry.flags &= 0xFFF7;
+            entry.compsize = 0;
+            entry.uncompsize = 0;
+            entry.crc = 0;
+        }
+        else if (entry.method == ZipEntry.STORED) {
+            final int size = (entry.uncompsize >= 0) ? entry.uncompsize : entry.compsize;
+            entry.version = 10;
+            entry.compsize = size;
+            entry.uncompsize = size;
+        }
+        else if (entry.method == ZipEntry.DEFLATED) {
+            entry.version = 20;
+            if (entry.isOnePassMode())
+                entry.flags |= 0x08; // EXT
+            if (!entry.isDirectory())
+                frontStream = new DeflaterOutputStream(out, deflater);
+        }
+        else
+            throw new ZipException("unsupported compression method: " + entry.method);
+        ongoingEntry = entry;
+        header.write(out, entry);
+    }
+
+    public void closeEntry() throws IOException {
+        ensureOpen();
+        flush();
+        if (frontStream instanceof DeflaterOutputStream) {
+            DeflaterOutputStream deflaterOutputStream = (DeflaterOutputStream)frontStream;
+            deflaterOutputStream.finish();
+            deflaterOutputStream.flush();
+            frontStream = out;
+        }
+        final int crc32 = (int)(crc.getValue() & 0xFFFFFFFF);
+        ongoingEntry.crc = crc32;
+        if (!ongoingEntry.isDirectory())
+            if (ongoingEntry.hasEXT()) {
+                ongoingEntry.compsize = deflater.getTotalOut();
+                ongoingEntry.uncompsize = deflater.getTotalIn();
+                header.writeEXT(out, ongoingEntry);
+            }
+            else if (ongoingEntry.method == ZipEntry.DEFLATED)
+                if (deflater.getTotalOut() != ongoingEntry.compsize
+                    || deflater.getTotalIn() != ongoingEntry.uncompsize)
+                    throw new ZipException("invalid header info");
+        deflater.reset();
+        crc.reset();
+        ongoingEntry = null;
+    }
+
+    @Override
+    public void write(int b) throws IOException {
+        super.write(b);
+        crc.update(b);
+    }
+
+    @Override
+    public void write(byte[] b, int off, int len) throws IOException {
+        super.write(b, off, len);
+        crc.update(b, off, len);
+    }
+
+    @Override
+    public void close() throws IOException {
+        try {
+            flush();
+            header.writeEND(out);
+            deflater.end();
+        }
+        finally {
+            header = null;
+            deflater = null;
+            crc = null;
+            ongoingEntry = null;
+            super.close();
+        }
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/compression/lzhuf/LzhHuffmanDecoder.java b/src/jp/sfjp/armadillo/compression/lzhuf/LzhHuffmanDecoder.java
new file mode 100644 (file)
index 0000000..cd71a4f
--- /dev/null
@@ -0,0 +1,228 @@
+package jp.sfjp.armadillo.compression.lzhuf;
+
+import java.io.*;
+import jp.sfjp.armadillo.io.*;
+
+/**
+ * Huffman decoder for LH4, LH5, LH6, LH7.
+ */
+public final class LzhHuffmanDecoder implements LzssDecoderReadable {
+
+    /** Work table bit length (16 bits and a guard) */
+    private static final int WORK_TABLE_BITLENGTH = 16 + 1;
+
+    private BitInputStream bin;
+    private int blockSize;
+    private int symbolMaxBitLength;
+    private short[] symbolLengthTable;
+    private short[] symbolCodeTable;
+    private int offsetMaxBitLength;
+    private short[] offsetLengthTable;
+    private short[] offsetCodeTable;
+
+    public LzhHuffmanDecoder(InputStream in) {
+        this(in, -1);
+    }
+
+    /**
+     * @param in InputStream
+     * @param limit when positive value: bytes to decode
+     *              when negative value: infinite (until EOF)
+     */
+    public LzhHuffmanDecoder(final InputStream in, final long limit) {
+        this.bin = (limit < 0) ? new BitInputStream(in) : new BitInputStream(new InputStream() {
+            private long remaining = limit;
+
+            @Override
+            public int read() throws IOException {
+                if (remaining <= 0)
+                    return -1;
+                final int read = in.read();
+                if (read != -1)
+                    --remaining;
+                return read;
+            }
+
+        });
+        this.blockSize = 0;
+    }
+
+    @Override
+    public int read() throws IOException {
+        try {
+            assert blockSize >= 0;
+            if (blockSize == 0) {
+                if (bin.prefetch() == -1)
+                    return -1;
+                final int n = bin.readBits(16);
+                if (n == -1)
+                    return -1;
+                this.blockSize = n;
+                assert blockSize > 0 : "block size = " + blockSize;
+                createSymbolTables();
+                createOffsetTables();
+            }
+            --blockSize;
+            final int b = bin.prefetchBits(symbolMaxBitLength);
+            assert b != -1;
+            final int code = symbolCodeTable[b];
+            bin.readBits(symbolLengthTable[code]);
+            assert code >= 0 && code < 511;
+            return code;
+        }
+        catch (final RuntimeException ex) {
+            throw new LzhufException("decode error", ex);
+        }
+    }
+
+    @Override
+    public int readOffset() throws IOException {
+        try {
+            final int b = bin.prefetchBits(offsetMaxBitLength);
+            assert b != -1;
+            final int code = offsetCodeTable[b];
+            final int codeLength = offsetLengthTable[code];
+            if (codeLength > 0)
+                bin.readBits(codeLength);
+            assert code >= 0 && codeLength >= 0;
+            int offset;
+            if (code > 1)
+                offset = (1 << (code - 1)) | bin.readBits(code - 1);
+            else
+                offset = code;
+            assert offset >= 0;
+            return offset;
+        }
+        catch (final RuntimeException ex) {
+            throw new LzhufException("decode error", ex);
+        }
+    }
+
+    private void createSymbolTables() throws IOException {
+        // initialize symbolMaxBitLength, symbolLengthTable and symbolCodeTable
+        final short[] lengthList = readCodeLengthList(5, 3);
+        final int blength = getMaxBitSize(lengthList);
+        final short[] table = createCodeTable(lengthList, blength);
+        final int n = bin.readBits(9);
+        if (n < 1)
+            throw new LzhufException("invalid compressed data: number of code lengths=" + n);
+        final short[] codeLengthList = new short[n];
+        for (int i = 0; i < codeLengthList.length;) {
+            final int code = bin.prefetchBits(blength);
+            if (code == -1)
+                throw new LzhufException("EOF appeared while reading symbol length list");
+            final int length = table[code];
+            final int bitLength = lengthList[length];
+            bin.readBits(bitLength);
+            switch (length) {
+                case 0:
+                    ++i;
+                    break;
+                case 1:
+                    i += bin.readBits(4) + 3;
+                    break;
+                case 2:
+                    i += bin.readBits(9) + 20;
+                    break;
+                default:
+                    codeLengthList[i++] = (short)(length - 2);
+            }
+        }
+        final int maxBitLength = getMaxBitSize(codeLengthList);
+        this.symbolMaxBitLength = maxBitLength;
+        this.symbolLengthTable = codeLengthList;
+        this.symbolCodeTable = createCodeTable(codeLengthList, maxBitLength);
+    }
+
+    private void createOffsetTables() throws IOException {
+        // initialize offsetMaxBitLength, offsetLengthTable and offsetCodeTable
+        short[] codeLengthList = readCodeLengthList(4, -1);
+        if (codeLengthList.length == 0) {
+            final int offset = bin.readBits(4);
+            codeLengthList = new short[offset + 1];
+            final short[] codeTable = new short[]{(short)offset, (short)offset};
+            this.offsetMaxBitLength = 1;
+            this.offsetLengthTable = codeLengthList;
+            this.offsetCodeTable = codeTable;
+        }
+        else {
+            final int maxBitLength = getMaxBitSize(codeLengthList);
+            this.offsetMaxBitLength = maxBitLength;
+            this.offsetLengthTable = codeLengthList;
+            this.offsetCodeTable = createCodeTable(codeLengthList, maxBitLength);
+        }
+    }
+
+    private short[] readCodeLengthList(int nBits, int special) throws IOException {
+        final int n = bin.readBits(nBits);
+        final short[] list = new short[n];
+        for (int i = 0; i < n; i++) {
+            if (i == special)
+                i += bin.readBits(2);
+            int length = bin.readBits(3);
+            if (length == 7)
+                while (bin.readBit() == 1)
+                    ++length;
+            list[i] = (short)length;
+        }
+        return list;
+    }
+
+    private static int getMaxBitSize(short[] bitLengthList) {
+        int max = 0;
+        for (int i = 0; i < bitLengthList.length; i++)
+            if (bitLengthList[i] > max)
+                max = bitLengthList[i];
+        return max;
+    }
+
+    private static short[] createCodeTable(short[] lengthList, int maxBitLength) {
+        final int[] codeList = createCodeList(lengthList);
+        final int tableSize = (1 << maxBitLength);
+        final short[] table = new short[tableSize];
+        for (int i = 0; i < lengthList.length; i++)
+            if (lengthList[i] > 0) {
+                final int rangeBits = maxBitLength - lengthList[i];
+                final int start = codeList[i] << rangeBits;
+                final int next = start + (1 << rangeBits);
+                for (int index = start; index < next; index++)
+                    table[index] = (short)i;
+            }
+        return table;
+    }
+
+    private static int[] createCodeList(short[] codeLengthList) {
+        assert codeLengthList.length > 0;
+        if (codeLengthList.length == 1)
+            return new int[1];
+        final int[] counts = new int[WORK_TABLE_BITLENGTH];
+        for (int i = 0; i < codeLengthList.length; i++)
+            ++counts[codeLengthList[i]];
+        final int[] baseCodes = new int[WORK_TABLE_BITLENGTH];
+        // i = bit length - 1
+        for (int i = 0; i < WORK_TABLE_BITLENGTH - 1; i++)
+            baseCodes[i + 1] = baseCodes[i] + counts[i + 1] << 1;
+        final int[] codeList = new int[codeLengthList.length];
+        for (int i = 0; i < codeList.length; i++) {
+            final int codeLength = codeLengthList[i];
+            if (codeLength > 0)
+                codeList[i] = baseCodes[codeLength - 1]++;
+        }
+        return codeList;
+    }
+
+    @Override
+    public void close() throws IOException {
+        try {
+            bin.close();
+        }
+        finally {
+            bin = null;
+            symbolLengthTable = null;
+            symbolCodeTable = null;
+            offsetLengthTable = null;
+            offsetCodeTable = null;
+        }
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/compression/lzhuf/LzhHuffmanEncoder.java b/src/jp/sfjp/armadillo/compression/lzhuf/LzhHuffmanEncoder.java
new file mode 100644 (file)
index 0000000..48be4c4
--- /dev/null
@@ -0,0 +1,257 @@
+package jp.sfjp.armadillo.compression.lzhuf;
+
+import java.io.*;
+import java.util.*;
+import jp.sfjp.armadillo.archive.lzh.*;
+import jp.sfjp.armadillo.io.*;
+
+/**
+ * Huffman encoder for LH4, LH5, LH6, LH7.
+ */
+public final class LzhHuffmanEncoder implements LzssEncoderWritable {
+
+    private static final int BUFFER_SIZE = 4096;
+
+    private final BitOutputStream out;
+    private final int threshold;
+    private int index;
+    private final int[] symbolBuffer;
+    private final int[] offsetBuffer;
+    private final int[] frequencyTable;
+
+    /*
+     * C: code table
+     * L: code length table
+     * T1: code huffman table
+     * T2: length huffman table of T1
+     * T3: huffman table bit length of offset
+     */
+    private int[] ct1, lt1, ct2, lt2, ct3, lt3;
+    private int t1size;
+
+    public LzhHuffmanEncoder(OutputStream out, int threshold) {
+        this.out = new BitOutputStream(new FilterOutputStream(out) {
+            @Override
+            public void close() throws IOException {
+                //
+            }
+        });
+        this.threshold = threshold;
+        this.index = 0;
+        this.symbolBuffer = new int[BUFFER_SIZE];
+        this.offsetBuffer = new int[BUFFER_SIZE];
+        this.frequencyTable = new int[512];
+    }
+
+    @Override
+    public void write(int symbol) throws IOException {
+        ++frequencyTable[symbol];
+        symbolBuffer[index++] = symbol;
+        if (index >= BUFFER_SIZE)
+            encode();
+    }
+
+    @Override
+    public void writeMatched(int offset, int length) throws IOException {
+        assert length >= threshold;
+        final int symbol = length - threshold + 0x100;
+        ++frequencyTable[symbol];
+        symbolBuffer[index] = symbol;
+        offsetBuffer[index++] = offset - 1;
+        if (index >= BUFFER_SIZE)
+            encode();
+    }
+
+    @Override
+    public void flush() throws IOException {
+        encode();
+    }
+
+    private void encode() throws IOException {
+        if (index == 0)
+            return;
+        try {
+            createTables();
+            outputCodes();
+        }
+        catch (final LzhQuit ex) {
+            throw ex;
+        }
+        catch (final RuntimeException ex) {
+            final IOException exception = new LzhufException("huffman encoding error");
+            exception.initCause(ex);
+            throw exception;
+        }
+        finally {
+            index = 0;
+            ct1 = null;
+            lt1 = null;
+            ct2 = null;
+            lt2 = null;
+            ct3 = null;
+            lt3 = null;
+            Arrays.fill(frequencyTable, 0);
+        }
+    }
+
+    private void createTables() {
+        // create T1
+        final LzhHuffmanTable table1 = LzhHuffmanTable.build(frequencyTable);
+        ct1 = table1.codeTable;
+        lt1 = table1.codeLengthTable;
+        t1size = getTrimmedSize(lt1);
+        final int size = getTrimmedSize(lt1);
+        final int[] ft = new int[512];
+        for (int i1 = 0; i1 < size;) {
+            final int length = lt1[i1++];
+            if (length == 0) {
+                int count = 1;
+                while (i1 < size && lt1[i1] == 0) {
+                    ++i1;
+                    ++count;
+                }
+                if (count <= 2)
+                    ft[0] += count;
+                else if (count <= 18)
+                    ++ft[1];
+                else if (count == 19) {
+                    ++ft[0];
+                    ++ft[1];
+                }
+                else
+                    ++ft[2];
+            }
+            else {
+                final int p = length + 2;
+                ++ft[p];
+            }
+        }
+        // create T2
+        final LzhHuffmanTable table2 = LzhHuffmanTable.build(ft);
+        ct2 = table2.codeTable;
+        lt2 = table2.codeLengthTable;
+        // create T3
+        final int[] ft3 = new int[17];
+        for (int i = 0; i < index; i++) {
+            if (symbolBuffer[i] < 0x100)
+                continue;
+            final int offset = offsetBuffer[i];
+            int bitLength = 0;
+            while (true) {
+                if (offset < 1 << bitLength)
+                    break;
+                ++bitLength;
+            }
+            ++ft3[bitLength];
+        }
+        final LzhHuffmanTable table3 = LzhHuffmanTable.build(ft3);
+        ct3 = table3.codeTable;
+        lt3 = table3.codeLengthTable;
+    }
+
+    private void outputCodes() throws IOException {
+        // a size of LZSS elements
+        final int t2size = getTrimmedSize(lt2);
+        out.writeBits(index, 16);
+        // T2
+        out.writeBits(t2size, 5);
+        for (int i = 0; i < t2size;) {
+            final int length = lt2[i++];
+            if (length <= 6)
+                out.writeBits(length, 3);
+            else {
+                out.writeBits(0xFFFFFFFF, length - 4);
+                out.writeBits(0, 1);
+            }
+            if (i == 3) {
+                while (i < 6 && lt2[i] == 0)
+                    ++i;
+                out.writeBits(i - 3, 2);
+            }
+        }
+        // T1 table size
+        out.writeBits(t1size, 9);
+        // T1
+        for (int i = 0; i < t1size;) {
+            final int length = lt1[i++];
+            if (length == 0) {
+                int count = 1;
+                while (i < t1size && lt1[i] == 0) {
+                    ++i;
+                    ++count;
+                }
+                if (count <= 2)
+                    for (int j = 0; j < count; j++)
+                        out.writeBits(ct2[0], lt2[0]);
+                else if (count <= 18) {
+                    out.writeBits(ct2[1], lt2[1]);
+                    out.writeBits(count - 3, 4);
+                }
+                else if (count == 19) {
+                    out.writeBits(ct2[0], lt2[0]);
+                    out.writeBits(ct2[1], lt2[1]);
+                    out.writeBits(15, 4);
+                }
+                else {
+                    out.writeBits(ct2[2], lt2[2]);
+                    out.writeBits(count - 20, 9);
+                }
+            }
+            else
+                out.writeBits(ct2[length + 2], lt2[length + 2]);
+        }
+        // T3
+        final int t3size = getTrimmedSize(lt3);
+        out.writeBits(t3size, 4);
+        if (t3size == 0)
+            out.writeBits(0, 4);
+        else
+            for (int i = 0; i < t3size;) {
+                final int length = lt3[i++];
+                if (length <= 6)
+                    out.writeBits(length, 3);
+                else {
+                    out.writeBits(0xFFFFFFFF, length - 4);
+                    out.writeBits(0, 1);
+                }
+            }
+        // LZSS
+        for (int i = 0; i < index; i++) {
+            final int symbol = symbolBuffer[i];
+            out.writeBits(ct1[symbol], lt1[symbol]);
+            if (symbol >= 0x100) {
+                final int offset = offsetBuffer[i];
+                int offsetBitLength = 1;
+                while (true) {
+                    if (offset < (1 << offsetBitLength))
+                        break;
+                    ++offsetBitLength;
+                }
+                if (offset == 0)
+                    offsetBitLength = 0;
+                out.writeBits(ct3[offsetBitLength], lt3[offsetBitLength]);
+                if (offsetBitLength > 1)
+                    out.writeBits(offset, offsetBitLength - 1);
+            }
+        }
+        out.flush();
+    }
+
+    private static int getTrimmedSize(int[] a) {
+        int i = a.length;
+        while (i > 0 && a[i - 1] == 0)
+            --i;
+        return i;
+    }
+
+    @Override
+    public void close() throws IOException {
+        try {
+            flush();
+        }
+        finally {
+            out.close();
+        }
+    }
+
+}
diff --git a/src/jp/sfjp/armadillo/compression/lzhuf/LzhHuffmanTable.java b/src/jp/sfjp/armadillo/compression/lzhuf/LzhHuffmanTable.java
new file mode 100644 (file)
index 0000000..c9de329
--- /dev/null
@@ -0,0 +1,218 @@
+package jp.sfjp.armadillo.compression.lzhuf;
+
+import java.util.*;
+import jp.sfjp.armadillo.archive.lzh.*;
+
+/**
+ * A huffman table for LZHUF.
+ */
+public final class LzhHuffmanTable {
+
+    private static final int MAX_CODE_LENGTH = 16;
+
+    final int[] codeTable;
+    final int[] codeLengthTable;
+
+    private LzhHuffmanTable(int[] frequencyTable) {
+        int tableSize = frequencyTable.length;
+        for (int i = tableSize - 1; i >= 0; i--) {
+            if (frequencyTable[i] > 0)
+                break;
+            --tableSize;
+        }
+        int[] ct = new int[tableSize];
+        int[] lt = new int[tableSize];
+        build(frequencyTable, tableSize, ct, lt);
+        this.codeTable = ct;
+        this.codeLengthTable = lt;
+    }
+
+    public static LzhHuffmanTable build(int[] frequencyTable) {
+        return new LzhHuffmanTable(frequencyTable);
+    }
+
+    /**
+     * Builds tables.
+     * @param frequencyTable
+     * @param tableSize
+     * @param ct code table
+     * @param lt code length table
+     */
+    private void build(int[] frequencyTable, int tableSize, int[] ct, int[] lt) {
+        // create node list
+        LinkedList<Node> q = new LinkedList<Node>();
+        for (int i = 0; i < tableSize; i++) {
+            final int frequency = frequencyTable[i];
+            if (frequency > 0)
+                q.add(new Node(i, frequency));
+        }
+        if (q.size() < 2) {
+            // queue size = ( 0, 1, 2 )
+            int queueSize = q.size();
+            if (queueSize == 1) {
+                // queue size = ( 1 )
+                lt[tableSize - 1] = 1;
+            }
+            else if (queueSize == 2) {
+                // queue size = ( 2 )
+                Node node1 = q.get(0);
+                Node node2 = q.get(1);
+                int[] numbers = {node1.number, node2.number};
+                Arrays.sort(numbers);
+                ct[numbers[0]] = 0;
+                lt[numbers[0]] = 1;
+                ct[numbers[1]] = 1;
+                lt[numbers[1]] = 1;
+            }
+            return;
+        }
+        // create tree
+        int number = tableSize;
+        while (q.size() > 1) {
+            Collections.sort(q);
+            Node node1 = q.remove(0);
+            Node node2 = q.remove(0);
+