Files
patch/tests/test_strip.cpp
Shannon Booth e0ae942bae system: do not treat a drive-relative path as absolute
"C:\foo" names the root of drive C, but "C:foo" names foo relative to
whatever the current directory on C happens to be. Classifying the
second as absolute means it is resolved from a root it does not name.

Rejecting absolute names was also the only thing keeping a
drive-relative name out of a patch, so is_safe_patch_path() now refuses
a colon itself. A colon never appears in a Windows file name, since
"file:stream" names an alternate data stream of "file" rather than a
file of that name.
2026-08-01 18:50:32 +02:00

209 lines
7.9 KiB
C++

// SPDX-License-Identifier: BSD-3-Clause
// Copyright 2022 Shannon Booth <shannon.ml.booth@gmail.com>
#include <patch/parser.h>
#include <patch/system.h>
#include <patch/test.h>
#include <stdexcept>
static void parse_file_line(const std::string& line, int strip, std::string& path, std::string* timestamp = nullptr)
{
Patch::LineParser parser(line);
parser.parse_file_line(strip, path, timestamp);
}
TEST(is_absolute_path)
{
EXPECT_FALSE(Patch::filesystem::is_absolute(""));
EXPECT_FALSE(Patch::filesystem::is_absolute("file.txt"));
EXPECT_FALSE(Patch::filesystem::is_absolute("dir/file.txt"));
EXPECT_FALSE(Patch::filesystem::is_absolute("../file.txt"));
EXPECT_TRUE(Patch::filesystem::is_absolute("/"));
EXPECT_TRUE(Patch::filesystem::is_absolute("/etc/passwd"));
#ifdef _WIN32
EXPECT_TRUE(Patch::filesystem::is_absolute("\\windows\\file.txt"));
EXPECT_TRUE(Patch::filesystem::is_absolute("C:\\file.txt"));
EXPECT_TRUE(Patch::filesystem::is_absolute("C:/file.txt"));
EXPECT_FALSE(Patch::filesystem::is_absolute("C:file.txt"));
EXPECT_FALSE(Patch::filesystem::is_absolute("C:"));
#else
EXPECT_FALSE(Patch::filesystem::is_absolute("C:\\file.txt"));
#endif
}
TEST(is_safe_patch_path)
{
EXPECT_TRUE(Patch::is_safe_patch_path(""));
EXPECT_TRUE(Patch::is_safe_patch_path("file.txt"));
EXPECT_TRUE(Patch::is_safe_patch_path("dir/file.txt"));
EXPECT_TRUE(Patch::is_safe_patch_path("./file.txt"));
EXPECT_TRUE(Patch::is_safe_patch_path("..file.txt"));
EXPECT_TRUE(Patch::is_safe_patch_path("dir/..file/x.txt"));
EXPECT_TRUE(Patch::is_safe_patch_path("dir/..."));
// Absolute, but a magic name rather than a path to patch.
EXPECT_TRUE(Patch::is_safe_patch_path("/dev/null"));
EXPECT_FALSE(Patch::is_safe_patch_path("/etc/passwd"));
EXPECT_FALSE(Patch::is_safe_patch_path("../file.txt"));
EXPECT_FALSE(Patch::is_safe_patch_path(".."));
EXPECT_FALSE(Patch::is_safe_patch_path("dir/../file.txt"));
EXPECT_FALSE(Patch::is_safe_patch_path("dir/.."));
EXPECT_FALSE(Patch::is_safe_patch_path("a/../../b.txt"));
#ifdef _WIN32
EXPECT_FALSE(Patch::is_safe_patch_path("\\windows\\file.txt"));
EXPECT_FALSE(Patch::is_safe_patch_path("C:\\file.txt"));
EXPECT_FALSE(Patch::is_safe_patch_path("C:file.txt"));
EXPECT_FALSE(Patch::is_safe_patch_path("file.txt:stream"));
EXPECT_FALSE(Patch::is_safe_patch_path("dir/file.txt:stream"));
EXPECT_FALSE(Patch::is_safe_patch_path("dir\\..\\file.txt"));
#endif
}
TEST(strip_linux_path)
{
EXPECT_EQ(Patch::strip_path("/my/path/for/test/purposes.txt", 0), "/my/path/for/test/purposes.txt");
EXPECT_EQ(Patch::strip_path("/my/path/for/test/purposes.txt", 1), "my/path/for/test/purposes.txt");
EXPECT_EQ(Patch::strip_path("/my/path/for/test/purposes.txt", 2), "path/for/test/purposes.txt");
EXPECT_EQ(Patch::strip_path("/my/path/for/test/purposes.txt", 3), "for/test/purposes.txt");
EXPECT_EQ(Patch::strip_path("/my/path/for/test/purposes.txt", 4), "test/purposes.txt");
EXPECT_EQ(Patch::strip_path("/my/path/for/test/purposes.txt", 5), "purposes.txt");
EXPECT_EQ(Patch::strip_path("/my/path/for/test/purposes.txt", 6), "");
}
TEST(strip_remove_all_leading)
{
EXPECT_EQ(Patch::strip_path("/my/path/for/test/purposes.txt", -1), "purposes.txt");
EXPECT_EQ(Patch::strip_path("/usr/bin/cat", -1), "cat");
EXPECT_EQ(Patch::strip_path("noslash", -1), "noslash");
#ifdef _WIN32
EXPECT_EQ(Patch::strip_path("C:\\Users\\Shannon\\thing.pdf", -1), "thing.pdf");
EXPECT_EQ(Patch::strip_path("\\Program Files\\Utilities\\test.exe", -1), "test.exe");
EXPECT_EQ(Patch::strip_path("2018\\thing.xlsx", -1), "thing.xlsx");
#endif
}
TEST(strip_multi_slash)
{
EXPECT_EQ(Patch::strip_path("/path//with/multiple/slashes", 3), "multiple/slashes");
#ifdef _WIN32
EXPECT_EQ(Patch::strip_path("C:\\Users\\\\Shannon\\another.txt", 2), "Shannon\\another.txt");
#endif
}
TEST(strip_windows_path)
{
#ifdef _WIN32
EXPECT_EQ(Patch::strip_path("C:\\Users\\Shannon\\test.pdf", 0), "C:\\Users\\Shannon\\test.pdf");
EXPECT_EQ(Patch::strip_path("C:\\Users\\Shannon\\test.pdf", 1), "Users\\Shannon\\test.pdf");
EXPECT_EQ(Patch::strip_path("C:\\Users\\Shannon\\test.pdf", 2), "Shannon\\test.pdf");
EXPECT_EQ(Patch::strip_path("C:\\Users\\Shannon\\test.pdf", 3), "test.pdf");
EXPECT_EQ(Patch::strip_path("C:\\Users\\Shannon\\test.pdf", 4), "");
#endif
}
TEST(strip_quoted_string_bad)
{
std::string path;
EXPECT_THROW(parse_file_line("\"path/with unterminated comma", -1, path), std::invalid_argument);
EXPECT_THROW(parse_file_line("\"secondUnterminatedCommaButAfterBackslash\\", -1, path), std::invalid_argument);
EXPECT_THROW(parse_file_line("\"badEscapeChar\\l\"", -1, path), std::invalid_argument);
EXPECT_THROW(parse_file_line(R"("\400")", -1, path), std::invalid_argument);
}
TEST(strip_quoted_string_good)
{
std::string path;
std::string timestamp;
path.clear();
parse_file_line("someInput", -1, path, &timestamp);
EXPECT_EQ(path, "someInput");
EXPECT_EQ(timestamp, "");
path.clear();
parse_file_line("\"some spaced input\"", -1, path, &timestamp);
EXPECT_EQ(path, "some spaced input");
EXPECT_EQ(timestamp, "");
path.clear();
parse_file_line(R"("with backslash \\ escape char")", 0, path, &timestamp);
EXPECT_EQ(path, "with backslash \\ escape char");
EXPECT_EQ(timestamp, "");
path.clear();
parse_file_line(R"("with quote \" escape char")", 0, path, &timestamp);
EXPECT_EQ(path, "with quote \" escape char");
EXPECT_EQ(timestamp, "");
// Octal simple
path.clear();
parse_file_line(R"("\110\145\154\154\157\054\040\167\157\162\154\144\041\040\061\062\063")", 0, path, &timestamp);
EXPECT_EQ(path, "Hello, world! 123");
EXPECT_EQ(timestamp, "");
// Octal escape codes (+ ASCII char)
path.clear();
parse_file_line(R"("\327\251\327\234\327\225\327\235 \327\242\327\225\327\234\327\235!")", 0, path, &timestamp);
EXPECT_EQ(path, "שלום עולם!");
EXPECT_EQ(timestamp, "");
// Octal escape codes, not all 3 chars
path.clear();
parse_file_line(R"("\110\145\154\154\157\054\40cruel \167\157\162\154\144\041\40\061\62\063123")", 0, path, &timestamp);
EXPECT_EQ(path, "Hello, cruel world! 123123");
EXPECT_EQ(timestamp, "");
path.clear();
parse_file_line(R"("quoted string \\ then \" timestamp" 2022-06-10 19:28:11.018017172 +1200)", 0, path, &timestamp);
EXPECT_EQ(path, "quoted string \\ then \" timestamp");
EXPECT_EQ(timestamp, "\t2022-06-10 19:28:11.018017172 +1200"); // NOTE: in future we may want leading space trimmed.
}
TEST(strip_standard_path)
{
std::string path;
std::string timestamp;
// Nothing
path.clear();
parse_file_line("a/file.txt", -1, path, &timestamp);
EXPECT_EQ(path, "file.txt");
EXPECT_EQ(timestamp, "");
// Trailing space
path.clear();
parse_file_line("a/file.txt 2022-06-10 19:28:11.018017172 +1200", -1, path, &timestamp);
EXPECT_EQ(path, "file.txt");
EXPECT_EQ(timestamp, " 2022-06-10 19:28:11.018017172 +1200"); // NOTE: in future we may want leading space trimmed.
// Trailing tab
path.clear();
parse_file_line("a/file.txt\t2022-06-10 19:28:11.018017172 +1200", -1, path, &timestamp);
EXPECT_EQ(path, "file.txt");
EXPECT_EQ(timestamp, "2022-06-10 19:28:11.018017172 +1200");
// Space with no timestamp
path.clear();
timestamp.clear();
parse_file_line("b/a file name\t", -1, path, &timestamp);
EXPECT_EQ(path, "a file name");
EXPECT_EQ(timestamp, "");
// Space with timestamp
path.clear();
parse_file_line("b/a file name\t2022-06-10 19:28:11.018017172 +1200", -1, path, &timestamp);
EXPECT_EQ(path, "a file name");
EXPECT_EQ(timestamp, "2022-06-10 19:28:11.018017172 +1200");
// Empty path
parse_file_line("", -1, path, &timestamp);
EXPECT_EQ(path, "");
EXPECT_EQ(timestamp, "");
}