package pdsh import ( "bufio" "bytes" "os" "strconv" "strings" "r00t2.io/sysutils/paths" ) /* ParseDshPtrn parses ptrn using the DSH group pattern ptrn as according to `HOSTLIST EXPRESSSIONS` in pdsh(1). `#include` directives are explicitly skipped; this only parses actual generation pattern strings. */ func ParseDshPtrn(ptrn string) (hostList []string, err error) { var r rune var pos int var s string var inToken bool var tokStr string var tok dshGrpToken var strBuf *bytes.Buffer = new(bytes.Buffer) var tokBuf *bytes.Buffer = new(bytes.Buffer) var parser *dshGrpGenerator = &dshGrpGenerator{ tokens: make([]dshGrpToken, 0), tokenized: make([]string, 0), text: ptrn, } s = strings.TrimSpace(ptrn) if s == "" { return } if strings.HasPrefix(s, "#") { return } // A quick sanity check. The end-state from the state machine below will catch any weird bracket issues beyond this. if strings.Count(s, "[") != strings.Count(s, "]") { err = ErrInvalidDshGrpSyntax return } // Now the hacky bits. We read until we get to a start-token ('['), end-token (']'), or a pattern separator (',') that is *outside* a range token. for pos, r = range s { switch r { case '[': if inToken { // Nested [...[ err = &PtrnParseErr{ pos: uint(pos), ptrn: ptrn, r: r, err: ErrInvalidDshGrpSyntax, } return } parser.tokenized = append(parser.tokenized, strBuf.String()) strBuf.Reset() inToken = true case ']': if !inToken { // Nested ]...] err = &PtrnParseErr{ pos: uint(pos), ptrn: ptrn, r: r, err: ErrInvalidDshGrpSyntax, } return } tokStr = tokBuf.String() if tok, err = parseDshGrpToken(tokStr); err != nil { err = &PtrnParseErr{ pos: uint(pos), ptrn: ptrn, r: r, err: err, } return } parser.tokens = append(parser.tokens, tok) tokBuf.Reset() inToken = false default: if inToken { // If it isn't between '0' and '9', isn't '-', and isn't ','... if !(0x30 <= r && r <= 0x39) && (r != 0x2d) && (r != 0x2c) { // It's not a valid token. (The actual syntax is validated in parseDshGrpToken and parseDshGrpSubtoken) err = &PtrnParseErr{ pos: uint(pos), ptrn: ptrn, r: r, err: ErrInvalidDshGrpSyntax, } return } tokBuf.WriteRune(r) } else { if strings.TrimSpace(string(r)) == "" || r == '#' { // Whitespace is "invalid" (treat it as the end of the pattern). // Same for end-of-line octothorpes. if tokBuf.Len() > 0 { // This should never happen. err = &PtrnParseErr{ pos: uint(pos), ptrn: ptrn, r: r, err: ErrInvalidDshGrpSyntax, } return } if strBuf.Len() > 0 { parser.tokenized = append(parser.tokenized, strBuf.String()) } break } // Otherwise we just check for valid DNS chars. if !(0x30 <= r && r <= 0x39) && // '0'-'9' (r != 0x2d) && // '-' (r != 0x2e) && // '.' !(0x41 <= r && r <= 0x5a) && // 'A' through 'Z' (inclusive) !(0x6a <= r && r <= 0x7a) { // 'a' through 'z' (inclusive) err = &PtrnParseErr{ pos: uint(pos), ptrn: ptrn, r: r, err: ErrInvalidDshGrpPtrn, } return } // (Probably) valid(-ish), so add it. strBuf.WriteRune(r) } } } // If the token never closed, it's also invalid. if inToken { err = ErrInvalidDshGrpSyntax return } return } // parseDshGrpToken parses a token string into a dshGrpToken. func parseDshGrpToken(tokenStr string) (token dshGrpToken, err error) { var s string var st []string var sub dshGrpSubtoken s = strings.TrimSpace(tokenStr) st = strings.Split(s, ",") token = dshGrpToken{ token: tokenStr, subtokens: make([]dshGrpSubtoken, 0, len(st)), } for _, s = range st { if strings.TrimSpace(s) == "" { continue } if sub, err = parseDshGrpSubtoken(s); err != nil { return } token.subtokens = append(token.subtokens, sub) } return } // parseDshGrpSubtoken parses a subtoken string into a dshGrpSubtoken. func parseDshGrpSubtoken(subTokenStr string) (subtoken dshGrpSubtoken, err error) { var u64 uint64 var vals []string var endPad string var startPad string var st dshGrpSubtoken var matches map[string][]string if matches = dshGrpSubTokenPtrn.MapString(subTokenStr, false, false, true); matches == nil || len(matches) == 0 { err = ErrInvalidDshGrpPtrn return } if vals = matches["start_pad"]; vals != nil && len(vals) == 1 { startPad = vals[0] } /* Due to a... particular quirk in the regex that I'm too tired to fix, the start_pad may be e.g. "0" (or "00", etc.) and start may be "" if the range starts *at* 0 (or 00, 000, etc.). */ if vals = matches["start"]; vals != nil && len(vals) == 1 { if u64, err = strconv.ParseUint(vals[0], 10, 64); err != nil { return } st.start = uint(u64) } else if startPad != "" { // Yeah, regex bug. So we remove one 0 from startPad, and set st.start to 0. st.start = 0 // This is implicit, though. startPad = startPad[:len(startPad)-1] } if vals = matches["end_pad"]; vals != nil && len(vals) == 1 { endPad = vals[0] } if vals = matches["end"]; vals != nil && len(vals) == 1 { if u64, err = strconv.ParseUint(vals[0], 10, 64); err != nil { return } st.end = uint(u64) } if startPad != "" && endPad != "" { // We set the pad to the largest. if len(startPad) > len(endPad) { st.pad = startPad } else { st.pad = endPad } } else if startPad != "" { st.pad = startPad } else if endPad != "" { st.pad = endPad } subtoken = st return } /* getDshGrpIncludes parses fpath for `#include ...` directives. It skips any entries in which `len(paths.SegmentSys(p) == []string{p}`, as these are inherently included by the dir read. It is assumed that fpath is a cleaned, absolute filepath. */ func getDshGrpIncludes(fpath string) (includes []string, err error) { var f *os.File var line string var exists bool var inclpath string var subIncl []string var segs []string var scanner *bufio.Scanner var matches map[string][]string if f, err = os.Open(fpath); err != nil { return } defer f.Close() scanner = bufio.NewScanner(f) for scanner.Scan() { line = strings.TrimSpace(scanner.Text()) if line == "" { continue } if !dshGrpInclPtrn.MatchString(line) { continue } matches = dshGrpInclPtrn.MapString(line, false, false, true) if matches == nil { err = ErrInvalidDshGrpSyntax return } if matches["incl"] == nil || len(matches["incl"]) == 0 { err = ErrInvalidDshGrpSyntax return } inclpath = matches["incl"][0] segs = paths.SegmentSys(inclpath, false, false) if segs == nil || len(segs) == 0 || (len(segs) == 1 && segs[0] == inclpath) { continue } if exists, err = paths.RealPathExists(&inclpath); err != nil { return } if !exists { continue } includes = append(includes, inclpath) if subIncl, err = getDshGrpIncludes(inclpath); err != nil { return } if subIncl != nil && len(subIncl) > 0 { includes = append(includes, subIncl...) } } return }