use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Manifest {
#[serde(default)]
pub name: String,
#[serde(default)]
pub manifest_version: u32,
#[serde(default)]
pub version: String,
#[serde(default)]
pub permissions: Vec<String>,
#[serde(default)]
pub optional_permissions: Vec<String>,
#[serde(default)]
pub host_permissions: Vec<String>,
#[serde(default)]
pub content_scripts: Vec<ContentScript>,
#[serde(default)]
pub background: Option<Background>,
#[serde(default)]
pub content_security_policy: Option<CspConfig>,
#[serde(default)]
pub externally_connectable: Option<ExternallyConnectable>,
#[serde(default)]
pub web_accessible_resources: Vec<serde_json::Value>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ContentScript {
#[serde(default)]
pub matches: Vec<String>,
#[serde(default)]
pub js: Vec<String>,
#[serde(default)]
pub css: Vec<String>,
#[serde(default)]
pub run_at: Option<String>,
#[serde(default)]
pub all_frames: bool,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct Background {
#[serde(default)]
pub service_worker: Option<String>,
#[serde(default)]
pub scripts: Vec<String>,
#[serde(default)]
pub page: Option<String>,
#[serde(default)]
pub persistent: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum CspConfig {
String(String),
Object {
#[serde(default)]
extension_pages: Option<String>,
#[serde(default)]
sandbox: Option<String>,
},
}
impl Default for CspConfig {
fn default() -> Self {
CspConfig::String(String::new())
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ExternallyConnectable {
#[serde(default)]
pub matches: Vec<String>,
#[serde(default)]
pub ids: Vec<String>,
#[serde(default)]
pub accepts_tls_channel_id: bool,
}
impl Manifest {
pub fn parse(json: &str) -> Result<Self, String> {
serde_json::from_str(json).map_err(|e| format!("manifest parse error: {e}"))
}
pub fn injects_all_urls(&self) -> bool {
self.content_scripts.iter().any(|cs| {
cs.matches
.iter()
.any(|m| m == "<all_urls>" || m == "*://*/*")
})
}
pub fn has_dangerous_permissions(&self) -> bool {
let dangerous = [
"tabs",
"webRequest",
"webRequestBlocking",
"cookies",
"history",
"bookmarks",
"downloads",
"management",
"nativeMessaging",
"debugger",
"proxy",
"<all_urls>",
"*://*/*",
"http://*/*",
"https://*/*",
];
self.permissions
.iter()
.any(|p| dangerous.contains(&p.as_str()))
|| self
.host_permissions
.iter()
.any(|p| dangerous.contains(&p.as_str()))
}
pub fn extension_csp(&self) -> Option<&str> {
match &self.content_security_policy {
Some(CspConfig::String(s)) => Some(s.as_str()),
Some(CspConfig::Object {
extension_pages, ..
}) => extension_pages.as_deref(),
None => None,
}
}
pub fn allows_eval(&self) -> bool {
match self.extension_csp() {
None => true, Some(csp) => csp.contains("'unsafe-eval'"),
}
}
pub fn all_scripts(&self) -> Vec<String> {
let mut scripts = Vec::new();
if let Some(bg) = &self.background {
if let Some(sw) = &bg.service_worker {
scripts.push(sw.clone());
}
scripts.extend(bg.scripts.clone());
}
for cs in &self.content_scripts {
scripts.extend(cs.js.clone());
}
scripts
}
pub fn is_externally_connectable(&self) -> bool {
self.externally_connectable
.as_ref()
.is_some_and(|ec| !ec.matches.is_empty() || !ec.ids.is_empty())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_mv3_manifest() {
let json = r#"{
"name": "Test Extension",
"manifest_version": 3,
"version": "1.0",
"permissions": ["tabs", "storage"],
"host_permissions": ["https://example.com/*"],
"background": {
"service_worker": "background.js"
},
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_idle"
}]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert_eq!(manifest.name, "Test Extension");
assert_eq!(manifest.manifest_version, 3);
assert!(manifest.injects_all_urls());
assert!(manifest.has_dangerous_permissions()); assert_eq!(manifest.all_scripts(), vec!["background.js", "content.js"]);
}
#[test]
fn parse_mv2_manifest() {
let json = r#"{
"name": "Legacy",
"manifest_version": 2,
"version": "0.1",
"permissions": ["storage"],
"background": {
"scripts": ["bg1.js", "bg2.js"],
"persistent": false
},
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'none'"
}"#;
let manifest = Manifest::parse(json).unwrap();
assert_eq!(manifest.manifest_version, 2);
assert!(manifest.allows_eval()); assert!(!manifest.has_dangerous_permissions()); }
#[test]
fn csp_blocks_eval() {
let json = r#"{
"name": "Strict",
"manifest_version": 3,
"version": "1.0",
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'none'"
}
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(!manifest.allows_eval());
}
#[test]
fn externally_connectable() {
let json = r#"{
"name": "Connectable",
"manifest_version": 3,
"version": "1.0",
"externally_connectable": {
"matches": ["https://trusted.example.com/*"]
}
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.is_externally_connectable());
}
#[test]
fn not_externally_connectable() {
let json = r#"{
"name": "Private",
"manifest_version": 3,
"version": "1.0"
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(!manifest.is_externally_connectable());
}
#[test]
fn empty_manifest() {
let manifest = Manifest::parse("{}").unwrap();
assert_eq!(manifest.name, "");
assert!(!manifest.injects_all_urls());
assert!(!manifest.has_dangerous_permissions());
assert!(manifest.all_scripts().is_empty());
}
#[test]
fn dangerous_host_permissions() {
let json = r#"{
"name": "HostPerms",
"manifest_version": 3,
"version": "1.0",
"host_permissions": ["<all_urls>"]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.has_dangerous_permissions());
}
#[test]
fn parse_invalid_json() {
let json = r#"{ invalid json }"#;
let result = Manifest::parse(json);
assert!(result.is_err());
assert!(result.unwrap_err().contains("parse error"));
}
#[test]
fn parse_json_with_trailing_comma() {
let json = r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0",
}"#;
let result = Manifest::parse(json);
assert!(result.is_err());
}
#[test]
fn parse_json_with_missing_closing_brace() {
let json = r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0"
"#;
let result = Manifest::parse(json);
assert!(result.is_err());
}
#[test]
fn parse_json_with_wrong_type_for_manifest_version() {
let json = r#"{
"name": "Test",
"manifest_version": "3",
"version": "1.0"
}"#;
let result = Manifest::parse(json);
assert!(result.is_err());
}
#[test]
fn parse_json_with_extra_fields() {
let json = r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0",
"unknown_field": "ignored",
"nested_extra": {"key": "value"}
}"#;
let manifest = Manifest::parse(json).unwrap();
assert_eq!(manifest.name, "Test");
assert_eq!(manifest.manifest_version, 3);
}
#[test]
fn parse_empty_string() {
let result = Manifest::parse("");
assert!(result.is_err());
}
#[test]
fn parse_whitespace_only() {
let result = Manifest::parse(" \n\t ");
assert!(result.is_err());
}
#[test]
fn parse_null_values() {
let json = r#"{
"name": null,
"manifest_version": 3,
"version": null
}"#;
let result = Manifest::parse(json);
if let Ok(manifest) = result {
assert_eq!(manifest.name, ""); }
}
#[test]
fn parse_permissions_as_object_instead_of_array() {
let json = r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0",
"permissions": {"tabs": true}
}"#;
let result = Manifest::parse(json);
assert!(result.is_err());
}
#[test]
fn mv3_service_worker_only() {
let json = r#"{
"name": "MV3",
"manifest_version": 3,
"version": "1.0",
"background": {
"service_worker": "sw.js"
}
}"#;
let manifest = Manifest::parse(json).unwrap();
assert_eq!(manifest.manifest_version, 3);
assert!(
manifest
.background
.as_ref()
.unwrap()
.service_worker
.is_some()
);
assert!(manifest.background.as_ref().unwrap().scripts.is_empty());
}
#[test]
fn mv2_background_scripts() {
let json = r#"{
"name": "MV2",
"manifest_version": 2,
"version": "1.0",
"background": {
"scripts": ["bg1.js", "bg2.js"],
"persistent": true
}
}"#;
let manifest = Manifest::parse(json).unwrap();
assert_eq!(manifest.manifest_version, 2);
assert_eq!(manifest.background.as_ref().unwrap().scripts.len(), 2);
assert!(
manifest
.background
.as_ref()
.unwrap()
.service_worker
.is_none()
);
assert_eq!(manifest.background.as_ref().unwrap().persistent, Some(true));
}
#[test]
fn mv2_background_page() {
let json = r#"{
"name": "MV2 Page",
"manifest_version": 2,
"version": "1.0",
"background": {
"page": "background.html"
}
}"#;
let manifest = Manifest::parse(json).unwrap();
assert_eq!(
manifest.background.as_ref().unwrap().page.as_ref().unwrap(),
"background.html"
);
}
#[test]
fn mv2_persistent_default_none() {
let json = r#"{
"name": "MV2",
"manifest_version": 2,
"version": "1.0",
"background": {
"scripts": ["bg.js"]
}
}"#;
let manifest = Manifest::parse(json).unwrap();
assert_eq!(manifest.background.as_ref().unwrap().persistent, None);
}
#[test]
fn csp_mv2_string_format() {
let json = r#"{
"name": "MV2 CSP",
"manifest_version": 2,
"version": "1.0",
"content_security_policy": "script-src 'self'; object-src 'none'"
}"#;
let manifest = Manifest::parse(json).unwrap();
match manifest.content_security_policy {
Some(CspConfig::String(s)) => assert_eq!(s, "script-src 'self'; object-src 'none'"),
_ => panic!("Expected String CSP"),
}
}
#[test]
fn csp_mv3_object_format() {
let json = r#"{
"name": "MV3 CSP",
"manifest_version": 3,
"version": "1.0",
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'none'",
"sandbox": "sandbox-src 'self'"
}
}"#;
let manifest = Manifest::parse(json).unwrap();
match manifest.content_security_policy {
Some(CspConfig::Object {
extension_pages,
sandbox,
}) => {
assert_eq!(
extension_pages.as_ref().unwrap(),
"script-src 'self'; object-src 'none'"
);
assert_eq!(sandbox.as_ref().unwrap(), "sandbox-src 'self'");
}
_ => panic!("Expected Object CSP"),
}
}
#[test]
fn csp_no_csp() {
let json = r#"{
"name": "No CSP",
"manifest_version": 3,
"version": "1.0"
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.content_security_policy.is_none());
assert!(manifest.allows_eval());
}
#[test]
fn csp_empty_string() {
let json = r#"{
"name": "Empty CSP",
"manifest_version": 2,
"version": "1.0",
"content_security_policy": ""
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(!manifest.allows_eval());
}
#[test]
fn csp_multiple_directives_with_unsafe_eval() {
let json = r#"{
"name": "Complex CSP",
"manifest_version": 2,
"version": "1.0",
"content_security_policy": "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self'"
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.allows_eval());
}
#[test]
fn csp_with_wasm_eval_but_not_unsafe_eval() {
let json = r#"{
"name": "WASM Eval",
"manifest_version": 3,
"version": "1.0",
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-eval'"
}
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(!manifest.allows_eval());
}
#[test]
fn csp_multiple_script_src() {
let json = r#"{
"name": "Multi Script",
"manifest_version": 3,
"version": "1.0",
"content_security_policy": {
"extension_pages": "script-src 'self'; script-src 'unsafe-eval'"
}
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.allows_eval());
}
#[test]
fn csp_case_sensitivity() {
let json = r#"{
"name": "Case Test",
"manifest_version": 2,
"version": "1.0",
"content_security_policy": "script-src 'SELF' 'UNSAFE-EVAL'"
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(!manifest.allows_eval());
}
#[test]
fn csp_object_with_only_sandbox() {
let json = r#"{
"name": "Sandbox Only",
"manifest_version": 3,
"version": "1.0",
"content_security_policy": {
"sandbox": "sandbox-src 'self'"
}
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.allows_eval());
}
#[test]
fn permission_tabs_alone_is_dangerous() {
let json = r#"{
"name": "Tabs",
"manifest_version": 3,
"version": "1.0",
"permissions": ["tabs"]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.has_dangerous_permissions());
}
#[test]
fn permission_webrequest_is_dangerous() {
let json = r#"{
"name": "WebReq",
"manifest_version": 3,
"version": "1.0",
"permissions": ["webRequest"]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.has_dangerous_permissions());
}
#[test]
fn permission_webrequest_blocking_is_dangerous() {
let json = r#"{
"name": "WebReqBlock",
"manifest_version": 3,
"version": "1.0",
"permissions": ["webRequestBlocking"]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.has_dangerous_permissions());
}
#[test]
fn permission_cookies_is_dangerous() {
let json = r#"{
"name": "Cookies",
"manifest_version": 3,
"version": "1.0",
"permissions": ["cookies"]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.has_dangerous_permissions());
}
#[test]
fn permission_history_is_dangerous() {
let json = r#"{
"name": "History",
"manifest_version": 3,
"version": "1.0",
"permissions": ["history"]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.has_dangerous_permissions());
}
#[test]
fn permission_bookmarks_is_dangerous() {
let json = r#"{
"name": "Bookmarks",
"manifest_version": 3,
"version": "1.0",
"permissions": ["bookmarks"]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.has_dangerous_permissions());
}
#[test]
fn permission_downloads_is_dangerous() {
let json = r#"{
"name": "Downloads",
"manifest_version": 3,
"version": "1.0",
"permissions": ["downloads"]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.has_dangerous_permissions());
}
#[test]
fn permission_management_is_dangerous() {
let json = r#"{
"name": "Management",
"manifest_version": 3,
"version": "1.0",
"permissions": ["management"]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.has_dangerous_permissions());
}
#[test]
fn permission_native_messaging_is_dangerous() {
let json = r#"{
"name": "Native",
"manifest_version": 3,
"version": "1.0",
"permissions": ["nativeMessaging"]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.has_dangerous_permissions());
}
#[test]
fn permission_debugger_is_dangerous() {
let json = r#"{
"name": "Debugger",
"manifest_version": 3,
"version": "1.0",
"permissions": ["debugger"]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.has_dangerous_permissions());
}
#[test]
fn permission_proxy_is_dangerous() {
let json = r#"{
"name": "Proxy",
"manifest_version": 3,
"version": "1.0",
"permissions": ["proxy"]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.has_dangerous_permissions());
}
#[test]
fn permission_storage_is_safe() {
let json = r#"{
"name": "Storage",
"manifest_version": 3,
"version": "1.0",
"permissions": ["storage"]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(!manifest.has_dangerous_permissions());
}
#[test]
fn permission_active_tab_is_safe() {
let json = r#"{
"name": "ActiveTab",
"manifest_version": 3,
"version": "1.0",
"permissions": ["activeTab"]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(!manifest.has_dangerous_permissions());
}
#[test]
fn multiple_permissions_mixed_dangerous() {
let json = r#"{
"name": "Mixed",
"manifest_version": 3,
"version": "1.0",
"permissions": ["storage", "activeTab", "tabs", "alarms"]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.has_dangerous_permissions());
}
#[test]
fn optional_permissions_checked() {
let json = r#"{
"name": "Optional",
"manifest_version": 3,
"version": "1.0",
"optional_permissions": ["tabs", "downloads"]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(!manifest.has_dangerous_permissions());
}
#[test]
fn host_permission_all_urls_is_dangerous() {
let json = r#"{
"name": "AllURLs",
"manifest_version": 3,
"version": "1.0",
"host_permissions": ["*://*/*"]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.has_dangerous_permissions());
}
#[test]
fn host_permission_http_wildcard_is_dangerous() {
let json = r#"{
"name": "HttpWildcard",
"manifest_version": 3,
"version": "1.0",
"host_permissions": ["http://*/*"]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.has_dangerous_permissions());
}
#[test]
fn host_permission_https_wildcard_is_dangerous() {
let json = r#"{
"name": "HttpsWildcard",
"manifest_version": 3,
"version": "1.0",
"host_permissions": ["https://*/*"]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.has_dangerous_permissions());
}
#[test]
fn host_permission_specific_is_safe() {
let json = r#"{
"name": "Specific",
"manifest_version": 3,
"version": "1.0",
"host_permissions": ["https://example.com/*"]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(!manifest.has_dangerous_permissions());
}
#[test]
fn content_script_single() {
let json = r#"{
"name": "Single CS",
"manifest_version": 3,
"version": "1.0",
"content_scripts": [{
"matches": ["https://*.example.com/*"],
"js": ["content.js"],
"css": ["styles.css"],
"run_at": "document_start",
"all_frames": true
}]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert_eq!(manifest.content_scripts.len(), 1);
let cs = &manifest.content_scripts[0];
assert_eq!(cs.matches[0], "https://*.example.com/*");
assert_eq!(cs.js[0], "content.js");
assert_eq!(cs.css[0], "styles.css");
assert_eq!(cs.run_at.as_ref().unwrap(), "document_start");
assert!(cs.all_frames);
}
#[test]
fn content_script_multiple() {
let json = r#"{
"name": "Multi CS",
"manifest_version": 3,
"version": "1.0",
"content_scripts": [
{
"matches": ["https://a.com/*"],
"js": ["a.js"]
},
{
"matches": ["https://b.com/*"],
"js": ["b.js"]
},
{
"matches": ["https://c.com/*"],
"js": ["c.js"]
}
]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert_eq!(manifest.content_scripts.len(), 3);
}
#[test]
fn content_script_overlapping_matches() {
let json = r#"{
"name": "Overlap",
"manifest_version": 3,
"version": "1.0",
"content_scripts": [
{
"matches": ["<all_urls>", "https://*/*"],
"js": ["all.js"]
}
]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.injects_all_urls());
}
#[test]
fn content_script_no_matches() {
let json = r#"{
"name": "No Matches",
"manifest_version": 3,
"version": "1.0",
"content_scripts": [{
"matches": [],
"js": ["content.js"]
}]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(!manifest.injects_all_urls());
assert!(manifest.all_scripts().contains(&"content.js".to_string()));
}
#[test]
fn content_script_no_js_files() {
let json = r#"{
"name": "No JS",
"manifest_version": 3,
"version": "1.0",
"content_scripts": [{
"matches": ["<all_urls>"],
"css": ["styles.css"]
}]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.injects_all_urls());
assert!(!manifest.all_scripts().contains(&"styles.css".to_string()));
}
#[test]
fn content_script_multiple_js_files() {
let json = r#"{
"name": "Multi JS",
"manifest_version": 3,
"version": "1.0",
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["lib.js", "content.js", "utils.js"]
}]
}"#;
let manifest = Manifest::parse(json).unwrap();
let scripts = manifest.all_scripts();
assert!(scripts.contains(&"lib.js".to_string()));
assert!(scripts.contains(&"content.js".to_string()));
assert!(scripts.contains(&"utils.js".to_string()));
}
#[test]
fn content_script_default_run_at() {
let json = r#"{
"name": "Default",
"manifest_version": 3,
"version": "1.0",
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content.js"]
}]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.content_scripts[0].run_at.is_none());
}
#[test]
fn content_script_default_all_frames() {
let json = r#"{
"name": "Default",
"manifest_version": 3,
"version": "1.0",
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content.js"]
}]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(!manifest.content_scripts[0].all_frames); }
#[test]
fn externally_connectable_empty_matches() {
let json = r#"{
"name": "Empty Matches",
"manifest_version": 3,
"version": "1.0",
"externally_connectable": {
"matches": []
}
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(!manifest.is_externally_connectable());
}
#[test]
fn externally_connectable_wildcard() {
let json = r#"{
"name": "Wildcard",
"manifest_version": 3,
"version": "1.0",
"externally_connectable": {
"matches": ["*://*/*"]
}
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.is_externally_connectable());
}
#[test]
fn externally_connectable_specific_domain() {
let json = r#"{
"name": "Specific",
"manifest_version": 3,
"version": "1.0",
"externally_connectable": {
"matches": ["https://trusted.com/*"]
}
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.is_externally_connectable());
}
#[test]
fn externally_connectable_multiple_domains() {
let json = r#"{
"name": "Multi",
"manifest_version": 3,
"version": "1.0",
"externally_connectable": {
"matches": ["https://a.com/*", "https://b.com/*", "https://c.com/*"]
}
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.is_externally_connectable());
}
#[test]
fn externally_connectable_only_ids() {
let json = r#"{
"name": "IDs Only",
"manifest_version": 3,
"version": "1.0",
"externally_connectable": {
"ids": ["other-extension-id"]
}
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.is_externally_connectable());
}
#[test]
fn externally_connectable_with_tls_channel_id() {
let json = r#"{
"name": "TLS",
"manifest_version": 3,
"version": "1.0",
"externally_connectable": {
"matches": ["https://secure.com/*"],
"accepts_tls_channel_id": true
}
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.is_externally_connectable());
assert!(
manifest
.externally_connectable
.as_ref()
.unwrap()
.accepts_tls_channel_id
);
}
#[test]
fn externally_connectable_both_empty() {
let json = r#"{
"name": "Both Empty",
"manifest_version": 3,
"version": "1.0",
"externally_connectable": {
"matches": [],
"ids": []
}
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(!manifest.is_externally_connectable());
}
#[test]
fn all_scripts_background_first() {
let json = r#"{
"name": "Order",
"manifest_version": 3,
"version": "1.0",
"background": {
"service_worker": "sw.js"
},
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content.js"]
}]
}"#;
let manifest = Manifest::parse(json).unwrap();
let scripts = manifest.all_scripts();
assert_eq!(scripts[0], "sw.js");
assert_eq!(scripts[1], "content.js");
}
#[test]
fn all_scripts_mv2_order() {
let json = r#"{
"name": "MV2 Order",
"manifest_version": 2,
"version": "1.0",
"background": {
"scripts": ["bg1.js", "bg2.js"]
},
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["cs1.js", "cs2.js"]
}]
}"#;
let manifest = Manifest::parse(json).unwrap();
let scripts = manifest.all_scripts();
assert_eq!(scripts[0], "bg1.js");
assert_eq!(scripts[1], "bg2.js");
assert_eq!(scripts[2], "cs1.js");
assert_eq!(scripts[3], "cs2.js");
}
#[test]
fn all_scripts_no_background() {
let json = r#"{
"name": "No BG",
"manifest_version": 3,
"version": "1.0",
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content.js"]
}]
}"#;
let manifest = Manifest::parse(json).unwrap();
let scripts = manifest.all_scripts();
assert_eq!(scripts.len(), 1);
assert_eq!(scripts[0], "content.js");
}
#[test]
fn all_scripts_multiple_content_scripts() {
let json = r#"{
"name": "Multi CS",
"manifest_version": 3,
"version": "1.0",
"content_scripts": [
{"matches": ["https://a.com/*"], "js": ["a.js"]},
{"matches": ["https://b.com/*"], "js": ["b.js"]}
]
}"#;
let manifest = Manifest::parse(json).unwrap();
let scripts = manifest.all_scripts();
assert_eq!(scripts.len(), 2);
assert_eq!(scripts[0], "a.js");
assert_eq!(scripts[1], "b.js");
}
#[test]
fn all_scripts_empty_content_scripts() {
let json = r#"{
"name": "Empty CS",
"manifest_version": 3,
"version": "1.0",
"content_scripts": [
{"matches": ["https://a.com/*"], "js": []},
{"matches": ["https://b.com/*"], "js": ["b.js"]}
]
}"#;
let manifest = Manifest::parse(json).unwrap();
let scripts = manifest.all_scripts();
assert_eq!(scripts.len(), 1);
assert_eq!(scripts[0], "b.js");
}
#[test]
fn allows_eval_no_csp() {
let manifest =
Manifest::parse(r#"{"name":"T","manifest_version":3,"version":"1.0"}"#).unwrap();
assert!(manifest.allows_eval());
}
#[test]
fn allows_eval_with_unsafe_eval() {
let json = r#"{
"name": "Unsafe",
"manifest_version": 2,
"version": "1.0",
"content_security_policy": "script-src 'self' 'unsafe-eval'"
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.allows_eval());
}
#[test]
fn allows_eval_without_unsafe_eval() {
let json = r#"{
"name": "Safe",
"manifest_version": 2,
"version": "1.0",
"content_security_policy": "script-src 'self'"
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(!manifest.allows_eval());
}
#[test]
fn allows_eval_nested_quotes() {
let json = r#"{
"name": "Nested",
"manifest_version": 2,
"version": "1.0",
"content_security_policy": "script-src ''unsafe-eval''"
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.allows_eval());
}
#[test]
fn injects_all_urls_exact_match() {
let json = r#"{
"name": "All URLs",
"manifest_version": 3,
"version": "1.0",
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content.js"]
}]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.injects_all_urls());
}
#[test]
fn injects_all_urls_wildcard_pattern() {
let json = r#"{
"name": "Wildcard",
"manifest_version": 3,
"version": "1.0",
"content_scripts": [{
"matches": ["*://*/*"],
"js": ["content.js"]
}]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.injects_all_urls());
}
#[test]
fn injects_all_urls_specific_domain() {
let json = r#"{
"name": "Specific",
"manifest_version": 3,
"version": "1.0",
"content_scripts": [{
"matches": ["https://example.com/*"],
"js": ["content.js"]
}]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(!manifest.injects_all_urls());
}
#[test]
fn injects_all_urls_partial_wildcard() {
let json = r#"{
"name": "Partial",
"manifest_version": 3,
"version": "1.0",
"content_scripts": [{
"matches": ["https://*/*"],
"js": ["content.js"]
}]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(!manifest.injects_all_urls());
}
#[test]
fn injects_all_urls_mixed_patterns() {
let json = r#"{
"name": "Mixed",
"manifest_version": 3,
"version": "1.0",
"content_scripts": [{
"matches": ["https://example.com/*", "<all_urls>"],
"js": ["content.js"]
}]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.injects_all_urls());
}
#[test]
fn injects_all_urls_no_content_scripts() {
let manifest =
Manifest::parse(r#"{"name":"T","manifest_version":3,"version":"1.0"}"#).unwrap();
assert!(!manifest.injects_all_urls());
}
#[test]
fn injects_all_urls_empty_matches() {
let json = r#"{
"name": "Empty",
"manifest_version": 3,
"version": "1.0",
"content_scripts": [{
"matches": [],
"js": ["content.js"]
}]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(!manifest.injects_all_urls());
}
#[test]
fn many_permissions() {
let perms: Vec<String> = (0..100).map(|i| format!("permission{}", i)).collect();
let json = format!(
r#"{{
"name": "Many Perms",
"manifest_version": 3,
"version": "1.0",
"permissions": {:?}
}}"#,
perms
);
let manifest = Manifest::parse(&json).unwrap();
assert_eq!(manifest.permissions.len(), 100);
}
#[test]
fn many_content_scripts() {
let scripts: Vec<String> = (0..100)
.map(|i| {
format!(
r#"{{"matches": ["https://site{}.com/*"], "js": ["script{}.js"]}}"#,
i, i
)
})
.collect();
let json = format!(
r#"{{
"name": "Many CS",
"manifest_version": 3,
"version": "1.0",
"content_scripts": [{}]
}}"#,
scripts.join(",")
);
let manifest = Manifest::parse(&json).unwrap();
assert_eq!(manifest.content_scripts.len(), 100);
}
#[test]
fn very_long_strings() {
let long_name = "x".repeat(10000);
let json = format!(
r#"{{
"name": "{}",
"manifest_version": 3,
"version": "{}"
}}"#,
long_name, "1.0"
);
let manifest = Manifest::parse(&json).unwrap();
assert_eq!(manifest.name.len(), 10000);
}
#[test]
fn deeply_nested_json_not_accepted() {
let json = r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0",
"web_accessible_resources": [
{"resources": ["a.js"], "matches": ["<all_urls>"]}
]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(!manifest.web_accessible_resources.is_empty());
}
#[test]
fn web_accessible_resources_mv3_format() {
let json = r#"{
"name": "WAR",
"manifest_version": 3,
"version": "1.0",
"web_accessible_resources": [
{
"resources": ["image.png", "script.js"],
"matches": ["<all_urls>"]
}
]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert_eq!(manifest.web_accessible_resources.len(), 1);
}
#[test]
fn web_accessible_resources_mv2_format() {
let json = r#"{
"name": "WAR MV2",
"manifest_version": 2,
"version": "1.0",
"web_accessible_resources": ["image.png", "script.js"]
}"#;
let manifest = Manifest::parse(json).unwrap();
assert_eq!(manifest.web_accessible_resources.len(), 2);
}
#[test]
fn web_accessible_resources_empty() {
let json = r#"{
"name": "Empty WAR",
"manifest_version": 3,
"version": "1.0",
"web_accessible_resources": []
}"#;
let manifest = Manifest::parse(json).unwrap();
assert!(manifest.web_accessible_resources.is_empty());
}
#[test]
fn extension_csp_mv2_string() {
let json = r#"{
"name": "MV2 CSP",
"manifest_version": 2,
"version": "1.0",
"content_security_policy": "script-src 'self'"
}"#;
let manifest = Manifest::parse(json).unwrap();
assert_eq!(manifest.extension_csp(), Some("script-src 'self'"));
}
#[test]
fn extension_csp_mv3_object() {
let json = r#"{
"name": "MV3 CSP",
"manifest_version": 3,
"version": "1.0",
"content_security_policy": {
"extension_pages": "script-src 'self'"
}
}"#;
let manifest = Manifest::parse(json).unwrap();
assert_eq!(manifest.extension_csp(), Some("script-src 'self'"));
}
#[test]
fn extension_csp_none() {
let manifest =
Manifest::parse(r#"{"name":"T","manifest_version":3,"version":"1.0"}"#).unwrap();
assert_eq!(manifest.extension_csp(), None);
}
#[test]
fn extension_csp_object_no_extension_pages() {
let json = r#"{
"name": "No Ext Pages",
"manifest_version": 3,
"version": "1.0",
"content_security_policy": {
"sandbox": "sandbox-src 'self'"
}
}"#;
let manifest = Manifest::parse(json).unwrap();
assert_eq!(manifest.extension_csp(), None);
}
#[test]
fn extension_csp_empty_string() {
let json = r#"{
"name": "Empty CSP",
"manifest_version": 2,
"version": "1.0",
"content_security_policy": ""
}"#;
let manifest = Manifest::parse(json).unwrap();
assert_eq!(manifest.extension_csp(), Some(""));
}
}