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/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.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/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/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", 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 }