Reading JAR/Zip Archive Comment (JDK1.6)

前回の話で、Pack200で圧縮するとJARファイルのファイルサイズがかなり小さくなり、JNLPのパフォーマンス向上に大きく寄与することがわかった。しかし、この高い圧縮率は

pack → unpack → jarsigner  →pack

という誰がどう見ても非効率なプリプロセスを前提としており、ネットワーク上の転送時間は高速化されるものの、その代償としての圧縮処理のパフォーマンス悪化があまりにも大きすぎる。特に最初のノーマライズをなんとかしたい。

もちろん、普通は配布前に一度だけオフラインバッチで流しておけばそれで問題ない。プリプロセスに少し手間をかけることで、本番でのネットワーク転送を高速化する、ここがPack200の本来の目的だろう。でも自分の場合もとはといえば、jarsignerの署名をオンデマンドで実行するアプリの高速化が目的で、テスト環境への配布がメインであるためリリースも多い。jarsignerまでオフラインに戻してしまうと、また署名忘れやバージョン不一致が起こりかねない。

かといって、この無駄に長い圧縮処理をそのままオンデマンドの処理に詰め込むとパフォーマンスが悪すぎて使い物にならない。

JAR.isNormalized?

そこで一つ考えたのが、最初のnormalizeを省略すること。文字通り丸っと省略するとさすがに署名検証エラーになるので、当然のことながら条件付きである。仮に、署名対象のJARが最初からnormalizeされているものであるとしたら、わざわざ再度pack/unpackするのは無駄以外の何ものでもない。だからJARファイルを見てnormalizeされているかどうかを事前に知ることができれば、最初のpack→unpackのステップを丸ごと省略できることになる。

この判定ができるのなら、最初からnormalizeされているJARはオンラインの自動処理でjarsigner → pack200してクライアントに送信、normalizeされていなければjarsignerだけかけて送信(normalizeとpackをかけてもいいけど、遅いのでここではしないことにする)、といった感じで処理を分けることができる。

まぁ、当然のことながら最初からnormalizeされているJARなんて存在しないので、pack200形式で流すためにはどこかで事前にpack200 –repackを流しておく必要はある。とすると、jarsignerだけオンライン/オンデマンドで、normalizeがオフライン/バッチに、とわざわざ分割する意味があるのか、jarsignerまで含めて全部バッチの方がいいんじゃないか、という意見もでるとは思う。

ここであえて2つに分けて考えるのにはもちろん理由がある。jarsignerをオフライン処理にしてしまうと署名忘れ=即障害になってしまうわけだけど、normalizeだけをオフラインバッチにすれば万が一normalizeし忘れたJARがあったとしても、最低限jarsignerはかかるのでエラーを回避することができる。本番運用では署名忘れなんてあってはいけないので論外で、事前のリリースプロセスにじっくり時間をかけて準備万端で挑むものだろう。しかしテスト環境ではリリースの速さとかダウンタイムの最小化が重要になってくるわけで、じっくり時間をかけてリリースに成功しても誰も褒めてはくれないし、そんなことをしていたら依頼されたリリース作業の半分もこなせない。jarsignerだけは何があっても必ずかかっている、ということに確信が持てるだけで、障害報告を受けたときに「jarsignerがうまくいっていないのかも?」という可能性をいきなり排除できるわけで、調査に無駄な時間を使うことを避けられるのだ。

Zipアーカイブコメントを読む

そんなわけで、JARファイルがnormalizeされているかどうかを調べる方法。unpackされたJARを判別するには、JARファイルにPACK200というアーカイブコメントがついているかどうかで判断できる。めったに使わないけど、Zipファイルってアーカイブレベルとファイルエントリーレベルでコメント書けるんだよね。

Javaの場合、アーカイブコメントはZipFileクラスのgetComment()で取得できる。しかしこのメソッド、残念なことに使えるのはJava SE 7からである。今開発しているシステムはJava SE 6なので、使えない。

とはいっても、まぁアーカイブコメントくらいなら、バイナリ形式が公開されているファイル形式だし何とかなるんじゃないかとおもって仕様書見ながら書いたのが以下のコード。

import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.math.*;
import static java.nio.channels.FileChannel.MapMode.*;

public class ZipParser {

  private final static int ARC_COMMENT_OFFSET = 22;
  private final static int MAX_COMMENT_LENGTH = (int) Character.MAX_VALUE;
  private final static int CENTRAL_DIR_LENGTH = ARC_COMMENT_OFFSET + MAX_COMMENT_LENGTH;

  public static void main(String... args) throws Exception {
    File source = new File(args[0]);
    if (Integer.MAX_VALUE < source.length()) {
       throw new IllegalArgumentException("File larger than 2GB is not supported.");
     }
     // Reverse iterate from end of file.  Stop search when;
     // 1) reached begining of file or
     // 2) scanned size of CENTRAL_DIR_LENGTH
     FileInputStream stream = new FileInputStream(source);
     try {
       long searchLimit = Math.max(0L, source.length() - CENTRAL_DIR_LENGTH);
       int i = (int) source.length();
       MappedByteBuffer buf = stream.getChannel().map(READ_ONLY, 0, (int) i);
       buf.order(ByteOrder.LITTLE_ENDIAN);
       while (i > searchLimit)
        if (0x06 == buf.get(--i))         // 0x06
          if (0x05 == buf.get(--i))       // 0x05
            if (0x4B == buf.get(--i))     // K
              if (0x50 == buf.get(--i)) { // P
                buf.position(i + ARC_COMMENT_OFFSET - 2 /* sizeof(short) */);
                break;
              }
      if (i >= searchLimit) {
        char commentLength = buf.getChar();
        if (0 < commentLength) {
          byte[] commentBinary = new byte[commentLength];
          buf.get(commentBinary, 0, commentLength);
          System.out.println(new String(commentBinary, Charset.forName("UTF-8")));
          return;
        }
        System.err.println("No archive comment found.");
        System.exit(1);
      }
    } finally {
      stream.close();
    }
  }
}

軽く説明

Zipファイルはファイル毎にエントリーがあって最後にcentral directoryという構造体がある。アーカイブコメントはこのcentral directoryの一番最後にあるend of central directoryについているので、今回の目的に限っていえば極端な話、このend of central directoryだけ検査すれば良い。

end of central directoryは22バイト+最大64KBの可変長アーカイブコメントからなる。ファイルの中身に全く興味がない以上頭から読んでいくのは馬鹿馬鹿しいので、ファイルの一番後ろから上に向かって最大65,558バイト分まで順番に読んでみて、end of central directoryのsignatureである0x06054b50(‘P’,’K’,0x05,0x06)を探す。

end of central directoryのsignatureを見つけたら晴れてend of central directoryの構造体が作れるわけだけど、今回はコメントにしか興味がないのでそれ用のclassは作らない。代わりに、最後の2バイトがアーカイブコメントの長さを表していることが分かっているので、いきなりoffset20バイト目を読んじゃう。16ビット整数はJavaだとshortだけど、この場合長さなので負の数ということはないはずで、Cでいうところのunsigned shortで取りたいのでByteBuffer#getShort()ではなくByteBuffer#getChar()で取る。長さが0よりも大きければ、シグネチャからのoffset22バイト目〜長さ分のbyte[]をもってきて、文字列に変換。

とりあえずこんな感じで無事、normalizeしたJARからPACK200が取れた。バイナリを読む処理とか久しぶりに書いたけどNew I/Oって便利だな。もっと広範囲につかわれてもいいAPIな気がするわ。

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s