215 Commits
Author SHA1 Message Date
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
Shannon Booth ebc8829df7 tests: check the result of _wmktemp_s rather than errno
_wmktemp_s reports a failure through its (positive) errno_t return
value, so comparing it against zero from below never caught an error.
2026-08-01 18:50:32 +02:00
Shannon Booth cd41a57bb4 tests: probe fixture files through the test library
Tests reach for the filesystem helpers exported by the patching code to
check that a file exists or to make a directory for a fixture. Route
those checks through helpers in the library the tests already link, so
that the tests do not care how the patching code spells its filesystem
access.

This keeps a later change to those helpers out of every test file.
2026-08-01 18:50:32 +02:00
Shannon Booth cb7ebfd02f 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.
2026-08-01 15:33:12 +02:00
Shannon Booth 9b9e58f166 meta: treat compiler warnings as errors in CI 2026-08-01 15:33:12 +02:00
Shannon Booth 5b09bf0032 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.
2026-08-01 15:33:12 +02:00
Shannon Booth 687562ea8e tests: mark the symlink patch path unused on Windows 2026-08-01 15:33:12 +02:00
Shannon Booth 2acbb60e04 tests: only inspect executable permissions on POSIX 2026-08-01 15:33:12 +02:00
Shannon Booth e51ad98033 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.
2026-08-01 15:33:12 +02:00
Shannon Booth 5f4812b199 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.
2026-08-01 15:33:12 +02:00
Shannon Booth 1f1d6c5a92 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.
2026-08-01 15:33:12 +02:00
Shannon Booth dd7cd56772 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.
2026-08-01 15:33:12 +02:00
Shannon Booth c68791ca53 tests: avoid relying on system error text
System error messages vary across standard libraries and locales. Make
the test suite portable in such situations.
2026-08-01 13:53:10 +02:00
Shannon Booth 8897bb9e82 tests: create the temporary test directory in the test library
Making a temporary directory is only ever done to give a test somewhere
to run. Keeping it in the library the tests already link means the
patching code does not carry an entry point nothing in it uses.
2026-08-01 12:03:54 +02:00
Shannon Booth 859367ccf8 tests: add skip helper for if symbolic link capability is unavailable
So other tests can easily skip tests under this condition.
2026-08-01 12:03:54 +02:00
Shannon Booth 7275cec1fe patch: write empty deletion results to explicit output files
-o specifies where to write the patched contents rather than a file to
modify in place. When a deletion patch produces empty output, write an
empty file instead of deleting the explicitly named output and its
parent directories.
2026-07-31 22:16:34 +02:00
Shannon Booth 0da032e361 patch: create the directories a rename or copy needs
Only an added file had its parent directories created, so renaming or
copying into a directory which does not exist yet failed when the
result could not be staged next to its destination.
2026-07-26 10:09:55 +02:00
Shannon Booth 2d183438de patch: only defer the writes which need deferring
Every regular write for a Git patch was held back until the whole input
had been read, so a later patch in the same input could not see what an
earlier one had written. Two commits touching the same file could then
fail to apply, and worse, the second patch then committed the file it
had read unchanged, silently undoing the first.

Only a rename or a copy needs to be held back, so that two renames which
swap a pair of files both read the original content. Install everything
else straight away.
2026-07-25 22:16:53 +02:00
Shannon Booth bf4b6699fd applier: do not read past the end of the file being patched
Fuzz can match a hunk whose trailing context runs past the end of the
file, since that context is dropped rather than compared. Those lines
were still written out from the input, reading it out of range, and the
resulting std::out_of_range escaped as the diagnostic "**** vector".
Skip context and removals which have no line left to consume.
2026-07-25 22:02:58 +02:00
Shannon Booth fafcc7d2f7 locator: keep a hunk with no context inside the file
A hunk with no surrounding context is placed wherever its range claims
it belongs, which may be beyond the end of the file, or before the start
of it once an offset from an earlier hunk is taken into account.
Applying such a patch read the input lines out of range, and the
resulting std::out_of_range escaped as the diagnostic "**** vector".

Clamp the guess into the file, so that a location is always somewhere
the applier can write.
2026-07-25 22:02:58 +02:00
Shannon Booth 59dbc08cb9 patch: back up a file before it is removed
Both paths which remove a file did so without a backup, so -b silently
preserved nothing. A backup was only ever taken by
write_patched_result_to_file, and only for the output path, which is not
reached at all when nothing is written.

Take the backup on both paths while the file is still there.
2026-07-25 21:15:02 +02:00
Shannon Booth 0f21e675b5 patch: refuse patch names which could escape the directory being patched
A name taken from a patch was used as is, so an absolute name or one
which kept a '..' component after -p stripping could read and write
anywhere on the filesystem.

