2012/02/22

C 中級者が意外と陥りやすいワナ

ある程度プログラミングにも慣れてきて 言語仕様もそこそこ理解した中級者にとって 意外と陥りやすくセキュリティホールの温床になりがちなワナ。
勿論オンラインマニュアルにも仕様として明記してあるのだが、 直感とは反する仕様のなので特に中級者にこれらのミスが多い気がする。

  1. strncpy(3)``\0'' 終端してくれない場合がある
  2. strcpy(3) はバッファオーバーフローの危険性があるから strncpy(3) を利用する様によく言われるが、 ここにワナが潜んでいる。

    strncpy(dst, src, len);

    とした時に 文字列 src の長さが len バイト未満の場合は dst``\0'' で終端される。
    終端どころか dst の残り領域は何故か 全て ``\0'' が詰められるという 無駄とも思われる謎仕様。
    ところが文字列 src の長さが len バイト以上の場合、 dstlen バイトだけコピーするという memmove(3) 同様の動作となってしまい、 dst``\0'' で終端されない。
    文字列操作の関数なのにも関わらず得られた結果が 文字列として扱えないという不思議な仕様なのだ。
    snprintf(3) などの様に len - 1 バイトを dst にコピーして ``\0'' で終端される事を期待していると 痛い目に遭ってしまう。

    文字列操作関数なので、 殆んどの場合 ``\0'' 終端が必要になると思うので 以下の様な処理が代替になる。

     strncpy(dst, src, sizeof(dst) - 1);
     *(dst + sizeof(dst) - 1) = '\0';
     			

  3. strncat(3) のサイズ指定はコピー先のサイズを指定するのではない
  4. strcat(3) も同様にバッファオーバーフローの危険性があるので strncat(3) を利用する様によく言われるが、 ここにもワナが潜んでいる。

    strncat(dst, src, len);

    とした時の len はコピーする src の長さであって、 前述の strncpy(3)snprintf(3)の様に dst のサイズ指定ではない ここで dst のサイズのつもりで len を指定すると バッファオーバーフローの原因になってしまう。

    殆んどの場合は dst の最大値が重要になると思うので 以下の処理が代替になる。

    strncat(dst, src, sizeof(dst) - strlen(src) - 1);
    			

  5. snprintf(3) は領域が重なってはいけない
  6. 前述した通りに使いやすいとは言いづらい strncpy(3)strncat(3) の代用として、 コピー先のサイズ指定が直感的な snprintf(3) を利用する場合も多いと思うが、 strncat(dst, src, len) の置き換えとして

    snprintf(dst, sizeof(dst), "%s%s", dst, src);

    とすると正しく動作しない(結果は処理系依存になる)。
    ANSI X3.159-1989 (ANSI C) や ISO/IEC 9899:1999 (ISO C99) 、 IEEE Std 1003.1-1988 (POSIX.1) 等をざっと探したけど 関連する記述は見つける事はできなかったが、 snprintf(3) は重なった領域では正しく動作しない様だ。

以上、C によるプログラミングに慣れた頃に陥りがちなワナなので 注意が必要だと思う。

2011/12/14

cvs や svn のリビジョン番号

プログラムを作成する際、 特にまだ未完成で頻繁に修正とコンパイルを繰り返している間は、 どのリビジョンのプログラムを実行しているのか知りたい事はよく有る。
そんな時に、例えば実行時オプションとして -v を指定すると cvs や svn のリビジョン番号とコミットした日時が表示されると便利だ。
しかし、多くの場合において -v オプションを解析し実行するだろう main 関数が含まれるファイルが最終修正されているとは限らない。 従って main 関数の含まれるファイル中に

static	char	*id = "$Id$";
	
などと定義しておいて commit 時に $Id$ を展開したとしても、 必ずしも最新のリビジョンにはならない場合が多い。

そこで -v オプションのハンドラ部分では

static	char	*id = "revision: " REVISION ", " COMMITED ;
	
と記述としておいて make 時に自動で REVISIONCOMMITED を全てのソース/ヘッダファイル中の cvs や svn の $Id$ キーワードから取得して設定する方法を検討してみる。

build 時に必ず実行する必要がある処理なので Makefile に記述して、 make 中で自動的に実行してしまうのが楽。

    :
# 変数定義
PROGRAM  = myprog
OBJECTS  = sub.o pthread.o common.o util.o
HEADER   = myprog.h const.h config.h
 
