Repository: sjbarag/brs
Pull request #441 (2020-06-11 00:03:41)

fix(stdlib): escape from value in string replace
# Change Summary
Before, we weren't escaping the `from` value in `string.replace`, which meant that `brs` was throwing an error due to an invalid `RegExp`. This PR adds an escape.


Patch:
diff --git a/src/brsTypes/components/RoSGNode.ts b/src/brsTypes/components/RoSGNode.ts
index f7a924d..b572fda 100644
--- a/src/brsTypes/components/RoSGNode.ts
+++ b/src/brsTypes/components/RoSGNode.ts
@@ -139,7 +139,7 @@ export class RoSGNode extends BrsComponent implements BrsValue, BrsIterable {
                 this.insertchildren,
             ],
             ifSGNodeFocus: [this.hasfocus, this.setfocus, this.isinfocuschain],
-            ifSGNodeDict: [this.findnode, this.issamenode, this.subtype],
+            ifSGNodeDict: [this.findnode, this.issamenode, this.subtype, this.callfunc],
         });
     }
 
@@ -325,6 +325,43 @@ export class RoSGNode extends BrsComponent implements BrsValue, BrsIterable {
         return false;
     }
 
+    /**
+     * Calls the function specified on this node.
+     */
+    private callfunc = new Callable("callfunc", {
+        signature: {
+            args: [
+                new StdlibArgument("functionname", ValueKind.String),
+                new StdlibArgument("functionargs", ValueKind.Dynamic, BrsInvalid.Instance),
+            ],
+            returns: ValueKind.Dynamic,
+        },
+        impl: (interpreter: Interpreter, functionname: BrsString, functionargs: BrsType) => {
+            // We need to search the callee's environment for this function rather than the caller's.
+            let componentDef = interpreter.environment.nodeDefMap.get(this.nodeSubtype);
+
+            // Only allow public functions (defined in the interface) to be called.
+            if (componentDef && functionname.value in componentDef.functions) {
+                return interpreter.inSubEnv(subInterpreter => {
+                    let functionToCall = subInterpreter.getCallableFunction(functionname.value);
+
+                    // Determine whether the function should get arguments are not.
+                    if (functionToCall.getFirstSatisfiedSignature([functionargs])) {
+                        return functionToCall.call(subInterpreter, functionargs);
+                    } else {
+                        return functionToCall.call(subInterpreter);
+                    }
+                }, componentDef.environment);
+            } else {
+                interpreter.stderr.write(
+                    `Warning calling function in ${this.nodeSubtype}: no function interface specified for ${functionname}`
+                );
+            }
+
+            return BrsInvalid.Instance;
+        },
+    });
+
     /** Removes all fields from the node */
     // ToDo: Built-in fields shouldn't be removed
     private clear = new Callable("clear", {
diff --git a/src/brsTypes/components/RoString.ts b/src/brsTypes/components/RoString.ts
index 355b198..e96f2df 100644
--- a/src/brsTypes/components/RoString.ts
+++ b/src/brsTypes/components/RoString.ts
@@ -252,8 +252,11 @@ export class RoString extends BrsComponent implements BrsValue, Comparable, Unbo
                 return this.intrinsic;
             }
 
+            // From Mozilla's guide to escaping regex:
+            // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping
+            let escapedFrom = from.value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
             return new BrsString(
-                this.intrinsic.value.replace(new RegExp(from.value, "g"), to.value)
+                this.intrinsic.value.replace(new RegExp(escapedFrom, "g"), to.value)
             );
         },
     });
diff --git a/src/componentprocessor/index.ts b/src/componentprocessor/index.ts
index a7571f8..437ba92 100644
--- a/src/componentprocessor/index.ts
+++ b/src/componentprocessor/index.ts
@@ -17,10 +17,18 @@ interface FieldAttributes {
     alwaysNotify?: string;
 }
 
