| // Copyright 2020 syzkaller project authors. All rights reserved. |
| // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. |
| |
| // Package kconfig implements parsing of the Linux kernel Kconfig and .config files |
| // and provides some algorithms to work with these files. For Kconfig reference see: |
| // https://www.kernel.org/doc/html/latest/kbuild/kconfig-language.html |
| package kconfig |
| |
| import ( |
| "fmt" |
| "os" |
| "path/filepath" |
| "strings" |
| "sync" |
| |
| "github.com/google/syzkaller/sys/targets" |
| ) |
| |
| // KConfig represents a parsed Kconfig file (including includes). |
| type KConfig struct { |
| Root *Menu // mainmenu |
| Configs map[string]*Menu // only config/menuconfig entries |
| } |
| |
| // Menu represents a single hierarchical menu or config. |
| type Menu struct { |
| Kind MenuKind // config/menu/choice/etc |
| Type ConfigType // tristate/bool/string/etc |
| Name string // name without CONFIG_ |
| Elems []*Menu // sub-elements for menus |
| Parent *Menu // parent menu, non-nil for everythign except for mainmenu |
| |
| kconf *KConfig // back-link to the owning KConfig |
| prompts []prompt |
| defaults []defaultVal |
| dependsOn expr |
| visibleIf expr |
| deps map[string]bool |
| depsOnce sync.Once |
| } |
| |
| type prompt struct { |
| text string |
| cond expr |
| } |
| |
| type defaultVal struct { |
| val expr |
| cond expr |
| } |
| |
| type ( |
| MenuKind int |
| ConfigType int |
| ) |
| |
| const ( |
| _ MenuKind = iota |
| MenuConfig |
| MenuGroup |
| MenuChoice |
| MenuComment |
| ) |
| const ( |
| _ ConfigType = iota |
| TypeBool |
| TypeTristate |
| TypeString |
| TypeInt |
| TypeHex |
| ) |
| |
| // DependsOn returns all transitive configs this config depends on. |
| func (m *Menu) DependsOn() map[string]bool { |
| m.depsOnce.Do(func() { |
| m.deps = make(map[string]bool) |
| if m.dependsOn != nil { |
| m.dependsOn.collectDeps(m.deps) |
| } |
| if m.visibleIf != nil { |
| m.visibleIf.collectDeps(m.deps) |
| } |
| var indirect []string |
| for cfg := range m.deps { |
| dep := m.kconf.Configs[cfg] |
| if dep == nil { |
| delete(m.deps, cfg) |
| continue |
| } |
| for cfg1 := range dep.DependsOn() { |
| indirect = append(indirect, cfg1) |
| } |
| } |
| for _, cfg := range indirect { |
| m.deps[cfg] = true |
| } |
| }) |
| return m.deps |
| } |
| |
| func (m *Menu) Prompt() string { |
| // TODO: check prompt conditions, some prompts may be not visible. |
| // If all prompts are not visible, then then menu if effectively disabled (at least for user). |
| for _, p := range m.prompts { |
| return p.text |
| } |
| return "" |
| } |
| |
| type kconfigParser struct { |
| *parser |
| target *targets.Target |
| includes []*parser |
| stack []*Menu |
| cur *Menu |
| baseDir string |
| helpIdent int |
| } |
| |
| func Parse(target *targets.Target, file string) (*KConfig, error) { |
| data, err := os.ReadFile(file) |
| if err != nil { |
| return nil, fmt.Errorf("failed to open Kconfig file %v: %w", file, err) |
| } |
| return ParseData(target, data, file) |
| } |
| |
| func ParseData(target *targets.Target, data []byte, file string) (*KConfig, error) { |
| kp := &kconfigParser{ |
| parser: newParser(data, file), |
| target: target, |
| baseDir: filepath.Dir(file), |
| } |
| kp.parseFile() |
| if kp.err != nil { |
| return nil, kp.err |
| } |
| if len(kp.stack) == 0 { |
| return nil, fmt.Errorf("no mainmenu in config") |
| } |
| root := kp.stack[0] |
| kconf := &KConfig{ |
| Root: root, |
| Configs: make(map[string]*Menu), |
| } |
| kconf.walk(root, nil, nil) |
| return kconf, nil |
| } |
| |
| func (kconf *KConfig) walk(m *Menu, dependsOn, visibleIf expr) { |
| m.kconf = kconf |
| m.dependsOn = exprAnd(dependsOn, m.dependsOn) |
| m.visibleIf = exprAnd(visibleIf, m.visibleIf) |
| if m.Kind == MenuConfig { |
| kconf.Configs[m.Name] = m |
| } |
| for _, elem := range m.Elems { |
| kconf.walk(elem, m.dependsOn, m.visibleIf) |
| } |
| } |
| |
| func (kp *kconfigParser) parseFile() { |
| for kp.nextLine() { |
| kp.parseLine() |
| if kp.TryConsume("#") { |
| _ = kp.ConsumeLine() |
| } |
| } |
| kp.endCurrent() |
| } |
| |
| func (kp *kconfigParser) parseLine() { |
| if kp.eol() { |
| return |
| } |
| if kp.helpIdent != 0 { |
| if kp.identLevel() >= kp.helpIdent { |
| _ = kp.ConsumeLine() |
| return |
| } |
| kp.helpIdent = 0 |
| } |
| if kp.TryConsume("#") { |
| _ = kp.ConsumeLine() |
| return |
| } |
| if kp.TryConsume("$") { |
| _ = kp.Shell() |
| return |
| } |
| ident := kp.Ident() |
| if kp.TryConsume("=") || kp.TryConsume(":=") { |
| // Macro definition, see: |
| // https://www.kernel.org/doc/html/latest/kbuild/kconfig-macro-language.html |
| // We don't use this for anything now. |
| kp.ConsumeLine() |
| return |
| } |
| kp.parseMenu(ident) |
| } |
| |
| func (kp *kconfigParser) parseMenu(cmd string) { |
| switch cmd { |
| case "source": |
| file, ok := kp.TryQuotedString() |
| if !ok { |
| file = kp.ConsumeLine() |
| } |
| kp.includeSource(file) |
| case "mainmenu": |
| kp.pushCurrent(&Menu{ |
| Kind: MenuConfig, |
| prompts: []prompt{{text: kp.QuotedString()}}, |
| }) |
| case "comment": |
| kp.newCurrent(&Menu{ |
| Kind: MenuComment, |
| prompts: []prompt{{text: kp.QuotedString()}}, |
| }) |
| case "menu": |
| kp.pushCurrent(&Menu{ |
| Kind: MenuGroup, |
| prompts: []prompt{{text: kp.QuotedString()}}, |
| }) |
| case "if": |
| kp.pushCurrent(&Menu{ |
| Kind: MenuGroup, |
| visibleIf: kp.parseExpr(), |
| }) |
| case "choice": |
| kp.pushCurrent(&Menu{ |
| Kind: MenuChoice, |
| }) |
| case "endmenu", "endif", "endchoice": |
| kp.popCurrent() |
| case "config", "menuconfig": |
| kp.newCurrent(&Menu{ |
| Kind: MenuConfig, |
| Name: kp.Ident(), |
| }) |
| default: |
| kp.parseConfigType(cmd) |
| } |
| } |
| |
| func (kp *kconfigParser) parseConfigType(typ string) { |
| cur := kp.current() |
| switch typ { |
| case "tristate": |
| cur.Type = TypeTristate |
| kp.tryParsePrompt() |
| case "def_tristate": |
| cur.Type = TypeTristate |
| kp.parseDefaultValue() |
| case "bool": |
| cur.Type = TypeBool |
| kp.tryParsePrompt() |
| case "def_bool": |
| cur.Type = TypeBool |
| kp.parseDefaultValue() |
| case "int": |
| cur.Type = TypeInt |
| kp.tryParsePrompt() |
| case "def_int": |
| cur.Type = TypeInt |
| kp.parseDefaultValue() |
| case "hex": |
| cur.Type = TypeHex |
| kp.tryParsePrompt() |
| case "def_hex": |
| cur.Type = TypeHex |
| kp.parseDefaultValue() |
| case "string": |
| cur.Type = TypeString |
| kp.tryParsePrompt() |
| case "def_string": |
| cur.Type = TypeString |
| kp.parseDefaultValue() |
| default: |
| kp.parseProperty(typ) |
| } |
| } |
| |
| func (kp *kconfigParser) parseProperty(prop string) { |
| cur := kp.current() |
| switch prop { |
| case "prompt": |
| kp.tryParsePrompt() |
| case "depends": |
| kp.MustConsume("on") |
| cur.dependsOn = exprAnd(cur.dependsOn, kp.parseExpr()) |
| case "visible": |
| kp.MustConsume("if") |
| cur.visibleIf = exprAnd(cur.visibleIf, kp.parseExpr()) |
| case "select", "imply": |
| _ = kp.Ident() |
| if kp.TryConsume("if") { |
| _ = kp.parseExpr() |
| } |
| case "option": |
| // It can be 'option foo', or 'option bar="BAZ"'. |
| kp.ConsumeLine() |
| case "modules": |
| case "optional": |
| case "default": |
| kp.parseDefaultValue() |
| case "range": |
| _, _ = kp.parseExpr(), kp.parseExpr() // from, to |
| if kp.TryConsume("if") { |
| _ = kp.parseExpr() |
| } |
| case "help", "---help---": |
| // Help rules are tricky: end of help is identified by smaller indentation level |
| // as would be rendered on a terminal with 8-column tabs setup, minus empty lines. |
| for kp.nextLine() { |
| if kp.eol() { |
| continue |
| } |
| kp.helpIdent = kp.identLevel() |
| kp.ConsumeLine() |
| break |
| } |
| default: |
| kp.failf("unknown line") |
| } |
| } |
| |
| func (kp *kconfigParser) includeSource(file string) { |
| kp.newCurrent(nil) |
| file = kp.expandString(file) |
| file = filepath.Join(kp.baseDir, file) |
| data, err := os.ReadFile(file) |
| if err != nil { |
| kp.failf("%v", err) |
| return |
| } |
| kp.includes = append(kp.includes, kp.parser) |
| kp.parser = newParser(data, file) |
| kp.parseFile() |
| err = kp.err |
| kp.parser = kp.includes[len(kp.includes)-1] |
| kp.includes = kp.includes[:len(kp.includes)-1] |
| if kp.err == nil { |
| kp.err = err |
| } |
| } |
| |
| func (kp *kconfigParser) pushCurrent(m *Menu) { |
| kp.endCurrent() |
| kp.cur = m |
| kp.stack = append(kp.stack, m) |
| } |
| |
| func (kp *kconfigParser) popCurrent() { |
| kp.endCurrent() |
| if len(kp.stack) < 2 { |
| kp.failf("unbalanced endmenu") |
| return |
| } |
| last := kp.stack[len(kp.stack)-1] |
| kp.stack = kp.stack[:len(kp.stack)-1] |
| top := kp.stack[len(kp.stack)-1] |
| last.Parent = top |
| top.Elems = append(top.Elems, last) |
| } |
| |
| func (kp *kconfigParser) newCurrent(m *Menu) { |
| kp.endCurrent() |
| kp.cur = m |
| } |
| |
| func (kp *kconfigParser) current() *Menu { |
| if kp.cur == nil { |
| kp.failf("config property outside of config") |
| return &Menu{} |
| } |
| return kp.cur |
| } |
| |
| func (kp *kconfigParser) endCurrent() { |
| if kp.cur == nil { |
| return |
| } |
| if len(kp.stack) == 0 { |
| kp.failf("unbalanced endmenu") |
| return |
| } |
| top := kp.stack[len(kp.stack)-1] |
| if top != kp.cur { |
| kp.cur.Parent = top |
| top.Elems = append(top.Elems, kp.cur) |
| } |
| kp.cur = nil |
| } |
| |
| func (kp *kconfigParser) tryParsePrompt() { |
| if str, ok := kp.TryQuotedString(); ok { |
| prompt := prompt{ |
| text: str, |
| } |
| if kp.TryConsume("if") { |
| prompt.cond = kp.parseExpr() |
| } |
| kp.current().prompts = append(kp.current().prompts, prompt) |
| } |
| } |
| |
| func (kp *kconfigParser) parseDefaultValue() { |
| def := defaultVal{val: kp.parseExpr()} |
| if kp.TryConsume("if") { |
| def.cond = kp.parseExpr() |
| } |
| kp.current().defaults = append(kp.current().defaults, def) |
| } |
| |
| func (kp *kconfigParser) expandString(str string) string { |
| str = strings.Replace(str, "$(SRCARCH)", kp.target.KernelHeaderArch, -1) |
| str = strings.Replace(str, "$SRCARCH", kp.target.KernelHeaderArch, -1) |
| str = strings.Replace(str, "$(KCONFIG_EXT_PREFIX)", "", -1) |
| return str |
| } |