Files
znc/test/UtilsTest.cpp
T
dxbjavid 01678166a8 fileutils: force owner write while copying read-only sources
Opening the destination at the source's exact mode breaks when the
source lacks owner write (e.g. r-xr-xr-x): the create still works but
the overwrite path can't reopen such a destination for writing. Force
owner read+write while copying and let the trailing Chmod() put the
source mode back, which only ever adds owner bits so the group/other
bits stay as restrictive as the source throughout. Add a regression
test covering the restricted-mode and read-only-source cases.
2026-06-09 09:53:57 +05:30

311 lines
10 KiB
C++

/*
* Copyright (C) 2004-2026 ZNC, see the NOTICE file for details.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <gtest/gtest.h>
#include <znc/FileUtils.h>
#include <znc/Utils.h>
namespace {
CString WriteTempFile(const CString& sContent, mode_t iMode) {
char sName[] = "./copytest-XXXXXX";
int fd = mkstemp(sName);
EXPECT_NE(fd, -1);
close(fd);
CFile File(sName);
EXPECT_TRUE(File.Open(O_WRONLY | O_TRUNC));
File.Write(sContent);
File.Close();
EXPECT_TRUE(CFile::Chmod(sName, iMode));
return sName;
}
unsigned ModeOf(const CString& sFile) {
struct stat st;
EXPECT_EQ(CFile::GetInfo(sFile, st), 0);
return st.st_mode & 07777;
}
} // namespace
// A 0600 source must never widen to the default 0644 during the copy, so the
// destination ends up restricted too.
TEST(FileUtilsTest, CopyKeepsRestrictiveMode) {
CString sSrc = WriteTempFile("secret", 0600);
CString sDst = sSrc + "-copy";
EXPECT_TRUE(CFile::Copy(sSrc, sDst));
EXPECT_EQ(ModeOf(sDst), 0600u);
CFile::Delete(sSrc);
CFile::Delete(sDst);
}
// A source the owner can't write to (r-xr-xr-x) must still copy and keep its
// mode; the copy forces owner write internally and chmods it back afterwards.
TEST(FileUtilsTest, CopyReadOnlySource) {
CString sSrc = WriteTempFile("public", 0555);
CString sDst = sSrc + "-copy";
EXPECT_TRUE(CFile::Copy(sSrc, sDst));
EXPECT_EQ(ModeOf(sDst), 0555u);
CString sContent;
CFile Dst(sDst);
ASSERT_TRUE(Dst.Open());
Dst.ReadFile(sContent);
Dst.Close();
EXPECT_EQ(sContent, "public");
CFile::Delete(sSrc);
CFile::Delete(sDst);
}
TEST(IRC32, GetMessageTags) {
EXPECT_EQ(CUtils::GetMessageTags(""), MCString());
EXPECT_EQ(CUtils::GetMessageTags(
":nick!ident@host PRIVMSG #chan :hello world"), MCString());
MCString exp = {{"a", "b"}};
EXPECT_EQ(CUtils::GetMessageTags("@a=b"), exp);
EXPECT_EQ(CUtils::GetMessageTags(
"@a=b :nick!ident@host PRIVMSG #chan :hello world"), exp);
EXPECT_EQ(CUtils::GetMessageTags("@a=b :rest"), exp);
exp.clear();
exp = {{"ab", "cdef"}, {"znc.in/gh-ij", "klmn,op"}};
EXPECT_EQ(CUtils::GetMessageTags("@ab=cdef;znc.in/gh-ij=klmn,op :rest"),
exp);
exp = {{"a", "==b=="}};
EXPECT_EQ(CUtils::GetMessageTags("@a===b== :rest"), exp);
exp.clear();
exp = {{"a", ""}, {"b", "c"}, {"d", ""}};
EXPECT_EQ(CUtils::GetMessageTags("@a;b=c;d :rest"), exp);
exp = {{"semi-colon", ";"}, {"space", " "}, {"NUL", {'\0'}},
{"backslash", "\\"}, {"CR", {'\r'}}, {"LF", {'\n'}}};
EXPECT_EQ(
CUtils::GetMessageTags(
R"(@semi-colon=\:;space=\s;NUL=\0;backslash=\\;CR=\r;LF=\n :rest)"),
exp);
exp.clear();
exp = {{"a", "; \\\r\n"}};
EXPECT_EQ(CUtils::GetMessageTags(R"(@a=\:\s\\\r\n :rest)"), exp);
exp.clear();
}
TEST(IRC32, SetMessageTags) {
CString sLine;
sLine = ":rest";
CUtils::SetMessageTags(sLine, MCString());
EXPECT_EQ(sLine, ":rest");
MCString tags = {{"a", "b"}};
CUtils::SetMessageTags(sLine, tags);
EXPECT_EQ(sLine, "@a=b :rest");
tags = {{"a", "b"}, {"c", "d"}};
CUtils::SetMessageTags(sLine, tags);
EXPECT_EQ(sLine, "@a=b;c=d :rest");
tags = {{"a", "b"}, {"c", "d"}, {"e", ""}};
CUtils::SetMessageTags(sLine, tags);
EXPECT_EQ(sLine, "@a=b;c=d;e :rest");
tags = {{"semi-colon", ";"}, {"space", " "}, {"NUL", {'\0'}},
{"backslash", "\\"}, {"CR", {'\r'}}, {"LF", {'\n'}}};
CUtils::SetMessageTags(sLine, tags);
EXPECT_EQ(
sLine,
R"(@CR=\r;LF=\n;NUL=\0;backslash=\\;semi-colon=\:;space=\s :rest)");
tags = {{"a", "; \\\r\n"}};
CUtils::SetMessageTags(sLine, tags);
EXPECT_EQ(sLine, R"(@a=\:\s\\\r\n :rest)");
}
TEST(UtilsTest, Timezone) {
char* oldTZ = getenv("TZ");
if (oldTZ) oldTZ = strdup(oldTZ);
setenv("TZ", "Europe/Berlin", 1);
tzset();
EXPECT_EQ(CUtils::FormatTime(1673122230, "%Y-%m-%d %H:%M:%S", "UTC"), "2023-01-07 20:10:30");
EXPECT_EQ(CUtils::FormatTime(1673122230, "%Y-%m-%d %H:%M:%S", "GMT"), "2023-01-07 20:10:30");
EXPECT_EQ(CUtils::FormatTime(1673122230, "%Y-%m-%d %H:%M:%S", "GMT+7"), "2023-01-08 03:10:30");
EXPECT_EQ(CUtils::FormatTime(1673122230, "%Y-%m-%d %H:%M:%S", "GMT-7"), "2023-01-07 13:10:30");
EXPECT_EQ(CUtils::FormatTime(1673122230, "%Y-%m-%d %H:%M:%S", "Asia/Vladivostok"), "2023-01-08 06:10:30");
// Local TZ, set to Berlin in this test
EXPECT_EQ(CUtils::FormatTime(1673122230, "%Y-%m-%d %H:%M:%S", ""), "2023-01-07 21:10:30");
if (oldTZ) {
setenv("TZ", oldTZ, 1);
free(oldTZ);
} else {
unsetenv("TZ");
}
tzset();
}
TEST(UtilsTest, ServerTime) {
char* oldTZ = getenv("TZ");
if (oldTZ) oldTZ = strdup(oldTZ);
setenv("TZ", "UTC", 1);
tzset();
timeval tv1 = CUtils::ParseServerTime("2011-10-19T16:40:51.620Z");
CString str1 = CUtils::FormatServerTime(tv1);
EXPECT_EQ(str1, "2011-10-19T16:40:51.620Z");
timeval now = CUtils::GetTime();
// Strip microseconds, server time is ms only
now.tv_usec = (now.tv_usec / 1000) * 1000;
CString str2 = CUtils::FormatServerTime(now);
timeval tv2 = CUtils::ParseServerTime(str2);
EXPECT_EQ(tv2.tv_sec, now.tv_sec);
EXPECT_EQ(tv2.tv_usec, now.tv_usec);
timeval tv3 = CUtils::ParseServerTime("invalid");
CString str3 = CUtils::FormatServerTime(tv3);
EXPECT_EQ(str3, "1970-01-01T00:00:00.000Z");
if (oldTZ) {
setenv("TZ", oldTZ, 1);
free(oldTZ);
} else {
unsetenv("TZ");
}
tzset();
}
TEST(UtilsTest, ConstantTimeEquals) {
// Functional correctness for the helper introduced for #2011.
// We can't measure timing in a unit test, so we verify the boolean
// contract: equal inputs match, any difference (length or content)
// does not.
EXPECT_TRUE(CUtils::ConstantTimeEquals("", ""));
EXPECT_TRUE(CUtils::ConstantTimeEquals("abc", "abc"));
EXPECT_TRUE(CUtils::ConstantTimeEquals(CString("\x00\x01\x02", 3),
CString("\x00\x01\x02", 3)));
// Differs in last byte (the hardest case for short-circuit compare).
EXPECT_FALSE(CUtils::ConstantTimeEquals("abc", "abd"));
// Differs in first byte.
EXPECT_FALSE(CUtils::ConstantTimeEquals("abc", "Xbc"));
// Length mismatch on either side.
EXPECT_FALSE(CUtils::ConstantTimeEquals("abc", "abcd"));
EXPECT_FALSE(CUtils::ConstantTimeEquals("abcd", "abc"));
EXPECT_FALSE(CUtils::ConstantTimeEquals("", "x"));
EXPECT_FALSE(CUtils::ConstantTimeEquals("x", ""));
// Case-sensitive (unlike CString::Equals default).
EXPECT_FALSE(CUtils::ConstantTimeEquals("abc", "ABC"));
// Embedded NUL is compared, not used as a terminator.
EXPECT_FALSE(CUtils::ConstantTimeEquals(CString("a\x00""c", 3),
CString("a\x00""d", 3)));
EXPECT_TRUE(CUtils::ConstantTimeEquals(CString("a\x00""c", 3),
CString("a\x00""c", 3)));
}
TEST(UtilsTest, ParseServerTime) {
char* oldTZ = getenv("TZ");
if (oldTZ) oldTZ = strdup(oldTZ);
setenv("TZ", "America/Montreal", 1);
tzset();
timeval tv4 = CUtils::ParseServerTime("2011-10-19T16:40:51.620Z");
CString str4 = CUtils::FormatServerTime(tv4);
EXPECT_EQ(str4, "2011-10-19T16:40:51.620Z");
if (oldTZ) {
setenv("TZ", oldTZ, 1);
free(oldTZ);
} else {
unsetenv("TZ");
}
tzset();
}
TEST(UtilsTest, ParseServerTimeOutOfRange) {
// Years past 5 digits trigger int64 overflow inside cctz' microseconds
// conversion (`seconds * 1_000_000`). Reject up front (#2008).
timeval tv = CUtils::ParseServerTime("999999-01-01T00:00:00.000Z");
EXPECT_EQ(tv.tv_sec, 0);
EXPECT_EQ(tv.tv_usec, 0);
tv = CUtils::ParseServerTime("12345678-01-01T00:00:00.000Z");
EXPECT_EQ(tv.tv_sec, 0);
EXPECT_EQ(tv.tv_usec, 0);
// Junk and empty input still return a zeroed timeval.
tv = CUtils::ParseServerTime("");
EXPECT_EQ(tv.tv_sec, 0);
EXPECT_EQ(tv.tv_usec, 0);
tv = CUtils::ParseServerTime("not-a-date-at-all");
EXPECT_EQ(tv.tv_sec, 0);
EXPECT_EQ(tv.tv_usec, 0);
// Canonical input still parses (regression).
tv = CUtils::ParseServerTime("2011-10-19T16:40:51.620Z");
EXPECT_EQ(CUtils::FormatServerTime(tv), "2011-10-19T16:40:51.620Z");
}
class TimeTest : public testing::TestWithParam<
std::tuple<timeval, CString, CString, CString>> {};
TEST_P(TimeTest, FormatTime) {
timeval tv = std::get<0>(GetParam());
EXPECT_EQ(std::get<1>(GetParam()), CUtils::FormatTime(tv, "%s.%f", "UTC"));
EXPECT_EQ(std::get<2>(GetParam()), CUtils::FormatTime(tv, "%s.%6f", "UTC"));
EXPECT_EQ(std::get<3>(GetParam()), CUtils::FormatTime(tv, "%s.%9f", "UTC"));
}
INSTANTIATE_TEST_CASE_P(
TimeTest, TimeTest,
testing::Values(
// leading zeroes
std::make_tuple(timeval{42, 12345}, "42.012", "42.012345", "42.012345000"),
// (no) rounding
std::make_tuple(timeval{42, 999999}, "42.999", "42.999999", "42.999999000"),
// no tv_usec part
std::make_tuple(timeval{42, 0}, "42.000", "42.000000", "42.000000000")));
TEST(UtilsTest, FormatTime) {
// Test passthrough
timeval tv1;
tv1.tv_sec = 42;
tv1.tv_usec = 123456;
CString str1 = CUtils::FormatTime(tv1, "%s", "UTC");
EXPECT_EQ(str1, "42");
// Test escapes
timeval tv2;
tv2.tv_sec = 42;
tv2.tv_usec = 123456;
CString str2 = CUtils::FormatTime(tv2, "%%f", "UTC");
EXPECT_EQ(str2, "%f");
// Test suffix
CString str3 = CUtils::FormatTime(tv2, "a%fb", "UTC");
EXPECT_EQ(str3, "a123b");
}