use std::fmt;
use std::path::{Path, PathBuf};
pub const FORBIDDEN_EXTENSIONS: &[&str] = &[
".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx", ".coffee", ];
pub const FORBIDDEN_DIRECTORIES: &[&str] = &[
"node_modules",
"npm_packages",
".npm",
".yarn",
"bower_components",
"jspm_packages",
];
pub const FORBIDDEN_FILES: &[&str] = &[
"package.json",
"package-lock.json",
"yarn.lock",
"pnpm-lock.yaml",
"bun.lockb",
"tsconfig.json",
"jsconfig.json",
".babelrc",
"babel.config.js",
"webpack.config.js",
"rollup.config.js",
"vite.config.js",
"esbuild.config.js",
".eslintrc.js",
".prettierrc.js",
];
pub const DANGEROUS_JS_PATTERNS: &[&str] = &[
"eval(",
"new Function(",
"Function(",
"document.write(",
"innerHTML =",
"outerHTML =",
"__proto__",
"prototype.constructor",
"with (",
"setTimeout(\"",
"setInterval(\"",
];
#[derive(Debug, Clone, Default)]
pub struct ZeroJsValidationResult {
pub valid: bool,
pub unauthorized_js_files: Vec<PathBuf>,
pub unauthorized_css_files: Vec<PathBuf>,
pub unauthorized_html_files: Vec<PathBuf>,
pub forbidden_directories: Vec<PathBuf>,
pub forbidden_tooling_files: Vec<PathBuf>,
pub inline_scripts_detected: Vec<InlineScriptViolation>,
pub external_scripts_without_manifest: Vec<String>,
pub dangerous_patterns: Vec<DangerousPatternViolation>,
pub verified_js_files: Vec<PathBuf>,
}
impl ZeroJsValidationResult {
#[must_use]
pub fn is_valid(&self) -> bool {
self.valid
&& self.unauthorized_js_files.is_empty()
&& self.unauthorized_css_files.is_empty()
&& self.unauthorized_html_files.is_empty()
&& self.forbidden_directories.is_empty()
&& self.forbidden_tooling_files.is_empty()
&& self.inline_scripts_detected.is_empty()
&& self.external_scripts_without_manifest.is_empty()
&& self.dangerous_patterns.is_empty()
}
#[must_use]
pub fn violation_count(&self) -> usize {
self.unauthorized_js_files.len()
+ self.unauthorized_css_files.len()
+ self.unauthorized_html_files.len()
+ self.forbidden_directories.len()
+ self.forbidden_tooling_files.len()
+ self.inline_scripts_detected.len()
+ self.external_scripts_without_manifest.len()
+ self.dangerous_patterns.len()
}
}
impl fmt::Display for ZeroJsValidationResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_valid() {
writeln!(f, "Zero-JS Validation: PASSED")?;
writeln!(f, " Verified JS files: {}", self.verified_js_files.len())?;
} else {
writeln!(f, "Zero-JS Validation: FAILED")?;
writeln!(f, " Total violations: {}", self.violation_count())?;
if !self.unauthorized_js_files.is_empty() {
writeln!(f, "\n Unauthorized JavaScript files:")?;
for path in &self.unauthorized_js_files {
writeln!(f, " - {}", path.display())?;
}
}
if !self.forbidden_directories.is_empty() {
writeln!(f, "\n Forbidden directories:")?;
for path in &self.forbidden_directories {
writeln!(f, " - {}", path.display())?;
}
}
if !self.inline_scripts_detected.is_empty() {
writeln!(f, "\n Inline scripts detected:")?;
for violation in &self.inline_scripts_detected {
writeln!(f, " - {}", violation)?;
}
}
if !self.dangerous_patterns.is_empty() {
writeln!(f, "\n Dangerous patterns:")?;
for violation in &self.dangerous_patterns {
writeln!(f, " - {}", violation)?;
}
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct InlineScriptViolation {
pub file: PathBuf,
pub line: usize,
pub preview: String,
pub is_wasm_generated: bool,
}
impl fmt::Display for InlineScriptViolation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}:{} - {}",
self.file.display(),
self.line,
self.preview
)
}
}
#[derive(Debug, Clone)]
pub struct DangerousPatternViolation {
pub file: PathBuf,
pub line: usize,
pub pattern: String,
pub context: String,
}
impl fmt::Display for DangerousPatternViolation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}:{} - '{}' in: {}",
self.file.display(),
self.line,
self.pattern,
self.context
)
}
}
#[derive(Debug, Clone)]
pub struct ZeroJsConfig {
pub allowed_js_patterns: Vec<String>,
pub allowed_css_patterns: Vec<String>,
pub skip_paths: Vec<PathBuf>,
pub require_manifest: bool,
pub check_dangerous_patterns: bool,
pub wasm_marker: String,
pub allow_wasm_inline_scripts: bool,
}
impl Default for ZeroJsConfig {
fn default() -> Self {
Self {
allowed_js_patterns: vec![],
allowed_css_patterns: vec![],
skip_paths: vec![],
require_manifest: true,
check_dangerous_patterns: true,
wasm_marker: "__PROBAR_WASM_GENERATED__".to_string(),
allow_wasm_inline_scripts: true,
}
}
}
impl ZeroJsConfig {
#[must_use]
pub fn strict() -> Self {
Self {
allowed_js_patterns: vec![],
allowed_css_patterns: vec![],
skip_paths: vec![],
require_manifest: true,
check_dangerous_patterns: true,
wasm_marker: "__PROBAR_WASM_GENERATED__".to_string(),
allow_wasm_inline_scripts: false,
}
}
#[must_use]
pub fn development() -> Self {
Self {
allowed_js_patterns: vec!["*.worker.js".to_string(), "*.worklet.js".to_string()],
allowed_css_patterns: vec!["*.css".to_string()],
skip_paths: vec![],
require_manifest: false,
check_dangerous_patterns: false,
wasm_marker: String::new(),
allow_wasm_inline_scripts: true,
}
}
#[must_use]
pub fn with_allowed_js(mut self, pattern: impl Into<String>) -> Self {
self.allowed_js_patterns.push(pattern.into());
self
}
#[must_use]
pub fn with_skip_path(mut self, path: impl Into<PathBuf>) -> Self {
self.skip_paths.push(path.into());
self
}
}
#[derive(Debug, Clone)]
pub struct ZeroJsValidator {
config: ZeroJsConfig,
}
impl Default for ZeroJsValidator {
fn default() -> Self {
Self::new()
}
}
impl ZeroJsValidator {
#[must_use]
pub fn new() -> Self {
Self {
config: ZeroJsConfig::default(),
}
}
#[must_use]
pub fn with_config(config: ZeroJsConfig) -> Self {
Self { config }
}
pub fn validate_directory(&self, path: &Path) -> std::io::Result<ZeroJsValidationResult> {
let mut result = ZeroJsValidationResult {
valid: true,
..Default::default()
};
self.scan_directory(path, &mut result)?;
result.valid = result.is_valid();
Ok(result)
}
#[must_use]
pub fn validate_html_content(
&self,
content: &str,
file_path: &Path,
) -> Vec<InlineScriptViolation> {
let mut violations = Vec::new();
let mut in_script = false;
let mut script_start_line = 0;
let mut script_content = String::new();
for (line_num, line) in content.lines().enumerate() {
let line_num = line_num + 1;
if line.contains("<script") && !line.contains("src=") {
in_script = true;
script_start_line = line_num;
script_content.clear();
}
if in_script {
script_content.push_str(line);
script_content.push('\n');
}
if line.contains("</script>") && in_script {
in_script = false;
let is_wasm_generated = script_content.contains(&self.config.wasm_marker)
|| script_content.contains("wasm")
|| script_content.contains("WebAssembly");
if !is_wasm_generated || !self.config.allow_wasm_inline_scripts {
let preview = script_content
.chars()
.take(100)
.collect::<String>()
.replace('\n', " ");
violations.push(InlineScriptViolation {
file: file_path.to_path_buf(),
line: script_start_line,
preview,
is_wasm_generated,
});
}
}
}
violations
}
#[must_use]
pub fn validate_js_content(
&self,
content: &str,
file_path: &Path,
) -> Vec<DangerousPatternViolation> {
if !self.config.check_dangerous_patterns {
return Vec::new();
}
let mut violations = Vec::new();
for (line_num, line) in content.lines().enumerate() {
let line_num = line_num + 1;
for pattern in DANGEROUS_JS_PATTERNS {
if line.contains(pattern) {
let context = line.trim().chars().take(80).collect::<String>();
violations.push(DangerousPatternViolation {
file: file_path.to_path_buf(),
line: line_num,
pattern: (*pattern).to_string(),
context,
});
}
}
}
violations
}
#[must_use]
pub fn has_valid_manifest(&self, js_path: &Path) -> bool {
let manifest_path = js_path.with_extension("js.manifest.json");
if !manifest_path.exists() {
return false;
}
if let Ok(content) = std::fs::read_to_string(&manifest_path) {
content.contains("manifest_version")
&& content.contains("output_hash")
&& content.contains("generation")
} else {
false
}
}
fn matches_allowed_pattern(&self, path: &Path, patterns: &[String]) -> bool {
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
for pattern in patterns {
if let Some(suffix) = pattern.strip_prefix('*') {
if file_name.ends_with(suffix) {
return true;
}
} else if let Some(prefix) = pattern.strip_suffix('*') {
if file_name.starts_with(prefix) {
return true;
}
} else if file_name == pattern {
return true;
}
}
false
}
fn should_skip(&self, path: &Path) -> bool {
for skip in &self.config.skip_paths {
if path.starts_with(skip) {
return true;
}
}
false
}
fn scan_directory(
&self,
dir: &Path,
result: &mut ZeroJsValidationResult,
) -> std::io::Result<()> {
if !dir.is_dir() {
return Ok(());
}
if let Some(dir_name) = dir.file_name().and_then(|n| n.to_str()) {
if FORBIDDEN_DIRECTORIES.contains(&dir_name) {
result.forbidden_directories.push(dir.to_path_buf());
return Ok(());
}
}
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if self.should_skip(&path) {
continue;
}
if path.is_dir() {
self.scan_directory(&path, result)?;
} else if path.is_file() {
self.check_file(&path, result)?;
}
}
Ok(())
}
fn check_file(&self, path: &Path, result: &mut ZeroJsValidationResult) -> std::io::Result<()> {
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("");
if FORBIDDEN_FILES.contains(&file_name) {
result.forbidden_tooling_files.push(path.to_path_buf());
return Ok(());
}
let ext_with_dot = format!(".{extension}");
let is_forbidden_ext = FORBIDDEN_EXTENSIONS.contains(&ext_with_dot.as_str());
if is_forbidden_ext {
if self.matches_allowed_pattern(path, &self.config.allowed_js_patterns) {
if self.config.require_manifest && !self.has_valid_manifest(path) {
result.unauthorized_js_files.push(path.to_path_buf());
} else {
result.verified_js_files.push(path.to_path_buf());
if let Ok(content) = std::fs::read_to_string(path) {
let violations = self.validate_js_content(&content, path);
result.dangerous_patterns.extend(violations);
}
}
} else {
if self.has_valid_manifest(path) {
result.verified_js_files.push(path.to_path_buf());
} else {
result.unauthorized_js_files.push(path.to_path_buf());
}
}
} else if extension == "html" || extension == "htm" {
if let Ok(content) = std::fs::read_to_string(path) {
let violations = self.validate_html_content(&content, path);
result.inline_scripts_detected.extend(violations);
}
} else if extension == "css" {
if !self.matches_allowed_pattern(path, &self.config.allowed_css_patterns) {
result.unauthorized_css_files.push(path.to_path_buf());
}
}
Ok(())
}
#[cfg(feature = "browser")]
pub async fn validate_page_cdp(
&self,
page: &chromiumoxide::Page,
) -> Result<ZeroJsValidationResult, ZeroJsError> {
let mut result = ZeroJsValidationResult {
valid: true,
..Default::default()
};
let scripts_json: String = page
.evaluate(
r#"
JSON.stringify(Array.from(document.querySelectorAll('script')).map(s => ({
src: s.src || '',
content: s.textContent || '',
type: s.type || ''
})))
"#,
)
.await
.map_err(|e| ZeroJsError::CdpError(e.to_string()))?
.into_value()
.unwrap_or_else(|_| "[]".to_string());
let scripts: Vec<serde_json::Value> = serde_json::from_str(&scripts_json)
.map_err(|e| ZeroJsError::ParseError(e.to_string()))?;
for script in scripts {
let src = script["src"].as_str().unwrap_or("");
let content = script["content"].as_str().unwrap_or("");
if src.is_empty() {
let is_wasm_generated = content.contains(&self.config.wasm_marker)
|| content.contains("wasm")
|| content.contains("WebAssembly");
if !is_wasm_generated || !self.config.allow_wasm_inline_scripts {
result.inline_scripts_detected.push(InlineScriptViolation {
file: PathBuf::from("(page)"),
line: 0,
preview: content.chars().take(100).collect(),
is_wasm_generated,
});
}
let violations =
self.validate_js_content(content, &PathBuf::from("(inline script)"));
result.dangerous_patterns.extend(violations);
} else {
result
.external_scripts_without_manifest
.push(src.to_string());
}
}
result.valid = result.is_valid();
Ok(result)
}
}
#[derive(Debug, Clone)]
pub enum ZeroJsError {
CdpError(String),
ParseError(String),
IoError(String),
}
impl fmt::Display for ZeroJsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::CdpError(msg) => write!(f, "CDP error: {msg}"),
Self::ParseError(msg) => write!(f, "Parse error: {msg}"),
Self::IoError(msg) => write!(f, "IO error: {msg}"),
}
}
}
impl std::error::Error for ZeroJsError {}
impl From<std::io::Error> for ZeroJsError {
fn from(err: std::io::Error) -> Self {
Self::IoError(err.to_string())
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn h0_zj_01_detect_js_file() {
let temp = TempDir::new().unwrap();
let js_path = temp.path().join("user_code.js");
std::fs::write(&js_path, "console.log('hello')").unwrap();
let validator = ZeroJsValidator::new();
let result = validator.validate_directory(temp.path()).unwrap();
assert!(!result.is_valid());
assert!(!result.unauthorized_js_files.is_empty());
}
#[test]
fn h0_zj_02_detect_ts_file() {
let temp = TempDir::new().unwrap();
let ts_path = temp.path().join("code.ts");
std::fs::write(&ts_path, "const x: number = 1;").unwrap();
let validator = ZeroJsValidator::new();
let result = validator.validate_directory(temp.path()).unwrap();
assert!(!result.is_valid());
assert!(!result.unauthorized_js_files.is_empty());
}
#[test]
fn h0_zj_03_detect_mjs_file() {
let temp = TempDir::new().unwrap();
let mjs_path = temp.path().join("module.mjs");
std::fs::write(&mjs_path, "export const x = 1;").unwrap();
let validator = ZeroJsValidator::new();
let result = validator.validate_directory(temp.path()).unwrap();
assert!(!result.is_valid());
}
#[test]
fn h0_zj_04_detect_node_modules() {
let temp = TempDir::new().unwrap();
let nm_path = temp.path().join("node_modules");
std::fs::create_dir(&nm_path).unwrap();
std::fs::write(nm_path.join("package.json"), "{}").unwrap();
let validator = ZeroJsValidator::new();
let result = validator.validate_directory(temp.path()).unwrap();
assert!(!result.is_valid());
assert!(!result.forbidden_directories.is_empty());
}
#[test]
fn h0_zj_05_detect_npm_directory() {
let temp = TempDir::new().unwrap();
let npm_path = temp.path().join(".npm");
std::fs::create_dir(&npm_path).unwrap();
let validator = ZeroJsValidator::new();
let result = validator.validate_directory(temp.path()).unwrap();
assert!(!result.is_valid());
}
#[test]
fn h0_zj_06_detect_package_json() {
let temp = TempDir::new().unwrap();
let pkg_path = temp.path().join("package.json");
std::fs::write(&pkg_path, r#"{"name": "test"}"#).unwrap();
let validator = ZeroJsValidator::new();
let result = validator.validate_directory(temp.path()).unwrap();
assert!(!result.is_valid());
assert!(!result.forbidden_tooling_files.is_empty());
}
#[test]
fn h0_zj_07_detect_tsconfig() {
let temp = TempDir::new().unwrap();
let cfg_path = temp.path().join("tsconfig.json");
std::fs::write(&cfg_path, "{}").unwrap();
let validator = ZeroJsValidator::new();
let result = validator.validate_directory(temp.path()).unwrap();
assert!(!result.is_valid());
}
#[test]
fn h0_zj_08_detect_webpack_config() {
let temp = TempDir::new().unwrap();
let cfg_path = temp.path().join("webpack.config.js");
std::fs::write(&cfg_path, "module.exports = {}").unwrap();
let validator = ZeroJsValidator::new();
let result = validator.validate_directory(temp.path()).unwrap();
assert!(!result.is_valid());
assert!(
!result.forbidden_tooling_files.is_empty() || !result.unauthorized_js_files.is_empty()
);
}
#[test]
fn h0_zj_09_detect_inline_script() {
let html = r#"
<!DOCTYPE html>
<html>
<head>
<script>alert('hello')</script>
</head>
<body></body>
</html>
"#;
let validator = ZeroJsValidator::with_config(ZeroJsConfig {
allow_wasm_inline_scripts: false,
..Default::default()
});
let violations = validator.validate_html_content(html, Path::new("test.html"));
assert!(!violations.is_empty());
assert!(violations[0].preview.contains("alert"));
}
#[test]
fn h0_zj_10_allow_wasm_inline_script() {
let html = r#"
<!DOCTYPE html>
<html>
<head>
<script>
// __PROBAR_WASM_GENERATED__
WebAssembly.instantiate(module);
</script>
</head>
<body></body>
</html>
"#;
let validator = ZeroJsValidator::new();
let violations = validator.validate_html_content(html, Path::new("test.html"));
assert!(violations.is_empty());
}
#[test]
fn h0_zj_11_detect_eval() {
let js = "const result = eval('1 + 1');";
let validator = ZeroJsValidator::new();
let violations = validator.validate_js_content(js, Path::new("code.js"));
assert!(!violations.is_empty());
assert!(violations[0].pattern.contains("eval("));
}
#[test]
fn h0_zj_12_detect_function_constructor() {
let js = "const fn = new Function('return 1');";
let validator = ZeroJsValidator::new();
let violations = validator.validate_js_content(js, Path::new("code.js"));
assert!(!violations.is_empty());
assert!(violations[0].pattern.contains("Function("));
}
#[test]
fn h0_zj_13_detect_innerhtml() {
let js = "element.innerHTML = userInput;";
let validator = ZeroJsValidator::new();
let violations = validator.validate_js_content(js, Path::new("code.js"));
assert!(!violations.is_empty());
assert!(violations[0].pattern.contains("innerHTML"));
}
#[test]
fn h0_zj_14_detect_document_write() {
let js = "document.write('<script>alert(1)</script>');";
let validator = ZeroJsValidator::new();
let violations = validator.validate_js_content(js, Path::new("code.js"));
assert!(!violations.is_empty());
}
#[test]
fn h0_zj_15_detect_proto_access() {
let js = "obj.__proto__.polluted = true;";
let validator = ZeroJsValidator::new();
let violations = validator.validate_js_content(js, Path::new("code.js"));
assert!(!violations.is_empty());
}
#[test]
fn h0_zj_16_js_with_manifest_allowed() {
let temp = TempDir::new().unwrap();
let js_path = temp.path().join("worker.js");
let manifest_path = temp.path().join("worker.js.manifest.json");
std::fs::write(&js_path, "self.onmessage = function() {}").unwrap();
std::fs::write(
&manifest_path,
r#"{"manifest_version": 1, "output_hash": "abc123", "generation": {}}"#,
)
.unwrap();
let validator = ZeroJsValidator::new();
let result = validator.validate_directory(temp.path()).unwrap();
assert!(result.is_valid());
assert!(!result.verified_js_files.is_empty());
}
#[test]
fn h0_zj_17_js_without_manifest_rejected() {
let temp = TempDir::new().unwrap();
let js_path = temp.path().join("worker.js");
std::fs::write(&js_path, "self.onmessage = function() {}").unwrap();
let validator = ZeroJsValidator::new();
let result = validator.validate_directory(temp.path()).unwrap();
assert!(!result.is_valid());
assert!(!result.unauthorized_js_files.is_empty());
}
#[test]
fn h0_zj_18_strict_mode() {
let config = ZeroJsConfig::strict();
assert!(config.require_manifest);
assert!(config.check_dangerous_patterns);
assert!(!config.allow_wasm_inline_scripts);
}
#[test]
fn h0_zj_19_development_mode() {
let config = ZeroJsConfig::development();
assert!(!config.require_manifest);
assert!(!config.check_dangerous_patterns);
}
#[test]
fn h0_zj_20_allowed_pattern() {
let temp = TempDir::new().unwrap();
let js_path = temp.path().join("audio.worker.js");
std::fs::write(&js_path, "self.onmessage = function() {}").unwrap();
let config = ZeroJsConfig::default()
.with_allowed_js("*.worker.js")
.with_allowed_js("*.worklet.js");
let validator = ZeroJsValidator::with_config(ZeroJsConfig {
require_manifest: false,
..config
});
let result = validator.validate_directory(temp.path()).unwrap();
assert!(result.is_valid());
}
#[test]
fn h0_zj_21_skip_path() {
let temp = TempDir::new().unwrap();
let vendor_path = temp.path().join("vendor");
std::fs::create_dir(&vendor_path).unwrap();
std::fs::write(vendor_path.join("lib.js"), "var x = 1;").unwrap();
let config = ZeroJsConfig::default().with_skip_path(vendor_path);
let validator = ZeroJsValidator::with_config(config);
let result = validator.validate_directory(temp.path()).unwrap();
assert!(result.is_valid());
}
#[test]
fn h0_zj_22_clean_wasm_directory() {
let temp = TempDir::new().unwrap();
std::fs::write(temp.path().join("app.wasm"), b"fake wasm").unwrap();
std::fs::write(
temp.path().join("index.html"),
"<!DOCTYPE html><html><head></head><body></body></html>",
)
.unwrap();
let validator = ZeroJsValidator::new();
let result = validator.validate_directory(temp.path()).unwrap();
assert!(result.is_valid());
assert_eq!(result.violation_count(), 0);
}
#[test]
fn h0_zj_23_result_display_passed() {
let result = ZeroJsValidationResult {
valid: true,
verified_js_files: vec![PathBuf::from("worker.js")],
..Default::default()
};
let display = format!("{result}");
assert!(display.contains("PASSED"));
assert!(display.contains("Verified JS files: 1"));
}
#[test]
fn h0_zj_24_result_display_failed() {
let result = ZeroJsValidationResult {
valid: false,
unauthorized_js_files: vec![PathBuf::from("bad.js")],
..Default::default()
};
let display = format!("{result}");
assert!(display.contains("FAILED"));
assert!(display.contains("bad.js"));
}
#[test]
fn h0_zj_25_inline_script_violation_display() {
let violation = InlineScriptViolation {
file: PathBuf::from("index.html"),
line: 10,
preview: "alert('hello')".to_string(),
is_wasm_generated: false,
};
let display = format!("{violation}");
assert!(display.contains("index.html"));
assert!(display.contains("10"));
assert!(display.contains("alert"));
}
#[test]
fn h0_zj_26_dangerous_pattern_display() {
let violation = DangerousPatternViolation {
file: PathBuf::from("code.js"),
line: 5,
pattern: "eval(".to_string(),
context: "eval(userInput)".to_string(),
};
let display = format!("{violation}");
assert!(display.contains("code.js"));
assert!(display.contains("eval("));
}
#[test]
fn h0_zj_27_error_display() {
let cdp_err = ZeroJsError::CdpError("connection failed".to_string());
assert!(cdp_err.to_string().contains("CDP"));
let parse_err = ZeroJsError::ParseError("invalid json".to_string());
assert!(parse_err.to_string().contains("Parse"));
let io_err = ZeroJsError::IoError("file not found".to_string());
assert!(io_err.to_string().contains("IO"));
}
#[test]
fn h0_zj_28_error_from_io() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
let zero_js_err: ZeroJsError = io_err.into();
assert!(matches!(zero_js_err, ZeroJsError::IoError(_)));
}
#[test]
fn h0_zj_29_empty_directory() {
let temp = TempDir::new().unwrap();
let validator = ZeroJsValidator::new();
let result = validator.validate_directory(temp.path()).unwrap();
assert!(result.is_valid());
}
#[test]
fn h0_zj_30_non_existent_directory() {
let validator = ZeroJsValidator::new();
let result = validator.validate_directory(Path::new("/nonexistent/path/12345"));
assert!(result.is_ok() || result.is_err());
}
#[test]
fn h0_zj_31_nested_directories() {
let temp = TempDir::new().unwrap();
let nested = temp.path().join("a").join("b").join("c");
std::fs::create_dir_all(&nested).unwrap();
std::fs::write(nested.join("deep.js"), "var x = 1;").unwrap();
let validator = ZeroJsValidator::new();
let result = validator.validate_directory(temp.path()).unwrap();
assert!(!result.is_valid());
assert!(!result.unauthorized_js_files.is_empty());
}
#[test]
fn h0_zj_32_manifest_invalid_json() {
let temp = TempDir::new().unwrap();
let js_path = temp.path().join("code.js");
let manifest_path = temp.path().join("code.js.manifest.json");
std::fs::write(&js_path, "var x = 1;").unwrap();
std::fs::write(&manifest_path, "not valid json").unwrap();
let validator = ZeroJsValidator::new();
assert!(!validator.has_valid_manifest(&js_path));
}
#[test]
fn h0_zj_33_manifest_missing_fields() {
let temp = TempDir::new().unwrap();
let js_path = temp.path().join("code.js");
let manifest_path = temp.path().join("code.js.manifest.json");
std::fs::write(&js_path, "var x = 1;").unwrap();
std::fs::write(&manifest_path, r#"{"only": "partial"}"#).unwrap();
let validator = ZeroJsValidator::new();
assert!(!validator.has_valid_manifest(&js_path));
}
#[test]
fn test_default_validator() {
let validator = ZeroJsValidator::default();
assert!(validator.config.require_manifest);
}
#[test]
fn test_violation_count() {
let result = ZeroJsValidationResult {
unauthorized_js_files: vec![PathBuf::from("a.js"), PathBuf::from("b.js")],
forbidden_directories: vec![PathBuf::from("node_modules")],
inline_scripts_detected: vec![InlineScriptViolation {
file: PathBuf::from("x.html"),
line: 1,
preview: String::new(),
is_wasm_generated: false,
}],
..Default::default()
};
assert_eq!(result.violation_count(), 4);
}
#[test]
fn test_with_timeout_string_pattern() {
let js = r#"setTimeout("alert(1)", 1000);"#;
let validator = ZeroJsValidator::new();
let violations = validator.validate_js_content(js, Path::new("code.js"));
assert!(!violations.is_empty());
}
#[test]
fn test_setinterval_string_pattern() {
let js = r#"setInterval("tick()", 100);"#;
let validator = ZeroJsValidator::new();
let violations = validator.validate_js_content(js, Path::new("code.js"));
assert!(!violations.is_empty());
}
#[test]
fn test_prototype_constructor_pattern() {
let js = "obj.prototype.constructor = Evil;";
let validator = ZeroJsValidator::new();
let violations = validator.validate_js_content(js, Path::new("code.js"));
assert!(!violations.is_empty());
}
#[test]
fn test_outerhtml_pattern() {
let js = "el.outerHTML = userInput;";
let validator = ZeroJsValidator::new();
let violations = validator.validate_js_content(js, Path::new("code.js"));
assert!(!violations.is_empty());
}
#[test]
fn test_with_statement_pattern() {
let js = "with (obj) { x = 1; }";
let validator = ZeroJsValidator::new();
let violations = validator.validate_js_content(js, Path::new("code.js"));
assert!(!violations.is_empty());
}
#[test]
fn test_clean_js_no_dangerous() {
let js = "const x = 1; console.log(x);";
let validator = ZeroJsValidator::new();
let violations = validator.validate_js_content(js, Path::new("code.js"));
assert!(violations.is_empty());
}
#[test]
fn test_check_dangerous_patterns_disabled() {
let js = "eval('code');";
let config = ZeroJsConfig {
check_dangerous_patterns: false,
..Default::default()
};
let validator = ZeroJsValidator::with_config(config);
let violations = validator.validate_js_content(js, Path::new("code.js"));
assert!(violations.is_empty());
}
#[test]
fn test_jsx_file_detected() {
let temp = TempDir::new().unwrap();
std::fs::write(
temp.path().join("component.jsx"),
"const App = () => <div/>",
)
.unwrap();
let validator = ZeroJsValidator::new();
let result = validator.validate_directory(temp.path()).unwrap();
assert!(!result.is_valid());
}
#[test]
fn test_tsx_file_detected() {
let temp = TempDir::new().unwrap();
std::fs::write(
temp.path().join("component.tsx"),
"const App: React.FC = () => <div/>",
)
.unwrap();
let validator = ZeroJsValidator::new();
let result = validator.validate_directory(temp.path()).unwrap();
assert!(!result.is_valid());
}
#[test]
fn test_coffee_file_detected() {
let temp = TempDir::new().unwrap();
std::fs::write(temp.path().join("app.coffee"), "x = 1").unwrap();
let validator = ZeroJsValidator::new();
let result = validator.validate_directory(temp.path()).unwrap();
assert!(!result.is_valid());
}
#[test]
fn test_cjs_file_detected() {
let temp = TempDir::new().unwrap();
std::fs::write(temp.path().join("module.cjs"), "module.exports = {}").unwrap();
let validator = ZeroJsValidator::new();
let result = validator.validate_directory(temp.path()).unwrap();
assert!(!result.is_valid());
}
#[test]
fn test_yarn_directory_detected() {
let temp = TempDir::new().unwrap();
std::fs::create_dir(temp.path().join(".yarn")).unwrap();
let validator = ZeroJsValidator::new();
let result = validator.validate_directory(temp.path()).unwrap();
assert!(!result.is_valid());
}
#[test]
fn test_pattern_matching_prefix() {
let validator = ZeroJsValidator::new();
assert!(
validator.matches_allowed_pattern(Path::new("bundle.abc"), &["bundle.*".to_string()])
);
}
#[test]
fn test_pattern_matching_exact() {
let validator = ZeroJsValidator::new();
assert!(validator.matches_allowed_pattern(Path::new("exact.js"), &["exact.js".to_string()]));
}
#[test]
fn test_pattern_matching_no_match() {
let validator = ZeroJsValidator::new();
assert!(
!validator.matches_allowed_pattern(Path::new("other.js"), &["exact.js".to_string()])
);
}
#[test]
fn test_result_display_with_all_violations() {
let result = ZeroJsValidationResult {
valid: false,
unauthorized_js_files: vec![PathBuf::from("bad.js")],
unauthorized_css_files: vec![],
unauthorized_html_files: vec![],
forbidden_directories: vec![PathBuf::from("node_modules")],
forbidden_tooling_files: vec![],
inline_scripts_detected: vec![InlineScriptViolation {
file: PathBuf::from("index.html"),
line: 5,
preview: "alert('hi')".into(),
is_wasm_generated: false,
}],
external_scripts_without_manifest: vec![],
dangerous_patterns: vec![DangerousPatternViolation {
file: PathBuf::from("code.js"),
line: 10,
pattern: "eval(".into(),
context: "eval(input)".into(),
}],
verified_js_files: vec![],
};
let display = format!("{result}");
assert!(display.contains("FAILED"));
assert!(display.contains("bad.js"));
assert!(display.contains("node_modules"));
assert!(display.contains("alert"));
assert!(display.contains("eval"));
}
#[test]
fn test_config_with_allowed_css() {
let temp = TempDir::new().unwrap();
std::fs::write(temp.path().join("styles.css"), "body { color: red; }").unwrap();
let config = ZeroJsConfig {
allowed_css_patterns: vec!["*.css".to_string()],
..Default::default()
};
let validator = ZeroJsValidator::with_config(config);
let result = validator.validate_directory(temp.path()).unwrap();
assert!(result.unauthorized_css_files.is_empty());
}
#[test]
fn test_css_file_not_allowed() {
let temp = TempDir::new().unwrap();
std::fs::write(temp.path().join("styles.css"), "body { color: red; }").unwrap();
let validator = ZeroJsValidator::new();
let result = validator.validate_directory(temp.path()).unwrap();
assert!(!result.unauthorized_css_files.is_empty());
}
#[test]
fn test_html_with_external_script() {
let html = r#"
<!DOCTYPE html>
<html>
<head>
<script src="external.js"></script>
</head>
<body></body>
</html>
"#;
let validator = ZeroJsValidator::new();
let violations = validator.validate_html_content(html, Path::new("test.html"));
assert!(violations.is_empty());
}
#[test]
fn test_html_with_wasm_marker() {
let html = r#"
<!DOCTYPE html>
<html>
<head>
<script>
// __PROBAR_WASM_GENERATED__
init();
</script>
</head>
<body></body>
</html>
"#;
let validator = ZeroJsValidator::new();
let violations = validator.validate_html_content(html, Path::new("test.html"));
assert!(violations.is_empty());
}
#[test]
fn test_inline_script_violation_is_wasm_generated() {
let violation = InlineScriptViolation {
file: PathBuf::from("test.html"),
line: 5,
preview: "WebAssembly.instantiate".into(),
is_wasm_generated: true,
};
assert!(violation.is_wasm_generated);
}
#[test]
fn test_forbidden_directories_all() {
for dir_name in FORBIDDEN_DIRECTORIES {
let temp = TempDir::new().unwrap();
let forbidden = temp.path().join(dir_name);
std::fs::create_dir(&forbidden).unwrap();
let validator = ZeroJsValidator::new();
let result = validator.validate_directory(temp.path()).unwrap();
assert!(
!result.is_valid(),
"Directory '{}' should be forbidden",
dir_name
);
}
}
#[test]
fn test_forbidden_tooling_files_all() {
let tooling_files = &[
"package-lock.json",
"yarn.lock",
"pnpm-lock.yaml",
"bun.lockb",
"jsconfig.json",
".babelrc",
"rollup.config.js",
"vite.config.js",
"esbuild.config.js",
".eslintrc.js",
".prettierrc.js",
];
for file_name in tooling_files {
let temp = TempDir::new().unwrap();
std::fs::write(temp.path().join(file_name), "{}").unwrap();
let validator = ZeroJsValidator::new();
let result = validator.validate_directory(temp.path()).unwrap();
assert!(
!result.is_valid(),
"File '{}' should be forbidden",
file_name
);
}
}
#[test]
fn test_dangerous_patterns_all() {
let code_samples = &[
"eval('code');",
"new Function('return 1');",
"Function('code')();",
"document.write('html');",
"el.innerHTML = x;",
"el.outerHTML = y;",
"obj.__proto__.bad = true;",
"Foo.prototype.constructor = Bar;",
"with (obj) {}",
r#"setTimeout("code", 1);"#,
r#"setInterval("tick", 1);"#,
];
let validator = ZeroJsValidator::new();
for code in code_samples {
let violations = validator.validate_js_content(code, Path::new("test.js"));
assert!(
!violations.is_empty(),
"Pattern should be detected in: {}",
code
);
}
}
#[test]
fn test_zero_js_error_std_error() {
let err = ZeroJsError::CdpError("test".into());
let _: &dyn std::error::Error = &err;
}
#[test]
fn test_validation_result_is_valid_comprehensive() {
let mut result = ZeroJsValidationResult::default();
result.valid = true;
assert!(result.is_valid());
result.unauthorized_js_files.push(PathBuf::from("a.js"));
assert!(!result.is_valid());
result.unauthorized_js_files.clear();
result.unauthorized_css_files.push(PathBuf::from("a.css"));
assert!(!result.is_valid());
result.unauthorized_css_files.clear();
result.unauthorized_html_files.push(PathBuf::from("a.html"));
assert!(!result.is_valid());
result.unauthorized_html_files.clear();
result
.forbidden_directories
.push(PathBuf::from("node_modules"));
assert!(!result.is_valid());
result.forbidden_directories.clear();
result
.forbidden_tooling_files
.push(PathBuf::from("package.json"));
assert!(!result.is_valid());
result.forbidden_tooling_files.clear();
result.inline_scripts_detected.push(InlineScriptViolation {
file: PathBuf::from("x.html"),
line: 1,
preview: String::new(),
is_wasm_generated: false,
});
assert!(!result.is_valid());
result.inline_scripts_detected.clear();
result
.external_scripts_without_manifest
.push("ext.js".into());
assert!(!result.is_valid());
result.external_scripts_without_manifest.clear();
result.dangerous_patterns.push(DangerousPatternViolation {
file: PathBuf::from("code.js"),
line: 1,
pattern: "eval(".into(),
context: String::new(),
});
assert!(!result.is_valid());
}
#[test]
fn test_file_without_extension() {
let temp = TempDir::new().unwrap();
std::fs::write(temp.path().join("Makefile"), "all:\n\techo hello").unwrap();
let validator = ZeroJsValidator::new();
let result = validator.validate_directory(temp.path()).unwrap();
assert!(result.unauthorized_js_files.is_empty());
}
#[test]
fn test_file_with_unknown_extension() {
let temp = TempDir::new().unwrap();
std::fs::write(temp.path().join("data.xyz"), "some data").unwrap();
let validator = ZeroJsValidator::new();
let result = validator.validate_directory(temp.path()).unwrap();
assert!(result.is_valid());
}
#[test]
fn test_htm_extension() {
let temp = TempDir::new().unwrap();
let html = r#"
<!DOCTYPE html>
<html>
<head>
<script>alert('test')</script>
</head>
<body></body>
</html>
"#;
std::fs::write(temp.path().join("page.htm"), html).unwrap();
let validator = ZeroJsValidator::with_config(ZeroJsConfig {
allow_wasm_inline_scripts: false,
..Default::default()
});
let result = validator.validate_directory(temp.path()).unwrap();
assert!(!result.inline_scripts_detected.is_empty());
}
#[test]
fn test_allowed_js_with_manifest_requirement() {
let temp = TempDir::new().unwrap();
let js_path = temp.path().join("app.worker.js");
let manifest_path = temp.path().join("app.worker.js.manifest.json");
std::fs::write(&js_path, "self.onmessage = function() {}").unwrap();
std::fs::write(
&manifest_path,
r#"{"manifest_version": 1, "output_hash": "abc", "generation": {}}"#,
)
.unwrap();
let config = ZeroJsConfig::default().with_allowed_js("*.worker.js");
let validator = ZeroJsValidator::with_config(config);
let result = validator.validate_directory(temp.path()).unwrap();
assert!(result.is_valid());
assert!(!result.verified_js_files.is_empty());
}
#[test]
fn test_allowed_js_without_manifest_fails() {
let temp = TempDir::new().unwrap();
let js_path = temp.path().join("app.worker.js");
std::fs::write(&js_path, "self.onmessage = function() {}").unwrap();
let config = ZeroJsConfig::default().with_allowed_js("*.worker.js");
let validator = ZeroJsValidator::with_config(config);
let result = validator.validate_directory(temp.path()).unwrap();
assert!(!result.is_valid());
}
#[test]
fn test_verified_js_with_dangerous_patterns() {
let temp = TempDir::new().unwrap();
let js_path = temp.path().join("worker.js");
let manifest_path = temp.path().join("worker.js.manifest.json");
std::fs::write(&js_path, "const x = eval('1')").unwrap();
std::fs::write(
&manifest_path,
r#"{"manifest_version": 1, "output_hash": "abc", "generation": {}}"#,
)
.unwrap();
let config = ZeroJsConfig::default().with_allowed_js("*.js");
let validator = ZeroJsValidator::with_config(ZeroJsConfig {
require_manifest: false,
..config
});
let result = validator.validate_directory(temp.path()).unwrap();
assert!(!result.verified_js_files.is_empty());
assert!(!result.dangerous_patterns.is_empty());
}
#[test]
fn test_skip_path_nested() {
let temp = TempDir::new().unwrap();
let vendor = temp.path().join("vendor").join("external");
std::fs::create_dir_all(&vendor).unwrap();
std::fs::write(vendor.join("lib.js"), "var x = 1;").unwrap();
let config = ZeroJsConfig::default().with_skip_path(temp.path().join("vendor"));
let validator = ZeroJsValidator::with_config(config);
let result = validator.validate_directory(temp.path()).unwrap();
assert!(result.is_valid());
}
#[test]
fn test_multiple_inline_scripts() {
let html = r#"
<!DOCTYPE html>
<html>
<head>
<script>first();</script>
<script>second();</script>
<script>third();</script>
</head>
<body></body>
</html>
"#;
let validator = ZeroJsValidator::with_config(ZeroJsConfig {
allow_wasm_inline_scripts: false,
..Default::default()
});
let violations = validator.validate_html_content(html, Path::new("test.html"));
assert_eq!(violations.len(), 3);
}
#[test]
fn test_html_script_not_closed() {
let html = r#"
<!DOCTYPE html>
<html>
<head>
<script>unclosed
"#;
let validator = ZeroJsValidator::with_config(ZeroJsConfig {
allow_wasm_inline_scripts: false,
..Default::default()
});
let violations = validator.validate_html_content(html, Path::new("test.html"));
assert!(violations.is_empty());
}
}