[gcc r16-9619] libstdc++: Make chrono::tzdb parser case-insensive [PR123510]

Tomasz Kaminski tkaminsk@gcc.gnu.org
Mon Aug 31 07:33:31 GMT 2026


https://gcc.gnu.org/g:d0558173f7fc411bd2f364f8e16eb3f5e5b5c48d

commit r16-9619-gd0558173f7fc411bd2f364f8e16eb3f5e5b5c48d
Author: Tomasz Kamiński <tkaminsk@redhat.com>
Date:   Thu Aug 27 07:44:59 2026 +0200

    libstdc++: Make chrono::tzdb parser case-insensive [PR123510]
    
    Expand switch and if-check to check both lowercase and uppercase letter.
    For line parsing in chrono::reload_tzdb, the type is inspected by default
    branch (check for 'Z'), this patch normalizes it for all branches
    consistently.
    
    For testing, we check minimum unambigous prefix and whole world as both
    lowercase and uppercase. This covers all code paths.
    
    libstdc++-v3/ChangeLog:
    
            PR libstdc++/123510
            * src/c++20/tzdb.cc (at_time::is_indicator): Add case labels
            for uppercase letters.
            (operator>>(istream&, years_from_to)): Check for 'M' and 'O',
            (chrono::reload_tzdb): Add case labels for lowercase letters,
            and normalize first letter of type.
            (operator>>(istream&, abbrev_month&)): Match all cases in
            switch and days_chars.
            (operator>>(istream&, on_month_day&)): Check for 'L'.
            * testsuite/std/time/tzdb/name_matching.cc: New test.
    
    Reviewed-by: Jonathan Wakely <jwakely@redhat.com>
    Signed-off-by: Tomasz Kamiński <tkaminsk@redhat.com>
    (cherry picked from commit 2509f241a097ef247fc4814c5614e3c4e3cc70d2)

Diff:
---
 libstdc++-v3/src/c++20/tzdb.cc                     |  52 +++-
 .../testsuite/std/time/tzdb/name_matching.cc       | 318 +++++++++++++++++++++
 2 files changed, 364 insertions(+), 6 deletions(-)

