quickwin 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.en.md +2 -1
  2. package/README.md +2 -1
  3. package/examples/PdfCanvas.js +3 -3
  4. package/examples/PdfCanvas.tsx +3 -3
  5. package/examples/pdf_preview2.js +34 -19
  6. package/examples/pdf_preview2.ts +34 -20
  7. package/examples/pdf_viewer.js +956 -215
  8. package/examples/pdf_viewer.tsx +29 -19
  9. package/examples/test_gallery.js +922 -194
  10. package/examples/test_react_lazy.js +905 -177
  11. package/examples/test_react_listbox.js +905 -177
  12. package/examples/test_root_window.js +905 -177
  13. package/examples/test_tab.js +905 -177
  14. package/lib/fetch.js +35 -7
  15. package/lib/fetch.ts +24 -4
  16. package/lib/ffi-bind.js +85 -0
  17. package/lib/ffi-bind.ts +170 -0
  18. package/lib/ffi-struct.js +224 -0
  19. package/lib/ffi-struct.ts +239 -0
  20. package/lib/react-qw/components/DateTimePicker.js +21 -15
  21. package/lib/react-qw/components/Link.js +4 -6
  22. package/lib/react-qw/components/ListView.js +84 -40
  23. package/lib/react-qw/components/PathPicker.js +40 -24
  24. package/lib/react-qw/components/Tab.js +21 -10
  25. package/lib/react-qw/components/Tooltip.js +27 -15
  26. package/lib/react-qw/components/TreeView.js +23 -33
  27. package/lib/react-qw/nmhdr.js +26 -0
  28. package/lib/react-qw/nmhdr.ts +32 -0
  29. package/lib/text-measure.js +14 -24
  30. package/lib/text-measure.ts +14 -24
  31. package/lib/win-common-structs.js +39 -0
  32. package/lib/win-common-structs.ts +41 -0
  33. package/main.js +46 -0
  34. package/package.json +2 -1
  35. package/quickwin.d.ts +7 -2
  36. package/qwin-nowasm-x86.exe +0 -0
  37. package/qwin-nowasm.exe +0 -0
  38. package/qwin-x86.exe +0 -0
  39. package/qwin.exe +0 -0
  40. package/test/run.js +3 -0
  41. package/test/run.ts +3 -0
  42. package/test/test_ffi.js +12 -8
  43. package/test/test_ffi.ts +12 -12
  44. package/test/test_ffi_bind.js +42 -0
  45. package/test/test_ffi_bind.ts +46 -0
  46. package/test/test_ffi_struct.js +201 -0
  47. package/test/test_ffi_struct.ts +223 -0
  48. package/test/test_net_fetch.js +62 -0
  49. package/test/test_net_fetch.ts +72 -0
  50. package/test/test_react_complex.js +908 -180
  51. package/test/test_react_counter.js +905 -177
  52. package/test/test_react_flex.js +905 -177
  53. package/test/test_react_hidden.js +907 -179
  54. package/test/test_react_listview_link.js +905 -177
  55. package/test/test_react_render.js +905 -177
  56. package/test/test_wolfssl_read.js +201 -0
  57. package/test/test_wolfssl_read.ts +181 -0
package/test/test_ffi.ts CHANGED
@@ -173,18 +173,18 @@ export const suite = {
173
173
  std.printf(' no printers installed (skip)\n')
174
174
  }
175
175
 
176
- t.section('pointer arg type check')
177
- let threw = false
178
- try {
179
- ffi.ffiCall(
180
- enumPrinters,
181
- [ffi.FFI_TYPE_POINTER] as any,
182
- [123] as any,
183
- ffi.FFI_TYPE_VOID
184
- )
185
- } catch (e) {
186
- threw = true
176
+ t.section('PTR accepts raw number handles (XP 32-bit fix)')
177
+ const hUser32 = win.LoadLibrary('user32.dll')
178
+ t.checkTrue('LoadLibrary("user32.dll") succeeds', hUser32 !== null)
179
+ if (hUser32) {
180
+ const getDC = win.GetProcAddress(hUser32, 'GetDC')
181
+ t.checkTrue('GetProcAddress("GetDC") succeeds', getDC !== null)
182
+ if (getDC) {
183
+ const screenDc = ffi.ffiCall(getDC, [ffi.FFI_TYPE_POINTER], [0], ffi.FFI_TYPE_POINTER)
184
+ t.checkTrue('PTR slot takes number (NULL hwnd) and returns a DC handle', screenDc !== 0 && screenDc !== null)
185
+ const bogus = ffi.ffiCall(getDC, [ffi.FFI_TYPE_POINTER], [0x12345678], ffi.FFI_TYPE_POINTER)
186
+ t.checkTrue('PTR slot takes non-zero number without throwing', bogus === 0 || bogus === null)
187
+ }
187
188
  }
188
- t.checkTrue('non-ArrayBuffer pointer arg throws TypeError', threw)
189
189
  }
190
190
  }
