we (web engine): Experimental web browser project to understand the limits of Claude
at texture-validation 853 lines 29 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 /// 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}