This is an automated email from the ASF dual-hosted git repository. sjanc pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/mynewt-newt.git
commit ae9932f75325a7341d836885b5017c43c245e68c Author: Szymon Janc <[email protected]> AuthorDate: Mon Oct 19 17:45:59 2026 +0200 newt/repo: Fix hang on version alias loop in repository.yml Version aliases in repository.yml (e.g. "1-latest": "1-dev") were followed without remembering visited versions, so a loop such as "1-latest": "1-dev" with "1-dev": "1-latest", or an alias mapping to itself, made NormalizeVersion spin forever. Track visited versions and report an error naming the repo and the version where the loop was detected. Assisted-by: Claude:claude-opus-5-5 --- newt/repo/version.go | 9 ++++ newt/repo/version_test.go | 119 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/newt/repo/version.go b/newt/repo/version.go index 8329e9da..3f0830cf 100644 --- a/newt/repo/version.go +++ b/newt/repo/version.go @@ -272,12 +272,21 @@ func (r *Repo) NormalizeVersion( ver newtutil.RepoVersion) (newtutil.RepoVersion, error) { origVer := ver + visited := map[newtutil.RepoVersion]struct{}{} for { if ver.Stability == "" || ver.Stability == newtutil.VERSION_STABILITY_NONE { return ver, nil } + if _, ok := visited[ver]; ok { + return origVer, util.FmtNewtError( + "cannot normalize version \"%s\" for repo \"%s\"; "+ + "version alias loop at \"%s\"", + origVer.String(), r.Name(), ver.String()) + } + visited[ver] = struct{}{} + verStr := r.vers[ver] if verStr == "" { return ver, util.FmtNewtError( diff --git a/newt/repo/version_test.go b/newt/repo/version_test.go new file mode 100644 index 00000000..1257e2bd --- /dev/null +++ b/newt/repo/version_test.go @@ -0,0 +1,119 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package repo + +import ( + "strings" + "testing" + "time" + + "mynewt.apache.org/newt/newt/newtutil" +) + +func mustParseVer(t *testing.T, s string) newtutil.RepoVersion { + t.Helper() + v, err := newtutil.ParseRepoVersion(s) + if err != nil { + t.Fatalf("failed to parse version %q: %v", s, err) + } + return v +} + +func normalizeWithTimeout(t *testing.T, r *Repo, + ver newtutil.RepoVersion) (newtutil.RepoVersion, error) { + + t.Helper() + + type result struct { + ver newtutil.RepoVersion + err error + } + ch := make(chan result, 1) + go func() { + v, err := r.NormalizeVersion(ver) + ch <- result{v, err} + }() + + select { + case res := <-ch: + return res.ver, res.err + case <-time.After(5 * time.Second): + t.Fatalf("NormalizeVersion(%s) did not return (alias loop?)", + ver.String()) + return ver, nil + } +} + +func TestNormalizeVersion(t *testing.T) { + r := &Repo{ + name: "dummy", + vers: map[newtutil.RepoVersion]string{ + mustParseVer(t, "1.2.3"): "v1.2.3", + mustParseVer(t, "1-latest"): "1.2.3", + mustParseVer(t, "1-dev"): "1-latest", + }, + } + + v, err := normalizeWithTimeout(t, r, mustParseVer(t, "1-dev")) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if v != mustParseVer(t, "1.2.3") { + t.Fatalf("unexpected version: %s", v.String()) + } +} + +func TestNormalizeVersionAliasLoop(t *testing.T) { + tests := map[string]map[string]string{ + "self": { + "1-dev": "1-dev", + }, + "two": { + "1-latest": "1-dev", + "1-dev": "1-latest", + }, + "three": { + "1-latest": "1-dev", + "1-dev": "1-stable", + "1-stable": "1-latest", + }, + } + + for name, vers := range tests { + t.Run(name, func(t *testing.T) { + r := &Repo{ + name: "dummy", + vers: map[newtutil.RepoVersion]string{}, + } + for k, v := range vers { + r.vers[mustParseVer(t, k)] = v + } + + _, err := normalizeWithTimeout(t, r, mustParseVer(t, "1-dev")) + if err == nil { + t.Fatalf("expected error for version alias loop") + } + if !strings.Contains(err.Error(), "dummy") || + !strings.Contains(err.Error(), "loop") { + t.Fatalf("unexpected error: %v", err) + } + }) + } +}
