1 /* Test that valid POSIX timezone strings are correctly parsed by tzset(3). */
2 #include <stdio.h>
3 #include <stdlib.h>
4 #include <stdint.h>
5
6 // BEGIN test vectors
7 #include <time.h>
8 #include <limits.h>
9
10 #define IN_SECONDS(h, m, s) ((int32_t) (h) * 3600 + (m) * 60 + (s))
11 #define NO_TIME INT_MIN
12
13 struct tz_test {
14 const char* tzstr;
15 int32_t offset_seconds;
16 int32_t dst_offset_seconds;
17 };
18
19 extern struct tm winter_tm;
20 extern struct tm summer_tm;
21 extern const time_t winter_time;
22 extern const time_t summer_time;
23 extern struct tz_test test_timezones[];
24
25 // winter time is March, 21st 2022 at 8:15pm and 20 seconds
26 struct tm winter_tm = {
27 .tm_sec = 20,
28 .tm_min = 15,
29 .tm_hour = 20,
30 .tm_mday = 21,
31 .tm_mon = 3 - 1,
32 .tm_year = 2022 - 1900,
33 .tm_isdst = 0
34 };
35
36 // summer time is July, 15th 2022 at 10:50am and 40 seconds
37 struct tm summer_tm = {
38 .tm_sec = 40,
39 .tm_min = 50,
40 .tm_hour = 10,
41 .tm_mday = 15,
42 .tm_mon = 7 - 1,
43 .tm_year = 2022 - 1900,
44 .tm_isdst = 1
45 };
46
47 // UTC unix time for the winter time
48 const time_t winter_time = 1647893720;
49 const time_t summer_time = 1657882240;
50
51 struct tz_test test_timezones[] = {
52 /*
53 * creating test vectors based on the POSIX spec (https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html#tag_08_03)
54 */
55 // normal std names
56 {"MAR1", IN_SECONDS(1, 0, 0), NO_TIME},
57 {"MAR-1", -IN_SECONDS(1, 0, 0), NO_TIME},
58 {"MAR+2", IN_SECONDS(2, 0, 0), NO_TIME},
59 {"MAR7", IN_SECONDS(7, 0, 0), NO_TIME},
60 {"MAR-7", -IN_SECONDS(7, 0, 0), NO_TIME},
61 {"MARS5", IN_SECONDS(5, 0, 0), NO_TIME},
62 {"MARSM5", IN_SECONDS(5, 0, 0), NO_TIME},
63 {"MARSMOON5", IN_SECONDS(5, 0, 0), NO_TIME}, // assuming TZNAME_MAX >= 8
64 {"MARS5:23:42", IN_SECONDS(5, 23, 42), NO_TIME},
65 {"SUN-7:14:24", -IN_SECONDS(7, 14, 24), NO_TIME},
66 // with DST
67 {"MAR5SMAR", IN_SECONDS(5, 0, 0), IN_SECONDS(4, 0, 0)}, // only DST name
68 {"MAR5SMAR2", IN_SECONDS(5, 0, 0), IN_SECONDS(2, 0, 0)}, // DST name with offset
69 {"MAR3SMAR-3", IN_SECONDS(3, 0, 0), -IN_SECONDS(3, 0, 0)},
70 {"MARSWINTER4MARSUMMER", IN_SECONDS(4, 0, 0), IN_SECONDS(3, 0, 0)},
71 {"MARSWINTER4MARSUMMER3", IN_SECONDS(4, 0, 0), IN_SECONDS(3, 0, 0)},
72 // with DST IN_SECONDSs
73 {"WMARS3SMARS,J80", IN_SECONDS(3, 0, 0), IN_SECONDS(2, 0, 0)},
74 {"WMARS3SMARS,J80,J134", IN_SECONDS(3, 0, 0), IN_SECONDS(2, 0, 0)},
75 {"WMARS3SMARS,79", IN_SECONDS(3, 0, 0), IN_SECONDS(2, 0, 0)},
76 {"WMARS3SMARS,76,134", IN_SECONDS(3, 0, 0), IN_SECONDS(2, 0, 0)},
77 {"WMARS3SMARS,76/02,134/03", IN_SECONDS(3, 0, 0), IN_SECONDS(2, 0, 0)},
78 {"WMARS3SMARS,76/02:15:45,134/03:40:20", IN_SECONDS(3, 0, 0), IN_SECONDS(2, 0, 0)},
79 {"WMARS3SMARS,M3.4.1/02:15:45,M8.3.1/03:40:20", IN_SECONDS(3, 0, 0), IN_SECONDS(2, 0, 0)},
80
81 // special std names
82 {"<UNK>-1", -IN_SECONDS(1, 0, 0), NO_TIME},
83 {"<UNKNOWN>-2", -IN_SECONDS(2, 0, 0), NO_TIME}, // require TZNAME_MAX >= 7 + 1
84 {"<003>3", IN_SECONDS(3, 0, 0), NO_TIME},
85 {"<+04>4", IN_SECONDS(4, 0, 0), NO_TIME},
86 {"<-05>-5", -IN_SECONDS(5, 0, 0), NO_TIME},
87 {"<A-5>6", IN_SECONDS(6, 0, 0), NO_TIME},
88 {"<+A5>-7", -IN_SECONDS(7, 0, 0), NO_TIME},
89 {"<0123456>8", IN_SECONDS(8, 0, 0), NO_TIME},
90 {"<0A1B2C3>9", IN_SECONDS(9, 0, 0), NO_TIME},
91 {"<RD-04>-4<RD+005>5", -IN_SECONDS(4, 0, 0), IN_SECONDS(5, 0, 0)},
92 {"<WINT+03>3<SUM+02>", IN_SECONDS(3, 0, 0), IN_SECONDS(2, 0, 0)},
93 {"<WINT+03>3<SUM+02>2", IN_SECONDS(3, 0, 0), IN_SECONDS(2, 0, 0)},
94 {"<WINT+03>3:15<SUM+02>2:30:15", IN_SECONDS(3, 15, 0), IN_SECONDS(2, 30, 15)},
95 {"<H3M15>3:15<H2M30S15>2:30:15", IN_SECONDS(3, 15, 0), IN_SECONDS(2, 30, 15)}, // requires TZNAME_MAX >= 8 + 1
96 {"<+H6M20S12>6:20:12<-H4M40S14>-4:40:14", IN_SECONDS(6, 20, 12), -IN_SECONDS(4, 40, 14)}, // requires TZNAME_MAX >= 9 + 1
97
98 /*
99 * real-world test vectors.
100 * IN_SECONDSzones extracted from the tzdb (https://github.com/eggert/tz#2019e).
101 * The IN_SECONDSzone strings can also be obtained from https://raw.githubusercontent.com/nayarsystems/posix_tz_db/master/zones.csv.
102 */
103 { /* Etc/GMT-14 */ "<+14>-14", -IN_SECONDS(14, 0, 0), NO_TIME},
104 { /* Etc/GMT+12 */ "<-12>12", IN_SECONDS(12, 0, 0), NO_TIME},
105 { /* Africa/Casablanca */ "<+01>-1", -IN_SECONDS(1, 0, 0), NO_TIME},
106 { /* America/Araguaina */ "<-03>3", IN_SECONDS(3, 0, 0), NO_TIME},
107 { /* America/Asuncion */ "<-04>4<-03>,M10.1.0/0,M3.4.0/0", IN_SECONDS(4, 0, 0), IN_SECONDS(3, 0, 0)},
108 { /* America/Los_Angeles */ "PST8PDT,M3.2.0,M11.1.0", IN_SECONDS(8, 0, 0), IN_SECONDS(7, 0, 0)},
109 { /* America/New_York */ "EST5EDT,M3.2.0,M11.1.0", IN_SECONDS(5, 0, 0), IN_SECONDS(4, 0, 0)},
110 { /* America/Scoresbysund */ "<-01>1<+00>,M3.5.0/0,M10.5.0/1", IN_SECONDS(1, 0, 0), IN_SECONDS(0, 0, 0)},
111 { /* Asia/Colombo */ "<+0530>-5:30", -IN_SECONDS(5, 30, 0), NO_TIME},
112 { /* Europe/Berlin */ "CET-1CEST,M3.5.0,M10.5.0/3", -IN_SECONDS(1, 0, 0), -IN_SECONDS(2, 0, 0)},
113
114 /// test parsing errors
115 // 1. names are too long
116 {"JUSTEXCEEDI1:11:11", 0, NO_TIME},
117 {"AVERYLONGNAMEWHICHEXCEEDSTZNAMEMAX2:22:22", 0, NO_TIME},
118 {"FIRSTVERYLONGNAME3:33:33SECONDVERYLONGNAME4:44:44", 0, 0},
119 {"<JUSTEXCEEDI>5:55:55", 0, NO_TIME},
120 {"<FIRSTVERYLONGNAME>3:33:33<SECONDVERYLONGNAME>4:44:44", 0, 0},
121 {"<+JUSTEXCEED>5:55:55", 0, NO_TIME},
122
123 // 2. names are too short
124 {"JU6:34:47", 0, NO_TIME},
125 {"HE6:34:47LO3:34:47", 0, 0},
126 {"<AB>2:34:47", 0, NO_TIME},
127 {"<AB>2:34:47<CD>3:34:47", 0, 0},
128
129 // 3. names contain invalid chars
130 {"N?ME2:10:56", 0, NO_TIME},
131 {"N!ME2:10:56", 0, NO_TIME},
132 {"N/ME2:10:56", 0, NO_TIME},
133 {"N$ME2:10:56", 0, NO_TIME},
134 {"NAME?2:10:56", 0, NO_TIME},
135 {"?NAME2:10:56", 0, NO_TIME},
136 {"NAME?UNK4:21:15", 0, NO_TIME},
137 {"NAME!UNK4:22:15NEXT/NAME4:23:15", 0, NO_TIME},
138
139 // 4. bogus strings
140 {"NOINFO", 0, NO_TIME},
141 {"HOUR:16:18", 0, NO_TIME},
142 {"<BEGIN", 0, NO_TIME},
143 {"<NEXT:55", 0, NO_TIME},
144 {">WRONG<2:15:00", 0, NO_TIME},
145 {"ST<ART4:30:00", 0, NO_TIME},
146 //{"MANY8:00:00:00", 0, NO_TIME},
147 {"\0", 0, NO_TIME},
148 {"M\0STR7:30:36", 0, NO_TIME}
149 };
150
151 // END test vectors
152
153 static int failed = 0;
154
155 #define TEST_ASSERT_EQUAL_INT_MESSAGE(...) assert_equal(__VA_ARGS__)
assert_equal(int lhs,int rhs,const char * msg)156 void assert_equal(int lhs, int rhs, const char* msg)
157 {
158 if (lhs != rhs)
159 {
160 printf("Assertion failed! Expected %d to equal %d. %s\n", lhs, rhs, msg);
161 ++failed;
162 }
163 }
164
test_TimezoneStrings(void)165 void test_TimezoneStrings(void)
166 {
167 char buffer[128];
168
169 for (size_t i = 0; i < (sizeof(test_timezones) / sizeof(struct tz_test)); ++i)
170 {
171 struct tz_test ptr = test_timezones[i];
172
173 setenv("TZ", ptr.tzstr, 1);
174 tzset();
175
176 snprintf(buffer, 128, "winter time, timezone = \"%s\"", ptr.tzstr);
177
178 struct tm winter_tm_copy = winter_tm; // copy
179 TEST_ASSERT_EQUAL_INT_MESSAGE(winter_time + ptr.offset_seconds, mktime(&winter_tm_copy), buffer);
180
181 if (ptr.dst_offset_seconds != NO_TIME)
182 {
183 snprintf(buffer, 128, "summer time, timezone = \"%s\"", ptr.tzstr);
184
185 struct tm summer_tm_copy = summer_tm; // copy
186 TEST_ASSERT_EQUAL_INT_MESSAGE(summer_time + ptr.dst_offset_seconds, mktime(&summer_tm_copy), buffer);
187 }
188 }
189 }
190
main(void)191 int main(void)
192 {
193 test_TimezoneStrings();
194 exit (failed);
195 }
196