/* -*- Mode: c++; -*- */
/*  --------------------------------------------------------------------
 *  Filename:
 *    storage.cc
 *  
 *  Description:
 *    Implementation of the Binc::Storage format.
 *
 *  Authors:
 *    Andreas Aardal Hanssen <andreas-binc@bincimap.org>
 *
 *  Bugs:
 *
 *  ChangeLog:
 *
 *  --------------------------------------------------------------------
 *  Copyright 2002-2005 Andreas Aardal Hanssen
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Street #330, Boston, MA 02111-1307, USA.
 *  --------------------------------------------------------------------
 */
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include "storage.h"
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <map>
#include <string>
#include "../src/convert.h"

using namespace ::std;
using namespace ::Binc;

class Lexer {
public:
  Lexer(FILE *fp);

  unsigned int getCurrentColumn() const { return col; }
  unsigned int getCurrentLine() const { return line; }
  const string &getLastError() const { return lastError; }

  enum Type {
    LeftCurly, RightCurly, Equals, Comma, QMark, Colon, Semicolon,
    Text, EndOfFile, Error
  };

  bool nextToken(string *token, Type *type);

protected:
  enum State {
    Searching, RestOfLineComment, CStyleComment,
    QuotedString, HexString, TextString
  };

private:
  FILE *fp;
  State state;
  unsigned int line;
  unsigned int col;
  string lastError;
};

//------------------------------------------------------------------------
Lexer::Lexer(FILE *f) : fp(f), state(Searching), line(0), col(0)
{
}

//------------------------------------------------------------------------
bool Lexer::nextToken(string *token, Type *type)
{
  int lastc = '\0';

  string buffer;
  bool escaped = false;

  for (;;) {
    int c = fgetc(fp);
    if (c == EOF) {
      if (ferror(fp)) {
	lastError = strerror(errno);
	*type = Error;
	return false;
      }

      if (state != Searching) {
	lastError = "unexpected end of file";
	*type = Error;
	return false;
      }

      *type = EndOfFile;
      return true;
    }

    if (c == '\n') {
      ++line;
      col = 0;
    } else
      ++col;

    switch (state) {
    case Searching:
      // If whitespace, keep searching
      if (c == ' ' || c == '\t' || c == '\r' || c == '\n'
	  || c == '\f' || c == '\v');
      else if (col == 0 && (c == '#' || c == ';'))
	state = RestOfLineComment;
      else if (lastc == '/' && c == '/')
	state = RestOfLineComment;
      else if (lastc == '/' && c == '*')
	state = CStyleComment;
      else if (lastc == '*' && c == '/' && state == CStyleComment) {
	state = Searching;
	c = '\0'; // avoid ambiguity
      } else if (c == '?') {
	*token = "?";
	*type = QMark;
	return true;
      } else if (c == ':') {
	*token = ":";
	*type = Colon;
	return true;
      } else if (c == ';') {
	*token = ";";
	*type = Semicolon;
	return true;
      } else if (c == '{') {
	*token = "{";
	*type = LeftCurly;
	return true;
      } else if (c == '}') {
	*token = "}";
	*type = RightCurly;
	return true;
      } else if (c == ',') {
	*token = ",";
	*type = Comma;
	return true;
      } else if (c == '=') {
	*token = "=";
	*type = Equals;
	return true;
      } else if (c == '\"') {
	state = QuotedString;
      } else if (c == '<') {
	state = HexString;
      } else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
		 || (c >= '0' && c <= '9') || c == '.' || c == '_'
		 || c == '-' || c == '^') {
	state = TextString;
	buffer = "";
	buffer += (char) c;
      } else if (c != '/') {
	// Syntax error
	lastError = "unexpected character '";
	lastError += (char) c;
	lastError += "'";
	return false;
      }

      break;
    case RestOfLineComment:
      if (c == '\n')
	state = Searching;
      break;
    case CStyleComment:
      if (c == '/' && lastc == '*')
	state = Searching;
      break;
    case QuotedString:
      if (escaped) {
	buffer += (char) c;
	escaped = false;
      } else if (c == '\\') {
	escaped = true;
      } else if (c == '\"') {
	*token = buffer;
	*type = Text;
	buffer = "";
	state = Searching;
	return true;
      } else {
	buffer += (char) c;
      }

      break;
    case TextString:
      if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
	  || (c >= '0' && c <= '9') || c == '_' 
	  || c == '.' || c == '-' || c == '^')
	buffer += (char) c;
      else {
	*token = buffer;
	buffer = "";
	*type = Text;
	ungetc(c, fp);
	state = Searching;
	return true;
      }
      break;
    case HexString:
      if ((c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
	  || (c >= '0' && c <= '9'))
	buffer += (char) c;
      else if (c == '>') {
	*token = fromHex(buffer);
	buffer = "";
	*type = Text;
	state = Searching;
	return true;
      } else {
	lastError = "expected a-f, A-F, 0-9 or '>'";
	return false;
      }
      break;
    };

    lastc = c;
  }
}

