using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace FastSeekWpf.Core; [Serializable] 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; } } [Serializable] public class CacheData { public List Entries { get; set; } = new(); public string DriveRoot { get; set; } = string.Empty; public List Checkpoints { get; set; } = new(); } // Compact in-memory entry — matches Rust IndexEntry exactly 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 readonly bool IsDir => (Flags & 1) != 0; public readonly FileKind Kind => IsDir ? FileKind.Directory : FileKind.File; } // Main index store — matches Rust IndexStore exactly (single drive_root) public class IndexStore { public List Entries = new(); public List NameArena = new(); // UTF-8 name bytes public List NameLowerArena = new(); // UTF-8 lowercase name bytes public List<(ulong fileRef, int idx)> RefLookup = new(); // sorted by file_ref public string DriveRoot = string.Empty; public List Checkpoints = new(); public int Count => Entries.Count; // Arena accessors — matches Rust name() and name_lower() public string Name(IndexEntry e) { return Encoding.UTF8.GetString(NameArena.ToArray(), (int)e.NameOff, e.NameLen); } public string NameLower(IndexEntry e) { return Encoding.UTF8.GetString(NameLowerArena.ToArray(), (int)e.NameLowerOff, e.NameLowerLen); } // Ref lookup (binary search) — matches Rust lookup_idx() 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)); } // Populate from MFT scan — matches Rust populate_from_scan() public void PopulateFromScan(ScanResult scan, string driveRoot) { this.DriveRoot = driveRoot; int count = scan.Records.Count; Entries.Capacity = count; NameArena.Capacity = count * 30; NameLowerArena.Capacity = count * 30; 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(); uint nOff = (uint)NameArena.Count; ushort nLen = (ushort)name.Length; NameArena.AddRange(Encoding.UTF8.GetBytes(name)); uint nlOff = (uint)NameLowerArena.Count; ushort nlLen = (ushort)nameLower.Length; NameLowerArena.AddRange(Encoding.UTF8.GetBytes(nameLower)); 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 }); } } // Sort entries by lowercase name and rebuild lookup — matches Rust finalize() public void Finalize() { // Rust uses sort_unstable_by with store_ptr for name_lower comparison. // We use a stable sort with proper key extraction. var indices = Enumerable.Range(0, Entries.Count).ToArray(); Array.Sort(indices, (a, b) => { var ea = Entries[a]; var eb = Entries[b]; return string.CompareOrdinal( Encoding.UTF8.GetString(NameLowerArena.ToArray(), (int)ea.NameLowerOff, ea.NameLowerLen), Encoding.UTF8.GetString(NameLowerArena.ToArray(), (int)eb.NameLowerOff, eb.NameLowerLen)); }); var sorted = new List(Entries.Count); foreach (var i in indices) sorted.Add(Entries[i]); Entries = sorted; RebuildRefLookup(); NameArena.TrimExcess(); NameLowerArena.TrimExcess(); } // Cache serialization — matches Rust to_cache() public CacheData ToCache() { return new CacheData { Entries = Entries.Select(e => new CachedEntry { FileRef = e.FileRef, ParentRef = e.ParentRef, Name = Name(e), Kind = e.Kind }).ToList(), DriveRoot = DriveRoot, Checkpoints = new List(Checkpoints) }; } // Cache deserialization — matches Rust from_cache() public static IndexStore FromCache(CacheData cache) { int count = cache.Entries.Count; var store = new IndexStore { DriveRoot = cache.DriveRoot, Checkpoints = new List(cache.Checkpoints) }; store.Entries.Capacity = count; store.NameArena.Capacity = count * 30; store.NameLowerArena.Capacity = count * 30; foreach (var c in cache.Entries) { string nameLower = c.Name.ToLowerInvariant(); uint nOff = (uint)store.NameArena.Count; ushort nLen = (ushort)c.Name.Length; store.NameArena.AddRange(Encoding.UTF8.GetBytes(c.Name)); uint nlOff = (uint)store.NameLowerArena.Count; ushort nlLen = (ushort)nameLower.Length; store.NameLowerArena.AddRange(Encoding.UTF8.GetBytes(nameLower)); 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 }); } store.RebuildRefLookup(); store.NameArena.TrimExcess(); store.NameLowerArena.TrimExcess(); return store; } // Live mutations — match Rust insert(), remove(), rename(), apply_move() public void Insert(FileRecord record) { string nameLower = record.Name.ToLowerInvariant(); uint nOff = (uint)NameArena.Count; ushort nLen = (ushort)record.Name.Length; NameArena.AddRange(Encoding.UTF8.GetBytes(record.Name)); uint nlOff = (uint)NameLowerArena.Count; ushort nlLen = (ushort)nameLower.Length; NameLowerArena.AddRange(Encoding.UTF8.GetBytes(nameLower)); 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 }; // Rust: partition_point by name_lower comparison int pos = 0; string key = nameLower; for (; pos < Entries.Count; pos++) { var e = Entries[pos]; string cmp = Encoding.UTF8.GetString(NameLowerArena.ToArray(), (int)e.NameLowerOff, e.NameLowerLen); if (string.CompareOrdinal(key, cmp) < 0) break; } Entries.Insert(pos, entry); RebuildRefLookup(); } public void Remove(ulong fileRef) { // Name bytes left as dead space in arena (negligible for rare deletes) Entries.RemoveAll(e => e.FileRef == fileRef); 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 }); } // Build path by walking parent chain — matches Rust build_path() public string BuildPath(ulong fileRef) { var components = new List(16); ulong current = fileRef; for (int i = 0; i < 64; i++) { var idx = LookupIdx(current); if (idx == null) break; var entry = Entries[(int)idx]; components.Add(Name(entry)); if (entry.ParentRef == current) break; current = entry.ParentRef; } components.Reverse(); var sb = new StringBuilder(DriveRoot); foreach (var comp in components) { if (sb.Length > 0 && sb[sb.Length - 1] != '\\' && sb[sb.Length - 1] != '/') sb.Append('\\'); sb.Append(comp); } return sb.ToString(); } }