local fs = {} local is_windows = package.config:sub(1,1) == '\\' local path_sep = is_windows and '\\' or '/' -- ====================================================================== -- FILE OPERATIONS -- ====================================================================== function fs.exists(path) local file = io.open(path, "r") if file then file:close() return true end return false end function fs.size(path) local file = io.open(path, "r") if not file then return nil end local size = file:seek("end") file:close() return size end function fs.is_dir(path) if not fs.exists(path) then return false end -- Check if we can list directory (platform specific) if is_windows then local handle = io.popen('dir "' .. path .. '" 2>nul') if handle then local result = handle:read("*a") handle:close() return result and result:match("Directory of") ~= nil end else local handle = io.popen('test -d "' .. path .. '" 2>/dev/null && echo "dir"') if handle then local result = handle:read("*l") handle:close() return result == "dir" end end return false end function fs.is_file(path) return fs.exists(path) and not fs.is_dir(path) end function fs.read(path) local file = io.open(path, "r") if not file then error("Failed to read file '" .. path .. "': file not found or permission denied") end local content = file:read("*all") file:close() if not content then error("Failed to read file '" .. path .. "': read error") end return content end function fs.write(path, content) -- Check for obviously invalid paths if path == "" or path:find("\0") then error("Failed to write file '" .. path .. "': invalid path") end local file = io.open(path, "w") if not file then error("Failed to write file '" .. path .. "': permission denied or invalid path") end local success = file:write(content) file:close() if not success then error("Failed to write file '" .. path .. "': write error") end return true end function fs.append(path, content) local file = io.open(path, "a") if not file then error("Failed to append to file '" .. path .. "': permission denied or invalid path") end local success = file:write(content) file:close() if not success then error("Failed to append to file '" .. path .. "': write error") end return true end function fs.copy(src, dst) local src_file = io.open(src, "rb") if not src_file then error("Failed to copy '" .. src .. "' to '" .. dst .. "': source file not found") end local dst_file = io.open(dst, "wb") if not dst_file then src_file:close() error("Failed to copy '" .. src .. "' to '" .. dst .. "': cannot create destination") end local chunk_size = 8192 while true do local chunk = src_file:read(chunk_size) if not chunk then break end if not dst_file:write(chunk) then src_file:close() dst_file:close() error("Failed to copy '" .. src .. "' to '" .. dst .. "': write error") end end src_file:close() dst_file:close() return true end function fs.move(src, dst) -- Try native rename first local success = os.rename(src, dst) if success then return true end -- Fallback to copy + delete fs.copy(src, dst) fs.remove(src) return true end function fs.remove(path) local success = os.remove(path) if not success then error("Failed to remove '" .. path .. "': file not found or permission denied") end return true end function fs.mtime(path) -- Use os.execute with stat command if is_windows then local cmd = 'for %i in ("' .. path .. '") do @echo %~ti' local handle = io.popen(cmd) if handle then local result = handle:read("*line") handle:close() if result then -- Parse Windows timestamp (basic implementation) return os.time() -- Fallback to current time end end else local cmd = 'stat -c %Y "' .. path .. '" 2>/dev/null' local handle = io.popen(cmd) if handle then local result = handle:read("*line") handle:close() if result and result:match("^%d+$") then return tonumber(result) end end end return nil end function fs.touch(path) if fs.exists(path) then -- Update timestamp - basic implementation local content = fs.read(path) fs.write(path, content) else -- Create empty file fs.write(path, "") end return true end function fs.lines(path) local lines = {} local file = io.open(path, "r") if not file then error("Failed to read lines from '" .. path .. "': file not found") end for line in file:lines() do lines[#lines + 1] = line end file:close() return lines end -- ====================================================================== -- DIRECTORY OPERATIONS -- ====================================================================== function fs.mkdir(path) local cmd if is_windows then cmd = 'mkdir "' .. path .. '" 2>nul' else cmd = 'mkdir -p "' .. path .. '"' end local success = os.execute(cmd) if not success then error("Failed to create directory '" .. path .. "'") end return true end function fs.rmdir(path) local cmd if is_windows then cmd = 'rmdir /s /q "' .. path .. '"' else cmd = 'rm -rf "' .. path .. '"' end local success = os.execute(cmd) if not success then error("Failed to remove directory '" .. path .. "'") end return true end function fs.list(path) if not fs.exists(path) then error("Failed to list directory '" .. path .. "': directory not found") end if not fs.is_dir(path) then error("Failed to list directory '" .. path .. "': not a directory") end local entries = {} local cmd if is_windows then cmd = 'dir /b "' .. path .. '" 2>nul' else cmd = 'ls -1 "' .. path .. '" 2>/dev/null' end local handle = io.popen(cmd) if not handle then return nil end for name in handle:lines() do local full_path = fs.join(path, name) entries[#entries + 1] = { name = name, is_dir = fs.is_dir(full_path), size = fs.is_file(full_path) and fs.size(full_path) or 0, mtime = fs.mtime(full_path) } end handle:close() return entries end function fs.list_files(path) local entries = fs.list(path) local files = {} for _, entry in ipairs(entries) do if not entry.is_dir then files[#files + 1] = entry end end return files end function fs.list_dirs(path) local entries = fs.list(path) local dirs = {} for _, entry in ipairs(entries) do if entry.is_dir then dirs[#dirs + 1] = entry end end return dirs end function fs.list_names(path) local entries = fs.list(path) local names = {} for _, entry in ipairs(entries) do names[#names + 1] = entry.name end return names end -- ====================================================================== -- PATH OPERATIONS (Optimized pure Lua) -- ====================================================================== function fs.join(...) local parts = {...} if #parts == 0 then return "" end if #parts == 1 then return parts[1] end local result = parts[1] for i = 2, #parts do local part = parts[i] if part ~= "" then if result:sub(-1) == path_sep or result == "" then result = result .. part else result = result .. path_sep .. part end end end return result end function fs.dirname(path) local pos = path:find("[/\\][^/\\]*$") if pos then return path:sub(1, pos - 1) end return "." end function fs.basename(path) return path:match("[^/\\]*$") or "" end function fs.ext(path) local base = fs.basename(path) local dot_pos = base:find("%.[^%.]*$") return dot_pos and base:sub(dot_pos) or "" end function fs.abs(path) if is_windows then if path:match("^[A-Za-z]:") or path:match("^\\\\") then return path end else if path:sub(1, 1) == "/" then return path end end local cwd = fs.getcwd() return fs.join(cwd, path) end function fs.clean(path) -- Normalize path separators path = path:gsub("[/\\]+", path_sep) -- Handle . and .. components local parts = {} for part in path:gmatch("[^" .. path_sep .. "]+") do if part == ".." and #parts > 0 and parts[#parts] ~= ".." then parts[#parts] = nil elseif part ~= "." then parts[#parts + 1] = part end end local result = table.concat(parts, path_sep) if path:sub(1, 1) == path_sep then result = path_sep .. result end return result ~= "" and result or "." end function fs.split(path) local pos = path:find("[/\\][^/\\]*$") if pos then return path:sub(1, pos), path:sub(pos + 1) end return "", path end function fs.splitext(path) local dir = fs.dirname(path) local base = fs.basename(path) local ext = fs.ext(path) local name = base if ext ~= "" then name = base:sub(1, -(#ext + 1)) end return dir, name, ext end -- ====================================================================== -- WORKING DIRECTORY -- ====================================================================== function fs.getcwd() local cwd, err = moonshark.getcwd() if not cwd then error("Failed to get current directory: " .. (err or "unknown error")) end return cwd end function fs.chdir(path) local success, err = moonshark.chdir(path) if not success then error("Failed to change directory to '" .. path .. "': " .. (err or "unknown error")) end return true end -- ====================================================================== -- TEMPORARY FILES -- ====================================================================== function fs.tempfile(prefix) prefix = prefix or "tmp" local temp_name = prefix .. "_" .. os.time() .. "_" .. math.random(10000) local temp_path if is_windows then local temp_dir = os.getenv("TEMP") or os.getenv("TMP") or "C:\\temp" temp_path = fs.join(temp_dir, temp_name) else temp_path = fs.join("/tmp", temp_name) end -- Create the file fs.write(temp_path, "") return temp_path end function fs.tempdir(prefix) prefix = prefix or "tmp" local temp_name = prefix .. "_" .. os.time() .. "_" .. math.random(10000) local temp_path if is_windows then local temp_dir = os.getenv("TEMP") or os.getenv("TMP") or "C:\\temp" temp_path = fs.join(temp_dir, temp_name) else temp_path = fs.join("/tmp", temp_name) end fs.mkdir(temp_path) return temp_path end -- ====================================================================== -- PATTERN MATCHING -- ====================================================================== function fs.glob(pattern) local cmd if is_windows then cmd = 'for %f in ("' .. pattern .. '") do @echo %f' else cmd = 'ls -1d ' .. pattern .. ' 2>/dev/null' end local matches = {} local handle = io.popen(cmd) if handle then for match in handle:lines() do matches[#matches + 1] = match end handle:close() end return matches end function fs.walk(root) local files = {} local function walk_recursive(path) if not fs.exists(path) then return end files[#files + 1] = path if fs.is_dir(path) then local entries = fs.list(path) for _, entry in ipairs(entries) do walk_recursive(fs.join(path, entry.name)) end end end walk_recursive(root) return files end -- ====================================================================== -- UTILITY FUNCTIONS (using optimized implementations) -- ====================================================================== function fs.extension(path) local ext = fs.ext(path) return ext:sub(2) -- Remove leading dot end function fs.change_ext(path, new_ext) local dir, name, _ = fs.splitext(path) if not new_ext:match("^%.") then new_ext = "." .. new_ext end if dir == "." then return name .. new_ext end return fs.join(dir, name .. new_ext) end function fs.ensure_dir(path) if not fs.exists(path) then fs.mkdir(path) elseif not fs.is_dir(path) then error("Path exists but is not a directory: " .. path) end return true end function fs.size_human(path) local size = fs.size(path) if not size then return nil end local units = {"B", "KB", "MB", "GB", "TB"} local unit_index = 1 local size_float = size while size_float >= 1024 and unit_index < #units do size_float = size_float / 1024 unit_index = unit_index + 1 end if unit_index == 1 then return string.format("%d %s", size_float, units[unit_index]) else return string.format("%.1f %s", size_float, units[unit_index]) end end function fs.is_safe_path(path) path = fs.clean(path) if path:match("%.%.") then return false end if path:match("^[/\\]") then return false end if path:match("^~") then return false end return true end -- Aliases for convenience fs.makedirs = fs.mkdir fs.removedirs = fs.rmdir function fs.copytree(src, dst) if not fs.exists(src) then error("Source directory does not exist: " .. src) end if not fs.is_dir(src) then error("Source is not a directory: " .. src) end fs.mkdir(dst) local entries = fs.list(src) for _, entry in ipairs(entries) do local src_path = fs.join(src, entry.name) local dst_path = fs.join(dst, entry.name) if entry.is_dir then fs.copytree(src_path, dst_path) else fs.copy(src_path, dst_path) end end return true end function fs.find(root, pattern, recursive) recursive = recursive ~= false local results = {} local function search(dir) local entries = fs.list(dir) for _, entry in ipairs(entries) do local full_path = fs.join(dir, entry.name) if not entry.is_dir and entry.name:match(pattern) then results[#results + 1] = full_path elseif entry.is_dir and recursive then search(full_path) end end end search(root) return results end -- Get directory tree as nested table function fs.tree(root, max_depth) max_depth = max_depth or 10 local function build_tree(path, depth) if depth > max_depth then return nil end if not fs.exists(path) then return nil end local node = { name = fs.basename(path), path = path, is_dir = fs.is_dir(path) } if node.is_dir then node.children = {} local entries = fs.list(path) for _, entry in ipairs(entries) do local child_path = fs.join(path, entry.name) local child = build_tree(child_path, depth + 1) if child then node.children[#node.children + 1] = child end end else node.size = fs.size(path) node.mtime = fs.mtime(path) end return node end return build_tree(root, 1) end return fs