コールバック

「この処理は、こことそこで使いまわせるよね。差分を関数で渡して埋められるよう、コールバックで実装してもらえないか?」と振ったら、「コールバック?」と固まってしまったようなのでフォローした。

そのときは、われながら教科書みたいな例を出せたんじゃないかなと自画自賛したくなって、今まとめなおしてみたのだけれど、あまりいい例ではなかった、気がしてきた。余計混乱させたんじゃなかろうか。無念。

      • -

メソッド、という言い方だとしっくりこないので「関数」と呼び変えます。
関数を呼ぶときに関数を渡し、呼び出した関数の中で渡された関数を呼び返す。これがコールバックです。

実例。

function add(a, b) { return a + b; };

これは(意味がないように見えますが)、次のように書き換えられます。

function add(a, b) {
  var plus = function(a, b) { return a + b; };
  return plus(a, b);
};

この add の中で定義した関数 plus は、関数の外から引数で渡すように書き換えられます。

function plus(a, b) { return a + b; };
function add(plus, a, b) { return plus(a, b); };

この時点での add 関数定義の意味を漂白すると、引数 a, b に(コールバックとして渡される)関数 f を適用する関数 apply が見えてきます。

function apply(f, a, b) { return f(a, b); };

apply に渡す関数 f を以下で差し替えるとどうなるでしょうか。

function sub(a, b) { return a - b; };
function mul(a, b) { return a * b; };
function div(a, b) { return a / b; };

何が言えるかのまとめ。
最初の定義 function add(a, b) { return a + b; } は (+) という計算アルゴリズムが固定されていて自由度がない。一方で、コールバック関数を渡せる apply を仲介すると各種計算アルゴリズムをあとから注入できる。(つまり設計の自由度、抽象度があがった)

なお、コールバックを使う代表的な設計手法に、テンプレートメソッドや計算アルゴリズムの差し替え(先の apply のような)があります。

ZipFileの生存期間とyield

zip ファイルに格納されたオブジェクトを、その名前とデータのタプルの列に変換して、後で処理できるようにしたい、とおもってですね。イテレーターオブジェクトにしておけば、複数の zip ファイルを itertools.chain でつないで別の処理に回すことができるじゃないかって、まあそんなことを考えたわけです。

結論から言えば、 yield を使ってこう書けばよい。

def iter_from(filename):
  from zipfile import ZipFile as openZip
  with openZip(filename) as zipf:
    for name in zipf.namelist():
      if not name.endswith('/'):
        yield (name, zipf.open(name))

上を使って、例えば以下のような処理が書ける:

for (name, data) in iter_from('hoge.zip'):
  print name, data.read()

や、面白みはないけれど、ちゃんと zip の中のファイルだけ取り出して中身を読み出せる。 iter_from の実装では with 構文を使っているので、最後まで処理すれば openZip で開いたファイルも閉じられる!

いくつかの苦労

最初はこんな実装をしたんですよ。

def iter_from(filename):
  from zipfile import ZipFile as openZip
  with openZip(filename) as zipf:
    return ((name, zipf.open(name)) for name in zipf.namelist() if not name.endswith('/'))

いつぞや内田さんに教えてもらった generator 構文を使って、いやあ、 python ってスッキリ書けて素晴らしいなあ、などと鼻歌まじりに。
そしたらですね、 with の中で return しているのでスコープを抜けて zipf が閉じられてしまうわけですよ。参照するとエラーが起きちゃうわけですよ。閉じた ZipFile を open すること、まかりならず! って。

仕方がないなあ…… じゃあ、 zipfile 開き直すのはどうにも癪だけど、こうか?

def iter_from(filename):
  from zipfile import ZipFile as openZip

  def _aux(name):
    from cStringIO import StringIO
    with openZip(filename) as zipf:
      return StringIO(zipf.open(name).read())

  with openZip(filename) as zipf:
    return ((name, _aux(name)) for name in zipf.namelist() if not name.endswith('/'))

