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