/* * Copyright 2018 Nikolay Sivov * Copyright 2018 Zhiyi Zhang * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA */ #include #include "windef.h" #include "winbase.h" #include "pathcch.h" #include "strsafe.h" #include "wine/debug.h" #include "wine/unicode.h" WINE_DEFAULT_DEBUG_CHANNEL(path); static SIZE_T strnlenW(const WCHAR *string, SIZE_T maxlen) { SIZE_T i; for (i = 0; i < maxlen; i++) if (!string[i]) break; return i; } static BOOL is_prefixed_unc(const WCHAR *string) { static const WCHAR prefixed_unc[] = {'\\', '\\', '?', '\\', 'U', 'N', 'C', '\\'}; return !strncmpiW(string, prefixed_unc, ARRAY_SIZE(prefixed_unc)); } static BOOL is_prefixed_disk(const WCHAR *string) { static const WCHAR prefix[] = {'\\', '\\', '?', '\\'}; return !strncmpW(string, prefix, ARRAY_SIZE(prefix)) && isalphaW(string[4]) && string[5] == ':'; } static BOOL is_prefixed_volume(const WCHAR *string) { static const WCHAR prefixed_volume[] = {'\\', '\\', '?', '\\', 'V', 'o', 'l', 'u', 'm', 'e'}; const WCHAR *guid; INT i = 0; if (strncmpiW(string, prefixed_volume, ARRAY_SIZE(prefixed_volume))) return FALSE; guid = string + ARRAY_SIZE(prefixed_volume); while (i <= 37) { switch (i) { case 0: if (guid[i] != '{') return FALSE; break; case 9: case 14: case 19: case 24: if (guid[i] != '-') return FALSE; break; case 37: if (guid[i] != '}') return FALSE; break; default: if (!isalnumW(guid[i])) return FALSE; break; } i++; } return TRUE; } /* Get the next character beyond end of the segment. Return TRUE if the last segment ends with a backslash */ static BOOL get_next_segment(const WCHAR *next, const WCHAR **next_segment) { while (*next && *next != '\\') next++; if (*next == '\\') { *next_segment = next + 1; return TRUE; } else { *next_segment = next; return FALSE; } } /* Find the last character of the root in a path, if there is one, without any segments */ static const WCHAR *get_root_end(const WCHAR *path) { /* Find path root */ if (is_prefixed_volume(path)) return path[48] == '\\' ? path + 48 : path + 47; else if (is_prefixed_unc(path)) return path + 7; else if (is_prefixed_disk(path)) return path[6] == '\\' ? path + 6 : path + 5; /* \\ */ else if (path[0] == '\\' && path[1] == '\\') return path + 1; /* \ */ else if (path[0] == '\\') return path; /* X:\ */ else if (isalphaW(path[0]) && path[1] == ':') return path[2] == '\\' ? path + 2 : path + 1; else return NULL; } HRESULT WINAPI PathAllocCanonicalize(const WCHAR *path_in, DWORD flags, WCHAR **path_out) { WCHAR *buffer, *dst; const WCHAR *src; const WCHAR *root_end; SIZE_T buffer_size, length; TRACE("%s %#x %p\n", debugstr_w(path_in), flags, path_out); if (!path_in || !path_out || ((flags & PATHCCH_FORCE_ENABLE_LONG_NAME_PROCESS) && (flags & PATHCCH_FORCE_DISABLE_LONG_NAME_PROCESS)) || (flags & (PATHCCH_FORCE_ENABLE_LONG_NAME_PROCESS | PATHCCH_FORCE_DISABLE_LONG_NAME_PROCESS) && !(flags & PATHCCH_ALLOW_LONG_PATHS)) || ((flags & PATHCCH_ENSURE_IS_EXTENDED_LENGTH_PATH) && (flags & PATHCCH_ALLOW_LONG_PATHS))) { if (path_out) *path_out = NULL; return E_INVALIDARG; } length = strlenW(path_in); if ((length + 1 > MAX_PATH && !(flags & (PATHCCH_ALLOW_LONG_PATHS | PATHCCH_ENSURE_IS_EXTENDED_LENGTH_PATH))) || (length + 1 > PATHCCH_MAX_CCH)) { *path_out = NULL; return HRESULT_FROM_WIN32(ERROR_FILENAME_EXCED_RANGE); } /* PATHCCH_ENSURE_IS_EXTENDED_LENGTH_PATH implies PATHCCH_DO_NOT_NORMALIZE_SEGMENTS */ if (flags & PATHCCH_ENSURE_IS_EXTENDED_LENGTH_PATH) flags |= PATHCCH_DO_NOT_NORMALIZE_SEGMENTS; /* path length + possible \\?\ addition + possible \ addition + NUL */ buffer_size = (length + 6) * sizeof(WCHAR); buffer = LocalAlloc(LMEM_ZEROINIT, buffer_size); if (!buffer) { *path_out = NULL; return E_OUTOFMEMORY; } src = path_in; dst = buffer; root_end = get_root_end(path_in); if (root_end) root_end = buffer + (root_end - path_in); /* Copy path root */ if (root_end) { memcpy(dst, src, (root_end - buffer + 1) * sizeof(WCHAR)); src += root_end - buffer + 1; if(PathCchStripPrefix(dst, length + 6) == S_OK) { /* Fill in \ in X:\ if the \ is missing */ if(isalphaW(dst[0]) && dst[1] == ':' && dst[2]!= '\\') { dst[2] = '\\'; dst[3] = 0; } dst = buffer + strlenW(buffer); root_end = dst; } else dst += root_end - buffer + 1; } while (*src) { if (src[0] == '.') { if (src[1] == '.') { /* Keep one . after * */ if (dst > buffer && dst[-1] == '*') { *dst++ = *src++; continue; } /* Keep the . if one of the following is true: * 1. PATHCCH_DO_NOT_NORMALIZE_SEGMENTS * 2. in form of a..b */ if (dst > buffer && (((flags & PATHCCH_DO_NOT_NORMALIZE_SEGMENTS) && dst[-1] != '\\') || (dst[-1] != '\\' && src[2] != '\\' && src[2]))) { *dst++ = *src++; *dst++ = *src++; continue; } /* Remove the \ before .. if the \ is not part of root */ if (dst > buffer && dst[-1] == '\\' && (!root_end || dst - 1 > root_end)) { *--dst = '\0'; /* Remove characters until a \ is encountered */ while (dst > buffer) { if (dst[-1] == '\\') { *--dst = 0; break; } else *--dst = 0; } } /* Remove the extra \ after .. if the \ before .. wasn't deleted */ else if (src[2] == '\\') src++; src += 2; } else { /* Keep the . if one of the following is true: * 1. PATHCCH_DO_NOT_NORMALIZE_SEGMENTS * 2. in form of a.b, which is used in domain names * 3. *. */ if (dst > buffer && ((flags & PATHCCH_DO_NOT_NORMALIZE_SEGMENTS && dst[-1] != '\\') || (dst[-1] != '\\' && src[1] != '\\' && src[1]) || (dst[-1] == '*'))) { *dst++ = *src++; continue; } /* Remove the \ before . if the \ is not part of root */ if (dst > buffer && dst[-1] == '\\' && (!root_end || dst - 1 > root_end)) dst--; /* Remove the extra \ after . if the \ before . wasn't deleted */ else if (src[1] == '\\') src++; src++; } /* If X:\ is not complete, then complete it */ if (isalphaW(buffer[0]) && buffer[1] == ':' && buffer[2] != '\\') { root_end = buffer + 2; dst = buffer + 3; buffer[2] = '\\'; /* If next character is \, use the \ to fill in */ if (src[0] == '\\') src++; } } /* Copy over */ else *dst++ = *src++; } /* End the path */ *dst = 0; /* If result path is empty, fill in \ */ if (!*buffer) { buffer[0] = '\\'; buffer[1] = 0; } /* Extend the path if needed */ length = strlenW(buffer); if (((length + 1 > MAX_PATH && isalphaW(buffer[0]) && buffer[1] == ':') || (isalphaW(buffer[0]) && buffer[1] == ':' && flags & PATHCCH_ENSURE_IS_EXTENDED_LENGTH_PATH)) && !(flags & PATHCCH_FORCE_ENABLE_LONG_NAME_PROCESS)) { memmove(buffer + 4, buffer, (length + 1) * sizeof(WCHAR)); buffer[0] = '\\'; buffer[1] = '\\'; buffer[2] = '?'; buffer[3] = '\\'; } /* Add a trailing backslash to the path if needed */ if (flags & PATHCCH_ENSURE_TRAILING_SLASH) PathCchAddBackslash(buffer, buffer_size); *path_out = buffer; return S_OK; } HRESULT WINAPI PathAllocCombine(const WCHAR *path1, const WCHAR *path2, DWORD flags, WCHAR **out) { SIZE_T combined_length, length2; WCHAR *combined_path; BOOL from_path2 = FALSE; HRESULT hr; TRACE("%s %s %#x %p\n", wine_dbgstr_w(path1), wine_dbgstr_w(path2), flags, out); if ((!path1 && !path2) || !out) { if (out) *out = NULL; return E_INVALIDARG; } if (!path1 || !path2) return PathAllocCanonicalize(path1 ? path1 : path2, flags, out); /* If path2 is fully qualified, use path2 only */ if (path2 && ((isalphaW(path2[0]) && path2[1] == ':') || (path2[0] == '\\' && path2[1] == '\\'))) { path1 = path2; path2 = NULL; from_path2 = TRUE; } length2 = path2 ? strlenW(path2) : 0; /* path1 length + path2 length + possible backslash + NULL */ combined_length = strlenW(path1) + length2 + 2; combined_path = HeapAlloc(GetProcessHeap(), 0, combined_length * sizeof(WCHAR)); if (!combined_path) { *out = NULL; return E_OUTOFMEMORY; } lstrcpyW(combined_path, path1); PathCchStripPrefix(combined_path, combined_length); if (from_path2) PathCchAddBackslashEx(combined_path, combined_length, NULL, NULL); if (path2 && path2[0]) { if (path2[0] == '\\' && path2[1] != '\\') { PathCchStripToRoot(combined_path, combined_length); path2++; } PathCchAddBackslashEx(combined_path, combined_length, NULL, NULL); lstrcatW(combined_path, path2); } hr = PathAllocCanonicalize(combined_path, flags, out); HeapFree(GetProcessHeap(), 0, combined_path); return hr; } HRESULT WINAPI PathCchAddBackslash(WCHAR *path, SIZE_T size) { return PathCchAddBackslashEx(path, size, NULL, NULL); } HRESULT WINAPI PathCchAddBackslashEx(WCHAR *path, SIZE_T size, WCHAR **endptr, SIZE_T *remaining) { BOOL needs_termination; SIZE_T length; TRACE("%s, %lu, %p, %p\n", debugstr_w(path), size, endptr, remaining); length = strlenW(path); needs_termination = size && length && path[length - 1] != '\\'; if (length >= (needs_termination ? size - 1 : size)) { if (endptr) *endptr = NULL; if (remaining) *remaining = 0; return STRSAFE_E_INSUFFICIENT_BUFFER; } if (!needs_termination) { if (endptr) *endptr = path + length; if (remaining) *remaining = size - length; return S_FALSE; } path[length++] = '\\'; path[length] = 0; if (endptr) *endptr = path + length; if (remaining) *remaining = size - length; return S_OK; } HRESULT WINAPI PathCchAddExtension(WCHAR *path, SIZE_T size, const WCHAR *extension) { const WCHAR *existing_extension, *next; SIZE_T path_length, extension_length, dot_length; BOOL has_dot; HRESULT hr; TRACE("%s %lu %s\n", wine_dbgstr_w(path), size, wine_dbgstr_w(extension)); if (!path || !size || size > PATHCCH_MAX_CCH || !extension) return E_INVALIDARG; next = extension; while (*next) { if ((*next == '.' && next > extension) || *next == ' ' || *next == '\\') return E_INVALIDARG; next++; } has_dot = extension[0] == '.' ? TRUE : FALSE; hr = PathCchFindExtension(path, size, &existing_extension); if (FAILED(hr)) return hr; if (*existing_extension) return S_FALSE; path_length = strnlenW(path, size); dot_length = has_dot ? 0 : 1; extension_length = strlenW(extension); if (path_length + dot_length + extension_length + 1 > size) return STRSAFE_E_INSUFFICIENT_BUFFER; /* If extension is empty or only dot, return S_OK with path unchanged */ if (!extension[0] || (extension[0] == '.' && !extension[1])) return S_OK; if (!has_dot) { path[path_length] = '.'; path_length++; } strcpyW(path + path_length, extension); return S_OK; } HRESULT WINAPI PathCchCanonicalize(WCHAR *out, SIZE_T size, const WCHAR *in) { TRACE("%p %lu %s\n", out, size, wine_dbgstr_w(in)); /* Not X:\ and path > MAX_PATH - 4, return HRESULT_FROM_WIN32(ERROR_FILENAME_EXCED_RANGE) */ if (strlenW(in) > MAX_PATH - 4 && !(isalphaW(in[0]) && in[1] == ':' && in[2] == '\\')) return HRESULT_FROM_WIN32(ERROR_FILENAME_EXCED_RANGE); return PathCchCanonicalizeEx(out, size, in, PATHCCH_NONE); } HRESULT WINAPI PathCchCanonicalizeEx(WCHAR *out, SIZE_T size, const WCHAR *in, DWORD flags) { WCHAR *buffer; SIZE_T length; HRESULT hr; TRACE("%p %lu %s %#x\n", out, size, wine_dbgstr_w(in), flags); if (!size) return E_INVALIDARG; hr = PathAllocCanonicalize(in, flags, &buffer); if (FAILED(hr)) return hr; length = strlenW(buffer); if (size < length + 1) { /* No root and path > MAX_PATH - 4, return HRESULT_FROM_WIN32(ERROR_FILENAME_EXCED_RANGE) */ if (length > MAX_PATH - 4 && !(in[0] == '\\' || (isalphaW(in[0]) && in[1] == ':' && in[2] == '\\'))) hr = HRESULT_FROM_WIN32(ERROR_FILENAME_EXCED_RANGE); else hr = STRSAFE_E_INSUFFICIENT_BUFFER; } if (SUCCEEDED(hr)) { memcpy(out, buffer, (length + 1) * sizeof(WCHAR)); /* Fill a backslash at the end of X: */ if (isalphaW(out[0]) && out[1] == ':' && !out[2] && size > 3) { out[2] = '\\'; out[3] = 0; } } LocalFree(buffer); return hr; } HRESULT WINAPI PathCchFindExtension(const WCHAR *path, SIZE_T size, const WCHAR **extension) { const WCHAR *lastpoint = NULL; SIZE_T counter = 0; TRACE("%s %lu %p\n", wine_dbgstr_w(path), size, extension); if (!path || !size || size > PATHCCH_MAX_CCH) { *extension = NULL; return E_INVALIDARG; } while (*path) { if (*path == '\\' || *path == ' ') lastpoint = NULL; else if (*path == '.') lastpoint = path; path++; counter++; if (counter == size || counter == PATHCCH_MAX_CCH) { *extension = NULL; return E_INVALIDARG; } } *extension = lastpoint ? lastpoint : path; return S_OK; } BOOL WINAPI PathCchIsRoot(const WCHAR *path) { const WCHAR *root_end; const WCHAR *next; BOOL is_unc; TRACE("%s\n", wine_dbgstr_w(path)); if (!path || !*path) return FALSE; root_end = get_root_end(path); if (!root_end) return FALSE; if ((is_unc = is_prefixed_unc(path)) || (path[0] == '\\' && path[1] == '\\' && path[2] != '?')) { next = root_end + 1; /* No extra segments */ if ((is_unc && !*next) || (!is_unc && !*next)) return TRUE; /* Has first segment with an ending backslash but no remaining characters */ if (get_next_segment(next, &next) && !*next) return FALSE; /* Has first segment with no ending backslash */ else if (!*next) return TRUE; /* Has first segment with an ending backslash and has remaining characters*/ else { next++; /* Second segment must have no backslash and no remaining characters */ return !get_next_segment(next, &next) && !*next; } } else if (*root_end == '\\' && !root_end[1]) return TRUE; else return FALSE; } HRESULT WINAPI PathCchRemoveBackslash(WCHAR *path, SIZE_T path_size) { WCHAR *path_end; SIZE_T free_size; TRACE("%s %lu\n", debugstr_w(path), path_size); return PathCchRemoveBackslashEx(path, path_size, &path_end, &free_size); } HRESULT WINAPI PathCchRemoveBackslashEx(WCHAR *path, SIZE_T path_size, WCHAR **path_end, SIZE_T *free_size) { const WCHAR *root_end; SIZE_T path_length; TRACE("%s %lu %p %p\n", debugstr_w(path), path_size, path_end, free_size); if (!path_size || !path_end || !free_size) { if (path_end) *path_end = NULL; if (free_size) *free_size = 0; return E_INVALIDARG; } path_length = strnlenW(path, path_size); if (path_length == path_size && !path[path_length]) return E_INVALIDARG; root_end = get_root_end(path); if (path_length > 0 && path[path_length - 1] == '\\') { *path_end = path + path_length - 1; *free_size = path_size - path_length + 1; /* If the last character is beyond end of root */ if (!root_end || path + path_length - 1 > root_end) { path[path_length - 1] = 0; return S_OK; } else return S_FALSE; } else { *path_end = path + path_length; *free_size = path_size - path_length; return S_FALSE; } } HRESULT WINAPI PathCchRemoveExtension(WCHAR *path, SIZE_T size) { const WCHAR *extension; WCHAR *next; HRESULT hr; TRACE("%s %lu\n", wine_dbgstr_w(path), size); if (!path || !size || size > PATHCCH_MAX_CCH) return E_INVALIDARG; hr = PathCchFindExtension(path, size, &extension); if (FAILED(hr)) return hr; next = path + (extension - path); while (next - path < size && *next) *next++ = 0; return next == extension ? S_FALSE : S_OK; } HRESULT WINAPI PathCchRemoveFileSpec(WCHAR *path, SIZE_T size) { const WCHAR *root_end = NULL; SIZE_T length; WCHAR *last; TRACE("%s %lu\n", wine_dbgstr_w(path), size); if (!path || !size || size > PATHCCH_MAX_CCH) return E_INVALIDARG; if (PathCchIsRoot(path)) return S_FALSE; PathCchSkipRoot(path, &root_end); /* The backslash at the end of UNC and \\* are not considered part of root in this case */ if (root_end && root_end > path && root_end[-1] == '\\' && (is_prefixed_unc(path) || (path[0] == '\\' && path[1] == '\\' && path[2] != '?'))) root_end--; length = strlenW(path); last = path + length - 1; while (last >= path && (!root_end || last >= root_end)) { if (last - path >= size) return E_INVALIDARG; if (*last == '\\') { *last-- = 0; break; } *last-- = 0; } return last != path + length - 1 ? S_OK : S_FALSE; } HRESULT WINAPI PathCchRenameExtension(WCHAR *path, SIZE_T size, const WCHAR *extension) { HRESULT hr; TRACE("%s %lu %s\n", wine_dbgstr_w(path), size, wine_dbgstr_w(extension)); hr = PathCchRemoveExtension(path, size); if (FAILED(hr)) return hr; hr = PathCchAddExtension(path, size, extension); return FAILED(hr) ? hr : S_OK; } HRESULT WINAPI PathCchSkipRoot(const WCHAR *path, const WCHAR **root_end) { static const WCHAR unc_prefix[] = {'\\', '\\', '?'}; TRACE("%s %p\n", debugstr_w(path), root_end); if (!path || !path[0] || !root_end || (!memicmpW(unc_prefix, path, ARRAY_SIZE(unc_prefix)) && !is_prefixed_volume(path) && !is_prefixed_unc(path) && !is_prefixed_disk(path))) return E_INVALIDARG; *root_end = get_root_end(path); if (*root_end) { (*root_end)++; if (is_prefixed_unc(path)) { get_next_segment(*root_end, root_end); get_next_segment(*root_end, root_end); } else if (path[0] == '\\' && path[1] == '\\' && path[2] != '?') { /* Skip share server */ get_next_segment(*root_end, root_end); /* If mount point is empty, don't skip over mount point */ if (**root_end != '\\') get_next_segment(*root_end, root_end); } } return *root_end ? S_OK : E_INVALIDARG; } HRESULT WINAPI PathCchStripPrefix(WCHAR *path, SIZE_T size) { TRACE("%s %lu\n", wine_dbgstr_w(path), size); if (!path || !size || size > PATHCCH_MAX_CCH) return E_INVALIDARG; if (is_prefixed_unc(path)) { /* \\?\UNC\a -> \\a */ if (size < strlenW(path + 8) + 3) return E_INVALIDARG; strcpyW(path + 2, path + 8); return S_OK; } else if (is_prefixed_disk(path)) { /* \\?\C:\ -> C:\ */ if (size < strlenW(path + 4) + 1) return E_INVALIDARG; strcpyW(path, path + 4); return S_OK; } else return S_FALSE; } HRESULT WINAPI PathCchStripToRoot(WCHAR *path, SIZE_T size) { const WCHAR *root_end; WCHAR *segment_end; BOOL is_unc; TRACE("%s %lu\n", wine_dbgstr_w(path), size); if (!path || !*path || !size || size > PATHCCH_MAX_CCH) return E_INVALIDARG; /* \\\\?\\UNC\\* and \\\\* have to have at least two extra segments to be striped, * e.g. \\\\?\\UNC\\a\\b\\c -> \\\\?\\UNC\\a\\b * \\\\a\\b\\c -> \\\\a\\b */ if ((is_unc = is_prefixed_unc(path)) || (path[0] == '\\' && path[1] == '\\' && path[2] != '?')) { root_end = is_unc ? path + 8 : path + 3; if (!get_next_segment(root_end, &root_end)) return S_FALSE; if (!get_next_segment(root_end, &root_end)) return S_FALSE; if (root_end - path >= size) return E_INVALIDARG; segment_end = path + (root_end - path) - 1; *segment_end = 0; return S_OK; } else if (PathCchSkipRoot(path, &root_end) == S_OK) { if (root_end - path >= size) return E_INVALIDARG; segment_end = path + (root_end - path); if (!*segment_end) return S_FALSE; *segment_end = 0; return S_OK; } else return E_INVALIDARG; } BOOL WINAPI PathIsUNCEx(const WCHAR *path, const WCHAR **server) { const WCHAR *result = NULL; TRACE("%s %p\n", wine_dbgstr_w(path), server); if (is_prefixed_unc(path)) result = path + 8; else if (path[0] == '\\' && path[1] == '\\' && path[2] != '?') result = path + 2; if (server) *server = result; return result ? TRUE : FALSE; }