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