Skip to content

Commit

Permalink
Remove Perms section from, AppImage, slight refactor
Browse files Browse the repository at this point in the history
 * Remove `Run` method in favor of running the AppImage directly,
   outside of aisap
 * Remove `AppImage.Perms` from AppImage
 * Removed old `profilegen` app
mgord9518 committed Jul 1, 2024
1 parent a248850 commit 490ab39
Showing 17 changed files with 517 additions and 446 deletions.
167 changes: 103 additions & 64 deletions appimage.go
Original file line number Diff line number Diff line change
@@ -8,43 +8,42 @@ import (
"bufio"
"crypto/md5"
"debug/elf"
"fmt"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"os"
"strings"

ini "gopkg.in/ini.v1"
helpers "github.com/mgord9518/aisap/helpers"
profiles "github.com/mgord9518/aisap/profiles"
squashfs "github.com/CalebQ42/squashfs"
xdg "github.com/adrg/xdg"
helpers "github.com/mgord9518/aisap/helpers"
permissions "github.com/mgord9518/aisap/permissions"
squashfs "github.com/CalebQ42/squashfs"
xdg "github.com/adrg/xdg"
profiles "github.com/mgord9518/aisap/profiles"
ini "gopkg.in/ini.v1"
)

type AppImage struct {
Desktop *ini.File // INI of internal desktop entry
Perms *permissions.AppImagePerms // Permissions
Path string // Location of AppImage
dataDir string // The AppImage's `HOME` directory
rootDir string // Can be used to give the AppImage fake system files
tempDir string // The AppImage's `/tmp` directory
mountDir string // The location the AppImage is mounted at
md5 string // MD5 of AppImage's URI
Name string // AppImage name from the desktop entry
Version string
UpdateInfo string
Offset int // Offset of SquashFS image
imageType int // Type of AppImage (1=ISO 9660 ELF, 2=squashfs ELF, -2=shImg shell)
Desktop *ini.File // INI of internal desktop entry
Path string // Location of AppImage
dataDir string // The AppImage's `HOME` directory
rootDir string // Can be used to give the AppImage fake system files
tempDir string // The AppImage's `/tmp` directory
mountDir string // The location the AppImage is mounted at
md5 string // MD5 of AppImage's URI
Name string // AppImage name from the desktop entry
Version string
UpdateInfo string
Offset int // Offset of SquashFS image
imageType int // Type of AppImage (1=ISO 9660 ELF, 2=squashfs ELF, -2=shImg shell)
architecture []string // List of CPU architectures supported by the bundle
reader *squashfs.Reader
file *os.File
reader *squashfs.Reader
file *os.File

// These will both be removed when the Zig-implemented C bindings
// become usable
CurrentArg int // Should only ever be used for the C bindings
CurrentArg int // Should only ever be used for the C bindings
WrapArgsList []string // Should only ever be used for the C bindings
}

@@ -69,35 +68,47 @@ func NewAppImage(src string) (*AppImage, error) {
ai.md5 = fmt.Sprintf("%x", b)

ai.imageType, err = helpers.GetAppImageType(ai.Path)
if err != nil { return nil, err }
if err != nil {
return nil, err
}

ai.rootDir = "/"
ai.dataDir = ai.Path + ".home"
ai.Offset, err = helpers.GetOffset(src)
if err != nil { return nil, err }
if err != nil {
return nil, err
}

if ai.imageType == -2 || ai.imageType == 2 {
ai.file, err = os.Open(ai.Path)
if err != nil { return nil, err }
if err != nil {
return nil, err
}

info, _ := ai.file.Stat()
off64 := int64(ai.Offset)
r := io.NewSectionReader(ai.file, off64, info.Size()-off64)

ai.reader, err = squashfs.NewReader(r)
if err != nil { return nil, err }
if err != nil {
return nil, err
}
}

// Prefer local entry if it exists (located at $XDG_DATA_HOME/aisap/[ai.Name])
desktopReader, err := ai.getEntry()
if err != nil { return ai, err }
if err != nil {
return ai, err
}

ai.Desktop, err = ini.LoadSources(ini.LoadOptions{
IgnoreInlineComment: true,
}, desktopReader)
if err != nil { return ai, err }
if err != nil {
return ai, err
}

ai.Name = ai.Desktop.Section("Desktop Entry").Key("Name").Value()
ai.Name = ai.Desktop.Section("Desktop Entry").Key("Name").Value()
ai.Version = ai.Desktop.Section("Desktop Entry").Key("X-AppImage-Version").Value()

ai.UpdateInfo, _ = helpers.ReadUpdateInfo(ai.Path)
@@ -106,6 +117,17 @@ func NewAppImage(src string) (*AppImage, error) {
ai.Version = "1.0"
}

return ai, nil
}

// Retrieve permissions from the AppImage in the following order:
//
// 1: User-configured settings in ~/.local/share/aisap/profiles/[ai.Name]
// 2: aisap internal permissions library
// 3: Permissions defined in the AppImage's desktop file
func (ai AppImage) Permissions() (*permissions.AppImagePerms, error) {
var perms *permissions.AppImagePerms
var err error

// If PREFER_AISAP_PROFILE is set, attempt to use it over the AppImage's
// suggested permissions. If no profile exists in aisap, fall back on saved
@@ -114,23 +136,25 @@ func NewAppImage(src string) (*AppImage, error) {
// Typically this should be unset unless testing a custom profile against
// aisap's
if _, present := os.LookupEnv("PREFER_AISAP_PROFILE"); present {
ai.Perms, err = profiles.FromName(ai.Name)
perms, err = profiles.FromName(ai.Name)

if err != nil {
ai.Perms, err = permissions.FromSystem(ai.Name)
perms, err = permissions.FromSystem(ai.Name)
}
} else {
ai.Perms, err = permissions.FromSystem(ai.Name)
perms, err = permissions.FromSystem(ai.Name)

if err != nil {
ai.Perms, err = profiles.FromName(ai.Name)
perms, err = profiles.FromName(ai.Name)
}
}

// Fall back to permissions inside AppImage if all else fails
if err != nil {
ai.Perms, _ = permissions.FromIni(ai.Desktop)
return permissions.FromIni(ai.Desktop)
}

return ai, nil
return perms, nil
}

// Returns `true` if the AppImage in question is both executable and has
@@ -149,7 +173,7 @@ func (ai *AppImage) Trusted() bool {
return false
}

return info.Mode() & 0100 != 0
return info.Mode()&0100 != 0
}

