On 2026-10-25 09:40, Johannes Schauer Marin Rodrigues wrote:
Yes, that's what libarchive does:

OK, let's do that in GNU Tar too. That's simpler than fooling with 
SOURCE_DATE_EPOCH. I installed the attached on Savannah master; please give it 
a try.
From 35cbd6e0255c27c7c5371ec9f7ba80959cb46f5c Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email protected]>
Date: Sun, 25 Oct 2026 18:22:30 -0700
Subject: [PATCH] tar: make GNUSparseFile headers reproducible
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Follow libarchive’s lead in using names of the form
"%d/GNUSparseFile.0/%f", not "%d/GNUSparseFile.%p/%f", when
building archives.  This makes the output reproducible.
Problem reported by Johannes Schauer Marin Rodrigues in:
https://lists.gnu.org/r/bug-tar/2026-10/msg00009.html
* src/sparse.c (pax_dump_header_0, pax_dump_header_1):
Use "0", not "%p", in the format of a GNUSparseFile header.
---
 NEWS            |  6 +++++-
 doc/sparse.texi |  4 ++--
 doc/tar.texi    | 27 ++++++++++++---------------
 src/sparse.c    |  9 +++++----
 4 files changed, 24 insertions(+), 22 deletions(-)

diff --git a/NEWS b/NEWS
index 0bfbbe4d..9f2610eb 100644
--- a/NEWS
+++ b/NEWS
@@ -1,4 +1,4 @@
-GNU tar NEWS - User visible changes. 2026-10-09
+GNU tar NEWS - User visible changes. 2026-10-25
 Please send GNU tar bug reports to <[email protected]>
 
 version 1.35.90 (git)
@@ -44,6 +44,10 @@ option.
    Formerly, the behavior was undocumented and differed from one tar
    version to the next.
 
+** pax extended headers for sparse files are now more reproducible.
+   Where the headers formerly had tar's process ID, now they have "0".
+   The process ID was not useful enough to justify irreproducibility.
+
 ** The *.tlz extension in a tarball now implies lzip, not lzma.
    This matches the change made to pkgtools in Slackware 15.0 (2022),
    after lzma was superseded by xz in 2009.
diff --git a/doc/sparse.texi b/doc/sparse.texi
index 51a691c5..11d65189 100644
--- a/doc/sparse.texi
+++ b/doc/sparse.texi
@@ -167,7 +167,7 @@ To address the 2nd problem, the @code{name} field in @code{ustar}
 is replaced with a special name, constructed using the following pattern:
 
 @smallexample
-%d/GNUSparseFile.%p/%f
+%d/GNUSparseFile.0/%f
 @end smallexample
 
 @vrindex GNU.sparse.name, extended header variable
@@ -208,7 +208,7 @@ The @code{name} field in @code{ustar} header contains a special name,
 constructed using the following pattern:
 
 @smallexample
-%d/GNUSparseFile.%p/%f
+%d/GNUSparseFile.0/%f
 @end smallexample
 
 @vrindex GNU.sparse.name, extended header variable, in v.1.0
diff --git a/doc/tar.texi b/doc/tar.texi
index 9b668bb3..d8e40cbb 100644
--- a/doc/tar.texi
+++ b/doc/tar.texi
@@ -11115,10 +11115,7 @@ version 1.0}@footnote{@xref{PAX 1}.}, which are the easiest to expand.
 The condensed file will contain both file map and file data, so no
 additional data will be needed to restore it.  If the original file
 name was @file{@var{dir}/@var{name}}, then the condensed file will be
-named @file{@var{dir}/@/GNUSparseFile.@var{n}/@/@var{name}}, where
-@var{n} is a decimal number@footnote{Technically speaking, @var{n} is a
-@dfn{process @acronym{ID}} of the @command{tar} process which created the
-archive (@pxref{PAX keywords}).}.
+named @file{@var{dir}/@/GNUSparseFile.0/@/@var{name}}.
 
 To expand a version 1.0 file, run @command{xsparse} as follows:
 
@@ -11155,9 +11152,9 @@ run mode is enabled by @option{-n} command line argument:
 
 @smallexample
 @group
-$ @kbd{xsparse -n /home/gray/GNUSparseFile.6058/sparsefile}
+$ @kbd{xsparse -n /home/gray/GNUSparseFile.0/sparsefile}
 Reading v.1.0 sparse map
-Expanding file '/home/gray/GNUSparseFile.6058/sparsefile' to
+Expanding file '/home/gray/GNUSparseFile.0/sparsefile' to
 '/home/gray/sparsefile'
 Finished dry run
 @end group
@@ -11166,7 +11163,7 @@ Finished dry run
 To actually expand the file, you would run:
 
 @smallexample
-$ @kbd{xsparse /home/gray/GNUSparseFile.6058/sparsefile}
+$ @kbd{xsparse /home/gray/GNUSparseFile.0/sparsefile}
 @end smallexample
 
 @noindent
@@ -11177,9 +11174,9 @@ similar to that from the dry run mode, use @option{-v} option:
 
 @smallexample
 @group
