| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- package geosite
- import (
- "bufio"
- "bytes"
- "encoding/binary"
- "strings"
- "testing"
- "github.com/sagernet/sing/common/varbin"
- "github.com/stretchr/testify/require"
- )
- // Old implementation using varbin reflection-based serialization
- func oldWriteString(writer varbin.Writer, value string) error {
- //nolint:staticcheck
- return varbin.Write(writer, binary.BigEndian, value)
- }
- func oldWriteItem(writer varbin.Writer, item Item) error {
- //nolint:staticcheck
- return varbin.Write(writer, binary.BigEndian, item)
- }
- func oldReadString(reader varbin.Reader) (string, error) {
- //nolint:staticcheck
- return varbin.ReadValue[string](reader, binary.BigEndian)
- }
- func oldReadItem(reader varbin.Reader) (Item, error) {
- //nolint:staticcheck
- return varbin.ReadValue[Item](reader, binary.BigEndian)
- }
- func TestStringCompat(t *testing.T) {
- t.Parallel()
- cases := []struct {
- name string
- input string
- }{
- {"empty", ""},
- {"single_char", "a"},
- {"ascii", "example.com"},
- {"utf8", "测试域名.中国"},
- {"special_chars", "\x00\xff\n\t"},
- {"127_bytes", strings.Repeat("x", 127)},
- {"128_bytes", strings.Repeat("x", 128)},
- {"16383_bytes", strings.Repeat("x", 16383)},
- {"16384_bytes", strings.Repeat("x", 16384)},
- }
- for _, tc := range cases {
- t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
- // Old write
- var oldBuf bytes.Buffer
- err := oldWriteString(&oldBuf, tc.input)
- require.NoError(t, err)
- // New write
- var newBuf bytes.Buffer
- err = writeString(&newBuf, tc.input)
- require.NoError(t, err)
- // Bytes must match
- require.Equal(t, oldBuf.Bytes(), newBuf.Bytes(),
- "mismatch for %q\nold: %x\nnew: %x", tc.name, oldBuf.Bytes(), newBuf.Bytes())
- // New write -> old read
- readBack, err := oldReadString(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
- require.NoError(t, err)
- require.Equal(t, tc.input, readBack)
- // Old write -> new read
- readBack2, err := readString(bufio.NewReader(bytes.NewReader(oldBuf.Bytes())))
- require.NoError(t, err)
- require.Equal(t, tc.input, readBack2)
- })
- }
- }
- func TestItemCompat(t *testing.T) {
- t.Parallel()
- // Note: varbin.Write has a bug where struct values (not pointers) don't write their fields
- // because field.CanSet() returns false for non-addressable values.
- // The old geosite code passed Item values to varbin.Write, which silently wrote nothing.
- // The new code correctly writes Type + Value using manual serialization.
- // This test verifies the new serialization format and round-trip correctness.
- cases := []struct {
- name string
- input Item
- }{
- {"domain_empty", Item{Type: RuleTypeDomain, Value: ""}},
- {"domain_normal", Item{Type: RuleTypeDomain, Value: "example.com"}},
- {"domain_suffix", Item{Type: RuleTypeDomainSuffix, Value: ".example.com"}},
- {"domain_keyword", Item{Type: RuleTypeDomainKeyword, Value: "google"}},
- {"domain_regex", Item{Type: RuleTypeDomainRegex, Value: `^.*\.example\.com$`}},
- {"utf8_domain", Item{Type: RuleTypeDomain, Value: "测试.com"}},
- {"long_domain", Item{Type: RuleTypeDomainSuffix, Value: strings.Repeat("a", 200) + ".com"}},
- {"128_bytes_value", Item{Type: RuleTypeDomain, Value: strings.Repeat("x", 128)}},
- }
- for _, tc := range cases {
- t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
- // New write
- var newBuf bytes.Buffer
- err := newBuf.WriteByte(byte(tc.input.Type))
- require.NoError(t, err)
- err = writeString(&newBuf, tc.input.Value)
- require.NoError(t, err)
- // Verify format: Type (1 byte) + Value (uvarint len + bytes)
- require.True(t, len(newBuf.Bytes()) >= 1, "output too short")
- require.Equal(t, byte(tc.input.Type), newBuf.Bytes()[0], "type byte mismatch")
- // New write -> old read (varbin can read correctly when given addressable target)
- readBack, err := oldReadItem(bufio.NewReader(bytes.NewReader(newBuf.Bytes())))
- require.NoError(t, err)
- require.Equal(t, tc.input, readBack)
- // New write -> new read
- reader := bufio.NewReader(bytes.NewReader(newBuf.Bytes()))
- typeByte, err := reader.ReadByte()
- require.NoError(t, err)
- value, err := readString(reader)
- require.NoError(t, err)
- require.Equal(t, tc.input, Item{Type: ItemType(typeByte), Value: value})
- })
- }
- }
- func TestGeositeWriteReadCompat(t *testing.T) {
- t.Parallel()
- cases := []struct {
- name string
- input map[string][]Item
- }{
- {
- "empty_map",
- map[string][]Item{},
- },
- {
- "single_code_empty_items",
- map[string][]Item{"test": {}},
- },
- {
- "single_code_single_item",
- map[string][]Item{"test": {{Type: RuleTypeDomain, Value: "a.com"}}},
- },
- {
- "single_code_multi_items",
- map[string][]Item{
- "test": {
- {Type: RuleTypeDomain, Value: "a.com"},
- {Type: RuleTypeDomainSuffix, Value: ".b.com"},
- {Type: RuleTypeDomainKeyword, Value: "keyword"},
- {Type: RuleTypeDomainRegex, Value: `^.*$`},
- },
- },
- },
- {
- "multi_code",
- map[string][]Item{
- "cn": {{Type: RuleTypeDomain, Value: "baidu.com"}, {Type: RuleTypeDomainSuffix, Value: ".cn"}},
- "us": {{Type: RuleTypeDomain, Value: "google.com"}},
- "jp": {{Type: RuleTypeDomainSuffix, Value: ".jp"}},
- },
- },
- {
- "utf8_values",
- map[string][]Item{
- "test": {
- {Type: RuleTypeDomain, Value: "测试.中国"},
- {Type: RuleTypeDomainSuffix, Value: ".テスト"},
- },
- },
- },
- {
- "large_items",
- generateLargeItems(1000),
- },
- }
- for _, tc := range cases {
- t.Run(tc.name, func(t *testing.T) {
- t.Parallel()
- // Write using new implementation
- var buf bytes.Buffer
- err := Write(&buf, tc.input)
- require.NoError(t, err)
- // Read back and verify
- reader, codes, err := NewReader(bytes.NewReader(buf.Bytes()))
- require.NoError(t, err)
- // Verify all codes exist
- codeSet := make(map[string]bool)
- for _, code := range codes {
- codeSet[code] = true
- }
- for code := range tc.input {
- require.True(t, codeSet[code], "missing code: %s", code)
- }
- // Verify items match
- for code, expectedItems := range tc.input {
- items, err := reader.Read(code)
- require.NoError(t, err)
- require.Equal(t, expectedItems, items, "items mismatch for code: %s", code)
- }
- })
- }
- }
- func generateLargeItems(count int) map[string][]Item {
- items := make([]Item, count)
- for i := 0; i < count; i++ {
- items[i] = Item{
- Type: ItemType(i % 4),
- Value: strings.Repeat("x", i%200) + ".com",
- }
- }
- return map[string][]Item{"large": items}
- }
|