Skip to content

Commit 0cdaba1

Browse files
authored
service: workaround for non-unicode strings in Variables (#4082)
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 f147ac3 commit 0cdaba1

File tree

3 files changed

+59
-1
lines changed

3 files changed

+59
-1
lines changed

_fixtures/testvariables2.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,7 @@ func main() {
432432
var messageVar Message = Message{1, 2, 3, 4, 5}
433433

434434
var iface7 interface{} = OnlyUsedInInterface{"test"}
435+
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}
435436

436437
var amb1 = 1
437438
runtime.Breakpoint()
@@ -443,5 +444,5 @@ func main() {
443444
longslice := make([]int, 100, 100)
444445

445446
runtime.Breakpoint()
446-
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, iface7)
447+
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, iface7, issue4072)
447448
}

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
)
@@ -328,6 +330,14 @@ type Variable struct {
328330
// Function variables will store the name of the function in this field
329331
Value string `json:"value"`
330332

333+
// StringValueBytes contains the Value for string variables that can not be
334+
// represented in unicode.
335+
// Specifically if v.Kind is reflect.String and v.Value is not a valid
336+
// unicode string the JSON marshaller will convert it to a valid unicode
337+
// string, altering its contents. This fields contains the unaltered string
338+
// contents.
339+
StringValueBytes []byte
340+
331341
// 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
332342
Len int64 `json:"len"`
333343
// Cap value for slices
@@ -355,6 +365,36 @@ type Variable struct {
355365
DeclLine int64
356366
}
357367

368+
type variableWithoutMarshaler Variable
369+
370+
func (v *Variable) MarshalJSON() (text []byte, err error) {
371+
var v2 *variableWithoutMarshaler = (*variableWithoutMarshaler)(v)
372+
if v2.Kind == reflect.String && !utf8.ValidString(v2.Value) {
373+
value := v2.Value
374+
// Should we blot out the value with something obviously wrong? Like:
375+
//
376+
// v2.Value = "<invalid-utf8-string>"
377+
// defer func() {
378+
// v2.Value = value
379+
// }()
380+
v2.StringValueBytes = []byte(value)
381+
}
382+
return json.Marshal(v2)
383+
}
384+
385+
func (v *Variable) UnmarshalJSON(buf []byte) error {
386+
var v2 *variableWithoutMarshaler = (*variableWithoutMarshaler)(v)
387+
err := json.Unmarshal(buf, v2)
388+
if err != nil {
389+
return err
390+
}
391+
if v.Kind == reflect.String && len(v.StringValueBytes) > 0 {
392+
v.Value = string(v.StringValueBytes)
393+
v.StringValueBytes = nil
394+
}
395+
return nil
396+
}
397+
358398
// LoadConfig describes how to load values from target's memory
359399
type LoadConfig struct {
360400
// 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
@@ -3428,3 +3428,20 @@ func TestCancelDownload(t *testing.T) {
34283428
}
34293429
})
34303430
}
3431+
3432+
func TestEvalNonunicodeString(t *testing.T) {
3433+
// Non-unicode strings can not be sent through json encoding without being
3434+
// altered, check that our workaround works.
3435+
// See issue #4072.
3436+
withTestClient2("testvariables2", t, func(c service.Client) {
3437+
state := <-c.Continue()
3438+
assertNoError(state.Err, t, "Continue")
3439+
v, err := c.EvalVariable(api.EvalScope{GoroutineID: -1}, "string(issue4072)", normalLoadConfig)
3440+
assertNoError(err, t, "EvalVariable")
3441+
t.Logf("%s", v.MultilineString("", ""))
3442+
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})
3443+
if v.Value != tgt {
3444+
t.Errorf("wrong value for string issue4072 expected %q, got %q", tgt, v.Value)
3445+
}
3446+
})
3447+
}

0 commit comments

Comments
 (0)