chr
2026-04-05 fe750b791d5b517cc4e9bc8e99a9a75139a0cfba
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
 
namespace OpenTap.Package
{
    /// <summary>
    /// This graph describes every version of every package in a memory-efficient way.
    /// Each unique name version and version specifier is represented by one value, connections between them are some dictionaries.
    /// The graph can be merged from multiple different sources - both file repositories and http repositories, but the source of each becomes lost when building the graph.
    /// When the source is needed a new lookup will be needed from a different place, this code is not really concerned with that.
    /// </summary>
    class PackageDependencyGraph
    {
        // all the existing names and versions.
        readonly List<string> nameLookup = new List<string>();
        readonly List<SemanticVersion> versionLookup = new List<SemanticVersion>();
        readonly List<VersionSpecifier> versionSpecifierLookup = new List<VersionSpecifier>();
 
        // lookup of X to id, ID being the index in the above tables.
        readonly Dictionary<string, int> name2Id = new Dictionary<string, int>();
        readonly Dictionary<SemanticVersion, int> version2Id = new Dictionary<SemanticVersion, int>();
        readonly Dictionary<VersionSpecifier, int> versionSpecifier2Id = new Dictionary<VersionSpecifier, int>();
 
        // Data structures for hosting versions.
        readonly Dictionary<string, SemanticVersion> stringToVersion = new Dictionary<string, SemanticVersion>();
        readonly Dictionary<string, VersionSpecifier> stringToVersionSpecifier = new Dictionary<string, VersionSpecifier>();
 
        // versions contains all versions of a given name. Eg all versions of OpenTAP/
        readonly Dictionary<int, HashSet<int>> versions = new Dictionary<int, HashSet<int>>();
 
        // Dependencies for a given version 
        readonly Dictionary<(int packageNameId, int packageVersion), (int packageNameId, int versionSpecifierId)[]> dependencies = new Dictionary<(int, int), (int, int)[]>();
 
        /// <summary>
        /// Callback for when additional packages are needed. For example if only release packages of one specific
        /// package name has been defined, this can be called to extend with e.g beta packages.
        /// </summary>
        public Action<string, string> UpdatePrerelease;
 
        /// <summary> The total number of packages contained in this graph. </summary>
        public int Count => versions.Sum(x => x.Value.Count);
 
        int GetNameId(string name)
        {
            if (!name2Id.TryGetValue(name, out var id))
            {
                name2Id[name] = nameLookup.Count;
                id = nameLookup.Count;
                versions[id] = new HashSet<int>();
                nameLookup.Add(name);
            }
 
            return id;
        }
 
        int GetVersionId(SemanticVersion v)
        {
            if (!version2Id.TryGetValue(v, out var id))
            {
                version2Id[v] = id = versionLookup.Count;
                versionLookup.Add(v);
            }
            return id;
        }
 
        int GetVersionId(string v2)
        {
            var v = stringToVersion.GetOrCreateValue(v2, SemanticVersion.Parse);
            return GetVersionId(v);
        }
 
        int GetVersionSpecifier(string v2)
        {
            var v = stringToVersionSpecifier.GetOrCreateValue(v2, VersionSpecifier.Parse);
            return GetVersionSpecifier(v);
        }
 
        int GetVersionSpecifier(VersionSpecifier v)
        {
            if (!versionSpecifier2Id.TryGetValue(v, out var id))
            {
                versionSpecifier2Id[v] = id = versionSpecifierLookup.Count;
                versionSpecifierLookup.Add(v);
            }
            return id;
        }
 
        public void LoadFromPackageDefs(IEnumerable<PackageDef> packages)
        {
            int addPackages = 0;
            foreach (var elem in packages.ToArray())
            {
                var name = elem.Name;
                var version = elem.Version ?? new SemanticVersion(0, 1, 0, null, null);
 
                var id = GetNameId(name);
                var thisVersions = versions[id];
                if (thisVersions.Add(GetVersionId(version)))
                    addPackages += 1;
                if (elem.Dependencies.Count > 0)
                {
                    var deps = new (int id, int y)[elem.Dependencies.Count];
 
                    for (int i = 0; i < deps.Length; i++)
                    {
                        var dep = elem.Dependencies[i];
                        var depname = dep.Name;
                        var depid = GetNameId(depname);
                        var depver = dep.Version;
                        deps[i] = (depid, GetVersionSpecifier(depver));
                    }
 
                    dependencies[(id, GetVersionId(version))] = deps;
                }
            }
 
        }
 
