package common import ( "eon/lib" "bufio" "bytes" "errors" "fmt" "math" "os" "plugin" "regexp" "sort" "strconv" "strings" "time" "crypto/ed25519" "database/sql" "encoding/binary" "math/big" "net/http" "net/url" "golang.org/x/term" "github.com/Masterminds/semver" "github.com/dustin/go-humanize" "github.com/fatih/color" "modernc.org/sqlite" ) type InstallPackage struct { IsRemote bool IsForced bool Url string Priority int StreamOrBytes lib.StreamOrBytes EPKPreMap *lib.EPKPreMap Repository lib.Repository } type RemovePackage struct { Name string Priority int DisplayData lib.DisplayData } 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") 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("Could not 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(": ") reader := bufio.NewReader(os.Stdin) userInput, err := reader.ReadString('\n') if err != nil { fmt.Println(color.RedString("[FATAL]"), "Could not read user input:", err) os.Exit(1) } else { return userInput[:len(userInput)-1] } } return "" }, PromptSupported: true, ProgressSupported: true, } var TimeMagnitudes = []humanize.RelTimeMagnitude{ {0, "now", 1}, {2 * time.Millisecond, "1 millisecond", 1}, {time.Second, "%d milliseconds", time.Millisecond}, // The following code is from an Expat / MIT licensed project. // Copyright (c) 2005-2008 Dustin Sallings // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // // // It is sublicensed under GPLv3. {2 * time.Second, "1 second", 1}, {time.Minute, "%d seconds", time.Second}, {2 * time.Minute, "1 minute", 1}, {time.Hour, "%d minutes", time.Minute}, {2 * time.Hour, "1 hour", 1}, {humanize.Day, "%d hours", time.Hour}, {2 * humanize.Day, "1 day", 1}, {humanize.Week, "%d days", humanize.Day}, {2 * humanize.Week, "1 week", 1}, {humanize.Month, "%d weeks", humanize.Week}, {2 * humanize.Month, "1 month", 1}, {humanize.Year, "%d months", humanize.Month}, {18 * humanize.Month, "1 year", 1}, {2 * humanize.Year, "2 years", 1}, {humanize.Year, "%d years", humanize.Year}, {math.MaxInt64, "a long while", 1}, // End of Expat / MIT licensed code } func HandleDependencies(previousDeps int, targetEPK InstallPackage, parentPriority int, epkList []lib.RemoteEPK, ListRemotePackagesInDB func() ([]lib.RemoteEPK, error), InstallPackageList []InstallPackage, previousPackages *[]string, logger *lib.Logger) ([]InstallPackage, int, error) { // Iterate through the dependencies of the target EPK. dependencyLoop: for _, dependency := range targetEPK.EPKPreMap.DisplayData.Dependencies { // Check if the dependency is already in the list of EPKs to install. for iterator, epk := range InstallPackageList { if epk.EPKPreMap.DisplayData.Name == dependency { // The dependency is already in the list of EPKs to install, check for its dependencies. if len(epk.EPKPreMap.DisplayData.Dependencies) == 0 || epk.EPKPreMap.DisplayData.Dependencies == nil { // All dependencies are handled - change the priority and continue with the next dependency. epk.Priority = parentPriority + 1 InstallPackageList[iterator] = epk continue dependencyLoop } else { // Check if it's a circular dependency. for _, epk := range *previousPackages { if epk == targetEPK.EPKPreMap.DisplayData.Name { // We have a circular dependency. Crash immediately. logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Circular dependency detected: " + targetEPK.EPKPreMap.DisplayData.Name + " -> " + dependency + " -> " + targetEPK.EPKPreMap.DisplayData.Name, Prompt: false, }) } } // Add the dependency to the list of previous packages. *previousPackages = append(*previousPackages, targetEPK.EPKPreMap.DisplayData.Name) // Recursively handle dependencies. installedPackage, addedDeps, err := HandleDependencies(previousDeps, epk, parentPriority+1, epkList, ListRemotePackagesInDB, InstallPackageList, previousPackages, logger) InstallPackageList = installedPackage if err != nil { return nil, 0, err } else { // Add the dependencies to the total number of dependencies. previousDeps += addedDeps // All dependencies are now handled - change the priority and continue with the next dependency. epk.Priority = parentPriority + 1 InstallPackageList[iterator] = epk continue dependencyLoop } } } } // Check if we already have a list of remote EPKs to use, if so use it. if len(epkList) == 0 || epkList == nil { var err error epkList, err = ListRemotePackagesInDB() if err != nil { return nil, 0, err } } // Check if we already have the EPK installed. version, exists, err := DefaultCheckEPKInDB(dependency) if err != nil { return nil, 0, err } var remoteEPK lib.RemoteEPK var epkEntry InstallPackage var dependencyExists bool // Iterate through the list of remote EPKs to find the dependency. for _, epk := range epkList { if epk.Name == dependency { dependencyExists = true if !exists { remoteEPK = epk break } else { // Check if the installed version is outdated. if version.LessThan(&epk.Version) { // Yes it is: Install the new version as an upgrade. remoteEPK = epk break } else { // The installed version is up-to-date. continue dependencyLoop } } } } // If the dependency doesn't exist, crash. if !dependencyExists { return nil, 0, errors.New("dependency " + dependency + " does not exist") } // Increase the dependency's priority. epkEntry.Priority = parentPriority + 1 // Add the dependency to the list of EPKs to install. epkDownloadUrl, err := url.JoinPath(remoteEPK.Repository.URL, remoteEPK.Path) if err != nil { logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not join URL path: " + err.Error(), Prompt: false, }) } // Set the URL and the fact that it is remote. epkEntry.IsRemote = true epkEntry.Url = epkDownloadUrl // Map the EPKs display data. epkPreMap, err, vagueErr := lib.PreMapRemoteEPK(remoteEPK, logger) if err != nil || vagueErr != nil { return nil, 0, err } epkPreMap.DisplayData.IsDependency = true // Set the EPKs display data. epkEntry.EPKPreMap = &epkPreMap if exists { // Set it as upgrade if it already exists. epkEntry.EPKPreMap.IsUpgrade = true } // Set the repository. epkEntry.Repository = remoteEPK.Repository // Make it noted that this is not a forced installation. epkEntry.IsForced = false // Check if the dependency has dependencies, and if so, recursively handle them. if !(len(remoteEPK.Dependencies) == 0 || remoteEPK.Dependencies == nil) { // Check if it's a circular dependency. for _, epk := range *previousPackages { if epk == targetEPK.EPKPreMap.DisplayData.Name { // We have a circular dependency. Crash immediately. logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Circular dependency detected: " + targetEPK.EPKPreMap.DisplayData.Name + " -> " + dependency + " -> " + targetEPK.EPKPreMap.DisplayData.Name, Prompt: false, }) } } // Add the dependency to the list of previous packages. *previousPackages = append(*previousPackages, epkEntry.EPKPreMap.DisplayData.Name) // Recursively handle dependencies. installPackages, addedDeps, err := HandleDependencies(previousDeps, epkEntry, epkEntry.Priority+1, epkList, ListRemotePackagesInDB, InstallPackageList, previousPackages, logger) InstallPackageList = installPackages if err != nil { return nil, 0, err } // Add the dependencies to the total number of dependencies. previousDeps += addedDeps } // All dependencies are now handled - continue with the next dependency. InstallPackageList = append(InstallPackageList, epkEntry) previousDeps++ continue } // If we reach this point, all dependencies have been handled. return InstallPackageList, previousDeps, nil } func GetTotalSize(InstallPackageList []InstallPackage) *big.Int { totalSize := new(big.Int) for _, epk := range InstallPackageList { totalSize.Add(totalSize, big.NewInt(epk.EPKPreMap.DisplayData.Size)) } return totalSize } func GetTotalInstalledSize(InstallPackageList []InstallPackage) *big.Int { totalSize := new(big.Int) for _, epk := range InstallPackageList { if !epk.IsRemote { totalSize.Add(totalSize, epk.EPKPreMap.DisplayData.DecompressedSize) } } return totalSize } 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, installedPaths TEXT, decompressedSize BLOB NOT NULL)") 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 could not be used. Please reset the database.", Prompt: false, }) os.Exit(1) } } else { logger.LogFunc(lib.Log{ Level: "FATAL", Content: "The database is corrupted and could not 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, installedPaths []string, removeScript []byte, dependency bool, hasRemoveScript bool, size int64, decompressedSize *big.Int, repository ...string) 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 = "[]" } var installedPathsString string if len(installedPaths) > 0 { installedPathsString = "[" + strings.Join(installedPaths, ", ") + "]" } else { installedPathsString = "[]" } // 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 if len(repository) > 0 { _, err = conn.Exec("INSERT INTO packages (name, description, longDescription, version, author, license, architecture, size, dependencies, removeScript, hasRemoveScript, isDependency, repository, installedPaths, decompressedSize) VALUES (?,?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", metadata.Name, metadata.Description, metadata.LongDescription, metadata.Version.String(), metadata.Author, metadata.License, metadata.Architecture, size, dependencies, string(removeScript), hasRemoveScript, dependency, repository[0], installedPathsString, decompressedSize.Bytes()) } else { _, err = conn.Exec("INSERT INTO packages (name, description, longDescription, version, author, license, architecture, size, dependencies, removeScript, hasRemoveScript, isDependency, installedPaths, decompressedSize) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", metadata.Name, metadata.Description, metadata.LongDescription, metadata.Version.String(), metadata.Author, metadata.License, metadata.Architecture, size, dependencies, string(removeScript), hasRemoveScript, dependency, installedPathsString, decompressedSize.Bytes()) } 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, string, []string, error) { var metadata lib.Metadata var removeScript string var dependency bool var versionString string var dependencies string var hasRemoveScript bool var repository string var installedPaths string var size int64 var decompressedSize []byte err := conn.QueryRow("SELECT description, longDescription, version, author, license, architecture, size, dependencies, removeScript, hasRemoveScript, isDependency, repository, installedPaths, decompressedSize FROM packages WHERE name = ?", name).Scan(&metadata.Description, &metadata.LongDescription, &versionString, &metadata.Author, &metadata.License, &metadata.Architecture, &size, &dependencies, &removeScript, &hasRemoveScript, &dependency, &repository, &installedPaths, &decompressedSize) metadata.DecompressedSize = big.NewInt(0).SetBytes(decompressedSize) if err != nil { return lib.Metadata{}, "", false, false, 0, "", nil, 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, "", nil, err } // Also unmarshal the installed paths var installedPathsSlice []string installedPathsSlice = strings.Split(strings.TrimSuffix(strings.TrimPrefix(installedPaths, "["), "]"), ", ") // For some reason NewVersion returns a pointer metadata.Version = *version return metadata, removeScript, dependency, hasRemoveScript, size, repository, installedPathsSlice, nil } func DefaultRemoveEPKFromDB(name string) error { _, err := conn.Exec("DELETE FROM packages WHERE name = ?", name) if err != nil { return err } return nil } func DefaultListEPKsInDB() (map[string]lib.DisplayData, map[string]*big.Int, map[string]lib.Repository, map[string]int64, error) { rows, err := conn.Query("SELECT name, description, version, author, architecture, size, dependencies, repository, isDependency, decompressedSize FROM packages") if err != nil { return nil, nil, nil, nil, err } repositoryMap := make(map[string]lib.Repository) repoNameToRepo := make(map[string]lib.Repository) metadataMap := make(map[string]lib.DisplayData) sizes := make(map[string]*big.Int) compressedSizes := make(map[string]int64) for rows.Next() { var metadata lib.DisplayData var size int64 var dependencies string var repository string var version string var isDependency bool var decompressedSize []byte err := rows.Scan(&metadata.Name, &metadata.Description, &version, &metadata.Author, &metadata.Architecture, &size, &dependencies, &repository, &isDependency, &decompressedSize) metadata.DecompressedSize = big.NewInt(0).SetBytes(decompressedSize) if err != nil { return nil, nil, nil, nil, err } metadata.IsDependency = isDependency semVer, err := semver.NewVersion(version) if err != nil { return nil, nil, nil, nil, err } // Stupid pointer metadata.Version = *semVer metadata.Dependencies = strings.Split(strings.TrimSuffix(strings.TrimPrefix(dependencies, "["), "]"), ", ") metadataMap[metadata.Name] = metadata sizes[metadata.Name] = big.NewInt(size) compressedSizes[metadata.Name] = size // Check if the repository is in the repoNameToRepo map var repo lib.Repository repo, ok := repoNameToRepo[repository] if !ok { // If it's not, find the repository and add it to the map repo.Name = repository err := conn.QueryRow("SELECT url, owner, description FROM repositories WHERE name = ?", repository).Scan(&repo.URL, &repo.Owner, &repo.Description) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, nil, nil, nil, errors.New("repository " + repository + " not found") } else { return nil, nil, nil, nil, err } } repoNameToRepo[repository] = repo } // Add the repository to the repository map repositoryMap[metadata.Name] = repo } return metadataMap, sizes, repositoryMap, compressedSizes, nil } func DefaultGetEPKRemoveInfoFromDB(name string) (string, []string, error) { var removeScript string var installedPaths string err := conn.QueryRow("SELECT removeScript, installedPaths FROM packages WHERE name = ?", name).Scan(&removeScript, &installedPaths) if err != nil { return "", nil, err } // Unmarshal the installed paths var installedPathsSlice []string installedPathsSlice = strings.Split(strings.TrimSuffix(strings.TrimPrefix(installedPaths, "["), "]"), ", ") return removeScript, installedPathsSlice, 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, forceReplace bool) error { if forceReplace { // Delete the repository if it already exists. This may happen in a force-install scenario. _, err := conn.Exec("DELETE FROM repositories WHERE name = ? OR url = ?", repository.Name, repository.URL) if err != nil { return err } } _, 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 DefaultCheckRepositoryInDB(repository string) (bool, error) { var name string err := conn.QueryRow("SELECT name FROM repositories WHERE name = ?", repository).Scan(&name) if err != nil { if errors.Is(err, sql.ErrNoRows) { return false, nil } return false, err } return true, nil } func DefaultListRepositoriesInDB() ([]lib.Repository, error) { rows, err := conn.Query("SELECT url, name, owner, description FROM repositories") if err != nil { return []lib.Repository{}, err } var repositories []lib.Repository for rows.Next() { var repoUrl, name, owner, description string err := rows.Scan(&repoUrl, &name, &owner, &description) if err != nil { return []lib.Repository{}, err } repositories = append(repositories, lib.Repository{Name: name, URL: repoUrl, Owner: owner, Description: description}) } return repositories, nil } func DefaultAddRemotePackageToDB(remoteEPK lib.RemoteEPK) error { // Delete the package if it already exists. This may happen in a force-install scenario. _, err := conn.Exec("DELETE FROM remotePackages WHERE name = ?", remoteEPK.Name) if err != nil { return err } 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, remoteEPK.Repository.Name) if err != nil { return err } return nil } func DefaultRemoveRemotePackageFromDB(name string) error { _, err := conn.Exec("DELETE FROM remotePackages WHERE name = ?", name) if err != nil { return err } return nil } func DefaultListRemotePackagesInDB() ([]lib.RemoteEPK, error) { rows, err := conn.Query("SELECT name, author, description, version, architecture, size, dependencies, path, arch, hash, repository FROM remotePackages ORDER BY version") if err != nil { return []lib.RemoteEPK{}, err } var remotePackages []lib.RemoteEPK for rows.Next() { var name, author, description, version, architecture, dependencies, path, arch, repository string var hashBytes []byte var size int64 err := rows.Scan(&name, &author, &description, &version, &architecture, &size, &dependencies, &path, &arch, &hashBytes, &repository) if err != nil { return []lib.RemoteEPK{}, err } var repositoryStruct lib.Repository repositoryStruct.Name = repository err = conn.QueryRow("SELECT url, owner, description FROM repositories WHERE name = ?", repository).Scan( &repositoryStruct.URL, &repositoryStruct.Owner, &repositoryStruct.Description) if err != nil { return []lib.RemoteEPK{}, err } // This is the world's most basic JSON unmarshaller :P // - Arzumify dependenciesList := strings.Split(strings.TrimSuffix(strings.TrimPrefix(dependencies, "["), "]"), ", ") // We use little-endian because big-endianness is never used on 99% of systems. Also, byte order is a myth. hash := binary.LittleEndian.Uint64(hashBytes) // We use MustParse because we know the version is valid. If it isn't, someone was messing with the database, // and that's no longer our problem. remotePackages = append(remotePackages, lib.RemoteEPK{ Name: name, Author: author, Description: description, Version: *semver.MustParse(version), Architecture: architecture, CompressedSize: size, Dependencies: dependenciesList, EPKHash: hash, Repository: repositoryStruct, Path: path, Arch: arch, }) } // Re-sort remotePackages by version, latest first - this stops the package manager from installing older versions. sort.Slice(remotePackages, func(i, j int) bool { return remotePackages[i].Version.GreaterThan(&remotePackages[j].Version) }) return remotePackages, 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`) // Now we can count the length of the string without the green escape sequences. itemLength := len(noGreenEscape.ReplaceAllString(item, "")) // Let's calculate how much padding we need to fit itemLength into maxSize. padding := maxSize - itemLength // The padding will always be positive due to an earlier check. // Check if the padding is even or odd. if padding%2 == 0 { // If it's even, we can just divide it by 2 and add that amount of padding to the left and right. leftPadding := strings.Repeat(" ", padding/2) rightPadding := strings.Repeat(" ", padding/2) printItem = leftPadding + item + rightPadding } else { // If it's odd, we add one more space to the right padding. leftPadding := strings.Repeat(" ", padding/2) rightPadding := strings.Repeat(" ", padding/2+1) printItem = leftPadding + item + rightPadding } // Print the string. fmt.Print(printItem) } func RefreshPackageList(listRepositoriesInDB func() ([]lib.Repository, error), addRemotePackageToDB func(lib.RemoteEPK) error, getFingerprintFromDB func([]byte, string) (bool, bool, bool, error), addFingerprintToDB func([]byte, string, bool) error, checkRepositoryInDB func(string) (bool, error), addRepositoryToDB func(lib.Repository, bool) error, listRemotePackagesInDB func() ([]lib.RemoteEPK, error), removeRemotePackageFromDB func(string) error, logger *lib.Logger) error { logger.LogFunc(lib.Log{ Level: "INFO", Content: "Refreshing package list...", }) startTime := time.Now() // Fetch new packages from repositories, that's easy. repositories, err := listRepositoriesInDB() if err != nil { return err } repositoryMap := make(map[string]lib.Repository) for _, repository := range repositories { repositoryMap[repository.Name] = repository _, err, vagueErr := lib.AddRepository(repository.URL, addRepositoryToDB, getFingerprintFromDB, addFingerprintToDB, addRemotePackageToDB, checkRepositoryInDB, true, logger) if err != nil || vagueErr != nil { AddRepositoryHandleError(err, vagueErr, logger) } } // We need to check if there are any orphaned packages with URLs that no longer exist. packages, err := listRemotePackagesInDB() if err != nil { return err } removedOrphanedPackages := 0 for _, remoteEpk := range packages { testUrl, err := url.JoinPath(remoteEpk.Repository.URL, remoteEpk.Path) if err != nil { return err } response, err := http.Head(testUrl) if err != nil { return err } if response.StatusCode != 200 && response.StatusCode != 404 { // We don't delete unless it's 404 because the server may be down. logger.LogFunc(lib.Log{ Level: "WARN", Content: "The package " + remoteEpk.Name + " has returned a status code of " + strconv.Itoa(response.StatusCode) + ". This may indicate a problem with the repository.", Prompt: false, }) } else if response.StatusCode == 404 { // Delete the package. err := removeRemotePackageFromDB(remoteEpk.Name) if err != nil { return err } removedOrphanedPackages++ } } logger.LogFunc(lib.Log{ Level: "INFO", Content: "Refreshed package list in " + humanize.CustomRelTime(startTime, time.Now(), "", "", TimeMagnitudes) + ". Removed " + strconv.Itoa(removedOrphanedPackages) + " orphaned packages.", Prompt: false, }) return nil } // 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.ErrPreMapEPKCouldNotRead): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not read file: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrPreMapEPKHasNetworkStream): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Network streams are not supported: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrPreMapEPKHasNotGotEPKMagic): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "The specified file is not an EPK.", Prompt: false, }) case errors.Is(vagueErr, lib.ErrPreMapEPKHasInvalidEndian): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "The specified file is corrupted or invalid: invalid endian", Prompt: false, }) case errors.Is(vagueErr, lib.ErrPreMapEPKCouldNotMapJSON): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not map metadata JSON: " + err.Error(), Prompt: false, }) } } // PreMapRemoteEPKHandleError handles errors that occur during the mapping of a remote EPKs display data. func PreMapRemoteEPKHandleError(err error, vagueErr error, logger *lib.Logger) { switch { case errors.Is(vagueErr, lib.ErrPreMapRemoteEPKCouldNotCreateURL): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not create URL: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrPreMapRemoteEPKCouldNotCreateRequest): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not create request: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrPreMapRemoteEPKCouldNotSendRequest): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not send request: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrPreMapRemoteEPKCouldNotRead): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not read EPK: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrPreMapRemoteEPKCouldNotCloseConnection): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not close connection: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrPreMapRemoteEPKUnexpectedStatusCode): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Unexpected status code: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrPreMapEPKHasNotGotEPKMagic): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "The specified file is not an EPK.", Prompt: false, }) case errors.Is(vagueErr, lib.ErrPreMapRemoteEPKInvalidEndian): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "The specified file is corrupted or invalid: invalid endian", Prompt: false, }) case errors.Is(vagueErr, lib.ErrPreMapRemoteEPKCouldNotMapJSON): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not 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.ErrFullyMapMetadataCouldNotRead): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not read file: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrFullyMapMetadataCouldNotJump): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not jump to metadata: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrFullyMapMetadataCouldNotAddFingerprint): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not add fingerprint to database: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrFullyMapMetadataCouldNotGetFingerprint): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not get fingerprint from database: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrFullyMapMetadataHasInvalidSignature): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "The specified file is corrupted or invalid: signature mismatch", Prompt: false, }) case errors.Is(vagueErr, lib.ErrFullyMapMetadataCouldNotMapJSON): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not 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.ErrInstallEPKCouldNotCreateTempDir): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not create temporary directory: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCreateZStandardReader): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not create ZStandard reader: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotDecompressTarArchive): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not decompress tar archive: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCreateDir): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not create directory: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotStatDir): logger.LogFunc(lib.Log{ Level: "ERROR", Content: "Could not get file information about directory: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotStatFile): logger.LogFunc(lib.Log{ Level: "ERROR", Content: "Could not get file information about file: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCreateFile): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not create file: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCloseTarReader): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not close tar reader: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotStatHook): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not get file information about hook: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotRunHook): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not run hook: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotAddEPKToDB): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not add EPK to database: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotRemoveTempDir): logger.LogFunc(lib.Log{ Level: "CRITICAL", Content: "Could not 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: "Could not 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.ErrAddRepositoryCouldNotCreateRequest): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not create request: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrAddRepositoryCouldNotSendRequest): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not send request: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrAddRepositoryHasUnexpectedStatusCode): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Unexpected status code: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrAddRepositoryCouldNotReadResponse): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not read response: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrAddRepositoryHasInvalidMagic): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Invalid magic: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrAddRepositoryCouldNotHash): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not copy file to hash: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrAddRepositoryCouldNotUnmarshalMetadata): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not unmarshal metadata: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrFullyMapMetadataCouldNotGetFingerprint): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not get fingerprint from database: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrAddRepositoryCouldNotAddFingerprint): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not add fingerprint to database: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrAddRepositoryHasInvalidMetadata): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Invalid metadata: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrAddRepositoryCouldNotAddPackage): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not add package to database: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrAddRepositoryHasRepositoryExists): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Repository already exists", Prompt: false, }) case errors.Is(vagueErr, lib.ErrAddRepositoryCouldNotAddRepository): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not add repository to database: " + err.Error(), Prompt: false, }) } } func RemoveRepositoryHandleError(err error, vagueErr error, logger *lib.Logger) { switch { case errors.Is(vagueErr, lib.ErrRemoveRepositoryDoesNotExist): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Repository does not exist", Prompt: false, }) case errors.Is(vagueErr, lib.ErrRemoveRepositoryCouldNotFindRepository): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not get file information about repository: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrRemoveRepositoryCouldNotRemoveRepository): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not remove repository files: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrRemoveRepositoryCouldNotRemoveRepositoryFromDB): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not remove repository from database: " + err.Error(), Prompt: false, }) } } // RemoveEPKHandleError handles errors that occur during the removal of an EPK. func RemoveEPKHandleError(err error, vagueErr error, logger *lib.Logger) { switch { case errors.Is(vagueErr, lib.ErrRemoveEPKCouldNotFindEPK): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not get EPK from database: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrRemoveEPKCouldNotCreateTempFile): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not create temporary file: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrRemoveEPKCouldNotWriteTempFile): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not write to temporary file: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrRemoveEPKCouldNotRunRemoveHook): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not run remove hook: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrRemoveEPKCouldNotRemoveEPKFromDB): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not remove EPK from database: " + err.Error(), Prompt: false, }) case errors.Is(vagueErr, lib.ErrRemoveEPKCouldNotRemoveFiles): logger.LogFunc(lib.Log{ Level: "FATAL", Content: "Could not remove files: " + 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