Commit 8d63e87a496 for php

commit 8d63e87a496b8430a36f10b12e294639f4b3c590
Author: Raphael Paez <paezraphael@gmail.com>
Date:   Thu Oct 29 23:25:10 2026 +0000

    Fix GH-8469: Phar::decompressFiles() keeps compressed contents (#25045)

    Phar::decompressFiles() and Phar::compressFiles() only changed the
    compression flags of the entries. phar_flush() then read the entry
    contents from the archive as they were stored, so the old compressed
    bytes were written out as the new contents.

    Decompress entries whose compression changes before updating the flags,
    so phar_flush() writes the real contents with the new compression.
    Skip directory entries, which have no contents to decompress.

    Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

    Closes #25045

diff --git a/NEWS b/NEWS
index 57d6e4694d0..798f849854c 100644
--- a/NEWS
+++ b/NEWS
@@ -207,6 +207,8 @@ PHP                                                                        NEWS
     out-of-range column index). (Ilia Alshanetsky)

 - Phar:
+  . Fixed bug GH-8469 (Phar::decompressFiles() and Phar::compressFiles() retain
+    compressed contents after changing compression). (Raphael Paez)
   . Fixed bug GH-24740 (Oversized tar metadata exhausts the memory limit).
     (Lazizbek Ergashev)

diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c
index a1736f221e2..79ea1cf2df9 100644
--- a/ext/phar/phar_object.c
+++ b/ext/phar/phar_object.c
@@ -3173,6 +3173,30 @@ static int phar_set_compression(zval *zv, void *argument) /* {{{ */
 }
 /* }}} */

+typedef struct {
+	uint32_t compress;
+	char *error;
+} phar_compression_args;
+
+static int phar_decompress_for_compression_change(zval *zv, void *argument) /* {{{ */
+{
+	phar_entry_info *entry = (phar_entry_info *)Z_PTR_P(zv);
+	phar_compression_args *args = (phar_compression_args *)argument;
+	uint32_t current = entry->flags & PHAR_ENT_COMPRESSION_MASK;
+
+	if (entry->is_deleted || entry->is_dir || !current || current == args->compress) {
+		return ZEND_HASH_APPLY_KEEP;
+	}
+
+	/* phar_flush() would otherwise copy the old compressed bytes as the new contents */
+	if (FAILURE == phar_open_entry_fp(entry, &args->error, 0)) {
+		return ZEND_HASH_APPLY_STOP;
+	}
+
+	return ZEND_HASH_APPLY_KEEP;
+}
+/* }}} */
+
 static int phar_test_compression(zval *zv, void *argument) /* {{{ */
 {
 	phar_entry_info *entry = (phar_entry_info *)Z_PTR_P(zv);
@@ -3197,9 +3221,18 @@ static int phar_test_compression(zval *zv, void *argument) /* {{{ */
 }
 /* }}} */

-static void pharobj_set_compression(HashTable *manifest, uint32_t compress) /* {{{ */
+static zend_result pharobj_set_compression(HashTable *manifest, uint32_t compress, char **error) /* {{{ */
 {
+	phar_compression_args args = { .compress = compress, .error = NULL };
+
+	zend_hash_apply_with_argument(manifest, phar_decompress_for_compression_change, &args);
+	if (args.error) {
+		*error = args.error;
+		return FAILURE;
+	}
+
 	zend_hash_apply_with_argument(manifest, phar_set_compression, &compress);
+	return SUCCESS;
 }
 /* }}} */

@@ -3390,7 +3423,11 @@ PHP_METHOD(Phar, compressFiles)
 		zend_throw_exception_ex(phar_ce_PharException, 0, "phar \"%s\" is persistent, unable to copy on write", phar_obj->archive->fname);
 		RETURN_THROWS();
 	}
-	pharobj_set_compression(&phar_obj->archive->manifest, flags);
+	if (FAILURE == pharobj_set_compression(&phar_obj->archive->manifest, flags, &error)) {
+		zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "%s", error);
+		efree(error);
+		RETURN_THROWS();
+	}
 	phar_obj->archive->is_modified = 1;
 	phar_flush(phar_obj->archive, &error);

@@ -3432,7 +3469,11 @@ PHP_METHOD(Phar, decompressFiles)
 			zend_throw_exception_ex(phar_ce_PharException, 0, "phar \"%s\" is persistent, unable to copy on write", phar_obj->archive->fname);
 			RETURN_THROWS();
 		}
-		pharobj_set_compression(&phar_obj->archive->manifest, PHAR_ENT_COMPRESSED_NONE);
+		if (FAILURE == pharobj_set_compression(&phar_obj->archive->manifest, PHAR_ENT_COMPRESSED_NONE, &error)) {
+			zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "%s", error);
+			efree(error);
+			RETURN_THROWS();
+		}
 	}

 	phar_obj->archive->is_modified = 1;
diff --git a/ext/phar/tests/gh8469.phpt b/ext/phar/tests/gh8469.phpt
new file mode 100644
index 00000000000..ca34dca256c
--- /dev/null
+++ b/ext/phar/tests/gh8469.phpt
@@ -0,0 +1,59 @@
+--TEST--
+GH-8469 (Phar::decompressFiles() and Phar::compressFiles() keep the old compressed contents)
+--EXTENSIONS--
+phar
+zlib
+bz2
+--INI--
+phar.require_hash=0
+phar.readonly=0
+--FILE--
+<?php
+$contents = str_repeat('four score and seven years ago ', 10);
+
+foreach (['phar', 'phar.zip'] as $ext) {
+    $fname = __DIR__ . "/gh8469.$ext";
+    $phar = new Phar($fname);
+    $phar['a.txt'] = $contents;
+    $phar['b.txt'] = 'our fathers';
+    $phar->addEmptyDir('dir');
+
+    $phar->compressFiles(Phar::GZ);
+    $phar->compressFiles(Phar::BZ2);
+    var_dump($phar['a.txt']->isCompressed(Phar::BZ2));
+    var_dump($phar['a.txt']->getContent() === $contents);
+
+    var_dump($phar->decompressFiles());
+    var_dump($phar['a.txt']->isCompressed());
+    var_dump($phar['a.txt']->getContent() === $contents);
+    var_dump($phar['b.txt']->getContent());
+    unset($phar);
+
+    $phar = new Phar($fname);
+    var_dump($phar['a.txt']->getContent() === $contents);
+    var_dump(file_get_contents("phar://$fname/b.txt"));
+    unset($phar);
+}
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/gh8469.phar');
+@unlink(__DIR__ . '/gh8469.phar.zip');
+?>
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+bool(false)
+bool(true)
+string(11) "our fathers"
+bool(true)
+string(11) "our fathers"
+bool(true)
+bool(true)
+bool(true)
+bool(false)
+bool(true)
+string(11) "our fathers"
+bool(true)
+string(11) "our fathers"