2023-10-07 09:05:13 +11:00
local fs = fs or require " filesystem "
2023-10-01 14:40:26 +11:00
local common = require " fs.rtfs "
local rtfs , proxy = { } , setmetatable ( { } , { __index = common.proxy } )
local ieformat = " >I2I8I8c46 " -- type (4 bits) and index (12 bits), start sector (32 bits), lenth in bytes (32 bits), name/path (46 bytes)
local sbformat = " >c4I2I8c18 " -- "rtfs" magic, version, index size in entries, label. label length subject to change.
local iesize = string.packsize ( ieformat )
local ftypes = {
empty = 0 ,
dfile = 1 ,
dfext = 2 ,
ddir = 3 ,
ddex = 4 ,
2023-10-07 22:55:17 +11:00
tfile = 5 ,
text = 6 ,
2023-10-01 14:40:26 +11:00
dlink = 7 ,
unused = 8 ,
file = 9 ,
fext = 10 ,
dir = 11 ,
dex = 12 ,
link = 15
}
-- data mangling functions
local fnormalize = common.fnormalize
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
-- superblock stuff
function proxy : updateSB ( )
self : cachedWrite ( 1 , string.pack ( sbformat , " rtfs " , 1 , 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 ) ) }
local et , ex = rt [ 1 ] >> 12 , rt [ 1 ] & 0x1FF
rt [ 4 ] = rt [ 4 ] : gsub ( " \0 " , " " )
rt [ 5 ] = nil
return et , ex , table.unpack ( rt , 2 )
end
function proxy : writeIEntry ( n , et , ex , es , el , en ) -- index, type, extent index, extent start, extent length, extent name
local ne = string.pack ( ieformat , ( et << 12 ) | ( ex & 0x1FF ) , es , el , en )
local sector , sstart , send = self : cSOI ( n )
-- check if there's an extent in the way of this entry
while self : findIEntry ( nil , nil , nil , nil , nil , sector ) do
self : log ( " auto-compact triggered " )
-- compactIndex will shrink the extent if it is safe to overwrite (deleted, free, etc)
n = self : compactIndex ( n ) or self.isize + 1
local newn = n == self.isize + 1
-- if that fails, because it can't be shrunk, we need to move it ourselves
local fi , ft = self : findIEntry ( nil , nil , nil , nil , nil , sector )
if fi and ft [ 1 ] > ftypes.unused then
self : log ( " auto-compact unsuccessful, trying to relocate extent " )
-- try to find a suitable free space
local lfi , lfis = self : bestFree ( )
local lf = { self : readIEntry ( lfi ) }
-- if it's not big enough, give up.
assert ( lf [ 4 ] >= ft [ 4 ] , " unable to relocate extent to write index entry " )
-- if it is, create an extent structure at the end of the free space
local de = { ft [ 1 ] , ft [ 2 ] , lf [ 3 ] + lf [ 4 ] // self.blockSize - math.ceil ( ft [ 4 ] / self.blockSize ) , ft [ 4 ] , ft [ 5 ] }
lf [ 4 ] = lf [ 4 ] - ( math.ceil ( ft [ 4 ] / self.blockSize ) * self.blockSize )
-- move the data to the new extent
for i = 0 , ft [ 4 ] // self.blockSize do
self : writeExtent ( de , i , self : readExtent ( ft , i ) )
end
-- overwrite the old entries, which shouldn't get us back here
self : writeIEntry ( fi , table.unpack ( de ) )
self : writeIEntry ( lfi , table.unpack ( lf ) )
-- and finally, make a new entry for the now old data, sans one sector
self : writeIEntry ( self : nextEntry ( ) , ft [ 1 ] - 8 , ft [ 2 ] , ft [ 3 ] , ft [ 4 ] - self.blockSize , ft [ 5 ] )
end
-- if the aim was to make a new entry, update n so it will do so
n = newn and self.isize + 1 or n
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 ) )
return 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
-- for debugging
function proxy : dumpIndex ( )
local t = { }
for i , tp , ei , st , sl , n in self : allIEntries ( ) do
t [ # t + 1 ] = { i , tp , ei , st , sl , ( st + math.ceil ( sl / self.blockSize ) ) - 1 , n }
end
table.sort ( t , function ( a , b ) return a [ 4 ] < b [ 4 ] end )
print ( " # ty ext# init len end name " )
for k , v in ipairs ( t ) do
if k % 23 == 0 then
io.read ( )
print ( " # ty ext# init len end name " )
end
print ( string.format ( " %2i %1x %4i %4i %8i %4i %s " , table.unpack ( v ) ) )
end
end
-- index searching
function proxy : findIEntry ( et , ex , 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 , ix , st , sl , n in self : allIEntries ( ) do
local sf = st + math.ceil ( sl / self.blockSize ) - 1
if ( et or tp ) == tp and ( ex or ix ) == ix 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 in self : allIEntries ( ) do
if tp == ftypes.unused then
ni = i
break
end
end
return ni
end
-- extent allocation
function proxy : bestFree ( last )
local ri , sri
local bs , sbs = 0 , 0
if last then
ri , rs = self : findIEntry ( nil , nil , last [ 3 ] + math.ceil ( last [ 4 ] / self.blockSize ) )
if ri and rs and rs [ 1 ] <= ftypes.ddex then
return ri , false , true
end
end
for i , tp , ei , st , sl , n in self : allIEntries ( ) do
if tp <= ftypes.ddex 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 : allocateExtent ( name , etype , ei , esize , last )
etype , ei , esize = etype or ftypes.tfile , ei or 0 , math.ceil ( math.max ( 4096 , esize or self.capacity / 256 ) / self.blockSize ) * self.blockSize
self : compactIndex ( )
local lfi , lfis , lfext = self : bestFree ( last )
local lf = { self : readIEntry ( lfi ) }
local esize = math.min ( esize , lf [ 4 ] )
local lfs , es = lf [ 4 ] // self.blockSize , esize // self.blockSize
-- if biggest available area, split and put extent in the middle
if lfis then
local new = { etype , ei , lf [ 3 ] + math.floor ( lfs / 2 ) , esize , name }
lf [ 4 ] = math.floor ( lfs / 2 ) * self.blockSize
local free = { ftypes.empty , 0 , new [ 3 ] + es , lfs * self.blockSize - new [ 4 ] - lf [ 4 ] , " " }
self : writeIEntry ( lfi , table.unpack ( lf ) )
self : writeIEntry ( self : nextEntry ( ) , table.unpack ( new ) )
self : writeIEntry ( self : nextEntry ( ) , table.unpack ( free ) )
return self : findIEntry ( etype , ei , nil , nil , name )
else -- otherwise, put it at the start
local new = { etype , ei , lf [ 3 ] , esize , name }
local free = { ftypes.empty , 0 , new [ 3 ] + es , lfs * self.blockSize - new [ 4 ] , " " }
local efi , eff
if last then
last = { table.unpack ( last ) }
efi , eff = self : findIEntry ( table.unpack ( last ) )
end
-- if a previous entry was provided and the name matches, continue the extent
if lfext and efi and name == eff [ 5 ] then
last [ 4 ] = math.ceil ( ( last [ 4 ] + new [ 4 ] ) / self.blockSize ) * self.blockSize
new = last
self : writeIEntry ( efi , table.unpack ( new ) )
self : writeIEntry ( lfi , table.unpack ( free ) )
lfi = efi
-- failing that, replace the free space entry with the new extent and
-- write a new free space entry
else
self : writeIEntry ( lfi , table.unpack ( new ) )
self : writeIEntry ( self : nextEntry ( ) , table.unpack ( free ) )
end
return lfi , new , lfext and new [ 4 ]
end
return false
end
function proxy : writeExtent ( ft , o , d ) -- write a sector to an extent
assert ( o >= 0 and o < ( ft [ 4 ] // self.blockSize ) + 1 , " out of range " )
assert ( # d <= self.blockSize , " block too large " )
return self.d . writeSector ( ft [ 3 ] + o , d .. ( " \0 " ) : rep ( self.blockSize - # d ) )
end
function proxy : readExtent ( ft , o ) -- read a sector from an extent
assert ( o >= 0 and o < ( ft [ 4 ] // self.blockSize ) + 1 , " out of range " )
return self.d . readSector ( ft [ 3 ] + o ) : sub ( 1 , math.min ( self.blockSize , ft [ 4 ] - ( o * self.blockSize ) ) )
end
function proxy : openHandle ( name , mode , fmt )
name , fmt = fnormalize ( name ) , fmt or { ftypes.file , ftypes.fext , ftypes.tfile , ftypes.text }
local handle , xt = { aft = { } , fmt = fmt , buffer = " " , bytes = 0 }
for c in mode : gmatch ( " . " ) do
handle [ c ] = true
end
local fi , oft = self : findIEntry ( fmt [ 1 ] , nil , nil , nil , name )
if handle.r then
if not fi then
return false , " file not found "
end
handle.ft , handle.aft [ 1 ] = oft , oft
elseif handle.w or handle.a then
if handle.a and fi then
local lfi , lf = self : findIEntry ( fmt [ 2 ] , oft [ 2 ] , nil , nil , oft [ 5 ] )
fi , handle.ft , xt = self : allocateExtent ( name , fmt [ 4 ] , oft [ 2 ] + 1 , nil , lf or oft )
-- if in append mode, put the file extent into the "all extents" table to be updated later
handle.aft [ 1 ] , handle.bytes = oft , xt and ( lfi and lf [ 4 ] or oft [ 4 ] ) or 0
-- read the last sector into the write buffer so it can be written back to disk with new data
-- then rewind the counter so it does get written
if handle.bytes % self.blockSize ~= 0 then
handle.buffer = self : readExtent ( lf or oft , handle.bytes // 512 )
handle.bytes = handle.bytes - # handle.buffer
end
else
-- allocate the new extent to write into
fi , handle.ft = self : allocateExtent ( name , fmt [ 3 ] )
end
handle.aft [ # handle.aft + ( xt and 0 or 1 ) ] = handle.ft
end
local ni = # self.handles + 1
self.handles [ ni ] = handle
return ni
end
function proxy : closeHandle ( h )
local handle = self.handles [ h ]
if handle.w or handle.a then
-- flush the buffer just in case
self : writeHandle ( h , " " , true )
local fs = handle.ft [ 4 ] - handle.bytes
-- update each index entry
self : compactIndex ( )
-- mark any old entries with a matching name as deleted when not appending
if not handle.a then
for i , tp , ei , st , sl , n in self : allIEntries ( ) do
if n == handle.aft [ 1 ] [ 5 ] and tp > 8 then
self : writeIEntry ( i , tp - 8 , ei , st , sl , n )
end
end
end
for k , v in ipairs ( handle.aft ) do
local fi , ft = self : findIEntry ( table.unpack ( v ) )
self : writeIEntry ( fi ,
-- determine if it's the file or extent part
( v [ 1 ] == handle.fmt [ 3 ] or v [ 1 ] == handle.fmt [ 1 ] ) and handle.fmt [ 1 ] or handle.fmt [ 2 ] ,
-- file part wants the number of extents, extents want their index
( v [ 1 ] == handle.fmt [ 3 ] or v [ 1 ] == handle.fmt [ 1 ] ) and handle.ft [ 2 ] or v [ 2 ] ,
-- the rest is fine as is, unless it's the last item
v [ 3 ] , v == handle.ft and handle.bytes or v [ 4 ] , v [ 5 ]
)
end
if fs > self.blockSize then
self : writeIEntry ( self : nextEntry ( ) , ftypes.empty , 0 , handle.ft [ 3 ] + math.ceil ( handle.bytes / self.blockSize ) , ( fs // self.blockSize ) * self.blockSize , " " )
self : compactIndex ( )
end
end
self.handles [ h ] = nil
end
function proxy : writeHandle ( h , d , f )
local handle = self.handles [ h ]
assert ( handle.w or handle.a , " file not open in writable mode " )
handle.buffer = handle.buffer .. d
local rv
-- only attempt to write when there's enough data, or forced
while # handle.buffer >= self.blockSize or f do
f = false
-- allocate a new extent if this one is full
if handle.bytes >= handle.ft [ 4 ] then
local fi , ft , xt = self : allocateExtent ( handle.ft [ 5 ] , handle.fmt [ 4 ] , handle.ft [ 2 ] + 1 , nil , handle.ft )
if xt then
handle.aft [ # handle.aft ] , handle.ft = ft , ft
else
handle.aft [ # handle.aft + 1 ] , handle.ft , handle.bytes = ft , ft , 0
end
end
-- write a block and increment the counter
local wb = handle.buffer : sub ( 1 , self.blockSize )
handle.buffer = handle.buffer : sub ( self.blockSize + 1 )
rv = self : writeExtent ( handle.ft , handle.bytes // self.blockSize , wb )
handle.bytes = handle.bytes + # wb
end
return rv
end
function proxy : readHandle ( h )
local handle = self.handles [ h ]
assert ( handle.r , " file not open in read mode " )
-- if there's no more data in the extent, find the next
if handle.bytes >= handle.ft [ 4 ] then
local ce = handle.ft [ 1 ] == handle.fmt [ 2 ] and handle.ft [ 2 ] or 0
if ce >= handle.aft [ 1 ] [ 2 ] then
return nil
end
local fi , ft = self : findIEntry ( handle.fmt [ 2 ] , ce + 1 , nil , nil , handle.ft [ 5 ] )
handle.aft [ # handle.aft + 1 ] , handle.ft , handle.bytes = ft , ft , 0
end
local rb = self : readExtent ( handle.ft , handle.bytes // 512 )
handle.bytes = handle.bytes + # rb
return rb
end
function proxy : compactIndex ( ti )
-- compact contingous free space
for i , tp , ei , st , sl , n in self : allIEntries ( ) do
if tp <= ftypes.ddex then
sl = math.max ( sl / self.blockSize )
local ni , nt = self : findIEntry ( nil , nil , st + sl )
if ni and nt and nt [ 1 ] <= ftypes.ddex then
self : writeIEntry ( i , tp , ei , st , ( sl * self.blockSize ) + nt [ 4 ] , n )
self : writeIEntry ( ni , ftypes.unused , 0 , 0 , 0 , " " )
elseif tp <= ftypes.ddex and sl < 1 then
self : writeIEntry ( i , ftypes.unused , 0 , 0 , 0 , " " )
end
end
end
local ri
-- reverse walk the index to find holes
for i = self.isize , 1 , - 1 do
local ne = self : nextEntry ( ) or self.isize
local ft = { self : readIEntry ( i ) }
if ne < i then
self : writeIEntry ( ne , table.unpack ( ft ) )
self : setISize ( i - 1 )
ri = ( ti == i ) and ne or ri
end
end
-- resize space at end of disk
local li , ls , lt = 0 , 0 , nil
for i , tp , ei , st , sl , n in self : allIEntries ( ) do
sl = math.max ( sl / self.blockSize )
if st + sl > ls then
li , ls = i , st + sl
lt = { tp , ei , st , sl , n }
end
end
if lt [ 1 ] <= ftypes.text then
local lastsec = ( self.capacity / self.blockSize ) - math.ceil ( self.isize / ( self.blockSize / iesize ) )
self : writeIEntry ( li , lt [ 1 ] , lt [ 2 ] , lt [ 3 ] , ( lastsec - lt [ 3 ] ) * self.blockSize , lt [ 5 ] )
end
return ri
end
2023-10-07 09:05:13 +11:00
function rtfs . mount ( p , ro )
2023-10-01 14:40:26 +11:00
local d = common.getProxy ( p )
local p = setmetatable ( { } , { __index = proxy } )
p.d = d
p.fstype , p.version , p.isize , p.label = string.unpack ( sbformat , d.readSector ( 1 ) )
assert ( p.fstype == " rtfs " , " incorrect magic " )
assert ( p.version == 1 , " incompatible rtfs version " )
p.d = d
p.cache , p.handles = { } , { }
p.cacheHits , p.cacheMisses = 0 , 0
p.blockSize , p.capacity , p.label = d.getSectorSize ( ) , d.getCapacity ( ) , p.label : gsub ( " \0 " , " " )
-- FS proxy functions
function p . exists ( name )
name = fnormalize ( name )
for i , tp , ex , st , sl , n in p : allIEntries ( ) do
if ( tp == ftypes.file or tp == ftypes.dir ) and n == name then
return true
end
end
end
function p . isDirectory ( name )
name = fnormalize ( name )
return name == " " and true or p : findIEntry ( ftypes.dir , nil , nil , nil , name ) ~= nil
end
function p . size ( name )
name = fnormalize ( name )
local rv = 0
for i , tp , ex , st , sl , n in p : allIEntries ( ) do
if n == name and tp > ftypes.unused then
rv = rv + sl
end
end
return rv
end
function p . spaceUsed ( )
local c = 0
for i , tp , ex , st , sl , n in p : allIEntries ( ) do
if tp > ftypes.unused then
c = c + sl
end
end
return c
end
function p . spaceTotal ( )
return p.capacity - ( math.ceil ( p.isize / ( p.blockSize / iesize ) ) * p.blockSize ) - p.blockSize
end
2023-10-07 09:05:13 +11:00
p.isReadOnly = p.d . isReadOnly or function ( ) return ro or false end
2023-10-01 14:40:26 +11:00
function p . list ( name )
name = fnormalize ( name )
local rt = { }
for i , tp , ex , 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.dir ) and pn == name then
rt [ # rt + 1 ] = fn .. ( ( tp == ftypes.dir and " / " ) or " " )
end
end
return rt
end
function p . makeDirectory ( name )
name = fnormalize ( name )
if # name < 1 or p : findIEntry ( nil , nil , nil , nil , name ) then return false end
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
local ni = p : nextEntry ( )
p : writeIEntry ( ni , ftypes.dir , 0 , 0 , 0 , name )
-- p:setISize(math.max(p.isize, ni))
return true
end
function p . remove ( name )
name = fnormalize ( name )
for i , tp , ex , st , sl , n in p : allIEntries ( ) do
if n == name and tp > ftypes.unused then
p : writeIEntry ( i , tp - 8 , ex , st , sl , n )
end
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.dir or ftypes.file , nil , nil , to )
ft [ 5 ] = 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 : openHandle ( 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 ( p , label )
local d = common.getProxy ( p )
print ( d.address )
d.writeSector ( 1 , string.pack ( sbformat , " rtfs " , 1 , 1 , label or " " ) )
rtfs.mount ( d ) : writeIEntry ( 1 , ftypes.empty , 0 , 2 , d.getCapacity ( ) - ( d.getSectorSize ( ) * 2 ) , " " )
end
return rtfs