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