Initial commit

This commit is contained in:
Tracker-Friendly 2024-08-18 20:20:22 +01:00
parent f906e1a194
commit 52755b9593
4 changed files with 331 additions and 0 deletions

15
go.mod Normal file
View File

@ -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
)

15
go.sum Normal file
View File

@ -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=

205
library/main.go Normal file
View File

@ -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
}

96
main.go Normal file
View File

@ -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 <path to rpm>")
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)
}
}