From 332cb7501aa2128ccc6fd2f233b85131d2cb29bf Mon Sep 17 00:00:00 2001 From: lfirmin Date: Mon, 25 May 2026 10:24:09 +0200 Subject: [PATCH] first commit --- README_PARSING.md | 132 +++++++++++++++++++++++++++++++ includes/CommandValidator.hpp | 20 +++++ includes/IrcMessage.hpp | 43 ++++++++++ includes/IrcParser.hpp | 28 +++++++ includes/ParseBuffer.hpp | 31 ++++++++ includes/Server.hpp | 58 ++++++++++++++ srcs/CommandValidator.cpp | 67 ++++++++++++++++ srcs/IrcMessage.cpp | 62 +++++++++++++++ srcs/IrcParser.cpp | 142 ++++++++++++++++++++++++++++++++++ srcs/ParseBuffer.cpp | 94 ++++++++++++++++++++++ srcs/Server.cpp | 39 ++++++++++ test/main.cpp | 47 +++++++++++ 12 files changed, 763 insertions(+) create mode 100644 README_PARSING.md create mode 100644 includes/CommandValidator.hpp create mode 100644 includes/IrcMessage.hpp create mode 100644 includes/IrcParser.hpp create mode 100644 includes/ParseBuffer.hpp create mode 100644 includes/Server.hpp create mode 100644 srcs/CommandValidator.cpp create mode 100644 srcs/IrcMessage.cpp create mode 100644 srcs/IrcParser.cpp create mode 100644 srcs/ParseBuffer.cpp create mode 100644 srcs/Server.cpp create mode 100644 test/main.cpp diff --git a/README_PARSING.md b/README_PARSING.md new file mode 100644 index 0000000..16b55c9 --- /dev/null +++ b/README_PARSING.md @@ -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 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 +``` diff --git a/includes/CommandValidator.hpp b/includes/CommandValidator.hpp new file mode 100644 index 0000000..3f7f3fb --- /dev/null +++ b/includes/CommandValidator.hpp @@ -0,0 +1,20 @@ +#ifndef COMMANDVALIDATOR_HPP +# define COMMANDVALIDATOR_HPP + +# include +# 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 diff --git a/includes/IrcMessage.hpp b/includes/IrcMessage.hpp new file mode 100644 index 0000000..f9d3210 --- /dev/null +++ b/includes/IrcMessage.hpp @@ -0,0 +1,43 @@ +#ifndef IRCMESSAGE_HPP +# define IRCMESSAGE_HPP + +# include +# include + +class IrcMessage +{ + private: + std::string _prefix; + std::string _command; + std::vector _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 ¶m); + 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 &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 ¶m(size_t index) const; + size_t paramCount(void) const; +}; + +#endif diff --git a/includes/IrcParser.hpp b/includes/IrcParser.hpp new file mode 100644 index 0000000..3dcd4be --- /dev/null +++ b/includes/IrcParser.hpp @@ -0,0 +1,28 @@ +#ifndef IRCPARSER_HPP +# define IRCPARSER_HPP + +# include +# include +# 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 diff --git a/includes/ParseBuffer.hpp b/includes/ParseBuffer.hpp new file mode 100644 index 0000000..475b980 --- /dev/null +++ b/includes/ParseBuffer.hpp @@ -0,0 +1,31 @@ +#ifndef PARSEBUFFER_HPP +# define PARSEBUFFER_HPP + +# include +# include +# 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 extractMessages(void); + void clear(void); + + const std::string &raw(void) const; + size_t size(void) const; +}; + +#endif diff --git a/includes/Server.hpp b/includes/Server.hpp new file mode 100644 index 0000000..461f60b --- /dev/null +++ b/includes/Server.hpp @@ -0,0 +1,58 @@ +#ifndef SERVER_HPP +# define SERVER_HPP + +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include "ParseBuffer.hpp" +# include +# include +# include + +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 _fds; + std::map _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 diff --git a/srcs/CommandValidator.cpp b/srcs/CommandValidator.cpp new file mode 100644 index 0000000..33f4ea4 --- /dev/null +++ b/srcs/CommandValidator.cpp @@ -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"); +} diff --git a/srcs/IrcMessage.cpp b/srcs/IrcMessage.cpp new file mode 100644 index 0000000..bffe60d --- /dev/null +++ b/srcs/IrcMessage.cpp @@ -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 ¶m) { _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 &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()); +} diff --git a/srcs/IrcParser.cpp b/srcs/IrcParser.cpp new file mode 100644 index 0000000..72787c3 --- /dev/null +++ b/srcs/IrcParser.cpp @@ -0,0 +1,142 @@ +#include "IrcParser.hpp" +#include + +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(std::toupper(static_cast(out[i]))); + return (out); +} + +bool IrcParser::isCommandChar(char c) +{ + return (std::isalpha(static_cast(c)) || std::isdigit(static_cast(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); +} diff --git a/srcs/ParseBuffer.cpp b/srcs/ParseBuffer.cpp new file mode 100644 index 0000000..170e9bc --- /dev/null +++ b/srcs/ParseBuffer.cpp @@ -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 ParseBuffer::extractMessages(void) +{ + std::vector 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()); +} diff --git a/srcs/Server.cpp b/srcs/Server.cpp new file mode 100644 index 0000000..78d3da5 --- /dev/null +++ b/srcs/Server.cpp @@ -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); +} \ No newline at end of file diff --git a/test/main.cpp b/test/main.cpp new file mode 100644 index 0000000..56d55eb --- /dev/null +++ b/test/main.cpp @@ -0,0 +1,47 @@ +#include +#include +#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 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); +}