primfs_primdrive.lsl
///////////////////////////////////////////////////////////////////////////
//  Copyright (C) Wizardry and Steamworks 2014 - License: GNU GPLv3      //
//  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  //
//  rights of fair usage, the disclaimer and warranty conditions.        //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//                       PrimFS™ and PrimDrive™                          //
//         The Primitive Filesystem for the Primitive Hard Drive.        //
///////////////////////////////////////////////////////////////////////////
// PrimFS™ and PrimDrive™ introduce an unconventional hard-drive that is
// able to store data in three-dimensions instead of two. While
// conventional hard-drives use superposed disks, PrimDrive™ uses a cube
// that is physically sub-divided into smaller blocks called sectors.
//
// Storing data in three dimensions instead of two allows for some helpful
// features, namely:
//   - each sector can change its own color to indicate how much data
//      has been written to it and also how much free space is available
//   - by consequence, looking at the PrimDrive™ will immediatly give
//      an overview of the clustering of data.
//
// Wizardry and Steamworks uses key-value data as the underlying data
// representation. The simplicity is desirable such that sectors can hold
// as much data as possible without too much structure overhead - by
// contrast with llJson* functions, for example.
//
// Due to the fact that each block is a physical object some interesting
// qualities can be implicitly observed, for instance:
//   - defragmenting the drive could be a mechanical operation (optical?)
//      that the filesystem (PrimFS™) could not even bother with.
//   - The PrimDrive™ could be extended as easily as attaching sectors.
//   - The PrimDrive™ could be shrunk as easily as detaching sectors.
//   - Sectors could be shared between PrimDrive™s just by detaching and
//      attaching them to other PrimDrive™s. PrimFS™ always uses just as
//      much sectors space _as needed_ such that it is easy (perhaps, out-
//      of-code) to identify unoccupied sectors and perform operations
//      them.
//
// Traditional drives go from the mathematical property of circles that
// have the maximal surface at the minimal perimeter. That is, no other
// shape can have a surface larger than a circle at a given perimeter.
//
// Nevertheless, the idea of disks are far too immature for a 3D world
// where data could be stored on three axes instead of two.
//
// Generalizing the PrimFS™ and PrimDrive™ to a real-adaptable scenario
// would be a matter of finding a shape that is closest to an inverse-
// sphere and then disposing sectors on its surface.
//
///////////////////////////////////////////////////////////////////////////
// Capacity = [1] 127 bytes (including descriptors) / sector
//
// Implemented Operations: Write, Read, Delete, Format
// Planned Operations: Defragment, (Extend, Shrink)
//
// [1] For Linden Labs-VW style implementations.
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//                      Second Life Constants                            //
//                   PrimFS™ Physical Parameters.                        //
///////////////////////////////////////////////////////////////////////////
 
// The maximum number of bytes that we can store in a description is set
// here to 127 bytes which is the current number of bytes that you can
// store in a primitive's description in Second Life.
integer BYTES_PER_SECTOR = 127;
// Primitives can have a zero-length description but that can only be
// accomplished by setting the description to the empty string using a
// script. Otherwise, any primitive created with actually have this
// string in the description indicating that no description is set.
string UNFORMATTED_MARKER = "(No Description)";
 
///////////////////////////////////////////////////////////////////////////
//                      Wizardry and Steamworks                          //
//          http://grimore.org/fuss/data_structures/key-value_pairs      //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueGet(string k, string data) {
    if(llStringLength(data) == 0) return "";
    if(llStringLength(k) == 0) return "";
    list a = llParseStringKeepNulls(data, ["&", "="], []);
    integer i = llListFindList(llList2ListStrided(a, 0, -1, 2), [ k ]);
    if(i != -1) return llList2String(a, 2*i+1);
    return "";
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueSet(string k, string v, string data) {
    if(llStringLength(data) == 0) return k + "=" + v;
    if(llStringLength(k) == 0) return "";
    if(llStringLength(v) == 0) return "";
    integer i = llListFindList(
        llList2ListStrided(
            llParseString2List(data, ["&", "="], []),
            0, -1, 2
        ),
    [ k ]);
    if(i != -1) return llDumpList2String(
        llListReplaceList(
            llParseString2List(data, ["&"], []),
            [ k + "=" + v ],
        i, i),
    "&");
    return data + "&" + k + "=" + v;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
string wasKeyValueDelete(string k, string data) {
    if(llStringLength(data) == 0) return "";
    if(llStringLength(k) == 0) return "";
    integer i = llListFindList(
        llList2ListStrided(
            llParseString2List(data, ["&", "="], []),
            0, -1, 2
        ),
    [ k ]);
    if(i != -1) return llDumpList2String(
        llDeleteSubList(
            llParseString2List(data, ["&"], []),
        i, i),
    "&");
    return data;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2015 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
list wasKeyValueGetKeys(string data) {
    return llList2ListStrided(
        llParseString2List(
            data, 
            ["&", "="], 
            []
        ), 
        0, 
        -1, 
        2
    );
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
// returns the size of a linked primitive description
string wasGetLinkDescription(integer link) {
    string d = (string)llGetLinkPrimitiveParams(link, [PRIM_DESC]);
    if(d == UNFORMATTED_MARKER || d == "") return "";
    return d;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
// sets the description of a linked primitive
wasSetLinkDescription(integer link, string description) {
    llSetLinkPrimitiveParamsFast(link, [PRIM_DESC, description]);
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2013 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
vector wasPercentToGradientBrighness(float percent, vector from, vector to) {
    return <from.x + (to.x-from.x)*percent/100,
        from.y + (to.y-from.y)*percent/100,
        from.z + (to.z-from.z)*percent/100>;
}
 
///////////////////////////////////////////////////////////////////////////
//                           PrimFS™ API                                 //
//          http://grimore.org/secondlife/primfs_and_primdrive           //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
// Remove all descriptors k and all data associated with descriptor k
// from head to tail and return the number of bytes deleted (excluding
// the length of the descriptor k).
integer wasPrimFSDelete(string k, integer head, integer tail) {
    integer b;
    do {
        string d = wasGetLinkDescription(tail);
        if(llStringLength(d) == 0) jump continue;
        b += llStringLength(wasKeyValueGet(k, d));
        wasSetLinkDescription(tail,
            wasKeyValueDelete(
                k,
                d
            )
        );
        // GC
        d = "";
@continue;
    } while(--tail>=head);
    return b;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
// Returns a list containing all unique file descriptors.
list wasPrimFSGetDescriptors(integer head, integer tail) {
    list descriptors = [];
    do {
        string d = wasGetLinkDescription(tail);
        if(llStringLength(d) == 0) jump continue;
        list c = llList2ListStrided(
            llParseString2List(d, ["&", "="], []),
            0, -1, 2
        );
        // GC
        d = "";
        do {
            string k = llList2String(c, 0);
            if(llListFindList(
                descriptors, (list)k
                    ) != -1) jump skip;
            descriptors += k;
            // GC
            k = "";
@skip;
            c = llDeleteSubList(c, 0, 0);
        } while(llGetListLength(c) != 0);
        // GC
        c = [];
@continue;
    } while(--tail>=head);
    return descriptors;
}
 
///////////////////////////////////////////////////////////////////////////
//                        PrimFS™ Read-Write                             //
//                         Descriptor Level.                             //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
// Formats all blocks for PrimFS™ between head and tail and returns the
// number of sectors for the new partition.
// The resulting formatted partions can be found between head and tail.
integer wasPrimFSFormat(integer head, integer tail) {
    integer seek = tail;
    do {
        wasSetLinkDescription(seek, "");
    } while(--seek>=head);
    return tail-seek;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
// Writes data to the filesystem in the partition between ead and tail
// and returns the number of written bytes.
//
// data cannot contain the = (equal, ASCII 061) sign, a restriction of
// the key-value structure (http://grimore.org/secondlife:key_value_data).
integer wasPrimFSWrite(string k, string data, integer head, integer tail) {
    integer b;
    do {
        string d = wasGetLinkDescription(tail);
        string v = "";
        if(llSubStringIndex(d, k) != -1)
            v = wasKeyValueGet(k, d);
        integer s = llStringLength(v);
        // if the key was found
        if(s != 0) {
            s = BYTES_PER_SECTOR - llStringLength(d) + s;
            v = llGetSubString(data, 0, s-1);
            if(llStringLength(v) != 0) jump write;
            wasSetLinkDescription(tail,
                wasKeyValueDelete(
                    k,
                    d
                )
            );
            jump continue;
        }
        // if the key was not found
        if(llStringLength(d) >= BYTES_PER_SECTOR) jump continue;
        s = BYTES_PER_SECTOR - llStringLength(d) - llStringLength(k) - 1;
        // &
        if(llStringLength(d) != 0) --s;
        v = llGetSubString(data, 0, s-1);
        if(llStringLength(v) == 0) jump continue;
        // write
@write;
        b += llStringLength(v) + llStringLength(k) + 1;
        v = wasKeyValueSet(k, v, d);
        wasSetLinkDescription(tail,v);
        // GC
        v = "";
        data = llDeleteSubString(data, 0, s-1);
@continue;
        // GC
        d = "";
    } while(--tail>=head);
    // return written bytes
    return b;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
// Reads the filesystem on the partition between head and tail and returns
// the data associated with the file descriptor k.
string wasPrimFSRead(string k, integer head, integer tail) {
    string output = "";
    do {
        string d = wasGetLinkDescription(tail);
        if(llStringLength(d) == 0) jump continue;
        if(llListFindList(wasKeyValueGetKeys(d), [ k ]) == -1)
            jump continue;
        output += wasKeyValueGet(k, d);
@continue;
        // GC
        d = "";
    } while(--tail>=head);
    return output;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
// Returns the number of free space (in bytes) on a partition bounded
// by head and tail
integer wasPrimFSGetFreeSpace(integer head, integer tail) {
    integer occupied = 0;
    integer seek = tail;
    do {
        occupied += llStringLength(wasGetLinkDescription(seek));
    } while(--seek>=head);
    return BYTES_PER_SECTOR*(tail-head)-occupied;
}
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
// Returns the number of free space (in bytes) on a partition bounded
// by head and tail
integer wasPrimFSGetUsedSpace(integer head, integer tail) {
    integer occupied = 0;
    do {
      occupied += llStringLength(wasGetLinkDescription(tail));
    } while(--tail>=head);
    return occupied;
}
 
///////////////////////////////////////////////////////////////////////////
//                           PrimDrive™ API                              //
//                            Drive Level.                               //
///////////////////////////////////////////////////////////////////////////
 
///////////////////////////////////////////////////////////////////////////
//    Copyright (C) 2014 Wizardry and Steamworks - License: GNU GPLv3    //
///////////////////////////////////////////////////////////////////////////
// Can be called after "update" (write or append) operations on the
// PrimDrive™ in order to highlight the data-consumption and clustering
// of sectors.
wasPrimDriveLED(integer head, integer tail) {
    // when tail overlaps head, we're done
    if(tail < head) return;
    string d = wasGetLinkDescription(tail);
    // indicate unformated blocks in red
    if(d == UNFORMATTED_MARKER) {
        llSetLinkPrimitiveParamsFast(tail, [
            PRIM_COLOR,
            ALL_SIDES,
            <1,0,0>,
            1]
        );
        jump continue;
    }
    integer size = llStringLength(d);
    // GC
    d = "";
    llSetLinkPrimitiveParamsFast(tail, [
        PRIM_COLOR,
        ALL_SIDES,
        wasPercentToGradientBrighness(
            100*size/(float)BYTES_PER_SECTOR,
            <1,1,1>, ZERO_VECTOR),
        .17 + size/(float)BYTES_PER_SECTOR]
    );
@continue;
    // GC
    d = "";
    wasPrimDriveLED(head, --tail);
}
 
///////////////////////////////////////////////////////////////////////////
 
list commands = [
            "write",
            "read",
            "format",
            "delete",
            "getfreespace",
            "getusedspace",
            "getdescriptors"
        ];
 
list commandStack = [];
string commandInterpreter(list input) {
 
    if(llGetListLength(input) == 0) {
 
        // pop command
        string operation = llList2String(commandStack, 0);
        commandStack = llDeleteSubList(commandStack, 0, 0);
 
        //pop head and tail off the stack
        integer tail = llList2Integer(commandStack, -1);
        integer head = llList2Integer(commandStack, -2);
        integer drive_size = llGetNumberOfPrims();
 
        //drive
        if(tail == 0 || tail > drive_size || head < 2) {
            tail = drive_size;
            head = 2;
            jump operations;
        }
        commandStack = llDeleteSubList(commandStack, -2, -1);
 
@operations;
        string output = "";
 
        // perform operations
        if(operation == "write") {
            // pop key
            string k = llList2String(commandStack, 0);
            commandStack = llDeleteSubList(commandStack, 0, 0);
 
            // got a key but no values
            if(llGetListLength(commandStack) == 0) jump feedback;
 
            output = (string)wasPrimFSWrite(
                k,
                llDumpList2String(commandStack, " "),
                head,
                tail
            );
            wasPrimDriveLED(head, tail);
            jump feedback;
        }
        if(operation == "read") {
            output = wasPrimFSRead(
                llList2String(commandStack, 0),
                head,
                tail
            );
            jump feedback;
        }
 
        if(operation == "format") {
            output = (string)wasPrimFSFormat(head, tail);
            wasPrimDriveLED(head, tail);
            jump feedback;
        }
 
        if(operation == "delete") {
            output = (string)wasPrimFSDelete(
                llList2String(commandStack, 0),
                head,
                tail
            );
            wasPrimDriveLED(head, tail);
            jump feedback;
        }
 
        if(operation == "getusedspace") {
            output = (string)wasPrimFSGetUsedSpace(head, tail);
            jump feedback;
        }
 
        if(operation == "getfreespace") {
            output = (string)wasPrimFSGetFreeSpace(head, tail);
            jump feedback;
        }
 
        if(operation == "getdescriptors") {
            output = llList2CSV(wasPrimFSGetDescriptors(head, tail));
            jump feedback;
        }
 
@feedback;
        commandStack = [];
        return output;
    }
 
    // pop input
    string i = llList2String(input, 0);
    input = llDeleteSubList(input, 0, 0);
 
    // check for valid command
    if(llListFindList(
        commands, (list)i) != -1) {
        commandStack += i;
        jump continue;
    }
 
    // check for numbers
    if(llListFindList(
        commands,
        llList2List(commandStack, 0, 0)) != -1) {
 
        integer o = (integer)i;
        // if not an integer, it's a string
        if(o == 0) {
            commandStack += i;
            jump continue;
        }
        // otherwise an integer
        commandStack += o;
 
    }
 
@continue;
    return commandInterpreter(input);
}
 
string URL = "";
 
default {
    state_entry() {
        llRequestURL();
    }
    http_request(key id, string method, string body) {
        if(method == URL_REQUEST_GRANTED) {
            URL = body;
            llSay(0, "My URL is: " + URL);
            state main;
        }
    }
}
 
state main {
    state_entry() {
        llListen(0, "", "", "");
        llListen(-673, "", "", "");
        llSetTimerEvent(1);
    }
    timer() {
        llGetNextEmail("", "");
    }
    http_request(key id, string method, string body) {
        body = commandInterpreter(llParseString2List(body, [" "], []));
 
        if(llStringLength(body) == 0) return;
        llSay(0, body);
    }
    link_message(integer sender, integer num, string str, key id) {
        str = commandInterpreter(llParseString2List(str, [" "], []));
 
        if(llStringLength(str) == 0) return;
        llSay(0, str);
    }
    listen(integer channel, string name, key id, string message) {
        message = commandInterpreter(llParseString2List(message, [" "], []));
 
        if(llStringLength(message) == 0) return;
        //llSay(0, message);
    }
    email(string time, string address, string subject, string message, integer num_left) {
        message = commandInterpreter(llParseString2List(message, [" "], []));
 
        if(llStringLength(message) == 0) return;
        llSay(0, message);
    }
}