//------------------------------------------------------------------------
Storage::Storage(const string &f, Mode m) 
  : fp(0), fd(0), fileName(f), state(Searching), mode(m),
    lastError("unknown error"), atEndOfFile(false), firstSection(true)
{
  if (m == ReadOnly) {
    if ((fp = fopen(f.c_str(), m == ReadOnly ? "r" : "w")) == 0) {
      lastError = "when opening \"" + f + "\": ";
      lastError += strerror(errno);
    }
  } else {
    // create a safe file name
    string tpl = fileName + "XXXXXX";
    char *ftemplate = new char[tpl.length() + 1];
    if (ftemplate == 0) {
      lastError = "out of memory, couldn't create safe filename";
      return;
    }

    if (ftemplate) {
      strcpy(ftemplate, tpl.c_str());
      while ((fd = mkstemp(ftemplate)) == 0 && errno == EEXIST) {
      }

      if (fd == 0) {
	lastError = "when opening \"" + f + "\": ";
	lastError += strerror(errno);
      }
    }

    fileName = ftemplate;
  }
}

//------------------------------------------------------------------------
Storage::~Storage(void)
{
  if (fp)
    fclose(fp);
  if (fd)
    close(fd);
}

//------------------------------------------------------------------------
bool Storage::ok(void) const
{
  return fp != 0;
}

