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



                  

                    
                 



                   
                                                                     
 
















                               

                                                                          




                                            
                                                
                                                                 

                         
                                                    
        

                               

                  
                         


                                                     
                      










                                           
                      
 
                                                   







                                                                          
                                                    


                 
                      
 
                                                                         



                                   
                        



                                                                          
                        



                                                         
                       
         
                            












                                                                        
                                                   





                     
                        
 
                                                                             


         
                           
         
                           
               
                       




                                                                   
                                                                        



                                  
                        






                                                              
                             

                                                       
                              

                     
                               








                                                                   
                                                                            




                                
                        









                                                                   
                                                                            



                                               

                                              


                                                             
                        





                                                                   
                                                                             




                                                                   
                           

             
                                                             





























                                         
                      
             
 
                                                                              


                                                                      
                                   
 
                                                                 




                                                               











                                              

                





                                                                   
                            
                   







                                                
                                                 

               

                                                                      



                                                  


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

import js/fromjs
import js/javascript
import js/jserror
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
    usNormal, usTilde, usDollar, usIdent, usBslash, usCurlyStart, usCurly,
    usCurlyHash, usCurlyPerc, usCurlyColon, usCurlyExpand, usDone

  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 = usDollar
  of '\\': ctx.state = usBslash
  of '~':
    if ctx.i == 0:
      ctx.state = usTilde
    else:
      ctx.s &= c
  elif ctx.terminal.isSome and ctx.terminal.get == c:
    ctx.state = usDone
  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 = usNormal

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 = usNormal

proc stateDollar(ctx: var UnquoteContext; c: char): ChaPathResult[void] =
  # $
  case c
  of '$':
    ctx.s &= $getCurrentProcessId()
    ctx.state = usNormal
  of '0':
    # Note: we intentionally use getAppFileName so that any symbolic links
    # are resolved.
    ctx.s &= getAppFileName()
    ctx.state = usNormal
  of '1'..'9':
    return err("Parameter substitution is not supported")
  of AsciiAlpha:
    ctx.identStr = $c
    ctx.state = usIdent
  of '{':
    ctx.state = usCurlyStart
  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 = usNormal

proc stateCurlyStart(ctx: var UnquoteContext; c: char): ChaPathResult[void] =
  # ${
  case c
  of '#':
    ctx.state = usCurlyHash
  of '%':
    ctx.state = usCurlyPerc
  of BareChars:
    ctx.state = usCurly
    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 = usNormal
    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 = usCurlyColon
    else:
      ctx.subChar = c
      ctx.state = usCurlyExpand
    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 = usNormal
    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 = usNormal
    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 = usCurlyExpand
  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
  ctx.state = usNormal
  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 usNormal: ctx.stateNormal(c)
    of usTilde: ctx.stateTilde(c)
    of usBslash: ctx.stateBSlash(c)
    of usDollar: ?ctx.stateDollar(c)
    of usIdent: ctx.stateIdent(c)
    of usCurlyStart: ?ctx.stateCurlyStart(c)
    of usCurly: ?ctx.stateCurly(c)
    of usCurlyHash: ?ctx.stateCurlyHash(c)
    of usCurlyPerc: ?ctx.stateCurlyPerc(c)
    of usCurlyColon: ?ctx.stateCurlyColon(c)
    of usCurlyExpand: ?ctx.stateCurlyExpand(c)
    of usDone: break
    inc ctx.i
  case ctx.state
  of usNormal, usDone: discard
  of usTilde: ctx.flushTilde()
  of usBslash: ctx.s &= '\\'
  of usDollar: ctx.s &= '$'
  of usIdent: ctx.flushIdent()
  of usCurlyStart, usCurly, usCurlyHash, usCurlyPerc, usCurlyColon:
    return err("} expected")
  of usCurlyExpand:
    ?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 fromJSChaPath*(ctx: JSContext; val: JSValue): JSResult[ChaPath] =
  return cast[JSResult[ChaPath]](fromJS[string](ctx, val))

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

proc unquoteGet*(p: ChaPath): string =
  return p.unquote().get