finder-wpf / FastSeekWpf /Core /IndexStore.cs
anshdadhich's picture
IndexStore: Exact match to Rust β€” single drive_root string (not multi-drive), Name() and NameLower() arena accessors, Finalize() with sort_unstable_by_key behavior. Remove multi-drive DriveIdx/DriveRoots divergence.
36690d3 verified
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<CachedEntry> Entries { get; set; } = new();
public string DriveRoot { get; set; } = string.Empty;
public List<JournalCheckpoint> 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<IndexEntry> Entries = new();
public List<byte> NameArena = new(); // UTF-8 name bytes
public List<byte> 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<JournalCheckpoint> 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<IndexEntry>(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<JournalCheckpoint>(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<JournalCheckpoint>(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<string>(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();
}
}