まあこれなら、冒頭の実装のように動くことは動く。ただタプルの要素を参照するたびに、再度 zipfile を作成し直さないといけないってのがどうにもイケてないな、と。

で、さらに色々考えて冒頭の yield 使ったバージョンにたどり着いたという次第。

ジェネレーター構文による簡潔さと比べるとあまりに拙いけれど、スコープとオブジェクトの生存期間とがうまくミックスしているあたりがわたくし好みです。

tar.gz から .deb をつくって Ubuntu に Oracle Java をインストール

Oracle JavaLinux 向け公式インストーラーは RPM と tarball との二択。
alian というパッケージ変換ツールを使うと RPMDEB に変えられるというので Ubuntu で試してみたところ、インストール後のスクリプトで alternatives がないとエラーが(というのも Ubuntu では update-alternatives を使うからだが)発生してしまった。

どうしたものかと少々さぐったところ、tarball から alien を使って DEB を作るという記事 http://uchan.hateblo.jp/entry/2016/03/19/004153 が見つかった。
というので真似してみた。

http://www.oracle.com/technetwork/java/javase/downloads/index.html から最新 64bit 版 JDK をダウンロード。(これを書いている時点では 8u91)
/opt/oraclejdk_8-amd64 に配置するつもりでパッケージ化準備を進める。

$ DST="/opt/oraclejdk_8-amd64"
$ mkdir -p /tmp/jdk{/DEBIAN,$DST}
$ cat >/tmp/jdk/DEBIAN/postinst <<EOF
#!/bin/sh
cd "$DST/bin"
for f in *; do
  update-alternatives --install "/usr/bin/\$f" "\$f"\
 "$DST/bin/\$f" 1000
done
EOF
$ chmod 0755 /tmp/jdk/DEBIAN/postinst
$ tar xzf ~/Downloads/jdk-8u91-linux-x64.tar.gz\
 -C /tmp/jdk$DST --strip-component 1
$ cd /tmp/jdk
$ tar czf $(basename $DST).tar.gz opt/ DEBIAN/
$ fakeroot alien --version=1.8.0.91 --bump=0 $(basename $DST).tar.gz

これで /tmp/jdk の下に oraclejdk-8-jdk_1.8.0.91-1_all.deb ができあがる。
これをシステムに導入するときは dpkg コマンドを使う。:

$ sudo dpkg -i oraclejdk-8-jdk_1.8.0.91-1_all.deb

うん、できた。
念のため update-alternatives も実行して、インストールした JDK を使うよう選択しておく。:

$ sudo update-alternatives --config java

領域を固定パターンで埋める memfill

ある値で int 配列を埋めたい、構造体配列を初期化したい、なんて要求はざらにあるような気がするのだけれど、標準ライブラリーにはいってないのはなぜなんだぜ?
見たとおり、速度は O(log nmemb)。

void* memfill(void *ptr, const void *data, size_t size, size_t nmemb) {
  char *const head = ptr;
  if (nmemb > 0) {
    size_t n;
    memcpy(head, data, size);
    for (n = 1; 2*n < nmemb; n *= 2)
      memcpy(head + n * size, head, n * size);
    memcpy(head + n * size, head, (nmemb - n) * size);
  }
  return head + size * nmemb;
}

戻り値は埋めたメモリー領域の末尾のひとつ向こう。続けて操作をしたいときだとかに便利だし、あと STL の標準アルゴリズムを真似したいというのがあってだね。

AES暗号ユーティリティ

AES 暗号をお手軽に使えるよう InputStream/OutputStream にかぶせて使うアダプター Stream をつくってみた。

暗号化アダプター

暗号化したいデータを格納した InputStream is があるとして、これに 16 バイト(あるいは 24、もしくは 32 バイト)の鍵データを適用すると IV 込みで暗号化したデータを読み出せる AesEncInputStream。

