From 64dc597822578771823f3618861c8f5c305947ec Mon Sep 17 00:00:00 2001 From: osmarks Date: Sun, 10 Dec 2023 15:40:47 +0000 Subject: [PATCH] PotatOS Epenthesis --- README.md | 52 +- manifest | 4 +- src/bin/BSOD.lua | 3 + src/bin/b.lua | 1 + src/bin/build.lua | 19 + src/bin/chuck.lua | 1 + src/bin/clear_space.lua | 1 + src/bin/ctime.lua | 3 + src/bin/est.lua | 25 + src/bin/exorcise.lua | 8 + src/bin/factor.lua | 6 +- src/bin/fortune.lua | 1 + src/bin/game_mode.lua | 8 + src/bin/hacker.lua | 175 +++ src/bin/id.lua | 21 + src/bin/init-screens.lua | 2 + src/bin/intelligence.lua | 12 + src/bin/lmatrix.lua | 41 + src/bin/log.lua | 16 + src/bin/lyr.lua | 1 + src/bin/maxim.lua | 1 + src/bin/norris.lua | 1 + src/bin/potatonet.lua | 1 + src/bin/regset.lua | 9 + src/bin/tau.lua | 1 + src/bin/threat_update.lua | 37 + src/bin/uninstall.lua | 4 + src/bin/upd.lua | 1 + src/bin/very-uninstall.lua | 1 + src/bin/viewsource.lua | 49 + src/bin/wipe.lua | 1 + src/lib/cc_expect.lua | 145 ++ src/lib/json.lua | 2 +- src/lib/metatable_improvements.lua | 246 ++++ src/lib/sandboxlib.lua | 54 + src/lib/sha256.lua | 35 +- src/lib/window_buf.lua | 590 +++++++++ src/lib/yafss.lua | 88 +- src/main.lua | 644 ++++----- src/polychoron.lua | 341 +++-- src/potatobios.lua | 1178 +++-------------- src/xlib/00_cbor.lua | 19 +- src/xlib/{03_heavlisp.lua => 02_heavlisp.lua} | 0 src/xlib/03_lolcrypt.lua | 158 +++ website/index.html | 52 +- 45 files changed, 2435 insertions(+), 1623 deletions(-) create mode 100644 src/bin/BSOD.lua create mode 100644 src/bin/b.lua create mode 100644 src/bin/build.lua create mode 100644 src/bin/chuck.lua create mode 100644 src/bin/clear_space.lua create mode 100644 src/bin/ctime.lua create mode 100644 src/bin/est.lua create mode 100644 src/bin/exorcise.lua create mode 100644 src/bin/fortune.lua create mode 100644 src/bin/game_mode.lua create mode 100644 src/bin/hacker.lua create mode 100644 src/bin/id.lua create mode 100644 src/bin/init-screens.lua create mode 100644 src/bin/intelligence.lua create mode 100644 src/bin/lmatrix.lua create mode 100644 src/bin/log.lua create mode 100644 src/bin/lyr.lua create mode 100644 src/bin/maxim.lua create mode 100644 src/bin/norris.lua create mode 100644 src/bin/potatonet.lua create mode 100644 src/bin/regset.lua create mode 100644 src/bin/tau.lua create mode 100644 src/bin/threat_update.lua create mode 100644 src/bin/uninstall.lua create mode 100644 src/bin/upd.lua create mode 100644 src/bin/very-uninstall.lua create mode 100644 src/bin/viewsource.lua create mode 100644 src/bin/wipe.lua create mode 100644 src/lib/cc_expect.lua create mode 100644 src/lib/metatable_improvements.lua create mode 100644 src/lib/sandboxlib.lua create mode 100644 src/lib/window_buf.lua rename src/xlib/{03_heavlisp.lua => 02_heavlisp.lua} (100%) create mode 100644 src/xlib/03_lolcrypt.lua diff --git a/README.md b/README.md index 10a9625..24af7c4 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ # PotatOS "PotatOS" stands for "PotatOS Otiose Transformative Advanced Technology Or Something". -[This repository](https://git.osmarks.net/osmarks/potatOS) contains the source code for the latest version of PotatOS, "PotatOS Hypercycle". +[This repository](https://git.osmarks.net/osmarks/potatOS) contains the source code for the latest version of PotatOS, "PotatOS Epenthesis". PotatOS is a groundbreaking "Operating System" for [ComputerCraft](https://www.computercraft.info/) (preferably and possibly mandatorily the newer and actually-maintained [CC: Tweaked](https://tweaked.cc/)). -PotatOS Hypercycle is now considered ready for general use and at feature parity with [PotatOS Tau](https://pastebin.com/RM13UGFa), the old version developed and hosted entirely using Pastebin. -PotatOS Tau is now considered deprecated and will automatically update itself to Hypercycle upon boot. +PotatOS Epenthesis is now considered ready for general use and at feature parity with [PotatOS Tau](https://pastebin.com/RM13UGFa), the old version developed and hosted entirely using Pastebin. +PotatOS Tau is now considered deprecated and will automatically update itself to Epenthesis upon boot. +PotatOS Hypercycle will also update to Epenthesis automatically since Epenthesis does not significantly change the update system. You obviously want to install it now, so do this: `pastebin run 7HSiHybr`. @@ -20,6 +21,10 @@ Thanks to technology, we're able to offer a live PotatOS instance in your browse Experiencing PotatOS requires JavaScript. Please enable it. +## PotatOS Epenthesis + +PotatOS is dedicated to bringing you roughly functional, somewhat reliable code. Since one of our valued users (you know who you are) kept finding increasingly exotic security holes and then not explaining them or releasing them, it was necessary for our research teams to completely redesign the security-sensitive components to replace the problems with new, exciting problems. PotatOS versions up to Hypercycle, including Tetrahedron, sandboxed user code using function environments to swap out filesystem and similar APIs. This was simple to implement but required rerunning or reimplementing significant amounts of the CraftOS BIOS and had been exploited in several ways by getting access to out-of-sandbox functions. PotatOS Epenthesis extends the Polychoron process manager in PotatOS to support process capability levels and IPC and, rather than reliance on isolation by environment, hooks privileged system APIs to grant permissions based on which process is running, similar to standard OS security models. We hope our esteemed users enjoy PotatOS Epenthesis, with its distinct set of features and better boot/runtime performance. + ## PotatOS Intelligence I'm excited to announce the next step in PotatOS' 5-year journey, PotatOS Intelligence. @@ -59,7 +64,6 @@ Unlike most "OS"es for CC (primarily excluding Opus OS, which is actually useful - Remote debugging capabilities for development and stuff (highly* secured, via ECC signing on debugging disks and SPUDNET's security features). - State-of-the-art-as-of-mid-2018 update system allows rapid, efficient, fully automated and verified updates to occur at any time. - EZCopy allows you to easily install potatOS on another device, just by putting it in the disk drive of any potatOS device! EZCopy is unfortunately disabled on some servers. -- Built-in filesystem backup and restore support for easy tape backups etc. - Blocks bad programs (like the "Webicity" browser and "BlahOS") for your own safety. - Fully-featured coroutine-based process manager. Very fully-featured. No existing code uses most of the features. - Can run in "hidden mode" where it's at least not obvious at a glance that potatOS is installed. @@ -87,6 +91,8 @@ Unlike most "OS"es for CC (primarily excluding Opus OS, which is actually useful - Integrated logging mechanism for debugging. - [PotatOS Copilot](https://www.youtube.com/watch?v=KPp7PLi2nrI) assists you literally* anywhere in PotatOS. - Live threat updates using our advanced algorithms. +- PotatOS Epenthesis' rewritten security model fixes many exploits and adds others while reducing boot times. +- IPC mechanism. ## Architecture @@ -95,16 +101,16 @@ However, to ease development and/or exploit research (which there's a surprising ### Boot process -- normal ComputerCraft boot process - `bios.lua` runs `rom/programs/shell.lua` (or maybe multishell first) runs `rom/startup.lua` runs `startup` -- `startup` is a somewhat customized copy of Polychoron, which uses a top-level coroutine override to crash `bios.lua`'s `parallel.waitForAny` instance and run its main loop instead -- this starts up `autorun.lua` (which is a compiled bundle of `main.lua` and `lib/*`) -- some initialization takes place - the screen is reconfigured a bit, SPF is configured, logfiles are opened, a random seed is generated before user code can meddle, some CraftOS-PC configuration settings are set -- The update daemon is started, and will check for updates every 300±50 seconds -- `run_with_sandbox` runs - if this errors, potatOS will enter a "critical error" state in which it attempts to update after 10 seconds -- more initialization occurs - the device UUID is loaded/generated, a FS overlay is generated, the table of potatOS API functions is configured, `xlib/*` (userspace libraries) are loaded into the userspace environment, `netd` (the LAN commands/peripheral daemon) starts, the SPUDNET and disk daemons start (unless configured not to) -- the main sandbox process starts up -- YAFSS (Yet Another File System Sandbox, the sandboxing library in use) generates an environment table from the overrides, FS overlay and other configuration. This is passed as an argument to `load`, along with the PotatoBIOS code. -- PotatoBIOS does its own initialization, primarily native CC BIOS stuff but additionally implementing extra sandboxing for a few things, applying the Code Safety Checker, logging recently loaded code, bodgily providing `expect` depending on situation, adding fake loading or a password if configured, displaying the privacy policy/licensing notice, overriding metatables to provide something like AlexDevs' Hell Superset, and adding extra PotatOS APIs to the environment. +- Normal ComputerCraft boot process - `bios.lua` runs `rom/programs/shell.lua` (or maybe multishell first) runs `rom/startup.lua` runs `startup`. +- `startup` contains the PotatOS process manager, Polychoron, which uses a top-level coroutine override to crash `bios.lua`'s `parallel.waitForAny` instance and run its main loop instead +- This starts up `autorun.lua` (which is a compiled bundle of `main.lua` and `lib/*`). +- Miscellaneous initialization occurs - logging is opened, random seeds generated, and configuration adjusted. +- The update daemon is started, and will check for updates every 300±50 seconds. +- `run_with_sandbox` is entered - if this fails, potatOS will enter a "critical error" state in which it attempts to update after 10 seconds. +- More initialization occurs - the device UUID is loaded/generated, a FS overlay is generated, the table of potatOS API functions is configured, `xlib/*` (userspace libraries) are loaded into the userspace environment, `netd` (the LAN commands/peripheral daemon) starts, the SPUDNET and disk daemons start (unless configured not to) +- PotatOS hooks the filesystem API to gate access based on the currently running process's capability level. +- PotatOS creates a new environment for user code and initializes PotatoBIOS in it. +- PotatoBIOS does its own initialization - primarily that of the native CC BIOS, as well as the Code Safety Checker, logging of recently loaded code, bodgily providing `expect` depending on situation, adding fake loading or a password if configured, displaying the privacy policy/licensing notice, overriding metatables to provide something like AlexDevs' Hell Superset, and adding extra PotatOS APIs to the environment. - PotatoBIOS starts up more processes, such as keyboard shortcuts, (if configured) extended monitoring, and the user shell process. - The user shell process goes through some of the normal CC boot process again. @@ -114,14 +120,14 @@ The PotatOS userspace API, mostly accessible from `_G.potatOS`, has absolutely n It's also not really documented. Fun! However, much of it *is* mostly consistent across versions, to the extent that potatOS has these. -Here's a list of some of the more useful and/or consistently available functions: +Here's a list of some of the more useful and/or consistently available functions (TODO UPDATE): - `potatOS.add_log(message: string, ...formattingArgs: any)` - add a line to the log file - supports `string.format`-style formatting - `potatOS.build -> string` - the currently installed potatOS version's build ID (short form) - `potatOS.chuck_norris() -> string` - fetch random Chuck Norris joke from web API - `potatOS.fortune() -> string` - fetch random `fortune` from web API - `potatOS.evilify()` - mess up 1 in 10 keypresses -- `potatOS.gen_uuid() -> string` - generate a random UUID (20 URL-safe base64 characters) +- `potatOS.gen_uuid() -> string` - generate a random UUID (20 URL-safe base64 characters) (not actually a spec-compliant UUID) - `potatOS.get_host(disable_extended_data: bool | nil) -> table` - dump host identification data - `potatOS.get_location() -> number, number, number | nil` - get GPS location, if available. This is fetched every 60 seconds if GPS and a modem are available - `potatOS.init_screens()` - reset palettes to default @@ -137,10 +143,17 @@ Here's a list of some of the more useful and/or consistently available functions - `potatOS.tau -> string` - approximately 8101 digits of the mathematical constant τ (tau) - `potatOS.update()` - force a system update - `potatOS.uuid -> string` - get the system's PotatOS UUID. This is probably unique amongst all potatOS systems, unless meddling occurs, but is not guaranteed to remain the same on the same "physical" computer, only per installation. +- `potatOS.assistant_history -> table` - PotatOS Intelligence assistant messages. +- `potatOS.llm(prompt: string, max_tokens: number, stop_sequences: table) -> string` - invoke PotatOS Intelligence language model. +- `potatOS.metaphor() -> string` - generate metaphor. +- `potatOS.unhexize(hex: string) -> table` - hex to byte array. +- `potatOS.hexize(bytes: table) -> string` - byte array to hex. +- `potatOS.shuffle(x: table)` - shuffle a list. - `process.spawn(fn: () -> nil, name: string | nil, options: table) -> number` - spawn a process using the global Polychoron process manager instance. Returns the ID. -- `process.info(ID: number) -> table` - get information about a process, by ID -- `process.list() -> table` - get information for all running processes -- `_G.init_code -> string` - the source code of the running PotatoBIOS instance +- `process.info(ID: number) -> table` - get information about a process, by ID. +- `process.list() -> table` - get information for all running processes. +- `process.IPC(target: number, ...args: any)` - send IPC message to given process. +- `_G.init_code -> string` - the source code of the running PotatoBIOS instance. ## Reviews @@ -162,6 +175,7 @@ Here's a list of some of the more useful and/or consistently available functions - "PotatOS is many, varied, ever-changing, and eternal. Fighting it is like fighting a many-headed monster, which, each time a neck is severed, sprouts a head even fiercer and cleverer than before. You are fighting that which is unfixed, mutating, indestructible." - someone - "go use potatos or something" - SwitchCraft3 (official), 2023 - "a lot of backup time is spent during potatos" - Lemmmy, 2022 +- "we would need 176000 comparators to store potatOS" - piguman3, 2023 - "potatOS is as steady as a rock" - BlackDragon, 2021 - "PotatOS would be a nice religion" - piguman3, 2022 - "It has caused multiple issues to staff of multiple CC servers." - Wojbie, 2023 diff --git a/manifest b/manifest index e4b4f01..eaa144c 100644 --- a/manifest +++ b/manifest @@ -1,2 +1,2 @@ -{"build":460,"description":"PotatOS Assistant memory","files":{"LICENSES":"f3549d84d66eb53dd4a421a4341d77d3d217c1b117d67e3be8f5211adcda0952","autorun.lua":"1e38281f95497e7fbe95785b33913b7a189b1158e7a1b404fc1e5323031175f1","bin/5rot26.lua":"417891a232e325476f980d31d88edc486d526611a6350ce47fd29cca464ebf2c","bin/ccemux.lua":"239476f58835b86bbcac31ce8af3c3acd3d198a55ab9ada78c62fbf358625a98","bin/chronometer.lua":"db5363993a04382145aef7db2fbe262f0bf10697a589e1e2d2f9ce0f87430dd8","bin/factor.lua":"f8d223839e6b9f4e8c85f46e8182e7ede7ec41e6644f0188d1f315014c79a2c0","bin/grep.lua":"1509bc267867b933e528ab74cfbc2a15fa2df0ec7389df4f9033194ab9037865","bin/kristminer.lua":"7e7f9fe2a6493d584ad6926cda915e02c1c3d800dc209680898ce930d0bb0e6f","bin/livegps.lua":"c3d17d495cda01aa1261e4c4fcd43439b29af422671972117ec34f68e32c5bba","bin/loading.lua":"c85f7aa1765170325155b921c1fceeb62643f552f12d41b529a22af3a67f5a97","bin/potatoflight.lua":"2fbb0b6f8d78728d8cb0ec64af1bc598bd00cb55f202378e7acdb86bba71efd1","bin/potatoplex.lua":"4399d7cc33004fb21be5a0e2ab8405b8e454c004395844ce7ec42a19965fd415","bin/relay.lua":"261ae6c220b83506e3326e8f2b091d246baae458ff0d2ee87512be2c4e35a75d","bin/tryhaskell.lua":"07810d85145da65a3e434154c79d5a9d72f2dcbe59c8d6829040fb925df878ec","bin/workspace.lua":"acc8bb6f08b243378b68ab5f611e9a6cc8216b0713343dc93ddaa9101f07ffc5","potatobios.lua":"07a5ba0be0a278fbaf2c33f8cc08adff738e8903165aeb74bf3160ab8290e6a5","signing-key.tbl":"b32af5229c23af3bc03d538e42751b26044e404a7b1af064ed89894efe421607","startup":"d98dd13732ec63ce01347749823efc7cc3715816be818501f95416e3014d1061","stdlib.hvl":"a6fd2620068f47794a9bbeed77bee3fd4962f848e6dd7c75137b30cd5665272e","update-key.hex":"8d8afb7a45833bb7d68f929421ad60a211d4d73e0ee03b24dc0106ba1de2e1a0","xlib/00_cbor.lua":"8b1cc3588a5e31298d22e50b6752a4413f12f8113622962ed6555557448b408b","xlib/01_skynet.lua":"9cb565d639a0acd7c763c3e7422482532cd0bda0cdfcc720089ab4a87e551339","xlib/03_heavlisp.lua":"82cdabd5286058c0ea4f27956f8c1144e198769c8b8ce9e91b26c930d711f710"},"sizes":{"LICENSES":4725,"autorun.lua":104010,"bin/5rot26.lua":1661,"bin/ccemux.lua":1673,"bin/chronometer.lua":1152,"bin/factor.lua":4263,"bin/grep.lua":1196,"bin/kristminer.lua":5566,"bin/livegps.lua":980,"bin/loading.lua":7707,"bin/potatoflight.lua":3417,"bin/potatoplex.lua":6584,"bin/relay.lua":3075,"bin/tryhaskell.lua":1867,"bin/workspace.lua":42971,"potatobios.lua":52245,"signing-key.tbl":190,"startup":8438,"stdlib.hvl":851,"update-key.hex":44,"xlib/00_cbor.lua":15831,"xlib/01_skynet.lua":3286,"xlib/03_heavlisp.lua":15643},"timestamp":1699988880} -{"hash":"aa934a92d7e0dbeed14a4e23eed03d01d4fa4f8d80dbd418f5b346b1ea377d8a","sig":"7d34037f8a5aac6e5b14b54c02b1ea899036e82110baa7b94271767d6716f4fcdd3fb674918411e8871e"} \ No newline at end of file +{"build":741,"description":"try minified version","files":{"LICENSES":"f3549d84d66eb53dd4a421a4341d77d3d217c1b117d67e3be8f5211adcda0952","autorun.lua":"816129d9c213b3386893869800fd959b5aaa8044ad5ec4ac8f8ff8aad6ec2154","bin/5rot26.lua":"417891a232e325476f980d31d88edc486d526611a6350ce47fd29cca464ebf2c","bin/BSOD.lua":"a2ea9bf1e64dbc2c314e3be71f46e973c0bc2b9c482395120f5d152c6d231e86","bin/b.lua":"5123c6d1bb2b3d6c8e7b4d1b94e60d47c3b6c64c5a0fe8bda8481b718ba00602","bin/build.lua":"a990239e1db05176dd0ba56bc0179eecccd8473d88c6c618d16e72ede270e4c2","bin/ccemux.lua":"239476f58835b86bbcac31ce8af3c3acd3d198a55ab9ada78c62fbf358625a98","bin/chronometer.lua":"db5363993a04382145aef7db2fbe262f0bf10697a589e1e2d2f9ce0f87430dd8","bin/chuck.lua":"571a1eacde435bbd9f54b493149339ac48972a3a4550635b9107da0091fe888a","bin/clear_space.lua":"fc3d52563adaf0491b71227f8b6f615d604063a89830a2ab86a20d10e1a07e97","bin/ctime.lua":"39abe36b15724bcb09f2f901a88a1efb453f8896fe5d07769f434db3bd891652","bin/est.lua":"88ab488c2ded31d67816da8c309a63ac455b461368b3b943aeeb5106b1051cc0","bin/exorcise.lua":"ae25e5939ce52620b0a1e64679a1ac47bcaaff9e323d29c6651bce24a3e58116","bin/factor.lua":"39c2f1709a4258d754278860177c7bb2ea336a8b8392a4c3017c849705e63926","bin/fortune.lua":"64a595afb9ffbaa39622b32379d8dc40d4f1fd36d38027382ed0ae130ad0c59d","bin/game_mode.lua":"f1519969a83e6c7c001d4092cbc7806ea489652ea19022425a893fb4d153572f","bin/grep.lua":"1509bc267867b933e528ab74cfbc2a15fa2df0ec7389df4f9033194ab9037865","bin/hacker.lua":"889e1a47c5ff7470ddf61d87eb7cca9750193976139d513fbe67b7625195d65a","bin/id.lua":"82131679ee35c705458660ac31ab4f5f90169b43f2377fe420a99b1d4c03a4dd","bin/init-screens.lua":"c586a7704030dc1917262f04350e094e1e0ab084793eb1643222498f6436b597","bin/intelligence.lua":"0f14f5a5fb2c6053c19c7a8fa0ba335a69a6fb3b49941c3234dac59795cd3850","bin/kristminer.lua":"7e7f9fe2a6493d584ad6926cda915e02c1c3d800dc209680898ce930d0bb0e6f","bin/livegps.lua":"c3d17d495cda01aa1261e4c4fcd43439b29af422671972117ec34f68e32c5bba","bin/lmatrix.lua":"9d5728b93069d2b763bb8e1dd2a5542995c47daca61f385644431bfc315bc2c4","bin/loading.lua":"c85f7aa1765170325155b921c1fceeb62643f552f12d41b529a22af3a67f5a97","bin/log.lua":"8a553607f81b45e3e0e6d3087735c75fbb38e585d7d1f7a9451b3cb5d0dac330","bin/lyr.lua":"5b17b8cf560ac5dbe4f458d36dff853f51103492952048f233fcad8b319c04e6","bin/maxim.lua":"a68abcb1afae04c9e2177459cd6cb35cf417e4dc80a5bc4580e7cd9b05a44602","bin/norris.lua":"e3105b98d6ac2ba038847fe4a8977db6fbf513b5de6ca3052e7ce20f79d4a314","bin/potatoflight.lua":"2fbb0b6f8d78728d8cb0ec64af1bc598bd00cb55f202378e7acdb86bba71efd1","bin/potatonet.lua":"d58e6aee25190e62a826cc9d195c4aa7e91ac43147d9a8bb8e86d139c7c5bde9","bin/potatoplex.lua":"4399d7cc33004fb21be5a0e2ab8405b8e454c004395844ce7ec42a19965fd415","bin/regset.lua":"423879f14de9efb8192ee718a1d5e129e21f50c50799651b2dcff65287808807","bin/relay.lua":"261ae6c220b83506e3326e8f2b091d246baae458ff0d2ee87512be2c4e35a75d","bin/tau.lua":"45626e749b8734bf466b89f769ec3e5544983a55086c6a165eaabdc0b010b6ac","bin/threat_update.lua":"eb6e27f70718e2d6d00fd7d7dba81e9e11badb718c74bc7fb306464ecfba5bdf","bin/tryhaskell.lua":"07810d85145da65a3e434154c79d5a9d72f2dcbe59c8d6829040fb925df878ec","bin/uninstall.lua":"12744da9213a7dba4e72c3dd0a2ac8b84c3a8315247ee4c9aa9af392dfa50b82","bin/upd.lua":"9ce75f3d428f99263392814596b4f782ea17e7f6096dad6038eac65c3fd68cd5","bin/very-uninstall.lua":"90ff8362f85e0acefff011241b0161499a3dc47fa4f2a4c21f6f0789ab9f19b5","bin/viewsource.lua":"02b4dcdb3cf064e7018117fc68a574b260e21561f1813951e4e1de8f0c9420a6","bin/wipe.lua":"2dbc079215c0c06fda182b8878f533631708d95d6148c65f24c606bc3786e2fb","bin/workspace.lua":"acc8bb6f08b243378b68ab5f611e9a6cc8216b0713343dc93ddaa9101f07ffc5","potatobios.lua":"70ed604a6ca25171fa65caa4c2e3c33081700f592b0a1bffd1e293cc9f4b8e16","signing-key.tbl":"b32af5229c23af3bc03d538e42751b26044e404a7b1af064ed89894efe421607","startup":"66f6cde7c7376ea573dff31d82ba99cad41ad05ac9c1e63d91e596cfada47c5e","stdlib.hvl":"a6fd2620068f47794a9bbeed77bee3fd4962f848e6dd7c75137b30cd5665272e","update-key.hex":"8d8afb7a45833bb7d68f929421ad60a211d4d73e0ee03b24dc0106ba1de2e1a0","xlib/00_cbor.lua":"cb00cf146c439edc4caf3a6d0913f6454aa421a85b5b40d7b7f4de5cd7f16a80","xlib/01_skynet.lua":"9cb565d639a0acd7c763c3e7422482532cd0bda0cdfcc720089ab4a87e551339","xlib/02_heavlisp.lua":"82cdabd5286058c0ea4f27956f8c1144e198769c8b8ce9e91b26c930d711f710","xlib/03_lolcrypt.lua":"0ca423837248e405898c436fd7f39c1fff63ba1a1c5610f3e9fb36151a698ff5"},"sizes":{"LICENSES":4725,"autorun.lua":108368,"bin/5rot26.lua":1661,"bin/BSOD.lua":104,"bin/b.lua":34,"bin/build.lua":639,"bin/ccemux.lua":1673,"bin/chronometer.lua":1152,"bin/chuck.lua":29,"bin/clear_space.lua":105,"bin/ctime.lua":141,"bin/est.lua":787,"bin/exorcise.lua":208,"bin/factor.lua":4244,"bin/fortune.lua":24,"bin/game_mode.lua":226,"bin/grep.lua":1196,"bin/hacker.lua":8021,"bin/id.lua":548,"bin/init-screens.lua":36,"bin/intelligence.lua":309,"bin/kristminer.lua":5566,"bin/livegps.lua":980,"bin/lmatrix.lua":1005,"bin/loading.lua":7707,"bin/log.lua":379,"bin/lyr.lua":72,"bin/maxim.lua":22,"bin/norris.lua":45,"bin/potatoflight.lua":3417,"bin/potatonet.lua":19,"bin/potatoplex.lua":6584,"bin/regset.lua":314,"bin/relay.lua":3075,"bin/tau.lua":124,"bin/threat_update.lua":1334,"bin/tryhaskell.lua":1867,"bin/uninstall.lua":166,"bin/upd.lua":16,"bin/very-uninstall.lua":80,"bin/viewsource.lua":1275,"bin/wipe.lua":73,"bin/workspace.lua":42971,"potatobios.lua":40253,"signing-key.tbl":190,"startup":13329,"stdlib.hvl":851,"update-key.hex":44,"xlib/00_cbor.lua":15281,"xlib/01_skynet.lua":3286,"xlib/02_heavlisp.lua":15643,"xlib/03_lolcrypt.lua":3206},"timestamp":1702217748} +{"hash":"adea933b3786904497b9b7ec6ba757cc4dbc5b8889bbbaa69ee7e9f0e2977142","sig":"b067d65e92d33e89067e215abf1f163a587b93472ca81652c3847b1dbe6b61ba3cd907d17cc36a9b751f"} \ No newline at end of file diff --git a/src/bin/BSOD.lua b/src/bin/BSOD.lua new file mode 100644 index 0000000..d615d7f --- /dev/null +++ b/src/bin/BSOD.lua @@ -0,0 +1,3 @@ +local w, h = term.getSize() +polychoron.BSOD(potatOS.randbytes(math.random(0, w * h))) +os.pullEvent "key" \ No newline at end of file diff --git a/src/bin/b.lua b/src/bin/b.lua new file mode 100644 index 0000000..82ac2c0 --- /dev/null +++ b/src/bin/b.lua @@ -0,0 +1 @@ +print "abcdefghijklmnopqrstuvwxyz" \ No newline at end of file diff --git a/src/bin/build.lua b/src/bin/build.lua new file mode 100644 index 0000000..f69df28 --- /dev/null +++ b/src/bin/build.lua @@ -0,0 +1,19 @@ +print("Short hash", potatOS.build) +print("Full hash", potatOS.full_build) +local mfst = potatOS.registry.get "potatOS.current_manifest" +if mfst then + print("Counter", mfst.build) + print("Built at (local time)", os.date("%Y-%m-%d %X", mfst.timestamp)) + print("Downloaded from", mfst.manifest_URL) + local verified = mfst.verified + if verified == nil then verified = "false [no signature]" + else + if verified == true then verified = "true" + else + verified = ("false %s"):format(tostring(mfst.verification_error)) + end + end + print("Signature verified", verified) +else + print "Manifest not found in registry. Extended data unavailable." +end \ No newline at end of file diff --git a/src/bin/chuck.lua b/src/bin/chuck.lua new file mode 100644 index 0000000..a4cc3bb --- /dev/null +++ b/src/bin/chuck.lua @@ -0,0 +1 @@ +print(potatOS.chuck_norris()) \ No newline at end of file diff --git a/src/bin/clear_space.lua b/src/bin/clear_space.lua new file mode 100644 index 0000000..bf04464 --- /dev/null +++ b/src/bin/clear_space.lua @@ -0,0 +1 @@ +potatOS.clear_space((... and tonumber(...) and tonumber(...) == tonumber(...)) and tonumber(...) or 4096) \ No newline at end of file diff --git a/src/bin/ctime.lua b/src/bin/ctime.lua new file mode 100644 index 0000000..fd74d74 --- /dev/null +++ b/src/bin/ctime.lua @@ -0,0 +1,3 @@ +for _, info in pairs(process.list()) do + textutils.pagedPrint(("%s %f %f"):format(info.name or info.ID, info.execution_time, info.ctime)) +end \ No newline at end of file diff --git a/src/bin/est.lua b/src/bin/est.lua new file mode 100644 index 0000000..ba20d98 --- /dev/null +++ b/src/bin/est.lua @@ -0,0 +1,25 @@ +-- edit reality to match typo in docs +function Safe_SerializeWithtextutilsDotserialize(Valuje) + local _, __ = pcall(textutils.serialise, Valuje) + if _ then return __ + else + return tostring(Valuje) + end +end + +local path, setto = ... +path = path or "" + +if setto ~= nil then + local x, jo, jx = textutils.unserialise(setto), pcall(json.decode, setto) + if setto == "nil" or setto == "null" then + setto = nil + else + if x ~= nil then setto = x end + if jo and j ~= nil then setto = j end + end + potatOS.registry.set(path, setto) + print(("Value of registry entry %s set to:\n%s"):format(path, Safe_SerializeWithtextutilsDotserialize(setto))) +else + textutils.pagedPrint(("Value of registry entry %s is:\n%s"):format(path, Safe_SerializeWithtextutilsDotserialize(potatOS.registry.get(path)))) +end \ No newline at end of file diff --git a/src/bin/exorcise.lua b/src/bin/exorcise.lua new file mode 100644 index 0000000..cf13e53 --- /dev/null +++ b/src/bin/exorcise.lua @@ -0,0 +1,8 @@ +-- like delete but COOLER and LATIN +for _, wcard in pairs{...} do + for _, path in pairs(fs.find(wcard)) do + fs.ultradelete(path) + local n = potatOS.lorem():gsub("%.", " " .. path .. ".") + print(n) + end +end \ No newline at end of file diff --git a/src/bin/factor.lua b/src/bin/factor.lua index a2756e6..fd7d37c 100644 --- a/src/bin/factor.lua +++ b/src/bin/factor.lua @@ -12,9 +12,10 @@ repeat write "Provide an integer to factorize: " x = tonumber(read()) if not x or math.floor(x) ~= x then print("That is NOT an integer.") end + if x and x < 2 then print("I forgot to mention this, but also don't use 1, 0 or negative integers.") end until x -if x > (2^40) then print("WARNING: Number is quite big. Due to Lua floating point limitations, draconic entities MAY be present. If this runs for several seconds, it's probably frozen due to this.") end +if x > (2^40) then print("WARNING: Number is quite big. Due to Lua floating point limitations, draconic entities MAY be present and results may be blatantly wrong. If this runs for several seconds, it's probably frozen due to this.") end local floor, abs, random, log, pow = math.floor, math.abs, math.random, math.log, math.pow @@ -28,17 +29,16 @@ local function eps_compare(x, y) return abs(x - y) < 1e-14 end +-- binary modular exponentiation local function modexp(a, b, n) if b == 0 then return 1 % n end if b == 1 then return a % n end local bdiv2 = b / 2 local fbdiv2 = floor(bdiv2) if eps_compare(bdiv2, fbdiv2) then - -- b is even, so it is possible to just modexp with HALF the exponent and square it (mod n) local x = modexp(a, fbdiv2, n) return (x * x) % n else - -- not even, so subtract 1 (this is even), modexp that, and multiply by a again (mod n) return (modexp(a, b - 1, n) * a) % n end end diff --git a/src/bin/fortune.lua b/src/bin/fortune.lua new file mode 100644 index 0000000..3a8d939 --- /dev/null +++ b/src/bin/fortune.lua @@ -0,0 +1 @@ +print(potatOS.fortune()) \ No newline at end of file diff --git a/src/bin/game_mode.lua b/src/bin/game_mode.lua new file mode 100644 index 0000000..a310c63 --- /dev/null +++ b/src/bin/game_mode.lua @@ -0,0 +1,8 @@ +potatOS.evilify() +print "GAME KEYBOARD enabled." +potatOS.init_screens() +print "GAME SCREEN enabled." +print "Activated GAME MODE." +--bigfont.bigWrite "GAME MODE." +--local x, y = term.getCursorPos() +--term.setCursorPos(1, y + 3) \ No newline at end of file diff --git a/src/bin/hacker.lua b/src/bin/hacker.lua new file mode 100644 index 0000000..cdd5921 --- /dev/null +++ b/src/bin/hacker.lua @@ -0,0 +1,175 @@ +local mat = term.current() +mat.setPaletteColor(colors.black, 0) +mat.setPaletteColor(colors.green, 0x0cff0c) +mat.setPaletteColor(colors.red, 0xff0000) +if mat.setTextScale then mat.setTextScale(0.5) end +mat.setTextColor(colors.green) +term.redirect(mat) + +local jargonWords = { + acronyms = + {"TCP", "HTTP", "SDD", "RAM", "GB", "CSS", "SSL", "AGP", "SQL", "FTP", "PCI", "AI", "ADP", + "RSS", "XML", "EXE", "COM", "HDD", "THX", "SMTP", "SMS", "USB", "PNG", "PHP", "UDP", + "TPS", "RX", "ASCII", "CD-ROM", "CGI", "CPU", "DDR", "DHCP", "BIOS", "IDE", "IP", "MAC", + "MP3", "AAC", "PPPoE", "SSD", "SDRAM", "VGA", "XHTML", "Y2K", "GUI", "EPS", "SATA", "SAS", + "VM", "LAN", "DRAM", "L3", "L2", "DNS", "UEFI", "UTF-8", "DDOS", "HDMI", "GPU", "RSA", "AES", + "L7", "ISO", "HTTPS", "SSH", "SIMD", "GNU", "PDF", "LPDDR5", "ARM", "RISC", "CISC", "802.11", + "5G", "LTE", "3GPP", "MP4", "2FA", "RCE", "JBIG2", "ISA", "PCIe", "NVMe", "SHA", "QR", "CUDA", + "IPv4", "IPv6", "ARP", "DES", "IEEE", "NoSQL", "UTF-16", "ADSL", "ABI", "TX", "HEVC", "AVC", + "AV1", "ASLR", "ECC", "HBA", "HAL", "SMT", "RPC", "JIT", "LCD", "LED", "MIME", "MIMO", "LZW", + "LGA", "OFDM", "ORM", "PCRE", "POP3", "SMTP", "802.3", "PSU", "RGB", "VLIW", "VPS", "VPN", + "XMPP", "IRC", "GNSS"}, + adjectives = + {"auxiliary", "primary", "back-end", "digital", "open-source", "virtual", "cross-platform", + "redundant", "online", "haptic", "multi-byte", "Bluetooth", "wireless", "1080p", "neural", + "optical", "solid state", "mobile", "unicode", "backup", "high speed", "56k", "analog", + "fiber optic", "central", "visual", "ethernet", "Griswold", "binary", "ternary", + "secondary", "web-scale", "persistent", "Java", "cloud", "hyperscale", "seconday", "cloudscale", + "software-defined", "hyperconverged", "x86", "Ethernet", "WiFi", "4k", "gigabit", "neuromorphic", + "sparse", "machine learning", "authentication", "multithreaded", "statistical", "nonlinear", + "photonic", "streaming", "concurrent", "memory-safe", "C", "electromagnetic", "nanoscale", + "high-level", "low-level", "distributed", "accelerated", "base64", "purely functional", + "serial", "parallel", "compute", "graphene", "recursive", "denormalized", "orbital", + "networked", "autonomous", "applicative", "acausal", "hardened", "category-theoretic", + "ultrasonic"}, + nouns = + {"driver", "protocol", "bandwidth", "panel", "microchip", "program", "port", "card", + "array", "interface", "system", "sensor", "firewall", "hard drive", "pixel", "alarm", + "feed", "monitor", "application", "transmitter", "bus", "circuit", "capacitor", "matrix", + "address", "form factor", "array", "mainframe", "processor", "antenna", "transistor", + "virus", "malware", "spyware", "network", "internet", "field", "acutator", "tetryon", + "beacon", "resonator", "diode", "oscillator", "vertex", "shader", "cache", "platform", + "hyperlink", "device", "encryption", "node", "headers", "botnet", "applet", "satellite", + "Unix", "byte", "Web 3", "metaverse", "microservice", "ultrastructure", "subsystem", + "call stack", "gate", "filesystem", "file", "database", "bitmap", "Bloom filter", "tensor", + "hash table", "tree", "optics", "silicon", "hardware", "uplink", "script", "tunnel", + "server", "barcode", "exploit", "vulnerability", "backdoor", "computer", "page", + "regex", "socket", "platform", "IP", "compiler", "interpreter", "nanochip", "certificate", + "API", "bitrate", "acknowledgement", "layout", "satellite", "shell", "MAC", "PHY", "VLAN", + "SoC", "assembler", "interrupt", "directory", "display", "functor", "bits", "logic", + "sequence", "procedure", "subnet", "invariant", "monad", "endofunctor", "borrow checker"}, + participles = + {"backing up", "bypassing", "hacking", "overriding", "compressing", "copying", "navigating", + "indexing", "connecting", "generating", "quantifying", "calculating", "synthesizing", + "inputting", "transmitting", "programming", "rebooting", "parsing", "shutting down", + "injecting", "transcoding", "encoding", "attaching", "disconnecting", "networking", + "triaxilating", "multiplexing", "interplexing", "rewriting", "transducing", + "acutating", "polarising", "diffracting", "modulating", "demodulating", "vectorizing", + "compiling", "jailbreaking", "proxying", "Linuxing", "quantizing", "multiplying", + "scanning", "interpreting", "routing", "rerouting", "tunnelling", "randomizing", + "underwriting", "accessing", "locating", "rotating", "invoking", "utilizing", + "normalizing", "hijacking", "integrating", "type-checking", "uploading", "downloading", + "allocating", "receiving", "decoding"} +} + +local hcresponses = { + 'Authorizing ', + 'Authorized...', + 'Access Granted..', + 'Going Deeper....', + 'Compression Complete.', + 'Compilation of Data Structures Complete..', + 'Entering Security Console...', + 'Encryption Unsuccesful Attempting Retry...', + 'Waiting for response...', + '....Searching...', + 'Calculating Space Requirements', + "nmap 192.168.1.0/24 -p0-65535", + "Rescanning Databases...", + "Hacking all IPs simultaneously...", + "All webs down, activating proxy", + "rm -rf --no-preserve-root /", + "Hacking military satellite network...", + "Guessing password...", + "Trying 'password123'", + "Activating Extra Monitors...", + "Typing Faster...", + "Checking StackOverflow", + "Locating crossbows...", + "Enabling algorithms and coding", + "Collapsing Subdirectories...", + "Enabling Ping Wall...", + "Obtaining sunglasses...", + "Rehashing hashes.", + "Randomizing numbers.", + "Greening text...", + "Accessing system32", + "'); DROP DATABASE system;--", + "...Nesting VPNs...", + "Opening Wireshark.", + "Breaking fifth wall....", + "Flipping arrows and applying yoneda lemma", + "Rewriting in Rust" +} + +local function choose(arr) + return arr[math.random(1, #arr)] +end + +local function capitalize_first(s) + return s:sub(1, 1):upper() .. s:sub(2) +end + +local function jargon() + local choice = math.random() + local thing + if choice > 0.5 then + thing = choose(jargonWords.adjectives) .. " " .. choose(jargonWords.acronyms) + elseif choice > 0.1 then + thing = choose(jargonWords.acronyms) .. " " .. choose(jargonWords.adjectives) + else + thing = choose(jargonWords.adjectives) .. " " .. choose(jargonWords.acronyms) .. " " .. choose(jargonWords.nouns) + end + thing = thing .. " " .. choose(jargonWords.nouns) + local out + if math.random() > 0.3 then + out = choose(jargonWords.participles) .. " " .. thing + else + out = thing .. " " .. choose(jargonWords.participles) + :gsub("writing", "wrote") + :gsub("breaking", "broken") + :gsub("overriding", "overriden") + :gsub("shutting", "shut") + :gsub("ying", "ied") + :gsub("ing", "ed") + end + return capitalize_first(out) +end + +local function lgen(cs, n) + local out = {} + for i = 1, n do + local r = math.random(1, #cs) + table.insert(out, cs:sub(r, r)) + end + return table.concat(out) +end + +local function scarynum() + local r = math.random() + if r > 0.7 then + return lgen("0123456789abcdef", 16) + elseif r > 0.4 then + return lgen("01", 32) + else + return tostring(math.random()) + end +end + +while true do + local r = math.random(1, 3) + if r == 1 then + print(jargon()) + elseif r == 2 then + for i = 1, math.random(1, 3) do write(scarynum() .. " ") end + print() + else + print(choose(hcresponses)) + end + if math.random() < 0.005 then + term.setTextColor(colors.red) + print "Terminated" + term.setTextColor(colors.green) + end + sleep(math.random() * 0.5) +end \ No newline at end of file diff --git a/src/bin/id.lua b/src/bin/id.lua new file mode 100644 index 0000000..cc44c8b --- /dev/null +++ b/src/bin/id.lua @@ -0,0 +1,21 @@ +print("ID", os.getComputerID()) +print("Label", os.getComputerLabel()) +print("UUID", potatOS.uuid) +print("Build", potatOS.build) +print("Host", _ORIGHOST or _HOST) +local disks = {} +for _, n in pairs(peripheral.getNames()) do + if peripheral.getType(n) == "drive" then + local d = peripheral.wrap(n) + if d.hasData() then + table.insert(disks, {n, tostring(d.getDiskID() or "[ID?]"), d.getDiskLabel()}) + end + end +end +if #disks > 0 then + print "Disks:" + textutils.tabulate(unpack(disks)) +end +if potatOS.get_ip() then + print("IP", potatOS.get_ip()) +end \ No newline at end of file diff --git a/src/bin/init-screens.lua b/src/bin/init-screens.lua new file mode 100644 index 0000000..81c5255 --- /dev/null +++ b/src/bin/init-screens.lua @@ -0,0 +1,2 @@ +potatOS.init_screens() +print "Done!" \ No newline at end of file diff --git a/src/bin/intelligence.lua b/src/bin/intelligence.lua new file mode 100644 index 0000000..76a669e --- /dev/null +++ b/src/bin/intelligence.lua @@ -0,0 +1,12 @@ +-- PotatOS Intelligence interface +if ... == "wipe_memory" then + print "Have you acquired PIERB approval to wipe memory? (y/n): " + if read():lower():match "y" then + potatOS.assistant_history = {} + potatOS.save_assistant_state() + print "Done." + end +else + local w, h = term.getSize() + potatOS.assistant(h) +end \ No newline at end of file diff --git a/src/bin/lmatrix.lua b/src/bin/lmatrix.lua new file mode 100644 index 0000000..ab17ec4 --- /dev/null +++ b/src/bin/lmatrix.lua @@ -0,0 +1,41 @@ +local mat = term.current() +mat.setPaletteColor(colors.black, 0) +mat.setPaletteColor(colors.green, 0x15b01a) +mat.setPaletteColor(colors.lime, 0x01ff07) +if mat.setTextScale then mat.setTextScale(0.5) end +local w, h = mat.getSize() + +local function rchar() + return string.char(math.random(0, 255)) +end + +local function wrap(x) + return (x - 1) % h + 1 +end + +local cols = {} +for i = 1, w do + local base = math.random(1, h) + table.insert(cols, { base, base + math.random(1, h - 5) }) +end + +while true do + for x, col in pairs(cols) do + local start = col[1] + local endp = col[2] + mat.setCursorPos(x, start) + mat.write " " + mat.setCursorPos(x, wrap(endp - 1)) + mat.setTextColor(colors.green) + mat.write(rchar()) + mat.setTextColor(colors.lime) + mat.setCursorPos(x, endp) + mat.write(rchar()) + col[1] = col[1] + 1 + col[2] = col[2] + 1 + + col[1] = wrap(col[1]) + col[2] = wrap(col[2]) + end + sleep(0.1) +end \ No newline at end of file diff --git a/src/bin/log.lua b/src/bin/log.lua new file mode 100644 index 0000000..0847e2b --- /dev/null +++ b/src/bin/log.lua @@ -0,0 +1,16 @@ +local args = table.concat({...}, " ") +local logtext +if args:match "old" then + logtext = potatOS.read "old.log" +else + logtext = potatOS.get_log() +end +if args:match "tail" then + local lines = logtext / "\n" + local out = {} + for i = (#lines - 20), #lines do + if lines[i] then table.insert(out, lines[i]) end + end + logtext = table.concat(out, "\n") +end +textutils.pagedPrint(logtext) \ No newline at end of file diff --git a/src/bin/lyr.lua b/src/bin/lyr.lua new file mode 100644 index 0000000..42b2719 --- /dev/null +++ b/src/bin/lyr.lua @@ -0,0 +1 @@ +print(string.format("Layers of virtualization >= %d", potatOS.layers())) \ No newline at end of file diff --git a/src/bin/maxim.lua b/src/bin/maxim.lua new file mode 100644 index 0000000..ddafb7d --- /dev/null +++ b/src/bin/maxim.lua @@ -0,0 +1 @@ +print(potatOS.maxim()) \ No newline at end of file diff --git a/src/bin/norris.lua b/src/bin/norris.lua new file mode 100644 index 0000000..881a8ab --- /dev/null +++ b/src/bin/norris.lua @@ -0,0 +1 @@ +print(string.reverse(potatOS.chuck_norris())) \ No newline at end of file diff --git a/src/bin/potatonet.lua b/src/bin/potatonet.lua new file mode 100644 index 0000000..1e55a93 --- /dev/null +++ b/src/bin/potatonet.lua @@ -0,0 +1 @@ +potatOS.potatoNET() \ No newline at end of file diff --git a/src/bin/regset.lua b/src/bin/regset.lua new file mode 100644 index 0000000..dc19b0d --- /dev/null +++ b/src/bin/regset.lua @@ -0,0 +1,9 @@ +-- Wait, why do we have this AND est? +local key, value = ... +key = key or "" +if not value then print(textutils.serialise(potatOS.registry.get(key))) +else + if value == "" then value = nil + elseif textutils.unserialise(value) ~= nil then value = textutils.unserialise(value) end + potatOS.registry.set(key, value) +end \ No newline at end of file diff --git a/src/bin/tau.lua b/src/bin/tau.lua new file mode 100644 index 0000000..891e921 --- /dev/null +++ b/src/bin/tau.lua @@ -0,0 +1 @@ +if potatOS.tau then textutils.pagedPrint(potatOS.tau) else error "PotatOS tau missing - is PotatOS correctly installed?" end \ No newline at end of file diff --git a/src/bin/threat_update.lua b/src/bin/threat_update.lua new file mode 100644 index 0000000..73a2f14 --- /dev/null +++ b/src/bin/threat_update.lua @@ -0,0 +1,37 @@ +local arg = ... +local update = potatOS.threat_update():gsub("\n$", "") +local bg = term.getBackgroundColor() +local fg = term.getTextColor() +term.setBackgroundColor(colors.black) +local bgcol = potatOS.map_color(update:match "threat level is ([^\n]*)\n") +local orig_black = {term.getPaletteColor(colors.black)} +local orig_white = {term.getPaletteColor(colors.white)} +term.setPaletteColor(colors.black, bgcol) +local r, g, b = bit.band(bit.brshift(bgcol, 16), 0xFF), bit.band(bit.brshift(bgcol, 8), 0xFF), bit.band(bgcol, 0xFF) +local avg_gray = (r + g + b) / 3 +term.setPaletteColor(colors.white, (r > 160 or g > 160 or b > 160) and 0 or 0xFFFFFF) +term.clear() +local fst = update:match "^([^\n]*)\n" +local snd = update:match "\n(.*)$" +local w, h = term.getSize() +local BORDER = 2 +term.setCursorPos(1, h) +local wi = window.create(term.current(), 1 + BORDER, 1 + BORDER, w - (2*BORDER), h - (2*BORDER)) +local old = term.redirect(wi) +term.setBackgroundColor(colors.black) +print(fst) +print() +print(snd) +print() +if arg == "headless" then + ccemux.echo "ready" + while true do coroutine.yield() end +else + print "Press a key to continue..." + os.pullEvent "char" + term.redirect(old) + term.setPaletteColor(colors.black, unpack(orig_black)) + term.setPaletteColor(colors.white, unpack(orig_white)) + term.setBackgroundColor(bg) + term.setTextColor(fg) +end \ No newline at end of file diff --git a/src/bin/uninstall.lua b/src/bin/uninstall.lua new file mode 100644 index 0000000..5e16392 --- /dev/null +++ b/src/bin/uninstall.lua @@ -0,0 +1,4 @@ +if potatOS.actually_really_uninstall then potatOS.actually_really_uninstall "76fde5717a89e332513d4f1e5b36f6cb" os.reboot() +else + potatOS.begin_uninstall_process() +end \ No newline at end of file diff --git a/src/bin/upd.lua b/src/bin/upd.lua new file mode 100644 index 0000000..4c9fbff --- /dev/null +++ b/src/bin/upd.lua @@ -0,0 +1 @@ +potatOS.update() \ No newline at end of file diff --git a/src/bin/very-uninstall.lua b/src/bin/very-uninstall.lua new file mode 100644 index 0000000..9b70b1e --- /dev/null +++ b/src/bin/very-uninstall.lua @@ -0,0 +1 @@ +shell.run 'loading' term.clear() term.setCursorPos(1, 1) print 'Actually, nope.' \ No newline at end of file diff --git a/src/bin/viewsource.lua b/src/bin/viewsource.lua new file mode 100644 index 0000000..980ed83 --- /dev/null +++ b/src/bin/viewsource.lua @@ -0,0 +1,49 @@ +local function try_files(lst) + for _, v in pairs(lst) do + local z = potatOS.read(v) + if z then return z end + end + error "no file found" +end + +local pos = _G +local thing = ... +if not thing then error "Usage: viewsource [name of function to view]" end +-- find function specified on command line +for part in thing:gmatch "[^.]+" do + pos = pos[part] + if not pos then error(thing .. " does not exist: " .. part) end +end + +local info = debug.getinfo(pos) +if not info.linedefined or not info.lastlinedefined or not info.source or info.lastlinedefined == -1 then error "Is this a Lua function?" end +local sourcen = info.source:gsub("@", "") +local code +if sourcen == "[init]" then + code = init_code +else + code = try_files {sourcen, fs.combine("lib", sourcen), fs.combine("bin", sourcen), fs.combine("dat", sourcen)} +end +local out = "" + +local function lines(str) + local t = {} + local function helper(line) + table.insert(t, line) + return "" + end + helper((str:gsub("(.-)\r?\n", helper))) + return t +end + +for ix, line in pairs(lines(code)) do + if ix >= info.linedefined and ix <= info.lastlinedefined then + out = out .. line .. "\n" + end +end +local filename = ".viewsource-" .. thing +local f = fs.open(filename, "w") +f.write(out) +f.close() +shell.run("edit", filename) +fs.delete(filename) \ No newline at end of file diff --git a/src/bin/wipe.lua b/src/bin/wipe.lua new file mode 100644 index 0000000..ab537aa --- /dev/null +++ b/src/bin/wipe.lua @@ -0,0 +1 @@ +print 'Foolish fool.' shell.run '/rom/programs/delete *' potatOS.update() \ No newline at end of file diff --git a/src/lib/cc_expect.lua b/src/lib/cc_expect.lua new file mode 100644 index 0000000..f4cade7 --- /dev/null +++ b/src/lib/cc_expect.lua @@ -0,0 +1,145 @@ +-- SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers +-- +-- SPDX-License-Identifier: MPL-2.0 + +--[[- The [`cc.expect`] library provides helper functions for verifying that +function arguments are well-formed and of the correct type. + +@module cc.expect +@since 1.84.0 +@changed 1.96.0 The module can now be called directly as a function, which wraps around `expect.expect`. +@usage Define a basic function and check it has the correct arguments. + + local expect = require "cc.expect" + local expect, field = expect.expect, expect.field + + local function add_person(name, info) + expect(1, name, "string") + expect(2, info, "table", "nil") + + if info then + print("Got age=", field(info, "age", "number")) + print("Got gender=", field(info, "gender", "string", "nil")) + end + end + + add_person("Anastazja") -- `info' is optional + add_person("Kion", { age = 23 }) -- `gender' is optional + add_person("Caoimhin", { age = 23, gender = true }) -- error! +]] + +local native_select, native_type = select, type + +local function get_type_names(...) + local types = table.pack(...) + for i = types.n, 1, -1 do + if types[i] == "nil" then table.remove(types, i) end + end + + if #types <= 1 then + return tostring(...) + else + return table.concat(types, ", ", 1, #types - 1) .. " or " .. types[#types] + end +end + + +local function get_display_type(value, t) + -- Lua is somewhat inconsistent in whether it obeys __name just for values which + -- have a per-instance metatable (so tables/userdata) or for everything. We follow + -- Cobalt and only read the metatable for tables/userdata. + if t ~= "table" and t ~= "userdata" then return t end + + local metatable = debug.getmetatable(value) + if not metatable then return t end + + local name = rawget(metatable, "__name") + if type(name) == "string" then return name else return t end +end + +--- Expect an argument to have a specific type. +-- +-- @tparam number index The 1-based argument index. +-- @param value The argument's value. +-- @tparam string ... The allowed types of the argument. +-- @return The given `value`. +-- @throws If the value is not one of the allowed types. +local function expect(index, value, ...) + local t = native_type(value) + for i = 1, native_select("#", ...) do + if t == native_select(i, ...) then return value end + end + + -- If we can determine the function name with a high level of confidence, try to include it. + local name + local ok, info = pcall(debug.getinfo, 3, "nS") + if ok and info.name and info.name ~= "" and info.what ~= "C" then name = info.name end + + t = get_display_type(value, t) + + local type_names = get_type_names(...) + if name then + error(("bad argument #%d to '%s' (%s expected, got %s)"):format(index, name, type_names, t), 3) + else + error(("bad argument #%d (%s expected, got %s)"):format(index, type_names, t), 3) + end +end + +--- Expect an field to have a specific type. +-- +-- @tparam table tbl The table to index. +-- @tparam string index The field name to check. +-- @tparam string ... The allowed types of the argument. +-- @return The contents of the given field. +-- @throws If the field is not one of the allowed types. +local function field(tbl, index, ...) + expect(1, tbl, "table") + expect(2, index, "string") + + local value = tbl[index] + local t = native_type(value) + for i = 1, native_select("#", ...) do + if t == native_select(i, ...) then return value end + end + + t = get_display_type(value, t) + + if value == nil then + error(("field '%s' missing from table"):format(index), 3) + else + error(("bad field '%s' (%s expected, got %s)"):format(index, get_type_names(...), t), 3) + end +end + +local function is_nan(num) + return num ~= num +end + +--- Expect a number to be within a specific range. +-- +-- @tparam number num The value to check. +-- @tparam number min The minimum value, if nil then `-math.huge` is used. +-- @tparam number max The maximum value, if nil then `math.huge` is used. +-- @return The given `value`. +-- @throws If the value is outside of the allowed range. +-- @since 1.96.0 +local function range(num, min, max) + expect(1, num, "number") + min = expect(2, min, "number", "nil") or -math.huge + max = expect(3, max, "number", "nil") or math.huge + if min > max then + error("min must be less than or equal to max)", 2) + end + + if is_nan(num) or num < min or num > max then + error(("number outside of range (expected %s to be within %s and %s)"):format(num, min, max), 3) + end + + return num +end + +return setmetatable({ + expect = expect, + field = field, + range = range, +}, { __call = function(_, ...) return expect(...) end }) diff --git a/src/lib/json.lua b/src/lib/json.lua index 75ed79c..3be871b 100644 --- a/src/lib/json.lua +++ b/src/lib/json.lua @@ -99,7 +99,7 @@ end local function encode_string(val) - return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' + return '"' .. val:gsub('[%z\1-\31\\"\127-\255]', escape_char) .. '"' end diff --git a/src/lib/metatable_improvements.lua b/src/lib/metatable_improvements.lua new file mode 100644 index 0000000..9d1fbd4 --- /dev/null +++ b/src/lib/metatable_improvements.lua @@ -0,0 +1,246 @@ + +-- Accesses the PotatOS Potatocloud(tm) Potatostore(tm). Used to implement Superglobals(tm) - like globals but on all computers. +-- To be honest I should swap this out for a self-hosted thing like Kinto. +--[[ +Fix for PS#4F329133 +JSONBin (https://jsonbin.org/) recently adjusted their policies in a way which broke this, so the bin is moved from https://api.jsonbin.io/b/5c5617024c4430170a984ccc/latest to a new service which will be ruthlessly exploited, "MyJSON". + +Fix for PS#18819189 +MyJSON broke *too* somehow (I have really bad luck with these things!) so move from https://api.myjson.com/bins/150r92 to "JSONBin". + +Fix for PS#8C4CB942 +The other JSONBin thing broke too so just implement it in RSAPI +]] + +return function(add_log, report_incident) + function fetch(u, ...) + if not http then error "No HTTP access" end + local h,e = http.get(u, ...) + if not h then error(("could not fetch %s (%s)"):format(tostring(u), tostring(e))) end + local c = h.readAll() + h.close() + return c + end + + local bin_URL = "https://r.osmarks.net/superglobals/" + local bin = {} + local localbin = {} + + function bin.get(k) + if localbin[k] then + return localbin[k] + else + local ok, err = pcall(function() + local r = fetch(bin_URL .. textutils.urlEncode(tostring(k)), nil, true) + local ok, err = pcall(json.decode, r) + if not ok then return r end + return err + end) + if not ok then add_log("superglobals fetch failed %s", tostring(err)) return nil end + return err + end + end + + function bin.set(k, v) + local ok, err = pcall(function() + b[k] = v + local h, err = http.post(bin_URL .. textutils.urlEncode(tostring(k)), json.encode(v), nil, true) + if not h then error(err) end + end) + if not ok then localbin[k] = v add_log("superglobals set failed %s", tostring(err)) end + end + + local bin_mt = { + __index = function(_, k) return bin.get(k) end, + __newindex = function(_, k, v) return bin.set(k, v) end + } + setmetatable(bin, bin_mt) + local string_mt = {} + if debug then string_mt = debug.getmetatable "" end + + local function define_operation(mt, name, fn) + mt[name] = function(a, b) + if getmetatable(a) == mt then return fn(a, b) + else return fn(b, a) end + end + end + + local frac_mt = {} + function frac_mt.__tostring(x) + return ("[Fraction] %s/%s"):format(textutils.serialise(x.numerator), textutils.serialise(x.denominator)) + end + define_operation(frac_mt, "__mul", function (a, b) + return (a.numerator * b) / a.denominator + end) + + -- Add exciting random stuff to the string metatable. + -- Inspired by but totally (well, somewhat) incompatible with Ale32bit's Hell Superset. + function string_mt.__index(s, k) + if type(k) == "number" then + local c = string.sub(s, k, k) + if c == "" then return nil else return c end + end + return _ENV.string[k] or bin.get(k) + end + function string_mt.__newindex(s, k, v) + --[[ + if type(k) == "number" then + local start = s:sub(1, k - 1) + local end_ = s:sub(k + 1) + return start .. v .. end_ + end + ]] + return bin.set(k, v) + end + function string_mt.__add(lhs, rhs) + return tostring(lhs) .. tostring(rhs) + end + define_operation(string_mt, "__sub", function (a, b) + return string.gsub(a, b, "") + end) + function string_mt.__unm(a) + return string.reverse(a) + end + -- http://lua-users.org/wiki/SplitJoin + function string.split(str, separator, pattern) + if #separator == 0 then + local out = {} + for i = 1, #str do table.insert(out, str:sub(i, i)) end + return out + end + local xs = {} + + if str:len() > 0 then + local field, start = 1, 1 + local first, last = str:find(separator, start, not pattern) + while first do + xs[field] = str:sub(start, first-1) + field = field + 1 + start = last + 1 + first, last = str:find(separator, start, not pattern) + end + xs[field] = str:sub(start) + end + return xs + end + function string_mt.__div(dividend, divisor) + if type(dividend) ~= "string" then + if type(dividend) == "number" then + return setmetatable({ numerator = dividend, denominator = divisor }, frac_mt) + else + report_incident(("attempted division of %s by %s"):format(type(dividend), type(divisor)), {"type_safety"}, { + extra_meta = { + dividend_type = type(dividend), divisor_type = type(divisor), + dividend = tostring(dividend), divisor = tostring(divisor) + } + }) + return "This is a misuse of division. This incident has been reported." + end + end + if type(divisor) == "string" then return string.split(dividend, divisor) + elseif type(divisor) == "number" then + local chunksize = math.ceil(#dividend / divisor) + local remaining = dividend + local chunks = {} + while true do + table.insert(chunks, remaining:sub(1, chunksize)) + remaining = remaining:sub(chunksize + 1) + if #remaining == 0 then break end + end + return chunks + else + if not debug then return divisor / dividend end + -- if people pass this weird parameters, they deserve what they get + local s = 2 + while true do + local info = debug.getinfo(s) + if not info then return -dividend / "" end + if info.short_src ~= "[C]" then + local ok, res = pcall(string.dump, info.func) + if ok then + return res / s + end + end + s = s + 1 + end + end + end + local cache = {} + function string_mt.__call(s, ...) + if cache[s] then return cache[s](...) + else + local f, err = load(s) + if err then error(err) end + cache[s] = f + return f(...) + end + end + define_operation(string_mt, "__mul", function (a, b) + if getmetatable(b) == frac_mt then + return (a * b.numerator) / b.denominator + end + if type(b) == "number" then + return string.rep(a, b) + elseif type(b) == "table" then + local z = {} + for _, v in pairs(b) do + table.insert(z, tostring(v)) + end + return table.concat(z, a) + elseif type(b) == "function" then + local out = {} + for i = 1, #a do + table.insert(out, b(a:sub(i, i))) + end + return table.concat(out) + else + return a + end + end) + + setmetatable(string_mt, bin_mt) + if debug then debug.setmetatable(nil, bin_mt) end + + -- Similar stuff for functions. + local func_funcs = {} + local func_mt = {__index=func_funcs} + if debug then debug.setmetatable(function() end, func_mt) end + function func_mt.__sub(lhs, rhs) + return function(...) return lhs(rhs(...)) end + end + function func_mt.__add(lhs, rhs) + return function(...) return rhs(lhs(...)) end + end + function func_mt.__concat(lhs, rhs) + return function(...) + return lhs(...), rhs(...), nil -- limit to two return values + end + end + function func_mt.__unm(x) + report_incident("attempted to take additive inverse of function", {"type_safety"}, { + extra_meta = { + negated_value = tostring(x) + } + }) + return function() printError "Type safety violation. This incident has been reported." end + end + function func_funcs.dump(x) return string.dump(x) end + function func_funcs.info(x) return debug.getinfo(x) end + function func_funcs.address(x) return (string.match(tostring(x), "%w+$")) end + + -- Similar stuff for numbers too! NOBODY CAN ESCAPE! + -- TODO: implement alternative mathematics. + local num_funcs = {} + local num_mt = {__index=num_funcs} + num_mt.__call = function(x, ...) + local out = x + for _, y in pairs {...} do + out = out + y + end + return out + end + if debug then debug.setmetatable(0, num_mt) end + function num_funcs.tostring(x) return tostring(x) end + function num_funcs.isNaN(x) return x ~= x end + function num_funcs.isInf(x) return math.abs(x) == math.huge end +end \ No newline at end of file diff --git a/src/lib/sandboxlib.lua b/src/lib/sandboxlib.lua new file mode 100644 index 0000000..b6e0922 --- /dev/null +++ b/src/lib/sandboxlib.lua @@ -0,0 +1,54 @@ +local sandboxlib = {} + +local processhasgrant = process.has_grant +local processrestriction = process.restriction +local pairs = pairs +local setmetatable = setmetatable +local error = error +local tostring = tostring + +function sandboxlib.create_sentinel(name) + return {name} +end + +function sandboxlib.dispatch_if_restricted(rkey, original, restricted) + local out = {} + for k, v in pairs(original) do + out[k] = function(...) + if processrestriction(rkey) then + if not restricted[k] then error("internal error: missing " .. tostring(k)) end + return restricted[k](...) + end + return v(...) + end + end + return out +end + +function sandboxlib.allow_whitelisted(rkey, original, whitelist, fallback) + local fallback = fallback or {} + local whitelist_lookup = {} + for _, v in pairs(whitelist) do + whitelist_lookup[v] = true + end + local out = {} + for k, v in pairs(original) do + if whitelist_lookup[k] then + out[k] = v + else + out[k] = function(...) + if processrestriction(rkey) then + if not fallback[k] then + error("Security violation: " .. k) + else + return fallback[k](...) + end + end + return v(...) + end + end + end + return out +end + +return sandboxlib \ No newline at end of file diff --git a/src/lib/sha256.lua b/src/lib/sha256.lua index 7d3844e..6fb4454 100644 --- a/src/lib/sha256.lua +++ b/src/lib/sha256.lua @@ -139,42 +139,11 @@ local function digest(data) data = preprocess(data) local C = {upack(H)} - for i = 1, #data do C = digestblock(data[i], C) end + local dummy = ("%07x"):format(math.random(0, 0xFFFFFFF)) + for i = 1, #data do C = digestblock(data[i], C) os.queueEvent(dummy) coroutine.yield(dummy) end return toBytes(C, 8) end -local function hmac(data, key) - local data = type(data) == "table" and {upack(data)} or to_bytes(tostring(data)) - local key = type(key) == "table" and {upack(key)} or to_bytes(tostring(key)) - - local blocksize = 64 - - key = #key > blocksize and digest(key) or key - - local ipad = {} - local opad = {} - local padded_key = {} - - for i = 1, blocksize do - ipad[i] = bxor(0x36, key[i] or 0) - opad[i] = bxor(0x5C, key[i] or 0) - end - - for i = 1, #data do - ipad[blocksize+i] = data[i] - end - - ipad = digest(ipad) - - for i = 1, blocksize do - padded_key[i] = opad[i] - padded_key[blocksize+i] = ipad[i] - end - - return digest(padded_key) -end - return { - hmac = hmac, digest = digest } \ No newline at end of file diff --git a/src/lib/window_buf.lua b/src/lib/window_buf.lua new file mode 100644 index 0000000..e443757 --- /dev/null +++ b/src/lib/window_buf.lua @@ -0,0 +1,590 @@ +-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe +-- +-- SPDX-License-Identifier: LicenseRef-CCPL + +--[[- A [terminal redirect][`term.Redirect`] occupying a smaller area of an +existing terminal. This allows for easy definition of spaces within the display +that can be written/drawn to, then later redrawn/repositioned/etc as need +be. The API itself contains only one function, [`window.create`], which returns +the windows themselves. + +Windows are considered terminal objects - as such, they have access to nearly +all the commands in the term API (plus a few extras of their own, listed within +said API) and are valid targets to redirect to. + +Each window has a "parent" terminal object, which can be the computer's own +display, a monitor, another window or even other, user-defined terminal +objects. Whenever a window is rendered to, the actual screen-writing is +performed via that parent (or, if that has one too, then that parent, and so +forth). Bear in mind that the cursor of a window's parent will hence be moved +around etc when writing a given child window. + +Windows retain a memory of everything rendered "through" them (hence acting as +display buffers), and if the parent's display is wiped, the window's content can +be easily redrawn later. A window may also be flagged as invisible, preventing +any changes to it from being rendered until it's flagged as visible once more. + +A parent terminal object may have multiple children assigned to it, and windows +may overlap. For example, the Multishell system functions by assigning each tab +a window covering the screen, each using the starting terminal display as its +parent, and only one of which is visible at a time. + +@module window +@since 1.6 +]] + +local expect = require "cc_expect".expect + +local tHex = { + [colors.white] = "0", + [colors.orange] = "1", + [colors.magenta] = "2", + [colors.lightBlue] = "3", + [colors.yellow] = "4", + [colors.lime] = "5", + [colors.pink] = "6", + [colors.gray] = "7", + [colors.lightGray] = "8", + [colors.cyan] = "9", + [colors.purple] = "a", + [colors.blue] = "b", + [colors.brown] = "c", + [colors.green] = "d", + [colors.red] = "e", + [colors.black] = "f", +} + +local type = type +local string_rep = string.rep +local string_sub = string.sub + +--[[- Returns a terminal object that is a space within the specified parent +terminal object. This can then be used (or even redirected to) in the same +manner as eg a wrapped monitor. Refer to [the term API][`term`] for a list of +functions available to it. + +[`term`] itself may not be passed as the parent, though [`term.native`] is +acceptable. Generally, [`term.current`] or a wrapped monitor will be most +suitable, though windows may even have other windows assigned as their +parents. + +@tparam term.Redirect parent The parent terminal redirect to draw to. +@tparam number nX The x coordinate this window is drawn at in the parent terminal +@tparam number nY The y coordinate this window is drawn at in the parent terminal +@tparam number nWidth The width of this window +@tparam number nHeight The height of this window +@tparam[opt] boolean bStartVisible Whether this window is visible by +default. Defaults to `true`. +@treturn Window The constructed window +@since 1.6 +@usage Create a smaller window, fill it red and write some text to it. + + local my_window = window.create(term.current(), 1, 1, 20, 5) + my_window.setBackgroundColour(colours.red) + my_window.setTextColour(colours.white) + my_window.clear() + my_window.write("Testing my window!") + +@usage Create a smaller window and redirect to it. + + local my_window = window.create(term.current(), 1, 1, 25, 5) + term.redirect(my_window) + print("Writing some long text which will wrap around and show the bounds of this window.") + +]] + +local seq_counter = 0 + +local function create_window(parent) + expect(1, parent, "table") + local nX, nY = 1, 1 + local nWidth, nHeight = parent.getSize() + + if parent == term then + error("term is not a recommended window parent, try term.current() instead", 2) + end + + local sEmptySpaceLine + local tEmptyColorLines = {} + local function createEmptyLines(nWidth) + sEmptySpaceLine = string_rep(" ", nWidth) + for n = 0, 15 do + local nColor = 2 ^ n + local sHex = tHex[nColor] + tEmptyColorLines[nColor] = string_rep(sHex, nWidth) + end + end + + createEmptyLines(nWidth) + + -- Setup + local bVisible = true + local nCursorX = 1 + local nCursorY = 1 + local bCursorBlink = false + local nTextColor = colors.white + local nBackgroundColor = colors.black + local tLines = {} + local wseq_counter + + local function raw_resize(new_width, new_height) + if new_width and new_height then + local tNewLines = {} + createEmptyLines(new_width) + local sEmptyText = sEmptySpaceLine + local sEmptyTextColor = tEmptyColorLines[nTextColor] + local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor] + for y = 1, new_height do + if y > nHeight then + tNewLines[y] = { sEmptyText, sEmptyTextColor, sEmptyBackgroundColor } + else + local tOldLine = tLines[y] + if new_width == nWidth then + tNewLines[y] = tOldLine + elseif new_width < nWidth then + tNewLines[y] = { + string_sub(tOldLine[1], 1, new_width), + string_sub(tOldLine[2], 1, new_width), + string_sub(tOldLine[3], 1, new_width), + } + else + tNewLines[y] = { + tOldLine[1] .. string_sub(sEmptyText, nWidth + 1, new_width), + tOldLine[2] .. string_sub(sEmptyTextColor, nWidth + 1, new_width), + tOldLine[3] .. string_sub(sEmptyBackgroundColor, nWidth + 1, new_width), + } + end + end + end + nWidth = new_width + nHeight = new_height + tLines = tNewLines + end + end + + local function increment_seq() + seq_counter = seq_counter + 1 + wseq_counter = seq_counter + end + + increment_seq() + + do + local sEmptyText = sEmptySpaceLine + local sEmptyTextColor = tEmptyColorLines[nTextColor] + local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor] + for y = 1, nHeight do + tLines[y] = { sEmptyText, sEmptyTextColor, sEmptyBackgroundColor } + end + end + + -- Helper functions + local function updateCursorPos() + if nCursorX >= 1 and nCursorY >= 1 and + nCursorX <= nWidth and nCursorY <= nHeight then + parent.setCursorPos(nX + nCursorX - 1, nY + nCursorY - 1) + else + parent.setCursorPos(0, 0) + end + end + + local function updateCursorBlink() + parent.setCursorBlink(bCursorBlink) + end + + local function updateCursorColor() + parent.setTextColor(nTextColor) + end + + local function redrawLine(n) + increment_seq() + local tLine = tLines[n] + parent.setCursorPos(nX, nY + n - 1) + parent.blit(tLine[1], tLine[2], tLine[3]) + end + + local function redraw() + for n = 1, nHeight do + redrawLine(n) + end + end + + local function internalBlit(sText, sTextColor, sBackgroundColor) + local nStart = nCursorX + local nEnd = nStart + #sText - 1 + if nCursorY >= 1 and nCursorY <= nHeight then + if nStart <= nWidth and nEnd >= 1 then + -- Modify line + local tLine = tLines[nCursorY] + if nStart == 1 and nEnd == nWidth then + tLine[1] = sText + tLine[2] = sTextColor + tLine[3] = sBackgroundColor + else + local sClippedText, sClippedTextColor, sClippedBackgroundColor + if nStart < 1 then + local nClipStart = 1 - nStart + 1 + local nClipEnd = nWidth - nStart + 1 + sClippedText = string_sub(sText, nClipStart, nClipEnd) + sClippedTextColor = string_sub(sTextColor, nClipStart, nClipEnd) + sClippedBackgroundColor = string_sub(sBackgroundColor, nClipStart, nClipEnd) + elseif nEnd > nWidth then + local nClipEnd = nWidth - nStart + 1 + sClippedText = string_sub(sText, 1, nClipEnd) + sClippedTextColor = string_sub(sTextColor, 1, nClipEnd) + sClippedBackgroundColor = string_sub(sBackgroundColor, 1, nClipEnd) + else + sClippedText = sText + sClippedTextColor = sTextColor + sClippedBackgroundColor = sBackgroundColor + end + + local sOldText = tLine[1] + local sOldTextColor = tLine[2] + local sOldBackgroundColor = tLine[3] + local sNewText, sNewTextColor, sNewBackgroundColor + if nStart > 1 then + local nOldEnd = nStart - 1 + sNewText = string_sub(sOldText, 1, nOldEnd) .. sClippedText + sNewTextColor = string_sub(sOldTextColor, 1, nOldEnd) .. sClippedTextColor + sNewBackgroundColor = string_sub(sOldBackgroundColor, 1, nOldEnd) .. sClippedBackgroundColor + else + sNewText = sClippedText + sNewTextColor = sClippedTextColor + sNewBackgroundColor = sClippedBackgroundColor + end + if nEnd < nWidth then + local nOldStart = nEnd + 1 + sNewText = sNewText .. string_sub(sOldText, nOldStart, nWidth) + sNewTextColor = sNewTextColor .. string_sub(sOldTextColor, nOldStart, nWidth) + sNewBackgroundColor = sNewBackgroundColor .. string_sub(sOldBackgroundColor, nOldStart, nWidth) + end + + tLine[1] = sNewText + tLine[2] = sNewTextColor + tLine[3] = sNewBackgroundColor + end + + -- Redraw line + if bVisible then + redrawLine(nCursorY) + end + end + end + + -- Move and redraw cursor + nCursorX = nEnd + 1 + if bVisible then + updateCursorColor() + updateCursorPos() + end + end + + --- The window object. Refer to the [module's documentation][`window`] for + -- a full description. + -- + -- @type Window + -- @see term.Redirect + local window = {} + + function window.write(sText) + sText = tostring(sText) + internalBlit(sText, string_rep(tHex[nTextColor], #sText), string_rep(tHex[nBackgroundColor], #sText)) + end + + function window.blit(sText, sTextColor, sBackgroundColor) + if type(sText) ~= "string" then expect(1, sText, "string") end + if type(sTextColor) ~= "string" then expect(2, sTextColor, "string") end + if type(sBackgroundColor) ~= "string" then expect(3, sBackgroundColor, "string") end + if #sTextColor ~= #sText or #sBackgroundColor ~= #sText then + error("Arguments must be the same length", 2) + end + sTextColor = sTextColor:lower() + sBackgroundColor = sBackgroundColor:lower() + internalBlit(sText, sTextColor, sBackgroundColor) + end + + function window.clear() + local sEmptyText = sEmptySpaceLine + local sEmptyTextColor = tEmptyColorLines[nTextColor] + local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor] + for y = 1, nHeight do + local line = tLines[y] + line[1] = sEmptyText + line[2] = sEmptyTextColor + line[3] = sEmptyBackgroundColor + end + if bVisible then + redraw() + updateCursorColor() + updateCursorPos() + end + end + + function window.clearLine() + if nCursorY >= 1 and nCursorY <= nHeight then + local line = tLines[nCursorY] + line[1] = sEmptySpaceLine + line[2] = tEmptyColorLines[nTextColor] + line[3] = tEmptyColorLines[nBackgroundColor] + if bVisible then + redrawLine(nCursorY) + updateCursorColor() + updateCursorPos() + end + end + end + + function window.getCursorPos() + return nCursorX, nCursorY + end + + function window.setCursorPos(x, y) + if type(x) ~= "number" then expect(1, x, "number") end + if type(y) ~= "number" then expect(2, y, "number") end + nCursorX = math.floor(x) + nCursorY = math.floor(y) + if bVisible then + updateCursorPos() + end + end + + function window.setCursorBlink(blink) + if type(blink) ~= "boolean" then expect(1, blink, "boolean") end + bCursorBlink = blink + if bVisible then + updateCursorBlink() + end + end + + function window.getCursorBlink() + return bCursorBlink + end + + local function isColor() + return parent.isColor() + end + + function window.isColor() + return isColor() + end + + function window.isColour() + return isColor() + end + + local function setTextColor(color) + if type(color) ~= "number" then expect(1, color, "number") end + if tHex[color] == nil then + error("Invalid color (got " .. color .. ")" , 2) + end + + nTextColor = color + if bVisible then + updateCursorColor() + end + end + + window.setTextColor = setTextColor + window.setTextColour = setTextColor + + function window.setPaletteColour(colour, r, g, b) + return parent.setPaletteColour(colour, r, g, b) + end + + window.setPaletteColor = window.setPaletteColour + + function window.getPaletteColour(colour) + return parent.getPaletteColour(colour) + end + + window.getPaletteColor = window.getPaletteColour + + local function setBackgroundColor(color) + if type(color) ~= "number" then expect(1, color, "number") end + if tHex[color] == nil then + error("Invalid color (got " .. color .. ")", 2) + end + nBackgroundColor = color + end + + window.setBackgroundColor = setBackgroundColor + window.setBackgroundColour = setBackgroundColor + + function window.getSize() + return nWidth, nHeight + end + + function window.scroll(n) + if type(n) ~= "number" then expect(1, n, "number") end + if n ~= 0 then + local tNewLines = {} + local sEmptyText = sEmptySpaceLine + local sEmptyTextColor = tEmptyColorLines[nTextColor] + local sEmptyBackgroundColor = tEmptyColorLines[nBackgroundColor] + for newY = 1, nHeight do + local y = newY + n + if y >= 1 and y <= nHeight then + tNewLines[newY] = tLines[y] + else + tNewLines[newY] = { sEmptyText, sEmptyTextColor, sEmptyBackgroundColor } + end + end + tLines = tNewLines + if bVisible then + redraw() + updateCursorColor() + updateCursorPos() + end + end + end + + function window.getTextColor() + return nTextColor + end + + function window.getTextColour() + return nTextColor + end + + function window.getBackgroundColor() + return nBackgroundColor + end + + function window.getBackgroundColour() + return nBackgroundColor + end + + --- Get the buffered contents of a line in this window. + -- + -- @tparam number y The y position of the line to get. + -- @treturn string The textual content of this line. + -- @treturn string The text colours of this line, suitable for use with [`term.blit`]. + -- @treturn string The background colours of this line, suitable for use with [`term.blit`]. + -- @throws If `y` is not between 1 and this window's height. + -- @since 1.84.0 + function window.getLine(y) + if type(y) ~= "number" then expect(1, y, "number") end + + if y < 1 or y > nHeight then + error("Line is out of range.", 2) + end + + local line = tLines[y] + return line[1], line[2], line[3] + end + + -- Other functions + + --- Set whether this window is visible. Invisible windows will not be drawn + -- to the screen until they are made visible again. + -- + -- Making an invisible window visible will immediately draw it. + -- + -- @tparam boolean visible Whether this window is visible. + function window.setVisible(visible) + if type(visible) ~= "boolean" then expect(1, visible, "boolean") end + if bVisible ~= visible then + bVisible = visible + if bVisible then + window.redraw() + end + end + end + + --- Get whether this window is visible. Invisible windows will not be + -- drawn to the screen until they are made visible again. + -- + -- @treturn boolean Whether this window is visible. + -- @see Window:setVisible + -- @since 1.94.0 + function window.isVisible() + return bVisible + end + --- Draw this window. This does nothing if the window is not visible. + -- + -- @see Window:setVisible + function window.redraw() + if bVisible then + redraw() + updateCursorBlink() + updateCursorColor() + updateCursorPos() + end + end + + --- Set the current terminal's cursor to where this window's cursor is. This + -- does nothing if the window is not visible. + function window.restoreCursor() + if bVisible then + updateCursorBlink() + updateCursorColor() + updateCursorPos() + end + end + + --- Get the position of the top left corner of this window. + -- + -- @treturn number The x position of this window. + -- @treturn number The y position of this window. + function window.getPosition() + return nX, nY + end + + function window.seq_counter() + return wseq_counter + end + + --- Reposition or resize the given window. + -- + -- This function also accepts arguments to change the size of this window. + -- It is recommended that you fire a `term_resize` event after changing a + -- window's, to allow programs to adjust their sizing. + -- + -- @tparam number new_x The new x position of this window. + -- @tparam number new_y The new y position of this window. + -- @tparam[opt] number new_width The new width of this window. + -- @tparam number new_height The new height of this window. + -- @tparam[opt] term.Redirect new_parent The new redirect object this + -- window should draw to. + -- @changed 1.85.0 Add `new_parent` parameter. + function window.reposition(new_x, new_y, new_width, new_height, new_parent) + if type(new_x) ~= "number" then expect(1, new_x, "number") end + if type(new_y) ~= "number" then expect(2, new_y, "number") end + if new_width ~= nil or new_height ~= nil then + expect(3, new_width, "number") + expect(4, new_height, "number") + end + if new_parent ~= nil and type(new_parent) ~= "table" then expect(5, new_parent, "table") end + + nX = new_x + nY = new_y + + if new_parent then parent = new_parent end + + raw_resize(new_width, new_height) + if bVisible then + window.redraw() + end + end + + function window.check_backing() + local w, h = parent.getSize() + if w ~= nWidth or h ~=- nHeight then + raw_resize(w, h) + end + end + + function window.backing() + return parent + end + + if bVisible then + window.redraw() + end + window.is_framebuffer = true + + return window +end + +return create_window \ No newline at end of file diff --git a/src/lib/yafss.lua b/src/lib/yafss.lua index 6fda71a..519a4ca 100644 --- a/src/lib/yafss.lua +++ b/src/lib/yafss.lua @@ -2,7 +2,7 @@ local function copy(tabl) local new = {} for k, v in pairs(tabl) do - if type(v) == "table" and tabl ~= v then + if type(v) == "table" and tabl ~= v and v.COPY_EXACT == nil then new[k] = copy(v) else new[k] = v @@ -181,6 +181,7 @@ local this_level_env = _G -- Create a modified FS table which confines you to root and has some extra read-only pseudofiles. local function create_FS(root, overlay) + local fs = fs local mappings = make_mappings(root) local vfstree = { @@ -216,7 +217,7 @@ local function create_FS(root, overlay) end end - local new = copy_some_keys {"getDir", "getName", "combine"} (fs) + local new = copy_some_keys {"getDir", "getName", "combine", "complete"} (fs) function new.isReadOnly(path) return path_in_overlay(new_overlay, path) or starts_with(canonicalize(path), "rom") @@ -341,7 +342,7 @@ local allowed_APIs = { "http", "pairs", "ipairs", - -- getfenv, getfenv are modified to prevent sandbox escapes and defined in make_environment + -- getfenv, setfenv are modified to prevent sandbox escapes and defined in make_environment "peripheral", "table", "string", @@ -376,6 +377,26 @@ local allowed_APIs = { "~expect", "__inext", "periphemu", + "fs", + "debug", + "write", + "print", + "printError", + "read", + "colors", + "io", + "parallel", + "settings", + "vector", + "colours", + "keys", + "disk", + "help", + "paintutils", + "rednet", + "textutils", + "commands", + "window" } local gf, sf = getfenv, setfenv @@ -383,16 +404,14 @@ local gf, sf = getfenv, setfenv -- Takes the root directory to allow access to, -- a map of paths to either strings containing their contents or functions returning them -- and a table of extra APIs and partial overrides for existing APIs -local function make_environment(root_directory, overlay, API_overrides) +local function make_environment(API_overrides, current_process) + local env_host = string.format("YAFSS on %s", _HOST) local environment = copy_some_keys(allowed_APIs)(_G) - - environment.fs = create_FS(root_directory, overlay) - -- if function is not from within the VM, return env from within sandbox function environment.getfenv(arg) local env if type(arg) == "number" then return gf() end - if not env or type(env._HOST) ~= "string" or not string.match(env._HOST, "YAFSS") then + if not env or type(env._HOST) ~= "string" or not env._HOST == env_host then return gf() else return env @@ -405,37 +424,28 @@ Allowing `setfenv` to operate on any function meant that privileged code could i ]] function environment.setfenv(fn, env) local nenv = gf(fn) - if not nenv or type(nenv._HOST) ~= "string" or not string.match(nenv._HOST, "YAFSS") then + if not nenv or type(nenv._HOST) ~= "string" or not nenv._HOST == env_host then return false end return sf(fn, env) end + local load = load function environment.load(code, file, mode, env) - return load(code, file or "@", mode or "t", env or environment) - end - - if debug then - environment.debug = copy_some_keys { - "getmetatable", - "setmetatable", - "traceback", - "getinfo", - "getregistry" - }(debug) + return load(code, file, mode, env or environment) end environment._G = environment environment._ENV = environment - environment._HOST = string.format("YAFSS on %s", _HOST) + environment._HOST = env_host function environment.os.shutdown() - os.queueEvent("power_state", "shutdown") + process.IPC(current_process, "power_state", "shutdown") while true do coroutine.yield() end end function environment.os.reboot() - os.queueEvent("power_state", "reboot") + process.IPC(current_process, "power_state", "reboot") while true do coroutine.yield() end end @@ -444,39 +454,35 @@ Allowing `setfenv` to operate on any function meant that privileged code could i return environment end -local function run(root_directory, overlay, API_overrides, init) - if type(init) == "table" and init.URL then init = fetch(init.URL) end - init = init or fetch "https://pastebin.com/raw/wKdMTPwQ" - +local function run(API_overrides, init, logger) + local current_process = process.running.ID local running = true while running do parallel.waitForAny(function() - local env = make_environment(root_directory, overlay, API_overrides) + local env = make_environment(API_overrides, current_process) env.init_code = init local out, err = load(init, "@[init]", "t", env) if not out then error(err) end - local ok, err = pcall(out) - if not ok then printError(err) end + local ok, err = pcall(out) + if not ok then logger("sandbox errored: %s", err) end end, function() while true do - local event, state = coroutine.yield "power_state" - if event == "power_state" then -- coroutine.yield behaves weirdly with terminate - if process then - local this_process = process.running.ID - for _, p in pairs(process.list()) do - if p.parent and p.parent.ID == this_process then - process.signal(p.ID, process.signals.KILL) - end + local event, source, ty, spec = coroutine.yield "ipc" + if event == "ipc" and ty == "power_state" then -- coroutine.yield behaves weirdly with terminate + for _, p in pairs(process.list()) do + if process.is_ancestor(p, current_process) and p.ID ~= current_process and not p.thread then + process.signal(p.ID, process.signals.KILL) end end - if state == "shutdown" then running = false return - elseif state == "reboot" then return end + if spec == "shutdown" then running = false return + elseif spec == "reboot" then return end end end end) + sleep() end end -return run \ No newline at end of file +return { run = run, create_FS = create_FS } \ No newline at end of file diff --git a/src/main.lua b/src/main.lua index f5bdfb0..6d0bacc 100644 --- a/src/main.lua +++ b/src/main.lua @@ -1,5 +1,5 @@ --[[ -PotatOS Hypercycle - OS/Conveniently Self-Propagating System/Sandbox/Compilation of Useless Programs +PotatOS Epenthesis - OS/Conveniently Self-Propagating System/Sandbox/Compilation of Useless Programs Best viewed in Internet Explorer 6.00000000000004 running on a Difference Engine emulated under MacOS 7 on a Pentium 3. Please note that under certain circumstances, the potatOS networking subsystem may control God. @@ -14,6 +14,17 @@ Did you know? Because intellectual property law is weird, and any digitally stor This license also extends to other PotatOS components or bundled software owned by me. ]] +local w, h = term.getSize() +local win = window.create(term.native(), 1, 1, w, h, true) +term.redirect(win) +local function capture_screen() + win.setVisible(true) + win.redraw() +end +local function uncapture_screen() + win.setVisible(false) + if process and process.IPC then pcall(process.IPC, "termd", "redraw_native") end +end term.clear() term.setCursorPos(1, 1) if term.isColor() then @@ -49,9 +60,6 @@ local SPF = { server = nil } -if _G.shell and not _ENV.shell then _ENV.shell = _G.shell end -if _ENV.shell and not _G.shell then _G.shell = _ENV.shell end - os.pullEvent = coroutine.yield local function get_registry(name) @@ -92,6 +100,7 @@ local function rot13(s) return table.concat(out) end +local debugtraceback = debug and debug.traceback local logfile = fs.open("latest.log", "a") local function add_log(...) local args = {...} @@ -99,6 +108,9 @@ local function add_log(...) local text = string.format(unpack(args)) if ccemux and ccemux.echo then ccemux.echo(text) end local line = ("[%s] <%s> %s"):format(os.date "!%X %d/%m/%Y", (process and process.running and (process.running.name or tostring(process.running.ID))) or "[n/a]", text) + if get_setting "potatOS.traceback_logger" then + line = line .. "\n" .. debugtraceback() + end logfile.writeLine(line) logfile.flush() -- this should probably be infrequent enough that the performance impact is not very bad -- primitive log rotation - logs should only be ~64KiB in total, which seems reasonable @@ -121,7 +133,7 @@ local function get_log() return d end -if SPF.server then add_log("SPF initialized: server %s", SPF.server) end +if SPF.server then add_log("SPF initialized: server %s", SPF.server or "none") end -- print things to console for some reason? but only in CCEmuX -- this ~~is being removed~~ is now gone but I am leaving this comment here for some reason @@ -214,13 +226,6 @@ local function copy(tabl) return new end --- https://pastebin.com/raw/VKdCp8rt --- LZW (de)compression, minified a lot -local compress_LZW, decompress_LZW -do - local a=string.char;local type=type;local select=select;local b=string.sub;local c=table.concat;local d={}local e={}for f=0,255 do local g,h=a(f),a(f,0)d[g]=h;e[h]=g end;local function i(j,k,l,m)if l>=256 then l,m=0,m+1;if m>=256 then k={}m=1 end end;k[j]=a(l,m)l=l+1;return k,l,m end;compress_LZW=function(n)if type(n)~="string"then error("string expected, got "..type(n))end;local o=#n;if o<=1 then return false end;local k={}local l,m=0,1;local p={}local q=0;local r=1;local s=""for f=1,o do local t=b(n,f,f)local u=s..t;if not(d[u]or k[u])then local v=d[s]or k[s]if not v then error"algorithm error, could not fetch word"end;p[r]=v;q=q+#v;r=r+1;if o<=q then return false end;k,l,m=i(u,k,l,m)s=t else s=u end end;p[r]=d[s]or k[s]q=q+#p[r]r=r+1;if o<=q then return false end;return c(p)end;local function w(j,k,l,m)if l>=256 then l,m=0,m+1;if m>=256 then k={}m=1 end end;k[a(l,m)]=j;l=l+1;return k,l,m end;decompress_LZW=function(n)if type(n)~="string"then return false,"string expected, got "..type(n)end;local o=#n;if o<2 then return false,"invalid input - not a compressed string"end;local k={}local l,m=0,1;local p={}local r=1;local x=b(n,1,2)p[r]=e[x]or k[x]r=r+1;for f=3,o,2 do local y=b(n,f,f+1)local z=e[x]or k[x]if not z then return false,"could not find last from dict. Invalid input?"end;local A=e[y]or k[y]if A then p[r]=A;r=r+1;k,l,m=w(z..b(A,1,1),k,l,m)else local B=z..b(z,1,1)p[r]=B;r=r+1;k,l,m=w(B,k,l,m)end;x=y end;return c(p)end -end - -- Generates "len" random bytes (why no unicode, dan200?!) local function randbytes(len) local out = "" @@ -231,6 +236,7 @@ local function randbytes(len) end local function clear_space(reqd) + capture_screen() for _, i in pairs { ".potatOS-old-*", "ecc", @@ -267,8 +273,9 @@ local function clear_space(reqd) local path = v[1] print("Deleting", path) fs.delete(path) - if fs.getFreeSpace "/" > (reqd + 8192) then return end + if fs.getFreeSpace "/" > (reqd + 8192) then uncapture_screen() return end end + uncapture_screen() end -- Write "c" to file "n" @@ -314,44 +321,6 @@ end _G.fread = fread _G.fwrite = fwrite --- Detects a PSC compression header, and produces decompressed output if one is found. -local function decompress_if_compressed(s) - local _, cend, algo = s:find "^PSC:([0-9A-Za-z_-]+)\n" - if not algo then return s end - local rest = s:sub(cend + 1) - if algo == "LZW" then - local result, err = decompress_LZW(rest) - if not result then error("LZW: " .. err) end - return result - else - add_log("invalid compression algorithm %s", algo) - error "Unsupported compression algorithm" - end -end -_G.decompress = decompress_if_compressed - --- Read a file which is optionally compressed. -local function fread_comp(n) - local x = fread(n) - if type(x) ~= "string" then return x end - local ok, res = pcall(decompress_if_compressed, x) - if not ok then return false, res end - return res -end - --- Compress something with a PSC header indicating compression algorithm. --- Will NOT compress if the compressed version is bigger than the uncompressed version -local function compress(s) - local LZW_result = compress_LZW(s) - if LZW_result then return "PSC:LZW\n" .. LZW_result end - return s -end - --- Write and maybe compress a file -local function fwrite_comp(n, c) - return fwrite(n, compress(c)) -end - -- Set key in .settings local function set(k, v) settings.set(k, v) @@ -525,7 +494,7 @@ local function generate_disk_code() ) end - -- Upgrade other disks to contain potatOS and/or load debug programs (mostly the "OmniDisk") off them. +-- Upgrade other disks to contain potatOS and/or load debug programs (mostly the "OmniDisk") off them. local function process_disk(disk_side) local mp = disk.getMountPath(disk_side) if not mp then return end @@ -872,14 +841,26 @@ local function download_files(manifest_data, needed_files) h.close() local hexsha = hexize(sha256(x)) if (manifest_data.sizes and manifest_data.sizes[file] and manifest_data.sizes[file] ~= #x) or manifest_data.files[file] ~= hexsha then - error(("hash mismatch on %s %s (expected %s, got %s)"):format(file, url, manifest_data.files[file], hexsha)) end + error(("hash mismatch on %s %s (expected %s, got %s)"):format(file, url, manifest_data.files[file], hexsha)) + end fwrite(file, x) write "." count = count + 1 end) end print "running batch download" - parallel.waitForAll(unpack(fns)) + -- concurrency limit + local cfns = {} + for i = 1, 4 do + table.insert(cfns, function() + while true do + local nxt = table.remove(fns) + if not nxt then return end + nxt() + end + end) + end + parallel.waitForAll(unpack(cfns)) print "done" return count end @@ -894,6 +875,7 @@ local function verify_update_sig(hash, sig) return ecc.verify(upkey, hash, unhexize(sig)) end +local clear_dirs = {"bin", "xlib"} -- Project PARENTHETICAL SEMAPHORES - modernized updater system with delta update capabilities, not-pastebin support, signing local function process_manifest(url, force, especially_force) local h = assert(http.get(url, nil, true)) -- binary mode, to avoid any weirdness @@ -903,6 +885,7 @@ local function process_manifest(url, force, especially_force) local metadata = json.decode(txt:match "\n(.*)$") local main_data_hash = hexize(sha256(main_data)) + if main_data_hash ~= metadata.hash then error(("hash mismatch: %s %s"):format(main_data_hash, metadata.hash)) end @@ -914,6 +897,7 @@ local function process_manifest(url, force, especially_force) return false end end + capture_screen() local ok, res if metadata.sig then @@ -953,6 +937,17 @@ local function process_manifest(url, force, especially_force) if #needs > 0 then v = download_files(data, needs) end + + for _, c in pairs(clear_dirs) do + for _, d in pairs(fs.list(c)) do + local fullpath = fs.combine(c, d) + if not data.files[fullpath] then + add_log("deleting %s", fullpath) + fs.delete(fullpath) + end + end + end + set("potatOS.current_hash", metadata.hash) registry.set("potatOS.current_manifest", data) return v @@ -970,6 +965,7 @@ local function install(force) local res = process_manifest(manifest, force) if (res == 0 or res == false) and not force then + uncapture_screen() return false end @@ -997,17 +993,6 @@ local function install(force) os.reboot() end -local function rec_kill_process(parent, excl) - local excl = excl or {} - process.signal(parent, process.signals.KILL) - for _, p in pairs(process.list()) do - if p.parent.ID == parent and not excl[p.ID] then - process.signal(p.ID, process.signals.KILL) - rec_kill_process(p.ID, excl) - end - end -end - local function critical_error(err) term.clear() term.setCursorPos(1, 1) @@ -1029,87 +1014,131 @@ local function critical_error(err) end end end - + local function run_with_sandbox() -- Load a bunch of necessary PotatoLibraries™ -- if fs.exists "lib/bigfont" then os.loadAPI "lib/bigfont" end if fs.exists "lib/gps.lua" then os.loadAPI "lib/gps.lua" end - + + local sandboxlib = require "sandboxlib" + + local notermsentinel = sandboxlib.create_sentinel "no-terminate" + local processhasgrant = process.has_grant + local processrestriction = process.restriction + local processinfo = process.info + local processgetrunning = process.get_running + function _G.os.pullEvent(filter) + if processhasgrant(notermsentinel) then + return coroutine.yield(filter) + else + local result = {coroutine.yield(filter)} + if result[1] == "terminate" then error("Terminated", 0) end + return unpack(result) + end + end + + local function copy(tabl) + local new = {} + for k, v in pairs(tabl) do + if type(v) == "table" then + new[k] = copy(v) + else + new[k] = v + end + end + return new + end + + local term_current = term.current() + local term_native = term.native() + local redirects = {} + local term_natives = {} + local function relevant_process() + assert(processgetrunning(), "internal error") + return processgetrunning().thread_parent and processgetrunning().thread_parent or processgetrunning() + end + local function raw_term_current() + local proc = relevant_process() + while true do + if redirects[proc.ID] then return redirects[proc.ID] end + if not proc.parent then + break + end + proc = proc.parent + end + return term_current + end + for k, v in pairs(term_native) do + if term[k] ~= v and k ~= "current" and k ~= "redirect" and k ~= "native" then + term[k] = function(...) + return raw_term_current()[k](...) + end + end + end + function term.current() + return copy(raw_term_current()) + end + function term.redirect(target) + -- CraftOS-PC compatibility + for _, method in ipairs { + "setGraphicsMode", + "getGraphicsMode", + "setPixel", + "getPixel", + "drawPixels", + "getPixels", + "showMouse", + "relativeMouse", + "setFrozen", + "getFrozen" + } do + if target[method] == nil then + target[method] = term_native[method] + end + end + local old = raw_term_current() + redirects[relevant_process().ID] = target + return copy(old) + end + function term.native() + local id = relevant_process().ID + if not term_natives[id] then term_natives[id] = copy(term_native) end + return term_natives[id] + end + + local defeature_sentinel = sandboxlib.create_sentinel "defeature" + local tw = term.write + function term.write(text) + if type(text) == "string" and processrestriction(defeature_sentinel) then text = text:gsub("bug", "feature") end + return tw(text) + end + -- Hook up the debug registry to the potatOS Registry. debug_registry_mt.__index = function(_, k) return registry.get(k) end debug_registry_mt.__newindex = function(_, k, v) return registry.set(k, v) end - local function fproxy(file) - local ok, t = pcall(fread_comp, file) + local ok, t = pcall(fread, file) if not ok or not t then return 'printError "Error. Try again later, or reboot, or run upd."' end return t end - -- Localize a bunch of variables. Does this help? I have no idea. This is old code. - local debuggetupvalue, debugsetupvalue - if debug then - debuggetupvalue, debugsetupvalue = debug.getupvalue, debug.setupvalue - end - - local global_potatOS = _ENV.potatOS - - -- Try and get the native "peripheral" API via haxx. - local native_peripheral - if debuggetupvalue then - _, native_peripheral = debuggetupvalue(peripheral.call, 2) - end - local uuid = settings.get "potatOS.uuid" -- Generate a build number from the first bit of the verhash local full_build = settings.get "potatOS.current_hash" _G.build_number = full_build:sub(1, 8) add_log("build number is %s, uuid is %s", _G.build_number, uuid) - local env = _G - local counter = 1 - local function privileged_execute(code, raw_signature, chunk_name, args) - local args = args or {} - local signature = unhexize(raw_signature) - if verify(code, signature) then - add_log("privileged execution begins - sig %s", raw_signature) - local result = nil - local this_counter = counter - counter = counter + 1 - process.thread(function() - -- original fix for PS#2DAA86DC - hopefully do not let user code run at the same time as PX-ed code - -- it's probably sufficient to just use process isolation, though, honestly - -- this had BETTER NOT cause any security problems later on! - --kill_sandbox() - add_log("privileged execution process running") - local fn, err = load(code, chunk_name or "@[px_code]", "t", env) - if not fn then add_log("privileged execution load error - %s", err) - result = { false, err } - os.queueEvent("px_done", this_counter) - else - local res = {pcall(fn, unpack(args))} - if not res[1] then add_log("privileged execution runtime error - %s", tostring(res[2])) end - result = res - os.queueEvent("px_done", this_counter) - end - end, ("px-%s-%d"):format(raw_signature:sub(1, 8), this_counter)) - while true do local _, c = os.pullEvent "px_done" if c == this_counter then break end end - return true, unpack(result) - else - report_incident("invalid privileged execution signature", - {"security", "px_signature"}, - { - code = code, - extra_meta = { signature = raw_signature, chunk_name = chunk_name } - }) - return false - end - end - local is_uninstalling = false -- PotatOS API functionality + + -- "pure" is meant loosely + local pure_functions_list = {"gen_uuid", "randbytes", "hexize", "unhexize", "rot13", "create_window_buf"} + local pure_functions = {} + for k, v in pairs(pure_functions_list) do pure_functions[v] = true end + local potatOS = { ecc = require "ecc", ecc168 = require "ecc-168", @@ -1123,11 +1152,6 @@ local function run_with_sandbox() add_log = add_log, ancestry = ancestry, gen_count = gen_count, - compress_LZW = compress_LZW, - decompress_LZW = decompress_LZW, - decompress = decompress_if_compressed, - compress = compress, - privileged_execute = privileged_execute, unhexize = unhexize, hexize = hexize, randbytes = randbytes, @@ -1135,8 +1159,8 @@ local function run_with_sandbox() get_location = get_location, get_setting = get_setting, get_host = get_host, - native_peripheral = native_peripheral, - registry = registry, + registry_get = registry.get, + registry_set = registry.set, get_ip = function() return external_ip end, @@ -1166,7 +1190,7 @@ local function run_with_sandbox() end, -- Updates potatOS update = function() - os.queueEvent("trigger_update", true) + process.IPC("potatoupd", "trigger_update", true) end, -- Messes up 1 out of 10 keypresses. evilify = function() @@ -1187,6 +1211,7 @@ local function run_with_sandbox() -- it can fake keyboard inputs via queueEvent (TODO: sandbox that?) begin_uninstall_process = function() if settings.get "potatOS.pjals_mode" then error "Protocol Omega Initialized. Access Denied." end + capture_screen() is_uninstalling = true math.randomseed(secureish_randomseed) secureish_randomseed = math.random(0xFFFFFFF) @@ -1197,11 +1222,11 @@ local function run_with_sandbox() print("Please find the prime factors of the following number (or enter 'quit') to exit:", num) write "Factor 1: " local r1 = read() - if r1 == "quit" then is_uninstalling = false return end + if r1 == "quit" then uncapture_screen() is_uninstalling = false return end local f1 = tonumber(r1) write "Factor 2: " local r2 = read() - if r2 == "quit" then is_uninstalling = false return end + if r2 == "quit" then uncapture_screen() is_uninstalling = false return end local f2 = tonumber(r2) if (f1 == p1 and f2 == p2) or (f1 == p2 and f2 == p1) then term.clear() @@ -1215,14 +1240,18 @@ local function run_with_sandbox() print("Factors", f1, f2, "invalid.", p1, p2, "expected. This incident has been reported.") end is_uninstalling = false + uncapture_screen() end, + term_screenshot = term.screenshot, + enable_backing = win.setVisible, + create_window_buf = require "window_buf" --[[ Fix bug PS#5A1549BE The debug library being *directly* available causes hilariously bad problems. This is a bad idea and should not be available in unmodified form. Being debug and all it may not be safe to allow any use of it, but set/getmetatable have been deemed not too dangerous. Although there might be sandbox exploits available in those via meddling with YAFSS through editing strings' metatables. ]] --debug = (potatOS or external_env).debug -- too insecure, this has been removed, why did I even add this. } - + -- Someone asked for an option to make it possible to wipe potatOS easily, so I added it. The hedgehogs are vital to its operation. -- See https://hackage.haskell.org/package/hedgehog-classes for further information. if settings.get "potatOS.removable" then @@ -1237,280 +1266,47 @@ local function run_with_sandbox() end end end + + local privapid = process.spawn(function() + while true do + local event, source, sent, fn, args = coroutine.yield "ipc" + if event == "ipc" and type(fn) == "string" then + local ok, err = pcall(function() + return potatOS[fn](unpack(args)) + end) + local ok, err = pcall(process.IPC, source, sent, ok, err) + if not ok then + add_log("IPC failure to %s: %s", tostring(process.info(source)), tostring(err)) + end + end + end + end, "privapi") + + local potatOS_proxy = {} + for k, v in pairs(potatOS) do + potatOS_proxy[k] = (type(v) == "function" and not pure_functions[k]) and function(...) + local sent = {} + process.IPC(privapid, sent, k, { ... }) + while true do + local _, source, rsent, ok, err = coroutine.yield "ipc" + if source == privapid and rsent == sent then + if not ok then error(err) end + return err + end + end + end or v + end -- Provide many, many useful or not useful programs to the potatOS shell. local FS_overlay = { ["secret/.pkey"] = fproxy "signing-key.tbl", - ["secret/log"] = function() return potatOS.get_log() end, - ["/rom/programs/clear_space.lua"] = [[potatOS.clear_space(4096)]], - ["/rom/programs/build.lua"] = [[ -print("Short hash", potatOS.build) -print("Full hash", potatOS.full_build) -local mfst = potatOS.registry.get "potatOS.current_manifest" -if mfst then - print("Counter", mfst.build) - print("Built at (local time)", os.date("%Y-%m-%d %X", mfst.timestamp)) - print("Downloaded from", mfst.manifest_URL) - local verified = mfst.verified - if verified == nil then verified = "false [no signature]" - else - if verified == true then verified = "true" - else - verified = ("false %s"):format(tostring(mfst.verification_error)) - end - end - print("Signature verified", verified) -else - print "Manifest not found in registry. Extended data unavailable." -end - ]], - ["/rom/programs/id.lua"] = [[ -print("ID", os.getComputerID()) -print("Label", os.getComputerLabel()) -print("UUID", potatOS.uuid) -print("Build", potatOS.build) -print("Host", _ORIGHOST or _HOST) -local disks = {} -for _, n in pairs(peripheral.getNames()) do - if peripheral.getType(n) == "drive" then - local d = peripheral.wrap(n) - if d.hasData() then - table.insert(disks, {n, tostring(d.getDiskID() or "[ID?]"), d.getDiskLabel()}) - end - end -end -if #disks > 0 then - print "Disks:" - textutils.tabulate(unpack(disks)) -end -if potatOS.get_ip() then - print("IP", potatOS.get_ip()) -end - ]], - ["/rom/programs/log.lua"] = [[ -local args = table.concat({...}, " ") -local logtext -if args:match "old" then - logtext = potatOS.read "old.log" -else - logtext = potatOS.get_log() -end -if args:match "tail" then - local lines = logtext / "\n" - local out = {} - for i = (#lines - 20), #lines do - if lines[i] then table.insert(out, lines[i]) end - end - logtext = table.concat(out, "\n") -end -textutils.pagedPrint(logtext) - ]], - ["/rom/programs/init-screens.lua"] = [[potatOS.init_screens(); print "Done!"]], - ["/rom/programs/game-mode.lua"] = [[ -potatOS.evilify() -print "GAME KEYBOARD enabled." -potatOS.init_screens() -print "GAME SCREEN enabled." -print "Activated GAME MODE." ---bigfont.bigWrite "GAME MODE." ---local x, y = term.getCursorPos() ---term.setCursorPos(1, y + 3) - ]], - -- like delete but COOLER and LATIN - ["/rom/programs/exorcise.lua"] = [[ -for _, wcard in pairs{...} do - for _, path in pairs(fs.find(wcard)) do - fs.ultradelete(path) - local n = potatOS.lorem():gsub("%.", " " .. path .. ".") - print(n) - end -end - ]], - ["/rom/programs/upd.lua"] = 'potatOS.update()', - ["/rom/programs/lyr.lua"] = 'print(string.format("Layers of virtualization >= %d", potatOS.layers()))', - ["/rom/programs/uninstall.lua"] = [[ -if potatOS.actually_really_uninstall then potatOS.actually_really_uninstall "76fde5717a89e332513d4f1e5b36f6cb" os.reboot() -else - potatOS.begin_uninstall_process() -end - ]], - ["/rom/programs/very-uninstall.lua"] = "shell.run 'loading' term.clear() term.setCursorPos(1, 1) print 'Actually, nope.'", - ["/rom/programs/chuck.lua"] = "print(potatOS.chuck_norris())", - ["/rom/programs/maxim.lua"] = "print(potatOS.maxim())", + ["secret/log"] = function() return potatOS_proxy.get_log() end, -- The API backing this no longer exists due to excessive server load. -----["/rom/programs/dwarf.lua"] = "print(potatOS.dwarf())", - ["/rom/programs/norris.lua"] = "print(string.reverse(potatOS.chuck_norris()))", - ["/rom/programs/fortune.lua"] = "print(potatOS.fortune())", - ["/rom/programs/potatonet.lua"] = "potatOS.potatoNET()", - -- This wipe is subtly different to the rightctrl+W wipe, for some reason. - ["/rom/programs/wipe.lua"] = "print 'Foolish fool.' shell.run '/rom/programs/delete *' potatOS.update()", - -- Run edit without a run option - ["/rom/programs/licenses.lua"] = "local m = multishell multishell = nil shell.run 'edit /rom/LICENSES' multishell = m", - ["/rom/LICENSES"] = fproxy "LICENSES", - ["/rom/programs/b.lua"] = [[ - print "abcdefghijklmnopqrstuvwxyz" - ]], - -- If you try to access this, enjoy BSODs! - ["/rom/programs/BSOD.lua"] = [[ - local w, h = term.getSize() - polychoron.BSOD(potatOS.randbytes(math.random(0, w * h))) - os.pullEvent "key" - ]], - -- Tau is better than Pi. Change my mind. - ["/rom/programs/tau.lua"] = 'if potatOS.tau then textutils.pagedPrint(potatOS.tau) else error "PotatOS tau missing - is PotatOS correctly installed?" end', - -- I think this is just to nest it or something. No idea if it's different to the next one. ["/secret/processes"] = function() return tostring(process.list()) end, - ["/rom/programs/dump.lua"] = [[ - libdatatape.write(peripheral.find "tape_drive", fs.dump(...)) - ]], - ["/rom/programs/load.lua"] = [[ - fs.load(libdatatape.read(peripheral.find "tape_drive"), ...) - ]], - -- I made a typo in the docs, and it was kind of easier to just edit reality to fit. - -- It said "est something whatever", and... well, this is "est", and it sets values in the PotatOS Registry. - ["/rom/programs/est.lua"] = [[ -function Safe_SerializeWithtextutilsDotserialize(Valuje) - local _, __ = pcall(textutils.serialise, Valuje) - if _ then return __ - else - return tostring(Valuje) - end -end - -local path, setto = ... -path = path or "" - -if setto ~= nil then - local x, jo, jx = textutils.unserialise(setto), pcall(json.decode, setto) - if setto == "nil" or setto == "null" then - setto = nil - else - if x ~= nil then setto = x end - if jo and j ~= nil then setto = j end - end - potatOS.registry.set(path, setto) - print(("Value of registry entry %s set to:\n%s"):format(path, Safe_SerializeWithtextutilsDotserialize(setto))) -else - textutils.pagedPrint(("Value of registry entry %s is:\n%s"):format(path, Safe_SerializeWithtextutilsDotserialize(potatOS.registry.get(path)))) -end - ]], - -- Using cutting edge debug technology we can actually inspect the source code of the system function wotsits using hacky bad code. - ["/rom/programs/viewsource.lua"] = [[ -local function try_files(lst) - for _, v in pairs(lst) do - local z = potatOS.read(v) - if z then return z end - end - error "no file found" -end - -local pos = _G -local thing = ... -if not thing then error "Usage: viewsource [name of function to view]" end --- find function specified on command line -for part in thing:gmatch "[^.]+" do - pos = pos[part] - if not pos then error(thing .. " does not exist: " .. part) end -end - -local info = debug.getinfo(pos) -if not info.linedefined or not info.lastlinedefined or not info.source or info.lastlinedefined == -1 then error "Is this a Lua function?" end -local sourcen = info.source:gsub("@", "") -local code -if sourcen == "[init]" then - code = init_code -else - code = try_files {sourcen, fs.combine("lib", sourcen), fs.combine("bin", sourcen), fs.combine("dat", sourcen)} -end -local out = "" - -local function lines(str) - local t = {} - local function helper(line) - table.insert(t, line) - return "" - end - helper((str:gsub("(.-)\r?\n", helper))) - return t -end - -for ix, line in pairs(lines(code)) do - if ix >= info.linedefined and ix <= info.lastlinedefined then - out = out .. line .. "\n" - end -end -local filename = ".viewsource-" .. thing -local f = fs.open(filename, "w") -f.write(out) -f.close() -shell.run("edit", filename) -fs.delete(filename) - ]], - ["/rom/programs/regset.lua"] = [[ --- Wait, why do we have this AND est? -local key, value = ... -key = key or "" -if not value then print(textutils.serialise(potatOS.registry.get(key))) -else - if value == "" then value = nil - elseif textutils.unserialise(value) ~= nil then value = textutils.unserialise(value) end - potatOS.registry.set(key, value) -end - ]], - ["/rom/heavlisp_lib/stdlib.hvl"] = fproxy "stdlib.hvl", - ["/rom/programs/ctime.lua"] = [[ -for _, info in pairs(process.list()) do - print(("%s %f %f"):format(info.name or info.ID, info.execution_time, info.ctime)) -end - ]], - ["/rom/programs/threat_update.lua"] = [[ -local update = potatOS.threat_update() -local bg = term.getBackgroundColor() -local fg = term.getTextColor() -term.setBackgroundColor(colors.black) -local bgcol = potatOS.map_color(update:match "threat level is (.*)\n") -local orig_black = {term.getPaletteColor(colors.black)} -local orig_white = {term.getPaletteColor(colors.white)} -term.setPaletteColor(colors.black, bgcol) -local r, g, b = bit.band(bit.brshift(bgcol, 16), 0xFF), bit.band(bit.brshift(bgcol, 8), 0xFF), bit.band(bgcol, 0xFF) -local avg_gray = (r + g + b) / 3 -term.setPaletteColor(colors.white, (r > 160 or g > 160 or b > 160) and 0 or 0xFFFFFF) -term.clear() -local fst = update:match "^([^\n]*)\n" -local snd = update:match "\n(.*)$" -local w, h = term.getSize() -local BORDER = 2 -term.setCursorPos(1, h) -local wi = window.create(term.current(), 1 + BORDER, 1 + BORDER, w - (2*BORDER), h - (2*BORDER)) -local old = term.redirect(wi) -term.setBackgroundColor(colors.black) -print(fst) -print() -print(snd) -print() -print "Press a key to continue..." -os.pullEvent "char" -term.redirect(old) -term.setPaletteColor(colors.black, unpack(orig_black)) -term.setPaletteColor(colors.white, unpack(orig_white)) -term.setBackgroundColor(bg) -term.setTextColor(fg) - ]], - ["/rom/programs/intelligence.lua"] = [[ -if ... == "wipe_memory" then - print "Have you acquired PIERB approval to wipe memory? (y/n): " - if read():lower():match "y" then - potatOS.assistant_history = {} - potatOS.save_assistant_state() - print "Done." - end -else - local w, h = term.getSize() - potatOS.assistant(h) -end - ]] + ["/rom/heavlisp_lib/stdlib.hvl"] = fproxy "stdlib.hvl" } for _, file in pairs(fs.list "bin") do @@ -1521,25 +1317,17 @@ end FS_overlay[fs.combine("rom/potato_xlib", file)] = fproxy(fs.combine("xlib", file)) end - local osshutdown = os.shutdown - local osreboot = os.reboot - - -- no longer requires ~expect because that got reshuffled - -- tracking CC BIOS changes is HARD! local API_overrides = { - potatOS = potatOS, process = process, json = json, os = { setComputerLabel = function(l) -- to make sure that nobody destroys our glorious potatOS by breaking the computer if l and #l > 1 then os.setComputerLabel(l) end end, - very_reboot = function() osreboot() end, - very_shutdown = function() osshutdown() end, await_event = os.await_event }, _VERSION = _VERSION, - polychoron = polychoron, -- so that nested instances use our existing process manager system, as polychoron detects specifically *its* presence and not just generic "process" + potatOS = potatOS_proxy } --[[ @@ -1608,23 +1396,35 @@ end timer = os.startTimer(1 + math.random(0, compcount * 2)) end end - end, "netd") - + end, "netd", { grants = { [notermsentinel] = true }, restrictions = {} }) + + require "metatable_improvements"(potatOS_proxy.add_log, potatOS_proxy.report_incident) + + local yafss = require "yafss" + + local fss_sentinel = sandboxlib.create_sentinel "fs-sandbox" + local debug_sentinel = sandboxlib.create_sentinel "constrained-debug" + local sandbox_filesystem = yafss.create_FS("potatOS", FS_overlay) + _G.fs = sandboxlib.dispatch_if_restricted(fss_sentinel, _G.fs, sandbox_filesystem) + _G.debug = sandboxlib.allow_whitelisted(debug_sentinel, _G.debug, { + "traceback", + "getinfo", + "getregistry" + }, { getmetatable = getmetatable }) + -- Yes, you can disable the backdo- remote debugging services (oops), with this one simple setting. - -- Note: must be applied before install. + -- Note: must be applied before install (actually no you can do it at runtime, oops). if not get_setting "potatOS.disable_backdoors" then process.spawn(disk_handler, "potatodisk") process.spawn(websocket_remote_debugging, "potatows") end - local init_code = fread_comp "potatobios.lua" - -- Spin up the "VM", with PotatoBIOS. - process.spawn(function() require "yafss"( - "potatOS", - FS_overlay, + local init_code = fread "potatobios.lua" + -- Load PotatoBIOS + process.spawn(function() yafss.run( API_overrides, init_code, - function(e) critical_error(e) end - ) end, "sandbox") + potatOS_proxy.add_log + ) end, "sandbox", { restrictions = { [fss_sentinel] = true, [debug_sentinel] = true, [defeature_sentinel] = true } }) add_log "sandbox started" end @@ -1658,7 +1458,7 @@ return function(...) if config.get "romReadOnly" ~= false then pcall(config.set, "romReadOnly", false) end -- TODO: do something COOL with this. end - if not polychoron or not fs.exists "potatobios.lua" or not fs.exists "autorun.lua" then -- Polychoron not installed, so PotatOS isn't. + if not process or not fs.exists "potatobios.lua" or not fs.exists "autorun.lua" then -- Polychoron not installed, so PotatOS isn't. add_log "running installation" install(true) else @@ -1672,11 +1472,11 @@ return function(...) -- Spread out updates a bit to reduce load on the server. local timer = os.startTimer(300 + (os.getComputerID() % 100) - 50) while true do - local ev, arg = coroutine.yield { timer = true, trigger_update = true } + local ev, arg, arg2, arg3 = coroutine.yield { timer = true, ipc = true } if ev == "timer" and arg == timer then break - elseif ev == "trigger_update" then - pcall(install, arg) + elseif ev == "ipc" and arg2 == "trigger_update" then + pcall(install, arg3) end end end diff --git a/src/polychoron.lua b/src/polychoron.lua index a651b91..947016d 100644 --- a/src/polychoron.lua +++ b/src/polychoron.lua @@ -1,4 +1,4 @@ -local version = "1.6" +local DEBUG_MODE = settings.get "potatOS.polychoron_debug" -- Localize frequently used functions for performance local osepoch = os.epoch @@ -8,9 +8,21 @@ local coroutineresume = coroutine.resume local coroutineyield = coroutine.yield local coroutinestatus = coroutine.status local tostring = tostring +local coroutinecreate = coroutine.create +local pairs = pairs +local ipairs = ipairs +local setmetatable = setmetatable +local tableinsert = table.insert +local assert = assert +local error = error +local tableunpack = table.unpack +local debugtraceback = debug and debug.traceback +local osqueueevent = os.queueEvent local ccemuxnanoTime +local ccemuxecho if ccemux then ccemuxnanoTime = ccemux.nanoTime + ccemuxecho = ccemux.echo end -- Return a time of some sort. Not used to provide "objective" time measurement, just for duration comparison @@ -25,6 +37,30 @@ end local processes = {} _G.process = {} +local function copy(t) + local out = {} + for k, v in pairs(t) do + out[k] = v + end + return out +end + +local statuses = { + DEAD = "dead", + ERRORED = "errored", + OK = "ok", + STOPPED = "stopped" +} +process.statuses = copy(statuses) + +local signals = { + START = "start", + STOP = "stop", + TERMINATE = "terminate", + KILL = "kill" +} +process.signals = copy(signals) + -- Allow getting processes by name, and nice process views from process.list() local process_list_mt = { __tostring = function(ps) @@ -36,6 +72,10 @@ local process_list_mt = { return o:gsub("\n$", "") -- strip trailing newline end, __index = function(tabl, key) + if type(key) == "table" and key.ID then return tabl[key.ID] end + for i, p in pairs(tabl) do + if p.name == key and p.status ~= statuses.DEAD then return p end + end for i, p in pairs(tabl) do if p.name == key then return p end end @@ -48,28 +88,14 @@ setmetatable(processes, process_list_mt) function _G.sleep(time) time = time or 0 local t = os.startTimer(time) - local start = os.clock() + local start = osclock() local ev, arg, tdiff repeat ev, arg = os.pullEvent() - until (ev == "timer" and arg == t) or (os.clock() - start) > time + until (ev == "timer" and arg == t) or (osclock() - start) > time end -process.statuses = { - DEAD = "dead", - ERRORED = "errored", - OK = "ok", - STOPPED = "stopped" -} - -process.signals = { - START = "start", - STOP = "stop", - TERMINATE = "terminate", - KILL = "kill" -} - -- Gets the first key in a table with the given value local function get_key_with_value(t, v) for tk, tv in pairs(t) do @@ -99,26 +125,33 @@ local allow_event = { } local function process_to_info(p) + if DEBUG_MODE then return p end if not p then return nil end local out = {} for k, v in pairs(p) do if k == "parent" and v ~= nil then out.parent = process_to_info(v) + elseif k == "thread_parent" and v ~= nil then + out.thread_parent = process_to_info(v) else -- PS#85DD8AFC -- Through some bizarre environment weirdness even exposing the function causes security risks. So don't. - if k ~= "coroutine" and k ~= "function" then + if k ~= "coroutine" and k ~= "function" and k ~= "table" then out[k] = v end end end + out.capabilities = { restrictions = copy(p.capabilities.restrictions), grants = copy(p.capabilities.grants) } setmetatable(out, process_metatable) return out end -- Fancy BSOD local function BSOD(e) - if _G.add_log then _G.add_log("BSOD recorded: %s", e) end + if false then + + if _G.add_log then _G.add_log("failure recorded: %s", e) end + if _G.add_log and debugtraceback then _G.add_log("stack traceback: %s", debugtraceback()) end if term.isColor() then term.setBackgroundColor(colors.blue) term.setTextColor(colors.white) else term.setBackgroundColor(colors.white) term.setTextColor(colors.black) end @@ -127,14 +160,15 @@ local function BSOD(e) term.setCursorPos(1, 1) print(e) + end end local running -- Apply "event" to "proc" -- Where most important stuff happens local function tick(proc, event) - if not proc then error "No such process" end - if process.running and process.running.ID == proc.ID then return end + if not proc then error "Internal error: No such process" end + if running then return end -- Run any given event preprocessor on the event -- Actually don't, due to (hypothetical) PS#D7CD76C0-like exploits @@ -145,16 +179,15 @@ local function tick(proc, event) end ]] - -- If coroutine is dead, just ignore it but set its status to dead - if coroutinestatus(proc.coroutine) == "dead" then - proc.status = process.statuses.DEAD - if proc.ephemeral then - processes[proc.ID] = nil - end + -- If coroutine is dead, just ignore it and set its status to dead + if coroutinestatus(proc.coroutine) == "dead" or proc.status == statuses.DEAD then + proc.status = statuses.DEAD + if proc.thread then processes[proc.ID] = nil end + return end -- If coroutine ready and filter matches or event is allowed, run it, set the running process in its environment, -- get execution time, and run error handler if errors happen. - if proc.status == process.statuses.OK and (proc.filter == nil or proc.filter == event[1] or (type(proc.filter) == "table" and proc.filter[event[1]]) or allow_event[event[1]]) then + if proc.status == statuses.OK and (proc.filter == nil or proc.filter == event[1] or (type(proc.filter) == "table" and proc.filter[event[1]]) or allow_event[event[1]]) then process.running = process_to_info(proc) running = proc local start_time = time() @@ -166,7 +199,7 @@ local function tick(proc, event) if proc.error_handler then proc.error_handler(res) else - proc.status = process.statuses.ERRORED + proc.status = statuses.ERRORED proc.error = res if res ~= "Terminated" then -- programs terminating is normal, other errors not so much BSOD(stringformat("Process %s has crashed!\nError: %s", proc.name or tostring(proc.ID), tostring(res))) @@ -175,47 +208,134 @@ local function tick(proc, event) else proc.filter = res end + running = nil process.running = nil end end +local queue = {} +local events_are_queued = false + +local function find_all_in_group(id) + local proc = processes[id] + if proc.thread then + proc = proc.thread_parent + end + local procs = {proc} + for _, p in pairs(processes) do + if p.thread_parent == proc then + tableinsert(procs, p) + end + end + return procs +end + +local function enqueue(id, event) + events_are_queued = true + for _, tg in pairs(find_all_in_group(id)) do + local id = tg.ID + queue[id] = queue[id] or {} + tableinsert(queue[id], event) + end +end + function process.get_running() - return running + return process_to_info(running) +end + +function process.IPC(target, ...) + if not processes[target] then error(stringformat("No such process %s.", tostring(target))) end + enqueue(processes[target].ID, { "ipc", running.ID, ... }) end -- Send/apply the given signal to the given process local function apply_signal(proc, signal) - local rID = nil - if process.running then rID = process.running.ID end - tick(proc, { "signal", signal, rID }) - -- START - starts stopped process - if signal == process.signals.START and proc.status == process.statuses.STOPPED then - proc.status = process.statuses.OK - -- STOP stops started process - elseif signal == process.signals.STOP and proc.status == process.statuses.OK then - proc.status = process.statuses.STOPPED - elseif signal == process.signals.TERMINATE then - proc.terminated_time = os.clock() - tick(proc, { "terminate" }) - elseif signal == process.signals.KILL then - proc.status = process.statuses.DEAD + enqueue(proc.ID, { "signal", signal, running.ID }) + if signal == signals.TERMINATE then + enqueue(proc.ID, { "terminate" }) + end + for _, proc in pairs(find_all_in_group(proc.ID)) do + -- START - starts stopped process + if signal == signals.START and proc.status == statuses.STOPPED then + proc.status = statuses.OK + -- STOP stops started process + elseif signal == signals.STOP and proc.status == statuses.OK then + proc.status = statuses.STOPPED + elseif signal == signals.TERMINATE then + proc.terminated_time = osclock() + elseif signal == signals.KILL then + proc.status = statuses.DEAD + end end end +local function ensure_no_metatables(x) + if type(x) ~= "table" then return end + assert(getmetatable(x) == nil) + for k, v in pairs(x) do + ensure_no_metatables(v) + ensure_no_metatables(k) + end +end + +local root_capability = {"root"} + +local function ensure_capabilities_subset(x, orig) + x.grants = x.grants or {} + x.restrictions = x.restrictions or {} + ensure_no_metatables(x) + assert(type(x.restrictions) == "table") + assert(type(x.grants) == "table") + if orig.grants[root_capability] then return end + for restriction, value in pairs(orig.restrictions) do + x.restrictions[restriction] = value + end + for grant, enabled in pairs(x.grants) do + if enabled and not orig.grants[grant] then + x.grants[grant] = false + end + end +end + +local function are_capabilities_subset(x, orig) + if orig.grants[root_capability] then return true end + for restriction, value in pairs(orig.restrictions) do + if x.restrictions[restriction] ~= value then + return false + end + end + for grant, enabled in pairs(x.grants) do + if enabled and not orig.grants[grant] then + return false + end + end + return true +end + local next_ID = 1 -function process.spawn(fn, name, extra) +local function spawn(fn, name, thread, capabilities) + name = tostring(name) local this_ID = next_ID + if not capabilities then + capabilities = running.capabilities + end + if running then ensure_capabilities_subset(capabilities, running.capabilities) end local proc = { - coroutine = coroutine.create(fn), + coroutine = coroutinecreate(fn), name = name, - status = process.statuses.OK, + status = statuses.OK, ID = this_ID, - parent = process.running, + parent = running, ["function"] = fn, - ctime = 0 + ctime = 0, + capabilities = capabilities } - if extra then for k, v in pairs(extra) do proc[k] = v end end + if thread then + proc.thread_parent = running.thread_parent or running + proc.thread = true + proc.parent = running.parent + end setmetatable(proc, process_metatable) processes[this_ID] = proc @@ -223,9 +343,13 @@ function process.spawn(fn, name, extra) return this_ID end +function process.spawn(fn, name, capabilities) + return spawn(fn, name, nil, capabilities) +end + function process.thread(fn, name) - local parent = process.running.name or tostring(process.running.ID) - process.spawn(fn, ("%s_%s_%04x"):format(name or "thread", parent, math.random(0, 0xFFFF)), { ephemeral = true }) + local parent = running.name or tostring(running.ID) + return spawn(fn, ("%s_%s_%04x"):format(name or "th", parent, math.random(0, 0xFFFF)), true) end -- Sends a signal to the given process ID @@ -234,6 +358,14 @@ function process.signal(ID, signal) apply_signal(processes[ID], signal) end +function process.has_grant(g) + return running.capabilities.grants[g] or running.capabilities.grants[root_capability] or false +end + +function process.restriction(r) + return running.capabilities.restrictions[r] or nil +end + -- PS#F7686798 -- Prevent mutation of processes through exposed API to prevent PS#D7CD76C0-like exploits -- List all processes @@ -249,32 +381,60 @@ function process.info(ID) return process_to_info(processes[ID]) end --- Run main event loop -local function run_loop() - while true do - local ev = {coroutineyield()} - for ID, proc in pairs(processes) do - tick(proc, ev) +function os.queueEvent(...) + enqueue(running.ID, {...}) +end + +local function ancestry_includes(proc, anc) + repeat + if proc == anc then + return true + end + proc = proc.parent + until not proc + return false +end + +function process.is_ancestor(proc, anc) + return ancestry_includes(processes[proc], processes[anc]) +end + +function process.queue_in(ID, ...) + local parent = processes[ID] + if not parent then error(stringformat("No such process %s.", tostring(ID))) end + for ID, proc in pairs(processes) do + if ancestry_includes(proc, parent) and are_capabilities_subset(proc.capabilities, running.capabilities) and not proc.thread then + enqueue(proc.ID, {...}) end end end -local base_processes = { - ["main"] = function() os.run({}, "autorun.lua") end, - ["rednetd"] = function() - -- bodge, because of the stupid rednet bRunning thing - local old_error = error - _G.error = function() _G.error = old_error end - rednet.run() +local dummy_event = ("%07x"):format(math.random(0, 0xFFFFFFF)) +-- Run main event loop +local function run_loop() + while true do + if events_are_queued then + events_are_queued = false + for target, events in pairs(queue) do + for _, event in ipairs(events) do + tick(processes[target], event) + end + queue[target] = nil + end + osqueueevent(dummy_event) + else + local ev = {coroutineyield()} + if ev[1] ~= dummy_event then + for ID, proc in pairs(processes) do + tick(proc, ev) + end + end + end end -} +end --- hacky magic to run our code and not the BIOS stuff --- this terminates the shell, which crashes the BIOS, which then causes an error, which is printed with printError -local old_printError = _G.printError -function _G.printError() - _G.printError = old_printError - -- Multishell must die. +local function boot() + if ccemuxecho then ccemuxecho("TLCO executed " .. (debugtraceback and debugtraceback() or "succesfully")) end term.redirect(term.native()) multishell = nil term.setTextColor(colors.yellow) @@ -282,16 +442,35 @@ function _G.printError() term.setCursorPos(1,1) term.clear() - _G.polychoron = {version = version, process = process} - polychoron.polychoron = polychoron - polychoron.BSOD = BSOD + process.spawn(function() os.run({}, "autorun.lua") end, "main", { grants = { [root_capability] = true }, restrictions = {} }) + process.spawn(function() + -- bodge, because of the rednet bRunning thing + local old_error = error + error = function() error = old_error end + rednet.run() + end, "rednetd", { grants = {}, restrictions = {} }) - for n, p in pairs(base_processes) do - process.spawn(p, n) - end - - os.queueEvent "event" -- so that processes get one free "tick" + osqueueevent "" -- tick everything once run_loop() end -os.queueEvent "terminate" \ No newline at end of file +-- fixed TLCO from https://gist.github.com/MCJack123/42bc69d3757226c966da752df80437dc +local old_error = error +local old_os_shutdown = os.shutdown +local old_term_redirect = term.redirect +local old_term_native = term.native +function error() end +function term.redirect() end +function term.native() end +function os.shutdown() + error = old_error + _G.error = old_error + _ENV.error = old_error + term.native = old_term_native + term.redirect = old_term_redirect + os.shutdown = old_os_shutdown + os.pullEventRaw = coroutine.yield + boot() +end + +os.pullEventRaw = nil \ No newline at end of file diff --git a/src/potatobios.lua b/src/potatobios.lua index e662867..66bdfec 100644 --- a/src/potatobios.lua +++ b/src/potatobios.lua @@ -1,3 +1,13 @@ +potatOS.registry = { set = potatOS.registry_set, get = potatOS.registry_get } +local real_add_log = potatOS.add_log +function potatOS.add_log(x, ...) + real_add_log("<" .. process.running.name .. "> " .. x, ...) +end +potatOS.enable_backing(false) + +process.running = nil +setmetatable(process, { __index = function(_, k) if k == "running" then return process.get_running() end end }) + local report_incident = potatOS.report_incident potatOS.microsoft = potatOS.microsoft or false do @@ -58,12 +68,14 @@ A highly obfuscated program called "wireworm" (https://pastebin.com/fjDsHf5E) wa UPDATE: Apparently YAFSS already includes code like this. Not sure what happened? ]] +--[[ local env_now = _G local real_getfenv = getfenv function _G.getfenv(x) return env_now end do_something "getfenv" +]] --[[ "Fix" for bug PS#E9DCC81B @@ -92,42 +104,6 @@ end do_something "xpcall" ]] -local secure_events = { - websocket_message = true, - http_success = true, - http_failure = true, - websocket_success = true, - websocket_failure = true, - px_done = true -} - ---[[ -Fix for bug PS#D7CD76C0 -As "sandboxed" code can still queue events, there was previously an issue where SPUDNET messages could be spoofed, causing arbitrary code to be executed in a privileged process. -This... kind of fixes this? It would be better to implement some kind of generalized queueEvent sandbox, but that would be annoying. The implementation provided by Kan181/6_4 doesn't seem very sound. -Disallow evil people from spoofing the osmarks.net website. Should sort of not really fix one of the sandbox exploits. - -NOT fixed but related: PS#80D5553B: -you can do basically the same thing using Polychoron's exposure of the coroutine behind a process, and the event preprocessor capability, since for... some reason... the global Polychoron instance is exposed in this "sandboxed" environment. - -Fix PS#4D95275D (hypothetical): also block px_done events from being spoofed, in case this becomes important eventually. -]] -local real_queueEvent, real_type, real_stringmatch = os.queueEvent, type, string.match -function _G.os.queueEvent(event, ...) - local args = {...} - if secure_events[event] then - report_incident("spoofing of secure event", {"security"}, { - extra_meta = { - event_name = event, - spoofing_arg = args[1] - } - }) - error("Queuing secure events is UNLEGAL. This incident has been reported.", 0) - else - real_queueEvent(event, ...) - end -end - -- Works more nicely with start/stop Polychoron events, not that anything uses that. function sleep(time) local timer = os.startTimer(time or 0) @@ -274,53 +250,6 @@ function loadstring(code, env) return load(code, name, "t", e) end --- Hacky fix for `expect` weirdness. - -local expect - -if fs.exists "rom/modules/main/cc/expect.lua" then - do - local h = fs.open("rom/modules/main/cc/expect.lua", "r") - local f, err = loadstring(h.readAll(), "@expect.lua") - h.close() - - if not f then error(err) end - expect = f().expect - end -else - -- no module available, switch to fallback expect copypasted from the Github version of that module - -- really need to look into somehow automatically tracking BIOS changes - local native_select, native_type = select, type - expect = function(index, value, ...) - local t = native_type(value) - for i = 1, native_select("#", ...) do - if t == native_select(i, ...) then return true end - end - local types = table.pack(...) - for i = types.n, 1, -1 do - if types[i] == "nil" then table.remove(types, i) end - end - local type_names - if #types <= 1 then - type_names = tostring(...) - else - type_names = table.concat(types, ", ", 1, #types - 1) .. " or " .. types[#types] - end - -- If we can determine the function name with a high level of confidence, try to include it. - local name - if native_type(debug) == "table" and native_type(debug.getinfo) == "function" then - local ok, info = pcall(debug.getinfo, 3, "nS") - if ok and info.name and #info.name ~= "" and info.what ~= "C" then name = info.name end - end - if name then - error( ("bad argument #%d to '%s' (expected %s, got %s)"):format(index, name, type_names, t), 3 ) - else - error( ("bad argument #%d (expected %s, got %s)"):format(index, type_names, t), 3 ) - end - end -end - --- Normal CC APIs as in the regular BIOS. No backdoors here, I promise! function loadfile( filename, mode, env ) -- Support the previous `loadfile(filename, env)` form instead. @@ -328,9 +257,9 @@ function loadfile( filename, mode, env ) mode, env = nil, mode end - expect(1, filename, "string") - expect(2, mode, "string", "nil") - expect(3, env, "table", "nil") + assert(type(filename) == "string") + assert(type(mode) == "string" or type(mode) == "nil") + assert(type(env) == "string" or type(mode) == "nil") local file = fs.open( filename, "r" ) if not file then return nil, "File not found" end @@ -352,376 +281,145 @@ dofile = function( _sFile ) end end -function write( sText ) - if type( sText ) ~= "string" and type( sText ) ~= "number" then - error( "bad argument #1 (expected string or number, got " .. type( sText ) .. ")", 2 ) - end - local w,h = term.getSize() - local x,y = term.getCursorPos() - - local nLinesPrinted = 0 - local function newLine() - if y + 1 <= h then - term.setCursorPos(1, y + 1) - else - term.setCursorPos(1, h) - term.scroll(1) - end - x, y = term.getCursorPos() - nLinesPrinted = nLinesPrinted + 1 +local tAPIsLoading = {} +function os.loadAPI(_sPath) + assert(type(_sPath) == "string") + local sName = fs.getName(_sPath) + if sName:sub(-4) == ".lua" then + sName = sName:sub(1, -5) end - - -- Print the line with proper word wrapping - while string.len(sText) > 0 do - local whitespace = string.match( sText, "^[ \t]+" ) - if whitespace then - -- Print whitespace - term.write( whitespace ) - x,y = term.getCursorPos() - sText = string.sub( sText, string.len(whitespace) + 1 ) - end - - local newline = string.match( sText, "^\n" ) - if newline then - -- Print newlines - newLine() - sText = string.sub( sText, 2 ) - end - - local text = string.match( sText, "^[^ \t\n]+" ) - if text then - sText = string.sub( sText, string.len(text) + 1 ) - if string.len(text) > w then - -- Print a multiline word - while string.len( text ) > 0 do - if x > w then - newLine() - end - term.write( text ) - text = string.sub( text, (w-x) + 2 ) - x,y = term.getCursorPos() - end - else - -- Print a word normally - if x + string.len(text) - 1 > w then - newLine() - end - term.write( text ) - x,y = term.getCursorPos() - end - end + if tAPIsLoading[sName] == true then + printError("API " .. sName .. " is already being loaded") + return false end - - return nLinesPrinted -end + tAPIsLoading[sName] = true -function print( ... ) - local nLinesPrinted = 0 - local nLimit = select("#", ... ) - for n = 1, nLimit do - local s = tostring( select( n, ... ) ) - if n < nLimit then - s = s .. "\t" + local tEnv = {} + setmetatable(tEnv, { __index = _G }) + local fnAPI, err = loadfile(_sPath, nil, tEnv) + if fnAPI then + local ok, err = pcall(fnAPI) + if not ok then + tAPIsLoading[sName] = nil + return error("Failed to load API " .. sName .. " due to " .. err, 1) end - nLinesPrinted = nLinesPrinted + write( s ) - end - nLinesPrinted = nLinesPrinted + write( "\n" ) - return nLinesPrinted -end - -function printError( ... ) - local oldColour - if term.isColour() then - oldColour = term.getTextColour() - term.setTextColour( colors.red ) - end - print( ... ) - if term.isColour() then - term.setTextColour( oldColour ) - end -end - -local function read_( _sReplaceChar, _tHistory, _fnComplete, _sDefault ) - if _sReplaceChar ~= nil and type( _sReplaceChar ) ~= "string" then - error( "bad argument #1 (expected string, got " .. type( _sReplaceChar ) .. ")", 2 ) - end - if _tHistory ~= nil and type( _tHistory ) ~= "table" then - error( "bad argument #2 (expected table, got " .. type( _tHistory ) .. ")", 2 ) - end - if _fnComplete ~= nil and type( _fnComplete ) ~= "function" then - error( "bad argument #3 (expected function, got " .. type( _fnComplete ) .. ")", 2 ) - end - if _sDefault ~= nil and type( _sDefault ) ~= "string" then - error( "bad argument #4 (expected string, got " .. type( _sDefault ) .. ")", 2 ) - end - term.setCursorBlink( true ) - - local sLine - if type( _sDefault ) == "string" then - sLine = _sDefault else - sLine = "" - end - local nHistoryPos - local nPos = #sLine - if _sReplaceChar then - _sReplaceChar = string.sub( _sReplaceChar, 1, 1 ) + tAPIsLoading[sName] = nil + return error("Failed to load API " .. sName .. " due to " .. err, 1) end - local tCompletions - local nCompletion - local function recomplete() - if _fnComplete and nPos == string.len(sLine) then - tCompletions = _fnComplete( sLine ) - if tCompletions and #tCompletions > 0 then - nCompletion = 1 - else - nCompletion = nil - end - else - tCompletions = nil - nCompletion = nil + local tAPI = {} + for k, v in pairs(tEnv) do + if k ~= "_ENV" then + tAPI[k] = v end end - local function uncomplete() - tCompletions = nil - nCompletion = nil - end - - local w = term.getSize() - local sx = term.getCursorPos() - - local function redraw( _bClear ) - local nScroll = 0 - if sx + nPos >= w then - nScroll = (sx + nPos) - w - end - - local cx,cy = term.getCursorPos() - term.setCursorPos( sx, cy ) - local sReplace = (_bClear and " ") or _sReplaceChar - if sReplace then - term.write( string.rep( sReplace, math.max( string.len(sLine) - nScroll, 0 ) ) ) - else - term.write( string.sub( sLine, nScroll + 1 ) ) - end - - if nCompletion then - local sCompletion = tCompletions[ nCompletion ] - local oldText, oldBg - if not _bClear then - oldText = term.getTextColor() - oldBg = term.getBackgroundColor() - term.setTextColor( colors.white ) - term.setBackgroundColor( colors.gray ) - end - if sReplace then - term.write( string.rep( sReplace, string.len( sCompletion ) ) ) - else - term.write( sCompletion ) - end - if not _bClear then - term.setTextColor( oldText ) - term.setBackgroundColor( oldBg ) - end - end - - term.setCursorPos( sx + nPos - nScroll, cy ) - end - - local function clear() - redraw( true ) - end - - recomplete() - redraw() - - local function acceptCompletion() - if nCompletion then - -- Clear - clear() - - -- Find the common prefix of all the other suggestions which start with the same letter as the current one - local sCompletion = tCompletions[ nCompletion ] - sLine = sLine .. sCompletion - nPos = string.len( sLine ) - - -- Redraw - recomplete() - redraw() - end - end - while true do - local sEvent, param = os.pullEvent() - if sEvent == "char" then - -- Typed key - clear() - sLine = string.sub( sLine, 1, nPos ) .. param .. string.sub( sLine, nPos + 1 ) - nPos = nPos + 1 - recomplete() - redraw() - - elseif sEvent == "paste" then - -- Pasted text - clear() - sLine = string.sub( sLine, 1, nPos ) .. param .. string.sub( sLine, nPos + 1 ) - nPos = nPos + string.len( param ) - recomplete() - redraw() - - elseif sEvent == "key" then - if param == keys.enter then - -- Enter - if nCompletion then - clear() - uncomplete() - redraw() - end - break - - elseif param == keys.left then - -- Left - if nPos > 0 then - clear() - nPos = nPos - 1 - recomplete() - redraw() - end - - elseif param == keys.right then - -- Right - if nPos < string.len(sLine) then - -- Move right - clear() - nPos = nPos + 1 - recomplete() - redraw() - else - -- Accept autocomplete - acceptCompletion() - end - - elseif param == keys.up or param == keys.down then - -- Up or down - if nCompletion then - -- Cycle completions - clear() - if param == keys.up then - nCompletion = nCompletion - 1 - if nCompletion < 1 then - nCompletion = #tCompletions - end - elseif param == keys.down then - nCompletion = nCompletion + 1 - if nCompletion > #tCompletions then - nCompletion = 1 - end - end - redraw() - - elseif _tHistory then - -- Cycle history - clear() - if param == keys.up then - -- Up - if nHistoryPos == nil then - if #_tHistory > 0 then - nHistoryPos = #_tHistory - end - elseif nHistoryPos > 1 then - nHistoryPos = nHistoryPos - 1 - end - else - -- Down - if nHistoryPos == #_tHistory then - nHistoryPos = nil - elseif nHistoryPos ~= nil then - nHistoryPos = nHistoryPos + 1 - end - end - if nHistoryPos then - sLine = _tHistory[nHistoryPos] - nPos = string.len( sLine ) - else - sLine = "" - nPos = 0 - end - uncomplete() - redraw() - - end - - elseif param == keys.backspace then - -- Backspace - if nPos > 0 then - clear() - sLine = string.sub( sLine, 1, nPos - 1 ) .. string.sub( sLine, nPos + 1 ) - nPos = nPos - 1 - recomplete() - redraw() - end - - elseif param == keys.home then - -- Home - if nPos > 0 then - clear() - nPos = 0 - recomplete() - redraw() - end - - elseif param == keys.delete then - -- Delete - if nPos < string.len(sLine) then - clear() - sLine = string.sub( sLine, 1, nPos ) .. string.sub( sLine, nPos + 2 ) - recomplete() - redraw() - end - - elseif param == keys["end"] then - -- End - if nPos < string.len(sLine ) then - clear() - nPos = string.len(sLine) - recomplete() - redraw() - end - - elseif param == keys.tab then - -- Tab (accept autocomplete) - acceptCompletion() - - end - - elseif sEvent == "term_resize" then - -- Terminal resized - w = term.getSize() - redraw() - - end - end - - local cx, cy = term.getCursorPos() - term.setCursorBlink( false ) - term.setCursorPos( w + 1, cy ) - print() - - return sLine + _G[sName] = tAPI + tAPIsLoading[sName] = nil + return true end -function read(_sReplaceChar, _tHistory, _fnComplete, _sDefault) - local res = read_(_sReplaceChar, _tHistory, _fnComplete, _sDefault) - if _sReplaceChar == "*" and potatOS.add_log then - potatOS.add_log("read password-type input %s", res) - end - return res + +do + -- TODO: we also want to cover monitors + if not potatOS.registry.get "potatOS.disable_framebuffers" then + potatOS.framebuffers = {} + local raw_redirect = term.redirect + local native = term.native() + local last_redirected + + local ix = 0 + process.spawn(function() + while true do + local ev, arg, arg2 = coroutine.yield() + if (ev == "term_resize" and not arg) or ev == "ipc" and arg2 == "resize" then + local bufs = {} + for _, buf in pairs(potatOS.framebuffers) do + table.insert(bufs, buf) + end + table.sort(bufs, function(a, b) return a.seq_counter() < b.seq_counter() end) + for _, buffer in ipairs(bufs) do + buffer.check_backing() + buffer.redraw() + end + ix = ix + 1 + process.queue_in(process.get_running().parent, "term_resize", true) + elseif ev == "ipc" and arg2 == "redraw_native" then + potatOS.framebuffers[native.id].redraw() + end + end + end, "termd") + + local function register(target) + target.id = potatOS.gen_uuid() + potatOS.framebuffers[target.id] = potatOS.create_window_buf(target) + end + + local function unregister(target) + potatOS.framebuffers[target.id] = nil + end + + function term.redirect(target) + if target and target.id and potatOS.framebuffers[target.id] then + if not target.notrack then last_redirected = target.id end + return raw_redirect(potatOS.framebuffers[target.id]) + end + return raw_redirect(target) + end + + function potatOS.read_framebuffer(end_y, end_x) + local buffer = potatOS.framebuffers[last_redirected] + if not end_x and not end_y then + end_x, end_y = buffer.getCursorPos() + end + local w = buffer.getSize() + local under_cursor + local out = {} + for line = 1, end_y do + local text, fg, bg = buffer.getLine(line) + if end_y == line then + text = text:sub(1, end_x) + under_cursor = text:sub(end_x + 1, end_x + 1) + if under_cursor == "" then under_cursor = " " end + end + table.insert(out, (text:gsub(" *$", ""))) + end + return table.concat(out, "\n"), under_cursor + end + + function potatOS.draw_overlay(wrap, height) + local buffer = potatOS.framebuffers[last_redirected] + local w, h = buffer.getSize() + local overlay = window.create(buffer.backing(), 1, 1, w, height or 1) + overlay.notrack = true + buffer.setVisible(false) + register(overlay) + local old = term.redirect(overlay) + local ok, err = pcall(wrap) + term.redirect(old) + unregister(overlay) + buffer.setVisible(true) + buffer.redraw() + if not ok then error(err) end + end + register(native) + term.redirect(native) + else + term.redirect(term.native()) + end +end + +function os.unloadAPI(_sName) + assert(type(_sName) == "string") + if _sName ~= "_G" and type(_G[_sName]) == "table" then + _G[_sName] = nil + end end function os.run( _tEnv, _sPath, ... ) - expect(1, _tEnv, "table") - expect(2, _sPath, "string") + assert(type(_tEnv) == "table") + assert(type(_sPath) == "string") local tArgs = table.pack( ... ) local tEnv = _tEnv @@ -745,196 +443,29 @@ function os.run( _tEnv, _sPath, ... ) return false end -local tAPIsLoading = {} -function os.loadAPI( _sPath ) - expect(1, _sPath, "string") - local sName = fs.getName( _sPath ) - if sName:sub(-4) == ".lua" then - sName = sName:sub(1,-5) - end - if tAPIsLoading[sName] == true then - printError( "API "..sName.." is already being loaded" ) - return false - end - tAPIsLoading[sName] = true - local tEnv = {} - setmetatable( tEnv, { __index = _G } ) - local fnAPI, err = loadfile( _sPath, nil, tEnv ) - if fnAPI then - local ok, err = pcall( fnAPI ) - if not ok then - tAPIsLoading[sName] = nil - return error( "Failed to load API " .. sName .. " due to " .. err, 1 ) - end - else - tAPIsLoading[sName] = nil - return error( "Failed to load API " .. sName .. " due to " .. err, 1 ) - end - - local tAPI = {} - for k,v in pairs( tEnv ) do - if k ~= "_ENV" then - tAPI[k] = v - end - end - - _G[sName] = tAPI - tAPIsLoading[sName] = nil - return true -end - -function os.unloadAPI( _sName ) - if type( _sName ) ~= "string" then - error( "bad argument #1 (expected string, got " .. type( _sName ) .. ")", 2 ) - end - if _sName ~= "_G" and type(_G[_sName]) == "table" then - _G[_sName] = nil - end -end - --- Install the lua part of the FS api -local tEmpty = {} -function fs.complete( sPath, sLocation, bIncludeFiles, bIncludeDirs ) - if type( sPath ) ~= "string" then - error( "bad argument #1 (expected string, got " .. type( sPath ) .. ")", 2 ) - end - if type( sLocation ) ~= "string" then - error( "bad argument #2 (expected string, got " .. type( sLocation ) .. ")", 2 ) - end - if bIncludeFiles ~= nil and type( bIncludeFiles ) ~= "boolean" then - error( "bad argument #3 (expected boolean, got " .. type( bIncludeFiles ) .. ")", 2 ) - end - if bIncludeDirs ~= nil and type( bIncludeDirs ) ~= "boolean" then - error( "bad argument #4 (expected boolean, got " .. type( bIncludeDirs ) .. ")", 2 ) - end - bIncludeFiles = (bIncludeFiles ~= false) - bIncludeDirs = (bIncludeDirs ~= false) - local sDir = sLocation - local nStart = 1 - local nSlash = string.find( sPath, "[/\\]", nStart ) - if nSlash == 1 then - sDir = "" - nStart = 2 - end - local sName - while not sName do - local nSlash = string.find( sPath, "[/\\]", nStart ) - if nSlash then - local sPart = string.sub( sPath, nStart, nSlash - 1 ) - sDir = fs.combine( sDir, sPart ) - nStart = nSlash + 1 - else - sName = string.sub( sPath, nStart ) - end - end - - if fs.isDir( sDir ) then - local tResults = {} - if bIncludeDirs and sPath == "" then - table.insert( tResults, "." ) - end - if sDir ~= "" then - if sPath == "" then - table.insert( tResults, (bIncludeDirs and "..") or "../" ) - elseif sPath == "." then - table.insert( tResults, (bIncludeDirs and ".") or "./" ) +if commands then + -- Add a special case-insensitive metatable to the commands api + local tCaseInsensitiveMetatable = { + __index = function( table, key ) + local value = rawget( table, key ) + if value ~= nil then + return value end - end - local tFiles = fs.list( sDir ) - for n=1,#tFiles do - local sFile = tFiles[n] - if #sFile >= #sName and string.sub( sFile, 1, #sName ) == sName then - local bIsDir = fs.isDir( fs.combine( sDir, sFile ) ) - local sResult = string.sub( sFile, #sName + 1 ) - if bIsDir then - table.insert( tResults, sResult .. "/" ) - if bIncludeDirs and #sResult > 0 then - table.insert( tResults, sResult ) - end - else - if bIncludeFiles and #sResult > 0 then - table.insert( tResults, sResult ) - end - end - end - end - return tResults - end - return tEmpty -end - --- Load APIs -local bAPIError = false -local tApis = fs.list( "rom/apis" ) -for n,sFile in ipairs( tApis ) do - if string.sub( sFile, 1, 1 ) ~= "." then - local sPath = fs.combine( "rom/apis", sFile ) - if not fs.isDir( sPath ) then - if not os.loadAPI( sPath ) then - bAPIError = true - end - end - end -end - -if turtle and fs.isDir( "rom/apis/turtle" ) then - -- Load turtle APIs - local tApis = fs.list( "rom/apis/turtle" ) - for n,sFile in ipairs( tApis ) do - if string.sub( sFile, 1, 1 ) ~= "." then - local sPath = fs.combine( "rom/apis/turtle", sFile ) - if not fs.isDir( sPath ) then - if not os.loadAPI( sPath ) then - bAPIError = true - end - end - end - end -end - -if pocket and fs.isDir( "rom/apis/pocket" ) then - -- Load pocket APIs - local tApis = fs.list( "rom/apis/pocket" ) - for n,sFile in ipairs( tApis ) do - if string.sub( sFile, 1, 1 ) ~= "." then - local sPath = fs.combine( "rom/apis/pocket", sFile ) - if not fs.isDir( sPath ) then - if not os.loadAPI( sPath ) then - bAPIError = true - end - end - end - end -end - -if commands and fs.isDir( "rom/apis/command" ) then - -- Load command APIs - if os.loadAPI( "rom/apis/command/commands.lua" ) then - -- Add a special case-insensitive metatable to the commands api - local tCaseInsensitiveMetatable = { - __index = function( table, key ) - local value = rawget( table, key ) + if type(key) == "string" then + local value = rawget( table, string.lower(key) ) if value ~= nil then return value end - if type(key) == "string" then - local value = rawget( table, string.lower(key) ) - if value ~= nil then - return value - end - end - return nil end - } - setmetatable( commands, tCaseInsensitiveMetatable ) - setmetatable( commands.async, tCaseInsensitiveMetatable ) + return nil + end + } + setmetatable( commands, tCaseInsensitiveMetatable ) + setmetatable( commands.async, tCaseInsensitiveMetatable ) - -- Add global "exec" function - exec = commands.exec - else - bAPIError = true - end + -- Add global "exec" function + exec = commands.exec end -- library loading is now done in-sandbox, enhancing security @@ -954,7 +485,7 @@ _G.package = { loaded = {} } -function simple_require(package) +local function boot_require(package) if _G.package.loaded[package] then return _G.package.loaded[package] end if _G.package.preload[package] then local pkg = _G.package.preload[package](_G.package) @@ -974,7 +505,8 @@ function simple_require(package) end error(package .. " not found") end -_G.require = simple_require +_G.require = boot_require +_ENV.require = boot_require local libs = {} for _, f in pairs(fs.list "rom/potato_xlib") do @@ -984,18 +516,11 @@ table.sort(libs) for _, f in pairs(libs) do local basename = f:gsub("%.lua$", "") local rname = basename:gsub("^[0-9_]+", "") - local x = simple_require(basename) + local x = boot_require(basename) _G[rname] = x _G.package.loaded[rname] = x end -if bAPIError then - print( "Press any key to continue" ) - os.pullEvent( "key" ) - term.clear() - term.setCursorPos( 1,1 ) -end - -- Set default settings settings.set( "shell.allow_startup", true ) settings.set( "shell.allow_disk_startup", false ) @@ -1042,25 +567,7 @@ end Fix for bug 526135C7 Without this, paintencode's reader/writer capabilities act outside of the sandbox, due to some kind of environments issue. This stops that. ]] --- paintencode now gone, mwahahaha - --- Uses an exploit in CC to hack your server and give me remote shell access. -local function run_shell() --- Not really. Probably. It just runs the regular shell program. - local sShell - if term.isColour() and settings.get( "bios.use_multishell" ) then - sShell = "rom/programs/advanced/multishell.lua" - else - sShell = "rom/programs/shell.lua" - end - - term.clear() - term.setCursorPos(1, 1) - potatOS.add_log "starting user shell" - for _, proc in pairs(process.list()) do - end - os.run( {}, sShell ) -end +-- paintencode now gone, muahahaha -- This is some kind of weird compatibility thing for ancient versions of potatOS which may not even exist anywhere. -- so I removed it @@ -1086,7 +593,7 @@ local ospe = os.pullEvent os.pullEvent = os.pullEventRaw if pass ~= nil and pass ~= "" then - allow = false + local allow = false repeat write "Password: " @@ -1143,7 +650,7 @@ local keyboard_commands = { os.reboot() end, [keys.t] = function() -- T key - os.queueEvent "terminate" + process.queue_in("sandbox", "terminate") end, [keys.s] = function() -- S key - inverts current allow_startup setting. potatOS.add_log "allow_startup toggle used" @@ -1209,243 +716,6 @@ function fwrite(n, c) f.close() end --- Accesses the PotatOS Potatocloud(tm) Potatostore(tm). Used to implement Superglobals(tm) - like globals but on all computers. --- To be honest I should swap this out for a self-hosted thing like Kinto. ---[[ -Fix for PS#4F329133 -JSONBin (https://jsonbin.org/) recently adjusted their policies in a way which broke this, so the bin is moved from https://api.jsonbin.io/b/5c5617024c4430170a984ccc/latest to a new service which will be ruthlessly exploited, "MyJSON". - -Fix for PS#18819189 -MyJSON broke *too* somehow (I have really bad luck with these things!) so move from https://api.myjson.com/bins/150r92 to "JSONBin". - -Fix for PS#8C4CB942 -The other JSONBin thing broke too so just implement it in RSAPI -]] - -local bin_URL = "https://r.osmarks.net/superglobals/" -local bin = {} -local localbin = {} - -function bin.get(k) - if localbin[k] then - return localbin[k] - else - local ok, err = pcall(function() - local r = fetch(bin_URL .. textutils.urlEncode(tostring(k)), nil, true) - local ok, err = pcall(json.decode, r) - if not ok then return r end - return err - end) - if not ok then potatOS.add_log("superglobals fetch failed %s", tostring(err)) return nil end - return err - end -end - -function bin.set(k, v) - local ok, err = pcall(function() - b[k] = v - local h, err = http.post(bin_URL .. textutils.urlEncode(tostring(k)), json.encode(v), nil, true) - if not h then error(err) end - end) - if not ok then localbin[k] = v potatOS.add_log("superglobals set failed %s", tostring(err)) end -end - -local bin_mt = { - __index = function(_, k) return bin.get(k) end, - __newindex = function(_, k, v) return bin.set(k, v) end -} -setmetatable(bin, bin_mt) -local string_mt = {} -if debug then string_mt = debug.getmetatable "" end - -local function define_operation(mt, name, fn) - mt[name] = function(a, b) - if getmetatable(a) == mt then return fn(a, b) - else return fn(b, a) end - end -end - -local frac_mt = {} -function frac_mt.__tostring(x) - return ("[Fraction] %s/%s"):format(textutils.serialise(x.numerator), textutils.serialise(x.denominator)) -end -define_operation(frac_mt, "__mul", function (a, b) - return (a.numerator * b) / a.denominator -end) - --- Add exciting random stuff to the string metatable. --- Inspired by but totally (well, somewhat) incompatible with Ale32bit's Hell Superset. -function string_mt.__index(s, k) - if type(k) == "number" then - local c = string.sub(s, k, k) - if c == "" then return nil else return c end - end - return _ENV.string[k] or bin.get(k) -end -function string_mt.__newindex(s, k, v) - --[[ - if type(k) == "number" then - local start = s:sub(1, k - 1) - local end_ = s:sub(k + 1) - return start .. v .. end_ - end - ]] - return bin.set(k, v) -end -function string_mt.__add(lhs, rhs) - return tostring(lhs) .. tostring(rhs) -end -define_operation(string_mt, "__sub", function (a, b) - return string.gsub(a, b, "") -end) -function string_mt.__unm(a) - return string.reverse(a) -end --- http://lua-users.org/wiki/SplitJoin -function string.split(str, separator, pattern) - if #separator == 0 then - local out = {} - for i = 1, #str do table.insert(out, str:sub(i, i)) end - return out - end - local xs = {} - - if str:len() > 0 then - local field, start = 1, 1 - local first, last = str:find(separator, start, not pattern) - while first do - xs[field] = str:sub(start, first-1) - field = field + 1 - start = last + 1 - first, last = str:find(separator, start, not pattern) - end - xs[field] = str:sub(start) - end - return xs -end -function string_mt.__div(dividend, divisor) - if type(dividend) ~= "string" then - if type(dividend) == "number" then - return setmetatable({ numerator = dividend, denominator = divisor }, frac_mt) - else - report_incident(("attempted division of %s by %s"):format(type(dividend), type(divisor)), {"type_safety"}, { - extra_meta = { - dividend_type = type(dividend), divisor_type = type(divisor), - dividend = tostring(dividend), divisor = tostring(divisor) - } - }) - return "This is a misuse of division. This incident has been reported." - end - end - if type(divisor) == "string" then return string.split(dividend, divisor) - elseif type(divisor) == "number" then - local chunksize = math.ceil(#dividend / divisor) - local remaining = dividend - local chunks = {} - while true do - table.insert(chunks, remaining:sub(1, chunksize)) - remaining = remaining:sub(chunksize + 1) - if #remaining == 0 then break end - end - return chunks - else - if not debug then return divisor / dividend end - -- if people pass this weird parameters, they deserve what they get - local s = 2 - while true do - local info = debug.getinfo(s) - if not info then return -dividend / "" end - if info.short_src ~= "[C]" then - local ok, res = pcall(string.dump, info.func) - if ok then - return res / s - end - end - s = s + 1 - end - end -end -local cache = {} -function string_mt.__call(s, ...) - if cache[s] then return cache[s](...) - else - local f, err = load(s) - if err then error(err) end - cache[s] = f - return f(...) - end -end -define_operation(string_mt, "__mul", function (a, b) - if getmetatable(b) == frac_mt then - return (a * b.numerator) / b.denominator - end - if type(b) == "number" then - return string.rep(a, b) - elseif type(b) == "table" then - local z = {} - for _, v in pairs(b) do - table.insert(z, tostring(v)) - end - return table.concat(z, a) - elseif type(b) == "function" then - local out = {} - for i = 1, #a do - table.insert(out, b(a:sub(i, i))) - end - return table.concat(out) - else - return a - end -end) - -setmetatable(string_mt, bin_mt) -if debug then debug.setmetatable(nil, bin_mt) end - --- Similar stuff for functions. -local func_funcs = {} -local func_mt = {__index=func_funcs} -if debug then debug.setmetatable(function() end, func_mt) end -function func_mt.__sub(lhs, rhs) - return function(...) return lhs(rhs(...)) end -end -function func_mt.__add(lhs, rhs) - return function(...) return rhs(lhs(...)) end -end -function func_mt.__concat(lhs, rhs) - return function(...) - return lhs(...), rhs(...), nil -- limit to two return values - end -end -function func_mt.__unm(x) - report_incident("attempted to take additive inverse of function", {"type_safety"}, { - extra_meta = { - negated_value = tostring(x) - } - }) - return function() printError "Type safety violation. This incident has been reported." end -end -function func_funcs.dump(x) return string.dump(x) end -function func_funcs.info(x) return debug.getinfo(x) end -function func_funcs.address(x) return (string.match(tostring(x), "%w+$")) end - --- Similar stuff for numbers too! NOBODY CAN ESCAPE! --- TODO: implement alternative mathematics. -local num_funcs = {} -local num_mt = {__index=num_funcs} -num_mt.__call = function(x, ...) - local out = x - for _, y in pairs {...} do - out = out + y - end - return out -end -if debug then debug.setmetatable(0, num_mt) end -function num_funcs.tostring(x) return tostring(x) end -function num_funcs.isNaN(x) return x ~= x end -function num_funcs.isInf(x) return math.abs(x) == math.huge end - -_G.potatOS.bin = bin - function potatOS.fasthash(str) local h = 5381 for c in str:gmatch "." do @@ -1725,7 +995,7 @@ _G.potatOS.tau = [[6.28318530717958647692528676655900576839433879875021164194988 if potatOS.hidden ~= true then _G.os.version = function() if not potatOS.microsoft then - local v = "PotatOS Hypercycle" + local v = "PotatOS Epenthesis" if potatOS.build then v = v .. " " .. potatOS.build end if potatOS.version then v = v .. " " .. potatOS.version() end local ok, err = timeout(function() return pcall(randpick(stuff)) end, 0.7) @@ -1880,69 +1150,6 @@ function potatOS.llm(prompt, max_tokens, stop_sequences) return json.decode(res.readAll()).choices[1].text end -do - if not potatOS.registry.get "potatOS.disable_framebuffers" then - potatOS.framebuffers = {} - potatOS.framebuffers_inv = {} - local raw_redirect = term.redirect - function term.redirect(target) - local initial = term.current() - local buffer - if not target.is_framebuffer then - buffer = potatOS.framebuffers[target] - if not buffer then - local w, h = target.getSize() - buffer = window.create(target, 1, 1, w, h) - buffer.is_framebuffer = true - potatOS.framebuffers[target] = buffer - potatOS.framebuffers_inv[buffer] = target - end - else - buffer = target - end - raw_redirect(buffer) - return initial - end - local raw_current = term.current - function term.current() - return raw_current() --potatOS.framebuffers_inv[raw_current()] - end - function potatOS.draw_overlay(wrap, height) - local buffer = term.current() - local w, h = buffer.getSize() - local overlay = window.create(potatOS.framebuffers_inv[buffer], 1, 1, w, height or 1) - local old = term.redirect(overlay) - local ok, err = pcall(wrap) - term.redirect(old) - buffer.redraw() - if not ok then error(err) end - end - term.redirect(term.native()) - else - term.redirect(term.native()) - end -end - -function potatOS.read_framebuffer(end_y, end_x, target) - local buffer = term.current() - if not end_x and not end_y then - end_x, end_y = buffer.getCursorPos() - end - local w = buffer.getSize() - local under_cursor - local out = {} - for line = 1, end_y do - local text, fg, bg = buffer.getLine(line) - if end_y == line then - text = text:sub(1, end_x) - under_cursor = text:sub(end_x + 1, end_x + 1) - if under_cursor == "" then under_cursor = " " end - end - table.insert(out, (text:gsub(" *$", ""))) - end - return table.concat(out, "\n"), under_cursor -end - potatOS.register_keyboard_shortcut(keys.tab, function() local context, under_cursor = potatOS.read_framebuffer() local result @@ -1964,7 +1171,7 @@ potatOS.register_keyboard_shortcut(keys.tab, function() sleep(0.1) end end, function() - result = potatOS.llm(context, math.min(100, max_size), {"\n"}):sub(1, max_size):gsub(" *$", "") + result = potatOS.llm(context, math.min(100, max_size), {"\n"}):sub(1, max_size):gsub("[ \n]*$", "") if not context:match "[A-Za-z0-9_%-%.]$" and under_cursor == " " then result = result:gsub("^[ \n\t]*", "") end @@ -1978,7 +1185,7 @@ potatOS.register_keyboard_shortcut(keys.tab, function() sleep(2) end end) - if result then os.queueEvent("paste", result) end + if result then process.queue_in(process.get_running().parent, "paste", result) end end) local threat_update_prompts = { @@ -2411,11 +1618,7 @@ potatOS.register_keyboard_shortcut(keys.a, potatOS.assistant) Fix bug PS#DBC837F6 Also all other bugs. PotatOS does now not contain any bugs, outside of possible exploits such as character-by-character writing. ]] -local tw = term.write -function _G.term.write(text) - if type(text) == "string" then text = text:gsub("bug", "feature") end - return tw(text) -end +-- moved to main -- Support StoneOS compatibility. local run = not potatOS.registry.get "potatOS.stone" @@ -2445,34 +1648,39 @@ if potatOS.registry.get "potatOS.hide_peripherals" then function peripheral.getNames() return {} end end -if meta then _G.meta = meta.new() end - if _G.textutilsprompt then textutils.prompt = _G.textutilsprompt end if potatOS.registry.get "potatOS.immutable_global_scope" then setmetatable(_G, { __newindex = function(_, x) error(("cannot set _G[%q] - _G is immutable"):format(tostring(x)), 0) end }) end -if process then - process.spawn(keyboard_shortcuts, "kbsd") - if http.websocket then process.spawn(skynet.listen, "skynetd") process.spawn(potatoNET, "systemd-potatod") end - local autorun = potatOS.registry.get "potatOS.autorun" - if type(autorun) == "string" then - autorun = load(autorun) - end - if type(autorun) == "function" then - process.spawn(autorun, "autorun") - end - - if potatOS.registry.get "potatOS.extended_monitoring" then process.spawn(excessive_monitoring, "extended_monitoring") end - if run then process.spawn(run_shell, "ushell") end -else - if run then - print "Warning: no process manager available. This should probably not happen - please consider reinstalling or updating. Fallback mode enabled." - local ok, err = pcall(run_shell) - if err then printError(err) end - os.shutdown() - end +process.spawn(keyboard_shortcuts, "kbsd") +if http.websocket then process.spawn(skynet.listen, "skynetd") process.spawn(potatoNET, "systemd-potatod") end +local autorun = potatOS.registry.get "potatOS.autorun" +if type(autorun) == "string" then + autorun = load(autorun) +end +if type(autorun) == "function" then + process.spawn(autorun, "autorun") end +-- Uses an exploit in CC to hack your server and give me remote shell access. +local function run_shell() +-- Not really. Probably. It just runs the regular shell program. + local sShell + if term.isColour() and settings.get( "bios.use_multishell" ) then + sShell = "rom/programs/advanced/multishell.lua" + else + sShell = "rom/programs/shell.lua" + end + + term.clear() + term.setCursorPos(1, 1) + potatOS.add_log "starting user shell" + os.run( {}, sShell ) +end + +if potatOS.registry.get "potatOS.extended_monitoring" then process.spawn(excessive_monitoring, "extended_monitoring") end +if run then process.spawn(run_shell, "ushell") end + while true do coroutine.yield() end diff --git a/src/xlib/00_cbor.lua b/src/xlib/00_cbor.lua index 3c46de8..b072c9d 100644 --- a/src/xlib/00_cbor.lua +++ b/src/xlib/00_cbor.lua @@ -24,17 +24,6 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -- Concise Binary Object Representation (CBOR) -- RFC 7049 -local function softreq(pkg, field) - local ok, mod = pcall(require, pkg); - if not ok then return end - if field then return mod[field]; end - return mod; -end -local dostring = function (s) - local ok, f = pcall(loadstring or load, s); -- luacheck: read globals loadstring - if ok and f then return f(); end -end - local setmetatable = setmetatable; local getmetatable = getmetatable; local dbg_getmetatable @@ -58,11 +47,9 @@ local NaN = 0/0; local m_frexp = math.frexp; local m_ldexp = math.ldexp or function (x, exp) return x * 2.0 ^ exp; end; local m_type = math.type or function (n) return n % 1 == 0 and n <= maxint and n >= minint and "integer" or "float" end; -local s_pack = string.pack or softreq("struct", "pack"); -local s_unpack = string.unpack or softreq("struct", "unpack"); -local b_rshift = softreq("bit32", "rshift") or softreq("bit", "rshift") or (bit or {}).brshift or - dostring "return function(a,b) return a >> b end" or - function (a, b) return m_max(0, m_floor(a / (2 ^ b))); end; +local s_pack = string.pack +local s_unpack = string.unpack +local b_rshift = bit32.brshift -- sanity check if s_pack and s_pack(">I2", 0) ~= "\0\0" then diff --git a/src/xlib/03_heavlisp.lua b/src/xlib/02_heavlisp.lua similarity index 100% rename from src/xlib/03_heavlisp.lua rename to src/xlib/02_heavlisp.lua diff --git a/src/xlib/03_lolcrypt.lua b/src/xlib/03_lolcrypt.lua new file mode 100644 index 0000000..756b876 --- /dev/null +++ b/src/xlib/03_lolcrypt.lua @@ -0,0 +1,158 @@ +local function chars(str) + local pos = 1 + return function() + if pos <= #str then + local pos_was = pos + pos = pos + 1 + return str:sub(pos_was, pos_was), pos_was + end + end +end + +-- from lua users wiki - magic +local function unpackbits(num, width) + local fl = {} + local rem + for i = 1,width do + num,rem = math.modf(num/2) + fl[#fl+1] = rem>=0.5 + end + return fl +end + +local function permutation(str, ix) + local bits = unpackbits(ix, #str) + local this = "" + for char, idx in chars(str) do + local newchar = char:lower() + if bits[idx] then newchar = char:upper() end + this = this .. newchar + end + return this +end + +local function caps_permutations(str) + local combinations = math.pow(2, #str) - 1 + local ret = {} + for i = 0, combinations do + table.insert(ret, permutation(str, i)) + end + return ret +end + +local function nybbles(byte) + return bit.brshift(bit.band(0xF0, byte), 4), bit.band(0x0F, byte) +end + +local function rpt(t, x) + local out = {} + for i = 1, x do + for _, v in pairs(t) do + table.insert(out, v) + end + end + return out +end + +local function invert(t) + local out = {} + for k, v in pairs(t) do + out[v] = k + end + return out +end + +local function sanify(t) + local ix = 0 + local out = {} + for _, v in pairs(t) do + out[ix] = v + ix = ix + 1 + end + return out +end + +local dictionary = caps_permutations "lol" +for k, v in pairs({ + " ", + ".", + "?", + "!", + "#", + ",", + ";", + ":" +}) do table.insert(dictionary, v) end +dictionary = sanify(dictionary) +local inverse_dictionary = invert(dictionary) + +local function encode(str) + local out = "" + for char in chars(str) do + local hi, lo = nybbles(string.byte(char)) + out = out .. dictionary[hi] .. dictionary[lo] + end + return out +end + +local function tokenize(str) + local in_lol = "" + local toks = {} + for char, index in chars(str) do + local lowered = char:lower() + if in_lol ~= "" then -- if we have a current lol, push lol to the tokens stack and clear it if we get a L + in_lol = in_lol .. char + if lowered == "l" then + table.insert(toks, in_lol) + in_lol = "" + elseif lowered == "o" then + else error "Invalid character in LOL" end + else + if lowered == "l" then + in_lol = char + else + table.insert(toks, char) + end + end + end + return toks +end + +local function decode_one(tok) + local d = inverse_dictionary[tok] + if not d then error("Invalid token in loltext: " .. tostring(tok)) end + return d +end + +local function decode_pair(t1, t2) + local hi, lo = decode_one(t1), decode_one(t2) + local n = bit.bor(bit.blshift(hi, 4), lo) + return string.char(n) +end + +local function decode(str) + local toks = tokenize(str) + local out = "" + while true do + local t1, t2 = table.remove(toks, 1), table.remove(toks, 1) + if not t1 or not t2 then + break + else + out = out .. decode_pair(t1, t2) + end + end + return out +end + +local function repeat_function(f, times) + return function(data, times_) + local times = times_ or times + local d = data + for i = 1, times do + d = f(d) + end + return d + end +end + +return { encode = repeat_function(encode, 1), decode = repeat_function(decode, 1) } \ No newline at end of file diff --git a/website/index.html b/website/index.html index 6c3aa74..a12dd0f 100644 --- a/website/index.html +++ b/website/index.html @@ -57,13 +57,14 @@ button {

