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