206 lines
6.9 KiB
Go
206 lines
6.9 KiB
Go
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
|
|
}
|