ARES-SDK/ARES/application/db.lsl

493 lines
16 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/* =========================================================================
*
* Nanite Systems Advanced Research Encapsulation System
*
* Copyright (c) 20222024 Nanite Systems Corporation
*
* =========================================================================
*
* Database Utility
*
* This program is covered under the terms of the ARES Software Copyright
* License, Section 2 (ASCL-ii). Although it appears in ARES as part of
* commercial software, it may be used as the basis of derivative,
* non-profit works that retain a compatible license. Derivative works of
* ASCL-ii software must retain proper attribution in documentation and
* source files as described in the terms of the ASCL. Furthermore, they
* must be distributed free of charge and provided with complete, legible
* source code included in the package.
*
* To see the full text of the ASCL, type 'help license' on any standard
* ARES distribution, or visit http://nanite-systems.com/ASCL for the
* current version.
*
* DISCLAIMER
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS
* IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED.
*
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY
* DAMAGES HOWEVER CAUSED ON ANY THEORY OF LIABILITY ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
* DAMAGE.
*
* =========================================================================
*
*/
#include <ARES/a>
#define FILE_STEP_SIZE 10
#include <ARES/api/file.h.lsl>
#include <ARES/api/auth.h.lsl>
#define CLIENT_VERSION "1.2.1"
#define CLIENT_VERSION_TAGS "beta"
key dbload_q;
string read_buffer;
/*
replaces (string) number with (integer)number
and (string)\"number\" with (string)number
*/
list process_keyname(list keyname) {
integer ki = count(keyname);
while(ki--) {
string ks = gets(keyname, ki);
if((string)((integer)ks) == ks)
keyname = alter(keyname, [(integer)ks], ki, ki);
else if(llOrd(ks, 0) == 0x22 && llOrd(ks, LAST) == 0x22)
keyname = alter(keyname, [substr(ks, 1, -2)], ki, ki);
}
return keyname;
}
string db_set(string section, list keyname, string new_value) {
string old_value = getdbl(section, keyname);
setdbl(section, keyname, new_value);
if(old_value == JSON_INVALID)
old_value = "(undefined)";
else if(strlen(old_value) > 100)
old_value = "(" + (string)strlen(old_value) + " bytes)";
if(strlen(new_value) > 100)
new_value = "(" + (string)strlen(new_value) + " bytes)";
if(count(keyname) == 0)
return "overwrote ENTIRE section " + section + ": " + old_value + " --> " + new_value;
else
return "modified " + section + " setting " + concat(keyname, ".") + ": " + old_value + " --> " + new_value;
}
string db_show_2(string value, integer sp) {
if(llOrd(value, 0) == 0x7b && llOrd(value, LAST) == 0x7d) { // { and }
list keys = jskeys(value);
integer kmax = count(keys);
if(kmax == 0) {
if(strlen(value) > 100)
return "(" + (string)strlen(value) + " bytes)";
else
return value;
}
string sps = " ";
if(sp) {
integer spi = sp;
while(spi--)
sps += " ";
}
list vout;
integer ki = 0;
while(ki < kmax) {
string k = gets(keys, ki);
string subval = getjs(value, [k]);
if(llGetFreeMemory() > 2000)
vout += k + ": " + db_show_2(subval, sp + 1);
else
vout += k + ": (out of memory)";
++ki;
}
return "\n" + sps + concat(vout, "\n" + sps);
} else {
return value;
}
}
main(integer src, integer n, string m, key outs, key ins, key user) {
if(n == SIGNAL_INVOKE) {
list argv = split(m, " ");
string action = gets(argv, 1);
integer argc = count(argv);
string msg;
integer allowed = sec_check(user, "database", outs, m, m);
if(allowed == DENIED) {
print(outs, user, "secondlife:///app/agent/" + (string)user + "/about is not authorized to access the database directly.");
return;
} else if(allowed == PENDING) {
return;
}
list parts;
string section;
list keyname;
integer user_rank = (integer)getdbl("security", ["user", user]);
if(action == "clone") {
parts = splitnulls(gets(argv, 2), ".");
section = gets(parts, 0);
list parts2 = splitnulls(gets(argv, 3), ".");
string section2 = gets(parts2, 0);
if(section2 == "security" && user_rank != SEC_OWNER) {
jump security_fail;
} else {
string text = getdbl(section, process_keyname(delitem(parts, 0)));
if(text == JSON_INVALID) {
msg = "Could not clone from " + gets(argv, 2) + " to " + gets(argv, 3) + ": entry " + gets(argv, 2) + " does not exist.";
} else {
setdbl(section2, process_keyname(delitem(parts2, 0)), text);
integer tl = strlen(text);
msg = "Cloned data from " + gets(argv, 2) + " to " + gets(argv, 3) + ".";
if(tl != 1)
msg += " (" + (string)tl + " bytes)";
else
msg += " (1 byte)";
}
}
} else if(action == "load") {
if(user_rank != SEC_OWNER) {
msg = "Only owners may perform database imports.";
} else if(gets(argv, 2) == "cancel") {
msg = "Aborted.";
resolve((integer)getjs(tasks_queue, [dbload_q, FILE_R]));
task_end(dbload_q);
dbload_q = "";
} else if(dbload_q) {
msg = "A database import is already in progress; run 'db load cancel' to abort.";
} else {
read_buffer = "";
string dbload_fn = gets(argv, 2);
dbload_q = fopen(outs, ins, user, dbload_fn);
msg = "Importing " + dbload_fn;
}
} else if(action == "delete" || action == "remove") {
parts = splitnulls(gets(argv, 2), ".");
section = gets(parts, 0);
if(section == "security" && user_rank != SEC_OWNER)
jump security_fail;
keyname = process_keyname(delitem(parts, 0));
if(getdbl(section, keyname) != JSON_INVALID) {
deletedbl(section, keyname);
msg = "Deleted " + concat(keyname, ".") + " in section " + section;
} else {
msg = concat(keyname, ".") + " in section " + section + " not present";
}
} else if(action == "toggle") {
parts = splitnulls(gets(argv, 2), ".");
section = gets(parts, 0);
if(section == "security" && user_rank != SEC_OWNER)
jump security_fail;
keyname = process_keyname(delitem(parts, 0));
integer val = !(integer)getdbl(section, keyname);
if(val)
msg = "Enabled ";
else
msg = "Disabled ";
setdbl(section, keyname, (string)val);
msg += gets(argv, 2);
} else if(action == "drop") {
section = gets(argv, 2);
if(section == "security" && user_rank != SEC_OWNER)
jump security_fail;
llLinksetDataDelete(section);
msg = "Deleted ENTIRE section " + section;
} else if(action == "set") {
parts = splitnulls(gets(argv, 2), ".");
section = gets(parts, 0);
if(section == "security" && user_rank != SEC_OWNER)
jump security_fail;
keyname = process_keyname(delitem(parts, 0));
string new_value = concat(delrange(argv, 0, 2), " ");
msg = db_set(section, keyname, new_value);
} else if(action == "append") {
parts = splitnulls(gets(argv, 2), ".");
section = gets(parts, 0);
if(section == "security" && user_rank != SEC_OWNER)
jump security_fail;
keyname = process_keyname(delitem(parts, 0)) + [JSON_APPEND];
string new_value = concat(delrange(argv, 0, 2), " ");
setdbl(section, keyname, new_value);
msg = "Added '" + new_value + "' to section " + section + " key " + concat(keyname, ".");
} else if(action == "") {
list all_keys = llLinksetDataListKeys(0, 0);
integer aki = count(all_keys);
// limited regex support can't handle ^(?!p:).+$
while(aki--)
if(substr(gets(all_keys, aki), 0, 1) == "p:")
all_keys = delitem(all_keys, aki);
msg = "Stored sections: " + concat(all_keys, ", ")
+ "\nLSD status: "
+ (string)llLinksetDataCountKeys() + " keys total, "
+ (string)llLinksetDataAvailable() + " bytes free.";
} else if(action == "u" || action == "usage") {
// list all_keys = llLinksetDataListKeys(0, 0);
integer akmax;
integer aki = akmax = llLinksetDataCountKeys(); // count(all_keys);
list fields;
while(aki--) {
string kn = gets(llLinksetDataListKeys(aki, 1), 0);
// msg += " - " + kn + " " + (string)strlen(llLinksetDataRead(kn)) + "\n";
fields += [kn, strlen_byte_inline(llLinksetDataRead(kn))];
}
fields = llListSortStrided(fields, 2, 1, TRUE);
aki = akmax;
while(aki--) {
integer L = geti(fields, (aki << 1) + 1);
string line = " - " + gets(fields, (aki << 1)) + " " + (string)L;
if(L != 1)
line += " bytes";
else
line += " byte";
// fields = alter(fields, [line], (aki << 1), (aki << 1) + 1);
fields = delrange(fields, (aki << 1), (aki << 1) + 1);
print(outs, user, line);
llSleep(0.05);
}
msg = /*concat(fields, "\n")
+ */"\nLSD status: "
+ (string)llLinksetDataCountKeys() + " keys total, "
+ (string)llLinksetDataAvailable() + " bytes free.";
} else if(action == "show") {
parts = splitnulls(gets(argv, 2), ".");
section = gets(parts, 0);
keyname = process_keyname(delitem(parts, 0));
// msg = db_show(section, keyname);
jump db_show;
} else if(action == "json") {
parts = splitnulls(gets(argv, 2), ".");
section = gets(parts, 0);
keyname = process_keyname(delitem(parts, 0));
msg = getdbl(section, keyname);
if(msg == JSON_INVALID)
msg = "(undefined)";
} else {
parts = splitnulls(action, ".");
section = gets(parts, 0);
if(llLinksetDataRead(section) != "") {
keyname = process_keyname(delitem(parts, 0));
if(argc > 2) {
if(count(keyname) > 0) {
if(section == "security" && user_rank != SEC_OWNER)
jump security_fail;
string new_value = concat(delrange(argv, 0, 1), " ");
msg = db_set(section, keyname, new_value);
} else {
msg = "Did you mean to do that? For safety, you must use 'db set' to replace an entire section.";
}
} else {
// msg = db_show(section, keyname);
jump db_show;
}
} else {
msg = PROGRAM_NAME + ": No section: " + action + "; see 'help db' for syntax.";
}
}
jump security_pass;
@db_show;
// (section and keyname are already populated)
string value;
if(count(keyname)) {
value = getdbl(section, keyname);
} else {
value = llLinksetDataRead(section);
}
if(value == JSON_INVALID) {
value = "(undefined)";
} else if(strlen(value) > 800 && count(jskeys(value))) {
value = "(keys only) " + concat(jskeys(value), ", ");
} else {
value = db_show_2(value, 0);
}
if(!count(keyname))
value = "(entire section) " + value;
msg = concat([section] + keyname, ".") + " " + value;
jump security_pass;
@security_fail;
msg = "Only the unit's owner may directly modify the 'security' section of the database.";
@security_pass;
if(msg != "")
print(outs, user, msg);
return;
} else if(n == SIGNAL_NOTIFY) {
if(m == PROGRAM_NAME + " file") {
if(ins == dbload_q) {
user = getjs(tasks_queue, [dbload_q, FILE_USER]);
outs = getjs(tasks_queue, [dbload_q, FILE_OUTS]);
string fn = getjs(tasks_queue, [dbload_q, FILE_NAME]);
string unit = getjs(tasks_queue, [dbload_q, FILE_UNIT]);
string result = fread(dbload_q);
string msg;
list parts;
string section;
list keyname;
if(result == JSON_FALSE) {
msg = "No file: " + fn;
dbload_q = "";
} else if(result == JSON_TRUE) {
msg = "Loading " + getjs(tasks_queue, [dbload_q, FILE_LENGTH]) + " " + getjs(tasks_queue, [dbload_q, FILE_UNIT]);
} else {
list lines = split(result, "\n");
if(unit != "l") {
lines = alter(lines, [read_buffer + gets(lines, 0)], 0, 0);
if(getjs(tasks_queue, [dbload_q]) == JSON_INVALID) {
// the end of the file; last line can be trusted
read_buffer = "";
} else {
read_buffer = gets(lines, LAST);
lines = delitem(lines, LAST);
}
}
integer li = 0;
integer lmax = count(lines);
while(li < lmax) {
string m = llStringTrim(gets(lines, li++), STRING_TRIM_HEAD);
if(m != "" && substr(m, 0, 0) == "#") {
// skip comments
} else {
list argv = splitnulls(m, " ");
integer mode = 0; // set
string mode_name = llToUpper(gets(argv, 0));
if(mode_name == "DELETE" || mode_name == "-") {
mode = 1;
argv = delitem(argv, 0);
} else if(mode_name == "CREATE" || mode_name == "+") {
mode = 2;
argv = delitem(argv, 0);
} else if(mode_name == "APPEND" || mode_name == "++") {
mode = 3;
argv = delitem(argv, 0);
// echo(m);
} else if(mode_name == "MERGE" || mode_name == "+=") {
mode = 5;
argv = delitem(argv, 0);
} else if(mode_name == "SET" || mode_name == "=") {
mode = 0;
argv = delitem(argv, 0);
} else if(mode_name == "DROP") {
mode = 4;
argv = delitem(argv, 0);
} else if(mode_name == "ALTER") {
mode = 6;
argv = delitem(argv, 0);
}
string varkey = gets(argv, 0);
if(varkey != "") {
string varvalue = concat(delitem(argv, 0), " ");
keyname = split(varkey, ".");
section = gets(keyname, 0);
keyname = process_keyname(delitem(keyname, 0));
string section_data = llLinksetDataRead(section);
if(mode == 1 && section_data == "") {
// nothing to delete
} else if(mode == 4) {
if(section_data != "") {
msg += "Deleted ENTIRE section " + section + "\n";
}
llLinksetDataDelete(section);
} else {
if(section_data == "") {
section_data = "{}";
msg += "Created section: " + section + "\n";
}
if(mode == 1)
varvalue = JSON_DELETE;
else if(mode == 3)
keyname += [JSON_APPEND];
integer invalid = (getjs(section_data, keyname) == JSON_INVALID);
if(mode == 2 && invalid) {
// value already exists; not replacing
} else {
// perform the update in memory:
if(mode == 5) {
list keys = jskeys(varvalue);
integer ki = count(keys);
while(ki--) {
string k = gets(keys, ki);
section_data = setjs(section_data, keyname + [k], getjs(varvalue, [k]));
}
} else if((mode == 6 && invalid) || mode != 6) {
section_data = setjs(section_data, keyname, varvalue);
}
// apply the update if successful:
if(section_data != JSON_INVALID) {
// only commit on successful update:
llLinksetDataWrite(section, section_data);
} else if(mode != 1) {
msg += "Operation failed: " + m + "\n";
} // else DELETEs fail silently
}
}
}
}
}
if(getjs(tasks_queue, [dbload_q]) == JSON_INVALID) {
msg += "Finished database import of " + fn
+ "\nLSD status: " + (string)llLinksetDataCountKeys() + " sections, " + (string)llLinksetDataAvailable() + " bytes free.";
dbload_q = "";
}
}
if(msg != "")
print(outs, user, msg);
} else {
echo("[" + PROGRAM_NAME + "] file data offered via unexpected pipe: " + (string)ins);
}
}
} else if(n == SIGNAL_INIT) {
#ifdef DEBUG
echo("[" + PROGRAM_NAME + "] init event");
#endif
} else if(n == SIGNAL_UNKNOWN_SCRIPT) {
echo("[" + PROGRAM_NAME + "] failed to run '" + m + "' (kernel could not find the program specified)");
} else {
echo("[" + PROGRAM_NAME + "] unimplemented signal " + (string)n + ": " + m);
}
}
#include <ARES/program>