From 6c9306de78ef80a8e42cff0ed87f18792e498261 Mon Sep 17 00:00:00 2001 From: Ethan Nordness Date: Sat, 28 Feb 2026 16:32:28 -0800 Subject: [PATCH 1/2] reproduce and fix duplicate package field in pip results crash --- StabilityMatrix.Core/Python/PipShowResult.cs | 25 ++- .../Core/PipShowResultsTests.cs | 177 ++++++++++++++++++ 2 files changed, 196 insertions(+), 6 deletions(-) create mode 100644 StabilityMatrix.Tests/Core/PipShowResultsTests.cs diff --git a/StabilityMatrix.Core/Python/PipShowResult.cs b/StabilityMatrix.Core/Python/PipShowResult.cs index 7e66ccbb..a1131b29 100644 --- a/StabilityMatrix.Core/Python/PipShowResult.cs +++ b/StabilityMatrix.Core/Python/PipShowResult.cs @@ -45,11 +45,24 @@ public static PipShowResult Parse(string output) lines.RemoveRange(indexOfLicense, indexOfLocation - indexOfLicense); } - var linesDict = lines - .Select(line => line.Split(':', 2)) - .Where(split => split.Length == 2) - .Select(split => new KeyValuePair(split[0].Trim(), split[1].Trim())) - .ToDictionary(pair => pair.Key, pair => pair.Value); + var linesDict = new Dictionary(); + foreach (var line in lines) + { + var split = line.Split(':', 2); + if (split.Length != 2) + continue; + + var key = split[0].Trim(); + var value = split[1].Trim(); + + if (key == "Name" && linesDict.ContainsKey("Name")) + { + // We've hit a new package, so stop parsing + break; + } + + linesDict.TryAdd(key, value); + } return new PipShowResult { @@ -68,7 +81,7 @@ public static PipShowResult Parse(string output) RequiredBy = linesDict .GetValueOrDefault("Required-by") ?.Split(',', StringSplitOptions.TrimEntries) - .ToList() + .ToList(), }; } diff --git a/StabilityMatrix.Tests/Core/PipShowResultsTests.cs b/StabilityMatrix.Tests/Core/PipShowResultsTests.cs new file mode 100644 index 00000000..355994af --- /dev/null +++ b/StabilityMatrix.Tests/Core/PipShowResultsTests.cs @@ -0,0 +1,177 @@ +using System.IO; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using StabilityMatrix.Core.Python; + +namespace StabilityMatrix.Tests.Core; + +[TestClass] +public class PipShowResultsTests +{ + [TestMethod] + public void TestSinglePackage() + { + var input = """ + Name: package-a + Version: 1.0.0 + Summary: A test package + Home-page: https://example.com + Author: John Doe + Author-email: john.doe@example.com + License: MIT + Location: /path/to/package + Requires: + Required-by: + """; + + var result = PipShowResult.Parse(input); + + Assert.IsNotNull(result); + Assert.AreEqual("package-a", result.Name); + Assert.AreEqual("1.0.0", result.Version); + Assert.AreEqual("A test package", result.Summary); + } + + [TestMethod] + public void TestMultiplePackages() + { + var input = """ + Name: package-a + Version: 1.0.0 + Summary: A test package + Home-page: https://example.com + Author: John Doe + Author-email: john.doe@example.com + License: MIT + Location: /path/to/package + Requires: + Required-by: + --- + Name: package-b + Version: 2.0.0 + Summary: Another test package + Home-page: https://example.com + Author: Jane Doe + Author-email: jane.doe@example.com + License: Apache-2.0 + Location: /path/to/another/package + Requires: package-a + Required-by: + """; + + var result = PipShowResult.Parse(input); + + Assert.IsNotNull(result); + Assert.AreEqual("package-a", result.Name); + Assert.AreEqual("1.0.0", result.Version); + Assert.AreNotEqual("package-b", result.Name); + } + + [TestMethod] + public void TestMalformedPackage() + { + var input = """ + Name: package-a + Version: 1.0.0 + Summary A test package + Home-page: https://example.com + Author: John Doe + Author-email: john.doe@example.com + License: MIT + Location: /path/to/package + Requires: + Required-by: + """; + + var result = PipShowResult.Parse(input); + + Assert.IsNotNull(result); + Assert.AreEqual("package-a", result.Name); + Assert.AreEqual("1.0.0", result.Version); + Assert.IsNull(result.Summary); + } + + [TestMethod] + public void TestMultiLineLicense() + { + var input = """ + Name: package-a + Version: 1.0.0 + Summary: A test package + Home-page: https://example.com + Author: John Doe + Author-email: john.doe@example.com + License: The MIT License (MIT) + + Copyright (c) 2015 John Doe + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + Location: /path/to/package + Requires: + Required-by: + """; + + var result = PipShowResult.Parse(input); + + Assert.IsNotNull(result); + Assert.AreEqual("package-a", result.Name); + Assert.AreEqual("1.0.0", result.Version); + Assert.IsTrue(result.License?.StartsWith("License: The MIT License (MIT)")); + } + + /// + /// This test simulates the input that caused the crash reported in Sentry issue b125504f. + /// The old implementation of PipShowResult.Parse used ToDictionary, which would throw an + /// ArgumentException if the input contained multiple packages, as the "Name" key would be + /// duplicated. The new implementation uses a foreach loop and TryAdd to prevent this crash. + /// + [TestMethod] + public void TestDuplicatePackageNameInOutput() + { + var input = """ + Name: package-a + Version: 1.0.0 + Summary: A test package + Home-page: https://example.com + Author: John Doe + Author-email: john.doe@example.com + License: MIT + Location: /path/to/package + Requires: + Required-by: + --- + Name: package-a + Version: 1.0.0 + Summary: A test package + Home-page: https://example.com + Author: John Doe + Author-email: john.doe@example.com + License: MIT + Location: /path/to/package + Requires: + Required-by: + """; + + var result = PipShowResult.Parse(input); + + Assert.IsNotNull(result); + Assert.AreEqual("package-a", result.Name); + Assert.AreEqual("1.0.0", result.Version); + } +} From b1292d041882f4d74b51f0ed549fbea6a094c9cf Mon Sep 17 00:00:00 2001 From: e-nord Date: Sat, 28 Feb 2026 21:12:09 -0800 Subject: [PATCH 2/2] validation fixup per gemini code assist review --- StabilityMatrix.Core/Python/PipShowResult.cs | 14 ++++++++++++-- StabilityMatrix.Tests/Core/PipShowResultsTests.cs | 7 +++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/StabilityMatrix.Core/Python/PipShowResult.cs b/StabilityMatrix.Core/Python/PipShowResult.cs index a1131b29..3df5ed38 100644 --- a/StabilityMatrix.Core/Python/PipShowResult.cs +++ b/StabilityMatrix.Core/Python/PipShowResult.cs @@ -64,10 +64,20 @@ public static PipShowResult Parse(string output) linesDict.TryAdd(key, value); } + if (!linesDict.TryGetValue("Name", out var name)) + { + throw new FormatException("The 'Name' key was not found in the pip show output."); + } + + if (!linesDict.TryGetValue("Version", out var version)) + { + throw new FormatException("The 'Version' key was not found in the pip show output."); + } + return new PipShowResult { - Name = linesDict["Name"], - Version = linesDict["Version"], + Name = name, + Version = version, Summary = linesDict.GetValueOrDefault("Summary"), HomePage = linesDict.GetValueOrDefault("Home-page"), Author = linesDict.GetValueOrDefault("Author"), diff --git a/StabilityMatrix.Tests/Core/PipShowResultsTests.cs b/StabilityMatrix.Tests/Core/PipShowResultsTests.cs index 355994af..08ebf4d5 100644 --- a/StabilityMatrix.Tests/Core/PipShowResultsTests.cs +++ b/StabilityMatrix.Tests/Core/PipShowResultsTests.cs @@ -174,4 +174,11 @@ public void TestDuplicatePackageNameInOutput() Assert.AreEqual("package-a", result.Name); Assert.AreEqual("1.0.0", result.Version); } + + [TestMethod] + public void TestEmptyInputThrowsFormatException() + { + var input = ""; + Assert.ThrowsException(() => PipShowResult.Parse(input)); + } }