InputStream wrapped = new AesEncInputStream(key, is, null);

三つめの引数は IV を生成する Random オブジェクトをオプションで渡せるようにしている。 null を渡すとパッケージローカルの SecureRandom オブジェクトを使う。

もしくは OutputStream os に書き込むときに暗号化を施すアダプター AesEncOutputStream。

OutputStream wrapped = new AesOutputStream(key, os, null);

三つめの引数は同様に IV 生成につかうオプション Random。パディングは AesEncOutputStream#close() を呼び出したときに付け足すので #close() 忘れに注意。

復号アダプター

冒頭に IV 16 バイトが付された暗号化データを読み出せる InputStream is があるとして、これに鍵データを適用すると、復号したデータを読み出せる AesDecInputStream。

InputStream wrapped = new AesDecInputStream(key, is);

もしくは OutputStream os に書き込むときに復号を実行する AesDecOutputStream。

OutputStream wrapped = new AesDecOutputStream(key, os);

最後のパディングを処理は AesDecOutputStream#close() を呼び出したときにするため、ちょっと使いにくいかもしれない。

実装

以下実装。

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.SequenceInputStream;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Enumeration;
import java.util.Random;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import static javax.crypto.Cipher.DECRYPT_MODE;
import static javax.crypto.Cipher.ENCRYPT_MODE;

class Local {
    static final SecureRandom random = new SecureRandom();
}

class AesEncInputStream extends SequenceInputStream {

    public AesEncInputStream(byte[] key, InputStream is, Random random)
            throws GeneralSecurityException {
        super(new Helper(key, is, random));
    }

    private static class Helper implements Enumeration<InputStream> {
        final InputStream[] iss = new InputStream[2];
        int pos = 0;

        Helper(byte[] key, InputStream is, Random random) throws GeneralSecurityException {
            final byte[] iv = new byte[16];
            (random != null ? random : Local.random).nextBytes(iv);

            final Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
            c.init(ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));

            iss[0] = new ByteArrayInputStream(iv);
            iss[1] = new CipherInputStream(is, c);
        }

        @Override
        public boolean hasMoreElements() {
            return pos < 2;
        }

        @Override
        public InputStream nextElement() {
            return iss[pos++];
        }

    }

}

class AesEncOutputStream extends CipherOutputStream {

    public AesEncOutputStream(byte[] key, OutputStream os, Random random)
            throws GeneralSecurityException {
        super(os, getCipher(key, os, random));
    }

    private static Cipher getCipher(byte[] key, OutputStream os, Random random)
            throws GeneralSecurityException {
        final byte[] iv = new byte[16];
        (random != null ? random : Local.random).nextBytes(iv);
        try {
            os.write(iv, 0, 16);
        } catch (IOException e) {
            throw new GeneralSecurityException("Cannot write IV (16bytes)", e);
        }

        final Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
        c.init(ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
        return c;
    }

}

class AesDecInputStream extends CipherInputStream {

    public AesDecInputStream(byte[] key, InputStream is)
            throws GeneralSecurityException {
        super(is, getCipher(key, is));
    }

    private static Cipher getCipher(byte[] key, InputStream is) throws GeneralSecurityException {
        Throwable t = null;
        try {
            final byte[] iv = new byte[16];
            if (is.read(iv) == 16) {
                final Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
                c.init(DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
                return c;
            }
        } catch (IOException e) {
            t = e;
        }
        throw new GeneralSecurityException("Cannot read IV (16bytes)", t);
    }

}

class AesDecOutputStream extends OutputStream {

    private final Cipher c;
    private final OutputStream os;
    private final byte[] key;
    private final byte[] iv = new byte[16];
    private int pos = 0;

    public AesDecOutputStream(byte[] key, OutputStream os) throws GeneralSecurityException {
        c = Cipher.getInstance("AES/CBC/PKCS5Padding");
        this.key = key;
        this.os = os;
    }

