Initial commit
This commit is contained in:
parent
f906e1a194
commit
52755b9593
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue