Import of CCTZ from GitHub. PiperOrigin-RevId: 958524522 Change-Id: I64ef0e82ebf7996c08498d065920cd8216b7f3ad
diff --git a/absl/time/internal/cctz/BUILD.bazel b/absl/time/internal/cctz/BUILD.bazel index 3b47877..edec282 100644 --- a/absl/time/internal/cctz/BUILD.bazel +++ b/absl/time/internal/cctz/BUILD.bazel
@@ -140,7 +140,10 @@ name = "time_zone_lookup_test", size = "small", timeout = "moderate", - srcs = ["src/time_zone_lookup_test.cc"], + srcs = [ + "src/time_zone_lookup_test.cc", + "src/tzfile.h", + ], copts = ABSL_TEST_COPTS, data = [":zoneinfo"], linkopts = ABSL_DEFAULT_LINKOPTS, @@ -177,6 +180,24 @@ ], ) +cc_test( + name = "time_zone_posix_test", + size = "small", + srcs = [ + "src/time_zone_posix.h", + "src/time_zone_posix_test.cc", + ], + copts = ABSL_TEST_COPTS, + linkopts = ABSL_DEFAULT_LINKOPTS, + deps = [ + ":civil_time", + ":time_zone", + "//absl/base:config", + "@googletest//:gtest", + "@googletest//:gtest_main", + ], +) + ### benchmarks cc_test(
diff --git a/absl/time/internal/cctz/src/cctz_benchmark.cc b/absl/time/internal/cctz/src/cctz_benchmark.cc index ce27818..17ff576 100644 --- a/absl/time/internal/cctz/src/cctz_benchmark.cc +++ b/absl/time/internal/cctz/src/cctz_benchmark.cc
@@ -28,6 +28,7 @@ namespace { +// SKIP_ABSL_INLINE_NAMESPACE_CHECK namespace cctz = absl::time_internal::cctz; void BM_Difference_Days(benchmark::State& state) {
diff --git a/absl/time/internal/cctz/src/time_zone_fixed.cc b/absl/time/internal/cctz/src/time_zone_fixed.cc index ed7f9cb..b33410d 100644 --- a/absl/time/internal/cctz/src/time_zone_fixed.cc +++ b/absl/time/internal/cctz/src/time_zone_fixed.cc
@@ -40,12 +40,19 @@ return p; } +// Returns the value of the decimal digit ch, or -1 if ch is not a digit. +// Note that std::strchr() also matches kDigits' terminating '\0', which +// would otherwise be taken for a tenth digit. +int ParseDigit(char ch) { + const char* const dp = std::strchr(kDigits, ch); + return (dp == nullptr || *dp == '\0') ? -1 : static_cast<int>(dp - kDigits); +} + int Parse02d(const char* p) { - if (const char* ap = std::strchr(kDigits, *p)) { - int v = static_cast<int>(ap - kDigits); - if (const char* bp = std::strchr(kDigits, *++p)) { - return (v * 10) + static_cast<int>(bp - kDigits); - } + const int hi = ParseDigit(p[0]); + if (hi >= 0) { + const int lo = ParseDigit(p[1]); + if (lo >= 0) return (hi * 10) + lo; } return -1; }
diff --git a/absl/time/internal/cctz/src/time_zone_format.cc b/absl/time/internal/cctz/src/time_zone_format.cc index 91b4621..f97de2f 100644 --- a/absl/time/internal/cctz/src/time_zone_format.cc +++ b/absl/time/internal/cctz/src/time_zone_format.cc
@@ -58,6 +58,16 @@ namespace { +// The ctype functions have undefined behavior for negative char values, +// so these helpers ensure the argument is always in the unsigned-char domain. +bool isdigit(char ch) { + return std::isdigit(static_cast<unsigned char>(ch)) != 0; +} + +bool isspace(char ch) { + return std::isspace(static_cast<unsigned char>(ch)) != 0; +} + #if !HAS_STRPTIME // Build a strptime() using C++11's std::get_time(). char* strptime(const char* s, const char* fmt, std::tm* tm) { @@ -553,7 +563,7 @@ bp = Format64(ep, 4, al.cs.year()); result.append(bp, ep); pending = cur += 2; - } else if (std::isdigit(*cur)) { + } else if (isdigit(*cur)) { // Possibly found %E#S or %E#f. int n = 0; if (const char* np = ParseInt(cur, 0, 0, 1024, &n)) { @@ -620,7 +630,7 @@ const char* ParseZone(const char* dp, std::string* zone) { zone->clear(); if (dp != nullptr) { - while (*dp != '\0' && !std::isspace(*dp)) zone->push_back(*dp++); + while (*dp != '\0' && !isspace(*dp)) zone->push_back(*dp++); if (zone->empty()) dp = nullptr; } return dp; @@ -706,7 +716,7 @@ const char* const edata = data + input.size(); // Skips leading whitespace. - while (std::isspace(*data)) ++data; + while (isspace(*data)) ++data; const year_t kyearmax = std::numeric_limits<year_t>::max(); const year_t kyearmin = std::numeric_limits<year_t>::min(); @@ -744,9 +754,9 @@ // Steps through format, one specifier at a time. while (data != nullptr && fmt != efmt) { - if (std::isspace(*fmt)) { - while (std::isspace(*data)) ++data; - while (std::isspace(*++fmt)) continue; + if (isspace(*fmt)) { + while (isspace(*data)) ++data; + while (isspace(*++fmt)) continue; continue; } @@ -898,7 +908,7 @@ continue; } if (fmt[0] == '*' && fmt[1] == 'f') { - if (data != nullptr && std::isdigit(*data)) { + if (data != nullptr && isdigit(*data)) { data = ParseSubSeconds(data, &subseconds); } fmt += 2; @@ -917,7 +927,7 @@ fmt += 2; continue; } - if (std::isdigit(*fmt)) { + if (isdigit(*fmt)) { int n = 0; // value ignored if (const char* np = ParseInt(fmt, 0, 0, 1024, &n)) { if (*np == 'S') { @@ -929,7 +939,7 @@ continue; } if (*np == 'f') { - if (data != nullptr && std::isdigit(*data)) { + if (data != nullptr && isdigit(*data)) { data = ParseSubSeconds(data, &subseconds); } fmt = ++np; @@ -977,7 +987,7 @@ } // Skip any remaining whitespace. - while (std::isspace(*data)) ++data; + while (isspace(*data)) ++data; // parse() must consume the entire input string. if (data != edata) {
diff --git a/absl/time/internal/cctz/src/time_zone_format_test.cc b/absl/time/internal/cctz/src/time_zone_format_test.cc index f047d93..8793a80 100644 --- a/absl/time/internal/cctz/src/time_zone_format_test.cc +++ b/absl/time/internal/cctz/src/time_zone_format_test.cc
@@ -1002,6 +1002,27 @@ EXPECT_FALSE(parse("%Ez", "-00:-0", tz, &tp)); } +TEST(Parse, NonAsciiInput) { + const time_zone tz = utc_time_zone(); + auto tp = chrono::system_clock::from_time_t(0); + + // High-bit-set bytes reach the ctype functions during parsing. 0xA0 is not + // ASCII whitespace, so the leading-whitespace skip must not consume it and + // the parse must fail rather than pass a negative char to std::isspace(). + EXPECT_FALSE(parse("%Y-%m-%d", + "\xA0" + "2016-01-02", + tz, &tp)); + EXPECT_FALSE(parse("%Y", + "\xA0" + "2016", + tz, &tp)); + + // A leading ASCII space is still skipped as before. + EXPECT_TRUE(parse("%Y-%m-%d", " 2016-01-02", tz, &tp)); + EXPECT_EQ(2016, convert(tp, utc_time_zone()).year()); +} + TEST(Parse, PosixConversions) { time_zone tz = utc_time_zone(); auto tp = chrono::system_clock::from_time_t(0);
diff --git a/absl/time/internal/cctz/src/time_zone_impl.cc b/absl/time/internal/cctz/src/time_zone_impl.cc index 5f2f49e..5cf38e7 100644 --- a/absl/time/internal/cctz/src/time_zone_impl.cc +++ b/absl/time/internal/cctz/src/time_zone_impl.cc
@@ -76,9 +76,18 @@ // Add the new time zone to the map. std::lock_guard<std::mutex> lock(TimeZoneMutex()); if (time_zone_map == nullptr) time_zone_map = new TimeZoneImplByName; + if (!new_impl->zone_) { + // Load failed, but a successful insertion may have happened concurrently. + // Check it now that we have the lock. Otherwise, avoid caching negative + // entries to avoid unbounded growth and DoS attacks. + auto itr = time_zone_map->find(name); + const Impl* impl = (itr != time_zone_map->end()) ? itr->second : utc_impl; + *tz = time_zone(impl); + return impl != utc_impl; + } const Impl*& impl = (*time_zone_map)[name]; if (impl == nullptr) { // this thread won any load race - impl = new_impl->zone_ ? new_impl.release() : utc_impl; + impl = new_impl.release(); } *tz = time_zone(impl); return impl != utc_impl;
diff --git a/absl/time/internal/cctz/src/time_zone_info.cc b/absl/time/internal/cctz/src/time_zone_info.cc index f8484c9..1e707c8 100644 --- a/absl/time/internal/cctz/src/time_zone_info.cc +++ b/absl/time/internal/cctz/src/time_zone_info.cc
@@ -32,6 +32,14 @@ #include "absl/time/internal/cctz/src/time_zone_info.h" +#include "absl/base/config.h" + +#if !defined(_MSC_VER) +#include <fcntl.h> +#include <sys/stat.h> +#include <unistd.h> +#endif + #include <algorithm> #include <cassert> #include <chrono> @@ -41,16 +49,17 @@ #include <cstring> #include <fstream> #include <functional> +#include <limits> #include <memory> #include <sstream> #include <string> #include <utility> #include <vector> -#include "absl/base/config.h" #include "absl/time/internal/cctz/include/cctz/civil_time.h" #include "absl/time/internal/cctz/src/time_zone_fixed.h" #include "absl/time/internal/cctz/src/time_zone_posix.h" +#include "absl/time/internal/cctz/src/tzfile.h" namespace absl { ABSL_NAMESPACE_BEGIN @@ -338,6 +347,14 @@ return EquivTransitions(transitions_.back().type_index, dst_ti); } + // We require that zoneinfo data with a rule for future transitions + // ends with a non-negative transition. This removes the need to add + // any "second-half" transition to ensure differences between adjacent + // transitions are always representable, while also guaranteeing that + // the arithmetic used to shift between 400-year cycles never overflows. + // All valid zones easily meet this requirement. + if (transitions_.back().unix_time < 0) return false; + // Extend the transitions for an additional 401 years using the future // specification. Years beyond those can be handled by mapping back to // a cycle-equivalent year within that range. Note that we need 401 @@ -382,18 +399,45 @@ using FilePtr = std::unique_ptr<FILE, int (*)(FILE*)>; -// fopen(3) adaptor. -inline FilePtr FOpen(const char* path, const char* mode) { +// fopen(3) adaptor for reading zoneinfo files (read-only binary mode). +inline FilePtr FOpen(const char* path) { #if defined(_MSC_VER) FILE* fp; - if (fopen_s(&fp, path, mode) != 0) fp = nullptr; + if (fopen_s(&fp, path, "rb") != 0) fp = nullptr; return FilePtr(fp, fclose); #else - // TODO: Enable the close-on-exec flag. - return FilePtr(fopen(path, mode), fclose); + // Open non-blocking and verify the target is a regular file before handing it + // to stdio. Zone names are potentially attacker-controlled, and a plain + // fopen() on a FIFO or device node (reachable via the "file:" prefix or an + // absolute path) would block indefinitely or read unbounded data. + const int fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC); + if (fd >= 0) { + struct stat st; + if (fstat(fd, &st) == 0 && S_ISREG(st.st_mode)) { + FILE* fp = fdopen(fd, "rb"); + if (fp != nullptr) return FilePtr(fp, fclose); + } + close(fd); + } + return FilePtr(nullptr, fclose); #endif } +// Returns true if the zone name starting at pos contains an unsafe path. +inline bool UnsafePath(const std::string& name, std::size_t pos) { + // Path traversal: exact match ".." + if (name.compare(pos, std::string::npos, "..") == 0) return true; + // Path traversal: leading component "../" + if (name.compare(pos, 3, "../") == 0) return true; + // Path traversal: interior component "/../" + if (name.find("/../", pos) != std::string::npos) return true; + // Path traversal: trailing component "/.." + if (name.size() - pos >= 3 && name.compare(name.size() - 3, 3, "/..") == 0) { + return true; + } + return false; +} + // A stdio(3)-backed implementation of ZoneInfoSource. class FileZoneInfoSource : public ZoneInfoSource { public: @@ -431,6 +475,11 @@ // Use of the "file:" prefix is intended for testing purposes only. const std::size_t pos = (name.compare(0, 5, "file:") == 0) ? 5 : 0; + // Reject unsafe paths (e.g., "../../etc/passwd"). + if (UnsafePath(name, pos)) { + return nullptr; + } + // Map the time-zone name to a path name. std::string path; if (pos == name.size() || name[pos] != '/') { @@ -451,7 +500,7 @@ path.append(name, pos, std::string::npos); // Open the zoneinfo file. - auto fp = FOpen(path.c_str(), "rb"); + auto fp = FOpen(path.c_str()); if (fp == nullptr) return nullptr; return std::unique_ptr<ZoneInfoSource>(new FileZoneInfoSource(std::move(fp))); } @@ -477,7 +526,7 @@ for (const char* tzdata : {"/apex/com.android.tzdata/etc/tz/tzdata", "/data/misc/zoneinfo/current/tzdata", "/system/usr/share/zoneinfo/tzdata"}) { - auto fp = FOpen(tzdata, "rb"); + auto fp = FOpen(tzdata); if (fp == nullptr) continue; char hbuf[24]; // covers header.zonetab_offset too @@ -497,9 +546,12 @@ if (zonecnt * sizeof(ebuf) != index_size) continue; for (std::size_t i = 0; i != zonecnt; ++i) { if (fread(ebuf, 1, sizeof(ebuf), fp.get()) != sizeof(ebuf)) break; - const std::int_fast32_t start = data_offset + Decode32(ebuf + 40); + const std::int_fast64_t start = + std::int_fast64_t{data_offset} + Decode32(ebuf + 40); const std::int_fast32_t length = Decode32(ebuf + 44); if (start < 0 || length < 0) break; + // fseek() takes a long + if (start > std::numeric_limits<long>::max()) break; ebuf[40] = '\0'; // ensure zone name is NUL terminated if (strcmp(name.c_str() + pos, ebuf) == 0) { if (fseek(fp.get(), static_cast<long>(start), SEEK_SET) != 0) break; @@ -537,6 +589,11 @@ // Use of the "file:" prefix is intended for testing purposes only. const std::size_t pos = (name.compare(0, 5, "file:") == 0) ? 5 : 0; + // Reject unsafe paths (e.g., "../../etc/passwd"). + if (UnsafePath(name, pos)) { + return nullptr; + } + // Prefixes where a Fuchsia component might find zoneinfo files, // in descending order of preference. const auto kTzdataPrefixes = { @@ -561,7 +618,7 @@ if (!prefix.empty()) path += "zoneinfo/tzif2/"; // format path.append(name, pos, std::string::npos); - auto fp = FOpen(path.c_str(), "rb"); + auto fp = FOpen(path.c_str()); if (fp == nullptr) continue; std::string version; @@ -662,6 +719,12 @@ if (hdr.ttisstdcnt != 0 && hdr.ttisstdcnt != hdr.typecnt) return false; if (hdr.ttisutcnt != 0 && hdr.ttisutcnt != hdr.typecnt) return false; + // Bound the header counts before sizing tbuf so that a hostile TZif blob + // cannot force a very large zero-filled allocation from a tiny input. + if (hdr.timecnt > TZ_MAX_TIMES) return false; + if (hdr.typecnt > TZ_MAX_TYPES) return false; + if (hdr.charcnt > TZ_MAX_CHARS) return false; + // Read the data into a local buffer. std::size_t len = hdr.DataLength(time_len); std::vector<char> tbuf(len); @@ -674,6 +737,14 @@ for (std::size_t i = 0; i != hdr.timecnt; ++i) { transitions_[i].unix_time = (time_len == 4) ? Decode32(bp) : Decode64(bp); bp += time_len; + // A valid zoneinfo file keeps transition times far from the int64 limits. + // A hostile one can place them at the extremes, where the + // reverse-conversion arithmetic in MakeTime() (tr.unix_time +/- a sub-day + // civil delta, see MakeSkipped()/MakeRepeated()) overflows. Bound them to + // +/-(1<<59), the times used by the no-op transitions added below. + if (transitions_[i].unix_time < -(1LL << 59) || + transitions_[i].unix_time > (1LL << 59)) + return false; // out of range if (i != 0) { // Check that the transitions are ordered by time (as zic guarantees). if (!Transition::ByUnixTime()(transitions_[i - 1], transitions_[i])) @@ -705,16 +776,22 @@ // Determine the before-first-transition type. default_transition_type_ = 0; if (seen_type_0 && hdr.timecnt != 0) { - std::uint_fast8_t index = 0; + std::size_t index = 0; if (transition_types_[0].is_dst) { index = transitions_[0].type_index; while (index != 0 && transition_types_[index].is_dst) --index; } while (index != hdr.typecnt && transition_types_[index].is_dst) ++index; - if (index != hdr.typecnt) default_transition_type_ = index; + if (index != hdr.typecnt) + default_transition_type_ = static_cast<std::uint_fast8_t>(index); } - // Copy all the abbreviations. + // Copy all the abbreviations. The area holds NUL-terminated strings, and + // LocalTime() hands out a pointer into it, so the final abbreviation has + // to be terminated within the area itself. Otherwise an abbreviation runs + // on into whatever ExtendTransitions() later appends. (hdr.charcnt != 0 + // because every abbr_index was validated to be less than it.) + if (bp[hdr.charcnt - 1] != '\0') return false; abbreviations_.reserve(hdr.charcnt + 10); abbreviations_.assign(bp, hdr.charcnt); bp += hdr.charcnt; @@ -771,6 +848,7 @@ // previous transition is always representable, without overflow. const Transition& last(transitions_.back()); if (last.unix_time < 0) { + assert(!extended_); const std::uint_fast8_t type_index = last.type_index; Transition& tr(*transitions_.emplace(transitions_.end())); tr.unix_time = 2147483647; // 2038-01-19T03:14:07+00:00
diff --git a/absl/time/internal/cctz/src/time_zone_lookup_test.cc b/absl/time/internal/cctz/src/time_zone_lookup_test.cc index cd08a35..1147e93 100644 --- a/absl/time/internal/cctz/src/time_zone_lookup_test.cc +++ b/absl/time/internal/cctz/src/time_zone_lookup_test.cc
@@ -15,21 +15,27 @@ #include <chrono> #include <cstddef> #include <cstdlib> +#include <cstring> #include <future> #include <limits> +#include <memory> #include <string> #include <thread> +#include <utility> #include <vector> #include "absl/base/config.h" #include "absl/time/internal/cctz/include/cctz/time_zone.h" + #if defined(__linux__) #include <features.h> #endif #include "gtest/gtest.h" #include "absl/time/internal/cctz/include/cctz/civil_time.h" +#include "absl/time/internal/cctz/include/cctz/zone_info_source.h" #include "absl/time/internal/cctz/src/test_time_zone_names.h" +#include "absl/time/internal/cctz/src/tzfile.h" namespace chrono = std::chrono; @@ -180,6 +186,24 @@ EXPECT_FALSE(load_time_zone("", &tz)); EXPECT_EQ(chrono::system_clock::from_time_t(0), convert(civil_second(1970, 1, 1, 0, 0, 0), tz)); // UTC + + // Reject path-traversal components. + EXPECT_FALSE(load_time_zone("file:../etc/passwd", &tz)); + EXPECT_FALSE(load_time_zone("file:../../etc/passwd", &tz)); + EXPECT_FALSE(load_time_zone("file:/../etc/passwd", &tz)); + EXPECT_FALSE(load_time_zone("file:America/../America/Los_Angeles", &tz)); + + // Reject a fixed-offset name with a NUL where a digit belongs. + for (const int i : {10, 11, 13, 14, 16, 17}) { + std::string name = "Fixed/UTC+00:00:00"; + name[static_cast<std::size_t>(i)] = '\0'; + EXPECT_FALSE(load_time_zone(name, &tz)) << "NUL at offset " << i; + } + + // Reject non-regular files and directories. + EXPECT_FALSE(load_time_zone("file:/dev/null", &tz)); + EXPECT_FALSE(load_time_zone("file:/dev/stdin", &tz)); + EXPECT_FALSE(load_time_zone("file:/tmp", &tz)); } TEST(TimeZone, Equality) { @@ -912,6 +936,181 @@ ExpectTime(tp, tz, 10000, 1, 1, 0, 0, 0, 0 * 3600, false, "UTC"); } +// A ZoneInfoSource implementation backed by an in-memory string buffer. +class StringZoneInfoSource : public ZoneInfoSource { + public: + explicit StringZoneInfoSource(std::string data) + : data_(std::move(data)), offset_(0) {} + + std::size_t Read(void* ptr, std::size_t size) override { + std::size_t n = (std::min)(size, data_.size() - offset_); + std::memcpy(ptr, data_.data() + offset_, n); + offset_ += n; + return n; + } + + int Skip(std::size_t offset) override { + if (offset > data_.size() - offset_) return -1; + offset_ += offset; + return 0; + } + + private: + std::string data_; + std::size_t offset_; +}; + +// Constructs a minimal TZif2 string with a single 64-bit transition +// at the given transition time and a future POSIX rule. The abbreviation +// area holds abbr verbatim, so a valid file's abbr must include the +// trailing '\0' (e.g., std::string{"EST", 4}). +std::string MakeExtendedTzif(std::int_fast64_t unix_time, + std::int_fast32_t utc_offset, + const std::string& abbr, + const std::string& future_spec) { + std::string s; + auto append32 = [&s](std::int_fast32_t v) { + const std::int_fast32_t s32max = 0x7fffffff; + const auto s32maxU = static_cast<std::uint_fast32_t>(s32max); + std::uint_fast32_t uv; + if (v >= 0) { + uv = static_cast<std::uint_fast32_t>(v); + } else { + uv = static_cast<std::uint_fast32_t>(v + s32max + 1) + s32maxU + 1; + } + for (int i = 3; i >= 0; --i) { + s.push_back(static_cast<char>((uv >> (i * 8)) & 0xff)); + } + }; + auto append64 = [&s](std::int_fast64_t v) { + const std::int_fast64_t s64max = 0x7fffffffffffffff; + const auto s64maxU = static_cast<std::uint_fast64_t>(s64max); + std::uint_fast64_t uv; + if (v >= 0) { + uv = static_cast<std::uint_fast64_t>(v); + } else { + uv = static_cast<std::uint_fast64_t>(v + s64max + 1) + s64maxU + 1; + } + for (int i = 7; i >= 0; --i) { + s.push_back(static_cast<char>((uv >> (i * 8)) & 0xff)); + } + }; + + const std::size_t charcnt = abbr.size(); + + // 32-bit header + s.append(TZ_MAGIC, 4); + s.push_back('2'); // tzh_version + s.append(15, '\0'); // tzh_reserved + append32(0); // tzh_ttisutcnt + append32(0); // tzh_ttisstdcnt + append32(0); // tzh_leapcnt + append32(0); // tzh_timecnt (0 32-bit transitions) + append32(1); // tzh_typecnt (1 ttinfo record) + append32(static_cast<std::int_fast32_t>(charcnt)); // tzh_charcnt + + // 32-bit data block + append32(utc_offset); // tt_utoff + s.push_back(0); // tt_isdst (standard time) + s.push_back(0); // tt_desigidx + s.append(abbr); // abbreviation table + + // 64-bit header + s.append(TZ_MAGIC, 4); + s.push_back('2'); // tzh_version + s.append(15, '\0'); // tzh_reserved + append32(0); // tzh_ttisutcnt + append32(0); // tzh_ttisstdcnt + append32(0); // tzh_leapcnt + append32(1); // tzh_timecnt (1 64-bit transition) + append32(1); // tzh_typecnt (1 ttinfo record) + append32(static_cast<std::int_fast32_t>(charcnt)); // tzh_charcnt + + // 64-bit data block + append64(unix_time); // transition time + s.push_back(0); // type index for transition + append32(utc_offset); // tt_utoff + s.push_back(0); // tt_isdst (standard time) + s.push_back(0); // tt_desigidx + s.append(abbr); // abbreviation table + + // POSIX footer + s.push_back('\n'); + s.append(future_spec); + s.push_back('\n'); + return s; +} + +std::unique_ptr<ZoneInfoSource> ExtendedTestFactory( + const std::string& name, + const std::function<std::unique_ptr<ZoneInfoSource>(const std::string&)>& + fallback) { + if (name == "test:ExtendedBeforeEpoch") { + // -1 (1969-12-31T23:59:59Z) is the latest final transition before the + // epoch, so the zone is rejected despite the future specification. + return std::unique_ptr<ZoneInfoSource>( + new StringZoneInfoSource(MakeExtendedTzif( + -1, -5 * 3600, std::string{"EST", 4}, "EST5EDT,M3.2.0,M11.1.0"))); + } + if (name == "test:ExtendedFarFuture") { + // 0 (1970-01-01T00:00:00Z) is the earliest final transition an extended + // zone may have, which maximizes the 400-year shift that BreakTime() + // needs for a lookup at the maximum time. + return std::unique_ptr<ZoneInfoSource>( + new StringZoneInfoSource(MakeExtendedTzif( + 0, -5 * 3600, std::string{"EST", 4}, "EST5EDT,M3.2.0,M11.1.0"))); + } + if (name == "test:UnterminatedAbbreviation") { + // The abbreviation area is missing its final NUL, so the abbreviation + // would run into whatever ExtendTransitions() appends behind it. + return std::unique_ptr<ZoneInfoSource>(new StringZoneInfoSource( + MakeExtendedTzif(0, -5 * 3600, "EST", "EST5EDT,M3.2.0,M11.1.0"))); + } + return fallback(name); +} + +// Tests that a TZif file whose abbreviation area is not NUL-terminated +// is rejected. +TEST(TimeZoneEdgeCase, UnterminatedAbbreviation) { + auto prev_factory = cctz_extension::zone_info_source_factory; + cctz_extension::zone_info_source_factory = ExtendedTestFactory; + + time_zone tz; + EXPECT_FALSE(load_time_zone("test:UnterminatedAbbreviation", &tz)); + + cctz_extension::zone_info_source_factory = prev_factory; +} + +// Tests that a TZif file whose explicit transitions end before epoch +// is rejected when it has a POSIX DST footer string. +TEST(TimeZoneEdgeCase, ExtendedBeforeEpoch) { + auto prev_factory = cctz_extension::zone_info_source_factory; + cctz_extension::zone_info_source_factory = ExtendedTestFactory; + + // Extended zones must end with a non-negative explicit transition. + time_zone tz; + EXPECT_FALSE(load_time_zone("test:ExtendedBeforeEpoch", &tz)); + + cctz_extension::zone_info_source_factory = prev_factory; +} + +// Looking up the maximum time in an extended zone must fold back through the +// 400-year cycle without overflowing when BreakTime() computes the shift. +TEST(TimeZoneEdgeCase, ExtendedFarFuture) { + auto prev_factory = cctz_extension::zone_info_source_factory; + cctz_extension::zone_info_source_factory = ExtendedTestFactory; + + time_zone tz; + ASSERT_TRUE(load_time_zone("test:ExtendedFarFuture", &tz)); + + auto tp_max = time_point<absl::time_internal::cctz::seconds>::max(); + ExpectTime(tp_max, tz, 292277026596, 12, 4, 10, 30, 7, -5 * 3600, false, + "EST"); + EXPECT_STREQ("EST", tz.lookup(tp_max).abbr); + + cctz_extension::zone_info_source_factory = prev_factory; +} + } // namespace cctz } // namespace time_internal ABSL_NAMESPACE_END
diff --git a/absl/time/internal/cctz/src/time_zone_posix.cc b/absl/time/internal/cctz/src/time_zone_posix.cc index efea080..c60f98b 100644 --- a/absl/time/internal/cctz/src/time_zone_posix.cc +++ b/absl/time/internal/cctz/src/time_zone_posix.cc
@@ -92,14 +92,17 @@ return p; } -// datetime = ( Jn | n | Mm.w.d ) [ / offset ] +// datetime = , ( Jn | n | Mm.w.d ) [ / offset ] const char* ParseDateTime(const char* p, PosixTransition* res) { - if (p != nullptr && *p == ',') { + if (p != nullptr) { + if (*p != ',') return nullptr; if (*++p == 'M') { int month = 0; - if ((p = ParseInt(p + 1, 1, 12, &month)) != nullptr && *p == '.') { + if ((p = ParseInt(p + 1, 1, 12, &month)) != nullptr) { + if (*p != '.') return nullptr; int week = 0; - if ((p = ParseInt(p + 1, 1, 5, &week)) != nullptr && *p == '.') { + if ((p = ParseInt(p + 1, 1, 5, &week)) != nullptr) { + if (*p != '.') return nullptr; int weekday = 0; if ((p = ParseInt(p + 1, 0, 6, &weekday)) != nullptr) { res->date.fmt = PosixTransition::M; @@ -122,17 +125,17 @@ res->date.n.day = static_cast<std::int_fast16_t>(day); } } - } - if (p != nullptr) { - res->time.offset = 2 * 60 * 60; // default offset is 02:00:00 - if (*p == '/') p = ParseOffset(p + 1, -167, 167, 1, &res->time.offset); + if (p != nullptr) { + res->time.offset = 2 * 60 * 60; // default offset is 02:00:00 + if (*p == '/') p = ParseOffset(p + 1, -167, 167, 1, &res->time.offset); + } } return p; } } // namespace -// spec = std offset [ dst [ offset ] , datetime , datetime ] +// spec = std offset [ dst [ offset ] datetime datetime ] bool ParsePosixSpec(const std::string& spec, PosixTimeZone* res) { const char* p = spec.c_str(); if (*p == ':') return false;
diff --git a/absl/time/internal/cctz/src/time_zone_posix.h b/absl/time/internal/cctz/src/time_zone_posix.h index 7fd2b9e..23f5699 100644 --- a/absl/time/internal/cctz/src/time_zone_posix.h +++ b/absl/time/internal/cctz/src/time_zone_posix.h
@@ -13,7 +13,7 @@ // limitations under the License. // Parsing of a POSIX zone spec as described in the TZ part of section 8.3 in -// http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html. +// https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap08.html. // // The current POSIX spec for America/Los_Angeles is "PST8PDT,M3.2.0,M11.1.0", // which would be broken down as ...
diff --git a/absl/time/internal/cctz/src/time_zone_posix_test.cc b/absl/time/internal/cctz/src/time_zone_posix_test.cc new file mode 100644 index 0000000..8e429f7 --- /dev/null +++ b/absl/time/internal/cctz/src/time_zone_posix_test.cc
@@ -0,0 +1,195 @@ +// Copyright 2026 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "absl/time/internal/cctz/src/time_zone_posix.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "absl/base/config.h" + +namespace absl { +ABSL_NAMESPACE_BEGIN +namespace time_internal { +namespace cctz { + +using ::testing::Eq; +using ::testing::IsEmpty; + +// We only support the second POSIX format (that is, neither the +// "first character is a <colon>" format, nor the "geographical +// or a special timezone" format). We also require DST start/end +// rules whenever a DST abbreviation is given (zic always provides +// them). So, ... +// +// spec = abbr offset [ abbr [ offset ] datetime datetime ] +// abbr = <.*?> | [^-+,\d]{3,} +// offset = [+|-]hh[:mm[:ss]] +// datetime = , ( Jn | n | Mm.w.d ) [ / offset ] + +TEST(ParsePosixSpec, UnsupportedFormats) { + PosixTimeZone zone; + EXPECT_FALSE(ParsePosixSpec(":characters", &zone)); + EXPECT_FALSE(ParsePosixSpec("Area/Location", &zone)); +} + +TEST(ParsePosixSpec, StdOnly) { + PosixTimeZone zone; + + // America/Cancun + EXPECT_TRUE(ParsePosixSpec("EST5", &zone)); + EXPECT_THAT(zone.std_abbr, Eq("EST")); + EXPECT_THAT(zone.std_offset, Eq(-5 * 60 * 60)); + EXPECT_THAT(zone.dst_abbr, IsEmpty()); + + // Australia/Darwin + EXPECT_TRUE(ParsePosixSpec("ACST-9:30", &zone)); + EXPECT_THAT(zone.std_abbr, Eq("ACST")); + EXPECT_THAT(zone.std_offset, Eq((9 * 60 + 30) * 60)); + EXPECT_THAT(zone.dst_abbr, IsEmpty()); + + // Australia/Eucla + EXPECT_TRUE(ParsePosixSpec("<+0845>-8:45", &zone)); + EXPECT_THAT(zone.std_abbr, Eq("+0845")); + EXPECT_THAT(zone.std_offset, Eq((8 * 60 + 45) * 60)); + EXPECT_THAT(zone.dst_abbr, IsEmpty()); +} + +TEST(ParsePosixSpec, WithDst) { + PosixTimeZone zone; + + // America/New_York + EXPECT_TRUE(ParsePosixSpec("EST5EDT,M3.2.0,M11.1.0", &zone)); + EXPECT_THAT(zone.std_abbr, Eq("EST")); + EXPECT_THAT(zone.std_offset, Eq(-5 * 60 * 60)); + EXPECT_THAT(zone.dst_abbr, Eq("EDT")); + EXPECT_THAT(zone.dst_offset, Eq(-4 * 60 * 60)); + EXPECT_THAT(zone.dst_start.date.fmt, Eq(PosixTransition::M)); + EXPECT_THAT(zone.dst_start.date.m.month, Eq(3)); + EXPECT_THAT(zone.dst_start.date.m.week, Eq(2)); + EXPECT_THAT(zone.dst_start.date.m.weekday, Eq(0)); + EXPECT_THAT(zone.dst_start.time.offset, Eq(2 * 60 * 60)); + EXPECT_THAT(zone.dst_end.date.fmt, Eq(PosixTransition::M)); + EXPECT_THAT(zone.dst_end.date.m.month, Eq(11)); + EXPECT_THAT(zone.dst_end.date.m.week, Eq(1)); + EXPECT_THAT(zone.dst_end.date.m.weekday, Eq(0)); + EXPECT_THAT(zone.dst_end.time.offset, Eq(2 * 60 * 60)); + + // Australia/Adelaide + EXPECT_TRUE(ParsePosixSpec("ACST-9:30ACDT,M10.1.0,M4.1.0/3", &zone)); + EXPECT_THAT(zone.std_abbr, Eq("ACST")); + EXPECT_THAT(zone.std_offset, Eq((9 * 60 + 30) * 60)); + EXPECT_THAT(zone.dst_abbr, Eq("ACDT")); + EXPECT_THAT(zone.dst_offset, Eq((10 * 60 + 30) * 60)); + EXPECT_THAT(zone.dst_start.date.fmt, Eq(PosixTransition::M)); + EXPECT_THAT(zone.dst_start.date.m.month, Eq(10)); + EXPECT_THAT(zone.dst_start.date.m.week, Eq(1)); + EXPECT_THAT(zone.dst_start.date.m.weekday, Eq(0)); + EXPECT_THAT(zone.dst_start.time.offset, Eq(2 * 60 * 60)); + EXPECT_THAT(zone.dst_end.date.fmt, Eq(PosixTransition::M)); + EXPECT_THAT(zone.dst_end.date.m.month, Eq(4)); + EXPECT_THAT(zone.dst_end.date.m.week, Eq(1)); + EXPECT_THAT(zone.dst_end.date.m.weekday, Eq(0)); + EXPECT_THAT(zone.dst_end.time.offset, Eq(3 * 60 * 60)); + + // Australia/Lord_Howe + EXPECT_TRUE(ParsePosixSpec("<+1030>-10:30<+11>-11,M10.1.0,M4.1.0", &zone)); + EXPECT_THAT(zone.std_abbr, Eq("+1030")); + EXPECT_THAT(zone.std_offset, Eq((10 * 60 + 30) * 60)); + EXPECT_THAT(zone.dst_abbr, Eq("+11")); + EXPECT_THAT(zone.dst_offset, Eq(11 * 60 * 60)); + EXPECT_THAT(zone.dst_start.date.fmt, Eq(PosixTransition::M)); + EXPECT_THAT(zone.dst_start.date.m.month, Eq(10)); + EXPECT_THAT(zone.dst_start.date.m.week, Eq(1)); + EXPECT_THAT(zone.dst_start.date.m.weekday, Eq(0)); + EXPECT_THAT(zone.dst_start.time.offset, Eq(2 * 60 * 60)); + EXPECT_THAT(zone.dst_end.date.fmt, Eq(PosixTransition::M)); + EXPECT_THAT(zone.dst_end.date.m.month, Eq(4)); + EXPECT_THAT(zone.dst_end.date.m.week, Eq(1)); + EXPECT_THAT(zone.dst_end.date.m.weekday, Eq(0)); + EXPECT_THAT(zone.dst_end.time.offset, Eq(2 * 60 * 60)); + + // Africa/Casablanca (year-round DST) + EXPECT_TRUE(ParsePosixSpec("<+00>0<+01>,0/0,J365/25", &zone)); + EXPECT_THAT(zone.std_abbr, Eq("+00")); + EXPECT_THAT(zone.std_offset, Eq(0)); + EXPECT_THAT(zone.dst_abbr, Eq("+01")); + EXPECT_THAT(zone.dst_offset, Eq(1 * 60 * 60)); + EXPECT_THAT(zone.dst_start.date.fmt, Eq(PosixTransition::N)); + EXPECT_THAT(zone.dst_start.date.n.day, Eq(0)); + EXPECT_THAT(zone.dst_start.time.offset, Eq(0)); + EXPECT_THAT(zone.dst_end.date.fmt, Eq(PosixTransition::J)); + EXPECT_THAT(zone.dst_end.date.n.day, Eq(365)); + EXPECT_THAT(zone.dst_end.time.offset, Eq(25 * 60 * 60)); +} + +TEST(TimeZonePosix, ParseErrors) { + PosixTimeZone zone; + + // STD abbreviation errors. + EXPECT_FALSE(ParsePosixSpec("ET5", &zone)); + EXPECT_FALSE(ParsePosixSpec("ET+", &zone)); + EXPECT_FALSE(ParsePosixSpec("ET-", &zone)); + EXPECT_FALSE(ParsePosixSpec("ET,", &zone)); + EXPECT_FALSE(ParsePosixSpec("<00", &zone)); + + // STD offset errors. + EXPECT_FALSE(ParsePosixSpec("<00>", &zone)); + EXPECT_FALSE(ParsePosixSpec("<00>+", &zone)); + EXPECT_FALSE(ParsePosixSpec("<00>-", &zone)); + EXPECT_FALSE(ParsePosixSpec("<00>?", &zone)); + + // DST abbreviation errors. + EXPECT_FALSE(ParsePosixSpec("EST5DT,M3.2.0,M11.1.0", &zone)); + EXPECT_FALSE(ParsePosixSpec("EST5EDT+,M3.2.0,M11.1.0", &zone)); + EXPECT_FALSE(ParsePosixSpec("EST5EDT-,M3.2.0,M11.1.0", &zone)); + EXPECT_FALSE(ParsePosixSpec("<00>0<-01", &zone)); + + // DST offset errors. + EXPECT_FALSE(ParsePosixSpec("<01>1<00>?,0,0", &zone)); + EXPECT_FALSE(ParsePosixSpec("<01>1<00>+?,0,0", &zone)); + EXPECT_FALSE(ParsePosixSpec("<01>1<00>-?,0,0", &zone)); + + // Malformed DST start date/time. + EXPECT_FALSE(ParsePosixSpec("EST5EDT", &zone)); + EXPECT_FALSE(ParsePosixSpec("EST5EDT,", &zone)); + EXPECT_FALSE(ParsePosixSpec("EST5EDT,M13.2.0,M11.1.0", &zone)); + EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.6.0,M11.1.0", &zone)); + EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.7,M11.1.0", &zone)); + EXPECT_FALSE(ParsePosixSpec("EST5EDT,J0,M11.1.0", &zone)); + EXPECT_FALSE(ParsePosixSpec("EST5EDT,366,M11.1.0", &zone)); + EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0/,M11.1.0", &zone)); + EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0/?,M11.1.0", &zone)); + EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0/1:?,M11.1.0", &zone)); + EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0/1:2:?,M11.1.0", &zone)); + + // Malformed DST end date/time. + EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0", &zone)); + EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0,M?.1.0", &zone)); + EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0,M11.?.0", &zone)); + EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0,M11.1.?", &zone)); + EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0,J?", &zone)); + EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0,?", &zone)); + EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0,M11.1.0/", &zone)); + EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0,M11.1.0/168", &zone)); + EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0,M11.1.0/167:60", &zone)); + EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0,M11.1.0/167:59:60", &zone)); + + // Trailing data. + EXPECT_FALSE(ParsePosixSpec("EST5EDT,M3.2.0,M11.1.0junk", &zone)); +} + +} // namespace cctz +} // namespace time_internal +ABSL_NAMESPACE_END +} // namespace absl
diff --git a/absl/time/internal/cctz/testdata/version b/absl/time/internal/cctz/testdata/version index 75d34ee..9217a2d 100644 --- a/absl/time/internal/cctz/testdata/version +++ b/absl/time/internal/cctz/testdata/version
@@ -1 +1 @@ -2026b +2026c
diff --git a/absl/time/internal/cctz/testdata/zoneinfo/Africa/Casablanca b/absl/time/internal/cctz/testdata/zoneinfo/Africa/Casablanca index 240ebb2..fb2f5cc 100644 --- a/absl/time/internal/cctz/testdata/zoneinfo/Africa/Casablanca +++ b/absl/time/internal/cctz/testdata/zoneinfo/Africa/Casablanca Binary files differ
diff --git a/absl/time/internal/cctz/testdata/zoneinfo/Africa/El_Aaiun b/absl/time/internal/cctz/testdata/zoneinfo/Africa/El_Aaiun index 909c5f9..46286b9 100644 --- a/absl/time/internal/cctz/testdata/zoneinfo/Africa/El_Aaiun +++ b/absl/time/internal/cctz/testdata/zoneinfo/Africa/El_Aaiun Binary files differ
diff --git a/absl/time/internal/cctz/testdata/zoneinfo/America/Edmonton b/absl/time/internal/cctz/testdata/zoneinfo/America/Edmonton index 645ee94..379e365 100644 --- a/absl/time/internal/cctz/testdata/zoneinfo/America/Edmonton +++ b/absl/time/internal/cctz/testdata/zoneinfo/America/Edmonton Binary files differ
diff --git a/absl/time/internal/cctz/testdata/zoneinfo/America/Yellowknife b/absl/time/internal/cctz/testdata/zoneinfo/America/Yellowknife index 645ee94..379e365 100644 --- a/absl/time/internal/cctz/testdata/zoneinfo/America/Yellowknife +++ b/absl/time/internal/cctz/testdata/zoneinfo/America/Yellowknife Binary files differ
diff --git a/absl/time/internal/cctz/testdata/zoneinfo/Canada/Mountain b/absl/time/internal/cctz/testdata/zoneinfo/Canada/Mountain index 645ee94..379e365 100644 --- a/absl/time/internal/cctz/testdata/zoneinfo/Canada/Mountain +++ b/absl/time/internal/cctz/testdata/zoneinfo/Canada/Mountain Binary files differ
diff --git a/absl/time/internal/cctz/testdata/zoneinfo/zone1970.tab b/absl/time/internal/cctz/testdata/zoneinfo/zone1970.tab index a9b47bc..635eabc 100644 --- a/absl/time/internal/cctz/testdata/zoneinfo/zone1970.tab +++ b/absl/time/internal/cctz/testdata/zoneinfo/zone1970.tab
@@ -112,7 +112,7 @@ CA +624900-0920459 America/Rankin_Inlet Central - NU (central) CA +5024-10439 America/Regina CST - SK (most areas) CA +5017-10750 America/Swift_Current CST - SK (midwest) -CA +5333-11328 America/Edmonton Mountain - AB, BC(E), NT(E), SK(W) +CA +5333-11328 America/Edmonton CST - AB, BC(E), NT(E), SK(W) CA +690650-1050310 America/Cambridge_Bay Mountain - NU (west) CA +682059-1334300 America/Inuvik Mountain - NT (west) CA +4916-12307 America/Vancouver MST - BC (most areas)
diff --git a/absl/time/internal/cctz/testdata/zoneinfo/zonenow.tab b/absl/time/internal/cctz/testdata/zoneinfo/zonenow.tab index 54e4485..9c3a8cf 100644 --- a/absl/time/internal/cctz/testdata/zoneinfo/zonenow.tab +++ b/absl/time/internal/cctz/testdata/zoneinfo/zonenow.tab
@@ -56,16 +56,19 @@ XX -2504-13005 Pacific/Pitcairn Pitcairn # # -08/-07 - PST/PDT (North America DST) -XX +340308-1181434 America/Los_Angeles Pacific (PST/PDT) - US & Canada; Mexico near US border +XX +340308-1181434 America/Los_Angeles Pacific (PST/PDT) - US; Mexico near US border # # -08/-07 - PST/PDT (North America DST) until 2026-11-01 02:00; then MST -XX +4916-12307 America/Vancouver MST - BC (most areas) +XX +4916-12307 America/Vancouver Mountain Standard (MST) - British Columbia (most areas) # # -07 - MST XX +332654-1120424 America/Phoenix Mountain Standard (MST) - Arizona; western Mexico; Yukon # # -07/-06 - MST/MDT (North America DST) -XX +394421-1045903 America/Denver Mountain (MST/MDT) - US & Canada; Mexico near US border +XX +394421-1045903 America/Denver Mountain (MST/MDT) - US; Mexico near US border; northern Canada +# +# -07/-06 - MST/MDT (North America DST) until 2026-11-01 02:00; then CST +XX +5333-11328 America/Edmonton Central Standard (CST) - Alberta and some neighbors # # -06 XX -0054-08936 Pacific/Galapagos Galápagos