mirror of
https://github.com/znc/znc.git
synced 2026-06-11 17:25:04 +02:00
01678166a8
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.
311 lines
10 KiB
C++
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");
|
|
}
|