2023-08-01 17:55:45 +10:00
local rtfs , internal = { } , { }
local proxy = { }
proxy.cacheSize = 8
local ieformat = " >BI8I8c47 " -- ftype, start, len, name
local iesize = string.packsize ( ieformat )
local sbformat = " >c4I2I8c18 " -- "rtfs" magic, version, index size in entries, label. label length subject to change.
local ftypes = {
unused = 0x00 ,
empty = 0x01 ,
file = 0x10 ,
tfile = 0x11 ,
dfile = 0x12 ,
directory = 0x20 ,
}
function proxy : log ( message , level )
syslog ( message , level or syslog.debug , " rtfs: " .. self.label )
end
-- data mangling functions
local function fnormalize ( s )
return table.concat ( fs.segments ( s ) , " / " )
end
function proxy : cSOI ( n )
n = n - 1
local ms = self.capacity / self.blockSize
local ePS = self.blockSize // iesize
local s = n // ePS
local so = self.blockSize - iesize - ( ( n % ePS ) * iesize )
local eo = so + iesize
return ms - s , so + 1 , eo
end
-- just for ease-of-use
local function getProxy ( addr )
if type ( addr ) == " string " then
return component.proxy ( component.get ( addr , " partition " ) or component.get ( addr ) or addr )
end
return addr
end
-- cache stuff
function proxy : cacheClean ( )
while # self.cache > self.cacheSize do
table.remove ( self.cache , 1 )
end
end
function proxy : cachedRead ( s )
for k , v in ipairs ( self.cache ) do
if v [ 1 ] == s then
self.cacheHits = self.cacheHits + 1
return v [ 2 ]
end
end
self.cache [ # self.cache + 1 ] = { s , self.d . readSector ( s ) }
self.cacheMisses = self.cacheMisses + 1
return self.cache [ # self.cache ] [ 2 ]
end
function proxy : cachedWrite ( s , d )
for k , v in ipairs ( self.cache ) do
if v [ 1 ] == s then
table.remove ( self.cache , k )
end
end
self.cache [ # self.cache + 1 ] = { s , d }
self : cacheClean ( )
return self.d . writeSector ( s , d )
end
-- superblock stuff
function proxy : updateSB ( )
self : cachedWrite ( 1 , string.pack ( sbformat , " rtfs " , 0 , self.isize , self.label ) )
end
function proxy : setISize ( n )
if self.isize ~= n then
self.isize = n
self : updateSB ( )
end
end
-- index I/O
function proxy : readIEntry ( n )
local sector , sstart , send = self : cSOI ( n )
local rt = { string.unpack ( ieformat , self : cachedRead ( sector ) : sub ( sstart , send ) ) }
rt [ 4 ] = rt [ 4 ] : gsub ( " \0 " , " " )
rt [ 5 ] = nil
return table.unpack ( rt )
end
function proxy : writeIEntry ( n , et , es , el , en )
local ne = string.pack ( ieformat , et , es , el , en )
local sector , sstart , send = self : cSOI ( n )
local fi , ft = self : findIEntry ( nil , nil , nil , nil , sector )
if fi then
self : log ( " auto-compact triggered " )
if not ( ft [ 1 ] == ftypes.empty or ft [ 1 ] == ftypes.dfile or ftypes [ 1 ] == ftypes.tfile ) then
self : log ( " automatic defragment triggered from: " .. debug.traceback ( ) )
assert ( not self.runningDefragment , " recursive defragment triggered " )
self : defragment ( )
end
n = self : compactIndex ( n ) or self.isize + 1
sector , sstart , send = self : cSOI ( n )
end
local pes = self : cachedRead ( sector )
local ns = pes : sub ( 1 , sstart - 1 ) .. ne .. pes : sub ( send + 1 )
self : cachedWrite ( sector , ns )
self : setISize ( math.max ( self.isize , n ) )
end
function proxy : allIEntries ( )
local i = 0
return function ( )
if i < self.isize then
i = i + 1
return i , self : readIEntry ( i )
end
return nil
end
end
-- index maintenance
function proxy : compactIndex ( ti ) -- number -- number -- Compact the index, optionally returning the new index of *ti*.
while self.runningCompact do -- wait for any existing compact run to complete
coroutine.yield ( )
end
self.runningCompact = true
-- look for contingous free space
local ri
for i , tp , st , sl , n in self : allIEntries ( ) do
if tp == ftypes.empty or tp == ftypes.dfile then
local ni , nt = self : findIEntry ( nil , st + sl )
if ni and nt then
if nt [ 1 ] == ftypes.empty or nt [ 1 ] == ftypes.dfile then
self : writeIEntry ( i , tp , st , sl + nt [ 3 ] , n )
self : writeIEntry ( ni , ftypes.unused , 0 , 0 , " " )
end
end
end
end
-- reverse walk to move any unused indexes
for i = self.isize , 1 , - 1 do
local ne = self : nextEntry ( ) or self.isize
local ft = { self : readIEntry ( i ) }
if ne < i then
-- modify any open handles to point at the new index
for k , v in pairs ( self.handles ) do
if v.fi == i then
v.fi = ne
end
end
self : writeIEntry ( ne , table.unpack ( ft ) )
ri = ( ti == i ) and ne or ri -- track the requested index
elseif ft [ 1 ] ~= ftypes.unused then
self : setISize ( i )
break
end
end
-- resize the free space / tentative file at the end of the FS to match the index size
local li , ls = 0 , 0
local lt
for i , tp , st , sl , n in self : allIEntries ( ) do
if st + sl > ls then
li , ls = i , st + sl
lt = { tp , st , sl , n }
end
end
if lt [ 1 ] == ftypes.empty or lt [ 1 ] == ftypes.dfile or lt [ 1 ] == ftypes.tfile then
local lastsec = ( self.capacity / self.blockSize ) - math.ceil ( self.isize / ( self.blockSize / iesize ) )
self : writeIEntry ( li , lt [ 1 ] , lt [ 2 ] , lastsec - lt [ 2 ] - 1 , lt [ 4 ] )
-- modify the file handle in memory, if applicable
if lt [ 1 ] == ftypes.tfile then
for k , v in ipairs ( self.handles ) do
if v.fi == li then
v.ft [ 3 ] = lastsec - lt [ 2 ]
v.maxWrite = v.ft [ 3 ] * self.blockSize
end
end
end
end
self.runningCompact = false
return ri
end
-- mostly just for debugging
function proxy : dumpIndex ( )
print ( " # ty init len end name " )
for i , tp , st , sl , n in self : allIEntries ( ) do
print ( string.format ( " %2i %2i %4i %4i %4i %s " , i , tp , st , sl , st + sl - 1 , n ) )
end
end
-- index searching
function proxy : findIEntry ( et , es , el , en , ef ) -- number number number string number -- Find an entry matching all provided parameters and ignoring any not specified.
local ri , rt
for i , tp , st , sl , n in self : allIEntries ( ) do
local sf = st + sl - 1
if ( et or tp ) == tp and ( es or st ) == st and ( el or sl ) == sl and ( en or n ) == n and ( ef or sf ) == sf then
ri , rt = i , { self : readIEntry ( i ) }
break
end
end
return ri , rt
end
function proxy : nextEntry ( )
local ni = self.isize + 1
for i , tp , st , sl , n in self : allIEntries ( ) do
if tp == ftypes.unused or tp == ftypes.dfile then
ni = i
break
end
end
return ni
end
-- file allocation
function proxy : bestFree ( )
local ri , sri
local bs , sbs = 0 , 0
for i , tp , st , sl , n in self : allIEntries ( ) do
if tp == ftypes.empty or tp == ftypes.dfile then
if sl > bs then
sri , sbs = ri , bs
ri , bs = i , sl
end
end
end
return bs / 2 > sbs and ri or sri , bs / 2 > sbs
end
function proxy : allocateNew ( name )
self : compactIndex ( )
local lfi , lfis = self : bestFree ( )
local lf = { self : readIEntry ( lfi ) }
local new
if lfis then
new = { ftypes.tfile , lf [ 2 ] + math.floor ( lf [ 3 ] / 2 ) , math.ceil ( lf [ 3 ] / 2 ) , name }
lf [ 3 ] = math.floor ( lf [ 3 ] / 2 )
self : writeIEntry ( lfi , table.unpack ( lf ) )
local ni = self : nextEntry ( )
self : writeIEntry ( ni , table.unpack ( new ) )
self : setISize ( math.max ( ni , self.isize ) )
return self : findIEntry ( ftypes.tfile , nil , nil , name )
-- return ni, new
else
new = { ftypes.tfile , lf [ 2 ] , lf [ 3 ] , name }
self : writeIEntry ( lfi , table.unpack ( new ) )
return lfi , new
end
return false
end
function proxy : relocateBlocks ( start , len , dest , loud )
local ot = os.getTimeout ( )
os.setTimeout ( 0.00001 ) -- disable coroutine.yield's delay
io.write ( loud and " \27 [s " or " " )
for i = 0 , len - 1 do
local buffer = self.d . readSector ( start + i )
self.d . writeSector ( dest + i , buffer )
io.write ( loud and string.format ( " \27 [u \27 [2K%i/%i %i -> %i " , i + 1 , len , start + i , dest + i ) or " " )
coroutine.yield ( )
end
io.write ( loud and " done! \n " or " " )
os.setTimeout ( ot )
return true
end
function proxy : relocateFile ( n , dest , loud )
local ft = { self : readIEntry ( n ) }
-- modify any writing handles to write to the destination before doing anything
local realLen
for k , v in pairs ( self.handles ) do
if v.ft [ 1 ] == n and ( v.w or v.a ) then
v.ft [ 2 ] = dest
realLen = v.currentSector
end
end
self : relocateBlocks ( ft [ 2 ] , realLen or ft [ 3 ] , dest , loud )
local w , e = pcall ( self.writeIEntry , self , n , ft [ 1 ] , dest , ft [ 3 ] , ft [ 4 ] )
-- modify any reading handles to read from the destination after moving the data
for k , v in pairs ( self.handles ) do
if v.ft [ 1 ] == n and v.r then
v.ft [ 2 ] = dest
end
end
return w , { ft [ 1 ] , dest , ft [ 3 ] , ft [ 4 ] }
end
function proxy : defragment ( loud )
while self.runningDefragment do
coroutine.yield ( )
end
self.runningDefragment = true
for i , tp , _ , _ , _ in self : allIEntries ( ) do
if tp == ftypes.empty or tp == ftypes.dfile then
self : writeIEntry ( i , ftypes.unused , 0 , 0 , " " )
end
end
local ftab = { }
local fsec , lsec = 2 , ( self.capacity / self.blockSize ) - math.ceil ( self.isize / ( self.blockSize / iesize ) )
for i , tp , st , sl , n in self : allIEntries ( ) do
if tp ~= ftypes.empty and tp ~= ftypes.dfile and sl > 0 then
ftab [ # ftab + 1 ] = { i , tp , st , sl , n }
end
end
-- sort files in order of their start sector
table.sort ( ftab , function ( a , b )
return a [ 3 ] < b [ 3 ]
end )
-- move files closer to the start if applicable
local deferred = { }
for k , ft in ipairs ( ftab ) do
io.write ( loud and string.format ( " %i/%i: %s \n " , k , # ftab , ft [ 5 ] ) or " " )
if ft [ 3 ] ~= fsec then
local w , dt = self : relocateFile ( ft [ 1 ] , fsec , loud )
deferred [ # deferred + 1 ] = ( not w and dt ) or nil
end
fsec = fsec + ft [ 4 ]
end
self : writeIEntry ( self : nextEntry ( ) , ftypes.empty , fsec , lsec - fsec , " " )
for k , v in ipairs ( deferred ) do
self : writeIEntry ( self : nextEntry ( ) , table.unpack ( v ) )
end
self.runningDefragment = false
end
-- handle management
function proxy : openFile ( name , mode )
name = fnormalize ( name )
local handle = { }
for c in mode : gmatch ( " . " ) do
handle [ c ] = true
end
local fi , ft = self : findIEntry ( ftypes.file , nil , nil , name )
if handle.r then
if not fi then
return false , " file not found "
end
handle.currentSector , handle.fi , handle.ft = 0 , fi , ft
elseif handle.w or handle.a then
handle.writeBuffer , handle.writeCounter = " " , 0
local oft = ft
fi , ft = self : allocateNew ( name )
if oft and handle.a then
assert ( ft [ 3 ] >= oft [ 3 ] , " no extent large enough for relocated file " )
handle.writeBuffer = self.d . readSector ( oft [ 2 ] + oft [ 3 ] - 1 ) : gsub ( " \0 +$ " , " " )
handle.currentSector , handle.writeCounter = oft [ 3 ] - 1 , ( ( oft [ 3 ] - 1 ) * 512 ) + # handle.writeBuffer
self : log ( string.format ( " appending %s, CS: %i, WC: %i, WB: %i " , name , handle.currentSector , handle.writeCounter , # handle.writeBuffer ) )
self : relocateBlocks ( oft [ 2 ] , oft [ 3 ] , ft [ 2 ] )
end
handle.fi , handle.ft , handle.maxWrite , handle.writeCounter , handle.currentSector = fi , ft , ft [ 3 ] * self.blockSize , handle.writeCounter or 0 , handle.currentSector or 0
end
self.handles [ # self.handles + 1 ] = handle
return # self.handles
end
function proxy : closeHandle ( h )
local handle = self.handles [ h ]
if handle.w or handle.a then
local ft = handle.ft
self.d . writeSector ( ft [ 2 ] + handle.currentSector , handle.writeBuffer .. ( " \0 " ) : rep ( self.blockSize - # handle.writeBuffer ) )
local fs = ft [ 3 ] - math.ceil ( handle.writeCounter / self.blockSize )
local ni , nt = self : findIEntry ( nil , ft [ 2 ] + ft [ 3 ] )
if ni and ( nt [ 1 ] == ftypes.empty or nt [ 1 ] == ftypes.dfile ) then
fs = fs + nt [ 3 ]
else
ni = self : nextEntry ( )
end
while self : findIEntry ( ftypes.file , nil , nil , ft [ 4 ] ) do
local di , dt = self : findIEntry ( ftypes.file , nil , nil , ft [ 4 ] )
dt [ 1 ] = ftypes.dfile
self : writeIEntry ( di , table.unpack ( dt ) )
self : log ( string.format ( " marking entry %i for %s as deleted " , di , dt [ 4 ] ) )
end
ft [ 1 ] , ft [ 3 ] = ftypes.file , math.ceil ( handle.writeCounter / self.blockSize )
self : writeIEntry ( handle.fi , table.unpack ( ft ) )
self : writeIEntry ( ni , ftypes.empty , ft [ 2 ] + ft [ 3 ] , fs , " " )
self : setISize ( math.max ( handle.fi , ni , self.isize ) )
end
self.handles [ h ] = nil
return true
end
-- handle I/O
function proxy : writeHandle ( h , s )
local handle = self.handles [ h ]
if s : len ( ) + handle.writeCounter > handle.maxWrite then
self : log ( " no space left in extent " , syslog.error )
return false , " no space left in extent "
end
handle.writeCounter = handle.writeCounter + s : len ( )
handle.writeBuffer = handle.writeBuffer .. s
while # handle.writeBuffer > self.blockSize do
self.d . writeSector ( handle.ft [ 2 ] + handle.currentSector , handle.writeBuffer : sub ( 1 , self.blockSize ) )
handle.currentSector = handle.currentSector + 1
handle.writeBuffer = handle.writeBuffer : sub ( self.blockSize + 1 )
end
return true
end
function proxy : readHandle ( h , s )
2023-08-06 11:10:13 +10:00
local handle , rb = self.handles [ h ]
if handle.currentSector < handle.ft [ 3 ] - 1 then
local rb = self.d . readSector ( handle.ft [ 2 ] + handle.currentSector )
2023-08-07 13:38:59 +10:00
elseif handle.currentSector == handle.ft [ 3 ] - 1 then
2023-08-01 17:55:45 +10:00
local rb = self.d . readSector ( handle.ft [ 2 ] + handle.currentSector ) : gsub ( " \0 +$ " , " " )
else
return nil
end
2023-08-06 11:10:13 +10:00
handle.currentSector = handle.currentSector + 1
return rb
2023-08-01 17:55:45 +10:00
end
-- primarily for debugging
function proxy : getHandle ( h )
return self.handles [ h ]
end
----------
function rtfs . mount ( d )
d = getProxy ( d )
local p = setmetatable ( { } , { __index = proxy } )
local magic , version , isize , label = string.unpack ( sbformat , d.readSector ( 1 ) )
assert ( magic == " rtfs " , " incorrect magic " )
p.d = d
p.fstype = " rtfs "
p.cache , p.handles = { } , { }
p.cacheHits , p.cacheMisses = 0 , 0
p.blockSize , p.capacity , p.label , p.isize = d.getSectorSize ( ) , d.getCapacity ( ) , label : gsub ( " \0 " , " " ) , isize
-- FS proxy functions
function p . exists ( name )
name = fnormalize ( name )
for i , tp , st , sl , n in p : allIEntries ( ) do
if ( tp == ftypes.file or tp == ftypes.directory ) and n == name then
return true
end
end
end
function p . isDirectory ( name )
name = fnormalize ( name )
2023-08-05 16:22:11 +10:00
return name == " " and true or p : findIEntry ( ftypes.directory , nil , nil , name ) ~= nil
2023-08-01 17:55:45 +10:00
end
function p . size ( name )
name = fnormalize ( name )
local fi , ft = p : findIEntry ( ftypes.file , nil , nil , name )
if not fi and ft then return 0 end
return ft [ 3 ] * p.blockSize
end
function p . spaceUsed ( )
local c = 0
for i , tp , st , sl , n in p : allIEntries ( ) do
if tp == ftypes.file then
c = c + sl
end
end
return c * p.blockSize
end
function p . spaceTotal ( )
return p.capacity - ( math.ceil ( p.isize / ( p.blockSize / iesize ) ) * p.blockSize ) - p.blockSize
end
p.isReadOnly = p.d . isReadOnly
function p . list ( name )
name = fnormalize ( name )
local rt = { }
for i , tp , st , sl , n in p : allIEntries ( ) do
local seg = fs.segments ( n )
local pn = table.concat ( seg , " / " , 1 , # seg - 1 )
local fn = seg [ # seg ]
if ( tp == ftypes.file or tp == ftypes.directory ) and pn == name then
rt [ # rt + 1 ] = fn .. ( ( tp == ftypes.directory and " / " ) or " " )
end
end
return rt
end
function p . makeDirectory ( name )
name = fnormalize ( name )
if # name < 1 or p : findIEntry ( nil , nil , nil , name ) then return false end
2023-08-05 16:22:11 +10:00
local seg = fs.segments ( name )
for j = 1 , # seg - 1 do
p.makeDirectory ( table.concat ( seg , " / " , 1 , j ) )
end
if not p.isDirectory ( table.concat ( seg , " / " , 1 , # seg - 1 ) ) then return false end
2023-08-01 17:55:45 +10:00
local ni = p : nextEntry ( )
p : writeIEntry ( ni , ftypes.directory , 0 , 0 , name )
p : setISize ( math.max ( p.isize , ni ) )
return true
end
function p . remove ( name )
name = fnormalize ( name )
local fi , ft = p : findIEntry ( ftypes.file , nil , nil , name )
if fi then
ft [ 1 ] = ftypes.dfile
p : writeIEntry ( fi , table.unpack ( ft ) )
end
local fi , ft = p : findIEntry ( ftypes.directory , nil , nil , name )
if fi then
2023-08-05 16:22:11 +10:00
for _ , f in ipairs ( p.list ( name ) ) do
p.remove ( name .. " / " .. f )
end
p : writeIEntry ( fi , ftypes.unused , 0 , 0 , " " )
2023-08-01 17:55:45 +10:00
end
end
function p . rename ( from , to )
from , to = fnormalize ( from ) , fnormalize ( to )
if p.exists ( from ) then
if p.exists ( to ) then
p.remove ( to )
end
local fi , ft = p : findIEntry ( p.isDirectory ( name ) and ftypes.directory or ftypes.file , nil , nil , to )
ft [ 4 ] = to
p : writeIEntry ( fi , table.unpack ( ft ) )
end
end
function p . lastModified ( )
return 0
end
function p . open ( name , mode )
name , mode = fnormalize ( name ) , mode or " r "
return p : openFile ( name , mode )
end
function p . write ( h , v )
return p : writeHandle ( h , v )
end
function p . read ( h , v )
return p : readHandle ( h , v )
end
function p . close ( h )
return p : closeHandle ( h )
end
function p . getLabel ( )
return p.label
end
function p . setLabel ( label )
p.label = label
p : updateSB ( )
end
return p
end
function rtfs . format ( d , label )
d = getProxy ( d )
print ( d.address )
d.writeSector ( 1 , string.pack ( sbformat , " rtfs " , 0 , 1 , label or " " ) )
rtfs.mount ( d ) : writeIEntry ( 1 , ftypes.empty , 2 , ( d.getCapacity ( ) / d.getSectorSize ( ) ) - 2 , " " )
end
return rtfs