Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@
"php": "^7.0"
},
"require-dev": {
"phpunit/phpunit": "^5.1",
"hamcrest/hamcrest-php": "^1.2",
"phpunit/phpunit": "~5.1.0",
"hamcrest/hamcrest-php": "~1.2.0",
"phpunit/php-token-stream": "~1.4.8",
"scrutinizer/ocular": "~1.3",
"squizlabs/php_codesniffer": "~2.5"
},
Expand Down
16 changes: 8 additions & 8 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

108 changes: 108 additions & 0 deletions src/Irc/Parser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

namespace PHPOxford\SpiresIrc\Irc;

class Parser
{
/**
* Parse a raw message following the message format defined in
* RFC 2812 for the Internet Relay Chat: Client Protocol
* http://tools.ietf.org/html/rfc2812#section-2.3.1
*/
public function parse(string $raw): array
{
// = %x0D %x0A ; "carriage return" "linefeed"
$crlf = '\r\n';

// = %x41-5A / %x61-7A ; A-Z / a-z
$letter = 'A-Z|a-z';

// = %x30-39 ; 0-9
$digit = '0-9';

// = digit / "A" / "B" / "C" / "D" / "E" / "F"
$hexdigit = "$digit|a-f|A-F";

// = %x5B-60 / %x7B-7D ; "[", "]", "\", "`", "_", "^", "{", "|", "}"
$special = '\x5B-\x60|\x7B-\x7D';

// = %x01-09 / %x0B-0C / %x0E-1F / %x21-39 / %x3B-FF ; any octet except NUL, CR, LF, " " and ":"
$nospcrlfcl = '\x01-\x09|\x0B-\x0C|\x0E-\x1F|\x21-\x39|\x3B-\xFF';

// = %x20 ; space character
$space = '\x20';

// Variables to make the following regular expressions easier to follow
$colon = ':';
$bang = '!';
$at = '@';
$dash = '-';
$dot = '\.';

// = 1*letter / 3digit
$command = "(?:[$letter]+|[$digit]{3})";

// = nospcrlfcl *( ":" / nospcrlfcl )
$middle = "(?:[$nospcrlfcl][$colon|$nospcrlfcl]*)";

// = *( ":" / " " / nospcrlfcl )
$trailing = "(?:[$colon|$space|$nospcrlfcl]*)";

// = *14( SPACE middle ) [ SPACE ":" trailing ]
// =/ 14( SPACE middle ) [ SPACE [ ":" ] trailing ]
$params = "(?:(?:$space$middle){0,14}(?:$space$colon$trailing)?" .
"|(?:$space$middle){14}(?:$space(?:$colon)?$trailing)?)";

// = ( letter / digit ) *( letter / digit / "-" ) *( letter / digit )
$shortname = "(?:[$letter$digit][$letter$digit$dash]*[$letter$digit]*)";

// = shortname *( "." shortname )
$hostname = "(?:$shortname(?:$dot$shortname)*)";

// = hostname
$servername = "$hostname";

// = 1*3digit "." 1*3digit "." 1*3digit "." 1*3digit
$ip4addr = "(?:(?:[$digit]{1,3})$dot(?:[$digit]{1,3})$dot(?:[$digit]{1,3})$dot(?:[$digit]{1,3}))";

// = 1*hexdigit 7( ":" 1*hexdigit )
// =/ "0:0:0:0:0:" ( "0" / "FFFF" ) ":" ip4addr
$ip6addr = "(?:(?:[$hexdigit]+?(?:$colon(?:[$hexdigit]+?)){7})|(?:0:0:0:0:0:(?:0|FFFF)$colon$ip4addr))";

// = ip4addr / ip6addr
$hostaddr = "(?:$ip4addr|$ip6addr)";

// = hostname / hostaddr
$host = "(?:$hostname|$hostaddr)";

// = ( letter / special ) *8( letter / digit / special / "-" )
$nickname = "(?:[$letter$special][$letter$digit$special$dash]{0,8})";

// = 1*( %x01-09 / %x0B-0C / %x0E-1F / %x21-3F / %x41-FF ) ; any octet except NUL, CR, LF, " " and "@"
$user = "(?:[\x01-\x09|\x0B-\x0C|\x0E-\x1F|\x21-\x3F|\x41-\xFF]+)";

// = servername / ( nickname [ [ "!" user ] "@" host ] )
$prefix = "(?:(?P<servername>$servername)" .
"|(?:(?P<nickname>$nickname)(?:$bang(?P<username>$user))?(?:$at(?P<hostname>$host))?))";

// = [ ":" prefix SPACE ] command [ params ] crlf
$message = "(?P<prefix>$colon$prefix$space)?(?P<command>$command)(?P<params>$params)?$crlf";

// Do the thing
preg_match("/^$message\$/SU", $raw, $matches);

// Trim whitespace
$matches = array_map('trim', $matches);

// Return only the named matches we want in the order we want
return [
'prefix' => $matches['prefix'] ?? '',
'nickname' => $matches['nickname'] ?? '',
'username' => $matches['username'] ?? '',
'hostname' => $matches['hostname'] ?? '',
'servername' => $matches['servername'] ?? '',
'command' => $matches['command'],
'params' => $matches['params'] ?? '',
];
}
}
198 changes: 198 additions & 0 deletions tests/Irc/ParserTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
<?php

namespace PHPOxford\SpiresIrc\Tests\Irc;

use PHPOxford\SpiresIrc\Irc\Parser;

