Skip to main content

lingxia_browser_shell/
lib.rs

1//! Shell product module and host registrations for LingXia.
2//!
3//! This crate owns product-level shell behavior on top of the generic
4//! runtime crates: address-bar resolution, downloads, settings, panels,
5//! and bundled browser assets. Platform SDK crates own their native shell UI.
6
7extern crate self as lingxia;
8
9mod address_bar;
10mod bookmarks;
11mod bookmarks_html;
12mod downloads;
13mod facade;
14mod history;
15mod panel;
16mod platform_error;
17mod privacy;
18#[cfg(all(any(target_os = "macos", target_os = "windows"), feature = "proxy"))]
19mod proxy;
20#[cfg(all(any(target_os = "macos", target_os = "windows"), feature = "proxy"))]
21mod proxy_settings;
22mod settings;
23mod url_match;
24
25pub use address_bar::{resolve_input, resolve_input_json};
26pub use bookmarks::{
27    BookmarkEntry, BookmarksSnapshot, command_json as bookmarks_command_json,
28    favicon_path as bookmark_favicon_path, is_bookmarked,
29    normalize_url_for_match as normalize_bookmark_url, pin_url as pin_bookmark_url,
30    pin_url_with_favicon as pin_bookmark_url_with_favicon, remove_by_url as remove_bookmark_by_url,
31    set_change_listener as set_bookmarks_change_listener, snapshot as bookmarks_snapshot,
32    snapshot_json as bookmarks_snapshot_json, store_favicon as store_bookmark_favicon,
33    toggle_bookmark,
34};
35pub use facade::{
36    APP_ID, classify_navigation, classify_navigation_json, close, download, open, open_for_app,
37    should_hide_url, tab_path, update_tab,
38};
39pub use history::{record_visit as record_history_visit, update_title as update_history_title};
40use lingxia_browser::LxAppError;
41pub use lingxia_browser::{
42    BrowserAddressAction, BrowserAddressInputContext, BrowserAddressInputRequest,
43    BrowserAddressInputResponse, BrowserAddressInputTrigger, BrowserAddressValueKind,
44    BrowserNavigationPolicyDecision, BrowserNavigationPolicyRequest,
45    BrowserNavigationPolicyResponse, BrowserTabInfo,
46};
47#[doc(hidden)]
48pub use lingxia_native_macros::native;
49use lingxia_platform::traits::app_runtime::AppRuntime;
50#[doc(hidden)]
51pub use lxapp::LxApp;
52#[doc(hidden)]
53pub use lxapp::host;
54pub use panel::{open_panel_lxapp, panel_item_for_id, panels_config_json};
55use serde::Deserialize;
56pub use settings::set_display_language_change_listener;
57use std::collections::BTreeMap;
58use std::io::Read;
59
60const BROWSER_WEBUI_MANIFEST_ASSET_PATH: &str = "app.lingxia.browser/lxapp.json";
61const BROWSER_CONTEXT_MENU_ASSET_PATH: &str = "app.lingxia.browser/public/browser-context-menu.js";
62
63#[derive(Debug, Deserialize)]
64struct BrowserWebUiManifest {
65    #[serde(default)]
66    version: Option<String>,
67    #[serde(default)]
68    pages: Vec<BrowserWebUiPage>,
69}
70
71#[derive(Debug, Deserialize)]
72struct BrowserWebUiPage {
73    name: String,
74    path: String,
75}
76
77fn parse_internal_pages(manifest_json: &str) -> Result<BTreeMap<String, String>, LxAppError> {
78    serde_json::from_str::<BrowserWebUiManifest>(manifest_json)
79        .map(|manifest| {
80            manifest
81                .pages
82                .into_iter()
83                .map(|page| (page.name, page.path))
84                .collect()
85        })
86        .map_err(|err| {
87            LxAppError::InvalidJsonFile(format!("{}: {}", BROWSER_WEBUI_MANIFEST_ASSET_PATH, err))
88        })
89}
90
91fn read_browser_asset_text(asset_path: &str) -> Result<String, LxAppError> {
92    let runtime = lxapp::get_platform().ok_or_else(|| {
93        LxAppError::Runtime(
94            "browser asset loading requires an initialized host runtime".to_string(),
95        )
96    })?;
97    let mut reader = runtime.read_asset(asset_path).map_err(|err| {
98        LxAppError::ResourceNotFound(format!("browser asset {} ({})", asset_path, err))
99    })?;
100    let mut content = String::new();
101    reader
102        .read_to_string(&mut content)
103        .map_err(|err| LxAppError::IoError(format!("failed to read {}: {}", asset_path, err)))?;
104    Ok(content)
105}
106
107fn bundled_internal_pages() -> Result<BTreeMap<String, String>, LxAppError> {
108    let manifest = read_browser_asset_text(BROWSER_WEBUI_MANIFEST_ASSET_PATH)?;
109    parse_internal_pages(&manifest)
110}
111
112pub(crate) fn bundled_webui_version() -> Option<String> {
113    let manifest = read_browser_asset_text(BROWSER_WEBUI_MANIFEST_ASSET_PATH).ok()?;
114    serde_json::from_str::<BrowserWebUiManifest>(&manifest)
115        .ok()?
116        .version
117}
118
119fn bundled_context_menu_script() -> Result<String, LxAppError> {
120    read_browser_asset_text(BROWSER_CONTEXT_MENU_ASSET_PATH)
121}
122
123/// Host routes live in a process-global registry with no per-app scoping, so
124/// every browser-private route must gate itself on the calling app's id.
125pub(crate) fn require_builtin_browser(app: &LxApp) -> Result<(), LxAppError> {
126    require_builtin_browser_appid(&app.appid)
127}
128
129fn require_builtin_browser_appid(appid: &str) -> Result<(), LxAppError> {
130    if appid == lingxia_browser::BUILTIN_BROWSER_APPID {
131        Ok(())
132    } else {
133        Err(LxAppError::UnsupportedOperation(format!(
134            "browser route is restricted to the built-in browser (caller: {appid})"
135        )))
136    }
137}
138
139#[doc(hidden)]
140pub fn register_runtime() {
141    lingxia_browser::install_runtime();
142    downloads::register();
143    bookmarks::register();
144    history::register();
145    privacy::register();
146    lingxia_browser::set_navigation_finished_handler(std::sync::Arc::new(|url, title| {
147        history::record_visit(url, title);
148    }));
149    lingxia_browser::set_title_changed_handler(std::sync::Arc::new(|url, title| {
150        history::update_title(url, title);
151    }));
152    #[cfg(all(any(target_os = "macos", target_os = "windows"), feature = "proxy"))]
153    proxy::register();
154    settings::register();
155}
156
157#[doc(hidden)]
158pub fn register_bundled_assets() {
159    match bundled_internal_pages() {
160        Ok(internal_pages) => {
161            // Upgrade the browser host from Synthetic to a real asset bundle so the
162            // lingxia:// scheme can serve newtab/settings/downloads pages.
163            lxapp::register_builtin_asset_bundle(lingxia_browser::BUILTIN_BROWSER_APPID);
164            for (route, entry_asset) in internal_pages {
165                if let Err(err) = lingxia_browser::register_internal_page(route, entry_asset) {
166                    lxapp::warn!(
167                        "[InternalBrowser] failed to register bundled browser page: {}",
168                        err
169                    );
170                }
171            }
172        }
173        Err(err) => {
174            panic!(
175                "browser-shell requires bundled browser webui manifest at {}: {}",
176                BROWSER_WEBUI_MANIFEST_ASSET_PATH, err
177            );
178        }
179    }
180
181    match bundled_context_menu_script() {
182        Ok(script) => lingxia_browser::register_document_script(script),
183        Err(err) => {
184            lxapp::info!(
185                "[InternalBrowser] bundled browser context menu unavailable; skipping startup script: {}",
186                err
187            );
188        }
189    }
190}
191
192#[doc(hidden)]
193pub fn warmup() {
194    #[cfg(all(any(target_os = "macos", target_os = "windows"), feature = "proxy"))]
195    proxy::warmup();
196    lingxia_browser::warmup();
197}
198
199#[cfg(test)]
200mod tests {
201    use super::{parse_internal_pages, require_builtin_browser_appid};
202
203    #[test]
204    fn browser_routes_reject_other_apps() {
205        assert!(require_builtin_browser_appid(lingxia_browser::BUILTIN_BROWSER_APPID).is_ok());
206        assert!(require_builtin_browser_appid("com.example.thirdparty").is_err());
207    }
208
209    #[test]
210    fn parses_named_internal_pages_manifest() {
211        let pages = parse_internal_pages(
212            r#"{
213                "pages": [
214                    { "name": "newtab", "path": "pages/newtab/index.html" },
215                    { "name": "history", "path": "pages/history/index.html" },
216                    { "name": "downloads", "path": "pages/downloads/index.html" },
217                    { "name": "settings", "path": "pages/settings/index.html" }
218                ]
219            }"#,
220        )
221        .expect("manifest should parse");
222        assert_eq!(
223            pages.get("newtab").map(String::as_str),
224            Some("pages/newtab/index.html")
225        );
226        assert_eq!(
227            pages.get("history").map(String::as_str),
228            Some("pages/history/index.html")
229        );
230        assert_eq!(
231            pages.get("downloads").map(String::as_str),
232            Some("pages/downloads/index.html")
233        );
234        assert_eq!(
235            pages.get("settings").map(String::as_str),
236            Some("pages/settings/index.html")
237        );
238    }
239
240    #[test]
241    fn rejects_legacy_ordered_pages_manifest() {
242        assert!(parse_internal_pages(r#"{ "pages": ["pages/newtab/index.html"] }"#).is_err());
243    }
244}