diff options
Diffstat (limited to 'src/tz_info.cc')
| -rw-r--r-- | src/tz_info.cc | 329 |
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)); +} + |
