using System.Collections.Generic;
namespace SabreTools.Wrappers
{
///
/// Lightweight GameCube / Wii File-System Table (FST) reader used by
/// to distinguish real-file regions from junk.
///
/// Mirrors Dolphin's FileSystemGCWii offset-to-file-info cache
/// (m_offset_file_info_cache).
///
public sealed class FileSystemTableReader
{
///
/// File entry with start and end byte offsets on disc
///
public struct FileEntry
{
public long FileStart;
public long FileEnd;
}
///
/// Sorted ascending by FileEnd for O(log n) upper_bound queries.
///
private readonly List _files;
private FileSystemTableReader(List files)
{
_files = files;
}
///
/// Parses a raw FST binary blob and returns a ,
/// or null if the data is too short or structurally invalid.
///
///
/// Raw FST bytes exactly as stored on disc (GameCube) or in decrypted
/// Wii partition data.
///
///
/// Bit-shift to convert raw file-offset fields to byte addresses.
/// 0 for GameCube (direct bytes); 2 for Wii (offset × 4).
///
public static FileSystemTableReader? TryParse(byte[] fstData, int offsetShift)
{
// Read the file system table
var table = Serialization.Readers.NintendoDisc.ParseFileSystemTable(fstData);
if (table is null)
return null;
// Filter out the entries to non-empty files only
var filtered = new List();
foreach (var entry in table.Entries)
{
// Directory entry
if ((entry.NameOffset & 0xFF000000) != 0)
continue;
// Empty file
if (entry.FileSize == 0)
continue;
long fileStart = entry.FileOffset << offsetShift;
long fileEnd = fileStart + entry.FileSize;
var fileEntry = new FileEntry
{
FileStart = fileStart,
FileEnd = fileEnd,
};
filtered.Add(fileEntry);
}
// Sort ascending by FileEnd so binary-search upper_bound works correctly.
filtered.Sort(delegate (FileEntry a, FileEntry b)
{
return a.FileEnd.CompareTo(b.FileEnd);
});
return new FileSystemTableReader(filtered);
}
///
/// Returns the file entry whose byte range contains ,
/// or null if no file does.
///
/// TODO: Determine how to use List.BinarySearch here
public FileEntry? FindFileInfo(long discOffset)
{
if (_files.Count == 0)
return null;
// Binary search: first index where _files[i].FileEnd > discOffset
int lo = 0, hi = _files.Count;
while (lo < hi)
{
int mid = (lo + hi) >> 1;
if (_files[mid].FileEnd <= discOffset)
lo = mid + 1;
else
hi = mid;
}
if (lo >= _files.Count)
return null;
var e = _files[lo];
if (e.FileStart <= discOffset)
return e;
return null;
}
///
/// Returns the smallest FileEnd value strictly greater than
/// , or null if there is none.
///
public long? FindNextFileEnd(long discOffset)
{
if (_files.Count == 0)
return null;
int lo = 0, hi = _files.Count;
while (lo < hi)
{
int mid = (lo + hi) >> 1;
if (_files[mid].FileEnd <= discOffset)
lo = mid + 1;
else
hi = mid;
}
return lo < _files.Count ? _files[lo].FileEnd : null;
}
///
/// Returns the smallest FileStart value strictly greater than
/// , or null if there is none.
///
public long? FindNextFileStart(long discOffset)
{
if (_files.Count == 0)
return null;
// Sort is by FileEnd; scan all entries whose FileEnd > discOffset
int lo = 0, hi = _files.Count;
while (lo < hi)
{
int mid = (lo + hi) >> 1;
if (_files[mid].FileEnd <= discOffset)
lo = mid + 1;
else
hi = mid;
}
long? best = null;
for (int i = lo; i < _files.Count; i++)
{
long start = _files[i].FileStart;
if (start <= discOffset)
continue;
if (best == null || start < best.Value)
best = start;
}
return best;
}
}
}