-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Added implementation of GetRuntimeVersion for non-windows systems #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -336,30 +336,37 @@ protected override void DisposeUnmanagedResources() | |
| /// <returns>The CLR runtime version or empty if the path does not exist.</returns> | ||
| internal static string GetRuntimeVersion(string path) | ||
| { | ||
| StringBuilder runtimeVersion = null; | ||
| uint hresult = 0; | ||
| uint actualBufferSize = 0; | ||
| if (NativeMethodsShared.IsWindows) | ||
| { | ||
| StringBuilder runtimeVersion = null; | ||
| uint hresult = 0; | ||
| uint actualBufferSize = 0; | ||
| #if _DEBUG | ||
| // Just to make sure and exercise the code that doubles the size | ||
| // every time GetRequestedRuntimeInfo fails due to insufficient buffer size. | ||
| int bufferLength = 1; | ||
| // Just to make sure and exercise the code that doubles the size | ||
| // every time GetRequestedRuntimeInfo fails due to insufficient buffer size. | ||
| int bufferLength = 1; | ||
| #else | ||
| int bufferLength = 11; // 11 is the length of a runtime version and null terminator v2.0.50727/0 | ||
| int bufferLength = 11; // 11 is the length of a runtime version and null terminator v2.0.50727/0 | ||
| #endif | ||
| do | ||
| { | ||
| runtimeVersion = new StringBuilder(bufferLength); | ||
| hresult = NativeMethods.GetFileVersion(path, runtimeVersion, bufferLength, out actualBufferSize); | ||
| bufferLength = bufferLength * 2; | ||
| } while (hresult == NativeMethodsShared.ERROR_INSUFFICIENT_BUFFER); | ||
| do | ||
| { | ||
| runtimeVersion = new StringBuilder(bufferLength); | ||
| hresult = NativeMethods.GetFileVersion(path, runtimeVersion, bufferLength, out actualBufferSize); | ||
| bufferLength = bufferLength * 2; | ||
| } while (hresult == NativeMethodsShared.ERROR_INSUFFICIENT_BUFFER); | ||
|
|
||
| if (hresult == NativeMethodsShared.S_OK && runtimeVersion != null) | ||
| { | ||
| return runtimeVersion.ToString(); | ||
| if (hresult == NativeMethodsShared.S_OK && runtimeVersion != null) | ||
| { | ||
| return runtimeVersion.ToString(); | ||
| } | ||
| else | ||
| { | ||
| return String.Empty; | ||
| } | ||
| } | ||
| else | ||
| { | ||
| return String.Empty; | ||
| return ManagedRuntimeVersionReader.GetRuntimeVersion(path); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -592,4 +599,214 @@ private void FreeAsmMeta(IntPtr asmMetaPtr) | |
| } | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Managed implementation of a reader for getting the runtime version of an assembly | ||
| /// </summary> | ||
| static class ManagedRuntimeVersionReader | ||
| { | ||
| class HeaderInfo | ||
| { | ||
| public uint VirtualAddress; | ||
| public uint Size; | ||
| public uint FileOffset; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Given a path get the CLR runtime version of the file | ||
| /// </summary> | ||
| /// <param name="path">path to the file</param> | ||
| /// <returns>The CLR runtime version or empty if the path does not exist or the file is not an assembly.</returns> | ||
| public static string GetRuntimeVersion(string path) | ||
| { | ||
| using (var sr = new BinaryReader(File.OpenRead(path))) | ||
| { | ||
| if (!File.Exists(path)) | ||
| return string.Empty; | ||
|
|
||
| // This algorithm for getting the runtime version is based on | ||
| // the ECMA Standard 335: The Common Language Infrastructure (CLI) | ||
| // http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf | ||
|
|
||
| try | ||
| { | ||
| const uint PEHeaderPointerOffset = 0x3c; | ||
| const uint PEHeaderSize = 20; | ||
| const uint OptionalPEHeaderSize = 224; | ||
| const uint OptionalPEPlusHeaderSize = 240; | ||
| const uint SectionHeaderSize = 40; | ||
|
|
||
| // The PE file format is specified in section II.25 | ||
|
|
||
| // A PE image starts with an MS-DOS header followed by a PE signature, followed by the PE file header, | ||
| // and then the PE optional header followed by PE section headers. | ||
| // There must be room for all of that. | ||
|
|
||
| if (sr.BaseStream.Length < PEHeaderPointerOffset + 4 + PEHeaderSize + OptionalPEHeaderSize + SectionHeaderSize) | ||
| return string.Empty; | ||
|
|
||
| // The PE format starts with an MS-DOS stub of 128 bytes. | ||
| // At offset 0x3c in the DOS header is a 4-byte unsigned integer offset to the PE | ||
| // signature (shall be “PE\0\0”), immediately followed by the PE file header | ||
|
|
||
| sr.BaseStream.Position = PEHeaderPointerOffset; | ||
| var peHeaderOffset = sr.ReadUInt32(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since you reuse this variable for various jumps through the header, perhaps it would be more clear if the name reflects that it is the current position?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is not really the current position, but a base offset from which other positions are calculated. I changed to code a bit to make this more clear. |
||
|
|
||
| if (peHeaderOffset + 4 + PEHeaderSize + OptionalPEHeaderSize + SectionHeaderSize >= sr.BaseStream.Length) | ||
| return string.Empty; | ||
|
|
||
| // The PE header is specified in section II.25.2 | ||
| // Read the PE header signature | ||
|
|
||
| sr.BaseStream.Position = peHeaderOffset; | ||
| if (!ReadBytes(sr, (byte)'P', (byte)'E', 0, 0)) | ||
| return string.Empty; | ||
|
|
||
| // The PE header immediately follows the signature | ||
| var peHeaderBase = peHeaderOffset + 4; | ||
|
|
||
| // At offset 2 of the PE header there is the number of sections | ||
| sr.BaseStream.Position = peHeaderBase + 2; | ||
| var numberOfSections = sr.ReadUInt16(); | ||
| if (numberOfSections > 96) | ||
| return string.Empty; // There can't be more than 96 sections, something is wrong | ||
|
|
||
| // Immediately after the PE Header is the PE Optional Header. | ||
| // This header is optional in the general PE spec, but always | ||
| // present in assembly files. | ||
| // From this header we'll get the CLI header RVA, which is | ||
| // at offset 208 for PE32, and at offset 224 for PE32+ | ||
|
|
||
| var optionalHeaderOffset = peHeaderBase + PEHeaderSize; | ||
|
|
||
| uint cliHeaderRvaOffset; | ||
| uint optionalPEHeaderSize; | ||
|
|
||
| sr.BaseStream.Position = optionalHeaderOffset; | ||
| var magicNumber = sr.ReadUInt16(); | ||
|
|
||
| if (magicNumber == 0x10b) // PE32 | ||
| { | ||
| optionalPEHeaderSize = OptionalPEHeaderSize; | ||
| cliHeaderRvaOffset = optionalHeaderOffset + 208; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You declared constants for various offsets. Merely curious why not for this 208 and the 224 few lines down.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this makes the code more readable since it is easier to compare the offset to what's in the specs. I added more comments to make easier to understand what those offsets are. |
||
| } | ||
| else if (magicNumber == 0x20b) // PE32+ | ||
| { | ||
| optionalPEHeaderSize = OptionalPEPlusHeaderSize; | ||
| cliHeaderRvaOffset = optionalHeaderOffset + 224; | ||
| } | ||
| else | ||
| return string.Empty; | ||
|
|
||
| // Read the CLI header RVA | ||
|
|
||
| sr.BaseStream.Position = cliHeaderRvaOffset; | ||
| var cliHeaderRva = sr.ReadUInt32(); | ||
| if (cliHeaderRva == 0) | ||
| return string.Empty; // No CLI section | ||
|
|
||
| // Immediately following the optional header is the Section | ||
| // Table, which contains a number of section headers. | ||
| // Section headers are specified in section II.25.3 | ||
|
|
||
| // Each section header has the base RVA, size, and file | ||
| // offset of the section. To find the file offset of the | ||
| // CLI header we need to find a section that contains | ||
| // its RVA, and the calculate the file offset using | ||
| // the base file offset of the section. | ||
|
|
||
| var sectionOffset = optionalHeaderOffset + optionalPEHeaderSize; | ||
|
|
||
| // Read all section headers, we need them to make RVA to | ||
| // offset conversions. | ||
|
|
||
| var sections = new HeaderInfo [numberOfSections]; | ||
| for (int n = 0; n < numberOfSections; n++) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the beginning I see a lot of careful checking for the length of the stream? In this case looping over max 96 sections is not bounded. Did I perhaps miss a length check? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi, There is a check on line (655) https://github.com/Microsoft/msbuild/pull/43/files#diff-c76040e4f3c877e3887a648405bec17eR655 for 96 sections. I think this is what you are asking.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, there is a check for the max 96 sections just after reading the number of sections value. At the beginning there is more careful checking because at that point we can't tell for sure that what are we reading is really an assembly. After reading the PE headers and getting a value for the CLI header position it's almost 100% sure we are reading an assembly, so offsets we read should have correct values. If they don't (maybe the file is corrupted), then the stream seek will throw and the exception will be cached. But that's an unlikely case. |
||
| { | ||
| // At offset 8 of the section is the section size | ||
| // and base RVA. At offset 20 there is the file offset | ||
| sr.BaseStream.Position = sectionOffset + 8; | ||
| var sectionSize = sr.ReadUInt32(); | ||
| var sectionRva = sr.ReadUInt32(); | ||
| sr.BaseStream.Position = sectionOffset + 20; | ||
| var sectionDataOffset = sr.ReadUInt32(); | ||
| sections[n] = new HeaderInfo { | ||
| VirtualAddress = sectionRva, | ||
| Size = sectionSize, | ||
| FileOffset = sectionDataOffset | ||
| }; | ||
| sectionOffset += SectionHeaderSize; | ||
| } | ||
|
|
||
| uint cliHeaderOffset = RvaToOffset(sections, cliHeaderRva); | ||
|
|
||
| // CLI section not found | ||
| if (cliHeaderOffset == 0) | ||
| return string.Empty; | ||
|
|
||
| // The CLI header is specified in section II.25.3.3. | ||
| // It contains all of the runtime-specific data entries and other information. | ||
| // From the CLI header we need to get the RVA of the metadata root, | ||
| // which is located at offset 8. | ||
|
|
||
| sr.BaseStream.Position = cliHeaderOffset + 8; | ||
| var metadataRva = sr.ReadUInt32(); | ||
|
|
||
| var metadataOffset = RvaToOffset(sections, metadataRva); | ||
| if (metadataOffset == 0) | ||
| return string.Empty; | ||
|
|
||
| // The metadata root is specified in section II.24.2.1 | ||
| // The first 4 bytes contain a signature. | ||
| // The version string is at offset 12. | ||
|
|
||
| sr.BaseStream.Position = metadataOffset; | ||
| if (!ReadBytes(sr, 0x42, 0x53, 0x4a, 0x42)) // Metadata root signature | ||
| return string.Empty; | ||
|
|
||
| // Read the version string length | ||
| sr.BaseStream.Position = metadataOffset + 12; | ||
| var length = sr.ReadInt32(); | ||
| if (length > 255 || length <= 0 || sr.BaseStream.Position + length >= sr.BaseStream.Length) | ||
| return string.Empty; | ||
|
|
||
| // Read the version string | ||
| var v = Encoding.UTF8.GetString(sr.ReadBytes(length)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The string thus generated has extra \0 bytes at the end. This parses OK into a Version, but does not compare to a version string. These extra characters should be trimmed. Alternatively, you could return below: return version.ToString();The aforementioned TestGetImageRuntimeVersion can be used to verify that the version string is good. |
||
| if (v.Length < 2 || v[0] != 'v') | ||
| return string.Empty; | ||
|
|
||
| // Make sure it is a version number | ||
| Version version; | ||
| if (!Version.TryParse(v.Substring(1), out version)) | ||
| return string.Empty; | ||
| return v; | ||
| } | ||
| catch | ||
| { | ||
| // Something went wrong in spite of all checks. Corrupt file? | ||
| return string.Empty; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| static bool ReadBytes(BinaryReader r, params byte[] bytes) | ||
| { | ||
| for (int n = 0; n < bytes.Length; n++) | ||
| { | ||
| if (bytes[n] != r.ReadByte()) | ||
| return false; | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| static uint RvaToOffset(HeaderInfo[] sections, uint rva) | ||
| { | ||
| foreach (var s in sections) | ||
| { | ||
| if (rva >= s.VirtualAddress && rva < s.VirtualAddress + s.Size) | ||
| return s.FileOffset + (rva - s.VirtualAddress); | ||
| } | ||
| return 0; | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you mind including a pointer to the PE header spec (and version) you used? That would be helpful for future maintenance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.