付録D URLエンコーディング(URLエンコード または URLエスケープ)

フォームから問い合わせ文字列を受け取ったり、 クッキーを発行するとき、 HTTPプロトコルとCGIが適正に処理できるデータ形式に変更する必要があります。 そこで、 付録Dでは、 HTTPクライアントとCGIプログラムで行われている、 URLエンコーディングについて解説します。

実はRFCで"%16進数表記"でエンコードしなくて良い文字が規定されてはいます。 しかし、 実際にエンコードされる文字はプログラムによってまちまちで、 統一されているわけではないのが2003年8月の現状です。

D.1 URLエンコーディングとデコーディング

まずは手っ取り早くURLエンコーディングの方法とデコードする方法について挙げ、 後からじっくりとHTTPやCGIの仕様ではどうなっているか解説します。

D.1.1 エンコードする方法

URLエンコードは1文字(ただし1バイト = 1オクテット)毎に行います。 その変換規則は下記になります。

すなわち、 そのまま(英数とピリオドとマイナスとアンダーバー)、 空白、 それ以外でエンコード方法が異なります。 その手順は以下、ほんの数行、で書けてしまいます。 アルファベットか数字を判別する標準ライブラリ関数 isalnum() と 、 16進数表記で書出す snprintf() に注意してください。 なお、 コードは解りやすさ優先なので効率は度外視しています。

#include<stdio.h> /* snprintf */
#include<ctype.h> /* isalnum */
#define MAX_CONTENT_LENGTH 4096
#define WHITE_SPACE ' '
#define MINUS '-'
#define UNDERBAR '_'
#define PERIOD '.'
(前略)
char source[MAX_CONTENT_LENGTH];
int source_length;
char destination[MAX_CONTENT_LENGTH * 3 + 1];
int destination_length;
int index;
(中略 : エンコード対象を source に,その長さを source_length にセットする)
index = 0;
destination_length = 0;
while(index < source_length && index < MAX_CONTENT_LENGTH)
  {
    int current = (unsigned char)(source[index]);
    if(isalnum(current) || current == PERIOD ||current == MINUS || current == UNDERBAR)
      {
        destination[destination_length] = current;
        destination_length++;
      }
    else if(current == WHITE_SPACE)
      {
        destination[destination_length] = '+';
        destination_length++;
      }
    else 
      {
        destination[destination_length] = '%';
        snprintf(&destination[destination_length],3,"%X",current);
        destination_length += 3;
      }
    index++;
  }
destination[destination_length] = '\0';
(後略 : エンコードされた文字は destination に , その長さは destination_length にセットされている)

D.1.2 デコードする方法

URLエンコードされた文字列をデコードする方法は、 エンコード方法の逆の処理を行うことです。 すなわち次の手順で行います。

特に改行コードはプラットフォームに依存します。 Windowsは復帰と行送り(0x0D と 0x0A)である一方、 UNIX系ですと行送り(0x0A)だけです。 従って、 テキストエリアで提出されたデータをデコードした際、 復帰文字の削除を忘れないで下さい。

より詳細な説明は言葉よりも疑似プログラムを挙げた方が良いかもしれません。 基数が16で文字列を数字に変換する関数 strtol() の使い方、 そして、 その後の変換後の数字のチェックを行う部分に注意してください。 なお、 コードは解りやすさ優先なので効率は度外視しています。

#include<stdlib.h> /* strtol */
#include<limits.h> /* strtol , UCHAR_MAX */

#define PLUS '+'
#define WHITE_SPACE ' '
#define MAX_CONTENT_LENGTH 4096
#define PERCENT '%'
(前略)
char source[MAX_CONTENT_LENGTH];
int source_length;
char destination[MAX_CONTENT_LENGTH + 1];
int destination_length;
int index;
(中略 : デコード対象を source に,その長さを source_length にセットする)
source_index = 0;
destination_length = 0;
index = 0;
while(index < source_length && index < MAX_CONTENT_LENGTH)
  {
    int current = (unsigned char)(source[index]);
    if(current == PERCENT && index + 2 < source_length && index + 2 < MAX_CONTENT_LENGTH)
      {
        char hexes[3];
        long decoded_value = 0x20;
        char* ptr_invalid_char;
        hexes[0] = source[index+1];
        hexes[1] = source[index+2];
        hexes[2] = '\0';
        decoded_value = strtol(hexes,&ptr_invalid_char,16);
        if((ptr_invalid_char == NULL) && (0<= decoded_value) && (decoded_value <= UCHAR_MAX))
          {
            destination[destination_length] = (char)(decoded_value);
            destination_length++;
          }
        else
          {
            destination[destination_length] = PERCENT;
            destination[destination_length + 1] = hexes[0];
            destination[destination_length + 2] = hexes[1];
            destination_length += 3;
          }
        index += 3;
      }
    else if(current == PLUS)
      {
        destination[destination_length] = WHITE_SPACE;
        destination_length++;
        index++;
      }
    else
      {
        destination[destination_length] = source[index];
        destination_length++;
        index++;
      }
  }
destination[destination_length] = '\0';
(後略 : デコードされた文字は destination に , その長さは destination_length にセットされている)

D.2 URLエンコード方法の由来

