2026-10-22 04:18:27 -06:00

91 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""Read-only Mac WeChat storage inventory. Does not decrypt or read messages."""
import argparse
import json
import os
import platform
import plistlib
from datetime import datetime, timezone
from pathlib import Path
def probe(roots=None, max_files=300):
home = Path.home()
default_roots = [
home / 'Library/Containers/com.tencent.xinWeChat/Data/Documents',
home / 'Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat',
home / 'Documents/xwechat_files',
]
report = {'created_utc': datetime.now(timezone.utc).isoformat(), 'platform': platform.system(),
'architecture': platform.machine(), 'wechat_versions': [], 'roots': [], 'databases': [], 'warnings': [],
'notes': ['只读取文件名、大小、修改时间和前 16 字节;不读取消息正文、不获取密钥、不修改微信文件。',
'检测到 SQLite 文件头不代表消息内容已解码或可用。',
'未覆盖的自定义存储位置可通过 --root 指定。没有找到不代表没有记录。']}
for app in (Path('/Applications/WeChat.app'), home / 'Applications/WeChat.app'):
info = app / 'Contents/Info.plist'
if info.is_file():
try:
with info.open('rb') as file:
data = plistlib.load(file)
report['wechat_versions'].append({'app': str(app), 'version': data.get('CFBundleShortVersionString'), 'build': data.get('CFBundleVersion')})
except (OSError, ValueError, plistlib.InvalidFileException):
report['warnings'].append('无法读取应用版本:' + str(info))
visited = set()
def error(exc):
report['warnings'].append('无法访问目录(可能需要文件访问权限):' + str(exc.filename))
for root in roots or default_roots:
root = Path(root).expanduser().absolute()
report['roots'].append({'path': str(root), 'exists': root.exists()})
if not root.exists():
continue
for directory, dirs, files in os.walk(root, followlinks=False, onerror=error):
relative = Path(directory).relative_to(root)
dirs[:] = [d for d in dirs if not (Path(directory)/d).is_symlink() and d not in ('Cache', 'Caches', 'FileStorage', 'video', 'image', 'temp')]
if len(relative.parts) >= 8:
dirs[:] = []
for name in files:
if not name.lower().endswith(('.db', '.sqlite', '.sqlite3')):
continue
path = Path(directory) / name
if path.is_symlink() or str(path) in visited:
continue
visited.add(str(path))
if len(report['databases']) >= max_files:
report['warnings'].append(f'达到 {max_files} 个数据库上限,报告不完整。')
return report
try:
stat = path.stat()
with path.open('rb') as file:
header = file.read(16)
report['databases'].append({'path': str(path), 'size': stat.st_size,
'modified_utc': datetime.fromtimestamp(stat.st_mtime, timezone.utc).isoformat(),
'format': 'sqlite_header' if header == b'SQLite format 3\x00' else 'encrypted_or_unknown',
'wal_present': Path(str(path) + '-wal').exists()})
except OSError:
report['warnings'].append('无法读取数据库头:' + str(path))
return report
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--root', action='append', help='额外/指定存储根目录,可重复;指定后替代默认目录')
parser.add_argument('--output', type=Path, default=Path('mac-wechat-diagnostic.json'))
args = parser.parse_args()
if platform.system() != 'Darwin' and not args.root:
parser.error('请在目标 Mac 上运行。测试时可显式指定 --root。')
report = probe(args.root)
# Exclusive creation: never overwrite an existing user file or database.
with args.output.open('x', encoding='utf-8') as file:
json.dump(report, file, ensure_ascii=False, indent=2)
try:
args.output.chmod(0o600)
except OSError:
pass
print('Diagnostic written to:', args.output.resolve())
print('Database files:', len(report['databases']))
print('Report contains local paths. Review before sharing.')
if __name__ == '__main__':
main()