Skip to main content

script/
unminify.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 std::env;
6use std::fs::{File, create_dir_all};
7use std::io::{Error, ErrorKind, Read, Seek, Write};
8use std::path::{Path, PathBuf};
9use std::process::Command;
10
11use servo_url::ServoUrl;
12use tempfile::NamedTempFile;
13use uuid::Uuid;
14
15pub(crate) trait ScriptSource {
16    fn unminified_dir(&self) -> Option<String>;
17    fn extract_bytes(&self) -> &[u8];
18    fn rewrite_source(&mut self, source: String);
19    fn url(&self) -> ServoUrl;
20    fn is_external(&self) -> bool;
21}
22
23pub(crate) fn create_temp_files() -> Option<(NamedTempFile, File)> {
24    // Write the minified code to a temporary file and pass its path as an argument
25    // to js-beautify to read from. Meanwhile, redirect the process' stdout into
26    // another temporary file and read that into a string. This avoids some hangs
27    // observed on macOS when using direct input/output pipes with very large
28    // unminified content.
29    let (input, output) = (NamedTempFile::new(), tempfile::tempfile());
30    if let (Ok(input), Ok(output)) = (input, output) {
31        Some((input, output))
32    } else {
33        log::warn!("Error creating input and output temp files");
34        None
35    }
36}
37
38#[derive(Debug)]
39pub(crate) enum BeautifyFileType {
40    Css,
41    Js,
42}
43
44pub(crate) fn execute_js_beautify(input: &Path, output: File, file_type: BeautifyFileType) -> bool {
45    let mut cmd = Command::new("js-beautify");
46    match file_type {
47        BeautifyFileType::Js => (),
48        BeautifyFileType::Css => {
49            cmd.arg("--type").arg("css");
50        },
51    }
52    match cmd.arg(input).stdout(output).status() {
53        Ok(status) => status.success(),
54        _ => {
55            log::warn!(
56                "Failed to execute js-beautify --type {:?}, Will store unmodified script",
57                file_type
58            );
59            false
60        },
61    }
62}
63
64pub fn create_output_file(
65    unminified_dir: String,
66    url: &ServoUrl,
67    external: Option<bool>,
68) -> Result<File, Error> {
69    let path = PathBuf::from(unminified_dir);
70
71    if url.scheme() == "data" {
72        return Err(Error::new(
73            ErrorKind::InvalidInput,
74            "data URLs cannot be written as unminified files",
75        ));
76    }
77
78    // Strip the query string from the URL before using it as a file path.
79    // '?' is a reserved character on Windows and causes file creation to fail
80    // silently. BeforeHost..AfterPath stops the slice before the '?' separator.
81    let url_path = &url[url::Position::BeforeHost..url::Position::AfterPath];
82
83    let (base, has_name) = match url.as_str().ends_with('/') {
84        true => (path.join(url_path).as_path().to_owned(), false),
85        false => (path.join(url_path).parent().unwrap().to_owned(), true),
86    };
87
88    create_dir_all(&base)?;
89
90    let path = if external.unwrap_or(true) && has_name {
91        // External.
92        path.join(url_path)
93    } else {
94        // Inline file or url ends with '/'
95        base.join(Uuid::new_v4().to_string())
96    };
97
98    debug!("Unminified files will be stored in {:?}", path);
99
100    File::create(path)
101}
102
103pub(crate) fn unminify_js(script: &mut dyn ScriptSource) {
104    let Some(unminified_dir) = script.unminified_dir() else {
105        return;
106    };
107
108    if let Some((mut input, mut output)) = create_temp_files() {
109        input.write_all(script.extract_bytes()).unwrap();
110
111        if execute_js_beautify(
112            input.path(),
113            output.try_clone().unwrap(),
114            BeautifyFileType::Js,
115        ) {
116            let mut script_content = String::new();
117            output.seek(std::io::SeekFrom::Start(0)).unwrap();
118            output.read_to_string(&mut script_content).unwrap();
119            script.rewrite_source(script_content);
120        }
121    }
122
123    match create_output_file(unminified_dir, &script.url(), Some(script.is_external())) {
124        Ok(mut file) => file.write_all(script.extract_bytes()).unwrap(),
125        Err(why) => warn!("Could not store script {:?}", why),
126    }
127}
128
129pub(crate) fn unminified_path(dir: &str) -> String {
130    let mut path = env::current_dir().unwrap();
131    path.push(dir);
132    path.into_os_string().into_string().unwrap()
133}