HTMLという言語で書かれたデータは、 HTTPというの取り決め(プロトコル)に従って、 CGIを経由します。 従って、 データはHTTPとCGIの両方に通らないと、 CGIプログラムには間違ったデータばかり入力されることになります。 そのため、 データをやりとりするには、 HTMLとHTTPとCGIの仕様に従うように変換する必要があります。

CGIはMosaicで有名なNCSAが規定しており、 2003年8月現在は、 W3CがRFCの草稿をまとめています。 すなわち、 CGIの仕様は統一されていません。 そのため、 CGIプログラムから他のCGIプログラムを呼び出したり、 HTTPクッキーに用いるURLエンコーディングの方法は、 プログラマによってばらばらだったりします。

CGIプログラマが用いるURLエンコーディングは、 隠しフォームやクッキーを用いて、 継続的なデータを交換するために用います。 そこで、 HTMLとHTTPとNCSAのCGIの仕様に基づくことは勿論、 CGIプログラムでURLデコーディングを行うことを考慮してエンコード方法を確認します。

D.2.1 HTML

HTMLはISO/IEC15445(HTML4.01)やRFC1866(HTML2.0)で規定されています。 フォームのエンコード方法はその中のRFC1866の第8.2.1節にあります。

  1. スペースは '+' に置換する。
  2. RFC1738(後述)で予約文字として定義されている文字はその16進数を%XX形式に変換する。
  3. 属性名と属性値はイコール"="で区切る。
  4. レコード(属性名と属性値の2つ組み)はアンド"&"で区切る。

D.2.2 HTTP

HTTPでは問合わせ文字列も含めてURLで指定したデータとやりとりします。 そのURLの書式は以下です。

場所 = http://ホスト名[:ポート番号][絶対パス[?問合せ文字列]]

現在対象としている部分は?以降の問い合わせ文字列です。 問合わせ文字列はRFC 2616で、 その中のエンコード方法はRFC2396(後述)で規定されています。

D.2.3 URLエンコードを規定したRFC2396(旧RFC1738)

HTTPもHTMLも、 そして、 CGIのRFCのドラフトでもURLエンコードに関してはRFC2396に従うと定められています。 そこでは、 予約文字(文法では reserved という文字集合で定義している)は勿論のこと、 非安全文字(文法では unsafe という文字集合で定義している)も%XXでエスケープすると規定しています。 %XXはパーセント("%")にその文字のコードの16進数表記を加えた形になります。 例えば、 セミコロン(";")という文字の文字コードは16進数で3Bなので、 "%3B"とエンコードされます。 予約文字は以下になります。 なお、 プラスは空白を置換するための特殊な用途で使用しているので、 予約文字になります。

; / ? : @ & = + , $
URLエンコードにおけるHTTPの非安全文字も、 %付の16進数表記に変換するべきだと触れています。 HTTPの非安全文字は以下になります。
# % < > "

英数字以外でURLエンコーディングしなくても良い文字は以下になります。

 - _ . ! ~ * ' ( ) 

D.2.4 統一的な規定なき文字たち「- _ . ! ~ * ' ( ) 」

2003年8月現在、 - _ . ! ~ * ' ( ) の9文字に関しては、 統一的なエンコード方法が定められていません。 URLエンコードの仕様を参照しても、 これらの文字はエンコードしなくても良いとされています。 従って、 これらの文字は、 HTTPクライアント(WWWブラウザ)や、 ライブラリで"%XX"の形式に変換する対象がまちまちだったりします。 以降は何がエンコードされるか眺めてみましょう。

まずは Microsoft Internet Explorer から。 2003年8月現在は8割以上のシェアがあるため、 CGIプログラムをテストを行う際、 動作確認を行わなければならないブラウザの1つです。 そんな Internet Explorer がエンコードしない文字は次の文字です。 特筆するべき点は、 アットマーク("@" アスキーコードは0x40)をエンコードしないことです。

(Internet Explorerがエンコードしない文字)  * - . @ _ 

次に Netscape Navigator です。 1997年にソースコードが公開され、 フリーの Mozilla が誕生しました。 これらがエンコードしない文字は以下で、 特筆すべき点はありません。

(Netscape Navigator や Mozilla がエンコードしない文字) * - . _ 

ライブラリの例として Java言語の java.net.URLEncoder を挙げます。 Sun Microsystems からダウンロードできるJ2SDK1.4.1に付属しているソースコードを見ると、 前述の Netscape Navigator と同じ文字、 すなたち、英数字、"*" 、 "-" 、"." 、 そして "_" 以外の文字をエンコードしていることがわかります。 以下は URLEncoder.java バージョン 1.26 に書かれているコメントの抜粋です。 要約すると、 「どうも "*" と "-" と "." と "_" 以外はエンコードしてるなぁ。 なぜかはよくわからないけど、 どうも安全を期しているのだろうな。 と言うわけで、O'Reilly の"HTML:The Definitve Guide"という本 (邦訳 HTML & XHTML) に従うことにした」 ということが書かれています。 特筆するべき点は、 Internet Explorer は RFC で予約文字と規定している "@" をエンコードしていないことを、 わざわざ1段落使って書いていることです。

