File: base-utility.cpp

package info (click to toggle)
icinga2 2.15.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 20,032 kB
  • sloc: cpp: 97,877; sql: 3,261; cs: 1,636; yacc: 1,584; sh: 1,009; ansic: 890; lex: 420; python: 80; makefile: 62; javascript: 12
file content (316 lines) | stat: -rw-r--r-- 14,628 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */

#include "base/utility.hpp"
#include "test/utils.hpp"
#include <chrono>
#include <BoostTestTargetConfig.h>

#ifdef _WIN32
# include <windows.h>
# include <shellapi.h>
#endif /* _WIN32 */

using namespace icinga;

BOOST_AUTO_TEST_SUITE(base_utility)

BOOST_AUTO_TEST_CASE(parse_version)
{
	BOOST_CHECK(Utility::ParseVersion("2.11.0-0.rc1.1") == "2.11.0");
	BOOST_CHECK(Utility::ParseVersion("v2.10.5") == "2.10.5");
	BOOST_CHECK(Utility::ParseVersion("r2.11.1") == "2.11.1");
	BOOST_CHECK(Utility::ParseVersion("v2.11.0-rc1-58-g7c1f716da") == "2.11.0");

	BOOST_CHECK(Utility::ParseVersion("v2.11butactually3.0") == "v2.11butactually3.0");
}

BOOST_AUTO_TEST_CASE(compare_version)
{
	BOOST_CHECK(Utility::CompareVersion("2.10.5", Utility::ParseVersion("v2.10.4")) < 0);
	BOOST_CHECK(Utility::CompareVersion("2.11.0", Utility::ParseVersion("2.11.0-0")) == 0);
	BOOST_CHECK(Utility::CompareVersion("2.10.5", Utility::ParseVersion("2.11.0-0.rc1.1")) > 0);
}

BOOST_AUTO_TEST_CASE(comparepasswords_works)
{
	BOOST_CHECK(Utility::ComparePasswords("", ""));

	BOOST_CHECK(!Utility::ComparePasswords("x", ""));
	BOOST_CHECK(!Utility::ComparePasswords("", "x"));

	BOOST_CHECK(Utility::ComparePasswords("x", "x"));
	BOOST_CHECK(!Utility::ComparePasswords("x", "y"));

	BOOST_CHECK(Utility::ComparePasswords("abcd", "abcd"));
	BOOST_CHECK(!Utility::ComparePasswords("abc", "abcd"));
	BOOST_CHECK(!Utility::ComparePasswords("abcde", "abcd"));
}

BOOST_AUTO_TEST_CASE(comparepasswords_issafe)
{
	using std::chrono::duration_cast;
	using std::chrono::microseconds;
	using std::chrono::steady_clock;

	String a, b;

	a.Append(200000001, 'a');
	b.Append(200000002, 'b');

	auto start1 (steady_clock::now());

	Utility::ComparePasswords(a, a);

	auto duration1 (steady_clock::now() - start1);

	auto start2 (steady_clock::now());

	Utility::ComparePasswords(a, b);

	auto duration2 (steady_clock::now() - start2);

	double diff = (double)duration_cast<microseconds>(duration1).count() / (double)duration_cast<microseconds>(duration2).count();
	BOOST_WARN(0.9 <= diff && diff <= 1.1);
}

BOOST_AUTO_TEST_CASE(validateutf8)
{
	BOOST_CHECK(Utility::ValidateUTF8("") == "");
	BOOST_CHECK(Utility::ValidateUTF8("a") == "a");
	BOOST_CHECK(Utility::ValidateUTF8("\xC3") == "\xEF\xBF\xBD");
	BOOST_CHECK(Utility::ValidateUTF8("\xC3\xA4") == "\xC3\xA4");
}

BOOST_AUTO_TEST_CASE(EscapeCreateProcessArg)
{
#ifdef _WIN32
	using convert = std::wstring_convert<std::codecvt<wchar_t, char, std::mbstate_t>, wchar_t>;

	std::vector<std::string> testdata = {
		R"(foobar)",
		R"(foo bar)",
		R"(foo"bar)",
		R"("foo bar")",
		R"(" \" \\" \\\" \\\\")",
		R"( !"#$$%&'()*+,-./09:;<=>?@AZ[\]^_`az{|}~ " \" \\" \\\" \\\\")",
		"'foo\nbar'",
	};

	for (const auto& t : testdata) {
		// Prepend some fake exec name as the first argument is handled differently.
		std::string escaped = "some.exe " + Utility::EscapeCreateProcessArg(t);
		int argc;
		std::shared_ptr<LPWSTR> argv(CommandLineToArgvW(convert{}.from_bytes(escaped.c_str()).data(), &argc), LocalFree);
		BOOST_CHECK_MESSAGE(argv != nullptr, "CommandLineToArgvW() should not return nullptr for " << t);
		BOOST_CHECK_MESSAGE(argc == 2, "CommandLineToArgvW() should find 2 arguments for " << t);
		if (argc >= 2) {
			std::string unescaped = convert{}.to_bytes(argv.get()[1]);
			BOOST_CHECK_MESSAGE(unescaped == t,
				"CommandLineToArgvW() should return original value for " << t << " (got: " << unescaped << ")");
		}
	}
#endif /* _WIN32 */
}

BOOST_AUTO_TEST_CASE(TruncateUsingHash)
{
	/*
	 * Note: be careful when changing the output of TruncateUsingHash as it is used to derive file names that should not
	 * change between versions or would need special handling if they do (/var/lib/icinga2/api/packages/_api).
	 */

	/* minimum allowed value for maxLength template parameter */
	BOOST_CHECK_EQUAL(Utility::TruncateUsingHash<44>(std::string(64, 'a')),
		"a...0098ba824b5c16427bd7a1122a5a442a25ec644d");

	BOOST_CHECK_EQUAL(Utility::TruncateUsingHash<80>(std::string(100, 'a')),
		std::string(37, 'a') + "...7f9000257a4918d7072655ea468540cdcbd42e0c");

	/* short enough values should not be truncated */
	BOOST_CHECK_EQUAL(Utility::TruncateUsingHash<80>(""), "");
	BOOST_CHECK_EQUAL(Utility::TruncateUsingHash<80>(std::string(60, 'a')), std::string(60, 'a'));
	BOOST_CHECK_EQUAL(Utility::TruncateUsingHash<80>(std::string(79, 'a')), std::string(79, 'a'));

	/* inputs of maxLength are hashed to avoid collisions */
	BOOST_CHECK_EQUAL(Utility::TruncateUsingHash<80>(std::string(80, 'a')),
		std::string(37, 'a') + "...86f33652fcffd7fa1443e246dd34fe5d00e25ffd");
}

