フォームデータのデコードとCookieのエンコード

フォームデータのデコード

前回までにフォームから受け取ったデータとして例に出したのは次のようなものでした。

name=No+Name&color=black&mail=&web=&comment=%82%C4%82%B7%82%C4%82%B7%82%C6&lines=3

この文字列はURLエンコードという方法により、エンコードされています。URLエンコードの規則は、次のようなものです。

文字列はCGIプログラムにエンコードされて渡されるので、元の文字列に戻すために、プログラム側でデコードする必要があります。エンコードと逆の手順を踏めば、文字列を元に戻すことができますね。

デコードする関数

  1. '+'を半角スペースに置き換える
  2. %で始まる3バイトを、1バイトの文字に変換する

以上の手順で処理を行うのが、次の関数です。

char* decodestr(char* str)
{
    char* temp_buf = (char*)malloc(strlen(str)+1);
    char *repchar,*buf_pointer=temp_buf;
    char hex_buf[3]; /* 2桁の16進数+文字列終了子 */
    int i;
    long hex;
    if(temp_buf == NULL) return NULL;

    while((repchar=strchr(str,'+')) != NULL) *repchar = ' ';

    /* 一文字ずつ変換しながら一時バッファにコピー */
    for(i=0 ; str[i] != '\0' ; i++,buf_pointer++){
        if(str[i] == '%'){    /* "%"で始まる3桁を1バイトの16進数に */
            sprintf(hex_buf,"%c%c",str[i+1],str[i+2]);
            hex = strtol(hex_buf,NULL,16);
            *buf_pointer = (char)hex; /* char型にキャストされる */
            i += 2;
        }
        else *buf_pointer = str[i];
    }
    *buf_pointer = '\0'; /* 文字列終了子を付加する */

    sprintf(str,"%s",temp_buf);
    free(temp_buf);
    return str;
}

まず文字列中の'+'をすべて半角スペースに置換し、その後、1文字ずつ一時バッファにコピーしていきます。ただし、コピー元の文字が'%'であった場合には、それに続く2バイトを16進数に変換して1文字のコードとし、それを一時バッファにコピーします。最後に一時バッファの内容を元の文字列のバッファにコピーし、一時バッファを開放して処理は終わりです。

"%xy"という3バイトの文字列を16進数に変換する処理としては、2バイト目を0x10倍(0x10をかけるか、4ビット左シフト)して3バイト目を加える、という処理方法が有名ですが、ここでは標準ライブラリ関数のsprintf()とstrtol()を用いて文字列を一発で16進数に変換する方法を紹介します。コードを見てのとおり、一時バッファに%に続く2バイトをsprintf()で文字列として書き込み、strtol()によって16進数に変換しています。strtol()の戻り値の型はlongなので、バッファにコピーするときにはcharにキャストしています。longをcharにキャストするのは一見乱暴なように見えますが、%に続く2桁の数字はもともとunsigned charで表現可能な範囲の文字コードをエンコードしたものなので、long int型の16進数に変換しても、ビット列としてcharで使用する範囲外のビットを使用することはありません。

この処理では、少なくともデコード後の文字列が元の文字列より長くなることは決してありません。したがって、渡された文字列が使っているバッファを、サイズの変更等の必要もなく、そのまま使うことができます。

注意としては、この関数を使ってデコードを行う前に、必ず区切り文字による分割を行う必要があります。そうでないと、データ中でエンコードされていた'&'や';'、'='といった区切り文字がデコードされ、うまく分割できなくなることがあります。

Cookieのエンコード

エンコードする理由とその方法

いろいろなCGIプログラムの解説を見ると、「Cookieの書き込み時にはエンコードする必要がある」と書いてあることが多いのですが、その根拠について詳しく説明されていることは少ないようです。どうやらHTTPで扱える文字種が理由のようですが、詳しく知っている人は教えてください。どちらにしろ、URLエンコードのような方法でエンコードしておいた方が無難ではあるようです。

ただ、少なくともCookieを読み出したときの区切り文字として使われる';'と'='については、何らかの手段でエンコードを行ってから書き込まなければ、次回の読み出し時に正しく読み出せなくなります。そこで、URLエンコードによく似た方法でエンコードしておくことにします。

エンコードする関数

URLエンコードとはいくつかの記号の扱いが微妙に違いますが、基本的には同じ方法でエンコードする関数を以下に示します。この関数でエンコードした文字列も、上で紹介したデコードする関数でそのままデコードすることができます。

/* encodestr()関数でエンコードの対象外となる文字列を定義する */
int isnoencode(const int c)
{
    int noenc[] = { '*','-','.','@','_',0 };
    int i;

    for(i=0;noenc[i]!=0;i++) if(c == noenc[i]) return 1;

    return 0;
}

/* 文字列を渡すとエンコードする */
/* 渡すバッファはmalloc()などで確保したものでなければならない */
/* 引数strの値が変わることがあるので、*/
/* 必ずstr = encodestr(str)のように返却値を使うこと。 */
char* encodestr(char* str)
{
    char* temp_buf = (char*)calloc(strlen(str)*3+1,1);
    char* part = temp_buf;
    int i;
    char cat_buf[10];
    if(temp_buf == NULL) return NULL;

    for(i=0; str[i] != '\0'; i++,part++){
        if(isalnum(str[i]) || isnoencode(str[i])) *part = str[i];
        else{
            sprintf(cat_buf,"%%%02X",(unsigned char)str[i]);
            strcat(temp_buf,cat_buf);
            part++;
            part++;
        }
    }

    str = (char*)realloc(str,strlen(temp_buf)+1);
    strcpy(str,temp_buf);

    free(temp_buf);
    return str;
}

渡された文字列のすべての文字をエンコードすると仮定すると、元の文字列の3倍の長さになります。そこで、この関数では作業用のバッファとして、渡された文字列の3倍(+1)の長さのバッファを確保しています。このバッファは、malloc()ではなくcalloc()を用いて確保します。その理由は、バッファのすべてのバイトを0で確保しておくと、書き換えていない部分はすべて文字列の終了を示す'\0'になり、strcat()関数を使用するために都合が良いからです。

エンコード処理の結果は一時バッファに格納されているので、この結果を関数の呼び出し側に返す必要があります。エンコードされた文字列は多くの場合元の文字列より長くなるので、関数の呼び出し時に渡された文字列が使っていたバッファをそのまま使うわけにはいきません。そこで、realloc()によって適切な大きさに変更してから一時バッファの内容をコピーします。このため、この関数に渡す文字列はmalloc()等で動的に確保されたバッファ中のものでなければなりません。また、realloc()の結果、バッファの先頭アドレスが変わってしまうことがあるので、コメントにもあるように、呼び出し側では必ず返却値を使用する必要があります。

Cookieの書き込み方法

Cookieの書き込みは、標準出力に対してContent-type:text/html\n\nを出力する前に行います。詳しくは、以下を参考にすると良いかもしれません。

ソースコード

使用しているヘッダファイルなど、実際のソースコードについては以下を参照してください。


Index Page/フォーム・Cookieの取得と分割