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