BOOST_AUTO_TEST_CASE(FormatDateTime) {
	using time_t_limit = std::numeric_limits<time_t>;
	using double_limit = std::numeric_limits<double>;
	using boost::numeric::negative_overflow;
	using boost::numeric::positive_overflow;

	// Helper to repeat a given string a number of times.
	auto repeat = [](const std::string& s, size_t n) {
		std::ostringstream stream;
		for (size_t i = 0; i < n; ++i) {
			stream << s;
		}
		return stream.str();
	};

	// Valid inputs.
	const double ts = 1136214245.0; // 2006-01-02 15:04:05 UTC
	BOOST_CHECK_EQUAL("2006-01-02 15:04:05", Utility::FormatDateTime("%F %T", ts));
	BOOST_CHECK_EQUAL("2006", Utility::FormatDateTime("%Y", ts));
	BOOST_CHECK_EQUAL("2006#2006", Utility::FormatDateTime("%Y#%Y", ts));
	BOOST_CHECK_EQUAL("%", Utility::FormatDateTime("%%", ts));
	BOOST_CHECK_EQUAL("%Y", Utility::FormatDateTime("%%Y", ts));
	BOOST_CHECK_EQUAL("", Utility::FormatDateTime("", ts));
	BOOST_CHECK_EQUAL("1970-01-01 00:00:00", Utility::FormatDateTime("%F %T", 0.0));
	BOOST_CHECK_EQUAL("2038-01-19 03:14:07", Utility::FormatDateTime("%F %T", 2147483647.0)); // 2^31 - 1
	if constexpr (sizeof(time_t) > sizeof(int32_t)) {
		BOOST_CHECK_EQUAL("2100-03-14 13:37:42", Utility::FormatDateTime("%F %T", 4108714662.0)); // Past year 2038
	} else {
		BOOST_WARN_MESSAGE(false, "skipping test with past 2038 input due to 32 bit time_t");
	}

	// Negative (pre-1970) timestamps.
#ifdef _MSC_VER
	// localtime_s() on Windows doesn't seem to like them and always errors out.
	BOOST_CHECK_THROW(Utility::FormatDateTime("%F %T", -1.0), posix_error);
	BOOST_CHECK_THROW(Utility::FormatDateTime("%F %T", -2147483648.0), posix_error); // -2^31
#else /* _MSC_VER */
	BOOST_CHECK_EQUAL("1969-12-31 23:59:59", Utility::FormatDateTime("%F %T", -1.0));
	BOOST_CHECK_EQUAL("1901-12-13 20:45:52", Utility::FormatDateTime("%F %T", -2147483648.0)); // -2^31
#endif /* _MSC_VER */

	// Values right at the limits of time_t.
	//
	// With 64 bit time_t, there may not be an exact double representation of its min/max value, std::nextafter() is
	// used to move the value towards 0 so that it's within the range of doubles that can be represented as time_t.
	//
	// These are expected to result in an error due to the intermediate struct tm not being able to represent these
	// timestamps, so localtime_r() returns EOVERFLOW which makes the implementation throw an exception.
	if constexpr (sizeof(time_t) > sizeof(int32_t)) {
		BOOST_CHECK_THROW(Utility::FormatDateTime("%Y", std::nextafter(time_t_limit::min(), 0)), posix_error);
		BOOST_CHECK_THROW(Utility::FormatDateTime("%Y", std::nextafter(time_t_limit::max(), 0)), posix_error);
	} else {
		BOOST_WARN_MESSAGE(false, "skipping test for struct tm overflow due to 32 bit time_t");
	}

	// Excessive format strings can result in something too large for the buffer, errors out to the empty string.
	// Note: both returning the proper result or throwing an exception would be fine too, unfortunately, that's
	// not really possible due to limitations in strftime() error handling, see comment in the implementation.
	BOOST_CHECK_EQUAL("", Utility::FormatDateTime(repeat("%Y", 1000).c_str(), ts));

	// Invalid format strings.
	for (const char* format : {"%", "x % y", "x %! y"}) {
		std::string result = Utility::FormatDateTime(format, ts);

		// Implementations of strftime() seem to either keep invalid format specifiers and return them in the output, or
		// treat them as an error which our implementation currently maps to the empty string due to strftime() not
		// properly reporting errors. If this limitation of our implementation is lifted, other behavior like throwing
		// an exception would also be valid.

		std::string percentLessOutput(format);
		// `strftime(3)` seems to return the provided invalid format specifiers on all Platforms as documented above,
		// i.e. even on macOS, but the macOS/*BSD libc implementations of `strftime(3)` appears to behave differently
		// and causes the `%` character not to be populated into the results buffer if invalid format specifiers such
		// as `"x %! y"` are given. If such specifiers are provided, the output will contain `x ! y` instead of the
		// given invalid format specifiers.
		percentLessOutput.erase(std::remove(percentLessOutput.begin(), percentLessOutput.end(), '%'), percentLessOutput.end());

		BOOST_CHECK_MESSAGE(result.empty() || result == format || result == percentLessOutput,
			"FormatDateTime(" << std::quoted(format) << ", " << ts << ") = " << std::quoted(result) <<
			" should be one of [\"\", " << std::quoted(format) << "]");
	}

	// Out of range timestamps.
	//
	// At the limits of a 64 bit time_t, doubles can no longer represent each integer value, so a simple x+1 or x-1 can
	// have x as the result, hence std::nextafter() is used to get the next representable value. However, around the
	// limits of a 32 bit time_t, doubles still can represent decimal places and less than 1 is added or subtracted by
	// std::nextafter() and casting back to time_t simply results in the limit again, so std::ceil()/std::floor() is
	// used to round it to the next integer value that is actually out of range.
	double negative_out_of_range = std::floor(std::nextafter(time_t_limit::min(), -double_limit::infinity()));
	double positive_out_of_range = std::ceil(std::nextafter(time_t_limit::max(), +double_limit::infinity()));
	BOOST_CHECK_THROW(Utility::FormatDateTime("%Y", negative_out_of_range), negative_overflow);
	BOOST_CHECK_THROW(Utility::FormatDateTime("%Y", positive_out_of_range), positive_overflow);
}

