#include #ifdef __NetBSD__ #include #else #include #endif #include #include #include #include #include "lua.h" #include "lauxlib.h" /* If you encounter assertion failures in this file and _didn't_ manually edit * it, lease report the .tlv file that caused them: http://akkartik.name/contact. * * Manually edited files can have cryptic errors. Teliva's first priority is * to be secure, so it requires a fairly rigid file format and errors out if * things are even slightly amiss. */ /* This code is surprisingly hairy. Estimate of buffer overflows: 2. */ static void teliva_load_multiline_string(lua_State* L, FILE* in, char* line, int capacity) { luaL_Buffer b; luaL_buffinit(L, &b); int expected_indent = -1; while (1) { if (feof(in)) break; char c = fgetc(in); ungetc(c, in); if (c != ' ') { /* new definition; signal end to caller without reading a new line */ strcpy(line, "-\n"); break; } memset(line, '\0', capacity); if (fgets(line, capacity, in) == NULL) break; /* eof */ int max = strlen(line); assert(line[max-1] == '\n'); int indent = 0; while (indent < max-1 && line[indent] == ' ') ++indent; if (line[indent] != '>') break; /* new key/value pair in definition */ if (expected_indent == -1) expected_indent = indent; else assert(expected_indent == indent); int start = indent+1; /* skip '>' */ luaL_addstring(&b, &line[start]); /* guaranteed to at least be null */ } luaL_pushresult(&b); /* final state of line goes out into the world */ } /* leave a single table on stack containing the next top-level definition from the file */ void teliva_load_definition(lua_State* L, FILE* in) { lua_newtable(L); int def_idx = lua_gettop(L); char line[1024] = {'\0'}; do { if (feof(in) || fgets(line, 1024, in) == NULL) { lua_pushnil(L); return; } } while (line[0] == '#'); /* comment at start of file */ assert(line[strlen(line)-1] == '\n'); do { assert(line[0] == '-' || line[0] == ' '); assert(line[1] == ' '); /* key/value pair always indented at 0, never empty, unambiguously not a multiline string */ char key[512] = {'\0'}; char value[1024] = {'\0'}; assert(line[2] != ' '); assert(line[2] != '>'); assert(line[2] != '\n'); assert(line[2] != '\0'); memset(key, 0, 512); memset(value, 0, 1024); sscanf(line+2, "%s%s", key, value); assert(key[strlen(key)-1] == ':'); key[strlen(key)-1] = '\0'; lua_pushstring(L, key); if (value[0] != '\0') { lua_pushstring(L, value); /* value string on same line */ char c = fgetc(in); ungetc(c, in); if (c == '-') { strcpy(line, "-\n"); } else { memset(line, '\0', 1024); fgets(line, 1024, in); } } else { teliva_load_multiline_string(L, in, line, 1024); /* load from later lines */ } lua_settable(L, def_idx); } while (line[0] == ' '); } void load_tlv(lua_State* L, char* filename) { lua_newtable(L); int history_array = lua_gettop(L); FILE* in = fopen(filename, "r"); if (in == NULL) { endwin(); fprintf(stderr, "no such file\n"); exit(1); } for (int i = 1; !feof(in); ++i) { teliva_load_definition(L, in); if (lua_isnil(L, -1)) break; lua_rawseti(L, history_array, i); } fclose(in); lua_setglobal(L, "teliva_program"); } void emit_multiline_string(FILE* out, const char* value) { fprintf(out, " >"); for (const char* curr = value; *curr != '\0'; ++curr) { if (*curr == '\n' && *(curr+1) != '\0') fprintf(out, "\n >"); else fprintf(out, "%c", *curr); } } void save_tlv(lua_State* L, char* filename) { lua_getglobal(L, "teliva_program"); int history_array = lua_gettop(L); int history_array_size = luaL_getn(L, history_array); char outfilename[] = "teliva_out_XXXXXX"; int outfd = mkstemp(outfilename); if (outfd == -1) { endwin(); perror("save_tlv: error in creating temporary file"); abort(); } FILE *out = fdopen(outfd, "w"); fprintf(out, "# .tlv file generated by https://github.com/akkartik/teliva\n"); fprintf(out, "# You may edit it if you are careful; however, you may see cryptic errors if you\n"); fprintf(out, "# violate Teliva's assumptions.\n"); fprintf(out, "#\n"); fprintf(out, "# .tlv files are representations of Teliva programs. Teliva programs consist of\n"); fprintf(out, "# sequences of definitions. Each definition is a table of key/value pairs. Keys\n"); fprintf(out, "# and values are both strings.\n"); fprintf(out, "#\n"); fprintf(out, "# Lines in .tlv files always follow exactly one of the following forms:\n"); fprintf(out, "# - comment lines at the top of the file starting with '#' at column 0\n"); fprintf(out, "# - beginnings of definitions starting with '- ' at column 0, followed by a\n"); fprintf(out, "# key/value pair\n"); fprintf(out, "# - key/value pairs consisting of ' ' at column 0, containing either a\n"); fprintf(out, "# spaceless value on the same line, or a multi-line value\n"); fprintf(out, "# - multiline values indented by more than 2 spaces, starting with a '>'\n"); fprintf(out, "#\n"); fprintf(out, "# If these constraints are violated, Teliva may unceremoniously crash. Please\n"); fprintf(out, "# report bugs at http://akkartik.name/contact\n"); for (int i = 1; i <= history_array_size; ++i) { lua_rawgeti(L, history_array, i); int table = lua_gettop(L); int first = 1; for (lua_pushnil(L); lua_next(L, table) != 0; lua_pop(L, 1)) { if (first) fprintf(out, "- "); else fprintf(out, " "); first = 0; const char* key = lua_tostring(L, -2); if (strcmp(key, "__teliva_undo") == 0) { fprintf(out, "%s: %ld\n", key, lua_tointeger(L, -1)); continue; } const char* value = lua_tostring(L, -1); if (strchr(value, ' ') || strchr(value, '\n')) { fprintf(out, "%s:\n", key); emit_multiline_string(out, value); } else { fprintf(out, "%s: %s\n", key, value); } } lua_pop(L, 1); } fclose(out); rename(outfilename, filename); lua_pop(L, 1); } static const char* special_history_keys[] = { "__teliva_timestamp", "__teliva_undo", "__teliva_note", NULL, }; int is_special_history_key(const char* key) { for (const char** curr = special_history_keys; *curr != NULL; ++curr) { if (strcmp(*curr, key) == 0) return 1; } return 0; }