Copyright (c) 2024 Huawei Technologies Co., Ltd.
openFuyao is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details.
*/
package checker
import (
"bufio"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"openfuyao.com/oscheck/internal/conf"
"openfuyao.com/oscheck/internal/utils"
)
var (
defaultKVSplitter = regexp.MustCompile(`[ \t]+`)
)
const (
kvSize = 2
)
type ConfKVChecker struct {
Source ConfCheckSource `json:"source"`
ConfKvParser ConfKVParser `json:"kvParser"`
SubItems []SubItemParamBase `json:"subItems"`
confFile string
}
type ConfCheckSource struct {
FilePaths []string `json:"file"`
Commands []string `json:"command"`
SoVersions []string `json:"soversions"`
}
type ConfKVParser struct {
ReSplitter string `json:"reSplitter"`
ReGroup string `json:"reGroup"`
ReIgnore string `json:"reIgnore"`
KeyCaseSensitive bool `json:"keyCaseSensitive"`
}
type kvChecker struct {
ctx Context
conf conf.CheckItem
kvParser KVParser
spec ConfKVChecker
subCheckers []kvSubChecker
logger *log.Logger
}
type KVParser interface {
parse(line string) []string
}
func (c *kvChecker) Init(ctx Context, conf conf.CheckItem) *ParamCheckError {
c.ctx = ctx
c.conf = conf
c.logger = utils.GetLogger("KVChecker")
hasErr := false
checkErr := ParamCheckError{
ErrField: make([]string, 0),
}
spec := ConfKVChecker{}
err := convertSpec2Special(conf, &spec)
if err != nil {
c.logger.Printf("Failed to parse config : %s, because: %s", conf.FilePath, err.Error())
unmarshallErr, ok := err.(*json.UnmarshalTypeError)
if ok {
checkErr.ErrField = append(checkErr.ErrField, fmt.Sprintf("spec.%s", unmarshallErr.Field))
}
checkErr.FormatErr = true
checkErr.Msg = "Format error"
return &checkErr
}
c.checkKvCheckerSource(&spec, &checkErr)
kvparser, err := c.getKvParser(spec.ConfKvParser)
if err != nil {
c.logger.Printf("Failed to parse config : %s, because: %s", conf.FilePath, err.Error())
hasErr = true
checkErr.FormatErr = true
checkErr.ErrField = append(checkErr.ErrField, "spec.KvParser")
} else {
c.kvParser = kvparser
}
c.spec = spec
hasErr = c.initSubCheckers(checkErr)
if hasErr {
return &checkErr
}
return nil
}
func (c *kvChecker) initSubCheckers(checkErr ParamCheckError) bool {
hasErr := false
for _, param := range c.spec.SubItems {
param.Name = strings.TrimSpace(param.Name)
if len(param.Name) == 0 {
checkErr.ErrField = append(checkErr.ErrField, fmt.Sprintf("spec.subItems.[name=%s]", param.Name))
c.logger.Printf("Failed to init checker from file: %s, because subItem name is empty", c.conf.FilePath)
hasErr = true
continue
}
valueChecker, err := GetValueChecker(param.Type, param.Expect)
if err != nil {
c.logger.Printf("Failed to init checker from file: %s, init valuce checker failed for: %s, because: %s",
c.conf.FilePath, param.Name, err.Error())
hasErr = true
checkErr.ErrField = append(checkErr.ErrField, fmt.Sprintf("spec.subItems.[name=%s]", param.Name))
continue
}
subChecker := &kvSubChecker{
param: param,
checker: valueChecker,
}
c.subCheckers = append(c.subCheckers, *subChecker)
}
return hasErr
}
func (c *kvChecker) getSource() (map[string][]string, error) {
rslt := make(map[string][]string)
lineProcessor := func(source, line string) {
line = strings.TrimSpace(line)
values := c.kvParser.parse(line)
if len(values) != kvSize {
c.logger.Printf("Failed to parse line, source: <%v>, raw line data: <%s>,parse result: <%s>",
source, line, values)
return
}
key := strings.TrimSpace(values[0])
if !c.spec.ConfKvParser.KeyCaseSensitive {
key = strings.ToUpper(key)
}
curValues, exist := rslt[key]
if !exist {
curValues = make([]string, 0, 1)
}
rslt[key] = append(curValues, values[1])
}
if len(c.spec.Source.FilePaths) > 0 {
err := c.getKvFromFiles(c.ctx.RootPath, c.spec.Source.FilePaths, lineProcessor)
return rslt, err
} else if len(c.spec.Source.Commands) > 0 {
err := c.getKvFromCommand(c.ctx.RootPath, c.spec.Source.Commands, lineProcessor)
return rslt, err
}
return rslt, fmt.Errorf("failed to get source, because there is no source")
}
func (c *kvChecker) getKvFromFiles(rootPath string, filePaths []string, lineprocessor func(string, string)) error {
for _, filePath := range filePaths {
originFilePath := filePath
filePath = filepath.Join(rootPath, filePath)
var fileNames []string
if strings.Contains(filePath, "*") {
var err error = nil
fileNames, err = filepath.Glob(filePath)
if err != nil {
return fmt.Errorf("failed to parse file for path: %s, because: %s", originFilePath, err.Error())
}
} else {
fileNames = []string{filePath}
}
for _, fileName := range fileNames {
file, err := os.Open(fileName)
if err != nil {
return fmt.Errorf("failed to read file for path: %s, because: %s", originFilePath, err.Error())
}
sc := bufio.NewScanner(file)
for sc.Scan() {
line := sc.Text()
lineprocessor(fileName, line)
}
utils.CloseAll(c.logger, file)
}
}
return nil
}
func (c *kvChecker) getKvFromCommand(rootPath string, commands []string, lineprocessor func(string, string)) error {
for _, command := range commands {
if strings.Contains(command, "$") {
command = strings.ReplaceAll(command, "$", "\\$")
}
exec := utils.Command{
Command: "sh",
Args: []string{"-c", command},
CombineOutput: true,
Timeout: utils.DefaultCmdTimeoutSeconds * time.Second,
RootPath: rootPath,
Logger: c.logger,
}
cmdRslt, err := exec.Exec()
if err != nil {
return err
}
msg := cmdRslt.Stdout
exitCode := cmdRslt.ExitCode
c.logger.Printf("Execute command: <%s>, exit code: <%d>, output: <%s>", command, exitCode, msg)
sc := bufio.NewScanner(strings.NewReader(msg))
for sc.Scan() {
line := sc.Text()
lineprocessor(command, line)
}
}
return nil
}
func (c *kvChecker) Check() ItemRslt {
hasErr := false
hasInvalid := false
rslt := ItemRslt{
Key: c.conf.Name,
Result: ResultValid,
Doc: c.conf.Doc,
SubItems: make([]SubItemRslt, 0, len(c.subCheckers)),
}
source, err := c.getSource()
if err != nil {
c.logger.Printf("Failed to get source for check file: <%s>, because: %s", c.spec.confFile, err.Error())
rslt.Result = ResultError
return rslt
}
for _, subchecker := range c.subCheckers {
valuesToCheck := c.getValuesToCheck(subchecker, source)
matchRslt := matchValue(subchecker.param.MatchType, subchecker.checker, valuesToCheck)
rsltValue := ResultValid
if !matchRslt {
hasInvalid = true
rsltValue = ResultInvalid
}
subItemRslt := SubItemRslt{
Key: subchecker.param.Name,
Expect: fmt.Sprintf("%v", subchecker.param.Expect),
Real: strings.Join(valuesToCheck, ";"),
Result: rsltValue,
Doc: subchecker.param.Doc,
}
rslt.SubItems = append(rslt.SubItems, subItemRslt)
}
if hasErr {
rslt.Result = ResultError
} else if hasInvalid {
rslt.Result = ResultInvalid
}
return rslt
}
func (c *kvChecker) getValuesToCheck(subchecker kvSubChecker, source map[string][]string) []string {
key := subchecker.param.Key
valuesToCheck := make([]string, 0, 1)
var keys []string
if len(key) > 0 {
keys = strings.Split(key, "\n")
} else {
keys = []string{subchecker.param.Name}
}
for _, key := range keys {
if !c.spec.ConfKvParser.KeyCaseSensitive {
key = strings.ToUpper(key)
}
values, ok := source[key]
if ok && len(values) > 0 {
valuesToCheck = append(valuesToCheck, values...)
}
}
if len(valuesToCheck) == 0 {
valuesToCheck = append(valuesToCheck, fmt.Sprintf("%v", subchecker.param.Default))
}
return valuesToCheck
}
func (c *kvChecker) checkKvCheckerSource(conf *ConfKVChecker, checkErr *ParamCheckError) {
if len(conf.Source.Commands) == 0 && len(conf.Source.FilePaths) == 0 {
c.logger.Printf("spec.source is empty: %v", conf.confFile)
checkErr.ErrField = append(checkErr.ErrField, "spec.source")
checkErr.FormatErr = true
return
}
if len(conf.Source.FilePaths) > 0 {
formatedSourceFiles := make([]string, 0, len(conf.Source.FilePaths))
confBasePath := filepath.Dir(conf.confFile)
for _, filePath := range conf.Source.FilePaths {
if filepath.IsAbs(filePath) {
formatedSourceFiles = append(formatedSourceFiles, filePath)
} else {
formatedSourceFiles = append(formatedSourceFiles, filepath.Join(confBasePath, filePath))
}
}
conf.Source.FilePaths = formatedSourceFiles
}
}
func (c *kvChecker) getKvParser(conf ConfKVParser) (KVParser, error) {
var reSplitter, reGroupper, reIgnore *regexp.Regexp
var kvParser KVParser
var err error
if len(conf.ReGroup) > 0 {
reGroupper, err = regexp.Compile(conf.ReGroup)
if err != nil {
c.logger.Printf("Failed to compile reGroup: %s, because: %s", conf.ReGroup, err.Error())
return kvParser, err
}
}
if len(conf.ReSplitter) > 0 {
reSplitter, err = regexp.Compile(conf.ReSplitter)
if err != nil {
c.logger.Printf("Failed to compile reSplitter: %s, because: %s", conf.ReSplitter, err.Error())
return kvParser, err
}
}
if len(conf.ReIgnore) > 0 {
reIgnore, err = regexp.Compile(conf.ReIgnore)
if err != nil {
c.logger.Printf("Failed to compile reIgnore: %s, because: %s", conf.ReIgnore, err.Error())
return kvParser, err
}
}
kvParser = &reKvParser{
reSplitter: reSplitter,
reGroupper: reGroupper,
reIgnore: reIgnore,
caseSensitive: conf.KeyCaseSensitive,
}
return kvParser, nil
}
type kvSubChecker struct {
param SubItemParamBase
checker ValueChecker
}
type reKvParser struct {
reSplitter *regexp.Regexp
reGroupper *regexp.Regexp
reIgnore *regexp.Regexp
caseSensitive bool
}
func (p *reKvParser) parse(line string) []string {
line = strings.TrimSpace(line)
if p.reIgnore != nil && p.reIgnore.MatchString(line) {
return []string{}
}
if p.reGroupper != nil {
rslt := p.reGroupper.FindStringSubmatch(line)
if len(rslt) > 1 {
if len(rslt) > kvSize {
return append([]string{strings.Join(rslt[1:len(rslt)-1], "-")}, rslt[len(rslt)-1])
}
return rslt[1:]
}
return []string{}
}
if p.reSplitter == nil {
p.reSplitter = defaultKVSplitter
}
rslt := p.reSplitter.Split(line, kvSize)
if len(rslt) > 0 {
return rslt
}
return []string{}
}
func init() {
RegisterCheckerFactory(
"kv",
func() Checker {
return &kvChecker{}
},
)
RegisterCheckerFactory(
"kv-checker",
func() Checker {
return &kvChecker{}
},
)
}