Repository: go-delve/delve
Pull request #3100 (2022-08-09 18:07:52)

_scripts: fix CI on go1.19/linux/386
Go 1.19 is broken on linux/386 with some C compilers, this is a
workaround for our build script. See:

https://github.com/golang/go/issues/52919

Also fix TestBuild if the first message reported by delve is not the
message that indicates the server is listening.



Patch:
diff --git a/_scripts/make.go b/_scripts/make.go
index 80b04300..0f4a9698 100644
--- a/_scripts/make.go
+++ b/_scripts/make.go
@@ -445,13 +445,17 @@ func testCmdIntl(testSet, testRegex, testBackend, testBuildMode string) {
 		buildModeFlag = "-test-buildmode=" + testBuildMode
 	}
 
+	env := []string{}
+	if os.Getenv("CI") != "" {
+		env = os.Environ()
+	}
+
 	if len(testPackages) > 3 {
-		env := []string{}
 		executeq(env, "go", "test", testFlags(), buildFlags(), testPackages, backendFlag, buildModeFlag)
 	} else if testRegex != "" {
-		execute("go", "test", testFlags(), buildFlags(), testPackages, "-run="+testRegex, backendFlag, buildModeFlag)
+		executeq(env, "go", "test", testFlags(), buildFlags(), testPackages, "-run="+testRegex, backendFlag, buildModeFlag)
 	} else {
-		execute("go", "test", testFlags(), buildFlags(), testPackages, backendFlag, buildModeFlag)
+		executeq(env, "go", "test", testFlags(), buildFlags(), testPackages, backendFlag, buildModeFlag)
 	}
 }
 
diff --git a/pkg/config/split.go b/pkg/config/split.go
index 05ca977e..a60b07d3 100644
--- a/pkg/config/split.go
+++ b/pkg/config/split.go
@@ -41,6 +41,7 @@ func SplitQuotedFields(in string, quote rune) []string {
 			} else if unicode.IsSpace(ch) {
 				r = append(r, buf.String())
 				buf.Reset()
+				state = inSpace
 			} else {
 				buf.WriteRune(ch)
 			}
@@ -60,7 +61,7 @@ func SplitQuotedFields(in string, quote rune) []string {
 		}
 	}
 
