Skip to content

Text Extractor — Go 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 Go implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Package extract is the Go twin of CosmoDev's src/lib/extract.ts (dual source:
// the web lib is TypeScript, the CLI lib is Go — kept in lock-step). Pure +
// deterministic, never panics. The table-driven tests in extract_test.go share
// vectors with src/lib/extract.test.ts so the two implementations are held to
// the same contract.
//
// Behavior mirrors the TS lib exactly: 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. A zero-length types slice defaults to all six kinds.
package extract

import (
	"regexp"
	"strings"
)

// Type is one of the six canonical extraction kinds. It mirrors the TS
// ExtractType union ('url' | 'email' | 'ipv4' | 'ipv6' | 'hash' | 'domain').
type Type string

// Canonical extraction kinds, exported for callers and tests. They are the
// Go spelling of the string literals used in src/lib/extract.ts.
const (
	TypeURL    Type = "url"
	TypeEmail  Type = "email"
	TypeIPv4   Type = "ipv4"
	TypeIPv6   Type = "ipv6"
	TypeHash   Type = "hash"
	TypeDomain Type = "domain"
)

// ExtractTypes is the canonical extraction-kind list in display order. It is
// the Go twin of EXTRACT_TYPES in src/lib/extract.ts.
var ExtractTypes = []Type{TypeURL, TypeEmail, TypeIPv4, TypeIPv6, TypeHash, TypeDomain}

// RE holds the per-kind patterns. These mirror the RE record in the TS lib
// verbatim (RE2 accepts every construct used: \b, \d, (?:...), counted
// repetition). The IPv6 pattern is intentionally permissive — a hex/colon run
// — and is post-filtered by isIPv6 so bare hex words, times, and MACs are
// rejected, exactly as in the TS lib.
var (
	reURL    = regexp.MustCompile(`https?://[^\s]+`)
	reEmail  = regexp.MustCompile(`[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}`)
	reIPv4   = regexp.MustCompile(`\b(?:\d{1,3}\.){3}\d{1,3}\b`)
	reIPv6   = regexp.MustCompile(`[0-9a-fA-F:]+`)
	reHash   = regexp.MustCompile(`\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`)
	reDomain = regexp.MustCompile(`\b[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z]{2,})+\b`)

	// reIPv6Group validates a single 1–4 hex-digit IPv6 hextet.
	reIPv6Group = regexp.MustCompile(`^[0-9a-fA-F]{1,4}$`)
)

// Result is the Go twin of the TS Record<ExtractType, string[]>. Every field is
// always present (never nil): unselected kinds and empty matches are empty
// slices, matching the TS lib's "always all six keys" contract.
type Result struct {
	URL    []string
	Email  []string
	IPv4   []string
	IPv6   []string
	Hash   []string
	Domain []string
}

// uniq deduplicates values, preserving first-occurrence order. It is the Go
// twin of the uniq() helper in src/lib/extract.ts. It never returns nil.
func uniq(values []string) []string {
	seen := make(map[string]struct{}, len(values))
	out := make([]string, 0, len(values))
	for _, v := range values {
		if _, ok := seen[v]; !ok {
			seen[v] = struct{}{}
			out = append(out, v)
		}
	}
	return out
}

// isIPv6 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.
func isIPv6(run string) bool {
	if !strings.Contains(run, ":") {
		return false
	}
	if strings.Contains(run, "::") {
		return true
	}
	groups := strings.Split(run, ":")
	if len(groups) != 8 {
		return false
	}
	for _, g := range groups {
		if !reIPv6Group.MatchString(g) {
			return false
		}
	}
	return true
}

// domainOf returns the domain part (after the last '@') of a matched email.
// Mirrors domainOf() in src/lib/extract.ts.
func domainOf(email string) string {
	if idx := strings.LastIndex(email, "@"); idx >= 0 {
		return email[idx+1:]
	}
	return email
}

// allMatches returns every non-overlapping match of re in text (never nil).
func allMatches(re *regexp.Regexp, text string) []string {
	m := re.FindAllString(text, -1)
	if m == nil {
		return []string{}
	}
	return m
}

// Extract pulls every occurrence of the given types (default: all six) from
// input. It returns a Result 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 Go twin of
// extract() in src/lib/extract.ts and must agree with it on every shared vector.
func Extract(input string, types []Type) Result {
	// TS does `const text = input ?? ''`; a Go string cannot be null, so input
	// is already the empty string when absent.
	text := input

	selected := types
	if len(selected) == 0 {
		selected = ExtractTypes
	}
	wantSet := make(map[Type]bool, len(selected))
	for _, t := range selected {
		wantSet[t] = true
	}
	want := func(t Type) bool { return wantSet[t] }

	out := Result{
		URL: []string{}, Email: []string{}, IPv4: []string{},
		IPv6: []string{}, Hash: []string{}, Domain: []string{},
	}

	if want(TypeURL) {
		out.URL = uniq(allMatches(reURL, text))
	}
	if want(TypeEmail) {
		out.Email = uniq(allMatches(reEmail, text))
	}
	if want(TypeIPv4) {
		out.IPv4 = uniq(allMatches(reIPv4, text))
	}
	if want(TypeIPv6) {
		runs := allMatches(reIPv6, text)
		filtered := make([]string, 0, len(runs))
		for _, run := range runs {
			if isIPv6(run) {
				filtered = append(filtered, run)
			}
		}
		out.IPv6 = uniq(filtered)
	}
	if want(TypeHash) {
		out.Hash = uniq(allMatches(reHash, text))
	}
	if want(TypeDomain) {
		fromRegex := allMatches(reDomain, text)
		combined := make([]string, 0, len(fromRegex))
		combined = append(combined, fromRegex...)
		// Cross-rule: an email also yields its domain in the domain list.
		if want(TypeEmail) {
			for _, e := range allMatches(reEmail, text) {
				combined = append(combined, domainOf(e))
			}
		}
		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 →