Fix padded Wine review strings
This commit is contained in:
1 parent
a8cb350467
commit
f40e471938
7 files changed
+416
-33
No files matched your search
+167
-31
@@ -3,6 +3,7 @@
|
||||
#include "control_class.h"
|
||||
#include "native_query.h"
|
||||
#include "review_x11.h"
|
||||
#include "review_query.h"
|
||||
#include <commctrl.h>
|
||||
#include <cstring>
|
||||
#include <oleacc.h>
|
||||
@@ -11,6 +12,7 @@
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <unordered_set>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace {
|
||||
// DF_WINE_VIRTUAL_DESKTOP from Wine's ntuser.h (that internal C header is not
|
||||
@@ -29,6 +31,15 @@ std::string utf8(const WCHAR *text, size_t length) {
|
||||
static_cast<int>(length), value.data(), size, nullptr, nullptr);
|
||||
return value;
|
||||
}
|
||||
std::string accessible_text(const WCHAR *text, size_t length) {
|
||||
// Some MSAA BSTRs retain the window's reported length past its text's
|
||||
// terminating NUL. D-Bus cannot represent those bytes, and the padding
|
||||
// need not contain text. Keep only the terminated label for display;
|
||||
// preserve the original UTF-16 separately for the stale check. Truncate
|
||||
// before conversion: padding may contain malformed surrogate pairs.
|
||||
const auto end=std::find(text,text+length,L'\0');
|
||||
return utf8(text,end-text);
|
||||
}
|
||||
bool query(HWND window, UINT message, WPARAM wp, LPARAM lp, uint64_t deadline,
|
||||
DWORD_PTR &result) {
|
||||
uint64_t now = GetTickCount64();
|
||||
@@ -104,17 +115,12 @@ bool edit_layout(HWND window, uint64_t deadline, EditLayout &layout) {
|
||||
layout.style=GetWindowLongPtrW(window,GWL_STYLE);
|
||||
layout.horizontal=GetScrollPos(window,SB_HORZ);
|
||||
layout.vertical=GetScrollPos(window,SB_VERT);
|
||||
for (auto message:{EM_GETRECT,EM_GETFIRSTVISIBLELINE,EM_GETLINECOUNT}) {
|
||||
LPARAM lp=message == EM_GETRECT ? reinterpret_cast<LPARAM>(&layout.format) : 0;
|
||||
DWORD_PTR *result=message == EM_GETFIRSTVISIBLELINE ? &layout.first :
|
||||
message == EM_GETLINECOUNT ? &layout.lines : &answer;
|
||||
if (!query(window,message,0,lp,deadline,*result)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Wine reports failure when marshalling a null WM_GETFONT result. Null is
|
||||
// the native default font; preserve the optional query used by Edit capture.
|
||||
query(window,WM_GETFONT,0,0,deadline,layout.font);
|
||||
if (!query(window, EM_GETRECT, 0, reinterpret_cast<LPARAM>(&layout.format), deadline, answer)) return false;
|
||||
std::vector<ReviewQuery> fields{{EM_GETFIRSTVISIBLELINE,0},{EM_GETLINECOUNT,0},{WM_GETFONT,0}};
|
||||
if (!review_query_many(window, fields, deadline)) return false;
|
||||
layout.first = fields[0].answer;
|
||||
layout.lines = fields[1].answer;
|
||||
layout.font = fields[2].answer;
|
||||
return GetTickCount64() < deadline;
|
||||
}
|
||||
bool same_layout(const EditLayout &a, const EditLayout &b) {
|
||||
@@ -129,11 +135,13 @@ struct SourceRead {
|
||||
struct AccessibleRead {
|
||||
IAccessible *object;
|
||||
LONG child;
|
||||
std::string name, value;
|
||||
std::vector<WCHAR> name, value;
|
||||
bool read_value;
|
||||
RECT bounds;
|
||||
};
|
||||
struct ListRead { HWND window; size_t index; std::vector<WCHAR> text; RECT bounds; int column = -1; };
|
||||
struct MdiRead { HWND window, active; RECT bounds; };
|
||||
struct CaptionRead { HWND window; std::string text; RECT bounds; };
|
||||
struct ReviewItem32 {
|
||||
uint32_t mask; int32_t item, subitem; uint32_t state, state_mask, text; int32_t capacity;
|
||||
};
|
||||
@@ -152,6 +160,8 @@ struct Capture {
|
||||
std::vector<SourceRead> reads;
|
||||
std::vector<AccessibleRead> accessible_reads;
|
||||
std::vector<ListRead> list_reads;
|
||||
std::vector<MdiRead> mdi_reads;
|
||||
std::vector<CaptionRead> caption_reads;
|
||||
std::unordered_set<HWND> native;
|
||||
size_t visited = 0;
|
||||
bool failed = false;
|
||||
@@ -189,9 +199,13 @@ struct Capture {
|
||||
EditLayout layout{};
|
||||
if (!edit_layout(window,deadline,layout)) { fail("unavailable"); return false; }
|
||||
std::vector<std::pair<size_t,DWORD_PTR>> positions;
|
||||
std::unordered_map<size_t,DWORD_PTR> position_cache;
|
||||
auto position_at = [&](size_t index, DWORD_PTR &position) {
|
||||
auto cached = position_cache.find(index);
|
||||
if (cached != position_cache.end()) { position = cached->second; return true; }
|
||||
if (!query(window,EM_POSFROMCHAR,index,0,deadline,position) ||
|
||||
position == static_cast<DWORD_PTR>(-1)) return false;
|
||||
position_cache.emplace(index,position);
|
||||
positions.emplace_back(index,position);
|
||||
return true;
|
||||
};
|
||||
@@ -235,11 +249,63 @@ struct Capture {
|
||||
bool good=true;
|
||||
const bool multiline = GetWindowLongPtrW(window, GWL_STYLE) & ES_MULTILINE;
|
||||
if (!multiline) first = 0; // Single-line message returns character index.
|
||||
const size_t native_first = first;
|
||||
// Queue scalar lookups together so a frame-based application services a
|
||||
// batch in one turn instead of sleeping between every character query.
|
||||
// Skip rows above a parent clip using the standard Edit's fixed line
|
||||
// height. Keep one extra row for a partly scrolled first line, and retain
|
||||
// native positions below to check which rows are actually fully visible.
|
||||
const LONG line_height = std::max<LONG>(1, metrics.tmHeight);
|
||||
const int64_t hidden_height = static_cast<int64_t>(visible.top)-origin.y-format.top;
|
||||
const size_t skipped = multiline && hidden_height > line_height ?
|
||||
std::min<size_t>(lines > first ? lines-first : 0, hidden_height/line_height-1) : 0;
|
||||
first += skipped;
|
||||
size_t wanted_rows=static_cast<size_t>(std::max<int64_t>(1,
|
||||
static_cast<int64_t>(visible.bottom)-origin.y-format.top)) /
|
||||
line_height+2;
|
||||
wanted_rows = wanted_rows > skipped ? wanted_rows-skipped : 0;
|
||||
if (wanted_rows > 258 && lines > first && lines-first > 258) {
|
||||
if (old) SelectObject(dc, old);
|
||||
ReleaseDC(window, dc);
|
||||
fail("limited"); return false;
|
||||
}
|
||||
size_t row_count=std::min<size_t>(lines>first ? lines-first:0,wanted_rows);
|
||||
std::vector<ReviewQuery> indices,details,last_positions;
|
||||
for (size_t row = 0; row < row_count; ++row) indices.push_back({EM_LINEINDEX, first+row});
|
||||
good = review_query_many(window, indices, deadline);
|
||||
if (good) for (const auto &index : indices) {
|
||||
if (index.answer > text.size()) { good = false; break; }
|
||||
details.push_back({EM_LINELENGTH, index.answer});
|
||||
if (index.answer < text.size()) details.push_back({EM_POSFROMCHAR, index.answer});
|
||||
}
|
||||
if (good) good = review_query_many(window, details, deadline);
|
||||
std::vector<DWORD_PTR> lengths;
|
||||
if (good) {
|
||||
size_t at = 0;
|
||||
for (const auto &index : indices) {
|
||||
auto length = details[at++].answer;
|
||||
lengths.push_back(length);
|
||||
if (length > text.size()-index.answer) { good = false; break; }
|
||||
if (index.answer < text.size()) {
|
||||
auto position = details[at++].answer;
|
||||
position_cache.emplace(index.answer, position);
|
||||
positions.emplace_back(index.answer, position);
|
||||
if (static_cast<short>(HIWORD(position))+origin.y >= visible.bottom) continue;
|
||||
}
|
||||
if (length) {
|
||||
size_t last = index.answer+length-1;
|
||||
if (offsets[last] == UINT64_MAX) --last;
|
||||
last_positions.push_back({EM_POSFROMCHAR, last});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (good) good = review_query_many(window, last_positions, deadline);
|
||||
if (good) for (const auto &position : last_positions)
|
||||
if (position_cache.emplace(position.index, position.answer).second)
|
||||
positions.emplace_back(position.index, position.answer);
|
||||
size_t captured = 0;
|
||||
for (size_t line = first; good && line < lines && !failed; ++line) {
|
||||
DWORD_PTR index = 0, length = 0, position = 0;
|
||||
good = query(window, EM_LINEINDEX, line, 0, deadline, index) &&
|
||||
query(window, EM_LINELENGTH, index, 0, deadline, length);
|
||||
for (size_t line = first; good && line-first < indices.size() && !failed; ++line) {
|
||||
DWORD_PTR index = indices[line-first].answer, length = lengths[line-first], position = 0;
|
||||
if (!good || index > text.size() || length > text.size() - index) { fail("stale"); break; }
|
||||
// EM_POSFROMCHAR has no position for an empty final row. Its visual
|
||||
// line geometry follows the control's actual font and format rectangle.
|
||||
@@ -247,7 +313,7 @@ struct Capture {
|
||||
good=position_at(index,position);
|
||||
if (!good) { fail("unavailable"); break; }
|
||||
} else {
|
||||
position = MAKELONG(format.left, format.top + (line - first) * metrics.tmHeight);
|
||||
position = MAKELONG(format.left, format.top + (line - native_first) * metrics.tmHeight);
|
||||
}
|
||||
LONG x = static_cast<short>(LOWORD(position)) + origin.x;
|
||||
LONG y = static_cast<short>(HIWORD(position)) + origin.y;
|
||||
@@ -255,6 +321,9 @@ struct Capture {
|
||||
SIZE extent{};
|
||||
if (length) GetTextExtentPoint32W(dc, text.data() + index, static_cast<int>(length), &extent);
|
||||
RECT bounds{x, y, x + std::max<LONG>(1, extent.cx), y + metrics.tmHeight};
|
||||
// Horizontal trimming cannot make a vertically clipped row visible.
|
||||
// Avoid round trips for characters that will be excluded regardless.
|
||||
if (bounds.top < visible.top || bounds.bottom > visible.bottom) continue;
|
||||
size_t finish = index + length;
|
||||
if (length) {
|
||||
size_t last=finish-1;
|
||||
@@ -487,11 +556,14 @@ struct Capture {
|
||||
// Do not ask a protected provider for either its name or its value.
|
||||
if (!node.protected_text) {
|
||||
BSTR name = nullptr, value = nullptr;
|
||||
std::string raw_name, raw_value;
|
||||
std::vector<WCHAR> raw_name, raw_value;
|
||||
object->get_accName(child, &name);
|
||||
if (name) {
|
||||
if (SysStringLen(name) > 4096) fail("limited");
|
||||
else raw_name = node.name = utf8(name, SysStringLen(name));
|
||||
else {
|
||||
raw_name.assign(name,name+SysStringLen(name));
|
||||
node.name = accessible_text(name,SysStringLen(name));
|
||||
}
|
||||
SysFreeString(name);
|
||||
}
|
||||
std::string text = node.name;
|
||||
@@ -499,7 +571,10 @@ struct Capture {
|
||||
object->get_accValue(child, &value);
|
||||
if (value) {
|
||||
if (SysStringLen(value) > 4096) fail("limited");
|
||||
else raw_value = text = utf8(value, SysStringLen(value));
|
||||
else {
|
||||
raw_value.assign(value,value+SysStringLen(value));
|
||||
text = accessible_text(value,SysStringLen(value));
|
||||
}
|
||||
SysFreeString(value);
|
||||
}
|
||||
}
|
||||
@@ -587,7 +662,32 @@ struct Capture {
|
||||
}
|
||||
if (!is_edit && !button && !label && !list_box && !list_view) {
|
||||
IAccessible *object = nullptr;
|
||||
if (SUCCEEDED(AccessibleObjectFromWindow(window, OBJID_CLIENT, accessible_iid, reinterpret_cast<void**>(&object)))) {
|
||||
DWORD_PTR response=0;
|
||||
// A real provider supplies its marshalled object via WM_GETOBJECT.
|
||||
// Synthesized container trees only repeat the native child hierarchy
|
||||
// already visited here. Keep the standard fallback for native leaves.
|
||||
HRESULT hr=E_FAIL;
|
||||
// Match AccessibleObjectFromWindow's actual message completion: a late
|
||||
// custom reply still owns a marshalled object that must be consumed.
|
||||
bool timely=remote_query(window,WM_GETOBJECT,0xffffffff,OBJID_CLIENT,deadline,response);
|
||||
if (response) hr = ObjectFromLresult(static_cast<LRESULT>(response), accessible_iid, 0, reinterpret_cast<void **>(&object));
|
||||
else if (timely && !GetWindow(window, GW_CHILD))
|
||||
hr = CreateStdAccessibleObject(window, OBJID_CLIENT, accessible_iid, reinterpret_cast<void **>(&object));
|
||||
if (!timely) {
|
||||
if (object) object->Release();
|
||||
fail("unavailable"); return;
|
||||
}
|
||||
if (!response && GetWindow(window, GW_CHILD) &&
|
||||
(GetWindowLongPtrW(window, GWL_STYLE) & WS_CAPTION) == WS_CAPTION && inside(bounds, clip)) {
|
||||
// Preserve painted MDI titles only for the native fallback. A real
|
||||
// provider retains authority over its own names and protection state.
|
||||
ReviewNode title;
|
||||
title.id = id + "_caption"; title.parent = id; title.role = "window";
|
||||
title.name = caption(window); title.bounds = clipped;
|
||||
caption_reads.push_back({window, title.name, bounds});
|
||||
if (!add(std::move(title))) return;
|
||||
}
|
||||
if (SUCCEEDED(hr) && object) {
|
||||
VARIANT self{}; self.vt = VT_I4; self.lVal = CHILDID_SELF;
|
||||
accessible(object, self, id + "_a", id, clipped, depth + 1);
|
||||
object->Release();
|
||||
@@ -596,6 +696,19 @@ struct Capture {
|
||||
RECT client{}; GetClientRect(window, &client);
|
||||
POINT point{0,0}; ClientToScreen(window, &point); OffsetRect(&client, point.x, point.y);
|
||||
if (!IntersectRect(&client, &client, &clipped)) return;
|
||||
if (wine_access_class_matches(cls, L"MDIClient")) {
|
||||
DWORD_PTR answer = 0;
|
||||
if (!query(window, WM_MDIGETACTIVE, 0, 0, deadline, answer)) { fail("unavailable"); return; }
|
||||
HWND active = reinterpret_cast<HWND>(answer);
|
||||
RECT active_bounds{};
|
||||
if (active && (GetParent(active) != window || !GetWindowRect(active, &active_bounds))) { fail("stale"); return; }
|
||||
mdi_reads.push_back({window, active, active_bounds});
|
||||
// A maximized foreground document covers its MDI siblings even when
|
||||
// their native WS_VISIBLE flags remain set. Do not read those copies.
|
||||
if (active && IsWindowVisible(active) && inside(client, active_bounds)) {
|
||||
this->window(active, id, client, depth+1); return;
|
||||
}
|
||||
}
|
||||
for (HWND child = GetWindow(window, GW_CHILD); child && !failed; child = GetWindow(child, GW_HWNDNEXT))
|
||||
this->window(child, id, client, depth + 1);
|
||||
}
|
||||
@@ -672,7 +785,7 @@ void ReviewIdentityStore::retire() {
|
||||
state_->identities.clear(); state_->window.clear(); state_->focus.clear(); ++state_->revision;
|
||||
}
|
||||
ReviewCapture capture_review_window(ReviewIdentityStore &store, const ReviewFocus &expected, uint64_t deadline) {
|
||||
Capture capture{store, {}, deadline, 0, 0, 512, {}, {}, {}, {}, 0, false};
|
||||
Capture capture{store, {}, deadline, 0, 0, 512, {}, {}, {}, {}, {}, {}, 0, false};
|
||||
capture.result.source = expected;
|
||||
auto initial = store.observe_foreground();
|
||||
if (expected.window.empty() || !equal_focus(expected, initial)) {
|
||||
@@ -698,21 +811,30 @@ ReviewCapture capture_review_window(ReviewIdentityStore &store, const ReviewFocu
|
||||
// would miss an equal-length replacement and permit a mixed snapshot.
|
||||
for (const auto &read : capture.reads) {
|
||||
if (capture.failed) break;
|
||||
std::vector<WCHAR> current; bool limited = false;
|
||||
std::vector<WCHAR> current;
|
||||
RECT bounds{};
|
||||
if (password(read.window) || !IsWindowVisible(read.window) || !GetWindowRect(read.window, &bounds) ||
|
||||
!EqualRect(&read.bounds, &bounds)) { capture.fail("stale"); break; }
|
||||
if (read.text.size() > max_source - capture.source_units) { capture.fail("limited"); break; }
|
||||
capture.source_units += read.text.size();
|
||||
if (!read_text(read.window, deadline, read.text.size(), current, limited) || current != read.text) capture.fail("stale");
|
||||
// The first read already supplies a bounded capacity. Copy once, then
|
||||
// verify length, content and protection without another preliminary
|
||||
// length round trip (growth must still reject a truncated old prefix).
|
||||
current.assign(read.text.size()+1,0);
|
||||
DWORD_PTR copied=0,length=0;
|
||||
if(!query(read.window,WM_GETTEXT,current.size(),reinterpret_cast<LPARAM>(current.data()),deadline,copied) ||
|
||||
password(read.window) || !query(read.window,WM_GETTEXTLENGTH,0,0,deadline,length) ||
|
||||
password(read.window) || copied!=read.text.size() || length!=read.text.size())capture.fail("stale");
|
||||
current.resize(read.text.size());
|
||||
if(current!=read.text)capture.fail("stale");
|
||||
if (read.layout && !capture.failed) {
|
||||
EditLayout layout{};
|
||||
if (!edit_layout(read.window,deadline,layout) || !same_layout(*read.layout,layout)) capture.fail("stale");
|
||||
for (const auto &[index,expected_position]:read.positions) {
|
||||
if (capture.failed) break;
|
||||
DWORD_PTR position=0;
|
||||
if (!query(read.window,EM_POSFROMCHAR,index,0,deadline,position) || position != expected_position) capture.fail("stale");
|
||||
}
|
||||
std::vector<ReviewQuery> positions;
|
||||
for(const auto &[index,position]:read.positions)positions.push_back({EM_POSFROMCHAR,index});
|
||||
if(!capture.failed && !review_query_many(read.window,positions,deadline))capture.fail("stale");
|
||||
for(size_t i=0;!capture.failed && i<positions.size();++i)
|
||||
if(positions[i].answer!=read.positions[i].second)capture.fail("stale");
|
||||
}
|
||||
}
|
||||
for (const auto &read : capture.list_reads) {
|
||||
@@ -732,11 +854,11 @@ ReviewCapture capture_review_window(ReviewIdentityStore &store, const ReviewFocu
|
||||
VariantClear(&state); capture.fail("stale"); break;
|
||||
}
|
||||
VariantClear(&state);
|
||||
auto matches = [&](bool value, const std::string &expected_text) {
|
||||
auto matches = [&](bool value, const std::vector<WCHAR> &expected_text) {
|
||||
BSTR text = nullptr;
|
||||
HRESULT hr = value ? read.object->get_accValue(child, &text) : read.object->get_accName(child, &text);
|
||||
bool match = (SUCCEEDED(hr) || hr == S_FALSE) && (!text || SysStringLen(text) <= 4096) &&
|
||||
(text ? utf8(text, SysStringLen(text)) : std::string{}) == expected_text;
|
||||
(text ? std::equal(expected_text.begin(),expected_text.end(),text,text+SysStringLen(text)) : expected_text.empty());
|
||||
if (text) SysFreeString(text);
|
||||
return match;
|
||||
};
|
||||
@@ -746,6 +868,20 @@ ReviewCapture capture_review_window(ReviewIdentityStore &store, const ReviewFocu
|
||||
x != read.bounds.left || y != read.bounds.top ||
|
||||
static_cast<int64_t>(x)+width != read.bounds.right || static_cast<int64_t>(y)+height != read.bounds.bottom) capture.fail("stale");
|
||||
}
|
||||
for(const auto &read:capture.mdi_reads) {
|
||||
if(capture.failed)break;
|
||||
DWORD_PTR active=0;RECT bounds{};
|
||||
if(!query(read.window,WM_MDIGETACTIVE,0,0,deadline,active) || reinterpret_cast<HWND>(active)!=read.active ||
|
||||
(read.active && (!GetWindowRect(read.active,&bounds) || !EqualRect(&bounds,&read.bounds))))capture.fail("stale");
|
||||
}
|
||||
for (const auto &read : capture.caption_reads) {
|
||||
if (capture.failed) break;
|
||||
RECT bounds{};
|
||||
if (!IsWindowVisible(read.window) || !GetWindowRect(read.window, &bounds) ||
|
||||
!EqualRect(&bounds, &read.bounds) ||
|
||||
(GetWindowLongPtrW(read.window, GWL_STYLE) & WS_CAPTION) != WS_CAPTION ||
|
||||
caption(read.window) != read.text) capture.fail("stale");
|
||||
}
|
||||
if (!capture.failed && !equal_focus(expected, store.observe_foreground())) capture.fail("stale");
|
||||
if (!capture.failed && physical_geometry) {
|
||||
POINT current_origin{};
|
||||
|
||||
Reference in new issue
Block a user