756 lines
28 KiB
C
756 lines
28 KiB
C
|
|
/**
|
||
|
|
* @file runtime_disk.c
|
||
|
|
* @brief The group F verbs: files, channels, and the ones that need a 1541.
|
||
|
|
*
|
||
|
|
* Split three ways, and the split is the whole design.
|
||
|
|
*
|
||
|
|
* - **Verbs that mean something on a filesystem** are implemented against
|
||
|
|
* `aksl_f*`: DOPEN, DCLOSE, APPEND, RECORD, SCRATCH, RENAME, COPY, CONCAT,
|
||
|
|
* DIRECTORY and the binary pair BSAVE/BLOAD.
|
||
|
|
* - **Verbs that are spellings of ones already here** are aliases: SAVE and
|
||
|
|
* LOAD are DSAVE and DLOAD, CATALOG is DIRECTORY, DVERIFY is VERIFY.
|
||
|
|
* - **Verbs that need the hardware** are refused by name, with the reason:
|
||
|
|
* HEADER, COLLECT, BACKUP, BOOT and DCLEAR. Formatting a disk, validating
|
||
|
|
* one, duplicating one and resetting a drive have no filesystem meaning that
|
||
|
|
* is not a lie.
|
||
|
|
*
|
||
|
|
* Channel numbers are the program's, written after `#`. A leading `#` is
|
||
|
|
* optional everywhere -- `DOPEN #1, "F"` and `DOPEN 1, "F"` are the same
|
||
|
|
* statement -- because the scanner reads `#` as its own token and a verb name
|
||
|
|
* cannot carry one.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <inttypes.h>
|
||
|
|
#include <stdio.h>
|
||
|
|
#include <string.h>
|
||
|
|
|
||
|
|
#include <akerror.h>
|
||
|
|
#include <akstdlib.h>
|
||
|
|
|
||
|
|
#include <akbasic/args.h>
|
||
|
|
#include <akbasic/error.h>
|
||
|
|
#include <akbasic/runtime.h>
|
||
|
|
|
||
|
|
#include "verbs.h"
|
||
|
|
|
||
|
|
/* Most verbs answer "did something happen"; this is that answer. */
|
||
|
|
#define SUCCEED_TRUE(__obj, __dest) \
|
||
|
|
do { \
|
||
|
|
*(__dest) = &(__obj)->staticTrueValue; \
|
||
|
|
} while ( 0 )
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_disk_state_init(akbasic_DiskState *obj)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL disk state in init");
|
||
|
|
memset(obj, 0, sizeof(*obj));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_disk_close_all(akbasic_DiskState *obj)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
int i = 0;
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL disk state in close_all");
|
||
|
|
/*
|
||
|
|
* A loop, so CATCH and the _BREAK macros are banned in here. A close that
|
||
|
|
* fails is reported, but the rest are still closed: leaking the others
|
||
|
|
* because one of them was already gone would be a poor trade.
|
||
|
|
*/
|
||
|
|
for ( i = 0; i < AKBASIC_MAX_CHANNELS; i++ ) {
|
||
|
|
if ( obj->channels[i].fp != NULL ) {
|
||
|
|
IGNORE(aksl_fclose(obj->channels[i].fp));
|
||
|
|
obj->channels[i].fp = NULL;
|
||
|
|
obj->channels[i].name[0] = '\0';
|
||
|
|
obj->channels[i].writing = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief Check a channel number and hand back its slot. */
|
||
|
|
static akerr_ErrorContext *channel_slot(akbasic_Runtime *obj, int64_t number, akbasic_Channel **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, (number >= 0 && number < AKBASIC_MAX_CHANNELS),
|
||
|
|
AKBASIC_ERR_BOUNDS, "Channel %" PRId64 " is outside 0..%d",
|
||
|
|
number, AKBASIC_MAX_CHANNELS - 1);
|
||
|
|
*dest = &obj->disk_state.channels[number];
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief The slot for an already-open channel, or an error naming the number. */
|
||
|
|
static akerr_ErrorContext *open_channel(akbasic_Runtime *obj, int64_t number, akbasic_Channel **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
PASS(errctx, channel_slot(obj, number, dest));
|
||
|
|
FAIL_ZERO_RETURN(errctx, ((*dest)->fp != NULL), AKBASIC_ERR_STATE,
|
||
|
|
"Channel %" PRId64 " is not open", number);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief Evaluate one argument as a string. */
|
||
|
|
static akerr_ErrorContext *string_arg(akbasic_Runtime *obj, akbasic_ASTLeaf *arg, const char *verb, char *dest, size_t len)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_Value *value = NULL;
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX, "%s expected a file name", verb);
|
||
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
|
||
|
|
FAIL_NONZERO_RETURN(errctx, (value->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
|
||
|
|
"%s expected a file name", verb);
|
||
|
|
snprintf(dest, len, "%s", value->stringval);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief Evaluate one argument as an integer. */
|
||
|
|
static akerr_ErrorContext *int_arg(akbasic_Runtime *obj, akbasic_ASTLeaf *arg, const char *verb, int64_t *dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_Value *value = NULL;
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, (arg != NULL), AKBASIC_ERR_SYNTAX, "%s expected a number", verb);
|
||
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, arg, &value));
|
||
|
|
FAIL_NONZERO_RETURN(errctx, (value->valuetype == AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
|
||
|
|
"%s expected a number", verb);
|
||
|
|
*dest = (value->valuetype == AKBASIC_TYPE_FLOAT)
|
||
|
|
? (int64_t)value->floatval : value->intval;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* --------------------------------------------------------- DOPEN / APPEND -- */
|
||
|
|
|
||
|
|
/** @brief Shared by DOPEN and APPEND, which differ only in the mode. */
|
||
|
|
static akerr_ErrorContext *open_file(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, const char *verb, const char *mode)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_ASTLeaf *arg = NULL;
|
||
|
|
akbasic_Channel *channel = NULL;
|
||
|
|
char name[AKBASIC_MAX_STRING_LENGTH];
|
||
|
|
int64_t number = 0;
|
||
|
|
bool writing = (mode[0] != 'r');
|
||
|
|
|
||
|
|
arg = akbasic_leaf_first_argument(expr);
|
||
|
|
PASS(errctx, int_arg(obj, arg, verb, &number));
|
||
|
|
PASS(errctx, channel_slot(obj, number, &channel));
|
||
|
|
FAIL_NONZERO_RETURN(errctx, (channel->fp != NULL), AKBASIC_ERR_STATE,
|
||
|
|
"Channel %" PRId64 " is already open on \"%s\"", number, channel->name);
|
||
|
|
PASS(errctx, string_arg(obj, (arg != NULL ? arg->next : NULL), verb, name, sizeof(name)));
|
||
|
|
|
||
|
|
/*
|
||
|
|
* DOPEN's third argument is the C128's `W` for write. It arrives as a bare
|
||
|
|
* identifier, which is a label to this dialect and so cannot be evaluated --
|
||
|
|
* so what is read is the *leaf*, not its value.
|
||
|
|
*/
|
||
|
|
if ( arg != NULL && arg->next != NULL && arg->next->next != NULL ) {
|
||
|
|
akbasic_ASTLeaf *modeleaf = arg->next->next;
|
||
|
|
|
||
|
|
if ( modeleaf->leaftype == AKBASIC_LEAF_IDENTIFIER &&
|
||
|
|
(modeleaf->identifier[0] == 'W' || modeleaf->identifier[0] == 'w') ) {
|
||
|
|
mode = "w";
|
||
|
|
writing = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
PASS(errctx, aksl_fopen(name, mode, &channel->fp));
|
||
|
|
snprintf(channel->name, sizeof(channel->name), "%s", name);
|
||
|
|
channel->writing = writing;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_dopen(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
(void)lval; (void)rval;
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in DOPEN");
|
||
|
|
PASS(errctx, open_file(obj, expr, "DOPEN", "r"));
|
||
|
|
SUCCEED_TRUE(obj, dest);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_append(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
(void)lval; (void)rval;
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in APPEND");
|
||
|
|
PASS(errctx, open_file(obj, expr, "APPEND", "a"));
|
||
|
|
SUCCEED_TRUE(obj, dest);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_dclose(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_ASTLeaf *arg = NULL;
|
||
|
|
akbasic_Channel *channel = NULL;
|
||
|
|
int64_t number = 0;
|
||
|
|
|
||
|
|
(void)lval; (void)rval;
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in DCLOSE");
|
||
|
|
arg = akbasic_leaf_first_argument(expr);
|
||
|
|
if ( arg == NULL ) {
|
||
|
|
/* Bare DCLOSE closes everything, which is what a C128 does. */
|
||
|
|
PASS(errctx, akbasic_disk_close_all(&obj->disk_state));
|
||
|
|
SUCCEED_TRUE(obj, dest);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
PASS(errctx, int_arg(obj, arg, "DCLOSE", &number));
|
||
|
|
PASS(errctx, open_channel(obj, number, &channel));
|
||
|
|
PASS(errctx, aksl_fclose(channel->fp));
|
||
|
|
channel->fp = NULL;
|
||
|
|
channel->name[0] = '\0';
|
||
|
|
channel->writing = false;
|
||
|
|
SUCCEED_TRUE(obj, dest);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ----------------------------------------------------------------- RECORD -- */
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_record(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_ASTLeaf *arg = NULL;
|
||
|
|
akbasic_Channel *channel = NULL;
|
||
|
|
int64_t number = 0;
|
||
|
|
int64_t record = 0;
|
||
|
|
int64_t offset = 1;
|
||
|
|
int64_t size = 0;
|
||
|
|
|
||
|
|
(void)lval; (void)rval;
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in RECORD");
|
||
|
|
arg = akbasic_leaf_first_argument(expr);
|
||
|
|
PASS(errctx, int_arg(obj, arg, "RECORD", &number));
|
||
|
|
PASS(errctx, open_channel(obj, number, &channel));
|
||
|
|
PASS(errctx, int_arg(obj, (arg != NULL ? arg->next : NULL), "RECORD", &record));
|
||
|
|
if ( arg != NULL && arg->next != NULL && arg->next->next != NULL ) {
|
||
|
|
PASS(errctx, int_arg(obj, arg->next->next, "RECORD", &offset));
|
||
|
|
}
|
||
|
|
|
||
|
|
/*
|
||
|
|
* A C128's RECORD positions within a *relative* file, whose record length was
|
||
|
|
* fixed when the file was created. There is no such thing on a filesystem, so
|
||
|
|
* a record is taken to be a line: the file is rewound and read forward. That
|
||
|
|
* is slower than a seek and it is the only definition that does not require
|
||
|
|
* inventing a record length the file does not have. TODO.md section 5.
|
||
|
|
*/
|
||
|
|
FAIL_ZERO_RETURN(errctx, (record >= 1), AKBASIC_ERR_VALUE,
|
||
|
|
"RECORD numbers records from 1, not %" PRId64, record);
|
||
|
|
PASS(errctx, aksl_fseek(channel->fp, 0, SEEK_SET));
|
||
|
|
for ( size = 1; size < record; size++ ) {
|
||
|
|
char line[AKBASIC_MAX_STRING_LENGTH];
|
||
|
|
size_t length = 0;
|
||
|
|
akerr_ErrorContext *got = aksl_fgets(line, sizeof(line), channel->fp, &length);
|
||
|
|
|
||
|
|
if ( got != NULL ) {
|
||
|
|
got->handled = true;
|
||
|
|
IGNORE(akerr_release_error(got));
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if ( offset > 1 ) {
|
||
|
|
PASS(errctx, aksl_fseek(channel->fp, (long)(offset - 1), SEEK_CUR));
|
||
|
|
}
|
||
|
|
SUCCEED_TRUE(obj, dest);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------------------- file management -- */
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_scratch(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
char name[AKBASIC_MAX_STRING_LENGTH];
|
||
|
|
|
||
|
|
(void)lval; (void)rval;
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in SCRATCH");
|
||
|
|
PASS(errctx, string_arg(obj, akbasic_leaf_first_argument(expr), "SCRATCH", name, sizeof(name)));
|
||
|
|
FAIL_ZERO_RETURN(errctx, (remove(name) == 0), AKERR_IO,
|
||
|
|
"SCRATCH could not delete \"%s\"", name);
|
||
|
|
SUCCEED_TRUE(obj, dest);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_rename(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_ASTLeaf *arg = NULL;
|
||
|
|
char from[AKBASIC_MAX_STRING_LENGTH];
|
||
|
|
char to[AKBASIC_MAX_STRING_LENGTH];
|
||
|
|
|
||
|
|
(void)lval; (void)rval;
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in RENAME");
|
||
|
|
arg = akbasic_leaf_first_argument(expr);
|
||
|
|
PASS(errctx, string_arg(obj, arg, "RENAME", from, sizeof(from)));
|
||
|
|
PASS(errctx, string_arg(obj, (arg != NULL ? arg->next : NULL), "RENAME", to, sizeof(to)));
|
||
|
|
FAIL_ZERO_RETURN(errctx, (rename(from, to) == 0), AKERR_IO,
|
||
|
|
"RENAME could not rename \"%s\" to \"%s\"", from, to);
|
||
|
|
SUCCEED_TRUE(obj, dest);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief Shared by COPY and CONCAT, which differ only in the destination mode. */
|
||
|
|
static akerr_ErrorContext *copy_file(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, const char *verb, const char *mode)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_ASTLeaf *arg = NULL;
|
||
|
|
FILE *in = NULL;
|
||
|
|
FILE *out = NULL;
|
||
|
|
char from[AKBASIC_MAX_STRING_LENGTH];
|
||
|
|
char to[AKBASIC_MAX_STRING_LENGTH];
|
||
|
|
char buffer[4096];
|
||
|
|
|
||
|
|
arg = akbasic_leaf_first_argument(expr);
|
||
|
|
PASS(errctx, string_arg(obj, arg, verb, from, sizeof(from)));
|
||
|
|
PASS(errctx, string_arg(obj, (arg != NULL ? arg->next : NULL), verb, to, sizeof(to)));
|
||
|
|
|
||
|
|
PASS(errctx, aksl_fopen(from, "rb", &in));
|
||
|
|
ATTEMPT {
|
||
|
|
CATCH(errctx, aksl_fopen(to, mode, &out));
|
||
|
|
for ( ;; ) {
|
||
|
|
size_t got = 0;
|
||
|
|
size_t put = 0;
|
||
|
|
bool done = false;
|
||
|
|
akerr_ErrorContext *read = aksl_fread(buffer, 1, sizeof(buffer), in, &got);
|
||
|
|
|
||
|
|
if ( read != NULL ) {
|
||
|
|
/*
|
||
|
|
* A short read is how a file *ends*: aksl_fread reports fewer
|
||
|
|
* items than asked for as an error, and `got` still holds what
|
||
|
|
* it managed. Writing that out before stopping is the whole
|
||
|
|
* copy for any file smaller than the buffer -- the first
|
||
|
|
* version broke here and produced an empty destination every
|
||
|
|
* time.
|
||
|
|
*/
|
||
|
|
read->handled = true;
|
||
|
|
IGNORE(akerr_release_error(read));
|
||
|
|
done = true;
|
||
|
|
}
|
||
|
|
if ( got > 0 ) {
|
||
|
|
/*
|
||
|
|
* PASS rather than CATCH: this is a loop, and CATCH expands to
|
||
|
|
* a break that would leave the rest of the ATTEMPT running with
|
||
|
|
* an error pending.
|
||
|
|
*/
|
||
|
|
PASS(errctx, aksl_fwrite(buffer, 1, got, out, &put));
|
||
|
|
}
|
||
|
|
if ( done || got == 0 ) {
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} CLEANUP {
|
||
|
|
IGNORE(aksl_fclose(in));
|
||
|
|
if ( out != NULL ) {
|
||
|
|
IGNORE(aksl_fclose(out));
|
||
|
|
}
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} FINISH(errctx, true);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_copy(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
(void)lval; (void)rval;
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in COPY");
|
||
|
|
PASS(errctx, copy_file(obj, expr, "COPY", "wb"));
|
||
|
|
SUCCEED_TRUE(obj, dest);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_concat(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
(void)lval; (void)rval;
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in CONCAT");
|
||
|
|
/* Appends rather than replaces, which is the whole difference from COPY. */
|
||
|
|
PASS(errctx, copy_file(obj, expr, "CONCAT", "ab"));
|
||
|
|
SUCCEED_TRUE(obj, dest);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------------------------- DIRECTORY -- */
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_directory(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
(void)expr; (void)lval; (void)rval;
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||
|
|
"NULL argument in DIRECTORY");
|
||
|
|
/*
|
||
|
|
* Refused rather than half-built. Listing a directory needs opendir/readdir,
|
||
|
|
* which `libakstdlib` does not wrap -- and this project's rule is that a
|
||
|
|
* missing capability gets filed upstream rather than worked around here
|
||
|
|
* (CLAUDE.md). Filed in deps/libakstdlib/TODO.md.
|
||
|
|
*
|
||
|
|
* The alternative was shelling out to `ls`, which a library has no business
|
||
|
|
* doing, or calling readdir directly and stepping outside the error
|
||
|
|
* convention every other call in this file follows.
|
||
|
|
*/
|
||
|
|
FAIL_RETURN(errctx, AKBASIC_ERR_DEVICE,
|
||
|
|
"DIRECTORY is not implemented: libakstdlib has no directory-reading wrapper yet");
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ------------------------------------------------------------ BSAVE/BLOAD -- */
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_bsave(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_ASTLeaf *arg = NULL;
|
||
|
|
FILE *fp = NULL;
|
||
|
|
char name[AKBASIC_MAX_STRING_LENGTH];
|
||
|
|
int64_t from = 0;
|
||
|
|
int64_t to = 0;
|
||
|
|
size_t put = 0;
|
||
|
|
|
||
|
|
(void)lval; (void)rval;
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in BSAVE");
|
||
|
|
arg = akbasic_leaf_first_argument(expr);
|
||
|
|
PASS(errctx, string_arg(obj, arg, "BSAVE", name, sizeof(name)));
|
||
|
|
PASS(errctx, int_arg(obj, (arg != NULL ? arg->next : NULL), "BSAVE", &from));
|
||
|
|
PASS(errctx, int_arg(obj, (arg != NULL && arg->next != NULL ? arg->next->next : NULL),
|
||
|
|
"BSAVE", &to));
|
||
|
|
FAIL_ZERO_RETURN(errctx, (from != 0 && to > from), AKBASIC_ERR_VALUE,
|
||
|
|
"BSAVE needs a start address and a higher end address");
|
||
|
|
|
||
|
|
PASS(errctx, aksl_fopen(name, "wb", &fp));
|
||
|
|
ATTEMPT {
|
||
|
|
CATCH(errctx, aksl_fwrite((const void *)(uintptr_t)from, 1, (size_t)(to - from), fp, &put));
|
||
|
|
} CLEANUP {
|
||
|
|
IGNORE(aksl_fclose(fp));
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} FINISH(errctx, true);
|
||
|
|
SUCCEED_TRUE(obj, dest);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_bload(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_ASTLeaf *arg = NULL;
|
||
|
|
FILE *fp = NULL;
|
||
|
|
char name[AKBASIC_MAX_STRING_LENGTH];
|
||
|
|
int64_t at = 0;
|
||
|
|
int64_t limit = 0;
|
||
|
|
size_t got = 0;
|
||
|
|
|
||
|
|
(void)lval; (void)rval;
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in BLOAD");
|
||
|
|
arg = akbasic_leaf_first_argument(expr);
|
||
|
|
PASS(errctx, string_arg(obj, arg, "BLOAD", name, sizeof(name)));
|
||
|
|
PASS(errctx, int_arg(obj, (arg != NULL ? arg->next : NULL), "BLOAD", &at));
|
||
|
|
/*
|
||
|
|
* The length is required, unlike a C128's BLOAD which reads until the file
|
||
|
|
* ends. A file longer than the caller expected would otherwise write past
|
||
|
|
* whatever it was pointed at, and there is no way to notice: a BASIC integer
|
||
|
|
* here is a real address. Refusing to guess is the only safe answer -- and
|
||
|
|
* checked before it is read, so a missing one says *that* rather than
|
||
|
|
* "expected a number".
|
||
|
|
*/
|
||
|
|
FAIL_ZERO_RETURN(errctx,
|
||
|
|
(arg != NULL && arg->next != NULL && arg->next->next != NULL),
|
||
|
|
AKBASIC_ERR_SYNTAX,
|
||
|
|
"BLOAD needs an address and a maximum length");
|
||
|
|
PASS(errctx, int_arg(obj, arg->next->next, "BLOAD", &limit));
|
||
|
|
FAIL_ZERO_RETURN(errctx, (at != 0 && limit > 0), AKBASIC_ERR_VALUE,
|
||
|
|
"BLOAD needs an address and a maximum length");
|
||
|
|
|
||
|
|
PASS(errctx, aksl_fopen(name, "rb", &fp));
|
||
|
|
ATTEMPT {
|
||
|
|
akerr_ErrorContext *read = aksl_fread((void *)(uintptr_t)at, 1, (size_t)limit, fp, &got);
|
||
|
|
|
||
|
|
if ( read != NULL ) {
|
||
|
|
/* A short read is the ordinary case: the file was smaller than the cap. */
|
||
|
|
read->handled = true;
|
||
|
|
IGNORE(akerr_release_error(read));
|
||
|
|
}
|
||
|
|
} CLEANUP {
|
||
|
|
IGNORE(aksl_fclose(fp));
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} FINISH(errctx, true);
|
||
|
|
SUCCEED_TRUE(obj, dest);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ------------------------------------------------------------ channel I/O -- */
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_disk_write(akbasic_Runtime *obj, int64_t number, const char *text)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_Channel *channel = NULL;
|
||
|
|
size_t put = 0;
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && text != NULL), AKERR_NULLPOINTER,
|
||
|
|
"NULL argument in disk_write");
|
||
|
|
PASS(errctx, open_channel(obj, number, &channel));
|
||
|
|
FAIL_ZERO_RETURN(errctx, channel->writing, AKBASIC_ERR_STATE,
|
||
|
|
"Channel %" PRId64 " was opened for reading", number);
|
||
|
|
PASS(errctx, aksl_fwrite(text, 1, strlen(text), channel->fp, &put));
|
||
|
|
PASS(errctx, aksl_fwrite("\n", 1, 1, channel->fp, &put));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_disk_readline(akbasic_Runtime *obj, int64_t number, char *dest, size_t len, bool *eof)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_Channel *channel = NULL;
|
||
|
|
akerr_ErrorContext *got = NULL;
|
||
|
|
char *newline = NULL;
|
||
|
|
size_t length = 0;
|
||
|
|
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL && eof != NULL), AKERR_NULLPOINTER,
|
||
|
|
"NULL argument in disk_readline");
|
||
|
|
PASS(errctx, open_channel(obj, number, &channel));
|
||
|
|
FAIL_NONZERO_RETURN(errctx, channel->writing, AKBASIC_ERR_STATE,
|
||
|
|
"Channel %" PRId64 " was opened for writing", number);
|
||
|
|
|
||
|
|
*eof = false;
|
||
|
|
dest[0] = '\0';
|
||
|
|
got = aksl_fgets(dest, len, channel->fp, &length);
|
||
|
|
if ( got != NULL ) {
|
||
|
|
/*
|
||
|
|
* End of file is not an error here, the same way it is not one for the
|
||
|
|
* sink's readline: running out of input is a state a program handles.
|
||
|
|
*/
|
||
|
|
got->handled = true;
|
||
|
|
IGNORE(akerr_release_error(got));
|
||
|
|
*eof = true;
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
newline = strchr(dest, '\n');
|
||
|
|
if ( newline != NULL ) {
|
||
|
|
*newline = '\0';
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_print_channel(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_ASTLeaf *arg = NULL;
|
||
|
|
akbasic_Value *value = NULL;
|
||
|
|
char text[AKBASIC_MAX_STRING_LENGTH];
|
||
|
|
int64_t number = 0;
|
||
|
|
|
||
|
|
(void)lval; (void)rval;
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||
|
|
"NULL argument in PRINT#");
|
||
|
|
arg = akbasic_leaf_first_argument(expr);
|
||
|
|
PASS(errctx, int_arg(obj, arg, "PRINT#", &number));
|
||
|
|
FAIL_ZERO_RETURN(errctx, (arg != NULL && arg->next != NULL), AKBASIC_ERR_SYNTAX,
|
||
|
|
"Expected PRINT #(channel), (expression)");
|
||
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, arg->next, &value));
|
||
|
|
PASS(errctx, akbasic_value_to_string(value, text, sizeof(text)));
|
||
|
|
PASS(errctx, akbasic_disk_write(obj, number, text));
|
||
|
|
SUCCEED_TRUE(obj, dest);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_input_channel(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
akbasic_ASTLeaf *arg = NULL;
|
||
|
|
akbasic_ASTLeaf *identifier = NULL;
|
||
|
|
akbasic_ASTLeaf literal;
|
||
|
|
akbasic_ASTLeaf assign;
|
||
|
|
akbasic_Value *unused = NULL;
|
||
|
|
char text[AKBASIC_MAX_STRING_LENGTH];
|
||
|
|
int64_t number = 0;
|
||
|
|
bool eof = false;
|
||
|
|
|
||
|
|
(void)lval; (void)rval;
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||
|
|
"NULL argument in INPUT#");
|
||
|
|
arg = akbasic_leaf_first_argument(expr);
|
||
|
|
PASS(errctx, int_arg(obj, arg, "INPUT#", &number));
|
||
|
|
identifier = (arg != NULL ? arg->next : NULL);
|
||
|
|
FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_identifier(identifier), AKBASIC_ERR_SYNTAX,
|
||
|
|
"Expected INPUT #(channel), (variable)");
|
||
|
|
|
||
|
|
PASS(errctx, akbasic_disk_readline(obj, number, text, sizeof(text), &eof));
|
||
|
|
/*
|
||
|
|
* End of file leaves the variable empty rather than raising. A program reads
|
||
|
|
* until it gets nothing back, which is what it can check -- and a numeric
|
||
|
|
* variable reading an empty line gets zero, the same as a C128.
|
||
|
|
*/
|
||
|
|
PASS(errctx, akbasic_leaf_init(&literal, AKBASIC_LEAF_LITERAL_STRING));
|
||
|
|
snprintf(literal.literal_string, sizeof(literal.literal_string), "%s", text);
|
||
|
|
if ( akbasic_leaf_identifier_type(identifier) == AKBASIC_TYPE_INTEGER ) {
|
||
|
|
long long converted = 0;
|
||
|
|
|
||
|
|
literal.leaftype = AKBASIC_LEAF_LITERAL_INT;
|
||
|
|
literal.literal_int = 0;
|
||
|
|
if ( !eof && text[0] != '\0' ) {
|
||
|
|
PASS(errctx, aksl_atoll(text, &converted));
|
||
|
|
literal.literal_int = (int64_t)converted;
|
||
|
|
}
|
||
|
|
} else if ( akbasic_leaf_identifier_type(identifier) == AKBASIC_TYPE_FLOAT ) {
|
||
|
|
literal.leaftype = AKBASIC_LEAF_LITERAL_FLOAT;
|
||
|
|
literal.literal_float = 0.0;
|
||
|
|
if ( !eof && text[0] != '\0' ) {
|
||
|
|
PASS(errctx, aksl_atof(text, &literal.literal_float));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
PASS(errctx, akbasic_leaf_init(&assign, AKBASIC_LEAF_BINARY));
|
||
|
|
assign.left = identifier;
|
||
|
|
assign.right = &literal;
|
||
|
|
assign.operator_ = AKBASIC_TOK_ASSIGNMENT;
|
||
|
|
PASS(errctx, akbasic_runtime_evaluate(obj, &assign, &unused));
|
||
|
|
SUCCEED_TRUE(obj, dest);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ---------------------------------------------------------------- VERIFY -- */
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_dverify(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
FILE *fp = NULL;
|
||
|
|
char name[AKBASIC_MAX_STRING_LENGTH];
|
||
|
|
char line[AKBASIC_MAX_LINE_LENGTH];
|
||
|
|
int64_t lineno = 0;
|
||
|
|
int64_t mismatch = 0;
|
||
|
|
|
||
|
|
(void)lval; (void)rval;
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||
|
|
"NULL argument in VERIFY");
|
||
|
|
PASS(errctx, string_arg(obj, akbasic_leaf_first_argument(expr), "VERIFY", name, sizeof(name)));
|
||
|
|
|
||
|
|
/*
|
||
|
|
* A C128 verifies the program in memory against the one on disk byte for
|
||
|
|
* byte, because a 1541 write could fail silently. Here the comparison is
|
||
|
|
* line by line against the file's text, which is the same question asked of
|
||
|
|
* a filesystem: is what I would save what is already there?
|
||
|
|
*/
|
||
|
|
PASS(errctx, aksl_fopen(name, "r", &fp));
|
||
|
|
ATTEMPT {
|
||
|
|
for ( lineno = 0; lineno < AKBASIC_MAX_SOURCE_LINES; lineno++ ) {
|
||
|
|
size_t length = 0;
|
||
|
|
akerr_ErrorContext *got = NULL;
|
||
|
|
char *newline = NULL;
|
||
|
|
|
||
|
|
if ( obj->source[lineno].code[0] == '\0' ) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
got = aksl_fgets(line, sizeof(line), fp, &length);
|
||
|
|
if ( got != NULL ) {
|
||
|
|
got->handled = true;
|
||
|
|
IGNORE(akerr_release_error(got));
|
||
|
|
mismatch += 1;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
newline = strchr(line, '\n');
|
||
|
|
if ( newline != NULL ) {
|
||
|
|
*newline = '\0';
|
||
|
|
}
|
||
|
|
/*
|
||
|
|
* The stored line has had its number stripped, so the file's copy is
|
||
|
|
* compared from past its own number. PASS rather than CATCH: this is
|
||
|
|
* a loop.
|
||
|
|
*/
|
||
|
|
{
|
||
|
|
const char *filetext = line;
|
||
|
|
|
||
|
|
while ( *filetext == ' ' ) {
|
||
|
|
filetext += 1;
|
||
|
|
}
|
||
|
|
while ( *filetext >= '0' && *filetext <= '9' ) {
|
||
|
|
filetext += 1;
|
||
|
|
}
|
||
|
|
while ( *filetext == ' ' ) {
|
||
|
|
filetext += 1;
|
||
|
|
}
|
||
|
|
if ( strcmp(filetext, obj->source[lineno].code) != 0 ) {
|
||
|
|
mismatch += 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} CLEANUP {
|
||
|
|
IGNORE(aksl_fclose(fp));
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} FINISH(errctx, true);
|
||
|
|
|
||
|
|
FAIL_NONZERO_RETURN(errctx, (mismatch != 0), AKBASIC_ERR_VALUE,
|
||
|
|
"VERIFY: \"%s\" does not match the program in memory (%" PRId64 " lines differ)",
|
||
|
|
name, mismatch);
|
||
|
|
PASS(errctx, akbasic_runtime_println(obj, "OK"));
|
||
|
|
SUCCEED_TRUE(obj, dest);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* --------------------------------------------------- needs a real 1541 --- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Refuse a verb that has no meaning without the drive, and say why.
|
||
|
|
*
|
||
|
|
* Five of them, and the reasoning is the same each time: formatting a disk,
|
||
|
|
* validating one, duplicating one, resetting a drive and booting from one are
|
||
|
|
* operations on a physical device. A filesystem has no equivalent that is not a
|
||
|
|
* lie -- `HEADER` would have to mean "delete everything in this directory",
|
||
|
|
* which is a spectacularly bad thing to do to somebody who typed a C128 verb.
|
||
|
|
*/
|
||
|
|
static akerr_ErrorContext *no_drive(const char *verb, const char *what)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
FAIL_RETURN(errctx, AKBASIC_ERR_DEVICE,
|
||
|
|
"%s %s, and there is no disk drive here -- only a filesystem", verb, what);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_header(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
(void)obj; (void)expr; (void)lval; (void)rval; (void)dest;
|
||
|
|
PASS(errctx, no_drive("HEADER", "formats a disk"));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_collect(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
(void)obj; (void)expr; (void)lval; (void)rval; (void)dest;
|
||
|
|
PASS(errctx, no_drive("COLLECT", "validates a disk's block allocation map"));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_backup(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
(void)obj; (void)expr; (void)lval; (void)rval; (void)dest;
|
||
|
|
PASS(errctx, no_drive("BACKUP", "duplicates one disk onto another"));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_boot(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
(void)obj; (void)expr; (void)lval; (void)rval; (void)dest;
|
||
|
|
PASS(errctx, no_drive("BOOT", "loads and runs a boot sector"));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
akerr_ErrorContext *akbasic_cmd_dclear(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
(void)expr; (void)lval; (void)rval;
|
||
|
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER, "NULL argument in DCLEAR");
|
||
|
|
/*
|
||
|
|
* The one of the five with a defensible meaning. DCLEAR resets the drive and
|
||
|
|
* closes every channel it had open; the second half is exactly
|
||
|
|
* akbasic_disk_close_all(), so that is what it does. The first half has
|
||
|
|
* nothing to reset.
|
||
|
|
*/
|
||
|
|
PASS(errctx, akbasic_disk_close_all(&obj->disk_state));
|
||
|
|
SUCCEED_TRUE(obj, dest);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|