[
  {
    "path": ".gitignore",
    "content": "kirc\n*.a\n*.o"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021-2025 Michael Czigler\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": ".POSIX:\n.SUFFIXES:\n\ninclude config.mk\n\nCFLAGS += -std=c99 -pedantic -Wall -Wextra\nCFLAGS += -Wformat-security -Wwrite-strings\nCFLAGS += -Wno-unused-parameter\nCFLAGS += -g -Iinclude -Isrc\n\nBIN = kirc\nSRC = src\nBUILD = build\n\n# Discover all source files\nSRCS = $(wildcard $(SRC)/*.c)\n\n# Create matching build/*.o paths\nOBJS = $(SRCS:$(SRC)/%.c=$(BUILD)/%.o)\n\nall: $(BIN)\n\n$(BIN): $(OBJS)\n\t$(CC) -o $@ $^ $(LDFLAGS)\n\n$(BUILD):\n\tmkdir -p $@\n\n# Pattern rule: build/xyz.o ← src/xyz.c\n$(BUILD)/%.o: $(SRC)/%.c | $(BUILD)\n\t$(CC) $(CFLAGS) -c -o $@ $<\n\nclean:\n\trm -f $(BIN) $(BUILD)/*\n\ninstall:\n\tmkdir -p $(DESTDIR)$(BINDIR)\n\tcp -f $(BIN) $(DESTDIR)$(BINDIR)\n\tchmod 755 $(DESTDIR)$(BINDIR)/$(BIN)\n\tmkdir -p $(DESTDIR)$(MANDIR)/man1\n\tcp -f $(BIN).1 $(DESTDIR)$(MANDIR)/man1\n\tchmod 644 $(DESTDIR)$(MANDIR)/man1/$(BIN).1\n\nuninstall:\n\trm -f $(DESTDIR)$(BINDIR)/$(BIN)\n\trm -f $(DESTDIR)$(MANDIR)/man1/$(BIN).1\n\n.PHONY: all clean install uninstall\n"
  },
  {
    "path": "README.md",
    "content": "kirc\n====\n\nkirc is a tiny, dependency-light IRC client that follows the\n\"Keep It Simple, Stupid\" (KISS) philosophy. It is intended for\nusers who prefer a minimal, keyboard-driven IRC experience in a\nterminal environment.\n\nFeatures\n--------\n\n* Small, portable C codebase with minimal runtime requirements\n* Keyboard-centric editing and navigation\n* Simple command and message aliases for fast interaction\n* Lightweight build using a standard Makefile\n\nInstall\n-------\n\nBuild and install from source (system-wide install requires\nappropriate privileges):\n\n    git clone https://github.com/mcpcpc/kirc\n    cd kirc/\n    make\n    sudo make install\n\nAlternatively, run the binary from the repository root after\nbuilding: `./kirc`.\n\nUsage\n-----\n\nSee `man kirc` for a more usage information.\n\n    kirc [-s server] [-p port] [-c channels] [-r realname]\n         [-u username] [-k password] [-a auth] <nickname>\n\nLicense\n-------\n\nSee the `LICENSE` file in the repository for details.\n"
  },
  {
    "path": "config.mk",
    "content": "# config.mk - default build configuration\n\nPREFIX = /usr/local\nBINDIR = $(PREFIX)/bin\nMANDIR = $(PREFIX)/share/man\n\nCC = cc\n"
  },
  {
    "path": "include/ansi.h",
    "content": "/*\n * ansi.h\n * ANSI escape codes and constants\n * Author: Michael Czigler\n * License: MIT\n */\n\n#ifndef __KIRC_ANSI_H\n#define __KIRC_ANSI_H\n\n#define SOH          0x01\n#define STX          0x02\n#define ETX          0x03\n#define EOT          0x04\n#define ENQ          0x05\n#define ACK          0x06\n#define BEL          0x07\n#define BS           0x08\n#define HT           0x09\n#define LF           0x0A\n#define VT           0x0B\n#define FF           0x0C\n#define CR           0x0D\n#define SO           0x0E\n#define SI           0x0F\n#define DLE          0x10\n#define DC1          0x11\n#define DC2          0x12\n#define DC3          0x13\n#define DC4          0x14\n#define NAK          0x15\n#define SYN          0x16\n#define ETB          0x17\n#define CAN          0x18\n#define EM           0x19\n#define SUB          0x1A\n#define ESC          0x1B\n#define FS           0x1C\n#define GS           0x1D\n#define RS           0x1E\n#define US           0x1F\n#define DEL          0x7F\n\n#define RESET        \"\\x1b[0m\"\n#define BOLD         \"\\x1b[1m\"\n#define DIM          \"\\x1b[2m\"\n#define ITALIC       \"\\x1b[3m\"\n#define REVERSE      \"\\x1b[7m\"\n#define RED          \"\\x1b[31m\"\n#define GREEN        \"\\x1b[32m\"\n#define YELLOW       \"\\x1b[33m\"\n#define BLUE         \"\\x1b[34m\"\n#define MAGENTA      \"\\x1b[35m\"\n#define CYAN         \"\\x1b[36m\"\n#define BOLD_RED     \"\\x1b[1;31m\"\n#define BOLD_GREEN   \"\\x1b[1;32m\"\n#define BOLD_YELLOW  \"\\x1b[1;33m\"\n#define BOLD_BLUE    \"\\x1b[1;34m\"\n#define BOLD_MAGENTA \"\\x1b[1;35m\"\n#define BOLD_CYAN    \"\\x1b[1;36m\"\n#define CLEAR_LINE   \"\\x1b[0K\"\n#define CURSOR_HOME  \"\\x1b[H\"\n#define CURSOR_POS   \"\\x1b[6n\"\n\n#endif  // __KIRC_ANSI_H\n"
  },
  {
    "path": "include/base64.h",
    "content": "/*\n * base64.h\n * Header for base64 encoding module\n * Author: Michael Czigler\n * License: MIT\n */\n\n#ifndef __KIRC_BASE64_H\n#define __KIRC_BASE64_H\n\n#include \"kirc.h\"\n\nint base64_encode(char *out, const char *in, size_t in_len);\n\n#endif // __KIRC_BASE64_H\n"
  },
  {
    "path": "include/config.h",
    "content": "/*\n * config.h\n * Header for configuration module\n * Author: Michael Czigler\n * License: MIT\n */\n\n#ifndef __KIRC_CONFIG_H\n#define __KIRC_CONFIG_H\n\n#include \"kirc.h\"\n#include \"helper.h\"\n#include \"base64.h\"\n\nint config_init(struct kirc_context *ctx);\nint config_parse_args(struct kirc_context *ctx, int argc, char *argv[]);\nint config_free(struct kirc_context *ctx);\n\n#endif  // __KIRC_CONFIG_H\n"
  },
  {
    "path": "include/ctcp.h",
    "content": "/*\n * ctcp.h\n * Header for the CTCP module\n * Author: Michael Czigler\n * License: MIT\n */\n\n#ifndef __KIRC_CTCP_H\n#define __KIRC_CTCP_H\n\n#include \"kirc.h\"\n#include \"event.h\"\n#include \"network.h\"\n#include \"output.h\"\n\nvoid ctcp_handle_clientinfo(struct network *network, struct event *event, struct output *output);\nvoid ctcp_handle_ping(struct network *network, struct event *event, struct output *output);\nvoid ctcp_handle_time(struct network *network, struct event *event, struct output *output);\nvoid ctcp_handle_version(struct network *network, struct event *event, struct output *output);\n\n#endif  // __KIRC_CTCP_H\n"
  },
  {
    "path": "include/dcc.h",
    "content": "/*\n * dcc.h\n * Header for the DCC module\n * Author: Michael Czigler\n * License: MIT\n */\n\n#ifndef __KIRC_DCC_H\n#define __KIRC_DCC_H\n\n#include \"kirc.h\"\n#include \"ansi.h\"\n#include \"helper.h\"\n#include \"event.h\"\n#include \"network.h\"\n#include \"handler.h\"\n\nenum dcc_type {\n    DCC_TYPE_SEND = 0,\n    DCC_TYPE_RECEIVE\n};\n\nenum dcc_state {\n    DCC_STATE_IDLE = 0,\n    DCC_STATE_CONNECTING,\n    DCC_STATE_TRANSFERRING,\n    DCC_STATE_COMPLETE,\n    DCC_STATE_ERROR\n};\n\nstruct dcc_transfer {\n    enum dcc_type type;\n    enum dcc_state state;\n    char filename[NAME_MAX];\n    char sender[MESSAGE_MAX_LEN];\n    unsigned long long filesize;\n    unsigned long long sent;\n    int file_fd;\n};\n\nstruct dcc {\n    struct kirc_context *ctx;\n    struct pollfd sock_fd[KIRC_DCC_TRANSFERS_MAX];\n    struct dcc_transfer transfer[KIRC_DCC_TRANSFERS_MAX];\n    int transfer_count;\n};\n\nint dcc_init(struct dcc *dcc, struct kirc_context *ctx);\nint dcc_free(struct dcc *dcc);\nint dcc_request(struct dcc *dcc, const char *sender,\n        const char *params);\nint dcc_send(struct dcc *dcc, int transfer_id);\nint dcc_process(struct dcc *dcc);\nint dcc_cancel(struct dcc *dcc, int transfer_id);\nvoid dcc_handle(struct dcc *dcc, struct network *network,\n        struct event *event);\n\n#endif  // __KIRC_DCC_H\n"
  },
  {
    "path": "include/editor.h",
    "content": "/*\n * editor.h\n * Header for the editor module\n * Author: Michael Czigler\n * License: MIT\n */\n\n#ifndef __KIRC_EDITOR_H\n#define __KIRC_EDITOR_H\n\n#include \"kirc.h\"\n#include \"terminal.h\"\n#include \"ansi.h\"\n#include \"utf8.h\"\n\nenum editor_state {\n    EDITOR_STATE_NONE = 0,\n    EDITOR_STATE_SEND,\n    EDITOR_STATE_TERMINATE\n};\n\nstruct editor {\n    struct kirc_context *ctx;\n    enum editor_state state;\n    char scratch[MESSAGE_MAX_LEN];\n    char history[KIRC_HISTORY_SIZE][MESSAGE_MAX_LEN];\n    int head;\n    int count;\n    int position; /* -1 when not browsing history, otherwise index into history[] */\n    int cursor;\n};\n\nchar *editor_last_entry(struct editor *editor);\nint editor_init(struct editor *editor, struct kirc_context *ctx);\nint editor_process_key(struct editor *editor);\nint editor_handle(struct editor *editor);\n\n#endif  // __KIRC_EDITOR_H\n"
  },
  {
    "path": "include/event.h",
    "content": "/*\n * event.h\n * Header for the event module\n * Author: Michael Czigler\n * License: MIT\n */\n\n#ifndef __KIRC_EVENT_H\n#define __KIRC_EVENT_H\n\n#include \"kirc.h\"\n#include \"helper.h\"\n\nenum event_type {\n    EVENT_NONE = 0,\n    EVENT_CTCP_ACTION,\n    EVENT_CTCP_CLIENTINFO,\n    EVENT_CTCP_DCC,\n    EVENT_CTCP_PING,\n    EVENT_CTCP_TIME,\n    EVENT_CTCP_VERSION,\n    EVENT_ERROR,\n    EVENT_EXT_CAP,\n    EVENT_EXT_AUTHENTICATE,\n    EVENT_JOIN,\n    EVENT_KICK,\n    EVENT_MODE,\n    EVENT_NICK,\n    EVENT_NOTICE,\n    EVENT_PART,\n    EVENT_PING,\n    EVENT_PRIVMSG,\n    EVENT_QUIT,\n    EVENT_TOPIC,\n    EVENT_001_RPL_WELCOME,\n    EVENT_002_RPL_YOURHOST,\n    EVENT_003_RPL_CREATED,\n    EVENT_004_RPL_MYINFO,\n    EVENT_005_RPL_BOUNCE,\n    EVENT_042_RPL_YOURID,\n    EVENT_200_RPL_TRACELINK,\n    EVENT_201_RPL_TRACECONNECTING,\n    EVENT_202_RPL_TRACEHANDSHAKE,\n    EVENT_203_RPL_TRACEUNKNOWN,\n    EVENT_204_RPL_TRACEOPERATOR,\n    EVENT_205_RPL_TRACEUSER,\n    EVENT_206_RPL_TRACESERVER,\n    EVENT_207_RPL_TRACESERVICE,\n    EVENT_208_RPL_TRACENEWTYPE,\n    EVENT_209_RPL_TRACECLASS,\n    EVENT_211_RPL_STATSLINKINFO,\n    EVENT_212_RPL_STATSCOMMANDS,\n    EVENT_213_RPL_STATSCLINE,\n    EVENT_215_RPL_STATSILINE,\n    EVENT_216_RPL_STATSKLINE,\n    EVENT_218_RPL_STATSYLINE,\n    EVENT_219_RPL_ENDOFSTATS,\n    EVENT_221_RPL_UMODEIS,\n    EVENT_234_RPL_SERVLIST,\n    EVENT_235_RPL_SERVLISTEND,\n    EVENT_241_RPL_STATSLLINE,\n    EVENT_242_RPL_STATSUPTIME,\n    EVENT_243_RPL_STATSOLINE,\n    EVENT_244_RPL_STATSHLINE,\n    EVENT_245_RPL_STATSSLINE,\n    EVENT_250_RPL_STATSCONN,\n    EVENT_251_RPL_LUSERCLIENT,\n    EVENT_252_RPL_LUSEROP,\n    EVENT_253_RPL_LUSERUNKNOWN,\n    EVENT_254_RPL_LUSERCHANNELS,\n    EVENT_255_RPL_LUSERME,\n    EVENT_256_RPL_ADMINME,\n    EVENT_257_RPL_ADMINLOC1,\n    EVENT_258_RPL_ADMINLOC2,\n    EVENT_259_RPL_ADMINEMAIL,\n    EVENT_261_RPL_TRACELOG,\n    EVENT_263_RPL_TRYAGAIN,\n    EVENT_265_RPL_LOCALUSERS,\n    EVENT_266_RPL_GLOBALUSERS,\n    EVENT_300_RPL_NONE,\n    EVENT_301_RPL_AWAY,\n    EVENT_302_RPL_USERHOST,\n    EVENT_303_RPL_ISON,\n    EVENT_305_RPL_UNAWAY,\n    EVENT_306_RPL_NOWAWAY,\n    EVENT_311_RPL_WHOISUSER,\n    EVENT_312_RPL_WHOISSERVER,\n    EVENT_313_RPL_WHOISOPERATOR,\n    EVENT_314_RPL_WHOWASUSER,\n    EVENT_315_RPL_ENDOFWHO,\n    EVENT_317_RPL_WHOISIDLE,\n    EVENT_318_RPL_ENDOFWHOIS,\n    EVENT_319_RPL_WHOISCHANNELS,\n    EVENT_322_RPL_LIST,\n    EVENT_323_RPL_LISTEND,\n    EVENT_324_RPL_CHANNELMODEIS,\n    EVENT_328_RPL_CHANNEL_URL,\n    EVENT_331_RPL_NOTOPIC,\n    EVENT_332_RPL_TOPIC,\n    EVENT_333_RPL_TOPICWHOTIME,\n    EVENT_341_RPL_INVITING,\n    EVENT_346_RPL_INVITELIST,\n    EVENT_347_RPL_ENDOFINVITELIST,\n    EVENT_348_RPL_EXCEPTLIST,\n    EVENT_349_RPL_ENDOFEXCEPTLIST,\n    EVENT_351_RPL_VERSION,\n    EVENT_352_RPL_WHOREPLY,\n    EVENT_353_RPL_NAMREPLY,\n    EVENT_364_RPL_LINKS,\n    EVENT_365_RPL_ENDOFLINKS,\n    EVENT_366_RPL_ENDOFNAMES,\n    EVENT_367_RPL_BANLIST,\n    EVENT_368_RPL_ENDOFBANLIST,\n    EVENT_369_RPL_ENDOFWHOWAS,\n    EVENT_371_RPL_INFO,\n    EVENT_372_RPL_MOTD,\n    EVENT_374_RPL_ENDOFINFO,\n    EVENT_375_RPL_MOTDSTART,\n    EVENT_376_RPL_ENDOFMOTD,\n    EVENT_381_RPL_YOUREOPER,\n    EVENT_382_RPL_REHASHING,\n    EVENT_383_RPL_YOURESERVICE,\n    EVENT_391_RPL_TIME,\n    EVENT_392_RPL_USERSSTART,\n    EVENT_393_RPL_USERS,\n    EVENT_394_RPL_ENDOFUSERS,\n    EVENT_395_RPL_NOUSERS,\n    EVENT_396_RPL_HOSTHIDDEN,\n    EVENT_400_ERR_UNKNOWNERROR,\n    EVENT_401_ERR_NOSUCHNICK,\n    EVENT_402_ERR_NOSUCHSERVER,\n    EVENT_403_ERR_NOSUCHCHANNEL,\n    EVENT_404_ERR_CANNOTSENDTOCHAN,\n    EVENT_405_ERR_TOOMANYCHANNELS,\n    EVENT_406_ERR_WASNOSUCHNICK,\n    EVENT_407_ERR_TOOMANYTARGETS,\n    EVENT_408_ERR_NOSUCHSERVICE,\n    EVENT_409_ERR_NOORIGIN,\n    EVENT_411_ERR_NORECIPIENT,\n    EVENT_412_ERR_NOTEXTTOSEND,\n    EVENT_413_ERR_NOTOPLEVEL,\n    EVENT_414_ERR_WILDTOPLEVEL,\n    EVENT_415_ERR_BADMASK,\n    EVENT_421_ERR_UNKNOWNCOMMAND,\n    EVENT_422_ERR_NOMOTD,\n    EVENT_423_ERR_NOADMININFO,\n    EVENT_424_ERR_FILEERROR,\n    EVENT_431_ERR_NONICKNAMEGIVEN,\n    EVENT_432_ERR_ERRONEUSNICKNAME,\n    EVENT_433_ERR_NICKNAMEINUSE,\n    EVENT_436_ERR_NICKCOLLISION,\n    EVENT_441_ERR_USERNOTINCHANNEL,\n    EVENT_442_ERR_NOTONCHANNEL,\n    EVENT_443_ERR_USERONCHANNEL,\n    EVENT_444_ERR_NOLOGIN,\n    EVENT_445_ERR_SUMMONDISABLED,\n    EVENT_446_ERR_USERSDISABLED,\n    EVENT_451_ERR_NOTREGISTERED,\n    EVENT_461_ERR_NEEDMOREPARAMS,\n    EVENT_462_ERR_ALREADYREGISTERED,\n    EVENT_463_ERR_NOPERMFORHOST,\n    EVENT_464_ERR_PASSWDMISMATCH,\n    EVENT_465_ERR_YOUREBANNEDCREEP,\n    EVENT_467_ERR_KEYSET,\n    EVENT_470_ERR_LINKCHANNEL,\n    EVENT_471_ERR_CHANNELISFULL,\n    EVENT_472_ERR_UNKNOWNMODE,\n    EVENT_473_ERR_INVITEONLYCHAN,\n    EVENT_474_ERR_BANNEDFROMCHAN,\n    EVENT_475_ERR_BADCHANNELKEY,\n    EVENT_476_ERR_BADCHANMASK,\n    EVENT_477_ERR_NEEDREGGEDNICK,\n    EVENT_478_ERR_BANLISTFULL,\n    EVENT_481_ERR_NOPRIVILEGES,\n    EVENT_482_ERR_CHANOPRIVSNEEDED,\n    EVENT_483_ERR_CANTKILLSERVER,\n    EVENT_485_ERR_UNIQOPRIVSNEEDED,\n    EVENT_491_ERR_NOOPERHOST,\n    EVENT_501_ERR_UMODEUNKNOWNFLAG,\n    EVENT_502_ERR_USERSDONTMATCH,\n    EVENT_524_ERR_HELPNOTFOUND,\n    EVENT_704_RPL_HELPSTART,\n    EVENT_705_RPL_HELPTXT,\n    EVENT_706_RPL_ENDOFHELP,\n    EVENT_900_RPL_LOGGEDIN,\n    EVENT_901_RPL_LOGGEDOUT,\n    EVENT_902_ERR_NICKLOCKED,\n    EVENT_903_RPL_SASLSUCCESS,\n    EVENT_904_ERR_SASLFAIL,\n    EVENT_905_ERR_SASLTOOLONG,\n    EVENT_906_ERR_SASLABORTED,\n    EVENT_907_ERR_SASLALREADY,\n    EVENT_908_RPL_SASLMECHS\n};\n\nstruct event_dispatch_table {\n    const char *command;\n    enum event_type type;\n};\n\nstruct event {\n    struct kirc_context *ctx;\n    enum event_type type;\n    char raw[MESSAGE_MAX_LEN];\n    char channel[CHANNEL_MAX_LEN];\n    char message[MESSAGE_MAX_LEN];\n    char command[MESSAGE_MAX_LEN];\n    char nickname[MESSAGE_MAX_LEN];\n    char params[MESSAGE_MAX_LEN];\n};\n\nint event_init(struct event *event, struct kirc_context *ctx);\nint event_parse(struct event *event, char *line);\n\n#endif  // __KIRC_EVENT_H\n"
  },
  {
    "path": "include/handler.h",
    "content": "/*\n * handler.h\n * Header for handler dispatch module\n * Author: Michael Czigler\n * License: MIT\n */\n\n#ifndef __KIRC_HANDLER_H\n#define __KIRC_HANDLER_H\n\n#include \"kirc.h\"\n#include \"event.h\"\n#include \"network.h\"\n#include \"output.h\"\n\ntypedef void (*event_handler_fn)(struct network *network, struct event *event, struct output *output);\n\nstruct handler {\n    struct kirc_context *ctx;\n    event_handler_fn handlers[KIRC_EVENT_TYPE_MAX];\n    event_handler_fn default_handler;\n};\n\nvoid handler_default(struct handler *handler, event_handler_fn handler_fn);\nvoid handler_register(struct handler *handler, enum event_type type, event_handler_fn handler_fn);\nvoid handler_dispatch(struct handler *handler, struct network *network, struct event *event, struct output *output);\n\nint handler_init(struct handler *handler, struct kirc_context *ctx);\n\n#endif  // __KIRC_HANDLER_H\n"
  },
  {
    "path": "include/helper.h",
    "content": "/*\n * helper.h\n * Header for the helper module\n * Author: Michael Czigler\n * License: MIT\n */\n\n#ifndef __KIRC_HELPER_H\n#define __KIRC_HELPER_H\n\n#include \"kirc.h\"\n\nint safecpy(char *s1, const char *s2, size_t n);\nint memzero(void *s, size_t n);\n\nchar *find_message_end(const char *buffer, size_t len);\n\n#endif  // __KIRC_HELPER_H\n"
  },
  {
    "path": "include/kirc.h",
    "content": "/*\n * kirc.h\n * Main header file for kirc IRC client\n * Author: Michael Czigler\n * License: MIT\n */\n\n#ifndef __KIRC_H\n#define __KIRC_H\n\n#ifndef _XOPEN_SOURCE\n#define _XOPEN_SOURCE 700\n#endif\n\n#include <ctype.h>\n#include <errno.h>\n#include <fcntl.h>\n#include <limits.h>\n#include <locale.h>\n#include <netdb.h>\n#include <poll.h>\n#include <stdarg.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <sys/ioctl.h>\n#include <termios.h>\n#include <time.h>\n#include <unistd.h>\n#include <wchar.h>\n#include <wctype.h>\n\n#ifndef NAME_MAX\n#define NAME_MAX                 255\n#endif\n\n#ifndef HOST_NAME_MAX\n#define HOST_NAME_MAX            255\n#endif\n\n#define CHANNEL_MAX_LEN          200  /* per RFC1459 */\n#define MESSAGE_MAX_LEN          512  /* per RFC1459 */\n#define AUTH_CHUNK_SIZE          400  /* per IRCv3.1 */\n\n#define KIRC_VERSION_MAJOR       \"1\"\n#define KIRC_VERSION_MINOR       \"2\"\n#define KIRC_VERSION_PATCH       \"2\"\n\n#define KIRC_CHANNEL_LIMIT       256\n#define KIRC_DCC_BUFFER_SIZE     8192\n#define KIRC_DCC_TRANSFERS_MAX   16\n#define KIRC_EVENT_TYPE_MAX      256\n#define KIRC_HANDLER_MAX_ENTRIES 256\n#define KIRC_HISTORY_SIZE        64\n#define KIRC_OUTPUT_BUFFER_SIZE  8192\n#define KIRC_PORT_RANGE_MAX      65535\n#define KIRC_TAB_WIDTH           4\n#define KIRC_TIMEOUT_MS          5000\n#define KIRC_TIMESTAMP_SIZE      6\n#define KIRC_TIMESTAMP_FORMAT    \"%H:%M\"\n\n#define KIRC_DEFAULT_COLUMNS     80\n#define KIRC_DEFAULT_PORT        \"6667\"\n#define KIRC_DEFAULT_SERVER      \"irc.libera.chat\"\n\nenum sasl_mechanism {\n    SASL_NONE = 0,\n    SASL_PLAIN,\n    SASL_EXTERNAL\n};\n\nstruct kirc_context {\n    char server[HOST_NAME_MAX];\n    char port[6];\n    char nickname[MESSAGE_MAX_LEN];\n    char realname[MESSAGE_MAX_LEN];\n    char username[MESSAGE_MAX_LEN];\n    char password[MESSAGE_MAX_LEN];\n    char channels[KIRC_CHANNEL_LIMIT][CHANNEL_MAX_LEN];\n    char target[KIRC_CHANNEL_LIMIT];\n    char auth[MESSAGE_MAX_LEN];\n    enum sasl_mechanism mechanism;\n};\n\n#endif  // __KIRC_H\n"
  },
  {
    "path": "include/network.h",
    "content": "/*\n * network.h\n * Header for the network module\n * Author: Michael Czigler\n * License: MIT\n */\n\n#ifndef __KIRC_NETWORK_H\n#define __KIRC_NETWORK_H\n\n#include \"kirc.h\"\n#include \"transport.h\"\n#include \"ansi.h\"\n#include \"helper.h\"\n#include \"output.h\"\n\nstruct network {\n    struct kirc_context *ctx;\n    struct transport *transport;\n    char buffer[MESSAGE_MAX_LEN];\n    int len;\n};\n\nint network_send(struct network *network, const char *fmt, ...);\nint network_receive(struct network *network);\nint network_connect(struct network *network);\nint network_command_handler(struct network *network, char *msg, struct output *output);\nint network_send_credentials(struct network *network);\n\nint network_init(struct network *network,\n        struct transport *transport, struct kirc_context *ctx);\nint network_free(struct network *network);\n\n#endif  // __KIRC_NETWORK_H\n"
  },
  {
    "path": "include/output.h",
    "content": "/*\n * output.h\n * Header for the buffered output module\n * Author: Michael Czigler\n * License: MIT\n */\n\n#ifndef __KIRC_OUTPUT_H\n#define __KIRC_OUTPUT_H\n\n#include \"kirc.h\"\n#include \"ansi.h\"\n\nstruct output {\n    struct kirc_context *ctx;\n    char buffer[KIRC_OUTPUT_BUFFER_SIZE];\n    int len;\n};\n\nint output_init(struct output *output,\n        struct kirc_context *ctx);\n\nint output_append(struct output *output,\n        const char *fmt, ...);\n\nvoid output_flush(struct output *output);\nvoid output_clear(struct output *output);\n\nint output_pending(struct output *output);\n\n#endif  // __KIRC_OUTPUT_H\n"
  },
  {
    "path": "include/protocol.h",
    "content": "/*\n * protocol.h\n * Header for the protocol module\n * Author: Michael Czigler\n * License: MIT\n */\n\n#ifndef __KIRC_PROTOCOL_H\n#define __KIRC_PROTOCOL_H\n\n#include \"kirc.h\"\n#include \"ansi.h\"\n#include \"event.h\"\n#include \"helper.h\"\n#include \"network.h\"\n#include \"output.h\"\n\nvoid protocol_noop(struct network *network, struct event *event, struct output *output);\nvoid protocol_ping(struct network *network, struct event *event, struct output *output);\nvoid protocol_authenticate(struct network *network, struct event *event, struct output *output);\nvoid protocol_welcome(struct network *network, struct event *event, struct output *output);\nvoid protocol_raw(struct network *network, struct event *event, struct output *output);\nvoid protocol_info(struct network *network, struct event *event, struct output *output);\nvoid protocol_error(struct network *network, struct event *event, struct output *output);\nvoid protocol_notice(struct network *network, struct event *event, struct output *output);\nvoid protocol_privmsg(struct network *network, struct event *event, struct output *output);\nvoid protocol_nick(struct network *network, struct event *event, struct output *output);\nvoid protocol_join(struct network *network, struct event *event, struct output *output);\nvoid protocol_part(struct network *network, struct event *event, struct output *output);\nvoid protocol_ctcp_action(struct network *network, struct event *event, struct output *output);\nvoid protocol_ctcp_info(struct network *network, struct event *event, struct output *output);\n\n#endif  // __KIRC_PROTOCOL_H\n"
  },
  {
    "path": "include/terminal.h",
    "content": "/*\n * terminal.h\n * Header for the terminal module\n * Author: Michael Czigler\n * License: MIT\n */\n\n#ifndef __KIRC_TERMINAL_H\n#define __KIRC_TERMINAL_H\n\n#include \"kirc.h\"\n#include \"ansi.h\"\n\nstruct terminal {\n    struct kirc_context *ctx;\n    struct termios original;\n    int raw_mode_enabled;\n};\n\nint terminal_columns(int tty_fd);\nint terminal_init(struct terminal *terminal,\n        struct kirc_context *ctx);\nint terminal_enable_raw(struct terminal *terminal);\nvoid terminal_disable_raw(struct terminal *terminal);\n\n#endif  // __KIRC_TERMINAL_H\n"
  },
  {
    "path": "include/transport.h",
    "content": "/*\n * transport.h\n * Header for the transport module\n * Author: Michael Czigler\n * License: MIT\n */\n\n#ifndef __KIRC_TRANSPORT_H\n#define __KIRC_TRANSPORT_H\n\n#include \"kirc.h\"\n\nstruct transport {\n    struct kirc_context *ctx;\n    int fd;\n};\n\nssize_t transport_send(struct transport *transport,\n        const char *buffer, size_t len);\nssize_t transport_receive(struct transport *transport,\n        char *buffer, size_t len);\n\nint transport_connect(struct transport *transport);\nint transport_init(struct transport *transport,\n        struct kirc_context *ctx);\nint transport_free(struct transport *transport);\n\n#endif  // __KIRC_TRANSPORT_H\n"
  },
  {
    "path": "include/utf8.h",
    "content": "/*\n * utf8.h\n * Header for the UTF-8 module\n * Author: Michael Czigler\n * License: MIT\n */\n\n#ifndef __KIRC_UTF8_H\n#define __KIRC_UTF8_H\n\n#include \"kirc.h\"\n\nint utf8_prev_char_len(const char *s, int pos);\nint utf8_next_char_len(const char *s, int pos, int maxlen);\nint utf8_validate(const char *s, int len);\n\n#endif  // __KIRC_UTF8_H\n"
  },
  {
    "path": "kirc.1",
    "content": ".TH KIRC 1 \"December 2025\"\n.SH NAME\nkirc \\- a tiny, dependency-light IRC client\n.SH SYNOPSIS\n.B kirc\n.RB [\\-s \" server\"]\n.RB [\\-p \" port\"]\n.RB [\\-c \" channel(s)\"]\n.RB [\\-r \" realname\"]\n.RB [\\-u \" username\"]\n.RB [\\-k \" password\"]\n.RB [\\-a \" auth\"]\n.RB <nickname>\n.SH DESCRIPTION\n.B kirc\nis an extremely fast and simple Internet Relay Chat (IRC) client designed with\nportability and minimalism in mind, following the \"Keep It Simple, Stupid\" (KISS)\nphilosophy. It provides a keyboard-driven interface for real-time IRC communication\nand is optimized to run on systems with minimal dependencies, making it suitable for\nembedded systems, lightweight containers, and resource-constrained environments.\n.PP\nThe primary design principle of\n.B kirc\nis to maintain a small codebase while providing essential IRC functionality. The\nclient follows the Unix philosophy of doing one thing well: it reads from STDIN\nand prints to STDOUT, allowing all traffic to be easily multiplexed and processed\nthrough external commands and shell pipelines. This design enables powerful text\nfiltering, logging, and integration with other Unix tools.\n.PP\nAll highlighted text and color can be controlled with ANSI escape sequences,\nproviding full control over output styling and appearance. The client is compatible\nwith both traditional and modern terminal emulators.\n.PP\nFor best results and optimal rendering with full color support, use a terminal\nemulator with 256 color support, such as xterm-256color, GNOME Terminal, Konsole,\nor iTerm2. Most modern terminal emulators will provide a pleasant experience with\n.B kirc\nwith default settings.\n.SH OPTIONS\nThe following options are available. These can also be set using environment\nvariables (see ENVIRONMENT section), which will be overridden by any command-line\narguments. The nickname argument is always required.\n.TP\n.BI \\-s \" server\"\nSpecifies the IRC server hostname or IP address to connect to. The server name is\npassed directly to the network layer without validation, so ensure it is correct\nbefore connecting.\n.br\nDefault: irc.libera.chat\n.TP\n.BI \\-p \" port\"\nSpecifies the TCP port number used to connect to the IRC server. Standard IRC\nports are 6667 (plaintext) and 6697 (with TLS/SSL). Ensure your firewall permits\noutbound connections on the specified port.\n.br\nDefault: 6667\n.TP\n.BI \\-c \" channel(s)\"\nSpecifies the channel(s) to JOIN upon successful connection. Multiple channels can\nbe specified as a comma-separated or pipe-separated list without spaces\n(e.g., \"#channel1,#channel2\"). The first channel in the list will be set as the\ndefault target for messages sent without an explicit destination. Channel names\nmust conform to IRC specifications (typically starting with # or &).\n.TP\n.BI \\-r \" realname\"\nSpecifies the user's real name as sent during the IRC connection handshake. This\nvalue is visible in WHOIS queries and other user information lookups performed by\nother users. If not specified, the client defaults to using the nickname. This can\nbe any string up to 63 characters.\n.TP\n.BI \\-u \" username\"\nSpecifies the USER connection username, which becomes part of your user's hostmask\n(e.g., user@host). This is displayed in various IRC commands and messages. If not\nspecified, the client defaults to using the nickname. This should typically be\nalphanumeric characters without spaces.\n.TP\n.BI \\-k \" password\"\nSpecifies the plain text PASS connection value, typically used for authentication\nwith the server or services. This is sent during the initial connection phase.\nBe cautious with this option as the password is visible in the command history and\nprocess list. For sensitive configurations, consider using the KIRC_PASSWORD\nenvironment variable instead.\n.TP\n.BI \\-a \" auth\"\nSpecifies SASL (Simple Authentication and Security Layer) authentication mechanism\nand token value to be used for connection authentication. The argument value is\ncomprised of a colon-delimited string, where the first token is the authentication\nmechanism (e.g., \"PLAIN\", \"EXTERNAL\", \"SCRAM-SHA-256\", etc.) and the second string\nis the mechanism-specific authentication credential (typically BASE64 encoded).\nFor the PLAIN mechanism, you can provide either a pre-encoded BASE64 token or three\ncolon-separated values (authzid:authcid:passwd) which will be automatically encoded.\n.br\nExample: \"PLAIN:amlsbGVzAGppbGxlcwBzZXNhbWU=\" or \"PLAIN:alice:alice:password\"\n.SH EXIT STATUS\n.TP\n.B 0\nSuccessful program execution and normal client termination.\n.TP\n.B 1\nUsage error, syntax error, connection failure, or other operational error. Check\nthe terminal output for specific error messages to diagnose the issue.\n.SH ENVIRONMENT\nThe following environment variables can be used to set defaults for connection\nparameters. All environment variables will be overridden by any corresponding\ncommand-line options. This is useful for creating shell aliases or scripts that\nestablish connections with specific parameters.\n.TP\n.B KIRC_SERVER\nDefault IRC server hostname or IP address. Equivalent to the\n.BI \\-s\noption.\n.TP\n.B KIRC_PORT\nDefault server port number. Equivalent to the\n.BI \\-p\noption.\n.TP\n.B KIRC_CHANNELS\nDefault channels to join upon connection (comma or pipe separated). Equivalent to\nthe\n.BI \\-c\noption.\n.TP\n.B KIRC_REALNAME\nDefault real name to use in WHOIS and other user information. Equivalent to the\n.BI \\-r\noption.\n.TP\n.B KIRC_USERNAME\nDefault username for the connection. Equivalent to the\n.BI \\-u\noption.\n.TP\n.B KIRC_PASSWORD\nDefault password for connection authentication. Equivalent to the\n.BI \\-k\noption. Be cautious storing sensitive information in environment variables.\n.TP\n.B KIRC_AUTH\nDefault SASL authentication token and mechanism. Equivalent to the\n.BI \\-a\noption.\n.SH COMMANDS\nOnce connected to an IRC server,\n.B kirc\nsupports the following commands. Commands are entered in the input line and\nexecuted by pressing ENTER. Text entered without a command prefix is treated as\na message to the current target.\n.TP\n.BI /<command>\nSend a raw IRC command to the server. This allows direct access to all IRC\nprotocol commands. Examples include /JOIN, /PART, /WHOIS, /NICK, /KICK, /BAN,\n/TOPIC, /MODE, /INVITE, and many others. Refer to IRC protocol documentation\n(RFC 1459) for a complete list of supported commands. The client does not\nvalidate or pre-process raw commands, so ensure correct syntax before sending.\n.TP\n.BI /set \" <target>\"\nSet the default message channel or nickname to\n.I <target>.\nAll subsequent messages entered without an explicit target will be sent to this\nchannel or user. The current target is typically displayed in the status line or\nprompt area.\n.TP\n.BI /topic \" <channel> [<topic>]\"\nQuery or set the topic for the specified\n.I <channel>.\nWhen invoked with only a channel name, the command requests and displays the\ncurrent channel topic. When a\n.I <topic>\nstring is provided, it sets the channel topic to the specified text (subject to\nchannel mode restrictions and operator privileges).\n.TP\n.BI <message>\nSend a PRIVMSG to the currently set target (channel or nickname) with\n.I <message>\nas the message content. This is the primary way to communicate in IRC. Messages\nare displayed to all users in the target channel or privately to the target user.\n.TP\n.BI @<target> \" <message>\"\nSend a PRIVMSG to a specified\n.I <target>\n(channel or nickname) with\n.I <message>\nas the content, without changing the default target. This allows sending one-off\nmessages to other destinations while maintaining your primary conversation context.\n.TP\n.BI /me \" <action>\"\nSend a CTCP ACTION command to the currently set target. This displays your message\nas an action (e.g., \"* alice does something\") to all users in the target channel\nor to the target user. Commonly used for expressing actions or emotions.\n.TP\n.BI /ctcp \" <target> <command> [<arguments>...]\"\nSend a raw Client-to-Client Protocol (CTCP) command to the specified\n.I <target>\nnickname or channel. Common CTCP commands include \"CLIENTINFO\", \"TIME\", \"VERSION\",\n\"PING\", and \"DCC\". Arguments are separated by spaces. CTCP commands are typically\nused for client discovery and file transfers (see DCC FILE TRANSFERS section).\n.SH KEY BINDINGS\n.B kirc\nprovides standard readline-style key bindings for line editing and command history\nnavigation. The following key combinations are supported:\n.TP\n.B CTRL+C\nForce quit\n.B kirc\nimmediately, disconnecting from the server without sending a QUIT command. This\nis useful for emergency termination or when the normal exit mechanism is unresponsive.\n.TP\n.B CTRL+U\nDelete the entire current input line, clearing all typed text and returning to an\nempty prompt.\n.TP\n.B Arrow Left (or CTRL+B)\nMove cursor one character to the left.\n.TP\n.B Arrow Right (or CTRL+F)\nMove cursor one character to the right.\n.TP\n.B Arrow Up (or CTRL+P)\nNavigate backwards through command history, displaying the previous command entered.\nPressing multiple times cycles through older commands.\n.TP\n.B Arrow Down (or CTRL+N)\nNavigate forwards through command history, displaying the next (more recent) command\nentered. Pressing at the end of history returns to the empty prompt.\n.TP\n.B Home (or CTRL+A)\nMove cursor to the beginning of the current input line.\n.TP\n.B End (or CTRL+E)\nMove cursor to the end of the current input line.\n.TP\n.B Delete\nDelete the character at the current cursor position.\n.TP\n.B Backspace (or CTRL+H)\nDelete the character immediately before the cursor position and move the cursor\nback one character.\n.SH EXAMPLES\n.SS Basic usage\n.TP\n.B kirc alice\nConnect with nickname 'alice' to the default Libera.Chat server (irc.libera.chat:6667).\nThe client will use 'alice' as the username and real name if not otherwise specified.\n.SS Advanced connection parameters\n.TP\n.B kirc \\-s irc.example.org \\-p 6667 \\-c \"#foo,#bar\" alice\nConnect to a custom IRC server (irc.example.org) on port 6667 and automatically join\nthe channels #foo and #bar upon connection. The #foo channel becomes the default\nmessage target.\n.SS Environment variables\n.TP\n.B KIRC_SERVER=irc.libera.chat KIRC_PORT=6697 kirc alice\nUse environment variables to set the server and port for this session. These values\ncan be permanently set in shell profile files for convenience.\n.SS Logging\n.TP\n.B kirc alice | tee -a kirc-$(date +%Y%m%d).log\nConnect and simultaneously save all output to a log file named with the current date\n(e.g., kirc-20251229.log). The\n.B tee\ncommand duplicates output while the\n.B \\\\-a\nflag appends to the file rather than overwriting it.\n.SS SASL authentication\n.TP\n.B kirc \\-a \"PLAIN:alice:alice:password\" alice\nConnect using SASL PLAIN authentication with automatic token encoding. The client\nwill automatically encode the authzid:authcid:passwd values into the proper BASE64\nformat required by the SASL PLAIN mechanism.\n.TP\n.B kirc \\-a \\\\[dq]PLAIN:$(printf \\[aq]%s\\0%s\\0%s\\[aq] \\[aq]alice\\[aq] \\[aq]alice\\[aq] \\[aq]password\\[aq] | base64)\\\\[dq] alice\nAlternatively, generate a BASE64-encoded SASL PLAIN authentication token manually\nand connect using SASL authentication. The token format is \"username\\0username\\0password\"\n(with NULL bytes represented as \\\\0) encoded in BASE64. This demonstrates manual token\ngeneration for the SASL PLAIN mechanism.\n.SS TLS/SSL connections via socat\n.TP\n.B socat tcp-listen:6667,reuseaddr,fork,bind=127.0.0.1 ssl:irc.example.org:6697 & kirc \\-s 127.0.0.1 alice\nEstablish a TLS/SSL encrypted connection to irc.example.org by proxying through socat.\nSocat listens on local port 6667 and forwards all traffic through an encrypted channel\nto the remote server's port 6697. This approach is useful when\n.B kirc\ndoesn't have native TLS support or when additional connection filtering is needed.\n.SS Proxy connections via socat\n.TP\n.B socat tcp-listen:6667,fork,reuseaddr,bind=127.0.0.1 proxy:<proxyurl>:irc.example.org:6667,proxyport=<proxyport> & kirc \\-s 127.0.0.1 \\-p 6667 alice\nConnect to irc.example.org through an HTTP/HTTPS proxy server using socat as an\nintermediary. Replace <proxyurl> and <proxyport> with your actual proxy server\ndetails. This technique allows IRC access from networks with strict outbound\nconnection policies.\n.SH DCC FILE TRANSFERS\n.SS Overview\n.B kirc\nsupports Direct Client-to-Client (DCC) file transfers, including XDCC file requests\ncommonly used on IRC file servers. DCC transfers occur directly between IRC clients\nwithout passing through the IRC server, making them independent of server bandwidth\nlimitations.\n.SS Receiving files\nWhen another IRC user sends you a file via DCC SEND, \n.B kirc\nautomatically detects the incoming file transfer request and begins downloading the\nfile to the current working directory. The client will display a status message:\n.PP\n.RS\n.nf\ndcc: receiving <filename> from <sender> (<size> bytes)\n.fi\n.RE\n.PP\nTransfer progress and completion messages will appear as the file downloads.\nThe file is saved with its original filename. If a file with the same name exists,\nthe behavior depends on the client configuration. Files are saved relative to the\ncurrent working directory from which\n.B kirc\nwas launched.\n.SS XDCC requests\nTo request a file from an XDCC bot, use the standard XDCC request format:\n.PP\n.RS\n.nf\n/ctcp <botname> XDCC SEND #<packet_number>\n.fi\n.RE\n.PP\nThe bot will initiate a DCC SEND transfer. \n.B kirc\nautomatically handles the incoming transfer and begins downloading the requested file.\nMultiple XDCC requests can be queued with a single bot, and the bot will process them\nsequentially.\n.SS Limitations\n.B kirc\nsupports up to 16 simultaneous DCC transfers. Exceeding this limit will reject\nadditional transfer requests. Files are always saved to the current working directory\nwith their original filenames. Ensure adequate disk space is available before initiating\nlarge file transfers.\n.SH SEE ALSO\n.BR irc (1),\n.BR socat (1)\n.SH STANDARDS\n.B kirc\nimplements the Internet Relay Chat protocol as defined in RFC 1459, RFC 2222 (SASL),\nand IRCv3.1 specifications. The client supports CTCP (Client-to-Client Protocol)\nextensions for client-to-client communication and file transfers.\n.SH BUGS\nhttps://github.com/mcpcpc/kirc/issues"
  },
  {
    "path": "src/base64.c",
    "content": "/*\n * base64.c\n * Base64 encoding handlers\n * Author: Michael Czigler\n * License: MIT\n */\n\n#include \"base64.h\"\n\n/**\n * base64_encode() - Encode data to base64 format\n * @out: Output buffer to store the base64-encoded string\n * @in: Input data buffer to encode\n * @in_len: Length of the input data in bytes\n *\n * Encodes binary data into base64 ASCII representation. The output buffer\n * must be large enough to hold the encoded data (use base64_encoded_size()\n * to determine the required size). Uses the standard base64 alphabet with\n * '+' and '/' characters.\n *\n * Return: 0 on success, -1 if input/output is NULL or input length is 0\n */\nint base64_encode(char *out, const char *in, size_t in_len)\n{\n    size_t i, j, v;\n    char base64_table[] = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n        \"abcdefghijklmnopqrstuvwxyz0123456789+/\";\n\n    if (in == NULL || in_len == 0 || out == NULL) {\n        return -1;\n    }\n\n    for (i = 0, j = 0; i < in_len; i += 3, j += 4) {\n        v = in[i];\n        v = (i + 1) < in_len ? v << 8 | in[i + 1] : v << 8;\n        v = (i + 2) < in_len ? v << 8 | in[i + 2] : v << 8;\n\n        out[j] = base64_table[(v >> 18) & 0x3F];\n        out[j + 1] = base64_table[(v >> 12) & 0x3F];\n\n        if ((i + 1) < in_len) {\n            out[j + 2] = base64_table[(v >> 6) & 0x3F];\n        } else {\n            out[j + 2] = '=';\n        }\n\n        if ((i + 2) < in_len) {\n            out[j + 3] = base64_table[v & 0x3F];\n        } else {\n            out[j + 3] = '=';\n        }\n    }\n\n    return 0;\n}\n"
  },
  {
    "path": "src/config.c",
    "content": "/*\n * config.c\n * Configuration parsing and initialization\n * Author: Michael Czigler\n * License: MIT\n */\n\n#include \"config.h\"\n\n/**\n * config_validate_port() - Validate a port number string\n * @value: String representation of the port number\n *\n * Validates that the given string is a valid port number. Checks for\n * non-numeric characters, range limits, and conversion errors.\n *\n * Return: 0 if valid port, -1 if invalid or out of range\n */\nstatic int config_validate_port(const char *value)\n{\n    if ((value == NULL) || (*value == '\\0')) {\n        return -1;\n    }\n\n    /* Check for invalid chars before conversion */\n    for (const char *p = value; *p != '\\0'; ++p) {\n        if ((*p < '0') || (*p > '9')) {\n            return -1;\n        }\n    }\n\n    errno = 0;\n    char *endptr;\n\n    long port = strtol(value, &endptr, 10);\n\n    if ((endptr == value) || (*endptr != '\\0')) {\n        return -1;\n    }\n\n    if (errno == ERANGE) {\n        return -1;\n    }\n\n    if ((port > KIRC_PORT_RANGE_MAX) || (port < 0)) {\n        return -1;\n    }\n\n    return 0;\n}\n\n/**\n * config_parse_channels() - Parse comma or pipe-separated channel list\n * @ctx: IRC context structure to store parsed channels\n * @value: String containing channel names separated by ',' or '|'\n *\n * Parses a string of channel names and stores them in the context structure.\n * Supports both comma and pipe delimiters. Limited to KIRC_CHANNEL_LIMIT\n * channels.\n */\nstatic void config_parse_channels(struct kirc_context *ctx, char *value)\n{\n    char *tok = NULL;\n    size_t idx = 0;\n\n    for (tok = strtok(value, \",|\"); tok != NULL && idx < KIRC_CHANNEL_LIMIT; \n        tok = strtok(NULL, \",|\")) {\n        safecpy(ctx->channels[idx], tok, sizeof(ctx->channels[idx]));\n        idx++;\n    }\n}\n\n/**\n * config_parse_mechanism() - Parse SASL authentication mechanism\n * @ctx: IRC context structure to store authentication settings\n * @value: String containing mechanism and credentials\n *\n * Parses SASL authentication mechanism (EXTERNAL or PLAIN) with optional\n * credentials. For PLAIN, expects format \"PLAIN:authzid:authcid:password\"\n * and base64-encodes the credentials. For EXTERNAL, no additional data needed.\n */\nstatic void config_parse_mechanism(struct kirc_context *ctx, char *value)\n{\n    char *mechanism = strtok(value, \":\");\n\n    if (mechanism == NULL) {\n        return;\n    }\n\n    if (strcmp(mechanism, \"EXTERNAL\") == 0) {\n        ctx->mechanism = SASL_EXTERNAL;\n        return;\n    } else if (strcmp(mechanism, \"PLAIN\") == 0) {\n        ctx->mechanism = SASL_PLAIN;\n    } else {\n        return;  /* invalid mechanism */\n    }\n\n    char *token = strtok(NULL, \"\");\n\n    if (token == NULL) {\n        return;  /* invalid token */\n    }\n\n    int count = 0;\n    \n    for (int i = 0; token[i] != '\\0'; ++i) {\n        if (token[i] == ':') count++;\n    }\n\n    if (count == 2) {\n        size_t plain_len = 0;\n        char plain[MESSAGE_MAX_LEN];\n        char *p = plain;\n\n        char *authzid = strtok(token, \":\");\n        \n        if (authzid == NULL) {\n            return;\n        }\n\n        size_t authzid_len = strnlen(authzid, 255);\n        memcpy(p, authzid, authzid_len);\n        p += authzid_len;\n        *p++ = '\\0';\n        plain_len += authzid_len + 1;\n\n        char *authcid = strtok(NULL, \":\");\n\n        if (authcid == NULL) {\n            return;\n        }\n\n        size_t authcid_len = strnlen(authcid, 255);\n        memcpy(p, authcid, authcid_len);\n        p += authcid_len;\n        *p++ = '\\0';\n        plain_len += authcid_len + 1;\n\n        char *passwd = strtok(NULL, \"\");\n\n        if (passwd == NULL) {\n            return;\n        }\n\n        size_t passwd_len = strnlen(passwd, 255);\n        memcpy(p, passwd, passwd_len);\n        plain_len += passwd_len;\n\n        base64_encode(ctx->auth, plain, plain_len);\n        memzero(plain, sizeof(plain));\n    } else {\n        safecpy(ctx->auth, token, sizeof(ctx->auth));\n    }\n}\n\n/**\n * config_apply_env() - Apply environment variable to configuration\n * @ctx: IRC context structure (unused)\n * @env_name: Name of the environment variable to read\n * @dest: Destination buffer for the value\n * @dest_size: Size of the destination buffer\n *\n * Reads an environment variable and copies its value to the destination\n * buffer if the variable exists and is non-empty.\n *\n * Return: 0 on success or if variable doesn't exist, error code from safecpy\n */\nstatic int config_apply_env(struct kirc_context *ctx, const char *env_name, \n        char *dest, size_t dest_size)\n{\n    char *env = getenv(env_name);\n    if (env && *env) {\n        return safecpy(dest, env, dest_size);\n    }\n    return 0;\n}\n\n/**\n * config_init() - Initialize configuration with defaults and environment\n * @ctx: IRC context structure to initialize\n *\n * Initializes the configuration context with default values and applies\n * settings from environment variables (KIRC_SERVER, KIRC_PORT, KIRC_CHANNELS,\n * KIRC_REALNAME, KIRC_USERNAME, KIRC_PASSWORD, KIRC_AUTH). Validates port\n * numbers and parses authentication mechanisms.\n *\n * Return: 0 on success, -1 if port validation fails\n */\nint config_init(struct kirc_context *ctx)\n{\n    memset(ctx, 0, sizeof(*ctx));\n\n    safecpy(ctx->server, KIRC_DEFAULT_SERVER,\n        sizeof(ctx->server));\n    \n    safecpy(ctx->port, KIRC_DEFAULT_PORT,\n        sizeof(ctx->port));\n\n    ctx->mechanism = SASL_NONE;\n\n    config_apply_env(ctx, \"KIRC_SERVER\", ctx->server, sizeof(ctx->server));\n\n    char *env_port = getenv(\"KIRC_PORT\");\n    if (env_port && *env_port) {\n        if (config_validate_port(env_port) < 0) {\n            fprintf(stderr, \"invalid port number in KIRC_PORT\\n\");\n            return -1;\n        }\n\n        safecpy(ctx->port, env_port, sizeof(ctx->port));\n    }\n\n    char *env_channels = getenv(\"KIRC_CHANNELS\");\n\n    if (env_channels && *env_channels) {\n        config_parse_channels(ctx, env_channels);\n    }\n\n    config_apply_env(ctx, \"KIRC_REALNAME\", ctx->realname,\n        sizeof(ctx->realname));\n\n    config_apply_env(ctx, \"KIRC_USERNAME\", ctx->username,\n        sizeof(ctx->username));\n\n    config_apply_env(ctx, \"KIRC_PASSWORD\", ctx->password,\n        sizeof(ctx->password));\n\n    char *env_auth = getenv(\"KIRC_AUTH\");\n    if (env_auth && *env_auth) {\n        config_parse_mechanism(ctx, env_auth);\n    }\n\n    return 0;\n}\n\n/**\n * config_parse_args() - Parse command-line arguments\n * @ctx: IRC context structure to populate with parsed values\n * @argc: Argument count\n * @argv: Argument vector\n *\n * Parses command-line options using getopt. Supports:\n *   -s server, -p port, -r realname, -u username, -k password,\n *   -c channels, -a auth_mechanism\n * The nickname is required as a positional argument.\n *\n * Return: 0 on success, -1 on error or invalid arguments\n */\nint config_parse_args(struct kirc_context *ctx, int argc, char *argv[])\n{\n    if (argc < 2) {\n        fprintf(stderr, \"%s: no arguments\\n\", argv[0]);\n        return -1;\n    }\n\n    int opt;\n\n    while ((opt = getopt(argc, argv, \"s:p:r:u:k:c:a:\")) > 0) {\n        switch (opt) {\n        case 's':  /* server */\n            safecpy(ctx->server, optarg, sizeof(ctx->server));\n            break;\n\n        case 'p':  /* port */\n            if (config_validate_port(optarg) < 0) {\n                fprintf(stderr, \"invalid port number\\n\");\n                return -1;\n            }\n            safecpy(ctx->port, optarg, sizeof(ctx->port));\n            break;\n\n        case 'r':  /* realname */\n            safecpy(ctx->realname, optarg, sizeof(ctx->realname));\n            break;\n\n        case 'u':  /* username */\n            safecpy(ctx->username, optarg, sizeof(ctx->username));\n            break;\n\n        case 'k':  /* password */\n            safecpy(ctx->password, optarg, sizeof(ctx->password));\n            break;\n\n        case 'c':  /* channel(s) */\n            config_parse_channels(ctx, optarg);\n            break;\n\n        case 'a':  /* SASL authentication */\n            config_parse_mechanism(ctx, optarg);\n            break;\n\n        case ':':\n            fprintf(stderr, \"%s: missing -%c value\\n\", argv[0], opt);\n            return -1;\n\n        case '?':\n            fprintf(stderr, \"%s: unknown argument\\n\", argv[0]);\n            return -1;\n\n        default:\n            return -1;\n        }\n    }\n\n    if (optind >= argc) {\n        fprintf(stderr, \"nickname not specified\\n\");\n        return -1;\n    }\n\n    size_t nickname_n = sizeof(ctx->nickname);\n    safecpy(ctx->nickname, argv[optind], nickname_n);\n\n    return 0;\n}\n\n/**\n * config_free() - Securely clear sensitive configuration data\n * @ctx: IRC context structure to clean up\n *\n * Securely zeroes out sensitive fields (auth token and password) from the\n * configuration context to prevent them from remaining in memory.\n *\n * Return: 0 on success, -1 if secure clearing fails\n */\nint config_free(struct kirc_context *ctx)\n{\n    if (memzero(ctx->auth, sizeof(ctx->auth)) < 0) {\n        fprintf(stderr, \"auth token value not safely cleared\\n\");\n        return -1;\n    }\n\n    if (memzero(ctx->password, sizeof(ctx->password)) < 0) {\n        fprintf(stderr, \"password value not safely cleared\\n\");\n        return -1;\n    }\n\n    return 0;\n}\n"
  },
  {
    "path": "src/ctcp.c",
    "content": "/*\n * ctcp.c\n * Client-to-client protocol (CTCP) event handling\n * Author: Michael Czigler\n * License: MIT\n */\n\n#include \"ctcp.h\"\n\n/**\n * ctcp_handle_clientinfo() - Handle CTCP CLIENTINFO request\n * @network: Network connection structure\n * @event: Event structure containing request details\n * @output: Output buffer structure (unused)\n *\n * Responds to CTCP CLIENTINFO queries with a list of supported CTCP\n * commands: PING, ACTION, CLIENTINFO, DCC, PING, TIME, VERSION.\n * Only responds to PRIVMSG, not NOTICE.\n */\nvoid ctcp_handle_clientinfo(struct network *network, struct event *event, struct output *output)\n{\n    (void)output;\n    const char *command = event->command;\n    const char *nickname = event->nickname;\n    \n    if (strcmp(command, \"PRIVMSG\") == 0) {\n        network_send(network,\n            \"NOTICE %s :\\001PING ACTION CLIENTINFO DCC PING TIME VERSION\\001\\r\\n\",\n            nickname);\n    }\n}\n\n/**\n * ctcp_handle_ping() - Handle CTCP PING request\n * @network: Network connection structure\n * @event: Event structure containing ping data\n * @output: Output buffer structure (unused)\n *\n * Responds to CTCP PING queries by echoing back any provided arguments.\n * Only responds to PRIVMSG, not NOTICE.\n */\nvoid ctcp_handle_ping(struct network *network, struct event *event, struct output *output)\n{\n    (void)output;\n    const char *command = event->command;\n    const char *nickname = event->nickname;\n    const char *message = event->message;\n    \n    if (strcmp(command, \"PRIVMSG\") == 0) {\n        if (message[0] != '\\0') {\n            network_send(network,\n                \"NOTICE %s :\\001PING %s\\001\\r\\n\",\n                nickname, message);\n        } else {\n            network_send(network,\n                \"NOTICE %s :\\001PING\\001\\r\\n\",\n                nickname);\n        }\n    }\n}\n\n/**\n * ctcp_handle_time() - Handle CTCP TIME request\n * @network: Network connection structure\n * @event: Event structure containing request details\n * @output: Output buffer structure (unused)\n *\n * Responds to CTCP TIME queries with the current local time in a formatted\n * string. Only responds to PRIVMSG, not NOTICE.\n */\nvoid ctcp_handle_time(struct network *network, struct event *event, struct output *output)\n{\n    (void)output;\n    const char *command = event->command;\n    const char *nickname = event->nickname;\n    \n    if (strcmp(command, \"PRIVMSG\") == 0) {\n        char tbuf[128];\n        time_t now;\n        time(&now);\n        struct tm *info = localtime(&now);\n        strftime(tbuf, sizeof(tbuf), \"%c\", info);\n        network_send(network,\n            \"NOTICE %s :\\001TIME %s\\001\\r\\n\",\n            nickname, tbuf);\n    }\n}\n\n/**\n * ctcp_handle_version() - Handle CTCP VERSION request\n * @network: Network connection structure\n * @event: Event structure containing request details\n * @output: Output buffer structure (unused)\n *\n * Responds to CTCP VERSION queries with the client name and version number.\n * Only responds to PRIVMSG, not NOTICE.\n */\nvoid ctcp_handle_version(struct network *network, struct event *event, struct output *output)\n{\n    (void)output;\n    const char *command = event->command;\n    const char *nickname = event->nickname;\n    \n    if (strcmp(command, \"PRIVMSG\") == 0) {\n        network_send(network,\n            \"NOTICE %s :\\001VERSION kirc %s\\001\\r\\n\", nickname, \n            KIRC_VERSION_MAJOR \".\" KIRC_VERSION_MINOR \".\" KIRC_VERSION_PATCH);\n    }\n}\n"
  },
  {
    "path": "src/dcc.c",
    "content": "/*\n * dcc.c\n * Direct client-to-client (DCC) event handling\n * Author: Michael Czigler\n * License: MIT\n */\n\n#include \"dcc.h\"\n\n/**\n * sanitize_filename() - Validate filename for security\n * @filename: Filename string to validate\n *\n * Checks a filename for security issues: rejects empty names, dotfiles,\n * path traversal attempts (..), path separators (/ or \\\\), and names\n * exceeding NAME_MAX length.\n *\n * Return: 0 if filename is safe, -1 if unsafe or invalid\n */\nstatic int sanitize_filename(char *filename)\n{\n    if (filename == NULL) {\n        return -1;\n    }\n\n    if ((filename[0] == '\\0') || (filename[0] == '.')) {\n        return -1;\n    }\n\n    if (strstr(filename, \"..\") != NULL) {\n        return -1;\n    }\n\n    if (strchr(filename, '/') != NULL) {\n        return -1;\n    }\n\n    if (strchr(filename, '\\\\') != NULL) {\n        return -1;\n    }\n\n    if (strlen(filename) >= NAME_MAX) {\n        return -1;\n    }\n\n    return 0;\n}\n\n/**\n * dcc_init() - Initialize DCC transfer management structure\n * @dcc: DCC structure to initialize\n * @ctx: IRC context structure\n *\n * Initializes the DCC transfer manager, setting all file descriptors to -1\n * and transfer states to idle. Prepares the structure for handling up to\n * KIRC_DCC_TRANSFERS_MAX concurrent transfers.\n *\n * Return: 0 on success, -1 if dcc or ctx is NULL\n */\nint dcc_init(struct dcc *dcc, struct kirc_context *ctx)\n{\n    if ((dcc == NULL) || (ctx == NULL)) {\n        return -1;\n    }\n\n    memset(dcc, 0, sizeof(*dcc));\n    dcc->ctx = ctx;\n\n    int limit = KIRC_DCC_TRANSFERS_MAX;\n\n    for (int i = 0; i < limit; ++i) {\n        dcc->sock_fd[i].fd = -1;\n        dcc->sock_fd[i].events = POLLIN;\n        dcc->transfer[i].state = DCC_STATE_IDLE;\n        dcc->transfer[i].file_fd = -1;\n    }\n\n    return 0;\n}\n\n/**\n * dcc_free() - Free DCC resources and close connections\n * @dcc: DCC structure to clean up\n *\n * Closes all open socket and file descriptors associated with DCC transfers.\n * Should be called before program termination to release resources.\n *\n * Return: 0 on success, -1 if dcc is NULL\n */\nint dcc_free(struct dcc *dcc)\n{\n    if (dcc == NULL) {\n        return -1;\n    }\n\n    int limit = KIRC_DCC_TRANSFERS_MAX;\n\n    for (int i = 0; i < limit; ++i) {\n        if (dcc->sock_fd[i].fd >= 0) {\n            close(dcc->sock_fd[i].fd);\n            dcc->sock_fd[i].fd = -1;\n        }\n\n        if (dcc->transfer[i].file_fd >= 0) {\n            close(dcc->transfer[i].file_fd);\n            dcc->transfer[i].file_fd = -1;\n        }\n    }\n\n    return 0;\n}\n\n/**\n * dcc_send() - Send data for an active DCC SEND transfer\n * @dcc: DCC structure containing transfer state\n * @transfer_id: ID of the transfer to send data for\n *\n * Reads a chunk from the source file and sends it over the network socket\n * for the specified transfer. Updates transfer state and progress. Handles\n * transfer completion when all data is sent.\n *\n * Return: Number of bytes sent, 0 if complete, -1 on error\n */\nint dcc_send(struct dcc *dcc, int transfer_id)\n{\n    if ((dcc == NULL) || (transfer_id < 0) ||\n        (transfer_id >= KIRC_DCC_TRANSFERS_MAX)) {\n        return -1;\n    }\n\n    struct dcc_transfer *transfer = &dcc->transfer[transfer_id];\n\n    if (transfer->type != DCC_TYPE_SEND) {\n        printf(\"\\r\" CLEAR_LINE DIM \"error: %d is not a SEND transfer\"\n            RESET \"\\r\\n\", transfer_id);\n        return -1;\n    }\n\n    if (transfer->state != DCC_STATE_TRANSFERRING) {\n        printf(\"\\r\" CLEAR_LINE DIM \"error: %d is not in the transferring state\"\n            RESET \"\\r\\n\", transfer_id);\n        return -1;\n    }\n\n    char buffer[KIRC_DCC_BUFFER_SIZE];\n    ssize_t nread = read(transfer->file_fd, buffer,\n        sizeof(buffer));\n\n    if (nread < 0) {\n        printf(\"\\r\" CLEAR_LINE DIM \"error: file read failed\"\n            RESET \"\\r\\n\");\n        transfer->state = DCC_STATE_ERROR;\n        return -1;\n    }\n\n    if (nread == 0) {\n        printf(\"\\r\" CLEAR_LINE DIM \"dcc: %d transfer complete\"\n            RESET \"\\r\\n\", transfer_id);\n        transfer->state = DCC_STATE_COMPLETE;\n        return 0;\n    }\n\n    ssize_t nsent = write(dcc->sock_fd[transfer_id].fd,\n        buffer, nread);\n\n    if (nsent < 0) {\n        if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) {\n            return 0;\n        }\n\n        printf(\"\\r\" CLEAR_LINE DIM \"error: send failed\"\n            RESET \"\\r\\n\");\n        transfer->state = DCC_STATE_ERROR;\n        return -1;\n    }\n\n    transfer->sent += nsent;\n\n    if (transfer->sent >= transfer->filesize) {\n        printf(\"\\r\" CLEAR_LINE DIM \"dcc: %d transfer complete\"\n            RESET \"\\r\\n\", transfer_id);\n        transfer->state = DCC_STATE_COMPLETE;\n    }\n    \n    return nsent;\n}\n\n/**\n * dcc_process() - Process pending DCC transfers\n * @dcc: DCC structure containing active transfers\n *\n * Polls all active DCC transfer sockets for I/O readiness and processes\n * data transfer, connection establishment, and error handling. Should be\n * called periodically in the main event loop. Handles both SEND and\n * RECEIVE transfers, cleaning up completed or failed transfers.\n *\n * Return: 0 on success, -1 on error\n */\nint dcc_process(struct dcc *dcc)\n{\n    if (dcc == NULL) {\n        return -1;\n    }\n\n    if (dcc->transfer_count == 0) {\n        return 0;\n    }\n\n    int limit = KIRC_DCC_TRANSFERS_MAX;\n    int rc = poll(dcc->sock_fd, limit, 0);\n\n    if (rc < 0) {\n        if (errno == EINTR) {\n            return 0;\n        }\n\n        printf(\"\\r\" CLEAR_LINE DIM \"error: poll failed\"\n            RESET \"\\r\\n\");\n        return -1;\n    }\n\n    if (rc == 0) {\n        return 0;\n    }\n\n    /* process each transfer */\n    for (int i = 0; i < limit; ++i) {\n        if (dcc->sock_fd[i].fd < 0) {\n            continue;\n        }\n\n        struct dcc_transfer *transfer = &dcc->transfer[i];\n\n        if (transfer->state == DCC_STATE_CONNECTING) {\n            if (dcc->sock_fd[i].revents & POLLOUT) {\n                int error = 0;\n                socklen_t len = sizeof(error);\n                if (getsockopt(dcc->sock_fd[i].fd, SOL_SOCKET, SO_ERROR, &error, &len) == 0) {\n                    if (error == 0) {\n                        printf(\"\\r\" CLEAR_LINE DIM \"dcc: %d connected\"\n                            RESET \"\\r\\n\", i);\n                        transfer->state = DCC_STATE_TRANSFERRING;\n                        dcc->sock_fd[i].events = POLLIN;\n                    } else {\n                        printf(\"\\r\" CLEAR_LINE DIM \"error: connection failed\"\n                            RESET \"\\r\\n\");\n                        transfer->state = DCC_STATE_ERROR;\n                    }\n                }\n            }\n            continue;\n        }\n\n        /* handle receive transfers */\n        if ((transfer->type == DCC_TYPE_RECEIVE) &&\n            (transfer->state == DCC_STATE_TRANSFERRING)) {\n\n            if (dcc->sock_fd[i].revents & POLLIN) {\n                char buffer[KIRC_DCC_BUFFER_SIZE];\n                ssize_t nread = read(dcc->sock_fd[i].fd, buffer,\n                    sizeof(buffer));\n\n                if (nread < 0) {\n                    if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) {\n                        continue; \n                    }\n\n                    printf(\"\\r\" CLEAR_LINE DIM \"error: receive failed\"\n                        RESET \"\\r\\n\");\n                    transfer->state = DCC_STATE_ERROR;\n                    continue;\n                }\n                \n                if (nread == 0) {\n                    if (transfer->sent >= transfer->filesize) {\n                        printf(\"\\r\" CLEAR_LINE DIM \"dcc: %d transfer complete (%llu bytes)\"\n                            RESET \"\\r\\n\", i, transfer->sent);\n                        transfer->state = DCC_STATE_COMPLETE;\n                    } else {\n                        printf(\"\\r\" CLEAR_LINE DIM \"error: %d transfer incomplete (%llu/%llu bytes)\"\n                            RESET \"\\r\\n\", i, transfer->sent, transfer->filesize);\n                        transfer->state = DCC_STATE_COMPLETE;\n                    }\n                    continue;\n                }\n\n                ssize_t nwritten = write(transfer->file_fd, buffer, nread);\n\n                if (nwritten < 0) {\n                    printf(\"\\r\" CLEAR_LINE DIM \"error: write failed\"\n                        RESET \"\\r\\n\");\n                    transfer->state = DCC_STATE_ERROR;\n                    continue;\n                }\n\n                transfer->sent += nwritten;\n\n                if (transfer->sent >= transfer->filesize) {\n                    printf(\"\\r\" CLEAR_LINE DIM \"dcc: %d transfer complete (%llu bytes)\"\n                        RESET \"\\r\\n\", i, transfer->sent);\n                    transfer->state = DCC_STATE_COMPLETE;\n                }\n            }\n        }\n        \n        /* handle send transfer */\n        if ((transfer->type == DCC_TYPE_SEND) &&\n            (transfer->state == DCC_STATE_TRANSFERRING)) {\n            if (dcc->sock_fd[i].revents & POLLOUT) {\n                dcc_send(dcc, i);\n            }\n        }\n\n        /* cleanup completed or error transfers */\n        if ((transfer->state == DCC_STATE_COMPLETE) ||\n            (transfer->state == DCC_STATE_ERROR)) {\n            if (dcc->sock_fd[i].fd >= 0) {\n                close(dcc->sock_fd[i].fd);\n                dcc->sock_fd[i].fd = -1;\n            }\n\n            if (dcc->transfer[i].file_fd >= 0) {\n                close(dcc->transfer[i].file_fd);\n                dcc->transfer[i].file_fd = -1;\n            }\n\n            transfer->state = DCC_STATE_IDLE;\n            dcc->transfer_count--;\n        }\n    }\n\n    return 0;\n}\n\n/**\n * dcc_request() - Handle incoming DCC SEND request\n * @dcc: DCC structure to register the transfer\n * @sender: Nickname of the user initiating the transfer\n * @params: DCC SEND parameters (filename, IP, port, filesize)\n *\n * Parses a DCC SEND request and initiates a file receive transfer. Creates\n * the destination file, establishes a network connection to the sender, and\n * registers the transfer for processing. Handles quoted filenames and\n * validates parameters for security.\n *\n * Return: Transfer ID on success, -1 on error\n */\nint dcc_request(struct dcc *dcc, const char *sender, const char *params)\n{\n    if ((dcc == NULL) || (sender == NULL) || (params == NULL)) {\n        return -1;\n    }\n\n    /* find free transfer slot */\n    int transfer_id = -1;\n    int limit = KIRC_DCC_TRANSFERS_MAX;\n\n    for (int i = 0; i < limit; ++i) {\n        if (dcc->transfer[i].state == DCC_STATE_IDLE) {\n            transfer_id = i;\n            break;\n        }\n    }\n\n    if (transfer_id < 0) {\n        printf(\"\\r\" CLEAR_LINE DIM \"error: no free DCC transfer slots\"\n            RESET \"\\r\\n\");\n        return -1;\n    }\n\n    /* parse DCC SEND parameters: SEND filename ip port filesize */\n    char params_copy[MESSAGE_MAX_LEN];\n    size_t siz = sizeof(params_copy);\n    safecpy(params_copy, params, siz);\n\n    char *command = strtok(params_copy, \" \");\n    if ((command == NULL) || (strcmp(command, \"SEND\") != 0)) {\n        printf(\"\\r\" CLEAR_LINE DIM \"error: unsupported DCC command\"\n            RESET \"\\r\\n\");\n        return -1;\n    }\n\n    char *filename_tok = strtok(NULL, \" \");\n    if (filename_tok == NULL) {\n        printf(\"\\r\" CLEAR_LINE DIM \"error: invalid DCC SEND format\"\n            RESET \"\\r\\n\");\n        return -1;\n    }\n\n    /* handle quoted filenames */\n    char filename[NAME_MAX];\n    if (filename_tok[0] == '\"') {\n        /* find closing quote */\n        char *end_quote = strchr(filename_tok + 1, '\"');\n        if (end_quote != NULL) {\n            size_t len = end_quote - filename_tok - 1;\n            if (len >= NAME_MAX) {\n                len = NAME_MAX;\n            } else {\n                len = len + 1;\n            }\n            safecpy(filename, filename_tok + 1, len);\n        } else {\n            /* filename spans multiple tokens */\n            size_t len = strlen(filename_tok + 1);\n\n            if (len >= NAME_MAX) {\n                len = NAME_MAX;\n            } else {\n                len = len + 1;\n            }\n\n            safecpy(filename, filename_tok + 1, len);\n\n            /* continue reading until closing quote */\n            char *next = strtok(NULL, \"\\\"\");\n\n            if (next != NULL) {\n                size_t flen = strlen(filename);\n                size_t nlen = strlen(next);\n\n                if (flen + nlen + 1 < NAME_MAX) {\n                    filename[flen] = ' ';\n                    strncpy(filename + flen + 1, next, NAME_MAX - flen - 2);\n                    filename[NAME_MAX - 1] = '\\0';\n                }\n            }\n        }\n    } else {\n        siz = sizeof(filename);\n        safecpy(filename, filename_tok, siz);\n    }\n\n    if (sanitize_filename(filename) < 0) {\n        printf(\"\\r\" CLEAR_LINE DIM \"error: invalid or unsafe filename\"\n            RESET \"\\r\\n\");\n        return -1;\n    }\n\n    char *server = strtok(NULL, \" \");\n    char *port = strtok(NULL, \" \");\n    char *filesize = strtok(NULL, \" \");\n\n    if ((server == NULL) || (port == NULL) || (filesize == NULL)) {\n        printf(\"\\r\" CLEAR_LINE DIM \"error: invalid DCC SEND format\"\n            RESET \"\\r\\n\");\n        return -1;\n    }\n\n    /* initialize transfer */\n    struct dcc_transfer *transfer = &dcc->transfer[transfer_id];\n    transfer->type = DCC_TYPE_RECEIVE;\n    transfer->state = DCC_STATE_CONNECTING;\n    transfer->filesize = strtoull(filesize, NULL, 10);\n    transfer->sent = 0;\n    \n    siz = sizeof(transfer->filename);\n    safecpy(transfer->filename, filename, siz);\n    \n    siz = sizeof(transfer->sender);\n    safecpy(transfer->sender, sender, siz);\n\n    /* open file for writing */\n    transfer->file_fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);\n\n    if (transfer->file_fd < 0) {\n        printf(\"\\r\" CLEAR_LINE DIM \"error: cannot create file %s\"\n            RESET \"\\r\\n\", filename);\n        transfer->state = DCC_STATE_IDLE;\n        return -1;\n    }\n\n    /* create socket */\n    struct addrinfo hints, *res;\n    memset(&hints, 0, sizeof(hints));\n    hints.ai_family = AF_INET;\n    hints.ai_socktype = SOCK_STREAM;\n\n    int rc = getaddrinfo(server, port, &hints, &res);\n    if (rc != 0) {\n        printf(\"\\r\" CLEAR_LINE DIM \"error: getaddrinfo failed: %s\"\n            RESET \"\\r\\n\", gai_strerror(rc));\n        close(transfer->file_fd);\n        transfer->file_fd = -1;\n        transfer->state = DCC_STATE_IDLE;\n        return -1;\n    }\n\n    int sock_fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);\n    if (sock_fd < 0) {\n        printf(\"\\r\" CLEAR_LINE DIM \"error: socket creation failed\"\n            RESET \"\\r\\n\");\n        freeaddrinfo(res);\n        close(transfer->file_fd);\n        transfer->file_fd = -1;\n        transfer->state = DCC_STATE_IDLE;\n        return -1;\n    }\n\n    /* set non-blocking */\n    int flags = fcntl(sock_fd, F_GETFL, 0);\n    fcntl(sock_fd, F_SETFL, flags | O_NONBLOCK);\n\n    /* connect */\n    rc = connect(sock_fd, res->ai_addr, res->ai_addrlen);\n    if ((rc < 0) && (errno != EINPROGRESS)) {\n        printf(\"\\r\" CLEAR_LINE DIM \"error: connection failed\"\n            RESET \"\\r\\n\");\n        close(sock_fd);\n        freeaddrinfo(res);\n        close(transfer->file_fd);\n        transfer->file_fd = -1;\n        transfer->state = DCC_STATE_IDLE;\n        return -1;\n    }\n\n    freeaddrinfo(res);\n\n    dcc->sock_fd[transfer_id].fd = sock_fd;\n    dcc->sock_fd[transfer_id].events = POLLOUT;\n    dcc->transfer_count++;\n\n    printf(\"\\r\" CLEAR_LINE DIM \"dcc: receiving %s from %s (%llu bytes)\"\n        RESET \"\\r\\n\", transfer->filename, transfer->sender,\n        transfer->filesize);\n\n    return transfer_id;\n}\n\n/**\n * dcc_cancel() - Cancel an active DCC transfer\n * @dcc: DCC structure containing the transfer\n * @transfer_id: ID of the transfer to cancel\n *\n * Cancels a DCC transfer by closing associated socket and file descriptors\n * and resetting the transfer state to idle. Decrements the active transfer\n * count.\n *\n * Return: 0 on success, -1 if parameters are invalid\n */\nint dcc_cancel(struct dcc *dcc, int transfer_id)\n{\n    if ((dcc == NULL) || (transfer_id < 0) ||\n        (transfer_id >= KIRC_DCC_TRANSFERS_MAX)) {\n        return -1;\n    }\n\n    struct dcc_transfer *transfer = &dcc->transfer[transfer_id];\n\n    if (transfer->state == DCC_STATE_IDLE) {\n        return 0;\n    }\n\n    printf(\"\\r\" CLEAR_LINE DIM \"dcc: cancelling transfer %d\"\n        RESET \"\\r\\n\", transfer_id);\n\n    if (dcc->sock_fd[transfer_id].fd >= 0) {\n        close(dcc->sock_fd[transfer_id].fd);\n        dcc->sock_fd[transfer_id].fd = -1;\n    }\n\n    if (dcc->transfer[transfer_id].file_fd >= 0) {\n        close(dcc->transfer[transfer_id].file_fd);\n        dcc->transfer[transfer_id].file_fd = -1;\n    }\n\n    transfer->state = DCC_STATE_IDLE;\n    dcc->transfer_count--;\n\n    return 0;\n}\n\n/**\n * dcc_handle() - Handle DCC-related IRC events\n * @dcc: DCC structure for managing transfers\n * @network: Network connection structure\n * @event: Event structure containing DCC command\n *\n * Processes DCC events from IRC messages. Currently handles DCC SEND\n * requests by calling dcc_request() when a CTCP DCC PRIVMSG is received.\n */\nvoid dcc_handle(struct dcc *dcc, struct network *network, struct event *event)\n{\n    (void)network;\n    \n    if (dcc == NULL || event == NULL) {\n        return;\n    }\n    \n    if (event->type != EVENT_CTCP_DCC) {\n        return;\n    }\n\n    if (strcmp(event->command, \"PRIVMSG\") == 0) {\n        dcc_request(dcc, event->nickname, event->message);\n    }\n}\n"
  },
  {
    "path": "src/editor.c",
    "content": "/*\n * editor.c\n * Text editor functionality for the IRC client\n * Author: Michael Czigler\n * License: MIT\n */\n\n#include \"editor.h\"\n\n/**\n * editor_backspace() - Delete the character before the cursor\n * @editor: Editor state structure\n *\n * Removes the UTF-8 character immediately before the cursor position,\n * properly handling multi-byte UTF-8 sequences. Does nothing if cursor\n * is at the beginning of the line.\n */\nstatic void editor_backspace(struct editor *editor)\n{\n    int siz = sizeof(editor->scratch) - 1;\n    int len = strnlen(editor->scratch, siz);\n\n    if (editor->cursor <= 0) {\n        return;  /* nothing to delete or out of range */\n    }\n\n    /* Get length of previous UTF-8 character */\n    int bytes = utf8_prev_char_len(editor->scratch, editor->cursor);\n    \n    if (bytes == 0) {\n        return;\n    }\n\n    editor->cursor -= bytes;\n\n    memmove(editor->scratch + editor->cursor,\n        editor->scratch + editor->cursor + bytes,\n        len - (editor->cursor + bytes) + 1);\n}\n\n/**\n * editor_delete_line() - Clear the entire input line\n * @editor: Editor state structure\n *\n * Erases all text from the scratch buffer and resets the cursor to the\n * beginning of the line.\n */\nstatic void editor_delete_line(struct editor *editor)\n{\n    editor->scratch[0] = '\\0';\n    editor->cursor = 0;\n}\n\n/**\n * editor_enter() - Process enter key to submit current line\n * @editor: Editor state structure\n *\n * Saves the current line to history, advances the history pointer, and\n * clears the scratch buffer. Does nothing if the line is empty.\n *\n * Return: 1 if line was submitted, 0 if line was empty\n */\nstatic int editor_enter(struct editor *editor)\n{\n    if (editor->scratch[0] == '\\0') {\n        return 0;  /* nothing to send */\n    }\n\n    int siz = sizeof(editor->scratch) - 1;\n\n    strncpy(editor->history[editor->head],\n        editor->scratch, siz);\n\n    editor->head = (editor->head + 1) % KIRC_HISTORY_SIZE;\n    if (editor->count < KIRC_HISTORY_SIZE) {\n        editor->count++;\n    }\n    editor->position = -1;\n\n    editor_delete_line(editor);\n    \n    return 1;\n}\n\n/**\n * editor_delete() - Delete the character at the cursor\n * @editor: Editor state structure\n *\n * Removes the UTF-8 character at the current cursor position, properly\n * handling multi-byte UTF-8 sequences. Does nothing if cursor is at the\n * end of the line.\n */\nstatic void editor_delete(struct editor *editor)\n{\n    int siz = sizeof(editor->scratch) - 1;\n    int len = strnlen(editor->scratch, siz);\n\n    if (editor->cursor >= len) {\n        return;  /* at end of scratch string */\n    }\n\n    /* Get length of next UTF-8 character using mbrtowc */\n    int bytes = utf8_next_char_len(editor->scratch, editor->cursor, len);\n    \n    if (bytes == 0) {\n        return;\n    }\n\n    memmove(editor->scratch + editor->cursor,\n        editor->scratch + editor->cursor + bytes,\n        len - (editor->cursor + bytes) + 1);\n}\n\n/**\n * editor_history() - Navigate through command history\n * @editor: Editor state structure\n * @dir: Direction to navigate (positive for previous, negative for next)\n *\n * Allows browsing through the command history using up/down arrows. Loads\n * the previous or next history entry into the scratch buffer and updates\n * the cursor position. Handles wraparound at history boundaries.\n */\nstatic void editor_history(struct editor *editor, int dir)\n{\n    int siz = KIRC_HISTORY_SIZE;\n\n    if (editor->count == 0) {\n        return;\n    }\n\n    int head = editor->head;\n    int oldest = (head - editor->count + siz) % siz;\n    int newest = (head - 1 + siz) % siz;\n\n    if (dir > 0) {  /* up or previous */\n        int pos;\n        if (editor->position == -1) {\n            pos = newest;\n        } else if (editor->position == oldest) {\n            return; /* already at oldest */\n        } else {\n            pos = (editor->position - 1 + siz) % siz;\n        }\n\n        strncpy(editor->scratch, editor->history[pos],\n            sizeof(editor->scratch) - 1);\n        editor->scratch[sizeof(editor->scratch) - 1] = '\\0';\n        editor->cursor = strnlen(editor->scratch,\n            sizeof(editor->scratch) - 1);\n        editor->position = pos;\n    } else { /* down or next */\n        if (editor->position == -1) {\n            return; /* not browsing */\n        }\n\n        if (editor->position == newest) {\n            editor_delete_line(editor);\n            editor->position = -1;\n            return;\n        }\n\n        int pos = (editor->position + 1) % siz;\n\n        strncpy(editor->scratch, editor->history[pos],\n            sizeof(editor->scratch) - 1);\n        editor->scratch[sizeof(editor->scratch) - 1] = '\\0';\n        editor->cursor = strnlen(editor->scratch,\n            sizeof(editor->scratch) - 1);\n        editor->position = pos;\n    }\n}\n\n/**\n * editor_move_right() - Move cursor one character to the right\n * @editor: Editor state structure\n *\n * Advances the cursor by one UTF-8 character, properly handling multi-byte\n * sequences. Does nothing if cursor is at the end of the line.\n */\nstatic void editor_move_right(struct editor *editor)\n{\n    int siz = sizeof(editor->scratch) - 1;\n    int len = strnlen(editor->scratch, siz);\n\n    if (editor->cursor >= len) {\n        return; /* at end */\n    }\n\n    /* Use mbrtowc to get proper character length */\n    int adv = utf8_next_char_len(editor->scratch, editor->cursor, len);\n    \n    if (adv > 0 && editor->cursor + adv <= len) {\n        editor->cursor += adv;\n    } else {\n        /* Invalid sequence or truncated, move to end */\n        editor->cursor = len;\n    }\n}\n\n/**\n * editor_move_left() - Move cursor one character to the left\n * @editor: Editor state structure\n *\n * Moves the cursor back by one UTF-8 character, properly handling multi-byte\n * sequences. Does nothing if cursor is at the beginning of the line.\n */\nstatic void editor_move_left(struct editor *editor)\n{\n    if (editor->cursor <= 0) {\n        return;\n    }\n\n    /* Get length of previous UTF-8 character */\n    int bytes = utf8_prev_char_len(editor->scratch, editor->cursor);\n    \n    if (bytes > 0) {\n        editor->cursor -= bytes;\n    }\n}\n\n/**\n * editor_move_home() - Move cursor to beginning of line\n * @editor: Editor state structure\n *\n * Sets the cursor position to the start of the input line.\n */\nstatic void editor_move_home(struct editor *editor)\n{\n    editor->cursor = 0;\n}\n\n/**\n * editor_move_end() - Move cursor to end of line\n * @editor: Editor state structure\n *\n * Sets the cursor position to the end of the current input line.\n */\nstatic void editor_move_end(struct editor *editor)\n{\n    size_t siz = sizeof(editor->scratch) - 1;\n\n    editor->cursor = strnlen(editor->scratch, siz);\n}\n\n/**\n * editor_escape() - Handle escape sequences for special keys\n * @editor: Editor state structure\n *\n * Processes ANSI escape sequences for cursor movement and special keys\n * (arrow keys, Home, End, Delete). Reads additional bytes from stdin\n * to determine the complete escape sequence and executes the appropriate\n * editor action.\n */\nstatic void editor_escape(struct editor *editor)\n{\n    char seq[3];\n\n    if (read(STDIN_FILENO, &seq[0], 1) != 1) {\n        return;\n    }\n\n    if (read(STDIN_FILENO, &seq[1], 1) != 1) {\n        return;\n    }\n\n    if (seq[0] == '[') {\n        if (seq[1] >= '0' && seq[1] <= '9') {\n            if (read(STDIN_FILENO, &seq[2], 1) != 1) {\n                return;\n            }\n\n            if (seq[1] == '3' && seq[2] == '~') {\n                editor_delete(editor);\n            }\n        } else {\n            switch (seq[1]) {\n            case 'A':\n                editor_history(editor, 1);\n                break;\n\n            case 'B':\n                editor_history(editor, -1);\n                break;\n\n            case 'C':\n                editor_move_right(editor);\n                break;\n\n            case 'D':\n                editor_move_left(editor);\n                break;\n\n            case 'H':\n                editor_move_home(editor);\n                break;\n\n            case 'F':\n                editor_move_end(editor);\n                break;\n            }\n        }\n    }\n}\n\n/**\n * editor_insert() - Insert a single character at cursor position\n * @editor: Editor state structure\n * @c: Character to insert\n *\n * Inserts a single byte character at the current cursor position, shifting\n * existing text to the right. Advances the cursor after insertion. Does\n * nothing if the buffer is full.\n */\nstatic void editor_insert(struct editor *editor, char c)\n{\n    int siz = sizeof(editor->scratch) - 1;\n    int len = strnlen(editor->scratch, siz);\n\n    if (editor->cursor > siz) {\n        return;  /* at end of scratch */\n    }\n\n    if (len + 1 >= siz) {\n        return;  /* scratch full */\n    }\n\n    memmove(editor->scratch + editor->cursor + 1,\n        editor->scratch + editor->cursor,\n        len - editor->cursor + 1);\n\n    editor->scratch[editor->cursor] = c;\n    editor->cursor++;\n}\n\n/**\n * editor_insert_bytes() - Insert multiple bytes at cursor position\n * @editor: Editor state structure\n * @buf: Buffer containing bytes to insert\n * @n: Number of bytes to insert\n *\n * Inserts a sequence of bytes (typically a multi-byte UTF-8 character) at\n * the current cursor position. Validates UTF-8 encoding before insertion.\n * Shifts existing text to the right and advances the cursor.\n */\nstatic void editor_insert_bytes(struct editor *editor, const char *buf, int n)\n{\n    int siz = sizeof(editor->scratch) - 1;\n    int len = strnlen(editor->scratch, siz);\n\n    if (editor->cursor > siz) {\n        return;  /* at end of scratch */\n    }\n\n    if (len + n >= siz) {\n        return;  /* scratch full */\n    }\n\n    /* Validate UTF-8 sequence before inserting */\n    if (!utf8_validate(buf, n)) {\n        return;  /* invalid UTF-8, reject */\n    }\n\n    memmove(editor->scratch + editor->cursor + n,\n        editor->scratch + editor->cursor,\n        len - editor->cursor + 1);\n\n    memcpy(editor->scratch + editor->cursor, buf, n);\n    editor->cursor += n;\n}\n\n/**\n * editor_clear() - Clear the current line on terminal\n * @editor: Editor state structure (unused)\n *\n * Sends ANSI escape codes to clear the current terminal line, moving\n * the cursor to the beginning.\n */\nstatic void editor_clear(struct editor *editor)\n{\n    printf(\"\\r\" CLEAR_LINE);\n}\n\n/**\n * display_width_bytes() - Calculate display width of UTF-8 string\n * @s: UTF-8 encoded string\n * @bytes: Number of bytes to measure\n *\n * Computes the display width (number of terminal columns) occupied by\n * a UTF-8 string, accounting for wide characters (e.g., CJK characters).\n * Handles invalid sequences by treating them as width 1.\n *\n * Return: Display width in terminal columns\n */\nstatic int display_width_bytes(const char *s, int bytes)\n{\n    mbstate_t st;\n    memset(&st, 0, sizeof(st));\n    int pos = 0;\n    int wsum = 0;\n\n    while (pos < bytes) {\n        wchar_t wc;\n        size_t ret = mbrtowc(&wc, s + pos, bytes - pos, &st);\n        if (ret == (size_t)-1 || ret == (size_t)-2) {\n            /* invalid sequence: treat as width 1 */\n            pos++;\n            wsum += 1;\n            memset(&st, 0, sizeof(st));\n            continue;\n        }\n        if (ret == 0) {\n            pos++;\n            continue;\n        }\n        int w = wcwidth(wc);\n        if (w < 0) w = 0;\n        wsum += w;\n        pos += ret;\n    }\n\n    return wsum;\n}\n\n/**\n * editor_tab() - Insert tab as spaces\n * @editor: Editor state structure\n *\n * Inserts KIRC_TAB_WIDTH spaces at the cursor position to simulate\n * a tab character.\n */\nstatic void editor_tab(struct editor *editor)\n{\n    int width = KIRC_TAB_WIDTH;\n\n    for (int i = 0; i < width; ++i) {\n        editor_insert(editor, ' ');\n    } \n}\n\n/**\n * editor_last_entry() - Get the most recent history entry\n * @editor: Editor state structure\n *\n * Returns a pointer to the last command entered in the history buffer.\n *\n * Return: Pointer to the most recent history string\n */\nchar *editor_last_entry(struct editor *editor)\n{\n    int head = editor->head;\n    int last = (head - 1 + KIRC_HISTORY_SIZE) % KIRC_HISTORY_SIZE;\n\n    return editor->history[last];\n}\n\n/**\n * editor_init() - Initialize the editor state\n * @editor: Editor structure to initialize\n * @ctx: IRC context structure\n *\n * Initializes the editor with zeroed state, sets up locale for UTF-8\n * support, and associates it with the IRC context. Prepares the editor\n * for input processing.\n *\n * Return: 0 on success, -1 if editor or ctx is NULL\n */\nint editor_init(struct editor *editor, struct kirc_context *ctx)\n{\n    if ((editor == NULL) || (ctx == NULL)) {\n        return -1;\n    }\n\n    memset(editor, 0, sizeof(*editor));\n    \n    editor->ctx = ctx;\n    editor->state = EDITOR_STATE_NONE;\n    editor->position = -1;\n\n    setlocale(LC_CTYPE, \"\");\n\n    return 0;\n}\n\n/**\n * editor_process_key() - Process a single keystroke\n * @editor: Editor state structure\n *\n * Reads and processes one character from stdin. Handles control characters,\n * escape sequences, and UTF-8 multi-byte input. Updates editor state\n * (SEND, TERMINATE, or NONE) based on the key pressed.\n *\n * Return: 0 on success, 1 if read failed\n */\nint editor_process_key(struct editor *editor)\n{\n    char c;\n    \n    if (read(STDIN_FILENO, &c, 1) < 1) {\n        return 1;\n    }\n    \n    editor->state = EDITOR_STATE_NONE;\n\n    switch(c) {\n    case HT:  /* CTRL-I or TAB */\n        editor_tab(editor);\n        break;\n\n    case ETX:  /* CTRL-C */\n        editor_clear(editor);\n        editor->state = EDITOR_STATE_TERMINATE;\n        break;\n\n    case NAK:  /* CTRL-U */\n        editor_delete_line(editor);\n        break;\n\n    case BS:  /* CTRL-H */\n    case DEL:\n        editor_backspace(editor);\n        break;\n\n    case CR:  /* CTRL-M or ENTER */\n        if (editor_enter(editor) > 0) {\n            editor->state = EDITOR_STATE_SEND;\n        }\n        break;\n\n    case ESC:  /* CTRL-[ */\n        editor_escape(editor);\n        break;\n\n    case SOH:  /* CTRL-A */\n    case STX:  /* CTRL-B */\n    case EOT:  /* CTRL-D */\n    case ENQ:  /* CTRL-E */\n    case ACK:  /* CTRL-F */\n    case BEL:  /* CTRL-G */\n    case LF:  /* CTRL-J */\n    case VT:  /* CTRL-K */\n    case FF:  /* CTRL-L */\n    case SO:  /* CTRL-N */\n    case SI:  /* CTRL-O */\n    case DLE:  /* CTRL-P */\n    case DC1:  /* CTRL-Q */\n    case DC2:  /* CTRL-R */\n    case DC3:  /* CTRL-S */\n    case DC4:  /* CTRL-T */\n    case SYN:  /* CTRL-V */\n    case ETB:  /* CTRL-W */\n    case CAN:  /* CTRL-X */\n    case EM:  /* CTRL-Y */\n    case SUB:  /* CTRL-Z */\n    case FS:  /* CTRL-\\ */\n    case GS:  /* CTRL-] */\n    case RS:  /* CTRL-^ */\n    case US:  /* CTRL-_ */\n        break;  /* not implemented yet */\n\n    default:\n        /* handle UTF-8 multi-byte input: read remaining continuation bytes */\n        {\n            unsigned char uc = (unsigned char)c;\n            char buf[5];\n            int need = 0;\n            buf[0] = c;\n\n            if ((uc & 0x80) == 0) {\n                editor_insert(editor, c);\n            } else {\n                if ((uc & 0xE0) == 0xC0) need = 1;\n                else if ((uc & 0xF0) == 0xE0) need = 2;\n                else if ((uc & 0xF8) == 0xF0) need = 3;\n                else need = 0;\n\n                int i;\n                for (i = 1; i <= need; i++) {\n                    if (read(STDIN_FILENO, &buf[i], 1) != 1) {\n                        break;\n                    }\n                }\n\n                editor_insert_bytes(editor, buf, 1 + (i - 1));\n            }\n        }\n        break;\n    }\n\n    return 0;\n}\n\n/**\n * editor_handle() - Render the editor display\n * @editor: Editor state structure\n *\n * Renders the current editor state to the terminal, displaying the target\n * channel/user and the input text. Handles line scrolling when text exceeds\n * terminal width and positions the cursor correctly. Accounts for UTF-8\n * character widths for proper display alignment.\n *\n * Return: 0 on success\n */\nint editor_handle(struct editor *editor)\n{\n    int cols = terminal_columns(STDIN_FILENO);\n    int size = strlen(editor->ctx->target) + 1;\n    int avail = cols - size - 1;\n    int siz = sizeof(editor->scratch) - 1;\n    int len = strnlen(editor->scratch, siz);\n\n    /* compute display width of bytes up to cursor */\n    int cursor_disp = display_width_bytes(editor->scratch, editor->cursor);\n\n    /* choose start byte offset so that the substring ending at cursor fits in avail */\n    int start = editor->cursor;\n    int used = 0;\n    while (start > 0) {\n        int char_bytes = utf8_prev_char_len(editor->scratch, start);\n        if (char_bytes == 0) break;\n        int cw = display_width_bytes(editor->scratch + start - char_bytes, char_bytes);\n        if (used + cw > avail) break;\n        used += cw;\n        start -= char_bytes;\n    }\n\n    /* compute how many bytes we can print from start given avail */\n    int bytes_to_print = 0;\n    int p = start;\n    int printed_width = 0;\n    while (p < len) {\n        int cb = utf8_next_char_len(editor->scratch, p, len);\n        if (cb == 0) break;\n        int cw = display_width_bytes(editor->scratch + p, cb);\n        if (printed_width + cw > avail) break;\n        printed_width += cw;\n        p += cb;\n    }\n    bytes_to_print = p - start;\n\n    printf(\"\\r%s:\", editor->ctx->target);\n\n    if (bytes_to_print > 0) {\n        fwrite(editor->scratch + start, 1, bytes_to_print, stdout);\n    }\n\n    printf(\" \" CLEAR_LINE);\n\n    printf(\"\\r\\x1b[%dC\", cursor_disp + size);\n\n    fflush(stdout);\n\n    return 0;\n}"
  },
  {
    "path": "src/event.c",
    "content": "/*\n * event.c\n * Message event handling\n * Author: Michael Czigler\n * License: MIT\n */\n\n#include \"event.h\"\n\n/* Event dispatch table - used only for parsing */\nstatic const struct event_dispatch_table event_table[] = {\n    { \"CAP\",     EVENT_EXT_CAP },\n    { \"JOIN\",    EVENT_JOIN },\n    { \"KICK\",    EVENT_KICK },\n    { \"MODE\",    EVENT_MODE },\n    { \"NICK\",    EVENT_NICK },\n    { \"NOTICE\",  EVENT_NOTICE },\n    { \"PART\",    EVENT_PART },\n    { \"PING\",    EVENT_PING },\n    { \"PRIVMSG\", EVENT_PRIVMSG },\n    { \"QUIT\",    EVENT_QUIT },\n    { \"TOPIC\",   EVENT_TOPIC },\n    { \"001\",     EVENT_001_RPL_WELCOME },\n    { \"002\",     EVENT_002_RPL_YOURHOST },\n    { \"003\",     EVENT_003_RPL_CREATED },\n    { \"004\",     EVENT_004_RPL_MYINFO },\n    { \"005\",     EVENT_005_RPL_BOUNCE },\n    { \"042\",     EVENT_042_RPL_YOURID },\n    { \"200\",     EVENT_200_RPL_TRACELINK },\n    { \"201\",     EVENT_201_RPL_TRACECONNECTING },\n    { \"202\",     EVENT_202_RPL_TRACEHANDSHAKE },\n    { \"203\",     EVENT_203_RPL_TRACEUNKNOWN },\n    { \"204\",     EVENT_204_RPL_TRACEOPERATOR },\n    { \"205\",     EVENT_205_RPL_TRACEUSER },\n    { \"206\",     EVENT_206_RPL_TRACESERVER },\n    { \"207\",     EVENT_207_RPL_TRACESERVICE },\n    { \"208\",     EVENT_208_RPL_TRACENEWTYPE },\n    { \"209\",     EVENT_209_RPL_TRACECLASS },\n    { \"211\",     EVENT_211_RPL_STATSLINKINFO },\n    { \"212\",     EVENT_212_RPL_STATSCOMMANDS },\n    { \"213\",     EVENT_213_RPL_STATSCLINE},\n    { \"215\",     EVENT_215_RPL_STATSILINE },\n    { \"216\",     EVENT_216_RPL_STATSKLINE },\n    { \"218\",     EVENT_218_RPL_STATSYLINE },\n    { \"219\",     EVENT_219_RPL_ENDOFSTATS },\n    { \"221\",     EVENT_221_RPL_UMODEIS },\n    { \"234\",     EVENT_234_RPL_SERVLIST },\n    { \"235\",     EVENT_235_RPL_SERVLISTEND },\n    { \"241\",     EVENT_241_RPL_STATSLLINE },\n    { \"242\",     EVENT_242_RPL_STATSUPTIME },\n    { \"243\",     EVENT_243_RPL_STATSOLINE },\n    { \"244\",     EVENT_244_RPL_STATSHLINE },\n    { \"245\",     EVENT_245_RPL_STATSSLINE },\n    { \"250\",     EVENT_250_RPL_STATSCONN },\n    { \"251\",     EVENT_251_RPL_LUSERCLIENT },\n    { \"252\",     EVENT_252_RPL_LUSEROP },\n    { \"253\",     EVENT_253_RPL_LUSERUNKNOWN },\n    { \"254\",     EVENT_254_RPL_LUSERCHANNELS },\n    { \"255\",     EVENT_255_RPL_LUSERME },\n    { \"256\",     EVENT_256_RPL_ADMINME },\n    { \"257\",     EVENT_257_RPL_ADMINLOC1 },\n    { \"258\",     EVENT_258_RPL_ADMINLOC2 },\n    { \"259\",     EVENT_259_RPL_ADMINEMAIL },\n    { \"261\",     EVENT_261_RPL_TRACELOG },\n    { \"263\",     EVENT_263_RPL_TRYAGAIN },\n    { \"265\",     EVENT_265_RPL_LOCALUSERS },\n    { \"266\",     EVENT_266_RPL_GLOBALUSERS },\n    { \"300\",     EVENT_300_RPL_NONE },\n    { \"301\",     EVENT_301_RPL_AWAY },\n    { \"302\",     EVENT_302_RPL_USERHOST },\n    { \"303\",     EVENT_303_RPL_ISON },\n    { \"305\",     EVENT_305_RPL_UNAWAY },\n    { \"306\",     EVENT_306_RPL_NOWAWAY },\n    { \"311\",     EVENT_311_RPL_WHOISUSER },\n    { \"312\",     EVENT_312_RPL_WHOISSERVER },\n    { \"313\",     EVENT_313_RPL_WHOISOPERATOR },\n    { \"314\",     EVENT_314_RPL_WHOWASUSER },\n    { \"315\",     EVENT_315_RPL_ENDOFWHO },\n    { \"317\",     EVENT_317_RPL_WHOISIDLE },\n    { \"318\",     EVENT_318_RPL_ENDOFWHOIS },\n    { \"319\",     EVENT_319_RPL_WHOISCHANNELS },\n    { \"322\",     EVENT_322_RPL_LIST },\n    { \"323\",     EVENT_323_RPL_LISTEND },\n    { \"324\",     EVENT_324_RPL_CHANNELMODEIS },\n    { \"328\",     EVENT_328_RPL_CHANNEL_URL },\n    { \"331\",     EVENT_331_RPL_NOTOPIC },\n    { \"332\",     EVENT_332_RPL_TOPIC },\n    { \"333\",     EVENT_333_RPL_TOPICWHOTIME },\n    { \"341\",     EVENT_341_RPL_INVITING },\n    { \"346\",     EVENT_346_RPL_INVITELIST },\n    { \"347\",     EVENT_347_RPL_ENDOFINVITELIST },\n    { \"348\",     EVENT_348_RPL_EXCEPTLIST },\n    { \"349\",     EVENT_349_RPL_ENDOFEXCEPTLIST },\n    { \"351\",     EVENT_351_RPL_VERSION },\n    { \"352\",     EVENT_352_RPL_WHOREPLY },\n    { \"353\",     EVENT_353_RPL_NAMREPLY },\n    { \"364\",     EVENT_364_RPL_LINKS },\n    { \"365\",     EVENT_365_RPL_ENDOFLINKS },\n    { \"366\",     EVENT_366_RPL_ENDOFNAMES },\n    { \"367\",     EVENT_367_RPL_BANLIST },\n    { \"368\",     EVENT_368_RPL_ENDOFBANLIST },\n    { \"369\",     EVENT_369_RPL_ENDOFWHOWAS },\n    { \"375\",     EVENT_375_RPL_MOTDSTART },\n    { \"371\",     EVENT_371_RPL_INFO },\n    { \"372\",     EVENT_372_RPL_MOTD },\n    { \"374\",     EVENT_374_RPL_ENDOFINFO },\n    { \"376\",     EVENT_376_RPL_ENDOFMOTD },\n    { \"381\",     EVENT_381_RPL_YOUREOPER },\n    { \"382\",     EVENT_382_RPL_REHASHING },\n    { \"383\",     EVENT_383_RPL_YOURESERVICE },\n    { \"391\",     EVENT_391_RPL_TIME },\n    { \"392\",     EVENT_392_RPL_USERSSTART },\n    { \"393\",     EVENT_393_RPL_USERS },\n    { \"394\",     EVENT_394_RPL_ENDOFUSERS },\n    { \"395\",     EVENT_395_RPL_NOUSERS },\n    { \"396\",     EVENT_396_RPL_HOSTHIDDEN },\n    { \"400\",     EVENT_400_ERR_UNKNOWNERROR },\n    { \"401\",     EVENT_401_ERR_NOSUCHNICK },\n    { \"402\",     EVENT_402_ERR_NOSUCHSERVER },\n    { \"403\",     EVENT_403_ERR_NOSUCHCHANNEL },\n    { \"404\",     EVENT_404_ERR_CANNOTSENDTOCHAN },\n    { \"405\",     EVENT_405_ERR_TOOMANYCHANNELS },\n    { \"406\",     EVENT_406_ERR_WASNOSUCHNICK },\n    { \"407\",     EVENT_407_ERR_TOOMANYTARGETS },\n    { \"408\",     EVENT_408_ERR_NOSUCHSERVICE },\n    { \"409\",     EVENT_409_ERR_NOORIGIN },\n    { \"411\",     EVENT_411_ERR_NORECIPIENT },\n    { \"412\",     EVENT_412_ERR_NOTEXTTOSEND },\n    { \"413\",     EVENT_413_ERR_NOTOPLEVEL },\n    { \"414\",     EVENT_414_ERR_WILDTOPLEVEL },\n    { \"415\",     EVENT_415_ERR_BADMASK },\n    { \"421\",     EVENT_421_ERR_UNKNOWNCOMMAND },\n    { \"422\",     EVENT_422_ERR_NOMOTD },\n    { \"423\",     EVENT_423_ERR_NOADMININFO },\n    { \"424\",     EVENT_424_ERR_FILEERROR },\n    { \"431\",     EVENT_431_ERR_NONICKNAMEGIVEN },\n    { \"432\",     EVENT_432_ERR_ERRONEUSNICKNAME },\n    { \"433\",     EVENT_433_ERR_NICKNAMEINUSE },\n    { \"436\",     EVENT_436_ERR_NICKCOLLISION },\n    { \"441\",     EVENT_441_ERR_USERNOTINCHANNEL },\n    { \"442\",     EVENT_442_ERR_NOTONCHANNEL },\n    { \"443\",     EVENT_443_ERR_USERONCHANNEL },\n    { \"444\",     EVENT_444_ERR_NOLOGIN },\n    { \"445\",     EVENT_445_ERR_SUMMONDISABLED },\n    { \"446\",     EVENT_446_ERR_USERSDISABLED },\n    { \"451\",     EVENT_451_ERR_NOTREGISTERED },\n    { \"461\",     EVENT_461_ERR_NEEDMOREPARAMS },\n    { \"462\",     EVENT_462_ERR_ALREADYREGISTERED },\n    { \"463\",     EVENT_463_ERR_NOPERMFORHOST },\n    { \"464\",     EVENT_464_ERR_PASSWDMISMATCH },\n    { \"465\",     EVENT_465_ERR_YOUREBANNEDCREEP },\n    { \"467\",     EVENT_467_ERR_KEYSET },\n    { \"470\",     EVENT_470_ERR_LINKCHANNEL },\n    { \"471\",     EVENT_471_ERR_CHANNELISFULL },\n    { \"472\",     EVENT_472_ERR_UNKNOWNMODE },\n    { \"473\",     EVENT_473_ERR_INVITEONLYCHAN },\n    { \"474\",     EVENT_474_ERR_BANNEDFROMCHAN },\n    { \"475\",     EVENT_475_ERR_BADCHANNELKEY },\n    { \"476\",     EVENT_476_ERR_BADCHANMASK },\n    { \"477\",     EVENT_477_ERR_NEEDREGGEDNICK },\n    { \"478\",     EVENT_478_ERR_BANLISTFULL },\n    { \"481\",     EVENT_481_ERR_NOPRIVILEGES },\n    { \"482\",     EVENT_482_ERR_CHANOPRIVSNEEDED },\n    { \"483\",     EVENT_483_ERR_CANTKILLSERVER },\n    { \"485\",     EVENT_485_ERR_UNIQOPRIVSNEEDED },\n    { \"491\",     EVENT_491_ERR_NOOPERHOST },\n    { \"501\",     EVENT_501_ERR_UMODEUNKNOWNFLAG },\n    { \"502\",     EVENT_502_ERR_USERSDONTMATCH },\n    { \"524\",     EVENT_524_ERR_HELPNOTFOUND },\n    { \"704\",     EVENT_704_RPL_HELPSTART },\n    { \"705\",     EVENT_705_RPL_HELPTXT },\n    { \"706\",     EVENT_706_RPL_ENDOFHELP },\n    { \"900\",     EVENT_900_RPL_LOGGEDIN },\n    { \"901\",     EVENT_901_RPL_LOGGEDOUT },\n    { \"902\",     EVENT_902_ERR_NICKLOCKED },\n    { \"903\",     EVENT_903_RPL_SASLSUCCESS },\n    { \"904\",     EVENT_904_ERR_SASLFAIL },\n    { \"905\",     EVENT_905_ERR_SASLTOOLONG },\n    { \"906\",     EVENT_906_ERR_SASLABORTED },\n    { \"907\",     EVENT_907_ERR_SASLALREADY },\n    { \"908\",     EVENT_908_RPL_SASLMECHS },\n    { NULL,      EVENT_NONE }\n};\n\n/**\n * event_init() - Initialize an event structure\n * @event: Event structure to initialize\n * @ctx: IRC context structure\n *\n * Zeros out the event structure and associates it with an IRC context.\n * Sets the event type to EVENT_NONE.\n *\n * Return: 0 on success\n */\nint event_init(struct event *event, struct kirc_context *ctx)\n{\n    memset(event, 0, sizeof(*event));\n\n    event->ctx = ctx;\n    event->type = EVENT_NONE;\n\n    return 0;\n}\n\n/**\n * event_ctcp_parse() - Parse CTCP commands from message\n * @event: Event structure containing the message to parse\n *\n * Detects and parses CTCP (Client-To-Client Protocol) commands embedded\n * in PRIVMSG or NOTICE events. CTCP commands are delimited by \\001 characters.\n * Supports ACTION, VERSION, PING, TIME, CLIENTINFO, and DCC commands.\n * Updates the event type and extracts command parameters.\n *\n * Return: 0 on success\n */\nstatic int event_ctcp_parse(struct event *event)\n{\n    if (((event->type == EVENT_PRIVMSG) ||\n        (event->type == EVENT_NOTICE)) &&\n        (event->message[0] == '\\001')) {\n        char ctcp[MESSAGE_MAX_LEN];\n        size_t siz = sizeof(event->message);\n        size_t len = strnlen(event->message, siz);\n        size_t end = 1;\n\n        while ((end < len) && event->message[end] != '\\001') {\n            end++;\n        }\n\n        size_t ctcp_len = (end > 1) ? end - 1 : 0;\n\n        if (ctcp_len > 0) {\n            size_t copy_n;\n\n            if (ctcp_len < sizeof(ctcp)) {\n                copy_n = ctcp_len + 1;\n            } else {\n                copy_n = sizeof(ctcp);\n            }\n\n            safecpy(ctcp, event->message + 1, copy_n); \n\n            char *command = strtok(ctcp, \" \");\n            char *args = strtok(NULL, \"\");\n\n            if (command != NULL) {\n                if (strcmp(command, \"ACTION\") == 0) {\n                    event->type = EVENT_CTCP_ACTION;\n                    if (args != NULL) {\n                        siz = sizeof(event->message);\n                        safecpy(event->message, args, siz);\n                    } else {\n                        event->message[0] = '\\0';\n                    }\n                } else if (strcmp(command, \"VERSION\") == 0) {\n                    event->type = EVENT_CTCP_VERSION;\n                    if (args != NULL) {\n                        siz = sizeof(event->params);\n                        safecpy(event->params, args, siz);\n                    } else {\n                        event->params[0] = '\\0';\n                    }\n                } else if (strcmp(command, \"PING\") == 0) {\n                    event->type = EVENT_CTCP_PING;\n                    if (args != NULL) {\n                        siz = sizeof(event->message);\n                        safecpy(event->message, args, siz);\n                    } else {\n                        event->message[0] = '\\0';\n                    }\n                } else if (strcmp(command, \"TIME\") == 0) {\n                    event->type = EVENT_CTCP_TIME;\n                } else if (strcmp(command, \"CLIENTINFO\") == 0) {\n                    event->type = EVENT_CTCP_CLIENTINFO;\n                } else if (strcmp(command, \"DCC\") == 0) {\n                    event->type = EVENT_CTCP_DCC;\n                    if (args != NULL) {\n                        siz = sizeof(event->message);\n                        safecpy(event->message, args, siz);\n                    } else {\n                        event->message[0] = '\\0';\n                    }\n                } else {\n                    event->message[0] = '\\0';\n                }\n            }\n        }\n    }\n\n    return 0;\n}\n\n/**\n * event_parse() - Parse IRC message into event structure\n * @event: Event structure to populate\n * @line: Raw IRC message line to parse\n *\n * Parses a raw IRC protocol message into structured event data. Handles\n * special cases (PING, AUTHENTICATE, ERROR) and general IRC message format.\n * Extracts prefix, command, channel, nickname, and message components.\n * Determines event type from command using the event dispatch table.\n * Calls event_ctcp_parse() to detect CTCP commands.\n *\n * Return: 0 on success, -1 if parsing fails\n */\nint event_parse(struct event *event, char *line)\n{\n\n    char line_copy[MESSAGE_MAX_LEN];\n    safecpy(line_copy, line, sizeof(line_copy));\n    safecpy(event->raw, line, sizeof(event->raw));\n\n    if (strncmp(line, \"PING\", 4) == 0) {\n        event->type = EVENT_PING;\n        size_t message_n = sizeof(event->message);\n        safecpy(event->message, line + 6, message_n);\n        return 0;\n    }\n\n    if (strncmp(line, \"AUTHENTICATE +\", 14) == 0) {\n        event->type = EVENT_EXT_AUTHENTICATE;\n        return 0;\n    }\n\n    if (strncmp(line, \"ERROR\", 5) == 0) {\n        event->type = EVENT_ERROR;\n        size_t message_n = sizeof(event->message);\n        safecpy(event->message, line + 7, message_n);\n        return 0;\n    }\n\n    char *token = strtok(line_copy, \" \");\n\n    if (token == NULL) {\n        return -1;\n    }\n\n    char *prefix = token + 1;\n    char *suffix = strtok(NULL, \":\");\n\n    if (suffix == NULL) {\n        return -1;\n    }\n\n    char *message = strtok(NULL, \"\\r\");\n\n    if (message != NULL) {\n        size_t message_n = sizeof(event->message);\n        safecpy(event->message, message, message_n);\n    }\n\n    char *nickname = strtok(prefix, \"!\");\n\n    if (nickname != NULL) {\n        size_t nickname_n = sizeof(event->nickname);\n        safecpy(event->nickname, nickname, nickname_n);\n    }\n\n    char *command = strtok(suffix, \"#& \");\n\n    if (command != NULL) {\n        size_t command_n = sizeof(event->command);\n        safecpy(event->command, command, command_n);\n    }\n\n    char *channel = strtok(NULL, \" \\r\");\n\n    if (channel != NULL) {\n        size_t channel_n = sizeof(event->channel);\n        safecpy(event->channel, channel, channel_n);\n    }\n\n    char *params = strtok(NULL, \":\\r\");\n\n    if (params != NULL) {\n        size_t params_n = sizeof(event->params);\n        safecpy(event->params, params, params_n);\n    }\n\n    for (int i = 0; event_table[i].command != NULL; i++) {\n        if (strcmp(event->command, event_table[i].command) == 0) {\n            event->type = event_table[i].type;\n            break;\n        }\n    }\n\n    event_ctcp_parse(event);\n\n    return 0;\n}\n"
  },
  {
    "path": "src/handler.c",
    "content": "/*\n * handler.c\n * Unified event handler dispatch system\n * Author: Michael Czigler\n * License: MIT\n */\n\n#include \"handler.h\"\n\n/**\n * handler_default() - Set the default event handler\n * @handler: Handler structure to configure\n * @handler_fn: Function to call for unhandled events\n *\n * Registers a fallback handler that will be called for events that don't\n * have a specific handler registered.\n */\nvoid handler_default(struct handler *handler,\n        event_handler_fn handler_fn)\n{\n    handler->default_handler = handler_fn;\n}\n\n/**\n * handler_register() - Register a handler for an event type\n * @handler: Handler structure to configure\n * @type: Event type to register handler for\n * @handler_fn: Function to call when this event occurs\n *\n * Associates a specific handler function with an IRC event type.\n * The handler will be invoked via O(1) array lookup when the event\n * is dispatched. Does nothing if the event type is out of range.\n */\nvoid handler_register(struct handler *handler, enum event_type type,\n        event_handler_fn handler_fn)\n{\n    if (type < 0 || type >= KIRC_EVENT_TYPE_MAX) {\n        return;\n    }\n    \n    handler->handlers[type] = handler_fn;\n}\n\n/**\n * handler_init() - Initialize the handler dispatch system\n * @handler: Handler structure to initialize\n * @ctx: IRC context structure\n *\n * Initializes the handler system by zeroing the structure and associating\n * it with an IRC context. All event handlers are initially NULL.\n *\n * Return: 0 on success, -1 if handler or ctx is NULL\n */\nint handler_init(struct handler *handler, struct kirc_context *ctx)\n{\n    if ((handler == NULL) || (ctx == NULL)) {\n        return -1;\n    }\n\n    memset(handler, 0, sizeof(*handler));\n\n    handler->ctx = ctx;\n\n    return 0;\n}\n\n/**\n * handler_dispatch() - Dispatch event to appropriate handler\n * @handler: Handler structure containing registered handlers\n * @network: Network connection for the event\n * @event: Event to dispatch\n * @output: Output buffer for handler responses\n *\n * Dispatches an IRC event to its registered handler using O(1) array lookup.\n * If no specific handler is registered, calls the default handler (typically\n * displays raw message). Does nothing if required parameters are NULL.\n */\nvoid handler_dispatch(struct handler *handler, struct network *network,\n        struct event *event, struct output *output)\n{\n    if (handler == NULL || network == NULL || event == NULL) {\n        return;\n    }\n    \n    /* O(1) direct array lookup instead of O(n) linear search */\n    if (event->type >= 0 && event->type < KIRC_EVENT_TYPE_MAX) {\n        event_handler_fn fn = handler->handlers[event->type];\n        if (fn != NULL) {\n            fn(network, event, output);\n            return;\n        }\n    }\n\n    /* If no handler found, display raw message */\n    if (handler->default_handler != NULL) {\n        handler->default_handler(network, event, output);\n    }\n}\n"
  },
  {
    "path": "src/helper.c",
    "content": "/*\n * helper.c\n * Helper functions\n * Author: Michael Czigler\n * License: MIT\n */\n\n#include \"helper.h\"\n\n/**\n * safecpy() - Safe string copy with guaranteed null termination\n * @s1: Destination buffer\n * @s2: Source string\n * @n: Size of destination buffer\n *\n * Copies a string ensuring the destination is always null-terminated.\n * Safer alternative to strncpy. Copies at most n-1 characters and\n * always adds a null terminator.\n *\n * Return: 0 on success, -1 if parameters are invalid\n */\nint safecpy(char *s1, const char *s2, size_t n)\n{\n    if ((s1 == NULL) || (s2 == NULL) || (n == 0)) {\n        return -1;\n    }\n    \n    strncpy(s1, s2, n - 1);\n    s1[n - 1] = '\\0';\n    \n    return 0;\n}\n\n/**\n * memzero() - Securely zero memory\n * @s: Memory buffer to zero\n * @n: Number of bytes to zero\n *\n * Securely overwrites memory with zeros, preventing compiler optimization\n * from removing the operation. Used for clearing sensitive data like\n * passwords. Uses volatile pointer to ensure the write is not optimized away.\n *\n * Return: 0 on success, -1 if parameters are invalid\n */\nint memzero(void *s, size_t n)\n{\n    if ((s == NULL) || (n == 0)) {\n        return -1;\n    }\n\n    /* use volatile to prevent compiler optimization */\n    volatile unsigned char *p = s;\n\n    while (n--) {\n        *p++ = 0;\n    }\n    \n    return 0;\n}\n\n/**\n * find_message_end() - Find IRC message terminator in buffer\n * @buffer: Buffer containing IRC protocol data\n * @len: Length of the buffer\n *\n * Searches for the IRC message delimiter (\\r\\n) while properly handling\n * CTCP sequences. CTCP commands are delimited by \\001 characters, and\n * message terminators within CTCP sequences are ignored. This prevents\n * premature message splitting when CTCP data contains \\r\\n.\n *\n * Return: Pointer to \\r character of the terminator, or NULL if not found\n */\nchar *find_message_end(const char *buffer, size_t len)\n{\n    int ctcp_active = 0;\n\n    for (size_t i = 0; i + 1 < len; ++i) {\n        if (buffer[i] == '\\001') {\n            /* Toggle CTCP state when marker encountered */\n            ctcp_active = !ctcp_active;\n        } else if (buffer[i] == '\\r' && buffer[i + 1] == '\\n') {\n            /* Message end found only if not inside CTCP sequence */\n            if (!ctcp_active) {\n                return (char *)(buffer + i);\n            }\n        }\n    }\n\n    return NULL;\n}\n"
  },
  {
    "path": "src/main.c",
    "content": "/*\n * main.c\n * Main entry point for the kirc IRC client\n * Author: Michael Czigler\n * License: MIT\n */\n\n#include \"config.h\"\n#include \"ctcp.h\"\n#include \"dcc.h\"\n#include \"editor.h\"\n#include \"event.h\"\n#include \"handler.h\"\n#include \"helper.h\"\n#include \"network.h\"\n#include \"output.h\"\n#include \"protocol.h\"\n#include \"terminal.h\"\n#include \"transport.h\"\n\n/**\n * kirc_register_handlers() - Register all IRC event handlers\n * @handler: Handler structure to configure\n *\n * Registers handler functions for all supported IRC events and CTCP commands.\n * Sets the default handler to protocol_raw for displaying unhandled events.\n * Maps event types to their corresponding protocol and CTCP handler functions.\n */\nstatic void kirc_register_handlers(struct handler *handler) {\n    handler_default(handler, protocol_raw);\n    handler_register(handler, EVENT_CTCP_CLIENTINFO, ctcp_handle_clientinfo); \n    handler_register(handler, EVENT_CTCP_PING, ctcp_handle_ping);\n    handler_register(handler, EVENT_CTCP_TIME, ctcp_handle_time);\n    handler_register(handler, EVENT_CTCP_VERSION, ctcp_handle_version);\n    handler_register(handler, EVENT_CTCP_ACTION, protocol_ctcp_action);\n    handler_register(handler, EVENT_CTCP_DCC, protocol_ctcp_info);\n    handler_register(handler, EVENT_ERROR, protocol_error);\n    handler_register(handler, EVENT_EXT_CAP, protocol_info);\n    handler_register(handler, EVENT_EXT_AUTHENTICATE, protocol_authenticate);\n    handler_register(handler, EVENT_JOIN, protocol_join);\n    handler_register(handler, EVENT_KICK, protocol_info);\n    handler_register(handler, EVENT_MODE, protocol_info);\n    handler_register(handler, EVENT_NICK, protocol_nick);\n    handler_register(handler, EVENT_NOTICE, protocol_notice);\n    handler_register(handler, EVENT_PART, protocol_part);\n    handler_register(handler, EVENT_PING, protocol_ping);\n    handler_register(handler, EVENT_PRIVMSG, protocol_privmsg);\n    handler_register(handler, EVENT_QUIT, protocol_noop);\n    handler_register(handler, EVENT_TOPIC, protocol_info);\n    handler_register(handler, EVENT_001_RPL_WELCOME, protocol_welcome);\n    handler_register(handler, EVENT_002_RPL_YOURHOST, protocol_info);\n    handler_register(handler, EVENT_003_RPL_CREATED, protocol_info);\n    handler_register(handler, EVENT_004_RPL_MYINFO, protocol_info);\n    handler_register(handler, EVENT_005_RPL_BOUNCE, protocol_info);\n    handler_register(handler, EVENT_042_RPL_YOURID, protocol_info);\n    handler_register(handler, EVENT_200_RPL_TRACELINK, protocol_info);\n    handler_register(handler, EVENT_201_RPL_TRACECONNECTING, protocol_info);\n    handler_register(handler, EVENT_202_RPL_TRACEHANDSHAKE, protocol_info);\n    handler_register(handler, EVENT_203_RPL_TRACEUNKNOWN, protocol_info);\n    handler_register(handler, EVENT_204_RPL_TRACEOPERATOR, protocol_info);\n    handler_register(handler, EVENT_205_RPL_TRACEUSER, protocol_info);\n    handler_register(handler, EVENT_206_RPL_TRACESERVER, protocol_info);\n    handler_register(handler, EVENT_207_RPL_TRACESERVICE, protocol_info);\n    handler_register(handler, EVENT_208_RPL_TRACENEWTYPE, protocol_info);\n    handler_register(handler, EVENT_209_RPL_TRACECLASS, protocol_info);\n    handler_register(handler, EVENT_211_RPL_STATSLINKINFO, protocol_info);\n    handler_register(handler, EVENT_212_RPL_STATSCOMMANDS, protocol_info);\n    handler_register(handler, EVENT_213_RPL_STATSCLINE, protocol_info);\n    handler_register(handler, EVENT_215_RPL_STATSILINE, protocol_info);\n    handler_register(handler, EVENT_216_RPL_STATSKLINE, protocol_info);\n    handler_register(handler, EVENT_218_RPL_STATSYLINE, protocol_info);\n    handler_register(handler, EVENT_219_RPL_ENDOFSTATS, protocol_info);\n    handler_register(handler, EVENT_221_RPL_UMODEIS, protocol_info);\n    handler_register(handler, EVENT_234_RPL_SERVLIST, protocol_info);\n    handler_register(handler, EVENT_235_RPL_SERVLISTEND, protocol_info);\n    handler_register(handler, EVENT_241_RPL_STATSLLINE, protocol_info);\n    handler_register(handler, EVENT_242_RPL_STATSUPTIME, protocol_info);\n    handler_register(handler, EVENT_243_RPL_STATSOLINE, protocol_info);\n    handler_register(handler, EVENT_244_RPL_STATSHLINE, protocol_info);\n    handler_register(handler, EVENT_245_RPL_STATSSLINE, protocol_info);\n    handler_register(handler, EVENT_250_RPL_STATSCONN, protocol_info);\n    handler_register(handler, EVENT_251_RPL_LUSERCLIENT, protocol_info);\n    handler_register(handler, EVENT_252_RPL_LUSEROP, protocol_info);\n    handler_register(handler, EVENT_253_RPL_LUSERUNKNOWN, protocol_info);\n    handler_register(handler, EVENT_254_RPL_LUSERCHANNELS, protocol_info);\n    handler_register(handler, EVENT_255_RPL_LUSERME, protocol_info);\n    handler_register(handler, EVENT_256_RPL_ADMINME, protocol_info);\n    handler_register(handler, EVENT_257_RPL_ADMINLOC1, protocol_info);\n    handler_register(handler, EVENT_258_RPL_ADMINLOC2, protocol_info);\n    handler_register(handler, EVENT_259_RPL_ADMINEMAIL, protocol_info);\n    handler_register(handler, EVENT_261_RPL_TRACELOG, protocol_info);\n    handler_register(handler, EVENT_263_RPL_TRYAGAIN, protocol_info);\n    handler_register(handler, EVENT_265_RPL_LOCALUSERS, protocol_info);\n    handler_register(handler, EVENT_266_RPL_GLOBALUSERS, protocol_info);\n    handler_register(handler, EVENT_300_RPL_NONE, protocol_info);\n    handler_register(handler, EVENT_301_RPL_AWAY, protocol_info);\n    handler_register(handler, EVENT_302_RPL_USERHOST, protocol_info);\n    handler_register(handler, EVENT_303_RPL_ISON, protocol_info);\n    handler_register(handler, EVENT_305_RPL_UNAWAY, protocol_info);\n    handler_register(handler, EVENT_306_RPL_NOWAWAY, protocol_info);\n    handler_register(handler, EVENT_311_RPL_WHOISUSER, protocol_info);\n    handler_register(handler, EVENT_312_RPL_WHOISSERVER, protocol_info);\n    handler_register(handler, EVENT_313_RPL_WHOISOPERATOR, protocol_info);\n    handler_register(handler, EVENT_314_RPL_WHOWASUSER, protocol_info);\n    handler_register(handler, EVENT_315_RPL_ENDOFWHO, protocol_info);\n    handler_register(handler, EVENT_317_RPL_WHOISIDLE, protocol_info);\n    handler_register(handler, EVENT_318_RPL_ENDOFWHOIS, protocol_info);\n    handler_register(handler, EVENT_319_RPL_WHOISCHANNELS, protocol_info);\n    handler_register(handler, EVENT_322_RPL_LIST, protocol_info);\n    handler_register(handler, EVENT_323_RPL_LISTEND, protocol_info);\n    handler_register(handler, EVENT_324_RPL_CHANNELMODEIS, protocol_info);\n    handler_register(handler, EVENT_328_RPL_CHANNEL_URL, protocol_info);\n    handler_register(handler, EVENT_331_RPL_NOTOPIC, protocol_info);\n    handler_register(handler, EVENT_332_RPL_TOPIC, protocol_info);\n    handler_register(handler, EVENT_333_RPL_TOPICWHOTIME, protocol_info);\n    handler_register(handler, EVENT_341_RPL_INVITING, protocol_info);\n    handler_register(handler, EVENT_346_RPL_INVITELIST, protocol_info);\n    handler_register(handler, EVENT_347_RPL_ENDOFINVITELIST, protocol_info);\n    handler_register(handler, EVENT_348_RPL_EXCEPTLIST, protocol_info);\n    handler_register(handler, EVENT_349_RPL_ENDOFEXCEPTLIST, protocol_info);\n    handler_register(handler, EVENT_351_RPL_VERSION, protocol_info);\n    handler_register(handler, EVENT_352_RPL_WHOREPLY, protocol_info);\n    handler_register(handler, EVENT_353_RPL_NAMREPLY, protocol_info);\n    handler_register(handler, EVENT_364_RPL_LINKS, protocol_info);\n    handler_register(handler, EVENT_365_RPL_ENDOFLINKS, protocol_info);\n    handler_register(handler, EVENT_366_RPL_ENDOFNAMES, protocol_info);\n    handler_register(handler, EVENT_367_RPL_BANLIST, protocol_info);\n    handler_register(handler, EVENT_368_RPL_ENDOFBANLIST, protocol_info);\n    handler_register(handler, EVENT_369_RPL_ENDOFWHOWAS, protocol_info);\n    handler_register(handler, EVENT_371_RPL_INFO, protocol_info);\n    handler_register(handler, EVENT_372_RPL_MOTD, protocol_info);\n    handler_register(handler, EVENT_374_RPL_ENDOFINFO, protocol_info);\n    handler_register(handler, EVENT_375_RPL_MOTDSTART, protocol_info);\n    handler_register(handler, EVENT_376_RPL_ENDOFMOTD, protocol_info);\n    handler_register(handler, EVENT_381_RPL_YOUREOPER, protocol_info);\n    handler_register(handler, EVENT_382_RPL_REHASHING, protocol_info);\n    handler_register(handler, EVENT_383_RPL_YOURESERVICE, protocol_info);\n    handler_register(handler, EVENT_391_RPL_TIME, protocol_info);\n    handler_register(handler, EVENT_392_RPL_USERSSTART, protocol_info);\n    handler_register(handler, EVENT_393_RPL_USERS, protocol_info);\n    handler_register(handler, EVENT_394_RPL_ENDOFUSERS, protocol_info);\n    handler_register(handler, EVENT_395_RPL_NOUSERS, protocol_info);\n    handler_register(handler, EVENT_396_RPL_HOSTHIDDEN, protocol_info);\n    handler_register(handler, EVENT_400_ERR_UNKNOWNERROR, protocol_error);\n    handler_register(handler, EVENT_401_ERR_NOSUCHNICK, protocol_error);\n    handler_register(handler, EVENT_402_ERR_NOSUCHSERVER, protocol_error);\n    handler_register(handler, EVENT_403_ERR_NOSUCHCHANNEL, protocol_error);\n    handler_register(handler, EVENT_404_ERR_CANNOTSENDTOCHAN, protocol_error);\n    handler_register(handler, EVENT_405_ERR_TOOMANYCHANNELS, protocol_error);\n    handler_register(handler, EVENT_406_ERR_WASNOSUCHNICK, protocol_error);\n    handler_register(handler, EVENT_407_ERR_TOOMANYTARGETS, protocol_error);\n    handler_register(handler, EVENT_408_ERR_NOSUCHSERVICE, protocol_error);\n    handler_register(handler, EVENT_409_ERR_NOORIGIN, protocol_error);\n    handler_register(handler, EVENT_411_ERR_NORECIPIENT, protocol_error);\n    handler_register(handler, EVENT_412_ERR_NOTEXTTOSEND, protocol_error);\n    handler_register(handler, EVENT_413_ERR_NOTOPLEVEL, protocol_error);\n    handler_register(handler, EVENT_414_ERR_WILDTOPLEVEL, protocol_error);\n    handler_register(handler, EVENT_415_ERR_BADMASK, protocol_error);\n    handler_register(handler, EVENT_421_ERR_UNKNOWNCOMMAND, protocol_error);\n    handler_register(handler, EVENT_422_ERR_NOMOTD, protocol_error);\n    handler_register(handler, EVENT_423_ERR_NOADMININFO, protocol_error);\n    handler_register(handler, EVENT_424_ERR_FILEERROR, protocol_error);\n    handler_register(handler, EVENT_431_ERR_NONICKNAMEGIVEN, protocol_error);\n    handler_register(handler, EVENT_432_ERR_ERRONEUSNICKNAME, protocol_error);\n    handler_register(handler, EVENT_433_ERR_NICKNAMEINUSE, protocol_error);\n    handler_register(handler, EVENT_436_ERR_NICKCOLLISION, protocol_error);\n    handler_register(handler, EVENT_441_ERR_USERNOTINCHANNEL, protocol_error);\n    handler_register(handler, EVENT_442_ERR_NOTONCHANNEL, protocol_error);\n    handler_register(handler, EVENT_443_ERR_USERONCHANNEL, protocol_error);\n    handler_register(handler, EVENT_444_ERR_NOLOGIN, protocol_error);\n    handler_register(handler, EVENT_445_ERR_SUMMONDISABLED, protocol_error);\n    handler_register(handler, EVENT_446_ERR_USERSDISABLED, protocol_error);\n    handler_register(handler, EVENT_451_ERR_NOTREGISTERED, protocol_error);\n    handler_register(handler, EVENT_461_ERR_NEEDMOREPARAMS, protocol_error);\n    handler_register(handler, EVENT_462_ERR_ALREADYREGISTERED, protocol_error);\n    handler_register(handler, EVENT_463_ERR_NOPERMFORHOST, protocol_error);\n    handler_register(handler, EVENT_464_ERR_PASSWDMISMATCH, protocol_error);\n    handler_register(handler, EVENT_465_ERR_YOUREBANNEDCREEP, protocol_error);\n    handler_register(handler, EVENT_467_ERR_KEYSET, protocol_error);\n    handler_register(handler, EVENT_470_ERR_LINKCHANNEL, protocol_error);\n    handler_register(handler, EVENT_471_ERR_CHANNELISFULL, protocol_error);\n    handler_register(handler, EVENT_472_ERR_UNKNOWNMODE, protocol_error);\n    handler_register(handler, EVENT_473_ERR_INVITEONLYCHAN, protocol_error);\n    handler_register(handler, EVENT_474_ERR_BANNEDFROMCHAN, protocol_error);\n    handler_register(handler, EVENT_475_ERR_BADCHANNELKEY, protocol_error);\n    handler_register(handler, EVENT_476_ERR_BADCHANMASK, protocol_error);\n    handler_register(handler, EVENT_477_ERR_NEEDREGGEDNICK, protocol_error);\n    handler_register(handler, EVENT_478_ERR_BANLISTFULL, protocol_error);\n    handler_register(handler, EVENT_481_ERR_NOPRIVILEGES, protocol_error);\n    handler_register(handler, EVENT_482_ERR_CHANOPRIVSNEEDED, protocol_error);\n    handler_register(handler, EVENT_483_ERR_CANTKILLSERVER, protocol_error);\n    handler_register(handler, EVENT_485_ERR_UNIQOPRIVSNEEDED, protocol_error);\n    handler_register(handler, EVENT_491_ERR_NOOPERHOST, protocol_error);\n    handler_register(handler, EVENT_501_ERR_UMODEUNKNOWNFLAG, protocol_error);\n    handler_register(handler, EVENT_502_ERR_USERSDONTMATCH, protocol_error);\n    handler_register(handler, EVENT_524_ERR_HELPNOTFOUND, protocol_error);\n    handler_register(handler, EVENT_704_RPL_HELPSTART, protocol_info);\n    handler_register(handler, EVENT_705_RPL_HELPTXT, protocol_info);\n    handler_register(handler, EVENT_706_RPL_ENDOFHELP, protocol_info);\n    handler_register(handler, EVENT_900_RPL_LOGGEDIN, protocol_info);\n    handler_register(handler, EVENT_901_RPL_LOGGEDOUT, protocol_info);\n    handler_register(handler, EVENT_902_ERR_NICKLOCKED, protocol_error);\n    handler_register(handler, EVENT_903_RPL_SASLSUCCESS, protocol_info);\n    handler_register(handler, EVENT_904_ERR_SASLFAIL, protocol_error);\n    handler_register(handler, EVENT_905_ERR_SASLTOOLONG, protocol_error);\n    handler_register(handler, EVENT_906_ERR_SASLABORTED, protocol_error);\n    handler_register(handler, EVENT_907_ERR_SASLALREADY, protocol_error);\n    handler_register(handler, EVENT_908_RPL_SASLMECHS, protocol_info);\n}\n\n/**\n * kirc_run() - Main IRC client event loop\n * @ctx: IRC context structure with connection settings\n *\n * Initializes all subsystems (editor, transport, network, DCC, handlers,\n * terminal, output), establishes the IRC connection, and runs the main\n * event loop. Polls stdin and network socket for events, processing user\n * input and IRC messages. Handles terminal raw mode and cleanup on exit.\n *\n * Return: 0 on clean exit, -1 on initialization or runtime error\n */\nstatic int kirc_run(struct kirc_context *ctx)\n{\n    struct editor editor;\n\n    if (editor_init(&editor, ctx) < 0) {\n        fprintf(stderr, \"editor_init failed\\n\");\n        return -1;\n    }\n\n    struct transport transport;\n\n    if (transport_init(&transport, ctx) < 0) {\n        fprintf(stderr, \"transport_init failed\\n\");\n        return -1;\n    }\n\n    struct network network;\n\n    if (network_init(&network, &transport, ctx) < 0) {\n        fprintf(stderr, \"network_init failed\\n\");\n        return -1;\n    }\n\n    struct dcc dcc;\n\n    if (dcc_init(&dcc, ctx) < 0) {\n        fprintf(stderr, \"dcc_init failed\\n\");\n        network_free(&network);\n        return -1;\n    }\n\n    struct handler handler;\n\n    if (handler_init(&handler, ctx) < 0) {\n        fprintf(stderr, \"handler_init failed\\n\");\n        dcc_free(&dcc);\n        network_free(&network);\n        return -1;\n    }\n\n    kirc_register_handlers(&handler);\n\n    if (network_connect(&network) < 0) {\n        fprintf(stderr, \"network_connect failed\\n\");\n        dcc_free(&dcc);\n        network_free(&network);\n        return -1;\n    }\n\n    if (network_send_credentials(&network) < 0) {\n        fprintf(stderr, \"network_send_credentials failed\\n\");\n        dcc_free(&dcc);\n        network_free(&network);\n        return -1;\n    }\n\n    size_t siz = sizeof(ctx->target);\n    safecpy(ctx->target, ctx->channels[0], siz);\n\n    struct terminal terminal;\n\n    if (terminal_init(&terminal, ctx) < 0) {\n        fprintf(stderr, \"terminal_init failed\\n\");\n        dcc_free(&dcc);\n        network_free(&network);\n        return -1;\n    }\n\n    struct output output;\n\n    if (output_init(&output, ctx) < 0) {\n        fprintf(stderr, \"output_init failed\\n\");\n        dcc_free(&dcc);\n        network_free(&network);\n        return -1;\n    }\n\n    if (terminal_enable_raw(&terminal) < 0) {\n        fprintf(stderr, \"terminal_enable_raw failed\\n\");\n        terminal_disable_raw(&terminal);\n        dcc_free(&dcc);\n        network_free(&network);\n        return -1;\n    }\n\n    struct pollfd fds[2] = {\n        { .fd = STDIN_FILENO, .events = POLLIN },\n        { .fd = network.transport->fd, .events = POLLIN }\n    };\n\n    for (;;) {\n        int rc = poll(fds, 2, -1);\n\n        if (rc == -1) {\n            if (errno == EINTR) {\n                continue;\n            }\n\n            terminal_disable_raw(&terminal);\n            fprintf(stderr, \"poll error: %s\\n\",\n                strerror(errno));\n            break;\n        }\n\n        if (rc == 0) {\n            continue;\n        }\n\n        if (fds[0].revents & (POLLERR | POLLHUP | POLLNVAL)) {\n            terminal_disable_raw(&terminal);\n            fprintf(stderr, \"stdin error or hangup\\n\");\n            break;\n        }\n\n        if (fds[1].revents & (POLLERR | POLLHUP | POLLNVAL)) {\n            terminal_disable_raw(&terminal);\n            fprintf(stderr, \"network connection error or closed\\n\");\n            break;\n        }\n\n        if (fds[0].revents & POLLIN) {\n            editor_process_key(&editor);\n\n            if (editor.state == EDITOR_STATE_TERMINATE)\n                break;\n\n            if (editor.state == EDITOR_STATE_SEND) {\n                char *msg = editor_last_entry(&editor);\n                network_command_handler(&network, msg, &output);\n                output_flush(&output);\n            }\n\n            editor_handle(&editor);\n        }\n\n        if (fds[1].revents & POLLIN) {\n            int recv = network_receive(&network);\n\n            if (recv < 0) {\n                terminal_disable_raw(&terminal);\n                fprintf(stderr, \"network_receive error\\n\");\n                break;\n            }\n\n            if (recv > 0) {\n                char *msg = network.buffer;\n                size_t remaining = network.len;\n\n                for (;;) {\n                    char *eol = find_message_end(msg, remaining);\n                    if (!eol) break; /* no complete message found */\n\n                    *eol = '\\0';\n\n                    struct event event;\n                    event_init(&event, ctx);\n                    event_parse(&event, msg);\n\n                    handler_dispatch(&handler, &network, &event, &output);\n                    dcc_handle(&dcc, &network, &event);\n\n                    msg = eol + 2;\n                    remaining = network.buffer + network.len - msg;\n                }\n\n                if (remaining > 0) {\n                    if ((remaining < sizeof(network.buffer)) &&\n                        (msg > network.buffer)) {\n                        memmove(network.buffer, msg, remaining);\n                    }\n                    network.len = remaining;\n                } else {\n                    network.len = 0;\n                }\n\n                output_flush(&output);\n                editor_handle(&editor);\n            }\n            \n        }\n\n        dcc_process(&dcc);\n    }\n\n    terminal_disable_raw(&terminal);\n    dcc_free(&dcc);\n    network_free(&network);\n\n    return 0;\n}\n\n/**\n * main() - Program entry point\n * @argc: Argument count\n * @argv: Argument vector\n *\n * Initializes configuration from environment and command-line arguments,\n * runs the IRC client, and cleans up on exit. Ensures secure cleanup of\n * sensitive data.\n *\n * Return: EXIT_SUCCESS on normal termination, EXIT_FAILURE on error\n */\nint main(int argc, char *argv[])\n{\n    struct kirc_context ctx;\n\n    if (config_init(&ctx) < 0) {\n        return EXIT_FAILURE;\n    }\n\n    if (config_parse_args(&ctx, argc, argv) < 0) {\n        config_free(&ctx);\n        return EXIT_FAILURE;\n    }\n\n    if (kirc_run(&ctx) < 0) {\n        config_free(&ctx);\n        return EXIT_FAILURE;\n    }\n\n    config_free(&ctx);\n\n    return EXIT_SUCCESS;\n}\n"
  },
  {
    "path": "src/network.c",
    "content": "/*\n * network.c\n * Network connection management\n * Author: Michael Czigler\n * License: MIT\n */\n\n#include \"network.h\"\n\n/**\n * network_send() - Send formatted message to IRC server\n * @network: Network connection structure\n * @fmt: printf-style format string\n * @...: Variable arguments for format string\n *\n * Constructs and sends a formatted message to the IRC server through the\n * transport layer. Messages are limited to MESSAGE_MAX_LEN bytes.\n *\n * Return: 0 on success, -1 if network is NULL or send fails\n */\nint network_send(struct network *network, const char *fmt, ...)\n{\n    if (network == NULL) {\n        return -1;\n    }\n\n    if (network->transport == NULL) {\n        return -1;\n    }\n\n    char buf[MESSAGE_MAX_LEN];\n    va_list ap;\n\n    va_start(ap, fmt);\n    vsnprintf(buf, sizeof(buf), fmt, ap);\n    va_end(ap);\n\n    size_t len = strnlen(buf, sizeof(buf));\n\n    ssize_t nsent = transport_send(\n        network->transport, buf, len);\n\n    if (nsent < 0) {\n        return -1;\n    }\n\n    return 0;\n}\n\n/**\n * network_receive() - Receive data from IRC server\n * @network: Network connection structure\n *\n * Reads available data from the server into the network buffer. Handles\n * non-blocking I/O and partial reads. Updates the buffer length with\n * newly received data. Returns immediately if no data is available.\n *\n * Return: Number of bytes received, 0 if would block, -1 on error or disconnect\n */\nint network_receive(struct network *network)\n{\n    size_t buffer_n = sizeof(network->buffer) - 1;\n    ssize_t nread = transport_receive(\n        network->transport,\n        network->buffer + network->len,\n        buffer_n - network->len);\n\n    if (nread < 0) {\n        if (errno == EAGAIN || errno == EWOULDBLOCK) {\n            return 0;\n        } else {\n            return -1;\n        }\n    }\n\n    if (nread == 0) {\n        return -1;\n    }\n\n    network->len += (int)nread;\n    network->buffer[network->len] = '\\0';\n\n    return nread;\n}\n\n/**\n * network_connect() - Establish connection to IRC server\n * @network: Network connection structure\n *\n * Initiates connection to the IRC server using the transport layer.\n * Connection details (server, port) are taken from the context.\n *\n * Return: 0 on success, -1 on connection failure\n */\nint network_connect(struct network *network)\n{\n    return transport_connect(network->transport);\n}\n\n/**\n * network_send_private_msg() - Send private message to user\n * @network: Network connection structure\n * @msg: Message string in format \"username message text\"\n * @output: Output buffer for display feedback\n *\n * Parses and sends a private message (PRIVMSG) to a specific user.\n * Expected format: \"username message text\". Validates message length\n * and displays sent message to user.\n */\nstatic void network_send_private_msg(struct network *network,\n        char *msg, struct output *output)\n{\n    char msg_copy[MESSAGE_MAX_LEN];\n    safecpy(msg_copy, msg, sizeof(msg_copy));\n\n    char *username = strtok(msg_copy, \" \");\n\n    if (username == NULL) {\n        const char *err = \"error: message malformed\";\n        output_append(output, \"\\r\" CLEAR_LINE DIM \"%s\" RESET \"\\r\\n\", err);\n        return;\n    }\n\n    char *message = strtok(NULL, \"\");\n\n    if (message == NULL) {\n        const char *err = \"error: message malformed\";\n        output_append(output, \"\\r\" CLEAR_LINE DIM \"%s\" RESET \"\\r\\n\", err);\n        return;\n    }\n\n    size_t overhead = strlen(username) + 13;\n    size_t msg_len = strlen(message);\n    \n    if (overhead + msg_len >= MESSAGE_MAX_LEN) {\n        const char *err = \"error: message too long\";\n        output_append(output, \"\\r\" CLEAR_LINE DIM \"%s\" RESET \"\\r\\n\", err);\n        return;\n    }\n\n    network_send(network, \"PRIVMSG %s :%s\\r\\n\",\n        username, message);\n    output_append(output, \"\\rto \" BOLD_RED \"%s\" RESET \": %s\" CLEAR_LINE \"\\r\\n\",\n        username, message);\n}\n\n/**\n * network_send_topic() - Send TOPIC command for channel\n * @network: Network connection structure\n * @msg: Topic command arguments (channel and optional new topic)\n * @output: Output buffer for error messages\n *\n * Parses and sends IRC TOPIC command. If only channel is provided,\n * requests the current topic. If topic text is included, sets a new topic.\n * Displays usage error if channel is missing.\n */\nstatic void network_send_topic(struct network *network,\n        char *msg, struct output *output)\n{\n    char msg_copy[MESSAGE_MAX_LEN];\n    safecpy(msg_copy, msg, sizeof(msg_copy));\n\n    char *channel = strtok(msg_copy, \" \");\n\n    if (channel == NULL) {\n        const char *err = \"usage: /topic <channel> [<topic>]\";\n        output_append(output, \"\\r\" CLEAR_LINE DIM \"%s\" RESET \"\\r\\n\", err);\n        return;\n    }\n \n    char *topic = strtok(NULL, \"\");\n\n    if (topic == NULL) {\n        network_send(network, \"TOPIC %s\\r\\n\", channel);\n        return;\n    }\n\n    network_send(network, \"TOPIC %s :%s\\r\\n\", channel, topic);\n}\n\n/**\n * network_send_ctcp_action() - Send CTCP ACTION to current target\n * @network: Network connection structure\n * @msg: Action text to send\n * @output: Output buffer for display feedback\n *\n * Sends a CTCP ACTION message (/me command) to the current target channel\n * or user. Validates message length and requires a target to be set.\n */\nstatic void network_send_ctcp_action(struct network *network,\n        char *msg, struct output *output)\n{\n    if (network->ctx->target[0] != '\\0') {\n        /* Validate total message length: \"PRIVMSG \" + target + \" :\\001ACTION \" + msg + \"\\001\\r\\n\" */\n        size_t overhead = 10 + strlen(network->ctx->target) + 10 + 3; /* \"PRIVMSG \" + target + \" :\\001ACTION \" + \"\\001\\r\\n\" */\n        size_t msg_len = strlen(msg);\n        \n        if (overhead + msg_len >= MESSAGE_MAX_LEN) {\n            const char *err = \"error: message too long\";\n            output_append(output, \"\\r\" CLEAR_LINE DIM \"%s\" RESET \"\\r\\n\", err);\n            return;\n        }\n\n        network_send(network,\n            \"PRIVMSG %s :\\001ACTION %s\\001\\r\\n\",\n            network->ctx->target, msg);\n        output_append(output, \"\\rto \\u2022 \" BOLD \"%s\" RESET \": %s\" CLEAR_LINE \"\\r\\n\",\n            network->ctx->target, msg);\n    } else {\n        const char *err = \"error: no channel set\";\n        output_append(output, \"\\r\" CLEAR_LINE DIM \"%s\" RESET \"\\r\\n\", err);\n    }\n}\n\n/**\n * network_send_ctcp_command() - Send CTCP command to target\n * @network: Network connection structure\n * @msg: Message in format \"target command [args]\"\n * @output: Output buffer for display feedback\n *\n * Parses and sends a CTCP command to a specified target. Expected format:\n * \"target command [args]\". Validates input and provides usage feedback.\n */\nstatic void network_send_ctcp_command(struct network *network,\n        char *msg, struct output *output)\n{\n    char msg_copy[MESSAGE_MAX_LEN];\n    safecpy(msg_copy, msg, sizeof(msg_copy));\n\n    char *target = strtok(msg_copy, \" \");\n\n    if (target == NULL) {\n        const char *err = \"usage: /ctcp <nick> <command>\";\n        output_append(output, \"\\r\" CLEAR_LINE DIM \"%s\" RESET \"\\r\\n\", err);\n        return;\n    }\n \n    char *command = strtok(NULL, \"\");\n\n    if (command == NULL) {\n        const char *err = \"usage: /ctcp <nick> <command>\";\n        output_append(output, \"\\r\" CLEAR_LINE DIM \"%s\" RESET \"\\r\\n\", err);\n        return;\n    }\n\n    network_send(network, \"PRIVMSG %s :\\001%s\\001\\r\\n\",\n        target, command);\n    output_append(output, \"\\rctcp: \" BOLD_RED \"%s\" RESET \": %s\" CLEAR_LINE \"\\r\\n\",\n        target, command);\n}\n\n/**\n * network_send_channel_msg() - Send message to current target channel\n * @network: Network connection structure\n * @msg: Message text to send\n * @output: Output buffer for display feedback\n *\n * Sends a channel message to the currently set target. Validates message\n * length and requires a target to be set. Displays sent message locally.\n */\nstatic void network_send_channel_msg(\n        struct network *network, char *msg, struct output *output)\n{\n    if (network->ctx->target[0] != '\\0') {\n        /* Validate total message length: \"PRIVMSG \" + target + \" :\" + msg + \"\\r\\n\" */\n        size_t overhead = 10 + strlen(network->ctx->target) + 3 + 2; /* \"PRIVMSG \" + target + \" :\" + \"\\r\\n\" */\n        size_t msg_len = strlen(msg);\n        \n        if (overhead + msg_len >= MESSAGE_MAX_LEN) {\n            const char *err = \"error: message too long\";\n            output_append(output, \"\\r\" CLEAR_LINE DIM \"%s\" RESET \"\\r\\n\", err);\n            return;\n        }\n\n        network_send(network, \"PRIVMSG %s :%s\\r\\n\",\n            network->ctx->target, msg);\n        output_append(output, \"\\rto \" BOLD \"%s\" RESET \": %s\" CLEAR_LINE \"\\r\\n\",\n            network->ctx->target, msg);\n    } else {\n        const char *err = \"error: no channel set\";\n        output_append(output, \"\\r\" CLEAR_LINE DIM \"%s\" RESET \"\\r\\n\", err);\n    }\n}\n\n/**\n * network_command_handler() - Process user input commands\n * @network: Network connection structure\n * @msg: User input message to process\n * @output: Output buffer for display feedback\n *\n * Routes user input to appropriate handlers based on prefix:\n *   / - IRC commands (/set, /me, /ctcp, or raw IRC commands)\n *   @ - Private messages to users\n *   (default) - Channel messages to current target\n * Parses commands and delegates to specialized send functions.\n *\n * Return: 0 on success\n */\nint network_command_handler(struct network *network, char *msg, struct output *output)\n{\n    switch (msg[0]) {\n    case '/':  /* system command message */\n        switch (msg[1]) {\n        case 's':  /* set target (channel or nickname) */\n            if (strncmp(msg + 1, \"set \", 4) == 0) {\n                size_t siz = sizeof(network->ctx->target);\n                safecpy(network->ctx->target, msg + 5, siz);\n            } else {\n                network_send(network, \"%s\\r\\n\", msg + 1);\n            }\n            break;\n\n        case 't':  /* send TOPIC command */\n            if (strncmp(msg + 1, \"topic \", 6) == 0) {\n                network_send_topic(network, msg + 7, output);\n            } else {\n                network_send(network, \"%s\\r\\n\", msg + 1);\n            }\n            break;\n\n        case 'm':  /* send CTCP ACTION to target */\n            if (strncmp(msg + 1, \"me \", 3) == 0) {\n                network_send_ctcp_action(network, msg + 4, output);\n            } else {\n                network_send(network, \"%s\\r\\n\", msg + 1);\n            }\n            break;\n\n        case 'c':  /* send CTCP command */\n            if (strncmp(msg + 1, \"ctcp \", 5) == 0) {\n                network_send_ctcp_command(network, msg + 6, output);\n            } else {\n                network_send(network, \"%s\\r\\n\", msg + 1);\n            }\n            break;\n\n        default:  /* send raw server command */\n            network_send(network, \"%s\\r\\n\", msg + 1);\n            break;  \n        }\n        break;\n\n    case '@':  /* private message */\n        network_send_private_msg(network, msg + 1, output);\n        break;\n\n    default:  /* channel message */\n        network_send_channel_msg(network, msg, output);\n        break;\n    }\n\n    return 0;\n}\n\n/**\n * network_send_credentials() - Send authentication credentials to server\n * @network: Network connection structure\n *\n * Sends initial authentication sequence to IRC server including SASL\n * capability negotiation (if configured), NICK, USER commands, and\n * optionally PASS (server password). Initiates SASL authentication\n * for EXTERNAL or PLAIN mechanisms.\n *\n * Return: 0 on success, -1 if SASL mechanism is unrecognized\n */\nint network_send_credentials(struct network *network)\n{\n    if (network->ctx->mechanism != SASL_NONE) {\n        network_send(network, \"CAP REQ :sasl\\r\\n\");\n    }\n\n    network_send(network, \"NICK %s\\r\\n\",\n        network->ctx->nickname);\n    \n    char *username, *realname;\n    \n    if (network->ctx->username[0] != '\\0') {\n        username = network->ctx->username;\n    } else {\n        username = network->ctx->nickname;\n    }\n\n    if (network->ctx->realname[0] != '\\0') {\n        realname = network->ctx->realname;\n    } else {\n        realname = network->ctx->nickname;\n    }\n\n    network_send(network, \"USER %s - - :%s\\r\\n\",\n        username, realname);\n\n    if (network->ctx->mechanism != SASL_NONE) {\n        if (network->ctx->mechanism == SASL_EXTERNAL) {\n            network_send(network, \"AUTHENTICATE EXTERNAL\\r\\n\");\n        } else if (network->ctx->mechanism == SASL_PLAIN) {\n            network_send(network, \"AUTHENTICATE PLAIN\\r\\n\");\n        } else {\n            fprintf(stderr, \"unrecognized SASL mechanism\\n\");\n            return -1;\n        }\n    }\n\n    if (network->ctx->password[0] != '\\0') {\n        network_send(network, \"PASS %s\\r\\n\",\n            network->ctx->password);\n    }\n\n    return 0;\n}\n\n/**\n * network_init() - Initialize network connection structure\n * @network: Network structure to initialize\n * @transport: Transport layer instance\n * @ctx: IRC context structure\n *\n * Initializes the network management structure, associating it with a\n * transport layer and IRC context. Zeros the buffer and state.\n *\n * Return: 0 on success, -1 if any parameter is NULL\n */\nint network_init(struct network *network, \n        struct transport *transport, struct kirc_context *ctx)\n{\n    if ((network == NULL) || (transport == NULL) || (ctx == NULL)) {\n        return -1;\n    }\n\n    memset(network, 0, sizeof(*network));\n\n    network->ctx = ctx;\n    network->transport = transport;\n\n    return 0;\n}\n\n/**\n * network_free() - Free network resources\n * @network: Network structure to clean up\n *\n * Releases resources associated with the network connection by freeing\n * the transport layer.\n *\n * Return: 0 on success, -1 if transport cleanup fails\n */\nint network_free(struct network *network)\n{\n    if (transport_free(network->transport) < 0) {\n        return -1;\n    }\n\n    return 0;\n}\n"
  },
  {
    "path": "src/output.c",
    "content": "/*\n * output.c\n * Buffered output for improved terminal responsiveness\n * Author: Michael Czigler\n * License: MIT\n */\n\n#include \"output.h\"\n\n/**\n * output_init() - Initialize output buffer\n * @output: Output structure to initialize\n * @ctx: IRC context structure\n *\n * Initializes the output buffer system, preparing it for buffered writes\n * to stdout. Associates the output with an IRC context.\n *\n * Return: 0 on success, -1 if output or ctx is NULL\n */\nint output_init(struct output *output,\n        struct kirc_context *ctx)\n{\n    if ((output == NULL) || (ctx == NULL)) {\n        return -1;\n    }\n\n    memset(output, 0, sizeof(*output));\n\n    output->ctx = ctx;\n    output->len = 0;\n    output->buffer[0] = '\\0';\n\n    return 0;\n}\n\n/**\n * output_append() - Append formatted text to output buffer\n * @output: Output buffer structure\n * @fmt: printf-style format string\n * @...: Variable arguments for format string\n *\n * Appends formatted text to the output buffer. Automatically flushes if\n * buffer becomes nearly full (within 256 bytes of capacity). If text\n * doesn't fit, flushes buffer and retries. Handles truncation for\n * extremely long messages.\n *\n * Return: 0 on success, -1 on error\n */\nint output_append(struct output *output,\n        const char *fmt, ...)\n{\n    if (output == NULL || fmt == NULL) {\n        return -1;\n    }\n    \n    /* Check if buffer has space for at least a reasonable message */\n    if (output->len >= KIRC_OUTPUT_BUFFER_SIZE - 256) {\n        /* Auto-flush if buffer is nearly full */\n        output_flush(output);\n    }\n    \n    va_list ap;\n    va_start(ap, fmt);\n    \n    int remaining = KIRC_OUTPUT_BUFFER_SIZE - output->len;\n    int written = vsnprintf(output->buffer + output->len,\n        remaining, fmt, ap);\n    \n    va_end(ap);\n    \n    if (written < 0) {\n        return -1;\n    }\n    \n    if (written >= remaining) {\n        /* Message was truncated, flush and try again */\n        output_flush(output);\n        \n        va_start(ap, fmt);\n        written = vsnprintf(output->buffer,\n            KIRC_OUTPUT_BUFFER_SIZE, fmt, ap);\n        va_end(ap);\n        \n        if (written < 0 || written >= KIRC_OUTPUT_BUFFER_SIZE) {\n            return -1;\n        }\n        \n        output->len = written;\n    } else {\n        output->len += written;\n    }\n    \n    return 0;\n}\n\n/**\n * output_flush() - Write buffered output to stdout\n * @output: Output buffer structure\n *\n * Writes the entire buffered content to stdout in a single system call\n * and resets the buffer. Improves performance by reducing write() calls.\n * Ignores write errors.\n */\nvoid output_flush(struct output *output)\n{\n    if (output == NULL || output->len == 0) {\n        return;\n    }\n    \n    /* Write entire buffer in one system call */\n    ssize_t written = write(STDOUT_FILENO,\n        output->buffer, output->len);\n    \n    (void)written;  /* Ignore errors */\n    \n    output->len = 0;\n    output->buffer[0] = '\\0';\n}\n\n/**\n * output_clear() - Clear output buffer without writing\n * @output: Output buffer structure\n *\n * Discards all buffered content without writing it to stdout. Resets\n * the buffer to empty state.\n */\nvoid output_clear(struct output *output)\n{\n    if (output == NULL) {\n        return;\n    }\n    \n    output->len = 0;\n    output->buffer[0] = '\\0';\n}\n\n/**\n * output_pending() - Check if output buffer has data\n * @output: Output buffer structure\n *\n * Returns the number of bytes currently buffered but not yet written\n * to stdout.\n *\n * Return: Number of buffered bytes, or 0 if output is NULL\n */\nint output_pending(struct output *output)\n{\n    if (output == NULL) {\n        return 0;\n    }\n    \n    return output->len;\n}\n"
  },
  {
    "path": "src/protocol.c",
    "content": "/*\n * protocol.c\n * IRC protocol handling\n * Author: Michael Czigler\n * License: MIT\n */\n\n#include \"protocol.h\"\n\n/**\n * protocol_get_time() - Get formatted timestamp string\n *\n * Returns the current local time as a formatted string using the format\n * defined by KIRC_TIMESTAMP_FORMAT. Uses a static buffer, so the returned\n * pointer is valid until the next call.\n *\n * Return: Pointer to static timestamp string\n */\nstatic const char *protocol_get_time(void)\n{\n    static char timestamp[KIRC_TIMESTAMP_SIZE];\n    time_t current;\n    time(&current);\n    struct tm *info = localtime(&current);\n    strftime(timestamp, KIRC_TIMESTAMP_SIZE,\n        KIRC_TIMESTAMP_FORMAT, info);\n    return timestamp;\n}\n\n/**\n * protocol_noop() - No-operation event handler\n * @network: Network connection (unused)\n * @event: Event structure (unused)\n * @output: Output buffer (unused)\n *\n * Handler that does nothing. Used for events that should be silently\n * ignored (e.g., JOIN, PART, QUIT).\n */\nvoid protocol_noop(struct network *network, struct event *event, struct output *output)\n{\n    (void)network;\n    (void)event;\n    (void)output;\n}\n\n/**\n * protocol_ping() - Handle PING requests from server\n * @network: Network connection structure\n * @event: Event containing ping data\n * @output: Output buffer (unused)\n *\n * Responds to server PING messages with appropriate PONG reply to\n * maintain connection keepalive.\n */\nvoid protocol_ping(struct network *network, struct event *event, struct output *output)\n{\n    (void)output;\n    network_send(network, \"PONG :%s\\r\\n\", event->message);\n}\n\n/**\n * network_authenticate_plain() - Send SASL PLAIN authentication\n * @network: Network connection structure\n *\n * Sends base64-encoded SASL PLAIN authentication data in chunks of\n * AUTH_CHUNK_SIZE bytes. If the total length is an exact multiple of\n * the chunk size, sends a final \"+\" to indicate completion.\n */\nstatic void network_authenticate_plain(struct network *network)\n{   \n    int len = strlen(network->ctx->auth);\n\n    for (int offset = 0; offset < len; offset += AUTH_CHUNK_SIZE) {\n        char chunk[AUTH_CHUNK_SIZE + 1];\n        safecpy(chunk, network->ctx->auth + offset, sizeof(chunk));\n        network_send(network, \"AUTHENTICATE %s\\r\\n\", chunk);\n    }\n    \n    if ((len > 0) && (len % AUTH_CHUNK_SIZE == 0)) {\n        network_send(network, \"AUTHENTICATE +\\r\\n\");\n    }\n}\n\n/**\n * protocol_authenticate() - Handle SASL authentication challenge\n * @network: Network connection structure\n * @event: Authentication event (unused)\n * @output: Output buffer (unused)\n *\n * Responds to server's AUTHENTICATE challenge based on configured SASL\n * mechanism (PLAIN or EXTERNAL). Sends authentication data and finalizes\n * capability negotiation with CAP END.\n */\nvoid protocol_authenticate(struct network *network, struct event *event, struct output *output)\n{\n    (void)event;\n    (void)output;\n\n    if (network->ctx->auth[0] == '\\0') {\n        network_send(network, \"AUTHENTICATE '*'\\r\\n\");\n        return;\n    }\n\n    switch (network->ctx->mechanism) {\n    case SASL_PLAIN:\n        network_authenticate_plain(network);\n        break;\n    \n    case SASL_EXTERNAL:\n        network_send(network, \"AUTHENTICATE +\\r\\n\");\n        break;\n    \n    default:\n        network_send(network, \"AUTHENTICATE '*'\\r\\n\");\n        break;\n    }\n    \n    if (network->ctx->mechanism != SASL_NONE) {\n        network_send(network, \"CAP END\\r\\n\");\n    }\n}\n\n/**\n * protocol_welcome() - Handle RPL_WELCOME (001) server message\n * @network: Network connection structure\n * @event: Welcome event (unused)\n * @output: Output buffer (unused)\n *\n * Processes the server welcome message by automatically joining all\n * configured channels from the context.\n */\nvoid protocol_welcome(struct network *network, struct event *event, struct output *output)\n{\n    (void)event;\n    (void)output;\n\n    for (int i = 0; network->ctx->channels[i][0] != '\\0'; ++i) {\n        network_send(network, \"JOIN %s\\r\\n\",\n            network->ctx->channels[i]);\n    }\n}\n\n/**\n * protocol_raw() - Display raw IRC message\n * @network: Network connection (unused)\n * @event: Event containing raw message\n * @output: Output buffer for display\n *\n * Displays the complete raw IRC message with timestamp. Used as the\n * default handler for events without specific handlers.\n */\nvoid protocol_raw(struct network *network, struct event *event, struct output *output)\n{\n    (void)network;\n\n    output_append(output, \"\\r\" CLEAR_LINE DIM \"%s\" RESET\n        \" \" REVERSE \"%s\" RESET \"\\r\\n\",\n        protocol_get_time(), event->raw);\n}\n\n/**\n * protocol_info() - Display informational message\n * @network: Network connection (unused)\n * @event: Event containing message\n * @output: Output buffer for display\n *\n * Displays an informational IRC message with timestamp in dimmed text.\n * Used for most server replies and status messages.\n */\nvoid protocol_info(struct network *network, struct event *event, struct output *output)\n{\n    (void)network;\n\n    output_append(output, \"\\r\" CLEAR_LINE DIM \"%s %s\" RESET \"\\r\\n\",\n        protocol_get_time(), event->message);\n}\n\n/**\n * protocol_error() - Display error message\n * @network: Network connection (unused)\n * @event: Event containing error message\n * @output: Output buffer for display\n *\n * Displays an IRC error message with timestamp. Error text is shown\n * in bold red to distinguish from normal messages.\n */\nvoid protocol_error(struct network *network, struct event *event, struct output *output)\n{\n    (void)network;\n\n    output_append(output, \"\\r\" CLEAR_LINE DIM \"%s\" RESET\n        \" \" BOLD_RED \"%s\" RESET \"\\r\\n\",\n        protocol_get_time(), event->message);\n}\n\n/**\n * protocol_notice() - Display NOTICE message\n * @network: Network connection (unused)\n * @event: Event containing notice details\n * @output: Output buffer for display\n *\n * Displays a NOTICE message with the sender's nickname in bold blue\n * and timestamp.\n */\nvoid protocol_notice(struct network *network, struct event *event, struct output *output)\n{\n    (void)network;\n\n    output_append(output, \"\\r\" CLEAR_LINE DIM \"%s\" RESET\n        \" \" BOLD_BLUE \"%s\" RESET \" %s\\r\\n\",\n        protocol_get_time(), event->nickname, event->message);\n}\n\n/**\n * protocol_privmsg_direct() - Display direct private message\n * @network: Network connection (unused)\n * @event: Event containing private message\n * @output: Output buffer for display\n *\n * Displays a private message sent directly to the user (not in a channel).\n * Shows sender in bold blue with message text in blue.\n */\nstatic void protocol_privmsg_direct(struct network *network, struct event *event, struct output *output)\n{\n    (void)network;\n\n    output_append(output, \"\\r\" CLEAR_LINE DIM \"%s\" RESET\n        \" \" BOLD_BLUE \"%s\" RESET \" \" BLUE \"%s\" RESET \"\\r\\n\",\n        protocol_get_time(), event->nickname, event->message);\n}\n\n/**\n * protocol_privmsg_indirect() - Display channel message\n * @network: Network connection (unused)\n * @event: Event containing channel message\n * @output: Output buffer for display\n *\n * Displays a message sent to a channel. Shows nickname in bold,\n * channel name in brackets, and message text.\n */\nstatic void protocol_privmsg_indirect(struct network *network, struct event *event, struct output *output)\n{\n    (void)network;\n\n    output_append(output, \"\\r\" CLEAR_LINE DIM \"%s\" RESET\n        \" \" BOLD \"%s\" RESET \" [%s]: %s\\r\\n\",\n        protocol_get_time(), event->nickname, event->channel, event->message);\n\n}\n\n/**\n * protocol_privmsg() - Route PRIVMSG to appropriate handler\n * @network: Network connection structure\n * @event: Event containing PRIVMSG details\n * @output: Output buffer for display\n *\n * Determines whether a PRIVMSG is a direct message or channel message\n * by comparing the target with the user's nickname, then routes to the\n * appropriate display function.\n */\nvoid protocol_privmsg(struct network *network, struct event *event, struct output *output)\n{\n    char *channel = event->channel;\n    char *nickname = event->ctx->nickname;\n\n    if (strcmp(channel, nickname) == 0) {\n        protocol_privmsg_direct(network, event, output);\n    } else {\n        protocol_privmsg_indirect(network, event, output);\n    }\n}\n\n/**\n * protocol_nick() - Handle nickname change events\n * @network: Network connection (unused)\n * @event: Event containing nickname change information\n * @output: Output buffer for display\n *\n * Handles NICK events by displaying the nickname change. If the change\n * is for the current user, updates the context with the new nickname.\n * Shows appropriate messages for self and other users.\n */\nvoid protocol_nick(struct network *network, struct event *event, struct output *output)\n{\n    (void)network;\n\n    struct kirc_context *ctx = event->ctx;\n    const char *timestamp = protocol_get_time();\n    \n    if (strcmp(event->nickname, ctx->nickname) == 0) {\n        size_t siz = sizeof(ctx->nickname) - 1;\n        strncpy(ctx->nickname, event->message, siz);\n        ctx->nickname[siz] = '\\0';\n        output_append(output, \"\\r\" CLEAR_LINE\n            DIM \"%s you are now known as %s\" RESET \"\\r\\n\",\n            timestamp, event->message);\n    } else {\n        output_append(output, \"\\r\" CLEAR_LINE\n            DIM \"%s %s is now known as %s\" RESET \"\\r\\n\",\n            timestamp, event->nickname, event->message);\n    }\n\n}\n\n/**\n * protocol_join() - Handle JOIN channel event\n * @network: Network connection (unused)\n * @event: Event containing JOIN details\n * @output: Output buffer for display\n *\n * Processes IRC JOIN events. If the joining user is the client itself,\n * displays a confirmation message. Otherwise, delegates to protocol_noop().\n */\nvoid protocol_join(struct network *network, struct event *event, struct output *output)\n{\n    (void)network;\n\n    if (strcmp(event->nickname, event->ctx->nickname) == 0) {\n        output_append(output, \"\\r\" CLEAR_LINE\n            DIM \"kirc: you've joined %s\" RESET \"\\r\\n\",\n            event->channel);\n    } else {\n        protocol_noop(network, event, output);\n    }\n}\n\n/**\n * protocol_part() - Handle PART channel event\n * @network: Network connection (unused)\n * @event: Event containing PART details\n * @output: Output buffer for display\n *\n * Processes IRC PART events. If the leaving user is the client itself,\n * displays a confirmation message. Otherwise, delegates to protocol_noop().\n */\nvoid protocol_part(struct network *network, struct event *event, struct output *output)\n{\n    (void)network;\n\n    if (strcmp(event->nickname, event->ctx->nickname) == 0) {\n        output_append(output, \"\\r\" CLEAR_LINE\n            DIM \"kirc: you left %s\" RESET \"\\r\\n\",\n            event->channel);\n    } else {\n        protocol_noop(network, event, output);\n    }\n}\n\n/**\n * protocol_ctcp_action() - Display CTCP ACTION message\n * @network: Network connection (unused)\n * @event: Event containing ACTION details\n * @output: Output buffer for display\n *\n * Displays a CTCP ACTION message (/me command) with a bullet point\n * prefix, timestamp, nickname, and action text in dimmed format.\n */\nvoid protocol_ctcp_action(struct network *network, struct event *event, struct output *output)\n{\n    (void)network;\n\n    output_append(output, \"\\r\" CLEAR_LINE DIM \"%s \\u2022 %s %s\" RESET \"\\r\\n\",\n        protocol_get_time(), event->nickname, event->message);\n}\n\n/**\n * protocol_ctcp_info() - Display CTCP informational message\n * @network: Network connection (unused)\n * @event: Event containing CTCP details\n * @output: Output buffer for display\n *\n * Displays CTCP protocol messages (CLIENTINFO, DCC, PING, TIME, VERSION)\n * with sender's nickname in bold blue and appropriate label. Shows\n * parameters or message content if present.\n */\nvoid protocol_ctcp_info(struct network *network, struct event *event, struct output *output)\n{\n    (void)network;\n\n    const char *label = \"\";\n    const char *timestamp = protocol_get_time();\n\n    switch(event->type) {\n    case EVENT_CTCP_CLIENTINFO:\n        label = \"CLIENTINFO\";\n        break;\n\n    case EVENT_CTCP_DCC:\n        label = \"DCC\";\n        break;\n\n    case EVENT_CTCP_PING:\n        label = \"PING\";\n        break;\n\n    case EVENT_CTCP_TIME:\n        label = \"TIME\";\n        break;\n\n    case EVENT_CTCP_VERSION: \n        label = \"VERSION\";\n        break;\n\n    default:\n        label = \"CTCP\";\n        break;\n    }\n\n    if (event->params[0] != '\\0') {\n        output_append(output, \"\\r\" CLEAR_LINE DIM \"%s \" RESET\n            BOLD_BLUE \"%s\" RESET \" %s: %s\\r\\n\",\n            timestamp, event->nickname, label, event->params);\n    } else if (event->message[0] != '\\0') {\n        output_append(output, \"\\r\" CLEAR_LINE DIM \"%s \" RESET\n            BOLD_BLUE \"%s\" RESET \" %s: %s\\r\\n\",\n            timestamp, event->nickname, label, event->message);\n    } else {\n        output_append(output, \"\\r\" CLEAR_LINE DIM \"%s \" RESET\n            BOLD_BLUE \"%s\" RESET \" %s\\r\\n\",\n            timestamp, event->nickname, label);\n    }\n}\n"
  },
  {
    "path": "src/terminal.c",
    "content": "/*\n * terminal.c\n * Terminal interface and display management\n * Author: Michael Czigler\n * License: MIT\n */\n\n#include \"terminal.h\"\n\n/**\n * terminal_get_cursor_column() - Query terminal cursor column position\n * @in_fd: File descriptor to read response from\n * @out_fd: File descriptor to send query to\n *\n * Queries the terminal for the current cursor position using ANSI escape\n * sequences and parses the response to extract the column number.\n *\n * Return: Current column position (1-based), or -1 on error\n */\nstatic int terminal_get_cursor_column(int in_fd, int out_fd)\n{\n    char buf[32];\n    unsigned int i = 0;\n    int row = 0, col = 0;\n\n    /* Request cursor position */\n    if (write(out_fd, CURSOR_POS, 4) != 4) {\n        return -1;\n    }\n\n    while (i < sizeof(buf) - 1) {\n        if (read(in_fd, buf + i, 1) != 1) {\n            break;\n        }\n        if (buf[i] == 'R') {\n            break;\n        }\n        i++;\n    }\n\n    buf[i] = '\\0';\n\n    if (buf[0] != 27 || buf[1] != '[') {\n        return -1;\n    }\n\n    if (sscanf(buf + 2, \"%d;%d\", &row, &col) != 2) {\n        return -1;\n    }\n\n    return col;\n}\n\n/**\n * terminal_columns() - Determine terminal width in columns\n * @tty_fd: TTY file descriptor\n *\n * Determines the terminal width using ioctl(TIOCGWINSZ) if available,\n * or falls back to manual probing by moving cursor and querying position.\n * Returns KIRC_DEFAULT_COLUMNS if detection fails.\n *\n * Return: Terminal width in columns\n */\nint terminal_columns(int tty_fd)\n{\n    struct winsize ws;\n\n    if (ioctl(tty_fd, TIOCGWINSZ, &ws) != -1 && ws.ws_col > 0) {\n        return ws.ws_col;\n    }\n\n    /* Fallback: manual probing */\n    int start = terminal_get_cursor_column(tty_fd,\n        STDOUT_FILENO);\n\n    if (start == -1) {\n        return KIRC_DEFAULT_COLUMNS;\n    }\n\n    /* Move far right */\n    if (write(STDOUT_FILENO, \"\\x1b[999C\", 6) != 6) {\n        return KIRC_DEFAULT_COLUMNS;\n    }\n\n    int end = terminal_get_cursor_column(tty_fd,\n        STDOUT_FILENO);\n\n    if (end == -1) {\n        return KIRC_DEFAULT_COLUMNS;\n    }\n\n    /* Move cursor back */\n    if (end > start) {\n        char seq[32];\n        int diff = end - start;\n        snprintf(seq, sizeof(seq), \"\\x1b[%dD\", diff);\n\n        if (write(STDOUT_FILENO, seq,\n            strnlen(seq, sizeof(seq))) < 32) {\n            return KIRC_DEFAULT_COLUMNS;\n        }\n    }\n\n    return (end > 0) ? end : KIRC_DEFAULT_COLUMNS;\n}\n\n/**\n * terminal_enable_raw() - Enable raw terminal mode\n * @terminal: Terminal state structure\n *\n * Configures the terminal for raw (non-canonical) input mode, disabling\n * line buffering, echo, and signal generation. Saves the original terminal\n * settings for later restoration. Required for character-by-character input\n * processing.\n *\n * Return: 0 on success, -1 if stdin is not a TTY or configuration fails\n */\nint terminal_enable_raw(struct terminal *terminal)\n{\n    if (!isatty(STDIN_FILENO)) {\n        return -1;\n    }\n\n    if (terminal->raw_mode_enabled) {\n        return 0;\n    }\n    \n    if (tcgetattr(STDIN_FILENO,\n        &terminal->original) == -1) {\n        return -1;\n    }\n\n    struct termios raw = terminal->original;\n\n    raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);\n    raw.c_oflag &= ~(OPOST);\n    raw.c_cflag |= (CS8);\n    raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);\n    raw.c_cc[VMIN]  = 1;\n    raw.c_cc[VTIME] = 0;\n\n    if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) {\n        return -1;\n    }\n\n    terminal->raw_mode_enabled = 1;\n\n    return 0;\n}\n\n/**\n * terminal_disable_raw() - Restore normal terminal mode\n * @terminal: Terminal state structure\n *\n * Restores the terminal to its original configuration, re-enabling\n * line buffering, echo, and signal generation. Should be called before\n * program exit or when switching away from raw mode.\n */\nvoid terminal_disable_raw(struct terminal *terminal)\n{\n    if (!terminal->raw_mode_enabled) {\n        return;\n    }\n\n    tcsetattr(STDIN_FILENO, TCSAFLUSH, &terminal->original);\n    terminal->raw_mode_enabled = 0;\n}\n\n/**\n * terminal_init() - Initialize terminal management structure\n * @terminal: Terminal structure to initialize\n * @ctx: IRC context structure\n *\n * Initializes the terminal state structure, zeroing all fields and\n * associating it with an IRC context.\n *\n * Return: 0 on success, -1 if terminal or ctx is NULL\n */\nint terminal_init(struct terminal *terminal,\n        struct kirc_context *ctx)\n{\n    if ((terminal == NULL) || (ctx == NULL)) {\n        return -1;\n    }\n\n    memset(terminal, 0, sizeof(*terminal));   \n\n    terminal->ctx = ctx;\n\n    return 0;\n}\n"
  },
  {
    "path": "src/transport.c",
    "content": "/*\n * transport.c\n * Data transport layer\n * Author: Michael Czigler\n * License: MIT\n */\n\n#include \"transport.h\"\n\n/**\n * poll_wait_write() - Wait for socket to be writable\n * @fd: File descriptor to poll\n * @timeout_ms: Timeout in milliseconds\n *\n * Polls a file descriptor for write readiness, handling EINTR by retrying.\n * Used during non-blocking connect() to wait for connection completion.\n *\n * Return: 0 if ready for write, -1 on timeout or error\n */\nstatic int poll_wait_write(int fd, int timeout_ms)\n{\n    struct pollfd pfd;\n    pfd.fd = fd;\n    pfd.events = POLLOUT;\n\n    for (;;) {\n        int rc = poll(&pfd, 1, timeout_ms);\n        if (rc > 0)\n            return 0;  /* ready */\n        if (rc == 0)\n            return -1;  /* timeout */\n        if (errno == EINTR)\n            continue;\n        return -1;  /* error */\n    }\n}\n\n/**\n * transport_send() - Send data over transport connection\n * @transport: Transport structure\n * @buffer: Data buffer to send\n * @len: Number of bytes to send\n *\n * Writes data to the transport file descriptor. Does not guarantee\n * that all data is sent in one call (partial writes possible).\n *\n * Return: Number of bytes written, or -1 on error\n */\nssize_t transport_send(struct transport *transport,\n        const char *buffer, size_t len)\n{\n    if (transport == NULL)\n        return -1;\n\n    if (transport->fd < 0)\n        return -1;\n\n    ssize_t rc = write(transport->fd,\n        buffer, len);\n\n    if (rc < 0)\n        return -1;\n\n    return rc;\n}\n\n/**\n * transport_receive() - Receive data from transport connection\n * @transport: Transport structure\n * @buffer: Buffer to store received data\n * @len: Maximum number of bytes to receive\n *\n * Reads data from the transport file descriptor. Handles non-blocking\n * I/O by distinguishing between EAGAIN/EWOULDBLOCK (no data available)\n * and other errors. Returns -1 on connection close (nread == 0).\n *\n * Return: Number of bytes read, 0 if would block, -1 on error or disconnect\n */\nssize_t transport_receive(struct transport *transport,\n        char *buffer, size_t len)\n{\n    if (transport == NULL)\n        return -1;\n\n    if (transport->fd < 0)\n        return -1;\n\n    ssize_t nread = read(transport->fd,\n        buffer, len);\n\n    if (nread < 0) {\n        if (errno == EAGAIN || errno == EWOULDBLOCK) {\n            return 0;\n        } else {\n            return -1;\n        } \n    }\n\n    if (nread == 0) {\n        return -1; \n    }\n\n    return nread;\n}\n\n\n/**\n * transport_connect() - Establish transport connection to server\n * @transport: Transport structure with server details\n *\n * Creates a socket and connects to the server specified in the transport\n * context (server name and port). Uses getaddrinfo() for address resolution,\n * supporting both IPv4 and IPv6. Configures socket for non-blocking I/O\n * and handles EINPROGRESS for asynchronous connection with timeout.\n *\n * Return: 0 on successful connection, -1 on failure\n */\nint transport_connect(struct transport *transport)\n{\n    struct addrinfo hints, *res = NULL, *p = NULL;\n    memset(&hints, 0, sizeof(hints));\n    hints.ai_socktype = SOCK_STREAM;\n    hints.ai_family = AF_UNSPEC;\n\n    int status = getaddrinfo(transport->ctx->server,\n        transport->ctx->port, &hints, &res);\n\n    if (status != 0) {\n        fprintf(stderr, \"getaddrinfo: %s\\n\",\n            gai_strerror(status));\n        return -1;\n    }\n\n    transport->fd = -1;\n\n    for (p = res; p; p = p->ai_next) {\n        int fd = socket(p->ai_family, p->ai_socktype,\n            p->ai_protocol);\n\n        if (fd < 0) {\n            continue;\n        }\n\n        int flags = fcntl(fd, F_GETFL, 0);\n        \n        if (flags < 0) {\n            close(fd);\n            continue;\n        }\n\n        if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {\n            close(fd);\n            continue;\n        }\n\n        int rc = connect(fd, p->ai_addr, p->ai_addrlen);\n\n        if (rc == 0) {\n            transport->fd = fd;\n            break;\n        }\n\n        if (errno == EINPROGRESS) {\n            int timeout_ms = KIRC_TIMEOUT_MS;\n            int rc = poll_wait_write(fd, timeout_ms);\n\n            if (rc == 0) {\n\n                int soerr = 0;\n                socklen_t slen = sizeof(soerr);\n\n                getsockopt(fd, SOL_SOCKET, SO_ERROR,\n                    &soerr, &slen);\n\n                if (soerr == 0) {\n                    transport->fd = fd;\n                    break;\n                }\n            }\n        }\n\n        close(fd);\n    }\n\n    freeaddrinfo(res);\n\n    if (transport->fd < 0) {\n        return -1;\n    }\n\n    return 0;\n}\n\n/**\n * transport_init() - Initialize transport structure\n * @transport: Transport structure to initialize\n * @ctx: IRC context containing connection details\n *\n * Initializes the transport layer, zeroing the structure and setting\n * the file descriptor to -1 (not connected). Associates transport with\n * IRC context for server and port information.\n *\n * Return: 0 on success, -1 if transport or ctx is NULL\n */\nint transport_init(struct transport *transport,\n        struct kirc_context *ctx)\n{\n    if ((transport == NULL) || (ctx == NULL)) {\n        return -1;\n    }\n\n    memset(transport, 0, sizeof(*transport));\n\n    transport->ctx = ctx;\n    transport->fd = -1;\n\n    return 0;\n}\n\n/**\n * transport_free() - Close and cleanup transport connection\n * @transport: Transport structure to free\n *\n * Closes the transport socket if open and resets the file descriptor\n * to -1. Should be called during cleanup to release resources.\n *\n * Return: 0 on success, -1 if transport is NULL\n */\nint transport_free(struct transport *transport)\n{\n    if (transport == NULL) {\n        return -1;\n    }\n\n    if (transport->fd != -1) {\n        close(transport->fd);\n        transport->fd = -1;\n    }\n    return 0;\n}\n"
  },
  {
    "path": "src/utf8.c",
    "content": "/*\n * utf8.c\n * UTF-8 utilities\n * Author: Michael Czigler\n * License: MIT\n */\n \n#include \"utf8.h\"\n\n/**\n * utf8_prev_char_len() - Get byte length of previous UTF-8 character\n * @s: UTF-8 string\n * @pos: Current byte position in string\n *\n * Calculates the byte length of the UTF-8 character immediately before\n * the given position by walking backward to find the start byte. Handles\n * multi-byte sequences by identifying continuation bytes (0x80-0xBF).\n *\n * Return: Number of bytes in previous character, or 0 if at start\n */\nint utf8_prev_char_len(const char *s, int pos)\n{\n    if (pos <= 0) {\n        return 0;\n    }\n\n    /* Walk back to find start of UTF-8 character */\n    int prev = pos - 1;\n    while (prev > 0 && ((unsigned char)s[prev] & 0xC0) == 0x80) {\n        prev--;\n    }\n\n    return pos - prev;\n}\n\n/**\n * utf8_next_char_len() - Get byte length of next UTF-8 character\n * @s: UTF-8 string\n * @pos: Current byte position in string\n * @maxlen: Maximum valid position in string\n *\n * Determines the byte length of the UTF-8 character at the given position\n * using mbrtowc(). Handles invalid sequences by treating them as single\n * bytes. Safely handles null characters and position boundaries.\n *\n * Return: Number of bytes in next character (1-4), or 0 if at end\n */\nint utf8_next_char_len(const char *s, int pos, int maxlen)\n{\n    if (pos >= maxlen) {\n        return 0;\n    }\n\n    mbstate_t st;\n    memset(&st, 0, sizeof(st));\n    \n    wchar_t wc;\n    size_t ret = mbrtowc(&wc, s + pos, maxlen - pos, &st);\n    \n    if (ret == (size_t)-1 || ret == (size_t)-2) {\n        /* Invalid UTF-8 sequence, treat as single byte */\n        return 1;\n    }\n    \n    if (ret == 0) {\n        /* Null character */\n        return 1;\n    }\n    \n    return (int)ret;\n}\n\n/**\n * utf8_validate() - Validate UTF-8 encoding of string\n * @s: String to validate\n * @len: Number of bytes to validate\n *\n * Checks whether a byte sequence is valid UTF-8 by attempting to decode\n * each character using mbrtowc(). Rejects strings with invalid sequences,\n * incomplete multi-byte sequences, or other encoding errors.\n *\n * Return: 1 if valid UTF-8, 0 if invalid or incomplete\n */\nint utf8_validate(const char *s, int len)\n{\n    mbstate_t st;\n    memset(&st, 0, sizeof(st));\n    int pos = 0;\n    \n    while (pos < len) {\n        wchar_t wc;\n        size_t ret = mbrtowc(&wc, s + pos, len - pos, &st);\n        \n        if (ret == (size_t)-1) {\n            /* Invalid sequence */\n            return 0;\n        }\n        \n        if (ret == (size_t)-2) {\n            /* Incomplete sequence at end */\n            return 0;\n        }\n        \n        if (ret == 0) {\n            /* Null encountered */\n            break;\n        }\n        \n        pos += ret;\n    }\n    \n    return 1;\n}\n"
  }
]