summaryrefslogtreecommitdiff
path: root/src/tz_info.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/tz_info.cc')
-rw-r--r--src/tz_info.cc329
1 files changed, 329 insertions, 0 deletions
diff --git a/src/tz_info.cc b/src/tz_info.cc
new file mode 100644
index 0000000..7c3361e
--- /dev/null
+++ b/src/tz_info.cc
@@ -0,0 +1,329 @@
+#include "common.hh"
+
+#include "io.hh"
+#include "logger.hh"
+#include "tz_info.hh"
+#include "tz_str.hh"
+
+#include <byteswap.h>
+#include <errno.h>
+#include <string.h>
+#include <vector>
+#include <unistd.h>
+
+namespace {
+
+struct Header {
+ char magic[4];
+ char ver;
+ char unused[15];
+ uint32_t isutcnt;
+ uint32_t isstdcnt;
+ uint32_t leapcnt;
+ uint32_t timecnt;
+ uint32_t typecnt;
+ uint32_t charcnt;
+};
+
+#if !HAVE_ATTRIBUTE_PACKED
+#pragma pack(push)
+#endif
+struct
+#if HAVE_ATTRIBUTE_PACKED
+__attribute__((packed))
+#endif
+LocalTimeType {
+ int32_t utoff;
+ uint8_t dst;
+ uint8_t idx;
+};
+
+struct
+#if HAVE_ATTRIBUTE_PACKED
+__attribute__((packed))
+#endif
+LeapSecond {
+ int64_t occur;
+ int32_t corr;
+};
+#if !HAVE_ATTRIBUTE_PACKED
+#pragma pack(pop)
+#endif
+
+struct Data {
+ std::vector<int64_t> transition_times;
+ std::vector<uint8_t> transition_types;
+ std::vector<LocalTimeType> local_time_type_records;
+ std::vector<char> time_zone_designations;
+ std::vector<LeapSecond> leap_second_records;
+ std::vector<uint8_t> standard_or_wall_indicators;
+ std::vector<uint8_t> ut_local_indicators;
+};
+
+inline uint32_t ntoh(uint32_t value) {
+#ifdef WORDS_BIGENDIAN
+ return value;
+#else
+ return bswap_32(value);
+#endif
+}
+
+inline int32_t ntoh(int32_t value) {
+#ifdef WORDS_BIGENDIAN
+ return value;
+#else
+ return bswap_32(value);
+#endif
+}
+
+inline int64_t ntoh(int64_t value) {
+#ifdef WORDS_BIGENDIAN
+ return value;
+#else
+ return bswap_64(value);
+#endif
+}
+
+class TzInfoImpl : public TzInfo {
+public:
+ TzInfoImpl(std::shared_ptr<Logger> logger,
+ std::filesystem::path tzinfo_dir)
+ : logger_(std::move(logger)), base_(std::move(tzinfo_dir)) {
+ static_assert(sizeof(Header) == 44, "Header must be packed");
+ }
+
+ std::optional<time_t> get_local_time(std::string_view tzname,
+ time_t utc_time) const override {
+ if (!base_.empty()) {
+ std::filesystem::path zone = base_ / tzname;
+ auto fd = io::open(zone, io::open_flags::rdonly);
+ if (fd) {
+ return read(zone, fd.get(), utc_time);
+ } else {
+ logger_->warn("Unable to open %s for reading: %s.", zone.c_str(),
+ strerror(errno));
+ }
+ }
+ return std::nullopt;
+ }
+
+private:
+ std::optional<time_t> read(std::filesystem::path const& zone, int fd,
+ time_t utc_time) const {
+ Header header;
+ if (!io::read_all(fd, &header, sizeof(header))) {
+ logger_->warn("%s: Error reading: %s.", zone.c_str(),
+ strerror(errno));
+ return std::nullopt;
+ }
+ if (!check_and_fix_header(header)) {
+ logger_->warn("%s: Not a TZinfo file.", zone.c_str());
+ return std::nullopt;
+ }
+ // Skip V1, 32bit time stamps are just so 90's.
+ auto ret = lseek(fd, data_size(header, '\0'), SEEK_CUR);
+ if (ret == -1) {
+ logger_->warn("%s: Error seeking: %s.", zone.c_str(), strerror(errno));
+ return std::nullopt;
+ }
+ if (!io::read_all(fd, &header, sizeof(header))) {
+ logger_->warn("%s: Error reading: %s.", zone.c_str(),
+ strerror(errno));
+ return std::nullopt;
+ }
+ if (header.ver < '2' || !check_and_fix_header(header)) {
+ logger_->warn("%s: Not a TZinfo V2+ file.", zone.c_str());
+ return std::nullopt;
+ }
+ Data data;
+ if (!read_data(fd, header, data)) {
+ logger_->warn("%s: Error reading: %s.", zone.c_str(),
+ strerror(errno));
+ return std::nullopt;
+ }
+ std::string footer;
+ // Note that read_footer might read past the footer, which is currently
+ // fine (as it is a footer) but good to know for future developers.
+ if (!read_footer(fd, footer)) {
+ logger_->warn("%s: Error reading (4): %s.", zone.c_str(),
+ strerror(errno));
+ return std::nullopt;
+ }
+
+ return utc_to_local(data, footer, utc_time);
+ }
+
+ static std::optional<time_t> utc_to_local(Data const& data,
+ std::string const& footer,
+ time_t utc) {
+ if (data.transition_times.empty())
+ return utc_to_local(footer, utc);
+ if (utc < data.transition_times.front())
+ return std::nullopt;
+ size_t i = 0;
+ while (i < data.transition_times.size() && utc > data.transition_times[i])
+ ++i;
+
+ if (i == 0)
+ return std::nullopt;
+ if (i == data.transition_times.size() && !footer.empty())
+ return utc_to_local(footer, utc);
+ --i;
+
+ return utc_to_local(data.local_time_type_records[
+ data.transition_types[i]], utc);
+ }
+
+ static std::optional<time_t> utc_to_local(LocalTimeType const& local_time,
+ time_t utc) {
+ return utc += local_time.utoff;
+ }
+
+ static std::optional<time_t> utc_to_local(std::string const& tz, time_t utc) {
+ return tz::get_local_time(tz, utc);
+ }
+
+ static size_t data_size(Header const& header, char version) {
+ auto time_size = version >= '2' ? 8 : 4;
+ return header.timecnt * time_size + header.timecnt + header.typecnt * 6 +
+ header.charcnt + header.leapcnt * (time_size + 4) + header.isstdcnt +
+ header.isutcnt;
+ }
+
+ static bool read_data(int fd, Header const& header, Data& data) {
+ assert(header.ver >= '2');
+ data.transition_times.resize(header.timecnt);
+ data.transition_types.resize(header.timecnt);
+ static_assert(sizeof(LocalTimeType) == 6, "LocalTimeType must be packed");
+ data.local_time_type_records.resize(header.typecnt);
+ data.time_zone_designations.resize(header.charcnt);
+ static_assert(sizeof(LeapSecond) == 12, "LeapSecond must be packed");
+ data.leap_second_records.resize(header.leapcnt);
+ data.standard_or_wall_indicators.resize(header.isstdcnt);
+ data.ut_local_indicators.resize(header.isutcnt);
+ if (!io::read_all(fd, data.transition_times.data(),
+ header.timecnt * sizeof(int64_t)) ||
+ !io::read_all(fd, data.transition_types.data(), header.timecnt) ||
+ !io::read_all(fd, data.local_time_type_records.data(),
+ header.typecnt * sizeof(LocalTimeType)) ||
+ !io::read_all(fd, data.time_zone_designations.data(), header.charcnt) ||
+ !io::read_all(fd, data.leap_second_records.data(),
+ header.leapcnt * sizeof(LeapSecond)) ||
+ !io::read_all(fd, data.standard_or_wall_indicators.data(),
+ header.isstdcnt) ||
+ !io::read_all(fd, data.ut_local_indicators.data(), header.isutcnt))
+ return false;
+
+ {
+ int64_t last = std::numeric_limits<int64_t>::min();
+ for (auto& time : data.transition_times) {
+ time = ntoh(time);
+ if (time <= last)
+ return false;
+ last = time;
+ }
+ }
+
+ for (auto const& type : data.transition_types)
+ if (type >= header.typecnt)
+ return false;
+
+ for (auto& local : data.local_time_type_records) {
+ local.utoff = ntoh(local.utoff);
+ if (local.utoff == -2147483648)
+ return false;
+ if (local.dst != 0 && local.dst != 1)
+ return false;
+ if (local.idx >= header.charcnt)
+ return false;
+ }
+
+ if (data.time_zone_designations.empty() ||
+ data.time_zone_designations.back() != '\0')
+ return false;
+
+ {
+ LeapSecond last;
+ last.occur = -1;
+ last.corr = 0;
+ for (auto& leap : data.leap_second_records) {
+ leap.occur = ntoh(leap.occur);
+ leap.corr = ntoh(leap.corr);
+ if (last.occur == -1) {
+ if (leap.occur < 0)
+ return false;
+ } else {
+ if (leap.occur <= last.occur)
+ return false;
+ }
+ auto diff = last.corr - leap.corr;
+ if (diff != 1 && diff != -1)
+ return false;
+ last.occur = leap.occur + 2419199;
+ last.corr = leap.corr;
+ }
+ }
+
+ for (auto const& standard_or_wall : data.standard_or_wall_indicators)
+ if (standard_or_wall != 1 && standard_or_wall != 0)
+ return false;
+
+ for (auto const& ut_or_local : data.ut_local_indicators)
+ if (ut_or_local != 1 && ut_or_local != 0)
+ return false;
+
+ return true;
+ }
+
+ static bool read_footer(int fd, std::string& footer) {
+ size_t offset = 0;
+ footer.resize(128);
+ while (true) {
+ auto got = io::read(fd, footer.data() + offset, footer.size() - offset);
+ if (got <= 0)
+ return false;
+ if (footer.front() != '\n')
+ return false;
+ auto end = footer.substr(0, got).find('\n', std::max(1lu, offset));
+ if (end != std::string::npos) {
+ footer = footer.substr(1, end - 1);
+ return true;
+ }
+ offset += got;
+ footer.resize(offset + 128);
+ }
+ }
+
+ static bool check_and_fix_header(Header& header) {
+ if (memcmp(header.magic, "TZif", 4))
+ return false;
+ header.isutcnt = ntoh(header.isutcnt);
+ header.isstdcnt = ntoh(header.isstdcnt);
+ header.leapcnt = ntoh(header.leapcnt);
+ header.timecnt = ntoh(header.timecnt);
+ header.typecnt = ntoh(header.typecnt);
+ header.charcnt = ntoh(header.charcnt);
+
+ if (header.isutcnt != 0 && header.isutcnt != header.typecnt)
+ return false;
+ if (header.isstdcnt != 0 && header.isstdcnt != header.typecnt)
+ return false;
+ if (header.typecnt == 0)
+ return false;
+ if (header.charcnt == 0)
+ return false;
+
+ return true;
+ }
+
+ std::shared_ptr<Logger> logger_;
+ std::filesystem::path base_;
+};
+
+} // namespace
+
+std::unique_ptr<TzInfo> TzInfo::create(std::shared_ptr<Logger> logger,
+ std::filesystem::path tzinfo_dir) {
+ return std::make_unique<TzInfoImpl>(std::move(logger), std::move(tzinfo_dir));
+}
+