diff --git a/libstdc++-v3/src/c++20/tzdb.cc b/libstdc++-v3/src/c++20/tzdb.cc
index 42b39a47ea95..ca63cf61ccca 100644
--- a/libstdc++-v3/src/c++20/tzdb.cc
+++ b/libstdc++-v3/src/c++20/tzdb.cc
@@ -267,14 +267,20 @@ namespace std::chrono
 	switch (c)
 	{
 	  case 's':
+	  case 'S':
 	    return {Standard, true};
 	  case 'u':
+	  case 'U':
 	  case 'g':
+	  case 'G':
 	  case 'z':
+	  case 'Z':
 	    return {Universal, true};
 	  case 'w':
+	  case 'W':
 	    return {Wall, true};
 	  case 'd':
+	  case 'D':
 	    return {Daylight, true};
 	  default:
 	    return {Wall, false};
@@ -394,7 +400,7 @@ namespace std::chrono
       {
 	string s;
 	auto c = ws(in).peek();
-	if (c == 'm') [[unlikely]] // keyword "minimum"
+	if (c == 'm' || c == 'M') [[unlikely]] // keyword "minimum"
 	  {
 	    in >> s; // extract the rest of the word
 	    yy.from = year(1900);
@@ -403,12 +409,12 @@ namespace std::chrono
 	  yy.from = year{num};
 
 	c = ws(in).peek();
-	if (c == 'm') // keyword "maximum"
+	if (c == 'm' || c == 'M') // keyword "maximum"
 	  {
 	    in >> s; // extract the rest of the word
 	    yy.to = year::max();
 	  }
-	else if (c == 'o') // keyword "only"
+	else if (c == 'o' || c == 'O') // keyword "only"
 	  {
 	    in >> s; // extract the rest of the word
 	    yy.to = yy.from;
@@ -1962,28 +1968,37 @@ constinit tzdb_list::_Node::NumLeapSeconds tzdb_list::_Node::num_leap_seconds;
 	    {
 	      case '#':
 		break;
+	      case 'r':
 	      case 'R':
 	      {
 		// Rule  NAME  FROM  TO  TYPE  IN  ON  AT  SAVE  LETTER/S
 		is >> type; // extract the "Rule" or "R" marker
+		type[0] = 'R';
+
 		Rule rule;
 		is >> rule;
 		node->rules.push_back(std::move(rule));
 		break;
 	      }
+	      case 'l':
 	      case 'L':
 	      {
 		// Link  TARGET           LINK-NAME
 		is >> type; // extract the "Link" or "L" marker
+		type[0] = 'L';
+
 		time_zone_link link(nullptr);
 		is >> quoted(link._M_target) >> quoted(link._M_name);
 		node->db.links.push_back(std::move(link));
 		break;
 	      }
+	      case 'z':
 	      case 'Z':
 	      {
 		// Zone  NAME        STDOFF  RULES   FORMAT  [UNTIL]
 		is >> type; // extract the "Zone" or "Z" marker
+		type[0] = 'Z';
+
 		time_zone tz(std::make_unique<time_zone::_Impl>(node));
 		is >> quoted(tz._M_name);
 		node->db.zones.push_back(time_zone(std::move(tz)));
@@ -2475,60 +2490,76 @@ constinit tzdb_list::_Node::NumLeapSeconds tzdb_list::_Node::num_leap_seconds;
       in >> s;
       switch (s[0])
       {
+      case 'j':
       case 'J':
 	switch (s[1])
 	{
 	case 'a':
+	case 'A':
 	  am.m = January;
 	  return in;
 	case 'u':
+	case 'U':
 	  switch (s[2])
 	  {
 	  case 'n':
+	  case 'N':
 	    am.m = June;
 	    return in;
 	  case 'l':
+	  case 'L':
 	    am.m = July;
 	    return in;
 	  }
 	  break;
 	}
 	break;
+      case 'f':
       case 'F':
 	am.m = February;
 	return in;
+      case 'm':
       case 'M':
-	if (s[1] == 'a') [[likely]]
+	if (s[1] == 'a' || s[1] == 'A') [[likely]]
 	  switch (s[2])
 	  {
 	  case 'r':
+	  case 'R':
 	    am.m = March;
 	    return in;
 	  case 'y':
+	  case 'Y':
 	    am.m = May;
 	    return in;
 	  }
 	break;
+      case 'a':
       case 'A':
 	switch (s[1])
 	{
 	case 'p':
+	case 'P':
 	  am.m = April;
 	  return in;
 	case 'u':
+	case 'U':
 	  am.m = August;
 	  return in;
 	}
 	break;
+      case 's':
       case 'S':
 	am.m = September;
 	return in;
+      case 'o':
       case 'O':
 	am.m = October;
 	return in;
+      case 'n':
       case 'N':
 	am.m = November;
 	return in;
+      case 'd':
       case 'D':
 	am.m = December;
 	return in;
@@ -2552,37 +2583,46 @@ constinit tzdb_list::_Node::NumLeapSeconds tzdb_list::_Node::num_leap_seconds;
 	// Just peek at one char at a time.
 	switch (in.peek())
 	{
+	case 'm':
 	case 'M':
 	  aw.wd = Monday;
 	  break;
+	case 't':
 	case 'T':
 	  in.ignore(1); // Discard the 'T'
 	  switch (in.peek())
 	  {
 	  case 'u':
+	  case 'U':
 	    aw.wd = Tuesday;
 	    break;
 	  case 'h':
+	  case 'H':
 	    aw.wd = Thursday;
 	    break;
 	  default:
 	    in.setstate(ios::failbit);
 	  }
 	  break;
+	case 'w':
 	case 'W':
 	  aw.wd = Wednesday;
 	  break;
+	case 'f':
 	case 'F':
 	  aw.wd = Friday;
 	  break;
+	case 's':
 	case 'S':
 	  in.ignore(1); // Discard the 'S'
 	  switch (in.peek())
 	  {
 	  case 'a':
+	  case 'A':
 	    aw.wd = Saturday;
 	    break;
 	  case 'u':
+	  case 'U':
 	    aw.wd = Sunday;
 	    break;
 	  default:
@@ -2595,7 +2635,7 @@ constinit tzdb_list::_Node::NumLeapSeconds tzdb_list::_Node::num_leap_seconds;
 	in.ignore(1); // Discard whichever char we just looked at.
 
 	// Discard any remaining chars from weekday, e.g. "onday".
-	string_view day_chars = "ondayesritu";
+	string_view day_chars = "ondayesrituONDAYESRITU";
 	auto is_day_char = [&day_chars](int c) {
 	  return c != char_traits<char>::eof()
 		   && day_chars.find((char)c) != day_chars.npos;
@@ -2629,7 +2669,7 @@ constinit tzdb_list::_Node::NumLeapSeconds tzdb_list::_Node::num_leap_seconds;
 		  return in;
 		}
 	    }
-	  else if (c == 'l') // lastSunday, lastWed, ...
+	  else if (c == 'l' || c == 'L') // lastSunday, lastWed, ...
 	    {
 	      in.ignore(4);
 	      if (abbrev_weekday w{}; in >> w) [[likely]]
diff --git a/libstdc++-v3/testsuite/std/time/tzdb/name_matching.cc b/libstdc++-v3/testsuite/std/time/tzdb/name_matching.cc
new file mode 100644
index 000000000000..8d845ab94b97
--- /dev/null
+++ b/libstdc++-v3/testsuite/std/time/tzdb/name_matching.cc
@@ -0,0 +1,318 @@
+// { dg-do run { target c++20 } }
+// { dg-require-effective-target tzdb }
+// { dg-require-effective-target cxx11_abi }
+// { dg-xfail-run-if "no weak override on AIX" { powerpc-ibm-aix* } }
+
+#include <chrono>
+#include <fstream>
+#include <format>
+#include <testsuite_hooks.h>
+#include <iostream>
+
+static bool override_used = false;
+
+namespace __gnu_cxx
+{
+  const char* zoneinfo_dir_override() {
+    override_used = true;
+    return "./";
+  }
+}
+
+using namespace std::chrono;
+
+template<typename Functor, typename... Args>
+  void
+  test_combinations(Functor&& func, std::string_view txt, size_t min, Args&&... args)
+  {
+    std::string lower(txt);
+    std::string upper(txt);
+    for (char& c : upper)
+      c = std::toupper(c);
+
+    func(lower.substr(0, min), args...);
+    func(upper.substr(0, min), args...);
+    func(lower, args...);
+    func(upper, args...);
+  }
+
+void
+test_day(std::string_view name, weekday wd)
+{
+  constexpr const char* templ = R"(# version test_day_{0}
+ # Zone  NAME  STDOFF  RULES  FORMAT  [UNTIL]
+ Zone    Test  0       -      Z1      2000 May last{0}
+               0       -      Z2      2010 May {0}>=10
+               0       -      Z3
+  )";
+
+  std::string input = std::format(templ, name);
+  std::ofstream("tzdata.zi") << input;
+
+  const auto& db = reload_tzdb();
+  VERIFY( override_used ); // If this fails then XFAIL for the target.
+
+  std::string_view ver = db.version;
+  VERIFY( ver.starts_with("test_day_") );
+  ver.remove_prefix(9);
+  VERIFY( ver == name );
+
+  sys_info info = locate_zone("Test")->get_info(sys_days(2005y/January/1));
+  VERIFY( info.begin == sys_days(2000y/May/wd[last]) );
+  year_month_day ymd(ceil<days>(info.end));
+  VERIFY( ymd.year() == 2010y );
+  VERIFY( ymd.month() == May );
+  VERIFY( ymd.day() >= day(10) );
+  VERIFY( weekday(ymd) == wd );
+}
+
+void
+test_last(std::string_view name)
+{
+  constexpr const char* templ = R"(# version test_{0}
+ # Zone  NAME  STDOFF  RULES  FORMAT  [UNTIL]
+ Zone    Test  0       -      Z1      2000 July {0}Mon
+               0       -      Z3
+  )";
+
+  std::string input = std::format(templ, name);
+  std::ofstream("tzdata.zi") << input;
+
+  const auto& db = reload_tzdb();
+  VERIFY( override_used ); // If this fails then XFAIL for the target.
+
+  std::string_view ver = db.version;
+  VERIFY( ver.starts_with("test_") );
+  ver.remove_prefix(5);
+  VERIFY( ver == name );
+
+  sys_info info = locate_zone("Test")->get_info(sys_days(2005y/January/1));
+  VERIFY( info.begin == sys_days(2000y/July/Monday[last]) );
+}
+
+void
+test_days()
+{
+  test_combinations(test_day, "monday", 1, Monday);
+  test_combinations(test_day, "tuesday", 2, Tuesday);
+  test_combinations(test_day, "wednesday", 1, Wednesday);
+  test_combinations(test_day, "thursday", 2, Thursday);
+  test_combinations(test_day, "friday", 1, Friday);
+  test_combinations(test_day, "saturday", 2, Saturday);
+  test_combinations(test_day, "sunday", 2, Sunday);
+
+  test_last("last");
+  test_last("LAST");
+}
+
+void
+test_month(std::string_view name, month m)
+{
+  constexpr const char* templ = R"(# version test_month_{0}
+ # Zone  NAME  STDOFF  RULES  FORMAT  [UNTIL]
+ Zone    Test  0       -      Z1      2000 {0} 13
+               0       -      Z2      2010 {0}
+               0       -      Z3
+  )";
+
+  std::string input = std::format(templ, name);
+  std::ofstream("tzdata.zi") << input;
+
+  const auto& db = reload_tzdb();
+  VERIFY( override_used ); // If this fails then XFAIL for the target.
+
+  std::string_view ver = db.version;
+  VERIFY( ver.starts_with("test_month_") );
+  ver.remove_prefix(11);
+  VERIFY( ver == name );
+
+  sys_info info = locate_zone("Test")->get_info(sys_days(2005y/January/1));
+  VERIFY( info.begin == sys_days(2000y/m/13) );
+  VERIFY( info.end == sys_days(2010y/m/1) );
+}
+
+void
+test_months()
+{
+  test_combinations(test_month, "january", 2, January);
+  test_combinations(test_month, "february", 1, February);
+  test_combinations(test_month, "march", 3, March);
+  test_combinations(test_month, "april", 2, April);
+  test_combinations(test_month, "may", 3, May);
+  test_combinations(test_month, "june", 3, June);
+  test_combinations(test_month, "july", 3, July);
+  test_combinations(test_month, "august", 3, August);
+  test_combinations(test_month, "september", 1, September);
+  test_combinations(test_month, "october", 1, October);
+  test_combinations(test_month, "november", 1, November);
+  test_combinations(test_month, "december", 1, December);
+}
+
+void
+test_year(std::string_view min, std::string_view max, std::string_view only)
+{
+  constexpr const char* templ = R"(# version {0}
+ # Rule  NAME  FROM  TO    -  IN   ON       AT    SAVE  LETTER/S
+ Rule    Rule  {1}   2020  -  Jan  10       0u    0     S
+ Rule    Rule  2000  {2}   -  Nov  12       0u    1     D
+ Rule    Rule  2010  {3}   -  Aug  11       0u    2     O
+ Rule    Rule  2020  {2}  -   Jan  13       0u    0     S
+ # Zone  NAME  STDOFF  RULES  FORMAT  [UNTIL]
+ Zone    Test  0       -      Z1      1800 Jan 1 0u
+               0       Rule   Z%s     3001 Jan 1 0u
+               0       -      Zl
+  )";
+
+  const std::string ver = std::format("test_{}_{}_{}", min, max, only);
+  std::string input = std::format(templ, ver, min, max, only);
+  std::ofstream("tzdata.zi") << input;
+
+  const auto& db = reload_tzdb();
+  VERIFY( override_used ); // If this fails then XFAIL for the target.
+  VERIFY( db.version == ver );
+
+  const time_zone* zone  = locate_zone("Test");
+
+  // min applies since 1900, check first transition
+  sys_info info = zone->get_info(sys_days(1900y/January/1));
+  VERIFY( info.begin == sys_days(1800y/January/1) );
+  VERIFY( info.end == sys_days(1900y/January/10) );
+
+  // Check 2010 only rule does not apply in 2009
+  info = zone->get_info(sys_days(2009y/February/1));
+  VERIFY( info.begin == sys_days(2009y/January/10) );
+  VERIFY( info.end == sys_days(2009y/November/12) );
+
+  // Check 2010 only rule in 2010
+  info = zone->get_info(sys_days(2010y/February/1));
+  VERIFY( info.begin == sys_days(2010y/January/10) );
+  VERIFY( info.end == sys_days(2010y/August/11) );
+
+  info = zone->get_info(sys_days(2010y/October/1));
+  VERIFY( info.begin == sys_days(2010y/August/11) );
+  VERIFY( info.end == sys_days(2010y/November/12) );
+
+  // Check 2010 only rule does not apply in 2011
+  info = zone->get_info(sys_days(2011y/February/1));
+  VERIFY( info.begin == sys_days(2011y/January/10) );
+  VERIFY( info.end == sys_days(2011y/November/12) );
+
+  // max rule applies forever
+  info = zone->get_info(sys_days(2500y/February/1));
+  VERIFY( info.begin == sys_days(2500y/January/13) );
+  VERIFY( info.end == sys_days(2500y/November/12) );
+
+  info = zone->get_info(sys_days(3000y/December/1));
+  VERIFY( info.begin == sys_days(3000y/November/12) );
+  VERIFY( info.end == sys_days(3001y/January/1) );
+}
+
+void
+test_years()
+{
+  test_year("min", "max", "only");
+  test_year("m", "m", "o");
+  test_year("MIN", "MAX", "ONLY");
+  test_year("M", "M", "O");
+}
+
+void
+test_time(char s, char u, char w, char d)
+{
+  constexpr const char* templ = R"(# version test_time_{0}{1}{2}{3}
+ # Zone  NAME  STDOFF  RULES  FORMAT  [UNTIL]
+ Zone    Test  1       0      Zs    2000 Jan 11 10{0}
+               3       1      Zu    2005 Feb 12 11{1}
+               5       1      Zw    2010 Mar 13 12{2}
+               7       1      Zd    2015 Apr 14 13{3}
+               9       0      Su    2020 May 15 14{1}
+               10      0      Sw    2025 Jun 16 15{2}
+               11      -      Zl
+  )";
+
+  std::string input = std::format(templ, s, u, w, d);
+  std::ofstream("tzdata.zi") << input;
+
+  const auto& db = reload_tzdb();
+  VERIFY( override_used ); // If this fails then XFAIL for the target.
+
+  std::string_view ver = db.version;
+  VERIFY( ver.starts_with("test_time_") );
+  VERIFY( ver[10] == s );
+  VERIFY( ver[11] == u );
+  VERIFY( ver[12] == w );
+  VERIFY( ver[13] == d );
+
+  const time_zone* zone  = locate_zone("Test");
+  sys_info info = zone->get_info(sys_days(2002y/January/1));
+  VERIFY( info.begin == sys_days(2000y/January/11) + 10h - 1h );
+  VERIFY( info.end == sys_days(2005y/February/12) + 11h );
+
+  info = zone->get_info(sys_days(2012y/January/1));
+  VERIFY( info.begin == sys_days(2010y/March/13) + 12h - 6h );
+  VERIFY( info.end == sys_days(2015y/April/14) + 13h - 8h );
+
+  info = zone->get_info(sys_days(2022y/January/1));
+  VERIFY( info.begin == sys_days(2020y/May/15) + 14h );
+  VERIFY( info.end == sys_days(2025y/June/16) + 15h - 10h );
+}
+
+void
+test_times()
+{
+  test_time('s', 'u', 'w', 'd');
+  test_time('S', 'U', 'W', 'D');
+  // Alternate spelling for Universal
+  test_time('s', 'g', 'w', 'd');
+  test_time('s', 'G', 'w', 'd');
+  test_time('s', 'z', 'w', 'd');
+  test_time('s', 'Z', 'w', 'd');
+}
+
+void
+test_line(std::string_view rule, std::string_view zone, std::string_view link)
+{
+  constexpr const char* templ = R"(# version {0}
+ # Rule  NAME  FROM  TO   -  IN   ON       AT    SAVE  LETTER/S
+ {1}     Rule  min   max  -  Jan  13       0u    0     D
+ {1}     Rule  min   max  -  Oct  12       0u    1     S
+ # Zone  NAME  STDOFF  RULES  FORMAT  [UNTIL]
+ {2}     Test  0       -      Z1      2000 Jan 13
+               0       Rule   Z%s
+ # Link  TARGET  LINK-NAME
+ {3}     Test    Link
+  )";
+
+  const std::string ver = std::format("test_{}_{}_{}", rule, zone, link);
+  std::string input = std::format(templ, ver, rule, zone, link);
+  std::ofstream("tzdata.zi") << input;
+
+  const auto& db = reload_tzdb();
+  VERIFY( override_used ); // If this fails then XFAIL for the target.
+  VERIFY( db.version == ver );
+
+  sys_info info = locate_zone("Test")->get_info(sys_days(2005y/February/1));
+  VERIFY( info.begin == sys_days(2005y/January/13) );
+  VERIFY( info.end == sys_days(2005y/October/12) );
+
+  const time_zone* target  = locate_zone("Link");
+  VERIFY( target->name() == "Test" );
+}
+
+void
+test_lines()
+{
+  test_line("rule", "zone", "link");
+  test_line("r", "z", "l");
+  test_line("RULE", "ZONE", "LINK");
+  test_line("R", "Z", "L");
+}
+
+int main()
+{
+  test_days();
+  test_months();
+  test_years();
+  test_times();
+  test_lines();
+}


More information about the Libstdc++-cvs mailing list