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/PartitionReader.lvt
namespace Livt.FileSystems
using Livt.Utils.Hashing
/**
* Internal scheduled parser phases.
*/
enum PartitionPhase
{
Mbr
GptHeader
HeaderCrc
TableCrc
TableEntry
}
/**
* Bounded partition discovery using a borrowed sector buffer. Supports a FAT32
* superfloppy, first primary MBR FAT32 partition, or first GPT Basic Data/EFI
* partition. GPT uses 128-byte entries and at most MAX_GPT_ENTRIES; primary header
* and complete entry-array CRC32 must pass. No backup recovery or extended MBR.
* The 32-bit block API rejects GPT addresses with nonzero high words.
* One scheduled owner serializes calls and services the shared sector buffer.
* Both share a clock/reset context; start/length are usable only after Success.
*/
component PartitionReader<MAX_GPT_ENTRIES: int = 128>
{
static assert MAX_GPT_ENTRIES > 0 && MAX_GPT_ENTRIES <= 4096, "Bound GPT table storage and scanning"
// Borrowed sector buffer and owned IEEE CRC32 helper.
sector: ISectorBuffer
crc: Crc
// Retained result and selected absolute block range.
public result: FileResult
public start: uint
public length: uint
// Discovery cursor and bounded GPT header/table checksum state.
capacity: uint
phase: PartitionPhase
index: int
headerSize: uint
expectedCrc: uint
runningCrc: logic[32]
tableLba: uint
tableBytes: uint
tableOffset: uint
firstUsable: uint
lastUsable: uint
selected: bool
/**
* Borrows the caller's buffer; caller services it before calling Service.
*/
new(sector: ISectorBuffer)
{
this.sector = sector
crc = new Crc()
}
/**
* Starts discovery for capacity 512-byte blocks, discarding the previous layout.
* Requires exclusive ownership and an idle sector buffer. Call sector.Service
* before this parser's Service until result is terminal. Zero capacity fails.
* Cancellation is owned by the caller, which cancels and drains the sector buffer.
*/
public fn Start(capacity: uint)
{
this.capacity = capacity
start = 0
length = 0
selected = false
result = FileResult.Active
Request(0, PartitionPhase.Mbr)
}
/**
* Issues a single sector request and records the next parsing phase.
*/
fn Request(lba: uint, next: PartitionPhase)
{
if (lba >= capacity || !sector.Start(lba)) {
result = FileResult.IoError
} else {
phase = next
}
}
/**
* Validates a nonempty range using subtraction before addition.
*/
fn InRange[](first: uint, count: uint) bool
{
return first < capacity && count > 0 && count <= capacity - first
}
/**
* Chooses a primary FAT32 partition, GPT, or an unpartitioned boot sector.
*/
fn ParseMbr()
{
if (sector.Read16(510) != 0xAA55) {
result = FileResult.Corrupt
return
}
var offset = 446
for (var entry: byte = 0; entry < 4; entry++) {
var kind = sector.ReadByte(offset + 4)
if (kind == 0xEE) {
Request(1, PartitionPhase.GptHeader)
return
}
if (kind == 0x0B || kind == 0x0C) {
start = sector.Read32(offset + 8)
length = sector.Read32(offset + 12)
result = FileResult.Corrupt
if (start > 0 && InRange(start, length)) {
result = FileResult.Success
}
return
}
offset = offset + 16
}
if (sector.Read16(11) == 512) {
start = 0
length = capacity
result = FileResult.Success
} else {
result = FileResult.Unsupported
}
}
/**
* Bounds all GPT arithmetic before calculating CRC or following addresses.
*/
fn ParseGpt()
{
headerSize = sector.Read32(12)
expectedCrc = sector.Read32(16)
firstUsable = sector.Read32(40)
lastUsable = sector.Read32(48)
tableLba = sector.Read32(72)
var entries = sector.Read32(80)
var lastBlock = capacity - 1
if (sector.Read32(0) != 0x20494645 || sector.Read32(4) != 0x54524150
|| sector.Read32(8) != 0x00010000 || headerSize < 92 || headerSize > 512
|| sector.Read32(20) != 0 || sector.Read32(24) != 1
|| sector.Read32(28) != 0 || sector.Read32(36) != 0
|| sector.Read32(32) != lastBlock || sector.Read32(44) != 0
|| sector.Read32(52) != 0 || sector.Read32(76) != 0
|| firstUsable < 34 || lastUsable < firstUsable || lastUsable >= lastBlock
|| tableLba < 2 || tableLba >= firstUsable) {
result = FileResult.Corrupt
return
}
if (sector.Read32(84) != 128 || entries == 0 || entries > (MAX_GPT_ENTRIES as uint)) {
result = FileResult.Unsupported
return
}
tableBytes = entries * 128
var roundedBytes = tableBytes + 511
var tableSectors = roundedBytes >> 9
var tableSpace = firstUsable - tableLba
if (tableSectors > tableSpace) {
result = FileResult.Corrupt
return
}
index = 0
runningCrc = 0xFFFFFFFF
phase = PartitionPhase.HeaderCrc
}
/**
* Validates the primary header CRC with its CRC field treated as zero.
*/
fn HeaderCrc()
{
var value = sector.ReadByte(index)
if (index >= 16 && index < 20) {
value = 0
}
runningCrc = crc.UpdateByte(runningCrc, value)
index++
if ((index as uint) == headerSize) {
var finalized = (runningCrc ^ 0xFFFFFFFF) as uint
if (finalized != expectedCrc) {
result = FileResult.Corrupt
return
}
expectedCrc = sector.Read32(88)
runningCrc = 0xFFFFFFFF
tableOffset = 0
index = 0
Request(tableLba, PartitionPhase.TableCrc)
}
}
/**
* Includes every declared entry byte, including unused entries, in the CRC.
*/
fn TableCrc()
{
runningCrc = crc.UpdateByte(runningCrc, sector.ReadByte(index))
index++
tableOffset++
if (index == 512 || tableOffset == tableBytes) {
index = 0
phase = PartitionPhase.TableEntry
}
}
/**
* Inspects one entry after hashing its sector; selects the first supported type.
*/
fn TableEntry()
{
var type0 = sector.Read32(index)
var type1 = sector.Read32(index + 4)
var type2 = sector.Read32(index + 8)
var type3 = sector.Read32(index + 12)
var basic = type0 == 0xEBD0A0A2 && type1 == 0x4433B9E5
&& type2 == 0xB668C087 && type3 == 0xC79926B7
var efi = type0 == 0xC12A7328 && type1 == 0x11D2F81F
&& type2 == 0xA0004BBA && type3 == 0x3BC93EC9
if (!selected && (basic || efi)) {
start = sector.Read32(index + 32)
var end = sector.Read32(index + 40)
if (sector.Read32(index + 36) != 0 || sector.Read32(index + 44) != 0
|| start < firstUsable || end > lastUsable || end < start) {
result = FileResult.Corrupt
return
}
length = end - start + 1
selected = true
}
index = index + 128
var used = ((tableOffset - 1) & 511) + 1
if ((index as uint) < used) {
return
}
var finalized = (runningCrc ^ 0xFFFFFFFF) as uint
if (tableOffset < tableBytes) {
tableLba++
index = 0
Request(tableLba, PartitionPhase.TableCrc)
} elif (finalized != expectedCrc) {
result = FileResult.Corrupt
} elif (!selected) {
result = FileResult.Unsupported
} else {
result = FileResult.Success
}
}
/**
* Advances one bounded parsing or CRC step once the buffer is complete.
*/
public fn Service()
{
if (result != FileResult.Active || sector.busy) {
return
}
if (!sector.valid || sector.failed) {
result = FileResult.IoError
return
}
match (phase)
{
PartitionPhase.Mbr =>
{
ParseMbr()
}
PartitionPhase.GptHeader =>
{
ParseGpt()
}
PartitionPhase.HeaderCrc =>
{
HeaderCrc()
}
PartitionPhase.TableCrc =>
{
TableCrc()
}
PartitionPhase.TableEntry =>
{
TableEntry()
}
}
}
}