Initial commit

This commit is contained in:
2024-01-22 13:36:06 +03:00
commit bb8e6e79b2
82 changed files with 11517 additions and 0 deletions

View 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
}

View 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, &notFoundErr) {
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())
}
}