357 lines
8.8 KiB
Go
357 lines
8.8 KiB
Go
// This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan.
|
|
// It has been modified as part of "ALR - Any Linux Repository" by the ALR Authors.
|
|
//
|
|
// ALR - Any Linux Repository
|
|
// Copyright (C) 2025 The ALR Authors
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package decoder
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/mitchellh/mapstructure"
|
|
"golang.org/x/exp/slices"
|
|
"mvdan.cc/sh/v3/expand"
|
|
"mvdan.cc/sh/v3/interp"
|
|
"mvdan.cc/sh/v3/syntax"
|
|
|
|
"gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides"
|
|
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
|
|
)
|
|
|
|
var ErrNotPointerToStruct = errors.New("val must be a pointer to a struct")
|
|
|
|
type VarNotFoundError struct {
|
|
name string
|
|
}
|
|
|
|
func (nfe VarNotFoundError) Error() string {
|
|
return "required variable '" + nfe.name + "' could not be found"
|
|
}
|
|
|
|
type InvalidTypeError struct {
|
|
name string
|
|
vartype string
|
|
exptype string
|
|
}
|
|
|
|
func (ite InvalidTypeError) Error() string {
|
|
return fmt.Sprintf("variable '%s' is of type %s, but %s is expected", ite.name, ite.vartype, ite.exptype)
|
|
}
|
|
|
|
// Decoder provides methods for decoding variable values
|
|
type Decoder struct {
|
|
info *distro.OSRelease
|
|
Runner *interp.Runner
|
|
// Enable distro overrides (true by default)
|
|
Overrides bool
|
|
// Enable using like distros for overrides
|
|
LikeDistros bool
|
|
}
|
|
|
|
// New creates a new variable decoder
|
|
func New(info *distro.OSRelease, runner *interp.Runner) *Decoder {
|
|
return &Decoder{info, runner, true, len(info.Like) > 0}
|
|
}
|
|
|
|
// DecodeVar decodes a variable to val using reflection.
|
|
// Structs should use the "sh" struct tag.
|
|
func (d *Decoder) DecodeVar(name string, val any) error {
|
|
origType := reflect.TypeOf(val).Elem()
|
|
isOverridableField := strings.Contains(origType.String(), "OverridableField[")
|
|
|
|
if !isOverridableField {
|
|
variable := d.getVarNoOverrides(name)
|
|
if variable == nil {
|
|
return VarNotFoundError{name}
|
|
}
|
|
|
|
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
|
WeaklyTypedInput: true,
|
|
Result: val, // передаем указатель на новое значение
|
|
TagName: "sh",
|
|
DecodeHook: mapstructure.DecodeHookFuncValue(func(from, to reflect.Value) (interface{}, error) {
|
|
if from.Kind() == reflect.Slice && to.Kind() == reflect.String {
|
|
s, ok := from.Interface().([]string)
|
|
if ok && len(s) == 1 {
|
|
return s[0], nil
|
|
}
|
|
}
|
|
return from.Interface(), nil
|
|
}),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch variable.Kind {
|
|
case expand.Indexed:
|
|
return dec.Decode(variable.List)
|
|
case expand.Associative:
|
|
return dec.Decode(variable.Map)
|
|
default:
|
|
return dec.Decode(variable.Str)
|
|
}
|
|
} else {
|
|
vars := d.getVarsByPrefix(name)
|
|
|
|
if len(vars) == 0 {
|
|
return VarNotFoundError{name}
|
|
}
|
|
|
|
reflectVal := reflect.ValueOf(val)
|
|
overridableVal := reflect.ValueOf(val).Elem()
|
|
|
|
dataField := overridableVal.FieldByName("data")
|
|
if !dataField.IsValid() {
|
|
return fmt.Errorf("data field not found in OverridableField")
|
|
}
|
|
mapType := dataField.Type() // map[string]T
|
|
elemType := mapType.Elem() // T
|
|
|
|
var overridablePtr reflect.Value
|
|
if reflectVal.Kind() == reflect.Ptr {
|
|
overridablePtr = reflectVal
|
|
} else {
|
|
if !reflectVal.CanAddr() {
|
|
return fmt.Errorf("OverridableField value is not addressable")
|
|
}
|
|
overridablePtr = reflectVal.Addr()
|
|
}
|
|
|
|
setValue := overridablePtr.MethodByName("Set")
|
|
if !setValue.IsValid() {
|
|
return fmt.Errorf("method Set not found on OverridableField")
|
|
}
|
|
|
|
for _, v := range vars {
|
|
varName := v.Name
|
|
|
|
key := strings.TrimPrefix(strings.TrimPrefix(varName, name), "_")
|
|
newVal := reflect.New(elemType)
|
|
|
|
if err := d.DecodeVar(varName, newVal.Interface()); err != nil {
|
|
return err
|
|
}
|
|
|
|
keyValue := reflect.ValueOf(key)
|
|
setValue.Call([]reflect.Value{keyValue, newVal.Elem()})
|
|
}
|
|
|
|
resolveValue := overridablePtr.MethodByName("Resolve")
|
|
if !resolveValue.IsValid() {
|
|
return fmt.Errorf("method Resolve not found on OverridableField")
|
|
}
|
|
|
|
names, err := overrides.Resolve(d.info, overrides.DefaultOpts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resolveValue.Call([]reflect.Value{reflect.ValueOf(names)})
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// DecodeVars decodes all variables to val using reflection.
|
|
// Structs should use the "sh" struct tag.
|
|
func (d *Decoder) DecodeVars(val any) error {
|
|
valKind := reflect.TypeOf(val).Kind()
|
|
if valKind != reflect.Pointer {
|
|
return ErrNotPointerToStruct
|
|
} else {
|
|
elemKind := reflect.TypeOf(val).Elem().Kind()
|
|
if elemKind != reflect.Struct {
|
|
return ErrNotPointerToStruct
|
|
}
|
|
}
|
|
|
|
rVal := reflect.ValueOf(val).Elem()
|
|
return d.decodeStruct(rVal)
|
|
}
|
|
|
|
func (d *Decoder) decodeStruct(rVal reflect.Value) error {
|
|
for i := 0; i < rVal.NumField(); i++ {
|
|
field := rVal.Field(i)
|
|
fieldType := rVal.Type().Field(i)
|
|
|
|
// Пропускаем неэкспортируемые поля
|
|
if !fieldType.IsExported() {
|
|
continue
|
|
}
|
|
|
|
// Обрабатываем встроенные поля рекурсивно
|
|
if fieldType.Anonymous {
|
|
if field.Kind() == reflect.Struct {
|
|
if err := d.decodeStruct(field); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
name := fieldType.Name
|
|
tag := fieldType.Tag.Get("sh")
|
|
required := false
|
|
if tag != "" {
|
|
if strings.Contains(tag, ",") {
|
|
splitTag := strings.Split(tag, ",")
|
|
name = splitTag[0]
|
|
|
|
if len(splitTag) > 1 {
|
|
if slices.Contains(splitTag, "required") {
|
|
required = true
|
|
}
|
|
}
|
|
} else {
|
|
name = tag
|
|
}
|
|
}
|
|
|
|
newVal := reflect.New(field.Type())
|
|
err := d.DecodeVar(name, newVal.Interface())
|
|
if _, ok := err.(VarNotFoundError); ok && !required {
|
|
continue
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
field.Set(newVal.Elem())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type (
|
|
ScriptFunc func(ctx context.Context, opts ...interp.RunnerOption) error
|
|
ScriptFuncWithSubshell func(ctx context.Context, opts ...interp.RunnerOption) (*interp.Runner, error)
|
|
)
|
|
|
|
// GetFunc returns a function corresponding to a bash function
|
|
// with the given name
|
|
func (d *Decoder) GetFunc(name string) (ScriptFunc, bool) {
|
|
return d.GetFuncP(name, nil)
|
|
}
|
|
|
|
type PrepareFunc func(context.Context, *interp.Runner) error
|
|
|
|
func (d *Decoder) GetFuncP(name string, prepare PrepareFunc) (ScriptFunc, bool) {
|
|
fn := d.getFunc(name)
|
|
if fn == nil {
|
|
return nil, false
|
|
}
|
|
|
|
return func(ctx context.Context, opts ...interp.RunnerOption) error {
|
|
sub := d.Runner.Subshell()
|
|
for _, opt := range opts {
|
|
err := opt(sub)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if prepare != nil {
|
|
if err := prepare(ctx, sub); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return sub.Run(ctx, fn)
|
|
}, true
|
|
}
|
|
|
|
func (d *Decoder) GetFuncWithSubshell(name string) (ScriptFuncWithSubshell, bool) {
|
|
fn := d.getFunc(name)
|
|
if fn == nil {
|
|
return nil, false
|
|
}
|
|
|
|
return func(ctx context.Context, opts ...interp.RunnerOption) (*interp.Runner, error) {
|
|
sub := d.Runner.Subshell()
|
|
for _, opt := range opts {
|
|
err := opt(sub)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return sub, sub.Run(ctx, fn)
|
|
}, true
|
|
}
|
|
|
|
func (d *Decoder) getFunc(name string) *syntax.Stmt {
|
|
names, err := overrides.Resolve(d.info, overrides.DefaultOpts.WithName(name))
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
for _, fnName := range names {
|
|
fn, ok := d.Runner.Funcs[fnName]
|
|
if ok {
|
|
return fn
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *Decoder) getVarNoOverrides(name string) *expand.Variable {
|
|
val, ok := d.Runner.Vars[name]
|
|
if ok {
|
|
// Resolve nameref variables
|
|
_, resolved := val.Resolve(expand.FuncEnviron(func(s string) string {
|
|
if val, ok := d.Runner.Vars[s]; ok {
|
|
return val.String()
|
|
}
|
|
return ""
|
|
}))
|
|
val = resolved
|
|
|
|
return &val
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type vars struct {
|
|
Name string
|
|
Value *expand.Variable
|
|
}
|
|
|
|
func (d *Decoder) getVarsByPrefix(prefix string) []*vars {
|
|
result := make([]*vars, 0)
|
|
for name, val := range d.Runner.Vars {
|
|
if !strings.HasPrefix(name, prefix) {
|
|
continue
|
|
}
|
|
switch prefix {
|
|
case "auto_req":
|
|
if strings.HasPrefix(name, "auto_req_skiplist") {
|
|
continue
|
|
}
|
|
case "auto_prov":
|
|
if strings.HasPrefix(name, "auto_prov_skiplist") {
|
|
continue
|
|
}
|
|
}
|
|
result = append(result, &vars{name, &val})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func IsTruthy(value string) bool {
|
|
value = strings.ToLower(strings.TrimSpace(value))
|
|
return value == "true" || value == "yes" || value == "1"
|
|
}
|