# 全ソース/ヘッダ中の `$Id' 行から最大のリビジョンを取得する
REVISION = $(shell awk '/\$$Id/{ if($$4 ~ /[[:digit:]]/) \
            if(max < $$4) max = $$4 } END{ print max }' \
            $(OBJECT:.o=.c) $(HEADER) $(PROGRAM).c)
# 全ソース/ヘッダ中の `$Id' 行から最大のリビジョンのコミット日時を取得する
COMMITED = $(shell awk '/\$$Id.*'$(REVISION)'/{ \
            print $$5 " " $$6; exit }' \
            $(OBJECT:.o=.c) $(HEADER) $(PROGRAM).c)
# コンパイルオプション(REVISIONとCOMMITEDを定義)
CFLAGS   = -g -Wall -Wextra -Werror -Wno-unused-parameter -I.-O2 \
            -DREVISION=\""$(REVISION)"\" -DCOMMITED=\""$(COMMITED)"\"
    :
    :
# ターゲット(デフォルトターゲットは touch と $(PROGRAM)に依存
all: touch $(PROGRAM)

# $(PROGRAM).o ファイルの削除($(PROGRAM).o ファイルは必ずコンパイルする)
touch:
    rm $(@).o

$(PROGRAM): $(OBJECTS)
    $(CC) $(CFLAGS) -o $(@) $^ 

$(OBJECTS): $(HEADER)
    :
	
これでどのファイルを commit した場合でも 必ず最新のリビジョンとコミット日付が -v で表示される。

当初ターゲット touchtouch $(PROGRAM).c としていたが、 とある方よりの指摘で rm $(PROGRAM).o に変更した。
ご指摘感謝です > N.S. さま

2011/11/26

shell での while ループにおける問題

シェルスクリプトでコマンドの出力をループ処理する場合は パイプ | を使用するのが一般的な手法だと思うが、 パイプ | を使うとループが別なプロセスとして実行されるので ループ内部で設定したシェル変数がループ外部で参照できないという問題があり、 (存在するとすれば) shell script 業界では割と FAQ 的な問題だ。
何が問題かと言って posix で定義されていないから シェルの実装に依存しているのが一番の問題だったりするのだろう。
この問題に対しては、シェルビルトインの exec (1) を利用して ディスクリプタを複製して while ループの入力を標準入力にしてしまうのが 一番汎用的で柔軟性のある対応だと思うのだが、 シェルを愛する面々は 皆同じ様な苦労をしているのだ と改めて認識した。

当初は一時ファイルを作成する方法を考えついたのだが、 自分的にはなるべく一時ファイルを使いたくなかったので ファイルから入力する部分をバッククォートによるコマンド実行に置き換え等、 色々と試行錯誤した結果ヒアドキュメントを使う方法に落ち着いた。

# これでは正常に動作しなかった
exec 3<&0  0<`${command}`

while read line
    :
	

勿論一時ファイルを作成しても良いのだが、 ヒアドキュメントを利用する事で一時ファイルが不要になる分だけ 処理的に美しいかなと思う (完全に好みの問題)。
  1#!/bin/sh
  2# コマンドの出力をバッククォートしてヒアドキュメントとして使用する
  3
  4command="/path/to/command"
  5count=0
  6
  7# exec 3<&0: 標準入力 (FD0) を FD3 に複製
  8# exec 0<<EOF: ヒアドキュメントを標準入力として使用
  9exec 3<&0  0<<EOF
 10`${command}`
 11EOF
 12
 13while read line
 14do
 15    :
 16    count=`expr ${count} + 1`
 17done
 18
 19# exec 0<&3: FD3 に複製した標準入力 (FD0) を復帰
 20# exec 3<&-: FD3 をクローズ</i>
 21exec 0<&3 3<-
 22
 23echo ${count}
    

もう一点は子プロセスで親プロセスの変数値を変更する方法。
こちらはプロセスが別になってしまうとファイルを使うしかないので、 同じプロセス内で関数呼び出しとして対応する方法が汎用的ではないだろうか。

  1#!/bin/sh
  2# eval を利用して呼び元の変数を設定する
  3
  4default="デフォルト値"
  5
  6sub()
  7{
  8
  9    local   _val
 10
 11    :
 12    _val="設定したい値"
 13
 14    eval "${1}='${_val:-${default}}'"
 15
 16}
 17
 18value="元の値"
 19
 20sub value
 21
 22echo ${value}
    