-interface ComponentField {
+interface ComponentFields {
     [key: string]: FieldAttributes;
 }
 
+interface FunctionAttributes {
+    name: string;
+}
+
+interface ComponentFunctions {
+    [key: string]: FunctionAttributes;
+}
+
 interface NodeField {
     [id: string]: string;
 }
@@ -43,7 +51,8 @@ export class ComponentDefinition {
     // indicates whether this component hierarchy has been processed before
     // which means the fields, children, and inherited functions are correctly set
     public processed: boolean = false;
-    public fields: ComponentField = {};
+    public fields: ComponentFields = {};
+    public functions: ComponentFunctions = {};
     public children: ComponentNode[] = [];
     public scripts: ComponentScript[] = [];
     public environment: Environment | undefined;
@@ -115,7 +124,7 @@ async function processXmlTree(
                 }
             }
 
-            let inheritedFields: ComponentField = {};
+            let inheritedFields: ComponentFields = {};
             // pop the stack & build our component
             // we can safely assume nodes are valid ComponentDefinition objects
             while (inheritanceStack.length > 0) {
@@ -124,16 +133,17 @@ async function processXmlTree(
                     if (newNodeDef.processed) {
                         inheritedFields = newNodeDef.fields;
                     } else {
-                        let nodeFields = getFields(newNodeDef.xmlNode!);
+                        let nodeInterface = processInterface(newNodeDef.xmlNode!);
                         // we will get run-time error if any fields are duplicated
                         // between inherited components, but here we will retain
                         // the original value without throwing an error for simplicity
                         // TODO: throw exception when fields are duplicated.
-                        inheritedFields = { ...nodeFields, ...inheritedFields };
+                        inheritedFields = { ...nodeInterface.fields, ...inheritedFields };
 
                         // We don't actually want our type definition to have inherited fields on it.
                         // Inherited fields will get added when we create the actual component.
-                        newNodeDef.fields = nodeFields;
+                        newNodeDef.fields = nodeInterface.fields;
+                        newNodeDef.functions = nodeInterface.functions;
                         newNodeDef.processed = true;
                     }
                 }
@@ -171,16 +181,20 @@ async function processXmlTree(
 }
 
 /**
- * Returns all the fields found in the Xml node
+ * Returns all the fields and functions found in the Xml node.
  * @param node Xml node with fields
- * @return The fields parsed as ComponentField
+ * @return { fields, functions }: the fields and functions parsed as
+ * ComponentFields and ComponentFunctions respectively
  */
-function getFields(node: XmlDocument): ComponentField {
+function processInterface(
+    node: XmlDocument
+): { fields: ComponentFields; functions: ComponentFunctions } {
     let iface = node.childNamed("interface");
-    let fields: ComponentField = {};
+    let fields: ComponentFields = {};
+    let functions: ComponentFunctions = {};
 
     if (!iface) {
-        return fields;
+        return { fields, functions };
     }
 
     iface.eachChild(child => {
@@ -193,10 +207,14 @@ function getFields(node: XmlDocument): ComponentField {
                 alwaysNotify: child.attr.alwaysNotify,
                 value: child.attr.value,
             };
+        } else if (child.name === "function") {
+            functions[child.attr.name] = {
+                name: child.attr.name,
+            };
         }
     });
 
-    return fields;
+    return { fields, functions };
 }
 
 /**


Test patch:
diff --git a/test/brsTypes/components/RoString.test.js b/test/brsTypes/components/RoString.test.js
index 6f4e6d9..fe8636f 100644
--- a/test/brsTypes/components/RoString.test.js
+++ b/test/brsTypes/components/RoString.test.js
@@ -284,6 +284,18 @@ describe("RoString", () => {
                     )
                 ).toEqual(new BrsString("tossed salad and scrambled eggs"));
             });
+
+            it("escapes strings with reserved regex characters", () => {
+                let s = new RoString(new BrsString("oh baby {1}"));
+                replace = s.getMethod("replace");
+                expect(
+                    replace.call(
+                        interpreter,
+                        new BrsString("{1}"),
+                        new BrsString("I hear the blues a-callin'")
+                    )
+                ).toEqual(new BrsString("oh baby I hear the blues a-callin'"));
+            });
         });
 
         describe("trim", () => {
diff --git a/test/e2e/BrsComponents.test.js b/test/e2e/BrsComponents.test.js
index f97006d..60c8459 100644
--- a/test/e2e/BrsComponents.test.js
+++ b/test/e2e/BrsComponents.test.js
@@ -559,4 +559,28 @@ describe("end to end brightscript functions", () => {
             "parentHandled modified",
         ]);
     });
+
+    test("components/scripts/CallFuncMain.brs", async () => {
+        await execute([resourceFile("components", "scripts", "CallFuncMain.brs")], outputStreams);
+
+        expect(allArgs(outputStreams.stdout.write).filter(arg => arg !== "\n")).toEqual([
+            "component: inside componentFunction, args.test: ",
+            "123",
+            "component: componentField:",
+            "componentField value",
+            "component: mainField:",
+            "invalid",
+            "main: componentFunction return value success:",
+            "true",
+            "component: inside componentVoidFunction",
+            "main: componentVoidFunction return value:",
+            "invalid",
+            "main: componentPrivateFunction return value:",
+            "invalid",
+        ]);
+
+        expect(allArgs(outputStreams.stderr.write).filter(arg => arg !== "\n")).toEqual([
+            "Warning calling function in CallFuncComponent: no function interface specified for componentPrivateFunction",
+        ]);
+    });
 });
diff --git a/test/e2e/resources/components/CallFuncComponent.xml b/test/e2e/resources/components/CallFuncComponent.xml
new file mode 100644
index 0000000..27053ca
--- /dev/null
+++ b/test/e2e/resources/components/CallFuncComponent.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<component name="CallFuncComponent">
+    <script type="text/brightscript" uri="pkg:/components/scripts/CallFuncComponent.brs" />
+    <interface>
+        <function name="componentFunction" />
+        <function name="componentVoidFunction" />
+    </interface>
+</component>
diff --git a/test/e2e/resources/components/scripts/CallFuncComponent.brs b/test/e2e/resources/components/scripts/CallFuncComponent.brs
new file mode 100644
index 0000000..5d2f686
--- /dev/null
+++ b/test/e2e/resources/components/scripts/CallFuncComponent.brs
@@ -0,0 +1,20 @@
+sub init()
+    m.componentField = "componentField value"
+end sub
+
+function componentFunction(args as object) as object
+    print "component: inside componentFunction, args.test: " args.test
+    print "component: componentField:" m.componentField
+    print "component: mainField:" m.mainField
+
+    return { "success": true }
+end function
+
+sub componentVoidFunction()
+    print "component: inside componentVoidFunction"
+end sub
+
+function componentPrivateFunction() as string
+    print "component: inside componentPrivateFunction"
+    return "private return value"
+end function
diff --git a/test/e2e/resources/components/scripts/CallFuncMain.brs b/test/e2e/resources/components/scripts/CallFuncMain.brs
new file mode 100644
index 0000000..208a763
--- /dev/null
+++ b/test/e2e/resources/components/scripts/CallFuncMain.brs
@@ -0,0 +1,15 @@
+sub Main()
+    m.mainField = "mainField value"
+    node = createObject("RoSGNode", "CallFuncComponent")
+    
+    m.componentField = "componentField modified value"
+
+    result = node.callFunc("componentFunction", { test: 123 })
+    print "main: componentFunction return value success:" result.success ' => true
+
+    voidResult = node.callFunc("componentVoidFunction")
+    print "main: componentVoidFunction return value:" voidResult ' => invalid
+
+    privateResult = node.callFunc("componentPrivateFunction")
+    print "main: componentPrivateFunction return value:" privateResult ' => invalid
+end sub