Skip to content

Instantly share code, notes, and snippets.

@SquidDev
Last active April 13, 2017 09:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SquidDev/6ced21eb437a776444aacef4d597c0f7 to your computer and use it in GitHub Desktop.
Save SquidDev/6ced21eb437a776444aacef4d597c0f7 to your computer and use it in GitHub Desktop.
--[[
The MIT License (MIT)
Copyright (c) 2015-2016 SquidDev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Diff Match and Patch
Copyright 2006 Google Inc.
http://code.google.com/p/google-diff-match-patch/
Based on the JavaScript implementation by Neil Fraser
Ported to Lua by Duncan Cross
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]
local loading = {}
local oldRequire, preload, loaded = require, {}, { startup = loading }
local function require(name)
local result = loaded[name]
if result ~= nil then
if result == loading then
error("loop or previous error loading module '" .. name .. "'", 2)
end
return result
end
loaded[name] = loading
local contents = preload[name]
if contents then
result = contents(name)
elseif oldRequire then
result = oldRequire(name)
else
error("cannot load '" .. name .. "'", 2)
end
if result == nil then result = true end
loaded[name] = result
return result
end
preload["bsrocks.rocks.rockspec"] = function(...)
local dependencies = require "bsrocks.rocks.dependencies"
local fileWrapper = require "bsrocks.lib.files"
local manifest = require "bsrocks.rocks.manifest"
local unserialize = require "bsrocks.lib.serialize".unserialize
local utils = require "bsrocks.lib.utils"
local log, warn, verbose, error = utils.log, utils.warn, utils.verbose, utils.error
local rockCache = {}
local function findRockspec(name)
for server, manifest in pairs(manifest.fetchAll()) do
if manifest.repository and manifest.repository[name] then
return manifest
end
end
return
end
local function latestVersion(manifest, name, constraints)
local module = manifest.repository[name]
if not module then error("Cannot find " .. name) end
local version
for name, dat in pairs(module) do
local ver = dependencies.parseVersion(name)
if constraints then
if dependencies.matchConstraints(ver, constraints) then
if not version or ver > version then
version = ver
end
end
elseif not version or ver > version then
version = ver
end
end
if not version then error("Cannot find version for " .. name) end
return version.name
end
local function fetchRockspec(server, name, version)
local whole = name .. "-" .. version
local rockspec = rockCache[whole]
if rockspec then return rockspec end
log("Fetching rockspec " .. whole)
verbose("Using '" .. server .. name .. '-' .. version .. ".rockspec' for " .. whole)
local handle = http.get(server .. name .. '-' .. version .. '.rockspec')
if not handle then
error("Canot fetch " .. name .. "-" .. version .. " from " .. server, 0)
end
local contents = handle.readAll()
handle.close()
rockspec = unserialize(contents)
rockCache[whole] = rockspec
return rockspec
end
--- Extract files to download from rockspec
-- @see https://github.com/keplerproject/luarocks/wiki/Rockspec-format
local function extractFiles(rockspec, blacklist)
local files, fileN = {}, 0
blacklist = blacklist or {}
local build = rockspec.build
if build then
if build.modules then
for _, file in pairs(build.modules) do
if not blacklist[file] then
fileN = fileN + 1
files[fileN] = file
end
end
end
-- Extract install locations
if build.install then
for _, install in pairs(build.install) do
for _, file in pairs(install) do
if not blacklist[file] then
fileN = fileN + 1
files[fileN] = file
end
end
end
end
end
return files
end
return {
findRockspec = findRockspec,
fetchRockspec = fetchRockspec,
latestVersion = latestVersion,
extractFiles = extractFiles
}
end
preload["bsrocks.rocks.patchspec"] = function(...)
local diff = require "bsrocks.lib.diff"
local fileWrapper = require "bsrocks.lib.files"
local manifest = require "bsrocks.rocks.manifest"
local patch = require "bsrocks.lib.patch"
local patchDirectory = require "bsrocks.lib.settings".patchDirectory
local unserialize = require "bsrocks.lib.serialize".unserialize
local utils = require "bsrocks.lib.utils"
local log, warn, verbose, error = utils.log, utils.warn, utils.verbose, utils.error
local patchCache = {}
local function findPatchspec(name)
for server, manifest in pairs(manifest.fetchAll()) do
if manifest.patches and manifest.patches[name] then
return manifest
end
end
return
end
local function fetchPatchspec(server, name)
local result = patchCache[name] or false
if result then return result end
log("Fetching patchspec " .. name)
verbose("Using '" .. server .. name .. ".patchspec' for " .. name)
local handle = http.get(server .. name .. '.patchspec')
if not handle then
error("Canot fetch " .. name .. " from " .. server, 0)
end
local contents = handle.readAll()
handle.close()
result = unserialize(contents)
result.server = server
patchCache[name] = result
return result
end
local installed = nil
local function getAll()
if not installed then
installed = {}
local dir = fs.combine(patchDirectory, "rocks")
for _, file in ipairs(fs.list(dir)) do
if file:match("%.patchspec$") then
local path = fs.combine(dir, file)
local patchspec = unserialize(fileWrapper.read(path))
installed[file:gsub("%.patchspec$", "")] = patchspec
end
end
end
return installed
end
local function extractFiles(patch)
local files, n = {}, 0
if patch.added then
for _, file in ipairs(patch.added) do
n = n + 1
files[n] = file
end
end
if patch.patches then
for _, file in ipairs(patch.patches) do
n = n + 1
files[n] = file .. ".patch"
end
end
return files
end
local function extractSource(rockS, patchS)
local source = patchS and patchS.source
if source then
local version = rockS.version
local out = {}
for k, v in pairs(source) do
if type(v) == "string" then v = v:gsub("%%{version}", version) end
out[k] = v
end
return out
end
return rockS.source
end
local function makePatches(original, changed)
local patches, remove = {}, {}
local files = {}
for path, originalContents in pairs(original) do
local changedContents = changed[path]
if changedContents then
local diffs = diff(originalContents, changedContents)
os.queueEvent("diff")
coroutine.yield("diff")
local patchData = patch.makePatch(diffs)
if #patchData > 0 then
patches[#patches + 1] = path
files[path .. ".patch"] = patch.writePatch(patchData, path)
end
os.queueEvent("diff")
coroutine.yield("diff")
else
remove[#remove + 1] = path
end
end
local added = {}
for path, contents in pairs(changed) do
if not original[path] then
added[#added + 1] = path
files[path] = contents
end
end
return files, patches, added, remove
end
local function applyPatches(original, files, patches, added, removed)
assert(type(original) == "table", "exected table for original")
assert(type(files) == "table", "exected table for replacement")
assert(type(patches) == "table", "exected table for patches")
assert(type(added) == "table", "exected table for added")
assert(type(removed) == "table", "exected table for removed")
local changed = {}
local modified = {}
local issues = false
for _, file in ipairs(patches) do
local patchContents = files[file .. ".patch"]
local originalContents = original[file]
if not patchContents then error("Cannot find patch " .. file .. ".patch") end
if not originalContents then error("Cannot find original " .. file) end
verbose("Applying patch to " .. file)
local patches = patch.readPatch(patchContents)
local success, message = patch.applyPatch(patches, originalContents, file)
if not success then
warn("Cannot apply " .. file .. ": " .. message)
issues = true
else
changed[file] = success
modified[file] = true
end
os.queueEvent("diff")
coroutine.yield("diff")
end
if issues then
error("Issues occured when patching", 0)
end
for _, file in ipairs(removed) do
modified[file] = true
end
for _, file in ipairs(added) do
local changedContents = files[file]
if not changedContents then error("Cannot find added file " .. file) end
changed[file] = changedContents
modified[file] = true
end
for file, contents in pairs(original) do
if not modified[file] then
changed[file] = contents
end
end
return changed
end
return {
findPatchspec = findPatchspec,
fetchPatchspec = fetchPatchspec,
makePatches = makePatches,
extractSource = extractSource,
applyPatches = applyPatches,
extractFiles = extractFiles,
getAll = getAll,
}
end
preload["bsrocks.rocks.manifest"] = function(...)
local fileWrapper = require "bsrocks.lib.files"
local log = require "bsrocks.lib.utils".log
local settings = require "bsrocks.lib.settings"
local unserialize = require "bsrocks.lib.serialize".unserialize
local manifestCache = {}
local servers = settings.servers
local patchDirectory = settings.patchDirectory
local function fetchManifest(server)
local manifest = manifestCache[server]
if manifest then return manifest end
log("Fetching manifest " .. server)
local handle = http.get(server .. "manifest-5.1")
if not handle then
error("Cannot fetch manifest: " .. server, 0)
end
local contents = handle.readAll()
handle.close()
manifest = unserialize(contents)
manifest.server = server
manifestCache[server] = manifest
return manifest
end
local function fetchAll()
local toFetch, n = {}, 0
for _, server in ipairs(servers) do
if not manifestCache[server] then
n = n + 1
toFetch[n] = function() fetchManifest(server) end
end
end
if n > 0 then
if n == 1 then
toFetch[1]()
else
parallel.waitForAll(unpack(toFetch))
end
end
return manifestCache
end
local function loadLocal()
local path = fs.combine(patchDirectory, "rocks/manifest-5.1")
if not fs.exists(path) then
return {
repository = {},
commands = {},
modules = {},
patches = {},
}, path
else
return unserialize(fileWrapper.read(path)), path
end
end
return {
fetchManifest = fetchManifest,
fetchAll = fetchAll,
loadLocal = loadLocal,
}
end
preload["bsrocks.rocks.install"] = function(...)
local dependencies = require "bsrocks.rocks.dependencies"
local download = require "bsrocks.downloaders"
local fileWrapper = require "bsrocks.lib.files"
local patchspec = require "bsrocks.rocks.patchspec"
local rockspec = require "bsrocks.rocks.rockspec"
local serialize = require "bsrocks.lib.serialize"
local settings = require "bsrocks.lib.settings"
local tree = require "bsrocks.downloaders.tree"
local utils = require "bsrocks.lib.utils"
local installDirectory = settings.installDirectory
local log, warn, verbose, error = utils.log, utils.warn, utils.verbose, utils.error
local fetched = false
local installed = {}
local installedPatches = {}
local function extractFiles(rockS, patchS)
local blacklist = {}
if patchS and patchS.remove then
for _, v in ipairs(patchS.remove) do blacklist[v] = true end
end
return rockspec.extractFiles(rockS, blacklist)
end
local function findIssues(rockS, patchS, files)
files = files or extractFiles(rockS, patchS)
local issues = {}
local error = false
local source = patchspec.extractSource(rockS, patchS)
if not download(source, false) then
issues[#issues + 1] = { "Cannot find downloader for " .. source.url .. ". Please suggest this package to be patched.", true }
error = true
end
for _, file in ipairs(files) do
if type(file) == "table" then
issues[#issues + 1] = { table.concat(file, ", ") .. " are packaged into one module. This will not work.", true }
else
local ext = file:match("[^/]%.(%w+)$")
if ext and ext ~= "lua" then
if ext == "c" or ext == "cpp" or ext == "h" or ext == "hpp" then
issues[#issues + 1] = { file .. " is a C file. This will not work.", true }
error = true
else
issues[#issues + 1] = { "File extension is not lua (for " .. file .. "). It may not work correctly.", false }
end
end
end
end
return error, issues
end
local function save(rockS, patchS)
local files = extractFiles(rockS, patchS)
local errored, issues = findIssues(rockS, patchS, files)
if #issues > 0 then
utils.printColoured("This package is incompatible", colors.red)
for _, v in ipairs(issues) do
local color = colors.yellow
if v[2] then color = colors.red end
utils.printColoured(" " .. v[1], color)
end
if errored then
error("This package is incompatible", 0)
end
end
local source = patchspec.extractSource(rockS, patchS)
local downloaded = download(source, files)
if not downloaded then
-- This should never be reached.
error("Cannot find downloader for " .. source.url .. ".", 0)
end
if patchS then
local patchFiles = patchspec.extractFiles(patchS)
local downloadPatch = tree(patchS.server .. rockS.package .. '/', patchFiles)
downloaded = patchspec.applyPatches(downloaded, downloadPatch, patchS.patches or {}, patchS.added or {}, patchS.removed or {})
end
local build = rockS.build
if build then
if build.modules then
local moduleDir = fs.combine(installDirectory, "lib")
for module, file in pairs(build.modules) do
verbose("Writing module " .. module)
fileWrapper.writeLines(fs.combine(moduleDir, module:gsub("%.", "/") .. ".lua"), downloaded[file])
end
end
-- Extract install locations
if build.install then
for name, install in pairs(build.install) do
local dir = fs.combine(installDirectory, name)
for name, file in pairs(install) do
verbose("Writing " .. name .. " to " .. dir)
if type(name) == "number" and name >= 1 and name <= #install then
name = file
end
fileWrapper.writeLines(fs.combine(dir, name .. ".lua"), downloaded[file])
end
end
end
end
fileWrapper.write(fs.combine(installDirectory, rockS.package .. ".rockspec"), serialize.serialize(rockS))
if patchS then
fileWrapper.write(fs.combine(installDirectory, rockS.package .. ".patchspec"), serialize.serialize(patchS))
end
installed[rockS.package] = rockS
end
local function remove(rockS, patchS)
local blacklist = {}
if patchspec and patchspec.remove then
for _, v in ipairs(patchspec.remove) do blacklist[v] = true end
end
local files = rockspec.extractFiles(rockS, blacklist)
local build = rockS.build
if build then
if build.modules then
local moduleDir = fs.combine(installDirectory, "lib")
for module, file in pairs(build.modules) do
fs.delete(fs.combine(moduleDir, module:gsub("%.", "/") .. ".lua"))
end
end
-- Extract install locations
if build.install then
for name, install in pairs(build.install) do
local dir = fs.combine(installDirectory, name)
for name, file in pairs(install) do
fs.delete(fs.combine(dir, name .. ".lua"))
end
end
end
end
fs.delete(fs.combine(installDirectory, rockS.package .. ".rockspec"))
installed[rockS.package] = nil
end
local function getInstalled()
if not fetched then
fetched = true
for name, version in pairs(settings.existing) do
installed[name:lower()] = { version = version, package = name, builtin = true }
end
if fs.exists(installDirectory) then
for _, file in ipairs(fs.list(installDirectory)) do
if file:match("%.rockspec") then
local data = serialize.unserialize(fileWrapper.read(fs.combine(installDirectory, file)))
installed[data.package:lower()] = data
elseif file:match("%.patchspec") then
local name = file:gsub("%.patchspec", ""):lower()
local data = serialize.unserialize(fileWrapper.read(fs.combine(installDirectory, file)))
installedPatches[name] = data
end
end
end
end
return installed, installedPatches
end
local function install(name, version, constraints)
name = name:lower()
verbose("Preparing to install " .. name .. " " .. (version or ""))
-- Do the cheapest action ASAP
local installed = getInstalled()
local current = installed[name]
if current and ((version == nil and constraints == nil) or current.version == version) then
error(name .. " already installed", 0)
end
local rockManifest = rockspec.findRockspec(name)
if not rockManifest then
error("Cannot find '" .. name .. "'", 0)
end
local patchManifest = patchspec.findPatchspec(name)
if not version then
if patchManifest then
version = patchManifest.patches[name]
else
version = rockspec.latestVersion(rockManifest, name, constraints)
end
end
if current and current.version == version then
error(name .. " already installed", 0)
end
local patchspec = patchManifest and patchspec.fetchPatchspec(patchManifest.server, name)
local rockspec = rockspec.fetchRockspec(rockManifest.server, name, version)
if rockspec.build and rockspec.build.type ~= "builtin" then
error("Cannot build type '" .. rockspec.build.type .. "'. Please suggest this package to be patched.", 0)
end
local deps = rockspec.dependencies
if patchspec and patchspec.dependencies then
deps = patchspec.dependencies
end
for _, deps in ipairs(deps or {}) do
local dependency = dependencies.parseDependency(deps)
local name = dependency.name:lower()
local current = installed[name]
if current then
local version = dependencies.parseVersion(current.version)
if not dependencies.matchConstraints(version, dependency.constraints) then
log("Updating dependency " .. name)
install(name, nil, dependency.constraints)
end
else
log("Installing dependency " .. name)
install(name, nil, dependency.constraints)
end
end
save(rockspec, patchspec)
end
return {
getInstalled = getInstalled,
install = install,
remove = remove,
findIssues = findIssues,
}
end
preload["bsrocks.rocks.dependencies"] = function(...)
local deltas = {
scm = 1100,
cvs = 1000,
rc = -1000,
pre = -10000,
beta = -100000,
alpha = -1000000
}
local versionMeta = {
--- Equality comparison for versions.
-- All version numbers must be equal.
-- If both versions have revision numbers, they must be equal;
-- otherwise the revision number is ignored.
-- @param v1 table: version table to compare.
-- @param v2 table: version table to compare.
-- @return boolean: true if they are considered equivalent.
__eq = function(v1, v2)
if #v1 ~= #v2 then
return false
end
for i = 1, #v1 do
if v1[i] ~= v2[i] then
return false
end
end
if v1.revision and v2.revision then
return (v1.revision == v2.revision)
end
return true
end,
--- Size comparison for versions.
-- All version numbers are compared.
-- If both versions have revision numbers, they are compared;
-- otherwise the revision number is ignored.
-- @param v1 table: version table to compare.
-- @param v2 table: version table to compare.
-- @return boolean: true if v1 is considered lower than v2.
__lt = function(v1, v2)
for i = 1, math.max(#v1, #v2) do
local v1i, v2i = v1[i] or 0, v2[i] or 0
if v1i ~= v2i then
return (v1i < v2i)
end
end
if v1.revision and v2.revision then
return (v1.revision < v2.revision)
end
-- They are equal, so we must escape
return false
end,
--- Size comparison for versions.
-- All version numbers are compared.
-- If both versions have revision numbers, they are compared;
-- otherwise the revision number is ignored.
-- @param v1 table: version table to compare.
-- @param v2 table: version table to compare.
-- @return boolean: true if v1 is considered lower or equal than v2.
__le = function(v1, v2)
for i = 1, math.max(#v1, #v2) do
local v1i, v2i = v1[i] or 0, v2[i] or 0
if v1i ~= v2i then
return (v1i <= v2i)
end
end
if v1.revision and v2.revision then
return (v1.revision <= v2.revision)
end
return true
end,
}
--- Parse a version string, converting to table format.
-- A version table contains all components of the version string
-- converted to numeric format, stored in the array part of the table.
-- If the version contains a revision, it is stored numerically
-- in the 'revision' field. The original string representation of
-- the string is preserved in the 'string' field.
-- Returned version tables use a metatable
-- allowing later comparison through relational operators.
-- @param vstring string: A version number in string format.
-- @return table or nil: A version table or nil
-- if the input string contains invalid characters.
local function parseVersion(vstring)
vstring = vstring:match("^%s*(.*)%s*$")
local main, revision = vstring:match("(.*)%-(%d+)$")
local version = {name=vstring}
local i = 1
if revision then
vstring = main
version.revision = tonumber(revision)
end
while #vstring > 0 do
-- extract a number
local token, rest = vstring:match("^(%d+)[%.%-%_]*(.*)")
if token then
local number = tonumber(token)
version[i] = version[i] and version[i] + number/100000 or number
i = i + 1
else
-- extract a word
token, rest = vstring:match("^(%a+)[%.%-%_]*(.*)")
if not token then
error("Warning: version number '"..vstring.."' could not be parsed.", 0)
if not version[i] then version[i] = 0 end
break
end
local number = deltas[token] or (token:byte() / 1000)
version[i] = version[i] and version[i] + number/100000 or number
end
vstring = rest
end
return setmetatable(version, versionMeta)
end
local operators = {
["=="] = "==", ["~="] = "~=",
[">"] = ">", ["<"] = "<",
[">="] = ">=", ["<="] = "<=", ["~>"] = "~>",
-- plus some convenience translations
[""] = "==", ["="] = "==", ["!="] = "~="
}
--- Consumes a constraint from a string, converting it to table format.
-- For example, a string ">= 1.0, > 2.0" is converted to a table in the
-- format {op = ">=", version={1,0}} and the rest, "> 2.0", is returned
-- back to the caller.
-- @param input string: A list of constraints in string format.
-- @return (table, string) or nil: A table representing the same
-- constraints and the string with the unused input, or nil if the
-- input string is invalid.
local function parseConstraint(constraint)
assert(type(constraint) == "string")
local no_upgrade, op, version, rest = constraint:match("^(@?)([<>=~!]*)%s*([%w%.%_%-]+)[%s,]*(.*)")
local _op = operators[op]
version = parseVersion(version)
if not _op then
return nil, "Encountered bad constraint operator: '" .. tostring(op) .. "' in '" .. input .. "'"
end
if not version then
return nil, "Could not parse version from constraint: '" .. input .. "'"
end
return { op = _op, version = version, no_upgrade = no_upgrade=="@" and true or nil }, rest
end
--- Convert a list of constraints from string to table format.
-- For example, a string ">= 1.0, < 2.0" is converted to a table in the format
-- {{op = ">=", version={1,0}}, {op = "<", version={2,0}}}.
-- Version tables use a metatable allowing later comparison through
-- relational operators.
-- @param input string: A list of constraints in string format.
-- @return table or nil: A table representing the same constraints,
-- or nil if the input string is invalid.
local function parseConstraints(input)
assert(type(input) == "string")
local constraints, constraint, oinput = {}, nil, input
while #input > 0 do
constraint, input = parseConstraint(input)
if constraint then
table.insert(constraints, constraint)
else
return nil, "Failed to parse constraint '"..tostring(oinput).."' with error: ".. input
end
end
return constraints
end
--- Convert a dependency from string to table format.
-- For example, a string "foo >= 1.0, < 2.0"
-- is converted to a table in the format
-- {name = "foo", constraints = {{op = ">=", version={1,0}},
-- {op = "<", version={2,0}}}}. Version tables use a metatable
-- allowing later comparison through relational operators.
-- @param dep string: A dependency in string format
-- as entered in rockspec files.
-- @return table or nil: A table representing the same dependency relation,
-- or nil if the input string is invalid.
local function parseDependency(dep)
assert(type(dep) == "string")
local name, rest = dep:match("^%s*([a-zA-Z0-9][a-zA-Z0-9%.%-%_]*)%s*(.*)")
if not name then return nil, "failed to extract dependency name from '" .. tostring(dep) .. "'" end
local constraints, err = parseConstraints(rest)
if not constraints then return nil, err end
return { name = name, constraints = constraints }
end
--- A more lenient check for equivalence between versions.
-- This returns true if the requested components of a version
-- match and ignore the ones that were not given. For example,
-- when requesting "2", then "2", "2.1", "2.3.5-9"... all match.
-- When requesting "2.1", then "2.1", "2.1.3" match, but "2.2"
-- doesn't.
-- @param version string or table: Version to be tested; may be
-- in string format or already parsed into a table.
-- @param requested string or table: Version requested; may be
-- in string format or already parsed into a table.
-- @return boolean: True if the tested version matches the requested
-- version, false otherwise.
local function partialMatch(version, requested)
assert(type(version) == "string" or type(version) == "table")
assert(type(requested) == "string" or type(version) == "table")
if type(version) ~= "table" then version = parseVersion(version) end
if type(requested) ~= "table" then requested = parseVersion(requested) end
if not version or not requested then return false end
for i, ri in ipairs(requested) do
local vi = version[i] or 0
if ri ~= vi then return false end
end
if requested.revision then
return requested.revision == version.revision
end
return true
end
--- Check if a version satisfies a set of constraints.
-- @param version table: A version in table format
-- @param constraints table: An array of constraints in table format.
-- @return boolean: True if version satisfies all constraints,
-- false otherwise.
local function matchConstraints(version, constraints)
assert(type(version) == "table")
assert(type(constraints) == "table")
local ok = true
for _, constraint in pairs(constraints) do
if type(constraint.version) == "string" then
constraint.version = parseVersion(constraint.version)
end
local constraintVersion, constraintOp = constraint.version, constraint.op
if constraintOp == "==" then ok = version == constraintVersion
elseif constraintOp == "~=" then ok = version ~= constraintVersion
elseif constraintOp == ">" then ok = version > constraintVersion
elseif constraintOp == "<" then ok = version < constraintVersion
elseif constraintOp == ">=" then ok = version >= constraintVersion
elseif constraintOp == "<=" then ok = version <= constraintVersion
elseif constraintOp == "~>" then ok = partialMatch(version, constraintVersion)
end
if not ok then break end
end
return ok
end
return {
parseVersion = parseVersion,
parseConstraints = parseConstraints,
parseDependency = parseDependency,
matchConstraints = matchConstraints,
}
end
preload["bsrocks.lib.utils"] = function(...)
local logFile = require "bsrocks.lib.settings".logFile
if fs.exists(logFile) then fs.delete(logFile) end
--- Checks an argument has the correct type
-- @param arg The argument to check
-- @tparam string argType The type that it should be
local function checkType(arg, argType)
local t = type(arg)
if t ~= argType then
error(argType .. " expected, got " .. t, 3)
end
return arg
end
--- Generate a temp name for a file
-- Pretty safe, though not 100% accurate
local function tmpName()
return "/tmp/" .. os.clock() .. "-" .. math.random(1, 2^31-1)
end
local function traceback(thread, message, level)
if type(thread) ~= "thread" then
level = message
message = thread
end
local level = checkType(level or 1, "number")
local result = {"stack traceback: "}
for i = 2, 20 do
local _, err = pcall(error, "", i + level)
if err == "" or err == "nil:" then
break
end
result[i] = err
end
local contents = table.concat(result, "\n\t")
if message then
return tostring(message) .. "\n" .. contents
end
return contents
end
local printColoured, writeColoured
if term.isColour() then
printColoured = function(text, colour)
term.setTextColour(colour)
print(text)
term.setTextColour(colours.white)
end
writeColoured = function(text, colour)
term.setTextColour(colour)
write(text)
term.setTextColour(colours.white)
end
else
printColoured = function(text) print(text) end
writeColoured = write
end
local function doLog(msg)
local handle
if fs.exists(logFile) then
handle = fs.open(logFile, "a")
else
handle = fs.open(logFile, "w")
end
handle.writeLine(msg)
handle.close()
end
local function verbose(msg)
doLog("[VERBOSE] " .. msg)
end
local function log(msg)
doLog("[LOG] " .. msg)
printColoured(msg, colours.lightGrey)
end
local function warn(msg)
doLog("[WARN] " .. msg)
printColoured(msg, colours.yellow)
end
local nativeError = error
local function error(msg, level)
doLog("[ERROR] " .. msg)
if level == nil then level = 2
elseif level ~= 0 then level = level + 1 end
nativeError(msg, level)
end
local matches = {
["^"] = "%^", ["$"] = "%$", ["("] = "%(", [")"] = "%)",
["%"] = "%%", ["."] = "%.", ["["] = "%[", ["]"] = "%]",
["*"] = "%*", ["+"] = "%+", ["-"] = "%-", ["?"] = "%?",
["\0"] = "%z",
}
--- Escape a string for using in a pattern
-- @tparam string pattern The string to escape
-- @treturn string The escaped pattern
local function escapePattern(pattern)
return (pattern:gsub(".", matches))
end
local term = term
local function printIndent(text, indent)
if type(text) ~= "string" then error("string expected, got " .. type(text), 2) end
if type(indent) ~= "number" then error("number expected, got " .. type(indent), 2) end
if stdout and stdout.isPiped then
return stdout.writeLine(text)
end
local w, h = term.getSize()
local x, y = term.getCursorPos()
term.setCursorPos(indent + 1, y)
local function newLine()
if y + 1 <= h then
term.setCursorPos(indent + 1, y + 1)
else
term.setCursorPos(indent + 1, h)
term.scroll(1)
end
x, y = term.getCursorPos()
end
-- Print the line with proper word wrapping
while #text > 0 do
local whitespace = text:match("^[ \t]+")
if whitespace then
-- Print whitespace
term.write(whitespace)
x, y = term.getCursorPos()
text = text:sub(#whitespace + 1 )
end
if text:sub(1, 1) == "\n" then
-- Print newlines
newLine()
text = text:sub(2)
end
local subtext = text:match("^[^ \t\n]+")
if subtext then
text = text:sub(#subtext + 1)
if #subtext > w then
-- Print a multiline word
while #subtext > 0 do
if x > w then newLine() end
term.write(subtext)
subtext = subtext:sub((w-x) + 2)
x, y = term.getCursorPos()
end
else
-- Print a word normally
if x + #subtext - 1 > w then newLine() end
term.write(subtext)
x, y = term.getCursorPos()
end
end
end
if y + 1 <= h then
term.setCursorPos(1, y + 1)
else
term.setCursorPos(1, h)
term.scroll(1)
end
end
return {
checkType = checkType,
escapePattern = escapePattern,
log = log,
printColoured = printColoured,
writeColoured = writeColoured,
printIndent = printIndent,
tmpName = tmpName,
traceback = traceback,
warn = warn,
verbose = verbose,
error = error,
}
end
preload["bsrocks.lib.settings"] = function(...)
local currentSettings = {
patchDirectory = "/rocks-patch",
installDirectory = "/rocks",
servers = {
'https://raw.githubusercontent.com/SquidDev-CC/Blue-Shiny-Rocks/rocks/',
'http://luarocks.org/',
},
tries = 3,
existing = {
lua = "5.1",
bit32 = "5.2.2-1", -- https://luarocks.org/modules/siffiejoe/bit32
computercraft = (_HOST and _HOST:match("ComputerCraft ([%d%.]+)")) or _CC_VERSION or "1.0"
},
libPath = {
"./?.lua",
"./?/init.lua",
"%{patchDirectory}/rocks/lib/?.lua",
"%{patchDirectory}/rocks/lib/?/init.lua",
"%{installDirectory}/lib/?.lua",
"%{installDirectory}/lib/?/init.lua",
},
binPath = {
"/rocks/bin/?.lua",
"/rocks/bin/?",
},
logFile = "bsrocks.log"
}
if fs.exists(".bsrocks") then
local serialize = require "bsrocks.lib.serialize"
local handle = fs.open(".bsrocks", "r")
local contents = handle.readAll()
handle.close()
for k, v in pairs(serialize.unserialize(contents)) do
currentSettings[k] = v
end
end
if settings then
if fs.exists(".settings") then settings.load(".settings") end
for k, v in pairs(currentSettings) do
currentSettings[k] = settings.get("bsrocks." .. k, v)
end
end
--- Add trailing slashes to servers
local function patchServers(servers)
for i, server in ipairs(servers) do
if server:sub(#server) ~= "/" then
servers[i] = server .. "/"
end
end
end
patchServers(currentSettings.servers)
return currentSettings
end
preload["bsrocks.lib.serialize"] = function(...)
local function unserialize(text)
local table = {}
assert(load(text, "unserialize", "t", table))()
table._ENV = nil
return table
end
local keywords = {
[ "and" ] = true, [ "break" ] = true, [ "do" ] = true, [ "else" ] = true,
[ "elseif" ] = true, [ "end" ] = true, [ "false" ] = true, [ "for" ] = true,
[ "function" ] = true, [ "if" ] = true, [ "in" ] = true, [ "local" ] = true,
[ "nil" ] = true, [ "not" ] = true, [ "or" ] = true, [ "repeat" ] = true, [ "return" ] = true,
[ "then" ] = true, [ "true" ] = true, [ "until" ] = true, [ "while" ] = true,
}
local function serializeImpl(value, tracking, indent, root)
local vType = type(value)
if vType == "table" then
if tracking[value] ~= nil then error("Cannot serialize table with recursive entries") end
tracking[value] = true
if next(value) == nil then
-- Empty tables are simple
if root then
return ""
else
return "{}"
end
else
-- Other tables take more work
local result, resultN = {}, 0
local subIndent = indent
if not root then
resultN = resultN + 1
result[resultN] = "{\n"
subIndent = indent .. " "
end
local seen = {}
local finish = "\n"
if not root then finish = ",\n" end
for k,v in ipairs(value) do
seen[k] = true
resultN = resultN + 1
result[resultN] = subIndent .. serializeImpl(v, tracking, subIndent, false) .. finish
end
local keys, keysN, allString = {}, 0, true
local t
for k,v in pairs(value) do
if not seen[k] then
allString = allString and type(k) == "string"
keysN = keysN + 1
keys[keysN] = k
end
end
if allString then
table.sort(keys)
end
for _, k in ipairs(keys) do
local entry
local v = value[k]
if type(k) == "string" and not keywords[k] and string.match( k, "^[%a_][%a%d_]*$" ) then
entry = k .. " = " .. serializeImpl(v, tracking, subIndent)
else
entry = "[ " .. serializeImpl(k, tracking, subIndent) .. " ] = " .. serializeImpl(v, tracking, subIndent)
end
resultN = resultN + 1
result[resultN] = subIndent .. entry .. finish
end
if not root then
resultN = resultN + 1
result[resultN] = indent .. "}"
end
return table.concat(result)
end
elseif vType == "string" then
return string.format( "%q", value )
elseif vType == "number" or vType == "boolean" or vType == "nil" then
return tostring(value)
else
error("Cannot serialize type " .. type, 0)
end
end
local function serialize(table)
return serializeImpl(table, {}, "", true)
end
return {
unserialize = unserialize,
serialize = serialize,
}
end
preload["bsrocks.lib.patch"] = function(...)
local CONTEXT_THRESHOLD = 3
local function makePatch(diff)
local out, n = {}, 0
local oLine, nLine = 1, 1
local current, cn = nil, 0
local context = 0
for i = 1, #diff do
local data = diff[i]
local mode, lines = data[1], data[2]
if mode == "=" then
oLine = oLine + #lines
nLine = nLine + #lines
if current then
local change
local finish = false
if #lines > context + CONTEXT_THRESHOLD then
-- We're not going to merge into the next group
-- so just write the remaining items
change = context
finish = true
else
-- We'll merge into another group, so write everything
change = #lines
end
for i = 1, change do
cn = cn + 1
current[cn] = { mode, lines[i] }
end
current.oCount = current.oCount + change
current.nCount = current.nCount + change
if finish then
-- We've finished this run, and there is more remaining, so
-- we shouldn't continue this patch
context = 0
current = nil
else
context = context - change
end
end
else
context = CONTEXT_THRESHOLD
if not current then
current = {
oLine = oLine,
oCount = 0,
nLine = nLine,
nCount = 0,
}
cn = 0
local previous = diff[i - 1]
if previous and previous[1] == "=" then
local lines = previous[2]
local change = math.min(CONTEXT_THRESHOLD, #lines)
current.oCount = current.oCount + change
current.nCount = current.nCount + change
current.oLine = current.oLine - change
current.nLine = current.nLine - change
for i = #lines - change + 1, #lines do
cn = cn + 1
current[cn] = { "=", lines[i] }
end
end
n = n + 1
out[n] = current
end
if mode == "+" then
nLine = nLine + #lines
current.nCount = current.nCount + #lines
elseif mode == "-" then
oLine = oLine + #lines
current.oCount = current.oCount + #lines
else
error("Unknown mode " .. tostring(mode))
end
for i = 1, #lines do
cn = cn + 1
current[cn] = { mode, lines[i] }
end
end
end
return out
end
local function writePatch(patch, name)
local out, n = {}, 0
if name then
n = 2
out[1] = "--- " .. name
out[2] = "+++ " .. name
end
for i = 1, #patch do
local p = patch[i]
n = n + 1
out[n] = ("@@ -%d,%d +%d,%d @@"):format(p.oLine, p.oCount, p.nLine, p.nCount)
for i = 1, #p do
local row = p[i]
local mode = row[1]
if mode == "=" then mode = " " end
n = n + 1
out[n] = mode .. row[2]
end
end
return out
end
local function readPatch(lines)
if lines[1]:sub(1, 3) ~= "---" then error("Invalid patch format on line #1") end
if lines[2]:sub(1, 3) ~= "+++" then error("Invalid patch format on line #2") end
local out, n = {}, 0
local current, cn = nil, 0
for i = 3, #lines do
local line = lines[i]
if line:sub(1, 2) == "@@" then
local oLine, oCount, nLine, nCount = line:match("^@@ %-(%d+),(%d+) %+(%d+),(%d+) @@$")
if not oLine then error("Invalid block on line #" .. i .. ": " .. line) end
current = {
oLine = oLine,
oCount = oCount,
nLine = nLine,
nCount = nCount,
}
cn = 0
n = n + 1
out[n] = current
else
local mode = line:sub(1, 1)
local data = line:sub(2)
if mode == " " or mode == "" then
-- Allow empty lines (when whitespace has been stripped)
mode = "="
elseif mode ~= "+" and mode ~= "-" then
error("Invalid mode on line #" .. i .. ": " .. line)
end
cn = cn + 1
if not current then error("No block for line #" .. i) end
current[cn] = { mode, data }
end
end
return out
end
local function applyPatch(patch, lines, file)
local out, n = {}, 0
local oLine = 1
for i = 1, #patch do
local data = patch[i]
for i = oLine, data.oLine - 1 do
n = n + 1
out[n] = lines[i]
oLine = oLine + 1
end
if oLine ~= data.oLine and oLine + 0 ~= data.oLine + 0 then
return false, "Incorrect lines. Expected: " .. data.oLine .. ", got " .. oLine .. ". This may be caused by overlapping patches."
end
for i = 1, #data do
local mode, line = data[i][1], data[i][2]
if mode == "=" then
if line ~= lines[oLine] then
return false, "line #" .. oLine .. " is not equal."
end
n = n + 1
out[n] = line
oLine = oLine + 1
elseif mode == "-" then
if line ~= lines[oLine] then
-- TODO: Diff the texts, compute difference, etc...
-- print(("%q"):format(line))
-- print(("%q"):format(lines[oLine]))
-- return false, "line #" .. oLine .. " does not exist"
end
oLine = oLine + 1
elseif mode == "+" then
n = n + 1
out[n] = line
end
end
end
for i = oLine, #lines do
n = n + 1
out[n] = lines[i]
end
return out
end
return {
makePatch = makePatch,
applyPatch = applyPatch,
writePatch = writePatch,
readPatch = readPatch,
}
end
preload["bsrocks.lib.parse"] = function(...)
--- Check if a Lua source is either invalid or incomplete
local setmeta = setmetatable
local function createLookup(tbl)
for _, v in ipairs(tbl) do tbl[v] = true end
return tbl
end
--- List of white chars
local whiteChars = createLookup { ' ', '\n', '\t', '\r' }
--- Lookup of escape characters
local escapeLookup = { ['\r'] = '\\r', ['\n'] = '\\n', ['\t'] = '\\t', ['"'] = '\\"', ["'"] = "\\'" }
--- Lookup of lower case characters
local lowerChars = createLookup {
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
}
--- Lookup of upper case characters
local upperChars = createLookup {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
}
--- Lookup of digits
local digits = createLookup { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }
--- Lookup of hex digits
local hexDigits = createLookup {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'a', 'B', 'b', 'C', 'c', 'D', 'd', 'E', 'e', 'F', 'f'
}
--- Lookup of valid symbols
local symbols = createLookup { '+', '-', '*', '/', '^', '%', ',', '{', '}', '[', ']', '(', ')', ';', '#' }
--- Lookup of valid keywords
local keywords = createLookup {
'and', 'break', 'do', 'else', 'elseif',
'end', 'false', 'for', 'function', 'goto', 'if',
'in', 'local', 'nil', 'not', 'or', 'repeat',
'return', 'then', 'true', 'until', 'while',
}
--- Keywords that end a block
local statListCloseKeywords = createLookup { 'end', 'else', 'elseif', 'until' }
--- Unary operators
local unops = createLookup { '-', 'not', '#' }
--- Stores a list of tokens
-- @type TokenList
-- @tfield table tokens List of tokens
-- @tfield number pointer Pointer to the current
-- @tfield table savedPointers A save point
local TokenList = {}
do
--- Get this element in the token list
-- @tparam int offset The offset in the token list
function TokenList:Peek(offset)
local tokens = self.tokens
offset = offset or 0
return tokens[math.min(#tokens, self.pointer + offset)]
end
--- Get the next token in the list
-- @tparam table tokenList Add the token onto this table
-- @treturn Token The token
function TokenList:Get(tokenList)
local tokens = self.tokens
local pointer = self.pointer
local token = tokens[pointer]
self.pointer = math.min(pointer + 1, #tokens)
if tokenList then
table.insert(tokenList, token)
end
return token
end
--- Check if the next token is of a type
-- @tparam string type The type to compare it with
-- @treturn bool If the type matches
function TokenList:Is(type)
return self:Peek().Type == type
end
--- Check if the next token is a symbol and return it
-- @tparam string symbol Symbol to check (Optional)
-- @tparam table tokenList Add the token onto this table
-- @treturn [ 0 ] ?|token If symbol is not specified, return the token
-- @treturn [ 1 ] boolean If symbol is specified, return true if it matches
function TokenList:ConsumeSymbol(symbol, tokenList)
local token = self:Peek()
if token.Type == 'Symbol' then
if symbol then
if token.Data == symbol then
self:Get(tokenList)
return true
else
return nil
end
else
self:Get(tokenList)
return token
end
else
return nil
end
end
--- Check if the next token is a keyword and return it
-- @tparam string kw Keyword to check (Optional)
-- @tparam table tokenList Add the token onto this table
-- @treturn [ 0 ] ?|token If kw is not specified, return the token
-- @treturn [ 1 ] boolean If kw is specified, return true if it matches
function TokenList:ConsumeKeyword(kw, tokenList)
local token = self:Peek()
if token.Type == 'Keyword' and token.Data == kw then
self:Get(tokenList)
return true
else
return nil
end
end
--- Check if the next token matches is a keyword
-- @tparam string kw The particular keyword
-- @treturn boolean If it matches or not
function TokenList:IsKeyword(kw)
local token = self:Peek()
return token.Type == 'Keyword' and token.Data == kw
end
--- Check if the next token matches is a symbol
-- @tparam string symbol The particular symbol
-- @treturn boolean If it matches or not
function TokenList:IsSymbol(symbol)
local token = self:Peek()
return token.Type == 'Symbol' and token.Data == symbol
end
--- Check if the next token is an end of file
-- @treturn boolean If the next token is an end of file
function TokenList:IsEof()
return self:Peek().Type == 'Eof'
end
end
--- Create a list of @{Token|tokens} from a Lua source
-- @tparam string src Lua source code
-- @treturn TokenList The list of @{Token|tokens}
local function lex(src)
--token dump
local tokens = {}
do -- Main bulk of the work
--line / char / pointer tracking
local pointer = 1
local line = 1
local char = 1
--get / peek functions
local function get()
local c = src:sub(pointer,pointer)
if c == '\n' then
char = 1
line = line + 1
else
char = char + 1
end
pointer = pointer + 1
return c
end
local function peek(n)
n = n or 0
return src:sub(pointer+n,pointer+n)
end
local function consume(chars)
local c = peek()
for i = 1, #chars do
if c == chars:sub(i,i) then return get() end
end
end
--shared stuff
local function generateError(err, resumable)
if resumable == true then
resumable = 1
else
resumable = 0
end
error(line..":"..char..":"..resumable..":"..err, 0)
end
local function tryGetLongString()
local start = pointer
if peek() == '[' then
local equalsCount = 0
local depth = 1
while peek(equalsCount+1) == '=' do
equalsCount = equalsCount + 1
end
if peek(equalsCount+1) == '[' then
--start parsing the string. Strip the starting bit
for _ = 0, equalsCount+1 do get() end
--get the contents
local contentStart = pointer
while true do
--check for eof
if peek() == '' then
generateError("Expected `]"..string.rep('=', equalsCount).."]` near <eof>.", true)
end
--check for the end
local foundEnd = true
if peek() == ']' then
for i = 1, equalsCount do
if peek(i) ~= '=' then foundEnd = false end
end
if peek(equalsCount+1) ~= ']' then
foundEnd = false
end
else
if peek() == '[' then
-- is there an embedded long string?
local embedded = true
for i = 1, equalsCount do
if peek(i) ~= '=' then
embedded = false
break
end
end
if peek(equalsCount + 1) == '[' and embedded then
-- oh look, there was
depth = depth + 1
for i = 1, (equalsCount + 2) do
get()
end
end
end
foundEnd = false
end
if foundEnd then
depth = depth - 1
if depth == 0 then
break
else
for i = 1, equalsCount + 2 do
get()
end
end
else
get()
end
end
--get the interior string
local contentString = src:sub(contentStart, pointer-1)
--found the end. Get rid of the trailing bit
for i = 0, equalsCount+1 do get() end
--get the exterior string
local longString = src:sub(start, pointer-1)
--return the stuff
return contentString, longString
else
return nil
end
else
return nil
end
end
--main token emitting loop
while true do
--get leading whitespace. The leading whitespace will include any comments
--preceding the token. This prevents the parser needing to deal with comments
--separately.
local longStr = false
while true do
local c = peek()
if c == '#' and peek(1) == '!' and line == 1 then
-- #! shebang for linux scripts
get()
get()
while peek() ~= '\n' and peek() ~= '' do
get()
end
end
if c == ' ' or c == '\t' or c == '\n' or c == '\r' then
get()
elseif c == '-' and peek(1) == '-' then
--comment
get() get()
local _, wholeText = tryGetLongString()
if not wholeText then
while peek() ~= '\n' and peek() ~= '' do
get()
end
end
else
break
end
end
--get the initial char
local thisLine = line
local thisChar = char
local errorAt = ":"..line..":"..char..":> "
local c = peek()
--symbol to emit
local toEmit = nil
--branch on type
if c == '' then
--eof
toEmit = { Type = 'Eof' }
elseif upperChars[c] or lowerChars[c] or c == '_' then
--ident or keyword
local start = pointer
repeat
get()
c = peek()
until not (upperChars[c] or lowerChars[c] or digits[c] or c == '_')
local dat = src:sub(start, pointer-1)
if keywords[dat] then
toEmit = {Type = 'Keyword', Data = dat}
else
toEmit = {Type = 'Ident', Data = dat}
end
elseif digits[c] or (peek() == '.' and digits[peek(1)]) then
--number const
local start = pointer
if c == '0' and peek(1) == 'x' then
get();get()
while hexDigits[peek()] do get() end
if consume('Pp') then
consume('+-')
while digits[peek()] do get() end
end
else
while digits[peek()] do get() end
if consume('.') then
while digits[peek()] do get() end
end
if consume('Ee') then
consume('+-')
if not digits[peek()] then generateError("Expected exponent") end
repeat get() until not digits[peek()]
end
local n = peek():lower()
if (n >= 'a' and n <= 'z') or n == '_' then
generateError("Invalid number format")
end
end
toEmit = {Type = 'Number', Data = src:sub(start, pointer-1)}
elseif c == '\'' or c == '\"' then
local start = pointer
--string const
local delim = get()
local contentStart = pointer
while true do
local c = get()
if c == '\\' then
get() --get the escape char
elseif c == delim then
break
elseif c == '' or c == '\n' then
generateError("Unfinished string near <eof>")
end
end
local content = src:sub(contentStart, pointer-2)
local constant = src:sub(start, pointer-1)
toEmit = {Type = 'String', Data = constant, Constant = content}
elseif c == '[' then
local content, wholetext = tryGetLongString()
if wholetext then
toEmit = {Type = 'String', Data = wholetext, Constant = content}
else
get()
toEmit = {Type = 'Symbol', Data = '['}
end
elseif consume('>=<') then
if consume('=') then
toEmit = {Type = 'Symbol', Data = c..'='}
else
toEmit = {Type = 'Symbol', Data = c}
end
elseif consume('~') then
if consume('=') then
toEmit = {Type = 'Symbol', Data = '~='}
else
generateError("Unexpected symbol `~` in source.")
end
elseif consume('.') then
if consume('.') then
if consume('.') then
toEmit = {Type = 'Symbol', Data = '...'}
else
toEmit = {Type = 'Symbol', Data = '..'}
end
else
toEmit = {Type = 'Symbol', Data = '.'}
end
elseif consume(':') then
if consume(':') then
toEmit = {Type = 'Symbol', Data = '::'}
else
toEmit = {Type = 'Symbol', Data = ':'}
end
elseif symbols[c] then
get()
toEmit = {Type = 'Symbol', Data = c}
else
local contents, all = tryGetLongString()
if contents then
toEmit = {Type = 'String', Data = all, Constant = contents}
else
generateError("Unexpected Symbol `"..c.."` in source.")
end
end
--add the emitted symbol, after adding some common data
toEmit.line = thisLine
toEmit.char = thisChar
tokens[#tokens+1] = toEmit
--halt after eof has been emitted
if toEmit.Type == 'Eof' then break end
end
end
--public interface:
local tokenList = setmetatable({
tokens = tokens,
pointer = 1
}, {__index = TokenList})
return tokenList
end
--- Create a AST tree from a Lua Source
-- @tparam TokenList tok List of tokens from @{lex}
-- @treturn table The AST tree
local function parse(tok)
--- Generate an error
-- @tparam string msg The error message
-- @raise The produces error message
local function GenerateError(msg) error(msg, 0) end
local ParseExpr,
ParseStatementList,
ParseSimpleExpr
--- Parse the function definition and its arguments
-- @tparam Scope.Scope scope The current scope
-- @treturn Node A function Node
local function ParseFunctionArgsAndBody()
if not tok:ConsumeSymbol('(') then
GenerateError("`(` expected.")
end
--arg list
while not tok:ConsumeSymbol(')') do
if tok:Is('Ident') then
tok:Get()
if not tok:ConsumeSymbol(',') then
if tok:ConsumeSymbol(')') then
break
else
GenerateError("`)` expected.")
end
end
elseif tok:ConsumeSymbol('...') then
if not tok:ConsumeSymbol(')') then
GenerateError("`...` must be the last argument of a function.")
end
break
else
GenerateError("Argument name or `...` expected")
end
end
ParseStatementList()
if not tok:ConsumeKeyword('end') then
GenerateError("`end` expected after function body")
end
end
--- Parse a simple expression
-- @tparam Scope.Scope scope The current scope
-- @treturn Node the resulting node
local function ParsePrimaryExpr()
if tok:ConsumeSymbol('(') then
ParseExpr()
if not tok:ConsumeSymbol(')') then
GenerateError("`)` Expected.")
end
return { AstType = "Paren" }
elseif tok:Is('Ident') then
tok:Get()
else
GenerateError("primary expression expected")
end
end
--- Parse some table related expressions
-- @tparam boolean onlyDotColon Only allow '.' or ':' nodes
-- @treturn Node The resulting node
function ParseSuffixedExpr(onlyDotColon)
--base primary expression
local prim = ParsePrimaryExpr() or { AstType = ""}
while true do
local tokenList = {}
if tok:ConsumeSymbol('.') or tok:ConsumeSymbol(':') then
if not tok:Is('Ident') then
GenerateError("<Ident> expected.")
end
tok:Get()
prim = { AstType = 'MemberExpr' }
elseif not onlyDotColon and tok:ConsumeSymbol('[') then
ParseExpr()
if not tok:ConsumeSymbol(']') then
GenerateError("`]` expected.")
end
prim = { AstType = 'IndexExpr' }
elseif not onlyDotColon and tok:ConsumeSymbol('(') then
while not tok:ConsumeSymbol(')') do
ParseExpr()
if not tok:ConsumeSymbol(',') then
if tok:ConsumeSymbol(')') then
break
else
GenerateError("`)` Expected.")
end
end
end
prim = { AstType = 'CallExpr' }
elseif not onlyDotColon and tok:Is('String') then
--string call
tok:Get()
prim = { AstType = 'StringCallExpr' }
elseif not onlyDotColon and tok:IsSymbol('{') then
--table call
ParseSimpleExpr()
prim = { AstType = 'TableCallExpr' }
else
break
end
end
return prim
end
--- Parse a simple expression (strings, numbers, booleans, varargs)
-- @treturn Node The resulting node
function ParseSimpleExpr()
if tok:Is('Number') or tok:Is('String') then
tok:Get()
elseif tok:ConsumeKeyword('nil') or tok:ConsumeKeyword('false') or tok:ConsumeKeyword('true') or tok:ConsumeSymbol('...') then
elseif tok:ConsumeSymbol('{') then
while true do
if tok:ConsumeSymbol('[') then
--key
ParseExpr()
if not tok:ConsumeSymbol(']') then
GenerateError("`]` Expected")
end
if not tok:ConsumeSymbol('=') then
GenerateError("`=` Expected")
end
ParseExpr()
elseif tok:Is('Ident') then
--value or key
local lookahead = tok:Peek(1)
if lookahead.Type == 'Symbol' and lookahead.Data == '=' then
--we are a key
local key = tok:Get()
if not tok:ConsumeSymbol('=') then
GenerateError("`=` Expected")
end
ParseExpr()
else
--we are a value
ParseExpr()
end
elseif tok:ConsumeSymbol('}') then
break
else
ParseExpr()
end
if tok:ConsumeSymbol(';') or tok:ConsumeSymbol(',') then
--all is good
elseif tok:ConsumeSymbol('}') then
break
else
GenerateError("`}` or table entry Expected")
end
end
elseif tok:ConsumeKeyword('function') then
return ParseFunctionArgsAndBody()
else
return ParseSuffixedExpr()
end
end
local unopprio = 8
local priority = {
['+'] = {6,6},
['-'] = {6,6},
['%'] = {7,7},
['/'] = {7,7},
['*'] = {7,7},
['^'] = {10,9},
['..'] = {5,4},
['=='] = {3,3},
['<'] = {3,3},
['<='] = {3,3},
['~='] = {3,3},
['>'] = {3,3},
['>='] = {3,3},
['and'] = {2,2},
['or'] = {1,1},
}
--- Parse an expression
-- @tparam int level Current level (Optional)
-- @treturn Node The resulting node
function ParseExpr(level)
level = level or 0
--base item, possibly with unop prefix
if unops[tok:Peek().Data] then
local op = tok:Get().Data
ParseExpr(unopprio)
else
ParseSimpleExpr()
end
--next items in chain
while true do
local prio = priority[tok:Peek().Data]
if prio and prio[1] > level then
local tokenList = {}
tok:Get()
ParseExpr(prio[2])
else
break
end
end
end
--- Parse a statement (if, for, while, etc...)
-- @treturn Node The resulting node
local function ParseStatement()
if tok:ConsumeKeyword('if') then
--clauses
repeat
ParseExpr()
if not tok:ConsumeKeyword('then') then
GenerateError("`then` expected.")
end
ParseStatementList()
until not tok:ConsumeKeyword('elseif')
--else clause
if tok:ConsumeKeyword('else') then
ParseStatementList()
end
--end
if not tok:ConsumeKeyword('end') then
GenerateError("`end` expected.")
end
elseif tok:ConsumeKeyword('while') then
--condition
ParseExpr()
--do
if not tok:ConsumeKeyword('do') then
return GenerateError("`do` expected.")
end
--body
ParseStatementList()
--end
if not tok:ConsumeKeyword('end') then
GenerateError("`end` expected.")
end
elseif tok:ConsumeKeyword('do') then
--do block
ParseStatementList()
if not tok:ConsumeKeyword('end') then
GenerateError("`end` expected.")
end
elseif tok:ConsumeKeyword('for') then
--for block
if not tok:Is('Ident') then
GenerateError("<ident> expected.")
end
tok:Get()
if tok:ConsumeSymbol('=') then
--numeric for
ParseExpr()
if not tok:ConsumeSymbol(',') then
GenerateError("`,` Expected")
end
ParseExpr()
if tok:ConsumeSymbol(',') then
ParseExpr()
end
if not tok:ConsumeKeyword('do') then
GenerateError("`do` expected")
end
ParseStatementList()
if not tok:ConsumeKeyword('end') then
GenerateError("`end` expected")
end
else
--generic for
while tok:ConsumeSymbol(',') do
if not tok:Is('Ident') then
GenerateError("for variable expected.")
end
tok:Get(tokenList)
end
if not tok:ConsumeKeyword('in') then
GenerateError("`in` expected.")
end
ParseExpr()
while tok:ConsumeSymbol(',') do
ParseExpr()
end
if not tok:ConsumeKeyword('do') then
GenerateError("`do` expected.")
end
ParseStatementList()
if not tok:ConsumeKeyword('end') then
GenerateError("`end` expected.")
end
end
elseif tok:ConsumeKeyword('repeat') then
ParseStatementList()
if not tok:ConsumeKeyword('until') then
GenerateError("`until` expected.")
end
ParseExpr()
elseif tok:ConsumeKeyword('function') then
if not tok:Is('Ident') then
GenerateError("Function name expected")
end
ParseSuffixedExpr(true) --true => only dots and colons
ParseFunctionArgsAndBody()
elseif tok:ConsumeKeyword('local') then
if tok:Is('Ident') then
tok:Get()
while tok:ConsumeSymbol(',') do
if not tok:Is('Ident') then
GenerateError("local var name expected")
end
tok:Get()
end
if tok:ConsumeSymbol('=') then
repeat
ParseExpr()
until not tok:ConsumeSymbol(',')
end
elseif tok:ConsumeKeyword('function') then
if not tok:Is('Ident') then
GenerateError("Function name expected")
end
tok:Get(tokenList)
ParseFunctionArgsAndBody()
else
GenerateError("local var or function def expected")
end
elseif tok:ConsumeSymbol('::') then
if not tok:Is('Ident') then
GenerateError('Label name expected')
end
tok:Get()
if not tok:ConsumeSymbol('::') then
GenerateError("`::` expected")
end
elseif tok:ConsumeKeyword('return') then
local exList = {}
local token = tok:Peek()
if token.Type == "Eof" or token.Type ~= "Keyword" or not statListCloseKeywords[token.Data] then
ParseExpr()
local token = tok:Peek()
while tok:ConsumeSymbol(',') do
ParseExpr()
end
end
elseif tok:ConsumeKeyword('break') then
elseif tok:ConsumeKeyword('goto') then
if not tok:Is('Ident') then
GenerateError("Label expected")
end
tok:Get(tokenList)
else
--statementParseExpr
local suffixed = ParseSuffixedExpr()
--assignment or call?
if tok:IsSymbol(',') or tok:IsSymbol('=') then
--check that it was not parenthesized, making it not an lvalue
if suffixed.AstType == "Paren" then
GenerateError("Can not assign to parenthesized expression, is not an lvalue")
end
--more processing needed
while tok:ConsumeSymbol(',') do
ParseSuffixedExpr()
end
--equals
if not tok:ConsumeSymbol('=') then
GenerateError("`=` Expected.")
end
--rhs
ParseExpr()
while tok:ConsumeSymbol(',') do
ParseExpr()
end
elseif suffixed.AstType == 'CallExpr' or
suffixed.AstType == 'TableCallExpr' or
suffixed.AstType == 'StringCallExpr'
then
--it's a call statement
else
GenerateError("Assignment Statement Expected")
end
end
tok:ConsumeSymbol(';')
end
--- Parse a a list of statements
-- @tparam Scope.Scope scope The current scope
-- @treturn Node The resulting node
function ParseStatementList()
while not statListCloseKeywords[tok:Peek().Data] and not tok:IsEof() do
ParseStatement()
end
end
return ParseStatementList()
end
return {
lex = lex,
parse = parse,
}
end
preload["bsrocks.lib.match"] = function(...)
--[[
* Diff Match and Patch
*
* Copyright 2006 Google Inc.
* http://code.google.com/p/google-diff-match-patch/
*
* Based on the JavaScript implementation by Neil Fraser.
* Ported to Lua by Duncan Cross.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
--]]
local band, bor, lshift = bit32.band, bit32.bor, bit32.lshift
local error = error
local strsub, strbyte, strchar, gmatch, gsub = string.sub, string.byte, string.char, string.gmatch, string.gsub
local strmatch, strfind, strformat = string.match, string.find, string.format
local tinsert, tremove, tconcat = table.insert, table.remove, table.concat
local max, min, floor, ceil, abs = math.max, math.min, math.floor, math.ceil, math.abs
local Match_Distance = 1000
local Match_Threshold = 0.3
local Match_MaxBits = 32
local function indexOf(a, b, start)
if (#b == 0) then
return nil
end
return strfind(a, b, start, true)
end
-- ---------------------------------------------------------------------------
-- MATCH API
-- ---------------------------------------------------------------------------
local _match_bitap, _match_alphabet
--[[
* Locate the best instance of 'pattern' in 'text' near 'loc'.
* @param {string} text The text to search.
* @param {string} pattern The pattern to search for.
* @param {number} loc The location to search around.
* @return {number} Best match index or -1.
--]]
local function match_main(text, pattern, loc)
-- Check for null inputs.
if text == nil or pattern == nil then error('Null inputs. (match_main)') end
if text == pattern then
-- Shortcut (potentially not guaranteed by the algorithm)
return 1
elseif #text == 0 then
-- Nothing to match.
return -1
end
loc = max(1, min(loc or 0, #text))
if strsub(text, loc, loc + #pattern - 1) == pattern then
-- Perfect match at the perfect spot! (Includes case of null pattern)
return loc
else
-- Do a fuzzy compare.
return _match_bitap(text, pattern, loc)
end
end
--[[
* Initialise the alphabet for the Bitap algorithm.
* @param {string} pattern The text to encode.
* @return {Object} Hash of character locations.
* @private
--]]
function _match_alphabet(pattern)
local s = {}
local i = 0
for c in gmatch(pattern, '.') do
s[c] = bor(s[c] or 0, lshift(1, #pattern - i - 1))
i = i + 1
end
return s
end
--[[
* Locate the best instance of 'pattern' in 'text' near 'loc' using the
* Bitap algorithm.
* @param {string} text The text to search.
* @param {string} pattern The pattern to search for.
* @param {number} loc The location to search around.
* @return {number} Best match index or -1.
* @private
--]]
function _match_bitap(text, pattern, loc)
if #pattern > Match_MaxBits then
error('Pattern too long.')
end
-- Initialise the alphabet.
local s = _match_alphabet(pattern)
--[[
* Compute and return the score for a match with e errors and x location.
* Accesses loc and pattern through being a closure.
* @param {number} e Number of errors in match.
* @param {number} x Location of match.
* @return {number} Overall score for match (0.0 = good, 1.0 = bad).
* @private
--]]
local function _match_bitapScore(e, x)
local accuracy = e / #pattern
local proximity = abs(loc - x)
if (Match_Distance == 0) then
-- Dodge divide by zero error.
return (proximity == 0) and 1 or accuracy
end
return accuracy + (proximity / Match_Distance)
end
-- Highest score beyond which we give up.
local score_threshold = Match_Threshold
-- Is there a nearby exact match? (speedup)
local best_loc = indexOf(text, pattern, loc)
if best_loc then
score_threshold = min(_match_bitapScore(0, best_loc), score_threshold)
-- LUANOTE: Ideally we'd also check from the other direction, but Lua
-- doesn't have an efficent lastIndexOf function.
end
-- Initialise the bit arrays.
local matchmask = lshift(1, #pattern - 1)
best_loc = -1
local bin_min, bin_mid
local bin_max = #pattern + #text
local last_rd
for d = 0, #pattern - 1, 1 do
-- Scan for the best match; each iteration allows for one more error.
-- Run a binary search to determine how far from 'loc' we can stray at this
-- error level.
bin_min = 0
bin_mid = bin_max
while (bin_min < bin_mid) do
if (_match_bitapScore(d, loc + bin_mid) <= score_threshold) then
bin_min = bin_mid
else
bin_max = bin_mid
end
bin_mid = floor(bin_min + (bin_max - bin_min) / 2)
end
-- Use the result from this iteration as the maximum for the next.
bin_max = bin_mid
local start = max(1, loc - bin_mid + 1)
local finish = min(loc + bin_mid, #text) + #pattern
local rd = {}
for j = start, finish do
rd[j] = 0
end
rd[finish + 1] = lshift(1, d) - 1
for j = finish, start, -1 do
local charMatch = s[strsub(text, j - 1, j - 1)] or 0
if (d == 0) then -- First pass: exact match.
rd[j] = band(bor((rd[j + 1] * 2), 1), charMatch)
else
-- Subsequent passes: fuzzy match.
-- Functions instead of operators make this hella messy.
rd[j] = bor(
band(
bor(
lshift(rd[j + 1], 1),
1
),
charMatch
),
bor(
bor(
lshift(bor(last_rd[j + 1], last_rd[j]), 1),
1
),
last_rd[j + 1]
)
)
end
if (band(rd[j], matchmask) ~= 0) then
local score = _match_bitapScore(d, j - 1)
-- This match will almost certainly be better than any existing match.
-- But check anyway.
if (score <= score_threshold) then
-- Told you so.
score_threshold = score
best_loc = j - 1
if (best_loc > loc) then
-- When passing loc, don't exceed our current distance from loc.
start = max(1, loc * 2 - best_loc)
else
-- Already passed loc, downhill from here on in.
break
end
end
end
end
-- No hope for a (better) match at greater error levels.
if (_match_bitapScore(d + 1, loc) > score_threshold) then
break
end
last_rd = rd
end
return best_loc
end
return match_main
end
preload["bsrocks.lib.files"] = function(...)
local function read(file)
local handle = fs.open(file, "r")
local contents = handle.readAll()
handle.close()
return contents
end
local function readLines(file)
local handle = fs.open(file, "r")
local out, n = {}, 0
for line in handle.readLine do
n = n + 1
out[n] = line
end
handle.close()
-- Trim trailing lines
while out[n] == "" do
out[n] = nil
n = n - 1
end
return out
end
local function write(file, contents)
local handle = fs.open(file, "w")
handle.write(contents)
handle.close()
end
local function writeLines(file, contents)
local handle = fs.open(file, "w")
for i = 1, #contents do
handle.writeLine(contents[i])
end
handle.close()
end
local function assertExists(file, name, level)
if not fs.exists(file) then
error("Cannot find " .. name .. " (Looking for " .. file .. ")", level or 1)
end
end
local function readDir(directory, reader)
reader = reader or read
local offset = #directory + 2
local stack, n = { directory }, 1
local files = {}
while n > 0 do
local top = stack[n]
n = n - 1
if fs.isDir(top) then
for _, file in ipairs(fs.list(top)) do
n = n + 1
stack[n] = fs.combine(top, file)
end
else
files[top:sub(offset)] = reader(top)
end
end
return files
end
local function writeDir(dir, files, writer)
writer = writer or write
for file, contents in pairs(files) do
writer(fs.combine(dir, file), contents)
end
end
return {
read = read,
readLines = readLines,
readDir = readDir,
write = write,
writeLines = writeLines,
writeDir = writeDir,
assertExists = assertExists,
}
end
preload["bsrocks.lib.dump"] = function(...)
local keywords = {
[ "and" ] = true, [ "break" ] = true, [ "do" ] = true, [ "else" ] = true,
[ "elseif" ] = true, [ "end" ] = true, [ "false" ] = true, [ "for" ] = true,
[ "function" ] = true, [ "if" ] = true, [ "in" ] = true, [ "local" ] = true,
[ "nil" ] = true, [ "not" ] = true, [ "or" ] = true, [ "repeat" ] = true, [ "return" ] = true,
[ "then" ] = true, [ "true" ] = true, [ "until" ] = true, [ "while" ] = true,
}
local function serializeImpl(t, tracking, indent, tupleLength)
local objType = type(t)
if objType == "table" and not tracking[t] then
tracking[t] = true
if next(t) == nil then
if tupleLength then
return "()"
else
return "{}"
end
else
local shouldNewLine = false
local length = tupleLength or #t
local builder = 0
for k,v in pairs(t) do
if type(k) == "table" or type(v) == "table" then
shouldNewLine = true
break
elseif type(k) == "number" and k >= 1 and k <= length and k % 1 == 0 then
builder = builder + #tostring(v) + 2
else
builder = builder + #tostring(v) + #tostring(k) + 2
end
if builder > 30 then
shouldNewLine = true
break
end
end
local newLine, nextNewLine, subIndent = "", ", ", ""
if shouldNewLine then
newLine = "\n"
nextNewLine = ",\n"
subIndent = indent .. " "
end
local result, n = {(tupleLength and "(" or "{") .. newLine}, 1
local seen = {}
local first = true
for k = 1, length do
seen[k] = true
n = n + 1
local entry = subIndent .. serializeImpl(t[k], tracking, subIndent)
if not first then
entry = nextNewLine .. entry
else
first = false
end
result[n] = entry
end
for k,v in pairs(t) do
if not seen[k] then
local entry
if type(k) == "string" and not keywords[k] and string.match( k, "^[%a_][%a%d_]*$" ) then
entry = k .. " = " .. serializeImpl(v, tracking, subIndent)
else
entry = "[" .. serializeImpl(k, tracking, subIndent) .. "] = " .. serializeImpl(v, tracking, subIndent)
end
entry = subIndent .. entry
if not first then
entry = nextNewLine .. entry
else
first = false
end
n = n + 1
result[n] = entry
end
end
n = n + 1
result[n] = newLine .. indent .. (tupleLength and ")" or "}")
return table.concat(result)
end
elseif objType == "string" then
return (string.format("%q", t):gsub("\\\n", "\\n"))
else
return tostring(t)
end
end
local function serialize(t, n)
return serializeImpl(t, {}, "", n)
end
return serialize
end
preload["bsrocks.lib.diffmatchpatch"] = function(...)
--[[
* Diff Match and Patch
*
* Copyright 2006 Google Inc.
* http://code.google.com/p/google-diff-match-patch/
*
* Based on the JavaScript implementation by Neil Fraser.
* Ported to Lua by Duncan Cross.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
--]]
--[[
-- Lua 5.1 and earlier requires the external BitOp library.
-- This library is built-in from Lua 5.2 and later as 'bit32'.
require 'bit' -- <http://bitop.luajit.org/>
local band, bor, lshift
= bit.band, bit.bor, bit.lshift
--]]
local band, bor, lshift = bit32.band, bit32.bor, bit32.lshift
local type, setmetatable, ipairs, select = type, setmetatable, ipairs, select
local unpack, tonumber, error = unpack, tonumber, error
local strsub, strbyte, strchar, gmatch, gsub = string.sub, string.byte, string.char, string.gmatch, string.gsub
local strmatch, strfind, strformat = string.match, string.find, string.format
local tinsert, tremove, tconcat = table.insert, table.remove, table.concat
local max, min, floor, ceil, abs = math.max, math.min, math.floor, math.ceil, math.abs
local clock = os.clock
-- Utility functions.
local percentEncode_pattern = '[^A-Za-z0-9%-=;\',./~!@#$%&*%(%)_%+ %?]'
local function percentEncode_replace(v)
return strformat('%%%02X', strbyte(v))
end
local function tsplice(t, idx, deletions, ...)
local insertions = select('#', ...)
for i = 1, deletions do
tremove(t, idx)
end
for i = insertions, 1, -1 do
-- do not remove parentheses around select
tinsert(t, idx, (select(i, ...)))
end
end
local function strelement(str, i)
return strsub(str, i, i)
end
local function indexOf(a, b, start)
if (#b == 0) then
return nil
end
return strfind(a, b, start, true)
end
local htmlEncode_pattern = '[&<>\n]'
local htmlEncode_replace = {
['&'] = '&amp;', ['<'] = '&lt;', ['>'] = '&gt;', ['\n'] = '&para;<br>'
}
-- Public API Functions
-- (Exported at the end of the script)
local
diff_main,
diff_cleanupSemantic,
diff_cleanupEfficiency,
diff_levenshtein,
diff_prettyHtml
local match_main
local
patch_make,
patch_toText,
patch_fromText,
patch_apply
--[[
* The data structure representing a diff is an array of tuples:
* {{DIFF_DELETE, 'Hello'}, {DIFF_INSERT, 'Goodbye'}, {DIFF_EQUAL, ' world.'}}
* which means: delete 'Hello', add 'Goodbye' and keep ' world.'
--]]
local DIFF_DELETE = -1
local DIFF_INSERT = 1
local DIFF_EQUAL = 0
-- Number of seconds to map a diff before giving up (0 for infinity).
local Diff_Timeout = 1.0
-- Cost of an empty edit operation in terms of edit characters.
local Diff_EditCost = 4
-- At what point is no match declared (0.0 = perfection, 1.0 = very loose).
local Match_Threshold = 0.5
-- How far to search for a match (0 = exact location, 1000+ = broad match).
-- A match this many characters away from the expected location will add
-- 1.0 to the score (0.0 is a perfect match).
local Match_Distance = 1000
-- When deleting a large block of text (over ~64 characters), how close do
-- the contents have to be to match the expected contents. (0.0 = perfection,
-- 1.0 = very loose). Note that Match_Threshold controls how closely the
-- end points of a delete need to match.
local Patch_DeleteThreshold = 0.5
-- Chunk size for context length.
local Patch_Margin = 4
-- The number of bits in an int.
local Match_MaxBits = 32
local function settings(new)
if new then
Diff_Timeout = new.Diff_Timeout or Diff_Timeout
Diff_EditCost = new.Diff_EditCost or Diff_EditCost
Match_Threshold = new.Match_Threshold or Match_Threshold
Match_Distance = new.Match_Distance or Match_Distance
Patch_DeleteThreshold = new.Patch_DeleteThreshold or Patch_DeleteThreshold
Patch_Margin = new.Patch_Margin or Patch_Margin
Match_MaxBits = new.Match_MaxBits or Match_MaxBits
else
return {
Diff_Timeout = Diff_Timeout;
Diff_EditCost = Diff_EditCost;
Match_Threshold = Match_Threshold;
Match_Distance = Match_Distance;
Patch_DeleteThreshold = Patch_DeleteThreshold;
Patch_Margin = Patch_Margin;
Match_MaxBits = Match_MaxBits;
}
end
end
-- ---------------------------------------------------------------------------
-- DIFF API
-- ---------------------------------------------------------------------------
-- The private diff functions
local
_diff_compute,
_diff_bisect,
_diff_bisectSplit,
_diff_halfMatchI,
_diff_halfMatch,
_diff_cleanupSemanticScore,
_diff_cleanupSemanticLossless,
_diff_cleanupMerge,
_diff_commonPrefix,
_diff_commonSuffix,
_diff_commonOverlap,
_diff_xIndex,
_diff_text1,
_diff_text2,
_diff_toDelta,
_diff_fromDelta
--[[
* Find the differences between two texts. Simplifies the problem by stripping
* any common prefix or suffix off the texts before diffing.
* @param {string} text1 Old string to be diffed.
* @param {string} text2 New string to be diffed.
* @param {boolean} opt_checklines Has no effect in Lua.
* @param {number} opt_deadline Optional time when the diff should be complete
* by. Used internally for recursive calls. Users should set DiffTimeout
* instead.
* @return {Array.<Array.<number|string>>} Array of diff tuples.
--]]
function diff_main(text1, text2, opt_checklines, opt_deadline)
-- Set a deadline by which time the diff must be complete.
if opt_deadline == nil then
if Diff_Timeout <= 0 then
opt_deadline = 2 ^ 31
else
opt_deadline = clock() + Diff_Timeout
end
end
local deadline = opt_deadline
-- Check for null inputs.
if text1 == nil or text1 == nil then
error('Null inputs. (diff_main)')
end
-- Check for equality (speedup).
if text1 == text2 then
if #text1 > 0 then
return {{DIFF_EQUAL, text1}}
end
return {}
end
-- LUANOTE: Due to the lack of Unicode support, Lua is incapable of
-- implementing the line-mode speedup.
local checklines = false
-- Trim off common prefix (speedup).
local commonlength = _diff_commonPrefix(text1, text2)
local commonprefix
if commonlength > 0 then
commonprefix = strsub(text1, 1, commonlength)
text1 = strsub(text1, commonlength + 1)
text2 = strsub(text2, commonlength + 1)
end
-- Trim off common suffix (speedup).
commonlength = _diff_commonSuffix(text1, text2)
local commonsuffix
if commonlength > 0 then
commonsuffix = strsub(text1, -commonlength)
text1 = strsub(text1, 1, -commonlength - 1)
text2 = strsub(text2, 1, -commonlength - 1)
end
-- Compute the diff on the middle block.
local diffs = _diff_compute(text1, text2, checklines, deadline)
-- Restore the prefix and suffix.
if commonprefix then
tinsert(diffs, 1, {DIFF_EQUAL, commonprefix})
end
if commonsuffix then
diffs[#diffs + 1] = {DIFF_EQUAL, commonsuffix}
end
_diff_cleanupMerge(diffs)
return diffs
end
--[[
* Reduce the number of edits by eliminating semantically trivial equalities.
* @param {Array.<Array.<number|string>>} diffs Array of diff tuples.
--]]
function diff_cleanupSemantic(diffs)
local changes = false
local equalities = {} -- Stack of indices where equalities are found.
local equalitiesLength = 0 -- Keeping our own length var is faster.
local lastequality = nil
-- Always equal to diffs[equalities[equalitiesLength]][2]
local pointer = 1 -- Index of current position.
-- Number of characters that changed prior to the equality.
local length_insertions1 = 0
local length_deletions1 = 0
-- Number of characters that changed after the equality.
local length_insertions2 = 0
local length_deletions2 = 0
while diffs[pointer] do
if diffs[pointer][1] == DIFF_EQUAL then -- Equality found.
equalitiesLength = equalitiesLength + 1
equalities[equalitiesLength] = pointer
length_insertions1 = length_insertions2
length_deletions1 = length_deletions2
length_insertions2 = 0
length_deletions2 = 0
lastequality = diffs[pointer][2]
else -- An insertion or deletion.
if diffs[pointer][1] == DIFF_INSERT then
length_insertions2 = length_insertions2 + #(diffs[pointer][2])
else
length_deletions2 = length_deletions2 + #(diffs[pointer][2])
end
-- Eliminate an equality that is smaller or equal to the edits on both
-- sides of it.
if lastequality
and (#lastequality <= max(length_insertions1, length_deletions1))
and (#lastequality <= max(length_insertions2, length_deletions2)) then
-- Duplicate record.
tinsert(diffs, equalities[equalitiesLength],
{DIFF_DELETE, lastequality})
-- Change second copy to insert.
diffs[equalities[equalitiesLength] + 1][1] = DIFF_INSERT
-- Throw away the equality we just deleted.
equalitiesLength = equalitiesLength - 1
-- Throw away the previous equality (it needs to be reevaluated).
equalitiesLength = equalitiesLength - 1
pointer = (equalitiesLength > 0) and equalities[equalitiesLength] or 0
length_insertions1, length_deletions1 = 0, 0 -- Reset the counters.
length_insertions2, length_deletions2 = 0, 0
lastequality = nil
changes = true
end
end
pointer = pointer + 1
end
-- Normalize the diff.
if changes then
_diff_cleanupMerge(diffs)
end
_diff_cleanupSemanticLossless(diffs)
-- Find any overlaps between deletions and insertions.
-- e.g: <del>abcxxx</del><ins>xxxdef</ins>
-- -> <del>abc</del>xxx<ins>def</ins>
-- e.g: <del>xxxabc</del><ins>defxxx</ins>
-- -> <ins>def</ins>xxx<del>abc</del>
-- Only extract an overlap if it is as big as the edit ahead or behind it.
pointer = 2
while diffs[pointer] do
if (diffs[pointer - 1][1] == DIFF_DELETE and
diffs[pointer][1] == DIFF_INSERT) then
local deletion = diffs[pointer - 1][2]
local insertion = diffs[pointer][2]
local overlap_length1 = _diff_commonOverlap(deletion, insertion)
local overlap_length2 = _diff_commonOverlap(insertion, deletion)
if (overlap_length1 >= overlap_length2) then
if (overlap_length1 >= #deletion / 2 or
overlap_length1 >= #insertion / 2) then
-- Overlap found. Insert an equality and trim the surrounding edits.
tinsert(diffs, pointer,
{DIFF_EQUAL, strsub(insertion, 1, overlap_length1)})
diffs[pointer - 1][2] =
strsub(deletion, 1, #deletion - overlap_length1)
diffs[pointer + 1][2] = strsub(insertion, overlap_length1 + 1)
pointer = pointer + 1
end
else
if (overlap_length2 >= #deletion / 2 or
overlap_length2 >= #insertion / 2) then
-- Reverse overlap found.
-- Insert an equality and swap and trim the surrounding edits.
tinsert(diffs, pointer,
{DIFF_EQUAL, strsub(deletion, 1, overlap_length2)})
diffs[pointer - 1] = {DIFF_INSERT,
strsub(insertion, 1, #insertion - overlap_length2)}
diffs[pointer + 1] = {DIFF_DELETE,
strsub(deletion, overlap_length2 + 1)}
pointer = pointer + 1
end
end
pointer = pointer + 1
end
pointer = pointer + 1
end
end
--[[
* Reduce the number of edits by eliminating operationally trivial equalities.
* @param {Array.<Array.<number|string>>} diffs Array of diff tuples.
--]]
function diff_cleanupEfficiency(diffs)
local changes = false
-- Stack of indices where equalities are found.
local equalities = {}
-- Keeping our own length var is faster.
local equalitiesLength = 0
-- Always equal to diffs[equalities[equalitiesLength]][2]
local lastequality = nil
-- Index of current position.
local pointer = 1
-- The following four are really booleans but are stored as numbers because
-- they are used at one point like this:
--
-- (pre_ins + pre_del + post_ins + post_del) == 3
--
-- ...i.e. checking that 3 of them are true and 1 of them is false.
-- Is there an insertion operation before the last equality.
local pre_ins = 0
-- Is there a deletion operation before the last equality.
local pre_del = 0
-- Is there an insertion operation after the last equality.
local post_ins = 0
-- Is there a deletion operation after the last equality.
local post_del = 0
while diffs[pointer] do
if diffs[pointer][1] == DIFF_EQUAL then -- Equality found.
local diffText = diffs[pointer][2]
if (#diffText < Diff_EditCost) and (post_ins == 1 or post_del == 1) then
-- Candidate found.
equalitiesLength = equalitiesLength + 1
equalities[equalitiesLength] = pointer
pre_ins, pre_del = post_ins, post_del
lastequality = diffText
else
-- Not a candidate, and can never become one.
equalitiesLength = 0
lastequality = nil
end
post_ins, post_del = 0, 0
else -- An insertion or deletion.
if diffs[pointer][1] == DIFF_DELETE then
post_del = 1
else
post_ins = 1
end
--[[
* Five types to be split:
* <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del>
* <ins>A</ins>X<ins>C</ins><del>D</del>
* <ins>A</ins><del>B</del>X<ins>C</ins>
* <ins>A</del>X<ins>C</ins><del>D</del>
* <ins>A</ins><del>B</del>X<del>C</del>
--]]
if lastequality and (
(pre_ins+pre_del+post_ins+post_del == 4)
or
(
(#lastequality < Diff_EditCost / 2)
and
(pre_ins+pre_del+post_ins+post_del == 3)
)) then
-- Duplicate record.
tinsert(diffs, equalities[equalitiesLength],
{DIFF_DELETE, lastequality})
-- Change second copy to insert.
diffs[equalities[equalitiesLength] + 1][1] = DIFF_INSERT
-- Throw away the equality we just deleted.
equalitiesLength = equalitiesLength - 1
lastequality = nil
if (pre_ins == 1) and (pre_del == 1) then
-- No changes made which could affect previous entry, keep going.
post_ins, post_del = 1, 1
equalitiesLength = 0
else
-- Throw away the previous equality.
equalitiesLength = equalitiesLength - 1
pointer = (equalitiesLength > 0) and equalities[equalitiesLength] or 0
post_ins, post_del = 0, 0
end
changes = true
end
end
pointer = pointer + 1
end
if changes then
_diff_cleanupMerge(diffs)
end
end
--[[
* Compute the Levenshtein distance; the number of inserted, deleted or
* substituted characters.
* @param {Array.<Array.<number|string>>} diffs Array of diff tuples.
* @return {number} Number of changes.
--]]
function diff_levenshtein(diffs)
local levenshtein = 0
local insertions, deletions = 0, 0
for x, diff in ipairs(diffs) do
local op, data = diff[1], diff[2]
if (op == DIFF_INSERT) then
insertions = insertions + #data
elseif (op == DIFF_DELETE) then
deletions = deletions + #data
elseif (op == DIFF_EQUAL) then
-- A deletion and an insertion is one substitution.
levenshtein = levenshtein + max(insertions, deletions)
insertions = 0
deletions = 0
end
end
levenshtein = levenshtein + max(insertions, deletions)
return levenshtein
end
--[[
* Convert a diff array into a pretty HTML report.
* @param {Array.<Array.<number|string>>} diffs Array of diff tuples.
* @return {string} HTML representation.
--]]
function diff_prettyHtml(diffs)
local html = {}
for x, diff in ipairs(diffs) do
local op = diff[1] -- Operation (insert, delete, equal)
local data = diff[2] -- Text of change.
local text = gsub(data, htmlEncode_pattern, htmlEncode_replace)
if op == DIFF_INSERT then
html[x] = '<ins style="background:#e6ffe6;">' .. text .. '</ins>'
elseif op == DIFF_DELETE then
html[x] = '<del style="background:#ffe6e6;">' .. text .. '</del>'
elseif op == DIFF_EQUAL then
html[x] = '<span>' .. text .. '</span>'
end
end
return tconcat(html)
end
-- ---------------------------------------------------------------------------
-- UNOFFICIAL/PRIVATE DIFF FUNCTIONS
-- ---------------------------------------------------------------------------
--[[
* Find the differences between two texts. Assumes that the texts do not
* have any common prefix or suffix.
* @param {string} text1 Old string to be diffed.
* @param {string} text2 New string to be diffed.
* @param {boolean} checklines Has no effect in Lua.
* @param {number} deadline Time when the diff should be complete by.
* @return {Array.<Array.<number|string>>} Array of diff tuples.
* @private
--]]
function _diff_compute(text1, text2, checklines, deadline)
if #text1 == 0 then
-- Just add some text (speedup).
return {{DIFF_INSERT, text2}}
end
if #text2 == 0 then
-- Just delete some text (speedup).
return {{DIFF_DELETE, text1}}
end
local diffs
local longtext = (#text1 > #text2) and text1 or text2
local shorttext = (#text1 > #text2) and text2 or text1
local i = indexOf(longtext, shorttext)
if i ~= nil then
-- Shorter text is inside the longer text (speedup).
diffs = {
{DIFF_INSERT, strsub(longtext, 1, i - 1)},
{DIFF_EQUAL, shorttext},
{DIFF_INSERT, strsub(longtext, i + #shorttext)}
}
-- Swap insertions for deletions if diff is reversed.
if #text1 > #text2 then
diffs[1][1], diffs[3][1] = DIFF_DELETE, DIFF_DELETE
end
return diffs
end
if #shorttext == 1 then
-- Single character string.
-- After the previous speedup, the character can't be an equality.
return {{DIFF_DELETE, text1}, {DIFF_INSERT, text2}}
end
-- Check to see if the problem can be split in two.
do
local
text1_a, text1_b,
text2_a, text2_b,
mid_common = _diff_halfMatch(text1, text2)
if text1_a then
-- A half-match was found, sort out the return data.
-- Send both pairs off for separate processing.
local diffs_a = diff_main(text1_a, text2_a, checklines, deadline)
local diffs_b = diff_main(text1_b, text2_b, checklines, deadline)
-- Merge the results.
local diffs_a_len = #diffs_a
diffs = diffs_a
diffs[diffs_a_len + 1] = {DIFF_EQUAL, mid_common}
for i, b_diff in ipairs(diffs_b) do
diffs[diffs_a_len + 1 + i] = b_diff
end
return diffs
end
end
return _diff_bisect(text1, text2, deadline)
end
--[[
* Find the 'middle snake' of a diff, split the problem in two
* and return the recursively constructed diff.
* See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.
* @param {string} text1 Old string to be diffed.
* @param {string} text2 New string to be diffed.
* @param {number} deadline Time at which to bail if not yet complete.
* @return {Array.<Array.<number|string>>} Array of diff tuples.
* @private
--]]
function _diff_bisect(text1, text2, deadline)
-- Cache the text lengths to prevent multiple calls.
local text1_length = #text1
local text2_length = #text2
local _sub, _element
local max_d = ceil((text1_length + text2_length) / 2)
local v_offset = max_d
local v_length = 2 * max_d
local v1 = {}
local v2 = {}
-- Setting all elements to -1 is faster in Lua than mixing integers and nil.
for x = 0, v_length - 1 do
v1[x] = -1
v2[x] = -1
end
v1[v_offset + 1] = 0
v2[v_offset + 1] = 0
local delta = text1_length - text2_length
-- If the total number of characters is odd, then
-- the front path will collide with the reverse path.
local front = (delta % 2 ~= 0)
-- Offsets for start and end of k loop.
-- Prevents mapping of space beyond the grid.
local k1start = 0
local k1end = 0
local k2start = 0
local k2end = 0
for d = 0, max_d - 1 do
-- Bail out if deadline is reached.
if clock() > deadline then
break
end
-- Walk the front path one step.
for k1 = -d + k1start, d - k1end, 2 do
local k1_offset = v_offset + k1
local x1
if (k1 == -d) or ((k1 ~= d) and
(v1[k1_offset - 1] < v1[k1_offset + 1])) then
x1 = v1[k1_offset + 1]
else
x1 = v1[k1_offset - 1] + 1
end
local y1 = x1 - k1
while (x1 <= text1_length) and (y1 <= text2_length)
and (strelement(text1, x1) == strelement(text2, y1)) do
x1 = x1 + 1
y1 = y1 + 1
end
v1[k1_offset] = x1
if x1 > text1_length + 1 then
-- Ran off the right of the graph.
k1end = k1end + 2
elseif y1 > text2_length + 1 then
-- Ran off the bottom of the graph.
k1start = k1start + 2
elseif front then
local k2_offset = v_offset + delta - k1
if k2_offset >= 0 and k2_offset < v_length and v2[k2_offset] ~= -1 then
-- Mirror x2 onto top-left coordinate system.
local x2 = text1_length - v2[k2_offset] + 1
if x1 > x2 then
-- Overlap detected.
return _diff_bisectSplit(text1, text2, x1, y1, deadline)
end
end
end
end
-- Walk the reverse path one step.
for k2 = -d + k2start, d - k2end, 2 do
local k2_offset = v_offset + k2
local x2
if (k2 == -d) or ((k2 ~= d) and
(v2[k2_offset - 1] < v2[k2_offset + 1])) then
x2 = v2[k2_offset + 1]
else
x2 = v2[k2_offset - 1] + 1
end
local y2 = x2 - k2
while (x2 <= text1_length) and (y2 <= text2_length)
and (strelement(text1, -x2) == strelement(text2, -y2)) do
x2 = x2 + 1
y2 = y2 + 1
end
v2[k2_offset] = x2
if x2 > text1_length + 1 then
-- Ran off the left of the graph.
k2end = k2end + 2
elseif y2 > text2_length + 1 then
-- Ran off the top of the graph.
k2start = k2start + 2
elseif not front then
local k1_offset = v_offset + delta - k2
if k1_offset >= 0 and k1_offset < v_length and v1[k1_offset] ~= -1 then
local x1 = v1[k1_offset]
local y1 = v_offset + x1 - k1_offset
-- Mirror x2 onto top-left coordinate system.
x2 = text1_length - x2 + 1
if x1 > x2 then
-- Overlap detected.
return _diff_bisectSplit(text1, text2, x1, y1, deadline)
end
end
end
end
end
-- Diff took too long and hit the deadline or
-- number of diffs equals number of characters, no commonality at all.
return {{DIFF_DELETE, text1}, {DIFF_INSERT, text2}}
end
--[[
* Given the location of the 'middle snake', split the diff in two parts
* and recurse.
* @param {string} text1 Old string to be diffed.
* @param {string} text2 New string to be diffed.
* @param {number} x Index of split point in text1.
* @param {number} y Index of split point in text2.
* @param {number} deadline Time at which to bail if not yet complete.
* @return {Array.<Array.<number|string>>} Array of diff tuples.
* @private
--]]
function _diff_bisectSplit(text1, text2, x, y, deadline)
local text1a = strsub(text1, 1, x - 1)
local text2a = strsub(text2, 1, y - 1)
local text1b = strsub(text1, x)
local text2b = strsub(text2, y)
-- Compute both diffs serially.
local diffs = diff_main(text1a, text2a, false, deadline)
local diffsb = diff_main(text1b, text2b, false, deadline)
local diffs_len = #diffs
for i, v in ipairs(diffsb) do
diffs[diffs_len + i] = v
end
return diffs
end
--[[
* Determine the common prefix of two strings.
* @param {string} text1 First string.
* @param {string} text2 Second string.
* @return {number} The number of characters common to the start of each
* string.
--]]
function _diff_commonPrefix(text1, text2)
-- Quick check for common null cases.
if (#text1 == 0) or (#text2 == 0) or (strbyte(text1, 1) ~= strbyte(text2, 1))
then
return 0
end
-- Binary search.
-- Performance analysis: http://neil.fraser.name/news/2007/10/09/
local pointermin = 1
local pointermax = min(#text1, #text2)
local pointermid = pointermax
local pointerstart = 1
while (pointermin < pointermid) do
if (strsub(text1, pointerstart, pointermid)
== strsub(text2, pointerstart, pointermid)) then
pointermin = pointermid
pointerstart = pointermin
else
pointermax = pointermid
end
pointermid = floor(pointermin + (pointermax - pointermin) / 2)
end
return pointermid
end
--[[
* Determine the common suffix of two strings.
* @param {string} text1 First string.
* @param {string} text2 Second string.
* @return {number} The number of characters common to the end of each string.
--]]
function _diff_commonSuffix(text1, text2)
-- Quick check for common null cases.
if (#text1 == 0) or (#text2 == 0)
or (strbyte(text1, -1) ~= strbyte(text2, -1)) then
return 0
end
-- Binary search.
-- Performance analysis: http://neil.fraser.name/news/2007/10/09/
local pointermin = 1
local pointermax = min(#text1, #text2)
local pointermid = pointermax
local pointerend = 1
while (pointermin < pointermid) do
if (strsub(text1, -pointermid, -pointerend)
== strsub(text2, -pointermid, -pointerend)) then
pointermin = pointermid
pointerend = pointermin
else
pointermax = pointermid
end
pointermid = floor(pointermin + (pointermax - pointermin) / 2)
end
return pointermid
end
--[[
* Determine if the suffix of one string is the prefix of another.
* @param {string} text1 First string.
* @param {string} text2 Second string.
* @return {number} The number of characters common to the end of the first
* string and the start of the second string.
* @private
--]]
function _diff_commonOverlap(text1, text2)
-- Cache the text lengths to prevent multiple calls.
local text1_length = #text1
local text2_length = #text2
-- Eliminate the null case.
if text1_length == 0 or text2_length == 0 then
return 0
end
-- Truncate the longer string.
if text1_length > text2_length then
text1 = strsub(text1, text1_length - text2_length + 1)
elseif text1_length < text2_length then
text2 = strsub(text2, 1, text1_length)
end
local text_length = min(text1_length, text2_length)
-- Quick check for the worst case.
if text1 == text2 then
return text_length
end
-- Start by looking for a single character match
-- and increase length until no match is found.
-- Performance analysis: http://neil.fraser.name/news/2010/11/04/
local best = 0
local length = 1
while true do
local pattern = strsub(text1, text_length - length + 1)
local found = strfind(text2, pattern, 1, true)
if found == nil then
return best
end
length = length + found - 1
if found == 1 or strsub(text1, text_length - length + 1) ==
strsub(text2, 1, length) then
best = length
length = length + 1
end
end
end
--[[
* Does a substring of shorttext exist within longtext such that the substring
* is at least half the length of longtext?
* This speedup can produce non-minimal diffs.
* Closure, but does not reference any external variables.
* @param {string} longtext Longer string.
* @param {string} shorttext Shorter string.
* @param {number} i Start index of quarter length substring within longtext.
* @return {?Array.<string>} Five element Array, containing the prefix of
* longtext, the suffix of longtext, the prefix of shorttext, the suffix
* of shorttext and the common middle. Or nil if there was no match.
* @private
--]]
function _diff_halfMatchI(longtext, shorttext, i)
-- Start with a 1/4 length substring at position i as a seed.
local seed = strsub(longtext, i, i + floor(#longtext / 4))
local j = 0 -- LUANOTE: do not change to 1, was originally -1
local best_common = ''
local best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b
while true do
j = indexOf(shorttext, seed, j + 1)
if (j == nil) then
break
end
local prefixLength = _diff_commonPrefix(strsub(longtext, i),
strsub(shorttext, j))
local suffixLength = _diff_commonSuffix(strsub(longtext, 1, i - 1),
strsub(shorttext, 1, j - 1))
if #best_common < suffixLength + prefixLength then
best_common = strsub(shorttext, j - suffixLength, j - 1)
.. strsub(shorttext, j, j + prefixLength - 1)
best_longtext_a = strsub(longtext, 1, i - suffixLength - 1)
best_longtext_b = strsub(longtext, i + prefixLength)
best_shorttext_a = strsub(shorttext, 1, j - suffixLength - 1)
best_shorttext_b = strsub(shorttext, j + prefixLength)
end
end
if #best_common * 2 >= #longtext then
return {best_longtext_a, best_longtext_b,
best_shorttext_a, best_shorttext_b, best_common}
else
return nil
end
end
--[[
* Do the two texts share a substring which is at least half the length of the
* longer text?
* @param {string} text1 First string.
* @param {string} text2 Second string.
* @return {?Array.<string>} Five element Array, containing the prefix of
* text1, the suffix of text1, the prefix of text2, the suffix of
* text2 and the common middle. Or nil if there was no match.
* @private
--]]
function _diff_halfMatch(text1, text2)
if Diff_Timeout <= 0 then
-- Don't risk returning a non-optimal diff if we have unlimited time.
return nil
end
local longtext = (#text1 > #text2) and text1 or text2
local shorttext = (#text1 > #text2) and text2 or text1
if (#longtext < 4) or (#shorttext * 2 < #longtext) then
return nil -- Pointless.
end
-- First check if the second quarter is the seed for a half-match.
local hm1 = _diff_halfMatchI(longtext, shorttext, ceil(#longtext / 4))
-- Check again based on the third quarter.
local hm2 = _diff_halfMatchI(longtext, shorttext, ceil(#longtext / 2))
local hm
if not hm1 and not hm2 then
return nil
elseif not hm2 then
hm = hm1
elseif not hm1 then
hm = hm2
else
-- Both matched. Select the longest.
hm = (#hm1[5] > #hm2[5]) and hm1 or hm2
end
-- A half-match was found, sort out the return data.
local text1_a, text1_b, text2_a, text2_b
if (#text1 > #text2) then
text1_a, text1_b = hm[1], hm[2]
text2_a, text2_b = hm[3], hm[4]
else
text2_a, text2_b = hm[1], hm[2]
text1_a, text1_b = hm[3], hm[4]
end
local mid_common = hm[5]
return text1_a, text1_b, text2_a, text2_b, mid_common
end
--[[
* Given two strings, compute a score representing whether the internal
* boundary falls on logical boundaries.
* Scores range from 6 (best) to 0 (worst).
* @param {string} one First string.
* @param {string} two Second string.
* @return {number} The score.
* @private
--]]
function _diff_cleanupSemanticScore(one, two)
if (#one == 0) or (#two == 0) then
-- Edges are the best.
return 6
end
-- Each port of this function behaves slightly differently due to
-- subtle differences in each language's definition of things like
-- 'whitespace'. Since this function's purpose is largely cosmetic,
-- the choice has been made to use each language's native features
-- rather than force total conformity.
local char1 = strsub(one, -1)
local char2 = strsub(two, 1, 1)
local nonAlphaNumeric1 = strmatch(char1, '%W')
local nonAlphaNumeric2 = strmatch(char2, '%W')
local whitespace1 = nonAlphaNumeric1 and strmatch(char1, '%s')
local whitespace2 = nonAlphaNumeric2 and strmatch(char2, '%s')
local lineBreak1 = whitespace1 and strmatch(char1, '%c')
local lineBreak2 = whitespace2 and strmatch(char2, '%c')
local blankLine1 = lineBreak1 and strmatch(one, '\n\r?\n$')
local blankLine2 = lineBreak2 and strmatch(two, '^\r?\n\r?\n')
if blankLine1 or blankLine2 then
-- Five points for blank lines.
return 5
elseif lineBreak1 or lineBreak2 then
-- Four points for line breaks.
return 4
elseif nonAlphaNumeric1 and not whitespace1 and whitespace2 then
-- Three points for end of sentences.
return 3
elseif whitespace1 or whitespace2 then
-- Two points for whitespace.
return 2
elseif nonAlphaNumeric1 or nonAlphaNumeric2 then
-- One point for non-alphanumeric.
return 1
end
return 0
end
--[[
* Look for single edits surrounded on both sides by equalities
* which can be shifted sideways to align the edit to a word boundary.
* e.g: The c<ins>at c</ins>ame. -> The <ins>cat </ins>came.
* @param {Array.<Array.<number|string>>} diffs Array of diff tuples.
--]]
function _diff_cleanupSemanticLossless(diffs)
local pointer = 2
-- Intentionally ignore the first and last element (don't need checking).
while diffs[pointer + 1] do
local prevDiff, nextDiff = diffs[pointer - 1], diffs[pointer + 1]
if (prevDiff[1] == DIFF_EQUAL) and (nextDiff[1] == DIFF_EQUAL) then
-- This is a single edit surrounded by equalities.
local diff = diffs[pointer]
local equality1 = prevDiff[2]
local edit = diff[2]
local equality2 = nextDiff[2]
-- First, shift the edit as far left as possible.
local commonOffset = _diff_commonSuffix(equality1, edit)
if commonOffset > 0 then
local commonString = strsub(edit, -commonOffset)
equality1 = strsub(equality1, 1, -commonOffset - 1)
edit = commonString .. strsub(edit, 1, -commonOffset - 1)
equality2 = commonString .. equality2
end
-- Second, step character by character right, looking for the best fit.
local bestEquality1 = equality1
local bestEdit = edit
local bestEquality2 = equality2
local bestScore = _diff_cleanupSemanticScore(equality1, edit)
+ _diff_cleanupSemanticScore(edit, equality2)
while strbyte(edit, 1) == strbyte(equality2, 1) do
equality1 = equality1 .. strsub(edit, 1, 1)
edit = strsub(edit, 2) .. strsub(equality2, 1, 1)
equality2 = strsub(equality2, 2)
local score = _diff_cleanupSemanticScore(equality1, edit)
+ _diff_cleanupSemanticScore(edit, equality2)
-- The >= encourages trailing rather than leading whitespace on edits.
if score >= bestScore then
bestScore = score
bestEquality1 = equality1
bestEdit = edit
bestEquality2 = equality2
end
end
if prevDiff[2] ~= bestEquality1 then
-- We have an improvement, save it back to the diff.
if #bestEquality1 > 0 then
diffs[pointer - 1][2] = bestEquality1
else
tremove(diffs, pointer - 1)
pointer = pointer - 1
end
diffs[pointer][2] = bestEdit
if #bestEquality2 > 0 then
diffs[pointer + 1][2] = bestEquality2
else
tremove(diffs, pointer + 1, 1)
pointer = pointer - 1
end
end
end
pointer = pointer + 1
end
end
--[[
* Reorder and merge like edit sections. Merge equalities.
* Any edit section can move as long as it doesn't cross an equality.
* @param {Array.<Array.<number|string>>} diffs Array of diff tuples.
--]]
function _diff_cleanupMerge(diffs)
diffs[#diffs + 1] = {DIFF_EQUAL, ''} -- Add a dummy entry at the end.
local pointer = 1
local count_delete, count_insert = 0, 0
local text_delete, text_insert = '', ''
local commonlength
while diffs[pointer] do
local diff_type = diffs[pointer][1]
if diff_type == DIFF_INSERT then
count_insert = count_insert + 1
text_insert = text_insert .. diffs[pointer][2]
pointer = pointer + 1
elseif diff_type == DIFF_DELETE then
count_delete = count_delete + 1
text_delete = text_delete .. diffs[pointer][2]
pointer = pointer + 1
elseif diff_type == DIFF_EQUAL then
-- Upon reaching an equality, check for prior redundancies.
if count_delete + count_insert > 1 then
if (count_delete > 0) and (count_insert > 0) then
-- Factor out any common prefixies.
commonlength = _diff_commonPrefix(text_insert, text_delete)
if commonlength > 0 then
local back_pointer = pointer - count_delete - count_insert
if (back_pointer > 1) and (diffs[back_pointer - 1][1] == DIFF_EQUAL)
then
diffs[back_pointer - 1][2] = diffs[back_pointer - 1][2]
.. strsub(text_insert, 1, commonlength)
else
tinsert(diffs, 1,
{DIFF_EQUAL, strsub(text_insert, 1, commonlength)})
pointer = pointer + 1
end
text_insert = strsub(text_insert, commonlength + 1)
text_delete = strsub(text_delete, commonlength + 1)
end
-- Factor out any common suffixies.
commonlength = _diff_commonSuffix(text_insert, text_delete)
if commonlength ~= 0 then
diffs[pointer][2] =
strsub(text_insert, -commonlength) .. diffs[pointer][2]
text_insert = strsub(text_insert, 1, -commonlength - 1)
text_delete = strsub(text_delete, 1, -commonlength - 1)
end
end
-- Delete the offending records and add the merged ones.
if count_delete == 0 then
tsplice(diffs, pointer - count_insert,
count_insert, {DIFF_INSERT, text_insert})
elseif count_insert == 0 then
tsplice(diffs, pointer - count_delete,
count_delete, {DIFF_DELETE, text_delete})
else
tsplice(diffs, pointer - count_delete - count_insert,
count_delete + count_insert,
{DIFF_DELETE, text_delete}, {DIFF_INSERT, text_insert})
end
pointer = pointer - count_delete - count_insert
+ (count_delete>0 and 1 or 0) + (count_insert>0 and 1 or 0) + 1
elseif (pointer > 1) and (diffs[pointer - 1][1] == DIFF_EQUAL) then
-- Merge this equality with the previous one.
diffs[pointer - 1][2] = diffs[pointer - 1][2] .. diffs[pointer][2]
tremove(diffs, pointer)
else
pointer = pointer + 1
end
count_insert, count_delete = 0, 0
text_delete, text_insert = '', ''
end
end
if diffs[#diffs][2] == '' then
diffs[#diffs] = nil -- Remove the dummy entry at the end.
end
-- Second pass: look for single edits surrounded on both sides by equalities
-- which can be shifted sideways to eliminate an equality.
-- e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC
local changes = false
pointer = 2
-- Intentionally ignore the first and last element (don't need checking).
while pointer < #diffs do
local prevDiff, nextDiff = diffs[pointer - 1], diffs[pointer + 1]
if (prevDiff[1] == DIFF_EQUAL) and (nextDiff[1] == DIFF_EQUAL) then
-- This is a single edit surrounded by equalities.
local diff = diffs[pointer]
local currentText = diff[2]
local prevText = prevDiff[2]
local nextText = nextDiff[2]
if strsub(currentText, -#prevText) == prevText then
-- Shift the edit over the previous equality.
diff[2] = prevText .. strsub(currentText, 1, -#prevText - 1)
nextDiff[2] = prevText .. nextDiff[2]
tremove(diffs, pointer - 1)
changes = true
elseif strsub(currentText, 1, #nextText) == nextText then
-- Shift the edit over the next equality.
prevDiff[2] = prevText .. nextText
diff[2] = strsub(currentText, #nextText + 1) .. nextText
tremove(diffs, pointer + 1)
changes = true
end
end
pointer = pointer + 1
end
-- If shifts were made, the diff needs reordering and another shift sweep.
if changes then
-- LUANOTE: no return value, but necessary to use 'return' to get
-- tail calls.
return _diff_cleanupMerge(diffs)
end
end
--[[
* loc is a location in text1, compute and return the equivalent location in
* text2.
* e.g. 'The cat' vs 'The big cat', 1->1, 5->8
* @param {Array.<Array.<number|string>>} diffs Array of diff tuples.
* @param {number} loc Location within text1.
* @return {number} Location within text2.
--]]
function _diff_xIndex(diffs, loc)
local chars1 = 1
local chars2 = 1
local last_chars1 = 1
local last_chars2 = 1
local x
for _x, diff in ipairs(diffs) do
x = _x
if diff[1] ~= DIFF_INSERT then -- Equality or deletion.
chars1 = chars1 + #diff[2]
end
if diff[1] ~= DIFF_DELETE then -- Equality or insertion.
chars2 = chars2 + #diff[2]
end
if chars1 > loc then -- Overshot the location.
break
end
last_chars1 = chars1
last_chars2 = chars2
end
-- Was the location deleted?
if diffs[x + 1] and (diffs[x][1] == DIFF_DELETE) then
return last_chars2
end
-- Add the remaining character length.
return last_chars2 + (loc - last_chars1)
end
--[[
* Compute and return the source text (all equalities and deletions).
* @param {Array.<Array.<number|string>>} diffs Array of diff tuples.
* @return {string} Source text.
--]]
function _diff_text1(diffs)
local text = {}
for x, diff in ipairs(diffs) do
if diff[1] ~= DIFF_INSERT then
text[#text + 1] = diff[2]
end
end
return tconcat(text)
end
--[[
* Compute and return the destination text (all equalities and insertions).
* @param {Array.<Array.<number|string>>} diffs Array of diff tuples.
* @return {string} Destination text.
--]]
function _diff_text2(diffs)
local text = {}
for x, diff in ipairs(diffs) do
if diff[1] ~= DIFF_DELETE then
text[#text + 1] = diff[2]
end
end
return tconcat(text)
end
--[[
* Crush the diff into an encoded string which describes the operations
* required to transform text1 into text2.
* E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'.
* Operations are tab-separated. Inserted text is escaped using %xx notation.
* @param {Array.<Array.<number|string>>} diffs Array of diff tuples.
* @return {string} Delta text.
--]]
function _diff_toDelta(diffs)
local text = {}
for x, diff in ipairs(diffs) do
local op, data = diff[1], diff[2]
if op == DIFF_INSERT then
text[x] = '+' .. gsub(data, percentEncode_pattern, percentEncode_replace)
elseif op == DIFF_DELETE then
text[x] = '-' .. #data
elseif op == DIFF_EQUAL then
text[x] = '=' .. #data
end
end
return tconcat(text, '\t')
end
--[[
* Given the original text1, and an encoded string which describes the
* operations required to transform text1 into text2, compute the full diff.
* @param {string} text1 Source string for the diff.
* @param {string} delta Delta text.
* @return {Array.<Array.<number|string>>} Array of diff tuples.
* @throws {Errorend If invalid input.
--]]
function _diff_fromDelta(text1, delta)
local diffs = {}
local diffsLength = 0 -- Keeping our own length var is faster
local pointer = 1 -- Cursor in text1
for token in gmatch(delta, '[^\t]+') do
-- Each token begins with a one character parameter which specifies the
-- operation of this token (delete, insert, equality).
local tokenchar, param = strsub(token, 1, 1), strsub(token, 2)
if (tokenchar == '+') then
local invalidDecode = false
local decoded = gsub(param, '%%(.?.?)',
function(c)
local n = tonumber(c, 16)
if (#c ~= 2) or (n == nil) then
invalidDecode = true
return ''
end
return strchar(n)
end)
if invalidDecode then
-- Malformed URI sequence.
error('Illegal escape in _diff_fromDelta: ' .. param)
end
diffsLength = diffsLength + 1
diffs[diffsLength] = {DIFF_INSERT, decoded}
elseif (tokenchar == '-') or (tokenchar == '=') then
local n = tonumber(param)
if (n == nil) or (n < 0) then
error('Invalid number in _diff_fromDelta: ' .. param)
end
local text = strsub(text1, pointer, pointer + n - 1)
pointer = pointer + n
if (tokenchar == '=') then
diffsLength = diffsLength + 1
diffs[diffsLength] = {DIFF_EQUAL, text}
else
diffsLength = diffsLength + 1
diffs[diffsLength] = {DIFF_DELETE, text}
end
else
error('Invalid diff operation in _diff_fromDelta: ' .. token)
end
end
if (pointer ~= #text1 + 1) then
error('Delta length (' .. (pointer - 1)
.. ') does not equal source text length (' .. #text1 .. ').')
end
return diffs
end
-- ---------------------------------------------------------------------------
-- MATCH API
-- ---------------------------------------------------------------------------
local _match_bitap, _match_alphabet
--[[
* Locate the best instance of 'pattern' in 'text' near 'loc'.
* @param {string} text The text to search.
* @param {string} pattern The pattern to search for.
* @param {number} loc The location to search around.
* @return {number} Best match index or -1.
--]]
function match_main(text, pattern, loc)
-- Check for null inputs.
if text == nil or pattern == nil then
error('Null inputs. (match_main)')
end
if text == pattern then
-- Shortcut (potentially not guaranteed by the algorithm)
return 1
elseif #text == 0 then
-- Nothing to match.
return -1
end
loc = max(1, min(loc or 0, #text))
if strsub(text, loc, loc + #pattern - 1) == pattern then
-- Perfect match at the perfect spot! (Includes case of null pattern)
return loc
else
-- Do a fuzzy compare.
return _match_bitap(text, pattern, loc)
end
end
-- ---------------------------------------------------------------------------
-- UNOFFICIAL/PRIVATE MATCH FUNCTIONS
-- ---------------------------------------------------------------------------
--[[
* Initialise the alphabet for the Bitap algorithm.
* @param {string} pattern The text to encode.
* @return {Object} Hash of character locations.
* @private
--]]
function _match_alphabet(pattern)
local s = {}
local i = 0
for c in gmatch(pattern, '.') do
s[c] = bor(s[c] or 0, lshift(1, #pattern - i - 1))
i = i + 1
end
return s
end
--[[
* Locate the best instance of 'pattern' in 'text' near 'loc' using the
* Bitap algorithm.
* @param {string} text The text to search.
* @param {string} pattern The pattern to search for.
* @param {number} loc The location to search around.
* @return {number} Best match index or -1.
* @private
--]]
function _match_bitap(text, pattern, loc)
if #pattern > Match_MaxBits then
error('Pattern too long.')
end
-- Initialise the alphabet.
local s = _match_alphabet(pattern)
--[[
* Compute and return the score for a match with e errors and x location.
* Accesses loc and pattern through being a closure.
* @param {number} e Number of errors in match.
* @param {number} x Location of match.
* @return {number} Overall score for match (0.0 = good, 1.0 = bad).
* @private
--]]
local function _match_bitapScore(e, x)
local accuracy = e / #pattern
local proximity = abs(loc - x)
if (Match_Distance == 0) then
-- Dodge divide by zero error.
return (proximity == 0) and 1 or accuracy
end
return accuracy + (proximity / Match_Distance)
end
-- Highest score beyond which we give up.
local score_threshold = Match_Threshold
-- Is there a nearby exact match? (speedup)
local best_loc = indexOf(text, pattern, loc)
if best_loc then
score_threshold = min(_match_bitapScore(0, best_loc), score_threshold)
-- LUANOTE: Ideally we'd also check from the other direction, but Lua
-- doesn't have an efficent lastIndexOf function.
end
-- Initialise the bit arrays.
local matchmask = lshift(1, #pattern - 1)
best_loc = -1
local bin_min, bin_mid
local bin_max = #pattern + #text
local last_rd
for d = 0, #pattern - 1, 1 do
-- Scan for the best match; each iteration allows for one more error.
-- Run a binary search to determine how far from 'loc' we can stray at this
-- error level.
bin_min = 0
bin_mid = bin_max
while (bin_min < bin_mid) do
if (_match_bitapScore(d, loc + bin_mid) <= score_threshold) then
bin_min = bin_mid
else
bin_max = bin_mid
end
bin_mid = floor(bin_min + (bin_max - bin_min) / 2)
end
-- Use the result from this iteration as the maximum for the next.
bin_max = bin_mid
local start = max(1, loc - bin_mid + 1)
local finish = min(loc + bin_mid, #text) + #pattern
local rd = {}
for j = start, finish do
rd[j] = 0
end
rd[finish + 1] = lshift(1, d) - 1
for j = finish, start, -1 do
local charMatch = s[strsub(text, j - 1, j - 1)] or 0
if (d == 0) then -- First pass: exact match.
rd[j] = band(bor((rd[j + 1] * 2), 1), charMatch)
else
-- Subsequent passes: fuzzy match.
-- Functions instead of operators make this hella messy.
rd[j] = bor(
band(
bor(
lshift(rd[j + 1], 1),
1
),
charMatch
),
bor(
bor(
lshift(bor(last_rd[j + 1], last_rd[j]), 1),
1
),
last_rd[j + 1]
)
)
end
if (band(rd[j], matchmask) ~= 0) then
local score = _match_bitapScore(d, j - 1)
-- This match will almost certainly be better than any existing match.
-- But check anyway.
if (score <= score_threshold) then
-- Told you so.
score_threshold = score
best_loc = j - 1
if (best_loc > loc) then
-- When passing loc, don't exceed our current distance from loc.
start = max(1, loc * 2 - best_loc)
else
-- Already passed loc, downhill from here on in.
break
end
end
end
end
-- No hope for a (better) match at greater error levels.
if (_match_bitapScore(d + 1, loc) > score_threshold) then
break
end
last_rd = rd
end
return best_loc
end
-- -----------------------------------------------------------------------------
-- PATCH API
-- -----------------------------------------------------------------------------
local _patch_addContext,
_patch_deepCopy,
_patch_addPadding,
_patch_splitMax,
_patch_appendText,
_new_patch_obj
--[[
* Compute a list of patches to turn text1 into text2.
* Use diffs if provided, otherwise compute it ourselves.
* There are four ways to call this function, depending on what data is
* available to the caller:
* Method 1:
* a = text1, b = text2
* Method 2:
* a = diffs
* Method 3 (optimal):
* a = text1, b = diffs
* Method 4 (deprecated, use method 3):
* a = text1, b = text2, c = diffs
*
* @param {string|Array.<Array.<number|string>>} a text1 (methods 1,3,4) or
* Array of diff tuples for text1 to text2 (method 2).
* @param {string|Array.<Array.<number|string>>} opt_b text2 (methods 1,4) or
* Array of diff tuples for text1 to text2 (method 3) or undefined (method 2).
* @param {string|Array.<Array.<number|string>>} opt_c Array of diff tuples for
* text1 to text2 (method 4) or undefined (methods 1,2,3).
* @return {Array.<_new_patch_obj>} Array of patch objects.
--]]
function patch_make(a, opt_b, opt_c)
local text1, diffs
local type_a, type_b, type_c = type(a), type(opt_b), type(opt_c)
if (type_a == 'string') and (type_b == 'string') and (type_c == 'nil') then
-- Method 1: text1, text2
-- Compute diffs from text1 and text2.
text1 = a
diffs = diff_main(text1, opt_b, true)
if (#diffs > 2) then
diff_cleanupSemantic(diffs)
diff_cleanupEfficiency(diffs)
end
elseif (type_a == 'table') and (type_b == 'nil') and (type_c == 'nil') then
-- Method 2: diffs
-- Compute text1 from diffs.
diffs = a
text1 = _diff_text1(diffs)
elseif (type_a == 'string') and (type_b == 'table') and (type_c == 'nil') then
-- Method 3: text1, diffs
text1 = a
diffs = opt_b
elseif (type_a == 'string') and (type_b == 'string') and (type_c == 'table')
then
-- Method 4: text1, text2, diffs
-- text2 is not used.
text1 = a
diffs = opt_c
else
error('Unknown call format to patch_make.')
end
if (diffs[1] == nil) then
return {} -- Get rid of the null case.
end
local patches = {}
local patch = _new_patch_obj()
local patchDiffLength = 0 -- Keeping our own length var is faster.
local char_count1 = 0 -- Number of characters into the text1 string.
local char_count2 = 0 -- Number of characters into the text2 string.
-- Start with text1 (prepatch_text) and apply the diffs until we arrive at
-- text2 (postpatch_text). We recreate the patches one by one to determine
-- context info.
local prepatch_text, postpatch_text = text1, text1
for x, diff in ipairs(diffs) do
local diff_type, diff_text = diff[1], diff[2]
if (patchDiffLength == 0) and (diff_type ~= DIFF_EQUAL) then
-- A new patch starts here.
patch.start1 = char_count1 + 1
patch.start2 = char_count2 + 1
end
if (diff_type == DIFF_INSERT) then
patchDiffLength = patchDiffLength + 1
patch.diffs[patchDiffLength] = diff
patch.length2 = patch.length2 + #diff_text
postpatch_text = strsub(postpatch_text, 1, char_count2)
.. diff_text .. strsub(postpatch_text, char_count2 + 1)
elseif (diff_type == DIFF_DELETE) then
patch.length1 = patch.length1 + #diff_text
patchDiffLength = patchDiffLength + 1
patch.diffs[patchDiffLength] = diff
postpatch_text = strsub(postpatch_text, 1, char_count2)
.. strsub(postpatch_text, char_count2 + #diff_text + 1)
elseif (diff_type == DIFF_EQUAL) then
if (#diff_text <= Patch_Margin * 2)
and (patchDiffLength ~= 0) and (#diffs ~= x) then
-- Small equality inside a patch.
patchDiffLength = patchDiffLength + 1
patch.diffs[patchDiffLength] = diff
patch.length1 = patch.length1 + #diff_text
patch.length2 = patch.length2 + #diff_text
elseif (#diff_text >= Patch_Margin * 2) then
-- Time for a new patch.
if (patchDiffLength ~= 0) then
_patch_addContext(patch, prepatch_text)
patches[#patches + 1] = patch
patch = _new_patch_obj()
patchDiffLength = 0
-- Unlike Unidiff, our patch lists have a rolling context.
-- http://code.google.com/p/google-diff-match-patch/wiki/Unidiff
-- Update prepatch text & pos to reflect the application of the
-- just completed patch.
prepatch_text = postpatch_text
char_count1 = char_count2
end
end
end
-- Update the current character count.
if (diff_type ~= DIFF_INSERT) then
char_count1 = char_count1 + #diff_text
end
if (diff_type ~= DIFF_DELETE) then
char_count2 = char_count2 + #diff_text
end
end
-- Pick up the leftover patch if not empty.
if (patchDiffLength > 0) then
_patch_addContext(patch, prepatch_text)
patches[#patches + 1] = patch
end
return patches
end
--[[
* Merge a set of patches onto the text. Return a patched text, as well
* as a list of true/false values indicating which patches were applied.
* @param {Array.<_new_patch_obj>} patches Array of patch objects.
* @param {string} text Old text.
* @return {Array.<string|Array.<boolean>>} Two return values, the
* new text and an array of boolean values.
--]]
function patch_apply(patches, text)
if patches[1] == nil then
return text, {}
end
-- Deep copy the patches so that no changes are made to originals.
patches = _patch_deepCopy(patches)
local nullPadding = _patch_addPadding(patches)
text = nullPadding .. text .. nullPadding
_patch_splitMax(patches)
-- delta keeps track of the offset between the expected and actual location
-- of the previous patch. If there are patches expected at positions 10 and
-- 20, but the first patch was found at 12, delta is 2 and the second patch
-- has an effective expected position of 22.
local delta = 0
local results = {}
for x, patch in ipairs(patches) do
local expected_loc = patch.start2 + delta
local text1 = _diff_text1(patch.diffs)
local start_loc
local end_loc = -1
if #text1 > Match_MaxBits then
-- _patch_splitMax will only provide an oversized pattern in
-- the case of a monster delete.
start_loc = match_main(text,
strsub(text1, 1, Match_MaxBits), expected_loc)
if start_loc ~= -1 then
end_loc = match_main(text, strsub(text1, -Match_MaxBits),
expected_loc + #text1 - Match_MaxBits)
if end_loc == -1 or start_loc >= end_loc then
-- Can't find valid trailing context. Drop this patch.
start_loc = -1
end
end
else
start_loc = match_main(text, text1, expected_loc)
end
if start_loc == -1 then
-- No match found. :(
results[x] = false
-- Subtract the delta for this failed patch from subsequent patches.
delta = delta - patch.length2 - patch.length1
else
-- Found a match. :)
results[x] = true
delta = start_loc - expected_loc
local text2
if end_loc == -1 then
text2 = strsub(text, start_loc, start_loc + #text1 - 1)
else
text2 = strsub(text, start_loc, end_loc + Match_MaxBits - 1)
end
if text1 == text2 then
-- Perfect match, just shove the replacement text in.
text = strsub(text, 1, start_loc - 1) .. _diff_text2(patch.diffs)
.. strsub(text, start_loc + #text1)
else
-- Imperfect match. Run a diff to get a framework of equivalent
-- indices.
local diffs = diff_main(text1, text2, false)
if (#text1 > Match_MaxBits)
and (diff_levenshtein(diffs) / #text1 > Patch_DeleteThreshold) then
-- The end points match, but the content is unacceptably bad.
results[x] = false
else
_diff_cleanupSemanticLossless(diffs)
local index1 = 1
local index2
for y, mod in ipairs(patch.diffs) do
if mod[1] ~= DIFF_EQUAL then
index2 = _diff_xIndex(diffs, index1)
end
if mod[1] == DIFF_INSERT then
text = strsub(text, 1, start_loc + index2 - 2)
.. mod[2] .. strsub(text, start_loc + index2 - 1)
elseif mod[1] == DIFF_DELETE then
text = strsub(text, 1, start_loc + index2 - 2) .. strsub(text,
start_loc + _diff_xIndex(diffs, index1 + #mod[2] - 1))
end
if mod[1] ~= DIFF_DELETE then
index1 = index1 + #mod[2]
end
end
end
end
end
end
-- Strip the padding off.
text = strsub(text, #nullPadding + 1, -#nullPadding - 1)
return text, results
end
--[[
* Take a list of patches and return a textual representation.
* @param {Array.<_new_patch_obj>} patches Array of patch objects.
* @return {string} Text representation of patches.
--]]
function patch_toText(patches)
local text = {}
for x, patch in ipairs(patches) do
_patch_appendText(patch, text)
end
return tconcat(text)
end
--[[
* Parse a textual representation of patches and return a list of patch objects.
* @param {string} textline Text representation of patches.
* @return {Array.<_new_patch_obj>} Array of patch objects.
* @throws {Error} If invalid input.
--]]
function patch_fromText(textline)
local patches = {}
if (#textline == 0) then
return patches
end
local text = {}
for line in gmatch(textline .. "\n", '([^\n]*)\n') do
text[#text + 1] = line
end
local textPointer = 1
while (textPointer <= #text) do
local start1, length1, start2, length2
= strmatch(text[textPointer], '^@@ %-(%d+),?(%d*) %+(%d+),?(%d*) @@$')
if (start1 == nil) then
error('Invalid patch string: "' .. text[textPointer] .. '"')
end
local patch = _new_patch_obj()
patches[#patches + 1] = patch
start1 = tonumber(start1)
length1 = tonumber(length1) or 1
if (length1 == 0) then
start1 = start1 + 1
end
patch.start1 = start1
patch.length1 = length1
start2 = tonumber(start2)
length2 = tonumber(length2) or 1
if (length2 == 0) then
start2 = start2 + 1
end
patch.start2 = start2
patch.length2 = length2
textPointer = textPointer + 1
while true do
local line = text[textPointer]
if (line == nil) then
break
end
local sign; sign, line = strsub(line, 1, 1), strsub(line, 2)
local invalidDecode = false
local decoded = gsub(line, '%%(.?.?)',
function(c)
local n = tonumber(c, 16)
if (#c ~= 2) or (n == nil) then
invalidDecode = true
return ''
end
return strchar(n)
end)
if invalidDecode then
-- Malformed URI sequence.
error('Illegal escape in patch_fromText: ' .. line)
end
line = decoded
if (sign == '-') then
-- Deletion.
patch.diffs[#patch.diffs + 1] = {DIFF_DELETE, line}
elseif (sign == '+') then
-- Insertion.
patch.diffs[#patch.diffs + 1] = {DIFF_INSERT, line}
elseif (sign == ' ') then
-- Minor equality.
patch.diffs[#patch.diffs + 1] = {DIFF_EQUAL, line}
elseif (sign == '@') then
-- Start of next patch.
break
elseif (sign == '') then
-- Blank line? Whatever.
else
-- WTF?
error('Invalid patch mode "' .. sign .. '" in: ' .. line)
end
textPointer = textPointer + 1
end
end
return patches
end
-- ---------------------------------------------------------------------------
-- UNOFFICIAL/PRIVATE PATCH FUNCTIONS
-- ---------------------------------------------------------------------------
local patch_meta = {
__tostring = function(patch)
local buf = {}
_patch_appendText(patch, buf)
return tconcat(buf)
end
}
--[[
* Class representing one patch operation.
* @constructor
--]]
function _new_patch_obj()
return setmetatable({
--[[ @type {Array.<Array.<number|string>>} ]]
diffs = {};
--[[ @type {?number} ]]
start1 = 1; -- nil;
--[[ @type {?number} ]]
start2 = 1; -- nil;
--[[ @type {number} ]]
length1 = 0;
--[[ @type {number} ]]
length2 = 0;
}, patch_meta)
end
--[[
* Increase the context until it is unique,
* but don't let the pattern expand beyond Match_MaxBits.
* @param {_new_patch_obj} patch The patch to grow.
* @param {string} text Source text.
* @private
--]]
function _patch_addContext(patch, text)
if (#text == 0) then
return
end
local pattern = strsub(text, patch.start2, patch.start2 + patch.length1 - 1)
local padding = 0
-- LUANOTE: Lua's lack of a lastIndexOf function results in slightly
-- different logic here than in other language ports.
-- Look for the first two matches of pattern in text. If two are found,
-- increase the pattern length.
local firstMatch = indexOf(text, pattern)
local secondMatch = nil
if (firstMatch ~= nil) then
secondMatch = indexOf(text, pattern, firstMatch + 1)
end
while (#pattern == 0 or secondMatch ~= nil)
and (#pattern < Match_MaxBits - Patch_Margin - Patch_Margin) do
padding = padding + Patch_Margin
pattern = strsub(text, max(1, patch.start2 - padding),
patch.start2 + patch.length1 - 1 + padding)
firstMatch = indexOf(text, pattern)
if (firstMatch ~= nil) then
secondMatch = indexOf(text, pattern, firstMatch + 1)
else
secondMatch = nil
end
end
-- Add one chunk for good luck.
padding = padding + Patch_Margin
-- Add the prefix.
local prefix = strsub(text, max(1, patch.start2 - padding), patch.start2 - 1)
if (#prefix > 0) then
tinsert(patch.diffs, 1, {DIFF_EQUAL, prefix})
end
-- Add the suffix.
local suffix = strsub(text, patch.start2 + patch.length1,
patch.start2 + patch.length1 - 1 + padding)
if (#suffix > 0) then
patch.diffs[#patch.diffs + 1] = {DIFF_EQUAL, suffix}
end
-- Roll back the start points.
patch.start1 = patch.start1 - #prefix
patch.start2 = patch.start2 - #prefix
-- Extend the lengths.
patch.length1 = patch.length1 + #prefix + #suffix
patch.length2 = patch.length2 + #prefix + #suffix
end
--[[
* Given an array of patches, return another array that is identical.
* @param {Array.<_new_patch_obj>} patches Array of patch objects.
* @return {Array.<_new_patch_obj>} Array of patch objects.
--]]
function _patch_deepCopy(patches)
local patchesCopy = {}
for x, patch in ipairs(patches) do
local patchCopy = _new_patch_obj()
local diffsCopy = {}
for i, diff in ipairs(patch.diffs) do
diffsCopy[i] = {diff[1], diff[2]}
end
patchCopy.diffs = diffsCopy
patchCopy.start1 = patch.start1
patchCopy.start2 = patch.start2
patchCopy.length1 = patch.length1
patchCopy.length2 = patch.length2
patchesCopy[x] = patchCopy
end
return patchesCopy
end
--[[
* Add some padding on text start and end so that edges can match something.
* Intended to be called only from within patch_apply.
* @param {Array.<_new_patch_obj>} patches Array of patch objects.
* @return {string} The padding string added to each side.
--]]
function _patch_addPadding(patches)
local paddingLength = Patch_Margin
local nullPadding = ''
for x = 1, paddingLength do
nullPadding = nullPadding .. strchar(x)
end
-- Bump all the patches forward.
for x, patch in ipairs(patches) do
patch.start1 = patch.start1 + paddingLength
patch.start2 = patch.start2 + paddingLength
end
-- Add some padding on start of first diff.
local patch = patches[1]
local diffs = patch.diffs
local firstDiff = diffs[1]
if (firstDiff == nil) or (firstDiff[1] ~= DIFF_EQUAL) then
-- Add nullPadding equality.
tinsert(diffs, 1, {DIFF_EQUAL, nullPadding})
patch.start1 = patch.start1 - paddingLength -- Should be 0.
patch.start2 = patch.start2 - paddingLength -- Should be 0.
patch.length1 = patch.length1 + paddingLength
patch.length2 = patch.length2 + paddingLength
elseif (paddingLength > #firstDiff[2]) then
-- Grow first equality.
local extraLength = paddingLength - #firstDiff[2]
firstDiff[2] = strsub(nullPadding, #firstDiff[2] + 1) .. firstDiff[2]
patch.start1 = patch.start1 - extraLength
patch.start2 = patch.start2 - extraLength
patch.length1 = patch.length1 + extraLength
patch.length2 = patch.length2 + extraLength
end
-- Add some padding on end of last diff.
patch = patches[#patches]
diffs = patch.diffs
local lastDiff = diffs[#diffs]
if (lastDiff == nil) or (lastDiff[1] ~= DIFF_EQUAL) then
-- Add nullPadding equality.
diffs[#diffs + 1] = {DIFF_EQUAL, nullPadding}
patch.length1 = patch.length1 + paddingLength
patch.length2 = patch.length2 + paddingLength
elseif (paddingLength > #lastDiff[2]) then
-- Grow last equality.
local extraLength = paddingLength - #lastDiff[2]
lastDiff[2] = lastDiff[2] .. strsub(nullPadding, 1, extraLength)
patch.length1 = patch.length1 + extraLength
patch.length2 = patch.length2 + extraLength
end
return nullPadding
end
--[[
* Look through the patches and break up any which are longer than the maximum
* limit of the match algorithm.
* Intended to be called only from within patch_apply.
* @param {Array.<_new_patch_obj>} patches Array of patch objects.
--]]
function _patch_splitMax(patches)
local patch_size = Match_MaxBits
local x = 1
while true do
local patch = patches[x]
if patch == nil then
return
end
if patch.length1 > patch_size then
local bigpatch = patch
-- Remove the big old patch.
tremove(patches, x)
x = x - 1
local start1 = bigpatch.start1
local start2 = bigpatch.start2
local precontext = ''
while bigpatch.diffs[1] do
-- Create one of several smaller patches.
local patch = _new_patch_obj()
local empty = true
patch.start1 = start1 - #precontext
patch.start2 = start2 - #precontext
if precontext ~= '' then
patch.length1, patch.length2 = #precontext, #precontext
patch.diffs[#patch.diffs + 1] = {DIFF_EQUAL, precontext}
end
while bigpatch.diffs[1] and (patch.length1 < patch_size-Patch_Margin) do
local diff_type = bigpatch.diffs[1][1]
local diff_text = bigpatch.diffs[1][2]
if (diff_type == DIFF_INSERT) then
-- Insertions are harmless.
patch.length2 = patch.length2 + #diff_text
start2 = start2 + #diff_text
patch.diffs[#(patch.diffs) + 1] = bigpatch.diffs[1]
tremove(bigpatch.diffs, 1)
empty = false
elseif (diff_type == DIFF_DELETE) and (#patch.diffs == 1)
and (patch.diffs[1][1] == DIFF_EQUAL)
and (#diff_text > 2 * patch_size) then
-- This is a large deletion. Let it pass in one chunk.
patch.length1 = patch.length1 + #diff_text
start1 = start1 + #diff_text
empty = false
patch.diffs[#patch.diffs + 1] = {diff_type, diff_text}
tremove(bigpatch.diffs, 1)
else
-- Deletion or equality.
-- Only take as much as we can stomach.
diff_text = strsub(diff_text, 1,
patch_size - patch.length1 - Patch_Margin)
patch.length1 = patch.length1 + #diff_text
start1 = start1 + #diff_text
if (diff_type == DIFF_EQUAL) then
patch.length2 = patch.length2 + #diff_text
start2 = start2 + #diff_text
else
empty = false
end
patch.diffs[#patch.diffs + 1] = {diff_type, diff_text}
if (diff_text == bigpatch.diffs[1][2]) then
tremove(bigpatch.diffs, 1)
else
bigpatch.diffs[1][2]
= strsub(bigpatch.diffs[1][2], #diff_text + 1)
end
end
end
-- Compute the head context for the next patch.
precontext = _diff_text2(patch.diffs)
precontext = strsub(precontext, -Patch_Margin)
-- Append the end context for this patch.
local postcontext = strsub(_diff_text1(bigpatch.diffs), 1, Patch_Margin)
if postcontext ~= '' then
patch.length1 = patch.length1 + #postcontext
patch.length2 = patch.length2 + #postcontext
if patch.diffs[1]
and (patch.diffs[#patch.diffs][1] == DIFF_EQUAL) then
patch.diffs[#patch.diffs][2] = patch.diffs[#patch.diffs][2]
.. postcontext
else
patch.diffs[#patch.diffs + 1] = {DIFF_EQUAL, postcontext}
end
end
if not empty then
x = x + 1
tinsert(patches, x, patch)
end
end
end
x = x + 1
end
end
--[[
* Emulate GNU diff's format.
* Header: @@ -382,8 +481,9 @@
* @return {string} The GNU diff string.
--]]
function _patch_appendText(patch, text)
local coords1, coords2
local length1, length2 = patch.length1, patch.length2
local start1, start2 = patch.start1, patch.start2
local diffs = patch.diffs
if length1 == 1 then
coords1 = start1
else
coords1 = ((length1 == 0) and (start1 - 1) or start1) .. ',' .. length1
end
if length2 == 1 then
coords2 = start2
else
coords2 = ((length2 == 0) and (start2 - 1) or start2) .. ',' .. length2
end
text[#text + 1] = '@@ -' .. coords1 .. ' +' .. coords2 .. ' @@\n'
local op
-- Escape the body of the patch with %xx notation.
for x, diff in ipairs(patch.diffs) do
local diff_type = diff[1]
if diff_type == DIFF_INSERT then
op = '+'
elseif diff_type == DIFF_DELETE then
op = '-'
elseif diff_type == DIFF_EQUAL then
op = ' '
end
text[#text + 1] = op
.. gsub(diffs[x][2], percentEncode_pattern, percentEncode_replace)
.. '\n'
end
return text
end
settings {
Match_Threshold = 0.3,
}
-- Expose the API
local _M = {}
_M.DIFF_DELETE = DIFF_DELETE
_M.DIFF_INSERT = DIFF_INSERT
_M.DIFF_EQUAL = DIFF_EQUAL
_M.diff_main = diff_main
_M.diff_cleanupSemantic = diff_cleanupSemantic
_M.diff_cleanupEfficiency = diff_cleanupEfficiency
_M.diff_levenshtein = diff_levenshtein
_M.diff_prettyHtml = diff_prettyHtml
_M.match_main = match_main
_M.patch_make = patch_make
_M.patch_toText = patch_toText
_M.patch_fromText = patch_fromText
_M.patch_apply = patch_apply
-- Expose some non-API functions as well, for testing purposes etc.
_M.diff_commonPrefix = _diff_commonPrefix
_M.diff_commonSuffix = _diff_commonSuffix
_M.diff_commonOverlap = _diff_commonOverlap
_M.diff_halfMatch = _diff_halfMatch
_M.diff_bisect = _diff_bisect
_M.diff_cleanupMerge = _diff_cleanupMerge
_M.diff_cleanupSemanticLossless = _diff_cleanupSemanticLossless
_M.diff_text1 = _diff_text1
_M.diff_text2 = _diff_text2
_M.diff_toDelta = _diff_toDelta
_M.diff_fromDelta = _diff_fromDelta
_M.diff_xIndex = _diff_xIndex
_M.match_alphabet = _match_alphabet
_M.match_bitap = _match_bitap
_M.new_patch_obj = _new_patch_obj
_M.patch_addContext = _patch_addContext
_M.patch_splitMax = _patch_splitMax
_M.patch_addPadding = _patch_addPadding
_M.settings = settings
return _M
end
preload["bsrocks.lib.diff"] = function(...)
--[[
(C) Paul Butler 2008-2012 <http://www.paulbutler.org/>
May be used and distributed under the zlib/libpng license
<http://www.opensource.org/licenses/zlib-license.php>
Adaptation to Lua by Philippe Fremy <phil at freehackers dot org>
Lua version copyright 2015
]]
local ipairs = ipairs
local function table_join(t1, t2, t3)
-- return a table containing all elements of t1 then t2 then t3
local t, n = {}, 0
for i,v in ipairs(t1) do
n = n + 1
t[n] = v
end
for i,v in ipairs(t2) do
n = n + 1
t[n] = v
end
if t3 then
for i,v in ipairs(t3) do
n = n + 1
t[n] = v
end
end
return t
end
local function table_subtable( t, start, stop )
-- 0 is first element, stop is last element
local ret = {}
if stop == nil then
stop = #t
end
if start < 0 or stop < 0 or start > stop then
error('Invalid values: '..start..' '..stop )
end
for i,v in ipairs(t) do
if (i-1) >= start and (i-1) < stop then
table.insert( ret, v )
end
end
return ret
end
--[[-
Find the differences between two lists or strings.
Returns a list of pairs, where the first value is in ['+','-','=']
and represents an insertion, deletion, or no change for that list.
The second value of the pair is the list of elements.
@tparam table old the old list of immutable, comparable values (ie. a list of strings)
@tparam table new the new list of immutable, comparable values
@return table A list of pairs, with the first part of the pair being one of three
strings ('-', '+', '=') and the second part being a list of values from
the original old and/or new lists. The first part of the pair
corresponds to whether the list of values is a deletion, insertion, or
unchanged, respectively.
@example
diff( {1,2,3,4}, {1,3,4})
{ {'=', {1} }, {'-', {2} }, {'=', {3, 4}} }
diff( {1,2,3,4}, {2,3,4,1} )
{ {'-', {1}}, {'=', {2, 3, 4}}, {'+', {1}} }
diff(
{ 'The', 'quick', 'brown', 'fox', 'jumps', 'over', 'the', 'lazy', 'dog' },
{ 'The', 'slow', 'blue', 'cheese', 'drips', 'over', 'the', 'lazy', 'carrot' }
)
{ {'=', {'The'} },
{'-', {'quick', 'brown', 'fox', 'jumps'} },
{'+', {'slow', 'blue', 'cheese', 'drips'} },
{'=', {'over', 'the', 'lazy'} },
{'-', {'dog'} },
{'+', {'carrot'} }
}
]]
local function diff(old, new)
-- Create a map from old values to their indices
local old_index_map = {}
for i, val in ipairs(old) do
if not old_index_map[val] then
old_index_map[val] = {}
end
table.insert( old_index_map[val], i-1 )
end
--[[
Find the largest substring common to old and new.
We use a dynamic programming approach here.
We iterate over each value in the `new` list, calling the
index `inew`. At each iteration, `overlap[i]` is the
length of the largest suffix of `old[:i]` equal to a suffix
of `new[:inew]` (or unset when `old[i]` != `new[inew]`).
At each stage of iteration, the new `overlap` (called
`_overlap` until the original `overlap` is no longer needed)
is built from the old one.
If the length of overlap exceeds the largest substring
seen so far (`sub_length`), we update the largest substring
to the overlapping strings.
`sub_start_old` is the index of the beginning of the largest overlapping
substring in the old list. `sub_start_new` is the index of the beginning
of the same substring in the new list. `sub_length` is the length that
overlaps in both.
These track the largest overlapping substring seen so far, so naturally
we start with a 0-length substring.
]]
local overlap = {}
local sub_start_old = 0
local sub_start_new = 0
local sub_length = 0
for inewInc, val in ipairs(new) do
local inew = inewInc-1
local _overlap = {}
if old_index_map[val] then
for _,iold in ipairs(old_index_map[val]) do
-- now we are considering all values of iold such that
-- `old[iold] == new[inew]`.
if iold <= 0 then
_overlap[iold] = 1
else
_overlap[iold] = (overlap[iold - 1] or 0) + 1
end
if (_overlap[iold] > sub_length) then
sub_length = _overlap[iold]
sub_start_old = iold - sub_length + 1
sub_start_new = inew - sub_length + 1
end
end
end
overlap = _overlap
end
if sub_length == 0 then
-- If no common substring is found, we return an insert and delete...
local oldRet = {}
local newRet = {}
if #old > 0 then
oldRet = { {'-', old} }
end
if #new > 0 then
newRet = { {'+', new} }
end
return table_join( oldRet, newRet )
else
-- ...otherwise, the common substring is unchanged and we recursively
-- diff the text before and after that substring
return table_join(
diff(
table_subtable( old, 0, sub_start_old),
table_subtable( new, 0, sub_start_new)
),
{ {'=', table_subtable(new,sub_start_new,sub_start_new + sub_length) } },
diff(
table_subtable( old, sub_start_old + sub_length ),
table_subtable( new, sub_start_new + sub_length )
)
)
end
end
return diff
end
preload["bsrocks.env.package"] = function(...)
--- The main package library - a pure lua reimplementation of the package library in lua
-- See: http://www.lua.org/manual/5.1/manual.html#5.3
local fileWrapper = require "bsrocks.lib.files"
local settings = require "bsrocks.lib.settings"
local utils = require "bsrocks.lib.utils"
local checkType = utils.checkType
return function(env)
local _G = env._G
local path = settings.libPath
if type(path) == "table" then path = table.concat(path, ";") end
path = path:gsub("%%{(%a+)}", settings)
local package = {
loaded = {},
preload = {},
path = path,
config = "/\n;\n?\n!\n-",
cpath = "",
}
-- Set as a global
_G.package = package
--- Load up the package data
-- This by default produces an error
function package.loadlib(libname, funcname)
return nil, "dynamic libraries not enabled", "absent"
end
--- Allows the module to access the global table
-- @tparam table module The module
function package.seeall(module)
checkType(module, "table")
local meta = getmetatable(module)
if not meta then
meta = {}
setmetatable(module, meta)
end
meta.__index = _G
end
package.loaders = {
--- Preloader - checks preload table
-- @tparam string name Package name to load
function(name)
checkType(name, "string")
return package.preload[name] or ("\n\tno field package.preload['" .. name .. "']")
end,
function(name)
checkType(name, "string")
local path = package.path
if type(path) ~= "string" then
error("package.path is not a string", 2)
end
name = name:gsub("%.", "/")
local errs = {}
local pos, len = 1, #path
while pos <= len do
local start = path:find(";", pos)
if not start then
start = len + 1
end
local filePath = env.resolve(path:sub(pos, start - 1):gsub("%?", name, 1))
pos = start + 1
local loaded, err
if fs.exists(filePath) then
loaded, err = load(fileWrapper.read(filePath), filePath, "t", _G)
elseif fs.exists(filePath .. ".lua") then
loaded, err = load(fileWrapper.read(filePath .. ".lua"), filePath, "t", _G)
else
err = "File not found"
end
if type(loaded) == "function" then
return loaded
end
errs[#errs + 1] = "'" .. filePath .. "': " .. err
end
return table.concat(errs, "\n\t")
end
}
--- Require a module
-- @tparam string name The name of the module
-- Checks each loader in turn. If it finds a function then it will
-- execute it and store the result in package.loaded[name]
function _G.require(name)
checkType(name, "string")
local loaded = package.loaded
local thisPackage = loaded[name]
if thisPackage ~= nil then
if thisPackage then return thisPackage end
error("loop or previous error loading module ' " .. name .. "'", 2)
end
local loaders = package.loaders
checkType(loaders, "table")
local errs = {}
for _, loader in ipairs(loaders) do
thisPackage = loader(name)
local lType = type(thisPackage)
if lType == "string" then
errs[#errs + 1] = thisPackage
elseif lType == "function" then
-- Prevent cyclic dependencies
loaded[name] = false
-- Execute the method
local result = thisPackage(name)
-- If we returned something then set the result to it
if result ~= nil then
loaded[name] = result
else
-- If set something in the package.loaded table then use that
result = loaded[name]
if result == false then
-- Otherwise just set it to true
loaded[name] = true
result = true
end
end
return result
end
end
-- Can't find it - just error
error("module '" .. name .. "' not found: " .. name .. table.concat(errs, ""))
end
-- Find the name of a table
-- @tparam table table The table to look in
-- @tparam string name The name to look up (abc.def.ghi)
-- @return The table for that name or a new one or nil if a non table has it already
local function findTable(table, name)
local pos, len = 1, #name
while pos <= len do
local start = name:find(".", pos, true)
if not start then
start = len + 1
end
local key = name:sub(pos, start - 1)
pos = start + 1
local val = rawget(table, key)
if val == nil then
-- If it doesn't exist then create it
val = {}
table[key] = val
table = val
elseif type(val) == "table" then
table = val
else
return nil
end
end
return table
end
-- Set the current env to be a module
-- @tparam lua
function _G.module(name, ...)
checkType(name, "string")
local module = package.loaded[name]
if type(module) ~= "table" then
module = findTable(_G, name)
if not module then
error("name conflict for module '" .. name .. "'", 2)
end
package.loaded[name] = module
end
-- Init properties
if module._NAME == nil then
module._M = module
module._NAME = name:gsub("([^.]+%.)", "") -- Everything afert last .
module._PACKAGE = name:gsub("%.[^%.]+$", "") or "" -- Everything before the last .
end
setfenv(2, module)
-- Applies functions. This could be package.seeall or similar
for _, modifier in pairs({...}) do
modifier(module)
end
end
-- Populate the package.loaded table
local loaded = package.loaded
for k, v in pairs(_G) do
if type(v) == "table" then
loaded[k] = v
end
end
return package
end
end
preload["bsrocks.env.os"] = function(...)
--- Pure lua implementation of the OS api
-- http://www.lua.org/manual/5.1/manual.html#5.8
local utils = require "bsrocks.lib.utils"
local date = require "bsrocks.env.date"
local checkType = utils.checkType
return function(env)
local os, shell = os, shell
local envVars = {}
local temp = {}
local clock = os.clock
if profiler and profiler.milliTime then
clock = function() return profiler.milliTime() * 1e-3 end
end
env._G.os = {
clock = clock,
date = function(format, time)
format = checkType(format or "%c", "string")
time = checkType(time or os.time(), "number")
-- Ignore UTC/CUT
if format:sub(1, 1) == "!" then format = format:sub(2) end
local d = date.create(time)
if format == "*t" then
return d
elseif format == "%c" then
return date.asctime(d)
else
return date.strftime(format, d)
end
end,
-- Returns the number of seconds from time t1 to time t2. In POSIX, Windows, and some other systems, this value is exactly t2-t1.
difftime = function(t1, t2)
return t2 - t1
end,
execute = function(command)
if shell.run(command) then
return 0
else
return 1
end
end,
exit = function(code) error("Exit code: " .. (code or 0), 0) end,
getenv = function(name)
-- I <3 ClamShell
if shell.getenv then
local val = shell.getenv(name)
if val ~= nil then return val end
end
if settings and settings.get then
local val = settings.get(name)
if val ~= nil then return val end
end
return envVars[name]
end,
remove = function(path)
return pcall(fs.delete, env.resolve(checkType(path, "string")))
end,
rename = function(oldname, newname)
return pcall(fs.rename, env.resolve(checkType(oldname, "string")), env.resolve(checkType(newname, "string")))
end,
setlocale = function() end,
-- Technically not
time = function(tbl)
if not tbl then return os.time() end
checkType(tbl, "table")
return date.timestamp(tbl)
end,
tmpname = function()
local name = utils.tmpName()
temp[name] = true
return name
end
}
-- Delete temp files
env.cleanup[#env.cleanup + 1] = function()
for file, _ in pairs(temp) do
pcall(fs.delete, file)
end
end
end
end
preload["bsrocks.env.io"] = function(...)
--- The main io library
-- See: http://www.lua.org/manual/5.1/manual.html#5.7
-- Some elements are duplicated in /rom/apis/io but this is a more accurate representation
local utils = require "bsrocks.lib.utils"
local ansi = require "bsrocks.env.ansi"
local checkType = utils.checkType
local function isFile(file)
return type(file) == "table" and file.close and file.flush and file.lines and file.read and file.seek and file.setvbuf and file.write
end
local function checkFile(file)
if not isFile(file) then
error("Not a file: Missing one of: close, flush, lines, read, seek, setvbuf, write", 3)
end
end
local function getHandle(file)
local t = type(file)
if t ~= "table" or not file.__handle then
error("FILE* expected, got " .. t)
end
if file.__isClosed then
error("attempt to use closed file", 3)
end
return file.__handle
end
local fileMeta = {
__index = {
close = function(self)
self.__handle.close()
self.__isClosed = true
end,
flush = function(self)
getHandle(self).flush()
end,
read = function(self, ...)
local handle = getHandle(self)
local returns = {}
local data = {...}
local n = select("#", ...)
if n == 0 then n = 1 end
for i = 1, n do
local format = data[i] or "l"
format = checkType(format, "string"):gsub("%*", ""):sub(1, 1) -- "*" is not needed after Lua 5.1 - lets be friendly
local res, msg
if format == "l" then
res, msg = handle.readLine()
elseif format == "a" then
res, msg = handle.readAll()
elseif format == "r" then
res, msg = handle.read() -- Binary only
else
error("(invalid format", 2)
end
if not res then return res, msg end
returns[#returns + 1] = res
end
return unpack(returns)
end,
seek = function(self, ...)
error("File seek is not implemented", 2)
end,
setvbuf = function() end,
write = function(self, ...)
local handle = getHandle(self)
local data = {...}
local n = select("#", ...)
for i = 1, n do
local item = data[i]
local t = type(item)
if t ~= "string" and t ~= "number" then
error("string expected, got " .. t)
end
handle.write(tostring(item))
end
return true
end,
lines = function(self, ...)
return self.__handle.readLine
end,
}
}
return function(env)
local io = {}
env._G.io = io
local function loadFile(path, mode)
path = env.resolve(path)
mode = (mode or "r"):gsub("%+", "")
local ok, result = pcall(fs.open, path, mode)
if not ok or not result then
return nil, result or "No such file or directory"
end
return setmetatable({ __handle = result }, fileMeta)
end
do -- Setup standard outputs
local function voidStub() end
local function closeStub() return nil, "cannot close standard file" end
local function readStub() return nil, "bad file descriptor" end
env.stdout = setmetatable({
__handle = {
close = closeStub,
flush = voidStub,
read = readStub, readLine = readStub, readAll = readStub,
write = function(arg) ansi.write(arg) end,
}
}, fileMeta)
env.stderr = setmetatable({
__handle = {
close = closeStub,
flush = voidStub,
read = readStub, readLine = readStub, readAll = readStub,
write = function(arg)
local c = term.isColor()
if c then term.setTextColor(colors.red) end
ansi.write(arg)
if c then term.setTextColor(colors.white) end
end,
}
}, fileMeta)
env.stdin = setmetatable({
__handle = {
close = closeStub,
flush = voidStub,
read = function() return string.byte(os.pullEvent("char")) end,
readLine = read, readAll = read,
write = function() error("cannot write to input", 3) end,
}
}, fileMeta)
io.stdout = env.stdout
io.stderr = env.stderr
io.stdin = env.stdin
end
function io.close(file)
(file or env.stdout):close()
end
function io.flush(file)
env.stdout:flush()
end
function io.input(file)
local t = type(file)
if t == "nil" then
return env.stdin
elseif t == "string" then
file = assert(loadFile(file, "r"))
elseif t ~= "table" then
error("string expected, got " .. t, 2)
end
checkFile(file)
io.stdin = file
env.stdin = file
return file
end
function io.output(file)
local t = type(file)
if t == "nil" then
return env.stdin
elseif t == "string" then
file = assert(loadFile(file, "w"))
elseif t ~= "table" then
error("string expected, got " .. t, 2)
end
checkFile(file)
io.stdout = file
env.stdout = file
return file
end
function io.popen(file)
error("io.popen is not implemented", 2)
end
function io.read(...)
return env.stdin:read(...)
end
local temp = {}
function io.tmpfile()
local name = utils.tmpName()
temp[name] = true
return loadFile(name, "w")
end
io.open = loadFile
function io.type(file)
if isFile(file) then
if file.__isClosed then return "closed file" end
return "file"
else
return type(file)
end
end
function io.write(...)
return env.stdout:write(...)
end
env._G.write = io.write
env._G.print = ansi.print
-- Delete temp files
env.cleanup[#env.cleanup + 1] = function()
for file, _ in pairs(temp) do
pcall(fs.delete, file)
end
end
return io
end
end
preload["bsrocks.env"] = function(...)
local fileWrapper = require "bsrocks.lib.files"
local function addWithMeta(src, dest)
for k, v in pairs(src) do
if dest[k] == nil then
dest[k] = v
end
end
local meta = getmetatable(src)
if type(meta) == "table" and type(meta.__index) == "table" then
return addWithMeta(meta.__index, dest)
end
end
return function()
local nImplemented = function(name)
return function()
error(name .. " is not implemented", 2)
end
end
local _G = {
math = math,
string = string,
table = table,
coroutine = coroutine,
collectgarbage = nImplemented("collectgarbage"),
_VERSION = _VERSION
}
_G._G = _G
_G._ENV = _G
local env = {
_G = _G,
dir = shell.dir(),
stdin = false,
stdout = false,
strerr = false,
cleanup = {}
}
function env.resolve(path)
if path:sub(1, 1) ~= "/" then
path = fs.combine(env.dir, path)
end
return path
end
function _G.load(func, chunk)
local cache = {}
while true do
local r = func()
if r == "" or r == nil then
break
end
cache[#cache + 1] = r
end
return _G.loadstring(table.concat(func), chunk or "=(load)")
end
-- Need to set environment
function _G.loadstring(name, chunk)
return load(name, chunk, nil, _G)
end
-- Customised loadfile function to work with relative files
function _G.loadfile(path)
path = env.resolve(path)
if fs.exists(path) then
return load(fileWrapper.read(path), path, "t", _G)
else
return nil, "File not found"
end
end
function _G.dofile(path)
assert(_G.loadfile(path))()
end
function _G.print(...)
local out = env.stdout
local tostring = _G.tostring -- Allow overriding
local t = {...}
for i = 1, select('#', ...) do
if i > 1 then
out:write("\t")
end
out:write(tostring(t[i]))
end
out:write("\n")
end
local errors, nilFiller = {}, {}
local function getError(message)
if message == nil then return nil end
local result = errors[message]
errors[message] = nil
if result == nilFiller then
result = nil
elseif result == nil then
result = message
end
return result
end
env.getError = getError
local e = {}
if select(2, pcall(error, e)) ~= e then
local function extractError(...)
local success, message = ...
if success then
return ...
else
return false, getError(message)
end
end
function _G.error(message, level)
level = level or 1
if level > 0 then level = level + 1 end
if type(message) ~= "string" then
local key = tostring({}) .. tostring(message)
if message == nil then message = nilFiller end
errors[key] = message
error(key, 0)
else
error(message, level)
end
end
function _G.pcall(func, ...)
return extractError(pcall(func, ...))
end
function _G.xpcall(func, handler)
return xpcall(func, function(result) return handler(getError(result)) end)
end
end
-- Setup other items
require "bsrocks.env.fixes"(env)
require "bsrocks.env.io"(env)
require "bsrocks.env.os"(env)
if not debug then
require "bsrocks.env.debug"(env)
else
_G.debug = debug
end
require "bsrocks.env.package"(env)
-- Copy functions across
addWithMeta(_ENV, _G)
_G._NATIVE = _ENV
return env
end
end
preload["bsrocks.env.fixes"] = function(...)
--- Various patches for LuaJ's
local type, pairs = type, pairs
local function copy(tbl)
local out = {}
for k, v in pairs(tbl) do out[k] = v end
return out
end
local function getmeta(obj)
local t = type(obj)
if t == "table" then
return getmetatable(obj)
elseif t == "string" then
return string
else
return nil
end
end
return function(env)
env._G.getmetatable = getmeta
if not table.pack().n then
local table = copy(table)
table.pack = function( ... ) return {n=select('#',...), ... } end
env._G.table = table
end
end
end
preload["bsrocks.env.debug"] = function(...)
--- Tiny ammount of the debug API
-- http://www.lua.org/manual/5.1/manual.html#5.9
local traceback = require "bsrocks.lib.utils".traceback
local function err(name)
return function() error(name .. " not implemented", 2) end
end
--- Really tacky getinfo
local function getInfo(thread, func, what)
if type(thread) ~= "thread" then
func = thread
end
local data = {
what = "lua",
source = "",
short_src = "",
linedefined = -1,
lastlinedefined = -1,
currentline = -1,
nups = -1,
name = "?",
namewhat = "",
activelines = {},
}
local t = type(func)
if t == "number" or t == "string" then
func = tonumber(func)
local _, source = pcall(error, "", 2 + func)
local name = source:gsub(":?[^:]*: *$", "", 1)
data.source = "@" .. name
data.short_src = name
local line = tonumber(source:match("^[^:]+:([%d]+):") or "")
if line then data.currentline = line end
elseif t == "function" then
-- We really can't do much
data.func = func
else
error("function or level expected", 2)
end
return data
end
return function(env)
local debug = {
getfenv = getfenv,
gethook = err("gethook"),
getinfo = getInfo,
getlocal = err("getlocal"),
gethook = err("gethook"),
getmetatable = env._G.getmetatable,
getregistry = err("getregistry"),
setfenv = setfenv,
sethook = err("sethook"),
setlocal = err("setlocal"),
setmetatable = setmetatable,
setupvalue = err("setupvalue"),
traceback = traceback,
}
env._G.debug = debug
end
end
preload["bsrocks.env.date"] = function(...)
--[[
The MIT License (MIT)
Copyright (c) 2013 Daurnimator
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
local strformat = string.format
local floor = math.floor
local function idiv(n, d) return floor(n / d) end
local mon_lengths = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
-- Number of days in year until start of month; not corrected for leap years
local months_to_days_cumulative = { 0 }
for i = 2, 12 do
months_to_days_cumulative [ i ] = months_to_days_cumulative [ i-1 ] + mon_lengths [ i-1 ]
end
-- For Sakamoto's Algorithm (day of week)
local sakamoto = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
local function is_leap ( y )
if (y % 4) ~= 0 then
return false
elseif (y % 100) ~= 0 then
return true
else
return (y % 400) == 0
end
end
local function year_length ( y )
return is_leap ( y ) and 366 or 365
end
local function month_length ( m, y )
if m == 2 then
return is_leap ( y ) and 29 or 28
else
return mon_lengths [ m ]
end
end
local function leap_years_since ( year )
return idiv ( year, 4 ) - idiv ( year, 100 ) + idiv ( year, 400 )
end
local function day_of_year ( day, month, year )
local yday = months_to_days_cumulative [ month ]
if month > 2 and is_leap ( year ) then
yday = yday + 1
end
return yday + day
end
local function day_of_week ( day, month, year )
if month < 3 then
year = year - 1
end
return ( year + leap_years_since ( year ) + sakamoto[month] + day ) % 7 + 1
end
local function borrow ( tens, units, base )
local frac = tens % 1
units = units + frac * base
tens = tens - frac
return tens, units
end
local function carry ( tens, units, base )
if units >= base then
tens = tens + idiv ( units, base )
units = units % base
elseif units < 0 then
tens = tens - 1 + idiv ( -units, base )
units = base - ( -units % base )
end
return tens, units
end
-- Modify parameters so they all fit within the "normal" range
local function normalise ( year, month, day, hour, min, sec )
-- `month` and `day` start from 1, need -1 and +1 so it works modulo
month, day = month - 1, day - 1
-- Convert everything (except seconds) to an integer
-- by propagating fractional components down.
year, month = borrow ( year, month, 12 )
-- Carry from month to year first, so we get month length correct in next line around leap years
year, month = carry ( year, month, 12 )
month, day = borrow ( month, day, month_length ( floor ( month + 1 ), year ) )
day, hour = borrow ( day, hour, 24 )
hour, min = borrow ( hour, min, 60 )
min, sec = borrow ( min, sec, 60 )
-- Propagate out of range values up
-- e.g. if `min` is 70, `hour` increments by 1 and `min` becomes 10
-- This has to happen for all columns after borrowing, as lower radixes may be pushed out of range
min, sec = carry ( min, sec, 60 ) -- TODO: consider leap seconds?
hour, min = carry ( hour, min, 60 )
day, hour = carry ( day, hour, 24 )
-- Ensure `day` is not underflowed
-- Add a whole year of days at a time, this is later resolved by adding months
-- TODO[OPTIMIZE]: This could be slow if `day` is far out of range
while day < 0 do
year = year - 1
day = day + year_length ( year )
end
year, month = carry ( year, month, 12 )
-- TODO[OPTIMIZE]: This could potentially be slow if `day` is very large
while true do
local i = month_length (month + 1, year)
if day < i then break end
day = day - i
month = month + 1
if month >= 12 then
month = 0
year = year + 1
end
end
-- Now we can place `day` and `month` back in their normal ranges
-- e.g. month as 1-12 instead of 0-11
month, day = month + 1, day + 1
return year, month, day, hour, min, sec
end
local function create(ts)
local year, month, day, hour, min, sec = normalise (1970, 1, 1, 0 , 0, ts)
return {
day = day,
month = month,
year = year,
hour = hour,
min = min,
sec = sec,
yday = day_of_year ( day , month , year ),
wday = day_of_week ( day , month , year )
}
end
local leap_years_since_1970 = leap_years_since ( 1970 )
local function timestamp(year, month, day, hour, min, sec )
year, month, day, hour, min, sec = normalise(year, month, day, hour, min, sec)
local days_since_epoch = day_of_year ( day, month, year )
+ 365 * ( year - 1970 )
-- Each leap year adds one day
+ ( leap_years_since ( year - 1 ) - leap_years_since_1970 ) - 1
return days_since_epoch * (60*60*24)
+ hour * (60*60)
+ min * 60
+ sec
end
local function timestampTbl(tbl)
return timestamp(tbl.year, tbl.month, tbl.day, tbl.hour or 0, tbl.min or 0, tbl.sec or 0)
end
local c_locale = {
abday = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" } ;
day = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" } ;
abmon = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" } ;
mon = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" } ;
am_pm = { "AM", "PM" } ;
}
--- ISO-8601 week logic
-- ISO 8601 weekday as number with Monday as 1 (1-7)
local function iso_8601_weekday ( wday )
if wday == 1 then
return 7
else
return wday - 1
end
end
local iso_8601_week do
-- Years that have 53 weeks according to ISO-8601
local long_years = { }
for _, v in ipairs {
4, 9, 15, 20, 26, 32, 37, 43, 48, 54, 60, 65, 71, 76, 82,
88, 93, 99, 105, 111, 116, 122, 128, 133, 139, 144, 150, 156, 161, 167,
172, 178, 184, 189, 195, 201, 207, 212, 218, 224, 229, 235, 240, 246, 252,
257, 263, 268, 274, 280, 285, 291, 296, 303, 308, 314, 320, 325, 331, 336,
342, 348, 353, 359, 364, 370, 376, 381, 387, 392, 398
} do
long_years [ v ] = true
end
local function is_long_year ( year )
return long_years [ year % 400 ]
end
function iso_8601_week ( self )
local wday = iso_8601_weekday ( self.wday )
local n = self.yday - wday
local year = self.year
if n < -3 then
year = year - 1
if is_long_year ( year ) then
return year, 53, wday
else
return year, 52, wday
end
elseif n >= 361 and not is_long_year ( year ) then
return year + 1, 1, wday
else
return year, idiv ( n + 10, 7 ), wday
end
end
end
--- Specifiers
local t = { }
function t:a ( locale )
return "%s", locale.abday [ self.wday ]
end
function t:A ( locale )
return "%s", locale.day [ self.wday ]
end
function t:b ( locale )
return "%s", locale.abmon [ self.month ]
end
function t:B ( locale )
return "%s", locale.mon [ self.month ]
end
function t:c ( locale )
return "%.3s %.3s%3d %.2d:%.2d:%.2d %d",
locale.abday [ self.wday ], locale.abmon [ self.month ],
self.day, self.hour, self.min, self.sec, self.year
end
-- Century
function t:C ( )
return "%02d", idiv ( self.year, 100 )
end
function t:d ( )
return "%02d", self.day
end
-- Short MM/DD/YY date, equivalent to %m/%d/%y
function t:D ( )
return "%02d/%02d/%02d", self.month, self.day, self.year % 100
end
function t:e ( )
return "%2d", self.day
end
-- Short YYYY-MM-DD date, equivalent to %Y-%m-%d
function t:F ( )
return "%d-%02d-%02d", self.year, self.month, self.day
end
-- Week-based year, last two digits (00-99)
function t:g ( )
return "%02d", iso_8601_week ( self ) % 100
end
-- Week-based year
function t:G ( )
return "%d", iso_8601_week ( self )
end
t.h = t.b
function t:H ( )
return "%02d", self.hour
end
function t:I ( )
return "%02d", (self.hour-1) % 12 + 1
end
function t:j ( )
return "%03d", self.yday
end
function t:m ( )
return "%02d", self.month
end
function t:M ( )
return "%02d", self.min
end
-- New-line character ('\n')
function t:n ( ) -- luacheck: ignore 212
return "\n"
end
function t:p ( locale )
return self.hour < 12 and locale.am_pm[1] or locale.am_pm[2]
end
-- TODO: should respect locale
function t:r ( locale )
return "%02d:%02d:%02d %s",
(self.hour-1) % 12 + 1, self.min, self.sec,
self.hour < 12 and locale.am_pm[1] or locale.am_pm[2]
end
-- 24-hour HH:MM time, equivalent to %H:%M
function t:R ( )
return "%02d:%02d", self.hour, self.min
end
function t:s ( )
return "%d", timestamp(self)
end
function t:S ( )
return "%02d", self.sec
end
-- Horizontal-tab character ('\t')
function t:t ( ) -- luacheck: ignore 212
return "\t"
end
-- ISO 8601 time format (HH:MM:SS), equivalent to %H:%M:%S
function t:T ( )
return "%02d:%02d:%02d", self.hour, self.min, self.sec
end
function t:u ( )
return "%d", iso_8601_weekday ( self.wday )
end
-- Week number with the first Sunday as the first day of week one (00-53)
function t:U ( )
return "%02d", idiv ( self.yday - self.wday + 7, 7 )
end
-- ISO 8601 week number (00-53)
function t:V ( )
return "%02d", select ( 2, iso_8601_week ( self ) )
end
-- Weekday as a decimal number with Sunday as 0 (0-6)
function t:w ( )
return "%d", self.wday - 1
end
-- Week number with the first Monday as the first day of week one (00-53)
function t:W ( )
return "%02d", idiv ( self.yday - iso_8601_weekday ( self.wday ) + 7, 7 )
end
-- TODO make t.x and t.X respect locale
t.x = t.D
t.X = t.T
function t:y ( )
return "%02d", self.year % 100
end
function t:Y ( )
return "%d", self.year
end
-- TODO timezones
function t:z ( ) -- luacheck: ignore 212
return "+0000"
end
function t:Z ( ) -- luacheck: ignore 212
return "GMT"
end
-- A literal '%' character.
t["%"] = function ( self ) -- luacheck: ignore 212
return "%%"
end
local function strftime ( format_string, timetable )
return ( string.gsub ( format_string, "%%([EO]?)(.)", function ( locale_modifier, specifier )
local func = t [ specifier ]
if func then
return strformat ( func ( timetable, c_locale ) )
else
error ( "invalid conversation specifier '%"..locale_modifier..specifier.."'", 3 )
end
end ) )
end
local function asctime ( timetable )
-- Equivalent to the format string "%c\n"
return strformat ( t.c ( timetable, c_locale ) )
end
return {
create = create,
timestamp = timestampTbl,
strftime = strftime,
asctime = asctime,
}
end
preload["bsrocks.env.ansi"] = function(...)
--- ANSI Support
-- @url https://en.wikipedia.org/wiki/ANSI_escape_code
local write, term = write, term
local type = type
local defBack, defText = term.getBackgroundColour(), term.getTextColour()
local setBack, setText = term.setBackgroundColour, term.setTextColour
local function create(func, col)
return function() func(col) end
end
local function clamp(val, min, max)
if val > max then
return max
elseif val < min then
return min
else
return val
end
end
local function move(x, y)
local cX, cY = term.getCursorPos()
local w, h = term.getSize()
term.setCursorPos(clamp(x + cX, 1, w), clamp(y + cY, 1, h))
end
local cols = {
['0'] = function()
setBack(defBack)
setText(defText)
end,
['7'] = function() -- Swap colours
local curBack = term.getBackgroundColour()
term.setBackgroundColour(term.getTextColour())
term.setText(curBack)
end,
['30'] = create(setText, colours.black),
['31'] = create(setText, colours.red),
['32'] = create(setText, colours.green),
['33'] = create(setText, colours.orange),
['34'] = create(setText, colours.blue),
['35'] = create(setText, colours.purple),
['36'] = create(setText, colours.cyan),
['37'] = create(setText, colours.lightGrey),
['40'] = create(setBack, colours.black),
['41'] = create(setBack, colours.red),
['42'] = create(setBack, colours.green),
['43'] = create(setBack, colours.orange),
['44'] = create(setBack, colours.blue),
['45'] = create(setBack, colours.purple),
['46'] = create(setBack, colours.cyan),
['47'] = create(setBack, colours.lightGrey),
['90'] = create(setText, colours.grey),
['91'] = create(setText, colours.red),
['92'] = create(setText, colours.lime),
['93'] = create(setText, colours.yellow),
['94'] = create(setText, colours.lightBlue),
['95'] = create(setText, colours.pink),
['96'] = create(setText, colours.cyan),
['97'] = create(setText, colours.white),
['100'] = create(setBack, colours.grey),
['101'] = create(setBack, colours.red),
['102'] = create(setBack, colours.lime),
['103'] = create(setBack, colours.yellow),
['104'] = create(setBack, colours.lightBlue),
['105'] = create(setBack, colours.pink),
['106'] = create(setBack, colours.cyan),
['107'] = create(setBack, colours.white),
}
local savedX, savedY = 1, 1
local actions = {
m = function(args)
for _, colour in ipairs(args) do
local func = cols[colour]
if func then func() end
end
end,
['A'] = function(args)
local y = tonumber(args[1])
if not y then return end
move(0, -y)
end,
['B'] = function(args)
local y = tonumber(args[1])
if not y then return end
move(0, y)
end,
['C'] = function(args)
local x = tonumber(args[1])
if not x then return end
move(x, 0)
end,
['D'] = function(args)
local x = tonumber(args[1])
if not x then return end
move(-x, 0)
end,
['H'] = function(args)
local x, y = tonumber(args[1]), tonumber(args[2])
if not x or not y then return end
local w, h = term.getSize()
term.setCursorPos(clamp(x, 1, w), clamp(y, 1, h))
end,
['J'] = function(args)
-- TODO: Support other modes
if args[1] == "2" then term.clear() end
end,
['s'] = function()
savedX, savedY = term.getCursorPos()
end,
['u'] = function()
term.setCursorPos(savedX, savedY)
end
}
local function writeAnsi(str)
if stdout and stdout.isPiped then
return stdout.write(text)
end
if type(str) ~= "string" then
error("bad argument #1 (string expected, got " .. type(ansi) .. ")", 2)
end
local offset = 1
while offset <= #str do
local start, finish = str:find("\27[", offset, true)
if start then
if offset < start then
write(str:sub(offset, start - 1))
end
local remaining = true
local args, n = {}, 0
local mode
while remaining do
finish = finish + 1
start = finish
while true do
local s = str:sub(finish, finish)
if s == ";" then
break
elseif (s >= 'A' and s <= 'Z') or (s >= 'a' and s <= 'z') then
mode = s
remaining = false
break
elseif s == "" or s == nil then
error("Invalid escape sequence at " .. s)
else
finish = finish + 1
end
end
n = n + 1
args[n] = str:sub(start, finish - 1)
end
local func = mode and actions[mode]
if func then func(args) end
offset = finish + 1
elseif offset == 1 then
write(str)
return
else
write(str:sub(offset))
return
end
end
end
function printAnsi(...)
local limit = select("#", ...)
for n = 1, limit do
local s = tostring(select(n, ... ))
if n < limit then
s = s .. "\t"
end
writeAnsi(s)
end
write("\n")
end
return {
write = writeAnsi,
print = printAnsi,
}
end
preload["bsrocks.downloaders.tree"] = function(...)
local error = require "bsrocks.lib.utils".error
local function callback(success, path, count, total)
if not success then
local x, y = term.getCursorPos()
term.setCursorPos(1, y)
term.clearLine()
printError("Cannot download " .. path)
end
local x, y = term.getCursorPos()
term.setCursorPos(1, y)
term.clearLine()
write(("Downloading: %s/%s (%s%%)"):format(count, total, count / total * 100))
end
local tries = require "bsrocks.lib.settings".tries
--- Download individual files
-- @tparam string prefix The url prefix to use
-- @tparam table files The list of files to download
-- @tparam int tries Number of times to attempt to download
local function tree(prefix, files)
local result = {}
local count = 0
local total = #files
if total == 0 then
print("No files to download")
return {}
end
-- Download a file and store it in the tree
local errored = false
local function download(path)
local contents
-- Attempt to download the file
for i = 1, tries do
local url = (prefix .. path):gsub(' ','%%20')
local f = http.get(url)
if f then
count = count + 1
local out, n = {}, 0
for line in f.readLine do
n = n + 1
out[n] = line
end
result[path] = out
f.close()
callback(true, path, count, total)
return
elseif errored then
-- Just abort
return
end
end
errored = true
callback(false, path, count, total)
end
local callbacks = {}
for i, file in ipairs(files) do
callbacks[i] = function() download(file) end
end
parallel.waitForAll(unpack(callbacks))
print()
if errored then
error("Cannot download " .. prefix)
end
return result
end
return tree
end
preload["bsrocks.downloaders"] = function(...)
local error = require "bsrocks.lib.utils".error
local tree = require "bsrocks.downloaders.tree"
local downloaders = {
-- GitHub
function(source, files)
local url = source.url
if not url then return end
local repo = url:match("git://github%.com/([^/]+/[^/]+)$") or url:match("https?://github%.com/([^/]+/[^/]+)$")
local branch = source.branch or source.tag or "master"
if repo then
repo = repo:gsub("%.git$", "")
else
-- If we have the archive then we can also fetch from GitHub
repo, branch = url:match("https?://github%.com/([^/]+/[^/]+)/archive/(.*).tar.gz")
if not repo then return end
end
if not files then
return true
end
print("Downloading " .. repo .. "@" .. branch)
return tree('https://raw.github.com/'..repo..'/'..branch..'/', files)
end,
function(source, files)
local url = source.single
if not url then return end
if not files then
return true
end
if #files ~= 1 then error("Expected 1 file for single, got " .. #files, 0) end
local handle, msg = http.get(url)
if not handle then
error(msg or "Cannot download " .. url, 0)
end
local contents = handle.readAll()
handle.close()
return { [files[1]] = contents }
end
}
return function(source, files)
for _, downloader in ipairs(downloaders) do
local result = downloader(source, files)
if result then
return result
end
end
return false
end
end
preload["bsrocks.commands.search"] = function(...)
local match = require "bsrocks.lib.match"
local rockspec = require "bsrocks.rocks.rockspec"
local manifest = require "bsrocks.rocks.manifest"
local function execute(search)
if not search then error("Expected <name>", 0) end
search = search:lower()
local names, namesN = {}, 0
local all, allN = {}, 0
for server, manifest in pairs(manifest.fetchAll()) do
for name, _ in pairs(manifest.repository) do
-- First try a loose search
local version = rockspec.latestVersion(manifest, name)
if name:find(search, 1, true) then
namesN = namesN + 1
names[namesN] = { name, version }
all = nil
elseif namesN == 0 then
allN = allN + 1
all[allN] = { name, version }
end
end
end
-- Now try a fuzzy search
if namesN == 0 then
printError("Could not find '" .. search .. "', trying a fuzzy search")
for _, name in ipairs(all) do
if match(name[1], search) > 0 then
namesN = namesN + 1
names[namesN] = name
end
end
end
-- Print out all found items + version
if namesN == 0 then
error("Cannot find " .. search, 0)
else
for i = 1, namesN do
local item = names[i]
print(item[1] .. ": " .. item[2])
end
end
end
local description = [[
<name> The name of the package to search for.
If the package cannot be found, it will query for packages with similar names.
]]
return {
name = "search",
help = "Search for a package",
description = description,
syntax = "<name>",
execute = execute,
}
end
preload["bsrocks.commands.repl"] = function(...)
local env = require "bsrocks.env"
local serialize = require "bsrocks.lib.dump"
local parse = require "bsrocks.lib.parse"
local function execute(...)
local running = true
local env = env()
local thisEnv = env._G
thisEnv.exit = setmetatable({}, {
__tostring = function() return "Call exit() to exit" end,
__call = function() running = false end,
})
-- We need to pass through a secondary function to prevent tail calls
thisEnv._noTail = function(...) return ... end
thisEnv.arg = { [0] = "repl", ... }
-- As per @demhydraz's suggestion. Because the prompt uses Out[n] as well
local output = {}
thisEnv.Out = output
local inputColour, outputColour, textColour = colours.green, colours.cyan, term.getTextColour()
local codeColour, pointerColour = colours.lightGrey, colours.lightBlue
if not term.isColour() then
inputColour = colours.white
outputColour = colours.white
codeColour = colours.white
pointerColour = colours.white
end
local autocomplete = nil
if not settings or settings.get("lua.autocomplete") then
autocomplete = function(line)
local start = line:find("[a-zA-Z0-9_%.]+$")
if start then
line = line:sub(start)
end
if #line > 0 then
return textutils.complete(line, thisEnv)
end
end
end
local history = {}
local counter = 1
--- Prints an output and sets the output variable
local function setOutput(out, length)
thisEnv._ = out
thisEnv['_' .. counter] = out
output[counter] = out
term.setTextColour(outputColour)
write("Out[" .. counter .. "]: ")
term.setTextColour(textColour)
if type(out) == "table" then
local meta = getmetatable(out)
if type(meta) == "table" and type(meta.__tostring) == "function" then
print(tostring(out))
else
print(serialize(out, length))
end
else
print(serialize(out))
end
end
--- Handle the result of the function
local function handle(forcePrint, success, ...)
if success then
local len = select('#', ...)
if len == 0 then
if forcePrint then
setOutput(nil)
end
elseif len == 1 then
setOutput(...)
else
setOutput({...}, len)
end
else
printError(...)
end
end
local function handleError(lines, line, column, message)
local contents = lines[line]
term.setTextColour(codeColour)
print(" " .. contents)
term.setTextColour(pointerColour)
print((" "):rep(column) .. "^ ")
printError(" " .. message)
end
local function execute(lines, force)
local buffer = table.concat(lines, "\n")
local forcePrint = false
local func, err = load(buffer, "lua", "t", thisEnv)
local func2, err2 = load("return " .. buffer, "lua", "t", thisEnv)
if not func then
if func2 then
func = load("return _noTail(" .. buffer .. ")", "lua", "t", thisEnv)
forcePrint = true
else
local success, tokens = pcall(parse.lex, buffer)
if not success then
local line, column, resumable, message = tokens:match("(%d+):(%d+):([01]):(.+)")
if line then
if line == #lines and column > #lines[line] and resumable == 1 then
return false
else
handleError(lines, tonumber(line), tonumber(column), message)
return true
end
else
printError(tokens)
return true
end
end
local success, message = pcall(parse.parse, tokens)
if not success then
if not force and tokens.pointer >= #tokens.tokens then
return false
else
local token = tokens.tokens[tokens.pointer]
handleError(lines, token.line, token.char, message)
return true
end
end
end
elseif func2 then
func = load("return _noTail(" .. buffer .. ")", "lua", "t", thisEnv)
end
if func then
handle(forcePrint, pcall(func))
counter = counter + 1
else
printError(err)
end
return true
end
local lines = {}
local input = "In [" .. counter .. "]: "
local isEmpty = false
while running do
term.setTextColour(inputColour)
write(input)
term.setTextColour(textColour)
local line = read(nil, history, autocomplete)
if not line then break end
if #line:gsub("%s", "") > 0 then
for i = #history, 1, -1 do
if history[i] == line then
table.remove(history, i)
break
end
end
history[#history + 1] = line
lines[#lines + 1] = line
isEmpty = false
if execute(lines) then
lines = {}
input = "In [" .. counter .. "]: "
else
input = (" "):rep(#tostring(counter) + 3) .. "... "
end
else
execute(lines, true)
lines = {}
isEmpty = false
input = "In [" .. counter .. "]: "
end
end
for _, v in pairs(env.cleanup) do v() end
end
local description = [[
This is almost identical to the built in Lua program with some simple differences.
Scripts are run in an environment similar to the exec command.
The result of the previous outputs are also stored in variables of the form _idx (the last result is also stored in _). For example: if Out[1] = 123 then _1 = 123 and _ = 123
]]
return {
name = "repl",
help = "Run a Lua repl in an emulated environment",
syntax = "",
description = description,
execute = execute,
}
end
preload["bsrocks.commands.remove"] = function(...)
local install = require "bsrocks.rocks.install"
local rockspec = require "bsrocks.rocks.rockspec"
local description = [[
<name> The name of the package to install
Removes a package. This does not remove its dependencies
]]
return {
name = "remove",
help = "Removes a package",
syntax = "<name>",
description = description,
execute = function(name, version)
if not name then error("Expected name", 0) end
name = name:lower()
local installed, installedPatches = install.getInstalled()
local rock, patch = installed[name], installedPatches[name]
if not rock then error(name .. " is not installed", 0) end
install.remove(rock, patch)
end
}
end
preload["bsrocks.commands.list"] = function(...)
local install = require "bsrocks.rocks.install"
local printColoured = require "bsrocks.lib.utils".printColoured
local function execute()
for _, data in pairs(install.getInstalled()) do
if not data.builtin then
print(data.package .. ": " .. data.version)
if data.description and data.description.summary then
printColoured(" " .. data.description.summary, colours.lightGrey)
end
end
end
end
return {
name = "list",
help = "List installed packages",
syntax = "",
execute = execute
}
end
preload["bsrocks.commands.install"] = function(...)
local install = require "bsrocks.rocks.install"
local description = [[
<name> The name of the package to install
[version] The version of the package to install
Installs a package and all dependencies. This will also
try to upgrade a package if required.
]]
return {
name = "install",
help = "Install a package",
syntax = "<name> [version]",
description = description,
execute = function(name, version)
if not name then error("Expected name", 0) end
install.install(name, version)
end
}
end
preload["bsrocks.commands.exec"] = function(...)
local env = require "bsrocks.env"
local settings = require "bsrocks.lib.settings"
local description = [[
<file> The file to execute relative to the current directory.
[args...] Arguments to pass to the program.
This will execute the program in an emulation of Lua 5.1's environment.
Please note that the environment is not a perfect emulation.
]]
return {
name = "exec",
help = "Execute a command in the emulated environment",
syntax = "<file> [args...]",
description = description,
execute = function(file, ...)
if not file then error("Expected file", 0) end
if file:sub(1, 1) == "@" then
file = file:sub(2)
local found
for _, path in ipairs(settings.binPath) do
path = path:gsub("%%{(%a+)}", settings):gsub("%?", file)
if fs.exists(path) then
found = path
break
end
end
file = found or shell.resolveProgram(file) or file
else
file = shell.resolve(file)
end
local env = env()
local thisEnv = env._G
thisEnv.arg = {[-2] = "/" .. shell.getRunningProgram(), [-1] = "exec", [0] = "/" .. file, ... }
local loaded, msg = loadfile(file, thisEnv)
if not loaded then error(msg, 0) end
local args = {...}
local success, msg = xpcall(
function() return loaded(unpack(args)) end,
function(msg)
msg = env.getError(msg)
if type(msg) == "string" then
local code = msg:match("^Exit code: (%d+)")
if code and code == "0" then return "<nop>" end
end
if msg == nil then
return "nil"
else
msg = tostring(msg)
end
return thisEnv.debug.traceback(msg, 2)
end
)
for _, v in pairs(env.cleanup) do v() end
if not success and msg ~= "<nop>" then
if msg == "nil" then msg = nil end
error(msg, 0)
end
end,
}
end
preload["bsrocks.commands.dumpsettings"] = function(...)
local fileWrapper = require "bsrocks.lib.files"
local serialize = require "bsrocks.lib.serialize"
local settings = require "bsrocks.lib.settings"
local utils = require "bsrocks.lib.utils"
return {
name = "dump-settings",
help = "Dump all settings",
syntax = "",
description = "Dump all settings to a .bsrocks file. This can be changed to load various configuration options.",
execute = function()
local dumped = serialize.serialize(settings)
utils.log("Dumping to .bsrocks")
fileWrapper.write(".bsrocks", dumped)
end,
}
end
preload["bsrocks.commands.desc"] = function(...)
local dependencies = require "bsrocks.rocks.dependencies"
local download = require "bsrocks.downloaders"
local install = require "bsrocks.rocks.install"
local patchspec = require "bsrocks.rocks.patchspec"
local rockspec = require "bsrocks.rocks.rockspec"
local settings = require "bsrocks.lib.settings"
local utils = require "bsrocks.lib.utils"
local servers = settings.servers
local printColoured, writeColoured = utils.printColoured, utils.writeColoured
local function execute(name)
if not name then error("Expected <name>", 0) end
name = name:lower()
local installed, installedPatches = install.getInstalled()
local isInstalled = true
local spec, patchS = installed[name], installedPatches[name]
if not spec then
isInstalled = false
local manifest = rockspec.findRockspec(name)
if not manifest then error("Cannot find '" .. name .. "'", 0) end
local patchManifest = patchspec.findPatchspec(name)
local version
if patchManifest then
version = patchManifest.patches[name]
else
version = rockspec.latestVersion(manifest, name, constraints)
end
spec = rockspec.fetchRockspec(manifest.server, name, version)
patchS = patchManifest and patchspec.fetchPatchspec(patchManifest.server, name)
end
write(name .. ": " .. spec.version .. " ")
if spec.builtin then
writeColoured("Built In", colours.magenta)
elseif isInstalled then
writeColoured("Installed", colours.green)
else
writeColoured("Not installed", colours.red)
end
if patchS then
writeColoured(" (+Patchspec)", colours.lime)
end
print()
local desc = spec.description
if desc then
if desc.summary then printColoured(desc.summary, colours.cyan) end
if desc.detailed then
local detailed = desc.detailed
local ident = detailed:match("^(%s+)")
if ident then
detailed = detailed:sub(#ident + 1):gsub("\n" .. ident, "\n")
end
-- Remove leading and trailing whitespace
detailed = detailed:gsub("^\n+", ""):gsub("%s+$", "")
printColoured(detailed, colours.white)
end
if desc.homepage then
printColoured("URL: " .. desc.homepage, colours.lightBlue)
end
end
if not isInstalled then
local error, issues = install.findIssues(spec, patchS)
if #issues > 0 then
printColoured("Issues", colours.orange)
if error then
printColoured("This package is incompatible", colors.red)
end
for _, v in ipairs(issues) do
local color = colors.yellow
if v[2] then color = colors.red end
printColoured(" " .. v[1], color)
end
end
end
local deps = spec.dependencies
if patchS and patchS.dependencies then
deps = patchS.dependencies
end
if deps and #deps > 0 then
printColoured("Dependencies", colours.orange)
local len = 0
for _, deps in ipairs(deps) do len = math.max(len, #deps) end
len = len + 1
for _, deps in ipairs(deps) do
local dependency = dependencies.parseDependency(deps)
local name = dependency.name
local current = installed[name]
write(" " .. deps .. (" "):rep(len - #deps))
if current then
local version = dependencies.parseVersion(current.version)
if not dependencies.matchConstraints(version, dependency.constraints) then
printColoured("Wrong version", colours.yellow)
elseif current.builtin then
printColoured("Built In", colours.magenta)
else
printColoured("Installed", colours.green)
end
else
printColoured("Not installed", colours.red)
end
end
end
end
local description = [[
<name> The name of the package to search for.
Prints a description about the package, listing its description, dependencies and other useful information.
]]
return {
name = "desc",
help = "Print a description about a package",
description = description,
syntax = "<name>",
execute = execute,
}
end
preload["bsrocks.commands.admin.make"] = function(...)
local fileWrapper = require "bsrocks.lib.files"
local log = require "bsrocks.lib.utils".log
local patchDirectory = require "bsrocks.lib.settings".patchDirectory
local patchspec = require "bsrocks.rocks.patchspec"
local serialize = require "bsrocks.lib.serialize"
local function execute(...)
local patched, force
if select("#", ...) == 0 then
force = false
patched = patchspec.getAll()
else
force = true
patched = {}
for _, name in pairs({...}) do
name = name:lower()
local file = fs.combine(patchDirectory, "rocks/" .. name .. ".patchspec")
if not fs.exists(file) then error("No such patchspec " .. name, 0) end
patched[name] = serialize.unserialize(fileWrapper.read(file))
end
end
for name, data in pairs(patched) do
local original = fs.combine(patchDirectory, "rocks-original/" .. name)
local changed = fs.combine(patchDirectory, "rocks-changes/" .. name)
local patch = fs.combine(patchDirectory, "rocks/" .. name)
local info = patch .. ".patchspec"
log("Making " .. name)
fileWrapper.assertExists(original, "original sources for " .. name, 0)
fileWrapper.assertExists(changed, "changed sources for " .. name, 0)
fileWrapper.assertExists(info, "patchspec for " .. name, 0)
local data = serialize.unserialize(fileWrapper.read(info))
local originalSources = fileWrapper.readDir(original, fileWrapper.readLines)
local changedSources = fileWrapper.readDir(changed, fileWrapper.readLines)
local files, patches, added, removed = patchspec.makePatches(originalSources, changedSources)
data.patches = patches
data.added = added
data.removed = removed
fileWrapper.writeDir(patch, files, fileWrapper.writeLines)
fileWrapper.write(info, serialize.serialize(data))
end
end
local description = [[
[name] The name of the package to create patches for. Otherwise all packages will make their patches.
]]
return {
name = "make-patches",
help = "Make patches for a package",
syntax = "[name]...",
description = description,
execute = execute
}
end
preload["bsrocks.commands.admin.fetch"] = function(...)
local download = require "bsrocks.downloaders"
local fileWrapper = require "bsrocks.lib.files"
local log = require "bsrocks.lib.utils".log
local patchspec = require "bsrocks.rocks.patchspec"
local rockspec = require "bsrocks.rocks.rockspec"
local serialize = require "bsrocks.lib.serialize"
local patchDirectory = require "bsrocks.lib.settings".patchDirectory
local function execute(...)
local patched, force
if select("#", ...) == 0 then
force = false
patched = patchspec.getAll()
else
force = true
patched = {}
for _, name in pairs({...}) do
name = name:lower()
local file = fs.combine(patchDirectory, "rocks/" .. name .. ".patchspec")
if not fs.exists(file) then error("No such patchspec " .. name, 0) end
patched[name] = serialize.unserialize(fileWrapper.read(file))
end
end
local hasChanged = false
for name, patchS in pairs(patched) do
local dir = fs.combine(patchDirectory, "rocks-original/" .. name)
if force or not fs.isDir(dir) then
hasChanged = true
log("Fetching " .. name)
fs.delete(dir)
local version = patchS.version
if not patchS.version then
error("Patchspec" .. name .. " has no version", 0)
end
local manifest = rockspec.findRockspec(name)
if not manifest then
error("Cannot find '" .. name .. "'", 0)
end
local rock = rockspec.fetchRockspec(manifest.server, name, patchS.version)
local files = rockspec.extractFiles(rock)
if #files == 0 then error("No files for " .. name .. "-" .. version, 0) end
local downloaded = download(patchspec.extractSource(rock, patchS), files)
if not downloaded then error("Cannot find downloader for " .. rock.source.url, 0) end
for name, contents in pairs(downloaded) do
fileWrapper.writeLines(fs.combine(dir, name), contents)
end
fs.delete(fs.combine(patchDirectory, "rocks-changes/" .. name))
end
end
if not hasChanged then
error("No packages to fetch", 0)
end
print("Run 'apply-patches' to apply")
end
local description = [[
[name] The name of the package to fetch. Otherwise all un-fetched packages will be fetched.
]]
return {
name = "fetch",
help = "Fetch a package for patching",
syntax = "[name]...",
description = description,
execute = execute,
}
end
preload["bsrocks.commands.admin.apply"] = function(...)
local fileWrapper = require "bsrocks.lib.files"
local log = require "bsrocks.lib.utils".log
local patchDirectory = require "bsrocks.lib.settings".patchDirectory
local patchspec = require "bsrocks.rocks.patchspec"
local serialize = require "bsrocks.lib.serialize"
local function execute(...)
local patched, force
if select("#", ...) == 0 then
force = false
patched = patchspec.getAll()
elseif select("#", ...) == 1 and (... == "-f" or ... == "--force") then
force = true
patched = patchspec.getAll()
else
force = true
patched = {}
for _, name in pairs({...}) do
name = name:lower()
local file = fs.combine(patchDirectory, "rocks/" .. name .. ".patchspec")
if not fs.exists(file) then error("No such patchspec " .. name, 0) end
patched[name] = serialize.unserialize(fileWrapper.read(file))
end
end
local hasChanged = false
for name, data in pairs(patched) do
local original = fs.combine(patchDirectory, "rocks-original/" .. name)
local patch = fs.combine(patchDirectory, "rocks/" .. name)
local changed = fs.combine(patchDirectory, "rocks-changes/" .. name)
if force or not fs.isDir(changed) then
hasChanged = true
log("Applying " .. name)
fileWrapper.assertExists(original, "original sources for " .. name, 0)
fs.delete(changed)
local originalSources = fileWrapper.readDir(original, fileWrapper.readLines)
local replaceSources = {}
if fs.exists(patch) then replaceSources = fileWrapper.readDir(patch, fileWrapper.readLines) end
local changedSources = patchspec.applyPatches(
originalSources, replaceSources,
data.patches or {}, data.added or {}, data.removed or {}
)
fileWrapper.writeDir(changed, changedSources, fileWrapper.writeLines)
end
end
if not hasChanged then
error("No packages to patch", 0)
end
end
local description = [[
[name] The name of the package to apply. Otherwise all un-applied packages will have their patches applied.
]]
return {
name = "apply-patches",
help = "Apply patches for a package",
syntax = "[name]...",
description = description,
execute = execute,
}
end
preload["bsrocks.commands.admin.addrockspec"] = function(...)
local fileWrapper = require "bsrocks.lib.files"
local manifest = require "bsrocks.rocks.manifest"
local patchDirectory = require "bsrocks.lib.settings".patchDirectory
local rockspec = require "bsrocks.rocks.rockspec"
local serialize = require "bsrocks.lib.serialize"
local function execute(name, version)
if not name then error("Expected name", 0) end
name = name:lower()
local rock = rockspec.findRockspec(name)
if not rock then
error("Cannot find '" .. name .. "'", 0)
end
if not version then
version = rockspec.latestVersion(rock, name)
end
local data = {}
local info = fs.combine(patchDirectory, "rocks/" .. name .. "-" .. version .. ".rockspec")
if fs.exists(info) then
data = serialize.unserialize(fileWrapper.read(info))
if data.version == version then
error("Already at version " .. version, 0)
end
else
data = rockspec.fetchRockspec(rock.server, name, version)
end
data.version = version
fileWrapper.write(info, serialize.serialize(data))
local locManifest, locPath = manifest.loadLocal()
local versions = locManifest.repository[name]
if not versions then
versions = {}
locManifest.repository[name] = versions
end
versions[version] = { { arch = "rockspec" } }
fileWrapper.write(locPath, serialize.serialize(locManifest))
print("Added rockspec. Feel free to edit away!")
end
local description = [[
<name> The name of the package
[version] The version to use
Refreshes a rockspec file to the original.
]]
return {
name = "add-rockspec",
help = "Add or update a rockspec",
syntax = "<name> [version]",
description = description,
execute = execute,
}
end
preload["bsrocks.commands.admin.addpatchspec"] = function(...)
local fileWrapper = require "bsrocks.lib.files"
local manifest = require "bsrocks.rocks.manifest"
local patchDirectory = require "bsrocks.lib.settings".patchDirectory
local rockspec = require "bsrocks.rocks.rockspec"
local serialize = require "bsrocks.lib.serialize"
local function execute(name, version)
if not name then error("Expected name", 0) end
name = name:lower()
local rock = rockspec.findRockspec(name)
if not rock then
error("Cannot find '" .. name .. "'", 0)
end
if not version then
version = rockspec.latestVersion(rock, name)
end
local data = {}
local info = fs.combine(patchDirectory, "rocks/" .. name .. ".patchspec")
if fs.exists(info) then
data = serialize.unserialize(fileWrapper.read(info))
end
if data.version == version then
error("Already at version " .. version, 0)
end
data.version = version
fileWrapper.write(info, serialize.serialize(data))
fs.delete(fs.combine(patchDirectory, "rocks-original/" .. name))
local locManifest, locPath = manifest.loadLocal()
locManifest.patches[name] = version
fileWrapper.write(locPath, serialize.serialize(locManifest))
print("Run 'fetch " .. name .. "' to download files")
end
local description = [[
<name> The name of the package
[version] The version to use
Adds a patchspec file, or sets the version of an existing one.
]]
return {
name = "add-patchspec",
help = "Add or update a package for patching",
syntax = "<name> [version]",
description = description,
execute = execute,
}
end
preload["bsrocks.bin.bsrocks"] = function(...)
local commands = { }
local function addCommand(command)
commands[command.name] = command
if command.alias then
for _, v in ipairs(command.alias) do
commands[v] = command
end
end
end
local utils = require "bsrocks.lib.utils"
local printColoured, printIndent = utils.printColoured, utils.printIndent
local patchDirectory = require "bsrocks.lib.settings".patchDirectory
-- Primary packages
addCommand(require "bsrocks.commands.desc")
addCommand(require "bsrocks.commands.dumpsettings")
addCommand(require "bsrocks.commands.exec")
addCommand(require "bsrocks.commands.install")
addCommand(require "bsrocks.commands.list")
addCommand(require "bsrocks.commands.remove")
addCommand(require "bsrocks.commands.repl")
addCommand(require "bsrocks.commands.search")
-- Install admin packages if we have a patch directory
if fs.exists(patchDirectory) then
addCommand(require "bsrocks.commands.admin.addpatchspec")
addCommand(require "bsrocks.commands.admin.addrockspec")
addCommand(require "bsrocks.commands.admin.apply")
addCommand(require "bsrocks.commands.admin.fetch")
addCommand(require "bsrocks.commands.admin.make")
end
local function getCommand(command)
local foundCommand = commands[command]
if not foundCommand then
-- No such command, print a list of suggestions
printError("Cannot find '" .. command .. "'.")
local match = require "bsrocks.lib.match"
local printDid = false
for cmd, _ in pairs(commands) do
if match(cmd, command) > 0 then
if not printDid then
printColoured("Did you mean: ", colours.yellow)
printDid = true
end
printColoured(" " .. cmd, colours.orange)
end
end
error("No such command", 0)
else
return foundCommand
end
end
addCommand({
name = "help",
help = "Provide help for a command",
syntax = "[command]",
description = " [command] The command to get help for. Leave blank to get some basic help for all commands.",
execute = function(cmd)
if cmd then
local command = getCommand(cmd)
print(command.help)
if command.syntax ~= "" then
printColoured("Synopsis", colours.orange)
printColoured(" " .. command.name .. " " .. command.syntax, colours.lightGrey)
end
if command.description then
printColoured("Description", colours.orange)
local description = command.description:gsub("^\n+", ""):gsub("\n+$", "")
if term.isColor() then term.setTextColour(colours.lightGrey) end
for line in (description .. "\n"):gmatch("([^\n]*)\n") do
local _, indent = line:find("^(%s*)")
printIndent(line:sub(indent + 1), indent)
end
if term.isColor() then term.setTextColour(colours.white) end
end
else
printColoured("bsrocks <command> [args]", colours.cyan)
printColoured("Available commands", colours.lightGrey)
for _, command in pairs(commands) do
print(" " .. command.name .. " " .. command.syntax)
printColoured(" " .. command.help, colours.lightGrey)
end
end
end
})
-- Default to printing help messages
local cmd = ...
if not cmd or cmd == "-h" or cmd == "--help" then
cmd = "help"
elseif select(2, ...) == "-h" or select(2, ...) == "--help" then
return getCommand("help").execute(cmd)
end
local foundCommand = getCommand(cmd)
local args = {...}
return foundCommand.execute(select(2, unpack(args)))
end
if not shell or type(... or nil) == 'table' then
local tbl = ... or {}
tbl.require = require tbl.preload = preload
return tbl
else
return preload["bsrocks.bin.bsrocks"](...)
end
--[[
The MIT License (MIT)
Copyright (c) 2015-2016 SquidDev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Diff Match and Patch
Copyright 2006 Google Inc.
http://code.google.com/p/google-diff-match-patch/
Based on the JavaScript implementation by Neil Fraser
Ported to Lua by Duncan Cross
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]
local e={}local t,a,o=require,{},{startup=e}
local function i(n)local s=o[n]
if s~=nil then if s==e then
error("loop or previous error loading module '"..n..
"'",2)end;return s end;o[n]=e;local h=a[n]if h then s=h(n)elseif t then s=t(n)else
error("cannot load '"..n.."'",2)end;if s==nil then s=true end;o[n]=s;return s end
a["bsrocks.rocks.rockspec"]=function(...)local n=i"bsrocks.rocks.dependencies"
local s=i"bsrocks.lib.files"local h=i"bsrocks.rocks.manifest"
local r=i"bsrocks.lib.serialize".unserialize;local d=i"bsrocks.lib.utils"local l,u,c,m=d.log,d.warn,d.verbose,d.error;local f={}local function w(b)
for g,h in
pairs(h.fetchAll())do if h.repository and h.repository[b]then return h end end;return end
local function y(h,b,g)
local k=h.repository[b]if not k then m("Cannot find "..b)end;local q
for b,j in pairs(k)do
local x=n.parseVersion(b)if g then
if n.matchConstraints(x,g)then if not q or x>q then q=x end end elseif not q or x>q then q=x end end
if not q then m("Cannot find version for "..b)end;return q.name end
local function p(b,g,k)local q=g.."-"..k;local j=f[q]if j then return j end
l("Fetching rockspec "..q)c("Using '"..
b..g..'-'..k..".rockspec' for "..q)
local x=http.get(
b..g..'-'..k..'.rockspec')if not x then
m("Canot fetch "..g.."-"..k.." from "..b,0)end;local z=x.readAll()x.close()j=r(z)
f[q]=j;return j end
local function v(b,g)local k,q={},0;g=g or{}local j=b.build
if j then
if j.modules then for x,z in pairs(j.modules)do
if not g[z]then q=q+1;k[q]=z end end end
if j.install then for x,z in pairs(j.install)do
for x,_ in pairs(z)do if not g[_]then q=q+1;k[q]=_ end end end end end;return k end;return{findRockspec=w,fetchRockspec=p,latestVersion=y,extractFiles=v}end
a["bsrocks.rocks.patchspec"]=function(...)local n=i"bsrocks.lib.diff"local s=i"bsrocks.lib.files"
local h=i"bsrocks.rocks.manifest"local r=i"bsrocks.lib.patch"
local d=i"bsrocks.lib.settings".patchDirectory;local l=i"bsrocks.lib.serialize".unserialize
local u=i"bsrocks.lib.utils"local c,m,f,w=u.log,u.warn,u.verbose,u.error;local y={}local function p(z)
for _,h in pairs(h.fetchAll())do if h.patches and
h.patches[z]then return h end end;return end
local function v(z,_)
local E=y[_]or false;if E then return E end;c("Fetching patchspec ".._)
f("Using '"..z.._..
".patchspec' for ".._)local T=http.get(z.._..'.patchspec')
if not T then w("Canot fetch ".._..
" from "..z,0)end;local A=T.readAll()T.close()E=l(A)E.server=z;y[_]=E;return E end;local b=nil
local function g()
if not b then b={}local z=fs.combine(d,"rocks")
for E,T in ipairs(fs.list(z))do if
T:match("%.patchspec$")then local _=fs.combine(z,T)local A=l(s.read(_))
b[T:gsub("%.patchspec$","")]=A end end end;return b end
local function k(r)local z,_={},0
if r.added then for E,T in ipairs(r.added)do _=_+1;z[_]=T end end;if r.patches then
for E,T in ipairs(r.patches)do _=_+1;z[_]=T..".patch"end end;return z end
local function q(z,_)local E=_ and _.source;if E then local T=z.version;local A={}
for O,I in pairs(E)do if type(I)=="string"then
I=I:gsub("%%{version}",T)end;A[O]=I end;return A end;return
z.source end
local function j(z,_)local E,T={},{}local A={}
for I,N in pairs(z)do local S=_[I]
if S then local H=n(N,S)os.queueEvent("diff")
coroutine.yield("diff")local R=r.makePatch(H)if#R>0 then E[#E+1]=I
A[I..".patch"]=r.writePatch(R,I)end;os.queueEvent("diff")
coroutine.yield("diff")else T[#T+1]=I end end;local O={}
for I,N in pairs(_)do if not z[I]then O[#O+1]=I;A[I]=N end end;return A,E,O,T end
local function x(z,_,E,T,A)
assert(type(z)=="table","exected table for original")
assert(type(_)=="table","exected table for replacement")
assert(type(E)=="table","exected table for patches")
assert(type(T)=="table","exected table for added")
assert(type(A)=="table","exected table for removed")local O={}local I={}local N=false
for S,H in ipairs(E)do local R=_[H..".patch"]local D=z[H]
if not R then w(
"Cannot find patch "..H..".patch")end
if not D then w("Cannot find original "..H)end;f("Applying patch to "..H)local E=r.readPatch(R)
local L,U=r.applyPatch(E,D,H)if not L then m("Cannot apply "..H..": "..U)N=true else
O[H]=L;I[H]=true end
os.queueEvent("diff")coroutine.yield("diff")end
if N then w("Issues occured when patching",0)end;for S,H in ipairs(A)do I[H]=true end
for S,H in ipairs(T)do local R=_[H]if not R then
w("Cannot find added file "..H)end;O[H]=R;I[H]=true end;for S,H in pairs(z)do if not I[S]then O[S]=H end end;return O end
return{findPatchspec=p,fetchPatchspec=v,makePatches=j,extractSource=q,applyPatches=x,extractFiles=k,getAll=g}end
a["bsrocks.rocks.manifest"]=function(...)local n=i"bsrocks.lib.files"
local s=i"bsrocks.lib.utils".log;local h=i"bsrocks.lib.settings"
local r=i"bsrocks.lib.serialize".unserialize;local d={}local l=h.servers;local u=h.patchDirectory
local function c(w)local y=d[w]if y then return y end;s(
"Fetching manifest "..w)
local p=http.get(w.."manifest-5.1")
if not p then error("Cannot fetch manifest: "..w,0)end;local v=p.readAll()p.close()y=r(v)y.server=w;d[w]=y;return y end
local function m()local w,y={},0;for p,v in ipairs(l)do
if not d[v]then y=y+1;w[y]=function()c(v)end end end;if y>0 then if y==1 then w[1]()else
parallel.waitForAll(unpack(w))end end;return d end
local function f()local w=fs.combine(u,"rocks/manifest-5.1")
if not fs.exists(w)then return
{repository={},commands={},modules={},patches={}},w else return r(n.read(w)),w end end;return{fetchManifest=c,fetchAll=m,loadLocal=f}end
a["bsrocks.rocks.install"]=function(...)local n=i"bsrocks.rocks.dependencies"
local s=i"bsrocks.downloaders"local h=i"bsrocks.lib.files"local r=i"bsrocks.rocks.patchspec"
local d=i"bsrocks.rocks.rockspec"local l=i"bsrocks.lib.serialize"local u=i"bsrocks.lib.settings"
local c=i"bsrocks.downloaders.tree"local m=i"bsrocks.lib.utils"local f=u.installDirectory
local w,y,p,v=m.log,m.warn,m.verbose,m.error;local b=false;local g={}local k={}
local function q(T,A)local O={}if A and A.remove then
for I,N in ipairs(A.remove)do O[N]=true end end;return d.extractFiles(T,O)end
local function j(T,A,O)O=O or q(T,A)local I={}local v=false;local N=r.extractSource(T,A)if
not s(N,false)then
I[#I+1]={"Cannot find downloader for "..N.url..". Please suggest this package to be patched.",true}v=true end
for S,H in
ipairs(O)do
if type(H)=="table"then
I[#I+1]={table.concat(H,", ").." are packaged into one module. This will not work.",true}else local R=H:match("[^/]%.(%w+)$")
if R and R~="lua"then
if R=="c"or R=="cpp"or
R=="h"or R=="hpp"then
I[#I+1]={H.." is a C file. This will not work.",true}v=true else
I[#I+1]={"File extension is not lua (for "..H.."). It may not work correctly.",false}end end end end;return v,I end
local function x(T,A)local O=q(T,A)local I,N=j(T,A,O)
if#N>0 then
m.printColoured("This package is incompatible",colors.red)for D,L in ipairs(N)do local U=colors.yellow;if L[2]then U=colors.red end
m.printColoured(" "..L[1],U)end;if I then
v("This package is incompatible",0)end end;local S=r.extractSource(T,A)local H=s(S,O)if not H then
v("Cannot find downloader for "..S.url..".",0)end
if A then local D=r.extractFiles(A)local L=c(A.server..
T.package..'/',D)H=r.applyPatches(H,L,A.patches or{},A.added or{},
A.removed or{})end;local R=T.build
if R then
if R.modules then local D=fs.combine(f,"lib")for L,U in pairs(R.modules)do
p("Writing module "..L)
h.writeLines(fs.combine(D,L:gsub("%.","/")..".lua"),H[U])end end
if R.install then
for D,L in pairs(R.install)do local U=fs.combine(f,D)for D,C in pairs(L)do
p("Writing "..D.." to "..U)
if type(D)=="number"and D>=1 and D<=#L then D=C end
h.writeLines(fs.combine(U,D..".lua"),H[C])end end end end
h.write(fs.combine(f,T.package..".rockspec"),l.serialize(T))if A then
h.write(fs.combine(f,T.package..".patchspec"),l.serialize(A))end;g[T.package]=T end
local function z(T,A)local O={}
if r and r.remove then for S,H in ipairs(r.remove)do O[H]=true end end;local I=d.extractFiles(T,O)local N=T.build
if N then
if N.modules then
local S=fs.combine(f,"lib")for H,R in pairs(N.modules)do
fs.delete(fs.combine(S,H:gsub("%.","/")..".lua"))end end;if N.install then
for S,H in pairs(N.install)do local R=fs.combine(f,S)for S,D in pairs(H)do
fs.delete(fs.combine(R,S..".lua"))end end end end
fs.delete(fs.combine(f,T.package..".rockspec"))g[T.package]=nil end
local function _()
if not b then b=true;for T,A in pairs(u.existing)do
g[T:lower()]={version=A,package=T,builtin=true}end
if fs.exists(f)then
for T,A in ipairs(fs.list(f))do
if
A:match("%.rockspec")then
local O=l.unserialize(h.read(fs.combine(f,A)))g[O.package:lower()]=O elseif A:match("%.patchspec")then
local O=A:gsub("%.patchspec",""):lower()
local I=l.unserialize(h.read(fs.combine(f,A)))k[O]=I end end end end;return g,k end
local function E(T,A,O)T=T:lower()
p("Preparing to install "..T.." ".. (A or""))local g=_()local I=g[T]
if I and
((A==nil and O==nil)or I.version==A)then v(T.." already installed",0)end;local N=d.findRockspec(T)if not N then
v("Cannot find '"..T.."'",0)end;local S=r.findPatchspec(T)
if not A then if S then
A=S.patches[T]else A=d.latestVersion(N,T,O)end end
if I and I.version==A then v(T.." already installed",0)end;local r=S and r.fetchPatchspec(S.server,T)
local d=d.fetchRockspec(N.server,T,A)if d.build and d.build.type~="builtin"then
v("Cannot build type '"..d.build.type..
"'. Please suggest this package to be patched.",0)end
local H=d.dependencies;if r and r.dependencies then H=r.dependencies end
for R,H in ipairs(H or{})do
local D=n.parseDependency(H)local T=D.name:lower()local I=g[T]
if I then
local A=n.parseVersion(I.version)
if not n.matchConstraints(A,D.constraints)then
w("Updating dependency "..T)E(T,nil,D.constraints)end else w("Installing dependency "..T)
E(T,nil,D.constraints)end end;x(d,r)end;return{getInstalled=_,install=E,remove=z,findIssues=j}end
a["bsrocks.rocks.dependencies"]=function(...)
local n={scm=1100,cvs=1000,rc=-1000,pre=-10000,beta=-100000,alpha=-1000000}
local s={__eq=function(f,w)if#f~=#w then return false end
for y=1,#f do if f[y]~=w[y]then return false end end
if f.revision and w.revision then return(f.revision==w.revision)end;return true end,__lt=function(f,w)
for y=1,math.max(
#f,#w)do local p,v=f[y]or 0,w[y]or 0;if p~=v then return(p<v)end end
if f.revision and w.revision then return(f.revision<w.revision)end;return false end,__le=function(f,w)
for y=1,math.max(
#f,#w)do local p,v=f[y]or 0,w[y]or 0;if p~=v then return(p<=v)end end
if f.revision and w.revision then return(f.revision<=w.revision)end;return true end}
local function h(f)f=f:match("^%s*(.*)%s*$")local w,y=f:match("(.*)%-(%d+)$")
local p={name=f}local v=1;if y then f=w;p.revision=tonumber(y)end
while#f>0 do
local b,g=f:match("^(%d+)[%.%-%_]*(.*)")
if b then local k=tonumber(b)
p[v]=p[v]and p[v]+k/100000 or k;v=v+1 else b,g=f:match("^(%a+)[%.%-%_]*(.*)")
if not b then
error("Warning: version number '"..f..
"' could not be parsed.",0)if not p[v]then p[v]=0 end;break end;local k=n[b]or(b:byte()/1000)p[v]=
p[v]and p[v]+k/100000 or k end;f=g end;return setmetatable(p,s)end
local r={["=="]="==",["~="]="~=",[">"]=">",["<"]="<",[">="]=">=",["<="]="<=",["~>"]="~>",[""]="==",["="]="==",["!="]="~="}
local function d(f)assert(type(f)=="string")
local w,y,p,v=f:match("^(@?)([<>=~!]*)%s*([%w%.%_%-]+)[%s,]*(.*)")local b=r[y]p=h(p)if not b then
return nil,"Encountered bad constraint operator: '"..
tostring(y).."' in '"..input.."'"end
if not p then return nil,
"Could not parse version from constraint: '"..input.."'"end
return{op=b,version=p,no_upgrade=w=="@"and true or nil},v end
local function l(f)assert(type(f)=="string")local w,y,p={},nil,f;while#f>0 do y,f=d(f)
if y then
table.insert(w,y)else return nil,
"Failed to parse constraint '"..tostring(p).."' with error: "..f end end;return w end
local function u(f)assert(type(f)=="string")
local w,y=f:match("^%s*([a-zA-Z0-9][a-zA-Z0-9%.%-%_]*)%s*(.*)")if not w then
return nil,"failed to extract dependency name from '"..tostring(f).."'"end;local p,v=l(y)
if not p then return nil,v end;return{name=w,constraints=p}end
local function c(f,w)
assert(type(f)=="string"or type(f)=="table")
assert(type(w)=="string"or type(f)=="table")if type(f)~="table"then f=h(f)end
if type(w)~="table"then w=h(w)end;if not f or not w then return false end;for y,p in ipairs(w)do local v=f[y]or 0;if p~=v then
return false end end;if w.revision then return
w.revision==f.revision end;return true end
local function m(f,w)assert(type(f)=="table")
assert(type(w)=="table")local y=true
for p,v in pairs(w)do
if type(v.version)=="string"then v.version=h(v.version)end;local b,g=v.version,v.op
if g=="=="then y=f==b elseif g=="~="then y=f~=b elseif g==">"then y=f>b elseif g=="<"then y=f<b elseif g==">="then
y=f>=b elseif g=="<="then y=f<=b elseif g=="~>"then y=c(f,b)end;if not y then break end end;return y end
return{parseVersion=h,parseConstraints=l,parseDependency=u,matchConstraints=m}end
a["bsrocks.lib.utils"]=function(...)local n=i"bsrocks.lib.settings".logFile;if
fs.exists(n)then fs.delete(n)end
local function s(k,q)local j=type(k)if j~=q then
error(q.." expected, got "..j,3)end;return k end;local function h()return
"/tmp/"..os.clock().."-"..math.random(1,2^31-1)end
local function r(k,q,j)if
type(k)~="thread"then j=q;q=k end;local j=s(j or 1,"number")
local x={"stack traceback: "}for _=2,20 do local E,T=pcall(error,"",_+j)if T==""or T=="nil:"then break end
x[_]=T end
local z=table.concat(x,"\n\t")if q then return tostring(q).."\n"..z end;return z end;local d,l
if term.isColour()then
d=function(k,q)term.setTextColour(q)print(k)
term.setTextColour(colours.white)end
l=function(k,q)term.setTextColour(q)write(k)
term.setTextColour(colours.white)end else d=function(k)print(k)end;l=write end
local function u(k)local q
if fs.exists(n)then q=fs.open(n,"a")else q=fs.open(n,"w")end;q.writeLine(k)q.close()end;local function c(k)u("[VERBOSE] "..k)end;local function m(k)
u("[LOG] "..k)d(k,colours.lightGrey)end;local function f(k)
u("[WARN] "..k)d(k,colours.yellow)end;local w=error
local function y(k,q)
u("[ERROR] "..k)if q==nil then q=2 elseif q~=0 then q=q+1 end;w(k,q)end
local p={["^"]="%^",["$"]="%$",["("]="%(",[")"]="%)",["%"]="%%",["."]="%.",["["]="%[",["]"]="%]",["*"]="%*",["+"]="%+",["-"]="%-",["?"]="%?",["\0"]="%z"}local function v(k)return(k:gsub(".",p))end;local b=term
local function g(k,q)
if
type(k)~="string"then y("string expected, got "..type(k),2)end;if type(q)~="number"then
y("number expected, got "..type(q),2)end;if stdout and stdout.isPiped then return
stdout.writeLine(k)end;local j,z=b.getSize()
local _,E=b.getCursorPos()b.setCursorPos(q+1,E)
local function T()
if E+1 <=z then b.setCursorPos(q+1,E+1)else b.setCursorPos(
q+1,z)b.scroll(1)end;_,E=b.getCursorPos()end
while#k>0 do local x=k:match("^[ \t]+")if x then b.write(x)
_,E=b.getCursorPos()k=k:sub(#x+1)end;if k:sub(1,1)=="\n"then T()
k=k:sub(2)end;local A=k:match("^[^ \t\n]+")
if A then
k=k:sub(#A+1)
if#A>j then while#A>0 do if _>j then T()end;b.write(A)A=A:sub((j-_)+2)
_,E=b.getCursorPos()end else if _+#A-1 >j then T()end
b.write(A)_,E=b.getCursorPos()end end end;if E+1 <=z then b.setCursorPos(1,E+1)else b.setCursorPos(1,z)
b.scroll(1)end end
return{checkType=s,escapePattern=v,log=m,printColoured=d,writeColoured=l,printIndent=g,tmpName=h,traceback=r,warn=f,verbose=c,error=y}end
a["bsrocks.lib.settings"]=function(...)
local n={patchDirectory="/rocks-patch",installDirectory="/rocks",servers={'https://raw.githubusercontent.com/SquidDev-CC/Blue-Shiny-Rocks/rocks/','http://luarocks.org/'},tries=3,existing={lua="5.1",bit32="5.2.2-1",computercraft=
(
_HOST and _HOST:match("ComputerCraft ([%d%.]+)"))or _CC_VERSION or"1.0"},libPath={"./?.lua","./?/init.lua","%{patchDirectory}/rocks/lib/?.lua","%{patchDirectory}/rocks/lib/?/init.lua","%{installDirectory}/lib/?.lua","%{installDirectory}/lib/?/init.lua"},binPath={"/rocks/bin/?.lua","/rocks/bin/?"},logFile="bsrocks.log"}
if fs.exists(".bsrocks")then local h=i"bsrocks.lib.serialize"
local r=fs.open(".bsrocks","r")local d=r.readAll()r.close()
for l,u in pairs(h.unserialize(d))do n[l]=u end end;if settings then
if fs.exists(".settings")then settings.load(".settings")end
for h,r in pairs(n)do n[h]=settings.get("bsrocks."..h,r)end end;local function s(h)
for r,d in
ipairs(h)do if d:sub(#d)~="/"then h[r]=d.."/"end end end;s(n.servers)return n end
a["bsrocks.lib.serialize"]=function(...)local function n(d)local l={}
assert(load(d,"unserialize","t",l))()l._ENV=nil;return l end
local s={["and"]=true,["break"]=true,["do"]=true,["else"]=true,["elseif"]=true,["end"]=true,["false"]=true,["for"]=true,["function"]=true,["if"]=true,["in"]=true,["local"]=true,["nil"]=true,["not"]=true,["or"]=true,["repeat"]=true,["return"]=true,["then"]=true,["true"]=true,["until"]=true,["while"]=true}
local function h(d,l,u,c)local m=type(d)
if m=="table"then if l[d]~=nil then
error("Cannot serialize table with recursive entries")end;l[d]=true
if next(d)==nil then
if c then return""else return"{}"end else local f,w={},0;local y=u;if not c then w=w+1;f[w]="{\n"y=u.." "end;local p={}
local v="\n"if not c then v=",\n"end;for j,x in ipairs(d)do p[j]=true;w=w+1
f[w]=y..h(x,l,y,false)..v end;local b,g,k={},0,true;local q
for j,x in pairs(d)do if not p[j]then k=k and type(j)==
"string"g=g+1;b[g]=j end end;if k then table.sort(b)end
for j,x in ipairs(b)do local z;local _=d[x]
if
type(x)=="string"and not
s[x]and string.match(x,"^[%a_][%a%d_]*$")then z=x.." = "..h(_,l,y)else z="[ "..
h(x,l,y).." ] = "..h(_,l,y)end;w=w+1;f[w]=y..z..v end;if not c then w=w+1;f[w]=u.."}"end;return table.concat(f)end elseif m=="string"then return string.format("%q",d)elseif
m=="number"or m=="boolean"or m=="nil"then return tostring(d)else
error("Cannot serialize type "..type,0)end end;local function r(d)return h(d,{},"",true)end
return{unserialize=n,serialize=r}end
a["bsrocks.lib.patch"]=function(...)local n=3
local function s(l)local u,c={},0;local m,f=1,1;local w,y=nil,0;local p=0
for v=1,#l do local b=l[v]
local g,k=b[1],b[2]
if g=="="then m=m+#k;f=f+#k
if w then local q;local j=false;if#k>p+n then q=p;j=true else q=#k end;for v=1,q do
y=y+1;w[y]={g,k[v]}end;w.oCount=w.oCount+q
w.nCount=w.nCount+q;if j then p=0;w=nil else p=p-q end end else p=n
if not w then w={oLine=m,oCount=0,nLine=f,nCount=0}y=0;local q=l[v-1]
if
q and q[1]=="="then local k=q[2]local j=math.min(n,#k)w.oCount=w.oCount+j
w.nCount=w.nCount+j;w.oLine=w.oLine-j;w.nLine=w.nLine-j;for v=#k-j+1,#k do y=y+1
w[y]={"=",k[v]}end end;c=c+1;u[c]=w end
if g=="+"then f=f+#k;w.nCount=w.nCount+#k elseif g=="-"then m=m+#k
w.oCount=w.oCount+#k else error("Unknown mode "..tostring(g))end;for v=1,#k do y=y+1;w[y]={g,k[v]}end end end;return u end
local function h(l,u)local c,m={},0;if u then m=2;c[1]="--- "..u;c[2]="+++ "..u end
for f=1,#l do
local w=l[f]m=m+1
c[m]=("@@ -%d,%d +%d,%d @@"):format(w.oLine,w.oCount,w.nLine,w.nCount)
for f=1,#w do local y=w[f]local p=y[1]if p=="="then p=" "end;m=m+1;c[m]=p..y[2]end end;return c end
local function r(l)if l[1]:sub(1,3)~="---"then
error("Invalid patch format on line #1")end;if l[2]:sub(1,3)~="+++"then
error("Invalid patch format on line #2")end;local u,c={},0;local m,f=nil,0
for w=3,#l do local y=l[w]
if y:sub(1,2)==
"@@"then
local p,v,b,g=y:match("^@@ %-(%d+),(%d+) %+(%d+),(%d+) @@$")if not p then
error("Invalid block on line #"..w..": "..y)end;m={oLine=p,oCount=v,nLine=b,nCount=g}f=0
c=c+1;u[c]=m else local p=y:sub(1,1)local v=y:sub(2)if p==" "or p==""then p="="elseif
p~="+"and p~="-"then
error("Invalid mode on line #"..w..": "..y)end;f=f+1;if not m then
error("No block for line #"..w)end;m[f]={p,v}end end;return u end
local function d(l,u,c)local m,f={},0;local w=1
for y=1,#l do local p=l[y]
for y=w,p.oLine-1 do f=f+1;m[f]=u[y]w=w+1 end
if w~=p.oLine and w+0 ~=p.oLine+0 then
return false,"Incorrect lines. Expected: "..
p.oLine..", got "..w..
". This may be caused by overlapping patches."end
for y=1,#p do local v,b=p[y][1],p[y][2]
if v=="="then if b~=u[w]then return false,
"line #"..w.." is not equal."end;f=f+1;m[f]=b;w=w+1 elseif v=="-"then if b~=
u[w]then end;w=w+1 elseif v=="+"then f=f+1;m[f]=b end end end;for y=w,#u do f=f+1;m[f]=u[y]end;return m end;return{makePatch=s,applyPatch=d,writePatch=h,readPatch=r}end
a["bsrocks.lib.parse"]=function(...)local n=setmetatable;local function s(g)for k,q in ipairs(g)do g[q]=true end
return g end
local h=s{' ','\n','\t','\r'}
local r={['\r']='\\r',['\n']='\\n',['\t']='\\t',['"']='\\"',["'"]="\\'"}
local d=s{'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'}
local l=s{'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'}
local u=s{'0','1','2','3','4','5','6','7','8','9'}
local c=s{'0','1','2','3','4','5','6','7','8','9','A','a','B','b','C','c','D','d','E','e','F','f'}
local m=s{'+','-','*','/','^','%',',','{','}','[',']','(',')',';','#'}
local f=s{'and','break','do','else','elseif','end','false','for','function','goto','if','in','local','nil','not','or','repeat','return','then','true','until','while'}local w=s{'end','else','elseif','until'}
local y=s{'-','not','#'}local p={}
do function p:Peek(g)local k=self.tokens;g=g or 0;return
k[math.min(#k,self.pointer+g)]end
function p:Get(g)
local k=self.tokens;local q=self.pointer;local j=k[q]self.pointer=math.min(q+1,#k)if g then
table.insert(g,j)end;return j end;function p:Is(g)return self:Peek().Type==g end
function p:ConsumeSymbol(g,k)
local q=self:Peek()if q.Type=='Symbol'then if g then
if q.Data==g then self:Get(k)return true else return nil end else self:Get(k)return q end else
return nil end end
function p:ConsumeKeyword(g,k)local q=self:Peek()if q.Type=='Keyword'and q.Data==g then
self:Get(k)return true else return nil end end;function p:IsKeyword(g)local k=self:Peek()
return k.Type=='Keyword'and k.Data==g end
function p:IsSymbol(g)local k=self:Peek()return k.Type==
'Symbol'and k.Data==g end
function p:IsEof()return self:Peek().Type=='Eof'end end
local function v(g)local k={}
do local j=1;local x=1;local z=1;local function _()local I=g:sub(j,j)
if I=='\n'then z=1;x=x+1 else z=z+1 end;j=j+1;return I end;local function E(I)I=I or 0;return
g:sub(j+I,j+I)end;local function T(I)local N=E()for S=1,#I do
if N==I:sub(S,S)then return _()end end end;local function A(I,N)if N==true then
N=1 else N=0 end
error(x..":"..z..":"..N..":"..I,0)end
local function O()local I=j
if E()=='['then local N=0;local S=1;while E(N+1)==
'='do N=N+1 end
if E(N+1)=='['then for L=0,N+1 do _()end;local H=j
while true do if E()==''then
A(
"Expected `]"..string.rep('=',N).."]` near <eof>.",true)end;local L=true
if E()==']'then for U=1,N do
if E(U)~='='then L=false end end;if E(N+1)~=']'then L=false end else
if E()=='['then local U=true;for C=1,N do if
E(C)~='='then U=false;break end end;if
E(N+1)=='['and U then S=S+1;for C=1,(N+2)do _()end end end;L=false end
if L then S=S-1;if S==0 then break else for U=1,N+2 do _()end end else _()end end;local R=g:sub(H,j-1)for L=0,N+1 do _()end;local D=g:sub(I,j-1)return R,D else return nil end else return nil end end
while true do local I=false
while true do local L=E()
if L=='#'and E(1)=='!'and x==1 then _()_()while
E()~='\n'and E()~=''do _()end end
if L==' 'or L=='\t'or L=='\n'or L=='\r'then _()elseif L=='-'and
E(1)=='-'then _()_()local U,C=O()
if not C then while E()~='\n'and E()~=''do _()end end else break end end;local N=x;local S=z;local H=":"..x..":"..z..":> "local R=E()local D=nil
if R==
''then D={Type='Eof'}elseif l[R]or d[R]or R=='_'then local L=j
repeat _()R=E()until not(
l[R]or d[R]or u[R]or R=='_')local U=g:sub(L,j-1)if f[U]then D={Type='Keyword',Data=U}else
D={Type='Ident',Data=U}end elseif
u[R]or(E()=='.'and u[E(1)])then local L=j
if R=='0'and E(1)=='x'then _()_()while c[E()]do _()end;if T('Pp')then T('+-')while
u[E()]do _()end end else while u[E()]do _()end;if T('.')then
while u[E()]do _()end end
if T('Ee')then T('+-')if not u[E()]then
A("Expected exponent")end;repeat _()until not u[E()]end;local U=E():lower()if(U>='a'and U<='z')or U=='_'then
A("Invalid number format")end end;D={Type='Number',Data=g:sub(L,j-1)}elseif R=='\''or R=='\"'then
local L=j;local U=_()local C=j
while true do local R=_()if R=='\\'then _()elseif R==U then break elseif R==''or R=='\n'then
A("Unfinished string near <eof>")end end;local M=g:sub(C,j-2)local F=g:sub(L,j-1)
D={Type='String',Data=F,Constant=M}elseif R=='['then local L,U=O()if U then D={Type='String',Data=U,Constant=L}else _()
D={Type='Symbol',Data='['}end elseif T('>=<')then if T('=')then
D={Type='Symbol',Data=R..'='}else D={Type='Symbol',Data=R}end elseif T('~')then if
T('=')then D={Type='Symbol',Data='~='}else
A("Unexpected symbol `~` in source.")end elseif T('.')then if T('.')then if T('.')then
D={Type='Symbol',Data='...'}else D={Type='Symbol',Data='..'}end else
D={Type='Symbol',Data='.'}end elseif T(':')then if T(':')then
D={Type='Symbol',Data='::'}else D={Type='Symbol',Data=':'}end elseif m[R]then _()
D={Type='Symbol',Data=R}else local L,U=O()
if L then D={Type='String',Data=U,Constant=L}else A("Unexpected Symbol `"..
R.."` in source.")end end;D.line=N;D.char=S;k[#k+1]=D;if D.Type=='Eof'then break end end end;local q=setmetatable({tokens=k,pointer=1},{__index=p})
return q end
local function b(g)local function k(O)error(O,0)end;local q,j,x
local function z()if not g:ConsumeSymbol('(')then
k("`(` expected.")end
while not g:ConsumeSymbol(')')do
if g:Is('Ident')then
g:Get()
if not g:ConsumeSymbol(',')then if g:ConsumeSymbol(')')then break else
k("`)` expected.")end end elseif g:ConsumeSymbol('...')then if not g:ConsumeSymbol(')')then
k("`...` must be the last argument of a function.")end;break else
k("Argument name or `...` expected")end end;j()if not g:ConsumeKeyword('end')then
k("`end` expected after function body")end end
local function _()
if g:ConsumeSymbol('(')then q()if not g:ConsumeSymbol(')')then
k("`)` Expected.")end;return{AstType="Paren"}elseif g:Is('Ident')then g:Get()else
k("primary expression expected")end end
function ParseSuffixedExpr(O)local I=_()or{AstType=""}
while true do local N={}
if g:ConsumeSymbol('.')or
g:ConsumeSymbol(':')then
if not g:Is('Ident')then k("<Ident> expected.")end;g:Get()I={AstType='MemberExpr'}elseif
not O and g:ConsumeSymbol('[')then q()
if not g:ConsumeSymbol(']')then k("`]` expected.")end;I={AstType='IndexExpr'}elseif not O and g:ConsumeSymbol('(')then while not
g:ConsumeSymbol(')')do q()
if not g:ConsumeSymbol(',')then if g:ConsumeSymbol(')')then break else
k("`)` Expected.")end end end
I={AstType='CallExpr'}elseif not O and g:Is('String')then g:Get()I={AstType='StringCallExpr'}elseif
not O and g:IsSymbol('{')then x()I={AstType='TableCallExpr'}else break end end;return I end
function x()
if g:Is('Number')or g:Is('String')then g:Get()elseif
g:ConsumeKeyword('nil')or g:ConsumeKeyword('false')or g:ConsumeKeyword('true')or g:ConsumeSymbol('...')then elseif
g:ConsumeSymbol('{')then
while true do
if g:ConsumeSymbol('[')then q()if not g:ConsumeSymbol(']')then
k("`]` Expected")end;if not g:ConsumeSymbol('=')then
k("`=` Expected")end;q()elseif g:Is('Ident')then local O=g:Peek(1)
if O.Type=='Symbol'and
O.Data=='='then local I=g:Get()if not g:ConsumeSymbol('=')then
k("`=` Expected")end;q()else q()end elseif g:ConsumeSymbol('}')then break else q()end
if g:ConsumeSymbol(';')or g:ConsumeSymbol(',')then elseif
g:ConsumeSymbol('}')then break else k("`}` or table entry Expected")end end elseif g:ConsumeKeyword('function')then return z()else return ParseSuffixedExpr()end end;local E=8
local T={['+']={6,6},['-']={6,6},['%']={7,7},['/']={7,7},['*']={7,7},['^']={10,9},['..']={5,4},['==']={3,3},['<']={3,3},['<=']={3,3},['~=']={3,3},['>']={3,3},['>=']={3,3},['and']={2,2},['or']={1,1}}
function q(O)O=O or 0
if y[g:Peek().Data]then local I=g:Get().Data;q(E)else x()end
while true do local I=T[g:Peek().Data]if I and I[1]>O then local N={}g:Get()
q(I[2])else break end end end
local function A()
if g:ConsumeKeyword('if')then
repeat q()if not g:ConsumeKeyword('then')then
k("`then` expected.")end;j()until not g:ConsumeKeyword('elseif')if g:ConsumeKeyword('else')then j()end;if
not g:ConsumeKeyword('end')then k("`end` expected.")end elseif
g:ConsumeKeyword('while')then q()
if not g:ConsumeKeyword('do')then return k("`do` expected.")end;j()
if not g:ConsumeKeyword('end')then k("`end` expected.")end elseif g:ConsumeKeyword('do')then j()if not g:ConsumeKeyword('end')then
k("`end` expected.")end elseif g:ConsumeKeyword('for')then if not g:Is('Ident')then
k("<ident> expected.")end;g:Get()
if g:ConsumeSymbol('=')then q()if not
g:ConsumeSymbol(',')then k("`,` Expected")end;q()if
g:ConsumeSymbol(',')then q()end;if not g:ConsumeKeyword('do')then
k("`do` expected")end;j()if not g:ConsumeKeyword('end')then
k("`end` expected")end else
while g:ConsumeSymbol(',')do if not g:Is('Ident')then
k("for variable expected.")end;g:Get(tokenList)end
if not g:ConsumeKeyword('in')then k("`in` expected.")end;q()while g:ConsumeSymbol(',')do q()end;if
not g:ConsumeKeyword('do')then k("`do` expected.")end;j()if not
g:ConsumeKeyword('end')then k("`end` expected.")end end elseif g:ConsumeKeyword('repeat')then j()if not g:ConsumeKeyword('until')then
k("`until` expected.")end;q()elseif g:ConsumeKeyword('function')then if
not g:Is('Ident')then k("Function name expected")end
ParseSuffixedExpr(true)z()elseif g:ConsumeKeyword('local')then
if g:Is('Ident')then g:Get()while g:ConsumeSymbol(',')do
if not
g:Is('Ident')then k("local var name expected")end;g:Get()end
if
g:ConsumeSymbol('=')then repeat q()until not g:ConsumeSymbol(',')end elseif g:ConsumeKeyword('function')then if not g:Is('Ident')then
k("Function name expected")end;g:Get(tokenList)z()else
k("local var or function def expected")end elseif g:ConsumeSymbol('::')then if not g:Is('Ident')then
k('Label name expected')end;g:Get()if not g:ConsumeSymbol('::')then
k("`::` expected")end elseif g:ConsumeKeyword('return')then local O={}local I=g:Peek()
if
I.Type=="Eof"or I.Type~="Keyword"or not w[I.Data]then q()local I=g:Peek()while g:ConsumeSymbol(',')do q()end end elseif g:ConsumeKeyword('break')then elseif g:ConsumeKeyword('goto')then if not g:Is('Ident')then
k("Label expected")end;g:Get(tokenList)else
local O=ParseSuffixedExpr()
if g:IsSymbol(',')or g:IsSymbol('=')then if O.AstType=="Paren"then
k("Can not assign to parenthesized expression, is not an lvalue")end;while g:ConsumeSymbol(',')do
ParseSuffixedExpr()end;if not g:ConsumeSymbol('=')then
k("`=` Expected.")end;q()while g:ConsumeSymbol(',')do q()end elseif
O.AstType=='CallExpr'or O.AstType=='TableCallExpr'or O.AstType==
'StringCallExpr'then else
k("Assignment Statement Expected")end end;g:ConsumeSymbol(';')end;function j()
while not w[g:Peek().Data]and not g:IsEof()do A()end end;return j()end;return{lex=v,parse=b}end
a["bsrocks.lib.match"]=function(...)local n,s,h=bit32.band,bit32.bor,bit32.lshift
local r=error
local d,l,u,c,m=string.sub,string.byte,string.char,string.gmatch,string.gsub;local f,w,y=string.match,string.find,string.format
local p,v,b=table.insert,table.remove,table.concat;local g,k,q,j,x=math.max,math.min,math.floor,math.ceil,math.abs;local z=1000
local _=0.3;local E=32
local function T(N,S,H)if(#S==0)then return nil end;return w(N,S,H,true)end;local A,O
local function I(N,S,H)
if N==nil or S==nil then r('Null inputs. (match_main)')end;if N==S then return 1 elseif#N==0 then return-1 end;H=g(1,k(H or 0,#N))if d(N,H,
H+#S-1)==S then return H else return A(N,S,H)end end;function O(N)local S={}local H=0
for R in c(N,'.')do S[R]=s(S[R]or 0,h(1,#N-H-1))H=H+1 end;return S end
function A(N,S,H)if#S>E then
r('Pattern too long.')end;local R=O(S)
local function D(P,V)local B=P/#S;local G=x(H-V)if(z==0)then return
(G==0)and 1 or B end;return B+ (G/z)end;local L=_;local U=T(N,S,H)if U then L=k(D(0,U),L)end;local C=h(1,#S-1)U=-1
local M,F;local W=#S+#N;local Y
for P=0,#S-1,1 do M=0;F=W;while(M<F)do if(D(P,H+F)<=L)then M=F else W=F end;F=q(M+ (
W-M)/2)end;W=F
local V=g(1,H-F+1)local B=k(H+F,#N)+#S;local G={}for K=V,B do G[K]=0 end;G[B+1]=h(1,P)-1
for K=B,V,-
1 do local Q=R[d(N,K-1,K-1)]or 0;if(P==0)then
G[K]=n(s((G[K+1]*2),1),Q)else
G[K]=s(n(s(h(G[K+1],1),1),Q),s(s(h(s(Y[K+1],Y[K]),1),1),Y[
K+1]))end;if
(n(G[K],C)~=0)then local J=D(P,K-1)
if(J<=L)then L=J;U=K-1;if(U>H)then V=g(1,H*2-U)else break end end end end;if(D(P+1,H)>L)then break end;Y=G end;return U end;return I end
a["bsrocks.lib.files"]=function(...)local function n(c)local m=fs.open(c,"r")local f=m.readAll()
m.close()return f end
local function s(c)local m=fs.open(c,"r")
local f,w={},0;for y in m.readLine do w=w+1;f[w]=y end;m.close()
while f[w]==""do f[w]=nil;w=w-1 end;return f end
local function h(c,m)local f=fs.open(c,"w")f.write(m)f.close()end;local function r(c,m)local f=fs.open(c,"w")for w=1,#m do f.writeLine(m[w])end
f.close()end
local function d(c,m,f)if not fs.exists(c)then
error("Cannot find "..m..
" (Looking for "..c..")",f or 1)end end
local function l(c,m)m=m or n;local f=#c+2;local w,y={c},1;local p={}
while y>0 do local v=w[y]y=y-1;if fs.isDir(v)then for b,g in
ipairs(fs.list(v))do y=y+1;w[y]=fs.combine(v,g)end else
p[v:sub(f)]=m(v)end end;return p end
local function u(c,m,f)f=f or h;for w,y in pairs(m)do f(fs.combine(c,w),y)end end
return{read=n,readLines=s,readDir=l,write=h,writeLines=r,writeDir=u,assertExists=d}end
a["bsrocks.lib.dump"]=function(...)
local n={["and"]=true,["break"]=true,["do"]=true,["else"]=true,["elseif"]=true,["end"]=true,["false"]=true,["for"]=true,["function"]=true,["if"]=true,["in"]=true,["local"]=true,["nil"]=true,["not"]=true,["or"]=true,["repeat"]=true,["return"]=true,["then"]=true,["true"]=true,["until"]=true,["while"]=true}
local function s(r,d,l,u)local c=type(r)
if c=="table"and not d[r]then d[r]=true
if next(r)==nil then if u then return"()"else
return"{}"end else local m=false;local f=u or#r;local w=0
for j,x in pairs(r)do
if type(j)=="table"or
type(x)=="table"then m=true;break elseif
type(j)=="number"and j>=1 and j<=f and j%1 ==0 then w=w+#tostring(x)+2 else
w=w+#
tostring(x)+#tostring(j)+2 end;if w>30 then m=true;break end end;local y,p,v="",", ",""if m then y="\n"p=",\n"v=l.." "end;local b,g={
(u and"("or"{")..y},1;local k={}local q=true
for j=1,f do k[j]=true;g=g+1;local x=v..
s(r[j],d,v)if not q then x=p..x else q=false end;b[g]=x end
for j,x in pairs(r)do
if not k[j]then local z
if type(j)=="string"and not n[j]and
string.match(j,"^[%a_][%a%d_]*$")then z=j.." = "..s(x,d,v)else z="["..
s(j,d,v).."] = "..s(x,d,v)end;z=v..z;if not q then z=p..z else q=false end;g=g+1;b[g]=z end end;g=g+1;b[g]=y..l.. (u and")"or"}")return
table.concat(b)end elseif c=="string"then return
(string.format("%q",r):gsub("\\\n","\\n"))else return tostring(r)end end;local function h(r,d)return s(r,{},"",d)end;return h end
a["bsrocks.lib.diffmatchpatch"]=function(...)
local n,s,h=bit32.band,bit32.bor,bit32.lshift;local r,d,l,u=type,setmetatable,ipairs,select;local c,m,f=unpack,tonumber,error
local w,y,p,v,b=string.sub,string.byte,string.char,string.gmatch,string.gsub;local g,k,q=string.match,string.find,string.format
local j,x,z=table.insert,table.remove,table.concat;local _,E,T,A,O=math.max,math.min,math.floor,math.ceil,math.abs
local I=os.clock;local N='[^A-Za-z0-9%-=;\',./~!@#$%&*%(%)_%+ %?]'
local function S(eS)return q('%%%02X',y(eS))end
local function H(eS,eH,eR,...)local eD=u('#',...)for eL=1,eR do x(eS,eH)end;for eL=eD,1,-1 do
j(eS,eH,(u(eL,...)))end end;local function R(eS,eH)return w(eS,eH,eH)end;local function D(eS,eH,eR)if(#eH==0)then return nil end;return
k(eS,eH,eR,true)end;local L='[&<>\n]'
local U={['&']='&amp;',['<']='&lt;',['>']='&gt;',['\n']='&para;<br>'}local C,M,F,W,Y;local P;local V,B,G,K;local Q=-1;local J=1;local X=0;local Z=1.0;local ee=4;local et=0.5;local ea=1000;local eo=0.5
local ei=4;local en=32
local function es(eS)
if eS then Z=eS.Diff_Timeout or Z;ee=eS.Diff_EditCost or ee;et=
eS.Match_Threshold or et;ea=eS.Match_Distance or ea;eo=
eS.Patch_DeleteThreshold or eo;ei=eS.Patch_Margin or ei;en=
eS.Match_MaxBits or en else return
{Diff_Timeout=Z,Diff_EditCost=ee,Match_Threshold=et,Match_Distance=ea,Patch_DeleteThreshold=eo,Patch_Margin=ei,Match_MaxBits=en}end end;local eh,er,ed,el,eu,ec,em,ef,ew,ey,ep,ev,eb,eg,ek,eq
function C(eS,eH,eR,eD)
if eD==nil then if Z<=0 then eD=2^31 else eD=I()+Z end end;local eL=eD
if eS==nil or eS==nil then f('Null inputs. (diff_main)')end;if eS==eH then if#eS>0 then return{{X,eS}}end;return{}end
local eU=false;local eC=ew(eS,eH)local eM
if eC>0 then eM=w(eS,1,eC)eS=w(eS,eC+1)eH=w(eH,eC+1)end;eC=ey(eS,eH)local eF;if eC>0 then eF=w(eS,-eC)eS=w(eS,1,-eC-1)
eH=w(eH,1,-eC-1)end;local eW=eh(eS,eH,eU,eL)if eM then
j(eW,1,{X,eM})end;if eF then eW[#eW+1]={X,eF}end;ef(eW)return eW end
function M(eS)local eH=false;local eR={}local eD=0;local eL=nil;local eU=1;local eC=0;local eM=0;local eF=0;local eW=0
while eS[eU]do
if
eS[eU][1]==X then eD=eD+1;eR[eD]=eU;eC=eF;eM=eW;eF=0;eW=0;eL=eS[eU][2]else
if eS[eU][1]==J then eF=eF+#
(eS[eU][2])else eW=eW+# (eS[eU][2])end
if
eL and(#eL<=_(eC,eM))and(#eL<=_(eF,eW))then j(eS,eR[eD],{Q,eL})eS[eR[eD]+1][1]=J;eD=eD-1
eD=eD-1;eU=(eD>0)and eR[eD]or 0;eC,eM=0,0;eF,eW=0,0;eL=nil;eH=true end end;eU=eU+1 end;if eH then ef(eS)end;em(eS)eU=2
while eS[eU]do
if
(eS[eU-1][1]==Q and eS[eU][1]==J)then local eY=eS[eU-1][2]local eP=eS[eU][2]local eV=ep(eY,eP)
local eB=ep(eP,eY)
if(eV>=eB)then if(eV>=#eY/2 or eV>=#eP/2)then
j(eS,eU,{X,w(eP,1,eV)})eS[eU-1][2]=w(eY,1,#eY-eV)
eS[eU+1][2]=w(eP,eV+1)eU=eU+1 end else if(
eB>=#eY/2 or eB>=#eP/2)then
j(eS,eU,{X,w(eY,1,eB)})eS[eU-1]={J,w(eP,1,#eP-eB)}
eS[eU+1]={Q,w(eY,eB+1)}eU=eU+1 end end;eU=eU+1 end;eU=eU+1 end end
function F(eS)local eH=false;local eR={}local eD=0;local eL=nil;local eU=1;local eC=0;local eM=0;local eF=0;local eW=0
while eS[eU]do
if
eS[eU][1]==X then local eY=eS[eU][2]if(#eY<ee)and(eF==1 or eW==1)then eD=eD+1
eR[eD]=eU;eC,eM=eF,eW;eL=eY else eD=0;eL=nil end
eF,eW=0,0 else if eS[eU][1]==Q then eW=1 else eF=1 end
if
eL and(
(eC+eM+eF+eW==4)or
((#eL<ee/2)and(eC+eM+eF+eW==3)))then j(eS,eR[eD],{Q,eL})eS[eR[eD]+1][1]=J;eD=eD-1
eL=nil;if(eC==1)and(eM==1)then eF,eW=1,1;eD=0 else eD=eD-1
eU=(eD>0)and eR[eD]or 0;eF,eW=0,0 end;eH=true end end;eU=eU+1 end;if eH then ef(eS)end end
function W(eS)local eH=0;local eR,eD=0,0;for eL,eU in l(eS)do local eC,eM=eU[1],eU[2]
if(eC==J)then eR=eR+#eM elseif(eC==Q)then
eD=eD+#eM elseif(eC==X)then eH=eH+_(eR,eD)eR=0;eD=0 end end;eH=eH+
_(eR,eD)return eH end
function Y(eS)local eH={}
for eR,eD in l(eS)do local eL=eD[1]local eU=eD[2]local eC=b(eU,L,U)
if eL==J then eH[eR]=
'<ins style="background:#e6ffe6;">'..eC..'</ins>'elseif eL==Q then eH[eR]=
'<del style="background:#ffe6e6;">'..eC..'</del>'elseif eL==X then eH[eR]=
'<span>'..eC..'</span>'end end;return z(eH)end
function eh(eS,eH,eR,eD)if#eS==0 then return{{J,eH}}end;if#eH==0 then return{{Q,eS}}end
local eL;local eU=(#eS>#eH)and eS or eH
local eC=(#eS>#eH)and eH or eS;local eM=D(eU,eC)
if eM~=nil then
eL={{J,w(eU,1,eM-1)},{X,eC},{J,w(eU,eM+#eC)}}if#eS>#eH then eL[1][1],eL[3][1]=Q,Q end;return eL end;if#eC==1 then return{{Q,eS},{J,eH}}end
do local eF,eW,eY,eP,eV=eu(eS,eH)
if
eF then local eB=C(eF,eY,eR,eD)local eG=C(eW,eP,eR,eD)local eK=#eB;eL=eB
eL[eK+1]={X,eV}for eM,eQ in l(eG)do eL[eK+1+eM]=eQ end;return eL end end;return er(eS,eH,eD)end
function er(eS,eH,eR)local eD=#eS;local eL=#eH;local eU,eC;local eM=A((eD+eL)/2)local eF=eM;local eW=2*eM
local eY={}local eP={}for eX=0,eW-1 do eY[eX]=-1;eP[eX]=-1 end;eY[eF+1]=0
eP[eF+1]=0;local eV=eD-eL;local eB=(eV%2 ~=0)local eG=0;local eK=0;local eQ=0;local eJ=0
for eX=0,eM-1 do
if I()>eR then break end
for te=-eX+eG,eX-eK,2 do local tt=eF+te;local ta;if(te==-eX)or((te~=eX)and
(eY[tt-1]<eY[tt+1]))then ta=eY[tt+1]else ta=
eY[tt-1]+1 end;local to=ta-te;while
(ta<=eD)and(to<=eL)and(R(eS,ta)==R(eH,to))do
ta=ta+1;to=to+1 end;eY[tt]=ta
if ta>eD+1 then eK=eK+2 elseif
to>eL+1 then eG=eG+2 elseif eB then local ti=eF+eV-te
if ti>=0 and ti<eW and eP[ti]~=-1 then local tn=eD-
eP[ti]+1;if ta>tn then return ed(eS,eH,ta,to,eR)end end end end
for te=-eX+eQ,eX-eJ,2 do local tt=eF+te;local ta;if(te==-eX)or((te~=eX)and
(eP[tt-1]<eP[tt+1]))then ta=eP[tt+1]else ta=
eP[tt-1]+1 end;local to=ta-te;while
(ta<=eD)and(to<=eL)and(R(eS,-ta)==R(eH,-to))do ta=ta+1;to=to+1 end;eP[tt]=ta
if ta>eD+1 then
eJ=eJ+2 elseif to>eL+1 then eQ=eQ+2 elseif not eB then local ti=eF+eV-te
if
ti>=0 and ti<eW and eY[ti]~=-1 then local tn=eY[ti]local ts=eF+tn-ti;ta=eD-ta+1;if tn>ta then
return ed(eS,eH,tn,ts,eR)end end end end end;return{{Q,eS},{J,eH}}end
function ed(eS,eH,eR,eD,eL)local eU=w(eS,1,eR-1)local eC=w(eH,1,eD-1)local eM=w(eS,eR)local eF=w(eH,eD)
local eW=C(eU,eC,false,eL)local eY=C(eM,eF,false,eL)local eP=#eW
for eV,eB in l(eY)do eW[eP+eV]=eB end;return eW end
function ew(eS,eH)if
(#eS==0)or(#eH==0)or(y(eS,1)~=y(eH,1))then return 0 end;local eR=1;local eD=E(#eS,#eH)local eL=eD;local eU=1
while
(eR<eL)do
if(w(eS,eU,eL)==w(eH,eU,eL))then eR=eL;eU=eR else eD=eL end;eL=T(eR+ (eD-eR)/2)end;return eL end
function ey(eS,eH)if
(#eS==0)or(#eH==0)or(y(eS,-1)~=y(eH,-1))then return 0 end;local eR=1;local eD=E(#eS,#eH)local eL=eD;local eU=1
while
(eR<eL)do
if(w(eS,-eL,-eU)==w(eH,-eL,-eU))then eR=eL;eU=eR else eD=eL end;eL=T(eR+ (eD-eR)/2)end;return eL end
function ep(eS,eH)local eR=#eS;local eD=#eH;if eR==0 or eD==0 then return 0 end;if eR>eD then
eS=w(eS,eR-eD+1)elseif eR<eD then eH=w(eH,1,eR)end;local eL=E(eR,eD)if eS==eH then
return eL end;local eU=0;local eC=1
while true do local eM=w(eS,eL-eC+1)
local eF=k(eH,eM,1,true)if eF==nil then return eU end;eC=eC+eF-1;if eF==1 or
w(eS,eL-eC+1)==w(eH,1,eC)then eU=eC;eC=eC+1 end end end
function el(eS,eH,eR)local eD=w(eS,eR,eR+T(#eS/4))local eL=0;local eU=''local eC,eM,eF,eW
while true do
eL=D(eH,eD,eL+1)if(eL==nil)then break end;local eY=ew(w(eS,eR),w(eH,eL))local eP=ey(w(eS,1,eR-1),w(eH,1,
eL-1))
if#eU<eP+eY then eU=w(eH,eL-eP,eL-1)..w(eH,eL,
eL+eY-1)
eC=w(eS,1,eR-eP-1)eM=w(eS,eR+eY)eF=w(eH,1,eL-eP-1)eW=w(eH,eL+eY)end end;if#eU*2 >=#eS then return{eC,eM,eF,eW,eU}else return nil end end
function eu(eS,eH)if Z<=0 then return nil end;local eR=(#eS>#eH)and eS or eH;local eD=(#eS>#
eH)and eH or eS;if(#eR<4)or
(#eD*2 <#eR)then return nil end;local eL=el(eR,eD,A(#eR/4))local eU=el(eR,eD,A(
#eR/2))local eC
if not eL and not eU then return nil elseif not eU then eC=eL elseif not eL then
eC=eU else eC=(#eL[5]>#eU[5])and eL or eU end;local eM,eF,eW,eY;if(#eS>#eH)then eM,eF=eC[1],eC[2]eW,eY=eC[3],eC[4]else
eW,eY=eC[1],eC[2]eM,eF=eC[3],eC[4]end
local eP=eC[5]return eM,eF,eW,eY,eP end
function ec(eS,eH)if(#eS==0)or(#eH==0)then return 6 end;local eR=w(eS,-1)
local eD=w(eH,1,1)local eL=g(eR,'%W')local eU=g(eD,'%W')local eC=eL and g(eR,'%s')local eM=eU and
g(eD,'%s')local eF=eC and g(eR,'%c')
local eW=eM and g(eD,'%c')local eY=eF and g(eS,'\n\r?\n$')
local eP=eW and g(eH,'^\r?\n\r?\n')
if eY or eP then return 5 elseif eF or eW then return 4 elseif eL and not eC and eM then return 3 elseif eC or eM then return 2 elseif eL or eU then return 1 end;return 0 end
function em(eS)local eH=2
while eS[eH+1]do local eR,eD=eS[eH-1],eS[eH+1]
if
(eR[1]==X)and(eD[1]==X)then local eL=eS[eH]local eU=eR[2]local eC=eL[2]local eM=eD[2]local eF=ey(eU,eC)
if eF>0 then
local eB=w(eC,-eF)eU=w(eU,1,-eF-1)eC=eB..w(eC,1,-eF-1)eM=eB..eM end;local eW=eU;local eY=eC;local eP=eM;local eV=ec(eU,eC)+ec(eC,eM)while
y(eC,1)==y(eM,1)do eU=eU..w(eC,1,1)eC=w(eC,2)..w(eM,1,1)eM=w(eM,2)local eB=
ec(eU,eC)+ec(eC,eM)
if eB>=eV then eV=eB;eW=eU;eY=eC;eP=eM end end;if
eR[2]~=eW then
if#eW>0 then eS[eH-1][2]=eW else x(eS,eH-1)eH=eH-1 end;eS[eH][2]=eY
if#eP>0 then eS[eH+1][2]=eP else x(eS,eH+1,1)eH=eH-1 end end end;eH=eH+1 end end
function ef(eS)eS[#eS+1]={X,''}local eH=1;local eR,eD=0,0;local eL,eU='',''local eC
while eS[eH]do
local eF=eS[eH][1]
if eF==J then eD=eD+1;eU=eU..eS[eH][2]eH=eH+1 elseif eF==Q then eR=eR+1
eL=eL..eS[eH][2]eH=eH+1 elseif eF==X then
if eR+eD>1 then
if(eR>0)and(eD>0)then eC=ew(eU,eL)
if eC>0 then
local eW=eH-eR-eD;if(eW>1)and(eS[eW-1][1]==X)then eS[eW-1][2]=
eS[eW-1][2]..w(eU,1,eC)else
j(eS,1,{X,w(eU,1,eC)})eH=eH+1 end
eU=w(eU,eC+1)eL=w(eL,eC+1)end;eC=ey(eU,eL)
if eC~=0 then eS[eH][2]=w(eU,-eC)..eS[eH][2]eU=w(eU,1,-
eC-1)eL=w(eL,1,-eC-1)end end
if eR==0 then H(eS,eH-eD,eD,{J,eU})elseif eD==0 then H(eS,eH-eR,eR,{Q,eL})else H(eS,
eH-eR-eD,eR+eD,{Q,eL},{J,eU})end;eH=
eH-eR-eD+ (eR>0 and 1 or 0)+ (eD>0 and 1 or 0)+1 elseif(eH>1)and
(eS[eH-1][1]==X)then
eS[eH-1][2]=eS[eH-1][2]..eS[eH][2]x(eS,eH)else eH=eH+1 end;eD,eR=0,0;eL,eU='',''end end;if eS[#eS][2]==''then eS[#eS]=nil end;local eM=false;eH=2
while eH<#eS do local eF,eW=eS[eH-1],eS[
eH+1]
if(eF[1]==X)and(eW[1]==X)then local eY=eS[eH]
local eP=eY[2]local eV=eF[2]local eB=eW[2]
if w(eP,-#eV)==eV then
eY[2]=eV..w(eP,1,-#eV-1)eW[2]=eV..eW[2]x(eS,eH-1)eM=true elseif w(eP,1,#eB)==eB then
eF[2]=eV..eB;eY[2]=w(eP,#eB+1)..eB;x(eS,eH+1)eM=true end end;eH=eH+1 end;if eM then return ef(eS)end end
function ev(eS,eH)local eR=1;local eD=1;local eL=1;local eU=1;local eC
for eM,eF in l(eS)do eC=eM
if eF[1]~=J then eR=eR+#eF[2]end;if eF[1]~=Q then eD=eD+#eF[2]end;if eR>eH then break end;eL=eR;eU=eD end;if eS[eC+1]and(eS[eC][1]==Q)then return eU end;return eU+ (eH-
eL)end;function eb(eS)local eH={}
for eR,eD in l(eS)do if eD[1]~=J then eH[#eH+1]=eD[2]end end;return z(eH)end;function eg(eS)local eH={}for eR,eD in l(eS)do if
eD[1]~=Q then eH[#eH+1]=eD[2]end end;return
z(eH)end
function ek(eS)local eH={}
for eR,eD in l(eS)do
local eL,eU=eD[1],eD[2]if eL==J then eH[eR]='+'..b(eU,N,S)elseif eL==Q then eH[eR]='-'..#eU elseif eL==X then
eH[eR]='='..#eU end end;return z(eH,'\t')end
function eq(eS,eH)local eR={}local eD=0;local eL=1
for eU in v(eH,'[^\t]+')do local eC,eM=w(eU,1,1),w(eU,2)
if(eC=='+')then
local eF=false
local eW=b(eM,'%%(.?.?)',function(eY)local eP=m(eY,16)
if(#eY~=2)or(eP==nil)then eF=true;return''end;return p(eP)end)if eF then
f('Illegal escape in _diff_fromDelta: '..eM)end;eD=eD+1;eR[eD]={J,eW}elseif
(eC=='-')or(eC=='=')then local eF=m(eM)if(eF==nil)or(eF<0)then
f('Invalid number in _diff_fromDelta: '..eM)end;local eW=w(eS,eL,eL+eF-1)
eL=eL+eF
if(eC=='=')then eD=eD+1;eR[eD]={X,eW}else eD=eD+1;eR[eD]={Q,eW}end else
f('Invalid diff operation in _diff_fromDelta: '..eU)end end;if(eL~=#eS+1)then
f('Delta length ('.. (eL-1)..
') does not equal source text length ('..#eS..').')end;return eR end;local ej,ex
function P(eS,eH,eR)
if eS==nil or eH==nil then f('Null inputs. (match_main)')end;if eS==eH then return 1 elseif#eS==0 then return-1 end
eR=_(1,E(eR or 0,#eS))
if w(eS,eR,eR+#eH-1)==eH then return eR else return ej(eS,eH,eR)end end
function ex(eS)local eH={}local eR=0;for eD in v(eS,'.')do
eH[eD]=s(eH[eD]or 0,h(1,#eS-eR-1))eR=eR+1 end;return eH end
function ej(eS,eH,eR)if#eH>en then f('Pattern too long.')end;local eD=ex(eH)
local function eL(eV,eB)
local eG=eV/#eH;local eK=O(eR-eB)
if(ea==0)then return(eK==0)and 1 or eG end;return eG+ (eK/ea)end;local eU=et;local eC=D(eS,eH,eR)if eC then eU=E(eL(0,eC),eU)end
local eM=h(1,#eH-1)eC=-1;local eF,eW;local eY=#eH+#eS;local eP
for eV=0,#eH-1,1 do eF=0;eW=eY
while(eF<eW)do if
(eL(eV,eR+eW)<=eU)then eF=eW else eY=eW end;eW=T(eF+ (eY-eF)/2)end;eY=eW;local eB=_(1,eR-eW+1)local eG=E(eR+eW,#eS)+#eH;local eK={}for eQ=eB,eG
do eK[eQ]=0 end;eK[eG+1]=h(1,eV)-1
for eQ=eG,eB,-1 do local eJ=
eD[w(eS,eQ-1,eQ-1)]or 0;if(eV==0)then
eK[eQ]=n(s((eK[eQ+1]*2),1),eJ)else
eK[eQ]=s(n(s(h(eK[eQ+1],1),1),eJ),s(s(h(s(eP[eQ+1],eP[eQ]),1),1),eP[
eQ+1]))end
if(
n(eK[eQ],eM)~=0)then local eX=eL(eV,eQ-1)if(eX<=eU)then eU=eX;eC=eQ-1;if(eC>eR)then
eB=_(1,eR*2-eC)else break end end end end;if(eL(eV+1,eR)>eU)then break end;eP=eK end;return eC end;local ez,e_,eE,eT,eA,eO
function V(eS,eH,eR)local eD,eL;local eU,eC,eM=r(eS),r(eH),r(eR)
if(eU=='string')and
(eC=='string')and(eM=='nil')then eD=eS
eL=C(eD,eH,true)if(#eL>2)then M(eL)F(eL)end elseif
(eU=='table')and(eC=='nil')and(eM=='nil')then eL=eS;eD=eb(eL)elseif(eU=='string')and
(eC=='table')and(eM=='nil')then eD=eS;eL=eH elseif
(eU=='string')and(eC=='string')and(eM=='table')then
eD=eS;eL=eR else f('Unknown call format to patch_make.')end;if(eL[1]==nil)then return{}end;local eF={}local eW=eO()local eY=0;local eP=0;local eV=0
local eB,eG=eD,eD
for eK,eQ in l(eL)do local eJ,eX=eQ[1],eQ[2]if(eY==0)and(eJ~=X)then eW.start1=eP+1
eW.start2=eV+1 end
if(eJ==J)then eY=eY+1;eW.diffs[eY]=eQ;eW.length2=
eW.length2+#eX
eG=w(eG,1,eV)..eX..w(eG,eV+1)elseif(eJ==Q)then eW.length1=eW.length1+#eX;eY=eY+1;eW.diffs[eY]=eQ;eG=
w(eG,1,eV)..w(eG,eV+#eX+1)elseif(eJ==X)then
if(#eX<=ei*2)and
(eY~=0)and(#eL~=eK)then eY=eY+1;eW.diffs[eY]=eQ;eW.length1=
eW.length1+#eX;eW.length2=eW.length2+#eX elseif(#eX>=ei*2)then
if
(eY~=0)then ez(eW,eB)eF[#eF+1]=eW;eW=eO()eY=0;eB=eG;eP=eV end end end;if(eJ~=J)then eP=eP+#eX end;if(eJ~=Q)then eV=eV+#eX end end;if(eY>0)then ez(eW,eB)eF[#eF+1]=eW end;return eF end
function K(eS,eH)if eS[1]==nil then return eH,{}end;eS=e_(eS)local eR=eE(eS)
eH=eR..eH..eR;eT(eS)local eD=0;local eL={}
for eU,eC in l(eS)do local eM=eC.start2+eD;local eF=eb(eC.diffs)local eW
local eY=-1
if#eF>en then eW=P(eH,w(eF,1,en),eM)
if eW~=-1 then
eY=P(eH,w(eF,-en),eM+#eF-en)if eY==-1 or eW>=eY then eW=-1 end end else eW=P(eH,eF,eM)end
if eW==-1 then eL[eU]=false;eD=eD-eC.length2-eC.length1 else
eL[eU]=true;eD=eW-eM;local eP;if eY==-1 then eP=w(eH,eW,eW+#eF-1)else
eP=w(eH,eW,eY+en-1)end
if eF==eP then eH=w(eH,1,eW-1)..
eg(eC.diffs)..w(eH,eW+#eF)else local eV=C(eF,eP,false)
if
(#eF>en)and(W(eV)/#eF>eo)then eL[eU]=false else em(eV)local eB=1;local eG
for eK,eQ in
l(eC.diffs)do if eQ[1]~=X then eG=ev(eV,eB)end;if eQ[1]==J then eH=w(eH,1,eW+eG-2)..eQ[2]..w(eH,
eW+eG-1)elseif eQ[1]==Q then
eH=w(eH,1,
eW+eG-2)..w(eH,eW+ev(eV,eB+#eQ[2]-1))end;if eQ[1]~=Q then eB=
eB+#eQ[2]end end end end end end;eH=w(eH,#eR+1,-#eR-1)return eH,eL end
function B(eS)local eH={}for eR,eD in l(eS)do eA(eD,eH)end;return z(eH)end
function G(eS)local eH={}if(#eS==0)then return eH end;local eR={}for eL in v(eS.."\n",'([^\n]*)\n')do
eR[#eR+1]=eL end;local eD=1
while(eD<=#eR)do
local eL,eU,eC,eM=g(eR[eD],'^@@ %-(%d+),?(%d*) %+(%d+),?(%d*) @@$')if(eL==nil)then
f('Invalid patch string: "'..eR[eD]..'"')end;local eF=eO()eH[#eH+1]=eF;eL=m(eL)
eU=m(eU)or 1;if(eU==0)then eL=eL+1 end;eF.start1=eL;eF.length1=eU;eC=m(eC)
eM=m(eM)or 1;if(eM==0)then eC=eC+1 end;eF.start2=eC;eF.length2=eM;eD=eD+1
while true do
local eW=eR[eD]if(eW==nil)then break end;local eY;eY,eW=w(eW,1,1),w(eW,2)local eP=false
local eV=b(eW,'%%(.?.?)',function(eB)
local eG=m(eB,16)if(#eB~=2)or(eG==nil)then eP=true;return''end;return p(eG)end)if eP then
f('Illegal escape in patch_fromText: '..eW)end;eW=eV
if(eY=='-')then
eF.diffs[#eF.diffs+1]={Q,eW}elseif(eY=='+')then eF.diffs[#eF.diffs+1]={J,eW}elseif(eY==' ')then eF.diffs[
#eF.diffs+1]={X,eW}elseif(eY=='@')then break elseif(eY=='')then else
f(
'Invalid patch mode "'..eY..'" in: '..eW)end;eD=eD+1 end end;return eH end
local eI={__tostring=function(eS)local eH={}eA(eS,eH)return z(eH)end}function eO()
return d({diffs={},start1=1,start2=1,length1=0,length2=0},eI)end
function ez(eS,eH)if(#eH==0)then return end;local eR=w(eH,eS.start2,
eS.start2+eS.length1-1)
local eD=0;local eL=D(eH,eR)local eU=nil;if(eL~=nil)then eU=D(eH,eR,eL+1)end
while(
#eR==0 or eU~=nil)and(#eR<en-ei-ei)do
eD=eD+ei
eR=w(eH,_(1,eS.start2-eD),eS.start2+eS.length1-1+eD)eL=D(eH,eR)if(eL~=nil)then eU=D(eH,eR,eL+1)else eU=nil end end;eD=eD+ei
local eC=w(eH,_(1,eS.start2-eD),eS.start2-1)if(#eC>0)then j(eS.diffs,1,{X,eC})end
local eM=w(eH,eS.start2+eS.length1,
eS.start2+eS.length1-1+eD)
if(#eM>0)then eS.diffs[#eS.diffs+1]={X,eM}end;eS.start1=eS.start1-#eC;eS.start2=eS.start2-#eC;eS.length1=eS.length1+#
eC+#eM
eS.length2=eS.length2+#eC+#eM end
function e_(eS)local eH={}
for eR,eD in l(eS)do local eL=eO()local eU={}
for eC,eM in l(eD.diffs)do eU[eC]={eM[1],eM[2]}end;eL.diffs=eU;eL.start1=eD.start1;eL.start2=eD.start2;eL.length1=eD.length1
eL.length2=eD.length2;eH[eR]=eL end;return eH end
function eE(eS)local eH=ei;local eR=''for eM=1,eH do eR=eR..p(eM)end;for eM,eF in l(eS)do
eF.start1=eF.start1+eH;eF.start2=eF.start2+eH end;local eD=eS[1]
local eL=eD.diffs;local eU=eL[1]
if(eU==nil)or(eU[1]~=X)then j(eL,1,{X,eR})eD.start1=
eD.start1-eH;eD.start2=eD.start2-eH
eD.length1=eD.length1+eH;eD.length2=eD.length2+eH elseif(eH>#eU[2])then local eM=eH-#eU[2]eU[2]=w(eR,
#eU[2]+1)..eU[2]
eD.start1=eD.start1-eM;eD.start2=eD.start2-eM;eD.length1=eD.length1+eM
eD.length2=eD.length2+eM end;eD=eS[#eS]eL=eD.diffs;local eC=eL[#eL]
if(eC==nil)or(eC[1]~=X)then eL[#
eL+1]={X,eR}eD.length1=eD.length1+eH
eD.length2=eD.length2+eH elseif(eH>#eC[2])then local eM=eH-#eC[2]
eC[2]=eC[2]..w(eR,1,eM)eD.length1=eD.length1+eM;eD.length2=eD.length2+eM end;return eR end
function eT(eS)local eH=en;local eR=1
while true do local eD=eS[eR]if eD==nil then return end
if eD.length1 >eH then local eL=eD
x(eS,eR)eR=eR-1;local eU=eL.start1;local eC=eL.start2;local eM=''
while eL.diffs[1]do local eD=eO()local eF=true;eD.start1=
eU-#eM;eD.start2=eC-#eM
if eM~=''then eD.length1,eD.length2=#eM,#eM;eD.diffs[
#eD.diffs+1]={X,eM}end
while eL.diffs[1]and(eD.length1 <eH-ei)do
local eY=eL.diffs[1][1]local eP=eL.diffs[1][2]
if(eY==J)then eD.length2=eD.length2+#eP;eC=eC+
#eP
eD.diffs[# (eD.diffs)+1]=eL.diffs[1]x(eL.diffs,1)eF=false elseif
(eY==Q)and(#eD.diffs==1)and(
eD.diffs[1][1]==X)and(#eP>2*eH)then eD.length1=eD.length1+#eP;eU=eU+#eP;eF=false
eD.diffs[#eD.diffs+1]={eY,eP}x(eL.diffs,1)else eP=w(eP,1,eH-eD.length1-ei)eD.length1=
eD.length1+#eP;eU=eU+#eP;if(eY==X)then
eD.length2=eD.length2+#eP;eC=eC+#eP else eF=false end
eD.diffs[#eD.diffs+1]={eY,eP}
if(eP==eL.diffs[1][2])then x(eL.diffs,1)else eL.diffs[1][2]=w(eL.diffs[1][2],
#eP+1)end end end;eM=eg(eD.diffs)eM=w(eM,-ei)local eW=w(eb(eL.diffs),1,ei)
if
eW~=''then eD.length1=eD.length1+#eW;eD.length2=eD.length2+#eW
if
eD.diffs[1]and(eD.diffs[#eD.diffs][1]==X)then
eD.diffs[#eD.diffs][2]=eD.diffs[#eD.diffs][2]..eW else eD.diffs[#eD.diffs+1]={X,eW}end end;if not eF then eR=eR+1;j(eS,eR,eD)end end end;eR=eR+1 end end
function eA(eS,eH)local eR,eD;local eL,eU=eS.length1,eS.length2;local eC,eM=eS.start1,eS.start2;local eF=eS.diffs;if
eL==1 then eR=eC else
eR=((eL==0)and(eC-1)or eC)..','..eL end;if eU==1 then eD=eM else eD=
((eU==0)and(eM-1)or eM)..','..eU end
eH[
#eH+1]='@@ -'..eR..' +'..eD..' @@\n'local eW
for eY,eP in l(eS.diffs)do local eV=eP[1]
if eV==J then eW='+'elseif eV==Q then eW='-'elseif eV==X then eW=' 'end
eH[#eH+1]=eW..b(eF[eY][2],N,S)..'\n'end;return eH end;es{Match_Threshold=0.3}local eN={}eN.DIFF_DELETE=Q
eN.DIFF_INSERT=J;eN.DIFF_EQUAL=X;eN.diff_main=C;eN.diff_cleanupSemantic=M
eN.diff_cleanupEfficiency=F;eN.diff_levenshtein=W;eN.diff_prettyHtml=Y;eN.match_main=P;eN.patch_make=V
eN.patch_toText=B;eN.patch_fromText=G;eN.patch_apply=K;eN.diff_commonPrefix=ew
eN.diff_commonSuffix=ey;eN.diff_commonOverlap=ep;eN.diff_halfMatch=eu;eN.diff_bisect=er
eN.diff_cleanupMerge=ef;eN.diff_cleanupSemanticLossless=em;eN.diff_text1=eb;eN.diff_text2=eg
eN.diff_toDelta=ek;eN.diff_fromDelta=eq;eN.diff_xIndex=ev;eN.match_alphabet=ex
eN.match_bitap=ej;eN.new_patch_obj=eO;eN.patch_addContext=ez;eN.patch_splitMax=eT
eN.patch_addPadding=eE;eN.settings=es;return eN end
a["bsrocks.lib.diff"]=function(...)local n=ipairs
local function s(d,l,u)local c,m={},0;for f,w in n(d)do m=m+1;c[m]=w end;for f,w in n(l)do
m=m+1;c[m]=w end
if u then for f,w in n(u)do m=m+1;c[m]=w end end;return c end
local function h(d,l,u)local c={}if u==nil then u=#d end;if l<0 or u<0 or l>u then
error('Invalid values: '..l..' '..u)end;for m,f in n(d)do if(m-1)>=l and(m-1)<u then
table.insert(c,f)end end;return c end
local function r(d,l)local u={}
for y,p in n(d)do if not u[p]then u[p]={}end;table.insert(u[p],y-1)end;local c={}local m=0;local f=0;local w=0
for y,p in n(l)do local v=y-1;local b={}if u[p]then
for g,k in n(u[p])do if k<=0 then b[k]=1 else b[k]=
(c[k-1]or 0)+1 end;if(b[k]>w)then w=b[k]m=k-w+1
f=v-w+1 end end end;c=b end
if w==0 then local y={}local p={}if#d>0 then y={{'-',d}}end
if#l>0 then p={{'+',l}}end;return s(y,p)else return
s(r(h(d,0,m),h(l,0,f)),{{'=',h(l,f,f+w)}},r(h(d,m+w),h(l,f+w)))end end;return r end
a["bsrocks.env.package"]=function(...)local n=i"bsrocks.lib.files"local s=i"bsrocks.lib.settings"
local h=i"bsrocks.lib.utils"local r=h.checkType
return
function(d)local l=d._G;local u=s.libPath;if type(u)=="table"then
u=table.concat(u,";")end;u=u:gsub("%%{(%a+)}",s)
local c={loaded={},preload={},path=u,config="/\n;\n?\n!\n-",cpath=""}l.package=c
function c.loadlib(f,w)return nil,"dynamic libraries not enabled","absent"end
function c.seeall(f)r(f,"table")local w=getmetatable(f)if not w then w={}
setmetatable(f,w)end;w.__index=l end
c.loaders={function(f)r(f,"string")return c.preload[f]or
("\n\tno field package.preload['"..f.."']")end,function(f)
r(f,"string")local u=c.path;if type(u)~="string"then
error("package.path is not a string",2)end;f=f:gsub("%.","/")local w={}local y,p=1,#u
while
y<=p do local v=u:find(";",y)if not v then v=p+1 end
local b=d.resolve(u:sub(y,v-1):gsub("%?",f,1))y=v+1;local o,g;if fs.exists(b)then o,g=load(n.read(b),b,"t",l)elseif
fs.exists(b..".lua")then o,g=load(n.read(b..".lua"),b,"t",l)else
g="File not found"end;if
type(o)=="function"then return o end
w[#w+1]="'"..b.."': "..g end;return table.concat(w,"\n\t")end}
function l.require(f)r(f,"string")local o=c.loaded;local w=o[f]if w~=nil then if w then return w end
error(
"loop or previous error loading module ' "..f.."'",2)end;local y=c.loaders
r(y,"table")local p={}
for v,b in ipairs(y)do w=b(f)local g=type(w)
if g=="string"then p[#p+1]=w elseif g=="function"then
o[f]=false;local k=w(f)
if k~=nil then o[f]=k else k=o[f]if k==false then o[f]=true;k=true end end;return k end end
error("module '"..
f.."' not found: "..f..table.concat(p,""))end
local function m(f,w)local y,p=1,#w
while y<=p do local v=w:find(".",y,true)if not v then v=p+1 end
local b=w:sub(y,v-1)y=v+1;local g=rawget(f,b)
if g==nil then g={}f[b]=g;f=g elseif type(g)=="table"then f=g else return nil end end;return f end
function l.module(f,...)r(f,"string")local w=c.loaded[f]if type(w)~="table"then w=m(l,f)
if not w then error(
"name conflict for module '"..f.."'",2)end;c.loaded[f]=w end
if
w._NAME==nil then w._M=w;w._NAME=f:gsub("([^.]+%.)","")w._PACKAGE=
f:gsub("%.[^%.]+$","")or""end;setfenv(2,w)for y,p in pairs({...})do p(w)end end;local o=c.loaded
for f,w in pairs(l)do if type(w)=="table"then o[f]=w end end;return c end end
a["bsrocks.env.os"]=function(...)local n=i"bsrocks.lib.utils"local s=i"bsrocks.env.date"
local h=n.checkType
return
function(r)local d,l=os,shell;local u={}local c={}local m=d.clock;if profiler and profiler.milliTime then
m=function()return
profiler.milliTime()*1e-3 end end
r._G.os={clock=m,date=function(f,w)
f=h(f or"%c","string")w=h(w or d.time(),"number")if f:sub(1,1)=="!"then
f=f:sub(2)end;local y=s.create(w)if f=="*t"then return y elseif f=="%c"then
return s.asctime(y)else return s.strftime(f,y)end end,difftime=function(f,w)return
w-f end,execute=function(f)if l.run(f)then return 0 else return 1 end end,exit=function(f)error(
"Exit code: ".. (f or 0),0)end,getenv=function(f)
if
l.getenv then local w=l.getenv(f)if w~=nil then return w end end;if settings and settings.get then local w=settings.get(f)
if w~=nil then return w end end;return u[f]end,remove=function(f)return
pcall(fs.delete,r.resolve(h(f,"string")))end,rename=function(f,w)return
pcall(fs.rename,r.resolve(h(f,"string")),r.resolve(h(w,"string")))end,setlocale=function()
end,time=function(f)if not f then return d.time()end;h(f,"table")
return s.timestamp(f)end,tmpname=function()local f=n.tmpName()c[f]=true;return f end}r.cleanup[#r.cleanup+1]=function()
for f,w in pairs(c)do pcall(fs.delete,f)end end end end
a["bsrocks.env.io"]=function(...)local n=i"bsrocks.lib.utils"local s=i"bsrocks.env.ansi"
local h=n.checkType
local function r(c)return
type(c)=="table"and c.close and c.flush and c.lines and c.read and c.seek and c.setvbuf and c.write end
local function d(c)if not r(c)then
error("Not a file: Missing one of: close, flush, lines, read, seek, setvbuf, write",3)end end
local function l(c)local m=type(c)if m~="table"or not c.__handle then
error("FILE* expected, got "..m)end;if c.__isClosed then
error("attempt to use closed file",3)end;return c.__handle end
local u={__index={close=function(c)c.__handle.close()c.__isClosed=true end,flush=function(c)
l(c).flush()end,read=function(c,...)local m=l(c)local f={}local w={...}local y=select("#",...)
if y==0 then y=1 end
for p=1,y do local v=w[p]or"l"
v=h(v,"string"):gsub("%*",""):sub(1,1)local b,g;if v=="l"then b,g=m.readLine()elseif v=="a"then b,g=m.readAll()elseif v=="r"then b,g=m.read()else
error("(invalid format",2)end
if not b then return b,g end;f[#f+1]=b end;return unpack(f)end,seek=function(c,...)
error("File seek is not implemented",2)end,setvbuf=function()end,write=function(c,...)local m=l(c)local f={...}
local w=select("#",...)
for y=1,w do local p=f[y]local v=type(p)if v~="string"and v~="number"then
error("string expected, got "..v)end;m.write(tostring(p))end;return true end,lines=function(c,...)return
c.__handle.readLine end}}
return
function(c)local m={}c._G.io=m
local function f(y,p)y=c.resolve(y)
p=(p or"r"):gsub("%+","")local v,b=pcall(fs.open,y,p)if not v or not b then
return nil,b or"No such file or directory"end
return setmetatable({__handle=b},u)end
do local function y()end
local function p()return nil,"cannot close standard file"end;local function v()return nil,"bad file descriptor"end
c.stdout=setmetatable({__handle={close=p,flush=y,read=v,readLine=v,readAll=v,write=function(b)
s.write(b)end}},u)
c.stderr=setmetatable({__handle={close=p,flush=y,read=v,readLine=v,readAll=v,write=function(b)local g=term.isColor()if g then
term.setTextColor(colors.red)end;s.write(b)if g then
term.setTextColor(colors.white)end end}},u)
c.stdin=setmetatable({__handle={close=p,flush=y,read=function()
return string.byte(os.pullEvent("char"))end,readLine=read,readAll=read,write=function()
error("cannot write to input",3)end}},u)m.stdout=c.stdout;m.stderr=c.stderr;m.stdin=c.stdin end
function m.close(y) (y or c.stdout):close()end;function m.flush(y)c.stdout:flush()end
function m.input(y)local p=type(y)if p=="nil"then return
c.stdin elseif p=="string"then y=assert(f(y,"r"))elseif p~="table"then
error("string expected, got "..p,2)end;d(y)
m.stdin=y;c.stdin=y;return y end
function m.output(y)local p=type(y)
if p=="nil"then return c.stdin elseif p=="string"then y=assert(f(y,"w"))elseif p~=
"table"then error("string expected, got "..p,2)end;d(y)m.stdout=y;c.stdout=y;return y end
function m.popen(y)error("io.popen is not implemented",2)end;function m.read(...)return c.stdin:read(...)end;local w={}function m.tmpfile()
local y=n.tmpName()w[y]=true;return f(y,"w")end;m.open=f;function m.type(y)
if r(y)then if
y.__isClosed then return"closed file"end;return"file"else return type(y)end end;function m.write(...)return
c.stdout:write(...)end;c._G.write=m.write
c._G.print=s.print;c.cleanup[#c.cleanup+1]=function()
for y,p in pairs(w)do pcall(fs.delete,y)end end;return m end end
a["bsrocks.env"]=function(...)local n=i"bsrocks.lib.files"
local function s(h,r)for l,u in pairs(h)do
if r[l]==nil then r[l]=u end end;local d=getmetatable(h)
if type(d)=="table"and
type(d.__index)=="table"then return s(d.__index,r)end end
return
function()
local h=function(f)return
function()error(f.." is not implemented",2)end end
local r={math=math,string=string,table=table,coroutine=coroutine,collectgarbage=h("collectgarbage"),_VERSION=_VERSION}r._G=r;r._ENV=r
local d={_G=r,dir=shell.dir(),stdin=false,stdout=false,strerr=false,cleanup={}}function d.resolve(f)
if f:sub(1,1)~="/"then f=fs.combine(d.dir,f)end;return f end
function r.load(f,w)local y={}while true do local p=f()if
p==""or p==nil then break end;y[#y+1]=p end;return r.loadstring(table.concat(f),
w or"=(load)")end;function r.loadstring(f,w)return load(f,w,nil,r)end;function r.loadfile(f)
f=d.resolve(f)
if fs.exists(f)then return load(n.read(f),f,"t",r)else return nil,"File not found"end end;function r.dofile(f)
assert(r.loadfile(f))()end
function r.print(...)local f=d.stdout;local w=r.tostring;local y={...}
for p=1,select('#',...)
do if p>1 then f:write("\t")end;f:write(w(y[p]))end;f:write("\n")end;local l,u={},{}local function c(f)if f==nil then return nil end;local w=l[f]l[f]=nil
if w==u then w=nil elseif w==nil then w=f end;return w end
d.getError=c;local m={}
if select(2,pcall(error,m))~=m then local function f(...)local w,y=...if w then return...else
return false,c(y)end end
function r.error(w,y)
y=y or 1;if y>0 then y=y+1 end
if type(w)~="string"then
local p=tostring({})..tostring(w)if w==nil then w=u end;l[p]=w;error(p,0)else error(w,y)end end;function r.pcall(w,...)return f(pcall(w,...))end
function r.xpcall(w,y)return xpcall(w,function(p)
return y(c(p))end)end end;i"bsrocks.env.fixes"(d)i"bsrocks.env.io"(d)
i"bsrocks.env.os"(d)
if not debug then i"bsrocks.env.debug"(d)else r.debug=debug end;i"bsrocks.env.package"(d)s(_ENV,r)r._NATIVE=_ENV;return d end end
a["bsrocks.env.fixes"]=function(...)local n,s=type,pairs
local function h(d)local l={}for u,c in s(d)do l[u]=c end;return l end;local function r(d)local l=n(d)
if l=="table"then return getmetatable(d)elseif l=="string"then return string else return nil end end
return function(d)
d._G.getmetatable=r
if not table.pack().n then local l=h(table)l.pack=function(...)
return{n=select('#',...),...}end;d._G.table=l end end end
a["bsrocks.env.debug"]=function(...)local n=i"bsrocks.lib.utils".traceback;local function s(r)
return function()error(r..
" not implemented",2)end end
local function h(r,d,l)if type(r)~="thread"then d=r end
local u={what="lua",source="",short_src="",linedefined=
-1,lastlinedefined=-1,currentline=-1,nups=-1,name="?",namewhat="",activelines={}}local c=type(d)
if c=="number"or c=="string"then d=tonumber(d)
local m,f=pcall(error,"",2+d)local w=f:gsub(":?[^:]*: *$","",1)u.source="@"..w;u.short_src=w;local y=tonumber(
f:match("^[^:]+:([%d]+):")or"")if y then
u.currentline=y end elseif c=="function"then u.func=d else
error("function or level expected",2)end;return u end
return
function(r)
local d={getfenv=getfenv,gethook=s("gethook"),getinfo=h,getlocal=s("getlocal"),gethook=s("gethook"),getmetatable=r._G.getmetatable,getregistry=s("getregistry"),setfenv=setfenv,sethook=s("sethook"),setlocal=s("setlocal"),setmetatable=setmetatable,setupvalue=s("setupvalue"),traceback=n}r._G.debug=d end end
a["bsrocks.env.date"]=function(...)local n=string.format;local s=math.floor
local function h(O,I)return s(O/I)end;local r={31,28,31,30,31,30,31,31,30,31,30,31}local d={0}for O=2,12 do d[O]=d[O-1]+
r[O-1]end;local l={0,3,2,5,0,3,5,1,4,6,2,4}local function u(O)
if
(O%4)~=0 then return false elseif(O%100)~=0 then return true else return(O%400)==0 end end;local function c(O)return
u(O)and 366 or 365 end;local function m(O,I)if O==2 then return u(I)and 29 or 28 else
return r[O]end end;local function f(O)return h(O,4)-
h(O,100)+h(O,400)end
local function w(O,I,N)
local S=d[I]if I>2 and u(N)then S=S+1 end;return S+O end;local function y(O,I,N)if I<3 then N=N-1 end
return(N+f(N)+l[I]+O)%7+1 end
local function p(O,I,N)local S=O%1;I=I+S*N;O=O-S;return O,I end;local function v(O,I,N)
if I>=N then O=O+h(I,N)I=I%N elseif I<0 then O=O-1+h(-I,N)I=N- (-I%N)end;return O,I end
local function b(O,I,N,S,H,R)I,N=I-1,N-1
O,I=p(O,I,12)O,I=v(O,I,12)I,N=p(I,N,m(s(I+1),O))N,S=p(N,S,24)
S,H=p(S,H,60)H,R=p(H,R,60)H,R=v(H,R,60)S,H=v(S,H,60)N,S=v(N,S,24)while N<0 do O=O-1
N=N+c(O)end;O,I=v(O,I,12)while true do local D=m(I+1,O)if N<D then break end;N=N-D;I=I+1;if
I>=12 then I=0;O=O+1 end end
I,N=I+1,N+1;return O,I,N,S,H,R end
local function g(O)local I,N,S,H,R,D=b(1970,1,1,0,0,O)return
{day=S,month=N,year=I,hour=H,min=R,sec=D,yday=w(S,N,I),wday=y(S,N,I)}end;local k=f(1970)
local function q(O,I,N,S,H,R)O,I,N,S,H,R=b(O,I,N,S,H,R)
local D=w(N,I,O)+365* (O-1970)+ (f(
O-1)-k)-1;return
D* (60*60*24)+S* (60*60)+H*60+R end;local function j(O)
return q(O.year,O.month,O.day,O.hour or 0,O.min or 0,O.sec or 0)end
local x={abday={"Sun","Mon","Tue","Wed","Thu","Fri","Sat"},day={"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"},abmon={"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"},mon={"January","February","March","April","May","June","July","August","September","October","November","December"},am_pm={"AM","PM"}}local function z(O)if O==1 then return 7 else return O-1 end end;local _
do
local O={}
for N,S in
ipairs{4,9,15,20,26,32,37,43,48,54,60,65,71,76,82,88,93,99,105,111,116,122,128,133,139,144,150,156,161,167,172,178,184,189,195,201,207,212,218,224,229,235,240,246,252,257,263,268,274,280,285,291,296,303,308,314,320,325,331,336,342,348,353,359,364,370,376,381,387,392,398}do O[S]=true end;local function I(N)return O[N%400]end
function _(N)local S=z(N.wday)local H=N.yday-S
local R=N.year
if H<-3 then R=R-1;if I(R)then return R,53,S else return R,52,S end elseif H>=361 and not I(R)then return
R+1,1,S else return R,h(H+10,7),S end end end;local E={}function E:a(O)return"%s",O.abday[self.wday]end;function E:A(O)return"%s",
O.day[self.wday]end;function E:b(O)
return"%s",O.abmon[self.month]end
function E:B(O)return"%s",O.mon[self.month]end
function E:c(O)return"%.3s %.3s%3d %.2d:%.2d:%.2d %d",O.abday[self.wday],O.abmon[self.month],self.day,self.hour,
self.min,self.sec,self.year end;function E:C()return"%02d",h(self.year,100)end;function E:d()
return"%02d",self.day end;function E:D()
return"%02d/%02d/%02d",self.month,self.day,self.year%100 end
function E:e()return"%2d",self.day end
function E:F()return"%d-%02d-%02d",self.year,self.month,self.day end;function E:g()return"%02d",_(self)%100 end
function E:G()return"%d",_(self)end;E.h=E.b;function E:H()return"%02d",self.hour end;function E:I()return"%02d",
(self.hour-1)%12+1 end
function E:j()return"%03d",self.yday end;function E:m()return"%02d",self.month end
function E:M()return"%02d",self.min end;function E:n()return"\n"end;function E:p(O)return
self.hour<12 and O.am_pm[1]or O.am_pm[2]end
function E:r(O)return"%02d:%02d:%02d %s",
(self.hour-1)%12+1,self.min,self.sec,self.hour<12 and O.am_pm[1]or
O.am_pm[2]end;function E:R()return"%02d:%02d",self.hour,self.min end;function E:s()
return"%d",q(self)end;function E:S()return"%02d",self.sec end
function E:t()return"\t"end
function E:T()return"%02d:%02d:%02d",self.hour,self.min,self.sec end;function E:u()return"%d",z(self.wday)end;function E:U()return"%02d",h(
self.yday-self.wday+7,7)end;function E:V()return
"%02d",select(2,_(self))end;function E:w()
return"%d",self.wday-1 end;function E:W()return"%02d",
h(self.yday-z(self.wday)+7,7)end;E.x=E.D;E.X=E.T;function E:y()return"%02d",
self.year%100 end
function E:Y()return"%d",self.year end;function E:z()return"+0000"end;function E:Z()return"GMT"end
E["%"]=function(O)return"%%"end
local function T(O,I)
return
(string.gsub(O,"%%([EO]?)(.)",function(N,S)local H=E[S]if H then return n(H(I,x))else
error("invalid conversation specifier '%"..N..S.."'",3)end end))end;local function A(O)return n(E.c(O,x))end
return{create=g,timestamp=j,strftime=T,asctime=A}end
a["bsrocks.env.ansi"]=function(...)local n,s=write,term;local h=type
local r,d=s.getBackgroundColour(),s.getTextColour()local l,u=s.setBackgroundColour,s.setTextColour
local function c(g,k)return function()g(k)end end
local function m(g,k,q)if g>q then return q elseif g<k then return k else return g end end;local function f(g,k)local q,j=s.getCursorPos()local x,z=s.getSize()
s.setCursorPos(m(g+q,1,x),m(k+j,1,z))end
local w={['0']=function()l(r)u(d)end,['7']=function()
local g=s.getBackgroundColour()s.setBackgroundColour(s.getTextColour())
s.setText(g)end,['30']=c(u,colours.black),['31']=c(u,colours.red),['32']=c(u,colours.green),['33']=c(u,colours.orange),['34']=c(u,colours.blue),['35']=c(u,colours.purple),['36']=c(u,colours.cyan),['37']=c(u,colours.lightGrey),['40']=c(l,colours.black),['41']=c(l,colours.red),['42']=c(l,colours.green),['43']=c(l,colours.orange),['44']=c(l,colours.blue),['45']=c(l,colours.purple),['46']=c(l,colours.cyan),['47']=c(l,colours.lightGrey),['90']=c(u,colours.grey),['91']=c(u,colours.red),['92']=c(u,colours.lime),['93']=c(u,colours.yellow),['94']=c(u,colours.lightBlue),['95']=c(u,colours.pink),['96']=c(u,colours.cyan),['97']=c(u,colours.white),['100']=c(l,colours.grey),['101']=c(l,colours.red),['102']=c(l,colours.lime),['103']=c(l,colours.yellow),['104']=c(l,colours.lightBlue),['105']=c(l,colours.pink),['106']=c(l,colours.cyan),['107']=c(l,colours.white)}local y,p=1,1
local v={m=function(g)for k,q in ipairs(g)do local j=w[q]if j then j()end end end,['A']=function(g)
local k=tonumber(g[1])if not k then return end;f(0,-k)end,['B']=function(g)
local k=tonumber(g[1])if not k then return end;f(0,k)end,['C']=function(g)
local k=tonumber(g[1])if not k then return end;f(k,0)end,['D']=function(g)
local k=tonumber(g[1])if not k then return end;f(-k,0)end,['H']=function(g)
local k,q=tonumber(g[1]),tonumber(g[2])if not k or not q then return end;local j,x=s.getSize()
s.setCursorPos(m(k,1,j),m(q,1,x))end,['J']=function(g)if
g[1]=="2"then s.clear()end end,['s']=function()
y,p=s.getCursorPos()end,['u']=function()s.setCursorPos(y,p)end}
local function b(g)
if stdout and stdout.isPiped then return stdout.write(text)end;if h(g)~="string"then
error("bad argument #1 (string expected, got "..h(ansi)..")",2)end;local k=1
while k<=#g do
local q,j=g:find("\27[",k,true)
if q then if k<q then n(g:sub(k,q-1))end;local x=true;local z,_={},0;local E
while x do j=j+1;q=j
while
true do local A=g:sub(j,j)
if A==";"then break elseif(A>='A'and A<='Z')or
(A>='a'and A<='z')then E=A;x=false;break elseif A==""or A==nil then
error("Invalid escape sequence at "..A)else j=j+1 end end;_=_+1;z[_]=g:sub(q,j-1)end;local T=E and v[E]if T then T(z)end;k=j+1 elseif k==1 then n(g)return else n(g:sub(k))return end end end;function printAnsi(...)local g=select("#",...)for k=1,g do local q=tostring(select(k,...))if
k<g then q=q.."\t"end;b(q)end
n("\n")end;return
{write=b,print=printAnsi}end
a["bsrocks.downloaders.tree"]=function(...)local n=i"bsrocks.lib.utils".error
local function s(d,l,u,c)if not d then
local w,p=term.getCursorPos()term.setCursorPos(1,p)term.clearLine()
printError("Cannot download "..l)end
local m,f=term.getCursorPos()term.setCursorPos(1,f)term.clearLine()
write(("Downloading: %s/%s (%s%%)"):format(u,c,
u/c*100))end;local h=i"bsrocks.lib.settings".tries
local function r(d,l)local u={}local c=0;local m=#l;if m==0 then
print("No files to download")return{}end;local f=false
local function w(p)local v
for b=1,h do
local g=(d..p):gsub(' ','%%20')local k=http.get(g)
if k then c=c+1;local q,j={},0;for x in k.readLine do j=j+1;q[j]=x end
u[p]=q;k.close()s(true,p,c,m)return elseif f then return end end;f=true;s(false,p,c,m)end;local y={}for p,v in ipairs(l)do y[p]=function()w(v)end end
parallel.waitForAll(unpack(y))print()if f then n("Cannot download "..d)end
return u end;return r end
a["bsrocks.downloaders"]=function(...)local n=i"bsrocks.lib.utils".error
local s=i"bsrocks.downloaders.tree"
local h={function(r,d)local l=r.url;if not l then return end
local u=l:match("git://github%.com/([^/]+/[^/]+)$")or
l:match("https?://github%.com/([^/]+/[^/]+)$")local c=r.branch or r.tag or"master"
if u then
u=u:gsub("%.git$","")else
u,c=l:match("https?://github%.com/([^/]+/[^/]+)/archive/(.*).tar.gz")if not u then return end end;if not d then return true end
print("Downloading "..u.."@"..c)return
s('https://raw.github.com/'..u..'/'..c..'/',d)end,function(r,d)
local l=r.single;if not l then return end;if not d then return true end;if#d~=1 then
n("Expected 1 file for single, got "..#d,0)end;local u,c=http.get(l)if not u then n(c or
"Cannot download "..l,0)end
local m=u.readAll()u.close()return{[d[1]]=m}end}
return function(r,d)for l,u in ipairs(h)do local c=u(r,d)if c then return c end end
return false end end
a["bsrocks.commands.search"]=function(...)local n=i"bsrocks.lib.match"
local s=i"bsrocks.rocks.rockspec"local h=i"bsrocks.rocks.manifest"
local function r(l)
if not l then error("Expected <name>",0)end;l=l:lower()local u,c={},0;local m,f={},0
for w,h in pairs(h.fetchAll())do for y,p in pairs(h.repository)do
local v=s.latestVersion(h,y)
if y:find(l,1,true)then c=c+1;u[c]={y,v}m=nil elseif c==0 then f=f+1;m[f]={y,v}end end end;if c==0 then
printError("Could not find '"..l.."', trying a fuzzy search")
for w,y in ipairs(m)do if n(y[1],l)>0 then c=c+1;u[c]=y end end end
if c==0 then error(
"Cannot find "..l,0)else for w=1,c do local y=u[w]
print(y[1]..": "..y[2])end end end
local d=[[
<name> The name of the package to search for.
If the package cannot be found, it will query for packages with similar names.
]]
return{name="search",help="Search for a package",description=d,syntax="<name>",execute=r}end
a["bsrocks.commands.repl"]=function(...)local n=i"bsrocks.env"local s=i"bsrocks.lib.dump"
local h=i"bsrocks.lib.parse"
local function r(...)local l=true;local n=n()local u=n._G
u.exit=setmetatable({},{__tostring=function()return"Call exit() to exit"end,__call=function()
l=false end})u._noTail=function(...)return...end;u.arg={[0]="repl",...}local c={}
u.Out=c;local m,f,w=colours.green,colours.cyan,term.getTextColour()
local y,p=colours.lightGrey,colours.lightBlue;if not term.isColour()then m=colours.white;f=colours.white
y=colours.white;p=colours.white end;local v=nil
if not
settings or settings.get("lua.autocomplete")then
v=function(E)
local T=E:find("[a-zA-Z0-9_%.]+$")if T then E=E:sub(T)end
if#E>0 then return textutils.complete(E,u)end end end;local b={}local g=1
local function k(E,T)u._=E;u['_'..g]=E;c[g]=E;term.setTextColour(f)write(
"Out["..g.."]: ")term.setTextColour(w)
if
type(E)=="table"then local A=getmetatable(E)if type(A)=="table"and type(A.__tostring)==
"function"then print(tostring(E))else
print(s(E,T))end else print(s(E))end end
local function q(E,T,...)
if T then local A=select('#',...)
if A==0 then if E then k(nil)end elseif A==1 then k(...)else k({...},A)end else printError(...)end end;local function j(E,T,A,O)local I=E[T]term.setTextColour(y)print(" "..I)
term.setTextColour(p)print((" "):rep(A).."^ ")
printError(" "..O)end
local function r(E,T)
local A=table.concat(E,"\n")local O=false;local I,N=load(A,"lua","t",u)
local S,H=load("return "..A,"lua","t",u)
if not I then
if S then
I=load("return _noTail("..A..")","lua","t",u)O=true else local R,D=pcall(h.lex,A)
if not R then
local U,C,M,F=D:match("(%d+):(%d+):([01]):(.+)")
if U then if U==#E and C>#E[U]and M==1 then return false else
j(E,tonumber(U),tonumber(C),F)return true end else printError(D)return
true end end;local R,L=pcall(h.parse,D)if not R then
if not T and D.pointer>=#D.tokens then return
false else local U=D.tokens[D.pointer]j(E,U.line,U.char,L)return true end end end elseif S then
I=load("return _noTail("..A..")","lua","t",u)end;if I then q(O,pcall(I))g=g+1 else printError(N)end
return true end;local x={}local z="In ["..g.."]: "local _=false
while l do
term.setTextColour(m)write(z)term.setTextColour(w)local E=read(nil,b,v)
if not E then break end
if#E:gsub("%s","")>0 then for T=#b,1,-1 do
if b[T]==E then table.remove(b,T)break end end;b[#b+1]=E;x[#x+1]=E;_=false;if r(x)then x={}z="In ["..g..
"]: "else
z=(" "):rep(#tostring(g)+3).."... "end else r(x,true)x={}_=false
z="In ["..g.."]: "end end;for E,T in pairs(n.cleanup)do T()end end
local d=[[
This is almost identical to the built in Lua program with some simple differences.
Scripts are run in an environment similar to the exec command.
The result of the previous outputs are also stored in variables of the form _idx (the last result is also stored in _). For example: if Out[1] = 123 then _1 = 123 and _ = 123
]]
return{name="repl",help="Run a Lua repl in an emulated environment",syntax="",description=d,execute=r}end
a["bsrocks.commands.remove"]=function(...)local n=i"bsrocks.rocks.install"
local s=i"bsrocks.rocks.rockspec"
local h=[[
<name> The name of the package to install
Removes a package. This does not remove its dependencies
]]
return
{name="remove",help="Removes a package",syntax="<name>",description=h,execute=function(r,d)
if not r then error("Expected name",0)end;r=r:lower()local l,u=n.getInstalled()local c,m=l[r],u[r]if not c then
error(r.." is not installed",0)end;n.remove(c,m)end}end
a["bsrocks.commands.list"]=function(...)local n=i"bsrocks.rocks.install"
local s=i"bsrocks.lib.utils".printColoured
local function h()
for r,d in pairs(n.getInstalled())do
if not d.builtin then
print(d.package..": "..d.version)if d.description and d.description.summary then
s(" "..d.description.summary,colours.lightGrey)end end end end
return{name="list",help="List installed packages",syntax="",execute=h}end
a["bsrocks.commands.install"]=function(...)local n=i"bsrocks.rocks.install"
local s=[[
<name> The name of the package to install
[version] The version of the package to install
Installs a package and all dependencies. This will also
try to upgrade a package if required.
]]
return
{name="install",help="Install a package",syntax="<name> [version]",description=s,execute=function(h,r)
if not h then error("Expected name",0)end;n.install(h,r)end}end
a["bsrocks.commands.exec"]=function(...)local n=i"bsrocks.env"local s=i"bsrocks.lib.settings"
local h=[[
<file> The file to execute relative to the current directory.
[args...] Arguments to pass to the program.
This will execute the program in an emulation of Lua 5.1's environment.
Please note that the environment is not a perfect emulation.
]]
return
{name="exec",help="Execute a command in the emulated environment",syntax="<file> [args...]",description=h,execute=function(r,...)
if not r then error("Expected file",0)end
if r:sub(1,1)=="@"then r=r:sub(2)local m
for f,w in ipairs(s.binPath)do
w=w:gsub("%%{(%a+)}",s):gsub("%?",r)if fs.exists(w)then m=w;break end end;r=m or shell.resolveProgram(r)or r else
r=shell.resolve(r)end;local n=n()local d=n._G
d.arg={[-2]="/"..shell.getRunningProgram(),[-1]="exec",[0]="/"..r,...}local o,l=loadfile(r,d)if not o then error(l,0)end;local u={...}
local c,l=xpcall(function()return
o(unpack(u))end,function(l)l=n.getError(l)
if type(l)=="string"then
local m=l:match("^Exit code: (%d+)")if m and m=="0"then return"<nop>"end end;if l==nil then return"nil"else l=tostring(l)end;return
d.debug.traceback(l,2)end)for m,f in pairs(n.cleanup)do f()end;if not c and l~="<nop>"then
if l=="nil"then l=nil end;error(l,0)end end}end
a["bsrocks.commands.dumpsettings"]=function(...)local n=i"bsrocks.lib.files"
local s=i"bsrocks.lib.serialize"local h=i"bsrocks.lib.settings"local r=i"bsrocks.lib.utils"
return
{name="dump-settings",help="Dump all settings",syntax="",description="Dump all settings to a .bsrocks file. This can be changed to load various configuration options.",execute=function()
local d=s.serialize(h)r.log("Dumping to .bsrocks")n.write(".bsrocks",d)end}end
a["bsrocks.commands.desc"]=function(...)local n=i"bsrocks.rocks.dependencies"
local s=i"bsrocks.downloaders"local h=i"bsrocks.rocks.install"local r=i"bsrocks.rocks.patchspec"
local d=i"bsrocks.rocks.rockspec"local l=i"bsrocks.lib.settings"local u=i"bsrocks.lib.utils"local c=l.servers
local m,f=u.printColoured,u.writeColoured
local function w(p)if not p then error("Expected <name>",0)end
p=p:lower()local v,b=h.getInstalled()local g=true;local k,q=v[p],b[p]
if not k then g=false
local z=d.findRockspec(p)
if not z then error("Cannot find '"..p.."'",0)end;local _=r.findPatchspec(p)local E;if _ then E=_.patches[p]else
E=d.latestVersion(z,p,constraints)end
k=d.fetchRockspec(z.server,p,E)q=_ and r.fetchPatchspec(_.server,p)end;write(p..": "..k.version.." ")if k.builtin then
f("Built In",colours.magenta)elseif g then f("Installed",colours.green)else
f("Not installed",colours.red)end;if q then
f(" (+Patchspec)",colours.lime)end;print()local j=k.description
if j then if j.summary then
m(j.summary,colours.cyan)end
if j.detailed then local z=j.detailed;local _=z:match("^(%s+)")
if _ then z=z:sub(
#_+1):gsub("\n".._,"\n")end;z=z:gsub("^\n+",""):gsub("%s+$","")
m(z,colours.white)end;if j.homepage then
m("URL: "..j.homepage,colours.lightBlue)end end
if not g then local z,_=h.findIssues(k,q)
if#_>0 then
m("Issues",colours.orange)
if z then m("This package is incompatible",colors.red)end;for E,T in ipairs(_)do local A=colors.yellow;if T[2]then A=colors.red end
m(" "..T[1],A)end end end;local x=k.dependencies
if q and q.dependencies then x=q.dependencies end
if x and#x>0 then m("Dependencies",colours.orange)local z=0;for E,x in ipairs(x)do z=math.max(z,
#x)end;z=z+1
for E,x in ipairs(x)do
local _=n.parseDependency(x)local p=_.name;local T=v[p]
write(" "..x.. (" "):rep(z-#x))
if T then local A=n.parseVersion(T.version)
if not
n.matchConstraints(A,_.constraints)then m("Wrong version",colours.yellow)elseif
T.builtin then m("Built In",colours.magenta)else
m("Installed",colours.green)end else m("Not installed",colours.red)end end end end
local y=[[
<name> The name of the package to search for.
Prints a description about the package, listing its description, dependencies and other useful information.
]]
return{name="desc",help="Print a description about a package",description=y,syntax="<name>",execute=w}end
a["bsrocks.commands.admin.make"]=function(...)local n=i"bsrocks.lib.files"
local s=i"bsrocks.lib.utils".log;local h=i"bsrocks.lib.settings".patchDirectory
local r=i"bsrocks.rocks.patchspec"local d=i"bsrocks.lib.serialize"
local function l(...)local c,m
if select("#",...)==0 then m=false
c=r.getAll()else m=true;c={}
for f,w in pairs({...})do w=w:lower()
local y=fs.combine(h,"rocks/"..w..".patchspec")if not fs.exists(y)then
error("No such patchspec "..w,0)end;c[w]=d.unserialize(n.read(y))end end
for f,w in pairs(c)do local y=fs.combine(h,"rocks-original/"..f)local p=fs.combine(h,
"rocks-changes/"..f)
local v=fs.combine(h,"rocks/"..f)local b=v..".patchspec"s("Making "..f)
n.assertExists(y,"original sources for "..f,0)
n.assertExists(p,"changed sources for "..f,0)n.assertExists(b,"patchspec for "..f,0)
local w=d.unserialize(n.read(b))local g=n.readDir(y,n.readLines)
local k=n.readDir(p,n.readLines)local q,j,x,z=r.makePatches(g,k)w.patches=j;w.added=x;w.removed=z
n.writeDir(v,q,n.writeLines)n.write(b,d.serialize(w))end end
local u=[[
[name] The name of the package to create patches for. Otherwise all packages will make their patches.
]]return
{name="make-patches",help="Make patches for a package",syntax="[name]...",description=u,execute=l}end
a["bsrocks.commands.admin.fetch"]=function(...)local n=i"bsrocks.downloaders"
local s=i"bsrocks.lib.files"local h=i"bsrocks.lib.utils".log;local r=i"bsrocks.rocks.patchspec"
local d=i"bsrocks.rocks.rockspec"local l=i"bsrocks.lib.serialize"
local u=i"bsrocks.lib.settings".patchDirectory
local function c(...)local f,w
if select("#",...)==0 then w=false;f=r.getAll()else w=true;f={}
for p,v in pairs({...})do
v=v:lower()
local b=fs.combine(u,"rocks/"..v..".patchspec")if not fs.exists(b)then
error("No such patchspec "..v,0)end;f[v]=l.unserialize(s.read(b))end end;local y=false
for p,v in pairs(f)do
local b=fs.combine(u,"rocks-original/"..p)
if w or not fs.isDir(b)then y=true;h("Fetching "..p)
fs.delete(b)local g=v.version;if not v.version then
error("Patchspec"..p.." has no version",0)end;local k=d.findRockspec(p)
if not k then error(
"Cannot find '"..p.."'",0)end;local q=d.fetchRockspec(k.server,p,v.version)
local j=d.extractFiles(q)
if#j==0 then error("No files for "..p.."-"..g,0)end;local x=n(r.extractSource(q,v),j)if not x then
error("Cannot find downloader for "..
q.source.url,0)end;for p,z in pairs(x)do
s.writeLines(fs.combine(b,p),z)end
fs.delete(fs.combine(u,"rocks-changes/"..p))end end;if not y then error("No packages to fetch",0)end
print("Run 'apply-patches' to apply")end
local m=[[
[name] The name of the package to fetch. Otherwise all un-fetched packages will be fetched.
]]
return{name="fetch",help="Fetch a package for patching",syntax="[name]...",description=m,execute=c}end
a["bsrocks.commands.admin.apply"]=function(...)local n=i"bsrocks.lib.files"
local s=i"bsrocks.lib.utils".log;local h=i"bsrocks.lib.settings".patchDirectory
local r=i"bsrocks.rocks.patchspec"local d=i"bsrocks.lib.serialize"
local function l(...)local c,m
if select("#",...)==0 then m=false
c=r.getAll()elseif
select("#",...)==1 and(...=="-f"or...=="--force")then m=true;c=r.getAll()else m=true;c={}
for w,y in pairs({...})do y=y:lower()local p=fs.combine(h,"rocks/"..
y..".patchspec")
if
not fs.exists(p)then error("No such patchspec "..y,0)end;c[y]=d.unserialize(n.read(p))end end;local f=false
for w,y in pairs(c)do
local p=fs.combine(h,"rocks-original/"..w)local v=fs.combine(h,"rocks/"..w)
local b=fs.combine(h,"rocks-changes/"..w)
if m or not fs.isDir(b)then f=true;s("Applying "..w)n.assertExists(p,
"original sources for "..w,0)
fs.delete(b)local g=n.readDir(p,n.readLines)local k={}if fs.exists(v)then
k=n.readDir(v,n.readLines)end
local q=r.applyPatches(g,k,y.patches or{},y.added or{},y.removed or{})n.writeDir(b,q,n.writeLines)end end;if not f then error("No packages to patch",0)end end
local u=[[
[name] The name of the package to apply. Otherwise all un-applied packages will have their patches applied.
]]return
{name="apply-patches",help="Apply patches for a package",syntax="[name]...",description=u,execute=l}end
a["bsrocks.commands.admin.addrockspec"]=function(...)local n=i"bsrocks.lib.files"
local s=i"bsrocks.rocks.manifest"local h=i"bsrocks.lib.settings".patchDirectory
local r=i"bsrocks.rocks.rockspec"local d=i"bsrocks.lib.serialize"
local function l(c,m)
if not c then error("Expected name",0)end;c=c:lower()local f=r.findRockspec(c)if not f then
error("Cannot find '"..c.."'",0)end
if not m then m=r.latestVersion(f,c)end;local w={}
local y=fs.combine(h,"rocks/"..c.."-"..m..".rockspec")
if fs.exists(y)then w=d.unserialize(n.read(y))
if w.version==m then error(
"Already at version "..m,0)end else w=r.fetchRockspec(f.server,c,m)end;w.version=m;n.write(y,d.serialize(w))
local p,v=s.loadLocal()local b=p.repository[c]
if not b then b={}p.repository[c]=b end;b[m]={{arch="rockspec"}}
n.write(v,d.serialize(p))print("Added rockspec. Feel free to edit away!")end
local u=[[
<name> The name of the package
[version] The version to use
Refreshes a rockspec file to the original.
]]return
{name="add-rockspec",help="Add or update a rockspec",syntax="<name> [version]",description=u,execute=l}end
a["bsrocks.commands.admin.addpatchspec"]=function(...)local n=i"bsrocks.lib.files"
local s=i"bsrocks.rocks.manifest"local h=i"bsrocks.lib.settings".patchDirectory
local r=i"bsrocks.rocks.rockspec"local d=i"bsrocks.lib.serialize"
local function l(c,m)
if not c then error("Expected name",0)end;c=c:lower()local f=r.findRockspec(c)if not f then
error("Cannot find '"..c.."'",0)end
if not m then m=r.latestVersion(f,c)end;local w={}
local y=fs.combine(h,"rocks/"..c..".patchspec")
if fs.exists(y)then w=d.unserialize(n.read(y))end
if w.version==m then error("Already at version "..m,0)end;w.version=m;n.write(y,d.serialize(w))
fs.delete(fs.combine(h,"rocks-original/"..c))local p,v=s.loadLocal()p.patches[c]=m
n.write(v,d.serialize(p))
print("Run 'fetch "..c.."' to download files")end
local u=[[
<name> The name of the package
[version] The version to use
Adds a patchspec file, or sets the version of an existing one.
]]return
{name="add-patchspec",help="Add or update a package for patching",syntax="<name> [version]",description=u,execute=l}end
a["bsrocks.bin.bsrocks"]=function(...)local n={}
local function s(w)n[w.name]=w;if w.alias then
for y,p in ipairs(w.alias)do n[p]=w end end end;local h=i"bsrocks.lib.utils"local r,d=h.printColoured,h.printIndent
local l=i"bsrocks.lib.settings".patchDirectory;s(i"bsrocks.commands.desc")
s(i"bsrocks.commands.dumpsettings")s(i"bsrocks.commands.exec")
s(i"bsrocks.commands.install")s(i"bsrocks.commands.list")
s(i"bsrocks.commands.remove")s(i"bsrocks.commands.repl")
s(i"bsrocks.commands.search")
if fs.exists(l)then s(i"bsrocks.commands.admin.addpatchspec")
s(i"bsrocks.commands.admin.addrockspec")s(i"bsrocks.commands.admin.apply")
s(i"bsrocks.commands.admin.fetch")s(i"bsrocks.commands.admin.make")end
local function u(w)local y=n[w]
if not y then
printError("Cannot find '"..w.."'.")local p=i"bsrocks.lib.match"local v=false
for b,g in pairs(n)do if p(b,w)>0 then if not v then
r("Did you mean: ",colours.yellow)v=true end
r(" "..b,colours.orange)end end;error("No such command",0)else return y end end
s({name="help",help="Provide help for a command",syntax="[command]",description=" [command] The command to get help for. Leave blank to get some basic help for all commands.",execute=function(w)
if w then
local y=u(w)print(y.help)if y.syntax~=""then r("Synopsis",colours.orange)
r(
" "..y.name.." "..y.syntax,colours.lightGrey)end
if y.description then
r("Description",colours.orange)
local p=y.description:gsub("^\n+",""):gsub("\n+$","")
if term.isColor()then term.setTextColour(colours.lightGrey)end;for v in(p.."\n"):gmatch("([^\n]*)\n")do local b,g=v:find("^(%s*)")
d(v:sub(g+1),g)end;if term.isColor()then
term.setTextColour(colours.white)end end else r("bsrocks <command> [args]",colours.cyan)
r("Available commands",colours.lightGrey)
for y,p in pairs(n)do
print(" "..p.name.." "..p.syntax)r(" "..p.help,colours.lightGrey)end end end})local c=...
if not c or c=="-h"or c=="--help"then c="help"elseif
select(2,...)=="-h"or select(2,...)=="--help"then return
u("help").execute(c)end;local m=u(c)local f={...}
return m.execute(select(2,unpack(f)))end
if not shell or type(...or nil)=='table'then local n=...or{}
n.require=i;n.preload=a;return n else return a["bsrocks.bin.bsrocks"](...)end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment