• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tstack / lnav / 19145742013-2651

06 Nov 2025 06:23PM UTC coverage: 69.035% (+0.02%) from 69.012%
19145742013-2651

push

github

tstack
[nits] some minor fixes

0 of 1 new or added line in 1 file covered. (0.0%)

389 existing lines in 1 file now uncovered.

50700 of 73441 relevant lines covered (69.04%)

436008.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.01
/src/base/fs_util.cc
1
/**
2
 * Copyright (c) 2022, Timothy Stack
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 * * Redistributions of source code must retain the above copyright notice, this
10
 * list of conditions and the following disclaimer.
11
 * * Redistributions in binary form must reproduce the above copyright notice,
12
 * this list of conditions and the following disclaimer in the documentation
13
 * and/or other materials provided with the distribution.
14
 * * Neither the name of Timothy Stack nor the names of its contributors
15
 * may be used to endorse or promote products derived from this software
16
 * without specific prior written permission.
17
 *
18
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
19
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
22
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
 */
29

30
#include <algorithm>
31
#include <cstring>
32
#include <filesystem>
33
#include <fstream>
34
#include <iterator>
35
#include <optional>
36
#include <string>
37
#include <utility>
38

39
#include "fs_util.hh"
40

41
#include <errno.h>
42
#include <fcntl.h>
43
#include <limits.h>
44
#include <stdlib.h>
45
#include <sys/param.h>
46
#include <unistd.h>
47

48
#ifdef HAVE_SYS_SYSCTL_H
49
#    include <sys/sysctl.h>
50
#endif
51

52
#include "config.h"
53
#include "fmt/format.h"
54
#include "itertools.hh"
55
#include "lnav_log.hh"
56
#include "opt_util.hh"
57
#include "pcrepp/pcre2pp.hh"
58
#include "scn/scan.h"
59
#include "short_alloc.h"
60
#include "string_util.hh"
61

62
#ifdef HAVE_LIBPROC_H
63
#    include <libproc.h>
64
#endif
65

