引用
起因
需求是这样的:需要将IE中的全部书签同步到Edge,并且保留IE中的特定顺序。(PS:不知道为什么,现在还有人有同步IE书签的需求🤔)
因为原始的项目代码是个缝合怪, 程序的本地是基于WPF,一部分书签合并的逻辑在WPF这,另一部分读取IE书签及顺序的代码在一个C++写的Dll中, 还有个一部分调用了另外的C++库来处理一些借口。
接手到这个代码之后,发现真是一层套一层,一环连一环,真像一个中国结 🤣,所以 就动手把所有逻辑用C# 重写了。
分析
Chromium版的Microsoft Edge的书签很好找到
开发版的书签文件路径为:
C:\Users\{用户名}\AppData\Local\Microsoft\Edge Dev\User Data\Default\Bookmarks
稳定版的书签文件路径为:
C:\Users\{用户名}\AppData\Local\Microsoft\Edge\User Data\Default\Bookmarks
Edge的书签文件很好处理,就是一个Json文件,反序列化后处理就完事了。书签的顺序和Children内的对象排序是一样的。
需要注意的是里面的date_added,是一个时间戳,转化为C#的DateTime的方式为:
var dateTime= new DateTime(1601, 1, 1).AddTicks(value * 10); //value即为要转换的值
IE的话,书签在
C:\Users\{用户名}\Favorites
如果不考虑顺序的话,直接递归遍历文件夹内所有Url文件,然后提取出地址就好,问题在这里并没有保存书签在IE内的顺序。
那么顺序在哪里呢?经过一顿的Google,在Chromium
中找到了一个文件
Chromium传送门
由上可知,IE收藏夹的排序信息保存在注册表的以下路径:HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\MenuOrder
引用以上连接的一段话:
// If the order of the items in a folder is customized by user, the order is
// recorded in the REG_BINARY value named “Order” of the corresponding key.
// The content of the “Order” value is a raw binary dump of an array of the
// following data structure
// struct {
// uint32_t size; // Note that ITEMIDLIST is variably-sized.
// uint32_t sort_index; // 0 means this is the first item, 1 the second,
// …
// ITEMIDLIST item_id;
// };
// where each item_id should correspond to a favorites link file (*.url) in the current folder.
(ps:不知道这是怎么分析的,希望哪位知道的能指导一二😉)
代码
参考上面的代码就很好写咯,下面直接给出C#的核心代码:
这其中用到的Model为:
public class IEBookMarkModel
{
public string FullName { get; set; }
public int SortIndex { get; set; }
public BookMarkType BookMarkType { get; set; }
public List<IEBookMarkModel> SubBookMark { get; set; }
}
public enum BookMarkType
{
Unknown,
URL,
Folder
}
[DllImport("shell32.dll", SetLastError = true, ExactSpelling = true, ThrowOnUnmappableChar = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SHGetPathFromIDListW(IntPtr pidl, [MarshalAs(UnmanagedType.LPWStr)]StringBuilder pszPath);
public static (bool, List<IEBookMarkModel>,string message) GetBookMarkSort(string registryPath,string path)
{
byte[] value = GetOrderFromRegistry(registryPath);
if (value == null || value.Length == 0) return (false, null,"can not access registry value");
int valueSize = value.Length;
var pVaule = Marshal.AllocHGlobal(valueSize);
Marshal.Copy(value, 0, pVaule, value.Length);
if (!BinaryRead(ref pVaule, valueSize, ItemCountOffset, out int itemCount)) return (false, null,"can not get item count");
List<IEBookMarkModel> result = new List<IEBookMarkModel>();
int baseOffset = ItemListBeginOffset;
for (int i = 0; i < itemCount; i++)
{
if (!BinaryRead(ref pVaule, valueSize,baseOffset+ItemSizeOffset, out int itemSize)
|| baseOffset + itemSize > valueSize
|| baseOffset + itemSize <= baseOffset) return (false, null,"can not get item size");
if (!BinaryRead(ref pVaule, valueSize, baseOffset+ ItemSortIndexOffset, out int sortIndex)) return (false, null,"can not get sort index");
if (!BinaryRead(ref pVaule, valueSize, baseOffset+ItemIDListOffset, out short idListSize)) return (false, null,"can not get IDList size");
var pIdList = ReadIDList(ref pVaule, valueSize, baseOffset + ItemIDListOffset, idListSize);
StringBuilder fullFileName = new StringBuilder(666); //这里其实用不到申请这么多的内存,只是为了666 😁
//这里使用 SHGetPathFromIDListW 根据IDList获取相关文件名
if (pIdList == IntPtr.Zero || !SHGetPathFromIDListW(pIdList, fullFileName)) return (false, null, "can not get IDList or SHGetPathFromIDListW failed");
var bookMarkPath = ($"{path}\\{GetName(fullFileName.ToString())}").ToLower();
OrderList.Add(bookMarkPath, sortIndex);
result.Add(new IEBookMarkModel() { FullName = bookMarkPath, SortIndex = sortIndex,BookMarkType=Utilities.GetBookMarkType(bookMarkPath)});
baseOffset += itemSize;
}
Marshal.FreeHGlobal(pVaule);
return (true,result,"fuck success");
}
static IntPtr ReadIDList(ref IntPtr source, int sourceSize, int offset, int idListSize)
{
int head = 0;
while(true)
{
short cb = 0;
if (head > idListSize || !BinaryRead(ref source, sourceSize, offset + head, out cb)) return IntPtr.Zero;
if (cb == 0) break;
head += cb;
}
return source + offset;
}
//这里处理的不怎么好,其实可以直接unsafe来处理
static bool BinaryRead<T>(ref IntPtr source,int sourceSize,int offset,out T value)
{
var type = typeof(T);
if((type==typeof(ushort)|| type == typeof(short)) && offset+sizeof(ushort)<sourceSize)
{
value =(T)(object)Marshal.ReadInt16(source, offset);
return true;
}else if((type == typeof(uint) || type == typeof(int)) && offset + sizeof(uint) < sourceSize)
{
value = (T)(object)Marshal.ReadInt32(source, offset);
return true;
}else if ((type == typeof(ulong) || type == typeof(long)) && offset + sizeof(ulong) < sourceSize)
{
value = (T)(object)Marshal.ReadInt64(source, offset);
return true;
}
else
{
value = (T)(object)0;
return false;
}
}
static byte[] GetOrderFromRegistry(string path)
{
using(var key=Registry.CurrentUser.OpenSubKey(path))
{
return (byte[])key?.GetValue(OrderValueName);
}
}
static string GetName(string path)
{
return Path.GetFileName(path);
}
结尾
获取完主文件夹的信息之后,剩下的只需要在 HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\MenuOrder
中递归搜索收藏文件夹内的排序信息。
如果相关的路径下没有Order的键值,那么就说明该子收藏夹无自定义的排序信息。