ramdisk2.lua (12067B)
1 local ramdisk = {} 2 3 -- Node metatable for directories 4 local dirNode = {} 5 dirNode.__index = dirNode 6 7 -- Node metatable for files 8 local fileNode = {} 9 fileNode.__index = fileNode 10 11 -- Helper function to split path into components 12 local function splitPath(path) 13 local components = {} 14 -- Remove leading slash if present 15 path = path:gsub("^/+", "") 16 -- Split by slash 17 for component in path:gmatch("[^/]+") do 18 table.insert(components, component) 19 end 20 return components 21 end 22 23 -- Helper function to find root node 24 local function findRoot(node) 25 while node.parent ~= nil do 26 node = node.parent 27 end 28 return node 29 end 30 31 -- Traverse method for both file and directory nodes 32 local function traverse(self, path) 33 -- Start from root 34 local root = findRoot(self) 35 local current = root 36 37 -- Split path into components 38 local components = splitPath(path) 39 40 -- Traverse through each component 41 for i, component in ipairs(components) do 42 if current.type ~= "directory" then 43 return nil, "not a directory: " .. current.name 44 end 45 46 -- Check if this is the last component 47 local isLast = (i == #components) 48 49 -- Search in directories first 50 local found = false 51 for _, dir in ipairs(current.dirs) do 52 if dir.name == component then 53 current = dir 54 found = true 55 break 56 end 57 end 58 59 -- If not found in dirs and this is the last component, check files 60 if not found and isLast then 61 for _, file in ipairs(current.files) do 62 if file.name == component then 63 current = file 64 found = true 65 break 66 end 67 end 68 end 69 70 -- If still not found, check in dirs for last component (could be a directory) 71 if not found then 72 return nil, "path not found: " .. component 73 end 74 end 75 76 return current 77 end 78 79 -- Method to create a new directory 80 function dirNode:newDir(name) 81 local dir = { 82 name = name, 83 type = "directory", 84 parent = self, 85 dirs = {}, 86 files = {} 87 } 88 setmetatable(dir, dirNode) 89 table.insert(self.dirs, dir) 90 return dir 91 end 92 93 -- Method to create a new file 94 function dirNode:newFile(name, contents) 95 local file = { 96 name = name, 97 type = "file", 98 parent = self, 99 contents = contents or "" 100 } 101 setmetatable(file, fileNode) 102 table.insert(self.files, file) 103 return file 104 end 105 106 -- Read method for files 107 function fileNode:read() 108 return self.contents 109 end 110 111 -- Write method for files 112 function fileNode:write(contents) 113 self.contents = contents 114 end 115 116 -- Delete method for files 117 function fileNode:delete() 118 if self.parent then 119 -- Remove from parent's files array 120 for i, file in ipairs(self.parent.files) do 121 if file == self then 122 table.remove(self.parent.files, i) 123 break 124 end 125 end 126 end 127 -- Clear file data 128 self.name = nil 129 self.parent = nil 130 self.contents = nil 131 end 132 133 -- Delete method for directories 134 function dirNode:delete() 135 if self.parent then 136 -- Remove from parent's dirs array 137 for i, dir in ipairs(self.parent.dirs) do 138 if dir == self then 139 table.remove(self.parent.dirs, i) 140 break 141 end 142 end 143 end 144 -- Clear directory data 145 self.name = nil 146 self.parent = nil 147 -- Note: dirs and files arrays are left for garbage collection 148 end 149 150 -- Add traverse method to both metatables 151 dirNode.traverse = traverse 152 fileNode.traverse = traverse 153 154 -- Function to create a root directory node 155 function ramdisk.createRoot() 156 local root = { 157 name = "", 158 type = "directory", 159 parent = nil, 160 dirs = {}, 161 files = {} 162 } 163 setmetatable(root, dirNode) 164 return root 165 end 166 167 -- Helper function to get full path of a node 168 local function getFullPath(node) 169 local parts = {} 170 local current = node 171 172 while current.parent ~= nil do 173 table.insert(parts, 1, current.name) 174 current = current.parent 175 end 176 177 return table.concat(parts, "/") 178 end 179 180 -- Helper function to check if content is binary (contains non-printable chars) 181 local function isBinary(content) 182 if type(content) ~= "string" then 183 return false 184 end 185 186 for i = 1, #content do 187 local byte = content:byte(i) 188 -- Check for control characters except tab, newline, carriage return 189 if byte < 32 and byte ~= 9 and byte ~= 10 and byte ~= 13 then 190 return true 191 end 192 -- Check for extended ASCII 193 if byte > 127 then 194 return true 195 end 196 end 197 return false 198 end 199 200 -- Helper function to encode binary data to base64 201 local function base64Encode(data) 202 local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' 203 return ((data:gsub('.', function(x) 204 local r,b='',x:byte() 205 for i=8,1,-1 do r=r..(b%2^i-b%2^(i-1)>0 and '1' or '0') end 206 return r; 207 end)..'0000'):gsub('%d%d%d?%d?%d?%d?', function(x) 208 if (#x < 6) then return '' end 209 local c=0 210 for i=1,6 do c=c+(x:sub(i,i)=='1' and 2^(6-i) or 0) end 211 return b:sub(c+1,c+1) 212 end)..({ '', '==', '=' })[#data%3+1]) 213 end 214 215 -- Helper function to decode base64 to binary data 216 local function base64Decode(data) 217 local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' 218 data = string.gsub(data, '[^'..b..'=]', '') 219 return (data:gsub('.', function(x) 220 if (x == '=') then return '' end 221 local r,f='',(b:find(x)-1) 222 for i=6,1,-1 do r=r..(f%2^i-f%2^(i-1)>0 and '1' or '0') end 223 return r; 224 end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x) 225 if (#x ~= 8) then return '' end 226 local c=0 227 for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end 228 return string.char(c) 229 end)) 230 end 231 232 -- Helper function to escape special characters in text content 233 local function escapeText(text) 234 text = text:gsub("\\", "\\\\") -- Backslash must be first 235 text = text:gsub("\n", "\\n") -- Newline 236 text = text:gsub("\t", "\\t") -- Tab 237 text = text:gsub("\r", "\\r") -- Carriage return 238 return text 239 end 240 241 -- Helper function to unescape special characters in text content 242 local function unescapeText(text) 243 text = text:gsub("\\r", "\r") -- Carriage return 244 text = text:gsub("\\t", "\t") -- Tab 245 text = text:gsub("\\n", "\n") -- Newline 246 text = text:gsub("\\\\", "\\") -- Backslash must be last 247 return text 248 end 249 250 -- Function to save the filesystem to serialized format 251 function ramdisk.saveFS(rootdir) 252 local dirs = {} 253 local files = {} 254 255 -- Recursive function to traverse directories 256 local function traverseDir(dir, currentPath) 257 -- Add directory path (with trailing slash) 258 if currentPath ~= "" then 259 table.insert(dirs, currentPath .. "/") 260 end 261 262 -- Process subdirectories 263 for _, subdir in ipairs(dir.dirs) do 264 local subdirPath = currentPath == "" and subdir.name or currentPath .. "/" .. subdir.name 265 traverseDir(subdir, subdirPath) 266 end 267 268 -- Process files 269 for _, file in ipairs(dir.files) do 270 local filePath = currentPath == "" and file.name or currentPath .. "/" .. file.name 271 local contents = file.contents or "" 272 273 if isBinary(contents) then 274 table.insert(files, filePath .. "~~~~BIN:" .. base64Encode(contents)) 275 else 276 table.insert(files, filePath .. "~~~~" .. escapeText(contents)) 277 end 278 end 279 end 280 281 -- Start traversal from root 282 traverseDir(rootdir, "") 283 284 -- Build the final format: [[dirs||||files]] 285 local dirSection = table.concat(dirs, "||||") 286 local fileSection = table.concat(files, "||||") 287 288 return "[[" .. dirSection .. "||||" .. fileSection .. "]]" 289 end 290 291 -- Function to load a filesystem from serialized format 292 function ramdisk.loadFS(serialized) 293 -- Create new root 294 local root = ramdisk.createRoot() 295 296 -- Remove the [[ and ]] wrapper 297 if not serialized:match("^%[%[.*%]%]$") then 298 error("Invalid serialized format: missing [[ ]] wrapper") 299 end 300 301 local content = serialized:sub(3, -3) -- Remove [[ and ]] 302 303 -- Split by |||| to separate entries 304 local entries = {} 305 local currentEntry = "" 306 local i = 1 307 while i <= #content do 308 -- Check if we're at a |||| separator 309 if content:sub(i, i+3) == "||||" then 310 if currentEntry ~= "" then 311 table.insert(entries, currentEntry) 312 currentEntry = "" 313 end 314 i = i + 4 -- Skip the |||| 315 else 316 currentEntry = currentEntry .. content:sub(i, i) 317 i = i + 1 318 end 319 end 320 -- Don't forget the last entry 321 if currentEntry ~= "" then 322 table.insert(entries, currentEntry) 323 end 324 325 -- Track created directories 326 local createdDirs = {} 327 createdDirs[""] = root -- Root is always available 328 329 -- Helper to ensure directory exists 330 local function ensureDir(path) 331 if createdDirs[path] then 332 return createdDirs[path] 333 end 334 335 -- Split path into components 336 local components = splitPath(path) 337 local current = root 338 local currentPath = "" 339 340 for _, component in ipairs(components) do 341 local nextPath = currentPath == "" and component or currentPath .. "/" .. component 342 343 if not createdDirs[nextPath] then 344 -- Check if directory already exists 345 local found = false 346 for _, dir in ipairs(current.dirs) do 347 if dir.name == component then 348 current = dir 349 found = true 350 break 351 end 352 end 353 354 if not found then 355 current = current:newDir(component) 356 end 357 358 createdDirs[nextPath] = current 359 else 360 current = createdDirs[nextPath] 361 end 362 363 currentPath = nextPath 364 end 365 366 return current 367 end 368 369 -- First pass: create all directories 370 for _, entry in ipairs(entries) do 371 if entry:match("/$") then -- It's a directory 372 local dirPath = entry:sub(1, -2) -- Remove trailing slash 373 ensureDir(dirPath) 374 end 375 end 376 377 -- Second pass: create all files 378 for _, entry in ipairs(entries) do 379 if entry:match("~~~~") then -- It's a file 380 local separatorPos = entry:find("~~~~") 381 if separatorPos then 382 local filePath = entry:sub(1, separatorPos - 1) 383 local fileContent = entry:sub(separatorPos + 4) 384 385 -- Determine parent directory and filename 386 local lastSlash = filePath:match("^.*()/") 387 local parentPath, fileName 388 389 if lastSlash then 390 parentPath = filePath:sub(1, lastSlash - 1) 391 fileName = filePath:sub(lastSlash + 1) 392 else 393 parentPath = "" 394 fileName = filePath 395 end 396 397 -- Get or create parent directory 398 local parentDir = ensureDir(parentPath) 399 400 -- Check if it's binary 401 if fileContent:match("^BIN:") then 402 local base64Data = fileContent:sub(5) -- Remove "BIN:" prefix 403 local binaryContent = base64Decode(base64Data) 404 parentDir:newFile(fileName, binaryContent) 405 else 406 local textContent = unescapeText(fileContent) 407 parentDir:newFile(fileName, textContent) 408 end 409 end 410 end 411 end 412 413 return root 414 end 415 416 return ramdisk