0.5.4
C++ to UML diagram generator based on Clang
Loading...
Searching...
No Matches
util.cc
Go to the documentation of this file.
1/**
2 * @file src/util/util.cc
3 *
4 * Copyright (c) 2021-2024 Bartek Kryza <bkryza@gmail.com>
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18#include "util.h"
19
20#include <spdlog/spdlog.h>
21
22#include <regex>
23#if __has_include(<sys/utsname.h>)
24#include <sys/utsname.h>
25#endif
26
27namespace clanguml::util {
28
29static const auto WHITESPACE = " \n\r\t\f\v";
30
31namespace {
32class pipe_t {
33public:
34 explicit pipe_t(const std::string &command, int &result)
35 : result_{result}
36 ,
37#if defined(__linux) || defined(__unix) || defined(__APPLE__)
38 pipe_{popen(fmt::format("{} 2>&1", command).c_str(), "r")}
39#elif defined(_WIN32)
40 pipe_{_popen(command.c_str(), "r")}
41#endif
42 {
43 }
44
45 ~pipe_t() { reset(); }
46
47 operator bool() const { return pipe_ != nullptr; }
48
49 FILE *get() const { return pipe_; }
50
51 void reset()
52 {
53 if (pipe_ == nullptr)
54 return;
55
56#if defined(__linux) || defined(__unix) || defined(__APPLE__)
57 result_ = pclose(pipe_);
58#elif defined(_WIN32)
59 result_ = _pclose(pipe_);
60#endif
61 pipe_ = nullptr;
62 }
63
64private:
65 int &result_;
66 FILE *pipe_;
67};
68} // namespace
69
70std::string get_process_output(const std::string &command)
71{
72 constexpr size_t kBufferSize{1024};
73 std::array<char, kBufferSize> buffer{};
74 std::string output;
75 int result{EXIT_FAILURE};
76
77 pipe_t pipe{command, result};
78
79 if (!pipe) {
80 throw std::runtime_error("popen() failed!");
81 }
82
83 while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
84 output += buffer.data();
85 }
86
87 pipe.reset();
88
89 if (result != EXIT_SUCCESS) {
90 throw std::runtime_error(
91 fmt::format("External command '{}' failed: {}", command, output));
92 }
93
94 return output;
95}
96
97void check_process_output(const std::string &command)
98{
99 constexpr size_t kBufferSize{1024};
100 std::array<char, kBufferSize> buffer{};
101 int result{EXIT_FAILURE};
102 std::string output;
103 pipe_t pipe{command, result};
104
105 if (!pipe) {
106 throw std::runtime_error("popen() failed!");
107 }
108
109 while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
110 output += buffer.data();
111 }
112
113 pipe.reset();
114
115 if (result != EXIT_SUCCESS) {
116 throw std::runtime_error(
117 fmt::format("External command '{}' failed: {}", command, output));
118 }
119}
120
121std::string get_env(const std::string &name)
122{
123#if defined(__linux) || defined(__unix)
124 const char *value = std::getenv(name.c_str()); // NOLINT
125
126 if (value == nullptr)
127 return {};
128
129 return std::string{value};
130#elif defined(WINDOWS) || defined(_WIN32) || defined(WIN32)
131 static constexpr auto kMaxEnvLength = 2096U;
132 static char value[kMaxEnvLength];
133 const DWORD ret =
134 GetEnvironmentVariableA(name.c_str(), value, kMaxEnvLength);
135 if (ret == 0 || ret > kMaxEnvLength)
136 return {};
137 else
138 return value;
139#else
140 return {};
141#endif
142}
143
145{
146 const auto env = get_env("CLANGUML_GIT_COMMIT");
147
148 if (!env.empty())
149 return true;
150
151 std::string output;
152
153 try {
154 output = get_process_output("git rev-parse --git-dir");
155 }
156 catch (std::runtime_error &e) {
157 return false;
158 }
159
160 return contains(trim(output), ".git");
161}
162
163std::string run_git_command(
164 const std::string &cmd, const std::string &env_override)
165{
166 auto env = get_env(env_override);
167
168 if (!env.empty())
169 return env;
170
171 std::string output;
172
173 try {
174 output = get_process_output(cmd);
175 }
176 catch (std::runtime_error &e) {
177 return {};
178 }
179
180 return trim(output);
181}
182
183std::string get_git_branch()
184{
185 return run_git_command(
186 "git rev-parse --abbrev-ref HEAD", "CLANGUML_GIT_BRANCH");
187}
188
189std::string get_git_revision()
190{
191 return run_git_command(
192 "git describe --tags --always", "CLANGUML_GIT_REVISION");
193}
194
195std::string get_git_commit()
196{
197 return run_git_command("git rev-parse HEAD", "CLANGUML_GIT_COMMIT");
198}
199
201{
202 return run_git_command(
203 "git rev-parse --show-toplevel", "CLANGUML_GIT_TOPLEVEL_DIR");
204}
205
206std::string get_os_name()
207{
208#ifdef _WIN32
209 return "Windows, 32-bit";
210#elif _WIN64
211 return "Windows, 64-bit";
212#elif __has_include(<sys/utsname.h>)
213 struct utsname utsn; // NOLINT
214 uname(&utsn);
215 return fmt::format("{} {} {}", utsn.sysname, utsn.machine, utsn.release);
216#elif __linux__
217 return "Linux";
218#elif __APPLE__ || __MACH__
219 return "macOS";
220#elif __FreeBSD__
221 return "FreeBSD";
222#elif __unix__ || __unix
223 return "Unix";
224#else
225 return "Unknown";
226#endif
227}
228
229std::string ltrim(const std::string &s)
230{
231 const size_t start = s.find_first_not_of(WHITESPACE);
232 return (start == std::string::npos) ? "" : s.substr(start);
233}
234
235std::string rtrim(const std::string &s)
236{
237 const size_t end = s.find_last_not_of(WHITESPACE);
238 return (end == std::string::npos) ? "" : s.substr(0, end + 1);
239}
240
241std::string trim_typename(const std::string &s)
242{
243 auto res = trim(s);
244 if (res.find("typename ") == 0)
245 return res.substr(strlen("typename "));
246
247 return res;
248}
249
250std::string trim(const std::string &s) { return rtrim(ltrim(s)); }
251
252std::vector<std::string> split(
253 std::string str, std::string_view delimiter, bool skip_empty)
254{
255 std::vector<std::string> result;
256
257 if (!contains(str, delimiter)) {
258 if (!str.empty())
259 result.push_back(std::move(str));
260 else if (!skip_empty)
261 result.push_back(std::move(str));
262 }
263 else
264 while (static_cast<unsigned int>(!str.empty()) != 0U) {
265 auto index = str.find(delimiter);
266 if (index != std::string::npos) {
267 auto tok = str.substr(0, index);
268 if (!tok.empty())
269 result.push_back(std::move(tok));
270 else if (!skip_empty)
271 result.push_back(std::move(tok));
272
273 str = str.substr(index + delimiter.size());
274 }
275 else {
276 if (!str.empty())
277 result.push_back(std::move(str));
278 else if (!skip_empty)
279 result.push_back(std::move(str));
280 str = "";
281 }
282 }
283
284 return result;
285}
286
287std::vector<std::string> split_isspace(std::string str)
288{
289 std::vector<std::string> result;
290
291 while (static_cast<unsigned int>(!str.empty()) != 0U) {
292 auto index = std::find_if(
293 str.begin(), str.end(), [](auto c) { return std::isspace(c); });
294 if (index != str.end()) {
295 auto tok = str.substr(0, std::distance(str.begin(), index));
296 if (!tok.empty())
297 result.push_back(std::move(tok));
298 str = str.substr(std::distance(str.begin(), index) + 1);
299 }
300 else {
301 if (!str.empty())
302 result.push_back(str);
303 str = "";
304 }
305 }
306 return result;
307}
308
309std::string join(
310 const std::vector<std::string> &toks, std::string_view delimiter)
311{
312 return fmt::format("{}", fmt::join(toks, delimiter));
313}
314
315std::string abbreviate(const std::string &s, const unsigned int max_length)
316{
317 if (s.size() <= max_length)
318 return s;
319
320 auto res = s;
321
322 res.resize(max_length);
323
324 if (res.size() > 3) {
325 res.replace(res.size() - 3, 3, 3, '.');
326 }
327
328 return res;
329}
330
332 const std::string &input, std::tuple<std::string, size_t, size_t> &result)
333{
334 const std::regex alias_regex(R"((@A\‍([^\).]+\)))");
335
336 auto alias_it =
337 std::sregex_iterator(input.begin(), input.end(), alias_regex);
338 auto end_it = std::sregex_iterator();
339
340 if (alias_it == end_it)
341 return false;
342
343 const std::smatch &match = *alias_it;
344 const std::string alias = match.str().substr(3, match.str().size() - 4);
345
346 std::get<0>(result) = alias;
347 std::get<1>(result) = match.position();
348 std::get<2>(result) = match.length();
349
350 return true;
351}
352
353bool replace_all(std::string &input, const std::string &pattern,
354 const std::string &replace_with)
355{
356 bool replaced{false};
357
358 auto pos = input.find(pattern);
359 while (pos < input.size()) {
360 input.replace(pos, pattern.size(), replace_with);
361 pos = input.find(pattern, pos + replace_with.size());
362 replaced = true;
363 }
364
365 return replaced;
366}
367
368template <>
370 const std::filesystem::path &path, const std::filesystem::path &prefix)
371{
372 auto normal_path = std::filesystem::path();
373 auto normal_prefix = std::filesystem::path();
374
375 for (const auto &element : path.lexically_normal()) {
376 if (!element.empty())
377 normal_path /= element;
378 }
379
380 for (const auto &element : prefix.lexically_normal()) {
381 if (!element.empty())
382 normal_prefix /= element;
383 }
384
385 auto normal_path_str = normal_path.string();
386 auto normal_prefix_str = normal_prefix.string();
387 return std::search(normal_path_str.begin(), normal_path_str.end(),
388 normal_prefix_str.begin(),
389 normal_prefix_str.end()) == normal_path_str.begin();
390}
391
392template <> bool starts_with(const std::string &s, const std::string &prefix)
393{
394 return s.rfind(prefix, 0) == 0;
395}
396
397template <> bool ends_with(const std::string &value, const std::string &suffix)
398{
399 if (suffix.size() > value.size())
400 return false;
401 return std::equal(suffix.rbegin(), suffix.rend(), value.rbegin());
402}
403
404std::size_t hash_seed(std::size_t seed)
405{
406 constexpr auto kSeedStart{0x6a3712b5};
407 constexpr auto kSeedShiftFirst{6};
408 constexpr auto kSeedShiftSecond{2};
409
410 return kSeedStart + (seed << kSeedShiftFirst) + (seed >> kSeedShiftSecond);
411}
412
413std::string path_to_url(const std::filesystem::path &p)
414{
415 std::vector<std::string> path_tokens;
416 auto it = p.begin();
417 if (p.has_root_directory()) {
418#ifdef _MSC_VER
419 // On Windows convert the root path using its drive letter, e.g.:
420 // C:\A\B\include.h -> /c/A/B/include.h
421 if (p.root_name().string().size() > 1) {
422 if (p.is_absolute()) {
423 path_tokens.push_back(std::string{
424 std::tolower(p.root_name().string().at(0), std::locale())});
425 }
426 it++;
427 }
428#endif
429 it++;
430 }
431
432 for (; it != p.end(); it++)
433 path_tokens.push_back(it->string());
434
435 if (p.has_root_directory())
436 return fmt::format("/{}", fmt::join(path_tokens, "/"));
437
438 return fmt::format("{}", fmt::join(path_tokens, "/"));
439}
440
441std::filesystem::path ensure_path_is_absolute(
442 const std::filesystem::path &p, const std::filesystem::path &root)
443{
444 if (p.is_absolute())
445 return p;
446
447 auto result = root / p;
448 result = result.lexically_normal();
449 result.make_preferred();
450
451 return result;
452}
453
455 const std::filesystem::path &child, const std::filesystem::path &parent)
456{
457 if (child.has_root_directory() != parent.has_root_directory())
458 return false;
459
460 return starts_with(weakly_canonical(child), weakly_canonical(parent));
461}
462
463std::string format_message_comment(const std::string &comment, unsigned width)
464{
465 if (width == 0)
466 return comment;
467
468 std::string result;
469
470 if (comment.empty())
471 return result;
472
473 auto tokens = split_isspace(comment);
474
475 if (tokens.empty())
476 return result;
477
478 unsigned current_line_length{0};
479 for (const auto &token : tokens) {
480 if (current_line_length < width) {
481 result += token;
482 result += ' ';
483 }
484 else {
485 result.back() = '\n';
486 current_line_length = 0;
487 result += token;
488 result += ' ';
489 }
490
491 current_line_length += token.size() + 1;
492 }
493
494 result.pop_back();
495
496 return result;
497}
498
499std::filesystem::path normalize_relative_path(const std::filesystem::path &path)
500{
501 if (path.is_absolute())
502 return path;
503
504 std::filesystem::path result;
505
506 for (const auto &part : path) {
507 if (part == ".") {
508 continue;
509 }
510 result /= part;
511 }
512
513 return result;
514}
515
517 const std::filesystem::path &path, const std::filesystem::path &base)
518{
519 if (path.empty())
520 return false;
521
522 auto normalized_path = normalize_relative_path(path);
523 auto normalized_base = normalize_relative_path(base);
524
525 if (normalized_path == normalized_base)
526 return true;
527
528 auto rel = std::filesystem::relative(normalized_path, normalized_base);
529
530 std::string rel_str = rel.string();
531 return !rel_str.empty() && rel.native()[0] != '.';
532}
533
534std::optional<std::pair<std::string, std::string>> find_entry_by_path_prefix(
535 const std::map<std::string, std::string> &m, const std::string &path)
536{
537 if (m.empty())
538 return {};
539
540 // Extract keys and sort them by length in descending order
541 std::vector<std::string> keys;
542 keys.reserve(m.size());
543 for (const auto &[key, pattern] : m) {
544 keys.push_back(key);
545 }
546
547 std::sort(keys.begin(), keys.end(),
548 [](const std::string &a, const std::string &b) {
549 return a.size() > b.size();
550 });
551
552 std::filesystem::path file_path{path};
553
554 for (const auto &key : keys) {
555 const auto &pattern = m.at(key);
556 std::filesystem::path key_path{key};
557
558 if (is_subpath(file_path, key_path)) {
559 return {{key, pattern}};
560 }
561 }
562
563 if ((path.empty() || file_path.is_relative() || path == ".") &&
564 m.count(".") > 0) {
565 return {{".", m.at(".")}};
566 }
567
568 return {};
569}
570} // namespace clanguml::util