各種言語におけるINIファイルのエラー扱い

INIファイルを間違って編集したことで、システム障害に至るケースに遭遇したので、これを期に自分が仕事で良く使っている言語におけるINIファイルのエラーの扱い方についてまとめた。
なお、現在INIファイルは https://ja.wikipedia.org/wiki/INI%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB によれば、規格化／標準化されていないらしい。

# 調査対象言語

Windows上で以下の言語とWin32 APIについて調査した。

|言語とバージョン            |ライブラリや関数       |
|----------------------------|-----------------------|
|PHP 7.4.22 (x64)            |parse_ini_file         |
|Strawberry Perl 5.32.1 (x64)|Config::Tiny           |
|Python 3.7.9 (x86)          |ConfigParser           |
|Ruby 2.7.2 (x64)            |IniFile                |
|kernel32.dll 10.0.19041.1151|GetPrivateProfileString|

# スクリプト

## PHP

``` test.php
<?php
if (count($argv) == 1) {
    printf("usage: %s <inifile>\r\n", $argv[0]);
    exit();
}
$ini = parse_ini_file($argv[1], true);
printf("%s\r\n", $ini['section']['key']);
?>
```

## Perl

``` test.pl
use Config::Tiny;
if ($#ARGV < 0) {
    print "usage: $0 <inifile>\n";
    exit;
}
$ini = Config::Tiny->read($ARGV[0]);
if (!defined($ini)) {
    print "undef\n";
    exit;
}
printf("%s\n", $ini->{'section'}->{'key'});
```

## Python

``` test.py
import configparser
import sys

if len(sys.argv) == 1:
    print("usage: %s <inifile>" % (sys.argv[0]))
    sys.exit()

ini = configparser.ConfigParser()
ini.read(sys.argv[1])
print(ini['section']['key'])
```

## Ruby

``` test.rb
require 'inifile'
if ARGV.length == 0
    puts "usage: #{$0} <inifile>"
    exit
end
ini = IniFile.load(ARGV[0])
puts ini['section']['key']
```

## Win32 API

コンパイルするのが面倒なので、PerlのWin32::APIを使用した。

``` testw.pl
use Win32::API;
if ($#ARGV < 0) {
	print "usage: $0 <inifile>\n";
	exit;
}
Win32::API->Import('kernel32',
	'DWORD GetPrivateProfileString(
			LPCTSTR appName, LPCTSTR keyName, LPCTSTR default,
			LPTSTR returned, DWORD size, LPCTSTR fname)');
$fname = $ARGV[0];
$fname = ".\\$fname" if index($fname, '\\') < 0;
$value = ' ' x 100;
GetPrivateProfileString('section', 'key', '', $value, length($value), $fname);
$value = substr($value, 0, index($value, "\0"));
print "$value\n";
```

# 調査内容

以下の不正行とはコメント、セクション、キーと値のいずれでもない行のことである。

## ケース1：ファイル先頭のセクション前の不正行

``` case1.ini
hoge
[section]
key=hello world
```

## ケース2：セクション内の不正行

``` case2.ini
[section]
hoge
key=hello world
```

## ケース3：キーと値を "==" で結合

``` case3.ini
[section]
key==hello world
```

## ケース4："#" によるコメント

``` case4.ini
#hoge
[section]
key=hello world
```

## ケース5：値の後に ";" によるコメント

``` case5.ini
[section]
key="hello world" ;hoge
```

## ケース6：値の後に "#" によるコメント

``` case6.ini
[section]
key="hello world" #hoge
```

# 調査結果

| 言語 | c1 | c2 | c3 | c4 | c5 | c6 |
|------|:--:|:--:|:--:|:--:|:--:|:--:|
|PHP   | o  | o  | x  | o  | o  | x  |
|Perl  | x  | x  | o  | o  | x  | x  |
|Python| x  | x  | o  | o  | x  | x  |
|Ruby  | x  | x  | o  | o  | o  | o  |
|API   | o  | o  | o  | o  | x  | x  |

- ケース1/2が○の場合、不正行は無視されて、値は「hello world」となった
- ケース3が○の場合、値は「=hello world」となった
- ケース4が○の場合、値は「hello world」となった
- ケース5/6が○の場合、値は「hello world」となった
- ケース5が×の場合、値は「"hello world" ;hoge」となった
- ケース6が×の場合、値はPHPが「hello world#hoge」、それ以外は「"hello world" #hoge」となった

# 総評

冒頭に記載した通り、ある設定行で "=" を "==" と間違って編集したため、他の大切な設定が読めなくなり、システム障害に至ったので、感情的になっているかも知れないが、以下のように総評する。
Win32 API以外の各種言語でINIファイルを扱う場合は特に、初期設定から変更しないものとそれ以外でINIファイルを分けるなど、対策を講じるように設計していただきたい。

- PHPは他の言語と真逆で、キーと値を "==" で結合するとsyntax errorとなった（腑に落ちない結果）
- Perl / Python / Rubyでは不正行はparse errorとなった（納得できる結果）
- Win32 APIでは不正行を無視してくれた（慣れ親しんだ結果）
- 値にコメントを含めない方が無難

以上