clash_yaml_test.go 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. package sub
  2. import (
  3. "strings"
  4. "testing"
  5. "github.com/goccy/go-yaml"
  6. )
  7. func TestAmbiguousScalarsAreQuoted(t *testing.T) {
  8. tests := []struct {
  9. name string
  10. value string
  11. mustQuote bool
  12. }{
  13. {"reality short-id read as a float", "2351e1", true},
  14. {"short exponent form", "0e1", true},
  15. {"long exponent form", "12e34", true},
  16. {"all digits", "123456", true},
  17. {"leading zeros read as octal", "0177", true},
  18. {"hex form", "0x1f", true},
  19. {"decimal point", "1.5", true},
  20. {"boolean word", "true", true},
  21. {"single letter bool", "y", true},
  22. {"null word", "null", true},
  23. {"tilde", "~", true},
  24. {"date", "2026-07-27", true},
  25. {"sexagesimal", "12:30", true},
  26. {"plain hex with letters", "6ba7b8", false},
  27. {"letters only", "abcdef", false},
  28. {"hostname", "example.com", false},
  29. {"dotted quad", "1.2.3.4", false},
  30. {"uuid", "6ba7b810-9dad-11d1-80b4-00c04fd430c8", false},
  31. {"alpn token", "h2", false},
  32. }
  33. for _, tt := range tests {
  34. t.Run(tt.name, func(t *testing.T) {
  35. out, err := marshalClashYAML(map[string]any{
  36. "reality-opts": map[string]any{"short-id": tt.value},
  37. })
  38. if err != nil {
  39. t.Fatalf("marshalClashYAML: %v", err)
  40. }
  41. got := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(string(out)), "reality-opts:"))
  42. quoted := strings.Contains(got, "'"+tt.value+"'") || strings.Contains(got, `"`+tt.value+`"`)
  43. if tt.mustQuote && !quoted {
  44. t.Errorf("%q must be emitted quoted, got %q", tt.value, got)
  45. }
  46. if !tt.mustQuote && quoted {
  47. t.Errorf("%q needs no quoting, got %q", tt.value, got)
  48. }
  49. var decoded map[string]any
  50. if err := yaml.Unmarshal(out, &decoded); err != nil {
  51. t.Fatalf("output must stay parseable: %v\n%s", err, out)
  52. }
  53. })
  54. }
  55. }
  56. func TestClashShortIDIsQuotedInOutput(t *testing.T) {
  57. out, err := marshalClashYAML(map[string]any{
  58. "proxies": []any{map[string]any{
  59. "name": "de-1",
  60. "reality-opts": map[string]any{"short-id": "2351e1"},
  61. }},
  62. })
  63. if err != nil {
  64. t.Fatalf("marshalClashYAML: %v", err)
  65. }
  66. if !strings.Contains(string(out), `'2351e1'`) {
  67. t.Errorf("short-id must be emitted as a quoted scalar, got:\n%s", out)
  68. }
  69. }
  70. func TestAmbiguousPasswordIsQuoted(t *testing.T) {
  71. out, err := marshalClashYAML(map[string]any{
  72. "proxies": []any{map[string]any{
  73. "name": "de-1",
  74. "password": "80e12",
  75. "obfs-password": "12345",
  76. "pre-shared-key": "9e9",
  77. }},
  78. })
  79. if err != nil {
  80. t.Fatalf("marshalClashYAML: %v", err)
  81. }
  82. for _, want := range []string{`'80e12'`, `'12345'`, `'9e9'`} {
  83. if !strings.Contains(string(out), want) {
  84. t.Errorf("expected %s in output:\n%s", want, out)
  85. }
  86. }
  87. }
  88. func TestUnambiguousScalarsAreUnchanged(t *testing.T) {
  89. config := map[string]any{
  90. "port": 7890,
  91. "mode": "rule",
  92. "proxies": []any{map[string]any{"name": "de-1", "udp": true, "port": 443}},
  93. "rules": []string{"MATCH,PROXY"},
  94. "alpn": []string{"h2", "http/1.1"},
  95. "nonempty": "example.com",
  96. }
  97. quoted, err := marshalClashYAML(config)
  98. if err != nil {
  99. t.Fatalf("marshalClashYAML: %v", err)
  100. }
  101. plain, err := yaml.Marshal(config)
  102. if err != nil {
  103. t.Fatalf("yaml.Marshal: %v", err)
  104. }
  105. if string(quoted) != string(plain) {
  106. t.Errorf("unambiguous document changed:\n--- got ---\n%s\n--- want ---\n%s", quoted, plain)
  107. }
  108. }
  109. func TestQuotedScalarEscapesQuotes(t *testing.T) {
  110. out, err := marshalClashYAML(map[string]any{"password": "1e2'3"})
  111. if err != nil {
  112. t.Fatalf("marshalClashYAML: %v", err)
  113. }
  114. var decoded map[string]any
  115. if err := yaml.Unmarshal(out, &decoded); err != nil {
  116. t.Fatalf("output must stay parseable: %v\n%s", err, out)
  117. }
  118. if got := decoded["password"]; got != any("1e2'3") {
  119. t.Errorf("password round-tripped to %#v\n%s", got, out)
  120. }
  121. }