先日、JavaでのZIP暗号化の考察という記事を書きましたが、zip4jのメンテナンスが再開されており、バージョン2系が公開されていましたので、これを使って通常のzip圧縮/解凍、パスワード付きzip圧縮/解凍(ZipCrypto, AES256)のサンプルを作成してみました。
概要
- zip4jはSrikanth Reddy Lingalaさんが開発しましたが2013年にメンテナンスが止まっていました。その後、JavaでのZIP圧縮ライブラリとしてzip4jが有名になったため、メンテナンスの再開を決意されたそうです。メンテナンスを再開してからのリリースは、バージョン2系として公開されています。
- zip4jの公式サイトはGitHubのzip4jです。
- バージョン2系では、より短いコードで簡単にファイル/ストリーム操作を行えるようになっています。単純な圧縮/解凍であればワンライナーでプログラミングが終わります。その他、処理状況を把握するためのプログレスモニターが追加されています。
- バージョン2系の実行には、JRE8以上が必要となります。
- 下記の調査結果の通り、Windows圧縮/解凍ではAES256を扱えませんが、その他のWindows、7-zip、Zip4j間での圧縮・暗号化の互換性は問題ありません。ファイル名に日本語が含まれる場合は文字化けする場合があるので、可能であれば日本語ファイル名を使用しないような設計を推奨します。
- 解凍すると1GBになるようなファイルの解凍を試しましたが、特に大きくメモリを消費することもありませんでした。
サンプルコード
ソースコード
- zip4jを使って、通常のZIP圧縮/解凍、パスワード付き圧縮/解凍(ZipCrypt, AES256)を行うサンプルです。JUnitで実行できます。
- create(), extract()はWebサーバ上で使う想定のサンプルになっています。Webサーバにアップロードしたzipファイルを、サーバ上のファイルシステムに展開し、何らかの業務処理を行った後に、zipファイルのストリームに戻す、という想定です。
- Webサーバにアップロードされたファイルは、サーバ上のプログラムからはInputStreamで参照することになります。そのため、解凍テスト用のメソッドextractTestでは、zipファイルの読み取り元としてInputStreamを使用します。
- Webサーバ上に展開されたフォルダ内容をzipファイルとしてダウンロード、DBに登録する場合を想定します。このような場合、zipファイルの内容をOutputStreamに出力することになります。そのため、圧縮テスト用のcreateTestでは、zipファイルの出力先としてOutputStreamを使用します。
- 内部的にメモリを大量に消費していないかを確認できるよう、debugメソッドでメモリ使用状況を出力するようにしています。
- mavenを使う前提のサンプルになっています。
- 分かりやすさを優先した関係で、一部でリソース解放やセキュリティの考慮がありませんので、業務で使用する際には検討してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 | package example.zip4j; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.BeforeClass; import org.junit.Test; // java.util.zipにいくつか同名クラスがあることに注意 import net.lingala.zip4j.io.inputstream.ZipInputStream; import net.lingala.zip4j.io.outputstream.ZipOutputStream; import net.lingala.zip4j.model.LocalFileHeader; import net.lingala.zip4j.model.ZipParameters; import net.lingala.zip4j.model.enums.AesKeyStrength; import net.lingala.zip4j.model.enums.EncryptionMethod; public class Zip4jExampleTest { private static final int BUF_SIZE = 4 * 1024 * 10; private static final String IN_TOP_DIR = ".\\indata\\"; // 入力フォルダ private static final String OUT_TOP_DIR = ".\\outdata\\"; // 出力先フォルダ(削除されます) private static final char[] PASSWORD = "password".toCharArray(); private byte[] buf = new byte[BUF_SIZE]; // 本来であればtry-with-resources等でストリームを閉じる必要があります! @BeforeClass public static void prepare() throws IOException { // 出力先ディレクトリを一旦削除 Path out = Paths.get(OUT_TOP_DIR).toAbsolutePath().normalize(); if (Files.exists(out)) { System.out.println("deleting: " + out); Files.walk(out).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); } Files.createDirectories(out); } @Test public void createZip() throws IOException { String[] files = { IN_TOP_DIR + "testdir" }; FileOutputStream fos = new FileOutputStream(OUT_TOP_DIR + "testdir_zip4j.zip"); ZipOutputStream zos = new ZipOutputStream(fos); ZipParameters baseParams = new ZipParameters(); baseParams.setEncryptFiles(false); create(files, baseParams, zos); zos.close(); } @Test public void createPasswordZip_ZipCrypto() throws IOException { String[] files = { IN_TOP_DIR + "testdir" }; FileOutputStream fos = new FileOutputStream(OUT_TOP_DIR + "testdir_zip4j_zipcrypto.zip"); ZipOutputStream zos = new ZipOutputStream(fos, PASSWORD); ZipParameters baseParams = new ZipParameters(); baseParams.setEncryptFiles(true); baseParams.setEncryptionMethod(EncryptionMethod.ZIP_STANDARD); create(files, baseParams, zos); zos.close(); } @Test public void createPasswordZip_AES256() throws IOException { String[] files = { IN_TOP_DIR + "testdir" }; FileOutputStream fos = new FileOutputStream(OUT_TOP_DIR + "testdir_zip4j_aes256.zip"); ZipOutputStream zos = new ZipOutputStream(fos, PASSWORD); ZipParameters baseParams = new ZipParameters(); baseParams.setEncryptFiles(true); baseParams.setEncryptionMethod(EncryptionMethod.AES); baseParams.setAesKeyStrength(AesKeyStrength.KEY_STRENGTH_256); // 128/192/256 create(files, baseParams, zos); zos.close(); } @Test public void extractZip() throws IOException { FileInputStream fis = new FileInputStream(IN_TOP_DIR + "testdir_win.zip"); ZipInputStream zis = new ZipInputStream(fis); ZipParameters baseParams = new ZipParameters(); baseParams.setEncryptFiles(false); extract(zis, OUT_TOP_DIR + "extracted_zip"); zis.close(); } @Test public void extractPasswordZip_ZipCrypto() throws IOException { FileInputStream fis = new FileInputStream(IN_TOP_DIR + "testdir_7zip_zipcrypto.zip"); ZipInputStream zis = new ZipInputStream(fis, PASSWORD); extract(zis, OUT_TOP_DIR + "extracted_zipcrypto"); zis.close(); } @Test public void extractPasswordZip_AES256() throws IOException { FileInputStream fis = new FileInputStream(IN_TOP_DIR + "testdir_7zip_aes256.zip"); ZipInputStream zis = new ZipInputStream(fis, PASSWORD); extract(zis, OUT_TOP_DIR + "extracted_aes256"); zis.close(); } public void create(String[] srcFiles, ZipParameters baseParams, ZipOutputStream zos) throws IOException { // 圧縮対象の元となるソースパスリスト List<Path> srcPathList = new ArrayList<>(); try (Stream<String> st = Arrays.stream(srcFiles)) { srcPathList = st.map(Paths::get).collect(Collectors.toList()); } debug("圧縮開始"); // ソースパス上のファイルを再帰的に圧縮 for (Path srcPath : srcPathList) { // ソースファイルのパスを正規化済の絶対パスに変換 srcPath = srcPath.toAbsolutePath().normalize(); Path parentPath = srcPath.getParent(); // 圧縮対象ファイルリスト List<Path> targetPathList = new ArrayList<>(); // パスがディレクトリの場合、ディレクトリ配下のファイルを再帰的に // 圧縮対象ファイルリストへ追加 if (Files.isDirectory(srcPath)) { // 解凍時にディレクトリは自動で作成されるのでディレクトリは除外 try (Stream<Path> st = Files.walk(srcPath)) { st.filter(Files::isRegularFile).forEach(targetPathList::add); } } else { targetPathList.add(srcPath); } // 圧縮対象ファイルリストのファイルをZIPストリームに出力 for (Path targetPath : targetPathList) { // ソースファイルのパスから、圧縮対象ファイルの相対パスを生成 Path relPath = parentPath.relativize(targetPath); // 対象ファイル用のパラメータ定義 ZipParameters targetParams = new ZipParameters(baseParams); targetParams.setFileNameInZip(relPath.toString()); // ZIPストリームにファイルを出力 zos.putNextEntry(targetParams); try (InputStream is = new FileInputStream(targetPath.toFile())) { int readSize; while ((readSize = is.read(buf)) > 0) { zos.write(buf, 0, readSize); } } zos.closeEntry(); debug("圧縮済: " + relPath + " <- " + targetPath); } } // close()が確実に実行される実装にしないと不正なzipファイルになる場合がある } public void extract(ZipInputStream zis, String extractTopDir) throws IOException { byte[] buf = new byte[BUF_SIZE]; // 展開先ディレクトリパス(存在しない場合は新規作成) Path extractTopPath = Paths.get(extractTopDir).toAbsolutePath().normalize(); if (!Files.exists(extractTopPath)) { Files.createDirectories(extractTopPath); } debug("解凍開始"); // ZIPストリームのエントリ毎にファイルを作成 LocalFileHeader fh; while ((fh = zis.getNextEntry()) != null) { // 解凍先パスの決定 String filename = fh.getFileName(); File extractFile = new File(extractTopPath + File.separator + filename); // ディレクトリを解凍 if (fh.isDirectory()) { extractFile.mkdirs(); // 2層以上作成する場合もあるのでmkdirsを使用 continue; } // ファイルを解凍 // ※zipファイルによってはディレクトリが含まれない場合があるので事前作成 File parentFile = extractFile.getParentFile(); if (!parentFile.exists()) { parentFile.mkdirs(); } try (OutputStream os = new BufferedOutputStream(new FileOutputStream(extractFile));) { int readSize; while ((readSize = zis.read(buf)) > 0) { os.write(buf, 0, readSize); } } debug("解凍済: " + filename + " -> " + extractFile); } } // debug purpose only private static void debug(String msg) { Runtime r = Runtime.getRuntime(); long base = 1024 * 1024; long total = r.totalMemory() / base; long free = r.freeMemory() / base; long used = total - free; String ts = new SimpleDateFormat("HH:mm:ss.SSS").format(new Date()); System.out.printf("%s:(%3d/%3d[MB]): %s\n", ts, used, total, msg); } } |
mavenを使う前提であり、pom.xmlでzip4jライブラリを指定します。
1 2 3 4 5 6 7 8 9 10 11 | <project ... <dependencies> ... <!-- for zip4j --> <dependency> <groupId>net.lingala.zip4j</groupId> <artifactId>zip4j</artifactId> <version>2.2.3</version> </dependency> ... </project> |
実行結果の例
※パスが長すぎるので一部を”…”で省略しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | deleting: C:\...\outdata 12:41:32.832:( 11/123[MB]): 解凍開始 12:41:32.841:( 11/123[MB]): 解凍済: testdir/dir1/dir1-1/dir1-1_file1.txt -> C:\...\outdata\extracted_zipcrypto\testdir\dir1\dir1-1\dir1-1_file1.txt 12:41:32.842:( 11/123[MB]): 解凍済: testdir/dir1/dir1-1/dir1-1_file2.txt -> C:\...\outdata\extracted_zipcrypto\testdir\dir1\dir1-1\dir1-1_file2.txt 12:41:32.843:( 11/123[MB]): 解凍済: testdir/dir1/dir1_file1.txt -> C:\...\outdata\extracted_zipcrypto\testdir\dir1\dir1_file1.txt 12:41:32.844:( 11/123[MB]): 解凍済: testdir/dir1/dir1_file2.txt -> C:\...\outdata\extracted_zipcrypto\testdir\dir1\dir1_file2.txt 12:41:32.846:( 11/123[MB]): 解凍済: testdir/file1.xlsx -> C:\...\outdata\extracted_zipcrypto\testdir\file1.xlsx 12:41:32.861:( 12/123[MB]): 圧縮開始 12:41:32.866:( 12/123[MB]): 圧縮済: testdir\dir1\dir1-1\dir1-1_file1.txt <- C:\...\indata\testdir\dir1\dir1-1\dir1-1_file1.txt 12:41:32.867:( 12/123[MB]): 圧縮済: testdir\dir1\dir1-1\dir1-1_file2.txt <- C:\...\indata\testdir\dir1\dir1-1\dir1-1_file2.txt 12:41:32.868:( 12/123[MB]): 圧縮済: testdir\dir1\dir1_file1.txt <- C:\...\indata\testdir\dir1\dir1_file1.txt 12:41:32.868:( 12/123[MB]): 圧縮済: testdir\dir1\dir1_file2.txt <- C:\...\indata\testdir\dir1\dir1_file2.txt 12:41:32.869:( 12/123[MB]): 圧縮済: testdir\file1.xlsx <- C:\...\indata\testdir\file1.xlsx 12:41:32.871:( 12/123[MB]): 解凍開始 12:41:32.873:( 12/123[MB]): 解凍済: testdir/dir1/dir1-1/dir1-1_file1.txt -> C:\...\outdata\extracted_zip\testdir\dir1\dir1-1\dir1-1_file1.txt 12:41:32.874:( 12/123[MB]): 解凍済: testdir/dir1/dir1-1/dir1-1_file2.txt -> C:\...\outdata\extracted_zip\testdir\dir1\dir1-1\dir1-1_file2.txt 12:41:32.876:( 12/123[MB]): 解凍済: testdir/dir1/dir1_file1.txt -> C:\...\outdata\extracted_zip\testdir\dir1\dir1_file1.txt 12:41:32.877:( 12/123[MB]): 解凍済: testdir/dir1/dir1_file2.txt -> C:\...\outdata\extracted_zip\testdir\dir1\dir1_file2.txt 12:41:32.879:( 12/123[MB]): 解凍済: testdir/file1.xlsx -> C:\...\outdata\extracted_zip\testdir\file1.xlsx 12:41:32.880:( 12/123[MB]): 解凍開始 12:41:32.980:( 13/123[MB]): 解凍済: testdir/dir1/dir1-1/dir1-1_file1.txt -> C:\...\outdata\extracted_aes256\testdir\dir1\dir1-1\dir1-1_file1.txt 12:41:32.990:( 14/123[MB]): 解凍済: testdir/dir1/dir1-1/dir1-1_file2.txt -> C:\...\outdata\extracted_aes256\testdir\dir1\dir1-1\dir1-1_file2.txt 12:41:33.005:( 14/123[MB]): 解凍済: testdir/dir1/dir1_file1.txt -> C:\...\outdata\extracted_aes256\testdir\dir1\dir1_file1.txt 12:41:33.021:( 14/123[MB]): 解凍済: testdir/dir1/dir1_file2.txt -> C:\...\outdata\extracted_aes256\testdir\dir1\dir1_file2.txt 12:41:33.042:( 14/123[MB]): 解凍済: testdir/file1.xlsx -> C:\...\outdata\extracted_aes256\testdir\file1.xlsx 12:41:33.044:( 14/123[MB]): 圧縮開始 12:41:33.056:( 14/123[MB]): 圧縮済: testdir\dir1\dir1-1\dir1-1_file1.txt <- C:\...\indata\testdir\dir1\dir1-1\dir1-1_file1.txt 12:41:33.062:( 14/123[MB]): 圧縮済: testdir\dir1\dir1-1\dir1-1_file2.txt <- C:\...\indata\testdir\dir1\dir1-1\dir1-1_file2.txt 12:41:33.070:( 14/123[MB]): 圧縮済: testdir\dir1\dir1_file1.txt <- C:\...\indata\testdir\dir1\dir1_file1.txt 12:41:33.079:( 15/123[MB]): 圧縮済: testdir\dir1\dir1_file2.txt <- C:\...\indata\testdir\dir1\dir1_file2.txt 12:41:33.086:( 15/123[MB]): 圧縮済: testdir\file1.xlsx <- C:\...\indata\testdir\file1.xlsx 12:41:33.088:( 15/123[MB]): 圧縮開始 12:41:33.091:( 15/123[MB]): 圧縮済: testdir\dir1\dir1-1\dir1-1_file1.txt <- C:\...\indata\testdir\dir1\dir1-1\dir1-1_file1.txt 12:41:33.093:( 15/123[MB]): 圧縮済: testdir\dir1\dir1-1\dir1-1_file2.txt <- C:\...\indata\testdir\dir1\dir1-1\dir1-1_file2.txt 12:41:33.093:( 15/123[MB]): 圧縮済: testdir\dir1\dir1_file1.txt <- C:\...\indata\testdir\dir1\dir1_file1.txt 12:41:33.094:( 15/123[MB]): 圧縮済: testdir\dir1\dir1_file2.txt <- C:\...\indata\testdir\dir1\dir1_file2.txt 12:41:33.097:( 15/123[MB]): 圧縮済: testdir\file1.xlsx <- C:\...\indata\testdir\file1.xlsx |
補足
Windows、7-zip、zip4jの圧縮・解凍の互換性を確認しました。
確認で使用したのは、Windows10、zip4jは2.2.3、7-zipは19.00(x64)です。
変換元 | 変換先 | 成否 | 備考 |
---|---|---|---|
Windows圧縮 | zip4j解凍 | OK | |
7-zip圧縮 | OK | ||
7-zip圧縮(ZipCrypto) | OK | ||
7-zip圧縮(AES-256) | OK | ||
zip4j圧縮 | Windows解凍 | OK | |
7-zip解凍 | OK | ||
zip4j解凍 | OK | ディレクトリのエントリが含まれていないようで、ファイル作成時に親ディレクトリの存在確認(205~208行目)が必要 | |
zip4j圧縮(ZipCrypto) | Windows解凍 | OK | |
7-zip解凍 | OK | ||
zip4j解凍 | OK | ディレクトリのエントリが含まれていないようで、ファイル作成時に親ディレクトリの存在確認(205~208行目)が必要 | |
zip4j圧縮(AES-256) | Windows解凍 | NG | Windows圧縮/解凍はAES未対応(こちらの記事を参考のこと) |
7-zip解凍 | OK | ||
zip4j解凍 | OK | ディレクトリのエントリが含まれていないようで、ファイル作成時に親ディレクトリの存在確認(205~208行目)が必要 |
日本語を含んだファイルの圧縮/解凍の正常性も確認しました。
変換元 | 変換先 | 成否 | 備考 |
---|---|---|---|
Windows圧縮 | zip4j解凍 | NG | ファイル名が文字化けし、ファイルを作成できない場合がある。 |
7-zip圧縮 | |||
zip4j圧縮 | Windows解凍 | OK | |
7-zip解凍 | OK | ||
zip4j解凍 | OK |