66
namespace lnav::filesystem {
67
static bool
68
have_cygdrive()
5✔
69
{
70
    static const auto RETVAL = access("/cygdrive", X_OK) == 0;
5✔
71

72
    return RETVAL;
5✔
73
}
74

75
std::string
76
escape_glob_for_win(std::string arg)
×
77
{
78
#if defined(__MSYS__)
79
    std::replace(arg.begin(), arg.end(), '\\', '/');
80
    std::replace(arg.begin(), arg.end(), '^', '\\');
81
    return arg;
82
#else
83
    return arg;
×
84
#endif
85
}
86

87
std::optional<std::filesystem::path>
88
self_path()
678✔
89
{
90
#if defined(HAVE_LIBPROC_H) && defined(PROC_PIDPATHINFO_MAXSIZE)
91
    auto pid = getpid();
92
    char pathbuf[PROC_PIDPATHINFO_MAXSIZE];
93

94
    auto rc = proc_pidpath(pid, pathbuf, sizeof(pathbuf));
95
    if (rc <= 0) {
96
        log_error("unable to determine self path: %s",
97
                  lnav::from_errno().message().c_str());
98
    } else {
99
        log_info("self path: %s", pathbuf);
100
        return std::filesystem::path(pathbuf);
101
    }
102
    return std::nullopt;
103
#elif defined(HAVE_SYS_SYSCTL_H) && defined(KERN_PROC_PATHNAME)
104
    char path[1024];
105
    int mib[4];
106
    size_t len = sizeof(path);
107

108
    mib[0] = CTL_KERN;
109
    mib[1] = KERN_PROC;
110
    mib[2] = KERN_PROC_PATHNAME;
111
    mib[3] = -1;  // current process
112

113
    if (sysctl(mib, 4, path, &len, NULL, 0) == 0) {
114
        return std::filesystem::path(path);
115
    }
116
    log_error("unable to determine path: %s", strerror(errno));
117
    return std::nullopt;
118
#else
119
    std::error_code ec;
678✔
120
    auto target = std::filesystem::read_symlink("/proc/self/exe", ec);
678✔
121
    if (ec) {
678✔
122
        log_error("failed to read /proc/self/exe: %s", ec.message().c_str());
×
123
        return std::nullopt;
×
124
    }
125
    return target;
678✔
126
#endif
127
}
678✔
128

129
static time_t
130
init_self_mtime()
678✔
131
{
132
    auto retval = time_t{};
678✔
133
    auto path_opt = self_path();
678✔
134

135
    time(&retval);
678✔
136
    if (path_opt) {
678✔
137
        auto stat_res = stat_file(path_opt.value());
678✔
138
        if (stat_res.isErr()) {
678✔
139
            log_error("unable to stat self: %s", stat_res.unwrapErr().c_str());
×
140
        } else {
141
            retval = stat_res.unwrap().st_mtime;
678✔
142
        }
143
    }
678✔
144

145
    return retval;
678✔
146
}
678✔
147

148
time_t
149
self_mtime()
118,529✔
150
{
151
    static auto RETVAL = init_self_mtime();
118,529✔
152

153
    return RETVAL;
118,529✔
154
}
155

156
std::string
157
escape_path(const std::filesystem::path& p, path_type pt)
2✔
158
{
159
    auto p_str = p.string();
2✔
160
    std::string retval;
2✔
161

162
    for (const auto ch : p_str) {
14✔
163
        switch (ch) {
12✔
164
            case ' ':
1✔
165
            case '$':
166
            case '\\':
167
            case ';':
168
            case '&':
169
            case '<':
170
            case '>':
171
            case '\'':
172
            case '"':
173
                retval.push_back('\\');
1✔
174
                break;
1✔
175
            case '*':
×
176
            case '[':
177
            case ']':
178
            case '?':
179
                switch (pt) {
×
180
                    case path_type::normal:
×
181
                    case path_type::windows:
182
                    case path_type::remote:
183
                    case path_type::url:
184
                        retval.push_back('\\');
×
185
                        break;
×
186
                    case path_type::pattern:
×
187
                        break;
×
188
                }
189
                break;
×
190
            default:
11✔
191
                break;
11✔
192
        }
193
        retval.push_back(ch);
12✔
194
    }
195

196
    return retval;
4✔
197
}
2✔
198

199
bool
200
is_url(const std::string& fn)
1,379✔
201
{
202
    static const auto url_re
203
        = lnav::pcre2pp::code::from_const("^(file|https?|ftps?|scp|sftp):.*");
1,379✔
204

205
    return url_re.find_in(fn).ignore_error().has_value();
1,379✔
206
}
207

208
path_type
209
determine_path_type(const std::string& arg)
545✔
210
{
211
    if (is_glob(arg)) {
545✔
212
        return path_type::pattern;
12✔
213
    }
214

215
    if (is_url(arg)) {
533✔
216
        return path_type::url;
×
217
    }
218

219
    const auto colon_pos = arg.find(':');
533✔
220
    if (colon_pos == std::string::npos) {
533✔
221
        return path_type::normal;
530✔
222
    }
223
    if (colon_pos == 1) {
3✔
224
        return path_type::windows;
×
225
    }
226
    return path_type::remote;
3✔
227
}
228

229
path_transcoder
230
path_transcoder::from(std::string arg)
6✔
231
{
232
    if (cget(arg, 1).value_or('\0') != ':') {
6✔
233
        std::optional<bool> caps;
1✔
234
#if defined(__MSYS__)
235
        if (arg.find('\\') != std::string::npos) {
236
            std::replace(arg.begin(), arg.end(), '\\', '/');
237
            caps = false;
238
        }
239
        if (startswith(arg, "//")) {
240
        } else if (startswith(arg, "/")) {
241
            auto cwd = std::filesystem::current_path();
242
            auto cwd_iter = std::next(cwd.begin());
243
            auto cwd_first_str = cwd_iter->string();
244
            if (cwd_first_str == "cygdrive") {
245
                auto drive_iter = std::next(cwd_iter);
246
                if (drive_iter != cwd.end()) {
247
                    auto cwd_drive_str = drive_iter->string();
248
                    arg.insert(0, cwd_drive_str);
249
                    arg.insert(0, "/");
250
                    caps = true;
251
                }
252
            }
253
            arg.insert(0, cwd_first_str);
254
            arg.insert(0, "/");
255
        }
256
#endif
257
        return {arg, caps};
1✔
258
    }
259

260
    bool caps = isupper(arg[0]);
5✔
261
    if (caps) {
5✔
262
        arg[0] = tolower(arg[0]);
1✔
263
    }
264

265
    switch (cget(arg, 2).value_or('\0')) {
5✔
266
        case '\\':
3✔
267
        case '/':
268
            arg.erase(1, 1);
3✔
269
            break;
3✔
270
        default:
2✔
271
            arg[1] = '/';
2✔
272
            break;
2✔
273
    }
274

275
    arg.insert(arg.begin(), '/');
5✔
276
    if (have_cygdrive()) {
5✔
277
        arg.insert(0, "/cygdrive");
×
278
    }
279
    std::replace(arg.begin(), arg.end(), '\\', '/');
5✔
280

281
    return {arg, caps};
5✔
282
}
6✔
283

284
std::string
285
path_transcoder::to_native(std::string arg)
×
286
{
NEW
287
    if (arg.empty() || !this->pt_root_name_capitalized) {
×
288
        return arg;
×
289
    }
290

291
    static const auto CYGDRIVE = "/cygdrive"_frag;
292

293
    if (startswith(arg, CYGDRIVE.data())) {
×
294
        arg.erase(0, CYGDRIVE.length());
×
295
    }
296

297
    if (arg[0] == '/' && !startswith(arg, "//")) {
×
298
        arg.erase(0, 1);
×
299
        if (cget(arg, 1).value_or('\0') == '/') {
×
300
            arg.insert(1, ":");
×
301
        }
302
    }
303
    if (this->pt_root_name_capitalized.value()) {
×
304
        arg[0] = toupper(arg[0]);
×
305
    }
306
    std::replace(arg.begin(), arg.end(), '/', '\\');
×
307

308
    return arg;
×
309
}
310

311
std::string
312
path_transcoder::to_shell_arg(std::string arg)
×
313
{
314
    static const auto plain_path_re
315
        = lnav::pcre2pp::code::from_const(R"(^[\w/]+$)");
×
316

317
    if (plain_path_re.find_in(arg).ignore_error()) {
×
318
        return arg;
×
319
    }
320

321
    // XXX
322
    return fmt::format(FMT_STRING("'{}'"), arg);
×
323
}
324

325
std::pair<std::string, file_location_t>
326
split_file_location(const std::string& file_path_str)
555✔
327
{
328
    if (access(file_path_str.c_str(), R_OK) == 0) {
555✔
329
        return {file_path_str, file_location_t{default_for_text_format{}}};
525✔
330
    }
331

332
    auto colon_index = file_path_str.rfind(':');
30✔
333
    if (colon_index != std::string::npos) {
30✔
334
        auto top_range
335
            = std::string_view{&file_path_str[colon_index + 1],
10✔
336
                               file_path_str.size() - colon_index - 1};
10✔
337
        auto scan_res = scn::scan_value<int>(top_range);
10✔
338

339
        if (scan_res && scan_res->range().empty()) {
10✔
340
            return std::make_pair(file_path_str.substr(0, colon_index),
4✔
341
                                  scan_res->value());
4✔
342
        }
343
        log_info("did not parse line number from file path with colon: %s",
8✔
344
                 file_path_str.c_str());
345
    }
346

347
    auto hash_index = file_path_str.rfind('#');
28✔
348
    if (hash_index != std::string::npos) {
28✔
349
        return std::make_pair(file_path_str.substr(0, hash_index),
2✔
350
                              file_path_str.substr(hash_index));
3✔
351
    }
352

353
    return std::make_pair(file_path_str,
354
                          file_location_t{default_for_text_format{}});
27✔
355
}
356

357
Result<std::filesystem::path, std::string>
358
realpath(const std::filesystem::path& path)
698✔
359
{
360
    char resolved[PATH_MAX];
361
    auto rc = ::realpath(path.c_str(), resolved);
698✔
362

363
    if (rc == nullptr) {
698✔
364
        return Err(lnav::from_errno().message());
×
365
    }
366

367
    return Ok(std::filesystem::path(resolved));
698✔
368
}
369

370
Result<auto_fd, std::string>
371
create_file(const std::filesystem::path& path, int flags, mode_t mode)
77✔
372
{
373
    auto fd = openp(path, flags | O_CREAT, mode);
77✔
374

375
    if (fd == -1) {
77✔
376
        return Err(fmt::format(FMT_STRING("Failed to open: {} -- {}"),
×
377
                               path.string(),
×
378
                               lnav::from_errno()));
×
379
    }
380

381
    return Ok(auto_fd(fd));
77✔
382
}
383

384
Result<auto_fd, std::string>
385
open_file(const std::filesystem::path& path, int flags)
1,401✔
386
{
387
    auto fd = openp(path, flags);
1,401✔
388

389
    if (fd == -1) {
1,401✔
390
        return Err(fmt::format(FMT_STRING("Failed to open: {} -- {}"),
×
391
                               path.string(),
×
392
                               lnav::from_errno()));
×
393
    }
394

395
    return Ok(auto_fd(fd));
1,401✔
396
}
397

398
Result<std::pair<std::filesystem::path, auto_fd>, std::string>
399
open_temp_file(const std::filesystem::path& pattern)
10,143✔
400
{
401
    auto pattern_str = pattern.string();
10,143✔
402
    stack_buf allocator;
10,143✔
403
    auto* pattern_copy = allocator.allocate(pattern_str.size() + 1);
10,143✔
404
    int fd;
405

406
    strcpy(pattern_copy, pattern_str.c_str());
10,143✔
407
#if HAVE_MKOSTEMP
408
    fd = mkostemp(pattern_copy, O_CLOEXEC);
10,143✔
409
#else
410
    fd = mkstemp(pattern_copy);
411
    if (fd != -1) {
412
        fcntl(fd, F_SETFD, FD_CLOEXEC);
413
    }
414
#endif
415
    if (fd == -1) {
10,143✔
416
        return Err(
6,052✔
417
            fmt::format(FMT_STRING("unable to create temporary file: {} -- {}"),
30,260✔
418
                        pattern.string(),
12,104✔
419
                        lnav::from_errno()));
18,156✔
420
    }
421

422
    return Ok(std::make_pair(std::filesystem::path(pattern_copy), auto_fd(fd)));
4,091✔
423
}
10,143✔
424

425
Result<std::string, std::string>
426
read_file(const std::filesystem::path& path)
1,500✔
427
{
428
    try {
429
        std::ifstream file_stream(path);
1,500✔
430

431
        if (!file_stream) {
1,500✔
432
            return Err(lnav::from_errno().message());
743✔
433
        }
434

435
        std::string retval;
757✔
436
        retval.assign((std::istreambuf_iterator<char>(file_stream)),
757✔
437
                      std::istreambuf_iterator<char>());
438
        return Ok(retval);
757✔
439
    } catch (const std::exception& e) {
1,500✔
440
        return Err(std::string(e.what()));
×
441
    }
×
442
}
443

444
Result<write_file_result, std::string>
445
write_file(const std::filesystem::path& path,
10,142✔
446
           string_fragment_producer& content,
447
           lnav::enums::bitset<write_file_options> options)
448
{
449
    write_file_result retval;
10,142✔
450
    auto tmp_pattern = path;
10,142✔
451
    tmp_pattern += ".XXXXXX";
10,142✔
452

453
    auto tmp_pair = TRY(open_temp_file(tmp_pattern));
10,142✔
454
    auto for_res = content.for_each(
455
        [&tmp_pair](string_fragment sf) -> Result<void, std::string> {
×
456
            auto bytes_written
457
                = write(tmp_pair.second.get(), sf.data(), sf.length());
8,899✔
458
            if (bytes_written < 0) {
8,899✔
459
                return Err(fmt::format(
×
460
                    FMT_STRING("unable to write to temporary file {}: {}"),
×
461
                    tmp_pair.first.string(),
×
462
                    lnav::from_errno()));
×
463
            }
464

465
            if (bytes_written != sf.length()) {
8,899✔
466
                return Err(
×
467
                    fmt::format(FMT_STRING("short write to file {}: {} < {}"),
×
468
                                tmp_pair.first.string(),
×
469
                                bytes_written,
470
                                sf.length()));
×
471
            }
472

473
            return Ok();
8,899✔
474
        });
4,090✔
475

476
    if (for_res.isErr()) {
4,090✔
477
        return Err(for_res.unwrapErr());
×
478
    }
479

480
    std::error_code ec;
4,090✔
481
    if (options.is_set<write_file_options::backup_existing>()) {
4,090✔
482
        if (std::filesystem::exists(path, ec)) {
2✔
483
            auto backup_path = path;
1✔
484

485
            backup_path += ".bak";
1✔
486
            std::filesystem::rename(path, backup_path, ec);
1✔
487
            if (ec) {
1✔
488
                return Err(
×
489
                    fmt::format(FMT_STRING("unable to backup file {}: {}"),
×
490
                                path.string(),
×
491
                                ec.message()));
×
492
            }
493

494
            retval.wfr_backup_path = backup_path;
1✔
495
        }
1✔
496
    }
497

498
    auto mode = S_IRUSR | S_IWUSR;
4,090✔
499
    if (options.is_set<write_file_options::executable>()) {
4,090✔
500
        mode |= S_IXUSR;
819✔
501
    }
502
    if (options.is_set<write_file_options::read_only>()) {
4,090✔
503
        mode &= ~S_IWUSR;
3,432✔
504
    }
505

506
    fchmod(tmp_pair.second.get(), mode);
4,090✔
507

508
    std::filesystem::rename(tmp_pair.first, path, ec);
4,090✔
509
    if (ec) {
4,090✔
510
        return Err(
×
511
            fmt::format(FMT_STRING("unable to move temporary file {}: {}"),
×
512
                        tmp_pair.first.string(),
×
513
                        ec.message()));
×
514
    }
515

516
    log_debug("wrote file: %s", path.c_str());
4,090✔
517
    return Ok(retval);
4,090✔
518
}
10,142✔
519

520
std::string
521
build_path(const std::vector<std::filesystem::path>& paths)
5✔
522
{
523
    return paths
524
        | lnav::itertools::map([](const auto& path) { return path.string(); })
22✔
525
        | lnav::itertools::append(getenv_opt("PATH").value_or(""))
15✔
526
        | lnav::itertools::filter_out(&std::string::empty)
15✔
527
        | lnav::itertools::fold(
10✔
528
               [](const auto& elem, auto& accum) {
9✔
529
                   if (!accum.empty()) {
9✔
530
                       accum.push_back(':');
5✔
531
                   }
532
                   return accum.append(elem);
9✔
533
               },
534
               std::string());
15✔
535
}
536

537
Result<struct stat, std::string>
538
stat_file(const std::filesystem::path& path)
82,079✔
539
{
540
    struct stat retval;
541

542
    if (statp(path, &retval) == 0) {
82,079✔
543
        return Ok(retval);
71,954✔
544
    }
545

546
    return Err(fmt::format(FMT_STRING("failed to find file: {} -- {}"),
50,625✔
547
                           path.string(),
20,250✔
548
                           lnav::from_errno()));
30,375✔
549
}
550

551
file_lock::file_lock(const std::filesystem::path& archive_path)
×
552
{
553
    auto lock_path = archive_path;
×
554

555
    lock_path += ".lck";
×
556
    auto open_res
557
        = lnav::filesystem::create_file(lock_path, O_RDWR | O_CLOEXEC, 0600);
×
558
    if (open_res.isErr()) {
×
559
        throw std::runtime_error(open_res.unwrapErr());
×
560
    }
561
    this->lh_fd = open_res.unwrap();
×
562
}
563
}  // namespace lnav::filesystem
564

565
namespace fmt {
566
auto
567
formatter<std::filesystem::path>::format(const std::filesystem::path& p,
1,218✔
568
                                         format_context& ctx)
569
    -> decltype(ctx.out()) const
570
{
571
    auto esc_res = fmt::v10::detail::find_escape(&(*p.native().begin()),
1,218✔
572
                                                 &(*p.native().end()));
1,218✔
573
    if (esc_res.end == nullptr) {
1,218✔
574
        return formatter<string_view>::format(p.native(), ctx);
2,436✔
575
    }
576

577
    return format_to(ctx.out(), FMT_STRING("{:?}"), p.native());
×
578
}
579
}  // namespace fmt
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc