From ec2f94cae866add2c3cd5ab78a050b2bc4ad62dd Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Wed, 3 Sep 2025 00:50:49 +0530 Subject: [PATCH] fix: handle datetime outside the valid range supported by dart (#21526) * fix: handle datetime outside the valid range supported by dart * add tests for tryFromSecondsSinceEpoch --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../domain/services/local_sync.service.dart | 7 ++- mobile/lib/utils/datetime_helpers.dart | 19 ++++++ .../modules/utils/datetime_helpers_test.dart | 58 +++++++++++++++++++ 3 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 mobile/lib/utils/datetime_helpers.dart create mode 100644 mobile/test/modules/utils/datetime_helpers_test.dart diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart index 119954cb47..b136b11bab 100644 --- a/mobile/lib/domain/services/local_sync.service.dart +++ b/mobile/lib/domain/services/local_sync.service.dart @@ -6,6 +6,7 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; +import 'package:immich_mobile/utils/datetime_helpers.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:logging/logging.dart'; import 'package:platform/platform.dart'; @@ -285,7 +286,7 @@ extension on Iterable { (e) => LocalAlbum( id: e.id, name: e.name, - updatedAt: e.updatedAt == null ? DateTime.now() : DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000), + updatedAt: tryFromSecondsSinceEpoch(e.updatedAt!) ?? DateTime.now(), assetCount: e.assetCount, ), ).toList(); @@ -300,8 +301,8 @@ extension on Iterable { name: e.name, checksum: null, type: AssetType.values.elementAtOrNull(e.type) ?? AssetType.other, - createdAt: e.createdAt == null ? DateTime.now() : DateTime.fromMillisecondsSinceEpoch(e.createdAt! * 1000), - updatedAt: e.updatedAt == null ? DateTime.now() : DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000), + createdAt: tryFromSecondsSinceEpoch(e.createdAt!) ?? DateTime.now(), + updatedAt: tryFromSecondsSinceEpoch(e.updatedAt!) ?? DateTime.now(), width: e.width, height: e.height, durationInSeconds: e.durationInSeconds, diff --git a/mobile/lib/utils/datetime_helpers.dart b/mobile/lib/utils/datetime_helpers.dart new file mode 100644 index 0000000000..829f71c37e --- /dev/null +++ b/mobile/lib/utils/datetime_helpers.dart @@ -0,0 +1,19 @@ +const int _maxMillisecondsSinceEpoch = 8640000000000000; // 275760-09-13 +const int _minMillisecondsSinceEpoch = -62135596800000; // 0001-01-01 + +DateTime? tryFromSecondsSinceEpoch(int? secondsSinceEpoch) { + if (secondsSinceEpoch == null) { + return null; + } + + final milliSeconds = secondsSinceEpoch * 1000; + if (milliSeconds < _minMillisecondsSinceEpoch || milliSeconds > _maxMillisecondsSinceEpoch) { + return null; + } + + try { + return DateTime.fromMillisecondsSinceEpoch(milliSeconds); + } catch (e) { + return null; + } +} diff --git a/mobile/test/modules/utils/datetime_helpers_test.dart b/mobile/test/modules/utils/datetime_helpers_test.dart new file mode 100644 index 0000000000..dfe83b4925 --- /dev/null +++ b/mobile/test/modules/utils/datetime_helpers_test.dart @@ -0,0 +1,58 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/utils/datetime_helpers.dart'; + +void main() { + group('tryFromSecondsSinceEpoch', () { + test('returns null for null input', () { + final result = tryFromSecondsSinceEpoch(null); + expect(result, isNull); + }); + + test('returns null for value below minimum allowed range', () { + // _minMillisecondsSinceEpoch = -62135596800000 + final seconds = -62135596800000 ~/ 1000 - 1; // One second before min allowed + final result = tryFromSecondsSinceEpoch(seconds); + expect(result, isNull); + }); + + test('returns null for value above maximum allowed range', () { + // _maxMillisecondsSinceEpoch = 8640000000000000 + final seconds = 8640000000000000 ~/ 1000 + 1; // One second after max allowed + final result = tryFromSecondsSinceEpoch(seconds); + expect(result, isNull); + }); + + test('returns correct DateTime for minimum allowed value', () { + final seconds = -62135596800000 ~/ 1000; // Minimum allowed timestamp + final result = tryFromSecondsSinceEpoch(seconds); + expect(result, DateTime.fromMillisecondsSinceEpoch(-62135596800000)); + }); + + test('returns correct DateTime for maximum allowed value', () { + final seconds = 8640000000000000 ~/ 1000; // Maximum allowed timestamp + final result = tryFromSecondsSinceEpoch(seconds); + expect(result, DateTime.fromMillisecondsSinceEpoch(8640000000000000)); + }); + + test('returns correct DateTime for negative timestamp', () { + final seconds = -1577836800; // Dec 31, 1919 (pre-epoch) + final result = tryFromSecondsSinceEpoch(seconds); + expect(result, DateTime.fromMillisecondsSinceEpoch(-1577836800 * 1000)); + }); + + test('returns correct DateTime for zero timestamp', () { + final seconds = 0; // Jan 1, 1970 (epoch) + final result = tryFromSecondsSinceEpoch(seconds); + expect(result, DateTime.fromMillisecondsSinceEpoch(0)); + }); + + test('returns correct DateTime for recent timestamp', () { + final now = DateTime.now(); + final seconds = now.millisecondsSinceEpoch ~/ 1000; + final result = tryFromSecondsSinceEpoch(seconds); + expect(result?.year, now.year); + expect(result?.month, now.month); + expect(result?.day, now.day); + }); + }); +}