-$ @kbd{xsparse -v /home/gray/GNUSparseFile.6058/sparsefile}
+$ @kbd{xsparse -v /home/gray/GNUSparseFile.0/sparsefile}
 Reading v.1.0 sparse map
-Expanding file '/home/gray/GNUSparseFile.6058/sparsefile' to
+Expanding file '/home/gray/GNUSparseFile.0/sparsefile' to
 '/home/gray/sparsefile'
 Done
 @end group
@@ -11194,14 +11191,14 @@ use.  Continuing our example:
 @smallexample
 @group
 $ @kbd{xsparse -v -x /home/gray/PaxHeaders/sparsefile \
-  /home/gray/GNUSparseFile/sparsefile}
+  /home/gray/GNUSparseFile.0/sparsefile}
 Reading extended header file
 Found variable GNU.sparse.major = 1
 Found variable GNU.sparse.minor = 0
 Found variable GNU.sparse.name = sparsefile
 Found variable GNU.sparse.realsize = 217481216
 Reading v.1.0 sparse map
-Expanding file '/home/gray/GNUSparseFile.6058/sparsefile' to
+Expanding file '/home/gray/GNUSparseFile.0/sparsefile' to
 '/home/gray/sparsefile'
 Done
 @end group
@@ -11251,8 +11248,8 @@ star: Unknown extended header keyword 'GNU.sparse.size' ignored.
 star: Unknown extended header keyword 'GNU.sparse.numblocks' ignored.
 star: Unknown extended header keyword 'GNU.sparse.name' ignored.
 star: Unknown extended header keyword 'GNU.sparse.map' ignored.
-block        56:  425984 -rw-r--r--  gray/users Jun 25 14:46 2006 GNUSparseFile.28124/sparsefile
-block       897:   65391 -rw-r--r--  gray/users Jun 24 20:06 2006 README
+block        56:  425984 -rw-r--r--  gray/users Oct 25 14:46 2026 GNUSparseFile.0/sparsefile
+block       897:   65391 -rw-r--r--  gray/users Oct 24 20:06 2026 README
 @dots{}
 @end group
 @end smallexample
@@ -11297,13 +11294,13 @@ Finally, you can expand the condensed file, using the obtained header:
 
 @smallexample
 @group
-$ @kbd{xsparse -v -x xhdr GNUSparseFile.6058/sparsefile}
+$ @kbd{xsparse -v -x xhdr GNUSparseFile.0/sparsefile}
 Reading extended header file
 Found variable GNU.sparse.size = 217481216
 Found variable GNU.sparse.numblocks = 208
 Found variable GNU.sparse.name = sparsefile
 Found variable GNU.sparse.map = 0,2048,1050624,2048,@dots{}
-Expanding file 'GNUSparseFile.28124/sparsefile' to 'sparsefile'
+Expanding file 'GNUSparseFile.0/sparsefile' to 'sparsefile'
 Done
 @end group
 @end smallexample
diff --git a/src/sparse.c b/src/sparse.c
index 050d91da..16abe9f3 100644
--- a/src/sparse.c
+++ b/src/sparse.c
@@ -1014,7 +1014,8 @@ static struct tar_sparse_optab const star_optab = {
                         GNU.sparse.size variable)
 
    The name field of the ustar header is constructed using the pattern
-   "%d/GNUSparseFile.%p/%f".
+   "%d/GNUSparseFile.0/%f".  Although the pattern formerly used "%p"
+   instead of "0", the "%p" hurt reproducibility.
 
    The sparse map itself is stored in the file data block, preceding the actual
    file data. It consists of a series of octal numbers of arbitrary length,
@@ -1082,8 +1083,8 @@ pax_dump_header_0 (struct tar_sparse_file *file)
     {
       xheader_store ("GNU.sparse.name", file->stat_info, NULL);
       save_file_name = file->stat_info->file_name;
-      file->stat_info->file_name = xheader_format_name (file->stat_info,
-					       "%d/GNUSparseFile.%p/%f", 0);
+      file->stat_info->file_name
+	= xheader_format_name (file->stat_info, "%d/GNUSparseFile.0/%f", 0);
 
       xheader_string_begin (&file->stat_info->xhdr);
       for (idx_t i = 0; i < file->stat_info->sparse_map_avail; i++)
@@ -1182,7 +1183,7 @@ pax_dump_header_1 (struct tar_sparse_file *file)
   xheader_store ("GNU.sparse.realsize", file->stat_info, NULL);
 
   file->stat_info->file_name =
-    xheader_format_name (file->stat_info, "%d/GNUSparseFile.%p/%f", 0);
+    xheader_format_name (file->stat_info, "%d/GNUSparseFile.0/%f", 0);
   /* Make sure the created header name is shorter than NAME_FIELD_SIZE: */
   if (strlen (file->stat_info->file_name) > NAME_FIELD_SIZE)
     file->stat_info->file_name[NAME_FIELD_SIZE] = 0;
-- 
2.56.0

Reply via email to