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.