1#[cfg(feature = "srv")]
2use std::str::FromStr;
3use std::{
4 borrow::Cow,
5 collections::HashMap,
6 fs::File,
7 io::{self, Read, Write},
8 net::{SocketAddr, TcpStream, ToSocketAddrs},
9 path::Path,
10 sync::LazyLock,
11 time::{Duration, Instant},
12};
13
14#[cfg(feature = "srv")]
15use hickory_client::{
16 client::{Client, ClientHandle},
17 proto::{
18 rr::{DNSClass, Name, RData, RecordType},
19 runtime::TokioRuntimeProvider,
20 udp::UdpClientStream,
21 },
22};
23use regex::bytes::Regex;
24use serde_json::{Map, Value};
25#[cfg(feature = "tokio")]
26use tokio::io::{AsyncReadExt, AsyncWriteExt};
27use validators::models::Host;
28
29use crate::{WhoIsError, WhoIsHost, WhoIsLookupOptions, WhoIsServerValue};
30
31const DEFAULT_WHOIS_HOST_PORT: u16 = 43;
32const DEFAULT_WHOIS_HOST_QUERY: &str = "$addr\r\n";
33const READ_BUFFER_SIZE: usize = 8 * 1024;
34
35static RE_SERVER: LazyLock<Regex> = LazyLock::new(|| {
37 Regex::new(r"(?m-u)^[^\S\n]*(?:Registrar[^\S\n]+)?(?:ReferralServer|Registrar Whois|Whois Server|WHOIS Server|Registrar WHOIS Server):[^\S\n]*(?:r?whois://)?(\S*)").unwrap()
38});
39
40fn extract_referral_host(query_result: &[u8]) -> Option<&str> {
42 let host = RE_SERVER.captures(query_result)?.get(1)?.as_bytes();
43 let host = std::str::from_utf8(host).ok()?;
44
45 if host.is_empty() { None } else { Some(host) }
46}
47
48#[cfg(not(feature = "charset"))]
50fn decode_response(bytes: Vec<u8>, _tld: Option<&str>) -> String {
51 match String::from_utf8(bytes) {
52 Ok(s) => s,
53 Err(error) => String::from_utf8_lossy(error.as_bytes()).into_owned(),
54 }
55}
56
57#[cfg(feature = "charset")]
59fn decode_response(bytes: Vec<u8>, tld: Option<&str>) -> String {
60 use chardetng::{EncodingDetector, Iso2022JpDetection, Utf8Detection};
61
62 let bytes = match String::from_utf8(bytes) {
63 Ok(s) => return s,
64 Err(error) => error.into_bytes(),
65 };
66
67 let mut detector = EncodingDetector::new(Iso2022JpDetection::Allow);
68
69 detector.feed(&bytes, true);
70
71 detector.guess(tld.map(str::as_bytes), Utf8Detection::Allow).decode(&bytes).0.into_owned()
72}
73
74fn tld_hint(host: &Host) -> Option<Cow<'_, str>> {
76 let Host::Domain(domain) = host else {
77 return None;
78 };
79
80 let tld = match domain.rfind('.') {
81 Some(index) => &domain[index + 1..],
82 None => domain.as_str(),
83 };
84
85 if tld.is_empty() || !tld.is_ascii() {
86 return None;
87 }
88
89 if tld.bytes().any(|b| b.is_ascii_uppercase()) {
90 Some(Cow::Owned(tld.to_ascii_lowercase()))
91 } else {
92 Some(Cow::Borrowed(tld))
93 }
94}
95
96fn query_text<'a>(server: &WhoIsServerValue, host: &'a Host) -> Cow<'a, str> {
98 match host {
99 Host::Domain(domain) => server.encode_domain(domain),
101 Host::IPv4(ip) => Cow::Owned(ip.to_string()),
102 Host::IPv6(ip) => Cow::Owned(ip.to_string()),
103 }
104}
105
106fn build_request(server: &WhoIsServerValue, text: &str) -> String {
108 let query = server.query.as_deref().unwrap_or(DEFAULT_WHOIS_HOST_QUERY);
109
110 let mut request = query.replace("$addr", text);
111
112 if !request.ends_with("\r\n") {
113 if request.ends_with('\n') {
114 request.pop();
115 }
116
117 request.push_str("\r\n");
118 }
119
120 request
121}
122
123fn same_host(a: &Host, b: &Host) -> bool {
125 match (a, b) {
126 (Host::Domain(a), Host::Domain(b)) => a.eq_ignore_ascii_case(b),
127 _ => a == b,
128 }
129}
130
131#[derive(Debug)]
133struct Deadline {
134 start: Instant,
135 timeout: Duration,
136}
137
138impl Deadline {
139 #[inline]
140 fn new(timeout: Duration) -> Self {
141 Deadline {
142 start: Instant::now(),
143 timeout,
144 }
145 }
146
147 fn remaining(&self) -> Result<Duration, io::Error> {
149 match self.timeout.checked_sub(self.start.elapsed()) {
150 Some(remaining) if !remaining.is_zero() => Ok(remaining),
151 _ => Err(io::Error::new(io::ErrorKind::TimedOut, "the WHOIS lookup timed out")),
152 }
153 }
154}
155
156#[inline]
158fn no_address_error() -> io::Error {
159 io::Error::new(
160 io::ErrorKind::AddrNotAvailable,
161 "the host is not resolved to any socket address",
162 )
163}
164
165fn connect(socket_addrs: &[SocketAddr], deadline: &Deadline) -> io::Result<TcpStream> {
167 let mut last_error = None;
168
169 for socket_addr in socket_addrs {
170 match TcpStream::connect_timeout(socket_addr, deadline.remaining()?) {
171 Ok(client) => return Ok(client),
172 Err(error) => last_error = Some(error),
173 }
174 }
175
176 Err(last_error.unwrap_or_else(no_address_error))
177}
178
179fn read_response(
181 client: &mut TcpStream,
182 max_response_size: Option<usize>,
183 deadline: Option<&Deadline>,
184) -> io::Result<Vec<u8>> {
185 let mut query_result = Vec::new();
186 let mut buffer = [0u8; READ_BUFFER_SIZE];
187
188 loop {
189 if let Some(deadline) = deadline {
190 client.set_read_timeout(Some(deadline.remaining()?))?;
191 }
192
193 let read = client.read(&mut buffer)?;
195
196 if read == 0 {
197 break;
198 }
199
200 if let Some(max) = max_response_size
201 && query_result.len() + read > max
202 {
203 return Err(io::Error::new(
204 io::ErrorKind::InvalidData,
205 "the WHOIS response is too big",
206 ));
207 }
208
209 query_result.extend_from_slice(&buffer[..read]);
210 }
211
212 Ok(query_result)
213}
214
215#[cfg(feature = "tokio")]
217async fn connect_async(
218 socket_addrs: &[SocketAddr],
219 deadline: &Deadline,
220) -> Result<tokio::net::TcpStream, WhoIsError> {
221 let mut last_error = None;
222
223 for socket_addr in socket_addrs {
224 match tokio::time::timeout(
225 deadline.remaining()?,
226 tokio::net::TcpStream::connect(socket_addr),
227 )
228 .await
229 {
230 Ok(Ok(client)) => return Ok(client),
231 Ok(Err(error)) => last_error = Some(WhoIsError::IOError(error)),
232 Err(error) => last_error = Some(WhoIsError::Elapsed(error)),
233 }
234 }
235
236 Err(last_error.unwrap_or_else(|| WhoIsError::IOError(no_address_error())))
237}
238
239#[cfg(feature = "srv")]
241fn parse_dns_server(dns_server: &str) -> Result<SocketAddr, WhoIsError> {
242 dns_server
243 .parse()
244 .map_err(|_error| WhoIsError::MapError("The DNS server address is incorrect."))
245}
246
247#[cfg(feature = "srv")]
249fn strip_label(tld: &str) -> Option<&str> {
250 let parent = &tld[tld.find('.')? + 1..];
251
252 if parent.is_empty() { None } else { Some(parent) }
253}
254
255#[cfg(feature = "srv")]
257async fn query_srv_targets_async(
258 dns_server: SocketAddr,
259 name: Name,
260) -> Result<Vec<String>, WhoIsError> {
261 let stream = UdpClientStream::builder(dns_server, TokioRuntimeProvider::new()).build();
262
263 let (mut client, background) = Client::connect(stream).await.map_err(io::Error::other)?;
264
265 let background = tokio::spawn(background);
267
268 let response = client.query(name, DNSClass::IN, RecordType::SRV).await;
269
270 background.abort();
271
272 let response = response.map_err(io::Error::other)?;
273
274 Ok(response
275 .answers()
276 .iter()
277 .filter_map(|record| match record.data() {
278 RData::SRV(srv) => Some(srv.target().to_string()),
279 _ => None,
280 })
281 .collect())
282}
283
284#[cfg(feature = "srv")]
286fn query_srv_targets(dns_server: SocketAddr, name: Name) -> Result<Vec<String>, WhoIsError> {
287 let thread = std::thread::spawn(move || -> Result<Vec<String>, WhoIsError> {
288 let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
289
290 runtime.block_on(query_srv_targets_async(dns_server, name))
291 });
292
293 match thread.join() {
294 Ok(targets) => targets,
295 Err(error) => std::panic::resume_unwind(error),
296 }
297}
298
299#[cfg(feature = "tokio")]
301async fn read_response_async(
302 client: &mut tokio::net::TcpStream,
303 max_response_size: Option<usize>,
304) -> io::Result<Vec<u8>> {
305 let mut query_result = Vec::new();
306
307 match max_response_size {
308 Some(max) => {
309 client.take(max.saturating_add(1) as u64).read_to_end(&mut query_result).await?;
311
312 if query_result.len() > max {
313 return Err(io::Error::new(
314 io::ErrorKind::InvalidData,
315 "the WHOIS response is too big",
316 ));
317 }
318 },
319 None => {
320 client.read_to_end(&mut query_result).await?;
321 },
322 }
323
324 Ok(query_result)
325}
326
327#[derive(Debug, Clone)]
329pub struct WhoIs {
330 map: HashMap<String, WhoIsServerValue>,
331 ip: WhoIsServerValue,
332}
333
334impl WhoIs {
335 pub fn from_host<T: AsRef<str>>(host: T) -> Result<WhoIs, WhoIsError> {
337 Ok(Self {
338 map: HashMap::new(), ip: WhoIsServerValue::from_string(host)?
339 })
340 }
341
342 #[inline]
344 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<WhoIs, WhoIsError> {
345 let path = path.as_ref();
346
347 let file = File::open(path)?;
348
349 let map: Map<String, Value> = serde_json::from_reader(file)?;
350
351 Self::from_inner(map)
352 }
353
354 #[cfg(feature = "tokio")]
356 #[inline]
357 pub async fn from_path_async<P: AsRef<Path>>(path: P) -> Result<WhoIs, WhoIsError> {
358 let file = tokio::fs::read(path).await?;
359
360 let map: Map<String, Value> = serde_json::from_slice(file.as_slice())?;
361
362 Self::from_inner(map)
363 }
364
365 #[inline]
367 pub fn from_string<S: AsRef<str>>(string: S) -> Result<WhoIs, WhoIsError> {
368 let string = string.as_ref();
369
370 let map: Map<String, Value> = serde_json::from_str(string)?;
371
372 Self::from_inner(map)
373 }
374
375 fn from_inner(mut map: Map<String, Value>) -> Result<WhoIs, WhoIsError> {
376 let ip = match map.remove("_") {
377 Some(server) => {
378 if let Value::Object(server) = server {
379 match server.get("ip") {
380 Some(server) => {
381 if server.is_null() {
382 return Err(WhoIsError::MapError(
383 "`ip` in the `_` object in the server list is null.",
384 ));
385 }
386
387 WhoIsServerValue::from_value(server)?
388 },
389 None => {
390 return Err(WhoIsError::MapError(
391 "Cannot find `ip` in the `_` object in the server list.",
392 ));
393 },
394 }
395 } else {
396 return Err(WhoIsError::MapError("`_` in the server list is not an object."));
397 }
398 },
399 None => return Err(WhoIsError::MapError("Cannot find `_` in the server list.")),
400 };
401
402 let mut new_map: HashMap<String, WhoIsServerValue> = HashMap::with_capacity(map.len());
403
404 for (k, v) in map {
405 if !v.is_null() {
406 let server_value = WhoIsServerValue::from_value(&v)?;
407 new_map.insert(k, server_value);
408 }
409 }
410
411 Ok(WhoIs {
412 map: new_map,
413 ip,
414 })
415 }
416}
417
418#[cfg(feature = "srv")]
419impl WhoIs {
420 fn cache_first_server(&mut self, tld: &str, targets: Vec<String>) -> bool {
422 for target in targets {
423 let Ok(new_server) = WhoIsServerValue::from_string(target.trim_end_matches('.')) else {
424 continue;
425 };
426
427 self.map.insert(tld.to_string(), new_server);
428
429 return true;
430 }
431
432 false
433 }
434
435 pub fn can_find_server_for_tld<T: AsRef<str>, D: AsRef<str>>(
441 &mut self,
442 tld: T,
443 dns_server: D,
444 ) -> Result<bool, WhoIsError> {
445 let mut tld = tld.as_ref();
446
447 if self.map.contains_key(tld) {
449 return Ok(true);
450 }
451
452 let address = parse_dns_server(dns_server.as_ref())?;
453
454 loop {
455 let Some(parent) = strip_label(tld) else {
456 return Ok(false);
457 };
458
459 tld = parent;
460
461 if self.map.contains_key(tld) {
463 return Ok(true);
464 }
465
466 let name =
467 Name::from_str(&format!("_nicname._tcp.{tld}.")).map_err(io::Error::other)?;
468
469 if self.cache_first_server(tld, query_srv_targets(address, name)?) {
470 return Ok(true);
471 }
472 }
473 }
474}
475
476#[cfg(all(feature = "srv", feature = "tokio"))]
477impl WhoIs {
478 pub async fn can_find_server_for_tld_async<T: AsRef<str>, D: AsRef<str>>(
482 &mut self,
483 tld: T,
484 dns_server: D,
485 ) -> Result<bool, WhoIsError> {
486 let mut tld = tld.as_ref();
487
488 if self.map.contains_key(tld) {
490 return Ok(true);
491 }
492
493 let address = parse_dns_server(dns_server.as_ref())?;
494
495 loop {
496 let Some(parent) = strip_label(tld) else {
497 return Ok(false);
498 };
499
500 tld = parent;
501
502 if self.map.contains_key(tld) {
504 return Ok(true);
505 }
506
507 let name =
508 Name::from_str(&format!("_nicname._tcp.{tld}.")).map_err(io::Error::other)?;
509
510 if self.cache_first_server(tld, query_srv_targets_async(address, name).await?) {
511 return Ok(true);
512 }
513 }
514 }
515}
516
517impl WhoIs {
518 pub fn get_server_by_tld(&self, mut tld: &str) -> Option<&WhoIsServerValue> {
520 let mut server;
521
522 loop {
523 server = self.map.get(tld);
524
525 if server.is_some() {
526 break;
527 }
528
529 if tld.is_empty() {
530 break;
531 }
532
533 match tld.find('.') {
534 Some(index) => {
535 tld = &tld[index + 1..];
536 },
537 None => {
538 tld = "";
539 },
540 }
541 }
542
543 server
544 }
545
546 fn get_server_by_host(&self, host: &WhoIsHost) -> Option<&WhoIsServerValue> {
548 self.map.values().find(|server| &server.host == host)
549 }
550
551 fn get_server<'a>(
553 &'a self,
554 options: &'a WhoIsLookupOptions,
555 ) -> Result<&'a WhoIsServerValue, WhoIsError> {
556 if let Some(server) = &options.server {
557 return Ok(server);
558 }
559
560 match &options.target.0 {
561 Host::IPv4(_) | Host::IPv6(_) => Ok(&self.ip),
562 Host::Domain(domain) => self
563 .get_server_by_tld(domain.as_str())
564 .ok_or(WhoIsError::MapError("No whois server is known for this kind of object.")),
565 }
566 }
567
568 fn lookup_once(
569 server: &WhoIsServerValue,
570 text: &str,
571 options: &WhoIsLookupOptions,
572 ) -> Result<Vec<u8>, WhoIsError> {
573 let addr = server.host.to_addr_string(DEFAULT_WHOIS_HOST_PORT);
574 let request = build_request(server, text);
575
576 match options.timeout {
577 Some(timeout) => {
578 let deadline = Deadline::new(timeout);
579
580 let socket_addrs: Vec<SocketAddr> = addr.to_socket_addrs()?.collect();
581
582 let mut client = connect(&socket_addrs, &deadline)?;
583
584 client.set_write_timeout(Some(deadline.remaining()?))?;
585 client.write_all(request.as_bytes())?;
586 client.flush()?;
587
588 Ok(read_response(&mut client, options.max_response_size, Some(&deadline))?)
589 },
590 None => {
591 let mut client = TcpStream::connect(&addr)?;
592
593 client.write_all(request.as_bytes())?;
594 client.flush()?;
595
596 Ok(read_response(&mut client, options.max_response_size, None)?)
597 },
598 }
599 }
600
601 fn lookup_inner(
602 &self,
603 server: &WhoIsServerValue,
604 options: &WhoIsLookupOptions,
605 ) -> Result<Vec<u8>, WhoIsError> {
606 let host = &options.target.0;
607 let mut follow = options.follow;
608 let mut server = Cow::Borrowed(server);
609
610 let text = query_text(&server, host);
611 let mut query_result = Self::lookup_once(&server, text.as_ref(), options)?;
612
613 while follow > 0 {
614 let Some(referral) = extract_referral_host(&query_result)
615 .and_then(|h| WhoIsServerValue::from_string(h).ok())
616 else {
617 break;
618 };
619
620 if same_host(referral.host.host(), server.host.host()) {
622 break;
623 }
624
625 server = match self.get_server_by_host(&referral.host) {
626 Some(server) => Cow::Borrowed(server),
627 None => Cow::Owned(referral),
628 };
629
630 let text = query_text(&server, host);
631
632 query_result = Self::lookup_once(&server, text.as_ref(), options)?;
633
634 follow -= 1;
635 }
636
637 Ok(query_result)
638 }
639
640 #[inline]
642 pub fn lookup_raw(&self, options: WhoIsLookupOptions) -> Result<Vec<u8>, WhoIsError> {
643 let server = self.get_server(&options)?;
644
645 self.lookup_inner(server, &options)
646 }
647
648 pub fn lookup(&self, options: WhoIsLookupOptions) -> Result<String, WhoIsError> {
650 let server = self.get_server(&options)?;
651
652 let query_result = self.lookup_inner(server, &options)?;
653
654 Ok(decode_response(query_result, tld_hint(&options.target.0).as_deref()))
655 }
656}
657
658#[cfg(feature = "tokio")]
659impl WhoIs {
660 async fn lookup_once_async(
661 server: &WhoIsServerValue,
662 text: &str,
663 options: &WhoIsLookupOptions,
664 ) -> Result<Vec<u8>, WhoIsError> {
665 let addr = server.host.to_addr_string(DEFAULT_WHOIS_HOST_PORT);
666 let request = build_request(server, text);
667
668 match options.timeout {
669 Some(timeout) => {
670 let deadline = Deadline::new(timeout);
671
672 let socket_addrs: Vec<SocketAddr> =
674 tokio::time::timeout(deadline.remaining()?, tokio::net::lookup_host(&addr))
675 .await??
676 .collect();
677
678 let mut client = connect_async(&socket_addrs, &deadline).await?;
679
680 tokio::time::timeout(deadline.remaining()?, client.write_all(request.as_bytes()))
681 .await??;
682 tokio::time::timeout(deadline.remaining()?, client.flush()).await??;
683
684 Ok(tokio::time::timeout(
685 deadline.remaining()?,
686 read_response_async(&mut client, options.max_response_size),
687 )
688 .await??)
689 },
690 None => {
691 let mut client = tokio::net::TcpStream::connect(&addr).await?;
692
693 client.write_all(request.as_bytes()).await?;
694 client.flush().await?;
695
696 Ok(read_response_async(&mut client, options.max_response_size).await?)
697 },
698 }
699 }
700
701 async fn lookup_inner_async(
702 &self,
703 server: &WhoIsServerValue,
704 options: &WhoIsLookupOptions,
705 ) -> Result<Vec<u8>, WhoIsError> {
706 let host = &options.target.0;
707 let mut follow = options.follow;
708 let mut server = Cow::Borrowed(server);
709
710 let text = query_text(&server, host);
711 let mut query_result = Self::lookup_once_async(&server, text.as_ref(), options).await?;
712
713 while follow > 0 {
714 let Some(referral) = extract_referral_host(&query_result)
715 .and_then(|h| WhoIsServerValue::from_string(h).ok())
716 else {
717 break;
718 };
719
720 if same_host(referral.host.host(), server.host.host()) {
722 break;
723 }
724
725 server = match self.get_server_by_host(&referral.host) {
726 Some(server) => Cow::Borrowed(server),
727 None => Cow::Owned(referral),
728 };
729
730 let text = query_text(&server, host);
731
732 query_result = Self::lookup_once_async(&server, text.as_ref(), options).await?;
733
734 follow -= 1;
735 }
736
737 Ok(query_result)
738 }
739
740 #[inline]
742 pub async fn lookup_raw_async(
743 &self,
744 options: WhoIsLookupOptions,
745 ) -> Result<Vec<u8>, WhoIsError> {
746 let server = self.get_server(&options)?;
747
748 self.lookup_inner_async(server, &options).await
749 }
750
751 pub async fn lookup_async(&self, options: WhoIsLookupOptions) -> Result<String, WhoIsError> {
753 let server = self.get_server(&options)?;
754
755 let query_result = self.lookup_inner_async(server, &options).await?;
756
757 Ok(decode_response(query_result, tld_hint(&options.target.0).as_deref()))
758 }
759}
760
761#[cfg(test)]
762mod tests {
763 use validators::models::Host;
764
765 use super::{build_request, decode_response, extract_referral_host, tld_hint};
766 use crate::WhoIsServerValue;
767
768 #[test]
769 fn extract_referral_host_trims_trailing_cr() {
770 let body = b"Domain: example.com\r\nReferralServer: whois://whois.arin.net\r\n";
771
772 assert_eq!(Some("whois.arin.net"), extract_referral_host(body));
773 }
774
775 #[test]
776 fn extract_referral_host_ignores_non_utf8_body() {
777 let body = b"\xB5n\xBFy: example.tw\r\nWhois Server: whois.twnic.net.tw\r\n";
779
780 assert_eq!(Some("whois.twnic.net.tw"), extract_referral_host(body));
781 }
782
783 #[test]
784 fn extract_referral_host_takes_the_first_token() {
785 let body = b" Registrar WHOIS Server: whois.example.com (see the note below)\r\n";
786
787 assert_eq!(Some("whois.example.com"), extract_referral_host(body));
788 }
789
790 #[test]
791 fn extract_referral_host_ignores_a_field_name_in_the_middle_of_a_line() {
792 let body = b"Do not trust any Whois Server: which is not listed above.\r\n";
793
794 assert_eq!(None, extract_referral_host(body));
795 }
796
797 #[test]
798 fn tld_hint_takes_the_last_label() {
799 assert_eq!(Some("tw"), tld_hint(&Host::Domain(String::from("magiclen.com.tw"))).as_deref());
800 assert_eq!(Some("com"), tld_hint(&Host::Domain(String::from("magiclen.COM"))).as_deref());
801 assert_eq!(None, tld_hint(&Host::IPv4("172.105.210.153".parse().unwrap())).as_deref());
802 }
803
804 #[test]
805 fn build_request_ends_with_crlf() {
806 let mut server = WhoIsServerValue::from_string("whois.example.com").unwrap();
807
808 assert_eq!("magiclen.org\r\n", build_request(&server, "magiclen.org"));
809
810 server.query = Some(String::from("domain $addr\r\n"));
811 assert_eq!("domain magiclen.org\r\n", build_request(&server, "magiclen.org"));
812
813 server.query = Some(String::from("domain $addr\n"));
815 assert_eq!("domain magiclen.org\r\n", build_request(&server, "magiclen.org"));
816
817 server.query = Some(String::from("domain $addr"));
818 assert_eq!("domain magiclen.org\r\n", build_request(&server, "magiclen.org"));
819 }
820
821 #[test]
822 fn decode_response_passes_through_utf8() {
823 assert_eq!("café", decode_response("café".as_bytes().to_vec(), None));
824 }
825
826 #[cfg(feature = "charset")]
828 #[test]
829 fn decode_response_detects_windows_1252() {
830 assert_eq!("café", decode_response(vec![b'c', b'a', b'f', 0xE9], None));
831 }
832
833 #[cfg(not(feature = "charset"))]
834 #[test]
835 fn decode_response_is_lossy_without_charset() {
836 assert_eq!("caf\u{FFFD}", decode_response(vec![b'c', b'a', b'f', 0xE9], None));
837 }
838}