Skip to content

Commit 4eb1b9d

Browse files
authored
Refactor shelllink.py for ITEMIDLIST structure and enhance IShellLink tests. (#926)
* refactor: Define `ITEMIDLIST` and `SHITEMID` as `Structure` in `shelllink.py`. - Replace the placeholder `ITEMIDLIST = c_int` with actual `Structure` definitions to match Windows API specifications. - Include `SHITEMID` with `cb` and `abID` fields. * test: Add `IShellLink::SetIDList` and `GetIDList` tests. - Add `test_set_and_get_idlist` to `Test_IShellLinkA` and `Test_IShellLinkW` in `test_shelllink.py`. - Verify setting and retrieving a manually constructed `ITEMIDLIST`. - Use `_CoTaskMemFree` to release the pointer returned by `GetIDList`. * refactor: Improve `IShellLink` type hints for `GetIDList` and `SetIDList`. - Use `_Pointer[ITEMIDLIST]` for `ITEMIDLIST` pointers in `IShellLinkA` and `IShellLinkW`. - Specify `hints.Hresult` as the return type for `SetIDList`. - Import `_Pointer` from `ctypes` within the `TYPE_CHECKING` block. * test: Use `ILIsEqual` for robust `ITEMIDLIST` comparisons in `test_shelllink.py`. - Add assertions using `ILIsEqual` in `Test_IShellLinkA` and `Test_IShellLinkW` to ensure correct `ITEMIDLIST` handling. * test: Verify `PIDL` memory allocation using `CoGetMalloc().DidAlloc` in `test_shelllink.py`. - Assert that input `PIDL`s are not COM-allocated and output `PIDL`s are, ensuring proper `CoTaskMemFree` usage.
1 parent 844a111 commit 4eb1b9d

2 files changed

Lines changed: 103 additions & 7 deletions

File tree

comtypes/shelllink.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from ctypes import (
22
POINTER,
3+
Structure,
34
byref,
45
c_char_p,
56
c_int,
@@ -8,12 +9,21 @@
89
create_string_buffer,
910
create_unicode_buffer,
1011
)
11-
from ctypes.wintypes import DWORD, MAX_PATH, WIN32_FIND_DATAA, WIN32_FIND_DATAW
12+
from ctypes.wintypes import (
13+
BYTE,
14+
DWORD,
15+
MAX_PATH,
16+
USHORT,
17+
WIN32_FIND_DATAA,
18+
WIN32_FIND_DATAW,
19+
)
1220
from typing import TYPE_CHECKING, Literal
1321

1422
from comtypes import COMMETHOD, GUID, HRESULT, CoClass, IUnknown
1523

1624
if TYPE_CHECKING:
25+
from ctypes import _Pointer
26+
1727
from comtypes import hints # type: ignore
1828

1929

@@ -42,8 +52,15 @@
4252
HOTKEYF_EXT = 0x08
4353
HOTKEYF_SHIFT = 0x01
4454

45-
# fake these...
46-
ITEMIDLIST = c_int
55+
56+
class SHITEMID(Structure):
57+
_fields_ = [("cb", USHORT), ("abID", BYTE * 1)]
58+
59+
60+
class ITEMIDLIST(Structure):
61+
_fields_ = [("mkid", SHITEMID)]
62+
63+
4764
LPITEMIDLIST = LPCITEMIDLIST = POINTER(ITEMIDLIST)
4865

4966

@@ -134,8 +151,8 @@ class IShellLinkA(IUnknown):
134151

135152
if TYPE_CHECKING:
136153

137-
def GetIDList(self) -> hints.Incomplete: ...
138-
def SetIDList(self, pidl: hints.Incomplete) -> hints.Incomplete: ...
154+
def GetIDList(self) -> _Pointer[ITEMIDLIST]: ...
155+
def SetIDList(self, pidl: _Pointer[ITEMIDLIST]) -> hints.Hresult: ...
139156
def SetDescription(self, pszName: bytes) -> hints.Incomplete: ...
140157
def SetWorkingDirectory(self, pszDir: bytes) -> hints.Hresult: ...
141158
def SetArguments(self, pszArgs: bytes) -> hints.Hresult: ...
@@ -269,8 +286,8 @@ class IShellLinkW(IUnknown):
269286

270287
if TYPE_CHECKING:
271288

272-
def GetIDList(self) -> hints.Incomplete: ...
273-
def SetIDList(self, pidl: hints.Incomplete) -> hints.Incomplete: ...
289+
def GetIDList(self) -> _Pointer[ITEMIDLIST]: ...
290+
def SetIDList(self, pidl: _Pointer[ITEMIDLIST]) -> hints.Hresult: ...
274291
def SetDescription(self, pszName: str) -> hints.Incomplete: ...
275292
def SetWorkingDirectory(self, pszDir: str) -> hints.Hresult: ...
276293
def SetArguments(self, pszArgs: str) -> hints.Hresult: ...

comtypes/test/test_shelllink.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
1+
import struct
12
import tempfile
23
import unittest as ut
4+
from ctypes import WinDLL, addressof, cast, create_string_buffer, string_at
5+
from ctypes.wintypes import BOOL
36
from pathlib import Path
47

58
import comtypes.hresult
69
from comtypes import GUID, CoCreateInstance, shelllink
10+
from comtypes.malloc import CoGetMalloc, _CoTaskMemFree
711
from comtypes.persist import IPersistFile
12+
from comtypes.shelllink import LPITEMIDLIST as PIDLIST_ABSOLUTE
813

914
CLSID_ShellLink = GUID("{00021401-0000-0000-C000-000000000046}")
1015

16+
_shell32 = WinDLL("shell32")
17+
18+
# https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-ilisequal
19+
_ILIsEqual = _shell32.ILIsEqual
20+
_ILIsEqual.argtypes = [PIDLIST_ABSOLUTE, PIDLIST_ABSOLUTE]
21+
_ILIsEqual.restype = BOOL
22+
1123

1224
class Test_IShellLinkA(ut.TestCase):
1325
def setUp(self):
@@ -66,6 +78,39 @@ def test_set_and_get_icon_location(self):
6678
self.assertEqual(icon_path, str(self.src_file).encode("utf-8"))
6779
self.assertEqual(index, 1)
6880

81+
def test_set_and_get_idlist(self):
82+
# Create a manual PIDL for testing.
83+
# In reality, the `abID` portion contains Shell namespace identifiers.
84+
# (e.g. file system item IDs, special folder tokens, virtual folder
85+
# GUIDs, etc.)
86+
# These IDs are referenced/used by Shell folders to identify and locate
87+
# specific items in the namespace.
88+
data = b"\xde\xad\xbe\xef" # dummy test data (meaningless in real use).
89+
cb = len(data) + 2
90+
# ITEMIDLIST format:
91+
# - little-endian ('<')
92+
# - cb as 16-bit unsigned integer ('H')
93+
# - data bytes of length ('{len(data)}s')
94+
# - terminator as 16-bit unsigned integer ('H')
95+
raw_pidl = struct.pack(f"<H{len(data)}sH", cb, data, 0)
96+
in_pidl = cast(create_string_buffer(raw_pidl), shelllink.LPCITEMIDLIST)
97+
shortcut = self._create_shortcut()
98+
shortcut.SetIDList(in_pidl)
99+
# Get it back and verify.
100+
out_pidl = shortcut.GetIDList()
101+
idlist = out_pidl.contents
102+
self.assertEqual(idlist.mkid.cb, cb)
103+
# Access the raw data from the pointer.
104+
self.assertEqual(string_at(addressof(idlist.mkid.abID), len(data)), data)
105+
self.assertTrue(_ILIsEqual(in_pidl, out_pidl))
106+
malloc = CoGetMalloc()
107+
# Verify that the input PIDL is not COM-allocated memory.
108+
self.assertFalse(malloc.DidAlloc(in_pidl))
109+
# Verify that the output PIDL IS COM-allocated memory, requiring
110+
# `CoTaskMemFree` for proper deallocation.
111+
self.assertTrue(malloc.DidAlloc(out_pidl))
112+
_CoTaskMemFree(out_pidl)
113+
69114

70115
class Test_IShellLinkW(ut.TestCase):
71116
def setUp(self):
@@ -126,3 +171,37 @@ def test_set_and_get_icon_location(self):
126171
icon_path, index = shortcut.GetIconLocation()
127172
self.assertEqual(icon_path, str(self.src_file))
128173
self.assertEqual(index, 1)
174+
175+
def test_set_and_get_idlist(self):
176+
# Create a manual PIDL for testing.
177+
# In reality, the `abID` portion contains Shell namespace identifiers.
178+
# (e.g. file system item IDs, special folder tokens, virtual folder
179+
# GUIDs, etc.)
180+
# These IDs are referenced/used by Shell folders to identify and locate
181+
# specific items in the namespace.
182+
data = b"\xca\xfe\xba\xbe" # dummy test data (meaningless in real use).
183+
cb = len(data) + 2
184+
# ITEMIDLIST format:
185+
# - little-endian ('<')
186+
# - cb as 16-bit unsigned integer ('H')
187+
# - data bytes of length ('{len(data)}s')
188+
# - terminator as 16-bit unsigned integer ('H')
189+
raw_pidl = struct.pack(f"<H{len(data)}sH", cb, data, 0)
190+
in_pidl = cast(create_string_buffer(raw_pidl), shelllink.LPCITEMIDLIST)
191+
# Set pidl.
192+
shortcut = self._create_shortcut()
193+
shortcut.SetIDList(in_pidl)
194+
# Get it back and verify.
195+
out_pidl = shortcut.GetIDList()
196+
idlist = out_pidl.contents
197+
self.assertEqual(idlist.mkid.cb, cb)
198+
# Access the raw data from the pointer.
199+
self.assertEqual(string_at(addressof(idlist.mkid.abID), len(data)), data)
200+
self.assertTrue(_ILIsEqual(in_pidl, out_pidl))
201+
malloc = CoGetMalloc()
202+
# Verify that the input PIDL is not COM-allocated memory.
203+
self.assertFalse(malloc.DidAlloc(in_pidl))
204+
# Verify that the output PIDL IS COM-allocated memory, requiring
205+
# `CoTaskMemFree` for proper deallocation.
206+
self.assertTrue(malloc.DidAlloc(out_pidl))
207+
_CoTaskMemFree(out_pidl)

0 commit comments

Comments
 (0)