火曜日 3 06, 2007

Java SE 6: Jar Improvements and Unicode Normalization

以前から、私は作成したドキュメントのアーカイブにはlhaを好んで使用していました。その理由は、ドキュメントのファイル名に日本語が使われていたとしても、lhaでアーカイブしておけば、どのOSにそのファイルアーカイブを持っていってもほぼ問題なく、もとの日本語のファイル名を復元することができるからです。tarやzipはファイル名に対する国際化対応がなされていないため、クロスプラットフォームでのドキュメント・アーカイブという目的には適していないというのが実情です。

しかし、どのOSでもlhaがいつでも手軽に使える状態にあるかというと、そうではありません。MacやWindowsならlhaのツールは簡単に入手できるのですが、Solarisでは別途(コンパイルを伴う)インストールが必要です。

また、lhaの漢字ファイル名対応にはもう一つ欠点があります。lhaでは、OS毎に漢字コードが決めうちになっており、UNIX版lhaではデフォルトでファイル名の漢字コードがEUCに固定されています。これは、lhaのコンパイル時に決定されるため、もし、SolarisやLinuxでEUC以外のロケールで運用する場合には、lhaのconfigureオプションで好みの漢字コードに変更してコンパイルし直す必要があります。参考までに、lhaのREADMEファイルに記述されている漢字ファイル名対応に関するconfigureオプションの記述を抜粋します。

・アーカイブ中の漢字ファイル名

  オリジナルの LHa for UNIX 1.14i はアーカイブに格納するファイル名の漢
  字コードに関して無頓着です。コンパイル時に MULTIBYTE_CHAR を定義した
  ときでもアーカイブ中の Shift JIS ファイル名を EUC にすることもなく、
  EUC コードのまま(正確にはシステムの漢字コードのまま)アーカイブに格納
  したりします。

  autoconf 版では、configure オプション --enable-multibyte-filename に
  より漢字ファイル名が使用でき、アーカイブに格納されるファイル名の漢字
  コードを SJIS 固定として扱います。

  --enable-multibyte-filename の引数(システムのファイル名の漢字コード
  指定)は、以下の通りです。

      --enable-multibyte-filename=sjis
            システムの漢字コードを SJIS として扱います。
      --enable-multibyte-filename=euc
            システムの漢字コードを EUC として扱います。
      --enable-multibyte-filename=utf8
            システムの漢字コードを UTF-8 として扱います。
            今のところ Mac OS X でだけこのオプションをサポートします。
      --enable-multibyte-filename=auto (または yes または引数なし)
            システムの漢字コードを自動で判別します。自動といっても現状は、
            Cygwin, MinGW, HP-UX の場合に SJIS、Mac OS X の場合 UTF-8、
            それ以外を EUC とみなすだけです。
      --enable-multibyte-filename=no
      --disable-multibyte-filename
            ファイル名のマルチバイトサポートを無効にします。

  デフォルトは、auto です。

ファイル名の漢字コードを自由に変更できるUNIX系OSにおいては、lhaコマンドはアーカイブ解凍時のファイル名の漢字コードをランタイムに指定できないばかりか、UTF-8固定にすることもできません。

それでは、クロスプラットフォームなファイルアーカイバとして、lhaの代わりにJDK/JREに含まれるjarコマンドで代用するというアイデアはどうでしょうか。jarコマンドは、アーカイブ形式にzip形式を採用しており、アーカイブ内のファイル名は常にUTF-8でエンコードされるようになっています。UNIX系OS上では、jarコマンドは圧縮・解凍するときにLANG環境変数をみて、適切にファイル名の漢字コードを変換するようになっています。一見、jarコマンドをlhaの代わりに使用するのは良さそうに思えますが、いくつか不満な点があります。それは、

  1. META-INFフォルダが必ず作成されてしまう
  2. jarコマンドはアーカイブの解凍時にファイルのタイムスタンプを復元しない
  3. Mac OS Xで圧縮したjarを他のOSで解凍すると一部のファイル名で文字化けが発生する

1.は解凍後に不用なMETA-INFフォルダを削除すればいいのですが、2.と3.については従来は回避方法がありませんでした。しかし、Java SE 6になって2.の問題は解決し、3.の問題はワークアラウンドが簡単に実現できるようになりました。

Java SE 6のjarがファイルのタイムスタンプを復元するようになったことは、Java SE 6のリリースノート:Jar and Zip Enhancementsにも記述されています。リリースノートには、64,000エントリ制限の解除、Windowsファイル名の256文字制限の解除などが明記されており、jarコマンドはクロスプラットフォームでのファイルアーカイバとしても実用的に使えるようになったと言えます。

残る問題はMac OS Xとのファイル名互換性問題だけですが、これもJava SE 6のUnicode Normalization APIのお蔭で簡単に回避できるようになりました。このワークアラウンドを紹介する前に、Mac OS Xのjarコマンドで何が問題なのかをもう少し詳しく見てみます。

