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










                    
                                                                     
 
























                                                                       
                                                


















































































































































                                                                             

                                              

















































                                                                             
             




                                                                              
                                   














































                                                                        
import std/options
import std/os
import std/posix

import js/error
import js/fromjs
import js/javascript
import js/tojs
import types/opt
import utils/twtstr

const libexecPath {.strdefine.} = "${%CHA_BIN_DIR}/../libexec/chawan"

type ChaPath* = distinct string

func `$`*(p: ChaPath): string =
  return string(p)

type
  UnquoteContext = object
    state: UnquoteState
    s: string
    p: string
    i: int
    identStr: string
    subChar: char
    hasColon: bool
    terminal: Option[char]

  UnquoteState = enum
    STATE_NORMAL, STATE_TILDE, STATE_DOLLAR, STATE_IDENT, STATE_BSLASH,
    STATE_CURLY_START, STATE_CURLY, STATE_CURLY_HASH, STATE_CURLY_PERC,
    STATE_CURLY_COLON, STATE_CURLY_EXPAND, STATE_DONE

  ChaPathError = string

  ChaPathResult[T] = Result[T, ChaPathError]

proc unquote*(p: ChaPath): ChaPathResult[string]
proc unquote(p: string, starti: var int, terminal: Option[char]):
    ChaPathResult[string]

proc stateNormal(ctx: var UnquoteContext, c: char) =
  case c
  of '$': ctx.state = STATE_DOLLAR
  of '\\': ctx.state = STATE_BSLASH
  of '~':
    if ctx.i == 0:
      ctx.state = STATE_TILDE
    else:
      ctx.s &= c
  elif ctx.terminal.isSome and ctx.terminal.get == c:
    ctx.state = STATE_DONE
  else:
    ctx.s &= c

proc flushTilde(ctx: var UnquoteContext) =
  if ctx.identStr == "":
    ctx.s &= getHomeDir()
  else:
    let p = getpwnam(cstring(ctx.identStr))
    if p != nil:
      ctx.s &= $p.pw_dir
    ctx.identStr = ""
  ctx.state = STATE_NORMAL

proc stateTilde(ctx: var UnquoteContext, c: char) =
  if c != '/':
    ctx.identStr &= c
  else:
    ctx.flushTilde()

# Kind of a hack. We special case `\$' (backslash-dollar) in TOML, so that
# it produces itself in dquote strings.
# Thus by applying stateBSlash we get '\$' -> "$", but also "\$" -> "$".
proc stateBSlash(ctx: var UnquoteContext, c: char) =
  if c != '$':
    ctx.s &= '\\'
  ctx.s &= c
  ctx.state = STATE_NORMAL

proc stateDollar(ctx: var UnquoteContext, c: char): ChaPathResult[void] =
  # $
  case c
  of '$':
    ctx.s &= $getCurrentProcessId()
    ctx.state = STATE_NORMAL
  of '0':
    # Note: we intentionally use getAppFileName so that any symbolic links
    # are resolved.
    ctx.s &= getAppFileName()
    ctx.state = STATE_NORMAL
  of '1'..'9':
    return err("Parameter substitution is not supported")
  of AsciiAlpha:
    ctx.identStr = $c
    ctx.state = STATE_IDENT
  of '{':
    ctx.state = STATE_CURLY_START
  else:
    # > If an unquoted '$' is followed by a character that is not one of
    # > the following: [...] the result is unspecified.
    # just error out here to be safe
    return err("Invalid dollar substitution")
  ok()

proc flushIdent(ctx: var UnquoteContext) =
  ctx.s &= getEnv(ctx.identStr)
  ctx.identStr = ""

const BareChars = AsciiAlphaNumeric + {'_'}

proc stateIdent(ctx: var UnquoteContext, c: char) =
  # $ident
  if c in BareChars:
    ctx.identStr &= c
  else:
    ctx.flushIdent()
    dec ctx.i
    ctx.state = STATE_NORMAL

proc stateCurlyStart(ctx: var UnquoteContext, c: char): ChaPathResult[void] =
  # ${
  case c
  of '#':
    ctx.state = STATE_CURLY_HASH
  of '%':
    ctx.state = STATE_CURLY_PERC
  of BareChars:
    ctx.state = STATE_CURLY
    dec ctx.i
  else:
    return err("unexpected character in substitution: '" & c & "'")
  return ok()

proc stateCurly(ctx: var UnquoteContext, c: char): ChaPathResult[void] =
  # ${ident
  case c
  of '}':
    ctx.s &= $getEnv(ctx.identStr)
    ctx.state = STATE_NORMAL
    return ok()
  of '$': # allow $ as first char only
    if ctx.identStr.len > 0:
      return err("unexpected dollar sign in substitution")
    ctx.identStr &= c
    return ok()
  of ':', '-', '?', '+': # note: we don't support `=' (assign)
    if ctx.identStr.len > 0:
      return err("substitution without parameter name")
    if c == ':':
      ctx.state = STATE_CURLY_COLON
    else:
      ctx.subChar = c
      ctx.state = STATE_CURLY_EXPAND
    return ok()
  of '1'..'9':
    return err("Parameter substitution is not supported")
  of BareChars - {'1'..'9'}:
    ctx.identStr &= c
    return ok()
  else:
    return err("unexpected character in substitution: '" & c & "'")

