Text Extractor — Kotlin 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 Kotlin 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: Kotlin (1.9+, standard library only)
// 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 throws.
// - Functionally equivalent to the TS/Go reference: same inputs -> same outputs.
// - The six per-kind patterns are reproduced VERBATIM from the Go twin so the
// "lock-step contract" between the two implementations is auditable at a
// glance.
//
// Engine note: kotlin.text.Regex delegates to java.util.regex, a backtracking
// engine, while Go's regexp is RE2 — but both implement leftmost-first
// (Perl-order) alternation and none of the patterns hinge on a
// backtracking-only or RE2-only subtlety, so matches agree on every input.
// Triple-quoted raw strings keep the pattern text character-for-character
// identical to the Go twin (no pattern below contains a dollar sign, so no raw
// string interpolation fires).
/** One of the six canonical extraction kinds. Mirrors the TS `ExtractType`
* union ('url' | 'email' | 'ipv4' | 'ipv6' | 'hash' | 'domain') and Go's
* `Type`. The [label] is the canonical spelling used in the TS/Go
* string-literal contract. */
enum class Kind(val label: String) {
URL("url"),
EMAIL("email"),
IPV4("ipv4"),
IPV6("ipv6"),
HASH("hash"),
DOMAIN("domain"),
}
/** The canonical kinds in display order — Go's `ExtractTypes` / TS's
* `EXTRACT_TYPES`. */
val ALL_KINDS: List<Kind> =
listOf(Kind.URL, Kind.EMAIL, Kind.IPV4, Kind.IPV6, Kind.HASH, Kind.DOMAIN)
// The per-kind patterns, compiled once and reused. They mirror the `RE` record
// in src/lib/extract.ts and the `re*` vars in the Go twin VERBATIM: same
// anchors (\b), character classes, and counted repetition.
private val RE_URL = Regex("""https?://[^\s]+""")
private val RE_EMAIL = Regex("""[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}""")
private val RE_IPV4 = Regex("""\b(?:\d{1,3}\.){3}\d{1,3}\b""")
/** Intentionally permissive — a hex/colon run — post-filtered by [isIpv6]. */
private val RE_IPV6 = Regex("""[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 val RE_HASH = Regex(
"""\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 val RE_DOMAIN = Regex(
"""\b[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z]{2,})+\b"""
)
/** Validates a single 1–4 hex-digit IPv6 hextet; [Regex.matches] pins both
* ends, so the ^/$ anchors in the sibling ports are implied. */
private val RE_IPV6_GROUP = Regex("""[0-9a-fA-F]{1,4}""")
/** The Kotlin twin of the TS `Record<ExtractType, string[]>`. Every field is
* always present (an empty list, never absent): unselected kinds and empty
* matches are empty, matching the TS lib's "always all six keys" contract —
* the default arguments give that shape for free. */
data class ExtractResult(
val url: List<String> = emptyList(),
val email: List<String> = emptyList(),
val ipv4: List<String> = emptyList(),
val ipv6: List<String> = emptyList(),
val hash: List<String> = emptyList(),
val domain: List<String> = emptyList(),
)
/** Deduplicate values, preserving first-occurrence order. The Kotlin twin of
* the `uniq()` helper in src/lib/extract.ts. [MutableSet.add] returns false on
* a repeat, so order is kept without a separate contains scan. */
private fun uniq(values: List<String>): List<String> {
val seen = HashSet<String>()
val out = ArrayList<String>(values.size)
for (v in values) {
if (seen.add(v)) out.add(v)
}
return out
}
/** Every non-overlapping match of [re] in [text], left to right — the Kotlin
* equivalent of Go's `regexp.FindAllString` and JS's
* `String.prototype.match` with the global flag. */
private fun allMatches(re: Regex, text: String): List<String> =
re.findAll(text).map { it.value }.toList()
/** Reports whether a hex/colon run is a plausible IPv6 address: it must
* contain a colon AND either hold a compressed zero-run (`::`) or be exactly
* eight groups of 1–4 hex digits. Mirrors `isIpv6()` in
* src/lib/extract.ts. */
fun isIpv6(run: String): Boolean {
if (':' !in run) return false
if ("::" in run) return true
// Kotlin's split(':') keeps empty segments at both ends, exactly like
// split(':') in the TS/Go/Python siblings.
val groups = run.split(":")
return groups.size == 8 && groups.all { RE_IPV6_GROUP.matches(it) }
}
/** The domain part (after the last `@`) of a matched email. Mirrors
* `domainOf()` in src/lib/extract.ts; with no `@`, the whole input is
* returned (the default argument of [substringAfterLast]). */
fun domainOf(email: String): String = email.substringAfterLast('@', email)
/** Extract pulls every occurrence of the given [types] (default: all six)
* from [input]. It returns an [ExtractResult] with one field per kind —
* always all six, populated only for the selected types. 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. It is
* the Kotlin twin of `extract()` in src/lib/extract.ts and must agree with it
* on every shared vector. */
fun extract(input: String?, types: List<Kind>? = null): ExtractResult {
val text = input ?: "" // TS does `const text = input ?? ''`
val selected = if (types.isNullOrEmpty()) ALL_KINDS else types
return ExtractResult(
url = if (Kind.URL in selected) uniq(allMatches(RE_URL, text)) else emptyList(),
email = if (Kind.EMAIL in selected) uniq(allMatches(RE_EMAIL, text)) else emptyList(),
ipv4 = if (Kind.IPV4 in selected) uniq(allMatches(RE_IPV4, text)) else emptyList(),
ipv6 = if (Kind.IPV6 in selected) {
uniq(allMatches(RE_IPV6, text).filter(::isIpv6))
} else {
emptyList()
},
hash = if (Kind.HASH in selected) uniq(allMatches(RE_HASH, text)) else emptyList(),
domain = if (Kind.DOMAIN in selected) {
val combined = allMatches(RE_DOMAIN, text).toMutableList()
// Cross-rule: an email also yields its domain in the domain list.
if (Kind.EMAIL in selected) {
combined += allMatches(RE_EMAIL, text).map(::domainOf)
}
uniq(combined)
} else {
emptyList()
},
)
}
// ---------- showcase tests (the canonical suite lives in src/lib) ----------
// Run this file as a program (`kotlin kotlin.kt`) for a five-vector smoke
// check — the Kotlin analogue of the Rust sibling's #[cfg(test)] module.
/** Well-known digests of the empty string (real hash values), shared with
* src/lib/extract.test.ts so the showcase uses identical vectors. */
private const val MD5_EMPTY = "d41d8cd98f00b204e9800998ecf8427e" // 32
private const val SHA256_EMPTY =
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" // 64
fun main() {
// Extracts and dedupes URLs, keeping first-occurrence order.
extract("a https://x.com b https://y.com c https://x.com").let {
check(it.url == listOf("https://x.com", "https://y.com")) { "url: ${it.url}" }
}
// Plus-tags and multi-part-TLD domains are matched; an email also
// contributes its domain when email AND domain are both selected.
extract("reach a.b+tag@mail.example.co.uk please").let {
check(it.email == listOf("a.b+tag@mail.example.co.uk")) { "email: ${it.email}" }
check(it.domain == listOf("mail.example.co.uk")) { "domain: ${it.domain}" }
}
// `::1` is a compressed zero-run; `12:30:45` has no `::` and only three
// groups, so it is rejected as a clock, not an address.
extract("loopback ::1 and time 12:30:45 now").let {
check(it.ipv6 == listOf("::1")) { "ipv6: ${it.ipv6}" }
}
// Hashes by length: md5 (32) and sha256 (64).
extract("m $MD5_EMPTY s $SHA256_EMPTY").let {
check(it.hash == listOf(MD5_EMPTY, SHA256_EMPTY)) { "hash: ${it.hash}" }
}
// Type selection: unselected kinds stay empty.
extract("https://x.com and a@b.com", listOf(Kind.URL)).let {
check(it.url == listOf("https://x.com")) { "url: ${it.url}" }
check(it.email.isEmpty()) { "email was not selected: ${it.email}" }
}
println("extract: all showcase tests passed")
}
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 →