filter_varの検証フィルタ「FILTER_VALIDATE_DOMAIN」の動作を確認するためにphptファイルにテストパターンを追加して実行してみた

### この記事を書くきっかけ

2022年9月24日、25日に開催される「[PHP Conference Japan 2022](https://phpcon.php.gr.jp/2022/)」で「[FQDN（ドメイン名）のバリデーションが意外と面倒だった](https://fortee.jp/phpcon-2022/proposal/fbbbf348-e535-4537-896e-ad771e46759b)」というタイトルで登壇予定なんですが、登壇スライドを作成している途中に調査したphptについてはLT（4分）の枠内に収められそうにないので、こちらでまとめておこうと思った次第です。

### 動作確認環境
- PC: MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports)
- OS: macOS Monterey 12.0.1
- CPU: 2.3 GHz クアッドコア Intel Core i7
- Memory: 32 GB

### 事前準備

任意のディレクトリにPHP 8.1.10（2022年9月8日時点の最新版）のソースをダウンロード。

```
% curl -LO https://www.php.net/distributions/php-8.1.10.tar.gz
```

展開したディレクトリへ移動。

```
% tar zxf php-8.1.10.tar.gz
% cd php-8.1.10
```

`libiconv` を未インストールの状態で `./configure` を実行すると以下のようなエラーが発生する。

```
% ./configure 
・
・
configure: error: Please specify the install prefix of iconv with --with-iconv=<DIR>
```

`libiconv` をインストールする場合は以下のように実行する。

```
% brew install libiconv
% ./configure --with-iconv=/usr/local/opt/libiconv
```

`libiconv` をインストールしない場合は以下のように実行する。

```
% ./configure --without-iconv
```

### filter_varの「FILTER_VALIDATE_DOMAIN」に対応するphptファイルを探す

`grep` してみると、どうやら `ext/filter/tests/056.phpt` が目的のファイルと思われる。

```
% grep 'FILTER_VALIDATE_DOMAIN' * -r |grep phpt
ext/filter/tests/056.phpt:filter_var() and FILTER_VALIDATE_DOMAIN
ext/filter/tests/056.phpt:    var_dump(filter_var($value, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME));
ext/filter/tests/056.phpt:var_dump(filter_var('_example.com', FILTER_VALIDATE_DOMAIN));
ext/filter/tests/056.phpt:var_dump(filter_var('_example.com', FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME));
ext/filter/tests/056.phpt:var_dump(filter_var('test_.example.com', FILTER_VALIDATE_DOMAIN));
ext/filter/tests/056.phpt:var_dump(filter_var('test_.example.com', FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME));
ext/filter/tests/056.phpt:var_dump(filter_var('te_st.example.com', FILTER_VALIDATE_DOMAIN));
ext/filter/tests/056.phpt:var_dump(filter_var('te_st.example.com', FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME));
ext/filter/tests/056.phpt:var_dump(filter_var('test._example.com', FILTER_VALIDATE_DOMAIN));
ext/filter/tests/056.phpt:var_dump(filter_var('test._example.com', FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME));
ext/filter/tests/filter_null_on_failure.phpt:var_dump(filter_var(".invalid", FILTER_VALIDATE_DOMAIN, FILTER_NULL_ON_FAILURE));
```

ファイルの中身を確認すると、それっぽい内容が書かれている。

```:ext/filter/tests/056.phpt
% cat ext/filter/tests/056.phpt
--TEST--
filter_var() and FILTER_VALIDATE_DOMAIN
--EXTENSIONS--
filter
--FILE--
<?php

$values = Array(
'example.com',
'www.thelongestdomainnameintheworldandthensomeandthensomemoreandmore.com',
'toolongtoolongtoolongtoolongtoolongtoolongtoolongtoolongtoolongtoolong.com',
'eauBcFReEmjLcoZwI0RuONNnwU4H9r151juCaqTI5VeIP5jcYIqhx1lh5vV00l2rTs6y7hOp7rYw42QZiq6VIzjcYrRm8gFRMk9U9Wi1grL8Mr5kLVloYLthHgyA94QK3SaXCATklxgo6XvcbXIqAGG7U0KxTr8hJJU1p2ZQ2mXHmp4DhYP8N9SRuEKzaCPcSIcW7uj21jZqBigsLsNAXEzU8SPXZjmVQVtwQATPWeWyGW4GuJhjP4Q8o0.com',
'kDTvHt1PPDgX5EiP2MwiXjcoWNOhhTuOVAUWJ3TmpBYCC9QoJV114LMYrV3Zl58.kDTvHt1PPDgX5EiP2MwiXjcoWNOhhTuOVAUWJ3TmpBYCC9QoJV114LMYrV3Zl58.kDTvHt1PPDgX5EiP2MwiXjcoWNOhhTuOVAUWJ3TmpBYCC9QoJV114LMYrV3Zl58.CQ1oT5Uq3jJt6Uhy3VH9u3Gi5YhfZCvZVKgLlaXNFhVKB1zJxvunR7SJa.com.',
'cont-ains.h-yph-en-s.com',
'..com',
'ab..cc.dd',
'a.-bc.com',
'ab.cd-.com',
'-.abc.com',
'abc.-.abc.com',
'underscore_.example.com',
'',
-1,
array(),
'\r\n',
);
foreach ($values as $value) {
    var_dump(filter_var($value, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME));
}

var_dump(filter_var('_example.com', FILTER_VALIDATE_DOMAIN));
var_dump(filter_var('_example.com', FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME));
var_dump(filter_var('test_.example.com', FILTER_VALIDATE_DOMAIN));
var_dump(filter_var('test_.example.com', FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME));
var_dump(filter_var('te_st.example.com', FILTER_VALIDATE_DOMAIN));
var_dump(filter_var('te_st.example.com', FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME));
var_dump(filter_var('test._example.com', FILTER_VALIDATE_DOMAIN));
var_dump(filter_var('test._example.com', FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME));

echo "Done\n";
?>
--EXPECT--
string(11) "example.com"
string(71) "www.thelongestdomainnameintheworldandthensomeandthensomemoreandmore.com"
bool(false)
bool(false)
string(254) "kDTvHt1PPDgX5EiP2MwiXjcoWNOhhTuOVAUWJ3TmpBYCC9QoJV114LMYrV3Zl58.kDTvHt1PPDgX5EiP2MwiXjcoWNOhhTuOVAUWJ3TmpBYCC9QoJV114LMYrV3Zl58.kDTvHt1PPDgX5EiP2MwiXjcoWNOhhTuOVAUWJ3TmpBYCC9QoJV114LMYrV3Zl58.CQ1oT5Uq3jJt6Uhy3VH9u3Gi5YhfZCvZVKgLlaXNFhVKB1zJxvunR7SJa.com."
string(24) "cont-ains.h-yph-en-s.com"
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
string(12) "_example.com"
bool(false)
string(17) "test_.example.com"
bool(false)
string(17) "te_st.example.com"
bool(false)
string(17) "test._example.com"
bool(false)
Done
```

