forked from dotnet/runtime
/
FileVersionInfo.Unix.cs
326 lines (297 loc) · 17.3 KB
/
FileVersionInfo.Unix.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.IO;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
namespace System.Diagnostics
{
public sealed partial class FileVersionInfo
{
private FileVersionInfo(string fileName)
{
_fileName = fileName;
// First make sure it's a file we can actually read from. Only regular files are relevant,
// and attempting to open and read from a file such as a named pipe file could cause us to
// stop responding (waiting for someone else to open and write to the file).
if (Interop.Sys.Stat(_fileName, out Interop.Sys.FileStatus fileStatus) != 0 ||
(fileStatus.Mode & Interop.Sys.FileTypes.S_IFMT) != Interop.Sys.FileTypes.S_IFREG)
{
throw new FileNotFoundException(SR.Format(SR.IO_FileNotFound_FileName, _fileName), _fileName);
}
// For managed assemblies, read the file version information from the assembly's metadata.
// This isn't quite what's done on Windows, which uses the Win32 GetFileVersionInfo to read
// the Win32 resource information from the file, and the managed compiler uses these attributes
// to fill in that resource information when compiling the assembly. It's possible
// that after compilation, someone could have modified the resource information such that it
// no longer matches what was or wasn't in the assembly. But that's a rare enough case
// that this should match for all intents and purposes. If this ever becomes a problem,
// we can implement a full-fledged Win32 resource parser; that would also enable support
// for native Win32 PE files on Unix, but that should also be an extremely rare case.
if (!TryLoadManagedAssemblyMetadata())
{
// We could try to parse Executable and Linkable Format (ELF) files, but at present
// for executables they don't store version information, which is typically just
// available in the filename itself. For now, we won't do anything special, but
// we can add more cases here as we find need and opportunity.
}
}
/// <summary>Attempt to load our fields from the metadata of the file, if it's a managed assembly.</summary>
/// <returns>true if the file is a managed assembly; otherwise, false.</returns>
private bool TryLoadManagedAssemblyMetadata()
{
try
{
// Try to load the file using the managed metadata reader
using (FileStream assemblyStream = File.OpenRead(_fileName))
using (PEReader peReader = new PEReader(assemblyStream))
{
if (peReader.HasMetadata)
{
MetadataReader metadataReader = peReader.GetMetadataReader();
if (metadataReader.IsAssembly)
{
LoadManagedAssemblyMetadata(metadataReader, peReader.PEHeaders.IsExe);
return true;
}
}
}
}
catch
{
// Obtaining this information is best effort and should not throw.
// Possible exceptions include BadImageFormatException if the file isn't an assembly,
// UnauthorizedAccessException if the caller doesn't have permissions to read the file,
// and other potential exceptions thrown by the FileStream ctor.
}
return false;
}
/// <summary>Load our fields from the metadata of the file as represented by the provided metadata reader.</summary>
/// <param name="metadataReader">The metadata reader for the CLI file this represents.</param>\
/// <param name="isExe">true if the assembly represents an executable; false if it's a dll.</param>
private void LoadManagedAssemblyMetadata(MetadataReader metadataReader, bool isExe)
{
AssemblyDefinition assemblyDefinition = metadataReader.GetAssemblyDefinition();
// Set the internal and original names based on the assembly name. We avoid using the
// current filename for determinism and better alignment with behavior on Windows.
string assemblyName = metadataReader.GetString(assemblyDefinition.Name);
if (!assemblyName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) &&
!assemblyName.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
{
assemblyName += isExe ? ".exe" : ".dll";
}
_internalName = _originalFilename = assemblyName;
// Set the product version based on the assembly's version (this may be overwritten
// later in the method).
Version productVersion = assemblyDefinition.Version;
_productVersion = productVersion.ToString();
_productMajor = productVersion.Major;
_productMinor = productVersion.Minor;
_productBuild = productVersion.Build != -1 ? productVersion.Build : 0;
_productPrivate = productVersion.Revision != -1 ? productVersion.Revision : 0;
// "Language Neutral" is used on Win32 for unknown language identifiers.
_language = "Language Neutral";
// Set other fields to default values in case they're not overwritten by attributes
_companyName = string.Empty;
_comments = string.Empty;
_fileDescription = " "; // this is what the managed compiler outputs when value isn't set
_fileVersion = string.Empty;
_legalCopyright = " "; // this is what the managed compiler outputs when value isn't set
_legalTrademarks = string.Empty;
_productName = string.Empty;
_privateBuild = string.Empty;
_specialBuild = string.Empty;
// Be explicit about initialization to suppress warning about fields not being set
_isDebug = false;
_isPatched = false;
_isPreRelease = false;
_isPrivateBuild = false;
_isSpecialBuild = false;
bool sawAssemblyInformationalVersionAttribute = false;
// Everything else is parsed from assembly attributes
MetadataStringComparer comparer = metadataReader.StringComparer;
foreach (CustomAttributeHandle attrHandle in assemblyDefinition.GetCustomAttributes())
{
CustomAttribute attr = metadataReader.GetCustomAttribute(attrHandle);
StringHandle typeNamespaceHandle = default(StringHandle), typeNameHandle = default(StringHandle);
if (TryGetAttributeName(metadataReader, attr, out typeNamespaceHandle, out typeNameHandle) &&
comparer.Equals(typeNamespaceHandle, "System.Reflection"))
{
if (comparer.Equals(typeNameHandle, "AssemblyCompanyAttribute"))
{
GetStringAttributeArgumentValue(metadataReader, attr, ref _companyName);
}
else if (comparer.Equals(typeNameHandle, "AssemblyCopyrightAttribute"))
{
GetStringAttributeArgumentValue(metadataReader, attr, ref _legalCopyright);
}
else if (comparer.Equals(typeNameHandle, "AssemblyDescriptionAttribute"))
{
GetStringAttributeArgumentValue(metadataReader, attr, ref _comments);
}
else if (comparer.Equals(typeNameHandle, "AssemblyFileVersionAttribute"))
{
GetStringAttributeArgumentValue(metadataReader, attr, ref _fileVersion);
ParseVersion(_fileVersion, out _fileMajor, out _fileMinor, out _fileBuild, out _filePrivate);
}
else if (comparer.Equals(typeNameHandle, "AssemblyInformationalVersionAttribute"))
{
GetStringAttributeArgumentValue(metadataReader, attr, ref _productVersion);
ParseVersion(_productVersion, out _productMajor, out _productMinor, out _productBuild, out _productPrivate);
sawAssemblyInformationalVersionAttribute = true;
}
else if (comparer.Equals(typeNameHandle, "AssemblyProductAttribute"))
{
GetStringAttributeArgumentValue(metadataReader, attr, ref _productName);
}
else if (comparer.Equals(typeNameHandle, "AssemblyTrademarkAttribute"))
{
GetStringAttributeArgumentValue(metadataReader, attr, ref _legalTrademarks);
}
else if (comparer.Equals(typeNameHandle, "AssemblyTitleAttribute"))
{
GetStringAttributeArgumentValue(metadataReader, attr, ref _fileDescription);
}
}
}
// When the managed compiler sees an [AssemblyVersion(...)] attribute, it uses that to set
// both the assembly version and the product version in the Win32 resources. If it doesn't
// see an [AssemblyVersion(...)], then it sets the assembly version to 0.0.0.0, however it
// sets the product version in the Win32 resources to whatever was defined in the
// [AssemblyFileVersionAttribute(...)] if there was one (unless there is an AssemblyInformationalVersionAttribute,
// in which case it always uses that for the product version). Without parsing the Win32 resources,
// we can't differentiate these two cases, so given the rarity of explicitly setting an
// assembly's version number to 0.0.0.0, we assume that if it is 0.0.0.0 then the attribute
// wasn't specified and we use the file version.
if (!sawAssemblyInformationalVersionAttribute && _productVersion == "0.0.0.0")
{
_productVersion = _fileVersion;
_productMajor = _fileMajor;
_productMinor = _fileMinor;
_productBuild = _fileBuild;
_productPrivate = _filePrivate;
}
}
/// <summary>Parses the version into its constituent parts.</summary>
private static void ParseVersion(string? versionString, out int major, out int minor, out int build, out int priv)
{
// Relatively-forgiving parsing of a version:
// - If there are more than four parts (separated by periods), all results are deemed 0
// - If any part fails to parse completely as an integer, no further parts are parsed and are left as 0.
// - If any part partially parses as an integer, that value is used for that part.
// - Whitespace is treated like any other non-digit character and thus isn't ignored.
// - Each component is parsed as a ushort, allowing for overflow.
major = minor = build = priv = 0;
if (versionString != null)
{
string[] parts = versionString.Split('.');
if (parts.Length <= 4 && parts.Length > 0)
{
major = ParseUInt16UntilNonDigit(parts[0], out bool endedEarly);
if (!endedEarly && parts.Length > 1)
{
minor = ParseUInt16UntilNonDigit(parts[1], out endedEarly);
if (!endedEarly && parts.Length > 2)
{
build = ParseUInt16UntilNonDigit(parts[2], out endedEarly);
if (!endedEarly && parts.Length > 3)
{
priv = ParseUInt16UntilNonDigit(parts[3], out _);
}
}
}
}
}
}
/// <summary>Parses a string as a UInt16 until it hits a non-digit.</summary>
/// <param name="s">The string to parse.</param>
/// <param name="endedEarly">Whether parsing ended prior to reaching the end of the input.</param>
/// <returns>The parsed value.</returns>
private static ushort ParseUInt16UntilNonDigit(string s, out bool endedEarly)
{
endedEarly = false;
ushort result = 0;
for (int index = 0; index < s.Length; index++)
{
char c = s[index];
if (!char.IsAsciiDigit(c))
{
endedEarly = true;
break;
}
result = (ushort)((result * 10) + (c - '0')); // explicitly allow for overflow, as this is the behavior employed on Windows
}
return result;
}
/// <summary>Gets the name of an attribute.</summary>
/// <param name="reader">The metadata reader.</param>
/// <param name="attr">The attribute.</param>
/// <param name="typeNamespaceHandle">The namespace of the attribute.</param>
/// <param name="typeNameHandle">The name of the attribute.</param>
/// <returns>true if the name could be retrieved; otherwise, false.</returns>
private static bool TryGetAttributeName(MetadataReader reader, CustomAttribute attr, out StringHandle typeNamespaceHandle, out StringHandle typeNameHandle)
{
EntityHandle ctorHandle = attr.Constructor;
switch (ctorHandle.Kind)
{
case HandleKind.MemberReference:
EntityHandle container = reader.GetMemberReference((MemberReferenceHandle)ctorHandle).Parent;
if (container.Kind == HandleKind.TypeReference)
{
TypeReference tr = reader.GetTypeReference((TypeReferenceHandle)container);
typeNamespaceHandle = tr.Namespace;
typeNameHandle = tr.Name;
return true;
}
break;
case HandleKind.MethodDefinition:
MethodDefinition md = reader.GetMethodDefinition((MethodDefinitionHandle)ctorHandle);
TypeDefinition td = reader.GetTypeDefinition(md.GetDeclaringType());
typeNamespaceHandle = td.Namespace;
typeNameHandle = td.Name;
return true;
}
// Unusual case, potentially invalid IL
typeNamespaceHandle = default(StringHandle);
typeNameHandle = default(StringHandle);
return false;
}
/// <summary>Gets the string argument value of an attribute with a single fixed string argument.</summary>
/// <param name="reader">The metadata reader.</param>
/// <param name="attr">The attribute.</param>
/// <param name="value">The value parsed from the attribute, if it could be retrieved; otherwise, the value is left unmodified.</param>
private static void GetStringAttributeArgumentValue(MetadataReader reader, CustomAttribute attr, ref string? value)
{
EntityHandle ctorHandle = attr.Constructor;
BlobHandle signature;
switch (ctorHandle.Kind)
{
case HandleKind.MemberReference:
signature = reader.GetMemberReference((MemberReferenceHandle)ctorHandle).Signature;
break;
case HandleKind.MethodDefinition:
signature = reader.GetMethodDefinition((MethodDefinitionHandle)ctorHandle).Signature;
break;
default:
// Unusual case, potentially invalid IL
return;
}
BlobReader signatureReader = reader.GetBlobReader(signature);
BlobReader valueReader = reader.GetBlobReader(attr.Value);
const ushort Prolog = 1; // two-byte "prolog" defined by ECMA-335 (II.23.3) to be at the beginning of attribute value blobs
if (valueReader.ReadUInt16() == Prolog)
{
SignatureHeader header = signatureReader.ReadSignatureHeader();
int parameterCount;
if (header.Kind == SignatureKind.Method && // attr ctor must be a method
!header.IsGeneric && // attr ctor must be non-generic
signatureReader.TryReadCompressedInteger(out parameterCount) && // read parameter count
parameterCount == 1 && // attr ctor must have 1 parameter
signatureReader.ReadSignatureTypeCode() == SignatureTypeCode.Void && // attr ctor return type must be void
signatureReader.ReadSignatureTypeCode() == SignatureTypeCode.String) // attr ctor first parameter must be string
{
value = valueReader.ReadSerializedString();
}
}
}
}
}