Skip to main content

orinium_browser/browser/core/
resource_loader.rs

1//! Browser resource loading process.
2//!
3//! Supports the `http(s)://` scheme (via the platform `NetworkCore`), and the
4//! network-free `resource:///`, `data:` and `file://` schemes.
5
6use crate::engine::origin::Origin;
7use crate::engine::permissions::Permissions;
8use crate::engine::port::{InProcess, Mailbox, Outbox, Transport};
9use crate::platform::network::{
10    NetworkConfig, NetworkCore, NetworkError, NetworkRequest, StatusCode,
11};
12use anyhow::{Context, Result, anyhow};
13use base64::Engine;
14use std::fmt;
15use url::Url;
16
17/// Whether a document holding `permissions` may load a resource addressed by
18/// `url`.
19///
20/// Capabilities are granted by the parent frame (see
21/// [`crate::engine::permissions::Permissions`]); an ordinary `resource://`
22/// document therefore has no local-file access, and internal schemes require the
23/// internal capability that only chrome origins hold.
24///
25/// This gate consults only scheme capabilities. Whether the response may later
26/// be *read* is a separate decision made by CORS at the call site; a CORS header
27/// can never make a disallowed scheme fetch happen, and a granted scheme never
28/// implies read access.
29fn scheme_allowed(permissions: Permissions, url: &Url) -> bool {
30    permissions.allows(url)
31}
32
33/// BrowserResourceLoader
34///
35/// High-level resource loading abstraction used by the browser core to obtain
36/// content for tabs and internal resources.
37///
38/// Responsibilities:
39/// - Resolve and fetch resources from `resource:///` scheme (bundled/local) and
40///   from standard HTTP/HTTPS URLs.
41/// - Decode `data:` URLs (base64 or percent-encoded payloads) without touching
42///   the network stack.
43/// - Provide a small synchronous/queuing abstraction over the platform network
44///   core so callers in the engine/browser can request resources without dealing
45///   with the network implementation details.
46///
47/// Processing flow (overview):
48/// 1. Caller requests a URL (`resource:///...`, `data:...` or `http(s)://...`).
49/// 2. Network-free schemes (`resource`, `data`) are resolved locally and posted
50///    to the internal channel as `BrowserNetworkMessage`s.
51/// 3. For HTTP/HTTPS, loader forwards the request to `NetworkCore` and manages
52///    request ids / pending responses. When the network reply is ready, the
53///    loader hands the response back to the browser/tab via the expected
54///    callback or message path.
55///
56/// Example usage:
57/// ```no_run
58/// use orinium_browser::browser::core::resource_loader::BrowserResourceLoader;
59/// use orinium_browser::platform::network::NetworkCore;
60///
61/// let loader = BrowserResourceLoader::new(NetworkCore::new().ok());
62/// ```
63///
64/// The loader is `Send`: it owns its [`NetworkCore`] outright rather than
65/// sharing it through a single-threaded handle. `BrowserApp` is not `Send`
66/// yet, because it still holds per-window `BrowserUi` state; the loader is
67/// free of that so it can move once the page boundary does.
68///
69/// Notes for contributors:
70/// - Keep the loader focused on scheme resolution, simple caching/pooling,
71///   and delegation to `NetworkCore`. Avoid adding heavy parsing logic here.
72/// - Do not reintroduce `Rc` (or any other `!Send` handle) around
73///   `NetworkCore`; it would make this type impossible to move.
74/// - Unit tests should validate `resource:///` and `data:` resolution and HTTP
75///   request delegation semantics (e.g. mapping of request IDs to responses).
76pub struct BrowserResourceLoader {
77    /// Optional platform network core used for HTTP/HTTPS requests.
78    ///
79    /// Owned directly: `NetworkCore` is already a handle to a separate
80    /// network process and every operation takes `&self`, so there is nothing
81    /// to share.
82    network: Option<NetworkCore>,
83
84    /// Where results of the network-free schemes are posted. A channel rather
85    /// than a `Vec` so a transport that carries these elsewhere keeps the same
86    /// call sites.
87    immediate_outbox: Outbox<BrowserNetworkMessage>,
88
89    /// The reading half of the same channel.
90    immediate_inbox: Mailbox<BrowserNetworkMessage>,
91}
92
93impl BrowserResourceLoader {
94    /// Construct a new resource loader.
95    ///
96    /// `network` is optional to allow operating in environments where the
97    /// network stack is not available (tests, limited examples, or when only
98    /// `resource:///` is needed).
99    pub fn new(network: Option<NetworkCore>) -> Self {
100        Self::with_transport(network, &InProcess)
101    }
102
103    /// Constructs a loader whose internal channel is carried by `transport`.
104    ///
105    /// The default [`InProcess`] hands messages over directly. Generic rather
106    /// than `&dyn Transport` because pairing the ends is generic over the
107    /// message type, which costs object safety — and the transport is only
108    /// needed here, so it does not have to be stored either.
109    pub fn with_transport<T: Transport>(network: Option<NetworkCore>, transport: &T) -> Self {
110        let (immediate_outbox, immediate_inbox) = transport.channel();
111        Self {
112            network,
113            immediate_outbox,
114            immediate_inbox,
115        }
116    }
117
118    /// Constructs a loader backed by `network`.
119    pub fn with_network(network: NetworkCore) -> Self {
120        Self::new(Some(network))
121    }
122
123    /// Constructs a loader that performs no network I/O.
124    ///
125    /// `resource:///`, `data:` and `file://` still resolve; anything else is
126    /// dropped.
127    pub fn offline() -> Self {
128        Self::new(None)
129    }
130
131    /// Applies a new configuration to the network process, if present.
132    pub fn set_network_config(&self, config: NetworkConfig) {
133        if let Some(net) = &self.network {
134            net.set_network_config(config);
135        }
136    }
137
138    /// Drops every cached response held by the network process, if present.
139    pub fn clear_cache(&self) {
140        if let Some(net) = &self.network {
141            net.clear_cache();
142        }
143    }
144
145    /// Async fetch: resolve immediate schemes (`resource` / `data`) in place and
146    /// push the result to `immediate_pool`; delegate all other schemes to `NetworkCore`.
147    ///
148    /// `initiator` is the origin of the requesting document and `permissions` its
149    /// effective capabilities. Scheme access is enforced here: a document can
150    /// never reach a scheme its `permissions` do not allow.
151    pub fn fetch_async(
152        &mut self,
153        url: Url,
154        id: usize,
155        initiator: &Origin,
156        permissions: Permissions,
157    ) {
158        self.fetch_request_async(
159            NetworkRequest::get(url.to_string()),
160            id,
161            initiator,
162            permissions,
163        );
164    }
165
166    /// Fetches a request while preserving method, headers, and body for HTTP(S).
167    pub fn fetch_request_async(
168        &mut self,
169        request: NetworkRequest,
170        id: usize,
171        initiator: &Origin,
172        permissions: Permissions,
173    ) {
174        let Ok(url) = Url::parse(&request.url) else {
175            self.post_immediate(BrowserNetworkMessage {
176                id,
177                response: Err(BrowserNetworkError::AnyhowError(anyhow!(
178                    "Invalid request URL: {}",
179                    request.url
180                ))),
181            });
182            return;
183        };
184        if !scheme_allowed(permissions, &url) {
185            log::warn!(
186                "Blocked {} from {} (scheme access denied)",
187                url,
188                initiator.ascii_serialization()
189            );
190            self.post_immediate(BrowserNetworkMessage {
191                id,
192                response: Err(BrowserNetworkError::AnyhowError(anyhow!(
193                    "Blocked request for {url}: the requesting page is not allowed to access this scheme"
194                ))),
195            });
196            return;
197        }
198        let Some(body) = load_immediate(&url) else {
199            if let Some(net) = &self.network {
200                net.fetch_request_async(request, id);
201            }
202            return;
203        };
204        let msg = BrowserNetworkMessage {
205            id,
206            response: if request.method == "GET" && request.body.is_empty() {
207                body.map(|body| make_response(&url, body))
208                    .map_err(BrowserNetworkError::AnyhowError)
209            } else {
210                Err(BrowserNetworkError::AnyhowError(anyhow!(
211                    "{} is not supported for {} URLs",
212                    request.method,
213                    url.scheme()
214                )))
215            },
216        };
217        self.post_immediate(msg);
218    }
219
220    /// Queues a message for the next `try_receive`.
221    ///
222    /// A send can only fail if the reading half is gone, and this type owns
223    /// both halves, so the `expect` documents that invariant.
224    fn post_immediate(&self, message: BrowserNetworkMessage) {
225        self.immediate_outbox
226            .send(message)
227            .expect("the loader owns the reading end of its own channel");
228    }
229
230    pub fn fetch_blocking(&self, url: Url) -> Result<BrowserResponse> {
231        if let Some(body) = load_immediate(&url) {
232            return body.map(|body| make_response(&url, body));
233        }
234        let Some(net) = &self.network else {
235            return Err(anyhow!("NetworkCore not available"));
236        };
237        net.fetch_blocking(url.as_str())
238            .map(|resp| BrowserResponse {
239                url: resp.url,
240                status: resp.status,
241                status_text: resp.reason_phrase,
242                body: resp.body,
243                headers: resp.headers,
244            })
245            .map_err(|e| anyhow!("NetworkError: {}", e))
246    }
247
248    /// Called from the UI thread: collect received network and immediate-scheme results.
249    pub fn try_receive(&mut self) -> Vec<BrowserNetworkMessage> {
250        let mut msgs: Vec<BrowserNetworkMessage> = self
251            .network
252            .as_ref()
253            .map(|net| {
254                net.try_receive()
255                    .into_iter()
256                    .map(|msg| BrowserNetworkMessage {
257                        id: msg.msg_id,
258                        response: msg
259                            .response
260                            .map(|resp| BrowserResponse {
261                                url: resp.url,
262                                status: resp.status,
263                                status_text: resp.reason_phrase,
264                                body: resp.body,
265                                headers: resp.headers,
266                            })
267                            .map_err(BrowserNetworkError::NetworkError),
268                    })
269                    .collect()
270            })
271            .unwrap_or_default();
272        msgs.extend(std::iter::from_fn(|| self.immediate_inbox.try_recv()));
273
274        msgs
275    }
276}
277
278/// Loads the body of schemes that are resolved without the network.
279///
280/// Returns `None` for schemes that must be delegated to `NetworkCore`.
281fn load_immediate(url: &Url) -> Option<Result<Vec<u8>>> {
282    match url.scheme() {
283        "resource" => Some(ResourceURI::load(url.as_str())),
284        "data" => Some(DataURI::decode(url.as_str())),
285        "file" => Some(FileURI::load(url)),
286        _ => None,
287    }
288}
289
290/// Builds a 200 OK response from the body of an immediate scheme.
291fn make_response(url: &Url, body: Vec<u8>) -> BrowserResponse {
292    BrowserResponse {
293        url: url.to_string(),
294        status: hyper::StatusCode::OK.into(),
295        status_text: "OK".to_string(),
296        body,
297        headers: vec![],
298    }
299}
300
301/// 統一レスポンス
302pub struct BrowserResponse {
303    pub url: String,
304    pub status: StatusCode,
305    pub status_text: String,
306    pub body: Vec<u8>,
307    pub headers: Vec<(String, String)>,
308}
309
310/// ネットワーク結果を UI スレッドで受け取るためのラッパー
311pub struct BrowserNetworkMessage {
312    pub id: usize,
313    pub response: Result<BrowserResponse, BrowserNetworkError>,
314}
315
316#[derive(Debug)]
317pub enum BrowserNetworkError {
318    NetworkError(NetworkError),
319    AnyhowError(anyhow::Error),
320}
321
322impl fmt::Display for BrowserNetworkError {
323    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324        match self {
325            Self::NetworkError(ne) => write!(f, "{ne}"),
326            Self::AnyhowError(ae) => write!(f, "{ae}"),
327        }
328    }
329}
330
331/// resource:/// 専用
332pub struct ResourceURI;
333
334impl ResourceURI {
335    pub fn load(url: &str) -> Result<Vec<u8>, anyhow::Error> {
336        use crate::platform::io;
337        if let Some(path) = url.strip_prefix("resource:///") {
338            io::load_resource(path)
339        } else {
340            Err(anyhow!("Unsupported scheme: {}", url))
341        }
342    }
343}
344
345/// `data:` URL decoder (RFC 2397)
346///
347/// Format: `data:[<mediatype>][;base64],<payload>`
348/// - With the `;base64` flag: base64-decode the payload (ignoring whitespace).
349/// - Otherwise: percent-decode the payload into bytes.
350pub struct DataURI;
351
352impl DataURI {
353    pub fn decode(url: &str) -> Result<Vec<u8>> {
354        let rest = url
355            .strip_prefix("data:")
356            .with_context(|| format!("Not a data: URL: {url}"))?;
357        let (metadata, payload) = rest
358            .split_once(',')
359            .context("data: URL is missing the ',' delimiter")?;
360
361        if metadata.to_ascii_lowercase().contains(";base64") {
362            let decoded = percent_decode(payload);
363            let cleaned: Vec<u8> = decoded
364                .into_iter()
365                .filter(|b| !b.is_ascii_whitespace())
366                .collect();
367            base64::engine::general_purpose::STANDARD
368                .decode(cleaned)
369                .context("failed to decode base64 data: URL")
370        } else {
371            Ok(percent_decode(payload))
372        }
373    }
374}
375
376/// `file://` 用ローダー。
377///
378/// URL をローカルファイルシステムのパスに変換して読み込む。`file://host/...`
379/// のように空でも `localhost` でもないホストを伴う URL は拒否し、ローカルの
380/// ファイル URL 以外は解決しない。
381pub struct FileURI;
382
383impl FileURI {
384    pub fn load(url: &Url) -> Result<Vec<u8>> {
385        let path = url
386            .to_file_path()
387            .map_err(|()| anyhow!("Unsupported file URL (non-local host): {url}"))?;
388        crate::platform::io::load_local_file(&path.to_string_lossy())
389    }
390}
391
392/// Converts `%XX` sequences into their byte values. Invalid `%` sequences are kept as-is.
393fn percent_decode(input: &str) -> Vec<u8> {
394    let bytes = input.as_bytes();
395    let mut out = Vec::with_capacity(bytes.len());
396    let mut i = 0;
397    while i < bytes.len() {
398        if bytes[i] == b'%'
399            && i + 2 < bytes.len()
400            && let (Some(hi), Some(lo)) = (hex_value(bytes[i + 1]), hex_value(bytes[i + 2]))
401        {
402            out.push(hi * 16 + lo);
403            i += 3;
404        } else {
405            out.push(bytes[i]);
406            i += 1;
407        }
408    }
409    out
410}
411
412fn hex_value(b: u8) -> Option<u8> {
413    match b {
414        b'0'..=b'9' => Some(b - b'0'),
415        b'a'..=b'f' => Some(b - b'a' + 10),
416        b'A'..=b'F' => Some(b - b'A' + 10),
417        _ => None,
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424    use crate::engine::port::{Consume, Disconnected, Produce, Undeliverable};
425    use crate::test_support::assert_send;
426    use std::sync::atomic::{AtomicUsize, Ordering};
427
428    /// The loader owns its `NetworkCore`, so it must be movable across
429    /// threads. This is the invariant that a `!Send` handle around
430    /// `NetworkCore` would silently break, so it is asserted directly.
431    #[test]
432    fn loader_is_send() {
433        assert_send::<BrowserResourceLoader>();
434        assert_send::<BrowserNetworkMessage>();
435        assert_send::<BrowserResponse>();
436        assert_send::<BrowserNetworkError>();
437    }
438
439    /// A loader can be built and dropped without ever opening a device or a
440    /// socket, which is what makes the offline mode usable in tests.
441    #[test]
442    fn offline_loader_needs_no_network_stack() {
443        let loader = BrowserResourceLoader::offline();
444        assert!(loader.network.is_none());
445    }
446
447    /// Creates a unique temporary file with `contents` and returns its path.
448    /// The caller is responsible for removing the file.
449    fn temp_file(contents: &[u8]) -> std::path::PathBuf {
450        static COUNTER: AtomicUsize = AtomicUsize::new(0);
451        let name = format!(
452            "orinium-file-uri-{}-{}",
453            std::process::id(),
454            COUNTER.fetch_add(1, Ordering::Relaxed)
455        );
456        let path = std::env::temp_dir().join(name);
457        std::fs::write(&path, contents).unwrap();
458        path
459    }
460
461    #[test]
462    fn data_uri_decodes_base64_payload() {
463        let encoded = base64::engine::general_purpose::STANDARD.encode(b"hello");
464        let url = format!("data:image/png;base64,{encoded}");
465        assert_eq!(DataURI::decode(&url).unwrap(), b"hello");
466    }
467
468    #[test]
469    fn data_uri_decodes_plain_payload() {
470        let url = "data:text/plain,hello%20world";
471        assert_eq!(DataURI::decode(url).unwrap(), b"hello world");
472    }
473
474    #[test]
475    fn data_uri_ignores_whitespace_in_base64_payload() {
476        let encoded = base64::engine::general_purpose::STANDARD.encode(b"line1line2");
477        let url = format!("data:text/plain;base64,{encoded}\n\r\t ");
478        assert_eq!(DataURI::decode(&url).unwrap(), b"line1line2");
479    }
480
481    #[test]
482    fn data_uri_rejects_missing_delimiter() {
483        assert!(DataURI::decode("data:text/plain").is_err());
484    }
485
486    #[test]
487    fn data_uri_rejects_invalid_base64() {
488        assert!(DataURI::decode("data:text/plain;base64,%%%").is_err());
489    }
490
491    #[test]
492    fn data_uri_flag_is_case_insensitive() {
493        let encoded = base64::engine::general_purpose::STANDARD.encode(b"ok");
494        let url = format!("data:text/plain;BASE64,{encoded}");
495        assert_eq!(DataURI::decode(&url).unwrap(), b"ok");
496    }
497
498    #[test]
499    fn url_parse_preserves_data_url() {
500        let url = Url::parse("data:image/png;base64,AAAA").unwrap();
501        assert_eq!(url.scheme(), "data");
502        assert_eq!(url.as_str(), "data:image/png;base64,AAAA");
503    }
504
505    #[test]
506    fn fetch_blocking_decodes_data_url_without_network() {
507        let loader = BrowserResourceLoader::offline();
508        let encoded = base64::engine::general_purpose::STANDARD.encode(b"png-bytes");
509        let url = Url::parse(&format!("data:image/png;base64,{encoded}")).unwrap();
510
511        let resp = loader.fetch_blocking(url).unwrap();
512        assert!(resp.status.is_success());
513        assert_eq!(resp.body, b"png-bytes");
514    }
515
516    #[test]
517    fn fetch_async_pushes_data_url_into_immediate_pool() {
518        let mut loader = BrowserResourceLoader::offline();
519        let url = Url::parse("data:text/plain,hi").unwrap();
520        let permissions = Permissions::content_for(&url);
521
522        loader.fetch_async(url, 7, &Origin::opaque(), permissions);
523        let msgs = loader.try_receive();
524        assert_eq!(msgs.len(), 1);
525        assert_eq!(msgs[0].id, 7);
526        let resp = msgs[0].response.as_ref().unwrap();
527        assert_eq!(resp.body, b"hi");
528    }
529
530    #[test]
531    fn immediate_urls_reject_non_get_requests() {
532        let mut loader = BrowserResourceLoader::offline();
533        loader.fetch_request_async(
534            NetworkRequest {
535                url: "data:text/plain,hi".to_string(),
536                method: "POST".to_string(),
537                headers: Vec::new(),
538                body: b"request body".to_vec(),
539            },
540            8,
541            &Origin::opaque(),
542            Permissions::chrome(),
543        );
544
545        let messages = loader.try_receive();
546        assert_eq!(messages.len(), 1);
547        assert_eq!(messages[0].id, 8);
548        assert!(messages[0].response.is_err());
549    }
550
551    #[test]
552    fn network_origin_cannot_reach_resource_scheme() {
553        let mut loader = BrowserResourceLoader::offline();
554        let web = Origin::from_url(&Url::parse("https://example.test/").unwrap());
555        let permissions = Permissions::content_for(&Url::parse("https://example.test/").unwrap());
556
557        loader.fetch_async(
558            Url::parse("resource:///devtools/index.html").unwrap(),
559            9,
560            &web,
561            permissions,
562        );
563
564        let messages = loader.try_receive();
565        assert_eq!(messages.len(), 1);
566        assert_eq!(messages[0].id, 9);
567        assert!(messages[0].response.is_err());
568    }
569
570    #[test]
571    fn internal_origin_can_reach_resource_scheme() {
572        let mut loader = BrowserResourceLoader::offline();
573        let internal = Origin::internal();
574
575        loader.fetch_async(
576            Url::parse("resource:///devtools/index.html").unwrap(),
577            10,
578            &internal,
579            Permissions::chrome(),
580        );
581
582        let messages = loader.try_receive();
583        assert_eq!(messages.len(), 1);
584        assert_eq!(messages[0].id, 10);
585        assert!(messages[0].response.as_ref().is_ok());
586    }
587
588    #[test]
589    fn any_origin_can_reach_data_scheme() {
590        let mut loader = BrowserResourceLoader::offline();
591        let web = Origin::from_url(&Url::parse("https://example.test/").unwrap());
592        let permissions = Permissions::content_for(&Url::parse("https://example.test/").unwrap());
593
594        loader.fetch_async(
595            Url::parse("data:text/plain,hi").unwrap(),
596            11,
597            &web,
598            permissions,
599        );
600
601        let messages = loader.try_receive();
602        assert_eq!(messages.len(), 1);
603        assert_eq!(messages[0].id, 11);
604        let resp = messages[0].response.as_ref().unwrap();
605        assert_eq!(resp.body, b"hi");
606    }
607
608    #[test]
609    fn data_scheme_requires_network_permission() {
610        // Reading a data: response is gated at fetch time by the network
611        // capability, not by the CORS read check.
612        let mut loader = BrowserResourceLoader::offline();
613
614        loader.fetch_async(
615            Url::parse("data:text/plain,hi").unwrap(),
616            25,
617            &Origin::opaque(),
618            Permissions::none(),
619        );
620
621        let messages = loader.try_receive();
622        assert_eq!(messages.len(), 1);
623        assert_eq!(messages[0].id, 25);
624        assert!(messages[0].response.is_err());
625    }
626
627    #[test]
628    fn fetch_blocking_rejects_data_url_without_network() {
629        let loader = BrowserResourceLoader::offline();
630        let url = Url::parse("data:image/png;base64,@@@not-base64@@@").unwrap();
631        assert!(loader.fetch_blocking(url).is_err());
632    }
633
634    #[test]
635    fn file_uri_loads_local_file_bytes() {
636        let path = temp_file(b"file content");
637        let url = Url::from_file_path(&path).unwrap();
638
639        assert_eq!(FileURI::load(&url).unwrap(), b"file content");
640        let _ = std::fs::remove_file(&path);
641    }
642
643    #[test]
644    fn file_url_with_nonlocal_host_is_rejected() {
645        let url = Url::parse("file://evil.example/etc/passwd").unwrap();
646        assert!(FileURI::load(&url).is_err());
647    }
648
649    #[test]
650    fn fetch_async_resolves_file_url_into_immediate_pool() {
651        let path = temp_file(b"file bytes");
652        let url = Url::from_file_path(&path).unwrap();
653        let mut loader = BrowserResourceLoader::offline();
654
655        loader.fetch_async(url, 12, &Origin::internal(), Permissions::chrome());
656        let msgs = loader.try_receive();
657        assert_eq!(msgs.len(), 1);
658        assert_eq!(msgs[0].id, 12);
659        let resp = msgs[0].response.as_ref().unwrap();
660        assert_eq!(resp.body, b"file bytes");
661        let _ = std::fs::remove_file(&path);
662    }
663
664    #[test]
665    fn internal_origin_can_read_local_file() {
666        let path = temp_file(b"local data");
667        let url = Url::from_file_path(&path).unwrap();
668        let mut loader = BrowserResourceLoader::offline();
669
670        loader.fetch_async(url, 13, &Origin::internal(), Permissions::chrome());
671        let msgs = loader.try_receive();
672        assert_eq!(msgs.len(), 1);
673        let resp = msgs[0].response.as_ref().unwrap();
674        assert_eq!(resp.body, b"local data");
675        let _ = std::fs::remove_file(&path);
676    }
677
678    #[test]
679    fn network_origin_cannot_read_local_file() {
680        let path = temp_file(b"secret");
681        let url = Url::from_file_path(&path).unwrap();
682        let web = Origin::from_url(&Url::parse("https://example.test/").unwrap());
683        let permissions = Permissions::content_for(&Url::parse("https://example.test/").unwrap());
684        let mut loader = BrowserResourceLoader::offline();
685
686        loader.fetch_async(url, 14, &web, permissions);
687        let msgs = loader.try_receive();
688        assert_eq!(msgs.len(), 1);
689        assert!(msgs[0].response.is_err());
690        let _ = std::fs::remove_file(&path);
691    }
692
693    /// Regression guard for the chrome-escalation bug: a page loaded from a
694    /// `resource://` URL holds the `resource` capability but must never be
695    /// treated as chrome, so it cannot reach `file://`.
696    #[test]
697    fn resource_page_cannot_read_local_file() {
698        let path = temp_file(b"secret");
699        let url = Url::from_file_path(&path).unwrap();
700        let resource_url = Url::parse("resource:///devtools/index.html").unwrap();
701        let origin = Origin::from_url(&resource_url);
702        let permissions = Permissions::content_for(&resource_url);
703        let mut loader = BrowserResourceLoader::offline();
704
705        assert!(permissions.resource);
706        assert!(!permissions.is_chrome());
707        loader.fetch_async(url, 24, &origin, permissions);
708        let msgs = loader.try_receive();
709        assert_eq!(msgs.len(), 1);
710        assert!(msgs[0].response.is_err());
711        let _ = std::fs::remove_file(&path);
712    }
713
714    #[test]
715    fn data_origin_cannot_reach_resource_scheme() {
716        let mut loader = BrowserResourceLoader::offline();
717        let data_origin = Origin::from_url(&Url::parse("data:text/html,evil").unwrap());
718        let permissions = Permissions::content_for(&Url::parse("data:text/html,evil").unwrap());
719
720        loader.fetch_async(
721            Url::parse("resource:///devtools/index.html").unwrap(),
722            20,
723            &data_origin,
724            permissions,
725        );
726
727        let messages = loader.try_receive();
728        assert_eq!(messages.len(), 1);
729        assert_eq!(messages[0].id, 20);
730        assert!(messages[0].response.is_err());
731    }
732
733    #[test]
734    fn data_origin_cannot_reach_file_scheme() {
735        let path = temp_file(b"secret");
736        let url = Url::from_file_path(&path).unwrap();
737        let data_origin = Origin::from_url(&Url::parse("data:text/html,evil").unwrap());
738        let permissions = Permissions::content_for(&Url::parse("data:text/html,evil").unwrap());
739        let mut loader = BrowserResourceLoader::offline();
740
741        loader.fetch_async(url, 21, &data_origin, permissions);
742        let msgs = loader.try_receive();
743        assert_eq!(msgs.len(), 1);
744        assert!(msgs[0].response.is_err());
745        let _ = std::fs::remove_file(&path);
746    }
747
748    #[test]
749    fn opaque_origin_cannot_reach_resource_or_file() {
750        let opaque = Origin::opaque();
751        let mut loader = BrowserResourceLoader::offline();
752
753        loader.fetch_async(
754            Url::parse("resource:///devtools/index.html").unwrap(),
755            22,
756            &opaque,
757            Permissions::none(),
758        );
759        let messages = loader.try_receive();
760        assert_eq!(messages.len(), 1);
761        assert!(messages[0].response.is_err());
762
763        let path = temp_file(b"secret");
764        let file_url = Url::from_file_path(&path).unwrap();
765        loader.fetch_async(file_url, 23, &opaque, Permissions::none());
766        let messages = loader.try_receive();
767        assert_eq!(messages.len(), 1);
768        assert!(messages[0].response.is_err());
769        let _ = std::fs::remove_file(&path);
770    }
771
772    /// A loader must behave the same over a transport that is not the default.
773    ///
774    /// [`Buffered`] is a different implementation, not the in-process channel
775    /// wearing another name, so this fails the moment the loader reaches past
776    /// the port and depends on `InProcess` specifically.
777    #[test]
778    fn results_arrive_over_a_transport_other_than_the_default() {
779        let mut loader = BrowserResourceLoader::with_transport(None, &Buffered);
780        let url = Url::parse("data:text/plain,relayed").unwrap();
781
782        let permissions = Permissions::content_for(&url);
783        loader.fetch_async(url, 15, &Origin::opaque(), permissions);
784        let msgs = loader.try_receive();
785
786        assert_eq!(msgs.len(), 1);
787        assert_eq!(msgs[0].id, 15);
788        assert_eq!(msgs[0].response.as_ref().unwrap().body, b"relayed");
789    }
790
791    #[test]
792    fn draining_an_idle_loader_reports_nothing_without_blocking() {
793        let mut loader = BrowserResourceLoader::with_transport(None, &Buffered);
794        assert!(loader.try_receive().is_empty());
795    }
796
797    /// The loader's channel must not stop the loader being movable, which is
798    /// the invariant the page boundary depends on. `loader_is_send` above
799    /// asserts the bound; this moves one to check the claim.
800    #[test]
801    fn loader_can_move_between_threads() {
802        let loader = BrowserResourceLoader::offline();
803        let url = Url::parse("data:text/plain,moved").unwrap();
804        let (sender, receiver) = std::sync::mpsc::channel();
805
806        std::thread::spawn(move || {
807            let mut loader = loader;
808            let permissions = Permissions::content_for(&url);
809            loader.fetch_async(url, 16, &Origin::opaque(), permissions);
810            for message in loader.try_receive() {
811                sender.send(message.id).expect("receiver is alive");
812            }
813        })
814        .join()
815        .expect("the loader thread finished");
816
817        assert_eq!(receiver.recv().expect("a message was posted"), 16);
818    }
819
820    /// A second [`Transport`] implementation, so the tests above cannot pass by
821    /// relying on [`InProcess`] specifically.
822    #[derive(Debug, Default, Clone, Copy)]
823    struct Buffered;
824
825    impl Transport for Buffered {
826        fn channel<T: Send + 'static>(&self) -> (Outbox<T>, Mailbox<T>) {
827            struct Queue<T> {
828                messages: std::sync::Mutex<std::collections::VecDeque<T>>,
829                arrived: std::sync::Condvar,
830            }
831
832            impl<T> Queue<T> {
833                fn push(&self, message: T) {
834                    self.messages
835                        .lock()
836                        .expect("lock poisoned")
837                        .push_back(message);
838                    self.arrived.notify_all();
839                }
840
841                fn pop(&self) -> Option<T> {
842                    self.messages.lock().expect("lock poisoned").pop_front()
843                }
844
845                fn pop_waiting(&self) -> Result<T, Disconnected> {
846                    let mut messages = self.messages.lock().expect("lock poisoned");
847                    loop {
848                        if let Some(message) = messages.pop_front() {
849                            return Ok(message);
850                        }
851                        messages = self.arrived.wait(messages).expect("lock poisoned");
852                    }
853                }
854            }
855
856            struct Enqueuer<T>(std::sync::Arc<Queue<T>>);
857            struct Dequeuer<T>(std::sync::Arc<Queue<T>>);
858
859            impl<T: Send + 'static> Produce<T> for Enqueuer<T> {
860                fn send(&self, message: T) -> Result<(), Undeliverable<T>> {
861                    self.0.push(message);
862                    Ok(())
863                }
864            }
865
866            impl<T: Send + 'static> Consume<T> for Dequeuer<T> {
867                fn recv(&self) -> Result<T, Disconnected> {
868                    self.0.pop_waiting()
869                }
870
871                fn try_recv(&self) -> Option<T> {
872                    self.0.pop()
873                }
874            }
875
876            let queue = std::sync::Arc::new(Queue {
877                messages: std::sync::Mutex::new(std::collections::VecDeque::new()),
878                arrived: std::sync::Condvar::new(),
879            });
880            (
881                Outbox::of(Enqueuer(std::sync::Arc::clone(&queue))),
882                Mailbox::of(Dequeuer(queue)),
883            )
884        }
885    }
886}