ちなみに上のリンク元にある parent.sh の中の処理で ${tmp_file} の内容を for ループで eval しているが、 これはシェルビルトインの . もしくは source を利用して . ${tmp_file} としても同一の結果が得られるので楽だと思う。

  1#!/bin/sh
  2
  3g1="aa"
  4g2="bb"
  5
  6tmp_file=`mktemp /tmp/ps.XXXXXX`
  7export tmp_file
  8
  9./child.sh    # child.sh で、上で設定した変数 g1, g2 の値を変更する
 10
 11# for v in `cat $tmp_file`; do
 12#   eval $v
 13#   done
 14# ${tmp_file} の内容は <tt>name='value'</tt> 形式が保証されているので
 15# ファイル自体を `.' コマンドで読み込むだけで
 16# 行毎に eval を実行しなくてもシェル変数に代入された値が利用できる
 17
 18if [ -f ${tmp_file} ]
 19then
 20    . ${fmp_file}
 21    rm -f ${tmp_file}
 22fi
 23
 24echo $g1 #=> cc に変更された
 25echo $g2 #=> dd に変更された
    
ブログに直接コメントしたかったのだが、 はてなのアカウントが無いので申し訳ないがここで意見させて頂く。

2011/11/16

メモリリークチェッカー

c で daemon 動作するサーバを作っていると malloc (3) などで動的に確保したメモリの free (3) 忘れによる いわゆるメモリリークを防ぐ事が非常に重要になってくる。
この様なメモリリークを防止するために役立つ 様々なツールが世の中には沢山存在しており それぞれ非常に有用ではあるのだが、 いざ利用しようと思うと割と面倒な作業が発生したり、 自分の求めている機能に対して明らかにオーバースペックだったりするので、 自分の用途に合わせて簡単に使える『俺様ツール』を作ってみた。

動作原理は簡単で malloc (3) や free (3) したアドレスを ログに出力して、 後からログを解析してメモリの確保と解放が対になっていない部分を 抽出するだけである。
そのためにまずは元となるソースコードを修正して、 malloc (3) や free (3) を独自に作成した wrapper 関数に置換えて 確保、解放したアドレスをログに出力する機能を組込む必要があるのだが、 malloc (3) などはエラー処理を含めて共通化するために あらかじめ wrapper 関数化されている場合が多いと思うので、 この部分はフォーマットを決めるだけで割りと簡単に追加できると思う。
今回は syslog (3) を利用して malloc (3)、strdup (3)、 free (3) で確保/解放するアドレスを出力した。

  1/*
  2 * malloc() の wrapper
  3 */
  4void    *mymalloc(size_t size)
  5{
  6
  7    void    *p;
  8
  9    if(!(p = malloc(size)))
 10        /* エラー処理 */;
 11    syslog(LOG_DEBUG, "malloc: %p\n", p);
 12
 13    return(p);
 14
 15}
 16
 17/*
 18 * strdup() の wrapper
 19 */
 20char    *mystrdup(const char *s)
 21{
 22
 23    char    *p = NULL;
 24
 25    if(s){
 26        if((p = strdup(s)))
 27            syslog(LOG_DEBUG, "strdup: %p\n", p);
 28        else
 29            /* エラー処理 */;
 30    }
 31
 32    return(p);
 33
 34}
 35
 36/*
 37 * free() の wrapper
 38 */
 39void    myfree(void *p)
 40{
 41
 42    if(p){
 43        free(p);
 44        syslog(LOG_DEBUG, "free: %p\n", p);
 45    }
 46
 47}
    

