From 23e93ca6674f0d9da6834bbc2eba6cc3cd9ba6ff Mon Sep 17 00:00:00 2001
From: Matteo Croce <teknoraver@meta.com>
Date: Mon, 30 Sep 2024 01:43:11 +0200
Subject: [PATCH 1/4] reflink: align data on block size on archive create

Add a --reflink option which, on archive creation, aligns file data
on a 4k block boundary.  This is done by padding the extended header
with a standard "comment" pax record, which is ignored on extraction.
For this reason the option implies --format=posix.

This will be used when extracting files using reflinks, as one of the
requirements is that the data is aligned on a 4k boundary.

The alignment is skipped, with a warning, in the cases where it
cannot work: a non-posix format selected explicitly, compressed,
multi-volume or remote archives, appending to an existing archive,
sparse members (which cannot be cloned on extraction), and
--pax-option keyword overrides, which would invalidate the computed
padding.

* src/tar.c (options, parse_opt): New option --reflink.
(decode_options): Warn about combinations that defeat the
alignment: an explicit non-posix format, compressed, multi-volume
or remote archives, appending to an existing archive.
* src/common.h (reflink_option): New global.
(REFLINK_BLOCK_SIZE): New constant.
(round_up): New inline function.
(struct xheader_comment): New type.
(xheader_comment_overridden_p): New prototype.
* src/create.c (start_header): Pad the extended header with a
"comment" record sized so that the member data starts on a
REFLINK_BLOCK_SIZE boundary.
* src/xheader.c (comment_coder): New coder.
(xheader_comment_overridden_p): New function.
(xhdr_tab): Use comment_coder for the "comment" keyword.
---
 src/common.h  | 26 ++++++++++++++++++++++++++
 src/create.c  | 47 +++++++++++++++++++++++++++++++++++++++++++++++
 src/tar.c     | 34 ++++++++++++++++++++++++++++++++++
 src/xheader.c | 23 ++++++++++++++++++++++-
 4 files changed, 129 insertions(+), 1 deletion(-)

diff --git a/src/common.h b/src/common.h
index e835d888..9326a360 100644
--- a/src/common.h
+++ b/src/common.h
@@ -276,6 +276,9 @@ extern int acls_option;
 /* If true, save the user and root xattrs.  */
 extern bool xattrs_option;
 
+/* If true, use reflinks when possible.  */
+extern bool reflink_option;
+
 /* When set, strip the given number of file name components from the file name
    before extracting */
 extern idx_t strip_name_components;
@@ -395,6 +398,20 @@ extern bool show_transformed_names_option;
    timestamps from archives with an unusual member order. It is automatically
    set for incremental archives. */
 extern bool delay_directory_restore_option;
+
+/* The block size that --reflink aligns member data to; FICLONERANGE
+   requires source and destination ranges aligned to the filesystem
+   block size, for which this is a safe upper bound.  */
+enum { REFLINK_BLOCK_SIZE = 4096 };
+
+/* Rounds up n to the next multiple of m. Only works if m is a power of two. */
+COMMON_INLINE off_t
+round_up (off_t n, off_t m)
+{
+  m--;
+  return (n + m) & ~m;
+}
+
 
 /* Declarations for each module.  */
 
@@ -927,6 +944,14 @@ void update_archive (void);
 
 /* Module xheader.c.  */
 
+/* Value for xheader_store of a "comment" keyword; used by --reflink
+   to pad the extended header to a REFLINK_BLOCK_SIZE boundary.  */
+struct xheader_comment
+{
+  char const *comment;
+  idx_t length;
+};
+
 void xheader_decode (struct tar_stat_info *stat);
 void xheader_decode_global (struct xheader *xhdr);
 void xheader_store (char const *keyword, struct tar_stat_info *st,
@@ -944,6 +969,7 @@ void xheader_string_begin (struct xheader *xhdr);
 void xheader_string_add (struct xheader *xhdr, char const *s);
 bool xheader_string_end (struct xheader *xhdr, char const *keyword);
 bool xheader_keyword_deleted_p (const char *kw);
+bool xheader_comment_overridden_p (void);
 char *xheader_format_name (struct tar_stat_info *st, const char *fmt,
 			   intmax_t n);
 void xheader_xattr_init (struct tar_stat_info *st);
diff --git a/src/create.c b/src/create.c
index bf9f9838..fcdca371 100644
--- a/src/create.c
+++ b/src/create.c
@@ -944,6 +944,53 @@ start_header (struct tar_stat_info *st)
       if (xattrs_option)
 	for (idx_t i = 0; i < st->xattr_map.xm_size; i++)
 	  xheader_store (st->xattr_map.xm_map[i].xkey, st, &i);
+      if (reflink_option && S_ISREG (st->stat.st_mode) && st->stat.st_size
+	  /* Sparse members add GNU.sparse.* records after this point,
+	     which would invalidate the alignment; they cannot be
+	     reflinked on extraction anyway.  */
+	  && ! (sparse_option && ST_IS_SPARSE (st->stat)))
+	{
+	  if (xheader_comment_overridden_p ())
+	    {
+	      static bool warned;
+	      if (!warned)
+		{
+		  paxwarn (0, _("--pax-option keyword overrides prevent"
+				" --reflink data alignment"));
+		  warned = true;
+		}
+	    }
+	  else
+	    {
+	      /* On disk the member is laid out as the pax extended header
+		 block, the pax data (rounded up to BLOCKSIZE), the ustar
+		 header block, then the file data.  Choose the comment
+		 payload length so that the file data lands on a
+		 REFLINK_BLOCK_SIZE boundary.  COMMENT_OVERHEAD is the
+		 record's own framing (up to 4 length digits + " comment="
+		 + "\n"); an over-estimate is harmless because the
+		 BLOCKSIZE rounding of the pax data absorbs the few unused
+		 bytes.  */
+	      enum { COMMENT_OVERHEAD = 14 };
+	      off_t pos = (records_written * record_size
+			   + (current_block->buffer - record_start->buffer));
+	      off_t before = (pos + 2 * BLOCKSIZE + st->xhdr.size
+			      + COMMENT_OVERHEAD);
+	      off_t n = round_up (before, REFLINK_BLOCK_SIZE) - before;
+	      static char comment_buf[REFLINK_BLOCK_SIZE];
+
+	      if (comment_buf[0] == 0)
+		memset (comment_buf, ' ', sizeof (comment_buf));
+
+	      struct xheader_comment comment =
+		{
+		  .comment = comment_buf,
+		  .length = n,
+		};
+
+	      xheader_store ("comment", st, &comment);
+	    }
+	}
     }
 
   return header;
