From f5b11e1aa0e79a814fb08f97c7ad6721634849e7 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 20:36:06 +0100 Subject: [PATCH] SD4 (step key parser): ParseStepKey reads both forms and answers the asker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .. and main's . (no asker); a third form is refused. sdk/buildrun follows. Builds: design 47 G §484, §649 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- sdk/buildrun/stale.go | 2 +- sdk/buildstep/step_test.go | 19 +++++++++++++++++++ sdk/buildstep/steps.go | 26 ++++++++++++++++++-------- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/sdk/buildrun/stale.go b/sdk/buildrun/stale.go index e0344de0..08160833 100644 --- a/sdk/buildrun/stale.go +++ b/sdk/buildrun/stale.go @@ -164,7 +164,7 @@ func EndStale(scan Scan, node string, before time.Time, logf func(string, ...any } var ended []string for _, k := range keys { - n, _, perr := buildstep.ParseStepKey(k) + _, n, _, perr := buildstep.ParseStepKey(k) if perr != nil || n != node { continue } diff --git a/sdk/buildstep/step_test.go b/sdk/buildstep/step_test.go index e40fa5bf..bc93f2b0 100644 --- a/sdk/buildstep/step_test.go +++ b/sdk/buildstep/step_test.go @@ -68,3 +68,22 @@ func TestTwoIDsInOneMillisecondDiffer(t *testing.T) { } } } + +// 1966 (G §484, §649): ParseStepKey reads both forms of a key, main's answering no asker, and refuses a third. +func TestParseStepKeyReadsBothFormsAndRefusesAThird(t *testing.T) { + for _, tc := range []struct{ key, asker, node, id string }{ + {"ace.abc123", "", "ace", "abc123"}, + {"runner.ace.abc123", "runner", "ace", "abc123"}, + {"builder.g14.x1", "builder", "g14", "x1"}, + } { + asker, node, id, err := ParseStepKey(tc.key) + if err != nil || asker != tc.asker || node != tc.node || id != tc.id { + t.Errorf("%s: %q %q %q %v", tc.key, asker, node, id, err) + } + } + for _, key := range []string{"ace", "a.b.c.d", "Runner.ace.abc123", "runner.ace.ABC", ".ace.abc123", "ace."} { + if _, _, _, err := ParseStepKey(key); err == nil { + t.Errorf("%q was read as a step key", key) + } + } +} diff --git a/sdk/buildstep/steps.go b/sdk/buildstep/steps.go index fc05b1a0..eea0428e 100644 --- a/sdk/buildstep/steps.go +++ b/sdk/buildstep/steps.go @@ -68,6 +68,7 @@ func Ended(s Step) bool { return s.State != StepRunning && s.State != StepAgain var ( nameOK = regexp.MustCompile(`^[-/_=a-zA-Z0-9]+$`) stepIDOK = regexp.MustCompile(`^[a-z0-9]+$`) + askerOK = regexp.MustCompile(`^[a-z][a-z0-9-]*$`) askIDOK = regexp.MustCompile(`^[0-9a-z]{8,9}[0-9a-f]{6}$`) ) @@ -89,22 +90,31 @@ func StepKey(node, id string) (string, error) { return node + "." + id, nil } -// ParseStepKey is the inverse of StepKey. -func ParseStepKey(key string) (node, id string, err error) { - node, id, ok := strings.Cut(key, ".") - if !ok { - return "", "", fmt.Errorf("buildstep: %q is not a step key, .", key) +// ParseStepKey reads both forms of a step's key (design 47 G §484, §649): `..`, and main's `.`, +// which answers no asker (an empty asker). A third form is refused. +func ParseStepKey(key string) (asker, node, id string, err error) { + parts := strings.Split(key, ".") + switch len(parts) { + case 2: + node, id = parts[0], parts[1] + case 3: + asker, node, id = parts[0], parts[1], parts[2] + if !askerOK.MatchString(asker) { + return "", "", "", fmt.Errorf("buildstep: %q is not a step key: %q is not an asker", key, asker) + } + default: + return "", "", "", fmt.Errorf("buildstep: %q is not a step key, .. or .", key) } if _, err := StepKey(node, id); err != nil { - return "", "", fmt.Errorf("buildstep: %q is not a step key: %w", key, err) + return "", "", "", fmt.Errorf("buildstep: %q is not a step key: %w", key, err) } - return node, id, nil + return asker, node, id, nil } // ParseAskStepKey is ParseStepKey for the key of a pulled ask's step, whose id is the ask's (base-36 milliseconds // and six hex digits). func ParseAskStepKey(key string) (node, askID string, err error) { - node, askID, err = ParseStepKey(key) + _, node, askID, err = ParseStepKey(key) if err != nil { return "", "", err }