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)
+                       }
+               })
+       }
+}

Reply via email to