3

Using Delphi 7, on a Windows >= XP, how can I retrieve the disk signature of every disk from the computer? Preferably without using WMI or Diskpart.

And if it's possible, to be fast too..

Thank you.

Later edit:

Documentation: http://pcsupport.about.com/od/termsd/g/disk-signature.htm
MBR disks: http://diddy.boot-land.net/firadisk/files/signature.htm
GPT disks: http://thestarman.pcministry.com/asm/mbr/GPT.htm

How to get it with DiskPart (method found on Google when searching "disk signature"):
Diskpart >> list disk >> select disk [n] >>
detail disk >> Disk ID: 0E35445B for MBR disks
and GUID: 55FD03F2-6B11-49DF-8167-D30B94A4509D for GPT Disks
Jack Compton
  • 59
  • 1
  • 6
  • 3
    I never like questions that rule out possible solutions. In this case you've ruled out WMI? Why ever so? This problem is exactly what WMI is meant for. – David Heffernan Jun 14 '13 at 14:14
  • 1
    So you want code that is fast, doesn't use WMI, is written in Delphi, and works on Linux too. That's not remotely realistic. Different operating systems are, well, different. You'll need different solutions for different operating systems, and WMI is a good solution for Windows. – David Heffernan Jun 14 '13 at 14:35
  • @DavidHeffernan WMI is slow in connection and either complex (when you use it Lego-like, coding all those tedious details directly) or, again, slow (when using simple wrapper functions, that combine that Lego of objects, fetch the resultsm\, and discar them without re-use). However i'm with you that this should be generic interface with different implementing classes for Win and Lin – Arioch 'The Jun 14 '13 at 15:37
  • possible duplicate of [in Delphi7, How can I retrieve hard disk unique serial number?](http://stackoverflow.com/questions/5202270/in-delphi7-how-can-i-retrieve-hard-disk-unique-serial-number) – RRUZ Jun 14 '13 at 16:35
  • Watch for GPT disks if you decide to read MBR directly. You can probably walk backwards from [DISK_PARTITION_INFO](http://msdn.microsoft.com/en-us/library/windows/hardware/ff552629%28v=vs.85%29.aspx), that's if Ian's answer does not provide the signature you're looking for. .. And I don't think an answer trying to help should be considered to be *against* you, but that might be my English.. – Sertac Akyuz Jun 14 '13 at 17:55

3 Answers3

5

You can use DeviceIoControl and IOCTL_DISK_GET_DRIVE_LAYOUT_EX to obtain the information that you require.

program DiskSignatureGuid;

{$APPTYPE CONSOLE}

uses
  SysUtils, Windows;

type
  TDriveLayoutInformationMbr = record
    Signature: DWORD;
  end;

  TDriveLayoutInformationGpt = record
    DiskId: TGuid;
    StartingUsableOffset: Int64;
    UsableLength: Int64;
    MaxPartitionCount: DWORD;
  end;

  TPartitionInformationMbr = record
    PartitionType: Byte;
    BootIndicator: Boolean;
    RecognizedPartition: Boolean;
    HiddenSectors: DWORD;
  end;

  TPartitionInformationGpt = record
    PartitionType: TGuid;
    PartitionId: TGuid;
    Attributes: Int64;
    Name: array [0..35] of WideChar;
  end;

  TPartitionInformationEx = record
    PartitionStyle: Integer;
    StartingOffset: Int64;
    PartitionLength: Int64;
    PartitionNumber: DWORD;
    RewritePartition: Boolean;
    case Integer of
      0: (Mbr: TPartitionInformationMbr);
      1: (Gpt: TPartitionInformationGpt);
  end;

  TDriveLayoutInformationEx = record
    PartitionStyle: DWORD;
    PartitionCount: DWORD;
    DriveLayoutInformation: record
      case Integer of
      0: (Mbr: TDriveLayoutInformationMbr);
      1: (Gpt: TDriveLayoutInformationGpt);
    end;
    PartitionEntry: array [0..15] of TPartitionInformationGpt;
    //hard-coded maximum of 16 partitions
  end;

const
  PARTITION_STYLE_MBR = 0;
  PARTITION_STYLE_GPT = 1;
  PARTITION_STYLE_RAW = 2;

const
  IOCTL_DISK_GET_DRIVE_LAYOUT_EX = $00070050;

procedure Main;
const
  // Max number of drives assuming primary/secondary, master/slave topology
  MAX_IDE_DRIVES = 16;
var
  i: Integer;
  Drive: string;
  hDevice: THandle;
  DriveLayoutInfo: TDriveLayoutInformationEx;
  BytesReturned: DWORD;
begin
  for i := 0 to MAX_IDE_DRIVES - 1 do
  begin
    Drive := '\\.\PHYSICALDRIVE' + IntToStr(i);
    hDevice := CreateFile(PChar(Drive), 0, FILE_SHARE_READ or FILE_SHARE_WRITE,
      nil, OPEN_EXISTING, 0, 0);
    if hDevice <> INVALID_HANDLE_VALUE then
    begin
      if DeviceIoControl(hDevice, IOCTL_DISK_GET_DRIVE_LAYOUT_EX, nil, 0,
        @DriveLayoutInfo, SizeOf(DriveLayoutInfo), BytesReturned, nil) then
      begin
        case DriveLayoutInfo.PartitionStyle of
        PARTITION_STYLE_MBR:
          Writeln(Drive + ', MBR, ' +
            IntToHex(DriveLayoutInfo.DriveLayoutInformation.Mbr.Signature, 8));
        PARTITION_STYLE_GPT:
          Writeln(Drive + ', GPT, ' +
            GUIDToString(DriveLayoutInfo.DriveLayoutInformation.Gpt.DiskId));
        PARTITION_STYLE_RAW:
          Writeln(Drive + ', RAW');
        end;
      end;
      CloseHandle(hDevice);
    end;
  end;
end;

begin
  Main;
  Readln;
end.

Note that since 0 is passed to the dwDesiredAccess parameter of CreateFile, elevated rights are not required. This is explained, albeit somewhat opaquely, in the documentation:

Direct access to the disk or to a volume is restricted ... The following requirements must be met for such a call to succeed:

  • The caller must have administrative privileges.
  • The dwCreationDisposition parameter must have the OPEN_EXISTINGflag.
  • When opening a volume or floppy disk, the dwShareMode parameter must have the FILE_SHARE_WRITEflag.

Note The dwDesiredAccess parameter can be zero, allowing the application to query device attributes without accessing a device. This is useful for an application to determine the size of a floppy disk drive and the formats it supports without requiring a floppy disk in a drive, for instance. It can also be used for reading statistics without requiring higher-level data read/write permission.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • So far I'm searching "IOCTL_DISK_GET_DRIVE_LAYOUT_EX" on internet, since you forgot to include it... – Jack Compton Jun 16 '13 at 12:19
  • OK, I did not need to include it since I'm on XE3 I guess and it's not defined in D7. Wait one moment please. Right, there it is! – David Heffernan Jun 16 '13 at 12:20
  • @JackCompton In case you are interested, the code in this answer runs without elevation. The key difference regarding elevation between it and your answer is that the code here passes `0` to the second parameter of `CreateFile`. – David Heffernan Jun 17 '13 at 10:32
  • @David Heffernan - great code segment. Thanks. Though I noticed as a potential improvement the following perhaps because the displayed signature is displayed back to front : IntToHex(SwapEndian(DriveLayoutInfo.DriveLayoutInformation.Mbr.Signature), 8)); – Gizmo_the_Great Jul 20 '15 at 14:11
  • @David Heffernan - how do you reference the GUID type (the 16 byte value that specifies what the partition was created by, e.g. MS Reserved type, Linux type etc)? The value is in TDriveLayoutInformationEx as PartitionEntry: array [0..15] of TPartitionInformationGpt; but how do you extract the 16 byte array of bytes as a TGUID type string? GUIDType := GUIDToString(DriveLayoutInfo.PartitionEntry) fails in compiler due to array type and not GUID type? – Gizmo_the_Great Nov 18 '15 at 23:42
2

Depends what a "disk signature" is. i don't know what that is. i do know the following code returns:

- \\?\ide#disksandisk_sdssdx120gg25___________________r201____#5&20f0fb49&0&0.0.0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}
- \\?\ide#diskst1000dm003-9yn162______________________hp13____#5&33aaabee&0&1.0.0#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}

Are those "disk signatures"?

It uses the Windows Setup API:

procedure GetDisks(slDisks: TStrings);
var
    InterfaceDevInfo: HDEVINFO;
    index: DWORD;
    status: BOOL;
    Name: string;
begin
    { Get the interface device information set that contains all devices of event class. }
    InterfaceDevInfo := SetupDiGetClassDevs(
            @GUID_DEVINTERFACE_DISK,
            nil,                            // Enumerator
            0,                              // Parent Window
            (DIGCF_PRESENT or DIGCF_INTERFACEDEVICE)    // Only Devices present & Interface class
        );

    if InterfaceDevInfo = HDEVINFO(INVALID_HANDLE_VALUE) then
    begin
        RaiseEnumerateDisksError('SetupDiGetClassDevs failed', GetLastError);
        Exit;
    end;

    { Enumerate all the disk devices }
    Index := 0;
    while (True) do
    begin
        Status := GetDeviceProperty(InterfaceDevInfo, index, Name);
        if not Status then
            Break;

        slDisks.Add(Name);

        Inc(Index);
    end;
    SetupDiDestroyDeviceInfoList(InterfaceDevInfo);
end;

function GetDeviceProperty(InterfaceDevInfo: HDEVINFO; Index: LongWord;
        out Name: string): Boolean;
var
    interfaceData: TSPDeviceInterfaceData;
    interfaceDetailData: PSPDeviceInterfaceDetailData;
    status: BOOL;
    interfaceDetailDataSize: LongInt;
    reqSize: Cardinal;
    errorCode: LongInt;
begin
    Result := False;

    ZeroMemory(@interfaceData, SizeOf(InterfaceData));
    interfaceData.cbSize := SizeOf(interfaceData);

    //Retreiving context structure for specified device interface
    status := SetupDiEnumDeviceInterfaces(
            InterfaceDevInfo,               // Interface Device Info handle
            nil,                                // Device Info data
            GUID_DEVINTERFACE_DISK,     // Interface registered by driver
            Index,                          // Member
            interfaceData);             // Device Interface Data

    if not status then
    begin
        errorCode := GetLastError;
        if (errorCode = ERROR_NO_MORE_ITEMS ) then
        begin
            //no more interfaces, exit returning default value of False
            Exit;
        end
        else
        begin
            RaiseEnumerateDisksError('SetupDiEnumDeviceInterfaces failed.', errorCode);
        end;
    end;

    // Find out required buffer size, so pass nil
    status := SetupDiGetDeviceInterfaceDetail(
            InterfaceDevInfo,       // Interface Device info handle
            @interfaceData,     // Interface data for the event class
            nil,                        // Checking for buffer size
            0,                          // Checking for buffer size
            reqSize,                    // Buffer size required to get the detail data
            nil);                       // Checking for buffer size

    // This call returns ERROR_INSUFFICIENT_BUFFER with reqSize
    // set to the required buffer size. Ignore the above error and
    // pass a bigger penis to get the detail data
    if not status then
    begin
        errorCode := GetLastError;
        if errorCode <> ERROR_INSUFFICIENT_BUFFER then
        begin
            RaiseEnumerateDisksError('SetupDiGetDeviceInterfaceDetail failed.', errorCode);
            Exit;
        end;
    end;

    // Allocate memory to get the interface detail data
    // This contains the devicepath we need to open the device
    interfaceDetailDataSize := reqSize;
    GetMem(interfaceDetailData, interfaceDetailDataSize);
    ZeroMemory(interfaceDetailData, interfaceDetailDataSize);

    interfaceDetailData.cbSize := SizeOf(TSPDeviceInterfaceDetailData);
//  ineerfaceDetailData.cbSize := 5; //ansi version
//  ineerfaceDetailData.cbSize := 6; //unicode version

    //Getting interface detail data into properly sized buffer...
    status := SetupDiGetDeviceInterfaceDetail(
            InterfaceDevInfo,               // Interface Device info handle
            @interfaceData,             // Interface data for the event class
            interfaceDetailData,            // Interface detail data
            interfaceDetailDataSize,    // Interface detail data size
            reqSize,                            // Buffer size required to get the detail data
            nil);                               // Interface device info

    if not Status then
    begin
        RaiseEnumerateDisksError('Error in SetupDiGetDeviceInterfaceDetail', GetLastError);
        Exit;
    end;

    // Now we have the device path.
    Name := PChar(@interfaceDetailData.DevicePath[0]);

(*
    //DevicePath is suitable for a CreateFile, whish is what i want in the end
    hDevice := CreateFile(
            PChar(interfaceDetailData.DevicePath),    // device interface name
            GENERIC_READ or GENERIC_WRITE,       // dwDesiredAccess
            FILE_SHARE_READ or FILE_SHARE_WRITE, // dwShareMode
            nil,                               // lpSecurityAttributes
            OPEN_EXISTING,                      // dwCreationDistribution
            0,                                  // dwFlagsAndAttributes
            0                                // hTemplateFile
        );
*)
    Result := True;
end;
Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219
  • @DavidHeffernan i didn't know what a *disk signature* was. But i see it in the question now; i was confused by the *"don't do this"* part. – Ian Boyd Jun 15 '13 at 04:00
  • OK, so now you do know what the question is about, are you just going to leave an answer that is totally wrong? – David Heffernan Jun 16 '13 at 09:16
1

I tested this code on several computers and it works:

MBR disks:

The Disk Signature/Identifier is a 4-byte (longword) number that is randomly generated when the Master Boot Record/Partition Table is first created and stored at byte offset 1B8 (hex) or 440 (dec) through 1BB (hex) or 443 (dec) in the MBR disk sector (0). So, on any MBR disks you can read it directly from that location:

const
  // Max number of drives assuming primary/secondary, master/slave topology
  MAX_IDE_DRIVES = 16;
var
   i: Integer;
   RawMBR: array[0..511] of Byte;
   btsIO: DWORD;
   hDevice: THandle;
   s: string;
begin
   s := '';
   for i := 0 to MAX_IDE_DRIVES - 1 do
   begin
      hDevice := CreateFile(PChar('\\.\PHYSICALDRIVE' + IntToStr(i)), GENERIC_READ,
         FILE_SHARE_READ or FILE_SHARE_WRITE, nil, OPEN_EXISTING, 0, 0);
      if hDevice <> INVALID_HANDLE_VALUE then
      begin
         SetFilePointer(hDevice, 0, nil, FILE_BEGIN);  //MBR starts in sector 0
         if not ReadFile(hDevice, RawMBR[0], 512, btsIO, nil) then
         begin
            CloseHandle(hDevice);
            Continue;
         end;
         CloseHandle(hDevice);
         s := s + 'Disk ' + IntToStr(i) + ' = ' + IntToHex(RawMBR[443], 2) + ' ' +
            IntToHex(RawMBR[442], 2) + ' ' + IntToHex(RawMBR[441], 2) +
            ' ' + IntToHex(RawMBR[440], 2) + #13#10;
      end;
   end;
   ShowMessage(s);
end;

GPT Disks:

The Disk Signature/Identifier is a 16-byte (GUID) number that is randomly generated when the GPT is first created and stored at byte offset 038 (hex) or 56 (dec) through 047 (hex) or 71 (dec) in the GPT disk sector (1). So, on any GPT disks you can read it directly from that location:

const
  // Max number of drives assuming primary/secondary, master/slave topology
  MAX_IDE_DRIVES = 16;
var
   i: Integer;
   RawMBR: array[0..511] of Byte;
   btsIO: DWORD;
   hDevice: THandle;
   s: string;
begin
   s := '';
   for i := 0 to MAX_IDE_DRIVES - 1 do
   begin
      hDevice := CreateFile(PChar('\\.\PHYSICALDRIVE' + IntToStr(i)), GENERIC_READ,
         FILE_SHARE_READ or FILE_SHARE_WRITE, nil, OPEN_EXISTING, 0, 0);
      if hDevice <> INVALID_HANDLE_VALUE then
      begin
         SetFilePointer(hDevice, 512, nil, FILE_BEGIN); //GPT starts in sector 1
         if not ReadFile(hDevice, RawMBR[0], 512, btsIO, nil) then
         begin
            CloseHandle(hDevice);
            Continue;
         end;
         CloseHandle(hDevice);
         s := s + 'Disk ' + IntToStr(i) + ' = ' + IntToHex(RawMBR[59], 2) +
            ' ' + IntToHex(RawMBR[58], 2) + ' ' + IntToHex(RawMBR[57], 2) +
            ' ' + IntToHex(RawMBR[56], 2) + ' - ' + IntToHex(RawMBR[61], 2) +
            ' ' + IntToHex(RawMBR[60], 2) + ' - ' + IntToHex(RawMBR[63], 2) +
            ' ' + IntToHex(RawMBR[62], 2) + ' - ' + IntToHex(RawMBR[64], 2) +
            ' ' + IntToHex(RawMBR[65], 2) + ' - ' + IntToHex(RawMBR[66], 2) +
            ' ' + IntToHex(RawMBR[67], 2) + ' ' + IntToHex(RawMBR[68], 2) +
            ' ' + IntToHex(RawMBR[69], 2) + ' ' + IntToHex(RawMBR[70], 2) +
            ' ' + IntToHex(RawMBR[71], 2) +
            #13#10;
      end;
   end;
   ShowMessage(s);
end;

Ok, now let's combine them:

procedure TForm1.Button1Click(Sender: TObject);
const
   // Max number of drives assuming primary/secondary, master/slave topology
   MAX_IDE_DRIVES = 16;
var
   i: Integer;
   DiskType: Byte;
   RawMBR: array[0..511] of Byte;
   btsIO: DWORD;
   hDevice: THandle;
   s: string;
begin
   s := '';
   for i := 0 to MAX_IDE_DRIVES - 1 do
   begin
      hDevice := CreateFile(PChar('\\.\PHYSICALDRIVE' + IntToStr(i)), GENERIC_READ,
         FILE_SHARE_READ or FILE_SHARE_WRITE, nil, OPEN_EXISTING, 0, 0);
      if hDevice <> INVALID_HANDLE_VALUE then
      begin
         SetFilePointer(hDevice, 512, nil, FILE_BEGIN); //sector 1 for GPT
         if not ReadFile(hDevice, RawMBR[0], 512, btsIO, nil) then
         begin
            CloseHandle(hDevice);
            Continue;
         end;
         if (IntToHex(RawMBR[0], 2) + IntToHex(RawMBR[1], 2) +
            IntToHex(RawMBR[2], 2) + IntToHex(RawMBR[3], 2) +
            IntToHex(RawMBR[4], 2) + IntToHex(RawMBR[5], 2) +
            IntToHex(RawMBR[6], 2) + IntToHex(RawMBR[7], 2)) =
            '4546492050415254' then //EFI PART
            DiskType := 1 //GPT
         else
         begin
            DiskType := 0; //MBR
            SetFilePointer(hDevice, 0, nil, FILE_BEGIN); //sector 0 for MBR
            if not ReadFile(hDevice, RawMBR[0], 512, btsIO, nil) then
            begin
               CloseHandle(hDevice);
               Continue;
            end;
         end;
         CloseHandle(hDevice);
         if DiskType = 0 then
            s := s + 'Disk ' + IntToStr(i) + ' = ' + IntToHex(RawMBR[443], 2) + ' ' +
               IntToHex(RawMBR[442], 2) + ' ' + IntToHex(RawMBR[441], 2) +
               ' ' + IntToHex(RawMBR[440], 2) + #13#10
         else
            s := s + 'Disk ' + IntToStr(i) + ' = ' + IntToHex(RawMBR[59], 2) +
               ' ' + IntToHex(RawMBR[58], 2) + ' ' + IntToHex(RawMBR[57], 2) +
               ' ' + IntToHex(RawMBR[56], 2) + ' - ' + IntToHex(RawMBR[61], 2) +
               ' ' + IntToHex(RawMBR[60], 2) + ' - ' + IntToHex(RawMBR[63], 2) +
               ' ' + IntToHex(RawMBR[62], 2) + ' - ' + IntToHex(RawMBR[64], 2) +
               ' ' + IntToHex(RawMBR[65], 2) + ' - ' + IntToHex(RawMBR[66], 2) +
               ' ' + IntToHex(RawMBR[67], 2) + ' ' + IntToHex(RawMBR[68], 2) +
               ' ' + IntToHex(RawMBR[69], 2) + ' ' + IntToHex(RawMBR[70], 2) +
               ' ' + IntToHex(RawMBR[71], 2) +
               #13#10;
      end;
   end;
   ShowMessage(s);
end;

This code requires elevated privileges to gain access to the disks.

Jack Compton
  • 59
  • 1
  • 6
  • Could you explain why this works. In case anyone wants to use it, it would help if they could determine whether the code is reliable across any hardware. What's more, doesn't the code require elevation in order to perform raw disk access? – David Heffernan Jun 14 '13 at 22:30
  • I don't understand the obsession with byte by byte IntToHex and string comparisons. Why not read into an integer variable of the right size. Comparing strings is weird. – David Heffernan Jun 15 '13 at 12:48
  • Well, this is just an example to see (more clear) how it works and to compare with what Diskpart shows (also in hexa). The actual implementation in an application will be more efficient... – Jack Compton Jun 15 '13 at 13:00
  • If you are going to require elevation, then at least use the supported mechanisms to read out the information. See my second answer for the details. The system provides supported API calls for reading this information which you should make use of. – David Heffernan Jun 16 '13 at 10:49
  • This can be seen by the fact that your code above does fail in some situtations. For example, I have a card reader on my machine and the drives associated with the card reader don't respond to any of the code in this answer. The call to `ReadFile` fails. But since your code doesn't check for errors it still writes out signatures for those drives and they happen to match the signatures of the main hard drive on the machine since `RawMBR` has not been modified. This is a good example of why it is prudent to use the officially supported APIs that get it right. – David Heffernan Jun 16 '13 at 11:05
  • Why would you prefer the code above to the officially supported approach? I also note that you got snotty about receiving a single down vote, but your voting record is nothing to write home about. – David Heffernan Jun 16 '13 at 12:14
  • Also, it would be so much nicer if this code did a `Move` or a cast to read out Integer or `TGUID`. Rather than those byte by byte coding. It would be very much shorter and more readable. If you aren't sure what I mean, I'd be happy to edit for you. – David Heffernan Jun 16 '13 at 12:23
  • @David Heffernan 1. Because for my program it's a great solution. 2. Because in my 38 years I learned that we should not always comply with what others say (= copy what they do) and be original if we want to make good programs. I agree that sometimes we should follow some guidelines but still... – Jack Compton Jun 16 '13 at 13:12
  • @David Heffernan I already optimized this code in my application, I could try to optimize it here but, to save time, you could edit it if you want. This is the best way, since you know 100% what you like :D Btw, it was not my choice to make the users of my application to run it elevated. I was forced to do that because of something I can't control. But so far no user complained about that. Only you did... – Jack Compton Jun 16 '13 at 13:12
  • @David Heffernan Regarding me been "snotty": I was just a little hurt+angry because you downvoted me after adding more info to the Q. Yes, I know it was you. Deny it, if you want, it doesn't matter. And since I'm new here with this name, it's normal I don't have votes, Yes, you're a "power user" and I'm a "nobody", thanks for point that out. That shows that I was right about how "power users" rule this "world" and "bending" the laws as they like. – Jack Compton Jun 16 '13 at 13:13
  • @David Heffernan And, yes, I know I can't win, that's 100% normal in any human world. That's why I erased most of my previous comments, because it's useless to try to fight that. Don't worry, I won't ask question(s) here EVER. Good readens to me. I let you been the "king" here and trash me like you want (with some other bk's "applauding"). I won't enter here anymore, not even to read some answer. Feel good about yourself, you won. Still, I wish you a long and happy life... – Jack Compton Jun 16 '13 at 13:13
  • It's not very easy to help you is it? Clearly it's a disadvantage that the `CreateFile` route requires elevation. If your app already requires elevation, then that's fine. But for readers other than yourself, it's worth pointing out the pros and cons and letting them decide. I made no judgement. I've no idea why you are accusing me of downvoting you. I've offered lots of help and provided answers. Why are you attacking me? I'm trying to help. – David Heffernan Jun 16 '13 at 13:18
  • Please do ask questions here. This is a good question and I upvoted it. I found it interesting, hence all my comments and my answers. I think your answer has value but it would be much much better if you didn't do all that byte by byte stuff. It's not about optimisation, it's about clarity and readability. You can pick out the GUID in a single line and then use `GuidToString` to output it. Much better. I'm encouraging you to improve your answer. I even offered to do it for you. For the benefit of future readers. – David Heffernan Jun 16 '13 at 13:20
  • And nobody here, least of all me, has been trashing you. We've all been trying to help. Finally, with regards voting, you can vote. We like people who contribute to the community. Ask good questions and improve them when requested, like you did. And partake in accepting the best answers and voting. Your presence here is very welcome, all the more so if you partake in the community. – David Heffernan Jun 16 '13 at 13:22