@@ -10,6 +10,37 @@ namespace Microsoft.Dotnet.Installation.Internal;
1010
1111internal class ChannelVersionResolver
1212{
13+ /// <summary>
14+ /// Channel keyword for the latest stable release.
15+ /// </summary>
16+ public const string LatestChannel = "latest" ;
17+
18+ /// <summary>
19+ /// Channel keyword for the latest preview release.
20+ /// </summary>
21+ public const string PreviewChannel = "preview" ;
22+
23+ /// <summary>
24+ /// Channel keyword for the latest Long Term Support (LTS) release.
25+ /// </summary>
26+ public const string LtsChannel = "lts" ;
27+
28+ /// <summary>
29+ /// Channel keyword for the latest Standard Term Support (STS) release.
30+ /// </summary>
31+ public const string StsChannel = "sts" ;
32+
33+ /// <summary>
34+ /// Known channel keywords that are always valid.
35+ /// </summary>
36+ public static readonly IReadOnlyList < string > KnownChannelKeywords = [ LatestChannel , PreviewChannel , LtsChannel , StsChannel ] ;
37+
38+ /// <summary>
39+ /// Maximum reasonable major version number. .NET versions are currently single-digit;
40+ /// anything above 99 is clearly invalid input (e.g., typos, random numbers).
41+ /// </summary>
42+ internal const int MaxReasonableMajorVersion = 99 ;
43+
1344 private ReleaseManifest _releaseManifest = new ( ) ;
1445
1546 public ChannelVersionResolver ( )
@@ -25,7 +56,7 @@ public ChannelVersionResolver(ReleaseManifest releaseManifest)
2556 public IEnumerable < string > GetSupportedChannels ( bool includeFeatureBands = true )
2657 {
2758 var productIndex = _releaseManifest . GetReleasesIndex ( ) ;
28- return [ "latest" , "preview" , "lts" , "sts" ,
59+ return [ .. KnownChannelKeywords ,
2960 ..productIndex
3061 . Where ( p => p . IsSupported )
3162 . OrderByDescending ( p => p . LatestReleaseVersion )
@@ -57,6 +88,89 @@ static IEnumerable<string> GetChannelsForProduct(Product product, bool includeFe
5788 return GetLatestVersionForChannel ( installRequest . Channel , installRequest . Component ) ;
5889 }
5990
91+ /// <summary>
92+ /// Checks if a channel string looks like a valid .NET version/channel format.
93+ /// This is a preliminary validation before attempting resolution.
94+ /// </summary>
95+ /// <param name="channel">The channel string to validate</param>
96+ /// <returns>True if the format appears valid, false if clearly invalid</returns>
97+ public static bool IsValidChannelFormat ( string channel )
98+ {
99+ if ( string . IsNullOrWhiteSpace ( channel ) )
100+ {
101+ return false ;
102+ }
103+
104+ // Known keywords are always valid
105+ if ( KnownChannelKeywords . Any ( k => string . Equals ( k , channel , StringComparison . OrdinalIgnoreCase ) ) )
106+ {
107+ return true ;
108+ }
109+
110+ // Check for prerelease suffix (e.g., "10.0.100-preview.1.32640")
111+ var dashIndex = channel . IndexOf ( '-' ) ;
112+ var hasPrerelease = dashIndex >= 0 ;
113+ var versionPart = hasPrerelease ? channel . Substring ( 0 , dashIndex ) : channel ;
114+
115+ // Try to parse as a version-like string
116+ var parts = versionPart . Split ( '.' ) ;
117+ if ( parts . Length == 0 || parts . Length > 4 )
118+ {
119+ return false ;
120+ }
121+
122+ // First part must be a valid major version
123+ if ( ! int . TryParse ( parts [ 0 ] , out var major ) || major < 0 || major > MaxReasonableMajorVersion )
124+ {
125+ return false ;
126+ }
127+
128+ // If there are more parts, validate them
129+ if ( parts . Length >= 2 )
130+ {
131+ if ( ! int . TryParse ( parts [ 1 ] , out var minor ) || minor < 0 )
132+ {
133+ return false ;
134+ }
135+ }
136+
137+ if ( parts . Length >= 3 )
138+ {
139+ var patch = parts [ 2 ] ;
140+ if ( string . IsNullOrEmpty ( patch ) )
141+ {
142+ return false ;
143+ }
144+
145+ // Allow either:
146+ // - a fully specified numeric patch (e.g., "103"), optionally with a prerelease suffix, or
147+ // - a feature band pattern with a numeric prefix and "xx" suffix (e.g., "1xx", "101xx"),
148+ // but NOT with a prerelease suffix (wildcards with prerelease not supported).
149+ if ( patch . EndsWith ( "xx" , StringComparison . OrdinalIgnoreCase ) )
150+ {
151+ if ( hasPrerelease )
152+ {
153+ return false ;
154+ }
155+
156+ var prefix = patch . Substring ( 0 , patch . Length - 2 ) ;
157+ if ( prefix . Length == 0 || ! int . TryParse ( prefix , out _ ) )
158+ {
159+ return false ;
160+ }
161+ }
162+ else
163+ {
164+ if ( ! int . TryParse ( patch , out var numericPatch ) || numericPatch < 0 )
165+ {
166+ return false ;
167+ }
168+ }
169+ }
170+
171+ return true ;
172+ }
173+
60174 /// <summary>
61175 /// Parses a version channel string into its components.
62176 /// </summary>
@@ -97,18 +211,18 @@ static IEnumerable<string> GetChannelsForProduct(Product product, bool includeFe
97211 /// <returns>Latest fully specified version string, or null if not found</returns>
98212 public ReleaseVersion ? GetLatestVersionForChannel ( UpdateChannel channel , InstallComponent component )
99213 {
100- if ( string . Equals ( channel . Name , "lts" , StringComparison . OrdinalIgnoreCase ) || string . Equals ( channel . Name , "sts" , StringComparison . OrdinalIgnoreCase ) )
214+ if ( string . Equals ( channel . Name , LtsChannel , StringComparison . OrdinalIgnoreCase ) || string . Equals ( channel . Name , StsChannel , StringComparison . OrdinalIgnoreCase ) )
101215 {
102- var releaseType = string . Equals ( channel . Name , "lts" , StringComparison . OrdinalIgnoreCase ) ? ReleaseType . LTS : ReleaseType . STS ;
216+ var releaseType = string . Equals ( channel . Name , LtsChannel , StringComparison . OrdinalIgnoreCase ) ? ReleaseType . LTS : ReleaseType . STS ;
103217 var productIndex = _releaseManifest . GetReleasesIndex ( ) ;
104218 return GetLatestVersionByReleaseType ( productIndex , releaseType , component ) ;
105219 }
106- else if ( string . Equals ( channel . Name , "preview" , StringComparison . OrdinalIgnoreCase ) )
220+ else if ( string . Equals ( channel . Name , PreviewChannel , StringComparison . OrdinalIgnoreCase ) )
107221 {
108222 var productIndex = _releaseManifest . GetReleasesIndex ( ) ;
109223 return GetLatestPreviewVersion ( productIndex , component ) ;
110224 }
111- else if ( string . Equals ( channel . Name , "latest" , StringComparison . OrdinalIgnoreCase ) )
225+ else if ( string . Equals ( channel . Name , LatestChannel , StringComparison . OrdinalIgnoreCase ) )
112226 {
113227 var productIndex = _releaseManifest . GetReleasesIndex ( ) ;
114228 return GetLatestActiveVersion ( productIndex , component ) ;
0 commit comments