|
// 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.
_ = TryLoadManagedAssemblyMetadata();
// If TryLoadManagedAssemblyMetadata returns false, 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;
ReadOnlySpan<char> versionSpan = versionString;
Span<Range> parts = stackalloc Range[5];
parts = parts.Slice(0, versionSpan.Split(parts, '.'));
if (parts.Length <= 4 && parts.Length > 0)
{
major = ParseUInt16UntilNonDigit(versionSpan[parts[0]], out bool endedEarly);
if (!endedEarly && parts.Length > 1)
{
minor = ParseUInt16UntilNonDigit(versionSpan[parts[1]], out endedEarly);
if (!endedEarly && parts.Length > 2)
{
build = ParseUInt16UntilNonDigit(versionSpan[parts[2]], out endedEarly);
if (!endedEarly && parts.Length > 3)
{
priv = ParseUInt16UntilNonDigit(versionSpan[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(ReadOnlySpan<char> 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();
}
}
}
}
}
|