From 79640be325c333d77b4078d37f7668b74d5682e3 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Sat, 29 Apr 2017 23:23:07 +0300 Subject: Add hxx extension for headers and lib prefix for library dirs --- libbbot/manifest.cxx | 988 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 988 insertions(+) create mode 100644 libbbot/manifest.cxx (limited to 'libbbot/manifest.cxx') diff --git a/libbbot/manifest.cxx b/libbbot/manifest.cxx new file mode 100644 index 0000000..3ea9d19 --- /dev/null +++ b/libbbot/manifest.cxx @@ -0,0 +1,988 @@ +// file : libbbot/manifest.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include + +#include +#include +#include // isxdigit() +#include +#include +#include // size_t +#include // move() +#include // uint64_t +#include // invalid_argument + +#include // digit() +#include +#include +#include +#include + +using namespace std; +using namespace butl; +using namespace bpkg; + +namespace bbot +{ + using parser = manifest_parser; + using parsing = manifest_parsing; + using serializer = manifest_serializer; + using serialization = manifest_serialization; + using name_value = manifest_name_value; + + using strings = vector; + + // result_status + // + string + to_string (result_status s) + { + switch (s) + { + case result_status::success: return "success"; + case result_status::warning: return "warning"; + case result_status::error: return "error"; + case result_status::abort: return "abort"; + case result_status::abnormal: return "abnormal"; + } + + assert (false); + return string (); + } + + result_status + to_result_status (const string& s) + { + if (s == "success") return result_status::success; + else if (s == "warning") return result_status::warning; + else if (s == "error") return result_status::error; + else if (s == "abort") return result_status::abort; + else if (s == "abnormal") return result_status::abnormal; + else throw invalid_argument ("invalid result status '" + s + "'"); + } + + // Utility functions + // + inline static bool + valid_sha256 (const string& s) noexcept + { + if (s.size () != 64) + return false; + + for (const auto& c: s) + { + if ((c < 'a' || c > 'f' ) && !digit (c)) + return false; + } + + return true; + } + + inline static bool + valid_fingerprint (const string& f) noexcept + { + size_t n (f.size ()); + if (n != 32 * 3 - 1) + return false; + + for (size_t i (0); i < n; ++i) + { + char c (f[i]); + if ((i + 1) % 3 == 0) + { + if (c != ':') + return false; + } + else if (!isxdigit (c)) + return false; + } + + return true; + } + + // machine_header_manifest + // + machine_header_manifest:: + machine_header_manifest (parser& p, bool iu) + : machine_header_manifest (p, p.next (), iu) + { + // Make sure this is the end. + // + name_value nv (p.next ()); + if (!nv.empty ()) + throw parsing (p.name (), nv.name_line, nv.name_column, + "single machine header manifest expected"); + } + + machine_header_manifest:: + machine_header_manifest (parser& p, name_value nv, bool iu) + { + auto bad_name = [&p, &nv] (const string& d) + { + throw parsing (p.name (), nv.name_line, nv.name_column, d); + }; + + auto bad_value = [&p, &nv] (const string& d) + { + throw parsing (p.name (), nv.value_line, nv.value_column, d); + }; + + // Make sure this is the start and we support the version. + // + if (!nv.name.empty ()) + bad_name ("start of machine header manifest expected"); + + if (nv.value != "1") + bad_value ("unsupported format version"); + + for (nv = p.next (); !nv.empty (); nv = p.next ()) + { + string& n (nv.name); + string& v (nv.value); + + if (n == "id") + { + if (!id.empty ()) + bad_name ("machine id redefinition"); + + if (v.empty ()) + bad_value ("empty machine id"); + + id = move (v); + } + else if (n == "name") + { + if (!name.empty ()) + bad_name ("machine name redefinition"); + + if (v.empty ()) + bad_value ("empty machine name"); + + name = move (v); + } + else if (n == "summary") + { + if (!summary.empty ()) + bad_name ("machine summary redefinition"); + + if (v.empty ()) + bad_value ("empty machine summary"); + + summary = move (v); + } + else if (!iu) + bad_name ("unknown name '" + n + "' in machine header manifest"); + } + + // Verify all non-optional values were specified. + // + if (id.empty ()) + bad_value ("no machine id specified"); + + if (name.empty ()) + bad_value ("no machine name specified"); + + if (summary.empty ()) + bad_value ("no machine summary specified"); + } + + void machine_header_manifest:: + serialize (serializer& s) const + { + // @@ Should we check that all non-optional values are specified and all + // values are valid? + // + s.next ("", "1"); // Start of manifest. + s.next ("id", id); + s.next ("name", name); + s.next ("summary", summary); + s.next ("", ""); // End of manifest. + } + + // task_request_manifest + // + task_request_manifest:: + task_request_manifest (parser& p, bool iu) + { + name_value nv (p.next ()); + + auto bad_name = [&p, &nv] (const string& d) + { + throw parsing (p.name (), nv.name_line, nv.name_column, d); + }; + + auto bad_value = [&p, &nv] (const string& d) + { + throw parsing (p.name (), nv.value_line, nv.value_column, d); + }; + + // Make sure this is the start and we support the version. + // + if (!nv.name.empty ()) + bad_name ("start of task request manifest expected"); + + if (nv.value != "1") + bad_value ("unsupported format version"); + + // Parse the task request manifest. + // + for (nv = p.next (); !nv.empty (); nv = p.next ()) + { + string& n (nv.name); + string& v (nv.value); + + if (n == "agent") + { + if (!agent.empty ()) + bad_name ("task request agent redefinition"); + + if (v.empty ()) + bad_value ("empty task request agent"); + + agent = move (v); + } + else if (n == "fingerprint") + { + if (fingerprint) + bad_name ("task request fingerprint redefinition"); + + if (!valid_sha256 (v)) + bad_value ("invalid task request fingerprint"); + + fingerprint = move (v); + } + else if (!iu) + bad_name ("unknown name '" + n + "' in task request manifest"); + } + + // Verify all non-optional values were specified. + // + if (agent.empty ()) + bad_value ("no task request agent specified"); + + // Parse machine header manifests. + // + for (nv = p.next (); !nv.empty (); nv = p.next ()) + machines.emplace_back (machine_header_manifest (p, nv, iu)); + + if (machines.empty ()) + bad_value ("no task request machines specified"); + } + + void task_request_manifest:: + serialize (serializer& s) const + { + // @@ Should we check that all non-optional values are specified and all + // values are valid? + // + s.next ("", "1"); // Start of manifest. + s.next ("agent", agent); + + if (fingerprint) + s.next ("fingerprint", *fingerprint); + + s.next ("", ""); // End of manifest. + + for (const machine_header_manifest& m: machines) + m.serialize (s); + + s.next ("", ""); // End of stream. + } + + // task_manifest + // + task_manifest:: + task_manifest (parser& p, bool iu) + : task_manifest (p, p.next (), iu) + { + // Make sure this is the end. + // + name_value nv (p.next ()); + if (!nv.empty ()) + throw parsing (p.name (), nv.name_line, nv.name_column, + "single task manifest expected"); + } + + task_manifest:: + task_manifest (parser& p, name_value nv, bool iu) + { + auto bad_name = [&p, &nv] (const string& d) + { + throw parsing (p.name (), nv.name_line, nv.name_column, d); + }; + + // Offsets are used to tie an error to the specific position inside a + // manifest value (possibly a multiline one). + // + auto bad_value = [&p, &nv] ( + const string& d, uint64_t column_offset = 0, uint64_t line_offset = 0) + { + throw parsing (p.name (), + nv.value_line + line_offset, + (line_offset == 0 ? nv.value_column : 1) + column_offset, + d); + }; + + // Make sure this is the start and we support the version. + // + if (!nv.name.empty ()) + bad_name ("start of task manifest expected"); + + if (nv.value != "1") + bad_value ("unsupported format version"); + + // Parse the task manifest. + // + for (nv = p.next (); !nv.empty (); nv = p.next ()) + { + string& n (nv.name); + string& v (nv.value); + + if (n == "name") + { + if (!name.empty ()) + bad_name ("task package name redefinition"); + + if (v.empty ()) + bad_value ("empty task package name"); + + name = move (v); + } + else if (n == "version") + { + if (!version.empty ()) + bad_name ("task package version redefinition"); + + try + { + version = bpkg::version (move (v)); + } + catch (const invalid_argument& e) + { + bad_value (string ("invalid task package version: ") + e.what ()); + } + + // Versions like 1.2.3- are forbidden in manifest as intended to be + // used for version constrains rather than actual releases. + // + if (version.release && version.release->empty ()) + bad_value ("invalid task package version release"); + } + else if (n == "repository") + { + if (!repository.empty ()) + bad_name ("task repository redefinition"); + + if (v.empty ()) + bad_value ("empty task repository"); + + try + { + // Call remote/absolute repository location constructor (throws + // invalid_argument for relative location). + // + repository = repository_location (move (v)); + } + catch (const invalid_argument& e) + { + bad_value (string ("invalid task repository: ") + e.what ()); + } + } + else if (n == "trust") + { + if (v != "yes" && !valid_fingerprint (v)) + bad_value ("invalid repository certificate fingerprint"); + + trust.emplace_back (move (v)); + } + else if (n == "machine") + { + if (!machine.empty ()) + bad_name ("task machine redefinition"); + + if (v.empty ()) + bad_value ("empty task machine"); + + machine = move (v); + } + else if (n == "target") + { + if (target) + bad_name ("task target redefinition"); + + try + { + target = target_triplet (v); + } + catch (const invalid_argument& e) + { + bad_value (string ("invalid task target: ") + e.what ()); + } + } + else if (n == "config") + { + if (!config.empty ()) + bad_name ("task configuration redefinition"); + + // Note that when reporting errors we combine the manifest value + // position with the respective field and error positions. + // + try + { + istringstream is (v); + tab_parser parser (is, ""); + + // Here we naturally support multiline config manifest. + // + tab_fields tl; + while (!(tl = parser.next ()).empty ()) + { + for (auto& tf: tl) + { + try + { + check_config (tf.value); + } + catch (const invalid_argument& e) + { + bad_value (string ("invalid task configuration: ") + e.what (), + tf.column - 1, + tl.line - 1); + } + + config.emplace_back (move (tf.value)); + } + } + } + catch (const tab_parsing& e) + { + bad_value ("invalid task configuration: " + e.description, + e.column - 1, + e.line - 1); + } + + if (config.empty ()) + bad_value ("empty task configuration"); + } + else if (!iu) + bad_name ("unknown name '" + n + "' in task manifest"); + } + + // Verify all non-optional values were specified. + // + if (name.empty ()) + bad_value ("no task package name specified"); + + if (version.empty ()) + bad_value ("no task package version specified"); + + if (repository.empty ()) + bad_value ("no task repository specified"); + + if (machine.empty ()) + bad_value ("no task machine specified"); + } + + void task_manifest:: + serialize (serializer& s) const + { + // @@ Should we check that all non-optional values are specified and all + // values are valid? + // + s.next ("", "1"); // Start of manifest. + s.next ("name", name); + s.next ("version", version.string ()); + s.next ("repository", repository.string ()); + + for (const auto& v: trust) + s.next ("trust", v); + + s.next ("machine", machine); + + if (target) + s.next ("target", target->string ()); + + // Recompose config string as a space-separated variable list, + // + if (!config.empty ()) + { + string v; + for (auto b (config.cbegin ()), i (b), e (config.cend ()); i != e; ++i) + { + if (i != b) + v += ' '; + + v += *i; + } + + s.next ("config", v); + } + + s.next ("", ""); // End of manifest. + } + + strings task_manifest:: + unquoted_config () const + { + return string_parser::unquote (config); + } + + void task_manifest:: + check_config (const string& s) + { + auto i (s.begin ()); + auto e (s.end ()); + + // Iterate until the variable name end and check that it contains no + // whitespaces. + // + for (; i != e; ++i) + { + char c (*i); + + if (c == ' ' || c == '\t') // Whitespace in name. + throw invalid_argument ("expected variable assignment"); + else if (c == '=') + break; + } + + if (i == e) + throw invalid_argument ("no variable value"); + } + + // task_response_manifest + // + task_response_manifest:: + task_response_manifest (parser& p, bool iu) + { + name_value nv (p.next ()); + + auto bad_name = [&p, &nv] (const string& d) + { + throw parsing (p.name (), nv.name_line, nv.name_column, d); + }; + + auto bad_value = [&p, &nv] (const string& d) + { + throw parsing (p.name (), nv.value_line, nv.value_column, d); + }; + + // Make sure this is the start and we support the version. + // + if (!nv.name.empty ()) + bad_name ("start of task response manifest expected"); + + if (nv.value != "1") + bad_value ("unsupported format version"); + + // Parse the task response manifest. + // + // Note that we need to distinguish an empty and absent session. + // + optional sess; + for (nv = p.next (); !nv.empty (); nv = p.next ()) + { + string& n (nv.name); + string& v (nv.value); + + if (n == "session") + { + if (sess) + bad_name ("task response session redefinition"); + + sess = move (v); + } + else if (n == "challenge") + { + if (challenge) + bad_name ("task response challenge redefinition"); + + if (v.empty ()) + bad_value ("empty task response challenge"); + + challenge = move (v); + } + else if (n == "result-url") + { + if (result_url) + bad_name ("task response result url redefinition"); + + if (v.empty ()) + bad_value ("empty task response result url"); + + result_url = move (v); + } + else if (!iu) + bad_name ("unknown name '" + n + "' in task response manifest"); + } + + // Verify all non-optional values were specified, and all values are + // expected. + // + if (!sess) + bad_value ("no task response session specified"); + + session = move (*sess); + + // If session is not empty then the challenge may, and the result url + // must, be present, otherwise they shouldn't. + // + if (!session.empty ()) + { + if (!result_url) + bad_value ("no task response result url specified"); + } + else + { + if (challenge) + bad_value ("unexpected task response challenge"); + + if (result_url) + bad_value ("unexpected task response result url"); + } + + // If session is not empty then the task manifest must follow, otherwise it + // shouldn't. + // + nv = p.next (); + + if (!session.empty ()) + { + if (nv.empty ()) + bad_value ("task manifest expected"); + + task = task_manifest (p, nv, iu); + + nv = p.next (); + } + + // Make sure this is the end. + // + if (!nv.empty ()) + throw parsing (p.name (), nv.name_line, nv.name_column, + "single task response manifest expected"); + } + + void task_response_manifest:: + serialize (serializer& s) const + { + // @@ Should we check that all non-optional values are specified and all + // values are valid? + // + s.next ("", "1"); // Start of manifest. + s.next ("session", session); + + if (challenge) + s.next ("challenge", *challenge); + + if (result_url) + s.next ("result-url", *result_url); + + s.next ("", ""); // End of manifest. + + if (task) + task->serialize (s); + + s.next ("", ""); // End of stream. + } + + // result_manifest + // + result_manifest:: + result_manifest (parser& p, bool iu) + : result_manifest (p, p.next (), iu) + { + // Make sure this is the end. + // + name_value nv (p.next ()); + if (!nv.empty ()) + throw parsing (p.name (), nv.name_line, nv.name_column, + "single result manifest expected"); + } + + result_manifest:: + result_manifest (parser& p, name_value nv, bool iu) + { + auto bad_name = [&p, &nv] (const string& d) + { + throw parsing (p.name (), nv.name_line, nv.name_column, d); + }; + + auto bad_value = [&p, &nv] (const string& d, size_t offset = 0) + { + throw parsing (p.name (), nv.value_line, nv.value_column + offset, d); + }; + + auto result_stat = + [&bad_value] (const string& v, const string& what) -> result_status + { + try + { + return to_result_status (v); + } + catch (const invalid_argument&) + { + bad_value ("invalid " + what); + } + + // Can't be here. Would be redundant if it were possible to declare + // lambda with the [[noreturn]] attribute. Note that GCC (non-portably) + // supports that. + // + return result_status::abnormal; + }; + + // Make sure this is the start and we support the version. + // + if (!nv.name.empty ()) + bad_name ("start of result manifest expected"); + + if (nv.value != "1") + bad_value ("unsupported format version"); + + // Parse the result manifest. + // + optional stat; + + // Number of parsed *-log values. Also denotes the next expected log type. + // + size_t nlog (0); + + for (nv = p.next (); !nv.empty (); nv = p.next ()) + { + string& n (nv.name); + string& v (nv.value); + + if (n == "name") + { + if (!name.empty ()) + bad_name ("result package name redefinition"); + + if (v.empty ()) + bad_value ("empty result package name"); + + name = move (v); + } + else if (n == "version") + { + if (!version.empty ()) + bad_name ("result package version redefinition"); + + try + { + version = bpkg::version (move (v)); + } + catch (const invalid_argument& e) + { + bad_value (string ("invalid result package version: ") + e.what ()); + } + + // Versions like 1.2.3- are forbidden in manifest as intended to be + // used for version constrains rather than actual releases. + // + if (version.release && version.release->empty ()) + bad_value ("invalid result package version release"); + } + else if (n == "status") + { + if (stat) + bad_name ("result status redefinition"); + + stat = result_stat (v, "result status"); + } + else + { + size_t nn (n.size ()); // Name length. + + // Note: returns false if nothing preceeds a suffix. + // + auto suffix = [&n, nn] (const char* s, size_t sn) -> bool + { + return nn > sn && n.compare (nn - sn, sn, s) == 0; + }; + + size_t sn; + if (suffix ("-status", sn = 7)) + { + if (!stat) + bad_name ("result status must appear first"); + + if (nlog > 0) // Some logs have already been parsed. + bad_name (n + " after operations logs"); + + string op (n, 0, nn - sn); + + // Make sure the operation result status is not redefined. + // + for (const auto& r: results) + { + if (r.operation == op) + bad_name ("result " + n + " redefinition"); + } + + // Add the operation result (log will come later). + // + results.push_back ({move (op), result_stat (v, n), string ()}); + } + else if (suffix ("-log", sn = 4)) + { + string op (n, 0, nn - sn); + + // Check that specifically this operation log is expected. + // + if (nlog >= results.size ()) + bad_name ("unexpected " + n); + + if (results[nlog].operation != op) + bad_name (results[nlog].operation + "-log is expected"); + + // Save operation log. + // + results[nlog++].log = move (v); + } + else if (!iu) + bad_name ("unknown name '" + n + "' in result manifest"); + } + } + + // Verify all non-optional values were specified. + // + if (name.empty ()) + bad_value ("no result package name specified"); + + if (version.empty ()) + bad_value ("no result package version specified"); + + if (!stat) + bad_value ("no result status specified"); + + // @@ Checking that the result status is consistent with operations + // statuses is a bit hairy, so let's postpone for now. + // + status = move (*stat); + + // Check that we have log for every operation result status. + // + if (nlog < results.size ()) + bad_name ("no result " + results[nlog].operation + "-log specified"); + } + + void result_manifest:: + serialize (serializer& s) const + { + // @@ Should we check that all non-optional values are specified and all + // values are valid? + // + s.next ("", "1"); // Start of manifest. + s.next ("name", name); + s.next ("version", version.string ()); + s.next ("status", to_string (status)); + + // Serialize *-status values. + // + for (const auto& r: results) + s.next (r.operation + "-status", to_string (r.status)); + + // Serialize *-log values. + // + for (const auto& r: results) + s.next (r.operation + "-log", r.log); + + s.next ("", ""); // End of manifest. + } + + // result_request_manifest + // + result_request_manifest:: + result_request_manifest (parser& p, bool iu) + { + name_value nv (p.next ()); + + auto bad_name = [&p, &nv] (const string& d) + { + throw parsing (p.name (), nv.name_line, nv.name_column, d); + }; + + auto bad_value = [&p, &nv] (const string& d) + { + throw parsing (p.name (), nv.value_line, nv.value_column, d); + }; + + // Make sure this is the start and we support the version. + // + if (!nv.name.empty ()) + bad_name ("start of result request manifest expected"); + + if (nv.value != "1") + bad_value ("unsupported format version"); + + // Parse the result request manifest. + // + for (nv = p.next (); !nv.empty (); nv = p.next ()) + { + string& n (nv.name); + string& v (nv.value); + + if (n == "session") + { + if (!session.empty ()) + bad_name ("result request session redefinition"); + + if (v.empty ()) + bad_value ("empty result request session"); + + session = move (v); + } + else if (n == "challenge") + { + if (challenge) + bad_name ("result request challenge redefinition"); + + if (v.empty ()) + bad_value ("empty result request challenge"); + + challenge = move (v); + } + else if (!iu) + bad_name ("unknown name '" + n + "' in result request manifest"); + } + + // Verify all non-optional values were specified. + // + if (session.empty ()) + bad_value ("no result request session specified"); + + nv = p.next (); + if (nv.empty ()) + bad_value ("result manifest expected"); + + result = result_manifest (p, nv, iu); + + // Make sure this is the end. + // + nv = p.next (); + if (!nv.empty ()) + throw parsing (p.name (), nv.name_line, nv.name_column, + "single result request manifest expected"); + } + + void result_request_manifest:: + serialize (serializer& s) const + { + // @@ Should we check that all non-optional values are specified and all + // values are valid? + // + s.next ("", "1"); // Start of manifest. + s.next ("session", session); + + if (challenge) + s.next ("challenge", *challenge); + + s.next ("", ""); // End of manifest. + + result.serialize (s); + s.next ("", ""); // End of stream. + } +} -- cgit v1.1