JavaScript IRC Parsers

Benchmark created by Fionn Kelleher on


Description

Benchmarking how long it takes for various IRC parsers to parse a line of IRC data.

Setup

var lines = [
        ":test!test@hello.this.is.my.test.net PRIVMSG #Node.js :Hello,   am looking for some help with express. Can you plz help me?????! !",
        ":prawn!salad@tasty.co LONGARGS #hello testing 1 2 3 test test test test hi abcdefghijklmnop :This was quite a good test!",
        "PING :return this, cretin!",
        "PING more :test coverage",
        ":service!nicks@services. 001 test :Welcome to the test IRC network test! Enjoy your stay."
    ];
    
    var mircColors = /\u0003\d?\d?,?\d?\d?/g;
    
    
    ircMessage = (function() {
      function ircMessage(line) {
        var nextspace, pair, position, rawTags, tag, _i, _len;
        this.tags = {};
        this.prefix = "";
        this.command = "";
        this.args = [];
        position = 0;
        nextspace = 0;
        if (line[0] === "@") {
          nextspace = line.indexOf(" ");
          if (nextspace === -1) {
            throw new Error("Expected prefix; malformed IRC message.");
          }
          rawTags = line.substring(1, nextspace).split(";");
          for (_i = 0, _len = rawTags.length; _i < _len; _i++) {
            tag = rawTags[_i];
            pair = tag.split("=");
            this.tags[pair[0]] = pair[1] || true;
          }
          position = nextspace + 1;
        }
        while (line.charAt(position) === " ") {
          position++;
        }
        if (line.charAt(position) === ":") {
          nextspace = line.indexOf(" ", position);
          if (nextspace === -1) {
            throw new Error("Expected command; malformed IRC message.");
          }
          this.prefix = line.substring(position + 1, nextspace);
          position = nextspace + 1;
          while (line.charAt(position) === " ") {
            position++;
          }
        }
        nextspace = line.indexOf(" ", position);
        if (nextspace === -1) {
          if (line.length > position) {
            this.command = line.substring(position).toUpperCase();
          } else {
            return;
          }
        }
        this.command = line.substring(position, nextspace).toUpperCase();
        position = nextspace + 1;
        while (line.charAt(position) === " ") {
          position++;
        }
        while (position < line.length) {
          nextspace = line.indexOf(" ", position);
          if (line.charAt(position) === ":") {
            this.args.push(line.substring(position + 1));
            break;
          }
          if (nextspace !== -1) {
            this.args.push(line.substring(position, nextspace));
            position = nextspace + 1;
            while (line.charAt(position) === " ") {
              position++;
            }
            continue;
          }
          if (nextspace === -1) {
            this.args.push(line.substring(position));
            break;
          }
        }
        return;
      }
    
      ircMessage.prototype.toString = function() {
        var arg, string, tag, value, _i, _len, _ref, _ref1;
        string = "";
        if (Object.keys(this.tags).length !== 0) {
          string += "@";
          _ref = this.tags;
          for (tag in _ref) {
            value = _ref[tag];
            if (value !== null) {
              string += "" + tag + "=" + value + ";";
            } else {
              string += "" + tag + ";";
            }
          }
          string = string.slice(0, -1) + " ";
        }
        if (this.prefix.length !== 0) {
          string += ":" + this.prefix + " ";
        }
        if (this.command.length !== 0) {
          string += "" + this.command + " ";
        }
        if (this.args.length !== 0) {
          _ref1 = this.args;
          for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
            arg = _ref1[_i];
            if (arg.indexOf(" " !== -1)) {
              string += "" + arg + " ";
            } else {
              string += ":" + arg + " ";
            }
          }
        }
        string = string.slice(0, -1);
        return string;
      };
    
      ircMessage.prototype.prefixIsUserHostmask = function() {
        return this.prefix.indexOf("@") !== -1 && this.prefix.indexOf("!") !== -1;
      };
    
      ircMessage.prototype.prefixIsServerHostname = function() {
        return this.prefix.indexOf("@") === -1 && this.prefix.indexOf("!") === -1 && this.prefix.indexOf(".") !== -1;
      };
    
      ircMessage.prototype.parseHostmask = function() {
        var hostname, nickname, parsed, username, _ref;
        _ref = this.prefix.split(/[!@]/), nickname = _ref[0], username = _ref[1], hostname = _ref[2];
        parsed = {
          nickname: nickname,
          username: username,
          hostname: hostname
        };
        return parsed;
      };
    
      return ircMessage;
    
    })();
    
    var parse_regex = /^(?:(?:(?:@([^ ]+) )?):(?:([a-z0-9\x5B-\x60\x7B-\x7D\.\-*]+)|([a-z0-9\x5B-\x60\x7B-\x7D\.\-*]+)!([^\x00\r\n\ ]+?)@?([a-z0-9\.\-:\/_]+)?) )?(\S+)(?: (?!:)(.+?))?(?: :(.+))?$/i;
    
    function kiwiParse(buffer_line) {
        var msg,
            i, j,
            tags = [],
            tag,
            line = '';
    
        // Decode server encoding
        line = buffer_line;
        if (!line) return;
    
        // Parse the complete line, removing any carriage returns
        msg = parse_regex.exec(line.replace(/^\r+|\r+$/, ''));
    
        if (!msg) {
            // The line was not parsed correctly, must be malformed
            console.log("Malformed IRC line: " + line.replace(/^\r+|\r+$/, ''));
            return;
        }
    
        // Extract any tags (msg[1])
        if (msg[1]) {
            tags = msg[1].split(';');
    
            for (j = 0; j < tags.length; j++) {
                tag = tags[j].split('=');
                tags[j] = {tag: tag[0], value: tag[1]};
            }
        }
    
        msg = {
            tags:       tags,
            prefix:     msg[2],
            nick:       msg[3],
            ident:      msg[4],
            hostname:   msg[5] || '',
            command:    msg[6],
            params:     msg[7] || '',
            trailing:   (msg[8]) ? msg[8].trim() : ''
        };
    
        msg.params = msg.params.split(' ');
    }
    
    nodeIRC = function (line, stripColors) { // {{{
        var message = {};
        var match;
    
        if (stripColors) {
            line = line.replace(/[\x02\x1f\x16\x0f]|\x03\d{0,2}(?:,\d{0,2})?/g, "");
        }
    
        // Parse prefix
        if ( match = line.match(/^:([^ ]+) +/) ) {
            message.prefix = match[1];
            line = line.replace(/^:[^ ]+ +/, '');
            if ( match = message.prefix.match(/^([_a-zA-Z0-9\[\]\\`^{}|-]*)(!([^@]+)@(.*))?$/) ) {
                message.nick = match[1];
                message.user = match[3];
                message.host = match[4];
            }
            else {
                message.server = message.prefix;
            }
        }
    
        // Parse command
        match = line.match(/^([^ ]+) */);
        message.command = match[1];
        message.rawCommand = match[1];
        message.commandType = 'normal';
        line = line.replace(/^[^ ]+ +/, '');
    
        message.args = [];
        var middle, trailing;
    
        // Parse parameters
        if ( line.search(/^:|\s+:/) != -1 ) {
            match = line.match(/(.*?)(?:^:|\s+:)(.*)/);
            middle = match[1].trimRight();
            trailing = match[2];
        }
        else {
            middle = line;
        }
    
        if ( middle.length )
            message.args = middle.split(/ +/);
    
        if ( typeof(trailing) != 'undefined' && trailing.length )
            message.args.push(trailing);
    
        return message;
    } // }}}
    
    var ircbParse = function (chunk) {
      var prefix = null,
          buffer = '',
          middle = [],
          command,
          trailing = null,
          matching,
          i;
    
      if (chunk[0] === ':') {
        matching = 'prefix';
        i = 1;
      }
      else {
        matching = 'command';
        i = 0;
      }
    
      for (; i < chunk.length; i++) {
        if (chunk[i] === ' ') {
          if (matching === 'prefix') {
            matching = 'command';
            prefix = buffer;
          }
          else if (matching === 'command') {
            matching = 'middle';
            command = buffer;
          }
          else if (matching === 'middle') {
            middle.push(buffer);
          }
          buffer = '';
        }
        else if (chunk[i] === ':' && matching === 'middle') {
          trailing = chunk.substring(i + 1, chunk.length);
          break;
        }
        else {
          buffer += chunk[i];
        }
      }
    
      if (matching === 'middle' && buffer) {
        middle.push(buffer);
      }
    
      return {
        prefix: prefix,
        command: command,
        middle: middle,
        trailing: trailing
      };
    };
    
    
    var tennu = function (message, receiver) {
        var ixa = -1, ixb = -1;
    
        if (tennu.hasSender(message)) {
            ixa = message.indexOf(" ");
    
            this.sender = message.slice(1, ixa);
        }
    
        ixb = message.indexOf(" ", ixa + 1);
    
        this.name = message.slice(ixa + 1, ixb).toLowerCase();
        this.args = tennu.parameterize(message.slice(ixb + 1));
        this.receiver = receiver;
    
        // Better than a bunch of unique constructors or calling an extra function.
        switch (this.name) {
            case "join":
            case "part":
            this.actor = this.sender.nick;
            this.channel = this.args[0].toLowerCase();
            break;
            case "privmsg":
            this.actor = this.sender.nick;
            this.message = this.args[1].trim().replace(mircColors, "");
            break;
            case "notice":
            this.actor = this.sender.nick;
            break;
            case "quit":
            this.actor = this.sender.nick;
            this.reason = this.args[0].toLowerCase();
            break;
            case "nick":
            this.actor = this.sender.nick;
            this.newNick = this.args[0];
            break;
            case "353":
            this.users = this.args[this.args.length - 1].trim().split(" ");
            this.channel = this.args[this.args.length - 2];
            break;
        }
    
        Object.freeze(this);
        Object.freeze(this.args);
    };
    
    tennu.prototype.equals = function (that) {
        for (var key in this) {
            if (this[key] !== that[key]) {
                return false;
            }
        }
    
        return true;
    };
    
    tennu.hasSender = function (message) {
        return message[0] === ":";
    };
    
    tennu.getSenderType = function (prefix) {
        var exclamation = /!/.test(prefix);
        var at = /@/.test(prefix);
        var dot = /\./.test(prefix);
    
        if (exclamation) {
            return "hostmask";
        }
    
        if (at && dot) {
            return "nick@host";
        }
    
        if (dot) {
            return "server";
        }
    
        return "nick";
    };
    
    tennu.parameterize = function (params) {
        var trailing, middle, trailingIx;
    
        trailingIx = params.indexOf(" :");
    
        if (params[0] === ":") {
            return [params.slice(1)];
        } else if (trailingIx === -1) {
            return params.split(" ");
        } else {
            trailing = params.slice(trailingIx + 2).trim();
            middle = params.slice(0, trailingIx).split(" ");
            middle.push(trailing); //push returns the length property
            return middle;
        }
    };

Test runner

Ready to run.

Testing in
TestOps/sec
irc-message
for (var i = 0; i < lines.length; i++) {
    new ircMessage(lines[i]);
}
ready
KiwiIRC
for (var i = 0; i < lines.length; i++) {
    kiwiParse(lines[i]);
}
ready
node-irc
for (var i = 0; i < lines.length; i++) {
    nodeIRC(lines[i]);
}
ready
ircb
for (var i = 0; i < lines.length; i++) {
    ircbParse(lines[i]);
}
ready
Tennu
for (var i = 0; i < lines.length; i++) {
    tennu(lines[i]);
}
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.

  • Revision 1: published by Fionn Kelleher on
  • Revision 2: published by Havvy on
  • Revision 3: published by Havvy on