mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-08-29 07:27:16 +00:00
Replace try! regex construction with a non-trapping SafeRegex helper (#1501)
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).
This commit is contained in:
parent
81837d7202
commit
4ef5558d7b
@ -104,12 +104,10 @@ final class LRUDeduplicationCache<Value> {
|
||||
enum ContentNormalizer {
|
||||
|
||||
/// Regex to simplify HTTP URLs by stripping query strings and fragments
|
||||
private static let simplifyHTTPURL: NSRegularExpression = {
|
||||
try! NSRegularExpression(
|
||||
pattern: "https?://[^\\s?#]+(?:[?#][^\\s]*)?",
|
||||
options: [.caseInsensitive]
|
||||
)
|
||||
}()
|
||||
private static let simplifyHTTPURL = SafeRegex.compile(
|
||||
"https?://[^\\s?#]+(?:[?#][^\\s]*)?",
|
||||
options: [.caseInsensitive]
|
||||
)
|
||||
|
||||
/// Normalizes content for deduplication comparison.
|
||||
/// - Parameters:
|
||||
|
||||
@ -39,37 +39,23 @@ final class MessageFormattingEngine {
|
||||
|
||||
/// Precompiled regex patterns for message content parsing
|
||||
enum Patterns {
|
||||
static let hashtag: NSRegularExpression = {
|
||||
try! NSRegularExpression(pattern: "#([a-zA-Z0-9_]+)", options: [])
|
||||
}()
|
||||
static let hashtag = SafeRegex.compile("#([a-zA-Z0-9_]+)")
|
||||
|
||||
static let mention: NSRegularExpression = {
|
||||
try! NSRegularExpression(pattern: "@([\\p{L}0-9_]+(?:#[a-fA-F0-9]{4})?)", options: [])
|
||||
}()
|
||||
static let mention = SafeRegex.compile("@([\\p{L}0-9_]+(?:#[a-fA-F0-9]{4})?)")
|
||||
|
||||
static let cashu: NSRegularExpression = {
|
||||
try! NSRegularExpression(pattern: "\\bcashu[AB][A-Za-z0-9._-]{40,}\\b", options: [])
|
||||
}()
|
||||
static let cashu = SafeRegex.compile("\\bcashu[AB][A-Za-z0-9._-]{40,}\\b")
|
||||
|
||||
static let bolt11: NSRegularExpression = {
|
||||
try! NSRegularExpression(pattern: "(?i)\\bln(bc|tb|bcrt)[0-9][a-z0-9]{50,}\\b", options: [])
|
||||
}()
|
||||
static let bolt11 = SafeRegex.compile("(?i)\\bln(bc|tb|bcrt)[0-9][a-z0-9]{50,}\\b")
|
||||
|
||||
static let lnurl: NSRegularExpression = {
|
||||
try! NSRegularExpression(pattern: "(?i)\\blnurl1[a-z0-9]{20,}\\b", options: [])
|
||||
}()
|
||||
static let lnurl = SafeRegex.compile("(?i)\\blnurl1[a-z0-9]{20,}\\b")
|
||||
|
||||
static let lightningScheme: NSRegularExpression = {
|
||||
try! NSRegularExpression(pattern: "(?i)\\blightning:[^\\s]+", options: [])
|
||||
}()
|
||||
static let lightningScheme = SafeRegex.compile("(?i)\\blightning:[^\\s]+")
|
||||
|
||||
static let linkDetector: NSDataDetector? = {
|
||||
try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
|
||||
}()
|
||||
|
||||
static let quickCashuPresence: NSRegularExpression = {
|
||||
try! NSRegularExpression(pattern: "\\bcashu[AB][A-Za-z0-9._-]{40,}\\b", options: [])
|
||||
}()
|
||||
static let quickCashuPresence = SafeRegex.compile("\\bcashu[AB][A-Za-z0-9._-]{40,}\\b")
|
||||
}
|
||||
|
||||
// MARK: - Match Types
|
||||
|
||||
36
bitchat/Utils/SafeRegex.swift
Normal file
36
bitchat/Utils/SafeRegex.swift
Normal file
@ -0,0 +1,36 @@
|
||||
//
|
||||
// SafeRegex.swift
|
||||
// bitchat
|
||||
//
|
||||
// Non-trapping construction for the app's compiled-in regex patterns.
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import BitLogger
|
||||
import Foundation
|
||||
|
||||
enum SafeRegex {
|
||||
/// Compiles a bundled pattern. On failure it logs and returns a regex
|
||||
/// that can never match, so a bad pattern degrades that one feature
|
||||
/// instead of crashing at startup.
|
||||
static func compile(_ pattern: String, options: NSRegularExpression.Options = []) -> NSRegularExpression {
|
||||
do {
|
||||
return try NSRegularExpression(pattern: pattern, options: options)
|
||||
} catch {
|
||||
SecureLogger.error("Regex pattern failed to compile, matching disabled: \(pattern) (\(error))", category: .session)
|
||||
return neverMatching
|
||||
}
|
||||
}
|
||||
|
||||
/// `(?!)` — an empty negative lookahead — always compiles and can never match.
|
||||
private static let neverMatching: NSRegularExpression = {
|
||||
if let regex = try? NSRegularExpression(pattern: "(?!)", options: []) {
|
||||
return regex
|
||||
}
|
||||
// Unreachable: "(?!)" is a valid ICU pattern. The inherited plain
|
||||
// initializer (empty pattern) is the least-bad non-trapping fallback
|
||||
// if ICU itself were ever broken.
|
||||
return NSRegularExpression()
|
||||
}()
|
||||
}
|
||||
64
bitchatTests/Services/SafeRegexTests.swift
Normal file
64
bitchatTests/Services/SafeRegexTests.swift
Normal file
@ -0,0 +1,64 @@
|
||||
//
|
||||
// SafeRegexTests.swift
|
||||
// bitchatTests
|
||||
//
|
||||
// SafeRegex must never trap: valid patterns compile normally, invalid ones
|
||||
// degrade to a regex that matches nothing. The production-pattern test keeps
|
||||
// the compile-time guarantee try! used to provide - a typo in any bundled
|
||||
// pattern fails here instead of crashing the app at startup.
|
||||
// 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 SafeRegexTests {
|
||||
|
||||
private func matchCount(_ regex: NSRegularExpression, _ text: String) -> Int {
|
||||
regex.numberOfMatches(in: text, options: [], range: NSRange(text.startIndex..., in: text))
|
||||
}
|
||||
|
||||
@Test
|
||||
func validPatternCompilesAndMatches() {
|
||||
let regex = SafeRegex.compile("#([a-zA-Z0-9_]+)")
|
||||
#expect(matchCount(regex, "tag #bitchat here") == 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
func invalidPatternDegradesToNeverMatching() {
|
||||
let regex = SafeRegex.compile("(unclosed")
|
||||
#expect(matchCount(regex, "(unclosed anything") == 0)
|
||||
#expect(matchCount(regex, "") == 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
func productionPatternsCompileAndMatchTheirTargets() {
|
||||
// A pattern that failed to compile would have degraded to
|
||||
// never-matching, so each positive match proves the literal compiled.
|
||||
#expect(matchCount(MessageFormattingEngine.Patterns.hashtag, "see #mesh") == 1)
|
||||
#expect(matchCount(MessageFormattingEngine.Patterns.mention, "hi @alice#ab12") == 1)
|
||||
|
||||
let cashuToken = "cashuA" + String(repeating: "x", count: 45)
|
||||
#expect(matchCount(MessageFormattingEngine.Patterns.cashu, cashuToken) == 1)
|
||||
#expect(matchCount(MessageFormattingEngine.Patterns.quickCashuPresence, cashuToken) == 1)
|
||||
|
||||
let bolt11 = "lnbc1" + String(repeating: "q", count: 55)
|
||||
#expect(matchCount(MessageFormattingEngine.Patterns.bolt11, bolt11) == 1)
|
||||
|
||||
let lnurl = "lnurl1" + String(repeating: "q", count: 25)
|
||||
#expect(matchCount(MessageFormattingEngine.Patterns.lnurl, lnurl) == 1)
|
||||
|
||||
#expect(matchCount(MessageFormattingEngine.Patterns.lightningScheme, "pay lightning:abc123") == 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
func contentNormalizerStillSimplifiesURLs() {
|
||||
// Exercises ContentNormalizer's regex through its public entry point:
|
||||
// same URL with different query strings must normalize identically.
|
||||
let a = ContentNormalizer.normalizedKey("check https://example.com/page?q=1")
|
||||
let b = ContentNormalizer.normalizedKey("check https://example.com/page?q=2")
|
||||
#expect(a == b)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user