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 | |
10 | namespace |
11 | { |
12 | |
13 | Poco::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 | |
27 | std::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 | |
140 | DateLUT::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 | |
148 | const 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 | |
159 | DateLUT & DateLUT::getInstance() |
160 | { |
161 | static DateLUT ret; |
162 | return ret; |
163 | } |
164 |