5. フォーム値の構造化とアクセス
前章では、GETやPOSTで送られてきたフォームデータを、文字列として受け取りました。
この章では、name=Alice&age=20 のような文字列を分解し、C言語で扱いやすい構造に変換します。フォーム値を構造体に格納しておくと、後の章で扱うDB挿入、認証、テンプレート表示でも同じ考え方を使えます。
この章で学ぶこと
application/x-www-form-urlencodedの基本形式&と=によるフォーム値の分解+と%XXのURLデコード- 構造体配列への格納
- 確保したメモリの解放
フォームデータの形式
HTMLフォームから送信された値は、標準的には次のような形式になります。
name=Alice&age=20&message=Hello+World%21
この形式では、次のルールで値が並びます。
- 項目同士は
&で区切る - キーと値は
=で区切る - 空白は
+で表す - 一部の記号や日本語は
%XX形式で表す
このままでは name の値だけを取り出す、といった処理が面倒です。そこで、キーと値のペアとして構造体に格納します。
構造体を定義する
まず、フォーム項目を表す構造体を定義します。
typedef struct {
char *key;
char *value;
} FormItem;
この章では、学習用として最大件数を固定した配列に格納します。
#define MAX_FORM_ITEMS 20
FormItem items[MAX_FORM_ITEMS];
フォーム項目の数が増える実用コードでは、動的配列やリストを検討します。ここでは、CGIでフォーム値を扱う基本の流れを優先します。
URLデコードする
フォーム値には、URLエンコードされた文字が含まれます。たとえば Hello+World%21 は Hello World! という意味です。
次の関数は、文字列をその場でURLデコードする簡易実装です。
#include <ctype.h>
#include <stdlib.h>
void url_decode(char *s) {
char *src = s;
char *dst = s;
while (*src != '\0') {
if (*src == '+') {
*dst++ = ' ';
src++;
} else if (
src[0] == '%' &&
isxdigit((unsigned char)src[1]) &&
isxdigit((unsigned char)src[2])
) {
char hex[3] = { src[1], src[2], '\0' };
*dst++ = (char)strtol(hex, NULL, 16);
src += 3;
} else {
*dst++ = *src++;
}
}
*dst = '\0';
}
この実装は、入力文字列を書き換えます。そのため、環境変数から得た文字列を直接渡すのではなく、コピーしてから使います。
クエリ文字列を分解する
次に、key=value&key2=value2 形式の文字列を分解して、FormItem 配列に格納します。
#include <stdlib.h>
#include <string.h>
int parse_form(char *input, FormItem *items, int max_items) {
int count = 0;
char *pair = strtok(input, "&");
while (pair != NULL && count < max_items) {
char *equal = strchr(pair, '=');
if (equal != NULL) {
*equal = '\0';
items[count].key = strdup(pair);
items[count].value = strdup(equal + 1);
if (items[count].key == NULL || items[count].value == NULL) {
free(items[count].key);
free(items[count].value);
break;
}
url_decode(items[count].key);
url_decode(items[count].value);
count++;
}
pair = strtok(NULL, "&");
}
return count;
}
strtok() は区切り文字の位置を '\0' に書き換えます。そのため、この関数に渡す input も書き換えられます。
値を取り出す
フォーム項目が構造体配列に入ると、キー名で値を探せます。
const char *form_get(FormItem *items, int count, const char *key) {
for (int i = 0; i < count; i++) {
if (strcmp(items[i].key, key) == 0) {
return items[i].value;
}
}
return NULL;
}
使用例です。
const char *name = form_get(items, count, "name");
if (name != NULL) {
printf("name = %s\n", name);
}
メモリを解放する
parse_form() では、strdup() によってキーと値の文字列を確保しています。使い終わったら解放します。
void free_form_items(FormItem *items, int count) {
for (int i = 0; i < count; i++) {
free(items[i].key);
free(items[i].value);
items[i].key = NULL;
items[i].value = NULL;
}
}
CGIはリクエストごとにプロセスが終了する構成も多いですが、C言語の教材としては、確保したメモリを明示的に解放する習慣をつけておく方がよいです。
GETで使う例
GETパラメータを解析する例です。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_FORM_ITEMS 20
int main(void) {
FormItem items[MAX_FORM_ITEMS];
const char *query = getenv("QUERY_STRING");
printf("Content-Type: text/plain; charset=UTF-8\r\n\r\n");
if (query == NULL || query[0] == '\0') {
printf("No query string.\n");
return 0;
}
char *copy = strdup(query);
if (copy == NULL) {
printf("Memory allocation failed.\n");
return 1;
}
int count = parse_form(copy, items, MAX_FORM_ITEMS);
const char *name = form_get(items, count, "name");
if (name != NULL) {
printf("name = %s\n", name);
} else {
printf("name is not found.\n");
}
free_form_items(items, count);
free(copy);
return 0;
}
注意点
この章の実装は、フォームデータを扱うための最小例です。実用コードでは、次のような点を追加で考える必要があります。
- 最大入力サイズを制限する
- 同じキーが複数回出てきた場合の扱いを決める
=を含まない項目の扱いを決める- 文字コードを統一する
- HTMLに出力する前にエスケープする
- SQLに渡す場合はプレースホルダやエスケープを使う
この章では「受け取った文字列を構造化する」ことに集中します。安全な出力については次章以降で扱います。
小まとめ
- フォームデータは
key=value&key2=value2形式で送られます。 +や%XXはURLデコードが必要です。- 構造体に格納すると、キー名で値を扱いやすくなります。
strdup()で確保した文字列はfree()で解放します。
次章では、受け取った値をHTMLに埋め込み、レスポンスとして返す方法を扱います。