return false
@@ -169,7 +193,7 @@ func (ai *AppImage) SetTrusted(trusted bool) error {
return err
}

os.Chmod(ai.Path, info.Mode() | 0100)
os.Chmod(ai.Path, info.Mode()|0100)

if helpers.FileExists(filePath) {
return errors.New("entry already exists in aisap config dir")
@@ -190,7 +214,9 @@ func (ai *AppImage) Thumbnail() (io.Reader, error) {
// Try to extract from zip, continue to SquashFS if it fails
if ai.imageType == -2 {
r, err := helpers.ExtractResourceReader(ai.Path, "icon/256.png")
if err == nil { return r, nil }
if err == nil {
return r, nil
}
}

return ai.ExtractFileReader(".DirIcon")
@@ -252,26 +278,34 @@ func (ai *AppImage) ExtractFile(path string, dest string, resolveSymlinks bool)

// True if file is symlink and `resolveSymlinks` is false
if info != nil && !resolveSymlinks &&
info.Mode()&os.ModeSymlink == os.ModeSymlink {
info.Mode()&os.ModeSymlink == os.ModeSymlink {
target, _ := os.Readlink(path)
err = os.Symlink(target, dest)
} else {
inF, err := ai.ExtractFileReader(path)
defer inF.Close()
if err != nil { return err }
if err != nil {
return err
}

info, err := os.Stat(path)
perms := info.Mode().Perm()

outF, err := os.Create(dest)
defer outF.Close()
if err != nil { return err }
if err != nil {
return err
}

err = os.Chmod(dest, perms)
if err != nil { return err }
if err != nil {
return err
}

_, err = io.Copy(outF, inF)
if err != nil { return err }
if err != nil {
return err
}
}

return err
@@ -297,10 +331,14 @@ func (ai *AppImage) ExtractFileReader(path string) (io.ReadCloser, error) {
func (ai *AppImage) Icon() (io.ReadCloser, string, error) {
if ai.imageType == -2 {
r, err := helpers.ExtractResourceReader(ai.Path, "icon/default.svg")
if err == nil { return r, "icon/default.svg", nil }
if err == nil {
return r, "icon/default.svg", nil
}

r, err = helpers.ExtractResourceReader(ai.Path, "icon/default.png")
if err == nil { return r, "icon/default.png", nil }
r, err = helpers.ExtractResourceReader(ai.Path, "icon/default.png")
if err == nil {
return r, "icon/default.png", nil
}
}

if ai.Desktop == nil {
@@ -325,7 +363,7 @@ func (ai *AppImage) Icon() (io.ReadCloser, string, error) {
".svg",
}

for _, ext := range(extensions) {
for _, ext := range extensions {
r, err := ai.ExtractFileReader(iconf + ext)

if err == nil {
@@ -338,7 +376,7 @@ func (ai *AppImage) Icon() (io.ReadCloser, string, error) {

// Extract the desktop file from the AppImage
func (ai *AppImage) getEntry() (io.Reader, error) {
var r io.Reader
var r io.Reader
var err error

if ai.imageType == -2 {
@@ -360,13 +398,12 @@ func (ai *AppImage) getEntry() (io.Reader, error) {
r := entry.(*squashfs.File)

if r.IsSymlink() {
r = r .GetSymlinkFile()
r = r.GetSymlinkFile()
}

return r, err
}


return r, err
}

@@ -382,32 +419,34 @@ func (ai *AppImage) getArchitectures() ([]string, error) {
// If undefined in the desktop entry, assume arch via ELF AppImage runtime
if ai.Type() >= 0 {
e, err := elf.NewFile(ai.file)
if err != nil {return s, err}
if err != nil {
return s, err
}

switch e.Machine {
case elf.EM_386:
return []string{"i386"}, nil
case elf.EM_X86_64:
return []string{"x86_64"}, nil
case elf.EM_ARM:
return []string{"armhf"}, nil
case elf.EM_AARCH64:
return []string{"aarch64"}, nil
case elf.EM_386:
return []string{"i386"}, nil
case elf.EM_X86_64:
return []string{"x86_64"}, nil
case elf.EM_ARM:
return []string{"armhf"}, nil
case elf.EM_AARCH64:
return []string{"aarch64"}, nil
}
}

// Assume arch via shImg runtime
if ai.Type() < -1 {
scanner := bufio.NewScanner(ai.file)
scanner := bufio.NewScanner(ai.file)
arches := []string{}

counter := 0
for scanner.Scan() {
counter++
if strings.HasPrefix(scanner.Text(), "arch='") {
str := scanner.Text()
str = strings.ReplaceAll(str, "arch='", "")
str = strings.ReplaceAll(str, "'", "")
str := scanner.Text()
str = strings.ReplaceAll(str, "arch='", "")
str = strings.ReplaceAll(str, "'", "")
arches = helpers.SplitKey(str)
return arches, nil
}
52 changes: 26 additions & 26 deletions cmd/aisap/flags.go
Original file line number Diff line number Diff line change
@@ -4,33 +4,33 @@ import (
"fmt"
"os"

flag "github.com/spf13/pflag"
clr "github.com/gookit/color"
clr "github.com/gookit/color"
aisap "github.com/mgord9518/aisap"
flag "github.com/spf13/pflag"
)

type arrayFlags []string

var (
// Normal flags
help = flag.BoolP("help", "h", false, "display this help menu")
help = flag.BoolP("help", "h", false, "display this help menu")
listPerms = flag.BoolP("list-perms", "l", false, "print all permissions to be granted to the app")
verbose = flag.BoolP("verbose", "v", false, "make output more verbose")
verbose = flag.BoolP("verbose", "v", false, "make output more verbose")

// Long-only flags
color = flag.Bool ("color", true, "whether to show color (default true)")
example = flag.Bool ("example", false, "print out examples")
level = flag.Int ("level", -1, "change the permissions level")
rootDir = flag.String("root-dir", "", "use a different filesystem root for system files")
dataDir = flag.String("data-dir", "", "change the AppImage's sandbox home location")
noDataDir = flag.Bool ("no-data-dir", false, "force AppImage's HOME to be a tmpfs (default false)")
extractIcon = flag.String("extract-icon", "", "extract the AppImage's icon")
extractThumbnail = flag.String("extract-thumbnail", "", "extract the AppImage's thumbnail preview")
profile = flag.String("profile", "", "use a profile from a desktop entry")
fallbackProfile = flag.String("fallback-profile", "", "set profile to fallback on if one isn't found")
version = flag.Bool ("version", false, "show the version and quit")
trustOnce = flag.Bool ("trust-once", false, "trust the AppImage for one run")
trust = flag.Bool ("trust", false, "set whether the AppImage is trusted or not")
color = flag.Bool("color", true, "whether to show color (default true)")
example = flag.Bool("example", false, "print out examples")
level = flag.Int("level", -1, "change the permissions level")
rootDir = flag.String("root-dir", "", "use a different filesystem root for system files")
dataDir = flag.String("data-dir", "", "change the AppImage's sandbox home location")
noDataDir = flag.Bool("no-data-dir", false, "force AppImage's HOME to be a tmpfs (default false)")
extractIcon = flag.String("extract-icon", "", "extract the AppImage's icon")
extractThumbnail = flag.String("extract-thumbnail", "", "extract the AppImage's thumbnail preview")
profile = flag.String("profile", "", "use a profile from a desktop entry")
fallbackProfile = flag.String("fallback-profile", "", "set profile to fallback on if one isn't found")
version = flag.Bool("version", false, "show the version and quit")
trustOnce = flag.Bool("trust-once", false, "trust the AppImage for one run")
trust = flag.Bool("trust", false, "set whether the AppImage is trusted or not")

addFile arrayFlags
addDevice arrayFlags
@@ -47,12 +47,12 @@ func init() {
var present bool
handleCtrlC()

flag.Var(&addFile, "add-file", "give the sandbox access to a filesystem object")
flag.Var(&addFile, "add-file", "give the sandbox access to a filesystem object")
flag.Var(&addDevice, "add-device", "add a device to the sandbox (eg dri)")
flag.Var(&addSocket, "add-socket", "allow the sandbox to access another socket (eg x11)")
flag.Var(&rmFile, "rm-file", "revoke a file from the sandbox")
flag.Var(&rmDevice, "rm-device", "remove access to a device")
flag.Var(&rmSocket, "rm-socket", "disable a socket")
flag.Var(&rmFile, "rm-file", "revoke a file from the sandbox")
flag.Var(&rmDevice, "rm-device", "remove access to a device")
flag.Var(&rmSocket, "rm-socket", "disable a socket")

// Prefer AppImage-provided variable `ARGV0` if present
if argv0, present = os.LookupEnv("ARGV0"); !present {
@@ -160,10 +160,10 @@ func (i *arrayFlags) Type() string {

func flagUsed(name string) bool {
found := false
flag.Visit(func(f *flag.Flag) {
if f.Name == name {
found = true
}
})
flag.Visit(func(f *flag.Flag) {
if f.Name == name {
found = true
}
})
return found
}
90 changes: 46 additions & 44 deletions cmd/aisap/main.go
Original file line number Diff line number Diff line change
@@ -32,17 +32,17 @@ import (
"os/signal"
"syscall"

aisap "github.com/mgord9518/aisap"
clr "github.com/gookit/color"
aisap "github.com/mgord9518/aisap"
permissions "github.com/mgord9518/aisap/permissions"
cli "github.com/mgord9518/cli"
check "github.com/mgord9518/aisap/spooky"
flag "github.com/spf13/pflag"
ini "gopkg.in/ini.v1"
clr "github.com/gookit/color"
check "github.com/mgord9518/aisap/spooky"
cli "github.com/mgord9518/cli"
flag "github.com/spf13/pflag"
ini "gopkg.in/ini.v1"
)

var (
ai *aisap.AppImage
ai *aisap.AppImage
argv0 string

invalidBundle = errors.New("failed to open bundle:")
@@ -51,8 +51,8 @@ var (
invalidPerms = errors.New("failed to get permissions from profile:")
invalidPermLevel = errors.New("failed to set permissions level (this shouldn't happen!):")
invalidFallbackProfile = errors.New("failed to set fallback profile:")
invalidSocketSet = errors.New("failed to set socket:")
cantRun = errors.New("failed to run application:")
invalidSocketSet = errors.New("failed to set socket:")
cantRun = errors.New("failed to run application:")
)

// Process flags
@@ -69,6 +69,12 @@ func main() {
return
}

perms, err := ai.Permissions()
if err != nil {
cli.Fatal(invalidPerms, err)
return
}

if *extractIcon != "" {
if *verbose {
cli.Notify("extracting icon to", *extractIcon)
@@ -127,41 +133,38 @@ func main() {
return
}

ai.Perms, err = permissions.FromReader(f)
perms, err = permissions.FromReader(f)
if err != nil {
cli.Fatal(invalidPerms, err)
return
}

if err != nil {
cli.Fatal(invalidPerms, err)
return
}
}

// Add (and remove) permissions as passed from flags. eg: `--add-file`
// Note: If *not* using XDG standard names (eg: `xdg-desktop`) you MUST
// Provide the full filepath when using `AddFiles`
ai.Perms.RemoveFiles(rmFile...)
ai.Perms.RemoveDevices(rmDevice...)
ai.Perms.RemoveSockets(rmSocket...)
ai.Perms.AddFiles(addFile...)
ai.Perms.AddDevices(addDevice...)
err = ai.Perms.AddSockets(addSocket...)

// Fail if socket is invalid
if err != nil {
perms.RemoveFiles(rmFile...)
perms.RemoveDevices(rmDevice...)
perms.RemoveSockets(rmSocket...)
perms.AddFiles(addFile...)
perms.AddDevices(addDevice...)
err = perms.AddSockets(addSocket...)

// Fail if socket is invalid
if err != nil {
// TODO: re-add socket list
// clr.Println("<yellow>notice</>:")
// cli.List("valid sockets are", permissions.ValidSockets(), 18)
// fmt.Println()
cli.Fatal(invalidSocketSet, err)
return
}

cli.Fatal(invalidSocketSet, err)
return
}

// If the `--level` flag is used, set the AppImage to that level
if *level > -1 && *level <= 3 {
err := ai.Perms.SetLevel(*level)
err := perms.SetLevel(*level)
if err != nil {
cli.Fatal(invalidPermLevel, err)
return
@@ -171,7 +174,7 @@ func main() {
noProfile := false

// Fallback on `--fallback-profile` if set, otherwise just set base level to 3
if ai.Perms.Level < 0 || ai.Perms.Level > 3 {
if perms.Level < 0 || perms.Level > 3 {
if *fallbackProfile != "" {
f, err := ini.LoadSources(ini.LoadOptions{
IgnoreInlineComment: true,
@@ -182,9 +185,9 @@ func main() {
return
}

ai.Perms, err = permissions.FromIni(f)
perms, err = permissions.FromIni(f)
} else {
ai.Perms.Level = 3
perms.Level = 3
}

noProfile = true
@@ -211,11 +214,11 @@ func main() {
fmt.Println()
}

cli.ListPerms(ai.Perms)
cli.ListPerms(perms)

fmt.Println()

if ai.Perms.Level == 0 {
if perms.Level == 0 {
cli.Warning("this app requests to be unsandboxed!")
cli.Warning("use the CLI flag <cyan>--level</> <gray>[</><green>1</><gray>..</><green>3</><gray>]</> to sandbox it anyway")
return
@@ -226,7 +229,7 @@ func main() {
}

// Warns if the AppImage contains potential escape vectors or suspicious files
for _, v := range(ai.Perms.Files) {
for _, v := range perms.Files {
if check.IsSpooky(v) {

cli.Warning("this app requests files/ directories that could be used to escape sandboxing")
@@ -239,7 +242,7 @@ func main() {
"x11",
}

for _, sock := range ai.Perms.Sockets {
for _, sock := range perms.Sockets {
for _, spookySock := range spookySockets {
if sock == spookySock {
cli.Warning("sockets used by this app could be used to escape the sandbox")
@@ -251,10 +254,10 @@ func main() {
}

err = ai.Mount()
if err != nil {
cli.Fatal(err, err)
return
}
if err != nil {
cli.Fatal(err, err)
return
}

if *rootDir != "" {
ai.SetRootDir(*rootDir)
@@ -265,7 +268,7 @@ func main() {
}

if *noDataDir {
ai.Perms.DataDir = false
perms.DataDir = false
}

if flagUsed("trust") {
@@ -278,15 +281,14 @@ func main() {
}

if *verbose {
wrapArg, _ := ai.WrapArgs([]string{})
cli.Notify("running with sandbox base level", ai.Perms.Level)
wrapArg, _ := ai.WrapArgs(perms, []string{})
cli.Notify("running with sandbox base level", perms.Level)
cli.Notify("bwrap flags:", wrapArg)
}

err = ai.Run(flag.Args()[1:])

err = ai.Sandbox(perms, flag.Args()[1:])
if err != nil {
fmt.Fprintln(os.Stdout, "exited non-zero status:", err)
fmt.Fprintln(os.Stdout, "sandbox error:", err)
return
}
}
3 changes: 0 additions & 3 deletions cmd/profilegen/go.mod

This file was deleted.

29 changes: 0 additions & 29 deletions cmd/profilegen/main.go

This file was deleted.

15 changes: 13 additions & 2 deletions cmd/tablegen/main.go
Original file line number Diff line number Diff line change
@@ -5,18 +5,29 @@ package main

import (
"fmt"
"sort"

profiles "github.com/mgord9518/aisap/profiles"
)

// Process flags
func main() {
profileKeys := make([]string, 0, len(profiles.Profiles()))

for key, _ := range profiles.Profiles() {
profileKeys = append(profileKeys, key)
}

sort.Strings(profileKeys)

// Table header
fmt.Printf("## Current supported applications (%d)\n", len(profiles.RawProfiles))
fmt.Printf("## Current supported applications (%d)\n", len(profileKeys))
fmt.Println("|name|level|devices|sockets|filesystem|")
fmt.Println("|-|-|-|-|-|")

for _, profile := range profiles.RawProfiles {
for i := range profileKeys {
profile := profiles.Profiles()[profileKeys[i]]

// Name
fmt.Printf("|%s", profile.Names[0])

16 changes: 6 additions & 10 deletions docs/aisap-go.3.md
Original file line number Diff line number Diff line change
@@ -31,13 +31,9 @@ Destroy() error
// directory in '$XDG_RUNTIME_DIR/aisap/mount'
Mount(dest ...string) error by

// If AppImage.Perms.Level > 0, the AppImage will be sandboxed with
// AppImage.Perms, if 0, it will run unsandboxed. args are passed directly to
// the AppImage
Run(args []string) error

// Identical to AppImage.Run, except AppImage.Perms.Level equaling 0 is an
// error condition
// Executes AppImage through bwrap and creates a portable home if one doesn't
// already exist
// Returns error if AppImagePerms.Level < 1
Sandbox(args []string) error

// Generates bwrap arguments for sandboxing based on the AppImage's permissions
@@ -59,15 +55,15 @@ Thumbnail() (io.Reader, error)
// Returns the type of the AppImage, currently only supports type 2 and shImg
// (-2). PRs for supporting type 1 and possibly other (well-defined) unofficial
// AppImage implementations are welcome
Type()
Type() int

// Returns the TempDir of the AppImage, which only exists if the AppImage is
// mounted, otherwise returns an empty string
TempDir()
TempDir() string

// Returns the directory the AppImage is mounted to. If not mounted, it will
// return an empty string
MountDir()
MountDir() string

// Change the root directory exposed to the sandbox. This could be useful for
// running an AppImage designed for another Linux distro
60 changes: 39 additions & 21 deletions helpers/helper.go
Original file line number Diff line number Diff line change
@@ -5,13 +5,13 @@ import (
"bufio"
"errors"
"io"
"path/filepath"
"math/rand"
"math/rand"
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"strings"
"os"
"os/exec"
"strings"

xdg "github.com/adrg/xdg"
)
@@ -26,18 +26,22 @@ func SplitKey(str string) []string {
}

func Contains(s []string, str string) (int, bool) {
for i, val := range(s) {
if val == str { return i, true }
for i, val := range s {
if val == str {
return i, true
}
}

return -1, false
}

// Checks if an array contains any of the elements from another array
func ContainsAny(s []string, s2 []string) (int, bool) {
for i := range(s2) {
for i := range s2 {
n, present := Contains(s, s2[i])
if present { return n, true }
if present {
return n, true
}
}

return -1, false
@@ -102,7 +106,7 @@ func CleanFile(str string) string {
}

func CleanFiles(s []string) []string {
for i := range(s) {
for i := range s {
s[i] = CleanFile(s[i])
}

@@ -118,7 +122,7 @@ func CleanDevice(str string) string {
}

func CleanDevices(s []string) []string {
for i := range(s) {
for i := range s {
s[i] = CleanDevice(s[i])
}

@@ -226,24 +230,32 @@ func ExpandGenericDir(str string) string {
func ExtractResource(aiPath string, src string, dest string) error {
inF, err := ExtractResourceReader(aiPath, src)
defer inF.Close()
if err != nil { return err }
if err != nil {
return err
}

outF, err := os.Create(dest)
defer outF.Close()
if err != nil { return err }
if err != nil {
return err
}

_, err = io.Copy(outF, inF)
_, err = io.Copy(outF, inF)
return err
}

func ExtractResourceReader(aiPath string, src string) (io.ReadCloser, error) {
zr, err := zip.OpenReader(aiPath)
if err != nil { return nil, err }
if err != nil {
return nil, err
}

for _, f := range(zr.File) {
for _, f := range zr.File {
if f.Name == filepath.Join(".APPIMAGE_RESOURCES", src) {
rc, err := f.Open()
if err != nil { return nil, err }
if err != nil {
return nil, err
}

return rc, nil
}
@@ -253,7 +265,7 @@ func ExtractResourceReader(aiPath string, src string) (io.ReadCloser, error) {
}

// Get the home directory using `/etc/passwd`, discarding the $HOME variable.
// This is used in aisap so that its config files can be stored in
// This is used in aisap so that its config files can be stored in
func RealHome() (string, error) {
uid := strconv.Itoa(os.Getuid())

@@ -277,20 +289,26 @@ func RealHome() (string, error) {
// Finds full path of running executable
func GetWorkDir() (string, error) {
e, err := os.Executable()
if err != nil { return "", err }
if err != nil {
return "", err
}

return path.Dir(e), nil
}

// Returns full path of command and true if in PATH or working directory
func CommandExists(str string) (string, bool) {
wd, err := GetWorkDir()
if err != nil { return "", false }
if err != nil {
return "", false
}

cmd, err := exec.LookPath(filepath.Join(wd, str))
if err != nil {
cmd, err = exec.LookPath(str)
if err != nil { return "", false }
if err != nil {
return "", false
}
}

return cmd, true
64 changes: 43 additions & 21 deletions helpers/offset.go
Original file line number Diff line number Diff line change
@@ -2,20 +2,22 @@ package helpers

import (
"bufio"
"debug/elf"
"encoding/binary"
"errors"
"debug/elf"
"io"
"os"
"strings"
"strconv"
"strings"
)

// GetOffset takes an AppImage (either ELF or shappimage), returning the offset
// of its SquashFS archive
func GetOffset(src string) (int, error) {
format, err := GetAppImageType(src)
if err != nil { return -1, err }
if err != nil {
return -1, err
}

if format == -2 {
return getShappImageSize(src)
@@ -33,19 +35,23 @@ func GetOffset(src string) (int, error) {
func getShappImageSize(src string) (int, error) {
f, err := os.Open(src)
defer f.Close()
if err != nil { return -1, err }
if err != nil {
return -1, err
}

_, err = f.Stat()
if err != nil { return -1, err }
if err != nil {
return -1, err
}

scanner := bufio.NewScanner(f)
for scanner.Scan() {
if len(scanner.Text()) > 10 && scanner.Text()[0:11] == "sfs_offset=" &&
len(strings.Split(scanner.Text(), "=")) == 2 {
len(strings.Split(scanner.Text(), "=")) == 2 {

offHex := strings.Split(scanner.Text(), "=")[1]
offHex = strings.ReplaceAll(offHex, "'", "")
offHex = strings.ReplaceAll(offHex, "\"", "")
offHex = strings.ReplaceAll(offHex, "'", "")
offHex = strings.ReplaceAll(offHex, "\"", "")
o, err := strconv.Atoi(offHex)

return int(o), err
@@ -63,7 +69,9 @@ func getElfSize(src string) (int, error) {
f, _ := os.Open(src)
defer f.Close()
e, err := elf.NewFile(f)
if err != nil { return -1, err }
if err != nil {
return -1, err
}

// Find offsets based on arch
sr := io.NewSectionReader(f, 0, 1<<63-1)
@@ -74,23 +82,31 @@ func getElfSize(src string) (int, error) {
hdr := new(elf.Header64)

_, err = sr.Seek(0, 0)
if err != nil { return -1, err }
if err != nil {
return -1, err
}
err = binary.Read(sr, e.ByteOrder, hdr)
if err != nil { return -1, err }
if err != nil {
return -1, err
}

shoff = int(hdr.Shoff)
shnum = int(hdr.Shnum)
shoff = int(hdr.Shoff)
shnum = int(hdr.Shnum)
shentsize = int(hdr.Shentsize)
case elf.ELFCLASS32:
hdr := new(elf.Header32)

_, err = sr.Seek(0, 0)
if err != nil { return -1, err }
if err != nil {
return -1, err
}
err := binary.Read(sr, e.ByteOrder, hdr)
if err != nil { return -1, err }
if err != nil {
return -1, err
}

shoff = int(hdr.Shoff)
shnum = int(hdr.Shnum)
shoff = int(hdr.Shoff)
shnum = int(hdr.Shnum)
shentsize = int(hdr.Shentsize)
default:
return 0, nil
@@ -106,10 +122,14 @@ func getElfSize(src string) (int, error) {
func GetAppImageType(src string) (int, error) {
f, err := os.Open(src)
defer f.Close()
if err != nil { return -1, err }
if err != nil {
return -1, err
}

_, err = f.Stat()
if err != nil { return -1, err }
if err != nil {
return -1, err
}

if HasMagic(f, "\x7fELF", 0) {
if HasMagic(f, "AI\x01", 8) {
@@ -134,11 +154,13 @@ func GetAppImageType(src string) (int, error) {
// if identical, return true
func HasMagic(r io.ReadSeeker, str string, offset int) bool {
magic := make([]byte, len(str))

r.Seek(int64(offset), io.SeekStart)

_, err := io.ReadFull(r, magic[:])
if err != nil { return false }
if err != nil {
return false
}

for i := 0; i < len(str); i++ {
if magic[i] != str[i] {
14 changes: 9 additions & 5 deletions helpers/updateinfo.go
Original file line number Diff line number Diff line change
@@ -2,15 +2,17 @@ package helpers

import (
"bufio"
"strings"
"bytes"
"errors"
"debug/elf"
"errors"
"strings"
)

func ReadUpdateInfo(src string) (string, error) {
format, err := GetAppImageType(src)
if err != nil { return "", err }
if err != nil {
return "", err
}

if format == 2 || format == 1 {
return readUpdateInfoFromElf(src)
@@ -49,7 +51,9 @@ func readUpdateInfoFromElf(src string) (string, error) {

func readUpdateInfoFromShappimage(src string) (string, error) {
f, err := ExtractResourceReader(src, "update_info")
if err != nil { return "", err }
if err != nil {
return "", err
}

scanner := bufio.NewScanner(f)
for scanner.Scan() {
@@ -59,6 +63,6 @@ func readUpdateInfoFromShappimage(src string) (string, error) {
return scanner.Text(), nil
}
}

return "", errors.New("unable to find update information in shImg")
}
24 changes: 15 additions & 9 deletions mount.go
Original file line number Diff line number Diff line change
@@ -3,15 +3,15 @@ package aisap
import (
"bufio"
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"errors"
"fmt"

xdg "github.com/adrg/xdg"
helpers "github.com/mgord9518/aisap/helpers"
xdg "github.com/adrg/xdg"
)

// mount mounts the requested AppImage `src` to `dest`
@@ -28,7 +28,7 @@ func mount(src string, dest string, offset int) error {

// Convert the offset to a string and mount using squashfuse
o := strconv.Itoa(offset)
mnt := exec.Command(squashfuse, "-o", "offset=" + o, src, dest)
mnt := exec.Command(squashfuse, "-o", "offset="+o, src, dest)
mnt.Stderr = errBuf

if mnt.Run() != nil {
@@ -59,11 +59,15 @@ func (ai *AppImage) Mount(dest ...string) error {

var err error

ai.tempDir, err = helpers.MakeTemp(xdg.RuntimeDir + "/aisap/tmp", ai.md5)
if err != nil { return err }
ai.tempDir, err = helpers.MakeTemp(xdg.RuntimeDir+"/aisap/tmp", ai.md5)
if err != nil {
return err
}

ai.mountDir, err = helpers.MakeTemp(xdg.RuntimeDir + "/aisap/mount", ai.md5)
if err != nil { return err }
ai.mountDir, err = helpers.MakeTemp(xdg.RuntimeDir+"/aisap/mount", ai.md5)
if err != nil {
return err
}

fmt.Println(ai.mountDir)
fmt.Println(ai.tempDir)
@@ -94,7 +98,9 @@ func (ai *AppImage) Destroy() error {
}

err := unmountDir(ai.MountDir())
if err != nil { return err }
if err != nil {
return err
}

ai.mountDir = ""

56 changes: 31 additions & 25 deletions permissions/permissions.go
Original file line number Diff line number Diff line change
@@ -8,16 +8,16 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"strconv"
"strings"

xdg "github.com/adrg/xdg"
helpers "github.com/mgord9518/aisap/helpers"
ini "gopkg.in/ini.v1"
xdg "github.com/adrg/xdg"
ini "gopkg.in/ini.v1"
)

var (
InvalidSocket = errors.New("socket invalid")
InvalidSocket = errors.New("socket invalid")
)

type File struct {
@@ -35,7 +35,7 @@ func SocketFromString(socketString string) (Socket, error) {
return socket, InvalidSocket
}

return socket, nil
return socket, nil
}

const (
@@ -73,18 +73,18 @@ var (
)

type AppImagePerms struct {
Level int `json:"level"` // How much access to system files
Files []string `json:"filesystem"` // Grant permission to access files
Devices []string `json:"devices"` // Access device files (eg: dri, input)
Sockets []Socket `json:"sockets"` // Use sockets (eg: x11, pulseaudio, network)
Level int `json:"level"` // How much access to system files
Files []string `json:"filesystem"` // Grant permission to access files
Devices []string `json:"devices"` // Access device files (eg: dri, input)
Sockets []Socket `json:"sockets"` // Use sockets (eg: x11, pulseaudio, network)

// TODO: rename to PersistentHome or something
DataDir bool `json:"data_dir"` // Whether or not a data dir should be created (only
DataDir bool `json:"data_dir"` // Whether or not a data dir should be created (only
// use if the AppImage saves ZERO data eg: 100% online or a game without
// save files)

// Only intended for unmarshalling, should not be used for other purposes
Names []string `json:"names"`
Names []string `json:"names"`
}

// FromIni attempts to read permissions from a provided *ini.File, if fail, it
@@ -93,8 +93,8 @@ func FromIni(e *ini.File) (*AppImagePerms, error) {
p := &AppImagePerms{}

// Get permissions from keys
level := e.Section("X-App Permissions").Key("Level").Value()
filePerms := e.Section("X-App Permissions").Key("Files").Value()
level := e.Section("X-App Permissions").Key("Level").Value()
filePerms := e.Section("X-App Permissions").Key("Files").Value()
devicePerms := e.Section("X-App Permissions").Key("Devices").Value()
socketPerms := e.Section("X-App Permissions").Key("Sockets").Value()

@@ -103,7 +103,7 @@ func FromIni(e *ini.File) (*AppImagePerms, error) {
p.DataDir = false
} else {
p.DataDir = true
}
}

l, err := strconv.Atoi(level)
if err != nil || l < 0 || l > 3 {
@@ -153,12 +153,16 @@ func FromSystem(name string) (*AppImagePerms, error) {

func FromReader(r io.Reader) (*AppImagePerms, error) {
b, err := ioutil.ReadAll(r)
if err != nil { return nil, err }
if err != nil {
return nil, err
}

b = bytes.ReplaceAll(b, []byte(";"), []byte(";"))

e, err := ini.Load(b)
if err != nil { return nil, err }
if err != nil {
return nil, err
}

return FromIni(e)
}
@@ -177,13 +181,15 @@ func (p *AppImagePerms) AddDevices(s ...string) {
}

func (p *AppImagePerms) AddSockets(socketStrings ...string) error {
if len(socketStrings) == 0 { return nil}
if len(socketStrings) == 0 {
return nil
}

p.RemoveSockets(socketStrings...)

for i := range(socketStrings) {
for i := range socketStrings {
socket, err := SocketFromString(socketStrings[i])

if err != nil {
return err
}
@@ -198,17 +204,17 @@ func (p *AppImagePerms) removeFile(str string) {
// Done this way to ensure there is an `extension` eg: `:ro` on the string,
// it will then be used to detect if that file already exists
str = helpers.CleanFiles([]string{str})[0]
s := strings.Split(str, ":")
s := strings.Split(str, ":")
str = strings.Join(s[:len(s)-1], ":")

if i, present := helpers.ContainsAny(p.Files,
[]string{ str + ":ro", str + ":rw" }); present {
[]string{str + ":ro", str + ":rw"}); present {
p.Files = append(p.Files[:i], p.Files[i+1:]...)
}
}

func (p *AppImagePerms) RemoveFiles(s ...string) {
for i := range(s) {
for i := range s {
p.removeFile(s[i])
}
}
@@ -220,7 +226,7 @@ func (p *AppImagePerms) removeDevice(str string) {
}

func (p *AppImagePerms) RemoveDevices(s ...string) {
for i := range(s) {
for i := range s {
p.removeDevice(s[i])
}
}
@@ -235,7 +241,7 @@ func (p *AppImagePerms) removeSocket(str string) {
}

func (p *AppImagePerms) RemoveSockets(s ...string) {
for i := range(s) {
for i := range s {
p.removeSocket(s[i])
}
}
24 changes: 20 additions & 4 deletions profiles/README.md
Original file line number Diff line number Diff line change
@@ -6,26 +6,31 @@ All items in this list are based on the name of their appropriate AppImage's
desktop entry, as some of the same apps may have different names depending on
the version, there is also a list of aliases that link to the original profile

## Current supported applications (105)
## Current supported applications (121)
|name|level|devices|sockets|filesystem|
|-|-|-|-|-|
|0 a.d.|3|dri|x11, alsa, network||
|aaaaxy|3|dri|x11, alsa||
|apk editor studio|1|dri|x11|xdg-templates:rw, xdg-download:rw|
|appimage pool|2|dri|wayland, x11, network|~/Applications:rw|
|appimageupdate|2|dri|x11, network|~/Applications:rw|
|aranym jit|3|dri, input|x11, audio, network|xdg-download:ro, ~/Games:ro, ~/Roms:ro|
|aranym|3|dri, input|x11, audio, network|xdg-download:ro, ~/Games:ro, ~/Roms:ro|
|aranym|3|dri, input|x11, audio, network|xdg-download:ro, ~/Games:ro, ~/Roms:ro|
|aranym|3|dri, input|x11, audio, network|xdg-download:ro, ~/Games:ro, ~/Roms:ro|
|armagetron advanced|3|dri, input|x11, audio, network||
|badlion client|2|dri|x11, audio, network||
|balenaetcher|0||||
|blender|2|dri|x11|xdg-templates:rw, xdg-documents:rw|
|brave|2|dri|x11, pulseaudio, network|xdg-download:rw|
|bugdom|1|dri|x11, audio, network||
|calibre|2|dri|x11|xdg-documents:ro|
|cemu|2|dri, input|x11, alsa, network|xdg-download:ro, ~/Games:ro, ~/Roms:ro|
|chromium|2|dri|x11, pulseaudio, network|xdg-download:rw|
|conky|1|dri|x11, pid||
|cool retro term|2|dri|x11, network|~/.config/nvim:ro, ~/.profile:ro, ~/.bashrc:ro, ~/.zshrc:ro, ~/.viminfo:ro|
|cro-mag rally|3|dri|x11, alsa||
|deadbeef|2|dri|x11, audio, network|xdg-music:rw|
|deadbeef|2|dri|x11, audio, network|xdg-music:rw|
|deemix-gui|2|dri|x11, audio, network|xdg-music:rw|
|densify|2|dri|x11|xdg-documents:rw|
|desmume|2|dri, input|x11, alsa|xdg-download:rw, ~/Games:rw, ~/Roms:rw|
@@ -39,9 +44,13 @@ the version, there is also a list of aliases that link to the original profile
|eternal lands (appimage)|1|dri|x11, audio, network||
|fireboy and watergirl: in the forest temple|1|dri|x11, pulseaudio||
|firefox|2|dri|x11, pulseaudio, network, dbus|xdg-download:rw|
|firefox|2|dri|x11, pulseaudio, network, dbus|xdg-download:rw|
|firefox|2|dri|x11, pulseaudio, network, dbus|xdg-download:rw|
|fontforge|2|dri|x11|xdg-documents:rw, ~/.fonts:rw|
|fractale|2|dri|x11||
|freecad conda|1|dri|x11|xdg-documents:rw, xdg-templates:rw|
|freecad|1|dri|x11|xdg-documents:rw, xdg-templates:rw|
|freecad|1|dri|x11|xdg-documents:rw, xdg-templates:rw|
|freecad|1|dri|x11|xdg-documents:rw, xdg-templates:rw|
|gambatte_qt|2|dri, input|x11, alsa|xdg-download:rw, ~/Games:rw, ~/Roms:rw|
|geometrize|2|dri|x11|xdg-pictures:rw|
|gnu image manipulation program|1|dri|x11|xdg-pictures:rw|
@@ -85,8 +94,11 @@ the version, there is also a list of aliases that link to the original profile
|pokete|3||alsa, network||
|potato presenter|2|dri|x11|xdg-documents:rw|
|ppsspp|2|dri|x11, audio|xdg-download:ro, ~/Games:ro, ~/Roms:ro|
|python3.10.1|3||||
|python|3||||
|python|3||||
|python|3||||
|runelite|1|dri|x11, audio, network||
|ryujinx|2|dri, input|x11, alsa, network|xdg-download:ro, ~/Games:ro, ~/Roms:ro|
|sengi|1|dri|x11, audio, network||
|signal|2|dri|x11, network||
|smallbasic|2|dri|x11|xdg-documents:rw|
@@ -100,8 +112,10 @@ the version, there is also a list of aliases that link to the original profile
|stellarium|1|dri|x11||
|stunt car remake|3|dri|x11, alsa||
|subsurface|1|dri|x11|xdg-documents:ro|
|yuzu|2|dri, input|x11, alsa, network|xdg-download:ro, ~/Games:ro, ~/Roms:ro|
|supertux 2|3|dri, input|x11, audio, network||
|supertuxkart|3|dri, input|x11, audio, network||
|yuzu|2|dri, input|x11, alsa, network|xdg-download:ro, ~/Games:ro, ~/Roms:ro|
|synthein|3|dri|x11, alsa||
|tapecalc|2|dri|x11||
|texstudio|1|dri|x11|xdg-documents:rw, xdg-templates:rw|
@@ -110,6 +124,8 @@ the version, there is also a list of aliases that link to the original profile
|tiled|2|dri|x11|xdg-documents:rw, xdg-pictures:rw, xdg-templates:rw|
|upscayl|1|dri|x11|xdg-pictures:rw|
|visual studio code|2|dri|x11, network|xdg-documents:rw|
|visual studio code|2|dri|x11, network|xdg-documents:rw|
|waterfox|2|dri|x11, pulseaudio, network, dbus|xdg-download:rw|
|waterfox|2|dri|x11, pulseaudio, network, dbus|xdg-download:rw|
|xonotic|3|dri|x11, alsa, network||
|yabg|3|dri, input|x11, pulseaudio||
6 changes: 3 additions & 3 deletions profiles/profiles.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package profiles

import (
"encoding/json"
"errors"
"strings"
"encoding/json"

helpers "github.com/mgord9518/aisap/helpers"
helpers "github.com/mgord9518/aisap/helpers"
permissions "github.com/mgord9518/aisap/permissions"

_ "embed"
@@ -29,7 +29,7 @@ func FromName(name string) (*permissions.AppImagePerms, error) {
return &p, nil
}

return &permissions.AppImagePerms{ Level: -1 }, errors.New("cannot find permissions for app `" + name + "`")
return &permissions.AppImagePerms{Level: -1}, errors.New("cannot find permissions for app `" + name + "`")
}

//go:embed profile_database.json
4 changes: 2 additions & 2 deletions spooky/isspooky.go
Original file line number Diff line number Diff line change
@@ -46,13 +46,13 @@ func IsSpooky(str string) bool {
slice := strings.Split(str, ":")
s1 := strings.Join(slice[:len(slice)-1], ":")

for _, val := range(spookyFiles) {
for _, val := range spookyFiles {
if s1 == val {
return true
}
}

for _, val := range(spookyDirs) {
for _, val := range spookyDirs {
if len(s1) < len(val) {
continue
}
337 changes: 160 additions & 177 deletions wrap.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion zig/build.zig.zon
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.{
.name = "aisap",
.version = "0.9.11-alpha",
.version = "0.10.11-alpha",
.paths = [][]const u8 {""},
.dependencies = .{
.squashfuse = .{

0 comments on commit 490ab39

Please sign in to comment.