Skip to content

Commit

Permalink
Merge pull request dotnet#1860 from stephentoub/fileversioninfo_unix
Browse files Browse the repository at this point in the history
Implement FileVersionInfo on Unix (for managed assemblies)
  • Loading branch information
stephentoub committed May 29, 2015
2 parents 09ea272 + 7ffa75b commit 5037193
Show file tree
Hide file tree
Showing 9 changed files with 479 additions and 217 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<FileAlignment>512</FileAlignment>
<DocumentationFile>$(OutputPath)System.Collections.Immutable.xml</DocumentationFile>
<GenerateAppxPackageOnBuild>False</GenerateAppxPackageOnBuild>
<AssemblyVersion>1.1.34</AssemblyVersion>
<AssemblyVersion>1.1.9999</AssemblyVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Generic;
using System.IO;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;

namespace System.Diagnostics
{
public sealed partial class FileVersionInfo
Expand All @@ -9,42 +14,236 @@ private FileVersionInfo(string fileName)
{
_fileName = fileName;

// TODO: Implement this
// 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.
}
}

// -----------------------------
// ---- PAL layer ends here ----
// -----------------------------

/// <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);
return true;
}
}
}
}
catch (BadImageFormatException) { }
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>
private void LoadManagedAssemblyMetadata(MetadataReader metadataReader)
{
AssemblyDefinition assemblyDefinition = metadataReader.GetAssemblyDefinition();

// Set the internal and original names based on the file name.
_internalName = _originalFilename = Path.GetFileName(_fileName);

// 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(4);
_productMajor = productVersion.Major;
_productMinor = productVersion.Minor;
_productBuild = productVersion.Build;
_productPrivate = productVersion.Revision;

// "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;
_fileDescription = string.Empty;
_fileVersion = string.Empty;
_internalName = string.Empty;
_legalCopyright = string.Empty;
_originalFilename = string.Empty;
_productName = string.Empty;
_productVersion = 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;

_language = string.Empty;

_fileMajor = 0;
_fileMinor = 0;
_fileBuild = 0;
_filePrivate = 0;
_productMajor = 0;
_productMinor = 0;
_productBuild = 0;
_productPrivate = 0;

// Be explicit about initialization to suppress warning about fields not being set
_isDebug = false;
_isPatched = false;
_isPrivateBuild = false;
_isPreRelease = false;
_isPrivateBuild = false;
_isSpecialBuild = 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"))
{
string versionString = string.Empty;
GetStringAttributeArgumentValue(metadataReader, attr, ref versionString);
Version v;
if (Version.TryParse(versionString, out v))
{
_fileVersion = v.ToString(4);
_fileMajor = v.Major;
_fileMinor = v.Minor;
_fileBuild = v.Build;
_filePrivate = v.Revision;

// 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. 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 (_productVersion == "0.0.0.0")
{
_productVersion = _fileVersion;
_productMajor = _fileMajor;
_productMinor = _fileMinor;
_productBuild = _fileBuild;
_productPrivate = _filePrivate;
}
}
}
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);
}
}
}
}

// -----------------------------
// ---- PAL layer ends here ----
// -----------------------------
/// <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();
}
}
}

}
}
1 change: 1 addition & 0 deletions src/System.Diagnostics.FileVersionInfo/src/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"System.IO.FileSystem": "4.0.0-beta-*",
"System.IO.FileSystem.Primitives": "4.0.0-beta-*",
"System.Reflection": "4.0.10-beta-*",
"System.Reflection.Metadata": "1.0.21",
"System.Reflection.Primitives": "4.0.0-beta-*",
"System.Runtime": "4.0.20-beta-*",
"System.Runtime.Extensions": "4.0.10-beta-*",
Expand Down
44 changes: 44 additions & 0 deletions src/System.Diagnostics.FileVersionInfo/src/project.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@
"lib/DNXCore50/System.Collections.dll"
]
},
"System.Collections.Immutable/1.1.36": {
"compile": [
"lib/portable-net45+win8+wp8+wpa81/System.Collections.Immutable.dll"
],
"runtime": [
"lib/portable-net45+win8+wp8+wpa81/System.Collections.Immutable.dll"
]
},
"System.Diagnostics.Debug/4.0.10-beta-22927": {
"dependencies": {
"System.Runtime": "4.0.0-beta-22927"
Expand Down Expand Up @@ -112,6 +120,17 @@
"lib/DNXCore50/System.Reflection.dll"
]
},
"System.Reflection.Metadata/1.0.21": {
"dependencies": {
"System.Collections.Immutable": "1.1.36"
},
"compile": [
"lib/portable-net45+win8/System.Reflection.Metadata.dll"
],
"runtime": [
"lib/portable-net45+win8/System.Reflection.Metadata.dll"
]
},
"System.Reflection.Primitives/4.0.0-beta-22927": {
"dependencies": {
"System.Runtime": "4.0.0-beta-22927"
Expand Down Expand Up @@ -258,6 +277,18 @@
"runtimes/win8-aot/lib/netcore50/System.Collections.dll"
]
},
"System.Collections.Immutable/1.1.36": {
"serviceable": true,
"sha512": "MOlivTIeAIQPPMUPWIIoMCvZczjFRLYUWSYwqi1szu8QPyeIbsaPeI+hpXe1DzTxNwnRnmfYaoToi6kXIfSPNg==",
"files": [
"License-Stable.rtf",
"System.Collections.Immutable.1.1.36.nupkg",
"System.Collections.Immutable.1.1.36.nupkg.sha512",
"System.Collections.Immutable.nuspec",
"lib/portable-net45+win8+wp8+wpa81/System.Collections.Immutable.dll",
"lib/portable-net45+win8+wp8+wpa81/System.Collections.Immutable.xml"
]
},
"System.Diagnostics.Debug/4.0.10-beta-22927": {
"sha512": "uhcpTHJXFOXz1BHrZ2I37p28nW+HkWEYssFDQ3I6Jvt/OrMHOOZ7un1ANIICBnKjH3IM2LAznyvEZDJ9DourSw==",
"files": [
Expand Down Expand Up @@ -366,6 +397,18 @@
"runtimes/win8-aot/lib/netcore50/System.Reflection.dll"
]
},
"System.Reflection.Metadata/1.0.21": {
"serviceable": true,
"sha512": "CAhcY0EJFLXfxo5YRV/ytkTOeTBho8zKjLu+9LvEI5NIVmanp1yMUzYQdjvXpHbyV0Qt1KuLXcTCc9t9FVH3Wg==",
"files": [
"License-Stable.rtf",
"System.Reflection.Metadata.1.0.21.nupkg",
"System.Reflection.Metadata.1.0.21.nupkg.sha512",
"System.Reflection.Metadata.nuspec",
"lib/portable-net45+win8/System.Reflection.Metadata.dll",
"lib/portable-net45+win8/System.Reflection.Metadata.xml"
]
},
"System.Reflection.Primitives/4.0.0-beta-22927": {
"sha512": "C3+Wx4snFHYvOncOW7slXJV1UTSSsREllmhJKXn+Ud5rC1UXut218w5ULD41ltV4TQxyH5nY/pkrhysVUU7etA==",
"files": [
Expand Down Expand Up @@ -529,6 +572,7 @@
"System.IO.FileSystem >= 4.0.0-beta-*",
"System.IO.FileSystem.Primitives >= 4.0.0-beta-*",
"System.Reflection >= 4.0.10-beta-*",
"System.Reflection.Metadata >= 1.0.21",
"System.Reflection.Primitives >= 4.0.0-beta-*",
"System.Runtime >= 4.0.20-beta-*",
"System.Runtime.Extensions >= 4.0.10-beta-*",
Expand Down
Loading

0 comments on commit 5037193

Please sign in to comment.