1 // Copyright (c) Microsoft. All rights reserved.
2 // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3 
4 using System;
5 using System.Collections;
6 using System.Collections.Generic;
7 using System.Xml;
8 using System.IO;
9 using System.Text;
10 using System.Globalization;
11 using System.Security;
12 using System.Text.RegularExpressions;
13 
14 using ErrorUtilities = Microsoft.Build.Shared.ErrorUtilities;
15 using VisualStudioConstants = Microsoft.Build.Shared.VisualStudioConstants;
16 using ProjectFileErrorUtilities = Microsoft.Build.Shared.ProjectFileErrorUtilities;
17 using BuildEventFileInfo = Microsoft.Build.Shared.BuildEventFileInfo;
18 using ResourceUtilities = Microsoft.Build.Shared.ResourceUtilities;
19 using ExceptionUtilities = Microsoft.Build.Shared.ExceptionHandling;
20 using System.Collections.ObjectModel;
21 
22 namespace Microsoft.Build.Construction
23 {
24     /// <remarks>
25     /// This class contains the functionality to parse a solution file and return a corresponding
26     /// MSBuild project file containing the projects and dependencies defined in the solution.
27     /// </remarks>
28     public sealed class SolutionFile
29     {
30         #region Solution specific constants
31 
32         // An example of a project line looks like this:
33         //  Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClassLibrary1", "ClassLibrary1\ClassLibrary1.csproj", "{05A5AD00-71B5-4612-AF2F-9EA9121C4111}"
34         private static readonly Lazy<Regex> s_crackProjectLine = new Lazy<Regex>(
35             () => new Regex
36                 (
37                 "^" // Beginning of line
38                 + "Project\\(\"(?<PROJECTTYPEGUID>.*)\"\\)"
39                 + "\\s*=\\s*" // Any amount of whitespace plus "=" plus any amount of whitespace
40                 + "\"(?<PROJECTNAME>.*)\""
41                 + "\\s*,\\s*" // Any amount of whitespace plus "," plus any amount of whitespace
42                 + "\"(?<RELATIVEPATH>.*)\""
43                 + "\\s*,\\s*" // Any amount of whitespace plus "," plus any amount of whitespace
44                 + "\"(?<PROJECTGUID>.*)\""
45                 + "$", // End-of-line
46                 RegexOptions.Compiled
47                 )
48             );
49 
50         // An example of a property line looks like this:
51         //      AspNetCompiler.VirtualPath = "/webprecompile"
52         // Because website projects now include the target framework moniker as
53         // one of their properties, <PROPERTYVALUE> may now have '=' in it.
54 
55         private static readonly Lazy<Regex> s_crackPropertyLine = new Lazy<Regex>(
56             () => new Regex
57                 (
58                 "^" // Beginning of line
59                 + "(?<PROPERTYNAME>[^=]*)"
60                 + "\\s*=\\s*" // Any amount of whitespace plus "=" plus any amount of whitespace
61                 + "(?<PROPERTYVALUE>.*)"
62                 + "$", // End-of-line
63                 RegexOptions.Compiled
64                 )
65             );
66 
67         internal const int slnFileMinUpgradableVersion = 7; // Minimum version for MSBuild to give a nice message
68         internal const int slnFileMinVersion = 9; // Minimum version for MSBuild to actually do anything useful
69         internal const int slnFileMaxVersion = VisualStudioConstants.CurrentVisualStudioSolutionFileVersion;
70 
71         private const string vbProjectGuid = "{F184B08F-C81C-45F6-A57F-5ABD9991F28F}";
72         private const string csProjectGuid = "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}";
73         private const string cpsProjectGuid = "{13B669BE-BB05-4DDF-9536-439F39A36129}";
74         private const string cpsCsProjectGuid = "{9A19103F-16F7-4668-BE54-9A1E7A4F7556}";
75         private const string cpsVbProjectGuid = "{778DAE3C-4631-46EA-AA77-85C1314464D9}";
76         private const string vjProjectGuid = "{E6FDF86B-F3D1-11D4-8576-0002A516ECE8}";
77         private const string vcProjectGuid = "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}";
78         private const string fsProjectGuid = "{F2A71F9B-5D33-465A-A702-920D77279786}";
79         private const string dbProjectGuid = "{C8D11400-126E-41CD-887F-60BD40844F9E}";
80         private const string wdProjectGuid = "{2CFEAB61-6A3B-4EB8-B523-560B4BEEF521}";
81         private const string webProjectGuid = "{E24C65DC-7377-472B-9ABA-BC803B73C61A}";
82         private const string solutionFolderGuid = "{2150E333-8FDC-42A3-9474-1A3956D46DE8}";
83         private const string sharedProjectGuid = "{D954291E-2A0B-460D-934E-DC6B0785DB48}";
84 
85         #endregion
86 
87         #region Member data
88 
89         private int _slnFileActualVersion = 0;               // The major version number of the .SLN file we're reading.
90         private string _solutionFile = null;                 // Could be absolute or relative path to the .SLN file.
91         private string _solutionFileDirectory = null;        // Absolute path the solution file
92         private bool _solutionContainsWebProjects = false;    // Does this SLN contain any web projects?
93         private bool _solutionContainsWebDeploymentProjects = false; // Does this SLN contain .wdproj projects?
94         private bool _parsingForConversionOnly = false;      // Are we parsing this solution to get project reference data during
95                                                              // conversion, or in preparation for actually building the solution?
96 
97         // The list of projects in this SLN, keyed by the project GUID.
98         private Dictionary<string, ProjectInSolution> _projects = null;
99 
100         // The list of projects in the SLN, in order of their appearance in the SLN.
101         private List<ProjectInSolution> _projectsInOrder = null;
102 
103         // The list of solution configurations in the solution
104         private List<SolutionConfigurationInSolution> _solutionConfigurations;
105 
106         // cached default configuration name for GetDefaultConfigurationName
107         private string _defaultConfigurationName;
108 
109         // cached default platform name for GetDefaultPlatformName
110         private string _defaultPlatformName;
111 
112         //List of warnings that occured while parsing solution
113         private ArrayList _solutionParserWarnings = null;
114 
115         //List of comments that occured while parsing solution
116         private ArrayList _solutionParserComments = null;
117 
118         // unit-testing only
119         private ArrayList _solutionParserErrorCodes = null;
120 
121         // VisualStudionVersion specified in Dev12+ solutions
122         private Version _currentVisualStudioVersion = null;
123 
124         private StreamReader _reader = null;
125         private int _currentLineNumber = 0;
126 
127         #endregion
128 
129         #region Constructors
130 
131         /// <summary>
132         /// Constructor
133         /// </summary>
SolutionFile()134         internal SolutionFile()
135         {
136             _solutionParserWarnings = new ArrayList();
137             _solutionParserErrorCodes = new ArrayList();
138             _solutionParserComments = new ArrayList();
139         }
140 
141         #endregion
142 
143         #region Properties
144 
145         /// <summary>
146         /// This property returns the list of warnings that were generated during solution parsing
147         /// </summary>
148         internal ArrayList SolutionParserWarnings
149         {
150             get
151             {
152                 return _solutionParserWarnings;
153             }
154         }
155 
156         /// <summary>
157         /// This property returns the list of comments that were generated during the solution parsing
158         /// </summary>
159         internal ArrayList SolutionParserComments
160         {
161             get
162             {
163                 return _solutionParserComments;
164             }
165         }
166 
167         /// <summary>
168         /// This property returns the list of error codes for warnings/errors that were generated during solution parsing.
169         /// </summary>
170         internal ArrayList SolutionParserErrorCodes
171         {
172             get
173             {
174                 return _solutionParserErrorCodes;
175             }
176         }
177 
178         /// <summary>
179         /// Returns the actual major version of the parsed solution file
180         /// </summary>
181         internal int Version
182         {
183             get
184             {
185                 return _slnFileActualVersion;
186             }
187         }
188 
189         /// <summary>
190         /// Returns Visual Studio major version
191         /// </summary>
192         internal int VisualStudioVersion
193         {
194             get
195             {
196                 if (_currentVisualStudioVersion != null)
197                 {
198                     return _currentVisualStudioVersion.Major;
199                 }
200                 else
201                 {
202                     return this.Version - 1;
203                 }
204             }
205         }
206 
207         /// <summary>
208         /// Returns true if the solution contains any web projects
209         /// </summary>
210         internal bool ContainsWebProjects
211         {
212             get
213             {
214                 return _solutionContainsWebProjects;
215             }
216         }
217 
218         /// <summary>
219         /// Returns true if the solution contains any .wdproj projects.  Used to determine
220         /// whether we need to load up any projects to examine dependencies.
221         /// </summary>
222         internal bool ContainsWebDeploymentProjects
223         {
224             get
225             {
226                 return _solutionContainsWebDeploymentProjects;
227             }
228         }
229 
230         /// <summary>
231         /// All projects in this solution, in the order they appeared in the solution file
232         /// </summary>
233         public IReadOnlyList<ProjectInSolution> ProjectsInOrder
234         {
235             get
236             {
237                 return _projectsInOrder.AsReadOnly();
238             }
239         }
240 
241         /// <summary>
242         /// The collection of projects in this solution, accessible by their guids as a
243         /// string in "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" form
244         /// </summary>
245         public IReadOnlyDictionary<string, ProjectInSolution> ProjectsByGuid
246         {
247             get
248             {
249                 return new ReadOnlyDictionary<string, ProjectInSolution>(_projects);
250             }
251         }
252 
253         /// <summary>
254         /// This is the read/write accessor for the solution file which we will parse.  This
255         /// must be set before calling any other methods on this class.
256         /// </summary>
257         /// <value></value>
258         internal string FullPath
259         {
260             get
261             {
262                 return _solutionFile;
263             }
264 
265             set
266             {
267                 // Should already be canonicalized to a full path
268                 ErrorUtilities.VerifyThrowInternalRooted(value);
269                 _solutionFile = value;
270             }
271         }
272 
273         internal string SolutionFileDirectory
274         {
275             get
276             {
277                 return _solutionFileDirectory;
278             }
279             // This setter is only used by the unit tests
280             set
281             {
282                 _solutionFileDirectory = value;
283             }
284         }
285 
286         /// <summary>
287         /// For unit-testing only.
288         /// </summary>
289         /// <value></value>
290         internal StreamReader SolutionReader
291         {
292             get
293             {
294                 return _reader;
295             }
296 
297             set
298             {
299                 _reader = value;
300             }
301         }
302 
303         /// <summary>
304         /// The list of all full solution configurations (configuration + platform) in this solution
305         /// </summary>
306         public IReadOnlyList<SolutionConfigurationInSolution> SolutionConfigurations
307         {
308             get
309             {
310                 return _solutionConfigurations.AsReadOnly();
311             }
312         }
313 
314         #endregion
315 
316         #region Methods
317 
318         /// <summary>
319         /// This method takes a path to a solution file, parses the projects and project dependencies
320         /// in the solution file, and creates internal data structures representing the projects within
321         /// the SLN.
322         /// </summary>
Parse(string solutionFile)323         public static SolutionFile Parse(string solutionFile)
324         {
325             SolutionFile parser = new SolutionFile();
326             parser.FullPath = solutionFile;
327 
328             parser.ParseSolutionFile();
329 
330             return parser;
331         }
332 
333         /// <summary>
334         /// Returns "true" if it's a project that's expected to be buildable, or false if it's
335         /// not (e.g. a solution folder)
336         /// </summary>
337         /// <param name="project">The project in the solution</param>
338         /// <returns>Whether the project is expected to be buildable</returns>
IsBuildableProject(ProjectInSolution project)339         internal static bool IsBuildableProject(ProjectInSolution project)
340         {
341             return (project.ProjectType != SolutionProjectType.SolutionFolder && project.ProjectConfigurations.Count > 0);
342         }
343 
344         /// <summary>
345         /// Given a solution file, parses the header and returns the major version numbers of the solution file
346         /// and the visual studio.
347         /// Throws InvalidProjectFileException if the solution header is invalid, or if the version is less than
348         /// our minimum version.
349         /// </summary>
GetSolutionFileAndVisualStudioMajorVersions(string solutionFile, out int solutionVersion, out int visualStudioMajorVersion)350         internal static void GetSolutionFileAndVisualStudioMajorVersions(string solutionFile, out int solutionVersion, out int visualStudioMajorVersion)
351         {
352             ErrorUtilities.VerifyThrow(!String.IsNullOrEmpty(solutionFile), "null solution file passed to GetSolutionFileMajorVersion!");
353             ErrorUtilities.VerifyThrowInternalRooted(solutionFile);
354 
355             const string slnFileHeaderNoVersion = "Microsoft Visual Studio Solution File, Format Version ";
356             const string slnFileVSVLinePrefix = "VisualStudioVersion";
357             FileStream fileStream = null;
358             StreamReader reader = null;
359             bool validVersionFound = false;
360 
361             solutionVersion = 0;
362             visualStudioMajorVersion = 0;
363 
364             try
365             {
366                 // Open the file
367                 fileStream = File.OpenRead(solutionFile);
368                 reader = new StreamReader(fileStream, Encoding.GetEncoding(0)); // HIGHCHAR: If solution files have no byte-order marks, then assume ANSI rather than ASCII.
369 
370                 // Read first 4 lines of the solution file.
371                 // The header is expected to be in line 1 or 2
372                 // VisualStudioVersion is expected to be in line 3 or 4.
373                 for (int i = 0; i < 4; i++)
374                 {
375                     string line = reader.ReadLine();
376 
377                     if (line == null)
378                     {
379                         break;
380                     }
381 
382                     if (line.Trim().StartsWith(slnFileHeaderNoVersion, StringComparison.Ordinal))
383                     {
384                         // Found it.  Validate the version.
385                         string fileVersionFromHeader = line.Substring(slnFileHeaderNoVersion.Length);
386 
387                         Version version = null;
388                         if (!System.Version.TryParse(fileVersionFromHeader, out version))
389                         {
390                             ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile
391                                 (
392                                     false /* just throw the exception */,
393                                     "SubCategoryForSolutionParsingErrors",
394                                     new BuildEventFileInfo(solutionFile),
395                                     "SolutionParseVersionMismatchError",
396                                     slnFileMinUpgradableVersion,
397                                     slnFileMaxVersion
398                                 );
399                         }
400 
401                         solutionVersion = version.Major;
402 
403                         // Validate against our min & max
404                         ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile
405                             (
406                                 solutionVersion >= slnFileMinUpgradableVersion,
407                                 "SubCategoryForSolutionParsingErrors",
408                                 new BuildEventFileInfo(solutionFile),
409                                 "SolutionParseVersionMismatchError",
410                                 slnFileMinUpgradableVersion,
411                                 slnFileMaxVersion
412                             );
413 
414                         validVersionFound = true;
415                     }
416                     else if (line.Trim().StartsWith(slnFileVSVLinePrefix, StringComparison.Ordinal))
417                     {
418                         Version visualStudioVersion = ParseVisualStudioVersion(line);
419                         if (visualStudioVersion != null)
420                         {
421                             visualStudioMajorVersion = visualStudioVersion.Major;
422                         }
423                     }
424                 }
425             }
426             finally
427             {
428                 if (fileStream != null)
429                 {
430                     fileStream.Dispose();
431                 }
432 
433                 if (reader != null)
434                 {
435                     reader.Dispose();
436                 }
437             }
438 
439             if (validVersionFound)
440             {
441                 return;
442             }
443 
444             // Didn't find the header in lines 1-4, so the solution file is invalid.
445             ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile
446                 (
447                     false /* just throw the exception */,
448                     "SubCategoryForSolutionParsingErrors",
449                     new BuildEventFileInfo(solutionFile),
450                     "SolutionParseNoHeaderError"
451                  );
452 
453             return; /* UNREACHABLE */
454         }
455 
456         /// <summary>
457         /// Adds a configuration to this solution
458         /// </summary>
AddSolutionConfiguration(string configurationName, string platformName)459         internal void AddSolutionConfiguration(string configurationName, string platformName)
460         {
461             _solutionConfigurations.Add(new SolutionConfigurationInSolution(configurationName, platformName));
462         }
463 
464         /// <summary>
465         /// Reads a line from the StreamReader, trimming leading and trailing whitespace.
466         /// </summary>
467         /// <returns></returns>
ReadLine()468         private string ReadLine()
469         {
470             ErrorUtilities.VerifyThrow(_reader != null, "ParseFileHeader(): reader is null!");
471 
472             string line = _reader.ReadLine();
473             _currentLineNumber++;
474 
475             if (line != null)
476             {
477                 line = line.Trim();
478             }
479 
480             return line;
481         }
482 
483         /// <summary>
484         /// This method takes a path to a solution file, parses the projects and project dependencies
485         /// in the solution file, and creates internal data structures representing the projects within
486         /// the SLN.  Used for conversion, which means it allows situations that we refuse to actually build.
487         /// </summary>
ParseSolutionFileForConversion()488         internal void ParseSolutionFileForConversion()
489         {
490             _parsingForConversionOnly = true;
491             ParseSolutionFile();
492         }
493 
494         /// <summary>
495         /// This method takes a path to a solution file, parses the projects and project dependencies
496         /// in the solution file, and creates internal data structures representing the projects within
497         /// the SLN.
498         /// </summary>
ParseSolutionFile()499         internal void ParseSolutionFile()
500         {
501             ErrorUtilities.VerifyThrow((_solutionFile != null) && (_solutionFile.Length != 0), "ParseSolutionFile() got a null solution file!");
502             ErrorUtilities.VerifyThrowInternalRooted(_solutionFile);
503 
504             FileStream fileStream = null;
505             _reader = null;
506 
507             try
508             {
509                 // Open the file
510                 fileStream = File.OpenRead(_solutionFile);
511                 // Store the directory of the file as the current directory may change while we are processes the file
512                 _solutionFileDirectory = Path.GetDirectoryName(_solutionFile);
513                 _reader = new StreamReader(fileStream, Encoding.GetEncoding(0)); // HIGHCHAR: If solution files have no byte-order marks, then assume ANSI rather than ASCII.
514                 this.ParseSolution();
515             }
516             catch (Exception e)
517             {
518                 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(!ExceptionUtilities.IsIoRelatedException(e), new BuildEventFileInfo(_solutionFile), "InvalidProjectFile", e.Message);
519                 throw;
520             }
521             finally
522             {
523                 if (fileStream != null)
524                 {
525                     fileStream.Dispose();
526                 }
527 
528                 if (_reader != null)
529                 {
530                     _reader.Dispose();
531                 }
532             }
533         }
534 
535         /// <summary>
536         /// Parses the SLN file represented by the StreamReader in this.reader, and populates internal
537         /// data structures based on the SLN file contents.
538         /// </summary>
ParseSolution()539         internal void ParseSolution()
540         {
541             _projects = new Dictionary<string, ProjectInSolution>(StringComparer.OrdinalIgnoreCase);
542             _projectsInOrder = new List<ProjectInSolution>();
543             _solutionContainsWebProjects = false;
544             _slnFileActualVersion = 0;
545             _currentLineNumber = 0;
546             _solutionConfigurations = new List<SolutionConfigurationInSolution>();
547             _defaultConfigurationName = null;
548             _defaultPlatformName = null;
549 
550             // the raw list of project configurations in solution configurations, to be processed after it's fully read in.
551             Hashtable rawProjectConfigurationsEntries = null;
552 
553             ParseFileHeader();
554 
555             string str;
556             while ((str = ReadLine()) != null)
557             {
558                 if (str.StartsWith("Project(", StringComparison.Ordinal))
559                 {
560                     ParseProject(str);
561                 }
562                 else if (str.StartsWith("GlobalSection(NestedProjects)", StringComparison.Ordinal))
563                 {
564                     ParseNestedProjects();
565                 }
566                 else if (str.StartsWith("GlobalSection(SolutionConfigurationPlatforms)", StringComparison.Ordinal))
567                 {
568                     ParseSolutionConfigurations();
569                 }
570                 else if (str.StartsWith("GlobalSection(ProjectConfigurationPlatforms)", StringComparison.Ordinal))
571                 {
572                     rawProjectConfigurationsEntries = ParseProjectConfigurations();
573                 }
574                 else if (str.StartsWith("VisualStudioVersion", StringComparison.Ordinal))
575                 {
576                     _currentVisualStudioVersion = ParseVisualStudioVersion(str);
577                 }
578                 else
579                 {
580                     // No other section types to process at this point, so just ignore the line
581                     // and continue.
582                 }
583             }
584 
585             if (rawProjectConfigurationsEntries != null)
586             {
587                 ProcessProjectConfigurationSection(rawProjectConfigurationsEntries);
588             }
589 
590             // Cache the unique name of each project, and check that we don't have any duplicates.
591             Hashtable projectsByUniqueName = new Hashtable(StringComparer.OrdinalIgnoreCase);
592 
593             foreach (ProjectInSolution proj in _projectsInOrder)
594             {
595                 // Find the unique name for the project.  This method also caches the unique name,
596                 // so it doesn't have to be recomputed later.
597                 string uniqueName = proj.GetUniqueProjectName();
598 
599                 if (proj.ProjectType == SolutionProjectType.WebProject)
600                 {
601                     // Examine port information and determine if we need to disambiguate similarly-named projects with different ports.
602                     Uri uri;
603                     if (Uri.TryCreate(proj.RelativePath, UriKind.Absolute, out uri))
604                     {
605                         if (!uri.IsDefaultPort)
606                         {
607                             // If there are no other projects with the same name as this one, then we will keep this project's unique name, otherwise
608                             // we will create a new unique name with the port added.
609                             foreach (ProjectInSolution otherProj in _projectsInOrder)
610                             {
611                                 if (Object.ReferenceEquals(proj, otherProj))
612                                 {
613                                     continue;
614                                 }
615 
616                                 if (String.Equals(otherProj.ProjectName, proj.ProjectName, StringComparison.OrdinalIgnoreCase))
617                                 {
618                                     uniqueName = String.Format(CultureInfo.InvariantCulture, "{0}:{1}", uniqueName, uri.Port);
619                                     proj.UpdateUniqueProjectName(uniqueName);
620                                     break;
621                                 }
622                             }
623                         }
624                     }
625                 }
626 
627                 // Throw an error if there are any duplicates
628                 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(
629                     projectsByUniqueName[uniqueName] == null,
630                     "SubCategoryForSolutionParsingErrors",
631                     new BuildEventFileInfo(FullPath),
632                     "SolutionParseDuplicateProject",
633                     uniqueName);
634 
635                 // Update the hash table with this unique name
636                 projectsByUniqueName[uniqueName] = proj;
637             }
638         } // ParseSolutionFile()
639 
640         /// <summary>
641         /// This method searches the first two lines of the solution file opened by the specified
642         /// StreamReader for the solution file header.  An exception is thrown if it is not found.
643         ///
644         /// The solution file header looks like this:
645         ///
646         ///     Microsoft Visual Studio Solution File, Format Version 9.00
647         ///
648         /// </summary>
ParseFileHeader()649         private void ParseFileHeader()
650         {
651             ErrorUtilities.VerifyThrow(_reader != null, "ParseFileHeader(): reader is null!");
652 
653             const string slnFileHeaderNoVersion = "Microsoft Visual Studio Solution File, Format Version ";
654 
655             // Read the file header.  This can be on either of the first two lines.
656             for (int i = 1; i <= 2; i++)
657             {
658                 string str = ReadLine();
659                 if (str == null)
660                 {
661                     break;
662                 }
663 
664                 if (str.StartsWith(slnFileHeaderNoVersion, StringComparison.Ordinal))
665                 {
666                     // Found it.  Validate the version.
667                     ValidateSolutionFileVersion(str.Substring(slnFileHeaderNoVersion.Length));
668                     return;
669                 }
670             }
671 
672             // Didn't find the header on either the first or second line, so the solution file
673             // is invalid.
674             ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(false, "SubCategoryForSolutionParsingErrors",
675                 new BuildEventFileInfo(FullPath), "SolutionParseNoHeaderError");
676         }
677 
678         /// <summary>
679         /// This method parses the Visual Studio version in Dev 12 solution files
680         /// The version line looks like this:
681         ///
682         /// VisualStudioVersion = 12.0.20311.0 VSPRO_PLATFORM
683         ///
684         /// If such a line is found, the version is stored in this.currentVisualStudioVersion
685         /// </summary>
ParseVisualStudioVersion(string str)686         private static Version ParseVisualStudioVersion(string str)
687         {
688             Version currentVisualStudioVersion = null;
689             char[] delimiterChars = { ' ', '=' };
690             string[] words = str.Split(delimiterChars, StringSplitOptions.RemoveEmptyEntries);
691 
692             if (words.Length >= 2)
693             {
694                 string versionStr = words[1];
695                 if (!System.Version.TryParse(versionStr, out currentVisualStudioVersion))
696                 {
697                     currentVisualStudioVersion = null;
698                 }
699             }
700 
701             return currentVisualStudioVersion;
702         }
703         /// <summary>
704         /// This method extracts the whole part of the version number from the specified line
705         /// containing the solution file format header, and throws an exception if the version number
706         /// is outside of the valid range.
707         ///
708         /// The solution file header looks like this:
709         ///
710         ///     Microsoft Visual Studio Solution File, Format Version 9.00
711         ///
712         /// </summary>
713         /// <param name="versionString"></param>
ValidateSolutionFileVersion(string versionString)714         private void ValidateSolutionFileVersion(string versionString)
715         {
716             ErrorUtilities.VerifyThrow(versionString != null, "ValidateSolutionFileVersion() got a null line!");
717 
718             Version version = null;
719 
720             if (!System.Version.TryParse(versionString, out version))
721             {
722                 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(false, "SubCategoryForSolutionParsingErrors",
723                     new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseVersionMismatchError",
724                     slnFileMinUpgradableVersion, slnFileMaxVersion);
725             }
726 
727             _slnFileActualVersion = version.Major;
728 
729             // Validate against our min & max
730             ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(
731                 _slnFileActualVersion >= slnFileMinUpgradableVersion,
732                 "SubCategoryForSolutionParsingErrors",
733                 new BuildEventFileInfo(FullPath, _currentLineNumber, 0),
734                 "SolutionParseVersionMismatchError",
735                 slnFileMinUpgradableVersion, slnFileMaxVersion);
736             // If the solution file version is greater than the maximum one we will create a comment rather than warn
737             // as users such as blend opening a dev10 project cannot do anything about it.
738             if (_slnFileActualVersion > slnFileMaxVersion)
739             {
740                 _solutionParserComments.Add(ResourceUtilities.FormatResourceString("UnrecognizedSolutionComment", _slnFileActualVersion));
741             }
742         }
743 
744         /// <summary>
745         ///
746         /// This method processes a "Project" section in the solution file opened by the specified
747         /// StreamReader, and returns a populated ProjectInSolution instance, if successful.
748         /// An exception is thrown if the solution file is invalid.
749         ///
750         /// The format of the parts of a Project section that we care about is as follows:
751         ///
752         ///  Project("{Project type GUID}") = "Project name", "Relative path to project file", "{Project GUID}"
753         ///      ProjectSection(ProjectDependencies) = postProject
754         ///          {Parent project unique name} = {Parent project unique name}
755         ///          ...
756         ///      EndProjectSection
757         ///  EndProject
758         ///
759         /// </summary>
760         /// <param name="firstLine"></param>
761         /// <returns></returns>
ParseProject(string firstLine)762         private void ParseProject(string firstLine)
763         {
764             ErrorUtilities.VerifyThrow((firstLine != null) && (firstLine.Length != 0), "ParseProject() got a null firstLine!");
765             ErrorUtilities.VerifyThrow(_reader != null, "ParseProject() got a null reader!");
766 
767             ProjectInSolution proj = new ProjectInSolution(this);
768 
769             // Extract the important information from the first line.
770             ParseFirstProjectLine(firstLine, proj);
771 
772             // Search for project dependencies.  Keeping reading lines until we either 1.) reach
773             // the end of the file, 2.) see "ProjectSection(ProjectDependencies)" at the beginning
774             // of the line, or 3.) see "EndProject" at the beginning of the line.
775             string line;
776             while ((line = ReadLine()) != null)
777             {
778                 // If we see an "EndProject", well ... that's the end of this project!
779                 if (line == "EndProject")
780                 {
781                     break;
782                 }
783                 else if (line.StartsWith("ProjectSection(ProjectDependencies)", StringComparison.Ordinal))
784                 {
785                     // We have a ProjectDependencies section.  Each subsequent line should identify
786                     // a dependency.
787                     line = ReadLine();
788                     while ((line != null) && (!line.StartsWith("EndProjectSection", StringComparison.Ordinal)))
789                     {
790                         // This should be a dependency.  The GUID identifying the parent project should
791                         // be both the property name and the property value.
792                         Match match = s_crackPropertyLine.Value.Match(line);
793                         ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(match.Success, "SubCategoryForSolutionParsingErrors",
794                             new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseProjectDepGuidError", proj.ProjectName);
795 
796                         string parentGuid = match.Groups["PROPERTYNAME"].Value.Trim();
797                         proj.AddDependency(parentGuid);
798 
799                         line = ReadLine();
800                     }
801                 }
802                 else if (line.StartsWith("ProjectSection(WebsiteProperties)", StringComparison.Ordinal))
803                 {
804                     // We have a WebsiteProperties section.  This section is present only in Venus
805                     // projects, and contains properties that we'll need in order to call the
806                     // AspNetCompiler task.
807                     line = ReadLine();
808                     while ((line != null) && (!line.StartsWith("EndProjectSection", StringComparison.Ordinal)))
809                     {
810                         Match match = s_crackPropertyLine.Value.Match(line);
811                         ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(match.Success, "SubCategoryForSolutionParsingErrors",
812                             new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseWebProjectPropertiesError", proj.ProjectName);
813 
814                         string propertyName = match.Groups["PROPERTYNAME"].Value.Trim();
815                         string propertyValue = match.Groups["PROPERTYVALUE"].Value.Trim();
816 
817                         ParseAspNetCompilerProperty(proj, propertyName, propertyValue);
818 
819                         line = ReadLine();
820                     }
821                 }
822             }
823 
824             ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(line != null, "SubCategoryForSolutionParsingErrors",
825                 new BuildEventFileInfo(FullPath), "SolutionParseProjectEofError", proj.ProjectName);
826 
827             // Add the project to the collection
828             AddProjectToSolution(proj);
829             // If the project is an etp project then parse the etp project file
830             // to get the projects contained in it.
831             if (IsEtpProjectFile(proj.RelativePath))
832             {
833                 ParseEtpProject(proj);
834             }
835         } // ParseProject()
836 
837         /// <summary>
838         /// This method will parse a .etp project recursively and
839         /// add all the projects found to projects and projectsInOrder
840         /// </summary>
841         /// <param name="etpProj">ETP Project</param>
ParseEtpProject(ProjectInSolution etpProj)842         internal void ParseEtpProject(ProjectInSolution etpProj)
843         {
844             XmlDocument etpProjectDocument = new XmlDocument();
845             // Get the full path to the .etp project file
846             string fullPathToEtpProj = Path.Combine(_solutionFileDirectory, etpProj.RelativePath);
847             string etpProjectRelativeDir = Path.GetDirectoryName(etpProj.RelativePath);
848             try
849             {
850                 /****************************************************************************
851                 * A Typical .etp project file will look like this
852                 *<?xml version="1.0"?>
853                 *<EFPROJECT>
854                 *    <GENERAL>
855                 *        <BANNER>Microsoft Visual Studio Application Template File</BANNER>
856                 *        <VERSION>1.00</VERSION>
857                 *        <Views>
858                 *            <ProjectExplorer>
859                 *                <File>ClassLibrary2\ClassLibrary2.csproj</File>
860                 *            </ProjectExplorer>
861                 *        </Views>
862                 *        <References>
863                 *            <Reference>
864                 *                <FILE>ClassLibrary2\ClassLibrary2.csproj</FILE>
865                 *                <GUIDPROJECTID>{73D0F4CE-D9D3-4E8B-81E4-B26FBF4CC2FE}</GUIDPROJECTID>
866                 *            </Reference>
867                 *        </References>
868                 *    </GENERAL>
869                 *</EFPROJECT>
870                 **********************************************************************************/
871                 // Make sure the XML reader ignores DTD processing
872                 XmlReaderSettings readerSettings = new XmlReaderSettings();
873                 readerSettings.DtdProcessing = DtdProcessing.Ignore;
874 
875                 // Load the .etp project file thru the XML reader
876                 using (XmlReader xmlReader = XmlReader.Create(fullPathToEtpProj, readerSettings))
877                 {
878                     etpProjectDocument.Load(xmlReader);
879                 }
880 
881                 // We need to parse the .etp project file to get the names of projects contained
882                 // in the .etp Project. The projects are listed under /EFPROJECT/GENERAL/References/Reference node in the .etp project file.
883                 // The /EFPROJECT/GENERAL/Views/ProjectExplorer node will not necessarily contain
884                 // all the projects in the .etp project. Therefore, we need to look at
885                 // /EFPROJECT/GENERAL/References/Reference.
886                 // Find the /EFPROJECT/GENERAL/References/Reference node
887                 // Note that this is case sensitive
888                 XmlNodeList referenceNodes = etpProjectDocument.DocumentElement.SelectNodes("/EFPROJECT/GENERAL/References/Reference");
889                 // Do the right thing for each <REference> element
890                 foreach (XmlNode referenceNode in referenceNodes)
891                 {
892                     // Get the relative path to the project file
893                     string fileElementValue = referenceNode.SelectSingleNode("FILE").InnerText;
894                     // If <FILE>  element is not present under <Reference> then we don't do anything.
895                     if (fileElementValue != null)
896                     {
897                         // Create and populate a ProjectInSolution for the project
898                         ProjectInSolution proj = new ProjectInSolution(this);
899                         proj.RelativePath = Path.Combine(etpProjectRelativeDir, fileElementValue);
900 
901                         // Verify the relative path specified in the .etp proj file
902                         ValidateProjectRelativePath(proj);
903                         proj.ProjectType = SolutionProjectType.EtpSubProject;
904                         proj.ProjectName = proj.RelativePath;
905                         XmlNode projGuidNode = referenceNode.SelectSingleNode("GUIDPROJECTID");
906                         if (projGuidNode != null)
907                         {
908                             proj.ProjectGuid = projGuidNode.InnerText;
909                         }
910                         // It is ok for a project to not have a guid inside an etp project.
911                         // If a solution file contains a project without a guid it fails to
912                         // load in Everett. But if an etp project contains a project without
913                         // a guid it loads well in Everett and p2p references to/from this project
914                         // are preserved. So we should make sure that we don’t error in this
915                         // situation while upgrading.
916                         else
917                         {
918                             proj.ProjectGuid = String.Empty;
919                         }
920                         // Add the recently created proj to the collection of projects
921                         AddProjectToSolution(proj);
922                         // If the project is an etp project recurse
923                         if (IsEtpProjectFile(fileElementValue))
924                         {
925                             ParseEtpProject(proj);
926                         }
927                     }
928                 }
929             }
930             // catch all sorts of exceptions - if we encounter any problems here, we just assume the .etp project file is not in the correct format
931 
932             // handle security errors
933             catch (SecurityException e)
934             {
935                 // Log a warning
936                 string errorCode, ignoredKeyword;
937                 string warning = ResourceUtilities.FormatResourceString(out errorCode, out ignoredKeyword, "Shared.ProjectFileCouldNotBeLoaded",
938                     etpProj.RelativePath, e.Message);
939                 _solutionParserWarnings.Add(warning);
940                 _solutionParserErrorCodes.Add(errorCode);
941             }
942             // handle errors in path resolution
943             catch (NotSupportedException e)
944             {
945                 // Log a warning
946                 string errorCode, ignoredKeyword;
947                 string warning = ResourceUtilities.FormatResourceString(out errorCode, out ignoredKeyword, "Shared.ProjectFileCouldNotBeLoaded",
948                     etpProj.RelativePath, e.Message);
949                 _solutionParserWarnings.Add(warning);
950                 _solutionParserErrorCodes.Add(errorCode);
951             }
952             // handle errors in loading project file
953             catch (IOException e)
954             {
955                 // Log a warning
956                 string errorCode, ignoredKeyword;
957                 string warning = ResourceUtilities.FormatResourceString(out errorCode, out ignoredKeyword, "Shared.ProjectFileCouldNotBeLoaded",
958                     etpProj.RelativePath, e.Message);
959                 _solutionParserWarnings.Add(warning);
960                 _solutionParserErrorCodes.Add(errorCode);
961             }
962             // handle errors in loading project file
963             catch (UnauthorizedAccessException e)
964             {
965                 // Log a warning
966                 string errorCode, ignoredKeyword;
967                 string warning = ResourceUtilities.FormatResourceString(out errorCode, out ignoredKeyword, "Shared.ProjectFileCouldNotBeLoaded",
968                     etpProj.RelativePath, e.Message);
969                 _solutionParserWarnings.Add(warning);
970                 _solutionParserErrorCodes.Add(errorCode);
971             }
972             // handle XML parsing errors
973             catch (XmlException e)
974             {
975                 // Log a warning
976                 string errorCode, ignoredKeyword;
977                 string warning = ResourceUtilities.FormatResourceString(out errorCode, out ignoredKeyword, "Shared.InvalidProjectFile",
978                    etpProj.RelativePath, e.Message);
979                 _solutionParserWarnings.Add(warning);
980                 _solutionParserErrorCodes.Add(errorCode);
981             }
982         }
983 
984         /// <summary>
985         /// Adds a given project to the project collections of this class
986         /// </summary>
987         /// <param name="proj">proj</param>
AddProjectToSolution(ProjectInSolution proj)988         private void AddProjectToSolution(ProjectInSolution proj)
989         {
990             if (!String.IsNullOrEmpty(proj.ProjectGuid))
991             {
992                 _projects[proj.ProjectGuid] = proj;
993             }
994             _projectsInOrder.Add(proj);
995         }
996 
997         /// <summary>
998         /// Checks whether a given project has a .etp extension.
999         /// </summary>
1000         /// <param name="projectFile"></param>
IsEtpProjectFile(string projectFile)1001         private bool IsEtpProjectFile(string projectFile)
1002         {
1003             return projectFile.EndsWith(".etp", StringComparison.OrdinalIgnoreCase);
1004         }
1005 
1006         /// <summary>
1007         /// Validate relative path of a project
1008         /// </summary>
1009         /// <param name="proj">proj</param>
ValidateProjectRelativePath(ProjectInSolution proj)1010         private void ValidateProjectRelativePath(ProjectInSolution proj)
1011         {
1012             // Verify the relative path is not null
1013             ErrorUtilities.VerifyThrow(proj.RelativePath != null, "Project relative path cannot be null.");
1014 
1015             // Verify the relative path does not contain invalid characters
1016             ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(proj.RelativePath.IndexOfAny(Path.GetInvalidPathChars()) == -1,
1017               "SubCategoryForSolutionParsingErrors",
1018               new BuildEventFileInfo(FullPath, _currentLineNumber, 0),
1019               "SolutionParseInvalidProjectFileNameCharacters",
1020               proj.ProjectName, proj.RelativePath);
1021 
1022             // Verify the relative path is not empty string
1023             ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(proj.RelativePath.Length > 0,
1024                   "SubCategoryForSolutionParsingErrors",
1025                   new BuildEventFileInfo(FullPath, _currentLineNumber, 0),
1026                   "SolutionParseInvalidProjectFileNameEmpty",
1027                   proj.ProjectName);
1028         }
1029 
1030         /// <summary>
1031         /// Takes a property name / value that comes from the SLN file for a Venus project, and
1032         /// stores it appropriately in our data structures.
1033         /// </summary>
1034         /// <param name="proj"></param>
1035         /// <param name="propertyName"></param>
1036         /// <param name="propertyValue"></param>
ParseAspNetCompilerProperty( ProjectInSolution proj, string propertyName, string propertyValue )1037         private void ParseAspNetCompilerProperty
1038             (
1039             ProjectInSolution proj,
1040             string propertyName,
1041             string propertyValue
1042             )
1043         {
1044             // What we expect to find in the SLN file is something that looks like this:
1045             //
1046             // Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "c:\...\myfirstwebsite\", "..\..\..\..\..\..\rajeev\temp\websites\myfirstwebsite", "{956CC04E-FD59-49A9-9099-96888CB6F366}"
1047             //     ProjectSection(WebsiteProperties) = preProject
1048             //       TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.0"
1049             //       ProjectReferences = "{FD705688-88D1-4C22-9BFF-86235D89C2FC}|CSClassLibrary1.dll;{F0726D09-042B-4A7A-8A01-6BED2422BD5D}|VCClassLibrary1.dll;"
1050             //       Debug.AspNetCompiler.VirtualPath = "/publishfirst"
1051             //       Debug.AspNetCompiler.PhysicalPath = "..\..\..\..\..\..\rajeev\temp\websites\myfirstwebsite\"
1052             //       Debug.AspNetCompiler.TargetPath = "..\..\..\..\..\..\rajeev\temp\publishfirst\"
1053             //       Debug.AspNetCompiler.ForceOverwrite = "true"
1054             //       Debug.AspNetCompiler.Updateable = "true"
1055             //       Debug.AspNetCompiler.Enabled = "true"
1056             //       Debug.AspNetCompiler.Debug = "true"
1057             //       Debug.AspNetCompiler.KeyFile = ""
1058             //       Debug.AspNetCompiler.KeyContainer = ""
1059             //       Debug.AspNetCompiler.DelaySign = "true"
1060             //       Debug.AspNetCompiler.AllowPartiallyTrustedCallers = "true"
1061             //       Debug.AspNetCompiler.FixedNames = "true"
1062             //       Release.AspNetCompiler.VirtualPath = "/publishfirst"
1063             //       Release.AspNetCompiler.PhysicalPath = "..\..\..\..\..\..\rajeev\temp\websites\myfirstwebsite\"
1064             //       Release.AspNetCompiler.TargetPath = "..\..\..\..\..\..\rajeev\temp\publishfirst\"
1065             //       Release.AspNetCompiler.ForceOverwrite = "true"
1066             //       Release.AspNetCompiler.Updateable = "true"
1067             //       Release.AspNetCompiler.Enabled = "true"
1068             //       Release.AspNetCompiler.Debug = "false"
1069             //       Release.AspNetCompiler.KeyFile = ""
1070             //       Release.AspNetCompiler.KeyContainer = ""
1071             //       Release.AspNetCompiler.DelaySign = "true"
1072             //       Release.AspNetCompiler.AllowPartiallyTrustedCallers = "true"
1073             //       Release.AspNetCompiler.FixedNames = "true"
1074             //     EndProjectSection
1075             // EndProject
1076             //
1077             // This method is responsible for parsing each of the lines within the "WebsiteProperties" section.
1078             // The first component of each property name is actually the configuration for which that
1079             // property applies.
1080 
1081             int indexOfFirstDot = propertyName.IndexOf('.');
1082             if (indexOfFirstDot != -1)
1083             {
1084                 // The portion before the first dot is the configuration name.
1085                 string configurationName = propertyName.Substring(0, indexOfFirstDot);
1086 
1087                 // The rest of it is the actual property name.
1088                 string aspNetPropertyName = ((propertyName.Length - indexOfFirstDot) > 0) ? propertyName.Substring(indexOfFirstDot + 1, propertyName.Length - indexOfFirstDot - 1) : "";
1089 
1090                 // And the part after the <equals> sign is the property value (which was parsed out for us prior
1091                 // to calling this method).
1092                 propertyValue = TrimQuotes(propertyValue);
1093 
1094                 // Grab the parameters for this specific configuration if they exist.
1095                 object aspNetCompilerParametersObject = proj.AspNetConfigurations[configurationName];
1096                 AspNetCompilerParameters aspNetCompilerParameters;
1097 
1098                 if (aspNetCompilerParametersObject == null)
1099                 {
1100                     // If it didn't exist, create a new one.
1101                     aspNetCompilerParameters = new AspNetCompilerParameters();
1102                     aspNetCompilerParameters.aspNetVirtualPath = String.Empty;
1103                     aspNetCompilerParameters.aspNetPhysicalPath = String.Empty;
1104                     aspNetCompilerParameters.aspNetTargetPath = String.Empty;
1105                     aspNetCompilerParameters.aspNetForce = String.Empty;
1106                     aspNetCompilerParameters.aspNetUpdateable = String.Empty;
1107                     aspNetCompilerParameters.aspNetDebug = String.Empty;
1108                     aspNetCompilerParameters.aspNetKeyFile = String.Empty;
1109                     aspNetCompilerParameters.aspNetKeyContainer = String.Empty;
1110                     aspNetCompilerParameters.aspNetDelaySign = String.Empty;
1111                     aspNetCompilerParameters.aspNetAPTCA = String.Empty;
1112                     aspNetCompilerParameters.aspNetFixedNames = String.Empty;
1113                 }
1114                 else
1115                 {
1116                     // Otherwise just unbox it.
1117                     aspNetCompilerParameters = (AspNetCompilerParameters)aspNetCompilerParametersObject;
1118                 }
1119 
1120                 // Update the appropriate field within the parameters struct.
1121                 if (aspNetPropertyName == "AspNetCompiler.VirtualPath")
1122                 {
1123                     aspNetCompilerParameters.aspNetVirtualPath = propertyValue;
1124                 }
1125                 else if (aspNetPropertyName == "AspNetCompiler.PhysicalPath")
1126                 {
1127                     aspNetCompilerParameters.aspNetPhysicalPath = propertyValue;
1128                 }
1129                 else if (aspNetPropertyName == "AspNetCompiler.TargetPath")
1130                 {
1131                     aspNetCompilerParameters.aspNetTargetPath = propertyValue;
1132                 }
1133                 else if (aspNetPropertyName == "AspNetCompiler.ForceOverwrite")
1134                 {
1135                     aspNetCompilerParameters.aspNetForce = propertyValue;
1136                 }
1137                 else if (aspNetPropertyName == "AspNetCompiler.Updateable")
1138                 {
1139                     aspNetCompilerParameters.aspNetUpdateable = propertyValue;
1140                 }
1141                 else if (aspNetPropertyName == "AspNetCompiler.Debug")
1142                 {
1143                     aspNetCompilerParameters.aspNetDebug = propertyValue;
1144                 }
1145                 else if (aspNetPropertyName == "AspNetCompiler.KeyFile")
1146                 {
1147                     aspNetCompilerParameters.aspNetKeyFile = propertyValue;
1148                 }
1149                 else if (aspNetPropertyName == "AspNetCompiler.KeyContainer")
1150                 {
1151                     aspNetCompilerParameters.aspNetKeyContainer = propertyValue;
1152                 }
1153                 else if (aspNetPropertyName == "AspNetCompiler.DelaySign")
1154                 {
1155                     aspNetCompilerParameters.aspNetDelaySign = propertyValue;
1156                 }
1157                 else if (aspNetPropertyName == "AspNetCompiler.AllowPartiallyTrustedCallers")
1158                 {
1159                     aspNetCompilerParameters.aspNetAPTCA = propertyValue;
1160                 }
1161                 else if (aspNetPropertyName == "AspNetCompiler.FixedNames")
1162                 {
1163                     aspNetCompilerParameters.aspNetFixedNames = propertyValue;
1164                 }
1165 
1166                 // Store the updated parameters struct back into the hashtable by configuration name.
1167                 proj.AspNetConfigurations[configurationName] = aspNetCompilerParameters;
1168             }
1169             else
1170             {
1171                 // ProjectReferences = "{FD705688-88D1-4C22-9BFF-86235D89C2FC}|CSClassLibrary1.dll;{F0726D09-042B-4A7A-8A01-6BED2422BD5D}|VCClassLibrary1.dll;"
1172                 if (string.Compare(propertyName, "ProjectReferences", StringComparison.OrdinalIgnoreCase) == 0)
1173                 {
1174                     string[] projectReferenceEntries = propertyValue.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
1175 
1176                     foreach (string projectReferenceEntry in projectReferenceEntries)
1177                     {
1178                         int indexOfBar = projectReferenceEntry.IndexOf('|');
1179 
1180                         // indexOfBar could be -1 if we had semicolons in the file names, so skip entries that
1181                         // don't contain a guid. File names may not contain the '|' character
1182                         if (indexOfBar != -1)
1183                         {
1184                             int indexOfOpeningBrace = projectReferenceEntry.IndexOf('{');
1185                             if (indexOfOpeningBrace != -1)
1186                             {
1187                                 int indexOfClosingBrace = projectReferenceEntry.IndexOf('}', indexOfOpeningBrace);
1188                                 if (indexOfClosingBrace != -1)
1189                                 {
1190                                     string referencedProjectGuid = projectReferenceEntry.Substring(indexOfOpeningBrace,
1191                                         indexOfClosingBrace - indexOfOpeningBrace + 1);
1192 
1193                                     proj.AddDependency(referencedProjectGuid);
1194                                     proj.ProjectReferences.Add(referencedProjectGuid);
1195                                 }
1196                             }
1197                         }
1198                     }
1199                 }
1200                 else if (String.Compare(propertyName, "TargetFrameworkMoniker", StringComparison.OrdinalIgnoreCase) == 0)
1201                 {
1202                     //Website project need to back support 3.5 msbuild parser for the Blend (it is not move to .Net4.0 yet.)
1203                     //However, 3.5 version of Solution parser can't handle a equal sign in the value.
1204                     //The "=" in targetframeworkMoniker was escaped to "%3D" for Orcas
1205                     string targetFrameworkMoniker = TrimQuotes(propertyValue);
1206                     proj.TargetFrameworkMoniker = Microsoft.Build.Shared.EscapingUtilities.UnescapeAll(targetFrameworkMoniker);
1207                 }
1208             }
1209         }
1210 
1211         /// <summary>
1212         /// Strips a single pair of leading/trailing double-quotes from a string.
1213         /// </summary>
1214         /// <param name="property"></param>
1215         /// <returns></returns>
TrimQuotes( string property )1216         private string TrimQuotes
1217             (
1218             string property
1219             )
1220         {
1221             // If the incoming string starts and ends with a double-quote, strip the double-quotes.
1222             if ((property != null) && (property.Length > 0) && (property[0] == '"') && (property[property.Length - 1] == '"'))
1223             {
1224                 return property.Substring(1, property.Length - 2);
1225             }
1226             else
1227             {
1228                 return property;
1229             }
1230         }
1231 
1232         /// <summary>
1233         /// Parse the first line of a Project section of a solution file. This line should look like:
1234         ///
1235         ///  Project("{Project type GUID}") = "Project name", "Relative path to project file", "{Project GUID}"
1236         ///
1237         /// </summary>
1238         /// <param name="firstLine"></param>
1239         /// <param name="proj"></param>
ParseFirstProjectLine( string firstLine, ProjectInSolution proj )1240         internal void ParseFirstProjectLine
1241         (
1242             string firstLine,
1243             ProjectInSolution proj
1244         )
1245         {
1246             Match match = s_crackProjectLine.Value.Match(firstLine);
1247             ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(match.Success, "SubCategoryForSolutionParsingErrors",
1248                 new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseProjectError");
1249 
1250             string projectTypeGuid = match.Groups["PROJECTTYPEGUID"].Value.Trim();
1251             proj.ProjectName = match.Groups["PROJECTNAME"].Value.Trim();
1252             proj.RelativePath = match.Groups["RELATIVEPATH"].Value.Trim();
1253             proj.ProjectGuid = match.Groups["PROJECTGUID"].Value.Trim();
1254 
1255             // If the project name is empty (as in some bad solutions) set it to some generated generic value.
1256             // This allows us to at least generate reasonable target names etc. instead of crashing.
1257             if (String.IsNullOrEmpty(proj.ProjectName))
1258             {
1259                 proj.ProjectName = "EmptyProjectName." + Guid.NewGuid();
1260             }
1261 
1262             // Validate project relative path
1263             ValidateProjectRelativePath(proj);
1264 
1265             // Figure out what type of project this is.
1266             if ((String.Compare(projectTypeGuid, vbProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) ||
1267                 (String.Compare(projectTypeGuid, csProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) ||
1268                 (String.Compare(projectTypeGuid, cpsProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) ||
1269                 (String.Compare(projectTypeGuid, cpsCsProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) ||
1270                 (String.Compare(projectTypeGuid, cpsVbProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) ||
1271                 (String.Compare(projectTypeGuid, fsProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) ||
1272                 (String.Compare(projectTypeGuid, dbProjectGuid, StringComparison.OrdinalIgnoreCase) == 0) ||
1273                 (String.Compare(projectTypeGuid, vjProjectGuid, StringComparison.OrdinalIgnoreCase) == 0))
1274             {
1275                 proj.ProjectType = SolutionProjectType.KnownToBeMSBuildFormat;
1276             }
1277             else if (String.Compare(projectTypeGuid, sharedProjectGuid, StringComparison.OrdinalIgnoreCase) == 0)
1278             {
1279                 proj.ProjectType = SolutionProjectType.SharedProject;
1280             }
1281             else if (String.Compare(projectTypeGuid, solutionFolderGuid, StringComparison.OrdinalIgnoreCase) == 0)
1282             {
1283                 proj.ProjectType = SolutionProjectType.SolutionFolder;
1284             }
1285             // MSBuild format VC projects have the same project type guid as old style VC projects.
1286             // If it's not an old-style VC project, we'll assume it's MSBuild format
1287             else if (String.Compare(projectTypeGuid, vcProjectGuid, StringComparison.OrdinalIgnoreCase) == 0)
1288             {
1289                 if (String.Equals(proj.Extension, ".vcproj", StringComparison.OrdinalIgnoreCase))
1290                 {
1291                     if (!_parsingForConversionOnly)
1292                     {
1293                         ProjectFileErrorUtilities.ThrowInvalidProjectFile(new BuildEventFileInfo(FullPath), "ProjectUpgradeNeededToVcxProj", proj.RelativePath);
1294                     }
1295                     // otherwise, we're parsing this solution file because we want the P2P information during
1296                     // conversion, and it's perfectly valid for an unconverted solution file to still contain .vcprojs
1297                 }
1298                 else
1299                 {
1300                     proj.ProjectType = SolutionProjectType.KnownToBeMSBuildFormat;
1301                 }
1302             }
1303             else if (String.Compare(projectTypeGuid, webProjectGuid, StringComparison.OrdinalIgnoreCase) == 0)
1304             {
1305                 proj.ProjectType = SolutionProjectType.WebProject;
1306                 _solutionContainsWebProjects = true;
1307             }
1308             else if (String.Compare(projectTypeGuid, wdProjectGuid, StringComparison.OrdinalIgnoreCase) == 0)
1309             {
1310                 proj.ProjectType = SolutionProjectType.WebDeploymentProject;
1311                 _solutionContainsWebDeploymentProjects = true;
1312             }
1313             else
1314             {
1315                 proj.ProjectType = SolutionProjectType.Unknown;
1316             }
1317         }
1318 
1319         /// <summary>
1320         /// Read nested projects section.
1321         /// This is required to find a unique name for each project's target
1322         /// </summary>
ParseNestedProjects()1323         internal void ParseNestedProjects()
1324         {
1325             string str;
1326 
1327             do
1328             {
1329                 str = ReadLine();
1330                 if ((str == null) || (str == "EndGlobalSection"))
1331                 {
1332                     break;
1333                 }
1334 
1335                 if (String.IsNullOrWhiteSpace(str))
1336                 {
1337                     continue;
1338                 }
1339 
1340                 Match match = s_crackPropertyLine.Value.Match(str);
1341                 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(match.Success, "SubCategoryForSolutionParsingErrors",
1342                     new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseNestedProjectError");
1343 
1344                 string projectGuid = match.Groups["PROPERTYNAME"].Value.Trim();
1345                 string parentProjectGuid = match.Groups["PROPERTYVALUE"].Value.Trim();
1346 
1347                 ProjectInSolution proj;
1348                 if (!_projects.TryGetValue(projectGuid, out proj))
1349                 {
1350                     ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(proj != null, "SubCategoryForSolutionParsingErrors",
1351                        new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseNestedProjectUndefinedError", projectGuid, parentProjectGuid);
1352                 }
1353 
1354                 proj.ParentProjectGuid = parentProjectGuid;
1355             } while (true);
1356         }
1357 
1358         /// <summary>
1359         /// Read solution configuration section.
1360         /// </summary>
1361         /// <remarks>
1362         /// A sample section:
1363         ///
1364         /// GlobalSection(SolutionConfigurationPlatforms) = preSolution
1365         ///     Debug|Any CPU = Debug|Any CPU
1366         ///     Release|Any CPU = Release|Any CPU
1367         /// EndGlobalSection
1368         /// </remarks>
ParseSolutionConfigurations()1369         internal void ParseSolutionConfigurations()
1370         {
1371             string str;
1372             char[] nameValueSeparators = new char[] { '=' };
1373             char[] configPlatformSeparators = new char[] { SolutionConfigurationInSolution.ConfigurationPlatformSeparator };
1374 
1375             do
1376             {
1377                 str = ReadLine();
1378 
1379                 if ((str == null) || (str == "EndGlobalSection"))
1380                 {
1381                     break;
1382                 }
1383 
1384                 if (String.IsNullOrWhiteSpace(str))
1385                 {
1386                     continue;
1387                 }
1388 
1389                 string[] configurationNames = str.Split(nameValueSeparators);
1390 
1391                 // There should be exactly one '=' character, separating two names.
1392                 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(configurationNames.Length == 2, "SubCategoryForSolutionParsingErrors",
1393                     new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseInvalidSolutionConfigurationEntry", str);
1394 
1395                 string fullConfigurationName = configurationNames[0].Trim();
1396 
1397                 //Fixing bug 555577: Solution file can have description information, in which case we ignore.
1398                 if (0 == String.Compare(fullConfigurationName, "DESCRIPTION", StringComparison.OrdinalIgnoreCase))
1399                     continue;
1400 
1401                 // Both names must be identical
1402                 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(fullConfigurationName == configurationNames[1].Trim(), "SubCategoryForSolutionParsingErrors",
1403                     new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseInvalidSolutionConfigurationEntry", str);
1404 
1405                 string[] configurationPlatformParts = fullConfigurationName.Split(configPlatformSeparators);
1406 
1407                 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(configurationPlatformParts.Length == 2, "SubCategoryForSolutionParsingErrors",
1408                     new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseInvalidSolutionConfigurationEntry", str);
1409 
1410                 _solutionConfigurations.Add(new SolutionConfigurationInSolution(configurationPlatformParts[0], configurationPlatformParts[1]));
1411             } while (true);
1412         }
1413 
1414         /// <summary>
1415         /// Read project configurations in solution configurations section.
1416         /// </summary>
1417         /// <remarks>
1418         /// A sample (incomplete) section:
1419         ///
1420         /// GlobalSection(ProjectConfigurationPlatforms) = postSolution
1421         /// 	{6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
1422         /// 	{6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Any CPU.Build.0 = Debug|Any CPU
1423         /// 	{6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.ActiveCfg = Release|Any CPU
1424         /// 	{6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Mixed Platforms.Build.0 = Release|Any CPU
1425         /// 	{6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Win32.ActiveCfg = Debug|Any CPU
1426         /// 	{A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Any CPU.ActiveCfg = Release|Win32
1427         /// 	{A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Mixed Platforms.ActiveCfg = Release|Win32
1428         /// 	{A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Mixed Platforms.Build.0 = Release|Win32
1429         /// 	{A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Win32.ActiveCfg = Release|Win32
1430         /// 	{A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Win32.Build.0 = Release|Win32
1431         /// EndGlobalSection
1432         /// </remarks>
1433         /// <returns>An unprocessed hashtable of entries in this section</returns>
ParseProjectConfigurations()1434         internal Hashtable ParseProjectConfigurations()
1435         {
1436             Hashtable rawProjectConfigurationsEntries = new Hashtable(StringComparer.OrdinalIgnoreCase);
1437             string str;
1438 
1439             do
1440             {
1441                 str = ReadLine();
1442 
1443                 if ((str == null) || (str == "EndGlobalSection"))
1444                 {
1445                     break;
1446                 }
1447 
1448                 if (String.IsNullOrWhiteSpace(str))
1449                 {
1450                     continue;
1451                 }
1452 
1453                 string[] nameValue = str.Split(new char[] { '=' });
1454 
1455                 // There should be exactly one '=' character, separating the name and value.
1456                 ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(nameValue.Length == 2, "SubCategoryForSolutionParsingErrors",
1457                     new BuildEventFileInfo(FullPath, _currentLineNumber, 0), "SolutionParseInvalidProjectSolutionConfigurationEntry", str);
1458 
1459                 rawProjectConfigurationsEntries[nameValue[0].Trim()] = nameValue[1].Trim();
1460             } while (true);
1461 
1462             return rawProjectConfigurationsEntries;
1463         }
1464 
1465         /// <summary>
1466         /// Read the project configuration information for every project in the solution, using pre-cached
1467         /// solution section data.
1468         /// </summary>
1469         /// <param name="rawProjectConfigurationsEntries">Cached data from the project configuration section</param>
ProcessProjectConfigurationSection(Hashtable rawProjectConfigurationsEntries)1470         internal void ProcessProjectConfigurationSection(Hashtable rawProjectConfigurationsEntries)
1471         {
1472             // Instead of parsing the data line by line, we parse it project by project, constructing the
1473             // entry name (e.g. "{A6F99D27-47B9-4EA4-BFC9-25157CBDC281}.Release|Any CPU.ActiveCfg") and retrieving its
1474             // value from the raw data. The reason for this is that the IDE does it this way, and as the result
1475             // the '.' character is allowed in configuration names although it technically separates different
1476             // parts of the entry name string. This could lead to ambiguous results if we tried to parse
1477             // the entry name instead of constructing it and looking it up. Although it's pretty unlikely that
1478             // this would ever be a problem, it's safer to do it the same way VS IDE does it.
1479             char[] configPlatformSeparators = new char[] { SolutionConfigurationInSolution.ConfigurationPlatformSeparator };
1480 
1481             foreach (ProjectInSolution project in _projectsInOrder)
1482             {
1483                 // Solution folders don't have configurations
1484                 if (project.ProjectType != SolutionProjectType.SolutionFolder)
1485                 {
1486                     foreach (SolutionConfigurationInSolution solutionConfiguration in _solutionConfigurations)
1487                     {
1488                         // The "ActiveCfg" entry defines the active project configuration in the given solution configuration
1489                         // This entry must be present for every possible solution configuration/project combination.
1490                         string entryNameActiveConfig = string.Format(CultureInfo.InvariantCulture, "{0}.{1}.ActiveCfg",
1491                             project.ProjectGuid, solutionConfiguration.FullName);
1492 
1493                         // The "Build.0" entry tells us whether to build the project configuration in the given solution configuration.
1494                         // Technically, it specifies a configuration name of its own which seems to be a remnant of an initial,
1495                         // more flexible design of solution configurations (as well as the '.0' suffix - no higher values are ever used).
1496                         // The configuration name is not used, and the whole entry means "build the project configuration"
1497                         // if it's present in the solution file, and "don't build" if it's not.
1498                         string entryNameBuild = string.Format(CultureInfo.InvariantCulture, "{0}.{1}.Build.0",
1499                             project.ProjectGuid, solutionConfiguration.FullName);
1500 
1501                         if (rawProjectConfigurationsEntries.ContainsKey(entryNameActiveConfig))
1502                         {
1503                             string[] configurationPlatformParts = ((string)(rawProjectConfigurationsEntries[entryNameActiveConfig])).Split(configPlatformSeparators);
1504 
1505                             // Project configuration may not necessarily contain the platform part. Some project support only the configuration part.
1506                             ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(configurationPlatformParts.Length <= 2, "SubCategoryForSolutionParsingErrors",
1507                                 new BuildEventFileInfo(FullPath), "SolutionParseInvalidProjectSolutionConfigurationEntry",
1508                                 string.Format(CultureInfo.InvariantCulture, "{0} = {1}", entryNameActiveConfig, rawProjectConfigurationsEntries[entryNameActiveConfig]));
1509 
1510                             ProjectConfigurationInSolution projectConfiguration = new ProjectConfigurationInSolution(
1511                                 configurationPlatformParts[0],
1512                                 (configurationPlatformParts.Length > 1) ? configurationPlatformParts[1] : string.Empty,
1513                                 rawProjectConfigurationsEntries.ContainsKey(entryNameBuild)
1514                             );
1515 
1516                             project.SetProjectConfiguration(solutionConfiguration.FullName, projectConfiguration);
1517                         }
1518                     }
1519                 }
1520             }
1521         }
1522 
1523         /// <summary>
1524         /// Gets the default configuration name for this solution. Usually it's Debug, unless it's not present
1525         /// in which case it's the first configuration name we find.
1526         /// </summary>
GetDefaultConfigurationName()1527         public string GetDefaultConfigurationName()
1528         {
1529             // Have we done this already? Return the cached name
1530             if (_defaultConfigurationName != null)
1531             {
1532                 return _defaultConfigurationName;
1533             }
1534 
1535             _defaultConfigurationName = string.Empty;
1536 
1537             // Pick the Debug configuration as default if present
1538             foreach (SolutionConfigurationInSolution solutionConfiguration in this.SolutionConfigurations)
1539             {
1540                 if (string.Compare(solutionConfiguration.ConfigurationName, "Debug", StringComparison.OrdinalIgnoreCase) == 0)
1541                 {
1542                     _defaultConfigurationName = solutionConfiguration.ConfigurationName;
1543                     break;
1544                 }
1545             }
1546 
1547             // Failing that, just pick the first configuration name as default
1548             if ((_defaultConfigurationName.Length == 0) && (this.SolutionConfigurations.Count > 0))
1549             {
1550                 _defaultConfigurationName = this.SolutionConfigurations[0].ConfigurationName;
1551             }
1552 
1553             return _defaultConfigurationName;
1554         }
1555 
1556         /// <summary>
1557         /// Gets the default platform name for this solution. Usually it's Mixed Platforms, unless it's not present
1558         /// in which case it's the first platform name we find.
1559         /// </summary>
GetDefaultPlatformName()1560         public string GetDefaultPlatformName()
1561         {
1562             // Have we done this already? Return the cached name
1563             if (_defaultPlatformName != null)
1564             {
1565                 return _defaultPlatformName;
1566             }
1567 
1568             _defaultPlatformName = string.Empty;
1569 
1570             // Pick the Mixed Platforms platform as default if present
1571             foreach (SolutionConfigurationInSolution solutionConfiguration in this.SolutionConfigurations)
1572             {
1573                 if (string.Compare(solutionConfiguration.PlatformName, "Mixed Platforms", StringComparison.OrdinalIgnoreCase) == 0)
1574                 {
1575                     _defaultPlatformName = solutionConfiguration.PlatformName;
1576                     break;
1577                 }
1578                 // We would like this to be chosen if Mixed platforms does not exist.
1579                 else if (string.Compare(solutionConfiguration.PlatformName, "Any CPU", StringComparison.OrdinalIgnoreCase) == 0)
1580                 {
1581                     _defaultPlatformName = solutionConfiguration.PlatformName;
1582                 }
1583             }
1584 
1585             // Failing that, just pick the first platform name as default
1586             if ((_defaultPlatformName.Length == 0) && (this.SolutionConfigurations.Count > 0))
1587             {
1588                 _defaultPlatformName = this.SolutionConfigurations[0].PlatformName;
1589             }
1590 
1591             return _defaultPlatformName;
1592         }
1593 
1594         /// <summary>
1595         /// This method takes a string representing one of the project's unique names (guid), and
1596         /// returns the corresponding "friendly" name for this project.
1597         /// </summary>
1598         /// <param name="projectGuid"></param>
1599         /// <returns></returns>
GetProjectUniqueNameByGuid(string projectGuid)1600         internal string GetProjectUniqueNameByGuid(string projectGuid)
1601         {
1602             ProjectInSolution proj;
1603             if (_projects.TryGetValue(projectGuid, out proj))
1604             {
1605                 return proj.GetUniqueProjectName();
1606             }
1607 
1608             return null;
1609         }
1610 
1611         /// <summary>
1612         /// This method takes a string representing one of the project's unique names (guid), and
1613         /// returns the corresponding relative path to this project.
1614         /// </summary>
1615         /// <param name="projectGuid"></param>
1616         /// <returns></returns>
GetProjectRelativePathByGuid(string projectGuid)1617         internal string GetProjectRelativePathByGuid(string projectGuid)
1618         {
1619             ProjectInSolution proj;
1620             if (_projects.TryGetValue(projectGuid, out proj))
1621             {
1622                 return proj.RelativePath;
1623             }
1624 
1625             return null;
1626         }
1627 
1628         #endregion
1629     } // class SolutionParser
1630 } // namespace Microsoft.Build.Construction
1631