Repository: NREL/OpenStudio (language: python)

Issue:
runner.getPastStepValuesForMeasure can cause segfault
Issue overview
--------------
I believe that certain values (i.e., containing special characters?) stored in "value" of a workflow step can cause a segfault. I have not investigated/narrowed this down, however.

## Current Behavior
<!--- Tell us what happens instead of the expected behavior -->

## Expected Behavior
<!--- Tell us what should happen -->

## Steps to Reproduce
Execute `runner.getPastStepValuesForMeasure('apply_upgrade')` from resstock's UpgradeCosts measure. Values can contain "(", "|", etc.

## Possible Solution
<!--- Optional, but if you can, suggest a fix/reason for the bug, -->

## Details

### Environment

Some additional details about your environment for this issue (if relevant):
 - Platform (Operating system, version):
 - Version of OpenStudio (if using an intermediate build, include SHA):

### Context
<!--- How has this issue affected you? What are you trying to accomplish? -->



Discussion / hints:
I think it's an encoding issue when converting from Json::Value to a string to be used as a key


I can reproduce using this

# Python: it works

```
from pathlib import Path
import sys
sys.path.insert(0, str(Path('./Products/python/').absolute()))
import openstudio


workflow = openstudio.WorkflowJSON()
runner = openstudio.measure.OSRunner(workflow)

step1 = openstudio.MeasureStep("MeasureName1")
step1.setArgument("Argument1", 100)

workflow_step_result1 = openstudio.WorkflowStepResult()
workflow_step_result1.setMeasureName("measure_name_1")

stepValue1 = openstudio.WorkflowStepValue("(|)'*$£*µ%é", True)
workflow_step_result1.addStepValue(stepValue1)

workflow_step_result1.setStepResult(openstudio.StepResult('Success'))
workflow_step_result1.setCompletedAt(openstudio.DateTime.nowUTC())
step1.setResult(workflow_step_result1)

workflow.setWorkflowSteps([step1])

workflow.saveAs("OSRunner_getPastStepValues_special_chars.osw")

print(runner.getPastStepValuesForMeasure("MeasureName1"))
```

```
{"(|)'*$£*µ%é": True}
```

# Ruby

```
require 'openstudio'

workflow = OpenStudio::WorkflowJSON.new()
runner = OpenStudio::Measure::OSRunner.new(workflow)

step1 = OpenStudio::MeasureStep.new("MeasureName1")
step1.setArgument("Argument1", 100)

workflow_step_result1 = OpenStudio::WorkflowStepResult.new()
workflow_step_result1.setMeasureName("measure_name_1")

stepValue1 = OpenStudio::WorkflowStepValue.new("(|)'*$£*µ%é", true)
workflow_step_result1.addStepValue(stepValue1)

workflow_step_result1.setStepResult('Success'.to_StepResult)
workflow_step_result1.setCompletedAt(OpenStudio::DateTime.nowUTC())
step1.setResult(workflow_step_result1)

workflow.setWorkflowSteps([step1])

workflow.saveAs("OSRunner_getPastStepValues_special_chars.osw")

puts runner.getPastStepValuesForMeasure("MeasureName1")
```

```
$ Products/openstudio test.rb

EncodingError: invalid symbol in encoding US-ASCII :"(|)'*$\xC2\xA3*\xC2\xB5%\xC3\xA9"
Backtrace:
	/home/julien/Software/Others/OS-build-release/test.rb:23:in `getPastStepValuesForMeasure'
	/home/julien/Software/Others/OS-build-release/test.rb:23:in `<top (required)>'
	eval:182:in `require'
	eval:182:in `require'
	<internal::/ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
	eval:4:in `<main>'
Failed to execute '/home/julien/Software/Others/OS-build-release/test.rb'
```

I try to make it a symbol... and that's the issue

https://github.com/NREL/OpenStudio/blob/53c1d69a81a6a833244d193a39a36cee601dd3be/src/utilities/core/jsoncpp.i#L111

rb_intern uses us_ascii encoding. There's a rb_intern3 that takes an encoding, but that isn't public by default and I can't get swig to find it yet

I don't know how to fix it.

Fix patch:
diff --git a/src/utilities/core/jsoncpp.i b/src/utilities/core/jsoncpp.i
index 5f57c61e54c..ab93cc82e1a 100644
--- a/src/utilities/core/jsoncpp.i
+++ b/src/utilities/core/jsoncpp.i
@@ -75,7 +75,19 @@
 #endif
 
 #if defined SWIGRUBY
-%fragment("JsonToDict","header", fragment="SWIG_FromCharPtrAndSize") {
+
+%fragment("<ruby_encoding>", "header") %{
+#include <ruby/encoding.h>
+%}
+
+%fragment("string_to_ruby_utf8_symbol", "header", fragment="<ruby_encoding>") {
+  SWIGINTERN VALUE SWIG_String_to_ruby_utf8_symbol(const std::string& str) {
+    return ID2SYM(rb_intern3(str.data(), str.size(), rb_utf8_encoding()));
+  }
+}
+
+%fragment("JsonToDict", "header", fragment="SWIG_FromCharPtrAndSize", fragment="string_to_ruby_utf8_symbol") {
+
   SWIGINTERN VALUE SWIG_From_JsonValue(const Json::Value& value) {
 
     switch(value.type()) {
@@ -107,7 +119,10 @@
     case Json::objectValue: {
       VALUE result = rb_hash_new();
       for( const auto& id : value.getMemberNames()) {
-        rb_hash_aset(result, ID2SYM(rb_intern(id.data())), SWIG_From_JsonValue(value[id]));
+        rb_hash_aset(result,
+                    SWIG_String_to_ruby_utf8_symbol(id),
+                    SWIG_From_JsonValue(value[id])
+        );
       }
       return result;
     }


Test patch:
diff --git a/python/test/test_swigjson.py b/python/test/test_swigjson.py
index f7fb06d3772..be320e059c7 100644
--- a/python/test/test_swigjson.py
+++ b/python/test/test_swigjson.py
@@ -47,3 +47,35 @@ def test_osargument_value_as_json():
     val = choiceArgument.valueAsJSON()
     assert isinstance(val, str)
     assert val == "handle1"
+
+
+def test_swig_json_special_chars():
+    workflow = openstudio.WorkflowJSON()
+    runner = openstudio.measure.OSRunner(workflow)
+
+    step1 = openstudio.MeasureStep("MeasureName1")
+    step1.setArgument("Argument1", 100)
+
+    workflow_step_result1 = openstudio.WorkflowStepResult()
+    workflow_step_result1.setMeasureName("measure_name_1")
+
+    name = "name_(|)'*$£*µ%é"
+    val = "val_(|)'*$£*µ%é"
+    stepValue1 = openstudio.WorkflowStepValue(name, val)
+    assert name == stepValue1.name()
+    assert val == stepValue1.valueAsString()
+    assert val == stepValue1.valueAsJSON()
+
+    workflow_step_result1.addStepValue(stepValue1)
+
+    workflow_step_result1.setStepResult(openstudio.StepResult("Success"))
+    workflow_step_result1.setCompletedAt(openstudio.DateTime.nowUTC())
+    step1.setResult(workflow_step_result1)
+
+    workflow.setWorkflowSteps([step1])
+
+    d = runner.getPastStepValuesForMeasure("MeasureName1")
+    assert len(d) == 1
+    k, v = next(iter(d.items()))
+    assert k == name
+    assert v == val
diff --git a/ruby/test/SwigJson_Test.rb b/ruby/test/SwigJson_Test.rb
index 77a59543541..d41545e7f58 100644
--- a/ruby/test/SwigJson_Test.rb
+++ b/ruby/test/SwigJson_Test.rb
@@ -55,4 +55,36 @@ def test_osargument_value_as_json
     assert_kind_of(String, val)
     assert_equal('handle1', val)
   end
+
+  def test_swig_json_special_chars
+    workflow = OpenStudio::WorkflowJSON.new
+    runner = OpenStudio::Measure::OSRunner.new(workflow)
+
+    step1 = OpenStudio::MeasureStep.new('MeasureName1')
+    step1.setArgument('Argument1', 100)
+
+    workflow_step_result1 = OpenStudio::WorkflowStepResult.new
+    workflow_step_result1.setMeasureName('measure_name_1')
+
+    name = "name_(|)'*$£*µ%é"
+    val = "val_(|)'*$£*µ%é"
+    stepValue1 = OpenStudio::WorkflowStepValue.new(name, val)
+    assert_equal(name, stepValue1.name)
+    assert_equal(val, stepValue1.valueAsString)
+    assert_equal(val, stepValue1.valueAsJSON)
+    workflow_step_result1.addStepValue(stepValue1)
+
+    workflow_step_result1.setStepResult('Success'.to_StepResult)
+    workflow_step_result1.setCompletedAt(OpenStudio::DateTime.nowUTC)
+    step1.setResult(workflow_step_result1)
+
+    workflow.setWorkflowSteps([step1])
+
+    # workflow.saveAs("OSRunner_getPastStepValues_special_chars.osw")
+
+    h = runner.getPastStepValuesForMeasure('MeasureName1')
+    assert_equal(1, h.size)
+    assert_equal(name.to_sym, h.keys.first)
+    assert_equal(val, h.values.first)
+  end
 end