        public void LoadFromDictionaries(List<Dictionary<string, object>> dicts)
        {
            foreach (var d in dicts)
            {
                var name = d["name"] as string;
                var version = d["version"] as string;
                if (!SemanticVersion.TryParse(version, out var v))
                    continue;
                var id = GetNameId(name);
                var thisVersion = versions[id];
                if (!thisVersion.Add(GetVersionId(version)))
                    continue; // package already added.
                var depMap = d["dependencies"] as List<Dictionary<string, object>>;
                if (depMap == null || depMap.Count == 0) continue;
                
                var deps = new (int id, int y)[depMap.Count];
                int i = 0; 
                foreach (var dep in depMap)
                {
                    var depname = dep["name"] as string;
                    var depid = GetNameId(depname);
                    var depver = dep["version"] as string;
                    deps[i] = (depid, GetVersionSpecifier(depver));
                    i++;
                }
 
                dependencies[(id, GetVersionId(version))] = deps;
            }
        }
 
        /// <summary>
        /// This is only used in unittests. It works for respones from the 3.1/query API, but not the 4.0/query API.
        /// </summary>
        public void LoadFromJson(JsonElement packages)
        {
            int addPackages = 0;
            foreach (var elem in packages.EnumerateArray())
            {
                var name = elem.GetProperty("name").GetString();
                var version = elem.GetProperty("version").GetString();
                if (!SemanticVersion.TryParse(version, out var v))
                    continue;
 
                var id = GetNameId(name);
                var thisVersions = versions[id];
                if (!thisVersions.Add(GetVersionId(version)))
                    continue; // package already added.
                addPackages += 1;
                if (elem.TryGetProperty("dependencies", out var obj))
                {
                    var l = obj.GetArrayLength();
                    if (l != 0)
                    {
                        var deps = new (int id, int y)[l];
                        int i = 0;
                        foreach (var dep in obj.EnumerateArray())
                        {
                            var depname = dep.GetProperty("name").GetString();
                            var depid = GetNameId(depname);
                            var depver = dep.GetProperty("version").GetString();
                            deps[i] = (depid, GetVersionSpecifier(depver));
                            i++;
                        }
 
                        dependencies[(id, GetVersionId(version))] = deps;
                    }
 
                }
            }
        }
 
        public bool EnsurePreReleasesCached(PackageSpecifier packageSpecifier)
        {
            if (!string.IsNullOrWhiteSpace(packageSpecifier.Version.PreRelease) || packageSpecifier.Version.MatchBehavior.HasFlag(VersionMatchBehavior.AnyPrerelease))
            {
                string newPreRelease;
                if (packageSpecifier.Version.MatchBehavior.HasFlag(VersionMatchBehavior.AnyPrerelease))
                {
                    newPreRelease = "^alpha";
                }
                else
                {
                    newPreRelease = packageSpecifier.Version.PreRelease.Split('.')[0];
                }
 
                // If the pre-release level has not been downloaded, or if its a higher prerelease than the current
                // for example, if current pre-release is rc, but a beta is asked for, we need to update the graph.
                if (!currentPreReleases.TryGetValue(packageSpecifier.Name, out var currentPrerelease) || VersionSpecifier.ComparePreRelease(newPreRelease, currentPrerelease) < 0)
                {
                    currentPreReleases[packageSpecifier.Name] = newPreRelease;
                    // update the package dependency graph.
                    if (UpdatePrerelease != null)
                        UpdatePrerelease(packageSpecifier.Name, newPreRelease);
                    return true;
 
                }
            }
            return false;
        }
 
        // tracks currently fetched pre-releases. This is an optimization to avoid having to 
        // pull absolutely all packages from the repository.
        readonly Dictionary<string, string> currentPreReleases = new Dictionary<string, string>();
        public IEnumerable<SemanticVersion> PackagesSatisfying(PackageSpecifier packageSpecifier)
        {
            EnsurePreReleasesCached(packageSpecifier);
            if (name2Id.TryGetValue(packageSpecifier.Name, out var Id))
            {
                var thisVersions = versions[Id];
                foreach (var v in thisVersions)
                {
                    var semv = versionLookup[v];
                    {
                        if (packageSpecifier.Version == VersionSpecifier.Any)
                        {
                            yield return semv;
                        }
                        else if (packageSpecifier.Version.IsCompatible(semv))
                            yield return semv;
 
                    }
                }
            }
        }
 
        /// <summary>
        /// Turn all the data back into PackageDefs, note that these PackageDefs are severly limited and
        /// should only be used for testing or for building another graph.
        /// </summary>
        /// <returns></returns>
        public IEnumerable<PackageDef> PackageSpecifiers()
        {
 
            foreach (var thing in versions)
            {
                var pkgName = nameLookup[thing.Key];
                foreach (var v in thing.Value)
                {
                    var version = this.versionLookup[v];
                    var pkg = new PackageDef
                    {
                        Name = pkgName,
                        Version = version
                    };
                    if (dependencies.TryGetValue((thing.Key, v), out var deps))
                    {
                        foreach (var dep in deps)
                        {
                            var name = nameLookup[dep.packageNameId];
                            var version2 = versionSpecifierLookup[dep.versionSpecifierId];
                            pkg.Dependencies.Add(new PackageDependency(name, version2));
                        }
                    }
 
                    yield return pkg;
                }
            }
        }
 
