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; } public byte DriveIdx { get; set; } } [Serializable] public class CacheData { public List Entries { get; set; } = new(); public List DriveRoots { get; set; } = new(); public List 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 Entries = new(); public List NameArena = new(); public List NameLowerArena = new(); public List NameCache = new(); public List NameLowerCache = new(); public List<(ulong fileRef, int idx)> RefLookup = new(); public List DriveRoots = new(); public List 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); } } /// /// Sort entries by lowercase name and rebuild lookup tables. /// Matches Rust IndexStore::finalize(). /// 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(Entries.Count); var sortedNames = new List(Entries.Count); var sortedLower = new List(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(DriveRoots), Checkpoints = new List(Checkpoints) }; } public static IndexStore FromCache(CacheData cache) { int count = cache.Entries.Count; var store = new IndexStore { DriveRoots = new List(cache.DriveRoots), Checkpoints = new List(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(); } }