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())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										63
									
								
								internal/shutils/handlers/exec.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								internal/shutils/handlers/exec.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"mvdan.cc/sh/v3/interp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func InsufficientArgsError(cmd string, exp, got int) error {
 | 
			
		||||
	argsWord := "arguments"
 | 
			
		||||
	if exp == 1 {
 | 
			
		||||
		argsWord = "argument"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return fmt.Errorf("%s: command requires at least %d %s, got %d", cmd, exp, argsWord, got)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ExecFunc func(hc interp.HandlerContext, name string, args []string) error
 | 
			
		||||
 | 
			
		||||
type ExecFuncs map[string]ExecFunc
 | 
			
		||||
 | 
			
		||||
// ExecHandler returns a new ExecHandlerFunc that falls back to fallback
 | 
			
		||||
// if the command cannot be found in the map. If fallback is nil, the default
 | 
			
		||||
// handler is used.
 | 
			
		||||
func (ef ExecFuncs) ExecHandler(fallback interp.ExecHandlerFunc) interp.ExecHandlerFunc {
 | 
			
		||||
	return func(ctx context.Context, args []string) error {
 | 
			
		||||
		name := args[0]
 | 
			
		||||
 | 
			
		||||
		if fn, ok := ef[name]; ok {
 | 
			
		||||
			hctx := interp.HandlerCtx(ctx)
 | 
			
		||||
			if len(args) > 1 {
 | 
			
		||||
				return fn(hctx, args[0], args[1:])
 | 
			
		||||
			} else {
 | 
			
		||||
				return fn(hctx, args[0], nil)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if fallback == nil {
 | 
			
		||||
			fallback = interp.DefaultExecHandler(2 * time.Second)
 | 
			
		||||
		}
 | 
			
		||||
		return fallback(ctx, args)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										124
									
								
								internal/shutils/handlers/exec_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								internal/shutils/handlers/exec_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,124 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 handlers_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"lure.sh/lure/internal/shutils/handlers"
 | 
			
		||||
	"lure.sh/lure/internal/shutils/decoder"
 | 
			
		||||
	"lure.sh/lure/pkg/distro"
 | 
			
		||||
	"mvdan.cc/sh/v3/interp"
 | 
			
		||||
	"mvdan.cc/sh/v3/syntax"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const testScript = `
 | 
			
		||||
	name='test'
 | 
			
		||||
	version='0.0.1'
 | 
			
		||||
	release=1
 | 
			
		||||
	epoch=2
 | 
			
		||||
	desc="Test package"
 | 
			
		||||
	homepage='https://lure.sh'
 | 
			
		||||
	maintainer='Elara Musayelyan <elara@elara.ws>'
 | 
			
		||||
	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() {
 | 
			
		||||
		test-cmd "Hello, World"
 | 
			
		||||
		test-fb
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	package() {
 | 
			
		||||
		install-binary test
 | 
			
		||||
	}
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
var osRelease = &distro.OSRelease{
 | 
			
		||||
	ID:   "test_os",
 | 
			
		||||
	Like: []string{"arch"},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestExecFuncs(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")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	eh := shutils.ExecFuncs{
 | 
			
		||||
		"test-cmd": func(hc interp.HandlerContext, name string, args []string) error {
 | 
			
		||||
			if name != "test-cmd" {
 | 
			
		||||
				t.Errorf("Expected name to be 'test-cmd', got '%s'", name)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if len(args) < 1 {
 | 
			
		||||
				t.Fatalf("Expected at least one argument, got %d", len(args))
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if args[0] != "Hello, World" {
 | 
			
		||||
				t.Errorf("Expected first argument to be 'Hello, World', got '%s'", args[0])
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return nil
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fbInvoked := false
 | 
			
		||||
	fbHandler := func(context.Context, []string) error {
 | 
			
		||||
		fbInvoked = true
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = fn(ctx, interp.ExecHandler(eh.ExecHandler(fbHandler)))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !fbInvoked {
 | 
			
		||||
		t.Errorf("Expected fallback handler to be invoked")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										114
									
								
								internal/shutils/handlers/fakeroot.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								internal/shutils/handlers/fakeroot.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,114 @@
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"runtime"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"syscall"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"lure.sh/fakeroot"
 | 
			
		||||
	"mvdan.cc/sh/v3/expand"
 | 
			
		||||
	"mvdan.cc/sh/v3/interp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// FakerootExecHandler was extracted from github.com/mvdan/sh/interp/handler.go
 | 
			
		||||
// and modified to run commands in a fakeroot environent.
 | 
			
		||||
func FakerootExecHandler(killTimeout time.Duration) interp.ExecHandlerFunc {
 | 
			
		||||
	return func(ctx context.Context, args []string) error {
 | 
			
		||||
		hc := interp.HandlerCtx(ctx)
 | 
			
		||||
		path, err := interp.LookPathDir(hc.Dir, hc.Env, args[0])
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			fmt.Fprintln(hc.Stderr, err)
 | 
			
		||||
			return interp.NewExitStatus(127)
 | 
			
		||||
		}
 | 
			
		||||
		cmd := &exec.Cmd{
 | 
			
		||||
			Path:   path,
 | 
			
		||||
			Args:   args,
 | 
			
		||||
			Env:    execEnv(hc.Env),
 | 
			
		||||
			Dir:    hc.Dir,
 | 
			
		||||
			Stdin:  hc.Stdin,
 | 
			
		||||
			Stdout: hc.Stdout,
 | 
			
		||||
			Stderr: hc.Stderr,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = fakeroot.Apply(cmd)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = cmd.Start()
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			if done := ctx.Done(); done != nil {
 | 
			
		||||
				go func() {
 | 
			
		||||
					<-done
 | 
			
		||||
 | 
			
		||||
					if killTimeout <= 0 || runtime.GOOS == "windows" {
 | 
			
		||||
						_ = cmd.Process.Signal(os.Kill)
 | 
			
		||||
						return
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					// TODO: don't temporarily leak this goroutine
 | 
			
		||||
					// if the program stops itself with the
 | 
			
		||||
					// interrupt.
 | 
			
		||||
					go func() {
 | 
			
		||||
						time.Sleep(killTimeout)
 | 
			
		||||
						_ = cmd.Process.Signal(os.Kill)
 | 
			
		||||
					}()
 | 
			
		||||
					_ = cmd.Process.Signal(os.Interrupt)
 | 
			
		||||
				}()
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			err = cmd.Wait()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		switch x := err.(type) {
 | 
			
		||||
		case *exec.ExitError:
 | 
			
		||||
			// started, but errored - default to 1 if OS
 | 
			
		||||
			// doesn't have exit statuses
 | 
			
		||||
			if status, ok := x.Sys().(syscall.WaitStatus); ok {
 | 
			
		||||
				if status.Signaled() {
 | 
			
		||||
					if ctx.Err() != nil {
 | 
			
		||||
						return ctx.Err()
 | 
			
		||||
					}
 | 
			
		||||
					return interp.NewExitStatus(uint8(128 + status.Signal()))
 | 
			
		||||
				}
 | 
			
		||||
				return interp.NewExitStatus(uint8(status.ExitStatus()))
 | 
			
		||||
			}
 | 
			
		||||
			return interp.NewExitStatus(1)
 | 
			
		||||
		case *exec.Error:
 | 
			
		||||
			// did not start
 | 
			
		||||
			fmt.Fprintf(hc.Stderr, "%v\n", err)
 | 
			
		||||
			return interp.NewExitStatus(127)
 | 
			
		||||
		default:
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// execEnv was extracted from github.com/mvdan/sh/interp/vars.go
 | 
			
		||||
func execEnv(env expand.Environ) []string {
 | 
			
		||||
	list := make([]string, 0, 64)
 | 
			
		||||
	env.Each(func(name string, vr expand.Variable) bool {
 | 
			
		||||
		if !vr.IsSet() {
 | 
			
		||||
			// If a variable is set globally but unset in the
 | 
			
		||||
			// runner, we need to ensure it's not part of the final
 | 
			
		||||
			// list. Seems like zeroing the element is enough.
 | 
			
		||||
			// This is a linear search, but this scenario should be
 | 
			
		||||
			// rare, and the number of variables shouldn't be large.
 | 
			
		||||
			for i, kv := range list {
 | 
			
		||||
				if strings.HasPrefix(kv, name+"=") {
 | 
			
		||||
					list[i] = ""
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if vr.Exported && vr.Kind == expand.String {
 | 
			
		||||
			list = append(list, name+"="+vr.String())
 | 
			
		||||
		}
 | 
			
		||||
		return true
 | 
			
		||||
	})
 | 
			
		||||
	return list
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										55
									
								
								internal/shutils/handlers/nop.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								internal/shutils/handlers/nop.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func NopReadDir(context.Context, string) ([]os.FileInfo, error) {
 | 
			
		||||
	return nil, os.ErrNotExist
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NopStat(context.Context, string, bool) (os.FileInfo, error) {
 | 
			
		||||
	return nil, os.ErrNotExist
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NopExec(context.Context, []string) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NopOpen(context.Context, string, int, os.FileMode) (io.ReadWriteCloser, error) {
 | 
			
		||||
	return NopRWC{}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type NopRWC struct{}
 | 
			
		||||
 | 
			
		||||
func (NopRWC) Read([]byte) (int, error) {
 | 
			
		||||
	return 0, io.EOF
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (NopRWC) Write(b []byte) (int, error) {
 | 
			
		||||
	return len(b), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (NopRWC) Close() error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										58
									
								
								internal/shutils/handlers/nop_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								internal/shutils/handlers/nop_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 handlers_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"lure.sh/lure/internal/shutils/handlers"
 | 
			
		||||
	"mvdan.cc/sh/v3/interp"
 | 
			
		||||
	"mvdan.cc/sh/v3/syntax"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestNopExec(t *testing.T) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	fl, err := syntax.NewParser().Parse(strings.NewReader(`/bin/echo test`), "lure.sh")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	buf := &bytes.Buffer{}
 | 
			
		||||
	runner, err := interp.New(
 | 
			
		||||
		interp.ExecHandler(handlers.NopExec),
 | 
			
		||||
		interp.StdIO(os.Stdin, buf, buf),
 | 
			
		||||
	)
 | 
			
		||||
	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)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if buf.String() != "" {
 | 
			
		||||
		t.Fatalf("Expected empty string, got %#v", buf.String())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										80
									
								
								internal/shutils/handlers/restricted.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								internal/shutils/handlers/restricted.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,80 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/exp/slices"
 | 
			
		||||
	"mvdan.cc/sh/v3/interp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func RestrictedReadDir(allowedPrefixes ...string) interp.ReadDirHandlerFunc {
 | 
			
		||||
	return func(ctx context.Context, s string) ([]fs.FileInfo, error) {
 | 
			
		||||
		path := filepath.Clean(s)
 | 
			
		||||
		for _, allowedPrefix := range allowedPrefixes {
 | 
			
		||||
			if strings.HasPrefix(path, allowedPrefix) {
 | 
			
		||||
				return interp.DefaultReadDirHandler()(ctx, s)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil, fs.ErrNotExist
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RestrictedStat(allowedPrefixes ...string) interp.StatHandlerFunc {
 | 
			
		||||
	return func(ctx context.Context, s string, b bool) (fs.FileInfo, error) {
 | 
			
		||||
		path := filepath.Clean(s)
 | 
			
		||||
		for _, allowedPrefix := range allowedPrefixes {
 | 
			
		||||
			if strings.HasPrefix(path, allowedPrefix) {
 | 
			
		||||
				return interp.DefaultStatHandler()(ctx, s, b)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil, fs.ErrNotExist
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RestrictedOpen(allowedPrefixes ...string) interp.OpenHandlerFunc {
 | 
			
		||||
	return func(ctx context.Context, s string, i int, fm fs.FileMode) (io.ReadWriteCloser, error) {
 | 
			
		||||
		path := filepath.Clean(s)
 | 
			
		||||
		for _, allowedPrefix := range allowedPrefixes {
 | 
			
		||||
			if strings.HasPrefix(path, allowedPrefix) {
 | 
			
		||||
				return interp.DefaultOpenHandler()(ctx, s, i, fm)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return NopRWC{}, nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RestrictedExec(allowedCmds ...string) interp.ExecHandlerFunc {
 | 
			
		||||
	return func(ctx context.Context, args []string) error {
 | 
			
		||||
		if slices.Contains(allowedCmds, args[0]) {
 | 
			
		||||
			return interp.DefaultExecHandler(2*time.Second)(ctx, args)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										284
									
								
								internal/shutils/helpers/helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										284
									
								
								internal/shutils/helpers/helpers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,284 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 helpers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"unsafe"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-git/go-git/v5"
 | 
			
		||||
	"github.com/go-git/go-git/v5/plumbing/object"
 | 
			
		||||
	"golang.org/x/exp/slices"
 | 
			
		||||
	"lure.sh/lure/internal/shutils/handlers"
 | 
			
		||||
	"mvdan.cc/sh/v3/interp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	ErrNoPipe         = errors.New("command requires data to be piped in")
 | 
			
		||||
	ErrNoDetectManNum = errors.New("manual number cannot be detected from the filename")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Helpers contains all the helper commands
 | 
			
		||||
var Helpers = handlers.ExecFuncs{
 | 
			
		||||
	"install-binary":       installHelperCmd("/usr/bin", 0o755),
 | 
			
		||||
	"install-systemd-user": installHelperCmd("/usr/lib/systemd/user", 0o644),
 | 
			
		||||
	"install-systemd":      installHelperCmd("/usr/lib/systemd/system", 0o644),
 | 
			
		||||
	"install-config":       installHelperCmd("/etc", 0o644),
 | 
			
		||||
	"install-license":      installHelperCmd("/usr/share/licenses", 0o644),
 | 
			
		||||
	"install-desktop":      installHelperCmd("/usr/share/applications", 0o644),
 | 
			
		||||
	"install-icon":         installHelperCmd("/usr/share/pixmaps", 0o644),
 | 
			
		||||
	"install-manual":       installManualCmd,
 | 
			
		||||
	"install-completion":   installCompletionCmd,
 | 
			
		||||
	"install-library":      installLibraryCmd,
 | 
			
		||||
	"git-version":          gitVersionCmd,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Restricted contains restricted read-only helper commands
 | 
			
		||||
// that don't modify any state
 | 
			
		||||
var Restricted = handlers.ExecFuncs{
 | 
			
		||||
	"git-version": gitVersionCmd,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func installHelperCmd(prefix string, perms os.FileMode) handlers.ExecFunc {
 | 
			
		||||
	return func(hc interp.HandlerContext, cmd string, args []string) error {
 | 
			
		||||
		if len(args) < 1 {
 | 
			
		||||
			return handlers.InsufficientArgsError(cmd, 1, len(args))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		from := resolvePath(hc, args[0])
 | 
			
		||||
		to := ""
 | 
			
		||||
		if len(args) > 1 {
 | 
			
		||||
			to = filepath.Join(hc.Env.Get("pkgdir").Str, prefix, args[1])
 | 
			
		||||
		} else {
 | 
			
		||||
			to = filepath.Join(hc.Env.Get("pkgdir").Str, prefix, filepath.Base(from))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err := helperInstall(from, to, perms)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("%s: %w", cmd, err)
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func installManualCmd(hc interp.HandlerContext, cmd string, args []string) error {
 | 
			
		||||
	if len(args) < 1 {
 | 
			
		||||
		return handlers.InsufficientArgsError(cmd, 1, len(args))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	from := resolvePath(hc, args[0])
 | 
			
		||||
	number := filepath.Base(from)
 | 
			
		||||
	// The man page may be compressed with gzip.
 | 
			
		||||
	// If it is, the .gz extension must be removed to properly
 | 
			
		||||
	// detect the number at the end of the filename.
 | 
			
		||||
	number = strings.TrimSuffix(number, ".gz")
 | 
			
		||||
	number = strings.TrimPrefix(filepath.Ext(number), ".")
 | 
			
		||||
 | 
			
		||||
	// If number is not actually a number, return an error
 | 
			
		||||
	if _, err := strconv.Atoi(number); err != nil {
 | 
			
		||||
		return fmt.Errorf("install-manual: %w", ErrNoDetectManNum)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	prefix := "/usr/share/man/man" + number
 | 
			
		||||
	to := filepath.Join(hc.Env.Get("pkgdir").Str, prefix, filepath.Base(from))
 | 
			
		||||
 | 
			
		||||
	return helperInstall(from, to, 0o644)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func installCompletionCmd(hc interp.HandlerContext, cmd string, args []string) error {
 | 
			
		||||
	// If the command's stdin is the same as the system's,
 | 
			
		||||
	// that means nothing was piped in. In this case, return an error.
 | 
			
		||||
	if hc.Stdin == os.Stdin {
 | 
			
		||||
		return fmt.Errorf("install-completion: %w", ErrNoPipe)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(args) < 2 {
 | 
			
		||||
		return handlers.InsufficientArgsError(cmd, 2, len(args))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	shell := args[0]
 | 
			
		||||
	name := args[1]
 | 
			
		||||
 | 
			
		||||
	var prefix string
 | 
			
		||||
	switch shell {
 | 
			
		||||
	case "bash":
 | 
			
		||||
		prefix = "/usr/share/bash-completion/completions"
 | 
			
		||||
	case "zsh":
 | 
			
		||||
		prefix = "/usr/share/zsh/site-functions"
 | 
			
		||||
		name = "_" + name
 | 
			
		||||
	case "fish":
 | 
			
		||||
		prefix = "/usr/share/fish/vendor_completions.d"
 | 
			
		||||
		name += ".fish"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	path := filepath.Join(hc.Env.Get("pkgdir").Str, prefix, name)
 | 
			
		||||
 | 
			
		||||
	err := os.MkdirAll(filepath.Dir(path), 0o755)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dst, err := os.OpenFile(path, os.O_TRUNC|os.O_CREATE|os.O_RDWR, 0o644)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer dst.Close()
 | 
			
		||||
 | 
			
		||||
	_, err = io.Copy(dst, hc.Stdin)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func installLibraryCmd(hc interp.HandlerContext, cmd string, args []string) error {
 | 
			
		||||
	prefix := getLibPrefix(hc)
 | 
			
		||||
	fn := installHelperCmd(prefix, 0o755)
 | 
			
		||||
	return fn(hc, cmd, args)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// See https://wiki.debian.org/Multiarch/Tuples
 | 
			
		||||
var multiarchTupleMap = map[string]string{
 | 
			
		||||
	"386":      "i386-linux-gnu",
 | 
			
		||||
	"amd64":    "x86_64-linux-gnu",
 | 
			
		||||
	"arm5":     "arm-linux-gnueabi",
 | 
			
		||||
	"arm6":     "arm-linux-gnueabihf",
 | 
			
		||||
	"arm7":     "arm-linux-gnueabihf",
 | 
			
		||||
	"arm64":    "aarch64-linux-gnu",
 | 
			
		||||
	"mips":     "mips-linux-gnu",
 | 
			
		||||
	"mipsle":   "mipsel-linux-gnu",
 | 
			
		||||
	"mips64":   "mips64-linux-gnuabi64",
 | 
			
		||||
	"mips64le": "mips64el-linux-gnuabi64",
 | 
			
		||||
	"ppc64":    "powerpc64-linux-gnu",
 | 
			
		||||
	"ppc64le":  "powerpc64le-linux-gnu",
 | 
			
		||||
	"s390x":    "s390x-linux-gnu",
 | 
			
		||||
	"riscv64":  "riscv64-linux-gnu",
 | 
			
		||||
	"loong64":  "loongarch64-linux-gnu",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// usrLibDistros is a list of distros that don't support
 | 
			
		||||
// /usr/lib64, and must use /usr/lib
 | 
			
		||||
var usrLibDistros = []string{
 | 
			
		||||
	"arch",
 | 
			
		||||
	"alpine",
 | 
			
		||||
	"void",
 | 
			
		||||
	"chimera",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Based on CMake's GNUInstallDirs
 | 
			
		||||
func getLibPrefix(hc interp.HandlerContext) string {
 | 
			
		||||
	if dir, ok := os.LookupEnv("LURE_LIB_DIR"); ok {
 | 
			
		||||
		return dir
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	out := "/usr/lib"
 | 
			
		||||
 | 
			
		||||
	distroID := hc.Env.Get("DISTRO_ID").Str
 | 
			
		||||
	distroLike := strings.Split(hc.Env.Get("DISTRO_ID_LIKE").Str, " ")
 | 
			
		||||
 | 
			
		||||
	for _, usrLibDistro := range usrLibDistros {
 | 
			
		||||
		if distroID == usrLibDistro || slices.Contains(distroLike, usrLibDistro) {
 | 
			
		||||
			return out
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wordSize := unsafe.Sizeof(uintptr(0))
 | 
			
		||||
	if wordSize == 8 {
 | 
			
		||||
		out = "/usr/lib64"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	architecture := hc.Env.Get("ARCH").Str
 | 
			
		||||
 | 
			
		||||
	if distroID == "debian" || slices.Contains(distroLike, "debian") ||
 | 
			
		||||
		distroID == "ubuntu" || slices.Contains(distroLike, "ubuntu") {
 | 
			
		||||
 | 
			
		||||
		tuple, ok := multiarchTupleMap[architecture]
 | 
			
		||||
		if ok {
 | 
			
		||||
			out = filepath.Join("/usr/lib", tuple)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func gitVersionCmd(hc interp.HandlerContext, cmd string, args []string) error {
 | 
			
		||||
	path := hc.Dir
 | 
			
		||||
	if len(args) > 0 {
 | 
			
		||||
		path = resolvePath(hc, args[0])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r, err := git.PlainOpen(path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("git-version: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	revNum := 0
 | 
			
		||||
	commits, err := r.Log(&git.LogOptions{})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("git-version: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	commits.ForEach(func(*object.Commit) error {
 | 
			
		||||
		revNum++
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	HEAD, err := r.Head()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("git-version: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hash := HEAD.Hash().String()
 | 
			
		||||
 | 
			
		||||
	fmt.Fprintf(hc.Stdout, "%d.%s\n", revNum, hash[:7])
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func helperInstall(from, to string, perms os.FileMode) error {
 | 
			
		||||
	err := os.MkdirAll(filepath.Dir(to), 0o755)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	src, err := os.Open(from)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer src.Close()
 | 
			
		||||
 | 
			
		||||
	dst, err := os.OpenFile(to, os.O_TRUNC|os.O_CREATE|os.O_RDWR, perms)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer dst.Close()
 | 
			
		||||
 | 
			
		||||
	_, err = io.Copy(dst, src)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func resolvePath(hc interp.HandlerContext, path string) string {
 | 
			
		||||
	if !filepath.IsAbs(path) {
 | 
			
		||||
		return filepath.Join(hc.Dir, path)
 | 
			
		||||
	}
 | 
			
		||||
	return path
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user