Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions lib/src/fota/repository/firmware_image_repository.dart
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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<int> 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;
}
}
46 changes: 46 additions & 0 deletions test/firmware_version_comparison_test.dart
Original file line number Diff line number Diff line change
@@ -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);
}
});
}
Loading