//------------------------------------------------------------------------
bool Storage::get(string *section, string *key, string *value)
{
  if (mode == WriteOnly) {
    lastError = "unable to read from \"" + fileName + "\" when"
      " in WriteOnly mode";
    return false;
  }

  if (!fp)
    return false;

  string keytmp;
  string valuetmp;
  
  Lexer lex(fp);
  Lexer::Type type;
  string token;
  while (lex.nextToken(&token, &type) && type != Lexer::Error) {
    if (type == Lexer::EndOfFile) {
      if (state != Searching) {
	lastError = "unexpected end of file";
	return false;
      }

      atEndOfFile = true;
      return false;
    }

    // convert from alias to token
    if (type == Lexer::Text && aliases.find(token) != aliases.end())
	token = aliases[token];

    switch (state) {
    case Searching:
      if (type == Lexer::QMark) {
	state = AliasKey;
      } else if (type != Lexer::Text) {
	lastError = "expected text";
	lastError += " at line " + toString(lex.getCurrentLine());
	lastError += ", col " + toString(lex.getCurrentColumn());
  	return false;
      } else {
	curSection = token;
	state = Section;
      }
      break;
    case AliasKey: 
      if (type != Lexer::Text) {
	lastError = "expected text";
	lastError += " at line " + toString(lex.getCurrentLine());
	lastError += ", col " + toString(lex.getCurrentColumn());
  	return false;
      }
      
      keytmp = token;
      state = AliasColon;
      break;
    case AliasColon:
      if (type != Lexer::Colon) {
	lastError = "expected colon";
	lastError += " at line " + toString(lex.getCurrentLine());
	lastError += ", col " + toString(lex.getCurrentColumn());
  	return false;
      }
      state = AliasValue;
      break;
    case AliasValue:
      if (type != Lexer::Text) {
	lastError = "expected text";
	lastError += " at line " + toString(lex.getCurrentLine());
	lastError += ", col " + toString(lex.getCurrentColumn());
  	return false;
      }
      
      valuetmp = token;
      state = AliasEnd;
      break;
    case AliasEnd:
      if (type != Lexer::Semicolon) {
	lastError = "expected text";
	lastError += " at line " + toString(lex.getCurrentLine());
	lastError += ", col " + toString(lex.getCurrentColumn());
  	return false;
      }

      aliases[keytmp] = valuetmp;
      state = Searching;
      break;
    case Section:
      if (type != Lexer::LeftCurly) {
	lastError = "expected '{'";
	lastError += " at line " + toString(lex.getCurrentLine());
	lastError += ", col " + toString(lex.getCurrentColumn());
	return false;
      }

      state = Key;
      keytmp = "";
      break;
    case Key:
      if (type == Lexer::Text) {
	if (keytmp != "")
	  keytmp += " ";
	keytmp += token;
      } else if (type == Lexer::Equals) {
	state = Value;
      } else {
	if (type == Lexer::RightCurly) {
	  if (keytmp != "") {
	    lastError = "unexpected '}'";
	    lastError += " at line " + toString(lex.getCurrentLine());
	    lastError += ", col " + toString(lex.getCurrentColumn());
	    return false;
	  } else
	    state = Searching;
	} else {
	  lastError = "expected text or '='";
	  lastError += " at line " + toString(lex.getCurrentLine());
	  lastError += ", col " + toString(lex.getCurrentColumn());
	  return false;
	}
      }
      break;
    case Value:
      if (type == Lexer::Text) {
	if (valuetmp != "")
	  valuetmp += " ";
	valuetmp += token;
      } else if (type == Lexer::Comma || type == Lexer::RightCurly) {
	*section = curSection;
	*key = keytmp;
	*value = valuetmp;
	keytmp = "";
	valuetmp = "";

	if (type == Lexer::RightCurly)
	  state = Searching;
	else
	  state = Key;

	return true;
      }
    };
  }

  lastError = lex.getLastError();
  lastError += " at line " + toString(lex.getCurrentLine() + 1);
  lastError += ", col " + toString(lex.getCurrentColumn() + 1);
  return false;
}

namespace {
  //----------------------------------------------------------------------
  inline bool islabelchar(char c)
  {
    switch (c) {
    case 0x2D: case 0x2E:
    case 0x30: case 0x31: case 0x32: case 0x33: case 0x34:
    case 0x35: case 0x36: case 0x37: case 0x38: case 0x39: 
    case 0x40: case 0x41: case 0x42: case 0x43: case 0x44:
    case 0x45: case 0x46: case 0x47: case 0x48: case 0x49:
    case 0x4A: case 0x4B: case 0x4C: case 0x4D: case 0x4E:
    case 0x4F: case 0x50: case 0x51: case 0x52: case 0x53:
    case 0x54: case 0x55: case 0x56: case 0x57: case 0x58:
    case 0x59: case 0x5A:
      //    case 0x5B:
      //    case 0x5C:
      //    case 0x5D:
    case 0x5E: case 0x5F:
      //    case 0x60:
    case 0x61: case 0x62: case 0x63: case 0x64: case 0x65:
    case 0x66: case 0x67: case 0x68: case 0x69: case 0x6A:
    case 0x6B: case 0x6C: case 0x6D: case 0x6E: case 0x6F:
    case 0x70: case 0x71: case 0x72: case 0x73: case 0x74:
    case 0x75: case 0x76: case 0x77: case 0x78: case 0x79:
    case 0x7A:
      return true;
    default: return false;
    }
  }

