daily dayflower

2010-02-02

RPDL 用 cups-raster フィルタを書いた

Ricoh IPSiO SP6120Linux から使うシリーズ。

前回Ricoh RPDL IV Laser Printer Foomatic ドライバの出力をフィルタするスクリプトを書いて両面印刷に対応させたりしました。

この場合の印刷フローは下記のようになっています。

  • ドキュメント =[cairo]=> PostScript =[Ghostscript rpdl driver]=> RPDL native

(実際には CUPS フロントエンドがデータをうけとって、foomatic-RIP フィルタを介して Ghostscript を呼び出したりしてるんですが、そのへんのフローは無視します)

で、Ghostscript rpdl driver の吐く RPDL native データ形式スクリプトで加工した、ということになってます。

今回作成した cups-raster フィルタを使った場合の印刷フローは下記のようになります。

(実際には pstoraster フィルタコマンドによって Ghostscript cups driver が呼び出されたりしてるんですが、そのへんのフローは無視します)

上記のフローに比べて1レイヤ増えていることがわかります。ですが、CUPS raster 形式という可搬性のあるフォーマットを経由している*1ので、プリンタドライバを開発する上ではこちらのほうが楽なのです。しかも標準入出力を読み書きするスタンドアロンプログラムを書けばいいので、デバッグも(そこそこ)楽です。


ともあれ、今回開発したドライバを下記においておきます。

LIPS & ESC/Page & NPDL & RPDL 対応 Ghostscript デバイスドライバからエスケープシーケンスやロジックなどを抜き出した*2ので、倣って GPL v2 としてあります。


以下注意点など。

  • さまざまな機種で入念にテストしたわけではないので、おかしな出力となり紙を浪費したり、紙詰まりをおこすことがありえます
  • どこに原因があるのかわかりませんが、ページ全体が真っ白になったり一部しか印字されないことがありました
    • Ubuntu 9.10 の cairo による PostScript 出力がおかしいのか
    • Ubuntu 9.10 の Ghostscript による PostScript 解釈がおかしいのか
    • Ubuntu 9.10 の Ghostscript cups driver がおかしいのか、不明
      • cups-raster フィルタに画像データがわたってくる時点でブランクになってたりした
      • 該当する PostScript ファイルを Ghostscript で画面に表示してもブランクになってた
      • でも通常の gs-rpdl ドライバだと出力されたりするんですよね、なぜか
  • landscape なデータを 2-up で portrait 出力する場合、なぜかページ順が上下逆になってしまいます
    • cups-raster フィルタ時点では 2-up 時点で統合した画像になっているので対処しようがありません(より上位の部分がおかしいんだと思う)
    • portrait なデータを 2-up で出力する場合は問題ありません

おまけ(リコーオフィシャルのドライバがあった)

と、この文章を書く上で「Ghostscript RPDL」で検索かけたら、リコーの提供している Ghostscript RPDL ドライバ*3がありました。最初っからそのキーワードで検索かければよかった。

Ubuntu 8.04 LTS用ですが、GPL なのでソースも公開されてます。このソースをもとに今回のコードをアップデートする作業がはじまるお*4

*1:この CUPS raster 形式を自力でパースする必要はありません。CUPS Raster API を使えばイメージデータを取得することができます。

*2:実際のインプリメントはほぼ書き下ろしましたが。

*3:といっても Ghostscript RPDL ドライバなので、cups-raster フィルタと異なり Ghostscript を置き換える必要があります。

*4:といっても、リバースエンジニアリングの結果からたいがいのシーケンスはもともと組み込んでありましたけど。このシーケンスを入れてもいいんだ、とか意味がいくつかわかったシーケンスがあるのが大きい。

2009-05-12

APR を利用したプログラムの autotools 化

APR を利用したプログラムを書いていて,それを配布したい。

という素晴らしいテキストがあるんだけど,これはソースディストリビューションに APR library を同梱する前提でかかれています。さいわいにして,最近の Linux ディストリビューションでは Apacheインストールすると APR library が共有ライブラリとしてインストールされますし,開発者向け APR library パッケージも用意されています。なので,システムにインストールされている APR library を利用する前提でやってみようと思いました。

と思ったら,

というリソースがありました。かなり参考にしつつ,一挙手一投足で書いてみます。

なお,これは autotools 入門ではありません。APR むけにステップが増えているので,これを参考にすると混乱すると思います。

サンプルプログラム

現在日時を取得して表示するプログラムです。

唯一のソース sample.c はこんな感じ。

/* sample.c */
#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#include <apr.h>
#include <apr_general.h>
#include <apr_pools.h>
#include <apr_strings.h>
#include <apr_file_io.h>
#include <apr_time.h>

static apr_pool_t *pool;
static apr_file_t *apr_stdout, *apr_stderr;

int
main(int argc, const char * const *argv, const char * const *envp)
{
    char *str_ctime;
    char *str;

    if (apr_app_initialize(&argc, &argv, &envp) != APR_SUCCESS) {
        fputs("apr_app_initialize() failed", stderr);
        exit(1);
    }
    atexit(apr_terminate);

    apr_pool_create(&pool, NULL);

    apr_file_open_stdout(&apr_stdout, pool);
    apr_file_open_stderr(&apr_stderr, pool);

    str_ctime = apr_palloc(pool, APR_CTIME_LEN);
    apr_ctime(str_ctime, apr_time_now());

    str = apr_pstrcat(pool, "Current time is ", str_ctime, ".\n", NULL);
    apr_file_puts(str, apr_stdout);

    return 0;
}

やや非効率な書き方もしてますが,サンプルということで。

  • apr_app_initialize() の部分は apr_initialize() でもいいと思いますが,いちおう CLI なのでこのようにしました。Windows の場合,このほうがロケールの初期設定とかもやってくれるそうですし。
  • わざわざ apr_file_open_stdout() などで標準入出力を取得していますが,ふつーに Unix で動くプログラムを書くなら stdio の stdout とか fprintf() を使って問題はないです。やはり Windows 環境を考慮する場合,このほうがベターらしい。
  • apr_pstrcat()strcat()インタフェースが異なるのでやや注意です。最後の NULL を忘れてハマったことがあります。仕様としては LL 系の join() に近く,便利便利。

んで,Makefile.am がこちら。とりあえずこれさえ用意すれば,さまざまなファイルの雛形をよしなに作成してくれるのが automake のうれしいところ。

ACLOCAL_AMFLAGS = -I m4

bin_PROGRAMS = sample

sample_SOURCES = sample.c

ACLOCAL_AMFLAGS という部分は,後述する aclocalフラグです。といっても,ここで指定したフラグが自動的に aclocal にわたされるわけではありません。

autotools には,ビルド時に configure.ac などの変更を検出して,configure 系のスクリプト等を自動的に再生成(autoreconf)する機能があるのですが,そのような自動再生成の際に aclocal を駆動するときのオプションです。あとで書きますが,手で aclocal を実行する時は aclocal -I m4 のように明示的に指定する必要があります。

bin_PROGRAMS などは通常の automake の記述なので説明は省略します。

configure.ac の雛形をつくる

Makefile.am(と,ソース)から configure.ac の雛形を autoscan コマンドで生成することができます。

$ autoscan

autom4te: configure.ac: no such file or directory
autoscan: /usr/bin/autom4te failed with exit status: 1

初めて実行した際にはこのようなエラーがでますが,無視して構いません。autoscan が,既存の configure.ac のチェック「も」行うためエラーとなっています。

これで雛形となる configure.scan というファイルが生成されます。今回のソースだと下記のような内容です。

#                                               -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.

AC_PREREQ(2.59)
AC_INIT(FULL-PACKAGE-NAME, VERSION, BUG-REPORT-ADDRESS)
AC_CONFIG_SRCDIR([sample.c])
AC_CONFIG_HEADER([config.h])

# Checks for programs.
AC_PROG_CC

# Checks for libraries.

# Checks for header files.
AC_HEADER_STDC
AC_CHECK_HEADERS([stdlib.h])

# Checks for typedefs, structures, and compiler characteristics.
AC_C_CONST

# Checks for library functions.
AC_CHECK_FUNCS([atexit])

AC_CONFIG_FILES([Makefile])
AC_OUTPUT 

この雛形ファイルを configure.ac というファイル名にコピーして,そちらを修正していきます。

$ cp configure.scan configure.ac

元来 AC_INIT の部分を修正すれば最低限の configure.ac となるのですが,今回は APR を利用しているので,その環境をうまくとりこむためのマクロなども書いていきます。

#                                               -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.

AC_PREREQ(2.59)
AC_INIT([sample], [0.1], [dayflower@example.com])
AM_INIT_AUTOMAKE([foreign no-installinfo dist-bzip2 no-dist-gzip])
AC_CONFIG_SRCDIR([sample.c])
AC_CONFIG_HEADER([config.h])

# Checks for programs.
AC_PROG_CC

APR_FIND_APR([], [], 1, 1)
APR_SETIFNULL(CC, `$apr_config --cc`)
APR_SETIFNULL(CPP, `$apr_config --cpp`)
APR_ADDTO(CPPFLAGS, `$apr_config --cppflags --includes`)
APR_ADDTO(LDFLAGS, `$apr_config --ldflags`)
APR_ADDTO(LIBS, `$apr_config --libs --link-ld`)

# Checks for libraries.

# Checks for header files.
AC_HEADER_STDC
AC_CHECK_HEADERS([stdlib.h])

# Checks for typedefs, structures, and compiler characteristics.
AC_C_CONST

# Checks for library functions.
AC_CHECK_FUNCS([atexit])

