diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f3c8e93 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module gitea.com/oreonproject/eonlite + +go 1.22.5 + +require ( + github.com/cavaliergopher/cpio v1.0.1 + github.com/fatih/color v1.17.0 + github.com/klauspost/compress v1.17.9 +) + +require ( + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + golang.org/x/sys v0.18.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..caef962 --- /dev/null +++ b/go.sum @@ -0,0 +1,15 @@ +github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM= +github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/library/main.go b/library/main.go new file mode 100644 index 0000000..0e60ac8 --- /dev/null +++ b/library/main.go @@ -0,0 +1,205 @@ +package library + +import ( + "github.com/cavaliergopher/cpio" + "github.com/klauspost/compress/zstd" + "io" + "os" + "path/filepath" + "strconv" +) + +func InstallRPM(path string, logText func(string, string, bool) string) (int, error, int) { + // Open the RPM file + file, err := os.Open(path) + if err != nil { + return 0, err, 1 + } + + // Here is some black magic for extracting the CPIO archive from the RPM file. It's adapted from an ancient redhat mailing list (https://web.archive.org/web/20050327190810/http://www.redhat.com/archives/rpm-list/2003-June/msg00367.html) + // Step 1: Define the offset to skip the lead section + offset := 104 + + // Step 2: Read the next 8 bytes after the lead tag + buf := make([]byte, 8) + _, err = file.ReadAt(buf, int64(offset)) + if err != nil { + return 0, err, 2 + } + + // Step 3: Extract the initial length (il) and data length (dl) + // These values tell us the size of the signature header and its payload + il := int(buf[0])<<24 + int(buf[1])<<16 + int(buf[2])<<8 + int(buf[3]) // Initial length + dl := int(buf[4])<<24 + int(buf[5])<<16 + int(buf[6])<<8 + int(buf[7]) // Data length + + // Step 4: Calculate the total size of the signature section + // The signature size is calculated using the formula: 8 + 16 * il + dl + sigSize := 8 + 16*il + dl + + // Step 5: Adjust the offset to skip over the signature section and align it to 8-byte boundary + offset += sigSize + (8-(sigSize%8))%8 + 8 + + // Step 6: Read the next 8 bytes at the adjusted offset (the header section) + _, err = file.ReadAt(buf, int64(offset)) + if err != nil { + return 0, err, 3 + } + + // Step 7: Extract the header initial length (il) and data length (dl) again + // These values correspond to the header section, which we will skip to reach the CPIO archive + il = int(buf[0])<<24 + int(buf[1])<<16 + int(buf[2])<<8 + int(buf[3]) // Initial length + dl = int(buf[4])<<24 + int(buf[5])<<16 + int(buf[6])<<8 + int(buf[7]) // Data length + + // Step 8: Calculate the total size of the header section + // The header size is calculated using the same formula: 8 + 16 * il + dl + headerSize := 8 + 16*il + dl + + // Step 9: Adjust the offset to skip over the header section and reach the CPIO archive + offset += headerSize + + // Step 10: Create a new section reader starting at the offset (where the CPIO archive begins) + // The CPIO archive contains the actual files packed within the RPM package, but first it needs to be un-ZStandard compressed + var zStandardReader *io.SectionReader + fileInfo, err := file.Stat() + if err != nil { + logText("WARN", "Failed to get file information: "+err.Error(), false) + } else { + logText("INFO", "Reading CPIO archive from offset: "+strconv.Itoa(offset)+" from file of size: "+strconv.FormatInt(fileInfo.Size(), 10), false) + zStandardReader = io.NewSectionReader(file, int64(offset), fileInfo.Size()-int64(offset)) + } + + // Output the length of the CPIO archive + logText("INFO", "CPIO archive length: "+strconv.FormatInt(zStandardReader.Size(), 10), false) + + // Now we need to Un-ZStandard the data + decoder, err := zstd.NewReader(zStandardReader) + if err != nil { + return 0, err, 5 + } + defer decoder.Close() + + // Un-CPIO the ZStandard data + CPIOReader := cpio.NewReader(decoder) + + var filesWritten int +fileIteration: + for { + header, err := CPIOReader.Next() + switch { + case err == io.EOF: + break fileIteration + case err != nil: + return filesWritten, err, 6 + case header == nil: + continue + } + + target := filepath.Join("/", header.Name) + if header.Mode.IsDir() { + // It is a directory, so we need to create it if it doesn't already exist + logText("INFO", "Creating directory: "+target, false) + fileInfo, err := os.Stat(target) + if err != nil { + if os.IsNotExist(err) { + // We can't use header.Mode.Perm() because the types conflict, despite being copy-pasted from os.FileInfo() + err := os.MkdirAll(target, header.FileInfo().Mode().Perm()) + if err != nil { + return filesWritten, err, 7 + } + // Log the successful creation of the directory + logText("INFO", "Created directory: "+target+" with permissions "+header.FileInfo().Mode().Perm().String(), false) + } else { + // If the file does exist, but we cannot access it, return an error. It's probably a permissions issue. + return filesWritten, err, 8 + } + } else { + // If the file exists, but is not a directory, return an error, because we can't put files in a non-directory + if !fileInfo.IsDir() { + return filesWritten, err, 9 + } else { + // The directory already exists, so we don't need to do anything + logText("INFO", "Directory already exists: "+target, false) + } + } + } else { + var stop = false + continueFileIteration: + for !stop { + stop = true + // It is a file, so we need to create it + logText("INFO", "Extracting file: "+target, false) + fileInfo, err := os.Stat(target) + if err != nil && !os.IsNotExist(err) { + return filesWritten, err, 10 + } else if !os.IsNotExist(err) && fileInfo.IsDir() { + // If the file exists, but is a directory, return an error, because we can't overwrite a directory with a file + return filesWritten, err, 11 + } else if !os.IsNotExist(err) { + stopPromptRepeat: + for { + // Let's inform the logger that the file already exists and wait for the user to respond with a decision + logText("INFO", "File already exists: "+target, false) + response := logText("INFO", "Do you want to [o]verwrite, [r]ename, or [s]kip? (o/r/s)", true) + switch response { + case "o": + // Overwrite the file + err := os.Remove(target) + if err != nil { + return filesWritten, err, 12 + } + break stopPromptRepeat + case "r": + // Rename the file + target = target + ".new" + logText("INFO", "Renaming file to: "+target, false) + // We need to repeat the file iteration, because we need to check if the new file already exists + stop = false + continue continueFileIteration + case "s": + // Skip the file + break continueFileIteration + default: + // Invalid response, so we need to ask again + continue + } + } + } + + // Create directories if they don't exist + err = os.MkdirAll(filepath.Dir(target), 0755) + if err != nil { + return filesWritten, err, 7 + } + + file, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, header.FileInfo().Mode().Perm()) + if err != nil { + return filesWritten, err, 13 + } + + _, err = io.Copy(file, CPIOReader) + if err != nil { + return filesWritten, err, 14 + } + + // Manually close the file, because we need to check if it was closed successfully + err = file.Close() + if err != nil { + return filesWritten, err, 15 + } + + // Increment the number of files written + filesWritten++ + + // Log the successful extraction of the file + logText("INFO", "Extracted file: "+target+" successfully with permissions "+header.FileInfo().Mode().Perm().String(), false) + } + } + } + + err = file.Close() + if err != nil { + return 0, err, 4 + } + + return filesWritten, nil, 0 +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..8bd5359 --- /dev/null +++ b/main.go @@ -0,0 +1,96 @@ +package main + +import ( + "fmt" + "gitea.com/oreonproject/eonlite/library" + "github.com/fatih/color" + "os" + "strconv" +) + +func main() { + if len(os.Args) < 2 { + fmt.Println(color.GreenString("[INFO]"), "Usage: eonlite ") + return + } + + filesInstalled, err, errorCode := library.InstallRPM(os.Args[1], func(severity string, content string, prompt bool) string { + var severityPretty string + switch severity { + case "INFO": + severityPretty = color.GreenString("[INFO]") + case "WARN": + severityPretty = color.YellowString("[WARN]") + case "ERROR": + severityPretty = color.HiYellowString("[ERROR]") + case "CRITICAL": + severityPretty = color.HiRedString("[CRITICAL]") + case "FATAL": + severityPretty = color.RedString("[FATAL]") + } + fmt.Println(severityPretty, content) + if prompt { + fmt.Print(": ") + var userInput string + _, err := fmt.Scanln(&userInput) + if err != nil { + fmt.Println(color.RedString("[FATAL]"), "Failed to read user input:", err) + os.Exit(17) + } else { + return userInput + } + } + return "" + }) + if err != nil { + switch errorCode { + case 1: + fmt.Println(color.RedString("[FATAL]"), "Failed to open RPM", os.Args[1]) + case 2: + fmt.Println(color.RedString("[FATAL]"), "Failed to create buffer:", err) + case 3: + fmt.Println(color.RedString("[FATAL]"), "Failed to read RPM at offset:", err) + case 4: + fmt.Println(color.RedString("[FATAL]"), "Failed to close RPM:", err) + case 5: + fmt.Println(color.RedString("[FATAL]"), "Failed to create ZStandard decoder:", err) + case 6: + fmt.Println(color.RedString("[FATAL]"), "Failed to un-CPIO file number", strconv.Itoa(filesInstalled)+":", err) + case 7: + fmt.Println(color.RedString("[FATAL]"), "Failed to create directory (are you running as root?):", err) + fmt.Println(color.GreenString("[INFO]"), filesInstalled, "files were installed before the error occurred.") + case 8: + fmt.Println(color.RedString("[FATAL]"), "Failed to read directory (are you running as root?):", err) + fmt.Println(color.GreenString("[INFO]"), filesInstalled, "files were installed before the error occurred.") + case 9: + fmt.Println(color.RedString("[FATAL]"), "You cannot put a file into a non-directory. Another file may be conflicting with a directory name of this RPM.") + fmt.Println(color.GreenString("[INFO]"), filesInstalled, "files were installed before the error occurred.") + case 10: + fmt.Println(color.RedString("[FATAL]"), "Failed to read file (are you running as root?):", err) + fmt.Println(color.GreenString("[INFO]"), filesInstalled, "files were installed before the error occurred.") + case 11: + fmt.Println(color.RedString("[FATAL]"), "You cannot write a file where a directory already exists. A directory name may be conflicting with a file in this RPM.") + fmt.Println(color.GreenString("[INFO]"), filesInstalled, "files were installed before the error occurred.") + case 12: + fmt.Println(color.RedString("[FATAL]"), "Failed to remove file:", err) + fmt.Println(color.GreenString("[INFO]"), filesInstalled, "files were installed before the error occurred.") + case 13: + fmt.Println(color.RedString("[FATAL]"), "Failed open file for writing:", err) + fmt.Println(color.GreenString("[INFO]"), filesInstalled, "files were installed before the error occurred.") + case 14: + fmt.Println(color.RedString("[FATAL]"), "Failed to write file:", err) + fmt.Println(color.GreenString("[INFO]"), filesInstalled, "files were installed before the error occurred.") + case 15: + fmt.Println(color.RedString("[FATAL]"), "Failed to close file after writing:", err) + fmt.Println(color.GreenString("[INFO]"), filesInstalled, "files were installed before the error occurred.") + + default: + fmt.Println(color.RedString("[FATAL]"), "An impossible logic error has occurred. Please check if the laws of physics still apply, and if so, please move your computer to a location with less radiation, such as a lead nuclear bunker: value of", errorCode, "is not in library code") + errorCode = 16 + } + os.Exit(errorCode) + } else { + fmt.Println(color.GreenString("[INFO]"), "Installation complete! Installed", filesInstalled, "files") + os.Exit(errorCode) + } +}