From dca680eeac8304b38c79ada68690481807a4ade2 Mon Sep 17 00:00:00 2001 From: Jonathan M Davis Date: Sat, 9 May 2026 01:34:10 -0600 Subject: [PATCH] Fix #11000: getInstalledTZNames can't handle a trailing slash. Normally, on most systems, PosixTimeZone.getInstalledTZNames uses /usr/share/zoneinfo/ for the time zone database directory, but it can be given a different directory, and it will use what TZDIR has been set to if it's been set (since the function used as the default argument checks TZDIR). So, while the bug refers specifically to TZDIR, that's just one way that the alternate time zone database directory can be passed to getInstalledTZNames. If getInstalledTZNames is given a directory name which does not end with a slash, then the logic for stripping off the time zone database directory name from the full paths to each time zone file leaves a slash on the front of the time zone name, which then doesn't work correctly, since it's not supposed to start with a slash. So, this fixes it so that the time zone names don't end up with slashes due to how many slashes the time zone database directory name does or doesn't have. I tested the subName argument as part of this in case it mattered and was somewhat horrified to realize that it had not actually been tested by any of the existing tests. So, I guess that I screwed up on that count. Fortunately, it seems to work correctly. --- std/datetime/timezone.d | 117 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/std/datetime/timezone.d b/std/datetime/timezone.d index c58af7f333b..6b4e4f4f6f2 100644 --- a/std/datetime/timezone.d +++ b/std/datetime/timezone.d @@ -2474,6 +2474,10 @@ public: { auto tzName = de.name[tzDatabaseDir.length .. $]; + // For the case where tzDatabaseDir does not have a trailing slash. + if (tzName.length > 1 && tzName[0] == '/') + tzName = tzName[1 .. $]; + if (!tzName.extension().empty || !tzName.startsWith(subName) || baseName(tzName) == "leapseconds" || @@ -2537,6 +2541,119 @@ public: } } + // https://github.com/dlang/phobos/issues/11000 + version (Posix) @safe unittest + { + version (Android) + {} + else + { + import std.algorithm.searching : canFind; + import std.file : chdir, copy, exists, getcwd, mkdirRecurse, rmdirRecurse, tempDir; + import std.path : buildPath; + + immutable baseDir = buildPath(tempDir, "tztest"); + immutable tzDir = buildPath(baseDir, "tz"); + immutable tzDirSlash = buildPath(baseDir, "tz/"); + immutable tzDirDoubleSlash = buildPath(baseDir, "tz//"); + assert(tzDirSlash[$ - 1] == '/'); // just in case buildPath ever strips the slash + + scope(failure) if (baseDir.exists) rmdirRecurse(baseDir); + + mkdirRecurse(buildPath(tzDir, "America")); + mkdirRecurse(buildPath(tzDir, "Europe")); + + copy(buildPath(defaultTZDatabaseDir, "America/Denver"), + buildPath(tzDir, "America/Denver")); + copy(buildPath(defaultTZDatabaseDir, "America/Denver"), + buildPath(tzDir, "America/Chicago")); + copy(buildPath(defaultTZDatabaseDir, "Europe/London"), + buildPath(tzDir, "Europe/London")); + copy(buildPath(defaultTZDatabaseDir, "UTC"), + buildPath(tzDir, "UTC")); + + foreach (dir; [tzDir, tzDirSlash, tzDirDoubleSlash]) + { + { + auto names = getInstalledTZNames("", dir); + assert(names.length == 4); + assert(names.canFind("America/Denver")); + assert(names.canFind("America/Chicago")); + assert(names.canFind("Europe/London")); + assert(names.canFind("UTC")); + } + { + auto names = getInstalledTZNames("America", dir); + assert(names.length == 2); + assert(names.canFind("America/Denver")); + assert(names.canFind("America/Chicago")); + } + } + + immutable cwd = getcwd(); + scope(exit) chdir(cwd); + + chdir(baseDir); + foreach (dir; ["tz", "tz/", "tz///"]) + { + { + auto names = getInstalledTZNames("", dir); + assert(names.length == 4); + assert(names.canFind("America/Denver")); + assert(names.canFind("America/Chicago")); + assert(names.canFind("Europe/London")); + assert(names.canFind("UTC")); + } + { + auto names = getInstalledTZNames("America", dir); + assert(names.length == 2); + assert(names.canFind("America/Denver")); + assert(names.canFind("America/Chicago")); + } + } + + immutable other = buildPath(baseDir, "other"); + mkdirRecurse(other); + chdir(other); + foreach (dir; ["../tz", "../tz/", "..///tz/////"]) + { + { + auto names = getInstalledTZNames("", dir); + assert(names.length == 4); + assert(names.canFind("America/Denver")); + assert(names.canFind("America/Chicago")); + assert(names.canFind("Europe/London")); + assert(names.canFind("UTC")); + } + { + auto names = getInstalledTZNames("America", dir); + assert(names.length == 2); + assert(names.canFind("America/Denver")); + assert(names.canFind("America/Chicago")); + } + } + + chdir(tzDir); + foreach (dir; [".", "./", ".///"]) + { + { + auto names = getInstalledTZNames("", dir); + assert(names.length == 4); + assert(names.canFind("America/Denver")); + assert(names.canFind("America/Chicago")); + assert(names.canFind("Europe/London")); + assert(names.canFind("UTC")); + } + { + auto names = getInstalledTZNames("America", dir); + assert(names.length == 2); + assert(names.canFind("America/Denver")); + assert(names.canFind("America/Chicago")); + } + } + } + } + private: