Skip to content

Commit fe7e39c

Browse files
committed
service: workaround for non-unicode strings in Variables
When string Variables containing non-unicode strings are encoded into json their value is changed, which leads to subtle bugs such as described in issue #4072. This CL addresses the problem by adding a new field to the api.Variable struct that will be used to transport the variable's value for strings that can not be unicode-encoded. This is the best we can do without breaking backwards compatibility. For our Go code the change is handled transparently by a MarshalJSON/UnmarshalJSON on the api.Variable type. Fixes #4072
1 parent 44123ae commit fe7e39c

File tree

3 files changed

+60
-1
lines changed

3 files changed

+60
-1
lines changed

_fixtures/testvariables2.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,8 @@ func main() {
427427

428428
var messageVar Message = Message{1, 2, 3, 4, 5}
429429

430+
issue4072 := []byte{116, 121, 112, 101, 32, 84, 32, 115, 116, 114, 117, 99, 116, 32, 123, 12, 12, 9, 255, 102, 108, 100, 99, 111, 109, 255}
431+
430432
var amb1 = 1
431433
runtime.Breakpoint()
432434
for amb1 := 0; amb1 < 10; amb1++ {
@@ -437,5 +439,5 @@ func main() {
437439
longslice := make([]int, 100, 100)
438440

439441
runtime.Breakpoint()
440-
fmt.Println(i1, i2, i3, p1, pp1, amb1, s1, s3, a0, a1, p2, p3, s2, as1, str1, f1, fn1, fn2, nilslice, nilptr, ch1, chnil, m1, mnil, m2, m3, m4, m5, upnil, up1, i4, i5, i6, err1, err2, errnil, iface1, iface2, ifacenil, arr1, parr, cpx1, const1, iface3, iface4, recursive1, recursive1.x, iface5, iface2fn1, iface2fn2, bencharr, benchparr, mapinf, mainMenu, b, b2, sd, anonstruct1, anonstruct2, anoniface1, anonfunc, mapanonstruct1, ifacearr, efacearr, ni8, ni16, ni32, ni64, pinf, ninf, nan, zsvmap, zsslice, zsvar, tm, rettm, errtypednil, emptyslice, emptymap, byteslice, bytestypeslice, runeslice, bytearray, bytetypearray, runearray, longstr, nilstruct, as2, as2.NonPointerReceiverMethod, s4, iface2map, issue1578, ll, unread, w2, w3, w4, w5, longarr, longslice, val, m6, m7, cl, tim1, tim2, typedstringvar, namedA1, namedA2, astructName1(namedA2), badslice, tim3, int3chan, longbyteslice, enum1, enum2, enum3, enum4, enum5, enum6, zeropoint4, mlarge, messageVar)
442+
fmt.Println(i1, i2, i3, p1, pp1, amb1, s1, s3, a0, a1, p2, p3, s2, as1, str1, f1, fn1, fn2, nilslice, nilptr, ch1, chnil, m1, mnil, m2, m3, m4, m5, upnil, up1, i4, i5, i6, err1, err2, errnil, iface1, iface2, ifacenil, arr1, parr, cpx1, const1, iface3, iface4, recursive1, recursive1.x, iface5, iface2fn1, iface2fn2, bencharr, benchparr, mapinf, mainMenu, b, b2, sd, anonstruct1, anonstruct2, anoniface1, anonfunc, mapanonstruct1, ifacearr, efacearr, ni8, ni16, ni32, ni64, pinf, ninf, nan, zsvmap, zsslice, zsvar, tm, rettm, errtypednil, emptyslice, emptymap, byteslice, bytestypeslice, runeslice, bytearray, bytetypearray, runearray, longstr, nilstruct, as2, as2.NonPointerReceiverMethod, s4, iface2map, issue1578, ll, unread, w2, w3, w4, w5, longarr, longslice, val, m6, m7, cl, tim1, tim2, typedstringvar, namedA1, namedA2, astructName1(namedA2), badslice, tim3, int3chan, longbyteslice, enum1, enum2, enum3, enum4, enum5, enum6, zeropoint4, mlarge, messageVar, issue4072)
441443
}

service/api/types.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package api
22

33
import (
44
"bytes"
5+
"encoding/json"
56
"errors"
67
"fmt"
78
"reflect"
89
"strconv"
910
"unicode"
11+
"unicode/utf8"
1012

1113
"github.com/go-delve/delve/pkg/proc"
1214
)
@@ -326,6 +328,14 @@ type Variable struct {
326328
// Function variables will store the name of the function in this field
327329
Value string `json:"value"`
328330

331+
// StringValueBytes contains the Value for string variables that can not be
332+
// represented in unicode.
333+
// Specifically if v.Kind is reflect.String and v.Value is not a valid
334+
// unicode string the JSON marshaller will convert it to a valid unicode
335+
// string, altering its contents. This fields contains the unaltered string
336+
// contents.
337+
StringValueBytes []byte
338+
329339
// Number of elements in an array or a slice, number of keys for a map, number of struct members for a struct, length of strings, number of captured variables for functions
330340
Len int64 `json:"len"`
331341
// Cap value for slices
@@ -353,6 +363,36 @@ type Variable struct {
353363
DeclLine int64
354364
}
355365

366+
type variableWithoutMarshaler Variable
367+
368+
func (v *Variable) MarshalJSON() (text []byte, err error) {
369+
var v2 *variableWithoutMarshaler = (*variableWithoutMarshaler)(v)
370+
if v2.Kind == reflect.String && !utf8.ValidString(v2.Value) {
371+
value := v2.Value
372+
// Should we blot out the value with something obviously wrong? Like:
373+
//
374+
// v2.Value = "<invalid-utf8-string>"
375+
// defer func() {
376+
// v2.Value = value
377+
// }()
378+
v2.StringValueBytes = []byte(value)
379+
}
380+
return json.Marshal(v2)
381+
}
382+
383+
func (v *Variable) UnmarshalJSON(buf []byte) error {
384+
var v2 *variableWithoutMarshaler = (*variableWithoutMarshaler)(v)
385+
err := json.Unmarshal(buf, v2)
386+
if err != nil {
387+
return err
388+
}
389+
if v.Kind == reflect.String && len(v.StringValueBytes) > 0 {
390+
v.Value = string(v.StringValueBytes)
391+
v.StringValueBytes = nil
392+
}
393+
return nil
394+
}
395+
356396
// LoadConfig describes how to load values from target's memory
357397
type LoadConfig struct {
358398
// FollowPointers requests pointers to be automatically dereferenced.

service/test/integration2_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3339,3 +3339,20 @@ func TestFollowExecFindLocation(t *testing.T) {
33393339
assertNoError(err, t, "FindLocation(spawn.go:19)")
33403340
})
33413341
}
3342+
3343+
func TestEvalNonunicodeString(t *testing.T) {
3344+
// Non-unicode strings can not be sent through json encoding without being
3345+
// altered, check that our workaround works.
3346+
// See issue #4072.
3347+
withTestClient2("testvariables2", t, func(c service.Client) {
3348+
state := <-c.Continue()
3349+
assertNoError(state.Err, t, "Continue")
3350+
v, err := c.EvalVariable(api.EvalScope{GoroutineID: -1}, "string(issue4072)", normalLoadConfig)
3351+
assertNoError(err, t, "EvalVariable")
3352+
t.Logf("%s", v.MultilineString("", ""))
3353+
tgt := string([]byte{116, 121, 112, 101, 32, 84, 32, 115, 116, 114, 117, 99, 116, 32, 123, 12, 12, 9, 255, 102, 108, 100, 99, 111, 109, 255})
3354+
if v.Value != tgt {
3355+
t.Errorf("wrong value for string issue4072 expected %q, got %q", tgt, v.Value)
3356+
}
3357+
})
3358+
}

0 commit comments

Comments
 (0)