diff --git a/dissect/target/helpers/utils.py b/dissect/target/helpers/utils.py index fdbee9d2ec..d3628e2c74 100644 --- a/dissect/target/helpers/utils.py +++ b/dissect/target/helpers/utils.py @@ -149,7 +149,14 @@ def year_rollover_helper( # We have to append the current_year to strptime instead of adding it using replace later. # This prevents DeprecationWarnings on cpython >= 3.13 and Exceptions on cpython >= 3.15. # See https://github.com/python/cpython/issues/70647 and https://github.com/python/cpython/pull/117107. - compare_ts = datetime.strptime(f"{timestamp.group(0)};1900", f"{ts_format};%Y") + # Use 1904 instead of 1900 to include leap days (29 Feb). + try: + compare_ts = datetime.strptime(f"{timestamp.group(0)};1904", f"{ts_format};%Y") + except ValueError as e: + log.warning("Unable to create comparison timestamp for %r in line %r: %s", timestamp.group(0), line, e) + log.debug("", exc_info=e) + continue + if last_seen_month and compare_ts.month > last_seen_month: current_year -= 1 last_seen_month = compare_ts.month @@ -157,7 +164,14 @@ def year_rollover_helper( try: relative_ts = datetime.strptime(f"{timestamp.group(0)};{current_year}", f"{ts_format};%Y") except ValueError as e: - log.warning("Timestamp '%s' does not match format '%s', skipping line.", timestamp.group(0), ts_format) + log.warning( + "Timestamp '%s;%s' does not match format '%s;%%Y', skipping line %r: %s", + timestamp.group(0), + current_year, + ts_format, + line, + e, + ) log.debug("", exc_info=e) continue diff --git a/tests/helpers/test_utils.py b/tests/helpers/test_utils.py index 65ff1e3439..279bdbefbb 100644 --- a/tests/helpers/test_utils.py +++ b/tests/helpers/test_utils.py @@ -58,6 +58,8 @@ def test_helpers_fsutil_year_rollover_helper() -> None: mocked_stat = fsutil.stat_result([stat.S_IFREG, 1337, id(vfs), 0, 0, 0, len(content), 0, 3384460800, 0]) with patch.object(path, "stat", return_value=mocked_stat): result = list(utils.year_rollover_helper(path, re_ts, ts_fmt)) + assert len(result) == 10 + year_line = [(ts.year, line) for ts, line in result] assert result[0][0].tzinfo == datetime.timezone.utc @@ -78,3 +80,26 @@ def test_helpers_fsutil_year_rollover_helper() -> None: assert year_line[0] == (2022, "Jan 1 13:21:34 Line 10") assert year_line[-1] == (2018, "Dec 31 03:14:15 Line 1") + + +def test_helpers_fsutil_year_rollover_helper_leap_day() -> None: + """test if we correctly handle leap days such as 2024-02-29.""" + + content = """ + Feb 28 11:00:00 Line 1 + Feb 29 12:00:00 Line 2 + Mar 1 13:00:00 Line 3 + """ + fs = VirtualFilesystem() + fs.map_file_fh("file", io.BytesIO(textwrap.dedent(content).encode())) + path = fs.path("file") + + re_ts = r"(\w+\s{1,2}\d+\s\d{2}:\d{2}:\d{2})" + ts_fmt = "%b %d %H:%M:%S" + mocked_stat = fsutil.stat_result( + [stat.S_IFREG, 1337, id(fs), 0, 0, 0, len(content), 0, 1709294400, 0] + ) # mtime is set to 2024-03-01. + + with patch.object(path, "stat", return_value=mocked_stat): + result = list(utils.year_rollover_helper(path, re_ts, ts_fmt)) + assert len(result) == 3