Text Extractor — Swift source
Pull URLs, emails, IPv4/IPv6 addresses, hashes (MD5/SHA-1/SHA-256/SHA-512), and domains out of logs, headers, or any pasted text.
This is the Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.
// extract — pull URLs, emails, IPv4/IPv6 addresses, hashes, and domains
// out of arbitrary text (logs, headers, config).
//
// Language: Swift 5.9+ (Foundation)
// Source: CosmoDev polyglot showcase port of the Extract tool, ported from
// src/lib/extract.ts (the canonical TypeScript implementation) and
// held in lock-step with its Go twin cli/extract/extract.go.
// License: display source — part of CosmoDev's polyglot tool pages.
//
// Design goals:
// - Pure + deterministic; never traps.
// - Functionally equivalent to the TS/Go reference: same inputs -> same outputs.
// - Foundation's NSRegularExpression (ICU) is the engine; every construct
// used (`\b`, `(?:...)`, counted repetition) behaves exactly as it does
// in the TS lib, so the pattern strings are copied verbatim — modulo
// Swift string escaping, where a backslash is doubled ("\\b" is `\b`).
//
// Behavior: pull URLs, emails, IPv4/IPv6 addresses, hashes (md5/sha1/sha256/
// sha512 by length), and domains out of arbitrary text. Matches are deduped
// per type preserving first-occurrence order; an email also contributes its
// domain to the domain list when both email and domain are selected. An empty
// types array defaults to all six kinds.
import Foundation
/// One of the six canonical extraction kinds. Mirrors the TS `ExtractType`
/// union ('url' | 'email' | 'ipv4' | 'ipv6' | 'hash' | 'domain') and Go's
/// `Type`. Declaration order == the `EXTRACT_TYPES` display order, so
/// `allCases` doubles as the default selection.
enum ExtractKind: String, CaseIterable {
case url, email, ipv4, ipv6, hash, domain
}
/// One field per kind — always all six, populated only for the selected
/// kinds (unselected kinds stay empty). Mirrors the TS
/// `Record<ExtractType, string[]>` and the Go twin's `Result`.
struct ExtractResult: Equatable {
var url: [String] = []
var email: [String] = []
var ipv4: [String] = []
var ipv6: [String] = []
var hash: [String] = []
var domain: [String] = []
}
enum Extractor {
/// The per-kind patterns, compiled once and reused (static lets are
/// lazily initialized and thread-safe — the LazyLock of this port).
/// `try!` is sound: these are static literals known valid at compile
/// time, the same justification as the Rust port's `.unwrap()`.
/// In Swift string literals a backslash is escaped: `\b` is "\\b".
private static let reURL = try! NSRegularExpression(pattern: "https?://[^\\s]+")
private static let reEmail =
try! NSRegularExpression(pattern: "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}")
private static let reIPv4 =
try! NSRegularExpression(pattern: "\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b")
/// Intentionally permissive — a hex/colon run — post-filtered by `isIpv6`.
private static let reIPv6 = try! NSRegularExpression(pattern: "[0-9a-fA-F:]+")
/// md5 (32) / sha1 (40) / sha256 (64) / sha512 (128). `\b` keeps each
/// length honest, so a 64-char run does not also match as a leading
/// 32-char hash.
private static let reHash = try! NSRegularExpression(
pattern: "\\b[a-fA-F0-9]{32}\\b|\\b[a-fA-F0-9]{40}\\b|"
+ "\\b[a-fA-F0-9]{64}\\b|\\b[a-fA-F0-9]{128}\\b")
private static let reDomain = try! NSRegularExpression(
pattern: "\\b[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z]{2,})+\\b")
/// ASCII hex digits only — the exact class behind `[0-9a-fA-F]`.
private static let hexSet: Set<Character> = Set("0123456789abcdefABCDEF")
/// Deduplicate, preserving first-occurrence order. The Go twin of the
/// `uniq()` helper in src/lib/extract.ts.
private static func uniq(_ values: [String]) -> [String] {
var seen = Set<String>()
var out: [String] = []
for v in values where !seen.contains(v) {
seen.insert(v)
out.append(v)
}
return out
}
/// Every non-overlapping match of `re` in `text`, left to right — the
/// ICU equivalent of JS `String.match` with the global flag and Go's
/// `FindAllString`. NSRange counts UTF-16 units, so the string is
/// bridged through NSString for slicing.
private static func allMatches(_ re: NSRegularExpression, in text: String) -> [String] {
let ns = text as NSString
return re.matches(in: text, range: NSRange(location: 0, length: ns.length))
.map { ns.substring(with: $0.range) }
}
/// A hex/colon run is a plausible IPv6: it has a colon AND either
/// contains `::` (a compressed zero-run) or is exactly eight groups of
/// 1-4 hex digits. Mirrors `isIpv6()` in src/lib/extract.ts.
static func isIpv6(_ run: some StringProtocol) -> Bool {
guard run.contains(":") else { return false }
if run.contains("::") { return true }
let groups = run.split(separator: ":", omittingEmptySubsequences: false)
return groups.count == 8 && groups.allSatisfy { g in
(1...4).contains(g.count) && g.allSatisfy(hexSet.contains)
}
}
/// Domain part (after the last `@`) of a matched email. Mirrors
/// `domainOf()` in src/lib/extract.ts (lastIndexOf('@') + slice).
private static func domainOf(_ email: String) -> String {
guard let at = email.lastIndex(of: "@") else { return email }
return String(email[email.index(after: at)...])
}
/// Extract every occurrence of the given `types` (default: all six) from
/// `input`. Returns an `ExtractResult` with one field per kind — always
/// all six, populated only for the selected types (unselected kinds stay
/// empty). Matches are deduped per kind, preserving first-occurrence
/// order. An email also contributes its domain to the `domain` list when
/// both `.email` and `.domain` are selected. Never traps; nil input is
/// treated as empty text.
static func extract(_ input: String?, types: [ExtractKind] = []) -> ExtractResult {
let text = input ?? ""
let selected = types.isEmpty ? ExtractKind.allCases : types
func want(_ k: ExtractKind) -> Bool { selected.contains(k) }
var out = ExtractResult()
if want(.url) { out.url = uniq(allMatches(reURL, in: text)) }
if want(.email) { out.email = uniq(allMatches(reEmail, in: text)) }
if want(.ipv4) { out.ipv4 = uniq(allMatches(reIPv4, in: text)) }
if want(.ipv6) {
out.ipv6 = uniq(allMatches(reIPv6, in: text).filter(isIpv6))
}
if want(.hash) { out.hash = uniq(allMatches(reHash, in: text)) }
if want(.domain) {
var combined = allMatches(reDomain, in: text)
// Cross-rule: an email also yields its domain in the domain list.
if want(.email) {
combined += allMatches(reEmail, in: text).map(domainOf)
}
out.domain = uniq(combined)
}
return out
}
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →