~singpolyma/biboumi

5bbd34a3a909fa904ee4402f01dac6bac59211b1 — Florent Le Coz 10 years ago 87aaacd
Add an XmppParser, and Stanza classes

Generate events on stanza and stream open/close.
Create Stanza and serialize them.

Note: XML namespaces are not handled yet.
M CMakeLists.txt => CMakeLists.txt +1 -1
@@ 23,7 23,7 @@ add_library(network STATIC ${source_network})
file(GLOB source_irc
  src/irc/*.[hc]pp)
add_library(irc STATIC ${source_irc})
target_link_libraries(irc network ${CRYPTO++_LIBRARIES})
target_link_libraries(irc network ${CRYPTO++_LIBRARIES} expatpp)

#
## xmpplib

A src/xmpp/xmpp_parser.cpp => src/xmpp/xmpp_parser.cpp +103 -0
@@ 0,0 1,103 @@
#include <xmpp/xmpp_parser.hpp>
#include <xmpp/xmpp_stanza.hpp>

#include <iostream>

XmppParser::XmppParser():
  level(0),
  current_node(nullptr)
{
}

XmppParser::~XmppParser()
{
  if (this->current_node)
    delete this->current_node;
}

void XmppParser::startElement(const XML_Char* name, const XML_Char** attribute)
{
  level++;

  XmlNode* new_node = new XmlNode(name, this->current_node);
  if (this->current_node)
    this->current_node->add_child(new_node);
  this->current_node = new_node;
  for (size_t i = 0; attribute[i]; i += 2)
    this->current_node->set_attribute(attribute[i], attribute[i+1]);
  if (this->level == 1)
    this->stream_open_event(*this->current_node);
}

void XmppParser::endElement(const XML_Char* name)
{
  assert(name == this->current_node->get_name());
  level--;
  this->current_node->close();
  if (level == 1)
    {
      this->stanza_event(*this->current_node);
    }
  if (level == 0)
    {
      this->stream_close_event(*this->current_node);
      delete this->current_node;
      this->current_node = nullptr;
    }
  else
    this->current_node = this->current_node->get_parent();
  if (level == 1)
    this->current_node->delete_all_children();
}

void XmppParser::charData(const XML_Char* data, int len)
{
  if (this->current_node->has_children())
    this->current_node->get_last_child()->set_tail(std::string(data, len));
  else
    this->current_node->set_inner(std::string(data, len));
}

void XmppParser::startNamespace(const XML_Char* prefix, const XML_Char* uri)
{
  std::cout << "startNamespace: " << prefix << ":" << uri << std::endl;
  this->namespaces.emplace(std::make_pair(prefix, uri));
}

void XmppParser::stanza_event(const Stanza& stanza) const
{
  for (const auto& callback: this->stanza_callbacks)
    callback(stanza);
}

void XmppParser::stream_open_event(const XmlNode& node) const
{
  for (const auto& callback: this->stream_open_callbacks)
    callback(node);
}

void XmppParser::stream_close_event(const XmlNode& node) const
{
  for (const auto& callback: this->stream_close_callbacks)
    callback(node);
}

void XmppParser::endNamespace(const XML_Char* coucou)
{
  std::cout << "endNamespace: " << coucou << std::endl;
}

void XmppParser::add_stanza_callback(std::function<void(const Stanza&)>&& callback)
{
  this->stanza_callbacks.emplace_back(std::move(callback));
}

void XmppParser::add_stream_open_callback(std::function<void(const XmlNode&)>&& callback)
{
  this->stream_open_callbacks.emplace_back(std::move(callback));
}

void XmppParser::add_stream_close_callback(std::function<void(const XmlNode&)>&& callback)
{
  this->stream_close_callbacks.emplace_back(std::move(callback));
}

A src/xmpp/xmpp_parser.hpp => src/xmpp/xmpp_parser.hpp +107 -0
@@ 0,0 1,107 @@
#ifndef XMPP_PARSER_INCLUDED
# define XMPP_PARSER_INCLUDED

#include <functional>
#include <stack>

#include <xmpp/xmpp_stanza.hpp>

#include <expatpp.h>

/**
 * A SAX XML parser that builds XML nodes and spawns events when a complete
 * stanza is received (an element of level 2), or when the document is
 * opened (an element of level 1)
 *
 * After a stanza_event has been spawned, we delete the whole stanza. This
 * means that even with a very long document (in XMPP the document is
 * potentially infinite), the memory then is never exhausted as long as each
 * stanza is reasonnably short.
 *
 * TODO: enforce the size-limit for the stanza (limit the number of childs
 * it can contain). For example forbid the parser going further than level
 * 20 (arbitrary number here), and each XML node to have more than 15 childs
 * (arbitrary number again).
 */
class XmppParser: public expatpp
{
public:
  explicit XmppParser();
  ~XmppParser();

public:
  /**
   * Add one callback for the various events that this parser can spawn.
   */
  void add_stanza_callback(std::function<void(const Stanza&)>&& callback);
  void add_stream_open_callback(std::function<void(const XmlNode&)>&& callback);
  void add_stream_close_callback(std::function<void(const XmlNode&)>&& callback);

private:
  /**
   * Called when a new XML element has been opened. We instanciate a new
   * XmlNode and set it as our current node. The parent of this new node is
   * the previous "current" node. We have all the element's attributes in
   * this event.
   *
   * We spawn a stream_event with this node if this is a level-1 element.
   */
  void startElement(const XML_Char* name, const XML_Char** attribute);
  /**
   * Called when an XML element has been closed. We close the current_node,
   * set our current_node as the parent of the current_node, and if that was
   * a level-2 element we spawn a stanza_event with this node.
   *
   * And we then delete the stanza (and everything under it, its children,
   * attribute, etc).
   */
  void endElement(const XML_Char* name);
  /**
   * Some inner or tail data has been parsed
   */
  void charData(const XML_Char* data, int len);
  /**
   * TODO use that.
   */
  void startNamespace(const XML_Char* prefix, const XML_Char* uri);
  /**
   * TODO and that.
   */
  void endNamespace(const XML_Char* prefix);
  /**
   * Calls all the stanza_callbacks one by one.
   */
  void stanza_event(const Stanza& stanza) const;
  /**
   * Calls all the stream_open_callbacks one by one. Note: the passed node is not
   * closed yet.
   */
  void stream_open_event(const XmlNode& node) const;
  /**
   * Calls all the stream_close_callbacks one by one.
   */
  void stream_close_event(const XmlNode& node) const;

  /**
   * The current depth in the XML document
   */
  size_t level;
  /**
   * The deepest XML node opened but not yet closed (to which we are adding
   * new children, inner or tail)
   */
  XmlNode* current_node;
  /**
   * A list of callbacks to be called on an *_event, receiving the
   * concerned Stanza/XmlNode.
   */
  std::vector<std::function<void(const Stanza&)>> stanza_callbacks;
  std::vector<std::function<void(const XmlNode&)>> stream_open_callbacks;
  std::vector<std::function<void(const XmlNode&)>> stream_close_callbacks;
  /**
   * TODO: also use that.
   */
  std::stack<std::pair<std::string, std::string>> namespaces;
};

#endif // XMPP_PARSER_INCLUDED

A src/xmpp/xmpp_stanza.cpp => src/xmpp/xmpp_stanza.cpp +127 -0
@@ 0,0 1,127 @@
#include <xmpp/xmpp_stanza.hpp>

#include <iostream>

XmlNode::XmlNode(const std::string& name, XmlNode* parent):
  name(name),
  parent(parent),
  closed(false)
{
}

XmlNode::XmlNode(const std::string& name):
  XmlNode(name, nullptr)
{
}

XmlNode::~XmlNode()
{
  this->delete_all_children();
}

void XmlNode::delete_all_children()
{
  for (auto& child: this->children)
    {
      delete child;
    }
  this->children.clear();
}

void XmlNode::set_attribute(const std::string& name, const std::string& value)
{
  this->attributes[name] = value;
}

void XmlNode::set_tail(const std::string& data)
{
  this->tail = data;
}

void XmlNode::set_inner(const std::string& data)
{
  this->inner = data;
}

void XmlNode::add_child(XmlNode* child)
{
  this->children.push_back(child);
}

void XmlNode::add_child(XmlNode&& child)
{
  XmlNode* new_node = new XmlNode(std::move(child));
  this->add_child(new_node);
}

XmlNode* XmlNode::get_last_child() const
{
  return this->children.back();
}

void XmlNode::close()
{
  if (this->closed)
    throw std::runtime_error("Closing an already closed XmlNode");
  this->closed = true;
}

XmlNode* XmlNode::get_parent() const
{
  return this->parent;
}

const std::string& XmlNode::get_name() const
{
  return this->name;
}

std::string XmlNode::to_string() const
{
  std::string res("<");
  res += this->name;
  for (const auto& it: this->attributes)
    res += " " + it.first + "='" + it.second + "'";
  if (this->closed && !this->has_children() && this->inner.empty())
    res += "/>";
  else
    {
      res += ">" + this->inner;
      for (const auto& child: this->children)
        res += child->to_string();
      if (this->closed)
        {
          res += "</" + this->name + ">";
        }
    }
  res += this->tail;
  return res;
}

void XmlNode::display() const
{
  std::cout << this->to_string() << std::endl;
}

bool XmlNode::has_children() const
{
  return !this->children.empty();
}

const std::string& XmlNode::operator[](const std::string& name) const
{
  try
    {
      const auto& value = this->attributes.at(name);
      return value;
    }
  catch (const std::out_of_range& e)
    {
      throw AttributeNotFound();
    }
}

std::string& XmlNode::operator[](const std::string& name)
{
  return this->attributes[name];
}

A src/xmpp/xmpp_stanza.hpp => src/xmpp/xmpp_stanza.hpp +106 -0
@@ 0,0 1,106 @@
#ifndef XMPP_STANZA_INCLUDED
# define XMPP_STANZA_INCLUDED

#include <unordered_map>
#include <string>
#include <vector>

#include <expatpp.h>

/**
 * Raised on operator[] when the attribute does not exist
 */
class AttributeNotFound: public std::exception
{
};

/**
 * Represent an XML node. It has
 * - A parent XML node (in the case of the first-level nodes, the parent is
     nullptr)
 * - zero, one or more children XML nodes
 * - A name
 * - attributes
 * - inner data (inside the node)
 * - tail data (just after the node)
 */
class XmlNode
{
public:
  explicit XmlNode(const std::string& name, XmlNode* parent);
  explicit XmlNode(const std::string& name);
  XmlNode(XmlNode&& node):
    name(std::move(node.name)),
    parent(std::move(node.parent)),
    closed(std::move(node.closed)),
    attributes(std::move(node.attributes)),
    children(std::move(node.children)),
    inner(std::move(node.inner)),
    tail(std::move(node.tail))
  {
    node.parent = nullptr;
  }

  ~XmlNode();

  void delete_all_children();
  void set_attribute(const std::string& name, const std::string& value);
  /**
   * Set the content of the tail, that is the text just after this node
   */
  void set_tail(const std::string& data);
  /**
   * Set the content of the inner, that is the text inside this node
   */
  void set_inner(const std::string& data);
  void add_child(XmlNode* child);
  void add_child(XmlNode&& child);
  XmlNode* get_last_child() const;
  /**
   * Mark this node as closed, nothing else
   */
  void close();
  XmlNode* get_parent() const;
  const std::string& get_name() const;
  /**
   * Serialize the stanza into a string
   */
  std::string to_string() const;
  void display() const;
  /**
   * Whether or not this node has at least one child (if not, this is a leaf
   * node)
   */
  bool has_children() const;
  /**
   * Gets the value for the given attribute, raises AttributeNotFound if the
   * node as no such attribute.
   */
  const std::string& operator[](const std::string& name) const;
  /**
   * Use this to set an attribute's value, like node["id"] = "12";
   */
  std::string& operator[](const std::string& name);

private:
  std::string name;
  XmlNode* parent;
  bool closed;
  std::unordered_map<std::string, std::string> attributes;
  std::vector<XmlNode*> children;
  std::string inner;
  std::string tail;

  XmlNode(const XmlNode&) = delete;
  XmlNode& operator=(const XmlNode&) = delete;
  XmlNode& operator=(XmlNode&&) = delete;
};

/**
 * An XMPP stanza is just an XML node of level 2 in the XMPP document (the
 * level 1 ones are the <stream::stream/>, and the ones about 2 are just the
 * content of the stanzas)
 */
typedef XmlNode Stanza;

#endif // XMPP_STANZA_INCLUDED