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