Compare commits
	
		
			No commits in common. "dfa3217037d8c24181be24484892ecb950a2a5ba" and "c64b8163e79d47f1e710c1d067589bb393f8463c" have entirely different histories.
		
	
	
		
			dfa3217037
			...
			c64b8163e7
		
	
		
@ -12,17 +12,16 @@ jobs:
 | 
				
			|||||||
      - name: Checkout
 | 
					      - name: Checkout
 | 
				
			||||||
        uses: actions/checkout@v4
 | 
					        uses: actions/checkout@v4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Run fortify tests
 | 
					      - name: Run tests
 | 
				
			||||||
        run: nix build --out-link "result-fortify" --print-out-paths --print-build-logs .#checks.x86_64-linux.fortify
 | 
					        run: |
 | 
				
			||||||
 | 
					          nix --print-build-logs --experimental-features 'nix-command flakes' flake check
 | 
				
			||||||
      - name: Run flake checks
 | 
					          nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.nixos-tests
 | 
				
			||||||
        run: nix --print-build-logs --experimental-features 'nix-command flakes' flake check
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Upload test output
 | 
					      - name: Upload test output
 | 
				
			||||||
        uses: actions/upload-artifact@v3
 | 
					        uses: actions/upload-artifact@v3
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          name: "fortify-vm-output"
 | 
					          name: "nixos-vm-output"
 | 
				
			||||||
          path: result-fortify/*
 | 
					          path: result/*
 | 
				
			||||||
          retention-days: 1
 | 
					          retention-days: 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  dist:
 | 
					  dist:
 | 
				
			||||||
 | 
				
			|||||||
@ -8,7 +8,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  lib,
 | 
					  lib,
 | 
				
			||||||
  writeScript,
 | 
					  writeScript,
 | 
				
			||||||
  writeScriptBin,
 | 
					 | 
				
			||||||
  runtimeShell,
 | 
					  runtimeShell,
 | 
				
			||||||
  writeText,
 | 
					  writeText,
 | 
				
			||||||
  symlinkJoin,
 | 
					  symlinkJoin,
 | 
				
			||||||
@ -178,7 +177,7 @@ let
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
in
 | 
					in
 | 
				
			||||||
 | 
					
 | 
				
			||||||
writeScriptBin "build-fpkg-${pname}" ''
 | 
					writeScript "fortify-${pname}-bundle-prelude" ''
 | 
				
			||||||
  #!${runtimeShell} -el
 | 
					  #!${runtimeShell} -el
 | 
				
			||||||
  OUT="$(mktemp -d)"
 | 
					  OUT="$(mktemp -d)"
 | 
				
			||||||
  TAR="$(mktemp -u)"
 | 
					  TAR="$(mktemp -u)"
 | 
				
			||||||
@ -9,10 +9,10 @@ import (
 | 
				
			|||||||
	"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
						"git.gensokyo.uk/security/fortify/internal/fmsg"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const shellPath = "/run/current-system/sw/bin/bash"
 | 
					const shell = "/run/current-system/sw/bin/bash"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func init() {
 | 
					func init() {
 | 
				
			||||||
	if err := os.Setenv("SHELL", shellPath); err != nil {
 | 
						if err := os.Setenv("SHELL", shell); err != nil {
 | 
				
			||||||
		log.Fatalf("cannot set $SHELL: %v", err)
 | 
							log.Fatalf("cannot set $SHELL: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -80,7 +80,7 @@ func actionStart(args []string) {
 | 
				
			|||||||
	if !dropShell {
 | 
						if !dropShell {
 | 
				
			||||||
		command[0] = app.Launcher
 | 
							command[0] = app.Launcher
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		command[0] = shellPath
 | 
							command[0] = shell
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	command = append(command, args[1:]...)
 | 
						command = append(command, args[1:]...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,7 @@ func withNixDaemon(
 | 
				
			|||||||
) {
 | 
					) {
 | 
				
			||||||
	fortifyAppDropShell(updateConfig(&fst.Config{
 | 
						fortifyAppDropShell(updateConfig(&fst.Config{
 | 
				
			||||||
		ID: app.ID,
 | 
							ID: app.ID,
 | 
				
			||||||
		Command: []string{shellPath, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
 | 
							Command: []string{shell, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
 | 
				
			||||||
			// start nix-daemon
 | 
								// start nix-daemon
 | 
				
			||||||
			"nix-daemon --store / & " +
 | 
								"nix-daemon --store / & " +
 | 
				
			||||||
			// wait for socket to appear
 | 
								// wait for socket to appear
 | 
				
			||||||
@ -59,7 +59,7 @@ func withNixDaemon(
 | 
				
			|||||||
func withCacheDir(action string, command []string, workDir string, app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
 | 
					func withCacheDir(action string, command []string, workDir string, app *bundleInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
 | 
				
			||||||
	fortifyAppDropShell(&fst.Config{
 | 
						fortifyAppDropShell(&fst.Config{
 | 
				
			||||||
		ID:      app.ID,
 | 
							ID:      app.ID,
 | 
				
			||||||
		Command: []string{shellPath, "-lc", strings.Join(command, " && ")},
 | 
							Command: []string{shell, "-lc", strings.Join(command, " && ")},
 | 
				
			||||||
		Confinement: fst.ConfinementConfig{
 | 
							Confinement: fst.ConfinementConfig{
 | 
				
			||||||
			AppID:    app.AppID,
 | 
								AppID:    app.AppID,
 | 
				
			||||||
			Username: "nixos",
 | 
								Username: "nixos",
 | 
				
			||||||
@ -92,7 +92,7 @@ func withCacheDir(action string, command []string, workDir string, app *bundleIn
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func fortifyAppDropShell(config *fst.Config, dropShell bool, beforeFail func()) {
 | 
					func fortifyAppDropShell(config *fst.Config, dropShell bool, beforeFail func()) {
 | 
				
			||||||
	if dropShell {
 | 
						if dropShell {
 | 
				
			||||||
		config.Command = []string{shellPath, "-l"}
 | 
							config.Command = []string{shell, "-l"}
 | 
				
			||||||
		fortifyApp(config, beforeFail)
 | 
							fortifyApp(config, beforeFail)
 | 
				
			||||||
		beforeFail()
 | 
							beforeFail()
 | 
				
			||||||
		internal.Exit(0)
 | 
							internal.Exit(0)
 | 
				
			||||||
 | 
				
			|||||||
@ -1,58 +0,0 @@
 | 
				
			|||||||
package command
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"flag"
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"io"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// New initialises a root Node.
 | 
					 | 
				
			||||||
func New(output io.Writer, logf LogFunc, name string) Command {
 | 
					 | 
				
			||||||
	return rootNode{newNode(output, logf, name, "")}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func newNode(output io.Writer, logf LogFunc, name, usage string) *node {
 | 
					 | 
				
			||||||
	n := &node{
 | 
					 | 
				
			||||||
		name: name, usage: usage,
 | 
					 | 
				
			||||||
		out: output, logf: logf,
 | 
					 | 
				
			||||||
		set: flag.NewFlagSet(name, flag.ContinueOnError),
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	n.set.SetOutput(output)
 | 
					 | 
				
			||||||
	n.set.Usage = func() {
 | 
					 | 
				
			||||||
		_ = n.writeHelp()
 | 
					 | 
				
			||||||
		if n.suffix.Len() > 0 {
 | 
					 | 
				
			||||||
			_, _ = fmt.Fprintln(output, "Flags:")
 | 
					 | 
				
			||||||
			n.set.PrintDefaults()
 | 
					 | 
				
			||||||
			_, _ = fmt.Fprintln(output)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return n
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (n *node) Command(name, usage string, f HandlerFunc) Node {
 | 
					 | 
				
			||||||
	if f == nil {
 | 
					 | 
				
			||||||
		panic("invalid handler")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if name == "" || usage == "" {
 | 
					 | 
				
			||||||
		panic("invalid subcommand")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	s := newNode(n.out, n.logf, name, usage)
 | 
					 | 
				
			||||||
	s.f = f
 | 
					 | 
				
			||||||
	if !n.adopt(s) {
 | 
					 | 
				
			||||||
		panic("attempted to initialise subcommand with non-unique name")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return n
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (n *node) New(name, usage string) Node {
 | 
					 | 
				
			||||||
	if name == "" || usage == "" {
 | 
					 | 
				
			||||||
		panic("invalid subcommand tree")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	s := newNode(n.out, n.logf, name, usage)
 | 
					 | 
				
			||||||
	if !n.adopt(s) {
 | 
					 | 
				
			||||||
		panic("attempted to initialise subcommand tree with non-unique name")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return s
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,56 +0,0 @@
 | 
				
			|||||||
package command_test
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"testing"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/command"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestBuild(t *testing.T) {
 | 
					 | 
				
			||||||
	c := command.New(nil, nil, "test")
 | 
					 | 
				
			||||||
	stubHandler := func([]string) error { panic("unreachable") }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("nil direct handler", func(t *testing.T) {
 | 
					 | 
				
			||||||
		defer checkRecover(t, "Command", "invalid handler")
 | 
					 | 
				
			||||||
		c.Command("name", "usage", nil)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("direct zero length", func(t *testing.T) {
 | 
					 | 
				
			||||||
		wantPanic := "invalid subcommand"
 | 
					 | 
				
			||||||
		t.Run("zero length name", func(t *testing.T) { defer checkRecover(t, "Command", wantPanic); c.Command("", "usage", stubHandler) })
 | 
					 | 
				
			||||||
		t.Run("zero length usage", func(t *testing.T) { defer checkRecover(t, "Command", wantPanic); c.Command("name", "", stubHandler) })
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("direct adopt unique names", func(t *testing.T) {
 | 
					 | 
				
			||||||
		c.Command("d0", "usage", stubHandler)
 | 
					 | 
				
			||||||
		c.Command("d1", "usage", stubHandler)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("direct adopt non-unique name", func(t *testing.T) {
 | 
					 | 
				
			||||||
		defer checkRecover(t, "Command", "attempted to initialise subcommand with non-unique name")
 | 
					 | 
				
			||||||
		c.Command("d0", "usage", stubHandler)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("zero length", func(t *testing.T) {
 | 
					 | 
				
			||||||
		wantPanic := "invalid subcommand tree"
 | 
					 | 
				
			||||||
		t.Run("zero length name", func(t *testing.T) { defer checkRecover(t, "New", wantPanic); c.New("", "usage") })
 | 
					 | 
				
			||||||
		t.Run("zero length usage", func(t *testing.T) { defer checkRecover(t, "New", wantPanic); c.New("name", "") })
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("direct adopt unique names", func(t *testing.T) {
 | 
					 | 
				
			||||||
		c.New("t0", "usage")
 | 
					 | 
				
			||||||
		c.New("t1", "usage")
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	t.Run("direct adopt non-unique name", func(t *testing.T) {
 | 
					 | 
				
			||||||
		defer checkRecover(t, "Command", "attempted to initialise subcommand tree with non-unique name")
 | 
					 | 
				
			||||||
		c.New("t0", "usage")
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func checkRecover(t *testing.T, name, wantPanic string) {
 | 
					 | 
				
			||||||
	if r := recover(); r != wantPanic {
 | 
					 | 
				
			||||||
		t.Errorf("%s: panic = %v; wantPanic %v",
 | 
					 | 
				
			||||||
			name, r, wantPanic)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,37 +0,0 @@
 | 
				
			|||||||
// Package command implements generic nested command parsing.
 | 
					 | 
				
			||||||
package command
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"flag"
 | 
					 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type (
 | 
					 | 
				
			||||||
	// HandlerFunc is called when matching a directly handled subcommand tree.
 | 
					 | 
				
			||||||
	HandlerFunc = func(args []string) error
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// LogFunc is the function signature of a printf function.
 | 
					 | 
				
			||||||
	LogFunc = func(format string, a ...any)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// FlagDefiner is a deferred flag definer value, usually encapsulating the default value.
 | 
					 | 
				
			||||||
	FlagDefiner interface {
 | 
					 | 
				
			||||||
		// Define defines the flag in set.
 | 
					 | 
				
			||||||
		Define(b *strings.Builder, set *flag.FlagSet, p any, name, usage string)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	Command interface {
 | 
					 | 
				
			||||||
		Parse(arguments []string) error
 | 
					 | 
				
			||||||
		baseNode[Command]
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	Node baseNode[Node]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	baseNode[T any] interface {
 | 
					 | 
				
			||||||
		// Command appends a subcommand with direct command handling.
 | 
					 | 
				
			||||||
		Command(name, usage string, f HandlerFunc) T
 | 
					 | 
				
			||||||
		// Flag defines a generic flag type in Node's flag set.
 | 
					 | 
				
			||||||
		Flag(p any, name string, value FlagDefiner, usage string) T
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// New returns a new subcommand tree.
 | 
					 | 
				
			||||||
		New(name, usage string) (sub Node)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
@ -1,49 +0,0 @@
 | 
				
			|||||||
package command
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"flag"
 | 
					 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// FlagError wraps errors returned by [flag].
 | 
					 | 
				
			||||||
type FlagError struct{ error }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (e FlagError) Success() bool { return errors.Is(e.error, flag.ErrHelp) }
 | 
					 | 
				
			||||||
func (e FlagError) Is(target error) bool {
 | 
					 | 
				
			||||||
	return (e.error == nil && target == nil) ||
 | 
					 | 
				
			||||||
		((e.error != nil && target != nil) && e.error.Error() == target.Error())
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (n *node) Flag(p any, name string, value FlagDefiner, usage string) Node {
 | 
					 | 
				
			||||||
	value.Define(&n.suffix, n.set, p, name, usage)
 | 
					 | 
				
			||||||
	return n
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// StringFlag is the default value of a string flag.
 | 
					 | 
				
			||||||
type StringFlag string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (v StringFlag) Define(b *strings.Builder, set *flag.FlagSet, p any, name, usage string) {
 | 
					 | 
				
			||||||
	set.StringVar(p.(*string), name, string(v), usage)
 | 
					 | 
				
			||||||
	b.WriteString(" [" + prettyFlag(name) + " <value>]")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// BoolFlag is the default value of a bool flag.
 | 
					 | 
				
			||||||
type BoolFlag bool
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (v BoolFlag) Define(b *strings.Builder, set *flag.FlagSet, p any, name, usage string) {
 | 
					 | 
				
			||||||
	set.BoolVar(p.(*bool), name, bool(v), usage)
 | 
					 | 
				
			||||||
	b.WriteString(" [" + prettyFlag(name) + "]")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// this has no effect on parse outcome
 | 
					 | 
				
			||||||
func prettyFlag(name string) string {
 | 
					 | 
				
			||||||
	switch len(name) {
 | 
					 | 
				
			||||||
	case 0:
 | 
					 | 
				
			||||||
		panic("zero length flag name")
 | 
					 | 
				
			||||||
	case 1:
 | 
					 | 
				
			||||||
		return "-" + name
 | 
					 | 
				
			||||||
	default:
 | 
					 | 
				
			||||||
		return "--" + name
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,49 +0,0 @@
 | 
				
			|||||||
package command
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"io"
 | 
					 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
	"text/tabwriter"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var ErrHelp = errors.New("help requested")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (n *node) writeHelp() error {
 | 
					 | 
				
			||||||
	if _, err := fmt.Fprintf(n.out,
 | 
					 | 
				
			||||||
		"\nUsage:\t%s [-h | --help]%s COMMAND [OPTIONS]\n",
 | 
					 | 
				
			||||||
		strings.Join(append(n.prefix, n.name), " "), &n.suffix,
 | 
					 | 
				
			||||||
	); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if n.child != nil {
 | 
					 | 
				
			||||||
		if _, err := fmt.Fprint(n.out, "\nCommands:\n"); err != nil {
 | 
					 | 
				
			||||||
			return err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	tw := tabwriter.NewWriter(n.out, 0, 1, 4, ' ', 0)
 | 
					 | 
				
			||||||
	if err := n.child.writeCommands(tw); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if err := tw.Flush(); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	_, err := n.out.Write([]byte{'\n'})
 | 
					 | 
				
			||||||
	if err == nil {
 | 
					 | 
				
			||||||
		err = ErrHelp
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return err
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (n *node) writeCommands(w io.Writer) error {
 | 
					 | 
				
			||||||
	if n == nil {
 | 
					 | 
				
			||||||
		return nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if _, err := fmt.Fprintf(w, "\t%s\t%s\n", n.name, n.usage); err != nil {
 | 
					 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return n.next.writeCommands(w)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,40 +0,0 @@
 | 
				
			|||||||
package command
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"flag"
 | 
					 | 
				
			||||||
	"io"
 | 
					 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type node struct {
 | 
					 | 
				
			||||||
	child, next *node
 | 
					 | 
				
			||||||
	name, usage string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	out  io.Writer
 | 
					 | 
				
			||||||
	logf LogFunc
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	prefix []string
 | 
					 | 
				
			||||||
	suffix strings.Builder
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	f   HandlerFunc
 | 
					 | 
				
			||||||
	set *flag.FlagSet
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (n *node) adopt(v *node) bool {
 | 
					 | 
				
			||||||
	if n.child != nil {
 | 
					 | 
				
			||||||
		return n.child.append(v)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	n.child = v
 | 
					 | 
				
			||||||
	return true
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (n *node) append(v *node) bool {
 | 
					 | 
				
			||||||
	if n.name == v.name {
 | 
					 | 
				
			||||||
		return false
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if n.next != nil {
 | 
					 | 
				
			||||||
		return n.next.append(v)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	n.next = v
 | 
					 | 
				
			||||||
	return true
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,72 +0,0 @@
 | 
				
			|||||||
package command
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"log"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var (
 | 
					 | 
				
			||||||
	ErrEmptyTree = errors.New("subcommand tree has no nodes")
 | 
					 | 
				
			||||||
	ErrNoMatch   = errors.New("did not match any subcommand")
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (n *node) Parse(arguments []string) error {
 | 
					 | 
				
			||||||
	if n.usage == "" { // root node has zero length usage
 | 
					 | 
				
			||||||
		if n.next != nil {
 | 
					 | 
				
			||||||
			panic("invalid toplevel state")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		goto match
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if len(arguments) == 0 {
 | 
					 | 
				
			||||||
		// unreachable: zero length args cause upper level to return with a help message
 | 
					 | 
				
			||||||
		panic("attempted to parse with zero length args")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if arguments[0] != n.name {
 | 
					 | 
				
			||||||
		if n.next == nil {
 | 
					 | 
				
			||||||
			n.printf("%q is not a valid command", arguments[0])
 | 
					 | 
				
			||||||
			return ErrNoMatch
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		n.next.prefix = n.prefix
 | 
					 | 
				
			||||||
		return n.next.Parse(arguments)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	arguments = arguments[1:]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
match:
 | 
					 | 
				
			||||||
	if n.child != nil {
 | 
					 | 
				
			||||||
		if n.f != nil {
 | 
					 | 
				
			||||||
			panic("invalid subcommand tree state")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		// propagate help prefix early: flag set usage dereferences help
 | 
					 | 
				
			||||||
		n.child.prefix = append(n.prefix, n.name)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if n.set.Parsed() {
 | 
					 | 
				
			||||||
		panic("invalid set state")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if err := n.set.Parse(arguments); err != nil {
 | 
					 | 
				
			||||||
		return FlagError{err}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	args := n.set.Args()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if n.child != nil {
 | 
					 | 
				
			||||||
		if len(args) == 0 {
 | 
					 | 
				
			||||||
			return n.writeHelp()
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return n.child.Parse(args)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if n.f == nil {
 | 
					 | 
				
			||||||
		n.printf("%q has no subcommands", n.name)
 | 
					 | 
				
			||||||
		return ErrEmptyTree
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return n.f(args)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (n *node) printf(format string, a ...any) {
 | 
					 | 
				
			||||||
	if n.logf == nil {
 | 
					 | 
				
			||||||
		log.Printf(format, a...)
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		n.logf(format, a...)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,292 +0,0 @@
 | 
				
			|||||||
package command_test
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"bytes"
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"flag"
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"io"
 | 
					 | 
				
			||||||
	"log"
 | 
					 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
	"testing"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"git.gensokyo.uk/security/fortify/command"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestParse(t *testing.T) {
 | 
					 | 
				
			||||||
	testCases := []struct {
 | 
					 | 
				
			||||||
		name      string
 | 
					 | 
				
			||||||
		buildTree func(wout, wlog io.Writer) command.Command
 | 
					 | 
				
			||||||
		args      []string
 | 
					 | 
				
			||||||
		want      string
 | 
					 | 
				
			||||||
		wantLog   string
 | 
					 | 
				
			||||||
		wantErr   error
 | 
					 | 
				
			||||||
	}{
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"d=0 empty sub",
 | 
					 | 
				
			||||||
			func(wout, wlog io.Writer) command.Command { return command.New(wout, newLogFunc(wlog), "root") },
 | 
					 | 
				
			||||||
			[]string{""},
 | 
					 | 
				
			||||||
			"", "test: \"root\" has no subcommands\n", command.ErrEmptyTree,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"d=0 empty sub garbage",
 | 
					 | 
				
			||||||
			func(wout, wlog io.Writer) command.Command { return command.New(wout, newLogFunc(wlog), "root") },
 | 
					 | 
				
			||||||
			[]string{"a", "b", "c", "d"},
 | 
					 | 
				
			||||||
			"", "test: \"root\" has no subcommands\n", command.ErrEmptyTree,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"d=0 no match",
 | 
					 | 
				
			||||||
			buildTestCommand,
 | 
					 | 
				
			||||||
			[]string{"nonexistent"},
 | 
					 | 
				
			||||||
			"", "test: \"nonexistent\" is not a valid command\n", command.ErrNoMatch,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"d=0 direct error",
 | 
					 | 
				
			||||||
			buildTestCommand,
 | 
					 | 
				
			||||||
			[]string{"error"},
 | 
					 | 
				
			||||||
			"", "", errSuccess,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"d=0 direct error garbage",
 | 
					 | 
				
			||||||
			buildTestCommand,
 | 
					 | 
				
			||||||
			[]string{"error", "0", "1", "2"},
 | 
					 | 
				
			||||||
			"", "", errSuccess,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"d=0 direct success out of order",
 | 
					 | 
				
			||||||
			buildTestCommand,
 | 
					 | 
				
			||||||
			[]string{"succeed"},
 | 
					 | 
				
			||||||
			"", "", nil,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"d=0 direct success output",
 | 
					 | 
				
			||||||
			buildTestCommand,
 | 
					 | 
				
			||||||
			[]string{"print", "0", "1", "2"},
 | 
					 | 
				
			||||||
			"012", "", nil,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"d=0 string flag",
 | 
					 | 
				
			||||||
			buildTestCommand,
 | 
					 | 
				
			||||||
			[]string{"--val", "64d3b4b7b21788585845060e2199a78f", "flag"},
 | 
					 | 
				
			||||||
			"64d3b4b7b21788585845060e2199a78f", "", nil,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"d=0 out of order string flag",
 | 
					 | 
				
			||||||
			buildTestCommand,
 | 
					 | 
				
			||||||
			[]string{"flag", "--val", "64d3b4b7b21788585845060e2199a78f"},
 | 
					 | 
				
			||||||
			"flag provided but not defined: -val\n\nUsage:\ttest flag [-h | --help] COMMAND [OPTIONS]\n\n", "",
 | 
					 | 
				
			||||||
			errors.New("flag provided but not defined: -val"),
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"d=1 empty sub",
 | 
					 | 
				
			||||||
			buildTestCommand,
 | 
					 | 
				
			||||||
			[]string{"empty"},
 | 
					 | 
				
			||||||
			"", "test: \"empty\" has no subcommands\n", command.ErrEmptyTree,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"d=1 empty sub garbage",
 | 
					 | 
				
			||||||
			buildTestCommand,
 | 
					 | 
				
			||||||
			[]string{"empty", "a", "b", "c", "d"},
 | 
					 | 
				
			||||||
			"", "test: \"empty\" has no subcommands\n", command.ErrEmptyTree,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"d=1 empty sub help",
 | 
					 | 
				
			||||||
			buildTestCommand,
 | 
					 | 
				
			||||||
			[]string{"empty", "-h"},
 | 
					 | 
				
			||||||
			"\nUsage:\ttest empty [-h | --help] COMMAND [OPTIONS]\n\n", "", flag.ErrHelp,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"d=1 no match",
 | 
					 | 
				
			||||||
			buildTestCommand,
 | 
					 | 
				
			||||||
			[]string{"join", "23aa3bb0", "34986782", "d8859355", "cd9ac317", ", "},
 | 
					 | 
				
			||||||
			"", "test: \"23aa3bb0\" is not a valid command\n", command.ErrNoMatch,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"d=1 direct success out",
 | 
					 | 
				
			||||||
			buildTestCommand,
 | 
					 | 
				
			||||||
			[]string{"join", "out", "23aa3bb0", "34986782", "d8859355", "cd9ac317", ", "},
 | 
					 | 
				
			||||||
			"23aa3bb0, 34986782, d8859355, cd9ac317", "", nil,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"d=1 direct success log",
 | 
					 | 
				
			||||||
			buildTestCommand,
 | 
					 | 
				
			||||||
			[]string{"join", "log", "23aa3bb0", "34986782", "d8859355", "cd9ac317", ", "},
 | 
					 | 
				
			||||||
			"", "test: 23aa3bb0, 34986782, d8859355, cd9ac317\n", nil,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"d=4 empty sub",
 | 
					 | 
				
			||||||
			buildTestCommand,
 | 
					 | 
				
			||||||
			[]string{"deep", "d=2", "d=3", "d=4"},
 | 
					 | 
				
			||||||
			"", "test: \"d=4\" has no subcommands\n", command.ErrEmptyTree},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"d=0 help",
 | 
					 | 
				
			||||||
			buildTestCommand,
 | 
					 | 
				
			||||||
			[]string{},
 | 
					 | 
				
			||||||
			`
 | 
					 | 
				
			||||||
Usage:	test [-h | --help] [-v] [--val <value>] COMMAND [OPTIONS]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Commands:
 | 
					 | 
				
			||||||
    error      return an error
 | 
					 | 
				
			||||||
    print      wraps Fprint
 | 
					 | 
				
			||||||
    flag       print value passed by flag
 | 
					 | 
				
			||||||
    empty      empty subcommand
 | 
					 | 
				
			||||||
    join       wraps strings.Join
 | 
					 | 
				
			||||||
    succeed    this command succeeds
 | 
					 | 
				
			||||||
    deep       top level of command tree with various levels
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
`, "", command.ErrHelp,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"d=0 help flag",
 | 
					 | 
				
			||||||
			buildTestCommand,
 | 
					 | 
				
			||||||
			[]string{"-h"},
 | 
					 | 
				
			||||||
			`
 | 
					 | 
				
			||||||
Usage:	test [-h | --help] [-v] [--val <value>] COMMAND [OPTIONS]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Commands:
 | 
					 | 
				
			||||||
    error      return an error
 | 
					 | 
				
			||||||
    print      wraps Fprint
 | 
					 | 
				
			||||||
    flag       print value passed by flag
 | 
					 | 
				
			||||||
    empty      empty subcommand
 | 
					 | 
				
			||||||
    join       wraps strings.Join
 | 
					 | 
				
			||||||
    succeed    this command succeeds
 | 
					 | 
				
			||||||
    deep       top level of command tree with various levels
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Flags:
 | 
					 | 
				
			||||||
  -v	verbosity
 | 
					 | 
				
			||||||
  -val string
 | 
					 | 
				
			||||||
    	store val for the "flag" command (default "default")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
`, "", flag.ErrHelp,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"d=1 help",
 | 
					 | 
				
			||||||
			buildTestCommand,
 | 
					 | 
				
			||||||
			[]string{"join"},
 | 
					 | 
				
			||||||
			`
 | 
					 | 
				
			||||||
Usage:	test join [-h | --help] COMMAND [OPTIONS]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Commands:
 | 
					 | 
				
			||||||
    out    write result to wout
 | 
					 | 
				
			||||||
    log    log result to wlog
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
`, "", command.ErrHelp,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"d=1 help flag",
 | 
					 | 
				
			||||||
			buildTestCommand,
 | 
					 | 
				
			||||||
			[]string{"join", "-h"},
 | 
					 | 
				
			||||||
			`
 | 
					 | 
				
			||||||
Usage:	test join [-h | --help] COMMAND [OPTIONS]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Commands:
 | 
					 | 
				
			||||||
    out    write result to wout
 | 
					 | 
				
			||||||
    log    log result to wlog
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
`, "", flag.ErrHelp,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"d=2 help",
 | 
					 | 
				
			||||||
			buildTestCommand,
 | 
					 | 
				
			||||||
			[]string{"deep", "d=2"},
 | 
					 | 
				
			||||||
			`
 | 
					 | 
				
			||||||
Usage:	test deep d=2 [-h | --help] COMMAND [OPTIONS]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Commands:
 | 
					 | 
				
			||||||
    d=3    relative third level
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
`, "", command.ErrHelp,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		{
 | 
					 | 
				
			||||||
			"d=2 help flag",
 | 
					 | 
				
			||||||
			buildTestCommand,
 | 
					 | 
				
			||||||
			[]string{"deep", "d=2", "-h"},
 | 
					 | 
				
			||||||
			`
 | 
					 | 
				
			||||||
Usage:	test deep d=2 [-h | --help] COMMAND [OPTIONS]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Commands:
 | 
					 | 
				
			||||||
    d=3    relative third level
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
`, "", flag.ErrHelp,
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	for _, tc := range testCases {
 | 
					 | 
				
			||||||
		t.Run(tc.name, func(t *testing.T) {
 | 
					 | 
				
			||||||
			wout, wlog := new(bytes.Buffer), new(bytes.Buffer)
 | 
					 | 
				
			||||||
			c := tc.buildTree(wout, wlog)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if err := c.Parse(tc.args); !errors.Is(err, tc.wantErr) {
 | 
					 | 
				
			||||||
				t.Errorf("Parse: error = %v; wantErr %v", err, tc.wantErr)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			if got := wout.String(); got != tc.want {
 | 
					 | 
				
			||||||
				t.Errorf("Parse: %s want %s", got, tc.want)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			if gotLog := wlog.String(); gotLog != tc.wantLog {
 | 
					 | 
				
			||||||
				t.Errorf("Parse: log = %s wantLog %s", gotLog, tc.wantLog)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var (
 | 
					 | 
				
			||||||
	errJoinLen = errors.New("not enough arguments to join")
 | 
					 | 
				
			||||||
	errSuccess = errors.New("success")
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func buildTestCommand(wout, wlog io.Writer) (c command.Command) {
 | 
					 | 
				
			||||||
	var val string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	logf := newLogFunc(wlog)
 | 
					 | 
				
			||||||
	c = command.New(wout, logf, "test").
 | 
					 | 
				
			||||||
		Flag(new(bool), "v", command.BoolFlag(false), "verbosity").
 | 
					 | 
				
			||||||
		Command("error", "return an error", func([]string) error {
 | 
					 | 
				
			||||||
			return errSuccess
 | 
					 | 
				
			||||||
		}).
 | 
					 | 
				
			||||||
		Command("print", "wraps Fprint", func(args []string) error {
 | 
					 | 
				
			||||||
			a := make([]any, len(args))
 | 
					 | 
				
			||||||
			for i, v := range args {
 | 
					 | 
				
			||||||
				a[i] = v
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			_, err := fmt.Fprint(wout, a...)
 | 
					 | 
				
			||||||
			return err
 | 
					 | 
				
			||||||
		}).
 | 
					 | 
				
			||||||
		Flag(&val, "val", command.StringFlag("default"), "store val for the \"flag\" command").
 | 
					 | 
				
			||||||
		Command("flag", "print value passed by flag", func(args []string) error {
 | 
					 | 
				
			||||||
			_, err := fmt.Fprint(wout, val)
 | 
					 | 
				
			||||||
			return err
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	c.New("empty", "empty subcommand")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	c.New("join", "wraps strings.Join").
 | 
					 | 
				
			||||||
		Command("out", "write result to wout", func(args []string) error {
 | 
					 | 
				
			||||||
			if len(args) == 0 {
 | 
					 | 
				
			||||||
				return errJoinLen
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			_, err := fmt.Fprint(wout, strings.Join(args[:len(args)-1], args[len(args)-1]))
 | 
					 | 
				
			||||||
			return err
 | 
					 | 
				
			||||||
		}).
 | 
					 | 
				
			||||||
		Command("log", "log result to wlog", func(args []string) error {
 | 
					 | 
				
			||||||
			if len(args) == 0 {
 | 
					 | 
				
			||||||
				return errJoinLen
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			logf("%s", strings.Join(args[:len(args)-1], args[len(args)-1]))
 | 
					 | 
				
			||||||
			return nil
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	c.Command("succeed", "this command succeeds", func([]string) error { return nil })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	c.New("deep", "top level of command tree with various levels").
 | 
					 | 
				
			||||||
		New("d=2", "relative second level").
 | 
					 | 
				
			||||||
		New("d=3", "relative third level").
 | 
					 | 
				
			||||||
		New("d=4", "relative fourth level")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func newLogFunc(w io.Writer) command.LogFunc { return log.New(w, "test: ", 0).Printf }
 | 
					 | 
				
			||||||
@ -1,54 +0,0 @@
 | 
				
			|||||||
package command
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"flag"
 | 
					 | 
				
			||||||
	"testing"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TestParseUnreachable(t *testing.T) {
 | 
					 | 
				
			||||||
	// top level bypasses name matching and recursive calls to Parse
 | 
					 | 
				
			||||||
	// returns when encountering zero-length args
 | 
					 | 
				
			||||||
	t.Run("zero-length args", func(t *testing.T) {
 | 
					 | 
				
			||||||
		defer checkRecover(t, "Parse", "attempted to parse with zero length args")
 | 
					 | 
				
			||||||
		_ = newNode(panicWriter{}, nil, " ", " ").Parse(nil)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// top level must not have siblings
 | 
					 | 
				
			||||||
	t.Run("toplevel siblings", func(t *testing.T) {
 | 
					 | 
				
			||||||
		defer checkRecover(t, "Parse", "invalid toplevel state")
 | 
					 | 
				
			||||||
		n := newNode(panicWriter{}, nil, " ", "")
 | 
					 | 
				
			||||||
		n.append(newNode(panicWriter{}, nil, "  ", " "))
 | 
					 | 
				
			||||||
		_ = n.Parse(nil)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// a node with descendents must not have a direct handler
 | 
					 | 
				
			||||||
	t.Run("sub handle conflict", func(t *testing.T) {
 | 
					 | 
				
			||||||
		defer checkRecover(t, "Parse", "invalid subcommand tree state")
 | 
					 | 
				
			||||||
		n := newNode(panicWriter{}, nil, " ", "")
 | 
					 | 
				
			||||||
		n.adopt(newNode(panicWriter{}, nil, " ", " "))
 | 
					 | 
				
			||||||
		n.f = func([]string) error { panic("unreachable") }
 | 
					 | 
				
			||||||
		_ = n.Parse(nil)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// this would only happen if a node was matched twice
 | 
					 | 
				
			||||||
	t.Run("parsed flag set", func(t *testing.T) {
 | 
					 | 
				
			||||||
		defer checkRecover(t, "Parse", "invalid set state")
 | 
					 | 
				
			||||||
		n := newNode(panicWriter{}, nil, " ", "")
 | 
					 | 
				
			||||||
		set := flag.NewFlagSet("parsed", flag.ContinueOnError)
 | 
					 | 
				
			||||||
		set.SetOutput(panicWriter{})
 | 
					 | 
				
			||||||
		_ = set.Parse(nil)
 | 
					 | 
				
			||||||
		n.set = set
 | 
					 | 
				
			||||||
		_ = n.Parse(nil)
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type panicWriter struct{}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (p panicWriter) Write([]byte) (int, error) { panic("unreachable") }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func checkRecover(t *testing.T, name, wantPanic string) {
 | 
					 | 
				
			||||||
	if r := recover(); r != wantPanic {
 | 
					 | 
				
			||||||
		t.Errorf("%s: panic = %v; wantPanic %v",
 | 
					 | 
				
			||||||
			name, r, wantPanic)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,14 +0,0 @@
 | 
				
			|||||||
package command
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// the top level node wants [Command] returned for its builder methods
 | 
					 | 
				
			||||||
type rootNode struct{ *node }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (r rootNode) Command(name, usage string, f HandlerFunc) Command {
 | 
					 | 
				
			||||||
	r.node.Command(name, usage, f)
 | 
					 | 
				
			||||||
	return r
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func (r rootNode) Flag(p any, name string, value FlagDefiner, usage string) Command {
 | 
					 | 
				
			||||||
	r.node.Flag(p, name, value, usage)
 | 
					 | 
				
			||||||
	return r
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										23
									
								
								flake.nix
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								flake.nix
									
									
									
									
									
								
							@ -32,7 +32,7 @@
 | 
				
			|||||||
      buildPackage = forAllSystems (
 | 
					      buildPackage = forAllSystems (
 | 
				
			||||||
        system:
 | 
					        system:
 | 
				
			||||||
        nixpkgsFor.${system}.callPackage (
 | 
					        nixpkgsFor.${system}.callPackage (
 | 
				
			||||||
          import ./cmd/fpkg/build.nix {
 | 
					          import ./bundle.nix {
 | 
				
			||||||
            inherit
 | 
					            inherit
 | 
				
			||||||
              nixpkgsFor
 | 
					              nixpkgsFor
 | 
				
			||||||
              system
 | 
					              system
 | 
				
			||||||
@ -57,16 +57,18 @@
 | 
				
			|||||||
            ;
 | 
					            ;
 | 
				
			||||||
        in
 | 
					        in
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
 | 
					          check-formatting =
 | 
				
			||||||
            cd ${./.}
 | 
					            runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; }
 | 
				
			||||||
 | 
					              ''
 | 
				
			||||||
 | 
					                cd ${./.}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            echo "running nixfmt..."
 | 
					                echo "running nixfmt..."
 | 
				
			||||||
            nixfmt --check .
 | 
					                nixfmt --check .
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            touch $out
 | 
					                touch $out
 | 
				
			||||||
          '';
 | 
					              '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          lint =
 | 
					          check-lint =
 | 
				
			||||||
            runCommandLocal "check-lint"
 | 
					            runCommandLocal "check-lint"
 | 
				
			||||||
              {
 | 
					              {
 | 
				
			||||||
                nativeBuildInputs = [
 | 
					                nativeBuildInputs = [
 | 
				
			||||||
@ -86,7 +88,10 @@
 | 
				
			|||||||
                touch $out
 | 
					                touch $out
 | 
				
			||||||
              '';
 | 
					              '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          fortify = callPackage ./tests/fortify { inherit system self; };
 | 
					          nixos-tests = callPackage ./test.nix {
 | 
				
			||||||
 | 
					            inherit system self home-manager;
 | 
				
			||||||
 | 
					            inherit (self.packages.${system}) fortify;
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,3 @@
 | 
				
			|||||||
// Package fst exports shared fortify types.
 | 
					 | 
				
			||||||
package fst
 | 
					package fst
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								fst/shared.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								fst/shared.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					// Package fst exports shared fortify types.
 | 
				
			||||||
 | 
					package fst
 | 
				
			||||||
@ -1,4 +1,3 @@
 | 
				
			|||||||
// Package system provides tools for safely interacting with the operating system.
 | 
					 | 
				
			||||||
package system
 | 
					package system
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
@ -15,7 +14,6 @@ const (
 | 
				
			|||||||
	Process = Enablement(ELen + 1)
 | 
						Process = Enablement(ELen + 1)
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Criteria specifies types of Op to revert.
 | 
					 | 
				
			||||||
type Criteria struct {
 | 
					type Criteria struct {
 | 
				
			||||||
	*Enablements
 | 
						*Enablements
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -44,7 +42,6 @@ type Op interface {
 | 
				
			|||||||
	String() string
 | 
						String() string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// TypeString returns the string representation of a type stored as an [Enablement].
 | 
					 | 
				
			||||||
func TypeString(e Enablement) string {
 | 
					func TypeString(e Enablement) string {
 | 
				
			||||||
	switch e {
 | 
						switch e {
 | 
				
			||||||
	case User:
 | 
						case User:
 | 
				
			||||||
@ -67,7 +64,6 @@ func New(uid int) (sys *I) {
 | 
				
			|||||||
	return
 | 
						return
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// An I provides indirect bulk operating system interaction. I must not be copied.
 | 
					 | 
				
			||||||
type I struct {
 | 
					type I struct {
 | 
				
			||||||
	uid int
 | 
						uid int
 | 
				
			||||||
	ops []Op
 | 
						ops []Op
 | 
				
			||||||
@ -95,7 +91,6 @@ func (sys *I) wrapErrSuffix(err error, a ...any) error {
 | 
				
			|||||||
	return sys.wrapErr(err, append(a, err)...)
 | 
						return sys.wrapErr(err, append(a, err)...)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Equal returns whether all [Op] instances held by v is identical to that of sys.
 | 
					 | 
				
			||||||
func (sys *I) Equal(v *I) bool {
 | 
					func (sys *I) Equal(v *I) bool {
 | 
				
			||||||
	if v == nil || sys.uid != v.uid || len(sys.ops) != len(v.ops) {
 | 
						if v == nil || sys.uid != v.uid || len(sys.ops) != len(v.ops) {
 | 
				
			||||||
		return false
 | 
							return false
 | 
				
			||||||
@ -110,8 +105,6 @@ func (sys *I) Equal(v *I) bool {
 | 
				
			|||||||
	return true
 | 
						return true
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Commit applies all [Op] held by [I] and reverts successful [Op] on first error encountered.
 | 
					 | 
				
			||||||
// Commit must not be called more than once.
 | 
					 | 
				
			||||||
func (sys *I) Commit(ctx context.Context) error {
 | 
					func (sys *I) Commit(ctx context.Context) error {
 | 
				
			||||||
	sys.lock.Lock()
 | 
						sys.lock.Lock()
 | 
				
			||||||
	defer sys.lock.Unlock()
 | 
						defer sys.lock.Unlock()
 | 
				
			||||||
@ -148,7 +141,6 @@ func (sys *I) Commit(ctx context.Context) error {
 | 
				
			|||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Revert reverts all [Op] meeting [Criteria] held by [I].
 | 
					 | 
				
			||||||
func (sys *I) Revert(ec *Criteria) error {
 | 
					func (sys *I) Revert(ec *Criteria) error {
 | 
				
			||||||
	sys.lock.Lock()
 | 
						sys.lock.Lock()
 | 
				
			||||||
	defer sys.lock.Unlock()
 | 
						defer sys.lock.Unlock()
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										401
									
								
								test.nix
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										401
									
								
								test.nix
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,401 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  system,
 | 
				
			||||||
 | 
					  self,
 | 
				
			||||||
 | 
					  home-manager,
 | 
				
			||||||
 | 
					  nixosTest,
 | 
				
			||||||
 | 
					  fortify,
 | 
				
			||||||
 | 
					}:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					nixosTest {
 | 
				
			||||||
 | 
					  name = "fortify";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # adapted from nixos sway integration tests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # testScriptWithTypes:49: error: Cannot call function of unknown type
 | 
				
			||||||
 | 
					  #           (machine.succeed if succeed else machine.execute)(
 | 
				
			||||||
 | 
					  #           ^
 | 
				
			||||||
 | 
					  # Found 1 error in 1 file (checked 1 source file)
 | 
				
			||||||
 | 
					  skipTypeCheck = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  nodes.machine =
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      lib,
 | 
				
			||||||
 | 
					      pkgs,
 | 
				
			||||||
 | 
					      config,
 | 
				
			||||||
 | 
					      ...
 | 
				
			||||||
 | 
					    }:
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      users.users = {
 | 
				
			||||||
 | 
					        alice = {
 | 
				
			||||||
 | 
					          isNormalUser = true;
 | 
				
			||||||
 | 
					          description = "Alice Foobar";
 | 
				
			||||||
 | 
					          password = "foobar";
 | 
				
			||||||
 | 
					          uid = 1000;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        untrusted = {
 | 
				
			||||||
 | 
					          isNormalUser = true;
 | 
				
			||||||
 | 
					          description = "Untrusted user";
 | 
				
			||||||
 | 
					          password = "foobar";
 | 
				
			||||||
 | 
					          uid = 1001;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          # For deny unmapped uid test:
 | 
				
			||||||
 | 
					          packages = [ config.environment.fortify.package ];
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      home-manager.users.alice.home.stateVersion = "24.11";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      # Automatically login on tty1 as a normal user:
 | 
				
			||||||
 | 
					      services.getty.autologinUser = "alice";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      environment = {
 | 
				
			||||||
 | 
					        systemPackages = with pkgs; [
 | 
				
			||||||
 | 
					          # For glinfo and wayland-info:
 | 
				
			||||||
 | 
					          mesa-demos
 | 
				
			||||||
 | 
					          wayland-utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          # For D-Bus tests:
 | 
				
			||||||
 | 
					          libnotify
 | 
				
			||||||
 | 
					          mako
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          # For go tests:
 | 
				
			||||||
 | 
					          self.packages.${system}.fhs
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        variables = {
 | 
				
			||||||
 | 
					          SWAYSOCK = "/tmp/sway-ipc.sock";
 | 
				
			||||||
 | 
					          WLR_RENDERER = "pixman";
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # To help with OCR:
 | 
				
			||||||
 | 
					        etc."xdg/foot/foot.ini".text = lib.generators.toINI { } {
 | 
				
			||||||
 | 
					          main = {
 | 
				
			||||||
 | 
					            font = "inconsolata:size=14";
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					          colors = rec {
 | 
				
			||||||
 | 
					            foreground = "000000";
 | 
				
			||||||
 | 
					            background = "ffffff";
 | 
				
			||||||
 | 
					            regular2 = foreground;
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      fonts.packages = [ pkgs.inconsolata ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      # Automatically configure and start Sway when logging in on tty1:
 | 
				
			||||||
 | 
					      programs.bash.loginShellInit = ''
 | 
				
			||||||
 | 
					        if [ "$(tty)" = "/dev/tty1" ]; then
 | 
				
			||||||
 | 
					          set -e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          mkdir -p ~/.config/sway
 | 
				
			||||||
 | 
					          (sed s/Mod4/Mod1/ /etc/sway/config &&
 | 
				
			||||||
 | 
					          echo 'output * bg ${pkgs.nixos-artwork.wallpapers.simple-light-gray.gnomeFilePath} fill' &&
 | 
				
			||||||
 | 
					          echo 'output Virtual-1 res 1680x1050') > ~/.config/sway/config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          sway --validate
 | 
				
			||||||
 | 
					          systemd-cat --identifier=sway sway && touch /tmp/sway-exit-ok
 | 
				
			||||||
 | 
					        fi
 | 
				
			||||||
 | 
					      '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      programs.sway.enable = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      # For PulseAudio tests:
 | 
				
			||||||
 | 
					      security.rtkit.enable = true;
 | 
				
			||||||
 | 
					      services.pipewire = {
 | 
				
			||||||
 | 
					        enable = true;
 | 
				
			||||||
 | 
					        alsa.enable = true;
 | 
				
			||||||
 | 
					        alsa.support32Bit = true;
 | 
				
			||||||
 | 
					        pulse.enable = true;
 | 
				
			||||||
 | 
					        jack.enable = true;
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      virtualisation.qemu.options = [
 | 
				
			||||||
 | 
					        # Need to switch to a different GPU driver than the default one (-vga std) so that Sway can launch:
 | 
				
			||||||
 | 
					        "-vga none -device virtio-gpu-pci"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Increase Go test compiler performance:
 | 
				
			||||||
 | 
					        "-smp 8"
 | 
				
			||||||
 | 
					      ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      environment.fortify = {
 | 
				
			||||||
 | 
					        enable = true;
 | 
				
			||||||
 | 
					        package = fortify.overrideAttrs (previousAttrs: {
 | 
				
			||||||
 | 
					          GOFLAGS = previousAttrs.GOFLAGS ++ [ "-race" ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          # fsu does not like cgo
 | 
				
			||||||
 | 
					          disallowedReferences = previousAttrs.disallowedReferences ++ [ fortify ];
 | 
				
			||||||
 | 
					          postInstall =
 | 
				
			||||||
 | 
					            previousAttrs.postInstall
 | 
				
			||||||
 | 
					            + ''
 | 
				
			||||||
 | 
					              cp -a "${fortify}/libexec/fsu" "$out/libexec/fsu"
 | 
				
			||||||
 | 
					              sed -i 's:${fortify}:${placeholder "out"}:' "$out/libexec/fsu"
 | 
				
			||||||
 | 
					            '';
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        stateDir = "/var/lib/fortify";
 | 
				
			||||||
 | 
					        users.alice = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        home-manager = _: _: { home.stateVersion = "23.05"; };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        apps = [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            name = "ne-foot";
 | 
				
			||||||
 | 
					            verbose = true;
 | 
				
			||||||
 | 
					            share = pkgs.foot;
 | 
				
			||||||
 | 
					            packages = [ pkgs.foot ];
 | 
				
			||||||
 | 
					            command = "foot";
 | 
				
			||||||
 | 
					            capability = {
 | 
				
			||||||
 | 
					              dbus = false;
 | 
				
			||||||
 | 
					              pulse = false;
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            name = "pa-foot";
 | 
				
			||||||
 | 
					            verbose = true;
 | 
				
			||||||
 | 
					            share = pkgs.foot;
 | 
				
			||||||
 | 
					            packages = [ pkgs.foot ];
 | 
				
			||||||
 | 
					            command = "foot";
 | 
				
			||||||
 | 
					            capability.dbus = false;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            name = "x11-alacritty";
 | 
				
			||||||
 | 
					            verbose = true;
 | 
				
			||||||
 | 
					            share = pkgs.alacritty;
 | 
				
			||||||
 | 
					            packages = [ pkgs.alacritty ];
 | 
				
			||||||
 | 
					            command = "alacritty";
 | 
				
			||||||
 | 
					            capability = {
 | 
				
			||||||
 | 
					              wayland = false;
 | 
				
			||||||
 | 
					              x11 = true;
 | 
				
			||||||
 | 
					              dbus = false;
 | 
				
			||||||
 | 
					              pulse = false;
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            name = "da-foot";
 | 
				
			||||||
 | 
					            verbose = true;
 | 
				
			||||||
 | 
					            insecureWayland = true;
 | 
				
			||||||
 | 
					            share = pkgs.foot;
 | 
				
			||||||
 | 
					            packages = [ pkgs.foot ];
 | 
				
			||||||
 | 
					            command = "foot";
 | 
				
			||||||
 | 
					            capability = {
 | 
				
			||||||
 | 
					              dbus = false;
 | 
				
			||||||
 | 
					              pulse = false;
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            name = "strace-failure";
 | 
				
			||||||
 | 
					            verbose = true;
 | 
				
			||||||
 | 
					            share = pkgs.strace;
 | 
				
			||||||
 | 
					            command = "strace true";
 | 
				
			||||||
 | 
					            capability = {
 | 
				
			||||||
 | 
					              wayland = false;
 | 
				
			||||||
 | 
					              x11 = false;
 | 
				
			||||||
 | 
					              dbus = false;
 | 
				
			||||||
 | 
					              pulse = false;
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      imports = [
 | 
				
			||||||
 | 
					        self.nixosModules.fortify
 | 
				
			||||||
 | 
					        home-manager.nixosModules.home-manager
 | 
				
			||||||
 | 
					      ];
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  testScript = ''
 | 
				
			||||||
 | 
					    import shlex
 | 
				
			||||||
 | 
					    import json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    q = shlex.quote
 | 
				
			||||||
 | 
					    NODE_GROUPS = ["nodes", "floating_nodes"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def swaymsg(command: str = "", succeed=True, type="command"):
 | 
				
			||||||
 | 
					        assert command != "" or type != "command", "Must specify command or type"
 | 
				
			||||||
 | 
					        shell = q(f"swaymsg -t {q(type)} -- {q(command)}")
 | 
				
			||||||
 | 
					        with machine.nested(
 | 
				
			||||||
 | 
					            f"sending swaymsg {shell!r}" + " (allowed to fail)" * (not succeed)
 | 
				
			||||||
 | 
					        ):
 | 
				
			||||||
 | 
					            ret = (machine.succeed if succeed else machine.execute)(
 | 
				
			||||||
 | 
					                f"su - alice -c {shell}"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # execute also returns a status code, but disregard.
 | 
				
			||||||
 | 
					        if not succeed:
 | 
				
			||||||
 | 
					            _, ret = ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not succeed and not ret:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        parsed = json.loads(ret)
 | 
				
			||||||
 | 
					        return parsed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def walk(tree):
 | 
				
			||||||
 | 
					        yield tree
 | 
				
			||||||
 | 
					        for group in NODE_GROUPS:
 | 
				
			||||||
 | 
					          for node in tree.get(group, []):
 | 
				
			||||||
 | 
					              yield from walk(node)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def wait_for_window(pattern):
 | 
				
			||||||
 | 
					        def func(last_chance):
 | 
				
			||||||
 | 
					            nodes = (node["name"] for node in walk(swaymsg(type="get_tree")))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if last_chance:
 | 
				
			||||||
 | 
					                nodes = list(nodes)
 | 
				
			||||||
 | 
					                machine.log(f"Last call! Current list of windows: {nodes}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return any(pattern in name for name in nodes)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        retry(func)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def collect_state_ui(name):
 | 
				
			||||||
 | 
					        swaymsg(f"exec fortify ps > '/tmp/{name}.ps'")
 | 
				
			||||||
 | 
					        machine.copy_from_vm(f"/tmp/{name}.ps", "")
 | 
				
			||||||
 | 
					        swaymsg(f"exec fortify --json ps > '/tmp/{name}.json'")
 | 
				
			||||||
 | 
					        machine.copy_from_vm(f"/tmp/{name}.json", "")
 | 
				
			||||||
 | 
					        machine.screenshot(name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def check_state(name, enablements):
 | 
				
			||||||
 | 
					        instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 fortify --json ps"))
 | 
				
			||||||
 | 
					        if len(instances) != 1:
 | 
				
			||||||
 | 
					            raise Exception(f"unexpected state length {len(instances)}")
 | 
				
			||||||
 | 
					        instance = next(iter(instances.values()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        config = instance['config']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if len(config['command']) != 1 or not(config['command'][0].startswith("/nix/store/")) or not(config['command'][0].endswith(f"{name}-start")):
 | 
				
			||||||
 | 
					            raise Exception(f"unexpected command {instance['config']['command']}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if config['confinement']['enablements'] != enablements:
 | 
				
			||||||
 | 
					            raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def fortify(command):
 | 
				
			||||||
 | 
					        swaymsg(f"exec fortify {command}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    start_all()
 | 
				
			||||||
 | 
					    machine.wait_for_unit("multi-user.target")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Run fortify Go tests outside of nix build in the background:
 | 
				
			||||||
 | 
					    machine.succeed("rm -rf /tmp/src && cp -a '${self.packages.${system}.fortify.src}' /tmp/src")
 | 
				
			||||||
 | 
					    machine.succeed("fortify-fhs -c '(cd /tmp/src && go generate ./... && go test ./... && touch /tmp/success-gotest)' &> /tmp/gotest &")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # To check fortify's version:
 | 
				
			||||||
 | 
					    print(machine.succeed("sudo -u alice -i fortify version"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Wait for Sway to complete startup:
 | 
				
			||||||
 | 
					    machine.wait_for_file("/run/user/1000/wayland-1")
 | 
				
			||||||
 | 
					    machine.wait_for_file("/tmp/sway-ipc.sock")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Deny unmapped uid:
 | 
				
			||||||
 | 
					    denyOutput = machine.fail("sudo -u untrusted -i fortify run &>/dev/stdout")
 | 
				
			||||||
 | 
					    print(denyOutput)
 | 
				
			||||||
 | 
					    denyOutputVerbose = machine.fail("sudo -u untrusted -i fortify -v run &>/dev/stdout")
 | 
				
			||||||
 | 
					    print(denyOutputVerbose)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Verify PrintBaseError behaviour:
 | 
				
			||||||
 | 
					    if denyOutput != "fsu: uid 1001 is not in the fsurc file\n":
 | 
				
			||||||
 | 
					        raise Exception(f"unexpected deny output:\n{denyOutput}")
 | 
				
			||||||
 | 
					    if denyOutputVerbose != "fsu: uid 1001 is not in the fsurc file\nfortify: *cannot obtain uid from fsu: permission denied\n":
 | 
				
			||||||
 | 
					        raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Start fortify permissive defaults outside Wayland session:
 | 
				
			||||||
 | 
					    print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare"))
 | 
				
			||||||
 | 
					    machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-bare")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Verify silent output permissive defaults:
 | 
				
			||||||
 | 
					    output = machine.succeed("sudo -u alice -i fortify run -a 0 true &>/dev/stdout")
 | 
				
			||||||
 | 
					    if output != "":
 | 
				
			||||||
 | 
					        raise Exception(f"unexpected output\n{output}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Verify graceful failure on bad Wayland display name:
 | 
				
			||||||
 | 
					    print(machine.fail("sudo -u alice -i fortify -v run --wayland true"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Start fortify permissive defaults within Wayland session:
 | 
				
			||||||
 | 
					    fortify('-v run --wayland --dbus notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-done')
 | 
				
			||||||
 | 
					    machine.wait_for_file("/tmp/dbus-done")
 | 
				
			||||||
 | 
					    collect_state_ui("dbus_notify_exited")
 | 
				
			||||||
 | 
					    machine.succeed("pkill -9 mako")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Start app (foot) with Wayland enablement:
 | 
				
			||||||
 | 
					    swaymsg("exec ne-foot")
 | 
				
			||||||
 | 
					    wait_for_window("u0_a1@machine")
 | 
				
			||||||
 | 
					    machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
 | 
				
			||||||
 | 
					    machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/success-client")
 | 
				
			||||||
 | 
					    collect_state_ui("foot_wayland")
 | 
				
			||||||
 | 
					    check_state("ne-foot", 1)
 | 
				
			||||||
 | 
					    # Verify acl on XDG_RUNTIME_DIR:
 | 
				
			||||||
 | 
					    print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000001"))
 | 
				
			||||||
 | 
					    machine.send_chars("exit\n")
 | 
				
			||||||
 | 
					    machine.wait_until_fails("pgrep foot")
 | 
				
			||||||
 | 
					    # Verify acl cleanup on XDG_RUNTIME_DIR:
 | 
				
			||||||
 | 
					    machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000001")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Start app (foot) with Wayland enablement from a terminal:
 | 
				
			||||||
 | 
					    swaymsg("exec foot $SHELL -c '(ne-foot) & sleep 1 && fortify show $(fortify ps --short) && touch /tmp/ps-show-ok && cat'")
 | 
				
			||||||
 | 
					    wait_for_window("u0_a1@machine")
 | 
				
			||||||
 | 
					    machine.send_chars("clear; wayland-info && touch /tmp/success-client-term\n")
 | 
				
			||||||
 | 
					    machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/success-client-term")
 | 
				
			||||||
 | 
					    machine.wait_for_file("/tmp/ps-show-ok")
 | 
				
			||||||
 | 
					    collect_state_ui("foot_wayland_term")
 | 
				
			||||||
 | 
					    check_state("ne-foot", 1)
 | 
				
			||||||
 | 
					    machine.send_chars("exit\n")
 | 
				
			||||||
 | 
					    wait_for_window("foot")
 | 
				
			||||||
 | 
					    machine.send_key("ctrl-c")
 | 
				
			||||||
 | 
					    machine.wait_until_fails("pgrep foot")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Test PulseAudio (fortify does not support PipeWire yet):
 | 
				
			||||||
 | 
					    swaymsg("exec pa-foot")
 | 
				
			||||||
 | 
					    wait_for_window("u0_a2@machine")
 | 
				
			||||||
 | 
					    machine.send_chars("clear; pactl info && touch /tmp/success-pulse\n")
 | 
				
			||||||
 | 
					    machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-pulse")
 | 
				
			||||||
 | 
					    collect_state_ui("pulse_wayland")
 | 
				
			||||||
 | 
					    check_state("pa-foot", 9)
 | 
				
			||||||
 | 
					    machine.send_chars("exit\n")
 | 
				
			||||||
 | 
					    machine.wait_until_fails("pgrep foot")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Test XWayland (foot does not support X):
 | 
				
			||||||
 | 
					    swaymsg("exec x11-alacritty")
 | 
				
			||||||
 | 
					    wait_for_window("u0_a3@machine")
 | 
				
			||||||
 | 
					    machine.send_chars("clear; glinfo && touch /tmp/success-client-x11\n")
 | 
				
			||||||
 | 
					    machine.wait_for_file("/tmp/fortify.1000/tmpdir/3/success-client-x11")
 | 
				
			||||||
 | 
					    collect_state_ui("alacritty_x11")
 | 
				
			||||||
 | 
					    check_state("x11-alacritty", 2)
 | 
				
			||||||
 | 
					    machine.send_chars("exit\n")
 | 
				
			||||||
 | 
					    machine.wait_until_fails("pgrep alacritty")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Start app (foot) with direct Wayland access:
 | 
				
			||||||
 | 
					    swaymsg("exec da-foot")
 | 
				
			||||||
 | 
					    wait_for_window("u0_a4@machine")
 | 
				
			||||||
 | 
					    machine.send_chars("clear; wayland-info && touch /tmp/success-direct\n")
 | 
				
			||||||
 | 
					    machine.wait_for_file("/tmp/fortify.1000/tmpdir/4/success-direct")
 | 
				
			||||||
 | 
					    collect_state_ui("foot_direct")
 | 
				
			||||||
 | 
					    check_state("da-foot", 1)
 | 
				
			||||||
 | 
					    # Verify acl on XDG_RUNTIME_DIR:
 | 
				
			||||||
 | 
					    print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000004"))
 | 
				
			||||||
 | 
					    machine.send_chars("exit\n")
 | 
				
			||||||
 | 
					    machine.wait_until_fails("pgrep foot")
 | 
				
			||||||
 | 
					    # Verify acl cleanup on XDG_RUNTIME_DIR:
 | 
				
			||||||
 | 
					    machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000004")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Test syscall filter:
 | 
				
			||||||
 | 
					    print(machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 strace-failure"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Exit Sway and verify process exit status 0:
 | 
				
			||||||
 | 
					    swaymsg("exit", succeed=False)
 | 
				
			||||||
 | 
					    machine.wait_until_fails("pgrep -x sway")
 | 
				
			||||||
 | 
					    machine.wait_for_file("/tmp/sway-exit-ok")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Print fortify runDir contents:
 | 
				
			||||||
 | 
					    print(machine.succeed("find /run/user/1000/fortify"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Verify go test status:
 | 
				
			||||||
 | 
					    machine.wait_for_file("/tmp/gotest")
 | 
				
			||||||
 | 
					    print(machine.succeed("cat /tmp/gotest"))
 | 
				
			||||||
 | 
					    machine.wait_for_file("/tmp/success-gotest")
 | 
				
			||||||
 | 
					  '';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,163 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  lib,
 | 
					 | 
				
			||||||
  pkgs,
 | 
					 | 
				
			||||||
  config,
 | 
					 | 
				
			||||||
  ...
 | 
					 | 
				
			||||||
}:
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
  users.users = {
 | 
					 | 
				
			||||||
    alice = {
 | 
					 | 
				
			||||||
      isNormalUser = true;
 | 
					 | 
				
			||||||
      description = "Alice Foobar";
 | 
					 | 
				
			||||||
      password = "foobar";
 | 
					 | 
				
			||||||
      uid = 1000;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    untrusted = {
 | 
					 | 
				
			||||||
      isNormalUser = true;
 | 
					 | 
				
			||||||
      description = "Untrusted user";
 | 
					 | 
				
			||||||
      password = "foobar";
 | 
					 | 
				
			||||||
      uid = 1001;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      # For deny unmapped uid test:
 | 
					 | 
				
			||||||
      packages = [ config.environment.fortify.package ];
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  home-manager.users.alice.home.stateVersion = "24.11";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  # Automatically login on tty1 as a normal user:
 | 
					 | 
				
			||||||
  services.getty.autologinUser = "alice";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  environment = {
 | 
					 | 
				
			||||||
    systemPackages = with pkgs; [
 | 
					 | 
				
			||||||
      # For glinfo and wayland-info:
 | 
					 | 
				
			||||||
      mesa-demos
 | 
					 | 
				
			||||||
      wayland-utils
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      # For D-Bus tests:
 | 
					 | 
				
			||||||
      libnotify
 | 
					 | 
				
			||||||
      mako
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    variables = {
 | 
					 | 
				
			||||||
      SWAYSOCK = "/tmp/sway-ipc.sock";
 | 
					 | 
				
			||||||
      WLR_RENDERER = "pixman";
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # To help with OCR:
 | 
					 | 
				
			||||||
    etc."xdg/foot/foot.ini".text = lib.generators.toINI { } {
 | 
					 | 
				
			||||||
      main = {
 | 
					 | 
				
			||||||
        font = "inconsolata:size=14";
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
      colors = rec {
 | 
					 | 
				
			||||||
        foreground = "000000";
 | 
					 | 
				
			||||||
        background = "ffffff";
 | 
					 | 
				
			||||||
        regular2 = foreground;
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  fonts.packages = [ pkgs.inconsolata ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  # Automatically configure and start Sway when logging in on tty1:
 | 
					 | 
				
			||||||
  programs.bash.loginShellInit = ''
 | 
					 | 
				
			||||||
    if [ "$(tty)" = "/dev/tty1" ]; then
 | 
					 | 
				
			||||||
      set -e
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      mkdir -p ~/.config/sway
 | 
					 | 
				
			||||||
      (sed s/Mod4/Mod1/ /etc/sway/config &&
 | 
					 | 
				
			||||||
      echo 'output * bg ${pkgs.nixos-artwork.wallpapers.simple-light-gray.gnomeFilePath} fill' &&
 | 
					 | 
				
			||||||
      echo 'output Virtual-1 res 1680x1050') > ~/.config/sway/config
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      sway --validate
 | 
					 | 
				
			||||||
      systemd-cat --identifier=sway sway && touch /tmp/sway-exit-ok
 | 
					 | 
				
			||||||
    fi
 | 
					 | 
				
			||||||
  '';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  programs.sway.enable = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  # For PulseAudio tests:
 | 
					 | 
				
			||||||
  security.rtkit.enable = true;
 | 
					 | 
				
			||||||
  services.pipewire = {
 | 
					 | 
				
			||||||
    enable = true;
 | 
					 | 
				
			||||||
    alsa.enable = true;
 | 
					 | 
				
			||||||
    alsa.support32Bit = true;
 | 
					 | 
				
			||||||
    pulse.enable = true;
 | 
					 | 
				
			||||||
    jack.enable = true;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  virtualisation.qemu.options = [
 | 
					 | 
				
			||||||
    # Need to switch to a different GPU driver than the default one (-vga std) so that Sway can launch:
 | 
					 | 
				
			||||||
    "-vga none -device virtio-gpu-pci"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Increase Go test compiler performance:
 | 
					 | 
				
			||||||
    "-smp 8"
 | 
					 | 
				
			||||||
  ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  environment.fortify = {
 | 
					 | 
				
			||||||
    enable = true;
 | 
					 | 
				
			||||||
    stateDir = "/var/lib/fortify";
 | 
					 | 
				
			||||||
    users.alice = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    home-manager = _: _: { home.stateVersion = "23.05"; };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    apps = [
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        name = "ne-foot";
 | 
					 | 
				
			||||||
        verbose = true;
 | 
					 | 
				
			||||||
        share = pkgs.foot;
 | 
					 | 
				
			||||||
        packages = [ pkgs.foot ];
 | 
					 | 
				
			||||||
        command = "foot";
 | 
					 | 
				
			||||||
        capability = {
 | 
					 | 
				
			||||||
          dbus = false;
 | 
					 | 
				
			||||||
          pulse = false;
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        name = "pa-foot";
 | 
					 | 
				
			||||||
        verbose = true;
 | 
					 | 
				
			||||||
        share = pkgs.foot;
 | 
					 | 
				
			||||||
        packages = [ pkgs.foot ];
 | 
					 | 
				
			||||||
        command = "foot";
 | 
					 | 
				
			||||||
        capability.dbus = false;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        name = "x11-alacritty";
 | 
					 | 
				
			||||||
        verbose = true;
 | 
					 | 
				
			||||||
        share = pkgs.alacritty;
 | 
					 | 
				
			||||||
        packages = [ pkgs.alacritty ];
 | 
					 | 
				
			||||||
        command = "alacritty";
 | 
					 | 
				
			||||||
        capability = {
 | 
					 | 
				
			||||||
          wayland = false;
 | 
					 | 
				
			||||||
          x11 = true;
 | 
					 | 
				
			||||||
          dbus = false;
 | 
					 | 
				
			||||||
          pulse = false;
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        name = "da-foot";
 | 
					 | 
				
			||||||
        verbose = true;
 | 
					 | 
				
			||||||
        insecureWayland = true;
 | 
					 | 
				
			||||||
        share = pkgs.foot;
 | 
					 | 
				
			||||||
        packages = [ pkgs.foot ];
 | 
					 | 
				
			||||||
        command = "foot";
 | 
					 | 
				
			||||||
        capability = {
 | 
					 | 
				
			||||||
          dbus = false;
 | 
					 | 
				
			||||||
          pulse = false;
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        name = "strace-failure";
 | 
					 | 
				
			||||||
        verbose = true;
 | 
					 | 
				
			||||||
        share = pkgs.strace;
 | 
					 | 
				
			||||||
        command = "strace true";
 | 
					 | 
				
			||||||
        capability = {
 | 
					 | 
				
			||||||
          wayland = false;
 | 
					 | 
				
			||||||
          x11 = false;
 | 
					 | 
				
			||||||
          dbus = false;
 | 
					 | 
				
			||||||
          pulse = false;
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,51 +0,0 @@
 | 
				
			|||||||
{
 | 
					 | 
				
			||||||
  system,
 | 
					 | 
				
			||||||
  self,
 | 
					 | 
				
			||||||
  nixosTest,
 | 
					 | 
				
			||||||
  writeShellScriptBin,
 | 
					 | 
				
			||||||
}:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
nixosTest {
 | 
					 | 
				
			||||||
  name = "fortify";
 | 
					 | 
				
			||||||
  nodes.machine = {
 | 
					 | 
				
			||||||
    environment.systemPackages = [
 | 
					 | 
				
			||||||
      # For go tests:
 | 
					 | 
				
			||||||
      self.packages.${system}.fhs
 | 
					 | 
				
			||||||
      (writeShellScriptBin "fortify-src" "echo -n ${self.packages.${system}.fortify.src}")
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Run with Go race detector:
 | 
					 | 
				
			||||||
    environment.fortify.package =
 | 
					 | 
				
			||||||
      let
 | 
					 | 
				
			||||||
        inherit (self.packages.${system}) fortify;
 | 
					 | 
				
			||||||
      in
 | 
					 | 
				
			||||||
      fortify.overrideAttrs (previousAttrs: {
 | 
					 | 
				
			||||||
        GOFLAGS = previousAttrs.GOFLAGS ++ [ "-race" ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # fsu does not like cgo
 | 
					 | 
				
			||||||
        disallowedReferences = previousAttrs.disallowedReferences ++ [ fortify ];
 | 
					 | 
				
			||||||
        postInstall =
 | 
					 | 
				
			||||||
          previousAttrs.postInstall
 | 
					 | 
				
			||||||
          + ''
 | 
					 | 
				
			||||||
            cp -a "${fortify}/libexec/fsu" "$out/libexec/fsu"
 | 
					 | 
				
			||||||
            sed -i 's:${fortify}:${placeholder "out"}:' "$out/libexec/fsu"
 | 
					 | 
				
			||||||
          '';
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    imports = [
 | 
					 | 
				
			||||||
      ./configuration.nix
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      self.nixosModules.fortify
 | 
					 | 
				
			||||||
      self.inputs.home-manager.nixosModules.home-manager
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  # adapted from nixos sway integration tests
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  # testScriptWithTypes:49: error: Cannot call function of unknown type
 | 
					 | 
				
			||||||
  #           (machine.succeed if succeed else machine.execute)(
 | 
					 | 
				
			||||||
  #           ^
 | 
					 | 
				
			||||||
  # Found 1 error in 1 file (checked 1 source file)
 | 
					 | 
				
			||||||
  skipTypeCheck = true;
 | 
					 | 
				
			||||||
  testScript = builtins.readFile ./test.py;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,199 +0,0 @@
 | 
				
			|||||||
import json
 | 
					 | 
				
			||||||
import shlex
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
q = shlex.quote
 | 
					 | 
				
			||||||
NODE_GROUPS = ["nodes", "floating_nodes"]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def swaymsg(command: str = "", succeed=True, type="command"):
 | 
					 | 
				
			||||||
    assert command != "" or type != "command", "Must specify command or type"
 | 
					 | 
				
			||||||
    shell = q(f"swaymsg -t {q(type)} -- {q(command)}")
 | 
					 | 
				
			||||||
    with machine.nested(
 | 
					 | 
				
			||||||
            f"sending swaymsg {shell!r}" + " (allowed to fail)" * (not succeed)
 | 
					 | 
				
			||||||
    ):
 | 
					 | 
				
			||||||
        ret = (machine.succeed if succeed else machine.execute)(
 | 
					 | 
				
			||||||
            f"su - alice -c {shell}"
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # execute also returns a status code, but disregard.
 | 
					 | 
				
			||||||
    if not succeed:
 | 
					 | 
				
			||||||
        _, ret = ret
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if not succeed and not ret:
 | 
					 | 
				
			||||||
        return None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    parsed = json.loads(ret)
 | 
					 | 
				
			||||||
    return parsed
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def walk(tree):
 | 
					 | 
				
			||||||
    yield tree
 | 
					 | 
				
			||||||
    for group in NODE_GROUPS:
 | 
					 | 
				
			||||||
        for node in tree.get(group, []):
 | 
					 | 
				
			||||||
            yield from walk(node)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def wait_for_window(pattern):
 | 
					 | 
				
			||||||
    def func(last_chance):
 | 
					 | 
				
			||||||
        nodes = (node["name"] for node in walk(swaymsg(type="get_tree")))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if last_chance:
 | 
					 | 
				
			||||||
            nodes = list(nodes)
 | 
					 | 
				
			||||||
            machine.log(f"Last call! Current list of windows: {nodes}")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return any(pattern in name for name in nodes)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    retry(func)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def collect_state_ui(name):
 | 
					 | 
				
			||||||
    swaymsg(f"exec fortify ps > '/tmp/{name}.ps'")
 | 
					 | 
				
			||||||
    machine.copy_from_vm(f"/tmp/{name}.ps", "")
 | 
					 | 
				
			||||||
    swaymsg(f"exec fortify --json ps > '/tmp/{name}.json'")
 | 
					 | 
				
			||||||
    machine.copy_from_vm(f"/tmp/{name}.json", "")
 | 
					 | 
				
			||||||
    machine.screenshot(name)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def check_state(name, enablements):
 | 
					 | 
				
			||||||
    instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 fortify --json ps"))
 | 
					 | 
				
			||||||
    if len(instances) != 1:
 | 
					 | 
				
			||||||
        raise Exception(f"unexpected state length {len(instances)}")
 | 
					 | 
				
			||||||
    instance = next(iter(instances.values()))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    config = instance['config']
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if len(config['command']) != 1 or not (config['command'][0].startswith("/nix/store/")) or not (
 | 
					 | 
				
			||||||
            config['command'][0].endswith(f"{name}-start")):
 | 
					 | 
				
			||||||
        raise Exception(f"unexpected command {instance['config']['command']}")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if config['confinement']['enablements'] != enablements:
 | 
					 | 
				
			||||||
        raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def fortify(command):
 | 
					 | 
				
			||||||
    swaymsg(f"exec fortify {command}")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
start_all()
 | 
					 | 
				
			||||||
machine.wait_for_unit("multi-user.target")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Run fortify Go tests outside of nix build in the background:
 | 
					 | 
				
			||||||
machine.succeed("rm -rf /tmp/src && cp -a \"$(fortify-src)\" /tmp/src")
 | 
					 | 
				
			||||||
machine.succeed(
 | 
					 | 
				
			||||||
    "fortify-fhs -c '(cd /tmp/src && go generate ./... && go test ./... && touch /tmp/success-gotest)' &> /tmp/gotest &")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# To check fortify's version:
 | 
					 | 
				
			||||||
print(machine.succeed("sudo -u alice -i fortify version"))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Wait for Sway to complete startup:
 | 
					 | 
				
			||||||
machine.wait_for_file("/run/user/1000/wayland-1")
 | 
					 | 
				
			||||||
machine.wait_for_file("/tmp/sway-ipc.sock")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Deny unmapped uid:
 | 
					 | 
				
			||||||
denyOutput = machine.fail("sudo -u untrusted -i fortify run &>/dev/stdout")
 | 
					 | 
				
			||||||
print(denyOutput)
 | 
					 | 
				
			||||||
denyOutputVerbose = machine.fail("sudo -u untrusted -i fortify -v run &>/dev/stdout")
 | 
					 | 
				
			||||||
print(denyOutputVerbose)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Verify PrintBaseError behaviour:
 | 
					 | 
				
			||||||
if denyOutput != "fsu: uid 1001 is not in the fsurc file\n":
 | 
					 | 
				
			||||||
    raise Exception(f"unexpected deny output:\n{denyOutput}")
 | 
					 | 
				
			||||||
if denyOutputVerbose != "fsu: uid 1001 is not in the fsurc file\nfortify: *cannot obtain uid from fsu: permission denied\n":
 | 
					 | 
				
			||||||
    raise Exception(f"unexpected deny verbose output:\n{denyOutputVerbose}")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Start fortify permissive defaults outside Wayland session:
 | 
					 | 
				
			||||||
print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare"))
 | 
					 | 
				
			||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-bare")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Verify silent output permissive defaults:
 | 
					 | 
				
			||||||
output = machine.succeed("sudo -u alice -i fortify run -a 0 true &>/dev/stdout")
 | 
					 | 
				
			||||||
if output != "":
 | 
					 | 
				
			||||||
    raise Exception(f"unexpected output\n{output}")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Verify graceful failure on bad Wayland display name:
 | 
					 | 
				
			||||||
print(machine.fail("sudo -u alice -i fortify -v run --wayland true"))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Start fortify permissive defaults within Wayland session:
 | 
					 | 
				
			||||||
fortify(
 | 
					 | 
				
			||||||
    '-v run --wayland --dbus notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-done')
 | 
					 | 
				
			||||||
machine.wait_for_file("/tmp/dbus-done")
 | 
					 | 
				
			||||||
collect_state_ui("dbus_notify_exited")
 | 
					 | 
				
			||||||
machine.succeed("pkill -9 mako")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Start app (foot) with Wayland enablement:
 | 
					 | 
				
			||||||
swaymsg("exec ne-foot")
 | 
					 | 
				
			||||||
wait_for_window("u0_a1@machine")
 | 
					 | 
				
			||||||
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
 | 
					 | 
				
			||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/success-client")
 | 
					 | 
				
			||||||
collect_state_ui("foot_wayland")
 | 
					 | 
				
			||||||
check_state("ne-foot", 1)
 | 
					 | 
				
			||||||
# Verify acl on XDG_RUNTIME_DIR:
 | 
					 | 
				
			||||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000001"))
 | 
					 | 
				
			||||||
machine.send_chars("exit\n")
 | 
					 | 
				
			||||||
machine.wait_until_fails("pgrep foot")
 | 
					 | 
				
			||||||
# Verify acl cleanup on XDG_RUNTIME_DIR:
 | 
					 | 
				
			||||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000001")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Start app (foot) with Wayland enablement from a terminal:
 | 
					 | 
				
			||||||
swaymsg(
 | 
					 | 
				
			||||||
    "exec foot $SHELL -c '(ne-foot) & sleep 1 && fortify show $(fortify ps --short) && touch /tmp/ps-show-ok && cat'")
 | 
					 | 
				
			||||||
wait_for_window("u0_a1@machine")
 | 
					 | 
				
			||||||
machine.send_chars("clear; wayland-info && touch /tmp/success-client-term\n")
 | 
					 | 
				
			||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/success-client-term")
 | 
					 | 
				
			||||||
machine.wait_for_file("/tmp/ps-show-ok")
 | 
					 | 
				
			||||||
collect_state_ui("foot_wayland_term")
 | 
					 | 
				
			||||||
check_state("ne-foot", 1)
 | 
					 | 
				
			||||||
machine.send_chars("exit\n")
 | 
					 | 
				
			||||||
wait_for_window("foot")
 | 
					 | 
				
			||||||
machine.send_key("ctrl-c")
 | 
					 | 
				
			||||||
machine.wait_until_fails("pgrep foot")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Test PulseAudio (fortify does not support PipeWire yet):
 | 
					 | 
				
			||||||
swaymsg("exec pa-foot")
 | 
					 | 
				
			||||||
wait_for_window("u0_a2@machine")
 | 
					 | 
				
			||||||
machine.send_chars("clear; pactl info && touch /tmp/success-pulse\n")
 | 
					 | 
				
			||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-pulse")
 | 
					 | 
				
			||||||
collect_state_ui("pulse_wayland")
 | 
					 | 
				
			||||||
check_state("pa-foot", 9)
 | 
					 | 
				
			||||||
machine.send_chars("exit\n")
 | 
					 | 
				
			||||||
machine.wait_until_fails("pgrep foot")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Test XWayland (foot does not support X):
 | 
					 | 
				
			||||||
swaymsg("exec x11-alacritty")
 | 
					 | 
				
			||||||
wait_for_window("u0_a3@machine")
 | 
					 | 
				
			||||||
machine.send_chars("clear; glinfo && touch /tmp/success-client-x11\n")
 | 
					 | 
				
			||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/3/success-client-x11")
 | 
					 | 
				
			||||||
collect_state_ui("alacritty_x11")
 | 
					 | 
				
			||||||
check_state("x11-alacritty", 2)
 | 
					 | 
				
			||||||
machine.send_chars("exit\n")
 | 
					 | 
				
			||||||
machine.wait_until_fails("pgrep alacritty")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Start app (foot) with direct Wayland access:
 | 
					 | 
				
			||||||
swaymsg("exec da-foot")
 | 
					 | 
				
			||||||
wait_for_window("u0_a4@machine")
 | 
					 | 
				
			||||||
machine.send_chars("clear; wayland-info && touch /tmp/success-direct\n")
 | 
					 | 
				
			||||||
machine.wait_for_file("/tmp/fortify.1000/tmpdir/4/success-direct")
 | 
					 | 
				
			||||||
collect_state_ui("foot_direct")
 | 
					 | 
				
			||||||
check_state("da-foot", 1)
 | 
					 | 
				
			||||||
# Verify acl on XDG_RUNTIME_DIR:
 | 
					 | 
				
			||||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000004"))
 | 
					 | 
				
			||||||
machine.send_chars("exit\n")
 | 
					 | 
				
			||||||
machine.wait_until_fails("pgrep foot")
 | 
					 | 
				
			||||||
# Verify acl cleanup on XDG_RUNTIME_DIR:
 | 
					 | 
				
			||||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000004")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Test syscall filter:
 | 
					 | 
				
			||||||
print(machine.fail("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 strace-failure"))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Exit Sway and verify process exit status 0:
 | 
					 | 
				
			||||||
swaymsg("exit", succeed=False)
 | 
					 | 
				
			||||||
machine.wait_until_fails("pgrep -x sway")
 | 
					 | 
				
			||||||
machine.wait_for_file("/tmp/sway-exit-ok")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Print fortify runDir contents:
 | 
					 | 
				
			||||||
print(machine.succeed("find /run/user/1000/fortify"))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Verify go test status:
 | 
					 | 
				
			||||||
machine.wait_for_file("/tmp/gotest")
 | 
					 | 
				
			||||||
print(machine.succeed("cat /tmp/gotest"))
 | 
					 | 
				
			||||||
machine.wait_for_file("/tmp/success-gotest")
 | 
					 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user