--- title: "Prior art and comparison" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Prior art and comparison} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ## Libraries in other ecosystems Punycode/IDN libraries exist in most ecosystems. `punycoder` is most directly a maintained, IDNA2008-era successor to the libidn-based R tooling — its public API (`puny_encode()` / `puny_decode()` / `is_punycode()`) descends from [`hrbrmstr/punycode`](https://github.com/hrbrmstr/punycode). The table below situates it against representative libraries. | | **punycoder** (R) | [hrbrmstr/punycode](https://github.com/hrbrmstr/punycode) (R) | [punycoder](https://pub.dev/packages/punycoder) (Dart) | [simonmittag/punycoder](https://github.com/simonmittag/punycoder) (Go) | |---|---|---|---|---| | Form | library | library | library | CLI tool | | RFC 3492 codec | yes | yes | yes | yes | | Engine | `libidn2` + in-tree fallback | GNU `libidn` | pure Dart | Go `x/net/idna` | | IDNA standard | 2008 / UTS #46 (non-transitional) | 2003 (nameprep) | RFC 3492 + IDNA helpers | UTS #46 (via `x/net`) | | Unicode NFC | explicit (UAX #15) | implicit in nameprep | not documented | via `x/net` | | Pinned Unicode version | yes — 17.0.0, regenerable; 16.0.0 also selectable | no (frozen at build) | no | tracks Go release | | CheckBidi / CheckJoiners | always on | not surfaced | not documented | partial | | UTS #46 conformance corpus (`IdnaTestV2`) | yes — one per shipped Unicode version | no | no | — | | Strict / `NA` per-element policy | yes | undocumented | `validate` flag | n/a (CLI) | | Vectorized | yes | yes | n/a | n/a | | Maintenance | active | last commit 2015 | maintained | maintained | > The most consequential row is **IDNA standard**. IDNA2003 (GNU `libidn`, > nameprep) and IDNA2008 / UTS #46 disagree on real domains: the *deviation > characters* `ß`, `ς`, and the joiners ZWJ/ZWNJ. Under IDNA2003 `faß.de` is > mapped to `fass.de` — a **different host** — whereas `punycoder`'s pinned > UTS #46 non-transitional profile preserves it as `xn--fa-hia.de`. A > libidn-era pipeline therefore silently rewrites some hosts rather than > erroring, which is the class of bug `punycoder` exists to remove. > Comparisons reflect each project's public documentation as of this writing and > describe documented behavior, not an independent audit. ## Observed behavior on the comparable R packages Running the same inputs through the comparable R packages surfaces concrete behavioral differences (observed against `punycode` 0.2.5, `urltools` 1.7.3.1, and the author's own upstack toolkit [`rurl`](https://CRAN.R-project.org/package=rurl) 1.4.0). The raw RFC 3492 codec output agrees byte-for-byte across the codecs once direction is aligned — the divergences are in multi-label handling, idempotency, validity philosophy, and input scope. `rurl` is a URL parser/normalizer rather than a Punycode codec; it is included to show where the URL-shaped inputs `punycoder` deliberately rejects are actually handled (it delegates IDNA host conversion to `punycoder`), so `—` below means "out of scope for that layer," not a defect: | Behavior | **punycoder** | hrbrmstr/punycode | urltools | rurl | |---|---|---|---|---| | Primary role | Punycode/IDNA host codec | Punycode codec (IDNA2003) | URL + punycode utilities | URL parser / normalizer | | `puny_encode()` direction | Unicode → ASCII | **ASCII → Unicode** (names inverted) | Unicode → ASCII | — (no codec; IDNA via `punycoder`) | | Decode multi-label `xn--hxakfddc2amo8b.xn--qxam` | `ελράδειγμα.ελ` ✓ | `ελράδειγμα.ελ` ✓ | `ελράδειγμα.ελράδειγμα` ✗ (second label corrupted) | — (no `xn--` → Unicode decoder) | | Re-encode an already-`xn--` label | unchanged — idempotent ✓ | unchanged ✓ | `xn--xn--…-.xn--xn--…-` ✗ (double-encoded) | — | | Round-trip `decode(encode(x)) == x` | yes | yes | no (from the decode bug above) | — | | `gr€€n.no` — EURO SIGN, valid under UTS #46 | accepted → `xn--grn-l50aa.no` | rejected by `puny_tld_check` (IDNA2008) | — | parses; host preserved | | Full-URL input (`http://…`) | rejected with an actionable error pointing at a URL parser (`rurl`) | n/a (domain-only) | passed through unchanged | **parsed** — scheme/host/domain/TLD extracted; `get_clean_url()` lowercases the host and resolves dot-segments | | Required system library | none (`libidn2` optional) | GNU `libidn` (v1) required to build | none | none | > `punycode` names its functions opposite to the usual convention: > `punycode::puny_encode()` maps `xn--` → Unicode and > `punycode::puny_decode()` maps Unicode → `xn--`. The rows above align > by transform direction, not by function name. > > `punycoder` + `rurl` (+ [`pslr`](https://CRAN.R-project.org/package=pslr) for > the public-suffix/TLD truth) are designed to compose: `rurl` parses the URL and > hands the host to `punycoder` for IDNA canonicalization, each package owning a > single concern.