次に出力されたログの解析処理であるが、 確保したメモリのアドレスをキーとしたハッシュテーブルを利用したいので 入力行の解析やハッシュテーブルが簡単に利用できる言語として 今回はコマンドライン版の php (1) を利用してみた。
もちろん awk (1) を駆使したり perl (1) を利用しても問題ない。

  1<?php
  2    /*
  3     * Copyright (c) 2011 Mitzyuki IMAIZUMI, All rights reserved.
  4     *
  5     * $Id: memcheck.php 1677 2011-10-12 06:58:03Z mitz $
  6     */
  7
  8    /* 冗長指定 */
  9    if($argv[1] == "-v"){
 10        $verbose = 1;
 11        array_shift($argv);
 12    }
 13
 14    if($fp = fopen($argv[1], "r")){
 15        $line = 0;
 16        while($buf = fgets($fp, 1024)){
 17            $buf = ltrim($buf);
 18            $line++;
 19            /*
 20             * malloc か strdup か free を含む行の場合
 21             * コマンドを配列 $p[0] に、アドレスを配列 $p[1] に格納する
 22             */
 23            if(preg_match("/^(malloc|strdup|free):\s(.*)/", $buf, $p))
 24                /* 解放処理 */
 25                if($p[1] == "free")
 26                    /* 確保済みテーブルに解放アドレスが存在する場合 */
 27                    if($alloc[$p[2]]){
 28                        if($verbose)
 29                            printf("%s: % 8d -> % 8d\n", $p[2], $alloc[$p[2]], $line);
 30                        $alloc[$p[2]] = 0;
 31                    }
 32
 33                    /* 確保済みテーブルに解放アドレスが存在しない場合 */
 34                    else
 35                        printf("%s: unknown free at % 8d\n", $p[2], $line);
 36                else
 37                /* アドレスをキーとしたハッシュテーブルに行番号を格納 */
 38                    $alloc[$p[2]] = $line;
 39
 40        }
 41        fclose($fp);
 42
 43        foreach($alloc as $k => $v)
 44            if($v)
 45                printf("%s: NOT free (% 8d)\n", $k, $v);
 46    }
 47?>
    
このスクリプトを実行する事により malloc(3) もしくは strdup(3) で確保したメモリが解放されていない場合、 もしくは確保していないメモリを解放した場合が簡単に発見可能である。

これらは自分が必要とする機能のみを簡単に実装したものであり、 例えば解放していない事を意図しているメモリまで警告されてしまう等、 汎用的に使える事を目指したツールでは勿論ない。 ただ、日頃発生しうる面倒な作業がちょっとの工夫で 多少なりとも楽になるだろう例の一つとして公開してみた次第である。

2011/11/28 追記

awk (1) 版の方が汎用的なので簡単に作成してみた。 処理内容は上記 php (1) 版と同じである。
  1#!/bin/sh
  2
  3if [ "${1}" = "-v" ]
  4then
  5    verbose=true
  6    shift
  7fi
  8
  9awk '
 10    /^(malloc|strdup)/{
 11        alloc[$4] = NR;
 12    }
 13    /^free/{
 14        if(alloc[$4]){
 15            if("'${verbose:-false}'" == "true")
 16                printf("% 5d %s: % 5d\n", NR, $4, $alloc[$4]);
 17            alloc[$4] = 0;
 18        }
 19        else
 20            printf("% 5d %s: unknown free.\n", NR, $4);
 21    }
 22    END{
 23        for(i in alloc)
 24            if(alloc[i])
 25                printf("% 5d %s: NOT free.\n", alloc[i], i);
 26    }
 27' ${1} | sort ${2}
    

2011/10/18

電源関係の小物たち

いつも鞄に入れて持ち歩いている電源達関係の小物

Image: RIMG0456.JPG

電源達

  1. Apple 純正の AC アダプタ
  2. 普段から MacBookAir を持ち歩いているので、 Apple 純正の AC アダプタは必須 (IBM のバンドでまとめてあるのは愛嬌です)。

  3. USB 電源アダプタ
  4. 携帯電話や BlueTooth 機器など USB から充電できる機器が増えているので、 100V コンセントから USB の 5V を出力するアダプタも何かと便利。

  5. HyperMac
  6. 特許の関係で入手が不可になった HyperMac とそのケーブル。 これは MacBookAir からは通常の電源に見えるので、 純正のエアラインアダプタとは異なり充電もできる優れもの。 このおかげでお世辞にも長いとは言えない MacBookAir の駆動時間が 飛躍的に向上出来るのでモバイルには必須とも言えるアイテム。
    Apple も特許が大切なのは判るんだけど、 こういう素晴らしい商品はライセンスするとか対応を考えて欲しいと思う。

  7. Griffin の USB Mini-Cableset。
  8. Image: RIMG0365.JPG

    Griffin の USB Mini-Cableset
    一般的な USB の A 端子コネクタを Mini-USB、Micro-USB、 そして iPod 用の端子に変換するためのケーブルセットなのだが、 短めのケーブルなので収納に困らず邪魔にもならない。 普段から一緒に持ち歩いている iPod や Jawbone の BlueTooth ヘッドセットなど、 殆どの携帯機器の接続や充電が可能になるので非常に便利。

