Not only is the Internet dead, it's starting to smell really bad.:2021年01月分

2021/01/12(Tue)

[プログラミング幻語C][オレオレN6] tzfile.hがいつの間やら*BSDとglibcで移植性がなくなってしまった件

家庭の事情により長いこと放置してたオレオレN6だけど、最後に作業してた OpenSSL周りの更新作業はとりあえず 1.0.2-stable相当にアプデしてEOL後に出てる

も対策したので、これで当面1.1.1系への更新がサボれるかなと。 まぁ外向きのサービスは全てpkgsrcのopenssl-1.1.1をリンクしとったし別に永遠に放置でもよかったのだが(どうせユーザー1名)。

お次はtzdataがN6のEOL時の2018dなままなので最新の2020fに更新かける作業をせんとならんとなぁという。 ちなみにTNF本家のHEADではファイルの場所がsrc/share/zoneinfoからsrc/external/public-domain/tz以下に変更になってるんだけど、src/lib/libc/time以下の整理には未着手のようやね。 あれzic(1)とかzdump(1)のソースまであそこに展開されてるのでまったくもって美しくないのだが、なんであそこに放り込んだのかな。

ところで本題のtzfile.hなのだけど、こいつ自身4.3BSD-Tahoeで導入された当初からちょっと設計が変なのである。

こいつはヘッダ名の通りlibcやzic(1)なんかがTZifファイルの読み書きするための機能を定義するんだけど、それとは無関係な定数が大量に存在してるのだ。

--- tz/tzfile.h 2021-01-11 14:04:16.000000000 +0900
+++ src/include/tzfile.h        2019-07-04 00:49:21.000000000 +0900
@@ -120,4 +119,56 @@ struct tzhead {
 #define TZ_MAX_LEAPS   50      /* Maximum number of leap second corrections */
 #endif /* !defined TZ_MAX_LEAPS */

-#endif /* !defined TZFILE_H */
+#define SECSPERMIN	60
+#define MINSPERHOUR	60
+#define HOURSPERDAY	24
+#define DAYSPERWEEK	7
+#define DAYSPERNYEAR	365
+#define DAYSPERLYEAR	366
+#define SECSPERHOUR	(SECSPERMIN * MINSPERHOUR)
+#define SECSPERDAY	((int_fast32_t) SECSPERHOUR * HOURSPERDAY)
+#define MONSPERYEAR	12
+
+#define TM_SUNDAY	0
+#define TM_MONDAY	1
+#define TM_TUESDAY	2
+#define TM_WEDNESDAY	3
+#define TM_THURSDAY	4
+#define TM_FRIDAY	5
+#define TM_SATURDAY	6
+
+#define TM_JANUARY	0
+#define TM_FEBRUARY	1
+#define TM_MARCH	2
+#define TM_APRIL	3
+#define TM_MAY		4
+#define TM_JUNE		5
+#define TM_JULY		6
+#define TM_AUGUST	7
+#define TM_SEPTEMBER	8
+#define TM_OCTOBER	9
+#define TM_NOVEMBER	10
+#define TM_DECEMBER	11
+
+#define TM_YEAR_BASE	1900
+
+#define EPOCH_YEAR	1970
+#define EPOCH_WDAY	TM_THURSDAY
+
+#define isleap(y) (((y) % 4) == 0 && (((y) % 100) != 0 || ((y) % 400) == 0))
+
+/*
+** Since everything in isleap is modulo 400 (or a factor of 400), we know that
+**     isleap(y) == isleap(y % 400)
+** and so
+**     isleap(a + b) == isleap((a + b) % 400)
+** or
+**     isleap(a + b) == isleap(a % 400 + b % 400)
+** This is true even if % means modulo rather than Fortran remainder
+** (which is allowed by C89 but not C99).
+** We use this to avoid addition overflow problems.
+*/
+
+#define isleap_sum(a, b)	isleap((a) % 400 + (b) % 400)
+
+#endif /* !defined _TZFILE_H_ */

こいつらは暦の計算に必要な定数であって、TZifファイルとは無関係でも使いたくなるシロモノである(まぁグレゴリオ暦しばりだけどさ)。

なもんでtzdataとは本来は無関係な

までもがtzfile.hをインクルードしてこれらの定数使っちゃってるんだよね、これ本来ならtime.hあたりが用意しておくべきもんだよな(まぁこれはC/POSIXの欠陥ですな)。

にもかかわらず、tzdataの現在のメンテナはパラノイアなのか(まぁtzdataのメンテに興味持つ時点でその気はあるだろう)、設計として美しくないムキーってことでこれらの定数を tzfile.hからprivate.hに移しちゃったんだよね。そんでprivate.hにはご丁寧に

/*
** This header is for use ONLY with the time conversion code.
** There is no guarantee that it will remain unchanged,
** or that it will remain at all.
** Do NOT copy it to any system include directory.
** Thank you!
*/

とまで入れて使うなよ!使うなよ!と念を押す始末。

ちなみにglibcではこの変更にきちんと 追従してしまったので、もはやこれらのマクロを使ったコードには移植性は無くなってしまったのだ。

よって移植性のあるコード書きたければ(そもそもtzfile.h自体がISO-C/POSIXといった規格には無いけどね)

#include <tzfile.h>
#ifndef SECSPERMIN
#define SECSPERMIN	60
#endif
…

といちいち再定義するかあるいは

#include <tzfile.h>
#include "private.h"
...

とする必要がある(ただし後者はコード読めば察するだろうが、インクルードするだけで暦の計算とは無関係な移植性のための黒魔術が多数発動するのであまりお勧めしない)。

さあ困った、確かにこれらのマクロはtzfile.hとは切り離すのが設計として正しいのには同意するけど、30年以上存在したものを今更消すかね普通…という気分。

あとこのメンテナさんだけど、private.hに移すついでに

- #define SECSPERDAY	((long) SECSPERHOUR * HOURSPERDAY)
+ #define SECSPERDAY	((int_fast32_t) SECSPERHOUR * HOURSPERDAY)

みたいな書換やってるんだけど、変更の意図としてこれ86400秒がlong型が16bitで溢れてしまう環境を想定してるのだろうか。 でもそれってそもそもISO-C規格違反な環境だから、C99のint_fast32_tがあると考えるのも頭お花畑過ぎねえかな…