    @Override
    public void write(int oneByte) throws IOException {
        final byte[] buffer = {(byte) oneByte};
        write(buffer, 0, 1);
    }

    @Override
    public void write(byte[] buffer, int offset, int count) throws IOException {
        final int n = pos == 16 ? 0 : Math.min(16 - pos, count);
        if (n > 0) {
            System.arraycopy(buffer, offset, iv, pos, n);
            if ((pos += n) == 16) {
                try {
                    c.init(DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
                } catch (GeneralSecurityException e) {
                    throw new IOException("Failed to initialize cipher", e);
                }
            }
        }
        if (count - n > 0) os.write(c.update(buffer, offset + n, count - n));
    }

    @Override
    public void flush() throws IOException {
        os.flush();
    }

    @Override
    public void close() throws IOException {
        try {
            if (pos == 16) os.write(c.doFinal());
        } catch (GeneralSecurityException e) {
            throw new IOException("Failed to finalize cipher", e);
        }
    }

}
使用例
    public void testInPair() throws Exception {
        final byte[] key = new SeucreRandom().get(16);
        final String expect = "hello, world! hello, world! hello, world! hello, world!!";

        final byte[] encrypted;
        {
            final InputStream is = new AesEncInputStream(key, new ByteArrayInputStream(expect.getBytes()), null);
            final ByteArrayOutputStream os = new ByteArrayOutputStream();
            final byte[] buffer = new byte[8];
            for (int read; (read = is.read(buffer, 0, Local.random.nextInt(8) + 1)) != -1; )
                os.write(buffer, 0, read);
            encrypted = os.toByteArray();
        }

        final byte[] decrypted;
        {
            final InputStream is = new AesDecInputStream(key, new ByteArrayInputStream(encrypted));
            final ByteArrayOutputStream os = new ByteArrayOutputStream();
            final byte[] buffer = new byte[8];
            for (int read; (read = is.read(buffer, 0, Local.random.nextInt(8) + 1)) != -1; )
                os.write(buffer, 0, read);
            decrypted = os.toByteArray();
        }

        assertEquals(expect, new String(decrypted));
    }

ブロック暗号の簡単な解説

以下のキーワードの関係をできるだけ平易に説明します。

  • ブロック暗号
  • IV
  • パディング
  • ストリーム暗号
  • 暗号化モード

ブロック暗号

AES や DES と呼ばれる暗号はブロック暗号、特定の長さのデータを「鍵」と呼ばれるデータで暗号化する方式です。
データと鍵の組み合わせに対して暗号化されるデータは唯一定まります。このため鍵と暗号化データからもとのデータを解読(復号)できます。

ストリーム暗号

一般的な応用では、処理するデータの長さは暗号方式の要求する長さ(ブロック長)にならないので対象データをブロックに分割します。そして最後のブロックはぴったりの長さになるよう詰め物(パディング)をします。
ストリーム暗号とは、このようにして任意の長さのデータを暗号化できるようにしたブロック暗号のことです。

暗号化モード

さてストリーム暗号では一般に、暗号化するブロックに前段のブロックの暗号化結果をかき混ぜて簡単に解読されないようにします。 *1
けれど最初のブロックにはかき混ぜるべき前の暗号化結果がないので、暗号化開始時に IV (initial vector) と呼ばれるランダムなデータ列を生成して使います。
ストリーム暗号でのデータのかき混ぜ方は暗号化モードと呼ばれていて、主流のモードは前段の "CBC" です。

*1:というのはデータと鍵が同じなら暗号化結果が唯一定まるため、なにもしなければ解読者へのヒントになってしまうためです。たとえばプロトコルの最初の "HELO" が "IFMP" に写されるとして、メッセージのやりとりを継続的に盗聴している人は "IFMP" を "HELO" と解読できなくてもセッションの特定のメッセージが発せられたことはわかります。