/* * Copyright (c) 2021-2022 joshua stein * * 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 #include #include #include "subtext.h" #include "board.h" #include "bile.h" #include "db.h" #include "folder.h" #include "logger.h" #include "mail.h" #include "main_menu.h" #include "user.h" #include "util.h" struct struct_field config_fields[] = { { "BBS Name", CONFIG_TYPE_STRING, offsetof(struct config, name), 1, member_size(struct config, name) }, { "Phone Number", CONFIG_TYPE_STRING, offsetof(struct config, phone_number), 1, member_size(struct config, phone_number) }, { "Location", CONFIG_TYPE_STRING, offsetof(struct config, location), 1, member_size(struct config, location) }, { "Timezone (Abbrev)", CONFIG_TYPE_STRING, offsetof(struct config, timezone), 1, member_size(struct config, timezone) }, { "Timezone (UTC Offset)", CONFIG_TYPE_SHORT, offsetof(struct config, timezone_utcoff), -1200, 1400 }, { "Allow Open Signup", CONFIG_TYPE_BOOLEAN, offsetof(struct config, open_signup), 0, 0 }, { "Hostname", CONFIG_TYPE_STRING, offsetof(struct config, hostname), 1, member_size(struct config, hostname) }, { "Telnet Port", CONFIG_TYPE_SHORT, offsetof(struct config, telnet_port), 0, 65535, CONFIG_REQUIRES_TELNET_REINIT }, { "Telnet Trusted Proxy IP", CONFIG_TYPE_IP, offsetof(struct config, trusted_proxy_ip), 0, 1, CONFIG_REQUIRES_TELNET_REINIT }, { "Telnet Trusted Proxy UDP Port", CONFIG_TYPE_SHORT, offsetof(struct config, trusted_proxy_udp_port), 0, 65535, CONFIG_REQUIRES_TELNET_REINIT }, { "IP Geolocation Database Path", CONFIG_TYPE_STRING, offsetof(struct config, ipdb_path), 0, member_size(struct config, ipdb_path), CONFIG_REQUIRES_IPDB_REINIT }, { "Syslog Server IP", CONFIG_TYPE_IP, offsetof(struct config, syslog_ip), 0, 1, CONFIG_REQUIRES_SYSLOG_REINIT }, { "Modem Port", CONFIG_TYPE_SHORT, offsetof(struct config, modem_port), 0, 2, CONFIG_REQUIRES_SERIAL_REINIT }, { "Modem Port Speed", CONFIG_TYPE_LONG, offsetof(struct config, modem_speed), 300, 115200, CONFIG_REQUIRES_SERIAL_REINIT }, { "Modem Bits/Parity/Stop", CONFIG_TYPE_STRING, offsetof(struct config, modem_parity), 1, member_size(struct config, modem_parity), CONFIG_REQUIRES_SERIAL_REINIT }, { "Modem Init String", CONFIG_TYPE_STRING, offsetof(struct config, modem_init), 1, member_size(struct config, modem_init), CONFIG_REQUIRES_SERIAL_REINIT }, { "Modem Answer After Rings", CONFIG_TYPE_SHORT, offsetof(struct config, modem_rings), 1, 100 }, { "Max Idle Minutes", CONFIG_TYPE_SHORT, offsetof(struct config, max_idle_minutes), 0, USHRT_MAX }, { "Max Sysop Idle Minutes", CONFIG_TYPE_SHORT, offsetof(struct config, max_sysop_idle_minutes), 0, USHRT_MAX }, { "Max Login Seconds", CONFIG_TYPE_SHORT, offsetof(struct config, max_login_seconds), 1, USHRT_MAX }, { "Screen Blanker Idle Seconds", CONFIG_TYPE_SHORT, offsetof(struct config, blanker_idle_seconds), 0, USHRT_MAX }, { "Screen Blanker Runtime Seconds", CONFIG_TYPE_SHORT, offsetof(struct config, blanker_runtime_seconds), 1, USHRT_MAX }, { "Prune Session Logs After N Days", CONFIG_TYPE_SHORT, offsetof(struct config, session_log_prune_days), 0, USHRT_MAX }, { "Prune Mail After N Days", CONFIG_TYPE_SHORT, offsetof(struct config, mail_prune_days), 0, USHRT_MAX }, { "FTN Network Name", CONFIG_TYPE_STRING, offsetof(struct config, ftn_network), 0, member_size(struct config, ftn_network), CONFIG_REQUIRES_BINKP_REINIT }, { "FTN Local Node Address", CONFIG_TYPE_STRING, offsetof(struct config, ftn_node_addr), 0, member_size(struct config, ftn_node_addr), CONFIG_REQUIRES_BINKP_REINIT }, { "FTN Hub Node Address", CONFIG_TYPE_STRING, offsetof(struct config, ftn_hub_addr), 0, member_size(struct config, ftn_hub_addr), CONFIG_REQUIRES_BINKP_REINIT }, { "FTN Hub Packet Password", CONFIG_TYPE_PASSWORD, offsetof(struct config, ftn_hub_pkt_password), 0, member_size(struct config, ftn_hub_pkt_password) }, { "FTN Hub Binkp Hostname", CONFIG_TYPE_STRING, offsetof(struct config, binkp_hostname), 0, member_size(struct config, binkp_hostname), CONFIG_REQUIRES_BINKP_REINIT }, { "FTN Hub Binkp Port", CONFIG_TYPE_SHORT, offsetof(struct config, binkp_port), 0, 65535 }, { "FTN Hub Binkp Password", CONFIG_TYPE_PASSWORD, offsetof(struct config, binkp_password), 0, member_size(struct config, binkp_password) }, { "FTN Hub Binkp Poll Seconds", CONFIG_TYPE_LONG, offsetof(struct config, binkp_interval_seconds), 1, LONG_MAX }, { "FTN Delete Packets After Processing", CONFIG_TYPE_BOOLEAN, offsetof(struct config, binkp_delete_done), 0, 0 }, { "FTN Max Tossed Message Size", CONFIG_TYPE_LONG, offsetof(struct config, ftn_max_tossed_message_size), 0, LONG_MAX }, }; size_t nconfig_fields = nitems(config_fields); struct db * db_init(Str255 file, short vrefnum, struct bile *bile); short db_migrate(struct db *tdb, short is_new, Str255 basepath); void db_config_load(struct db *tdb); const char * db_view_filename(short id); struct db * db_open(Str255 file, short vrefnum, bool ignore_last) { Str255 filepath; struct stat sb; Point pt = { 75, 100 }; SFReply reply = { 0 }; SFTypeList types; StringHandle lastfileh; struct db *ret; if (file[0]) { getpath(vrefnum, file, filepath, true); if (FStat(filepath, &sb) == 0) return db_init(file, vrefnum, NULL); warn("Failed to open %s", PtoCstr(file)); } if (!ignore_last && (lastfileh = GetString(STR_LAST_DB))) { HLock(lastfileh); memcpy(filepath, *lastfileh, sizeof(filepath)); HUnlock(lastfileh); ReleaseResource(lastfileh); if (FStat(filepath, &sb) == 0) { ret = db_init(filepath, 0, NULL); return ret; } } /* no file passed, no last file, prompt */ types[0] = DB_TYPE; SFGetFile(pt, NULL, NULL, 1, &types, NULL, &reply); if (reply.good) { ret = db_init(reply.fName, reply.vRefNum, NULL); return ret; } return db_create(); } struct db * db_create(void) { Point pt = { 75, 100 }; SFReply reply; struct bile *bile; short error = 0; SFPutFile(pt, "\pCreate new Subtext DB:", "\p", NULL, &reply); if (!reply.good) return NULL; bile = bile_create(reply.fName, reply.vRefNum, SUBTEXT_CREATOR, DB_TYPE); if (bile == NULL && bile_error(NULL) == dupFNErr) { error = FSDelete(reply.fName, reply.vRefNum); if (error) panic("Failed to re-create file %s: %d", PtoCstr(reply.fName), error); bile = bile_create(reply.fName, reply.vRefNum, SUBTEXT_CREATOR, DB_TYPE); } if (bile == NULL) panic("Failed to create file %s: %d", PtoCstr(reply.fName), error); return db_init(reply.fName, reply.vRefNum, bile); } struct db * db_init(Str255 path, short vrefnum, struct bile *bile) { Str255 fullpath, newfullpath, viewsdir; struct db *tdb; Handle lastfileh; short was_new, error; long dirid; was_new = (bile != NULL); if (bile == NULL) { bile = bile_open(path, vrefnum); if (bile == NULL) { if (ask("Attempt recovery with backup map?")) { bile = bile_open_recover_map(path, vrefnum); if (bile == NULL) panic("Failed to recover DB file %s: %d", PtoCstr(path), bile_error(NULL)); } else { panic("Failed to open DB file %s: %d", PtoCstr(path), bile_error(NULL)); } } } /* we got this far, store it as the last-accessed file */ if (vrefnum == 0) memcpy(&fullpath, path, sizeof(fullpath)); else getpath(vrefnum, path, fullpath, true); lastfileh = Get1Resource('STR ', STR_LAST_DB); if (lastfileh) xSetHandleSize(lastfileh, fullpath[0] + 1); else lastfileh = xNewHandle(fullpath[0] + 1); HLock(lastfileh); memcpy(*lastfileh, fullpath, fullpath[0] + 1); HUnlock(lastfileh); if (HomeResFile(lastfileh) == -1) AddResource(lastfileh, 'STR ', STR_LAST_DB, "\pSTR_LAST_DB"); else ChangedResource(lastfileh); WriteResource(lastfileh); DetachResource(lastfileh); tdb = xmalloczero(sizeof(struct db)); if (tdb == NULL) panic("Can't create db"); tdb->bile = bile; /* create views directory if it doesn't exist */ if (getpath(tdb->bile->vrefnum, tdb->bile->filename, viewsdir, false) != 0) panic("getpath failed on %s", PtoCstr(tdb->bile->filename)); PtoCstr(viewsdir); strlcat((char *)viewsdir, ":views", sizeof(Str255)); CtoPstr(viewsdir); if (!FIsDir(viewsdir)) { error = DirCreate(tdb->bile->vrefnum, 0, viewsdir, &dirid); if (error) panic("Failed creating %s: %d", PtoCstr(viewsdir), error); } if (db_migrate(tdb, was_new, fullpath) != 0) { bile_close(tdb->bile); xfree(&tdb); return NULL; } if (!was_new) db_config_load(tdb); memcpy(newfullpath, fullpath, sizeof(newfullpath)); PtoCstr(newfullpath); strlcat((char *)&newfullpath, "-sessions", sizeof(newfullpath)); CtoPstr(newfullpath); tdb->sessions_bile = bile_open(newfullpath, vrefnum); if (tdb->sessions_bile == NULL) { tdb->sessions_bile = bile_create(newfullpath, vrefnum, SUBTEXT_CREATOR, SL_TYPE); if (tdb->sessions_bile == NULL) panic("Couldn't create %s: %d", PtoCstr(newfullpath), bile_error(NULL)); } memcpy(newfullpath, fullpath, sizeof(newfullpath)); PtoCstr(newfullpath); strlcat((char *)&newfullpath, "-mail", sizeof(newfullpath)); CtoPstr(newfullpath); tdb->mail_bile = bile_open(newfullpath, vrefnum); if (tdb->mail_bile == NULL) { tdb->mail_bile = bile_create(newfullpath, vrefnum, SUBTEXT_CREATOR, MAIL_SPOOL_TYPE); if (tdb->mail_bile == NULL) panic("Couldn't create %s: %d", PtoCstr(newfullpath), bile_error(NULL)); } return tdb; } void db_close(struct db *tdb) { short n; bile_close(tdb->bile); xfree(&tdb->bile); if (tdb->sessions_bile != NULL) { bile_close(tdb->sessions_bile); xfree(&tdb->sessions_bile); } if (tdb->mail_bile != NULL) { bile_close(tdb->mail_bile); xfree(&tdb->mail_bile); } if (tdb->boards) { for (n = 0; n < tdb->nboards; n++) { if (tdb->boards[n].bile) bile_close(tdb->boards[n].bile); } xfree(&tdb->boards); } if (tdb->folders) { for (n = 0; n < tdb->nfolders; n++) { if (tdb->folders[n].bile) bile_close(tdb->folders[n].bile); } xfree(&tdb->folders); } xfree(&tdb); } short db_migrate(struct db *tdb, short is_new, Str255 fullpath) { struct user *suser; struct db *olddb; char ver; 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"); sprintf(tdb->config.timezone, "CT"); tdb->config.modem_speed = 9600; sprintf(tdb->config.modem_init, "ATV1S0=2&D2"); sprintf(tdb->config.modem_parity, "8N1"); tdb->config.modem_rings = 1; tdb->config.max_idle_minutes = 5; tdb->config.max_sysop_idle_minutes = 60; tdb->config.max_login_seconds = 90; tdb->config.blanker_idle_seconds = (60 * 60); tdb->config.blanker_runtime_seconds = 30; tdb->config.session_log_prune_days = 21; tdb->config.binkp_port = 24554; tdb->config.binkp_interval_seconds = (60 * 60 * 3); tdb->config.mail_prune_days = 180; tdb->config.ftn_max_tossed_message_size = 16 * 1024; db_config_save(tdb); /* create a default sysop user */ suser = xmalloczero(sizeof(struct user)); if (suser == NULL) panic("Can't allocate new user"); strncpy(suser->username, "sysop", sizeof(suser->username)); suser->created_at = Time; suser->is_enabled = DB_TRUE; user_set_password(suser, "p4ssw0rd"); suser->is_sysop = DB_TRUE; /* user_save assumes db is already set */ olddb = db; db = tdb; user_save(suser); xfree(&suser); user_cache_usernames(); db = olddb; } else { if (bile_read(tdb->bile, DB_VERS_RTYPE, 1, &ver, 1) != 1) ver = 1; 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 */ while (ver < DB_CUR_VERS) { progress("Migrating from version %d to %d...", (short)ver, (short)(ver + 1)); if (ver < 18) panic("This Subtext database is too old to upgrade. Please " "run an older Subtext release first to upgrade it."); switch (ver) { case 18: { /* 18->19, ipdb_path, add session_log.location */ struct config new_config = { 0 }; Str255 newfullpath; struct bile *sessions_bile; size_t nids, n, size; unsigned long *ids; char *data; bile_read(tdb->bile, DB_CONFIG_RTYPE, 1, (char *)&new_config, sizeof(new_config)); new_config.ipdb_path[0] = '\0'; bile_write(tdb->bile, DB_CONFIG_RTYPE, 1, &new_config, sizeof(new_config)); /* migrate session log entries */ memcpy(&newfullpath, fullpath, sizeof(newfullpath)); PtoCstr(newfullpath); strlcat((char *)&newfullpath, "-sessions", sizeof(newfullpath)); CtoPstr(newfullpath); sessions_bile = bile_open(newfullpath, tdb->bile->vrefnum); if (sessions_bile == NULL) /* nothing to migrate */ break; nids = bile_ids_by_type(sessions_bile, SL_LOG_RTYPE, &ids); for (n = 0; n < nids; n++) { size = bile_read_alloc(sessions_bile, SL_LOG_RTYPE, ids[n], &data); size += member_size(struct session_log, location); if (bile_resize(sessions_bile, SL_LOG_RTYPE, ids[n], size) != size) panic("failed resizing session log %ld", ids[n]); } bile_flush(sessions_bile, true); xfree(&ids); bile_close(sessions_bile); break; } case 19: { /* 19->20, modem rings */ struct config new_config = { 0 }; bile_read(tdb->bile, DB_CONFIG_RTYPE, 1, (char *)&new_config, sizeof(new_config)); new_config.modem_rings = 1; bile_write(tdb->bile, DB_CONFIG_RTYPE, 1, &new_config, sizeof(new_config)); break; } case 20: { /* 20->21, move views out of db */ size_t size; char *data; #define DB_TEXT_TYPE 'TEXT' #define DB_TEXT_MENU_ID 1 #define DB_TEXT_SHORTMENU_ID 2 #define DB_TEXT_ISSUE_ID 3 #define DB_TEXT_SIGNUP_ID 4 #define DB_TEXT_PAGE_SYSOP_ID 5 #define DB_TEXT_NO_FREE_NODES_ID 6 #define DB_TEXT_SIGNOFF_ID 7 #define DB_TEXT_MENU_OPTIONS_ID 8 progress("Migrating views out of database..."); if ((size = bile_read_alloc(tdb->bile, DB_TEXT_TYPE, DB_TEXT_MENU_ID, &data))) { db_view_write(tdb, DB_VIEW_MENU, data, size); xfree(&data); bile_delete(tdb->bile, DB_TEXT_TYPE, DB_TEXT_MENU_ID, 0); } if ((size = bile_read_alloc(tdb->bile, DB_TEXT_TYPE, DB_TEXT_SHORTMENU_ID, &data))) { db_view_write(tdb, DB_VIEW_SHORTMENU, data, size); xfree(&data); bile_delete(tdb->bile, DB_TEXT_TYPE, DB_TEXT_SHORTMENU_ID, 0); } if ((size = bile_read_alloc(tdb->bile, DB_TEXT_TYPE, DB_TEXT_ISSUE_ID, &data))) { db_view_write(tdb, DB_VIEW_ISSUE, data, size); xfree(&data); bile_delete(tdb->bile, DB_TEXT_TYPE, DB_TEXT_ISSUE_ID, 0); } if ((size = bile_read_alloc(tdb->bile, DB_TEXT_TYPE, DB_TEXT_SIGNUP_ID, &data))) { db_view_write(tdb, DB_VIEW_SIGNUP, data, size); xfree(&data); bile_delete(tdb->bile, DB_TEXT_TYPE, DB_TEXT_SIGNUP_ID, 0); } if ((size = bile_read_alloc(tdb->bile, DB_TEXT_TYPE, DB_TEXT_PAGE_SYSOP_ID, &data))) { db_view_write(tdb, DB_VIEW_PAGE_SYSOP, data, size); xfree(&data); bile_delete(tdb->bile, DB_TEXT_TYPE, DB_TEXT_PAGE_SYSOP_ID, 0); } if ((size = bile_read_alloc(tdb->bile, DB_TEXT_TYPE, DB_TEXT_NO_FREE_NODES_ID, &data))) { db_view_write(tdb, DB_VIEW_NO_FREE_NODES, data, size); xfree(&data); bile_delete(tdb->bile, DB_TEXT_TYPE, DB_TEXT_NO_FREE_NODES_ID, 0); } if ((size = bile_read_alloc(tdb->bile, DB_TEXT_TYPE, DB_TEXT_SIGNOFF_ID, &data))) { db_view_write(tdb, DB_VIEW_SIGNOFF, data, size); xfree(&data); bile_delete(tdb->bile, DB_TEXT_TYPE, DB_TEXT_SIGNOFF_ID, 0); } if ((size = bile_read_alloc(tdb->bile, DB_TEXT_TYPE, DB_TEXT_MENU_OPTIONS_ID, &data))) { db_view_write(tdb, DB_VIEW_MENU_OPTIONS, data, size); xfree(&data); bile_delete(tdb->bile, DB_TEXT_TYPE, DB_TEXT_MENU_OPTIONS_ID, 0); } break; } case 21: { /* 20->21, syslog ip */ struct config new_config = { 0 }; bile_read(tdb->bile, DB_CONFIG_RTYPE, 1, (char *)&new_config, sizeof(new_config)); new_config.syslog_ip = 0; bile_write(tdb->bile, DB_CONFIG_RTYPE, 1, &new_config, sizeof(new_config)); break; } } ver++; } progress(NULL); /* store new version */ ver = DB_CUR_VERS; if (bile_write(tdb->bile, DB_VERS_RTYPE, 1, &ver, 1) != 1) panic("Failed writing new version: %d", bile_error(tdb->bile)); return 0; } void db_config_save(struct db *tdb) { if (bile_write(tdb->bile, DB_CONFIG_RTYPE, 1, &tdb->config, sizeof(tdb->config)) != sizeof(tdb->config)) panic("db_config_save: failed to write: %d", bile_error(tdb->bile)); } void db_config_load(struct db *tdb) { size_t rlen; char *newconfig; rlen = bile_read_alloc(tdb->bile, DB_CONFIG_RTYPE, 1, &newconfig); if (rlen == 0 || bile_error(tdb->bile)) panic("db_config_load: error reading config: %d", bile_error(tdb->bile)); if (rlen != sizeof(tdb->config)) warn("db_config_load: read config of size %lu, but expected %lu", rlen, sizeof(tdb->config)); memcpy(&tdb->config, newconfig, sizeof(tdb->config)); xfree(&newconfig); } void db_cache_boards(struct db *tdb) { Str255 db_filename, board_filename; struct board_id_time_map first_map; size_t n, size; unsigned long *ids; struct bile_object *obj; char *data = NULL; if (tdb->boards) { for (n = 0; n < tdb->nboards; n++) { if (tdb->boards[n].bile) bile_close(tdb->boards[n].bile); } xfree(&tdb->boards); } if (getpath(tdb->bile->vrefnum, tdb->bile->filename, db_filename, false) != 0) panic("getpath failed on %s", PtoCstr(tdb->bile->filename)); PtoCstr(db_filename); tdb->nboards = bile_sorted_ids_by_type(tdb->bile, DB_BOARD_RTYPE, &ids); if (!tdb->nboards) return; tdb->boards = xcalloc(tdb->nboards, sizeof(struct board)); if (tdb->boards == NULL) { tdb->nboards = 0; return; } /* * Read ids first so we have a consistent order, then try to open or * fix/recreate each bile, which may change their order in the map */ for (n = 0; n < tdb->nboards; n++) { obj = bile_find(tdb->bile, DB_BOARD_RTYPE, ids[n]); if (obj == NULL) break; size = bile_read_alloc(tdb->bile, DB_BOARD_RTYPE, obj->id, &data); bile_unmarshall_object(tdb->bile, board_object_fields, nboard_object_fields, data, size, (char *)(&tdb->boards[n]), sizeof(struct board), true); xfree(&data); xfree(&obj); } xfree(&ids); for (n = 0; n < tdb->nboards; n++) { snprintf((char *)board_filename, sizeof(board_filename), "%s:%lu.%s", db_filename, tdb->boards[n].id, BOARD_FILENAME_EXT); CtoPstr(board_filename); tdb->boards[n].bile = bile_open(board_filename, tdb->bile->vrefnum); if (tdb->boards[n].bile != NULL) goto opened; PtoCstr(board_filename); if (bile_error(NULL) != fnfErr && ask("Attempt recovery of %s with backup map?", board_filename)) { CtoPstr(board_filename); tdb->boards[n].bile = bile_open_recover_map(board_filename, tdb->bile->vrefnum); if (tdb->boards[n].bile != NULL) continue; warn("Failed to recover board file %s: %d", PtoCstr(board_filename), bile_error(NULL)); } if (ask("Failed to open board bile %s (error %d), recreate it?", board_filename, bile_error(NULL)) == false) panic("Can't open board file, exiting"); tdb->boards[n].bile = db_board_create(tdb, &tdb->boards[n], true); if (tdb->boards[n].bile == NULL) panic("Failed to create board file %s: %d", board_filename, bile_error(NULL)); opened: size = bile_read(tdb->boards[n].bile, BOARD_SORTED_ID_MAP_RTYPE, 1, &first_map, sizeof(first_map)); if (size != sizeof(first_map)) { logger_printf("[db] Reindexing ids on board %s", tdb->boards[n].name); board_index_sorted_post_ids(&tdb->boards[n], NULL); size = bile_read(tdb->boards[n].bile, BOARD_SORTED_ID_MAP_RTYPE, 1, &first_map, sizeof(first_map)); if (size != sizeof(first_map)) { tdb->boards[n].last_post_at = 0; continue; } } tdb->boards[n].last_post_at = first_map.time; } } struct bile * db_board_create(struct db *tdb, struct board *board, bool delete_first) { Str255 db_filename, board_filename; struct bile *board_bile; size_t size; short ret; char *data; ret = bile_marshall_object(tdb->bile, board_object_fields, nboard_object_fields, board, &data, &size); if (ret != 0 || size == 0) { warn("db_board_create: failed to marshall object"); return NULL; } if (bile_write(tdb->bile, DB_BOARD_RTYPE, board->id, data, size) != size) panic("save of new board failed: %d", bile_error(tdb->bile)); xfree(&data); if (getpath(tdb->bile->vrefnum, tdb->bile->filename, db_filename, false) != 0) panic("getpath failed on %s", PtoCstr(tdb->bile->filename)); PtoCstr(db_filename); snprintf((char *)&board_filename, sizeof(board_filename), "%s:%lu.%s", db_filename, board->id, BOARD_FILENAME_EXT); CtoPstr(board_filename); if (delete_first) FSDelete(board_filename, tdb->bile->vrefnum); board_bile = bile_create(board_filename, tdb->bile->vrefnum, SUBTEXT_CREATOR, DB_BOARD_RTYPE); if (board_bile == NULL) panic("Failed creating new board bile at %s: %d", PtoCstr(board_filename), bile_error(NULL)); return board_bile; } void db_board_delete(struct db *tdb, struct board *board) { if (bile_delete(tdb->bile, DB_BOARD_RTYPE, board->id, 0) != 0) { warn("deletion of board %ld failed: %d", board->id, bile_error(tdb->bile)); return; } bile_close(board->bile); } void db_cache_folders(struct db *tdb) { Str255 db_filename, folder_filename, folder_dir; size_t n, size; unsigned long *ids, id; struct bile_object *obj; char *data = NULL; short error; if (tdb->folders) { for (n = 0; n < tdb->nfolders; n++) { if (tdb->folders[n].bile) bile_close(tdb->folders[n].bile); } xfree(&tdb->folders); } if (getpath(tdb->bile->vrefnum, tdb->bile->filename, db_filename, false) != 0) panic("getpath failed on %s", PtoCstr(tdb->bile->filename)); PtoCstr(db_filename); tdb->nfolders = bile_sorted_ids_by_type(tdb->bile, DB_FOLDER_RTYPE, &ids); if (!tdb->nfolders) return; tdb->folders = xcalloc(tdb->nfolders, sizeof(struct folder)); if (tdb->folders == NULL) { tdb->nfolders = 0; return; } /* * Read ids first so we have a consistent order, then try to open or * fix/recreate each bile, which may change their order in the map */ for (n = 0; n < tdb->nfolders; n++) { obj = bile_find(tdb->bile, DB_FOLDER_RTYPE, ids[n]); if (obj == NULL) break; size = bile_read_alloc(tdb->bile, DB_FOLDER_RTYPE, obj->id, &data); if (size == 0 || data == NULL) break; bile_unmarshall_object(tdb->bile, folder_object_fields, nfolder_object_fields, data, size, (char *)(&tdb->folders[n]), sizeof(struct folder), true); xfree(&data); } xfree(&ids); for (n = 0; n < tdb->nfolders; n++) { snprintf((char *)folder_filename, sizeof(folder_filename), "%s:%lu.%s", db_filename, tdb->folders[n].id, FOLDER_FILENAME_EXT); CtoPstr(folder_filename); tdb->folders[n].bile = bile_open(folder_filename, tdb->bile->vrefnum); if (tdb->folders[n].bile != NULL) continue; PtoCstr(folder_filename); if (bile_error(NULL) != fnfErr && ask("Attempt recovery of %s with backup map?", folder_filename)) { CtoPstr(folder_filename); tdb->folders[n].bile = bile_open_recover_map(folder_filename, tdb->bile->vrefnum); if (tdb->folders[n].bile != NULL) continue; warn("Failed to recover folder bile %s: %d", PtoCstr(folder_filename), bile_error(NULL)); } if (ask("Failed to open folder bile %s (error %d), recreate it?", folder_filename, bile_error(NULL)) == false) exit(0); tdb->folders[n].bile = db_folder_create(tdb, &tdb->folders[n], true); if (tdb->folders[n].bile == NULL) panic("Failed to create folder bile %s: %d", folder_filename, bile_error(NULL)); } for (n = 0; n < tdb->nfolders; n++) { /* make sure directory exists */ memcpy(folder_dir, tdb->folders[n].path, sizeof(folder_dir)); CtoPstr(folder_dir); if (!FIsDir(folder_dir) && ask("Folder %ld path \"%s\" does not exist, create it?", tdb->folders[n].id, tdb->folders[n].path)) { error = DirCreate(db->bile->vrefnum, 0, folder_dir, &id); if (error) warn("Failed creating %s: %d", tdb->folders[n].path, error); } } } struct bile * db_folder_create(struct db *tdb, struct folder *folder, bool delete_first) { Str255 db_filename, folder_filename, folder_dir; struct bile *folder_bile; size_t size; short ret, newid, error; char *data; ret = bile_marshall_object(tdb->bile, folder_object_fields, nfolder_object_fields, folder, &data, &size); if (ret != 0 || size == 0) { warn("db_folder_create: failed to marshall object"); return NULL; } if (bile_write(tdb->bile, DB_FOLDER_RTYPE, folder->id, data, size) != size) panic("save of new folder failed: %d", bile_error(tdb->bile)); xfree(&data); if (getpath(tdb->bile->vrefnum, tdb->bile->filename, db_filename, false) != 0) panic("getpath failed on %s", PtoCstr(tdb->bile->filename)); PtoCstr(db_filename); snprintf((char *)&folder_filename, sizeof(folder_filename), "%s:%lu.%s", db_filename, folder->id, FOLDER_FILENAME_EXT); CtoPstr(folder_filename); if (delete_first) FSDelete(folder_filename, tdb->bile->vrefnum); folder_bile = bile_create(folder_filename, tdb->bile->vrefnum, SUBTEXT_CREATOR, DB_FOLDER_RTYPE); if (folder_bile == NULL) panic("Failed creating new folder bile at %s: %d", PtoCstr(folder_filename), bile_error(NULL)); memcpy(&folder_dir, folder->path, sizeof(folder_dir)); CtoPstr(folder_dir); if (!FIsDir(folder_dir)) { error = DirCreate(tdb->bile->vrefnum, 0, folder_dir, &newid); if (error) warn("Failed creating %s: %d", folder->path, error); } return folder_bile; } void db_folder_delete(struct db *tdb, struct folder *folder) { if (bile_delete(tdb->bile, DB_FOLDER_RTYPE, folder->id, 0) != 0) { warn("deletion of folder %ld failed: %d", folder->id, bile_error(tdb->bile)); return; } bile_close(folder->bile); } const char * db_view_filename(short id) { switch (id) { case DB_VIEW_MENU: return "menu.txt"; case DB_VIEW_SHORTMENU: return "short_menu.txt"; case DB_VIEW_ISSUE: return "issue.txt"; case DB_VIEW_SIGNUP: return "signup.txt"; case DB_VIEW_PAGE_SYSOP: return "page_sysop.txt"; case DB_VIEW_NO_FREE_NODES: return "no_free_nodes.txt"; case DB_VIEW_SIGNOFF: return "signoff.txt"; case DB_VIEW_MENU_OPTIONS: return "menu_options.txt"; default: panic("db_view_filename: unknown id %d", id); } } void db_cache_views(struct db *tdb) { Str255 viewdir, viewpath; struct stat sb; struct main_menu_option *opts; Handle default_menu; short n; size_t size; short error, frefnum; logger_printf("[db] Caching views"); if (getpath(tdb->bile->vrefnum, tdb->bile->filename, viewdir, false) != 0) panic("getpath failed on %s", PtoCstr(tdb->bile->filename)); PtoCstr(viewdir); strlcat((char *)viewdir, ":views:", sizeof(Str255)); for (n = 0; n < DB_VIEW_COUNT; n++) { if (tdb->views[n]) xfree(&tdb->views[n]); tdb->views[n] = NULL; strlcpy((char *)viewpath, (char *)viewdir, sizeof(Str255)); strlcat((char *)viewpath, db_view_filename(n), sizeof(Str255)); CtoPstr(viewpath); if (FStat(viewpath, &sb) != 0) { if (n == DB_VIEW_MENU_OPTIONS) { default_menu = GetResource('TEXT', MENU_DEFAULTS_ID); if (default_menu == NULL) panic("Failed to find TEXT resource %d for menu options", MENU_DEFAULTS_ID); size = GetHandleSize(default_menu); HLock(default_menu); db_view_write(tdb, n, *default_menu, size); HUnlock(default_menu); ReleaseResource(default_menu); FStat(viewpath, &sb); } else { /* create a blank file so the sysop knows what to edit */ db_view_write(tdb, n, "", 0); /* but leave the view NULL */ continue; } } if (sb.st_size == 0) continue; tdb->views[n] = xmalloc(sb.st_size + 1); if (tdb->views[n] == NULL) panic("Failed allocating %ld for view %s", sb.st_size, PtoCstr(viewpath)); error = FSOpen(viewpath, 0, &frefnum); if (error) panic("Error opening view %s: %d", PtoCstr(viewpath), error); size = sb.st_size; error = FSRead(frefnum, &size, tdb->views[n]); FSClose(frefnum); if (error && error != eofErr) panic("Error reading view %s: %d", PtoCstr(viewpath), error); if (size > sb.st_size) panic("FSRead read more (%ld) than file size (%ld) for %s", size, sb.st_size, PtoCstr(viewpath)); tdb->views[n][size] = '\0'; if (n == DB_VIEW_MENU_OPTIONS) { opts = main_menu_parse(tdb->views[n], sb.st_size); if (opts) { if (main_menu_options != NULL) xfree(&main_menu_options); main_menu_options = opts; } } } } void db_view_write(struct db *tdb, short id, char *str, size_t size) { Str255 viewpath; size_t len; short error, frefnum; if (getpath(tdb->bile->vrefnum, tdb->bile->filename, viewpath, false) != 0) panic("getpath failed on %s", PtoCstr(tdb->bile->filename)); PtoCstr(viewpath); strlcat((char *)viewpath, ":views:", sizeof(Str255)); strlcat((char *)viewpath, db_view_filename(id), sizeof(Str255)); logger_printf("[db] Writing default view %s", viewpath); CtoPstr(viewpath); error = Create(viewpath, 0, 'ttxt', 'TEXT'); if (error == dupFNErr) panic("View file already exists: %s", PtoCstr(viewpath)); if (error) panic("Failed creating %s: %d", PtoCstr(viewpath), error); if ((error = FSOpen(viewpath, 0, &frefnum))) panic("Failed opening newly-created %s: %d", PtoCstr(viewpath), error); len = size; if ((error = Allocate(frefnum, &len))) panic("Failed setting %s to size %ld: %d", PtoCstr(viewpath), size, error); len = size; if ((error = FSWrite(frefnum, &len, str))) panic("Failed writing view to file %s: %d", PtoCstr(viewpath), error); if (len != size) panic("Short write of view to file %s: %ld != %ld", PtoCstr(viewpath), len, size); FSClose(frefnum); }