From a0719bc2bf5eacf8b498198914982fa302ba5605 Mon Sep 17 00:00:00 2001 From: KnugiHK <24708955+KnugiHK@users.noreply.github.com> Date: Sat, 24 Jan 2026 23:05:00 +0800 Subject: [PATCH 1/6] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bace80a..1fa93a3 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ Do an iPhone/iPad Backup with iTunes/Finder first. > [!NOTE] > If you are working on unencrypted iOS/iPadOS backup, skip this. -If you want to work on an encrypted iOS/iPadOS Backup, you should install iphone_backup_decrypt from [KnugiHK/iphone_backup_decrypt](https://github.com/KnugiHK/iphone_backup_decrypt) before you run the extract_iphone_media.py. +If you want to work on an encrypted iOS/iPadOS Backup, you should install `iphone_backup_decrypt` from [KnugiHK/iphone_backup_decrypt](https://github.com/KnugiHK/iphone_backup_decrypt) before you run the extract_iphone_media.py. ```sh pip install git+https://github.com/KnugiHK/iphone_backup_decrypt ``` From a2bcc39e6360804986411aef720ec3a638b92b3d Mon Sep 17 00:00:00 2001 From: Tang Vu Date: Thu, 26 Mar 2026 03:25:44 +0700 Subject: [PATCH 2/6] refactor: crash in timestamp formatting when timezone_offset is none In `Timing.format_timestamp`, if `self.timezone_offset` is `None` (which is explicitly allowed by the `Optional[int]` type hint), it instantiates `TimeZone(None)`. When `datetime.fromtimestamp()` calls the `utcoffset()` method on this timezone object, it executes `timedelta(hours=self.offset)`, which evaluates to `timedelta(hours=None)`. This raises a `TypeError: unsupported type for timedelta hours component: NoneType`, causing the application to crash during export. Affected files: data_model.py Signed-off-by: Tang Vu --- Whatsapp_Chat_Exporter/data_model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Whatsapp_Chat_Exporter/data_model.py b/Whatsapp_Chat_Exporter/data_model.py index 52f9bae..d8255ce 100644 --- a/Whatsapp_Chat_Exporter/data_model.py +++ b/Whatsapp_Chat_Exporter/data_model.py @@ -30,7 +30,8 @@ class Timing: """ if timestamp is not None: timestamp = timestamp / 1000 if timestamp > 9999999999 else timestamp - return datetime.fromtimestamp(timestamp, TimeZone(self.timezone_offset)).strftime(format) + tz = TimeZone(self.timezone_offset) if self.timezone_offset is not None else None + return datetime.fromtimestamp(timestamp, tz).strftime(format) return None From bb860533d52c5ef9d720d51ba1d91a8f93664b20 Mon Sep 17 00:00:00 2001 From: KnugiHK <24708955+KnugiHK@users.noreply.github.com> Date: Thu, 26 Mar 2026 21:23:59 +0800 Subject: [PATCH 3/6] Add a default value for `timezone_offset` in `Timing.__init__` --- Whatsapp_Chat_Exporter/data_model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Whatsapp_Chat_Exporter/data_model.py b/Whatsapp_Chat_Exporter/data_model.py index d8255ce..af58dce 100644 --- a/Whatsapp_Chat_Exporter/data_model.py +++ b/Whatsapp_Chat_Exporter/data_model.py @@ -8,12 +8,12 @@ class Timing: Handles timestamp formatting with timezone support. """ - def __init__(self, timezone_offset: Optional[int]) -> None: + def __init__(self, timezone_offset: Optional[int] = None) -> None: """ Initialize Timing object. Args: - timezone_offset (Optional[int]): Hours offset from UTC + timezone_offset (Optional[int]): Hours offset from UTC. Defaults to None (auto-detect). """ self.timezone_offset = timezone_offset From 18a0d822b34b8e8053b7f800a397cddc9048ced6 Mon Sep 17 00:00:00 2001 From: KnugiHK <24708955+KnugiHK@users.noreply.github.com> Date: Thu, 26 Mar 2026 21:30:10 +0800 Subject: [PATCH 4/6] Timezone offset should also accepts float --- Whatsapp_Chat_Exporter/data_model.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Whatsapp_Chat_Exporter/data_model.py b/Whatsapp_Chat_Exporter/data_model.py index af58dce..506f76c 100644 --- a/Whatsapp_Chat_Exporter/data_model.py +++ b/Whatsapp_Chat_Exporter/data_model.py @@ -8,12 +8,12 @@ class Timing: Handles timestamp formatting with timezone support. """ - def __init__(self, timezone_offset: Optional[int] = None) -> None: + def __init__(self, timezone_offset: Optional[Union[int, float]] = None) -> None: """ Initialize Timing object. Args: - timezone_offset (Optional[int]): Hours offset from UTC. Defaults to None (auto-detect). + timezone_offset (Optional[Union[int, float]]): Hours offset from UTC. Defaults to None (auto-detect). """ self.timezone_offset = timezone_offset @@ -40,12 +40,12 @@ class TimeZone(tzinfo): Custom timezone class with fixed offset. """ - def __init__(self, offset: int) -> None: + def __init__(self, offset: Union[int, float]) -> None: """ Initialize TimeZone object. Args: - offset (int): Hours offset from UTC + offset (Union[int, float]): Hours offset from UTC """ self.offset = offset From 9e138d3a1f6488697560b641446f69631765ff03 Mon Sep 17 00:00:00 2001 From: KnugiHK <24708955+KnugiHK@users.noreply.github.com> Date: Thu, 26 Mar 2026 21:31:52 +0800 Subject: [PATCH 5/6] Cache TimeZone object in Timing class --- Whatsapp_Chat_Exporter/data_model.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Whatsapp_Chat_Exporter/data_model.py b/Whatsapp_Chat_Exporter/data_model.py index 506f76c..4a53762 100644 --- a/Whatsapp_Chat_Exporter/data_model.py +++ b/Whatsapp_Chat_Exporter/data_model.py @@ -15,7 +15,7 @@ class Timing: Args: timezone_offset (Optional[Union[int, float]]): Hours offset from UTC. Defaults to None (auto-detect). """ - self.timezone_offset = timezone_offset + self.tz = TimeZone(timezone_offset) if timezone_offset is not None else None def format_timestamp(self, timestamp: Optional[Union[int, float]], format: str) -> Optional[str]: """ @@ -30,8 +30,7 @@ class Timing: """ if timestamp is not None: timestamp = timestamp / 1000 if timestamp > 9999999999 else timestamp - tz = TimeZone(self.timezone_offset) if self.timezone_offset is not None else None - return datetime.fromtimestamp(timestamp, tz).strftime(format) + return datetime.fromtimestamp(timestamp, self.tz).strftime(format) return None From abf4f3c814c59ba9742d47f85b02de20018c1610 Mon Sep 17 00:00:00 2001 From: KnugiHK <24708955+KnugiHK@users.noreply.github.com> Date: Thu, 26 Mar 2026 21:53:44 +0800 Subject: [PATCH 6/6] Implement tests for classes `TimeZone` and `Timing` --- tests/test_data_model.py | 55 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 tests/test_data_model.py diff --git a/tests/test_data_model.py b/tests/test_data_model.py new file mode 100644 index 0000000..7ad56a4 --- /dev/null +++ b/tests/test_data_model.py @@ -0,0 +1,55 @@ +import pytest +from Whatsapp_Chat_Exporter.data_model import TimeZone, Timing +from datetime import timedelta + + +class TestTimeZone: + def test_utcoffset(self): + tz = TimeZone(5.5) + assert tz.utcoffset(None) == timedelta(hours=5.5) + + def test_dst(self): + tz = TimeZone(2) + assert tz.dst(None) == timedelta(0) + + +class TestTiming: + @pytest.mark.parametrize("offset, expected_hour", [ + (8, "08:00"), # Integer (e.g., Hong Kong Standard Time) + (-8, "16:00"), # Negative Integer (e.g., PST) + (5.5, "05:30"), # Positive Float (e.g., IST) + (-3.5, "20:30"), # Negative Float (e.g., Newfoundland) + ]) + + def test_format_timestamp_various_offsets(self, offset, expected_hour): + """Verify that both int and float offsets calculate time correctly.""" + t = Timing(offset) + result = t.format_timestamp(1672531200, "%H:%M") + assert result == expected_hour + + @pytest.mark.parametrize("ts_input", [ + 1672531200, # Unix timestamp as int + 1672531200.0, # Unix timestamp as float + ]) + + def test_timestamp_input_types(self, ts_input): + """Verify the method accepts both int and float timestamps.""" + t = Timing(0) + result = t.format_timestamp(ts_input, "%Y") + assert result == "2023" + + def test_timing_none_offset(self): + """Verify initialization with None doesn't crash and uses system time.""" + t = Timing(None) + assert t.tz is None + # Should still return a valid string based on local machine time without crashing + result = t.format_timestamp(1672531200, "%Y") + assert result == "2023" + + def test_millisecond_scaling(self): + """Verify that timestamps in milliseconds are correctly scaled down.""" + t = Timing(0) + # Milliseconds as int + assert t.format_timestamp(1672531200000, "%Y") == "2023" + # Milliseconds as float + assert t.format_timestamp(1672531200000.0, "%Y") == "2023"