BOOST_AUTO_TEST_CASE(NormalizeTm)
{
	GlobalTimezoneFixture tz(GlobalTimezoneFixture::TestTimezoneWithDST);

	auto normalize = [](const std::string_view& input) {
		tm t = make_tm(std::string(input));
		return Utility::NormalizeTm(&t);
	};

	auto is_dst = [](const std::string_view& input) {
		tm t = make_tm(std::string(input));
		Utility::NormalizeTm(&t);
		BOOST_CHECK_GE(t.tm_isdst, 0);
		return t.tm_isdst > 0;
	};

	// The whole day 2021-01-01 uses PST (24h day)
	BOOST_CHECK(!is_dst("2021-01-01 10:00:00"));
	BOOST_CHECK_EQUAL(normalize("2021-01-01 10:00:00"), 1609524000);
	BOOST_CHECK_EQUAL(normalize("2021-01-01 10:00:00 PST"), 1609524000);
	BOOST_CHECK_EQUAL(normalize("2021-01-01 11:00:00 PDT"), 1609524000); // normalized to 10:00 PST
	BOOST_CHECK_EQUAL(normalize("2021-01-02 00:00:00") - normalize("2021-01-01 00:00:00"), 24*60*60);

	// The whole day 2021-07-01 uses PDT (24h day)
	BOOST_CHECK(is_dst("2021-07-01 10:00:00"));
	BOOST_CHECK_EQUAL(normalize("2021-07-01 10:00:00"), 1625158800);
	BOOST_CHECK_EQUAL(normalize("2021-07-01 10:00:00 PDT"), 1625158800);
	BOOST_CHECK_EQUAL(normalize("2021-07-01 09:00:00 PST"), 1625158800); // normalized to 10:00 PDT
	BOOST_CHECK_EQUAL(normalize("2021-07-02 00:00:00") - normalize("2021-07-01 00:00:00"), 24*60*60);

	// On 2021-03-14, PST changes to PDT (23h day)
	BOOST_CHECK(!is_dst("2021-03-14 00:00:00"));
	BOOST_CHECK(is_dst("2021-03-14 23:59:59"));
	BOOST_CHECK_EQUAL(normalize("2021-03-15 00:00:00") - normalize("2021-03-14 00:00:00"), 23*60*60);

	BOOST_CHECK_EQUAL(normalize("2021-03-14 01:59:59 PST"), 1615715999);
	// The following three times do not exist on that day in that timezone.
	// They are interpreted as UTC-8, which is the offset of PST.
	BOOST_CHECK_EQUAL(normalize("2021-03-14 02:00:00 PST"), 1615716000);
	BOOST_CHECK_EQUAL(normalize("2021-03-14 02:30:00 PST"), 1615717800);
	BOOST_CHECK_EQUAL(normalize("2021-03-14 03:00:00 PST"), 1615719600);

	BOOST_CHECK_EQUAL(normalize("2021-03-14 03:00:00 PDT"), 1615716000);
	// The following three times do not exist on that day in that timezone.
	// They are interpreted as UTC-7, which is the offset of PDT.
	BOOST_CHECK_EQUAL(normalize("2021-03-14 01:59:59 PDT"), 1615712399);
	BOOST_CHECK_EQUAL(normalize("2021-03-14 02:00:00 PDT"), 1615712400);
	BOOST_CHECK_EQUAL(normalize("2021-03-14 02:30:00 PDT"), 1615714200);

	BOOST_CHECK_EQUAL(normalize("2021-03-14 01:59:59"), 1615715999);
	BOOST_CHECK_EQUAL(normalize("2021-03-14 03:00:00"), 1615716000);
	// The following two times don't exist on that day, they are within the hour that is skipped.
	// They are interpreted as UTC-8 (offset of PST) and then normalized to PDT.
	BOOST_CHECK_EQUAL(normalize("2021-03-14 02:00:00"), 1615716000);
	BOOST_CHECK_EQUAL(normalize("2021-03-14 02:30:00"), 1615717800);

	// On 2021-11-07, PDT changes to PST (25h day)
	BOOST_CHECK(is_dst("2021-11-07 00:00:00"));
	BOOST_CHECK(!is_dst("2021-11-07 23:59:59"));
	BOOST_CHECK_EQUAL(normalize("2021-11-08 00:00:00") - normalize("2021-11-07 00:00:00"), 25*60*60);

	BOOST_CHECK_EQUAL(normalize("2021-11-07 00:59:59 PDT"), 1636271999);
	BOOST_CHECK_EQUAL(normalize("2021-11-07 01:00:00 PDT"), 1636272000);
	BOOST_CHECK_EQUAL(normalize("2021-11-07 01:30:00 PDT"), 1636273800);
	BOOST_CHECK_EQUAL(normalize("2021-11-07 01:59:59 PDT"), 1636275599);
	// The following time does not exist on that day in that timezone, it's interpreted as 01:00:00 PST.
	BOOST_CHECK_EQUAL(normalize("2021-11-07 02:00:00 PDT"), 1636275600);

	// The following time does not exist on that day in that timezone, it's interpreted as 01:59:59 PDT.
	BOOST_CHECK_EQUAL(normalize("2021-11-07 00:59:59 PST"), 1636275599);
	BOOST_CHECK_EQUAL(normalize("2021-11-07 01:00:00 PST"), 1636275600);
	BOOST_CHECK_EQUAL(normalize("2021-11-07 01:30:00 PST"), 1636277400);
	BOOST_CHECK_EQUAL(normalize("2021-11-07 01:59:59 PST"), 1636279199);
	BOOST_CHECK_EQUAL(normalize("2021-11-07 02:00:00 PST"), 1636279200);

	BOOST_CHECK_EQUAL(normalize("2021-11-07 00:59:59"), 1636271999); // unambiguous: PDT
	BOOST_CHECK_EQUAL(normalize("2021-11-07 01:00:00"), 1636272000); // exists twice, interpreted as PDT
	BOOST_CHECK_EQUAL(normalize("2021-11-07 01:30:00"), 1636273800); // exists twice, interpreted as PDT
	BOOST_CHECK_EQUAL(normalize("2021-11-07 01:59:59"), 1636275599); // exists twice, interpreted as PDT
	BOOST_CHECK_EQUAL(normalize("2021-11-07 02:00:00"), 1636279200); // unambiguous: PST
}

BOOST_AUTO_TEST_SUITE_END()