use oxc_allocator::Allocator;
use oxc_ast::ast::{StringLiteral, TemplateLiteral};
use oxc_parser::Parser;
use oxc_span::SourceType;
pub fn mask(source: &str) -> (String, Vec<String>) {
if let Some(result) = mask_with_oxc(source) {
return result;
}
mask_heuristic(source)
}
pub fn unmask(source: &str, masks: &[String]) -> String {
let mut result = source.to_string();
for (idx, content) in masks.iter().enumerate() {
result = result.replace(&format!("__RWE_MASK_{idx}__"), content);
}
result
}
fn mask_with_oxc(source: &str) -> Option<(String, Vec<String>)> {
let alloc = Allocator::default();
let source_type = SourceType::default()
.with_module(true)
.with_jsx(true)
.with_typescript(true);
let parsed = Parser::new(&alloc, source, source_type).parse();
if parsed.panicked {
return None;
}
let mut collector = SpanCollector {
spans: Vec::new(),
text_spans: Vec::new(),
};
oxc_ast_visit::walk::walk_program(&mut collector, &parsed.program);
let mut all: Vec<(u32, u32, bool)> = collector
.spans
.iter()
.map(|(a, b)| (*a, *b, true))
.chain(collector.text_spans.iter().map(|(a, b)| (*a, *b, false)))
.collect();
all.sort_by_key(|s| s.0);
all.dedup();
let mut spans: Vec<(u32, u32, bool)> = Vec::with_capacity(all.len());
for entry in all {
if let Some(last) = spans.last() {
if entry.0 < last.1 {
continue;
}
}
spans.push(entry);
}
let mut masks: Vec<String> = Vec::new();
let mut result = String::with_capacity(source.len());
let mut cursor: usize = 0;
for (start, end, delimited) in &spans {
let s = *start as usize;
let e = *end as usize;
if s > source.len() || e > source.len() || s >= e {
continue;
}
if s > cursor {
result.push_str(&source[cursor..s]);
}
let span_text = &source[s..e];
if !*delimited {
let idx = masks.len();
masks.push(span_text.to_string());
result.push_str(&format!("__RWE_MASK_{idx}__"));
cursor = e;
continue;
}
if span_text.starts_with('`') {
result.push('`');
let inner = &span_text[1..span_text.len().saturating_sub(1)];
let idx = masks.len();
masks.push(inner.to_string());
result.push_str(&format!("__RWE_MASK_{idx}__"));
if span_text.ends_with('`') {
result.push('`');
}
} else if span_text.starts_with('"') || span_text.starts_with('\'') {
let quote = span_text.chars().next().unwrap();
result.push(quote);
let inner = &span_text[1..span_text.len().saturating_sub(1)];
let idx = masks.len();
masks.push(inner.to_string());
result.push_str(&format!("__RWE_MASK_{idx}__"));
if span_text.ends_with(quote) {
result.push(quote);
}
} else {
result.push_str(span_text);
}
cursor = e;
}
if cursor < source.len() {
result.push_str(&source[cursor..]);
}
Some((result, masks))
}
struct SpanCollector {
spans: Vec<(u32, u32)>,
text_spans: Vec<(u32, u32)>,
}
impl<'a> oxc_ast_visit::Visit<'a> for SpanCollector {
fn visit_string_literal(&mut self, lit: &StringLiteral<'a>) {
self.spans.push((lit.span.start, lit.span.end));
}
fn visit_template_literal(&mut self, lit: &TemplateLiteral<'a>) {
for quasi in &lit.quasis {
if quasi.span.end > quasi.span.start {
self.text_spans.push((quasi.span.start, quasi.span.end));
}
}
for expr in &lit.expressions {
oxc_ast_visit::walk::walk_expression(self, expr);
}
}
}
fn mask_heuristic(source: &str) -> (String, Vec<String>) {
let mut masks: Vec<String> = Vec::new();
let mut result = String::with_capacity(source.len());
let chars: Vec<char> = source.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
if chars[i] == '/' && i + 1 < len && chars[i + 1] == '/' {
result.push(chars[i]);
i += 1;
result.push(chars[i]);
i += 1;
while i < len && chars[i] != '\n' {
result.push(chars[i]);
i += 1;
}
if i < len {
result.push(chars[i]); }
i += 1;
continue;
}
if chars[i] == '/' && i + 1 < len && chars[i + 1] == '*' {
result.push(chars[i]);
i += 1;
result.push(chars[i]);
i += 1;
while i < len {
if chars[i] == '*' && i + 1 < len && chars[i + 1] == '/' {
result.push(chars[i]);
i += 1;
result.push(chars[i]);
i += 1;
break;
}
result.push(chars[i]);
i += 1;
}
continue;
}
if chars[i] == '/' && i + 1 < len && chars[i + 1] != '/' && chars[i + 1] != '*' {
let prev_significant = result.chars().rev().find(|c| !c.is_ascii_whitespace());
let is_regex = match prev_significant {
None => true,
Some(c) => matches!(
c,
'(' | '='
| '['
| '!'
| '&'
| '|'
| '?'
| ':'
| ','
| ';'
| '{'
| '}'
| '~'
| '^'
| '%'
| '<'
| '>'
| '+'
| '-'
| '*'
| '/'
| '\n'
),
};
if is_regex {
result.push(chars[i]);
i += 1;
while i < len {
if chars[i] == '\\' {
result.push(chars[i]);
i += 1;
if i < len {
result.push(chars[i]);
i += 1;
}
} else if chars[i] == '/' {
result.push(chars[i]);
i += 1;
while i < len && chars[i].is_ascii_alphabetic() {
result.push(chars[i]);
i += 1;
}
break;
} else if chars[i] == '\n' {
break;
} else {
result.push(chars[i]);
i += 1;
}
}
continue;
}
}
match chars[i] {
'"' | '\'' => {
let quote = chars[i];
let mut content = String::new();
i += 1;
while i < len {
let ch = chars[i];
if ch == '\\' {
content.push(ch);
i += 1;
if i < len {
content.push(chars[i]);
}
} else if ch == quote {
break;
} else if ch == '\n' {
content.push(ch);
break;
} else {
content.push(ch);
}
i += 1;
}
let idx = masks.len();
masks.push(content);
result.push(quote);
result.push_str(&format!("__RWE_MASK_{idx}__"));
result.push(quote);
}
'`' => {
result.push('`');
i += 1;
let mut chunk = String::new();
while i < len {
let ch = chars[i];
if ch == '\\' {
chunk.push(ch);
i += 1;
if i < len {
chunk.push(chars[i]);
i += 1;
}
continue;
}
if ch == '`' {
break;
}
if ch == '$' && i + 1 < len && chars[i + 1] == '{' {
if !chunk.is_empty() {
let idx = masks.len();
masks.push(std::mem::take(&mut chunk));
result.push_str(&format!("__RWE_MASK_{idx}__"));
}
result.push('$');
result.push('{');
i += 2;
let mut depth = 1i32;
while i < len && depth > 0 {
let c2 = chars[i];
if c2 == '{' {
depth += 1;
} else if c2 == '}' {
depth -= 1;
if depth == 0 {
break;
}
}
result.push(c2);
i += 1;
}
result.push('}');
i += 1;
continue;
}
chunk.push(ch);
i += 1;
}
if !chunk.is_empty() {
let idx = masks.len();
masks.push(chunk);
result.push_str(&format!("__RWE_MASK_{idx}__"));
}
result.push('`');
}
c => result.push(c),
}
i += 1;
}
(result, masks)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_substitution_stays_visible_as_code() {
let src = "const ROW_GRID = \"grid\";\nconst cls = `${ROW_GRID} text-sm`;\n";
let (masked, _) = mask(src);
assert!(
masked.contains("${ROW_GRID}"),
"substitution was masked away: {masked}"
);
assert!(!masked.contains("grid`"), "text chunk was left unmasked: {masked}");
}
#[test]
fn template_text_is_still_masked() {
let src = "const a = `export default function nope() {}`;\n";
let (masked, _) = mask(src);
assert!(
!masked.contains("export default function"),
"code-like text inside a template leaked: {masked}"
);
}
#[test]
fn a_string_inside_a_substitution_is_masked() {
let src = "const a = `${cx(\"export default function\")}`;\n";
let (masked, _) = mask(src);
assert!(masked.contains("cx("), "the call was masked away: {masked}");
assert!(
!masked.contains("export default function"),
"the nested string leaked: {masked}"
);
}
#[test]
fn masking_round_trips() {
for src in [
"const a = `${x} and ${y}`;\n",
"const b = \"plain\";\nconst c = `a${b}c${d}`;\n",
"const d = `no substitutions here`;\n",
"const e = `${`${inner}`}`;\n",
] {
let (masked, masks) = mask(src);
assert_eq!(unmask(&masked, &masks), src, "round trip failed for {src}");
}
}
#[test]
fn the_fallback_masker_follows_the_same_rule() {
let src = "const ROW_GRID = 'grid';\nconst cls = `${ROW_GRID} text-sm`;\n";
let (masked, masks) = mask_heuristic(src);
assert!(masked.contains("${ROW_GRID}"), "fallback masked it away: {masked}");
assert_eq!(unmask(&masked, &masks), src);
}
}