1#include <common/DateLUT.h>
2
3#include <filesystem>
4#include <Poco/Exception.h>
5#include <Poco/SHA1Engine.h>
6#include <Poco/DigestStream.h>
7#include <fstream>
8
9
10namespace
11{
12
13Poco::DigestEngine::Digest calcSHA1(const std::string & path)
14{
15 std::ifstream stream(path);
16 if (!stream)
17 throw Poco::Exception("Error while opening file: '" + path + "'.");
18 Poco::SHA1Engine digest_engine;
19 Poco::DigestInputStream digest_stream(digest_engine, stream);
20 digest_stream.ignore(std::numeric_limits<std::streamsize>::max());
21 if (!stream.eof())
22 throw Poco::Exception("Error while reading file: '" + path + "'.");
23 return digest_engine.digest();
24}
25
26
27std::string determineDefaultTimeZone()
28{
29 namespace fs = std::filesystem;
30
31 const char * tzdir_env_var = std::getenv("TZDIR");
32 fs::path tz_database_path = tzdir_env_var ? tzdir_env_var : "/usr/share/zoneinfo/";
33
34 fs::path tz_file_path;
35 std::string error_prefix;
36 const char * tz_env_var = std::getenv("TZ");
37
38 /// In recent tzdata packages some files now are symlinks and canonical path resolution
39 /// may give wrong timezone names - store the name as it is, if possible.
40 std::string tz_name;
41
42 if (tz_env_var)
43 {
44 error_prefix = std::string("Could not determine time zone from TZ variable value: '") + tz_env_var + "': ";
45
46 if (*tz_env_var == ':')
47 ++tz_env_var;
48
49 tz_file_path = tz_env_var;
50 tz_name = tz_env_var;
51 }
52 else
53 {
54 error_prefix = "Could not determine local time zone: ";
55 tz_file_path = "/etc/localtime";
56
57 /// No TZ variable and no tzdata installed (e.g. Docker)
58 if (!fs::exists(tz_file_path))
59 return "UTC";
60
61 /// Read symlink but not transitive.
62 /// Example:
63 /// /etc/localtime -> /usr/share/zoneinfo//UTC
64 /// /usr/share/zoneinfo//UTC -> UCT
65 /// But the preferred time zone name is pointed by the first link (UTC), and the second link is just an internal detail.
66 if (fs::is_symlink(tz_file_path))
67 {
68 tz_file_path = fs::read_symlink(tz_file_path);
69 /// If it's relative - make it absolute.
70 if (tz_file_path.is_relative())
71 tz_file_path = (fs::path("/etc/") / tz_file_path).lexically_normal();
72 }
73 }
74
75 try
76 {
77 tz_database_path = fs::canonical(tz_database_path);
78
79 /// The tzdata file exists. If it is inside the tz_database_dir,
80 /// then the relative path is the time zone id.
81 {
82 fs::path relative_path = tz_file_path.lexically_relative(tz_database_path);
83
84 if (!relative_path.empty() && *relative_path.begin() != ".." && *relative_path.begin() != ".")
85 return tz_name.empty() ? relative_path.string() : tz_name;
86 }
87
88 /// Try the same with full symlinks resolution
89 {
90 if (!tz_file_path.is_absolute())
91 tz_file_path = tz_database_path / tz_file_path;
92
93 tz_file_path = fs::canonical(tz_file_path);
94
95 fs::path relative_path = tz_file_path.lexically_relative(tz_database_path);
96 if (!relative_path.empty() && *relative_path.begin() != ".." && *relative_path.begin() != ".")
97 return tz_name.empty() ? relative_path.string() : tz_name;
98 }
99
100 /// The file is not inside the tz_database_dir, so we hope that it was copied (not symlinked)
101 /// and try to find the file with exact same contents in the database.
102
103 size_t tzfile_size = fs::file_size(tz_file_path);
104 Poco::SHA1Engine::Digest tzfile_sha1 = calcSHA1(tz_file_path.string());
105
106 fs::recursive_directory_iterator begin(tz_database_path);
107 fs::recursive_directory_iterator end;
108 for (auto candidate_it = begin; candidate_it != end; ++candidate_it)
109 {
110 const auto & path = candidate_it->path();
111 if (path.filename() == "posix" || path.filename() == "right")
112 {
113 /// Some timezone databases contain copies of toplevel tzdata files in the posix/ directory
114 /// and tzdata files with leap seconds in the right/ directory. Skip them.
115 candidate_it.disable_recursion_pending();
116 continue;
117 }
118
119 if (!fs::is_regular_file(*candidate_it) || path.filename() == "localtime")
120 continue;
121
122 if (fs::file_size(path) == tzfile_size && calcSHA1(path.string()) == tzfile_sha1)
123 return path.lexically_relative(tz_database_path).string();
124 }
125 }
126 catch (const Poco::Exception & ex)
127 {
128 throw Poco::Exception(error_prefix + ex.message(), ex);
129 }
130 catch (const std::exception & ex)
131 {
132 throw Poco::Exception(error_prefix + ex.what());
133 }
134
135 throw Poco::Exception(error_prefix + "custom time zone file used.");
136}
137
138}
139
140DateLUT::DateLUT()
141{
142 /// Initialize the pointer to the default DateLUTImpl.
143 std::string default_time_zone = determineDefaultTimeZone();
144 default_impl.store(&getImplementation(default_time_zone), std::memory_order_release);
145}
146
147
148const DateLUTImpl & DateLUT::getImplementation(const std::string & time_zone) const
149{
150 std::lock_guard<std::mutex> lock(mutex);
151
152 auto it = impls.emplace(time_zone, nullptr).first;
153 if (!it->second)
154 it->second = std::make_unique<DateLUTImpl>(time_zone);
155
156 return *it->second;
157}
158
159DateLUT & DateLUT::getInstance()
160{
161 static DateLUT ret;
162 return ret;
163}
164