first commit

This commit is contained in:
LEO FIRMIN 2026-05-25 10:24:09 +02:00
commit 332cb7501a
12 changed files with 763 additions and 0 deletions

132
README_PARSING.md Normal file
View File

@ -0,0 +1,132 @@
# ft_irc parsing module
Partie parsing C++98 pour `ft_irc`.
## Ce que ça fait
- accumule les données TCP reçues dans un buffer par client ;
- extrait les lignes complètes terminées par `\r\n` ou `\n` ;
- parse une ligne IRC en :
- prefix optionnel ;
- command ;
- params ;
- trailing param après `:`, qui peut contenir des espaces ;
- normalise les commandes en uppercase ;
- fournit une validation minimale du nombre de paramètres.
## Ce que ça ne fait volontairement pas
Le parser ne doit pas gérer la logique métier :
- vérifier le password ;
- vérifier si le nickname existe déjà ;
- créer les channels ;
- vérifier les permissions operator ;
- appliquer les modes ;
- envoyer les numeric replies.
Tout ça appartient aux handlers de commandes.
## Utilisation dans ton Client
Chaque client doit avoir son propre `ParseBuffer`.
Exemple :
```cpp
// Dans ta classe Client
ParseBuffer _parseBuffer;
```
Quand `poll()` indique que le fd client est lisible :
```cpp
char buf[4096];
ssize_t n = recv(clientFd, buf, sizeof(buf), 0);
if (n <= 0)
{
// déconnexion ou erreur
}
else
{
client.getParseBuffer().append(buf, n);
std::vector<IrcMessage> messages = client.getParseBuffer().extractMessages();
for (size_t i = 0; i < messages.size(); ++i)
{
IrcMessage &msg = messages[i];
if (!msg.isValid())
{
// selon l'erreur:
// - ignorer
// - répondre ERR_UNKNOWNCOMMAND
// - fermer si line too long
continue;
}
// dispatcher.handle(client, msg);
}
}
```
## Compilation test
```bash
c++ -Wall -Wextra -Werror -std=c++98 \
-Iincludes \
srcs/IrcMessage.cpp \
srcs/IrcParser.cpp \
srcs/ParseBuffer.cpp \
srcs/CommandValidator.cpp \
test/main.cpp \
-o parser_test
./parser_test
```
## Format IRC géré
```txt
COMMAND param1 param2 :trailing avec espaces
```
Exemples :
```txt
PASS secret
NICK jeremy
USER jeremy 0 * :Jeremy Real Name
JOIN #42
PRIVMSG #42 :salut les gars
MODE #42 +it
MODE #42 +k password
KICK #42 bob :raison du kick
```
## Intégration recommandée
Architecture propre :
```txt
Client
- fd
- nick
- username
- ParseBuffer
Server
- poll loop
- accept
- recv
- donne les IrcMessage au dispatcher
CommandDispatcher
- map command -> handler
- PASS/NICK/USER/JOIN/PRIVMSG/MODE/etc.
Parser
- aucune logique métier
```

View File

@ -0,0 +1,20 @@
#ifndef COMMANDVALIDATOR_HPP
# define COMMANDVALIDATOR_HPP
# include <string>
# include "IrcMessage.hpp"
class CommandValidator
{
public:
CommandValidator(void);
CommandValidator(const CommandValidator &other);
CommandValidator &operator=(const CommandValidator &other);
~CommandValidator(void);
static bool hasEnoughParams(const IrcMessage &msg);
static bool needsRegistration(const std::string &command);
static bool isKnownCommand(const std::string &command);
};
#endif

43
includes/IrcMessage.hpp Normal file
View File

