about summary refs log blame commit diff stats
path: root/src/config/toml.nim
blob: c34e46a76142e9c8efbd43d1bd4bfb7b9bf04d68 (plain) (tree)
1
2
3
4
5
6
7
8






                
                


                   
                   







                                    



                                    
                    






                        
                


































                                                    

                                                                 

                







                                                                 












                                                           
                                                                             

                                                 
                                                                            










































                                                                       
                              



                        
                              


















                                               

                                 















































                                                                





                                            

                                                            


































































                                                                              





                                                            
           

                             















                                                                   
                          
                                      









                                                                      



                                                        






                                                                                             










                                                                   

                                                               



















































                                                       





                                                 
               
         

                                          


                                








































                                                                  











                                                                   
                                   


                                 

                                       








                                                               
                                                                                       



                            

                           
                      
















                                                                        
import streams
import tables
import times
import strutils
import strformat
import unicode

import utils/opt
import utils/twtstr

type
  ValueType* = enum
    VALUE_STRING = "string"
    VALUE_INTEGER = "integer"
    VALUE_FLOAT = "float"
    VALUE_BOOLEAN = "boolean"
    VALUE_DATE_TIME = "datetime"
    VALUE_TABLE = "table"
    VALUE_ARRAY = "array"
    VALUE_TABLE_ARRAY = "tablearray"

  SyntaxError = object of ValueError

  TomlParser = object
    filename: string
    at: int
    line: int
    stream: Stream
    buf: string
    root: TomlTable
    node: TomlNode
    currkey: seq[string]
    tarray: bool

  TomlValue* = ref object
    case vt*: ValueType
    of VALUE_STRING:
      s*: string
    of VALUE_INTEGER:
      i*: int64
    of VALUE_FLOAT:
      f*: float64
    of VALUE_BOOLEAN:
      b*: bool
    of VALUE_TABLE:
      t*: TomlTable
    of VALUE_DATE_TIME:
      dt*: DateTime
    of VALUE_ARRAY:
      a*: seq[TomlValue]
    of VALUE_TABLE_ARRAY:
      ta*: seq[TomlTable]

  TomlNode = ref object of RootObj
    comment: string

  TomlKVPair = ref object of TomlNode
    key*: seq[string]
    value*: TomlValue

  TomlTable = ref object of TomlNode
    key: seq[string]
    nodes: seq[TomlNode]
    map: Table[string, TomlValue]

func `[]`*(val: TomlValue, key: string): TomlValue =
  return val.t.map[key]

iterator pairs*(val: TomlTable): (string, TomlValue) {.inline.} =
  for k, v in val.map.pairs:
    yield (k, v)

iterator pairs*(val: TomlValue): (string, TomlValue) {.inline.} =
  for k, v in val.t:
    yield (k, v)

iterator items*(val: TomlValue): TomlTable {.inline.} =
  for v in val.ta:
    yield v

func contains*(val: TomlValue, key: string): bool =
  return key in val.t.map

func isBare(c: char): bool =
  return c == '-' or c == '_' or c.isAlphaNumeric()

func peek(state: TomlParser, i: int): char =
  return state.buf[state.at + i]

func peek(state: TomlParser, i: int, len: int): string =
  return state.buf.substr(state.at + i, state.at + i + len)

proc syntaxError(state: TomlParser, msg: string) =
  raise newException(SyntaxError, fmt"{state.filename}({state.line}): {msg}")

proc valueError(state: TomlParser, msg: string) =
  raise newException(ValueError, fmt"{state.filename}({state.line}): {msg}")

proc consume(state: var TomlParser): char =
  result = state.buf[state.at]
  inc state.at

proc reconsume(state: var TomlParser) =
  dec state.at

proc has(state: var TomlParser, i: int = 0): bool =
  if state.at + i >= state.buf.len and not state.stream.atEnd():
    state.buf &= state.stream.readLine() & '\n'
  return state.at + i < state.buf.len

