]> git.ipfire.org Git - thirdparty/snapper.git/commitdiff
- handle content-length of stomp in zypper plugin 919/head
authorArvin Schnell <aschnell@suse.de>
Fri, 5 Jul 2024 14:07:46 +0000 (16:07 +0200)
committerArvin Schnell <aschnell@suse.de>
Fri, 5 Jul 2024 14:07:46 +0000 (16:07 +0200)
15 files changed:
Makefile.am
configure.ac
package/snapper.changes
stomp/.gitignore [new file with mode: 0644]
stomp/Makefile.am [new file with mode: 0644]
stomp/Stomp.cc [new file with mode: 0644]
stomp/Stomp.h [new file with mode: 0644]
stomp/testsuite/.gitignore [new file with mode: 0644]
stomp/testsuite/Makefile.am [new file with mode: 0644]
stomp/testsuite/escape.cc [new file with mode: 0644]
stomp/testsuite/read1.cc [new file with mode: 0644]
stomp/testsuite/write1.cc [new file with mode: 0644]
zypp-plugin/Makefile.am
zypp-plugin/zypp-plugin.cc
zypp-plugin/zypp-plugin.h

index 69b66787a46fce3b0b05af6e04bd4757c9ca80fe..fe6a1b9e20b4fad611a38ca538f0f5da547211c2 100644 (file)
@@ -3,7 +3,7 @@
 #
 
 SUBDIRS = snapper dbus server client scripts pam data doc po examples          \
-       testsuite testsuite-real testsuite-cmp zypp-plugin
+       testsuite testsuite-real testsuite-cmp stomp zypp-plugin
 
 AUTOMAKE_OPTIONS = foreign dist-xz no-dist-gzip
 
index b5fe5f4d23f810e2a7b86718ecfd2da6b965d2e8..53a095f84a37a7834d8a038f9268bd5d19ebf8ad 100644 (file)
@@ -231,6 +231,8 @@ AC_CONFIG_FILES([
        testsuite/Makefile
        testsuite-real/Makefile
        testsuite-cmp/Makefile
+       stomp/Makefile
+       stomp/testsuite/Makefile
        zypp-plugin/Makefile
        zypp-plugin/testsuite/Makefile
        package/snapper.spec:snapper.spec.in
index d2590aa008126bf7140d225b087bb6b927381380..1feb50587f3fa11d99dff28082b4cfd824750d80 100644 (file)
@@ -1,3 +1,9 @@
+-------------------------------------------------------------------
+Fri Jul 05 16:03:42 CEST 2024 - aschnell@suse.com
+
+- handle content-length of stomp in zypper plugin
+  (gh#openSUSE/snapper#918)
+
 -------------------------------------------------------------------
 Wed May 22 17:21:18 CEST 2024 - aschnell@suse.com
 
diff --git a/stomp/.gitignore b/stomp/.gitignore
new file mode 100644 (file)
index 0000000..66a3f3f
--- /dev/null
@@ -0,0 +1,2 @@
+*.lo
+*.la
diff --git a/stomp/Makefile.am b/stomp/Makefile.am
new file mode 100644 (file)
index 0000000..ef425da
--- /dev/null
@@ -0,0 +1,10 @@
+#
+# Makefile.am for snapper/stomp
+#
+
+SUBDIRS = . testsuite
+
+noinst_LTLIBRARIES = libstomp.la
+
+libstomp_la_SOURCES =  \
+       Stomp.h         Stomp.cc
diff --git a/stomp/Stomp.cc b/stomp/Stomp.cc
new file mode 100644 (file)
index 0000000..ebdd2af
--- /dev/null
@@ -0,0 +1,216 @@
+/*
+ * Copyright (c) [2019-2024] SUSE LLC
+ *
+ * All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of version 2 of the GNU General Public License as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, contact SUSE LLC.
+ *
+ * To contact SUSE about this file by physical or electronic mail, you may
+ * find current contact information at www.suse.com.
+ */
+
+
+#include <iostream>
+#include <regex>
+
+
+using namespace std;
+
+
+#include "Stomp.h"
+
+
+namespace Stomp
+{
+
+    Message
+    read_message(istream& is)
+    {
+       static const regex rx_command("[A-Za-z0-9_]+", regex::extended);
+
+       enum class State { Start, Headers, Body } state = State::Start;
+       bool has_content_length = false;
+       ssize_t content_length = 0;
+
+       Message msg;
+
+       while (!is.eof())
+       {
+           string line;
+           getline(is, line);
+
+           if (state == State::Start)
+           {
+               if (is.eof())
+                   return msg; // empty
+
+               if (line.empty())
+                   continue;
+
+               if (regex_match(line, rx_command))
+               {
+                   msg = Message();
+                   msg.command = line;
+                   state = State::Headers;
+               }
+               else
+               {
+                   throw runtime_error("stomp error: expected a command, got '" + line + "'");
+               }
+           }
+           else if (state == State::Headers)
+           {
+               if (line.empty())
+               {
+                   state = State::Body;
+
+                   if (has_content_length)
+                   {
+                       if (content_length > 0)
+                       {
+                           vector<char> buf(content_length);
+                           is.read(buf.data(), content_length);
+                           msg.body.assign(buf.data(), content_length);
+                       }
+
+                       // still read the \0 that terminates the frame
+                       char buf2 = '-';
+                       is.read(&buf2, 1);
+                       if (buf2 != '\0')
+                           throw runtime_error("stomp error: missing \\0 at frame end");
+                   }
+                   else
+                   {
+                       getline(is, msg.body, '\0');
+                   }
+
+                   return msg;
+               }
+               else
+               {
+                   string::size_type pos = line.find(':');
+                   if (pos == string::npos)
+                       throw runtime_error("stomp error: expected a header or new line, got '" + line + "'");
+
+                   string key = unescape_header(line.substr(0, pos));
+                   string value = unescape_header(line.substr(pos + 1));
+
+                   if (key == "content-length")
+                   {
+                       has_content_length = true;
+                       content_length = std::stol(value.c_str());
+                   }
+
+                   msg.headers[key] = value;
+               }
+           }
+       }
+
+       throw runtime_error("stomp error: expected a message, got a part of it");
+    }
+
+
+    void
+    write_message(ostream& os, const Message& msg)
+    {
+       os << msg.command << '\n';
+       for (auto it : msg.headers)
+           os << escape_header(it.first) << ':' << escape_header(it.second) << '\n';
+       os << '\n';
+       os << msg.body << '\0';
+       os.flush();
+    }
+
+
+    Message
+    ack()
+    {
+       Message msg;
+       msg.command = "ACK";
+       return msg;
+    }
+
+
+    Message
+    nack()
+    {
+       Message msg;
+       msg.command = "NACK";
+       return msg;
+    }
+
+
+    std::string
+    escape_header(const std::string& in)
+    {
+       string out;
+
+       for (const char c : in)
+       {
+           switch (c)
+           {
+               case '\r':
+                   out += "\\r"; break;
+               case '\n':
+                   out += "\\n"; break;
+               case ':':
+                   out += "\\c"; break;
+               case '\\':
+                   out += "\\\\"; break;
+
+               default:
+                   out += c;
+           }
+       }
+
+       return out;
+    }
+
+
+    std::string
+    unescape_header(const std::string& in)
+    {
+       string out;
+
+       for (string::const_iterator it = in.begin(); it != in.end(); ++it)
+       {
+           if (*it == '\\')
+           {
+               if (++it == in.end())
+                   throw runtime_error("stomp error: invalid start of escape sequence");
+
+               switch (*it)
+               {
+                   case 'r':
+                       out += '\r'; break;
+                   case 'n':
+                       out += '\n'; break;
+                   case 'c':
+                       out += ':'; break;
+                   case '\\':
+                       out += '\\'; break;
+
+                   default:
+                       throw runtime_error("stomp error: unknown escape sequence");
+               }
+           }
+           else
+           {
+               out += *it;
+           }
+       }
+
+       return out;
+    }
+
+}
diff --git a/stomp/Stomp.h b/stomp/Stomp.h
new file mode 100644 (file)
index 0000000..2220d8d
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) [2019-2024] SUSE LLC
+ *
+ * All Rights Reserved.
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of version 2 of the GNU General Public License as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, contact SUSE LLC.
+ *
+ * To contact SUSE about this file by physical or electronic mail, you may
+ * find current contact information at www.suse.com.
+ */
+
+
+#ifndef SNAPPER_STOMP_H
+#define SNAPPER_STOMP_H
+
+
+#include <istream>
+#include <ostream>
+#include <string>
+#include <map>
+
+
+/**
+ * A tiny STOMP (https://stomp.github.io/) implementation.
+ */
+
+namespace Stomp
+{
+
+    struct Message
+    {
+       std::string command;
+       std::map<std::string, std::string> headers;
+       std::string body;
+    };
+
+
+    Message read_message(std::istream& is);
+    void write_message(std::ostream& os, const Message& msg);
+
+    Message ack();
+    Message nack();
+
+    std::string escape_header(const std::string& in);
+    std::string unescape_header(const std::string& in);
+
+}
+
+
+#endif
diff --git a/stomp/testsuite/.gitignore b/stomp/testsuite/.gitignore
new file mode 100644 (file)
index 0000000..85f0d0b
--- /dev/null
@@ -0,0 +1,5 @@
+*.log
+*.o
+*.test
+*.trs
+test-suite.log
diff --git a/stomp/testsuite/Makefile.am b/stomp/testsuite/Makefile.am
new file mode 100644 (file)
index 0000000..424ce18
--- /dev/null
@@ -0,0 +1,17 @@
+#
+# Makefile.am for snapper/stomp/testsuite
+#
+
+SUBDIRS = .
+
+LDADD =                                                                \
+       ../libstomp.la                                          \
+       -lboost_unit_test_framework
+
+check_PROGRAMS =                                               \
+       read1.test write1.test escape.test
+
+AM_DEFAULT_SOURCE_EXT = .cc
+
+TESTS = $(check_PROGRAMS)
+
diff --git a/stomp/testsuite/escape.cc b/stomp/testsuite/escape.cc
new file mode 100644 (file)
index 0000000..cad9aa2
--- /dev/null
@@ -0,0 +1,29 @@
+
+#define BOOST_TEST_DYN_LINK
+#define BOOST_TEST_MODULE snapper
+
+#include <boost/test/unit_test.hpp>
+
+#include "../Stomp.h"
+
+
+using namespace std;
+using namespace Stomp;
+
+
+BOOST_AUTO_TEST_CASE(escape)
+{
+    BOOST_CHECK_EQUAL(Stomp::escape_header("hello"), "hello");
+
+    BOOST_CHECK_EQUAL(Stomp::escape_header("hello\nworld"), "hello\\nworld");
+    BOOST_CHECK_EQUAL(Stomp::escape_header("hello:world"), "hello\\cworld");
+}
+
+
+BOOST_AUTO_TEST_CASE(unescape)
+{
+    BOOST_CHECK_EQUAL(Stomp::unescape_header("hello"), "hello");
+
+    BOOST_CHECK_EQUAL(Stomp::unescape_header("hello\\nworld"), "hello\nworld");
+    BOOST_CHECK_EQUAL(Stomp::unescape_header("hello\\cworld"), "hello:world");
+}
diff --git a/stomp/testsuite/read1.cc b/stomp/testsuite/read1.cc
new file mode 100644 (file)
index 0000000..2d89dd7
--- /dev/null
@@ -0,0 +1,73 @@
+
+#define BOOST_TEST_DYN_LINK
+#define BOOST_TEST_MODULE snapper
+
+#include <boost/test/unit_test.hpp>
+
+#include "../Stomp.h"
+
+
+using namespace std;
+using namespace Stomp;
+
+
+const string null("\0", 1);
+
+
+BOOST_AUTO_TEST_CASE(test1)
+{
+    // no optional content-lenght
+
+    istringstream s1("HELLO\nkey:value\n\nWORLD" + null);
+    istream s2(s1.rdbuf());
+
+    Message msg = read_message(s2);
+
+    BOOST_CHECK_EQUAL(s2.peek(), -1);
+
+    BOOST_CHECK_EQUAL(msg.command, "HELLO");
+
+    BOOST_CHECK_EQUAL(msg.headers.size(), 1);
+    BOOST_CHECK_EQUAL(msg.headers["key"], "value");
+
+    BOOST_CHECK_EQUAL(msg.body, "WORLD");
+}
+
+
+BOOST_AUTO_TEST_CASE(test2)
+{
+    // optional content-lenght
+
+    istringstream s1("HELLO\nkey:value\ncontent-length:5\n\nWORLD" + null);
+    istream s2(s1.rdbuf());
+
+    Message msg = read_message(s2);
+
+    BOOST_CHECK_EQUAL(s2.peek(), -1);
+
+    BOOST_CHECK_EQUAL(msg.command, "HELLO");
+
+    BOOST_CHECK_EQUAL(msg.headers.size(), 2);
+    BOOST_CHECK_EQUAL(msg.headers["key"], "value");
+    BOOST_CHECK_EQUAL(msg.headers["content-length"], "5");
+
+    BOOST_CHECK_EQUAL(msg.body, "WORLD");
+}
+
+
+BOOST_AUTO_TEST_CASE(escape1)
+{
+    // special characters in header
+
+    istringstream s1("HELLO\nGermany\\cSpain:2\\c1\n\nWORLD" + null);
+    istream s2(s1.rdbuf());
+
+    Message msg = read_message(s2);
+
+    BOOST_CHECK_EQUAL(msg.command, "HELLO");
+
+    BOOST_CHECK_EQUAL(msg.headers.size(), 1);
+    BOOST_CHECK_EQUAL(msg.headers["Germany:Spain"], "2:1");
+
+    BOOST_CHECK_EQUAL(msg.body, "WORLD");
+}
diff --git a/stomp/testsuite/write1.cc b/stomp/testsuite/write1.cc
new file mode 100644 (file)
index 0000000..a231605
--- /dev/null
@@ -0,0 +1,46 @@
+
+#define BOOST_TEST_DYN_LINK
+#define BOOST_TEST_MODULE snapper
+
+#include <boost/test/unit_test.hpp>
+
+#include "../Stomp.h"
+
+
+using namespace std;
+using namespace Stomp;
+
+
+const string null("\0", 1);
+
+
+BOOST_AUTO_TEST_CASE(test1)
+{
+    Message msg;
+    msg.command = "HELLO";
+    msg.headers["key"] = "value";
+    msg.body = "WORLD";
+
+    ostringstream s1;
+    write_message(s1, msg);
+    string s2 = s1.str();
+
+    BOOST_CHECK_EQUAL(s2, "HELLO\nkey:value\n\nWORLD" + null);
+}
+
+
+BOOST_AUTO_TEST_CASE(escape1)
+{
+    // special characters in header
+
+    Message msg;
+    msg.command = "HELLO";
+    msg.headers["Germany:Spain"] = "2:1";
+    msg.body = "WORLD";
+
+    ostringstream s1;
+    write_message(s1, msg);
+    string s2 = s1.str();
+
+    BOOST_CHECK_EQUAL(s2, "HELLO\nGermany\\cSpain:2\\c1\n\nWORLD" + null);
+}
index 3e4ddf6cc4093ea9078526a9616358bbd3c5aeae..2fd99b8cc28b766f33559d895ea9ace25172a5e5 100644 (file)
@@ -21,6 +21,7 @@ snapper_zypp_plugin_LDADD = \
        ../client/libclient.la \
        ../snapper/libsnapper.la \
        ../dbus/libdbus.la \
+       ../stomp/libstomp.la \
        $(JSONC_LIBS)
 
 check_PROGRAMS = solvable-matcher.test forwarding-zypp-plugin
@@ -31,6 +32,7 @@ forwarding_zypp_plugin_SOURCES = \
 
 forwarding_zypp_plugin_LDADD = \
        ../snapper/libsnapper.la \
+       ../stomp/libstomp.la \
        -lboost_system \
        -lpthread
 
@@ -42,6 +44,7 @@ solvable_matcher_test_SOURCES = \
 
 solvable_matcher_test_LDADD = \
        ../snapper/libsnapper.la \
+       ../stomp/libstomp.la \
        $(XML2_LIBS) \
        -lboost_unit_test_framework
 
index 2640337632a76287a3117bf0e5c6f0ac5178a824..943963f4936f7e6d613ecc13c6546bf9b9734dca 100644 (file)
@@ -48,84 +48,6 @@ ZyppPlugin::main()
 }
 
 
-ZyppPlugin::Message
-ZyppPlugin::read_message(istream& is) const
-{
-    enum class State { Start, Headers, Body } state = State::Start;
-
-    Message msg;
-
-    while (!is.eof())
-    {
-       string line;
-
-       getline(is, line);
-       boost::trim_right(line);
-
-       if (state == State::Start)
-       {
-           if (is.eof())
-               return msg; //empty
-
-           if (line.empty())
-               continue;
-
-           static const regex rx_word("[A-Za-z0-9_]+", regex::extended);
-           if (regex_match(line, rx_word))
-           {
-               msg = Message();
-               msg.command = line;
-               state = State::Headers;
-           }
-           else
-           {
-               throw runtime_error("Plugin protocol error: expected a command. Got '" + line + "'");
-           }
-       }
-       else if (state == State::Headers)
-       {
-           if (line.empty())
-           {
-               state = State::Body;
-               getline(is, msg.body, '\0');
-
-               return msg;
-           }
-           else
-           {
-               static const regex rx_header("([A-Za-z0-9_]+):[ \t]*(.+)", regex::extended);
-               smatch match;
-
-               if (regex_match(line, match, rx_header))
-               {
-                   string key = match[1];
-                   string value = match[2];
-                   msg.headers[key] = value;
-               }
-               else
-               {
-                   throw runtime_error("Plugin protocol error: expected a header or new line. Got '" + line + "'");
-               }
-           }
-       }
-    }
-
-    throw runtime_error("Plugin protocol error: expected a message, got a part of it");
-}
-
-
-void
-ZyppPlugin::write_message(ostream& os, const Message& msg) const
-{
-    os << msg.command << endl;
-    for (auto it : msg.headers)
-       os << it.first << ':' << it.second << endl;
-    os << endl;
-    os << msg.body << '\0';
-    os.flush();
-}
-
-
 ZyppPlugin::Message
 ZyppPlugin::dispatch(const Message& msg)
 {
index cf51d05a772c6ff93e1f4bccfda53e1d08dba194..ed356178a4155ba0da33e1b7e231209fe4d6473f 100644 (file)
@@ -24,8 +24,8 @@
 
 
 #include <iostream>
-#include <map>
-#include <string>
+
+#include "../stomp/Stomp.h"
 
 
 class ZyppPlugin
@@ -34,12 +34,7 @@ public:
 
     // Plugin message aka frame
     // https://doc.opensuse.org/projects/libzypp/SLE12SP2/zypp-plugins.html
-    struct Message
-    {
-       std::string command;
-       std::map<std::string, std::string> headers;
-       std::string body;
-    };
+    using Message = Stomp::Message;
 
     /// Where the protocol reads from
     std::istream& pin;
@@ -50,26 +45,22 @@ public:
        : pin(in), pout(out)
     {}
 
-    virtual ~ZyppPlugin() {}
+    virtual ~ZyppPlugin() = default;
 
     virtual int main();
 
 protected:
 
-    Message read_message(std::istream& is) const;
-    void write_message(std::ostream& os, const Message& msg) const;
+    Message read_message(std::istream& is) const { return Stomp::read_message(is); }
+    void write_message(std::ostream& os, const Message& msg) const { Stomp::write_message(os, msg); }
 
     /// Handle a message and return a reply.
     // Derived classes must override it.
     // The base acks a _DISCONNECT and replies _ENOMETHOD to everything else.
     virtual Message dispatch(const Message& msg) = 0;
 
-    Message ack() const
-    {
-       Message a;
-       a.command = "ACK";
-       return a;
-    }
+    Message ack() const { return Stomp::ack(); }
+
 };
 
 #endif