-	if buf.Len() != 0 {
+	if state == inField || buf.Len() != 0 {
 		r = append(r, buf.String())
 	}
 
diff --git a/pkg/locspec/locations.go b/pkg/locspec/locations.go
index a126e7af..721398e6 100644
--- a/pkg/locspec/locations.go
+++ b/pkg/locspec/locations.go
@@ -498,13 +498,19 @@ func SubstitutePath(path string, rules [][2]string) string {
 		}
 
 		// Otherwise check if it's a directory prefix.
-		if !strings.HasSuffix(from, separator) {
+		if from != "" && !strings.HasSuffix(from, separator) {
 			from = from + separator
 		}
-		if !strings.HasSuffix(to, separator) {
+		if to != "" && !strings.HasSuffix(to, separator) {
 			to = to + separator
 		}
-		if strings.HasPrefix(path, from) {
+
+		// Expand relative paths with the specified prefix
+		if from == "" && !filepath.IsAbs(path) {
+			return strings.Replace(path, from, to, 1)
+		}
+
+		if from != "" && strings.HasPrefix(path, from) {
 			return strings.Replace(path, from, to, 1)
 		}
 	}
diff --git a/pkg/proc/eval.go b/pkg/proc/eval.go
index 61109352..12e13942 100644
--- a/pkg/proc/eval.go
+++ b/pkg/proc/eval.go
@@ -36,6 +36,7 @@ type EvalScope struct {
 	g       *G
 	BinInfo *BinaryInfo
 	target  *Target
+	loadCfg *LoadConfig
 
 	frameOffset int64
 
@@ -189,10 +190,9 @@ func (scope *EvalScope) EvalExpression(expr string, cfg LoadConfig) (*Variable,
 		return nil, err
 	}
 
-	ev, err := scope.evalToplevelTypeCast(t, cfg)
-	if ev == nil && err == nil {
-		ev, err = scope.evalAST(t)
-	}
+	scope.loadCfg = &cfg
+
+	ev, err := scope.evalAST(t)
 	if err != nil {
 		scope.callCtx.doReturn(nil, err)
 		return nil, err
@@ -615,133 +615,6 @@ func (scope *EvalScope) PtrSize() int {
 	return scope.BinInfo.Arch.PtrSize()
 }
 
-// evalToplevelTypeCast implements certain type casts that we only support
-// at the outermost levels of an expression.
-func (scope *EvalScope) evalToplevelTypeCast(t ast.Expr, cfg LoadConfig) (*Variable, error) {
-	call, _ := t.(*ast.CallExpr)
-	if call == nil || len(call.Args) != 1 {
-		return nil, nil
-	}
-	targetTypeStr := exprToString(removeParen(call.Fun))
-	var targetType godwarf.Type
-	switch targetTypeStr {
-	case "[]byte", "[]uint8":
-		targetType = fakeSliceType(&godwarf.IntType{BasicType: godwarf.BasicType{CommonType: godwarf.CommonType{ByteSize: 1, Name: "uint8"}, BitSize: 8, BitOffset: 0}})
-	case "[]int32", "[]rune":
-		targetType = fakeSliceType(&godwarf.IntType{BasicType: godwarf.BasicType{CommonType: godwarf.CommonType{ByteSize: 1, Name: "int32"}, BitSize: 32, BitOffset: 0}})
-	case "string":
-		var err error
-		targetType, err = scope.BinInfo.findType("string")
-		if err != nil {
-			return nil, err
-		}
-	default:
-		return nil, nil
-	}
-
-	argv, err := scope.evalToplevelTypeCast(call.Args[0], cfg)
-	if argv == nil && err == nil {
-		argv, err = scope.evalAST(call.Args[0])
-	}
-	if err != nil {
-		return nil, err
-	}
-	argv.loadValue(cfg)
-	if argv.Unreadable != nil {
-		return nil, argv.Unreadable
-	}
-
-	v := newVariable("", 0, targetType, scope.BinInfo, scope.Mem)
-	v.loaded = true
-
-	converr := fmt.Errorf("can not convert %q to %s", exprToString(call.Args[0]), targetTypeStr)
-
-	switch targetTypeStr {
-	case "[]byte", "[]uint8":
-		if argv.Kind != reflect.String {
-			return nil, converr
-		}
-		for i, ch := range []byte(constant.StringVal(argv.Value)) {
-			e := newVariable("", argv.Addr+uint64(i), targetType.(*godwarf.SliceType).ElemType, scope.BinInfo, argv.mem)
-			e.loaded = true
-			e.Value = constant.MakeInt64(int64(ch))
-			v.Children = append(v.Children, *e)
-		}
-		v.Len = int64(len(v.Children))
-		v.Cap = v.Len
-		return v, nil
-
-	case "[]int32", "[]rune":
-		if argv.Kind != reflect.String {
-			return nil, converr
-		}
-		for i, ch := range constant.StringVal(argv.Value) {
-			e := newVariable("", argv.Addr+uint64(i), targetType.(*godwarf.SliceType).ElemType, scope.BinInfo, argv.mem)
-			e.loaded = true
-			e.Value = constant.MakeInt64(int64(ch))
-			v.Children = append(v.Children, *e)
-		}
-		v.Len = int64(len(v.Children))
-		v.Cap = v.Len
-		return v, nil
-
-	case "string":
-		switch argv.Kind {
-		case reflect.String:
-			s := constant.StringVal(argv.Value)
-			v.Value = constant.MakeString(s)
-			v.Len = int64(len(s))
-			return v, nil
-		case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint, reflect.Uintptr:
-			b, _ := constant.Int64Val(argv.Value)
-			s := string(rune(b))
-			v.Value = constant.MakeString(s)
-			v.Len = int64(len(s))
-			return v, nil
-		case reflect.Slice, reflect.Array:
-			var elem godwarf.Type
-			if argv.Kind == reflect.Slice {
-				elem = argv.RealType.(*godwarf.SliceType).ElemType
-			} else {
-				elem = argv.RealType.(*godwarf.ArrayType).Type
-			}
-			switch elemType := elem.(type) {
-			case *godwarf.UintType:
-				if elemType.Name != "uint8" && elemType.Name != "byte" {
-					return nil, nil
-				}
-				bytes := make([]byte, len(argv.Children))
-				for i := range argv.Children {
-					n, _ := constant.Int64Val(argv.Children[i].Value)
-					bytes[i] = byte(n)
-				}
-				v.Value = constant.MakeString(string(bytes))
-
-			case *godwarf.IntType:
-				if elemType.Name != "int32" && elemType.Name != "rune" {
-					return nil, nil
-				}
-				runes := make([]rune, len(argv.Children))
-				for i := range argv.Children {
-					n, _ := constant.Int64Val(argv.Children[i].Value)
-					runes[i] = rune(n)
-				}
-				v.Value = constant.MakeString(string(runes))
-
-			default:
-				return nil, nil
-			}
-			v.Len = int64(len(constant.StringVal(v.Value)))
-			return v, nil
-
-		default:
-			return nil, nil
-		}
-	}
-
-	return nil, nil
-}
-
 func (scope *EvalScope) evalAST(t ast.Expr) (*Variable, error) {
 	switch node := t.(type) {
 	case *ast.CallExpr:
@@ -914,19 +787,23 @@ func (scope *EvalScope) evalTypeCast(node *ast.CallExpr) (*Variable, error) {
 	if err != nil {
 		return nil, err
 	}
-	argv.loadValue(loadSingleValue)
-	if argv.Unreadable != nil {
-		return nil, argv.Unreadable
-	}
 
 	fnnode := node.Fun
 
 	// remove all enclosing parenthesis from the type name
 	fnnode = removeParen(fnnode)
 
+	targetTypeStr := exprToString(removeParen(node.Fun))
 	styp, err := scope.BinInfo.findTypeExpr(fnnode)
 	if err != nil {
-		return nil, err
+		switch targetTypeStr {
+		case "[]byte", "[]uint8":
+			styp = fakeSliceType(&godwarf.IntType{BasicType: godwarf.BasicType{CommonType: godwarf.CommonType{ByteSize: 1, Name: "uint8"}, BitSize: 8, BitOffset: 0}})
+		case "[]int32", "[]rune":
+			styp = fakeSliceType(&godwarf.IntType{BasicType: godwarf.BasicType{CommonType: godwarf.CommonType{ByteSize: 1, Name: "int32"}, BitSize: 32, BitOffset: 0}})
+		default:
+			return nil, err
+		}
 	}
 	typ := resolveTypedef(styp)
 
@@ -946,6 +823,11 @@ func (scope *EvalScope) evalTypeCast(node *ast.CallExpr) (*Variable, error) {
 			return nil, converr
 		}
 
+		argv.loadValue(loadSingleValue)
+		if argv.Unreadable != nil {
+			return nil, argv.Unreadable
+		}
+
 		n, _ := constant.Int64Val(argv.Value)
 
 		mem := scope.Mem
@@ -960,6 +842,10 @@ func (scope *EvalScope) evalTypeCast(node *ast.CallExpr) (*Variable, error) {
 		return v, nil
 
 	case *godwarf.UintType:
+		argv.loadValue(loadSingleValue)
+		if argv.Unreadable != nil {
+			return nil, argv.Unreadable
+		}
 		switch argv.Kind {
 		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
 			n, _ := constant.Int64Val(argv.Value)
@@ -978,6 +864,10 @@ func (scope *EvalScope) evalTypeCast(node *ast.CallExpr) (*Variable, error) {
 			return v, nil
 		}
 	case *godwarf.IntType:
+		argv.loadValue(loadSingleValue)
+		if argv.Unreadable != nil {
+			return nil, argv.Unreadable
+		}
 		switch argv.Kind {
 		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
 			n, _ := constant.Int64Val(argv.Value)
@@ -993,6 +883,10 @@ func (scope *EvalScope) evalTypeCast(node *ast.CallExpr) (*Variable, error) {
 			return v, nil
 		}
 	case *godwarf.FloatType:
+		argv.loadValue(loadSingleValue)
+		if argv.Unreadable != nil {
+			return nil, argv.Unreadable
+		}
 		switch argv.Kind {
 		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
 			fallthrough
@@ -1003,6 +897,10 @@ func (scope *EvalScope) evalTypeCast(node *ast.CallExpr) (*Variable, error) {
 			return v, nil
 		}
 	case *godwarf.ComplexType:
+		argv.loadValue(loadSingleValue)
+		if argv.Unreadable != nil {
+			return nil, argv.Unreadable
+		}
 		switch argv.Kind {
 		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
 			fallthrough
@@ -1014,6 +912,95 @@ func (scope *EvalScope) evalTypeCast(node *ast.CallExpr) (*Variable, error) {
 		}
 	}
 
+	cfg := loadFullValue
+	if scope.loadCfg != nil {
+		cfg = *scope.loadCfg
+	}
+	argv.loadValue(cfg)
+	if argv.Unreadable != nil {
+		return nil, argv.Unreadable
+	}
+
+	switch targetTypeStr {
+	case "[]byte", "[]uint8":
+		if argv.Kind != reflect.String {
+			return nil, converr
+		}
+		for i, ch := range []byte(constant.StringVal(argv.Value)) {
+			e := newVariable("", argv.Addr+uint64(i), typ.(*godwarf.SliceType).ElemType, scope.BinInfo, argv.mem)
+			e.loaded = true
+			e.Value = constant.MakeInt64(int64(ch))
+			v.Children = append(v.Children, *e)
+		}
+		v.Len = int64(len(v.Children))
+		v.Cap = v.Len
+		return v, nil
+
+	case "[]int32", "[]rune":
+		if argv.Kind != reflect.String {
+			return nil, converr
+		}
+		for i, ch := range constant.StringVal(argv.Value) {
+			e := newVariable("", argv.Addr+uint64(i), typ.(*godwarf.SliceType).ElemType, scope.BinInfo, argv.mem)
+			e.loaded = true
+			e.Value = constant.MakeInt64(int64(ch))
+			v.Children = append(v.Children, *e)
+		}
+		v.Len = int64(len(v.Children))
+		v.Cap = v.Len
+		return v, nil
+
+	case "string":
+		switch argv.Kind {
+		case reflect.String:
+			s := constant.StringVal(argv.Value)
+			v.Value = constant.MakeString(s)
+			v.Len = int64(len(s))
+			return v, nil
+		case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint, reflect.Uintptr:
+			b, _ := constant.Int64Val(argv.Value)
+			s := string(rune(b))
+			v.Value = constant.MakeString(s)
+			v.Len = int64(len(s))
+			return v, nil
+		case reflect.Slice, reflect.Array:
+			var elem godwarf.Type
+			if argv.Kind == reflect.Slice {
+				elem = argv.RealType.(*godwarf.SliceType).ElemType
+			} else {
+				elem = argv.RealType.(*godwarf.ArrayType).Type
+			}
+			switch elemType := elem.(type) {
+			case *godwarf.UintType:
+				if elemType.Name != "uint8" && elemType.Name != "byte" {
+					return nil, converr
+				}
+				bytes := make([]byte, len(argv.Children))
+				for i := range argv.Children {
+					n, _ := constant.Int64Val(argv.Children[i].Value)
+					bytes[i] = byte(n)
+				}
+				v.Value = constant.MakeString(string(bytes))
+
+			case *godwarf.IntType:
+				if elemType.Name != "int32" && elemType.Name != "rune" {
+					return nil, converr
+				}
+				runes := make([]rune, len(argv.Children))
+				for i := range argv.Children {
+					n, _ := constant.Int64Val(argv.Children[i].Value)
+					runes[i] = rune(n)
+				}
+				v.Value = constant.MakeString(string(runes))
+
+			default:
+				return nil, converr
+			}
+			v.Len = int64(len(constant.StringVal(v.Value)))
+			return v, nil
+		}
+	}
+
 	return nil, converr
 }
 
diff --git a/service/dap/types.go b/service/dap/types.go
index 27978f43..f67a03f6 100644
--- a/service/dap/types.go
+++ b/service/dap/types.go
@@ -189,7 +189,9 @@ type LaunchAttachCommonConfig struct {
 }
 
 // SubstitutePath defines a mapping from a local path to the remote path.
-// Both 'from' and 'to' must be specified and non-empty.
+// Both 'from' and 'to' must be specified and non-null.
+// Empty values can be used to add or remove absolute path prefixes when mapping.
+// For example, mapping with empy 'to' can be used to work with binaries with trimmed paths.
 type SubstitutePath struct {
 	// The local path to be replaced when passing paths to the debugger.
 	From string `json:"from,omitempty"`
@@ -199,7 +201,10 @@ type SubstitutePath struct {
 
 func (m *SubstitutePath) UnmarshalJSON(data []byte) error {
 	// use custom unmarshal to check if both from/to are set.
-	type tmpType SubstitutePath
+	type tmpType struct {
+		From *string
+		To   *string
+	}
 	var tmp tmpType
 
 	if err := json.Unmarshal(data, &tmp); err != nil {
@@ -208,10 +213,10 @@ func (m *SubstitutePath) UnmarshalJSON(data []byte) error {
 		}
 		return err
 	}
-	if tmp.From == "" || tmp.To == "" {
+	if tmp.From == nil || tmp.To == nil {
 		return errors.New("'substitutePath' requires both 'from' and 'to' entries")
 	}
-	*m = SubstitutePath(tmp)
+	*m = SubstitutePath{*tmp.From, *tmp.To}
 	return nil
 }
 


Test patch:
diff --git a/_scripts/test_linux.sh b/_scripts/test_linux.sh
index ddced784..0ae64d6d 100755
--- a/_scripts/test_linux.sh
+++ b/_scripts/test_linux.sh
@@ -57,6 +57,13 @@ elif [ ${version:4:2} -gt 17 ]; then
 	export GOFLAGS=-buildvcs=false
 fi
 
+if [ "$arch" = "386" ]; then
+	ver=$(go version)
+	if [ "$ver" = "go version go1.19 linux/386" ]; then
+		export CGO_CFLAGS='-g -O0 -fno-stack-protector'
+	fi
+fi
+
 set +e
 make test
 x=$?
diff --git a/cmd/dlv/dlv_test.go b/cmd/dlv/dlv_test.go
index 785dd303..ffe123c6 100644
--- a/cmd/dlv/dlv_test.go
+++ b/cmd/dlv/dlv_test.go
@@ -111,8 +111,13 @@ func TestBuild(t *testing.T) {
 
 	scan := bufio.NewScanner(stderr)
 	// wait for the debugger to start
-	scan.Scan()
-	t.Log(scan.Text())
+	for scan.Scan() {
+		text := scan.Text()
+		t.Log(text)
+		if strings.Contains(text, "API server pid = ") {
+			break
+		}
+	}
 	go func() {
 		for scan.Scan() {
 			t.Log(scan.Text())
diff --git a/pkg/config/split_test.go b/pkg/config/split_test.go
index d2c258d0..04a97dfb 100644
--- a/pkg/config/split_test.go
+++ b/pkg/config/split_test.go
@@ -21,18 +21,52 @@ func TestSplitQuotedFields(t *testing.T) {
 }
 
 func TestSplitDoubleQuotedFields(t *testing.T) {
-	in := `field"A" "fieldB" fie"l'd"C "field\"D" "yet another field"`
-	tgt := []string{"fieldA", "fieldB", "fiel'dC", "field\"D", "yet another field"}
-	out := SplitQuotedFields(in, '"')
-
-	if len(tgt) != len(out) {
-		t.Fatalf("expected %#v, got %#v (len mismatch)", tgt, out)
+	tests := []struct {
+		name     string
+		in       string
+		expected []string
+	}{
+		{
+			name:     "generic test case",
+			in:       `field"A" "fieldB" fie"l'd"C "field\"D" "yet another field"`,
+			expected: []string{"fieldA", "fieldB", "fiel'dC", "field\"D", "yet another field"},
+		},
+		{
+			name:     "with empty string in the end",
+			in:       `field"A" "" `,
+			expected: []string{"fieldA", ""},
+		},
+		{
+			name:     "with empty string at the beginning",
+			in:       ` "" field"A"`,
+			expected: []string{"", "fieldA"},
+		},
+		{
+			name:     "lots of spaces",
+			in:       `    field"A"   `,
+			expected: []string{"fieldA"},
+		},
+		{
+			name:     "only empty string",
+			in:       ` "" "" "" """" "" `,
+			expected: []string{"", "", "", "", ""},
+		},
 	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			in := tt.in
+			tgt := tt.expected
+			out := SplitQuotedFields(in, '"')
+			if len(tgt) != len(out) {
+				t.Fatalf("expected %#v, got %#v (len mismatch)", tgt, out)
+			}
 
-	for i := range tgt {
-		if tgt[i] != out[i] {
-			t.Fatalf(" expected %#v, got %#v (mismatch at %d)", tgt, out, i)
-		}
+			for i := range tgt {
+				if tgt[i] != out[i] {
+					t.Fatalf(" expected %#v, got %#v (mismatch at %d)", tgt, out, i)
+				}
+			}
+		})
 	}
 }
 
diff --git a/pkg/locspec/locations_test.go b/pkg/locspec/locations_test.go
index 5be02f5b..f83f513f 100644
--- a/pkg/locspec/locations_test.go
+++ b/pkg/locspec/locations_test.go
@@ -1,6 +1,7 @@
 package locspec
 
 import (
+	"runtime"
 	"testing"
 )
 
@@ -66,3 +67,57 @@ func TestFunctionLocationParsing(t *testing.T) {
 	assertNormalLocationSpec(t, "github.com/go-delve/delve/pkg/proc.Process.Continue:10", NormalLocationSpec{"github.com/go-delve/delve/pkg/proc.Process.Continue", &FuncLocationSpec{PackageName: "github.com/go-delve/delve/pkg/proc", ReceiverName: "Process", BaseName: "Continue"}, 10})
 	assertNormalLocationSpec(t, "github.com/go-delve/delve/pkg/proc.Continue:10", NormalLocationSpec{"github.com/go-delve/delve/pkg/proc.Continue", &FuncLocationSpec{PackageName: "github.com/go-delve/delve/pkg/proc", BaseName: "Continue"}, 10})
 }
+
+func assertSubstitutePathEqual(t *testing.T, expected string, substituted string) {
+	if expected != substituted {
+		t.Fatalf("Expected substitutedPath to be %s got %s instead", expected, substituted)
+	}
+}
+
+func TestSubstitutePathUnix(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("Skipping unix SubstitutePath test in windows")
+	}
+
+	// Relative paths mapping
+	assertSubstitutePathEqual(t, "/my/asb/folder/relative/path", SubstitutePath("relative/path", [][2]string{{"", "/my/asb/folder/"}}))
+	assertSubstitutePathEqual(t, "/already/abs/path", SubstitutePath("/already/abs/path", [][2]string{{"", "/my/asb/folder/"}}))
+	assertSubstitutePathEqual(t, "relative/path", SubstitutePath("/my/asb/folder/relative/path", [][2]string{{"/my/asb/folder/", ""}}))
+	assertSubstitutePathEqual(t, "/another/folder/relative/path", SubstitutePath("/another/folder/relative/path", [][2]string{{"/my/asb/folder/", ""}}))
+	assertSubstitutePathEqual(t, "my/path", SubstitutePath("relative/path/my/path", [][2]string{{"relative/path", ""}}))
+	assertSubstitutePathEqual(t, "/abs/my/path", SubstitutePath("/abs/my/path", [][2]string{{"abs/my", ""}}))
+
+	// Absolute paths mapping
+	assertSubstitutePathEqual(t, "/new/mapping/path", SubstitutePath("/original/path", [][2]string{{"/original", "/new/mapping"}}))
+	assertSubstitutePathEqual(t, "/no/change/path", SubstitutePath("/no/change/path", [][2]string{{"/original", "/new/mapping"}}))
+	assertSubstitutePathEqual(t, "/folder/should_not_be_replaced/path", SubstitutePath("/folder/should_not_be_replaced/path", [][2]string{{"should_not_be_replaced", ""}}))
+
+	// Mix absolute and relative mapping
+	assertSubstitutePathEqual(t, "/new/mapping/path", SubstitutePath("/original/path", [][2]string{{"", "/my/asb/folder/"}, {"/my/asb/folder/", ""}, {"/original", "/new/mapping"}}))
+	assertSubstitutePathEqual(t, "/my/asb/folder/path", SubstitutePath("path", [][2]string{{"/original", "/new/mapping"}, {"", "/my/asb/folder/"}, {"/my/asb/folder/", ""}}))
+	assertSubstitutePathEqual(t, "path", SubstitutePath("/my/asb/folder/path", [][2]string{{"/original", "/new/mapping"}, {"/my/asb/folder/", ""}, {"", "/my/asb/folder/"}}))
+}
+
+func TestSubstitutePathWindows(t *testing.T) {
+	if runtime.GOOS != "windows" {
+		t.Skip("Skipping windows SubstitutePath test in unix")
+	}
+
+	// Relative paths mapping
+	assertSubstitutePathEqual(t, "c:\\my\\asb\\folder\\relative\\path", SubstitutePath("relative\\path", [][2]string{{"", "c:\\my\\asb\\folder\\"}}))
+	assertSubstitutePathEqual(t, "f:\\already\\abs\\path", SubstitutePath("F:\\already\\abs\\path", [][2]string{{"", "c:\\my\\asb\\folder\\"}}))
+	assertSubstitutePathEqual(t, "relative\\path", SubstitutePath("C:\\my\\asb\\folder\\relative\\path", [][2]string{{"c:\\my\\asb\\folder\\", ""}}))
+	assertSubstitutePathEqual(t, "f:\\another\\folder\\relative\\path", SubstitutePath("F:\\another\\folder\\relative\\path", [][2]string{{"c:\\my\\asb\\folder\\", ""}}))
+	assertSubstitutePathEqual(t, "my\\path", SubstitutePath("relative\\path\\my\\path", [][2]string{{"relative\\path", ""}}))
+	assertSubstitutePathEqual(t, "c:\\abs\\my\\path", SubstitutePath("c:\\abs\\my\\path", [][2]string{{"abs\\my", ""}}))
+
+	// Absolute paths mapping
+	assertSubstitutePathEqual(t, "c:\\new\\mapping\\path", SubstitutePath("D:\\original\\path", [][2]string{{"d:\\original", "c:\\new\\mapping"}}))
+	assertSubstitutePathEqual(t, "f:\\no\\change\\path", SubstitutePath("F:\\no\\change\\path", [][2]string{{"d:\\original", "c:\\new\\mapping"}}))
+	assertSubstitutePathEqual(t, "c:\\folder\\should_not_be_replaced\\path", SubstitutePath("c:\\folder\\should_not_be_replaced\\path", [][2]string{{"should_not_be_replaced", ""}}))
+
+	// Mix absolute and relative mapping
+	assertSubstitutePathEqual(t, "c:\\new\\mapping\\path", SubstitutePath("D:\\original\\path", [][2]string{{"", "c:\\my\\asb\\folder\\"}, {"c:\\my\\asb\\folder\\", ""}, {"d:\\original", "c:\\new\\mapping"}}))
+	assertSubstitutePathEqual(t, "c:\\my\\asb\\folder\\path\\", SubstitutePath("path\\", [][2]string{{"d:\\original", "c:\\new\\mapping"}, {"", "c:\\my\\asb\\folder\\"}, {"c:\\my\\asb\\folder\\", ""}}))
+	assertSubstitutePathEqual(t, "path", SubstitutePath("C:\\my\\asb\\folder\\path", [][2]string{{"d:\\original", "c:\\new\\mapping"}, {"c:\\my\\asb\\folder\\", ""}, {"", "c:\\my\\asb\\folder\\"}}))
+}
diff --git a/pkg/proc/test/support.go b/pkg/proc/test/support.go
index 71cf4452..11abf9ba 100644
--- a/pkg/proc/test/support.go
+++ b/pkg/proc/test/support.go
@@ -93,7 +93,9 @@ func BuildFixture(name string, flags BuildFlags) Fixture {
 	}
 
 	if flags&EnableCGOOptimization == 0 {
-		os.Setenv("CGO_CFLAGS", "-O0 -g")
+		if os.Getenv("CI") == "" || os.Getenv("CGO_CFLAGS") == "" {
+			os.Setenv("CGO_CFLAGS", "-O0 -g")
+		}
 	}
 
 	fixturesDir := FindFixturesDir()
@@ -163,6 +165,9 @@ func BuildFixture(name string, flags BuildFlags) Fixture {
 
 	cmd := exec.Command("go", buildFlags...)
 	cmd.Dir = dir
+	if os.Getenv("CI") != "" {
+		cmd.Env = os.Environ()
+	}
 
 	// Build the test binary
 	if out, err := cmd.CombinedOutput(); err != nil {
diff --git a/pkg/proc/variables_test.go b/pkg/proc/variables_test.go
index 43403851..537bb86f 100644
--- a/pkg/proc/variables_test.go
+++ b/pkg/proc/variables_test.go
@@ -797,6 +797,9 @@ func TestEvalExpression(t *testing.T) {
 
 		// issue #3034 - map access with long string key
 		{`m6["very long string 0123456789a0123456789b0123456789c0123456789d0123456789e0123456789f0123456789g012345678h90123456789i0123456789j0123456789"]`, false, `123`, `123`, "int", nil},
+
+		// issue #1423 - special typecasts everywhere
+		{`string(byteslice) == "tèst"`, false, `true`, `false`, "", nil},
 	}
 
 	ver, _ := goversion.Parse(runtime.Version())
diff --git a/service/dap/config_test.go b/service/dap/config_test.go
index 225d05dd..d8ee2684 100644
--- a/service/dap/config_test.go
+++ b/service/dap/config_test.go
@@ -95,6 +95,74 @@ func TestConfigureSetSubstitutePath(t *testing.T) {
 			},
 			wantErr: false,
 		},
+		{
+			name: "add rule from empty string",
+			args: args{
+				args: &launchAttachArgs{
+					substitutePathClientToServer: [][2]string{},
+					substitutePathServerToClient: [][2]string{},
+				},
+				rest: `"" /path/to/client/dir`,
+			},
+			wantRules: [][2]string{{"", "/path/to/client/dir"}},
+			wantErr:   false,
+		},
+		{
+			name: "add rule to empty string",
+			args: args{
+				args: &launchAttachArgs{
+					substitutePathClientToServer: [][2]string{},
+					substitutePathServerToClient: [][2]string{},
+				},
+				rest: `/path/to/client/dir ""`,
+			},
+			wantRules: [][2]string{{"/path/to/client/dir", ""}},
+			wantErr:   false,
+		},
+		{
+			name: "add rule from empty string(multiple)",
+			args: args{
+				args: &launchAttachArgs{
+					substitutePathClientToServer: [][2]string{
+						{"/path/to/client/dir/a", "/path/to/server/dir/a"},
+						{"/path/to/client/dir/b", "/path/to/server/dir/b"},
+					},
+					substitutePathServerToClient: [][2]string{
+						{"/path/to/server/dir/a", "/path/to/client/dir/a"},
+						{"/path/to/server/dir/b", "/path/to/client/dir/b"},
+					},
+				},
+				rest: `"" /path/to/client/dir/c`,
+			},
+			wantRules: [][2]string{
+				{"/path/to/client/dir/a", "/path/to/server/dir/a"},
+				{"/path/to/client/dir/b", "/path/to/server/dir/b"},
+				{"", "/path/to/client/dir/c"},
+			},
+			wantErr: false,
+		},
+		{
+			name: "add rule to empty string(multiple)",
+			args: args{
+				args: &launchAttachArgs{
+					substitutePathClientToServer: [][2]string{
+						{"/path/to/client/dir/a", "/path/to/server/dir/a"},
+						{"/path/to/client/dir/b", "/path/to/server/dir/b"},
+					},
+					substitutePathServerToClient: [][2]string{
+						{"/path/to/server/dir/a", "/path/to/client/dir/a"},
+						{"/path/to/server/dir/b", "/path/to/client/dir/b"},
+					},
+				},
+				rest: `/path/to/client/dir/c ""`,
+			},
+			wantRules: [][2]string{
+				{"/path/to/client/dir/a", "/path/to/server/dir/a"},
+				{"/path/to/client/dir/b", "/path/to/server/dir/b"},
+				{"/path/to/client/dir/c", ""},
+			},
+			wantErr: false,
+		},
 		// Test modify rule.
 		{
 			name: "modify rule",
@@ -108,6 +176,30 @@ func TestConfigureSetSubstitutePath(t *testing.T) {
 			wantRules: [][2]string{{"/path/to/client/dir", "/new/path/to/server/dir"}},
 			wantErr:   false,
 		},
+		{
+			name: "modify rule with from as empty string",
+			args: args{
+				args: &launchAttachArgs{
+					substitutePathClientToServer: [][2]string{{"", "/path/to/server/dir"}},
+					substitutePathServerToClient: [][2]string{{"/path/to/server/dir", ""}},
+				},
+				rest: `"" /new/path/to/server/dir`,
+			},
+			wantRules: [][2]string{{"", "/new/path/to/server/dir"}},
+			wantErr:   false,
+		},
+		{
+			name: "modify rule with to as empty string",
+			args: args{
+				args: &launchAttachArgs{
+					substitutePathClientToServer: [][2]string{{"/path/to/client/dir", ""}},
+					substitutePathServerToClient: [][2]string{{"", "/path/to/client/dir"}},
+				},
+				rest: `/path/to/client/dir ""`,
+			},
+			wantRules: [][2]string{{"/path/to/client/dir", ""}},
+			wantErr:   false,
+		},
 		{
 			name: "modify rule (multiple)",
 			args: args{
@@ -132,6 +224,54 @@ func TestConfigureSetSubstitutePath(t *testing.T) {
 			},
 			wantErr: false,
 		},
+		{
+			name: "modify rule with from as empty string(multiple)",
+			args: args{
+				args: &launchAttachArgs{
+					substitutePathClientToServer: [][2]string{
+						{"/path/to/client/dir/a", "/path/to/server/dir/a"},
+						{"", "/path/to/server/dir/b"},
+						{"/path/to/client/dir/c", "/path/to/server/dir/b"},
+					},
+					substitutePathServerToClient: [][2]string{
+						{"/path/to/server/dir/a", "/path/to/client/dir/a"},
+						{"/path/to/server/dir/b", ""},
+						{"/path/to/server/dir/b", "/path/to/client/dir/c"},
+					},
+				},
+				rest: `"" /new/path`,
+			},
+			wantRules: [][2]string{
+				{"/path/to/client/dir/a", "/path/to/server/dir/a"},
+				{"", "/new/path"},
+				{"/path/to/client/dir/c", "/path/to/server/dir/b"},
+			},
+			wantErr: false,
+		},
+		{
+			name: "modify rule with to as empty string(multiple)",
+			args: args{
+				args: &launchAttachArgs{
+					substitutePathClientToServer: [][2]string{
+						{"/path/to/client/dir/a", "/path/to/server/dir/a"},
+						{"/path/to/client/dir/b", "/path/to/server/dir/b"},
+						{"/path/to/client/dir/c", "/path/to/server/dir/b"},
+					},
+					substitutePathServerToClient: [][2]string{
+						{"/path/to/server/dir/a", "/path/to/client/dir/a"},
+						{"/path/to/server/dir/b", "/path/to/client/dir/b"},
+						{"/path/to/server/dir/b", "/path/to/client/dir/c"},
+					},
+				},
+				rest: `/path/to/client/dir/b ""`,
+			},
+			wantRules: [][2]string{
+				{"/path/to/client/dir/a", "/path/to/server/dir/a"},
+				{"/path/to/client/dir/b", ""},
+				{"/path/to/client/dir/c", "/path/to/server/dir/b"},
+			},
+			wantErr: false,
+		},
 		// Test delete rule.
 		{
 			name: "delete rule",
@@ -145,6 +285,18 @@ func TestConfigureSetSubstitutePath(t *testing.T) {
 			wantRules: [][2]string{},
 			wantErr:   false,
 		},
+		{
+			name: "delete rule, empty string",
+			args: args{
+				args: &launchAttachArgs{
+					substitutePathClientToServer: [][2]string{{"", "/path/to/server/dir"}},
+					substitutePathServerToClient: [][2]string{{"/path/to/server/dir", ""}},
+				},
+				rest: `""`,
+			},
+			wantRules: [][2]string{},
+			wantErr:   false,
+		},
 		// Test invalid input.
 		{
 			name: "error on empty args",