入力フォームから受け取ったデータやCookieの処理方法を考えてみましょう。
WebブラウザがCGIプログラムにアクセスしたとき、サーバにはさまざまな環境変数が渡されます。CGIプログラムの処理の多くは、環境変数を用いて行います。Cの標準ライブラリにはgetenv()という関数があるので、これを用いて環境変数を取得します。
| char* getenv(const char* str) | |
|---|---|
| 標準ライブラリ関数/stdlib.h | |
| 引数 | 検索する環境変数の名前 |
| 機能 | strと一致する名前の環境変数を探す。 |
| 返却値 | 環境変数が見つかった場合、環境変数の値の文字列の先頭へのポインタを返す。見つからない場合、NULLを返す。 |
| 注意 | この関数によって得た環境変数の値(文字列)を変更してはならない。 |
POSTメソッドの場合、入力フォームの文字列は、標準入力(stdin)から渡されます。また、渡された文字列の長さは、環境変数CONTENT_LENGTHに設定されます。CONTENT_LENGTHを調べてその文字数だけバッファを確保しても構いませんが、掲示板のようなプログラムの場合、普通にはあり得ないような異常に長い文字列に対応する必要はないので、受け取る文字数をある程度制限しておくべきでしょう。そこで、実際のCGIプログラムでは、大き目の固定長のバッファを確保しておき、CONTENT_LENGTHの値がバッファのサイズより大きい場合にはエラーを返すなどの方法をとります。
標準入力から文字列を取得するためにgets()関数を使わないでください。読み込む最大文字数を指定できないので、バッファオーバーフローのようなセキュリティホールの原因になります。fgets()関数を用いて取得する、ごく単純化した例は次のようになります。
char dat[4096]; fgets(dat,sizeof dat,stdin);
Cookieの文字列は、環境変数HTTP_COOKIEに格納されます。したがってgetenv()関数で取得できます。
char* cookie_str = getenv("HTTP_COOKIE");
フォームから取得したデータは、次のような文字列になっています。
少しごちゃごちゃしていてわかりにくいのですが、これは以下のような文字列を連結したものです。
name=No+Name color=black mail= web= comment=%82%C4%82%B7%82%C4%82%B7%82%C6 lines=3
これと最初の文字列を比較すると、name=No+Nameやcolor=blackといった文字列が、'&'という文字を間に挟む形で連なっていることがわかると思います。
また、環境変数HTTP_COOKIEに格納されているCookieの文字列は、次のようになっています。
name=No Name; color=black; mailad=; urlad=; lines=30;
Cookieの文字列もフォームの場合と同じような形ですが、';'という文字を間に挟む形で連なっています。この例では';'の後にスペースがありますが、Webブラウザによってはこの半角スペースがない文字列を渡してくるものもあるようです。
このように、フォームとCookieから受け取ったデータは、よく似た構造の文字列になっています。名前=値が区切り記号で区切られ、データの個数だけ連なっています。両者の違いは一組のデータの終わりを表す区切り記号だけです。フォームの場合は'&'、Cookieの場合は';'で区切られています。このような形態の文字列に名前があるのかどうかわかりませんが、ここでは便宜上文字列チェーンと呼ぶことにしましょう。
このようなデータをどう処理するかですが、ここでちょっとデータの構造に注目してみましょう。データ名=データ値で、データの名前と内容(値)がセットになっていますね。これをまとめて扱うことができれば便利そうです。そこで、こういう形式のデータを扱うための型を考えましょう。
typedef struct{
char* tag;
char* str;
}taggedstr;
char型のポインタが2個入っているだけというこれ以上ないくらい単純な構造体ですね。
C++にしてstd::mapを使えよという声がどこかから聞こえてきましたが、ここではとりあえずC言語のみで扱う方法を考えていきます。データ名=データ値で、データ名を「タグ」、データ値を「文字列」とします。ということで、このタイプのデータは「タグ付き文字列」と呼ぶことにして、この構造体で扱うことにしましょう。「タグ付き」とはいっても、ここでいう「タグ」とは構造体タグとは全く関係なく、ましてやHTMLの要素の始まりを示すタグのことでもありません。
データ名=値&データ名=値&...というように'&'や';'で区切られて連なっている文字列を整理して、上で考えたタグ付き文字列の配列に代入することを考えてみましょう。
この文字列には2種類の区切り文字があります。1セットのデータと次のデータを区切るもの(&,;)と、1セットのデータ中で「タグ」と「文字列」を区切るもの(=)です。まず'&'あるいは';'で分割し、その後'='で分割するという二段階の手順で処理を行います。
タグ付き文字列の配列にデータを整理するのが目的なので、その配列の要素数がいくつ必要なのかを調べ、把握しておく必要があります。このあたりがPerlなどと比べたときのCの面倒なところですね。とはいっても手順は決まっているので、難しいことはありません。
ここでは下のようなデータをフォームから受け取ったとして、これを処理することを例に考えていきましょう。
フォームデータの場合、データ一件ごとの区切り文字は'&'になっています。上のデータの場合、データの数は6件で、'&'が5個含まれています。データが何件ある場合でも、区切り文字である'&'の数がわかれば、その数+1件のデータが渡されたということになります。
そこで、元の文字列を単純に先頭から参照していって、出てきた'&'の数+1を配列の要素数とすることにしましょう。これが入力フォームではなくCookieから受け取ったデータの場合は区切り文字が';'になるので、';'の数を数えることになります。
分割した結果が何個の要素になるのか調べた上で、'&'の部分で文字列を分割するわけですが、この分割にもいくつか方法が考えられます。
元の文字列を保持するか書き換えるかという選択になるわけですが、ここでは後者を採用します。渡された文字列データの最初の状態を保持しておいても、あまりメリットがありません。分割したデータのために一時的にメモリ領域を確保してコピーするのはメモリの無駄といえば無駄な上に、エラー処理などのコーディングが面倒になります。
ということで、1段目の分割では、渡された文字列を'&'の位置で区切ることにします。こういうときに、標準ライブラリ関数のstrtok()を使うと便利ですね。strtok()関数については、strtok()の動作で詳しく解説しています。ただ、分割した結果の要素の数だけポインタが必要になります。最初に要素数を調べることになっているので、判明した要素数分を動的に確保することにしましょう。
分割処理を行う関数として記述したのが、次のコードです。引数として書き換え可能な元の文字列、区切り文字、taggedstr構造体のポインタのアドレスを渡します。
/* Cookieやフォームのデータをタグ付き文字列の配列に整理して格納する */
/* 1以上の分割した数を返す。メモリの確保に失敗した場合には、0を返す。 */
int divntag(char* str,const char* delim,taggedstr** element)
{
int e_id,elems = 1;
char *token,*devpt;
/* 一段目の区切り文字の数を調べる */
for(devpt=str; *devpt!='\0'; devpt++) if(*devpt == *delim) elems++;
/* タグ付き文字列配列を確保 */
*element = (taggedstr*)malloc((elems+1) * sizeof(taggedstr));
if(*element == NULL) return 0;
(*element)[0].tag = strtok(str,delim); /* まず最初の区切り文字で分割する */
for(e_id=1 ; (token=strtok(NULL,delim))!=NULL ; e_id++){
(*element)[e_id].tag = token;
}
/* 分割してポインタをタグ付き文字列配列に代入 */
for(e_id=0;e_id<elems;e_id++){
(*element)[e_id].tag = strtok((*element)[e_id].tag,"=");
(*element)[e_id].str = strtok(NULL,"=");
if((*element)[e_id].str == NULL){
if(((*element)[e_id].str = (char*)malloc(1)) == NULL) return 0;
*(*element)[e_id].str = '\0';
}
}
(*element)[e_id].tag = NULL; /* タグがNULLの場合を配列の最後とする */
(*element)[e_id].str = NULL;
return elems;
}
最初にデータの要素数を数え、その要素数+1の分だけタグ付き文字列の配列をmalloc()関数で確保します。もしここでメモリの確保に失敗してしまった場合には、返却値として0を返すことにします。要素1個分多くメモリを確保したのは後の検索処理を簡単にするためで、最後尾にNULLを代入しておきます。
関数中でタグ付き文字列の配列を確保し、それを呼び出し側でも使うために、この関数ではタグ付き文字列配列の先頭へのポインタを、常に間接参照によって操作しています。この理由は関数側でmalloc()するときの注意を参照してください。このために、構造体配列の要素の指定方法が少々ややこしくなっています。たとえば、
(*element)[e_id].tag = token;
という部分では、[]によって配列の要素を指定する演算子の方が、*による間接参照よりも優先順位が高いので、先に間接参照が行われるように(*element)のようにする必要があります。もしこの括弧を忘れて*element[e_id].tagのようにしてしまうと、char**で宣言したelementというポインタをe_id回インクリメントしたところにある実体のtagというメンバを参照するという意味になってしまいます。*elementはtaggedstr構造体に対応するポインタで、増減の単位はsizeof(taggedstr)ですが、elementはtaggedstr構造体に対応するポインタのポインタで、増減の単位はその処理系でのポインタのサイズになります。したがってそのような間違いをすると矛盾が生じ、実行するとバグの原因になります。
この関数では、確保したタグ付き文字列の文字列タグをとりあえず、strtok()関数により、順次'&'の次から始まる文字列に割り当てていきます。この例のデータの場合、次のようなイメージになります。
(*element)[0].tag→name=No+Name (*element)[1].tag→color=black (*element)[2].tag→mail= (*element)[3].tag→web= (*element)[4].tag→comment=%82%C4%82%B7%82%C4%82%B7%82%C6 (*element)[5].tag→lines=3
実際に各要素のポインタが示している実体は文字列の先頭の文字だけで、本当は改行もないのですが、感覚的にわかりやすいように上のように書きました。まず、こうして文字列をすべて要素に分割します。こうすると、あとは2段階目の分割だけになりましたね。
特に難しいことはなく、'='で区切って、前半部分をタグ付き文字列のタグに、後半部分をタグ付き文字列の文字列に代入しています。ここでも演算子の優先順位に注意してください。
mail=やweb=などのようにキーのみで値となる文字列が存在しない場合、strtok()関数の仕様により、ポインタにNULLが代入されます。仕様としてはNULLのままにしておくのが自然なのですが、そうすると呼び出し側での処理が煩雑になってしまいます。呼び出し側で文字列として他の関数にNULLを渡してしまいエラーになることを防ぐため、毎回チェックが必要になります。また現状でもメモリリークの原因となりやすいのであまり良くありません。グローバル変数として空文字列を用意することも考えたのですがあまりスマートでないのでやめました。今後はNULLのままで処理する方向にしていきましょう。
「キー」と「値」として「タグ」と「文字列」を持つタグ付き文字列構造体ですが、指定したキー(つまりこの場合はタグ)が、配列の何番目の要素に格納されているのか知ることができなければ、その後の処理ができません。そこで作ったのが次の関数です。
/* タグ付き文字列の配列から、与えられたタグを持つデータをmaxまで探す */
/* その要素番号を返す。なければ負の数を返す */
/* この場合要素数はさほど多くないはずなので逐次探索を行う */
int specifydata(const taggedstr* dataset,const char* key)
{
int i;
for(i=0;dataset[i].tag!=NULL;i++) if(strcmp(key,dataset[i].tag) == 0) return i;
return -1;
}
常識的に考えて、フォームデータの要素数やCookie変数が何百や何千もあるようなことは考えにくいので、アルゴリズムを工夫する必要も特になく、最も簡単な逐次探索を用いています。
今回紹介した関数のソースコードを、以下に置いてあります。