Search code examples
javascriptparsingexpressionpegpegjs

Access Control String (ACS) Parser/Interpreter with PEG.js


Preface

I'm working on creating a Access Control String (or System) (ACS) string Parser/Interpreter with PEG.js. ACS strings are commonly used on Bulletin Board Systems (BBSs) to check access rights to particular areas of the board. For example, see Renegade's ACS documentation.

Example ACS Strings

Below are some simplified strings and their English translations for illustration:

// Has GM123 OR NOT GM456
GM123|!GM456

// Has GM123 OR NOT (GM456 AND GM789) (note: AND is implied in this grammar if not specified)
GM123|!(GM456GM789)

// Has GM123 AND NOT GM456 OR has GM789
GM123!GM456|GM789

// Has GM1 OR (NOT GM2 OR GM3)
GM1|(!GM2|GM3) 

What I'm Trying to Achieve

What I would like to do here is parse and interpret (or "run") the ACS string and ultimately end up with a final boolean.

Grammar So Far

Below is the PEG.js grammer I've some up with so far. Note that the ACS strings themselves are a bit more complex than the examples above (I allow for example GM['abc','def']) but I think up to this point it's fairly self explanatory.

{   
  function checkAccessSingle(acsName, arg) {
    return true;
  }

  function checkAccessMulti(acsName, args, anyMatch) {
    return true;
  }

  function makeNot(not, x) {
    return not ? !x : x;
  }
}

start
  = acsString


whitespaceChar
  = ' '

ws
  = whitespaceChar*

lineTerminatorChar
  = [\r\n\u2028\u2029]

decimalDigit
  = [0-9]

integer
  = decimalDigit+ { return parseInt(text(), 10); }

asciiPrintableChar
  = [ -~]

singleAsciiStringChar
  = !("'") asciiPrintableChar { return text(); }

doubleAsciiStringChar
  = !('"') asciiPrintableChar { return text(); }

nonEmptyStringLiteral
  = "'" chars:singleAsciiStringChar+ "'" { return chars.join(''); }
  / '"' chars:doubleAsciiStringChar+ '"' { return chars.join(''); }

AND
  = '&'

OR
  = '|'

NOT
  = '!'

acsName
  = n:([A-Z][A-Z]) { return n.join(''); }

acsArg
  = nonEmptyStringLiteral
  / integer

acsArgs
  = first:acsArg rest:(ws ',' ws a:acsArg { return a; })* {
      var args = [ first ];
      for(var i = 0; i < rest.length; ++i) {
        args.push(rest[i]);
      }
      return args;
    }

singleAcsCheck
  = not:NOT? n:acsName a:acsArg* {
    return function() {
      makeNot(not, checkAccessSingle(n, a));
      }
    }
  / not:NOT? n:acsName '[' a:acsArgs ']' {
    return function() {
      return makeNot(not, checkAccessMulti(n, a, false));
      }
    }
  / not:NOT? n:acsName '{' a:acsArgs '}' {
    return function() {
      return makeNot(not, checkAccessMulti(n, a, true));
      }
    }

multiAcsCheck
  = singleAcsCheck+

acsString = multiAcsCheck

Where I Need Help

The main issue I'm having (if not others I haven't run into yet!) is handling precedence with () and the OR clauses. This may be something simple, but I've worked on this for days and have some up short. Again, what I'm ultimately attempting to achieve here is to feed in an ACS string and output a final boolean result. The various ACS "commands" (e.g. 'GM' in the above example) should make method calls that do the dirty work.


Solution

  • Here's a quick demo that parses your example input properly and shows how you could go about evaluating the expressions on the fly (which will return a boolean):

    {
      function check(name, value) {
        // Dummy implementation: returns true when the name starts with 'A'
        return name.charAt(0) == 'A';
      }
    }
    
    start
     = expr
    
    expr
     = or_expr
    
    or_expr
     = left:and_expr '|' right:expr { return left || right; }
     / and_expr
    
    and_expr
     = left:not_expr '&'? right:expr { return left && right; }
     / not_expr
    
    not_expr
     = '!' value:atom { return !value; }
     / atom
    
    atom
     = acs_check
     / '(' value:expr ')' { return value; }
    
    acs_check
     = n:name a:arg { return check(n, a); }
    
    name
     = c:([A-Z][A-Z]) { return c.join(''); }
    
    arg
     = c:[A-Z]+ { return c.join(''); }
     / d:[0-9]+ { return d.join(''); }