perf(cli-rs): build url without ICU by pinning idna_adapter 1.0.0

The idna_adapter 1.0.0 stub turns IDNA off in url, which drops ICU4X: 21 crates, 2 build scripts and 4 proc macros. A non-ASCII host now fails to parse; punycode hosts still work. The client names the cause when a base URL such as --server has one (deviation A10-ascii-hosts, decision E1).
This commit is contained in:
Simon Klee
2026-11-07 03:48:51 +01:00
Unverified
parent 7240933714
commit bf11fd7fd9
6 changed files with 51 additions and 227 deletions
+3 -225
View File
@@ -292,17 +292,6 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "displaydoc"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[package]]
name = "equivalent"
version = "1.0.3"
@@ -577,89 +566,6 @@ dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd"
dependencies = [
"displaydoc",
"potential_utf",
"utf8_iter",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locale_core"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_normalizer"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599"
dependencies = [
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc"
[[package]]
name = "icu_properties"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locale_core",
"icu_properties_data",
"icu_provider",
"zerotrie",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf"
[[package]]
name = "icu_provider"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514"
dependencies = [
"displaydoc",
"icu_locale_core",
"writeable",
"yoke",
"zerofrom",
"zerotrie",
"zerovec",
]
[[package]]
name = "idna"
version = "1.1.0"
@@ -673,13 +579,9 @@ dependencies = [
[[package]]
name = "idna_adapter"
version = "1.2.2"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
dependencies = [
"icu_normalizer",
"icu_properties",
]
checksum = "cfdf4f5d937a025381f5ab13624b1c5f51414bfe5c9885663226eae8d6d39560"
[[package]]
name = "indexmap"
@@ -833,12 +735,6 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
[[package]]
name = "log"
version = "0.4.35"
@@ -961,6 +857,7 @@ dependencies = [
"base64",
"bytes",
"futures-util",
"idna_adapter",
"indexmap",
"log",
"memchr",
@@ -1302,15 +1199,6 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "potential_utf"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
dependencies = [
"zerovec",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
@@ -1712,12 +1600,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "streaming-iterator"
version = "0.1.9"
@@ -1767,17 +1649,6 @@ dependencies = [
"futures-core",
]
[[package]]
name = "synstructure"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[package]]
name = "tempfile"
version = "3.27.0"
@@ -1821,16 +1692,6 @@ dependencies = [
"syn 3.0.9",
]
[[package]]
name = "tinystr"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a"
dependencies = [
"displaydoc",
"zerovec",
]
[[package]]
name = "tokio"
version = "1.54.1"
@@ -2688,95 +2549,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "writeable"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
[[package]]
name = "yoke"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3"
dependencies = [
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"synstructure",
]
[[package]]
name = "zerofrom"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
[[package]]
name = "zerotrie"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
]
[[package]]
name = "zerovec"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.11.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[package]]
name = "zmij"
version = "1.0.23"
+3
View File
@@ -40,6 +40,9 @@ chrono = { version = "0.4", default-features = false, features = ["std", "clock"
clap = { version = "4", features = ["derive", "wrap_help"] }
clap_complete = "4"
futures-util = { version = "0.3", default-features = false, features = ["std", "sink"] }
# The IDNA stub: `url` rejects non-ASCII hosts instead of pulling ICU4X (deviation A10-ascii-hosts). Pinned so a
# `cargo update` cannot bring 1.2 back.
idna_adapter = "=1.0.0"
indexmap = { version = "2", features = ["serde"] }
libloading = "0.9"
log = { version = "0.4", features = ["std"] }
+1
View File
@@ -14,6 +14,7 @@ opencode-api-types.workspace = true
base64.workspace = true
bytes.workspace = true
futures-util.workspace = true
idna_adapter.workspace = true
log.workspace = true
memchr.workspace = true
percent-encoding.workspace = true
+7 -2
View File
@@ -77,8 +77,13 @@ impl Client {
}
pub fn with_transport(base_url: &str, transport: Arc<dyn Transport>) -> Result<Self, Error> {
let mut base_url =
url::Url::parse(base_url).map_err(|error| Error::Transport(format!("{base_url}: {error}")))?;
let mut base_url = url::Url::parse(base_url).map_err(|error| match error {
// `url` is built without IDNA (deviation A10-ascii-hosts).
url::ParseError::IdnaError => Error::Transport(format!(
"{base_url}: a host name must be ASCII; write an international name in its punycode (xn--) form"
)),
error => Error::Transport(format!("{base_url}: {error}")),
})?;
// A leading slash would replace any path prefix on the base URL, so paths join relative to it.
if !base_url.path().ends_with('/') {
let path = format!("{}/", base_url.path());
+16
View File
@@ -44,6 +44,22 @@ fn client(script: &Arc<Script>) -> Client {
Client::with_transport("http://127.0.0.1:4096", script.clone()).unwrap()
}
#[test]
fn hosts_must_be_ascii_or_punycode() {
let script = Script::new(200, "application/json", "{}");
for url in ["https://example.com", "https://xn--bcher-kva.example"] {
let client = Client::with_transport(url, script.clone()).unwrap();
assert_eq!(client.base_url().as_str(), format!("{url}/"));
}
let Err(Error::Transport(message)) = Client::with_transport("https://bücher.example", script) else {
panic!("a Unicode host parsed");
};
assert_eq!(
message,
"https://bücher.example: a host name must be ASCII; write an international name in its punycode (xn--) form"
);
}
#[test]
fn tagged_union_decodes_known_rpc_and_unknown_members() {
let connected: V2Event =
@@ -0,0 +1,21 @@
# A10: URL hosts must be ASCII
TypeScript: `new URL` follows the WHATWG URL standard, which maps an international host name to
its punycode form with UTS 46. `https://bücher.example` parses as `https://xn--bcher-kva.example/`.
Rust: the `url` crate is built with the `idna_adapter` 1.0.0 stub (pinned in the root
`Cargo.toml`, used by `crates/api`), which turns IDNA off. A host with a non-ASCII character fails
to parse. A punycode host (`https://xn--bcher-kva.example`) and every ASCII host parse as before.
Where a user can type such a URL, the failure is reported:
- `--server` and any other base URL given to `opencode_api::Client`: "<url>: a host name must be
ASCII; write an international name in its punycode (xn--) form".
- MCP and form URL fields (`format: uri`): "Expected a URL", as for any URL that does not parse.
- Opening a link: "Only http and https links can be opened in the browser: <url>".
The stub also stops `url` from checking UTS 46 rules on punycode labels, so a malformed `xn--`
label parses where TypeScript rejects it; the server or DNS rejects it later.
Reason: decision O-58 (E1). Full IDNA pulls ICU4X into the build, 21 crates with 2 build scripts
and 4 proc macros, for host names that a user can write in punycode.