2011/07/07

パイプコマンド中の終了ステータス取得

シェルスクリプトでは頻繁に複数のコマンドをパイプで連結して使用するが、 パイプの途中のコマンドの終了ステータスは通常では参照できない。
そこでリダイレクトを利用してパイプの途中のコマンドの 終了ステータスを取得する方法を考えてみる。

  1exec 3>&1
  2ret1=`{ { command1; echo $? 1>&3; } | command2; } 3>&1`
  3ret2=$?
    
exec を利用してあらかじめ FD3 を利用可能にしておき、 command1 の終了ステータスを FD3 に出力している。
command2 の実行後に FD3 の出力を FD1(標準出力) に変更しているので、 ret1 は標準出力から command1 の終了ステータスが取得できる。 command2 の終了ステータスはそのまま $? を参照して ret2 に格納される。

2011/07/04

awk (1) 小技

先日参加した勉強会で awk (1) 関連の発表があったので、 ちょっとした小技を紹介してみる。

system() を利用する際の注意
awk の中で外部コマンドを実行する場合は 組み込み関数 system() が利用できるが、 この関数を利用して外部コマンドを実行すると 入出力 stream がオープンされるので、 ファイルディスクリプタが消費されてしまう。
従って awk 内で何度も system() を実行すると、 オープンできるディスクリプタの上限を超過してしまい、 awk の実行が中断されてしまう可能性がある。
その場合 close() を利用して system() がオープンした stream を閉じれば良いのだが、 awksystem() では stream を明示的に扱わない。 そこで system() で実行したコマンドを close() の引数で指定する事で stream を閉じる事ができる。
  1awk '{
  2    :
  3    # コマンドを生成
  4    command = sprintf("%s %s %s", command, arg1, arg2);
  5    # コマンドを実行して結果を取得
  6    buf = system(command);
  7    # stream をclose
  8    close(command);
  9    :
 10}'
            
関数内ローカルな変数の使用
nawkfunction を利用する事で 内部関数を作成する事ができるが、 関数内でローカルな変数を明示的に定義する事ができない。 そこで関数宣言の仮引数部に変数を定義する事で、 関数内ローカルの変数として利用できる。
  1awk '{
  2        :
  3    # 関数 foo の定義
  4    #   num: ループ数
  5    # 以下ローカル変数
  6    #   i: ループウンタ
  7    function    foo(num,        i)
  8    {
  9        # i はローカル変数扱いなので呼出元の i は影響を受けない
 10        for(i=0; i<1num; i++)
 11            :
 12        }
 13    :
 14    foo(num);
 15        :
 16    }'
            
但し関数呼び出し時と関数定義時に引数の数が等しくないので、 後に混乱しないためにコメントで明記するなどの処置は必要。

2011/07/01

Keyboards

現在職場で愛用中のキーボード達。
改めて見ると個人所有のキーボードが沢山あって驚いた。

PFU Happy Hacking Keyboard PD-KB02

Image: RIMG0044.JPG

PFU Happy Hacking Keyboard PD-KB02

Happy Hacking Keyboard としては2代目の製品かな? いちばん古くから愛用しているキーボード。
メンプレンスイッチのキータッチと シリンドリカル・カーブドスカルプチャの構成は 今でもいちばん気に入っていて、 FreeBSD 端末に接続されている。 プログラムを書くときは、このキーボードと FreeBSD と vi が必須。

PFU Happy Hacking Keyboard Professional 2 PD-KB400WN

Image: RIMG0045.JPG

PFU Happy Hacking Keyboard Professional 2 PD-KB400WN

USB 接続タイプの新世代 Happy Hacking Keyboard。
静電容量無接点方式で押下圧が 45g なので、 キータッチは PD-KB02 に比べると若干軽く感じるが、 打鍵音が若干「カチャカチャ」気味なのが気になる。 シリンドリカル・ステップスカルプチャなので、 手を自然に置いた時の感じが PD-KB02 とは若干異なり 微妙な違和感を感じる。

Apple Wireless Keyboard MB167LL/A

Image: RIMG0043.JPG

Apple Wireless Keyboard MB167LL/A

