diff --git a/CHANGES b/CHANGES index 070dc70b6..0c4679f8b 100644 --- a/CHANGES +++ b/CHANGES @@ -15,6 +15,7 @@ Version 0.13 - Fix bug where xattrs on symlinks were not correctly restored - Added cachedir support. CACHEDIR.TAG compatible cache directories can now be excluded using ``--exclude-caches`` (#74) +- Fix crash on extreme mtime timestamps (year 2400+) (#81) Version 0.12 ------------ diff --git a/attic/archive.py b/attic/archive.py index 3f6cb4387..e796d71af 100644 --- a/attic/archive.py +++ b/attic/archive.py @@ -18,7 +18,7 @@ from attic.platform import acl_get, acl_set from attic.chunker import chunkify from attic.hashindex import ChunkIndex from attic.helpers import Error, uid2user, user2uid, gid2group, group2gid, \ - Manifest, Statistics, decode_dict, st_mtime_ns, make_path_safe, StableDict + Manifest, Statistics, decode_dict, st_mtime_ns, make_path_safe, StableDict, int_to_bigint, bigint_to_int ITEMS_BUFFER = 1024 * 1024 CHUNK_MIN = 1024 @@ -311,12 +311,13 @@ class Archive: os.chmod(path, item[b'mode']) elif has_lchmod: # Not available on Linux os.lchmod(path, item[b'mode']) + mtime = bigint_to_int(item[b'mtime']) if fd and utime_supports_fd: # Python >= 3.3 - os.utime(fd, None, ns=(item[b'mtime'], item[b'mtime'])) + os.utime(fd, None, ns=(mtime, mtime)) elif utime_supports_fd: # Python >= 3.3 - os.utime(path, None, ns=(item[b'mtime'], item[b'mtime']), follow_symlinks=False) + os.utime(path, None, ns=(mtime, mtime), follow_symlinks=False) elif not symlink: - os.utime(path, (item[b'mtime'] / 10**9, item[b'mtime'] / 10**9)) + os.utime(path, (mtime / 1e9, mtime / 1e9)) acl_set(path, item, self.numeric_owner) # Only available on OS X and FreeBSD if has_lchflags and b'bsdflags' in item: @@ -343,7 +344,7 @@ class Archive: b'mode': st.st_mode, b'uid': st.st_uid, b'user': uid2user(st.st_uid), b'gid': st.st_gid, b'group': gid2group(st.st_gid), - b'mtime': st_mtime_ns(st), + b'mtime': int_to_bigint(st_mtime_ns(st)) } if self.numeric_owner: item[b'user'] = item[b'group'] = None diff --git a/attic/archiver.py b/attic/archiver.py index dafab9640..0760468e4 100644 --- a/attic/archiver.py +++ b/attic/archiver.py @@ -18,7 +18,7 @@ from attic.helpers import Error, location_validator, format_time, \ format_file_mode, ExcludePattern, exclude_path, adjust_patterns, to_localtime, \ get_cache_dir, get_keys_dir, format_timedelta, prune_within, prune_split, \ Manifest, remove_surrogates, update_excludes, format_archive, check_extension_modules, Statistics, \ - is_cachedir + is_cachedir, bigint_to_int from attic.remote import RepositoryServer, RemoteRepository @@ -272,7 +272,7 @@ Type "Yes I am sure" if you understand this and want to continue.\n""") size = sum(size for _, size, _ in item[b'chunks']) except KeyError: pass - mtime = format_time(datetime.fromtimestamp(item[b'mtime'] / 10**9)) + mtime = format_time(datetime.fromtimestamp(bigint_to_int(item[b'mtime']) / 1e9)) if b'source' in item: if type == 'l': extra = ' -> %s' % item[b'source'] diff --git a/attic/helpers.py b/attic/helpers.py index 0f56b659f..bc8ceea85 100644 --- a/attic/helpers.py +++ b/attic/helpers.py @@ -280,7 +280,7 @@ def walk_path(path, skip_inodes=None): def format_time(t): """Format datetime suitable for fixed length list output """ - if (datetime.now() - t).days < 365: + if abs((datetime.now() - t).days) < 365: return t.strftime('%b %d %H:%M') else: return t.strftime('%b %d %Y') @@ -548,3 +548,21 @@ else: return st.st_mtime_ns unhexlify = binascii.unhexlify + + +def bigint_to_int(mtime): + """Convert bytearray to int + """ + if isinstance(mtime, bytes): + return int.from_bytes(mtime, 'little', signed=True) + return mtime + + +def int_to_bigint(value): + """Convert integers larger than 64 bits to bytearray + + Smaller integers are left alone + """ + if value.bit_length() > 63: + return value.to_bytes((value.bit_length() + 9) // 8, 'little', signed=True) + return value diff --git a/attic/testsuite/archiver.py b/attic/testsuite/archiver.py index 696d61eb6..ee664c46a 100644 --- a/attic/testsuite/archiver.py +++ b/attic/testsuite/archiver.py @@ -120,6 +120,8 @@ class ArchiverTestCase(ArchiverTestCaseBase): """ # File self.create_regular_file('empty', size=0) + # 2600-01-01 > 2**64 ns + os.utime('input/empty', (19880895600, 19880895600)) self.create_regular_file('file1', size=1024 * 80) self.create_regular_file('flagfile', size=1024) # Directory diff --git a/attic/testsuite/helpers.py b/attic/testsuite/helpers.py index cd6c0abe9..3f72b4864 100644 --- a/attic/testsuite/helpers.py +++ b/attic/testsuite/helpers.py @@ -5,11 +5,23 @@ import os import tempfile import unittest from attic.helpers import adjust_patterns, exclude_path, Location, format_timedelta, IncludePattern, ExcludePattern, make_path_safe, UpgradableLock, prune_within, prune_split, to_localtime, \ - StableDict + StableDict, int_to_bigint, bigint_to_int from attic.testsuite import AtticTestCase import msgpack +class BigIntTestCase(AtticTestCase): + + def test_bigint(self): + self.assert_equal(int_to_bigint(0), 0) + self.assert_equal(int_to_bigint(2**63-1), 2**63-1) + self.assert_equal(int_to_bigint(-2**63+1), -2**63+1) + self.assert_equal(int_to_bigint(2**63), b'\x00\x00\x00\x00\x00\x00\x00\x80\x00') + self.assert_equal(int_to_bigint(-2**63), b'\x00\x00\x00\x00\x00\x00\x00\x80\xff') + self.assert_equal(bigint_to_int(int_to_bigint(-2**70)), -2**70) + self.assert_equal(bigint_to_int(int_to_bigint(2**70)), 2**70) + + class LocationTestCase(AtticTestCase): def test(self):