package plaintext import ( "bufio" "fmt" "io" "strings" "github.com/Loyalsoldier/geoip/lib" "github.com/tidwall/gjson" "gopkg.in/yaml.v2" ) type text_in struct { Type string Action lib.Action Description string Name string URI string IPOrCIDR []string InputDir string Want map[string]bool OnlyIPType lib.IPType JSONPath []string RemovePrefixesInLine []string RemoveSuffixesInLine []string } func (t *text_in) scanFile(reader io.Reader, entry *lib.Entry) error { var err error switch t.Type { case TypeTextIn: err = t.scanFileForTextIn(reader, entry) case TypeJSONIn: err = t.scanFileForJSONIn(reader, entry) case TypeClashRuleSetClassicalIn: err = t.scanFileForClashClassicalRuleSetIn(reader, entry) case TypeClashRuleSetIPCIDRIn: err = t.scanFileForClashIPCIDRRuleSetIn(reader, entry) case TypeSurgeRuleSetIn: err = t.scanFileForSurgeRuleSetIn(reader, entry) default: return lib.ErrNotSupportedFormat } return err } func (t *text_in) scanFileForTextIn(reader io.Reader, entry *lib.Entry) error { scanner := bufio.NewScanner(reader) for scanner.Scan() { line := scanner.Text() line, _, _ = strings.Cut(line, "#") line, _, _ = strings.Cut(line, "//") line, _, _ = strings.Cut(line, "/*") line = strings.TrimSpace(line) if line == "" { continue } line = strings.ToLower(line) for _, prefix := range t.RemovePrefixesInLine { line = strings.TrimSpace(strings.TrimPrefix(line, strings.ToLower(strings.TrimSpace(prefix)))) } for _, suffix := range t.RemoveSuffixesInLine { line = strings.TrimSpace(strings.TrimSuffix(line, strings.ToLower(strings.TrimSpace(suffix)))) } line = strings.TrimSpace(line) if line == "" { continue } if err := entry.AddPrefix(line); err != nil { return err } } if err := scanner.Err(); err != nil { return err } return nil } func (t *text_in) readClashRuleSetYAMLFile(reader io.Reader) ([]string, error) { var payload struct { Payload []string `yaml:"payload"` } data, err := io.ReadAll(reader) if err != nil { return nil, err } if err := yaml.Unmarshal(data, &payload); err != nil { return nil, err } return payload.Payload, nil } func (t *text_in) scanFileForClashIPCIDRRuleSetIn(reader io.Reader, entry *lib.Entry) error { payload, err := t.readClashRuleSetYAMLFile(reader) if err != nil { return err } for _, cidrStr := range payload { cidrStr = strings.TrimSpace(cidrStr) if cidrStr == "" { continue } if err := entry.AddPrefix(cidrStr); err != nil { return err } } return nil } func (t *text_in) scanFileForClashClassicalRuleSetIn(reader io.Reader, entry *lib.Entry) error { payload, err := t.readClashRuleSetYAMLFile(reader) if err != nil { return err } for _, line := range payload { line = strings.ToLower(strings.TrimSpace(line)) if line == "" { continue } // Examples: // IP-CIDR,162.208.16.0/24 // IP-CIDR6,2a0b:e40:1::/48 // IP-CIDR,162.208.16.0/24,no-resolve // IP-CIDR6,2a0b:e40:1::/48,no-resolve if strings.HasPrefix(line, "ip-cidr,") || strings.HasPrefix(line, "ip-cidr6,") { parts := strings.Split(line, ",") if len(parts) < 2 { continue } line = strings.TrimSpace(parts[1]) if line == "" { continue } if err := entry.AddPrefix(line); err != nil { return err } } } return nil } func (t *text_in) scanFileForSurgeRuleSetIn(reader io.Reader, entry *lib.Entry) error { scanner := bufio.NewScanner(reader) for scanner.Scan() { line := scanner.Text() line, _, _ = strings.Cut(line, "#") line, _, _ = strings.Cut(line, "//") line, _, _ = strings.Cut(line, "/*") line = strings.ToLower(strings.TrimSpace(line)) if line == "" { continue } // Examples: // IP-CIDR,162.208.16.0/24 // IP-CIDR6,2a0b:e40:1::/48 // IP-CIDR,162.208.16.0/24,no-resolve // IP-CIDR6,2a0b:e40:1::/48,no-resolve if strings.HasPrefix(line, "ip-cidr,") || strings.HasPrefix(line, "ip-cidr6,") { parts := strings.Split(line, ",") if len(parts) < 2 { continue } line = strings.TrimSpace(parts[1]) if line == "" { continue } if err := entry.AddPrefix(line); err != nil { return err } } } if err := scanner.Err(); err != nil { return err } return nil } func (t *text_in) scanFileForJSONIn(reader io.Reader, entry *lib.Entry) error { data, err := io.ReadAll(reader) if err != nil { return err } if !gjson.ValidBytes(data) { return fmt.Errorf("❌ [type %s | action %s] invalid JSON data", t.Type, t.Action) } // JSON Path syntax: // https://github.com/tidwall/gjson/blob/master/SYNTAX.md for _, path := range t.JSONPath { path = strings.TrimSpace(path) result := gjson.GetBytes(data, path) if err := t.processJSONResult(result, entry); err != nil { return fmt.Errorf("❌ [type %s | action %s] failed to process JSON: %v", t.Type, t.Action, err) } } return nil } func (t *text_in) processJSONResult(result gjson.Result, entry *lib.Entry) error { switch { case !result.Exists(): return fmt.Errorf("invaild IP address or CIDR (value not exist), please check your specified JSON path or JSON source") case result.Type == gjson.String: cidr := strings.TrimSpace(result.String()) if cidr == "" { return fmt.Errorf("empty string, please check your specified JSON path or JSON source") } if err := entry.AddPrefix(cidr); err != nil { return err } case result.IsArray(): for _, item := range result.Array() { if err := t.processJSONResult(item, entry); err != nil { return err } } default: return fmt.Errorf("invaild IP address or CIDR, please check your specified JSON path or JSON source") } return nil }