Loading .NET Assemblies From Memory

Many scripts load external DLL files to get access to more functionality. Those files can be embedded into scripts as text to remove dependencies.

Many scripts load external DLL files to get access to more functionality. Those files can be embedded into scripts as text to remove dependencies.

Often a PowerShell script or module needs access to additional .NET functionality and uses Add-Type to load external DLL files. This adds dependencies to your script, though (it won’t work without the external DLL files), and while your script is accessing the DLL, it is blocked in the filesystem and cannot be removed or renamed.

By embedding the binary resource into your script as a Base64 encoded string, you can load it directly into memory. No more dependencies and blocked files.

This article describes how you can embed binary dependencies in your scripts. Whether this is always a good idea depends entirely on the use case. There is nothing fundamentally bad about loading resources from external files, and it may well be a couple of milliseconds faster. However, sometimes a single script is required, or binary dependencies are not permitted. This is when the technique described below is very helpful.

I’ll demo embedding a DLL into a script by creating a new function Get-PSOneDirectory: it can handle super-long file names. If you want to use it, too, simply install the module PSOneTools with minimum version 1.5 from the PowerShell Gallery:

Install-Module -Name PSOneTools -Scope CurrentUser -MinimumVersion 1.5 -Force

Example: Support For Super-Long Path Names

As you may have painfully experienced, the Windows operating system occasionally chokes on super-long file paths: when they exceed 256 or so bytes, Windows Explorer can no longer handle or even delete them.

There is a fairly old .NET assembly called Microsoft.Experimental.IO.dll that contains methods to work around this limitation and access files and folders with super-long path names. Provided you were lucky enough to find this DLL, PowerShell would load the dll via Add-Type and then access its methods:

# load the assembly
$Path = "C:\...\Microsoft.Experimental.IO.dll"
$assembly = Add-Type -Path $Path -PassThru

# dump the list of types imported:
$assembly

Add-Type loads the DLL into memory, and when you specify -PassThru, you get back the imported assembly. It tells you the names of the newly imported types:

IsPublic IsSerial Name                                     BaseType            
-------- -------- ----                                     --------            
False    False    LongPathCommon                           System.Object       
True     False    LongPathFile                             System.Object       
True     False    LongPathDirectory                        System.Object       
False    False    SafeFindHandle                           Microsoft.Win32.S...
False    False    NativeMethods                            System.Object       
False    True     EFileAccess                              System.Enum         
False    False    WIN32_FIND_DATA                          System.ValueType    
False    False    <EnumerateFileSystemIterator>d__0 

Test-Driving New Types

Let’s create a function Get-PSOneDirectory that can traverse filesystem directories similar to Get-ChildItem but without any path length limitations.

The function uses the static methods EnumerateDirectories() and EnumerateFiles() from the newly imported type [Microsoft.Experimental.IO.LongPathDirectory] and calls them recursively to enumerate the entire directory tree.

Note that you cannot run the script below unless you also have the DLL and have properly imported it. At the end of this article, we’ll have a script that has no dependencies and runs as-is. I have added Get-PSOneDirectory to the module PSOneTools (version 1.5) so you just need to install it from the PowerShell Gallery to immediately be able to use Get-PSOneDirectory.

# load the external DLL with the long file name support methods
# IMPORTANT: in this example, the DLL must be in the same folder as the script
# since you DO NOT HAVE the dll, you CANNOT run the script
# (which is the point of this article, and we'll REMOVE this dependency later)
Add-Type -Path "$PSScriptRoot\Microsoft.Experimental.IO.dll" 

function Get-PSOneDirectory
{
    [CmdletBinding()]
    param
    (
        # Folder path to enumerate
        [Parameter(Mandatory)]
        [string]
        $Path
    )

    # emit the path and its length:
    [PSCustomObject]@{
        PathLength = $path.Length
        Type = 'Folder'
        Path = $Path
    }

    # try and enumerate subfolders:
    try
    {
        [Microsoft.Experimental.IO.LongPathDirectory]::EnumerateDirectories($Path) |
        ForEach-Object {
            # recursively enumerate child folders:
            Get-PSOneDirectory -path $_
        }
    }
    catch 
    {
        Write-Error "Unable to open folder '$Path': $_"
    }
    
    # try and enumerate files in folder
    try
    {
        # get all files from current folder:
        [Microsoft.Experimental.IO.LongPathDirectory]::EnumerateFiles($Path) |
        ForEach-Object {
            [PSCustomObject]@{
                PathLength = $_.Length
                Type = 'Folder'
                Path = $_
            }
        }
    }
    catch 
    {
        Write-Error "Unable to open folder '$Path': $_"
    }    
} 

To find paths that exceed a given length, simply filter the results. This would list all paths inside the Windows folder that are longer than 200 characters:

# find all folders with path names longer than 200 chars
# (extend to whatever you like, i.e. to 256 to find illegal paths)
Get-PSOneDirectory c:\windows -ErrorAction SilentlyContinue | 
    Where-Object PathLength -gt 200

The result looks similar to this:

PathLength Type   Path                                                         
---------- ----   ----                                                         
       207 Folder c:\windows\assembly\GAC_MSIL\Microsoft.VisualStudio.Tools....
       204 Folder c:\windows\assembly\GAC_MSIL\Microsoft.VisualStudio.Tools....
       224 Folder c:\windows\Microsoft.NET\assembly\GAC_32\Microsoft.Securit...
       224 Folder c:\windows\Microsoft.NET\assembly\GAC_64\Microsoft.Securit...
       202 Folder c:\windows\Microsoft.NET\assembly\GAC_MSIL\Microsoft.Backg...
       202 Folder c:\windows\Microsoft.NET\assembly\GAC_MSIL\Microsoft.Backg...
       216 Folder c:\windows\Microsoft.NET\assembly\GAC_MSIL\Microsoft.Secur...
       216 Folder c:\windows\Microsoft.NET\assembly\GAC_MSIL\Microsoft.Secur...
       206 Folder c:\windows\Microsoft.NET\assembly\GAC_MSIL\Microsoft.Secur...
       228 Folder c:\windows\Microsoft.NET\assembly\GAC_MSIL\Microsoft.Secur...

This looks all very promising but since you don’t have the underlying DLL, you cannot run the script.

Embedding Binary Dependencies

To make the script run stand-alone, let’s embed the binary DLL into the script. For this, the file needs to first be converted into a Base64 Encoded string.

Step 1: Base64 Encoding

To turn any binary file into a string, you simply read the file content as bytes, then convert the bytes to a Base64-encoded string:

# take a file...
$Path = "$PSScriptRoot\Microsoft.Experimental.IO.dll"
# read all bytes...
$bytes = [System.IO.File]::ReadAllBytes($Path)
# turn bytes into Base64 string:
$string = [System.Convert]::ToBase64String($bytes)

# the result is a VERY long string. It is much longer
# than the original file:
$file = Get-Item -Path $Path
[PSCustomObject]@{
    'Original File Length' = $file.Length
    'Base64 String Length' = $string.Length
    'Size Increase' = ('{0:p}' -f ($string.Length / $file.Length))
} | Format-List

The encoded string is considerably larger than the original file because of the Base64 Encoding which is the inevitable price you pay for embedding the file into your script:

Original File Length : 13824
Base64 String Length : 18432
Size Increase        : 133.33%

Step 2: Embedding the Resource

Now that you have the encoded file as string, you can place it into your script: simply copy it into the clipboard, and paste it back as a quoted string into your script:

$string | Set-Clipboard

To load the string into memory and get access to the DLL functionality, convert it back into a byte array and load the bytes into memory. Unfortunately, Add-Type can load only file-based DLLs into memory. But you can resort to [System.Reflection.Assembly]::Load() instead.

So in the end, in the script you replace Add-Type with this chunk of code to remove the dependency. I cut out most of the original string for space reasons here, but if you want to see the full code I have used in the module PSOneTools, simply visit GitHub.

$dll = 'TVqQAAMAAAAEAAAA//8AALgAAAAA...'
$bytes = [System.Convert]::FromBase64String($dll)
[System.Reflection.Assembly]::Load($bytes)

Long Path Support Without DLLs

On Github you can view the full implementation of Get-PSOneDirectory: simply copy and paste the script code, and you are ready to use the function. I’d rather recommend you install the module PSOneTools as explained at the beginning of this article if you’d like to play with Get-PSOneDirectory yourself - that’s a lot easier.

Manipulating Files and Folders

Get-PSOneDirectory just traverses folders and can help find files and folders with long paths. It is simple to use and no longer has dependencies - that’s what this article was about.

If you want to manipulate such items, i.e. delete, copy, move or rename them, it is up to you to add more functions based on the methods available from the embedded DLL. Which isn’t really hard because there are all the basic static methods that you would need:

Action Method
Create Folder void [Microsoft.Experimental.IO.LongPathDirectory]::Create(string path)
Delete Folder void [Microsoft.Experimental.IO.LongPathDirectory]::Delete(string path)
Test Folder bool [Microsoft.Experimental.IO.LongPathDirectory]::Exists(string path)
Copy File void [Microsoft.Experimental.IO.LongPathFile]::Copy(string sourcePath, string destinationPath, bool overwrite)
Delete File void [Microsoft.Experimental.IO.LongPathFile]::Delete(string path)
Test File bool [Microsoft.Experimental.IO.LongPathFile]::Exists(string path)
Move File void [Microsoft.Experimental.IO.LongPathFile]::Move(string sourcePath, string destinationPath)
Read File System.IO.FileStream [Microsoft.Experimental.IO.LongPathFile]::Open(string path, System.IO.FileMode mode, System.IO.FileAccess access)

Renaming a file isn’t directly supported. Renaming generally really is a Move operation.

Unfortunately, there do not seem to be methods to move folders. That’s because the DLL is really basic. To move (or rename) a folder, you’d have to write your own function which recursively moves all the files in the folder.