Created
December 3, 2015 10:32
-
-
Save cavaliercoder/7a34bf380248bc2404c4 to your computer and use it in GitHub Desktop.
go-rpm - a naive rpm implementation in go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// see: | |
// https://github.com/rpm-software-management/rpm/tree/master/lib | |
// http://www.rpm.org/max-rpm/s1-rpm-file-format-rpm-file-format.html | |
package main | |
import ( | |
"bytes" | |
"fmt" | |
"math" | |
"os" | |
"time" | |
) | |
type RPMFile struct { | |
Metadata RPMFileMetadata | |
Lead RPMFileLeadSection | |
Headers Headers | |
} | |
type RPMFileMetadata struct { | |
FilePath string | |
} | |
type RPMFileLeadSection struct { | |
VersionMajor int | |
VersionMinor int | |
Name string | |
Type int | |
Architecture int | |
OperatingSystem int | |
SignatureType int | |
} | |
type Header struct { | |
Version int | |
IndexCount int | |
Length int64 | |
Indexes IndexEntries | |
} | |
type Headers []Header | |
type IndexEntry struct { | |
Tag int64 | |
Type int64 | |
Offset int64 | |
ItemCount int64 | |
Value interface{} | |
} | |
type IndexEntries []IndexEntry | |
const ( | |
IndexDataTypeNull int64 = iota | |
IndexDataTypeChar | |
IndexDataTypeInt8 | |
IndexDataTypeInt16 | |
IndexDataTypeInt32 | |
IndexDataTypeInt64 | |
IndexDataTypeString | |
IndexDataTypeBinary | |
IndexDataTypeStringArray | |
IndexDataTypeI8NString | |
) | |
var HeaderTagNames = map[int64]string{ | |
1000: "RPMTAG_NAME", | |
1001: "RPMTAG_VERSION", | |
1002: "RPMTAG_RELEASE", | |
1003: "RPMTAG_SERIAL", | |
1004: "RPMTAG_SUMMARY", | |
1005: "RPMTAG_DESCRIPTION", | |
1006: "RPMTAG_BUILDTIME", | |
1007: "RPMTAG_BUILDHOST", | |
1008: "RPMTAG_INSTALLTIME", | |
1009: "RPMTAG_SIZE", | |
1010: "RPMTAG_DISTRIBUTION", | |
1011: "RPMTAG_VENDOR", | |
1012: "RPMTAG_GIF", | |
1013: "RPMTAG_XPM", | |
1014: "RPMTAG_COPYRIGHT", | |
1015: "RPMTAG_PACKAGER", | |
1016: "RPMTAG_GROUP", | |
1017: "RPMTAG_CHANGELOG", | |
1018: "RPMTAG_SOURCE", | |
1019: "RPMTAG_PATCH", | |
1020: "RPMTAG_URL", | |
1021: "RPMTAG_OS", | |
1022: "RPMTAG_ARCH", | |
1023: "RPMTAG_PREIN", | |
1024: "RPMTAG_POSTIN", | |
1025: "RPMTAG_PREUN", | |
1026: "RPMTAG_POSTUN", | |
1027: "RPMTAG_FILENAMES", | |
1028: "RPMTAG_FILESIZES", | |
1029: "RPMTAG_FILESTATES", | |
1030: "RPMTAG_FILEMODES", | |
1031: "RPMTAG_FILEUIDS", | |
1032: "RPMTAG_FILEGIDS", | |
1033: "RPMTAG_FILERDEVS", | |
1034: "RPMTAG_FILEMTIMES", | |
1035: "RPMTAG_FILEMD5S", | |
1036: "RPMTAG_FILELINKTOS", | |
1037: "RPMTAG_FILEFLAGS", | |
1038: "RPMTAG_ROOT", | |
1039: "RPMTAG_FILEUSERNAME", | |
1040: "RPMTAG_FILEGROUPNAME", | |
1041: "RPMTAG_EXCLUDE", | |
1042: "RPMTAG_EXCLUSIVE", | |
1043: "RPMTAG_ICON", | |
1044: "RPMTAG_SOURCERPM", | |
1045: "RPMTAG_FILEVERIFYFLAGS", | |
1046: "RPMTAG_ARCHIVESIZE", | |
1047: "RPMTAG_PROVIDES", | |
1048: "RPMTAG_REQUIREFLAGS", | |
1049: "RPMTAG_REQUIRENAME", | |
1050: "RPMTAG_REQUIREVERSION", | |
1051: "RPMTAG_NOSOURCE", | |
1052: "RPMTAG_NOPATCH", | |
1053: "RPMTAG_CONFLICTFLAGS", | |
1054: "RPMTAG_CONFLICTNAME", | |
1055: "RPMTAG_CONFLICTVERSION", | |
1056: "RPMTAG_DEFAULTPREFIX", | |
1057: "RPMTAG_BUILDROOT", | |
1058: "RPMTAG_INSTALLPREFIX", | |
1059: "RPMTAG_EXCLUDEARCH", | |
1060: "RPMTAG_EXCLUDEOS", | |
1061: "RPMTAG_EXCLUSIVEARCH", | |
1062: "RPMTAG_EXCLUSIVEOS", | |
1063: "RPMTAG_AUTOREQPROV", | |
1064: "RPMTAG_RPMVERSION", | |
1065: "RPMTAG_TRIGGERSCRIPTS", | |
1066: "RPMTAG_TRIGGERNAME", | |
1067: "RPMTAG_TRIGGERVERSION", | |
1068: "RPMTAG_TRIGGERFLAGS", | |
1069: "RPMTAG_TRIGGERINDEX", | |
1079: "RPMTAG_VERIFYSCRIPT", | |
} | |
func ReadRPMFile(path string) (*RPMFile, error) { | |
rpmfile := &RPMFile{} | |
// open file | |
f, err := os.Open(path) | |
if err != nil { | |
return nil, fmt.Errorf("Error opening RPM file: %s", err) | |
} | |
defer f.Close() | |
// read the deprecated "lead" | |
lead := make([]byte, 96) | |
n, err := f.Read(lead) | |
if err != nil { | |
return nil, fmt.Errorf("Error reading RPM Lead section: %s", err) | |
} | |
if n != 96 { | |
return nil, fmt.Errorf("RPM Lead section is incorrect length") | |
} | |
// check magic number | |
if 0 != bytes.Compare(lead[:4], []byte{0xED, 0xAB, 0xEE, 0xDB}) { | |
return nil, fmt.Errorf("RPM file descriptor is invalid") | |
} | |
// translate lead | |
rpmfile.Lead.VersionMajor = int(lead[5]) | |
rpmfile.Lead.VersionMinor = int(lead[6]) | |
rpmfile.Lead.Type = (int(lead[7]) << 8) + int(lead[8]) | |
rpmfile.Lead.Architecture = (int(lead[9]) << 8) + int(lead[10]) | |
rpmfile.Lead.Name = string(lead[10:77]) | |
rpmfile.Lead.OperatingSystem = (int(lead[76]) << 8) + int(lead[77]) | |
rpmfile.Lead.SignatureType = (int(lead[78]) << 8) + int(lead[79]) | |
// TODO: validate lead value ranges | |
// parse headers | |
rpmfile.Headers = make(Headers, 0) | |
// TODO: find last header without using hard limit of 2 | |
for i := 1; i < 3; i++ { | |
// read the "header structure header" | |
header := make([]byte, 16) | |
n, err = f.Read(header) | |
if err != nil { | |
return nil, fmt.Errorf("Error reading RPM structure header for header %d: %s", err, i) | |
} | |
if n != 16 { | |
return nil, fmt.Errorf("Error reading RPM structure header for header %d: only %d bytes returned", i, n) | |
} | |
// check magic number | |
if 0 != bytes.Compare(header[:3], []byte{0x8E, 0xAD, 0xE8}) { | |
return nil, fmt.Errorf("RPM header %d is invalid", i) | |
} | |
// translate header | |
h := Header{} | |
h.Version = int(header[3]) | |
h.IndexCount = (int(header[8]) << 24) + (int(header[9]) << 16) + (int(header[10]) << 8) + int(header[11]) | |
h.Length = (int64(header[12]) << 24) + (int64(header[13]) << 16) + (int64(header[14]) << 8) + int64(header[15]) | |
h.Indexes = make(IndexEntries, h.IndexCount) | |
// read indexes | |
indexLength := 16 * h.IndexCount | |
indexes := make([]byte, indexLength) | |
n, err = f.Read(indexes) | |
if err != nil { | |
return nil, fmt.Errorf("Error reading index entries for header %d: %s", i, err) | |
} | |
if n != indexLength { | |
return nil, fmt.Errorf("Error reading index entries for header %d: only %d bytes returned", i, n) | |
} | |
for x := 0; x < h.IndexCount; x++ { | |
o := 16 * x | |
index := IndexEntry{} | |
index.Tag = (int64(indexes[o]) << 24) + (int64(indexes[o+1]) << 16) + (int64(indexes[o+2]) << 8) + int64(indexes[o+3]) | |
index.Type = (int64(indexes[o+4]) << 24) + (int64(indexes[o+5]) << 16) + (int64(indexes[o+6]) << 8) + int64(indexes[o+7]) | |
index.Offset = (int64(indexes[o+8]) << 24) + (int64(indexes[o+9]) << 16) + (int64(indexes[o+10]) << 8) + int64(indexes[o+11]) | |
index.ItemCount = (int64(indexes[o+12]) << 24) + (int64(indexes[o+13]) << 16) + (int64(indexes[o+14]) << 8) + int64(indexes[o+15]) | |
h.Indexes[x] = index | |
} | |
// read the "store" | |
store := make([]byte, h.Length) | |
n, err = f.Read(store) | |
if err != nil { | |
return nil, fmt.Errorf("Error reading store for header %d: %s", i, err) | |
} | |
if int64(n) != h.Length { | |
return nil, fmt.Errorf("Error reading store for header %d: only %d bytes returned", i, n) | |
} | |
for x := 0; x < h.IndexCount; x++ { | |
index := h.Indexes[x] | |
switch index.Type { | |
case IndexDataTypeChar: | |
index.Value = uint8(store[index.Offset]) | |
break | |
case IndexDataTypeInt8: | |
index.Value = int8(store[index.Offset]) | |
break | |
case IndexDataTypeInt16: | |
index.Value = (int16(store[index.Offset]) << 8) + int16(store[index.Offset+1]) | |
break | |
case IndexDataTypeInt32: | |
index.Value = (int32(store[index.Offset]) << 24) + (int32(store[index.Offset+1]) << 16) + (int32(store[index.Offset+2]) << 8) + int32(store[index.Offset+3]) | |
break | |
case IndexDataTypeInt64: | |
index.Value = (int64(store[index.Offset]) << 56) + (int64(store[index.Offset+1]) << 48) + (int64(store[index.Offset+2]) << 40) + (int64(store[index.Offset+3]) << 32) + (int64(store[index.Offset+4]) << 24) + (int64(store[index.Offset+5]) << 16) + (int64(store[index.Offset+6]) << 8) + int64(store[index.Offset+7]) | |
break | |
case IndexDataTypeString: | |
// calculate string length | |
var j int64 | |
for j = 0; store[int64(j)+index.Offset] != 0; j++ { | |
} | |
index.Value = string(store[index.Offset : index.Offset+int64(j)]) | |
break | |
case IndexDataTypeBinary: | |
b := make([]byte, index.ItemCount) | |
copy(b, store[index.Offset:index.Offset+index.ItemCount]) | |
index.Value = b | |
break | |
case IndexDataTypeStringArray, IndexDataTypeI8NString: | |
vs := make([]string, index.ItemCount) | |
o := index.Offset | |
for s := 0; int64(s) < index.ItemCount; s++ { | |
// calculate string length | |
var j int64 | |
for j = 0; store[int64(j)+o] != 0; j++ { | |
} | |
vs[s] = string(store[o : o+j]) | |
o += j + 1 | |
} | |
index.Value = vs | |
break | |
} | |
// save in array | |
h.Indexes[x] = index | |
} | |
// add header | |
rpmfile.Headers = append(rpmfile.Headers, h) | |
// calculate location of next header by padding to a multiple of 8 | |
o := 8 - int64(math.Mod(float64(h.Length), 8)) | |
// seek to next header | |
_, err := f.Seek(o, 1) | |
if err != nil { | |
return nil, err | |
} | |
} | |
return rpmfile, nil | |
} | |
func (c IndexEntries) Get(tag int64) *IndexEntry { | |
for _, e := range c { | |
if e.Tag == tag { | |
return &e | |
} | |
} | |
return nil | |
} | |
func (c *RPMFile) String() string { | |
return fmt.Sprintf("%v", c.BuildTime()) | |
} | |
func (c *RPMFile) Name() string { | |
return c.Headers[1].Indexes.Get(1000).Value.(string) | |
} | |
func (c *RPMFile) Version() string { | |
return c.Headers[1].Indexes.Get(1001).Value.(string) | |
} | |
func (c *RPMFile) BuildTime() time.Time { | |
return time.Unix(int64(c.Headers[1].Indexes.Get(1006).Value.(int32)), 0) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"io/ioutil" | |
"path/filepath" | |
"testing" | |
) | |
func TestReadRPMFile(t *testing.T) { | |
dir := "/var/www/html/pub/centos/7/updates/x86_64/" | |
// list RPM files | |
files, err := ioutil.ReadDir(dir) | |
if err != nil { | |
t.Fatalf(err.Error()) | |
} | |
valid := 0 | |
for _, f := range files { | |
path := filepath.Join(dir, f.Name()) | |
rpmfile, err := ReadRPMFile(path) | |
if err != nil { | |
t.Errorf("Error loading RPM file %s: %s", f.Name(), err) | |
} else { | |
valid++ | |
t.Logf("Validated package: %s", rpmfile) | |
} | |
} | |
if valid == 0 { | |
t.Errorf("No RPM files found for validation in %s", dir) | |
} else { | |
t.Logf("Validated %d RPM files", valid) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment