独自キャラクタセットの作成

Javaでは簡単に独自キャラクタセットを追加する方法が提供されています。以下はそのサンプルです。

サンプルの仕様

サンプルで作成するキャラクタセットは「Unicode文字の各バイトで上位4ビットと下位4ビットを入れ替える」といった単純な仕様とします。例として「あ」という文字は 0x3042 ですが、このキャラクタセットでは 0x0324になります。

当然、コンソールやエディタ等で表示はできませんが、ちょっとしたObfuscatorのように使えるかもしれません。

実装が必要なクラス

独自キャラクタセットを作成するためには以下の具象クラスを作成する必要があります。

抽象クラス説明
java.nio.charset.spi.CharsetProviderキャラクタセットサービスプロバイダ
java.nio.charset.Charsetキャラクタセット
java.nio.charset.CharsetEncoderエンコーダ(Unicodeからbyte)
java.nio.charset.CharsetDecoderデコーダ(byteからUnicode)

キャラクタセットサービスプロバイダは複数のキャラクタセットを提供可能です。この場合、Charset, CharsetEncoder, CharsetDecoderのセットを複数作成することになります。

CharsetProvider

CharsetProviderのサンプルです。このクラスはProviderがサポートするキャラクタセットのIteratorを返すcharsetsメソッド、キャラクタセット名に対応したCharsetインスタンスを返すcharsetForNameメソッドを実装します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package testcs;

public class TestProvider extends CharsetProvider{

    private Map _charsets = new Hashtable();

    public TestProvider(){
        _charsets.put("TESTCS"new TestCharset("TESTCS"));
    }

    public Iterator charsets(){
        return _charsets.values().iterator();
    }

    public Charset charsetForName(String csName){
        return (Charset)_charsets.get(csName.toUpperCase());
    }
}

サンプルでは"TESTCS"というキャラクタセット名を提供します。

Charset

Charsetクラスのサンプルです。このクラスではキャラクタセットのエンコーダ、デコーダのインスタンス生成などを行います。containsメソッドは引数で与えられたキャラクタセットの全ての文字を表現できる場合にtrueとなります。含んだ部分のコードポイントが同じ「スーパーセット」である必要はありません。

サンプルのCharsetはUnicode全文字を表現可能なので、常にtrueを返します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package testcs;

public class TestCharset extends Charset{

    public TestCharset(String name){
        super(name, null);
    }

    public CharsetDecoder newDecoder(){
        return new TestDecoder(this);
    }

    public CharsetEncoder newEncoder(){
        return new TestEncoder(this);
    }

    public boolean contains(Charset charset){
        return true;
    }
}

CharsetEncoder

エンコーダはUnicodeをbyte列に変換します。エンコーダは基本的に encodeLoop のみ実装すればよく、必要に応じて他のメソッドを実装します。例えばエンコーダが内部状態(シフトの状態など)を維持する必要がある場合、resetやflushメソッドを実装する必要があるかも知れません。サンプルのエンコーダは単純にUnicode文字各バイトの上位4bitと下位4bitを入れ替えます。

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
package testcs;

public class TestEncoder extends CharsetEncoder{

    public TestEncoder(Charset cs){
        super(cs, 2.0f, 2.0f,
                new byte[]{ (byte)0xff, (byte)0xdf });
    }

    public CoderResult encodeLoop(
                CharBuffer in, ByteBuffer out){

        int pos = in.position();

        try{
            while (in.hasRemaining()){
                int unicode = in.get();
                int b1 = (unicode & 0xf000) >> 12 |
                         (unicode & 0x0f00) >>  4;
                int b2 = (unicode & 0x00f0) >>  4 |
                         (unicode & 0x000f) <<  4;

                if (out.remaining() < 2){
                    return CoderResult.OVERFLOW;
                }
                out.put(new byte[]{ (byte)b1, (byte)b2 });
                pos++;
            }
        }
        finally{
            in.position(pos);
        }
        return CoderResult.UNDERFLOW;
    }
}

コンストラクタにおけるスーパークラスの呼び出しで 置換バイトを 0xff, 0xdf としています。これは UnicodeのREPLACEMENT CHARACTER(U+FFFD)をサンプルの仕様に合わせてビットを入れ替えたものです。

CharsetDecoder

デコーダはbyte列からUnicode文字に変換します。サンプルではビット並びを元に戻すことでUnicode文字を復元します。エンコーダと同様に decodeLoop のみ実装が必要です。

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
package testcs;

public class TestDecoder extends CharsetDecoder{

    public TestDecoder(Charset cs){
        super(cs, 0.5f, 1.0f);
    }

    public CoderResult decodeLoop(
                    ByteBuffer in, CharBuffer out){

        int pos = in.position();

        try{
            while (in.hasRemaining()){
                if (in.remaining() < 2){
                    return CoderResult.UNDERFLOW;
                }
                int b1 = in.get() & 0xff;
                int b2 = in.get() & 0xff;

                int unicode =
                    (b1 & 0xf0) << 4  |
                    (b1 & 0x0f) << 12 |
                    (b2 & 0xf0) >> 4  |
                    (b2 & 0x0f) << 4;

                if (out.remaining() < 1){
                    return CoderResult.OVERFLOW;
                }
                out.put((char)unicode);
                pos += 2;
            }
        }
        finally{
            in.position(pos);
        }
        return CoderResult.UNDERFLOW;
    }
}

プロバイダ構成ファイル

プロバイダ構成ファイルをMETA-INF/servicesディレクトリに "java.nio.charset.spi.CharsetProvider"というファイル名で作成し、キャラクタセットプロバイダのクラス名を記述します。

testcs.TestProvider

サンプルではtestcs.TestProviderを指定します。

jarファイルの作成

作成したクラスとプロバイダ構成ファイルをjarにパッケージングします。

jar cvf testcs.jar testcs META-INF

jarファイルの中身は以下のような構成になります。

META-INF/MANIFEST.MF
META-INF/services/java.nio.charset.spi.CharsetProvider
testcs/TestProvider.class
testcs/TestCharset.class
testcs/TestEncoder.class
testcs/TestDecoder.class

テスト

以上で独自キャラクタセットが利用可能になります。クラスパスに testcs.jar を設定し、新しいキャラクタセット"TESTCS"が使用できるかテストします。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test{

    public static void main(String[] args)
                        throws java.io.IOException{

        String s = "abcdefg";
        byte[] bytes = s.getBytes("TESTCS");

        new sun.misc.HexDumpEncoder().encodeBuffer(
                            bytes, System.out);
        System.out.println(new String(bytes, "TESTCS"));
    }
}

サンプルソース

testcs.zip