niumasoftware/shortcut_target.py
2026-04-03 21:50:36 +08:00

124 lines
3.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""将用户选中的 .lnk / .url 解析为实际要存储的路径(目标程序或 URL"""
import base64
import json
import os
import re
import subprocess
import sys
def _looks_like_shell_lnk(path: str) -> bool:
"""Windows Shell Link 文件头为 4C 00 00 00扩展名异常时仍可识别"""
try:
with open(path, "rb") as f:
return f.read(4) == b"L\x00\x00\x00"
except OSError:
return False
def _read_internet_shortcut_url(path: str) -> str | None:
try:
with open(path, "r", encoding="utf-8", errors="ignore") as f:
for line in f:
s = line.strip()
if s[:4].upper() == "URL=":
return s[4:].strip()
except OSError:
pass
return None
def _resolve_windows_lnk(lnk_path: str) -> str | None:
if sys.platform != "win32":
return None
env = os.environ.copy()
env["LNK_B64"] = base64.b64encode(os.path.normpath(lnk_path).encode("utf-8")).decode(
"ascii"
)
ps = (
"$p=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($env:LNK_B64));"
"$sc=(New-Object -ComObject WScript.Shell).CreateShortcut($p);"
"@{ t=[string]$sc.TargetPath; a=[string]$sc.Arguments }|ConvertTo-Json -Compress"
)
try:
r = subprocess.run(
[
"powershell",
"-NoProfile",
"-Sta",
"-Command",
ps,
],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
env=env,
timeout=15,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
raw = (r.stdout or "").strip().lstrip("\ufeff")
if not raw:
return None
data = json.loads(raw)
target = (data.get("t") or "").strip()
args = (data.get("a") or "").strip()
if not target:
return None
low = target.lower()
if low.endswith("cmd.exe") or low.endswith("\\cmd.exe"):
for m in re.finditer(
r"([a-zA-Z]:[^\"'\s]+\.(?:bat|cmd))|\x22([a-zA-Z]:[^\"]+\.(?:bat|cmd))\x22",
args,
re.IGNORECASE,
):
cand = (m.group(1) or m.group(2) or "").strip('"')
if cand and os.path.isfile(cand):
return os.path.normpath(cand)
if os.path.isfile(target) or low.startswith("\\\\"):
return os.path.normpath(target)
return os.path.normpath(target)
except Exception:
return None
def path_for_storage(local_path: str) -> str:
"""
拖拽/选择文件时写入数据库的 path.lnk/.url 解析为目标,其它保持原路径。
解析失败时退回本地路径。
"""
if not local_path:
return local_path
local_path = os.path.normpath(local_path.strip())
ext = os.path.splitext(local_path)[1].lower()
if ext == ".url":
try:
if os.path.isfile(local_path):
url = _read_internet_shortcut_url(local_path)
if url:
return url
except (OSError, ValueError):
pass
return local_path
try:
is_file = os.path.isfile(local_path)
except (OSError, ValueError):
is_file = False
is_lnk = ext in (".lnk", ".ink") or (
is_file and _looks_like_shell_lnk(local_path)
)
if is_lnk and is_file:
resolved = _resolve_windows_lnk(local_path)
if resolved:
return resolved
return local_path
def item_name_from_sources(local_path: str, store_path: str) -> str:
"""列表显示名:仍用快捷方式/所选文件的文件名(无扩展名),与原来行为一致。"""
base = os.path.splitext(os.path.basename(local_path))[0]
return base or os.path.basename(store_path)