compat_test.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. package geosite
  2. import (
  3. "bufio"
  4. "bytes"
  5. "encoding/binary"
  6. "strings"
  7. "testing"
  8. "github.com/sagernet/sing/common/varbin"
  9. "github.com/stretchr/testify/require"
  10. )
  11. // Old implementation using varbin reflection-based serialization
  12. func oldWriteString(writer varbin.Writer, value string) error {
  13. //nolint:staticcheck
  14. return varbin.Write(writer, binary.BigEndian, value)
  15. }
  16. func oldWriteItem(writer varbin.Writer, item Item) error {
  17. //nolint:staticcheck
  18. return varbin.Write(writer, binary.BigEndian, item)
  19. }
  20. func oldReadString(reader varbin.Reader) (string, error) {
  21. //nolint:staticcheck
  22. return varbin.ReadValue[string](reader, binary.BigEndian)
  23. }
  24. func oldReadItem(reader varbin.Reader) (Item, error) {
  25. //nolint:staticcheck
  26. return varbin.ReadValue[Item](reader, binary.BigEndian)
  27. }
  28. func TestStringCompat(t *testing.T) {
  29. t.Parallel()
  30. cases := []struct {
  31. name string
  32. input string
  33. }{
  34. {"empty", ""},
  35. {"single_char", "a"},
  36. {"ascii", "example.com"},
  37. {"utf8", "测试域名.中国"},
  38. {"special_chars", "\x00\xff\n\t"},
  39. {"127_bytes", strings.Repeat("x", 127)},
  40. {"128_bytes", strings.Repeat("x", 128)},
  41. {"16383_bytes", strings.Repeat("x", 16383)},
  42. {"16384_bytes", strings.Repeat("x", 16384)},
  43. }
  44. for _, tc := range cases {
  45. t.Run(tc.name, func(t *testing.T) {
  46. t.Parallel()
  47. // Old write
  48. var oldBuf bytes.Buffer
  49. err := oldWriteString(&oldBuf, tc.input)
  50. require.NoError(t, err)
  51. // New write
  52. var newBuf bytes.Buffer
  53. err = writeString(&newBuf, tc.input)
  54. require.NoError(t, err)
  55. // Bytes must match
  56. require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(),
  57. "mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes())
  58. // New write -> old read
  59. readBack, err := oldReadString(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
  60. require.NoError(t, err)
  61. require.Equal(t, tc.input, readBack)
  62. // Old write -> new read
  63. readBack2, err := readString(bufio.NewReader(bytes.NewReader(oldBuf.Bytes())))
  64. require.NoError(t, err)
  65. require.Equal(t, tc.input, readBack2)
  66. })
  67. }
  68. }
  69. func TestItemCompat(t *testing.T) {
  70. t.Parallel()
  71. // Note: varbin.Write has a bug where struct values (not pointers) don't write their fields
  72. // because field.CanSet() returns false for non-addressable values.
  73. // The old geosite code passed Item values to varbin.Write, which silently wrote nothing.
  74. // The new code correctly writes Type + Value using manual serialization.
  75. // This test verifies the new serialization format and round-trip correctness.
  76. cases := []struct {
  77. name string
  78. input Item
  79. }{
  80. {"domain_empty", Item{Type: RuleTypeDomain, Value: ""}},
  81. {"domain_normal", Item{Type: RuleTypeDomain, Value: "example.com"}},
  82. {"domain_suffix", Item{Type: RuleTypeDomainSuffix, Value: ".example.com"}},
  83. {"domain_keyword", Item{Type: RuleTypeDomainKeyword, Value: "google"}},
  84. {"domain_regex", Item{Type: RuleTypeDomainRegex, Value: `^.*\.example\.com$`}},
  85. {"utf8_domain", Item{Type: RuleTypeDomain, Value: "测试.com"}},
  86. {"long_domain", Item{Type: RuleTypeDomainSuffix, Value: strings.Repeat("a", 200) + ".com"}},
  87. {"128_bytes_value", Item{Type: RuleTypeDomain, Value: strings.Repeat("x", 128)}},
  88. }
  89. for _, tc := range cases {
  90. t.Run(tc.name, func(t *testing.T) {
  91. t.Parallel()
  92. // New write
  93. var newBuf bytes.Buffer
  94. err := newBuf.WriteByte(byte(tc.input.Type))
  95. require.NoError(t, err)
  96. err = writeString(&newBuf, tc.input.Value)
  97. require.NoError(t, err)
  98. // Verify format: Type (1 byte) + Value (uvarint len + bytes)
  99. require.True(t, len(newBuf.Bytes()) >= 1, "output too short")
  100. require.Equal(t, byte(tc.input.Type), newBuf.Bytes()[0], "type byte mismatch")
  101. // New write -> old read (varbin can read correctly when given addressable target)
  102. readBack, err := oldReadItem(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
  103. require.NoError(t, err)
  104. require.Equal(t, tc.input, readBack)
  105. // New write -> new read
  106. reader := bufio.NewReader(bytes.NewReader(newBuf.Bytes()))
  107. typeByte, err := reader.ReadByte()
  108. require.NoError(t, err)
  109. value, err := readString(reader)
  110. require.NoError(t, err)
  111. require.Equal(t, tc.input, Item{Type: ItemType(typeByte), Value: value})
  112. })
  113. }
  114. }
  115. func TestGeositeWriteReadCompat(t *testing.T) {
  116. t.Parallel()
  117. cases := []struct {
  118. name string
  119. input map[string][]Item
  120. }{
  121. {
  122. "empty_map",
  123. map[string][]Item{},
  124. },
  125. {
  126. "single_code_empty_items",
  127. map[string][]Item{"test": {}},
  128. },
  129. {
  130. "single_code_single_item",
  131. map[string][]Item{"test": {{Type: RuleTypeDomain, Value: "a.com"}}},
  132. },
  133. {
  134. "single_code_multi_items",
  135. map[string][]Item{
  136. "test": {
  137. {Type: RuleTypeDomain, Value: "a.com"},
  138. {Type: RuleTypeDomainSuffix, Value: ".b.com"},
  139. {Type: RuleTypeDomainKeyword, Value: "keyword"},
  140. {Type: RuleTypeDomainRegex, Value: `^.*$`},
  141. },
  142. },
  143. },
  144. {
  145. "multi_code",
  146. map[string][]Item{
  147. "cn": {{Type: RuleTypeDomain, Value: "baidu.com"}, {Type: RuleTypeDomainSuffix, Value: ".cn"}},
  148. "us": {{Type: RuleTypeDomain, Value: "google.com"}},
  149. "jp": {{Type: RuleTypeDomainSuffix, Value: ".jp"}},
  150. },
  151. },
  152. {
  153. "utf8_values",
  154. map[string][]Item{
  155. "test": {
  156. {Type: RuleTypeDomain, Value: "测试.中国"},
  157. {Type: RuleTypeDomainSuffix, Value: ".テスト"},
  158. },
  159. },
  160. },
  161. {
  162. "large_items",
  163. generateLargeItems(1000),
  164. },
  165. }
  166. for _, tc := range cases {
  167. t.Run(tc.name, func(t *testing.T) {
  168. t.Parallel()
  169. // Write using new implementation
  170. var buf bytes.Buffer
  171. err := Write(&buf, tc.input)
  172. require.NoError(t, err)
  173. // Read back and verify
  174. reader, codes, err := NewReader(bytes.NewReader(buf.Bytes()))
  175. require.NoError(t, err)
  176. // Verify all codes exist
  177. codeSet := make(map[string]bool)
  178. for _, code := range codes {
  179. codeSet[code] = true
  180. }
  181. for code := range tc.input {
  182. require.True(t, codeSet[code], "missing code: %s", code)
  183. }
  184. // Verify items match
  185. for code, expectedItems := range tc.input {
  186. items, err := reader.Read(code)
  187. require.NoError(t, err)
  188. require.Equal(t, expectedItems, items, "items mismatch for code: %s", code)
  189. }
  190. })
  191. }
  192. }
  193. func generateLargeItems(count int) map[string][]Item {
  194. items := make([]Item, count)
  195. for i := 0; i < count; i++ {
  196. items[i] = Item{
  197. Type: ItemType(i % 4),
  198. Value: strings.Repeat("x", i%200) + ".com",
  199. }
  200. }
  201. return map[string][]Item{"large": items}
  202. }