薄型 Bluetooth 接続の Apple Wireless Keyboard で、 AA セルを 3 本利用する初代タイプ。
酸化皮膜処理のアルミニウムボディは非常に美しいのだが、 パンタグラフタイプなのでキータッチが軽すぎ、 しかもストロークが短いので打鍵感が頼りなく感じてしまい、 キー入力時に底打ちしてしまうのが微妙に使いづらい感じ。 以前は 27インチの iMac のメインキーボードとして活躍してた。 現在は自宅の TV に接続されている Mac MINI のキーボード。

Lenovo ThinkPad USB TrackPoint Keyboard 55Y9003

Image: RIMG0046.JPG

Lenovo ThinkPad USB TrackPoint Keyboard 55Y9003

かつての IBM Space Saver Keyboard の流れをくむ トラックポイントが搭載された薄型のキーボード。
こちらもパンタグラフタイプなのでキータッチが軽く、 ストロークも短めなので本家 IBM Space Saver Keyboard ほどは打鍵感が良くない。

Apple USB Keyboard

Image: RIMG0047.JPG

Apple USB Keyboard

USB 接続の Apple 社純正フルキーボード。
メンプレンスイッチの打鍵感は素晴らしく、 シリンドリカル・カーブドスカルプチャなので違和感を感じない。
テンキーが付いているフルキーボードは邪魔なのだが、 Macintosh 独特の特殊キー(コマンドキー)をクリックしながら 電源をオンにするといった特殊な用途のために USB 接続の純正キーボードが必要な場合がまれにあるので 手元に置いてある。

Fujitsu FKB8745-T101

Image: RIMG0041.JPG

Fujitsu FKB8745-T101

一部では割と有名な富士通高見澤製のキーボード。
メンプレンスイッチのキータッチと シリンドリカル・カーブドスカルプチャというのは PD-KB02 と同じ構成だ。
そもそも PD-KB02 も富士通高見澤製なので 打鍵感なども殆ど変わらない。 PD-KB02 にファンクションキーと カーソルキーを付けた様なものだと思う。
Windows マシンを操作する場合はカーソルキーやファンクションキーが あった方が楽なので手元に置いてあったのだが、 今は手元に Windows マシンがなくなってしまったので、 サーバルームのコンソール用に利用している (良いキーボードなので若干勿体ないな)。

PFU Happy Hacking Keyboard Type-S PD-KB400WNS

Image: RIMG0294.JPG

PFU Happy Hacking Keyboard Type-S PD-KB400WNS

前述の Happy Hacking Keyboard Professional の進化版。
従来のモデルに比較するとキータッチが格段に良くなっている。 カチャカチャ音が完全に消されていて 静かで心地よい音になっているので、 静かなオフィスでも気にせずにタイピングに集中できるし 何よりも打鍵音に安っぽさがなくなって非常に良い感じ。
しかも各キーの軸が安定していてブレる感じがないので キー入力時に全く不快感を感じなくなった。 Type-S という名称だけど全然別モノな感じでとても良い。 現在は iMac に接続されているが、 今の所 PD-KB02 と同じ位気に入った。
問題は高価な事かな。

2011/06/15

php の proc_open() を利用した openssl コマンドの実行

php ネタ

openssl (1) で暗号化されたファイルを復号化する処理を php で実装する必要があった。
pear を探せばその手のモジュールは多分あるだろうと思ったが、 標準で含まれないモジュールに依存したくなかったので 外部コマンドの openssl を利用する方法を試してみた。

