Skip to content

Commit b0d8bcb

Browse files
authored
Parse the Cargo package list by column instead of by regex (#5239) (#5312)
1 parent 90c1041 commit b0d8bcb

4 files changed

Lines changed: 438 additions & 45 deletions

File tree

src/UniGetUI.PackageEngine.Managers.Cargo/Cargo.cs

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,6 @@ public partial class Cargo : PackageManager
2121
[GeneratedRegex(@"([\w-]+)\s=\s""(\d+\.\d+\.\d+)""\s*#\s(.*)")]
2222
private static partial Regex SearchLineRegex();
2323

24-
[GeneratedRegex(@"(.+)v(\d+\.\d+\.\d+)\s*v(\d+\.\d+\.\d+)\s*(Yes|No)")]
25-
private static partial Regex UpdateLineRegex();
26-
27-
// Matches "ripgrep v15.1.0:" lines from `cargo install --list`
28-
[GeneratedRegex(@"^([\w-]+)\s+v(\d+\.\d+\.\d+):")]
29-
private static partial Regex InstallListLineRegex();
30-
3124
public Cargo()
3225
{
3326
string cargoCommand = OperatingSystem.IsWindows() ? "cargo.exe" : "cargo";
@@ -193,28 +186,44 @@ protected override void _loadManagerVersion(out string version)
193186
}
194187

195188
public void InvalidateInstalledCache() =>
196-
TaskRecycler<List<Match>>.RemoveFromCache(GetInstalledCommandOutput);
189+
TaskRecycler<List<CargoListEntry>>.RemoveFromCache(GetInstalledCommandOutput);
197190

198191
private IReadOnlyList<Package> GetPackages(LoggableTaskType taskType)
199192
{
200193
List<Package> Packages = [];
201-
foreach (var match in TaskRecycler<List<Match>>.RunOrAttach(GetInstalledCommandOutput, 15))
194+
var entries = TaskRecycler<List<CargoListEntry>>.RunOrAttach(GetInstalledCommandOutput, 15);
195+
foreach (var entry in entries)
202196
{
203-
var id = match.Groups[1]?.Value?.Trim() ?? "";
204-
var name = CoreTools.FormatAsName(id);
205-
var oldVersion = match.Groups[2]?.Value?.Trim() ?? "";
206-
var newVersion = match.Groups[3]?.Value?.Trim() ?? "";
207-
if (taskType is LoggableTaskType.ListUpdates && oldVersion != newVersion)
208-
Packages.Add(new Package(name, id, oldVersion, newVersion, DefaultSource, this));
197+
var name = CoreTools.FormatAsName(entry.Id);
198+
if (taskType is LoggableTaskType.ListUpdates)
199+
{
200+
if (
201+
entry.NeedsUpdate
202+
&& entry.LatestVersion is { Length: > 0 } latestVersion
203+
&& latestVersion != entry.InstalledVersion
204+
)
205+
Packages.Add(
206+
new Package(
207+
name,
208+
entry.Id,
209+
entry.InstalledVersion,
210+
latestVersion,
211+
DefaultSource,
212+
this
213+
)
214+
);
215+
}
209216
else if (taskType is LoggableTaskType.ListInstalledPackages)
210-
Packages.Add(new Package(name, id, oldVersion, DefaultSource, this));
217+
Packages.Add(
218+
new Package(name, entry.Id, entry.InstalledVersion, DefaultSource, this)
219+
);
211220
}
212221
return Packages;
213222
}
214223

215-
private List<Match> GetInstalledCommandOutput()
224+
private List<CargoListEntry> GetInstalledCommandOutput()
216225
{
217-
List<Match> output = [];
226+
List<string> stdout = [];
218227
using Process p = GetProcess(Status.ExecutablePath, "install-update --list");
219228
IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.OtherTask, p);
220229
logger.AddToStdOut("Other task: Call the install-update command");
@@ -224,38 +233,39 @@ private List<Match> GetInstalledCommandOutput()
224233
while ((line = p.StandardOutput.ReadLine()) is not null)
225234
{
226235
logger.AddToStdOut(line);
227-
var match = UpdateLineRegex().Match(line);
228-
if (match.Success)
229-
output.Add(match);
236+
stdout.Add(line);
230237
}
231238
logger.AddToStdErr(p.StandardError.ReadToEnd());
232239
p.WaitForExit();
240+
241+
List<string> skippedRows = [];
242+
var output = ParseInstallUpdateList(stdout, skippedRows);
243+
foreach (var skippedRow in skippedRows)
244+
logger.AddToStdErr($"Ignored unrecognized `install-update --list` row: {skippedRow}");
233245
logger.Close(p.ExitCode);
234246

235247
if (output.Count > 0)
236248
return output;
237249

238-
// Fallback: cargo-update is not installed, use the built-in `cargo install --list`.
239-
// No latest-version info is available, so updates won't be detected, but the installed
240-
// packages list will be populated correctly.
250+
List<string> fallbackStdout = [];
241251
using Process fallback = GetProcess(Status.ExecutablePath, "install --list");
242-
IProcessTaskLogger fallbackLogger = TaskLogger.CreateNew(LoggableTaskType.OtherTask, fallback);
243-
fallbackLogger.AddToStdOut("Falling back to `cargo install --list` (cargo-update not available)");
252+
IProcessTaskLogger fallbackLogger = TaskLogger.CreateNew(
253+
LoggableTaskType.OtherTask,
254+
fallback
255+
);
256+
fallbackLogger.AddToStdOut(
257+
"Falling back to `cargo install --list` (cargo-update reported no packages)"
258+
);
244259
fallback.Start();
245260
while ((line = fallback.StandardOutput.ReadLine()) is not null)
246261
{
247262
fallbackLogger.AddToStdOut(line);
248-
var m = InstallListLineRegex().Match(line);
249-
if (!m.Success) continue;
250-
// Synthesise a match compatible with UpdateLineRegex (same installed and latest version → no update)
251-
var fake = UpdateLineRegex().Match($"{m.Groups[1].Value} v{m.Groups[2].Value} v{m.Groups[2].Value} No");
252-
if (fake.Success)
253-
output.Add(fake);
263+
fallbackStdout.Add(line);
254264
}
255265
fallbackLogger.AddToStdErr(fallback.StandardError.ReadToEnd());
256266
fallback.WaitForExit();
257267
fallbackLogger.Close(fallback.ExitCode);
258-
return output;
268+
return ParseInstallList(fallbackStdout);
259269
}
260270

