@bitblit/asyncglk
Version:
A Typescript Glk library
392 lines (388 loc) • 12.8 kB
JavaScript
/*
Streaming Node implementation of Dialog
=======================================
Mostly lifted directly from electrofs.js
Copyright (c) 2016-2020 Andrew Plotkin
Copyright (c) 2022 Dannii Willis
MIT licenced
https://github.com/curiousdannii/asyncglk
*/
import fs from 'fs';
import os from 'os';
import path from 'path';
import { filemode_Read, filemode_ReadWrite, filemode_Write, filemode_WriteAppend, fileusage_Data, fileusage_InputRecord, fileusage_SavedGame, fileusage_Transcript, seekmode_Current, seekmode_End, } from '../../glkapi/constants.js';
export default class NodeStreamingDialog {
'async' = false;
extfilepath;
GlkOte;
is_inited = false;
streaming = true;
userpath;
constructor() {
this.userpath = this.get_user_path();
this.extfilepath = path.join(this.userpath, 'quixe-files');
}
init_async(options, callback) {
if (this.is_inited) {
callback();
return;
}
this.GlkOte = options.GlkOte;
this.is_inited = true;
callback();
}
autosave_read(signature) {
const gamedirpath = path.join(this.userpath, 'games', signature);
const pathj = path.join(gamedirpath, 'autosave.json');
const pathr = path.join(gamedirpath, 'autosave.ram');
try {
const str = fs.readFileSync(pathj, { encoding: 'utf8' });
const snapshot = JSON.parse(str);
try {
const buf = fs.readFileSync(pathr, { encoding: null });
const ram = Array.from(buf.values());
snapshot.ram = ram;
}
catch { }
return snapshot;
}
catch {
return null;
}
}
autosave_write(signature, snapshot) {
const gamedirpath = path.join(this.userpath, 'games', signature);
// Make sure the gamedirpath exists.
let stat = null;
try {
stat = fs.statSync(gamedirpath);
}
catch { }
if (!stat || !stat.isDirectory()) {
try {
fs.mkdirSync(path.join(this.userpath, 'games'));
}
catch { }
try {
fs.mkdirSync(gamedirpath);
}
catch { }
stat = null;
try {
stat = fs.statSync(gamedirpath);
}
catch { }
if (!stat || !stat.isDirectory()) {
// Can't create the directory; give up.
this.GlkOte.log('Unable to create gamedirpath: ' + gamedirpath);
return;
}
}
/* We'll save the snapshot in two files: a .ram file (snapshot.ram
as raw bytes) and a .json file (the rest of snapshot, stringified).
*/
const pathj = path.join(gamedirpath, 'autosave.json');
const pathr = path.join(gamedirpath, 'autosave.ram');
if (!snapshot) {
try {
fs.unlinkSync(pathj);
}
catch { }
try {
fs.unlinkSync(pathr);
}
catch { }
return;
}
/* Pull snapshot.ram out, if it exists. (It's okay to munge the
snapshot object,the caller doesn't want it back.) */
const ram = snapshot.ram;
snapshot.ram = undefined;
const str = JSON.stringify(snapshot);
fs.writeFileSync(pathj, str, { encoding: 'utf8' });
if (ram) {
const buf = Buffer.from(ram);
fs.writeFileSync(pathr, buf);
}
}
file_clean_fixed_name(filename, usage) {
let res = filename.replace(/["/\\<>:|?*]/g, '');
const pos = res.indexOf('.');
if (pos >= 0) {
res = res.slice(0, pos);
}
if (res.length === 0) {
res = 'null';
}
switch (usage) {
case fileusage_Data:
return res + '.glkdata';
case fileusage_SavedGame:
return res + '.glksave';
case fileusage_Transcript:
case fileusage_InputRecord:
return res + '.txt';
default:
return res;
}
}
file_construct_ref(filename = '', usage = '', _gameid = '') {
const filepath = path.join(this.extfilepath, filename);
return {
filename: filepath,
usage: usage,
};
}
file_construct_temp_ref(usage) {
const filename = `_temp_${Date.now()}_${Math.random()}`.replace('.', '');
const filepath = path.join(this.get_temp_path(), filename);
return {
filename: filepath,
usage: usage,
};
}
file_fopen(fmode, fref) {
/* The spec says that Write, ReadWrite, and WriteAppend create the
file if necessary. However, open(filename, "r+") doesn't create
a file. So we have to pre-create it in the ReadWrite and
WriteAppend cases. (We use "a" so as not to truncate.) */
if (fmode === filemode_ReadWrite || fmode === filemode_WriteAppend) {
try {
const tempfd = fs.openSync(fref.filename, 'a');
fs.closeSync(tempfd);
}
catch (ex) {
this.GlkOte.log(`fopen: failed to open ${fref.filename}: ${ex}`);
return null;
}
}
/* Another Unix quirk: in r+ mode, you're not supposed to flip from
reading to writing or vice versa without doing an fseek. We will
track the most recent operation (as lastop) -- Write, Read, or
0 if either is legal next. */
let modestr = 'r';
switch (fmode) {
case filemode_Write:
modestr = 'w';
break;
case filemode_ReadWrite:
modestr = 'r+';
break;
case filemode_WriteAppend:
/* Can't use "a" here, because then fseek wouldn't work.
Instead we use "r+" and then fseek to the end. */
modestr = 'r+';
break;
}
let fd;
try {
fd = fs.openSync(fref.filename, modestr);
}
catch (ex) {
this.GlkOte.log(`fopen: failed to open ${fref.filename}: ${ex}`);
return null;
}
/* This object is analogous to a FILE* in C code. Yes, we're
reimplementing fopen() for Node.js. I'm not proud. Or tired.
The good news is, the logic winds up identical to that in
the C libraries.
*/
const fstream = new NodeFileStream(fd, fref.filename, fmode, this.GlkOte);
if (fmode === filemode_WriteAppend) {
/* We must jump to the end of the file. */
fstream.fseek(0, seekmode_End);
}
return fstream;
}
file_ref_exists(fref) {
try {
fs.accessSync(fref.filename, fs.constants.F_OK);
return true;
}
catch {
return false;
}
}
file_remove_ref(fref) {
try {
fs.unlinkSync(fref.filename);
}
catch { }
}
getlibrary(name) {
switch (name) {
case 'GlkOte': return this.GlkOte;
}
// Unrecognized library name.
return null;
}
get_temp_path() {
return os.tmpdir();
}
inited() {
return this.is_inited;
}
}
// The size of our stream buffering.
const BUFFER_SIZE = 256;
export class NodeFileStream {
BufferClass = Buffer;
buffer = Buffer.alloc(BUFFER_SIZE);
/** How much of the buffer is used */
buflen = 0;
/** How much of the buffer has been read out (readmode only) */
bufmark = 0;
/** bufuse is filemode_Read or filemode_Write, if the buffer is being used
for reading or writing. For writing, the buffer starts at mark and
covers buflen bytes. For reading, the buffer starts at mark amd runs
buflen bytes, but bufmark bytes have been consumed from it. */
bufuse = 0;
fd;
filename;
//private fmode: number
GlkOte;
/** Position in file */
mark = 0;
constructor(fd, filename, _fmode, GlkOte) {
this.fd = fd;
this.filename = filename;
//this.fmode = fmode
this.GlkOte = GlkOte;
}
fclose() {
if (this.fd === null) {
this.GlkOte.log('file already closed: ' + this.filename);
return;
}
// flush any unwritten data
this.fflush();
fs.closeSync(this.fd);
this.bufuse = 0;
this.fd = null;
}
fflush() {
if (this.bufuse === filemode_Read) {
// Do nothing, just advance the mark.
this.mark += this.bufmark;
}
else if (this.bufuse === filemode_Write) {
if (this.buflen) {
const count = fs.writeSync(this.fd, this.buffer, 0, this.buflen, this.mark);
this.mark += count;
}
}
this.bufuse = 0;
this.buflen = 0;
this.bufmark = 0;
}
fread(buf, len) {
if (len === undefined) {
len = buf.length;
}
/* got will be our mark in the buf argument. When got reaches
len, we're done. (Unless we hit EOF first.) */
let got = 0;
while (true) {
if (this.bufuse === filemode_Read) {
if (this.bufmark < this.buflen) {
let want = len - got;
if (want > this.buflen - this.bufmark) {
want = this.buflen - this.bufmark;
}
if (want > 0) {
buf.set(this.buffer.subarray(this.bufmark, this.bufmark + want), got);
this.bufmark += want;
got += want;
}
}
if (got >= len) {
return got;
}
/* We need more, but we've consumed the entire buffer. Fall
through to the next step where we will fflush and keep
reading. */
}
if (this.bufuse) {
this.fflush();
}
/* ### if len-got >= BUFFER_SIZE, we could read directly and ignore
our buffer. */
this.bufuse = filemode_Read;
this.bufmark = 0;
this.buflen = fs.readSync(this.fd, this.buffer, 0, BUFFER_SIZE, this.mark);
if (this.buflen === 0) {
// End of file. Mark the buffer unused, since it's empty.
this.bufuse = 0;
return got;
}
// mark stays at the buffer start position
}
}
fseek(pos, seekmode) {
/* ### we could seek within the current buffer, which would be
efficient for small moves. */
this.fflush();
let val = 0;
if (seekmode === seekmode_Current) {
val = this.mark + pos;
}
else if (seekmode === seekmode_End) {
try {
const stats = fs.fstatSync(this.fd);
val = stats.size + pos;
}
catch {
val = this.mark + pos;
}
}
else {
val = pos;
}
if (val < 0) {
val = 0;
}
this.mark = val;
}
ftell() {
if (this.bufuse === filemode_Read) {
return this.mark + this.bufmark;
}
else if (this.bufuse === filemode_Write) {
return this.mark + this.buflen;
}
else {
return this.mark;
}
}
fwrite(buf, len) {
if (len === undefined) {
len = buf.length;
}
let from = 0;
while (true) {
if (this.bufuse === filemode_Write) {
let want = len - from;
if (want > BUFFER_SIZE - this.buflen) {
want = BUFFER_SIZE - this.buflen;
}
if (want > 0) {
this.buffer.set(buf.subarray(from, from + want), this.buflen);
this.buflen += want;
from += want;
}
}
if (from >= len) {
return from;
}
/* We need to write more, but the buffer is full. Fall through
to the next step where we will fflush and keep writing. */
if (this.bufuse) {
this.fflush();
}
/* ### if len-from >= BUFFER_SIZE, we could write directly and
ignore our buffer. */
this.bufuse = filemode_Write;
this.buflen = 0;
}
}
}