proc consumeEscape(state: var TomlParser, c: char): Rune =
  var len = 4
  if c == 'U':
    len = 8
  let c = state.consume()
  var num = hexValue(c)
  if num != -1:
    var i = 0
    while state.has() and i < len:
      let c = state.peek(0)
      if hexValue(c) == -1:
        break
      discard state.consume()
      num *= 0x10
      num += hexValue(c)
      inc i
    if i != len - 1:
      state.syntaxError(fmt"invalid escaped length ({i}, needs {len})")
    if num > 0x10FFFF or num in {0xD800..0xDFFF}:
      state.syntaxError(fmt"invalid escaped codepoint {num}")
    else:
      return Rune(num)
  else:
    state.syntaxError(fmt"invalid escaped codepoint {c}")

proc consumeString(state: var TomlParser, first: char): string =
  var multiline = false

  if first == '"':
    if state.has(1):
      let s = state.peek(0, 1)
      if s == "\"\"":
        multiline = true
  elif first == '\'':
    if state.has(1):
      let s = state.peek(0, 1)
      if s == "''":
        multiline = true

  if multiline:
    let c = state.peek(0)
    if c == '\n':
      discard state.consume()

  var escape = false
  var ml_trim = false
  while state.has():
    let c = state.consume()
    if c == '\n' and not multiline:
      state.syntaxError(fmt"newline in string")
    elif c == first:
      if multiline and state.has(1):
        let c2 = state.peek(0)
        let c3 = state.peek(1)
        if c2 == first and c3 == first:
          discard state.consume()
          discard state.consume()
          break
      else:
        break
    elif first == '"' and c == '\\':
      escape = true
    elif escape:
      case c
      of 'b': result &= '\b'
      of 't': result &= '\t'
      of 'n': result &= '\n'
      of 'f': result &= '\f'
      of 'r': result &= '\r'
      of '"': result &= '"'
      of '\\': result &= '\\'
      of 'u', 'U': result &= state.consumeEscape(c)
      of '\n': ml_trim = true
      else: state.syntaxError(fmt"invalid escape sequence \{c}")
      escape = false
    elif ml_trim:
      if not (c in {'\n', ' ', '\t'}):
        result &= c
        ml_trim = false
    else:
      result &= c

proc consumeBare(state: var TomlParser, c: char): string =
  result &= c
  while state.has():
    let c = state.consume()
    case c
    of ' ', '\t': break
    of '.', '=', ']', '\n':
      state.reconsume()
      break
    elif c.isBare():
      result &= c
    else:
      state.syntaxError(fmt"invalid value in token: {c}")

proc flushLine(state: var TomlParser) =
  if state.node != nil:
    if state.node of TomlKVPair:
      var i = 0
      let keys = state.currkey & TomlKVPair(state.node).key
      var table = state.root
      while i < keys.len - 1:
        if keys[i] in table.map:
          let node = table.map[keys[i]]
          if node.vt == VALUE_TABLE:
            table = node.t
          elif node.vt == VALUE_TABLE_ARRAY:
            assert state.tarray
            table = node.ta[^1]
          else:
            let s = keys.join(".")
            state.valueError(fmt"re-definition of node {s}")
        else:
          let node = TomlTable()
          table.map[keys[i]] = TomlValue(vt: VALUE_TABLE, t: node)
          table = node
        inc i

      if keys[i] in table.map:
        let s = keys.join(".")
        state.valueError(fmt"re-definition of node {s}")

      table.map[keys[i]] = TomlKVPair(state.node).value
      table.nodes.add(state.node)
    state.node = nil
  inc state.line

proc consumeComment(state: var TomlParser) =
  state.node = TomlNode()
  while state.has():
    let c = state.consume()
    if c == '\n':
      state.reconsume()
      break
    else:
      state.node.comment &= c