AC_CONFIG_FILES([Makefile])
AC_OUTPUT 

変更したのは

  • AC_INIT() のプロジェクト名とかあれこれ
  • AM_INIT_AUTOMAKE()
  • APR_* まわり

だけになります。

まず AM_INIT_AUTOMAKE() ですが,autoscan をおこなっただけではこのマクロを追加してくれないので,手でいれておきます。AM_INIT_AUTOMAKE() についていくつかの書き方の流儀があるのですが,現在では AC_INIT() にパッケージ名やバージョンを指定しておいて,無引数AM_INIT_AUTOMAKE を書くやり方が推奨されるようです。

上記の例では無引数ではなく,

AM_INIT_AUTOMAKE([foreign no-installinfo dist-bzip2 no-dist-gzip])

のようにいくつかの automake のためのフラグを指定しています。

foreign というのは,このディストリビューションGNU などの標準ソースディストリビューションと異なるということを明示するものです。これがないと gnu モードとなり,COPYING や INSTALL などのファイルを要求されてしまいます。

dist-bzip2 というのは,配布物を tar.bz2 として固めるという意味になります。これを記述するだけだと,tar.gz と tar.bz2 の双方が生成されるので no-dist-gzip というフラグを追加して tar.gz を生成しないようにしています。

これらのフラグについて詳細は Changing Automake’s Behavior - automake を参照してください。

APR まわりのマクロについて上記から抜粋します。

APR_FIND_APR([], [], 1, 1)
APR_SETIFNULL(CC, `$apr_config --cc`)
APR_SETIFNULL(CPP, `$apr_config --cpp`)
APR_ADDTO(CPPFLAGS, `$apr_config --cppflags --includes`)
APR_ADDTO(LDFLAGS, `$apr_config --ldflags`)
APR_ADDTO(LIBS, `$apr_config --libs --link-ld`)

APR_FIND_APR() というマクロapr-(1-)config コマンドがどこにあるか,とか,APR library のインストール状況について調べています。また,第一引数と第二引数で空文字列[])を指定していますが,これは今回 APR library を添付せずに,システムにインストールされているものをそのまま使うため,このような指定になっています。

第3引数の「1」は,configure 時に --with-apr で APR の場所を指定しなかった場合,インストールされている apr-(1-)config を探してきてくれるようにするためのスイッチです。まあつまり,今回のようなケース(APR をバンドルしない場合)では 1 を指定しておけってこった。

第4引数の「1」は要求する APR library のメジャーバージョン番号です。RHEL 5 では 1.2 系列なので「1」にしておきました。

このマクロの仕様について詳しくは後述する find_apr.m4 を覗いてみてください。

APR_SETIFNULL()APR_ADDTO()コンパイラコンパイルフラグなどを APR むけに調整しています。apr-(1-)config コマンドを使ったことがあるのなら,意味はわかると思います。といいつつ,実は APR とか CFL とか - odz buffer からまるっとコピーしました。

APR をハンドリングするための configure 用 m4 マクロを準備する

と,ここまで当然のように APR_HOGEHOGE などのマクロを利用してきましたが,これは automake / autoconf で標準的に使われるマクロではありません。これらのマクロを使うための「ライブラリ」を別途インストール(添付)する必要があります。

小山さんの例では sinclude() 関数configure.ac から直接インクルードしていますが,最近の autotools では m4/ というディレクトリを用意してそこに拡張マクロライブラリを添付し,aclocal引数-I m4 とすることで,そこからもマクロライブラリをロードするようにするのが一般的なようです*1

今回使ったマクロライブラリは APR library に apr_common.m4find_apr.m4 として添付されているのですが,RHEL の apr-devel には残念ながら含まれていませんでした。

なのでわたしは下記の SVN レポジトリからファイルを取得しました。

(バージョンが 1.2.7 なのは,RHEL 5 の APR が 1.2.7 なのでそれにあわせたためです。もっと新しいものを利用してもいいと思います)

$ mkdir m4 && cd m4

$ wget http://svn.apache.org/.../apr_common.m4
$ wget http://svn.apache.org/.../find_apr.m4

$ cd ..

上記レポジトリには apr_common.m4, find_apr.m4 のほかに apr_hints.m4, apr_network.m4, apr_threads.m4 なども用意されていますが,お好みで。入れておいても害はありません(バグがあったらストップしてしまいますけど)。

configure で利用される aclocal.m4 を生成する

さて,これで m4 用の環境がととのったので,今回のプロジェクトのための aclocal.m4 マクロファイルを aclocal コマンドで生成します。

さきほど述べたように,手で生成する場合は -I m4 オプションを加える必要があります(おっと,もちろん,APR など利用していなくて通常の autotools を利用したいだけなら,オプションなしで実行して構いません)。

$ aclocal -I m4

m4/apr_common.m4:25: warning: underquoted definition of APR_CONFIG_NICE
m4/apr_common.m4:77: warning: underquoted definition of APR_MKDIR_P_CHECK
...... snip snip snip ......

結構たくさんの warnings が出力されましたが,現在のところ無視して構いません*2

ともかく,これで configure スクリプトの生成に必要となる aclocal.m4 が無事生成されました*3

config.h.in を生成する

C 向けにアーキテクチャごとの差異を表現してくれる config.h のもととなる config.h.in も生成します。これは autoheader コマンドを実行すれば生成されます。

$ autoheader

ポータビリティを吸収する自分オリジナルの #define を書きたくなる誘惑にかられますが,autoreconf でも再生成されるようですし,これを手でいじるのは得策ではないようです。

どうしても,という場合は AH_* マクロconfigure.ac に書けばいいのかな。

Makefile.am (と configure.ac)から Makefile.in を生成する

Makefile の雛形となる Makefile.in も生成しなくてはなりません。これは automake コマンドで生成できます。

$ automake -a -c

