Kinx ライブラリ - Zip

# Zip

## はじめに

**「見た目は JavaScript、頭脳(中身)は Ruby、（安定感は AC/DC）」** でお届けしているスクリプト言語 [Kinx](https://github.com/Kray-G/kinx)。言語はライブラリが命。ということでライブラリの使い方編。 

今回は Zip です。

* 参考
    * 最初の動機 ... [スクリプト言語 KINX（ご紹介）](https://qiita.com/Kray-G/items/ca08b6fb40d15dd0ec76)
        * 個別記事へのリンクは全てここに集約してあります。
    * リポジトリ ... [https://github.com/Kray-G/kinx](https://github.com/Kray-G/kinx)
        * Pull Request 等お待ちしております。 

アプリ作る時に Zip 機能は欠かせないですね。あと、脆弱だ、脆弱だ、言われてますけど、業務アプリの中ではパスワード付き Zip が（建前上）求められることも事実。なので、パスワード付き Zip が作れないと実用にならない（日本では...?）。

## Zip

### Zip アーカイブ作成

#### Zip インスタンスの作成（`class Zip`）

基本的にはこんな感じ。

```javascript
var zip = new Zip("zipfile.zip", File.READ|File.WRITE);
```

ファイル名とモードを指定する。

##### モード

|    モード     |      意味      |                           動作概要                           |
| ------------- | -------------- | ------------------------------------------------------------ |
| `File.READ`   | 読み込みモード | 単独で指定した場合、ファイルが存在しなければ `ZipException`  |
| `File.WRITE`  | 書き込みモード | ファイルが存在しても新規に作成しなおすモード                 |
| `File.APPEND` | 追記モード     | ファイルが存在した場合、そのファイルに追記するモード         |

File と同じ。

##### メソッド

Zip インスタンスのメソッドは以下の通り。

|             メソッド             |                       内容                        |             オプション             |
| -------------------------------- | ------------------------------------------------- | ---------------------------------- |
| `extract(name, [opts])`          | 展開して文字列で取得                              | `{ password, overwrite, skip, }`   |
| `extractTo(name, file [, opts])` | ファイルに展開                                    | (同上)                             |
| `find(name)`                     | エントリを検索、エントリ・オブジェクトを返す      |                                    |
| `addFile(filename [, opts])`     | Zip ファイルにエントリを追加                      | `{ password, method, aes, level }` |
| `addString(text [, opts])`       | Zip ファイルにエントリを追加                      | (同上)                             |
| `setPassword(password)`          | 全 Zip エントリで共通して使用するパスワードを設定 |                                    |
| `setOverwrite(truefalse)`        | 上書き設定を一括で指定しておくために使用する      |                                    |

#### エントリの追加（`addFile()`/`addString()`）

`addFile()` または `addString()` を使います。即座にエントリが追加される。

```javascript
zip.addFile("README.md");
```

ファイルにディレクトリ名を与えた場合、ディレクトリは以下のファイルが追加される。また、第 2 引数にオプションを渡せる。オプションの内容は次の通り。

* `password`: パスワード付き Zip のパスワード。デフォルトは無し。
* `method`: 圧縮方法。デフォルトは `deflate`。その他、指定できるのは `"store"`、`"bzip2"`、`"lzma"`。
* `aes`: WinZIP 互換の AES 暗号化を有効にするか（true/false）。デフォルト false。尚、`addString` の場合には無視される。
* `level`: 圧縮レベル。0-9。

オプションを付ける例は以下の通り。

```javascript
zip.addFile("README.md", {
    method: "bzip2",
    password: "password",
    aes: true,
});
zip.addString("test/test1.txt", {
    content: "test/test\n",
    // aes: true,  // addString では無視される.
});
```

パスワードは、展開の際に個別に指定するようにすればエントリごとに別々につけることもできる。

#### 暗号化

`addFile()` の際にオプションで指定できるが、一括で最初に設定して置く場合は `setPassword()` を使うことができる。

```javascript
var zip = new Zip("zipfile.zip", File.READ|File.WRITE);
zip.setPassword("password");
```

### Zip ファイル一覧の表示

`zip` インスタンスには既に配列としてエントリ・オブジェクトが格納されている。以下のようにすると一覧表示することができる。尚、`zip.totalFiles` にエントリ数が格納されている。

```javascript
var zip = new Zip("zipfile.zip", File.READ);
System.println("totalFiles = ", zip.totalFiles);
zip.each(function(e) {
    System.println("%s:" % e.filename);
    e.keySet().each(&(key) => {
        if (e[key].isFunction || e[key].isObject || e[key].isUndefined) {
            return; // 展開系の関数などはスキップ。
        }
        if (key == "crc32") { // CRC は 16 進表示
            System.println("    %-14s = %10X" % key % e[key]);
        } else if (key != "time" && key != "filename") { // 別で表示
            System.println("    %-14s = %10d" % key % e[key]);
        }
    });
    // time はさらにオブジェクト構造になっている
    e.time.keySet().each(&(k) => {
        System.println("      time.%-7d = %10d" % k % e.time[k]);
    });
    // // エントリを個別に展開することも可能。
    // if (e.filename == "README.md") {
    //     e.extractTo("READMEXX.md", { password: "text", overwrite: true });
    // }
});
```

以下のような感じで表示される。

```
totalFiles = 4
README.md:
    compsize       =       4413
    size           =      11621
    isDirectory    =          0
    crc32          =   EFD9A09C
    isEncrypted    =          1
    method         =    deflate
      time.month   =          3
      time.minute  =          1
      time.day     =         19
      time.year    =       2020
      time.second  =          2
      time.hour    =         16
...
```

### 展開

Zip ファイルの展開は、以下の 2 通りの方法が可能。

* 直接 Zip インスタンスから展開する。
* Zip エントリオブジェクトから展開する。

Zip エントリから展開する場合は個別の展開になる。その場合、上記のようにイテレートして選択する方法と、`find` メソッドを使う方法の 2 種類がある。`find` メソッドは、指定したファイル名のエントリがあれば Zip エントリオブジェクトを返す。

展開時のオプションの意味は、以下の通り。

* `password`: 展開に使うパスワード。指定されなかった場合、`setPassword()` で設定されたものを使う。`setPassword()` でも設定されてなかった場合、パスワードなしで展開しようとする。
* `overwrite`: true を指定し、同名ファイルが既に存在した場合、上書きする。
* `skip`: true を指定し、同名ファイルが既に存在した場合、スキップする。

尚、`overwrite` も `skip` も指定されずに同名ファイルが存在した場合、`ZipException` 例外が送出される。

#### すべて展開

すべて展開するには、上記イテレートしたエントリに対して `extractTo` を実施する。必要なディレクトリは自動的に作成される。

```javascript
zip.each(&(e) => e.extractTo("examples/zip/dst" / e.filename, { password: "text", skip: true }));
```

> なんか説明してなかった気がするが、文字列に対して `/` オペレータを適用すると、`/` で連結された文字列になる。

#### ファイルを指定して展開

直接 Zip インスタンスに対して `extract` または `extractTo` メソッドを使うことが可能。

```javascript
zip.extractTo("README.md", "READMEXX.md", { password: "text", skip: true });
```

`extract` を使用した場合、展開した内容を文字列として返す。

```javascript
var text = zip.extract("README.md", { password: "text" });
```

> 現在、バイナリで取得する方法が無いのに気がついたので、追加する予定。オプションに `{ binary: true }` をつけるイメージ。
>> プレビュー 0.5.x 以降で `{ binary: true }` をサポートしました。

Zip エントリオブジェクトの場合、エントリ名を指定する引数がなくなる。

|          メソッド          |         内容         |            オプション            |
| -------------------------- | -------------------- | -------------------------------- |
| `extract([opts])`          | 展開して文字列で取得 | `{ password, overwrite, skip, }` |
| `extractTo(file [, opts])` | ファイルに展開       | (同上)                           |

`find` を使った例は以下の通り。

```javascript
zip.find("README.md")
   .extractTo("READMEXX.md", { password: "text", skip: true });
```

```javascript
var text = zip.find("README.md")
              .extract({ password: "text" });
```

## その他

### 使っているライブラリ

これです。

* [Minizip](https://github.com/nmoinvaz/minizip)

機能一覧。全然 Mini な感じがしないですね。

* Features
   * Creating and extracting zip archives.
   * Adding and removing entries from zip archives.
   * Read and write raw zip entry data.
   * Reading and writing zip archives from memory.
   * Zlib, BZIP2, and LZMA compression methods.
   * Password protection through Traditional PKWARE and WinZIP AES encryption.
   * Buffered streaming for improved I/O performance.
   * NTFS timestamp support for UTC last modified, last accessed, and creation dates.
   * Disk split support for splitting zip archives into multiple files.
   * Preservation of file attributes across file systems.
   * Follow and store symbolic links.
   * Unicode filename support through UTF-8 encoding.
   * Legacy character encoding support CP437, CP932, CP936, CP950.
   * Turn off compilation of compression, decompression, or encryption.
   * Windows (Win32 & WinRT), macOS and Linux platform support.
   * Streaming interface for easy implementation of additional platforms.
   * Support for Apple's compression library ZLIB implementation.
   * Zero out local file header information.
   * Zip/unzip of central directory to reduce size.
   * Ability to generate and verify CMS signature for each entry.
   * Recover the central directory if it is corrupt or missing.
   * Example minizip command line tool.

### Zip64 対応

Zip64 も対応されている模様。4G 超えもいけるとの話だがテストできていない。

## おわりに

Zip/Unzip はスクリプト言語を使う目的としては上位に来る機能でしょう。間違っても C で組みたいとは思わないし、簡単に Zip ファイル作りたい。

では、また次回。