-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathshellenum.go
More file actions
209 lines (191 loc) · 7.23 KB
/
Copy pathshellenum.go
File metadata and controls
209 lines (191 loc) · 7.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
package main
import (
"fmt"
"os"
"path/filepath"
"syscall"
"unsafe"
"github.com/go-ole/go-ole"
"golang.org/x/sys/windows"
)
// ---------------- Shell 目录枚举(IShellFolder::EnumObjects) ----------------
// 与资源管理器同款枚举路径。对 OneDrive 等"云占位目录"(reparse tag 0x9000xxxx1A),
// Win32 FindFirstFile(os.ReadDir)枚举会触发 OneDrive 下载目录全部内容;
// 而 shell API 枚举走 OneDrive 的虚拟化路径,只返回元数据,不触发下载。
const (
shcontFolders = 0x20 // SHCONTF_FOLDERS
shcontNonFolders = 0x40 // SHCONTF_NONFOLDERS
shcontIncludeHidden = 0x80 // SHCONTF_INCLUDEHIDDEN
sfgaoFolder = 0x20000000 // SFGAO_FOLDER
sfgaoStream = 0x04000000 // SFGAO_STREAM
sfgaoFilesystem = 0x40000000 // SFGAO_FILESYSTEM
shgdnForParsing = 0x8000 // SHGDN_FORPARSING
)
var (
shellILCombine = syscall.NewLazyDLL("shell32.dll").NewProc("ILCombine")
shellGetPathFromIDList = syscall.NewLazyDLL("shell32.dll").NewProc("SHGetPathFromIDListW")
)
// isReparseDir 检测目录是否为 reparse point(OneDrive 云占位目录、符号链接、junction 等)。
// 用 FindFirstFile 读属性(Go os.Lstat 的 REPARSE 位会被 OneDrive/cldflt 隐藏);
// 单次 FindFirstFile 只取目录自身属性、不枚举内容,不会触发云盘下载。
func isReparseDir(path string) bool {
var fd windows.Win32finddata
p, _ := windows.UTF16PtrFromString(path)
h, err := windows.FindFirstFile(p, &fd)
if err != nil {
return false
}
windows.FindClose(h)
return fd.FileAttributes&windows.FILE_ATTRIBUTE_DIRECTORY != 0 &&
fd.FileAttributes&windows.FILE_ATTRIBUTE_REPARSE_POINT != 0
}
// listDirShell 用 IShellFolder::EnumObjects 枚举目录(资源管理器同款)。
// 失败返回 error,由调用方回退 os.ReadDir。
func listDirShell(path string) ([]FileEntry, error) {
leave := shellCOMEnter()
defer leave()
p, _ := syscall.UTF16PtrFromString(path)
var absPidl uintptr
hr, _, _ := shellParseDisplayName.Call(uintptr(unsafe.Pointer(p)), 0, uintptr(unsafe.Pointer(&absPidl)), 0, 0)
if absPidl != 0 {
defer procCoTaskMemFree.Call(absPidl)
}
if hr != 0 || absPidl == 0 {
return nil, fmt.Errorf("SHParseDisplayName 失败:0x%08x", uint32(hr))
}
iidFolder, _ := ole.CLSIDFromString("{000214E6-0000-0000-C000-000000000046}")
var parentFolder uintptr
var childPidl uintptr
hr, _, _ = shellBindToParent.Call(absPidl, uintptr(unsafe.Pointer(iidFolder)), uintptr(unsafe.Pointer(&parentFolder)), uintptr(unsafe.Pointer(&childPidl)))
if parentFolder != 0 {
defer callComVtable(parentFolder, 2)
}
if hr != 0 || childPidl == 0 {
return nil, fmt.Errorf("SHBindToParent 失败:0x%08x", uint32(hr))
}
// BindToObject 绑定目标目录自身的 IShellFolder —— vtable 索引 5。
// 注意:不能直接 EnumObjects(parentFolder)(那是父目录的内容);
// 且 OneDrive 等云盘路径被 shell 扩展接管为虚拟命名空间,
// 必须经 BindToObject 落到目标节点(云端视图,只读元数据不触发下载)。
var folder uintptr
hr = callComVtable(parentFolder, 5, childPidl, 0, uintptr(unsafe.Pointer(iidFolder)), uintptr(unsafe.Pointer(&folder)))
if folder != 0 {
defer callComVtable(folder, 2)
}
if hr != 0 {
return nil, fmt.Errorf("BindToObject 失败:0x%08x", uint32(hr))
}
// IShellFolder::EnumObjects(hwnd, grfFlags, ppenumIDList) —— vtable 索引 4
var enum uintptr
hr = callComVtable(folder, 4, 0, shcontFolders|shcontNonFolders|shcontIncludeHidden, uintptr(unsafe.Pointer(&enum)))
if enum != 0 {
defer callComVtable(enum, 2)
}
if hr != 0 {
return nil, fmt.Errorf("EnumObjects 失败:0x%08x", uint32(hr))
}
entries := []FileEntry{}
for {
// IEnumIDList::Next(celt, rgelt, pceltFetched) —— vtable 索引 3
var pidl uintptr
var fetched uint32
hr = callComVtable(enum, 3, 1, uintptr(unsafe.Pointer(&pidl)), uintptr(unsafe.Pointer(&fetched)))
if hr != 0 || fetched == 0 || pidl == 0 {
if pidl != 0 {
procCoTaskMemFree.Call(pidl)
}
break
}
// IShellFolder::GetAttributesOf —— vtable 索引 9:云端未下载条目本地无实体,
// 目录标志只能从 shell 属性获取
var sfgao uint32 = sfgaoFolder | sfgaoStream | sfgaoFilesystem
hrAttr := callComVtable(folder, 9, 1, uintptr(unsafe.Pointer(&pidl)), uintptr(unsafe.Pointer(&sfgao)))
shellIsDir := hrAttr == 0 && sfgao&sfgaoFolder != 0
full, ok := shellPidlToPath(folder, absPidl, pidl)
procCoTaskMemFree.Call(pidl)
if !ok {
continue
}
e := FileEntry{
Name: filepath.Base(full),
Path: full,
IsDir: shellIsDir,
Size: -1,
ModTime: "未知",
ModTimeUnix: 0,
}
// 元数据用 Lstat 获取(占位符属性本地可用,不触发云盘下载);
// 云端未下载条目本地无实体,Lstat 失败时保留 shell 属性判断
if fi, err := os.Lstat(full); err == nil {
e.IsDir = fi.IsDir()
e.ModTime = fi.ModTime().Format("2006-01-02 15:04")
e.ModTimeUnix = fi.ModTime().Unix()
if !fi.IsDir() {
e.Size = fi.Size()
}
}
if e.IsDir {
e.IconType = "folder"
} else {
e.IconType = iconType(full)
}
e.Icon = getSystemIcon(full, e.IsDir)
entries = append(entries, e)
}
return entries, nil
}
// shellStrret STRRET 结构(x64 布局必须与 C 一致:uType 4 + pad 4 + union 264 = 272 字节,
// 否则 shell 写入 cStr[260] 会写穿 Go 栈缓冲)
// union:STRRET_WSTR=0 → 前 8 字节为动态宽字符串指针;STRRET_OFFSET=1 → pidl 内 ANSI 偏移
// (仅 SHGDN_INFOLDER 使用,FORPARSING 不返回,不支持);STRRET_CSTR=2 → 内嵌 ANSI 数组
type shellStrret struct {
uType uint32
_ uint32
union [264]byte
}
// shellStrretToString 解析 STRRET 为字符串(STRRET_WSTR 调用方负责 CoTaskMemFree,此处代释)
func shellStrretToString(s *shellStrret) string {
switch s.uType {
case 0:
pOleStr := *(*uintptr)(unsafe.Pointer(&s.union[0]))
str := utf16PtrToString(pOleStr)
procCoTaskMemFree.Call(pOleStr)
return str
case 1:
return "" // STRRET_OFFSET:仅 SHGDN_INFOLDER 产生,本路径(FORPARSING)不会出现
case 2:
// STRRET_CSTR:结构内嵌 ANSI 数组,上限 260(MAX_PATH)防越界
base := unsafe.Pointer(&s.union[0])
var buf []byte
for i := uint32(0); i < 260; i++ {
c := *(*byte)(unsafe.Add(base, uintptr(i)))
if c == 0 {
break
}
buf = append(buf, c)
}
return string(buf)
}
return ""
}
// shellPidlToPath 组合父绝对 PIDL 与子相对 PIDL,转文件系统路径。
// 先用 SHGetPathFromIDListW(MAX_PATH 限制);超长路径回退
// GetDisplayNameOf(SHGDN_FORPARSING)(动态分配,不受长度限制)。
func shellPidlToPath(folder, parent, child uintptr) (string, bool) {
abs, _, _ := shellILCombine.Call(parent, child)
if abs != 0 {
defer procCoTaskMemFree.Call(abs)
var buf [windows.MAX_PATH]uint16
if r, _, _ := shellGetPathFromIDList.Call(abs, uintptr(unsafe.Pointer(&buf[0]))); r != 0 {
return syscall.UTF16ToString(buf[:]), true
}
}
// 超长路径回退:IShellFolder::GetDisplayNameOf —— vtable 索引 11
var sr shellStrret
hr := callComVtable(folder, 11, child, shgdnForParsing, uintptr(unsafe.Pointer(&sr)))
if hr != 0 {
return "", false
}
s := shellStrretToString(&sr)
return s, s != ""
}