Harden text formatting against Unicode range mismatches

- Use NSString.length for all NSRange regex scans
- Guard string slicing when lastEnd > match.lowerBound
- Advance lastEnd safely to avoid backtracking overlaps
- Apply to formatMessageContent, formatMessage, formatMessageAsText, parseMentions
This commit is contained in:
jack
2025-08-28 09:22:34 +01:00
parent 11cd5527ed
commit 3e5043f875
+28 -18
View File
@@ -3043,8 +3043,10 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
let mentionRegex = try? NSRegularExpression(pattern: mentionPattern, options: []) let mentionRegex = try? NSRegularExpression(pattern: mentionPattern, options: [])
let hashtagRegex = try? NSRegularExpression(pattern: hashtagPattern, options: []) let hashtagRegex = try? NSRegularExpression(pattern: hashtagPattern, options: [])
let mentionMatches = mentionRegex?.matches(in: contentText, options: [], range: NSRange(location: 0, length: contentText.count)) ?? [] let nsContent = contentText as NSString
let hashtagMatches = hashtagRegex?.matches(in: contentText, options: [], range: NSRange(location: 0, length: contentText.count)) ?? [] let nsLen = nsContent.length
let mentionMatches = mentionRegex?.matches(in: contentText, options: [], range: NSRange(location: 0, length: nsLen)) ?? []
let hashtagMatches = hashtagRegex?.matches(in: contentText, options: [], range: NSRange(location: 0, length: nsLen)) ?? []
// Combine and sort all matches // Combine and sort all matches
var allMatches: [(range: NSRange, type: String)] = [] var allMatches: [(range: NSRange, type: String)] = []
@@ -3061,12 +3063,14 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
for (matchRange, matchType) in allMatches { for (matchRange, matchType) in allMatches {
// Add text before the match // Add text before the match
if let range = Range(matchRange, in: contentText) { if let range = Range(matchRange, in: contentText) {
let beforeText = String(contentText[lastEndIndex..<range.lowerBound]) if lastEndIndex < range.lowerBound {
if !beforeText.isEmpty { let beforeText = String(contentText[lastEndIndex..<range.lowerBound])
var normalStyle = AttributeContainer() if !beforeText.isEmpty {
normalStyle.font = .system(size: 14, design: .monospaced) var normalStyle = AttributeContainer()
normalStyle.foregroundColor = isDark ? Color.white : Color.black normalStyle.font = .system(size: 14, design: .monospaced)
processedContent.append(AttributedString(beforeText).mergingAttributes(normalStyle)) normalStyle.foregroundColor = isDark ? Color.white : Color.black
processedContent.append(AttributedString(beforeText).mergingAttributes(normalStyle))
}
} }
// Add the match with appropriate styling // Add the match with appropriate styling
@@ -3084,7 +3088,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
processedContent.append(AttributedString(matchText).mergingAttributes(matchStyle)) processedContent.append(AttributedString(matchText).mergingAttributes(matchStyle))
lastEndIndex = range.upperBound if lastEndIndex < range.upperBound { lastEndIndex = range.upperBound }
} }
} }
@@ -3484,19 +3488,23 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
// Regular expression to find @mentions // Regular expression to find @mentions
let pattern = "@([\\p{L}0-9_]+)" let pattern = "@([\\p{L}0-9_]+)"
let regex = try? NSRegularExpression(pattern: pattern, options: []) let regex = try? NSRegularExpression(pattern: pattern, options: [])
let matches = regex?.matches(in: contentText, options: [], range: NSRange(location: 0, length: contentText.count)) ?? [] let nsContent = contentText as NSString
let nsLen = nsContent.length
let matches = regex?.matches(in: contentText, options: [], range: NSRange(location: 0, length: nsLen)) ?? []
var lastEndIndex = contentText.startIndex var lastEndIndex = contentText.startIndex
for match in matches { for match in matches {
// Add text before the mention // Add text before the mention
if let range = Range(match.range(at: 0), in: contentText) { if let range = Range(match.range(at: 0), in: contentText) {
let beforeText = String(contentText[lastEndIndex..<range.lowerBound]) if lastEndIndex < range.lowerBound {
if !beforeText.isEmpty { let beforeText = String(contentText[lastEndIndex..<range.lowerBound])
var normalStyle = AttributeContainer() if !beforeText.isEmpty {
normalStyle.font = .system(size: 14, design: .monospaced) var normalStyle = AttributeContainer()
normalStyle.foregroundColor = isDark ? Color.white : Color.black normalStyle.font = .system(size: 14, design: .monospaced)
processedContent.append(AttributedString(beforeText).mergingAttributes(normalStyle)) normalStyle.foregroundColor = isDark ? Color.white : Color.black
processedContent.append(AttributedString(beforeText).mergingAttributes(normalStyle))
}
} }
// Add the mention with highlight // Add the mention with highlight
@@ -3506,7 +3514,7 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
mentionStyle.foregroundColor = Color.orange mentionStyle.foregroundColor = Color.orange
processedContent.append(AttributedString(mentionText).mergingAttributes(mentionStyle)) processedContent.append(AttributedString(mentionText).mergingAttributes(mentionStyle))
lastEndIndex = range.upperBound if lastEndIndex < range.upperBound { lastEndIndex = range.upperBound }
} }
} }
@@ -4704,7 +4712,9 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
// Allow optional disambiguation suffix '#abcd' for duplicate nicknames // Allow optional disambiguation suffix '#abcd' for duplicate nicknames
let pattern = "@([\\p{L}0-9_]+(?:#[a-fA-F0-9]{4})?)" let pattern = "@([\\p{L}0-9_]+(?:#[a-fA-F0-9]{4})?)"
let regex = try? NSRegularExpression(pattern: pattern, options: []) let regex = try? NSRegularExpression(pattern: pattern, options: [])
let matches = regex?.matches(in: content, options: [], range: NSRange(location: 0, length: content.count)) ?? [] let nsContent = content as NSString
let nsLen = nsContent.length
let matches = regex?.matches(in: content, options: [], range: NSRange(location: 0, length: nsLen)) ?? []
var mentions: [String] = [] var mentions: [String] = []
let peerNicknames = meshService.getPeerNicknames() let peerNicknames = meshService.getPeerNicknames()