        public bool CouldSatisfy(string pkgName, VersionSpecifier version, PackageSpecifier[] others, PackageSpecifier[] fixedPackages)
        {
            // we only do this if version can actually be interpreted as a semantic version.
            // if version is ^ or incomplete we just assume that it 'could' satisfy the others.
            if (name2Id.TryGetValue(pkgName, out var Id) && version.TryAsExactSemanticVersion(out var v))
            {
                if (!dependencies.TryGetValue((Id, GetVersionId(v)), out var deps))
                    return true; // this package has no dependencies, so yes.
                foreach (var dep in deps)
                {
                    var depVersion = versionSpecifierLookup[dep.Item2];
 
                    var ps = new PackageSpecifier(nameLookup[dep.Item1], depVersion);
                    var o = others.FirstOrDefault(x => x.Name == ps.Name);
                    if (o == null)
                        continue;
                    if (ps.Version.IsSatisfiedBy(o.Version) == false && o.Version.IsSatisfiedBy(ps.Version) == false)
                        return false;
 
                    var o2 = fixedPackages.FirstOrDefault(x => x.Name == ps.Name);
                    if (o2 == null)
                        continue;
                    if (ps.Version.IsSatisfiedBy(o.Version) == false && o2.Version.IsSatisfiedBy(ps.Version) == false)
                        return false;
 
                    // todo protect from circular dependencies here.
                    if (!CouldSatisfy(ps.Name, depVersion, others, Array.Empty<PackageSpecifier>()))
                        return false;
                }
 
                return true;
            }
 
            return true;
        }
 
        /// <summary>
        /// Gets all the dependencies for multiples versions of multiple packages.
        /// Since many of them share specific dependencies, this makes certain queries faster.
        /// </summary>
        IEnumerable<PackageSpecifier> GetAllDependencies(IEnumerable<(string, IEnumerable<SemanticVersion>)> allPackages)
        {
            var lookup = new HashSet<(int packageId, int versionId)>();
 
            foreach (var (pkgName, versions) in allPackages)
            {
                if (name2Id.TryGetValue(pkgName, out var Id))
                {
                    foreach (var version in versions)
                    {
                        if (dependencies.TryGetValue((Id, GetVersionId(version)), out var deps))
                        {
                            foreach (var dep in deps)
                            {
                                lookup.Add(dep);
                            }
                        }
                    }
                }
            }
            if (lookup.Count == 0)
                return Array.Empty<PackageSpecifier>();
 
            return lookup.Select(dep 
                => new PackageSpecifier(nameLookup[dep.packageId], versionSpecifierLookup[dep.versionId]));
        }
 
 
        /// <summary>
        /// Gets the dependencies for a specific package version.
        /// </summary>
        public IEnumerable<PackageSpecifier> GetDependencies(string pkgName, SemanticVersion version)
        {
            int Id = 0;
            bool cached = false;
            (int packageNameId, int versionSpecifierId)[] deps = [];
            
            cached = name2Id.TryGetValue(pkgName, out Id) && dependencies.TryGetValue((Id, GetVersionId(version)), out deps);
            if (!cached)
            {
                EnsurePreReleasesCached(new PackageSpecifier(pkgName, version.AsExactSpecifier())); 
                cached = name2Id.TryGetValue(pkgName, out Id) && dependencies.TryGetValue((Id, GetVersionId(version)), out deps);
            }
            
            if (cached)
            {
                foreach (var dep in deps)
                {
                    var depVersion = versionSpecifierLookup[dep.Item2];
 
                    yield return new PackageSpecifier(nameLookup[dep.Item1], depVersion);
                }
            }
        }
 
        public bool HasPackage(string pkgName, SemanticVersion version)
        {
            return name2Id.TryGetValue(pkgName, out var Id)
                   && version2Id.TryGetValue(version, out var vId)
                   && versions.TryGetValue(Id, out var set)
                   && set.Contains(vId);
        }
 
        /// <summary> Absorbs another package dependency graph into the structure. </summary>
        public void Absorb(PackageDependencyGraph graph)
        {
            LoadFromPackageDefs(graph.PackageSpecifiers());
        }
        
        public void EnsurePackagePreReleasesCached(List<PackageSpecifier> packages)
        {
            var pkgNamesAndVersions = packages
                .Select(x => (x.Name, PackagesSatisfying(x)));
            var allDependencies = GetAllDependencies(pkgNamesAndVersions);
            bool retry;
            do
            {
                retry = false;
                foreach (var dep in allDependencies)
                {
                    // if new packages are cached, it means the search space could have expanded.
                    // it is a bit unlikely to happen, but edge but it could happen if
                    // dependencies looks like: A:beta -> B:beta -> C:beta -> D:beta
                    retry |= EnsurePreReleasesCached(dep);
                }
            } while (retry);
        }
    }
}