Contents

ekscrypto/swiftpublicsuffixlist

This library is a Swift implementation of the necessary code to check a domain name against the [Public Suffix List](https://publicsuffix.org) and identify if the domains should be restricted.

Performance

The library ships a pre-compiled binary trie (registry.trie) that is memory-mapped at first use. There is no JSON parsing and no per-rule allocation — load cost is effectively the time to mmap a ~150 KB file (well under 1 ms on any supported device).

Matching walks the trie by label from TLD inward. A single isUnrestricted(_:) call runs in microseconds and does not allocate heap memory proportional to the rule set, so checking thousands of domains in a loop is a non-issue.

This replaces the pre-v2 behaviour where loading the JSON-backed rule set and scanning it linearly could take up to ~1 s on a mobile device.

Hostnames must be in ACE (Punycode) form

Since v3.0, isUnrestricted(:) and match(:) expect hostnames in ASCII / ACE form. IDN labels must be Punycode-encoded by the caller before the check:

// ❌ rejected — raw Unicode is not a valid wire-format hostname PublicSuffixList.isUnrestricted("example.香港")

// ✅ correct PublicSuffixList.isUnrestricted("example.xn--j6w193g")

This matches the DNS wire format and the RFC 5321 hostname syntax used in email. The embedded trie stores ACE-form labels only, so the matcher can compare byte-for-byte against whatever the caller supplies without surprise. Rule inputs to .rules([[String]]) may still be in UTF-8 form — the builder runs each label through an RFC 3492 Punycode encoder at build time.

If your input is a Unicode hostname, convert it with PublicSuffixList.ace(_:) before the check:

let host = PublicSuffixList.ace("example.香港") // "example.xn--j6w193g" PublicSuffixList.isUnrestricted(host) // true

ace(_:) applies the internal Punycode encoder to each label and rejoins with . — it does no case folding or Unicode normalization, so feed already-canonicalized labels (NFC, lowercased as appropriate) if you need full IDNA processing. The library intentionally ships no decoder; rendering ACE back to Unicode is the caller's responsibility.

Classes & Usage

PublicSuffixList

.match(_ candidate: String) -> Match?

import SwiftPublicSuffixList

Using the default built-in Public Suffix List rules:

if let match = PublicSuffixList.match("yahoo.com") { // match.isRestricted == false // match.prevailingRule == ["com"] }

// or using a PublicSuffixList instance… let publicSuffixList = PublicSuffixList() if let match = publicSuffixList.match("yahoo.com") { // match.isRestricted == false }

// or the async equivalent let publicSuffixList = await PublicSuffixList.list() if let match = publicSuffixList.match("yahoo.com") { // match.isRestricted == false }

Using a single custom validation rule, requiring domains to end with .com but allowing any domain within the .com TLD:

if let match = PublicSuffixList.match("yahoo.com", rules: [["com"]]) { // match.isRestricted == false // match.prevailingRule == ["com"] }

// or using a PublicSuffixList instance… let publicSuffixList = PublicSuffixList(source: .rules([["com"]])) if let match = publicSuffixList.match("yahoo.com") { // match.isRestricted == false // match.prevailingRule == ["com"] }

Using a single custom validation rule that restricts domains ending with .com but allows any subdomain:

if let match = PublicSuffixList.match("yahoo.com", rules: [["","com"]]) { // yahoo.com matches .com and so it is restricted // match.isRestricted == true // match.prevailingRule == ["com"] // wildcard edge walked; rule body is the labels matched }

if let match = PublicSuffixList.match("www.yahoo.com", rules: [["","com"]]) { // yahoo.com matches .com and is restricted, but www.yahoo.com has one // more label than the rule, so it's registrable. // match.isRestricted == false }

Defining an exception to a more generic rule:

if let match = PublicSuffixList.match("yahoo.com", rules: [["","com"],["!yahoo","com"]]) { // The exception (!yahoo.com) overrides the .com rule. // match.isRestricted == false // match.prevailingRule == ["!yahoo","com"] }

.isUnrestricted(_ candidate: String) -> Bool

Convenience that returns !match.isRestricted, or false if no rule matches or the host is syntactically invalid. This is the fastest path — zero heap allocations proportional to the rule set.

if PublicSuffixList.isUnrestricted("yahoo.com") { // true — yahoo.com is unrestricted by default }

// or using a PublicSuffixList instance… let publicSuffixList = PublicSuffixList() if publicSuffixList.isUnrestricted("yahoo.com") { // true — yahoo.com is unrestricted by default }

Match

Match exposes only two fields — the prevailing rule and whether the candidate is restricted:

public struct Match { public let prevailingRule: [String] public let isRestricted: Bool }

(The legacy matchedRules field was removed in v2; it was never part of the public-suffix algorithm and its construction cost dominated match time.)

Package Metadata

Repository: ekscrypto/swiftpublicsuffixlist

Default branch: main

README: README.md