Converting IADsLargeInteger objects to DateTime objects in Powershell

There are several existing blog posts that discuss various methods for doing just this task.  One that pops up in a number of search engines is the following:

PowerShell: Convert Active Directory IADSLargeInteger to System.Int64

I found that this code has a fatal error.  In the code, there are no comments or documentation in the script to indicate what type of object is expected to be passed in $adsLargeInteger parameter.  I tried the following:

$DE = [ADSI]$LDAP_PATH
$i64val = ConvertADSLargeInteger $DE.PwdLastSet

The cmdlet spewed an error that InvokeMember could not find the HighPart and LowPart properties.  As I investigated this problem, I recalled my experience with Windows Scripting Host and how it would mundge type date into different VARIANTS and on occasion, you would get a VARIANT pointing to a reference for the value and on others you would get a VARIANT that pointed to the value itself.  This got me thinking, so I began looking at the type information being passed to the ConvertADSLargeInteger code. 

As I suspected, accessing the PwdLastSet property using the $DE.PwdLastSet notation, powershell returned a System.DirectoryServices.PropertyValueCollection and the ConvertADSlargeInteger code as it was written expected a System.__ComObject.  If I used the following notation the cmdlet  would actually return an int64 representation of the PwdLastSet property of the user:

$DE = [ADSI]$LDAP_PATH
$i64val = ConvertADSLargeInteger $DE.Properties[“PwdLastSet”][0]

To make the code a bit more robust, I added a type check to insure that before I called the InvokeMember method that object being used was in fact a System.__ComObject.  In addition, I did not need the int64 representation but the actual DateTime object represented in the int64 value.  To make the code easy to understand and modify or use later, I took advantage of the self documenting features of powershell, creating the source code in the below code window.  You can cut and past the code into a file, then use dot sourcing to pull the cmdlet into your runspace host.  Once in the runspace, you can use the Get-Help cmdlet to display help examples and details on how the cmdlet works.

<#######################################################################
#
# DISCLAIMER:
#
# This sample is provided as is and is not meant for use on a production environment.
# It is provided only for illustrative purposes. The end user must test and modify the
# sample to suit their target environment.
#
# Microsoft can make no representation concerning the content of this sample. Microsoft
# is providing this information only as a convenience to you. This is to inform you that
# Microsoft has not tested the sample and therefore cannot make any representations
# regarding the quality, safety, or suitability of any code or information found here.
#
#
# Date : 9/5/2014
# Version : 1.0
# Description :
# Source for Convert-ADSlargeIntegerToDateTime used to convert IADsLargeInteger values
# System.Datetime values.
#
########################################################################>
function Convert-ADSLargeIntegerToDateTime{
<#
.DESCRIPTION
This CMDLET takes as input an object that represents an IADsLargeInteger and returns
a System.DateTime object that represents the FILETIME value represented in the
IADsLargeInteger.

Note:
This cmdlet is derived from the blog post:
<weblogs.asp.net/adweigert/powershell-convert-active-directory-iadslargeinteger-to-system-int64>

The method assumes that you are passing in a System.__ComObject and will work unmodified
if this is the case. However, powershell will access the IADsLargeInteger wrapper object as
several different types, the two that the method will now handle are:

System.DirectoryServices.PropertyValueCollection
Returned when accessing the PwdLastSet property in the following manner:
$User = [ADSI]$ldap_ADsPath
$PwdLst = $User.PwdLastSet
Since LDAP API returns the value as if its an array, the DirectoryEntry .net object
wraps the values up in a ProertyValueCollection.

and

System.__ComObject
Returned when you access the PwdlastSet property in the following manner:
$User = [ADSI]$ldap_ADsPath
$PwdLst = $User.Properties["PwdLastSet"][0]
Using this method, the code accesses the 0th element in the PropertyValueCollection
which will be the com object wrapper for the IADsLargeInteger interface. 

.SYNOPSIS
This cmdlet will take as input a System.DirectoryServices.ProperyValueCollection or a
System.__ComObject and will return the date that is converted from the IADslargeInteger.
basically, the function will convert to a data the PwdLastSet and other AD attributes that
are returned by ADSI as IADsLargeInteger objects.

.Example
To use the cmdlet you need to make sure that the PS1 file that contains the comdlet has
been dot sourced into the runspace.

This can be done using the following method.
Lets assume the cmdlet source code is in a file with the path C:\Psh\largeinteger.ps1
you can dot source the file into the run space by typing:

.<SPACE><Path_To_File>

In this case:

. c:\psh\largeinteger.ps1

Dot sourcing is similiar to using a #include statment in C++.

.EXAMPLE
For a user, return the last time the user changed their password. This information
is stored in the PwdLastSet attribute of the DirectoryEntry object.
In this case, a System.DirectoryServices.PropertyValueCollection will be returned.

The powershell command stream would looks similiar to the following:

$Path = "LDAP://CN=Ford Prefect,ou=hitch hikers,dc=br549root,dc=nttest,dc=microsoft,dc=com"
$DE = [ADSI]$Path
Convert-ADSlargeIntegerToDateTime $DE.PwdLastSet

Bind to the user object.
Once bound, pass the PwdLastSet property to the Convert-ADSLargeIntegerToDateTime cmdlet.

.EXAMPLE
For a user, return the last time the user changed their password. This information
is stored in the PwdLastSet attribute of the DirectoryEntry object. Access the
property using the DirectryEntry.Properties attribute by indexing into the properties
using the PwdLastSet index string, returning the 1st element in the array.
Using this methond, a System.__ComObject will be returned by .net

$Path = "LDAP://CN=Ford Prefect,ou=hitch hikers,dc=br549root,dc=nttest,dc=microsoft,dc=com"
$DE = [ADSI]$Path
Convert-ADSlargeIntegerDateTime $DE.Properties["PwdLastSet"][0]

Bind to the user object.
Once bound, pass the PwdLastSet property to the Convert-ADSLargeIntegerToDateTime cmdlet.

#>
Param(
[Parameter(HelpMessage="ADsLargeInteger")]
[ValidateNotNullOrEmpty()]
[object] $ADsLargeInteger
)
#
# check to make sure that the object is a System.__ComObject
# if it is not, then argument is assumed to be an array of System.__ComObjects
# and we must have a single instance of the System.__ComObject
# that represents a single IADsLargeInteger interface.
# otherwise, the InvokeMember method will fail.
#
if( $adsLargeInteger.GetType().ToString -eq "System.__ComObject" )
{
$wkval = $adsLargeInteger
}
else
{
#
# Array of System.__ComObject objects
# Access the first one.
#
$wkval = $adsLargeinteger[0]
}
#
# get the high and low parts
#
$highPart = $wkval.GetType().InvokeMember("HighPart", [System.Reflection.BindingFlags]::GetProperty, $null, $wkval, $null)
$lowPart = $wkval.GetType().InvokeMember("LowPart", [System.Reflection.BindingFlags]::GetProperty, $null, $wkval, $null)

    $bytes = [System.BitConverter]::GetBytes($highPart)
$tmp = [System.Byte[]]@(0,0,0,0,0,0,0,0)
[System.Array]::Copy($bytes, 0, $tmp, 4, 4)
$highPart = [System.BitConverter]::ToInt64($tmp, 0)

    $bytes = [System.BitConverter]::GetBytes($lowPart)
$lowPart = [System.BitConverter]::ToUInt32($bytes, 0)
#
# Combine the values to get a tick value
# that represents a FILETIME for the date.
# see the following link for information on the FILETIME
# structure
# msdn.microsoft.com/en-us/library/windows/desktop/ms724284(v=vs.85).aspx
    #
#
$tics = $lowPart + $highPart
[datetime]::FromFileTimeUTC($tics)
}