diff --git a/PInvoke/Kernel32/PathCch.cs b/PInvoke/Kernel32/PathCch.cs index 2ff89cb9..8bc374ab 100644 --- a/PInvoke/Kernel32/PathCch.cs +++ b/PInvoke/Kernel32/PathCch.cs @@ -332,6 +332,54 @@ namespace Vanara.PInvoke [PInvokeData("pathcch.h", MSDNShortId = "89adf45f-f16d-49d1-9e76-b57b73b4d4c3")] public static extern HRESULT PathCchAddBackslashEx(StringBuilder pszPath, SizeT cchPath, out IntPtr ppszEnd, out SizeT pcchRemaining); + /// + /// + /// 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. + /// + /// + /// This function differs from PathCchAddBackslash in that it can return a pointer to the new end of the string and report the number + /// of unused characters remaining in the buffer. + /// + /// This function differs from PathAddBackslash in that it accepts paths with "\", "\?" and "\?\UNC" prefixes. + /// + /// Note This function, or PathCchAddBackslashEx, should be used in place of PathAddBackslash to prevent the + /// possibility of a buffer overrun. + /// + /// + /// + /// + /// A pointer to the path string. When this function returns successfully, the buffer contains the string with the appended + /// backslash. This value should not be NULL. + /// + /// + /// + /// The size of the buffer pointed to by pszPath, in characters. + /// + /// + /// + /// A value that, when this function returns successfully, receives the address of a pointer to the terminating null character at the + /// end of the string. + /// + /// + /// + /// + /// A pointer to a value that, when this function returns successfully, is set to the number of unused characters in the destination + /// buffer, including the terminating null character. + /// + /// + /// + /// + /// This function returns S_OK if the function was successful, S_FALSE if the path string already ends in a backslash, or an error + /// code otherwise. + /// + /// + // https://docs.microsoft.com/en-us/windows/desktop/api/pathcch/nf-pathcch-pathcchaddbackslashex HRESULT PathCchAddBackslashEx( PWSTR + // pszPath, SizeT cchPath, PWSTR *ppszEnd, SizeT *pcchRemaining ); + [DllImport(Lib.KernelBase, SetLastError = false, ExactSpelling = true, CharSet = CharSet.Unicode)] + [PInvokeData("pathcch.h", MSDNShortId = "89adf45f-f16d-49d1-9e76-b57b73b4d4c3")] + public static extern HRESULT PathCchAddBackslashEx(IntPtr pszPath, SizeT cchPath, out IntPtr ppszEnd, out SizeT pcchRemaining); + /// /// Adds a file name extension to a path string. /// This function differs from PathAddExtension in that it accepts paths with "\", "\?" and "\?\UNC" prefixes. @@ -1028,6 +1076,35 @@ namespace Vanara.PInvoke [PInvokeData("pathcch.h", MSDNShortId = "dac6cf02-7b53-449c-b788-4a7b6d1622ed")] public static extern HRESULT PathCchFindExtension(string pszPath, SizeT cchPath, out IntPtr ppszExt); + /// + /// + /// Searches a path to find its file name extension, such as ".exe" or ".ini". This function does not search for a specific + /// extension; it searches for the presence of any extension. + /// + /// This function differs from PathFindExtension in that it accepts paths with "\", "\?" and "\?\UNC" prefixes. + /// Note This function should be used in place of PathFindExtension to prevent the possibility of a buffer overrun. + /// + /// + /// A pointer to the path to search. + /// + /// + /// The size of the buffer pointed to by pszPath, in characters. + /// + /// + /// + /// The address of a pointer that, when this function returns successfully, points to the "." character that precedes the extension + /// within pszPath. If no extension is found, it points to the string's terminating null character. + /// + /// + /// + /// If this function succeeds, it returns S_OK. Otherwise, it returns an HRESULT error code. + /// + // https://docs.microsoft.com/en-us/windows/desktop/api/pathcch/nf-pathcch-pathcchfindextension HRESULT PathCchFindExtension( PCWSTR + // pszPath, SizeT cchPath, PCWSTR *ppszExt ); + [DllImport(Lib.KernelBase, SetLastError = false, ExactSpelling = true, CharSet = CharSet.Unicode)] + [PInvokeData("pathcch.h", MSDNShortId = "dac6cf02-7b53-449c-b788-4a7b6d1622ed")] + public static extern HRESULT PathCchFindExtension(IntPtr pszPath, SizeT cchPath, out IntPtr ppszExt); + /// /// Determines whether a path string refers to the root of a volume. /// This function differs from PathIsRoot in that it accepts paths with "\", "\?" and "\?\UNC" prefixes. @@ -1238,6 +1315,55 @@ namespace Vanara.PInvoke [PInvokeData("pathcch.h", MSDNShortId = "250c2faa-94bb-42c1-97d4-37f8f59dbde6")] public static extern HRESULT PathCchRemoveBackslashEx(StringBuilder pszPath, SizeT cchPath, out IntPtr ppszEnd, out SizeT pcchRemaining); + /// + /// Removes the trailing backslash from the end of a path string. + /// + /// This function differs from PathCchRemoveBackslash in that it can return a pointer to the new end of the string and report the + /// number of unused characters remaining in the buffer. + /// + /// This function differs from PathRemoveBackslash in that it accepts paths with "\", "\?" and "\?\UNC" prefixes. + /// + /// Note This function, or PathCchRemoveBackslash, should be used in place of PathRemoveBackslash to prevent the possibility + /// of a buffer overrun. + /// + /// + /// + /// + /// A pointer to the path string. When this function returns successfully, the string contains the path with any trailing backslash + /// removed. If no trailing backslash was found, the string is unchanged. + /// + /// + /// + /// The size of the buffer pointed to by pszPath, in characters. + /// + /// + /// + /// A value that, when this function returns successfully, receives the address of a pointer to end of the new string. If the string + /// is a root path such as "C:", the pointer points to the backslash; otherwise the pointer points to the string's terminating null character. + /// + /// + /// + /// + /// A pointer to a value that, when this function returns successfully, receives the number of unused characters in the destination + /// buffer, including the terminating null character. If the string is a root path such as "C:", this count includes the backslash in + /// that string. + /// + /// + /// + /// + /// This function returns S_OK if the function was successful, S_FALSE if the string was a root path or if no backslash was found, or + /// an error code otherwise. + /// + /// + /// + /// This function will not remove the backslash from a root path string, such as "C:". + /// + // https://docs.microsoft.com/en-us/windows/desktop/api/pathcch/nf-pathcch-pathcchremovebackslashex HRESULT PathCchRemoveBackslashEx( + // PWSTR pszPath, SizeT cchPath, PWSTR *ppszEnd, SizeT *pcchRemaining ); + [DllImport(Lib.KernelBase, SetLastError = false, ExactSpelling = true, CharSet = CharSet.Unicode)] + [PInvokeData("pathcch.h", MSDNShortId = "250c2faa-94bb-42c1-97d4-37f8f59dbde6")] + public static extern HRESULT PathCchRemoveBackslashEx(IntPtr pszPath, SizeT cchPath, out IntPtr ppszEnd, out SizeT pcchRemaining); + /// /// Removes the file name extension from a path, if one is present. /// This function differs from PathRemoveExtension in that it accepts paths with "\", "\?" and "\?\UNC" prefixes. @@ -1375,6 +1501,32 @@ namespace Vanara.PInvoke [PInvokeData("pathcch.h", MSDNShortId = "187bc49e-c5ae-42b8-acbd-a765f871d73b")] public static extern HRESULT PathCchSkipRoot(string pszPath, out IntPtr ppszRootEnd); + /// + /// + /// Retrieves a pointer to the first character in a path following the drive letter or Universal Naming Convention (UNC) server/share + /// path elements. + /// + /// This function differs from PathSkipRoot in that it accepts paths with "\", "\?" and "\?\UNC" prefixes. + /// + /// + /// A pointer to the path string. + /// + /// + /// + /// The address of a pointer that, when this function returns successfully, points to the first character in a path following the + /// drive letter or UNC server/share path elements. If the path consists of only a root, this value will point to the string's + /// terminating null character. + /// + /// + /// + /// If this function succeeds, it returns S_OK. Otherwise, it returns an HRESULT error code. + /// + // https://docs.microsoft.com/en-us/windows/desktop/api/pathcch/nf-pathcch-pathcchskiproot HRESULT PathCchSkipRoot( PCWSTR pszPath, + // PCWSTR *ppszRootEnd ); + [DllImport(Lib.KernelBase, SetLastError = false, ExactSpelling = true, CharSet = CharSet.Unicode)] + [PInvokeData("pathcch.h", MSDNShortId = "187bc49e-c5ae-42b8-acbd-a765f871d73b")] + public static extern HRESULT PathCchSkipRoot(IntPtr pszPath, out IntPtr ppszRootEnd); + /// /// Removes the "\?" prefix, if present, from a file path. /// @@ -1465,6 +1617,29 @@ namespace Vanara.PInvoke [DllImport(Lib.KernelBase, SetLastError = false, ExactSpelling = true, CharSet = CharSet.Unicode)] [PInvokeData("pathcch.h", MSDNShortId = "3b2a4158-63ec-49eb-a031-7493d02f2caa")] [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool PathIsUNCEx(string pszPath, ref StringBuilder ppszServer); + public static extern bool PathIsUNCEx(string pszPath, out IntPtr ppszServer); + + /// + /// Determines if a path string is a valid Universal Naming Convention (UNC) path, as opposed to a path based on a drive letter. + /// This function differs from PathIsUNC in that it also allows you to extract the name of the server from the path. + /// + /// + /// A pointer to the path string. + /// + /// + /// + /// A pointer to a string that, when this function returns successfully, receives the server portion of the UNC path. This value can + /// be NULL if you don't need this information. + /// + /// + /// + /// Returns TRUE if the string is a valid UNC path; otherwise, FALSE. + /// + // https://docs.microsoft.com/en-us/windows/desktop/api/pathcch/nf-pathcch-pathisuncex BOOL PathIsUNCEx( PCWSTR pszPath, PCWSTR + // *ppszServer ); + [DllImport(Lib.KernelBase, SetLastError = false, ExactSpelling = true, CharSet = CharSet.Unicode)] + [PInvokeData("pathcch.h", MSDNShortId = "3b2a4158-63ec-49eb-a031-7493d02f2caa")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool PathIsUNCEx(IntPtr pszPath, out IntPtr ppszServer); } } \ No newline at end of file diff --git a/UnitTests/PInvoke/Kernel32/PathCchTests.cs b/UnitTests/PInvoke/Kernel32/PathCchTests.cs new file mode 100644 index 00000000..5ea89a0a --- /dev/null +++ b/UnitTests/PInvoke/Kernel32/PathCchTests.cs @@ -0,0 +1,253 @@ +using NUnit.Framework; +using System; +using System.Runtime.InteropServices; +using System.Text; +using Vanara.Extensions; +using Vanara.InteropServices; +using static Vanara.PInvoke.AdvApi32; +using static Vanara.PInvoke.Kernel32; + +namespace Vanara.PInvoke.Tests +{ + [TestFixture] + public class PathCchTests + { + [Test] + public void PathAllocCanonicalizeTest() + { + Assert.That(PathAllocCanonicalize(@"C:\name_1\.\name_2\..\name_3", PATHCCH_OPTIONS.PATHCCH_NONE, out var ret), Is.EqualTo((HRESULT)0)); + Assert.That(ret, Is.EqualTo(@"C:\name_1\name_3")); + } + + [Test] + public void LocalStringMarshalerTest() + { + var originalByteCount = GC.GetTotalMemory(true); + for (int i = 0; i < 5000; i++) + { + PathAllocCanonicalize(@"C:\name_1\.\name_2\..\name_3", PATHCCH_OPTIONS.PATHCCH_NONE, out var ret); + Assert.That(ret, Is.EqualTo(@"C:\name_1\name_3")); + } + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + var finalByteCount = GC.GetTotalMemory(true); + Assert.That(Math.Abs(finalByteCount - originalByteCount), Is.LessThan(2000)); + } + + [Test] + public void PathAllocCombineTest() + { + Assert.That(PathAllocCombine(@"C:\name_1\.\name_2\..", @"name_3", PATHCCH_OPTIONS.PATHCCH_NONE, out var ret), Is.EqualTo((HRESULT)0)); + Assert.That(ret, Is.EqualTo(@"C:\name_1\name_3")); + } + + [Test] + public void PathCchAddBackslashTest() + { + var sb = new StringBuilder(@"C:\Temp\", 100); + Assert.That(PathCchAddBackslash(sb, sb.Capacity), Is.EqualTo((HRESULT)HRESULT.S_FALSE)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\Temp\")); + + sb = new StringBuilder(@"C:\Temp", 100); + Assert.That(PathCchAddBackslash(sb, sb.Capacity), Is.EqualTo((HRESULT)0)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\Temp\")); + } + + [Test] + public void PathCchAddBackslashExTest() + { + var sb = new StringBuilder(@"C:\Temp\", 64); + Assert.That(PathCchAddBackslashEx(sb, sb.Capacity, out var end, out var rem), Is.EqualTo((HRESULT)HRESULT.S_FALSE)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\Temp\")); + Assert.That(end, Is.Not.EqualTo(IntPtr.Zero)); + Assert.That(rem, Is.LessThan(60)); + + sb = new StringBuilder(@"C:\Temp", 64); + Assert.That(PathCchAddBackslashEx(sb, sb.Capacity, out end, out rem), Is.EqualTo((HRESULT)0)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\Temp\")); + Assert.That(rem, Is.LessThan(60)); + } + + [Test] + public void PathCchAddBackslashExTest2() + { + var sb = new SafeCoTaskMemString(@"C:\Temp\", 64); + Assert.That(PathCchAddBackslashEx((IntPtr)sb, sb.Capacity, out var end, out var rem), Is.EqualTo((HRESULT)HRESULT.S_FALSE)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\Temp\")); + Assert.That(end, Is.EqualTo(sb.DangerousGetHandle().Offset(sb.Length * 2))); + + sb = new SafeCoTaskMemString(@"C:\Temp", 64); + Assert.That(PathCchAddBackslashEx((IntPtr)sb, sb.Size, out end, out rem), Is.EqualTo((HRESULT)0)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\Temp\")); + Assert.That(end, Is.EqualTo(sb.DangerousGetHandle().Offset(sb.Length * 2))); + } + + [Test] + public void PathCchAddExtensionTest() + { + var sb = new StringBuilder(@"C:\Temp\Dog", 64); + Assert.That(PathCchAddExtension(sb, sb.Capacity, "txt"), Is.EqualTo((HRESULT)0)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\Temp\Dog.txt")); + } + + [Test] + public void PathCchAppendTest() + { + var sb = new StringBuilder(@"C:\Temp\Dog", 64); + Assert.That(PathCchAppend(sb, sb.Capacity, "txt"), Is.EqualTo((HRESULT)0)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\Temp\Dog\txt")); + } + + [Test] + public void PathCchAppendExTest() + { + var sb = new StringBuilder(@"C:\Temp\Dog", 64); + Assert.That(PathCchAppendEx(sb, sb.Capacity, "txt", PATHCCH_OPTIONS.PATHCCH_ENSURE_TRAILING_SLASH), Is.EqualTo((HRESULT)0)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\Temp\Dog\txt\")); + } + + [Test] + public void PathCchCanonicalizeTest() + { + var sb = new StringBuilder(64); + Assert.That(PathCchCanonicalize(sb, sb.Capacity, @"C:\name_1\.\name_2\..\name_3"), Is.EqualTo((HRESULT)0)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\name_1\name_3")); + } + + [Test] + public void PathCchCanonicalizeExTest() + { + var sb = new StringBuilder(64); + Assert.That(PathCchCanonicalizeEx(sb, sb.Capacity, @"C:\name_1\.\name_2\..\name_3", PATHCCH_OPTIONS.PATHCCH_ENSURE_TRAILING_SLASH), Is.EqualTo((HRESULT)0)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\name_1\name_3\")); + } + + [Test] + public void PathCchCombineTest() + { + var sb = new StringBuilder(64); + Assert.That(PathCchCombine(sb, sb.Capacity, @"C:\name_1\.\name_2\..", @"name_3"), Is.EqualTo((HRESULT)0)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\name_1\name_3")); + } + + [Test] + public void PathCchCombineExTest() + { + var sb = new StringBuilder(64); + Assert.That(PathCchCombineEx(sb, sb.Capacity, @"C:\name_1\.\name_2\..", @"name_3", PATHCCH_OPTIONS.PATHCCH_ENSURE_TRAILING_SLASH), Is.EqualTo((HRESULT)0)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\name_1\name_3\")); + } + + [Test] + public void PathCchFindExtensionTest() + { + var sb = new SafeCoTaskMemString(@"C:\Temp\dog.txt", 64); + Assert.That(PathCchFindExtension((IntPtr)sb, sb.Capacity, out var ptr), Is.EqualTo((HRESULT)0)); + Assert.That(ptr, Is.EqualTo(sb.DangerousGetHandle().Offset(22))); + } + + [Test] + public void PathCchIsRootTest() + { + Assert.That(PathCchIsRoot(@"C:\"), Is.True); + } + + [Test] + public void PathCchRemoveBackslashTest() + { + var sb = new StringBuilder(@"C:\Temp\", 64); + Assert.That(PathCchRemoveBackslash(sb, sb.Capacity), Is.EqualTo((HRESULT)0)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\Temp")); + + sb = new StringBuilder(@"C:\Temp", 64); + Assert.That(PathCchRemoveBackslash(sb, sb.Capacity), Is.EqualTo((HRESULT)HRESULT.S_FALSE)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\Temp")); + } + + [Test] + public void PathCchRemoveBackslashExTest() + { + var sb = new SafeCoTaskMemString(@"C:\Temp\", 64); + Assert.That(PathCchRemoveBackslashEx((IntPtr)sb, sb.Capacity, out var end, out var rem), Is.EqualTo((HRESULT)0)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\Temp")); + Assert.That(end, Is.EqualTo(sb.DangerousGetHandle().Offset(14))); + + sb = new SafeCoTaskMemString(@"C:\Temp", 64); + Assert.That(PathCchRemoveBackslashEx((IntPtr)sb, sb.Capacity, out end, out rem), Is.EqualTo((HRESULT)HRESULT.S_FALSE)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\Temp")); + Assert.That(end, Is.EqualTo(sb.DangerousGetHandle().Offset(14))); + } + + [Test] + public void PathCchRemoveExtensionTest() + { + var sb = new StringBuilder(@"C:\Temp\dog.txt", 64); + Assert.That(PathCchRemoveExtension(sb, sb.Capacity), Is.EqualTo((HRESULT)0)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\Temp\dog")); + + sb = new StringBuilder(@"C:\Temp\dog", 64); + Assert.That(PathCchRemoveExtension(sb, sb.Capacity), Is.EqualTo((HRESULT)HRESULT.S_FALSE)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\Temp\dog")); + } + + [Test] + public void PathCchRemoveFileSpecTest() + { + var sb = new StringBuilder(@"C:\foo\bar.txt", 64); + Assert.That(PathCchRemoveFileSpec(sb, sb.Capacity), Is.EqualTo((HRESULT)0)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\foo")); + } + + [Test] + public void PathCchRenameExtensionTest() + { + var sb = new StringBuilder(@"C:\Temp\dog.txt", 64); + Assert.That(PathCchRenameExtension(sb, sb.Capacity, "doc"), Is.EqualTo((HRESULT)0)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\Temp\dog.doc")); + + sb = new StringBuilder(@"C:\Temp\dog", 64); + Assert.That(PathCchRenameExtension(sb, sb.Capacity, "txt"), Is.EqualTo((HRESULT)0)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\Temp\dog.txt")); + } + + [Test] + public void PathCchSkipRootTest() + { + var sb = new SafeCoTaskMemString(@"C:\Temp\", 64); + Assert.That(PathCchSkipRoot((IntPtr)sb, out var end), Is.EqualTo((HRESULT)0)); + Assert.That(end, Is.EqualTo(sb.DangerousGetHandle().Offset(6))); + } + + [Test] + public void PathCchStripPrefixTest() + { + var sb = new StringBuilder(@"\\?\C:\foo\bar.txt", 64); + Assert.That(PathCchStripPrefix(sb, sb.Capacity), Is.EqualTo((HRESULT)0)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\foo\bar.txt")); + + sb = new StringBuilder(@"C:\foo\bar.txt", 64); + Assert.That(PathCchStripPrefix(sb, sb.Capacity), Is.EqualTo((HRESULT)HRESULT.S_FALSE)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\foo\bar.txt")); + } + + [Test] + public void PathCchStripToRootTest() + { + var sb = new StringBuilder(@"C:\foo\bar.txt", 64); + Assert.That(PathCchStripToRoot(sb, sb.Capacity), Is.EqualTo((HRESULT)0)); + Assert.That(sb.ToString(), Is.EqualTo(@"C:\")); + + sb = new StringBuilder(@"\\path1\path2", 64); + Assert.That(PathCchStripToRoot(sb, sb.Capacity), Is.EqualTo((HRESULT)HRESULT.S_FALSE)); + Assert.That(sb.ToString(), Is.EqualTo(@"\\path1\path2")); + } + + [Test] + public void PathIsUNCExTest() + { + Assert.That(PathIsUNCEx(@"\\path1\path2\path3", out var svr), Is.True); + Assert.That(Marshal.PtrToStringUni(svr), Is.EqualTo(@"path1\path2\path3")); + } + } +} \ No newline at end of file