Coverage Report

Created: 2026-11-05 00:47

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/root/doris/be/src/util/timezone_utils.cpp
Line
Count
Source
1
// Licensed to the Apache Software Foundation (ASF) under one
2
// or more contributor license agreements.  See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership.  The ASF licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License.  You may obtain a copy of the License at
8
//
9
//   http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied.  See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
#include "util/timezone_utils.h"
19
20
#include <cctz/civil_time.h>
21
#include <cctz/time_zone.h>
22
#include <fcntl.h>
23
#include <glog/logging.h>
24
#include <re2/re2.h>
25
#include <re2/stringpiece.h>
26
#include <sys/mman.h>
27
#include <sys/stat.h>
28
#include <sys/types.h>
29
#include <unistd.h>
30
31
#include <algorithm>
32
#include <boost/algorithm/string.hpp>
33
#include <boost/algorithm/string/case_conv.hpp>
34
#include <cctype>
35
#include <chrono>
36
#include <cstdlib>
37
#include <filesystem>
38
#include <memory>
39
#include <string>
40
#include <string_view>
41
42
#include "common/exception.h"
43
#include "common/logging.h"
44
#include "common/status.h"
45
46
using boost::algorithm::to_lower_copy;
47
48
namespace fs = std::filesystem;
49
#include "common/compile_check_begin.h"
50
51
namespace doris {
52
53
using ZoneList = std::unordered_map<std::string, cctz::time_zone>;
54
using FixedZoneList = std::unordered_map<int32_t, cctz::time_zone>;
55
56
RE2 time_zone_offset_format_reg(R"(^[+-]{1}\d{2}\:\d{2}$)"); // visiting is thread-safe
57
58
// for ut, make it never nullptr.
59
std::unique_ptr<ZoneList> lower_zone_cache_ = std::make_unique<ZoneList>();
60
std::unique_ptr<FixedZoneList> fixed_zone_cache_ = std::make_unique<FixedZoneList>();
61
62
const std::string TimezoneUtils::default_time_zone = "+08:00";
63
static const char* tzdir = "/usr/share/zoneinfo"; // default value, may change by TZDIR env var
64
65
74.9k
cctz::time_zone TimezoneUtils::create_fixed_offset_time_zone(int32_t offset_seconds) {
66
74.9k
    if (auto it = fixed_zone_cache_->find(offset_seconds); it != fixed_zone_cache_->end()) {
67
25.3k
        return it->second;
68
25.3k
    }
69
49.6k
    return cctz::fixed_time_zone(cctz::seconds(offset_seconds));
70
74.9k
}
71
72
11
void TimezoneUtils::clear_timezone_caches() {
73
11
    lower_zone_cache_->clear();
74
11
    fixed_zone_cache_->clear();
75
11
}
76
2
size_t TimezoneUtils::cache_size() {
77
2
    return lower_zone_cache_->size();
78
2
}
79
80
179k
static bool parse_save_name_tz(const std::string& tz_name) {
81
179k
    cctz::time_zone tz;
82
179k
    PROPAGATE_FALSE(cctz::load_time_zone(tz_name, &tz));
83
178k
    lower_zone_cache_->emplace(to_lower_copy(tz_name), tz);
84
178k
    return true;
85
179k
}
86
87
300
void TimezoneUtils::load_timezones_to_cache() {
88
300
    std::string base_str;
89
    // try get from system
90
300
    char* tzdir_env = std::getenv("TZDIR");
91
300
    if (tzdir_env && *tzdir_env) {
92
0
        tzdir = tzdir_env;
93
0
    }
94
95
300
    base_str = tzdir;
96
300
    base_str += '/';
97
98
300
    const auto root_path = fs::path {base_str};
99
300
    if (!exists(root_path)) {
100
0
        throw Exception(Status::FatalError("Cannot find system tzfile. Doris exiting!"));
101
0
    }
102
103
300
    std::set<std::string> ignore_paths = {"posix", "right"}; // duplications. ignore them.
104
105
186k
    for (fs::recursive_directory_iterator it {base_str}; it != end(it); it++) {
106
186k
        const auto& dir_entry = *it;
107
186k
        if (dir_entry.is_regular_file() ||
108
186k
            (dir_entry.is_symlink() && is_regular_file(read_symlink(dir_entry)))) {
109
179k
            auto tz_name = dir_entry.path().string().substr(base_str.length());
110
179k
            if (!parse_save_name_tz(tz_name)) {
111
1.50k
                LOG(WARNING) << "Meet illegal tzdata file: " << tz_name << ". skipped";
112
1.50k
            }
113
179k
        } else if (dir_entry.is_directory() && ignore_paths.contains(dir_entry.path().filename())) {
114
600
            it.disable_recursion_pending();
115
600
        }
116
186k
    }
117
    // some special cases. Z = Zulu. CST = Asia/Shanghai
118
300
    if (auto it = lower_zone_cache_->find("zulu"); it != lower_zone_cache_->end()) {
119
300
        lower_zone_cache_->emplace("z", it->second);
120
300
    }
121
300
    if (auto it = lower_zone_cache_->find("asia/shanghai"); it != lower_zone_cache_->end()) {
122
300
        lower_zone_cache_->emplace("cst", it->second);
123
300
    }
124
125
300
    lower_zone_cache_->erase("lmt"); // local mean time for every timezone
126
127
300
    load_offsets_to_cache();
128
300
    LOG(INFO) << "Preloaded" << lower_zone_cache_->size() << " timezones.";
129
300
}
130
131
24.9k
static std::string to_hour_string(int arg) {
132
24.9k
    if (arg < 0 && arg > -10) { // -9 to -1
133
8.74k
        return std::string {"-0"} + std::to_string(std::abs(arg));
134
16.2k
    } else if (arg >= 0 && arg < 10) { //0 to 9
135
9.72k
        return std::string {"0"} + std::to_string(arg);
136
9.72k
    }
137
6.48k
    return std::to_string(arg);
138
24.9k
}
139
140
324
void TimezoneUtils::load_offsets_to_cache() {
141
324
    static constexpr int supported_minutes[] = {0, 30, 45};
142
9.07k
    for (int hour = -12; hour <= +14; hour++) {
143
26.2k
        for (int minute : supported_minutes) {
144
26.2k
            if ((hour == -12 || hour == 14) && minute != 0) {
145
1.29k
                continue;
146
1.29k
            }
147
24.9k
            char min_str[3];
148
24.9k
            snprintf(min_str, sizeof(min_str), "%02d", minute);
149
24.9k
            std::string offset_str = (hour >= 0 ? "+" : "") + to_hour_string(hour) + ':' + min_str;
150
24.9k
            cctz::time_zone result;
151
24.9k
            CHECK(parse_tz_offset_string(offset_str, result));
152
24.9k
            lower_zone_cache_->emplace(offset_str, result);
153
24.9k
            const int offset_seconds = hour * 3600 + (hour < 0 ? -minute : minute) * 60;
154
24.9k
            fixed_zone_cache_->emplace(offset_seconds, result);
155
24.9k
        }
156
8.74k
    }
157
    // -00 for hour is also valid
158
324
    std::string offset_str = "-00:00";
159
324
    cctz::time_zone result;
160
324
    CHECK(parse_tz_offset_string(offset_str, result));
161
324
    lower_zone_cache_->emplace(offset_str, result);
162
324
    fixed_zone_cache_->emplace(0, result);
163
324
    offset_str = "-00:30";
164
324
    CHECK(parse_tz_offset_string(offset_str, result));
165
324
    lower_zone_cache_->emplace(offset_str, result);
166
324
    fixed_zone_cache_->emplace(-30 * 60, result);
167
324
    offset_str = "-00:45";
168
324
    CHECK(parse_tz_offset_string(offset_str, result));
169
324
    lower_zone_cache_->emplace(offset_str, result);
170
324
    fixed_zone_cache_->emplace(-45 * 60, result);
171
324
}
172
173
192k
bool TimezoneUtils::find_cctz_time_zone(const std::string& timezone, cctz::time_zone& ctz) {
174
192k
    if (auto it = lower_zone_cache_->find(to_lower_copy(timezone)); it != lower_zone_cache_->end())
175
143k
            [[likely]] {
176
143k
        ctz = it->second;
177
143k
        return true;
178
143k
    }
179
180
49.0k
    std::string normalized;
181
49.0k
    if (!normalize_timezone_name(timezone, &normalized)) {
182
158
        return false;
183
158
    }
184
48.9k
    if (auto it = lower_zone_cache_->find(to_lower_copy(normalized));
185
48.9k
        it != lower_zone_cache_->end()) [[likely]] {
186
20
        ctz = it->second;
187
20
        return true;
188
20
    }
189
48.8k
    return parse_tz_offset_string(normalized, ctz);
190
48.9k
}
191
192
bool TimezoneUtils::try_get_fixed_offset_seconds(const cctz::time_zone& timezone,
193
33
                                                 int32_t* offset_seconds) {
194
33
    DORIS_CHECK(offset_seconds != nullptr);
195
33
    const std::string& timezone_name = timezone.name();
196
33
    if (timezone_name == "UTC" || timezone_name == "Etc/UTC" || timezone_name == "Etc/GMT") {
197
19
        *offset_seconds = 0;
198
19
        return true;
199
19
    }
200
201
    // cctz names fixed_time_zone() instances with the "Fixed/" prefix. TZDB's Etc/GMT*
202
    // zones are fixed offsets too; cctz handles their POSIX-style reversed sign in lookup_offset().
203
    // If this naming convention changes, falling through to the generic path remains correct.
204
14
    static const auto epoch = std::chrono::time_point_cast<cctz::sys_seconds>(
205
14
            std::chrono::system_clock::from_time_t(0));
206
14
    if (timezone_name.compare(0, 6, "Fixed/") == 0 || timezone_name.compare(0, 7, "Etc/GMT") == 0) {
207
4
        *offset_seconds = timezone.lookup_offset(epoch).offset;
208
4
        return true;
209
4
    }
210
10
    return false;
211
14
}
212
213
static bool normalize_offset_string(const std::string& timezone, bool allow_hour_only,
214
123k
                                    std::string* normalized) {
215
123k
    if (timezone.size() < 2 || (timezone[0] != '+' && timezone[0] != '-')) {
216
0
        return false;
217
0
    }
218
219
123k
    const bool positive = timezone[0] == '+';
220
123k
    const std::string_view rest(timezone.data() + 1, timezone.size() - 1);
221
123k
    int hour = 0;
222
123k
    int minute = 0;
223
224
247k
    const auto parse_digit = [](char c) -> int { return c - '0'; };
225
123k
    const auto is_two_digits = [](std::string_view value) -> bool {
226
123k
        return value.size() == 2 && std::isdigit(static_cast<unsigned char>(value[0])) &&
227
123k
               std::isdigit(static_cast<unsigned char>(value[1]));
228
123k
    };
229
123k
    const auto is_one_or_two_digits = [](std::string_view value) -> bool {
230
123k
        return (value.size() == 1 || value.size() == 2) &&
231
123k
               std::all_of(value.begin(), value.end(),
232
247k
                           [](char c) { return std::isdigit(static_cast<unsigned char>(c)); });
233
123k
    };
234
235
123k
    const auto colon_pos = rest.find(':');
236
123k
    if (colon_pos != std::string_view::npos) {
237
123k
        const std::string_view hour_part = rest.substr(0, colon_pos);
238
123k
        const std::string_view minute_part = rest.substr(colon_pos + 1);
239
123k
        if (!is_one_or_two_digits(hour_part) || !is_two_digits(minute_part)) {
240
0
            return false;
241
0
        }
242
123k
        hour = std::stoi(std::string(hour_part));
243
123k
        minute = parse_digit(minute_part[0]) * 10 + parse_digit(minute_part[1]);
244
123k
    } else {
245
7
        if (!allow_hour_only || !is_one_or_two_digits(rest)) {
246
2
            return false;
247
2
        }
248
5
        hour = std::stoi(std::string(rest));
249
5
    }
250
251
123k
    const int limit = positive ? 14 : 12;
252
123k
    if (hour > limit || (hour == limit && minute != 0) || minute >= 60) {
253
119
        return false;
254
119
    }
255
256
123k
    *normalized = std::string(1, positive ? '+' : '-') + (hour < 10 ? "0" : "") +
257
123k
                  std::to_string(hour) + ":" + (minute < 10 ? "0" : "") + std::to_string(minute);
258
123k
    return true;
259
123k
}
260
261
123k
bool TimezoneUtils::normalize_timezone_name(const std::string& timezone, std::string* normalized) {
262
123k
    DORIS_CHECK(normalized != nullptr);
263
123k
    const std::string lower = to_lower_copy(timezone);
264
123k
    if (lower == "utc" || lower == "etc/utc" || lower == "zulu") {
265
286
        *normalized = "UTC";
266
286
        return true;
267
286
    }
268
269
123k
    if (lower.rfind("utc", 0) == 0 || lower.rfind("gmt", 0) == 0) {
270
150
        if (timezone.size() <= 3) {
271
0
            return false;
272
0
        }
273
150
        return normalize_offset_string(timezone.substr(3), true, normalized);
274
150
    }
275
276
123k
    if (!timezone.empty() && (timezone[0] == '+' || timezone[0] == '-')) {
277
123k
        return normalize_offset_string(timezone, false, normalized);
278
123k
    }
279
280
53
    return false;
281
123k
}
282
283
74.8k
bool TimezoneUtils::parse_tz_offset_string(const std::string& timezone, cctz::time_zone& ctz) {
284
74.8k
    std::string normalized;
285
74.8k
    if (!normalize_timezone_name(timezone, &normalized)) {
286
16
        return false;
287
16
    }
288
74.8k
    if (normalized == "UTC") {
289
143
        ctz = cctz::utc_time_zone();
290
143
        return true;
291
143
    }
292
293
74.6k
    re2::StringPiece value;
294
74.6k
    if (time_zone_offset_format_reg.Match(normalized, 0, normalized.size(), RE2::UNANCHORED, &value,
295
74.6k
                                          1)) [[likely]] {
296
74.6k
        const bool positive = value[0] != '-';
297
74.6k
        const int hour = std::stoi(value.substr(1, 2).as_string());
298
74.6k
        const int minute = std::stoi(value.substr(4, 2).as_string());
299
74.6k
        int offset = hour * 60 * 60 + minute * 60;
300
74.6k
        offset *= positive ? 1 : -1;
301
74.6k
        ctz = create_fixed_offset_time_zone(offset);
302
74.6k
        return true;
303
74.6k
    }
304
0
    return false;
305
74.6k
}
306
307
#include "common/compile_check_end.h"
308
} // namespace doris