// SPDX-License-Identifier: LGPL-2.1-or-later #include "list_view.h" #include "control_class.h" #include "native_query.h" #include #include #include #include #include namespace { using native_control::query; // LVM_GETITEMTEXT is not marshalled by USER32. The structure and text buffer // must live in the target process, with pointers matching its architecture. struct Item32 { uint32_t mask; int32_t item, subItem; uint32_t state, stateMask, text; int32_t capacity; }; struct Item64 { uint32_t mask; int32_t item, subItem; uint32_t state, stateMask, padding; uint64_t text; int32_t capacity; uint32_t padding2; }; static_assert(offsetof(Item32, text) == 20); static_assert(offsetof(Item64, text) == 24); constexpr size_t textOffset = 128; constexpr size_t textCapacity = 1024; constexpr size_t bufferSize = textOffset + textCapacity * sizeof(wchar_t); } bool is_list_view(HWND window) { wchar_t className[64] = {}; return GetClassNameW(window, className, std::size(className)) && wine_access_class_matches(className, L"SysListView32"); } int list_view_index(HWND window, int *count) { DWORD_PTR result = 0; *count = 0; if (!query(window, LVM_GETITEMCOUNT, 0, 0, &result) || static_cast(result) <= 0) return -1; *count = static_cast(result); if (query(window, LVM_GETNEXTITEM, static_cast(-1), LVNI_FOCUSED, &result) && static_cast(result) >= 0) return static_cast(result); if (query(window, LVM_GETNEXTITEM, static_cast(-1), LVNI_SELECTED, &result) && static_cast(result) >= 0) return static_cast(result); // An installer can focus a populated list without selecting any row. return 0; } ListViewRow read_list_view(HWND window) { ListViewRow row; row.index = list_view_index(window, &row.count); if (row.index < 0 || row.index >= row.count) return row; DWORD_PTR state = 0; if (query(window, LVM_GETITEMSTATE, row.index, LVIS_SELECTED, &state)) row.selected = state & LVIS_SELECTED; row.checked = list_view_checked(window, row.index); native_control::RemoteBuffer remote(window, bufferSize); if (!remote) return row; const bool wow64 = remote.is32Bit(); DWORD_PTR header = 0, columnCount = 1; if (query(window, LVM_GETHEADER, 0, 0, &header) && header != 0) { DWORD_PTR columns = 0; if (query(reinterpret_cast(header), HDM_GETITEMCOUNT, 0, 0, &columns) && columns > 0) columnCount = columns; } // Keep provider work and the protocol payload bounded even for huge tables. if (columnCount > 8) columnCount = 8; for (DWORD_PTR column = 0; column < columnCount; ++column) { BYTE buffer[bufferSize] = {}; const auto textAddress = remote.address(textOffset); if (wow64) { Item32 item{LVIF_TEXT, row.index, static_cast(column), 0, 0, static_cast(textAddress), textCapacity}; memcpy(buffer, &item, sizeof(item)); } else { Item64 item{LVIF_TEXT, row.index, static_cast(column), 0, 0, 0, textAddress, textCapacity, 0}; memcpy(buffer, &item, sizeof(item)); } DWORD_PTR result = 0; wchar_t text[textCapacity] = {}; if (!remote.write(buffer, sizeof(buffer)) || !query(window, LVM_GETITEMTEXTW, row.index, static_cast(remote.address()), &result) || !remote.read(text, sizeof(text), textOffset)) break; text[textCapacity - 1] = L'\0'; if (!text[0]) continue; char utf8[textCapacity * 4] = {}; if (!WideCharToMultiByte(CP_UTF8, 0, text, -1, utf8, sizeof(utf8), nullptr, nullptr)) continue; if (!row.text.empty()) row.text += ", "; row.text += utf8; } return row; } int list_view_checked(HWND window, int index) { DWORD_PTR style = 0, state = 0; if (index < 0 || !query(window, LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0, &style) || !(style & LVS_EX_CHECKBOXES) || !query(window, LVM_GETITEMSTATE, index, LVIS_STATEIMAGEMASK, &state)) return -1; const DWORD_PTR image = (state & LVIS_STATEIMAGEMASK) >> 12; // Other state images are application-defined; do not invent a check state. return image == 1 ? 0 : image == 2 ? 1 : -1; }