Copilot b49404a24e
Fix panic and rule loss in list parsing/resolution (#3905)
* Fix panic on empty inclusion attribute, rule loss in selective inclusion and other bugs

* Exercise domain/list-name validators in tests

* Consolidate parsed list state into ParsedList and simplify inclusion parsing

Co-authored-by: Loyalsoldier <10487845+Loyalsoldier@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Loyalsoldier <10487845+Loyalsoldier@users.noreply.github.com>
2026-08-04 18:22:31 +08:00

176 lines
4.4 KiB
Go

package main
import (
"bufio"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/v2fly/domain-list-community/internal/dlc"
router "github.com/v2fly/v2ray-core/v5/app/router/routercommon"
"google.golang.org/protobuf/proto"
)
var (
inputData = flag.String("inputdata", "dlc.dat", "Name of the geosite dat file")
outputDir = flag.String("outputdir", "./", "Directory to place all generated files")
exportLists = flag.String("exportlists", "", "Lists to be exported, separated by ',' (empty for _all_)")
)
type GeoSites struct {
Sites []*router.GeoSite
SiteIdx map[string]int
}
func loadGeosite(path string) (*GeoSites, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read geosite file: %w", err)
}
vgeositeList := new(router.GeoSiteList)
if err := proto.Unmarshal(data, vgeositeList); err != nil {
return nil, fmt.Errorf("failed to unmarshal: %w", err)
}
gs := &GeoSites{Sites: vgeositeList.Entry}
gs.SiteIdx = make(map[string]int, len(gs.Sites))
for i, site := range gs.Sites {
gs.SiteIdx[strings.ToUpper(site.CountryCode)] = i
}
return gs, nil
}
func domain2Builder(d *router.Domain, b *strings.Builder) error {
switch d.Type {
case router.Domain_RootDomain:
b.WriteString(dlc.RuleTypeDomain)
case router.Domain_Full:
b.WriteString(dlc.RuleTypeFullDomain)
case router.Domain_Plain:
b.WriteString(dlc.RuleTypeKeyword)
case router.Domain_Regex:
b.WriteString(dlc.RuleTypeRegexp)
default:
return fmt.Errorf("invalid rule type: %+v", d.Type)
}
b.WriteByte(':')
b.WriteString(d.Value)
for i, attr := range d.Attribute {
if i == 0 {
b.WriteByte(':')
} else {
b.WriteByte(',')
}
b.WriteByte('@')
b.WriteString(attr.Key)
}
return nil
}
func exportSite(name string, gs *GeoSites) error {
idx, ok := gs.SiteIdx[strings.ToUpper(name)]
if !ok {
return fmt.Errorf("list %q does not exist", name)
}
vDomains := gs.Sites[idx].Domain
if len(vDomains) == 0 {
return fmt.Errorf("list %q is empty", name)
}
file, err := os.Create(filepath.Join(*outputDir, name+".yml"))
if err != nil {
return err
}
defer file.Close()
w := bufio.NewWriter(file)
fmt.Fprintf(w, "%s:\n", name)
var b strings.Builder
b.Grow(64)
for _, vdomain := range vDomains {
b.Reset()
if err := domain2Builder(vdomain, &b); err != nil {
return err
}
fmt.Fprintf(w, " - %q\n", b.String())
}
return w.Flush()
}
func exportAll(filename string, gs *GeoSites) error {
file, err := os.Create(filepath.Join(*outputDir, filename))
if err != nil {
return err
}
defer file.Close()
w := bufio.NewWriter(file)
w.WriteString("lists:\n")
var b strings.Builder
b.Grow(64)
for _, site := range gs.Sites {
fmt.Fprintf(w, " - name: %q\n", strings.ToLower(site.CountryCode))
fmt.Fprintf(w, " length: %d\n", len(site.Domain))
w.WriteString(" rules:\n")
for _, vdomain := range site.Domain {
b.Reset()
if err := domain2Builder(vdomain, &b); err != nil {
return err
}
fmt.Fprintf(w, " - %q\n", b.String())
}
}
return w.Flush()
}
func run() error {
// Make sure output directory exists
if err := os.MkdirAll(*outputDir, 0755); err != nil {
return fmt.Errorf("failed to create output directory: %w", err)
}
fmt.Printf("loading source data %q...\n", *inputData)
geoSites, err := loadGeosite(*inputData)
if err != nil {
return fmt.Errorf("failed to loadGeosite: %w", err)
}
var exportListSlice []string
for raw := range strings.SplitSeq(*exportLists, ",") {
if trimmed := strings.TrimSpace(raw); trimmed != "" {
exportListSlice = append(exportListSlice, trimmed)
}
}
if len(exportListSlice) == 0 {
exportListSlice = []string{"_all_"}
}
failedCount := 0
for _, eplistname := range exportListSlice {
if strings.EqualFold(eplistname, "_all_") {
if err := exportAll(filepath.Base(*inputData)+"_plain.yml", geoSites); err != nil {
fmt.Printf("[Error] failed to exportAll: %v\n", err)
failedCount++
continue
}
} else {
if err := exportSite(eplistname, geoSites); err != nil {
fmt.Printf("[Error] failed to exportSite: %v\n", err)
failedCount++
continue
}
}
fmt.Printf("list: %q has been exported successfully\n", eplistname)
}
if failedCount > 0 {
return fmt.Errorf("%d list(s) failed to be exported", failedCount)
}
return nil
}
func main() {
flag.Parse()
if err := run(); err != nil {
fmt.Printf("[Fatal] critical error: %v\n", err)
os.Exit(1)
}
}