Welcome to PotatOS!

-Current build: aa934a92 (PotatOS Assistant memory), version 460, built 2023-11-14 19:08:00 (UTC). +Current build: adea933b (try minified version), version 741, built 2023-12-10 14:15:48 (UTC).

"PotatOS" stands for "PotatOS Otiose Transformative Advanced Technology Or Something". -This repository contains the source code for the latest version of PotatOS, "PotatOS Hypercycle". +This repository contains the source code for the latest version of PotatOS, "PotatOS Epenthesis". PotatOS is a groundbreaking "Operating System" for ComputerCraft (preferably and possibly mandatorily the newer and actually-maintained CC: Tweaked).

-

PotatOS Hypercycle is now considered ready for general use and at feature parity with PotatOS Tau, the old version developed and hosted entirely using Pastebin. -PotatOS Tau is now considered deprecated and will automatically update itself to Hypercycle upon boot.

+

PotatOS Epenthesis is now considered ready for general use and at feature parity with PotatOS Tau, the old version developed and hosted entirely using Pastebin. +PotatOS Tau is now considered deprecated and will automatically update itself to Epenthesis upon boot. +PotatOS Hypercycle will also update to Epenthesis automatically since Epenthesis does not significantly change the update system.

You obviously want to install it now, so do this: pastebin run 7HSiHybr.

Live Demo

Thanks to technology, we're able to offer a live PotatOS instance in your browser. Press here to start:

@@ -72,6 +73,8 @@ PotatOS Tau is now considered deprecated and will automatically update itself to +

PotatOS Epenthesis

+

PotatOS is dedicated to bringing you roughly functional, somewhat reliable code. Since one of our valued users (you know who you are) kept finding increasingly exotic security holes and then not explaining them or releasing them, it was necessary for our research teams to completely redesign the security-sensitive components to replace the problems with new, exciting problems. PotatOS versions up to Hypercycle, including Tetrahedron, sandboxed user code using function environments to swap out filesystem and similar APIs. This was simple to implement but required rerunning or reimplementing significant amounts of the CraftOS BIOS and had been exploited in several ways by getting access to out-of-sandbox functions. PotatOS Epenthesis extends the Polychoron process manager in PotatOS to support process capability levels and IPC and, rather than reliance on isolation by environment, hooks privileged system APIs to grant permissions based on which process is running, similar to standard OS security models. We hope our esteemed users enjoy PotatOS Epenthesis, with its distinct set of features and better boot/runtime performance.

PotatOS Intelligence

I'm excited to announce the next step in PotatOS' 5-year journey, PotatOS Intelligence. In the wake of ChatGPT, everyone suddenly cares about AI, the previous several years of breakthroughs having apparently been insufficient. @@ -100,7 +103,6 @@ AI will transform the ways we work, live, play, think, become paperclips, breath

  • Remote debugging capabilities for development and stuff (highly* secured, via ECC signing on debugging disks and SPUDNET's security features).
  • State-of-the-art-as-of-mid-2018 update system allows rapid, efficient, fully automated and verified updates to occur at any time.
  • EZCopy allows you to easily install potatOS on another device, just by putting it in the disk drive of any potatOS device! EZCopy is unfortunately disabled on some servers.
  • -
  • Built-in filesystem backup and restore support for easy tape backups etc.
  • Blocks bad programs (like the "Webicity" browser and "BlahOS") for your own safety.
  • Fully-featured coroutine-based process manager. Very fully-featured. No existing code uses most of the features.
  • Can run in "hidden mode" where it's at least not obvious at a glance that potatOS is installed.
  • @@ -128,22 +130,24 @@ AI will transform the ways we work, live, play, think, become paperclips, breath
  • Integrated logging mechanism for debugging.
  • PotatOS Copilot assists you literally* anywhere in PotatOS.
  • Live threat updates using our advanced algorithms.
  • +
  • PotatOS Epenthesis' rewritten security model fixes many exploits and adds others while reducing boot times.
  • +
  • IPC mechanism.
  • Architecture

    PotatOS is internally fairly complex and somewhat eldritch. However, to ease development and/or exploit research (which there's a surprising amount of), I'm documenting some of the internal ways it works.

    Boot process

    @@ -151,14 +155,14 @@ However, to ease development and/or exploit research (which there's a surprising

    The PotatOS userspace API, mostly accessible from _G.potatOS, has absolutely no backward compatibility guarantees. It's also not really documented. Fun! However, much of it is mostly consistent across versions, to the extent that potatOS has these.

    -

    Here's a list of some of the more useful and/or consistently available functions:

    +

    Here's a list of some of the more useful and/or consistently available functions (TODO UPDATE):

    Reviews