@ -0,0 +1,43 @@
#ifndef IRCMESSAGE_HPP
# define IRCMESSAGE_HPP
# include <string>
# include <vector>
class IrcMessage
{
private:
std::string _prefix;
std::string _command;
std::vector<std::string> _params;
std::string _raw;
bool _valid;
std::string _error;
public:
IrcMessage(void);
IrcMessage(const IrcMessage &other);
IrcMessage &operator=(const IrcMessage &other);
~IrcMessage(void);
void setPrefix(const std::string &prefix);
void setCommand(const std::string &command);
void addParam(const std::string &param);
void setRaw(const std::string &raw);
void setValid(bool valid);
void setError(const std::string &error);
const std::string &getPrefix(void) const;
const std::string &getCommand(void) const;
const std::vector<std::string> &getParams(void) const;
const std::string &getRaw(void) const;
bool isValid(void) const;
const std::string &getError(void) const;
bool hasPrefix(void) const;
bool hasParam(size_t index) const;
const std::string &param(size_t index) const;
size_t paramCount(void) const;
};
#endif

28
includes/IrcParser.hpp Normal file
View File

@ -0,0 +1,28 @@
#ifndef IRCPARSER_HPP
# define IRCPARSER_HPP
# include <string>
# include <vector>
# include "IrcMessage.hpp"
class IrcParser
{
private:
static std::string trimLeft(const std::string &s);
static std::string trimRight(const std::string &s);
static std::string toUpper(const std::string &s);
static bool isSpace(char c);
static bool isCommandChar(char c);
static bool isValidCommand(const std::string &command);
static void parseParams(const std::string &s, size_t pos, IrcMessage &msg);
public:
IrcParser(void);
IrcParser(const IrcParser &other);
IrcParser &operator=(const IrcParser &other);
~IrcParser(void);
static IrcMessage parseLine(const std::string &line);
};
#endif

31
includes/ParseBuffer.hpp Normal file
View File

@ -0,0 +1,31 @@
#ifndef PARSEBUFFER_HPP
# define PARSEBUFFER_HPP
# include <string>
# include <vector>
# include "IrcMessage.hpp"
class ParseBuffer
{
private:
std::string _buffer;
size_t _maxLineSize;
bool extractOneLine(std::string &line);
public:
ParseBuffer(void);
explicit ParseBuffer(size_t maxLineSize);
ParseBuffer(const ParseBuffer &other);
ParseBuffer &operator=(const ParseBuffer &other);
~ParseBuffer(void);
void append(const char *data, size_t len);
std::vector<IrcMessage> extractMessages(void);
void clear(void);
const std::string &raw(void) const;
size_t size(void) const;
};
#endif

58
includes/Server.hpp Normal file
View File

@ -0,0 +1,58 @@
#ifndef SERVER_HPP
# define SERVER_HPP
# include <string>
# include <vector>
# include <map>
# include <sys/socket.h>
# include <netinet/in.h>
# include <arpa/inet.h>
# include <poll.h>
# include <fcntl.h>
# include <unistd.h>
# include "ParseBuffer.hpp"
# include <stdexcept>
# include <cstring>
# include <cerrno>
struct ConnectedUser
{
int fd;
std::string nick;
std::string username;
std::string realname;
std::string hostname;
bool passOk;
bool nickOk;
bool userOk;
ParseBuffer parseBuffer;
explicit ConnectedUser(int fd);
bool isRegistered(void) const;
};
class Server
{
private:
int _serverFd;
int _port;
std::string _password;
std::vector<struct pollfd> _fds;
std::map<int, ConnectedUser> _users;
void acceptNew(void);
void handleRecv(int fd);
void removeUser(int fd);
public:
Server(int port, const std::string &password);
Server(const Server &other);
Server &operator=(const Server &other);
~Server(void);
void start(void);
void run(void);
void sendReply(int fd, const std::string &msg);
};
#endif

67
srcs/CommandValidator.cpp Normal file
View File

