From 8ce97a5730eb7732a332f419a2139071405b997d Mon Sep 17 00:00:00 2001
From: Seth Michael Larson <seth@python.org>
Date: Wed, 11 Mar 2026 08:47:55 -0500
Subject: [PATCH] gh-141707: Skip TarInfo DIRTYPE normalization during GNU long
 name handling (cherry picked from commit
 42d754e34c06e57ad6b8e7f92f32af679912d8ab)

Co-authored-by: Seth Michael Larson <seth@python.org>
Co-authored-by: Eashwar Ranganathan <eashwar@eashwar.com>
---
 Lib/tarfile.py                                                          |   29 ++++++++--
 Lib/test/test_tarfile.py                                                |   21 +++++++
 Misc/ACKS                                                               |    1 
 Misc/NEWS.d/next/Library/2025-11-18-06-35-53.gh-issue-141707.DBmQIy.rst |    2 
 4 files changed, 49 insertions(+), 4 deletions(-)
 create mode 100644 Misc/NEWS.d/next/Library/2025-11-18-06-35-53.gh-issue-141707.DBmQIy.rst

Index: Python-3.6.15/Lib/tarfile.py
===================================================================
--- Python-3.6.15.orig/Lib/tarfile.py	2026-03-26 20:58:51.203690688 +0100
+++ Python-3.6.15/Lib/tarfile.py	2026-03-26 20:58:51.438680931 +0100
@@ -1213,6 +1213,20 @@
     @classmethod
     def frombuf(cls, buf, encoding, errors):
         """Construct a TarInfo object from a 512 byte bytes object.
+
+        To support the old v7 tar format AREGTYPE headers are
+        transformed to DIRTYPE headers if their name ends in '/'.
+        """
+        return cls._frombuf(buf, encoding, errors)
+
+    @classmethod
+    def _frombuf(cls, buf, encoding, errors, *, dircheck=True):
+        """Construct a TarInfo object from a 512 byte bytes object.
+
+        If ``dircheck`` is set to ``True`` then ``AREGTYPE`` headers will
+        be normalized to ``DIRTYPE`` if the name ends in a trailing slash.
+        ``dircheck`` must be set to ``False`` if this function is called
+        on a follow-up header such as ``GNUTYPE_LONGNAME``.
         """
         if len(buf) == 0:
             raise EmptyHeaderError("empty header")
@@ -1243,7 +1257,7 @@
 
         # Old V7 tar format represents a directory as a regular
         # file with a trailing slash.
-        if obj.type == AREGTYPE and obj.name.endswith("/"):
+        if dircheck and obj.type == AREGTYPE and obj.name.endswith("/"):
             obj.type = DIRTYPE
 
         # The old GNU sparse format occupies some of the unused
@@ -1278,8 +1292,15 @@
         """Return the next TarInfo object from TarFile object
            tarfile.
         """
+        return cls._fromtarfile(tarfile)
+
+    @classmethod
+    def _fromtarfile(cls, tarfile, *, dircheck=True):
+        """
+        See dircheck documentation in _frombuf().
+        """
         buf = tarfile.fileobj.read(BLOCKSIZE)
-        obj = cls.frombuf(buf, tarfile.encoding, tarfile.errors)
+        obj = cls._frombuf(buf, tarfile.encoding, tarfile.errors, dircheck=dircheck)
         obj.offset = tarfile.fileobj.tell() - BLOCKSIZE
         return obj._proc_member(tarfile)
 
@@ -1332,7 +1353,7 @@
 
         # Fetch the next header and process it.
         try:
-            next = self.fromtarfile(tarfile)
+            next = self._fromtarfile(tarfile, dircheck=False)
         except HeaderError:
             raise SubsequentHeaderError("missing or bad subsequent header")
 
@@ -1463,7 +1484,7 @@
 
         # Fetch the next header.
         try:
-            next = self.fromtarfile(tarfile)
+            next = self._fromtarfile(tarfile, dircheck=False)
         except HeaderError:
             raise SubsequentHeaderError("missing or bad subsequent header")
 
Index: Python-3.6.15/Lib/test/test_tarfile.py
===================================================================
--- Python-3.6.15.orig/Lib/test/test_tarfile.py	2026-03-26 20:58:51.204212205 +0100
+++ Python-3.6.15/Lib/test/test_tarfile.py	2026-03-26 20:58:51.439644758 +0100
@@ -948,10 +948,30 @@
             self.assertEqual(tarinfo.type, self.longnametype)
 
 
+    def test_longname_file_not_directory(self):
+        # Test reading a longname file and ensure it is not handled as a directory
+        # Issue #141707
+        buf = io.BytesIO()
+        with tarfile.open(mode='w', fileobj=buf, format=self.format) as tar:
+            ti = tarfile.TarInfo()
+            ti.type = tarfile.AREGTYPE
+            ti.name = ('a' * 99) + '/' + ('b' * 3)
+            tar.addfile(ti)
+
+            expected = {t.name: t.type for t in tar.getmembers()}
+
+        buf.seek(0)
+        with tarfile.open(mode='r', fileobj=buf) as tar:
+            actual = {t.name: t.type for t in tar.getmembers()}
+
+        self.assertEqual(expected, actual)
+
+
 class GNUReadTest(LongnameTest, ReadTest, unittest.TestCase):
 
     subdir = "gnu"
     longnametype = tarfile.GNUTYPE_LONGNAME
+    format = tarfile.GNU_FORMAT
 
     # Since 3.2 tarfile is supposed to accurately restore sparse members and
     # produce files with holes. This is what we actually want to test here.
@@ -1006,6 +1026,7 @@
 
     subdir = "pax"
     longnametype = tarfile.XHDTYPE
+    format = tarfile.PAX_FORMAT
 
     def test_pax_global_headers(self):
         tar = tarfile.open(tarname, encoding="iso8859-1")
Index: Python-3.6.15/Misc/ACKS
===================================================================
--- Python-3.6.15.orig/Misc/ACKS	2026-03-26 20:58:50.446887757 +0100
+++ Python-3.6.15/Misc/ACKS	2026-03-26 20:58:51.440359426 +0100
@@ -1265,6 +1265,7 @@
 Shorya Raj
 Jeff Ramnani
 Bayard Randel
+Eashwar Ranganathan
 Varpu Rantala
 Brodie Rao
 Rémi Rampin
Index: Python-3.6.15/Misc/NEWS.d/next/Library/2025-11-18-06-35-53.gh-issue-141707.DBmQIy.rst
===================================================================
--- /dev/null	1970-01-01 00:00:00.000000000 +0000
+++ Python-3.6.15/Misc/NEWS.d/next/Library/2025-11-18-06-35-53.gh-issue-141707.DBmQIy.rst	2026-03-26 20:58:51.440688506 +0100
@@ -0,0 +1,2 @@
+Don't change :class:`tarfile.TarInfo` type from ``AREGTYPE`` to ``DIRTYPE`` when parsing
+GNU long name or link headers.
