clash_yaml.go 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. package sub
  2. import (
  3. "reflect"
  4. "regexp"
  5. "strings"
  6. "github.com/goccy/go-yaml"
  7. )
  8. // yamlQuotedString is a string that must reach a YAML parser as a string.
  9. // Single-quoted style is used because it carries the value literally — no
  10. // escape processing — which suits hex ids, passwords and pre-shared keys.
  11. type yamlQuotedString string
  12. func (s yamlQuotedString) MarshalYAML() ([]byte, error) {
  13. return []byte("'" + strings.ReplaceAll(string(s), "'", "''") + "'"), nil
  14. }
  15. // yamlPlainScalarNotString matches the plain scalars a YAML parser resolves to
  16. // something other than a string. It covers the YAML 1.2 core schema plus the
  17. // 1.1 forms go-yaml v3 — the library the Clash cores use — still resolves:
  18. // null, booleans (including y/yes/on and friends), integers in every base,
  19. // sexagesimals, floats, and dates.
  20. var yamlPlainScalarNotString = regexp.MustCompile(`^(?:` +
  21. `~|[Nn]ull|NULL|` +
  22. `[Tt]rue|TRUE|[Ff]alse|FALSE|[Yy]es|YES|[Nn]o|NO|[Oo]n|ON|[Oo]ff|OFF|[YyNn]|` +
  23. `[-+]?[0-9]+|` +
  24. `0[oO]?[0-7]+|0[xX][0-9a-fA-F]+|` +
  25. `[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+|` +
  26. `[-+]?(?:[0-9]*\.[0-9]+|[0-9]+\.?[0-9]*)(?:[eE][-+]?[0-9]+)?|` +
  27. `[-+]?\.(?:inf|Inf|INF)|\.(?:nan|NaN|NAN)|` +
  28. `[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}(?:[Tt ].*)?` +
  29. `)$`)
  30. // yamlScalarIsAmbiguous reports whether s must be quoted to survive as a string.
  31. //
  32. // The encoder quotes most of these itself, but not the exponent-float form: a
  33. // REALITY short-id such as "2351e1" is valid hex and, emitted bare, is read as
  34. // 23510. A Clash core then hex-decodes a five-digit number, rejects the proxy
  35. // and drops the whole provider to zero nodes (#6104). goccy's own parser reads
  36. // that token back as a string, so the encoder never sees a problem and a
  37. // round-trip check through it cannot find one either — the resolution rules,
  38. // not the encoder, are the thing to test against. The same shape reaches
  39. // passwords, obfs-passwords and pre-shared keys, so every string in the
  40. // document is checked rather than an enumerated list of fields.
  41. func yamlScalarIsAmbiguous(s string) bool {
  42. if s == "" {
  43. return false
  44. }
  45. return yamlPlainScalarNotString.MatchString(s)
  46. }
  47. // quoteAmbiguousYAMLScalars rebuilds v with every ambiguous string wrapped so
  48. // the encoder is forced to quote it. Containers are rebuilt as map[string]any /
  49. // []any because a typed container cannot hold the wrapper; unambiguous values
  50. // are carried through untouched, so the emitted document is unchanged apart
  51. // from the quotes that were missing.
  52. func quoteAmbiguousYAMLScalars(v any) any {
  53. if v == nil {
  54. return nil
  55. }
  56. if s, ok := v.(string); ok {
  57. if yamlScalarIsAmbiguous(s) {
  58. return yamlQuotedString(s)
  59. }
  60. return s
  61. }
  62. rv := reflect.ValueOf(v)
  63. switch rv.Kind() {
  64. case reflect.Map:
  65. out := make(map[string]any, rv.Len())
  66. iter := rv.MapRange()
  67. for iter.Next() {
  68. key, ok := iter.Key().Interface().(string)
  69. if !ok {
  70. return v
  71. }
  72. out[key] = quoteAmbiguousYAMLScalars(iter.Value().Interface())
  73. }
  74. return out
  75. case reflect.Slice, reflect.Array:
  76. if rv.Kind() == reflect.Slice && rv.Type().Elem().Kind() == reflect.Uint8 {
  77. return v
  78. }
  79. out := make([]any, rv.Len())
  80. for i := range rv.Len() {
  81. out[i] = quoteAmbiguousYAMLScalars(rv.Index(i).Interface())
  82. }
  83. return out
  84. case reflect.Ptr, reflect.Interface:
  85. if rv.IsNil() {
  86. return v
  87. }
  88. return quoteAmbiguousYAMLScalars(rv.Elem().Interface())
  89. default:
  90. return v
  91. }
  92. }
  93. // marshalClashYAML serializes a Clash config, keeping string values typed as
  94. // strings in the output.
  95. func marshalClashYAML(config any) ([]byte, error) {
  96. return yaml.Marshal(quoteAmbiguousYAMLScalars(config))
  97. }