package importer import ( "bufio" "bytes" "fmt" "os" "regexp" "strings" "github.com/darroyo/nasctl/internal/db" "github.com/darroyo/nasctl/internal/system" ) var nfsClientPattern = regexp.MustCompile(`^[a-zA-Z0-9_.:\-/\*@]+$`) func ImportNFSExports(path string) ([]db.NFSExport, error) { data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, fmt.Errorf("read exports: %w", err) } return parseExports(data) } func parseExports(data []byte) ([]db.NFSExport, error) { var exports []db.NFSExport scanner := bufio.NewScanner(bytes.NewReader(data)) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } export, ok := parseExportLine(line) if !ok { continue } if err := system.ValidatePath(export.Path); err != nil { continue } validClients := make([]string, 0, len(export.Clients)) for _, c := range export.Clients { if err := system.ValidateNFSClient(c); err != nil { continue } validClients = append(validClients, c) } if len(validClients) == 0 && len(export.Clients) > 0 { continue } export.Clients = validClients if export.Options != "" { if err := system.ValidateNFSOptions(export.Options); err != nil { continue } } exports = append(exports, export) } return exports, nil } func parseExportLine(line string) (db.NFSExport, bool) { parenDepth := 0 spaceIdx := -1 for i, ch := range line { switch ch { case '(': parenDepth++ case ')': parenDepth-- case ' ': if parenDepth == 0 && spaceIdx < 0 { spaceIdx = i } } } if spaceIdx < 0 { return db.NFSExport{}, false } path := strings.TrimSpace(line[:spaceIdx]) if path == "" { return db.NFSExport{}, false } rest := strings.TrimSpace(line[spaceIdx:]) if rest == "" { return db.NFSExport{}, false } var clients []string var options string parts := splitExportsClients(rest) for _, part := range parts { part = strings.TrimSpace(part) if part == "" { continue } open := strings.IndexByte(part, '(') close := strings.LastIndexByte(part, ')') var client, opts string if open >= 0 && close > open { client = strings.TrimSpace(part[:open]) opts = strings.TrimSpace(part[open+1 : close]) } else { client = part opts = "" } if !nfsClientPattern.MatchString(client) { continue } clients = append(clients, client) if opts != "" && options == "" { options = opts } } if len(clients) == 0 { return db.NFSExport{}, false } if options == "" { options = "rw,sync,no_root_squash" } return db.NFSExport{ Path: path, Clients: clients, Options: options, }, true } func splitExportsClients(s string) []string { var result []string var current []byte parenDepth := 0 for _, ch := range []byte(s) { switch ch { case '(': parenDepth++ current = append(current, ch) case ')': parenDepth-- current = append(current, ch) case ' ': if parenDepth == 0 { if len(current) > 0 { result = append(result, string(current)) current = nil } continue } current = append(current, ch) default: current = append(current, ch) } } if len(current) > 0 { result = append(result, string(current)) } return result }