diff --git a/diskpart/lib/diskpart.lua b/diskpart/lib/diskpart.lua new file mode 100644 index 0000000..29f1aff --- /dev/null +++ b/diskpart/lib/diskpart.lua @@ -0,0 +1,86 @@ +local partition = {} + +local eformat = "c20c4>I4>I4" +function partition.parse(s) + local rt = {} + for i = 1, s:len(), 32 do + local n, t, start, length = string.unpack(eformat, s, i) + n = n:gsub("\0", "") + if n ~= "" then + rt[#rt+1] = {n,t,start,length} + end + end + return rt +end +function partition.generate(pt) + local ps = "" + for k,v in ipairs(pt) do + ps = ps .. string.pack(eformat, table.unpack(v)) + end + return ps +end + +local function getProxy(addr) + if type(addr) == "string" then + return component.proxy(component.get(addr)) + end + return addr +end + +function partition.getPartitions(drive) + drive = getProxy(drive) + local rv = partition.parse(drive.readSector(drive.getCapacity() / drive.getSectorSize())) + return rv[1][2] == "mtpt" and rv or {} +end +function partition.setPartitions(drive, pt, name) + drive = getProxy(drive) + name = name or "" + if pt[1][2] ~= "mtpt" then table.insert(pt, 1, {name, "mtpt", 0, 0}) end + local ns = partition.generate(pt) + return drive.writeSector(drive.getCapacity() / drive.getSectorSize(), ns .. ("\0"):rep(drive.getSectorSize() - #ns)) +end + +function partition.proxyPartition(drive, index) + drive = getProxy(drive) + local part = partition.getPartitions(drive)[index] + local sectorOffset, byteOffset, finish = part[3] - 1, (part[3]-1) * drive.getSectorSize() + local proxy = {label=part[1],ptype=part[2]} + + function proxy.getCapacity() + return drive.getSectorSize() * part[4] + end + function proxy.getLabel() + return part[1] + end + function proxy.setLabel() + return false + end + + local function offsetSector(sector) + if sector < 1 or sector > part[4] then error("invalid offset, not in a usable sector") end + return sectorOffset + sector + end + function proxy.readSector(sector) + return drive.readSector(offsetSector(sector)) + end + function proxy.writeSector(sector, data) + return drive.writeSector(offsetSector(sector), data) + end + + local function offsetByte(byte) + if byte < 1 or byte > part[4]*drive.getSectorSize() then return 0 end + return byteOffset + byte + end + function proxy.readByte(byte) + return drive.readByte(offsetByte(byte)) + end + function proxy.writeByte(byte, data) + return drive.writeByte(offsetByte(byte), data) + end + for k,v in pairs(drive) do + proxy[k] = proxy[k] or v + end + return proxy +end + +return partition diff --git a/diskpart/package.cfg b/diskpart/package.cfg new file mode 100644 index 0000000..9347e9c --- /dev/null +++ b/diskpart/package.cfg @@ -0,0 +1,3 @@ +{["name"]="diskpart", + ["description"]="Unmanaged disk partition library", + ["authors"]="Izaya"} diff --git a/rtfs/lib/rtfs.lua b/rtfs/lib/rtfs.lua new file mode 100644 index 0000000..da93760 --- /dev/null +++ b/rtfs/lib/rtfs.lua @@ -0,0 +1,540 @@ +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 diff --git a/rtfs/package.cfg b/rtfs/package.cfg new file mode 100644 index 0000000..d810f28 --- /dev/null +++ b/rtfs/package.cfg @@ -0,0 +1,3 @@ +{["name"]="rtfs", + ["description"]="RT-11 filesystem clone", + ["authors"]="Izaya"}