Написал алгоритм сжатия совместимый с UTF-8. Не очень эффективный, экономит 10-15% на тексте. Добавил генератор стартовой памяти, убран хардкод из MMC
510 lines
15 KiB
Lua
510 lines
15 KiB
Lua
-- MMC.lua — Memory/FS Controller (AtlasOS v2.0)
|
|
-- in[1] = mem_rx ← CMC.out[4] (FS request)
|
|
-- in[3] = mem_read ← Memory.out[1]
|
|
-- out[1] = mem_tx → CMC.in[2] (FS response)
|
|
-- out[5] = mem_data → Memory.in[1]
|
|
-- out[6] = mem_write → Memory.in[2]
|
|
|
|
local SEG_SIZE = 8192
|
|
local MAX_INODES = 64
|
|
local ROOT_INO = 1
|
|
|
|
local _unpack = table.unpack or unpack
|
|
|
|
-- === LZ4 compression (pure Lua) ===
|
|
|
|
function lz4Compress(data)
|
|
local n = #data
|
|
if n == 0 then return "" end
|
|
local ht, res, ip, anchor = {}, {}, 1, 1
|
|
local hashSize = 4096
|
|
local function hash4(p)
|
|
return (((data:byte(p) * 256 + data:byte(p + 1)) * 256 + data:byte(p + 2)) * 256 + data:byte(p + 3)) % hashSize
|
|
end
|
|
while ip + 3 <= n do
|
|
local h = hash4(ip)
|
|
local ref = ht[h]
|
|
ht[h] = ip
|
|
if ref and ip - ref <= 65535 and data:byte(ref) == data:byte(ip)
|
|
and data:byte(ref + 1) == data:byte(ip + 1)
|
|
and data:byte(ref + 2) == data:byte(ip + 2)
|
|
and data:byte(ref + 3) == data:byte(ip + 3) then
|
|
local ml = 4
|
|
while ip + ml <= n and data:byte(ref + ml) == data:byte(ip + ml) do ml = ml + 1 end
|
|
local litLen = ip - anchor
|
|
local matchLen = ml - 4
|
|
local tokLit = litLen < 15 and litLen or 15
|
|
local tokMatch = matchLen < 15 and matchLen or 15
|
|
res[#res + 1] = string.char(tokLit * 16 + tokMatch)
|
|
if litLen >= 15 then
|
|
local e = litLen - 15
|
|
while e >= 255 do res[#res + 1] = string.char(255); e = e - 255 end
|
|
res[#res + 1] = string.char(e)
|
|
end
|
|
if litLen > 0 then res[#res + 1] = data:sub(anchor, ip - 1) end
|
|
local off = ip - ref
|
|
res[#res + 1] = string.char(off % 256)
|
|
res[#res + 1] = string.char(math.floor(off / 256))
|
|
if matchLen >= 15 then
|
|
local e = matchLen - 15
|
|
while e >= 255 do res[#res + 1] = string.char(255); e = e - 255 end
|
|
res[#res + 1] = string.char(e)
|
|
end
|
|
ip = ip + ml
|
|
anchor = ip
|
|
else
|
|
ip = ip + 1
|
|
end
|
|
end
|
|
local last = n - anchor + 1
|
|
if last > 0 then
|
|
local tokLit = last < 15 and last or 15
|
|
res[#res + 1] = string.char(tokLit * 16)
|
|
if last >= 15 then
|
|
local e = last - 15
|
|
while e >= 255 do res[#res + 1] = string.char(255); e = e - 255 end
|
|
res[#res + 1] = string.char(e)
|
|
end
|
|
res[#res + 1] = data:sub(anchor, n)
|
|
end
|
|
return table.concat(res)
|
|
end
|
|
|
|
function lz4Decompress(compressed)
|
|
local pos, out, oi = 1, {}, 1
|
|
local n = #compressed
|
|
while pos <= n do
|
|
local token = compressed:byte(pos); pos = pos + 1
|
|
local litLen = math.floor(token / 16)
|
|
if litLen == 15 then
|
|
local s
|
|
repeat s = compressed:byte(pos); pos = pos + 1; litLen = litLen + s until s < 255
|
|
end
|
|
for _ = 1, litLen do out[oi] = compressed:byte(pos); oi = oi + 1; pos = pos + 1 end
|
|
if pos > n then break end
|
|
local off = compressed:byte(pos) + compressed:byte(pos + 1) * 256; pos = pos + 2
|
|
local matchLen = (token % 16) + 4
|
|
if matchLen == 19 then
|
|
local s
|
|
repeat s = compressed:byte(pos); pos = pos + 1; matchLen = matchLen + s until s < 255
|
|
end
|
|
local mp = oi - off
|
|
for _ = 1, matchLen do out[oi] = out[mp]; oi = oi + 1; mp = mp + 1 end
|
|
end
|
|
return string.char(_unpack(out))
|
|
end
|
|
|
|
local b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
local b64dec = {}
|
|
for i = 1, 64 do b64dec[b64:sub(i, i)] = i - 1 end
|
|
|
|
function encode(s)
|
|
local out, i, n = {}, 1, #s
|
|
while i <= n do
|
|
local a, b, c = s:byte(i), s:byte(i + 1), s:byte(i + 2)
|
|
out[#out + 1] = b64:sub(math.floor(a / 4) + 1, math.floor(a / 4) + 1)
|
|
out[#out + 1] = b64:sub((a % 4) * 16 + math.floor((b or 0) / 16) + 1, (a % 4) * 16 + math.floor((b or 0) / 16) + 1)
|
|
if i + 1 > n then
|
|
out[#out + 1] = "="
|
|
else
|
|
out[#out + 1] = b64:sub(((b or 0) % 16) * 4 + math.floor((c or 0) / 64) + 1, ((b or 0) % 16) * 4 + math.floor((c or 0) / 64) + 1)
|
|
end
|
|
if i + 2 > n then
|
|
out[#out + 1] = "="
|
|
else
|
|
out[#out + 1] = b64:sub(((c or 0) % 64) + 1, ((c or 0) % 64) + 1)
|
|
end
|
|
i = i + 3
|
|
end
|
|
return table.concat(out)
|
|
end
|
|
|
|
function decode(str)
|
|
local out, i, n = {}, 1, #str
|
|
while i <= n and str:sub(i, i) ~= "=" do
|
|
local a = b64dec[str:sub(i, i)] or 0
|
|
local b = b64dec[str:sub(i + 1, i + 1)] or 0
|
|
local c = b64dec[str:sub(i + 2, i + 2)] or 0
|
|
local d = b64dec[str:sub(i + 3, i + 3)] or 0
|
|
out[#out + 1] = string.char(a * 4 + math.floor(b / 16))
|
|
if str:sub(i + 2, i + 2) ~= "=" then
|
|
out[#out + 1] = string.char((b % 16) * 16 + math.floor(c / 4))
|
|
end
|
|
if str:sub(i + 3, i + 3) ~= "=" then
|
|
out[#out + 1] = string.char((c % 4) * 64 + d)
|
|
end
|
|
i = i + 4
|
|
end
|
|
return table.concat(out)
|
|
end
|
|
|
|
local function escRaw(s)
|
|
s = s:gsub("\x01", "\x01\x01")
|
|
s = s:gsub("\n", "\x01n")
|
|
return s
|
|
end
|
|
|
|
local function unescRaw(s)
|
|
local out, i, n = {}, 1, #s
|
|
while i <= n do
|
|
local b = s:byte(i)
|
|
if b == 1 then
|
|
i = i + 1
|
|
out[#out + 1] = s:byte(i) == 1 and "\x01" or "\n"
|
|
else
|
|
out[#out + 1] = s:sub(i, i)
|
|
end
|
|
i = i + 1
|
|
end
|
|
return table.concat(out)
|
|
end
|
|
|
|
function compressStr(s)
|
|
local lz = encode(lz4Compress(s))
|
|
if #lz < #s then return lz end
|
|
return "\x01" .. escRaw(s)
|
|
end
|
|
|
|
function decompressStr(c)
|
|
if c:byte(1) == 1 then return unescRaw(c:sub(2)) end
|
|
return lz4Decompress(decode(c))
|
|
end
|
|
|
|
-- ================================
|
|
|
|
local function parseMsg(msg)
|
|
local p = msg:find("|")
|
|
return p and msg:sub(1, p - 1) or msg, p and msg:sub(p + 1) or nil
|
|
end
|
|
|
|
local function split(str, sep)
|
|
local t = {}
|
|
if not str then return t end
|
|
for s in str:gmatch("[^" .. sep .. "]+") do
|
|
t[#t + 1] = s
|
|
end
|
|
return t
|
|
end
|
|
|
|
local inodes = {}
|
|
local dirs = {}
|
|
local files = {}
|
|
local users = {}
|
|
local sb = { seg_size = SEG_SIZE, max_inodes = MAX_INODES, root_ino = ROOT_INO }
|
|
|
|
function fsSerialize()
|
|
local lines = {}
|
|
lines[#lines + 1] = "V|1"
|
|
lines[#lines + 1] = "S|" .. sb.seg_size .. "|" .. sb.max_inodes .. "|" .. sb.root_ino
|
|
for _, u in pairs(users) do
|
|
lines[#lines + 1] = "U|" .. u.uid .. "|" .. u.name .. "|" .. u.gid .. "|" .. u.home .. "|" .. (u.hash or "-")
|
|
end
|
|
for i = 1, MAX_INODES do
|
|
local ino = inodes[i]
|
|
if ino then
|
|
lines[#lines + 1] = "I|" .. ino.ino .. "|" .. ino.typ .. "|" .. ino.mode .. "|" .. ino.uid .. "|" .. ino.gid .. "|" .. ino.size .. "|" .. ino.mtime .. "|"
|
|
if ino.typ == "d" and dirs[ino.ino] then
|
|
local entries = {}
|
|
for _, e in ipairs(dirs[ino.ino]) do
|
|
entries[#entries + 1] = e.name .. ":" .. e.ino
|
|
end
|
|
lines[#lines + 1] = "D|" .. ino.ino .. "|" .. table.concat(entries, "|")
|
|
elseif ino.typ == "f" and files[ino.ino] then
|
|
lines[#lines + 1] = "Z|" .. ino.ino .. "|" .. files[ino.ino]
|
|
end
|
|
end
|
|
end
|
|
return table.concat(lines, "\n")
|
|
end
|
|
|
|
local function fsDeserialize(str)
|
|
inodes = {}
|
|
dirs = {}
|
|
files = {}
|
|
users = {}
|
|
sb = { seg_size = SEG_SIZE, max_inodes = MAX_INODES, root_ino = ROOT_INO }
|
|
if not str or #str == 0 then return false end
|
|
for line in str:gmatch("[^\n]+") do
|
|
local typ = line:sub(1, 1)
|
|
local rest = line:sub(3)
|
|
if typ == "V" then
|
|
elseif typ == "S" then
|
|
local parts = split(rest, "|")
|
|
if #parts >= 3 then
|
|
sb.seg_size = tonumber(parts[1]) or SEG_SIZE
|
|
sb.max_inodes = tonumber(parts[2]) or MAX_INODES
|
|
sb.root_ino = tonumber(parts[3]) or ROOT_INO
|
|
end
|
|
elseif typ == "U" then
|
|
local parts = split(rest, "|")
|
|
if #parts >= 4 then
|
|
users[tonumber(parts[1])] = { uid = tonumber(parts[1]), name = parts[2], gid = tonumber(parts[3]), home = parts[4], hash = parts[5] or "-" }
|
|
end
|
|
elseif typ == "I" then
|
|
local parts = split(rest, "|")
|
|
if #parts >= 7 then
|
|
local ino = tonumber(parts[1])
|
|
inodes[ino] = { ino = ino, typ = parts[2], mode = parts[3], uid = tonumber(parts[4]), gid = tonumber(parts[5]), size = tonumber(parts[6]), mtime = tonumber(parts[7]) or 0 }
|
|
end
|
|
elseif typ == "D" then
|
|
local parts = split(rest, "|")
|
|
if #parts >= 2 then
|
|
local ino = tonumber(parts[1])
|
|
local entries = {}
|
|
for i = 2, #parts do
|
|
local eparts = split(parts[i], ":")
|
|
if #eparts >= 2 then
|
|
entries[#entries + 1] = { name = eparts[1], ino = tonumber(eparts[2]) }
|
|
end
|
|
end
|
|
dirs[ino] = entries
|
|
end
|
|
elseif typ == "Z" then
|
|
local parts = split(rest, "|")
|
|
if #parts >= 2 then
|
|
local ino = tonumber(parts[1])
|
|
local b64 = parts[2] or ""
|
|
for i = 3, #parts do
|
|
b64 = b64 .. "|" .. parts[i]
|
|
end
|
|
files[ino] = b64
|
|
end
|
|
end
|
|
end
|
|
return next(inodes) ~= nil
|
|
end
|
|
|
|
local function findInodeByPath(path)
|
|
if not path or #path == 0 then path = "/" end
|
|
local parts = split(path, "/")
|
|
local cur = inodes[sb.root_ino]
|
|
if not cur then return nil end
|
|
if path == "/" then return cur end
|
|
local curIno = sb.root_ino
|
|
for i = 1, #parts do
|
|
local name = parts[i]
|
|
if #name > 0 then
|
|
local found = false
|
|
local dentries = dirs[curIno]
|
|
if dentries then
|
|
for _, e in ipairs(dentries) do
|
|
if e.name == name then
|
|
curIno = e.ino
|
|
cur = inodes[curIno]
|
|
found = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if not found then return nil end
|
|
end
|
|
end
|
|
return cur and inodes[curIno] or nil
|
|
end
|
|
|
|
local respSeq = 0
|
|
|
|
local function processReq(req)
|
|
local op, rest = parseMsg(req)
|
|
if not op then return "ERR|EINVAL" end
|
|
respSeq = respSeq + 1
|
|
local seqStr = tostring(respSeq) .. "|"
|
|
|
|
if op == "STAT" then
|
|
local path = rest or "/"
|
|
local ino = findInodeByPath(path)
|
|
if not ino then return "ERR|" .. seqStr .. "ENOENT" end
|
|
return "OK|" .. seqStr .. ino.ino .. "|" .. ino.typ .. "|" .. ino.mode .. "|" .. ino.uid .. "|" .. ino.gid .. "|" .. ino.size .. "|" .. ino.mtime
|
|
|
|
elseif op == "READ" then
|
|
local parts = split(rest or "", "|")
|
|
local path = parts[1] or ""
|
|
local offset = tonumber(parts[2]) or 0
|
|
local len = tonumber(parts[3]) or 8192
|
|
local ino = findInodeByPath(path)
|
|
if not ino then return "ERR|" .. seqStr .. "ENOENT" end
|
|
if ino.typ ~= "f" then return "ERR|" .. seqStr .. "EISDIR" end
|
|
local raw = decompressStr(files[ino.ino]) or ""
|
|
local data = raw:sub(offset + 1, offset + len)
|
|
return "OK|" .. seqStr .. data
|
|
|
|
elseif op == "LS" then
|
|
local path = rest or "/"
|
|
local ino = findInodeByPath(path)
|
|
if not ino then return "ERR|" .. seqStr .. "ENOENT" end
|
|
if ino.typ ~= "d" then return "ERR|" .. seqStr .. "ENOTDIR" end
|
|
local dentries = dirs[ino.ino]
|
|
if not dentries then return "OK|" .. seqStr end
|
|
local names = {}
|
|
for _, e in ipairs(dentries) do
|
|
names[#names + 1] = e.name
|
|
end
|
|
return "OK|" .. seqStr .. table.concat(names, "|")
|
|
|
|
elseif op == "STATFS" then
|
|
local total = sb.seg_size
|
|
local used = #fsSerialize()
|
|
return "OK|" .. seqStr .. total .. "|" .. used
|
|
|
|
elseif op == "CHDIR" then
|
|
local path = rest or "/"
|
|
local ino = findInodeByPath(path)
|
|
if not ino then return "ERR|" .. seqStr .. "ENOENT" end
|
|
if ino.typ ~= "d" then return "ERR|" .. seqStr .. "ENOTDIR" end
|
|
return "OK|" .. seqStr .. path
|
|
|
|
elseif op == "WRITE" then
|
|
return "ERR|" .. seqStr .. "EROFS"
|
|
end
|
|
|
|
return "ERR|" .. seqStr .. "EINVAL"
|
|
end
|
|
|
|
-- Exported FS API for external tools (generators, tests)
|
|
FS = {}
|
|
|
|
function FS.init()
|
|
inodes = {}
|
|
dirs = {}
|
|
files = {}
|
|
users = {}
|
|
sb = { seg_size = SEG_SIZE, max_inodes = MAX_INODES, root_ino = ROOT_INO }
|
|
inodes[1] = { ino = 1, typ = "d", mode = "755", uid = 0, gid = 0, size = 0, mtime = 0 }
|
|
dirs[1] = { { name = ".", ino = 1 }, { name = "..", ino = 1 } }
|
|
end
|
|
|
|
function FS.addUser(uid, name, gid, home, hash)
|
|
users[uid] = { uid = uid, name = name, gid = gid, home = home, hash = hash or "-" }
|
|
end
|
|
|
|
function FS.resolve(path)
|
|
return findInodeByPath(path)
|
|
end
|
|
|
|
function FS.serialize()
|
|
return fsSerialize()
|
|
end
|
|
|
|
function FS.deserialize(str)
|
|
return fsDeserialize(str)
|
|
end
|
|
|
|
function FS.mkdir(path, mode, uid, gid)
|
|
local parentPath, name = path:match("^(.*)/([^/]+)$")
|
|
if not name then
|
|
parentPath, name = "/", path:match("^/?(.+)$") or path
|
|
end
|
|
local parent = findInodeByPath(parentPath)
|
|
if not parent then return nil, "parent not found" end
|
|
if parent.typ ~= "d" then return nil, "not a directory" end
|
|
local ino
|
|
for i = 1, MAX_INODES do
|
|
if not inodes[i] then ino = i; break end
|
|
end
|
|
if not ino then return nil, "no free inodes" end
|
|
inodes[ino] = { ino = ino, typ = "d", mode = mode or "755", uid = uid or 0, gid = gid or 0, size = 0, mtime = 0 }
|
|
dirs[ino] = { { name = ".", ino = ino }, { name = "..", ino = parent.ino } }
|
|
table.insert(dirs[parent.ino], { name = name, ino = ino })
|
|
return ino
|
|
end
|
|
|
|
function FS.mkfile(path, content, mode, uid, gid)
|
|
local parentPath, name = path:match("^(.*)/([^/]+)$")
|
|
if not name then
|
|
parentPath, name = "/", path:match("^/?(.+)$") or path
|
|
end
|
|
local parent = findInodeByPath(parentPath)
|
|
if not parent then return nil, "parent not found" end
|
|
if parent.typ ~= "d" then return nil, "not a directory" end
|
|
local ino
|
|
for i = 1, MAX_INODES do
|
|
if not inodes[i] then ino = i; break end
|
|
end
|
|
if not ino then return nil, "no free inodes" end
|
|
content = content or ""
|
|
inodes[ino] = { ino = ino, typ = "f", mode = mode or "644", uid = uid or 0, gid = gid or 0, size = #content, mtime = 0 }
|
|
files[ino] = compressStr(content)
|
|
table.insert(dirs[parent.ino], { name = name, ino = ino })
|
|
return ino
|
|
end
|
|
|
|
function FS.write(path, content)
|
|
local ino = findInodeByPath(path)
|
|
if not ino then return nil, "not found" end
|
|
if ino.typ ~= "f" then return nil, "not a file" end
|
|
content = content or ""
|
|
files[ino.ino] = compressStr(content)
|
|
inodes[ino.ino].size = #content
|
|
return true
|
|
end
|
|
|
|
function FS.read(path)
|
|
local ino = findInodeByPath(path)
|
|
if not ino then return nil, "not found" end
|
|
if ino.typ ~= "f" then return nil, "not a file" end
|
|
return decompressStr(files[ino.ino])
|
|
end
|
|
|
|
function FS.ls(path)
|
|
local ino = findInodeByPath(path or "/")
|
|
if not ino then return nil, "not found" end
|
|
if ino.typ ~= "d" then return nil, "not a directory" end
|
|
local result = {}
|
|
for _, e in ipairs(dirs[ino.ino]) do
|
|
result[#result + 1] = e.name
|
|
end
|
|
return result
|
|
end
|
|
|
|
function FS.stat(path)
|
|
local ino = findInodeByPath(path)
|
|
if not ino then return nil, "not found" end
|
|
local result = {}
|
|
for k, v in pairs(ino) do
|
|
result[k] = v
|
|
end
|
|
return result
|
|
end
|
|
|
|
local fsLoaded = false
|
|
local prevReq = ""
|
|
local writePending = false
|
|
|
|
inp = {}
|
|
|
|
function upd()
|
|
if not fsLoaded then
|
|
local memStr = inp[3] or ""
|
|
if #memStr > 0 and fsDeserialize(memStr) then
|
|
fsLoaded = true
|
|
else
|
|
fsLoaded = true
|
|
FS.init()
|
|
FS.addUser(0, "root", 0, "/", "-")
|
|
writePending = true
|
|
end
|
|
end
|
|
|
|
if writePending then
|
|
local str = fsSerialize()
|
|
out[5] = str
|
|
out[6] = 1
|
|
writePending = false
|
|
end
|
|
|
|
if inp[1] ~= nil then
|
|
local req = tostring(inp[1])
|
|
if #req == 0 then
|
|
prevReq = ""
|
|
elseif req ~= prevReq then
|
|
prevReq = req
|
|
local resp = processReq(req)
|
|
out[1] = resp
|
|
end
|
|
else
|
|
prevReq = ""
|
|
end
|
|
|
|
table.clear(inp)
|
|
end
|