posix_errno_t efile_make_hard_link(const efile_path_t *existing_path, const efile_path_t *new_path) { ASSERT_PATH_FORMAT(existing_path); ASSERT_PATH_FORMAT(new_path); if(!CreateHardLinkW((WCHAR*)new_path->data, (WCHAR*)existing_path->data, NULL)) { return windows_to_posix_errno(GetLastError()); } return 0; }
posix_errno_t efile_read_link(ErlNifEnv *env, const efile_path_t *path, ERL_NIF_TERM *result) { posix_errno_t posix_errno; ErlNifBinary result_bin; DWORD attributes; ASSERT_PATH_FORMAT(path); attributes = GetFileAttributesW((WCHAR*)path->data); if(attributes == INVALID_FILE_ATTRIBUTES) { return windows_to_posix_errno(GetLastError()); } else if(!(attributes & FILE_ATTRIBUTE_REPARSE_POINT)) { return EINVAL; } if(!is_name_surrogate(path)) { return EINVAL; } posix_errno = internal_read_link(path, &result_bin); if(posix_errno == 0) { if(!normalize_path_result(&result_bin)) { enif_release_binary(&result_bin); return ENOMEM; } (*result) = enif_make_binary(env, &result_bin); } return posix_errno; }
posix_errno_t efile_altname(ErlNifEnv *env, const efile_path_t *path, ERL_NIF_TERM *result) { ErlNifBinary result_bin; ASSERT_PATH_FORMAT(path); if(is_path_root(path)) { /* Root paths can't be queried so we'll just return them as they are. */ if(!enif_alloc_binary(path->size, &result_bin)) { return ENOMEM; } sys_memcpy(result_bin.data, path->data, path->size); } else { WIN32_FIND_DATAW data; HANDLE handle; WCHAR *name_buffer; int name_length; /* Reject path wildcards. */ if(wcspbrk(&((const WCHAR*)path->data)[LP_PREFIX_LENGTH], L"?*")) { return ENOENT; } handle = FindFirstFileW((const WCHAR*)path->data, &data); if(handle == INVALID_HANDLE_VALUE) { return windows_to_posix_errno(GetLastError()); } FindClose(handle); name_length = wcslen(data.cAlternateFileName); if(name_length > 0) { name_buffer = data.cAlternateFileName; } else { name_length = wcslen(data.cFileName); name_buffer = data.cFileName; } /* Include NUL-terminator; it will be removed after normalization. */ name_length += 1; if(!enif_alloc_binary(name_length * sizeof(WCHAR), &result_bin)) { return ENOMEM; } sys_memcpy(result_bin.data, name_buffer, name_length * sizeof(WCHAR)); } if(!normalize_path_result(&result_bin)) { enif_release_binary(&result_bin); return ENOMEM; } (*result) = enif_make_binary(env, &result_bin); return 0; }
posix_errno_t efile_set_cwd(const efile_path_t *path) { const WCHAR *path_start; ASSERT_PATH_FORMAT(path); /* We have to use _wchdir since that's the only function that updates the * per-drive working directory, but it naively assumes that all paths * starting with \\ are UNC paths, so we have to skip the long-path prefix. * * _wchdir doesn't handle long-prefixed UNC paths either so we hand those * to SetCurrentDirectoryW instead. The per-drive working directory is * irrelevant for such paths anyway. */ if(!IS_LONG_UNC_PATH(PATH_LENGTH(path), path->data)) { path_start = (WCHAR*)path->data + LP_PREFIX_LENGTH; if(_wchdir(path_start)) { return windows_to_posix_errno(GetLastError()); } } else { if(!SetCurrentDirectoryW((WCHAR*)path->data)) { return windows_to_posix_errno(GetLastError()); } } return 0; }
posix_errno_t efile_make_dir(const efile_path_t *path) { ASSERT_PATH_FORMAT(path); if(!CreateDirectoryW((WCHAR*)path->data, NULL)) { return windows_to_posix_errno(GetLastError()); } return 0; }
posix_errno_t efile_make_soft_link(const efile_path_t *existing_path, const efile_path_t *new_path) { DWORD link_flags; ASSERT_PATH_FORMAT(existing_path); ASSERT_PATH_FORMAT(new_path); if(has_file_attributes(existing_path, FILE_ATTRIBUTE_DIRECTORY)) { link_flags = SYMBOLIC_LINK_FLAG_DIRECTORY; } else { link_flags = 0; } if(!CreateSymbolicLinkW((WCHAR*)new_path->data, (WCHAR*)existing_path->data, link_flags)) { return windows_to_posix_errno(GetLastError()); } return 0; }
posix_errno_t efile_list_dir(ErlNifEnv *env, const efile_path_t *path, ERL_NIF_TERM *result) { ERL_NIF_TERM list_head; WIN32_FIND_DATAW data; HANDLE search_handle; WCHAR *search_path; DWORD last_error; ASSERT_PATH_FORMAT(path); search_path = enif_alloc(path->size + 2 * sizeof(WCHAR)); if(search_path == NULL) { return ENOMEM; } sys_memcpy(search_path, path->data, path->size); search_path[PATH_LENGTH(path) + 0] = L'\\'; search_path[PATH_LENGTH(path) + 1] = L'*'; search_path[PATH_LENGTH(path) + 2] = L'\0'; search_handle = FindFirstFileW(search_path, &data); last_error = GetLastError(); enif_free(search_path); if(search_handle == INVALID_HANDLE_VALUE) { return windows_to_posix_errno(last_error); } list_head = enif_make_list(env, 0); do { int name_length = wcslen(data.cFileName); if(!is_ignored_name(name_length, data.cFileName)) { unsigned char *name_bytes; ERL_NIF_TERM name_term; size_t name_size; name_size = name_length * sizeof(WCHAR); name_bytes = enif_make_new_binary(env, name_size, &name_term); sys_memcpy(name_bytes, data.cFileName, name_size); list_head = enif_make_list_cell(env, name_term, list_head); } } while(FindNextFileW(search_handle, &data)); FindClose(search_handle); (*result) = list_head; return 0; }
/* Mirrors the PathIsRootW function of the shell API, but doesn't choke on * paths longer than MAX_PATH. */ static int is_path_root(const efile_path_t *path) { const WCHAR *path_start, *path_end, *path_iterator; int length; ASSERT_PATH_FORMAT(path); if(!IS_LONG_UNC_PATH(PATH_LENGTH(path), path->data)) { path_start = (WCHAR*)path->data + LP_PREFIX_LENGTH; length = PATH_LENGTH(path) - LP_PREFIX_LENGTH; /* A single \ refers to the root of the current working directory. */ if(length == 1) { return IS_SLASH(path_start[0]); } /* Drive letter. */ if(length == 3 && iswalpha(path_start[0]) && path_start[1] == L':') { return IS_SLASH(path_start[2]); } return 0; } /* Check whether we're a UNC root, eg. \\server, \\server\share */ path_start = (WCHAR*)path->data + LP_UNC_PREFIX_LENGTH; length = PATH_LENGTH(path) - LP_UNC_PREFIX_LENGTH; path_end = &path_start[length]; path_iterator = path_start; /* Server name must be at least one character. */ if(length <= 1) { return 0; } /* Slide to the slash between the server and share names, if present. */ while(path_iterator < path_end && !IS_SLASH(*path_iterator)) { path_iterator++; } /* Slide past the end of the string, stopping at the first slash we * encounter. */ do { path_iterator++; } while(path_iterator < path_end && !IS_SLASH(*path_iterator)); /* If we're past the end of the string and it didnt't end with a slash, * then we're a root path. */ return path_iterator >= path_end && !IS_SLASH(path_start[length - 1]); }
posix_errno_t efile_del_dir(const efile_path_t *path) { ASSERT_PATH_FORMAT(path); if(!RemoveDirectoryW((WCHAR*)path->data)) { DWORD last_error = GetLastError(); if(last_error == ERROR_DIRECTORY) { return ENOTDIR; } return windows_to_posix_errno(last_error); } return 0; }
posix_errno_t efile_set_cwd(const efile_path_t *path) { const WCHAR *path_start; ASSERT_PATH_FORMAT(path); /* We have to use _wchdir since that's the only function that updates the * per-drive working directory, but it naively assumes that all paths * starting with \\ are UNC paths, so we have to skip the \\?\-prefix. */ path_start = (WCHAR*)path->data + LP_PREFIX_LENGTH; if(_wchdir(path_start)) { return windows_to_posix_errno(GetLastError()); } return 0; }
static posix_errno_t internal_read_link(const efile_path_t *path, efile_path_t *result) { DWORD required_length, actual_length; HANDLE link_handle; DWORD last_error; link_handle = CreateFileW((WCHAR*)path->data, GENERIC_READ, FILE_SHARE_FLAGS, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); last_error = GetLastError(); if(link_handle == INVALID_HANDLE_VALUE) { return windows_to_posix_errno(last_error); } required_length = GetFinalPathNameByHandleW(link_handle, NULL, 0, 0); last_error = GetLastError(); if(required_length <= 0) { CloseHandle(link_handle); return windows_to_posix_errno(last_error); } /* Unlike many other path functions (eg. GetFullPathNameW), this one * includes the NUL terminator in its required length. */ if(!enif_alloc_binary(required_length * sizeof(WCHAR), result)) { CloseHandle(link_handle); return ENOMEM; } actual_length = GetFinalPathNameByHandleW(link_handle, (WCHAR*)result->data, required_length, 0); last_error = GetLastError(); CloseHandle(link_handle); if(actual_length == 0 || actual_length >= required_length) { enif_release_binary(result); return windows_to_posix_errno(last_error); } /* GetFinalPathNameByHandle always prepends with "\\?\" and NUL-terminates, * so we never have to touch-up the resulting path. */ ASSERT_PATH_FORMAT(result); return 0; }
static int get_drive_number(const efile_path_t *path) { const WCHAR *path_start; int length; ASSERT_PATH_FORMAT(path); path_start = (WCHAR*)path->data + LP_PREFIX_LENGTH; length = PATH_LENGTH(path) - LP_PREFIX_LENGTH; if(length >= 2 && path_start[1] == L':') { WCHAR drive_letter = path_start[0]; if(drive_letter >= L'A' && drive_letter <= L'Z') { return drive_letter - L'A' + 1; } else if(drive_letter >= L'a' && drive_letter <= L'z') { return drive_letter - L'a' + 1; } } return -1; }
posix_errno_t efile_del_file(const efile_path_t *path) { ASSERT_PATH_FORMAT(path); if(!DeleteFileW((WCHAR*)path->data)) { DWORD last_error = GetLastError(); switch(last_error) { case ERROR_INVALID_NAME: /* Attempted to delete a device or similar. */ return EACCES; case ERROR_ACCESS_DENIED: /* Windows NT reports removing a directory as EACCES instead of * EPERM. */ if(has_file_attributes(path, FILE_ATTRIBUTE_DIRECTORY)) { return EPERM; } break; } return windows_to_posix_errno(last_error); } return 0; }
posix_errno_t efile_open(const efile_path_t *path, enum efile_modes_t modes, ErlNifResourceType *nif_type, efile_data_t **d) { DWORD attributes, access_flags, open_mode; HANDLE handle; ASSERT_PATH_FORMAT(path); access_flags = 0; open_mode = 0; if(modes & EFILE_MODE_READ && !(modes & EFILE_MODE_WRITE)) { access_flags = GENERIC_READ; open_mode = OPEN_EXISTING; } else if(modes & EFILE_MODE_WRITE && !(modes & EFILE_MODE_READ)) { access_flags = GENERIC_WRITE; open_mode = CREATE_ALWAYS; } else if(modes & EFILE_MODE_READ_WRITE) { access_flags = GENERIC_READ | GENERIC_WRITE; open_mode = OPEN_ALWAYS; } else { return EINVAL; } if(modes & EFILE_MODE_APPEND) { access_flags |= FILE_APPEND_DATA; open_mode = OPEN_ALWAYS; } if(modes & EFILE_MODE_EXCLUSIVE) { open_mode = CREATE_NEW; } if(modes & EFILE_MODE_SYNC) { attributes = FILE_FLAG_WRITE_THROUGH; } else { attributes = FILE_ATTRIBUTE_NORMAL; } handle = CreateFileW((WCHAR*)path->data, access_flags, FILE_SHARE_FLAGS, NULL, open_mode, attributes, NULL); if(handle != INVALID_HANDLE_VALUE) { efile_win_t *w; w = (efile_win_t*)enif_alloc_resource(nif_type, sizeof(efile_win_t)); w->handle = handle; EFILE_INIT_RESOURCE(&w->common, modes); (*d) = &w->common; return 0; } else { DWORD last_error = GetLastError(); /* Rewrite all failures on directories to EISDIR to match the old * driver. */ if(has_file_attributes(path, FILE_ATTRIBUTE_DIRECTORY)) { return EISDIR; } return windows_to_posix_errno(last_error); } }
posix_errno_t efile_rename(const efile_path_t *old_path, const efile_path_t *new_path) { BOOL old_is_directory, new_is_directory; DWORD move_flags, last_error; ASSERT_PATH_FORMAT(old_path); ASSERT_PATH_FORMAT(new_path); move_flags = MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH; if(MoveFileExW((WCHAR*)old_path->data, (WCHAR*)new_path->data, move_flags)) { return 0; } last_error = GetLastError(); old_is_directory = has_file_attributes(old_path, FILE_ATTRIBUTE_DIRECTORY); new_is_directory = has_file_attributes(new_path, FILE_ATTRIBUTE_DIRECTORY); switch(last_error) { case ERROR_SHARING_VIOLATION: case ERROR_ACCESS_DENIED: if(old_is_directory) { BOOL moved_into_itself; moved_into_itself = (old_path->size <= new_path->size) && !_wcsnicmp((WCHAR*)old_path->data, (WCHAR*)new_path->data, PATH_LENGTH(old_path)); if(moved_into_itself) { return EINVAL; } else if(is_path_root(old_path)) { return EINVAL; } /* Renaming a directory across volumes needs to be rewritten as * EXDEV so that the caller can respond by simulating it with * copy/delete operations. * * Files are handled through MOVEFILE_COPY_ALLOWED. */ if(!has_same_mount_point(old_path, new_path)) { return EXDEV; } } break; case ERROR_PATH_NOT_FOUND: case ERROR_FILE_NOT_FOUND: return ENOENT; case ERROR_ALREADY_EXISTS: case ERROR_FILE_EXISTS: if(old_is_directory && !new_is_directory) { return ENOTDIR; } else if(!old_is_directory && new_is_directory) { return EISDIR; } else if(old_is_directory && new_is_directory) { /* This will fail if the destination isn't empty. */ if(RemoveDirectoryW((WCHAR*)new_path->data)) { return efile_rename(old_path, new_path); } return EEXIST; } else if(!old_is_directory && !new_is_directory) { /* This is pretty iffy; the public documentation says that the * operation may EACCES on some systems when either file is open, * which gives us room to use MOVEFILE_REPLACE_EXISTING and be done * with it, but the old implementation simulated Unix semantics and * there's a lot of code that relies on that. * * The simulation renames the destination to a scratch name to get * around the fact that it's impossible to open (and by extension * rename) a file that's been deleted while open. It has a few * drawbacks though; * * 1) It's not atomic as there's a small window where there's no * file at all on the destination path. * 2) It will confuse applications that subscribe to folder * changes. * 3) It will fail if we lack general permission to write in the * same folder. */ WCHAR *swap_path = enif_alloc(new_path->size + sizeof(WCHAR) * 64); if(swap_path == NULL) { return ENOMEM; } else { static LONGLONG unique_counter = 0; WCHAR *swap_path_end; /* We swap in the same folder as the destination to be * reasonably sure that it's on the same volume. Note that * we're avoiding GetTempFileNameW as it will fail on long * paths. */ sys_memcpy(swap_path, (WCHAR*)new_path->data, new_path->size); swap_path_end = swap_path + PATH_LENGTH(new_path); while(!IS_SLASH(*swap_path_end)) { ASSERT(swap_path_end > swap_path); swap_path_end--; } StringCchPrintfW(&swap_path_end[1], 64, L"erl-%lx-%llx.tmp", GetCurrentProcessId(), unique_counter); InterlockedIncrement64(&unique_counter); } if(MoveFileExW((WCHAR*)new_path->data, swap_path, MOVEFILE_REPLACE_EXISTING)) { if(MoveFileExW((WCHAR*)old_path->data, (WCHAR*)new_path->data, move_flags)) { last_error = ERROR_SUCCESS; DeleteFileW(swap_path); } else { last_error = GetLastError(); MoveFileW(swap_path, (WCHAR*)new_path->data); } } else { last_error = GetLastError(); DeleteFileW(swap_path); } enif_free(swap_path); return windows_to_posix_errno(last_error); } return EEXIST; } return windows_to_posix_errno(last_error); }