class ParserTest extends \PHPUnit_Framework_TestCase
{
/**
* @test
* @covers \PHPOxford\SpiresIrc\Irc\Parser::parse
* @dataProvider dataMessages
*/
public function can_parse_valid_messages_to_an_array_of_data($message, $expected)
{
$parser = new Parser();

$parse = $parser->parse($message);

assertThat($parse, is(identicalTo($expected)));
}

public function dataMessages()
{
return [
'Command with no params' => [
"QUIT\r\n",
[
'prefix' => '',
'nickname' => '',
'username' => '',
'hostname' => '',
'servername' => '',
'command' => 'QUIT',
'params' => '',
]
],
'Command with only one param' => [
"NICK dilling\r\n",
[
'prefix' => '',
'nickname' => '',
'username' => '',
'hostname' => '',
'servername' => '',
'command' => 'NICK',
'params' => 'dilling',
]
],
'Command with multiple params' => [
"USER guest 0 * :Ronnie Reagan\r\n",
[
'prefix' => '',
'nickname' => '',
'username' => '',
'hostname' => '',
'servername' => '',
'command' => 'USER',
'params' => 'guest 0 * :Ronnie Reagan',
]
],
'Only first part of the prefix present, then it is the servername' => [
":asimov.freenode.net QUIT\r\n",
[
'prefix' => ':asimov.freenode.net',
'nickname' => '',
'username' => '',
'hostname' => '',
'servername' => 'asimov.freenode.net',
'command' => 'QUIT',
'params' => '',
]
],
'First two parts of the prefix present, then it is the nickname and username' => [
":dilling!~dilling JOIN #phpoxford\r\n",
[
'prefix' => ':dilling!~dilling',
'nickname' => 'dilling',
'username' => '~dilling',
'hostname' => '',
'servername' => '',
'command' => 'JOIN',
'params' => '#phpoxford',
]
],
'All three parts of the prefix present, then it is the nickname, username and hostname' => [
":dilling!~dilling@cable.virginm.net NICK martindilling\r\n",
[
'prefix' => ':dilling!~dilling@cable.virginm.net',
'nickname' => 'dilling',
'username' => '~dilling',
'hostname' => 'cable.virginm.net',
'servername' => '',
'command' => 'NICK',
'params' => 'martindilling',
]
],
'The hostname can be a IPv4 address' => [
":dilling!~dilling@168.12.8.204 NICK martindilling\r\n",
[
'prefix' => ':dilling!~dilling@168.12.8.204',
'nickname' => 'dilling',
'username' => '~dilling',
'hostname' => '168.12.8.204',
'servername' => '',
'command' => 'NICK',
'params' => 'martindilling',
]
],
'The hostname can be a IPv6 address' => [
":dilling!~dilling@2001:0db8:0a0b:12f0:0000:0000:0000:0001 NICK martindilling\r\n",
[
'prefix' => ':dilling!~dilling@2001:0db8:0a0b:12f0:0000:0000:0000:0001',
'nickname' => 'dilling',
'username' => '~dilling',
'hostname' => '2001:0db8:0a0b:12f0:0000:0000:0000:0001',
'servername' => '',
'command' => 'NICK',
'params' => 'martindilling',
]
],
'Recognises really long messages' => [
":dilling!~dilling@cable.virginm.net PRIVMSG ascii-soup :This is a really really long message! " .
"It is longer than most message, so I really hope this works as it should\r\n",
[
'prefix' => ':dilling!~dilling@cable.virginm.net',
'nickname' => 'dilling',
'username' => '~dilling',
'hostname' => 'cable.virginm.net',
'servername' => '',
'command' => 'PRIVMSG',
'params' => 'ascii-soup :This is a really really long message! ' .
'It is longer than most message, so I really hope this works as it should',
]
],
'Safety: PING command' => [
"PING :wolfe.freenode.net\r\n",
[
'prefix' => '',
'nickname' => '',
'username' => '',
'hostname' => '',
'servername' => '',
'command' => 'PING',
'params' => ':wolfe.freenode.net',
]
],
'Safety: NICK command' => [
":dilling!~dilling@cable.virginm.net NICK :imchanged\r\n",
[
'prefix' => ':dilling!~dilling@cable.virginm.net',
'nickname' => 'dilling',
'username' => '~dilling',
'hostname' => 'cable.virginm.net',
'servername' => '',
'command' => 'NICK',
'params' => ':imchanged',
]
],
'Safety: JOIN command' => [
":dilling!~dilling@cable.virginm.net JOIN #phpoxford\r\n",
[
'prefix' => ':dilling!~dilling@cable.virginm.net',
'nickname' => 'dilling',
'username' => '~dilling',
'hostname' => 'cable.virginm.net',
'servername' => '',
'command' => 'JOIN',
'params' => '#phpoxford',
]
],
'Safety: PART command' => [
":dilling!~dilling@cable.virginm.net PART #phpoxford :Leaving\r\n",
[
'prefix' => ':dilling!~dilling@cable.virginm.net',
'nickname' => 'dilling',
'username' => '~dilling',
'hostname' => 'cable.virginm.net',
'servername' => '',
'command' => 'PART',
'params' => '#phpoxford :Leaving',
]
],
'Safety: QUIT command' => [
":dilling!~dilling@cable.virginm.net QUIT :Quit: I'm outta here\r\n",
[
'prefix' => ':dilling!~dilling@cable.virginm.net',
'nickname' => 'dilling',
'username' => '~dilling',
'hostname' => 'cable.virginm.net',
'servername' => '',
'command' => 'QUIT',
'params' => ':Quit: I\'m outta here',
]
],
];
}
}