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 }