Skip to main content

netget/protocol/
client_registry.rs

1//! Client protocol registry
2//!
3//! This module provides a centralized registry that maps client protocol names
4//! to their client implementations. It enables trait-based client lookup
5//! and keyword-based parsing for client connections.
6
7use crate::llm::actions::Client;
8use std::collections::HashMap;
9use std::sync::Arc;
10
11/// Global client protocol registry mapping protocol names to client implementations
12pub struct ClientRegistry {
13    /// Maps protocol name (e.g., "TCP", "HTTP") to client implementation
14    protocols: HashMap<String, Arc<dyn Client>>,
15    /// Maps lowercase keywords to protocol name for fast parsing
16    keyword_map: HashMap<String, String>,
17}
18
19impl ClientRegistry {
20    /// Create a new client protocol registry
21    fn new() -> Self {
22        tracing::debug!("ClientRegistry::new() - Creating new client registry");
23        let mut registry = Self {
24            protocols: HashMap::new(),
25            keyword_map: HashMap::new(),
26        };
27
28        // Register every client protocol this platform builds
29        tracing::debug!("ClientRegistry::new() - Registering client protocols");
30        registry.register_protocols();
31        tracing::debug!("ClientRegistry::new() - Client protocols registered");
32
33        tracing::debug!("ClientRegistry::new() - Building keyword map");
34        registry.build_keyword_map();
35        tracing::debug!("ClientRegistry::new() - Keyword map built");
36
37        // Validate that no keywords overlap between protocols
38        tracing::debug!("ClientRegistry::new() - Validating keyword uniqueness");
39        registry.validate_keyword_uniqueness();
40        tracing::debug!("ClientRegistry::new() - Keyword uniqueness validated");
41
42        tracing::debug!("ClientRegistry::new() - Client registry created");
43        registry
44    }
45
46    /// Register all available client protocols based on compiled features
47    fn register_protocols(&mut self) {
48        #[cfg(all(unix, feature = "libpcap"))]
49        self.register(Arc::new(crate::client::arp::ArpClientProtocol::new()));
50
51        self.register(Arc::new(crate::client::bgp::BgpClientProtocol::new()));
52
53        self.register(Arc::new(
54            crate::client::bitcoin::BitcoinClientProtocol::new(),
55        ));
56        #[cfg(not(target_arch = "wasm32"))]
57        self.register(Arc::new(
58            crate::client::devp2p::actions::Devp2pClientProtocol::new(),
59        ));
60
61        #[cfg(all(unix, feature = "bluetooth"))]
62        self.register(Arc::new(
63            crate::client::bluetooth::BluetoothClientProtocol::new(),
64        ));
65
66        self.register(Arc::new(crate::client::bootp::BootpClientProtocol::new()));
67
68        #[cfg(not(target_arch = "wasm32"))]
69        self.register(Arc::new(
70            crate::client::cassandra::CassandraClientProtocol::new(),
71        ));
72
73        #[cfg(all(unix, feature = "libpcap"))]
74        self.register(Arc::new(
75            crate::client::datalink::DataLinkClientProtocol::new(),
76        ));
77
78        #[cfg(not(target_arch = "wasm32"))]
79        self.register(Arc::new(
80            crate::client::dc_peer::actions::DcPeerClientProtocol::new(),
81        ));
82
83        #[cfg(not(target_arch = "wasm32"))]
84        self.register(Arc::new(
85            crate::client::adc::actions::AdcClientProtocol::new(),
86        ));
87
88        #[cfg(not(target_arch = "wasm32"))]
89        self.register(Arc::new(
90            crate::client::adc_peer::actions::AdcPeerClientProtocol::new(),
91        ));
92
93        #[cfg(not(target_arch = "wasm32"))]
94        self.register(Arc::new(
95            crate::client::soulseek::actions::SoulseekClientProtocol::new(),
96        ));
97
98        #[cfg(not(target_arch = "wasm32"))]
99        self.register(Arc::new(
100            crate::client::soulseek_peer::actions::SoulseekPeerClientProtocol::new(),
101        ));
102
103        #[cfg(not(target_arch = "wasm32"))]
104        self.register(Arc::new(
105            crate::client::gnutella::actions::GnutellaClientProtocol::new(),
106        ));
107
108        self.register(Arc::new(crate::client::dc::DcClientProtocol::new()));
109
110        self.register(Arc::new(crate::client::dhcp::DhcpClientProtocol::new()));
111
112        #[cfg(not(target_arch = "wasm32"))]
113        self.register(Arc::new(crate::client::dns::DnsClientProtocol::new()));
114
115        #[cfg(not(target_arch = "wasm32"))]
116        self.register(Arc::new(crate::client::doh::DohClientProtocol::new()));
117
118        #[cfg(not(target_arch = "wasm32"))]
119        self.register(Arc::new(
120            crate::client::doq::actions::DoqClientProtocol::new(),
121        ));
122
123        #[cfg(not(target_arch = "wasm32"))]
124        self.register(Arc::new(crate::client::dot::DotClientProtocol::new()));
125
126        #[cfg(unix)]
127        self.register(Arc::new(
128            crate::client::dynamodb::DynamoDbClientProtocol::new(),
129        ));
130
131        self.register(Arc::new(
132            crate::client::elasticsearch::ElasticsearchClientProtocol::new(),
133        ));
134
135        #[cfg(not(target_arch = "wasm32"))]
136        self.register(Arc::new(crate::client::etcd::EtcdClientProtocol::new()));
137
138        #[cfg(not(target_arch = "wasm32"))]
139        self.register(Arc::new(crate::client::git::GitClientProtocol::new()));
140
141        #[cfg(not(target_arch = "wasm32"))]
142        self.register(Arc::new(
143            crate::client::zookeeper::ZookeeperClientProtocol::new(),
144        ));
145
146        #[cfg(not(target_arch = "wasm32"))]
147        self.register(Arc::new(crate::client::grpc::GrpcClientProtocol::new()));
148        #[cfg(not(target_arch = "wasm32"))]
149        self.register(Arc::new(
150            crate::client::gnmi::actions::GnmiClientProtocol::new(),
151        ));
152        #[cfg(not(target_arch = "wasm32"))]
153        self.register(Arc::new(
154            crate::client::grpc_web::actions::GrpcWebClientProtocol::new(),
155        ));
156        #[cfg(not(target_arch = "wasm32"))]
157        self.register(Arc::new(
158            crate::client::connect_rpc::actions::ConnectRpcClientProtocol::new(),
159        ));
160
161        {
162            tracing::debug!("ClientRegistry: Registering HTTP client protocol");
163            self.register(Arc::new(crate::client::http::HttpClientProtocol::new()));
164            tracing::debug!("ClientRegistry: HTTP client protocol registered");
165        }
166
167        self.register(Arc::new(crate::client::http2::Http2ClientProtocol::new()));
168
169        // Raw QUIC and RFC 9114 HTTP/3 have distinct client/server registrations.
170        #[cfg(not(target_arch = "wasm32"))]
171        self.register(Arc::new(
172            crate::client::quic::actions::QuicClientProtocol::new(),
173        ));
174
175        #[cfg(not(target_arch = "wasm32"))]
176        self.register(Arc::new(crate::client::http3::Http3ClientProtocol::new()));
177
178        #[cfg(unix)]
179        self.register(Arc::new(
180            crate::client::http_proxy::HttpProxyClientProtocol::new(),
181        ));
182
183        #[cfg(unix)]
184        self.register(Arc::new(crate::client::igmp::IgmpClientProtocol::new()));
185
186        #[cfg(not(target_arch = "wasm32"))]
187        self.register(Arc::new(crate::client::imap::ImapClientProtocol::new()));
188
189        #[cfg(not(target_arch = "wasm32"))]
190        self.register(Arc::new(crate::client::ipp::IppClientProtocol::new()));
191
192        #[cfg(not(target_arch = "wasm32"))]
193        self.register(Arc::new(crate::client::irc::IrcClientProtocol::new()));
194
195        #[cfg(all(unix, feature = "libpcap"))]
196        self.register(Arc::new(crate::client::isis::IsisClientProtocol::new()));
197
198        self.register(Arc::new(
199            crate::client::jsonrpc::JsonRpcClientProtocol::new(),
200        ));
201
202        self.register(Arc::new(crate::client::kafka::KafkaClientProtocol::new()));
203
204        #[cfg(not(target_arch = "wasm32"))]
205        self.register(Arc::new(
206            crate::client::kubernetes::KubernetesClientProtocol::new(),
207        ));
208
209        #[cfg(not(target_arch = "wasm32"))]
210        self.register(Arc::new(crate::client::ldap::LdapClientProtocol::new()));
211
212        self.register(Arc::new(crate::client::maven::MavenClientProtocol::new()));
213
214        #[cfg(not(target_arch = "wasm32"))]
215        self.register(Arc::new(crate::client::mcp::McpClientProtocol::new()));
216
217        #[cfg(not(target_arch = "wasm32"))]
218        self.register(Arc::new(crate::client::mdns::MdnsClientProtocol::new()));
219
220        #[cfg(not(target_arch = "wasm32"))]
221        self.register(Arc::new(crate::client::mqtt::MqttClientProtocol::new()));
222
223        #[cfg(not(target_arch = "wasm32"))]
224        self.register(Arc::new(crate::client::amqp::AmqpClientProtocol::new()));
225
226        #[cfg(not(target_arch = "wasm32"))]
227        self.register(Arc::new(crate::client::mysql::MysqlClientProtocol::new()));
228
229        #[cfg(not(target_arch = "wasm32"))]
230        self.register(Arc::new(
231            crate::client::mongodb::MongodbClientProtocol::new(),
232        ));
233
234        #[cfg(unix)]
235        self.register(Arc::new(crate::client::nfs::NfsClientProtocol::new()));
236
237        #[cfg(all(unix, feature = "pcsc"))]
238        self.register(Arc::new(crate::client::nfc::NfcClientProtocol));
239
240        self.register(Arc::new(crate::client::nntp::NntpClientProtocol::new()));
241
242        self.register(Arc::new(crate::client::npm::NpmClientProtocol::new()));
243
244        #[cfg(not(target_arch = "wasm32"))]
245        self.register(Arc::new(crate::client::ntp::NtpClientProtocol::new()));
246
247        self.register(Arc::new(crate::client::oauth2::OAuth2ClientProtocol::new()));
248
249        #[cfg(not(target_arch = "wasm32"))]
250        self.register(Arc::new(crate::client::openai::OpenAiClientProtocol::new()));
251
252        self.register(Arc::new(crate::client::ollama::OllamaClientProtocol::new()));
253
254        self.register(Arc::new(
255            crate::client::openapi::OpenApiClientProtocol::new(),
256        ));
257
258        self.register(Arc::new(
259            crate::client::openidconnect::OpenIdConnectClientProtocol::new(),
260        ));
261
262        #[cfg(unix)]
263        self.register(Arc::new(crate::client::ospf::OspfClientProtocol::new()));
264
265        #[cfg(not(target_arch = "wasm32"))]
266        self.register(Arc::new(
267            crate::client::otlp::actions::OtlpClientProtocol::new(),
268        ));
269
270        #[cfg(not(target_arch = "wasm32"))]
271        self.register(Arc::new(
272            crate::client::postgresql::PostgresqlClientProtocol::new(),
273        ));
274
275        self.register(Arc::new(crate::client::pypi::PypiClientProtocol::new()));
276
277        #[cfg(not(target_arch = "wasm32"))]
278        self.register(Arc::new(crate::client::mssql::MssqlClientProtocol::new()));
279
280        #[cfg(not(target_arch = "wasm32"))]
281        self.register(Arc::new(crate::client::redis::RedisClientProtocol::new()));
282
283        #[cfg(unix)]
284        self.register(Arc::new(crate::client::icmp::IcmpClientProtocol::new()));
285
286        #[cfg(not(target_arch = "wasm32"))]
287        self.register(Arc::new(
288            crate::client::couchdb::CouchDbClientProtocol::new(),
289        ));
290
291        self.register(Arc::new(crate::client::tftp::TftpClientProtocol::new()));
292
293        self.register(Arc::new(crate::client::rss::RssClientProtocol::new()));
294
295        #[cfg(not(target_arch = "wasm32"))]
296        self.register(Arc::new(crate::client::rip::RipClientProtocol::new()));
297
298        #[cfg(not(target_arch = "wasm32"))]
299        self.register(Arc::new(crate::client::s3::S3ClientProtocol::new()));
300
301        self.register(Arc::new(crate::client::saml::SamlClientProtocol::new()));
302
303        self.register(Arc::new(crate::client::sip::SipClientProtocol::new()));
304        #[cfg(not(target_arch = "wasm32"))]
305        self.register(Arc::new(
306            crate::client::h323::actions::H323ClientProtocol::new(),
307        ));
308
309        #[cfg(all(unix, feature = "libsmbclient"))]
310        self.register(Arc::new(crate::client::smb::SmbClientProtocol::new()));
311
312        #[cfg(not(target_arch = "wasm32"))]
313        self.register(Arc::new(crate::client::smtp::SmtpClientProtocol::new()));
314
315        self.register(Arc::new(crate::client::ftp::FtpClientProtocol::new()));
316
317        self.register(Arc::new(crate::client::pop3::Pop3ClientProtocol::new()));
318
319        self.register(Arc::new(crate::client::snmp::SnmpClientProtocol::new()));
320
321        #[cfg(not(target_arch = "wasm32"))]
322        self.register(Arc::new(crate::client::socks5::Socks5ClientProtocol::new()));
323
324        #[cfg(unix)]
325        self.register(Arc::new(
326            crate::client::socket_file::SocketFileClientProtocol::new(),
327        ));
328
329        #[cfg(not(target_arch = "wasm32"))]
330        self.register(Arc::new(crate::client::sqs::SqsClientProtocol::new()));
331
332        #[cfg(not(target_arch = "wasm32"))]
333        self.register(Arc::new(crate::client::ssh::SshClientProtocol::new()));
334
335        #[cfg(unix)]
336        self.register(Arc::new(
337            crate::client::ssh_agent::SshAgentClientProtocol::new(),
338        ));
339
340        #[cfg(unix)]
341        self.register(Arc::new(crate::client::stun::StunClientProtocol::new()));
342
343        #[cfg(not(target_arch = "wasm32"))]
344        self.register(Arc::new(
345            crate::client::diameter::actions::DiameterClientProtocol::new(),
346        ));
347
348        #[cfg(not(target_arch = "wasm32"))]
349        self.register(Arc::new(
350            crate::client::tacacs::actions::TacacsClientProtocol::new(),
351        ));
352
353        #[cfg(not(target_arch = "wasm32"))]
354        self.register(Arc::new(
355            crate::client::netconf::actions::NetconfClientProtocol::new(),
356        ));
357
358        #[cfg(not(target_arch = "wasm32"))]
359        self.register(Arc::new(
360            crate::client::rpki_rtr::actions::RpkiRtrClientProtocol::new(),
361        ));
362
363        #[cfg(not(target_arch = "wasm32"))]
364        self.register(Arc::new(
365            crate::client::rdap::actions::RdapClientProtocol::new(),
366        ));
367
368        #[cfg(not(target_arch = "wasm32"))]
369        self.register(Arc::new(
370            crate::client::hl7::actions::Hl7ClientProtocol::new(),
371        ));
372
373        #[cfg(not(target_arch = "wasm32"))]
374        self.register(Arc::new(
375            crate::client::icap::actions::IcapClientProtocol::new(),
376        ));
377
378        #[cfg(not(target_arch = "wasm32"))]
379        self.register(Arc::new(
380            crate::client::s7comm::actions::S7commClientProtocol::new(),
381        ));
382
383        #[cfg(not(target_arch = "wasm32"))]
384        self.register(Arc::new(
385            crate::client::ethernet_ip::actions::EthernetIpClientProtocol::new(),
386        ));
387
388        #[cfg(not(target_arch = "wasm32"))]
389        self.register(Arc::new(
390            crate::client::dnp3::actions::Dnp3ClientProtocol::new(),
391        ));
392
393        #[cfg(not(target_arch = "wasm32"))]
394        self.register(Arc::new(
395            crate::client::iec104::actions::Iec104ClientProtocol::new(),
396        ));
397
398        #[cfg(not(target_arch = "wasm32"))]
399        self.register(Arc::new(
400            crate::client::bacnet::actions::BacnetClientProtocol::new(),
401        ));
402
403        #[cfg(not(target_arch = "wasm32"))]
404        self.register(Arc::new(
405            crate::client::opcua::actions::OpcuaClientProtocol::new(),
406        ));
407
408        #[cfg(not(target_arch = "wasm32"))]
409        self.register(Arc::new(
410            crate::client::ocpp::actions::OcppClientProtocol::new(),
411        ));
412
413        #[cfg(not(target_arch = "wasm32"))]
414        self.register(Arc::new(
415            crate::client::a2a::actions::A2aClientProtocol::new(),
416        ));
417
418        #[cfg(not(target_arch = "wasm32"))]
419        self.register(Arc::new(
420            crate::client::graphql::actions::GraphqlClientProtocol::new(),
421        ));
422
423        #[cfg(not(target_arch = "wasm32"))]
424        self.register(Arc::new(
425            crate::client::fastcgi::actions::FastcgiClientProtocol::new(),
426        ));
427
428        #[cfg(not(target_arch = "wasm32"))]
429        self.register(Arc::new(
430            crate::client::redfish::actions::RedfishClientProtocol::new(),
431        ));
432
433        #[cfg(not(target_arch = "wasm32"))]
434        self.register(Arc::new(
435            crate::client::scim::actions::ScimClientProtocol::new(),
436        ));
437
438        #[cfg(not(target_arch = "wasm32"))]
439        self.register(Arc::new(
440            crate::client::socketio::actions::SocketIoClientProtocol::new(),
441        ));
442
443        #[cfg(not(target_arch = "wasm32"))]
444        self.register(Arc::new(
445            crate::client::caldav::actions::CalDavClientProtocol::new(),
446        ));
447
448        #[cfg(not(target_arch = "wasm32"))]
449        self.register(Arc::new(
450            crate::client::carddav::actions::CardDavClientProtocol::new(),
451        ));
452
453        #[cfg(not(target_arch = "wasm32"))]
454        self.register(Arc::new(
455            crate::client::dicom::actions::DicomClientProtocol::new(),
456        ));
457
458        #[cfg(not(target_arch = "wasm32"))]
459        self.register(Arc::new(
460            crate::client::acme::actions::AcmeClientProtocol::new(),
461        ));
462
463        #[cfg(not(target_arch = "wasm32"))]
464        self.register(Arc::new(
465            crate::client::fix::actions::FixClientProtocol::new(),
466        ));
467
468        #[cfg(not(target_arch = "wasm32"))]
469        self.register(Arc::new(
470            crate::client::wamp::actions::WampClientProtocol::new(),
471        ));
472
473        #[cfg(not(target_arch = "wasm32"))]
474        self.register(Arc::new(
475            crate::client::rtmp::actions::RtmpClientProtocol::new(),
476        ));
477
478        #[cfg(not(target_arch = "wasm32"))]
479        self.register(Arc::new(
480            crate::client::srt::actions::SrtClientProtocol::new(),
481        ));
482
483        #[cfg(not(target_arch = "wasm32"))]
484        self.register(Arc::new(
485            crate::client::amqp1::actions::Amqp1ClientProtocol::new(),
486        ));
487
488        #[cfg(not(target_arch = "wasm32"))]
489        self.register(Arc::new(
490            crate::client::thrift::actions::ThriftClientProtocol::new(),
491        ));
492        #[cfg(not(target_arch = "wasm32"))]
493        self.register(Arc::new(
494            crate::client::bmp::actions::BmpClientProtocol::new(),
495        ));
496        #[cfg(not(target_arch = "wasm32"))]
497        self.register(Arc::new(
498            crate::client::mqtt_sn::actions::MqttSnClientProtocol::new(),
499        ));
500        #[cfg(not(target_arch = "wasm32"))]
501        self.register(Arc::new(
502            crate::client::nbd::actions::NbdClientProtocol::new(),
503        ));
504        #[cfg(not(target_arch = "wasm32"))]
505        self.register(Arc::new(
506            crate::client::managesieve::actions::ManageSieveClientProtocol::new(),
507        ));
508        #[cfg(not(target_arch = "wasm32"))]
509        self.register(Arc::new(
510            crate::client::zenoh::actions::ZenohClientProtocol::new(),
511        ));
512        #[cfg(not(target_arch = "wasm32"))]
513        self.register(Arc::new(
514            crate::client::lwm2m::actions::Lwm2mClientProtocol::new(),
515        ));
516        #[cfg(not(target_arch = "wasm32"))]
517        self.register(Arc::new(
518            crate::client::restconf::actions::RestconfClientProtocol::new(),
519        ));
520        #[cfg(not(target_arch = "wasm32"))]
521        self.register(Arc::new(
522            crate::client::webtransport::actions::WebTransportClientProtocol::new(),
523        ));
524        #[cfg(not(target_arch = "wasm32"))]
525        self.register(Arc::new(
526            crate::client::jmap::actions::JmapClientProtocol::new(),
527        ));
528        #[cfg(not(target_arch = "wasm32"))]
529        self.register(Arc::new(
530            crate::client::epp::actions::EppClientProtocol::new(),
531        ));
532        #[cfg(not(target_arch = "wasm32"))]
533        self.register(Arc::new(
534            crate::client::lmtp::actions::LmtpClientProtocol::new(),
535        ));
536        #[cfg(not(target_arch = "wasm32"))]
537        self.register(Arc::new(
538            crate::client::lpd::actions::LpdClientProtocol::new(),
539        ));
540        #[cfg(not(target_arch = "wasm32"))]
541        self.register(Arc::new(
542            crate::client::inetd::actions::EchoClientProtocol::new(),
543        ));
544        #[cfg(not(target_arch = "wasm32"))]
545        self.register(Arc::new(
546            crate::client::inetd::actions::DiscardClientProtocol::new(),
547        ));
548        #[cfg(not(target_arch = "wasm32"))]
549        self.register(Arc::new(
550            crate::client::inetd::actions::DaytimeClientProtocol::new(),
551        ));
552        #[cfg(not(target_arch = "wasm32"))]
553        self.register(Arc::new(
554            crate::client::inetd::actions::QotdClientProtocol::new(),
555        ));
556        #[cfg(not(target_arch = "wasm32"))]
557        self.register(Arc::new(
558            crate::client::inetd::actions::ChargenClientProtocol::new(),
559        ));
560        #[cfg(not(target_arch = "wasm32"))]
561        self.register(Arc::new(
562            crate::client::inetd::actions::TimeClientProtocol::new(),
563        ));
564        #[cfg(not(target_arch = "wasm32"))]
565        self.register(Arc::new(
566            crate::client::rcon::actions::RconClientProtocol::new(),
567        ));
568        #[cfg(not(target_arch = "wasm32"))]
569        self.register(Arc::new(
570            crate::client::a2s::actions::A2sClientProtocol::new(),
571        ));
572        #[cfg(not(target_arch = "wasm32"))]
573        self.register(Arc::new(
574            crate::client::minecraft::actions::MinecraftClientProtocol::new(),
575        ));
576        #[cfg(not(target_arch = "wasm32"))]
577        self.register(Arc::new(
578            crate::client::ninep::actions::NinepClientProtocol::new(),
579        ));
580        #[cfg(not(target_arch = "wasm32"))]
581        self.register(Arc::new(
582            crate::client::zeromq::actions::ZeromqClientProtocol::new(),
583        ));
584        #[cfg(not(target_arch = "wasm32"))]
585        self.register(Arc::new(
586            crate::client::smpp::actions::SmppClientProtocol::new(),
587        ));
588        #[cfg(not(target_arch = "wasm32"))]
589        self.register(Arc::new(
590            crate::client::clickhouse::actions::ClickhouseClientProtocol::new(),
591        ));
592        #[cfg(not(target_arch = "wasm32"))]
593        self.register(Arc::new(
594            crate::client::msgpack_rpc::actions::MsgpackRpcClientProtocol::new(),
595        ));
596        #[cfg(not(target_arch = "wasm32"))]
597        self.register(Arc::new(
598            crate::client::zipkin::actions::ZipkinClientProtocol::new(),
599        ));
600        #[cfg(not(target_arch = "wasm32"))]
601        self.register(Arc::new(
602            crate::client::capnp_rpc::actions::CapnpRpcClientProtocol::new(),
603        ));
604        #[cfg(not(target_arch = "wasm32"))]
605        self.register(Arc::new(
606            crate::client::knx::actions::KnxClientProtocol::new(),
607        ));
608        #[cfg(not(target_arch = "wasm32"))]
609        self.register(Arc::new(
610            crate::client::consul::actions::ConsulClientProtocol::new(),
611        ));
612        #[cfg(not(target_arch = "wasm32"))]
613        self.register(Arc::new(
614            crate::client::milter::actions::MilterClientProtocol::new(),
615        ));
616        #[cfg(not(target_arch = "wasm32"))]
617        self.register(Arc::new(
618            crate::client::anthropic::actions::AnthropicClientProtocol::new(),
619        ));
620        #[cfg(not(target_arch = "wasm32"))]
621        self.register(Arc::new(
622            crate::client::radsec::actions::RadsecClientProtocol::new(),
623        ));
624        #[cfg(not(target_arch = "wasm32"))]
625        self.register(Arc::new(
626            crate::client::dbus::actions::DbusClientProtocol::new(),
627        ));
628        #[cfg(not(target_arch = "wasm32"))]
629        self.register(Arc::new(
630            crate::client::bfd::actions::BfdClientProtocol::new(),
631        ));
632        #[cfg(not(target_arch = "wasm32"))]
633        self.register(Arc::new(
634            crate::client::vxlan::actions::VxlanClientProtocol::new(),
635        ));
636        #[cfg(not(target_arch = "wasm32"))]
637        self.register(Arc::new(
638            crate::client::rsync::actions::RsyncClientProtocol::new(),
639        ));
640        #[cfg(unix)]
641        self.register(Arc::new(
642            crate::client::rdp::actions::RdpClientProtocol::new(),
643        ));
644        #[cfg(not(target_arch = "wasm32"))]
645        self.register(Arc::new(
646            crate::client::spice::actions::SpiceClientProtocol::new(),
647        ));
648        #[cfg(unix)]
649        self.register(Arc::new(
650            crate::client::m3ua::actions::M3uaClientProtocol::new(),
651        ));
652        #[cfg(unix)]
653        self.register(Arc::new(
654            crate::client::ngap::actions::NgapClientProtocol::new(),
655        ));
656        #[cfg(not(target_arch = "wasm32"))]
657        self.register(Arc::new(
658            crate::client::pfcp::actions::PfcpClientProtocol::new(),
659        ));
660        self.register(Arc::new(
661            crate::client::gtp::actions::GtpClientProtocol::new(),
662        ));
663        self.register(Arc::new(
664            crate::client::db2::actions::Db2ClientProtocol::new(),
665        ));
666        self.register(Arc::new(
667            crate::client::snowflake::actions::SnowflakeClientProtocol::new(),
668        ));
669        self.register(Arc::new(
670            crate::client::spark::actions::SparkClientProtocol::new(),
671        ));
672        self.register(Arc::new(
673            crate::client::yarn::actions::YarnClientProtocol::new(),
674        ));
675        #[cfg(not(target_arch = "wasm32"))]
676        self.register(Arc::new(
677            crate::client::matrix::actions::MatrixClientProtocol::new(),
678        ));
679        #[cfg(not(target_arch = "wasm32"))]
680        self.register(Arc::new(
681            crate::client::libp2p::actions::Libp2pClientProtocol::new(),
682        ));
683        #[cfg(not(target_arch = "wasm32"))]
684        self.register(Arc::new(
685            crate::client::sunrpc::actions::SunRpcClientProtocol::new(),
686        ));
687        #[cfg(not(target_arch = "wasm32"))]
688        self.register(Arc::new(
689            crate::client::guacamole::actions::GuacamoleClientProtocol::new(),
690        ));
691        #[cfg(not(target_arch = "wasm32"))]
692        self.register(Arc::new(
693            crate::client::activitypub::actions::ActivityPubClientProtocol::new(),
694        ));
695        #[cfg(not(target_arch = "wasm32"))]
696        self.register(Arc::new(
697            crate::client::stratum::actions::StratumClientProtocol::new(),
698        ));
699        #[cfg(not(target_arch = "wasm32"))]
700        self.register(Arc::new(
701            crate::client::stratum_v2::actions::StratumV2ClientProtocol::new(),
702        ));
703        #[cfg(not(target_arch = "wasm32"))]
704        self.register(Arc::new(
705            crate::client::pulsar::actions::PulsarClientProtocol::new(),
706        ));
707        #[cfg(not(target_arch = "wasm32"))]
708        self.register(Arc::new(
709            crate::client::tr069::actions::Tr069ClientProtocol::new(),
710        ));
711        #[cfg(not(target_arch = "wasm32"))]
712        self.register(Arc::new(
713            crate::client::sse::actions::SseClientProtocol::new(),
714        ));
715        #[cfg(not(target_arch = "wasm32"))]
716        self.register(Arc::new(
717            crate::client::zabbix::actions::ZabbixClientProtocol::new(),
718        ));
719        self.register(Arc::new(
720            crate::client::rtp::actions::RtpClientProtocol::new(),
721        ));
722        self.register(Arc::new(
723            crate::client::rtsp::actions::RtspClientProtocol::new(),
724        ));
725        self.register(Arc::new(
726            crate::client::hls::actions::HlsClientProtocol::new(),
727        ));
728        #[cfg(not(target_arch = "wasm32"))]
729        self.register(Arc::new(
730            crate::client::wsdiscovery::actions::WsDiscoveryClientProtocol::new(),
731        ));
732        #[cfg(not(target_arch = "wasm32"))]
733        self.register(Arc::new(
734            crate::client::onvif::actions::OnvifClientProtocol::new(),
735        ));
736        #[cfg(not(target_arch = "wasm32"))]
737        self.register(Arc::new(
738            crate::client::kerberos::actions::KerberosClientProtocol::new(),
739        ));
740        #[cfg(not(target_arch = "wasm32"))]
741        self.register(Arc::new(
742            crate::client::x11::actions::X11ClientProtocol::new(),
743        ));
744
745        #[cfg(not(target_arch = "wasm32"))]
746        self.register(Arc::new(
747            crate::client::netflow_v9::actions::NetflowV9ClientProtocol::new(),
748        ));
749        #[cfg(not(target_arch = "wasm32"))]
750        self.register(Arc::new(
751            crate::client::ipfix::actions::IpfixClientProtocol::new(),
752        ));
753        #[cfg(not(target_arch = "wasm32"))]
754        self.register(Arc::new(
755            crate::client::loki::actions::LokiClientProtocol::new(),
756        ));
757        #[cfg(not(target_arch = "wasm32"))]
758        self.register(Arc::new(
759            crate::client::influxdb::actions::InfluxDbClientProtocol::new(),
760        ));
761        #[cfg(not(target_arch = "wasm32"))]
762        self.register(Arc::new(crate::client::prometheus_remote_write::actions::PrometheusRemoteWriteClientProtocol::new()));
763        #[cfg(not(target_arch = "wasm32"))]
764        self.register(Arc::new(
765            crate::client::sflow::actions::SflowClientProtocol::new(),
766        ));
767
768        #[cfg(not(target_arch = "wasm32"))]
769        self.register(Arc::new(
770            crate::client::fluent_forward::actions::FluentForwardClientProtocol::new(),
771        ));
772
773        #[cfg(not(target_arch = "wasm32"))]
774        self.register(Arc::new(
775            crate::client::gelf::actions::GelfClientProtocol::new(),
776        ));
777
778        #[cfg(not(target_arch = "wasm32"))]
779        self.register(Arc::new(
780            crate::client::graphite::actions::GraphiteClientProtocol::new(),
781        ));
782
783        #[cfg(not(target_arch = "wasm32"))]
784        self.register(Arc::new(
785            crate::client::statsd::actions::StatsdClientProtocol::new(),
786        ));
787
788        self.register(Arc::new(crate::client::syslog::SyslogClientProtocol::new()));
789
790        self.register(Arc::new(crate::client::tcp::TcpClientProtocol::new()));
791
792        self.register(Arc::new(crate::client::telnet::TelnetClientProtocol::new()));
793
794        self.register(Arc::new(crate::client::tls::TlsClientProtocol::new()));
795
796        #[cfg(unix)]
797        self.register(Arc::new(crate::client::tor::TorClientProtocol::new()));
798
799        self.register(Arc::new(
800            crate::client::torrent_dht::TorrentDhtClientProtocol::new(),
801        ));
802
803        self.register(Arc::new(
804            crate::client::torrent_peer::TorrentPeerClientProtocol::new(),
805        ));
806
807        self.register(Arc::new(
808            crate::client::torrent_tracker::TorrentTrackerClientProtocol::new(),
809        ));
810
811        #[cfg(unix)]
812        self.register(Arc::new(crate::client::turn::TurnClientProtocol::new()));
813
814        self.register(Arc::new(crate::client::udp::UdpClientProtocol::new()));
815
816        #[cfg(not(target_arch = "wasm32"))]
817        self.register(Arc::new(crate::client::usb::UsbClientProtocol::new()));
818
819        self.register(Arc::new(crate::client::vnc::VncClientProtocol::new()));
820
821        #[cfg(not(target_arch = "wasm32"))]
822        self.register(Arc::new(crate::client::webdav::WebdavClientProtocol::new()));
823
824        #[cfg(not(target_arch = "wasm32"))]
825        self.register(Arc::new(crate::client::webrtc::WebRtcClientProtocol::new()));
826
827        #[cfg(not(target_arch = "wasm32"))]
828        self.register(Arc::new(
829            crate::client::websocket::WebSocketClientProtocol::new(),
830        ));
831
832        #[cfg(not(target_arch = "wasm32"))]
833        self.register(Arc::new(crate::client::gemini::GeminiClientProtocol::new()));
834
835        #[cfg(not(target_arch = "wasm32"))]
836        self.register(Arc::new(crate::client::dict::DictClientProtocol::new()));
837
838        #[cfg(not(target_arch = "wasm32"))]
839        self.register(Arc::new(
840            crate::client::beanstalkd::BeanstalkdClientProtocol::new(),
841        ));
842
843        #[cfg(not(target_arch = "wasm32"))]
844        self.register(Arc::new(crate::client::vault::VaultClientProtocol::new()));
845
846        #[cfg(not(target_arch = "wasm32"))]
847        self.register(Arc::new(crate::client::bolt::BoltClientProtocol::new()));
848
849        self.register(Arc::new(
850            crate::client::oci_registry::OciRegistryClientProtocol::new(),
851        ));
852
853        #[cfg(not(target_arch = "wasm32"))]
854        self.register(Arc::new(crate::client::nostr::NostrClientProtocol::new()));
855
856        #[cfg(not(target_arch = "wasm32"))]
857        self.register(Arc::new(
858            crate::client::prometheus::PrometheusClientProtocol::new(),
859        ));
860        #[cfg(not(target_arch = "wasm32"))]
861        self.register(Arc::new(crate::client::docker::DockerClientProtocol::new()));
862
863        #[cfg(not(target_arch = "wasm32"))]
864        self.register(Arc::new(
865            crate::client::gearman::GearmanClientProtocol::new(),
866        ));
867
868        #[cfg(not(target_arch = "wasm32"))]
869        self.register(Arc::new(crate::client::nsq::NsqClientProtocol::new()));
870
871        #[cfg(not(target_arch = "wasm32"))]
872        self.register(Arc::new(
873            crate::client::nut::actions::NutClientProtocol::new(),
874        ));
875
876        self.register(Arc::new(crate::client::whois::WhoisClientProtocol::new()));
877        self.register(Arc::new(crate::client::stomp::StompClientProtocol::new()));
878        self.register(Arc::new(
879            crate::client::netbios_ns::NetbiosNsClientProtocol::new(),
880        ));
881        self.register(Arc::new(crate::client::nats::NatsClientProtocol::new()));
882        self.register(Arc::new(
883            crate::client::memcached::MemcachedClientProtocol::new(),
884        ));
885        self.register(Arc::new(crate::client::modbus::ModbusClientProtocol::new()));
886        self.register(Arc::new(crate::client::coap::CoapClientProtocol::new()));
887        self.register(Arc::new(crate::client::radius::RadiusClientProtocol::new()));
888        self.register(Arc::new(crate::client::ssdp::SsdpClientProtocol::new()));
889        self.register(Arc::new(crate::client::gopher::GopherClientProtocol::new()));
890        self.register(Arc::new(crate::client::finger::FingerClientProtocol::new()));
891        self.register(Arc::new(crate::client::ident::IdentClientProtocol::new()));
892        #[cfg(not(target_arch = "wasm32"))]
893        self.register(Arc::new(crate::client::llmnr::LlmnrClientProtocol::new()));
894
895        #[cfg(unix)]
896        self.register(Arc::new(
897            crate::client::wireguard::WireguardClientProtocol::new(),
898        ));
899
900        #[cfg(not(target_arch = "wasm32"))]
901        self.register(Arc::new(crate::client::xmlrpc::XmlRpcClientProtocol::new()));
902
903        #[cfg(unix)]
904        self.register(Arc::new(crate::client::xmpp::XmppClientProtocol::new()));
905    }
906
907    /// Build keyword map for fast protocol parsing
908    fn build_keyword_map(&mut self) {
909        for (protocol_name, protocol) in &self.protocols {
910            // Add all protocol keywords
911            for keyword in protocol.keywords() {
912                let lower = keyword.to_lowercase();
913                // Store the hyphen/space-normalized form too. Input is normalized
914                // before lookup, so a keyword declared as "ssh-agent" was previously
915                // unreachable: "ssh_agent" never contains "ssh-agent".
916                let normalized = lower.replace(['-', ' '], "_");
917                self.keyword_map.insert(lower, protocol_name.clone());
918                self.keyword_map.insert(normalized, protocol_name.clone());
919            }
920
921            // Also add the full stack name as a keyword
922            // This allows parsing inputs like "eth>ip>tcp>http" or "ETH>IP>UDP>DNS"
923            let stack_name = protocol.stack_name().to_lowercase();
924            self.keyword_map.insert(stack_name, protocol_name.clone());
925        }
926    }
927
928    /// Validate that no two protocols share the same keyword
929    ///
930    /// This ensures keyword uniqueness across all registered protocols.
931    /// Panics if overlapping keywords are detected.
932    fn validate_keyword_uniqueness(&self) {
933        use std::collections::HashMap;
934
935        // Build a map: keyword (lowercase) -> Vec<(protocol_name, keyword_source)>
936        // keyword_source is either "keyword" or "stack_name"
937        let mut keyword_to_protocols: HashMap<String, Vec<(String, String)>> = HashMap::new();
938
939        for (protocol_name, protocol) in &self.protocols {
940            // Collect all keywords from keywords()
941            for keyword in protocol.keywords() {
942                let key = keyword.to_lowercase();
943                keyword_to_protocols
944                    .entry(key)
945                    .or_default()
946                    .push((protocol_name.clone(), format!("keyword '{}'", keyword)));
947            }
948
949            // Also collect the stack name as a keyword
950            let stack_name = protocol.stack_name();
951            let key = stack_name.to_lowercase();
952            keyword_to_protocols.entry(key).or_default().push((
953                protocol_name.clone(),
954                format!("stack_name '{}'", stack_name),
955            ));
956        }
957
958        // Only count overlaps across DISTINCT protocols — a protocol whose keyword
959        // equals its own stack_name contributes two entries for one keyword and is
960        // not an overlap.
961        let mut overlaps = Vec::new();
962        for (keyword, protocols) in &keyword_to_protocols {
963            let distinct: std::collections::HashSet<&String> =
964                protocols.iter().map(|(name, _)| name).collect();
965            if distinct.len() > 1 {
966                overlaps.push((keyword.clone(), protocols.clone()));
967            }
968        }
969
970        // Development-time advisory only — log at DEBUG (netget.log) rather than
971        // spamming the TUI on every startup. `test_keyword_overlaps` is the gate.
972        if !overlaps.is_empty() {
973            use tracing::debug;
974            debug!("Client keyword overlaps detected between protocols:");
975
976            for (keyword, protocols) in &overlaps {
977                debug!("  Keyword '{}' is used by:", keyword);
978                for (protocol_name, source) in protocols {
979                    debug!("    - {} ({})", protocol_name, source);
980                }
981            }
982
983            debug!("Note: Each keyword should ideally be unique to a single protocol.");
984            debug!(
985                "      Run 'cargo test test_keyword_overlaps -- --ignored' to see all overlaps."
986            );
987        }
988    }
989
990    /// Register a client protocol implementation
991    #[allow(dead_code)]
992    fn register(&mut self, protocol: Arc<dyn Client>) {
993        let protocol_name = protocol.protocol_name().to_string();
994        self.protocols.insert(protocol_name, protocol);
995    }
996
997    /// Get client protocol implementation by protocol name
998    pub fn get(&self, protocol_name: &str) -> Option<Arc<dyn Client>> {
999        self.protocols.get(protocol_name).cloned()
1000    }
1001
1002    /// Resolve user/LLM-supplied input (a protocol name or keyword) to a
1003    /// registered client protocol implementation.
1004    ///
1005    /// Mirrors `ServerRegistry::resolve()`: unlike calling `get()` /
1006    /// `parse_from_str()` directly, a failure here distinguishes "this
1007    /// protocol exists but wasn't compiled into this build" from "this is not
1008    /// a real NetGet client protocol at all", and offers a "did you mean"
1009    /// suggestion for likely typos — see [`ClientProtocolLookupError`].
1010    pub fn resolve(&self, input: &str) -> Result<Arc<dyn Client>, ClientProtocolLookupError> {
1011        // Exact canonical name first — handles a name already resolved by a
1012        // previous call.
1013        if let Some(p) = self.get(input) {
1014            return Ok(p);
1015        }
1016
1017        // Fall back to keyword / stack-name parsing (case-insensitive,
1018        // hyphen/space tolerant) against currently-compiled protocols.
1019        if let Some(name) = self.parse_from_str(input) {
1020            if let Some(p) = self.get(&name) {
1021                return Ok(p);
1022            }
1023        }
1024
1025        // Not found among compiled-in protocols. Check whether `input` names a
1026        // real NetGet client protocol that simply wasn't compiled into this
1027        // build.
1028        let input_norm = input.to_lowercase().replace(['-', ' '], "_");
1029        for (name, requires) in ALL_KNOWN_CLIENT_PROTOCOLS {
1030            let name_norm = name.to_lowercase().replace(['-', ' '], "_");
1031            if name_norm == input_norm {
1032                return Err(ClientProtocolLookupError::NotCompiled { name, requires });
1033            }
1034        }
1035
1036        // Truly unknown. Offer a "did you mean" suggestion via edit distance
1037        // against every known client protocol name, compiled in or not.
1038        Err(ClientProtocolLookupError::Unknown {
1039            input: input.to_string(),
1040            suggestion: suggest_client_protocol_name(input, ALL_KNOWN_CLIENT_PROTOCOLS),
1041        })
1042    }
1043
1044    /// Parse client protocol from user input string
1045    ///
1046    /// Attempts to match keywords from registered client protocols.
1047    /// Returns protocol name if match found, None otherwise.
1048    pub fn parse_from_str(&self, input: &str) -> Option<String> {
1049        let input_lower = input.to_lowercase();
1050        // Normalize hyphens and spaces to underscores for consistent matching
1051        // e.g., "bluetooth-ble" -> "bluetooth_ble", "WebRTC Signaling" -> "webrtc_signaling"
1052        let input_normalized = input_lower.replace(['-', ' '], "_");
1053
1054        // First, try exact match with stack names (for LLM-generated responses)
1055        for (protocol_name, protocol) in &self.protocols {
1056            let stack_lower = protocol.stack_name().to_lowercase();
1057            let stack_normalized = stack_lower.replace(['-', ' '], "_");
1058            if input_lower == stack_lower
1059                || input_normalized == stack_lower
1060                || input_normalized == stack_normalized
1061            {
1062                return Some(protocol_name.clone());
1063            }
1064        }
1065
1066        // An exact keyword match wins outright, before any substring test.
1067        if let Some(protocol_name) = self
1068            .keyword_map
1069            .get(&input_normalized)
1070            .or_else(|| self.keyword_map.get(&input_lower))
1071        {
1072            return Some(protocol_name.clone());
1073        }
1074
1075        // Substring matching, longest keyword first.
1076        //
1077        // This used to return the first `contains()` hit while iterating a HashMap,
1078        // whose order is unspecified. "ssh_agent" contains both "ssh" and "agent", so
1079        // it resolved to SSH or to SSH Agent depending on hash order -- creating an
1080        // ssh_agent client could silently produce an SSH client instead, and the same
1081        // input could resolve differently between runs or between feature sets.
1082        // Preferring the longest matching keyword makes the more specific protocol
1083        // win, and the name tie-break makes the answer identical on every run.
1084        let mut best: Option<(usize, &String)> = None;
1085        for (keyword, protocol_name) in &self.keyword_map {
1086            if !(input_lower.contains(keyword) || input_normalized.contains(keyword)) {
1087                continue;
1088            }
1089            let better = match best {
1090                None => true,
1091                Some((best_len, best_name)) => {
1092                    keyword.len() > best_len
1093                        || (keyword.len() == best_len && protocol_name < best_name)
1094                }
1095            };
1096            if better {
1097                best = Some((keyword.len(), protocol_name));
1098            }
1099        }
1100        best.map(|(_, name)| name.clone())
1101    }
1102
1103    /// List all registered client protocol names
1104    pub fn list_protocols(&self) -> Vec<String> {
1105        self.protocols.keys().cloned().collect()
1106    }
1107
1108    /// Check if a protocol is registered
1109    pub fn has_protocol(&self, protocol_name: &str) -> bool {
1110        self.protocols.contains_key(protocol_name)
1111    }
1112
1113    /// Get all registered client protocols
1114    pub fn get_all(&self) -> Vec<Arc<dyn Client>> {
1115        self.protocols.values().cloned().collect()
1116    }
1117
1118    /// Get protocols that are excluded due to missing dependencies
1119    ///
1120    /// Returns a map of protocol name -> list of missing dependencies
1121    pub fn get_excluded_protocols(
1122        &self,
1123        caps: &crate::privilege::SystemCapabilities,
1124    ) -> std::collections::HashMap<String, Vec<super::dependencies::ProtocolDependency>> {
1125        let mut excluded = std::collections::HashMap::new();
1126
1127        for (protocol_name, protocol) in &self.protocols {
1128            let dependencies = protocol.get_dependencies();
1129            let mut missing = Vec::new();
1130
1131            for dep in dependencies {
1132                if !dep.is_available(caps) {
1133                    missing.push(dep);
1134                }
1135            }
1136
1137            if !missing.is_empty() {
1138                excluded.insert(protocol_name.clone(), missing);
1139            }
1140        }
1141
1142        excluded
1143    }
1144
1145    /// Get protocols that are available (have all dependencies met)
1146    ///
1147    /// Returns a list of protocol names that can be used
1148    pub fn get_available_protocols(
1149        &self,
1150        caps: &crate::privilege::SystemCapabilities,
1151    ) -> Vec<String> {
1152        let excluded = self.get_excluded_protocols(caps);
1153
1154        self.protocols
1155            .keys()
1156            .filter(|name| !excluded.contains_key(*name))
1157            .cloned()
1158            .collect()
1159    }
1160
1161    /// Check if a specific protocol is available (has all dependencies met)
1162    pub fn is_protocol_available(
1163        &self,
1164        protocol_name: &str,
1165        caps: &crate::privilege::SystemCapabilities,
1166    ) -> bool {
1167        if let Some(protocol) = self.get(protocol_name) {
1168            let dependencies = protocol.get_dependencies();
1169            dependencies.iter().all(|dep| dep.is_available(caps))
1170        } else {
1171            false
1172        }
1173    }
1174}
1175
1176/// All client protocol names known to the netget codebase, paired with what a build needs to
1177/// include each one ("a native build (not the browser)", "the `libpcap` feature, on Unix",
1178/// …) — independent of what *this* build includes. This lets [`ClientRegistry::resolve`] tell
1179/// "protocol exists but wasn't compiled in" apart from "no such protocol", and say why.
1180///
1181/// Kept manually in sync with the platform gates on the registrations in
1182/// `register_protocols()` above. This is the one acceptable central touchpoint
1183/// per CLAUDE.md's decentralization policy (it plays the same role as the
1184/// `register()` calls themselves) — when adding a new client protocol, add its
1185/// (canonical name, requirement) pair here too.
1186pub(crate) const ALL_KNOWN_CLIENT_PROTOCOLS: &[(&str, &str)] = &[
1187    ("BGP", "nothing (every build has it)"),
1188    ("BOOTP", "nothing (every build has it)"),
1189    ("Cassandra", "a native build (not the browser)"),
1190    ("DataLink", "the `libpcap` feature, on Unix"),
1191    ("NMDC Peer", "a native build (not the browser)"),
1192    ("ADC", "a native build (not the browser)"),
1193    ("ADC Peer", "a native build (not the browser)"),
1194    ("Soulseek", "a native build (not the browser)"),
1195    ("Soulseek Peer", "a native build (not the browser)"),
1196    ("Gnutella", "a native build (not the browser)"),
1197    ("DC", "nothing (every build has it)"),
1198    ("DHCP", "nothing (every build has it)"),
1199    ("DNS", "a native build (not the browser)"),
1200    ("DNS-over-HTTPS", "a native build (not the browser)"),
1201    ("DoT", "a native build (not the browser)"),
1202    ("DoQ", "a native build (not the browser)"),
1203    ("etcd", "a native build (not the browser)"),
1204    ("ZooKeeper", "a native build (not the browser)"),
1205    ("gRPC", "a native build (not the browser)"),
1206    ("gRPC-Web", "a native build (not the browser)"),
1207    ("gNMI", "a native build (not the browser)"),
1208    ("ConnectRPC", "a native build (not the browser)"),
1209    ("TACACS", "a native build (not the browser)"),
1210    ("NETCONF", "a native build (not the browser)"),
1211    ("RPKI-RTR", "a native build (not the browser)"),
1212    ("RDAP", "a native build (not the browser)"),
1213    ("HL7", "a native build (not the browser)"),
1214    ("ICAP", "a native build (not the browser)"),
1215    ("OCPP", "a native build (not the browser)"),
1216    ("A2A", "a native build (not the browser)"),
1217    ("GraphQL", "a native build (not the browser)"),
1218    ("FastCGI", "a native build (not the browser)"),
1219    ("Redfish", "a native build (not the browser)"),
1220    ("SCIM", "a native build (not the browser)"),
1221    ("Socket.IO", "a native build (not the browser)"),
1222    ("CalDAV", "a native build (not the browser)"),
1223    ("CardDAV", "a native build (not the browser)"),
1224    ("DICOM", "a native build (not the browser)"),
1225    ("ACME", "a native build (not the browser)"),
1226    ("FIX", "a native build (not the browser)"),
1227    ("WAMP", "a native build (not the browser)"),
1228    ("RTMP", "a native build (not the browser)"),
1229    ("SRT", "a native build (not the browser)"),
1230    ("AMQP1", "a native build (not the browser)"),
1231    ("Thrift", "a native build (not the browser)"),
1232    ("BMP", "a native build (not the browser)"),
1233    ("MQTT-SN", "a native build (not the browser)"),
1234    ("NBD", "a native build (not the browser)"),
1235    ("ManageSieve", "a native build (not the browser)"),
1236    ("Zenoh", "a native build (not the browser)"),
1237    ("LwM2M", "a native build (not the browser)"),
1238    ("RESTCONF", "a native build (not the browser)"),
1239    ("WebTransport", "a native build (not the browser)"),
1240    ("JMAP", "a native build (not the browser)"),
1241    ("EPP", "a native build (not the browser)"),
1242    ("LMTP", "a native build (not the browser)"),
1243    ("LPD", "a native build (not the browser)"),
1244    ("Echo", "a native build (not the browser)"),
1245    ("Discard", "a native build (not the browser)"),
1246    ("Daytime", "a native build (not the browser)"),
1247    ("QOTD", "a native build (not the browser)"),
1248    ("Chargen", "a native build (not the browser)"),
1249    ("Time", "a native build (not the browser)"),
1250    ("RCON", "a native build (not the browser)"),
1251    ("A2S", "a native build (not the browser)"),
1252    ("Minecraft", "a native build (not the browser)"),
1253    ("9P", "a native build (not the browser)"),
1254    ("ZeroMQ", "a native build (not the browser)"),
1255    ("SMPP", "a native build (not the browser)"),
1256    ("ClickHouse", "a native build (not the browser)"),
1257    ("MessagePack-RPC", "a native build (not the browser)"),
1258    ("Zipkin", "a native build (not the browser)"),
1259    ("Cap'n Proto RPC", "a native build (not the browser)"),
1260    ("KNX/IP", "a native build (not the browser)"),
1261    ("Consul", "a native build (not the browser)"),
1262    ("Milter", "a native build (not the browser)"),
1263    ("Anthropic", "a native build (not the browser)"),
1264    ("RadSec", "a native build (not the browser)"),
1265    ("D-Bus", "a native build (not the browser)"),
1266    ("BFD", "a native build (not the browser)"),
1267    ("VXLAN", "a native build (not the browser)"),
1268    ("rsync", "a native build (not the browser)"),
1269    ("RDP", "a Unix build (macOS or Linux)"),
1270    ("SPICE", "a native build (not the browser)"),
1271    ("PFCP", "a native build (not the browser)"),
1272    ("GTP", "nothing (every build has it)"),
1273    ("Db2", "nothing (every build has it)"),
1274    ("Snowflake", "nothing (every build has it)"),
1275    ("Spark", "nothing (every build has it)"),
1276    ("YARN", "nothing (every build has it)"),
1277    ("M3UA", "a Unix build (macOS or Linux)"),
1278    ("NGAP", "a Unix build (macOS or Linux)"),
1279    ("Matrix", "a native build (not the browser)"),
1280    ("libp2p", "a native build (not the browser)"),
1281    ("SunRPC", "a native build (not the browser)"),
1282    ("Guacamole", "a native build (not the browser)"),
1283    ("ActivityPub", "a native build (not the browser)"),
1284    ("Stratum", "a native build (not the browser)"),
1285    ("Stratum V2", "a native build (not the browser)"),
1286    ("Apache Pulsar", "a native build (not the browser)"),
1287    ("TR-069 (CWMP)", "a native build (not the browser)"),
1288    ("SSE", "a native build (not the browser)"),
1289    ("Zabbix", "a native build (not the browser)"),
1290    ("RTP", "nothing (every build has it)"),
1291    ("RTSP", "nothing (every build has it)"),
1292    ("HLS", "nothing (every build has it)"),
1293    ("WS-Discovery", "a native build (not the browser)"),
1294    ("ONVIF", "a native build (not the browser)"),
1295    ("Kerberos", "a native build (not the browser)"),
1296    ("X11", "a native build (not the browser)"),
1297    ("HTTP", "nothing (every build has it)"),
1298    ("HTTP2", "nothing (every build has it)"),
1299    ("HTTP3", "a native build (not the browser)"),
1300    ("QUIC", "a native build (not the browser)"),
1301    ("igmp", "a Unix build (macOS or Linux)"),
1302    ("IPP", "a native build (not the browser)"),
1303    ("IS-IS", "the `libpcap` feature, on Unix"),
1304    ("JSON-RPC", "nothing (every build has it)"),
1305    ("Kafka", "nothing (every build has it)"),
1306    ("mDNS", "a native build (not the browser)"),
1307    ("AMQP", "a native build (not the browser)"),
1308    ("MySQL", "a native build (not the browser)"),
1309    ("nfc", "the `pcsc` feature, on Unix"),
1310    ("NTP", "a native build (not the browser)"),
1311    ("OpenAI", "a native build (not the browser)"),
1312    ("OTLP", "a native build (not the browser)"),
1313    ("PostgreSQL", "a native build (not the browser)"),
1314    ("PyPI", "nothing (every build has it)"),
1315    ("MSSQL", "a native build (not the browser)"),
1316    ("Redis", "a native build (not the browser)"),
1317    ("ICMP", "a Unix build (macOS or Linux)"),
1318    ("RIP", "a native build (not the browser)"),
1319    ("SAML", "nothing (every build has it)"),
1320    ("SIP", "nothing (every build has it)"),
1321    ("H323", "a native build (not the browser)"),
1322    ("SMTP", "a native build (not the browser)"),
1323    ("FTP", "nothing (every build has it)"),
1324    ("POP3", "nothing (every build has it)"),
1325    ("SNMP", "nothing (every build has it)"),
1326    ("SOCKS5", "a native build (not the browser)"),
1327    ("SocketFile", "a Unix build (macOS or Linux)"),
1328    ("SSH", "a native build (not the browser)"),
1329    ("SSH Agent", "a Unix build (macOS or Linux)"),
1330    ("Syslog", "nothing (every build has it)"),
1331    ("FluentForward", "a native build (not the browser)"),
1332    ("NETFLOWV9", "a native build (not the browser)"),
1333    ("NETFLOW-V9", "a native build (not the browser)"),
1334    ("NETFLOW_V9", "a native build (not the browser)"),
1335    ("IPFIX", "a native build (not the browser)"),
1336    ("IPFIX-UDP", "a native build (not the browser)"),
1337    ("PrometheusRemoteWrite", "a native build (not the browser)"),
1338    ("RemoteWrite", "a native build (not the browser)"),
1339    ("sFlow", "a native build (not the browser)"),
1340    ("sFlow-v5", "a native build (not the browser)"),
1341    ("Loki", "a native build (not the browser)"),
1342    ("LokiPush", "a native build (not the browser)"),
1343    ("InfluxDB", "a native build (not the browser)"),
1344    ("InfluxDB2", "a native build (not the browser)"),
1345    ("GELF", "a native build (not the browser)"),
1346    ("Graylog", "a native build (not the browser)"),
1347    ("Graphite", "a native build (not the browser)"),
1348    ("StatsD", "a native build (not the browser)"),
1349    ("DogStatsD", "a native build (not the browser)"),
1350    ("TCP", "nothing (every build has it)"),
1351    ("Telnet", "nothing (every build has it)"),
1352    ("TLS", "nothing (every build has it)"),
1353    ("Tor", "a Unix build (macOS or Linux)"),
1354    ("BitTorrent DHT", "nothing (every build has it)"),
1355    ("BitTorrent Peer Wire", "nothing (every build has it)"),
1356    ("BitTorrent Tracker", "nothing (every build has it)"),
1357    ("TURN", "a Unix build (macOS or Linux)"),
1358    ("UDP", "nothing (every build has it)"),
1359    ("Gemini", "a native build (not the browser)"),
1360    ("DICT", "a native build (not the browser)"),
1361    ("Beanstalkd", "a native build (not the browser)"),
1362    ("Gearman", "a native build (not the browser)"),
1363    ("Prometheus", "a native build (not the browser)"),
1364    ("Docker", "a native build (not the browser)"),
1365    ("Vault", "a native build (not the browser)"),
1366    ("Nostr", "a native build (not the browser)"),
1367    ("Bolt", "a native build (not the browser)"),
1368    ("OCI-Registry", "nothing (every build has it)"),
1369    ("NSQ", "a native build (not the browser)"),
1370    ("NUT", "a native build (not the browser)"),
1371    ("WHOIS", "nothing (every build has it)"),
1372    ("STOMP", "nothing (every build has it)"),
1373    ("NetBIOS-NS", "nothing (every build has it)"),
1374    ("NATS", "nothing (every build has it)"),
1375    ("Memcached", "nothing (every build has it)"),
1376    ("Modbus", "nothing (every build has it)"),
1377    ("CoAP", "nothing (every build has it)"),
1378    ("RADIUS", "nothing (every build has it)"),
1379    ("DIAMETER", "a native build (not the browser)"),
1380    ("SSDP", "nothing (every build has it)"),
1381    ("Gopher", "nothing (every build has it)"),
1382    ("Finger", "nothing (every build has it)"),
1383    ("Ident", "nothing (every build has it)"),
1384    ("LLMNR", "a native build (not the browser)"),
1385    ("wireguard", "a Unix build (macOS or Linux)"),
1386    ("XML-RPC", "a native build (not the browser)"),
1387    ("XMPP", "a Unix build (macOS or Linux)"),
1388    ("ARP", "the `libpcap` feature, on Unix"),
1389    ("HTTP Proxy", "a Unix build (macOS or Linux)"),
1390    ("IMAP", "a native build (not the browser)"),
1391    ("IRC", "a native build (not the browser)"),
1392    ("MCP", "a native build (not the browser)"),
1393    ("MQTT", "a native build (not the browser)"),
1394    ("MongoDB", "a native build (not the browser)"),
1395    ("NNTP", "nothing (every build has it)"),
1396    ("NPM", "nothing (every build has it)"),
1397    ("Ollama", "nothing (every build has it)"),
1398    ("OpenAPI", "nothing (every build has it)"),
1399    ("ospf", "a Unix build (macOS or Linux)"),
1400    ("SMB", "the `libsmbclient` feature, on Unix"),
1401    ("SQS", "a native build (not the browser)"),
1402    ("STUN", "a Unix build (macOS or Linux)"),
1403    ("USB", "a native build (not the browser)"),
1404    ("VNC", "nothing (every build has it)"),
1405    ("WebRTC", "a native build (not the browser)"),
1406    ("WebSocket", "a native build (not the browser)"),
1407    ("Bitcoin", "nothing (every build has it)"),
1408    ("devp2p", "a native build (not the browser)"),
1409    ("Bluetooth (BLE)", "the `bluetooth` feature, on Unix"),
1410    ("DynamoDB", "a Unix build (macOS or Linux)"),
1411    ("Elasticsearch", "nothing (every build has it)"),
1412    ("Git", "a native build (not the browser)"),
1413    ("Kubernetes", "a native build (not the browser)"),
1414    ("LDAP", "a native build (not the browser)"),
1415    ("Maven", "nothing (every build has it)"),
1416    ("NFS", "a Unix build (macOS or Linux)"),
1417    ("OAuth2", "nothing (every build has it)"),
1418    ("OpenIDConnect", "nothing (every build has it)"),
1419    ("CouchDB", "a native build (not the browser)"),
1420    ("S3", "a native build (not the browser)"),
1421    ("WebDAV", "a native build (not the browser)"),
1422];
1423
1424/// Why [`ClientRegistry::resolve`] could not find a protocol for the given
1425/// input.
1426#[derive(Debug, Clone, PartialEq, Eq)]
1427pub enum ClientProtocolLookupError {
1428    /// A real NetGet client protocol by this name exists, but this build does not
1429    /// include it (`requires` says what does).
1430    NotCompiled {
1431        name: &'static str,
1432        requires: &'static str,
1433    },
1434    /// No client protocol by this name/keyword is known to NetGet at all, in
1435    /// any build.
1436    Unknown {
1437        input: String,
1438        suggestion: Option<&'static str>,
1439    },
1440}
1441
1442impl std::fmt::Display for ClientProtocolLookupError {
1443    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1444        match self {
1445            ClientProtocolLookupError::NotCompiled { name, requires } => write!(
1446                f,
1447                "Client protocol '{name}' exists but is not compiled into this build: it needs {requires}"
1448            ),
1449            ClientProtocolLookupError::Unknown {
1450                input,
1451                suggestion: Some(s),
1452            } => write!(f, "Unknown client protocol: '{input}'. Did you mean '{s}'?"),
1453            ClientProtocolLookupError::Unknown {
1454                input,
1455                suggestion: None,
1456            } => write!(f, "Unknown client protocol: '{input}'"),
1457        }
1458    }
1459}
1460
1461impl std::error::Error for ClientProtocolLookupError {}
1462
1463/// Suggest the closest known client protocol name to `input` via Levenshtein
1464/// edit distance, if it's plausibly a typo rather than something unrelated.
1465fn suggest_client_protocol_name(
1466    input: &str,
1467    known: &[(&'static str, &'static str)],
1468) -> Option<&'static str> {
1469    let input_norm = input.to_lowercase();
1470    let mut best: Option<(&'static str, usize)> = None;
1471
1472    for (name, _feature) in known {
1473        let distance = levenshtein(&input_norm, &name.to_lowercase());
1474        best = match best {
1475            Some((_, best_dist)) if best_dist <= distance => best,
1476            _ => Some((name, distance)),
1477        };
1478    }
1479
1480    best.and_then(|(name, distance)| {
1481        let threshold = (input_norm.chars().count() / 3).max(2);
1482        if distance <= threshold {
1483            Some(name)
1484        } else {
1485            None
1486        }
1487    })
1488}
1489
1490/// Minimal Levenshtein edit distance between two strings. Implemented by hand
1491/// (no external dependency) since a "did you mean" suggestion doesn't justify
1492/// adding a crate.
1493fn levenshtein(a: &str, b: &str) -> usize {
1494    let a: Vec<char> = a.chars().collect();
1495    let b: Vec<char> = b.chars().collect();
1496    let mut dp: Vec<usize> = (0..=b.len()).collect();
1497
1498    for i in 1..=a.len() {
1499        let mut prev = dp[0];
1500        dp[0] = i;
1501        for j in 1..=b.len() {
1502            let tmp = dp[j];
1503            dp[j] = if a[i - 1] == b[j - 1] {
1504                prev
1505            } else {
1506                1 + prev.min(dp[j]).min(dp[j - 1])
1507            };
1508            prev = tmp;
1509        }
1510    }
1511
1512    dp[b.len()]
1513}
1514
1515/// Global client protocol registry instance
1516///
1517/// This registry is initialized once at startup with all available client protocols
1518/// based on compiled features. Use `CLIENT_REGISTRY.get(protocol_name)` to retrieve
1519/// a client protocol implementation.
1520pub static CLIENT_REGISTRY: std::sync::LazyLock<ClientRegistry> =
1521    std::sync::LazyLock::new(ClientRegistry::new);