proc consumeKey(state: var TomlParser): seq[string] =
  var str = ""
  while state.has():
    let c = state.consume()
    case c
    of '"', '\'':
      if str.len > 0:
        state.syntaxError("multiple strings without dot")
      str = state.consumeString(c)
    of '=', ']':
      if str.len != 0:
        result.add(str)
        str = ""
      return result
    of '.':
      if str.len == 0: #TODO empty strings are allowed, only empty keys aren't
        state.syntaxError("redundant dot")
      else:
        result.add(str)
        str = ""
    of ' ', '\t': discard
    of '\n':
      if state.node != nil:
        state.syntaxError("newline without value")
      else:
        state.flushLine()
    elif c.isBare():
      if str.len > 0:
        state.syntaxError(fmt"multiple strings without dot: {str}")
      str = state.consumeBare(c)
    else: state.syntaxError(fmt"invalid character in key: {c}")

  state.syntaxError("key without value")

proc consumeTable(state: var TomlParser): TomlTable =
  new(result)
  while state.has():
    let c = state.peek(0)
    case c
    of ' ', '\t': discard
    of '\n':
      return result
    of ']':
      if state.tarray:
        discard state.consume()
        return result
      else:
        state.syntaxError("redundant ] character after key")
    of '[':
      state.tarray = true
      discard state.consume()
    of '"', '\'':
      result.key = state.consumeKey()
    elif c.isBare():
      result.key = state.consumeKey()
    else: state.syntaxError(fmt"invalid character before key: {c}")
  state.syntaxError("unexpected end of file")

proc consumeNoState(state: var TomlParser): bool =
  while state.has():
    let c = state.peek(0)
    case c
    of '#', '\n':
      return false
    of ' ', '\t': discard
    of '[':
      discard state.consume()
      state.tarray = false
      let table = state.consumeTable()
      if state.tarray:
        var node = state.root
        for i in 0 ..< table.key.high:
          if table.key[i] in node.map:
            node = node.map[table.key[i]].t
          else:
            let t2 = TomlTable()
            node.map[table.key[i]] = TomlValue(vt: VALUE_TABLE, t: t2)
            node = t2
        if table.key[^1] in node.map:
          var last = node.map[table.key[^1]]
          if last.vt == VALUE_ARRAY and last.a.len == 0:
            last = TomlValue(vt: VALUE_TABLE_ARRAY)
            node.map[table.key[^1]] = last
          if last.vt != VALUE_TABLE_ARRAY:
            let key = table.key.join('.')
            state.valueError(fmt"re-definition of node {key} as table array (was {last.vt})")
          last.ta.add(table)
        else:
          let last = TomlValue(vt: VALUE_TABLE_ARRAY, ta: @[table])
          node.map[table.key[^1]] = last
      state.currkey = table.key
      state.node = table
      return false
    elif c == '"' or c == '\'' or c.isBare():
      let kvpair = TomlKVPair()
      kvpair.key = state.consumeKey()
      state.node = kvpair
      return true
    else: state.syntaxError(fmt"invalid character before key: {c}")
  state.syntaxError("unexpected end of file")

proc consumeNumber(state: var TomlParser, c: char): TomlValue =
  var repr = $c
  var isfloat = false
  if state.has():
    if state.peek(0) == '+' or state.peek(0) == '-':
      repr &= state.consume()

  while state.has() and isDigit(state.peek(0)):
    repr &= state.consume()

  if state.has(1):
    if state.peek(0) == '.' and isDigit(state.peek(1)):
      repr &= state.consume()
      repr &= state.consume()
      isfloat = true
      while state.has() and isDigit(state.peek(0)):
        repr &= state.consume()

  if state.has(1):
    if state.peek(0) == 'E' or state.peek(0) == 'e':
      var j = 2
      if state.peek(1) == '-' or state.peek(1) == '+':
        inc j
      if state.has(j) and isDigit(state.peek(j)):
        while j > 0:
          repr &= state.consume()
          dec j

        while state.has() and isDigit(state.peek(0)):
          repr &= state.consume()

  if isfloat:
    let val = parseFloat64(repr)
    return TomlValue(vt: VALUE_FLOAT, f: val)

  let val = parseInt64(repr)
  return TomlValue(vt: VALUE_INTEGER, i: val)

