From 5eb338ff59a3b23ac32cfcdd60624dbc952d3f09 Mon Sep 17 00:00:00 2001 From: TobiasRoeddiger Date: Sun, 27 Sep 2026 12:29:36 +0200 Subject: [PATCH] Handle development firmware labels in update comparisons --- .../repository/firmware_image_repository.dart | 31 +++++++++---- test/firmware_version_comparison_test.dart | 46 +++++++++++++++++++ 2 files changed, 68 insertions(+), 9 deletions(-) create mode 100644 test/firmware_version_comparison_test.dart diff --git a/lib/src/fota/repository/firmware_image_repository.dart b/lib/src/fota/repository/firmware_image_repository.dart index 5bf6de56..a4f31d66 100644 --- a/lib/src/fota/repository/firmware_image_repository.dart +++ b/lib/src/fota/repository/firmware_image_repository.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:pub_semver/pub_semver.dart'; import '../model/firmware_update_request.dart'; @@ -87,17 +88,29 @@ class FirmwareImageRepository { return (latestRelease['tag_name'] as String).replaceFirst('v', ''); } - /// Compares semantic version strings and returns `true` when [latest] is - /// newer than [current]. + /// Returns whether [latest] is newer, ignoring build metadata. + /// Unrecognized version labels do not trigger an update recommendation. bool isNewerVersion(String latest, String current) { - List parse(String v) => v.split('.').map(int.parse).toList(); - final latestParts = parse(latest); - final currentParts = parse(current); + Version parse(String label) { + // Older firmware included the C string terminator in its GATT value. + final normalized = label + .trim() + .replaceFirst(RegExp(r'\x00+$'), '') + .trim() + .replaceFirst(RegExp(r'^[vV]'), ''); + final version = Version.parse(normalized); + return Version( + version.major, + version.minor, + version.patch, + pre: version.preRelease.join('.'), + ); + } - for (int i = 0; i < latestParts.length; i++) { - if (latestParts[i] > currentParts[i]) return true; - if (latestParts[i] < currentParts[i]) return false; + try { + return parse(latest) > parse(current); + } on FormatException { + return false; } - return false; } } diff --git a/test/firmware_version_comparison_test.dart b/test/firmware_version_comparison_test.dart new file mode 100644 index 00000000..4a27082d --- /dev/null +++ b/test/firmware_version_comparison_test.dart @@ -0,0 +1,46 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:open_earable_flutter/src/fota/repository/firmware_image_repository.dart'; + +void main() { + final repository = FirmwareImageRepository(); + + test('compares stable release components numerically', () { + expect(repository.isNewerVersion('2.2.10', '2.2.9'), isTrue); + expect(repository.isNewerVersion('2.3.0', '2.2.10'), isTrue); + expect(repository.isNewerVersion('2.2.9', '2.2.9'), isFalse); + expect(repository.isNewerVersion('2.2.8', '2.2.9'), isFalse); + }); + + test('accepts the development label observed on the earable', () { + const current = '2.2.9-dev.100+g94986934.dirty'; + expect(repository.isNewerVersion('2.2.8', current), isFalse); + expect(repository.isNewerVersion('2.2.9', current), isTrue); + expect(repository.isNewerVersion('2.2.10', current), isTrue); + }); + + test('orders prereleases numerically and below the stable release', () { + expect(repository.isNewerVersion('2.2.9-dev.10', '2.2.9-dev.9'), isTrue); + expect(repository.isNewerVersion('2.2.9-pr292', '2.2.9'), isFalse); + expect(repository.isNewerVersion('2.2.9', '2.2.9-pr292'), isTrue); + }); + + test('ignores hashes and other build metadata', () { + expect(repository.isNewerVersion('2.2.9+zzz', '2.2.9+aaa'), isFalse); + expect( + repository.isNewerVersion('2.2.9-dev.1+gabc', '2.2.9-dev.1+gdef.dirty'), + isFalse, + ); + }); + + test('accepts release tag prefixes and legacy trailing NUL bytes', () { + expect(repository.isNewerVersion('v2.2.9', '2.2.8\u0000'), isTrue); + expect(repository.isNewerVersion(' V2.2.9 ', '2.2.9\u0000'), isFalse); + }); + + test('malformed or incomplete versions do not throw or suggest updates', () { + for (final label in ['', 'unknown', '2', '2.2.x', 'garbage2.2.9']) { + expect(repository.isNewerVersion('2.2.9', label), isFalse); + expect(repository.isNewerVersion(label, '2.2.9'), isFalse); + } + }); +}