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