Skip to main content

servoshell/
prefs.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use core::panic;
6use std::cell::Cell;
7use std::collections::HashMap;
8use std::fs::{self, read_to_string};
9use std::path::{Path, PathBuf};
10use std::rc::Rc;
11use std::str::FromStr;
12#[cfg(any(target_os = "android", target_env = "ohos"))]
13use std::sync::OnceLock;
14use std::{env, fmt};
15
16use bpaf::*;
17use euclid::Size2D;
18use log::warn;
19use serde_json::Value;
20use servo::user_contents::UserStyleSheet;
21use servo::{
22    DeviceIndependentPixel, DiagnosticsLogging, DiagnosticsLoggingOption, Opts, OutputOptions,
23    PrefValue, Preferences,
24};
25use url::Url;
26
27use crate::VERSION;
28
29/// Preferences enabled when servoshell is launched with the `--enable-experimental-web-platform-features` flag.
30///
31/// These preferences are disabled by default but activated in experimental mode.
32/// For more details, see the
33/// [experimental features documentation](https://book.servo.org/design-documentation/experimental-features.html).
34pub(crate) static EXPERIMENTAL_PREFS: &[&str] = &[
35    "dom_async_clipboard_enabled",
36    "dom_exec_command_enabled",
37    "dom_fontface_enabled",
38    "dom_indexeddb_enabled",
39    "dom_intersection_observer_enabled",
40    "dom_navigator_protocol_handlers_enabled",
41    "dom_notification_enabled",
42    "dom_offscreen_canvas_enabled",
43    "dom_permissions_enabled",
44    "dom_sanitizer_enabled",
45    "dom_storage_manager_api_enabled",
46    "dom_webgl2_enabled",
47    "dom_webgpu_enabled",
48    "layout_css_alpha_color_function_enabled",
49    "layout_css_ellipse_corners_enabled",
50    "layout_css_progress_function_enabled",
51    "layout_columns_enabled",
52    "layout_container_queries_enabled",
53    "layout_variable_fonts_enabled",
54];
55
56#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
57#[derive(Clone)]
58pub(crate) struct ServoShellPreferences {
59    /// A URL to load when starting servoshell.
60    pub url: Option<String>,
61    /// An override value for the device pixel ratio.
62    pub device_pixel_ratio_override: Option<f32>,
63    /// Whether or not to attempt clean shutdown.
64    pub clean_shutdown: bool,
65    /// Enable native window's titlebar and decorations.
66    pub no_native_titlebar: bool,
67    /// URL string of the homepage.
68    pub homepage: String,
69    /// URL string of the search engine page with '%s' standing in for the search term.
70    /// For example <https://duckduckgo.com/html/?q=%s>.
71    pub searchpage: String,
72    /// Whether or not to run servoshell in headless mode. While running in headless
73    /// mode, image output is supported.
74    pub headless: bool,
75    /// Filter directives for our tracing implementation.
76    ///
77    /// Overrides directives specified via `SERVO_TRACING` if set.
78    /// See: <https://docs.rs/tracing-subscriber/0.3.19/tracing_subscriber/filter/struct.EnvFilter.html#directives>
79    pub tracing_filter: Option<String>,
80    /// The initial requested inner size of the window.
81    pub initial_window_size: Size2D<u32, DeviceIndependentPixel>,
82    /// An override for the screen resolution. This is useful for testing behavior on different screen sizes,
83    /// such as the screen of a mobile device.
84    pub screen_size_override: Option<Size2D<u32, DeviceIndependentPixel>>,
85    /// Whether or not to simulate touch events using mouse events.
86    pub simulate_touch_events: bool,
87    /// If not-None, the path to a file to output the default WebView's rendered output
88    /// after waiting for a stable image, this implies `Self::exit_after_load`.
89    pub output_image_path: Option<String>,
90    /// Whether or not to exit after Servo detects a stable output image in all WebViews.
91    pub exit_after_stable_image: bool,
92    /// Where to load userscripts from, if any.
93    /// and if the option isn't passed userscripts won't be loaded.
94    pub userscripts_directory: Option<PathBuf>,
95    /// A set of [`UserStylesheets`] to load for content.
96    pub user_stylesheets: Vec<Rc<UserStyleSheet>>,
97    /// `None` to disable WebDriver or `Some` with a port number to start a server to listen to
98    /// remote WebDriver commands.
99    pub webdriver_port: Cell<Option<u16>>,
100    /// Whether the CLI option to enable experimental prefs was present at startup.
101    pub experimental_preferences_enabled: bool,
102    /// Log filter given in the `log_filter` spec as a String, if any.
103    /// If a filter is passed, the logger should adjust accordingly.
104    #[cfg(target_env = "ohos")]
105    pub log_filter: Option<String>,
106    /// Log also to a file
107    #[cfg(target_env = "ohos")]
108    pub log_to_file: bool,
109}
110
111impl Default for ServoShellPreferences {
112    fn default() -> Self {
113        Self {
114            clean_shutdown: false,
115            device_pixel_ratio_override: None,
116            headless: false,
117            homepage: "https://servo.org".into(),
118            initial_window_size: Size2D::new(1024, 740),
119            no_native_titlebar: true,
120            screen_size_override: None,
121            simulate_touch_events: false,
122            searchpage: "https://duckduckgo.com/html/?q=%s".into(),
123            tracing_filter: None,
124            url: None,
125            output_image_path: None,
126            exit_after_stable_image: false,
127            userscripts_directory: None,
128            user_stylesheets: Default::default(),
129            webdriver_port: Cell::new(None),
130            #[cfg(target_env = "ohos")]
131            log_filter: None,
132            #[cfg(target_env = "ohos")]
133            log_to_file: false,
134            experimental_preferences_enabled: false,
135        }
136    }
137}
138
139#[cfg(all(
140    unix,
141    not(target_os = "macos"),
142    not(target_os = "ios"),
143    not(target_os = "android"),
144    not(target_env = "ohos")
145))]
146pub fn default_config_dir() -> Option<PathBuf> {
147    let mut config_dir = ::dirs::config_dir().unwrap();
148    config_dir.push("servo");
149    config_dir.push("default");
150    Some(config_dir)
151}
152
153/// Overrides the default preference dir
154#[cfg(any(target_os = "android", target_env = "ohos"))]
155pub(crate) static DEFAULT_CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
156#[cfg(any(target_os = "android", target_env = "ohos"))]
157pub fn default_config_dir() -> Option<PathBuf> {
158    DEFAULT_CONFIG_DIR.get().cloned()
159}
160
161#[cfg(target_os = "macos")]
162pub fn default_config_dir() -> Option<PathBuf> {
163    // FIXME: use `config_dir()` ($HOME/Library/Preferences)
164    // instead of `data_dir()` ($HOME/Library/Application Support) ?
165    let mut config_dir = ::dirs::data_dir().unwrap();
166    config_dir.push("Servo");
167    Some(config_dir)
168}
169
170#[cfg(target_os = "windows")]
171pub fn default_config_dir() -> Option<PathBuf> {
172    let mut config_dir = ::dirs::config_dir().unwrap();
173    config_dir.push("Servo");
174    Some(config_dir)
175}
176
177/// Get a Servo [`Preferences`] to use when initializing Servo by first reading the user
178/// preferences file and then overriding these preferences with the ones from the `--prefs-file`
179/// command-line argument, if given.
180fn get_preferences(prefs_files: &[PathBuf], config_dir: &Option<PathBuf>) -> Preferences {
181    // Do not read any preferences files from the disk when testing as we do not want it
182    // to throw off test results.
183    if cfg!(test) {
184        return Preferences::default();
185    }
186
187    let user_prefs_path = config_dir
188        .clone()
189        .map(|path| path.join("prefs.json"))
190        .filter(|path| path.exists());
191    let user_prefs_hash = user_prefs_path.map(read_prefs_file).unwrap_or_default();
192
193    let apply_preferences =
194        |preferences: &mut Preferences, preferences_hash: HashMap<String, PrefValue>| {
195            for (key, value) in preferences_hash.iter() {
196                preferences.set_value(key, value.clone());
197            }
198        };
199
200    let mut preferences = Preferences::default();
201    apply_preferences(&mut preferences, user_prefs_hash);
202    for pref_file_path in prefs_files.iter() {
203        apply_preferences(&mut preferences, read_prefs_file(pref_file_path))
204    }
205
206    preferences
207}
208
209fn read_prefs_file<P: AsRef<Path>>(path: P) -> HashMap<String, PrefValue> {
210    read_prefs_map(&read_to_string(path).expect("Error opening user prefs"))
211}
212
213pub fn read_prefs_map(txt: &str) -> HashMap<String, PrefValue> {
214    let prefs: HashMap<String, Value> = serde_json::from_str(txt)
215        .map_err(|_| panic!("Could not parse preferences JSON"))
216        .unwrap();
217    prefs
218        .into_iter()
219        .map(|(key, value)| {
220            let value = (&value)
221                .try_into()
222                .map_err(|error| panic!("{error}"))
223                .unwrap();
224            (key, value)
225        })
226        .collect()
227}
228
229#[expect(clippy::large_enum_variant)]
230#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
231pub(crate) enum ArgumentParsingResult {
232    ChromeProcess(Opts, Preferences, ServoShellPreferences),
233    ContentProcess(String),
234    Exit,
235    ErrorParsing,
236}
237
238enum ParseResolutionError {
239    InvalidFormat,
240    ZeroDimension,
241    ParseError(std::num::ParseIntError),
242}
243
244impl fmt::Display for ParseResolutionError {
245    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246        match self {
247            ParseResolutionError::InvalidFormat => write!(f, "invalid resolution format"),
248            ParseResolutionError::ZeroDimension => {
249                write!(f, "width and height must be greater than 0")
250            },
251            ParseResolutionError::ParseError(e) => write!(f, "{e}"),
252        }
253    }
254}
255
256/// Parse a resolution string into a Size2D.
257fn parse_resolution_string(
258    string: String,
259) -> Result<Option<Size2D<u32, DeviceIndependentPixel>>, ParseResolutionError> {
260    if string.is_empty() {
261        Ok(None)
262    } else {
263        let (width, height) = string
264            .split_once(['x', 'X'])
265            .ok_or(ParseResolutionError::InvalidFormat)?;
266
267        let width = width.trim();
268        let height = height.trim();
269        if width.is_empty() || height.is_empty() {
270            return Err(ParseResolutionError::InvalidFormat);
271        }
272
273        let width = width.parse().map_err(ParseResolutionError::ParseError)?;
274        let height = height.parse().map_err(ParseResolutionError::ParseError)?;
275        if width == 0 || height == 0 {
276            return Err(ParseResolutionError::ZeroDimension);
277        }
278
279        Ok(Some(Size2D::new(width, height)))
280    }
281}
282
283/// Parse a space or comma-separated list of stylesheet paths into a vector of
284/// [`UserStyleSheet`].
285fn parse_user_stylesheets(string: String) -> Result<Vec<Rc<UserStyleSheet>>, std::io::Error> {
286    let mut results = Vec::new();
287    for path_string in string.split([' ', ',']) {
288        let path = env::current_dir()?.join(path_string);
289        results.push(Rc::new(UserStyleSheet::new(
290            read_to_string(&path)?,
291            Url::from_file_path(&path).unwrap(),
292        )));
293    }
294    Ok(results)
295}
296
297/// This is a helper function that fulfills the following parsing task
298/// check for long/short cmd. If there is the flag with this
299/// If the flag is not there, parse `None``
300/// If the flag is there but no argument, parse `Some(default)`
301/// If the flag is there and an argument parse the argument
302fn flag_with_default_parser<S, T>(
303    short_cmd: Option<char>,
304    long_cmd: &'static str,
305    argument_help: &'static str,
306    help: &'static str,
307    default: T,
308    transform: fn(S) -> T,
309) -> impl Parser<Option<T>>
310where
311    S: FromStr + 'static,
312    <S as FromStr>::Err: fmt::Display,
313    T: Clone + 'static,
314{
315    let just_flag = if let Some(c) = short_cmd {
316        short(c).long(long_cmd)
317    } else {
318        long(long_cmd)
319    }
320    .req_flag(default)
321    .hide();
322
323    let arg = if let Some(c) = short_cmd {
324        short(c).long(long_cmd)
325    } else {
326        long(long_cmd)
327    }
328    .argument::<S>(argument_help)
329    .help(help)
330    .map(transform);
331
332    construct!([arg, just_flag]).optional()
333}
334
335fn profile() -> impl Parser<Option<OutputOptions>> {
336    flag_with_default_parser(
337        Some('p'),
338        "profile",
339        "",
340        "uses 5.0 output as standard if no argument supplied",
341        OutputOptions::Stdout(5.0),
342        |val: String| {
343            if let Ok(float) = val.parse::<f64>() {
344                OutputOptions::Stdout(float)
345            } else {
346                OutputOptions::FileName(val)
347            }
348        },
349    )
350}
351
352fn userscripts() -> impl Parser<Option<PathBuf>> {
353    let arg = long("userscripts")
354        .argument::<String>("your/directory")
355        .help("Uses userscripts in specified full path")
356        .map(PathBuf::from);
357
358    construct!([arg]).optional()
359}
360
361fn webdriver_port() -> impl Parser<Option<u16>> {
362    flag_with_default_parser(
363        None,
364        "webdriver",
365        "7000",
366        "Start remote WebDriver server on port",
367        7000,
368        |val| val,
369    )
370}
371
372fn map_debug_options(arg: String) -> Vec<String> {
373    arg.split(',').map(|s| s.to_owned()).collect()
374}
375
376#[derive(Bpaf, Clone, Debug)]
377#[bpaf(options, version(VERSION), usage("servoshell [OPTIONS] URL"))]
378// Newlines in comments are intentional to have the right formatting for the help message.
379struct CmdArgs {
380    /// Background Hang Monitor enabled.
381    #[bpaf(short('B'), long("bhm"))]
382    background_hang_monitor: bool,
383
384    ///
385    ///  Path to find SSL certificates.
386    #[bpaf(argument("/home/servo/resources/certs"))]
387    certificate_path: Option<PathBuf>,
388
389    /// Do not shutdown until all threads have finished (macos only).
390    #[bpaf(long)]
391    clean_shutdown: bool,
392
393    ///
394    ///  Config directory following xdg spec on linux platform.
395    #[bpaf(argument("~/.config/servo"))]
396    config_dir: Option<PathBuf>,
397
398    ///
399    ///  Run as a content process and connect to the given pipe.
400    #[bpaf(argument("servo-ipc-channel.abcdefg"))]
401    content_process: Option<String>,
402
403    ///
404    ///  A comma-separated string of debug options. Pass help to show available options.
405    #[bpaf(
406        short('Z'),
407        argument("layout_grid_enabled=true,dom_async_clipboard_enabled"),
408        long,
409        map(map_debug_options),
410        fallback(vec![])
411    )]
412    debug: Vec<String>,
413
414    ///
415    ///  Device pixels per px.
416    #[bpaf(argument("1.0"))]
417    device_pixel_ratio: Option<f32>,
418
419    /// Start remote devtools server on port listening on this address. <address>:<port> and <port> are valid values.
420    #[bpaf(argument("127.0.0.1:7000"))]
421    devtools: Option<String>,
422
423    ///
424    ///  Whether or not to enable experimental web platform features.
425    #[bpaf(long)]
426    enable_experimental_web_platform_features: bool,
427
428    // Exit after Servo has loaded the page and detected a stable output image.
429    #[bpaf(short('x'), long)]
430    exit: bool,
431
432    // Use ipc_channel in singleprocess mode.
433    #[bpaf(short('I'), long("force-ipc"))]
434    force_ipc: bool,
435
436    /// Exit on thread failure instead of displaying about:failure.
437    #[bpaf(short('f'), long)]
438    hard_fail: bool,
439
440    /// Headless mode.
441    #[bpaf(short('z'), long)]
442    headless: bool,
443
444    ///
445    /// Path to a hosts file (like `/etc/hosts`).
446    /// Ignored if the `HOST_FILE` environment variable is set.
447    #[bpaf(long("host-file"), argument("/path/to/hosts"))]
448    host_file: Option<PathBuf>,
449
450    ///
451    ///  Whether or not to completely ignore certificate errors.
452    #[bpaf(long)]
453    ignore_certificate_errors: bool,
454
455    /// Number of threads to use for layout.
456    #[bpaf(short('y'), long, argument("1"))]
457    layout_threads: Option<i64>,
458
459    ///
460    ///  Directory root with unminified scripts.
461    #[bpaf(argument("~/.local/share/servo"))]
462    local_script_source: Option<PathBuf>,
463
464    #[cfg(target_env = "ohos")]
465    /// Define a custom filter for logging.
466    #[bpaf(argument("FILTER"))]
467    log_filter: Option<String>,
468
469    #[cfg(target_env = "ohos")]
470    /// Also log to a file (/data/app/el2/100/base/org.servo.servo/cache/servo.log).
471    #[bpaf(long)]
472    log_to_file: bool,
473
474    /// Run in multiprocess mode.
475    #[bpaf(short('M'), long)]
476    multiprocess: bool,
477
478    /// Do not use native titlebar.
479    #[bpaf(short('b'), long)]
480    no_native_titlebar: bool,
481
482    /// Path to an output image. The format of the image is determined by the extension.
483    /// Supports all formats that `rust-image` does.
484    #[bpaf(short('o'), argument("test.png"), long)]
485    output: Option<PathBuf>,
486
487    /// Time profiler flag and either a TSV output filename
488    /// OR an interval for output to Stdout (blank for Stdout with interval of 5s).
489    #[bpaf(external)]
490    profile: Option<OutputOptions>,
491
492    ///
493    ///  Path to dump a self-contained HTML timeline of profiler traces.
494    #[bpaf(argument("trace.html"), long)]
495    profiler_trace_path: Option<PathBuf>,
496
497    ///
498    ///  A preference to set.
499    #[bpaf(argument("dom_bluetooth_enabled"), many)]
500    pref: Vec<String>,
501
502    ///
503    ///  Load in additional prefs from a file.
504    #[bpaf(long, argument("/path/to/prefs.json"), many)]
505    prefs_file: Vec<PathBuf>,
506
507    ///
508    ///  Probability of randomly closing a pipeline (for testing constellation hardening).
509    #[bpaf(argument("0.25"))]
510    random_pipeline_closure_probability: Option<f32>,
511
512    /// A fixed seed for repeatbility of random pipeline closure.
513    random_pipeline_closure_seed: Option<usize>,
514
515    /// Run in a sandbox if multiprocess.
516    #[bpaf(short('S'), long)]
517    sandbox: bool,
518
519    /// Shaders will be loaded from the specified directory instead of using the builtin ones.
520    shaders: Option<PathBuf>,
521
522    ///
523    ///  Override the screen resolution in logical (device independent) pixels.
524    #[bpaf(long("screen-size"), argument::<String>("1024x768"),
525        parse(parse_resolution_string), fallback(None))]
526    screen_size_override: Option<Size2D<u32, DeviceIndependentPixel>>,
527
528    /// Use mouse events to simulate touch events. Left button presses will be converted to touch
529    /// and mouse movements while the left button is pressed will be converted to touch movements.
530    #[bpaf(long("simulate-touch-events"))]
531    simulate_touch_events: bool,
532
533    /// Use temporary storage (data on disk will not persist across restarts).
534    #[bpaf(long)]
535    temporary_storage: bool,
536    /// Define a custom filter for traces. Overrides `SERVO_TRACING` if set.
537    #[bpaf(long("tracing-filter"), argument("FILTER"))]
538    tracing_filter: Option<String>,
539
540    /// Unminify Javascript.
541    #[bpaf(long)]
542    unminify_js: bool,
543
544    /// Unminify Css.
545    #[bpaf(long)]
546    unminify_css: bool,
547
548    ///
549    ///  Set custom user agent string (or ios / android / desktop for platform default).
550    #[bpaf(short('u'),long,argument::<String>("NCSA mosaic/1.0 (X11;SunOS 4.1.4 sun4m"))]
551    user_agent: Option<String>,
552
553    ///
554    ///  Uses userscripts in a specified full path.
555    #[bpaf(external)]
556    userscripts: Option<PathBuf>,
557
558    ///
559    /// Add each of the given UTF-8 encoded CSS files in the space or comma-separated
560    /// list as user stylesheet to apply to every page loaded.
561    #[bpaf(argument::<String>("file.css"), parse(parse_user_stylesheets),
562    fallback(vec![]))]
563    user_stylesheet: Vec<Rc<UserStyleSheet>>,
564
565    /// Start remote WebDriver server on port.
566    #[bpaf(external)]
567    webdriver_port: Option<u16>,
568
569    ///
570    ///  Set the initial window size in logical (device independent) pixels.
571    #[bpaf(argument::<String>("1024x740"), parse(parse_resolution_string), fallback(None))]
572    window_size: Option<Size2D<u32, DeviceIndependentPixel>>,
573
574    /// Set js_mem_gc_zeal_level=2 and js_mem_gc_zeal_frequency=1
575    #[bpaf(long)]
576    zealous_gc: bool,
577
578    /// The url we should load.
579    #[bpaf(positional("URL"), fallback(String::from("https://www.servo.org")))]
580    url: String,
581}
582
583fn update_preferences_from_command_line_arguments(
584    preferences: &mut Preferences,
585    cmd_args: &CmdArgs,
586) {
587    if let Some(listen_address) = &cmd_args.devtools {
588        preferences.devtools_server_enabled = true;
589        preferences.devtools_server_listen_address = listen_address.clone();
590    }
591
592    if cmd_args.enable_experimental_web_platform_features {
593        for pref in EXPERIMENTAL_PREFS {
594            preferences.set_value(pref, PrefValue::Bool(true));
595        }
596    }
597
598    for pref in &cmd_args.pref {
599        let split: Vec<&str> = pref.splitn(2, '=').collect();
600        let pref_name = split[0];
601        let pref_value = PrefValue::from_booleanish_str(split.get(1).copied().unwrap_or("true"));
602        preferences.set_value(pref_name, pref_value);
603    }
604
605    if let Some(layout_threads) = cmd_args.layout_threads {
606        preferences.layout_threads = layout_threads;
607    }
608
609    if cmd_args.headless && preferences.media_glvideo_enabled {
610        warn!("GL video rendering is not supported on headless windows.");
611        preferences.media_glvideo_enabled = false;
612    }
613
614    if let Some(user_agent) = cmd_args.user_agent.clone() {
615        preferences.user_agent = user_agent;
616    }
617
618    if cmd_args.webdriver_port.is_some() {
619        preferences.dom_testing_html_input_element_select_files_enabled = true;
620    }
621
622    if cmd_args.zealous_gc {
623        #[cfg(not(feature = "debugmozjs"))]
624        warn!(
625            "The zealous-gc option requires Servo to be compiled with debug-mozjs to take effect."
626        );
627
628        preferences.js_mem_gc_zeal_level = 2;
629        preferences.js_mem_gc_zeal_frequency = 1;
630    }
631}
632
633/// Parse Commandline arguments
634///
635/// Please note that e.g. `env::args` traditionally includes the binary name as the first
636///  argument; However, the binary name must not be included in `args_without_binary`.
637pub(crate) fn parse_command_line_arguments<'a>(
638    args_without_binary: impl Into<Args<'a>>,
639) -> ArgumentParsingResult {
640    parse_arguments_helper(args_without_binary.into())
641}
642fn parse_arguments_helper(args_without_binary: Args) -> ArgumentParsingResult {
643    let cmd_args = cmd_args().run_inner(args_without_binary);
644    let cmd_args = match cmd_args {
645        Ok(cmd_args) => cmd_args,
646        Err(error) => {
647            // Servo will exit after printing the parsing error, which makes the stdout / stderr
648            // redirection via a seperate thread racy, so we log directly to the system logger.
649            if cfg!(target_os = "android") || cfg!(target_env = "ohos") {
650                match &error {
651                    ParseFailure::Stderr(doc) => log::error!("{doc}"),
652                    // '--help' will be parsed by the next one.
653                    ParseFailure::Stdout(doc, _) => log::error!("{doc}"),
654                    ParseFailure::Completion(_) => log::error!("Not supported on these platforms"),
655                }
656            } else {
657                error.print_message(80);
658            }
659
660            return if error.exit_code() == 0 {
661                ArgumentParsingResult::Exit
662            } else {
663                ArgumentParsingResult::ErrorParsing
664            };
665        },
666    };
667
668    // If this is the content process, we'll receive the real options over IPC. So fill in some dummy options for now.
669    if let Some(content_process) = cmd_args.content_process {
670        return ArgumentParsingResult::ContentProcess(content_process);
671    }
672
673    let config_dir = cmd_args
674        .config_dir
675        .clone()
676        .or_else(default_config_dir)
677        .inspect(|config_dir| {
678            if !config_dir.exists() {
679                fs::create_dir_all(config_dir).expect("Could not create config_dir");
680            }
681        });
682    let temporary_storage = cmd_args.temporary_storage;
683    if let Some(ref time_profiler_trace_path) = cmd_args.profiler_trace_path {
684        let mut path = PathBuf::from(time_profiler_trace_path);
685        path.pop();
686        fs::create_dir_all(&path).expect("Error in creating profiler trace path");
687    }
688
689    let mut preferences = get_preferences(&cmd_args.prefs_file, &config_dir);
690
691    update_preferences_from_command_line_arguments(&mut preferences, &cmd_args);
692
693    // FIXME: enable JIT compilation on 32-bit Android after the startup crash issue (#31134) is fixed.
694    if cfg!(target_os = "android") && cfg!(target_pointer_width = "32") {
695        preferences.js_baseline_interpreter_enabled = false;
696        preferences.js_baseline_jit_enabled = false;
697        preferences.js_ion_enabled = false;
698    }
699
700    // Make sure the default window size is not larger than any provided screen size.
701    let default_window_size = Size2D::new(1024, 740);
702    let default_window_size = cmd_args
703        .screen_size_override
704        .map_or(default_window_size, |screen_size_override| {
705            default_window_size.min(screen_size_override)
706        });
707
708    let servoshell_preferences = ServoShellPreferences {
709        url: Some(cmd_args.url),
710        no_native_titlebar: cmd_args.no_native_titlebar,
711        device_pixel_ratio_override: cmd_args.device_pixel_ratio,
712        clean_shutdown: cmd_args.clean_shutdown,
713        headless: cmd_args.headless,
714        tracing_filter: cmd_args.tracing_filter,
715        initial_window_size: cmd_args.window_size.unwrap_or(default_window_size),
716        screen_size_override: cmd_args.screen_size_override,
717        simulate_touch_events: cmd_args.simulate_touch_events,
718        webdriver_port: Cell::new(cmd_args.webdriver_port),
719        output_image_path: cmd_args.output.map(|p| p.to_string_lossy().into_owned()),
720        exit_after_stable_image: cmd_args.exit,
721        userscripts_directory: cmd_args.userscripts,
722        user_stylesheets: cmd_args.user_stylesheet,
723        experimental_preferences_enabled: cmd_args.enable_experimental_web_platform_features,
724        #[cfg(target_env = "ohos")]
725        log_filter: cmd_args.log_filter.or_else(|| {
726            (!preferences.log_filter.is_empty()).then(|| preferences.log_filter.clone())
727        }),
728        #[cfg(target_env = "ohos")]
729        log_to_file: cmd_args.log_to_file,
730        ..Default::default()
731    };
732
733    let Ok(debug_options) = parse_diagnostics_logging(cmd_args.debug) else {
734        return ArgumentParsingResult::ErrorParsing;
735    };
736
737    let opts = Opts {
738        debug: debug_options,
739        time_profiling: cmd_args.profile,
740        time_profiler_trace_path: cmd_args
741            .profiler_trace_path
742            .map(|p| p.to_string_lossy().into_owned()),
743        hard_fail: cmd_args.hard_fail,
744        multiprocess: cmd_args.multiprocess,
745        background_hang_monitor: cmd_args.background_hang_monitor,
746        sandbox: cmd_args.sandbox,
747        random_pipeline_closure_probability: cmd_args.random_pipeline_closure_probability,
748        random_pipeline_closure_seed: cmd_args.random_pipeline_closure_seed,
749        config_dir,
750        temporary_storage,
751        shaders_path: cmd_args.shaders,
752        certificate_path: cmd_args
753            .certificate_path
754            .map(|p| p.to_string_lossy().into_owned()),
755        host_file: cmd_args.host_file,
756        ignore_certificate_errors: cmd_args.ignore_certificate_errors,
757        unminify_js: cmd_args.unminify_js,
758        local_script_source: cmd_args
759            .local_script_source
760            .map(|p| p.to_string_lossy().into_owned()),
761        unminify_css: cmd_args.unminify_css,
762        force_ipc: cmd_args.force_ipc,
763    };
764
765    ArgumentParsingResult::ChromeProcess(opts, preferences, servoshell_preferences)
766}
767
768/// Parse the '-Z' command-line flags.
769fn parse_diagnostics_logging(cli_options: Vec<String>) -> Result<DiagnosticsLogging, ()> {
770    fn print_option(name: &str, description: &str) {
771        println!("\t{:<35} {}", name, description);
772    }
773
774    if cli_options.contains(&"help".into()) {
775        // TODO: Remove hardcoded binary name by perhaps receiving this as an argument.
776        println!("Usage: servoshell -Z option,[option,...]\n\twhere options include:");
777        print_option("help", "Show this help message");
778        for option in DiagnosticsLoggingOption::iter() {
779            print_option(option.help_option(), option.help_message())
780        }
781
782        std::process::exit(0);
783    }
784
785    let mut diagnostics_logging = DiagnosticsLogging::new();
786    for cli_option in cli_options.iter() {
787        if let Err(error) = diagnostics_logging.extend_from_string(cli_option) {
788            eprintln!("Could not parse debug logging option: {error}");
789            return Err(());
790        }
791    }
792
793    Ok(diagnostics_logging)
794}
795
796#[cfg(test)]
797fn test_parse_pref(arg: &str) -> Preferences {
798    let args = ["--pref", arg];
799    match parse_command_line_arguments(args.as_slice()) {
800        ArgumentParsingResult::ContentProcess(..) => {
801            unreachable!("No preferences for content process")
802        },
803        ArgumentParsingResult::ChromeProcess(_, preferences, _) => preferences,
804        ArgumentParsingResult::Exit => {
805            panic!("we supplied a --pref argument above which should be parsed")
806        },
807        ArgumentParsingResult::ErrorParsing => {
808            unreachable!("we supplied a --pref argument above which should be parsed")
809        },
810    }
811}
812
813#[test]
814fn test_parse_pref_from_command_line() {
815    // Test with boolean values.
816    let preferences = test_parse_pref("dom_bluetooth_enabled=true");
817    assert!(preferences.dom_bluetooth_enabled);
818
819    let preferences = test_parse_pref("dom_bluetooth_enabled=false");
820    assert!(!preferences.dom_bluetooth_enabled);
821
822    // Test with numbers
823    let preferences = test_parse_pref("layout_threads=42");
824    assert_eq!(preferences.layout_threads, 42);
825
826    // Test with unsigned numbers
827    let preferences = test_parse_pref("network_http_cache_size=50");
828    assert_eq!(preferences.network_http_cache_size, 50);
829    let preferences = test_parse_pref("network_connection_timeout=30");
830    assert_eq!(preferences.network_connection_timeout, 30);
831
832    // Test string.
833    let preferences = test_parse_pref("fonts_default=Lucida");
834    assert_eq!(preferences.fonts_default, "Lucida");
835
836    // Test with no value (defaults to true).
837    let preferences = test_parse_pref("dom_bluetooth_enabled");
838    assert!(preferences.dom_bluetooth_enabled);
839}
840
841#[test]
842fn test_invalid_prefs_from_command_line_panics() {
843    let err_msg = std::panic::catch_unwind(|| {
844        test_parse_pref("doesntexist=true");
845    })
846    .err()
847    .and_then(|a| a.downcast_ref::<String>().cloned())
848    .expect("Should panic");
849    assert_eq!(
850        err_msg, "Unknown preference: \"doesntexist\"",
851        "Message should describe the problem"
852    )
853}
854
855#[test]
856fn test_create_prefs_map() {
857    let json_str = "{
858        \"layout.writing-mode.enabled\": true,
859        \"network.mime.sniff\": false,
860        \"shell.homepage\": \"https://servo.org\"
861    }";
862    assert_eq!(read_prefs_map(json_str).len(), 3);
863}
864
865#[cfg(test)]
866fn test_parse(arg: &str) -> (Opts, Preferences, ServoShellPreferences) {
867    // bpaf requires the arguments that are separated by whitespace to be different elements of the vector.
868    let args_split: Vec<&str> = arg.split_whitespace().collect();
869    match parse_command_line_arguments(args_split.as_slice()) {
870        ArgumentParsingResult::ContentProcess(..) => {
871            unreachable!("No preferences for content process")
872        },
873        ArgumentParsingResult::ChromeProcess(opts, preferences, servoshell_preferences) => {
874            (opts, preferences, servoshell_preferences)
875        },
876        ArgumentParsingResult::Exit | ArgumentParsingResult::ErrorParsing => {
877            unreachable!("We always have valid preference in our test cases")
878        },
879    }
880}
881
882#[test]
883fn test_profiling_args() {
884    assert_eq!(
885        test_parse("-p").0.time_profiling.unwrap(),
886        OutputOptions::Stdout(5_f64)
887    );
888
889    assert_eq!(
890        test_parse("-p 10").0.time_profiling.unwrap(),
891        OutputOptions::Stdout(10_f64)
892    );
893
894    assert_eq!(
895        test_parse("-p 10.0").0.time_profiling.unwrap(),
896        OutputOptions::Stdout(10_f64)
897    );
898
899    assert_eq!(
900        test_parse("-p foo.txt").0.time_profiling.unwrap(),
901        OutputOptions::FileName(String::from("foo.txt"))
902    );
903}
904
905#[test]
906fn test_servoshell_cmd() {
907    assert_eq!(
908        test_parse("--screen-size=1000x1000")
909            .2
910            .screen_size_override
911            .unwrap(),
912        Size2D::new(1000, 1000)
913    );
914
915    assert_eq!(
916        test_parse("--certificate-path=/tmp/test")
917            .0
918            .certificate_path
919            .unwrap(),
920        String::from("/tmp/test")
921    );
922
923    assert!({
924        let p = test_parse("--zealous-gc").1;
925        p.js_mem_gc_zeal_level == 2 && p.js_mem_gc_zeal_frequency == 1
926    });
927}