261271
private Process GetProcess(string fileName, string extraArguments)
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
using System.Text.RegularExpressions;
2+
3+
namespace UniGetUI.PackageEngine.Managers.CargoManager;
4+
5+
internal sealed record CargoListEntry(
6+
string Id,
7+
string InstalledVersion,
8+
string? LatestVersion,
9+
bool NeedsUpdate
10+
);
11+
12+
public partial class Cargo
13+
{
14+
[GeneratedRegex(@"^[A-Za-z0-9_][A-Za-z0-9_-]*$")]
15+
private static partial Regex CrateNameRegex();
16+
17+
[GeneratedRegex(@"[ \t]{2,}|\t")]
18+
private static partial Regex ColumnSeparatorRegex();
19+
20+
[GeneratedRegex(@"^v(?<version>[0-9][A-Za-z0-9.+-]*)(?:\s+\(v[^)]*\))?$")]
21+
private static partial Regex VersionCellRegex();
22+
23+
[GeneratedRegex(@"^(?<id>[A-Za-z0-9_][A-Za-z0-9_-]*)\s+v(?<version>[0-9][A-Za-z0-9.+-]*)(?:\s+\(.*\))?:$")]
24+
private static partial Regex InstallListLineRegex();
25+
26+
internal static List<CargoListEntry> ParseInstallUpdateList(
27+
IEnumerable<string> lines,
28+
List<string>? skippedRows = null
29+
)
30+
{
31+
List<CargoListEntry> entries = [];
32+
bool insideTable = false;
33+
34+
foreach (var rawLine in lines)
35+
{
36+
var line = rawLine.Trim();
37+
38+
if (line.Length is 0)
39+
{
40+
insideTable = false;
41+
continue;
42+
}
43+
44+
if (IsTableHeader(line))
45+
{
46+
insideTable = true;
47+
continue;
48+
}
49+
50+
if (!insideTable)
51+
continue;
52+
53+
var entry = ParseInstallUpdateRow(line);
54+
if (entry is null)
55+
skippedRows?.Add(line);
56+
else
57+
entries.Add(entry);
58+
}
59+
60+
return entries;
61+
}
62+
63+
internal static List<CargoListEntry> ParseInstallList(IEnumerable<string> lines)
64+
{
65+
List<CargoListEntry> entries = [];
66+
67+
foreach (var rawLine in lines)
68+
{
69+
var match = InstallListLineRegex().Match(rawLine.TrimEnd());
70+
if (match.Success)
71+
entries.Add(
72+
new CargoListEntry(
73+
match.Groups["id"].Value,
74+
match.Groups["version"].Value,
75+
null,
76+
false
77+
)
78+
);
79+
}
80+
81+
return entries;
82+
}
83+
84+
private static bool IsTableHeader(string line) =>
85+
line.StartsWith("Package", StringComparison.Ordinal)
86+
&& line.Contains("Installed", StringComparison.Ordinal)
87+
&& line.Contains("Latest", StringComparison.Ordinal)
88+
&& line.Contains("Needs update", StringComparison.Ordinal);
89+
90+
private static CargoListEntry? ParseInstallUpdateRow(string line)
91+
{
92+
var cells = ColumnSeparatorRegex().Split(line);
93+
if (cells.Length < 4)
94+
return null;
95+
96+
var id = cells[0].Trim();
97+
if (!CrateNameRegex().IsMatch(id))
98+
return null;
99+
100+
var installedVersion = ParseVersionCell(cells[1]);
101+
if (installedVersion is null)
102+
return null;
103+
104+
return new CargoListEntry(
105+
id,
106+
installedVersion,
107+
ParseVersionCell(cells[2]),
108+
cells[3].Trim().Equals("Yes", StringComparison.OrdinalIgnoreCase)
109+
);
110+
}
111+
112+
private static string? ParseVersionCell(string cell)
113+
{
114+
var match = VersionCellRegex().Match(cell.Trim());
115+
return match.Success ? match.Groups["version"].Value : null;
116+
}
117+
}

