Prevent duplicate hash channels

This commit is contained in:
Janez T
2026-03-13 18:46:33 +01:00
parent 11f3bf9f97
commit 40072175e3
2 changed files with 48 additions and 31 deletions

View File

@@ -830,6 +830,14 @@ class ConnectionProvider with ChangeNotifier {
return secret.any((byte) => byte != 0); return secret.any((byte) => byte != 0);
} }
@visibleForTesting
static bool isDuplicateChannelName({
required String requestedName,
required String existingName,
}) {
return requestedName.startsWith('#') && existingName == requestedName;
}
bool _channelSlotIsOccupied(Object channel) { bool _channelSlotIsOccupied(Object channel) {
final channelName = (channel as dynamic).name as String?; final channelName = (channel as dynamic).name as String?;
final secret = (channel as dynamic).secret; final secret = (channel as dynamic).secret;
@@ -986,8 +994,9 @@ class ConnectionProvider with ChangeNotifier {
// Determine channel type // Determine channel type
final bool isHashChannel = channelName.startsWith('#'); final bool isHashChannel = channelName.startsWith('#');
// Check for duplicate channels // Match meshcore-open behavior:
int? existingSlot; // - deterministic hash channels (#name) cannot be duplicated
// - private channels always use the next empty slot, even if the name matches
if (getChannelInfo != null) { if (getChannelInfo != null) {
final maxChannels = _deviceInfo.maxChannels ?? 40; final maxChannels = _deviceInfo.maxChannels ?? 40;
for (int i = 1; i < maxChannels; i++) { for (int i = 1; i < maxChannels; i++) {
@@ -995,8 +1004,12 @@ class ConnectionProvider with ChangeNotifier {
if (channel != null) { if (channel != null) {
final existingName = (channel as dynamic).name as String?; final existingName = (channel as dynamic).name as String?;
if (existingName != null && existingName.isNotEmpty) { if (existingName != null && existingName.isNotEmpty) {
// For hash channels (#name), check exact match to prevent duplicates if (
if (isHashChannel && existingName == channelName) { isDuplicateChannelName(
requestedName: channelName,
existingName: existingName,
)
) {
debugPrint( debugPrint(
' ⚠️ Hash channel "$channelName" already exists in slot $i', ' ⚠️ Hash channel "$channelName" already exists in slot $i',
); );
@@ -1004,38 +1017,22 @@ class ConnectionProvider with ChangeNotifier {
'Channel "$channelName" already exists. Hash channels cannot be duplicated.', 'Channel "$channelName" already exists. Hash channels cannot be duplicated.',
); );
} }
// For private channels, check name match to allow overwrite
else if (!isHashChannel && existingName == channelName) {
debugPrint(
' Private channel "$channelName" found in slot $i - will overwrite',
);
existingSlot = i;
break;
}
} }
} }
} }
} }
// Determine slot to use // Find next empty slot for any new channel.
final int slotIdx; final maxChannels = _deviceInfo.maxChannels ?? 40;
if (existingSlot != null) { final maxCustomChannels = maxChannels > 0 ? maxChannels - 1 : 0;
// Overwrite existing private channel final emptySlot = await findNextEmptyChannelSlot();
slotIdx = existingSlot; if (emptySlot == null) {
debugPrint(' Using existing slot: $slotIdx (overwrite mode)'); throw Exception(
} else { 'All channel slots are in use (maximum $maxCustomChannels custom channels)',
// Find next empty slot for new channel );
final maxChannels = _deviceInfo.maxChannels ?? 40;
final maxCustomChannels = maxChannels > 0 ? maxChannels - 1 : 0;
final emptySlot = await findNextEmptyChannelSlot();
if (emptySlot == null) {
throw Exception(
'All channel slots are in use (maximum $maxCustomChannels custom channels)',
);
}
slotIdx = emptySlot;
debugPrint(' Using empty slot: $slotIdx (new channel)');
} }
final slotIdx = emptySlot;
debugPrint(' Using empty slot: $slotIdx (new channel)');
// Generate secret // Generate secret
final List<int> secretBytes; final List<int> secretBytes;
@@ -1076,7 +1073,7 @@ class ConnectionProvider with ChangeNotifier {
} }
debugPrint( debugPrint(
'✅ [Provider] Channel ${existingSlot != null ? 'updated' : 'created'} successfully in slot $slotIdx', '✅ [Provider] Channel created successfully in slot $slotIdx',
); );
} catch (e) { } catch (e) {
_error = 'Failed to create channel: $e'; _error = 'Failed to create channel: $e';

View File

@@ -17,5 +17,25 @@ void main() {
isTrue, isTrue,
); );
}); });
test('treats duplicate hashtag channels as conflicts', () {
expect(
ConnectionProvider.isDuplicateChannelName(
requestedName: '#sar',
existingName: '#sar',
),
isTrue,
);
});
test('allows private channels with the same name to coexist', () {
expect(
ConnectionProvider.isDuplicateChannelName(
requestedName: 'Ops',
existingName: 'Ops',
),
isFalse,
);
});
}); });
} }