This seems to have been settled on as 'unsafe', so refuse such paths
after stripping, so that stripping may still turn an unsafe name into a
safe one.
2026-07-25 20:00:41 +02:00
Shannon Booth 7a8994b2b9 system: do not treat a git submodule mode as a symbolic link
A file mode holds a file type above its permission bits, and POSIX
requires selecting that type with S_IFMT and comparing it for equality.
is_symlink instead tested whether 0120000's bits were set, which also
matched git's 0160000 "gitlink" mode, since 0120000's type bits are a
subset of 0160000's.
2026-07-25 18:32:49 +02:00
Shannon Booth e32d89c939 locator: keep hunks at the start or end of a file
Hunks at the start or end of a file may have less context on that side.
The locator previously ignored this and could apply them elsewhere with
--fuzz=0.

Keep them at the start or end until enough fuzz is allowed.

I think this also explains/fixes the behavior I was previously confused
about when a hunk from the beginning of a file is found later. The
missing leading context marks the beginning of the original file, so
moving the hunk requires fuzz even when the remaining context matches.
The same applies to missing trailing context at the end of a file.
2026-07-25 11:20:19 +02:00
Shannon Booth b55996f81e applier: avoid double-counting rejected hunk offsets
Rejected hunk ranges are adjusted by line-count changes from earlier
hunks before they are written. The failure diagnostic added the same
adjustment again, reporting a line one too large after an insertion.

Use the already-adjusted hunk range when reporting a failure.
2026-07-25 11:20:19 +02:00
Shannon Booth 6a6259c729 tests: declare compatibility per test
GNU comparison was controlled per test executable. That kept process
tests in test_unit from being compared, while enabling comparison there
would also run its unit tests twice.

Introduce COMPAT_TEST for cases that run against both sb_patch and GNU
patch. Keep PATCH_TEST for project-specific behavior and TEST for unit
coverage. Mark stable GNU differences as expected compatibility failures.
Keep host-dependent ed behavior project-only because GNU delegates those
scripts to an external editor.

Merge the normal and conditional PTY tests into one patch_tests runner
now that executable boundaries no longer control GNU comparison. Keep
test_oom separate because it runs patch_oom.
2026-07-25 09:53:53 +02:00
Shannon Booth 59c7ef8ba4 patch: warn before forcing creation over an existing file
An add patch describes an empty or nonexistent input. When the target
already exists, --force still attempts the hunk, but the user was not
told why the patch no longer applied cleanly. Start reporting a warning
for this situation.
2026-07-24 22:49:23 +02:00
Shannon Booth e821111e3b patch: create parent directories for backups
Create the parents of the fully computed backup name before staging
it. This supports nested output paths and backup prefixes that place the
backup in a separate directory tree.
2026-07-24 22:20:05 +02:00
Shannon Booth a6f7b81de0 meta: honor GNU install directories
Install library artifacts using GNUInstallDirs variables and rely on
the target install interface for include paths. Add an external consumer
test with non-default library and include directories.
2026-07-24 22:05:07 +02:00
Shannon Booth 96d8ac4b18 patch: replace files through same-directory temporaries
Do not truncate or make a destination writable before its replacement
and requested backup are ready. Stage patched contents in a temporary
file beside the destination. Preserve its mode and rename it into place
only when the replacement is ready.

This leaves a destination unchanged if preparing its replacement or
backup fails, and prevents partially written contents from becoming
visible. Git-style writes remain deferred while their inputs are needed,
but completed files remain applied if a later patch fails.
2026-07-24 21:16:36 +02:00
Shannon Booth a4c32ee71d system: do not follow symlinks in is_regular_file 2026-07-24 16:58:20 +02:00
Shannon Booth 970ca705a2 tests: support skipping tests at runtime
Add skip_test(reason), which aborts the running test and reports it as
skipped rather than passed or failed. The single test runner exits with 77
for a skip, and CTest is told via SKIP_RETURN_CODE 77 to mark it skipped.
run_all_tests counts and prints skips separately.

This lets tests that require a capability the environment may lack (such as
creating symbolic links without privilege) opt out cleanly instead of
failing.
2026-07-24 16:58:20 +02:00
Shannon Booth 51790e6e01 patch: support applying ed-style diffs
Add native parsing and application for scripts produced by diff -e.
Support numeric append, change, and delete commands, including ranges,
along with the s/.// command emitted when inserted text begins with a
literal dot.

Detect ed scripts automatically or select them explicitly with -e.
Execute commands sequentially against an in-memory copy of the input,
avoiding a dependency on the external ed utility and preventing invalid
addresses from partially rewriting the destination.