configure.ac: installing `./install-sh'
configure.ac: installing `./missing'
Makefile.am: installing `./depcomp'

-a--add-missing と同義で,一般的に必要なファイル(ちらりと触れた gnu スタイルだと COPYING とか)や configure システムで必要となるサブプログラムinstall-sh とか)を追加してくれます。

-c--copy と同義で,追加するファイルをシンボリックリンクではなく実体コピーしてくれます。

configure.ac から configure を生成する

最後に configure スクリプトautoconf コマンドで生成しましょう。

$ autoconf

記述に問題がなければ configure スクリプトが生成されるはずです。長い道のりでした。

configure して build

やっとこ configure できるようになりました。

$ ./configure

checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes

...... snip snip snip ......

checking dependency style of gcc... gcc3
checking for APR... yes
  setting CPP to "gcc -E"
  setting CPPFLAGS to " -DLINUX=2 -D_REENTRANT -D_GNU_SOURCE
                        -D_LARGEFILE64_SOURCE -I/usr/include/apr-1 "
  setting LDFLAGS to " "
  setting LIBS to "  -lpthread -ldl -lapr-1"
checking how to run the C preprocessor... gcc -E
checking for egrep... grep -E
checking for ANSI C header files... yes

...... snip snip snip ......

configure: creating ./config.status
config.status: creating Makefile
config.status: creating config.h
config.status: executing depfiles commands

APR についても無事検出してくれたようです。APR_* マクロはもっと後段に書けばよかったかな?

配布物を生成する

以上で

  • make all
  • make clean
  • make install
  • make uninstall

など,「ありがち」なターゲットをサポートしたポータブルな Makefile が完成しました。

わずか5行(実質2行)の Makefile.am ファイルから,ここまでのものが出来上がるのはちょっぴり感動……てほどでもないですか,最近は。

あと,ご存知のかたも多いかと思いますが,make dist とすると,配布物を tar ボールで固めて生成してくれます。

$ make dist

...... snip snip snip ......

$ ls *tar*

sample-0.1.tar.bz2

automakeオプションdist-bzip2 no-dist-gzip を指定しておいたので,tar.bz2 が配布物になっています。

この dist 系のターゲットは

  • make dist (tar ボールで固めた配布物を生成)
  • make distdir (tar ボールで固める前の,展開されたディレクトリを生成)
  • make distclean (配布物むけに不要なファイル―― Makefileconfig.h ――も削除する clean; より強力な clean として使ってらっしゃる方も多いかと)

などがあります。

まとめ

  • APR を利用したプロジェクトの autotools 化には,APR library に添付されている m4 マクロを利用すると便利
  • RHEL 5 の apr-devel には,この m4 マクロは添付されていない
  • 外部 m4 マクロを configure で利用したい場合,同一階層におかず,m4/ などディレクトリを掘ってそこに配置したほうがよい
    • そして aclocal の際に aclocal -I m4

おわりに

あ,libtoolize について触れてなかった。個人的に好きくないので触れませんでした。

autoconf だの automake だのややこしいですが,

  • どのような環境でもビルドできるようにするための configure スクリプト
  • configure スクリプトを生成するための autoconf ツールスイート
    • おもに m4 を利用している
  • configure.in とか Makefile.in をいちから手で書くのが面倒なので automake ツールスイートができた

と思えばいいのかな。TeX に対する LaTeX のように,autoconf に対する automake。違ってたらすいません。


実際には Linux 限定ならわざわざ autotools 化する必要はなくて,apr-config や pkg-config を使って Makefile をごりごり書いてもいいと思います。とくにインハウスツールなら。


そもそも論として APR 人気ないというのもありますね。GLib のほうが人気?

みたいな話もあるし。あと,APR を使った「ライブラリ」というのは作りにくい気がする。apr_initialize() は誰が発行するの?とか。

*1Handling Local Macros - automake 参照

*2Writing your own aclocal macros - automake にあれこれ書いてあったんですが,ちょっと意味がわかりませんでした。

*3autom4te.cache なるフォルダも生成されます。What is autom4te.cache? - Autoconf 参照。

2009-03-17

APR-util の DBD API を使ってみる

APR(の一部の APR-util)には,各種データベースを統一インタフェースで使うことのできる DBD API があります。のでちょっと使ってみました。

ちなみに普通に C でアプリケーションを書いている人にはあまりおすすめできないと思います。統一インタフェースというほど方言を吸収しているとはいえないので。もっとも portability layer としてすでに APR を使用していて,ちょっくら DB も(軽く)使用しようかというケースなら向いているかもしれません。どちらかというと,DB を使用する Apache のモジュールを開発している場合に,個別のデータベースクライアントライブラリにバインドするよりはこちらを使ったほうが,柔軟性(データベースの選択)が増すという用途です。さらに mod_dbd なんてのもあわせて使うとおもしろいかも。

どのデータベースエンジンがサポートされているのか

apr_dbd_open_ex()API ドキュメントによると,

  • PostgreSQL
  • SQLite2
  • SQLite3
  • Oracle
  • MySQL
  • FreeTDS

がサポートされているようです。

実際にはどうなのか,apr_dbd_get_driver() を利用してたしかめてみます(CentOS 5.2 の APR Util 1.2.7 で検証)。

#include <stdio.h>
#include <stdlib.h>
#include <apr_pools.h>
#include <apu.h>
#include <apr_dbd.h>

static void
print_apr_error(apr_status_t status, const char *message)
{
    char buf[1024];

    fprintf(stderr, "%s%s%s\n", apr_strerror(status, buf, sizeof(buf)),
                                message ? ": "    : "",
                                message ? message : "");
}

static void
error_exit(apr_status_t status)
{   
    print_apr_error(status, NULL);
    exit(status);
}

int
main(int argc, char *argv[])
{
    apr_pool_t *pool;
    apr_status_t r;
    const apr_dbd_driver_t *driver;
    const char **pdb;

    static const char *dbs[] = {
        "sqlite3",
        "mysql",
        "pgsql",
        NULL,
    };

    apr_pool_initialize();
    apr_pool_create(&pool, NULL);   /* root pool */

    r = apr_dbd_init(pool);
    if (r != APR_SUCCESS)
        error_exit(r);

    for (pdb = dbs; *pdb; pdb ++) {
        fprintf(stdout, "\n[%s]\n", *pdb);

        r = apr_dbd_get_driver(pool, *pdb, &driver);
        if (r != APR_SUCCESS) {
            print_apr_error(r, NULL);
        }   
        else {
            fputs("OK!\n", stdout);
        }   
    }

    return 0;
}

ビルドするのがちょっと骨なので pkg-config を利用した Makefile を置いておきます。

CFLAGS :=       $(shell pkg-config --cflags apr-1) \
                $(shell pkg-config --cflags apr-util-1)
LIBS :=         $(shell pkg-config --libs apr-1) \
                $(shell pkg-config --libs apr-util-1)

TARGET :=       drivers

all:    build

build:  $(TARGET)

clean:
        rm -f $(TARGET)

test:   $(TARGET)
        ./$(TARGET)

$(TARGET):      main.o
        $(CC) -o $@ $^ $(LIBS)

.PHONY: all build clean test

さて,実行すると,

$ ./drivers

[sqlite3]
OK!

[mysql]
This function has not been implemented on this platform

[pgsql]
OK!

のような結果になりました。

実は apu.h ヘッダに

#define APU_HAVE_PGSQL         1
#define APU_HAVE_MYSQL         0
#define APU_HAVE_SQLITE3       1
#define APU_HAVE_SQLITE2       0

とあります(もちろん環境により内容が異なります)。プログラム書いてまでたしかめるもんでもなかったな。

MySQL 用ライブラリが含まれていないのは,CentOS 5.2 採録の Apache 2.2.3 のころはライセンス不整合問題のため含まれていなかったようです*1。今回は SQLite3 を使うつもりなので,まぁいいや。

データベースの準備

もちろん DBD API で query を発行してテーブルを作成してもよいんですが,面倒なので CLI から作成しておきます。

$ sqlite3 test.db

SQLite version 3.3.6
Enter ".help" for instructions

sqlite> CREATE TABLE t_user ( id INTEGER, name VARCHAR );

sqlite> INSERT INTO t_user (id, name) VALUES ( 1, 'dayflower' );

sqlite> SELECT * FROM t_user;
1|dayflower

sqlite> .q

なにはともあれ SELECT

めんどうなので一部実装(エラー表示やエラーチェック)をはしょっていますが,SELECT を行うプログラムを書いてみました。

#include <stdio.h>
#include <stdlib.h>
#include <apr_pools.h>
#include <apu.h>
#include <apr_dbd.h>

static void print_apr_error(apr_status_t status, const char *message);
static void error_exit(apr_status_t status);

int
main(int argc, char *argv[])
{
    apr_status_t r;
    apr_pool_t *pool;
    const apr_dbd_driver_t *driver;
    apr_dbd_t *dbh;
    apr_dbd_results_t *dbr;
    apr_dbd_row_t *row;
    int n, nrows, ncols;

    apr_pool_initialize();
    apr_pool_create(&pool, NULL);   /* root pool */

    r = apr_dbd_init(pool);
    if (r != APR_SUCCESS)
        error_exit(r);

    r = apr_dbd_get_driver(pool, "sqlite3", &driver);
    if (r != APR_SUCCESS)
        error_exit(r);

    r = apr_dbd_open(driver, pool, "test.db", &dbh);
    if (r != APR_SUCCESS)
        error_exit(r);

    dbr = NULL;
    apr_dbd_select(driver, pool, dbh, &dbr, "SELECT * FROM t_user", 0);

    ncols = apr_dbd_num_cols(driver, dbr);
    fprintf(stdout, "num_cols = %d\n", ncols);

    nrows = apr_dbd_num_tuples(driver, dbr);
    fprintf(stdout, "num_tuples = %d\n", nrows);

    row = NULL;
    for (n = 0; apr_dbd_get_row(driver, pool, dbr, &row, -1) == 0; n ++) {
        int i;

        fprintf(stdout, "record #%d\n", n);
        if (row != NULL)
            for (i = 0; i < ncols; i ++)
                fprintf(stdout, "[%d]: %s\n", i,
                                apr_dbd_get_entry(driver, row, i));
    }

    r = apr_dbd_close(driver, dbh);

    return 0;
}
  1. apr_dbd_open() で DB をオープンして
  2. apr_dbd_select() で選択系 SQL を発行し
  3. apr_dbd_get_row() で各行を fetch*2
  4. apr_dbd_get_entry() で各行の各コラムを取得
  5. さいごに apr_dbd_close() でクローズ

という手順です。

APR-util 1.4 だと apr_dbd_get_name() という API でコラムの名前(テーブルの列名)を取得できるのですが,APR-util 1.3 ではできませんでした。残念。

    dbr = NULL;
    apr_dbd_select(driver, pool, dbh, &dbr, "SELECT * FROM t_user", 0);

のような表現が頻出しますが,このような場所では NULL を入れておかないとセグフォって落ちます。API ドキュメントに

res
pointer to result set pointer. May point to NULL on entry

と書いてあったので,取得する行によっては NULL が返ることもあるよ,とかのほほんと構えていたら落ちたのでびっくりしました。よくよく読んだら,最初に実行する時は,とかそういう意味ですね。

ほんとは変数宣言部分で NULL を代入すればいいんですが,このような経緯もあり,自戒をこめてあえて後段で NULL を代入しています。


さて,実行します。

$ ./select

num_cols = 2
num_tuples = 1
record #0
[0]: 1
[1]: dayflower

無事取得できました。

せっかくだから INSERT

つぎに,DBD API から INSERT をしてみます。

#include <stdio.h>
#include <stdlib.h>
#include <apr_pools.h>
#include <apr_strings.h>
#include <apu.h>
#include <apr_dbd.h>

static void print_apr_error(apr_status_t status, const char *message);
static void error_exit(apr_status_t status);

int
main(int argc, char *argv[])
{   
    apr_status_t r;
    apr_pool_t *pool;
    const apr_dbd_driver_t *driver;
    apr_dbd_t *dbh;
    apr_dbd_transaction_t *trans;
    int nrows;

    apr_pool_initialize();
    apr_pool_create(&pool, NULL);   /* root pool */

    r = apr_dbd_init(pool);
    if (r != APR_SUCCESS)
        error_exit(r);

    r = apr_dbd_get_driver(pool, "sqlite3", &driver);
    if (r != APR_SUCCESS)
        error_exit(r);

    r = apr_dbd_open(driver, pool, "test.db", &dbh);
    if (r != APR_SUCCESS)
        error_exit(r);

    trans = NULL;
    apr_dbd_transaction_start(driver, pool, dbh, &trans);

    apr_dbd_query(driver, dbh, &nrows,
                  apr_psprintf(pool, "INSERT INTO t_user (id, name)"
                                     "VALUES (\"%s\", \"%s\")",
                               apr_dbd_escape(driver, pool, "123", dbh),
                               apr_dbd_escape(driver, pool, "foo", dbh))
                 );

    fprintf(stdout, "%d rows accected.\n", nrows);

    apr_dbd_transaction_end(driver, pool, trans);

    r = apr_dbd_close(driver, dbh);

    return 0;
}

無駄にトランザクション処理も行っています。まぁトランザクションといっても,エラーが発生したらロールバックし(というよりその後に発行される SQL を無視する)発生しなかったらコミットするというだけのプリミティブなものですが*3

手順としては,

  1. apr_dbd_escape() でエスケープ処理を行って
  2. apr_dbd_query() で選択系以外の SQL 文を発行

ということになります。

プリペアードステートメントは使えないの?と思いますが,一応 API として用意はされています。しかし,APR-util 1.3 と SQLite の組み合わせでは使えなかった*4ので,今回のプログラムでは上記のように自力でエスケープ処理を行っています。プリペアードステートメントを使う場合,

    apr_dbd_prepared_t *ps;
    const char *values[2];

    ps = NULL;
    apr_dbd_prepare(driver, pool, dbh, "INSERT INTO t_user (id, name)"
                                       "VALUES (?, ?)",
                    NULL, &ps);

    values[0] = "12345";
    values[1] = "foo";
    apr_dbd_pquery(driver, pool, dbh, &nrows, ps, 2, values);

のような構文になります。

さて,実行しますと,

$ ./insert

1 rows accected.

無事1行挿入できたようです。

CLI をつかってたしかめてもいいのですが,せっかくなので先ほど作成した SELECT コマンドを実行してみます。

$ ./select

num_cols = 2
num_tuples = 2
record #0
[0]: 1
[1]: dayflower
record #1
[0]: 123
[1]: foo

うまくいっていますね。

メモリ使用量について

「ハンドル」を取得するための API は,だいたい次のような形になります。

int apr_dbd_get_row(const apr_dbd_driver_t *driver, apr_pool_t *pool,
                    apr_dbd_results_t *res, apr_dbd_row_t **row, int rownum)

このように,ポインタへのポインタをとります。ので,ハンドルのメモリアロケーションは API 側でやってくれるようです。そしてリリースのための API は定義されていません。

ということは,一度データベースのハンドルを取得したのちに,複数の SQL 文を発行する(=apr_dbd_results_t ハンドルが生成される)と,どんどんメモリが確保されていくことになります(ソースを追っていないので詳細は不明ですが,おそらくそうでしょう)。今回のように CLI を書くだけなら問題にはなりませんが,Apache モジュールを書いている場合,(プロセスが落ちるまで)どんどんメモリの使用量がふえていきます。

今回はそこまでのコーディングはやっていませんが,このような場合,各リクエストごとなどの単位でサブメモリプールを生成して(あるいは request_recpool を利用して),リクエスト終了時にメモリプールを破棄するようにすればいいでしょう。

そうやって考えてみれば,APR-util DBD API の多くの関数で,つどつど apr_pool_t を要求している理由がわかりました。

*1漢(オトコ)のコンピュータ道: Apache mod_dbd設定編 参照

*2apr_dbd_select()random パラメータを 1 にすると,行指定で fetch できます。今回は random = 0 なので,next row fetch しているだけです。

*3:APR-util 1.4 だとトランザクションのモードを指定できるようになっています。といってもやはりプリミティブな機能であることにはかわりありませんが。

*4:どちらが悪いのかは不明。

2008-12-05

gnome-keyring を利用してコマンドライン上のパスワードを置換する

今日はサーバ向けじゃなくて Linux クライアント向け(かつ GNOME 上)な話です。

コマンドラインのオプションからパスワードを指定できるコマンドがありますよね。例えば,リモートデスクトップに接続するコマンド rdesktop

% rdesktop -u dayflower -p hogehoge machine

とか。mysql とか psql とかもそうですね。

この手のコマンドをふつうにシェルから実行すると,ヒストリに残ったりしていやな感じです。シェルから実行するのではなくとも,自動実行のためにスクリプト等を書くと,どうしてもそのスクリプトに生パスワードが残ることになります。

なので,パスワードを gnome-keyring に保管してもらうことにして,保管したパスワードを使ってコマンドを実行できるようなプログラムを書いてみました。一度(ログイン時などに)keyring を解除すれば,その後パスワードを聞かれることなくプログラムを実行できるというわけです。ssh-agent みたいな感じです。

使い方

使い方ですが,まず

% pwexec -r rdesktop set

Password: ********
Retype Password: ********
Successfully stored password.

のように(今後コマンドラインで利用する)パスワードを登録します。-r オプションの部分は一意であればなんでもよいです(パスワードのラベルのようなものです)。今回はわかりやすく rdesktop にしました。

もしこれまでの操作でまだ keyring を解除していない場合,keyring のパスワードを聞かれるかもしれません。

さて,いざ実行する段ですが,

% pwexec -r rdesktop exec rdesktop -u dayflower -p %PASSWORD% machine

のように,exec というアクションを指定します。そしてそのあとに,通常実行する場合のコマンドラインを書いていきます。%PASSWORD% の部分は例示ではなく,実際にその値を指定します。このトークンで示された部分がパスワードで置換されます。


おっと。いままでの例だとセッション keyring を利用していたので,一度ログオフするとパスワードが消えてしまいます。永続化して保存したい場合,

% pwexec -r rdesktop -k default set

のように,-k default として keyring の名前を明示的に指定します。default とすると,デフォルトの keyring になります。なにも指定しない場合,-k session のように指定したのと同じ意味をもつことになります。

セッション keyring でない場合,seahorse(アクセサリの「パスワードと暗号鍵」)の「パスワード」タブに表示され*1,そちらで変更とか削除とかもできます。このプログラムでも変更や削除もできるようにしてますが。


ちなみに,上記の例を実行して ps を実行すると,

% ps -efw| grep rdesktop

dayflower 6245 6235 1 15:32 pts/0 00:00:00 rdesktop -u dayflower -p XXXXXXXX

のようになります。これはこのプログラムがパスワードを XXXXXXXX で隠したわけではなく,rdesktop がメモリ上のコマンドラインのパスワード部分を隠すようにインプリメントしてあるからです。コマンドラインからパスワードを指定できるプログラムはたいていこのようになっています*2。ですが,このパスワード隠蔽にいたるまでの一瞬のタイミングでパスワードを読み取れることがありうるので,セキュリティ上厳密な環境ではコマンドラインからパスワードを与えることは避けたほうがよいでしょう。えーっと何がいいたいかというと,このプログラムではパスワードを置換して execvp() でなげているだけので,シェルのヒストリ等には残らなくても理論上はパスワードが漏洩しうる,ということです。まして上記のようなインプリメントを行っていない insecure なアプリケーションの場合,ps コマンドや /proc/* インタフェースから生パスワードを得ることが可能になってしまいます。構造上このプログラムでは対策のしようは(たぶん)ありません。

内部構造

GNOME Keyring の Simple Password Storage API を利用しています。なのでわりと最近(2.21 以降?)の GNOME でしかビルドできないと思います。Ubuntu 8.10 (Intrepid) で動作確認をおこないました。

Simple Password Storage API はかなり単純な API です*3ので下記サイトを読めば同じようなプログラムを書くことは難しくありません。なお,一般的には非同期インタフェースを利用するほうがよいということになっていますが,面倒だったので同期インタフェース - *_sync()API を利用しました。

参考にしたサイト

より深く

Simple Password Storage API では「スキーマ」というものを指定します。デフォルトでGNOME_KEYRING_NETWORK_PASSWORD というスキーマが用意されていますが,自分でスキーマを定義するのもそんなに難しくありません。本プログラムでも独自のスキーマを定義しています。

スキーマとパスワードの関係についてちょっと説明します。

さきほどあげた GNOME_KEYRING_NETWORK_PASSWORD には,下記の attributes が存在します。

  • user
  • server
  • protocol
  • domain
  • port

パスワードを store する際,これらの attributes をすべて指定する必要はありません。たとえば,

  • user = dayflower
  • server = foo
  • protocol = http

だけを指定して password = bar のようにパスワードを store できます。

そしてパスワードを find する際 attributes を指定してマッチしたパスワードを得るわけですが,たとえば

  • user = dayflower

という attribute だけを指定しても*4上記の password がひっかかります。同一の user が指定されたパスワードがいくつかある場合,どれが返されるかはわかりません。たとえていうなら,

SELECT password FROM network_passwords WHERE user = dayflower LIMIT 1;

のようにしてパスワードを取得しているようなものです。しかも Simple Password Storage API では取得したパスワードに関連付けられた他の attributes を得る手段はありません*5

なお,スキーマが異なる場合,該当するパスワードがあっても取得できません。たとえば,

  • user
  • group

というスキーマが存在しており,このスキーム下で user = dayflower のパスワードを登録してあったとしましょう。上記のように GNOME_KEYRING_NETWORK_PASSWORD というスキーマのもとで user = dayflower という attributes のパスワードを取得しようとしても,こちらのスキーマのパスワードは取得対象にはならないということです。


今回のプログラムでは,

  • pwexec_ver (UINT32 型)
  • realm (STRING 型)

という attributes をもつスキーマを定義して使用しています。本当は realm attribute 1つで十分なのですが,上記のようにたまたま realm attribute 1つのスキーマを他のアプリケーションで利用していた場合にバッティングがおこってしまうので,あえてプログラムのバージョンをいれて基本的にバッティングが発生しないようにしています。

ソース

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <getopt.h>
#include <glib.h>
#include <gnome-keyring.h>

static const char  APPVER_KEY[] = "pwexec_ver";
static const guint APPVER_VALUE = 1;
static const char  REALM[]      = "realm";

static GnomeKeyringPasswordSchema pwexec_schema = {
    GNOME_KEYRING_ITEM_GENERIC_SECRET,
    {
        { APPVER_KEY, GNOME_KEYRING_ATTRIBUTE_TYPE_UINT32 },
        { REALM,      GNOME_KEYRING_ATTRIBUTE_TYPE_STRING },
        { NULL, 0 },
    }
};

static const gchar *action  = NULL;
static const gchar *keyring = GNOME_KEYRING_SESSION;
static const gchar *realm   = NULL;

static void
usage(void)
{
    fputs("usage: pwexec [OPTIONS] <action> [<command> <args> ...]\n", stderr);
}

int
main(int argc, char *argv[])
{
    static struct option options[] = {
        /* options */
        { "keyring", required_argument, NULL, 'k' },
        { "realm",   required_argument, NULL, 'r' },

        /* command, can be set as option form */
        { "set",     no_argument,       NULL, 'S' },
        { "delete",  no_argument,       NULL, 'D' },
        { "exec",    no_argument,       NULL, 'X' },

        /* help */
        { "help",    no_argument,       NULL, '?' },

        { NULL, 0, NULL, 0 }
    };
    GnomeKeyringResult keyres;

    g_set_application_name("pwexec");

    for (;;) {
        int c, final = 0;

        c = getopt_long(argc, argv, "-k:r:", options, NULL);
        if (c < 0)
            break;

        switch (c) {

        case 0:
        case 1:
            if (optind <= 1)
                return 1;
            action = argv[optind - 1];
            final = 1;
            break;

        case 'S':
        case 'D':
        case 'X':
            if (optind <= 1)
                return 1;
            action = argv[optind - 1] + 2;  /* skip prefix '--' */
            final = 1;
            break;

        case 'k':
            keyring = optarg;
            break;
        case 'r':
            realm = optarg;
            break;

        case '?':
            usage();
            return 1;
        }

        if (final)
            break;
    }

    if (! action
     || (strcmp(action, "exec")
      && strcmp(action, "set")
      && strcmp(action, "delete"))) {
        fputs("error: ", stderr);
        if (action)
            fputs("unsupported action was specified; ", stderr);
        else
            fputs("action was not specified; ", stderr);
        fputs("action can be one of 'exec', 'set', 'delete'.\n", stderr);
        usage();
        return 1;
    }

    if (! realm) {
        fputs("error: realm was not specified.\n", stderr);
        return 1;
    }

    if (! strcmp(action, "delete")) {
        keyres = gnome_keyring_delete_password_sync(&pwexec_schema,
                                                    APPVER_KEY, APPVER_VALUE,
                                                    REALM,      realm,
                                                    NULL);
        if (keyres != GNOME_KEYRING_RESULT_OK) {
            fprintf(stderr, "failed to delete password: %s\n",
                            gnome_keyring_result_to_message(keyres));

            return keyres;
        }

        fputs("Successfully deleted password.\n", stderr);
        return 0;
    }
    else if (! strcmp(action, "set")) {
        gchar *password = NULL, *retype = NULL;
        gchar *name = NULL;

        /* you must specify keyring */
        if (! keyring) {
            fputs("error: kerring was not specified.\n", stderr);
            return 1;
        }
        if (keyring && ! strcasecmp(keyring, "default"))
            keyring = GNOME_KEYRING_DEFAULT;

        /* overwrite check */
        keyres = gnome_keyring_find_password_sync(&pwexec_schema,
                                                  &password,
                                                  APPVER_KEY, APPVER_VALUE,
                                                  REALM,      realm,
                                                  NULL);
        if (keyres == GNOME_KEYRING_RESULT_OK) {
            char buf[16];

            gnome_keyring_free_password(password);
            fputs("password already exists in specified realm.\n"
                  "Overwrite? [Y/n]", stderr);
            fflush(stderr);

            if (! fgets(buf, 16, stdin))
                return 0;
            g_strstrip(buf);

            /* not 'YES' => don't advance */
            if (*buf != 0 && strcasecmp(buf, "yes") && strcasecmp(buf, "y"))
                return 0;
        }

        password = g_strdup(getpass("Password: "));
        retype   =          getpass("Retype Password: ");
        if (! password || ! retype || strcmp(password, retype)) {
            fputs("error: password error\n", stderr);
            gnome_keyring_free_password(password);
            return 1;
        }

        name = g_strdup_printf("pwexec key for %s", realm);

        keyres = gnome_keyring_store_password_sync(&pwexec_schema,
                                                   keyring,
                                                   name,
                                                   password,
                                                   APPVER_KEY, APPVER_VALUE,
                                                   REALM,      realm,
                                                   NULL);
        gnome_keyring_free_password(password);
        g_free(name);
        if (keyres != GNOME_KEYRING_RESULT_OK) {
            fprintf(stderr, "failed to store password: %s\n",
                            gnome_keyring_result_to_message(keyres));

            return keyres;
        }

        fputs("Successfully stored password.\n", stderr);
        return 0;
    }
    else if (! strcmp(action, "exec")) {
        gchar *password = NULL;
        int i, pindex = -1, r;

        if (! argv[optind]) {
            fputs("error: command line was not specified.\n", stderr);
            usage();
            return 1;
        }

        for (i = optind; argv[i]; i ++) {
            if (! strcmp(argv[i], "%PASSWORD%"))
                pindex = i;
        }
        if (pindex < 0) {
            fputs("error: '%PASSWORD%' template was not specified"
                  " in command line.\n", stderr);
            usage();
            return 1;
        }

        keyres = gnome_keyring_find_password_sync(&pwexec_schema,
                                                  &password,
                                                  APPVER_KEY, APPVER_VALUE,
                                                  REALM,      realm,
                                                  NULL);
        if (keyres != GNOME_KEYRING_RESULT_OK) {
            fprintf(stderr, "failed to find password: %s\n",
                            gnome_keyring_result_to_message(keyres));

            return keyres;
        }

        /* overwrite password template */
        argv[pindex] = password;

        /* now, execute! */
        r = execvp(argv[optind], argv + optind);
        /* in successful condition, it doesn't reach */

        perror("failed to execute");

        gnome_keyring_free_password(password);

        return r;
    }

    return 0;
}

ビルド方法

libglib や libgnome-keyring を使用しているのでいろいろインクルードディレクトリ等を指定しなきゃいけないんですが,pkg-config を使用すると取得できます。なので,下記のような Makefile を書いてビルドしました。

TARGET:=	pwexec

GLIB:=		glib-2.0
GNOME_KEYRING:=	gnome-keyring-1

CFLAGS+=	-O2 -Wall

CFLAGS+=	$(shell pkg-config --cflags $(GLIB))
CFLAGS+=	$(shell pkg-config --cflags $(GNOME_KEYRING))
LIBS+=		$(shell pkg-config --libs   $(GLIB))
LIBS+=		$(shell pkg-config --libs   $(GNOME_KEYRING))

all:	build

build:	$(TARGET)

clean:
	rm -f $(TARGET) *.o

$(TARGET):	$(TARGET).o
	$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< $(LIBS)

余談

コマンドラインのパースに popt を使おうと思ったんですが,今回のように「後続する引数を使ってコマンドを実行する」場合にうまくいかない(実行するコマンドのオプションも解釈してしまう)ので,結局 getopt_long を使いました。なので help が充実していません。残念。

コマンドライン解析部分で結構な行数をとってしまったのも残念。コアロジックはたいしたことないのですが。

*1:かつての gnome-keyring-manager ではセッション keyring も管理できたような気がするのですが。当時全然使ったことがなかったので記憶違いかもしれません。

*2mysql コマンドもこのように隠します。

*3:そのぶん利用できる機能も制限されていますが。

*4:もちろんすべての attibutes の値を指定してパスワードを取り出すこともできます。

*5:legacy でより低レベルな API でできるかどうかはわかりません。が Seahorse で attributes を見ることができるのでたぶんできるでしょう。

2008-11-16

mod_proxy_mapper.c

/* 
**  mod_proxy_mapper.c
*/ 

#include "httpd.h"
#include "http_config.h"
#include "http_log.h"
#include "http_protocol.h"
#include "http_request.h"
#include "http_core.h"
#include "ap_config.h"
#include "apr_strings.h"

#if 0 || defined(__GNUC__)      /* for syntax highlight */
#define strbegin(S, T)  \
    ({ size_t _l = strlen(T); \
       (strlen(S) >= _l && ! memcmp((S), (T), _l)); })
#else
#define strbegin(S, T)  \
    (strlen(S) >= strlen(T) && ! memcmp((S), (T), strlen(T)))
#endif

static int proxy_available = 0;

typedef enum mapper_type {
    MAPPER_UNDEFINED,
    MAPPER_NONE,
    MAPPER_FILE,
    MAPPER_URI,
} mapper_type;

typedef struct proxy_mapper_conf {
    mapper_type type;
    const char  *mapper;
} proxy_mapper_conf;

static void *
create_dir_config(apr_pool_t *p, char *dir)
{
    proxy_mapper_conf *conf
        = (proxy_mapper_conf *) apr_pcalloc(p, sizeof(proxy_mapper_conf));

    conf->type   = MAPPER_UNDEFINED;
    conf->mapper = NULL;

    return conf;
}

static const char *
set_proxy_mapper(cmd_parms *cmd, void *mconfig, const char *arg)
{
    proxy_mapper_conf *conf = (proxy_mapper_conf *) mconfig;
    int len;

    len = strlen(arg);
    if (! strcasecmp(arg, "none")) {
        conf->type   = MAPPER_NONE;
        conf->mapper = NULL;
    }
    else if (len >= 7 && ! memcmp(arg, "file://", 7)) {
        if (len == 7)
            return "file part of ProxyMapper is not specified.";
        /* @@TODO: check existence */

        conf->type   = MAPPER_FILE;
        conf->mapper = apr_pstrdup(cmd->pool, arg + 7);
    }
    else {
        conf->type   = MAPPER_URI;
        conf->mapper = apr_pstrdup(cmd->pool, arg);
    }

    return NULL;
}

static const command_rec proxy_mapper_cmds[] = {
    AP_INIT_TAKE1("ProxyMapper", set_proxy_mapper,
                  NULL, ACCESS_CONF | RSRC_CONF | OR_ALL,
                  "URI or path of mapper script"),
    { NULL }
};

static ap_filter_rec_t *null_input_filter_handle;
static ap_filter_rec_t *null_output_filter_handle;

static apr_status_t
null_input_filter(ap_filter_t *f, apr_bucket_brigade *bb,
                  ap_input_mode_t mode, apr_read_type_e block,
                  apr_off_t readbytes)
{
    apr_bucket *b;

    switch (mode) {

    case AP_MODE_INIT:
        return APR_SUCCESS;

    case AP_MODE_READBYTES:
    case AP_MODE_SPECULATIVE:
        b = apr_bucket_eos_create(f->c->bucket_alloc);
        APR_BRIGADE_INSERT_TAIL(bb, b);
        return APR_SUCCESS;

    default:
        return APR_EOF;

    }
}

static apr_status_t
null_output_filter(ap_filter_t *f, apr_bucket_brigade *bb)
{
    apr_brigade_destroy(bb);

    /* Yes, I know what I'm doing.
       This filter behaves like the bottom of output filters.
       So I do not call ap_pass_brigade() intentionally.  */
    return APR_SUCCESS;
}

static apr_status_t
set_null_input_filter(request_rec *r)
{
    ap_filter_t *f = apr_pcalloc(r->pool, sizeof(ap_filter_t));
    /* @@TODO: assert f */

    f->frec = null_input_filter_handle;
    f->r    = r;
    f->c    = r->connection;
    f->next = NULL;

    r->input_filters       = f;
    r->proto_input_filters = f;

    return APR_SUCCESS;
}

static apr_status_t
set_null_output_filter(request_rec *r)
{
    ap_filter_t *f = apr_pcalloc(r->pool, sizeof(ap_filter_t));
    /* @@TODO: assert f */

    f->frec = null_output_filter_handle;
    f->r    = r;
    f->c    = r->connection;
    f->next = NULL;

    r->output_filters       = f;
    r->proto_output_filters = f;

    return APR_SUCCESS;
}

module AP_MODULE_DECLARE_DATA proxy_mapper_module;

static int
hook_fixups(request_rec *r)
{
    proxy_mapper_conf   *conf;
    request_rec         *subreq = NULL;
    const char          *target = NULL;

    /*
     * no REQUEST_URI, it seems to be subreq-file; fast exit.
     */
    if (! r->uri || ! *(r->uri))
        return DECLINED;

    conf = (proxy_mapper_conf *) ap_get_module_config(r->per_dir_config,
                                                      &proxy_mapper_module);

    if (! conf)
        return DECLINED;

    if (conf->type == MAPPER_NONE || conf->type == MAPPER_UNDEFINED)
        return DECLINED;

    for (subreq = r->main; subreq; subreq = subreq->main) {
        if (! strcmp(subreq->uri, r->uri)) {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                          "Proxy Mapper loop detected on URI %s.",
                              r->uri);
            return HTTP_FORBIDDEN;
        }
    }

    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                  "Proxy Mapper start on URI %s.",
                      r->uri);

    switch (conf->type) {

    case MAPPER_FILE:
        if (! strcmp(r->canonical_filename, conf->mapper)) {
            ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r,
                          "Proxy Mapper itself detected on URI %s; DECLINED.",
                              r->uri);

            return DECLINED;
        }

        subreq = ap_sub_req_lookup_file(conf->mapper, r, NULL);
        break;

    case MAPPER_URI:
        if (! strcmp(r->uri, conf->mapper)) {
            ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r,
                          "Proxy Mapper itself detected on URI %s; DECLINED.",
                              r->uri);

            return DECLINED;
        }

        subreq = ap_sub_req_lookup_uri(conf->mapper, r, NULL);
        break;

    default:
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                      "Unsupported proxy mapper type (%d) for URI %s.",
                          conf->type, r->uri);
        return HTTP_FORBIDDEN;

    }

    if (subreq) {
        apr_status_t    status;
        apr_table_t     *outhdr;
        const char      *value;

        /* subreq might overwrite headers_in */
        subreq->headers_in = apr_table_copy(r->pool, r->headers_in);

        /* forbid use of request and response for subreq */
        set_null_input_filter(subreq);
        set_null_output_filter(subreq);

        /* set Content-Length to 0 */
        if (apr_table_get(subreq->headers_in, "Content-Length"))
            apr_table_set(subreq->headers_in, "Content-Length",
                                              apr_pstrdup(r->pool, "0"));

        status = ap_run_sub_req(subreq);
        if (status == 0 && subreq->status != 0)
            status = subreq->status;

        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                      "subreq status = %d on URI %s.",
                          status, r->uri);

        if (ap_is_HTTP_REDIRECT(status)) {
            /* Redirect status is permitted. */
            status = OK;
        }
        else if (ap_is_HTTP_CLIENT_ERROR(status)) {
            /* Client Error (eg. NOT FOUND) => as is status */
            return status;
        }

        if (status != OK && status != HTTP_OK) {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                          "Sub-request of proxy mapper failed: status = %d"
                          " on URI %s.",
                              status, r->uri);
            return HTTP_FORBIDDEN;      /* safety */
        }

        outhdr = apr_table_overlay(r->pool, subreq->err_headers_out,
                                            subreq->headers_out);

        do {
            value = apr_table_get(outhdr, "Location");
            if (value) {
                if (strbegin(value, "proxy:")) {
                    target = apr_pstrdup(r->pool, value);
                }
                else if (strbegin(value, "http://")
                 || strbegin(value, "https://")) {
                    target = apr_pstrcat(r->pool, "proxy:", value, NULL);
                }
                else {
                    /* UNSUPPORTED FEATURE */
                    ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                                  "Unsupported redirection to '%s'"
                                  " on URI %s.",
                                      value, r->uri);
                }
            }
        } while (0);
    }
    else {
        if (conf->type == MAPPER_FILE || conf->type == MAPPER_URI) {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                          "Lookup for proxy mapper sub-request failed"
                          " on URI %s.",
                              r->uri);
            return HTTP_FORBIDDEN;
        }
    }

    if (! target) {
        /* not for proxy remap */
        return DECLINED;
    }

    ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r,
                  "Proxy Mapper mapped URI %s to %s.",
                      r->uri, target);

    /* req->filename construction */
    r->filename = apr_pstrdup(r->pool, target);

    r->proxyreq = PROXYREQ_REVERSE;
    r->handler  = "proxy-server";

    return OK;
}

static int
hook_post_config(apr_pool_t *p, apr_pool_t *plog, apr_pool_t *ptemp,
                 server_rec *s)
{
    proxy_available = (ap_find_linked_module("mod_proxy.c") != NULL);

    if (! proxy_available) {
        ap_log_error(APLOG_MARK, APLOG_CRIT, APR_EGENERAL, s,
                     "mod_proxy_mapper: you must enable mod_proxy.");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    return OK;
}

static void
register_hooks(apr_pool_t *p)
{
    /* fixup after mod_proxy, so that the proxied url will not
     * escape accidentally by mod_proxy's fixup.
     */
    static const char * const asz_pre[] = { "mod_proxy.c", NULL };

    null_input_filter_handle
        = ap_register_input_filter("NULL_INPUT_FILTER", null_input_filter,
                                   NULL, AP_FTYPE_CONTENT_SET);
    null_output_filter_handle
        = ap_register_output_filter("NULL_OUTPUT_FILTER", null_output_filter,
                                    NULL, AP_FTYPE_CONTENT_SET);

    ap_hook_post_config(hook_post_config, NULL, NULL, APR_HOOK_LAST);
    ap_hook_fixups(hook_fixups, asz_pre, NULL, APR_HOOK_FIRST);
}

/* Dispatch list for API hooks */
module AP_MODULE_DECLARE_DATA proxy_mapper_module = {
    STANDARD20_MODULE_STUFF, 
    create_dir_config,      /* create per-dir    config structures */
    NULL,                   /* merge  per-dir    config structures */
    NULL,                   /* create per-server config structures */
    NULL,                   /* merge  per-server config structures */
    proxy_mapper_cmds,      /* table of config file commands       */
    register_hooks          /* register hooks                      */
};

いつか書く

2008-11-12

Apache の provider 機構 - 他モジュールに移譲するしくみ

2008-11-13 追記: タイトルを変更しました

用語定義をしておきます。

consumer
provider の提供する情報を取得する役割をになう
provider
consumer の要求する情報を提供する役割をになう

「情報」というのは const void * 型の値1つ,なのでなんでもよいです。文字列定数でもいいですし*1関数ポインタでもいいです。たいていは(関数ポインタを内包した)構造体を登録/取得します。

関数ポインタなり関数ポインタを内包した構造体なりを「情報」としてうけわたすことができると何がうれしいかというと,「移譲」ができることです。あるモジュールで大枠のロジックインプリメントし,他のモジュールで具象的な関数を登録してもらう,などすると,実装を切り替えることができます。

もちろん,同一のモジュールで consumer と provider を実装しても構いません。利用する関数の決定に一手間かかってしまいますが,のちのち他のモジュールで provider を置き換えることができるなど柔軟性がまします。

Apache で使われている providers

Apache 付属のモジュールの中で,以下のモジュールが provider 機構を利用しています。

Provider Groupconsumerprovider
authnmod_auth_basic, mod_auth_digest, mod_authn_aliasmod_authn_file, mod_authn_dbm, mod_authn_ldap, mod_authn_dbd, mod_authn_anon, mod_authn_alias
cachemod_cachemod_disk_cache, mod_mem_cache
proxylbmethodmod_proxy, mod_proxy_balancermod_proxy_balancer
davmod_davmod_dav_fs (, mod_dav_svn)
dav-lock(mod_dav_svn)mod_dav_lock

コードを読んでいないので憶測まじりになりますが。

authn provider は,ユーザ名とパスワードをもとに認証を行う provider です。consumer がクライアントとユーザ名を agent とやりとりし,認証自体を provider になげている恰好です。provider にはたとえば生 file や DBM から認証情報を読み取って認証するものがあります。

cache provider は,要求されたドキュメントをキャッシュから取り出す provider です。Disk や Memory から取り出す provider が用意されており,切り替えることができます。

proxylbmethod provider は proxy のロードバランスを行う provider です。独自の provider を実装することでロードバランシングのアルゴリズムなどを自作して適用することができます。詳しくは mod_proxy_balancerに独自振り分けロジックを追加できる気がする | 眠る開発屋blog に譲ります。

dav provider や dav-lock についてはコードを読むのがめんどうなので省略します。付属モジュールだけだとファイルシステム上のリソースを DAV で提供することしかできませんが,mod_dav_svn モジュールを使うとバックエンドとして Subversion レポジトリ上のリソースを DAV で提供できるようになります。この切り替えを provider-consumer アーキテクチャで実現しているのですね。

インタフェース定義

インタフェース定義は include/ap_provider.h にあります。また実装は server/provider.c にあります。たいして行数もないですし,ロジックも難しくないので読んでみるのもいいでしょう。

3つの関数のみ定義されています。

ap_register_provider()
provider の定義
ap_lookup_provider()
provider の取得
ap_list_provider_names()
provider の列挙

いちおう各関数の仕様をみていきます。

/**
 * This function is used to register a provider with the global
 * provider pool.
 * @param pool The pool to create any storage from
 * @param provider_group The group to store the provider in
 * @param provider_name The name for this provider
 * @param provider_version The version for this provider
 * @param provider Opaque structure for this provider
 * @return APR_SUCCESS if all went well
 */
AP_DECLARE(apr_status_t) ap_register_provider(apr_pool_t *pool,
                                              const char *provider_group,
                                              const char *provider_name,
                                              const char *provider_version,
                                              const void *provider);

group, name, version の3つのキーで一意となるように provider を登録します。provider を提供するモジュールで呼び出します。

基本的に groupインタフェースの「機能」を一意に定め,各モジュールがユニークな name で登録します。そして httpd.conf の設定子で name を指定する,という使われ方が多いようです。

たとえば,cache provider-consumer インタフェースだと,

  • mod_mem_cache が memfd という name の provider を登録
  • mod_disk_cache が disk という name の provider を登録

して,CacheEnable 設定子で provider を指定する形になっています。

version というとバージョンコントロール(上位バージョンは下位バージョンに対して上位互換性をもつなど)がありそうなイメージですが,とくにそのような機構は用意されていません。必要ならば consumer と provider でそのような機構をインタフェース仕様を規定・実装することになるでしょう。

version は 0 スタートの数値文字列"0" 等)を利用するのが慣例のようです。特に決まりはありませんが。

/**
 * This function is used to retrieve a provider from the global
 * provider pool.
 * @param provider_group The group to look for this provider in
 * @param provider_name The name for the provider
 * @param provider_version The version for the provider
 * @return provider pointer to provider if found, NULL otherwise
 */
AP_DECLARE(void *) ap_lookup_provider(const char *provider_group,
                                      const char *provider_name,
                                      const char *provider_version);

group, name, version の3つのキーで一意となる provider を取得します。consumer モジュールで呼び出します。

/**
 * This function is used to retrieve a list (array) of provider
 * names from the specified group with the specified version.
 * @param pool The pool to create any storage from
 * @param provider_group The group to look for this provider in
 * @param provider_version The version for the provider
 * @return pointer to array of ap_list_provider_names_t of provider names (could
 be empty)
 */

AP_DECLARE(apr_array_header_t *) ap_list_provider_names(apr_pool_t *pool,
                                              const char *provider_group,
                                              const char *provider_version);

指定した group, version にマッチする provider の name を列挙します。

Apache 付属モジュールでまともに使われている例はありませんが,Hooks 機構のように group に登録された provider をすべて呼び出すようなメカニズムを利用してみてもおもしろいかもしれませんね。

サンプルモジュール

とまあ座学だけでは退屈なので,実際に provider 機構を利用したモジュールを作ってみましょう。

  • consumer は2つの数値を「演算」する大枠のアルゴリズムを提供する
  • provider として具体的な「演算」(add, subtract など)を実装する

という役割分担です。

consumer の実装

まずは consumer(mod_consumer)の実装から。

/* 
**  mod_consumer.c
*/ 

#include "httpd.h"
#include "http_config.h"
#include "http_protocol.h"
#include "ap_config.h"
#include "ap_provider.h"
#include "apr_strings.h"

/* Provider Definitions */

typedef int (*arithmetic_method)(int arg1, int arg2);

/* Configuration */

typedef struct consumer_conf {
    const char *method_name;
} consumer_conf;

static void *
create_dir_config(apr_pool_t *p, char *dir)
{
    consumer_conf *conf
        = (consumer_conf *) apr_pcalloc(p, sizeof(consumer_conf));
    return conf;
}

static const char *
set_arithmetic_method(cmd_parms *cmd, void *mconfig, const char *arg)
{
    consumer_conf *conf = (consumer_conf *) mconfig;

    conf->method_name = apr_pstrdup(cmd->pool, arg);

    return NULL;    /* NO ERROR */
}

static const command_rec consumer_commands[] = {
    AP_INIT_TAKE1("ArithmeticMethod", set_arithmetic_method, NULL,
                  ACCESS_CONF | RSRC_CONF,
                  "Set method name for calculation"),
    { NULL }
};

/* Content Handler */

module AP_MODULE_DECLARE_DATA consumer_module;  /* declaration */

static int
consumer_handler(request_rec *r)
{
    consumer_conf       *conf;
    arithmetic_method   provider;
    int                 result;

    if (strcmp(r->handler, "consumer"))
        return DECLINED;

    conf = (consumer_conf *) ap_get_module_config(r->per_dir_config,
                                                  &consumer_module);

    if (! conf || ! conf->method_name)
        return HTTP_INTERNAL_SERVER_ERROR;

    provider = ap_lookup_provider("arithmetic", conf->method_name, "0");

    if (! provider)
        return HTTP_INTERNAL_SERVER_ERROR;

    result = (provider)(6, 2);

    r->content_type = "text/plain";      
    ap_rprintf(r, "%s(6, 2) = %d\n", conf->method_name, result);

    return OK;
}

/* Hook Registration and Module Settings */

static void
register_hooks(apr_pool_t *p)
{
    ap_hook_handler(consumer_handler, NULL, NULL, APR_HOOK_MIDDLE);
}

module AP_MODULE_DECLARE_DATA consumer_module = {
    STANDARD20_MODULE_STUFF, 
    create_dir_config,      /* create per-dir    config structures */
    NULL,                   /* merge  per-dir    config structures */
    NULL,                   /* create per-server config structures */
    NULL,                   /* merge  per-server config structures */
    consumer_commands,      /* table of config file commands       */
    register_hooks          /* register hooks                      */
};

最初にも書いたように通常 provider data としては構造体へのポインタをうけわたすことが多いのですが,今回は演算を行う関数ポインタ自身を provider data としています。

    provider = ap_lookup_provider("arithmetic", conf->method_name, "0");

のように,ArithmeticMethod 設定子で設定された name の provider を取得しています。group は arithmetic,version は 0 です。

これで provider関数ポインタ自身ですから,

    result = (provider)(6, 2);

のようにして演算を provider におこなってもらいます。演算の引数は 6 と 2 の決め打ちw

provider の実装

まずは加算をおこなう provider,mod_arith_add の実装です。

/* 
**  mod_arith_add.c
*/ 

#include "httpd.h"
#include "http_config.h"
#include "ap_provider.h"

/* Provider Definitions */

typedef int (*arithmetic_method)(int arg1, int arg2);

/* Arithmetic Operation */

static int
arith_add(int arg1, int arg2)
{
    return arg1 + arg2;
}

/* Hook Registration and Module Settings */

static void
register_hooks(apr_pool_t *p)
{
    ap_register_provider(p, "arithmetic", "add", "0", &arith_add);
}

module AP_MODULE_DECLARE_DATA arith_add_module = {
    STANDARD20_MODULE_STUFF, 
    NULL,                   /* create per-dir    config structures */
    NULL,                   /* merge  per-dir    config structures */
    NULL,                   /* create per-server config structures */
    NULL,                   /* merge  per-server config structures */
    NULL,                   /* table of config file commands       */
    register_hooks          /* register hooks                      */
};

先ほどの consumer の実装では,config を処理する必要があったのでどうしてもあるていどのコード量になってしまいましたが,こちらはとても短くおさめることができました。

add という name で arith_add() という関数ポインタを provider data として登録しています。

register_hooks で provider を登録するのが慣例のようです。登録された provider を管理するためにメモリプールが必要となります。このメモリプールは system wide に共通のものを利用しなくてはなりません*2ap_register_provider() ではこれを第一引数で与えているのですが,register_hooks が呼び出されたときの pool は server/main.c::main() で用意された global pool なのでたしかに適しているのでしょう。まぁ config stage でも global pool がわたされているのでそこで登録しても構いませんが。


さて同じように他の演算を行う provider を実装していきましょう。といっても実質下記の部分だけ書き換えれば OK です。

/* Arithmetic Operation */

static int
arith_sub(int arg1, int arg2)
{
    return arg1 - arg2;
}

/* Hook Registration and Module Settings */

static void
register_hooks(apr_pool_t *p)
{
    ap_register_provider(p, "arithmetic", "sub", "0", &arith_sub);
}

実行する

モジュールをビルドしてインストールしたうえで,下記のような設定*3でウェブサーバと立ち上げます。

$ cat /etc/httpd/conf/httpd.conf

...... snip snip snip ......

LoadModule consumer_module  modules/mod_consumer.so
LoadModule arith_add_module modules/mod_arith_add.so
LoadModule arith_sub_module modules/mod_arith_sub.so
LoadModule arith_mul_module modules/mod_arith_mul.so
LoadModule arith_div_module modules/mod_arith_div.so

<Location /arith>
    SetHandler consumer
    ArithmeticMethod add
</Location>

...... snip snip snip ......

これでリクエストを発行してみると……

$ wget -nv -O - http://localhost/arith 
add(6, 2) = 8
11:25:28 URL:http://localhost/arith [14/14] -> "-" [1]

おお,きちんと 6 + 2 を計算することができました。

このままだとつまらないので,httpd.conf を書き換えて,subtract 演算をおこなうようにしてみます。

$ sudo vi /etc/httpd/conf/httpd.conf

...... snip snip snip ......

<Location /arith>
    SetHandler consumer
    ArithmeticMethod sub
</Location>

...... snip snip snip ......

$ sudo /sbin/service httpd restart
Stopping httpd:                                            [  OK  ]
Starting httpd:                                            [  OK  ]

これで実行すると,

$ wget -nv -O - http://localhost/arith
sub(6, 2) = 4
11:26:07 URL:http://localhost/arith [14/14] -> "-" [1]

こんどは 6 - 2 を計算することができました。

consumer のロジックを修正することなく provider を選択することで任意の演算をおこなうことができるようになりました。

自力で APR の DSO ハンドリング関数を使ってもできるんじゃ?

できますけど,こちらのほうがより抽象化されてます(から利用が楽です)し,DSO を使っていない static linked された httpd でも利用可能なところが違うと思うです。

*1:極端な話キャストした数値定数でもいいはず。

*2:さもないと運が悪いと ap_lookup_provider() 時にセグフォってしまうはずです。

*3:無駄に演算 provider のモジュールをわけましたが,今考えたら一つのモジュールにまとめてもよかったかもですね。

2008-09-19

libxml2 の XmlTextReader インタフェースで XML をパースする

libxml2 のドキュメントを眺めていたら,DOM インタフェースや SAX インタフェースだけではなく,XmlTextReader インタフェースというのもありました。

これはドキュメントをパースしながら(ストリーミング)処理をしていくという点で SAX インタフェースに似ているのですが,SAX は callback インタフェースであるのに対して,こちらは能動的に情報を pull するという点でプログラミングがしやすいです。またストリーミング処理なので,(逐一ノード情報を preserve するのでなければ)DOM インタフェースと比べてメモリ消費の点でも優しいです。

Perl の XML::LibXML で使ってみました。C での使い方や概念については Libxml2 XmlTextReader Interface tutorial に載っています。

基本的な使い方

use strict;
use warnings;

use XML::LibXML::Reader qw( :types );

my $reader
    = XML::LibXML::Reader->new( location => 'test.xml' );

while ($reader->read()) {
    printf "%d %s%s\n",
        $reader->depth,
        $reader->nodeType == XML_READER_TYPE_END_ELEMENT ? q{/} : q{},
        $reader->name;
}

手順としては,

  1. new() でインスタンスを作成する
  2. read() するたびに処理中のノードが進んでいく(カーソル的)
    • 現在のノード情報をインスタンスメソッドで取得

になります。

実行すると,

0 html
1 #text
1 head
2 #text
2 title
3 #text
2 /title
2 #text
1 /head
1 #text
1 body
2 #text
2 div
3 #text
3 span
4 #text
3 /span
3 #text
2 /div
2 #text
2 div
3 #text
3 span
4 #text
3 /span
3 #text
2 /div
2 #text
1 /body
1 #text
0 /html

現在処理中のノードについての情報を,現在の depth も含めて取得できるので便利ですね。

preserveNode() を使ってフィルタリング

XmlTextReader インタフェースのもうひとつのポイントは,処理中のノードを preserve しておくと,最後に preserve したノード群を取得できるというところです。

たとえば,

<html>
    <head>
        <title>XmlTextReader test</title>
    </head>
    <body>
        <div class="skipme">
            <span>Konnichiha, Sekai!</span>
        </div>
        <div class="keepme">
            <span>Hello, world!</span>
        </div>
    </body>
</html>

みたいな HTML があったときに,skipme というクラスが指定されたノードを取り除きたいとします。

use strict;
use warnings;

use XML::LibXML::Reader qw( :types );

my $reader
    = XML::LibXML::Reader->new( location => 'test.xml' );

while ($reader->read()) {
    # 変数名が直感的でないので,エイリアスしてる
    my $node = $reader;

    # 開きエレメント,だったら
    if ($node->nodeType == XML_READER_TYPE_ELEMENT) {
        my $class = $node->getAttribute('class');
        if (defined $class && $class eq 'skipme') {
            # 下位ノードをスキップ
            $node->next();
            next;
        }
    }

    # 開きエレメントで preserveNode() すると
    # 下位ノードもまるごと preserve されるのでよろしくない
    # だから skip
    if ($node->nodeType == XML_READER_TYPE_ELEMENT) {
        next;
    }

    $node->preserveNode();
}
$reader->finish();

# preserve されたドキュメントを document で取り出せる
print $reader->document->toString(1);

このように,手順としては

  1. 保存しておきたいノードについて preserveNode() する
  2. パース終了時に finish() する
  3. document() を呼ぶと,これまで preserve されたノードだけ取得できる

です。

ちょっと注意が必要なのは,開きエレメントで preserveNode() してしまうと,仮に下位ノードを preserve しなくても,ノードツリー全体が preserve されてしまうところです。今回の例だと,<body><html>preserveNode() してしまうと,下位の <div> のところで preserve しなかったとしても,それが最終的なドキュメントに残ってしまいます。なので上記コードではエレメントの開きの部分では preserveNode() していません(閉じエレメントのときに preserve すればそれで OK)。

実行結果は,

<?xml version="1.0"?>
<html>
    <head>
        <title>XmlTextReader test</title>
    </head>
    <body>
        <div class="keepme">
            <span>Hello, world!</span>
        </div>
    </body>
</html>

このように,class="skipme" のところだけ filter out されました。


しかしながら preserveNode() を使っても,条件に合致するノードの削除(反対にいうと条件に合致するノードのみの抽出)しかできません。いまいち使いどころがないような。

DOM と XmlTextReader インタフェースを絡めて aggregate

copyCurrentNode() という API を使うと,現在処理中のノードを DOM ノードとして取得することができます。なので,これと DOM API を絡めると,条件にマッチしたノードを集めて好きなように加工・保存することができます。

たとえば,

<?xml version="1.0"?>
<html>
    <head>
        <title>Opinions</title>
    </head>
    <body>
        <p class="tatemae">建前 1</p>
        <p class="honne">本音 1</p>
        <hr />
        <p class="tatemae">建前 2</p>
        <p class="honne">本音 2</p>
        <hr />
        <p class="tatemae">建前 3</p>
    </body>
</html>

のような HTML から class="tatemae" なところだけ抜き出して XML を生成したいとします。

use strict;
use warnings;

use XML::LibXML;
use XML::LibXML::Reader qw( :types );

# DOM 操作をして XML ドキュメントを生成(ありがちな操作なので解説しません)
my $doc = XML::LibXML::Document->createDocument(1, 'utf-8');
$doc->setStandalone(1);

my $root = $doc->createElement('opinions');
$doc->setDocumentElement($root);

my $reader
    = XML::LibXML::Reader->new( location => 'sample.xml' );

while ($reader->read()) {
    my $node = $reader;

    if ($node->nodeType == XML_READER_TYPE_ELEMENT) {
        my $class = $node->getAttribute('class');
        if (defined $class && $class eq 'tatemae') {
            # 現在のノードを XML::LibXML::Node として取り出す
            # 下位ノードも含め deep copy
            my $hn = $node->copyCurrentNode(1);

            # XML ドキュメントに appendChild() する
            my $item = $doc->createElement('item');
            $item->appendChild($hn->firstChild);

            $root->appendChild($item);
        }
    }

}
$reader->finish();

print $doc->toString(1);

なんだか JavaScript で DOM を操作しているみたいですね。

実行すると,

<?xml version="1" encoding="utf-8" standalone="yes"?>
<opinions>
  <item>建前 1</item>
  <item>建前 2</item>
  <item>建前 3</item>
</opinions>

のようになります。

今回は DOM ドキュメントを生成して appendChild() していきましたが,必ずしもそうする必要はありません。copyCurrentNode() でノードを取得して attribute 等を操作し自力でシリアライズするなどの利用方法も考えられます。

おわりに(自分用メモ)

SAX で触るより使いやすい高位 API なのでこれ使おうと思ったんですが,SAX より高位だと named entity のハンドリングのやり方がまだわからないしなぁ*1

*1:Perl からだと SAX レイヤでもうまくできてないです……