src/UniGetUI.PackageEngine.Managers.Cargo/Helpers/CargoPkgDetailsHelper.cs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,22 @@ protected override void GetDetails_UnSafe(IPackageDetails details)
4444
var categories = manifest.categories?.Select(c => c.category) ?? [];
4545
details.Tags = [.. keywords, .. categories];
4646

47-
var versionData = manifest
48-
.versions.Where((v) => v.num == details.Package.VersionString)
49-
.First();
50-
51-
details.Author = versionData.published_by?.name;
52-
details.License = versionData.license;
53-
details.InstallerUrl = new Uri(
54-
(CratesIOClient.ApiUrl + versionData.dl_path).Replace("/api/v1/api/v1", "/api/v1")
47+
var versionData = manifest.versions.FirstOrDefault(v =>
48+
v.num == details.Package.VersionString
5549
);
56-
details.InstallerSize = versionData.crate_size ?? 0;
57-
details.InstallerHash = versionData.checksum;
58-
details.Publisher = versionData.published_by?.name;
59-
details.UpdateDate = versionData.updated_at;
50+
51+
if (versionData is not null)
52+
{
53+
details.Author = versionData.published_by?.name;
54+
details.License = versionData.license;
55+
details.InstallerUrl = new Uri(
56+
(CratesIOClient.ApiUrl + versionData.dl_path).Replace("/api/v1/api/v1", "/api/v1")
57+
);
58+
details.InstallerSize = versionData.crate_size ?? 0;
59+
details.InstallerHash = versionData.checksum;
60+
details.Publisher = versionData.published_by?.name;
61+
details.UpdateDate = versionData.updated_at;
62+
}
6063

6164
// TODO: most packages are hosted on Github; see if there's a way to use the repository
6265
// info to extract release notes

0 commit comments

Comments
 (0)