| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106 |
- package sub
- import (
- "reflect"
- "regexp"
- "strings"
- "github.com/goccy/go-yaml"
- )
- // yamlQuotedString is a string that must reach a YAML parser as a string.
- // Single-quoted style is used because it carries the value literally — no
- // escape processing — which suits hex ids, passwords and pre-shared keys.
- type yamlQuotedString string
- func (s yamlQuotedString) MarshalYAML() ([]byte, error) {
- return []byte("'" + strings.ReplaceAll(string(s), "'", "''") + "'"), nil
- }
- // yamlPlainScalarNotString matches the plain scalars a YAML parser resolves to
- // something other than a string. It covers the YAML 1.2 core schema plus the
- // 1.1 forms go-yaml v3 — the library the Clash cores use — still resolves:
- // null, booleans (including y/yes/on and friends), integers in every base,
- // sexagesimals, floats, and dates.
- var yamlPlainScalarNotString = regexp.MustCompile(`^(?:` +
- `~|[Nn]ull|NULL|` +
- `[Tt]rue|TRUE|[Ff]alse|FALSE|[Yy]es|YES|[Nn]o|NO|[Oo]n|ON|[Oo]ff|OFF|[YyNn]|` +
- `[-+]?[0-9]+|` +
- `0[oO]?[0-7]+|0[xX][0-9a-fA-F]+|` +
- `[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+|` +
- `[-+]?(?:[0-9]*\.[0-9]+|[0-9]+\.?[0-9]*)(?:[eE][-+]?[0-9]+)?|` +
- `[-+]?\.(?:inf|Inf|INF)|\.(?:nan|NaN|NAN)|` +
- `[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}(?:[Tt ].*)?` +
- `)$`)
- // yamlScalarIsAmbiguous reports whether s must be quoted to survive as a string.
- //
- // The encoder quotes most of these itself, but not the exponent-float form: a
- // REALITY short-id such as "2351e1" is valid hex and, emitted bare, is read as
- // 23510. A Clash core then hex-decodes a five-digit number, rejects the proxy
- // and drops the whole provider to zero nodes (#6104). goccy's own parser reads
- // that token back as a string, so the encoder never sees a problem and a
- // round-trip check through it cannot find one either — the resolution rules,
- // not the encoder, are the thing to test against. The same shape reaches
- // passwords, obfs-passwords and pre-shared keys, so every string in the
- // document is checked rather than an enumerated list of fields.
- func yamlScalarIsAmbiguous(s string) bool {
- if s == "" {
- return false
- }
- return yamlPlainScalarNotString.MatchString(s)
- }
- // quoteAmbiguousYAMLScalars rebuilds v with every ambiguous string wrapped so
- // the encoder is forced to quote it. Containers are rebuilt as map[string]any /
- // []any because a typed container cannot hold the wrapper; unambiguous values
- // are carried through untouched, so the emitted document is unchanged apart
- // from the quotes that were missing.
- func quoteAmbiguousYAMLScalars(v any) any {
- if v == nil {
- return nil
- }
- if s, ok := v.(string); ok {
- if yamlScalarIsAmbiguous(s) {
- return yamlQuotedString(s)
- }
- return s
- }
- rv := reflect.ValueOf(v)
- switch rv.Kind() {
- case reflect.Map:
- out := make(map[string]any, rv.Len())
- iter := rv.MapRange()
- for iter.Next() {
- key, ok := iter.Key().Interface().(string)
- if !ok {
- return v
- }
- out[key] = quoteAmbiguousYAMLScalars(iter.Value().Interface())
- }
- return out
- case reflect.Slice, reflect.Array:
- if rv.Kind() == reflect.Slice && rv.Type().Elem().Kind() == reflect.Uint8 {
- return v
- }
- out := make([]any, rv.Len())
- for i := range rv.Len() {
- out[i] = quoteAmbiguousYAMLScalars(rv.Index(i).Interface())
- }
- return out
- case reflect.Ptr, reflect.Interface:
- if rv.IsNil() {
- return v
- }
- return quoteAmbiguousYAMLScalars(rv.Elem().Interface())
- default:
- return v
- }
- }
- // marshalClashYAML serializes a Clash config, keeping string values typed as
- // strings in the output.
- func marshalClashYAML(config any) ([]byte, error) {
- return yaml.Marshal(quoteAmbiguousYAMLScalars(config))
- }
|