php では入力、もしくは出力のみの外部コマンド実行は popen() を利用できるが、 入出力双方向が必要なので proc_open() を利用する。
最初は proc_open() した openssl の入力側パイプに 暗号化されたデータを全て出力 (fwrite()) した後で 出力側パイプから復号化されたデータを読み出し (fread()) ていたのだが、 入力データがある程度のサイズを越えると openssl はデータ読込みの途中でデータを出力しないと 読込み (もしくは処理) をブロックしてしまう様なので、 fwrite() の後に fread() を実行する様に修正した。
この時、当初は stream_select() を利用してパイプの出力側からの 入力可否を監視する様にしたのだが、 stream_set_bloking() を利用してパイプの出力側を 非ブロックモードにする事で、 パイプから読込めない場合でも fread() が即リターンするので stream_select() の呼出しによりコードが煩雑になる事が避けられた。

  1<?php
  2    /*
  3     * Copyright (c) 2011 Mitzyuki IMAIZUMI. All rights reserved.
  4     *
  5     * $Id: decrypt.php 3 2011-06-16 16:15:48Z mitz $
  6     */
  7
  8    define("OPENSSL",    "openssl enc -d -des3 -pass pass:%s");
  9    define("BLOCSIZE",   "4096");
 10
 11    /*
 12     * ファイルの復号化
 13     *   $1: ファイル名
 14     *   $2: サイズ
 15     *   $3: パスフレーズ
 16     */
 17    function    decrypt($file, $size, $pass)
 18    {
 19
 20        $desc = array(
 21            0 => array("pipe", "r"),                /* stdin:  pipe */
 22            1 => array("pipe", "w"),                /* stdout: pipe */
 23            2 => array("file", "/dev/null", "w")    /* stderr: /dev/null */
 24        );
 25
 26        if(($fp = fopen($file, "r+"))){
 27            if($data = fread($fp, $size)){
 28                if(preg_match("/^Salted_/", $data)){
 29                    /* 暗号化されている場合 */
 30                    if($pp = proc_open(sprintf(OPENSSL, $pass), $desc, $pipe)){
 31                        /*
 32                         * `openssl enc -d …' を実行する。
 33                         *
 34                         * ファイルの内容はすでに $data に格納されているので
 35                         * BLOCSIZE 単位で openssl の標準入力に出力する。
 36                         */
 37
 38                        stream_set_write_buffer($pipe[0], 0);
 39                        stream_set_blocking($pipe[1], 0);
 40
 41                        $buf = "";
 42                        
 43                        while($size > 0){
 44                            /*
 45                             * 1 ブロック出力
 46                             * substr() は開始位置に負の値を指定すると
 47                             * 文字列の終端を起点とした開始位置からの
 48                             * 部分文字列が取得できる。
 49                             */
 50                            fwrite($pipe[0], substr($data, 0 - $size, BLOCSIZE));
 51                            $size -= BLOCSIZE;
 52                            /*
 53                             * openssl からの読み出し処理
 54                             * 非ブロッキングなので、
 55                             * 読めない場合は即座に fread() から戻る。
 56                             *
 57                            $buf .= fread($pipe[1], BLOCSIZE);
 58                        }
 59                        fclose($pipe[0]);
 60
 61                        while(!feof($pipe[1]))
 62                            $buf .= fread($pipe[1], BLOCSIZE);
 63                        fclose($pipe[1]);
 64
 65                        proc_close($pp);
 66                    }
 67                    /*
 68                     * 復号化したデータの出力
 69                     * 入力ファイルを書き換える
 70                     */
 71                    fseek($fp, 0);
 72                    ftruncate($fp, 0);
 73                    fwrite($fp, $buf);
 74                }
 75            }
 76            /* 暗号化されていない場合はそのまま close() する */
 77            fclose($fp);
 78        }
 79
 80    }
 81
 82?>
    

2011/06/16 追記

ブロック転送のロジックを整理して最適化した。
データサイズ $size をループの制御変数とする事で 余計な変数や転送ブロック数の計算を削除し、 openssl への出力データの部分文字列切り出し処理で substr() を利用する際に負の値を指定して 開始位置を文字列後端からの位置で指定する様に変更した。

2011/05/24

シェル変数への情報セット

例えば wget (1) コマンドへ proxy サーバの情報を伝えるためには http_proxy シェル変数に URL を http://[ID[:pass@]]FQDN[:port] 形式でセットする必要があるが、 proxy 情報が以下の形式でファイル格納されていれば 1 行で簡単にシェル変数に設定できる。

proxy.example.com                           # プロキシサーバの FQDN
8080                                        # プロキシサーバのポート番号
user                                        # プロキシサーバの認証 ID 
passwd                                      # プロキシサーバの認証パスワード
	
  1#!/bin/sh
  2
  3setproxy()
  4{
  5
  6    local   _var
  7
  8    _var=${1}
  9
 10    if [ -f ${2} ]
 11    then
 12        set -- `sed 's/#.*//g' ${2}`
 13        test -n "${1}" && eval "${_var}=\${1:+http_proxy=http://\${3:+\$3\${4:+:\${4}}@}\${1}\${2:+:\${2}}}"
 14    fi
 15
 16}
 17
 18setproxy http_proxy ファイル名
 19sh -c "${http_proxy} wget …"
 20    :
    


Copyright © Mitzyuki IMAIZUMI 2008,2009. All rights reserved.