From 1642060f5701c92901721cbe4743eaf6180b3b05 Mon Sep 17 00:00:00 2001 From: Shubham Padkonde Date: Thu, 24 Sep 2026 09:26:22 +0000 Subject: [PATCH] fix: truncate an existing destination in make_targz make_targz opened dest_name with O_WRONLY | O_CREAT but without O_TRUNC, so when the destination already existed and was larger than the new archive, its old trailing bytes were left after the gzip stream. The result had a different digest than a fresh archive and failed to decompress (gzip.BadGzipFile: Not a gzipped file). Signed-off-by: Shubham Padkonde --- CHANGELOG.md | 1 + oras/tests/test_utils.py | 18 ++++++++++++++++++ oras/utils/fileio.py | 3 ++- oras/version.py | 2 +- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b20b09d1..c86f66f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and **Merged pull requests**. Critical items to know are: The versions coincide with releases on pip. Only major versions will be released as tags on Github. ## [0.0.x](https://github.com/oras-project/oras-py/tree/main) (0.0.x) + - truncate an existing destination file in `make_targz`, which otherwise kept stale trailing bytes and produced a corrupt archive (0.2.44) - route push completion output through the logger and support the documented `quiet` option, closes issue [229](https://github.com/oras-project/oras-py/issues/229) (0.2.43) - add Layout `copy` for pull_from_registry capability (0.2.42) - make `get_manifest()` validation optional, fix `Accept` header join, and expand default `Accept` header types to cover all supported response types for the `/v2//manifests/` endpoint (0.2.41) diff --git a/oras/tests/test_utils.py b/oras/tests/test_utils.py index cb5ac685..1c5b46b7 100644 --- a/oras/tests/test_utils.py +++ b/oras/tests/test_utils.py @@ -6,6 +6,7 @@ import os import pathlib import shutil +import tarfile import pytest @@ -146,3 +147,20 @@ def test_make_targz_files_with_same_content_generates_same_hash(tmp_path): hash_tar_2 = utils.get_file_hash(tmp_tar_2) assert hash_tar_1 == hash_tar_2 + + +def test_make_targz_overwrites_existing_destination(tmp_path): + tmp_file = str(tmp_path / "written_file.txt") + utils.write_file(tmp_file, "hello!") + + expected = str(tmp_path / "expected.tar.gz") + utils.make_targz(tmp_file, expected) + + # a larger, unrelated file already exists at the destination + dest = str(tmp_path / "existing.tar.gz") + utils.write_file(dest, "x" * 100000) + utils.make_targz(tmp_file, dest) + + assert utils.get_file_hash(dest) == utils.get_file_hash(expected) + with tarfile.open(dest, "r:gz") as tar: + assert tar.getnames() == ["written_file.txt"] diff --git a/oras/utils/fileio.py b/oras/utils/fileio.py index 294f11e1..ece97c1e 100644 --- a/oras/utils/fileio.py +++ b/oras/utils/fileio.py @@ -41,8 +41,9 @@ def make_targz(source_dir: str, dest_name: Optional[str] = None) -> str: # os.O_WRONLY tells the computer you are only going to writo to the file, not read # os.O_CREATE tells the computer to create the file if it doesn't exist + # os.O_TRUNC discards any previous content of an existing file with os.fdopen( - os.open(dest_name, os.O_WRONLY | os.O_CREAT, 0o644), "wb" + os.open(dest_name, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644), "wb" ) as out_file: with gzip.GzipFile(mode="wb", fileobj=out_file, mtime=0) as gzip_file: with tarfile.open(fileobj=gzip_file, mode="w:") as tar_file: diff --git a/oras/version.py b/oras/version.py index 28241dea..2dda9b59 100644 --- a/oras/version.py +++ b/oras/version.py @@ -2,7 +2,7 @@ __copyright__ = "Copyright The ORAS Authors." __license__ = "Apache-2.0" -__version__ = "0.2.43" +__version__ = "0.2.44" AUTHOR = "Vanessa Sochat" EMAIL = "vsoch@users.noreply.github.com" NAME = "oras"