@ -0,0 +1,67 @@
#include "CommandValidator.hpp"
CommandValidator::CommandValidator(void) {}
CommandValidator::CommandValidator(const CommandValidator &other) { (void)other; }
CommandValidator &CommandValidator::operator=(const CommandValidator &other) { (void)other; return (*this); }
CommandValidator::~CommandValidator(void) {}
bool CommandValidator::isKnownCommand(const std::string &cmd)
{
return (cmd == "PASS"
|| cmd == "NICK"
|| cmd == "USER"
|| cmd == "JOIN"
|| cmd == "PART"
|| cmd == "PRIVMSG"
|| cmd == "NOTICE"
|| cmd == "QUIT"
|| cmd == "PING"
|| cmd == "PONG"
|| cmd == "KICK"
|| cmd == "INVITE"
|| cmd == "TOPIC"
|| cmd == "MODE");
}
/*
This is only syntactic/minimal validation.
The real command handler still checks:
- password correctness
- nickname already used
- channel exists
- permissions/operator status
- invite-only/key/limit modes
*/
bool CommandValidator::hasEnoughParams(const IrcMessage &msg)
{
const std::string &cmd = msg.getCommand();
if (cmd == "PASS") return (msg.paramCount() >= 1);
if (cmd == "NICK") return (msg.paramCount() >= 1);
if (cmd == "USER") return (msg.paramCount() >= 4);
if (cmd == "JOIN") return (msg.paramCount() >= 1);
if (cmd == "PART") return (msg.paramCount() >= 1);
if (cmd == "PRIVMSG") return (msg.paramCount() >= 2);
if (cmd == "NOTICE") return (msg.paramCount() >= 2);
if (cmd == "QUIT") return (true);
if (cmd == "PING") return (msg.paramCount() >= 1);
if (cmd == "PONG") return (msg.paramCount() >= 1);
if (cmd == "KICK") return (msg.paramCount() >= 2);
if (cmd == "INVITE") return (msg.paramCount() >= 2);
if (cmd == "TOPIC") return (msg.paramCount() >= 1);
if (cmd == "MODE") return (msg.paramCount() >= 1);
return (false);
}
/*
Before registration, usually only PASS, NICK, USER, PING/PONG/QUIT are useful.
*/
bool CommandValidator::needsRegistration(const std::string &cmd)
{
return !(cmd == "PASS"
|| cmd == "NICK"
|| cmd == "USER"
|| cmd == "PING"
|| cmd == "PONG"
|| cmd == "QUIT");
}

62
srcs/IrcMessage.cpp Normal file
View File

@ -0,0 +1,62 @@
#include "IrcMessage.hpp"
IrcMessage::IrcMessage(void) : _valid(false)
{
}
IrcMessage::IrcMessage(const IrcMessage &other)
{
*this = other;
}
IrcMessage &IrcMessage::operator=(const IrcMessage &other)
{
if (this != &other)
{
_prefix = other._prefix;
_command = other._command;
_params = other._params;
_raw = other._raw;
_valid = other._valid;
_error = other._error;
}
return (*this);
}
IrcMessage::~IrcMessage(void)
{
}
void IrcMessage::setPrefix(const std::string &prefix) { _prefix = prefix; }
void IrcMessage::setCommand(const std::string &command) { _command = command; }
void IrcMessage::addParam(const std::string &param) { _params.push_back(param); }
void IrcMessage::setRaw(const std::string &raw) { _raw = raw; }
void IrcMessage::setValid(bool valid) { _valid = valid; }
void IrcMessage::setError(const std::string &error) { _error = error; }
const std::string &IrcMessage::getPrefix(void) const { return (_prefix); }
const std::string &IrcMessage::getCommand(void) const { return (_command); }
const std::vector<std::string> &IrcMessage::getParams(void) const { return (_params); }
const std::string &IrcMessage::getRaw(void) const { return (_raw); }
bool IrcMessage::isValid(void) const { return (_valid); }
const std::string &IrcMessage::getError(void) const { return (_error); }
bool IrcMessage::hasPrefix(void) const
{
return (!_prefix.empty());
}
bool IrcMessage::hasParam(size_t index) const
{
return (index < _params.size());
}
const std::string &IrcMessage::param(size_t index) const
{
return (_params[index]);
}
size_t IrcMessage::paramCount(void) const
{
return (_params.size());
}

142
srcs/IrcParser.cpp Normal file
View File

