mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-09-13 05:38:05 -04:00
469 lines
15 KiB
C++
469 lines
15 KiB
C++
/*
|
|
* This file is part of the ZoneMinder Project. See AUTHORS file for Copyright information
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the
|
|
* Free Software Foundation; either version 2 of the License, or (at your
|
|
* option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
* more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "zm_catch2.h"
|
|
|
|
#include "zm_utils.h"
|
|
#include <sstream>
|
|
|
|
TEST_CASE("Trim") {
|
|
REQUIRE(Trim("", "") == "");
|
|
REQUIRE(Trim("test", "") == "test");
|
|
REQUIRE(Trim(" ", "") == " ");
|
|
|
|
REQUIRE(Trim("\"test", "\"") == "test");
|
|
REQUIRE(Trim("test\"", "\"") == "test");
|
|
REQUIRE(Trim("\"test\"", "\"") == "test");
|
|
|
|
REQUIRE(Trim("te\"st", "\"") == "te\"st");
|
|
REQUIRE(Trim("\"te\"st\"", "\"") == "te\"st");
|
|
}
|
|
|
|
TEST_CASE("TrimSpaces") {
|
|
REQUIRE(TrimSpaces(" ") == "");
|
|
|
|
REQUIRE(TrimSpaces("test") == "test");
|
|
REQUIRE(TrimSpaces(" test ") == "test");
|
|
REQUIRE(TrimSpaces(" test ") == "test");
|
|
REQUIRE(TrimSpaces(" test ") == "test");
|
|
REQUIRE(TrimSpaces(" test") == "test");
|
|
REQUIRE(TrimSpaces("\ttest") == "test");
|
|
REQUIRE(TrimSpaces("test\t") == "test");
|
|
REQUIRE(TrimSpaces("\ttest\t") == "test");
|
|
REQUIRE(TrimSpaces(" test\t") == "test");
|
|
REQUIRE(TrimSpaces("\ttest ") == "test");
|
|
REQUIRE(TrimSpaces("\t test \t") == "test");
|
|
|
|
REQUIRE(TrimSpaces("\t te st \t") == "te st");
|
|
}
|
|
|
|
TEST_CASE("ReplaceAll") {
|
|
REQUIRE(ReplaceAll("", "", "") == "");
|
|
|
|
REQUIRE(ReplaceAll("a", "", "b") == "a");
|
|
REQUIRE(ReplaceAll("a", "a", "b") == "b");
|
|
REQUIRE(ReplaceAll("a", "b", "c") == "a");
|
|
|
|
REQUIRE(ReplaceAll("aa", "a", "b") == "bb");
|
|
REQUIRE(ReplaceAll("aba", "a", "c") == "cbc");
|
|
|
|
REQUIRE(ReplaceAll("aTOKENa", "TOKEN", "VAL") == "aVALa");
|
|
REQUIRE(ReplaceAll("aTOKENaTOKEN", "TOKEN", "VAL") == "aVALaVAL");
|
|
}
|
|
|
|
TEST_CASE("StartsWith") {
|
|
REQUIRE(StartsWith("", "") == true);
|
|
|
|
REQUIRE(StartsWith("test", "test") == true);
|
|
REQUIRE(StartsWith("test=abc", "test") == true);
|
|
REQUIRE(StartsWith(" test=abc", "test") == false);
|
|
}
|
|
|
|
TEST_CASE("Split (char delimiter)") {
|
|
std::vector<std::string> items = Split("", ' ');
|
|
REQUIRE(items == std::vector<std::string>{""});
|
|
|
|
items = Split("abc def ghi", ' ');
|
|
REQUIRE(items == std::vector<std::string>{"abc", "def", "ghi"});
|
|
|
|
items = Split("abc,def,,ghi", ',');
|
|
REQUIRE(items == std::vector<std::string>{"abc", "def", "", "ghi"});
|
|
}
|
|
|
|
TEST_CASE("Split (string delimiter)") {
|
|
std::vector<std::string> items;
|
|
|
|
items = Split("", "");
|
|
REQUIRE(items == std::vector<std::string>{""});
|
|
|
|
items = Split("", " ");
|
|
REQUIRE(items == std::vector<std::string>{""});
|
|
|
|
items = Split("", " \t");
|
|
REQUIRE(items == std::vector<std::string>{""});
|
|
|
|
items = Split("", " \t");
|
|
REQUIRE(items == std::vector<std::string>{""});
|
|
|
|
items = Split(" ", " ");
|
|
REQUIRE(items.size() == 0);
|
|
|
|
items = Split(" ", " ");
|
|
REQUIRE(items.size() == 0);
|
|
|
|
items = Split(" ", " \t");
|
|
REQUIRE(items.size() == 0);
|
|
|
|
items = Split("a b", "");
|
|
REQUIRE(items == std::vector<std::string>{"a b"});
|
|
|
|
items = Split("a b", " ");
|
|
REQUIRE(items == std::vector<std::string>{"a", "b"});
|
|
|
|
items = Split("a \tb", " \t");
|
|
REQUIRE(items == std::vector<std::string>{"a", "b"});
|
|
|
|
items = Split(" a \tb ", " \t");
|
|
REQUIRE(items == std::vector<std::string>{"a", "b"});
|
|
|
|
items = Split(" a=b ", "=");
|
|
REQUIRE(items == std::vector<std::string>{" a", "b "});
|
|
|
|
items = Split(" a=b ", " =");
|
|
REQUIRE(items == std::vector<std::string>{"a", "b"});
|
|
|
|
items = Split("a b c", " ", 2);
|
|
REQUIRE(items == std::vector<std::string>{"a", "b c"});
|
|
|
|
// LibvlcCamera splits the monitor Options field this way. The field is a
|
|
// textarea, so an option may be on its own line rather than after a comma,
|
|
// and runs of separators (a blank line, or crlf) must not become entries.
|
|
items = Split("--rate=1\n--no-audio", kOptionSeparators);
|
|
REQUIRE(items == std::vector<std::string>{"--rate=1", "--no-audio"});
|
|
|
|
items = Split("--rate=1,--no-audio", kOptionSeparators);
|
|
REQUIRE(items == std::vector<std::string>{"--rate=1", "--no-audio"});
|
|
|
|
items = Split("--rate=1\r\n\r\n--no-audio\n", kOptionSeparators);
|
|
REQUIRE(items == std::vector<std::string>{"--rate=1", "--no-audio"});
|
|
}
|
|
|
|
TEST_CASE("Join") {
|
|
REQUIRE(Join({}, "") == "");
|
|
REQUIRE(Join({}, " ") == "");
|
|
REQUIRE(Join({""}, "") == "");
|
|
REQUIRE(Join({"a"}, "") == "a");
|
|
REQUIRE(Join({"a"}, ",") == "a");
|
|
REQUIRE(Join({"a", "b"}, ",") == "a,b");
|
|
REQUIRE(Join({"a", "b"}, "") == "ab");
|
|
}
|
|
|
|
TEST_CASE("ByteArrayToHexString") {
|
|
std::vector<uint8> bytes;
|
|
|
|
REQUIRE(ByteArrayToHexString(bytes) == "");
|
|
|
|
bytes = {0x00};
|
|
REQUIRE(ByteArrayToHexString(bytes) == "00");
|
|
|
|
bytes = {0x00, 0x01, 0x02, 0xff};
|
|
REQUIRE(ByteArrayToHexString(bytes) == "000102ff");
|
|
}
|
|
|
|
TEST_CASE("Base64Encode") {
|
|
REQUIRE(Base64Encode("") == "");
|
|
REQUIRE(Base64Encode("f") == "Zg==");
|
|
REQUIRE(Base64Encode("fo") == "Zm8=");
|
|
REQUIRE(Base64Encode("foo") == "Zm9v");
|
|
REQUIRE(Base64Encode("foob") == "Zm9vYg==");
|
|
REQUIRE(Base64Encode("fooba") == "Zm9vYmE=");
|
|
REQUIRE(Base64Encode("foobar") == "Zm9vYmFy");
|
|
}
|
|
|
|
TEST_CASE("ZM::clamp") {
|
|
REQUIRE(zm::clamp(1, 0, 2) == 1);
|
|
REQUIRE(zm::clamp(3, 0, 2) == 2);
|
|
REQUIRE(zm::clamp(-1, 0, 2) == 0);
|
|
}
|
|
|
|
TEST_CASE("UriDecode") {
|
|
REQUIRE(UriDecode("abcABC123-_.~%21%28%29%26%3d%20") == "abcABC123-_.~!()&= ");
|
|
REQUIRE(UriDecode("abcABC123-_.~%21%28%29%26%3d+") == "abcABC123-_.~!()&= ");
|
|
}
|
|
|
|
TEST_CASE("UriEncode") {
|
|
// Basic alphanumeric and safe characters should not be encoded
|
|
REQUIRE(UriEncode("abcABC123") == "abcABC123");
|
|
REQUIRE(UriEncode("-_.~") == "-_.~");
|
|
|
|
// Space should be encoded as %20
|
|
REQUIRE(UriEncode(" ") == "%20");
|
|
REQUIRE(UriEncode("hello world") == "hello%20world");
|
|
REQUIRE(UriEncode("a b c") == "a%20b%20c");
|
|
|
|
// Special characters should be percent-encoded
|
|
REQUIRE(UriEncode("!") == "%21");
|
|
REQUIRE(UriEncode("&") == "%26");
|
|
REQUIRE(UriEncode("=") == "%3D");
|
|
REQUIRE(UriEncode("?") == "%3F");
|
|
|
|
// Empty string
|
|
REQUIRE(UriEncode("") == "");
|
|
|
|
// Round-trip test: encode then decode should return original
|
|
std::string original = "hello world!";
|
|
REQUIRE(UriDecode(UriEncode(original)) == original);
|
|
|
|
// Non-ASCII bytes must each be escaped as a single %XX pair. A signed char
|
|
// sign-extends here and yields "%FF" (truncated from "FFFFFFD0") instead.
|
|
REQUIRE(UriEncode("\xD0\xB2") == "%D0%B2");
|
|
REQUIRE(UriEncode("\xFF") == "%FF");
|
|
REQUIRE(UriEncode("\x80") == "%80");
|
|
|
|
// A Cyrillic name (U+0432 U+0445 U+043E U+0434) - the kind of name the
|
|
// utf8mb4 switch made storable. Written as bytes so this file stays ASCII.
|
|
REQUIRE(UriEncode("\xD0\xB2\xD1\x85\xD0\xBE\xD0\xB4") == "%D0%B2%D1%85%D0%BE%D0%B4");
|
|
|
|
// Mixed ASCII and UTF-8, and a multi-byte round trip.
|
|
REQUIRE(UriEncode("Cam \xC3\xA9") == "Cam%20%C3%A9");
|
|
REQUIRE(UriDecode(UriEncode("\xD0\xB2\xD1\x85")) == "\xD0\xB2\xD1\x85");
|
|
}
|
|
|
|
TEST_CASE("escape_json_string") {
|
|
// Nothing to do.
|
|
REQUIRE(escape_json_string("") == "");
|
|
REQUIRE(escape_json_string("Front Door") == "Front Door");
|
|
|
|
// A quote must come out as one backslash then a quote. The previous
|
|
// implementation escaped quotes before backslashes, so it emitted \\" here:
|
|
// an escaped backslash followed by a bare quote, which closed the JSON
|
|
// string it was embedded in.
|
|
REQUIRE(escape_json_string("\"") == "\\\"");
|
|
REQUIRE(escape_json_string("Front \"Door\"") == "Front \\\"Door\\\"");
|
|
|
|
// A backslash doubles, and that doubling must not itself be re-doubled.
|
|
REQUIRE(escape_json_string("\\") == "\\\\");
|
|
REQUIRE(escape_json_string("back\\slash") == "back\\\\slash");
|
|
|
|
// A backslash adjacent to a quote is where ordering errors surface.
|
|
REQUIRE(escape_json_string("\\\"") == "\\\\\\\"");
|
|
|
|
// Named control character escapes.
|
|
REQUIRE(escape_json_string("\n") == "\\n");
|
|
REQUIRE(escape_json_string("\r") == "\\r");
|
|
REQUIRE(escape_json_string("\t") == "\\t");
|
|
REQUIRE(escape_json_string("\b") == "\\b");
|
|
REQUIRE(escape_json_string("\f") == "\\f");
|
|
REQUIRE(escape_json_string("line\nbreak") == "line\\nbreak");
|
|
|
|
// Control characters without a short form must still be escaped.
|
|
REQUIRE(escape_json_string(std::string(1, '\a')) == "\\u0007");
|
|
REQUIRE(escape_json_string(std::string(1, '\x01')) == "\\u0001");
|
|
REQUIRE(escape_json_string(std::string(1, '\x1f')) == "\\u001f");
|
|
|
|
// An embedded NUL is a control character like any other and must survive.
|
|
REQUIRE(escape_json_string(std::string("a\0b", 3)) == "a\\u0000b");
|
|
|
|
// 0x7f is not a JSON control character, and UTF-8 passes through intact.
|
|
REQUIRE(escape_json_string(std::string(1, '\x7f')) == std::string(1, '\x7f'));
|
|
REQUIRE(escape_json_string("\xD0\xB2\xD1\x85") == "\xD0\xB2\xD1\x85");
|
|
|
|
// Escaping twice is not the same as escaping once. This is the double
|
|
// escape the go2rtc credential path was hitting.
|
|
const std::string once = escape_json_string("pa\\ss");
|
|
REQUIRE(once == "pa\\\\ss");
|
|
REQUIRE(escape_json_string(once) == "pa\\\\\\\\ss");
|
|
}
|
|
|
|
TEST_CASE("QueryString") {
|
|
SECTION("no value") {
|
|
std::stringstream str("name1=");
|
|
QueryString qs(str);
|
|
|
|
REQUIRE(qs.size() == 1);
|
|
REQUIRE(qs.has("name1") == true);
|
|
|
|
const QueryParameter *p = qs.get("name1");
|
|
REQUIRE(p != nullptr);
|
|
REQUIRE(p->name() == "name1");
|
|
REQUIRE(p->size() == 0);
|
|
}
|
|
|
|
SECTION("no value and ampersand") {
|
|
std::stringstream str("name1=&");
|
|
QueryString qs(str);
|
|
|
|
REQUIRE(qs.size() == 1);
|
|
REQUIRE(qs.has("name1") == true);
|
|
|
|
const QueryParameter *p = qs.get("name1");
|
|
REQUIRE(p != nullptr);
|
|
REQUIRE(p->name() == "name1");
|
|
REQUIRE(p->size() == 0);
|
|
}
|
|
|
|
SECTION("one parameter, one value") {
|
|
std::stringstream str("name1=value1");
|
|
QueryString qs(str);
|
|
|
|
REQUIRE(qs.size() == 1);
|
|
REQUIRE(qs.has("name1") == true);
|
|
|
|
const QueryParameter *p = qs.get("name1");
|
|
REQUIRE(p != nullptr);
|
|
REQUIRE(p->name() == "name1");
|
|
REQUIRE(p->size() == 1);
|
|
REQUIRE(p->values()[0] == "value1");
|
|
}
|
|
|
|
SECTION("one parameter, multiple values") {
|
|
std::stringstream str("name1=value1&name1=value2");
|
|
QueryString qs(str);
|
|
|
|
REQUIRE(qs.size() == 1);
|
|
REQUIRE(qs.has("name1") == true);
|
|
|
|
const QueryParameter *p = qs.get("name1");
|
|
REQUIRE(p != nullptr);
|
|
REQUIRE(p->name() == "name1");
|
|
REQUIRE(p->size() == 2);
|
|
REQUIRE(p->values()[0] == "value1");
|
|
REQUIRE(p->values()[1] == "value2");
|
|
}
|
|
|
|
SECTION("multiple parameters, multiple values") {
|
|
std::stringstream str("name1=value1&name2=value2");
|
|
QueryString qs(str);
|
|
|
|
REQUIRE(qs.size() == 2);
|
|
REQUIRE(qs.has("name1") == true);
|
|
REQUIRE(qs.has("name2") == true);
|
|
|
|
const QueryParameter *p1 = qs.get("name1");
|
|
REQUIRE(p1 != nullptr);
|
|
REQUIRE(p1->name() == "name1");
|
|
REQUIRE(p1->size() == 1);
|
|
REQUIRE(p1->values()[0] == "value1");
|
|
|
|
const QueryParameter *p2 = qs.get("name2");
|
|
REQUIRE(p2 != nullptr);
|
|
REQUIRE(p2->name() == "name2");
|
|
REQUIRE(p2->size() == 1);
|
|
REQUIRE(p2->values()[0] == "value2");
|
|
}
|
|
}
|
|
|
|
TEST_CASE("mask_authentication") {
|
|
SECTION("no authentication") {
|
|
std::string url("http://192.168.1.1");
|
|
std::string result = mask_authentication(url);
|
|
REQUIRE(url == result);
|
|
}
|
|
SECTION("has username no password has scheme") {
|
|
std::string url("http://username@192.168.1.1");
|
|
std::string result = mask_authentication(url);
|
|
REQUIRE(result == "http://********@192.168.1.1");
|
|
}
|
|
SECTION("has username no password no scheme") {
|
|
std::string url("username@192.168.1.1");
|
|
std::string result = mask_authentication(url);
|
|
REQUIRE(result == "********@192.168.1.1");
|
|
}
|
|
SECTION("has username has password no scheme") {
|
|
std::string url("username:password@192.168.1.1");
|
|
std::string result = mask_authentication(url);
|
|
REQUIRE(result == "********:********@192.168.1.1");
|
|
}
|
|
SECTION("has username has password has scheme") {
|
|
std::string url("http://username:password@192.168.1.1");
|
|
std::string result = mask_authentication(url);
|
|
REQUIRE(result == "http://********:********@192.168.1.1");
|
|
}
|
|
}
|
|
|
|
TEST_CASE("remove_authentication") {
|
|
SECTION("no authentication") {
|
|
std::string url("http://192.168.1.1");
|
|
std::string result = remove_authentication(url);
|
|
REQUIRE(url == result);
|
|
}
|
|
SECTION("has username no password has scheme") {
|
|
std::string url("http://username@192.168.1.1");
|
|
std::string result = remove_authentication(url);
|
|
REQUIRE(result == "http://192.168.1.1");
|
|
}
|
|
SECTION("has username no password no scheme") {
|
|
std::string url("username@192.168.1.1");
|
|
std::string result = remove_authentication(url);
|
|
REQUIRE(result == "192.168.1.1");
|
|
}
|
|
SECTION("has username has password no scheme") {
|
|
std::string url("username:password@192.168.1.1");
|
|
std::string result = remove_authentication(url);
|
|
REQUIRE(result == "192.168.1.1");
|
|
}
|
|
SECTION("has username has password has scheme") {
|
|
std::string url("http://username:password@192.168.1.1");
|
|
std::string result = remove_authentication(url);
|
|
REQUIRE(result == "http://192.168.1.1");
|
|
}
|
|
}
|
|
|
|
TEST_CASE("zm_strncpy") {
|
|
char buf[8];
|
|
|
|
SECTION("fits, null-terminated") {
|
|
zm_strncpy(buf, "abc", sizeof(buf));
|
|
REQUIRE(std::string(buf) == "abc");
|
|
}
|
|
SECTION("oversize truncated to size-1 and terminated") {
|
|
zm_strncpy(buf, "abcdefghijkl", sizeof(buf));
|
|
REQUIRE(std::string(buf) == "abcdefg"); // 7 chars + '\0'
|
|
REQUIRE(buf[7] == '\0');
|
|
}
|
|
SECTION("delimiter length n limits copy") {
|
|
zm_strncpy(buf, "abcdef", sizeof(buf), 3); // src not null-terminated at field end
|
|
REQUIRE(std::string(buf) == "abc");
|
|
}
|
|
SECTION("delimiter length also capped to buffer") {
|
|
zm_strncpy(buf, "abcdefghij", sizeof(buf), 20);
|
|
REQUIRE(std::string(buf) == "abcdefg");
|
|
REQUIRE(buf[7] == '\0');
|
|
}
|
|
SECTION("empty source") {
|
|
zm_strncpy(buf, "", sizeof(buf));
|
|
REQUIRE(buf[0] == '\0');
|
|
}
|
|
}
|
|
|
|
TEST_CASE("TimevalToString") {
|
|
// tv_usec used to go to the format string as %ld. Under glibc's 64 bit time
|
|
// ABI on a 32 bit platform (Debian trixie on armhf) tv_usec is
|
|
// __suseconds64_t while long is still 32 bits, so %ld consumed half the
|
|
// argument and every vararg after it was read from the wrong offset. zmc
|
|
// crashed on armhf because this result is written into a buffer and then
|
|
// used. See #4580 and Debian bug 1120447.
|
|
//
|
|
// amd64 cannot reproduce the mismatch (long is already 64 bits), so what
|
|
// this pins is the rendering: six zero padded digits of microseconds, which
|
|
// is what a wrong conversion would disturb.
|
|
const char *tz = getenv("TZ");
|
|
std::string saved_tz = tz ? tz : "";
|
|
setenv("TZ", "UTC", 1);
|
|
tzset();
|
|
|
|
SECTION("pads microseconds to six digits") {
|
|
REQUIRE(TimevalToString(timeval{0, 0}) == "1970-01-01 00:00:00.000000");
|
|
REQUIRE(TimevalToString(timeval{0, 1}) == "1970-01-01 00:00:00.000001");
|
|
REQUIRE(TimevalToString(timeval{0, 999999}) == "1970-01-01 00:00:00.999999");
|
|
}
|
|
|
|
SECTION("does not truncate or reorder the arguments") {
|
|
// A conversion that mis-reads tv_usec tends to show up either as a wrong
|
|
// number here or as the seconds field being wrong, since the bad read
|
|
// shifts everything after it.
|
|
REQUIRE(TimevalToString(timeval{1700000000, 123456}) == "2023-11-14 22:13:20.123456");
|
|
}
|
|
|
|
if (saved_tz.empty()) {
|
|
unsetenv("TZ");
|
|
} else {
|
|
setenv("TZ", saved_tz.c_str(), 1);
|
|
}
|
|
tzset();
|
|
}
|