@@ -0,0 +1,42 @@
1
+ import * as std from 'std';
2
+ import { bind, bindLib } from '../lib/ffi-bind.js';
3
+ export const suite = {
4
+ name: 'ffi-bind',
5
+ run: (t) => {
6
+ t.section('bind single proc');
7
+ const getDC = bind('user32.dll', 'GetDC', 'ptr -> ptr');
8
+ const dc = getDC(0);
9
+ t.checkTrue('bind GetDC(NULL) returns screen DC', !!dc);
10
+ t.section('bindLib batch with wstr auto-encode');
11
+ const user32 = bindLib('user32.dll', {
12
+ DrawTextW: 'ptr wstr i32 ptr i32 -> i32',
13
+ ReleaseDC: 'ptr ptr -> i32',
14
+ });
15
+ const rect = new ArrayBuffer(16);
16
+ const dv = new DataView(rect);
17
+ dv.setInt32(8, 500, true);
18
+ const lines = user32.DrawTextW(dc, 'hello ffi-bind', -1, rect, 1024 /* gui.DrawTextFlag.CALCRECT */);
19
+ t.checkTrue('DrawTextW returns line count > 0', lines > 0);
20
+ t.checkTrue('measured width > 0', dv.getInt32(8, true) > 0);
21
+ t.checkTrue('measured height > 0', dv.getInt32(12, true) > 0);
22
+ if (dc)
23
+ user32.ReleaseDC(0, dc);
24
+ t.section('signedness read-back: i32 vs u32');
25
+ const lstrcmpI = bind('kernel32.dll', 'lstrcmpW', 'wstr wstr -> i32');
26
+ const lstrcmpU = bind('kernel32.dll', 'lstrcmpW', 'wstr wstr -> u32');
27
+ const setLastError = bind('kernel32.dll', 'SetLastError', 'u32 -> void');
28
+ const getErrU = bind('kernel32.dll', 'GetLastError', ' -> u32');
29
+ const getErrI = bind('kernel32.dll', 'GetLastError', ' -> i32');
30
+ const ri = lstrcmpI('a', 'b');
31
+ const ru = lstrcmpU('a', 'b');
32
+ std.printf(' lstrcmpW("a","b"): i32=%s u32=%s\n', String(ri), String(ru));
33
+ t.checkTrue('i32 read-back keeps negative sign', ri < 0);
34
+ t.check('u32 read-back is 0xFFFFFFFF', 4294967295, ru);
35
+ setLastError(0x80000000);
36
+ const glU = getErrU();
37
+ const glI = getErrI();
38
+ std.printf(' GetLastError after SetLastError(0x80000000): u32=%s i32=%s\n', String(glU), String(glI));
39
+ t.check('GetLastError u32 keeps big value', 2147483648, glU);
40
+ t.check('GetLastError i32 reads as negative', -2147483648, glI);
41
+ },
42
+ };
@@ -0,0 +1,46 @@
1
+ import * as std from 'std'
2
+ import * as gui from 'gui'
3
+ import { Tester } from './test_helper.js'
4
+ import { bind, bindLib } from '../lib/ffi-bind.js'
5
+
6
+ export const suite = {
7
+ name: 'ffi-bind',
8
+ run: (t: Tester) => {
9
+ t.section('bind single proc')
10
+ const getDC = bind('user32.dll', 'GetDC', 'ptr -> ptr')
11
+ const dc = getDC(0)
12
+ t.checkTrue('bind GetDC(NULL) returns screen DC', !!dc)
13
+
14
+ t.section('bindLib batch with wstr auto-encode')
15
+ const user32 = bindLib('user32.dll', {
16
+ DrawTextW: 'ptr wstr i32 ptr i32 -> i32',
17
+ ReleaseDC: 'ptr ptr -> i32',
18
+ })
19
+ const rect = new ArrayBuffer(16)
20
+ const dv = new DataView(rect)
21
+ dv.setInt32(8, 500, true)
22
+ const lines = user32.DrawTextW(dc, 'hello ffi-bind', -1, rect, gui.DrawTextFlag.CALCRECT)
23
+ t.checkTrue('DrawTextW returns line count > 0', lines > 0)
24
+ t.checkTrue('measured width > 0', dv.getInt32(8, true) > 0)
25
+ t.checkTrue('measured height > 0', dv.getInt32(12, true) > 0)
26
+ if (dc) user32.ReleaseDC(0, dc)
27
+
28
+ t.section('signedness read-back: i32 vs u32')
29
+ const lstrcmpI = bind('kernel32.dll', 'lstrcmpW', 'wstr wstr -> i32')
30
+ const lstrcmpU = bind('kernel32.dll', 'lstrcmpW', 'wstr wstr -> u32')
31
+ const setLastError = bind('kernel32.dll', 'SetLastError', 'u32 -> void')
32
+ const getErrU = bind('kernel32.dll', 'GetLastError', ' -> u32')
33
+ const getErrI = bind('kernel32.dll', 'GetLastError', ' -> i32')
34
+ const ri = lstrcmpI('a', 'b')
35
+ const ru = lstrcmpU('a', 'b')
36
+ std.printf(' lstrcmpW("a","b"): i32=%s u32=%s\n', String(ri), String(ru))
37
+ t.checkTrue('i32 read-back keeps negative sign', ri < 0)
38
+ t.check('u32 read-back is 0xFFFFFFFF', 4294967295, ru)
39
+ setLastError(0x80000000)
40
+ const glU = getErrU()
41
+ const glI = getErrI()
42
+ std.printf(' GetLastError after SetLastError(0x80000000): u32=%s i32=%s\n', String(glU), String(glI))
43
+ t.check('GetLastError u32 keeps big value', 2147483648, glU)
44
+ t.check('GetLastError i32 reads as negative', -2147483648, glI)
45
+ },
46
+ }
@@ -0,0 +1,201 @@
1
+ import * as os from 'os';
2
+ import { struct } from '../lib/ffi-struct.js';
3
+ import { bind } from '../lib/ffi-bind.js';
4
+ const RECT = struct({ left: 'i32', top: 'i32', right: 'i32', bottom: 'i32' });
5
+ const TVITEM = struct({
6
+ mask: 'u32',
7
+ hItem: 'ptr',
8
+ state: 'u32',
9
+ stateMask: 'u32',
10
+ pszText: 'ptr',
11
+ cchTextMax: 'i32',
12
+ iImage: 'i32',
13
+ iSelectedImage: 'i32',
14
+ cChildren: 'i32',
15
+ lParam: 'ptr',
16
+ });
17
+ const TVINSERTSTRUCT = struct({ hParent: 'ptr', hInsertAfter: 'ptr', item: TVITEM });
18
+ export const suite = {
19
+ name: 'ffi-struct',
20
+ run: (t) => {
21
+ const is64 = os.arch === 'x64';
22
+ t.section('RECT layout & roundtrip');
23
+ t.check('RECT.size == 16', 16, RECT.size);
24
+ const rbuf = RECT.write({ left: 10, top: 20, right: 30, bottom: 40 });
25
+ const r = RECT.read(rbuf);
26
+ t.check('left', 10, r.left);
27
+ t.check('top', 20, r.top);
28
+ t.check('right', 30, r.right);
29
+ t.check('bottom', 40, r.bottom);
30
+ t.check('offsetOf left', 0, RECT.offsetOf('left'));
31
+ t.check('offsetOf bottom', 12, RECT.offsetOf('bottom'));
32
+ t.section('GetWindowRect fills RECT buffer');
33
+ const getWindowRect = bind('user32.dll', 'GetWindowRect', 'ptr ptr -> int');
34
+ const getDesktopWindow = bind('user32.dll', 'GetDesktopWindow', ' -> ptr');
35
+ const hwnd = getDesktopWindow();
36
+ const wrect = new ArrayBuffer(16);
37
+ const ok = getWindowRect(hwnd, wrect);
38
+ t.checkTrue('GetWindowRect succeeds', ok !== 0);
39
+ const wr = RECT.read(wrect);
40
+ t.checkTrue('screen RECT non-empty', wr.right > 0 && wr.bottom > 0);
41
+ t.section('ptr layout matches arch');
42
+ t.check(`TVITEM.size (${os.arch})`, is64 ? 56 : 40, TVITEM.size);
43
+ t.check('TVITEM.offsetOf pszText', is64 ? 24 : 16, TVITEM.offsetOf('pszText'));
44
+ t.check('TVINSERTSTRUCT.size', is64 ? 72 : 48, TVINSERTSTRUCT.size);
45
+ t.check('TVINSERTSTRUCT.offsetOf item', is64 ? 16 : 8, TVINSERTSTRUCT.offsetOf('item'));
46
+ t.section('nested struct roundtrip');
47
+ const ins = TVINSERTSTRUCT.write({
48
+ hParent: 0x11111111,
49
+ hInsertAfter: 0x22222222,
50
+ item: {
51
+ mask: 1, hItem: 0x33333333, state: 0, stateMask: 0,
52
+ pszText: 0x44444444, cchTextMax: 100,
53
+ iImage: 0, iSelectedImage: 0, cChildren: 5, lParam: 0x55555555,
54
+ },
55
+ });
56
+ const got = TVINSERTSTRUCT.read(ins);
57
+ t.check('nested hParent', 0x11111111, got.hParent);
58
+ t.check('nested hInsertAfter', 0x22222222, got.hInsertAfter);
59
+ t.check('nested cChildren', 5, got.item.cChildren);
60
+ t.check('nested ptr pszText', 0x44444444, got.item.pszText);
61
+ t.section('C typedef aliases (int/DWORD/LPARAM/LONG_PTR/short)');
62
+ const M = struct({ n: 'int', d: 'DWORD', w: 'LPARAM', s: 'short', q: 'LONG_PTR' });
63
+ t.check(`aliased size (${os.arch})`, is64 ? 32 : 20, M.size);
64
+ const mb = M.write({ n: -5, d: 0xFFFFFFFF, w: 0xAABBCCDD, s: -7, q: 0x11223344 });
65
+ const m = M.read(mb);
66
+ t.check('int -5', -5, m.n);
67
+ t.check('DWORD', 0xFFFFFFFF, m.d);
68
+ t.check('LPARAM', 0xAABBCCDD, m.w);
69
+ t.check('short -7', -7, m.s);
70
+ t.check('LONG_PTR', 0x11223344, m.q);
71
+ t.section('wstr[8] roundtrip & truncation');
72
+ const W = struct({ name: 'wstr[8]' });
73
+ t.check('wstr[8].size == 16', 16, W.size);
74
+ const wb = W.write({ name: 'hello' });
75
+ t.check('read back "hello"', 'hello', W.read(wb).name);
76
+ const lb = W.write({ name: 'a very long string over' });
77
+ t.check('truncated to 7 chars', 'a very ', W.read(lb).name);
78
+ t.section('numeric arrays roundtrip');
79
+ const A = struct({ v: 'u16[4]', k: 'i32[2]' });
80
+ t.check('size == 16', 16, A.size);
81
+ const ab = A.write({ v: [1, 2, 3, 4], k: [-1, 300000] });
82
+ const ad = A.read(ab);
83
+ t.check('u16[0]', 1, ad.v[0]);
84
+ t.check('u16[3]', 4, ad.v[3]);
85
+ t.check('i32[0] -1', -1, ad.k[0]);
86
+ t.check('i32[1]', 300000, ad.k[1]);
87
+ t.section('f32/f64 layout (MSVC: f@0, double@8)');
88
+ const FL = struct({ f: 'float', d: 'double' });
89
+ t.check('size == 16 (f@0, double aligned @8)', 16, FL.size);
90
+ const fb = FL.write({ f: 1.5, d: -2.25 });
91
+ const fr = FL.read(fb);
92
+ t.check('float', 1.5, fr.f);
93
+ t.check('double', -2.25, fr.d);
94
+ t.section('comctl dialog structs track 32/64-bit ABI');
95
+ const TCITEMW = struct({
96
+ mask: 'u32', dwState: 'u32', dwStateMask: 'u32',
97
+ pszText: 'ptr', cchTextMax: 'i32', iImage: 'i32', lParam: 'ptr',
98
+ });
99
+ t.check('TCITEMW.size', is64 ? 40 : 28, TCITEMW.size);
100
+ t.check('TCITEMW.offsetOf pszText', is64 ? 16 : 12, TCITEMW.offsetOf('pszText'));
101
+ t.check('TCITEMW.offsetOf cchTextMax', is64 ? 24 : 16, TCITEMW.offsetOf('cchTextMax'));
102
+ const TTTOOLINFOW = struct({
103
+ cbSize: 'u32', uFlags: 'u32', hwnd: 'ptr', uId: 'ptr',
104
+ rect: 'i32[4]', hinst: 'ptr', lpszText: 'ptr', lParam: 'ptr',
105
+ lpReserved: 'ptr', // WinXP+ 追加
106
+ });
107
+ t.check('TTTOOLINFOW.size', is64 ? 72 : 48, TTTOOLINFOW.size);
108
+ t.check('TTTOOLINFOW.offsetOf lpszText', is64 ? 48 : 36, TTTOOLINFOW.offsetOf('lpszText'));
109
+ const OPENFILENAMEW = struct({
110
+ lStructSize: 'u32', hwndOwner: 'ptr', hInstance: 'ptr', lpstrFilter: 'ptr',
111
+ lpstrCustomFilter: 'ptr', nMaxCustFilter: 'u32', nFilterIndex: 'u32',
112
+ lpstrFile: 'ptr', nMaxFile: 'u32', lpstrFileTitle: 'ptr', nMaxFileTitle: 'u32',
113
+ lpstrInitialDir: 'ptr', lpstrTitle: 'ptr', Flags: 'u32',
114
+ nFileOffset: 'u16', nFileExtension: 'u16',
115
+ lpstrDefExt: 'ptr', lCustData: 'ptr', lpfnHook: 'ptr', lpTemplateName: 'ptr',
116
+ pvReserved: 'ptr', dwReserved: 'u32', FlagsEx: 'u32', // Win2000+ 追加
117
+ });
118
+ t.check('OPENFILENAMEW.size', is64 ? 152 : 88, OPENFILENAMEW.size);
119
+ t.check('OPENFILENAMEW.offsetOf lpstrFile', is64 ? 48 : 28, OPENFILENAMEW.offsetOf('lpstrFile'));
120
+ t.check('OPENFILENAMEW.offsetOf Flags', is64 ? 96 : 52, OPENFILENAMEW.offsetOf('Flags'));
121
+ t.check('OPENFILENAMEW.offsetOf lpstrTitle', is64 ? 88 : 48, OPENFILENAMEW.offsetOf('lpstrTitle'));
122
+ const BROWSEINFOW = struct({
123
+ hwndOwner: 'ptr', pidlRoot: 'ptr', pszDisplayName: 'ptr', lpszTitle: 'ptr',
124
+ ulFlags: 'u32', lpfn: 'ptr', lParam: 'ptr', iImage: 'i32',
125
+ });
126
+ t.check('BROWSEINFOW.size', is64 ? 64 : 32, BROWSEINFOW.size);
127
+ t.check('BROWSEINFOW.offsetOf lpszTitle', is64 ? 24 : 12, BROWSEINFOW.offsetOf('lpszTitle'));
128
+ t.check('BROWSEINFOW.offsetOf ulFlags', is64 ? 32 : 16, BROWSEINFOW.offsetOf('ulFlags'));
129
+ t.section('NMHDR(嵌套时 MSVC 尾 padding 传染)');
130
+ const NMHDR = struct({ hwndFrom: 'ptr', idFrom: 'ptr', code: 'i32' });
131
+ t.check('NMHDR.size', is64 ? 24 : 12, NMHDR.size);
132
+ t.check('NMHDR.offsetOf code', is64 ? 16 : 8, NMHDR.offsetOf('code'));
133
+ t.section('NMCUSTOMDRAW / NMLVCUSTOMDRAW (ListView custom draw)');
134
+ const NMCUSTOMDRAW = struct({
135
+ hdr: NMHDR,
136
+ dwDrawStage: 'u32', hdc: 'ptr', rc: 'i32[4]',
137
+ dwItemSpec: 'ptr', uItemState: 'u32', lItemlParam: 'ptr',
138
+ });
139
+ t.check('NMCUSTOMDRAW.size', is64 ? 80 : 48, NMCUSTOMDRAW.size);
140
+ t.check('NMCUSTOMDRAW.offsetOf dwDrawStage', is64 ? 24 : 12, NMCUSTOMDRAW.offsetOf('dwDrawStage'));
141
+ t.check('NMCUSTOMDRAW.offsetOf hdc', is64 ? 32 : 16, NMCUSTOMDRAW.offsetOf('hdc'));
142
+ t.check('NMCUSTOMDRAW.offsetOf dwItemSpec', is64 ? 56 : 36, NMCUSTOMDRAW.offsetOf('dwItemSpec'));
143
+ const NMLVCUSTOMDRAW = struct({
144
+ hdr: NMHDR,
145
+ dwDrawStage: 'u32', hdc: 'ptr', rc: 'i32[4]',
146
+ dwItemSpec: 'ptr', uItemState: 'u32', lItemlParam: 'ptr',
147
+ clrText: 'u32', clrTextBk: 'u32', iSubItem: 'i32', dwItemType: 'u32',
148
+ });
149
+ t.check('NMLVCUSTOMDRAW.size', is64 ? 96 : 64, NMLVCUSTOMDRAW.size);
150
+ t.check('NMLVCUSTOMDRAW.offsetOf clrText', is64 ? 80 : 48, NMLVCUSTOMDRAW.offsetOf('clrText'));
151
+ t.check('NMLVCUSTOMDRAW.offsetOf clrTextBk', is64 ? 84 : 52, NMLVCUSTOMDRAW.offsetOf('clrTextBk'));
152
+ t.check('NMLVCUSTOMDRAW.offsetOf iSubItem', is64 ? 88 : 56, NMLVCUSTOMDRAW.offsetOf('iSubItem'));
153
+ t.section('NMLISTVIEW / NMITEMACTIVATE');
154
+ const NMLISTVIEW = struct({
155
+ hdr: NMHDR,
156
+ iItem: 'i32', iSubItem: 'i32', uNewState: 'u32', uOldState: 'u32', uChanged: 'u32',
157
+ ptAction: 'i32[2]', lParam: 'ptr',
158
+ });
159
+ t.check('NMLISTVIEW.size', is64 ? 64 : 44, NMLISTVIEW.size);
160
+ t.check('NMLISTVIEW.offsetOf iItem', is64 ? 24 : 12, NMLISTVIEW.offsetOf('iItem'));
161
+ t.check('NMLISTVIEW.offsetOf iSubItem', is64 ? 28 : 16, NMLISTVIEW.offsetOf('iSubItem'));
162
+ t.check('NMLISTVIEW.offsetOf uNewState', is64 ? 32 : 20, NMLISTVIEW.offsetOf('uNewState'));
163
+ t.check('NMLISTVIEW.offsetOf uOldState', is64 ? 36 : 24, NMLISTVIEW.offsetOf('uOldState'));
164
+ t.section('LVITEMW / LVCOLUMNW');
165
+ const LVITEMW = struct({
166
+ mask: 'u32', iItem: 'i32', iSubItem: 'i32',
167
+ state: 'u32', stateMask: 'u32',
168
+ pszText: 'ptr', cchTextMax: 'i32', iImage: 'i32', lParam: 'ptr',
169
+ iIndent: 'i32', iGroupId: 'i32', cColumns: 'u32',
170
+ puColumns: 'ptr', piColFmt: 'ptr', iGroup: 'i32',
171
+ });
172
+ t.check('LVITEMW.size', is64 ? 88 : 60, LVITEMW.size);
173
+ t.check('LVITEMW.offsetOf iItem', 4, LVITEMW.offsetOf('iItem'));
174
+ t.check('LVITEMW.offsetOf pszText', is64 ? 24 : 20, LVITEMW.offsetOf('pszText'));
175
+ t.check('LVITEMW.offsetOf iImage', is64 ? 36 : 28, LVITEMW.offsetOf('iImage'));
176
+ t.check('LVITEMW.offsetOf lParam', is64 ? 40 : 32, LVITEMW.offsetOf('lParam'));
177
+ const LVCOLUMNW = struct({
178
+ mask: 'u32', fmt: 'i32', cx: 'i32',
179
+ pszText: 'ptr', cchTextMax: 'i32', iSubItem: 'i32', iImage: 'i32', iOrder: 'i32',
180
+ cxMin: 'i32', cxDefault: 'i32', cxIdeal: 'i32',
181
+ });
182
+ t.check('LVCOLUMNW.size', is64 ? 56 : 44, LVCOLUMNW.size);
183
+ t.check('LVCOLUMNW.offsetOf pszText', is64 ? 16 : 12, LVCOLUMNW.offsetOf('pszText'));
184
+ t.check('LVCOLUMNW.offsetOf iSubItem', is64 ? 28 : 20, LVCOLUMNW.offsetOf('iSubItem'));
185
+ t.check('LVCOLUMNW.offsetOf iOrder', is64 ? 36 : 28, LVCOLUMNW.offsetOf('iOrder'));
186
+ t.section('NMDATETIMECHANGE');
187
+ const SYSTEMTIME = struct({
188
+ wYear: 'u16', wMonth: 'u16', wDayOfWeek: 'u16', wDay: 'u16',
189
+ wHour: 'u16', wMinute: 'u16', wSecond: 'u16', wMilliseconds: 'u16',
190
+ });
191
+ const NMDATETIMECHANGE = struct({ hdr: NMHDR, dwFlags: 'u32', st: SYSTEMTIME });
192
+ t.check('NMDATETIMECHANGE.size', is64 ? 48 : 32, NMDATETIMECHANGE.size);
193
+ t.check('NMDATETIMECHANGE.offsetOf dwFlags', is64 ? 24 : 12, NMDATETIMECHANGE.offsetOf('dwFlags'));
194
+ t.check('NMDATETIMECHANGE.offsetOf st', is64 ? 28 : 16, NMDATETIMECHANGE.offsetOf('st'));
195
+ t.section('NMLINK szUrl offset');
196
+ const LITEM = struct({ mask: 'u32', iLink: 'i32', state: 'u32', stateMask: 'u32', szID: 'wstr[48]' });
197
+ const NMLINK = struct({ hdr: NMHDR, item: LITEM, szUrl: 'wstr[2084]' });
198
+ t.check('LITEM.offsetOf szID', 16, LITEM.offsetOf('szID'));
199
+ t.check('NMLINK.offsetOf szUrl', is64 ? 136 : 124, NMLINK.offsetOf('szUrl'));
200
+ },
201
+ };
@@ -0,0 +1,223 @@
1
+ import * as os from 'os'
2
+ import { Tester } from './test_helper.js'
3
+ import { struct } from '../lib/ffi-struct.js'
4
+ import { bind } from '../lib/ffi-bind.js'
5
+
6
+ const RECT = struct({ left: 'i32', top: 'i32', right: 'i32', bottom: 'i32' })
7
+
8
+ const TVITEM = struct({
9
+ mask: 'u32',
10
+ hItem: 'ptr',
11
+ state: 'u32',
12
+ stateMask: 'u32',
13
+ pszText: 'ptr',
14
+ cchTextMax: 'i32',
15
+ iImage: 'i32',
16
+ iSelectedImage: 'i32',
17
+ cChildren: 'i32',
18
+ lParam: 'ptr',
19
+ })
20
+ const TVINSERTSTRUCT = struct({ hParent: 'ptr', hInsertAfter: 'ptr', item: TVITEM })
21
+
22
+ export const suite = {
23
+ name: 'ffi-struct',
24
+ run: (t: Tester) => {
25
+ const is64 = os.arch === 'x64'
26
+
27
+ t.section('RECT layout & roundtrip')
28
+ t.check('RECT.size == 16', 16, RECT.size)
29
+ const rbuf = RECT.write({ left: 10, top: 20, right: 30, bottom: 40 })
30
+ const r = RECT.read(rbuf)
31
+ t.check('left', 10, r.left)
32
+ t.check('top', 20, r.top)
33
+ t.check('right', 30, r.right)
34
+ t.check('bottom', 40, r.bottom)
35
+ t.check('offsetOf left', 0, RECT.offsetOf('left'))
36
+ t.check('offsetOf bottom', 12, RECT.offsetOf('bottom'))
37
+
38
+ t.section('GetWindowRect fills RECT buffer')
39
+ const getWindowRect = bind('user32.dll', 'GetWindowRect', 'ptr ptr -> int')
40
+ const getDesktopWindow = bind('user32.dll', 'GetDesktopWindow', ' -> ptr')
41
+ const hwnd = getDesktopWindow()
42
+ const wrect = new ArrayBuffer(16)
43
+ const ok = getWindowRect(hwnd, wrect)
44
+ t.checkTrue('GetWindowRect succeeds', ok !== 0)
45
+ const wr = RECT.read(wrect)
46
+ t.checkTrue('screen RECT non-empty', wr.right > 0 && wr.bottom > 0)
47
+
48
+ t.section('ptr layout matches arch')
49
+ t.check(`TVITEM.size (${os.arch})`, is64 ? 56 : 40, TVITEM.size)
50
+ t.check('TVITEM.offsetOf pszText', is64 ? 24 : 16, TVITEM.offsetOf('pszText'))
51
+ t.check('TVINSERTSTRUCT.size', is64 ? 72 : 48, TVINSERTSTRUCT.size)
52
+ t.check('TVINSERTSTRUCT.offsetOf item', is64 ? 16 : 8, TVINSERTSTRUCT.offsetOf('item'))
53
+
54
+ t.section('nested struct roundtrip')
55
+ const ins = TVINSERTSTRUCT.write({
56
+ hParent: 0x11111111,
57
+ hInsertAfter: 0x22222222,
58
+ item: {
59
+ mask: 1, hItem: 0x33333333, state: 0, stateMask: 0,
60
+ pszText: 0x44444444, cchTextMax: 100,
61
+ iImage: 0, iSelectedImage: 0, cChildren: 5, lParam: 0x55555555,
62
+ },
63
+ })
64
+ const got = TVINSERTSTRUCT.read(ins)
65
+ t.check('nested hParent', 0x11111111, got.hParent)
66
+ t.check('nested hInsertAfter', 0x22222222, got.hInsertAfter)
67
+ t.check('nested cChildren', 5, got.item.cChildren)
68
+ t.check('nested ptr pszText', 0x44444444, got.item.pszText)
69
+
70
+ t.section('C typedef aliases (int/DWORD/LPARAM/LONG_PTR/short)')
71
+ const M = struct({ n: 'int', d: 'DWORD', w: 'LPARAM', s: 'short', q: 'LONG_PTR' })
72
+ t.check(`aliased size (${os.arch})`, is64 ? 32 : 20, M.size)
73
+ const mb = M.write({ n: -5, d: 0xFFFFFFFF, w: 0xAABBCCDD, s: -7, q: 0x11223344 })
74
+ const m = M.read(mb)
75
+ t.check('int -5', -5, m.n)
76
+ t.check('DWORD', 0xFFFFFFFF, m.d)
77
+ t.check('LPARAM', 0xAABBCCDD, m.w)
78
+ t.check('short -7', -7, m.s)
79
+ t.check('LONG_PTR', 0x11223344, m.q)
80
+
81
+ t.section('wstr[8] roundtrip & truncation')
82
+ const W = struct({ name: 'wstr[8]' })
83
+ t.check('wstr[8].size == 16', 16, W.size)
84
+ const wb = W.write({ name: 'hello' })
85
+ t.check('read back "hello"', 'hello', W.read(wb).name)
86
+ const lb = W.write({ name: 'a very long string over' })
87
+ t.check('truncated to 7 chars', 'a very ', W.read(lb).name)
88
+
89
+ t.section('numeric arrays roundtrip')
90
+ const A = struct({ v: 'u16[4]', k: 'i32[2]' })
91
+ t.check('size == 16', 16, A.size)
92
+ const ab = A.write({ v: [1, 2, 3, 4], k: [-1, 300000] })
93
+ const ad = A.read(ab)
94
+ t.check('u16[0]', 1, ad.v[0])
95
+ t.check('u16[3]', 4, ad.v[3])
96
+ t.check('i32[0] -1', -1, ad.k[0])
97
+ t.check('i32[1]', 300000, ad.k[1])
98
+
99
+ t.section('f32/f64 layout (MSVC: f@0, double@8)')
100
+ const FL = struct({ f: 'float', d: 'double' })
101
+ t.check('size == 16 (f@0, double aligned @8)', 16, FL.size)
102
+ const fb = FL.write({ f: 1.5, d: -2.25 })
103
+ const fr = FL.read(fb)
104
+ t.check('float', 1.5, fr.f)
105
+ t.check('double', -2.25, fr.d)
106
+
107
+ t.section('comctl dialog structs track 32/64-bit ABI')
108
+ const TCITEMW = struct({
109
+ mask: 'u32', dwState: 'u32', dwStateMask: 'u32',
110
+ pszText: 'ptr', cchTextMax: 'i32', iImage: 'i32', lParam: 'ptr',
111
+ })
112
+ t.check('TCITEMW.size', is64 ? 40 : 28, TCITEMW.size)
113
+ t.check('TCITEMW.offsetOf pszText', is64 ? 16 : 12, TCITEMW.offsetOf('pszText'))
114
+ t.check('TCITEMW.offsetOf cchTextMax', is64 ? 24 : 16, TCITEMW.offsetOf('cchTextMax'))
115
+
116
+ const TTTOOLINFOW = struct({
117
+ cbSize: 'u32', uFlags: 'u32', hwnd: 'ptr', uId: 'ptr',
118
+ rect: 'i32[4]', hinst: 'ptr', lpszText: 'ptr', lParam: 'ptr',
119
+ lpReserved: 'ptr', // WinXP+ 追加
120
+ })
121
+ t.check('TTTOOLINFOW.size', is64 ? 72 : 48, TTTOOLINFOW.size)
122
+ t.check('TTTOOLINFOW.offsetOf lpszText', is64 ? 48 : 36, TTTOOLINFOW.offsetOf('lpszText'))
123
+
124
+ const OPENFILENAMEW = struct({
125
+ lStructSize: 'u32', hwndOwner: 'ptr', hInstance: 'ptr', lpstrFilter: 'ptr',
126
+ lpstrCustomFilter: 'ptr', nMaxCustFilter: 'u32', nFilterIndex: 'u32',
127
+ lpstrFile: 'ptr', nMaxFile: 'u32', lpstrFileTitle: 'ptr', nMaxFileTitle: 'u32',
128
+ lpstrInitialDir: 'ptr', lpstrTitle: 'ptr', Flags: 'u32',
129
+ nFileOffset: 'u16', nFileExtension: 'u16',
130
+ lpstrDefExt: 'ptr', lCustData: 'ptr', lpfnHook: 'ptr', lpTemplateName: 'ptr',
131
+ pvReserved: 'ptr', dwReserved: 'u32', FlagsEx: 'u32', // Win2000+ 追加
132
+ })
133
+ t.check('OPENFILENAMEW.size', is64 ? 152 : 88, OPENFILENAMEW.size)
134
+ t.check('OPENFILENAMEW.offsetOf lpstrFile', is64 ? 48 : 28, OPENFILENAMEW.offsetOf('lpstrFile'))
135
+ t.check('OPENFILENAMEW.offsetOf Flags', is64 ? 96 : 52, OPENFILENAMEW.offsetOf('Flags'))
136
+ t.check('OPENFILENAMEW.offsetOf lpstrTitle', is64 ? 88 : 48, OPENFILENAMEW.offsetOf('lpstrTitle'))
137
+
138
+ const BROWSEINFOW = struct({
139
+ hwndOwner: 'ptr', pidlRoot: 'ptr', pszDisplayName: 'ptr', lpszTitle: 'ptr',
140
+ ulFlags: 'u32', lpfn: 'ptr', lParam: 'ptr', iImage: 'i32',
141
+ })
142
+ t.check('BROWSEINFOW.size', is64 ? 64 : 32, BROWSEINFOW.size)
143
+ t.check('BROWSEINFOW.offsetOf lpszTitle', is64 ? 24 : 12, BROWSEINFOW.offsetOf('lpszTitle'))
144
+ t.check('BROWSEINFOW.offsetOf ulFlags', is64 ? 32 : 16, BROWSEINFOW.offsetOf('ulFlags'))
145
+
146
+ t.section('NMHDR(嵌套时 MSVC 尾 padding 传染)')
147
+ const NMHDR = struct({ hwndFrom: 'ptr', idFrom: 'ptr', code: 'i32' })
148
+ t.check('NMHDR.size', is64 ? 24 : 12, NMHDR.size)
149
+ t.check('NMHDR.offsetOf code', is64 ? 16 : 8, NMHDR.offsetOf('code'))
150
+
151
+ t.section('NMCUSTOMDRAW / NMLVCUSTOMDRAW (ListView custom draw)')
152
+ const NMCUSTOMDRAW = struct({
153
+ hdr: NMHDR,
154
+ dwDrawStage: 'u32', hdc: 'ptr', rc: 'i32[4]',
155
+ dwItemSpec: 'ptr', uItemState: 'u32', lItemlParam: 'ptr',
156
+ })
157
+ t.check('NMCUSTOMDRAW.size', is64 ? 80 : 48, NMCUSTOMDRAW.size)
158
+ t.check('NMCUSTOMDRAW.offsetOf dwDrawStage', is64 ? 24 : 12, NMCUSTOMDRAW.offsetOf('dwDrawStage'))
159
+ t.check('NMCUSTOMDRAW.offsetOf hdc', is64 ? 32 : 16, NMCUSTOMDRAW.offsetOf('hdc'))
160
+ t.check('NMCUSTOMDRAW.offsetOf dwItemSpec', is64 ? 56 : 36, NMCUSTOMDRAW.offsetOf('dwItemSpec'))
161
+ const NMLVCUSTOMDRAW = struct({
162
+ hdr: NMHDR,
163
+ dwDrawStage: 'u32', hdc: 'ptr', rc: 'i32[4]',
164
+ dwItemSpec: 'ptr', uItemState: 'u32', lItemlParam: 'ptr',
165
+ clrText: 'u32', clrTextBk: 'u32', iSubItem: 'i32', dwItemType: 'u32',
166
+ })
167
+ t.check('NMLVCUSTOMDRAW.size', is64 ? 96 : 64, NMLVCUSTOMDRAW.size)
168
+ t.check('NMLVCUSTOMDRAW.offsetOf clrText', is64 ? 80 : 48, NMLVCUSTOMDRAW.offsetOf('clrText'))
169
+ t.check('NMLVCUSTOMDRAW.offsetOf clrTextBk', is64 ? 84 : 52, NMLVCUSTOMDRAW.offsetOf('clrTextBk'))
170
+ t.check('NMLVCUSTOMDRAW.offsetOf iSubItem', is64 ? 88 : 56, NMLVCUSTOMDRAW.offsetOf('iSubItem'))
171
+
172
+ t.section('NMLISTVIEW / NMITEMACTIVATE')
173
+ const NMLISTVIEW = struct({
174
+ hdr: NMHDR,
175
+ iItem: 'i32', iSubItem: 'i32', uNewState: 'u32', uOldState: 'u32', uChanged: 'u32',
176
+ ptAction: 'i32[2]', lParam: 'ptr',
177
+ })
178
+ t.check('NMLISTVIEW.size', is64 ? 64 : 44, NMLISTVIEW.size)
179
+ t.check('NMLISTVIEW.offsetOf iItem', is64 ? 24 : 12, NMLISTVIEW.offsetOf('iItem'))
180
+ t.check('NMLISTVIEW.offsetOf iSubItem', is64 ? 28 : 16, NMLISTVIEW.offsetOf('iSubItem'))
181
+ t.check('NMLISTVIEW.offsetOf uNewState', is64 ? 32 : 20, NMLISTVIEW.offsetOf('uNewState'))
182
+ t.check('NMLISTVIEW.offsetOf uOldState', is64 ? 36 : 24, NMLISTVIEW.offsetOf('uOldState'))
183
+
184
+ t.section('LVITEMW / LVCOLUMNW')
185
+ const LVITEMW = struct({
186
+ mask: 'u32', iItem: 'i32', iSubItem: 'i32',
187
+ state: 'u32', stateMask: 'u32',
188
+ pszText: 'ptr', cchTextMax: 'i32', iImage: 'i32', lParam: 'ptr',
189
+ iIndent: 'i32', iGroupId: 'i32', cColumns: 'u32',
190
+ puColumns: 'ptr', piColFmt: 'ptr', iGroup: 'i32',
191
+ })
192
+ t.check('LVITEMW.size', is64 ? 88 : 60, LVITEMW.size)
193
+ t.check('LVITEMW.offsetOf iItem', 4, LVITEMW.offsetOf('iItem'))
194
+ t.check('LVITEMW.offsetOf pszText', is64 ? 24 : 20, LVITEMW.offsetOf('pszText'))
195
+ t.check('LVITEMW.offsetOf iImage', is64 ? 36 : 28, LVITEMW.offsetOf('iImage'))
196
+ t.check('LVITEMW.offsetOf lParam', is64 ? 40 : 32, LVITEMW.offsetOf('lParam'))
197
+ const LVCOLUMNW = struct({
198
+ mask: 'u32', fmt: 'i32', cx: 'i32',
199
+ pszText: 'ptr', cchTextMax: 'i32', iSubItem: 'i32', iImage: 'i32', iOrder: 'i32',
200
+ cxMin: 'i32', cxDefault: 'i32', cxIdeal: 'i32',
201
+ })
202
+ t.check('LVCOLUMNW.size', is64 ? 56 : 44, LVCOLUMNW.size)
203
+ t.check('LVCOLUMNW.offsetOf pszText', is64 ? 16 : 12, LVCOLUMNW.offsetOf('pszText'))
204
+ t.check('LVCOLUMNW.offsetOf iSubItem', is64 ? 28 : 20, LVCOLUMNW.offsetOf('iSubItem'))
205
+ t.check('LVCOLUMNW.offsetOf iOrder', is64 ? 36 : 28, LVCOLUMNW.offsetOf('iOrder'))
206
+
207
+ t.section('NMDATETIMECHANGE')
208
+ const SYSTEMTIME = struct({
209
+ wYear: 'u16', wMonth: 'u16', wDayOfWeek: 'u16', wDay: 'u16',
210
+ wHour: 'u16', wMinute: 'u16', wSecond: 'u16', wMilliseconds: 'u16',
211
+ })
212
+ const NMDATETIMECHANGE = struct({ hdr: NMHDR, dwFlags: 'u32', st: SYSTEMTIME })
213
+ t.check('NMDATETIMECHANGE.size', is64 ? 48 : 32, NMDATETIMECHANGE.size)
214
+ t.check('NMDATETIMECHANGE.offsetOf dwFlags', is64 ? 24 : 12, NMDATETIMECHANGE.offsetOf('dwFlags'))
215
+ t.check('NMDATETIMECHANGE.offsetOf st', is64 ? 28 : 16, NMDATETIMECHANGE.offsetOf('st'))
216
+
217
+ t.section('NMLINK szUrl offset')
218
+ const LITEM = struct({ mask: 'u32', iLink: 'i32', state: 'u32', stateMask: 'u32', szID: 'wstr[48]' })
219
+ const NMLINK = struct({ hdr: NMHDR, item: LITEM, szUrl: 'wstr[2084]' })
220
+ t.check('LITEM.offsetOf szID', 16, LITEM.offsetOf('szID'))
221
+ t.check('NMLINK.offsetOf szUrl', is64 ? 136 : 124, NMLINK.offsetOf('szUrl'))
222
+ },
223
+ }
@@ -1,5 +1,6 @@
1
1
  import '../lib/fetch.js';
2
2
  import * as std from 'std';
3
+ import * as os from 'os';
3
4
  export const suite = {
4
5
  name: 'net-fetch',
5
6
  run: async (t) => {
@@ -255,5 +256,66 @@ export const suite = {
255
256
  else {
256
257
  assert('https endpoint reachable', false);
257
258
  }
259
+ // ── large HTTPS body (wolfSSL_read null regression) ──
260
+ // Pre-fix: wolfSSL_read returned number -1 on WANT_READ →
261
+ // receivedBytes became NaN → stream never closed → hang.
262
+ t.section('large HTTPS body');
263
+ {
264
+ const LARGE = 200000;
265
+ try {
266
+ const r = await Promise.race([
267
+ fetch(`https://localhost:18924/large/${LARGE}`, { timeout: 15000 }),
268
+ new Promise((_, rej) => os.setTimeout(() => rej(new Error('Body hang watchdog 15s')), 15000))
269
+ ]);
270
+ assert('large HTTPS status 200', r.status === 200);
271
+ const buf = await Promise.race([
272
+ r.arrayBuffer(),
273
+ new Promise((_, rej) => os.setTimeout(() => rej(new Error('arrayBuffer hang watchdog 15s')), 15000))
274
+ ]);
275
+ assert('large HTTPS byteLength exact', buf.byteLength === LARGE);
276
+ }
277
+ catch (e) {
278
+ assert('large HTTPS completed without hang (' + String(e.message) + ')', false);
279
+ }
280
+ }
281
+ // ── large HTTP body (sock.recv path same null contract) ──
282
+ t.section('large HTTP body');
283
+ {
284
+ const LARGE = 200000;
285
+ try {
286
+ const r = await Promise.race([
287
+ fetch(`http://localhost:18923/large/${LARGE}`, { timeout: 15000 }),
288
+ new Promise((_, rej) => os.setTimeout(() => rej(new Error('Body hang watchdog 15s')), 15000))
289
+ ]);
290
+ assert('large HTTP status 200', r.status === 200);
291
+ const buf = await Promise.race([
292
+ r.arrayBuffer(),
293
+ new Promise((_, rej) => os.setTimeout(() => rej(new Error('arrayBuffer hang watchdog 15s')), 15000))
294
+ ]);
295
+ assert('large HTTP byteLength exact', buf.byteLength === LARGE);
296
+ }
297
+ catch (e) {
298
+ assert('large HTTP completed (' + String(e.message) + ')', false);
299
+ }
300
+ }
301
+ // ── body timeout after headers (armTimeout after doResolve) ──
302
+ t.section('HTTPS body timeout');
303
+ {
304
+ let rejected = false;
305
+ let msg = '';
306
+ try {
307
+ const r = await fetch('https://localhost:18924/stall', { timeout: 2000 });
308
+ await Promise.race([
309
+ r.arrayBuffer(),
310
+ new Promise((_, rej) => os.setTimeout(() => rej(new Error('Body timeout')), 8000))
311
+ ]);
312
+ }
313
+ catch (e) {
314
+ rejected = true;
315
+ msg = String(e.message);
316
+ }
317
+ assert('stall body rejects (timeout)', rejected);
318
+ assert('stall error mentions timeout', /timeout/i.test(msg));
319
+ }
258
320
  }
259
321
  };