mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-08-08 06:56:10 +00:00
* Replace try! regex construction with a non-trapping SafeRegex helper MessageFormattingEngine and MessageDeduplicationService compiled eight bundled regex literals with try!, so a bad pattern would crash the app at startup - in the middle of the message-render path (#645). Add SafeRegex.compile: it compiles the pattern normally, and on failure logs through SecureLogger and returns a never-matching regex ('(?!)'), so a broken pattern degrades that one formatting feature instead of trapping. Pattern properties stay non-optional, so no call-site churn across ChatMessageFormatter, MessageTextHelpers, and ChatComposerCoordinator. The compile-time guarantee try! provided moves into tests: each production pattern is asserted to compile and match a known-good sample, so a typo in a pattern now fails CI instead of crashing users. Part of #645 (the remaining try! sites; NoiseSessionManager's force-unwrap is addressed separately in #1456). * Normalize nicknames to Unicode NFC at storage and comparison boundaries A nickname containing an accent can arrive in two canonically equivalent but bytewise different forms: precomposed (U+00E9) or decomposed (e + U+0301), depending on the keyboard and platform that produced it. Nicknames were stored and compared without normalization, so visually identical names silently failed to match: mentions of your own name did not highlight or notify, /msg and /block could not resolve the peer, autocomplete skipped candidates, and geohash DM resolution failed (#214). Fix by canonicalizing to NFC (String.normalizedNickname) at every boundary where a nickname enters storage - own nickname (ChatViewModel didSet, alongside the existing trim), verified announce ingest (BLEPeerRegistry), geohash presence (LocationPresenceStore), and InputValidator.validateNickname - and by normalizing both sides at comparison sites that can still see pre-normalization data (persisted favorites, message-content mentions): peer resolution in UnifiedPeerService and ChatPeerIdentityCoordinator, the three mention checks, and autocomplete prefix matching. The wire codec (AnnouncementPacket) is deliberately untouched: announces are signature-verified against raw bytes, so canonicalization happens at the storage layer, never during parsing. Fixes #214
54 lines
2.0 KiB
Swift
54 lines
2.0 KiB
Swift
//
|
|
// NicknameNormalizationTests.swift
|
|
// bitchatTests
|
|
//
|
|
// Nicknames must compare equal regardless of how the user's keyboard
|
|
// produced them: "café" as precomposed U+00E9 and as "e" + combining
|
|
// U+0301 are canonically equivalent but bytewise different, which broke
|
|
// mention matching, DM resolution, and autocomplete (#214). Storage and
|
|
// comparison both canonicalize to NFC via String.normalizedNickname.
|
|
// This is free and unencumbered software released into the public domain.
|
|
// For more information, see <https://unlicense.org>
|
|
//
|
|
|
|
import Foundation
|
|
import Testing
|
|
@testable import bitchat
|
|
|
|
struct NicknameNormalizationTests {
|
|
/// "café" with a combining acute accent (NFD form)
|
|
private let decomposed = "cafe\u{0301}"
|
|
/// "café" with precomposed é (NFC form)
|
|
private let precomposed = "caf\u{00E9}"
|
|
|
|
@Test
|
|
func canonicallyEquivalentFormsNormalizeIdentically() {
|
|
// Sanity: the raw forms really are different strings byte-wise …
|
|
#expect(decomposed.unicodeScalars.count != precomposed.unicodeScalars.count)
|
|
// … and normalization unifies them.
|
|
#expect(decomposed.normalizedNickname == precomposed.normalizedNickname)
|
|
#expect(decomposed.normalizedNickname == precomposed)
|
|
}
|
|
|
|
@Test
|
|
func asciiNicknamesPassThroughUnchanged() {
|
|
#expect("alice_42".normalizedNickname == "alice_42")
|
|
#expect("".normalizedNickname == "")
|
|
}
|
|
|
|
@Test
|
|
func validateNicknameReturnsCanonicalForm() {
|
|
#expect(InputValidator.validateNickname(decomposed) == precomposed)
|
|
#expect(InputValidator.validateNickname(" \(decomposed) ") == precomposed)
|
|
// Validation behavior is otherwise unchanged.
|
|
#expect(InputValidator.validateNickname(" ") == nil)
|
|
}
|
|
|
|
@Test
|
|
func collisionSuffixSplittingSurvivesNormalization() {
|
|
let (base, suffix) = (decomposed.normalizedNickname + "#ab12").splitSuffix()
|
|
#expect(base == precomposed)
|
|
#expect(suffix == "#ab12")
|
|
}
|
|
}
|