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