funcr: Escape strings when they are not known-safe
Tim Hockin
2 years ago
235 | 235 | if hook := f.opts.RenderBuiltinsHook; hook != nil { |
236 | 236 | vals = hook(f.sanitize(vals)) |
237 | 237 | } |
238 | f.flatten(buf, vals, false) | |
238 | f.flatten(buf, vals, false, false) // keys are ours, no need to escape | |
239 | 239 | continuing := len(builtins) > 0 |
240 | 240 | if len(f.valuesStr) > 0 { |
241 | 241 | if continuing { |
252 | 252 | if hook := f.opts.RenderArgsHook; hook != nil { |
253 | 253 | vals = hook(f.sanitize(vals)) |
254 | 254 | } |
255 | f.flatten(buf, vals, continuing) | |
255 | f.flatten(buf, vals, continuing, true) // escape user-provided keys | |
256 | 256 | if f.outputFormat == outputJSON { |
257 | 257 | buf.WriteByte('}') |
258 | 258 | } |
262 | 262 | // flatten renders a list of key-value pairs into a buffer. If continuing is |
263 | 263 | // true, it assumes that the buffer has previous values and will emit a |
264 | 264 | // separator (which depends on the output format) before the first pair it |
265 | // writes. This also returns a potentially modified version of kvList, which | |
265 | // writes. If escapeKeys is true, the keys are assumed to have | |
266 | // non-JSON-compatible characters in them and must be evaluated for escapes. | |
267 | // | |
268 | // This function returns a potentially modified version of kvList, which | |
266 | 269 | // ensures that there is a value for every key (adding a value if needed) and |
267 | 270 | // that each key is a string (substituting a key if needed). |
268 | func (f Formatter) flatten(buf *bytes.Buffer, kvList []interface{}, continuing bool) []interface{} { | |
271 | func (f Formatter) flatten(buf *bytes.Buffer, kvList []interface{}, continuing bool, escapeKeys bool) []interface{} { | |
269 | 272 | // This logic overlaps with sanitize() but saves one type-cast per key, |
270 | 273 | // which can be measurable. |
271 | 274 | if len(kvList)%2 != 0 { |
289 | 292 | } |
290 | 293 | } |
291 | 294 | |
292 | buf.WriteByte('"') | |
293 | buf.WriteString(k) | |
294 | buf.WriteByte('"') | |
295 | if escapeKeys { | |
296 | buf.WriteString(prettyString(k)) | |
297 | } else { | |
298 | // this is faster | |
299 | buf.WriteByte('"') | |
300 | buf.WriteString(k) | |
301 | buf.WriteByte('"') | |
302 | } | |
295 | 303 | if f.outputFormat == outputJSON { |
296 | 304 | buf.WriteByte(':') |
297 | 305 | } else { |
307 | 315 | } |
308 | 316 | |
309 | 317 | const ( |
310 | flagRawString = 0x1 // do not print quotes on strings | |
311 | flagRawStruct = 0x2 // do not print braces on structs | |
318 | flagRawStruct = 0x1 // do not print braces on structs | |
312 | 319 | ) |
313 | 320 | |
314 | 321 | // TODO: This is not fast. Most of the overhead goes here. |
333 | 340 | case bool: |
334 | 341 | return strconv.FormatBool(v) |
335 | 342 | case string: |
336 | if flags&flagRawString > 0 { | |
337 | return v | |
338 | } | |
339 | // This is empirically faster than strings.Builder. | |
340 | return strconv.Quote(v) | |
343 | return prettyString(v) | |
341 | 344 | case int: |
342 | 345 | return strconv.FormatInt(int64(v), 10) |
343 | 346 | case int8: |
378 | 381 | if i > 0 { |
379 | 382 | buf.WriteByte(',') |
380 | 383 | } |
381 | buf.WriteByte('"') | |
382 | buf.WriteString(v[i].(string)) | |
383 | buf.WriteByte('"') | |
384 | // arbitrary keys might need escaping | |
385 | buf.WriteString(prettyString(v[i].(string))) | |
384 | 386 | buf.WriteByte(':') |
385 | 387 | buf.WriteString(f.pretty(v[i+1])) |
386 | 388 | } |
400 | 402 | case reflect.Bool: |
401 | 403 | return strconv.FormatBool(v.Bool()) |
402 | 404 | case reflect.String: |
403 | if flags&flagRawString > 0 { | |
404 | return v.String() | |
405 | } | |
406 | // This is empirically faster than strings.Builder. | |
407 | return `"` + v.String() + `"` | |
405 | return prettyString(v.String()) | |
408 | 406 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
409 | 407 | return strconv.FormatInt(int64(v.Int()), 10) |
410 | 408 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: |
462 | 460 | if name == "" { |
463 | 461 | name = fld.Name |
464 | 462 | } |
463 | // field names can't contain characters which need escaping | |
465 | 464 | buf.WriteByte('"') |
466 | 465 | buf.WriteString(name) |
467 | 466 | buf.WriteByte('"') |
492 | 491 | if i > 0 { |
493 | 492 | buf.WriteByte(',') |
494 | 493 | } |
495 | // JSON only does string keys. | |
496 | buf.WriteByte('"') | |
497 | buf.WriteString(f.prettyWithFlags(it.Key().Interface(), flagRawString)) | |
498 | buf.WriteByte('"') | |
494 | // prettyWithFlags will produce already-escaped values | |
495 | keystr := f.prettyWithFlags(it.Key().Interface(), 0) | |
496 | if t.Key().Kind() != reflect.String { | |
497 | // JSON only does string keys. Unlike Go's standard JSON, we'll | |
498 | // convert just about anything to a string. | |
499 | keystr = prettyString(keystr) | |
500 | } | |
501 | buf.WriteString(keystr) | |
499 | 502 | buf.WriteByte(':') |
500 | 503 | buf.WriteString(f.pretty(it.Value().Interface())) |
501 | 504 | i++ |
509 | 512 | return f.pretty(v.Elem().Interface()) |
510 | 513 | } |
511 | 514 | return fmt.Sprintf(`"<unhandled-%s>"`, t.Kind().String()) |
515 | } | |
516 | ||
517 | func prettyString(s string) string { | |
518 | // Avoid escaping (which does allocations) if we can. | |
519 | if needsEscape(s) { | |
520 | return strconv.Quote(s) | |
521 | } | |
522 | b := bytes.NewBuffer(make([]byte, 0, 1024)) | |
523 | b.WriteByte('"') | |
524 | b.WriteString(s) | |
525 | b.WriteByte('"') | |
526 | return b.String() | |
527 | } | |
528 | ||
529 | // needsEscape determines whether the input string needs to be escaped or not, | |
530 | // without doing any allocations. | |
531 | func needsEscape(s string) bool { | |
532 | for _, r := range s { | |
533 | if !strconv.IsPrint(r) || r == '\\' || r == '"' { | |
534 | return true | |
535 | } | |
536 | } | |
537 | return false | |
512 | 538 | } |
513 | 539 | |
514 | 540 | func isEmpty(v reflect.Value) bool { |
682 | 708 | |
683 | 709 | // Pre-render values, so we don't have to do it on each Info/Error call. |
684 | 710 | buf := bytes.NewBuffer(make([]byte, 0, 1024)) |
685 | f.values = f.flatten(buf, vals, false) | |
711 | f.values = f.flatten(buf, vals, false, true) // escape user-provided keys | |
686 | 712 | f.valuesStr = buf.String() |
687 | 713 | } |
688 | 714 |
26 | 26 | "github.com/go-logr/logr" |
27 | 27 | ) |
28 | 28 | |
29 | // Will be handled via reflection instead of type assertions. | |
29 | 30 | type substr string |
30 | 31 | |
31 | 32 | func ptrint(i int) *int { |
197 | 198 | exp string // used in cases where JSON can't handle it |
198 | 199 | }{ |
199 | 200 | {val: "strval"}, |
201 | {val: "strval\nwith\t\"escapes\""}, | |
200 | 202 | {val: substr("substrval")}, |
203 | {val: substr("substrval\nwith\t\"escapes\"")}, | |
201 | 204 | {val: true}, |
202 | 205 | {val: false}, |
203 | 206 | {val: int(93)}, |
234 | 237 | exp: `[]`, |
235 | 238 | }, |
236 | 239 | {val: []int{9, 3, 7, 6}}, |
240 | {val: []string{"str", "with\tescape"}}, | |
241 | {val: []substr{"substr", "with\tescape"}}, | |
237 | 242 | {val: [4]int{9, 3, 7, 6}}, |
243 | {val: [2]string{"str", "with\tescape"}}, | |
244 | {val: [2]substr{"substr", "with\tescape"}}, | |
238 | 245 | { |
239 | 246 | val: struct { |
240 | 247 | Int int |
255 | 262 | }, |
256 | 263 | }, |
257 | 264 | { |
265 | val: map[string]int{ | |
266 | "with\tescape": 76, | |
267 | }, | |
268 | }, | |
269 | { | |
258 | 270 | val: map[substr]int{ |
259 | 271 | "nine": 3, |
260 | 272 | }, |
273 | }, | |
274 | { | |
275 | val: map[substr]int{ | |
276 | "with\tescape": 76, | |
277 | }, | |
278 | }, | |
279 | { | |
280 | val: map[int]int{ | |
281 | 9: 3, | |
282 | }, | |
283 | }, | |
284 | { | |
285 | val: map[float64]int{ | |
286 | 9.5: 3, | |
287 | }, | |
288 | exp: `{"9.5":3}`, | |
261 | 289 | }, |
262 | 290 | { |
263 | 291 | val: struct { |
282 | 310 | val: []struct{ X, Y string }{ |
283 | 311 | {"nine", "three"}, |
284 | 312 | {"seven", "six"}, |
313 | {"with\t", "\tescapes"}, | |
285 | 314 | }, |
286 | 315 | }, |
287 | 316 | { |
436 | 465 | { |
437 | 466 | val: PseudoStruct(makeKV("f1", 1, "f2", true, "f3", []int{})), |
438 | 467 | exp: `{"f1":1,"f2":true,"f3":[]}`, |
468 | }, | |
469 | { | |
470 | val: map[TjsontagsString]int{ | |
471 | {String1: `"quoted"`, String4: `unquoted`}: 1, | |
472 | }, | |
473 | exp: `{"{\"string1\":\"\\\"quoted\\\"\",\"-\":\"\",\"string4\":\"unquoted\",\"String5\":\"\"}":1}`, | |
474 | }, | |
475 | { | |
476 | val: map[TjsontagsInt]int{ | |
477 | {Int1: 1, Int2: 2}: 3, | |
478 | }, | |
479 | exp: `{"{\"int1\":1,\"-\":0,\"Int5\":0}":3}`, | |
480 | }, | |
481 | { | |
482 | val: map[[2]struct{ S string }]int{ | |
483 | {{S: `"quoted"`}, {S: "unquoted"}}: 1, | |
484 | }, | |
485 | exp: `{"[{\"S\":\"\\\"quoted\\\"\"},{\"S\":\"unquoted\"}]":1}`, | |
439 | 486 | }, |
440 | 487 | } |
441 | 488 | |
448 | 495 | } else { |
449 | 496 | jb, err := json.Marshal(tc.val) |
450 | 497 | if err != nil { |
451 | t.Errorf("[%d]: unexpected error: %v", i, err) | |
498 | t.Fatalf("[%d]: unexpected error: %v", i, err) | |
452 | 499 | } |
453 | 500 | want = string(jb) |
454 | 501 | } |
496 | 543 | expectKV: `"int"={"intsub":1} "str"={"strsub":"2"} "bool"={"boolsub":true}`, |
497 | 544 | expectJSON: `{"int":{"intsub":1},"str":{"strsub":"2"},"bool":{"boolsub":true}}`, |
498 | 545 | }, { |
546 | name: "escapes", | |
547 | builtins: makeKV("\"1\"", 1), // will not be escaped, but should never happen | |
548 | values: makeKV("\tstr", "ABC"), // escaped | |
549 | args: makeKV("bool\n", true), // escaped | |
550 | expectKV: `""1""=1 "\tstr"="ABC" "bool\n"=true`, | |
551 | expectJSON: `{""1"":1,"\tstr":"ABC","bool\n":true}`, | |
552 | }, { | |
499 | 553 | name: "missing value", |
500 | 554 | builtins: makeKV("builtin"), |
501 | 555 | values: makeKV("value"), |
504 | 558 | expectJSON: `{"builtin":"<no-value>","value":"<no-value>","arg":"<no-value>"}`, |
505 | 559 | }, { |
506 | 560 | name: "non-string key int", |
507 | args: makeKV(123, "val"), | |
561 | builtins: makeKV(123, "val"), // should never happen | |
508 | 562 | values: makeKV(456, "val"), |
509 | builtins: makeKV(789, "val"), | |
510 | expectKV: `"<non-string-key: 789>"="val" "<non-string-key: 456>"="val" "<non-string-key: 123>"="val"`, | |
511 | expectJSON: `{"<non-string-key: 789>":"val","<non-string-key: 456>":"val","<non-string-key: 123>":"val"}`, | |
563 | args: makeKV(789, "val"), | |
564 | expectKV: `"<non-string-key: 123>"="val" "<non-string-key: 456>"="val" "<non-string-key: 789>"="val"`, | |
565 | expectJSON: `{"<non-string-key: 123>":"val","<non-string-key: 456>":"val","<non-string-key: 789>":"val"}`, | |
512 | 566 | }, { |
513 | 567 | name: "non-string key struct", |
514 | args: makeKV(struct { | |
568 | builtins: makeKV(struct { // will not be escaped, but should never happen | |
515 | 569 | F1 string |
516 | 570 | F2 int |
517 | }{"arg", 123}, "val"), | |
571 | }{"builtin", 123}, "val"), | |
518 | 572 | values: makeKV(struct { |
519 | 573 | F1 string |
520 | 574 | F2 int |
521 | 575 | }{"value", 456}, "val"), |
522 | builtins: makeKV(struct { | |
576 | args: makeKV(struct { | |
523 | 577 | F1 string |
524 | 578 | F2 int |
525 | }{"builtin", 789}, "val"), | |
526 | expectKV: `"<non-string-key: {"F1":"builtin",>"="val" "<non-string-key: {"F1":"value","F>"="val" "<non-string-key: {"F1":"arg","F2">"="val"`, | |
527 | expectJSON: `{"<non-string-key: {"F1":"builtin",>":"val","<non-string-key: {"F1":"value","F>":"val","<non-string-key: {"F1":"arg","F2">":"val"}`, | |
579 | }{"arg", 789}, "val"), | |
580 | expectKV: `"<non-string-key: {"F1":"builtin",>"="val" "<non-string-key: {\"F1\":\"value\",\"F>"="val" "<non-string-key: {\"F1\":\"arg\",\"F2\">"="val"`, | |
581 | expectJSON: `{"<non-string-key: {"F1":"builtin",>":"val","<non-string-key: {\"F1\":\"value\",\"F>":"val","<non-string-key: {\"F1\":\"arg\",\"F2\">":"val"}`, | |
528 | 582 | }} |
529 | 583 | |
530 | 584 | for _, tc := range testCases { |
533 | 587 | formatter.AddValues(tc.values) |
534 | 588 | r := formatter.render(tc.builtins, tc.args) |
535 | 589 | if r != expect { |
536 | t.Errorf("wrong output:\nexpected %q\n got %q", expect, r) | |
590 | t.Errorf("wrong output:\nexpected %v\n got %v", expect, r) | |
537 | 591 | } |
538 | 592 | } |
539 | 593 | t.Run("KV", func(t *testing.T) { |