It appears that both Netscape and Internet Explorer escape all special characters from this list with the exception of "-", "_", ".", "*". While it is not clear why they are escaping the other characters, perhaps it is safest to assume that there might be contexts in which the others are unsafe if not escaped. Therefore, we will use the same list. It is also noteworthy that this is consistent with O'Reilly's "HTML: The Definitive Guide" (page 164).

As a last note, Intenet Explorer does not encode the "@" character which is clearly not unreserved according to the RFC. We are being consistent with the RFC in this matter, as is Netscape.

D.2.5 結局ピリオド"."とマイナス"-"とアンダーバー"_"と英数字以外は

RFCやISOでHTMLやHTTPが規定されている一方で、 実際のブラウザやライブラリは、 URLエンコーディングする文字がまちまちだったりするのが現状です。 そこで、 当ウェブページでは、 万難を排して、 以下の指針でエンコードを行います。 特筆するべき点はアスタリスク"*"もエンコードすることです。

D.3 プログラム

第D.1節ではURLエンコード/デコード説明するために、 C言語風のプログラムを挙げました。 ここでは、 わたしが汎用的な関数として使用しているプログラムを挙げます。

D.3.1 ソースコードのダウンロード

ソースコードは以下の x_www_form_url_translator.c とurl_translator_test.c です。 前者はURLエンコードとデコードの関数本体で、 後者はそのエンコードとデコード関数をテストするプログラムです。

Makefileはテストプログラムの作り方が書かれています。

  1. x_www_form_url_translator.c
  2. url_translator_test.c
  3. Makefile

D.3.2 プログラムの仕様

C言語の関数としては以下の形式が適当だと思います。

/* URLエンコードする. */
int url_encode(char* dst,size_t dst_len,const char* src,size_t src_len);
/* URLデコードする */
int url_encode(char* dst,size_t dst_len,const char* src,size_t src_len,char** end_ptr);

引数はエンコードは4つ、 デコードは5つです。 始めの4つまでは共通で、 dstは変換先の文字列の格納先の先頭へのポインタ、 dst_lenは変換先の文字列の最大の長さです。 srcは変換元の文字列の格納先の先頭へのポインタ、 src_lenは変換元の文字列の最大の長さです。 長さを指定するのは バッファオーバーフロー を防ぐためです。 ただし、 src と dst の領域が重なった場合、 あるいは、 十分な大きさ出ない場合の動作は未定義です。 セキュリティホールを招くので、 データを読み書きする領域には注意を払う必要があります。

デコードの5番目の引数は、 デコードが失敗した際、 その文字へのポインタを格納する変数です。 5番目の引数がNULLで無い場合に保存されます。 成功した場合はデコードが終了した時点での位置を保存します。 NULLを渡した場合、 変換が失敗した位置を探す必要があります。

これらの関数の返値は、 変換した文字 (エンコード対象となっていない "." , "_" , "-" も含む) の個数を返します。 変換したバイト数ではありません。 ただし、 デコード対象に不適正な書式(例えば"%XX"形式のXXが16進数表記でない)の場合、

D.3.3 エンコードとデコードのプログラム

プログラム x_www_form_url_translator.c にはエンコードとデコードを行う関数が書かれています。 28行目からがエンコード、98行目からがデコードです。

エンコード方法の"%XX"の形式に変換している部分が64行目です。 この形式は1文字が3文字になります。 そのため63行目で変換した文字を書き出す領域が十分か検査し、 不足していれば変換を行わずにループから抜け出します。

一方、 デコードの方法については、 第5引数の end_ptr の使い方に注意してください。 これはポインタへのポインタです。

