· 8 years ago · Jun 17, 2018, 12:34 AM
1-- amber-client-2018-06-17T00:24:56Z-master-fb61285a
2local _nuggetFiles = {
3 [ "apis/serializer" ] = "function readFile(path)\
4 local file = fs.open(path, \"r\")\
5 if file == nil then\
6 return nil\
7 end\
8 local contents = file.readAll()\
9 file.close()\
10 return contents\
11end\
12\
13function readFromFile(path)\
14 local file = readFile(path)\
15 if file == nil then\
16 return {}\
17 end\
18 return textutils.unserialize(file)\
19end\
20\
21function writeFile(path, contents)\
22 local file = fs.open(path, \"w\")\
23 file.writeLine(contents)\
24 file.close()\
25end\
26\
27function writeToFile(path, contents)\
28 writeFile(path, textutils.serialize(contents))\
29end",
30 [ "apis/github" ] = "-- Forked from https://github.com/eric-wieser/computercraft-github/blob/master/apis/github\
31local JSON = dofile(\"apis/dkjson\")\
32\
33-- Build a github API url, with authorization headers.\
34local function getAPI(path, auth)\
35 local url = ('https://api.github.com/%s'):format(path)\
36 local headers\
37 if auth and auth.type == 'oauth' then\
38 headers = { ['Authorization'] = ('token %s'):format(auth.token) }\
39 end\
40 local req = http.get(url, headers)\
41 if req then\
42 return req.getResponseCode(), JSON.decode(req.readAll())\
43 else\
44 return nil, {}\
45 end\
46end\
47\
48local function encodeURI(s)\
49 -- TODO: Use textutils.urlEncode.\
50 return s:gsub(' ', '%20')\
51end\
52\
53-- A class for authorization\
54local authFile = '.github-auth'\
55local function writeAuth(data)\
56 f = fs.open(authFile, 'w')\
57 f.write(textutils.serialize(data))\
58 f.close()\
59end\
60local function getAuthTable()\
61 local authTable = {}\
62 if fs.exists(authFile) then\
63 f = fs.open(authFile, 'r')\
64 authTable = textutils.unserialize(f.readAll())\
65 f.close()\
66 end\
67 return authTable\
68end\
69local Auth = {}\
70Auth.__index = Auth\
71Auth.new = function(type, user, token)\
72 return setmetatable({type=type, user=user, token=token}, Auth)\
73end\
74Auth.get = function(user)\
75 local authTable = getAuthTable()\
76 local auth = authTable[user]\
77 if auth then\
78 auth = Auth.new(auth.type, auth.user, auth.token)\
79 end\
80 return auth\
81end\
82Auth.checkToken = function(self)\
83 local status, request = getAPI('user', self)\
84 return status == 200\
85end\
86Auth.save = function(self)\
87 local authTable = getAuthTable()\
88 authTable[self.user] = self\
89 writeAuth(authTable)\
90end\
91Auth.delete = function(user)\
92 local authTable = getAuthTable()\
93 authTable[user] = nil\
94 writeAuth(authTable)\
95end\
96\
97-- A class for a commit\
98local Commit = {}\
99Commit.__index = Commit\
100Commit.new = function(repo, sha)\
101 local url = ('repos/%s/%s/git/commits/%s'):format(repo.user, repo.name, sha)\
102 local status, data = getAPI(url, repo.auth)\
103 if not status then\
104 error('Could not get github API from ' ..url)\
105 end\
106 return setmetatable({data = data}, Commit)\
107end\
108\
109-- A class for a blob (aka a file)\
110local Blob = {}\
111Blob.__index = Blob\
112Blob.new = function(repo, sha, path)\
113 return setmetatable({repo=repo, sha=sha, path=path}, Blob)\
114end\
115Blob.fullPath = function(self, base)\
116 if self.parent and self.parent ~= base then\
117 return fs.combine(self.parent:fullPath(base), self.path)\
118 else\
119 return self.path\
120 end\
121end\
122\
123-- A class for a tree (aka a folder)\
124local Tree = {}\
125Tree.__index = Tree\
126Tree.new = function(repo, sha, path, rootSha)\
127 local url = ('repos/%s/%s/git/trees/%s'):format(repo.user, repo.name, sha)\
128 local status, data = getAPI(url, repo.auth)\
129 if not status then\
130 error('Could not get github API from ' ..url)\
131 end\
132 if data.tree then\
133 local tree = setmetatable({\
134 repo = repo,\
135 rootSha = rootSha or data.sha,\
136 sha = data.sha,\
137 path = path or '',\
138 size = 0,\
139 contents = {}\
140 }, Tree)\
141 for _, childdata in ipairs(data.tree) do\
142 childdata.fullPath = fs.combine(tree:fullPath(), childdata.path)\
143 local child\
144 if childdata.type == 'blob' then\
145 child = Blob.new(repo, childdata.sha, childdata.path)\
146 child.size = childdata.size\
147 elseif childdata.type == 'tree' then\
148 child = Tree.new(repo, childdata.sha, childdata.path, tree.rootSha)\
149 else\
150 error(\"uh oh\", JSON.encode(childdata))\
151 child = childdata\
152 end\
153 tree.size = tree.size + child.size\
154 child.parent = tree\
155 table.insert(tree.contents, child)\
156 end\
157 return tree\
158 else\
159 error(\"uh oh\", JSON.encode(data))\
160 end\
161end\
162local function walkTree(t, level)\
163 for _, item in ipairs(t.contents) do\
164 coroutine.yield(item, level)\
165 if getmetatable(item) == Tree then\
166 walkTree(item, level + 1)\
167 end\
168 end\
169end\
170Tree.getChild = function(self, path)\
171 for _, item in ipairs(self.contents) do\
172 if item.path == path then\
173 return item\
174 end\
175 end\
176end\
177Tree.getCommit = function(self)\
178 return Commit.new(self.repo, self.rootSha)\
179end\
180Tree.iter = function(self)\
181 return coroutine.wrap(function()\
182 walkTree(self, 0)\
183 end)\
184end\
185Tree.cloneTo = function(self, dest, onProgress)\
186 if not fs.exists(dest) then\
187 fs.makeDir(dest)\
188 elseif not fs.isDir(dest) then\
189 return error(\"Destination is a file!\")\
190 end\
191\
192 for item in self:iter() do\
193 local gitpath = item:fullPath()\
194 local path = fs.combine(dest, gitpath)\
195 if getmetatable(item) == Tree then\
196 fs.makeDir(path)\
197 elseif getmetatable(item) == Blob then\
198 local data = http.get(\
199 ('https://raw.github.com/%s/%s/%s/%s'):format(\
200 self.repo.user, self.repo.name, self.sha,\
201 encodeURI(gitpath)\
202 )\
203 )\
204 local h = fs.open(path, 'w')\
205 local text = data.readAll()\
206 h.write(text)\
207 h.close()\
208 end\
209 if onProgress then onProgress(item) end\
210 end\
211end\
212Tree.copyTo = function(self, dest, onProgress)\
213 for _, item in ipairs(self.contents) do\
214 local gitpath = item:fullPath()\
215 if getmetatable(item) == Tree then\
216 dest[item.path] = {}\
217 item:copyTo(dest[item.path], onProgress)\
218 elseif getmetatable(item) == Blob then\
219 local data = http.get(\
220 ('https://raw.github.com/%s/%s/%s/%s'):format(\
221 self.repo.user, self.repo.name, self.rootSha,\
222 encodeURI(gitpath)\
223 )\
224 )\
225 dest[item.path] = data.readAll()\
226 end\
227 if onProgress then onProgress(item) end\
228 end\
229end\
230Tree.fullPath = Blob.fullPath\
231\
232-- A class for a release\
233local Release = {}\
234Release.__index = Release\
235Release.new = function(repo, tag)\
236 local r = setmetatable({repo=repo, tag=tag}, Release)\
237 return r\
238end\
239Release.tree = function(self)\
240 return self.repo:tree(self.tag)\
241end\
242\
243-- A class for a repository\
244local __repoPriv = setmetatable({}, {mode='k'})\
245local Repository = {}\
246Repository.__index = Repository\
247Repository.new = function(user, name, auth)\
248 local r = setmetatable({user=user, name=name, auth=auth}, Repository)\
249 __repoPriv[r] = {trees={}}\
250 return r\
251end\
252Repository.tree = function(self, sha)\
253 sha = sha or \"master\"\
254 -- TODO: This is a nice thought, but not great in practice since master can change frequently!\
255 if not __repoPriv[self].trees[sha] then\
256 __repoPriv[self].trees[sha] = Tree.new(self, sha)\
257 end\
258 return __repoPriv[self].trees[sha]\
259end\
260Repository.latestRelease = function(self)\
261 local url = ('repos/%s/%s/releases/latest'):format(self.user, self.name)\
262 local status, data = getAPI(url, self.auth)\
263 if not status then\
264 error('Could not get github API from ' ..url)\
265 end\
266 return Release.new(self, data[\"tag_name\"])\
267end\
268Repository.releaseForTag = function(self, tag)\
269 local url = ('repos/%s/%s/releases/tags/%s'):format(self.user, self.name, tag)\
270 local status, data = getAPI(url, self.auth)\
271 if not status then\
272 error('Could not get github API from ' ..url)\
273 end\
274 return Release.new(self, data[\"tag_name\"])\
275end\
276Repository.__tostring = function(self) return (\"Repo@%s/%s\"):format(self.user, self.name) end\
277\
278-- Export members\
279local github = {}\
280github.Repository = Repository\
281github.Commit = Commit\
282github.Blob = Blob\
283github.Tree = Tree\
284github.Auth = Auth\
285github.Release = Release\
286github.repo = Repository.new\
287return github",
288 [ "apis/net" ] = "os.loadAPI(\"apis/events\")\
289os.loadAPI(\"apis/util\")\
290os.loadAPI(\"apis/dns\")\
291\
292-- Modems\
293function detectModem()\
294 local sides = peripheral.getNames()\
295 for _, side in ipairs(sides) do\
296 if peripheral.getType(side) == \"modem\" and peripheral.call(side, \"isWireless\") then\
297 return side\
298 end\
299 end\
300 return nil\
301end\
302\
303function openModem(side)\
304 if side == nil then\
305 side = detectModem()\
306 if side == nil then\
307 error(\"Unable to automatically detect modem -- did you attach one?\")\
308 end\
309 end\
310 rednet.open(side)\
311 return side\
312end\
313\
314-- Message delivery\
315function sendRawMessage(destination, msg)\
316 local address = dns.resolve(destination)\
317 if address == nil then\
318 return false\
319 end\
320 return rednet.send(address, msg)\
321end\
322\
323function createMessage(msgType, data)\
324 return {msgType, data}\
325end\
326\
327function sendMessage(destination, msgType, data)\
328 return sendRawMessage(destination, createMessage(msgType, data))\
329end\
330\
331function sendMessages(destination, messages)\
332 return sendRawMessage(destination, messages)\
333end\
334\
335function broadcastMessage(destination, msgType, data)\
336 local recipients = {dns.resolve(destination)}\
337 for _, recipient in ipairs(recipients) do\
338 sendMessage(recipient, msgType, data)\
339 end\
340end\
341\
342-- Message handlers\
343local rednetHandlers = util.initializeGlobalTable(\"rednetHandlers\")\
344\
345function pullMessage(msgType)\
346 -- TODO: Better to implement this with protocols, which weren't supported when this library was first written.\
347 while true do\
348 local senderId, msg = rednet.receive()\
349 if msg[1] == msgType then\
350 return msg[2]\
351 end\
352 end\
353end\
354\
355function registerRawMessageHandler(msgType, handler)\
356 rednetHandlers[msgType] = handler\
357end\
358\
359function registerRawLocalMessageHandler(msgType, handler)\
360 util.getCoroutineTable(\"rednetHandlers\")[msgType] = handler\
361end\
362\
363function registerMessageHandler(msgType, handler)\
364 registerRawMessageHandler(msgType, function(data) return handler(data[2]) end)\
365end\
366\
367function registerLocalMessageHandler(msgType, handler)\
368 registerRawLocalMessageHandler(msgType, function(data) return handler(data[2]) end)\
369end\
370\
371function removeHandler(msgType)\
372 rednetHandlers[msgType] = nil\
373end\
374\
375function removeLocalHandler(msgType)\
376 util.getCoroutineTable(\"rednetHandlers\")[msgType] = nil\
377end\
378\
379function getRednetHandler(msgType)\
380 local handler = util.getCoroutineTable(\"rednetHandlers\")[msgType]\
381 if handler == nil then\
382 handler = rednetHandlers[msgType]\
383 end\
384 return handler\
385end\
386\
387function dispatchMessage(msg, sender, distance)\
388 local handler = getRednetHandler(msg[1])\
389 if handler ~= nil then\
390 return handler(msg, sender, distance)\
391 end\
392end\
393\
394function handleRednetMessage(msgType, sender, msg, distance)\
395 if msg == nil then\
396 return\
397 end\
398 if type(msg[1]) == \"table\" then\
399 for _, subMessage in ipairs(msg) do\
400 if dispatchMessage(subMessage, sender, distance) == false then\
401 return false\
402 end\
403 end\
404 else\
405 return dispatchMessage(msg, sender, distance)\
406 end\
407end\
408\
409events.registerHandler(\"rednet_message\", handleRednetMessage)\
410\
411registerRawMessageHandler(\"ping\", function(data, sender)\
412 print(string.format(\"Ping received; sending reply to %i\", sender))\
413 sendMessage(sender, \"pingReply\", {position = {gps.locate(5)}})\
414end)",
415 [ "apis/version" ] = "local Version = {}\
416Version.__index = Version\
417Version.compareTo = function(a, b)\
418 -- TODO: Doesn't handle trailing zeroes correctly. Could iterate through max(#a, #b).\
419 -- We should probably just imagine that the versions both have an unlimited number of \
420 -- trailing zeroes.\
421 for i, aPart in ipairs(a.components) do\
422 local bPart = b.components[i]\
423 if bPart == nil then\
424 return 1 -- More numbers means a is a later version.\
425 else\
426 if aPart < bPart then\
427 return -1\
428 elseif aPart > bPart then\
429 return 1\
430 end\
431 end\
432 end\
433 -- If they are equal so far but b has more numbers, b is later.\
434 if #b.components > #a.components then\
435 return -1\
436 end\
437 return 0\
438end\
439\
440function new(version)\
441 local parsed = {}\
442 version:gsub(\"(%d+)\", function(match) table.insert(parsed, tonumber(match)) end)\
443 return setmetatable({components = parsed}, Version)\
444end",
445 [ "apis/amber-repositories/GitHub" ] = "local github = dofile(\"apis/github\")\
446\
447local GitHub = {}\
448GitHub.__index = GitHub\
449GitHub.new = function(data)\
450 local auth\
451 if data.auth then\
452 auth = github.Auth.new(data.auth.type, data.auth.user, data.auth.token)\
453 end\
454 local repo = github.repo(data.user, data.name, auth)\
455 return setmetatable({\
456 repo = repo,\
457 defaultVersion = data.defaultVersion,\
458 path = data.path or \"src\"\
459 }, GitHub)\
460end\
461GitHub.getDefaultTree = function(self)\
462 local version, tree\
463 if self.defaultVersion then\
464 tree = self.repo:tree(self.defaultVersion)\
465 version = string.format(\"%s-%s-%s\", tree:getCommit().data.committer.date, self.defaultVersion, tree.sha:sub(1, 8))\
466 else\
467 local release = self.repo:latestRelease()\
468 version = release.tag\
469 tree = release:tree()\
470 end\
471 return version, tree\
472end\
473GitHub.bindPackageId = function(self, packageId)\
474 local version = packageId.version\
475 if version == nil then\
476 return self:getDefaultTree()\
477 end\
478 local release = self.repo:releaseByTag(version)\
479 return version, release:tree()\
480end\
481GitHub.bindPackage = function(self, packageId)\
482 local version, tree = self:bindPackageId(packageId)\
483 local repoRoot = tree:getChild(self.path)\
484 if repoRoot == nil then\
485 return nil, string.format(\"Unable to locate repository root %s\", self.path)\
486 end\
487 local packageTree = repoRoot:getChild(packageId.name)\
488 if packageTree == nil then\
489 return nil, string.format(\"Unable to find package %s in repository root %s\", packageId.name, self.path)\
490 end\
491 \
492 local contents = {}\
493 packageTree:copyTo(contents, nil)\
494 return {\
495 id = {name = packageId.name, version = version},\
496 contents = contents\
497 }\
498end\
499GitHub.getAvailablePackages = function(self)\
500 local version, tree = self:getDefaultTree()\
501 local repoRoot = tree:getChild(self.path)\
502 if repoRoot == nil then\
503 return {}\
504 end\
505 \
506 local packages = {}\
507 for _, file in ipairs(repoRoot.contents) do\
508 if getmetatable(file) == github.Tree then\
509 -- TODO: Read the manifest.\
510 packages[file.path] = {\
511 id = {name = file.path, version = version}\
512 }\
513 end\
514 end\
515 return packages\
516end\
517\
518return GitHub",
519 [ "nugget/.repositories" ] = "{\
520 {\
521 type = \"GitHub\",\
522 user = \"danports\",\
523 name = \"amber\",\
524 defaultVersion = \"master\",\
525 }\
526}",
527 amber = "-- If we're running off a disk, we want to load all of the APIs from the disk, not the computer,\
528-- to ensure that they are compatible with this code.\
529-- TODO: Side-by-side loading (we could just embed all dependencies into this file).\
530if os.originalLoadAPI == nil then\
531 os.originalLoadAPI = os.loadAPI\
532 local dir = fs.getDir(shell.getRunningProgram())\
533 os.loadAPI = function(api)\
534 os.originalLoadAPI(fs.combine(dir, api))\
535 end\
536end\
537\
538os.loadAPI(\"apis/events\")\
539os.loadAPI(\"apis/net\")\
540os.loadAPI(\"apis/amber\")\
541\
542-- Connect to an Amber server by default.\
543-- Load the configuration from the program's location, but update the current folder.\
544local client = amber.PackageClient.fromConfigurationPath(fs.getDir(shell.getRunningProgram()), {{type = \"AmberServer\"}})\
545local installation = amber.PackageInstallation.new(\
546 shell.dir(),\
547 -- Edit template files interactively.\
548 function(file) shell.run(\"edit\", \"/\" .. file) end,\
549 function(progress) print(progress) end\
550)\
551\
552local function createPackageIds(names)\
553 local ids = {}\
554 for _, name in ipairs(names) do\
555 table.insert(ids, {name = name})\
556 end\
557 return ids\
558end\
559local function resolveNuggetItems(items)\
560 local packages = {}\
561 local files = {}\
562 for _, item in ipairs(items) do\
563 if item:sub(1, 5) == \"file:\" then\
564 table.insert(files, shell.resolve(item:sub(6)))\
565 elseif item:sub(1, 8) == \"package:\" then\
566 table.insert(packages, {name = item:sub(9)})\
567 else\
568 print(string.format(\"Unknown nugget item %s; ignoring it\", item))\
569 end\
570 end\
571 return packages, files\
572end\
573\
574local commands = {\
575 install = function(args)\
576 if next(args) == nil then\
577 return false\
578 end\
579 installation:installPackages(client, createPackageIds(args))\
580 end,\
581 update = function(args)\
582 local toUpdate\
583 if next(args) ~= nil then\
584 toUpdate = createPackageIds(args)\
585 end\
586 installation:updatePackages(client, toUpdate)\
587 end,\
588 remove = function(args)\
589 if next(args) == nil then\
590 return false\
591 end\
592 installation:removePackages(createPackageIds(args))\
593 end,\
594 search = function(args)\
595 local packages = client:getAvailablePackages()\
596 for name, package in pairs(packages) do\
597 if args[1] == nil or name:find(args[1]) then\
598 print(amber.formatPackageId(package.id))\
599 end\
600 end\
601 end,\
602 nugget = function(args)\
603 if #args < 2 then\
604 return false\
605 end\
606 local op = args[1]\
607 local opActions = {\
608 save = function(packageId, ...)\
609 local additionalPackages, additionalFiles = resolveNuggetItems({...})\
610 local nugget, errorMessage = client:buildNuggetFor({name = packageId}, additionalPackages, additionalFiles)\
611 if nugget == nil then\
612 printError(errorMessage)\
613 return\
614 end\
615 nugget:writeTo(shell.dir())\
616 print(string.format(\"%s nugget saved.\", packageId))\
617 end,\
618 run = function(packageId, ...)\
619 local nugget, errorMessage = client:buildNuggetFor({name = packageId})\
620 if nugget == nil then\
621 printError(errorMessage)\
622 return\
623 end\
624 nugget:run(...)\
625 end\
626 }\
627 local opAction = opActions[op]\
628 if opAction == nil then\
629 return false\
630 end\
631 local packageId = args[2]\
632 opAction(packageId, select(3, table.unpack(args)))\
633 end\
634}\
635\
636-- TODO: Do we need this? We might be performing a local operation only.\
637net.openModem()\
638local action = select(1, ...)\
639local args = {select(2, ...)}\
640if action == nil or commands[action] == nil or commands[action](args) == false then\
641 print(\"Usage:\")\
642 print(\"amber install <package1> <package2>...\")\
643 print(\" Installs or updates specified packages.\")\
644 print(\"amber update [<package1> <package2>...]\")\
645 print(\" Updates specified or all installed packages.\")\
646 print(\"amber remove <package1> <package2>...\")\
647 print(\" Removes specified packages.\")\
648 print(\"amber search [<pattern>]\")\
649 print(\" Lists all available packages, or just those matching <pattern>.\")\
650 print(\"amber nugget save <package> [<package:name> <file:path>...]\")\
651 print(\" Save nugget to file with additional packages and files included.\")\
652 print(\"amber nugget run <package> ...\")\
653 print(\" Runs nugget with provided arguments.\")\
654end",
655 [ "apis/events" ] = "os.loadAPI(\"apis/util\")\
656\
657local eventHandlers = util.initializeGlobalTable(\"eventHandlers\")\
658\
659function registerHandler(eventType, handler)\
660 eventHandlers[eventType] = handler\
661end\
662\
663function registerLocalHandler(eventType, handler)\
664 util.getCoroutineTable(\"eventHandlers\")[eventType] = handler\
665end\
666\
667local timerHandlers = util.initializeGlobalTable(\"timerHandlers\")\
668\
669function handleTimer(evt, timer)\
670 local handler = timerHandlers[timer]\
671 if handler == nil then\
672 return\
673 end\
674 timerHandlers[timer] = nil\
675 return handler(timer)\
676end\
677\
678function setTimer(timeout, handler)\
679 local id = os.startTimer(timeout)\
680 timerHandlers[id] = handler\
681 return id\
682end\
683\
684registerHandler(\"timer\", handleTimer)\
685registerHandler(\"terminate\", function() return false end)\
686\
687function getEventHandler(eventType)\
688 local handler = util.getCoroutineTable(\"eventHandlers\")[eventType]\
689 if handler == nil then\
690 handler = eventHandlers[eventType]\
691 end\
692 return handler\
693end\
694\
695function dispatchMessage(eventType, ...)\
696 local handler = getEventHandler(eventType)\
697 if handler == nil then\
698 return\
699 end\
700 if handler(eventType, ...) == false then\
701 return false\
702 end\
703end\
704\
705function runMessageLoop()\
706 while true do\
707 if dispatchMessage(os.pullEvent()) == false then\
708 return\
709 end\
710 end\
711end\
712\
713function runParallelMessageLoop()\
714 local routines = {}\
715 local filters = {}\
716 while true do\
717 -- TODO: We should use table.pack; see:\
718 -- https://github.com/dan200/ComputerCraft/commit/bd14223ea86e607bfe5e3cbeb02d33542c0c2ec9\
719 local eventData = {os.pullEventRaw()}\
720 -- Add a new coroutine to handle the current event.\
721 -- TODO: How is this going to work with coroutine-specific event handlers? Not well...\
722 table.insert(routines, coroutine.create(dispatchMessage))\
723\
724 -- Dispatch the event to all active coroutines and clean up the dead ones.\
725 for n = #routines, 1, -1 do\
726 local r = routines[n]\
727 if coroutine.status(r) == \"dead\" then\
728 table.remove(routines, n)\
729 filters[r] = nil\
730 elseif filters[r] == nil or filters[r] == eventData[1] or eventData[1] == \"terminate\" then\
731 -- We assume that the coroutine yielded with an os.pullEvent call.\
732 local ok, param = coroutine.resume(r, table.unpack(eventData))\
733 if coroutine.status(r) == \"dead\" then\
734 table.remove(routines, n)\
735 filters[r] = nil\
736 end\
737 if ok then\
738 -- dispatchMessage returns false to indicate that we should quit the message loop.\
739 if param == false then\
740 return\
741 else\
742 filters[r] = param\
743 end\
744 else\
745 error(param, 0)\
746 end\
747 end\
748 end\
749 end\
750end",
751 [ "apis/dkjson" ] = " -- Module options:\
752 local always_try_using_lpeg = false\
753\
754 --[==[\
755\
756David Kolf's JSON module for Lua 5.1/5.2\
757========================================\
758\
759*Version 2.2*\
760\
761This module writes no global values, not even the module table.\
762Import it using\
763\
764 json = require (\"dkjson\")\
765\
766Exported functions and values:\
767\
768`json.encode (object [, state])`\
769--------------------------------\
770\
771Create a string representing the object. `Object` can be a table,\
772a string, a number, a boolean, `nil`, `json.null` or any object with\
773a function `__tojson` in its metatable. A table can only use strings\
774and numbers as keys and its values have to be valid objects as\
775well. It raises an error for any invalid data types or reference\
776cycles.\
777\
778`state` is an optional table with the following fields:\
779\
780 - `indent` \
781 When `indent` (a boolean) is set, the created string will contain\
782 newlines and indentations. Otherwise it will be one long line.\
783 - `keyorder` \
784 `keyorder` is an array to specify the ordering of keys in the\
785 encoded output. If an object has keys which are not in this array\
786 they are written after the sorted keys.\
787 - `level` \
788 This is the initial level of indentation used when `indent` is\
789 set. For each level two spaces are added. When absent it is set\
790 to 0.\
791 - `buffer` \
792 `buffer` is an array to store the strings for the result so they\
793 can be concatenated at once. When it isn't given, the encode\
794 function will create it temporary and will return the\
795 concatenated result.\
796 - `bufferlen` \
797 When `bufferlen` is set, it has to be the index of the last\
798 element of `buffer`.\
799 - `tables` \
800 `tables` is a set to detect reference cycles. It is created\
801 temporary when absent. Every table that is currently processed\
802 is used as key, the value is `true`.\
803\
804When `state.buffer` was set, the return value will be `true` on\
805success. Without `state.buffer` the return value will be a string.\
806\
807`json.decode (string [, position [, null]])`\
808--------------------------------------------\
809\
810Decode `string` starting at `position` or at 1 if `position` was\
811omitted.\
812\
813`null` is an optional value to be returned for null values. The\
814default is `nil`, but you could set it to `json.null` or any other\
815value.\
816\
817The return values are the object or `nil`, the position of the next\
818character that doesn't belong to the object, and in case of errors\
819an error message.\
820\
821Two metatables are created. Every array or object that is decoded gets\
822a metatable with the `__jsontype` field set to either `array` or\
823`object`. If you want to provide your own metatables use the syntax\
824\
825 json.decode (string, position, null, objectmeta, arraymeta)\
826\
827To prevent the assigning of metatables pass `nil`:\
828\
829 json.decode (string, position, null, nil)\
830\
831`<metatable>.__jsonorder`\
832-------------------------\
833\
834`__jsonorder` can overwrite the `keyorder` for a specific table.\
835\
836`<metatable>.__jsontype`\
837------------------------\
838\
839`__jsontype` can be either `\"array\"` or `\"object\"`. This value is only\
840checked for empty tables. (The default for empty tables is `\"array\"`).\
841\
842`<metatable>.__tojson (self, state)`\
843------------------------------------\
844\
845You can provide your own `__tojson` function in a metatable. In this\
846function you can either add directly to the buffer and return true,\
847or you can return a string. On errors nil and a message should be\
848returned.\
849\
850`json.null`\
851-----------\
852\
853You can use this value for setting explicit `null` values.\
854\
855`json.version`\
856--------------\
857\
858Set to `\"dkjson 2.2\"`.\
859\
860`json.quotestring (string)`\
861---------------------------\
862\
863Quote a UTF-8 string and escape critical characters using JSON\
864escape sequences. This function is only necessary when you build\
865your own `__tojson` functions.\
866\
867`json.addnewline (state)`\
868-------------------------\
869\
870When `state.indent` is set, add a newline to `state.buffer` and spaces\
871according to `state.level`.\
872\
873LPeg support\
874------------\
875\
876When the local configuration variable `always_try_using_lpeg` is set,\
877this module tries to load LPeg to replace the `decode` function. The\
878speed increase is significant. You can get the LPeg module at\
879 <http://www.inf.puc-rio.br/~roberto/lpeg/>.\
880When LPeg couldn't be loaded, the pure Lua functions stay active.\
881\
882In case you don't want this module to require LPeg on its own,\
883disable the option `always_try_using_lpeg` in the options section at\
884the top of the module.\
885\
886In this case you can later load LPeg support using\
887\
888### `json.use_lpeg ()`\
889\
890Require the LPeg module and replace the functions `quotestring` and\
891and `decode` with functions that use LPeg patterns.\
892This function returns the module table, so you can load the module\
893using:\
894\
895 json = require \"dkjson\".use_lpeg()\
896\
897Alternatively you can use `pcall` so the JSON module still works when\
898LPeg isn't found.\
899\
900 json = require \"dkjson\"\
901 pcall (json.use_lpeg)\
902\
903### `json.using_lpeg`\
904\
905This variable is set to `true` when LPeg was loaded successfully.\
906\
907---------------------------------------------------------------------\
908\
909Contact\
910-------\
911\
912You can contact the author by sending an e-mail to 'kolf' at the\
913e-mail provider 'gmx.de'.\
914\
915---------------------------------------------------------------------\
916\
917*Copyright (C) 2010, 2011, 2012 David Heiko Kolf*\
918\
919Permission is hereby granted, free of charge, to any person obtaining\
920a copy of this software and associated documentation files (the\
921\"Software\"), to deal in the Software without restriction, including\
922without limitation the rights to use, copy, modify, merge, publish,\
923distribute, sublicense, and/or sell copies of the Software, and to\
924permit persons to whom the Software is furnished to do so, subject to\
925the following conditions:\
926\
927The above copyright notice and this permission notice shall be\
928included in all copies or substantial portions of the Software.\
929\
930THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\
931EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\
932MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\
933NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS\
934BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN\
935ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\
936CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\
937SOFTWARE.\
938\
939<!-- This documentation can be parsed using Markdown to generate HTML.\
940 The source code is enclosed in a HTML comment so it won't be displayed\
941 by browsers, but it should be removed from the final HTML file as\
942 it isn't a valid HTML comment (and wastes space).\
943 -->\
944\
945 <!--]==]\
946\
947-- global dependencies:\
948local pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset =\
949 pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset\
950local error, require, pcall, select = error, require, pcall, select\
951local floor, huge = math.floor, math.huge\
952local strrep, gsub, strsub, strbyte, strchar, strfind, strlen, strformat =\
953 string.rep, string.gsub, string.sub, string.byte, string.char,\
954 string.find, string.len, string.format\
955local concat = table.concat\
956\
957local _ENV = nil -- blocking globals in Lua 5.2\
958\
959local json = { version = \"dkjson 2.2\" }\
960\
961pcall (function()\
962 -- Enable access to blocked metatables.\
963 -- Don't worry, this module doesn't change anything in them.\
964 local debmeta = require \"debug\".getmetatable\
965 if debmeta then getmetatable = debmeta end\
966end)\
967\
968json.null = setmetatable ({}, {\
969 __tojson = function () return \"null\" end\
970})\
971\
972local function isarray (tbl)\
973 local max, n, arraylen = 0, 0, 0\
974 for k,v in pairs (tbl) do\
975 if k == 'n' and type(v) == 'number' then\
976 arraylen = v\
977 if v > max then\
978 max = v\
979 end\
980 else\
981 if type(k) ~= 'number' or k < 1 or floor(k) ~= k then\
982 return false\
983 end\
984 if k > max then\
985 max = k\
986 end\
987 n = n + 1\
988 end\
989 end\
990 if max > 10 and max > arraylen and max > n * 2 then\
991 return false -- don't create an array with too many holes\
992 end\
993 return true, max\
994end\
995\
996local escapecodes = {\
997 [\"\\\"\"] = \"\\\\\\\"\", [\"\\\\\"] = \"\\\\\\\\\", [\"\\b\"] = \"\\\\b\", [\"\\f\"] = \"\\\\f\",\
998 [\"\\n\"] = \"\\\\n\", [\"\\r\"] = \"\\\\r\", [\"\\t\"] = \"\\\\t\"\
999}\
1000\
1001local function escapeutf8 (uchar)\
1002 local value = escapecodes[uchar]\
1003 if value then\
1004 return value\
1005 end\
1006 local a, b, c, d = strbyte (uchar, 1, 4)\
1007 a, b, c, d = a or 0, b or 0, c or 0, d or 0\
1008 if a <= 0x7f then\
1009 value = a\
1010 elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then\
1011 value = (a - 0xc0) * 0x40 + b - 0x80\
1012 elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then\
1013 value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80\
1014 elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then\
1015 value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80\
1016 else\
1017 return \"\"\
1018 end\
1019 if value <= 0xffff then\
1020 return strformat (\"\\\\u%.4x\", value)\
1021 elseif value <= 0x10ffff then\
1022 -- encode as UTF-16 surrogate pair\
1023 value = value - 0x10000\
1024 local highsur, lowsur = 0xD800 + floor (value/0x400), 0xDC00 + (value % 0x400)\
1025 return strformat (\"\\\\u%.4x\\\\u%.4x\", highsur, lowsur)\
1026 else\
1027 return \"\"\
1028 end\
1029end\
1030\
1031local function fsub (str, pattern, repl)\
1032 -- gsub always builds a new string in a buffer, even when no match\
1033 -- exists. First using find should be more efficient when most strings\
1034 -- don't contain the pattern.\
1035 if strfind (str, pattern) then\
1036 return gsub (str, pattern, repl)\
1037 else\
1038 return str\
1039 end\
1040end\
1041\
1042local function quotestring (value)\
1043 -- based on the regexp \"escapable\" in https://github.com/douglascrockford/JSON-js\
1044 value = fsub (value, \"[%z\\1-\\31\\\"\\\\\\127]\", escapeutf8)\
1045 if strfind (value, \"[\\194\\216\\220\\225\\226\\239]\") then\
1046 value = fsub (value, \"\\194[\\128-\\159\\173]\", escapeutf8)\
1047 value = fsub (value, \"\\216[\\128-\\132]\", escapeutf8)\
1048 value = fsub (value, \"\\220\\143\", escapeutf8)\
1049 value = fsub (value, \"\\225\\158[\\180\\181]\", escapeutf8)\
1050 value = fsub (value, \"\\226\\128[\\140-\\143\\168\\175]\", escapeutf8)\
1051 value = fsub (value, \"\\226\\129[\\160-\\175]\", escapeutf8)\
1052 value = fsub (value, \"\\239\\187\\191\", escapeutf8)\
1053 value = fsub (value, \"\\239\\191[\\176\\191]\", escapeutf8)\
1054 end\
1055 return \"\\\"\" .. value .. \"\\\"\"\
1056end\
1057json.quotestring = quotestring\
1058\
1059local function addnewline2 (level, buffer, buflen)\
1060 buffer[buflen+1] = \"\\n\"\
1061 buffer[buflen+2] = strrep (\" \", level)\
1062 buflen = buflen + 2\
1063 return buflen\
1064end\
1065\
1066function json.addnewline (state)\
1067 if state.indent then\
1068 state.bufferlen = addnewline2 (state.level or 0,\
1069 state.buffer, state.bufferlen or #(state.buffer))\
1070 end\
1071end\
1072\
1073local encode2 -- forward declaration\
1074\
1075local function addpair (key, value, prev, indent, level, buffer, buflen, tables, globalorder)\
1076 local kt = type (key)\
1077 if kt ~= 'string' and kt ~= 'number' then\
1078 return nil, \"type '\" .. kt .. \"' is not supported as a key by JSON.\"\
1079 end\
1080 if prev then\
1081 buflen = buflen + 1\
1082 buffer[buflen] = \",\"\
1083 end\
1084 if indent then\
1085 buflen = addnewline2 (level, buffer, buflen)\
1086 end\
1087 buffer[buflen+1] = quotestring (key)\
1088 buffer[buflen+2] = \":\"\
1089 return encode2 (value, indent, level, buffer, buflen + 2, tables, globalorder)\
1090end\
1091\
1092encode2 = function (value, indent, level, buffer, buflen, tables, globalorder)\
1093 local valtype = type (value)\
1094 local valmeta = getmetatable (value)\
1095 valmeta = type (valmeta) == 'table' and valmeta -- only tables\
1096 local valtojson = valmeta and valmeta.__tojson\
1097 if valtojson then\
1098 if tables[value] then\
1099 return nil, \"reference cycle\"\
1100 end\
1101 tables[value] = true\
1102 local state = {\
1103 indent = indent, level = level, buffer = buffer,\
1104 bufferlen = buflen, tables = tables, keyorder = globalorder\
1105 }\
1106 local ret, msg = valtojson (value, state)\
1107 if not ret then return nil, msg end\
1108 tables[value] = nil\
1109 buflen = state.bufferlen\
1110 if type (ret) == 'string' then\
1111 buflen = buflen + 1\
1112 buffer[buflen] = ret\
1113 end\
1114 elseif value == nil then\
1115 buflen = buflen + 1\
1116 buffer[buflen] = \"null\"\
1117 elseif valtype == 'number' then\
1118 local s\
1119 if value ~= value or value >= huge or -value >= huge then\
1120 -- This is the behaviour of the original JSON implementation.\
1121 s = \"null\"\
1122 else\
1123 s = tostring (value)\
1124 end\
1125 buflen = buflen + 1\
1126 buffer[buflen] = s\
1127 elseif valtype == 'boolean' then\
1128 buflen = buflen + 1\
1129 buffer[buflen] = value and \"true\" or \"false\"\
1130 elseif valtype == 'string' then\
1131 buflen = buflen + 1\
1132 buffer[buflen] = quotestring (value)\
1133 elseif valtype == 'table' then\
1134 if tables[value] then\
1135 return nil, \"reference cycle\"\
1136 end\
1137 tables[value] = true\
1138 level = level + 1\
1139 local isa, n = isarray (value)\
1140 if n == 0 and valmeta and valmeta.__jsontype == 'object' then\
1141 isa = false\
1142 end\
1143 local msg\
1144 if isa then -- JSON array\
1145 buflen = buflen + 1\
1146 buffer[buflen] = \"[\"\
1147 for i = 1, n do\
1148 buflen, msg = encode2 (value[i], indent, level, buffer, buflen, tables, globalorder)\
1149 if not buflen then return nil, msg end\
1150 if i < n then\
1151 buflen = buflen + 1\
1152 buffer[buflen] = \",\"\
1153 end\
1154 end\
1155 buflen = buflen + 1\
1156 buffer[buflen] = \"]\"\
1157 else -- JSON object\
1158 local prev = false\
1159 buflen = buflen + 1\
1160 buffer[buflen] = \"{\"\
1161 local order = valmeta and valmeta.__jsonorder or globalorder\
1162 if order then\
1163 local used = {}\
1164 n = #order\
1165 for i = 1, n do\
1166 local k = order[i]\
1167 local v = value[k]\
1168 if v then\
1169 used[k] = true\
1170 buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder)\
1171 prev = true -- add a seperator before the next element\
1172 end\
1173 end\
1174 for k,v in pairs (value) do\
1175 if not used[k] then\
1176 buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder)\
1177 if not buflen then return nil, msg end\
1178 prev = true -- add a seperator before the next element\
1179 end\
1180 end\
1181 else -- unordered\
1182 for k,v in pairs (value) do\
1183 buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder)\
1184 if not buflen then return nil, msg end\
1185 prev = true -- add a seperator before the next element\
1186 end\
1187 end\
1188 if indent then\
1189 buflen = addnewline2 (level - 1, buffer, buflen)\
1190 end\
1191 buflen = buflen + 1\
1192 buffer[buflen] = \"}\"\
1193 end\
1194 tables[value] = nil\
1195 else\
1196 return nil, \"type '\" .. valtype .. \"' is not supported by JSON.\"\
1197 end\
1198 return buflen\
1199end\
1200\
1201function json.encode (value, state)\
1202 state = state or {}\
1203 local oldbuffer = state.buffer\
1204 local buffer = oldbuffer or {}\
1205 local ret, msg = encode2 (value, state.indent, state.level or 0,\
1206 buffer, state.bufferlen or 0, state.tables or {}, state.keyorder)\
1207 if not ret then\
1208 error (msg, 2)\
1209 elseif oldbuffer then\
1210 state.bufferlen = ret\
1211 return true\
1212 else\
1213 return concat (buffer)\
1214 end\
1215end\
1216\
1217local function loc (str, where)\
1218 local line, pos, linepos = 1, 1, 0\
1219 while true do\
1220 pos = strfind (str, \"\\n\", pos, true)\
1221 if pos and pos < where then\
1222 line = line + 1\
1223 linepos = pos\
1224 pos = pos + 1\
1225 else\
1226 break\
1227 end\
1228 end\
1229 return \"line \" .. line .. \", column \" .. (where - linepos)\
1230end\
1231\
1232local function unterminated (str, what, where)\
1233 return nil, strlen (str) + 1, \"unterminated \" .. what .. \" at \" .. loc (str, where)\
1234end\
1235\
1236local function scanwhite (str, pos)\
1237 while true do\
1238 pos = strfind (str, \"%S\", pos)\
1239 if not pos then return nil end\
1240 if strsub (str, pos, pos + 2) == \"\\239\\187\\191\" then\
1241 -- UTF-8 Byte Order Mark\
1242 pos = pos + 3\
1243 else\
1244 return pos\
1245 end\
1246 end\
1247end\
1248\
1249local escapechars = {\
1250 [\"\\\"\"] = \"\\\"\", [\"\\\\\"] = \"\\\\\", [\"/\"] = \"/\", [\"b\"] = \"\\b\", [\"f\"] = \"\\f\",\
1251 [\"n\"] = \"\\n\", [\"r\"] = \"\\r\", [\"t\"] = \"\\t\"\
1252}\
1253\
1254local function unichar (value)\
1255 if value < 0 then\
1256 return nil\
1257 elseif value <= 0x007f then\
1258 return strchar (value)\
1259 elseif value <= 0x07ff then\
1260 return strchar (0xc0 + floor(value/0x40),\
1261 0x80 + (floor(value) % 0x40))\
1262 elseif value <= 0xffff then\
1263 return strchar (0xe0 + floor(value/0x1000),\
1264 0x80 + (floor(value/0x40) % 0x40),\
1265 0x80 + (floor(value) % 0x40))\
1266 elseif value <= 0x10ffff then\
1267 return strchar (0xf0 + floor(value/0x40000),\
1268 0x80 + (floor(value/0x1000) % 0x40),\
1269 0x80 + (floor(value/0x40) % 0x40),\
1270 0x80 + (floor(value) % 0x40))\
1271 else\
1272 return nil\
1273 end\
1274end\
1275\
1276local function scanstring (str, pos)\
1277 local lastpos = pos + 1\
1278 local buffer, n = {}, 0\
1279 while true do\
1280 local nextpos = strfind (str, \"[\\\"\\\\]\", lastpos)\
1281 if not nextpos then\
1282 return unterminated (str, \"string\", pos)\
1283 end\
1284 if nextpos > lastpos then\
1285 n = n + 1\
1286 buffer[n] = strsub (str, lastpos, nextpos - 1)\
1287 end\
1288 if strsub (str, nextpos, nextpos) == \"\\\"\" then\
1289 lastpos = nextpos + 1\
1290 break\
1291 else\
1292 local escchar = strsub (str, nextpos + 1, nextpos + 1)\
1293 local value\
1294 if escchar == \"u\" then\
1295 value = tonumber (strsub (str, nextpos + 2, nextpos + 5), 16)\
1296 if value then\
1297 local value2\
1298 if 0xD800 <= value and value <= 0xDBff then\
1299 -- we have the high surrogate of UTF-16. Check if there is a\
1300 -- low surrogate escaped nearby to combine them.\
1301 if strsub (str, nextpos + 6, nextpos + 7) == \"\\\\u\" then\
1302 value2 = tonumber (strsub (str, nextpos + 8, nextpos + 11), 16)\
1303 if value2 and 0xDC00 <= value2 and value2 <= 0xDFFF then\
1304 value = (value - 0xD800) * 0x400 + (value2 - 0xDC00) + 0x10000\
1305 else\
1306 value2 = nil -- in case it was out of range for a low surrogate\
1307 end\
1308 end\
1309 end\
1310 value = value and unichar (value)\
1311 if value then\
1312 if value2 then\
1313 lastpos = nextpos + 12\
1314 else\
1315 lastpos = nextpos + 6\
1316 end\
1317 end\
1318 end\
1319 end\
1320 if not value then\
1321 value = escapechars[escchar] or escchar\
1322 lastpos = nextpos + 2\
1323 end\
1324 n = n + 1\
1325 buffer[n] = value\
1326 end\
1327 end\
1328 if n == 1 then\
1329 return buffer[1], lastpos\
1330 elseif n > 1 then\
1331 return concat (buffer), lastpos\
1332 else\
1333 return \"\", lastpos\
1334 end\
1335end\
1336\
1337local scanvalue -- forward declaration\
1338\
1339local function scantable (what, closechar, str, startpos, nullval, objectmeta, arraymeta)\
1340 local len = strlen (str)\
1341 local tbl, n = {}, 0\
1342 local pos = startpos + 1\
1343 if what == 'object' then\
1344 setmetatable (tbl, objectmeta)\
1345 else\
1346 setmetatable (tbl, arraymeta)\
1347 end\
1348 while true do\
1349 pos = scanwhite (str, pos)\
1350 if not pos then return unterminated (str, what, startpos) end\
1351 local char = strsub (str, pos, pos)\
1352 if char == closechar then\
1353 return tbl, pos + 1\
1354 end\
1355 local val1, err\
1356 val1, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta)\
1357 if err then return nil, pos, err end\
1358 pos = scanwhite (str, pos)\
1359 if not pos then return unterminated (str, what, startpos) end\
1360 char = strsub (str, pos, pos)\
1361 if char == \":\" then\
1362 if val1 == nil then\
1363 return nil, pos, \"cannot use nil as table index (at \" .. loc (str, pos) .. \")\"\
1364 end\
1365 pos = scanwhite (str, pos + 1)\
1366 if not pos then return unterminated (str, what, startpos) end\
1367 local val2\
1368 val2, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta)\
1369 if err then return nil, pos, err end\
1370 tbl[val1] = val2\
1371 pos = scanwhite (str, pos)\
1372 if not pos then return unterminated (str, what, startpos) end\
1373 char = strsub (str, pos, pos)\
1374 else\
1375 n = n + 1\
1376 tbl[n] = val1\
1377 end\
1378 if char == \",\" then\
1379 pos = pos + 1\
1380 end\
1381 end\
1382end\
1383\
1384scanvalue = function (str, pos, nullval, objectmeta, arraymeta)\
1385 pos = pos or 1\
1386 pos = scanwhite (str, pos)\
1387 if not pos then\
1388 return nil, strlen (str) + 1, \"no valid JSON value (reached the end)\"\
1389 end\
1390 local char = strsub (str, pos, pos)\
1391 if char == \"{\" then\
1392 return scantable ('object', \"}\", str, pos, nullval, objectmeta, arraymeta)\
1393 elseif char == \"[\" then\
1394 return scantable ('array', \"]\", str, pos, nullval, objectmeta, arraymeta)\
1395 elseif char == \"\\\"\" then\
1396 return scanstring (str, pos)\
1397 else\
1398 local pstart, pend = strfind (str, \"^%-?[%d%.]+[eE]?[%+%-]?%d*\", pos)\
1399 if pstart then\
1400 local number = tonumber (strsub (str, pstart, pend))\
1401 if number then\
1402 return number, pend + 1\
1403 end\
1404 end\
1405 pstart, pend = strfind (str, \"^%a%w*\", pos)\
1406 if pstart then\
1407 local name = strsub (str, pstart, pend)\
1408 if name == \"true\" then\
1409 return true, pend + 1\
1410 elseif name == \"false\" then\
1411 return false, pend + 1\
1412 elseif name == \"null\" then\
1413 return nullval, pend + 1\
1414 end\
1415 end\
1416 return nil, pos, \"no valid JSON value at \" .. loc (str, pos)\
1417 end\
1418end\
1419\
1420local function optionalmetatables(...)\
1421 if select(\"#\", ...) > 0 then\
1422 return ...\
1423 else\
1424 return {__jsontype = 'object'}, {__jsontype = 'array'}\
1425 end\
1426end\
1427\
1428function json.decode (str, pos, nullval, ...)\
1429 local objectmeta, arraymeta = optionalmetatables(...)\
1430 return scanvalue (str, pos, nullval, objectmeta, arraymeta)\
1431end\
1432\
1433function json.use_lpeg ()\
1434 local g = require (\"lpeg\")\
1435 local pegmatch = g.match\
1436 local P, S, R, V = g.P, g.S, g.R, g.V\
1437\
1438 local function ErrorCall (str, pos, msg, state)\
1439 if not state.msg then\
1440 state.msg = msg .. \" at \" .. loc (str, pos)\
1441 state.pos = pos\
1442 end\
1443 return false\
1444 end\
1445\
1446 local function Err (msg)\
1447 return g.Cmt (g.Cc (msg) * g.Carg (2), ErrorCall)\
1448 end\
1449\
1450 local Space = (S\" \\n\\r\\t\" + P\"\\239\\187\\191\")^0\
1451\
1452 local PlainChar = 1 - S\"\\\"\\\\\\n\\r\"\
1453 local EscapeSequence = (P\"\\\\\" * g.C (S\"\\\"\\\\/bfnrt\" + Err \"unsupported escape sequence\")) / escapechars\
1454 local HexDigit = R(\"09\", \"af\", \"AF\")\
1455 local function UTF16Surrogate (match, pos, high, low)\
1456 high, low = tonumber (high, 16), tonumber (low, 16)\
1457 if 0xD800 <= high and high <= 0xDBff and 0xDC00 <= low and low <= 0xDFFF then\
1458 return true, unichar ((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000)\
1459 else\
1460 return false\
1461 end\
1462 end\
1463 local function UTF16BMP (hex)\
1464 return unichar (tonumber (hex, 16))\
1465 end\
1466 local U16Sequence = (P\"\\\\u\" * g.C (HexDigit * HexDigit * HexDigit * HexDigit))\
1467 local UnicodeEscape = g.Cmt (U16Sequence * U16Sequence, UTF16Surrogate) + U16Sequence/UTF16BMP\
1468 local Char = UnicodeEscape + EscapeSequence + PlainChar\
1469 local String = P\"\\\"\" * g.Cs (Char ^ 0) * (P\"\\\"\" + Err \"unterminated string\")\
1470 local Integer = P\"-\"^(-1) * (P\"0\" + (R\"19\" * R\"09\"^0))\
1471 local Fractal = P\".\" * R\"09\"^0\
1472 local Exponent = (S\"eE\") * (S\"+-\")^(-1) * R\"09\"^1\
1473 local Number = (Integer * Fractal^(-1) * Exponent^(-1))/tonumber\
1474 local Constant = P\"true\" * g.Cc (true) + P\"false\" * g.Cc (false) + P\"null\" * g.Carg (1)\
1475 local SimpleValue = Number + String + Constant\
1476 local ArrayContent, ObjectContent\
1477\
1478 -- The functions parsearray and parseobject parse only a single value/pair\
1479 -- at a time and store them directly to avoid hitting the LPeg limits.\
1480 local function parsearray (str, pos, nullval, state)\
1481 local obj, cont\
1482 local npos\
1483 local t, nt = {}, 0\
1484 repeat\
1485 obj, cont, npos = pegmatch (ArrayContent, str, pos, nullval, state)\
1486 if not npos then break end\
1487 pos = npos\
1488 nt = nt + 1\
1489 t[nt] = obj\
1490 until cont == 'last'\
1491 return pos, setmetatable (t, state.arraymeta)\
1492 end\
1493\
1494 local function parseobject (str, pos, nullval, state)\
1495 local obj, key, cont\
1496 local npos\
1497 local t = {}\
1498 repeat\
1499 key, obj, cont, npos = pegmatch (ObjectContent, str, pos, nullval, state)\
1500 if not npos then break end\
1501 pos = npos\
1502 t[key] = obj\
1503 until cont == 'last'\
1504 return pos, setmetatable (t, state.objectmeta)\
1505 end\
1506\
1507 local Array = P\"[\" * g.Cmt (g.Carg(1) * g.Carg(2), parsearray) * Space * (P\"]\" + Err \"']' expected\")\
1508 local Object = P\"{\" * g.Cmt (g.Carg(1) * g.Carg(2), parseobject) * Space * (P\"}\" + Err \"'}' expected\")\
1509 local Value = Space * (Array + Object + SimpleValue)\
1510 local ExpectedValue = Value + Space * Err \"value expected\"\
1511 ArrayContent = Value * Space * (P\",\" * g.Cc'cont' + g.Cc'last') * g.Cp()\
1512 local Pair = g.Cg (Space * String * Space * (P\":\" + Err \"colon expected\") * ExpectedValue)\
1513 ObjectContent = Pair * Space * (P\",\" * g.Cc'cont' + g.Cc'last') * g.Cp()\
1514 local DecodeValue = ExpectedValue * g.Cp ()\
1515\
1516 function json.decode (str, pos, nullval, ...)\
1517 local state = {}\
1518 state.objectmeta, state.arraymeta = optionalmetatables(...)\
1519 local obj, retpos = pegmatch (DecodeValue, str, pos, nullval, state)\
1520 if state.msg then\
1521 return nil, state.pos, state.msg\
1522 else\
1523 return obj, retpos\
1524 end\
1525 end\
1526\
1527 -- use this function only once:\
1528 json.use_lpeg = function () return json end\
1529\
1530 json.using_lpeg = true\
1531\
1532 return json -- so you can get the module using json = require \"dkjson\".use_lpeg()\
1533end\
1534\
1535if always_try_using_lpeg then\
1536 pcall (json.use_lpeg)\
1537end\
1538\
1539return json\
1540\
1541-->",
1542 [ "apis/util" ] = "-- The problem is that we call os.loadAPI multiple times, and CC loads and overwrites the API every time.\
1543-- We can either stash API variables in a global table like this or patch CC to load each API once.\
1544function initializeGlobalTable(name)\
1545 return getTable(_G, name)\
1546end\
1547\
1548function getCoroutineTable(name)\
1549 return getTable(getTable(initializeGlobalTable(\"coroutineStorage\"), coroutine.running()), name)\
1550end\
1551\
1552function getTable(obj, key)\
1553 local value = obj[key]\
1554 if value == nil then\
1555 value = {}\
1556 obj[key] = value\
1557 end\
1558 return value\
1559end\
1560\
1561function getNextUnusedIndex(obj)\
1562 local id = 1\
1563 while obj[id] ~= nil do\
1564 id = id + 1\
1565 end\
1566 return id\
1567end\
1568\
1569function insertRange(obj, items)\
1570 for _, item in pairs(items) do\
1571 table.insert(obj, item)\
1572 end\
1573end\
1574\
1575function removeWhere(obj, test)\
1576 local position = 1\
1577 while position <= #obj do\
1578 if test(obj[position]) then\
1579 table.remove(obj, position)\
1580 else\
1581 position = position + 1\
1582 end\
1583 end\
1584end\
1585\
1586function deepClone(obj)\
1587 if type(obj) ~= \"table\" then\
1588 return obj\
1589 end\
1590 local clone = {}\
1591 for k, v in pairs(obj) do\
1592 clone[k] = deepClone(v)\
1593 end\
1594 return clone\
1595end",
1596 [ "apis/amber-repositories/AmberServer" ] = "os.loadAPI(\"apis/net\")\
1597\
1598local AmberServer = {}\
1599AmberServer.__index = AmberServer\
1600AmberServer.new = function(data)\
1601 return setmetatable({server = data.server or \"amber://\"}, AmberServer)\
1602end\
1603AmberServer.bindPackage = function(self, packageId)\
1604 net.sendMessage(self.server, \"downloadPackage\", packageId)\
1605 -- This assumes we're running a parallel message loop (or that the caller doesn't mind losing other events).\
1606 local package = net.pullMessage(\"packageDownload\")\
1607 if package.errorMessage then\
1608 return nil, package.errorMessage\
1609 end\
1610 return package\
1611end\
1612AmberServer.getAvailablePackages = function(self)\
1613 net.sendMessage(self.server, \"sendAvailablePackages\", {})\
1614 local package = net.pullMessage(\"availablePackages\")\
1615 if package.errorMessage then\
1616 return nil, package.errorMessage\
1617 end\
1618 return package\
1619end\
1620\
1621return AmberServer",
1622 [ "apis/amber-repositories/LocalDirectory" ] = "os.loadAPI(\"apis/version\")\
1623\
1624local LocalDirectory = {}\
1625LocalDirectory.__index = LocalDirectory\
1626LocalDirectory.new = function(data)\
1627 return setmetatable({path = data.path or \"/packages\"}, LocalDirectory)\
1628end\
1629LocalDirectory.getPackageRootPath = function(self, packageId)\
1630 return fs.combine(self.path, packageId.name)\
1631end\
1632LocalDirectory.getPackagePath = function(self, packageId)\
1633 return fs.combine(self:getPackageRootPath(packageId), packageId.version)\
1634end\
1635LocalDirectory.getAllVersions = function(self, packageId)\
1636 local path = self:getPackageRootPath(packageId)\
1637 if fs.isDir(path) then\
1638 return fs.list(path)\
1639 end\
1640end\
1641LocalDirectory.getLatestVersion = function(self, packageId)\
1642 local versions = self:getAllVersions(packageId)\
1643 if versions == nil then\
1644 return\
1645 end\
1646 local latest\
1647 for _, v in pairs(versions) do\
1648 if latest == nil or version.new(latest):compareTo(version.new(v)) < 0 then\
1649 latest = v\
1650 end\
1651 end\
1652 return latest\
1653end\
1654local function loadFilesIn(path)\
1655 local files = fs.list(path)\
1656 local fileData = {}\
1657 for _, v in pairs(files) do\
1658 local filePath = fs.combine(path, v)\
1659 if fs.isDir(filePath) then\
1660 fileData[v] = loadFilesIn(filePath)\
1661 else\
1662 fileData[v] = serializer.readFile(filePath)\
1663 end\
1664 end\
1665 return fileData\
1666end\
1667LocalDirectory.readPackage = function(self, packageId, metadataOnly)\
1668 local path = self:getPackagePath(packageId)\
1669 if fs.isDir(path) then\
1670 local package = {id = packageId}\
1671 if not metadataOnly then\
1672 package.contents = loadFilesIn(path)\
1673 end\
1674 return package\
1675 else\
1676 return nil, \"Unable to locate package path\"\
1677 end\
1678end\
1679LocalDirectory.bindPackageId = function(self, packageId)\
1680 local version = packageId.version\
1681 if version == nil then\
1682 version = self:getLatestVersion(packageId)\
1683 end\
1684 if version == nil then\
1685 return nil, \"Unable to find latest version of package\"\
1686 end\
1687 return {name = packageId.name, version = version}\
1688end\
1689LocalDirectory.bindPackage = function(self, packageId, metadataOnly)\
1690 local package, errorMessage = self:bindPackageId(packageId)\
1691 if package == nil then\
1692 return nil, errorMessage\
1693 end\
1694 return self:readPackage(package, metadataOnly)\
1695end\
1696LocalDirectory.getAvailablePackages = function(self)\
1697 local packages = {}\
1698 for _, file in ipairs(fs.list(self.path)) do\
1699 packages[file] = self:bindPackage({name = file}, nil, true)\
1700 end\
1701 return packages\
1702end\
1703\
1704return LocalDirectory",
1705 [ "apis/amber" ] = "os.loadAPI(\"apis/serializer\")\
1706os.loadAPI(\"apis/net\")\
1707os.loadAPI(\"apis/version\")\
1708\
1709-- TODO: Extract package ID, package, and package bundle concepts.\
1710function formatPackageId(packageId)\
1711 local result = packageId.name\
1712 if packageId.version ~= nil then\
1713 -- TODO: Revisit this - use a different character to separate name & version.\
1714 result = result .. \"-\" .. packageId.version\
1715 end\
1716 return result\
1717end\
1718\
1719-- Package binding\
1720PackageClient = {}\
1721PackageClient.__index = PackageClient\
1722PackageClient.new = function(repoConfigs, repoTypesPath)\
1723 local repositories = {}\
1724 local repoTypes = {}\
1725 for _, repoConfig in pairs(repoConfigs) do\
1726 local repoType = repoTypes[repoConfig.type]\
1727 if repoType == nil then\
1728 -- TODO: Better error handling.\
1729 repoType = dofile(fs.combine(repoTypesPath, repoConfig.type))\
1730 repoTypes[repoConfig.type] = repoType\
1731 end\
1732 table.insert(repositories, repoType.new(repoConfig))\
1733 end\
1734\
1735 return setmetatable({repositories = repositories}, PackageClient)\
1736end\
1737PackageClient.fromConfigurationPath = function(path, defaultConfig)\
1738 path = path or \"\"\
1739 local repoConfigs = serializer.readFromFile(fs.combine(path, \".repositories\"))\
1740 if next(repoConfigs) == nil and defaultConfig ~= nil then\
1741 repoConfigs = defaultConfig\
1742 end\
1743 return PackageClient.new(repoConfigs, fs.combine(path, \"apis/amber-repositories\"))\
1744end\
1745local function createPackage(package)\
1746 if not package.manifest then\
1747 -- TODO: The latter is a legacy path for backwards compatibility.\
1748 -- Remove it after all packages have been migrated to the new format.\
1749 local paths = {\".manifest\", \"manifest\"}\
1750 for _, path in ipairs(paths) do\
1751 local contents = package.contents[path]\
1752 if contents then\
1753 package.manifest = textutils.unserialize(contents)\
1754 package.contents[path] = nil\
1755 break\
1756 end\
1757 end\
1758 if not package.manifest then\
1759 package.manifest = {}\
1760 end\
1761 end\
1762 return package\
1763end\
1764PackageClient.bindPackage = function(self, packageId)\
1765 for _, repo in pairs(self.repositories) do\
1766 local result = repo:bindPackage(packageId)\
1767 if result ~= nil then\
1768 return createPackage(result)\
1769 end\
1770 end\
1771end\
1772local function isSatisfiedBy(specId, packageId)\
1773 if specId.name ~= packageId.name then\
1774 return false\
1775 end\
1776 if specId.version == nil then\
1777 return true\
1778 end\
1779 return version.new(specId.version):compareTo(version.new(packageId.version)) == 0\
1780end\
1781local function bundleContains(msg, packageId)\
1782 for _, package in pairs(msg) do\
1783 if isSatisfiedBy(packageId, package.id) then\
1784 return true\
1785 end\
1786 end\
1787 return false\
1788end\
1789PackageClient.bundlePackage = function(self, msg, packageId, userInstalled)\
1790 if bundleContains(msg, packageId) then\
1791 return\
1792 end\
1793 local package = self:bindPackage(packageId)\
1794 if package == nil then\
1795 return {errorMessage = string.format(\"Unable to locate package %s\", formatPackageId(packageId))}\
1796 end\
1797 package.userInstalled = userInstalled\
1798 table.insert(msg, package)\
1799 self:bundlePackages(msg, package.manifest.dependencies)\
1800end\
1801PackageClient.bundlePackages = function(self, msg, toPackage, userInstalled)\
1802 if toPackage == nil then\
1803 return\
1804 end\
1805 for _, packageId in pairs(toPackage) do\
1806 self:bundlePackage(msg, packageId, userInstalled)\
1807 end\
1808end\
1809PackageClient.getBundle = function(self, toPackage, userInstalled)\
1810 local bundle = {}\
1811 self:bundlePackages(bundle, toPackage, userInstalled)\
1812 return bundle\
1813end\
1814PackageClient.getAvailablePackages = function(self)\
1815 local packages = {}\
1816 for _, repo in pairs(self.repositories) do\
1817 for name, packageInfo in pairs(repo:getAvailablePackages()) do\
1818 if packages[name] == nil then\
1819 packages[name] = packageInfo\
1820 end\
1821 end\
1822 end\
1823 return packages\
1824end\
1825local function writeToFilesystem(filesystem, path, contents)\
1826 for name, data in pairs(contents) do\
1827 local fullPath = fs.combine(path, name)\
1828 if type(data) == \"table\" then\
1829 writeToFilesystem(filesystem, fullPath, data)\
1830 else\
1831 filesystem[fullPath] = data\
1832 end\
1833 end\
1834end\
1835local function buildFilesystem(bundle)\
1836 local filesystem = {}\
1837 for _, package in pairs(bundle) do\
1838 writeToFilesystem(filesystem, \"\", package.contents)\
1839 end\
1840 return filesystem\
1841end\
1842local function loadIntoFilesystem(filesystem, files)\
1843 for _, file in pairs(files) do\
1844 if fs.exists(file) then\
1845 if fs.isDir(file) then\
1846 return false, string.format(\"File %s is a directory\", file)\
1847 else\
1848 filesystem[file] = serializer.readFile(file)\
1849 end\
1850 else\
1851 return false, string.format(\"File %s does not exist\", file)\
1852 end\
1853 end\
1854 return true\
1855end\
1856PackageClient.buildNuggetFor = function(self, packageId, additionalPackages, additionalFiles)\
1857 local package = self:bindPackage(packageId)\
1858 if package == nil then\
1859 return nil, \"Unable to locate package\"\
1860 end\
1861 \
1862 local entryPoint = package.manifest.entryPoint\
1863 if entryPoint == nil then\
1864 return nil, \"Package does not specify entry point\"\
1865 end\
1866 \
1867 local program = package.contents[entryPoint]\
1868 if program == nil then\
1869 return nil, \"Unable to locate entry point\"\
1870 end\
1871 \
1872 local bundle = {package}\
1873 self:bundlePackages(bundle, package.manifest.dependencies)\
1874 self:bundlePackages(bundle, additionalPackages)\
1875 local filesystem = buildFilesystem(bundle)\
1876 local ok, err = loadIntoFilesystem(filesystem, additionalFiles)\
1877 if not ok then\
1878 return nil, err\
1879 end\
1880\
1881 -- TODO: Implement a full string reader handle.\
1882 return Nugget.new(entryPoint, \"-- \" .. formatPackageId(package.id) .. [[\
1883\
1884local _nuggetFiles = ]] .. textutils.serialize(filesystem) .. [[\
1885\
1886local _baseFolder = fs.getDir(shell.getRunningProgram())\
1887local originalOpen = fs.open\
1888fs.open = function(file, mode)\
1889 local localFile\
1890 if _baseFolder == \"\" then\
1891 localFile = file\
1892 elseif file:sub(1, _baseFolder:len() + 1) == _baseFolder .. \"/\" then\
1893 localFile = file:sub(_baseFolder:len() + 2)\
1894 end\
1895 local ours = _nuggetFiles[localFile]\
1896 if not ours then\
1897 return originalOpen(file, mode)\
1898 end\
1899 return {\
1900 readAll = function() return ours end,\
1901 close = function() end\
1902 }\
1903end\
1904\
1905local args = {...}\
1906local ok, err = pcall(function()\
1907 local program = loadfile(fs.combine(_baseFolder, ]] .. textutils.serialize(entryPoint) .. [[), _ENV)\
1908 program(table.unpack(args))\
1909end)\
1910if not ok then\
1911 printError(err)\
1912end\
1913\
1914fs.open = originalOpen]])\
1915end\
1916\
1917-- Nuggets\
1918Nugget = {}\
1919Nugget.__index = Nugget\
1920Nugget.new = function(entryPoint, contents)\
1921 return setmetatable({\
1922 entryPoint = entryPoint,\
1923 contents = contents\
1924 }, Nugget)\
1925end\
1926Nugget.writeTo = function(self, path)\
1927 local file = fs.open(fs.combine(path, self.entryPoint), \"w\")\
1928 file.write(self.contents)\
1929 file.close()\
1930end\
1931Nugget.run = function(self, ...)\
1932 local f = loadstring(self.contents)\
1933 return f(...)\
1934end\
1935\
1936-- Local package installation\
1937-- TODO: Add other operations like list, info, etc. on the installation/client.\
1938PackageInstallation = {}\
1939PackageInstallation.__index = PackageInstallation\
1940PackageInstallation.new = function(path, onEdit, onProgress)\
1941 return setmetatable({\
1942 path = path or \"\",\
1943 onEdit = onEdit,\
1944 onProgress = onProgress or function() end\
1945 }, PackageInstallation)\
1946end\
1947PackageInstallation.getPackageListPaths = function(self)\
1948 -- TODO: The latter path is a legacy one that exists only for backwards compatibility.\
1949 return {fs.combine(self.path, \".packages\"), fs.combine(self.path, \"packageList\")}\
1950end\
1951PackageInstallation.getInstalledPackages = function(self)\
1952 for _, path in ipairs(self:getPackageListPaths()) do\
1953 local contents = serializer.readFromFile(path)\
1954 if next(contents) ~= nil then\
1955 if contents.version == nil then\
1956 -- Upgrade v0 package lists.\
1957 local newContents = {version = 1, packages = {}}\
1958 for k, v in pairs(contents) do\
1959 newContents.packages[k] = {id = v}\
1960 end\
1961 return newContents\
1962 end\
1963 return contents\
1964 end\
1965 end\
1966 return {version = 1, packages = {}}\
1967end\
1968PackageInstallation.writePackageFiles = function(self, path, data, allFiles)\
1969 local rebootNeeded = false\
1970 if not fs.isDir(path) then\
1971 fs.makeDir(path)\
1972 end\
1973 for file, contents in pairs(data) do\
1974 local destination = fs.combine(path, file)\
1975 if type(contents) == \"table\" then\
1976 rebootNeeded = self:writePackageFiles(destination, contents, allFiles) or rebootNeeded\
1977 else\
1978 if file == \"startup\" then\
1979 rebootNeeded = true\
1980 end\
1981 local writeTo = destination\
1982 allFiles[writeTo] = {}\
1983 local editFile = false\
1984 -- TODO: Write to the template file AND copy it to the destination path if it does not exist.\
1985 -- That way we can detect changes to the file when a new version is installed.\
1986 if destination:sub(-2) == \".t\" then\
1987 local baseName = destination:sub(1, #destination - 2)\
1988 if not fs.exists(baseName) then\
1989 writeTo = baseName\
1990 editFile = true\
1991 self.onProgress(string.format(\" %s => %s\", destination, writeTo))\
1992 else\
1993 self.onProgress(string.format(\" %s\", writeTo))\
1994 end\
1995 else\
1996 self.onProgress(string.format(\" %s\", writeTo))\
1997 end\
1998 local file = fs.open(writeTo, \"w\")\
1999 file.write(contents)\
2000 file.close()\
2001 if editFile and self.onEdit ~= nil then\
2002 self.onProgress(string.format(\" Editing: %s\", writeTo))\
2003 self.onEdit(writeTo)\
2004 end\
2005 end\
2006 end\
2007 return rebootNeeded\
2008end\
2009local function extractPackageMetadata(package, allFiles)\
2010 local metadata = {}\
2011 for k, v in pairs(package) do\
2012 if k == \"contents\" then\
2013 metadata[k] = allFiles\
2014 else\
2015 metadata[k] = v\
2016 end\
2017 end\
2018 return metadata\
2019end\
2020PackageInstallation.writeInstalledPackages = function(self, installed)\
2021 local paths = self:getPackageListPaths()\
2022 serializer.writeToFile(paths[1], installed)\
2023 -- Clean up legacy package lists.\
2024 for i = 2, #paths do\
2025 fs.delete(paths[i])\
2026 end\
2027end\
2028PackageInstallation.installPackage = function(self, package)\
2029 if package.errorMessage ~= nil then\
2030 -- TODO: onError callback.\
2031 self.onProgress(string.format(\"ERROR: %s\", package.errorMessage))\
2032 return\
2033 end\
2034 \
2035 self.onProgress(string.format(\"Installing package %s...\", formatPackageId(package.id)))\
2036 local installed = self:getInstalledPackages()\
2037 local existing = installed.packages[package.id.name] or {}\
2038 \
2039 local filesWritten = {}\
2040 local result = self:writePackageFiles(self.path, package.contents, filesWritten)\
2041 -- TODO: This check is only needed for legacy package lists.\
2042 if existing.contents ~= nil then\
2043 for existingFile in pairs(existing.contents) do\
2044 if filesWritten[existingFile] == nil then\
2045 self.onProgress(string.format(\" Removing obsolete file: %s\", existingFile))\
2046 fs.delete(existingFile)\
2047 end\
2048 end\
2049 end\
2050 \
2051 local newMetadata = extractPackageMetadata(package, filesWritten)\
2052 newMetadata.userInstalled = newMetadata.userInstalled or existing.userInstalled\
2053 installed.packages[package.id.name] = newMetadata\
2054 self:writeInstalledPackages(installed)\
2055 \
2056 return result\
2057end\
2058PackageInstallation.installBundle = function(self, bundle)\
2059 local rebootNeeded = false\
2060 for _, package in pairs(bundle) do\
2061 rebootNeeded = self:installPackage(package) or rebootNeeded\
2062 end\
2063 if rebootNeeded then\
2064 self.onProgress(\"Rebooting...\")\
2065 os.sleep(3)\
2066 os.reboot()\
2067 end\
2068end\
2069PackageInstallation.installPackagesCore = function(self, client, packages, userInstalled)\
2070 local bundle = client:getBundle(packages, userInstalled)\
2071 self:installBundle(bundle)\
2072end\
2073PackageInstallation.installPackages = function(self, client, packages, userInstalled)\
2074 if userInstalled == nil then\
2075 userInstalled = true\
2076 end\
2077 self.onProgress(string.format(\"Installing %i packages...\", #packages))\
2078 self:installPackagesCore(client, packages, userInstalled)\
2079end\
2080local function packageIdListContains(packages, packageId)\
2081 for _, id in pairs(packages) do\
2082 -- We don't check the version since that's not relevant for upgrades.\
2083 if id.name == packageId.name then\
2084 return true\
2085 end\
2086 end\
2087 return false\
2088end\
2089PackageInstallation.getInstalledPackage = function(self, packageId)\
2090 local installed = self:getInstalledPackages()\
2091 for _, pkg in pairs(installed.packages) do\
2092 if isSatisfiedBy(packageId, pkg.id) then\
2093 return pkg\
2094 end\
2095 end\
2096end\
2097PackageInstallation.updatePackages = function(self, client, packages)\
2098 local installed = self:getInstalledPackages()\
2099 local toRequest = {}\
2100 for _, pkg in pairs(installed.packages) do\
2101 if packages == nil or packageIdListContains(packages, pkg.id) then\
2102 table.insert(toRequest, {name = pkg.id.name})\
2103 end\
2104 end\
2105 if next(toRequest) == nil then\
2106 return\
2107 end\
2108 self.onProgress(string.format(\"Updating %i packages...\", #toRequest))\
2109 self:installPackagesCore(client, toRequest, false)\
2110end\
2111PackageInstallation.removePackage = function(self, package)\
2112 self.onProgress(string.format(\"Removing package %s...\", formatPackageId(package)))\
2113 local installed = self:getInstalledPackages()\
2114 local existing = installed.packages[package.name]\
2115 if existing == nil then\
2116 local message = string.format(\"ERROR: Package %s not installed\", formatPackageId(package))\
2117 self.onProgress(message)\
2118 return false, message\
2119 end\
2120\
2121 for existingFile in pairs(existing.contents) do\
2122 -- TODO: What about template files?\
2123 -- TODO: What about empty directories?\
2124 -- TODO: What if this package is needed by others?\
2125 -- TODO: What about purging dependencies which are no longer needed?\
2126 self.onProgress(string.format(\" Removing: %s\", existingFile))\
2127 fs.delete(existingFile)\
2128 end\
2129 \
2130 installed.packages[package.name] = nil\
2131 self:writeInstalledPackages(installed)\
2132\
2133 return true\
2134end\
2135PackageInstallation.removePackages = function(self, packages)\
2136 self.onProgress(string.format(\"Removing %i packages...\", #packages))\
2137 for _, package in ipairs(packages) do\
2138 self:removePackage(package)\
2139 end\
2140end",
2141 [ "apis/dns" ] = "os.loadAPI(\"apis/util\")\
2142\
2143local cache = util.initializeGlobalTable(\"dnsCache\")\
2144\
2145function resolve(url)\
2146 local id = tonumber(url)\
2147 if id ~= nil then\
2148 return id\
2149 end\
2150 local entry = cache[url]\
2151 if entry == nil then\
2152 local protocol, hostname = string.match(url, \"(.+)://(.*)\")\
2153 if hostname == \"\" then\
2154 hostname = nil\
2155 end\
2156 entry = {rednet.lookup(protocol, hostname)}\
2157 cache[url] = entry\
2158 end\
2159 return unpack(entry)\
2160end\
2161\
2162function getHostname()\
2163 local hostname = os.getComputerLabel()\
2164 if hostname == nil then\
2165 hostname = tostring(os.computerID())\
2166 end\
2167 return hostname\
2168end\
2169\
2170function register(protocol, hostname)\
2171 if hostname == nil then\
2172 hostname = getHostname()\
2173 end\
2174 rednet.host(protocol, hostname)\
2175end",
2176}
2177local _baseFolder = fs.getDir(shell.getRunningProgram())
2178local originalOpen = fs.open
2179fs.open = function(file, mode)
2180 local localFile
2181 if _baseFolder == "" then
2182 localFile = file
2183 elseif file:sub(1, _baseFolder:len() + 1) == _baseFolder .. "/" then
2184 localFile = file:sub(_baseFolder:len() + 2)
2185 end
2186 local ours = _nuggetFiles[localFile]
2187 if not ours then
2188 return originalOpen(file, mode)
2189 end
2190 return {
2191 readAll = function() return ours end,
2192 close = function() end
2193 }
2194end
2195
2196local args = {...}
2197local ok, err = pcall(function()
2198 local program = loadfile(fs.combine(_baseFolder, "amber"), _ENV)
2199 program(table.unpack(args))
2200end)
2201if not ok then
2202 printError(err)
2203end
2204
2205fs.open = originalOpen