  //----------------------------------------------------------------------
  inline const string &encode(const string &s)
  {
    static string output;
    output = "";

    for (string::const_iterator i = s.begin(); i != s.end(); ++i)
      if ((unsigned char)*i < 32 || (unsigned char)*i > 127) {
	output += "<";
	output += toHex(s);
	output += ">";
	return output;
      }

    for (string::const_iterator i = s.begin(); i != s.end(); ++i)
      if (!islabelchar(*i)) {
	output = "\"";
	char c;
	for (string::const_iterator i = s.begin(); i != s.end(); ++i)
	  switch ((c = *i)) {
	  case '\"': output += "\\\""; break;
	  case '\\': output += "\\\\"; break;
	  default: output += c; break;
	  }
	output += "\"";
	return output;
      }

    return output = s;
  }

  //----------------------------------------------------------------------
  inline bool writeData(int fd, const string &out)
  {
    static string buffer;

    buffer += out;
    if (buffer.size() >= 4096 || out == "") {
      int written = 0;
      while (1) {
	int retval = write(fd, buffer.c_str() + written,
			   buffer.length() - written);
	if (retval == (int) buffer.length())
	  break;
	
	if (retval == -1) {
	  if (errno == EINTR) continue;
	  return false;
	}
	
	written += retval;
      }

      buffer = "";
    }

    return true;
  }
}

//------------------------------------------------------------------------
bool Storage::put(const string &section, const string &key,
		  const string &value)
{
  if (!fd)
    return false;

  if (mode == ReadOnly) {
    lastError = "unable to write to \"" + fileName
      + "\" when in ReadOnly mode";
    return false;
  }

  string data;
  if (curSection != section) {
    if (firstSection)
      firstSection = false;
    else
      data = "\n}\n";

    data += section + " {";
    curSection = section;
  } else
    data = ",";

  data += "\n\t";
  data += encode(key);
  data += " = ";
  data += encode(value);

  if (!writeData(fd, data)) {
    lastError = "when writing to \"" + fileName + "\": ";
    lastError += strerror(errno);
    return false;
  }  
  
  return true;
}

//------------------------------------------------------------------------
bool Storage::commit(void)
{
  if (!writeData(fd, "\n}\n")) {
    lastError = "when writing to \"" + fileName + "\":";
    lastError += strerror(errno);
    unlink(fileName.c_str());
  }

  if (!writeData(fd, "")) {
    lastError = "when writing to \"" + fileName + "\":";
    lastError += strerror(errno);
    unlink(fileName.c_str());
  }

  if (fsync(fd) != 0 && errno != EINVAL) {
    lastError = "when syncing \"" + fileName + "\":";
    lastError += strerror(errno);
    return false;
  }

  if (rename(fileName.c_str(), 
	     fileName.substr(0, fileName.length() - 6).c_str()) != 0) {
    lastError = "when renaming \"" + fileName + "\" to \""
      + fileName.substr(0, fileName.length() - 6) + "\": ";
    lastError += strerror(errno);
    unlink(fileName.c_str());
    return false;
  }

  if (close(fd) != 0) {
    lastError = "when committing \"" + fileName + "\":";
    lastError += strerror(errno);
    return false;
  }

  fd = 0;

  string::size_type pos = fileName.rfind('/');
  string dirName = pos ? fileName.substr(0, pos) : ".";
  int dfd = open(dirName.c_str(), O_RDONLY);
  if (dfd == -1 || fsync(dfd) != 0 && errno != EINVAL || close(dfd) != 0) {
    lastError = "when syncing \"" + dirName + "\":";
    lastError += strerror(errno);
    return false;
  }

  return true;
}


syntax highlighted by Code2HTML, v. 0.9.1