akbasic's src/ now calls libakstdlib 313 times and raw libc 7 -- 2.2% bypassed, against 86.4% on the same tree before this. The submodule bump 669b2b3 -> 2b79aca needed no source change of its own: the release is drop-in for what akbasic already used. Seven of the eight sites the earlier port left on raw libc change their own signature rather than swallowing an error, per andrew's ruling on libakstdlib#38. word_is, the is_waiting_for pair, the scanner's is_at_end, peek, peek_next and match_next_char, format.c's overflow, and sink_akgl's scroll/newline/putchar_at/echo_line/edit_key chain all return an akerr_ErrorContext * and hand the answer back through an out parameter. is_waiting_for and is_waiting_for_any are a public header change; every call site that used one as a term in a condition hoists it into a statement first. verb_compare is the eighth and stays on strcmp. bsearch(3) fixes the comparator's signature, so there is no out parameter to report through -- which is what libakstdlib#38 concluded. It carries a comment saying so and saying why the bypass is safe there. Six snprintf sites stay raw because they want truncation as an answer rather than an error, and aksl_snprintf cannot express that until libakstdlib#34 hands the required length back. Each of the six says so at the site. Two of them, in host.c, are a latent defect rather than a decision: a host type name over 31 characters truncates silently and two sharing a prefix then collide, where structtype.c refuses the same case. DLOAD leaked a file descriptor. Its read loop sat inside an ATTEMPT and the PASS in it returned past CLEANUP, so a scan error left the file open. Hoisting the loop into its own helper to convert fgets fixes it. Refs libakstdlib#26, libakstdlib#38 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
795 lines
30 KiB
C
795 lines
30 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 <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");
|
|
PASS(errctx, aksl_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);
|
|
/*
|
|
* A plain copy, so aksl_strcpy rather than aksl_snprintf: it refuses rather
|
|
* than truncates, and every caller here hands in a buffer the size of a
|
|
* string value, so a name that did not fit could only ever be a bug.
|
|
*/
|
|
PASS(errctx, aksl_strcpy(dest, len, 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));
|
|
PASS(errctx, aksl_strcpy(channel->name, sizeof(channel->name), 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);
|
|
akerr_ErrorContext *failure = NULL;
|
|
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)));
|
|
/*
|
|
* Caught and restated rather than passed. aksl_remove raises the bare errno,
|
|
* and "No such file or directory" on its own does not say which file or
|
|
* which verb wanted it -- so the wrapper's error is retired here and the
|
|
* message the program sees keeps naming both.
|
|
*/
|
|
failure = aksl_remove(name);
|
|
if ( failure != NULL ) {
|
|
failure->handled = true;
|
|
IGNORE(akerr_release_error(failure));
|
|
FAIL_RETURN(errctx, 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);
|
|
akerr_ErrorContext *failure = NULL;
|
|
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)));
|
|
/* Caught and restated, for the reason SCRATCH gives: the message names both files. */
|
|
failure = aksl_rename(from, to);
|
|
if ( failure != NULL ) {
|
|
failure->handled = true;
|
|
IGNORE(akerr_release_error(failure));
|
|
FAIL_RETURN(errctx, 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
|
|
* (MAINTENANCE.md). Filed as libakstdlib issue #10.
|
|
*
|
|
* 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 length = 0;
|
|
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_strlen(text, &length));
|
|
PASS(errctx, aksl_fwrite(text, 1, length, 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);
|
|
}
|
|
PASS(errctx, aksl_strchr(dest, '\n', &newline));
|
|
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));
|
|
PASS(errctx, aksl_strcpy(literal.literal_string, sizeof(literal.literal_string), 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 -- */
|
|
|
|
/**
|
|
* @brief Count the lines of @p fp that differ from the program in memory.
|
|
*
|
|
* Its own function so that the comparison can be reached with a single CATCH.
|
|
* Written inline it would be a loop inside VERIFY's ATTEMPT, where neither form
|
|
* is available: CATCH expands to a break that would escape only the loop, and
|
|
* PASS returns past the CLEANUP that closes the file. One call is neither, so
|
|
* the wrappers below can PASS in the ordinary way.
|
|
*/
|
|
static akerr_ErrorContext *verify_lines(akbasic_Runtime *obj, FILE *fp, int64_t *dest)
|
|
{
|
|
PREPARE_ERROR(errctx);
|
|
akerr_ErrorContext *got = NULL;
|
|
char line[AKBASIC_MAX_LINE_LENGTH];
|
|
const char *filetext = NULL;
|
|
char *newline = NULL;
|
|
int64_t lineno = 0;
|
|
size_t length = 0;
|
|
int cmp = 0;
|
|
|
|
*dest = 0;
|
|
for ( lineno = 0; lineno < AKBASIC_MAX_SOURCE_LINES; lineno++ ) {
|
|
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));
|
|
*dest += 1;
|
|
break;
|
|
}
|
|
PASS(errctx, aksl_strchr(line, '\n', &newline));
|
|
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.
|
|
*/
|
|
filetext = line;
|
|
while ( *filetext == ' ' ) {
|
|
filetext += 1;
|
|
}
|
|
while ( *filetext >= '0' && *filetext <= '9' ) {
|
|
filetext += 1;
|
|
}
|
|
while ( *filetext == ' ' ) {
|
|
filetext += 1;
|
|
}
|
|
PASS(errctx, aksl_strcmp(filetext, obj->source[lineno].code, &cmp));
|
|
if ( cmp != 0 ) {
|
|
*dest += 1;
|
|
}
|
|
}
|
|
SUCCEED_RETURN(errctx);
|
|
}
|
|
|
|
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];
|
|
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 {
|
|
CATCH(errctx, verify_lines(obj, fp, &mismatch));
|
|
} 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);
|
|
}
|