Skip to content

Instantly share code, notes, and snippets.

@jborean93
Created April 14, 2021 22:26
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jborean93/50a517a8105338b28256ff0ea27ab2c8 to your computer and use it in GitHub Desktop.
Save jborean93/50a517a8105338b28256ff0ea27ab2c8 to your computer and use it in GitHub Desktop.
Gets extended attributes for a file on an NTFS volume
# Copyright: (c) 2021, Jordan Borean (@jborean93) <jborean93@gmail.com>
# MIT License (see LICENSE or https://opensource.org/licenses/MIT)
class EncodingTransformAttribute : Management.Automation.ArgumentTransformationAttribute {
[object] Transform([Management.Automation.EngineIntrinsics]$engineIntrinsics, [object]$InputData) {
$outputData = switch ($InputData) {
{ $_ -is [Text.Encoding] } { $_ }
{ $_ -is [string] } {
switch ($_) {
ASCII { [Text.ASCIIEncoding]::new() }
BigEndianUnicode { [Text.UnicodeEncoding]::new($true, $true) }
BigEndianUTF32 { [Text.UTF32Encoding]::new($true, $true) }
ANSI {
# I don't like using the term ANSI here but it's better than Default used in Windows
# PowerShell and it's more relevant to the terms that Windows uses. This is the "ANSI"
# system codepage set and is the most likely encoding used for EA values.
# We cannot use [Text.Encoding]::Default as on .NET Core it's always set to UTF-8.
$raw = Add-Type -Namespace Encoding -Name Native -PassThru -MemberDefinition @'
[DllImport("Kernel32.dll")]
public static extern Int32 GetACP();
'@
[Text.Encoding]::GetEncoding($raw::GetACP())
}
OEM { [Console]::OutputEncoding }
Unicode { [Text.UnicodeEncoding]::new() }
UTF8 {
# I feel strongly that UTF8 should be BOM-less and you need to opt-in to a BOM. While this
# differs from Windows PowerShell it's how PowerShell acts.
[Text.UTF8Encoding]::new($false)
}
UTF8BOM { [Text.UTF8Encoding]::new($true) }
UTF8NoBOM { [Text.UTF8Encoding]::new($false) }
UTF32 { [Text.UTF32Encoding]::new() }
default { [Text.Encoding]::GetEncoding($_) }
}
}
{ $_ -is [int] } { [Text.Encoding]::GetEncoding($_) }
default {
throw [Management.Automation.ArgumentTransformationMetadataException]::new(
"Could not convert input '$_' to a valid Encoding object."
)
}
}
return $outputData
}
}
Function Get-ExtendedAttribute {
<#
.SYNOPSIS
Get NTFS extended attributes from a file.
.DESCRIPTION
Gets the extended attributes on an NTFS file system for the file(s) specified.
.PARAMETER Path
The list of paths to get the extended attributes for, can include wildcards.
.PARAMETER LiteralPath
The list of literal paths to get the extended attributes for.
.PARAMETER Encoding
The encoding used when reading the extended attribute value. This defaults to the system "ANSI" encoding but can
be set to another value like UTF8 if the extended attribute values are encoding differently.
.EXAMPLE
'my_file.txt' | Get-ExtendedAttribute
.EXAMPLE Get extended attribute with UTF-8 encoding
'my_file.txt' | Get-ExtendedAttribute -Encoding UTF8
.NOTES
Extended attributes (EA) on NTFS are not the same as an Alternate Data Stream (ADS). EA's can store a maximum of
65535 bytes in a file.
#>
[CmdletBinding(DefaultParameterSetName='Path')]
[OutputType('ExtendedAttribute')]
param (
[Parameter(
Mandatory = $true,
Position = 0,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
ParameterSetName = 'Path'
)]
[SupportsWildcards()]
[ValidateNotNullOrEmpty()]
[String[]]
$Path,
[Parameter(
Mandatory = $true,
Position = 0,
ValueFromPipelineByPropertyName = $true,
ParameterSetName = 'LiteralPath'
)]
[Alias('PSPath')]
[ValidateNotNullOrEmpty()]
[String[]]
$LiteralPath,
[EncodingTransformAttribute()]
[Text.Encoding]
$Encoding = 'ANSI'
)
begin {
Add-Type -TypeDefinition @'
using Microsoft.Win32.SafeHandles;
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
namespace EA
{
internal class NativeHelpers
{
[StructLayout(LayoutKind.Sequential)]
public struct FILE_EA_INFORMATION
{
public Int32 EaSize;
}
[StructLayout(LayoutKind.Sequential)]
public struct FILE_FULL_EA_INFORMATION
{
public Int32 NextEntryOffset;
public byte Flags;
public byte EaNameLength;
public UInt16 EaValueLength;
[MarshalAs(UnmanagedType.ByValArray, SizeConst=1)] public char[] EaName;
}
[StructLayout(LayoutKind.Sequential)]
public struct IO_STATUS_BLOCK
{
public UInt32 status;
public IntPtr Information;
}
}
internal class NativeMethods
{
[DllImport("Kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern SafeFileHandle CreateFileW(
string lpFileName,
NativeFileAccess dwDesiredAccess,
FileShare dwShareMode,
IntPtr lpSecurityAttributes,
FileMode dwCreationDisposition,
UInt32 dwFlagsAndAttributes,
IntPtr hTemplateFile);
[DllImport("Kernel32.dll")]
public static extern Int32 GetACP();
[DllImport("Ntdll.dll")]
public static extern UInt32 NtQueryEaFile(
SafeHandle FileHandle,
out NativeHelpers.IO_STATUS_BLOCK IoStatusBlock,
IntPtr Buffer,
Int32 Length,
bool ReturnSingleEntry,
IntPtr EaList,
UInt32 EaListLength,
IntPtr EaIndex,
bool RestartScan);
[DllImport("NtDll.dll")]
public static extern UInt32 NtQueryInformationFile(
SafeHandle FileHandle,
out NativeHelpers.IO_STATUS_BLOCK IOStatusBlock,
IntPtr FileInformation,
Int32 Length,
UInt32 FileInformationClass);
[DllImport("Ntdll.dll")]
public static extern Int32 RtlNtStatusToDosError(
UInt32 Status);
}
internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid
{
public SafeMemoryBuffer(int cb) : base(true)
{
base.SetHandle(Marshal.AllocHGlobal(cb));
}
protected override bool ReleaseHandle()
{
Marshal.FreeHGlobal(handle);
return true;
}
}
public enum EAFlag : byte
{
None = 0x00,
NeedEa = 0x80,
}
[Flags]
public enum FileFlags : uint
{
OpenNoRecall = 0x00100000,
OpenReparsePoint = 0x00200000,
SessionAware = 0x00800000,
PosixSemantics = 0x01000000,
BackupSemantics = 0x02000000,
DeleteOnClose = 0x04000000,
SequentialScan = 0x08000000,
RandomAccess = 0x10000000,
NoBuffering = 0x20000000,
Overlapped = 0x40000000,
WriteThrough = 0x80000000,
}
[Flags]
public enum NativeFileAccess : uint
{
ReadData = 0x00000001,
WriteData = 0x00000002,
AppendData = 0x00000004,
ReadEA = 0x00000008,
WriteEA = 0x00000010,
Execute = 0x00000020,
DeleteChild = 0x00000040,
ReadAttributes = 0x00000080,
WriteAttributes = 0x00000100,
Delete = 0x00010000,
ReadControl = 0x00020000,
WriteDAC = 0x00040000,
WriteOwner = 0x00080000,
Synchronize = 0x00100000,
AccessSystemSecurity = 0x01000000,
MaximumAllowed = 0x02000000,
GenericAll = 0x10000000,
GenericExecute = 0x20000000,
GenericWrite = 0x40000000,
GenericRead = 0x80000000,
}
public class RawExtendedAttribute
{
public EAFlag Flags { get; set; }
public String Name { get; set; }
public byte[] Value { get; set; }
}
public class Win32Exception : System.ComponentModel.Win32Exception
{
private string _msg;
public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
public Win32Exception(int errorCode, string message) : base(errorCode)
{
_msg = String.Format("{0} ({1}, Win32ErrorCode {2} - 0x{2:X8})", message, base.Message, errorCode);
}
public override string Message { get { return _msg; } }
public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
}
public class Utils
{
public static SafeFileHandle CreateFile(string fileName, NativeFileAccess desiredAccess, FileShare shareMode,
FileMode creationDisposition, FileAttributes attributes, FileFlags flags)
{
UInt32 flagsAndAttrs = (UInt32)attributes | (UInt32)flags;
SafeFileHandle handle = NativeMethods.CreateFileW(fileName, desiredAccess, shareMode, IntPtr.Zero,
creationDisposition, flagsAndAttrs, IntPtr.Zero);
if (handle.IsInvalid)
throw new Win32Exception(String.Format("Failed to open {0}", fileName));
return handle;
}
public static IEnumerable<RawExtendedAttribute> QueryEAFile(SafeHandle handle)
{
int eaLength = GetEaLength(handle);
if (eaLength == 0)
yield break;
using (SafeMemoryBuffer eaBuf = new SafeMemoryBuffer(eaLength))
{
NativeHelpers.IO_STATUS_BLOCK ioStatus;
IntPtr offset = eaBuf.DangerousGetHandle();
UInt32 res = NativeMethods.NtQueryEaFile(handle, out ioStatus, offset, eaLength,
false, IntPtr.Zero, 0, IntPtr.Zero, false);
if (res != 0)
{
int win32ErrorCode = NativeMethods.RtlNtStatusToDosError(res);
throw new Win32Exception(win32ErrorCode, "NtQueryEaFile()) failed");
}
// The EA name is encoded with the system's "ANSI" codepage. The ANSI term here is misued but that is
// how it is referred to in Windows. We cannot use Encoding.Default as on .NET Core this always
// returns UTF-8 so we use tha native function to get the proper encoding.
Encoding ansiEncoding = Encoding.GetEncoding(NativeMethods.GetACP());
Int32 nextOffset = 0;
do
{
var eaInfo = (NativeHelpers.FILE_FULL_EA_INFORMATION)Marshal.PtrToStructure(offset,
typeof(NativeHelpers.FILE_FULL_EA_INFORMATION));
nextOffset = eaInfo.NextEntryOffset;
byte[] rawName = new byte[eaInfo.EaNameLength];
IntPtr nameBuffer = IntPtr.Add(offset, 8);
Marshal.Copy(nameBuffer, rawName, 0, rawName.Length);
string name = ansiEncoding.GetString(rawName);
byte[] value = new byte[eaInfo.EaValueLength];
IntPtr valueBuffer = IntPtr.Add(nameBuffer, rawName.Length + 1);
Marshal.Copy(valueBuffer, value, 0, value.Length);
RawExtendedAttribute rawEA = new RawExtendedAttribute()
{
Flags = (EAFlag)eaInfo.Flags,
Name = name,
Value = value,
};
yield return rawEA;
offset = IntPtr.Add(offset, nextOffset);
} while (nextOffset != 0);
}
}
private static int GetEaLength(SafeHandle handle)
{
int bufferLength = Marshal.SizeOf(typeof(NativeHelpers.FILE_EA_INFORMATION));
using (SafeMemoryBuffer buffer = new SafeMemoryBuffer(bufferLength))
{
NativeHelpers.IO_STATUS_BLOCK ioStatus;
UInt32 res = NativeMethods.NtQueryInformationFile(handle, out ioStatus, buffer.DangerousGetHandle(),
bufferLength, 7);
if (res != 0)
{
int win32ErrorCode = NativeMethods.RtlNtStatusToDosError(res);
throw new Win32Exception(win32ErrorCode, "NtQueryInformationFile(FileEaInformation) failed");
}
var eaInfo = (NativeHelpers.FILE_EA_INFORMATION)Marshal.PtrToStructure(
buffer.DangerousGetHandle(), typeof(NativeHelpers.FILE_EA_INFORMATION));
return eaInfo.EaSize;
}
}
}
}
'@
}
process {
if ($PSCmdlet.ParameterSetName -eq 'Path') {
$allPaths = $Path | ForEach-Object -Process {
$provider = $null
try {
$PSCmdlet.SessionState.Path.GetResolvedProviderPathFromPSPath($_, [ref]$provider)
}
catch [System.Management.Automation.ItemNotFoundException] {
$PSCmdlet.WriteError($_)
}
}
}
elseif ($PSCmdlet.ParameterSetName -eq 'LiteralPath') {
$allPaths = $LiteralPath | ForEach-Object -Process {
$PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($_)
}
}
foreach ($filePath in $allPaths) {
if (-not (Test-Path -LiteralPath $filePath)) {
$errDetails = @{
Message = "Cannot find path '$filePath' because it does not exist."
Category = 'ObjectNotFound'
TargetObject = $filePath
}
Write-Error @errDetails
continue
}
$handle = $null
try {
$handle = [EA.Utils]::CreateFile($filePath, 'ReadEA', 'ReadWrite', 'Open', 'Normal',
'BackupSemantics')
[EA.Utils]::QueryEAFile($handle) | ForEach-Object -Process {
[PSCustomObject]@{
PSTypeName = 'ExtendedAttribute'
Path = $filePath
Name = $_.Name
Value = $Encoding.GetString($_.Value)
Flags = $_.Flags
}
}
}
catch {
$PSCmdlet.WriteError($_)
}
finally {
if ($handle) {
$handle.Dispose()
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment