PsychOSPackages/rtfs/lib/rtfs.lua

541 lines
15 KiB
Lua
Raw Normal View History

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("%2x %2x %4x %4x %4x %s",i,tp,st,sl,st+sl-1, n))
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
print(n, v.ft[1])
if v.ft[1] == n and (v.w or v.a) then
print("found file handle")
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)
loud=true
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)
local handle = self.handles[h]
if handle.currentSector <= handle.ft[3] - 1 then
local rb = self.d.readSector(handle.ft[2] + handle.currentSector):gsub("\0+$","")
handle.currentSector = handle.currentSector + 1
return rb
else
return nil
end
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")
if magic ~= "rtfs" then error("incorrect magic") end
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)
return p:findIEntry(ftypes.directory, nil, nil, name) ~= nil
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
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
error("removing dirs not implemented")
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