diff --git a/go/clients/nodeengineclient/keys.go b/go/clients/nodeengineclient/keys.go new file mode 100644 index 0000000..edb9b0a --- /dev/null +++ b/go/clients/nodeengineclient/keys.go @@ -0,0 +1,122 @@ +package nodeengineclient + +import ( + "fmt" + "regexp" + "strings" + + "git.novox.be/novox/mesh-sdk/go/keys" +) + +// NodeWord is the last part of a node's own key in the states have and composed. +const NodeWord = "node" + +var moduleNameOK = regexp.MustCompile(`^[-/_a-zA-Z0-9]+(\.[-/_a-zA-Z0-9]+)*$`) + +// moduleKeyPart is a module's name as a key part (issue 1150): a name may hold a dot (`novox.be`), which a key part may not, +// so a dot is escaped as keys.EscapeName writes it (`novox=2ebe`). A name that is empty, or holds a wildcard, whitespace, +// `=`, or an empty part between dots, is refused. +func moduleKeyPart(module string) (string, error) { + if module == "" { + return "", fmt.Errorf("node-engine: the module name is empty") + } + if !moduleNameOK.MatchString(module) { + return "", fmt.Errorf("node-engine: the module name %q holds a wildcard, whitespace, '=', an empty part between dots or a character a key refuses", module) + } + return keys.EscapeName(module), nil +} + +// parseModuleKeyPart is the inverse of moduleKeyPart; it refuses a part that is not the one moduleKeyPart writes for its name. +func parseModuleKeyPart(part string) (string, error) { + if err := keys.CheckName("module", part); err != nil { + return "", err + } + module, err := keys.UnescapeName(part) + if err != nil { + return "", err + } + if back, err := moduleKeyPart(module); err != nil || back != part { + return "", fmt.Errorf("node-engine: the module part %q is not a module name as a key writes it", part) + } + return module, nil +} + +// HaveKey is the key, in the state have, of a module's have on a node: `.`. A module named `node` is refused, +// since `.node` is the node's own facts. +func HaveKey(node, module string) (string, error) { + if err := keys.CheckName("node", node); err != nil { + return "", err + } + part, err := moduleKeyPart(module) + if err != nil { + return "", err + } + if module == NodeWord { + return "", fmt.Errorf(`node-engine: the module name "node" is refused: %s.node is the node's own key`, node) + } + return node + "." + part, nil +} + +// ParseHaveKey is the inverse of HaveKey and of the node's own key: isNode is true for `.node`, with module empty. +func ParseHaveKey(key string) (node, module string, isNode bool, err error) { + node, module, ok := strings.Cut(key, ".") + if !ok { + return "", "", false, fmt.Errorf("node-engine: %q is not a have key, .", key) + } + if err := keys.CheckName("node", node); err != nil { + return "", "", false, fmt.Errorf("node-engine: %q is not a have key: %w", key, err) + } + if module == NodeWord { + return node, "", true, nil + } + if module, err = parseModuleKeyPart(module); err != nil { + return "", "", false, fmt.Errorf("node-engine: %q is not a have key: %w", key, err) + } + return node, module, false, nil +} + +// ComposedKey is the key, in the state composed, of a module's composition for a node at a fingerprint: +// `..`, the hex being the fingerprint's key part (64 lowercase hex digits, the mesh-catalog client's +// KeyPart). A module named `node` is refused. +func ComposedKey(node, module, fingerprintPart string) (string, error) { + have, err := HaveKey(node, module) + if err != nil { + return "", err + } + if !keys.IsHex64(fingerprintPart) { + return "", fmt.Errorf("node-engine: %q is not the key part of a fingerprint, 64 lowercase hex digits", fingerprintPart) + } + return have + "." + fingerprintPart, nil +} + +// ParseComposedKey is the inverse of ComposedKey and of the node's own key `.node` (isNode, with module and +// fingerprintPart empty). It refuses any other shape, naming the key. +func ParseComposedKey(key string) (node, module, fingerprintPart string, isNode bool, err error) { + refused := func(why error) (string, string, string, bool, error) { + return "", "", "", false, fmt.Errorf("node-engine: %q is not a composition key, .. or .node: %w", key, why) + } + parts := strings.Split(key, ".") + switch len(parts) { + case 2: + if parts[1] != NodeWord { + return refused(fmt.Errorf("a module's composition names its fingerprint")) + } + if err := keys.CheckName("node", parts[0]); err != nil { + return refused(err) + } + return parts[0], "", "", true, nil + case 3: + node, module, isNode, err := ParseHaveKey(parts[0] + "." + parts[1]) + if err != nil { + return refused(err) + } + if isNode { + return refused(fmt.Errorf("the node's own key names no fingerprint")) + } + if !keys.IsHex64(parts[2]) { + return refused(fmt.Errorf("%q is not 64 lowercase hex digits", parts[2])) + } + return node, module, parts[2], false, nil + } + return refused(fmt.Errorf("the key has %d parts", len(parts))) +} diff --git a/go/clients/nodeengineclient/keys_test.go b/go/clients/nodeengineclient/keys_test.go new file mode 100644 index 0000000..d8500b1 --- /dev/null +++ b/go/clients/nodeengineclient/keys_test.go @@ -0,0 +1,45 @@ +package nodeengineclient + +import ( + "strings" + "testing" +) + +func TestTheHaveAndComposedKeysKeepTheirForms(t *testing.T) { + fp := strings.Repeat("ab", 32) + for module, want := range map[string]string{"gitea": "n1.gitea", "novox.be": "n1.novox=2ebe", "a/b": "n1.a/b"} { + key, err := HaveKey("n1", module) + if err != nil || key != want { + t.Errorf("HaveKey(%q) = %q, %v", module, key, err) + } + if node, back, isNode, err := ParseHaveKey(key); err != nil || node != "n1" || back != module || isNode { + t.Errorf("ParseHaveKey(%q) = %q %q %v %v", key, node, back, isNode, err) + } + ck, err := ComposedKey("n1", module, fp) + if err != nil || ck != want+"."+fp { + t.Errorf("ComposedKey(%q) = %q, %v", module, ck, err) + } + if node, back, part, isNode, err := ParseComposedKey(ck); err != nil || node != "n1" || back != module || part != fp || isNode { + t.Errorf("ParseComposedKey(%q) = %q %q %q %v %v", ck, node, back, part, isNode, err) + } + } + if node, module, isNode, err := ParseHaveKey("n1.node"); err != nil || node != "n1" || module != "" || !isNode { + t.Errorf("the node's own have key: %q %q %v %v", node, module, isNode, err) + } + if node, _, _, isNode, err := ParseComposedKey("n1.node"); err != nil || node != "n1" || !isNode { + t.Errorf("the node's own composition key: %q %v %v", node, isNode, err) + } + for _, bad := range [][2]string{{"", "m"}, {"n.1", "m"}, {"n1", ""}, {"n1", "node"}, {"n1", "a b"}, {"n1", "a=b"}, {"n1", "a..b"}} { + if _, err := HaveKey(bad[0], bad[1]); err == nil { + t.Errorf("HaveKey(%q, %q) keys", bad[0], bad[1]) + } + } + if _, err := ComposedKey("n1", "m", "src2:"+fp); err == nil { + t.Error("a fingerprint with its prefix is a key part") + } + for _, bad := range []string{"n1", "n1.m", "n1.m." + fp[:10], "n1.node." + fp, "n1.m." + fp + ".x"} { + if _, _, _, _, err := ParseComposedKey(bad); err == nil { + t.Errorf("ParseComposedKey(%q) reads a wrong key", bad) + } + } +}