Fix IndexStore: rename Finalize() to CompleteIndex() to fix CS0465 collision with Object.Finalize
daf680a verified | using System; | |
| using System.Collections.Generic; | |
| using System.Linq; | |
| using System.Text; | |
| namespace FastSeekWpf.Core; | |
| [] | |
| public class CachedEntry | |
| { | |
| public ulong FileRef { get; set; } | |
| public ulong ParentRef { get; set; } | |
| public string Name { get; set; } = string.Empty; | |
| public FileKind Kind { get; set; } | |
| public byte DriveIdx { get; set; } | |
| } | |
| [] | |
| public class CacheData | |
| { | |
| public List<CachedEntry> Entries { get; set; } = new(); | |
| public List<string> DriveRoots { get; set; } = new(); | |
| public List<JournalCheckpoint> Checkpoints { get; set; } = new(); | |
| } | |
| public struct IndexEntry | |
| { | |
| public ulong FileRef; | |
| public ulong ParentRef; | |
| public uint NameOff; | |
| public uint NameLowerOff; | |
| public ushort NameLen; | |
| public ushort NameLowerLen; | |
| public byte Flags; // bit 0 = is_dir | |
| public byte DriveIdx; // index into IndexStore.DriveRoots | |
| public readonly bool IsDir => (Flags & 1) != 0; | |
| public readonly FileKind Kind => IsDir ? FileKind.Directory : FileKind.File; | |
| } | |
| public class IndexStore | |
| { | |
| public List<IndexEntry> Entries = new(); | |
| public List<byte> NameArena = new(); | |
| public List<byte> NameLowerArena = new(); | |
| public List<string> NameCache = new(); | |
| public List<string> NameLowerCache = new(); | |
| public List<(ulong fileRef, int idx)> RefLookup = new(); | |
| public List<string> DriveRoots = new(); | |
| public List<JournalCheckpoint> Checkpoints = new(); | |
| public int Count => Entries.Count; | |
| public string NameAt(int i) => NameCache[i]; | |
| public string NameLowerAt(int i) => NameLowerCache[i]; | |
| public string DriveRootAt(int i) => DriveRoots[Entries[i].DriveIdx]; | |
| public uint? LookupIdx(ulong fileRef) | |
| { | |
| int lo = 0, hi = RefLookup.Count - 1; | |
| while (lo <= hi) | |
| { | |
| int mid = (lo + hi) >>> 1; | |
| var r = RefLookup[mid].fileRef; | |
| if (r == fileRef) return (uint)RefLookup[mid].idx; | |
| if (r < fileRef) lo = mid + 1; | |
| else hi = mid - 1; | |
| } | |
| return null; | |
| } | |
| private void RebuildRefLookup() | |
| { | |
| RefLookup.Clear(); | |
| RefLookup.Capacity = Entries.Count; | |
| for (int i = 0; i < Entries.Count; i++) | |
| RefLookup.Add((Entries[i].FileRef, i)); | |
| RefLookup.Sort((a, b) => a.fileRef.CompareTo(b.fileRef)); | |
| } | |
| public void PopulateFromScan(ScanResult scan, string driveRoot) | |
| { | |
| byte driveIdx = 0; | |
| int existing = DriveRoots.IndexOf(driveRoot); | |
| if (existing >= 0) | |
| { | |
| driveIdx = (byte)existing; | |
| } | |
| else | |
| { | |
| driveIdx = (byte)DriveRoots.Count; | |
| DriveRoots.Add(driveRoot); | |
| } | |
| int count = scan.Records.Count; | |
| Entries.Capacity = Math.Max(Entries.Capacity, Entries.Count + count); | |
| NameArena.Capacity = Math.Max(NameArena.Capacity, NameArena.Count + count * 30); | |
| NameLowerArena.Capacity = Math.Max(NameLowerArena.Capacity, NameLowerArena.Count + count * 30); | |
| NameCache.Capacity = Math.Max(NameCache.Capacity, NameCache.Count + count); | |
| NameLowerCache.Capacity = Math.Max(NameLowerCache.Capacity, NameLowerCache.Count + count); | |
| foreach (var r in scan.Records) | |
| { | |
| int start = (int)r.NameOff; | |
| int len = r.NameLen; | |
| var nameChars = new char[len]; | |
| for (int i = 0; i < len; i++) | |
| nameChars[i] = scan.NameData[start + i]; | |
| string name = new string(nameChars); | |
| string nameLower = name.ToLowerInvariant(); | |
| byte[] nameBytes = Encoding.UTF8.GetBytes(name); | |
| byte[] lowerBytes = Encoding.UTF8.GetBytes(nameLower); | |
| uint nOff = (uint)NameArena.Count; | |
| ushort nLen = (ushort)nameBytes.Length; | |
| NameArena.AddRange(nameBytes); | |
| uint nlOff = (uint)NameLowerArena.Count; | |
| ushort nlLen = (ushort)lowerBytes.Length; | |
| NameLowerArena.AddRange(lowerBytes); | |
| Entries.Add(new IndexEntry | |
| { | |
| FileRef = r.FileRef, | |
| ParentRef = r.ParentRef, | |
| NameOff = nOff, | |
| NameLowerOff = nlOff, | |
| NameLen = nLen, | |
| NameLowerLen = nlLen, | |
| Flags = r.IsDir ? (byte)1 : (byte)0, | |
| DriveIdx = driveIdx | |
| }); | |
| NameCache.Add(name); | |
| NameLowerCache.Add(nameLower); | |
| } | |
| } | |
| /// <summary> | |
| /// Sort entries by lowercase name and rebuild lookup tables. | |
| /// Matches Rust IndexStore::finalize(). | |
| /// </summary> | |
| public void CompleteIndex() | |
| { | |
| var indices = Enumerable.Range(0, Entries.Count).ToArray(); | |
| Array.Sort(indices, (a, b) => string.CompareOrdinal(NameLowerCache[a], NameLowerCache[b])); | |
| var sortedEntries = new List<IndexEntry>(Entries.Count); | |
| var sortedNames = new List<string>(Entries.Count); | |
| var sortedLower = new List<string>(Entries.Count); | |
| foreach (var i in indices) | |
| { | |
| sortedEntries.Add(Entries[i]); | |
| sortedNames.Add(NameCache[i]); | |
| sortedLower.Add(NameLowerCache[i]); | |
| } | |
| Entries = sortedEntries; | |
| NameCache = sortedNames; | |
| NameLowerCache = sortedLower; | |
| RebuildRefLookup(); | |
| NameArena.TrimExcess(); | |
| NameLowerArena.TrimExcess(); | |
| } | |
| public CacheData ToCache() | |
| { | |
| return new CacheData | |
| { | |
| Entries = Entries.Select((e, i) => new CachedEntry | |
| { | |
| FileRef = e.FileRef, | |
| ParentRef = e.ParentRef, | |
| Name = NameAt(i), | |
| Kind = e.Kind, | |
| DriveIdx = e.DriveIdx | |
| }).ToList(), | |
| DriveRoots = new List<string>(DriveRoots), | |
| Checkpoints = new List<JournalCheckpoint>(Checkpoints) | |
| }; | |
| } | |
| public static IndexStore FromCache(CacheData cache) | |
| { | |
| int count = cache.Entries.Count; | |
| var store = new IndexStore | |
| { | |
| DriveRoots = new List<string>(cache.DriveRoots), | |
| Checkpoints = new List<JournalCheckpoint>(cache.Checkpoints) | |
| }; | |
| store.Entries.Capacity = count; | |
| store.NameArena.Capacity = count * 30; | |
| store.NameLowerArena.Capacity = count * 30; | |
| store.NameCache.Capacity = count; | |
| store.NameLowerCache.Capacity = count; | |
| store.RefLookup.Capacity = count; | |
| foreach (var c in cache.Entries) | |
| { | |
| string nameLower = c.Name.ToLowerInvariant(); | |
| byte[] nameBytes = Encoding.UTF8.GetBytes(c.Name); | |
| byte[] lowerBytes = Encoding.UTF8.GetBytes(nameLower); | |
| uint nOff = (uint)store.NameArena.Count; | |
| ushort nLen = (ushort)nameBytes.Length; | |
| store.NameArena.AddRange(nameBytes); | |
| uint nlOff = (uint)store.NameLowerArena.Count; | |
| ushort nlLen = (ushort)lowerBytes.Length; | |
| store.NameLowerArena.AddRange(lowerBytes); | |
| store.Entries.Add(new IndexEntry | |
| { | |
| FileRef = c.FileRef, | |
| ParentRef = c.ParentRef, | |
| NameOff = nOff, | |
| NameLowerOff = nlOff, | |
| NameLen = nLen, | |
| NameLowerLen = nlLen, | |
| Flags = c.Kind == FileKind.Directory ? (byte)1 : (byte)0, | |
| DriveIdx = c.DriveIdx | |
| }); | |
| store.NameCache.Add(c.Name); | |
| store.NameLowerCache.Add(nameLower); | |
| } | |
| store.RebuildRefLookup(); | |
| store.NameArena.TrimExcess(); | |
| store.NameLowerArena.TrimExcess(); | |
| return store; | |
| } | |
| public void Insert(FileRecord record) | |
| { | |
| string nameLower = record.Name.ToLowerInvariant(); | |
| var nameBytes = Encoding.UTF8.GetBytes(record.Name); | |
| var lowerBytes = Encoding.UTF8.GetBytes(nameLower); | |
| uint nOff = (uint)NameArena.Count; | |
| ushort nLen = (ushort)nameBytes.Length; | |
| NameArena.AddRange(nameBytes); | |
| uint nlOff = (uint)NameLowerArena.Count; | |
| ushort nlLen = (ushort)lowerBytes.Length; | |
| NameLowerArena.AddRange(lowerBytes); | |
| byte driveIdx = 0; | |
| if (DriveRoots.Count == 0) | |
| DriveRoots.Add("C:\\"); | |
| var entry = new IndexEntry | |
| { | |
| FileRef = record.FileRef, | |
| ParentRef = record.ParentRef, | |
| NameOff = nOff, | |
| NameLowerOff = nlOff, | |
| NameLen = nLen, | |
| NameLowerLen = nlLen, | |
| Flags = record.Kind == FileKind.Directory ? (byte)1 : (byte)0, | |
| DriveIdx = driveIdx | |
| }; | |
| int pos = 0; | |
| for (; pos < Entries.Count; pos++) | |
| { | |
| if (string.CompareOrdinal(nameLower, NameLowerAt(pos)) < 0) | |
| break; | |
| } | |
| Entries.Insert(pos, entry); | |
| NameCache.Insert(pos, record.Name); | |
| NameLowerCache.Insert(pos, nameLower); | |
| RebuildRefLookup(); | |
| } | |
| public void Remove(ulong fileRef) | |
| { | |
| int idx = -1; | |
| for (int i = 0; i < Entries.Count; i++) | |
| { | |
| if (Entries[i].FileRef == fileRef) | |
| { | |
| idx = i; | |
| break; | |
| } | |
| } | |
| if (idx < 0) return; | |
| Entries.RemoveAt(idx); | |
| NameCache.RemoveAt(idx); | |
| NameLowerCache.RemoveAt(idx); | |
| RebuildRefLookup(); | |
| } | |
| public void Rename(ulong oldRef, FileRecord newRecord) | |
| { | |
| Remove(oldRef); | |
| Insert(newRecord); | |
| } | |
| public void ApplyMove(ulong fileRef, ulong newParentRef, string name, FileKind kind) | |
| { | |
| Remove(fileRef); | |
| Insert(new FileRecord | |
| { | |
| FileRef = fileRef, | |
| ParentRef = newParentRef, | |
| Name = name, | |
| Kind = kind | |
| }); | |
| } | |
| public string BuildPath(ulong fileRef) | |
| { | |
| string[] components = new string[64]; | |
| int compCount = 0; | |
| ulong current = fileRef; | |
| byte driveIdx = 0; | |
| for (int i = 0; i < 64; i++) | |
| { | |
| var idx = LookupIdx(current); | |
| if (idx == null) break; | |
| int eIdx = (int)idx; | |
| components[compCount++] = NameAt(eIdx); | |
| var entry = Entries[eIdx]; | |
| driveIdx = entry.DriveIdx; | |
| if (entry.ParentRef == current) break; | |
| current = entry.ParentRef; | |
| } | |
| string driveRoot = driveIdx < DriveRoots.Count ? DriveRoots[driveIdx] : "C:\\"; | |
| var sb = new StringBuilder(driveRoot.Length + compCount * 32); | |
| sb.Append(driveRoot); | |
| for (int i = compCount - 1; i >= 0; i--) | |
| { | |
| if (sb.Length > 0 && sb[sb.Length - 1] != '\\' && sb[sb.Length - 1] != '/') | |
| sb.Append('\\'); | |
| sb.Append(components[i]); | |
| } | |
| return sb.ToString(); | |
| } | |
| } | |