Skip to content

Fixes #68

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
end_of_line = lf
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
__pycache__
*.pyc
*.pyo
./*.egg-info
./build
./dist
./.eggs
150 changes: 96 additions & 54 deletions patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ def xisabs(filename):
elif re.match(b'\\w:[\\\\/]', filename): # Windows
return True
return False


def xnormpath(path):
""" Cross-platform version of os.path.normpath """
Expand Down Expand Up @@ -697,8 +698,8 @@ def _normalize_filenames(self):
for i,p in enumerate(self.items):
if debugmode:
debug(" patch type = " + p.type)
debug(" source = " + p.source)
debug(" target = " + p.target)
debug(" source = " + p.source.decode(encoding="utf-8"))
debug(" target = " + p.target.decode(encoding="utf-8"))
if p.type in (HG, GIT):
# TODO: figure out how to deal with /dev/null entries
debug("stripping a/ and b/ prefixes")
Expand Down Expand Up @@ -730,16 +731,24 @@ def _normalize_filenames(self):
while p.target.startswith(b".." + sep):
p.target = p.target.partition(sep)[2]
# absolute paths are not allowed
if xisabs(p.source) or xisabs(p.target):
warning("error: absolute paths are not allowed - file no.%d" % (i+1))
self.warnings += 1
if xisabs(p.source):
warning("stripping absolute path from source name '%s'" % p.source)
p.source = xstrip(p.source)
if xisabs(p.target):
warning("stripping absolute path from target name '%s'" % p.target)
p.target = xstrip(p.target)


allowedPaths = {b"/dev/null"}
if xisabs(p.source):
if p.source not in allowedPaths:
warning("error: absolute source paths are not allowed - file no.%d" % (i+1))
self.warnings += 1
if xisabs(p.source):
warning("stripping absolute path from source name '%s'" % p.source)
p.source = xstrip(p.source)

if xisabs(p.target):
if p.target not in allowedPaths:
warning("error: absolute target paths are not allowed - file no.%d" % (i+1))
self.warnings += 1
if xisabs(p.target):
warning("stripping absolute path from source name '%s'" % p.target)
p.source = xstrip(p.target)

self.items[i].source = p.source
self.items[i].target = p.target

Expand Down Expand Up @@ -803,22 +812,30 @@ def diffstat(self):

def findfile(self, old, new):
""" return name of file to be patched or None """
if exists(old):
return old
elif exists(new):
return new
if exists(old) and exists(new):
return old, new
else:
# [w] Google Code generates broken patches with its online editor
debug("broken patch from Google Code, stripping prefixes..")
if old.startswith(b'a/') and new.startswith(b'b/'):
old, new = old[2:], new[2:]
oldPrefixed = old.startswith(b'a/')
newPrefixed = new.startswith(b'b/')
oldIsDevNull = False
newIsDevNull = False
if not oldPrefixed:
if old in {b"/dev/null"}:
oldIsDevNull = True
if not newPrefixed:
if new in {b"/dev/null"}:
newIsDevNull = True
if (oldPrefixed or oldIsDevNull) and (newPrefixed or newIsDevNull):
if oldPrefixed:
old = old[2:]
if newPrefixed:
new = new[2:]
debug(" %s" % old)
debug(" %s" % new)
if exists(old):
return old
elif exists(new):
return new
return None
return old, new
return None, None


def apply(self, strip=0, root=None):
Expand Down Expand Up @@ -854,28 +871,33 @@ def apply(self, strip=0, root=None):
else:
old, new = p.source, p.target

filename = self.findfile(old, new)
resolvedOld, resolvedNew = self.findfile(old, new)
isDevNull = resolvedOld in {b"/dev/null"}

if not filename:
if not resolvedOld:
warning("source/target file does not exist:\n --- %s\n +++ %s" % (old, new))
errors += 1
continue
if not isfile(filename):
warning("not a file - %s" % filename)
errors += 1
continue
if not isfile(resolvedOld):
if not isDevNull:
warning("not a file - %s" % resolvedOld)
errors += 1
continue

# [ ] check absolute paths security here
debug("processing %d/%d:\t %s" % (i+1, total, filename))
debug("processing %d/%d:\t %s -> %s" % (i+1, total, resolvedOld, resolvedNew))

# validate before patching
f2fp = open(filename, 'rb')
if not isDevNull:
f2fp = open(resolvedOld, 'rb')
else:
f2fp = ()
hunkno = 0
hunk = p.hunks[hunkno]
hunkfind = []
hunkreplace = []
validhunks = 0
canpatch = False
canpatch = isDevNull
for lineno, line in enumerate(f2fp):
if lineno+1 < hunk.startsrc:
continue
Expand All @@ -892,7 +914,8 @@ def apply(self, strip=0, root=None):
if line.rstrip(b"\r\n") == hunkfind[hunklineno]:
hunklineno+=1
else:
info("file %d/%d:\t %s" % (i+1, total, filename))
errors += 1
info("file %d/%d:\t %s" % (i+1, total, resolvedOld))
info(" hunk no.%d doesn't match source file at line %d" % (hunkno+1, lineno+1))
info(" expected: %s" % hunkfind[hunklineno])
info(" actual : %s" % line.rstrip(b"\r\n"))
Expand All @@ -913,7 +936,7 @@ def apply(self, strip=0, root=None):

# check if processed line is the last line
if lineno+1 == hunk.startsrc+len(hunkfind)-1:
debug(" hunk no.%d for file %s -- is ready to be patched" % (hunkno+1, filename))
debug(" hunk no.%d for file %s -- is ready to be patched" % (hunkno+1, resolvedOld))
hunkno+=1
validhunks+=1
if hunkno < len(p.hunks):
Expand All @@ -924,35 +947,49 @@ def apply(self, strip=0, root=None):
canpatch = True
break
else:
if hunkno < len(p.hunks):
warning("premature end of source file %s at hunk %d" % (filename, hunkno+1))
errors += 1
if not isDevNull:
if hunkno < len(p.hunks):
warning("premature end of source file %s at hunk %d" % (resolvedOld, hunkno+1))
errors += 1
else:
pass

f2fp.close()
if not isinstance(f2fp, tuple): # techtonic, I am not going to do your job instead of you. It is YOUR responsibility to use context managers instead of this shit
f2fp.close()

if validhunks < len(p.hunks):
if self._match_file_hunks(filename, p.hunks):
warning("already patched %s" % filename)
if not isDevNull and validhunks < len(p.hunks):
if self._match_file_hunks(resolvedOld, p.hunks):
warning("already patched %s" % resolvedOld)
else:
warning("source file is different - %s" % filename)
warning("source file is different - %s" % resolvedOld)
errors += 1
if canpatch:
backupname = filename+b".orig"
backupname = resolvedNew+b".orig"
if exists(backupname):
warning("can't backup original file to %s - aborting" % backupname)
else:
import shutil
shutil.move(filename, backupname)
if self.write_hunks(backupname, filename, p.hunks):
info("successfully patched %d/%d:\t %s" % (i+1, total, filename))
os.unlink(backupname)
resNewExists = exists(resolvedNew)
if resNewExists == isDevNull:
errors += 1
warning("error: requested file creation but it already exists: " + str(resolvedNew))
continue
if not isDevNull:
shutil.move(resolvedNew, backupname)
else:
backupname = None

if self.write_hunks(backupname, resolvedNew, p.hunks):
info("successfully patched %d/%d:\t %s" % (i+1, total, resolvedNew))
if backupname:
os.unlink(backupname)
else:
errors += 1
warning("error patching file %s" % filename)
shutil.copy(filename, filename+".invalid")
warning("invalid version is saved to %s" % filename+".invalid")
warning("error patching file %s" % new)
shutil.copy(new, new+".invalid")
warning("invalid version is saved to %s" % new+".invalid")
# todo: proper rejects
shutil.move(backupname, filename)
shutil.move(backupname, new)

if root:
os.chdir(prevdir)
Expand Down Expand Up @@ -1097,17 +1134,22 @@ def get_line():


def write_hunks(self, srcname, tgtname, hunks):
src = open(srcname, "rb")
if srcname:
src = open(srcname, "rb")
else:
src = ()
tgt = open(tgtname, "wb")

debug("processing target file %s" % tgtname)

tgt.writelines(self.patch_stream(src, hunks))

tgt.close()
src.close()
if srcname:
src.close()
# [ ] TODO: add test for permission copy
shutil.copymode(srcname, tgtname)
if srcname:
shutil.copymode(srcname, tgtname)
return True


Expand Down
14 changes: 14 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[metadata]
name = patch
version = 1.16
author = anatoly techtonik <[email protected]>
license = MIT
description = Patch utility to apply unified diffs
url = https://github.com/techtonik/python-patch/
classifiers =
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 3

[options]
py_modules = patch
setup_requires = setuptools; setuptools_scm
4 changes: 4 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env python3
from setuptools import setup
if __name__ == "__main__":
setup(use_scm_version = True)