package acl import ( "fmt" "os" "regexp" "strings" ) var linePattern = regexp.MustCompile(`^(\w+)\s*\(([^,]+)(?:,([^,]+))?(?:,([^,]+))?\)$`) type InvalidSyntaxError struct { Line string LineNum int } func (e *InvalidSyntaxError) Error() string { return fmt.Sprintf("invalid syntax at line %d: %s", e.LineNum, e.Line) } // TextRule is the struct representation of a (non-comment) line parsed from an ACL file. // A line can be parsed into a TextRule as long as it matches one of the following patterns: // // outbound(address) // outbound(address,protoPort) // outbound(address,protoPort,hijackAddress) // // It does not check whether any of the fields is valid - it's up to the compiler to do so. type TextRule struct { Outbound string Address string ProtoPort string HijackAddress string } func parseLine(line string) *TextRule { matches := linePattern.FindStringSubmatch(line) if matches == nil { return nil } return &TextRule{ Outbound: matches[1], Address: strings.TrimSpace(matches[2]), ProtoPort: strings.TrimSpace(matches[3]), HijackAddress: strings.TrimSpace(matches[4]), } } func ParseTextRules(text string) ([]TextRule, error) { rules := make([]TextRule, 0) lineNum := 0 for _, line := range strings.Split(text, "\n") { lineNum++ // Remove comments if i := strings.Index(line, "#"); i >= 0 { line = line[:i] } line = strings.TrimSpace(line) // Skip empty lines if len(line) == 0 { continue } // Parse line rule := parseLine(line) if rule == nil { return nil, &InvalidSyntaxError{line, lineNum} } rules = append(rules, *rule) } return rules, nil } func ParseTextRulesFile(filename string) ([]TextRule, error) { bs, err := os.ReadFile(filename) if err != nil { return nil, err } return ParseTextRules(string(bs)) }