File pr919.patch of Package snapper.35403
diff --git a/Makefile.am b/Makefile.am
index 69b66787..fe6a1b9e 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -3,7 +3,7 @@
#
SUBDIRS = snapper examples dbus server client scripts pam data doc po \
- testsuite testsuite-real testsuite-cmp zypp-plugin
+ testsuite testsuite-real testsuite-cmp stomp zypp-plugin
AUTOMAKE_OPTIONS = foreign dist-bzip2 no-dist-gzip
diff --git a/configure.ac b/configure.ac
index b5fe5f4d..53a095f8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -232,6 +232,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
diff --git a/stomp/.gitignore b/stomp/.gitignore
new file mode 100644
index 00000000..66a3f3fe
--- /dev/null
+++ b/stomp/.gitignore
@@ -0,0 +1,2 @@
+*.lo
+*.la
diff --git a/stomp/Makefile.am b/stomp/Makefile.am
new file mode 100644
index 00000000..ef425daf
--- /dev/null
+++ b/stomp/Makefile.am
@@ -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
index 00000000..ebdd2af9
--- /dev/null
+++ b/stomp/Stomp.cc
@@ -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
index 00000000..2220d8dc
--- /dev/null
+++ b/stomp/Stomp.h
@@ -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
index 00000000..85f0d0bb
--- /dev/null
+++ b/stomp/testsuite/.gitignore
@@ -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
index 00000000..424ce182
--- /dev/null
+++ b/stomp/testsuite/Makefile.am
@@ -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
index 00000000..cad9aa22
--- /dev/null
+++ b/stomp/testsuite/escape.cc
@@ -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
index 00000000..2d89dd70
--- /dev/null
+++ b/stomp/testsuite/read1.cc
@@ -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
index 00000000..a231605d
--- /dev/null
+++ b/stomp/testsuite/write1.cc
@@ -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);
+}
diff --git a/zypp-plugin/Makefile.am b/zypp-plugin/Makefile.am
index 3e4ddf6c..2fd99b8c 100644
--- a/zypp-plugin/Makefile.am
+++ b/zypp-plugin/Makefile.am
@@ -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
@@ -41,6 +43,7 @@ solvable_matcher_test_SOURCES = \
solvable_matcher_test_LDADD = \
../snapper/libsnapper.la \
+ ../stomp/libstomp.la \
$(XML2_LIBS) \
-lboost_unit_test_framework
diff --git a/zypp-plugin/zypp_plugin.cc b/zypp-plugin/zypp_plugin.cc
index 26403376..943963f4 100644
--- a/zypp-plugin/zypp_plugin.cc
+++ b/zypp-plugin/zypp_plugin.cc
@@ -38,79 +38,6 @@
return 0;
}
-void ZyppPlugin::write_message(ostream& os, const Message& msg) {
- 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::read_message(istream& is) {
- 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");
-}
-
ZyppPlugin::Message ZyppPlugin::dispatch(const Message& msg) {
if (msg.command == "_DISCONNECT") {
return ack();
diff --git a/zypp-plugin/zypp_plugin.h b/zypp-plugin/zypp_plugin.h
index cf51d05a..ed356178 100644
--- a/zypp-plugin/zypp_plugin.h
+++ b/zypp-plugin/zypp_plugin.h
@@ -23,18 +23,15 @@
#define ZYPP_PLUGIN_H
#include <iostream>
-#include <map>
-#include <string>
-class ZyppPlugin {
+#include "../stomp/Stomp.h"
+
+class ZyppPlugin
+{
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;
@@ -49,24 +46,22 @@
, pout(out)
, plog(log)
{}
- virtual ~ZyppPlugin() {}
+ virtual ~ZyppPlugin() = default;
virtual int main();
protected:
+
+ 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 should override it.
// The base acks a _DISCONNECT and replies _ENOMETHOD to everything else.
virtual Message dispatch(const Message&);
- Message read_message(std::istream& is);
- void write_message(std::ostream& os, const Message& msg);
+ Message ack() const { return Stomp::ack(); }
- Message ack() {
- Message a;
- a.command = "ACK";
- return a;
- }
};
-#endif //ZYPP_PLUGIN_H
+#endif