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