using System.Collections.Generic;
using SabreTools.Numerics.Extensions;
namespace SabreTools.IO.Compression.RVZPack
{
///
/// 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
{
///
/// 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 = ParseFileSystemTable(fstData, offsetShift);
if (table is null)
return null;
// Filter out the entries to non-empty files only
var filtered = new List();
foreach (var entry in table)
{
// Directory entry
if (entry.IsDirectory)
continue;
// Empty file
if (entry.FileStart == entry.FileEnd)
continue;
filtered.Add(entry);
}
// 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;
}
///
/// Parse a byte array into a list of file entries
///
/// Byte array to parse
///
/// Bit-shift to convert raw file-offset fields to byte addresses.
/// 0 for GameCube (direct bytes); 2 for Wii (offset × 4).
///
/// Filled file entry list on success, null on error
/// Adapted from Serialization
public static List? ParseFileSystemTable(byte[] data, int offsetShift)
{
// Check that the root entry exists
if (data.Length < 12)
return null;
// Read the root entry first
int offset = 0;
_ = data.ReadBytes(ref offset, 8);
uint entryCount = data.ReadUInt32BigEndian(ref offset);
if (entryCount < 1 || (entryCount * 12) > data.Length)
return null;
// Read all entries
offset = 0;
var obj = new List();
for (int i = 0; i < entryCount; i++)
{
var entry = ParseFileSystemTableEntry(data, ref offset, offsetShift);
obj.Add(entry);
}
return obj;
}
///
/// Parse a byte array into a FileSystemTableEntry
///
/// Byte array to parse
///
/// Bit-shift to convert raw file-offset fields to byte addresses.
/// 0 for GameCube (direct bytes); 2 for Wii (offset × 4).
///
/// Filled FileSystemTableEntry on success, null on error
/// Adapted from Serialization
public static FileEntry ParseFileSystemTableEntry(byte[] data, ref int offset, int offsetShift)
{
var obj = new FileEntry();
uint nameOffset = data.ReadUInt32BigEndian(ref offset);
obj.IsDirectory = (nameOffset & 0xFF000000) != 0;
uint fileOffset = data.ReadUInt32BigEndian(ref offset);
uint fileSize = data.ReadUInt32BigEndian(ref offset);
obj.FileStart = fileOffset << offsetShift;
obj.FileEnd = obj.FileStart + fileSize;
return obj;
}
}
}