@ -0,0 +1,142 @@
#include "IrcParser.hpp"
#include <cctype>
IrcParser::IrcParser(void) {}
IrcParser::IrcParser(const IrcParser &other) { (void)other; }
IrcParser &IrcParser::operator=(const IrcParser &other) { (void)other; return (*this); }
IrcParser::~IrcParser(void) {}
bool IrcParser::isSpace(char c)
{
return (c == ' ' || c == '\t');
}
std::string IrcParser::trimLeft(const std::string &s)
{
size_t i = 0;
while (i < s.size() && isSpace(s[i]))
i++;
return (s.substr(i));
}
std::string IrcParser::trimRight(const std::string &s)
{
size_t end = s.size();
while (end > 0 && (s[end - 1] == '\r' || s[end - 1] == '\n' || isSpace(s[end - 1])))
end--;
return (s.substr(0, end));
}
std::string IrcParser::toUpper(const std::string &s)
{
std::string out = s;
for (size_t i = 0; i < out.size(); ++i)
out[i] = static_cast<char>(std::toupper(static_cast<unsigned char>(out[i])));
return (out);
}
bool IrcParser::isCommandChar(char c)
{
return (std::isalpha(static_cast<unsigned char>(c)) || std::isdigit(static_cast<unsigned char>(c)));
}
bool IrcParser::isValidCommand(const std::string &command)
{
if (command.empty())
return (false);
for (size_t i = 0; i < command.size(); ++i)
{
if (!isCommandChar(command[i]))
return (false);
}
return (true);
}
/*
IRC params rule:
- parameters are separated by spaces
- if a parameter starts with ':', it is the trailing parameter
- the trailing parameter may contain spaces and is always the last param
Example:
PRIVMSG #chan :hello les gars
params[0] = "#chan"
params[1] = "hello les gars"
*/
void IrcParser::parseParams(const std::string &s, size_t pos, IrcMessage &msg)
{
while (pos < s.size())
{
while (pos < s.size() && isSpace(s[pos]))
pos++;
if (pos >= s.size())
break;
if (s[pos] == ':')
{
msg.addParam(s.substr(pos + 1));
break;
}
size_t start = pos;
while (pos < s.size() && !isSpace(s[pos]))
pos++;
msg.addParam(s.substr(start, pos - start));
}
}
IrcMessage IrcParser::parseLine(const std::string &line)
{
IrcMessage msg;
std::string s;
size_t pos;
size_t start;
msg.setRaw(line);
s = trimRight(trimLeft(line));
if (s.empty())
{
msg.setError("empty line");
return (msg);
}
/*
Optional prefix:
:nick!user@host COMMAND params...
In ft_irc, clients usually do not send prefixes, but accepting it makes the parser robust.
*/
pos = 0;
if (s[pos] == ':')
{
size_t prefixEnd = s.find(' ', pos);
if (prefixEnd == std::string::npos)
{
msg.setError("prefix without command");
return (msg);
}
msg.setPrefix(s.substr(1, prefixEnd - 1));
pos = prefixEnd + 1;
while (pos < s.size() && isSpace(s[pos]))
pos++;
}
start = pos;
while (pos < s.size() && !isSpace(s[pos]))
pos++;
msg.setCommand(toUpper(s.substr(start, pos - start)));
if (!isValidCommand(msg.getCommand()))
{
msg.setError("invalid command");
return (msg);
}
parseParams(s, pos, msg);
msg.setValid(true);
return (msg);
}

94
srcs/ParseBuffer.cpp Normal file
View File

@ -0,0 +1,94 @@
#include "ParseBuffer.hpp"
#include "IrcParser.hpp"
ParseBuffer::ParseBuffer(void) : _maxLineSize(512)
{
}
ParseBuffer::ParseBuffer(size_t maxLineSize) : _maxLineSize(maxLineSize)
{
}
ParseBuffer::ParseBuffer(const ParseBuffer &other)
{
*this = other;
}
ParseBuffer &ParseBuffer::operator=(const ParseBuffer &other)
{
if (this != &other)
{
_buffer = other._buffer;
_maxLineSize = other._maxLineSize;
}
return (*this);
}
ParseBuffer::~ParseBuffer(void)
{
}
void ParseBuffer::append(const char *data, size_t len)
{
if (data && len > 0)
_buffer.append(data, len);
}
/*
Extract lines terminated by:
- "\r\n" standard IRC
- "\n" useful for netcat/manual tests
The returned line does not include CRLF/LF.
*/
bool ParseBuffer::extractOneLine(std::string &line)
{
size_t lf = _buffer.find('\n');
if (lf == std::string::npos)
return (false);
line = _buffer.substr(0, lf);
if (!line.empty() && line[line.size() - 1] == '\r')
line.erase(line.size() - 1);
_buffer.erase(0, lf + 1);
return (true);
}
std::vector<IrcMessage> ParseBuffer::extractMessages(void)
{
std::vector<IrcMessage> messages;
std::string line;
while (extractOneLine(line))
{
IrcMessage msg;
if (line.size() + 2 > _maxLineSize)
{
msg.setRaw(line);
msg.setError("line too long");
msg.setValid(false);
}
else
msg = IrcParser::parseLine(line);
messages.push_back(msg);
}
return (messages);
}
void ParseBuffer::clear(void)
{
_buffer.clear();
}
const std::string &ParseBuffer::raw(void) const
{
return (_buffer);
}
size_t ParseBuffer::size(void) const
{
return (_buffer.size());
}

