Repository navigation
Expand file tree
/
Copy pathprotocol.go
More file actions
447 lines (397 loc) · 13.5 KB
/
Copy pathprotocol.go
File metadata and controls
447 lines (397 loc) · 13.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
// Package dbgp implements the DBGp protocol for PHP debugging
package dbgp
import (
"bytes"
"encoding/base64"
"encoding/xml"
"fmt"
"io"
"net/url"
"path/filepath"
"strings"
"unicode"
"golang.org/x/text/encoding/charmap"
)
// charsetReader handles non-UTF-8 XML encodings, decoding as it reads, so a
// large packet is not copied whole.
func charsetReader(encoding string, input io.Reader) (io.Reader, error) {
switch strings.ToLower(encoding) {
case "utf-8", "us-ascii":
return input, nil
case "iso-8859-1", "latin1":
return charmap.ISO8859_1.NewDecoder().Reader(input), nil
case "windows-1252":
return charmap.Windows1252.NewDecoder().Reader(input), nil
default:
return nil, fmt.Errorf("unsupported charset: %s", encoding)
}
}
// Status values
const (
StatusStarting = "starting"
StatusStopping = "stopping"
StatusStopped = "stopped"
StatusRunning = "running"
StatusBreak = "break"
StatusDetached = "detached"
)
// Breakpoint types
const (
BreakpointLine = "line"
BreakpointConditional = "conditional"
BreakpointCall = "call"
BreakpointReturn = "return"
BreakpointException = "exception"
BreakpointWatch = "watch"
)
// InitPacket is sent by PHP when connection is established
type InitPacket struct {
XMLName xml.Name `xml:"init"`
AppID string `xml:"appid,attr"`
IDEKey string `xml:"idekey,attr"`
Session string `xml:"session,attr"`
Thread string `xml:"thread,attr"`
Parent string `xml:"parent,attr"`
Language string `xml:"language,attr"`
Protocol string `xml:"protocol_version,attr"`
FileURI string `xml:"fileuri,attr"`
Engine Engine `xml:"engine"`
// EngineVersion is Engine.Version, kept for compatibility.
EngineVersion string `xml:"-"`
}
// Engine identifies the debugger engine, e.g. PHP Debugger or Xdebug.
type Engine struct {
Name string `xml:",chardata"`
Version string `xml:"version,attr"`
}
// Response is the generic DBGp response
type Response struct {
XMLName xml.Name `xml:"response"`
Command string `xml:"command,attr"`
Transaction int `xml:"transaction_id,attr"`
Status string `xml:"status,attr,omitempty"`
Reason string `xml:"reason,attr,omitempty"`
Success string `xml:"success,attr,omitempty"`
BreakpointID int `xml:"id,attr,omitempty"`
Encoding string `xml:"encoding,attr,omitempty"`
// For feature_get: "0" when the engine does not support the feature
FeatureName string `xml:"feature_name,attr,omitempty"`
Supported string `xml:"supported,attr,omitempty"`
// For breakpoint_get, breakpoint_list, breakpoint_remove
Breakpoints []BreakpointInfo `xml:"breakpoint,omitempty"`
// For context_get, property_get and eval
Context int `xml:"context,attr,omitempty"`
Properties []Property `xml:"property,omitempty"`
// For property_value
Type string `xml:"type,attr,omitempty"`
Size int `xml:"size,attr,omitempty"`
// For context_names
Contexts []ContextName `xml:"context,omitempty"`
// For typemap_get
TypeMap []TypeMapEntry `xml:"map,omitempty"`
// For stack_get and stack_depth
Stack []StackFrame `xml:"stack,omitempty"`
Depth int `xml:"depth,attr,omitempty"`
// For errors
Error *Error `xml:"error,omitempty"`
// Message for breakpoint hit
Message *Message `xml:"https://xdebug.org/dbgp/xdebug message,omitempty"`
// Raw for debugging
Raw string `xml:",innerxml"`
// Value contains raw response chardata when present
Value string `xml:",chardata"`
}
// Error represents a DBGp error
type Error struct {
Code int `xml:"code,attr"`
Message string `xml:"message"`
}
// Message is the xdebug:message element: where execution stopped, or the
// PHP error an "error" notification reports. Exception, Type, Code and Text
// are set for exceptions and errors.
type Message struct {
Filename string `xml:"filename,attr"`
Lineno int `xml:"lineno,attr"`
Exception string `xml:"exception,attr,omitempty"`
Type string `xml:"type,attr,omitempty"`
Code string `xml:"code,attr,omitempty"`
Text string `xml:",chardata"`
}
// Notification is an asynchronous notify packet from the engine, such as
// "error" (a PHP warning or notice) or "breakpoint_resolved".
type Notification struct {
XMLName xml.Name `xml:"notify"`
Name string `xml:"name,attr"`
Message *Message `xml:"https://xdebug.org/dbgp/xdebug message"`
Breakpoint *BreakpointInfo `xml:"breakpoint"`
}
// Stream is program output the engine forwards (see the stdout command).
type Stream struct {
XMLName xml.Name `xml:"stream"`
Type string `xml:"type,attr"`
Encoding string `xml:"encoding,attr"`
Value string `xml:",chardata"`
}
// BreakpointInfo contains breakpoint details
type BreakpointInfo struct {
ID int `xml:"id,attr"`
Type string `xml:"type,attr"`
Filename string `xml:"filename,attr"`
Lineno int `xml:"lineno,attr"`
Function string `xml:"function,attr,omitempty"`
State string `xml:"state,attr"`
Resolved string `xml:"resolved,attr,omitempty"`
Exception string `xml:"exception,attr,omitempty"`
HitCount int `xml:"hit_count,attr,omitempty"`
HitValue int `xml:"hit_value,attr,omitempty"`
HitCondition string `xml:"hit_condition,attr,omitempty"`
Temporary int `xml:"temporary,attr,omitempty"`
// Expression is the decoded condition of a conditional breakpoint.
Expression string `xml:"-"`
}
// UnmarshalXML reads the expression from its <expression> child element
// (base64-encoded by Xdebug and PHP Debugger), or from an expression
// attribute as some engines send it.
func (b *BreakpointInfo) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
type plain BreakpointInfo // drops this method, avoiding recursion
var raw struct {
plain
ExpressionAttr string `xml:"expression,attr"`
ExpressionElem *struct {
Encoding string `xml:"encoding,attr"`
Value string `xml:",chardata"`
} `xml:"expression"`
}
if err := d.DecodeElement(&raw, &start); err != nil {
return err
}
*b = BreakpointInfo(raw.plain)
b.Expression = raw.ExpressionAttr
if e := raw.ExpressionElem; e != nil {
expr, err := decodeEncoded(e.Encoding, e.Value)
if err != nil {
return fmt.Errorf("breakpoint %d expression: %w", b.ID, err)
}
b.Expression = expr
}
return nil
}
// decodeEncoded decodes a value sent with the given DBGp encoding attribute.
func decodeEncoded(encoding, value string) (string, error) {
switch encoding {
case "", "none":
return value, nil
case "base64":
cleaned := strings.Map(func(r rune) rune {
if unicode.IsSpace(r) {
return -1
}
return r
}, value)
decoded, err := base64.StdEncoding.DecodeString(cleaned)
if err != nil {
return "", fmt.Errorf("decode base64: %w", err)
}
return string(decoded), nil
default:
return "", fmt.Errorf("unsupported encoding %q", encoding)
}
}
// Property represents a variable
type Property struct {
Name string `xml:"name,attr"`
FullName string `xml:"fullname,attr"`
Type string `xml:"type,attr"`
ClassName string `xml:"classname,attr,omitempty"`
Facet string `xml:"facet,attr,omitempty"`
Size int `xml:"size,attr,omitempty"` // full length of a string value
Children int `xml:"children,attr,omitempty"`
NumChildren int `xml:"numchildren,attr,omitempty"`
// Page and PageSize locate ChildProperties among all NumChildren.
Page int `xml:"page,attr,omitempty"`
PageSize int `xml:"pagesize,attr,omitempty"`
Encoding string `xml:"encoding,attr,omitempty"`
Value string `xml:",chardata"`
ChildProperties []Property `xml:"property,omitempty"`
}
// DecodedValue returns the property's value with its encoding removed.
// Arrays and objects have no value of their own; see ChildProperties.
func (p Property) DecodedValue() (string, error) {
return decodeEncoded(p.Encoding, p.Value)
}
// Truncated reports whether the engine sent only part of a string value
// (see the max_data feature); GetPropertyValue returns all of it.
func (p Property) Truncated() bool {
value, err := p.DecodedValue()
return err == nil && p.Size > len(value)
}
// Pages returns how many pages of children the property has.
func (p Property) Pages() int {
if p.PageSize <= 0 || p.NumChildren == 0 {
return 1
}
return (p.NumChildren + p.PageSize - 1) / p.PageSize
}
// ContextName is a variable context, such as Locals or Superglobals.
type ContextName struct {
ID int `xml:"id,attr"`
Name string `xml:"name,attr"`
}
// TypeMapEntry maps a language type to its DBGp type and XML Schema type.
type TypeMapEntry struct {
Name string // PHP type name, e.g. "int"
Type string // DBGp common type, e.g. "int" or "hash"
SchemaType string // XML Schema type, e.g. "xsd:decimal"; may be empty
}
const xsiNamespace = "http://www.w3.org/2001/XMLSchema-instance"
// UnmarshalXML reads a map element. It has both type and xsi:type
// attributes, which plain struct tags cannot tell apart.
func (m *TypeMapEntry) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
for _, a := range start.Attr {
switch {
case a.Name.Local == "name":
m.Name = a.Value
case a.Name.Local == "type" && a.Name.Space == "":
m.Type = a.Value
case a.Name.Local == "type" && a.Name.Space == xsiNamespace:
m.SchemaType = a.Value
}
}
return d.Skip()
}
// StackFrame represents a call stack entry
type StackFrame struct {
Level int `xml:"level,attr"`
Type string `xml:"type,attr"`
Filename string `xml:"filename,attr"`
Lineno int `xml:"lineno,attr"`
Where string `xml:"where,attr"`
Cmmd string `xml:"cmmd,attr,omitempty"`
// Facet is "mapped" when the engine translated Filename with its own
// path maps, or "skipped" for code its maps mark as skipped.
Facet string `xml:"https://xdebug.org/dbgp/xdebug facet,attr,omitempty"`
}
// ParseInit parses the init packet from PHP
func ParseInit(data []byte) (*InitPacket, error) {
var init InitPacket
decoder := xml.NewDecoder(bytes.NewReader(data))
decoder.CharsetReader = charsetReader
if err := decoder.Decode(&init); err != nil {
return nil, fmt.Errorf("parse init: %w", err)
}
init.EngineVersion = init.Engine.Version
return &init, nil
}
// ParseResponse parses a DBGp response
func ParseResponse(data []byte) (*Response, error) {
var resp Response
decoder := xml.NewDecoder(bytes.NewReader(data))
decoder.CharsetReader = charsetReader
if err := decoder.Decode(&resp); err != nil {
return nil, fmt.Errorf("parse response: %w", err)
}
return &resp, nil
}
// ParseNotification parses a notify packet
func ParseNotification(data []byte) (*Notification, error) {
var n Notification
decoder := xml.NewDecoder(bytes.NewReader(data))
decoder.CharsetReader = charsetReader
if err := decoder.Decode(&n); err != nil {
return nil, fmt.Errorf("parse notification: %w", err)
}
return &n, nil
}
// ParseStream parses a stream packet and returns its decoded output
func ParseStream(data []byte) (*Stream, string, error) {
var st Stream
decoder := xml.NewDecoder(bytes.NewReader(data))
decoder.CharsetReader = charsetReader
if err := decoder.Decode(&st); err != nil {
return nil, "", fmt.Errorf("parse stream: %w", err)
}
text, err := decodeEncoded(st.Encoding, st.Value)
if err != nil {
return nil, "", fmt.Errorf("parse stream: %w", err)
}
return &st, text, nil
}
// packetKind returns the root element name of a packet: "init",
// "response", "notify" or "stream".
func packetKind(data []byte) (string, error) {
decoder := xml.NewDecoder(bytes.NewReader(data))
decoder.CharsetReader = charsetReader
for {
tok, err := decoder.Token()
if err != nil {
return "", fmt.Errorf("read packet root: %w", err)
}
if start, ok := tok.(xml.StartElement); ok {
return start.Name.Local, nil
}
}
}
// ParseMessage extracts breakpoint hit info from response
func (r *Response) ParseMessage() (file string, line int) {
if r.Message != nil {
file = FormatFileURI(r.Message.Filename)
line = r.Message.Lineno
}
return
}
// FormatStack formats the call stack for display
func FormatStack(frames []StackFrame) []string {
lines := make([]string, len(frames))
for i, f := range frames {
file := FormatFileURI(f.Filename)
lines[i] = fmt.Sprintf("#%d %s() at %s:%d", f.Level, f.Where, file, f.Lineno)
}
return lines
}
// FormatFileURI converts file:// URI to path
func FormatFileURI(uri string) string {
u, err := url.Parse(uri)
if err != nil || u.Scheme != "file" {
return strings.TrimPrefix(uri, "file://")
}
path, err := url.PathUnescape(u.Path)
if err != nil {
path = u.Path
}
if len(path) >= 3 && path[0] == '/' && path[2] == ':' {
path = path[1:]
}
if u.Host != "" {
return "//" + u.Host + filepath.FromSlash(path)
}
return filepath.FromSlash(path)
}
// MakeFileURI converts path to file:// URI
func MakeFileURI(path string) string {
if strings.HasPrefix(path, "file://") {
u, err := url.Parse(path)
if err == nil {
return u.String()
}
return path
}
if strings.HasPrefix(path, `\\`) || strings.HasPrefix(path, "//") {
trimmed := strings.TrimLeft(path, `/\`)
parts := strings.SplitN(trimmed, `\`, 2)
if len(parts) == 1 {
parts = strings.SplitN(trimmed, "/", 2)
}
host := parts[0]
sharePath := ""
if len(parts) == 2 {
sharePath = "/" + strings.ReplaceAll(parts[1], `\`, `/`)
}
return (&url.URL{Scheme: "file", Host: host, Path: sharePath}).String()
}
slashed := strings.ReplaceAll(path, `\`, `/`)
slashed = filepath.ToSlash(slashed)
if strings.HasPrefix(slashed, "/") {
return (&url.URL{Scheme: "file", Path: slashed}).String()
}
return (&url.URL{Scheme: "file", Path: "/" + slashed}).String()
}