From afaff8fa7d3ace1c4be229c138559293723d9671 Mon Sep 17 00:00:00 2001 From: Daniel Slapman Date: Tue, 6 Dec 2022 15:09:09 +0100 Subject: [PATCH 1/9] Rhino-based js sandbox --- backend/build.sbt | 1 + .../tcb/utils/sandboxing/CounterContext.scala | 15 ++++++ .../tcb/utils/sandboxing/Exceptions.scala | 5 ++ .../tcb/utils/sandboxing/RhinoJsSandbox.scala | 26 +++++++++ .../utils/sandboxing/SafeClassShutter.scala | 7 +++ .../utils/sandboxing/SafeContextFactory.scala | 53 +++++++++++++++++++ .../utils/sandboxing/RhinoJsSandboxSpec.scala | 49 +++++++++++++++++ 7 files changed, 156 insertions(+) create mode 100644 backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/CounterContext.scala create mode 100644 backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/Exceptions.scala create mode 100644 backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandbox.scala create mode 100644 backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeClassShutter.scala create mode 100644 backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeContextFactory.scala create mode 100644 backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandboxSpec.scala diff --git a/backend/build.sbt b/backend/build.sbt index 16ccb786..d74f56b1 100644 --- a/backend/build.sbt +++ b/backend/build.sbt @@ -76,6 +76,7 @@ val mockingbird = (project in file("mockingbird")) "com.github.geirolz" %% "advxml-core" % "2.5.1", "com.github.geirolz" %% "advxml-xpath" % "2.5.1", "io.estatico" %% "newtype" % "0.4.4", + "org.mozilla" % "rhino" % "1.7.14", "org.slf4j" % "slf4j-api" % "1.7.30" % Provided ), Compile / unmanagedResourceDirectories += file("../frontend/dist") diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/CounterContext.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/CounterContext.scala new file mode 100644 index 00000000..d41ea16e --- /dev/null +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/CounterContext.scala @@ -0,0 +1,15 @@ +package ru.tinkoff.tcb.utils.sandboxing + +import scala.concurrent.duration.Deadline + +import org.mozilla.javascript.Context +import org.mozilla.javascript.ContextFactory + +class CounterContext(factory: ContextFactory) extends Context(factory) { + var deadline: Deadline = Deadline.now + var instructions: Long = 0 +} + +object CounterContext { + final val InstructionSteps = 10000 +} diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/Exceptions.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/Exceptions.scala new file mode 100644 index 00000000..48dc7bd7 --- /dev/null +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/Exceptions.scala @@ -0,0 +1,5 @@ +package ru.tinkoff.tcb.utils.sandboxing + +class ScriptCPUAbuseException extends RuntimeException + +class ScriptDurationException extends RuntimeException diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandbox.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandbox.scala new file mode 100644 index 00000000..5d1a9e3a --- /dev/null +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandbox.scala @@ -0,0 +1,26 @@ +package ru.tinkoff.tcb.utils.sandboxing + +import scala.concurrent.duration.FiniteDuration +import scala.reflect.ClassTag +import scala.reflect.classTag + +import org.mozilla.javascript.Context + +class RhinoJsSandbox(maxRunTime: Option[FiniteDuration] = None, maxInstructions: Option[Long] = None) { + private val contextFactory = new SafeContextFactory(maxRunTime, maxInstructions) + + def eval[T: ClassTag](code: String, environment: Map[String, Any] = Map.empty): T = { + val ctx = contextFactory.enterContext() + + try { + val scope = ctx.initSafeStandardObjects() + for ((key, value) <- environment) + scope.put(key, scope, Context.javaToJS(value, scope, ctx)) + + val result = ctx.evaluateString(scope, code, "virtual", 1, null) + + Context.jsToJava(result, classTag[T].runtimeClass).asInstanceOf[T] + } finally + Context.exit() + } +} diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeClassShutter.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeClassShutter.scala new file mode 100644 index 00000000..8e660ee5 --- /dev/null +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeClassShutter.scala @@ -0,0 +1,7 @@ +package ru.tinkoff.tcb.utils.sandboxing + +import org.mozilla.javascript.ClassShutter + +object SafeClassShutter extends ClassShutter { + override def visibleToScripts(fullClassName: String): Boolean = fullClassName.startsWith("adapter") +} diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeContextFactory.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeContextFactory.scala new file mode 100644 index 00000000..2dedbd91 --- /dev/null +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeContextFactory.scala @@ -0,0 +1,53 @@ +package ru.tinkoff.tcb.utils.sandboxing + +import java.util.concurrent.TimeUnit +import scala.concurrent.duration.FiniteDuration + +import org.mozilla.javascript.Callable +import org.mozilla.javascript.Context +import org.mozilla.javascript.ContextFactory +import org.mozilla.javascript.Scriptable + +class SafeContextFactory(maxRunTime: Option[FiniteDuration], maxInstructions: Option[Long]) extends ContextFactory { + override def makeContext(): Context = + new CounterContext(this).tap { cc => + cc.setLanguageVersion(Context.VERSION_ES6) + cc.setOptimizationLevel(-1) + cc.setInstructionObserverThreshold(CounterContext.InstructionSteps) + cc.setClassShutter(SafeClassShutter) + } + + override def hasFeature(cx: Context, featureIndex: RuntimeFlags): Boolean = featureIndex match { + case Context.FEATURE_NON_ECMA_GET_YEAR | Context.FEATURE_MEMBER_EXPR_AS_FUNCTION_NAME | + Context.FEATURE_RESERVED_KEYWORD_AS_IDENTIFIER => + true + case Context.FEATURE_PARENT_PROTO_PROPERTIES => false + case _ => super.hasFeature(cx, featureIndex) + } + + override def observeInstructionCount(cx: Context, instructionCount: RuntimeFlags): Unit = { + val counter = cx.asInstanceOf[CounterContext] + + if (counter.deadline.isOverdue()) + throw new ScriptDurationException + + counter.instructions += CounterContext.InstructionSteps + if (maxInstructions.exists(_ <= counter.instructions)) + throw new ScriptCPUAbuseException + } + + override def doTopCall( + callable: Callable, + cx: Context, + scope: Scriptable, + thisObj: Scriptable, + args: Array[AnyRef] + ): AnyRef = { + val counter = cx.asInstanceOf[CounterContext] + counter.deadline = maxRunTime.fold(minute.fromNow)(fd => fd.fromNow) + counter.instructions = 0 + super.doTopCall(callable, cx, scope, thisObj, args) + } + + final private val minute = FiniteDuration(1, TimeUnit.MINUTES) +} diff --git a/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandboxSpec.scala b/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandboxSpec.scala new file mode 100644 index 00000000..9042d71a --- /dev/null +++ b/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandboxSpec.scala @@ -0,0 +1,49 @@ +package ru.tinkoff.tcb.utils.sandboxing + +import java.util.concurrent.TimeUnit +import scala.concurrent.duration.FiniteDuration + +import org.mozilla.javascript.EcmaError +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +class RhinoJsSandboxSpec extends AnyFunSuite with Matchers { + private val sandbox = new RhinoJsSandbox + + test("Eval simple arithmetics") { + sandbox.eval[Int]("1 + 2") shouldBe 3 + } + + test("Java classes are inaccessable") { + assertThrows[EcmaError] { + sandbox.eval[Int]("java.lang.System.out.println('hello');") + } + } + + test("Eval with context") { + sandbox.eval[Int]("a + b", Map("a" -> 1, "b" -> 2)) shouldBe 3 + } + + test("Run time limit test") { + val limitedSandbox = new RhinoJsSandbox(maxRunTime = Some(FiniteDuration.apply(1, TimeUnit.SECONDS))) + + assertThrows[ScriptDurationException] { + limitedSandbox.eval[Int]("while (true) { }") + } + } + + test("Instruction limit test") { + val limitedSandbox = new RhinoJsSandbox(maxInstructions = Some(100)) + + assertThrows[ScriptCPUAbuseException] { + limitedSandbox.eval[Int]("while (true) { }") + } + } + + test("Evaluations should not have shared data") { + sandbox.eval[Unit]("a = 42;") + assertThrows[EcmaError] { + sandbox.eval[Int]("a") + } + } +} From 8368949e7655f6bcbf80d03ed271f7d68e140d43 Mon Sep 17 00:00:00 2001 From: Daniel Slapman Date: Wed, 7 Dec 2022 17:57:04 +0100 Subject: [PATCH 2/9] Circe <-> Rhino conversions --- .../org/ringojs/util/CaseInsensitiveMap.java | 133 ++++ .../java/org/ringojs/util/DebuggerBase.java | 127 ++++ .../java/org/ringojs/util/ScriptUtils.java | 226 +++++++ .../java/org/ringojs/util/StackUtils.java | 39 ++ .../java/org/ringojs/util/StringUtils.java | 143 ++++ .../main/java/org/ringojs/util/package.html | 6 + .../java/org/ringojs/wrappers/Binary.java | 614 ++++++++++++++++++ .../org/ringojs/wrappers/ScriptableList.java | 219 +++++++ .../org/ringojs/wrappers/ScriptableMap.java | 224 +++++++ .../ringojs/wrappers/ScriptableWrapper.java | 90 +++ .../java/org/ringojs/wrappers/Storable.java | 268 ++++++++ .../java/org/ringojs/wrappers/Stream.java | 243 +++++++ .../java/org/ringojs/wrappers/package.html | 6 + .../tcb/utils/sandboxing/RhinoJsSandbox.scala | 10 +- .../utils/sandboxing/SafeClassShutter.scala | 12 +- .../utils/sandboxing/SafeContextFactory.scala | 6 +- .../utils/sandboxing/SandboxWrapFactory.scala | 22 + .../transformation/json/js_eval/package.scala | 28 + .../transformation/json/JsEvalSpec.scala | 58 ++ 19 files changed, 2467 insertions(+), 7 deletions(-) create mode 100644 backend/mockingbird/src/main/java/org/ringojs/util/CaseInsensitiveMap.java create mode 100644 backend/mockingbird/src/main/java/org/ringojs/util/DebuggerBase.java create mode 100644 backend/mockingbird/src/main/java/org/ringojs/util/ScriptUtils.java create mode 100644 backend/mockingbird/src/main/java/org/ringojs/util/StackUtils.java create mode 100644 backend/mockingbird/src/main/java/org/ringojs/util/StringUtils.java create mode 100644 backend/mockingbird/src/main/java/org/ringojs/util/package.html create mode 100644 backend/mockingbird/src/main/java/org/ringojs/wrappers/Binary.java create mode 100644 backend/mockingbird/src/main/java/org/ringojs/wrappers/ScriptableList.java create mode 100644 backend/mockingbird/src/main/java/org/ringojs/wrappers/ScriptableMap.java create mode 100644 backend/mockingbird/src/main/java/org/ringojs/wrappers/ScriptableWrapper.java create mode 100644 backend/mockingbird/src/main/java/org/ringojs/wrappers/Storable.java create mode 100644 backend/mockingbird/src/main/java/org/ringojs/wrappers/Stream.java create mode 100644 backend/mockingbird/src/main/java/org/ringojs/wrappers/package.html create mode 100644 backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SandboxWrapFactory.scala create mode 100644 backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/json/js_eval/package.scala create mode 100644 backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/json/JsEvalSpec.scala diff --git a/backend/mockingbird/src/main/java/org/ringojs/util/CaseInsensitiveMap.java b/backend/mockingbird/src/main/java/org/ringojs/util/CaseInsensitiveMap.java new file mode 100644 index 00000000..cd47a9da --- /dev/null +++ b/backend/mockingbird/src/main/java/org/ringojs/util/CaseInsensitiveMap.java @@ -0,0 +1,133 @@ +/* + * Copyright 2006 Hannes Wallnoefer + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ringojs.util; + +import java.util.Map; +import java.util.Set; +import java.util.Collection; +import java.util.HashMap; + +/** + * Map wrapper that makes a string-keyed map case insensitive while + * still preserving case in key collections. + */ +public class CaseInsensitiveMap implements Map { + + private final Map wrapped; + private final Map keymap; + + public CaseInsensitiveMap() { + wrapped = new HashMap<>(); + keymap = new HashMap<>(); + } + + public CaseInsensitiveMap(Map map) { + assert map != null; + wrapped = map; + keymap = new HashMap<>(); + for (String key: map.keySet()) { + keymap.put(processKey(key), key); + } + } + + public int size() { + return wrapped.size(); + } + + public boolean isEmpty() { + return wrapped.isEmpty(); + } + + public boolean containsKey(Object key) { + return keymap.containsKey(processKey(key)); + } + + public boolean containsValue(Object value) { + return wrapped.containsValue(value); + } + + public V get(Object key) { + key = keymap.get(processKey(key)); + return key == null ? null : wrapped.get(key); + } + + /** + * Puts a new key-value pair into the map. + * @param key key + * @param value value + * @return the old value, if an old value got replaced + */ + public V put(String key, V value) { + String pkey = processKey(key); + String previousKey = keymap.put(pkey, key); + V previousValue = wrapped.put(key, value); + if (previousValue == null && previousKey != null) + previousValue = wrapped.remove(previousKey); + return previousValue; + } + + public V remove(Object key) { + String pkey = processKey(key); + String previousKey = keymap.remove(pkey); + return previousKey == null ? null : wrapped.remove(previousKey); + } + + public void putAll(Map t) { + for (String key: t.keySet()) { + String previousKey = keymap.put(processKey(key), key); + if (previousKey != null) { + wrapped.remove(previousKey); + } + } + wrapped.putAll(t); + } + + public void clear() { + keymap.clear(); + wrapped.clear(); + } + + public Set keySet() { + return wrapped.keySet(); + } + + public Collection values() { + return wrapped.values(); + } + + public Set> entrySet() { + return wrapped.entrySet(); + } + + public String toString() { + return wrapped.toString(); + } + + public boolean equals(Object obj) { + return wrapped.equals(obj); + } + + public int hashCode() { + return wrapped.hashCode(); + } + + private String processKey(Object key) { + assert key != null; + return key instanceof String ? + ((String) key).toLowerCase() : key.toString().toLowerCase(); + } +} diff --git a/backend/mockingbird/src/main/java/org/ringojs/util/DebuggerBase.java b/backend/mockingbird/src/main/java/org/ringojs/util/DebuggerBase.java new file mode 100644 index 00000000..28b86371 --- /dev/null +++ b/backend/mockingbird/src/main/java/org/ringojs/util/DebuggerBase.java @@ -0,0 +1,127 @@ +package org.ringojs.util; + +import org.mozilla.javascript.Context; +import org.mozilla.javascript.Scriptable; +import org.mozilla.javascript.ContextFactory; +import org.mozilla.javascript.debug.DebugFrame; +import org.mozilla.javascript.debug.DebuggableScript; +import org.mozilla.javascript.debug.Debugger; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A base class for Debuggers and Profilers implemented in Javascript. + * This allows to exclude the debugger/profiler module and all modules + * it uses to be excluded from debugging/profiling. + */ +public abstract class DebuggerBase implements Debugger { + + String debuggerScript; + int debuggerScriptDepth = 0; + final Logger log = Logger.getLogger(DebuggerBase.class.getName()); + + public abstract DebuggerBase createDebugger(); + + public abstract Object createContextData(); + + public abstract void handleCompilationDone(Context cx, DebuggableScript fnOrScript, String source); + + public abstract DebugFrame getScriptFrame(Context cx, DebuggableScript fnOrScript); + + public void attach() { + attach(createContextData()); + } + + public void setDebuggerScript(String path) { + debuggerScript = path; + } + + public void install() { + ContextFactory factory = Context.getCurrentContext().getFactory(); + factory.addListener(new ContextFactory.Listener() { + public void contextCreated(Context cx) { + DebuggerBase debugger = createDebugger(); + if (debugger != null) { + debugger.attach(createContextData()); + } + } + public void contextReleased(Context cx) { + } + }); + } + + public void attach(Object contextData) { + Context cx = Context.getCurrentContext(); + cx.setDebugger(this, contextData); + cx.setOptimizationLevel(-1); + cx.setGeneratingDebug(true); + } + + public void detach() { + Context cx = Context.getCurrentContext(); + cx.setDebugger(null, null); + } + + public Object getContextData() { + return Context.getCurrentContext().getDebuggerContextData(); + } + + public synchronized void suspend() { + try { + wait(); + } catch (InterruptedException ir) { + Thread.currentThread().interrupt(); + } + } + + public synchronized void resume() { + notify(); + } + + public DebugFrame getFrame(Context cx, DebuggableScript fnOrScript) { + String path = fnOrScript.getSourceName(); + if (log.isLoggable(Level.FINE)) { + log.fine("Getting Frame for " + path + + ", debugger script depth is " + debuggerScriptDepth); + } + if (debuggerScriptDepth > 0 || path.equals(debuggerScript)) { + return new DebuggerScriptFrame(); + } else { + return getScriptFrame(cx, fnOrScript); + } + } + + /** + * Get a string representation for the given script + * @param script a function or script + * @return the file and/or function name of the script + */ + static String getScriptName(DebuggableScript script) { + if (script.isFunction()) { + return script.getSourceName() + ": " + script.getFunctionName(); + } else { + return script.getSourceName(); + } + } + + class DebuggerScriptFrame implements DebugFrame { + + public void onEnter(Context cx, Scriptable activation, Scriptable thisObj, Object[] args) { + log.fine("Entering debugger script frame"); + debuggerScriptDepth ++; + } + + public void onExit(Context cx, boolean byThrow, Object resultOrException) { + log.fine("Exiting debugger script frame"); + debuggerScriptDepth --; + } + + public void onLineChange(Context cx, int lineNumber) {} + + public void onExceptionThrown(Context cx, Throwable ex) {} + + public void onDebuggerStatement(Context cx) {} + } + +} diff --git a/backend/mockingbird/src/main/java/org/ringojs/util/ScriptUtils.java b/backend/mockingbird/src/main/java/org/ringojs/util/ScriptUtils.java new file mode 100644 index 00000000..641ce46b --- /dev/null +++ b/backend/mockingbird/src/main/java/org/ringojs/util/ScriptUtils.java @@ -0,0 +1,226 @@ +/* + * Copyright 2006 Hannes Wallnoefer + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ringojs.util; + +import org.mozilla.javascript.*; +import org.ringojs.wrappers.ScriptableMap; +import org.ringojs.wrappers.ScriptableList; + +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A collection of Rhino utility methods. + */ +public class ScriptUtils { + + /** + * Coerce/wrap a java object to a JS object, and mask Lists and Maps + * as native JS objects. + * @param obj the object to coerce/wrap + * @param scope the scope + * @return the wrapped/masked java object + */ + @SuppressWarnings("unchecked") + public static Object javaToJS(Object obj, Scriptable scope) { + if (obj instanceof Scriptable) { + if (obj instanceof ScriptableObject + && ((Scriptable) obj).getParentScope() == null + && ((Scriptable) obj).getPrototype() == null) { + ScriptRuntime.setObjectProtoAndParent((ScriptableObject) obj, scope); + } + return obj; + } else if (obj instanceof List) { + return new ScriptableList(scope, (List) obj); + } else if (obj instanceof Map) { + return new ScriptableMap(scope, (Map) obj); + } else { + return Context.javaToJS(obj, scope); + } + } + + /** + * Unwrap a JS object to a java object. This is much more conservative than + * Context.jsToJava in that it will preserve undefined values. + * @param obj a JavaScript value + * @return a Java object corresponding to obj + */ + public static Object jsToJava(Object obj) { + while (obj instanceof Wrapper) { + obj = ((Wrapper) obj).unwrap(); + } + return obj; + } + + /** + * Return a class prototype, or the object prototype if the class + * is not defined. + * @param scope the scope + * @param className the class name + * @return the class or object prototype + */ + public static Scriptable getClassOrObjectProto(Scriptable scope, String className) { + Scriptable proto = ScriptableObject.getClassPrototype(scope, className); + if (proto == null) { + proto = ScriptableObject.getObjectPrototype(scope); + } + return proto; + } + + /** + * Make sure that number of arguments is valid. + * @param args the argument array + * @param min the minimum number of arguments + * @param max the maximum number of arguments + * @throws IllegalArgumentException if the number of arguments is not valid + */ + public static void checkArguments(Object[] args, int min, int max) { + if (min > -1 && args.length < min) + throw new IllegalArgumentException(); + if (max > -1 && args.length > max) + throw new IllegalArgumentException(); + } + + /** + * Get an argument as ScriptableObject + * @param args the argument array + * @param pos the position of the requested argument + * @return the argument as ScriptableObject + * @throws IllegalArgumentException if the argument can't be converted to a map + */ + public static ScriptableObject getScriptableArgument(Object[] args, int pos, boolean allowNull) + throws IllegalArgumentException { + if (pos >= args.length || args[pos] == null || args[pos] == Undefined.instance) { + if (allowNull) return null; + throw ScriptRuntime.constructError("Error", "Argument " + (pos + 1) + " must not be null"); + } if (args[pos] instanceof ScriptableObject) { + return (ScriptableObject) args[pos]; + } + throw ScriptRuntime.constructError("Error", "Can't convert to ScriptableObject: " + args[pos]); + } + + /** + * Get an argument as string + * @param args the argument array + * @param pos the position of the requested argument + * @return the argument as string + */ + public static String getStringArgument(Object[] args, int pos, boolean allowNull) { + if (pos >= args.length || args[pos] == null || args[pos] == Undefined.instance) { + if (allowNull) return null; + throw ScriptRuntime.constructError("Error", "Argument " + (pos + 1) + " must not be null"); + } + return ScriptRuntime.toString(args[pos].toString()); + } + + /** + * Get an argument as Map + * @param args the argument array + * @param pos the position of the requested argument + * @return the argument as map + * @throws IllegalArgumentException if the argument can't be converted to a map + */ + public static Map getMapArgument(Object[] args, int pos, boolean allowNull) + throws IllegalArgumentException { + if (pos >= args.length || args[pos] == null || args[pos] == Undefined.instance) { + if (allowNull) return null; + throw ScriptRuntime.constructError("Error", "Argument " + (pos + 1) + " must not be null"); + } if (args[pos] instanceof Map) { + return (Map) args[pos]; + } + throw ScriptRuntime.constructError("Error", "Can't convert to java.util.Map: " + args[pos]); + } + + /** + * Get an argument as object + * @param args the argument array + * @param pos the position of the requested argument + * @return the argument as object + */ + public static Object getObjectArgument(Object[] args, int pos, boolean allowNull) { + if (pos >= args.length || args[pos] == null || args[pos] == Undefined.instance) { + if (allowNull) return null; + throw ScriptRuntime.constructError("Error", "Argument " + (pos + 1) + " must not be null"); + } + return args[pos]; + } + + /** + * Try to convert an object to an int value, returning the default value if conversion fails. + * @param obj the value + * @param defaultValue the default value + * @return the converted value + */ + public static int toInt(Object obj, int defaultValue) { + double d = ScriptRuntime.toNumber(obj); + if (d == ScriptRuntime.NaN || (int)d != d) { + return defaultValue; + } + return (int) d; + } + + + /** + * Get a snapshot of the current JavaScript evaluation state by creating + * an Error object and invoke the function on it passing along any arguments. + * Used to invoke console.trace() and friends because implementing this + * in JavaScript would mess with the evaluation state. + * @param function the function to call + * @param args optional arguments to pass to the function. + */ + public static void traceHelper(Function function, Object... args) { + Context cx = Context.getCurrentContext(); + Scriptable scope = ScriptableObject.getTopLevelScope(function); + EcmaError error = ScriptRuntime.constructError("Trace", ""); + WrapFactory wrapFactory = cx.getWrapFactory(); + Scriptable thisObj = wrapFactory.wrapAsJavaObject(cx, scope, error, null); + for (int i = 0; i < args.length; i++) { + args[i] = wrapFactory.wrap(cx, scope, args[i], null); + } + function.call(cx, scope, thisObj, args); + } + + /** + * Helper for console.assert(). Implemented in Java in order not to + * modify the JavaScript stack. + * @param condition the condition to test + * @param args one or more message parts + */ + public static void assertHelper(Object condition, Object... args) { + if (ScriptRuntime.toBoolean(condition)) { + return; + } + // assertion failed + String msg = ""; + if (args.length > 0) { + msg = ScriptRuntime.toString(args[0]); + Pattern pattern = Pattern.compile("%[sdifo]"); + for (int i = 1; i < args.length; i++) { + Matcher matcher = pattern.matcher(msg); + if (matcher.find()) { + msg = matcher.replaceFirst(ScriptRuntime.toString(args[i])); + } else { + msg = msg + " " + ScriptRuntime.toString(args[i]); + } + } + } + throw ScriptRuntime.constructError("AssertionError", msg); + } + +} diff --git a/backend/mockingbird/src/main/java/org/ringojs/util/StackUtils.java b/backend/mockingbird/src/main/java/org/ringojs/util/StackUtils.java new file mode 100644 index 00000000..eba7121b --- /dev/null +++ b/backend/mockingbird/src/main/java/org/ringojs/util/StackUtils.java @@ -0,0 +1,39 @@ +/* + * Copyright 2006 Hannes Wallnoefer + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ringojs.util; + +import java.util.List; +import java.util.ArrayList; + +/** + * Utility class to extract pure JavaScript stack trace from Java exceptions + */ +public class StackUtils { + + public static StackTraceElement[] getJavaScriptStack(Throwable t) { + List list = new ArrayList<>(); + StackTraceElement[] stack = t.getStackTrace(); + for (StackTraceElement e: stack) { + String name = e.getFileName(); + if (e.getLineNumber() > -1 && + (name.endsWith(".js") || name.endsWith(".hac"))) { + list.add(e); + } + } + return list.toArray(new StackTraceElement[list.size()]); + } +} diff --git a/backend/mockingbird/src/main/java/org/ringojs/util/StringUtils.java b/backend/mockingbird/src/main/java/org/ringojs/util/StringUtils.java new file mode 100644 index 00000000..5e62ef07 --- /dev/null +++ b/backend/mockingbird/src/main/java/org/ringojs/util/StringUtils.java @@ -0,0 +1,143 @@ +/* + * Helma License Notice + * + * The contents of this file are subject to the Helma License + * Version 2.0 (the "License"). You may not use this file except in + * compliance with the License. A copy of the License is available at + * http://adele.invoker.org/download/invoker/license.txt + * + * Copyright 2005 Hannes Wallnoefer. All Rights Reserved. + */ + +package org.ringojs.util; + + +import java.nio.CharBuffer; +import java.util.StringTokenizer; + +/** + * Utility class for String manipulation. + */ +public class StringUtils { + + + /** + * Split a string into an array of strings. Use comma and space + * as delimiters. + * @param str the string to split + * @return the string split into a string array + */ + public static String[] split(String str) { + return split(str, ", \t\n\r\f"); + } + + /** + * Split a string into an array of strings. + * @param str the string to split + * @param delim the delimiter to split the string at + * @return the string split into a string array + */ + public static String[] split(String str, String delim) { + if (str == null) { + return new String[0]; + } + StringTokenizer st = new StringTokenizer(str, delim); + String[] s = new String[st.countTokens()]; + for (int i=0; i", ">") + .replaceAll("\"", """); + } + + /** + * Split a string and try to convert to an array of classes. + * @param str a string containint class names + * @param delim the delimiter + * @return an array of classes + * @throws ClassNotFoundException if any class name contained in the string + * couldn't be resolved + */ + public static Class[] toClassArray(String str, String delim) + throws ClassNotFoundException { + String[] s = split(str, delim); + Class[] classes = new Class[s.length]; + for (int i=0; i= to) { + return -1; + } + char[] chars = buffer.array(); + for (int i = from; i < to; i++) { + if (chars[i] == '\n' || (chars[i] == '\r' && i < to - 1)) { + return i; + } + } + return -1; + } + +} diff --git a/backend/mockingbird/src/main/java/org/ringojs/util/package.html b/backend/mockingbird/src/main/java/org/ringojs/util/package.html new file mode 100644 index 00000000..709110ff --- /dev/null +++ b/backend/mockingbird/src/main/java/org/ringojs/util/package.html @@ -0,0 +1,6 @@ + + + +Various utility classes. + + diff --git a/backend/mockingbird/src/main/java/org/ringojs/wrappers/Binary.java b/backend/mockingbird/src/main/java/org/ringojs/wrappers/Binary.java new file mode 100644 index 00000000..bf33ea02 --- /dev/null +++ b/backend/mockingbird/src/main/java/org/ringojs/wrappers/Binary.java @@ -0,0 +1,614 @@ +package org.ringojs.wrappers; + +import org.mozilla.javascript.*; +import org.mozilla.javascript.annotations.JSFunction; +import org.mozilla.javascript.annotations.JSGetter; +import org.mozilla.javascript.annotations.JSSetter; +import org.mozilla.javascript.annotations.JSConstructor; +import org.ringojs.util.ScriptUtils; + +import java.io.UnsupportedEncodingException; +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.lang.reflect.Method; + +/** + *

A wrapper around a Java byte array compliant to the Binary/ByteArray/ByteString + * classes defined in the Binary/B proposal. + * To register Binary, ByteArray and ByteString as a host objects in Rhino call the + * defineClass() function with this class as argument.

+ * + *
defineClass(org.ringojs.wrappers.Binary);
+ * + * The JavaScript Binary class serves as common base class for ByteArray and ByteString + * and can't be instantiated. ByteArray implements a modifiable and resizable byte buffer, + * while ByteString implements an immutable byte sequence. The ByteArray and ByteString + * constructors can take several arguments. Have a look at the proposal for details. + * + * When passed to a Java method that expects a byte array, instances of thes class + * are automatically unwrapped. + */ +public class Binary extends ScriptableObject implements Wrapper { + + private byte[] bytes; + private int length; + private final Type type; + + enum Type { + Binary, ByteArray, ByteString + } + + public Binary() { + type = Type.Binary; + } + + public Binary(Type type) { + this.type = type; + } + + public Binary(Scriptable scope, Type type, int length) { + super(scope, ScriptUtils.getClassOrObjectProto(scope, type.toString())); + this.type = type; + this.bytes = new byte[Math.max(length, 8)]; + this.length = length; + } + + public Binary(Scriptable scope, Type type, byte[] bytes) { + this(scope, type, bytes, 0, bytes.length); + } + + public Binary(Scriptable scope, Type type, byte[] bytes, int offset, int length) { + super(scope, ScriptUtils.getClassOrObjectProto(scope, type.toString())); + this.bytes = new byte[length]; + this.length = length; + this.type = type; + System.arraycopy(bytes, offset, this.bytes, 0, length); + } + + @JSConstructor + public static Object construct(Context cx, Object[] args, Function ctorObj, boolean inNewExpr) { + ScriptUtils.checkArguments(args, 0, 2); + Scriptable scope = ctorObj.getParentScope(); + Type type = Type.valueOf((String) ctorObj.get("name", ctorObj)); + if (type == Type.Binary) { + throw ScriptRuntime.constructError("Error", "cannot instantiate Binary base class"); + } + if (args.length == 0) { + return new Binary(scope, type, 0); + } + Object arg = args[0]; + if (arg instanceof Wrapper) { + arg = ((Wrapper) arg).unwrap(); + } + if (args.length == 2) { + if (!(arg instanceof String)) { + throw ScriptRuntime.constructError("Error", "Expected string as first argument"); + } else if (!(args[1] instanceof String)) { + throw ScriptRuntime.constructError("Error", "Expected string as second argument"); + } + try { + return new Binary(scope, type, ((String) arg).getBytes((String) args[1])); + } catch (UnsupportedEncodingException uee) { + throw ScriptRuntime.constructError("Error", "Unsupported encoding: " + args[1]); + } + } else if (arg instanceof Number && type == Type.ByteArray) { + return new Binary(scope, type, ((Number) arg).intValue()); + } else if (ScriptRuntime.isArrayObject(arg)) { + Scriptable object = (Scriptable) arg; + long longLength = ScriptRuntime.toUint32( + ScriptRuntime.getObjectProp(object, "length", cx)); + if (longLength > Integer.MAX_VALUE) { + throw new IllegalArgumentException(); + } + int length = (int) longLength; + Binary bytes = new Binary(scope, type, length); + for (int i = 0; i < length; i++) { + Object value = ScriptableObject.getProperty(object, i); + bytes.putInternal(i, value); + } + return bytes; + } else if (arg instanceof byte[]) { + return new Binary(scope, type, (byte[]) arg); + } else if (arg instanceof Binary) { + return new Binary(scope, type, ((Binary) arg).getBytes()); + } else if (arg == Undefined.instance) { + return new Binary(scope, type, 0); + } else { + throw ScriptRuntime.constructError("Error", "Unsupported argument: " + arg); + } + } + + // Called after the host class has been defined. + public static void finishInit(Scriptable scope, FunctionObject ctor, Scriptable prototype) + throws NoSuchMethodException{ + initClass(scope, prototype, Type.ByteArray); + initClass(scope, prototype, Type.ByteString); + } + + private static void initClass(Scriptable scope, Scriptable parentProto, Type type) + throws NoSuchMethodException { + Binary prototype = new Binary(type); + prototype.setPrototype(parentProto); + Method ctorMember = Binary.class.getMethod("construct", Context.class, Object[].class, Function.class, Boolean.TYPE); + FunctionObject constructor = new FunctionObject(type.toString(), ctorMember, scope); + constructor.addAsConstructor(scope, prototype); + constructor.defineProperty("wrap", new Wrap(scope, prototype, type), + DONTENUM | READONLY | PERMANENT); + } + + public Type getType() { + return type; + } + + @Override + public Object get(int index, Scriptable start) { + if (index < 0 || index >= length) { + return Undefined.instance; + } + return Integer.valueOf(0xff & bytes[index]); + } + + @Override + public boolean has(int index, Scriptable start) { + return index >= 0 && index < length; + } + + @Override + public void put(int index, Scriptable start, Object value) { + if (type != Type.ByteArray) { + return; + } + putInternal(index, value); + } + + private void putInternal(int index, Object value) { + if (index < 0) { + throw ScriptRuntime.constructError("Error", "Negative ByteArray index"); + } + if (!(value instanceof Number)) { + throw ScriptRuntime.constructError("Error", "Non-numeric ByteArray member: " + value); + } + if (index >= length) { + setLength(index + 1); + } + int n = ((Number) value).intValue(); + bytes[index] = (byte) (0xff & n); + } + + @JSGetter + public int getLength() { + return length; + } + + @JSSetter + public synchronized void setLength(Object length) { + int l = ScriptUtils.toInt(length, -1); + if (l < 0) { + throw ScriptRuntime.constructError("Error", "Inappropriate ByteArray length"); + } + setLength(l); + } + + protected synchronized void setLength(int newLength) { + if (type != Type.ByteArray) { + return; + } + if (newLength < length) { + // if shrinking clear the old buffer + Arrays.fill(bytes, newLength, length, (byte) 0); + } else if (newLength > bytes.length) { + // if growing make sure the buffer is large enough + int newSize = Math.max(newLength, bytes.length * 2); + byte[] b = new byte[newSize]; + System.arraycopy(bytes, 0, b, 0, length); + bytes = b; + } + length = newLength; + } + + @JSFunction + public Object get(Object index) { + int i = ScriptUtils.toInt(index, -1); + if (i < 0 || i >= length) { + return Undefined.instance; + } + return Integer.valueOf(0xff & bytes[i]); + } + + @JSFunction + public Object charCodeAt(Object index) { + return get(index); + } + + @JSFunction + public Object byteAt(Object index) { + int i = ScriptUtils.toInt(index, -1); + if (i < 0 || i >= length) { + return new Binary(getParentScope(), type, 0); + } + return new Binary(getParentScope(), type, new byte[] {bytes[i]}); + } + + @JSFunction + public Object charAt(Object index) { + return byteAt(index); + } + + @JSFunction + public void set(Object index, int value) { + if (type != Type.ByteArray) { + return; + } + int i = ScriptUtils.toInt(index, -1); + if (i > -1) { + if (i >= length) { + setLength(i + 1); + } + bytes[i] = (byte) (0xff & value); + } + } + + @JSFunction + public Object toByteArray(Object sourceCharset, Object targetCharset) + throws UnsupportedEncodingException { + return makeCopy(Type.ByteArray, sourceCharset, targetCharset); + } + + @JSFunction + public Object toByteString(Object sourceCharset, Object targetCharset) + throws UnsupportedEncodingException { + // no need to make a copy of an unmodifiable ByteString + if (type == Type.ByteString && + sourceCharset == Undefined.instance && + targetCharset == Undefined.instance) { + return this; + } + return makeCopy(Type.ByteString, sourceCharset, targetCharset); + } + + private synchronized Binary makeCopy(Type targetType, Object sourceCharset, + Object targetCharset) + throws UnsupportedEncodingException { + String source = toCharset(sourceCharset); + String target = toCharset(targetCharset); + if (source != null && target != null) { + String str = new String(bytes, 0, length, source); + return new Binary(getParentScope(), targetType, str.getBytes(target)); + } + if (this.type == Type.ByteString && this.type == targetType) { + return this; + } + return new Binary(getParentScope(), targetType, bytes, 0, length); + } + + @JSFunction + public void copy(int srcStartIndex, int srcEndIndex, Binary target, Object targetIndex) { + if (target.type != Type.ByteArray) { + throw ScriptRuntime.constructError("Error", "Target object is not writable"); + } else if (srcStartIndex < 0 || srcStartIndex >= length) { + throw ScriptRuntime.constructError("Error", "Invalid start index: " + srcStartIndex); + } else if (srcEndIndex < srcStartIndex || srcEndIndex > length) { + throw ScriptRuntime.constructError("Error", "Invalid end index: " + srcEndIndex); + } + int targetIdx = ScriptUtils.toInt(targetIndex, 0); + if (targetIdx < 0) { + throw ScriptRuntime.constructError("Error", "Invalid target index: " + targetIndex); + } + int size = srcEndIndex - srcStartIndex; + target.copyFrom(bytes, srcStartIndex, size, targetIdx); + + } + + private synchronized void copyFrom(byte[] source, int srcIndex, int length, int targetIndex) { + ensureLength(targetIndex + length); + System.arraycopy(source, srcIndex, bytes, targetIndex, length); + } + + @JSFunction + public synchronized Object toArray(Object charset) + throws UnsupportedEncodingException { + Object[] elements; + String cs = toCharset(charset); + if (cs != null) { + String str = new String(bytes, 0, length, cs); + elements = new Object[str.length()]; + for (int i = 0; i < elements.length; i++) { + elements[i] = Integer.valueOf(str.charAt(i)); + } + } else { + elements = new Object[length]; + for (int i = 0; i < length; i++) { + elements[i] = Integer.valueOf(0xff & bytes[i]); + } + } + return Context.getCurrentContext().newArray(getParentScope(), elements); + } + + @Override + @JSFunction + public String toString() { + if (bytes != null) { + return "[" + type.toString() + " " + length + "]"; + } + return "[object " + type.toString() + "]"; + } + + /* @JSFunction + public String toString(Object encoding) { + return encoding == Undefined.instance ? + toString() : decodeToString(encoding); + } */ + + @JSFunction + public Object slice(Object begin, Object end) { + if (begin == Undefined.instance && end == Undefined.instance) { + return new Binary(getParentScope(), type, bytes, 0, length); + } + int from = ScriptUtils.toInt(begin, 0); + if (from < 0) { + from += length; + } + from = Math.min(length, Math.max(0, from)); + int to = end == Undefined.instance ? length : ScriptUtils.toInt(end, from); + if (to < 0) { + to += length; + } + int len = Math.max(0, Math.min(length - from, to - from)); + return new Binary(getParentScope(), type, bytes, from, len); + } + + @JSFunction + public static Object concat(Context cx, Scriptable thisObj, + Object[] args, Function func) { + int arglength = 0; + List arglist = new ArrayList(args.length); + for (Object arg : args) { + if (arg instanceof Binary) { + byte[] bytes = ((Binary) arg).getBytes(); + arglength += bytes.length; + arglist.add(bytes); + } else if (ScriptRuntime.isArrayObject(arg)) { + Scriptable object = (Scriptable) arg; + long longLength = ScriptRuntime.toUint32( + ScriptRuntime.getObjectProp(object, "length", cx)); + if (longLength > Integer.MAX_VALUE) { + throw new IllegalArgumentException(); + } + int length = (int) longLength; + byte[] bytes = new byte[length]; + for (int i = 0; i < length; i++) { + Object value = ScriptableObject.getProperty(object, i); + if (!(value instanceof Number)) { + throw ScriptRuntime.constructError("Error", "Non-numeric ByteArray member: " + value); + } + int n = ((Number) value).intValue(); + bytes[i] = (byte) (0xff & n); + } + arglength += bytes.length; + arglist.add(bytes); + } else { + throw ScriptRuntime.constructError("Error", "Unsupported argument: " + arg); + } + } + Binary thisByteArray = (Binary) thisObj; + synchronized (thisByteArray) { + byte[] newBytes = new byte[thisByteArray.length + arglength]; + System.arraycopy(thisByteArray.bytes, 0, newBytes, 0, thisByteArray.length); + int index = thisByteArray.length; + for (byte[] b : arglist) { + System.arraycopy(b, 0, newBytes, index, b.length); + index += b.length; + } + return new Binary(thisObj.getParentScope(), thisByteArray.type, newBytes); + } + } + + @JSFunction + public String decodeToString(Object charset) { + String cs = toCharset(charset); + try { + return cs == null ? + new String(bytes, 0, length) : + new String(bytes, 0, length, cs); + } catch (UnsupportedEncodingException uee) { + throw ScriptRuntime.constructError("Error", "Unsupported encoding: " + charset); + } + } + + @JSFunction + public int indexOf(Object arg, Object from, Object to) { + byte[] b = getBytesArgument(arg); + int start = Math.max(0, Math.min(length - 1, ScriptUtils.toInt(from, 0))); + int end = Math.max(0, Math.min(length - b.length + 1, ScriptUtils.toInt(to, length))); + outer: + for (int i = start; i < end; i++) { + for (int j = 0; j < b.length; j++) { + if (bytes[i + j] != b[j]) { + continue outer; + } + } + return i; + } + return -1; + } + + @JSFunction + public int lastIndexOf(Object arg, Object from, Object to) { + byte[] b = getBytesArgument(arg); + int start = Math.max(0, Math.min(length - 1, ScriptUtils.toInt(from, 0))); + int end = Math.max(0, Math.min(length - b.length + 1, ScriptUtils.toInt(to, length))); + outer: + for (int i = end - 1; i >= start; i--) { + for (int j = 0; j < b.length; j++) { + if (bytes[i + j] != b[j]) { + continue outer; + } + } + return i; + } + return -1; + } + + @JSFunction + public synchronized Object split(Object delim, Object options) { + byte[][] delimiters = getSplitDelimiters(delim); + boolean includeDelimiter = false; + int count = Integer.MAX_VALUE; + if (options instanceof Scriptable) { + Scriptable o = (Scriptable) options; + Object include = ScriptableObject.getProperty(o, "includeDelimiter"); + includeDelimiter = include != NOT_FOUND && ScriptRuntime.toBoolean(include); + Object max = ScriptableObject.getProperty(o, "count"); + if (max != NOT_FOUND) count = ScriptRuntime.toInt32(max); + } + List list = new ArrayList(); + Scriptable scope = getParentScope(); + int index = 0; + int found = 0; + outer: + for (int i = 0; i < length && found < count - 1; i++) { + inner: + for (byte[] delimiter : delimiters) { + if (i + delimiter.length > length) { + continue; + } + for (int j = 0; j < delimiter.length; j++) { + if (bytes[i + j] != delimiter[j]) { + continue inner; + } + } + list.add(new Binary(scope, type, bytes, index, i - index)); + if (includeDelimiter) { + list.add(new Binary(scope, type, delimiter)); + } + index = i + delimiter.length; + i = index - 1; + found++; + continue outer; + } + } + if (index == 0) { + list.add(this); + } else { + list.add(new Binary(scope, type, bytes, index, length - index)); + } + return Context.getCurrentContext().newArray(scope, list.toArray()); + } + + protected static Binary wrap(Type type, byte[] bytes, Scriptable scope, + Scriptable prototype) { + Binary wrapper = new Binary(type); + wrapper.bytes = bytes; + wrapper.length = bytes.length; + wrapper.setParentScope(scope); + wrapper.setPrototype(prototype); + return wrapper; + } + + /** + * Static method that wraps a java byte array without copying. + */ + static class Wrap extends BaseFunction { + final Type type; + final Scriptable prototype; + + Wrap(Scriptable scope, Scriptable prototype, Type type) { + super(scope, ScriptableObject.getFunctionPrototype(scope)); + this.type = type; + this.prototype = prototype; + } + + @Override + public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + Object arg = ScriptUtils.getObjectArgument(args, 0, false); + if (arg instanceof Wrapper) { + arg = ((Wrapper) arg).unwrap(); + } + if (!(arg instanceof byte[])) { + throw ScriptRuntime.constructError("Error", "wrap() requires an argument of type byte[]"); + } + byte[] bytes = (byte[]) arg; + return wrap(type, bytes, getTopLevelScope(scope), prototype); + } + } + + @JSFunction("unwrap") + public Object jsunwrap() { + return NativeJavaArray.wrap(getParentScope(), getBytes()); + } + + /** + * Unwrap the object by returning the wrapped value. + * + * @return a wrapped value + */ + public Object unwrap() { + return getBytes(); + } + + public byte[] getBytes() { + normalize(); + return bytes; + } + + public String getClassName() { + return type.toString(); + } + + protected synchronized void ensureLength(int minLength) { + if (minLength > length) { + setLength(minLength); + } + } + + private synchronized void normalize() { + if (length != bytes.length) { + byte[] b = new byte[length]; + System.arraycopy(bytes, 0, b, 0, length); + bytes = b; + } + } + + private byte[] getBytesArgument(Object arg) { + if (arg instanceof Number) { + return new byte[] {(byte) (0xff & ((Number) arg).intValue())}; + } else if (arg instanceof Binary) { + return ((Binary) arg).getBytes(); + } else { + throw new RuntimeException("unsupported delimiter: " + arg); + } + } + + private byte[][] getSplitDelimiters(Object delim) { + List list = new ArrayList(); + Collection values = null; + if (delim instanceof Collection) { + values = (Collection) delim; + } else if (delim instanceof Map && delim instanceof NativeArray) { + // NativeArray used to implement java.util.Map + values = ((Map) delim).values(); + } + if (values != null) { + for (Object value : values) { + list.add(getBytesArgument(value)); + } + } else { + list.add(getBytesArgument(delim)); + } + return list.toArray(new byte[list.size()][]); + } + + private String toCharset(Object charset) { + if (charset == Undefined.instance || charset == null) { + return null; + } + if (!(charset instanceof String)) { + throw ScriptRuntime.constructError("Error", + "Charset is not a string: " + charset); + } + return (String) charset; + } +} diff --git a/backend/mockingbird/src/main/java/org/ringojs/wrappers/ScriptableList.java b/backend/mockingbird/src/main/java/org/ringojs/wrappers/ScriptableList.java new file mode 100644 index 00000000..afac79a3 --- /dev/null +++ b/backend/mockingbird/src/main/java/org/ringojs/wrappers/ScriptableList.java @@ -0,0 +1,219 @@ +/* + * Copyright 2006 Hannes Wallnoefer + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ringojs.wrappers; + +import org.mozilla.javascript.*; +import org.ringojs.util.ScriptUtils; + +import java.util.List; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; + +/** + * ScriptableList is a wrapper for java.util.List instances that allows developers + * to interact with them like it was a native JavaScript array. + */ +public class ScriptableList extends NativeJavaObject { + + List list; + static final String CLASSNAME = "ScriptableList"; + + // Set up a custom constructor, for this class is somewhere between a host class and + // a native wrapper, for which no standard constructor class exists + public static void init(Scriptable scope) throws NoSuchMethodException { + BaseFunction ctor = new BaseFunction(scope, ScriptableObject.getFunctionPrototype(scope)) { + @Override + public Scriptable construct(Context cx, Scriptable scope, Object[] args) { + if (args.length > 1) { + throw new EvaluatorException("ScriptableList() requires a java.util.List argument"); + } + return new ScriptableList(scope, args.length == 0 ? null : args[0]); + } + @Override + public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + return construct(cx, scope, args); + } + }; + ScriptableObject.defineProperty(scope, CLASSNAME, ctor, + ScriptableObject.DONTENUM | ScriptableObject.READONLY); + } + + /** + * Create a ScriptableList wrapper around a java.util.List + * @param scope the scope + * @param obj the list, possibly wrapped + */ + @SuppressWarnings("unchecked") + private ScriptableList(Scriptable scope, Object obj) { + this.parent = scope; + if (obj instanceof Wrapper) { + obj = ((Wrapper) obj).unwrap(); + } + if (obj instanceof List) { + this.javaObject = this.list = (List) obj; + } else if (obj instanceof Collection) { + this.javaObject = this.list = new ArrayList<>((Collection) obj); + } else if (obj instanceof Map) { + this.javaObject = this.list = new ArrayList<>(((Map) obj).values()); + } else if (obj == null || obj == Undefined.instance) { + this.javaObject = this.list = new ArrayList<>(); + } else { + throw new EvaluatorException("Invalid argument to ScriptableList(): " + obj); + } + this.staticType = this.list.getClass(); + initMembers(); + initPrototype(scope); + } + + + /** + * Create a ScriptableList wrapper around a java.util.List. + * @param scope the scope + * @param list the list instance + */ + @SuppressWarnings("unchecked") + public ScriptableList(Scriptable scope, List list) { + super(scope, list, list.getClass()); + this.list = list; + initPrototype(scope); + } + + /** + * Set the prototype to the Array prototype so we can use array methds such as + * push, pop, shift, slice etc. + * @param scope the global scope for looking up the Array constructor + */ + protected void initPrototype(Scriptable scope) { + Scriptable arrayProto = ScriptableObject.getClassPrototype(scope, "Array"); + if (arrayProto != null) { + this.setPrototype(arrayProto); + } + } + + public void delete(int index) { + if (list != null) { + try { + list.remove(index); + } catch (RuntimeException e) { + throw Context.throwAsScriptRuntimeEx(e); + } + } else { + super.delete(index); + } + } + + public Object get(int index, Scriptable start) { + if (list == null) + return super.get(index, start); + try { + if (index < 0 || index >= list.size()) { + return Undefined.instance; + } else { + return ScriptUtils.javaToJS(list.get(index), getParentScope()); + } + } catch (RuntimeException e) { + throw Context.throwAsScriptRuntimeEx(e); + } + } + + public boolean has(int index, Scriptable start) { + if (list == null) + return super.has(index, start); + return index >= 0 && index < list.size(); + } + + public void put(String name, Scriptable start, Object value) { + if (list != null && "length".equals(name)) { + double d = ScriptRuntime.toNumber(value); + long longVal = ScriptRuntime.toUint32(d); + if (longVal != d) { + String msg = ScriptRuntime.getMessageById("msg.arraylength.bad"); + throw ScriptRuntime.constructError("RangeError", msg); + } + int size = list.size(); + if (longVal > size) { + for (int i = size; i < longVal; i++) { + // push nulls as undefined is probably meaningless to java code + list.add(null); + } + } else if (longVal < size) { + for (int i = size - 1; i >= longVal; i--) { + list.remove(i); + } + } + } else { + super.put(name, start, value); + } + } + + public void put(int index, Scriptable start, Object value) { + if (list != null) { + try { + if (index == list.size()) { + list.add(ScriptUtils.jsToJava(value)); + } else { + list.set(index, ScriptUtils.jsToJava(value)); + } + } catch (RuntimeException e) { + Context.throwAsScriptRuntimeEx(e); + } + } else { + super.put(index, start, value); + } + } + + public Object get(String name, Scriptable start) { + if ("length".equals(name) && list != null) { + return list.size(); + } + return super.get(name, start); + } + + public Object[] getIds() { + if (list == null) + return super.getIds(); + int size = list.size(); + Object[] ids = new Object[size]; + for (int i = 0; i < size; ++i) { + ids[i] = i; + } + return ids; + } + + public String toString() { + if (list == null) + return super.toString(); + return list.toString(); + } + + public Object getDefaultValue(Class typeHint) { + return toString(); + } + + public Object unwrap() { + return list; + } + + public List getList() { + return list; + } + + public String getClassName() { + return CLASSNAME; + } +} diff --git a/backend/mockingbird/src/main/java/org/ringojs/wrappers/ScriptableMap.java b/backend/mockingbird/src/main/java/org/ringojs/wrappers/ScriptableMap.java new file mode 100644 index 00000000..fdaaf41b --- /dev/null +++ b/backend/mockingbird/src/main/java/org/ringojs/wrappers/ScriptableMap.java @@ -0,0 +1,224 @@ +/* + * Copyright 2006 Hannes Wallnoefer + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ringojs.wrappers; + +import org.mozilla.javascript.*; +import org.ringojs.util.ScriptUtils; + +import java.util.Map; +import java.util.HashMap; + +/** + * ScriptableMap is a wrapper for java.util.Map instances that allows developers + * to interact with them as if it were a native JavaScript object. + */ +public class ScriptableMap extends NativeJavaObject { + + boolean reflect; + Map map; + final static String CLASSNAME = "ScriptableMap"; + + // Set up a custom constructor, for this class is somewhere between a host class and + // a native wrapper, for which no standard constructor class exists + public static void init(Scriptable scope) throws NoSuchMethodException { + BaseFunction ctor = new BaseFunction(scope, ScriptableObject.getFunctionPrototype(scope)) { + @Override + public Scriptable construct(Context cx, Scriptable scope, Object[] args) { + boolean reflect = false; + if (args.length > 2) { + throw new EvaluatorException("ScriptableMap() called with too many arguments"); + } if (args.length == 2) { + reflect = ScriptRuntime.toBoolean(args[1]); + } + return new ScriptableMap(scope, args.length == 0 ? null : args[0], reflect); + } + @Override + public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + return construct(cx, scope, args); + } + }; + ScriptableObject.defineProperty(scope, CLASSNAME, ctor, + ScriptableObject.DONTENUM | ScriptableObject.READONLY); + } + + @SuppressWarnings("unchecked") + private ScriptableMap(Scriptable scope, Object obj, boolean reflect) { + this.parent = scope; + this.reflect = reflect; + if (obj instanceof Wrapper) { + obj = ((Wrapper) obj).unwrap(); + } + if (obj instanceof Map) { + this.map = (Map) obj; + } else if (obj == null || obj == Undefined.instance) { + this.map = new HashMap(); + } else if (obj instanceof Scriptable) { + this.map = new HashMap(); + Scriptable s = (Scriptable) obj; + Object[] ids = s.getIds(); + for (Object id: ids) { + if (id instanceof String) { + map.put(id, s.get((String)id, s)); + } else if (id instanceof Number) { + map.put(id, s.get(((Number)id).intValue(), s)); + } + } + } else { + throw new EvaluatorException("Invalid argument to ScriptableMap(): " + obj); + } + this.javaObject = this.map; + this.staticType = this.map.getClass(); + initMembers(); + initPrototype(scope); + + } + + public ScriptableMap(Scriptable scope, Map map) { + super(scope, map, map.getClass()); + this.map = map; + initPrototype(scope); + } + + /** + * Set the prototype to the Object prototype so we can use object methods such as + * getOwnPropertyNames, hasOwnProperty, keys etc. + * @param scope the global scope for looking up the Object constructor + */ + protected void initPrototype(Scriptable scope) { + Scriptable objectProto = ScriptableObject.getClassPrototype(scope, "Object"); + if (objectProto != null) { + this.setPrototype(objectProto); + } + } + + public Object get(String name, Scriptable start) { + if (map == null || (reflect && super.has(name, start))) { + return super.get(name, start); + } + return getInternal(name); + } + + public Object get(int index, Scriptable start) { + if (map == null) { + return super.get(index, start); + } + return getInternal(index); + } + + private Object getInternal(Object key) { + Object value = map.get(key); + if (value == null) { + return Scriptable.NOT_FOUND; + } + return ScriptUtils.javaToJS(value, getParentScope()); + } + + public boolean has(String name, Scriptable start) { + if (map == null || (reflect && super.has(name, start))) { + return super.has(name, start); + } else { + return map.containsKey(name); + } + } + + public boolean has(int index, Scriptable start) { + if (map == null) { + return super.has(index, start); + } else { + return map.containsKey(index); + } + } + + public void put(String name, Scriptable start, Object value) { + if (map == null || (reflect && super.has(name, start))) { + super.put(name, start, value); + } else { + putInternal(name, value); + } + } + + public void put(int index, Scriptable start, Object value) { + if (map == null) { + super.put(index, start, value); + } else { + putInternal(index, value); + } + } + + @SuppressWarnings("unchecked") + private void putInternal(Object key, Object value) { + try { + map.put(key, ScriptUtils.jsToJava(value)); + } catch (RuntimeException e) { + Context.throwAsScriptRuntimeEx(e); + } + } + + public void delete(String name) { + if (map != null) { + try { + map.remove(name); + } catch (RuntimeException e) { + Context.throwAsScriptRuntimeEx(e); + } + } else { + super.delete(name); + } + } + + public void delete(int index) { + if (map != null) { + try { + map.remove(index); + } catch (RuntimeException e) { + Context.throwAsScriptRuntimeEx(e); + } + } else { + super.delete(index); + } + } + + public Object[] getIds() { + if (map == null) { + return super.getIds(); + } else { + return map.keySet().toArray(); + } + } + + public String toString() { + if (map == null) + return super.toString(); + return map.toString(); + } + + public Object getDefaultValue(Class typeHint) { + return toString(); + } + + public Object unwrap() { + return map; + } + + public Map getMap() { + return map; + } + + public String getClassName() { + return CLASSNAME; + } +} diff --git a/backend/mockingbird/src/main/java/org/ringojs/wrappers/ScriptableWrapper.java b/backend/mockingbird/src/main/java/org/ringojs/wrappers/ScriptableWrapper.java new file mode 100644 index 00000000..cebcc20e --- /dev/null +++ b/backend/mockingbird/src/main/java/org/ringojs/wrappers/ScriptableWrapper.java @@ -0,0 +1,90 @@ +/* + * Copyright 2009 Hannes Wallnoefer + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.ringojs.wrappers; + +import org.mozilla.javascript.ScriptableObject; +import org.mozilla.javascript.Scriptable; + +/** + * A scriptable wrapper around a scriptable object. + */ +public class ScriptableWrapper extends ScriptableObject { + + private Scriptable wrapped, properties; + + public ScriptableWrapper() {} + + public ScriptableWrapper(Scriptable wrapped, Scriptable properties) { + this.wrapped = wrapped; + this.properties = properties; + } + + public String getClassName() { + return "ScriptableWrapper"; + } + + @Override + public Object get(String name, Scriptable start) { + if (wrapped == null) { + return super.get(name, start); + } + if (name.startsWith("super$")) { + return wrapped.get(name.substring(6), wrapped); + } + if (properties != null) { + Object value = properties.get(name, properties); + if (value != NOT_FOUND) { + return value; + } + } + return wrapped.get(name, wrapped); + } + + @Override + public void put(String name, Scriptable start, Object value) { + if (wrapped == null) { + super.put(name, this, value); + } else { + if (properties.has(name, start)) { + properties.put(name, properties, value); + } else { + wrapped.put(name, wrapped, value); + } + } + } + + @Override + public void delete(String name) { + if (wrapped == null) { + super.delete(name); + } else { + if (properties.has(name, properties)) { + properties.delete(name); + } else { + wrapped.delete(name); + } + } + } + + @Override + public boolean has(String name, Scriptable start) { + if (wrapped == null) { + return super.has(name, this); + } + return wrapped.has(name, wrapped) || properties.has(name, properties); + } +} diff --git a/backend/mockingbird/src/main/java/org/ringojs/wrappers/Storable.java b/backend/mockingbird/src/main/java/org/ringojs/wrappers/Storable.java new file mode 100644 index 00000000..2680d35d --- /dev/null +++ b/backend/mockingbird/src/main/java/org/ringojs/wrappers/Storable.java @@ -0,0 +1,268 @@ +package org.ringojs.wrappers; + +import org.mozilla.javascript.*; +import org.mozilla.javascript.annotations.JSStaticFunction; +import org.mozilla.javascript.annotations.JSFunction; +import org.mozilla.javascript.annotations.JSGetter; +import org.ringojs.util.ScriptUtils; + +public class Storable extends ScriptableObject { + + private Scriptable store; + private String type; + private final boolean isPrototype; + + private Scriptable properties; + private Object key; + private Object entity; + + enum FactoryType {CONSTRUCTOR, FACTORY} + + public Storable() { + this.isPrototype = true; + } + + private Storable(Scriptable store, String type) { + this.store = store; + this.type = type; + this.isPrototype = true; + } + + private Storable(Storable prototype) { + this.store = prototype.store; + this.type = prototype.type; + this.isPrototype = false; + } + + static class FactoryFunction extends BaseFunction { + + final Storable prototype; + final FactoryType factoryType; + + FactoryFunction(Storable prototype, Scriptable scope, FactoryType factoryType) { + this.prototype = prototype; + this.factoryType = factoryType; + ScriptRuntime.setFunctionProtoAndParent(this, scope); + } + + @Override + public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + Storable storable = new Storable(prototype); + switch (factoryType) { + case CONSTRUCTOR: + ScriptUtils.checkArguments(args, 0, 1); + Scriptable properties = ScriptUtils.getScriptableArgument(args, 0, true); + if (properties == null) { + properties = cx.newObject(scope); + } + storable.properties = properties; + break; + case FACTORY: + ScriptUtils.checkArguments(args, 1, 2); + storable.key = ScriptUtils.getObjectArgument(args, 0, false); + storable.entity = ScriptUtils.getObjectArgument(args, 1, true); + break; + } + storable.setParentScope(scope); + storable.setPrototype(prototype); + return storable; + } + + @Override + public int getArity() { + return factoryType == FactoryType.CONSTRUCTOR ? 1 : 2; + } + + @Override + public String getFunctionName() { + return prototype.getType(); + } + + @Override + public int getLength() { + return getArity(); + } + } + + @JSStaticFunction + public static Scriptable defineEntity(Scriptable store, String type, Object mapping) + throws NoSuchMethodException { + int attr = DONTENUM | PERMANENT | READONLY; + Scriptable scope = ScriptRuntime.getTopCallScope(Context.getCurrentContext()); + Storable prototype = new Storable(store, type); + prototype.setParentScope(scope); + prototype.setPrototype(ScriptableObject.getClassPrototype(scope, "Storable")); + // create the constructor, visible to the application + BaseFunction ctor = new FactoryFunction(prototype, scope, FactoryType.CONSTRUCTOR); + ctor.setImmunePrototypeProperty(prototype); + defineProperty(prototype, "constructor", ctor, attr); + // create the factory function, visible to the store implementation + BaseFunction factory = new FactoryFunction(prototype, scope, FactoryType.FACTORY); + ScriptableObject.defineProperty(ctor, "createInstance", factory, attr); + if (mapping != Undefined.instance) { + ctor.defineProperty("mapping", mapping, attr); + factory.defineProperty("mapping", mapping, attr); + } + return ctor; + } + + public String getClassName() { + return "Storable"; + } + + /** + * Custom == operator. + * Must return {@link org.mozilla.javascript.Scriptable#NOT_FOUND} if this object does not + * have custom equality operator for the given value, + * Boolean.TRUE if this object is equivalent to value, + * Boolean.FALSE if this object is not equivalent to + * value. + * + * The default implementation returns Boolean.TRUE + * if this == value or {@link org.mozilla.javascript.Scriptable#NOT_FOUND} otherwise. + * It indicates that by default custom equality is available only if + * value is this in which case true is returned. + */ + @Override + protected Object equivalentValues(Object value) { + if (this == value) { + return Boolean.TRUE; + } + if (value instanceof Storable && isPersistent()) { + Storable s = (Storable) value; + return invokeStoreMethod("equalKeys", key, s.key); + } + return NOT_FOUND; + } + + @JSFunction + public void save(Object transaction) { + if (!isPrototype) { + if (entity == null) { + entity = invokeStoreMethod("getEntity", type, properties != null ? properties : key); + } + if (transaction == Undefined.instance) { + invokeStoreMethod("save", properties, entity); + } else { + invokeStoreMethod("save", properties, entity, transaction); + } + } + } + + @JSFunction("remove") + public void jsremove(Object transaction) { + if (!isPrototype && isPersistent()) { + if (key == null) { + key = invokeStoreMethod("getKey", type, entity); + } + if (transaction == Undefined.instance) { + invokeStoreMethod("remove", key); + } else { + invokeStoreMethod("remove", key, transaction); + } + } + } + + @JSGetter("_key") + public Object getKey() { + if (!isPrototype && isPersistent()) { + if (key == null) { + key = invokeStoreMethod("getKey", type, entity); + } + return key; + } + return Undefined.instance; + } + + @JSGetter("_id") + public Object getId() { + Object k = getKey(); + if (k != Undefined.instance) { + return invokeStoreMethod("getId", k); + } + return Undefined.instance; + } + + @Override + public boolean has(String name, Scriptable start) { + if (super.has(name, this)) { + return true; + } + if (isPrototype) { + return super.has(name, start); + } + if (properties == null && isPersistent()) { + properties = loadProperties(); + } + return properties != null && properties.has(name, properties); + } + + @Override + public Object get(String name, Scriptable start) { + if (isPrototype || super.has(name, this)) { + return super.get(name, start); + } + if (properties == null && isPersistent()) { + properties = loadProperties(); + } + return properties == null ? Scriptable.NOT_FOUND : properties.get(name, properties); + } + + @Override + public void put(String name, Scriptable start, Object value) { + if (isPrototype || super.has(name, this)) { + super.put(name, start, value); + } else { + if (properties == null) { + properties = loadProperties(); + } + properties.put(name, properties, value); + } + } + + @Override + public void delete(String name) { + if (isPrototype || super.has(name, this)) { + super.delete(name); + } else { + if (properties == null) { + properties = loadProperties(); + } + properties.put(name, properties, null); + } + } + + public Object[] getIds() { + if (isPrototype) { + return super.getIds(); + } + if (properties == null) { + properties = loadProperties(); + } + return properties.getIds(); + } + + public String getType() { + return type; + } + + private boolean isPersistent() { + return key != null || entity != null; + } + + private Scriptable loadProperties() { + if (entity == null) { + entity = invokeStoreMethod("getEntity", type, key); + } + return (Scriptable) invokeStoreMethod("getProperties", store, entity); + } + + private Object invokeStoreMethod(String method, Object... args) { + Object value = ScriptableObject.getProperty(store, method); + if (value instanceof Callable) { + return ((Callable) value).call(Context.getCurrentContext(), getParentScope(), store, args); + } + throw new RuntimeException("Store does not implement '" + method + "' method"); + } + +} diff --git a/backend/mockingbird/src/main/java/org/ringojs/wrappers/Stream.java b/backend/mockingbird/src/main/java/org/ringojs/wrappers/Stream.java new file mode 100644 index 00000000..e89213b8 --- /dev/null +++ b/backend/mockingbird/src/main/java/org/ringojs/wrappers/Stream.java @@ -0,0 +1,243 @@ +package org.ringojs.wrappers; + +import org.mozilla.javascript.*; +import org.mozilla.javascript.annotations.JSFunction; +import org.mozilla.javascript.annotations.JSConstructor; +import org.mozilla.javascript.annotations.JSGetter; +import org.ringojs.util.ScriptUtils; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +/** + *

A CommonJS-compliant wrapper around a Java input or output stream. To + * register Stream as a host object in Rhino call the defineClass() function + * with this class.

+ * + *
defineClass(org.ringojs.wrappers.Stream);
+ * + *

To create an Stream wrapper around an instance of java.io.InputStream + * or java.io.OutputStream call the constructor with the stream as argument:

+ * + *
var io = new Stream(javaInputStream);
+ * + *

When passed to a Java method that expects an input or output stream, Stream wrappers + * are automatically unwrapped. use the {@link #unwrap()} method to explicitly get the + * wrapped stream.

+ */ +public class Stream extends ScriptableObject implements Wrapper { + + private InputStream input; + private OutputStream output; + private boolean closed = false; + + private final static String CLASSNAME = "Stream"; + + public Stream() { + input = null; + output = null; + } + + public Stream(Scriptable scope, Object arg1, Object arg2) { + super(scope, ScriptUtils.getClassOrObjectProto(scope, CLASSNAME)); + init(arg1, arg2); + } + + @JSConstructor + public void init(Object arg1, Object arg2) { + setStream(arg1); + setStream(arg2); + } + + private void setStream(Object arg) { + if (arg instanceof Wrapper) { + arg = ((Wrapper) arg).unwrap(); + } + if (arg instanceof InputStream) { + input = (InputStream) arg; + } else if (arg instanceof OutputStream) { + output = (OutputStream) arg; + } else if (arg != Undefined.instance && arg != null) { + throw ScriptRuntime.constructError("Error", "Unsupported argument: " + arg); + } + } + + @JSFunction + public boolean readable() { + return input != null; + } + + @JSFunction + public boolean writable() { + return output != null; + } + + @JSFunction + public boolean seekable() { + return false; + } + + @JSFunction + public Object read(Object limit) { + if (input == null) { + throw ScriptRuntime.constructError("Error", "no input stream"); + } + int max = limit == Undefined.instance ? -1 : ScriptRuntime.toInt32(limit); + Scriptable scope = ScriptableObject.getTopLevelScope(this); + if (max > -1) { + try { + byte[] bytes = new byte[max]; + int read = input.read(bytes); + return read > -1 ? + new Binary(scope, Binary.Type.ByteString, bytes, 0, read) : + new Binary(scope, Binary.Type.ByteString, 0); + } catch (IOException iox) { + throw new WrappedException(iox); + } + } else { + byte[] buffer = new byte[8192]; + int read, count = 0; + try { + while ((read = input.read(buffer, count, buffer.length - count)) > -1) { + count += read; + if (count == buffer.length) { + byte[] b = new byte[buffer.length * 2]; + System.arraycopy(buffer, 0, b, 0, count); + buffer = b; + } + } + return count > -1 ? + new Binary(scope, Binary.Type.ByteString, buffer, 0, count) : + new Binary(scope, Binary.Type.ByteString, 0); + } catch (IOException iox) { + throw ScriptRuntime.constructError("Error", "Error reading from input stream: " + iox); + } + } + } + + @JSFunction + public int readInto(Binary bytes, Object start, Object end) { + if (input == null) { + throw ScriptRuntime.constructError("Error", "no input stream"); + } else if (bytes == Undefined.instance || bytes == null) { + throw ScriptRuntime.constructError("Error", "readInto called without Binary argument"); + } else if (bytes.getType() != Binary.Type.ByteArray) { + throw ScriptRuntime.constructError("Error", "argument to readInto must be ByteArray"); + } + int from = ScriptUtils.toInt(start, 0); + int to = ScriptUtils.toInt(end, bytes.getLength()); + try { + byte[] b = bytes.getBytes(); + return input.read(b, from, to - from); + } catch (IOException iox) { + throw new WrappedException(iox); + } + } + + @JSFunction + public void write(Object arg, Object start, Object end) { + if (arg instanceof Wrapper) { + arg = ((Wrapper) arg).unwrap(); + } + byte[] bytes; + if (arg instanceof Binary) { + bytes = ((Binary) arg).getBytes(); + } else if (arg instanceof byte[]) { + bytes = (byte[]) arg; + } else if (arg instanceof String) { + // TODO this is for narwhal compatibility only + System.err.println("Warning: binary write called with string argument. Using default encoding."); + bytes = ((String) arg).getBytes(); + } else { + throw Context.reportRuntimeError("write called with illegal argument: " + arg); + } + if (output == null) { + throw ScriptRuntime.constructError("Error", "no output stream"); + } + int from = start == Undefined.instance ? 0 : ScriptRuntime.toInt32(start); + int to = end == Undefined.instance ? bytes.length : ScriptRuntime.toInt32(end); + try { + output.write(bytes, from, to - from); + } catch (IOException iox) { + throw Context.throwAsScriptRuntimeEx(iox); + } + } + + @JSFunction + public void flush() { + if (output == null) { + throw ScriptRuntime.constructError("Error", "no output stream"); + } + try { + output.flush(); + } catch (IOException iox) { + throw new WrappedException(iox); + } + } + + @JSFunction + public int skip(int num) { + try { + if (input != null) { + return (int) input.skip(num); + } else { + throw Context.reportRuntimeError( + "skip() invoked on non-readable Stream"); + } + } catch (IOException iox) { + throw new WrappedException(iox); + } + } + + @JSFunction + public void close() { + try { + if (output != null) { + output.close(); + } + if (input != null) { + input.close(); + } + closed = true; + } catch (IOException iox) { + throw new WrappedException(iox); + } + } + + @JSFunction + public boolean closed() { + return closed; + } + + @JSFunction("unwrap") + public Object jsunwrap() { + return new NativeJavaObject(getParentScope(), unwrap(), null); + } + + @JSGetter + public Object getInputStream() { + return input == null ? null : new NativeJavaObject(getParentScope(), input, null); + } + + @JSGetter + public Object getOutputStream() { + return output == null ? null : new NativeJavaObject(getParentScope(), output, null); + } + + /** + * Unwrap the object by returning the wrapped value. + * @return a wrapped value + */ + public Object unwrap() { + return input != null ? input : output; + } + + /** + * Return the name of the class. + * @return the class name + */ + public String getClassName() { + return CLASSNAME; + } +} diff --git a/backend/mockingbird/src/main/java/org/ringojs/wrappers/package.html b/backend/mockingbird/src/main/java/org/ringojs/wrappers/package.html new file mode 100644 index 00000000..8cbcf095 --- /dev/null +++ b/backend/mockingbird/src/main/java/org/ringojs/wrappers/package.html @@ -0,0 +1,6 @@ + + + +A collection of JavaScript wrappers and host objects. + + diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandbox.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandbox.scala index 5d1a9e3a..7679533c 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandbox.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandbox.scala @@ -6,14 +6,18 @@ import scala.reflect.classTag import org.mozilla.javascript.Context -class RhinoJsSandbox(maxRunTime: Option[FiniteDuration] = None, maxInstructions: Option[Long] = None) { - private val contextFactory = new SafeContextFactory(maxRunTime, maxInstructions) +class RhinoJsSandbox( + maxRunTime: Option[FiniteDuration] = None, + maxInstructions: Option[Long] = None, + allowedClasses: List[String] = Nil +) { + private val contextFactory = new SafeContextFactory(maxRunTime, maxInstructions, allowedClasses) def eval[T: ClassTag](code: String, environment: Map[String, Any] = Map.empty): T = { val ctx = contextFactory.enterContext() try { - val scope = ctx.initSafeStandardObjects() + val scope = ctx.initStandardObjects() for ((key, value) <- environment) scope.put(key, scope, Context.javaToJS(value, scope, ctx)) diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeClassShutter.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeClassShutter.scala index 8e660ee5..dec9069b 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeClassShutter.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeClassShutter.scala @@ -2,6 +2,14 @@ package ru.tinkoff.tcb.utils.sandboxing import org.mozilla.javascript.ClassShutter -object SafeClassShutter extends ClassShutter { - override def visibleToScripts(fullClassName: String): Boolean = fullClassName.startsWith("adapter") +class SafeClassShutter(userAllowedClasses: Set[String]) extends ClassShutter { + override def visibleToScripts(fullClassName: String): Boolean = + fullClassName.startsWith("adapter") || + allowedClasses(fullClassName) || + userAllowedClasses(fullClassName) + + private val allowedClasses = Set( + "scala.collection.convert.JavaCollectionWrappers$MapWrapper", + "scala.collection.convert.JavaCollectionWrappers$SeqWrapper" + ) } diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeContextFactory.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeContextFactory.scala index 2dedbd91..72b8a904 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeContextFactory.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeContextFactory.scala @@ -8,13 +8,15 @@ import org.mozilla.javascript.Context import org.mozilla.javascript.ContextFactory import org.mozilla.javascript.Scriptable -class SafeContextFactory(maxRunTime: Option[FiniteDuration], maxInstructions: Option[Long]) extends ContextFactory { +class SafeContextFactory(maxRunTime: Option[FiniteDuration], maxInstructions: Option[Long], allowedClasses: List[String]) + extends ContextFactory { override def makeContext(): Context = new CounterContext(this).tap { cc => cc.setLanguageVersion(Context.VERSION_ES6) cc.setOptimizationLevel(-1) cc.setInstructionObserverThreshold(CounterContext.InstructionSteps) - cc.setClassShutter(SafeClassShutter) + cc.setClassShutter(new SafeClassShutter(allowedClasses.to(Set))) + cc.setWrapFactory(SandboxWrapFactory) } override def hasFeature(cx: Context, featureIndex: RuntimeFlags): Boolean = featureIndex match { diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SandboxWrapFactory.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SandboxWrapFactory.scala new file mode 100644 index 00000000..dfd55749 --- /dev/null +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SandboxWrapFactory.scala @@ -0,0 +1,22 @@ +package ru.tinkoff.tcb.utils.sandboxing + +import java.util.Map as JMap + +import org.mozilla.javascript.Context +import org.mozilla.javascript.Scriptable +import org.mozilla.javascript.WrapFactory +import org.ringojs.wrappers.ScriptableMap + +object SandboxWrapFactory extends WrapFactory { + override def wrapAsJavaObject( + cx: Context, + scope: Scriptable, + javaObject: AnyRef, + staticType: Class[?] + ): Scriptable = + javaObject.getClass match { + case jmap if classOf[JMap[?, ?]].isAssignableFrom(jmap) => + new ScriptableMap(scope, javaObject.asInstanceOf[JMap[?, ?]]) + case _ => super.wrapAsJavaObject(cx, scope, javaObject, staticType) + } +} diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/json/js_eval/package.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/json/js_eval/package.scala new file mode 100644 index 00000000..0a51c3db --- /dev/null +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/json/js_eval/package.scala @@ -0,0 +1,28 @@ +package ru.tinkoff.tcb.utils.transformation.json + +import java.lang.Boolean as JBoolean +import scala.jdk.CollectionConverters.* + +import io.circe.Json +import io.circe.JsonNumber +import io.circe.JsonObject + +package object js_eval { + val circe2js: Json.Folder[AnyRef] = new Json.Folder[AnyRef] { + override def onNull: AnyRef = null + + override def onBoolean(value: Boolean): AnyRef = JBoolean.valueOf(value) + + override def onNumber(value: JsonNumber): AnyRef = value.toBigDecimal.map(_.bigDecimal).orNull + + override def onString(value: String): AnyRef = value + + override def onArray(value: Vector[Json]): AnyRef = value.map(_.foldWith[AnyRef](this)).asJava + + override def onObject(value: JsonObject): AnyRef = + value.toMap.view + .mapValues(_.foldWith[AnyRef](this)) + .toMap + .asJava + } +} diff --git a/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/json/JsEvalSpec.scala b/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/json/JsEvalSpec.scala new file mode 100644 index 00000000..3f951279 --- /dev/null +++ b/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/json/JsEvalSpec.scala @@ -0,0 +1,58 @@ +package ru.tinkoff.tcb.utils.transformation.json + +import java.math.BigDecimal as JBD +import java.security.MessageDigest +import java.util.List as JList +import scala.jdk.CollectionConverters.* + +import io.circe.Json +import io.circe.syntax.* +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +import ru.tinkoff.tcb.utils.sandboxing.RhinoJsSandbox +import ru.tinkoff.tcb.utils.transformation.json.js_eval.circe2js + +class JsEvalSpec extends AnyFunSuite with Matchers { + private val sandbox = new RhinoJsSandbox + + test("Simple expressions") { + val data = Json.obj("a" := Json.obj("b" := 42, "c" := "test", "d" := 1 :: 2 :: Nil)) + + val res = sandbox.eval[Int]("req.a.b", Map("req" -> data.foldWith(circe2js))) + + res shouldBe 42 + + val res2 = sandbox.eval[String]("req.a.c", Map("req" -> data.foldWith(circe2js))) + + res2 shouldBe "test" + + val res3 = sandbox.eval[JList[JBD]]("req.a.d", Map("req" -> data.foldWith(circe2js))) + + res3.asScala should contain theSameElementsInOrderAs List(1, 2).map(JBD.valueOf(_)) + + val res4 = sandbox.eval[Int]("req.a.d[0]", Map("req" -> data.foldWith(circe2js))) + + res4 shouldBe 1 + } + + test("JS functions") { + val aesSandbox = new RhinoJsSandbox( + allowedClasses = List( + "java.security.MessageDigest", + "java.security.MessageDigest$Delegate$CloneableDelegate", + "java.lang.String", + "java.lang.Object" + ) + ) + + val etalon = MessageDigest.getInstance("SHA-1").digest("abc".getBytes) + + val res = aesSandbox.eval[Array[Byte]]( + """var md = java.security.MessageDigest.getInstance("SHA-1"); + |md.digest((new java.lang.String("abc")).getBytes());""".stripMargin + ) + + res shouldBe etalon + } +} From 5d94755a66ab64d463ec0a1f29141d064c9956d0 Mon Sep 17 00:00:00 2001 From: Daniel Slapman Date: Thu, 8 Dec 2022 09:35:31 +0100 Subject: [PATCH 3/9] Remove instruction count limit --- .../ru/tinkoff/tcb/utils/sandboxing/CounterContext.scala | 1 - .../ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandbox.scala | 3 +-- .../tinkoff/tcb/utils/sandboxing/SafeContextFactory.scala | 8 +------- .../tinkoff/tcb/utils/sandboxing/RhinoJsSandboxSpec.scala | 8 -------- 4 files changed, 2 insertions(+), 18 deletions(-) diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/CounterContext.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/CounterContext.scala index d41ea16e..54838981 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/CounterContext.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/CounterContext.scala @@ -7,7 +7,6 @@ import org.mozilla.javascript.ContextFactory class CounterContext(factory: ContextFactory) extends Context(factory) { var deadline: Deadline = Deadline.now - var instructions: Long = 0 } object CounterContext { diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandbox.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandbox.scala index 7679533c..246e68b9 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandbox.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandbox.scala @@ -8,10 +8,9 @@ import org.mozilla.javascript.Context class RhinoJsSandbox( maxRunTime: Option[FiniteDuration] = None, - maxInstructions: Option[Long] = None, allowedClasses: List[String] = Nil ) { - private val contextFactory = new SafeContextFactory(maxRunTime, maxInstructions, allowedClasses) + private val contextFactory = new SafeContextFactory(maxRunTime, allowedClasses) def eval[T: ClassTag](code: String, environment: Map[String, Any] = Map.empty): T = { val ctx = contextFactory.enterContext() diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeContextFactory.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeContextFactory.scala index 72b8a904..cac318cb 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeContextFactory.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeContextFactory.scala @@ -8,8 +8,7 @@ import org.mozilla.javascript.Context import org.mozilla.javascript.ContextFactory import org.mozilla.javascript.Scriptable -class SafeContextFactory(maxRunTime: Option[FiniteDuration], maxInstructions: Option[Long], allowedClasses: List[String]) - extends ContextFactory { +class SafeContextFactory(maxRunTime: Option[FiniteDuration], allowedClasses: List[String]) extends ContextFactory { override def makeContext(): Context = new CounterContext(this).tap { cc => cc.setLanguageVersion(Context.VERSION_ES6) @@ -32,10 +31,6 @@ class SafeContextFactory(maxRunTime: Option[FiniteDuration], maxInstructions: Op if (counter.deadline.isOverdue()) throw new ScriptDurationException - - counter.instructions += CounterContext.InstructionSteps - if (maxInstructions.exists(_ <= counter.instructions)) - throw new ScriptCPUAbuseException } override def doTopCall( @@ -47,7 +42,6 @@ class SafeContextFactory(maxRunTime: Option[FiniteDuration], maxInstructions: Op ): AnyRef = { val counter = cx.asInstanceOf[CounterContext] counter.deadline = maxRunTime.fold(minute.fromNow)(fd => fd.fromNow) - counter.instructions = 0 super.doTopCall(callable, cx, scope, thisObj, args) } diff --git a/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandboxSpec.scala b/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandboxSpec.scala index 9042d71a..62f0576c 100644 --- a/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandboxSpec.scala +++ b/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandboxSpec.scala @@ -32,14 +32,6 @@ class RhinoJsSandboxSpec extends AnyFunSuite with Matchers { } } - test("Instruction limit test") { - val limitedSandbox = new RhinoJsSandbox(maxInstructions = Some(100)) - - assertThrows[ScriptCPUAbuseException] { - limitedSandbox.eval[Int]("while (true) { }") - } - } - test("Evaluations should not have shared data") { sandbox.eval[Unit]("a = 42;") assertThrows[EcmaError] { From f47ce0fd0d861e538f02ab6320a4d51dd5045a88 Mon Sep 17 00:00:00 2001 From: Daniel Slapman Date: Thu, 8 Dec 2022 18:13:20 +0100 Subject: [PATCH 4/9] Use GraalVM instead of rhino --- .github/workflows/ci.yml | 2 +- backend/build.sbt | 1 + .../mockingbird/native-image.properties | 1 + .../org/ringojs/util/CaseInsensitiveMap.java | 133 ---- .../java/org/ringojs/util/DebuggerBase.java | 127 ---- .../java/org/ringojs/util/ScriptUtils.java | 226 ------- .../java/org/ringojs/util/StackUtils.java | 39 -- .../java/org/ringojs/util/StringUtils.java | 143 ---- .../main/java/org/ringojs/util/package.html | 6 - .../java/org/ringojs/wrappers/Binary.java | 614 ------------------ .../org/ringojs/wrappers/ScriptableList.java | 219 ------- .../org/ringojs/wrappers/ScriptableMap.java | 224 ------- .../ringojs/wrappers/ScriptableWrapper.java | 90 --- .../java/org/ringojs/wrappers/Storable.java | 268 -------- .../java/org/ringojs/wrappers/Stream.java | 243 ------- .../java/org/ringojs/wrappers/package.html | 6 - .../utils/sandboxing/ClassAccessRule.scala | 13 + .../tcb/utils/sandboxing/CounterContext.scala | 14 - .../tcb/utils/sandboxing/Exceptions.scala | 5 - .../tcb/utils/sandboxing/GraalJsSandbox.scala | 49 ++ .../tcb/utils/sandboxing/RhinoJsSandbox.scala | 29 - .../utils/sandboxing/SafeClassShutter.scala | 15 - .../utils/sandboxing/SafeContextFactory.scala | 49 -- .../utils/sandboxing/SandboxWrapFactory.scala | 22 - .../utils/sandboxing/GraalJsSandboxSpec.scala | 27 + .../utils/sandboxing/RhinoJsSandboxSpec.scala | 41 -- .../transformation/json/JsEvalSpec.scala | 36 +- backend/project/Versions.scala | 2 +- 28 files changed, 111 insertions(+), 2533 deletions(-) delete mode 100644 backend/mockingbird/src/main/java/org/ringojs/util/CaseInsensitiveMap.java delete mode 100644 backend/mockingbird/src/main/java/org/ringojs/util/DebuggerBase.java delete mode 100644 backend/mockingbird/src/main/java/org/ringojs/util/ScriptUtils.java delete mode 100644 backend/mockingbird/src/main/java/org/ringojs/util/StackUtils.java delete mode 100644 backend/mockingbird/src/main/java/org/ringojs/util/StringUtils.java delete mode 100644 backend/mockingbird/src/main/java/org/ringojs/util/package.html delete mode 100644 backend/mockingbird/src/main/java/org/ringojs/wrappers/Binary.java delete mode 100644 backend/mockingbird/src/main/java/org/ringojs/wrappers/ScriptableList.java delete mode 100644 backend/mockingbird/src/main/java/org/ringojs/wrappers/ScriptableMap.java delete mode 100644 backend/mockingbird/src/main/java/org/ringojs/wrappers/ScriptableWrapper.java delete mode 100644 backend/mockingbird/src/main/java/org/ringojs/wrappers/Storable.java delete mode 100644 backend/mockingbird/src/main/java/org/ringojs/wrappers/Stream.java delete mode 100644 backend/mockingbird/src/main/java/org/ringojs/wrappers/package.html create mode 100644 backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/ClassAccessRule.scala delete mode 100644 backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/CounterContext.scala delete mode 100644 backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/Exceptions.scala create mode 100644 backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/GraalJsSandbox.scala delete mode 100644 backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandbox.scala delete mode 100644 backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeClassShutter.scala delete mode 100644 backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeContextFactory.scala delete mode 100644 backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SandboxWrapFactory.scala create mode 100644 backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/sandboxing/GraalJsSandboxSpec.scala delete mode 100644 backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandboxSpec.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef4a6513..db3b8f59 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -131,7 +131,7 @@ jobs: with: version: '22.3.1' java-version: '17' - components: 'native-image' + components: 'native-image,js' github-token: ${{ secrets.GITHUB_TOKEN }} native-image-job-reports: 'true' diff --git a/backend/build.sbt b/backend/build.sbt index d74f56b1..a1d51cd1 100644 --- a/backend/build.sbt +++ b/backend/build.sbt @@ -77,6 +77,7 @@ val mockingbird = (project in file("mockingbird")) "com.github.geirolz" %% "advxml-xpath" % "2.5.1", "io.estatico" %% "newtype" % "0.4.4", "org.mozilla" % "rhino" % "1.7.14", + "org.graalvm.js" % "js" % "22.3.0", "org.slf4j" % "slf4j-api" % "1.7.30" % Provided ), Compile / unmanagedResourceDirectories += file("../frontend/dist") diff --git a/backend/mockingbird-native/src/main/resources/META-INF/native-image/ru.tinkoff.tcb/mockingbird/native-image.properties b/backend/mockingbird-native/src/main/resources/META-INF/native-image/ru.tinkoff.tcb/mockingbird/native-image.properties index 7eddc68a..fe0768ff 100644 --- a/backend/mockingbird-native/src/main/resources/META-INF/native-image/ru.tinkoff.tcb/mockingbird/native-image.properties +++ b/backend/mockingbird-native/src/main/resources/META-INF/native-image/ru.tinkoff.tcb/mockingbird/native-image.properties @@ -1,5 +1,6 @@ Args = -H:+AddAllCharsets \ --no-fallback \ +--language:js \ --initialize-at-run-time=io.netty.bootstrap.Bootstrap,\ io.netty.bootstrap.ServerBootstrap,\ io.netty.buffer,\ diff --git a/backend/mockingbird/src/main/java/org/ringojs/util/CaseInsensitiveMap.java b/backend/mockingbird/src/main/java/org/ringojs/util/CaseInsensitiveMap.java deleted file mode 100644 index cd47a9da..00000000 --- a/backend/mockingbird/src/main/java/org/ringojs/util/CaseInsensitiveMap.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2006 Hannes Wallnoefer - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.ringojs.util; - -import java.util.Map; -import java.util.Set; -import java.util.Collection; -import java.util.HashMap; - -/** - * Map wrapper that makes a string-keyed map case insensitive while - * still preserving case in key collections. - */ -public class CaseInsensitiveMap implements Map { - - private final Map wrapped; - private final Map keymap; - - public CaseInsensitiveMap() { - wrapped = new HashMap<>(); - keymap = new HashMap<>(); - } - - public CaseInsensitiveMap(Map map) { - assert map != null; - wrapped = map; - keymap = new HashMap<>(); - for (String key: map.keySet()) { - keymap.put(processKey(key), key); - } - } - - public int size() { - return wrapped.size(); - } - - public boolean isEmpty() { - return wrapped.isEmpty(); - } - - public boolean containsKey(Object key) { - return keymap.containsKey(processKey(key)); - } - - public boolean containsValue(Object value) { - return wrapped.containsValue(value); - } - - public V get(Object key) { - key = keymap.get(processKey(key)); - return key == null ? null : wrapped.get(key); - } - - /** - * Puts a new key-value pair into the map. - * @param key key - * @param value value - * @return the old value, if an old value got replaced - */ - public V put(String key, V value) { - String pkey = processKey(key); - String previousKey = keymap.put(pkey, key); - V previousValue = wrapped.put(key, value); - if (previousValue == null && previousKey != null) - previousValue = wrapped.remove(previousKey); - return previousValue; - } - - public V remove(Object key) { - String pkey = processKey(key); - String previousKey = keymap.remove(pkey); - return previousKey == null ? null : wrapped.remove(previousKey); - } - - public void putAll(Map t) { - for (String key: t.keySet()) { - String previousKey = keymap.put(processKey(key), key); - if (previousKey != null) { - wrapped.remove(previousKey); - } - } - wrapped.putAll(t); - } - - public void clear() { - keymap.clear(); - wrapped.clear(); - } - - public Set keySet() { - return wrapped.keySet(); - } - - public Collection values() { - return wrapped.values(); - } - - public Set> entrySet() { - return wrapped.entrySet(); - } - - public String toString() { - return wrapped.toString(); - } - - public boolean equals(Object obj) { - return wrapped.equals(obj); - } - - public int hashCode() { - return wrapped.hashCode(); - } - - private String processKey(Object key) { - assert key != null; - return key instanceof String ? - ((String) key).toLowerCase() : key.toString().toLowerCase(); - } -} diff --git a/backend/mockingbird/src/main/java/org/ringojs/util/DebuggerBase.java b/backend/mockingbird/src/main/java/org/ringojs/util/DebuggerBase.java deleted file mode 100644 index 28b86371..00000000 --- a/backend/mockingbird/src/main/java/org/ringojs/util/DebuggerBase.java +++ /dev/null @@ -1,127 +0,0 @@ -package org.ringojs.util; - -import org.mozilla.javascript.Context; -import org.mozilla.javascript.Scriptable; -import org.mozilla.javascript.ContextFactory; -import org.mozilla.javascript.debug.DebugFrame; -import org.mozilla.javascript.debug.DebuggableScript; -import org.mozilla.javascript.debug.Debugger; - -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * A base class for Debuggers and Profilers implemented in Javascript. - * This allows to exclude the debugger/profiler module and all modules - * it uses to be excluded from debugging/profiling. - */ -public abstract class DebuggerBase implements Debugger { - - String debuggerScript; - int debuggerScriptDepth = 0; - final Logger log = Logger.getLogger(DebuggerBase.class.getName()); - - public abstract DebuggerBase createDebugger(); - - public abstract Object createContextData(); - - public abstract void handleCompilationDone(Context cx, DebuggableScript fnOrScript, String source); - - public abstract DebugFrame getScriptFrame(Context cx, DebuggableScript fnOrScript); - - public void attach() { - attach(createContextData()); - } - - public void setDebuggerScript(String path) { - debuggerScript = path; - } - - public void install() { - ContextFactory factory = Context.getCurrentContext().getFactory(); - factory.addListener(new ContextFactory.Listener() { - public void contextCreated(Context cx) { - DebuggerBase debugger = createDebugger(); - if (debugger != null) { - debugger.attach(createContextData()); - } - } - public void contextReleased(Context cx) { - } - }); - } - - public void attach(Object contextData) { - Context cx = Context.getCurrentContext(); - cx.setDebugger(this, contextData); - cx.setOptimizationLevel(-1); - cx.setGeneratingDebug(true); - } - - public void detach() { - Context cx = Context.getCurrentContext(); - cx.setDebugger(null, null); - } - - public Object getContextData() { - return Context.getCurrentContext().getDebuggerContextData(); - } - - public synchronized void suspend() { - try { - wait(); - } catch (InterruptedException ir) { - Thread.currentThread().interrupt(); - } - } - - public synchronized void resume() { - notify(); - } - - public DebugFrame getFrame(Context cx, DebuggableScript fnOrScript) { - String path = fnOrScript.getSourceName(); - if (log.isLoggable(Level.FINE)) { - log.fine("Getting Frame for " + path + - ", debugger script depth is " + debuggerScriptDepth); - } - if (debuggerScriptDepth > 0 || path.equals(debuggerScript)) { - return new DebuggerScriptFrame(); - } else { - return getScriptFrame(cx, fnOrScript); - } - } - - /** - * Get a string representation for the given script - * @param script a function or script - * @return the file and/or function name of the script - */ - static String getScriptName(DebuggableScript script) { - if (script.isFunction()) { - return script.getSourceName() + ": " + script.getFunctionName(); - } else { - return script.getSourceName(); - } - } - - class DebuggerScriptFrame implements DebugFrame { - - public void onEnter(Context cx, Scriptable activation, Scriptable thisObj, Object[] args) { - log.fine("Entering debugger script frame"); - debuggerScriptDepth ++; - } - - public void onExit(Context cx, boolean byThrow, Object resultOrException) { - log.fine("Exiting debugger script frame"); - debuggerScriptDepth --; - } - - public void onLineChange(Context cx, int lineNumber) {} - - public void onExceptionThrown(Context cx, Throwable ex) {} - - public void onDebuggerStatement(Context cx) {} - } - -} diff --git a/backend/mockingbird/src/main/java/org/ringojs/util/ScriptUtils.java b/backend/mockingbird/src/main/java/org/ringojs/util/ScriptUtils.java deleted file mode 100644 index 641ce46b..00000000 --- a/backend/mockingbird/src/main/java/org/ringojs/util/ScriptUtils.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright 2006 Hannes Wallnoefer - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.ringojs.util; - -import org.mozilla.javascript.*; -import org.ringojs.wrappers.ScriptableMap; -import org.ringojs.wrappers.ScriptableList; - -import java.util.List; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * A collection of Rhino utility methods. - */ -public class ScriptUtils { - - /** - * Coerce/wrap a java object to a JS object, and mask Lists and Maps - * as native JS objects. - * @param obj the object to coerce/wrap - * @param scope the scope - * @return the wrapped/masked java object - */ - @SuppressWarnings("unchecked") - public static Object javaToJS(Object obj, Scriptable scope) { - if (obj instanceof Scriptable) { - if (obj instanceof ScriptableObject - && ((Scriptable) obj).getParentScope() == null - && ((Scriptable) obj).getPrototype() == null) { - ScriptRuntime.setObjectProtoAndParent((ScriptableObject) obj, scope); - } - return obj; - } else if (obj instanceof List) { - return new ScriptableList(scope, (List) obj); - } else if (obj instanceof Map) { - return new ScriptableMap(scope, (Map) obj); - } else { - return Context.javaToJS(obj, scope); - } - } - - /** - * Unwrap a JS object to a java object. This is much more conservative than - * Context.jsToJava in that it will preserve undefined values. - * @param obj a JavaScript value - * @return a Java object corresponding to obj - */ - public static Object jsToJava(Object obj) { - while (obj instanceof Wrapper) { - obj = ((Wrapper) obj).unwrap(); - } - return obj; - } - - /** - * Return a class prototype, or the object prototype if the class - * is not defined. - * @param scope the scope - * @param className the class name - * @return the class or object prototype - */ - public static Scriptable getClassOrObjectProto(Scriptable scope, String className) { - Scriptable proto = ScriptableObject.getClassPrototype(scope, className); - if (proto == null) { - proto = ScriptableObject.getObjectPrototype(scope); - } - return proto; - } - - /** - * Make sure that number of arguments is valid. - * @param args the argument array - * @param min the minimum number of arguments - * @param max the maximum number of arguments - * @throws IllegalArgumentException if the number of arguments is not valid - */ - public static void checkArguments(Object[] args, int min, int max) { - if (min > -1 && args.length < min) - throw new IllegalArgumentException(); - if (max > -1 && args.length > max) - throw new IllegalArgumentException(); - } - - /** - * Get an argument as ScriptableObject - * @param args the argument array - * @param pos the position of the requested argument - * @return the argument as ScriptableObject - * @throws IllegalArgumentException if the argument can't be converted to a map - */ - public static ScriptableObject getScriptableArgument(Object[] args, int pos, boolean allowNull) - throws IllegalArgumentException { - if (pos >= args.length || args[pos] == null || args[pos] == Undefined.instance) { - if (allowNull) return null; - throw ScriptRuntime.constructError("Error", "Argument " + (pos + 1) + " must not be null"); - } if (args[pos] instanceof ScriptableObject) { - return (ScriptableObject) args[pos]; - } - throw ScriptRuntime.constructError("Error", "Can't convert to ScriptableObject: " + args[pos]); - } - - /** - * Get an argument as string - * @param args the argument array - * @param pos the position of the requested argument - * @return the argument as string - */ - public static String getStringArgument(Object[] args, int pos, boolean allowNull) { - if (pos >= args.length || args[pos] == null || args[pos] == Undefined.instance) { - if (allowNull) return null; - throw ScriptRuntime.constructError("Error", "Argument " + (pos + 1) + " must not be null"); - } - return ScriptRuntime.toString(args[pos].toString()); - } - - /** - * Get an argument as Map - * @param args the argument array - * @param pos the position of the requested argument - * @return the argument as map - * @throws IllegalArgumentException if the argument can't be converted to a map - */ - public static Map getMapArgument(Object[] args, int pos, boolean allowNull) - throws IllegalArgumentException { - if (pos >= args.length || args[pos] == null || args[pos] == Undefined.instance) { - if (allowNull) return null; - throw ScriptRuntime.constructError("Error", "Argument " + (pos + 1) + " must not be null"); - } if (args[pos] instanceof Map) { - return (Map) args[pos]; - } - throw ScriptRuntime.constructError("Error", "Can't convert to java.util.Map: " + args[pos]); - } - - /** - * Get an argument as object - * @param args the argument array - * @param pos the position of the requested argument - * @return the argument as object - */ - public static Object getObjectArgument(Object[] args, int pos, boolean allowNull) { - if (pos >= args.length || args[pos] == null || args[pos] == Undefined.instance) { - if (allowNull) return null; - throw ScriptRuntime.constructError("Error", "Argument " + (pos + 1) + " must not be null"); - } - return args[pos]; - } - - /** - * Try to convert an object to an int value, returning the default value if conversion fails. - * @param obj the value - * @param defaultValue the default value - * @return the converted value - */ - public static int toInt(Object obj, int defaultValue) { - double d = ScriptRuntime.toNumber(obj); - if (d == ScriptRuntime.NaN || (int)d != d) { - return defaultValue; - } - return (int) d; - } - - - /** - * Get a snapshot of the current JavaScript evaluation state by creating - * an Error object and invoke the function on it passing along any arguments. - * Used to invoke console.trace() and friends because implementing this - * in JavaScript would mess with the evaluation state. - * @param function the function to call - * @param args optional arguments to pass to the function. - */ - public static void traceHelper(Function function, Object... args) { - Context cx = Context.getCurrentContext(); - Scriptable scope = ScriptableObject.getTopLevelScope(function); - EcmaError error = ScriptRuntime.constructError("Trace", ""); - WrapFactory wrapFactory = cx.getWrapFactory(); - Scriptable thisObj = wrapFactory.wrapAsJavaObject(cx, scope, error, null); - for (int i = 0; i < args.length; i++) { - args[i] = wrapFactory.wrap(cx, scope, args[i], null); - } - function.call(cx, scope, thisObj, args); - } - - /** - * Helper for console.assert(). Implemented in Java in order not to - * modify the JavaScript stack. - * @param condition the condition to test - * @param args one or more message parts - */ - public static void assertHelper(Object condition, Object... args) { - if (ScriptRuntime.toBoolean(condition)) { - return; - } - // assertion failed - String msg = ""; - if (args.length > 0) { - msg = ScriptRuntime.toString(args[0]); - Pattern pattern = Pattern.compile("%[sdifo]"); - for (int i = 1; i < args.length; i++) { - Matcher matcher = pattern.matcher(msg); - if (matcher.find()) { - msg = matcher.replaceFirst(ScriptRuntime.toString(args[i])); - } else { - msg = msg + " " + ScriptRuntime.toString(args[i]); - } - } - } - throw ScriptRuntime.constructError("AssertionError", msg); - } - -} diff --git a/backend/mockingbird/src/main/java/org/ringojs/util/StackUtils.java b/backend/mockingbird/src/main/java/org/ringojs/util/StackUtils.java deleted file mode 100644 index eba7121b..00000000 --- a/backend/mockingbird/src/main/java/org/ringojs/util/StackUtils.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2006 Hannes Wallnoefer - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.ringojs.util; - -import java.util.List; -import java.util.ArrayList; - -/** - * Utility class to extract pure JavaScript stack trace from Java exceptions - */ -public class StackUtils { - - public static StackTraceElement[] getJavaScriptStack(Throwable t) { - List list = new ArrayList<>(); - StackTraceElement[] stack = t.getStackTrace(); - for (StackTraceElement e: stack) { - String name = e.getFileName(); - if (e.getLineNumber() > -1 && - (name.endsWith(".js") || name.endsWith(".hac"))) { - list.add(e); - } - } - return list.toArray(new StackTraceElement[list.size()]); - } -} diff --git a/backend/mockingbird/src/main/java/org/ringojs/util/StringUtils.java b/backend/mockingbird/src/main/java/org/ringojs/util/StringUtils.java deleted file mode 100644 index 5e62ef07..00000000 --- a/backend/mockingbird/src/main/java/org/ringojs/util/StringUtils.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Helma License Notice - * - * The contents of this file are subject to the Helma License - * Version 2.0 (the "License"). You may not use this file except in - * compliance with the License. A copy of the License is available at - * http://adele.invoker.org/download/invoker/license.txt - * - * Copyright 2005 Hannes Wallnoefer. All Rights Reserved. - */ - -package org.ringojs.util; - - -import java.nio.CharBuffer; -import java.util.StringTokenizer; - -/** - * Utility class for String manipulation. - */ -public class StringUtils { - - - /** - * Split a string into an array of strings. Use comma and space - * as delimiters. - * @param str the string to split - * @return the string split into a string array - */ - public static String[] split(String str) { - return split(str, ", \t\n\r\f"); - } - - /** - * Split a string into an array of strings. - * @param str the string to split - * @param delim the delimiter to split the string at - * @return the string split into a string array - */ - public static String[] split(String str, String delim) { - if (str == null) { - return new String[0]; - } - StringTokenizer st = new StringTokenizer(str, delim); - String[] s = new String[st.countTokens()]; - for (int i=0; i", ">") - .replaceAll("\"", """); - } - - /** - * Split a string and try to convert to an array of classes. - * @param str a string containint class names - * @param delim the delimiter - * @return an array of classes - * @throws ClassNotFoundException if any class name contained in the string - * couldn't be resolved - */ - public static Class[] toClassArray(String str, String delim) - throws ClassNotFoundException { - String[] s = split(str, delim); - Class[] classes = new Class[s.length]; - for (int i=0; i= to) { - return -1; - } - char[] chars = buffer.array(); - for (int i = from; i < to; i++) { - if (chars[i] == '\n' || (chars[i] == '\r' && i < to - 1)) { - return i; - } - } - return -1; - } - -} diff --git a/backend/mockingbird/src/main/java/org/ringojs/util/package.html b/backend/mockingbird/src/main/java/org/ringojs/util/package.html deleted file mode 100644 index 709110ff..00000000 --- a/backend/mockingbird/src/main/java/org/ringojs/util/package.html +++ /dev/null @@ -1,6 +0,0 @@ - - - -Various utility classes. - - diff --git a/backend/mockingbird/src/main/java/org/ringojs/wrappers/Binary.java b/backend/mockingbird/src/main/java/org/ringojs/wrappers/Binary.java deleted file mode 100644 index bf33ea02..00000000 --- a/backend/mockingbird/src/main/java/org/ringojs/wrappers/Binary.java +++ /dev/null @@ -1,614 +0,0 @@ -package org.ringojs.wrappers; - -import org.mozilla.javascript.*; -import org.mozilla.javascript.annotations.JSFunction; -import org.mozilla.javascript.annotations.JSGetter; -import org.mozilla.javascript.annotations.JSSetter; -import org.mozilla.javascript.annotations.JSConstructor; -import org.ringojs.util.ScriptUtils; - -import java.io.UnsupportedEncodingException; -import java.util.List; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Map; -import java.lang.reflect.Method; - -/** - *

A wrapper around a Java byte array compliant to the Binary/ByteArray/ByteString - * classes defined in the Binary/B proposal. - * To register Binary, ByteArray and ByteString as a host objects in Rhino call the - * defineClass() function with this class as argument.

- * - *
defineClass(org.ringojs.wrappers.Binary);
- * - * The JavaScript Binary class serves as common base class for ByteArray and ByteString - * and can't be instantiated. ByteArray implements a modifiable and resizable byte buffer, - * while ByteString implements an immutable byte sequence. The ByteArray and ByteString - * constructors can take several arguments. Have a look at the proposal for details. - * - * When passed to a Java method that expects a byte array, instances of thes class - * are automatically unwrapped. - */ -public class Binary extends ScriptableObject implements Wrapper { - - private byte[] bytes; - private int length; - private final Type type; - - enum Type { - Binary, ByteArray, ByteString - } - - public Binary() { - type = Type.Binary; - } - - public Binary(Type type) { - this.type = type; - } - - public Binary(Scriptable scope, Type type, int length) { - super(scope, ScriptUtils.getClassOrObjectProto(scope, type.toString())); - this.type = type; - this.bytes = new byte[Math.max(length, 8)]; - this.length = length; - } - - public Binary(Scriptable scope, Type type, byte[] bytes) { - this(scope, type, bytes, 0, bytes.length); - } - - public Binary(Scriptable scope, Type type, byte[] bytes, int offset, int length) { - super(scope, ScriptUtils.getClassOrObjectProto(scope, type.toString())); - this.bytes = new byte[length]; - this.length = length; - this.type = type; - System.arraycopy(bytes, offset, this.bytes, 0, length); - } - - @JSConstructor - public static Object construct(Context cx, Object[] args, Function ctorObj, boolean inNewExpr) { - ScriptUtils.checkArguments(args, 0, 2); - Scriptable scope = ctorObj.getParentScope(); - Type type = Type.valueOf((String) ctorObj.get("name", ctorObj)); - if (type == Type.Binary) { - throw ScriptRuntime.constructError("Error", "cannot instantiate Binary base class"); - } - if (args.length == 0) { - return new Binary(scope, type, 0); - } - Object arg = args[0]; - if (arg instanceof Wrapper) { - arg = ((Wrapper) arg).unwrap(); - } - if (args.length == 2) { - if (!(arg instanceof String)) { - throw ScriptRuntime.constructError("Error", "Expected string as first argument"); - } else if (!(args[1] instanceof String)) { - throw ScriptRuntime.constructError("Error", "Expected string as second argument"); - } - try { - return new Binary(scope, type, ((String) arg).getBytes((String) args[1])); - } catch (UnsupportedEncodingException uee) { - throw ScriptRuntime.constructError("Error", "Unsupported encoding: " + args[1]); - } - } else if (arg instanceof Number && type == Type.ByteArray) { - return new Binary(scope, type, ((Number) arg).intValue()); - } else if (ScriptRuntime.isArrayObject(arg)) { - Scriptable object = (Scriptable) arg; - long longLength = ScriptRuntime.toUint32( - ScriptRuntime.getObjectProp(object, "length", cx)); - if (longLength > Integer.MAX_VALUE) { - throw new IllegalArgumentException(); - } - int length = (int) longLength; - Binary bytes = new Binary(scope, type, length); - for (int i = 0; i < length; i++) { - Object value = ScriptableObject.getProperty(object, i); - bytes.putInternal(i, value); - } - return bytes; - } else if (arg instanceof byte[]) { - return new Binary(scope, type, (byte[]) arg); - } else if (arg instanceof Binary) { - return new Binary(scope, type, ((Binary) arg).getBytes()); - } else if (arg == Undefined.instance) { - return new Binary(scope, type, 0); - } else { - throw ScriptRuntime.constructError("Error", "Unsupported argument: " + arg); - } - } - - // Called after the host class has been defined. - public static void finishInit(Scriptable scope, FunctionObject ctor, Scriptable prototype) - throws NoSuchMethodException{ - initClass(scope, prototype, Type.ByteArray); - initClass(scope, prototype, Type.ByteString); - } - - private static void initClass(Scriptable scope, Scriptable parentProto, Type type) - throws NoSuchMethodException { - Binary prototype = new Binary(type); - prototype.setPrototype(parentProto); - Method ctorMember = Binary.class.getMethod("construct", Context.class, Object[].class, Function.class, Boolean.TYPE); - FunctionObject constructor = new FunctionObject(type.toString(), ctorMember, scope); - constructor.addAsConstructor(scope, prototype); - constructor.defineProperty("wrap", new Wrap(scope, prototype, type), - DONTENUM | READONLY | PERMANENT); - } - - public Type getType() { - return type; - } - - @Override - public Object get(int index, Scriptable start) { - if (index < 0 || index >= length) { - return Undefined.instance; - } - return Integer.valueOf(0xff & bytes[index]); - } - - @Override - public boolean has(int index, Scriptable start) { - return index >= 0 && index < length; - } - - @Override - public void put(int index, Scriptable start, Object value) { - if (type != Type.ByteArray) { - return; - } - putInternal(index, value); - } - - private void putInternal(int index, Object value) { - if (index < 0) { - throw ScriptRuntime.constructError("Error", "Negative ByteArray index"); - } - if (!(value instanceof Number)) { - throw ScriptRuntime.constructError("Error", "Non-numeric ByteArray member: " + value); - } - if (index >= length) { - setLength(index + 1); - } - int n = ((Number) value).intValue(); - bytes[index] = (byte) (0xff & n); - } - - @JSGetter - public int getLength() { - return length; - } - - @JSSetter - public synchronized void setLength(Object length) { - int l = ScriptUtils.toInt(length, -1); - if (l < 0) { - throw ScriptRuntime.constructError("Error", "Inappropriate ByteArray length"); - } - setLength(l); - } - - protected synchronized void setLength(int newLength) { - if (type != Type.ByteArray) { - return; - } - if (newLength < length) { - // if shrinking clear the old buffer - Arrays.fill(bytes, newLength, length, (byte) 0); - } else if (newLength > bytes.length) { - // if growing make sure the buffer is large enough - int newSize = Math.max(newLength, bytes.length * 2); - byte[] b = new byte[newSize]; - System.arraycopy(bytes, 0, b, 0, length); - bytes = b; - } - length = newLength; - } - - @JSFunction - public Object get(Object index) { - int i = ScriptUtils.toInt(index, -1); - if (i < 0 || i >= length) { - return Undefined.instance; - } - return Integer.valueOf(0xff & bytes[i]); - } - - @JSFunction - public Object charCodeAt(Object index) { - return get(index); - } - - @JSFunction - public Object byteAt(Object index) { - int i = ScriptUtils.toInt(index, -1); - if (i < 0 || i >= length) { - return new Binary(getParentScope(), type, 0); - } - return new Binary(getParentScope(), type, new byte[] {bytes[i]}); - } - - @JSFunction - public Object charAt(Object index) { - return byteAt(index); - } - - @JSFunction - public void set(Object index, int value) { - if (type != Type.ByteArray) { - return; - } - int i = ScriptUtils.toInt(index, -1); - if (i > -1) { - if (i >= length) { - setLength(i + 1); - } - bytes[i] = (byte) (0xff & value); - } - } - - @JSFunction - public Object toByteArray(Object sourceCharset, Object targetCharset) - throws UnsupportedEncodingException { - return makeCopy(Type.ByteArray, sourceCharset, targetCharset); - } - - @JSFunction - public Object toByteString(Object sourceCharset, Object targetCharset) - throws UnsupportedEncodingException { - // no need to make a copy of an unmodifiable ByteString - if (type == Type.ByteString && - sourceCharset == Undefined.instance && - targetCharset == Undefined.instance) { - return this; - } - return makeCopy(Type.ByteString, sourceCharset, targetCharset); - } - - private synchronized Binary makeCopy(Type targetType, Object sourceCharset, - Object targetCharset) - throws UnsupportedEncodingException { - String source = toCharset(sourceCharset); - String target = toCharset(targetCharset); - if (source != null && target != null) { - String str = new String(bytes, 0, length, source); - return new Binary(getParentScope(), targetType, str.getBytes(target)); - } - if (this.type == Type.ByteString && this.type == targetType) { - return this; - } - return new Binary(getParentScope(), targetType, bytes, 0, length); - } - - @JSFunction - public void copy(int srcStartIndex, int srcEndIndex, Binary target, Object targetIndex) { - if (target.type != Type.ByteArray) { - throw ScriptRuntime.constructError("Error", "Target object is not writable"); - } else if (srcStartIndex < 0 || srcStartIndex >= length) { - throw ScriptRuntime.constructError("Error", "Invalid start index: " + srcStartIndex); - } else if (srcEndIndex < srcStartIndex || srcEndIndex > length) { - throw ScriptRuntime.constructError("Error", "Invalid end index: " + srcEndIndex); - } - int targetIdx = ScriptUtils.toInt(targetIndex, 0); - if (targetIdx < 0) { - throw ScriptRuntime.constructError("Error", "Invalid target index: " + targetIndex); - } - int size = srcEndIndex - srcStartIndex; - target.copyFrom(bytes, srcStartIndex, size, targetIdx); - - } - - private synchronized void copyFrom(byte[] source, int srcIndex, int length, int targetIndex) { - ensureLength(targetIndex + length); - System.arraycopy(source, srcIndex, bytes, targetIndex, length); - } - - @JSFunction - public synchronized Object toArray(Object charset) - throws UnsupportedEncodingException { - Object[] elements; - String cs = toCharset(charset); - if (cs != null) { - String str = new String(bytes, 0, length, cs); - elements = new Object[str.length()]; - for (int i = 0; i < elements.length; i++) { - elements[i] = Integer.valueOf(str.charAt(i)); - } - } else { - elements = new Object[length]; - for (int i = 0; i < length; i++) { - elements[i] = Integer.valueOf(0xff & bytes[i]); - } - } - return Context.getCurrentContext().newArray(getParentScope(), elements); - } - - @Override - @JSFunction - public String toString() { - if (bytes != null) { - return "[" + type.toString() + " " + length + "]"; - } - return "[object " + type.toString() + "]"; - } - - /* @JSFunction - public String toString(Object encoding) { - return encoding == Undefined.instance ? - toString() : decodeToString(encoding); - } */ - - @JSFunction - public Object slice(Object begin, Object end) { - if (begin == Undefined.instance && end == Undefined.instance) { - return new Binary(getParentScope(), type, bytes, 0, length); - } - int from = ScriptUtils.toInt(begin, 0); - if (from < 0) { - from += length; - } - from = Math.min(length, Math.max(0, from)); - int to = end == Undefined.instance ? length : ScriptUtils.toInt(end, from); - if (to < 0) { - to += length; - } - int len = Math.max(0, Math.min(length - from, to - from)); - return new Binary(getParentScope(), type, bytes, from, len); - } - - @JSFunction - public static Object concat(Context cx, Scriptable thisObj, - Object[] args, Function func) { - int arglength = 0; - List arglist = new ArrayList(args.length); - for (Object arg : args) { - if (arg instanceof Binary) { - byte[] bytes = ((Binary) arg).getBytes(); - arglength += bytes.length; - arglist.add(bytes); - } else if (ScriptRuntime.isArrayObject(arg)) { - Scriptable object = (Scriptable) arg; - long longLength = ScriptRuntime.toUint32( - ScriptRuntime.getObjectProp(object, "length", cx)); - if (longLength > Integer.MAX_VALUE) { - throw new IllegalArgumentException(); - } - int length = (int) longLength; - byte[] bytes = new byte[length]; - for (int i = 0; i < length; i++) { - Object value = ScriptableObject.getProperty(object, i); - if (!(value instanceof Number)) { - throw ScriptRuntime.constructError("Error", "Non-numeric ByteArray member: " + value); - } - int n = ((Number) value).intValue(); - bytes[i] = (byte) (0xff & n); - } - arglength += bytes.length; - arglist.add(bytes); - } else { - throw ScriptRuntime.constructError("Error", "Unsupported argument: " + arg); - } - } - Binary thisByteArray = (Binary) thisObj; - synchronized (thisByteArray) { - byte[] newBytes = new byte[thisByteArray.length + arglength]; - System.arraycopy(thisByteArray.bytes, 0, newBytes, 0, thisByteArray.length); - int index = thisByteArray.length; - for (byte[] b : arglist) { - System.arraycopy(b, 0, newBytes, index, b.length); - index += b.length; - } - return new Binary(thisObj.getParentScope(), thisByteArray.type, newBytes); - } - } - - @JSFunction - public String decodeToString(Object charset) { - String cs = toCharset(charset); - try { - return cs == null ? - new String(bytes, 0, length) : - new String(bytes, 0, length, cs); - } catch (UnsupportedEncodingException uee) { - throw ScriptRuntime.constructError("Error", "Unsupported encoding: " + charset); - } - } - - @JSFunction - public int indexOf(Object arg, Object from, Object to) { - byte[] b = getBytesArgument(arg); - int start = Math.max(0, Math.min(length - 1, ScriptUtils.toInt(from, 0))); - int end = Math.max(0, Math.min(length - b.length + 1, ScriptUtils.toInt(to, length))); - outer: - for (int i = start; i < end; i++) { - for (int j = 0; j < b.length; j++) { - if (bytes[i + j] != b[j]) { - continue outer; - } - } - return i; - } - return -1; - } - - @JSFunction - public int lastIndexOf(Object arg, Object from, Object to) { - byte[] b = getBytesArgument(arg); - int start = Math.max(0, Math.min(length - 1, ScriptUtils.toInt(from, 0))); - int end = Math.max(0, Math.min(length - b.length + 1, ScriptUtils.toInt(to, length))); - outer: - for (int i = end - 1; i >= start; i--) { - for (int j = 0; j < b.length; j++) { - if (bytes[i + j] != b[j]) { - continue outer; - } - } - return i; - } - return -1; - } - - @JSFunction - public synchronized Object split(Object delim, Object options) { - byte[][] delimiters = getSplitDelimiters(delim); - boolean includeDelimiter = false; - int count = Integer.MAX_VALUE; - if (options instanceof Scriptable) { - Scriptable o = (Scriptable) options; - Object include = ScriptableObject.getProperty(o, "includeDelimiter"); - includeDelimiter = include != NOT_FOUND && ScriptRuntime.toBoolean(include); - Object max = ScriptableObject.getProperty(o, "count"); - if (max != NOT_FOUND) count = ScriptRuntime.toInt32(max); - } - List list = new ArrayList(); - Scriptable scope = getParentScope(); - int index = 0; - int found = 0; - outer: - for (int i = 0; i < length && found < count - 1; i++) { - inner: - for (byte[] delimiter : delimiters) { - if (i + delimiter.length > length) { - continue; - } - for (int j = 0; j < delimiter.length; j++) { - if (bytes[i + j] != delimiter[j]) { - continue inner; - } - } - list.add(new Binary(scope, type, bytes, index, i - index)); - if (includeDelimiter) { - list.add(new Binary(scope, type, delimiter)); - } - index = i + delimiter.length; - i = index - 1; - found++; - continue outer; - } - } - if (index == 0) { - list.add(this); - } else { - list.add(new Binary(scope, type, bytes, index, length - index)); - } - return Context.getCurrentContext().newArray(scope, list.toArray()); - } - - protected static Binary wrap(Type type, byte[] bytes, Scriptable scope, - Scriptable prototype) { - Binary wrapper = new Binary(type); - wrapper.bytes = bytes; - wrapper.length = bytes.length; - wrapper.setParentScope(scope); - wrapper.setPrototype(prototype); - return wrapper; - } - - /** - * Static method that wraps a java byte array without copying. - */ - static class Wrap extends BaseFunction { - final Type type; - final Scriptable prototype; - - Wrap(Scriptable scope, Scriptable prototype, Type type) { - super(scope, ScriptableObject.getFunctionPrototype(scope)); - this.type = type; - this.prototype = prototype; - } - - @Override - public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { - Object arg = ScriptUtils.getObjectArgument(args, 0, false); - if (arg instanceof Wrapper) { - arg = ((Wrapper) arg).unwrap(); - } - if (!(arg instanceof byte[])) { - throw ScriptRuntime.constructError("Error", "wrap() requires an argument of type byte[]"); - } - byte[] bytes = (byte[]) arg; - return wrap(type, bytes, getTopLevelScope(scope), prototype); - } - } - - @JSFunction("unwrap") - public Object jsunwrap() { - return NativeJavaArray.wrap(getParentScope(), getBytes()); - } - - /** - * Unwrap the object by returning the wrapped value. - * - * @return a wrapped value - */ - public Object unwrap() { - return getBytes(); - } - - public byte[] getBytes() { - normalize(); - return bytes; - } - - public String getClassName() { - return type.toString(); - } - - protected synchronized void ensureLength(int minLength) { - if (minLength > length) { - setLength(minLength); - } - } - - private synchronized void normalize() { - if (length != bytes.length) { - byte[] b = new byte[length]; - System.arraycopy(bytes, 0, b, 0, length); - bytes = b; - } - } - - private byte[] getBytesArgument(Object arg) { - if (arg instanceof Number) { - return new byte[] {(byte) (0xff & ((Number) arg).intValue())}; - } else if (arg instanceof Binary) { - return ((Binary) arg).getBytes(); - } else { - throw new RuntimeException("unsupported delimiter: " + arg); - } - } - - private byte[][] getSplitDelimiters(Object delim) { - List list = new ArrayList(); - Collection values = null; - if (delim instanceof Collection) { - values = (Collection) delim; - } else if (delim instanceof Map && delim instanceof NativeArray) { - // NativeArray used to implement java.util.Map - values = ((Map) delim).values(); - } - if (values != null) { - for (Object value : values) { - list.add(getBytesArgument(value)); - } - } else { - list.add(getBytesArgument(delim)); - } - return list.toArray(new byte[list.size()][]); - } - - private String toCharset(Object charset) { - if (charset == Undefined.instance || charset == null) { - return null; - } - if (!(charset instanceof String)) { - throw ScriptRuntime.constructError("Error", - "Charset is not a string: " + charset); - } - return (String) charset; - } -} diff --git a/backend/mockingbird/src/main/java/org/ringojs/wrappers/ScriptableList.java b/backend/mockingbird/src/main/java/org/ringojs/wrappers/ScriptableList.java deleted file mode 100644 index afac79a3..00000000 --- a/backend/mockingbird/src/main/java/org/ringojs/wrappers/ScriptableList.java +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright 2006 Hannes Wallnoefer - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.ringojs.wrappers; - -import org.mozilla.javascript.*; -import org.ringojs.util.ScriptUtils; - -import java.util.List; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Map; - -/** - * ScriptableList is a wrapper for java.util.List instances that allows developers - * to interact with them like it was a native JavaScript array. - */ -public class ScriptableList extends NativeJavaObject { - - List list; - static final String CLASSNAME = "ScriptableList"; - - // Set up a custom constructor, for this class is somewhere between a host class and - // a native wrapper, for which no standard constructor class exists - public static void init(Scriptable scope) throws NoSuchMethodException { - BaseFunction ctor = new BaseFunction(scope, ScriptableObject.getFunctionPrototype(scope)) { - @Override - public Scriptable construct(Context cx, Scriptable scope, Object[] args) { - if (args.length > 1) { - throw new EvaluatorException("ScriptableList() requires a java.util.List argument"); - } - return new ScriptableList(scope, args.length == 0 ? null : args[0]); - } - @Override - public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { - return construct(cx, scope, args); - } - }; - ScriptableObject.defineProperty(scope, CLASSNAME, ctor, - ScriptableObject.DONTENUM | ScriptableObject.READONLY); - } - - /** - * Create a ScriptableList wrapper around a java.util.List - * @param scope the scope - * @param obj the list, possibly wrapped - */ - @SuppressWarnings("unchecked") - private ScriptableList(Scriptable scope, Object obj) { - this.parent = scope; - if (obj instanceof Wrapper) { - obj = ((Wrapper) obj).unwrap(); - } - if (obj instanceof List) { - this.javaObject = this.list = (List) obj; - } else if (obj instanceof Collection) { - this.javaObject = this.list = new ArrayList<>((Collection) obj); - } else if (obj instanceof Map) { - this.javaObject = this.list = new ArrayList<>(((Map) obj).values()); - } else if (obj == null || obj == Undefined.instance) { - this.javaObject = this.list = new ArrayList<>(); - } else { - throw new EvaluatorException("Invalid argument to ScriptableList(): " + obj); - } - this.staticType = this.list.getClass(); - initMembers(); - initPrototype(scope); - } - - - /** - * Create a ScriptableList wrapper around a java.util.List. - * @param scope the scope - * @param list the list instance - */ - @SuppressWarnings("unchecked") - public ScriptableList(Scriptable scope, List list) { - super(scope, list, list.getClass()); - this.list = list; - initPrototype(scope); - } - - /** - * Set the prototype to the Array prototype so we can use array methds such as - * push, pop, shift, slice etc. - * @param scope the global scope for looking up the Array constructor - */ - protected void initPrototype(Scriptable scope) { - Scriptable arrayProto = ScriptableObject.getClassPrototype(scope, "Array"); - if (arrayProto != null) { - this.setPrototype(arrayProto); - } - } - - public void delete(int index) { - if (list != null) { - try { - list.remove(index); - } catch (RuntimeException e) { - throw Context.throwAsScriptRuntimeEx(e); - } - } else { - super.delete(index); - } - } - - public Object get(int index, Scriptable start) { - if (list == null) - return super.get(index, start); - try { - if (index < 0 || index >= list.size()) { - return Undefined.instance; - } else { - return ScriptUtils.javaToJS(list.get(index), getParentScope()); - } - } catch (RuntimeException e) { - throw Context.throwAsScriptRuntimeEx(e); - } - } - - public boolean has(int index, Scriptable start) { - if (list == null) - return super.has(index, start); - return index >= 0 && index < list.size(); - } - - public void put(String name, Scriptable start, Object value) { - if (list != null && "length".equals(name)) { - double d = ScriptRuntime.toNumber(value); - long longVal = ScriptRuntime.toUint32(d); - if (longVal != d) { - String msg = ScriptRuntime.getMessageById("msg.arraylength.bad"); - throw ScriptRuntime.constructError("RangeError", msg); - } - int size = list.size(); - if (longVal > size) { - for (int i = size; i < longVal; i++) { - // push nulls as undefined is probably meaningless to java code - list.add(null); - } - } else if (longVal < size) { - for (int i = size - 1; i >= longVal; i--) { - list.remove(i); - } - } - } else { - super.put(name, start, value); - } - } - - public void put(int index, Scriptable start, Object value) { - if (list != null) { - try { - if (index == list.size()) { - list.add(ScriptUtils.jsToJava(value)); - } else { - list.set(index, ScriptUtils.jsToJava(value)); - } - } catch (RuntimeException e) { - Context.throwAsScriptRuntimeEx(e); - } - } else { - super.put(index, start, value); - } - } - - public Object get(String name, Scriptable start) { - if ("length".equals(name) && list != null) { - return list.size(); - } - return super.get(name, start); - } - - public Object[] getIds() { - if (list == null) - return super.getIds(); - int size = list.size(); - Object[] ids = new Object[size]; - for (int i = 0; i < size; ++i) { - ids[i] = i; - } - return ids; - } - - public String toString() { - if (list == null) - return super.toString(); - return list.toString(); - } - - public Object getDefaultValue(Class typeHint) { - return toString(); - } - - public Object unwrap() { - return list; - } - - public List getList() { - return list; - } - - public String getClassName() { - return CLASSNAME; - } -} diff --git a/backend/mockingbird/src/main/java/org/ringojs/wrappers/ScriptableMap.java b/backend/mockingbird/src/main/java/org/ringojs/wrappers/ScriptableMap.java deleted file mode 100644 index fdaaf41b..00000000 --- a/backend/mockingbird/src/main/java/org/ringojs/wrappers/ScriptableMap.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright 2006 Hannes Wallnoefer - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.ringojs.wrappers; - -import org.mozilla.javascript.*; -import org.ringojs.util.ScriptUtils; - -import java.util.Map; -import java.util.HashMap; - -/** - * ScriptableMap is a wrapper for java.util.Map instances that allows developers - * to interact with them as if it were a native JavaScript object. - */ -public class ScriptableMap extends NativeJavaObject { - - boolean reflect; - Map map; - final static String CLASSNAME = "ScriptableMap"; - - // Set up a custom constructor, for this class is somewhere between a host class and - // a native wrapper, for which no standard constructor class exists - public static void init(Scriptable scope) throws NoSuchMethodException { - BaseFunction ctor = new BaseFunction(scope, ScriptableObject.getFunctionPrototype(scope)) { - @Override - public Scriptable construct(Context cx, Scriptable scope, Object[] args) { - boolean reflect = false; - if (args.length > 2) { - throw new EvaluatorException("ScriptableMap() called with too many arguments"); - } if (args.length == 2) { - reflect = ScriptRuntime.toBoolean(args[1]); - } - return new ScriptableMap(scope, args.length == 0 ? null : args[0], reflect); - } - @Override - public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { - return construct(cx, scope, args); - } - }; - ScriptableObject.defineProperty(scope, CLASSNAME, ctor, - ScriptableObject.DONTENUM | ScriptableObject.READONLY); - } - - @SuppressWarnings("unchecked") - private ScriptableMap(Scriptable scope, Object obj, boolean reflect) { - this.parent = scope; - this.reflect = reflect; - if (obj instanceof Wrapper) { - obj = ((Wrapper) obj).unwrap(); - } - if (obj instanceof Map) { - this.map = (Map) obj; - } else if (obj == null || obj == Undefined.instance) { - this.map = new HashMap(); - } else if (obj instanceof Scriptable) { - this.map = new HashMap(); - Scriptable s = (Scriptable) obj; - Object[] ids = s.getIds(); - for (Object id: ids) { - if (id instanceof String) { - map.put(id, s.get((String)id, s)); - } else if (id instanceof Number) { - map.put(id, s.get(((Number)id).intValue(), s)); - } - } - } else { - throw new EvaluatorException("Invalid argument to ScriptableMap(): " + obj); - } - this.javaObject = this.map; - this.staticType = this.map.getClass(); - initMembers(); - initPrototype(scope); - - } - - public ScriptableMap(Scriptable scope, Map map) { - super(scope, map, map.getClass()); - this.map = map; - initPrototype(scope); - } - - /** - * Set the prototype to the Object prototype so we can use object methods such as - * getOwnPropertyNames, hasOwnProperty, keys etc. - * @param scope the global scope for looking up the Object constructor - */ - protected void initPrototype(Scriptable scope) { - Scriptable objectProto = ScriptableObject.getClassPrototype(scope, "Object"); - if (objectProto != null) { - this.setPrototype(objectProto); - } - } - - public Object get(String name, Scriptable start) { - if (map == null || (reflect && super.has(name, start))) { - return super.get(name, start); - } - return getInternal(name); - } - - public Object get(int index, Scriptable start) { - if (map == null) { - return super.get(index, start); - } - return getInternal(index); - } - - private Object getInternal(Object key) { - Object value = map.get(key); - if (value == null) { - return Scriptable.NOT_FOUND; - } - return ScriptUtils.javaToJS(value, getParentScope()); - } - - public boolean has(String name, Scriptable start) { - if (map == null || (reflect && super.has(name, start))) { - return super.has(name, start); - } else { - return map.containsKey(name); - } - } - - public boolean has(int index, Scriptable start) { - if (map == null) { - return super.has(index, start); - } else { - return map.containsKey(index); - } - } - - public void put(String name, Scriptable start, Object value) { - if (map == null || (reflect && super.has(name, start))) { - super.put(name, start, value); - } else { - putInternal(name, value); - } - } - - public void put(int index, Scriptable start, Object value) { - if (map == null) { - super.put(index, start, value); - } else { - putInternal(index, value); - } - } - - @SuppressWarnings("unchecked") - private void putInternal(Object key, Object value) { - try { - map.put(key, ScriptUtils.jsToJava(value)); - } catch (RuntimeException e) { - Context.throwAsScriptRuntimeEx(e); - } - } - - public void delete(String name) { - if (map != null) { - try { - map.remove(name); - } catch (RuntimeException e) { - Context.throwAsScriptRuntimeEx(e); - } - } else { - super.delete(name); - } - } - - public void delete(int index) { - if (map != null) { - try { - map.remove(index); - } catch (RuntimeException e) { - Context.throwAsScriptRuntimeEx(e); - } - } else { - super.delete(index); - } - } - - public Object[] getIds() { - if (map == null) { - return super.getIds(); - } else { - return map.keySet().toArray(); - } - } - - public String toString() { - if (map == null) - return super.toString(); - return map.toString(); - } - - public Object getDefaultValue(Class typeHint) { - return toString(); - } - - public Object unwrap() { - return map; - } - - public Map getMap() { - return map; - } - - public String getClassName() { - return CLASSNAME; - } -} diff --git a/backend/mockingbird/src/main/java/org/ringojs/wrappers/ScriptableWrapper.java b/backend/mockingbird/src/main/java/org/ringojs/wrappers/ScriptableWrapper.java deleted file mode 100644 index cebcc20e..00000000 --- a/backend/mockingbird/src/main/java/org/ringojs/wrappers/ScriptableWrapper.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2009 Hannes Wallnoefer - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.ringojs.wrappers; - -import org.mozilla.javascript.ScriptableObject; -import org.mozilla.javascript.Scriptable; - -/** - * A scriptable wrapper around a scriptable object. - */ -public class ScriptableWrapper extends ScriptableObject { - - private Scriptable wrapped, properties; - - public ScriptableWrapper() {} - - public ScriptableWrapper(Scriptable wrapped, Scriptable properties) { - this.wrapped = wrapped; - this.properties = properties; - } - - public String getClassName() { - return "ScriptableWrapper"; - } - - @Override - public Object get(String name, Scriptable start) { - if (wrapped == null) { - return super.get(name, start); - } - if (name.startsWith("super$")) { - return wrapped.get(name.substring(6), wrapped); - } - if (properties != null) { - Object value = properties.get(name, properties); - if (value != NOT_FOUND) { - return value; - } - } - return wrapped.get(name, wrapped); - } - - @Override - public void put(String name, Scriptable start, Object value) { - if (wrapped == null) { - super.put(name, this, value); - } else { - if (properties.has(name, start)) { - properties.put(name, properties, value); - } else { - wrapped.put(name, wrapped, value); - } - } - } - - @Override - public void delete(String name) { - if (wrapped == null) { - super.delete(name); - } else { - if (properties.has(name, properties)) { - properties.delete(name); - } else { - wrapped.delete(name); - } - } - } - - @Override - public boolean has(String name, Scriptable start) { - if (wrapped == null) { - return super.has(name, this); - } - return wrapped.has(name, wrapped) || properties.has(name, properties); - } -} diff --git a/backend/mockingbird/src/main/java/org/ringojs/wrappers/Storable.java b/backend/mockingbird/src/main/java/org/ringojs/wrappers/Storable.java deleted file mode 100644 index 2680d35d..00000000 --- a/backend/mockingbird/src/main/java/org/ringojs/wrappers/Storable.java +++ /dev/null @@ -1,268 +0,0 @@ -package org.ringojs.wrappers; - -import org.mozilla.javascript.*; -import org.mozilla.javascript.annotations.JSStaticFunction; -import org.mozilla.javascript.annotations.JSFunction; -import org.mozilla.javascript.annotations.JSGetter; -import org.ringojs.util.ScriptUtils; - -public class Storable extends ScriptableObject { - - private Scriptable store; - private String type; - private final boolean isPrototype; - - private Scriptable properties; - private Object key; - private Object entity; - - enum FactoryType {CONSTRUCTOR, FACTORY} - - public Storable() { - this.isPrototype = true; - } - - private Storable(Scriptable store, String type) { - this.store = store; - this.type = type; - this.isPrototype = true; - } - - private Storable(Storable prototype) { - this.store = prototype.store; - this.type = prototype.type; - this.isPrototype = false; - } - - static class FactoryFunction extends BaseFunction { - - final Storable prototype; - final FactoryType factoryType; - - FactoryFunction(Storable prototype, Scriptable scope, FactoryType factoryType) { - this.prototype = prototype; - this.factoryType = factoryType; - ScriptRuntime.setFunctionProtoAndParent(this, scope); - } - - @Override - public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { - Storable storable = new Storable(prototype); - switch (factoryType) { - case CONSTRUCTOR: - ScriptUtils.checkArguments(args, 0, 1); - Scriptable properties = ScriptUtils.getScriptableArgument(args, 0, true); - if (properties == null) { - properties = cx.newObject(scope); - } - storable.properties = properties; - break; - case FACTORY: - ScriptUtils.checkArguments(args, 1, 2); - storable.key = ScriptUtils.getObjectArgument(args, 0, false); - storable.entity = ScriptUtils.getObjectArgument(args, 1, true); - break; - } - storable.setParentScope(scope); - storable.setPrototype(prototype); - return storable; - } - - @Override - public int getArity() { - return factoryType == FactoryType.CONSTRUCTOR ? 1 : 2; - } - - @Override - public String getFunctionName() { - return prototype.getType(); - } - - @Override - public int getLength() { - return getArity(); - } - } - - @JSStaticFunction - public static Scriptable defineEntity(Scriptable store, String type, Object mapping) - throws NoSuchMethodException { - int attr = DONTENUM | PERMANENT | READONLY; - Scriptable scope = ScriptRuntime.getTopCallScope(Context.getCurrentContext()); - Storable prototype = new Storable(store, type); - prototype.setParentScope(scope); - prototype.setPrototype(ScriptableObject.getClassPrototype(scope, "Storable")); - // create the constructor, visible to the application - BaseFunction ctor = new FactoryFunction(prototype, scope, FactoryType.CONSTRUCTOR); - ctor.setImmunePrototypeProperty(prototype); - defineProperty(prototype, "constructor", ctor, attr); - // create the factory function, visible to the store implementation - BaseFunction factory = new FactoryFunction(prototype, scope, FactoryType.FACTORY); - ScriptableObject.defineProperty(ctor, "createInstance", factory, attr); - if (mapping != Undefined.instance) { - ctor.defineProperty("mapping", mapping, attr); - factory.defineProperty("mapping", mapping, attr); - } - return ctor; - } - - public String getClassName() { - return "Storable"; - } - - /** - * Custom == operator. - * Must return {@link org.mozilla.javascript.Scriptable#NOT_FOUND} if this object does not - * have custom equality operator for the given value, - * Boolean.TRUE if this object is equivalent to value, - * Boolean.FALSE if this object is not equivalent to - * value. - * - * The default implementation returns Boolean.TRUE - * if this == value or {@link org.mozilla.javascript.Scriptable#NOT_FOUND} otherwise. - * It indicates that by default custom equality is available only if - * value is this in which case true is returned. - */ - @Override - protected Object equivalentValues(Object value) { - if (this == value) { - return Boolean.TRUE; - } - if (value instanceof Storable && isPersistent()) { - Storable s = (Storable) value; - return invokeStoreMethod("equalKeys", key, s.key); - } - return NOT_FOUND; - } - - @JSFunction - public void save(Object transaction) { - if (!isPrototype) { - if (entity == null) { - entity = invokeStoreMethod("getEntity", type, properties != null ? properties : key); - } - if (transaction == Undefined.instance) { - invokeStoreMethod("save", properties, entity); - } else { - invokeStoreMethod("save", properties, entity, transaction); - } - } - } - - @JSFunction("remove") - public void jsremove(Object transaction) { - if (!isPrototype && isPersistent()) { - if (key == null) { - key = invokeStoreMethod("getKey", type, entity); - } - if (transaction == Undefined.instance) { - invokeStoreMethod("remove", key); - } else { - invokeStoreMethod("remove", key, transaction); - } - } - } - - @JSGetter("_key") - public Object getKey() { - if (!isPrototype && isPersistent()) { - if (key == null) { - key = invokeStoreMethod("getKey", type, entity); - } - return key; - } - return Undefined.instance; - } - - @JSGetter("_id") - public Object getId() { - Object k = getKey(); - if (k != Undefined.instance) { - return invokeStoreMethod("getId", k); - } - return Undefined.instance; - } - - @Override - public boolean has(String name, Scriptable start) { - if (super.has(name, this)) { - return true; - } - if (isPrototype) { - return super.has(name, start); - } - if (properties == null && isPersistent()) { - properties = loadProperties(); - } - return properties != null && properties.has(name, properties); - } - - @Override - public Object get(String name, Scriptable start) { - if (isPrototype || super.has(name, this)) { - return super.get(name, start); - } - if (properties == null && isPersistent()) { - properties = loadProperties(); - } - return properties == null ? Scriptable.NOT_FOUND : properties.get(name, properties); - } - - @Override - public void put(String name, Scriptable start, Object value) { - if (isPrototype || super.has(name, this)) { - super.put(name, start, value); - } else { - if (properties == null) { - properties = loadProperties(); - } - properties.put(name, properties, value); - } - } - - @Override - public void delete(String name) { - if (isPrototype || super.has(name, this)) { - super.delete(name); - } else { - if (properties == null) { - properties = loadProperties(); - } - properties.put(name, properties, null); - } - } - - public Object[] getIds() { - if (isPrototype) { - return super.getIds(); - } - if (properties == null) { - properties = loadProperties(); - } - return properties.getIds(); - } - - public String getType() { - return type; - } - - private boolean isPersistent() { - return key != null || entity != null; - } - - private Scriptable loadProperties() { - if (entity == null) { - entity = invokeStoreMethod("getEntity", type, key); - } - return (Scriptable) invokeStoreMethod("getProperties", store, entity); - } - - private Object invokeStoreMethod(String method, Object... args) { - Object value = ScriptableObject.getProperty(store, method); - if (value instanceof Callable) { - return ((Callable) value).call(Context.getCurrentContext(), getParentScope(), store, args); - } - throw new RuntimeException("Store does not implement '" + method + "' method"); - } - -} diff --git a/backend/mockingbird/src/main/java/org/ringojs/wrappers/Stream.java b/backend/mockingbird/src/main/java/org/ringojs/wrappers/Stream.java deleted file mode 100644 index e89213b8..00000000 --- a/backend/mockingbird/src/main/java/org/ringojs/wrappers/Stream.java +++ /dev/null @@ -1,243 +0,0 @@ -package org.ringojs.wrappers; - -import org.mozilla.javascript.*; -import org.mozilla.javascript.annotations.JSFunction; -import org.mozilla.javascript.annotations.JSConstructor; -import org.mozilla.javascript.annotations.JSGetter; -import org.ringojs.util.ScriptUtils; - -import java.io.InputStream; -import java.io.OutputStream; -import java.io.IOException; - -/** - *

A CommonJS-compliant wrapper around a Java input or output stream. To - * register Stream as a host object in Rhino call the defineClass() function - * with this class.

- * - *
defineClass(org.ringojs.wrappers.Stream);
- * - *

To create an Stream wrapper around an instance of java.io.InputStream - * or java.io.OutputStream call the constructor with the stream as argument:

- * - *
var io = new Stream(javaInputStream);
- * - *

When passed to a Java method that expects an input or output stream, Stream wrappers - * are automatically unwrapped. use the {@link #unwrap()} method to explicitly get the - * wrapped stream.

- */ -public class Stream extends ScriptableObject implements Wrapper { - - private InputStream input; - private OutputStream output; - private boolean closed = false; - - private final static String CLASSNAME = "Stream"; - - public Stream() { - input = null; - output = null; - } - - public Stream(Scriptable scope, Object arg1, Object arg2) { - super(scope, ScriptUtils.getClassOrObjectProto(scope, CLASSNAME)); - init(arg1, arg2); - } - - @JSConstructor - public void init(Object arg1, Object arg2) { - setStream(arg1); - setStream(arg2); - } - - private void setStream(Object arg) { - if (arg instanceof Wrapper) { - arg = ((Wrapper) arg).unwrap(); - } - if (arg instanceof InputStream) { - input = (InputStream) arg; - } else if (arg instanceof OutputStream) { - output = (OutputStream) arg; - } else if (arg != Undefined.instance && arg != null) { - throw ScriptRuntime.constructError("Error", "Unsupported argument: " + arg); - } - } - - @JSFunction - public boolean readable() { - return input != null; - } - - @JSFunction - public boolean writable() { - return output != null; - } - - @JSFunction - public boolean seekable() { - return false; - } - - @JSFunction - public Object read(Object limit) { - if (input == null) { - throw ScriptRuntime.constructError("Error", "no input stream"); - } - int max = limit == Undefined.instance ? -1 : ScriptRuntime.toInt32(limit); - Scriptable scope = ScriptableObject.getTopLevelScope(this); - if (max > -1) { - try { - byte[] bytes = new byte[max]; - int read = input.read(bytes); - return read > -1 ? - new Binary(scope, Binary.Type.ByteString, bytes, 0, read) : - new Binary(scope, Binary.Type.ByteString, 0); - } catch (IOException iox) { - throw new WrappedException(iox); - } - } else { - byte[] buffer = new byte[8192]; - int read, count = 0; - try { - while ((read = input.read(buffer, count, buffer.length - count)) > -1) { - count += read; - if (count == buffer.length) { - byte[] b = new byte[buffer.length * 2]; - System.arraycopy(buffer, 0, b, 0, count); - buffer = b; - } - } - return count > -1 ? - new Binary(scope, Binary.Type.ByteString, buffer, 0, count) : - new Binary(scope, Binary.Type.ByteString, 0); - } catch (IOException iox) { - throw ScriptRuntime.constructError("Error", "Error reading from input stream: " + iox); - } - } - } - - @JSFunction - public int readInto(Binary bytes, Object start, Object end) { - if (input == null) { - throw ScriptRuntime.constructError("Error", "no input stream"); - } else if (bytes == Undefined.instance || bytes == null) { - throw ScriptRuntime.constructError("Error", "readInto called without Binary argument"); - } else if (bytes.getType() != Binary.Type.ByteArray) { - throw ScriptRuntime.constructError("Error", "argument to readInto must be ByteArray"); - } - int from = ScriptUtils.toInt(start, 0); - int to = ScriptUtils.toInt(end, bytes.getLength()); - try { - byte[] b = bytes.getBytes(); - return input.read(b, from, to - from); - } catch (IOException iox) { - throw new WrappedException(iox); - } - } - - @JSFunction - public void write(Object arg, Object start, Object end) { - if (arg instanceof Wrapper) { - arg = ((Wrapper) arg).unwrap(); - } - byte[] bytes; - if (arg instanceof Binary) { - bytes = ((Binary) arg).getBytes(); - } else if (arg instanceof byte[]) { - bytes = (byte[]) arg; - } else if (arg instanceof String) { - // TODO this is for narwhal compatibility only - System.err.println("Warning: binary write called with string argument. Using default encoding."); - bytes = ((String) arg).getBytes(); - } else { - throw Context.reportRuntimeError("write called with illegal argument: " + arg); - } - if (output == null) { - throw ScriptRuntime.constructError("Error", "no output stream"); - } - int from = start == Undefined.instance ? 0 : ScriptRuntime.toInt32(start); - int to = end == Undefined.instance ? bytes.length : ScriptRuntime.toInt32(end); - try { - output.write(bytes, from, to - from); - } catch (IOException iox) { - throw Context.throwAsScriptRuntimeEx(iox); - } - } - - @JSFunction - public void flush() { - if (output == null) { - throw ScriptRuntime.constructError("Error", "no output stream"); - } - try { - output.flush(); - } catch (IOException iox) { - throw new WrappedException(iox); - } - } - - @JSFunction - public int skip(int num) { - try { - if (input != null) { - return (int) input.skip(num); - } else { - throw Context.reportRuntimeError( - "skip() invoked on non-readable Stream"); - } - } catch (IOException iox) { - throw new WrappedException(iox); - } - } - - @JSFunction - public void close() { - try { - if (output != null) { - output.close(); - } - if (input != null) { - input.close(); - } - closed = true; - } catch (IOException iox) { - throw new WrappedException(iox); - } - } - - @JSFunction - public boolean closed() { - return closed; - } - - @JSFunction("unwrap") - public Object jsunwrap() { - return new NativeJavaObject(getParentScope(), unwrap(), null); - } - - @JSGetter - public Object getInputStream() { - return input == null ? null : new NativeJavaObject(getParentScope(), input, null); - } - - @JSGetter - public Object getOutputStream() { - return output == null ? null : new NativeJavaObject(getParentScope(), output, null); - } - - /** - * Unwrap the object by returning the wrapped value. - * @return a wrapped value - */ - public Object unwrap() { - return input != null ? input : output; - } - - /** - * Return the name of the class. - * @return the class name - */ - public String getClassName() { - return CLASSNAME; - } -} diff --git a/backend/mockingbird/src/main/java/org/ringojs/wrappers/package.html b/backend/mockingbird/src/main/java/org/ringojs/wrappers/package.html deleted file mode 100644 index 8cbcf095..00000000 --- a/backend/mockingbird/src/main/java/org/ringojs/wrappers/package.html +++ /dev/null @@ -1,6 +0,0 @@ - - - -A collection of JavaScript wrappers and host objects. - - diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/ClassAccessRule.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/ClassAccessRule.scala new file mode 100644 index 00000000..8cef1aa4 --- /dev/null +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/ClassAccessRule.scala @@ -0,0 +1,13 @@ +package ru.tinkoff.tcb.utils.sandboxing + +sealed trait ClassAccessRule extends (String => Boolean) + +object ClassAccessRule { + case class Exact(className: String) extends ClassAccessRule { + override def apply(arg: String): Boolean = arg == className + } + + case class StartsWith(prefix: String) extends ClassAccessRule { + override def apply(arg: String): Boolean = arg.startsWith(prefix) + } +} diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/CounterContext.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/CounterContext.scala deleted file mode 100644 index 54838981..00000000 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/CounterContext.scala +++ /dev/null @@ -1,14 +0,0 @@ -package ru.tinkoff.tcb.utils.sandboxing - -import scala.concurrent.duration.Deadline - -import org.mozilla.javascript.Context -import org.mozilla.javascript.ContextFactory - -class CounterContext(factory: ContextFactory) extends Context(factory) { - var deadline: Deadline = Deadline.now -} - -object CounterContext { - final val InstructionSteps = 10000 -} diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/Exceptions.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/Exceptions.scala deleted file mode 100644 index 48dc7bd7..00000000 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/Exceptions.scala +++ /dev/null @@ -1,5 +0,0 @@ -package ru.tinkoff.tcb.utils.sandboxing - -class ScriptCPUAbuseException extends RuntimeException - -class ScriptDurationException extends RuntimeException diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/GraalJsSandbox.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/GraalJsSandbox.scala new file mode 100644 index 00000000..f99be0be --- /dev/null +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/GraalJsSandbox.scala @@ -0,0 +1,49 @@ +package ru.tinkoff.tcb.utils.sandboxing + +import scala.reflect.ClassTag +import scala.reflect.classTag +import scala.util.Try +import scala.util.Using + +import org.graalvm.polyglot.* + +import ru.tinkoff.tcb.utils.instances.predicate.or.* + +class GraalJsSandbox(classAccessRules: List[ClassAccessRule] = GraalJsSandbox.DefaultAccess) { + private val accessRule = classAccessRules.asInstanceOf[List[String => Boolean]].combineAll + + def eval[T: ClassTag](code: String, environment: Map[String, Any] = Map.empty): Try[T] = + Using( + Context + .newBuilder("js") + .allowHostAccess(HostAccess.ALL) + .allowHostClassLookup((t: String) => accessRule(t)) + .build() + ) { context => + context.getBindings("js").pipe { bindings => + for ((key, value) <- environment) + bindings.putMember(key, value) + } + context.eval("js", code).as(classTag[T].runtimeClass.asInstanceOf[Class[T]]) + } +} + +object GraalJsSandbox { + val DefaultAccess: List[ClassAccessRule] = List( + ClassAccessRule.StartsWith("java.lang.Byte"), + ClassAccessRule.StartsWith("java.lang.Boolean"), + ClassAccessRule.StartsWith("java.lang.Double"), + ClassAccessRule.StartsWith("java.lang.Float"), + ClassAccessRule.StartsWith("java.lang.Integer"), + ClassAccessRule.StartsWith("java.lang.Long"), + ClassAccessRule.StartsWith("java.lang.Math"), + ClassAccessRule.StartsWith("java.lang.Short"), + ClassAccessRule.StartsWith("java.lang.String"), + ClassAccessRule.StartsWith("java.math.BigDecimal"), + ClassAccessRule.StartsWith("java.math.BigInteger"), + ClassAccessRule.StartsWith("java.util.List"), + ClassAccessRule.StartsWith("java.util.Map"), + ClassAccessRule.StartsWith("java.util.Random"), + ClassAccessRule.StartsWith("java.util.Set") + ) +} diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandbox.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandbox.scala deleted file mode 100644 index 246e68b9..00000000 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandbox.scala +++ /dev/null @@ -1,29 +0,0 @@ -package ru.tinkoff.tcb.utils.sandboxing - -import scala.concurrent.duration.FiniteDuration -import scala.reflect.ClassTag -import scala.reflect.classTag - -import org.mozilla.javascript.Context - -class RhinoJsSandbox( - maxRunTime: Option[FiniteDuration] = None, - allowedClasses: List[String] = Nil -) { - private val contextFactory = new SafeContextFactory(maxRunTime, allowedClasses) - - def eval[T: ClassTag](code: String, environment: Map[String, Any] = Map.empty): T = { - val ctx = contextFactory.enterContext() - - try { - val scope = ctx.initStandardObjects() - for ((key, value) <- environment) - scope.put(key, scope, Context.javaToJS(value, scope, ctx)) - - val result = ctx.evaluateString(scope, code, "virtual", 1, null) - - Context.jsToJava(result, classTag[T].runtimeClass).asInstanceOf[T] - } finally - Context.exit() - } -} diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeClassShutter.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeClassShutter.scala deleted file mode 100644 index dec9069b..00000000 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeClassShutter.scala +++ /dev/null @@ -1,15 +0,0 @@ -package ru.tinkoff.tcb.utils.sandboxing - -import org.mozilla.javascript.ClassShutter - -class SafeClassShutter(userAllowedClasses: Set[String]) extends ClassShutter { - override def visibleToScripts(fullClassName: String): Boolean = - fullClassName.startsWith("adapter") || - allowedClasses(fullClassName) || - userAllowedClasses(fullClassName) - - private val allowedClasses = Set( - "scala.collection.convert.JavaCollectionWrappers$MapWrapper", - "scala.collection.convert.JavaCollectionWrappers$SeqWrapper" - ) -} diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeContextFactory.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeContextFactory.scala deleted file mode 100644 index cac318cb..00000000 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SafeContextFactory.scala +++ /dev/null @@ -1,49 +0,0 @@ -package ru.tinkoff.tcb.utils.sandboxing - -import java.util.concurrent.TimeUnit -import scala.concurrent.duration.FiniteDuration - -import org.mozilla.javascript.Callable -import org.mozilla.javascript.Context -import org.mozilla.javascript.ContextFactory -import org.mozilla.javascript.Scriptable - -class SafeContextFactory(maxRunTime: Option[FiniteDuration], allowedClasses: List[String]) extends ContextFactory { - override def makeContext(): Context = - new CounterContext(this).tap { cc => - cc.setLanguageVersion(Context.VERSION_ES6) - cc.setOptimizationLevel(-1) - cc.setInstructionObserverThreshold(CounterContext.InstructionSteps) - cc.setClassShutter(new SafeClassShutter(allowedClasses.to(Set))) - cc.setWrapFactory(SandboxWrapFactory) - } - - override def hasFeature(cx: Context, featureIndex: RuntimeFlags): Boolean = featureIndex match { - case Context.FEATURE_NON_ECMA_GET_YEAR | Context.FEATURE_MEMBER_EXPR_AS_FUNCTION_NAME | - Context.FEATURE_RESERVED_KEYWORD_AS_IDENTIFIER => - true - case Context.FEATURE_PARENT_PROTO_PROPERTIES => false - case _ => super.hasFeature(cx, featureIndex) - } - - override def observeInstructionCount(cx: Context, instructionCount: RuntimeFlags): Unit = { - val counter = cx.asInstanceOf[CounterContext] - - if (counter.deadline.isOverdue()) - throw new ScriptDurationException - } - - override def doTopCall( - callable: Callable, - cx: Context, - scope: Scriptable, - thisObj: Scriptable, - args: Array[AnyRef] - ): AnyRef = { - val counter = cx.asInstanceOf[CounterContext] - counter.deadline = maxRunTime.fold(minute.fromNow)(fd => fd.fromNow) - super.doTopCall(callable, cx, scope, thisObj, args) - } - - final private val minute = FiniteDuration(1, TimeUnit.MINUTES) -} diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SandboxWrapFactory.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SandboxWrapFactory.scala deleted file mode 100644 index dfd55749..00000000 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/SandboxWrapFactory.scala +++ /dev/null @@ -1,22 +0,0 @@ -package ru.tinkoff.tcb.utils.sandboxing - -import java.util.Map as JMap - -import org.mozilla.javascript.Context -import org.mozilla.javascript.Scriptable -import org.mozilla.javascript.WrapFactory -import org.ringojs.wrappers.ScriptableMap - -object SandboxWrapFactory extends WrapFactory { - override def wrapAsJavaObject( - cx: Context, - scope: Scriptable, - javaObject: AnyRef, - staticType: Class[?] - ): Scriptable = - javaObject.getClass match { - case jmap if classOf[JMap[?, ?]].isAssignableFrom(jmap) => - new ScriptableMap(scope, javaObject.asInstanceOf[JMap[?, ?]]) - case _ => super.wrapAsJavaObject(cx, scope, javaObject, staticType) - } -} diff --git a/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/sandboxing/GraalJsSandboxSpec.scala b/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/sandboxing/GraalJsSandboxSpec.scala new file mode 100644 index 00000000..1314cc13 --- /dev/null +++ b/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/sandboxing/GraalJsSandboxSpec.scala @@ -0,0 +1,27 @@ +package ru.tinkoff.tcb.utils.sandboxing + +import org.graalvm.polyglot.PolyglotException +import org.scalatest.TryValues +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +class GraalJsSandboxSpec extends AnyFunSuite with Matchers with TryValues { + private val sandbox = new GraalJsSandbox + + test("Eval simple arithmetics") { + sandbox.eval[Int]("1 + 2").success.value shouldBe 3 + } + + test("Java classes are inaccessable") { + sandbox.eval[Any]("java.lang.System.out.println('hello');").failure.exception shouldBe a[PolyglotException] + } + + test("Eval with context") { + sandbox.eval[Int]("a + b", Map("a" -> 1, "b" -> 2)).success.value shouldBe 3 + } + + test("Evaluations should not have shared data") { + sandbox.eval[Any]("a = 42;").success + sandbox.eval[Int]("a").failure.exception shouldBe a[PolyglotException] + } +} diff --git a/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandboxSpec.scala b/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandboxSpec.scala deleted file mode 100644 index 62f0576c..00000000 --- a/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/sandboxing/RhinoJsSandboxSpec.scala +++ /dev/null @@ -1,41 +0,0 @@ -package ru.tinkoff.tcb.utils.sandboxing - -import java.util.concurrent.TimeUnit -import scala.concurrent.duration.FiniteDuration - -import org.mozilla.javascript.EcmaError -import org.scalatest.funsuite.AnyFunSuite -import org.scalatest.matchers.should.Matchers - -class RhinoJsSandboxSpec extends AnyFunSuite with Matchers { - private val sandbox = new RhinoJsSandbox - - test("Eval simple arithmetics") { - sandbox.eval[Int]("1 + 2") shouldBe 3 - } - - test("Java classes are inaccessable") { - assertThrows[EcmaError] { - sandbox.eval[Int]("java.lang.System.out.println('hello');") - } - } - - test("Eval with context") { - sandbox.eval[Int]("a + b", Map("a" -> 1, "b" -> 2)) shouldBe 3 - } - - test("Run time limit test") { - val limitedSandbox = new RhinoJsSandbox(maxRunTime = Some(FiniteDuration.apply(1, TimeUnit.SECONDS))) - - assertThrows[ScriptDurationException] { - limitedSandbox.eval[Int]("while (true) { }") - } - } - - test("Evaluations should not have shared data") { - sandbox.eval[Unit]("a = 42;") - assertThrows[EcmaError] { - sandbox.eval[Int]("a") - } - } -} diff --git a/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/json/JsEvalSpec.scala b/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/json/JsEvalSpec.scala index 3f951279..55769367 100644 --- a/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/json/JsEvalSpec.scala +++ b/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/json/JsEvalSpec.scala @@ -7,52 +7,52 @@ import scala.jdk.CollectionConverters.* import io.circe.Json import io.circe.syntax.* +import org.scalatest.TryValues import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers -import ru.tinkoff.tcb.utils.sandboxing.RhinoJsSandbox +import ru.tinkoff.tcb.utils.sandboxing.ClassAccessRule +import ru.tinkoff.tcb.utils.sandboxing.GraalJsSandbox import ru.tinkoff.tcb.utils.transformation.json.js_eval.circe2js -class JsEvalSpec extends AnyFunSuite with Matchers { - private val sandbox = new RhinoJsSandbox +class JsEvalSpec extends AnyFunSuite with Matchers with TryValues { + private val sandbox = new GraalJsSandbox test("Simple expressions") { val data = Json.obj("a" := Json.obj("b" := 42, "c" := "test", "d" := 1 :: 2 :: Nil)) - val res = sandbox.eval[Int]("req.a.b", Map("req" -> data.foldWith(circe2js))) + val res = sandbox.eval[JBD]("req.a.b", Map("req" -> data.foldWith(circe2js))) - res shouldBe 42 + res.success.value shouldBe BigDecimal(42).bigDecimal val res2 = sandbox.eval[String]("req.a.c", Map("req" -> data.foldWith(circe2js))) - res2 shouldBe "test" + res2.success.value shouldBe "test" val res3 = sandbox.eval[JList[JBD]]("req.a.d", Map("req" -> data.foldWith(circe2js))) - res3.asScala should contain theSameElementsInOrderAs List(1, 2).map(JBD.valueOf(_)) + res3.success.value.asScala should contain theSameElementsInOrderAs List(1, 2).map(JBD.valueOf(_)) - val res4 = sandbox.eval[Int]("req.a.d[0]", Map("req" -> data.foldWith(circe2js))) + val res4 = sandbox.eval[JBD]("req.a.d[0]", Map("req" -> data.foldWith(circe2js))) - res4 shouldBe 1 + res4.success.value shouldBe BigDecimal(1).bigDecimal } test("JS functions") { - val aesSandbox = new RhinoJsSandbox( - allowedClasses = List( - "java.security.MessageDigest", - "java.security.MessageDigest$Delegate$CloneableDelegate", - "java.lang.String", - "java.lang.Object" - ) + val aesSandbox = new GraalJsSandbox( + classAccessRules = List( + ClassAccessRule.Exact("java.security.MessageDigest") + ) ::: GraalJsSandbox.DefaultAccess ) val etalon = MessageDigest.getInstance("SHA-1").digest("abc".getBytes) + // https://stackoverflow.com/a/22861911/3819595 val res = aesSandbox.eval[Array[Byte]]( """var md = java.security.MessageDigest.getInstance("SHA-1"); - |md.digest((new java.lang.String("abc")).getBytes());""".stripMargin + |md.digest('abc'.split('').map(c => c.charCodeAt(0)));""".stripMargin ) - res shouldBe etalon + res.success.value shouldBe etalon } } diff --git a/backend/project/Versions.scala b/backend/project/Versions.scala index 4eaf5c5e..4bac58c7 100644 --- a/backend/project/Versions.scala +++ b/backend/project/Versions.scala @@ -4,7 +4,7 @@ object Versions { val simulacrum = "0.5.4" val tapir = "1.4.0" val derevo = "0.13.0" - val graalvm = "22.2.0" + val graalvm = "22.3.0" val micrometer = "1.8.5" val glass = "0.2.1" val sttp = "3.8.8" From a6a46f07d766352d6e21d4a27365f15e85e03db0 Mon Sep 17 00:00:00 2001 From: Daniel Slapman Date: Fri, 9 Dec 2022 15:30:47 +0100 Subject: [PATCH 5/9] Re-implement builtin pseudofunctions --- .../mockingbird/src/main/resources/prelude.js | 61 +++++++++++++++++++ .../tcb/utils/sandboxing/GraalJsSandbox.scala | 42 ++++++++----- .../utils/transformation/json/package.scala | 19 ++++++ .../tcb/utils/transformation/package.scala | 1 + .../json/JsonTransformationsSpec.scala | 56 +++++++++++++++++ .../tinkoff/tcb/utils/resource/package.scala | 16 +++++ readme.md | 6 +- 7 files changed, 182 insertions(+), 19 deletions(-) create mode 100644 backend/mockingbird/src/main/resources/prelude.js create mode 100644 backend/utils/src/main/scala/ru/tinkoff/tcb/utils/resource/package.scala diff --git a/backend/mockingbird/src/main/resources/prelude.js b/backend/mockingbird/src/main/resources/prelude.js new file mode 100644 index 00000000..bb872b78 --- /dev/null +++ b/backend/mockingbird/src/main/resources/prelude.js @@ -0,0 +1,61 @@ +function randomInt(lbound, rbound) { + if (typeof rbound === "undefined") + return Math.floor(Math.random() * lbound); + var min = Math.ceil(lbound); + var max = Math.floor(rbound); + return Math.floor(Math.random() * (max - min) + min); +} + +function randomLong(lbound, rbound) { + return randomInt(lbound, rbound); +} + +function randomString(param1, param2, param3) { + if (typeof param2 === "undefined" || typeof param3 === "undefined") { + var result = ''; + var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + var charactersLength = characters.length; + for ( var i = 0; i < param1; i++ ) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; + } + + var result = ''; + var charactersLength = param1.length; + for ( var i = 0; i < randomInt(param2, param3); i++ ) { + result += param1.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +} + +function randomNumericString(length) { + return randomString('0123456789', length, length + 1); +} + +// https://stackoverflow.com/a/8809472/3819595 +function UUID() { // Public Domain/MIT + var d = new Date().getTime();//Timestamp + var d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now()*1000)) || 0;//Time in microseconds since page-load or 0 if unsupported + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random() * 16;//random number between 0 and 16 + if(d > 0){//Use timestamp until depleted + r = (d + r)%16 | 0; + d = Math.floor(d/16); + } else {//Use microseconds since page-load if supported + r = (d2 + r)%16 | 0; + d2 = Math.floor(d2/16); + } + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); + }); +} + +function now(pattern) { + var format = java.time.format.DateTimeFormatter.ofPattern(pattern); + return java.time.LocalDateTime.now().format(format); +} + +function today(pattern) { + var format = java.time.format.DateTimeFormatter.ofPattern(pattern); + return java.time.LocalDate.now().format(format); +} \ No newline at end of file diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/GraalJsSandbox.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/GraalJsSandbox.scala index f99be0be..cedb9d3a 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/GraalJsSandbox.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/GraalJsSandbox.scala @@ -9,41 +9,51 @@ import org.graalvm.polyglot.* import ru.tinkoff.tcb.utils.instances.predicate.or.* -class GraalJsSandbox(classAccessRules: List[ClassAccessRule] = GraalJsSandbox.DefaultAccess) { +class GraalJsSandbox( + classAccessRules: List[ClassAccessRule] = GraalJsSandbox.DefaultAccess, + prelude: Option[String] = None +) { private val accessRule = classAccessRules.asInstanceOf[List[String => Boolean]].combineAll + private val preludeSource = prelude.map(Source.create("js", _)) + def eval[T: ClassTag](code: String, environment: Map[String, Any] = Map.empty): Try[T] = Using( Context .newBuilder("js") .allowHostAccess(HostAccess.ALL) .allowHostClassLookup((t: String) => accessRule(t)) + .option("engine.WarnInterpreterOnly", "false") .build() ) { context => context.getBindings("js").pipe { bindings => for ((key, value) <- environment) bindings.putMember(key, value) } + preludeSource.foreach(context.eval) context.eval("js", code).as(classTag[T].runtimeClass.asInstanceOf[Class[T]]) } } object GraalJsSandbox { val DefaultAccess: List[ClassAccessRule] = List( - ClassAccessRule.StartsWith("java.lang.Byte"), - ClassAccessRule.StartsWith("java.lang.Boolean"), - ClassAccessRule.StartsWith("java.lang.Double"), - ClassAccessRule.StartsWith("java.lang.Float"), - ClassAccessRule.StartsWith("java.lang.Integer"), - ClassAccessRule.StartsWith("java.lang.Long"), - ClassAccessRule.StartsWith("java.lang.Math"), - ClassAccessRule.StartsWith("java.lang.Short"), - ClassAccessRule.StartsWith("java.lang.String"), - ClassAccessRule.StartsWith("java.math.BigDecimal"), - ClassAccessRule.StartsWith("java.math.BigInteger"), - ClassAccessRule.StartsWith("java.util.List"), - ClassAccessRule.StartsWith("java.util.Map"), - ClassAccessRule.StartsWith("java.util.Random"), - ClassAccessRule.StartsWith("java.util.Set") + ClassAccessRule.Exact("java.lang.Byte"), + ClassAccessRule.Exact("java.lang.Boolean"), + ClassAccessRule.Exact("java.lang.Double"), + ClassAccessRule.Exact("java.lang.Float"), + ClassAccessRule.Exact("java.lang.Integer"), + ClassAccessRule.Exact("java.lang.Long"), + ClassAccessRule.Exact("java.lang.Math"), + ClassAccessRule.Exact("java.lang.Short"), + ClassAccessRule.Exact("java.lang.String"), + ClassAccessRule.Exact("java.math.BigDecimal"), + ClassAccessRule.Exact("java.math.BigInteger"), + ClassAccessRule.Exact("java.time.LocalDate"), + ClassAccessRule.Exact("java.time.LocalDateTime"), + ClassAccessRule.Exact("java.time.format.DateTimeFormatter"), + ClassAccessRule.Exact("java.util.List"), + ClassAccessRule.Exact("java.util.Map"), + ClassAccessRule.Exact("java.util.Random"), + ClassAccessRule.Exact("java.util.Set") ) } diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/json/package.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/json/package.scala index f35c249c..ce080bdc 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/json/package.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/json/package.scala @@ -1,5 +1,9 @@ package ru.tinkoff.tcb.utils.transformation +import java.lang as jl +import java.math as jm +import scala.util.Failure +import scala.util.Success import scala.util.control.TailCalls import scala.util.control.TailCalls.TailRec @@ -12,6 +16,7 @@ import ru.tinkoff.tcb.utils.circe.* import ru.tinkoff.tcb.utils.circe.optics.JsonOptic import ru.tinkoff.tcb.utils.json.json2StringFolder import ru.tinkoff.tcb.utils.regex.OneOrMore +import ru.tinkoff.tcb.utils.sandboxing.GraalJsSandbox import ru.tinkoff.tcb.utils.transformation.xml.nodeTemplater package object json { @@ -107,6 +112,20 @@ package object json { .getOrElse(js) }.result + def eval2(implicit sandbox: GraalJsSandbox): Json = + transformValues { + case js @ JsonString(CodeRx(code)) => + (sandbox.eval[AnyRef](code) match { + case Success(str: String) => Option(Json.fromString(str)) + case Success(bd: jm.BigDecimal) => Option(Json.fromBigDecimal(bd)) + case Success(i: jl.Integer) => Option(Json.fromInt(i.intValue())) + case Success(l: jl.Long) => Option(Json.fromLong(l.longValue())) + case Success(other) => throw new Exception(s"${other.getClass.getCanonicalName}: $other") + case Failure(exception) => throw exception + }).getOrElse(js) + case JsonString(other) => throw new Exception(other) + }.result + def patch(values: Json, schema: Map[JsonOptic, String]): Json = jsonTemplater(values).pipe { templater => schema.foldLeft(j) { case (acc, (optic, defn)) => diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/package.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/package.scala index c3a87bb2..db16b13f 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/package.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/package.scala @@ -11,6 +11,7 @@ import ru.tinkoff.tcb.utils.time.* package object transformation { val SubstRx: Regex = """\$\{(.*?)\}""".r val FunRx: Regex = """%\{.*?\}""".r + val CodeRx: Regex = """%\{(.*?)\}""".r val RandStr: Regex = """%\{randomString\((\d+)\)\}""".r val RandAlphabetStr: Regex = """%\{randomString\(\"(.*?)\",\s*(\d+),\s*(\d+)\)\}""".r diff --git a/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/json/JsonTransformationsSpec.scala b/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/json/JsonTransformationsSpec.scala index bf7ee0bb..0c0907aa 100644 --- a/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/json/JsonTransformationsSpec.scala +++ b/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/json/JsonTransformationsSpec.scala @@ -13,6 +13,8 @@ import org.scalatest.matchers.should.Matchers import ru.tinkoff.tcb.utils.circe.* import ru.tinkoff.tcb.utils.circe.optics.JLens import ru.tinkoff.tcb.utils.circe.optics.JsonOptic +import ru.tinkoff.tcb.utils.resource.readStr +import ru.tinkoff.tcb.utils.sandboxing.GraalJsSandbox class JsonTransformationsSpec extends AnyFunSuite with Matchers with OptionValues { test("Fill template") { @@ -198,6 +200,60 @@ class JsonTransformationsSpec extends AnyFunSuite with Matchers with OptionValue noException should be thrownBy dFormatter.parse(f) } + test("JavaScript eval") { + val datePattern = "yyyy-MM-dd" + val dFormatter = DateTimeFormatter.ofPattern(datePattern) + val pattern = "yyyy-MM-dd'T'HH:mm:ss" + val formatter = DateTimeFormatter.ofPattern(pattern) + + val prelude = readStr("prelude.js") + implicit val sandbox: GraalJsSandbox = new GraalJsSandbox(prelude = Option(prelude)) + + val template = Json.obj( + "a" := "%{randomString(10)}", + "ai" := "%{randomString(\"ABCDEF1234567890\", 4, 6)}", + "b" := "%{randomInt(5)}", + "bi" := "%{randomInt(3, 8)}", + "c" := "%{randomLong(5)}", + "ci" := "%{randomLong(3, 8)}", + "d" := "%{UUID()}", + "e" := s"%{now(\"$pattern\")}", + "f" := s"%{today('$datePattern')}" + ) + + val res = template.eval2 + + (res \\ "a").headOption.flatMap(_.asString).value should have length 10 + + info((res \\ "ai").headOption.flatMap(_.asString).value) + (res \\ "ai").headOption.flatMap(_.asString).value should fullyMatch regex """[ABCDEF1234567890]{4,5}""" + + val b = (res \\ "b").headOption.flatMap(_.asNumber).flatMap(_.toInt).value + b should be >= 0 + b should be < 5 + + val bi = (res \\ "bi").headOption.flatMap(_.asNumber).flatMap(_.toInt).value + bi should be >= 3 + bi should be < 8 + + val c = (res \\ "c").headOption.flatMap(_.asNumber).flatMap(_.toLong).value + c should be >= 0L + c should be < 5L + + val ci = (res \\ "ci").headOption.flatMap(_.asNumber).flatMap(_.toLong).value + ci should be >= 3L + ci should be < 8L + + val d = (res \\ "d").headOption.flatMap(_.asString).value + noException should be thrownBy UUID.fromString(d) + + val e = (res \\ "e").headOption.flatMap(_.asString).value + noException should be thrownBy formatter.parse(e) + + val f = (res \\ "f").headOption.flatMap(_.asString).value + noException should be thrownBy dFormatter.parse(f) + } + test("Formatted eval") { val template = Json.obj( "fmt" := "%{randomInt(10)}: %{randomLong(10)} | %{randomString(12)}" diff --git a/backend/utils/src/main/scala/ru/tinkoff/tcb/utils/resource/package.scala b/backend/utils/src/main/scala/ru/tinkoff/tcb/utils/resource/package.scala new file mode 100644 index 00000000..56ce9b56 --- /dev/null +++ b/backend/utils/src/main/scala/ru/tinkoff/tcb/utils/resource/package.scala @@ -0,0 +1,16 @@ +package ru.tinkoff.tcb.utils + +import scala.io.Source +import scala.util.Using + +package object resource { + def getResPath(fileName: String): String = getClass.getResource(s"/$fileName").getPath + + def readBytes(fileName: String): Array[Byte] = { + val path = getResPath(fileName) + import java.nio.file.{Files, Paths} + Files.readAllBytes(Paths.get(path)) + } + + def readStr(fileName: String): String = Using.resource(Source.fromFile(getResPath(fileName)))(_.mkString) +} diff --git a/readme.md b/readme.md index c6d8f7cb..b2d36532 100644 --- a/readme.md +++ b/readme.md @@ -163,9 +163,9 @@ State аккумулятивно дописывается. Разрешено п * `%{randomInt(m,n)}` - подстановка случайного Int в диапазоне [m, n) * `%{randomLong(n)}` - подстановка случайного Long в диапазоне [0, n) * `%{randomLong(m,n)}` - подстановка случайного Long в диапазоне [m, n) -* `%{UUID}` - подстановка случайного UUID -* `%{now(yyyy-MM-dd'T'HH:mm:ss)}` - текущее время в заданном [формате](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) -* `%{today(yyyy-MM-dd)}` - текущая дата в заданном [формате](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) +* `%{UUID()}` - подстановка случайного UUID +* `%{now("yyyy-MM-dd'T'HH:mm:ss")}` - текущее время в заданном [формате](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) +* `%{today("yyyy-MM-dd")}` - текущая дата в заданном [формате](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) Можно определять строки со сложным форматом: `%{randomInt(10)}: %{randomLong(10)} | %{randomString(12)}`, поддерживаются все псевдофункции из списка выше From 7548ee64f42fbcf63d6b21a04ebf682b74ba67b3 Mon Sep 17 00:00:00 2001 From: Daniel Slapman Date: Sun, 11 Dec 2022 13:26:06 +0100 Subject: [PATCH 6/9] Allow port 8228 for local development --- backend/mockingbird/src/main/resources/application.conf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/mockingbird/src/main/resources/application.conf b/backend/mockingbird/src/main/resources/application.conf index 9b54efc3..50229711 100644 --- a/backend/mockingbird/src/main/resources/application.conf +++ b/backend/mockingbird/src/main/resources/application.conf @@ -19,7 +19,8 @@ ru.tinkoff.tcb { port = 8228 allowedOrigins = [ "http://localhost", - "http://localhost:3000" + "http://localhost:3000", + "http://localhost:8228" ] } From 6a2c6cc2fca3f240c9dd2280361083d2e662c893 Mon Sep 17 00:00:00 2001 From: Daniel Slapman Date: Sun, 11 Dec 2022 20:03:13 +0100 Subject: [PATCH 7/9] Replace pseudofunction mechanism with JS sandboxing --- .../tinkoff/tcb/mockingbird/Mockingbird.scala | 6 +- .../mockingbird/api/PublicApiHandler.scala | 5 +- .../mockingbird/config/Configuration.scala | 4 +- .../mockingbird/grpc/GrpcRequestHandler.scala | 7 ++- .../mockingbird/scenario/ScenarioEngine.scala | 5 +- .../utils/sandboxing/ClassAccessRule.scala | 13 ---- .../tcb/utils/sandboxing/GraalJsSandbox.scala | 56 +++++++++-------- .../transformation/json/js_eval/package.scala | 13 +++- .../utils/transformation/json/package.scala | 44 +++---------- .../tcb/utils/transformation/package.scala | 37 ----------- .../utils/sandboxing/GraalJsSandboxSpec.scala | 4 +- .../transformation/json/JsEvalSpec.scala | 10 +-- .../json/JsonTransformationsSpec.scala | 63 +++---------------- readme.md | 2 +- 14 files changed, 85 insertions(+), 184 deletions(-) delete mode 100644 backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/ClassAccessRule.scala diff --git a/backend/mockingbird-api/src/main/scala/ru/tinkoff/tcb/mockingbird/Mockingbird.scala b/backend/mockingbird-api/src/main/scala/ru/tinkoff/tcb/mockingbird/Mockingbird.scala index 3466ec55..07d80417 100644 --- a/backend/mockingbird-api/src/main/scala/ru/tinkoff/tcb/mockingbird/Mockingbird.scala +++ b/backend/mockingbird-api/src/main/scala/ru/tinkoff/tcb/mockingbird/Mockingbird.scala @@ -21,7 +21,6 @@ import sttp.client3.armeria.zio.ArmeriaZioBackend import tofu.logging.Logging import tofu.logging.impl.ZUniversalLogging import zio.managed.* - import ru.tinkoff.tcb.mockingbird.api.AdminApiHandler import ru.tinkoff.tcb.mockingbird.api.AdminHttp import ru.tinkoff.tcb.mockingbird.api.MetricsHttp @@ -51,6 +50,8 @@ import ru.tinkoff.tcb.mockingbird.stream.EphemeralCleaner import ru.tinkoff.tcb.mockingbird.stream.EventSpawner import ru.tinkoff.tcb.mockingbird.stream.SDFetcher import ru.tinkoff.tcb.utils.metrics.makeRegistry +import ru.tinkoff.tcb.utils.resource.readStr +import ru.tinkoff.tcb.utils.sandboxing.GraalJsSandbox object Mockingbird extends scala.App { type FL = WLD & ServerConfig & PublicHttp & EventSpawner & ResourceManager & EphemeralCleaner & GrpcRequestHandler @@ -142,6 +143,7 @@ object Mockingbird extends scala.App { scopedBackend <- ArmeriaZioBackend.scopedUsingClient(webClient) } yield scopedBackend }, + (ZLayer.service[ServerConfig].project(_.sandbox) ++ ZLayer.fromZIO(ZIO.attempt(readStr("prelude.js")).map(Option(_)))) >>> GraalJsSandbox.live, mongoLayer, aesEncoder, collection(_.stub) >>> HttpStubDAOImpl.live, @@ -186,10 +188,12 @@ object Mockingbird extends scala.App { .exec(bytes) .provideSome[RequestContext]( Tracing.live, + MockingbirdConfiguration.server, MockingbirdConfiguration.mongo, mongoLayer, collection(_.state) >>> PersistentStateDAOImpl.live, collection(_.grpcStub) >>> GrpcStubDAOImpl.live, + (ZLayer.service[ServerConfig].project(_.sandbox) ++ ZLayer.fromZIO(ZIO.attempt(readStr("prelude.js")).map(Option(_)))) >>> GraalJsSandbox.live, GrpcStubResolverImpl.live, GrpcRequestHandlerImpl.live ) diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/PublicApiHandler.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/PublicApiHandler.scala index edb904d9..19464ea1 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/PublicApiHandler.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/PublicApiHandler.scala @@ -41,6 +41,7 @@ import ru.tinkoff.tcb.mockingbird.scenario.CallbackEngine import ru.tinkoff.tcb.mockingbird.scenario.ScenarioEngine import ru.tinkoff.tcb.utils.circe.optics.JsonOptic import ru.tinkoff.tcb.utils.regex.* +import ru.tinkoff.tcb.utils.sandboxing.GraalJsSandbox import ru.tinkoff.tcb.utils.transformation.json.* import ru.tinkoff.tcb.utils.transformation.string.* import ru.tinkoff.tcb.utils.transformation.xml.* @@ -54,6 +55,7 @@ final class PublicApiHandler( stateDAO: PersistentStateDAO[Task], resolver: StubResolver, engine: CallbackEngine, + implicit val jsSandbox: GraalJsSandbox, private val httpBackend: SttpBackend[Task, ?], proxyConfig: ProxyConfig ) { @@ -304,8 +306,9 @@ object PublicApiHandler { ssd <- ZIO.service[PersistentStateDAO[Task]] resolver <- ZIO.service[StubResolver] engine <- ZIO.service[ScenarioEngine] + jsSandbox <- ZIO.service[GraalJsSandbox] sttpClient <- ZIO.service[SttpBackend[Task, Any]] proxyCfg <- ZIO.service[ProxyConfig] - } yield new PublicApiHandler(hsd, ssd, resolver, engine, sttpClient, proxyCfg) + } yield new PublicApiHandler(hsd, ssd, resolver, engine, jsSandbox, sttpClient, proxyCfg) } } diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/config/Configuration.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/config/Configuration.scala index 470351da..e45177f8 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/config/Configuration.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/config/Configuration.scala @@ -8,7 +8,9 @@ import net.ceedubs.ficus.Ficus.* import net.ceedubs.ficus.readers.ArbitraryTypeReader.* import net.ceedubs.ficus.readers.EnumerationReader.* -case class ServerConfig(interface: String, port: Int, allowedOrigins: Seq[String], healthCheckRoute: Option[String]) +case class JsSandboxConfig(allowedClasses: Set[String] = Set()) + +case class ServerConfig(interface: String, port: Int, allowedOrigins: Seq[String], healthCheckRoute: Option[String], sandbox: JsSandboxConfig) case class SecurityConfig(secret: String) diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/grpc/GrpcRequestHandler.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/grpc/GrpcRequestHandler.scala index b6b262f1..156a55a7 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/grpc/GrpcRequestHandler.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/grpc/GrpcRequestHandler.scala @@ -18,13 +18,15 @@ import ru.tinkoff.tcb.mockingbird.model.FillResponse import ru.tinkoff.tcb.mockingbird.model.GProxyResponse import ru.tinkoff.tcb.mockingbird.model.PersistentState import ru.tinkoff.tcb.mockingbird.model.Scope +import ru.tinkoff.tcb.utils.sandboxing.GraalJsSandbox import ru.tinkoff.tcb.utils.transformation.json.* trait GrpcRequestHandler { def exec(bytes: Array[Byte]): RIO[WLD & RequestContext, Array[Byte]] } -class GrpcRequestHandlerImpl(stubResolver: GrpcStubResolver) extends GrpcRequestHandler { +class GrpcRequestHandlerImpl(stubResolver: GrpcStubResolver, implicit val jsSandbox: GraalJsSandbox) + extends GrpcRequestHandler { override def exec(bytes: Array[Byte]): RIO[WLD & RequestContext, Array[Byte]] = for { context <- ZIO.service[RequestContext] @@ -86,7 +88,8 @@ class GrpcRequestHandlerImpl(stubResolver: GrpcStubResolver) extends GrpcRequest } object GrpcRequestHandlerImpl { - val live: URLayer[GrpcStubResolver, GrpcRequestHandler] = ZLayer.fromFunction(new GrpcRequestHandlerImpl(_)) + val live: URLayer[GrpcStubResolver & GraalJsSandbox, GrpcRequestHandlerImpl] = + ZLayer.fromFunction(new GrpcRequestHandlerImpl(_, _)) } object GrpcRequestHandler { diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/scenario/ScenarioEngine.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/scenario/ScenarioEngine.scala index 4eb69f9b..1d3da61f 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/scenario/ScenarioEngine.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/scenario/ScenarioEngine.scala @@ -42,6 +42,7 @@ import ru.tinkoff.tcb.mockingbird.model.XMLCallbackRequest import ru.tinkoff.tcb.mockingbird.model.XmlOutput import ru.tinkoff.tcb.mockingbird.stream.SDFetcher import ru.tinkoff.tcb.utils.id.SID +import ru.tinkoff.tcb.utils.sandboxing.GraalJsSandbox import ru.tinkoff.tcb.utils.transformation.json.* import ru.tinkoff.tcb.utils.transformation.string.* import ru.tinkoff.tcb.utils.transformation.xml.* @@ -62,6 +63,7 @@ final class ScenarioEngine( stateDAO: PersistentStateDAO[Task], resolver: ScenarioResolver, fetcher: SDFetcher, + implicit val jsSandbox: GraalJsSandbox, private val httpBackend: SttpBackend[Task, ?] ) extends CallbackEngine { private val log = MDCLogging.`for`[WLD](this) @@ -222,7 +224,8 @@ object ScenarioEngine { psd <- ZIO.service[PersistentStateDAO[Task]] resolver <- ZIO.service[ScenarioResolver] fetcher <- ZIO.service[SDFetcher] + jsSandbox <- ZIO.service[GraalJsSandbox] sttpClient <- ZIO.service[SttpBackend[Task, Any]] - } yield new ScenarioEngine(sd, psd, resolver, fetcher, sttpClient) + } yield new ScenarioEngine(sd, psd, resolver, fetcher, jsSandbox, sttpClient) } } diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/ClassAccessRule.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/ClassAccessRule.scala deleted file mode 100644 index 8cef1aa4..00000000 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/ClassAccessRule.scala +++ /dev/null @@ -1,13 +0,0 @@ -package ru.tinkoff.tcb.utils.sandboxing - -sealed trait ClassAccessRule extends (String => Boolean) - -object ClassAccessRule { - case class Exact(className: String) extends ClassAccessRule { - override def apply(arg: String): Boolean = arg == className - } - - case class StartsWith(prefix: String) extends ClassAccessRule { - override def apply(arg: String): Boolean = arg.startsWith(prefix) - } -} diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/GraalJsSandbox.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/GraalJsSandbox.scala index cedb9d3a..694b7ced 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/GraalJsSandbox.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/GraalJsSandbox.scala @@ -7,22 +7,21 @@ import scala.util.Using import org.graalvm.polyglot.* -import ru.tinkoff.tcb.utils.instances.predicate.or.* +import ru.tinkoff.tcb.mockingbird.config.JsSandboxConfig class GraalJsSandbox( - classAccessRules: List[ClassAccessRule] = GraalJsSandbox.DefaultAccess, + jsSandboxConfig: JsSandboxConfig, prelude: Option[String] = None ) { - private val accessRule = classAccessRules.asInstanceOf[List[String => Boolean]].combineAll - - private val preludeSource = prelude.map(Source.create("js", _)) + private val allowedClasses = GraalJsSandbox.DefaultAccess ++ jsSandboxConfig.allowedClasses + private val preludeSource = prelude.map(Source.create("js", _)) def eval[T: ClassTag](code: String, environment: Map[String, Any] = Map.empty): Try[T] = Using( Context .newBuilder("js") .allowHostAccess(HostAccess.ALL) - .allowHostClassLookup((t: String) => accessRule(t)) + .allowHostClassLookup((t: String) => allowedClasses(t)) .option("engine.WarnInterpreterOnly", "false") .build() ) { context => @@ -36,24 +35,31 @@ class GraalJsSandbox( } object GraalJsSandbox { - val DefaultAccess: List[ClassAccessRule] = List( - ClassAccessRule.Exact("java.lang.Byte"), - ClassAccessRule.Exact("java.lang.Boolean"), - ClassAccessRule.Exact("java.lang.Double"), - ClassAccessRule.Exact("java.lang.Float"), - ClassAccessRule.Exact("java.lang.Integer"), - ClassAccessRule.Exact("java.lang.Long"), - ClassAccessRule.Exact("java.lang.Math"), - ClassAccessRule.Exact("java.lang.Short"), - ClassAccessRule.Exact("java.lang.String"), - ClassAccessRule.Exact("java.math.BigDecimal"), - ClassAccessRule.Exact("java.math.BigInteger"), - ClassAccessRule.Exact("java.time.LocalDate"), - ClassAccessRule.Exact("java.time.LocalDateTime"), - ClassAccessRule.Exact("java.time.format.DateTimeFormatter"), - ClassAccessRule.Exact("java.util.List"), - ClassAccessRule.Exact("java.util.Map"), - ClassAccessRule.Exact("java.util.Random"), - ClassAccessRule.Exact("java.util.Set") + val live: URLayer[Option[String] & JsSandboxConfig, GraalJsSandbox] = ZLayer { + for { + sandboxConfig <- ZIO.service[JsSandboxConfig] + prelude <- ZIO.service[Option[String]] + } yield new GraalJsSandbox(sandboxConfig, prelude) + } + + val DefaultAccess: Set[String] = Set( + "java.lang.Byte", + "java.lang.Boolean", + "java.lang.Double", + "java.lang.Float", + "java.lang.Integer", + "java.lang.Long", + "java.lang.Math", + "java.lang.Short", + "java.lang.String", + "java.math.BigDecimal", + "java.math.BigInteger", + "java.time.LocalDate", + "java.time.LocalDateTime", + "java.time.format.DateTimeFormatter", + "java.util.List", + "java.util.Map", + "java.util.Random", + "java.util.Set" ) } diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/json/js_eval/package.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/json/js_eval/package.scala index 0a51c3db..ff67e71e 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/json/js_eval/package.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/json/js_eval/package.scala @@ -1,6 +1,7 @@ package ru.tinkoff.tcb.utils.transformation.json -import java.lang.Boolean as JBoolean +import java.lang as jl +import java.math as jm import scala.jdk.CollectionConverters.* import io.circe.Json @@ -11,7 +12,7 @@ package object js_eval { val circe2js: Json.Folder[AnyRef] = new Json.Folder[AnyRef] { override def onNull: AnyRef = null - override def onBoolean(value: Boolean): AnyRef = JBoolean.valueOf(value) + override def onBoolean(value: Boolean): AnyRef = jl.Boolean.valueOf(value) override def onNumber(value: JsonNumber): AnyRef = value.toBigDecimal.map(_.bigDecimal).orNull @@ -25,4 +26,12 @@ package object js_eval { .toMap .asJava } + + val fold2Json: PartialFunction[AnyRef, Json] = { + case b: jl.Boolean => Json.fromBoolean(b) + case s: String => Json.fromString(s) + case bd: jm.BigDecimal => Json.fromBigDecimal(bd) + case i: jl.Integer => Json.fromInt(i.intValue()) + case l: jl.Long => Json.fromLong(l.longValue()) + } } diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/json/package.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/json/package.scala index ce080bdc..a1314514 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/json/package.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/json/package.scala @@ -1,7 +1,5 @@ package ru.tinkoff.tcb.utils.transformation -import java.lang as jl -import java.math as jm import scala.util.Failure import scala.util.Success import scala.util.control.TailCalls @@ -10,13 +8,13 @@ import scala.util.control.TailCalls.TailRec import io.circe.Json import io.circe.JsonNumber as JNumber import kantan.xpath.* -import mouse.boolean.* import ru.tinkoff.tcb.utils.circe.* import ru.tinkoff.tcb.utils.circe.optics.JsonOptic import ru.tinkoff.tcb.utils.json.json2StringFolder import ru.tinkoff.tcb.utils.regex.OneOrMore import ru.tinkoff.tcb.utils.sandboxing.GraalJsSandbox +import ru.tinkoff.tcb.utils.transformation.json.js_eval.fold2Json import ru.tinkoff.tcb.utils.transformation.xml.nodeTemplater package object json { @@ -91,39 +89,13 @@ package object json { }.result } - def eval: Json = - transformValues { case js @ JsonString(str) => - str - .foldTemplate( - Json.fromString, - Json.fromInt, - Json.fromLong - ) - .orElse { - FunRx - .findFirstIn(str) - .isDefined - .option( - FunRx - .replaceSomeIn(str, m => m.matched.foldTemplate(identity, _.toString(), _.toString())) - ) - .map(Json.fromString) - } - .getOrElse(js) - }.result - - def eval2(implicit sandbox: GraalJsSandbox): Json = - transformValues { - case js @ JsonString(CodeRx(code)) => - (sandbox.eval[AnyRef](code) match { - case Success(str: String) => Option(Json.fromString(str)) - case Success(bd: jm.BigDecimal) => Option(Json.fromBigDecimal(bd)) - case Success(i: jl.Integer) => Option(Json.fromInt(i.intValue())) - case Success(l: jl.Long) => Option(Json.fromLong(l.longValue())) - case Success(other) => throw new Exception(s"${other.getClass.getCanonicalName}: $other") - case Failure(exception) => throw exception - }).getOrElse(js) - case JsonString(other) => throw new Exception(other) + def eval(implicit sandbox: GraalJsSandbox): Json = + transformValues { case js @ JsonString(CodeRx(code)) => + (sandbox.eval[AnyRef](code) match { + case Success(value) if fold2Json.isDefinedAt(value) => Option(fold2Json(value)) + case Success(other) => throw new Exception(s"${other.getClass.getCanonicalName}: $other") + case Failure(exception) => throw exception + }).getOrElse(js) }.result def patch(values: Json, schema: Map[JsonOptic, String]): Json = diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/package.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/package.scala index db16b13f..1ef76a13 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/package.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/package.scala @@ -1,13 +1,7 @@ package ru.tinkoff.tcb.utils -import java.time.LocalDate -import java.time.LocalDateTime -import java.util.UUID -import scala.util.Random import scala.util.matching.Regex -import ru.tinkoff.tcb.utils.time.* - package object transformation { val SubstRx: Regex = """\$\{(.*?)\}""".r val FunRx: Regex = """%\{.*?\}""".r @@ -24,35 +18,4 @@ package object transformation { val Today: Regex = """%\{today\((.*?)\)\}""".r val Now: Regex = """%\{now\((.*?)\)\}""".r - - implicit final class TemplateTransformations(private val template: String) extends AnyVal { - def foldTemplate[T](foldString: String => T, foldInt: Int => T, foldLong: Long => T): Option[T] = - template match { - case RandStr(len) => - Some(foldString(Random.alphanumeric.take(len.toInt).mkString)) - case RandAlphabetStr(alphabet, minLen, maxLen) => - Some( - foldString( - List.fill(Random.between(minLen.toInt, maxLen.toInt))(alphabet(Random.nextInt(alphabet.length))).mkString - ) - ) - case RandNumStr(len) => - Some(foldString(Seq.fill(len.toInt)(Random.nextInt(10)).mkString)) - case RandInt(max) => - Some(foldInt(Random.nextInt(max.toInt))) - case RandIntInterval(min, max) => - Some(foldInt(Random.between(min.toInt, max.toInt))) - case RandLong(max) => - Some(foldLong(Random.nextLong(max.toLong))) - case RandLongInterval(min, max) => - Some(foldLong(Random.between(min.toLong, max.toLong))) - case RandUUID() => - Some(foldString(UUID.randomUUID().toString)) - case Today(Formatter(fmt)) => - Some(foldString(LocalDate.now().format(fmt))) - case Now(Formatter(fmt)) => - Some(foldString(LocalDateTime.now().format(fmt))) - case _ => None - } - } } diff --git a/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/sandboxing/GraalJsSandboxSpec.scala b/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/sandboxing/GraalJsSandboxSpec.scala index 1314cc13..d4a0dd38 100644 --- a/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/sandboxing/GraalJsSandboxSpec.scala +++ b/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/sandboxing/GraalJsSandboxSpec.scala @@ -5,8 +5,10 @@ import org.scalatest.TryValues import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers +import ru.tinkoff.tcb.mockingbird.config.JsSandboxConfig + class GraalJsSandboxSpec extends AnyFunSuite with Matchers with TryValues { - private val sandbox = new GraalJsSandbox + private val sandbox = new GraalJsSandbox(new JsSandboxConfig()) test("Eval simple arithmetics") { sandbox.eval[Int]("1 + 2").success.value shouldBe 3 diff --git a/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/json/JsEvalSpec.scala b/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/json/JsEvalSpec.scala index 55769367..a80a47dd 100644 --- a/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/json/JsEvalSpec.scala +++ b/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/json/JsEvalSpec.scala @@ -11,12 +11,12 @@ import org.scalatest.TryValues import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers -import ru.tinkoff.tcb.utils.sandboxing.ClassAccessRule +import ru.tinkoff.tcb.mockingbird.config.JsSandboxConfig import ru.tinkoff.tcb.utils.sandboxing.GraalJsSandbox import ru.tinkoff.tcb.utils.transformation.json.js_eval.circe2js class JsEvalSpec extends AnyFunSuite with Matchers with TryValues { - private val sandbox = new GraalJsSandbox + private val sandbox = new GraalJsSandbox(JsSandboxConfig()) test("Simple expressions") { val data = Json.obj("a" := Json.obj("b" := 42, "c" := "test", "d" := 1 :: 2 :: Nil)) @@ -39,11 +39,7 @@ class JsEvalSpec extends AnyFunSuite with Matchers with TryValues { } test("JS functions") { - val aesSandbox = new GraalJsSandbox( - classAccessRules = List( - ClassAccessRule.Exact("java.security.MessageDigest") - ) ::: GraalJsSandbox.DefaultAccess - ) + val aesSandbox = new GraalJsSandbox(JsSandboxConfig(Set("java.security.MessageDigest"))) val etalon = MessageDigest.getInstance("SHA-1").digest("abc".getBytes) diff --git a/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/json/JsonTransformationsSpec.scala b/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/json/JsonTransformationsSpec.scala index 0c0907aa..d4a1c8b3 100644 --- a/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/json/JsonTransformationsSpec.scala +++ b/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/json/JsonTransformationsSpec.scala @@ -10,6 +10,7 @@ import org.scalatest.OptionValues import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers +import ru.tinkoff.tcb.mockingbird.config.JsSandboxConfig import ru.tinkoff.tcb.utils.circe.* import ru.tinkoff.tcb.utils.circe.optics.JLens import ru.tinkoff.tcb.utils.circe.optics.JsonOptic @@ -147,59 +148,6 @@ class JsonTransformationsSpec extends AnyFunSuite with Matchers with OptionValue Json.obj().substitute(Json.obj()) } - test("Simple eval") { - val datePattern = "yyyy-MM-dd" - val dFormatter = DateTimeFormatter.ofPattern(datePattern) - val pattern = "yyyy-MM-dd'T'HH:mm:ss" - val formatter = DateTimeFormatter.ofPattern(pattern) - - val template = Json.obj( - "a" := "%{randomString(10)}", - "ai" := "%{randomString(\"ABCDEF1234567890\", 4, 6)}", - "b" := "%{randomInt(5)}", - "bi" := "%{randomInt(3, 8)}", - "c" := "%{randomLong(5)}", - "ci" := "%{randomLong(3, 8)}", - "d" := "%{UUID}", - "e" := s"%{now($pattern)}", - "f" := s"%{today($datePattern)}" - ) - - template.isTemplate shouldBe true - - val res = template.eval - - (res \\ "a").headOption.flatMap(_.asString).value should have length 10 - - info((res \\ "ai").headOption.flatMap(_.asString).value) - (res \\ "ai").headOption.flatMap(_.asString).value should fullyMatch regex """[ABCDEF1234567890]{4,5}""" - - val b = (res \\ "b").headOption.flatMap(_.asNumber).flatMap(_.toInt).value - b should be >= 0 - b should be < 5 - - val bi = (res \\ "bi").headOption.flatMap(_.asNumber).flatMap(_.toInt).value - bi should be >= 3 - bi should be < 8 - - val c = (res \\ "c").headOption.flatMap(_.asNumber).flatMap(_.toLong).value - c should be >= 0L - c should be < 5L - - val ci = (res \\ "ci").headOption.flatMap(_.asNumber).flatMap(_.toLong).value - ci should be >= 3L - ci should be < 8L - - val d = (res \\ "d").headOption.flatMap(_.asString).value - noException should be thrownBy UUID.fromString(d) - - val e = (res \\ "e").headOption.flatMap(_.asString).value - noException should be thrownBy formatter.parse(e) - - val f = (res \\ "f").headOption.flatMap(_.asString).value - noException should be thrownBy dFormatter.parse(f) - } - test("JavaScript eval") { val datePattern = "yyyy-MM-dd" val dFormatter = DateTimeFormatter.ofPattern(datePattern) @@ -207,7 +155,7 @@ class JsonTransformationsSpec extends AnyFunSuite with Matchers with OptionValue val formatter = DateTimeFormatter.ofPattern(pattern) val prelude = readStr("prelude.js") - implicit val sandbox: GraalJsSandbox = new GraalJsSandbox(prelude = Option(prelude)) + implicit val sandbox: GraalJsSandbox = new GraalJsSandbox(JsSandboxConfig(), prelude = Option(prelude)) val template = Json.obj( "a" := "%{randomString(10)}", @@ -221,7 +169,7 @@ class JsonTransformationsSpec extends AnyFunSuite with Matchers with OptionValue "f" := s"%{today('$datePattern')}" ) - val res = template.eval2 + val res = template.eval (res \\ "a").headOption.flatMap(_.asString).value should have length 10 @@ -255,8 +203,11 @@ class JsonTransformationsSpec extends AnyFunSuite with Matchers with OptionValue } test("Formatted eval") { + val prelude = readStr("prelude.js") + implicit val sandbox: GraalJsSandbox = new GraalJsSandbox(JsSandboxConfig(), prelude = Option(prelude)) + val template = Json.obj( - "fmt" := "%{randomInt(10)}: %{randomLong(10)} | %{randomString(12)}" + "fmt" := "%{randomInt(10) + ': ' + randomLong(10) + ' | ' + randomString(12)}" ) template.isTemplate shouldBe true diff --git a/readme.md b/readme.md index b2d36532..6a3bd0fc 100644 --- a/readme.md +++ b/readme.md @@ -167,7 +167,7 @@ State аккумулятивно дописывается. Разрешено п * `%{now("yyyy-MM-dd'T'HH:mm:ss")}` - текущее время в заданном [формате](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) * `%{today("yyyy-MM-dd")}` - текущая дата в заданном [формате](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) -Можно определять строки со сложным форматом: `%{randomInt(10)}: %{randomLong(10)} | %{randomString(12)}`, поддерживаются все псевдофункции из списка выше +Можно определять строки со сложным форматом: `%{randomInt(10) +': ' + randomLong(10) + ' | ' + {randomString(12)}`, поддерживаются все псевдофункции из списка выше ## Резолвинг заглушек/сценариев From 7ba8ba981c42b23f720bdf90e3b61d38c29764e7 Mon Sep 17 00:00:00 2001 From: Daniel Slapman Date: Sun, 11 Dec 2022 20:24:26 +0100 Subject: [PATCH 8/9] Remove rhino dependency --- backend/build.sbt | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/build.sbt b/backend/build.sbt index a1d51cd1..53419dbc 100644 --- a/backend/build.sbt +++ b/backend/build.sbt @@ -76,7 +76,6 @@ val mockingbird = (project in file("mockingbird")) "com.github.geirolz" %% "advxml-core" % "2.5.1", "com.github.geirolz" %% "advxml-xpath" % "2.5.1", "io.estatico" %% "newtype" % "0.4.4", - "org.mozilla" % "rhino" % "1.7.14", "org.graalvm.js" % "js" % "22.3.0", "org.slf4j" % "slf4j-api" % "1.7.30" % Provided ), From 4a401e4bf243a801e569e31a80fe0521315743c2 Mon Sep 17 00:00:00 2001 From: Daniel Slapman Date: Fri, 20 Jan 2023 12:38:41 +0100 Subject: [PATCH 9/9] Re-format code --- .../scala/ru/tinkoff/tcb/mockingbird/Mockingbird.scala | 8 ++++++-- .../ru/tinkoff/tcb/mockingbird/config/Configuration.scala | 8 +++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/backend/mockingbird-api/src/main/scala/ru/tinkoff/tcb/mockingbird/Mockingbird.scala b/backend/mockingbird-api/src/main/scala/ru/tinkoff/tcb/mockingbird/Mockingbird.scala index 07d80417..b1b6c2e0 100644 --- a/backend/mockingbird-api/src/main/scala/ru/tinkoff/tcb/mockingbird/Mockingbird.scala +++ b/backend/mockingbird-api/src/main/scala/ru/tinkoff/tcb/mockingbird/Mockingbird.scala @@ -21,6 +21,7 @@ import sttp.client3.armeria.zio.ArmeriaZioBackend import tofu.logging.Logging import tofu.logging.impl.ZUniversalLogging import zio.managed.* + import ru.tinkoff.tcb.mockingbird.api.AdminApiHandler import ru.tinkoff.tcb.mockingbird.api.AdminHttp import ru.tinkoff.tcb.mockingbird.api.MetricsHttp @@ -143,7 +144,9 @@ object Mockingbird extends scala.App { scopedBackend <- ArmeriaZioBackend.scopedUsingClient(webClient) } yield scopedBackend }, - (ZLayer.service[ServerConfig].project(_.sandbox) ++ ZLayer.fromZIO(ZIO.attempt(readStr("prelude.js")).map(Option(_)))) >>> GraalJsSandbox.live, + (ZLayer.service[ServerConfig].project(_.sandbox) ++ ZLayer.fromZIO( + ZIO.attempt(readStr("prelude.js")).map(Option(_)) + )) >>> GraalJsSandbox.live, mongoLayer, aesEncoder, collection(_.stub) >>> HttpStubDAOImpl.live, @@ -193,7 +196,8 @@ object Mockingbird extends scala.App { mongoLayer, collection(_.state) >>> PersistentStateDAOImpl.live, collection(_.grpcStub) >>> GrpcStubDAOImpl.live, - (ZLayer.service[ServerConfig].project(_.sandbox) ++ ZLayer.fromZIO(ZIO.attempt(readStr("prelude.js")).map(Option(_)))) >>> GraalJsSandbox.live, + (ZLayer.service[ServerConfig].project(_.sandbox) ++ ZLayer + .fromZIO(ZIO.attempt(readStr("prelude.js")).map(Option(_)))) >>> GraalJsSandbox.live, GrpcStubResolverImpl.live, GrpcRequestHandlerImpl.live ) diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/config/Configuration.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/config/Configuration.scala index e45177f8..8c598382 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/config/Configuration.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/config/Configuration.scala @@ -10,7 +10,13 @@ import net.ceedubs.ficus.readers.EnumerationReader.* case class JsSandboxConfig(allowedClasses: Set[String] = Set()) -case class ServerConfig(interface: String, port: Int, allowedOrigins: Seq[String], healthCheckRoute: Option[String], sandbox: JsSandboxConfig) +case class ServerConfig( + interface: String, + port: Int, + allowedOrigins: Seq[String], + healthCheckRoute: Option[String], + sandbox: JsSandboxConfig +) case class SecurityConfig(secret: String)