Vanara/System/Path.cs

475 lines
26 KiB
C#

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Vanara.PInvoke;
using static Vanara.PInvoke.Kernel32;
using static Vanara.PInvoke.ShlwApi;
namespace Vanara.IO;
/// <summary>
/// Performs operations on String instances that contain file or directory path information. These operations are performed in a cross-platform manner.
/// </summary>
public static class PathEx
{
/// <summary>The type of character retrieved from <see cref="GetCharType(char)"/>.</summary>
[Flags]
public enum PathCharType : uint
{
/// <summary>The character is not valid in a path.</summary>
Invalid = 0x0000,
/// <summary>The character is valid in a long file name.</summary>
LongFileName = 0x0001,
/// <summary>The character is valid in a short (8.3) file name.</summary>
ShortFileName = 0x0002,
/// <summary>The character is a wildcard character.</summary>
Wildcard = 0x0004,
/// <summary>The character is a path separator.</summary>
Separator = 0x0008,
}
/// <summary>
/// Adds a backslash to the end of a string to create the correct syntax for a path. If the source path already has a trailing
/// backslash, no backslash will be added.
/// </summary>
/// <param name="pszPath">The path string. This value should not be <see langword="null"/>.</param>
/// <returns>The path string with the appended backslash.</returns>
public static string AddBackslash(string pszPath) => SBAllocCallRet((s, sz) => PathCchAddBackslash(s, sz), pszPath, err: ThrowIfNotOkOrFalse);
/// <summary>
/// Adds a file name extension to a path string.
/// </summary>
/// <param name="path">The path of the file. This value should not be <see langword="null"/>.</param>
/// <param name="ext">The extension to add. This string can be given either with or without a preceding period (".ext" or "ext").</param>
/// <returns>The string with the appended extension.</returns>
public static string AddExtension(string path, string ext) => SBAllocCallRet((s, sz) => PathCchAddExtension(s, sz, ext), path, err: ThrowIfNotOkOrFalse);
/// <summary>
/// Converts a path string into a canonical form.
/// </summary>
/// <param name="path">The file or directory to canonicalize.</param>
/// <returns>The canonicalized path string.</returns>
public static string Canonicalize(string path) { PathAllocCanonicalize(path, PATHCCH_OPTIONS.PATHCCH_ALLOW_LONG_PATHS, out var s).ThrowIfFailed(); return s; }
/// <summary>Truncates a path to fit within a certain number of characters by replacing path components with ellipses.</summary>
/// <param name="path">The file or directory to compact.</param>
/// <param name="maxChars">The maximum number of characters to be contained in the new string.</param>
/// <returns>The altered, compacted path string.</returns>
public static string Compact(string path, int maxChars) => SBAllocCallRet((s, sz) => PathCompactPathEx(s, path, (uint)maxChars + 1));
#if !NETSTANDARD2_0
/// <summary>
/// Truncates a file path to fit within a given pixel width by replacing path components with ellipses.
/// </summary>
/// <param name="path">The file or directory to compact.</param>
/// <param name="pixelWidth">The width, in pixels, in which the string must fit.</param>
/// <param name="dc">A device context used for font metrics. This value can be <see langword="null"/>.</param>
/// <returns>The modified string.</returns>
public static string Compact(string path, int pixelWidth, System.Drawing.IDeviceContext dc) => SBAllocCallRet((s, sz) => PathCompactPath(dc?.GetHdc() ?? default, s, (uint)pixelWidth), path, (uint)(path?.Length + 1 ?? MAX_PATH));
#endif
/// <summary>Converts a file URL to a Microsoft MS-DOS path.</summary>
/// <param name="url">The URL.</param>
/// <returns>The resulting MS-DOS path.</returns>
public static string CreateFromUrl(string url) => SBAllocCallRet((s, sz) => PathCreateFromUrl(url, s, ref sz));
/// <summary>Searches for a file in a list of directories.</summary>
/// <param name="fileName">The file name for which to search.</param>
/// <param name="searchDirectories">
/// An optional array of directories to be searched first. This value can be <see langword="null"/>. If no directories are specified,
/// it attempts to find the file by searching standard directories such as System32 and the directories specified in the PATH
/// environment variable.
/// </param>
/// <param name="foundFilePath">If the file is found, this variable holds the fully qualified path name of the found file.</param>
/// <returns>Returns <see langword="true"/> if found, or <see langword="false"/> otherwise.</returns>
public static bool FileExistsOnPath(string fileName, IEnumerable<string>? searchDirectories, [NotNullWhen(true)] out string? foundFilePath)
{
var sb = new StringBuilder(fileName, MAX_PATH);
string?[]? dirs = searchDirectories?.ToArray();
if (dirs != null)
{
Array.Resize(ref dirs, dirs.Length + 1);
dirs[dirs.Length - 1] = null;
}
foundFilePath = PathFindOnPath(sb, dirs?.WhereNotNull().ToArray()) ? sb.ToString() : null;
return foundFilePath != null;
}
/// <summary>Determines the type of character in relation to a path string.</summary>
/// <param name="ch">The character for which to determine the type.</param>
/// <returns>The type of character.</returns>
public static PathCharType GetCharType(char ch) => (PathCharType)PathGetCharType(ch);
/// <summary>Searches a path for a drive letter within the range of 'A' to 'Z' and returns the corresponding drive number.</summary>
/// <param name="path">The file or directory to assess.</param>
/// <returns>Returns 0 through 25 (corresponding to 'A' through 'Z') if the path has a drive letter, or -1 otherwise.</returns>
public static int GetDriveNumber(string path) => PathGetDriveNumber(path);
/// <summary>Removes the file name extension from a path, if one is present.</summary>
/// <param name="path">The path string to edit.</param>
/// <returns>The path with any extension removed. If no extension was found, the string is unchanged.</returns>
public static string GetPathWithoutExtension(string path) => SBAllocCallRet((s, sz) => PathCchRemoveExtension(s, sz), path, err: ThrowIfNotOkOrFalse);
/// <summary>
/// Removes the last element in a path string, whether that element is a file name or a directory name. The element's leading
/// backslash is also removed.
/// </summary>
/// <param name="path">The path string to edit.</param>
/// <returns>
/// The path with its last element and its leading backslash removed. This function does not affect root paths such as "C:". In the
/// case of a root path, the path string is returned unaltered. If a path string ends with a trailing backslash, only that backslash
/// is removed.
/// </returns>
public static string GetPathWithoutLastElement(string path) => SBAllocCallRet((s, sz) => PathCchRemoveFileSpec(s, sz), path, err: ThrowIfNotOkOrFalse);
/// <summary>
/// Gets the string following the drive letter or Universal Naming Convention (UNC) server/share path elements in a path.
/// </summary>
/// <param name="path">The path string.</param>
/// <returns>
/// The string following the drive letter or UNC server/share path elements. If the path consists of only a root, this value will be
/// <see cref="string.Empty"/>.
/// </returns>
public static string GetPathWithoutRoot(string path)
{
PathCchSkipRoot(path, out var ptr).ThrowIfFailed();
return ptr.ToString();
}
/// <summary>Gets the path without the trailing backslash from the end of a path string.</summary>
/// <param name="path">The path string.</param>
/// <returns>
/// The path with any trailing backslash removed. If no trailing backslash was found, the string is the same as <paramref name="path"/>.
/// </returns>
public static string GetPathWithoutTrailingBackslash(string path) => SBAllocCallRet((s, sz) => PathCchRemoveBackslash(s, sz), path, (uint)path.Length + 1, ThrowIfNotOkOrFalse);
/// <summary>Gets a relative path from one file or folder to another.</summary>
/// <param name="fromPath">The path that defines the start of the relative path.</param>
/// <param name="fromIsDir">If <see langword="true"/>, assume that <paramref name="fromPath"/> is a directory.</param>
/// <param name="toPath">The path that defines the endpoint of the relative path.</param>
/// <param name="toIsDir">If <see langword="true"/>, assume that <paramref name="toPath"/> is a directory.</param>
/// <returns>The relative path of <paramref name="toPath"/> from <paramref name="fromPath"/>.</returns>
/// <remarks>
/// This function takes a pair of paths and generates a relative path from one to the other. The paths do not have to be fully
/// qualified, but they must have a common prefix, or the function will fail and return <see langword="null"/>.
/// <para>
/// For example, let the starting point, <paramref name="fromPath"/>, be "c:\FolderA\FolderB\FolderC", and the ending point,
/// <paramref name="toPath"/>, be "c:\FolderA\FolderD\FolderE". <c>GetRelativePath</c> will return the relative path from
/// <paramref name="fromPath"/> to <paramref name="toPath"/> as: "....\FolderD\FolderE". You will get the same result if you set
/// <paramref name="fromPath"/> to "\FolderA\FolderB\FolderC" and <paramref name="toPath"/> to "\FolderA\FolderD\FolderE". On the other hand,
/// "c:\FolderA\FolderB" and "a:\FolderA\FolderD" do not share a common prefix, and the function will fail. Note that "\" is not
/// considered a prefix and is ignored. If you set <paramref name="fromPath"/> to "\FolderA\FolderB", and <paramref name="toPath"/>
/// to "\FolderC\FolderD", the function will return <see langword="null"/>.
/// </para>
/// </remarks>
public static string GetRelativePath(string fromPath, bool fromIsDir, string toPath, bool toIsDir) => SBAllocCallRet((s, sz) => PathRelativePathTo(s, fromPath, fromIsDir ? FileFlagsAndAttributes.FILE_ATTRIBUTE_EA : 0, toPath, toIsDir ? FileFlagsAndAttributes.FILE_ATTRIBUTE_DIRECTORY : 0));
/// <summary>Creates a root path from a given drive number.</summary>
/// <param name="drive">A variable that indicates the desired drive number. It should be between 0 and 25.</param>
/// <returns>
/// The constructed root path. If the call fails for any reason (for example, an invalid drive number), <see cref="string.Empty"/> is returned.
/// </returns>
public static string GetRootFromDriveNumber(int drive) => SBAllocCallRet((s, sz) => PathBuildRoot(s, drive), "", 4);
/// <summary>Gets the short path form of the specified path.</summary>
/// <param name="path">The path to convert.</param>
/// <returns>The short form of <paramref name="path"/>.</returns>
public static string GetShortPath(string path)
{
if (string.IsNullOrEmpty(path)) return path;
var shortPath = path;
var l = path.Length + 5;
var sb = new StringBuilder(l, l);
var rl = GetShortPathName(path.Length > MAX_PATH ? @"\\?\" + path : path, sb, (uint)sb.Capacity);
if (rl > 0 && rl <= l) shortPath = sb.ToString();
return shortPath;
}
/// <summary>
/// Determines if a path string is a valid Universal Naming Convention (UNC) path and extract the name of the server from the path.
/// </summary>
/// <param name="path">The UNC path.</param>
/// <returns>On success, the server portion of the UNC path. On failure, <see langword="null"/> is returned.</returns>
public static string? GetUNCServer(string path) => PathIsUNCEx(path, out var ptr) ? ptr.ToString() : null;
/// <summary>
/// Searches a path to determine if it contains a valid prefix of the type passed by <paramref name="prefix"/>. A prefix is one of
/// these types: "C:\", ".", "..", "..\".
/// </summary>
/// <param name="path">The file or directory path.</param>
/// <param name="prefix">The prefix for which to search.</param>
/// <returns><c>true</c> if the specified path has the supplied prefix; otherwise, <c>false</c>.</returns>
public static bool HasPrefix(string path, string prefix) => PathIsPrefix(prefix, path);
/// <summary>Determines whether a given file name has one of a list of suffixes.</summary>
/// <param name="path">The file path.</param>
/// <param name="suffixes">The list of suffixes to check.</param>
/// <returns>
/// A string with the matching suffix if successful, or <see langword="null"/> if <paramref name="path"/> does not end with one of
/// the specified suffixes.
/// </returns>
public static string HasSuffix(string path, string[] suffixes) => PathFindSuffixArray(path, suffixes, suffixes.Length).ToString();
/// <summary>Compares two paths to determine if they have a common root component.</summary>
/// <param name="path1">A string of maximum length MAX_PATH that contains the first path to be compared.</param>
/// <param name="path2">A string of maximum length MAX_PATH that contains the second path to be compared.</param>
/// <returns>
/// Returns <see langword="true"/> if both strings have the same root component, or <see langword="false"/> otherwise. If
/// <paramref name="path1"/> contains only the server and share, this function also returns <see langword="false"/>.
/// </returns>
public static bool HaveSameRoot(string path1, string path2) => PathIsSameRoot(path1, path2);
/// <summary>
/// Determines if a file's registered content type matches the specified content type. This function obtains the content type for the
/// specified file type and compares that string with the <paramref name="contentType"/>. The comparison is not case-sensitive.
/// </summary>
/// <param name="path">A string of maximum length MAX_PATH that contains the file whose content type will be compared.</param>
/// <param name="contentType">The content type string to which the file's registered content type will be compared.</param>
/// <returns>
/// Returns <see langword="true"/> if the file's registered content type matches <paramref name="contentType"/>, or
/// <see langword="false"/> otherwise.
/// </returns>
public static bool IsContentType(string path, string contentType) => PathIsContentType(path, contentType);
/// <summary>Verifies that a path is a valid directory.</summary>
/// <param name="path">A string of maximum length MAX_PATH that contains the path to verify.</param>
/// <returns>Returns <see langword="true"/> if the path is a valid directory; otherwise, <see langword="false"/>.</returns>
public static bool IsDirectory(string path) => PathIsDirectory(path);
/// <summary>Determines whether a specified path is an empty directory.</summary>
/// <param name="path">A string of maximum length MAX_PATH that contains the path to be tested.</param>
/// <returns>
/// Returns <see langword="true"/> if <paramref name="path"/> is an empty directory. Returns <see langword="false"/> if
/// <paramref name="path"/> is not a directory, or if it contains at least one file other than "." or "..".
/// </returns>
/// <remarks>"C:" is considered a directory.</remarks>
public static bool IsEmptyDirectory(string path) => PathIsDirectoryEmpty(path);
/// <summary>
/// Searches a path for any path-delimiting characters (for example, ':' or '' ). If there are no path-delimiting characters present,
/// the path is considered to be a File Spec path.
/// </summary>
/// <param name="path">A string of maximum length MAX_PATH that contains the path to be searched.</param>
/// <returns>
/// Returns <see langword="true"/> if there are no path-delimiting characters within the path, or <see langword="false"/> if there
/// are path-delimiting characters.
/// </returns>
public static bool IsFileSpec(string path) => PathIsFileSpec(path);
/// <summary>Determines whether a path string represents a network resource.</summary>
/// <param name="path">A string of maximum length MAX_PATH that contains the path.</param>
/// <returns>Returns <see langword="true"/> if the string represents a network resource, or <see langword="false"/> otherwise.</returns>
public static bool IsNetworkPath(string path) => PathIsNetworkPath(path);
/// <summary>Searches a path and determines if it is relative.</summary>
/// <param name="path">A string of maximum length MAX_PATH that contains the path to be searched.</param>
/// <returns>Returns <see langword="true"/> if the path is relative, or <see langword="false"/> if it is absolute.</returns>
public static bool IsRelative(string path) => PathIsRelative(path);
/// <summary>Determines whether a file name is in long format.</summary>
/// <param name="path">A string of maximum length MAX_PATH that contains the file name to be tested.</param>
/// <returns>
/// Returns <see langword="true"/> if <paramref name="path"/> exceeds the number of characters allowed by the 8.3 format, or
/// <see langword="false"/> otherwise.
/// </returns>
public static bool IsShortFileNameCompatible(string path) => !PathIsLFNFileSpec(path);
/// <summary>Determines if an existing folder contains the attributes that make it a system folder.</summary>
/// <param name="path">
/// A string of maximum length MAX_PATH that contains the name of an existing folder. The attributes for this folder will be
/// retrieved and compared with those that define a system folder. If this folder contains the attributes to make it a system folder,
/// the function returns <see langword="true"/>.
/// </param>
/// <returns>
/// Returns <see langword="true"/> if the <paramref name="path"/> represents a system folder, or <see langword="false"/> otherwise.
/// </returns>
public static bool IsSystemFolder(string path) => PathIsSystemFolder(path, 0);
/// <summary>
/// Determines if a path string is a valid Universal Naming Convention (UNC) path, as opposed to a path based on a drive letter.
/// </summary>
/// <param name="path">A string of maximum length MAX_PATH that contains the path to validate.</param>
/// <returns>Returns <see langword="true"/> if the string is a valid UNC path; otherwise, <see langword="false"/>.</returns>
public static bool IsUNC(string path) => PathIsUNC(path);
/// <summary>Determines if a string is a valid Universal Naming Convention (UNC) for a server path only.</summary>
/// <param name="path">A string of maximum length MAX_PATH that contains the path to validate.</param>
/// <returns>
/// Returns <see langword="true"/> if the string is a valid UNC path for a server only (no share name), or <see langword="false"/> otherwise.
/// </returns>
public static bool IsUNCServer(string path) => PathIsUNCServer(path);
/// <summary>Determines if a string is a valid Universal Naming Convention (UNC) share path, \server\share.</summary>
/// <param name="path">A string of maximum length MAX_PATH that contains the path to be validated.</param>
/// <returns>Returns <see langword="true"/> if the string is in the form \server\share, or <see langword="false"/> otherwise.</returns>
public static bool IsUNCServerShare(string path) => PathIsUNCServerShare(path);
/// <summary>Tests a given string to determine if it conforms to a valid URL format.</summary>
/// <param name="path">A string of maximum length MAX_PATH that contains the URL path to validate.</param>
/// <returns>Returns <see langword="true"/> if <paramref name="path"/> has a valid URL format, or <see langword="false"/> otherwise.</returns>
/// <remarks>This function does not verify that the path points to an existing site—only that it has a valid URL format.</remarks>
public static bool IsURL(string path) => PathIsURL(path);
/// <summary>Gives an existing folder the proper attributes to become a system folder.</summary>
/// <param name="path">
/// A string of length MAX_PATH that contains the name of an existing folder that will be made into a system folder.
/// </param>
/// <returns>Returns <see langword="true"/> if successful, or <see langword="false"/> otherwise.</returns>
public static bool MakeSystemFolder(string path) => PathMakeSystemFolder(path);
/// <summary>Matches a file name from a path against one or more file name patterns.</summary>
/// <param name="path">A string of maximum length MAX_PATH that contains the path from which the file name to be matched is taken.</param>
/// <param name="pattern">
/// A string of maximum length MAX_PATH that contains the file name pattern for which to search. This can be the exact name, or it
/// can contain wildcard characters. This value can contain one pattern or multiple patterns separated with semicolons.
/// </param>
/// <param name="ignorePatternWhitespace">
/// If <see langword="true"/>, ignore leading spaces in the string pointed to by <paramref name="pattern"/>.
/// </param>
/// <returns>
/// Return <see langword="true"/> if a file name pattern specified in <paramref name="pattern"/> matched the file name found in the
/// string pointed to by <paramref name="path"/>.
/// </returns>
public static bool MatchesLookupPattern(string path, string pattern, bool ignorePatternWhitespace = false) => PathMatchSpecEx(path, pattern, (ignorePatternWhitespace ? PMSF.PMSF_DONT_STRIP_SPACES : 0) | (pattern.IndexOf(';') < 0 ? PMSF.PMSF_NORMAL : PMSF.PMSF_MULTIPLE)) == HRESULT.S_OK;
/// <summary>Searches a path for spaces. If spaces are found, the entire path is enclosed in quotation marks.</summary>
/// <param name="path">A string that contains the path to search.</param>
/// <returns>A quoted string if spaces were found; otherwise, the value of <paramref name="path"/>.</returns>
public static string QuoteIfHasSpaces(string path) => SBAllocCallRet((s, sz) => PathQuoteSpaces(s), path);
/// <summary>
/// Replaces a file name's extension at the end of a path string with a new extension. If the path string does not end with an
/// extension, the new extension is added.
/// </summary>
/// <param name="path">A path string.</param>
/// <param name="newExt">
/// A new extension string. The leading '.' character is optional. In the case of an empty string (""), any existing extension in the
/// path string is removed.
/// </param>
/// <returns>If this function succeeds, it returns <paramref name="path"/> with the renamed or added extension.</returns>
public static string RenameExtension(string path, string newExt) => SBAllocCallRet((s, sz) => PathCchRenameExtension(s, sz, newExt), path);
/// <summary>Removes the decoration from a path string.</summary>
/// <param name="path">A string of length MAX_PATH that contains the path.</param>
/// <returns>The undecorated string.</returns>
/// <remarks>
/// <para>
/// A decoration consists of a pair of square brackets with one or more digits in between, inserted immediately after the base name
/// and before the file name extension.
/// </para>
/// <para>Examples</para>
/// <para>The following table illustrates how strings are modified by <c>Undecorate</c>.</para>
/// <list type="table">
/// <listheader>
/// <term>Initial String</term>
/// <term>Undecorated String</term>
/// </listheader>
/// <item>
/// <term>C:\Path\File[5].txt</term>
/// <term>C:\Path\File.txt</term>
/// </item>
/// <item>
/// <term>C:\Path\File[12]</term>
/// <term>C:\Path\File</term>
/// </item>
/// <item>
/// <term>C:\Path\File.txt</term>
/// <term>C:\Path\File.txt</term>
/// </item>
/// <item>
/// <term>C:\Path\[3].txt</term>
/// <term>C:\Path\[3].txt</term>
/// </item>
/// </list>
/// </remarks>
public static string Undecorate(string path) => SBAllocCallRet((s, sz) => PathUndecorate(s), path);
/// <summary>Replaces certain folder names in a fully qualified path with their associated environment string.</summary>
/// <param name="path">A string of maximum length MAX_PATH that contains the path to be unexpanded.</param>
/// <returns>Returns <see langword="true"/> if successful; otherwise, <see langword="false"/>.</returns>
/// <remarks>
/// <para>The following folder paths are replaced by their equivalent environment string.</para>
/// <list type="table">
/// <listheader>
/// <term>Folder</term>
/// <term>Environment String</term>
/// </listheader>
/// <item>
/// <term>The All Users profile folder</term>
/// <term>%ALLUSERSPROFILE%</term>
/// </item>
/// <item>
/// <term>The current user's application data folder (Windows Vista and later only).</term>
/// <term>%APPDATA%</term>
/// </item>
/// <item>
/// <term>The system name</term>
/// <term>%COMPUTERNAME%</term>
/// </item>
/// <item>
/// <term>The Program Files folder</term>
/// <term>%ProgramFiles%</term>
/// </item>
/// <item>
/// <term>The system root folder</term>
/// <term>%SystemRoot%</term>
/// </item>
/// <item>
/// <term>The system drive letter</term>
/// <term>%SystemDrive%</term>
/// </item>
/// <item>
/// <term>The current user's profile folder</term>
/// <term>%USERPROFILE%</term>
/// </item>
/// </list>
/// <para>
/// <c>Note</c> %APPDATA% and %USERPROFILE% are relative to the user making the call. This function does not work if the user is
/// being impersonated from a service. For further discussion of access control issues, see Access Control.
/// </para>
/// <para>
/// The environment variables listed in the above table might not all be set on all systems. If an environment variable is not set,
/// it is not unexpanded.
/// </para>
/// </remarks>
public static string UnexpandEnvironmentStrings(string path) => SBAllocCallRet((s, sz) => PathUnExpandEnvStrings(path, s, sz));
/// <summary>
/// <para>Removes the attributes from a folder that make it a system folder. This folder must actually exist in the file system.</para>
/// </summary>
/// <param name="path">
/// A string of maximum length MAX_PATH that contains the name of an existing folder that will have the system folder attributes removed.
/// </param>
/// <returns>Returns <see langword="true"/> if successful, or <see langword="false"/> otherwise.</returns>
public static bool UnmakeSystemFolder(string path) => PathUnmakeSystemFolder(path);
/// <summary>Removes quotes from the beginning and end of a path.</summary>
/// <param name="path">A string of length MAX_PATH that contains the path.</param>
/// <returns>A string with beginning and ending quotation marks removed.</returns>
public static string Unquote(string path) => SBAllocCallRet((s, sz) => PathUnquoteSpaces(s), path);
private static string SBAllocCallRet(Action<StringBuilder, uint> func, string inval = "", uint sz = MAX_PATH)
{
if (inval == null) throw new ArgumentNullException(nameof(inval));
var sb = new StringBuilder((int)sz, (int)sz);
sb.Append(inval); // Done this way in case the input string is longer than the buffer.
func(sb, sz);
return sb.ToString();
}
private static string SBAllocCallRet(Func<StringBuilder, uint, HRESULT> func, string inval = "", uint sz = PATHCCH_MAX_CCH, Action<HRESULT>? err = null)
{
if (inval == null) throw new ArgumentNullException(nameof(inval));
var sb = new StringBuilder(inval, (int)sz);
var hr = func(sb, sz);
if (err != null) err?.Invoke(hr); else hr.ThrowIfFailed();
return sb.ToString();
}
private static void ThrowIfNotOkOrFalse(HRESULT hr) { if (hr.Failed && hr != HRESULT.S_FALSE) hr.ThrowIfFailed(); }
}