From 83f05b95904da01683bccdb73fc0243865eaaac3 Mon Sep 17 00:00:00 2001 From: filoxenace Date: Tue, 23 Sep 2025 21:56:09 -0400 Subject: [PATCH] Split modules --- detector.luau | 218 +++------------------------------------------- lib/cli.luau | 88 +++++++++++++++++++ lib/core.luau | 52 +++++++++++ lib/fileproc.luau | 80 +++++++++++++++++ 4 files changed, 230 insertions(+), 208 deletions(-) create mode 100644 lib/cli.luau create mode 100644 lib/core.luau create mode 100644 lib/fileproc.luau diff --git a/detector.luau b/detector.luau index 10c2785..3397f51 100644 --- a/detector.luau +++ b/detector.luau @@ -1,221 +1,23 @@ -local process = require("@lune/process") +local cli = require("./lib/cli") +local fileproc = require("./lib/fileproc") +local core = require("./lib/core") local fs = require("@lune/fs") -local roblox = require("@lune/roblox") local stdio = require("@lune/stdio") -local serde = require("@lune/serde") -local args = process.args - -local outputFile = nil -local directoryMode = false -local directoryPath = nil --- i mainly added this arg because the modelscrape files dont have extensions and i dont feel like changing all of them ~ivy -local forceBinaryRead = false -local printInstanceNames = false -local zlibDecompressFiles = false -local filesToProcess = {} - -if #args < 1 then - stdio.write(stdio.color("red")) - stdio.write("Error: Please provide file path(s) or use --directory flag.\n") - stdio.write("Usage: lune run detector file1.rbxm file2.rbxm file3.rbxmx\n") - stdio.write(" lune run detector --directory path/to/folder\n") - stdio.write(" lune run detector --d path/to/folder\n") - stdio.write(" lune run detector --o output.txt file1.rbxm file2.rbxm\n") - stdio.write(" lune run detector --output output.txt file1.rbxm file2.rbxm\n") - stdio.write(" lune run detector --directory path/to/folder --o output.txt\n") - stdio.write(" lune run detector --force-binary-read file1 file2.bin file3.robloxmodelfile\n") - stdio.write(" lune run detector --print-instance-names file1.rbxm\n") - stdio.write(" lune run detector --zlib-decompress file1.rbxm\n") - stdio.write(stdio.color("reset")) - process.exit(1) -end - --- takes in a model as defined by lune (a table of children) --- returns a boolean if that model contains a workspace or not --- recursively searches entire model -function scanForWorkspace(model: {Instance}): boolean - for _, child in pairs(model) do - if printInstanceNames then - print(child:GetFullName()) - end - if child:IsA("Workspace") or scanForWorkspace(child:GetChildren()) then - return true - end - end - return false -end - --- takes in fileContents as a string and deserializes them returning the results of scanForWorkspace() on the deserialized model. if it can't be deserialized, it will return the results of a naive search through the xml -function fileContainsWorkspace(fileContents: string): boolean - if zlibDecompressFiles then - local success = pcall(function() fileContents = serde.decompress("zlib", fileContents) end) - if not success then - stdio.write(stdio.color("yellow")) - stdio.write("Warning: Failed to decompress file with zlib. Proceeding with original contents.\n") - stdio.write(stdio.color("reset")) - end - end - local success, instances = pcall(function() return roblox.deserializeModel(fileContents) end) - if not success then -- roblox doesn't like seeing files, so this is a work-around - return string.find(fileContents, "Item class=\"Workspace\"") and true or false - end - return scanForWorkspace(instances) -end - -function formatResult(result: boolean, fileName: string): string - if result then - return stdio.color("green") .. `File {fileName} contains a Workspace instance.` .. stdio.color("reset") - else - return stdio.color("yellow") .. `File {fileName} does not contain a Workspace instance.` .. stdio.color("reset") - end -end - --- checks if file has valid extension -function isValidModelFile(fileName: string): boolean - local ext = string.match(fileName, "%.([^%.]+)$") - return ext == "rbxm" or ext == "rbxmx" -end - - -local i = 1 -while i <= #args do - local arg = args[i] - - if arg == "--o" or arg == "--output" then - if i + 1 > #args then - stdio.write(stdio.color("red")) - stdio.write("Error: --o or --output flag requires an output filename.\n") - stdio.write(stdio.color("reset")) - process.exit(1) - end - outputFile = args[i + 1] - i = i + 2 - elseif arg == "--directory" or arg == "--d" then - if i + 1 > #args then - stdio.write(stdio.color("red")) - stdio.write("Error: --directory (or --d) flag requires a directory path.\n") - stdio.write(stdio.color("reset")) - process.exit(1) - end - directoryMode = true - directoryPath = args[i + 1] - i = i + 2 - elseif arg == "--force-binary-read" then - forceBinaryRead = true - i = i + 1 - elseif arg == "--print-instance-names" then - printInstanceNames = true - i = i+1 - elseif arg == "--zlib-decompress" then - zlibDecompressFiles = true - i = i + 1 - else - -- regular file argument - if not directoryMode then - table.insert(filesToProcess, arg) - end - i = i + 1 - end -end - --- check if output file already exists -if outputFile then - if fs.isFile(outputFile) then - stdio.write(stdio.color("red")) - stdio.write(`Error: Output file {outputFile} already exists. Will not overwrite.\n`) - stdio.write(stdio.color("reset")) - process.exit(1) - end -end - -if directoryMode then - -- check if directory exists - if not fs.isDir(directoryPath) then - stdio.write(stdio.color("red")) - stdio.write(`Error: Directory {directoryPath} does not exist.\n`) - stdio.write(stdio.color("reset")) - process.exit(1) - end - - -- read directory and collect files - local function readDirLoop(dirPath) - for _, file in pairs(fs.readDir(dirPath)) do - local fullPath = dirPath .. "/" .. file - if fs.isFile(fullPath) then - if forceBinaryRead or isValidModelFile(file) then - table.insert(filesToProcess, fullPath) - end - elseif fs.isDir(fullPath) then - readDirLoop(fullPath) - end - end - end - - readDirLoop(directoryPath) - - if #filesToProcess == 0 then - stdio.write(stdio.color("yellow")) - stdio.write(`Warning: No files found in directory {directoryPath}.\n`) - stdio.write(stdio.color("reset")) - process.exit(0) - end -end - -if #filesToProcess == 0 then - stdio.write(stdio.color("red")) - stdio.write("Error: No files to process.\n") - stdio.write(stdio.color("reset")) - process.exit(1) -end +local opts = cli.parseArgs() +cli.checkOutputFile(opts.outputFile) +local filesToProcess = fileproc.collectFiles(opts) local totalFiles = #filesToProcess -local filesWithWorkspace = {} -local processedFiles = 0 - - stdio.write(`Processing {totalFiles} file(s)...\n\n`) +local filesWithWorkspace = fileproc.processFiles(filesToProcess, opts) -for _, filePath in pairs(filesToProcess) do - processedFiles = processedFiles + 1 - - if fs.isFile(filePath) then - if forceBinaryRead or directoryMode or isValidModelFile(filePath) then - local success, result = pcall(function() - local fileContents = fs.readFile(filePath) - return fileContainsWorkspace(fileContents) - end) - - if success then - if result then - table.insert(filesWithWorkspace, filePath) - end - print(formatResult(result, filePath)) - else - stdio.write(stdio.color("red")) - stdio.write(`Error processing {filePath}: {result}\n`) - stdio.write(stdio.color("reset")) - end - else - stdio.write(stdio.color("yellow")) - stdio.write(`Warning: {filePath} is not a .rbxm or .rbxmx file, skipping.\n`) - stdio.write(stdio.color("reset")) - end - else - stdio.write(stdio.color("red")) - stdio.write(`Error: File {filePath} does not exist.\n`) - stdio.write(stdio.color("reset")) - end -end - --- write to output file -if outputFile then +if opts.outputFile then local outputContent = table.concat(filesWithWorkspace, "\n") .. "\n" - fs.writeFile(outputFile, outputContent) - stdio.write(`Output written to {outputFile}\n`) + fs.writeFile(opts.outputFile, outputContent) + stdio.write(`Output written to {opts.outputFile}\n`) end --- print summary stdio.write("\nFiles containing a Workspace instance:\n") for _, file in pairs(filesWithWorkspace) do stdio.write(stdio.color("green")) diff --git a/lib/cli.luau b/lib/cli.luau new file mode 100644 index 0000000..136e9d1 --- /dev/null +++ b/lib/cli.luau @@ -0,0 +1,88 @@ +-- handles cli argument parsing and usage printing +local process = require("@lune/process") +local stdio = require("@lune/stdio") +local fs = require("@lune/fs") + +local cli = {} + +function cli.parseArgs() + local args = process.args + local opts = { + outputFile = nil, + directoryMode = false, + directoryPath = nil, + forceBinaryRead = false, + printInstanceNames = false, + zlibDecompressFiles = false, + filesToProcess = {} + } + if #args < 1 then + cli.printUsage() + process.exit(1) + end + local i = 1 + while i <= #args do + local arg = args[i] + if arg == "--o" or arg == "--output" then + if i + 1 > #args then + stdio.write(stdio.color("red")) + stdio.write("Error: --o or --output flag requires an output filename.\n") + stdio.write(stdio.color("reset")) + process.exit(1) + end + opts.outputFile = args[i + 1] + i = i + 2 + elseif arg == "--directory" or arg == "--d" then + if i + 1 > #args then + stdio.write(stdio.color("red")) + stdio.write("Error: --directory (or --d) flag requires a directory path.\n") + stdio.write(stdio.color("reset")) + process.exit(1) + end + opts.directoryMode = true + opts.directoryPath = args[i + 1] + i = i + 2 + elseif arg == "--force-binary-read" then + opts.forceBinaryRead = true + i = i + 1 + elseif arg == "--print-instance-names" then + opts.printInstanceNames = true + i = i + 1 + elseif arg == "--zlib-decompress" then + opts.zlibDecompressFiles = true + i = i + 1 + else + if not opts.directoryMode then + table.insert(opts.filesToProcess, arg) + end + i = i + 1 + end + end + return opts +end + +function cli.printUsage() + stdio.write(stdio.color("red")) + stdio.write("Error: Please provide file path(s) or use --directory flag.\n") + stdio.write("Usage: lune run detector file1.rbxm file2.rbxm file3.rbxmx\n") + stdio.write(" lune run detector --directory path/to/folder\n") + stdio.write(" lune run detector --d path/to/folder\n") + stdio.write(" lune run detector --o output.txt file1.rbxm file2.rbxm\n") + stdio.write(" lune run detector --output output.txt file1.rbxm file2.rbxm\n") + stdio.write(" lune run detector --directory path/to/folder --o output.txt\n") + stdio.write(" lune run detector --force-binary-read file1 file2.bin file3.robloxmodelfile\n") + stdio.write(" lune run detector --print-instance-names file1.rbxm\n") + stdio.write(" lune run detector --zlib-decompress file1.rbxm\n") + stdio.write(stdio.color("reset")) +end + +function cli.checkOutputFile(outputFile) + if outputFile and fs.isFile(outputFile) then + stdio.write(stdio.color("red")) + stdio.write(`Error: Output file {outputFile} already exists. Will not overwrite.\n`) + stdio.write(stdio.color("reset")) + process.exit(1) + end +end + +return cli diff --git a/lib/core.luau b/lib/core.luau new file mode 100644 index 0000000..288f66e --- /dev/null +++ b/lib/core.luau @@ -0,0 +1,52 @@ +-- handles core detection logic for workspace in models +local roblox = require("@lune/roblox") +local stdio = require("@lune/stdio") +local serde = require("@lune/serde") + +local core = {} + +-- recursively searches a model for a workspace instance +function core.scanForWorkspace(model, printInstanceNames) + for _, child in pairs(model) do + if printInstanceNames then + print(child:GetFullName()) + end + if child:IsA("Workspace") or core.scanForWorkspace(child:GetChildren(), printInstanceNames) then + return true + end + end + return false +end + +-- checks if file has valid extension +function core.isValidModelFile(fileName) + local ext = string.match(fileName, "%.([^%.]+)$") + return ext == "rbxm" or ext == "rbxmx" +end + +-- takes in fileContents as a string and deserializes them returning the results of scanForWorkspace() on the deserialized model. if it can't be deserialized, it will return the results of a naive search through the xml +function core.fileContainsWorkspace(fileContents, opts) + if opts.zlibDecompressFiles then + local success = pcall(function() fileContents = serde.decompress("zlib", fileContents) end) + if not success then + stdio.write(stdio.color("yellow")) + stdio.write("Warning: Failed to decompress file with zlib. Proceeding with original contents.\n") + stdio.write(stdio.color("reset")) + end + end + local success, instances = pcall(function() return roblox.deserializeModel(fileContents) end) + if not success then + return string.find(fileContents, "Item class=\"Workspace\"") and true or false + end + return core.scanForWorkspace(instances, opts.printInstanceNames) +end + +function core.formatResult(result, fileName) + if result then + return stdio.color("green") .. `File {fileName} contains a Workspace instance.` .. stdio.color("reset") + else + return stdio.color("yellow") .. `File {fileName} does not contain a Workspace instance.` .. stdio.color("reset") + end +end + +return core diff --git a/lib/fileproc.luau b/lib/fileproc.luau new file mode 100644 index 0000000..86b9dcc --- /dev/null +++ b/lib/fileproc.luau @@ -0,0 +1,80 @@ +-- handles file and directory processing +local fs = require("@lune/fs") +local stdio = require("@lune/stdio") +local process = require("@lune/process") +local core = require("./core") + +local fileproc = {} + +function fileproc.collectFiles(opts) + local filesToProcess = opts.filesToProcess or {} + if opts.directoryMode then + if not fs.isDir(opts.directoryPath) then + stdio.write(stdio.color("red")) + stdio.write(`Error: Directory {opts.directoryPath} does not exist.\n`) + stdio.write(stdio.color("reset")) + process.exit(1) + end + local function readDirLoop(dirPath) + for _, file in pairs(fs.readDir(dirPath)) do + local fullPath = dirPath .. "/" .. file + if fs.isFile(fullPath) then + if opts.forceBinaryRead or core.isValidModelFile(file) then + table.insert(filesToProcess, fullPath) + end + elseif fs.isDir(fullPath) then + readDirLoop(fullPath) + end + end + end + readDirLoop(opts.directoryPath) + if #filesToProcess == 0 then + stdio.write(stdio.color("yellow")) + stdio.write(`Warning: No files found in directory {opts.directoryPath}.\n`) + stdio.write(stdio.color("reset")) + process.exit(0) + end + end + if #filesToProcess == 0 then + stdio.write(stdio.color("red")) + stdio.write("Error: No files to process.\n") + stdio.write(stdio.color("reset")) + process.exit(1) + end + return filesToProcess +end + +function fileproc.processFiles(filesToProcess, opts) + local filesWithWorkspace = {} + for _, filePath in pairs(filesToProcess) do + if fs.isFile(filePath) then + if opts.forceBinaryRead or opts.directoryMode or core.isValidModelFile(filePath) then + local success, result = pcall(function() + local fileContents = fs.readFile(filePath) + return core.fileContainsWorkspace(fileContents, opts) + end) + if success then + if result then + table.insert(filesWithWorkspace, filePath) + end + print(core.formatResult(result, filePath)) + else + stdio.write(stdio.color("red")) + stdio.write(`Error processing {filePath}: {result}\n`) + stdio.write(stdio.color("reset")) + end + else + stdio.write(stdio.color("yellow")) + stdio.write(`Warning: {filePath} is not a .rbxm or .rbxmx file, skipping.\n`) + stdio.write(stdio.color("reset")) + end + else + stdio.write(stdio.color("red")) + stdio.write(`Error: File {filePath} does not exist.\n`) + stdio.write(stdio.color("reset")) + end + end + return filesWithWorkspace +end + +return fileproc