### 追加したいテストパターンを洗い出す

- ホスト名のみ
    - www
    - localhost
- ホスト名 + ドメイン名
    - www.example.com
- ドメイン名のみ（ネイキッドドメイン（Zone Apex））
    - example.com（※既存のテストパターンに記述済みなので追加不要）
- ホスト名 + ドメイン名が253文字以下
    - wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.www.tsunagi.me
    - w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.tsunagi.me
- ホスト名 + ドメイン名が254文字以上
    - wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwww.tsunagi.me
    - w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.tsunagi.me
- ラベルが63文字以下
    - wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.tsunagi.me
- ラベルが64文字以上
    - wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.tsunagi.me
- ラベル内に不正文字
    - www.example,com
    - www.example com
    - www.examp!e.com

### phptファイルにテストパターンを追加する

```
% cp ext/filter/tests/056.phpt ext/filter/tests/056.phpt.orig
% diff ext/filter/tests/056.phpt ext/filter/tests/056.phpt.orig
26,37d25
< 'www',
< 'localhost',
< 'www.example.com',
< 'wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.www.tsunagi.me',
< 'w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.tsunagi.me',
< 'wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwww.tsunagi.me',
< 'w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.tsunagi.me',
< 'wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.tsunagi.me',
< 'wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.tsunagi.me',
< 'www.example,com',
< 'www.example com',
< 'www.examp!e.com',
72,83d59
< string(3) "www"
< string(9) "localhost"
< string(15) "www.example.com"
< string(253) "wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.www.tsunagi.me"
< string(252) "w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.tsunagi.me"
< bool(false)
< bool(false)
< string(74) "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.tsunagi.me"
< bool(false)
< bool(false)
< bool(false)
< bool(false)
```

### テストを実行する

※初回はビルド（コンパイル）が行われているようなのでかなり時間がかかります。
`TESTS` にphptファイルを指定することで、特定のテストを実行することが可能です。
`Tests passed    :    1 (100.0%) (100.0%)` と表示されれば、追加したパターンも含めてすべてのテストが通っていることになります。

```
% make test TESTS='--show-diff ext/filter/tests/056.phpt'

Build complete.
Don't forget to run 'make test'.
・
・
・
=====================================================================
Running selected tests.
PASS filter_var() and FILTER_VALIDATE_DOMAIN [ext/filter/tests/056.phpt] 
=====================================================================
Number of tests :    1                 1
Tests skipped   :    0 (  0.0%) --------
Tests warned    :    0 (  0.0%) (  0.0%)
Tests failed    :    0 (  0.0%) (  0.0%)
Tests passed    :    1 (100.0%) (100.0%)
---------------------------------------------------------------------
Time taken      :    0 seconds
=====================================================================
```


`TESTS` に `--show-diff` を指定しておくことで、テストに失敗した際に以下のようにどこで失敗したのかがわかります。

```
% make test TESTS='--show-diff ext/filter/tests/056.phpt'

Build complete.
Don't forget to run 'make test'.
・
・
・
=====================================================================
Running selected tests.
TEST 1/1 [ext/filter/tests/056.phpt]
========DIFF========
--
     string(3) "www"
     string(9) "localhost"
     string(15) "www.example.com"
021+ string(253) "wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.www.tsunagi.me"
021- string(252) "wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.wwwwwwwww.www.tsunagi.me"
     string(252) "w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.w.tsunagi.me"
     bool(false)
     bool(false)
--
========DONE========
FAIL filter_var() and FILTER_VALIDATE_DOMAIN [ext/filter/tests/056.phpt] 
=====================================================================
Number of tests :    1                 1
Tests skipped   :    0 (  0.0%) --------
Tests warned    :    0 (  0.0%) (  0.0%)
Tests failed    :    1 (100.0%) (100.0%)
Tests passed    :    0 (  0.0%) (  0.0%)
---------------------------------------------------------------------
Time taken      :    0 seconds
=====================================================================

=====================================================================
FAILED TEST SUMMARY
---------------------------------------------------------------------
filter_var() and FILTER_VALIDATE_DOMAIN [ext/filter/tests/056.phpt]
=====================================================================
make: *** [test] Error 1
```

### 参考URL
- [PHP8の新機能をPHP内部コードのテストphptから読む](https://speakerdeck.com/hgsgtk/learn-php8-by-reading-phpt)
- [PHP extensionのmake testに関するTIPS](https://qiita.com/hnw/items/f70e52eaa065ae573668)