/* Gopher.js - Backend for fetching, storing and manipulating data over the Gopher network.
(c) 2020 Senri Software
Requires an environment with mozTCPSocket. Primarily intended for KaiOS.
This is pre-alpha quality software, and a "sneak peek" of what to come.
I don't yet feel comfortable licensing Gopher.js.
*/
let decoder = new TextDecoder();
let encoder = new TextEncoder();
function completeGopherEntry(entry) {
/* Takes an incomplete GopherEntry and returns a new GopherEntry
with unset values set to default. */
let newentry = Object.assign({}, emptyentry, entry);
for (let p in newentry) {
if (!newentry[p]) {
newentry[p] = defaultentry[p];
};
};
return newentry;
};
function GopherSocket(entry) {
/* Returns a GopherSocket connecting to the given GopherEntry. */
entry = completeGopherEntry(entry);
let datapackets = [];
let obj = {
"socket": navigator.mozTCPSocket.open(entry.host, entry.port, {binaryType: "arraybuffer"}),
"entry": entry,
"received": 0,
"data": new Uint8Array(0)
};
obj.socket.ondata = function(event) {
datapackets.push(event.data);
obj.received += event.data.byteLength;
console.log("%d bytes received", obj.received);
};
obj.socket.onopen = function() {
console.log("Connected to %s:%d. Sending selector \"%s\" with query \"%s\"",
entry.host, entry.port, entry.path, entry.query);
let outbuffer
if (entry.query) {
outbuffer = encoder.encode(entry.path+"\t"+entry.query+"\r\n");
} else {
outbuffer = encoder.encode(entry.path+"\r\n");
};
obj.socket.send(outbuffer.buffer);
};
obj.socket.onclose = function() {
console.log("Disconnected from %s:%d.", entry.host, entry.port);
obj.data = new Uint8Array(obj.received);
let acc = 0;
datapackets.forEach( function(packet) {
obj.data.set(new Uint8Array(packet), acc);
acc += packet.byteLength;
});
datapackets = [];
if (entry.type === "1" || entry.type === "7") {
obj.dir = rawToGopherDirectory(obj.data);
};
};
obj.socket.onerror = function() {
console.error("A connection error occurred (%s:%d). Disconnecting...", entry.host, entry.port);
obj.socket.close();
};
return obj;
};
function rawToGopherEntry(dirline) {
/* Gets a single gopher entry as a raw string (trimming \r\n if necessary)
and returns a GopherEntry. */
dirline = dirline.trim();
if (dirline.length < 2) {
return null;
};
let direntry = dirline.split("\t");
if (direntry.length < 4) {
console.error("Malformed gopher entry (not enough fields specified): %o", direntry);
return {
"type": "3",
"name": "(Client-side error: Malformed entry)",
"path": "",
"host": "error.invalid",
"port": 1
};
};
return {
"type": direntry[0][0],
"name": direntry[0].slice(1),
"path": direntry[1],
"host": direntry[2],
"port": parseInt(direntry[3],10)
};
};
function rawToGopherDirectory(directory) {
/* Gets a gopher directory as a Uint8Array and returns a GopherDirectory. */
var dirarray = decoder.decode(directory).split("\n");
dirarray.forEach(function(entry, i, array) {
array.splice(i, 1, rawToGopherEntry(entry));
});
return dirarray;
};
function GopherEntryToURL(entry) {
/* Gets a GopherEntry and returns its URL. */
let port;
if (entry.port === 70 || !entry.port) {
port="";
} else {
port=":" + String(entry.port);
};
let path = entry.path;
if (entry.query) {
path += "\t" + entry.query;
};
return encodeURI("gopher://" + entry.host + port + "/" + entry.type + path);
};
function URLToGopherEntry(url) {
/* Gets a gopher:// URL and returns its GopherEntry, filling the name with host+path. */
if (!url.startsWith("gopher://")) { return null; };
url = decodeURI(url.slice(9));
let urlarray = url.split(/\/(.*)/);
let server = urlarray[0].split(":");
let selector;
if (urlarray[1]) {
selector = urlarray[1].split("\t");
} else {
selector = ["", ""];
};
return {
"type": selector[0][0] || "1",
"name": server[0] + "/" + selector[0].slice(1),
"path": selector[0].slice(1),
"host": server[0],
"port": parseInt(server[1],10) || 70,
"query": selector[1] || ""
};
};