// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. use std::fmt::Write as _; use std::fs; use std::io; use std::path::Path; use crate::common::{self, FileResult, LintIssue}; pub fn run(files: &[String], fix: bool, linter: &str, root: &str) { let licenses_path = Path::new(root).join("tools/lint/license/valid-licenses.txt"); let licenses = match load_valid_licenses(licenses_path.to_str().unwrap_or("")) { Ok(l) => l, Err(e) => { eprintln!("Warning: could not load valid-licenses.txt: {e}"); return; } }; let license_html_path = Path::new(root).join("toolkit/content/license.html"); let license_html_str = match license_html_path.to_str() { Some(s) => s.to_string(), None => { eprintln!( "Warning: license.html path is not valid UTF-8: {}", license_html_path.display() ); return; } }; common::par_map_lint_results(files, |path| check_license(path, &licenses, fix, linter)); if files.iter().any(|p| p == &license_html_str) { check_license_html(&license_html_str, linter, root); } } /// Load the list of license patterns. fn load_valid_licenses(path: &str) -> io::Result> { let content = fs::read_to_string(path)?; Ok(content .lines() .map(|l| l.trim().to_lowercase()) .filter(|l| !l.is_empty()) .collect()) } fn check_license(path: &str, licenses: &[String], fix: bool, linter: &str) -> Vec { let mut results = Vec::new(); if !is_valid_license(path, licenses) { if fix { if fix_file(path) { results.push(FileResult::Fixed); } else { results.push(FileResult::Issue(LintIssue::error( path, None, "No matching license strings found in tools/lint/license/valid-licenses.txt" .to_string(), linter, ))); } } else { results.push(FileResult::Issue(LintIssue::error( path, None, "No matching license strings found in tools/lint/license/valid-licenses.txt" .to_string(), linter, ))); } } results } fn check_license_html(path: &str, linter: &str, root: &str) { let issues: Vec = lint_license_html(path, root) .into_iter() .map(|(invalid_path, lineno)| { LintIssue::error( path, Some(lineno), format!("references unknown path {invalid_path}"), linter, ) }) .collect(); for issue in &issues { let json = serde_json::to_string(issue).expect("LintIssue should always serialize"); println!("{json}"); } } /// From a given file, check if we can find the license patterns /// in the first lines of the file. fn is_valid_license(path: &str, licenses: &[String]) -> bool { let Ok(content) = fs::read_to_string(path) else { eprintln!("Warning: could not read {path} for license check"); return true; }; // Empty files don't need a license. if content.is_empty() { return true; } let lower = content.to_lowercase(); licenses.iter().any(|l| lower.contains(l.as_str())) } const TEST_PATHS: &[&str] = &[ "/tests/", "/test/", "/test_", "/gtest", "/crashtest", "/mochitest", "/reftest", "/imptest", "/androidTest", "/jit-test/", "jsapi-tests/", ]; /// Is the file a test or not? fn is_test(path: &str) -> bool { // For the unit tests if path.contains("lint/test/") || path.contains("lint_license_test_tmp_file.js") { return false; } TEST_PATHS.iter().any(|p| path.contains(p)) } // Official source: https://www.mozilla.org/en-US/MPL/headers/ const MPL2_TEMPLATE: &[&str] = &[ "This Source Code Form is subject to the terms of the Mozilla Public", "License, v. 2.0. If a copy of the MPL was not distributed with this", "file, You can obtain one at https://mozilla.org/MPL/2.0/.", ]; const PUBLIC_DOMAIN_TEMPLATE: &[&str] = &[ "Any copyright is dedicated to the public domain.", "https://creativecommons.org/publicdomain/zero/1.0/", ]; /// Add the copyright notice to the top of the file. fn fix_file(path: &str) -> bool { let ext = Path::new(path) .extension() .and_then(|e| e.to_str()) .unwrap_or(""); let template = if is_test(path) { PUBLIC_DOMAIN_TEMPLATE } else { MPL2_TEMPLATE }; let header = match ext { "cpp" | "c" | "cc" | "h" | "m" | "mm" | "rs" | "java" | "kt" | "js" | "jsx" | "mjs" | "css" | "idl" | "webidl" => build_c_style_header(template), "py" | "ftl" | "properties" => build_hash_header(template), "xml" | "html" | "xhtml" | "dtd" | "svg" => build_xml_header(template, ext, is_test(path)), _ => return false, }; let Ok(content) = fs::read_to_string(path) else { return false; }; let lines: Vec<&str> = content.lines().collect(); let insert_at = if lines.is_empty() { 0 } else if lines[0].starts_with("#!") || lines[0].starts_with(" = header.lines().collect(); // Build the new content: prefix lines, header, empty separator line, body. let mut parts: Vec<&str> = Vec::new(); parts.extend_from_slice(&lines[..insert_at]); parts.extend(header_lines); parts.push(""); parts.extend_from_slice(&lines[insert_at..]); let mut new_content = parts.join("\n"); if content.ends_with('\n') || !content.contains('\n') { new_content.push('\n'); } fs::write(path, new_content).is_ok() } fn build_c_style_header(template: &[&str]) -> String { let mut result = String::new(); for (i, line) in template.iter().enumerate() { let start = if i == 0 { "/" } else { " " }; let end = if i == template.len() - 1 { " */" } else { "" }; let _ = writeln!(result, "{}* {}{}", start, line, end); } result } fn build_hash_header(template: &[&str]) -> String { let mut result = String::new(); for line in template { let _ = writeln!(result, "# {}", line); } result } fn build_xml_header(template: &[&str], ext: &str, is_test: bool) -> String { let mut result = String::new(); let last_idx = if is_test { 1 } else { 2 }; for (i, line) in template.iter().enumerate() { let start = if i == 0 { "" } else { "" }; let _ = write!(result, "{}{}{}", start, line, end); // When dealing with an svg, we should not have a space between // the license and the content if ext != "svg" || end.is_empty() { result.push('\n'); } } result } fn lint_license_html(path: &str, root: &str) -> Vec<(String, usize)> { let Ok(content) = fs::read_to_string(path) else { return Vec::new(); }; let dom = tl::parse(&content, tl::ParserOptions::default()).expect("HTML should parse"); let parser = dom.parser(); let mut results = Vec::new(); let Some(handles) = dom.query_selector("code") else { return results; }; for handle in handles { let Some(node) = handle.get(parser) else { continue; }; let code_text = node.inner_text(parser); let code_text = code_text.trim(); if code_text.is_empty() { continue; } let abs = Path::new(root).join(code_text); if !abs.exists() && glob::glob(abs.to_str().unwrap_or("")) .map(|mut g| g.next().is_none()) .unwrap_or(true) { let lineno = content .find(code_text) .map_or(0, |pos| content[..pos].lines().count()); results.push((code_text.to_string(), lineno)); } } results } #[cfg(test)] mod tests { use super::*; use std::io::Write; #[test] fn test_valid_license_mpl2() { let dir = tempfile::tempdir().unwrap(); let file_path = dir.path().join("test.rs"); std::fs::write( &file_path, "// file, You can obtain one at https://mozilla.org/MPL/2.0/.\nfn main() {}\n", ) .unwrap(); let licenses = vec!["mozilla.org/mpl/".to_string()]; assert!(is_valid_license(file_path.to_str().unwrap(), &licenses)); } #[test] fn test_missing_license() { let dir = tempfile::tempdir().unwrap(); let file_path = dir.path().join("test.rs"); std::fs::write(&file_path, "fn main() {}\n").unwrap(); let licenses = vec!["mozilla.org/mpl/".to_string()]; assert!(!is_valid_license(file_path.to_str().unwrap(), &licenses)); } #[test] fn test_empty_file_is_valid() { let dir = tempfile::tempdir().unwrap(); let file_path = dir.path().join("empty.py"); std::fs::write(&file_path, "").unwrap(); let licenses = vec!["mozilla.org/mpl/".to_string()]; assert!(is_valid_license(file_path.to_str().unwrap(), &licenses)); } #[test] fn test_is_test() { assert!(is_test("/repo/dom/base/tests/foo.js")); assert!(is_test("/repo/layout/crashtest/foo.html")); assert!(!is_test("/repo/dom/base/Document.cpp")); assert!(!is_test("/repo/tools/lint/test/test_license.py")); } #[test] fn test_fix_c_style() { let dir = tempfile::tempdir().unwrap(); let file_path = dir.path().join("test.js"); std::fs::write(&file_path, "var x = 1;\n").unwrap(); assert!(fix_file(file_path.to_str().unwrap())); let content = std::fs::read_to_string(&file_path).unwrap(); assert!(content.contains("Mozilla Public")); assert!(content.contains("var x = 1;")); } #[test] fn test_fix_python_style() { let dir = tempfile::tempdir().unwrap(); let file_path = dir.path().join("test.py"); std::fs::write(&file_path, "x = 1\n").unwrap(); assert!(fix_file(file_path.to_str().unwrap())); let content = std::fs::read_to_string(&file_path).unwrap(); assert!(content.starts_with("# This Source Code Form")); assert!(content.contains("x = 1")); } #[test] fn test_fix_preserves_shebang() { let dir = tempfile::tempdir().unwrap(); let file_path = dir.path().join("script.py"); std::fs::write(&file_path, "#!/usr/bin/env python3\nx = 1\n").unwrap(); assert!(fix_file(file_path.to_str().unwrap())); let content = std::fs::read_to_string(&file_path).unwrap(); assert!(content.starts_with("#!/usr/bin/env python3\n")); assert!(content.contains("# This Source Code Form")); } #[test] fn test_load_valid_licenses() { let dir = tempfile::tempdir().unwrap(); let file_path = dir.path().join("licenses.txt"); { let mut f = std::fs::File::create(&file_path).unwrap(); writeln!(f, "mozilla.org/MPL/").unwrap(); writeln!(f).unwrap(); writeln!(f, "Apache License").unwrap(); } let licenses = load_valid_licenses(file_path.to_str().unwrap()).unwrap(); assert_eq!(licenses.len(), 2); assert_eq!(licenses[0], "mozilla.org/mpl/"); assert_eq!(licenses[1], "apache license"); } }