forked from Plemya-x/ALR
		
	Initial commit
This commit is contained in:
		
							
								
								
									
										223
									
								
								internal/shutils/decoder/decoder.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								internal/shutils/decoder/decoder.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,223 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * 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"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/mitchellh/mapstructure"
 | 
			
		||||
	"lure.sh/lure/internal/overrides"
 | 
			
		||||
	"lure.sh/lure/pkg/distro"
 | 
			
		||||
	"golang.org/x/exp/slices"
 | 
			
		||||
	"mvdan.cc/sh/v3/expand"
 | 
			
		||||
	"mvdan.cc/sh/v3/interp"
 | 
			
		||||
	"mvdan.cc/sh/v3/syntax"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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 "variable '" + ite.name + "' is of type " + ite.vartype + ", but " + ite.exptype + " is expected"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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 {
 | 
			
		||||
	variable := d.getVar(name)
 | 
			
		||||
	if variable == nil {
 | 
			
		||||
		return VarNotFoundError{name}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
 | 
			
		||||
		WeaklyTypedInput: true,
 | 
			
		||||
		DecodeHook: mapstructure.DecodeHookFuncValue(func(from, to reflect.Value) (interface{}, error) {
 | 
			
		||||
			if strings.Contains(to.Type().String(), "db.JSON") {
 | 
			
		||||
				valType := to.FieldByName("Val").Type()
 | 
			
		||||
				if !from.Type().AssignableTo(valType) {
 | 
			
		||||
					return nil, InvalidTypeError{name, from.Type().String(), valType.String()}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				to.FieldByName("Val").Set(from)
 | 
			
		||||
				return to, nil
 | 
			
		||||
			}
 | 
			
		||||
			return from.Interface(), nil
 | 
			
		||||
		}),
 | 
			
		||||
		Result:  val,
 | 
			
		||||
		TagName: "sh",
 | 
			
		||||
	})
 | 
			
		||||
	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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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()
 | 
			
		||||
 | 
			
		||||
	for i := 0; i < rVal.NumField(); i++ {
 | 
			
		||||
		field := rVal.Field(i)
 | 
			
		||||
		fieldType := rVal.Type().Field(i)
 | 
			
		||||
 | 
			
		||||
		if !fieldType.IsExported() {
 | 
			
		||||
			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
 | 
			
		||||
 | 
			
		||||
// GetFunc returns a function corresponding to a bash function
 | 
			
		||||
// with the given name
 | 
			
		||||
func (d *Decoder) GetFunc(name string) (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 {
 | 
			
		||||
			opt(sub)
 | 
			
		||||
		}
 | 
			
		||||
		return 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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getVar gets a variable based on its name, taking into account
 | 
			
		||||
// override variables and nameref variables.
 | 
			
		||||
func (d *Decoder) getVar(name string) *expand.Variable {
 | 
			
		||||
	names, err := overrides.Resolve(d.info, overrides.DefaultOpts.WithName(name))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, varName := range names {
 | 
			
		||||
		val, ok := d.Runner.Vars[varName]
 | 
			
		||||
		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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										224
									
								
								internal/shutils/decoder/decoder_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										224
									
								
								internal/shutils/decoder/decoder_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,224 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * 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_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"os"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"lure.sh/lure/internal/shutils/decoder"
 | 
			
		||||
	"lure.sh/lure/pkg/distro"
 | 
			
		||||
	"mvdan.cc/sh/v3/interp"
 | 
			
		||||
	"mvdan.cc/sh/v3/syntax"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type BuildVars struct {
 | 
			
		||||
	Name          string   `sh:"name,required"`
 | 
			
		||||
	Version       string   `sh:"version,required"`
 | 
			
		||||
	Release       int      `sh:"release,required"`
 | 
			
		||||
	Epoch         uint     `sh:"epoch"`
 | 
			
		||||
	Description   string   `sh:"desc"`
 | 
			
		||||
	Homepage      string   `sh:"homepage"`
 | 
			
		||||
	Maintainer    string   `sh:"maintainer"`
 | 
			
		||||
	Architectures []string `sh:"architectures"`
 | 
			
		||||
	Licenses      []string `sh:"license"`
 | 
			
		||||
	Provides      []string `sh:"provides"`
 | 
			
		||||
	Conflicts     []string `sh:"conflicts"`
 | 
			
		||||
	Depends       []string `sh:"deps"`
 | 
			
		||||
	BuildDepends  []string `sh:"build_deps"`
 | 
			
		||||
	Replaces      []string `sh:"replaces"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const testScript = `
 | 
			
		||||
	name='test'
 | 
			
		||||
	version='0.0.1'
 | 
			
		||||
	release=1
 | 
			
		||||
	epoch=2
 | 
			
		||||
	desc="Test package"
 | 
			
		||||
	homepage='https://lure.arsenm.dev'
 | 
			
		||||
	maintainer='Arsen Musayelyan <arsen@arsenm.dev>'
 | 
			
		||||
	architectures=('arm64' 'amd64')
 | 
			
		||||
	license=('GPL-3.0-or-later')
 | 
			
		||||
	provides=('test')
 | 
			
		||||
	conflicts=('test')
 | 
			
		||||
	replaces=('test-old')
 | 
			
		||||
	replaces_test_os=('test-legacy')
 | 
			
		||||
 | 
			
		||||
	deps=('sudo')
 | 
			
		||||
 | 
			
		||||
	build_deps=('golang')
 | 
			
		||||
	build_deps_arch=('go')
 | 
			
		||||
 | 
			
		||||
	test() {
 | 
			
		||||
		echo "Test"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	package() {
 | 
			
		||||
		install-binary test
 | 
			
		||||
	}
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
var osRelease = &distro.OSRelease{
 | 
			
		||||
	ID:   "test_os",
 | 
			
		||||
	Like: []string{"arch"},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDecodeVars(t *testing.T) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "lure.sh")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	runner, err := interp.New()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = runner.Run(ctx, fl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dec := decoder.New(osRelease, runner)
 | 
			
		||||
 | 
			
		||||
	var bv BuildVars
 | 
			
		||||
	err = dec.DecodeVars(&bv)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expected := BuildVars{
 | 
			
		||||
		Name:          "test",
 | 
			
		||||
		Version:       "0.0.1",
 | 
			
		||||
		Release:       1,
 | 
			
		||||
		Epoch:         2,
 | 
			
		||||
		Description:   "Test package",
 | 
			
		||||
		Homepage:      "https://lure.arsenm.dev",
 | 
			
		||||
		Maintainer:    "Arsen Musayelyan <arsen@arsenm.dev>",
 | 
			
		||||
		Architectures: []string{"arm64", "amd64"},
 | 
			
		||||
		Licenses:      []string{"GPL-3.0-or-later"},
 | 
			
		||||
		Provides:      []string{"test"},
 | 
			
		||||
		Conflicts:     []string{"test"},
 | 
			
		||||
		Replaces:      []string{"test-legacy"},
 | 
			
		||||
		Depends:       []string{"sudo"},
 | 
			
		||||
		BuildDepends:  []string{"go"},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !reflect.DeepEqual(bv, expected) {
 | 
			
		||||
		t.Errorf("Expected %v, got %v", expected, bv)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDecodeVarsMissing(t *testing.T) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	const testScript = `
 | 
			
		||||
		name='test'
 | 
			
		||||
		epoch=2
 | 
			
		||||
		desc="Test package"
 | 
			
		||||
		homepage='https://lure.arsenm.dev'
 | 
			
		||||
		maintainer='Arsen Musayelyan <arsen@arsenm.dev>'
 | 
			
		||||
		architectures=('arm64' 'amd64')
 | 
			
		||||
		license=('GPL-3.0-or-later')
 | 
			
		||||
		provides=('test')
 | 
			
		||||
		conflicts=('test')
 | 
			
		||||
		replaces=('test-old')
 | 
			
		||||
		replaces_test_os=('test-legacy')
 | 
			
		||||
 | 
			
		||||
		deps=('sudo')
 | 
			
		||||
 | 
			
		||||
		build_deps=('golang')
 | 
			
		||||
		build_deps_arch=('go')
 | 
			
		||||
 | 
			
		||||
		test() {
 | 
			
		||||
			echo "Test"
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		package() {
 | 
			
		||||
			install-binary test
 | 
			
		||||
		}
 | 
			
		||||
	`
 | 
			
		||||
 | 
			
		||||
	fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "lure.sh")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	runner, err := interp.New()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = runner.Run(ctx, fl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dec := decoder.New(osRelease, runner)
 | 
			
		||||
 | 
			
		||||
	var bv BuildVars
 | 
			
		||||
	err = dec.DecodeVars(&bv)
 | 
			
		||||
 | 
			
		||||
	var notFoundErr decoder.VarNotFoundError
 | 
			
		||||
	if !errors.As(err, ¬FoundErr) {
 | 
			
		||||
		t.Fatalf("Expected VarNotFoundError, got %T %v", err, err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetFunc(t *testing.T) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "lure.sh")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	runner, err := interp.New()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = runner.Run(ctx, fl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dec := decoder.New(osRelease, runner)
 | 
			
		||||
	fn, ok := dec.GetFunc("test")
 | 
			
		||||
	if !ok {
 | 
			
		||||
		t.Fatalf("Expected test() function to exist")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	buf := &bytes.Buffer{}
 | 
			
		||||
	err = fn(ctx, interp.StdIO(os.Stdin, buf, buf))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if buf.String() != "Test\n" {
 | 
			
		||||
		t.Fatalf(`Expected "Test\n", got %#v`, buf.String())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user