diff --git a/src/tar.c b/src/tar.c
index a920d9c2..4b104158 100644
--- a/src/tar.c
+++ b/src/tar.c
@@ -89,6 +89,7 @@ bool xattrs_option;
 idx_t strip_name_components;
 bool show_omitted_dirs_option;
 bool sparse_option;
+bool reflink_option;
 intmax_t tar_sparse_major;
 intmax_t tar_sparse_minor;
 enum hole_detection_method hole_detection;
@@ -406,6 +407,7 @@ enum
   QUOTING_STYLE_OPTION,
   RECORD_SIZE_OPTION,
   RECURSIVE_UNLINK_OPTION,
+  REFLINK_OPTION,
   REMOVE_FILES_OPTION,
   RESTRICT_OPTION,
   RMT_COMMAND_OPTION,
@@ -587,6 +589,9 @@ static struct argp_option options[] = {
   {"check-device", CHECK_DEVICE_OPTION, NULL, 0,
    N_("check device numbers when creating incremental archives (default)"),
    GRID_MODIFIER },
+  {"reflink", REFLINK_OPTION, NULL, 0,
+   N_("align file data when creating an archive, clone file data when"
+      " extracting"), GRID_MODIFIER },
 
   {NULL, 0, NULL, 0,
    N_("Overwrite control:"), GRH_OVERWRITE },
@@ -1763,6 +1768,11 @@ parse_opt (int key, char *arg, struct argp_state *state)
       sparse_option = true;
       break;
 
+    case REFLINK_OPTION:
+      set_archive_format ("posix");
+      reflink_option = true;
+      break;
+
     case SKIP_OLD_FILES_OPTION:
       set_old_files_option (SKIP_OLD_FILES, args->loc);
       break;
@@ -2502,6 +2512,30 @@ decode_options (int argc, char **argv)
 		   | format_mask (GNU_FORMAT)
 		   | format_mask (POSIX_FORMAT));
 
+  /* Data alignment for --reflink needs the pax comment record and a
+     final archive that is a plain uncompressed local file; warn about
+     combinations that silently defeat it.  */
+  if (reflink_option)
+    {
+      if (subcommand_option == CREATE_SUBCOMMAND)
+	{
+	  if (archive_format != POSIX_FORMAT)
+	    paxwarn (0, _("--reflink data alignment requires the posix"
+			  " format"));
+	  if (multi_volume_option)
+	    paxwarn (0, _("--reflink is ineffective with multi-volume"
+			  " archives"));
+	  if (use_compress_program_option)
+	    paxwarn (0, _("--reflink is ineffective with compressed"
+			  " archives"));
+	}
+      else if (subcommand_option == APPEND_SUBCOMMAND
+	       || subcommand_option == UPDATE_SUBCOMMAND
+	       || subcommand_option == CAT_SUBCOMMAND)
+	paxwarn (0, _("--reflink is ineffective when appending to an"
+		      " archive"));
+    }
+
   if (occurrence_option)
     {
       if (!name_more_files ())
diff --git a/src/xheader.c b/src/xheader.c
index 05f905ed..8df4098b 100644
--- a/src/xheader.c
+++ b/src/xheader.c
@@ -129,6 +129,17 @@ xheader_keyword_override_p (const char *keyword)
   return false;
 }
 
+/* Return true if pax keyword overrides prevent sizing the "comment"
+   record used by --reflink to align member data: 'keyword:=value'
+   overrides are appended after the comment record is sized, and a
+   deleted "comment" keyword drops the record altogether.  */
+bool
+xheader_comment_overridden_p (void)
+{
+  return keyword_override_list != NULL
+	 || xheader_keyword_deleted_p ("comment");
+}
+
 static void
 xheader_list_append (struct keyword_list **root, char const *kw,
 		     char const *value)
@@ -1662,9 +1673,19 @@ sparse_minor_decoder (struct tar_stat_info *st,
     st->sparse_minor = u;
 }
 
+static void
+comment_coder (struct tar_stat_info const *UNNAMED (st),
+	       char const *keyword,
+	       struct xheader *xhdr,
+	       void const *data)
+{
+  struct xheader_comment const *comment = data;
+  xheader_print_n (xhdr, keyword, comment->comment, comment->length);
+}
+
 struct xhdr_tab const xhdr_tab[] = {
   { "atime",    atime_coder,    atime_decoder,    0, false },
-  { "comment",  dummy_coder,    dummy_decoder,    0, false },
+  { "comment",  comment_coder,  dummy_decoder,    0, false },
   { "charset",  dummy_coder,    dummy_decoder,    0, false },
   { "ctime",    ctime_coder,    ctime_decoder,    0, false },
   { "gid",      gid_coder,      gid_decoder,      0, false },
-- 
2.50.1

