mirror of
https://github.com/MercuryWorkshop/scramjet.git
synced 2025-05-15 23:30:00 -04:00
rewriter -> js
This commit is contained in:
parent
531cc7273c
commit
e8f504f0dc
13 changed files with 22 additions and 22 deletions
16
rewriter/js/Cargo.toml
Normal file
16
rewriter/js/Cargo.toml
Normal file
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "js"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
oxc = { workspace = true }
|
||||
smallvec = "1.14.0"
|
||||
thiserror = "2.0.12"
|
||||
|
||||
[features]
|
||||
default = ["debug"]
|
||||
debug = []
|
26
rewriter/js/src/cfg.rs
Normal file
26
rewriter/js/src/cfg.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
use oxc::allocator::{Allocator, String};
|
||||
|
||||
pub struct Config<'alloc, E>
|
||||
where
|
||||
E: Fn(&str, &'alloc Allocator) -> String<'alloc>,
|
||||
{
|
||||
pub prefix: &'alloc str,
|
||||
pub sourcetag: &'alloc str,
|
||||
pub base: &'alloc str,
|
||||
|
||||
pub wrapfn: &'alloc str,
|
||||
pub wrapthisfn: &'alloc str,
|
||||
pub importfn: &'alloc str,
|
||||
pub rewritefn: &'alloc str,
|
||||
pub setrealmfn: &'alloc str,
|
||||
pub metafn: &'alloc str,
|
||||
pub pushsourcemapfn: &'alloc str,
|
||||
|
||||
/// URL REWRITER IS RESPONSIBLE FOR ADDING BASE
|
||||
pub urlrewriter: E,
|
||||
|
||||
pub capture_errors: bool,
|
||||
pub scramitize: bool,
|
||||
pub do_sourcemaps: bool,
|
||||
pub strict_rewrites: bool,
|
||||
}
|
517
rewriter/js/src/changes.rs
Normal file
517
rewriter/js/src/changes.rs
Normal file
|
@ -0,0 +1,517 @@
|
|||
use std::cmp::Ordering;
|
||||
|
||||
use oxc::{
|
||||
allocator::{Allocator, String, Vec},
|
||||
ast::ast::AssignmentOperator,
|
||||
span::{format_compact_str, Atom, Span},
|
||||
};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
|
||||
use crate::{cfg::Config, RewriterError};
|
||||
|
||||
// const STRICTCHECKER: &str = "(function(a){arguments[0]=false;return a})(true)";
|
||||
const STRICTCHECKER: &str = "(function(){return !this;})()";
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Rewrite<'data> {
|
||||
/// `(cfg.wrapfn(ident,strictchecker))` | `cfg.wrapfn(ident,strictchecker)`
|
||||
WrapFn {
|
||||
span: Span,
|
||||
wrapped: bool,
|
||||
},
|
||||
/// `cfg.setrealmfn({}).ident`
|
||||
SetRealmFn {
|
||||
span: Span,
|
||||
},
|
||||
/// `cfg.wrapthis(this)`
|
||||
WrapThisFn {
|
||||
span: Span,
|
||||
},
|
||||
/// `(cfg.importfn("cfg.base"))`
|
||||
ImportFn {
|
||||
span: Span,
|
||||
},
|
||||
/// `cfg.metafn("cfg.base")`
|
||||
MetaFn {
|
||||
span: Span,
|
||||
},
|
||||
|
||||
// dead code only if debug is disabled
|
||||
#[allow(dead_code)]
|
||||
/// `$scramerr(name)`
|
||||
ScramErr {
|
||||
span: Span,
|
||||
ident: Atom<'data>,
|
||||
},
|
||||
/// `$scramitize(span)`
|
||||
Scramitize {
|
||||
span: Span,
|
||||
},
|
||||
|
||||
/// `eval(cfg.rewritefn(inner))`
|
||||
Eval {
|
||||
span: Span,
|
||||
inner: Span,
|
||||
},
|
||||
/// `((t)=>$scramjet$tryset(name,"op",t)||(name op t))(rhsspan)`
|
||||
Assignment {
|
||||
name: Atom<'data>,
|
||||
entirespan: Span,
|
||||
rhsspan: Span,
|
||||
op: AssignmentOperator,
|
||||
},
|
||||
/// `ident,` -> `ident: cfg.wrapfn(ident),`
|
||||
ShorthandObj {
|
||||
span: Span,
|
||||
name: Atom<'data>,
|
||||
},
|
||||
SourceTag {
|
||||
span: Span,
|
||||
},
|
||||
|
||||
// don't use for anything static, only use for stuff like rewriteurl
|
||||
Replace {
|
||||
span: Span,
|
||||
text: String<'data>,
|
||||
},
|
||||
Delete {
|
||||
span: Span,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'data> Rewrite<'data> {
|
||||
fn into_inner(self) -> SmallVec<[JsChange<'data>; 4]> {
|
||||
match self {
|
||||
Self::WrapFn {
|
||||
wrapped: extra,
|
||||
span,
|
||||
} => {
|
||||
let start = Span::new(span.start, span.start);
|
||||
let end = Span::new(span.end, span.end);
|
||||
|
||||
smallvec![
|
||||
JsChange::WrapFnLeft { span: start, extra },
|
||||
JsChange::WrapFnRight { span: end, extra }
|
||||
]
|
||||
}
|
||||
Self::SetRealmFn { span } => smallvec![JsChange::SetRealmFn { span }],
|
||||
Self::WrapThisFn { span } => smallvec![
|
||||
JsChange::WrapThisFn {
|
||||
span: Span::new(span.start, span.start)
|
||||
},
|
||||
JsChange::ClosingParen {
|
||||
span: Span::new(span.end, span.end),
|
||||
semi: false,
|
||||
}
|
||||
],
|
||||
Self::ImportFn { span } => smallvec![JsChange::ImportFn { span }],
|
||||
Self::MetaFn { span } => smallvec![JsChange::MetaFn { span }],
|
||||
|
||||
Self::ScramErr { span, ident } => {
|
||||
smallvec![JsChange::ScramErrFn {
|
||||
span: Span::new(span.start, span.start),
|
||||
ident,
|
||||
},]
|
||||
}
|
||||
Self::Scramitize { span } => {
|
||||
smallvec![
|
||||
JsChange::ScramitizeFn {
|
||||
span: Span::new(span.start, span.start)
|
||||
},
|
||||
JsChange::ClosingParen {
|
||||
span: Span::new(span.end, span.end),
|
||||
semi: false,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Self::Eval { inner, span } => smallvec![
|
||||
JsChange::EvalRewriteFn {
|
||||
span: Span::new(span.start, inner.start)
|
||||
},
|
||||
JsChange::ReplaceClosingParen {
|
||||
span: Span::new(inner.end, span.end),
|
||||
}
|
||||
],
|
||||
Self::Assignment {
|
||||
name,
|
||||
rhsspan,
|
||||
op,
|
||||
entirespan,
|
||||
} => smallvec![
|
||||
JsChange::AssignmentLeft {
|
||||
name,
|
||||
op,
|
||||
span: Span::new(entirespan.start, rhsspan.start)
|
||||
},
|
||||
JsChange::ReplaceClosingParen {
|
||||
span: Span::new(rhsspan.end, entirespan.end)
|
||||
}
|
||||
],
|
||||
// maps to insert
|
||||
Self::ShorthandObj { name, span } => smallvec![JsChange::ShorthandObj {
|
||||
ident: name,
|
||||
span: Span::new(span.end, span.end)
|
||||
}],
|
||||
// maps to insert
|
||||
Self::SourceTag { span } => smallvec![JsChange::SourceTag { span }],
|
||||
Self::Replace { text, span } => smallvec![JsChange::Replace { span, text }],
|
||||
Self::Delete { span } => smallvec![JsChange::Delete { span }],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Change<'a> {
|
||||
Str(&'a str),
|
||||
U32(u32),
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Change<'a> {
|
||||
fn from(value: &'a str) -> Self {
|
||||
Self::Str(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a String<'_>> for Change<'a> {
|
||||
fn from(value: &'a String<'_>) -> Self {
|
||||
Self::Str(value.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Atom<'_>> for Change<'a> {
|
||||
fn from(value: &'a Atom<'_>) -> Self {
|
||||
Self::Str(value.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a AssignmentOperator> for Change<'a> {
|
||||
fn from(value: &'a AssignmentOperator) -> Self {
|
||||
Self::Str(value.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for Change<'static> {
|
||||
fn from(value: u32) -> Self {
|
||||
Self::U32(value)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! changes {
|
||||
[$($change:expr),+] => {
|
||||
smallvec![$(Change::from($change)),+]
|
||||
};
|
||||
}
|
||||
|
||||
type Changes<'a> = SmallVec<[Change<'a>; 8]>;
|
||||
|
||||
enum JsChangeInner<'a> {
|
||||
Insert { loc: u32, str: Changes<'a> },
|
||||
Replace { str: Changes<'a> },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum JsChange<'data> {
|
||||
/// insert `${cfg.wrapfn}(`
|
||||
WrapFnLeft { span: Span, extra: bool },
|
||||
/// insert `,strictchecker)`
|
||||
WrapFnRight { span: Span, extra: bool },
|
||||
/// insert `${cfg.setrealmfn}({}).`
|
||||
SetRealmFn { span: Span },
|
||||
/// insert `${cfg.wrapthis}(`
|
||||
WrapThisFn { span: Span },
|
||||
/// insert `$scramerr(ident);`
|
||||
ScramErrFn { span: Span, ident: Atom<'data> },
|
||||
/// insert `$scramitize(`
|
||||
ScramitizeFn { span: Span },
|
||||
/// insert `eval(${cfg.rewritefn}(`
|
||||
EvalRewriteFn { span: Span },
|
||||
/// insert `: ${cfg.wrapfn}(ident)`
|
||||
ShorthandObj { span: Span, ident: Atom<'data> },
|
||||
/// insert scramtag
|
||||
SourceTag { span: Span },
|
||||
|
||||
/// replace span with `${cfg.importfn}`
|
||||
ImportFn { span: Span },
|
||||
/// replace span with `${cfg.metafn}("${cfg.base}")`
|
||||
MetaFn { span: Span },
|
||||
/// replace span with `((t)=>$scramjet$tryset(${name},"${op}",t)||(${name}${op}t))(`
|
||||
AssignmentLeft {
|
||||
span: Span,
|
||||
name: Atom<'data>,
|
||||
op: AssignmentOperator,
|
||||
},
|
||||
|
||||
/// replace span with `)`
|
||||
ReplaceClosingParen { span: Span },
|
||||
/// insert `)`
|
||||
ClosingParen { span: Span, semi: bool },
|
||||
|
||||
/// replace span with text
|
||||
Replace { span: Span, text: String<'data> },
|
||||
/// replace span with ""
|
||||
Delete { span: Span },
|
||||
}
|
||||
|
||||
impl JsChange<'_> {
|
||||
fn get_span(&self) -> &Span {
|
||||
match self {
|
||||
Self::WrapFnLeft { span, .. }
|
||||
| Self::WrapFnRight { span, .. }
|
||||
| Self::SetRealmFn { span }
|
||||
| Self::WrapThisFn { span }
|
||||
| Self::ScramErrFn { span, .. }
|
||||
| Self::ScramitizeFn { span }
|
||||
| Self::EvalRewriteFn { span }
|
||||
| Self::ShorthandObj { span, .. }
|
||||
| Self::SourceTag { span, .. }
|
||||
| Self::ImportFn { span }
|
||||
| Self::MetaFn { span }
|
||||
| Self::AssignmentLeft { span, .. }
|
||||
| Self::ReplaceClosingParen { span }
|
||||
| Self::ClosingParen { span, .. }
|
||||
| Self::Replace { span, .. }
|
||||
| Self::Delete { span } => span,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_inner<'alloc, 'change, E>(
|
||||
&'change self,
|
||||
cfg: &'change Config<'alloc, E>,
|
||||
offset: u32,
|
||||
) -> JsChangeInner<'change>
|
||||
where
|
||||
E: Fn(&str, &'alloc Allocator) -> String<'alloc>,
|
||||
{
|
||||
match self {
|
||||
Self::WrapFnLeft { span, extra } => {
|
||||
if *extra {
|
||||
JsChangeInner::Insert {
|
||||
loc: span.start,
|
||||
str: changes!["(", cfg.wrapfn, "("],
|
||||
}
|
||||
} else {
|
||||
JsChangeInner::Insert {
|
||||
loc: span.start,
|
||||
str: changes![cfg.wrapfn, "("],
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::WrapFnRight { span, extra } => {
|
||||
if *extra {
|
||||
JsChangeInner::Insert {
|
||||
loc: span.start,
|
||||
str: changes![",", STRICTCHECKER, "))"],
|
||||
}
|
||||
} else {
|
||||
JsChangeInner::Insert {
|
||||
loc: span.start,
|
||||
str: changes![",", STRICTCHECKER, ")"],
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::SetRealmFn { span } => JsChangeInner::Insert {
|
||||
loc: span.start,
|
||||
str: changes![cfg.setrealmfn, "({})."],
|
||||
},
|
||||
Self::WrapThisFn { span } => JsChangeInner::Insert {
|
||||
loc: span.start,
|
||||
str: changes![cfg.wrapthisfn, "("],
|
||||
},
|
||||
Self::ScramErrFn { span, ident } => JsChangeInner::Insert {
|
||||
loc: span.start,
|
||||
str: changes!["$scramerr(", ident, ");"],
|
||||
},
|
||||
Self::ScramitizeFn { span } => JsChangeInner::Insert {
|
||||
loc: span.start,
|
||||
str: changes![" $scramitize("],
|
||||
},
|
||||
Self::EvalRewriteFn { .. } => JsChangeInner::Replace {
|
||||
str: changes!["eval(", cfg.rewritefn, "("],
|
||||
},
|
||||
Self::ShorthandObj { span, ident } => JsChangeInner::Insert {
|
||||
loc: span.start,
|
||||
str: changes![":", cfg.wrapfn, "(", ident, ")"],
|
||||
},
|
||||
Self::SourceTag { span } => JsChangeInner::Insert {
|
||||
loc: span.start,
|
||||
str: changes!["/*scramtag ", span.start + offset, " ", cfg.sourcetag, "*/"],
|
||||
},
|
||||
Self::ImportFn { .. } => JsChangeInner::Replace {
|
||||
str: changes![cfg.importfn, "(\"", cfg.base, "\","],
|
||||
},
|
||||
Self::MetaFn { .. } => JsChangeInner::Replace {
|
||||
str: changes![cfg.metafn, "(\"", cfg.base, "\")"],
|
||||
},
|
||||
Self::AssignmentLeft { name, op, .. } => JsChangeInner::Replace {
|
||||
str: changes![
|
||||
"((t)=>$scramjet$tryset(",
|
||||
name,
|
||||
",\"",
|
||||
op,
|
||||
"\",t)||(",
|
||||
name,
|
||||
op,
|
||||
"t))("
|
||||
],
|
||||
},
|
||||
Self::ReplaceClosingParen { .. } => JsChangeInner::Replace { str: changes![")"] },
|
||||
Self::ClosingParen { span, semi } => JsChangeInner::Insert {
|
||||
loc: span.start,
|
||||
str: if *semi { changes![");"] } else { changes![")"] },
|
||||
},
|
||||
Self::Replace { text, .. } => JsChangeInner::Replace {
|
||||
str: changes![text],
|
||||
},
|
||||
Self::Delete { .. } => JsChangeInner::Replace { str: changes![""] },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for JsChange<'_> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for JsChange<'_> {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
match self.get_span().start.cmp(&other.get_span().start) {
|
||||
Ordering::Equal => match (self, other) {
|
||||
(Self::ScramErrFn { .. }, _) => Ordering::Less,
|
||||
(_, Self::ScramErrFn { .. }) => Ordering::Greater,
|
||||
_ => Ordering::Equal,
|
||||
},
|
||||
x => x,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct JsChangeResult<'alloc> {
|
||||
pub js: Vec<'alloc, u8>,
|
||||
pub sourcemap: Vec<'alloc, u8>,
|
||||
}
|
||||
|
||||
pub(crate) struct JsChanges<'alloc: 'data, 'data> {
|
||||
alloc: &'alloc Allocator,
|
||||
inner: Vec<'alloc, JsChange<'data>>,
|
||||
}
|
||||
|
||||
impl<'alloc: 'data, 'data> JsChanges<'alloc, 'data> {
|
||||
pub fn new(alloc: &'alloc Allocator, capacity: usize) -> Self {
|
||||
Self {
|
||||
inner: Vec::with_capacity_in(capacity, alloc),
|
||||
alloc,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, rewrite: Rewrite<'data>) {
|
||||
for change in rewrite.into_inner() {
|
||||
self.inner.push(change);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn perform<E>(
|
||||
&mut self,
|
||||
js: &str,
|
||||
cfg: &Config<'alloc, E>,
|
||||
) -> Result<JsChangeResult<'alloc>, RewriterError>
|
||||
where
|
||||
E: Fn(&str, &'alloc Allocator) -> String<'alloc>,
|
||||
{
|
||||
// using wrapping adds for perf, 4gb large files is really an edge case
|
||||
|
||||
let mut cursor = 0;
|
||||
let mut offset = 0i32;
|
||||
let mut buffer = Vec::with_capacity_in(js.len() * 2, self.alloc);
|
||||
|
||||
macro_rules! tryget {
|
||||
($start:ident..$end:ident) => {
|
||||
js.get($start as usize..$end as usize)
|
||||
.ok_or_else(|| RewriterError::Oob($start, $end))?
|
||||
};
|
||||
}
|
||||
macro_rules! eval {
|
||||
($change:expr) => {
|
||||
match $change {
|
||||
Change::Str(x) => {
|
||||
buffer.extend_from_slice(x.as_bytes());
|
||||
x.len()
|
||||
}
|
||||
Change::U32(x) => {
|
||||
let x = format_compact_str!("{}", x);
|
||||
buffer.extend_from_slice(x.as_bytes());
|
||||
x.len()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// insert has a 9 byte size, replace has a 13 byte minimum and usually it's like 5 bytes
|
||||
// for the old str added on so use 16 as a really rough estimate
|
||||
let mut map = Vec::with_capacity_in(self.inner.len() * 16, self.alloc);
|
||||
map.extend_from_slice(&(self.inner.len() as u32).to_le_bytes());
|
||||
|
||||
self.inner.sort();
|
||||
|
||||
for change in &self.inner {
|
||||
let span = change.get_span();
|
||||
let start = span.start;
|
||||
let end = span.end;
|
||||
|
||||
buffer.extend_from_slice(tryget!(cursor..start).as_bytes());
|
||||
|
||||
match change.to_inner(cfg, cursor) {
|
||||
JsChangeInner::Insert { loc, str } => {
|
||||
let mut len = 0u32;
|
||||
buffer.extend_from_slice(tryget!(start..loc).as_bytes());
|
||||
for str in &str {
|
||||
len += eval!(str) as u32;
|
||||
}
|
||||
buffer.extend_from_slice(tryget!(loc..end).as_bytes());
|
||||
|
||||
// INSERT op
|
||||
map.push(0);
|
||||
// pos
|
||||
map.extend_from_slice(&loc.wrapping_add_signed(offset).to_le_bytes());
|
||||
// size
|
||||
map.extend_from_slice(&len.to_le_bytes());
|
||||
|
||||
offset = offset.wrapping_add_unsigned(len);
|
||||
}
|
||||
JsChangeInner::Replace { str } => {
|
||||
let mut len = 0u32;
|
||||
for str in &str {
|
||||
len += eval!(str) as u32;
|
||||
}
|
||||
|
||||
// REPLACE op
|
||||
map.push(1);
|
||||
// len
|
||||
map.extend_from_slice(&(span.end - span.start).to_le_bytes());
|
||||
// start
|
||||
map.extend_from_slice(&(span.start.wrapping_add_signed(offset)).to_le_bytes());
|
||||
// end
|
||||
map.extend_from_slice(
|
||||
&((span.start + len).wrapping_add_signed(offset)).to_le_bytes(),
|
||||
);
|
||||
// oldstr
|
||||
map.extend_from_slice(tryget!(start..end).as_bytes());
|
||||
|
||||
let len = i32::try_from(len).map_err(|_| RewriterError::AddedTooLarge)?;
|
||||
let diff = len.wrapping_sub_unsigned(span.end - span.start);
|
||||
offset = offset.wrapping_add(diff);
|
||||
}
|
||||
}
|
||||
|
||||
cursor = end;
|
||||
}
|
||||
|
||||
let js_len = js.len() as u32;
|
||||
buffer.extend_from_slice(tryget!(cursor..js_len).as_bytes());
|
||||
|
||||
Ok(JsChangeResult {
|
||||
js: buffer,
|
||||
sourcemap: map,
|
||||
})
|
||||
}
|
||||
}
|
89
rewriter/js/src/lib.rs
Normal file
89
rewriter/js/src/lib.rs
Normal file
|
@ -0,0 +1,89 @@
|
|||
use oxc::{
|
||||
allocator::{Allocator, String, Vec},
|
||||
ast_visit::Visit,
|
||||
diagnostics::OxcDiagnostic,
|
||||
parser::{ParseOptions, Parser},
|
||||
span::SourceType,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod cfg;
|
||||
pub mod changes;
|
||||
mod visitor;
|
||||
|
||||
use cfg::Config;
|
||||
use changes::{JsChangeResult, JsChanges};
|
||||
use visitor::Visitor;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum RewriterError {
|
||||
#[error("oxc panicked in parser: {0}")]
|
||||
OxcPanicked(std::string::String),
|
||||
#[error("out of bounds while applying range: {0}..{1})")]
|
||||
Oob(u32, u32),
|
||||
#[error("too much code added while applying changes")]
|
||||
AddedTooLarge,
|
||||
#[error("formatting error: {0}")]
|
||||
Formatting(#[from] std::fmt::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RewriteResult<'alloc> {
|
||||
pub js: Vec<'alloc, u8>,
|
||||
pub sourcemap: Vec<'alloc, u8>,
|
||||
pub errors: std::vec::Vec<OxcDiagnostic>,
|
||||
}
|
||||
|
||||
pub fn rewrite<'alloc, 'data, E>(
|
||||
alloc: &'alloc Allocator,
|
||||
js: &'data str,
|
||||
config: Config<'alloc, E>,
|
||||
module: bool,
|
||||
capacity: usize,
|
||||
) -> Result<RewriteResult<'alloc>, RewriterError>
|
||||
where
|
||||
E: Fn(&str, &'alloc Allocator) -> String<'alloc>,
|
||||
{
|
||||
let source_type = SourceType::unambiguous()
|
||||
.with_javascript(true)
|
||||
.with_module(module)
|
||||
.with_standard(true);
|
||||
let ret = Parser::new(alloc, js, source_type)
|
||||
.with_options(ParseOptions {
|
||||
parse_regular_expression: false,
|
||||
allow_v8_intrinsics: true,
|
||||
allow_return_outside_function: true,
|
||||
preserve_parens: true,
|
||||
})
|
||||
.parse();
|
||||
|
||||
if ret.panicked {
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut errors = std::string::String::new();
|
||||
for error in ret.errors {
|
||||
writeln!(errors, "{error}")?;
|
||||
}
|
||||
return Err(RewriterError::OxcPanicked(errors));
|
||||
}
|
||||
|
||||
let mut visitor = Visitor {
|
||||
jschanges: JsChanges::new(alloc, capacity),
|
||||
config,
|
||||
alloc,
|
||||
};
|
||||
visitor.visit_program(&ret.program);
|
||||
let Visitor {
|
||||
mut jschanges,
|
||||
config,
|
||||
alloc: _,
|
||||
} = visitor;
|
||||
|
||||
let JsChangeResult { js, sourcemap } = jschanges.perform(js, &config)?;
|
||||
|
||||
Ok(RewriteResult {
|
||||
js,
|
||||
sourcemap,
|
||||
errors: ret.errors,
|
||||
})
|
||||
}
|
313
rewriter/js/src/visitor.rs
Normal file
313
rewriter/js/src/visitor.rs
Normal file
|
@ -0,0 +1,313 @@
|
|||
use oxc::{
|
||||
allocator::{Allocator, String},
|
||||
ast::ast::{
|
||||
AssignmentExpression, AssignmentTarget, CallExpression, DebuggerStatement,
|
||||
ExportAllDeclaration, ExportNamedDeclaration, Expression, FunctionBody,
|
||||
IdentifierReference, ImportDeclaration, ImportExpression, MemberExpression, MetaProperty,
|
||||
NewExpression, ObjectExpression, ObjectPropertyKind, ReturnStatement, ThisExpression,
|
||||
UnaryExpression, UnaryOperator, UpdateExpression,
|
||||
},
|
||||
ast_visit::{walk, Visit},
|
||||
span::{Atom, GetSpan, Span},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
cfg::Config,
|
||||
changes::{JsChanges, Rewrite},
|
||||
};
|
||||
|
||||
// js MUST not be able to get a reference to any of these because sbx
|
||||
//
|
||||
// maybe move this out of this lib?
|
||||
const UNSAFE_GLOBALS: &[&str] = &[
|
||||
"window",
|
||||
"self",
|
||||
"globalThis",
|
||||
"this",
|
||||
"parent",
|
||||
"top",
|
||||
"location",
|
||||
"document",
|
||||
"eval",
|
||||
"frames",
|
||||
];
|
||||
|
||||
pub struct Visitor<'alloc, 'data, E>
|
||||
where
|
||||
E: Fn(&str, &'alloc Allocator) -> String<'alloc>,
|
||||
{
|
||||
pub jschanges: JsChanges<'alloc, 'data>,
|
||||
pub config: Config<'alloc, E>,
|
||||
pub alloc: &'alloc Allocator,
|
||||
}
|
||||
|
||||
impl<'alloc, 'data, E> Visitor<'alloc, 'data, E>
|
||||
where
|
||||
E: Fn(&str, &'alloc Allocator) -> String<'alloc>,
|
||||
{
|
||||
fn rewrite_url(&mut self, url: Atom<'data>) -> oxc::allocator::String<'alloc> {
|
||||
let mut urlencoded = (self.config.urlrewriter)(&url, self.alloc);
|
||||
urlencoded.insert_str(0, self.config.prefix);
|
||||
urlencoded
|
||||
}
|
||||
|
||||
fn rewrite_ident(&mut self, name: &Atom, span: Span) {
|
||||
if UNSAFE_GLOBALS.contains(&name.as_str()) {
|
||||
self.jschanges.add(Rewrite::WrapFn {
|
||||
span,
|
||||
wrapped: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn walk_member_expression(&mut self, it: &Expression) -> bool {
|
||||
match it {
|
||||
Expression::Identifier(s) => {
|
||||
self.rewrite_ident(&s.name, s.span);
|
||||
true
|
||||
}
|
||||
Expression::StaticMemberExpression(s) => self.walk_member_expression(&s.object),
|
||||
Expression::ComputedMemberExpression(s) => self.walk_member_expression(&s.object),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn scramitize(&mut self, span: Span) {
|
||||
self.jschanges.add(Rewrite::Scramitize { span });
|
||||
}
|
||||
}
|
||||
|
||||
impl<'alloc, 'data, E> Visit<'data> for Visitor<'alloc, 'data, E>
|
||||
where
|
||||
E: Fn(&str, &'alloc Allocator) -> String<'alloc>,
|
||||
{
|
||||
fn visit_identifier_reference(&mut self, it: &IdentifierReference) {
|
||||
// if self.config.capture_errors {
|
||||
// self.jschanges.insert(JsChange::GenericChange {
|
||||
// span: it.span,
|
||||
// text: format!(
|
||||
// "{}({}, typeof arguments != 'undefined' && arguments)",
|
||||
// self.config.wrapfn, it.name
|
||||
// ),
|
||||
// });
|
||||
// } else {
|
||||
//
|
||||
if UNSAFE_GLOBALS.contains(&it.name.as_str()) {
|
||||
self.jschanges.add(Rewrite::WrapFn {
|
||||
span: it.span,
|
||||
wrapped: false,
|
||||
});
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
fn visit_new_expression(&mut self, it: &NewExpression<'data>) {
|
||||
self.walk_member_expression(&it.callee);
|
||||
walk::walk_arguments(self, &it.arguments);
|
||||
}
|
||||
|
||||
fn visit_member_expression(&mut self, it: &MemberExpression<'data>) {
|
||||
// TODO
|
||||
// you could break this with ["postMessage"] etc
|
||||
// however this code only exists because of recaptcha whatever
|
||||
// and it would slow down js execution a lot
|
||||
if let MemberExpression::StaticMemberExpression(s) = it {
|
||||
if s.property.name == "postMessage" {
|
||||
self.jschanges.add(Rewrite::SetRealmFn {
|
||||
span: s.property.span,
|
||||
});
|
||||
|
||||
walk::walk_expression(self, &s.object);
|
||||
return; // unwise to walk the rest of the tree
|
||||
}
|
||||
|
||||
if !self.config.strict_rewrites && !UNSAFE_GLOBALS.contains(&s.property.name.as_str()) {
|
||||
if let Expression::Identifier(_) | Expression::ThisExpression(_) = &s.object {
|
||||
// cull tree - this should be safe
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if self.config.scramitize
|
||||
&& !matches!(s.object, Expression::MetaProperty(_) | Expression::Super(_))
|
||||
{
|
||||
self.scramitize(s.object.span());
|
||||
}
|
||||
}
|
||||
|
||||
walk::walk_member_expression(self, it);
|
||||
}
|
||||
fn visit_this_expression(&mut self, it: &ThisExpression) {
|
||||
self.jschanges.add(Rewrite::WrapThisFn { span: it.span });
|
||||
}
|
||||
|
||||
fn visit_debugger_statement(&mut self, it: &DebuggerStatement) {
|
||||
// delete debugger statements entirely. some sites will spam debugger as an anti-debugging measure, and we don't want that!
|
||||
self.jschanges.add(Rewrite::Delete { span: it.span });
|
||||
}
|
||||
|
||||
// we can't overwrite window.eval in the normal way because that would make everything an
|
||||
// indirect eval, which could break things. we handle that edge case here
|
||||
fn visit_call_expression(&mut self, it: &CallExpression<'data>) {
|
||||
if let Expression::Identifier(s) = &it.callee {
|
||||
// if it's optional that actually makes it an indirect eval which is handled separately
|
||||
if s.name == "eval" && !it.optional {
|
||||
self.jschanges.add(Rewrite::Eval {
|
||||
span: it.span,
|
||||
inner: Span::new(s.span.end + 1, it.span.end),
|
||||
});
|
||||
|
||||
// then we walk the arguments, but not the callee, since we want it to resolve to
|
||||
// the real eval
|
||||
walk::walk_arguments(self, &it.arguments);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if self.config.scramitize {
|
||||
self.scramitize(it.span);
|
||||
}
|
||||
walk::walk_call_expression(self, it);
|
||||
}
|
||||
|
||||
fn visit_import_declaration(&mut self, it: &ImportDeclaration<'data>) {
|
||||
let text = self.rewrite_url(it.source.value);
|
||||
self.jschanges.add(Rewrite::Replace {
|
||||
span: it.source.span.shrink(1),
|
||||
text,
|
||||
});
|
||||
walk::walk_import_declaration(self, it);
|
||||
}
|
||||
fn visit_import_expression(&mut self, it: &ImportExpression<'data>) {
|
||||
self.jschanges.add(Rewrite::ImportFn {
|
||||
span: Span::new(it.span.start, it.span.start + 7),
|
||||
});
|
||||
walk::walk_import_expression(self, it);
|
||||
}
|
||||
|
||||
fn visit_export_all_declaration(&mut self, it: &ExportAllDeclaration<'data>) {
|
||||
let text = self.rewrite_url(it.source.value);
|
||||
self.jschanges.add(Rewrite::Replace {
|
||||
span: it.source.span.shrink(1),
|
||||
text,
|
||||
});
|
||||
}
|
||||
fn visit_export_named_declaration(&mut self, it: &ExportNamedDeclaration<'data>) {
|
||||
if let Some(source) = &it.source {
|
||||
let text = self.rewrite_url(source.value);
|
||||
self.jschanges.add(Rewrite::Replace {
|
||||
span: source.span.shrink(1),
|
||||
text,
|
||||
});
|
||||
}
|
||||
// do not walk further, we don't want to rewrite the identifiers
|
||||
}
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
fn visit_try_statement(&mut self, it: &oxc::ast::ast::TryStatement<'data>) {
|
||||
// for debugging we need to know what the error was
|
||||
|
||||
if self.config.capture_errors {
|
||||
if let Some(h) = &it.handler {
|
||||
if let Some(name) = &h.param {
|
||||
if let Some(ident) = name.pattern.get_identifier_name() {
|
||||
self.jschanges.add(Rewrite::ScramErr {
|
||||
span: Span::new(h.body.span.start + 1, h.body.span.start + 1),
|
||||
ident,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
walk::walk_try_statement(self, it);
|
||||
}
|
||||
|
||||
fn visit_object_expression(&mut self, it: &ObjectExpression<'data>) {
|
||||
for prop in &it.properties {
|
||||
if let ObjectPropertyKind::ObjectProperty(p) = prop {
|
||||
if let Expression::Identifier(s) = &p.value {
|
||||
if UNSAFE_GLOBALS.contains(&s.name.to_string().as_str()) && p.shorthand {
|
||||
self.jschanges.add(Rewrite::ShorthandObj {
|
||||
span: s.span,
|
||||
name: s.name,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk::walk_object_expression(self, it);
|
||||
}
|
||||
|
||||
fn visit_function_body(&mut self, it: &FunctionBody<'data>) {
|
||||
// tag function for use in sourcemaps
|
||||
if self.config.do_sourcemaps {
|
||||
self.jschanges.add(Rewrite::SourceTag {
|
||||
span: Span::new(it.span.start, it.span.start),
|
||||
});
|
||||
}
|
||||
walk::walk_function_body(self, it);
|
||||
}
|
||||
|
||||
fn visit_return_statement(&mut self, it: &ReturnStatement<'data>) {
|
||||
// if let Some(arg) = &it.argument {
|
||||
// self.jschanges.insert(JsChange::GenericChange {
|
||||
// span: Span::new(it.span.start + 6, it.span.start + 6),
|
||||
// text: format!(" $scramdbg((()=>{{ try {{return arguments}} catch(_){{}} }})(),("),
|
||||
// });
|
||||
// self.jschanges.insert(JsChange::GenericChange {
|
||||
// span: Span::new(expression_span(arg).end, expression_span(arg).end),
|
||||
// text: format!("))"),
|
||||
// });
|
||||
// }
|
||||
walk::walk_return_statement(self, it);
|
||||
}
|
||||
|
||||
fn visit_unary_expression(&mut self, it: &UnaryExpression<'data>) {
|
||||
if matches!(it.operator, UnaryOperator::Typeof) {
|
||||
// don't walk to identifier rewrites since it won't matter
|
||||
return;
|
||||
}
|
||||
walk::walk_unary_expression(self, it);
|
||||
}
|
||||
|
||||
fn visit_update_expression(&mut self, _it: &UpdateExpression<'data>) {
|
||||
// then no, don't walk it, we don't care
|
||||
}
|
||||
|
||||
fn visit_meta_property(&mut self, it: &MetaProperty<'data>) {
|
||||
if it.meta.name == "import" {
|
||||
self.jschanges.add(Rewrite::MetaFn { span: it.span });
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_assignment_expression(&mut self, it: &AssignmentExpression<'data>) {
|
||||
match &it.left {
|
||||
AssignmentTarget::AssignmentTargetIdentifier(s) => {
|
||||
if ["location"].contains(&s.name.to_string().as_str()) {
|
||||
self.jschanges.add(Rewrite::Assignment {
|
||||
name: s.name,
|
||||
entirespan: it.span,
|
||||
rhsspan: it.right.span(),
|
||||
op: it.operator,
|
||||
});
|
||||
|
||||
// avoid walking rest of tree, i would need to figure out nested rewrites
|
||||
// somehow
|
||||
return;
|
||||
}
|
||||
}
|
||||
AssignmentTarget::ArrayAssignmentTarget(_) => {
|
||||
// [location] = ["https://example.com"]
|
||||
// this is such a ridiculously specific edge case. just ignore it
|
||||
return;
|
||||
}
|
||||
_ => {
|
||||
// only walk the left side if it isn't an identifier, we can't replace the
|
||||
// identifier with a function obviously
|
||||
walk::walk_assignment_target(self, &it.left);
|
||||
}
|
||||
}
|
||||
walk::walk_expression(self, &it.right);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue