eonlite/library/main.go

206 lines
6.9 KiB
Go
Raw Permalink Normal View History

2024-08-18 20:20:22 +01:00
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
}