From: Zhaofeng Li Date: Thu, 15 May 2025 12:08:14 +0000 (-0600) Subject: bsdtar: Support `--mtime` and `--clamp-mtime` (#2601) X-Git-Tag: v3.8.0~20 X-Git-Url: http://git.ipfire.org/gitweb/gitweb.cgi?a=commitdiff_plain;h=c26f0377457db392bd57a640e8fe25506120f810;p=thirdparty%2Flibarchive.git bsdtar: Support `--mtime` and `--clamp-mtime` (#2601) 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 , 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 Co-authored-by: Dustin L. Howett --- diff --git a/COPYING b/COPYING index 1b9723574..9d0bcf81f 100644 --- 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: diff --git a/Makefile.am b/Makefile.am index 0beb283b1..4e17ab2e2 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 \ diff --git a/contrib/android/Android.mk b/contrib/android/Android.mk index 3f7619d66..8e7fbdace 100644 --- a/contrib/android/Android.mk +++ b/contrib/android/Android.mk @@ -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 \ diff --git a/libarchive/CMakeLists.txt b/libarchive/CMakeLists.txt index fe13be1be..556710ad8 100644 --- a/libarchive/CMakeLists.txt +++ b/libarchive/CMakeLists.txt @@ -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 diff --git a/libarchive/archive.h b/libarchive/archive.h index aeef45271..aad372f54 100644 --- a/libarchive/archive.h +++ b/libarchive/archive.h @@ -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 index cfd49ddf7..000000000 --- a/libarchive/archive_getdate.h +++ /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_t __archive_get_date(time_t now, const char *); - -#endif diff --git a/libarchive/archive_match.c b/libarchive/archive_match.c index c08b06092..20880d3d3 100644 --- a/libarchive/archive_match.c +++ b/libarchive/archive_match.c @@ -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 diff --git a/libarchive/archive_getdate.c b/libarchive/archive_parse_date.c similarity index 99% rename from libarchive/archive_getdate.c rename to libarchive/archive_parse_date.c index bf387ac69..5e2af2aa0 100644 --- a/libarchive/archive_getdate.c +++ b/libarchive/archive_parse_date.c @@ -35,8 +35,7 @@ #include #include -#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; diff --git a/libarchive/test/CMakeLists.txt b/libarchive/test/CMakeLists.txt index 210221574..4217e4362 100644 --- a/libarchive/test/CMakeLists.txt +++ b/libarchive/test/CMakeLists.txt @@ -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 diff --git a/libarchive/test/test_archive_match_time.c b/libarchive/test/test_archive_match_time.c index 27ad1da2f..f957fcf70 100644 --- a/libarchive/test/test_archive_match_time.c +++ b/libarchive/test/test_archive_match_time.c @@ -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"); diff --git a/libarchive/test/test_archive_getdate.c b/libarchive/test/test_archive_parse_date.c similarity index 95% rename from libarchive/test/test_archive_getdate.c rename to libarchive/test/test_archive_parse_date.c index e5b8bf7fa..fffec9ea6 100644 --- a/libarchive/test/test_archive_getdate.c +++ b/libarchive/test/test_archive_parse_date.c @@ -26,16 +26,13 @@ #include -#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); diff --git a/tar/bsdtar.1 b/tar/bsdtar.1 index 555ac7eda..0062789e2 100644 --- a/tar/bsdtar.1 +++ b/tar/bsdtar.1 @@ -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 diff --git a/tar/bsdtar.c b/tar/bsdtar.c index c656cfbc8..a043cd6b7 100644 --- a/tar/bsdtar.c +++ b/tar/bsdtar.c @@ -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 "); + /* * 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 Skip files that match pattern\n" + " --mtime Set modification times for added files\n" + " --clamp-mtime Only set modification times for files newer than --mtime\n" " -C Change to before processing remaining files\n" " @ Add entries from to output\n" "List: %p -t [options] []\n" diff --git a/tar/bsdtar.h b/tar/bsdtar.h index ac995c82b..45dfeed7d 100644 --- a/tar/bsdtar.h +++ b/tar/bsdtar.h @@ -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); diff --git a/tar/cmdline.c b/tar/cmdline.c index 35cb1cb43..c766c1a52 100644 --- a/tar/cmdline.c +++ b/tar/cmdline.c @@ -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 }, diff --git a/tar/test/CMakeLists.txt b/tar/test/CMakeLists.txt index 7a3006f75..7a3803bdc 100644 --- a/tar/test/CMakeLists.txt +++ b/tar/test/CMakeLists.txt @@ -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 index 000000000..75cdb8390 --- /dev/null +++ b/tar/test/test_option_mtime.c @@ -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(".."); +} diff --git a/tar/util.c b/tar/util.c index 39d5c452e..7b9db5ddc 100644 --- a/tar/util.c +++ b/tar/util.c @@ -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 diff --git a/tar/write.c b/tar/write.c index f1838d6b5..21984e980 100644 --- a/tar/write.c +++ b/tar/write.c @@ -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 ");