package opensuse import ( "strconv" "strings" "unicode" ) // expand performs a textual expansion of RPM macros in s using the parser's macro table: // %{name}, %name, %{?name}, %{?name:text}, %{!?name:text}, %{nil}, %{defined x}, %{with x}. // Unknown macros, shell expansions %(...) and %{lua:...}-style bodies are left verbatim so the // caller can still recognise them. Expansion iterates to a fixed point (bounded). func (p *specParser) expand(s string) string { for i := 0; i < 8; i++ { n := expandOnce(p.spec.Macros, s, 0) if n == s { return n } s = n } return s } func isMacroStart(r rune) bool { return r == '_' || unicode.IsLetter(r) } func isMacroChar(r rune) bool { return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r) } func expandOnce(macros map[string]string, s string, depth int) string { if depth > 16 || !strings.Contains(s, "%") { return s } var b strings.Builder i := 0 for i < len(s) { c := s[i] if c != '%' { b.WriteByte(c) i++ continue } if i+1 >= len(s) { b.WriteByte(c) i++ continue } switch s[i+1] { case '%': b.WriteString("%%") i += 2 case '{': end := matchBrace(s, i+1, '{', '}') if end < 0 { b.WriteString(s[i:]) return b.String() } body := s[i+2 : end] out, ok := expandBody(macros, body, depth) if ok { b.WriteString(out) } else { b.WriteString(s[i : end+1]) } i = end + 1 case '(': end := matchBrace(s, i+1, '(', ')') if end < 0 { b.WriteString(s[i:]) return b.String() } b.WriteString(s[i : end+1]) i = end + 1 default: j := i + 1 if j < len(s) && s[j] == '?' { j++ } k := j for k < len(s) && (isMacroChar(rune(s[k])) && (k > j || isMacroStart(rune(s[k])))) { k++ } if k == j { b.WriteByte(c) i++ continue } name := s[j:k] opt := s[i+1] == '?' if v, ok := macros[name]; ok { b.WriteString(expandOnce(macros, v, depth+1)) } else if opt { // %?name undefined -> empty } else { b.WriteString(s[i:k]) } i = k } } return b.String() } // matchBrace returns the index of the bracket closing the one at s[open], or -1. func matchBrace(s string, open int, o, c byte) int { depth := 0 for i := open; i < len(s); i++ { switch s[i] { case o: depth++ case c: depth-- if depth == 0 { return i } } } return -1 } // expandBody expands the inside of %{...}. ok is false when the body is not understood and // should be kept verbatim. func expandBody(macros map[string]string, body string, depth int) (string, bool) { body = strings.TrimSpace(body) if body == "" { return "", false } // %{?name}, %{?name:text}, %{!?name:text}, %{?!name:text} if strings.HasPrefix(body, "?") || strings.HasPrefix(body, "!?") || strings.HasPrefix(body, "?!") { neg := strings.HasPrefix(body, "!") || strings.HasPrefix(body, "?!") rest := strings.TrimLeft(body, "?!") name, text, hasText := strings.Cut(rest, ":") name = strings.TrimSpace(name) if name == "" || !validMacroName(name) { return "", false } v, defined := macros[name] if neg { defined = !defined } if hasText { if defined { return expandOnce(macros, text, depth+1), true } return "", true } if defined && !neg { return expandOnce(macros, v, depth+1), true } return "", true } if body == "nil" { return "", true } if kw, arg, ok := strings.Cut(body, " "); ok { arg = strings.TrimSpace(arg) switch kw { case "defined", "undefined": _, d := macros[arg] if kw == "undefined" { d = !d } return boolStr(d), true case "with", "without": _, d := macros["with_"+arg] if kw == "without" { d = !d } return boolStr(d), true } return "", false } if strings.Contains(body, ":") { return "", false // %{expand:...}, %{lua:...}, %{basename:...} } if !validMacroName(body) { return "", false } if v, ok := macros[body]; ok { return expandOnce(macros, v, depth+1), true } return "", false } func boolStr(b bool) string { if b { return "1" } return "0" } func validMacroName(s string) bool { for i, r := range s { if i == 0 && !isMacroStart(r) || i > 0 && !isMacroChar(r) { return false } } return s != "" } // eval evaluates a %if expression. Anything it cannot decide (unexpanded macros, %ifarch // style tests, syntax it does not know) is condUnknown. func (p *specParser) eval(expr string) condState { e := strings.TrimSpace(p.expand(expr)) if e == "" || strings.Contains(e, "%") { return condUnknown } toks, ok := tokenize(e) if !ok { return condUnknown } ev := &evaluator{toks: toks} v, ok := ev.parseOr() if !ok || ev.pos != len(ev.toks) { return condUnknown } if v.unknown { return condUnknown } if v.truthy() { return condTrue } return condFalse } type token struct { kind string // "str", "num", "word", "op" val string } func tokenize(s string) ([]token, bool) { var toks []token i := 0 for i < len(s) { c := s[i] switch { case c == ' ' || c == '\t': i++ case c == '"': j := strings.IndexByte(s[i+1:], '"') if j < 0 { return nil, false } toks = append(toks, token{"str", s[i+1 : i+1+j]}) i += j + 2 case strings.HasPrefix(s[i:], "==") || strings.HasPrefix(s[i:], "!=") || strings.HasPrefix(s[i:], ">=") || strings.HasPrefix(s[i:], "<=") || strings.HasPrefix(s[i:], "&&") || strings.HasPrefix(s[i:], "||"): toks = append(toks, token{"op", s[i : i+2]}) i += 2 case c == '>' || c == '<' || c == '!' || c == '(' || c == ')': toks = append(toks, token{"op", string(c)}) i++ default: j := i for j < len(s) && s[j] != ' ' && s[j] != '\t' && s[j] != '"' && s[j] != '(' && s[j] != ')' && !strings.HasPrefix(s[j:], "==") && !strings.HasPrefix(s[j:], "!=") && !strings.HasPrefix(s[j:], ">=") && !strings.HasPrefix(s[j:], "<=") && !strings.HasPrefix(s[j:], "&&") && !strings.HasPrefix(s[j:], "||") && s[j] != '>' && s[j] != '<' { j++ } if j == i { return nil, false } w := s[i:j] if _, err := strconv.ParseInt(w, 10, 64); err == nil { toks = append(toks, token{"num", w}) } else { toks = append(toks, token{"word", w}) } i = j } } return toks, true } type value struct { num int64 str string isNum bool unknown bool } func (v value) truthy() bool { if v.isNum { return v.num != 0 } return v.str != "" } type evaluator struct { toks []token pos int } func (e *evaluator) peek() *token { if e.pos < len(e.toks) { return &e.toks[e.pos] } return nil } func (e *evaluator) parseOr() (value, bool) { l, ok := e.parseAnd() if !ok { return l, false } for t := e.peek(); t != nil && t.kind == "op" && t.val == "||"; t = e.peek() { e.pos++ r, ok := e.parseAnd() if !ok { return r, false } l = boolValue(l.truthy() || r.truthy(), l.unknown || r.unknown) } return l, true } func (e *evaluator) parseAnd() (value, bool) { l, ok := e.parseNot() if !ok { return l, false } for t := e.peek(); t != nil && t.kind == "op" && t.val == "&&"; t = e.peek() { e.pos++ r, ok := e.parseNot() if !ok { return r, false } l = boolValue(l.truthy() && r.truthy(), l.unknown || r.unknown) } return l, true } func (e *evaluator) parseNot() (value, bool) { if t := e.peek(); t != nil && t.kind == "op" && t.val == "!" { e.pos++ v, ok := e.parseNot() if !ok { return v, false } return boolValue(!v.truthy(), v.unknown), true } return e.parseCmp() } func (e *evaluator) parseCmp() (value, bool) { l, ok := e.parsePrimary() if !ok { return l, false } t := e.peek() if t == nil || t.kind != "op" || t.val == "&&" || t.val == "||" || t.val == ")" { return l, true } op := t.val e.pos++ r, ok := e.parsePrimary() if !ok { return r, false } if l.unknown || r.unknown { return value{unknown: true}, true } var res bool if l.isNum && r.isNum { switch op { case "==": res = l.num == r.num case "!=": res = l.num != r.num case ">": res = l.num > r.num case "<": res = l.num < r.num case ">=": res = l.num >= r.num case "<=": res = l.num <= r.num default: return value{}, false } } else { ls, rs := l.str, r.str if l.isNum { ls = strconv.FormatInt(l.num, 10) } if r.isNum { rs = strconv.FormatInt(r.num, 10) } switch op { case "==": res = ls == rs case "!=": res = ls != rs case ">": res = ls > rs case "<": res = ls < rs case ">=": res = ls >= rs case "<=": res = ls <= rs default: return value{}, false } } return boolValue(res, false), true } func (e *evaluator) parsePrimary() (value, bool) { t := e.peek() if t == nil { return value{}, false } switch t.kind { case "op": if t.val != "(" { return value{}, false } e.pos++ v, ok := e.parseOr() if !ok { return v, false } if c := e.peek(); c == nil || c.kind != "op" || c.val != ")" { return value{}, false } e.pos++ return v, true case "num": e.pos++ n, _ := strconv.ParseInt(t.val, 10, 64) return value{num: n, isNum: true}, true case "str": e.pos++ if n, err := strconv.ParseInt(strings.TrimSpace(t.val), 10, 64); err == nil { return value{num: n, isNum: true, str: t.val}, true } return value{str: t.val}, true default: // A bare word (an unexpanded value or an arch name): we cannot know. e.pos++ return value{str: t.val, unknown: true}, true } } func boolValue(b, unknown bool) value { v := value{isNum: true, unknown: unknown} if b { v.num = 1 } return v }