957 lines
32 KiB
Go
957 lines
32 KiB
Go
|
package common
|
||
|
|
||
|
import (
|
||
|
"encoding/binary"
|
||
|
"eon/lib"
|
||
|
|
||
|
"bytes"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"os"
|
||
|
"plugin"
|
||
|
"regexp"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"crypto/ed25519"
|
||
|
"database/sql"
|
||
|
"math/big"
|
||
|
|
||
|
"golang.org/x/term"
|
||
|
|
||
|
"github.com/Masterminds/semver"
|
||
|
"github.com/fatih/color"
|
||
|
"modernc.org/sqlite"
|
||
|
)
|
||
|
|
||
|
type InstallPackage struct {
|
||
|
LocalFile bool
|
||
|
Url string
|
||
|
StreamOrBytes lib.StreamOrBytes
|
||
|
EPKPreMap *lib.EPKPreMap
|
||
|
}
|
||
|
|
||
|
type PluginInfo struct {
|
||
|
Name string
|
||
|
HasErrHandler bool
|
||
|
HasVagueErr bool
|
||
|
OverridesExisting bool
|
||
|
}
|
||
|
|
||
|
type Plugin struct {
|
||
|
Info PluginInfo
|
||
|
MainFunc func(version string, dbVersion string, args []string, conn *sql.DB, logger *lib.Logger) (etc map[string]interface{}, err error, vagueErr error)
|
||
|
ErrHandler func(etc map[string]interface{}, err error, vagueErr error, logger *lib.Logger)
|
||
|
}
|
||
|
|
||
|
var conn *sql.DB
|
||
|
var dbVersion = semver.MustParse("1.0.0-beta.2")
|
||
|
|
||
|
var DefaultLogger = lib.Logger{
|
||
|
LogFunc: func(log lib.Log) string {
|
||
|
if log.PlaySound {
|
||
|
fmt.Print("\a")
|
||
|
}
|
||
|
switch log.Level {
|
||
|
case "WARN":
|
||
|
fmt.Println(color.YellowString(log.Content))
|
||
|
case "ERROR":
|
||
|
fmt.Println(color.HiYellowString(log.Content))
|
||
|
case "CRITICAL":
|
||
|
fmt.Println(color.HiRedString(log.Content))
|
||
|
case "FATAL":
|
||
|
fmt.Println(color.RedString(log.Content))
|
||
|
os.Exit(1)
|
||
|
case "PROGRESS":
|
||
|
// Maths time! We need to calculate the percentage of the progress bar.
|
||
|
// Convert the total and progress to big.Floats so we can do division.
|
||
|
floatTotal := new(big.Float)
|
||
|
floatProgress := new(big.Float)
|
||
|
floatTotal.SetInt(log.Total)
|
||
|
floatProgress.SetInt(log.Progress)
|
||
|
// Calculate the fraction we need to multiply by 100 to get the percentage.
|
||
|
percentageBig := new(big.Float).Quo(floatProgress, floatTotal)
|
||
|
percentage, _ := percentageBig.Float64()
|
||
|
// Get the terminal width so we can calculate the width of the progress bar.
|
||
|
width, _, err := term.GetSize(int(os.Stdout.Fd()))
|
||
|
if err != nil {
|
||
|
fmt.Println(color.RedString("Failed to get terminal width: " + err.Error()))
|
||
|
os.Exit(1)
|
||
|
}
|
||
|
// Calculate the percentage in text form.
|
||
|
percentageText := " " + strconv.Itoa(int(percentage*100)) + "%"
|
||
|
// We need to give the percentage text a bit of padding so it doesn't look weird.
|
||
|
switch len(percentageText) {
|
||
|
case 3:
|
||
|
percentageText = " " + percentageText + " "
|
||
|
case 4:
|
||
|
percentageText = percentageText + " "
|
||
|
}
|
||
|
// Print the percentage text and the beginning of the progress bar.
|
||
|
fmt.Print("\r" + percentageText + " [")
|
||
|
|
||
|
// Calculate how much of the bar should be filled.
|
||
|
targetWidth := int(float64(width-8) * percentage)
|
||
|
// Print the filled part of the bar.
|
||
|
for range targetWidth {
|
||
|
fmt.Print("=")
|
||
|
}
|
||
|
// If it isn't 100%, print the arrow.
|
||
|
if int(percentage) != 1 {
|
||
|
fmt.Print(">")
|
||
|
}
|
||
|
// Print the empty part of the bar.
|
||
|
for range width - targetWidth - 9 {
|
||
|
fmt.Print(" ")
|
||
|
}
|
||
|
// And voilà! Cap the bar off. Don't put a space here, because it causes a newline in some terminals.
|
||
|
if log.Overwrite {
|
||
|
fmt.Print("]")
|
||
|
} else {
|
||
|
fmt.Print("]\n")
|
||
|
}
|
||
|
default:
|
||
|
fmt.Println(log.Content)
|
||
|
}
|
||
|
if log.Prompt {
|
||
|
fmt.Print(": ")
|
||
|
var userInput string
|
||
|
_, err := fmt.Scanln(&userInput)
|
||
|
if err != nil {
|
||
|
fmt.Println(color.RedString("Failed to read input: " + err.Error()))
|
||
|
os.Exit(1)
|
||
|
} else {
|
||
|
return userInput
|
||
|
}
|
||
|
}
|
||
|
return ""
|
||
|
},
|
||
|
PromptSupported: true,
|
||
|
ProgressSupported: true,
|
||
|
}
|
||
|
|
||
|
func LoadPlugin(pluginName string, logger *lib.Logger) (Plugin, error) {
|
||
|
// Check the plugin's signature
|
||
|
// Load the plugin file
|
||
|
pluginFile, err := os.ReadFile("/var/lib/eon/plugins/" + pluginName + ".so")
|
||
|
if err != nil {
|
||
|
return Plugin{}, err
|
||
|
}
|
||
|
// Load the signature file
|
||
|
bypassSignature := false
|
||
|
sigFile, err := os.ReadFile("/var/lib/eon/plugins/" + pluginName + ".eps")
|
||
|
if err != nil {
|
||
|
if errors.Is(err, os.ErrNotExist) {
|
||
|
response := logger.LogFunc(lib.Log{
|
||
|
Level: "WARN",
|
||
|
Content: "Plugin " + pluginName + " is not signed. That means that this code is UNTRUSTED and may be " +
|
||
|
"MALWARE. This can seriously harm your system! Only proceed if you trust the plugin author, and " +
|
||
|
"are absolutely 100% sure it is from them! Do you want to load the plugin anyway (y/n)?",
|
||
|
Prompt: true,
|
||
|
PlaySound: true,
|
||
|
})
|
||
|
if strings.ToLower(response) != "y" {
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Plugin " + pluginName + " is not signed. Refusing to load the plugin. Glad you have some " +
|
||
|
"common sense!",
|
||
|
Prompt: false,
|
||
|
})
|
||
|
} else {
|
||
|
// This is your final warning before you blow your system to bits.
|
||
|
// If you are a confused user looking through the source code, seriously, DO NOT load the plugin!
|
||
|
// Any good plugin author should sign their plugins. Even if you are testing a plugin, you should
|
||
|
// sign it. If your friend doesn't sign their plugin, tell them to sign it!
|
||
|
finalWarning := logger.LogFunc(lib.Log{
|
||
|
Level: "WARN",
|
||
|
Content: "Final warning. Is what you're doing really worth risking your entire system? If someone " +
|
||
|
"is telling you to bypass this warning, they are probably LYING! Are you SURE you know what " +
|
||
|
"you're doing (if you do, you probably won't be here)? If you would like to load the plugin, " +
|
||
|
"please type the following (case sensitive): \"Yes, I know what I'm doing, no one is telling me " +
|
||
|
"to do this, destroy my system!\". Otherwise, type anything else.",
|
||
|
Prompt: true,
|
||
|
PlaySound: true,
|
||
|
})
|
||
|
if finalWarning != "Yes, I know what I'm doing, no one is telling me to do this, destroy my system!" {
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Plugin " + pluginName + " is not signed. Refusing to load the plugin. Glad you have " +
|
||
|
"some common sense!",
|
||
|
Prompt: false,
|
||
|
})
|
||
|
} else {
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "WARN",
|
||
|
Content: "I'll give you 5 seconds to Ctrl+C out of this. If you don't, you're on your own.",
|
||
|
Prompt: false,
|
||
|
})
|
||
|
time.Sleep(5 * time.Second)
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "WARN",
|
||
|
Content: "You asked for it. Loading plugin " + pluginName + " without a signature. Don't say I " +
|
||
|
"didn't warn you.",
|
||
|
Prompt: false,
|
||
|
})
|
||
|
bypassSignature = true
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
// Get the public key from the database
|
||
|
if !bypassSignature {
|
||
|
err = conn.QueryRow("SELECT fingerprint FROM keys WHERE owner = ?", string(sigFile[96:])).Scan(&sigFile)
|
||
|
if err != nil {
|
||
|
if errors.Is(err, sql.ErrNoRows) {
|
||
|
fingerprint := lib.ByteToFingerprint(sigFile[:32])
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "WARN",
|
||
|
Content: "The public key for the plugin " + pluginName + " is not in the database:\n " + string(sigFile[96:]) + " " + fingerprint + "\n Do you want to add it to the database (y/n)?",
|
||
|
Prompt: true,
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
if ed25519.Verify(sigFile[:32], pluginFile, sigFile[32:96]) {
|
||
|
return Plugin{}, errors.New("invalid signature")
|
||
|
}
|
||
|
}
|
||
|
// Load the plugin. Unfortunately, there seems to be no way to load a plugin from memory, so we have to use the
|
||
|
// filesystem.
|
||
|
loadedPlugin, err := plugin.Open("/var/lib/eon/plugins/" + pluginName + ".so")
|
||
|
if err != nil {
|
||
|
return Plugin{}, err
|
||
|
}
|
||
|
// Look up the Info function
|
||
|
infoSymbol, err := loadedPlugin.Lookup("Info")
|
||
|
if err != nil {
|
||
|
return Plugin{}, err
|
||
|
}
|
||
|
// Assert the Info function to the correct type
|
||
|
infoFunction, ok := infoSymbol.(func() PluginInfo)
|
||
|
if !ok {
|
||
|
return Plugin{}, errors.New("plugin " + pluginName + " does not have an Info function")
|
||
|
}
|
||
|
// Get the plugin info
|
||
|
pluginInfo := infoFunction()
|
||
|
// Look up the Main function
|
||
|
mainSymbol, err := loadedPlugin.Lookup("Main")
|
||
|
if err != nil {
|
||
|
return Plugin{}, err
|
||
|
}
|
||
|
// Assert the Main function to the correct type
|
||
|
main, ok := mainSymbol.(func(version string, dbVersion string, args []string, conn *sql.DB, logger *lib.Logger) (etc map[string]interface{}, err error, vagueErr error))
|
||
|
if !ok {
|
||
|
return Plugin{}, errors.New("plugin " + pluginName + " does not have a Main function")
|
||
|
}
|
||
|
// Return the plugin struct
|
||
|
return Plugin{
|
||
|
Info: pluginInfo,
|
||
|
MainFunc: main,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
func PluginInstalled(pluginName string) bool {
|
||
|
_, err := os.Stat("/var/lib/eon/plugins/" + pluginName + ".so")
|
||
|
if err != nil {
|
||
|
return false
|
||
|
} else {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func InitDB(conn *sql.DB) error {
|
||
|
_, err := conn.Exec("DROP TABLE IF EXISTS global;" +
|
||
|
"DROP TABLE IF EXISTS packages;" +
|
||
|
"DROP TABLE IF EXISTS keys;" +
|
||
|
"DROP TABLE IF EXISTS repositories;" +
|
||
|
"DROP TABLE IF EXISTS remotePackages;")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Huge query moment. This is way too big to read comfortably, but too bad! It breaks the Jetbrains SQL formatter if
|
||
|
// I break it into multiple lines.
|
||
|
_, err = conn.Exec("CREATE TABLE packages (name TEXT NOT NULL UNIQUE, description TEXT NOT NULL, longDescription TEXT NOT NULL, version TEXT NOT NULL, author TEXT NOT NULL, license TEXT NOT NULL, architecture TEXT NOT NULL, size INTEGER NOT NULL, dependencies TEXT NOT NULL, removeScript TEXT NOT NULL, hasRemoveScript BOOLEAN NOT NULL, isDependency BOOLEAN NOT NULL DEFAULT false, repository TEXT)")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// We store the RSA fingerprint of a public key so we can alert if a key is already in the database.
|
||
|
_, err = conn.Exec("CREATE TABLE keys (fingerprint BLOB NOT NULL, owner TEXT NOT NULL UNIQUE)")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
_, err = conn.Exec("CREATE TABLE repositories (url TEXT NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE, owner TEXT NOT NULL, description TEXT NOT NULL)")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Way to big. Blame IntelliJ.
|
||
|
_, err = conn.Exec("CREATE TABLE remotePackages (name TEXT NOT NULL UNIQUE, author TEXT NOT NULL, description TEXT NOT NULL, version TEXT NOT NULL, architecture TEXT NOT NULL, size BLOB NOT NULL, dependencies TEXT NOT NULL, path TEXT NOT NULL, arch TEXT NOT NULL, hash TEXT NOT NULL, repository TEXT NOT NULL)")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Having exists here makes sure that there is only one global row in the table. This is a bit of a hack,
|
||
|
// but it's the only way to make sure that the version and endian-ness are always part of the database.
|
||
|
|
||
|
// Note that this is the only hack in this program (so far)! I feel somewhat proud, especially since it's less of
|
||
|
// a hack and more not utilizing the full power of SQL.
|
||
|
// - Arzumify
|
||
|
_, err = conn.Exec("CREATE TABLE global (version TEXT NOT NULL, uniquenessCheck BOOLEAN NOT NULL UNIQUE CHECK (uniquenessCheck = true) DEFAULT true)")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// We insert global last so that if the program crashes, the database is not marked as initialized.
|
||
|
_, err = conn.Exec("INSERT INTO global (version) VALUES (?)",
|
||
|
dbVersion.String())
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// EstablishDBConnection establishes a connection to the database. If the database does not exist, it will be created.
|
||
|
func EstablishDBConnection(logger *lib.Logger) error {
|
||
|
_, err := os.Stat("/var/lib/eon/packages.db")
|
||
|
if err != nil {
|
||
|
if errors.Is(err, os.ErrNotExist) {
|
||
|
err := os.MkdirAll("/var/lib/eon", 0755)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
conn, err = sql.Open("sqlite", "/var/lib/eon/packages.db")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
var version string
|
||
|
err = conn.QueryRow("SELECT version FROM global").Scan(&version)
|
||
|
if err != nil {
|
||
|
if err.(*sqlite.Error).Code() == 1 || errors.Is(err, sql.ErrNoRows) {
|
||
|
// Initialize database
|
||
|
err := InitDB(conn)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
} else {
|
||
|
return err
|
||
|
}
|
||
|
} else {
|
||
|
// Check version
|
||
|
semanticVersion, err := semver.NewVersion(version)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
isCurrentVersion := semanticVersion.Compare(dbVersion)
|
||
|
if isCurrentVersion < 0 {
|
||
|
if !PluginInstalled("database-migration") {
|
||
|
// Warn the user that the database is outdated
|
||
|
if logger.PromptSupported {
|
||
|
response := logger.LogFunc(lib.Log{
|
||
|
Level: "WARN",
|
||
|
Content: "The database is outdated and may not be compatible with the current version of Eon. " +
|
||
|
"The database migration plugin is not installed. Would you like to reset the database (y/n)?",
|
||
|
Prompt: true,
|
||
|
})
|
||
|
if strings.ToLower(response) == "y" {
|
||
|
err := InitDB(conn)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
} else {
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "The database is corrupted and cannot be used. Please reset the database.",
|
||
|
Prompt: false,
|
||
|
})
|
||
|
os.Exit(1)
|
||
|
}
|
||
|
} else {
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "The database is corrupted and cannot be used. Please reset the database.",
|
||
|
Prompt: false,
|
||
|
})
|
||
|
os.Exit(1)
|
||
|
}
|
||
|
} else {
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "INFO",
|
||
|
Content: "Loading plugin for database migration... All logs will be returned by the plugin until the migration is complete.",
|
||
|
Prompt: false,
|
||
|
})
|
||
|
loadedPlugin, err := LoadPlugin("database-migration", logger)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
etc, err, vagueErr := loadedPlugin.MainFunc(dbVersion.String(), version, nil, conn, logger)
|
||
|
if err != nil || vagueErr != nil {
|
||
|
if loadedPlugin.Info.HasErrHandler {
|
||
|
loadedPlugin.ErrHandler(etc, err, vagueErr, logger)
|
||
|
return errors.New("plugin " + loadedPlugin.Info.Name + "'s error handler did not stop program execution")
|
||
|
} else {
|
||
|
if loadedPlugin.Info.HasVagueErr {
|
||
|
return errors.New(vagueErr.Error() + ": " + err.Error())
|
||
|
} else {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "INFO",
|
||
|
Content: "Database migrated (hopefully). Please consult the logs returned by the plugin for more information",
|
||
|
Prompt: false,
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
} else if isCurrentVersion > 0 {
|
||
|
// Warn the user that the database is ahead of the current version of Eon
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "WARN",
|
||
|
Content: "The database is ahead of the current version of Eon. This may cause compatibility issues",
|
||
|
Prompt: false,
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func DefaultAddEPKToDB(metadata *lib.Metadata, removeScript []byte, dependency bool, hasRemoveScript bool, size int64) error {
|
||
|
// If it already exists, delete it. This may happen in a force-install scenario.
|
||
|
_, err := conn.Exec("DELETE FROM packages WHERE name = ?", metadata.Name)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
var dependencies string
|
||
|
if len(metadata.Dependencies) > 0 {
|
||
|
// This is the world's most basic JSON marshaller :P
|
||
|
// - Arzumify
|
||
|
dependencies = "[" + strings.Join(metadata.Dependencies, ", ") + "]"
|
||
|
} else {
|
||
|
// Best to make sure that the dependencies are always a JSON array, even if it's empty. Strings.Join might mess
|
||
|
// it up by adding whitespace or something.
|
||
|
dependencies = "[]"
|
||
|
}
|
||
|
|
||
|
// Another fat unreadable line so Jetbrains doesn't break the SQL formatter. Too bad!
|
||
|
// But seriously though why doesn't Jetbrains support this? It's literally their own IDE which tells me to not make
|
||
|
// super-long lines, but then doesn't support it.
|
||
|
// And I pay huge amounts of money for this.
|
||
|
|
||
|
// Not that I'm complaining or anything.
|
||
|
// - Arzumify
|
||
|
_, err = conn.Exec("INSERT INTO packages (name, description, longDescription, version, author, license, architecture, size, dependencies, removeScript, hasRemoveScript, isDependency) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||
|
metadata.Name, metadata.Description, metadata.LongDescription, metadata.Version.String(), metadata.Author,
|
||
|
metadata.License, metadata.Architecture, size, dependencies, string(removeScript), hasRemoveScript, dependency)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Note I never complain a lot in lib/main.go, because that's the one other people will probably use.
|
||
|
// common/main.go is very tailored towards cmd/main.go, and is probably worth nothing to anything other than CLIs
|
||
|
// which like to use my style of logging, error handling, database management, progress bars, etc.
|
||
|
// - Arzumify
|
||
|
// P.S I sign off comments which have my personal thoughts. I do write other ones, but they're usually more formal
|
||
|
// and contain less of... me.
|
||
|
// Ok I promise I'll stop now and actually let the compiler compile.
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// DefaultCheckEPKInDB checks if an EPK is in the database.
|
||
|
func DefaultCheckEPKInDB(name string) (*semver.Version, bool, error) {
|
||
|
var versionString string
|
||
|
err := conn.QueryRow("SELECT version FROM packages WHERE name = ?", name).Scan(&versionString)
|
||
|
if err != nil {
|
||
|
if errors.Is(err, sql.ErrNoRows) {
|
||
|
return &semver.Version{}, false, nil
|
||
|
}
|
||
|
return &semver.Version{}, false, err
|
||
|
}
|
||
|
version, err := semver.NewVersion(versionString)
|
||
|
if err != nil {
|
||
|
return &semver.Version{}, true, err
|
||
|
}
|
||
|
return version, true, nil
|
||
|
}
|
||
|
|
||
|
func DefaultGetEPKFromDB(name string) (lib.Metadata, string, bool, bool, int64, error) {
|
||
|
var metadata lib.Metadata
|
||
|
var removeScript string
|
||
|
var dependency bool
|
||
|
var versionString string
|
||
|
var dependencies string
|
||
|
var hasRemoveScript bool
|
||
|
var size int64
|
||
|
err := conn.QueryRow("SELECT name, description, longDescription, version, author, license, architecture, size, "+
|
||
|
"dependencies, removeScript, hasRemoveScript, isDependency FROM packages WHERE name = ?", name).Scan(
|
||
|
&metadata.Name, &metadata.Description, &metadata.LongDescription, &versionString, &metadata.Author,
|
||
|
&metadata.License, &metadata.Architecture, &size, &dependencies, &removeScript, &hasRemoveScript,
|
||
|
&dependency)
|
||
|
if err != nil {
|
||
|
return lib.Metadata{}, "", false, false, 0, err
|
||
|
}
|
||
|
// This is the world's most basic JSON unmarshaller :P
|
||
|
// - Arzumify
|
||
|
metadata.Dependencies = strings.Split(strings.TrimSuffix(strings.TrimPrefix(dependencies, "["), "]"), ", ")
|
||
|
version, err := semver.NewVersion(versionString)
|
||
|
if err != nil {
|
||
|
return lib.Metadata{}, "", false, false, 0, err
|
||
|
}
|
||
|
// For some reason NewVersion returns a pointer
|
||
|
metadata.Version = *version
|
||
|
return metadata, removeScript, dependency, hasRemoveScript, size, nil
|
||
|
}
|
||
|
|
||
|
func DefaultAddFingerprintToDB(fingerprint []byte, owner string, replace bool) error {
|
||
|
if replace {
|
||
|
_, err := conn.Exec("UPDATE keys SET owner = ? AND fingerprint = ? WHERE owner = ? OR fingerprint = ?", owner, fingerprint, owner, fingerprint)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
} else {
|
||
|
return nil
|
||
|
}
|
||
|
} else {
|
||
|
_, err := conn.Exec("INSERT INTO keys (fingerprint, owner) VALUES (?, ?)", fingerprint, owner)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
} else {
|
||
|
return nil
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func DefaultGetFingerprintFromDB(fingerprint []byte, author string) (bool, bool, bool, error) {
|
||
|
var authorCheck string
|
||
|
var fingerprintCheck []byte
|
||
|
err := conn.QueryRow("SELECT owner, fingerprint FROM keys WHERE owner = ? OR fingerprint = ?", author, fingerprint).Scan(&authorCheck, &fingerprintCheck)
|
||
|
if err != nil {
|
||
|
if errors.Is(err, sql.ErrNoRows) {
|
||
|
return false, false, false, nil
|
||
|
}
|
||
|
return false, false, false, err
|
||
|
} else {
|
||
|
if authorCheck == author && bytes.Equal(fingerprintCheck, fingerprint) {
|
||
|
return true, true, true, nil
|
||
|
} else if authorCheck == author {
|
||
|
return true, false, true, nil
|
||
|
} else {
|
||
|
return true, true, false, nil
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func DefaultAddRepositoryToDB(repository lib.Repository) error {
|
||
|
_, err := conn.Exec("INSERT INTO repositories (url, name, owner, description) VALUES (?, ?, ?, ?)",
|
||
|
repository.URL, repository.Name, repository.Owner, repository.Description)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func DefaultRemoveRepositoryFromDB(repository string) error {
|
||
|
_, err := conn.Exec("DELETE FROM repositories WHERE name = ?", repository)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
_, err = conn.Exec("DELETE FROM remotePackages WHERE repository = ?", repository)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func DefaultListRepositoriesInDB() ([]lib.Repository, error) {
|
||
|
rows, err := conn.Query("SELECT url, name, owner FROM repositories")
|
||
|
if err != nil {
|
||
|
return []lib.Repository{}, err
|
||
|
}
|
||
|
var repositories []lib.Repository
|
||
|
for rows.Next() {
|
||
|
var url, name, owner string
|
||
|
err := rows.Scan(&url, &name, &owner)
|
||
|
if err != nil {
|
||
|
return []lib.Repository{}, err
|
||
|
}
|
||
|
repositories = append(repositories, lib.Repository{Name: name, URL: url, Owner: owner})
|
||
|
}
|
||
|
return repositories, nil
|
||
|
}
|
||
|
|
||
|
func DefaultAddRemotePackageToDB(repository string, remoteEPK lib.RemoteEPK) error {
|
||
|
var dependencies string
|
||
|
// This is the world's most basic JSON marshaller :P
|
||
|
// - Arzumify
|
||
|
if len(remoteEPK.Dependencies) > 0 {
|
||
|
dependencies = "[" + strings.Join(remoteEPK.Dependencies, ", ") + "]"
|
||
|
} else {
|
||
|
// Best to make sure that the dependencies are always a JSON array, even if it's empty. Strings.Join might mess
|
||
|
// it up by adding whitespace or something.
|
||
|
dependencies = "[]"
|
||
|
}
|
||
|
|
||
|
// We use little-endian because big-endianness is never used on 99% of systems. Also, byte order is a myth.
|
||
|
hashBytes := make([]byte, 8)
|
||
|
binary.LittleEndian.PutUint64(hashBytes, remoteEPK.EPKHash)
|
||
|
|
||
|
_, err := conn.Exec("INSERT INTO remotePackages (name, author, description, version, architecture, size, dependencies, path, arch, hash, repository) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||
|
remoteEPK.Name, remoteEPK.Author, remoteEPK.Description, remoteEPK.Version.String(), remoteEPK.Architecture,
|
||
|
remoteEPK.CompressedSize, dependencies, remoteEPK.Path, remoteEPK.Arch, hashBytes, repository)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func PrintWithEvenPadding(item string, maxSize int) {
|
||
|
var printItem string
|
||
|
// Make sure green escape sequences are not counted in the length by using a regex.
|
||
|
noGreenEscape := regexp.MustCompile(`\x1b\[32m|\x1b\[0m`)
|
||
|
// Minus 2 to ensure we have space for padding.
|
||
|
if len(noGreenEscape.ReplaceAllString(item, "")) > maxSize-2 {
|
||
|
// We have already checked that the terminal is wide enough, so we shouldn't have to worry about
|
||
|
// maxSize being less than 6.
|
||
|
// It's minus 5 because we need to add "..." to the end of the string and have space for padding.
|
||
|
printItem = item[:maxSize-5] + "..."
|
||
|
} else {
|
||
|
printItem = item
|
||
|
}
|
||
|
// Now we need to check how much space we have left for padding.
|
||
|
padding := maxSize - len(noGreenEscape.ReplaceAllString(printItem, ""))
|
||
|
// Check if padding is odd or even.
|
||
|
if padding%2 == 0 {
|
||
|
// Padding is even.
|
||
|
for range padding / 2 {
|
||
|
fmt.Print(" ")
|
||
|
}
|
||
|
fmt.Print(printItem)
|
||
|
for range padding / 2 {
|
||
|
fmt.Print(" ")
|
||
|
}
|
||
|
} else {
|
||
|
// Padding is odd.
|
||
|
for range (padding + 1) / 2 {
|
||
|
fmt.Print(" ")
|
||
|
}
|
||
|
fmt.Print(printItem)
|
||
|
for range (padding - 1) / 2 {
|
||
|
fmt.Print(" ")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// I hate the vagueError system - it feels too much like a hack, but there's no chance in hell I'm going to find every
|
||
|
// error from every library I use and make a HandleError function for it. I'm not that insane.
|
||
|
// Anyway I should put a useful formal comment so that people using IntelliSense or whatever it's called in VSCode can
|
||
|
// see what this function does.
|
||
|
// - Arzumify
|
||
|
|
||
|
// PreMapEPKHandleError handles errors that occur during the mapping of an EPKs display data.
|
||
|
func PreMapEPKHandleError(err error, vagueErr error, logger *lib.Logger) {
|
||
|
switch {
|
||
|
case errors.Is(vagueErr, lib.ErrPreMapEPKFailedToReadError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to read file: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrPreMapEPKNotEPKError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "The specified file is not an EPK.",
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrPreMapEPKInvalidEndianError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "The specified file is corrupted or invalid: invalid endian",
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrPreMapEPKCouldNotParseJSONError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to parse metadata JSON: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrPreMapEPKCouldNotMapJSONError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to map metadata JSON: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// FullyMapMetadataHandleError handles errors that occur during the mapping of an EPK file.
|
||
|
func FullyMapMetadataHandleError(err error, vagueErr error, logger *lib.Logger) {
|
||
|
switch {
|
||
|
case errors.Is(vagueErr, lib.ErrFullyMapMetadataFailedToReadError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to read file: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrFullyMapMetadataFailedToJumpError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to jump to metadata: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrFullyMapMetadataFailedToAddFingerprintError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to add fingerprint to database: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrFullyMapMetadataFailedToGetFingerprintError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to get fingerprint from database: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrFullyMapMetadataInvalidSignatureError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "The specified file is corrupted or invalid: signature mismatch",
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrFullyMapMetadataCouldNotMapJSONError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to map metadata JSON: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// InstallEPKHandleError handles errors that occur during the installation of an EPK file.
|
||
|
func InstallEPKHandleError(tempDir string, err error, vagueErr error, logger *lib.Logger) {
|
||
|
doNotRemoveTempDir := false
|
||
|
switch {
|
||
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCreateTempDirError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to create temporary directory: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCreateZStandardReaderError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to create ZStandard reader: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotDecompressTarArchiveError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to decompress tar archive: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCreateDirError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to create directory: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotStatDirError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "ERROR",
|
||
|
Content: "Could not get file information about directory: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotStatFileError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "ERROR",
|
||
|
Content: "Could not get file information about file: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCreateFileError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to create file: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCloseTarReaderError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to close tar reader: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotStatHookError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Could not get file information about hook: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotRunHookError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to run hook: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotAddEPKToDBError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to add EPK to database: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotRemoveTempDirError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "CRITICAL",
|
||
|
Content: "Failed to remove temporary directory: " + err.Error() + ", please remove the directory " + tempDir + " manually",
|
||
|
Prompt: false,
|
||
|
})
|
||
|
doNotRemoveTempDir = true
|
||
|
}
|
||
|
if doNotRemoveTempDir {
|
||
|
err := os.RemoveAll(tempDir)
|
||
|
if err != nil {
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "CRITICAL",
|
||
|
Content: "Failed to remove temporary directory: " + err.Error() + ", please remove the directory " + tempDir + " manually",
|
||
|
Prompt: false,
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func AddRepositoryHandleError(err error, vagueErr error, logger *lib.Logger) {
|
||
|
switch {
|
||
|
case errors.Is(vagueErr, lib.ErrAddRepositoryCannotCreateRequestError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to create request: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrAddRepositoryCannotSendRequestError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to send request: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrAddRepositoryUnexpectedStatusCodeError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Unexpected status code: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrAddRepositoryCannotReadResponseError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to read response: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrAddRepositoryInvalidMagicError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Invalid magic: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrAddRepositoryCannotHashError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to copy file to hash: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrAddRepositoryUnmarshalMetadataError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to unmarshal metadata: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrFullyMapMetadataFailedToGetFingerprintError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to get fingerprint from database: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrAddRepositoryFailedToAddFingerprintError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to add fingerprint to database: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrAddRepositoryInvalidMetadataError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Invalid metadata: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrAddRepositoryCannotCreateDirError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to create repository directory: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrAddRepositoryCannotStatDirError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to get file information about repository directory: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrAddRepositoryFailedToAddPackageError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to add package to database: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrAddRepositoryRepositoryAlreadyExistsError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Repository already exists",
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrAddRepositoryFailedToAddRepositoryError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to add repository to database: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func RemoveRepositoryHandleError(err error, vagueErr error, logger *lib.Logger) {
|
||
|
switch {
|
||
|
case errors.Is(vagueErr, lib.ErrRemoveRepositoryDoesNotExistError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Repository does not exist",
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrRemoveRepositoryCannotStatRepositoryError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to get file information about repository: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrRemoveRepositoryCannotRemoveRepositoryError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to remove repository files: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
case errors.Is(vagueErr, lib.ErrRemoveRepositoryFailedToRemoveRepositoryFromDBError):
|
||
|
logger.LogFunc(lib.Log{
|
||
|
Level: "FATAL",
|
||
|
Content: "Failed to remove repository from database: " + err.Error(),
|
||
|
Prompt: false,
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Ironic how I sign off comments with my name when I'm literally the only contributor to this project, and git shows
|
||
|
// my name anyway. What is this, ftp?
|
||
|
// - Arzumify
|