diff --git a/go/clients/nodeengineclient/client.go b/go/clients/nodeengineclient/client.go index 0b1939f..b39982e 100644 --- a/go/clients/nodeengineclient/client.go +++ b/go/clients/nodeengineclient/client.go @@ -10,10 +10,11 @@ import ( "errors" "fmt" "regexp" + "strings" "time" seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/keys" ) const ( @@ -56,7 +57,7 @@ var ErrUnknownVersion = errors.New("node-engine: unknown compose-candidate answe var nameOK = regexp.MustCompile(`^[-/_=a-zA-Z0-9]+$`) func checkAsk(a seat.ComposeCandidateInput) error { - if _, _, err := delivery.ParseCompositionKey(a.Candidate); err != nil { + if _, _, err := ParseCompositionKey(a.Candidate); err != nil { return fmt.Errorf("node-engine: a compose-candidate ask names no candidate: %w", err) } return nil @@ -89,7 +90,7 @@ func CheckAnswer(a seat.ComposeCandidateAnswer) error { if a.Node == "" || !nameOK.MatchString(a.Node) { return fmt.Errorf("node-engine: the answer's node name %q is empty or holds a dot, a wildcard, whitespace or a character a key refuses", a.Node) } - if _, _, err := delivery.ParseCompositionKey(a.Candidate); err != nil { + if _, _, err := ParseCompositionKey(a.Candidate); err != nil { return err } if a.Took < 0 { @@ -181,3 +182,37 @@ func DecodeAnswer(data []byte) (seat.ComposeCandidateAnswer, error) { } return a, CheckAnswer(a) } + +var heads12 = regexp.MustCompile(`^[0-9a-f]{12}$`) + +// CompositionKey is the key of a candidate the node is asked to compose: the group's check at its heads, `.`, the group escaped as a key part and the heads hash 12 lowercase hex digits (the checker's verdict +// key of the same check). +func CompositionKey(group, headsHash string) (string, error) { + if group == "" { + return "", errors.New("node-engine: the group name is empty") + } + if !heads12.MatchString(headsHash) { + return "", fmt.Errorf("node-engine: %q is not a heads hash, 12 lowercase hex digits", headsHash) + } + return keys.EscapeName(group) + "." + headsHash, nil +} + +// ParseCompositionKey is the inverse of CompositionKey; it refuses a key that is not one, naming it. +func ParseCompositionKey(key string) (group, headsHash string, err error) { + i := strings.LastIndex(key, ".") + if i < 0 { + return "", "", fmt.Errorf("node-engine: %q is not a composition key, .", key) + } + part, headsHash := key[:i], key[i+1:] + if !heads12.MatchString(headsHash) { + return "", "", fmt.Errorf("node-engine: %q is not a composition key: %q is not a heads hash, 12 lowercase hex digits", key, headsHash) + } + if part == "" || strings.Contains(part, ".") { + return "", "", fmt.Errorf("node-engine: %q is not a composition key: the group part is empty or holds a dot", key) + } + if group, err = keys.UnescapeName(part); err != nil { + return "", "", fmt.Errorf("node-engine: %q is not a composition key: %w", key, err) + } + return group, headsHash, nil +} diff --git a/go/clients/nodeengineclient/client_test.go b/go/clients/nodeengineclient/client_test.go index 542b207..1586968 100644 --- a/go/clients/nodeengineclient/client_test.go +++ b/go/clients/nodeengineclient/client_test.go @@ -8,13 +8,12 @@ import ( "testing" seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // The verb compose-candidate's ask and answer (ADR 0593 §7, §9): each round-trips, and a reader refuses what it could // not take, naming it. func TestComposeCandidateRoundTripsAndRefuses(t *testing.T) { - key, err := delivery.CompositionKey("mesh-sdk#7", "0123456789ab") + key, err := CompositionKey("mesh-sdk#7", "0123456789ab") if err != nil { t.Fatal(err) } @@ -95,3 +94,22 @@ func TestComposeCandidateRoundTripsAndRefuses(t *testing.T) { } } } + +// A composition key round-trips its group and heads hash, and refuses what is not one. +func TestCompositionKeyRoundTripsAndRefuses(t *testing.T) { + key, err := CompositionKey("fix/1.2", "0123456789ab") + if err != nil { + t.Fatal(err) + } + if g, h, err := ParseCompositionKey(key); err != nil || g != "fix/1.2" || h != "0123456789ab" { + t.Fatalf("round trip: %q %q %v", g, h, err) + } + for _, bad := range []string{"", "nogroup", "g.0123456789AB", "g.short", ".0123456789ab", "a.b.0123456789ab"} { + if _, _, err := ParseCompositionKey(bad); err == nil { + t.Errorf("%q was taken for a composition key", bad) + } + } + if _, err := CompositionKey("", "0123456789ab"); err == nil { + t.Error("a key with no group was made") + } +}