1: #include<string.h>
2: #include<stdlib.h> /* strtol */
3: #include<limits.h> /* LONG_MAX , LONG_MIN */
4: #include<stdio.h> /* sprintf */
5: /*
6:  * URLエンコードを行います.
7:  * 機能 : URLデコードを行う
8:  * 書式 : int url_encode
9:  * (char* dst,size_t dst_len,const char* src,int src_len);
10:  * 引数 : dst 変換した文字の書き出し先.
11:  *       dst_len 変換した文字の書き出し先の最大長.
12:  *       src 変換元の文字.
13:  *       src_len 変換元の文字の長さ.
14:  * 返値 : エンコードした文字の数(そのままも含む)
15:  * 詳細 : 
16:  *  URLエンコードを行います.
17:  *  空白,英数字,ピリオド,アンダーバー,そして,マイナス以外は全て %XX 形式に
18:  *  エンコードされます.
19:  *  ただし空白はプラスに変換されます.
20:  *  エンコードすると1文字が最大で3文字になります.
21:  *  そのため,
22:  *  変換した文字の書き出し先に十分な領域が無い場合,
23:  *  変換を中断します.
24:  *  これは返値が strlen(src) より小さいことで確かめることができます.
25:  *  この関数は '\0' をエンコードしません.
26:  *  C言語の文字列における終端文字として扱います.
27:  */
28: int 
29: url_encode(char* dst,
30:           size_t dst_len,
31:           const char* src,
32:           int src_len)
33: {
34:   /*--- 局所変数 ------------------------------------------------------------*/
35:   int idx_src;
36:   int idx_dst;
37:   /* 現在の変換した個数. */
38:   int cnt;
39:   /*--- 引数チェック --------------------------------------------------------*/
40:   if((dst == NULL) || (dst_len < 1) || (src == NULL) || (src_len < 1))
41:    {
42:      return 0;
43:    }
44:   /*--- 初期化 --------------------------------------------------------------*/
45:   cnt = 0;
46:   /*--- 本体 ----------------------------------------------------------------*/
47:   for(idx_src = idx_dst = 0 ; 
48:      (idx_src < src_len) && (idx_dst < dst_len) && (src[idx_src] != '\0');
49:      idx_src++)
50:    {
51:      if(src[idx_src] == '.' || src[idx_src] == '_' || src[idx_src] == '-')
52:        {
53:          dst[idx_dst] = src[idx_src];
54:          idx_dst += 1;
55:        }
56:      else if(src[idx_src] == ' ')
57:        {
58:          dst[idx_dst] = '+';
59:          idx_dst += 1;
60:        }
61:      else
62:        {
63:          if(idx_dst + 3 > dst_len)break;
64:          idx_dst += sprintf(&dst[idx_dst],"%%%2X",(unsigned char)(src[idx_src]));
65:        }
66:      cnt++;
67:      if(idx_dst + 1 < dst_len)dst[idx_dst] = '\0';
68:    }
69:   goto FINALLY;
70:   /*--- 後始末 --------------------------------------------------------------*/
71:  FINALLY:
72:   return cnt;
73: }
74: /*
75:  * URLデコードを行います.
76:  * 機能 : URLデコードを行う
77:  * 書式 : int url_decode
78:  * (char* dst,size_t dst_len,const char* src,int src_len,char** end_ptr);
79:  * 引数 : dst 変換した文字の書き出し先.
80:  *       dst_len 変換した文字の書き出し先の最大長.
81:  *       src 変換元の文字.
82:  *       src_len 変換元の文字の長さ.
83:  *       end_ptr デコードを終了した文字へのポインタのポインタ.
84:  * 返値 : デコードした文字の数(そのままも含む)
85:  * 詳細 :
86:  *  URLデコードを行います.
87:  *  プラスは空白に,他の文字は"%XX"表記されている場合は16進数への変換を行います.
88:  *  この関数は変換に成功した文字数を返し,end_ptrがNULLで無ければ,
89:  *  **end_ptr は '\0' になります.
90:  *  変換した文字列の書き出し先に十分な領域が無い場合は変換を中断し,
91:  *  デコードした(そのままも含む)文字の個数を返します.
92:  *  またデコードに不適正な値が含まれている場合,
93:  *  例えば"%XX"の"XX"の部分が2桁の16進数表記で無い場合,
94:  *  end_ptr が NULL でなければそれに不適正な値への参照を設定します.
95:  *  この関数は '\0' をエンコードしません.
96:  *  C言語の文字列における終端文字として扱います.
97:  */
98: int 
99: url_decode(char* dst,size_t dst_len,const char* src,int src_len,char** end_ptr)
100: {
101:   /*--- 局所変数 ------------------------------------------------------------*/
102:   int idx_src;
103:   int idx_dst;
104:   int cnt;
105:   char work[3];
106:   char* strtol_end_ptr;
107:   unsigned int code;
108:   /*--- 引数チェック --------------------------------------------------------*/
109:   if((dst == NULL) || (dst_len < 1) || (src == NULL) || (src_len < 1))
110:   {
111:     if(end_ptr != NULL)*end_ptr = (char*)src;
112:     return 0;
113:   }
114:   /*--- 初期化 --------------------------------------------------------------*/
115:   cnt = 0;
116:   if(end_ptr != NULL)*end_ptr = NULL;
117:   /*--- 本体 ----------------------------------------------------------------*/
118:   for(idx_src = idx_dst = 0;
119:     (idx_src < src_len) && (idx_dst < dst_len) && (src[idx_src] != '\0');
120:     idx_dst++ , cnt++)
121:   {
122:     if(src[idx_src] == '%')
123:       {
124:         if(idx_src + 2 > src_len)goto EXCEPTION;
125:         work[0] = src[idx_src+1];
126:         work[1] = src[idx_src+2];
127:         work[2] = '\0';
128:         code = strtol(work,&strtol_end_ptr,16);
129:         *end_ptr = (char*)(&src[idx_src + (strtol_end_ptr - work) + 1]);
130:         if(code == LONG_MIN || code == LONG_MAX)
131:           {
132:             goto EXCEPTION;
133:           }
134:         if(strtol_end_ptr != NULL)
135:           {
136:             if(*strtol_end_ptr != '\0')goto EXCEPTION;
137:           }
138:         dst[idx_dst] = code;
139:         idx_src += 3;
140:       }
141:     else if(src[idx_src] == '+')
142:       {
143:         dst[idx_dst] = ' ';
144:         idx_src += 1;
145:       }
146:     else
147:       {
148:         dst[idx_dst] = src[idx_src];
149:         idx_src += 1;
150:       }
151:     if(idx_dst + 1 < dst_len)dst[idx_dst + 1] = '\0';
152:   }
153:   if(end_ptr != NULL)*end_ptr = (char*)(src + idx_src);
154:   goto FINALLY;
155:   /*--- 例外 ---------------------------------------------------------------*/
156:  EXCEPTION:
157:   /*--- 後始末 -------------------------------------------------------------*/
158:  FINALLY:
159:   return cnt;
160: }

D.3.4 テストケース

URLエンコードする文字はたったの1オクテット、すなわち、256種類しかありません。 そのため、 全ての文字でエンコードとデコードが正しく行われることを確認することができます。 その中でも印字できない文字はコードでしか確認できないため、 今回は目で見て確かめられる印字可能なアスキーコードを対象に扱います。

印字可能なアスキーコードに付いては, 254行目から始まる関数で検査します. テストデータは14行目から214行目までの文字 あるいは 固定文字列です。 265行目から281行目で配列にデータを設定しています。

また、 351行目からはデコードできない文字が与えられた場合、 エラー処理が正常に行われているか検査するコードです。 356行目でわざと不正な入力データ ("%G1"の部分が16進数表記でない不正なデータ) を定義してデコード関数に与え、 関数の返値を368行目で、 変換を中断した文字へのポインタを374行目で確認しています。

389行目からはISO-2022-JP文字セットのEUCコードの文字で検査するコードです。 掲示板を作る際は日本語も処理することが必要です。 ここではEUCコードについてテストしています。 実際は、 ユーザ入力のデータは日本語EUCコードとは限らないため、 別にコードの判別と変換するプログラムが必要です。

1: #include<stdio.h>  /* printf */
2: #include<stdlib.h> /* exit */
3: #include<string.h> /* memset */
4: /*
5:  * url_encode() と url_decode() をテストします.
6:  */
7: /*----------------------------------------------------------------------------
8:  * 定数 : テストデータ
9:  *--------------------------------------------------------------------------*/
10: #define PRINTABLE_ASCII_TABLE_SIZE 95
11: /* 
12:  * 印字可能なアスキー文字です(0x20番から0x7E番まで). 
13:  */
14: static const char 
15: CHARS_OF_PRINTABLE_ASCII[PRINTABLE_ASCII_TABLE_SIZE] =
16: {
17:   ' ' , /* 0x20 */ 
18:   '!' , /* 0x21 */ 
19:   '"' , /* 0x22 */ 
20:   '#' , /* 0x23 */ 
21:   '$' , /* 0x24 */ 
22:   '%' , /* 0x25 */ 
23:   '&' , /* 0x26 */ 
24:   '\'', /* 0x27 */ 
25:   '(' , /* 0x28 */ 
26:   ')' , /* 0x29 */ 
27:   '*' , /* 0x2A */ 
28:   '+' , /* 0x2B */ 
29:   ',' , /* 0x2C */ 
30:   '-' , /* 0x2D */ 
31:   '.' , /* 0x2E */ 
32:   '/' , /* 0x2F */ 
33:   '0' , /* 0x30 */ 
34:   '1' , /* 0x31 */ 
35:   '2' , /* 0x32 */ 
36:   '3' , /* 0x33 */ 
37:   '4' , /* 0x34 */ 
38:   '5' , /* 0x35 */ 
39:   '6' , /* 0x36 */ 
40:   '7' , /* 0x37 */ 
41:   '8' , /* 0x38 */ 
42:   '9' , /* 0x39 */ 
43:   ':' , /* 0x3A */ 
44:   ';' , /* 0x3B */ 
45:   '<' , /* 0x3C */ 
46:   '=' , /* 0x3D */ 
47:   '>' , /* 0x3E */ 
48:   '?' , /* 0x3F */ 
49:   '@' , /* 0x40 */ 
50:   'A' , /* 0x41 */ 
51:   'B' , /* 0x42 */ 
52:   'C' , /* 0x43 */ 
53:   'D' , /* 0x44 */ 
54:   'E' , /* 0x45 */ 
55:   'F' , /* 0x46 */ 
56:   'G' , /* 0x47 */ 
57:   'H' , /* 0x48 */ 
58:   'I' , /* 0x49 */ 
59:   'J' , /* 0x4A */ 
60:   'K' , /* 0x4B */ 
61:   'L' , /* 0x4C */ 
62:   'M' , /* 0x4D */ 
63:   'N' , /* 0x4E */ 
64:   'O' , /* 0x4F */ 
65:   'P' , /* 0x50 */ 
66:   'Q' , /* 0x51 */ 
67:   'R' , /* 0x52 */ 
68:   'S' , /* 0x53 */ 
69:   'T' , /* 0x54 */ 
70:   'U' , /* 0x55 */ 
71:   'V' , /* 0x56 */ 
72:   'W' , /* 0x57 */ 
73:   'X' , /* 0x58 */ 
74:   'Y' , /* 0x59 */ 
75:   'Z' , /* 0x5A */ 
76:   '[' , /* 0x5B */ 
77:   '\\', /* 0x5C */ 
78:   ']' , /* 0x5D */ 
79:   '^' , /* 0x5E */ 
80:   '_' , /* 0x5F */ 
81:   '`' , /* 0x60 */ 
82:   'a' , /* 0x61 */ 
83:   'b' , /* 0x62 */ 
84:   'c' , /* 0x63 */ 
85:   'd' , /* 0x64 */ 
86:   'e' , /* 0x65 */ 
87:   'f' , /* 0x66 */ 
88:   'g' , /* 0x67 */ 
89:   'h' , /* 0x68 */ 
90:   'i' , /* 0x69 */ 
91:   'j' , /* 0x6A */ 
92:   'k' , /* 0x6B */ 
93:   'l' , /* 0x6C */ 
94:   'm' , /* 0x6D */ 
95:   'n' , /* 0x6E */ 
96:   'o' , /* 0x6F */ 
97:   'p' , /* 0x70 */ 
98:   'q' , /* 0x71 */ 
99:   'r' , /* 0x72 */ 
100:   's' , /* 0x73 */ 
101:   't' , /* 0x74 */ 
102:   'u' , /* 0x75 */ 
103:   'v' , /* 0x76 */ 
104:   'w' , /* 0x77 */ 
105:   'x' , /* 0x78 */ 
106:   'y' , /* 0x79 */ 
107:   'z' , /* 0x7A */ 
108:   '{' , /* 0x7B */ 
109:   '|' , /* 0x7C */ 
110:   '}' , /* 0x7D */ 
111:   '~' /* 0x7E */
112: };
113: /* 
114:  * エンコードした印字可能なアスキー文字です(0x20番から0x7E番まで). 
115:  */
116: static const char* 
117: STRINGS_OF_URL_ENCODED_PRINTABLE_ASCII[PRINTABLE_ASCII_TABLE_SIZE] =
118: {
119:   "+" , /*   */ 
120:   "%21" , /* ! */ 
121:   "%22" , /*   */ 
122:   "%23" , /* # */ 
123:   "%24" , /* $ */ 
124:   "%25" , /* % */ 
125:   "%26" , /* & */ 
126:   "%27" , /* ' */ 
127:   "%28" , /* ( */ 
128:   "%29" , /* ) */ 
129:   "%2A" , /* * */ 
130:   "%2B" , /* + */ 
131:   "%2C" , /* , */ 
132:   "-" , /* - */ 
133:   "." , /* . */ 
134:   "%2F" , /* / */ 
135:   "%30" , /* 0 */ 
136:   "%31" , /* 1 */ 
137:   "%32" , /* 2 */ 
138:   "%33" , /* 3 */ 
139:   "%34" , /* 4 */ 
140:   "%35" , /* 5 */ 
141:   "%36" , /* 6 */ 
142:   "%37" , /* 7 */ 
143:   "%38" , /* 8 */ 
144:   "%39" , /* 9 */ 
145:   "%3A" , /* : */ 
146:   "%3B" , /* ; */ 
147:   "%3C" , /* < */ 
148:   "%3D" , /* = */ 
149:   "%3E" , /* > */ 
150:   "%3F" , /* ? */ 
151:   "%40" , /* @ */ 
152:   "%41" , /* A */ 
153:   "%42" , /* B */ 
154:   "%43" , /* C */ 
155:   "%44" , /* D */ 
156:   "%45" , /* E */ 
157:   "%46" , /* F */ 
158:   "%47" , /* G */ 
159:   "%48" , /* H */ 
160:   "%49" , /* I */ 
161:   "%4A" , /* J */ 
162:   "%4B" , /* K */ 
163:   "%4C" , /* L */ 
164:   "%4D" , /* M */ 
165:   "%4E" , /* N */ 
166:   "%4F" , /* O */ 
167:   "%50" , /* P */ 
168:   "%51" , /* Q */ 
169:   "%52" , /* R */ 
170:   "%53" , /* S */ 
171:   "%54" , /* T */ 
172:   "%55" , /* U */ 
173:   "%56" , /* V */ 
174:   "%57" , /* W */ 
175:   "%58" , /* X */ 
176:   "%59" , /* Y */ 
177:   "%5A" , /* Z */ 
178:   "%5B" , /* [ */ 
179:   "%5C" , /* \ */ 
180:   "%5D" , /* ] */ 
181:   "%5E" , /* ^ */ 
182:   "_" , /* _ */ 
183:   "%60" , /* ` */ 
184:   "%61" , /* a */ 
185:   "%62" , /* b */ 
186:   "%63" , /* c */ 
187:   "%64" , /* d */ 
188:   "%65" , /* e */ 
189:   "%66" , /* f */ 
190:   "%67" , /* g */ 
191:   "%68" , /* h */ 
192:   "%69" , /* i */ 
193:   "%6A" , /* j */ 
194:   "%6B" , /* k */ 
195:   "%6C" , /* l */ 
196:   "%6D" , /* m */ 
197:   "%6E" , /* n */ 
198:   "%6F" , /* o */ 
199:   "%70" , /* p */ 
200:   "%71" , /* q */ 
201:   "%72" , /* r */ 
202:   "%73" , /* s */ 
203:   "%74" , /* t */ 
204:   "%75" , /* u */ 
205:   "%76" , /* v */ 
206:   "%77" , /* w */ 
207:   "%78" , /* x */ 
208:   "%79" , /* y */ 
209:   "%7A" , /* z */ 
210:   "%7B" , /* { */ 
211:   "%7C" , /* | */ 
212:   "%7D" , /* } */ 
213:   "%7E"     /* ~ */ 
214: };
215: /*----------------------------------------------------------------------------
216:  * 外部関数
217:  *--------------------------------------------------------------------------*/
218: int url_encode(char*,size_t,const char*,size_t);
219: int url_decode(char*,size_t,const char*,size_t,char**);
220: /*----------------------------------------------------------------------------
221:  * 関数
222:  *--------------------------------------------------------------------------*/
223: static int test_regular_case();
224: static int test_printable_ascii_case();
225: static int test_irregular_case();
226: static int test_euc_jp_case();
227: /*-----------------------------------------------------------------------------
228:  * メイン関数
229:  *---------------------------------------------------------------------------*/
230: int 
231: main(int argc,char* argv[])
232: {
233:   if(test_regular_case() && test_irregular_case())
234:   {
235:     printf("test ok\n");
236:   }
237:   exit(EXIT_SUCCESS);
238: }
239: /*
240:  * 正常な文字列を与えた場合,正常にエンコードされることをチェックします.
241:  */
242: static int 
243: test_regular_case()
244: {
245:   return test_printable_ascii_case() && test_euc_jp_case();
246: }
247: /*
248:  * 印字可能なアスキー文字(0x20から0x7E)に付いてテストします.
249:  * 英数字とピリオド,アンダーバー,マイナスはそのまま,
250:  * 空白はプラスに,
251:  * 他の記号は"%16進数表記"に変換(エンコード/デコード)されることをチェックします.
252:  */
253: static int 
254: test_printable_ascii_case()
255: {
256:   /*--- 局所変数 -----------------------------------------------------------*/
257:   char data_original[PRINTABLE_ASCII_TABLE_SIZE + 1];
258:   int idx_original;
259:   char data_encoded[PRINTABLE_ASCII_TABLE_SIZE * 3 + 1];
260:   int idx_encoded;
261:   char data_current[PRINTABLE_ASCII_TABLE_SIZE * 3 + 1];
262:   int idx_current;
263:   char* end_ptr;
264:   int return_value;
265:   /*--- 初期化 -------------------------------------------------------------*/
266:   data_original[0] = '\0';
267:   for(idx_original = 0 ; 
268:     idx_original < PRINTABLE_ASCII_TABLE_SIZE ; 
269:     idx_original++)
270:   {
271:     data_original[idx_original] = CHARS_OF_PRINTABLE_ASCII[idx_original];
272:   }
273:   data_original[PRINTABLE_ASCII_TABLE_SIZE] = '\0';
274:   data_encoded[0] = '\0';
275:   for(idx_encoded = 0 ; 
276:     idx_encoded < PRINTABLE_ASCII_TABLE_SIZE ; 
277:     idx_encoded++)
278:   {
279:     strcat(data_encoded,
280:            STRINGS_OF_URL_ENCODED_PRINTABLE_ASCII[idx_encoded]);
281:   }
282:   /*--- 本体1 : エンコード -------------------------------------------------*/
283:   return_value = url_encode(data_current,
284:                           PRINTABLE_ASCII_TABLE_SIZE * 3 + 1,
285:                           data_original,
286:                           PRINTABLE_ASCII_TABLE_SIZE + 1);
287:   if(return_value != PRINTABLE_ASCII_TABLE_SIZE)
288:   {
289:     fprintf(stderr,
290:             "unexpected return value : %d (%d)\n",
291:             return_value,
292:             PRINTABLE_ASCII_TABLE_SIZE);
293:     exit(EXIT_FAILURE);
294:   }
295:   for(idx_current = 0 ; 
296:     data_encoded[idx_current] != '\0';
297:     idx_current++)
298:   {
299:     if(data_current[idx_current] != data_encoded[idx_current])
300:       {
301:         fprintf(stderr,"misencode at index : %d\n",idx_current);
302:         fprintf(stderr,"original data : \n%s\n",data_original);
303:         fprintf(stderr,"expected encoded data : \n%s\n",data_encoded);
304:         fprintf(stderr,"actual encoded  data : \n%s\n",data_current);
305:         exit(EXIT_FAILURE);
306:       }
307:   }
308:   /*--- 本体2 : デコード ---------------------------------------------------*/
309:   return_value = url_decode(data_current,
310:                           PRINTABLE_ASCII_TABLE_SIZE * 3 + 1,
311:                           data_encoded,
312:                           PRINTABLE_ASCII_TABLE_SIZE * 3 + 1,
313:                           &end_ptr);
314:   if(return_value != PRINTABLE_ASCII_TABLE_SIZE)
315:   {
316:     fprintf(stderr,
317:             "unexpected return value : %d (%d)\n",
318:             return_value,
319:             PRINTABLE_ASCII_TABLE_SIZE);
320:     exit(EXIT_FAILURE);
321:   }
322:   if(end_ptr != NULL)
323:   {
324:     if(*end_ptr != '\0')
325:       {
326:         fprintf(stderr,"end pointer is not set\n");
327:         exit(EXIT_FAILURE);
328:       }
329:   }
330:   for(idx_current = 0 ; 
331:     data_original[idx_current] != '\0';
332:     idx_current++)
333:   {
334:     if(data_current[idx_current] != data_original[idx_current])
335:       {
336:         fprintf(stderr,"misdecode at index : %d\n",idx_current);
337:         fprintf(stderr,"original data : \n%s\n",data_encoded);
338:         fprintf(stderr,"expected decoded  data : \n%s\n",data_original);
339:         fprintf(stderr,"actual decoded  data : \n%s\n",data_current);
340:         exit(EXIT_FAILURE);
341:       }
342:   }
343:   return 1;
344: }
345: 
346: /*
347:  * url_decode()のエラー処理が正しく行われているかテストする関数です.
348:  * url_decode()に対し,わざと不正な文字列("%20%G1%20")をデコードさせて,
349:  * 返値とデコードに失敗した文字へのポインタを検査します.
350:  */
351: static int
352: test_irregular_case()
353: {
354:   /*--- 局所変数 -----------------------------------------------------------*/
355:   char* end_ptr;
356:   /* "%20%G1%20" の間の%G1は不適正なデータ. */
357:   char data_irregular[] = "%20%G1%20";
358:   char data_decoded[32];
359:   int return_value;
360:   /*--- 本体1 : デコード ---------------------------------------------------*/
361:   return_value = url_decode(data_decoded,
362:                           sizeof(data_decoded),
363:                           data_irregular,
364:                           sizeof(data_irregular),
365:                           &end_ptr);
366:   /*--- 本体2 : 検証 -------------------------------------------------------*/
367:   /* はじめの1文字だけデコードされる. */
368:   if(return_value != 1)
369:   {
370:     fprintf(stderr,"unexpected return value\n");
371:     exit(EXIT_FAILURE);
372:   }
373:   /* 不適正なデータを指していることが期待される. */
374:   if(end_ptr - data_irregular != 4)
375:   {
376:     fprintf(stderr,"unexpected end pointer\n");
377:     exit(EXIT_FAILURE);
378:   }
379:   return 1;
380: }
381: /*
382:  * ISO-2022-JP文字セットの日本語EUCコードの文字について,
383:  * URLエンコードとデコードをテストします.
384:  * 日本語EUCの文字は,
385:  * 上位バイト 0xA1 から 0xF3 に関しては下位バイトは 0xA1 から 0xFE まで,
386:  * 上位バイト 0xF4 に関しては下位バイトは 0xA1 から 0xA6 までです.
387:  * これらは "%上位バイトの16進表記%下位バイトの16進表記" でエンコードされます.
388:  */
389: static int 
390: test_euc_jp_case()
391: {
392:   unsigned char cur_lower;
393:   unsigned char cur_upper;
394:   char original[3];
395:   char encoded[7];
396:   char actual[7];
397:   for(cur_upper = 0xA1 ; cur_upper <= 0xF4 ; cur_upper++)
398:   {
399:     for(cur_lower = 0xA1 ; 
400:         (cur_upper < 0xF4  && cur_lower <= 0xFE) || 
401:         (cur_upper == 0xF4 && cur_lower <= 0xA6) ;
402:         cur_lower++)
403:       {
404:         original[0] = cur_upper;
405:         original[1] = cur_lower;
406:         original[2] = '\0';
407:         sprintf(encoded,"%%%2X%%%2X",cur_upper,cur_lower);
408:         memset(actual,'\0',7);
409:         if(url_encode(actual,7,original,2) != 2)
410:           {
411:             fprintf(stderr,"expected encode 2 chars\n");
412:             exit(EXIT_FAILURE);
413:           }
414:         if(strcmp(encoded,actual) != 0)
415:           {
416:             fprintf(stderr,"misencode \n");
417:             fprintf(stderr,"original : \n%s\n",original);
418:             fprintf(stderr,"expected encoded : \n%s\n",encoded);
419:             fprintf(stderr,"actual : \n%s\n",actual);
420:           }
421:         if(url_decode(actual,7,encoded,7,NULL) != 2)
422:           {
423:             fprintf(stderr,"expected decode 2 chars\n");
424:             exit(EXIT_FAILURE);             
425:           }
426:         if(strcmp(original,actual) != 0)
427:           {
428:             fprintf(stderr,"misdecode \n");
429:             fprintf(stderr,"original : \n%s\n",encoded);
430:             fprintf(stderr,"expected decoded : \n%s\n",original);
431:             fprintf(stderr,"actual : \n%s\n",actual);
432:           }
433:       }
434:   }
435:   return 1;
436: }

D.3.5 プログラムの使い方

URLエンコードおよびデコードを行う関数は、 x_www_form_url_translator.c に本体が書かれています。 このコンパイルした中間ファイルをリンクすれば使うことができます。 ANSIに準拠したライブラリ関数しか用いていないため、 ANSIに準拠したC言語のコンパイラならば正しく動作するはずです。

また、 url_encode()とurl_decode()をより効率の良い方法で書き直した場合、 url_translator_test.c をテストコードとして使用することができます。 このテストコードはエンコードした内容だけでなく、 エラー処理のために返値や変換を中断したポインタも検査します。

C言語で作るCGI入門の目次へ
ホームページへ