#include "common.hh" #include "io.hh" #include "logger.hh" #include "tz_info.hh" #include "tz_str.hh" #include #include #include #include #include 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 transition_times; std::vector transition_types; std::vector local_time_type_records; std::vector time_zone_designations; std::vector leap_second_records; std::vector standard_or_wall_indicators; std::vector 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, 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 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 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 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 utc_to_local(LocalTimeType const& local_time, time_t utc) { return utc += local_time.utoff; } static std::optional 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::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_; std::filesystem::path base_; }; } // namespace std::unique_ptr TzInfo::create(std::shared_ptr logger, std::filesystem::path tzinfo_dir) { return std::make_unique(std::move(logger), std::move(tzinfo_dir)); }