From d1d81db97a4ed8f02b063ba1ac18c941b2c27d15 Mon Sep 17 00:00:00 2001 From: Shahadat Islam Koyel <179203101+ShahadatTest@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:49:13 +0600 Subject: [PATCH] Clean temporary output when fdopen fails before in-place fallback --- jsonpatch_cli.py | 12 ++++++++++++ tests.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/jsonpatch_cli.py b/jsonpatch_cli.py index e84d0f7..261aec1 100644 --- a/jsonpatch_cli.py +++ b/jsonpatch_cli.py @@ -78,6 +78,7 @@ def patch_files(): args.ORIGINAL.close() dirname = os.path.abspath(os.path.dirname(args.ORIGINAL.name)) + fd = None try: # Attempt to replace the file atomically. We do this by # creating a temporary file in the same directory as the @@ -90,6 +91,17 @@ def patch_files(): atomic = True except OSError: + # mkstemp may have succeeded before fdopen failed. Neither the + # descriptor nor the temporary file belongs to the fallback. + if fd is not None: + try: + os.close(fd) + except OSError: + pass + try: + os.unlink(pathname) + except OSError: + pass # We failed to create the temporary file for an atomic # replace, so fall back to non-atomic mode by backing up # the original (if desired) and writing a new file. diff --git a/tests.py b/tests.py index 1719774..3d39006 100755 --- a/tests.py +++ b/tests.py @@ -2114,6 +2114,45 @@ def dump(value, stream, **kwargs): self.assertEqual(self.read(doc), {'a': 1}) self.assertEqual(sorted(os.listdir(self.dir)), ['doc.json', 'patch.json']) + def test_jsonpatch_in_place_cleans_temp_on_fdopen_error(self): + for backup in (False, True): + with self.subTest(backup=backup): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + self.dir = tmp.name + doc = self.write('doc.json', {'a': 1}) + patch = self.write('patch.json', [{'op': 'replace', 'path': '/a', 'value': 2}]) + parser = jsonpatch_cli.jsonpatch_parser + args = parser.parse_args(['-i', doc, patch] + (['-b'] if backup else [])) + self.addCleanup(args.ORIGINAL.close) + self.addCleanup(args.PATCH.close) + descriptors = [] + + def close_if_open(fd): + try: + os.close(fd) + except OSError: + pass + + def fail_fdopen(fd, *args, **kwargs): + descriptors.append(fd) + self.addCleanup(close_if_open, fd) + raise OSError('cannot wrap temporary descriptor') + + with mock.patch.object(parser, 'parse_args', return_value=args), \ + mock.patch('os.fdopen', side_effect=fail_fdopen): + jsonpatch_cli.patch_files() + + self.assertEqual(self.read(doc), {'a': 2}) + if backup: + self.assertEqual(self.read(doc + '.orig'), {'a': 1}) + with self.assertRaises(OSError): + os.fstat(descriptors[0]) + expected = ['doc.json', 'patch.json'] + if backup: + expected.append('doc.json.orig') + self.assertEqual(sorted(os.listdir(self.dir)), sorted(expected)) + def test_jsonpatch_in_place_preserves_original_on_replace_error(self): doc = self.write('doc.json', {'a': 1}) patch = self.write('patch.json', [{'op': 'replace', 'path': '/a', 'value': 2}])