Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 24 additions & 7 deletions comtypes/shelllink.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from ctypes import (
POINTER,
Structure,
byref,
c_char_p,
c_int,
Expand All @@ -8,12 +9,21 @@
create_string_buffer,
create_unicode_buffer,
)
from ctypes.wintypes import DWORD, MAX_PATH, WIN32_FIND_DATAA, WIN32_FIND_DATAW
from ctypes.wintypes import (
BYTE,
DWORD,
MAX_PATH,
USHORT,
WIN32_FIND_DATAA,
WIN32_FIND_DATAW,
)
from typing import TYPE_CHECKING, Literal

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

if TYPE_CHECKING:
from ctypes import _Pointer

from comtypes import hints # type: ignore


Expand Down Expand Up @@ -42,8 +52,15 @@
HOTKEYF_EXT = 0x08
HOTKEYF_SHIFT = 0x01

# fake these...
ITEMIDLIST = c_int

class SHITEMID(Structure):
_fields_ = [("cb", USHORT), ("abID", BYTE * 1)]


class ITEMIDLIST(Structure):
_fields_ = [("mkid", SHITEMID)]


LPITEMIDLIST = LPCITEMIDLIST = POINTER(ITEMIDLIST)


Expand Down Expand Up @@ -134,8 +151,8 @@ class IShellLinkA(IUnknown):

if TYPE_CHECKING:

def GetIDList(self) -> hints.Incomplete: ...
def SetIDList(self, pidl: hints.Incomplete) -> hints.Incomplete: ...
def GetIDList(self) -> _Pointer[ITEMIDLIST]: ...
def SetIDList(self, pidl: _Pointer[ITEMIDLIST]) -> hints.Hresult: ...
def SetDescription(self, pszName: bytes) -> hints.Incomplete: ...
def SetWorkingDirectory(self, pszDir: bytes) -> hints.Hresult: ...
def SetArguments(self, pszArgs: bytes) -> hints.Hresult: ...
Expand Down Expand Up @@ -269,8 +286,8 @@ class IShellLinkW(IUnknown):

if TYPE_CHECKING:

def GetIDList(self) -> hints.Incomplete: ...
def SetIDList(self, pidl: hints.Incomplete) -> hints.Incomplete: ...
def GetIDList(self) -> _Pointer[ITEMIDLIST]: ...
def SetIDList(self, pidl: _Pointer[ITEMIDLIST]) -> hints.Hresult: ...
def SetDescription(self, pszName: str) -> hints.Incomplete: ...
def SetWorkingDirectory(self, pszDir: str) -> hints.Hresult: ...
def SetArguments(self, pszArgs: str) -> hints.Hresult: ...
Expand Down
79 changes: 79 additions & 0 deletions comtypes/test/test_shelllink.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import struct
import tempfile
import unittest as ut
from ctypes import WinDLL, addressof, cast, create_string_buffer, string_at
from ctypes.wintypes import BOOL
from pathlib import Path

import comtypes.hresult
from comtypes import GUID, CoCreateInstance, shelllink
from comtypes.malloc import CoGetMalloc, _CoTaskMemFree
from comtypes.persist import IPersistFile
from comtypes.shelllink import LPITEMIDLIST as PIDLIST_ABSOLUTE

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

_shell32 = WinDLL("shell32")

# https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-ilisequal
_ILIsEqual = _shell32.ILIsEqual
_ILIsEqual.argtypes = [PIDLIST_ABSOLUTE, PIDLIST_ABSOLUTE]
_ILIsEqual.restype = BOOL


class Test_IShellLinkA(ut.TestCase):
def setUp(self):
Expand Down Expand Up @@ -66,6 +78,39 @@ def test_set_and_get_icon_location(self):
self.assertEqual(icon_path, str(self.src_file).encode("utf-8"))
self.assertEqual(index, 1)

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


class Test_IShellLinkW(ut.TestCase):
def setUp(self):
Expand Down Expand Up @@ -126,3 +171,37 @@ def test_set_and_get_icon_location(self):
icon_path, index = shortcut.GetIconLocation()
self.assertEqual(icon_path, str(self.src_file))
self.assertEqual(index, 1)

def test_set_and_get_idlist(self):
# Create a manual PIDL for testing.
# In reality, the `abID` portion contains Shell namespace identifiers.
# (e.g. file system item IDs, special folder tokens, virtual folder
# GUIDs, etc.)
# These IDs are referenced/used by Shell folders to identify and locate
# specific items in the namespace.
data = b"\xca\xfe\xba\xbe" # dummy test data (meaningless in real use).
cb = len(data) + 2
# ITEMIDLIST format:
# - little-endian ('<')
# - cb as 16-bit unsigned integer ('H')
# - data bytes of length ('{len(data)}s')
# - terminator as 16-bit unsigned integer ('H')
raw_pidl = struct.pack(f"<H{len(data)}sH", cb, data, 0)
in_pidl = cast(create_string_buffer(raw_pidl), shelllink.LPCITEMIDLIST)
# Set pidl.
shortcut = self._create_shortcut()
shortcut.SetIDList(in_pidl)
# Get it back and verify.
out_pidl = shortcut.GetIDList()
idlist = out_pidl.contents
self.assertEqual(idlist.mkid.cb, cb)
# Access the raw data from the pointer.
self.assertEqual(string_at(addressof(idlist.mkid.abID), len(data)), data)
self.assertTrue(_ILIsEqual(in_pidl, out_pidl))
malloc = CoGetMalloc()
# Verify that the input PIDL is not COM-allocated memory.
self.assertFalse(malloc.DidAlloc(in_pidl))
# Verify that the output PIDL IS COM-allocated memory, requiring
# `CoTaskMemFree` for proper deallocation.
self.assertTrue(malloc.DidAlloc(out_pidl))
_CoTaskMemFree(out_pidl)