ldd-CC/progdor2.lua

801 lines
21 KiB
Lua

--[[
Progdor 2.0 - File Packaging
by LDDestroier
Get with:
wget https://raw.githubusercontent.com/LDDestroier/CC/master/progdor2.lua
Uses CCA compression API, made by minizbot2012.
--]]
local progdor = {
version = "2.0",
PBlogPath = ".progdor_PB_uploads"
}
local scr_x, scr_y = term.getSize()
local function interpretArgs(tInput, tArgs)
local output = {}
local errors = {}
local usedEntries = {}
for aName, aType in pairs(tArgs) do
output[aName] = false
for i = 1, #tInput do
if not usedEntries[i] then
if tInput[i] == aName and not output[aName] then
if aType then
usedEntries[i] = true
if type(tInput[i+1]) == aType or type(tonumber(tInput[i+1])) == aType then
usedEntries[i+1] = true
if aType == "number" then
output[aName] = tonumber(tInput[i+1])
else
output[aName] = tInput[i+1]
end
else
output[aName] = nil
errors[1] = errors[1] and (errors[1] + 1) or 1
errors[aName] = "expected " .. aType .. ", got " .. type(tInput[i+1])
end
else
usedEntries[i] = true
output[aName] = true
end
end
end
end
end
for i = 1, #tInput do
if not usedEntries[i] then
output[#output+1] = tInput[i]
end
end
return output, errors
end
local yield = function()
os.queueEvent("yield")
os.pullEvent("yield")
end
-- CCA API START --
local bit = bit32
local function pack(bn1, bn2)
return bit.band(bn1, 0xFF), bit.rshift(bn1, 8) + bit.lshift(bit.band(bn2, 0xF), 4), bit.rshift(bn2, 4)
end
local function upack(b1, b2, b3)
return (b1 + bit.lshift(bit.band(b2, 0xF), 8)), (bit.lshift(b3,4) + bit.band(bit.rshift(b2, 4), 0xF))
end
local function createDict(bool)
local ret = {}
for i = 1, 255 do
if bool then
ret[string.char(i)] = i
else
ret[i] = string.char(i)
end
end
if not bool then ret[256] = 256 end
return ret
end
local function cp(sInput)
local dic = createDict(true)
local s = ""
local ch
local dlen = 256
local result = {}
local temp
for i = 1, #sInput do
if dlen == 4095 then
result[#result + 1] = dic[s]
result[#result + 1] = 256
dic = createDict(true)
dlen = 256
s = ""
end
ch = sInput:sub(i, i)
temp = s..ch
if dic[temp] then
s = temp
else
result[#result + 1] = dic[s]
dlen = dlen +1
dic[temp] = dlen
s = ch
end
end
result[#result + 1] = dic[s]
return result
end
local function dc(data)
local dic = createDict(false)
local entry
local ch
local currCode
local result = {}
result[#result + 1] = dic[data[1]]
prefix = dic[data[1]]
for i = 2, #data do
currCode = data[i]
if currCode == 256 then
dic = createDict(false)
prefix = ""
else
entry = dic[currCode]
if entry then--exists in dictionary
ch = entry:sub(1, 1)
result[#result + 1] = entry
if prefix ~= "" then
dic[#dic+1] = prefix .. ch
end
else
ch = prefix:sub(1, 1)
result[#result + 1] = prefix..ch
dic[#dic + 1] = prefix..ch
end
prefix = dic[currCode]
end
end
return table.concat(result)
end
local function trim(inp)
for i = 0,2 do
if inp[#inp] == 0 then
inp[#inp] = nil
end
end
end
local function decompress(input)
local rec = {}
for i = 1, #input, 3 do
if i % 66 == 0 then
yield()
end
rec[#rec+1], rec[#rec+2] = upack(input[i], input[i+1] or 0, input[i+2] or 0)
end
trim(rec)
return dc(rec)
end
local function compress(input)
local rec = {}
local data = cp(input)
for i=1, #data, 2 do
yield()
rec[#rec+1], rec[#rec+2], rec[#rec+3] = pack(data[i], data[i+1] or 0)
end
trim(rec)
return rec
end
local function strCompress(input)
local output = {}
local tbl = compress(input)
for i = 1, #tbl do
output[i] = string.char(tbl[i])
end
return table.concat(output)
end
local function strDecompress(input)
local output = {}
for i = 1, #input do
output[i] = string.byte(input:sub(i,i))
end
return decompress(output)
end
-- CCA API END --
-- colors that are always safe to set to
local safeColorList = {
[colors.white] = true,
[colors.lightGray] = true,
[colors.gray] = true,
[colors.black] = true
}
-- pastebin uploads have a 512K limit
local pastebinFileSizeLimit = 1024 * 512
local argData = {
["-pb"] = "string", -- pastebin get
["-dd"] = "string", -- direct URL download
["-m"] = "string", -- specify main file
["-PB"] = false, -- pastebin upload
["-e"] = false, -- automatic self-extractor
["-s"] = false, -- silent
["-a"] = false, -- use as API with require, also makes silent
["-c"] = false, -- use CCA compression
["-h"] = false, -- show help
["-i"] = false, -- inspect mode
}
local argList, argErrors = interpretArgs({...}, argData)
if #argErrors > 0 then
local errList = ""
for k,v in pairs(argErrors) do
if k ~= 1 then
errList = errList .. "\"" .. k .. "\": " .. v .. "; "
end
error(errList:sub(1, -2))
end
end
local pastebinGet = argList["-pb"] -- string, pastebin code
local directDownload = argList["-dd"] -- string, download URL
local mainFile = argList["-m"] -- string, main executable file
local pastebinUpload = argList["-PB"] -- boolean
local selfExtractor = argList["-e"] -- boolean
local silent = argList["-s"] -- boolean
local useCompression = argList["-c"] -- boolean
local justOverwrite = argList["-o"] -- boolean
if useCompression and selfExtract then
error("Cannot use compression with self-extractor.")
end
local sWrite = function(text)
if not silent then
return write(text)
end
end
local sPrint = function(text)
if not silent then
return print(text)
end
end
local function showHelp()
local helpInfo = {
"progdor v" .. progdor.version,
"Usage: progdor [options] inputFolder (outputFile)",
" progdor [options] inputFile (outputFolder)",
"",
"Progdor is a file/folder packaging program with support for CCA compression and self-extraction.",
"",
"Options:",
" -pb [pastebin ID] : Download from Pastebin.", -- added
" -PB : Upload to pastebin.", -- added
" -dd [download URL] : Download from URL.", -- added
" -e : Adds on self-extractor code to archive.", -- added
" -s : Silences all terminal writing", -- added
" -a : Allows programs to use require() on Progdor.", -- added
" -c : Enables CCA compression.", -- added
" -m : Specify main executable file in archive.", -- added
" -i : Inspect archive without extracting.", -- added
" -o : Overwrite files without asking.", -- added
" -h : Show this help.", -- added
"",
" This Progdor has Super Cow Powers." -- not actually added
}
for y = 1, #helpInfo do
sPrint(helpInfo[y])
end
end
local setTextColor = function(color)
if (not silent) and (term.isColor() or safeColorList[color]) then
term.setTextColor(color)
end
end
local setBackgroundColor = function(color)
if (not silent) and (term.isColor() or safeColorList[color]) then
term.setBackgroundColor(color)
end
end
local inputPath = argList[1]
local outputPath = argList[2] or inputPath
local exists, mode
if inputPath == "moo" and not fs.exists(inputPath) then
print([[
\_/
m00h (__) -(_)-
\ ~Oo~___ / \
(..) |\
___________|_|_|_____________
..."Have you mooed today?"..."]])
return
end
if argList["-h"] then
return showHelp()
elseif argList["-a"] then
mode = "api"
elseif inputPath then
exists = fs.exists(inputPath)
if argList["-i"] then
mode = "inspect"
elseif fs.isDir(inputPath) then
mode = "pack"
else
mode = "unpack"
end
else
return showHelp()
end
if mode == "api" then
silent = true
elseif (pastebinGet or directDownload) and pastebinUpload then
error("Cannot upload and download at the same time!")
end
local specialWrite = function(left, colored, right, color)
local origTextColor = term.getTextColor()
sWrite(left)
setTextColor(color)
sWrite(colored)
setTextColor(origTextColor)
sWrite(right)
end
local specialPrint = function(left, colored, right, color)
return specialWrite(left, colored, right .. "\n", color)
end
local function listAll(path, includePath)
local output = {}
local list = fs.list(path)
local fc = fs.combine
for i = 1, #list do
if fs.isDir(fc(path, list[i])) then
if #fs.list(fc(path, list[i])) == 0 then
output[#output+1] = (includePath and fc(path, list[i]) or list[i]) .. "/"
else
local la = listAll(fc(path, list[i]))
for ii = 1, #la do
output[#output+1] = includePath and fc(path, fc(list[i], la[ii])) or fc(list[i], la[ii])
end
end
else
output[#output+1] = includePath and fc(path, list[i]) or list[i]
end
end
return output
end
local makeFileList = function(path, doCompress)
local output = {}
local list = listAll(path, false)
local file
sPrint("Packing files...")
for i = 1, #list do
setTextColor(colors.lightGray)
sWrite("'" .. list[i] .. "'...")
if list[i]:sub(-1,-1) == "/" then
output[list[i]] = true -- indicates empty directory
else
file = fs.open(fs.combine(path, list[i]), "r")
output[list[i]] = doCompress and strCompress(file.readAll()) or file.readAll()
file.close()
setTextColor(colors.green)
sPrint("good")
end
end
setTextColor(colors.white)
return output
end
local buildArchive = function(path, mainFile, doCompress)
local output = {
compressed = doCompress, -- uses CCA compression
mainFile = mainFile, -- specifies the main program within the archive to run, should I implement something to use that
data = makeFileList(path, doCompress) -- files and folders and whatnot
}
return textutils.serialize(output)
end
local parseArchiveData = function(input, doNotDecompress)
local archive = textutils.unserialize(input)
if archive then
if archive.compressed and (not doNotDecompress) then
for name, contents in pairs(archive.data) do
archive.data[name] = strDecompress(contents)
end
archive.compressed = false
end
return archive
else
return false
end
end
local parseArchive = function(path, doNotDecompress)
local file = fs.open(path, "r")
local output = parseArchiveData(file.readAll(), doNotDecompress)
file.close()
return output
end
local round = function(number, places)
return math.floor(number * (10^places)) / (10^places)
end
local choice = function(input,verbose)
if not input then
input = "yn"
end
if verbose then
sWrite("[")
for a = 1, #input do
sWrite(input:sub(a,a):upper())
if a < #input then
sWrite(",")
end
end
sWrite("]?")
end
local evt,char
repeat
evt,char = os.pullEvent("char")
until string.find(input:lower(),char:lower())
if verbose then
sPrint(char:upper())
end
local pos = string.find(input:lower(), char:lower())
return pos, char:lower()
end
local overwriteOutputPath = function(inputPath, outputPath, allowMerge, override)
setTextColor(colors.white)
local c
if override then
return true, true
else
if allowMerge then
write("Overwrite [Y/N]? Or [M]erge? ")
c = choice("ynm", false)
else
write("Overwrite [Y/N]?")
c = choice("yn", false)
end
write("\n")
if c == 1 then
return true, true
elseif c == 2 then
sPrint("Abort.")
return false, false
elseif c == 3 then
return true, false
end
end
end
local uploadToPastebin = function(archive, name)
if #archive > pastebinFileSizeLimit then
error("That archive is too large to be uploaded to Pastebin. (limit is 512 KB)")
return false
else
local key = "0ec2eb25b6166c0c27a394ae118ad829"
local response = http.post(
"https://pastebin.com/api/api_post.php",
"api_option=paste&" ..
"api_dev_key=" .. key .. "&" ..
"api_paste_format=lua&" ..
"api_paste_name=" .. textutils.urlEncode(name) .. "&" ..
"api_paste_code=" .. textutils.urlEncode(archive)
)
if response then
local sResponse = response.readAll()
response.close()
local sCode = string.match( sResponse, "[^/]+$" )
return sCode, sResponse
else
return false
end
end
end
local writeArchiveData = function(archive, outputPath)
local file
for name, contents in pairs(archive.data) do
setTextColor(colors.lightGray)
sWrite("'" .. name .. "'...")
if contents == true then -- indicates empty directory
fs.makeDir(fs.combine(outputPath, name))
else
file = fs.open(fs.combine(outputPath, name), "w")
if file then
file.write(contents)
file.close()
end
end
if file then
setTextColor(colors.green)
sPrint("good")
else
setTextColor(colors.red)
sPrint("fail")
end
end
setTextColor(colors.white)
specialPrint("Unpacked to '", outputPath .. "/", "'.", colors.yellow)
end
local archive
local doOverwrite, doContinue = false, true
if mode == "pack" then
if not pastebinUpload then
if fs.isReadOnly(outputPath) then
error("Output path is read-only.")
elseif fs.exists(outputPath) and (outputPath ~= inputPath) then
doContinue, doOverwrite = overwriteOutputPath(inputPath, outputPath, false, justOverwrite)
end
if not doContinue then
return false
elseif outputPath == inputPath then
doOverwrite = true
end
end
archive = buildArchive(inputPath, mainFile, useCompression)
if exists then
if useCompression then
sPrint("Using CCA compression.")
elseif selfExtractor then
sPrint("Tacking on self-extractor.")
archive = ([[
local tArg = {...}
local outputPath, file = tArg[1] and fs.combine(shell.dir(), tArg[1]) or shell.getRunningProgram()
local safeColorList = {[colors.white] = true,[colors.lightGray] = true,[colors.gray] = true,[colors.black] = true}
local stc = function(color) if (term.isColor() or safeColorList[color]) then term.setTextColor(color) end end
local archive = textutils.unserialize(]] ..
textutils.serialize(archive) ..
[[)
if fs.isReadOnly(outputPath) then
error("Output path is read-only.")
elseif fs.getFreeSpace(outputPath) <= #archive then
error("Insufficient space.")
end
fs.delete(shell.getRunningProgram()) -- saves space
for name, contents in pairs(archive.data) do
stc(colors.lightGray)
write("'" .. name .. "'...")
if contents == true then -- indicates empty directory
fs.makeDir(fs.combine(outputPath, name))
else
file = fs.open(fs.combine(outputPath, name), "w")
if file then
file.write(contents)
file.close()
end
end
if file then
stc(colors.green)
print("good")
else
stc(colors.red)
print("fail")
end
end
stc(colors.white)
write("Unpacked to '")
stc(colors.yellow)
write(outputPath .. "/")
stc(colors.white)
print("'.")
]])
end
if pastebinUpload then
sWrite("Uploading to Pastebin...")
local id, url = uploadToPastebin(archive, fs.getName(inputPath))
if id then
setTextColor(colors.green)
sPrint("success!")
setTextColor(colors.white)
sPrint("Uploaded to '" .. url .. "'.")
specialPrint("Retrieve with \"", "progdor -pb " .. id .. " " .. fs.getName(inputPath), "\".", colors.yellow)
sPrint("You may need to do a Captcha on the website.")
if not fs.exists(progdor.PBlogPath) then
setTextColor(colors.lightGray)
specialPrint("(PB uploads are logged at \"", progdor.PBlogPath, "\".)", colors.yellow)
setTextColor(colors.white)
end
-- precautionary log file
local file = fs.open(progdor.PBlogPath, "a")
file.writeLine("uploaded \"" .. inputPath .. "\" to \"" .. url .. "\"")
file.close()
else
sPrint("failed!")
end
else
if doOverwrite then
fs.delete(outputPath)
end
local file = fs.open(outputPath, "w")
file.write(archive)
file.close()
if selfExtract then
specialPrint("Written self-extractor to '", outputPath, "'.", colors.yellow)
else
specialPrint("Written to '", outputPath, "'.", colors.yellow)
end
end
else
error("No such input path exists.")
return false
end
elseif mode == "unpack" then -- unpack OR upload
if pastebinUpload then
local file = fs.open(inputPath, "r")
archive = file.readAll()
file.close()
sWrite("Uploading to Pastebin...")
local id, url = uploadToPastebin(archive, fs.getName(inputPath))
if id then
setTextColor(colors.green)
sPrint("success!")
setTextColor(colors.white)
sPrint("Uploaded to '" .. url .. "'.")
specialPrint("Retrieve with \"", "progdor -pb " .. id .. " " .. fs.getName(inputPath), "\".", colors.yellow)
sPrint("You may need to do a Captcha on the website.")
if not fs.exists(progdor.PBlogPath) then
setTextColor(colors.lightGray)
specialPrint("(PB uploads are logged at \"", progdor.PBlogPath, "\".)", colors.yellow)
setTextColor(colors.white)
end
-- precautionary log file
local file = fs.open(progdor.PBlogPath, "a")
file.writeLine("uploaded \"" .. inputPath .. "\" to \"" .. url .. "\"")
file.close()
else
setTextColor(colors.red)
sPrint("failed!")
setTextColor(colors.white)
return false
end
elseif pastebinGet or directDownload then
local url, contents
if pastebinGet and directDownload then
error("Cannot do both pastebin get and direct download.")
elseif fs.isReadOnly(outputPath) then
error("Output path is read only.")
else
if pastebinGet then
url = "http://www.pastebin.com/raw/" .. pastebinGet
elseif directDownload then
url = directDownload
end
if fs.exists(outputPath) and (outputPath ~= inputPath) or outputPath == shell.getRunningProgram() then
doContinue, doOverwrite = overwriteOutputPath(inputPath, outputPath, true, justOverwrite)
end
if not doContinue then
return false
elseif outputPath == inputPath then
doOverwrite = true
end
sWrite("Connecting to \"")
setTextColor(colors.yellow)
sWrite(url)
setTextColor(colors.white)
sWrite("\"...")
local handle = http.get(url)
if handle then
setTextColor(colors.green)
sPrint("success!")
setTextColor(colors.white)
contents = handle.readAll()
handle.close()
-- detects if you didn't solve the captcha, since archives commonly trigger anti-spam measures
if (
pastebinGet and
(not textutils.unserialize(contents)) and
contents:find("Your paste has triggered our automatic SPAM detection filter.")
) then
specialPrint("You must go to '", url, "' and do the Captcha to be able to download that paste.", colors.yellow)
return false
end
setTextColor(colors.lightGray)
sWrite("Parsing archive...")
archive = parseArchiveData(contents)
if archive then
setTextColor(colors.green)
sPrint("good")
else
setTextColor(colors.red)
sPrint("Invalid archive file.")
return false
end
if doOverwrite then
fs.delete(outputPath)
end
writeArchiveData(archive, outputPath)
else
setTextColor(colors.red)
sPrint("failed!")
setTextColor(colors.white)
return false
end
end
else -- regular unpack
if exists then
if fs.exists(outputPath) and (outputPath ~= inputPath) or outputPath == shell.getRunningProgram() then
doContinue, doOverwrite = overwriteOutputPath(inputPath, outputPath, true, justOverwrite)
end
if not doContinue then
return false
elseif outputPath == inputPath then
doOverwrite = true
end
setTextColor(colors.lightGray)
sWrite("Parsing archive...")
archive = parseArchive(inputPath)
setTextColor(colors.green)
sPrint("good")
if doOverwrite then
fs.delete(outputPath)
end
writeArchiveData(archive, outputPath)
else
error("No such input path exists.")
end
end
elseif mode == "inspect" then
if exists and (not fs.isDir(inputPath)) then
archive = parseArchive(inputPath, true)
local totalSize = 0
local amountOfFiles = 0
local averageSize = 0
local output = {}
if archive then
for k,v in pairs(archive) do
if k == "data" then
for name, contents in pairs(v) do
if contents then -- don't count directories, where contents == false
totalSize = totalSize + #contents
amountOfFiles = amountOfFiles + 1
end
end
averageSize = math.ceil(totalSize / amountOfFiles)
else
output[#output + 1] = k .. " = \"" .. tostring(v) .. "\""
end
end
sPrint("# of files: " .. amountOfFiles)
sPrint("Total size: " .. totalSize .. " bytes (" .. round(totalSize / 1024, 1) .. " KB)")
sPrint("Aveg. size: " .. averageSize .. " bytes (" .. round(averageSize / 1024, 1) .. " KB)")
sPrint(("-"):rep(scr_x))
for i = 1, #output do
sPrint(output[i])
end
else
error("Invalid archive file.")
end
else
if fs.isDir(inputPath) then
error("Cannot inspect directories.")
else
error("No such input path exists.")
end
end
elseif mode == "api" then
return {
parseArchive = parseArchive,
parseArchiveData = parseArchiveData,
buildArchive = buildArchive,
uploadToPastebin = uploadToPastebin,
}
end