From dd767ee0c3920072156ff04b6ef7037d852a0bd6 Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sat, 1 Aug 2026 14:20:41 +0200 Subject: [PATCH 01/15] tests: make the poll descriptor count conversion explicit The process helper polls at most stdout and stderr, so its descriptor count always fits nfds_t. Express that conversion explicitly instead of relying on platform specific type widths. --- tests/lib/src/unix_process.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/src/unix_process.cpp b/tests/lib/src/unix_process.cpp index 1f4773e..0e2a575 100644 --- a/tests/lib/src/unix_process.cpp +++ b/tests/lib/src/unix_process.cpp @@ -140,7 +140,7 @@ Process::Process(const char* cmd, const std::vector& args, const st if (fds.empty()) break; - int poll_result = ::poll(fds.data(), fds.size(), 5000); + int poll_result = ::poll(fds.data(), static_cast(fds.size()), 5000); if (poll_result < 0) throw std::system_error(errno, std::generic_category(), "Poll failed waiting for data"); From 0194a844044ad7b1d336a5305eb42c7b4f3a7b3b Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sat, 1 Aug 2026 14:20:58 +0200 Subject: [PATCH 02/15] tests: make byte conversion explicit in diagnostics Read strings through their char representation before converting each value to an unsigned byte. This keeps escaping independent of whether plain char is signed. --- tests/lib/include/patch/test.h | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/lib/include/patch/test.h b/tests/lib/include/patch/test.h index 4a3d151..1918d4e 100644 --- a/tests/lib/include/patch/test.h +++ b/tests/lib/include/patch/test.h @@ -41,7 +41,8 @@ inline std::string escaped_string_for_test_output(const std::string& value) std::ostringstream out; out << '"'; - for (unsigned char c : value) { + for (char c : value) { + const auto byte = static_cast(c); switch (c) { case '\\': out << "\\\\"; @@ -59,10 +60,10 @@ inline std::string escaped_string_for_test_output(const std::string& value) out << "\\t"; break; default: - if (c >= 0x20 && c < 0x7f) { - out << static_cast(c); + if (byte >= 0x20 && byte < 0x7f) { + out << c; } else { - out << "\\x" << std::hex << std::setw(2) << std::setfill('0') << static_cast(c) << std::dec; + out << "\\x" << std::hex << std::setw(2) << std::setfill('0') << static_cast(byte) << std::dec; } break; } From 2d454a9a83e4577eb4c66668dc4ec4a40d815155 Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sat, 1 Aug 2026 14:21:20 +0200 Subject: [PATCH 03/15] applier: use an unsigned type for hunk counts Rejected and failed hunks are counts and cannot be negative. Keep them as size_t throughout so reporting them alongside the hunk container does not require a signed conversion. --- include/patch/applier.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/patch/applier.h b/include/patch/applier.h index 0f5326a..d7ce2d3 100644 --- a/include/patch/applier.h +++ b/include/patch/applier.h @@ -24,19 +24,19 @@ class RejectWriter { void write_reject_file(const Hunk& hunk); - int rejected_hunks() const { return m_rejected_hunks; } + size_t rejected_hunks() const { return m_rejected_hunks; } private: bool should_write_as_unified() const; const Patch& m_patch; - int m_rejected_hunks { 0 }; + size_t m_rejected_hunks { 0 }; File& m_reject_file; Options::RejectFormat m_reject_format { Options::RejectFormat::Default }; }; struct Result { - int failed_hunks; + size_t failed_hunks; bool was_skipped; bool all_hunks_applied_perfectly; }; From ed915ebf149c0459862e08cc320b58502a0f083f Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sat, 1 Aug 2026 14:21:46 +0200 Subject: [PATCH 04/15] system: validate file sizes before converting them stat represents file sizes with a signed type even though the public size API is unsigned. Reject an invalid negative size before making the otherwise lossless conversion explicit. --- src/system.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/system.cpp b/src/system.cpp index d71443e..f1f0655 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -647,7 +647,10 @@ uintmax_t file_size(FILE* file) if (fstat(fileno(file), &buf) != 0) throw std::system_error(errno, std::generic_category(), "Unable to fstat file"); - return buf.st_size; + if (buf.st_size < 0) + throw std::system_error(std::make_error_code(std::errc::invalid_argument), "File has a negative size"); + + return static_cast(buf.st_size); } } // namespace filesystem From 871484ba324c8bbc605dd95cde438311b8d3e70a Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sat, 1 Aug 2026 14:22:00 +0200 Subject: [PATCH 05/15] parser: keep diagnostic line numbers as size_t The saved range header position comes directly from the parser input counter and is used only in diagnostics. Preserve that type instead of narrowing it to a patch line number. --- src/parser.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser.cpp b/src/parser.cpp index 3c3a15b..dd3d55b 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -807,7 +807,7 @@ void Parser::parse_context_hunk(std::vector& old_lines, LineNumber& o { std::string line; - LineNumber from_file_range_line_number = 0; + size_t from_file_range_line_number = 0; LineNumber old_end_line = 0; LineNumber new_end_line = 0; From 69897acf3baccfb6525a85a2f37a592885412843 Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sat, 1 Aug 2026 14:37:58 +0200 Subject: [PATCH 06/15] parser: reject octal escapes larger than a byte Quoted paths encode each filename byte with at most three octal digits. Accumulate in a wider type and reject values above 0377 instead of relying on an implicit narrowing conversion and wraparound. --- src/parser.cpp | 11 +++++++---- tests/test_strip.cpp | 1 + 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/parser.cpp b/src/parser.cpp index dd3d55b..7b332a5 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -124,20 +124,23 @@ std::string LineParser::parse_quoted_string() case '5': case '6': case '7': { - unsigned char result = static_cast(c) - '0'; + unsigned int result = static_cast(c - '0'); for (int i = 1; i < 3; ++i) { char octal_val = peek(); if (!is_octal(octal_val)) break; - unsigned char digit_val = static_cast(octal_val) - '0'; - result = result * 8 + digit_val; + unsigned int digit_val = static_cast(octal_val - '0'); + result = result * 8U + digit_val; ++m_current; } - output += static_cast(result); + if (result > static_cast(std::numeric_limits::max())) + throw std::invalid_argument("Octal escape is out of range in path " + std::string(begin, m_current)); + + output += static_cast(static_cast(result)); break; } default: diff --git a/tests/test_strip.cpp b/tests/test_strip.cpp index 181d240..14ab5e6 100644 --- a/tests/test_strip.cpp +++ b/tests/test_strip.cpp @@ -109,6 +109,7 @@ TEST(strip_quoted_string_bad) 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) From c6f7414b3ac1a5e39db955b8d74bb14af6390be4 Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sat, 1 Aug 2026 14:39:11 +0200 Subject: [PATCH 07/15] cmdline: make the Windows argument count conversion explicit CommandLineToArgvW returns its argument count through a signed Windows API type. Convert that successful result once to size_t before using it as the capacity for both narrowed argument vectors. --- src/cmdline.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cmdline.cpp b/src/cmdline.cpp index 1fa2669..93c21e2 100644 --- a/src/cmdline.cpp +++ b/src/cmdline.cpp @@ -2,6 +2,7 @@ // Copyright 2022-2023 Shannon Booth #include +#include #include #include #include @@ -29,8 +30,9 @@ CmdLine::CmdLine(int argc, const char* const* argv) if (!wide_argv) throw std::bad_alloc(); - narrowed_argv_str.reserve(m_argc); - narrowed_argv.reserve(m_argc); + const auto argument_count = static_cast(m_argc); + narrowed_argv_str.reserve(argument_count); + narrowed_argv.reserve(argument_count); for (int i = 0; i < m_argc; ++i) { narrowed_argv_str.emplace_back(to_narrow(wide_argv[i])); From 96ee1838276d524fa7f0dc5869c3809b75263714 Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sat, 1 Aug 2026 14:45:36 +0200 Subject: [PATCH 08/15] system: limit Windows reads to the supported size The Windows read API accepts an unsigned-int byte count and reports the result as int. Cap each request at INT_MAX before converting it, while preserving size_t requests on POSIX. --- src/system.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/system.cpp b/src/system.cpp index f1f0655..8764b9a 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -79,7 +79,14 @@ std::string read_tty_until_enter() size_t offset = 0; while (true) { - auto ret = ::read(fd, &buffer[0] + offset, buffer.size() - offset); + const auto available_size = buffer.size() - offset; +#ifdef _WIN32 + const auto read_size = std::min(available_size, static_cast(INT_MAX)); + auto ret = ::read(fd, &buffer[0] + offset, static_cast(read_size)); +#else + const auto read_size = available_size; + auto ret = ::read(fd, &buffer[0] + offset, read_size); +#endif if (ret < 0) { int saved_errno = errno; ::close(fd); @@ -88,7 +95,7 @@ std::string read_tty_until_enter() // Finish if we didn't read up until the end of our buffer, indicating input has finished, or // if the last character given was an enter which means that the user has submitted their answer. - if (buffer.size() - offset != static_cast(ret) || buffer.back() == '\n') { + if (read_size != static_cast(ret) || buffer.back() == '\n') { // Trim to size, any pop any trailing '\n' since that is not part of their answer. buffer.resize(offset + static_cast(ret)); if (!buffer.empty() && buffer.back() == '\n') From 3e2f7f95a61a7fe5c48b1d9c4616f88a32b6b81f Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sat, 1 Aug 2026 14:46:28 +0200 Subject: [PATCH 09/15] system: make Windows API conversions explicit Windows reports errors and process metadata with DWORD while the standard error and test-process interfaces use int. Keep unavoidable conversions in private Win32 helpers and use explicit casts for the remaining API boundaries. --- src/system.cpp | 48 ++++++++++++++++++------------- src/windows_error.h | 33 +++++++++++++++++++++ tests/CMakeLists.txt | 1 + tests/lib/src/test.cpp | 4 +-- tests/lib/src/windows_process.cpp | 16 +++++------ 5 files changed, 72 insertions(+), 30 deletions(-) create mode 100644 src/windows_error.h diff --git a/src/system.cpp b/src/system.cpp index 8764b9a..04597c2 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -20,9 +20,9 @@ #include #ifdef _WIN32 +# include "windows_error.h" # include # include -# include # define close _close # define read _read # define open _open @@ -194,7 +194,7 @@ std::string current_path() const auto size = GetCurrentDirectoryW(requested_size, &result[0]); if (size == 0) - throw std::system_error(GetLastError(), std::system_category(), "Failed getting current directory"); + throw last_win32_error("Failed getting current directory"); result.resize(size); if (size <= requested_size) @@ -317,7 +317,7 @@ std::string temp_directory_path() const auto size = GetTempPathW(requested_size, &result[0]); if (size == 0) - throw std::system_error(GetLastError(), std::system_category(), "Failed getting current directory"); + throw last_win32_error("Failed getting current directory"); result.resize(size); if (size <= requested_size) @@ -379,7 +379,7 @@ void symlink(const std::string& target, const std::string& linkpath) error = GetLastError(); } - throw std::system_error(error, std::system_category(), "Can't create symbolic link " + target + " "); + throw win32_error(error, "Can't create symbolic link " + target + " "); #else int ret = ::symlink(target.c_str(), linkpath.c_str()); if (ret != 0) @@ -518,19 +518,23 @@ void rename(const std::string& old_path, const std::string& new_path) const DWORD attributes = GetFileAttributesW(native_new_path.c_str()); if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_READONLY)) { const DWORD writable_attributes = attributes & ~static_cast(FILE_ATTRIBUTE_READONLY); - if (SetFileAttributesW(native_new_path.c_str(), writable_attributes) == 0) - throw std::system_error(GetLastError(), std::system_category(), "Unable to make file writable " + new_path); + if (SetFileAttributesW(native_new_path.c_str(), writable_attributes) == 0) { + const auto attributes_error = GetLastError(); + throw win32_error(attributes_error, "Unable to make file writable " + new_path); + } if (MoveFileExW(native_old_path.c_str(), native_new_path.c_str(), MOVEFILE_REPLACE_EXISTING) != 0) return; error = GetLastError(); - if (SetFileAttributesW(native_new_path.c_str(), attributes) == 0) - throw std::system_error(GetLastError(), std::system_category(), "Unable to restore permissions to " + new_path); + if (SetFileAttributesW(native_new_path.c_str(), attributes) == 0) { + const auto attributes_error = GetLastError(); + throw win32_error(attributes_error, "Unable to restore permissions to " + new_path); + } } } - throw std::system_error(error, std::system_category(), "Unable to rename " + old_path + " to " + new_path); + throw win32_error(error, "Unable to rename " + old_path + " to " + new_path); #else if (std::rename(old_path.c_str(), new_path.c_str()) != 0) throw std::system_error(errno, std::generic_category(), "Unable to rename " + old_path + " to " + new_path); @@ -546,8 +550,10 @@ void permissions(const std::string& path, perms permissions) const auto native = to_native(path); DWORD attributes = GetFileAttributesW(native.c_str()); - if (attributes == INVALID_FILE_ATTRIBUTES) - throw std::system_error(GetLastError(), std::system_category(), "Unable to set permissions to " + path); + if (attributes == INVALID_FILE_ATTRIBUTES) { + const auto attributes_error = GetLastError(); + throw win32_error(attributes_error, "Unable to set permissions to " + path); + } // No group/owner/all on Windows - if any are set treat as write permissions. const auto write_perms = perms::owner_write | perms::group_write | perms::others_write; @@ -563,8 +569,10 @@ void permissions(const std::string& path, perms permissions) else attributes &= ~FILE_ATTRIBUTE_READONLY; - if (SetFileAttributesW(native.c_str(), attributes) == 0) - throw std::system_error(GetLastError(), std::system_category(), "Unable to set permissions to " + path); + if (SetFileAttributesW(native.c_str(), attributes) == 0) { + const auto attributes_error = GetLastError(); + throw win32_error(attributes_error, "Unable to set permissions to " + path); + } #else if (::chmod(path.c_str(), static_cast(permissions)) != 0) @@ -607,7 +615,7 @@ void permissions(FILE* file, perms permissions) FILE_BASIC_INFO info; if (GetFileInformationByHandleEx(handle, FileBasicInfo, &info, sizeof(info)) == 0) - throw std::system_error(GetLastError(), std::system_category(), "Unable to change permissions"); + throw last_win32_error("Unable to change permissions"); const auto write_permissions = perms::owner_write | perms::group_write | perms::others_write; if ((permissions & write_permissions) == perms::none) @@ -616,7 +624,7 @@ void permissions(FILE* file, perms permissions) info.FileAttributes &= ~static_cast(FILE_ATTRIBUTE_READONLY); if (SetFileInformationByHandle(handle, FileBasicInfo, &info, sizeof(info)) == 0) - throw std::system_error(GetLastError(), std::system_category(), "Unable to change permissions"); + throw last_win32_error("Unable to change permissions"); #else if (::fchmod(fileno(file), static_cast(permissions)) != 0) throw std::system_error(errno, std::generic_category(), "Unable to change permissions"); @@ -632,7 +640,7 @@ perms get_permissions(FILE* file) FILE_BASIC_INFO info; if (GetFileInformationByHandleEx(handle, FileBasicInfo, &info, sizeof(info)) == 0) - throw std::system_error(GetLastError(), std::system_category(), "Unable to get permissions"); + throw last_win32_error("Unable to get permissions"); perms permissions = perms::owner_read | perms::group_read | perms::others_read; if (!(info.FileAttributes & FILE_ATTRIBUTE_READONLY)) @@ -671,14 +679,14 @@ std::wstring to_wide(const std::string& str) int length = MultiByteToWideChar(CP_UTF8, 0, str.data(), static_cast(str.size()), nullptr, 0); if (length == 0) - throw std::system_error(GetLastError(), std::system_category(), "Failed widening string"); + throw last_win32_error("Failed widening string"); std::wstring wide_str; wide_str.resize(static_cast(length)); length = MultiByteToWideChar(CP_UTF8, 0, str.data(), static_cast(str.size()), &wide_str[0], length); if (length == 0) - throw std::system_error(GetLastError(), std::system_category(), "Failed widening string"); + throw last_win32_error("Failed widening string"); return wide_str; } @@ -690,14 +698,14 @@ std::string to_narrow(const std::wstring& str) int length = WideCharToMultiByte(CP_UTF8, 0, str.c_str(), static_cast(str.size()), nullptr, 0, nullptr, nullptr); if (length == 0) - throw std::system_error(GetLastError(), std::system_category(), "Failed narrowing string"); + throw last_win32_error("Failed narrowing string"); std::string narrow_str; narrow_str.resize(static_cast(length)); length = WideCharToMultiByte(CP_UTF8, 0, str.c_str(), static_cast(str.size()), &narrow_str[0], length, nullptr, nullptr); if (length == 0) - throw std::system_error(GetLastError(), std::system_category(), "Failed narrowing string"); + throw last_win32_error("Failed narrowing string"); return narrow_str; } diff --git a/src/windows_error.h b/src/windows_error.h new file mode 100644 index 0000000..32c53c0 --- /dev/null +++ b/src/windows_error.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright 2026 Shannon Booth + +#pragma once + +#include +#include +#include + +namespace Patch { + +inline std::error_code win32_error_code(DWORD error) +{ + return { static_cast(error), std::system_category() }; +} + +class win32_error : public std::system_error { +public: + win32_error(DWORD error, const std::string& message) + : std::system_error(win32_error_code(error), message) + { + } +}; + +class last_win32_error : public win32_error { +public: + explicit last_win32_error(const char* message) + : win32_error(GetLastError(), message) + { + } +}; + +} // namespace Patch diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7dbccac..27294df 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -19,6 +19,7 @@ target_include_directories(patch_test lib/include PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/lib/src + ${PROJECT_SOURCE_DIR}/src ) find_program(GNU_PATCH patch) diff --git a/tests/lib/src/test.cpp b/tests/lib/src/test.cpp index cff19d8..225600c 100644 --- a/tests/lib/src/test.cpp +++ b/tests/lib/src/test.cpp @@ -10,7 +10,7 @@ #ifdef _WIN32 # include # include -# include +# include #else # include #endif @@ -52,7 +52,7 @@ void skip_without_symlink_support() filesystem::symlink("target", "symlink-support-probe"); filesystem::remove("symlink-support-probe"); } catch (const std::system_error& error) { - if (error.code() != std::error_code(ERROR_PRIVILEGE_NOT_HELD, std::system_category())) + if (error.code() != win32_error_code(ERROR_PRIVILEGE_NOT_HELD)) throw; skip_test("creating a symbolic link requires a privilege this environment does not grant"); } diff --git a/tests/lib/src/windows_process.cpp b/tests/lib/src/windows_process.cpp index c92daa5..695f5ba 100644 --- a/tests/lib/src/windows_process.cpp +++ b/tests/lib/src/windows_process.cpp @@ -5,24 +5,24 @@ #include #include #include -#include +#include class Pipe { public: explicit Pipe(bool inherit_for_read = true) { SECURITY_ATTRIBUTES attr; - attr.nLength = sizeof(SECURITY_ATTRIBUTES); + attr.nLength = static_cast(sizeof(SECURITY_ATTRIBUTES)); attr.bInheritHandle = true; attr.lpSecurityDescriptor = nullptr; if (!CreatePipe(&m_read_handle, &m_write_handle, &attr, 0)) - throw std::system_error(GetLastError(), std::system_category(), "Failed creating stdout pipe"); + throw Patch::last_win32_error("Failed creating stdout pipe"); if (!SetHandleInformation(inherit_for_read ? m_read_handle : m_write_handle, HANDLE_FLAG_INHERIT, 0)) { CloseHandle(m_read_handle); CloseHandle(m_write_handle); - throw std::system_error(GetLastError(), std::system_category(), "Failed setting handle information"); + throw Patch::last_win32_error("Failed setting handle information"); } } @@ -115,7 +115,7 @@ Process::Process(const char* cmd, const std::vector& args, const st PROCESS_INFORMATION process_info {}; STARTUPINFOW start_info {}; - start_info.cb = sizeof(start_info); + start_info.cb = static_cast(sizeof(start_info)); start_info.hStdOutput = stdout_pipe.write_handle(); start_info.hStdError = stderr_pipe.write_handle(); start_info.hStdInput = stdin_pipe.read_handle(); @@ -136,7 +136,7 @@ Process::Process(const char* cmd, const std::vector& args, const st &process_info); // receives PROCESS_INFORMATION if (ret == 0) - throw std::system_error(GetLastError(), std::system_category(), "Failed creating process"); + throw Patch::last_win32_error("Failed creating process"); stdout_pipe.close_write_handle(); stderr_pipe.close_write_handle(); @@ -167,7 +167,7 @@ Process::Process(const char* cmd, const std::vector& args, const st CloseHandle(process_info.hThread); if (ret == 0) - throw std::system_error(GetLastError(), std::system_category(), "Failed getting process exit code"); + throw Patch::last_win32_error("Failed getting process exit code"); - m_return_code = exit_code; + m_return_code = static_cast(exit_code); } From b24f476d1dcd2ad7d71233ed9bd863988fd30ec4 Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sat, 1 Aug 2026 14:46:43 +0200 Subject: [PATCH 10/15] system: complement Windows attribute flags as DWORD Keep the read-only attribute mask in the DWORD domain before complementing it. This avoids sign-extending the integer literal before applying it to Windows file attributes. --- src/system.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/system.cpp b/src/system.cpp index 04597c2..fefe012 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -567,7 +567,7 @@ void permissions(const std::string& path, perms permissions) if (should_be_read_only) attributes |= FILE_ATTRIBUTE_READONLY; else - attributes &= ~FILE_ATTRIBUTE_READONLY; + attributes &= ~static_cast(FILE_ATTRIBUTE_READONLY); if (SetFileAttributesW(native.c_str(), attributes) == 0) { const auto attributes_error = GetLastError(); From fae9260c5f2ad78f0fd0a02f15bd6b51d0f7ace4 Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sat, 1 Aug 2026 15:11:02 +0200 Subject: [PATCH 11/15] tests: only inspect executable permissions on POSIX --- tests/test_basic.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_basic.cpp b/tests/test_basic.cpp index b5408c1..5d1f991 100644 --- a/tests/test_basic.cpp +++ b/tests/test_basic.cpp @@ -2485,8 +2485,8 @@ new mode 100755 EXPECT_EQ(process.return_code(), 0); EXPECT_FILE_EQ("file", to_patch); - const auto perms = Patch::filesystem::get_permissions("file"); #ifndef _WIN32 + const auto perms = Patch::filesystem::get_permissions("file"); EXPECT_TRUE((perms & Patch::filesystem::perms::owner_exec) != Patch::filesystem::perms::none); EXPECT_TRUE((perms & Patch::filesystem::perms::group_exec) != Patch::filesystem::perms::none); EXPECT_TRUE((perms & Patch::filesystem::perms::others_exec) != Patch::filesystem::perms::none); From 1c0ea4a4fb1f03db2c2985f1511e5548ca80e7d5 Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sat, 1 Aug 2026 15:11:16 +0200 Subject: [PATCH 12/15] tests: mark the symlink patch path unused on Windows --- tests/test_basic.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_basic.cpp b/tests/test_basic.cpp index 5d1f991..3894bbf 100644 --- a/tests/test_basic.cpp +++ b/tests/test_basic.cpp @@ -2876,7 +2876,9 @@ COMPAT_TEST(reversed_patch_batch) COMPAT_TEST(basic_add_symlink_file) { -#ifndef _WIN32 +#ifdef _WIN32 + (void)patch_path; +#else { Patch::File file("diff.patch", std::ios_base::out); From 65ecac8edf08dd5ee1c24d06a0904492edadc0f1 Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sat, 1 Aug 2026 15:29:03 +0200 Subject: [PATCH 13/15] tests: mark POSIX-only quoting paths unused on Windows Several compatibility cases use characters that Windows filenames cannot contain. Explicitly consume the generated patch path in their empty Windows branches. --- tests/test_quoting.cpp | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/tests/test_quoting.cpp b/tests/test_quoting.cpp index c5685c1..1b22e30 100644 --- a/tests/test_quoting.cpp +++ b/tests/test_quoting.cpp @@ -60,7 +60,9 @@ COMPAT_TEST(quoting_c_style_single_quote) COMPAT_TEST(quoting_c_style_double_quote) { -#ifndef _WIN32 +#ifdef _WIN32 + (void)patch_path; +#else Patch::set_env("QUOTING_STYLE", "c"); test_quoting(patch_path, "with\"quote", "patching file \"with\\\"quote\"\n"); #endif @@ -68,7 +70,9 @@ COMPAT_TEST(quoting_c_style_double_quote) COMPAT_TEST(quoting_c_style_backslash) { -#ifndef _WIN32 +#ifdef _WIN32 + (void)patch_path; +#else Patch::set_env("QUOTING_STYLE", "c"); test_quoting(patch_path, "with\\slash", "patching file \"with\\\\slash\"\n"); #endif @@ -76,7 +80,9 @@ COMPAT_TEST(quoting_c_style_backslash) COMPAT_TEST(quoting_c_style_newline) { -#ifndef _WIN32 +#ifdef _WIN32 + (void)patch_path; +#else Patch::set_env("QUOTING_STYLE", "c"); test_quoting(patch_path, "with\nnewline", R"("with\nnewline")", "patching file \"with\\nnewline\"\n"); #endif @@ -84,7 +90,9 @@ COMPAT_TEST(quoting_c_style_newline) COMPAT_TEST(quoting_c_style_tab) { -#ifndef _WIN32 +#ifdef _WIN32 + (void)patch_path; +#else Patch::set_env("QUOTING_STYLE", "c"); test_quoting(patch_path, "with\ttab", R"("with\ttab")", "patching file \"with\\ttab\"\n"); #endif @@ -104,7 +112,9 @@ COMPAT_TEST(quoting_shell_always_style_single_quote_only) COMPAT_TEST(quoting_shell_always_style_double_quote_only) { -#ifndef _WIN32 +#ifdef _WIN32 + (void)patch_path; +#else Patch::set_env("QUOTING_STYLE", "shell-always"); test_quoting(patch_path, "with\"quote", "patching file 'with\"quote'\n"); #endif @@ -112,7 +122,9 @@ COMPAT_TEST(quoting_shell_always_style_double_quote_only) COMPAT_TEST(quoting_shell_style_double_quote_only) { -#ifndef _WIN32 +#ifdef _WIN32 + (void)patch_path; +#else Patch::set_env("QUOTING_STYLE", "shell"); test_quoting(patch_path, "with\"quote", "patching file 'with\"quote'\n"); #endif From fef5a72293b6e270bbe89646d912f32fe6ae8811 Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sat, 1 Aug 2026 14:23:45 +0200 Subject: [PATCH 14/15] meta: treat compiler warnings as errors in CI --- .github/workflows/cmake.yml | 6 +++--- .github/workflows/msvc.yml | 2 +- CMakeLists.txt | 27 +++++++++++++++++++++++++++ app/CMakeLists.txt | 1 + tests/CMakeLists.txt | 4 ++++ 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 1c69a17..1bd3a78 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -27,7 +27,7 @@ jobs: run: sudo apt-get update -qy && sudo apt-get install gcovr -qy - name: configure - run: cmake -DBUILD_TESTING=on -DPATCH_ENABLE_COVERAGE=yes -S . -B build + run: cmake -DBUILD_TESTING=on -DPATCH_ENABLE_COVERAGE=yes -DPATCH_WARNINGS_AS_ERRORS=on -S . -B build - name: compile run: cmake --build build -j2 - name: test @@ -49,7 +49,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: configure - run: cmake -DBUILD_TESTING=on -S . -B build + run: cmake -DBUILD_TESTING=on -DPATCH_WARNINGS_AS_ERRORS=on -S . -B build - name: compile run: cmake --build build -j2 - name: test @@ -73,7 +73,7 @@ jobs: mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-ninja - name: configure - run: cmake -G Ninja -DBUILD_TESTING=on -S . -B build + run: cmake -G Ninja -DBUILD_TESTING=on -DPATCH_WARNINGS_AS_ERRORS=on -S . -B build - name: compile run: cmake --build build -j2 - name: test diff --git a/.github/workflows/msvc.yml b/.github/workflows/msvc.yml index 140f26a..fd91eed 100644 --- a/.github/workflows/msvc.yml +++ b/.github/workflows/msvc.yml @@ -26,7 +26,7 @@ jobs: uses: actions/checkout@v6 - name: Configure CMake - run: cmake -B ${{ env.build }} -DCMAKE_BUILD_TYPE=${{ env.config }} + run: cmake -B ${{ env.build }} -DCMAKE_BUILD_TYPE=${{ env.config }} -DPATCH_WARNINGS_AS_ERRORS=on - name: Initialize MSVC Code Analysis uses: microsoft/msvc-code-analysis-action@v0.1.1 diff --git a/CMakeLists.txt b/CMakeLists.txt index f365d3b..241e518 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,8 +22,34 @@ include(GNUInstallDirs) include(coverage) option(PATCH_ENABLE_COVERAGE "Build with gcov support" OFF) +option(PATCH_WARNINGS_AS_ERRORS "Enable strict compiler warnings and treat them as errors" OFF) option(BUILD_TESTING "Build the tests" OFF) +function(patch_enable_warnings target_name) + if(NOT PATCH_WARNINGS_AS_ERRORS) + return() + endif() + + if(MSVC) + target_compile_options(${target_name} PRIVATE /W4 /WX) + target_compile_definitions(${target_name} PRIVATE + _CRT_NONSTDC_NO_WARNINGS + _CRT_SECURE_NO_WARNINGS + ) + elseif(CMAKE_CXX_COMPILER_ID MATCHES "^(AppleClang|Clang|GNU)$") + target_compile_options(${target_name} PRIVATE + -Wall + -Wextra + -Wpedantic + -Wconversion + -Wsign-conversion + -Werror + ) + else() + message(FATAL_ERROR "PATCH_WARNINGS_AS_ERRORS does not support ${CMAKE_CXX_COMPILER_ID}") + endif() +endfunction() + if(PATCH_ENABLE_COVERAGE) add_coverage_flags() endif() @@ -39,6 +65,7 @@ add_library(patch src/system.cpp src/file.cpp ) +patch_enable_warnings(patch) target_compile_features(patch PUBLIC cxx_std_11) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 35f57c6..1bd3e28 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -2,6 +2,7 @@ # Copyright 2022 Shannon Booth add_executable(sb_patch main.cpp) +patch_enable_warnings(sb_patch) target_link_libraries(sb_patch PRIVATE patch::patch) install(TARGETS sb_patch diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 27294df..c736007 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -13,6 +13,7 @@ unset(CMAKE_REQUIRED_LIBRARIES) configure_file(lib/src/config.h.in lib/src/config.h) add_library(patch_test lib/src/test.cpp) +patch_enable_warnings(patch_test) target_link_libraries(patch_test PUBLIC patch) target_include_directories(patch_test PUBLIC @@ -83,9 +84,11 @@ add_executable(patch_oom ${PROJECT_SOURCE_DIR}/src/options.cpp ${PROJECT_SOURCE_DIR}/app/main.cpp ) +patch_enable_warnings(patch_oom) target_include_directories(patch_oom PRIVATE ${PROJECT_SOURCE_DIR}/include) add_executable(test_oom test_oom.cpp) +patch_enable_warnings(test_oom) target_link_libraries(test_oom PRIVATE patch_test) patch_add_tests(test_oom patch_oom) @@ -109,6 +112,7 @@ add_executable(patch_tests test_reject.cpp test_strip.cpp ) +patch_enable_warnings(patch_tests) target_link_libraries(patch_tests PRIVATE patch_test) if(HAVE_FORKPTY_PTY OR HAVE_FORKPTY_UTIL) From fb16a93ca965b26167149b9352f659636aa50984 Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sat, 1 Aug 2026 15:10:37 +0200 Subject: [PATCH 15/15] meta: use minimal Windows headers for project targets Define WIN32_LEAN_AND_MEAN and NOMINMAX privately for every project target. Include the Shell API after its Windows header prerequisite now that it is no longer provided transitively. This reduces the SDK surface and avoids min and max macros without passing the policy to consumers. --- CMakeLists.txt | 11 +++++++++-- app/CMakeLists.txt | 2 +- src/cmdline.cpp | 3 +++ tests/CMakeLists.txt | 8 ++++---- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 241e518..20680d3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,7 +25,14 @@ option(PATCH_ENABLE_COVERAGE "Build with gcov support" OFF) option(PATCH_WARNINGS_AS_ERRORS "Enable strict compiler warnings and treat them as errors" OFF) option(BUILD_TESTING "Build the tests" OFF) -function(patch_enable_warnings target_name) +function(patch_configure_target target_name) + if(WIN32) + target_compile_definitions(${target_name} PRIVATE + NOMINMAX + WIN32_LEAN_AND_MEAN + ) + endif() + if(NOT PATCH_WARNINGS_AS_ERRORS) return() endif() @@ -65,7 +72,7 @@ add_library(patch src/system.cpp src/file.cpp ) -patch_enable_warnings(patch) +patch_configure_target(patch) target_compile_features(patch PUBLIC cxx_std_11) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 1bd3e28..5379870 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -2,7 +2,7 @@ # Copyright 2022 Shannon Booth add_executable(sb_patch main.cpp) -patch_enable_warnings(sb_patch) +patch_configure_target(sb_patch) target_link_libraries(sb_patch PRIVATE patch::patch) install(TARGETS sb_patch diff --git a/src/cmdline.cpp b/src/cmdline.cpp index 93c21e2..95b5731 100644 --- a/src/cmdline.cpp +++ b/src/cmdline.cpp @@ -12,6 +12,9 @@ #ifdef _WIN32 # include + +// shellapi.h depends on types and macros declared by windows.h. +# include #endif namespace Patch { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c736007..76c372f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -13,7 +13,7 @@ unset(CMAKE_REQUIRED_LIBRARIES) configure_file(lib/src/config.h.in lib/src/config.h) add_library(patch_test lib/src/test.cpp) -patch_enable_warnings(patch_test) +patch_configure_target(patch_test) target_link_libraries(patch_test PUBLIC patch) target_include_directories(patch_test PUBLIC @@ -84,11 +84,11 @@ add_executable(patch_oom ${PROJECT_SOURCE_DIR}/src/options.cpp ${PROJECT_SOURCE_DIR}/app/main.cpp ) -patch_enable_warnings(patch_oom) +patch_configure_target(patch_oom) target_include_directories(patch_oom PRIVATE ${PROJECT_SOURCE_DIR}/include) add_executable(test_oom test_oom.cpp) -patch_enable_warnings(test_oom) +patch_configure_target(test_oom) target_link_libraries(test_oom PRIVATE patch_test) patch_add_tests(test_oom patch_oom) @@ -112,7 +112,7 @@ add_executable(patch_tests test_reject.cpp test_strip.cpp ) -patch_enable_warnings(patch_tests) +patch_configure_target(patch_tests) target_link_libraries(patch_tests PRIVATE patch_test) if(HAVE_FORKPTY_PTY OR HAVE_FORKPTY_UTIL)