luajitos

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

RamDisk.lua (29278B)


      1 
      2 local RamDisk = {}
      3 
      4 -- Node metatable for directories
      5 local dirNode = {}
      6 dirNode.__index = dirNode
      7 
      8 -- Node metatable for files
      9 local fileNode = {}
     10 fileNode.__index = fileNode
     11 
     12 -- Helper function to split path into components
     13 local function splitPath(path)
     14     local components = {}
     15     -- Remove leading slash if present
     16     path = path:gsub("^/+", "")
     17     -- Split by slash
     18     for component in path:gmatch("[^/]+") do
     19         table.insert(components, component)
     20     end
     21     return components
     22 end
     23 
     24 -- Helper function to find root node
     25 local function findRoot(node)
     26     while node.parent ~= nil do
     27         node = node.parent
     28     end
     29     return node
     30 end
     31 
     32 -- Traverse method for both file and directory nodes (absolute paths from root)
     33 local function traverse(self, path)
     34     -- Start from root
     35     local root = findRoot(self)
     36     local current = root
     37 
     38     -- Split path into components
     39     local components = splitPath(path)
     40 
     41     -- Traverse through each component
     42     for i, component in ipairs(components) do
     43         if current.type ~= "directory" then
     44             return nil, "not a directory: " .. current.name
     45         end
     46 
     47         -- Check if this is the last component
     48         local isLast = (i == #components)
     49 
     50         -- Search in directories first
     51         local found = false
     52         for _, dir in ipairs(current.dirs) do
     53             if dir.name == component then
     54                 current = dir
     55                 found = true
     56                 break
     57             end
     58         end
     59 
     60         -- If not found in dirs and this is the last component, check files
     61         if not found and isLast then
     62             for _, file in ipairs(current.files) do
     63                 if file.name == component then
     64                     current = file
     65                     found = true
     66                     break
     67                 end
     68             end
     69         end
     70 
     71         -- If still not found, check in dirs for last component (could be a directory)
     72         if not found then
     73             return nil, "path not found: " .. component
     74         end
     75     end
     76 
     77     return current
     78 end
     79 
     80 -- Find method for both file and directory nodes (supports relative paths)
     81 local function find(self, path)
     82     -- Check if path starts with / (absolute path)
     83     if path:sub(1, 1) == "/" then
     84         -- Absolute path - start from root
     85         return traverse(self, path)
     86     end
     87 
     88     -- Relative path - start from current node
     89     local current = self
     90 
     91     -- Split path into components
     92     local components = splitPath(path)
     93 
     94     -- Traverse through each component
     95     for i, component in ipairs(components) do
     96         if current.type ~= "directory" then
     97             return nil, "not a directory: " .. current.name
     98         end
     99 
    100         -- Check if this is the last component
    101         local isLast = (i == #components)
    102 
    103         -- Search in directories first
    104         local found = false
    105         for _, dir in ipairs(current.dirs) do
    106             if dir.name == component then
    107                 current = dir
    108                 found = true
    109                 break
    110             end
    111         end
    112 
    113         -- If not found in dirs and this is the last component, check files
    114         if not found and isLast then
    115             for _, file in ipairs(current.files) do
    116                 if file.name == component then
    117                     current = file
    118                     found = true
    119                     break
    120                 end
    121             end
    122         end
    123 
    124         -- If still not found, return nil
    125         if not found then
    126             return nil, "path not found: " .. component
    127         end
    128     end
    129 
    130     return current
    131 end
    132 
    133 -- Method to create a new directory
    134 function dirNode:newDir(name)
    135     local dir = {
    136         name = name,
    137         type = "directory",
    138         parent = self,
    139         dirs = {},
    140         files = {}
    141     }
    142     setmetatable(dir, dirNode)
    143     table.insert(self.dirs, dir)
    144     return dir
    145 end
    146 
    147 -- Method to create a new file
    148 function dirNode:newFile(name, contents)
    149     local file = {
    150         name = name,
    151         type = "file",
    152         parent = self,
    153         contents = contents or ""
    154     }
    155     setmetatable(file, fileNode)
    156     table.insert(self.files, file)
    157     return file
    158 end
    159 
    160 -- Read method for files
    161 function fileNode:read()
    162     return self.contents
    163 end
    164 
    165 -- Write method for files
    166 function fileNode:write(contents)
    167     self.contents = contents
    168 end
    169 
    170 -- Delete method for files
    171 function fileNode:delete()
    172     if self.parent then
    173         -- Remove from parent's files array
    174         for i, file in ipairs(self.parent.files) do
    175             if file == self then
    176                 table.remove(self.parent.files, i)
    177                 break
    178             end
    179         end
    180     end
    181     -- Clear file data
    182     self.name = nil
    183     self.parent = nil
    184     self.contents = nil
    185 end
    186 
    187 -- Delete method for directories
    188 function dirNode:delete()
    189     if self.parent then
    190         -- Remove from parent's dirs array
    191         for i, dir in ipairs(self.parent.dirs) do
    192             if dir == self then
    193                 table.remove(self.parent.dirs, i)
    194                 break
    195             end
    196         end
    197     end
    198     -- Clear directory data
    199     self.name = nil
    200     self.parent = nil
    201     -- Note: dirs and files arrays are left for garbage collection
    202 end
    203 
    204 -- Add traverse and find methods to both metatables
    205 dirNode.traverse = traverse
    206 fileNode.traverse = traverse
    207 dirNode.find = find
    208 fileNode.find = find
    209 
    210 -- Function to create a root directory node
    211 local function createRoot()
    212     local root = {
    213         name = "",
    214         type = "directory",
    215         parent = nil,
    216         dirs = {},
    217         files = {}
    218     }
    219     setmetatable(root, dirNode)
    220     return root
    221 end
    222 
    223 -- Helper function to get full path of a node
    224 local function getFullPath(node)
    225     local parts = {}
    226     local current = node
    227 
    228     while current.parent ~= nil do
    229         table.insert(parts, 1, current.name)
    230         current = current.parent
    231     end
    232 
    233     return table.concat(parts, "/")
    234 end
    235 
    236 -- Helper function to check if content is binary (contains non-printable chars)
    237 local function isBinary(content)
    238     if type(content) ~= "string" then
    239         return false
    240     end
    241 
    242     for i = 1, #content do
    243         local byte = content:byte(i)
    244         -- Check for control characters except tab, newline, carriage return
    245         if byte < 32 and byte ~= 9 and byte ~= 10 and byte ~= 13 then
    246             return true
    247         end
    248         -- Check for extended ASCII
    249         if byte > 127 then
    250             return true
    251         end
    252     end
    253     return false
    254 end
    255 
    256 -- Helper function to find character position in base64 alphabet
    257 local function base64CharPos(c)
    258     local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
    259     for i = 1, 64 do
    260         if b:sub(i, i) == c then
    261             return i - 1
    262         end
    263     end
    264     return nil
    265 end
    266 
    267 -- Helper function to encode binary data to base64
    268 local function base64Encode(data)
    269     local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
    270     local result = {}
    271     local bit = require("bit")
    272 
    273     -- Process 3 bytes at a time
    274     for i = 1, #data, 3 do
    275         local b1 = string.byte(data, i)
    276         local b2 = string.byte(data, i + 1)
    277         local b3 = string.byte(data, i + 2)
    278 
    279         -- First 6 bits from b1
    280         local c1 = b:sub(bit.rshift(b1, 2) + 1, bit.rshift(b1, 2) + 1)
    281 
    282         if b2 then
    283             -- Last 2 bits of b1 + first 4 bits of b2
    284             local c2 = b:sub(bit.bor(bit.lshift(bit.band(b1, 0x03), 4), bit.rshift(b2, 4)) + 1, bit.bor(bit.lshift(bit.band(b1, 0x03), 4), bit.rshift(b2, 4)) + 1)
    285 
    286             if b3 then
    287                 -- Last 4 bits of b2 + first 2 bits of b3
    288                 local c3 = b:sub(bit.bor(bit.lshift(bit.band(b2, 0x0F), 2), bit.rshift(b3, 6)) + 1, bit.bor(bit.lshift(bit.band(b2, 0x0F), 2), bit.rshift(b3, 6)) + 1)
    289                 -- Last 6 bits of b3
    290                 local c4 = b:sub(bit.band(b3, 0x3F) + 1, bit.band(b3, 0x3F) + 1)
    291                 table.insert(result, c1 .. c2 .. c3 .. c4)
    292             else
    293                 -- Last 4 bits of b2, padded
    294                 local c3 = b:sub(bit.lshift(bit.band(b2, 0x0F), 2) + 1, bit.lshift(bit.band(b2, 0x0F), 2) + 1)
    295                 table.insert(result, c1 .. c2 .. c3 .. '=')
    296             end
    297         else
    298             -- Last 2 bits of b1, padded
    299             local c2 = b:sub(bit.lshift(bit.band(b1, 0x03), 4) + 1, bit.lshift(bit.band(b1, 0x03), 4) + 1)
    300             table.insert(result, c1 .. c2 .. '==')
    301         end
    302     end
    303 
    304     return table.concat(result)
    305 end
    306 
    307 -- Helper function to decode base64 to binary data
    308 local function base64Decode(data)
    309     osprint("base64Decode: Starting decode, data length = ")
    310     osprint(tostring(#data))
    311     osprint("\n")
    312 
    313     local result = {}
    314     local dataLen = #data
    315     local bit = require("bit")
    316 
    317     -- Process 4 characters at a time
    318     for i = 1, dataLen, 4 do
    319         local c1 = data:sub(i, i)
    320         local c2 = data:sub(i + 1, i + 1)
    321         local c3 = data:sub(i + 2, i + 2)
    322         local c4 = data:sub(i + 3, i + 3)
    323 
    324         osprint("base64Decode: Processing position ")
    325         osprint(tostring(i))
    326         osprint("\n")
    327 
    328         -- Decode first two chars (always present)
    329         local v1 = base64CharPos(c1)
    330         local v2 = base64CharPos(c2)
    331 
    332         if v1 and v2 then
    333             -- First byte: 6 bits from v1 + 2 bits from v2
    334             local b1 = bit.bor(bit.lshift(v1, 2), bit.rshift(v2, 4))
    335             table.insert(result, string.char(b1))
    336 
    337             -- Check for padding
    338             if c3 ~= '=' then
    339                 local v3 = base64CharPos(c3)
    340                 if v3 then
    341                     -- Second byte: 4 bits from v2 + 4 bits from v3
    342                     local b2 = bit.bor(bit.lshift(bit.band(v2, 0x0F), 4), bit.rshift(v3, 2))
    343                     table.insert(result, string.char(b2))
    344 
    345                     if c4 ~= '=' then
    346                         local v4 = base64CharPos(c4)
    347                         if v4 then
    348                             -- Third byte: 2 bits from v3 + 6 bits from v4
    349                             local b3 = bit.bor(bit.lshift(bit.band(v3, 0x03), 6), v4)
    350                             table.insert(result, string.char(b3))
    351                         end
    352                     end
    353                 end
    354             end
    355         end
    356 
    357         if i % 100 == 0 then
    358             osprint("base64Decode: Progress ")
    359             osprint(tostring(i))
    360             osprint("/")
    361             osprint(tostring(dataLen))
    362             osprint("\n")
    363         end
    364     end
    365 
    366     osprint("base64Decode: Completed\n")
    367     return table.concat(result)
    368 end
    369 
    370 -- Helper function to escape special characters in text content
    371 local function escapeText(text)
    372     local result = {}
    373     for i = 1, #text do
    374         local c = text:sub(i, i)
    375         if c == "\\" then
    376             table.insert(result, "\\\\")
    377         elseif c == "\n" then
    378             table.insert(result, "\\n")
    379         elseif c == "\t" then
    380             table.insert(result, "\\t")
    381         elseif c == "\r" then
    382             table.insert(result, "\\r")
    383         else
    384             table.insert(result, c)
    385         end
    386     end
    387     return table.concat(result)
    388 end
    389 
    390 -- Helper function to unescape special characters in text content
    391 local function unescapeText(text)
    392     osprint("unescapeText: Starting, length = ")
    393     osprint(tostring(#text))
    394     osprint("\n")
    395 
    396     local result = {}
    397     local i = 1
    398     local textLen = #text
    399 
    400     while i <= textLen do
    401         local c = text:sub(i, i)
    402         if c == "\\" and i < textLen then
    403             local next = text:sub(i + 1, i + 1)
    404             if next == "r" then
    405                 table.insert(result, "\r")
    406                 i = i + 2
    407             elseif next == "t" then
    408                 table.insert(result, "\t")
    409                 i = i + 2
    410             elseif next == "n" then
    411                 table.insert(result, "\n")
    412                 i = i + 2
    413             elseif next == "\\" then
    414                 table.insert(result, "\\")
    415                 i = i + 2
    416             else
    417                 table.insert(result, c)
    418                 i = i + 1
    419             end
    420         else
    421             table.insert(result, c)
    422             i = i + 1
    423         end
    424     end
    425 
    426     osprint("unescapeText: Completed\n")
    427     return table.concat(result)
    428 end
    429 
    430 -- Function to save the filesystem to serialized format
    431 local function saveFS(rootdir)
    432     local dirs = {}
    433     local files = {}
    434 
    435     -- Recursive function to traverse directories
    436     local function traverseDir(dir, currentPath)
    437         -- Add directory path (with trailing slash)
    438         if currentPath ~= "" then
    439             table.insert(dirs, currentPath .. "/")
    440         end
    441 
    442         -- Process subdirectories
    443         for _, subdir in ipairs(dir.dirs) do
    444             local subdirPath = currentPath == "" and subdir.name or currentPath .. "/" .. subdir.name
    445             traverseDir(subdir, subdirPath)
    446         end
    447 
    448         -- Process files
    449         for _, file in ipairs(dir.files) do
    450             local filePath = currentPath == "" and file.name or currentPath .. "/" .. file.name
    451             local contents = file.contents or ""
    452 
    453             if isBinary(contents) then
    454                 table.insert(files, filePath .. "~~".."~~BIN:" .. base64Encode(contents))
    455             else
    456                 table.insert(files, filePath .. "~~".."~~" .. escapeText(contents))
    457             end
    458         end
    459     end
    460 
    461     -- Start traversal from root
    462     traverseDir(rootdir, "")
    463 
    464     -- Build the final format: [[dirs||||files]]
    465     local dirSection = table.concat(dirs, "||".."||")
    466     local fileSection = table.concat(files, "||".."||")
    467 
    468     return "[[" .. dirSection .. "||".."||" .. fileSection .. "]]"
    469 end
    470 
    471 -- Function to load a filesystem from serialized format
    472 local function loadFS(serialized)
    473     osprint("loadFS: Creating root\n")
    474     -- Create new root
    475     local root = createRoot()
    476 
    477     osprint("loadFS: Processing content\n")
    478     -- The serialized data is already in the raw format (no [[ ]] wrapper)
    479     local content = serialized
    480 
    481     osprint("loadFS: Splitting entries\n")
    482     -- Split by |||| to separate entries (optimized with table for string building)
    483     local entries = {}
    484     local currentEntry = {}
    485     local i = 1
    486     local content_len = #content
    487 
    488     while i <= content_len do
    489         -- Check if we're at a |||| separator
    490         if i + 3 <= content_len and content:sub(i, i+3) == "||".."||" then
    491             if #currentEntry > 0 then
    492                 table.insert(entries, table.concat(currentEntry))
    493                 currentEntry = {}
    494             end
    495             i = i + 4  -- Skip the ||||
    496         else
    497             table.insert(currentEntry, content:sub(i, i))
    498             i = i + 1
    499         end
    500     end
    501     -- Don't forget the last entry
    502     if #currentEntry > 0 then
    503         table.insert(entries, table.concat(currentEntry))
    504     end
    505 
    506     osprint("loadFS: Found ")
    507     osprint(tostring(#entries))
    508     osprint(" entries\n")
    509 
    510     -- Track created directories
    511     local createdDirs = {}
    512     createdDirs[""] = root  -- Root is always available
    513 
    514     -- Helper to ensure directory exists
    515     local function ensureDir(path)
    516         if createdDirs[path] then
    517             return createdDirs[path]
    518         end
    519 
    520         -- Split path into components
    521         local components = splitPath(path)
    522         local current = root
    523         local currentPath = ""
    524 
    525         for _, component in ipairs(components) do
    526             local nextPath = currentPath == "" and component or currentPath .. "/" .. component
    527 
    528             if not createdDirs[nextPath] then
    529                 -- Check if directory already exists
    530                 local found = false
    531                 for _, dir in ipairs(current.dirs) do
    532                     if dir.name == component then
    533                         current = dir
    534                         found = true
    535                         break
    536                     end
    537                 end
    538 
    539                 if not found then
    540                     current = current:newDir(component)
    541                 end
    542 
    543                 createdDirs[nextPath] = current
    544             else
    545                 current = createdDirs[nextPath]
    546             end
    547 
    548             currentPath = nextPath
    549         end
    550 
    551         return current
    552     end
    553 
    554     -- First pass: create all directories
    555     osprint("loadFS: Creating directories...\n")
    556     for i, entry in ipairs(entries) do
    557         osprint("loadFS: Processing entry ")
    558         osprint(tostring(i))
    559         osprint("\n")
    560         -- Check if ends with "/" by looking at last character
    561         if #entry > 0 and entry:sub(-1, -1) == "/" then  -- It's a directory
    562             osprint("loadFS: Found directory: ")
    563             osprint(entry)
    564             osprint("\n")
    565             local dirPath = entry:sub(1, -2)  -- Remove trailing slash
    566             ensureDir(dirPath)
    567         end
    568     end
    569     osprint("loadFS: All directories created\n")
    570 
    571     -- Second pass: create all files
    572     osprint("loadFS: Creating files...\n")
    573     for i, entry in ipairs(entries) do
    574         -- Check if contains ~~~~ by manual search
    575         local separatorPos = nil
    576         for j = 1, #entry - 3 do
    577             if entry:sub(j, j+3) == "~~".."~~" then
    578                 separatorPos = j
    579                 break
    580             end
    581         end
    582 
    583         if separatorPos then  -- It's a file
    584             osprint("loadFS: Processing file entry ")
    585             osprint(tostring(i))
    586             osprint("\n")
    587 
    588             local filePath = entry:sub(1, separatorPos - 1)
    589             local fileContent = entry:sub(separatorPos + 4)
    590 
    591             osprint("loadFS: File path: ")
    592             osprint(filePath)
    593             osprint("\n")
    594 
    595             -- Determine parent directory and filename by finding last /
    596             local lastSlash = nil
    597             for j = #filePath, 1, -1 do
    598                 if filePath:sub(j, j) == "/" then
    599                     lastSlash = j
    600                     break
    601                 end
    602             end
    603 
    604             local parentPath, fileName
    605             if lastSlash then
    606                 parentPath = filePath:sub(1, lastSlash - 1)
    607                 fileName = filePath:sub(lastSlash + 1)
    608             else
    609                 parentPath = ""
    610                 fileName = filePath
    611             end
    612 
    613             osprint("loadFS: Parent path: [")
    614             osprint(parentPath)
    615             osprint("] File name: [")
    616             osprint(fileName)
    617             osprint("]\n")
    618 
    619             -- Get or create parent directory
    620             local parentDir = ensureDir(parentPath)
    621 
    622             -- Check if it's binary by looking at first 4 chars
    623             if #fileContent >= 4 and fileContent:sub(1, 4) == "BIN:" then
    624                 osprint("loadFS: Binary file, decoding...\n")
    625                 local base64Data = fileContent:sub(5)  -- Remove "BIN:" prefix
    626                 local binaryContent = base64Decode(base64Data)
    627                 parentDir:newFile(fileName, binaryContent)
    628             else
    629                 osprint("loadFS: Text file, unescaping...\n")
    630                 local textContent = unescapeText(fileContent)
    631                 parentDir:newFile(fileName, textContent)
    632             end
    633             osprint("loadFS: File created\n")
    634         end
    635     end
    636     osprint("loadFS: All files created\n")
    637 
    638     return root
    639 end
    640 
    641 -- Initialize the filesystem from ramdisk_packed provided by C
    642 -- ramdisk_packed is expected to be a global string set by the C kernel
    643 local root_fs
    644 
    645 if ramdisk_packed and type(ramdisk_packed) == "string" and #ramdisk_packed > 0 then
    646     -- Load filesystem from the packed ramdisk string
    647     osprint("Loading filesystem from ramdisk_packed...\n")
    648     osprint("Ramdisk size: ")
    649     osprint(tostring(#ramdisk_packed))
    650     osprint(" bytes\n")
    651 
    652     osprint("About to call loadFS...\n")
    653     local success, result = pcall(loadFS, ramdisk_packed)
    654     osprint("loadFS call completed\n")
    655 
    656     if success then
    657         root_fs = result
    658         osprint("Filesystem loaded successfully!\n")
    659     else
    660         osprint("Error loading filesystem: ")
    661         osprint(tostring(result))
    662         osprint("\n")
    663         osprint("Creating empty root filesystem...\n")
    664         root_fs = createRoot()
    665     end
    666 else
    667     -- No ramdisk_packed provided, create empty filesystem
    668     osprint("No ramdisk_packed found, creating empty root filesystem...\n")
    669     root_fs = createRoot()
    670 end
    671 
    672 -- Make the root filesystem and utility functions globally available
    673 _G.root_fs = root_fs
    674 _G.createRoot = createRoot
    675 _G.saveFS = saveFS
    676 _G.loadFS = loadFS
    677 
    678 -- File Handle System
    679 -- Provides io.open() compatible file handles with read/write methods
    680 
    681 local FileHandle = {}
    682 FileHandle.__index = FileHandle
    683 
    684 function FileHandle:read(format)
    685     if not self.node or self.node.type ~= "file" then
    686         return nil, "invalid file handle"
    687     end
    688 
    689     if self.mode ~= "r" and self.mode ~= "r+" and self.mode ~= "a+" then
    690         return nil, "file not opened for reading"
    691     end
    692 
    693     format = format or "*l"  -- default: read line
    694 
    695     -- Handle pseudo files
    696     local content
    697     if self.node.isPseudo then
    698         if self.node.onRead then
    699             -- Get arguments from temporary storage if present
    700             local args = self.node._tempArgs
    701             if args then
    702                 content = self.node.onRead(args)
    703                 -- Clear temporary args after use
    704                 self.node._tempArgs = nil
    705             else
    706                 content = self.node.onRead()
    707             end
    708             content = content or ""
    709         else
    710             return nil, "Pseudo file has no onRead callback"
    711         end
    712     else
    713         content = self.node.contents or ""
    714     end
    715 
    716     if format == "*a" or format == "*all" then
    717         -- Read entire file
    718         self.position = #content
    719         return content
    720     elseif format == "*l" or format == "*line" then
    721         -- Read single line
    722         if self.position >= #content then
    723             return nil  -- EOF
    724         end
    725 
    726         local start_pos = self.position + 1
    727         local newline_pos = content:find("\n", start_pos, true)
    728 
    729         if newline_pos then
    730             local line = content:sub(start_pos, newline_pos - 1)
    731             self.position = newline_pos
    732             return line
    733         else
    734             -- Last line without newline
    735             local line = content:sub(start_pos)
    736             self.position = #content
    737             return line
    738         end
    739     elseif format == "*n" or format == "*number" then
    740         -- Read number
    741         if self.position >= #content then
    742             return nil
    743         end
    744 
    745         -- Skip whitespace
    746         local start_pos = self.position + 1
    747         while start_pos <= #content and content:sub(start_pos, start_pos):match("%s") do
    748             start_pos = start_pos + 1
    749         end
    750 
    751         if start_pos > #content then
    752             return nil
    753         end
    754 
    755         -- Read number
    756         local num_match = content:sub(start_pos):match("^[+-]?%d+%.?%d*")
    757         if num_match then
    758             self.position = start_pos + #num_match - 1
    759             return tonumber(num_match)
    760         end
    761 
    762         return nil
    763     elseif type(format) == "number" then
    764         -- Read n bytes
    765         if self.position >= #content then
    766             return nil
    767         end
    768 
    769         local start_pos = self.position + 1
    770         local end_pos = math.min(start_pos + format - 1, #content)
    771         local data = content:sub(start_pos, end_pos)
    772         self.position = end_pos
    773         return data
    774     else
    775         return nil, "invalid format"
    776     end
    777 end
    778 
    779 function FileHandle:write(...)
    780     if not self.node or self.node.type ~= "file" then
    781         return nil, "invalid file handle"
    782     end
    783 
    784     if self.mode ~= "w" and self.mode ~= "a" and self.mode ~= "r+" and self.mode ~= "a+" then
    785         return nil, "file not opened for writing"
    786     end
    787 
    788     local args = {...}
    789     for _, data in ipairs(args) do
    790         local str = tostring(data)
    791 
    792         if self.mode == "w" or self.mode == "r+" then
    793             -- Write mode: insert at current position
    794             local content = self.node.contents or ""
    795             local before = content:sub(1, self.position)
    796             local after = content:sub(self.position + #str + 1)
    797             self.node.contents = before .. str .. after
    798             self.position = self.position + #str
    799         elseif self.mode == "a" or self.mode == "a+" then
    800             -- Append mode: always append to end
    801             self.node.contents = (self.node.contents or "") .. str
    802             self.position = #self.node.contents
    803         end
    804     end
    805 
    806     return self
    807 end
    808 
    809 function FileHandle:lines()
    810     if not self.node or self.node.type ~= "file" then
    811         return function() return nil end
    812     end
    813 
    814     if self.mode ~= "r" and self.mode ~= "r+" and self.mode ~= "a+" then
    815         return function() return nil end
    816     end
    817 
    818     -- Reset position for iteration
    819     self.position = 0
    820 
    821     return function()
    822         return self:read("*l")
    823     end
    824 end
    825 
    826 function FileHandle:seek(whence, offset)
    827     if not self.node or self.node.type ~= "file" then
    828         return nil, "invalid file handle"
    829     end
    830 
    831     whence = whence or "cur"
    832     offset = offset or 0
    833 
    834     local content_length = #(self.node.contents or "")
    835 
    836     if whence == "set" then
    837         self.position = offset
    838     elseif whence == "cur" then
    839         self.position = self.position + offset
    840     elseif whence == "end" then
    841         self.position = content_length + offset
    842     else
    843         return nil, "invalid whence"
    844     end
    845 
    846     -- Clamp position
    847     self.position = math.max(0, math.min(self.position, content_length))
    848 
    849     return self.position
    850 end
    851 
    852 function FileHandle:close()
    853     self.node = nil
    854     self.mode = nil
    855     self.position = nil
    856     return true
    857 end
    858 
    859 function FileHandle:flush()
    860     -- No-op for in-memory filesystem
    861     return true
    862 end
    863 
    864 -- io.open() implementation
    865 local function io_open(filepath, mode)
    866     mode = mode or "r"
    867 
    868     -- Validate mode
    869     if not mode:match("^[rwa]%+?b?$") then
    870         return nil, "invalid mode"
    871     end
    872 
    873     -- Remove binary flag (we don't distinguish in ramdisk)
    874     mode = mode:gsub("b", "")
    875 
    876     -- Check if filepath contains arguments (space after filename)
    877     local actualPath = filepath
    878     local args = nil
    879     local spacePos = filepath:find(" ")
    880 
    881     if spacePos then
    882         actualPath = filepath:sub(1, spacePos - 1)
    883         args = filepath:sub(spacePos + 1)
    884     end
    885 
    886     local node, err
    887 
    888     if mode:sub(1, 1) == "r" then
    889         -- Read mode: file must exist
    890         node, err = root_fs:traverse(actualPath)
    891         if not node then
    892             return nil, err or "file not found"
    893         end
    894 
    895         if node.type ~= "file" then
    896             return nil, "not a file"
    897         end
    898 
    899         -- Handle pseudo files with arguments
    900         if node.isPseudo and args then
    901             -- Store args for the file handle to use
    902             node._tempArgs = args
    903         end
    904     elseif mode:sub(1, 1) == "w" then
    905         -- Write mode: create or truncate file
    906         node, err = root_fs:traverse(filepath)
    907 
    908         if node then
    909             if node.type ~= "file" then
    910                 return nil, "not a file"
    911             end
    912             -- Truncate existing file
    913             node.contents = ""
    914         else
    915             -- Create new file
    916             -- Parse directory path
    917             local dir_path = filepath:match("^(.+)/[^/]+$")
    918             local filename = filepath:match("([^/]+)$")
    919 
    920             local parent_dir
    921             if dir_path then
    922                 parent_dir, err = root_fs:traverse(dir_path)
    923                 if not parent_dir then
    924                     return nil, "directory not found: " .. (dir_path or "")
    925                 end
    926             else
    927                 parent_dir = root_fs
    928             end
    929 
    930             if parent_dir.type ~= "directory" then
    931                 return nil, "parent is not a directory"
    932             end
    933 
    934             node = parent_dir:newFile(filename, "")
    935         end
    936     elseif mode:sub(1, 1) == "a" then
    937         -- Append mode: create if doesn't exist
    938         node, err = root_fs:traverse(filepath)
    939 
    940         if node then
    941             if node.type ~= "file" then
    942                 return nil, "not a file"
    943             end
    944         else
    945             -- Create new file
    946             local dir_path = filepath:match("^(.+)/[^/]+$")
    947             local filename = filepath:match("([^/]+)$")
    948 
    949             local parent_dir
    950             if dir_path then
    951                 parent_dir, err = root_fs:traverse(dir_path)
    952                 if not parent_dir then
    953                     return nil, "directory not found: " .. (dir_path or "")
    954                 end
    955             else
    956                 parent_dir = root_fs
    957             end
    958 
    959             if parent_dir.type ~= "directory" then
    960                 return nil, "parent is not a directory"
    961             end
    962 
    963             node = parent_dir:newFile(filename, "")
    964         end
    965     end
    966 
    967     -- Create file handle
    968     local handle = {
    969         node = node,
    970         mode = mode,
    971         position = (mode:sub(1, 1) == "a") and #(node.contents or "") or 0
    972     }
    973     setmetatable(handle, FileHandle)
    974 
    975     return handle
    976 end
    977 
    978 -- Global io table
    979 _G.io = _G.io or {}
    980 _G.io.open = io_open
    981 
    982 -- Helper: read entire file (shortcut)
    983 function _G.readFile(filepath)
    984     local file, err = io.open(filepath, "r")
    985     if not file then
    986         return nil, err
    987     end
    988 
    989     local content = file:read("*a")
    990     file:close()
    991 
    992     return content
    993 end
    994 
    995 -- Helper: write entire file (shortcut)
    996 function _G.writeFile(filepath, content)
    997     local file, err = io.open(filepath, "w")
    998     if not file then
    999         return nil, err
   1000     end
   1001 
   1002     file:write(content)
   1003     file:close()
   1004 
   1005     return true
   1006 end