proc stateCurlyHash(ctx: var UnquoteContext, c: char): ChaPathResult[void] =
  # ${#ident
  if c == '}':
    let s = getEnv(ctx.identStr)
    ctx.s &= $s.len
    ctx.identStr = ""
    ctx.state = STATE_NORMAL
    return ok()
  if c == '$': # allow $ as first char only
    if ctx.identStr.len > 0:
      return err("unexpected dollar sign in substitution")
    # fall through
  elif c notin BareChars:
    return err("unexpected character in substitution: '" & c & "'")
  ctx.identStr &= c
  return ok()

proc stateCurlyPerc(ctx: var UnquoteContext, c: char): ChaPathResult[void] =
  # ${%ident
  if c == '}':
    if ctx.identStr == "CHA_BIN_DIR":
      ctx.s &= getAppFileName().beforeLast('/')
    elif ctx.identStr == "CHA_LIBEXEC_DIR":
      ctx.s &= ?ChaPath(libexecPath).unquote()
    else:
      return err("Unknown internal variable " & ctx.identStr)
    ctx.identStr = ""
    ctx.state = STATE_NORMAL
    return ok()
  if c notin BareChars:
    return err("unexpected character in substitution: '" & c & "'")
  ctx.identStr &= c
  return ok()

proc stateCurlyColon(ctx: var UnquoteContext, c: char): ChaPathResult[void] =
  # ${ident:
  if c notin {'-', '?', '+'}: # Note: we don't support `=' (assign)
    return err("unexpected character after colon: '" & c & "'")
  ctx.hasColon = true
  ctx.subChar = c
  ctx.state = STATE_CURLY_EXPAND
  return ok()

proc flushCurlyExpand(ctx: var UnquoteContext, word: string):
    ChaPathResult[void] =
  case ctx.subChar
  of '-':
    if ctx.hasColon:
      ctx.s &= getEnv(ctx.identStr, word)
    else:
      if existsEnv(ctx.identStr):
        ctx.s &= getEnv(ctx.identStr)
      else:
        ctx.s &= word
  of '?':
    if ctx.hasColon:
      let s = getEnv(ctx.identStr)
      if s.len == 0:
        return err(word)
      ctx.s &= s
    else:
      if not existsEnv(ctx.identStr):
        return err(word)
      ctx.s &= getEnv(ctx.identStr)
  of '+':
    if ctx.hasColon:
      if getEnv(ctx.identStr).len > 0:
        ctx.s &= word
    else:
      if existsEnv(ctx.identStr):
        ctx.s &= word
  else: assert false
  ctx.subChar = '\0'
  ctx.hasColon = false
  return ok()

proc stateCurlyExpand(ctx: var UnquoteContext, c: char): ChaPathResult[void] =
  # ${ident:-[word], ${ident:=[word], ${ident:?[word], ${ident:+[word]
  # word must be unquoted too.
  let word = ?unquote(ctx.p, ctx.i, some('}'))
  return ctx.flushCurlyExpand(word)

proc unquote(p: string, starti: var int, terminal: Option[char]):
    ChaPathResult[string] =
  var ctx = UnquoteContext(p: p, i: starti, terminal: terminal)
  while ctx.i < p.len:
    let c = p[ctx.i]
    case ctx.state
    of STATE_NORMAL: ctx.stateNormal(c)
    of STATE_TILDE: ctx.stateTilde(c)
    of STATE_BSLASH: ctx.stateBSlash(c)
    of STATE_DOLLAR: ?ctx.stateDollar(c)
    of STATE_IDENT: ctx.stateIdent(c)
    of STATE_CURLY_START: ?ctx.stateCurlyStart(c)
    of STATE_CURLY: ?ctx.stateCurly(c)
    of STATE_CURLY_HASH: ?ctx.stateCurlyHash(c)
    of STATE_CURLY_PERC: ?ctx.stateCurlyPerc(c)
    of STATE_CURLY_COLON: ?ctx.stateCurlyColon(c)
    of STATE_CURLY_EXPAND: ?ctx.stateCurlyExpand(c)
    of STATE_DONE: break
    inc ctx.i
  case ctx.state
  of STATE_NORMAL, STATE_DONE: discard
  of STATE_TILDE: ctx.flushTilde()
  of STATE_BSLASH: ctx.s &= '\\'
  of STATE_DOLLAR: ctx.s &= '$'
  of STATE_IDENT: ctx.flushIdent()
  of STATE_CURLY_START, STATE_CURLY, STATE_CURLY_HASH, STATE_CURLY_PERC,
      STATE_CURLY_COLON:
    return err("} expected")
  of STATE_CURLY_EXPAND:
    ?ctx.flushCurlyExpand("")
  starti = ctx.i
  return ok(ctx.s)

proc unquote(p: string): ChaPathResult[string] =
  var dummy = 0
  return unquote(p, dummy, none(char))

proc toJS*(ctx: JSContext, p: ChaPath): JSValue =
  toJS(ctx, $p)

proc fromJS2*(ctx: JSContext, val: JSValue, o: var JSResult[ChaPath]) =
  o = cast[JSResult[ChaPath]](fromJS[string](ctx, val))

proc unquote*(p: ChaPath): ChaPathResult[string] =
  let s = ?unquote(string(p))
  return ok(normalizedPath(s))