package controllers import ( "errors" "fmt" "strconv" "strings" "unicode" "apiserver/backend" ) // REST element selections (job-array-one-based-index, design "ApiSel and // ApiMatch"). ParseIdentitySelection and ParseNameSelection are pure parsers: // they depend only on the backend ID helpers and the standard library. // SelForm is the shape of a parsed selection. type SelForm int const ( SelBare SelForm = iota // no "[...]" SelSingle // "[k]" (Lo == Hi == k) SelRange // "[a-b]" (a == b allowed) ) // Selection is one parsed identity or name selection. type Selection struct { Raw string // as the user wrote it (trimmed) Name string // name selections only (job_name / -J) Key uint64 // identity selections: exact ID (bare) or array ID Form SelForm Lo, Hi int // element numbers, Lo == Hi for SelSingle } // SelError is a rejected selection. Rule is the requirement it breaks // ("5.16", "5.14", "5.17", "5.8", "5.7", "5.5"); Msg names the selection as // written (trimmed). type SelError struct { Rule string Msg string } func (e *SelError) Error() string { return e.Msg } // maxElementNumber is the largest element number a selection may name (5.7): // the 15-bit array-index field. const maxElementNumber = 32767 func selErr(rule, format string, args ...any) (Selection, *SelError) { return Selection{}, &SelError{Rule: rule, Msg: fmt.Sprintf(format, args...)} } // selIndex is the element part of a selection. type selIndex struct { present bool isRange bool lo, hi int // saturated at exprNumSaturate loDigits, hiDigits string // leading zeros stripped } // splitSelection splits a trimmed selection into the part before the // element part and the element part. ok is false when the text breaks the // 5.16 grammar of the element part: a ']' before any '[', an element part that // is not "[num]" or "[num-num]" (num = one or more decimal digits), or any // character after the closing ']'. func splitSelection(s string) (head string, idx selIndex, ok bool) { open := strings.IndexAny(s, "[]") if open < 0 { return s, idx, true } if s[open] == ']' { return "", idx, false } head = s[:open] idx.present = true var good bool pos := open + 1 idx.lo, idx.loDigits, pos, good = readExprNum(s, pos) if !good { return "", idx, false } idx.hi, idx.hiDigits = idx.lo, idx.loDigits if pos < len(s) && s[pos] == '-' { idx.hi, idx.hiDigits, pos, good = readExprNum(s, pos+1) if !good { return "", idx, false } idx.isRange = true } if pos != len(s)-1 || s[pos] != ']' { return "", idx, false } return head, idx, true } // checkIndex applies 5.7 and then 5.5 to an element part and fills in the // selection's form and numbers. func checkIndex(sel Selection, idx selIndex) (Selection, *SelError) { if !idx.present { sel.Form = SelBare return sel, nil } // 5.7 (a saturated number is always above the bound) if idx.lo > maxElementNumber || idx.hi > maxElementNumber { return selErr("5.7", "invalid element selection '%s': element numbers must be at most %d", sel.Raw, maxElementNumber) } // 5.5 (compared on the digit strings, so the order is exact) if digitsLess(idx.hiDigits, idx.loDigits) { return selErr("5.5", "invalid element selection '%s': the range is reversed (last element number is less than the first)", sel.Raw) } sel.Lo, sel.Hi = idx.lo, idx.hi sel.Form = SelSingle if idx.isRange { sel.Form = SelRange } return sel, nil } func isDecimal(s string) bool { if s == "" { return false } for i := 0; i < len(s); i++ { if s[i] < '0' || s[i] > '9' { return false } } return true } // ParseIdentitySelection parses a job identity, optionally followed by "[k]" // or "[a-b]". The identity is the written form c-a-ts-seq (strict field // widths) or a decimal uint64 (base 10). Ranges are always allowed at the // server. Whitespace before the identity or after the closing ']' is ignored; // whitespace anywhere else is invalid. Checks run in the order of 5.12 -- // 5.16, 5.14, 5.17, 5.8, 5.7, 5.5 -- and only the first failure is reported. // Numbers may have leading zeros (1.5). func ParseIdentitySelection(raw string) (Selection, *SelError) { sel := Selection{Raw: strings.TrimSpace(raw)} // 5.16: grammar of the element part and of the identity itself. head, idx, ok := splitSelection(sel.Raw) if !ok { return selErr("5.16", "invalid job identity or element selection '%s': expected , [k] or [a-b]", sel.Raw) } decimal := isDecimal(head) var key uint64 var err error if decimal { key, err = strconv.ParseUint(head, 10, 64) if err != nil { // all digits, so the only failure is a value above uint64 (5.14) return selErr("5.14", "invalid job identity '%s': the job number is too large", sel.Raw) } } else { key, err = backend.ParseHumanJobIDStrict(head) if errors.Is(err, backend.ErrJobIDWidth) { // 5.14 return selErr("5.14", "invalid job identity '%s': a field is wider than allowed (cluster <= 63, API server <= 15, timestamp < 2^32, sequence < 2^20)", sel.Raw) } if err != nil { return selErr("5.16", "invalid job identity or element selection '%s': expected , [k] or [a-b]", sel.Raw) } } sel.Key = key // 5.17: a decimal key carrying the array marking, without [k]. The // written form never carries the marking. if decimal && !idx.present && backend.IsArrayJob(key) { return selErr("5.17", "invalid job identity '%s': array elements must be named as [k]", sel.Raw) } // 5.8: only with an element part. if idx.present && !backend.IsArrayCapableKey(key) { return selErr("5.8", "invalid job identity '%s': '%s' is not an array identity", sel.Raw, head) } return checkIndex(sel, idx) } // ParseNameSelection parses a job-name selection "name[k]" or "name[a-b]". // Only 5.16, 5.7 and 5.5 apply (in that order). The name part is zero or more // characters, none of them whitespace, '[' or ']'; it is not checked against // the submission array-name rule (a name no array can have selects no job). // Whitespace before the name or after the closing ']' is ignored. // // A value with no '[' or ']' is not an element selection: it is returned as a // SelBare selection whose Name is the value exactly as given, so a caller // converting a whole filter entry keeps today's plain-name match for it. func ParseNameSelection(raw string) (Selection, *SelError) { sel := Selection{Raw: strings.TrimSpace(raw)} if !strings.ContainsAny(raw, "[]") { sel.Name = raw sel.Form = SelBare return sel, nil } // 5.16 head, idx, ok := splitSelection(sel.Raw) if ok && strings.IndexFunc(head, unicode.IsSpace) >= 0 { ok = false } if !ok { return selErr("5.16", "invalid element selection '%s': expected name[k] or name[a-b] (no whitespace in the name or brackets)", sel.Raw) } sel.Name = head return checkIndex(sel, idx) }