diff --git a/src/borg/helpers/time.py b/src/borg/helpers/time.py index 7dfe0d38c4..b98d7fe1f4 100644 --- a/src/borg/helpers/time.py +++ b/src/borg/helpers/time.py @@ -26,12 +26,21 @@ def parse_local_timestamp(timestamp, tzinfo=None): return dt +_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc) + + +def utcfromtimestampns(ts_ns: int) -> datetime: + # similar to datetime.fromtimestamp, but works with ns and avoids floating point. + # also, it would avoid an overflow on 32bit platforms with old glibc. + return _EPOCH + timedelta(microseconds=ts_ns // 1000) + + def timestamp(s): """Convert a --timestamp=s argument to a datetime object.""" try: # is it pointing to a file / directory? - ts = safe_s(os.stat(s).st_mtime) - return datetime.fromtimestamp(ts, tz=timezone.utc) + ts_ns = safe_ns(os.stat(s).st_mtime_ns) + return utcfromtimestampns(ts_ns) except OSError: # didn't work, try parsing as an ISO timestamp. if no TZ is given, we assume local timezone. return parse_local_timestamp(s) @@ -44,7 +53,7 @@ def timestamp(s): # As long as people are using borg on 32bit platforms to access borg archives, we must # keep this value True. But we can expect that we can stop supporting 32bit platforms # well before coming close to the year 2038, so this will never be a practical problem. -SUPPORT_32BIT_PLATFORMS = True # set this to False before y2038. +SUPPORT_32BIT_PLATFORMS = False # set this to False before y2038. if SUPPORT_32BIT_PLATFORMS: # second timestamps will fit into a signed int32 (platform time_t limit). @@ -84,7 +93,7 @@ def safe_ns(ts): def safe_timestamp(item_timestamp_ns): t_ns = safe_ns(item_timestamp_ns) - return datetime.fromtimestamp(t_ns / 1e9, timezone.utc) # return tz-aware utc datetime obj + return utcfromtimestampns(t_ns) # return tz-aware utc datetime obj def format_time(ts: datetime, format_spec=""): diff --git a/src/borg/testsuite/archiver/create_cmd_test.py b/src/borg/testsuite/archiver/create_cmd_test.py index 00edc0935d..f99b187a6f 100644 --- a/src/borg/testsuite/archiver/create_cmd_test.py +++ b/src/borg/testsuite/archiver/create_cmd_test.py @@ -223,7 +223,7 @@ def test_nobirthtime(archivers, request): assert same_ts_ns(sti.st_birthtime * 1e9, birthtime * 1e9) assert same_ts_ns(sto.st_birthtime * 1e9, mtime * 1e9) assert same_ts_ns(sti.st_mtime_ns, sto.st_mtime_ns) - assert same_ts_ns(sto.st_mtime_ns, mtime * 1e9) + assert same_ts_ns(sto.st_mtime_ns, mtime * 10**9) def test_create_stdin(archivers, request): diff --git a/src/borg/testsuite/archiver/extract_cmd_test.py b/src/borg/testsuite/archiver/extract_cmd_test.py index fe41c23a00..2b89db1cbc 100644 --- a/src/borg/testsuite/archiver/extract_cmd_test.py +++ b/src/borg/testsuite/archiver/extract_cmd_test.py @@ -153,13 +153,13 @@ def has_noatime(some_file): sti = os.stat("input/file1") sto = os.stat("output/input/file1") assert same_ts_ns(sti.st_mtime_ns, sto.st_mtime_ns) - assert same_ts_ns(sto.st_mtime_ns, mtime * 1e9) + assert same_ts_ns(sto.st_mtime_ns, mtime * 10**9) if have_noatime: assert same_ts_ns(sti.st_atime_ns, sto.st_atime_ns) - assert same_ts_ns(sto.st_atime_ns, atime * 1e9) + assert same_ts_ns(sto.st_atime_ns, atime * 10**9) else: # it touched the input file's atime while backing it up - assert same_ts_ns(sto.st_atime_ns, atime * 1e9) + assert same_ts_ns(sto.st_atime_ns, atime * 10**9) @pytest.mark.skipif(not is_utime_fully_supported(), reason="cannot setup and execute test without utime") @@ -179,7 +179,7 @@ def test_birthtime(archivers, request): assert same_ts_ns(sti.st_birthtime * 1e9, sto.st_birthtime * 1e9) assert same_ts_ns(sto.st_birthtime * 1e9, birthtime * 1e9) assert same_ts_ns(sti.st_mtime_ns, sto.st_mtime_ns) - assert same_ts_ns(sto.st_mtime_ns, mtime * 1e9) + assert same_ts_ns(sto.st_mtime_ns, mtime * 10**9) @pytest.mark.skipif(is_win32, reason="frequent test failures on github CI on win32") @@ -825,3 +825,19 @@ def test_extract_existing_directory(archivers, request): cmd(archiver, "extract", "test") st2 = os.stat("input/dir") assert st1.st_ino == st2.st_ino + + +@pytest.mark.skipif(not is_utime_fully_supported(), reason="cannot properly setup and execute test without utime") +def test_extract_y2261(archivers, request): + # test if roundtripping of timestamps well beyond y2038 works + archiver = request.getfixturevalue(archivers) + create_regular_file(archiver.input_path, "file_y2261", contents=b"post y2038 test") + # 2261-01-01 00:00:00 UTC as a Unix timestamp (seconds). + time_y2261 = 9183110400 + os.utime("input/file_y2261", (time_y2261, time_y2261)) + cmd(archiver, "repo-create", RK_ENCRYPTION) + cmd(archiver, "create", "test", "input") + with changedir("output"): + cmd(archiver, "extract", "test") + sto = os.stat("output/input/file_y2261") + assert same_ts_ns(sto.st_mtime_ns, time_y2261 * 10**9) diff --git a/src/borg/testsuite/archiver/repo12.tar.gz b/src/borg/testsuite/archiver/repo12.tar.gz index cd4a45b3f1..72d97b02f7 100644 Binary files a/src/borg/testsuite/archiver/repo12.tar.gz and b/src/borg/testsuite/archiver/repo12.tar.gz differ