DCCL v4
Loading...
Searching...
No Matches
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
49void 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}
60void trim(std::string& s)
61{
62 trim_if(s, [](char ch) -> bool { return std::isspace(ch); });
63}
64std::string trim_copy(const std::string& s)
65{
66 auto cs = s;
67 trim(cs);
68 return cs;
69}
70
71enum Action
72{
73 NO_ACTION,
74 ENCODE,
75 DECODE,
76 ANALYZE,
77 DISP_PROTO
78};
79enum Format
80{
81 BINARY,
82 TEXTFORMAT,
83 HEX,
84 BASE64
85};
86
87namespace dccl
88{
90namespace tool
91{
92struct 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
111void analyze(dccl::Codec& dccl, const dccl::tool::Config& cfg);
112void encode(dccl::Codec& dccl, dccl::tool::Config& cfg);
113void decode(dccl::Codec& dccl, const dccl::tool::Config& cfg);
114void disp_proto(dccl::Codec& dccl, const dccl::tool::Config& cfg);
115
116// return { success, hash }
117std::pair<bool, std::size_t> load_desc(dccl::Codec* dccl, const google::protobuf::Descriptor* desc,
118 const std::string& name);
119void parse_options(int argc, char* argv[], dccl::tool::Config* cfg, int& console_width_);
120
121int main(int argc, char* argv[])
122{
123 {
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
215void 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
235void 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
366void 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
430void 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
444std::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
470void 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}
The Dynamic CCL enCODer/DECoder. This is the main class you will use to load, encode and decode DCCL ...
Definition codec.h:63
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.
static void enable_compilation()
Enable on the fly compilation of .proto files on the local disk. Must be called before load_from_prot...
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...
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.
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
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
Dynamic Compact Control Language namespace.
Definition any.h:47
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
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