package importer import ( "bufio" "bytes" "crypto/rand" "encoding/binary" "encoding/json" "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 fsid, err := generateImportFSID() if err != nil { continue } export.FSID = fsid 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" } readOnly := strings.Contains(options, "ro") async := strings.Contains(options, "async") rootSquash := !strings.Contains(options, "no_root_squash") subtreeCheck := strings.Contains(options, "subtree_check") adv := db.NFSAdvanced{ AllSquash: strings.Contains(options, "all_squash"), Secure: strings.Contains(options, "secure"), WDelay: strings.Contains(options, "wdelay"), Hide: strings.Contains(options, "hide"), Crossmnt: strings.Contains(options, "crossmnt"), } advJSON, _ := json.Marshal(adv) return db.NFSExport{ Path: path, Clients: clients, ReadOnly: readOnly, Async: async, RootSquash: rootSquash, SubtreeCheck: subtreeCheck, Advanced: string(advJSON), }, true } func generateImportFSID() (int64, error) { var b [4]byte if _, err := rand.Read(b[:]); err != nil { return 0, err } n := binary.BigEndian.Uint32(b[:]) if n == 0 { n = 1 } return int64(n), nil } 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 }