proc consumeValue(state: var TomlParser): TomlValue

proc consumeArray(state: var TomlParser): TomlValue =
  result = TomlValue(vt: VALUE_ARRAY)
  var val: TomlValue
  while state.has():
    let c = state.consume()
    case c
    of ' ', '\t', '\n': discard
    of ']':
      if val != nil:
        result.a.add(val)
      break
    of ',':
      if val == nil:
        state.syntaxError("comma without element")
      if val.vt == VALUE_TABLE:
        # inline table array
        result = TomlValue(vt: VALUE_TABLE_ARRAY)
        result.ta.add(val.t)
      else:
        result.a.add(val)
      val = nil
    else:
      if val != nil:
        state.syntaxError("missing comma")
      state.reconsume()
      val = state.consumeValue()

proc consumeInlineTable(state: var TomlParser): TomlValue =
  result = TomlValue(vt: VALUE_TABLE, t: TomlTable())
  var key: seq[string]
  var haskey: bool
  var val: TomlValue
  while state.has():
    let c = state.consume()
    case c
    of ' ', '\t', '\n': discard
    of '}':
      if val != nil:
        result.a.add(val)
      break
    of ',':
      if key.len == 0:
        state.syntaxError("missing key")
      if val == nil:
        state.syntaxError("comma without element")
      var table = result.t
      for i in 0 ..< key.high:
        let k = key[i]
        if k in table.map:
          state.syntaxError(fmt"invalid re-definition of key {k}")
        else:
          let node = TomlTable()
          table.map[k] = TomlValue(vt: VALUE_TABLE, t: node)
          table = node
      let k = key[^1]
      if k in table.map:
        state.syntaxError(fmt"invalid re-definition of key {k}")
      table.map[k] = val
    else:
      if val != nil:
        state.syntaxError("missing comma")
      if not haskey:
        key = state.consumeKey()
        haskey = true
      else:
        state.reconsume()
        val = state.consumeValue()

proc consumeValue(state: var TomlParser): TomlValue =
  while state.has():
    let c = state.consume()
    case c
    of '"', '\'':
      return TomlValue(vt: VALUE_STRING, s: state.consumeString(c))
    of ' ', '\t': discard
    of '\n':
      state.syntaxError("newline without value")
    of '#':
      state.syntaxError("comment without value")
    of '+', '-', '0'..'9':
      return state.consumeNumber(c)
      #TODO date-time
    of '[':
      return state.consumeArray()
    of '{':
      return state.consumeInlineTable()
    elif c.isBare():
      let s = state.consumeBare(c)
      case s
      of "true": return TomlValue(vt: VALUE_BOOLEAN, b: true)
      of "false": return TomlValue(vt: VALUE_BOOLEAN, b: false)
      else: state.syntaxError(fmt"invalid token {s}")
    else:
      state.syntaxError(fmt"invalid character in value: {c}")

proc parseToml*(inputStream: Stream, filename = "<input>"): Result[TomlValue, string] =
  var state: TomlParser
  state.stream = inputStream
  state.line = 1
  state.root = TomlTable()
  state.filename = filename
  try:
    while state.has():
      if state.consumeNoState():
        let kvpair = TomlKVPair(state.node)
        kvpair.value = state.consumeValue()
      while state.has():
        let c = state.consume()
        case c
        of '\n':
          state.flushLine()
          break
        of '#':
          state.consumeComment()
        of '\t', ' ': discard
        else: state.syntaxError(fmt"invalid character after value: {c}")
    inputStream.close()
    return ok(TomlValue(vt: VALUE_TABLE, t: state.root))
  except SyntaxError, ValueError:
    return err(getCurrentExceptionMsg())