This intentionally only implements the grammar emitted by diff -e rather
than the complete ed command language. Ed scripts contain no original
text or file names, so the target normally needs to be supplied as an
operand, and reversing with -R is unsupported (as mentioned by POSIX).
The --ifdef option is also rejected for ed patches.
2026-07-21 15:43:08 +02:00
Shannon Booth 15358ea32a patch: avoid missing-file prompts in noninteractive modes
When no target file could be selected, process_patch always prompted
through /dev/tty, even when --force or --batch was specified.
2026-07-20 14:14:48 +02:00
Shannon Booth f5280bb852 parser: recognize additions with named file headers
Commit fcf718f stopped inferring additions from empty old ranges when
file headers were present. As a result, patches creating files without a
/dev/null header were classified as changes, leaving the nonexistent
destination unselected and causing patch to prompt for a file.

Restore addition inference for zero-start old ranges while retaining the
rule that an empty new range with named headers only empties the file
rather than deleting it.
2026-07-20 14:14:48 +02:00
Shannon Booth 6b05c297b4 file: handle reopening existing streams correctly
File::open replaced the current FILE pointer without first closing it,
leaking the previous handle. It also retained the cached EOF and error
state, so successfully reopening a File after reaching EOF could leave
the new stream unusable.

Open the new file before modifying the existing stream. On success,
close the previous handle, install the new one, and reset the cached
state. If opening fails, preserve the existing handle, position, and
stream state.

Current call sites only open default-constructed File objects, so this
is a latent public API issue rather than a bug triggered by existing
usage.
2026-07-20 10:37:58 +02:00
Shannon Booth 41e80c30d3 parser: correctly count inclusive normal diff ranges
Normal diff ranges specify inclusive start and end line numbers. We were
storing the old range endpoint directly as the number of lines, causing
a range such as 5,7c5,7 to expect seven old lines instead of three and
reject an otherwise valid patch.

Convert inclusive endpoints to line counts using checked arithmetic for
both old and new ranges. Reject descending ranges and ranges whose line
count would overflow.
2026-07-20 10:18:48 +02:00
Shannon Booth ee1e1edff3 tests: cover CRLF preservation for deferred git writes 2026-07-20 09:09:06 +02:00
Shannon Booth fcf718f006 parser: avoid deleting files for unified diffs that only empty them
A unified diff that removes all lines from a file uses a new file range of
0,0, but that does not mean the file should be deleted. We were inferring
Operation::Delete from that hunk header alone, so applying such a patch
removed the target file entirely instead of leaving it as an empty file.

Match traditional patch behavior by only inferring add/delete from /dev/null
headers when file paths are present, and keep the old hunk-range fallback for
headerless cases.
2026-04-05 14:25:05 +02:00
Shannon Booth 3bfafb0b93 applier: fix pluralization for negative hunk offsets 2026-04-05 13:18:17 +02:00
Shannon Booth b52836c1ef applier: handle empty input correctly when closing --ifdef hunks 2026-04-05 12:46:57 +02:00
Shannon Booth 951a9b2542 tests: improve assertion output to better show whitespace differences 2026-04-05 12:46:57 +02:00
Shannon Booth bd7ff508df patch: do not issue spurious warning removing a file in dry-run
Since dry run was not writing to the output file, the file size was
being determined as non-empty. To fix this, simply check for this on the
temporary file which has been written to, even on a dry run.
2024-02-11 13:55:39 +13:00
Shannon Booth 6059f85d23 patch: also write reject file when outputting to stdout 2024-02-05 12:13:18 +13:00
Shannon Booth fbd5c6f0c8 options: update copyright in help message
It's 2024 now :^)
2024-02-04 16:15:34 +13:00
Shannon Booth f9e893c6c1 tests: add a test for a failure applying patch adding file
It seems that for this case we are assuming that everything is fine, and
will apply the patch even though:

 1. The file already exists! (GNU patch seems to warn for this case)
 2. Even after confirming that we do not mind that the file already
    exists, we do not detect that there the content in that file
    _should_ cause that patch to fail.
2023-11-11 20:23:10 +13:00
Shannon Booth d3b466c0e4 cmdline: factor out a cmdline handler to make option parsing generic
The cmdline parser can now be used for parsing commandlines other than
just what we expect for patch. This is done by factoring out a class
that has a virtual process_option function. Inheriting command line
handlers can implement this to interpret the given option in the
application specific method.
2023-06-24 12:23:47 +12:00
Shannon Booth d70b768359 parser: factor some functions to make use of line parser
In particular, this has the nice side effect of slightly simplifying
parse_git_header_name to not have a dead check for the git header.

We are also able to remove LineParser::end() and LineParser::current()
with these changes.

Finally, this allows us to get rid of the substr with magic number
checks when parsing file names.
2023-05-14 14:15:07 +12:00
Shannon Booth c2b9866161 parser: ignore file modes that have an invalid format
Instead of our previous approach of aborting with an exception. While
making this change, also handle a few more cases of the mode string
being invalid.
2023-05-14 14:15:07 +12:00
Shannon Booth f2361a54a9 tests: add a test for a bad --newline-output option 2023-05-14 14:15:07 +12:00