From e8ae3ec8d3cf6a3eb93b0872c57c1ba2a2267cb4 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 13:42:59 +0100 Subject: [PATCH] =?UTF-8?q?go/keys:=20the=20version=20of=20a=20kept=20valu?= =?UTF-8?q?e=20and=20the=20key-part=20escaping=20that=20more=20than=20one?= =?UTF-8?q?=20domain=20shares=20(ADR=200634=20=C2=A74;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/keys/keys.go | 59 ++++++++++++++++++++++++++++++++++++++++ go/keys/keys_test.go | 29 ++++++++++++++++++++ go/packages_list_test.go | 2 +- 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 go/keys/keys.go create mode 100644 go/keys/keys_test.go diff --git a/go/keys/keys.go b/go/keys/keys.go new file mode 100644 index 0000000..489b37c --- /dev/null +++ b/go/keys/keys.go @@ -0,0 +1,59 @@ +// Package keys holds what the mesh's kept values of more than one domain share: the version a kept value carries and +// the escaping of a name into a key part (novox/hq ADR 0634 §1, §4; they were go/delivery's and are used by the +// Delivery, Placement and Identity and access domains alike). It names no seat and no module. +package keys + +import ( + "encoding/hex" + "fmt" + "regexp" + "strings" +) + +// Version is the version of every kept value that carries one and the only one its readers read. +const Version = 1 + +var nameOK = regexp.MustCompile(`^[-/_=a-zA-Z0-9]+$`) + +// EscapeName is name as a key part: a character outside letters, digits, `-`, `/` and `_` is written `=` and its +// two lowercase hex digits per byte, and `=` itself is escaped, so any non-empty name keys and a name of allowed +// characters only keys as it is. UnescapeName is its inverse. +func EscapeName(name string) string { + var b strings.Builder + for i := 0; i < len(name); i++ { + c := name[i] + switch { + case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9', c == '-', c == '/', c == '_': + b.WriteByte(c) + default: + fmt.Fprintf(&b, "=%02x", c) + } + } + return b.String() +} + +// UnescapeName is the inverse of EscapeName; it refuses a part with a bare `=` or any character EscapeName leaves +// escaped. +func UnescapeName(part string) (string, error) { + var out []byte + for i := 0; i < len(part); i++ { + c := part[i] + if c != '=' { + if !nameOK.MatchString(string(c)) { + return "", fmt.Errorf("keys: %q holds %q, which a key part escapes", part, c) + } + out = append(out, c) + continue + } + if i+3 > len(part) { + return "", fmt.Errorf("keys: %q ends in an incomplete escape", part) + } + v, err := hex.DecodeString(part[i+1 : i+3]) + if err != nil || part[i+1:i+3] != strings.ToLower(part[i+1:i+3]) { + return "", fmt.Errorf("keys: %q holds a malformed escape %q", part, part[i:i+3]) + } + out = append(out, v[0]) + i += 2 + } + return string(out), nil +} diff --git a/go/keys/keys_test.go b/go/keys/keys_test.go new file mode 100644 index 0000000..25ed7f3 --- /dev/null +++ b/go/keys/keys_test.go @@ -0,0 +1,29 @@ +package keys + +import ( + "strings" + "testing" +) + +func TestNamesEscapeIntoKeyPartsAndBack(t *testing.T) { + for _, c := range []struct{ name, esc string }{ + {"fix/1.2", "fix/1=2e2"}, {"a.b", "a=2eb"}, {"a=b", "a=3db"}, {"a+b", "a=2bb"}, {"plain-name_1/x", "plain-name_1/x"}, + {"fix/ü", "fix/=c3=bc"}, {"x y", "x=20y"}, + } { + esc := EscapeName(c.name) + if esc != c.esc || strings.ContainsAny(esc, ".+ ") { + t.Errorf("escape of %q = %q, want %q", c.name, esc, c.esc) + } + if back, err := UnescapeName(esc); err != nil || back != c.name { + t.Errorf("unescape(%q) = %q, %v; want %q", esc, back, err, c.name) + } + } + for _, bad := range []string{"a.b", "a=", "a=2", "a=zz", "a=2E", "a b"} { + if got, err := UnescapeName(bad); err == nil { + t.Errorf("unescape(%q) = %q, accepted", bad, got) + } + } + if Version != 1 { + t.Errorf("Version = %d: a kept value's readers read the one version", Version) + } +} diff --git a/go/packages_list_test.go b/go/packages_list_test.go index aa60388..66df9f7 100644 --- a/go/packages_list_test.go +++ b/go/packages_list_test.go @@ -24,7 +24,7 @@ import ( // request: does every module share it, or is it a shape more than one part reads? var meshSDKPackages = []string{ ".", "announce", "announcetest", "buildstep", "buscalls", "buscompose", "busstate", "busretry", "calladdress", "catalogue", "channel", - "composer", "declaration", "delivery", "forgekey", "handacts", "health", "internal/beside", "kvlimit", "membership", + "composer", "declaration", "delivery", "forgekey", "handacts", "health", "internal/beside", "keys", "kvlimit", "membership", "objectlink", "outward", "overlay", "parts", "pernode", "process", "provider", "provider/harness", "provisions", "sdkversion", "seal", "seatdef", "seatlist", "seatlist/seatlisttest", "testbus", "views", "views/viewstest", }