39
srcs/Server.cpp Normal file
View File

@ -0,0 +1,39 @@
#include "Server.hpp"
void Server::start(void)
{
_serverFd = socket(AF_INET, SOCK_STREAM, 0);
if (_serverFd == -1)
throw std::runtime_error(strerror(errno));
int opt = 1;
if (setsockopt(_serverFd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1)
{
close(_serverFd);
throw std::runtime_error(strerror(errno));
}
if (fcntl(_serverFd, F_SETFL, O_NONBLOCK) == -1)
{
close(_serverFd);
throw std::runtime_error(strerror(errno));
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(_port);
addr.sin_addr.s_addr = INADDR_ANY;
if (bind(_serverFd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
close(_serverFd);
throw std::runtime_error(strerror(errno));
}
if (listen(_serverFd, SOMAXCONN) == -1)
{
close(_serverFd);
throw std::runtime_error(strerror(errno));
}
struct pollfd pollfd;
pollfd.fd = _serverFd;
pollfd.events = POLLIN;
pollfd.revents = 0;
_fds.push_back(pollfd);
}

47
test/main.cpp Normal file
View File

@ -0,0 +1,47 @@
#include <iostream>
#include <vector>
#include "ParseBuffer.hpp"
#include "CommandValidator.hpp"
static void printMessage(const IrcMessage &msg)
{
std::cout << "raw=[" << msg.getRaw() << "]\n";
std::cout << "valid=" << (msg.isValid() ? "yes" : "no") << "\n";
if (!msg.getError().empty())
std::cout << "error=" << msg.getError() << "\n";
if (msg.hasPrefix())
std::cout << "prefix=" << msg.getPrefix() << "\n";
std::cout << "command=" << msg.getCommand() << "\n";
for (size_t i = 0; i < msg.paramCount(); ++i)
std::cout << "param[" << i << "]=[" << msg.param(i) << "]\n";
std::cout << "known=" << (CommandValidator::isKnownCommand(msg.getCommand()) ? "yes" : "no") << "\n";
std::cout << "enough_params=" << (CommandValidator::hasEnoughParams(msg) ? "yes" : "no") << "\n";
std::cout << "-----\n";
}
int main(void)
{
ParseBuffer buffer;
std::string chunk1 = "PASS secret\r\nNICK je";
std::string chunk2 = "remy\r\nUSER jeremy 0 * :Jeremy Real Name\r\n";
std::string chunk3 = "JOIN #42\r\nPRIVMSG #42 :salut les gars ca va ?\r\n";
std::string chunk4 = ":nick!user@host PRIVMSG jeremy :hello from prefixed line\r\n";
buffer.append(chunk1.c_str(), chunk1.size());
std::vector<IrcMessage> messages = buffer.extractMessages();
for (size_t i = 0; i < messages.size(); ++i)
printMessage(messages[i]);
buffer.append(chunk2.c_str(), chunk2.size());
messages = buffer.extractMessages();
for (size_t i = 0; i < messages.size(); ++i)
printMessage(messages[i]);
buffer.append(chunk3.c_str(), chunk3.size());
buffer.append(chunk4.c_str(), chunk4.size());
messages = buffer.extractMessages();
for (size_t i = 0; i < messages.size(); ++i)
printMessage(messages[i]);
return (0);
}