pslr now installs from CRAN alone: the
punycoder dependency floor is >= 1.1.0 (the
current CRAN release) and the development Remotes: pin is
dropped. pslr uses only punycoder API present
since 1.1.0 and is forward-compatible with punycoder 1.2.x,
whose default host_normalize() output is byte-identical
(PSLR-xwcqnnls).
The result cache no longer evicts on an oversized one-shot query: a single call with more distinct hosts than the whole cache capacity is still matched-but-not-cached, but it now leaves any existing warm entries intact instead of flushing the table to make room it could never use (PSLR-wyvauroc).
Addressed goodpractice style findings: brought
valid_psl_manifest(), parse_psl_lines(), and
psl_validate_keep() under the cyclomatic-complexity
threshold, dropped a <<- out of the cache-marker
reader, deduplicated the path argument docs via
@inheritParams, tightened the freshness-corpus tests to
expect_identical(), and extended
inst/WORDLIST; no user-facing change
(PSLR-cftnwsxa).
Added a PSL-freshness harm corpus (McQuistin et al., IMC ’23,
Table 2) that pins newly added multi-label eTLDs
(e.g. myshopify.com, netlify.app,
*.digitaloceanspaces.com) against the bundled snapshot,
guarding against shipping a list stale enough to put the
registrable-domain boundary above the tenant label
(PSLR-cowbpwsy).
The core matcher is now a reverse-label trie: one right-to-left label descent per host replaces the previous per-suffix hash-set probes, roughly halving direct-match time on cache-cold, large-batch, and cache-disabled workloads; results are byte-identical (PSLR-rqcslhfc).
The five query functions (public_suffix(),
registrable_domain(), is_public_suffix(),
suffix_extract(), public_suffix_rule()) gain
an optional engine= argument to query a specific
psl_engine() snapshot instead of the session-global list;
omitting it is unchanged (PSLR-hflrsfgp).
New psl_engine() constructs a self-contained,
process-local PSL engine (source = "bundled" or
"path") for querying a specific snapshot without
psl_use() (PSLR-ntqoiglh).
psl_version() now reports the active snapshot’s
source url as a new column – the upstream download URL for
the bundled list; NA for cache or custom-path sources
(PSLR-qcvrfoun).
output = "unicode" now decodes each distinct A-label
only once per call – suffix_extract() pools its five
columns into a single punycoder::puny_decode() crossing and
every query deduplicates repeats – so unicode output on batches with
repeated or overlapping hosts is markedly faster; decoded output is
byte-identical (PSLR-cpzrjksw).
Every query no longer pays for a per-call strsplit()
to count host labels; is_public_suffix() now reads the
matcher’s ps_start offset, which is equivalent, so results
are unchanged (PSLR-rriiajin).
The session result cache now pre-sizes its key index to the incoming unique-batch size, so a large cold batch fills a right-sized hash table in one shot instead of rehashing repeatedly during insertion; results are unchanged (PSLR-gtvggjmd).
The PSL-format parser now preallocates its rule columns and fills them by index instead of growing them one accepted rule at a time, making a full list rebuild roughly 3x faster; output is byte-identical (PSLR-wanopbqy).
New psl_cache_prune() removes superseded on-disk
psl-<hex>.dat cache snapshots, always keeping the
active snapshot plus the keep most-recent others (default
one previous); a no-op when there is no cache or marker
(PSLR-nwdejhkf).
Cache checksum verification now recomputes the algorithm named by
the recorded sha256:/md5: prefix instead of
whichever hash digest availability picks at call time, so a
cache is no longer spuriously reported as corrupt across machines that
differ in the optional digest package; a missing
digest for an sha256-recorded cache now raises
an actionable install error rather than a corruption error
(PSLR-mxohlxiq).
The cache commit marker is now structurally validated when read,
so a readable-but-malformed current.rds (missing
dat_file, a short meta, or wrong field types)
raises the actionable cache-corruption error advising
psl_refresh(force = TRUE) instead of degrading into silent
NULL reads; newly written markers carry a schema version
and markers from earlier releases stay valid (PSLR-nwdejhkf).
New psl_outdated() reports whether the active list
snapshot is older than a threshold (default 180 days), purely offline
from psl_version()’s list_date, as a nudge
toward psl_refresh(); the snapshot age in days is returned
in the "age_days" attribute (#62).
New options(pslr.cache = FALSE) escape hatch
disables the session result cache for the current session, skipping
every cache read and write. It never changes a result – misses are
derived by the same code path – so output is byte-identical to the
cached path; it only controls whether entries are stored and read.
Useful for one-shot batches of mostly-unique hosts, where within a
single vectorized call unique() already deduplicates and
the per-key cache read is pure overhead (roughly 1.5x faster with the
cache off on a 200,000-unique batch). Caching stays on by
default.
The core C++ matcher hardened its construction:
psl_build_matcher() now validates that its
keys/kinds/sections columns share
a length and that each section (0/1) and kind
(normal/wildcard/exception) is in
range – an unknown kind now errors instead of being silently bucketed as
an exception – exact-reserves the six rule sets from a counting pass to
avoid rehashing, and builds via a std::unique_ptr released
only after the R external pointer is registered;
psl_match() rejects a NULL external pointer before
dereferencing. Behaviour on valid input is byte-identical
(PSLR-sfppglqs).
psl_use() now uses a scalar formal default validated
by check_choice(), matching the query functions, and the
sole-purpose match_opt() helper is removed; argument
handling and every result/error are unchanged (PSLR-vmwsipkm).
Added ^\.claude$ to .Rbuildignore so a
contributor using Claude Code no longer sweeps the gitignored
.claude/ session directory into the build tarball,
silencing the spurious R CMD check hidden-files NOTE; no
package content changes (PSLR-juyrfjls).
Query internals are now projection-aware: a query derives only
the result string columns it consumes instead of all five on every call.
psl_query_cols()/psl_resolve_cores()/psl_derive_strings()
take a fields projection (the cheap
kind/rule_section enum columns and the byte
offsets stay always-derived; the substr-heavy
public_suffix/registrable_domain/rule
columns are gated), the compact structural cache is untouched, and
public_suffix()/registrable_domain() now share
one internal psl_query_vector(). Every result is
byte-identical (differential oracle unchanged) (PSLR-sscuznba).
The session result cache now stores only compact integer
structural columns (public-suffix depth, the three byte offsets, and the
kind/section enum codes) rather than the
derived strings; the user-facing
public_suffix/registrable_domain/rule/kind/rule_section
columns are reconstructed on read by a new
psl_derive_strings() after cache assembly, splitting the
result schema (psl_result_cols) from the compact cache
schema (psl_cache_cols). Results are byte-identical
(differential oracle and cache-on/off identity unchanged)
(PSLR-muyzxbpl).
The global query functions now resolve a single process-wide
default engine via psl_default_engine() and thread it
explicitly through the internal match/cache path
(psl_query_cols -> psl_resolve_cores ->
psl_match_records), replacing the implicit global-state
fetch; psl_use() and the refresh activation paths replace
that default engine. Public signatures and behaviour are byte-identical
(PSLR-cchcomkk).
Moved result-cache ownership into the psl_engine:
each engine mints its own new_psl_cache(), so activating a
list swaps the whole engine (starting cold) instead of clearing a shared
global, and the cache key drops the now-redundant list-identity prefix
(keyed on section + canonical host); public behaviour is byte-identical,
with new engine-cache isolation tests (PSLR-bcgedhmy).
Modelled the active-list state as internal
psl_snapshot (rules + metadata + source identity) and
process-local psl_engine (snapshot + compiled matcher)
objects, and unified the two cache-activation paths onto a shared
psl_load_cached_snapshot() loader behind a single
psl_activate_snapshot() choke-point; internal only, public
behaviour byte-identical (PSLR-fvotbdti).
The five public query functions and psl_rules() now
carry scalar formal defaults validated by an internal
check_choice(), dropping the missing()-based
supplied-flag bookkeeping; argument handling and every result and error
are unchanged (PSLR-adsnjbjg).
Consolidated the snapshot-metadata field schema behind a single
owner: new_psl_meta() (construction/defaults),
validate_psl_meta() (checked boundary), and
as_psl_version_df() (the one-row psl_version()
frame) all read one psl_meta_fields schema, replacing the
field lists that were re-spelled in R/matcher.R,
R/metadata.R, and data-raw/update_psl.R;
output is byte-identical (PSLR-bnrbjhur).
Consolidated the two overlapping benchmark scripts into a single
authoritative harness under bench/ (shared fixtures/timing
in bench/helpers.R), removed the unreferenced
inst/bench/match-bench.R, and fixed two integrity defects:
the “unique” corpus is now deterministic and exactly-n distinct (via the
internal, unit-tested psl_bench_unique_hosts()), and every
scenario resets its intended cache state inside each timed rep so a cold
measurement is no longer contaminated by the previous rep’s warm cache
(PSLR-cefytpjr).
Raised test coverage from 96% to 100% by exercising the
previously-uncovered error and fallback branches across
refresh.R, matcher.R, cache.R,
canonicalize.R, duplicates.R, and
parser.R (mocked
downloader/digest/curl/system.file
seams, crafted inputs); the one unreachable C++ epilogue brace in
matcher.cpp is excluded with a # nocov marker.
No behaviour change; the differential oracle is unchanged
(#66).
Collapsed the repeated
section/unknown/invalid
option-validation preamble across the five exported query functions into
a shared resolve_common_opts() helper, factored
suffix_extract()’s byte-offset slicing into
psl_slice_registrant(), and drove
psl_query_cols()’s eight match columns off the shared
schema (reusing psl_match_alloc(), now
NA-filled). Clears the remaining goodpractice
function-length findings for R/query.R; results are
byte-identical (oracle unchanged) (#65).
Drove psl_resolve_cores()’s eight parallel match
columns off the shared cache schema (psl_cache_cols)
instead of spelling each column out four times, and factored the
cache-index lookup into psl_cache_lookup(). Clears the
goodpractice function-length finding; results are
byte-identical (oracle and cache tests unchanged) (#64).
Raised the default session-cache bound from 50,000 to 200,000 entries. The columnar store (from the P2-P4 rewrite) costs about 80 bytes per entry (~16 MB for a full 200,000-entry table) and memory scales with live entries, so small sessions pay nothing. The higher bound lets a large working set re-queried across calls stay warm instead of tripping the full-flush eviction cliff – on a 200,000-unique benchmark the second pass drops from ~1.63 s (flush and re-derive) to ~0.83 s (a true cache hit). The full-flush eviction semantics above the bound are unchanged.
The core C++ matcher (psl_match()) now also returns
1-based byte offsets into the canonical ASCII host
(ps_start / rd_start /
ps1_start). The R engine derives the public-suffix,
registrable-domain, and rule strings for the whole miss vector at once
with a single vectorized substr() per column, replacing the
per-host derive_one()/suffix_labels()
paste() loop (now removed). Pure internal restructuring;
query results are byte-identical (the differential oracle is unchanged),
but the miss-path string derivation is roughly 40x faster and drops out
of the query profile.
The session result cache is now columnar. Instead of one R list
per host, it keeps a key -> integer-index environment alongside
parallel column vectors (including the ps_start /
rd_start byte offsets), grown by doubling. Cache hits
resolve via a single mget() of indices plus vectorized
column subsetting, and psl_resolve_cores() returns its
columns directly, removing the six per-call vapply()
reassembly passes over the whole unique-host list. Pure internal
restructuring; the cache key semantics and the differential oracle are
unchanged, but the warm-cache query path is dramatically
faster.
The shared per-element query builder
(psl_query_frame(), now psl_query_cols())
returns a plain list of parallel column vectors instead of constructing
a 12-column data.frame on every call. The length-preserving
accessors (public_suffix() /
registrable_domain() / is_public_suffix())
read the one or two columns they need directly; only
suffix_extract() and public_suffix_rule()
build a data.frame, once, at the end.
suffix_extract() additionally drops its per-row
strsplit loop, slicing the registrant label and subdomain
out of the canonical host with vectorized substr() over the
matcher’s ps_start / rd_start byte offsets.
Pure internal restructuring; query results and the differential oracle
are unchanged, but the fixed per-call overhead falls sharply (warm
scalar registrable_domain() roughly halves).
strict = TRUE argument from
punycoder::host_normalize() calls. punycoder
removed the inert strict flag in favour of explicit UTS #46
flags (all defaulting to the strict profile), so the bare call is
behavior-preserving and forward-compatible with that release. No
user-visible change; this keeps pslr installable against
the upcoming punycoder release.psl_canonicalize(),
parse_psl_lines(), the core matcher resolution, and
psl_refresh()/psl_use() into smaller helpers
to clear goodpractice cyclomatic-complexity and
function-length findings. Pure internal restructuring; no behavior or
API change.Launch-readiness audit follow-ups (no API changes).
suffix_extract(output = "unicode") no longer turns an
empty subdomain into NA. An absent subdomain is reported as
"" for both "ascii" and "unicode"
output, matching the documented schema.section,
output, unknown, invalid, and
psl_use()’s source) now abort when a caller
supplies a non-scalar value, even one that happens to equal the formal’s
default vector (e.g. invalid = c("na", "error")).
Previously such a call was mistaken for the untouched default and
silently used the first choice. Omitted options still default to their
first choice.current.rds) is now handled
gracefully instead of leaking a raw readRDS() “unknown
input format” error. psl_refresh(force = TRUE) ignores an
unreadable marker and republishes a valid cache, and
psl_use("cache") reports a pslr cache-corruption error with
remediation.BEGIN for either aborts the parse instead of
loading both copies.domain
(e.g. numeric(0), NULL) now aborts with the
documented type error instead of being silently coerced to an empty
result. This is consistent with the input contract: a wrong argument
type is a programming error regardless of length. The valid empty
character vector character(0) still returns a zero-length
result.First public release: a spec-complete Public Suffix List engine for R.
9186eee (list date 2026-06-13), with a deterministic
data-raw/ regeneration script, an internal validated rule
index, generation metadata (commit, source URL, checksum, normalization
profile, Unicode version), and MPL-2.0 data licensing separate from the
package’s MIT code license.public_suffix(),
registrable_domain(), is_public_suffix(),
suffix_extract(), and public_suffix_rule().
All are vectorised, length- and name-preserving, NA-safe, and share the
section / output / unknown /
invalid policies. Input is canonicalized through
punycoder with terminal-dot preservation and dotted-decimal
IPv4 literal rejection, and repeated queries are served from a bounded
session cache keyed by host, active-list identity, and section.psl_refresh(),
psl_use(), psl_version(), and
psl_rules(). psl_refresh() is the only network
path – an explicit, https-only, credential- and
downgrade-redirect-rejecting download with a size ceiling, full
validation, a 24-hour reuse throttle, and an atomic cache publish that
never exposes a partial snapshot or replaces a valid cache after a
failed refresh. psl_use() switches the session’s active
list between the bundled snapshot, the user cache, and a custom path,
validating before any state changes and clearing the result cache on a
successful switch. The bundled index is rebuilt in memory from source
when its normalization profile or Unicode version differs from the
runtime normalizer, never mixing profiles. psl_version()
reports the active-list identity and runtime normalization identifiers
needed to reproduce a result; psl_rules() exposes the
active rule table.punycoder normalization and a single C++ matcher call
regardless of multiplicity. A non-CRAN benchmark and its release gate
live in bench/benchmark.R.