0
0
mirror of https://github.com/libarchive/libarchive.git synced 2026-01-18 17:11:25 +01:00

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
<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>
This commit is contained in:
Zhaofeng Li
2025-05-15 06:08:14 -06:00
committed by GitHub
parent 84ac71335f
commit c26f037745
19 changed files with 187 additions and 92 deletions

View File

@@ -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:

View 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 \

View 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 \

View 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

View 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 *);

View File

@@ -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

View 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

View 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;

View 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

View 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");

View File

@@ -26,16 +26,13 @@
#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);

View 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

View 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"

View 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);

View 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 },

View 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

View File

@@ -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("..");
}

View 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

View 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 ");