例えば、Mac OS X上で「Česká」と「漢字コード」というファイル名を含むアーカイブをjarコマンドで作成します。これをWindows 2000/XPに転送し、jarコマンドで解凍すると下図のように文字化けを起こします。

このように、「ド」は「ト・」に、「Č」は「C・」に、「á」は「a´」に化けてしまっています。実は、Mac OS Xにおけるファイル名の漢字コードは、ロケールに関わらずUTF-8が使用されています。それはそれでよいのですが、このUTF-8が問題で、Mac OS XのファイルシステムではNFD(Normalization Form Decomposition)というUnicodeの方言が使用されています。NFDコーディングルールに従えば、

「ド(U+30C9)」 → 「ト(U+30C8)」+「゛(U+3099)」
「Č(U+010C)」 → 「C(U+0043)」+「ˇ(U+030C)」

のように分離されます。Mac OS Xのjarコマンドは、NFD形式の分離されたUTF-8のファイル名でjarアーカイブを作成してしまうのですが、これを他のWindows 2000/XPのjarコマンドで解凍する際、分離された個別の文字をそのままShift_JISにマッピングしようとするため、文字によっては2文字分に展開され、マッピングできない文字は「・」として表示されてしまったと言うことになります。

ちなみに、WindowsではVistaからNFD形式にも対応しているため、下図のようにUTF-8+NFD形式のファイル名であっても正しく表示することができます。

しかしながら、多くのOSやアプリケーションではロケールをja_JP.UTF-8にした場合でもNFDに対応していないものが多く、「が」のような文字を1つの文字として扱うNFC(Normalization Form Composition)の方が一般的に使用されているようです。

この問題のワークアラウンドとしては、Mac OS Xで作成されたNFD形式のjarのファイル名をNFC形式に変換してやればよいことになります。Java SE 6ではNormalization形式の異なる文字列間を変換するためのAPI java.text.Normalizerクラスが追加されました。このAPIを使用すれば比較的簡単に、NFD形式ファイル名のjarファイルをより一般的なNFC形式ファイル名のjarファイルに変換することができます。以下に、このjarファイル変換のためのサンプルコードを示します。

/\*
 \* JarFileNameFixer.java
 \*
 \* Mac OS Xで作成されたjarファイルに含まれるファイル名のコーディング形式を
 \* NFD形式からNFC形式に変換する。要Java SE 6。
 \* 使い方:
 \* $ java JarFileNameFixer <old.jar> <new.jar>
 \*/

import java.io.BufferedInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.Normalizer;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;

public class JarFileNameFixer {
    
    private void convertEntryNames(JarFile src, JarOutputStream dst)
    throws IOException {
        for (Enumeration<JarEntry> entries = src.entries();
        entries.hasMoreElements();) {
            JarEntry oldent = entries.nextElement();

            // ディレクトリは無視
            if (oldent.isDirectory()) {
                continue;
            }
            
            // ファイル名をNFC形式に変換する。
            String newname = Normalizer
                    .normalize(oldent.getName(), Normalizer.Form.NFC);
            System.out.println(newname);
            
            // 変換したファイル名でJarエントリを作成
            JarEntry newent = new JarEntry(newname);
            newent.setTime(oldent.getTime());
            newent.setComment(oldent.getComment());
            
            // Jarエントリの追加
            dst.putNextEntry(newent);
            
            // コンテンツの書き込み
            BufferedInputStream is =
                    new BufferedInputStream(src.getInputStream(oldent));
            byte buf[] = new byte[1024];
            int count;
            while((count = is.read( buf, 0, 1024)) != -1) {
                dst.write(buf, 0, count);
            }
            is.close();
            dst.closeEntry();
        }
    }

    private void usage() {
        System.out.println("Usage: java "+this.getClass().getName()
        +" <old.jar> <new.jar>");
    }
    
    public static void main(String[] args) throws IOException {
        JarFileNameFixer fixer = new JarFileNameFixer();
        if (args.length != 2) {
            fixer.usage();
            System.exit(1);
        }
        JarFile srcjar = new JarFile(args[0]);
        JarOutputStream dstjar = new JarOutputStream(
                new FileOutputStream(args[1]));

        fixer.convertEntryNames(srcjar, dstjar);

        dstjar.close();
    }
    
}

jarファイルの扱いが面倒なので若干長いソースコードになっていますが、ポイントは赤字で示した Normalizar.normalize(CharSequence,Normalizer.Form) メソッドの呼出し部分だけです。上記のツールでNFC形式に変換したMac OS Xで作成されたjarファイルは、Mac OS X、Windows、Linux、Solarisのいずれの環境でも漢字混じりのファイル名を適切に展開できるようになります。また、もしアプリケーション開発の中で上記のような文字化けを起こした場合は、Java SE 6のNormalization APIを思い出してください。

参考:

About

Takashi Nishigaya
Principal Consultant
Technology Solution Consulting
Oracle Consulting Services

Search

Categories
Archives
« 7月 2014
  
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
  
       
今日