AmendHub

Download:

jcs

/

subtext

/

amendments

/

8

db+user: Initial work on creating a database, creating users

Of note, 'C###' templates are a C string in ### bytes in hex, not dec

jcs made amendment 8 over 2 years ago
--- db.c Sun Dec 5 16:07:46 2021 +++ db.c Sun Dec 5 16:07:46 2021 @@ -0,0 +1,212 @@ +/* + * Copyright (c) 2021 joshua stein <jcs@jcs.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <stdio.h> +#include <string.h> +#include <time.h> + +#include "db.h" +#include "user.h" +#include "util.h" + +struct tmpl tmpls[10]; +short ntmpls; + +struct db *db_init(SFReply reply, short is_new); +short db_migrate(struct db *tdb, short is_new); + +void +load_db_tmpls(void) +{ + short n; + + ntmpls = Count1Resources('TMPL'); + if (ntmpls > nitems(tmpls)) + err(1, "ntmpls %d > %d", ntmpls, nitems(tmpls)); + for (n = 0; n < ntmpls; n++) { + tmpls[n].h = GetIndResource('TMPL', n + 1); + if (tmpls[n].h == NULL) + err(1, "Failed fetching TMPL %d", n + 1); + GetResInfo(tmpls[n].h, &tmpls[n].id, &tmpls[n].type, + &tmpls[n].name); + DetachResource(tmpls[n].h); + } +} + +struct db * +db_open(AppFile *file) +{ + Point pt = { 75, 100 }; + SFReply reply; + SFTypeList types; + + if (file) { + reply.vRefNum = file->vRefNum; + memcpy(&reply.fName, file->fName, sizeof(reply.fName)); + } else { + types[0] = DB_TYPE; + + SFGetFile(pt, NULL, NULL, 1, &types, NULL, &reply); + if (!reply.good) + return db_create(); + } + + return db_init(reply, 0); +} + +struct db * +db_create(void) +{ + Point pt = { 75, 100 }; + SFReply reply; + char *newpath = NULL; + short error, fh, tfh, i; + + SFPutFile(pt, "\pCreate new Subtext DB:", NULL, NULL, &reply); + if (!reply.good) + return NULL; + + getpath(reply.vRefNum, reply.fName, &newpath, 1); + CtoPstr(newpath); + + error = Create(reply.fName, reply.vRefNum, SUBTEXT_CREATOR, DB_TYPE); + if (error == dupFNErr) { + error = FSDelete(reply.fName, reply.vRefNum); + if (error) + err(1, "Failed to re-create file %s: %d", PtoCstr(reply.fName), + error); + error = Create(reply.fName, reply.vRefNum, SUBTEXT_CREATOR, DB_TYPE); + } + if (error) + err(1, "Failed to create %s: %d", PtoCstr(reply.fName), error); + + CreateResFile(newpath); + if (ResError() != 0) + err(1, "Failed to create DB %s: %d", PtoCstr(newpath), ResError()); + + free(newpath); + + return db_init(reply, 1); +} + +struct db * +db_init(SFReply reply, short is_new) +{ + Str255 buf; + struct db *tdb; + char *newpath; + Handle resh; + short error, fh, i; + + getpath(reply.vRefNum, reply.fName, &newpath, 1); + CtoPstr(newpath); + + fh = OpenResFile(newpath); + if (fh == -1) + err(1, "Failed to open %s: %d", PtoCstr(newpath), ResError()); + + tdb = xmalloczero(sizeof(struct db)); + tdb->fh = fh; + tdb->vrefnum = reply.vRefNum; + + memcpy(tdb->filename, reply.fName, reply.fName[0] + 1); + PtoCstr(tdb->filename); + + if (db_migrate(tdb, is_new) != 0) { + free(tdb); + return NULL; + } + + return tdb; +} + +void +db_close(struct db *tdb) +{ + CloseResFile(tdb->vrefnum); + free(tdb); +} + +short +db_migrate(struct db *tdb, short is_new) +{ + Handle verh, resh; + short ver, add = 0, i, nfiles, size; + struct user *user; + char *error; + + if (is_new) { + ver = DB_CUR_VERS; + + /* setup some defaults */ + sprintf(tdb->config.name, "Example Subtext BBS"); + sprintf(tdb->config.phone_number, "(555) 867-5309"); + sprintf(tdb->config.location, "Springfield"); + sprintf(tdb->config.hostname, "bbs.example.com"); + tdb->config.telnet_port = 23; + + /* create a default sysop user */ + user = user_build(tdb, "sysop", &error); + if (!user) + err(1, "Failed creating initial sysop user: %s", error); + user_set_password(tdb, user, "p4ssw0rd"); + user->is_sysop = DB_TRUE; + user_save(tdb, user); + } else { + verh = Get1Resource(DB_VERS_RTYPE, 1); + if (verh == NULL) + ver = 1; + else { + ver = (unsigned char)((*verh)[0]); + ReleaseResource(verh); + } + + if (ver == DB_CUR_VERS) + return 0; + + if (ask("Migrate this database from version %d to %d to open it?", + ver, DB_CUR_VERS) != ASK_YES) + return -1; + } + + /* per-version migrations */ + + /* store new version */ + verh = Get1Resource(DB_VERS_RTYPE, 1); + if (verh == NULL) { + verh = NewHandle(1); + add = 1; + } + (*verh)[0] = ver; + if (add) + AddResource(verh, DB_VERS_RTYPE, 1, "\p"); + else + ChangedResource(verh); + + /* update templates */ + for (i = 0; i < ntmpls; i++) { + resh = Get1Resource(tmpls[i].type, tmpls[i].id); + if (resh) + RmveResource(resh); + AddResource(tmpls[i].h, tmpls[i].type, tmpls[i].id, tmpls[i].name); + WriteResource(tmpls[i].h); + DetachResource(tmpls[i].h); + } + + UpdateResFile(tdb->fh); + + return 0; +} --- db.h Sun Dec 5 16:07:41 2021 +++ db.h Sun Dec 5 16:07:41 2021 @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2021 joshua stein <jcs@jcs.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef __DB_H__ +#define __DB_H__ + +#include <time.h> + +#include "sha2.h" + +#define SUBTEXT_CREATOR 'SUBT' + +#define DB_TYPE 'STDB' + +#define DB_CONFIG_RTYPE 'STCF' +#define DB_USER_RTYPE 'STUS' +#define DB_BOARD_RTYPE 'STBD' +#define DB_FILEAREA_RTYPE 'STFL' +#define DB_VERS_RTYPE 'STVR' + +#define DB_CUR_VERS 1 + +#define DB_TRUE 0x100 +#define DB_FALSE 0x000 + +struct config { + char name[32]; + char phone_number[16]; + char location[64]; + char hostname[32]; + short telnet_port; +}; + +struct user { + short id; + char username[32]; + char password_hash[SHA256_DIGEST_STRING_LENGTH + 1]; /* 66 */ + char password_salt[SHA256_DIGEST_STRING_LENGTH + 1]; + unsigned long created_at; + unsigned long last_seen_at; + short is_sysop; +}; + +struct db { + char filename[256]; + short fh; + short vrefnum; + struct config config; + short nusers; +}; + +struct tmpl { + Handle h; + ResType type; + Str255 name; + short id; +}; + +extern struct tmpl tmpls[10]; +extern short ntmpls; + +void load_db_tmpls(void); +struct db *db_open(AppFile *file); +struct db *db_create(void); +void db_close(struct db *tdb); + +#endif --- main.c Tue Nov 30 21:57:29 2021 +++ main.c Fri Dec 3 17:34:34 2021 @@ -19,6 +19,7 @@ #include "subtext.h" #include "console.h" +#include "db.h" #include "session.h" #include "telnet.h" #include "uthread.h" @@ -27,7 +28,7 @@ MenuHandle file_menu; short quitting = 0; struct console *cur_console = NULL; -struct config config; +struct db *db; void handle_menu(long menu_id); void update_menu(void); @@ -40,9 +41,10 @@ main(void) EventRecord event; WindowPtr event_win; GrafPtr old_port; - short event_in, n; + AppFile finder_file; + short event_in, n, finder_action, finder_count; char key; - + uthread_init(); InitGraf(&thePort); @@ -56,7 +58,8 @@ main(void) MaxApplZone(); err_init(); - + load_db_tmpls(); + mbar = GetNewMBar(MBAR_ID); SetMenuBar(mbar); apple_menu = GetMHandle(APPLE_MENU_ID); @@ -64,12 +67,20 @@ main(void) file_menu = GetMHandle(FILE_MENU_ID); update_menu(); DrawMenuBar(); - - /* TODO: get these from resources */ - memset(&config, 0, sizeof(config)); - strcpy(config.name, "Kludge BBS"); - strcpy(config.hostname, "klud.ge"); + /* see if we were started by double-clicking a .bbs file */ + CountAppFiles(&finder_action, &finder_count); + if (finder_count) { + GetAppFiles(1, &finder_file); + ClrAppFiles(1); + finder_count = 0; + db = db_open(&finder_file); + } else + db = db_open(NULL); + + if (!db) + ExitToShell(); + cur_console = console_init(); telnet_init(); @@ -152,6 +163,8 @@ main(void) break; } } + + db_close(db); return 0; } --- session.c Tue Nov 30 20:15:38 2021 +++ session.c Fri Dec 3 10:07:56 2021 @@ -174,7 +174,7 @@ session_run(struct uthread *uthread, void *arg) session_output(s, "\r\n" "Welcome to %s (%s)\r\n" - "\r\n", config.name, s->node); + "\r\n", db->config.name, s->node); session_output(s, "login: "); line = session_field_input(s, 50); --- subtext.π.r Mon Nov 22 10:07:07 2021 +++ subtext.π.r Sun Dec 5 16:19:28 2021 @@ -45,12 +45,25 @@ data 'DLOG' (128) { $"0000 0080 001A 280A" /* ...Ä..(. */ }; -data 'TMPL' (128, "USER") { - $"0269 6444 5752 4408 7573 6572 6E61 6D65" /* .idDWRD.username */ - $"4353 5452" /* CSTR */ -}; - data 'USER' (128) { $"0001 6A63 7300" /* ..jcs. */ +}; + +data 'TMPL' (128, "STCF") { + $"046E 616D 6543 3032 300C 7068 6F6E 655F" /* .nameC020.phone_ */ + $"6E75 6D62 6572 4330 3130 086C 6F63 6174" /* numberC010.locat */ + $"696F 6E43 3034 3008 686F 7374 6E61 6D65" /* ionC040.hostname */ + $"4330 3230 0B74 656C 6E65 745F 706F 7274" /* C020.telnet_port */ + $"4457 5244" /* DWRD */ +}; + +data 'TMPL' (129, "STUS") { + $"0269 6444 5752 4408 7573 6572 6E61 6D65" /* .idDWRD.username */ + $"4330 3230 0D70 6173 7377 6F72 645F 6861" /* C020¬password_ha */ + $"7368 4330 3432 0D70 6173 7377 6F72 645F" /* shC042¬password_ */ + $"7361 6C74 4330 3432 0A63 7265 6174 6564" /* saltC042.created */ + $"5F61 7444 4C4E 470C 6C61 7374 5F73 6565" /* _atDLNG.last_see */ + $"6E5F 6174 444C 4E47 0869 735F 7379 736F" /* n_atDLNG.is_syso */ + $"7042 4F4F 4C" /* pBOOL */ }; --- subtext.h Wed Nov 17 15:40:42 2021 +++ subtext.h Fri Dec 3 09:58:22 2021 @@ -17,6 +17,8 @@ #ifndef __SUBTEXT_H__ #define __SUBTEXT_H__ +#include "db.h" + #define PROGRAM_NAME "Subtext" #define MBAR_ID 128 @@ -27,12 +29,7 @@ #define FILE_MENU_ID 129 #define FILE_MENU_QUIT_ID 1 -struct config { - char name[32]; - char phone_number[16]; - char hostname[32]; -}; -extern struct config config; +extern struct db *db; extern MenuHandle file_menu; --- user.c Sun Dec 5 16:06:29 2021 +++ user.c Sun Dec 5 16:06:29 2021 @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2021 joshua stein <jcs@jcs.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <ctype.h> +#include <string.h> + +#include "db.h" +#include "sha2.h" +#include "subtext.h" +#include "user.h" +#include "util.h" + +struct user * +user_build(struct db *tdb, char *username, char **error) +{ + struct user suser; + struct user *user; + size_t len, n; + + len = strlen(username); + if (len >= sizeof(suser.username)) { + *error = xstrdup("username is too long"); + return NULL; + } + + for (n = 0; n < len; n++) { + if (!isalnum(username[n])) { + *error = xstrdup("username cannot contain " + "non-alphanumeric characters"); + return NULL; + } + } + + if ((user = user_find(tdb, username))) { + free(user); + *error = xstrdup("username already exists"); + return NULL; + } + + user = xmalloczero(sizeof(struct user)); + strncpy(user->username, username, sizeof(user->username)); + user->created_at = Time; + + return user; +} + +void +user_save(struct db *tdb, struct user *user) +{ + Handle huser = NULL; + size_t hsize = 0, len; + size_t datapos; + char *data, *lusername; + short n, is_new = 0; + + if (user->id) { + huser = Get1Resource(DB_USER_RTYPE, user->id); + if (!huser) + err(1, "user_save: user %s has id %d but no resource", + user->username, user->id); + hsize = GetHandleSize(huser); + } else { + hsize = sizeof(struct user); + huser = xNewHandle(hsize); + user->id = Count1Resources(DB_USER_RTYPE) + 1; + user->created_at = Time; + is_new = 1; + } + + HLock(huser); + data = (char *)(*huser); + datapos = 0; + + memcpy(data + datapos, &user->id, sizeof(user->id)); + datapos += sizeof(user->id); + + memcpy(data + datapos, &user->username, sizeof(user->username)); + datapos += sizeof(user->username); + + memcpy(data + datapos, &user->password_hash, sizeof(user->password_hash)); + datapos += sizeof(user->password_hash); + + memcpy(data + datapos, &user->password_salt, sizeof(user->password_salt)); + datapos += sizeof(user->password_salt); + + memcpy(data + datapos, &user->created_at, sizeof(user->created_at)); + datapos += sizeof(user->created_at); + + memcpy(data + datapos, &user->last_seen_at, sizeof(user->last_seen_at)); + datapos += sizeof(user->last_seen_at); + + memcpy(data + datapos, &user->is_sysop, sizeof(user->is_sysop)); + datapos += sizeof(user->is_sysop); + + if (datapos != hsize) + err(1, "user_save handle size %ld, data position %ld", + hsize, datapos); + + HUnlock(huser); + + /* update resource name with lowercase username for index */ + len = strlen(user->username); + lusername = xmalloc(len + 1); + lusername[0] = len; + for (n = 0; n < len; n++) + lusername[n + 1] = tolower(user->username[n]); + + if (is_new) { + AddResource(huser, DB_USER_RTYPE, user->id, lusername); + if (ResError()) + err(1, "user_save: failed to AddResource: %d", ResError()); + } else { + SetResInfo(huser, user->id, lusername); + ChangedResource(huser); + } + + WriteResource(huser); + if (ResError()) + err(1, "user_save: failed to WriteResource: %d", ResError()); + ReleaseResource(huser); + + tdb->nusers = Count1Resources(DB_USER_RTYPE); +} + +struct user * +user_find(struct db *tdb, char *username) +{ + struct user suser; + short n; + Handle huser; + Str255 pusername = { 0 }; + size_t len; + + len = strlen(username); + if (len > sizeof(suser.username)) + return NULL; + + pusername[0] = len; + for (n = 0; n < len; n++) + pusername[n + 1] = tolower(username[n]); + + huser = Get1NamedResource(DB_USER_RTYPE, pusername); + if (!huser) + return NULL; + + return user_parse(tdb, huser); +} + +struct user * +user_parse(struct db *tdb, Handle huser) +{ + struct user *user; + short hlen, datapos; + char *data; + + user = xmalloczero(sizeof(struct user)); + + data = (char *)(*huser); + datapos = 0; + + hlen = GetHandleSize(huser); + + memcpy(&user->id, data + datapos, sizeof(user->id)); + datapos += sizeof(user->id); + + memcpy(&user->username, data + datapos, sizeof(user->username)); + datapos += sizeof(user->username); + + memcpy(&user->password_hash, data + datapos, sizeof(user->password_hash)); + datapos += sizeof(user->password_hash); + + memcpy(&user->password_salt, data + datapos, sizeof(user->password_salt)); + datapos += sizeof(user->password_salt); + + memcpy(&user->created_at, data + datapos, sizeof(user->created_at)); + datapos += sizeof(user->created_at); + + memcpy(&user->last_seen_at, data + datapos, sizeof(user->last_seen_at)); + datapos += sizeof(user->last_seen_at); + + memcpy(&user->is_sysop, data + datapos, sizeof(user->is_sysop)); + datapos += sizeof(user->is_sysop); + + if (datapos != hlen) + err(1, "user_parse handle len %d, data position %d", hlen, datapos); + + HUnlock(huser); + ReleaseResource(huser); + + return user; +} + +short +user_authenticate(struct db *tdb, struct user *user, char *password) +{ + +} + +void +user_set_password(struct db *tdb, struct user *user, char *password) +{ + char hash[SHA256_DIGEST_STRING_LENGTH]; + char salt[SHA256_DIGEST_STRING_LENGTH]; + char *salted; + unsigned long tmp[8]; + size_t plen; + short n; + + for (n = 0; n < sizeof(tmp); n++) + tmp[n] = xorshift32(); + + SHA256Data((const u_int8_t *)tmp, sizeof(tmp), salt); + memcpy(&user->password_salt, &salt, sizeof(user->password_salt)); + + plen = strlen(password); + salted = xmalloc(plen + sizeof(salt)); + memcpy(salted, salt, sizeof(salt)); + memcpy(salted + sizeof(salt), password, plen); + SHA256Data((const u_int8_t *)salted, plen + sizeof(salt), hash); + free(salted); + + memcpy(&user->password_hash, &hash, sizeof(user->password_hash)); + + memset(&hash, 0, sizeof(hash)); + memset(&salt, 0, sizeof(salt)); +} --- user.h Sun Dec 5 08:41:18 2021 +++ user.h Sun Dec 5 08:41:18 2021 @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021 joshua stein <jcs@jcs.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef __USER_H__ +#define __USER_H__ + +#include "sha2.h" + +#define AUTH_USER_OK 1 +#define AUTH_USER_FAILED 2 + +struct user *user_build(struct db *tdb, char *username, char **error); +void user_save(struct db *tdb, struct user *user); +struct user *user_find(struct db *tdb, char *username); +short user_authenticate(struct db *tdb, struct user *user, char *password); +void user_set_password(struct db *tdb, struct user *user, char *password); +struct user *user_parse(struct db *tdb, Handle huser); + +#endif --- util.c Thu Oct 28 17:01:17 2021 +++ util.c Sat Dec 4 20:28:02 2021 @@ -21,7 +21,10 @@ #include "util.h" +/* ALRT resources */ #define ERROR_ALERT_ID 129 +#define ASK_ALERT_ID 130 + #define ERROR_STRING_SIZE 1024 enum { @@ -226,7 +229,52 @@ note(const char *format, ...) va_end(ap); } +short +ask(const char *format, ...) +{ + size_t len; + short ret; + WindowPtr win; + va_list ap; + + GetPort(&win); + + HLock(err_str); + va_start(ap, format); + len = vsprintf(*err_str, format, ap); + va_end(ap); + if (len >= ERROR_STRING_SIZE) + err(1, "ask string overflow!"); + + ParamText(CtoPstr(*err_str), "\p", "\p", "\p"); + ret = StopAlert(ASK_ALERT_ID, nil); + HUnlock(err_str); + + SetPort(win); + return ret; +} + + /* + * General Mac-specific non-GUI functions + */ +static unsigned long _xorshift_state = 0; +unsigned long +xorshift32(void) +{ + unsigned long x = _xorshift_state; + if (x == 0) { + x = Time; + /* TODO: stir in seed stored in resource file */ + } + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + return _xorshift_state = x; +} + + +/* * Error checking wrappers for Mac toolkit functions */ @@ -266,6 +314,7 @@ xGetString(short id) return h; } + /* * Filesystem utilities */ @@ -367,9 +416,9 @@ getpath(short vRefNum, Str255 fileName, char **ret, bo warn("Unknown filesystem type 0x%x", wvol.ioVSigWord); return 1; } - - wcinfo.ioNamePtr = (StringPtr)&name; + wcinfo.ioVRefNum = vRefNum; + wcinfo.ioNamePtr = (StringPtr)&name; wcinfo.ioFDirIndex = -1; wcinfo.ioDrParID = wdir.ioWDDirID; wcinfo.ioDrDirID = wdir.ioWDDirID; @@ -482,6 +531,7 @@ FSReadLine(short frefnum, char *buf, size_t buflen) /* nothing found until the end of the file */ return total_read; } + /* * General Mac-specific GUI functions --- util.h Thu Oct 28 17:01:09 2021 +++ util.h Sun Dec 5 16:22:51 2021 @@ -61,15 +61,22 @@ void *xreallocarray(void *, size_t, size_t); char *xstrdup(const char *); short getline(char *str, size_t len, char **ret); +#if 0 /* from strnatcmp.c */ int strnatcmp(char const *a, char const *b); int strnatcasecmp(char const *a, char const *b); +#endif +unsigned long xorshift32(void); + void err_init(void); void warnx(const char *format, ...); void warn(const char *format, ...); void err(short retcode, const char *format, ...); void note(const char *format, ...); +short ask(const char *format, ...); +#define ASK_YES 1 +#define ASK_NO 2 Handle xNewHandle(unsigned long size); Handle xGetResource(ResType type, short id);