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