/* * 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 . */ #include "zm_catch2.h" #include "zm_utils.h" #include 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 items = Split("", ' '); REQUIRE(items == std::vector{""}); items = Split("abc def ghi", ' '); REQUIRE(items == std::vector{"abc", "def", "ghi"}); items = Split("abc,def,,ghi", ','); REQUIRE(items == std::vector{"abc", "def", "", "ghi"}); } TEST_CASE("Split (string delimiter)") { std::vector items; items = Split("", ""); REQUIRE(items == std::vector{""}); items = Split("", " "); REQUIRE(items == std::vector{""}); items = Split("", " \t"); REQUIRE(items == std::vector{""}); items = Split("", " \t"); REQUIRE(items == std::vector{""}); 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{"a b"}); items = Split("a b", " "); REQUIRE(items == std::vector{"a", "b"}); items = Split("a \tb", " \t"); REQUIRE(items == std::vector{"a", "b"}); items = Split(" a \tb ", " \t"); REQUIRE(items == std::vector{"a", "b"}); items = Split(" a=b ", "="); REQUIRE(items == std::vector{" a", "b "}); items = Split(" a=b ", " ="); REQUIRE(items == std::vector{"a", "b"}); items = Split("a b c", " ", 2); REQUIRE(items == std::vector{"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{"--rate=1", "--no-audio"}); items = Split("--rate=1,--no-audio", kOptionSeparators); REQUIRE(items == std::vector{"--rate=1", "--no-audio"}); items = Split("--rate=1\r\n\r\n--no-audio\n", kOptionSeparators); REQUIRE(items == std::vector{"--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 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(); }