DCCL v4
dccl_tool.cpp
1 // Copyright 2014-2023:
2 // GobySoft, LLC (2013-)
3 // Massachusetts Institute of Technology (2007-2014)
4 // Community contributors (see AUTHORS file)
5 // File authors:
6 // Toby Schneider <toby@gobysoft.org>
7 // philboske <philboske@gmail.com>
8 //
9 //
10 // This file is part of the Dynamic Compact Control Language Library
11 // ("DCCL").
12 //
13 // DCCL is free software: you can redistribute it and/or modify
14 // it under the terms of the GNU Lesser General Public License as published by
15 // the Free Software Foundation, either version 2.1 of the License, or
16 // (at your option) any later version.
17 //
18 // DCCL is distributed in the hope that it will be useful,
19 // but WITHOUT ANY WARRANTY; without even the implied warranty of
20 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 // GNU Lesser General Public License for more details.
22 //
23 // You should have received a copy of the GNU Lesser General Public License
24 // along with DCCL. If not, see <http://www.gnu.org/licenses/>.
25 //
26 // For the 'dccl' tool: loading non-GPL shared libraries for the purpose of
27 // using this tool does *not* violate the GPL license terms of DCCL.
28 //
29 
30 #include <fstream>
31 #include <sstream>
32 
33 #include <google/protobuf/descriptor.h>
34 #include <google/protobuf/descriptor.pb.h>
35 #include <google/protobuf/text_format.h>
36 
37 #include "../../binary.h"
38 #include "../../cli_option.h"
39 #include "../../codec.h"
40 
41 #include "dccl/version.h"
42 #include "dccl_tool.pb.h"
43 
44 // for realpath
45 #include <climits>
46 #include <cstdlib>
47 
48 // replacement for boost::trim_if
49 void trim_if(std::string& s, bool (*predicate)(char))
50 {
51  // Trim from the start
52  s.erase(s.begin(),
53  std::find_if(s.begin(), s.end(), [predicate](char ch) { return !predicate(ch); }));
54 
55  // Trim from the end
56  s.erase(
57  std::find_if(s.rbegin(), s.rend(), [predicate](char ch) { return !predicate(ch); }).base(),
58  s.end());
59 }
60 void trim(std::string& s)
61 {
62  trim_if(s, [](char ch) -> bool { return std::isspace(ch); });
63 }
64 std::string trim_copy(const std::string& s)
65 {
66  auto cs = s;
67  trim(cs);
68  return cs;
69 }
70 
71 enum Action
72 {
73  NO_ACTION,
74  ENCODE,
75  DECODE,
76  ANALYZE,
77  DISP_PROTO
78 };
79 enum Format
80 {
81  BINARY,
82  TEXTFORMAT,
83  HEX,
84  BASE64
85 };
86 
87 namespace dccl
88 {
90 namespace tool
91 {
92 struct Config
93 {
94  Config() : id_codec(dccl::Codec::default_id_codec_name()) {}
95 
96  Action action{NO_ACTION};
97  std::set<std::string> include;
98  std::vector<std::string> dlopen;
99  std::set<std::string> message;
100  std::map<std::string, std::size_t> hash;
101  std::set<std::string> proto_file;
102  Format format{BINARY};
103  std::string id_codec;
104  bool verbose{false};
105  bool omit_prefix{false};
106  bool hash_only{false};
107 };
108 } // namespace tool
109 } // namespace dccl
110 
111 void analyze(dccl::Codec& dccl, const dccl::tool::Config& cfg);
112 void encode(dccl::Codec& dccl, dccl::tool::Config& cfg);
113 void decode(dccl::Codec& dccl, const dccl::tool::Config& cfg);
114 void disp_proto(dccl::Codec& dccl, const dccl::tool::Config& cfg);
115 
116 // return { success, hash }
117 std::pair<bool, std::size_t> load_desc(dccl::Codec* dccl, const google::protobuf::Descriptor* desc,
118  const std::string& name);
119 void parse_options(int argc, char* argv[], dccl::tool::Config* cfg, int& console_width_);
120 
121 int main(int argc, char* argv[])
122 {
123  {
124  dccl::tool::Config cfg;
125  int console_width = -1;
126  parse_options(argc, argv, &cfg, console_width);
127 
128  if (!cfg.verbose)
129  dccl::dlog.connect(dccl::logger::WARN_PLUS, &std::cerr);
130  else
131  dccl::dlog.connect(dccl::logger::DEBUG1_PLUS, &std::cerr);
132 
134 
135  for (const auto& it : cfg.include) dccl::DynamicProtobufManager::add_include_path(it);
136 
137  std::string first_dl;
138  if (cfg.dlopen.size())
139  first_dl = cfg.dlopen[0];
140 
141  dccl::Codec dccl(cfg.id_codec, first_dl);
142 
143  if (console_width >= 0)
144  {
145  dccl.set_console_width(console_width);
146  }
147 
148  if (cfg.dlopen.size() > 1)
149  {
150  for (auto it = cfg.dlopen.begin() + 1, n = cfg.dlopen.end(); it != n; ++it)
151  dccl.load_library(*it);
152  }
153  bool no_messages_specified = cfg.message.empty();
154  for (auto it = cfg.proto_file.begin(), end = cfg.proto_file.end(); it != end; ++it)
155  {
156  const google::protobuf::FileDescriptor* file_desc =
158 
159  if (!file_desc)
160  {
161  std::cerr << "failed to read in: " << *it << std::endl;
162  exit(EXIT_FAILURE);
163  }
164 
165  // if no messages explicitly specified, load them all.
166  if (no_messages_specified)
167  {
168  for (int i = 0, n = file_desc->message_type_count(); i < n; ++i)
169  {
170  cfg.message.insert(file_desc->message_type(i)->full_name());
171  if (i == 0 && cfg.action == ENCODE)
172  {
173  std::cerr << "Encoding assuming message: "
174  << file_desc->message_type(i)->full_name() << std::endl;
175  break;
176  }
177  }
178  }
179  }
180 
181  // Load up all the messages
182  for (auto it = cfg.message.begin(); it != cfg.message.end();)
183  {
184  const google::protobuf::Descriptor* desc =
186  // if we can't load the message, erase it from our set of messages
187  bool success;
188  std::size_t hash;
189  std::tie(success, hash) = load_desc(&dccl, desc, *it);
190  if (!success)
191  {
192  it = cfg.message.erase(it);
193  }
194  else
195  {
196  cfg.hash.insert(std::make_pair(*it, hash));
197  ++it;
198  }
199  }
200 
201  switch (cfg.action)
202  {
203  case ENCODE: encode(dccl, cfg); break;
204  case DECODE: decode(dccl, cfg); break;
205  case ANALYZE: analyze(dccl, cfg); break;
206  case DISP_PROTO: disp_proto(dccl, cfg); break;
207  default:
208  std::cerr << "No action specified (e.g. analyze, decode, encode). Try --help."
209  << std::endl;
210  exit(EXIT_SUCCESS);
211  }
212  }
213 }
214 
215 void analyze(dccl::Codec& codec, const dccl::tool::Config& cfg)
216 {
217  for (const auto& name : cfg.message)
218  {
219  if (!cfg.hash_only)
220  {
221  const google::protobuf::Descriptor* desc =
223  codec.info(desc, &std::cout);
224  }
225  else
226  {
227  // only write name when providing hashes for multiple messages
228  if (cfg.message.size() > 1)
229  std::cout << name << ": ";
230  std::cout << dccl::hash_as_string(cfg.hash.at(name)) << std::endl;
231  }
232  }
233 }
234 
235 void encode(dccl::Codec& dccl, dccl::tool::Config& cfg)
236 {
237  if (cfg.message.size() > 1)
238  {
239  std::cerr << "No more than one DCCL message can be specified with -m or --message for "
240  "encoding."
241  << std::endl;
242  exit(EXIT_FAILURE);
243  }
244  else if (cfg.message.size() == 0)
245  {
246  std::cerr << "You must specify a DCCL message to encode with -m" << std::endl;
247  exit(EXIT_FAILURE);
248  }
249 
250  std::string command_line_name = *cfg.message.begin();
251 
252  while (!std::cin.eof())
253  {
254  std::string input;
255  std::getline(std::cin, input);
256 
257  trim(input);
258  if (input.empty())
259  continue;
260 
261  std::string name;
262  if (input[0] == '|')
263  {
264  std::string::size_type close_bracket_pos = input.find('|', 1);
265  if (close_bracket_pos == std::string::npos)
266  {
267  std::cerr << "Incorrectly formatted input: expected '|'" << std::endl;
268  exit(EXIT_FAILURE);
269  }
270 
271  name = input.substr(1, close_bracket_pos - 1);
272  if (cfg.message.find(name) == cfg.message.end())
273  {
274  const google::protobuf::Descriptor* desc =
276  if (!load_desc(&dccl, desc, name).first)
277  {
278  std::cerr << "Could not load descriptor for message " << name << std::endl;
279  exit(EXIT_FAILURE);
280  }
281 
282  cfg.message.insert(name);
283  }
284 
285  if (input.size() > close_bracket_pos + 1)
286  input = input.substr(close_bracket_pos + 1);
287  else
288  input.clear();
289  }
290  else
291  {
292  if (cfg.message.size() == 0)
293  {
294  std::cerr << "Message name not given with -m or in the input (i.e. '[Name] field1: "
295  "value field2: value')."
296  << std::endl;
297  exit(EXIT_FAILURE);
298  }
299 
300  name = command_line_name;
301  }
302 
303  const google::protobuf::Descriptor* desc =
305  if (desc == nullptr)
306  {
307  std::cerr << "No descriptor with name " << name
308  << " found! Make sure you have loaded all the necessary .proto files and/or "
309  "shared libraries. Also make sure you specified the fully qualified name "
310  "including the package, if any (e.g. 'goby.acomms.protobuf.NetworkAck', "
311  "not just 'NetworkAck')."
312  << std::endl;
313  exit(EXIT_FAILURE);
314  }
315 
316  std::shared_ptr<google::protobuf::Message> msg =
318  google::protobuf::TextFormat::ParseFromString(input, msg.get());
319 
320  if (msg->IsInitialized())
321  {
322  std::string encoded;
323  dccl.encode(&encoded, *msg);
324  switch (cfg.format)
325  {
326  default:
327  case BINARY:
328  {
329  std::ofstream fout("/dev/stdout", std::ios::binary | std::ios::app);
330  fout.write(encoded.data(), encoded.size());
331  break;
332  }
333 
334  case TEXTFORMAT:
335  {
336  dccl::tool::protobuf::ByteString s;
337  s.set_b(encoded);
338  std::string output;
339  google::protobuf::TextFormat::PrintFieldValueToString(
340  s, s.GetDescriptor()->FindFieldByNumber(1), -1, &output);
341 
342  std::cout << output << std::endl;
343  break;
344  }
345 
346  case HEX: std::cout << dccl::hex_encode(encoded) << std::endl; break;
347  case BASE64:
348 #if DCCL_HAS_B64
349  std::stringstream instream(encoded);
350  std::stringstream outstream;
351  ::base64::encoder D;
352  D.encode(instream, outstream);
353  std::cout << outstream.str();
354 #else
355  std::cerr << "dccl was not compiled with libb64-dev, so no Base64 "
356  "functionality is available."
357  << std::endl;
358  exit(EXIT_FAILURE);
359 #endif
360  break;
361  }
362  }
363  }
364 }
365 
366 void decode(dccl::Codec& dccl, const dccl::tool::Config& cfg)
367 {
368  std::string input;
369  if (cfg.format == BINARY)
370  {
371  std::ifstream fin("/dev/stdin", std::ios::binary);
372  std::ostringstream ostrm;
373  ostrm << fin.rdbuf();
374  input = ostrm.str();
375  }
376  else
377  {
378  while (!std::cin.eof())
379  {
380  std::string line;
381  std::getline(std::cin, line);
382 
383  if (trim_copy(line).empty())
384  continue;
385 
386  switch (cfg.format)
387  {
388  default:
389  case BINARY: break;
390 
391  case TEXTFORMAT:
392  {
393  trim_if(line, [](char ch) -> bool { return ch == '"'; });
394 
395  dccl::tool::protobuf::ByteString s;
396  google::protobuf::TextFormat::ParseFieldValueFromString(
397  "\"" + line + "\"", s.GetDescriptor()->FindFieldByNumber(1), &s);
398  input += s.b();
399  break;
400  }
401  case HEX: input += dccl::hex_decode(line); break;
402  case BASE64:
403 #if DCCL_HAS_B64
404  std::stringstream instream(line);
405  std::stringstream outstream;
406  ::base64::decoder D;
407  D.decode(instream, outstream);
408  input += outstream.str();
409  break;
410 #else
411  std::cerr << "dccl was not compiled with libb64-dev, so no Base64 "
412  "functionality is available."
413  << std::endl;
414  exit(EXIT_FAILURE);
415 #endif
416  }
417  }
418  }
419 
420  while (!input.empty())
421  {
422  std::shared_ptr<google::protobuf::Message> msg =
423  dccl.decode<std::shared_ptr<google::protobuf::Message>>(&input);
424  if (!cfg.omit_prefix)
425  std::cout << "|" << msg->GetDescriptor()->full_name() << "| ";
426  std::cout << msg->ShortDebugString() << std::endl;
427  }
428 }
429 
430 void disp_proto(dccl::Codec& /*dccl*/, const dccl::tool::Config& cfg)
431 {
432  std::cout << "Please note that for Google Protobuf versions < 2.5.0, the dccl extensions will "
433  "not be show below, so you'll need to refer to the original .proto file."
434  << std::endl;
435  for (const auto& it : cfg.message)
436  {
437  const google::protobuf::Descriptor* desc =
439 
440  std::cout << desc->DebugString();
441  }
442 }
443 
444 std::pair<bool, std::size_t> load_desc(dccl::Codec* dccl, const google::protobuf::Descriptor* desc,
445  const std::string& name)
446 {
447  if (desc)
448  {
449  try
450  {
451  std::size_t hash = dccl->load(desc);
452  return {true, hash};
453  }
454  catch (std::exception& e)
455  {
456  std::cerr << "Not a valid DCCL message: " << desc->full_name()
457  << "\n\tWhy: " << e.what() << std::endl;
458  }
459  }
460  else
461  {
462  std::cerr << "No descriptor with name " << name
463  << " found! Make sure you have loaded all the necessary .proto files and/or "
464  "shared libraries. Try --help."
465  << std::endl;
466  }
467  return {false, 0};
468 }
469 
470 void parse_options(int argc, char* argv[], dccl::tool::Config* cfg, int& console_width_)
471 {
472  std::vector<dccl::Option> options;
473  options.emplace_back('e', "encode", no_argument, "Encode a DCCL message to STDOUT from STDIN");
474  options.emplace_back('d', "decode", no_argument, "Decode a DCCL message to STDOUT from STDIN");
475  options.emplace_back(
476  'a', "analyze", no_argument,
477  "Provides information on a given DCCL message definition (e.g. field sizes)");
478  options.emplace_back('p', "display_proto", no_argument,
479  "Display the .proto definition of this message.");
480  options.emplace_back('h', "help", no_argument, "Gives help on the usage of 'dccl'");
481  options.emplace_back('I', "proto_path", required_argument,
482  "Add another search directory for .proto files");
483  options.emplace_back('l', "dlopen", required_argument,
484  "Open this shared library containing compiled DCCL messages.");
485  options.emplace_back('m', "message", required_argument,
486  "Message name to encode, decode or analyze.");
487  options.emplace_back('f', "proto_file", required_argument, ".proto file to load.");
488  options.emplace_back(0, "format", required_argument,
489  "Format for encode output or decode input: 'bin' (default) is raw binary, "
490  "'hex' is ascii-encoded hexadecimal, 'textformat' is a Google Protobuf "
491  "TextFormat byte string, 'base64' is ascii-encoded base 64.");
492  options.emplace_back('v', "verbose", no_argument, "Display extra debugging information.");
493  options.emplace_back('o', "omit_prefix", no_argument,
494  "Omit the DCCL type name prefix from the output of decode.");
495  options.emplace_back('i', "id_codec", required_argument,
496  "(Advanced) name for a nonstandard DCCL ID codec to use");
497  options.emplace_back('V', "version", no_argument, "DCCL Version");
498  options.emplace_back('w', "console_width", required_argument,
499  "Maximum number of characters used for prettifying console outputs.");
500  options.emplace_back('H', "hash_only", no_argument, "Only display hash for --analyze action.");
501 
502  std::vector<option> long_options;
503  std::string opt_string;
504  dccl::Option::convert_vector(options, &long_options, &opt_string);
505 
506  while (1)
507  {
508  int option_index = 0;
509 
510  int c = getopt_long(argc, argv, opt_string.c_str(), &long_options[0], &option_index);
511  if (c == -1)
512  break;
513 
514  switch (c)
515  {
516  case 0:
517  // If this option set a flag, do nothing else now.
518  if (long_options[option_index].flag != nullptr)
519  break;
520 
521  if (!strcmp(long_options[option_index].name, "format"))
522  {
523  if (!strcmp(optarg, "textformat"))
524  cfg->format = TEXTFORMAT;
525  else if (!strcmp(optarg, "hex"))
526  cfg->format = HEX;
527  else if (!strcmp(optarg, "base64"))
528  cfg->format = BASE64;
529  else if (!strcmp(optarg, "bin"))
530  cfg->format = BINARY;
531  else
532  {
533  std::cerr << "Invalid format '" << optarg << "'" << std::endl;
534  exit(EXIT_FAILURE);
535  }
536  }
537  else
538  {
539  std::cerr << "Try --help for valid options." << std::endl;
540  exit(EXIT_FAILURE);
541  }
542 
543  break;
544 
545  case 'e': cfg->action = ENCODE; break;
546  case 'd': cfg->action = DECODE; break;
547  case 'a': cfg->action = ANALYZE; break;
548  case 'p': cfg->action = DISP_PROTO; break;
549  case 'I': cfg->include.insert(optarg); break;
550  case 'l': cfg->dlopen.emplace_back(optarg); break;
551  case 'm': cfg->message.insert(optarg); break;
552  case 'f':
553  {
554  char* proto_file_canonical_path = realpath(optarg, nullptr);
555  if (proto_file_canonical_path)
556  {
557  cfg->proto_file.insert(proto_file_canonical_path);
558  free(proto_file_canonical_path);
559  }
560  else
561  {
562  std::cerr << "Invalid proto file path: '" << optarg << "'" << std::endl;
563  exit(EXIT_FAILURE);
564  }
565  break;
566  }
567  case 'i': cfg->id_codec = optarg; break;
568  case 'v': cfg->verbose = true; break;
569  case 'o': cfg->omit_prefix = true; break;
570  case 'H': cfg->hash_only = true; break;
571 
572  case 'h':
573  std::cout << "Usage of the Dynamic Compact Control Language (DCCL) tool ('dccl'): "
574  << std::endl;
575  for (auto& option : options) std::cout << " " << option.usage() << std::endl;
576  exit(EXIT_SUCCESS);
577  break;
578 
579  case 'V':
580  std::cout << dccl::VERSION_STRING << std::endl;
581  exit(EXIT_SUCCESS);
582  break;
583 
584  case 'w':
585  {
586  // Robust error checking inspired by https://stackoverflow.com/a/26083517.
587  char* end_ptr = nullptr;
588  errno = 0;
589  auto number = strtol(optarg, &end_ptr, 10);
590 
591  if (optarg == end_ptr)
592  {
593  std::cerr << "Option -w value \'" << optarg
594  << "\' was invalid (no digits found, 0 returned)." << std::endl;
595  exit(EXIT_FAILURE);
596  }
597  else if ((errno == ERANGE) && (number == LONG_MIN))
598  {
599  std::cerr << "Option -w value \'" << optarg
600  << "\' was invalid (underflow occurred)." << std::endl;
601  exit(EXIT_FAILURE);
602  }
603  else if ((errno == ERANGE) && (number == LONG_MAX))
604  {
605  std::cerr << "Option -w value \'" << optarg
606  << "\' was invalid (overflow occurred)." << std::endl;
607  exit(EXIT_FAILURE);
608  }
609  else if (errno == EINVAL)
610  {
611  std::cerr << "Option -w value \'" << optarg
612  << "\' was invalid (base contains unsupported value)." << std::endl;
613  exit(EXIT_FAILURE);
614  }
615  else if ((errno != 0) && (number == 0))
616  {
617  std::cerr << "Option -w value \'" << optarg
618  << "\' was invalid (unspecified error occurred)." << std::endl;
619  exit(EXIT_FAILURE);
620  }
621  else if ((errno == 0) && optarg && (*end_ptr != 0))
622  {
623  std::cerr << "Option -w value \'" << optarg
624  << "\' was invalid (contains additional characters)." << std::endl;
625  exit(EXIT_FAILURE);
626  }
627  else if ((errno == 0) && optarg && !*end_ptr)
628  {
629  if (number >= 0)
630  {
631  console_width_ = number;
632  }
633  else
634  {
635  std::cerr << "Option -w value \'" << optarg
636  << "\' was invalid (negative number)." << std::endl;
637  exit(EXIT_FAILURE);
638  }
639  }
640  else
641  {
642  std::cerr << "Option -w value \'" << optarg << "\' was invalid (unknown error)."
643  << std::endl;
644  exit(EXIT_FAILURE);
645  }
646 
647  break;
648  }
649 
650  case '?': std::cerr << "Try --help for valid options." << std::endl; exit(EXIT_FAILURE);
651  default: exit(EXIT_FAILURE);
652  }
653  }
654 
655  /* Print any remaining command line arguments (not options). */
656  if (optind < argc)
657  {
658  std::cerr << "Unknown arguments: \n";
659  while (optind < argc) std::cerr << argv[optind++];
660  std::cerr << std::endl;
661  std::cerr << "Try --help for valid options." << std::endl;
662  exit(EXIT_FAILURE);
663  }
664 }
dccl
Dynamic Compact Control Language namespace.
Definition: any.h:46
dccl::Codec
The Dynamic CCL enCODer/DECoder. This is the main class you will use to load, encode and decode DCCL ...
Definition: codec.h:62
dccl::DynamicProtobufManager::load_from_proto_file
static const google::protobuf::FileDescriptor * load_from_proto_file(const std::string &protofile_absolute_path)
Load a message from a .proto file on the disk. enable_compilation() must be called first.
Definition: dynamic_protobuf_manager.cpp:112
dccl::Logger::connect
void connect(int verbosity_mask, Slot slot)
Connect the output of one or more given verbosities to a slot (function pointer or similar)
Definition: logger.h:214
dccl::hex_encode
void hex_encode(CharIterator begin, CharIterator end, std::string *out, bool upper_case=false)
Encodes a (little-endian) hexadecimal string from a byte string. Index 0 of begin is written to index...
Definition: binary.h:100
dccl::DynamicProtobufManager::enable_compilation
static void enable_compilation()
Enable on the fly compilation of .proto files on the local disk. Must be called before load_from_prot...
Definition: dynamic_protobuf_manager.h:123
dccl::DynamicProtobufManager::add_include_path
static void add_include_path(const std::string &path)
Add a path for searching for import messages when loading .proto files using load_from_proto_file()
Definition: dynamic_protobuf_manager.cpp:86
dccl::DynamicProtobufManager::new_protobuf_message
static GoogleProtobufMessagePointer new_protobuf_message(const std::string &protobuf_type_name, bool user_pool_first=false)
Create a new (empty) Google Protobuf message of a given type by name.
Definition: dynamic_protobuf_manager.h:76
dccl::Option::convert_vector
static void convert_vector(const std::vector< Option > &options, std::vector< option > *c_options, std::string *opt_string)
Convert a vector of Options into a vector of options (from getopt.h) and an opt_string,...
Definition: cli_option.h:88
dccl::tool::Config
Definition: dccl_tool.cpp:92
dccl::hex_decode
void hex_decode(const std::string &in, std::string *out)
Decodes a (little-endian) hexadecimal string to a byte string. Index 0 and 1 (first byte) of in are w...
Definition: binary.h:51
dccl::DynamicProtobufManager::find_descriptor
static const google::protobuf::Descriptor * find_descriptor(const std::string &protobuf_type_name, bool user_pool_first=false)
Finds the Google Protobuf Descriptor (essentially a meta-class for a given Message) from a given Mess...
Definition: dynamic_protobuf_manager.cpp:36