]> git.ipfire.org Git - thirdparty/libarchive.git/commitdiff
bsdtar: Support `--mtime` and `--clamp-mtime` (#2601)
authorZhaofeng Li <hello@zhaofeng.li>
Thu, 15 May 2025 12:08:14 +0000 (06:08 -0600)
committerGitHub <noreply@github.com>
Thu, 15 May 2025 12:08:14 +0000 (14:08 +0200)
Hi,

This PR adds support for setting a forced mtime on all written files
(`--mtime` and `--clamp-mtime`) in bsdtar.

The end goal will be to support all functionalities in
<https://reproducible-builds.org/docs/archives/#full-example>, namely
`--sort` and disabling other attributes (atime, ctime, etc.).

Fixes #971.

## History

- [v1](https://github.com/zhaofengli/libarchive/tree/forced-mtime-v1):
Added `archive_read_disk_set_forced_mtime` in libarchive. As a result,
it was only applied when reading from the filesystem and not from other
archives.
- [v2](https://github.com/zhaofengli/libarchive/tree/forced-mtime-v2):
Refactored to apply the forced mtime in `archive_write`.
- v3 (current): Reduced libarchive change to exposing
`archive_parse_date`, moved clamping logic into bsdtar.

---------

Signed-off-by: Zhaofeng Li <hello@zhaofeng.li>
Co-authored-by: Dustin L. Howett <dustin@howett.net>
19 files changed:
COPYING
Makefile.am
contrib/android/Android.mk
libarchive/CMakeLists.txt
libarchive/archive.h
libarchive/archive_getdate.h [deleted file]
libarchive/archive_match.c
libarchive/archive_parse_date.c [moved from libarchive/archive_getdate.c with 99% similarity]
libarchive/test/CMakeLists.txt
libarchive/test/test_archive_match_time.c
libarchive/test/test_archive_parse_date.c [moved from libarchive/test/test_archive_getdate.c with 95% similarity]
tar/bsdtar.1
tar/bsdtar.c
tar/bsdtar.h
tar/cmdline.c
tar/test/CMakeLists.txt
tar/test/test_option_mtime.c [new file with mode: 0644]
tar/util.c
tar/write.c

diff --git a/COPYING b/COPYING
index 1b9723574a7be8d9c107239fc298723a32c0d5ae..9d0bcf81ff613bf5f36d71983b75e20cd0f69213 100644 (file)
--- a/COPYING
+++ b/COPYING
@@ -20,7 +20,7 @@ the actual statements in the files are controlling.
    libarchive/mtree.5
 
 * The following source files are in the public domain:
-   libarchive/archive_getdate.c
+   libarchive/archive_parse_date.c
 
 * The following source files are triple-licensed with the ability to choose
   from CC0 1.0 Universal, OpenSSL or Apache 2.0 licenses:
index 0beb283b1294b77bfc40edf9760e70493f8c8a96..4e17ab2e280b130df58db6f660965d9190ec178c 100644 (file)
@@ -118,8 +118,6 @@ libarchive_la_SOURCES= \
        libarchive/archive_entry_stat.c \
        libarchive/archive_entry_strmode.c \
        libarchive/archive_entry_xattr.c \
-       libarchive/archive_getdate.c \
-       libarchive/archive_getdate.h \
        libarchive/archive_hmac.c \
        libarchive/archive_hmac_private.h \
        libarchive/archive_match.c \
@@ -129,6 +127,7 @@ libarchive_la_SOURCES= \
        libarchive/archive_options_private.h \
        libarchive/archive_pack_dev.h \
        libarchive/archive_pack_dev.c \
+       libarchive/archive_parse_date.c \
        libarchive/archive_pathmatch.c \
        libarchive/archive_pathmatch.h \
        libarchive/archive_platform.h \
@@ -379,10 +378,10 @@ libarchive_test_SOURCES= \
        libarchive/test/test_archive_clear_error.c \
        libarchive/test/test_archive_cmdline.c \
        libarchive/test/test_archive_digest.c \
-       libarchive/test/test_archive_getdate.c \
        libarchive/test/test_archive_match_owner.c \
        libarchive/test/test_archive_match_path.c \
        libarchive/test/test_archive_match_time.c \
+       libarchive/test/test_archive_parse_date.c \
        libarchive/test/test_archive_pathmatch.c \
        libarchive/test/test_archive_read.c \
        libarchive/test/test_archive_read_add_passphrase.c \
@@ -1178,6 +1177,7 @@ bsdtar_test_SOURCES= \
        tar/test/test_option_lz4.c \
        tar/test/test_option_lzma.c \
        tar/test/test_option_lzop.c \
+       tar/test/test_option_mtime.c \
        tar/test/test_option_n.c \
        tar/test/test_option_newer_than.c \
        tar/test/test_option_nodump.c \
index 3f7619d66e273aea63acc1e4c34fa820d29a3d76..8e7fbdacefe96db6c9a7c6e73e5877b077f39cd3 100644 (file)
@@ -37,11 +37,11 @@ libarchive_src_files := libarchive/archive_acl.c \
                                                libarchive/archive_entry_stat.c \
                                                libarchive/archive_entry_strmode.c \
                                                libarchive/archive_entry_xattr.c \
-                                               libarchive/archive_getdate.c \
                                                libarchive/archive_hmac.c \
                                                libarchive/archive_match.c \
                                                libarchive/archive_options.c \
                                                libarchive/archive_pack_dev.c \
+                                               libarchive/archive_parse_date.c \
                                                libarchive/archive_pathmatch.c \
                                                libarchive/archive_ppmd7.c \
                                                libarchive/archive_random.c \
index fe13be1be1e7164a4d342ec6da42ccfb42fbac17..556710ad8285b5a57b5f693814eb5c1b3d2d0f90 100644 (file)
@@ -38,8 +38,6 @@ SET(libarchive_SOURCES
   archive_entry_stat.c
   archive_entry_strmode.c
   archive_entry_xattr.c
-  archive_getdate.c
-  archive_getdate.h
   archive_hmac.c
   archive_hmac_private.h
   archive_match.c
@@ -49,6 +47,7 @@ SET(libarchive_SOURCES
   archive_options_private.h
   archive_pack_dev.h
   archive_pack_dev.c
+  archive_parse_date.c
   archive_pathmatch.c
   archive_pathmatch.h
   archive_platform.h
index aeef45271f54f32b3e8a17ee2a63efb6bb3f338d..aad372f547f18b8e5a4952cc42304ca7ff001275 100644 (file)
@@ -1128,6 +1128,10 @@ __LA_DECL int             archive_compression(struct archive *)
                                __LA_DEPRECATED;
 #endif
 
+/* Parses a date string relative to the current time.
+ * NOTE: This is not intended for general date parsing, and the resulting timestamp should only be used for libarchive. */
+__LA_DECL time_t       archive_parse_date(time_t now, const char *datestr);
+
 __LA_DECL int           archive_errno(struct archive *);
 __LA_DECL const char   *archive_error_string(struct archive *);
 __LA_DECL const char   *archive_format_name(struct archive *);
diff --git a/libarchive/archive_getdate.h b/libarchive/archive_getdate.h
deleted file mode 100644 (file)
index cfd49dd..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-/*-
- * Copyright (c) 2003-2015 Tim Kientzle
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
- * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
- * IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-#ifndef ARCHIVE_GETDATE_H_INCLUDED
-#define ARCHIVE_GETDATE_H_INCLUDED
-
-#ifndef __LIBARCHIVE_BUILD
-#error This header is only to be used internally to libarchive.
-#endif
-
-#include <time.h>
-
-time_t __archive_get_date(time_t now, const char *);
-
-#endif
index c08b06092a46434e2e0fe8866a7c48a2b066cd97..20880d3d39af88a161d1ab3758d0618f9c7b45cd 100644 (file)
@@ -39,7 +39,6 @@
 #include "archive.h"
 #include "archive_private.h"
 #include "archive_entry.h"
-#include "archive_getdate.h"
 #include "archive_pathmatch.h"
 #include "archive_rb.h"
 #include "archive_string.h"
@@ -188,7 +187,7 @@ static int  time_excluded(struct archive_match *,
                    struct archive_entry *);
 static int     validate_time_flag(struct archive *, int, const char *);
 
-#define get_date __archive_get_date
+#define get_date archive_parse_date
 
 static const struct archive_rb_tree_ops rb_ops_mbs = {
        cmp_node_mbs, cmp_key_mbs
similarity index 99%
rename from libarchive/archive_getdate.c
rename to libarchive/archive_parse_date.c
index bf387ac695d609210d4edeae483734a8ac1ada8a..5e2af2aa0c447286bd09add3068bde34d860f011 100644 (file)
@@ -35,8 +35,7 @@
 #include <string.h>
 #include <time.h>
 
-#define __LIBARCHIVE_BUILD 1
-#include "archive_getdate.h"
+#include "archive.h"
 
 /* Basic time units. */
 #define        EPOCH           1970
@@ -937,7 +936,7 @@ difftm (struct tm *a, struct tm *b)
  * TODO: tokens[] array should be dynamically sized.
  */
 time_t
-__archive_get_date(time_t now, const char *p)
+archive_parse_date(time_t now, const char *p)
 {
        struct token    tokens[256];
        struct gdstate  _gds;
index 210221574bb98de10a6834d1fa997460e3d14dc2..4217e43623ed271072bbbc6bee0b209986b2b9a2 100644 (file)
@@ -21,10 +21,10 @@ IF(ENABLE_TEST)
     test_archive_clear_error.c
     test_archive_cmdline.c
     test_archive_digest.c
-    test_archive_getdate.c
     test_archive_match_owner.c
     test_archive_match_path.c
     test_archive_match_time.c
+    test_archive_parse_date.c
     test_archive_pathmatch.c
     test_archive_read.c
     test_archive_read_add_passphrase.c
index 27ad1da2f1394cc35eb9c1809d1de57b1e72946e..f957fcf70c2c87824ad95627ab0c701bab4e2d5e 100644 (file)
@@ -25,8 +25,7 @@
 
 #include "test.h"
 
-#define __LIBARCHIVE_BUILD 1
-#include "archive_getdate.h"
+#define parse_date archive_parse_date
 
 static void
 test_newer_time(void)
@@ -102,27 +101,27 @@ test_newer_time_str(void)
 
        /* Test1: Allow newer time. */
        archive_entry_copy_pathname(ae, "file1");
-       t = __archive_get_date(now, "1980/2/1 0:0:0 UTC");
+       t = parse_date(now, "1980/2/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 0);
        archive_entry_set_ctime(ae, t, 0);
        failure("Both Its mtime and ctime should be excluded");
        assertEqualInt(1, archive_match_time_excluded(m, ae));
        assertEqualInt(1, archive_match_excluded(m, ae));
-       t = __archive_get_date(now, "1980/1/1 0:0:0 UTC");
+       t = parse_date(now, "1980/1/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 0);
        archive_entry_set_ctime(ae, t, 0);
        failure("Both Its mtime and ctime should be excluded");
        assertEqualInt(1, archive_match_time_excluded(m, ae));
        assertEqualInt(1, archive_match_excluded(m, ae));
 
-       t = __archive_get_date(now, "1980/2/1 0:0:1 UTC");
+       t = parse_date(now, "1980/2/1 0:0:1 UTC");
        archive_entry_set_mtime(ae, t, 0);
        archive_entry_set_ctime(ae, t, 0);
        failure("Both Its mtime and ctime should not be excluded");
        assertEqualInt(0, archive_match_time_excluded(m, ae));
        assertEqualInt(0, archive_match_excluded(m, ae));
 
-       t = __archive_get_date(now, "1980/2/1 0:0:0 UTC");
+       t = parse_date(now, "1980/2/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 1);
        archive_entry_set_ctime(ae, t, 0);
        failure("Its mtime should be excluded");
@@ -143,20 +142,20 @@ test_newer_time_str(void)
            "1980/2/1 0:0:0 UTC"));
 
        archive_entry_copy_pathname(ae, "file1");
-       t = __archive_get_date(now, "1980/2/1 0:0:0 UTC");
+       t = parse_date(now, "1980/2/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 0);
        archive_entry_set_ctime(ae, t, 0);
        failure("Both Its mtime and ctime should not be excluded");
        assertEqualInt(0, archive_match_time_excluded(m, ae));
        assertEqualInt(0, archive_match_excluded(m, ae));
-       t = __archive_get_date(now, "1980/1/1 0:0:0 UTC");
+       t = parse_date(now, "1980/1/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 0);
        archive_entry_set_ctime(ae, t, 0);
        failure("Both Its mtime and ctime should be excluded");
        assertEqualInt(1, archive_match_time_excluded(m, ae));
        assertEqualInt(1, archive_match_excluded(m, ae));
 
-       t = __archive_get_date(now, "1980/2/1 0:0:1 UTC");
+       t = parse_date(now, "1980/2/1 0:0:1 UTC");
        archive_entry_set_mtime(ae, t, 0);
        archive_entry_set_ctime(ae, t, 0);
        failure("Both Its mtime and ctime should not be excluded");
@@ -190,27 +189,27 @@ test_newer_time_str_w(void)
 
        /* Test1: Allow newer time. */
        archive_entry_copy_pathname(ae, "file1");
-       t = __archive_get_date(now, "1980/2/1 0:0:0 UTC");
+       t = parse_date(now, "1980/2/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 0);
        archive_entry_set_ctime(ae, t, 0);
        failure("Both Its mtime and ctime should be excluded");
        assertEqualInt(1, archive_match_time_excluded(m, ae));
        assertEqualInt(1, archive_match_excluded(m, ae));
-       t = __archive_get_date(now, "1980/1/1 0:0:0 UTC");
+       t = parse_date(now, "1980/1/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 0);
        archive_entry_set_ctime(ae, t, 0);
        failure("Both Its mtime and ctime should be excluded");
        assertEqualInt(1, archive_match_time_excluded(m, ae));
        assertEqualInt(1, archive_match_excluded(m, ae));
 
-       t = __archive_get_date(now, "1980/2/1 0:0:1 UTC");
+       t = parse_date(now, "1980/2/1 0:0:1 UTC");
        archive_entry_set_mtime(ae, t, 0);
        archive_entry_set_ctime(ae, t, 0);
        failure("Both Its mtime and ctime should not be excluded");
        assertEqualInt(0, archive_match_time_excluded(m, ae));
        assertEqualInt(0, archive_match_excluded(m, ae));
 
-       t = __archive_get_date(now, "1980/2/1 0:0:0 UTC");
+       t = parse_date(now, "1980/2/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 1);
        archive_entry_set_ctime(ae, t, 0);
        failure("Its mtime should be excluded");
@@ -231,20 +230,20 @@ test_newer_time_str_w(void)
            L"1980/2/1 0:0:0 UTC"));
 
        archive_entry_copy_pathname(ae, "file1");
-       t = __archive_get_date(now, "1980/2/1 0:0:0 UTC");
+       t = parse_date(now, "1980/2/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 0);
        archive_entry_set_ctime(ae, t, 0);
        failure("Both Its mtime and ctime should not be excluded");
        assertEqualInt(0, archive_match_time_excluded(m, ae));
        assertEqualInt(0, archive_match_excluded(m, ae));
-       t = __archive_get_date(now, "1980/1/1 0:0:0 UTC");
+       t = parse_date(now, "1980/1/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 0);
        archive_entry_set_ctime(ae, t, 0);
        failure("Both Its mtime and ctime should be excluded");
        assertEqualInt(1, archive_match_time_excluded(m, ae));
        assertEqualInt(1, archive_match_excluded(m, ae));
 
-       t = __archive_get_date(now, "1980/2/1 0:0:1 UTC");
+       t = parse_date(now, "1980/2/1 0:0:1 UTC");
        archive_entry_set_mtime(ae, t, 0);
        archive_entry_set_ctime(ae, t, 0);
        failure("Both Its mtime and ctime should not be excluded");
@@ -569,37 +568,37 @@ test_older_time_str(void)
            ARCHIVE_MATCH_OLDER, "1980/2/1 0:0:0 UTC"));
 
        archive_entry_copy_pathname(ae, "file1");
-       t = __archive_get_date(now, "1980/2/1 0:0:0 UTC");
+       t = parse_date(now, "1980/2/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 0);
        archive_entry_set_ctime(ae, t, 0);
        failure("Both Its mtime and ctime should be excluded");
        assertEqualInt(1, archive_match_time_excluded(m, ae));
        assertEqualInt(1, archive_match_excluded(m, ae));
-       t = __archive_get_date(now, "1980/1/1 0:0:0 UTC");
+       t = parse_date(now, "1980/1/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 0);
        archive_entry_set_ctime(ae, t, 0);
        failure("Both Its mtime and ctime should not be excluded");
        assertEqualInt(0, archive_match_time_excluded(m, ae));
        assertEqualInt(0, archive_match_excluded(m, ae));
 
-       t = __archive_get_date(now, "1980/3/1 0:0:0 UTC");
+       t = parse_date(now, "1980/3/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 0);
        archive_entry_set_ctime(ae, t, 0);
        failure("Both Its mtime and ctime should be excluded");
        assertEqualInt(1, archive_match_time_excluded(m, ae));
        assertEqualInt(1, archive_match_excluded(m, ae));
 
-       t = __archive_get_date(now, "1980/3/1 0:0:0 UTC");
+       t = parse_date(now, "1980/3/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 0);
-       t = __archive_get_date(now, "1980/1/1 0:0:0 UTC");
+       t = parse_date(now, "1980/1/1 0:0:0 UTC");
        archive_entry_set_ctime(ae, t, 0);
        failure("Its mtime should be excluded");
        assertEqualInt(1, archive_match_time_excluded(m, ae));
        assertEqualInt(1, archive_match_excluded(m, ae));
 
-       t = __archive_get_date(now, "1980/1/1 0:0:0 UTC");
+       t = parse_date(now, "1980/1/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 0);
-       t = __archive_get_date(now, "1980/3/1 0:0:0 UTC");
+       t = parse_date(now, "1980/3/1 0:0:0 UTC");
        archive_entry_set_ctime(ae, t, 0);
        failure("Its ctime should be excluded");
        assertEqualInt(1, archive_match_time_excluded(m, ae));
@@ -612,20 +611,20 @@ test_older_time_str(void)
            "1980/2/1 0:0:0 UTC"));
 
        archive_entry_copy_pathname(ae, "file1");
-       t = __archive_get_date(now, "1980/2/1 0:0:0 UTC");
+       t = parse_date(now, "1980/2/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 0);
        archive_entry_set_ctime(ae, t, 0);
        failure("Both Its mtime and ctime should not be excluded");
        assertEqualInt(0, archive_match_time_excluded(m, ae));
        assertEqualInt(0, archive_match_excluded(m, ae));
-       t = __archive_get_date(now, "1980/1/1 0:0:0 UTC");
+       t = parse_date(now, "1980/1/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 0);
        archive_entry_set_ctime(ae, t, 0);
        failure("Both Its mtime and ctime should not be excluded");
        assertEqualInt(0, archive_match_time_excluded(m, ae));
        assertEqualInt(0, archive_match_excluded(m, ae));
 
-       t = __archive_get_date(now, "1980/3/1 0:0:0 UTC");
+       t = parse_date(now, "1980/3/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 0);
        archive_entry_set_ctime(ae, t, 0);
        failure("Both Its mtime and ctime should be excluded");
@@ -659,37 +658,37 @@ test_older_time_str_w(void)
            ARCHIVE_MATCH_OLDER, L"1980/2/1 0:0:0 UTC"));
 
        archive_entry_copy_pathname(ae, "file1");
-       t = __archive_get_date(now, "1980/2/1 0:0:0 UTC");
+       t = parse_date(now, "1980/2/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 0);
        archive_entry_set_ctime(ae, t, 0);
        failure("Both Its mtime and ctime should be excluded");
        assertEqualInt(1, archive_match_time_excluded(m, ae));
        assertEqualInt(1, archive_match_excluded(m, ae));
-       t = __archive_get_date(now, "1980/1/1 0:0:0 UTC");
+       t = parse_date(now, "1980/1/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 0);
        archive_entry_set_ctime(ae, t, 0);
        failure("Both Its mtime and ctime should not be excluded");
        assertEqualInt(0, archive_match_time_excluded(m, ae));
        assertEqualInt(0, archive_match_excluded(m, ae));
 
-       t = __archive_get_date(now, "1980/3/1 0:0:0 UTC");
+       t = parse_date(now, "1980/3/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 0);
        archive_entry_set_ctime(ae, t, 0);
        failure("Both Its mtime and ctime should be excluded");
        assertEqualInt(1, archive_match_time_excluded(m, ae));
        assertEqualInt(1, archive_match_excluded(m, ae));
 
-       t = __archive_get_date(now, "1980/3/1 0:0:0 UTC");
+       t = parse_date(now, "1980/3/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 0);
-       t = __archive_get_date(now, "1980/1/1 0:0:0 UTC");
+       t = parse_date(now, "1980/1/1 0:0:0 UTC");
        archive_entry_set_ctime(ae, t, 0);
        failure("Its mtime should be excluded");
        assertEqualInt(1, archive_match_time_excluded(m, ae));
        assertEqualInt(1, archive_match_excluded(m, ae));
 
-       t = __archive_get_date(now, "1980/1/1 0:0:0 UTC");
+       t = parse_date(now, "1980/1/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 0);
-       t = __archive_get_date(now, "1980/3/1 0:0:0 UTC");
+       t = parse_date(now, "1980/3/1 0:0:0 UTC");
        archive_entry_set_ctime(ae, t, 0);
        failure("Its ctime should be excluded");
        assertEqualInt(1, archive_match_time_excluded(m, ae));
@@ -702,20 +701,20 @@ test_older_time_str_w(void)
            L"1980/2/1 0:0:0 UTC"));
 
        archive_entry_copy_pathname(ae, "file1");
-       t = __archive_get_date(now, "1980/2/1 0:0:0 UTC");
+       t = parse_date(now, "1980/2/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 0);
        archive_entry_set_ctime(ae, t, 0);
        failure("Both Its mtime and ctime should not be excluded");
        assertEqualInt(0, archive_match_time_excluded(m, ae));
        assertEqualInt(0, archive_match_excluded(m, ae));
-       t = __archive_get_date(now, "1980/1/1 0:0:0 UTC");
+       t = parse_date(now, "1980/1/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 0);
        archive_entry_set_ctime(ae, t, 0);
        failure("Both Its mtime and ctime should not be excluded");
        assertEqualInt(0, archive_match_time_excluded(m, ae));
        assertEqualInt(0, archive_match_excluded(m, ae));
 
-       t = __archive_get_date(now, "1980/3/1 0:0:0 UTC");
+       t = parse_date(now, "1980/3/1 0:0:0 UTC");
        archive_entry_set_mtime(ae, t, 0);
        archive_entry_set_ctime(ae, t, 0);
        failure("Both Its mtime and ctime should be excluded");
similarity index 95%
rename from libarchive/test/test_archive_getdate.c
rename to libarchive/test/test_archive_parse_date.c
index e5b8bf7fa8f8057233216a9a6b8bfd311d3e66c0..fffec9ea64ae4c1190100c16ece25318ae8a04e8 100644 (file)
 
 #include <time.h>
 
-#define __LIBARCHIVE_BUILD 1
-#include "archive_getdate.h"
-
 /*
- * Verify that the getdate() function works.
+ * Verify that the archive_parse_date() function works.
  */
 
-#define get_date __archive_get_date
+#define get_date archive_parse_date
 
-DEFINE_TEST(test_archive_getdate)
+DEFINE_TEST(test_archive_parse_date)
 {
        time_t now = time(NULL);
 
index 555ac7edaee587549a4dd8d86eacaba5a0d5460e..0062789e27e961c89940cbe63608336840a97e3e 100644 (file)
@@ -196,6 +196,11 @@ but before extracting entries from the archive.
 to the current directory after processing any
 .Fl C
 options and before extracting any files.
+.It Fl Fl clamp-mtime
+(use with
+.Fl Fl mtime )
+Only set the modification time if the file is newer than the date specified in
+.Fl Fl mtime .
 .It Fl Fl clear-nochange-fflags
 (x mode only)
 Before removing file system objects to replace them, clear platform-specific
@@ -416,6 +421,9 @@ is run in x mode as root.
 Currently supported only for pax formats
 .Po including "pax restricted", the default tar format for
 .Nm bsdtar Pc
+.It Fl Fl mtime Ar date
+(c, r, u modes only)
+Set the modification times of added files to the specified date.
 .It Fl n , Fl Fl norecurse , Fl Fl no-recursion
 Do not operate recursively on the content of directories.
 .It Fl Fl newer Ar date
index c656cfbc81f0779880a08e7d4a042e35852e4a26..a043cd6b75ced3701ae32d4f1de9857316080e7f 100644 (file)
@@ -146,6 +146,7 @@ main(int argc, char **argv)
        char                     possible_help_request;
        char                     buff[16];
        long                     l;
+       time_t                  now;
 
        /*
         * Use a pointer for consistency, but stack-allocated storage
@@ -160,6 +161,7 @@ main(int argc, char **argv)
        compression = compression2 = '\0';
        compression_name = compression2_name = NULL;
        compress_program = NULL;
+       time(&now);
 
 #if defined(HAVE_SIGACTION)
        { /* Set up signal handling. */
@@ -676,6 +678,16 @@ main(int argc, char **argv)
                                }
                        }
                        break;
+               case OPTION_MTIME: /* GNU tar */
+                       bsdtar->has_mtime = 1;
+                       bsdtar->mtime = archive_parse_date(now, bsdtar->argument);
+                       if (bsdtar->mtime == (time_t)-1) {
+                               lafe_errc(1, 0, "Invalid argument to --mtime (bad date string)");
+                       }
+                       break;
+               case OPTION_CLAMP_MTIME: /* GNU tar */
+                       bsdtar->clamp_mtime = 1;
+                       break;
 #if 0
                /*
                 * The common BSD -P option is not necessary, since
@@ -962,6 +974,10 @@ main(int argc, char **argv)
                only_mode(bsdtar, buff, "cru");
        }
 
+       if (!bsdtar->has_mtime && bsdtar->clamp_mtime)
+               lafe_errc(1, 0,
+                   "--clamp-mtime is not valid without --mtime <date>");
+
        /*
         * When creating an archive from a directory tree, the directory
         * walking code will already avoid entering directories when
@@ -1066,6 +1082,8 @@ static const char *long_help_msg =
        "  -z, -j, -J, --lzma  Compress archive with gzip/bzip2/xz/lzma\n"
        "  --format {ustar|pax|cpio|shar}  Select archive format\n"
        "  --exclude <pattern>  Skip files that match pattern\n"
+       "  --mtime <date>  Set modification times for added files\n"
+       "  --clamp-mtime   Only set modification times for files newer than --mtime\n"
        "  -C <dir>  Change to <dir> before processing remaining files\n"
        "  @<archive>  Add entries from <archive> to output\n"
        "List: %p -t [options] [<patterns>]\n"
index ac995c82b3bcf559cb0344ba447c03a609f0d2cd..45dfeed7dce3ff1075907f31020f48d63e5e4bbf 100644 (file)
@@ -46,6 +46,9 @@ struct bsdtar {
        char              symlink_mode; /* H or L, per BSD conventions */
        const char       *option_options; /* --options */
        char              day_first; /* show day before month in -tv output */
+       char            has_mtime; /* --mtime exists (0 or 1) */
+       char            clamp_mtime; /* --clamp-mtime (0 or 1)*/
+       time_t          mtime; /* --mtime */
        struct creation_set *cset;
 
        /* Option parser state */
@@ -175,11 +178,14 @@ enum {
        OPTION_VERSION,
        OPTION_XATTRS,
        OPTION_ZSTD,
+       OPTION_MTIME,
+       OPTION_CLAMP_MTIME,
 };
 
 int    bsdtar_getopt(struct bsdtar *);
 void   do_chdir(struct bsdtar *);
 int    edit_pathname(struct bsdtar *, struct archive_entry *);
+void   edit_mtime(struct bsdtar *, struct archive_entry *);
 int    need_report(void);
 int    pathcmp(const char *a, const char *b);
 void   safe_fprintf(FILE * restrict, const char * restrict fmt, ...) __LA_PRINTF(2, 3);
index 35cb1cb43bf1e00270ec84d3aa0439f31202d237..c766c1a52dbb794700352f44bf9018616e99aeca 100644 (file)
@@ -57,6 +57,7 @@ static const struct bsdtar_option {
        { "cd",                   1, 'C' },
        { "check-links",          0, OPTION_CHECK_LINKS },
        { "chroot",               0, OPTION_CHROOT },
+       { "clamp-mtime",          0, OPTION_CLAMP_MTIME },
        { "clear-nochange-fflags", 0, OPTION_CLEAR_NOCHANGE_FFLAGS },
        { "compress",             0, 'Z' },
        { "confirmation",         0, 'w' },
@@ -95,6 +96,7 @@ static const struct bsdtar_option {
        { "lzop",                 0, OPTION_LZOP },
        { "mac-metadata",         0, OPTION_MAC_METADATA },
        { "modification-time",    0, 'm' },
+       { "mtime",                1, OPTION_MTIME },
        { "newer",                1, OPTION_NEWER_CTIME },
        { "newer-ctime",          1, OPTION_NEWER_CTIME },
        { "newer-ctime-than",     1, OPTION_NEWER_CTIME_THAN },
index 7a3006f753de144e21f4fb4314dff545cd2ce5f8..7a3803bdcb66cefa0d99f357ef7c958b6d9a2c1f 100644 (file)
@@ -58,6 +58,7 @@ IF(ENABLE_TAR AND ENABLE_TEST)
     test_option_lz4.c
     test_option_lzma.c
     test_option_lzop.c
+    test_option_mtime.c
     test_option_n.c
     test_option_newer_than.c
     test_option_nodump.c
diff --git a/tar/test/test_option_mtime.c b/tar/test/test_option_mtime.c
new file mode 100644 (file)
index 0000000..75cdb83
--- /dev/null
@@ -0,0 +1,82 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2025 Zhaofeng Li
+ * All rights reserved.
+ */
+#include "test.h"
+
+#if defined(_WIN32) && !defined(__CYGWIN__)
+/* system() on Windows runs its arguments through CMD.EXE, which has
+ * notoriously unfriendly quoting rules. The current best documented way around
+ * them is to wrap your *entire commandline* in sacrificial quotes.
+ *
+ * See CMD.EXE /? for more information. Excerpted here:
+ * | Otherwise, old behavior is to see if the first character is
+ * | a quote character and if so, strip the leading character and
+ * | remove the last quote character on the command line, preserving
+ * | any text after the last quote character.
+ *
+ * Since this test makes heavy use of systemf() with quoted arguments inside
+ * the commandline, this macro is unfortunately an easier workaround.
+ */
+#define systemf(command, ...) systemf("\"" command "\"", __VA_ARGS__)
+#endif
+
+DEFINE_TEST(test_option_mtime)
+{
+       /* Create three files with different mtimes. */
+       assertMakeDir("in", 0755);
+       assertChdir("in");
+       assertMakeFile("new_mtime", 0666, "new");
+       assertUtimes("new_mtime", 100000, 0, 100000, 0);
+       assertMakeFile("mid_mtime", 0666, "mid");
+       assertUtimes("mid_mtime", 10000, 0, 10000, 0);
+       assertMakeFile("old_mtime", 0666, "old");
+       // assertion_utimes silently discards 0 :(
+       assertUtimes("old_mtime", 1, 0, 1, 0);
+
+       /* Archive with --mtime 86400 */
+       assertEqualInt(0,
+               systemf("%s --format pax -cf ../noclamp.tar "
+                       "--mtime \"1970/1/2 0:0:0 UTC\" .",
+                       testprog));
+       assertChdir("..");
+
+       assertMakeDir("out.noclamp", 0755);
+       assertChdir("out.noclamp");
+       assertEqualInt(0, systemf("%s xf ../noclamp.tar", testprog));
+       assertFileMtime("new_mtime", 86400, 0);
+       assertFileMtime("mid_mtime", 86400, 0);
+       assertFileMtime("old_mtime", 86400, 0);
+       assertChdir("..");
+
+       /* Archive with --mtime 86400 --clamp-mtime */
+       assertChdir("in");
+       assertEqualInt(0,
+               systemf("%s --format pax -cf ../clamp.tar "
+                       "--mtime \"1970/1/2 0:0:0 UTC\" --clamp-mtime .",
+                       testprog));
+       assertChdir("..");
+
+       assertMakeDir("out.clamp", 0755);
+       assertChdir("out.clamp");
+       assertEqualInt(0, systemf("%s xf ../clamp.tar", testprog));
+       assertFileMtime("new_mtime", 86400, 0);
+       assertFileMtime("mid_mtime", 10000, 0);
+       assertFileMtime("old_mtime", 1, 0);
+       assertChdir("..");
+
+       /* Archive-to-archive copy with --mtime 0 */
+       assertEqualInt(0,
+               systemf("%s --format pax -cf ./archive2archive.tar "
+                       "--mtime \"1970/1/1 0:0:0 UTC\" @noclamp.tar",
+                       testprog));
+       assertMakeDir("out.archive2archive", 0755);
+       assertChdir("out.archive2archive");
+       assertEqualInt(0, systemf("%s xf ../archive2archive.tar", testprog));
+       assertFileMtime("new_mtime", 0, 0);
+       assertFileMtime("mid_mtime", 0, 0);
+       assertFileMtime("old_mtime", 0, 0);
+       assertChdir("..");
+}
index 39d5c452e633915af282374c273f87c015d68124..7b9db5ddc652d687419d841506a328a6996d273b 100644 (file)
@@ -562,6 +562,20 @@ edit_pathname(struct bsdtar *bsdtar, struct archive_entry *entry)
        return (0);
 }
 
+/*
+ * Apply --mtime and --clamp-mtime options.
+ */
+void
+edit_mtime(struct bsdtar *bsdtar, struct archive_entry *entry)
+{
+       if (!bsdtar->has_mtime)
+               return;
+
+       __LA_TIME_T entry_mtime = archive_entry_mtime(entry);
+       if (!bsdtar->clamp_mtime || entry_mtime > bsdtar->mtime)
+               archive_entry_set_mtime(entry, bsdtar->mtime, 0);
+}
+
 /*
  * It would be nice to just use printf() for formatting large numbers,
  * but the compatibility problems are quite a headache.  Hence the
index f1838d6b5dead99757f71cb7580850dfedc45125..21984e980ebd8433ad874bf4d4f9de78d31a287f 100644 (file)
@@ -682,6 +682,7 @@ append_archive(struct bsdtar *bsdtar, struct archive *a, struct archive *ina)
                if ((bsdtar->flags & OPTFLAG_INTERACTIVE) &&
                    !yes("copy '%s'", archive_entry_pathname(in_entry)))
                        continue;
+               edit_mtime(bsdtar, in_entry);
                if (bsdtar->verbose > 1) {
                        safe_fprintf(stderr, "a ");
                        list_item_verbose(bsdtar, stderr, in_entry);
@@ -908,6 +909,9 @@ write_hierarchy(struct bsdtar *bsdtar, struct archive *a, const char *path)
                if (edit_pathname(bsdtar, entry))
                        continue;
 
+               /* Rewrite the mtime. */
+               edit_mtime(bsdtar, entry);
+
                /* Display entry as we process it. */
                if (bsdtar->verbose > 1) {
                        safe_fprintf(stderr, "a ");