about summary refs log blame commit diff stats
path: root/src/tlv.c
blob: 4e2255e3b56f56b118164143e0795b0c3bee390b (plain) (tree)
1
2
3
4
5
6
7
8
                   


                   
                    
      
                  
                   




                    
                
 
                                                                              




                                                                                 
 


                                                                                            

                       


                           
                       





                                                                           






                                                            
                                                                           

                               
        


                                                                           

                      
                                                   


                                                                                          
                                                     


                              







                                                           

                                             






                                                                                                





                                       
                           
                                                                





                            
                                 
                              
       



                                                                                   
                             
                           





                                             




                                      







                                     
 
                                                          








                                                         























                                                                    
                                             


                                                       







                                                         





















                                                                                                     










                                                      
                                                                  

                                                      


                                    
                                                



                  
                                

                
 






                                                                         
#include <assert.h>
#ifdef __NetBSD__
#include <curses.h>
#else
#include <ncurses.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>

#include "lua.h"
#include "lauxlib.h"
#include "tlv.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);
  }
}

static const char* special_history_keys[] = {
  "__teliva_timestamp",
  "__teliva_note",
  "__teliva_undo",
  NULL,
};

/* save key and its value at top of stack to out
 * no stack side effects */
static void save_tlv_key(lua_State* L, const char* key, FILE* out) {
  if (strcmp(key, "__teliva_undo") == 0) {
    fprintf(out, "%s: %ld\n", key, lua_tointeger(L, -1));
    return;
  }
  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);
  }
}

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;
    // standardize order of special keys
    for (int k = 0;  special_history_keys[k];  ++k) {
      lua_getfield(L, table, special_history_keys[k]);
      if (!lua_isnil(L, -1)) {
        if (first) fprintf(out, "- ");
        else fprintf(out, "  ");
        first = 0;
        save_tlv_key(L, special_history_keys[k], out);
      }
      lua_pop(L, 1);
    }
    for (lua_pushnil(L); lua_next(L, table) != 0; lua_pop(L, 1)) {
      if (is_special_history_key(lua_tostring(L, -2)))
        continue;
      if (first) fprintf(out, "- ");
      else fprintf(out, "  ");
      first = 0;
      save_tlv_key(L, lua_tostring(L, -2), out);
    }
    lua_pop(L, 1);
  }
  fclose(out);
  rename(outfilename, filename);
  lua_pop(L, 1);
}

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;
}