Livt packages
Livt.FileSystems
Version 0.0.2
Bounded, read-only FAT32 file streaming and partition discovery for Livt FPGA applications.
Published files (15)
src/Fat32Reader.lvt
namespace Livt.FileSystems
using Livt.IO
/**
* Internal scheduled parser phases.
*/
enum Fat32ReaderPhase
{
Discover
Mount
DirectoryEntry
DirectoryLink
ValidateLink
FileLink
FileData
Request
Finish
}
/**
* Read-only root-directory and file streaming over any IReadOnlyBlockDevice.
* One scheduled owner calls all methods; there is no hidden worker process.
* TryStart remounts the medium, Service advances bounded work, and output remains
* retained until consumed. FileResult.Success is the only successful end marker;
* bytes received before cancellation/error are an incomplete load and must be
* discarded by consumers requiring atomic data. No file-sized allocation occurs.
* The complete file chain is validated before streaming, including its final EOC.
* A generic sector provider controls BRAM/register storage and scheduled latency.
* The device and reader share the inherited clock/reset context; reset discards
* metadata and ownership without requiring payload RAM to be cleared.
*/
component Fat32Reader<STORAGE: IRam<byte> & new() = SequentialRam<byte, 512, MemoryStyle.Block>,
MAX_FILE_BYTES: uint = 16777216, MAX_DIRECTORY_CLUSTERS: uint = 256,
MAX_GPT_ENTRIES: int = 128>
{
static assert MAX_FILE_BYTES > 0, "File limit must be positive"
static assert MAX_DIRECTORY_CLUSTERS > 0, "Directory traversal must be bounded"
// Borrowed block device and owned sector store, partition parser and geometry.
device: IReadOnlyBlockDevice
sector: SectorBuffer<STORAGE>
partition: PartitionReader<MAX_GPT_ENTRIES>
geometry: Fat32Geometry
// Retained operation and directory-entry metadata.
public result: FileResult
public fileSize: uint
public bytesRead: uint
public entryReady: bool
public entrySize: uint
public entryAttributes: byte
entryName: byte[11]
wantedName: byte[11]
loading: bool
phase: Fat32ReaderPhase
// Branches queue state directly; Service owns the scheduled device calls.
finishResult: FileResult
requestedLba: uint
nextPhase: Fat32ReaderPhase
// One cluster/sector cursor, reused for root scan, chain validation and streaming.
cluster: uint
currentLink: uint
firstFileCluster: uint
sectorInCluster: uint
offset: int
directoryClusters: uint
clustersRemaining: uint
remaining: uint
/**
* Borrows exclusive block-device access and owns all filesystem state.
*/
new(device: IReadOnlyBlockDevice)
{
this.device = device
sector = new SectorBuffer<STORAGE>(device)
partition = new PartitionReader<MAX_GPT_ENTRIES>(sector)
geometry = new Fat32Geometry()
}
/**
* Sets one alias byte at index 0..10 while no job is active.
* Supply all eleven uppercase, space-padded 8+3 bytes before TryStart(true).
* Invalid indices and calls during an active job are ignored.
*/
public fn SetNameByte(index: int, value: byte)
{
if (result != FileResult.Active && index >= 0 && index < 11) {
wantedName[index] = value
}
}
/**
* Starts listing (load=false) or opening the configured alias (load=true).
* False changes nothing. Accepted operations retain their result until another
* accepted TryStart. Requires an idle, exclusively owned block device.
*/
public fn TryStart(load: bool) bool
{
if (result == FileResult.Active || !device.IsReady()
|| device.GetReadState() != BlockReadState.Idle || sector.busy) {
return false
}
result = FileResult.Active
entryReady = false
fileSize = 0
bytesRead = 0
loading = load
phase = Fat32ReaderPhase.Discover
if (device.GetBlockSize() != 512) {
finishResult = FileResult.Unsupported
entryReady = false
phase = Fat32ReaderPhase.Finish
} else {
partition.Start(device.GetBlockCount())
}
return true
}
/**
* Requests cancellation; Service drains any transport request before completion.
*/
public fn Cancel()
{
if (result == FileResult.Active) {
finishResult = FileResult.Cancelled
entryReady = false
phase = Fat32ReaderPhase.Finish
}
}
/**
* Returns one retained 8+3 name byte at index 0..10 while entryReady.
* Returns zero otherwise; raw media bytes may need sanitizing for display.
*/
public fn GetEntryName(index: int) byte
{
if (!entryReady || index < 0 || index >= 11) {
return 0
}
return entryName[index]
}
/**
* Releases a listed entry; false leaves state unchanged.
*/
public fn ConsumeEntry() bool
{
if (!entryReady) {
return false
}
entryReady = false
return true
}
/**
* Transfers at most one file byte, preserving it under caller backpressure.
* False yields zero and consumes nothing. Service must run between calls.
*/
public fn TryReadByte(value: out byte) bool
{
value = 0
if (result != FileResult.Active || phase != Fat32ReaderPhase.FileData || !sector.valid
|| sector.busy || offset >= 512 || remaining == 0 || !device.IsReady()) {
return false
}
value = sector.ReadByte(offset)
offset++
remaining--
bytesRead++
return true
}
/**
* Mounts the selected FAT32 volume and starts the root directory cursor.
*/
fn Mount()
{
if (!geometry.Parse(sector, partition.start, partition.length)) {
finishResult = FileResult.Corrupt
entryReady = false
phase = Fat32ReaderPhase.Finish
return
}
cluster = geometry.rootCluster
directoryClusters = 1
sectorInCluster = 0
offset = 0
requestedLba = geometry.ClusterLba(cluster)
nextPhase = Fat32ReaderPhase.DirectoryEntry
phase = Fat32ReaderPhase.Request
}
/**
* Parses one root entry; skips deleted, LFN and volume-label entries.
*/
fn DirectoryEntry()
{
if (entryReady) {
return
}
if (offset == 512) {
AdvanceSector(Fat32ReaderPhase.DirectoryEntry, Fat32ReaderPhase.DirectoryLink)
return
}
var first = sector.ReadByte(offset)
if (first == 0) {
if (loading) {
finishResult = FileResult.NotFound
entryReady = false
phase = Fat32ReaderPhase.Finish
} else {
finishResult = FileResult.Success
entryReady = false
phase = Fat32ReaderPhase.Finish
}
return
}
entryAttributes = sector.ReadByte(offset + 11)
if (first == 0xE5 || (entryAttributes & 8) != 0) {
offset = offset + 32
return
}
var matches = true
for (var index = 0; index < 11; index++) {
var value = sector.ReadByte(offset + index)
entryName[index] = value
if (value != wantedName[index]) {
matches = false
}
}
entrySize = sector.Read32(offset + 28)
if (loading && matches && (entryAttributes & 16) == 0) {
firstFileCluster = (sector.Read16(offset + 20) << 16) | sector.Read16(offset + 26)
fileSize = entrySize
OpenFile()
return
}
offset = offset + 32
if (!loading) {
entryReady = true
}
}
/**
* Starts exact-length chain validation before any payload is delivered.
*/
fn OpenFile()
{
if (fileSize > MAX_FILE_BYTES) {
finishResult = FileResult.LimitExceeded
entryReady = false
phase = Fat32ReaderPhase.Finish
return
}
if (fileSize == 0) {
if (firstFileCluster != 0) {
finishResult = FileResult.Corrupt
entryReady = false
phase = Fat32ReaderPhase.Finish
} else {
finishResult = FileResult.Success
entryReady = false
phase = Fat32ReaderPhase.Finish
}
return
}
if (!geometry.IsCluster(firstFileCluster)) {
finishResult = FileResult.Corrupt
entryReady = false
phase = Fat32ReaderPhase.Finish
return
}
var lastByte = fileSize - 1
var lastSector = lastByte >> 9
var lastCluster = lastSector >> geometry.clusterShift
clustersRemaining = lastCluster + 1
if (clustersRemaining > geometry.clusterCount) {
finishResult = FileResult.Corrupt
entryReady = false
phase = Fat32ReaderPhase.Finish
return
}
cluster = firstFileCluster
requestedLba = geometry.fatStart + (cluster >> 7)
nextPhase = Fat32ReaderPhase.ValidateLink
phase = Fat32ReaderPhase.Request
}
/**
* Moves within a cluster or fetches its FAT link at the cluster boundary.
*/
fn AdvanceSector(dataPhase: Fat32ReaderPhase, fatPhase: Fat32ReaderPhase)
{
sectorInCluster++
offset = 0
if (sectorInCluster < geometry.sectorsPerCluster) {
requestedLba = geometry.ClusterLba(cluster) + sectorInCluster
nextPhase = dataPhase
phase = Fat32ReaderPhase.Request
} else {
requestedLba = geometry.fatStart + (cluster >> 7)
nextPhase = fatPhase
phase = Fat32ReaderPhase.Request
}
}
/**
* Follows a bounded directory chain; corrupt cycles eventually hit the limit.
*/
fn DirectoryLink()
{
var next = currentLink
if (next >= 0x0FFFFFF8) {
if (loading) {
finishResult = FileResult.NotFound
entryReady = false
phase = Fat32ReaderPhase.Finish
} else {
finishResult = FileResult.Success
entryReady = false
phase = Fat32ReaderPhase.Finish
}
return
}
if (!geometry.IsCluster(next)) {
finishResult = FileResult.Corrupt
entryReady = false
phase = Fat32ReaderPhase.Finish
return
}
if (directoryClusters >= MAX_DIRECTORY_CLUSTERS || directoryClusters >= geometry.clusterCount) {
finishResult = FileResult.LimitExceeded
entryReady = false
phase = Fat32ReaderPhase.Finish
return
}
directoryClusters++
cluster = next
sectorInCluster = 0
offset = 0
requestedLba = geometry.ClusterLba(cluster)
nextPhase = Fat32ReaderPhase.DirectoryEntry
phase = Fat32ReaderPhase.Request
}
/**
* Requires exactly the number of clusters implied by file size, ending in EOC.
*/
fn ValidateLink()
{
var next = currentLink
clustersRemaining--
if (clustersRemaining == 0) {
if (next < 0x0FFFFFF8) {
finishResult = FileResult.Corrupt
entryReady = false
phase = Fat32ReaderPhase.Finish
return
}
cluster = firstFileCluster
sectorInCluster = 0
offset = 0
remaining = fileSize
requestedLba = geometry.ClusterLba(cluster)
nextPhase = Fat32ReaderPhase.FileData
phase = Fat32ReaderPhase.Request
} elif (!geometry.IsCluster(next)) {
finishResult = FileResult.Corrupt
entryReady = false
phase = Fat32ReaderPhase.Finish
} else {
cluster = next
requestedLba = geometry.fatStart + (cluster >> 7)
nextPhase = Fat32ReaderPhase.ValidateLink
phase = Fat32ReaderPhase.Request
}
}
/**
* Follows a validated file link; still rejects invalid live-medium changes.
*/
fn FileLink()
{
var next = currentLink
if (!geometry.IsCluster(next)) {
finishResult = FileResult.Corrupt
entryReady = false
phase = Fat32ReaderPhase.Finish
return
}
cluster = next
sectorInCluster = 0
offset = 0
requestedLba = geometry.ClusterLba(cluster)
nextPhase = Fat32ReaderPhase.FileData
phase = Fat32ReaderPhase.Request
}
/**
* Advances one bounded step; waits naturally on transport and consumer stalls.
*/
public fn Service()
{
if (result != FileResult.Active) {
return
}
if (phase == Fat32ReaderPhase.Finish) {
sector.Cancel()
sector.Service()
if (!sector.busy) {
result = finishResult
}
return
}
sector.Service()
if (!device.IsReady() || sector.failed) {
finishResult = FileResult.IoError
entryReady = false
phase = Fat32ReaderPhase.Finish
return
}
if (sector.busy) {
return
}
if (phase == Fat32ReaderPhase.DirectoryLink || phase == Fat32ReaderPhase.ValidateLink
|| phase == Fat32ReaderPhase.FileLink) {
currentLink = sector.Read32(((cluster & 127) * 4) as int) & 0x0FFFFFFF
}
match (phase)
{
Fat32ReaderPhase.Request =>
{
if (sector.Start(requestedLba)) {
phase = nextPhase
} else {
finishResult = FileResult.IoError
entryReady = false
phase = Fat32ReaderPhase.Finish
}
}
Fat32ReaderPhase.Discover =>
{
partition.Service()
if (partition.result == FileResult.Success) {
requestedLba = partition.start
nextPhase = Fat32ReaderPhase.Mount
phase = Fat32ReaderPhase.Request
} elif (partition.result != FileResult.Active) {
finishResult = partition.result
entryReady = false
phase = Fat32ReaderPhase.Finish
}
}
Fat32ReaderPhase.Mount =>
{
Mount()
}
Fat32ReaderPhase.DirectoryEntry =>
{
DirectoryEntry()
}
Fat32ReaderPhase.DirectoryLink =>
{
DirectoryLink()
}
Fat32ReaderPhase.ValidateLink =>
{
ValidateLink()
}
Fat32ReaderPhase.FileLink =>
{
FileLink()
}
Fat32ReaderPhase.FileData =>
{
if (remaining == 0) {
finishResult = FileResult.Success
entryReady = false
phase = Fat32ReaderPhase.Finish
} elif (offset == 512) {
AdvanceSector(Fat32ReaderPhase.FileData, Fat32ReaderPhase.FileLink)
}
}
_ =>
{
finishResult = FileResult.Corrupt
entryReady = false
phase = Fat32ReaderPhase.Finish
}
}
}
}