we (web engine): Experimental web browser project to understand the limits of Claude
1//! Resource loader: fetch URLs and decode their content.
2//!
3//! Brings together `net` (HTTP client), `encoding` (charset detection and decoding),
4//! `url` (URL parsing and resolution), and `image` (image decoding) into a single
5//! `ResourceLoader` that the browser uses to load web pages and subresources.
6
7use std::fmt;
8
9use we_encoding::sniff::sniff_encoding;
10use we_encoding::Encoding;
11use we_net::client::{ClientError, HttpClient};
12use we_net::http::ContentType;
13use we_url::data_url::{is_data_url, parse_data_url};
14use we_url::Url;
15
16// ---------------------------------------------------------------------------
17// Error type
18// ---------------------------------------------------------------------------
19
20/// Errors that can occur during resource loading.
21#[derive(Debug)]
22pub enum LoadError {
23 /// URL parsing failed.
24 InvalidUrl(String),
25 /// Network or HTTP error from the underlying client.
26 Network(ClientError),
27 /// HTTP response indicated an error status.
28 HttpStatus { status: u16, reason: String },
29 /// Encoding or decoding error.
30 Encoding(String),
31}
32
33impl fmt::Display for LoadError {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 match self {
36 Self::InvalidUrl(s) => write!(f, "invalid URL: {s}"),
37 Self::Network(e) => write!(f, "network error: {e}"),
38 Self::HttpStatus { status, reason } => {
39 write!(f, "HTTP {status} {reason}")
40 }
41 Self::Encoding(s) => write!(f, "encoding error: {s}"),
42 }
43 }
44}
45
46impl From<ClientError> for LoadError {
47 fn from(e: ClientError) -> Self {
48 Self::Network(e)
49 }
50}
51
52// ---------------------------------------------------------------------------
53// Resource types
54// ---------------------------------------------------------------------------
55
56/// A loaded resource with its decoded content and metadata.
57#[derive(Debug)]
58pub enum Resource {
59 /// An HTML document.
60 Html {
61 text: String,
62 base_url: Url,
63 encoding: Encoding,
64 },
65 /// A CSS stylesheet.
66 Css { text: String, url: Url },
67 /// A decoded image.
68 Image {
69 data: Vec<u8>,
70 mime_type: String,
71 url: Url,
72 },
73 /// A JavaScript script.
74 Script { text: String, url: Url },
75 /// Any other resource type (binary).
76 Other {
77 data: Vec<u8>,
78 mime_type: String,
79 url: Url,
80 },
81}
82
83// ---------------------------------------------------------------------------
84// ResourceLoader
85// ---------------------------------------------------------------------------
86
87/// Loads resources over HTTP/HTTPS with encoding detection and content-type handling.
88pub struct ResourceLoader {
89 client: HttpClient,
90}
91
92impl ResourceLoader {
93 /// Create a new resource loader with default settings.
94 pub fn new() -> Self {
95 Self {
96 client: HttpClient::new(),
97 }
98 }
99
100 /// Fetch a resource at the given URL.
101 ///
102 /// Determines the resource type from the HTTP Content-Type header, decodes
103 /// text resources using the appropriate character encoding (per WHATWG spec),
104 /// and returns the result as a typed `Resource`.
105 ///
106 /// Handles `data:` and `about:` URLs locally without network access.
107 pub fn fetch(&mut self, url: &Url) -> Result<Resource, LoadError> {
108 // Handle data: URLs without network fetch.
109 if url.scheme() == "data" {
110 return fetch_data_url(&url.serialize());
111 }
112
113 // Handle about: URLs without network fetch.
114 if url.scheme() == "about" {
115 return fetch_about_url(url);
116 }
117
118 let response = self.client.get(url)?;
119
120 // Check for HTTP error status codes
121 if response.status_code >= 400 {
122 return Err(LoadError::HttpStatus {
123 status: response.status_code,
124 reason: response.reason.clone(),
125 });
126 }
127
128 let content_type = response.content_type();
129 let mime = content_type
130 .as_ref()
131 .map(|ct| ct.mime_type.as_str())
132 .unwrap_or("application/octet-stream");
133
134 match classify_mime(mime) {
135 MimeClass::Html => {
136 let (text, encoding) =
137 decode_text_resource(&response.body, content_type.as_ref(), true);
138 Ok(Resource::Html {
139 text,
140 base_url: url.clone(),
141 encoding,
142 })
143 }
144 MimeClass::Css => {
145 let (text, _encoding) =
146 decode_text_resource(&response.body, content_type.as_ref(), false);
147 Ok(Resource::Css {
148 text,
149 url: url.clone(),
150 })
151 }
152 MimeClass::Script => {
153 let (text, _encoding) =
154 decode_text_resource(&response.body, content_type.as_ref(), false);
155 Ok(Resource::Script {
156 text,
157 url: url.clone(),
158 })
159 }
160 MimeClass::Image => Ok(Resource::Image {
161 data: response.body,
162 mime_type: mime.to_string(),
163 url: url.clone(),
164 }),
165 MimeClass::Other => {
166 // Check if it's a text type we should decode
167 if mime.starts_with("text/") {
168 let (text, _encoding) =
169 decode_text_resource(&response.body, content_type.as_ref(), false);
170 Ok(Resource::Other {
171 data: text.into_bytes(),
172 mime_type: mime.to_string(),
173 url: url.clone(),
174 })
175 } else {
176 Ok(Resource::Other {
177 data: response.body,
178 mime_type: mime.to_string(),
179 url: url.clone(),
180 })
181 }
182 }
183 }
184 }
185
186 /// Fetch a URL string, resolving it against an optional base URL.
187 ///
188 /// Handles `data:` and `about:` URLs locally without network access.
189 pub fn fetch_url(&mut self, url_str: &str, base: Option<&Url>) -> Result<Resource, LoadError> {
190 // Handle data URLs directly — no network fetch needed.
191 if is_data_url(url_str) {
192 return fetch_data_url(url_str);
193 }
194
195 // Handle about: URLs without network fetch.
196 if url_str.starts_with("about:") {
197 let url =
198 Url::parse(url_str).map_err(|_| LoadError::InvalidUrl(url_str.to_string()))?;
199 return fetch_about_url(&url);
200 }
201
202 let url = match base {
203 Some(base_url) => Url::parse_with_base(url_str, base_url)
204 .or_else(|_| Url::parse(url_str))
205 .map_err(|_| LoadError::InvalidUrl(url_str.to_string()))?,
206 None => Url::parse(url_str).map_err(|_| LoadError::InvalidUrl(url_str.to_string()))?,
207 };
208 self.fetch(&url)
209 }
210}
211
212impl Default for ResourceLoader {
213 fn default() -> Self {
214 Self::new()
215 }
216}
217
218// ---------------------------------------------------------------------------
219// MIME classification
220// ---------------------------------------------------------------------------
221
222enum MimeClass {
223 Html,
224 Css,
225 Script,
226 Image,
227 Other,
228}
229
230fn classify_mime(mime: &str) -> MimeClass {
231 match mime {
232 "text/html" | "application/xhtml+xml" => MimeClass::Html,
233 "text/css" => MimeClass::Css,
234 "text/javascript" | "application/javascript" | "application/x-javascript" => {
235 MimeClass::Script
236 }
237 "image/png" | "image/jpeg" | "image/gif" | "image/webp" | "image/svg+xml" => {
238 MimeClass::Image
239 }
240 _ => MimeClass::Other,
241 }
242}
243
244// ---------------------------------------------------------------------------
245// Text decoding
246// ---------------------------------------------------------------------------
247
248/// Decode a text resource's bytes to a String using WHATWG encoding sniffing.
249///
250/// For HTML resources, uses BOM > HTTP charset > meta prescan > default.
251/// For non-HTML text resources, uses BOM > HTTP charset > default (UTF-8).
252fn decode_text_resource(
253 bytes: &[u8],
254 content_type: Option<&ContentType>,
255 is_html: bool,
256) -> (String, Encoding) {
257 let http_ct_value = content_type.map(|ct| {
258 // Reconstruct a Content-Type header value for the sniffing function
259 match &ct.charset {
260 Some(charset) => format!("{}; charset={}", ct.mime_type, charset),
261 None => ct.mime_type.clone(),
262 }
263 });
264
265 if is_html {
266 // Full WHATWG sniffing: BOM > HTTP > meta prescan > default (Windows-1252)
267 let (encoding, _source) = sniff_encoding(bytes, http_ct_value.as_deref());
268 let text = decode_with_bom_handling(bytes, encoding);
269 (text, encoding)
270 } else {
271 // Non-HTML: BOM > HTTP charset > default (UTF-8)
272 let (bom_enc, after_bom) = we_encoding::bom_sniff(bytes);
273 if let Some(enc) = bom_enc {
274 let text = we_encoding::decode(after_bom, enc);
275 return (text, enc);
276 }
277
278 // Try HTTP charset
279 if let Some(charset) = content_type.and_then(|ct| ct.charset.as_deref()) {
280 if let Some(enc) = we_encoding::lookup(charset) {
281 let text = we_encoding::decode(bytes, enc);
282 return (text, enc);
283 }
284 }
285
286 // Default to UTF-8 for non-HTML text
287 let text = we_encoding::decode(bytes, Encoding::Utf8);
288 (text, Encoding::Utf8)
289 }
290}
291
292/// Decode bytes with BOM handling — strip BOM bytes before decoding.
293fn decode_with_bom_handling(bytes: &[u8], encoding: Encoding) -> String {
294 let (bom_enc, after_bom) = we_encoding::bom_sniff(bytes);
295 if bom_enc.is_some() {
296 // BOM was present — decode the bytes after the BOM
297 we_encoding::decode(after_bom, encoding)
298 } else {
299 we_encoding::decode(bytes, encoding)
300 }
301}
302
303// ---------------------------------------------------------------------------
304// Data URL handling
305// ---------------------------------------------------------------------------
306
307/// Fetch a data URL, decoding its payload and returning the appropriate Resource type.
308fn fetch_data_url(url_str: &str) -> Result<Resource, LoadError> {
309 let parsed = parse_data_url(url_str)
310 .map_err(|e| LoadError::InvalidUrl(format!("data URL error: {e}")))?;
311
312 let mime = &parsed.mime_type;
313
314 // Create a synthetic Url for the resource metadata.
315 let url = Url::parse(url_str).map_err(|_| LoadError::InvalidUrl(url_str.to_string()))?;
316
317 match classify_mime(mime) {
318 MimeClass::Html => {
319 let encoding = charset_to_encoding(parsed.charset.as_deref());
320 let text = we_encoding::decode(&parsed.data, encoding);
321 Ok(Resource::Html {
322 text,
323 base_url: url,
324 encoding,
325 })
326 }
327 MimeClass::Css => {
328 let encoding = charset_to_encoding(parsed.charset.as_deref());
329 let text = we_encoding::decode(&parsed.data, encoding);
330 Ok(Resource::Css { text, url })
331 }
332 MimeClass::Script => {
333 let encoding = charset_to_encoding(parsed.charset.as_deref());
334 let text = we_encoding::decode(&parsed.data, encoding);
335 Ok(Resource::Script { text, url })
336 }
337 MimeClass::Image => Ok(Resource::Image {
338 data: parsed.data,
339 mime_type: mime.to_string(),
340 url,
341 }),
342 MimeClass::Other => {
343 if mime.starts_with("text/") {
344 let encoding = charset_to_encoding(parsed.charset.as_deref());
345 let text = we_encoding::decode(&parsed.data, encoding);
346 Ok(Resource::Other {
347 data: text.into_bytes(),
348 mime_type: mime.to_string(),
349 url,
350 })
351 } else {
352 Ok(Resource::Other {
353 data: parsed.data,
354 mime_type: mime.to_string(),
355 url,
356 })
357 }
358 }
359 }
360}
361
362// ---------------------------------------------------------------------------
363// about: URL handling
364// ---------------------------------------------------------------------------
365
366/// The minimal HTML document for about:blank.
367pub const ABOUT_BLANK_HTML: &str = "<!DOCTYPE html><html><head></head><body></body></html>";
368
369/// Fetch an about: URL, returning the appropriate resource.
370///
371/// Currently only `about:blank` is supported, which returns an empty HTML
372/// document with UTF-8 encoding.
373fn fetch_about_url(url: &Url) -> Result<Resource, LoadError> {
374 match url.path().as_str() {
375 "blank" => Ok(Resource::Html {
376 text: ABOUT_BLANK_HTML.to_string(),
377 base_url: url.clone(),
378 encoding: Encoding::Utf8,
379 }),
380 other => Err(LoadError::InvalidUrl(format!(
381 "unsupported about: URL: about:{other}"
382 ))),
383 }
384}
385
386/// Map a charset name to an Encoding, defaulting to UTF-8.
387fn charset_to_encoding(charset: Option<&str>) -> Encoding {
388 charset
389 .and_then(we_encoding::lookup)
390 .unwrap_or(Encoding::Utf8)
391}
392
393// ---------------------------------------------------------------------------
394// Tests
395// ---------------------------------------------------------------------------
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 // -----------------------------------------------------------------------
402 // LoadError Display
403 // -----------------------------------------------------------------------
404
405 #[test]
406 fn load_error_display_invalid_url() {
407 let e = LoadError::InvalidUrl("bad://url".to_string());
408 assert_eq!(e.to_string(), "invalid URL: bad://url");
409 }
410
411 #[test]
412 fn load_error_display_http_status() {
413 let e = LoadError::HttpStatus {
414 status: 404,
415 reason: "Not Found".to_string(),
416 };
417 assert_eq!(e.to_string(), "HTTP 404 Not Found");
418 }
419
420 #[test]
421 fn load_error_display_encoding() {
422 let e = LoadError::Encoding("bad charset".to_string());
423 assert_eq!(e.to_string(), "encoding error: bad charset");
424 }
425
426 // -----------------------------------------------------------------------
427 // MIME classification
428 // -----------------------------------------------------------------------
429
430 #[test]
431 fn classify_text_html() {
432 assert!(matches!(classify_mime("text/html"), MimeClass::Html));
433 }
434
435 #[test]
436 fn classify_xhtml() {
437 assert!(matches!(
438 classify_mime("application/xhtml+xml"),
439 MimeClass::Html
440 ));
441 }
442
443 #[test]
444 fn classify_text_css() {
445 assert!(matches!(classify_mime("text/css"), MimeClass::Css));
446 }
447
448 #[test]
449 fn classify_image_png() {
450 assert!(matches!(classify_mime("image/png"), MimeClass::Image));
451 }
452
453 #[test]
454 fn classify_image_jpeg() {
455 assert!(matches!(classify_mime("image/jpeg"), MimeClass::Image));
456 }
457
458 #[test]
459 fn classify_image_gif() {
460 assert!(matches!(classify_mime("image/gif"), MimeClass::Image));
461 }
462
463 #[test]
464 fn classify_application_json() {
465 assert!(matches!(
466 classify_mime("application/json"),
467 MimeClass::Other
468 ));
469 }
470
471 #[test]
472 fn classify_text_plain() {
473 assert!(matches!(classify_mime("text/plain"), MimeClass::Other));
474 }
475
476 #[test]
477 fn classify_octet_stream() {
478 assert!(matches!(
479 classify_mime("application/octet-stream"),
480 MimeClass::Other
481 ));
482 }
483
484 // -----------------------------------------------------------------------
485 // Text decoding — HTML
486 // -----------------------------------------------------------------------
487
488 #[test]
489 fn decode_html_utf8_bom() {
490 let bytes = b"\xEF\xBB\xBF<html>Hello</html>";
491 let (text, enc) = decode_text_resource(bytes, None, true);
492 assert_eq!(enc, Encoding::Utf8);
493 assert_eq!(text, "<html>Hello</html>");
494 }
495
496 #[test]
497 fn decode_html_utf8_from_http_charset() {
498 let ct = ContentType {
499 mime_type: "text/html".to_string(),
500 charset: Some("utf-8".to_string()),
501 };
502 let bytes = b"<html>Hello</html>";
503 let (text, enc) = decode_text_resource(bytes, Some(&ct), true);
504 assert_eq!(enc, Encoding::Utf8);
505 assert_eq!(text, "<html>Hello</html>");
506 }
507
508 #[test]
509 fn decode_html_meta_charset() {
510 let html = b"<meta charset=\"utf-8\"><html>Hello</html>";
511 let (text, enc) = decode_text_resource(html, None, true);
512 assert_eq!(enc, Encoding::Utf8);
513 assert!(text.contains("Hello"));
514 }
515
516 #[test]
517 fn decode_html_default_windows_1252() {
518 let bytes = b"<html>Hello</html>";
519 let (text, enc) = decode_text_resource(bytes, None, true);
520 assert_eq!(enc, Encoding::Windows1252);
521 assert!(text.contains("Hello"));
522 }
523
524 #[test]
525 fn decode_html_windows_1252_special_chars() {
526 // \x93 and \x94 are left/right double quotation marks in Windows-1252
527 let bytes = b"<html>\x93Hello\x94</html>";
528 let (text, enc) = decode_text_resource(bytes, None, true);
529 assert_eq!(enc, Encoding::Windows1252);
530 assert!(text.contains('\u{201C}')); // left double quote
531 assert!(text.contains('\u{201D}')); // right double quote
532 }
533
534 #[test]
535 fn decode_html_bom_beats_http_charset() {
536 let ct = ContentType {
537 mime_type: "text/html".to_string(),
538 charset: Some("windows-1252".to_string()),
539 };
540 let mut bytes = vec![0xEF, 0xBB, 0xBF];
541 bytes.extend_from_slice(b"<html>Hello</html>");
542 let (text, enc) = decode_text_resource(&bytes, Some(&ct), true);
543 assert_eq!(enc, Encoding::Utf8);
544 assert_eq!(text, "<html>Hello</html>");
545 }
546
547 // -----------------------------------------------------------------------
548 // Text decoding — non-HTML (CSS, etc.)
549 // -----------------------------------------------------------------------
550
551 #[test]
552 fn decode_css_utf8_default() {
553 let bytes = b"body { color: red; }";
554 let (text, enc) = decode_text_resource(bytes, None, false);
555 assert_eq!(enc, Encoding::Utf8);
556 assert_eq!(text, "body { color: red; }");
557 }
558
559 #[test]
560 fn decode_css_bom_utf8() {
561 let bytes = b"\xEF\xBB\xBFbody { color: red; }";
562 let (text, enc) = decode_text_resource(bytes, None, false);
563 assert_eq!(enc, Encoding::Utf8);
564 assert_eq!(text, "body { color: red; }");
565 }
566
567 #[test]
568 fn decode_css_http_charset() {
569 let ct = ContentType {
570 mime_type: "text/css".to_string(),
571 charset: Some("utf-8".to_string()),
572 };
573 let bytes = b"body { color: red; }";
574 let (text, enc) = decode_text_resource(bytes, Some(&ct), false);
575 assert_eq!(enc, Encoding::Utf8);
576 assert_eq!(text, "body { color: red; }");
577 }
578
579 // -----------------------------------------------------------------------
580 // BOM handling
581 // -----------------------------------------------------------------------
582
583 #[test]
584 fn decode_with_bom_strips_utf8_bom() {
585 let bytes = b"\xEF\xBB\xBFHello";
586 let text = decode_with_bom_handling(bytes, Encoding::Utf8);
587 assert_eq!(text, "Hello");
588 }
589
590 #[test]
591 fn decode_without_bom_passes_through() {
592 let bytes = b"Hello";
593 let text = decode_with_bom_handling(bytes, Encoding::Utf8);
594 assert_eq!(text, "Hello");
595 }
596
597 #[test]
598 fn decode_with_utf16le_bom() {
599 let bytes = b"\xFF\xFEH\x00e\x00l\x00l\x00o\x00";
600 let text = decode_with_bom_handling(bytes, Encoding::Utf16Le);
601 assert_eq!(text, "Hello");
602 }
603
604 #[test]
605 fn decode_with_utf16be_bom() {
606 let bytes = b"\xFE\xFF\x00H\x00e\x00l\x00l\x00o";
607 let text = decode_with_bom_handling(bytes, Encoding::Utf16Be);
608 assert_eq!(text, "Hello");
609 }
610
611 // -----------------------------------------------------------------------
612 // ResourceLoader construction
613 // -----------------------------------------------------------------------
614
615 #[test]
616 fn resource_loader_new() {
617 let _loader = ResourceLoader::new();
618 }
619
620 #[test]
621 fn resource_loader_default() {
622 let _loader = ResourceLoader::default();
623 }
624
625 // -----------------------------------------------------------------------
626 // URL resolution
627 // -----------------------------------------------------------------------
628
629 #[test]
630 fn fetch_url_invalid_url_error() {
631 let mut loader = ResourceLoader::new();
632 let result = loader.fetch_url("not a url at all", None);
633 assert!(matches!(result, Err(LoadError::InvalidUrl(_))));
634 }
635
636 #[test]
637 fn fetch_url_relative_without_base_errors() {
638 let mut loader = ResourceLoader::new();
639 let result = loader.fetch_url("/relative/path", None);
640 assert!(matches!(result, Err(LoadError::InvalidUrl(_))));
641 }
642
643 #[test]
644 fn fetch_url_relative_with_base_resolves() {
645 let mut loader = ResourceLoader::new();
646 let base = Url::parse("http://example.com/page").unwrap();
647 // This will fail since we can't actually connect in tests,
648 // but the URL resolution itself should work (it won't be InvalidUrl).
649 let result = loader.fetch_url("/style.css", Some(&base));
650 assert!(result.is_err());
651 // The error should NOT be InvalidUrl — the URL resolved successfully.
652 assert!(!matches!(result, Err(LoadError::InvalidUrl(_))));
653 }
654
655 // -----------------------------------------------------------------------
656 // Data URL loading
657 // -----------------------------------------------------------------------
658
659 #[test]
660 fn data_url_plain_text() {
661 let mut loader = ResourceLoader::new();
662 let result = loader.fetch_url("data:text/plain,Hello%20World", None);
663 assert!(result.is_ok());
664 match result.unwrap() {
665 Resource::Other {
666 data, mime_type, ..
667 } => {
668 assert_eq!(mime_type, "text/plain");
669 assert_eq!(String::from_utf8(data).unwrap(), "Hello World");
670 }
671 other => panic!("expected Other, got {:?}", other),
672 }
673 }
674
675 #[test]
676 fn data_url_html() {
677 let mut loader = ResourceLoader::new();
678 let result = loader.fetch_url("data:text/html,<h1>Hello</h1>", None);
679 assert!(result.is_ok());
680 match result.unwrap() {
681 Resource::Html { text, .. } => {
682 assert_eq!(text, "<h1>Hello</h1>");
683 }
684 other => panic!("expected Html, got {:?}", other),
685 }
686 }
687
688 #[test]
689 fn data_url_css() {
690 let mut loader = ResourceLoader::new();
691 let result = loader.fetch_url("data:text/css,body{color:red}", None);
692 assert!(result.is_ok());
693 match result.unwrap() {
694 Resource::Css { text, .. } => {
695 assert_eq!(text, "body{color:red}");
696 }
697 other => panic!("expected Css, got {:?}", other),
698 }
699 }
700
701 #[test]
702 fn data_url_image() {
703 let mut loader = ResourceLoader::new();
704 let result = loader.fetch_url("data:image/png;base64,/wCq", None);
705 assert!(result.is_ok());
706 match result.unwrap() {
707 Resource::Image {
708 data, mime_type, ..
709 } => {
710 assert_eq!(mime_type, "image/png");
711 assert_eq!(data, vec![0xFF, 0x00, 0xAA]);
712 }
713 other => panic!("expected Image, got {:?}", other),
714 }
715 }
716
717 #[test]
718 fn data_url_base64() {
719 let mut loader = ResourceLoader::new();
720 let result = loader.fetch_url("data:text/plain;base64,SGVsbG8=", None);
721 assert!(result.is_ok());
722 match result.unwrap() {
723 Resource::Other { data, .. } => {
724 assert_eq!(String::from_utf8(data).unwrap(), "Hello");
725 }
726 other => panic!("expected Other, got {:?}", other),
727 }
728 }
729
730 #[test]
731 fn data_url_empty() {
732 let mut loader = ResourceLoader::new();
733 let result = loader.fetch_url("data:,", None);
734 assert!(result.is_ok());
735 }
736
737 #[test]
738 fn data_url_via_fetch_method() {
739 let mut loader = ResourceLoader::new();
740 let url = Url::parse("data:text/plain,Hello").unwrap();
741 let result = loader.fetch(&url);
742 assert!(result.is_ok());
743 match result.unwrap() {
744 Resource::Other { data, .. } => {
745 assert_eq!(String::from_utf8(data).unwrap(), "Hello");
746 }
747 other => panic!("expected Other, got {:?}", other),
748 }
749 }
750
751 #[test]
752 fn data_url_invalid() {
753 let mut loader = ResourceLoader::new();
754 let result = loader.fetch_url("data:text/plain", None);
755 assert!(matches!(result, Err(LoadError::InvalidUrl(_))));
756 }
757
758 #[test]
759 fn data_url_binary() {
760 let mut loader = ResourceLoader::new();
761 let result = loader.fetch_url("data:application/octet-stream;base64,/wCq", None);
762 assert!(result.is_ok());
763 match result.unwrap() {
764 Resource::Other {
765 data, mime_type, ..
766 } => {
767 assert_eq!(mime_type, "application/octet-stream");
768 assert_eq!(data, vec![0xFF, 0x00, 0xAA]);
769 }
770 other => panic!("expected Other, got {:?}", other),
771 }
772 }
773
774 // -----------------------------------------------------------------------
775 // about: URL loading
776 // -----------------------------------------------------------------------
777
778 #[test]
779 fn about_blank_via_fetch_url() {
780 let mut loader = ResourceLoader::new();
781 let result = loader.fetch_url("about:blank", None);
782 assert!(result.is_ok());
783 match result.unwrap() {
784 Resource::Html {
785 text,
786 encoding,
787 base_url,
788 ..
789 } => {
790 assert_eq!(text, ABOUT_BLANK_HTML);
791 assert_eq!(encoding, Encoding::Utf8);
792 assert_eq!(base_url.scheme(), "about");
793 }
794 other => panic!("expected Html, got {:?}", other),
795 }
796 }
797
798 #[test]
799 fn about_blank_via_fetch() {
800 let mut loader = ResourceLoader::new();
801 let url = Url::parse("about:blank").unwrap();
802 let result = loader.fetch(&url);
803 assert!(result.is_ok());
804 match result.unwrap() {
805 Resource::Html {
806 text,
807 encoding,
808 base_url,
809 ..
810 } => {
811 assert_eq!(text, ABOUT_BLANK_HTML);
812 assert_eq!(encoding, Encoding::Utf8);
813 assert_eq!(base_url.scheme(), "about");
814 }
815 other => panic!("expected Html, got {:?}", other),
816 }
817 }
818
819 #[test]
820 fn about_blank_dom_structure() {
821 let doc = we_html::parse_html(ABOUT_BLANK_HTML);
822
823 // Find the <html> element under the document root.
824 let html = doc
825 .children(doc.root())
826 .find(|&n| doc.tag_name(n) == Some("html"));
827 assert!(html.is_some(), "document should have an <html> element");
828 let html = html.unwrap();
829
830 // The DOM should have html > head + body structure.
831 let children: Vec<_> = doc
832 .children(html)
833 .filter(|&n| doc.tag_name(n).is_some())
834 .collect();
835 assert_eq!(children.len(), 2);
836 assert_eq!(doc.tag_name(children[0]).unwrap(), "head");
837 assert_eq!(doc.tag_name(children[1]).unwrap(), "body");
838
839 // Body should have no child elements.
840 let body_children: Vec<_> = doc
841 .children(children[1])
842 .filter(|&n| doc.tag_name(n).is_some())
843 .collect();
844 assert!(body_children.is_empty());
845 }
846
847 #[test]
848 fn about_unsupported_url() {
849 let mut loader = ResourceLoader::new();
850 let result = loader.fetch_url("about:invalid", None);
851 assert!(matches!(result, Err(LoadError::InvalidUrl(_))));
852 }
853}