-- 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