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.IO;
8 using System.Security;
9 using System.Text;
10 using System.Xml;
11 using Microsoft.Build.Shared;
12 
13 using XMakeAttributes = Microsoft.Build.Shared.XMakeAttributes;
14 using ProjectFileErrorUtilities = Microsoft.Build.Shared.ProjectFileErrorUtilities;
15 using BuildEventFileInfo = Microsoft.Build.Shared.BuildEventFileInfo;
16 using ErrorUtilities = Microsoft.Build.Shared.ErrorUtilities;
17 using System.Collections.ObjectModel;
18 using System.Linq;
19 
20 namespace Microsoft.Build.Construction
21 {
22     /// <remarks>
23     /// An enumeration defining the different types of projects we might find in an SLN.
24     /// </remarks>
25     public enum SolutionProjectType
26     {
27         /// <summary>
28         /// Everything else besides the below well-known project types.
29         /// </summary>
30         Unknown,
31         /// <summary>
32         /// C#, VB, F#, and VJ# projects
33         /// </summary>
34         KnownToBeMSBuildFormat,
35         /// <summary>
36         /// Solution folders appear in the .sln file, but aren't buildable projects.
37         /// </summary>
38         SolutionFolder,
39         /// <summary>
40         /// ASP.NET projects
41         /// </summary>
42         WebProject,
43         /// <summary>
44         /// Web Deployment (.wdproj) projects
45         /// </summary>
46         WebDeploymentProject, //  MSBuildFormat, but Whidbey-era ones specify ProjectReferences differently
47         /// <summary>
48         /// Project inside an Enterprise Template project
49         /// </summary>
50         EtpSubProject,
51         /// <summary>
52         /// A shared project represents a collection of shared files that is not buildable on its own.
53         /// </summary>
54         SharedProject
55     }
56 
57     internal struct AspNetCompilerParameters
58     {
59         internal string aspNetVirtualPath;      // For Venus projects only, Virtual path for web
60         internal string aspNetPhysicalPath;     // For Venus projects only, Physical path for web
61         internal string aspNetTargetPath;       // For Venus projects only, Target for output files
62         internal string aspNetForce;            // For Venus projects only, Force overwrite of target
63         internal string aspNetUpdateable;       // For Venus projects only, compiled web application is updateable
64         internal string aspNetDebug;            // For Venus projects only, generate symbols, etc.
65         internal string aspNetKeyFile;          // For Venus projects only, strong name key file.
66         internal string aspNetKeyContainer;     // For Venus projects only, strong name key container.
67         internal string aspNetDelaySign;        // For Venus projects only, delay sign strong name.
68         internal string aspNetAPTCA;            // For Venus projects only, AllowPartiallyTrustedCallers.
69         internal string aspNetFixedNames;       // For Venus projects only, generate fixed assembly names.
70     }
71 
72     /// <remarks>
73     /// This class represents a project (or SLN folder) that is read in from a solution file.
74     /// </remarks>
75     public sealed class ProjectInSolution
76     {
77         #region Constants
78 
79         /// <summary>
80         /// Characters that need to be cleansed from a project name.
81         /// </summary>
82         private static readonly char[] s_charsToCleanse = { '%', '$', '@', ';', '.', '(', ')', '\'' };
83 
84         /// <summary>
85         /// Project names that need to be disambiguated when forming a target name
86         /// </summary>
87         internal static readonly string[] projectNamesToDisambiguate = { "Build", "Rebuild", "Clean", "Publish" };
88 
89         /// <summary>
90         /// Character that will be used to replace 'unclean' ones.
91         /// </summary>
92         private const char cleanCharacter = '_';
93 
94         #endregion
95 
96         #region Member data
97 
98         private SolutionProjectType _projectType;      // For example, KnownToBeMSBuildFormat, VCProject, WebProject, etc.
99         private string _projectName;          // For example, "WindowsApplication1"
100         private string _relativePath;         // Relative from .SLN file.  For example, "WindowsApplication1\WindowsApplication1.csproj"
101         private string _projectGuid;          // The unique Guid assigned to this project or SLN folder.
102         private List<string> _dependencies;     // A list of strings representing the Guids of the dependent projects.
103         private ArrayList _projectReferences; // A list of strings representing the guids of referenced projects.
104                                               // This is only used for VC/Venus projects
105         private string _parentProjectGuid;    // If this project (or SLN folder) is within a SLN folder, this is the Guid of the parent SLN folder.
106         private string _uniqueProjectName;    // For example, "MySlnFolder\MySubSlnFolder\WindowsApplication1"
107         private Hashtable _aspNetConfigurations;    // Key is configuration name, value is [struct] AspNetCompilerParameters
108         private SolutionFile _parentSolution; // The parent solution for this project
109         private string _targetFrameworkMoniker; // used for website projects, since they don't have a project file in which the
110                                                 // target framework is stored.  Defaults to .NETFX 3.5
111 
112         /// <summary>
113         /// The project configuration in given solution configuration
114         /// K: full solution configuration name (cfg + platform)
115         /// V: project configuration
116         /// </summary>
117         private Dictionary<string, ProjectConfigurationInSolution> _projectConfigurations;
118 
119         #endregion
120 
121         #region Constructors
122 
ProjectInSolution(SolutionFile solution)123         internal ProjectInSolution(SolutionFile solution)
124         {
125             _projectType = SolutionProjectType.Unknown;
126             _projectName = null;
127             _relativePath = null;
128             _projectGuid = null;
129             _dependencies = new List<string>();
130             _projectReferences = new ArrayList();
131             _parentProjectGuid = null;
132             _uniqueProjectName = null;
133             _parentSolution = solution;
134 
135             // default to .NET Framework 3.5 if this is an old solution that doesn't explicitly say.
136             _targetFrameworkMoniker = ".NETFramework,Version=v3.5";
137 
138             // This hashtable stores a AspNetCompilerParameters struct for each configuration name supported.
139             _aspNetConfigurations = new Hashtable(StringComparer.OrdinalIgnoreCase);
140 
141             _projectConfigurations = new Dictionary<string, ProjectConfigurationInSolution>(StringComparer.OrdinalIgnoreCase);
142         }
143 
144         #endregion
145 
146         #region Properties
147 
148         /// <summary>
149         /// This project's name
150         /// </summary>
151         public string ProjectName
152         {
153             get { return _projectName; }
154             internal set { _projectName = value; }
155         }
156 
157         /// <summary>
158         /// The path to this project file, relative to the solution location
159         /// </summary>
160         public string RelativePath
161         {
162             get { return _relativePath; }
163             internal set
164             {
165                 _relativePath = FileUtilities.MaybeAdjustFilePath(value,
166                                                     baseDirectory:this.ParentSolution.SolutionFileDirectory ?? String.Empty);
167             }
168         }
169 
170         /// <summary>
171         /// Returns the absolute path for this project
172         /// </summary>
173         public string AbsolutePath
174         {
175             get
176             {
177                 return Path.Combine(this.ParentSolution.SolutionFileDirectory, this.RelativePath);
178             }
179         }
180 
181         /// <summary>
182         /// The unique guid associated with this project, in "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" form
183         /// </summary>
184         public string ProjectGuid
185         {
186             get { return _projectGuid; }
187             internal set { _projectGuid = value; }
188         }
189 
190         /// <summary>
191         /// The guid, in "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" form, of this project's
192         /// parent project, if any.
193         /// </summary>
194         public string ParentProjectGuid
195         {
196             get { return _parentProjectGuid; }
197             internal set { _parentProjectGuid = value; }
198         }
199 
200         /// <summary>
201         /// List of guids, in "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" form, mapping to projects
202         /// that this project has a build order dependency on, as defined in the solution file.
203         /// </summary>
204         public IReadOnlyList<string> Dependencies
205         {
206             get { return _dependencies.AsReadOnly(); }
207         }
208 
209         /// <summary>
210         /// Configurations for this project, keyed off the configuration's full name, e.g. "Debug|x86"
211         /// </summary>
212         public IReadOnlyDictionary<string, ProjectConfigurationInSolution> ProjectConfigurations
213         {
214             get { return new ReadOnlyDictionary<string, ProjectConfigurationInSolution>(_projectConfigurations); }
215         }
216 
217         /// <summary>
218         /// Extension of the project file, if any
219         /// </summary>
220         internal string Extension
221         {
222             get
223             {
224                 return Path.GetExtension(_relativePath);
225             }
226         }
227 
228         /// <summary>
229         /// This project's type.
230         /// </summary>
231         public SolutionProjectType ProjectType
232         {
233             get { return _projectType; }
234             set { _projectType = value; }
235         }
236 
237         /// <summary>
238         /// Only applies to websites -- for other project types, references are
239         /// either specified as Dependencies above, or as ProjectReferences in the
240         /// project file, which the solution doesn't have insight into.
241         /// </summary>
242         internal ArrayList ProjectReferences
243         {
244             get { return _projectReferences; }
245         }
246 
247         internal SolutionFile ParentSolution
248         {
249             get { return _parentSolution; }
250             set { _parentSolution = value; }
251         }
252 
253         internal Hashtable AspNetConfigurations
254         {
255             get { return _aspNetConfigurations; }
256             set { _aspNetConfigurations = value; }
257         }
258 
259         internal string TargetFrameworkMoniker
260         {
261             get { return _targetFrameworkMoniker; }
262             set { _targetFrameworkMoniker = value; }
263         }
264 
265         #endregion
266 
267         #region Methods
268 
269         private bool _checkedIfCanBeMSBuildProjectFile = false;
270         private bool _canBeMSBuildProjectFile;
271         private string _canBeMSBuildProjectFileErrorMessage;
272 
273         /// <summary>
274         /// Add the guid of a referenced project to our dependencies list.
275         /// </summary>
AddDependency(string referencedProjectGuid)276         internal void AddDependency(string referencedProjectGuid)
277         {
278             _dependencies.Add(referencedProjectGuid);
279         }
280 
281         /// <summary>
282         /// Set the requested project configuration.
283         /// </summary>
SetProjectConfiguration(string configurationName, ProjectConfigurationInSolution configuration)284         internal void SetProjectConfiguration(string configurationName, ProjectConfigurationInSolution configuration)
285         {
286             _projectConfigurations[configurationName] = configuration;
287         }
288 
289         /// <summary>
290         /// Looks at the project file node and determines (roughly) if the project file is in the MSBuild format.
291         /// The results are cached in case this method is called multiple times.
292         /// </summary>
293         /// <param name="errorMessage">Detailed error message in case we encounter critical problems reading the file</param>
294         /// <returns></returns>
CanBeMSBuildProjectFile(out string errorMessage)295         internal bool CanBeMSBuildProjectFile(out string errorMessage)
296         {
297             if (_checkedIfCanBeMSBuildProjectFile)
298             {
299                 errorMessage = _canBeMSBuildProjectFileErrorMessage;
300                 return _canBeMSBuildProjectFile;
301             }
302 
303             _checkedIfCanBeMSBuildProjectFile = true;
304             _canBeMSBuildProjectFile = false;
305             errorMessage = null;
306 
307             try
308             {
309                 // Read project thru a XmlReader with proper setting to avoid DTD processing
310                 XmlReaderSettings xrSettings = new XmlReaderSettings();
311                 xrSettings.DtdProcessing = DtdProcessing.Ignore;
312 
313                 XmlDocument projectDocument = new XmlDocument();
314 
315                 using (XmlReader xmlReader = XmlReader.Create(this.AbsolutePath, xrSettings))
316                 {
317                     // Load the project file and get the first node
318                     projectDocument.Load(xmlReader);
319                 }
320 
321                 XmlElement mainProjectElement = null;
322 
323                 // The XML parser will guarantee that we only have one real root element,
324                 // but we need to find it amongst the other types of XmlNode at the root.
325                 foreach (XmlNode childNode in projectDocument.ChildNodes)
326                 {
327                     if (childNode.NodeType == XmlNodeType.Element)
328                     {
329                         mainProjectElement = (XmlElement)childNode;
330                         break;
331                     }
332                 }
333 
334                 if (mainProjectElement != null && mainProjectElement.LocalName == "Project")
335                 {
336                     // MSBuild supports project files with an empty (supported in Visual Studio 2017) or the default MSBuild
337                     // namespace.
338                     bool emptyNamespace = string.IsNullOrEmpty(mainProjectElement.NamespaceURI);
339                     bool defaultNamespace = String.Compare(mainProjectElement.NamespaceURI,
340                                                 XMakeAttributes.defaultXmlNamespace,
341                                                 StringComparison.OrdinalIgnoreCase) == 0;
342                     bool projectElementInvalid = ElementContainsInvalidNamespaceDefitions(mainProjectElement);
343 
344                     // If the MSBuild namespace is declared, it is very likely an MSBuild project that should be built.
345                     if (defaultNamespace)
346                     {
347                         _canBeMSBuildProjectFile = true;
348                         return _canBeMSBuildProjectFile;
349                     }
350 
351                     // This is a bit of a special case, but an rptproj file will contain a Project with no schema that is
352                     // not an MSBuild file. It will however have ToolsVersion="2.0" which is not supported with an empty
353                     // schema. This is not a great solution, but it should cover the customer reported issue. See:
354                     // https://github.com/Microsoft/msbuild/issues/2064
355                     if (emptyNamespace && !projectElementInvalid && mainProjectElement.GetAttribute("ToolsVersion") != "2.0")
356                     {
357                         _canBeMSBuildProjectFile = true;
358                         return _canBeMSBuildProjectFile;
359                     }
360                 }
361             }
362             // catch all sorts of exceptions - if we encounter any problems here, we just assume the project file is not
363             // in the MSBuild format
364 
365             // handle errors in path resolution
366             catch (SecurityException e)
367             {
368                 _canBeMSBuildProjectFileErrorMessage = e.Message;
369             }
370             // handle errors in path resolution
371             catch (NotSupportedException e)
372             {
373                 _canBeMSBuildProjectFileErrorMessage = e.Message;
374             }
375             // handle errors in loading project file
376             catch (IOException e)
377             {
378                 _canBeMSBuildProjectFileErrorMessage = e.Message;
379             }
380             // handle errors in loading project file
381             catch (UnauthorizedAccessException e)
382             {
383                 _canBeMSBuildProjectFileErrorMessage = e.Message;
384             }
385             // handle XML parsing errors (when reading project file)
386             // this is not critical, since the project file doesn't have to be in XML formal
387             catch (XmlException)
388             {
389             }
390 
391             errorMessage = _canBeMSBuildProjectFileErrorMessage;
392 
393             return _canBeMSBuildProjectFile;
394         }
395 
396         /// <summary>
397         /// Find the unique name for this project, e.g. SolutionFolder\SubSolutionFolder\ProjectName
398         /// </summary>
GetUniqueProjectName()399         internal string GetUniqueProjectName()
400         {
401             if (_uniqueProjectName == null)
402             {
403                 // EtpSubProject and Venus projects have names that are already unique.  No need to prepend the SLN folder.
404                 if ((this.ProjectType == SolutionProjectType.WebProject) || (this.ProjectType == SolutionProjectType.EtpSubProject))
405                 {
406                     _uniqueProjectName = CleanseProjectName(this.ProjectName);
407                 }
408                 else
409                 {
410                     // This is "normal" project, which in this context means anything non-Venus and non-EtpSubProject.
411 
412                     // If this project has a parent SLN folder, first get the full unique name for the SLN folder,
413                     // and tack on trailing backslash.
414                     string uniqueName = String.Empty;
415 
416                     if (this.ParentProjectGuid != null)
417                     {
418                         ProjectInSolution proj;
419                         if (!this.ParentSolution.ProjectsByGuid.TryGetValue(this.ParentProjectGuid, out proj))
420                         {
421                             ProjectFileErrorUtilities.VerifyThrowInvalidProjectFile(proj != null, "SubCategoryForSolutionParsingErrors",
422                                 new BuildEventFileInfo(_parentSolution.FullPath), "SolutionParseNestedProjectError");
423                         }
424 
425                         uniqueName = proj.GetUniqueProjectName() + "\\";
426                     }
427 
428                     // Now tack on our own project name, and cache it in the ProjectInSolution object for future quick access.
429                     _uniqueProjectName = CleanseProjectName(uniqueName + this.ProjectName);
430                 }
431             }
432 
433             return _uniqueProjectName;
434         }
435 
436         /// <summary>
437         /// Changes the unique name of the project.
438         /// </summary>
UpdateUniqueProjectName(string newUniqueName)439         internal void UpdateUniqueProjectName(string newUniqueName)
440         {
441             ErrorUtilities.VerifyThrowArgumentLength(newUniqueName, "newUniqueName");
442 
443             _uniqueProjectName = newUniqueName;
444         }
445 
446         /// <summary>
447         /// Cleanse the project name, by replacing characters like '@', '$' with '_'
448         /// </summary>
449         /// <param name="projectName">The name to be cleansed</param>
450         /// <returns>string</returns>
CleanseProjectName(string projectName)451         static private string CleanseProjectName(string projectName)
452         {
453             ErrorUtilities.VerifyThrow(projectName != null, "Null strings not allowed.");
454 
455             // If there are no special chars, just return the original string immediately.
456             // Don't even instantiate the StringBuilder.
457             int indexOfChar = projectName.IndexOfAny(s_charsToCleanse);
458             if (indexOfChar == -1)
459             {
460                 return projectName;
461             }
462 
463             // This is where we're going to work on the final string to return to the caller.
464             StringBuilder cleanProjectName = new StringBuilder(projectName);
465 
466             // Replace each unclean character with a clean one
467             foreach (char uncleanChar in s_charsToCleanse)
468             {
469                 cleanProjectName.Replace(uncleanChar, cleanCharacter);
470             }
471 
472             return cleanProjectName.ToString();
473         }
474 
475         /// <summary>
476         /// If the unique project name provided collides with one of the standard Solution project
477         /// entry point targets (Build, Rebuild, Clean, Publish), then disambiguate it by prepending the string "Solution:"
478         /// </summary>
479         /// <param name="uniqueProjectName">The unique name for the project</param>
480         /// <returns>string</returns>
DisambiguateProjectTargetName(string uniqueProjectName)481         static internal string DisambiguateProjectTargetName(string uniqueProjectName)
482         {
483             // Test our unique project name against those names that collide with Solution
484             // entry point targets
485             foreach (string projectName in projectNamesToDisambiguate)
486             {
487                 if (String.Compare(uniqueProjectName, projectName, StringComparison.OrdinalIgnoreCase) == 0)
488                 {
489                     // Prepend "Solution:" so that the collision is resolved, but the
490                     // log of the solution project still looks reasonable.
491                     return "Solution:" + uniqueProjectName;
492                 }
493             }
494 
495             return uniqueProjectName;
496         }
497 
498         /// <summary>
499         /// Check a Project element for known invalid namespace definitions.
500         /// </summary>
501         /// <param name="mainProjectElement">Project XML Element</param>
502         /// <returns>True if the element contains known invalid namespace definitions</returns>
ElementContainsInvalidNamespaceDefitions(XmlElement mainProjectElement)503         private static bool ElementContainsInvalidNamespaceDefitions(XmlElement mainProjectElement)
504         {
505             if (mainProjectElement.HasAttributes)
506             {
507                 // Data warehouse projects (.dwproj) will contain a Project element but are invalid MSBuild. Check attributes
508                 // on Project for signs that this is a .dwproj file. If there are, it's not a valid MSBuild file.
509                 return mainProjectElement.Attributes.OfType<XmlAttribute>().Any(a =>
510                     a.Name.Equals("xmlns:dwd", StringComparison.OrdinalIgnoreCase) ||
511                     a.Name.StartsWith("xmlns:dd", StringComparison.OrdinalIgnoreCase));
512             }
513 
514             return false;
515         }
516 
517         #endregion
518 
519         #region Constants
520 
521         internal const int DependencyLevelUnknown = -1;
522         internal const int DependencyLevelBeingDetermined = -2;
523 
524         #endregion
525     }
526 }
527