Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add relation fields #22

Merged
merged 12 commits into from
Oct 3, 2019
2 changes: 2 additions & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ go_library(
"tags.go",
"tar.go",
"file_types.go",
"sense.go"
],
importpath = "github.com/google/rpmpack",
visibility = ["//visibility:public"],
Expand All @@ -35,6 +36,7 @@ go_test(
"header_test.go",
"tar_test.go",
"file_types_test.go",
"sense_test.go",
],
data = glob(["testdata/**"]),
embed = [":go_default_library"],
Expand Down
18 changes: 18 additions & 0 deletions cmd/tar2rpm/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ import (
)

var (
provides,
obsoletes,
suggests,
recommends,
requires,
conflicts rpmpack.Relations
name = flag.String("name", "", "the package name")
version = flag.String("version", "", "the package version")
release = flag.String("release", "", "the rpm release")
Expand Down Expand Up @@ -55,6 +61,12 @@ Options:
}

func main() {
flag.Var(&provides, "provides", "rpm provides values, can be just name or in the form of name=version (eg. bla=1.2.3)")
flag.Var(&obsoletes, "obsoletes", "rpm obsoletes values, can be just name or in the form of name=version (eg. bla=1.2.3)")
flag.Var(&suggests, "suggests", "rpm suggests values, can be just name or in the form of name=version (eg. bla=1.2.3)")
flag.Var(&recommends, "recommends", "rpm recommends values, can be just name or in the form of name=version (eg. bla=1.2.3)")
flag.Var(&requires, "requires", "rpm requires values, can be just name or in the form of name=version (eg. bla=1.2.3)")
flag.Var(&conflicts, "conflicts", "rpm provides values, can be just name or in the form of name=version (eg. bla=1.2.3)")
flag.Usage = usage
flag.Parse()
if *name == "" || *version == "" {
Expand Down Expand Up @@ -104,6 +116,12 @@ func main() {
Licence: *licence,
Description: *description,
Compressor: *compressor,
Provides: provides,
Obsoletes: obsoletes,
Suggests: suggests,
Recommends: recommends,
Requires: requires,
Conflicts: conflicts,
})
r.AddPrein(*prein)
r.AddPostin(*postin)
Expand Down
52 changes: 46 additions & 6 deletions rpm.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ type RPMMetaData struct {
Packager,
Licence,
Compressor string
Provides,
Obsoletes,
Suggests,
Recommends,
Requires,
Conflicts Relations
}

// RPM holds the state of a particular rpm file. Please use NewRPM to instantiate it.
Expand Down Expand Up @@ -102,14 +108,24 @@ func NewRPM(m RPMMetaData) (*RPM, error) {
if err != nil {
return nil, errors.Wrap(err, "failed to create compression writer")
}
return &RPM{

rpm := &RPM{
RPMMetaData: m,
di: newDirIndex(),
payload: p,
compressedPayload: z,
cpio: cpio.NewWriter(z),
files: make(map[string]RPMFile),
}, nil
}

// A package must provide itself...
rpm.Provides.addIfMissing(&Relation{
Name: rpm.Name,
Version: rpm.FullVersion(),
Sense: SenseEqual,
})

return rpm, nil
}

// FullVersion properly combines version and release fields to a version string
Expand Down Expand Up @@ -151,6 +167,10 @@ func (r *RPM) Write(w io.Writer) error {
h := newIndex(immutable)
r.writeGenIndexes(h)
r.writeFileIndexes(h)
if err := r.writeRelationIndexes(h); err != nil {
return err
}

hb, err := h.Bytes()
if err != nil {
return errors.Wrap(err, "failed to retrieve header")
Expand Down Expand Up @@ -185,6 +205,30 @@ func (r *RPM) writeSignatures(sigHeader *index, regHeader []byte) {
sigHeader.Add(sigPayloadSize, entry([]int32{int32(r.payloadSize)}))
}

func (r *RPM) writeRelationIndexes(h *index) error {
// add all relation categories
if err := r.Provides.AddToIndex(h, tagProvides, tagProvideVersion, tagProvideFlags); err != nil {
return errors.Wrap(err, "failed to add provides")
}
if err := r.Obsoletes.AddToIndex(h, tagObsoletes, tagObsoleteVersion, tagObsoleteFlags); err != nil {
return errors.Wrap(err, "failed to add obsoletes")
}
if err := r.Suggests.AddToIndex(h, tagSuggests, tagSuggestVersion, tagSuggestFlags); err != nil {
return errors.Wrap(err, "failed to add suggests")
}
if err := r.Recommends.AddToIndex(h, tagRecommends, tagRecommendVersion, tagRecommendFlags); err != nil {
return errors.Wrap(err, "failed to add recommends")
}
if err := r.Requires.AddToIndex(h, tagRequires, tagRequireVersion, tagRequireFlags); err != nil {
return errors.Wrap(err, "failed to add requires")
}
if err := r.Conflicts.AddToIndex(h, tagConflicts, tagConflictVersion, tagConflictFlags); err != nil {
return errors.Wrap(err, "failed to add conflicts")
}

return nil
}

func (r *RPM) writeGenIndexes(h *index) {
h.Add(tagHeaderI18NTable, entry("C"))
h.Add(tagSize, entry([]int32{int32(r.payloadSize)}))
Expand All @@ -203,10 +247,6 @@ func (r *RPM) writeGenIndexes(h *index) {
h.Add(tagPackager, entry(r.Packager))
h.Add(tagURL, entry(r.URL))

// A package must provide itself...
h.Add(tagProvides, entry([]string{r.Name}))
h.Add(tagProvideVersion, entry([]string{r.FullVersion()}))
h.Add(tagProvideFlags, entry([]uint32{uint32(1 << 3)})) // means "="
// rpm utilities look for the sourcerpm tag to deduce if this is not a source rpm (if it has a sourcerpm,
// it is NOT a source rpm).
h.Add(tagSourceRPM, entry(fmt.Sprintf("%s-%s.src.rpm", r.Name, r.FullVersion())))
Expand Down
162 changes: 162 additions & 0 deletions sense.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package rpmpack

import (
"fmt"
"regexp"
)

type rpmSense uint32

// SenseAny specifies no specific version compare
// SenseLess specifies less then the specified version
// SenseGreater specifies greater then the specified version
// SenseEqual specifies equal to the specified version
const (
SenseAny rpmSense = 1 << iota >> 1
SenseLess
SenseGreater
SenseEqual
)

var relationMatch = regexp.MustCompile(`([^=<>\s]*)\s*((?:=|>|<)*)\s*(.*)?`)

// Relation is the structure of rpm sense relationships
type Relation struct {
Name string
Version string
Sense rpmSense
}

// String return the string representation of the Relation
func (r *Relation) String() string {
return fmt.Sprintf("%s%v%s", r.Name, r.Sense, r.Version)
}

// Equal compare the equality of two relations
func (r *Relation) Equal(o *Relation) bool {
return r.Name == o.Name && r.Version == o.Version && r.Sense == o.Sense
}

// Relations is a slice of Relation pointers
type Relations []*Relation

// String return the string representation of the Relations
func (r *Relations) String() string {
var (
val string
total = len(*r)
)

for idx, relation := range *r {
val += relation.String()
if idx < total-1 {
val += ","
}
}
djgilcrease marked this conversation as resolved.
Show resolved Hide resolved

return val
}
djgilcrease marked this conversation as resolved.
Show resolved Hide resolved

// Set parse a string into a Relation and append it to the Relations slice if it is missing
// this is used by the flag package
func (r *Relations) Set(value string) error {
relation, err := NewRelation(value)
if err != nil {
return err
}
r.addIfMissing(relation)
jarondl marked this conversation as resolved.
Show resolved Hide resolved

return nil
}

func (r *Relations) addIfMissing(value *Relation) {
for _, relation := range *r {
if relation.Equal(value) {
return
}
}

*r = append(*r, value)
}

// AddToIndex add the relations to the specified category on the index
djgilcrease marked this conversation as resolved.
Show resolved Hide resolved
func (r *Relations) AddToIndex(h *index, nameTag, versionTag, flagsTag int) error {
var (
num = len(*r)
names = make([]string, num)
versions = make([]string, num)
flags = make([]uint32, num)
)

if num == 0 {
return nil
}

for idx := range *r {
djgilcrease marked this conversation as resolved.
Show resolved Hide resolved
relation := (*r)[idx]
names[idx] = relation.Name
versions[idx] = relation.Version
flags[idx] = uint32(relation.Sense)
}

h.Add(nameTag, entry(names))
h.Add(versionTag, entry(versions))
h.Add(flagsTag, entry(flags))

return nil
}

// NewRelation parse a string into a Relation
func NewRelation(related string) (*Relation, error) {
var (
err error
sense rpmSense
)
parts := relationMatch.FindStringSubmatch(related)
if sense, err = parseSense(parts[2]); err != nil {
return nil, err
}

return &Relation{
Name: parts[1],
Version: parts[3],
Sense: sense,
}, nil
}

var stringToSense = map[string]rpmSense{
"": SenseAny,
"<": SenseLess,
">": SenseGreater,
"=": SenseEqual,
"<=": SenseLess | SenseEqual,
">=": SenseGreater | SenseEqual,
}

// String return the string representation of the rpmSense
func (r rpmSense) String() string {
var (
val rpmSense
ret string
)

for ret, val = range stringToSense {
if r == val {
return ret
}
}

return "unknown"
}

func parseSense(sense string) (rpmSense, error) {
djgilcrease marked this conversation as resolved.
Show resolved Hide resolved
var (
ret rpmSense
ok bool
)
if ret, ok = stringToSense[sense]; !ok {
return SenseAny, fmt.Errorf("unknown sense value: %s", sense)
}

return ret, nil
}
81 changes: 81 additions & 0 deletions sense_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package rpmpack

import (
"testing"
)

func TestNewRelation(t *testing.T) {
testCases := []struct {
input, output string
errExpected bool
}{
{
input: "python >= 3.7",
output: "python>=3.7",
},
{
input: "python",
output: "python",
},
{
input: "python=2",
output: "python=2",
},
{
input: "python >=3.5",
output: "python>=3.5",
},
{
input: "python >< 3.5",
output: "",
errExpected: true,
},
{
input: "python <> 3.5",
output: "",
errExpected: true,
},
{
input: "python == 3.5",
output: "",
errExpected: true,
},
{
input: "python =< 3.5",
output: "",
errExpected: true,
},
{
input: "python => 3.5",
output: "",
errExpected: true,
},
}

for _, tc := range testCases {
testCase := tc
t.Run(testCase.input, func(tt *testing.T) {
relation, err := NewRelation(testCase.input)
switch {
case testCase.errExpected && err == nil:
tt.Errorf("%s should have returned an error", testCase.input)
return
case !testCase.errExpected && err != nil:
tt.Errorf("%s should not have returned an error: %v", testCase.input, err)
return
case testCase.errExpected && err != nil:
return
}

if relation == nil {
tt.Errorf("%s should not have returned a nil relation", testCase.input)
return
}

val := relation.String()
if !testCase.errExpected && val != testCase.output {
tt.Errorf("%s should have returned %s not %s", testCase.input, testCase.output, val)
}
})
}
}
Loading