Introduction to punycoder
Bart Turczynski
2026-07-26
Source:vignettes/punycoder-intro.Rmd
punycoder-intro.RmdIntroduction
The punycoder package provides high-performance Unicode
and Punycode encoding/decoding for internationalized domain names
(IDNs). It addresses critical gaps in R’s URL processing capabilities by
offering reliable, fast conversion between Unicode and ASCII
representations of domain names.
Why punycoder?
The Problem
International domain names containing Unicode characters (like café.com or москва.рф) need to be converted to ASCII format for use in many network protocols and systems. Existing R packages have limitations:
- Inconsistent legacy helpers: Existing workflows may produce incorrect punycode output
- Limited functionality: No comprehensive IDN handling
- Performance: No efficient bulk processing
Basic Usage
Domain Encoding and Decoding
library(punycoder)
# Encode Unicode domains to ASCII
puny_encode("café.com")
# Returns: "xn--caf-dma.com"
puny_encode("москва.рф")
# Returns: "xn--80adxhks.xn--p1ai"
# Decode ASCII domains back to Unicode
puny_decode("xn--caf-dma.com")
# Returns: "café.com"
# Vectorized operations
domains <- c("café.com", "москва.рф", "北京.中国")
encoded <- puny_encode(domains)
print(encoded)Validation and Utilities
# Check if domain is already punycode
is_punycode("xn--caf-dma.com") # TRUE
is_punycode("café.com") # FALSE
# Check if domain contains Unicode characters
is_idn("café.com") # TRUE
is_idn("example.com") # FALSE
# Comprehensive domain validation
result <- validate_domain(c("café.com", "invalid..domain", "valid.org"))
print(result)Host Normalization
puny_encode() is the raw RFC 3492 transform: it
Punycode-encodes the labels you hand it, and nothing more.
host_normalize() is the higher-level operation. It applies
the UTS #46 canonical-host pipeline — character mapping, case folding,
NFC normalization, label validation — and then encodes, so it
answers “what is the canonical ASCII form of this host?” rather than
“what is the Punycode of this string?”.
library(punycoder)
# Mapped, case-folded, validated, then encoded
host_normalize("Café.Example.COM")
#> [1] "xn--caf-dma.example.com"
# The codec transforms what it is given, and leaves case alone
puny_encode("Café.Example.COM")
#> [1] "xn--Caf-dma.Example.COM"Invalid Input Returns NA
host_normalize() does not follow the strict/non-strict
switch described above. It is a separate contract: a host that fails UTS
#46 validation is reported as NA and never raises an error,
so callers can layer their own policy on top.
host_normalize(c("valid.example", "example..com", "-bad-.example"))
#> [1] "valid.example" NA NAProfile Identity
Normalization is pinned to a named profile.
normalization_profile_info() returns that identity as a
one-row data frame, which is what you record alongside results you
intend to reproduce later:
normalization_profile_info()
#> profile unicode_version idna transitional use_std3
#> 1 uts46-nontransitional-std3-v2 17.0.0 uts46 FALSE TRUE
#> check_hyphens check_bidi check_joiners verify_dns_length backend
#> 1 TRUE TRUE TRUE TRUE fallbackUnicode Version Selection
A build of punycoder ships a set of vendored Unicode
table versions, one of which is the pinned default:
unicode_versions()
#> [1] "16.0.0" "17.0.0"Any shipped version can be selected per call. The difference between versions is confined to code points assigned in the newer one — a character that does not exist yet in the older tables is disallowed there, so the host is invalid:
host <- paste0(intToUtf8(0xA7CF), ".example")
host_normalize(host, unicode_version = "16.0.0")
#> [1] NA
host_normalize(host, unicode_version = "17.0.0")
#> [1] "xn--078a.example"Selecting a non-default version is reflected in the profile token, so the identity you record always describes the normalization that actually ran:
normalization_profile_info(unicode_version = "16.0.0")$profile
#> [1] "uts46-nontransitional-std3-v2+unicode-16.0.0"The argument defaults to NULL, which means the pinned
version — deliberately not “the newest available”, so adding a table set
in a later release cannot silently change results under existing code.
Naming a version the build does not ship is an error listing what is
available, never a quiet fall back to the pin.
See ?host_normalize for the relaxable validation flags
and the full profile parameters.
Data Analysis Workflows
Bulk Domain Processing
# Example: Processing large datasets
set.seed(123)
sample_domains <- c(
rep("example.com", 1000),
rep("café.com", 1000),
rep("test.org", 1000)
)
# Efficient vectorized encoding
system.time({
encoded_domains <- puny_encode(sample_domains)
})
# Check results
table(is_punycode(encoded_domains))Error Handling
The package provides robust error handling with informative messages:
# Strict validation (default)
try({
puny_encode(c("valid.com", "")) # Empty string causes error
})
# Non-strict mode returns NA for invalid input
result <- puny_encode(c("valid.com", ""), strict = FALSE)
print(result)
# Validation provides detailed error information
validation <- validate_domain(c("valid.com", "invalid..domain", ""))
print(validation)Performance Considerations
The package is designed for high-performance processing:
- Vectorized operations: Process thousands of domains efficiently
- C++ backend: Native implementation for speed
- Memory efficient: Handles large datasets without excessive memory use
# Benchmark with large dataset
large_domains <- rep(c("example.com", "café.com"), 5000)
system.time({
encoded <- puny_encode(large_domains)
})
# Should process 10,000+ domains per secondIntegration with Other Packages
punycoder is designed to integrate well with other R
packages:
# With data.table
library(data.table)
dt <- data.table(
original = c("café.com", "москва.рф"),
encoded = puny_encode(c("café.com", "москва.рф"))
)
# With dplyr
library(dplyr)
domains_df <- data.frame(
unicode_domain = c("café.com", "москва.рф")
) |>
mutate(
ascii_domain = puny_encode(unicode_domain),
is_international = is_idn(unicode_domain)
)Next Steps
- Explore the function documentation:
help(package = "punycoder") - Check out the test suite for more examples
- Report issues at: https://github.com/bart-turczynski/punycoder/issues
Technical Details
The package uses a C++ backend with Rcpp for performance, and follows
RFC 3492 standards for punycode implementation. When
libidn2 is available at build time, punycoder
uses it behind the same R-level API and falls back to the built-in
implementation otherwise.
See also
punycoder is used as the Punycode and IDNA engine by two
sibling packages:
-
pslr —
Public Suffix List engine. Uses
punycoderfor host canonicalization before PSL matching. Reach for it when you need eTLD or registrable-domain queries. -
rurl — Full
URL parsing, normalization, cleaning, and joining toolkit. Builds on
both
punycoderandpslrto handle the complete URL processing pipeline.
Acknowledgments
punycoder descends from the earlier R hrbrmstr/punycode
package and implements published standards directly: RFC 3492
(Punycode), the IDNA2008 RFCs, Unicode UTS #46, and UAX #15
normalization, validated against the Unicode Consortium’s IdnaTestV2
conformance data. It is built on Rcpp with an optional GNU
libidn2 backend.
The full list of credits — prior art, dependencies, the standards
this code implements, and the data sources it serves — is in ACKNOWLEDGMENTS.md.