diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4128ae5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +end_of_line = lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7972a2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +__pycache__ +*.pyc +*.pyo +./*.egg-info +./build +./dist +./.eggs diff --git a/patch.py b/patch.py index 4b82af0..d9c7766 100755 --- a/patch.py +++ b/patch.py @@ -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 """ @@ -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") @@ -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 @@ -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): @@ -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 @@ -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")) @@ -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): @@ -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) @@ -1097,7 +1134,10 @@ 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) @@ -1105,9 +1145,11 @@ def write_hunks(self, srcname, tgtname, hunks): 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 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..530bed1 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,14 @@ +[metadata] +name = patch +version = 1.16 +author = anatoly techtonik +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 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d808dd6 --- /dev/null +++ b/setup.py @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 +from setuptools import setup +if __name__ == "__main__": + setup(use_scm_version = True)