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

saitoha / libsixel / 19389365033

15 Nov 2025 11:44AM UTC coverage: 43.379% (-0.4%) from 43.821%
19389365033

push

github

saitoha
palette: refactor palette helpers into dedicated modules

8474 of 27744 branches covered (30.54%)

44 of 650 new or added lines in 4 files covered. (6.77%)

106 existing lines in 9 files now uncovered.

11581 of 26697 relevant lines covered (43.38%)

973309.3 hits per line

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

76.86
/src/options.c
1
/*
2
 * Copyright (c) 2025 libsixel developers. See `AUTHORS`.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 */
23

24
#if !defined(_POSIX_C_SOURCE)
25
# define _POSIX_C_SOURCE 200809L
26
#endif
27

28
#include "config.h"
29

30
#include <ctype.h>
31
#include <errno.h>
32
#include <stdio.h>
33
#include <stdlib.h>
34
#include <string.h>
35
#include <time.h>
36

37
#if HAVE_DIRENT_H
38
# include <dirent.h>
39
#endif
40
#if HAVE_SYS_STAT_H
41
# include <sys/stat.h>
42
#endif
43

44
#include <sixel.h>
45
#include "options.h"
46
#include "output.h"
47

48
/*
49
 * The option helper entry points centralize prefix matching and
50
 * diagnostic reporting used by both the encoder and the decoder.  The
51
 * implementations stay here so the CLI remains thin while the library
52
 * can share the matching logic.
53
 */
54

55
sixel_option_choice_result_t
56
sixel_option_match_choice(
192✔
57
    char const *value,
58
    sixel_option_choice_t const *choices,
59
    size_t choice_count,
60
    int *matched_value,
61
    char *diagnostic,
62
    size_t diagnostic_size)
63
{
64
    size_t index;
65
    size_t value_length;
66
    int candidate_index;
67
    size_t match_count;
68
    int base_value;
69
    int base_value_set;
70
    int ambiguous_values;
71
    size_t diag_length;
72
    size_t copy_length;
73

74
    if (diagnostic != NULL && diagnostic_size > 0u) {
192!
75
        diagnostic[0] = '\0';
192✔
76
    }
77
    if (value == NULL) {
192!
78
        return SIXEL_OPTION_CHOICE_NONE;
×
79
    }
80

81
    value_length = strlen(value);
192✔
82
    if (value_length == 0u) {
192!
83
        return SIXEL_OPTION_CHOICE_NONE;
×
84
    }
85

86
    index = 0u;
192✔
87
    candidate_index = (-1);
192✔
88
    match_count = 0u;
192✔
89
    base_value = 0;
192✔
90
    base_value_set = 0;
192✔
91
    ambiguous_values = 0;
192✔
92
    diag_length = 0u;
192✔
93

94
    while (index < choice_count) {
1,030✔
95
        if (strncmp(choices[index].name, value, value_length) == 0) {
970✔
96
            if (choices[index].name[value_length] == '\0') {
178✔
97
                *matched_value = choices[index].value;
132✔
98
                return SIXEL_OPTION_CHOICE_MATCH;
132✔
99
            }
100
            if (!base_value_set) {
46✔
101
                base_value = choices[index].value;
34✔
102
                base_value_set = 1;
34✔
103
            } else if (choices[index].value != base_value) {
12✔
104
                ambiguous_values = 1;
8✔
105
            }
106
            if (candidate_index == (-1)) {
46✔
107
                candidate_index = (int)index;
34✔
108
            }
109
            ++match_count;
46✔
110
            if (diagnostic != NULL && diagnostic_size > 0u) {
46!
111
                if (diag_length > 0u && diag_length + 2u < diagnostic_size) {
46!
112
                    diagnostic[diag_length] = ',';
12✔
113
                    diagnostic[diag_length + 1u] = ' ';
12✔
114
                    diag_length += 2u;
12✔
115
                    diagnostic[diag_length] = '\0';
12✔
116
                }
117
                copy_length = strlen(choices[index].name);
46✔
118
                if (copy_length > diagnostic_size - diag_length - 1u) {
46!
119
                    copy_length = diagnostic_size - diag_length - 1u;
×
120
                }
121
                memcpy(diagnostic + diag_length,
46✔
122
                       choices[index].name,
46✔
123
                       copy_length);
124
                diag_length += copy_length;
46✔
125
                diagnostic[diag_length] = '\0';
46✔
126
            }
127
        }
128
        ++index;
838✔
129
    }
130

131
    if (match_count == 0u || candidate_index == (-1)) {
60!
132
        return SIXEL_OPTION_CHOICE_NONE;
26✔
133
    }
134
    if (!ambiguous_values) {
34✔
135
        *matched_value = choices[candidate_index].value;
28✔
136
        return SIXEL_OPTION_CHOICE_MATCH;
28✔
137
    }
138

139
    return SIXEL_OPTION_CHOICE_AMBIGUOUS;
6✔
140
}
141

142
void
143
sixel_option_report_ambiguous_prefix(
6✔
144
    char const *value,
145
    char const *candidates,
146
    char *buffer,
147
    size_t buffer_size)
148
{
149
    int written;
150

151
    if (buffer == NULL || buffer_size == 0u) {
6!
152
        return;
×
153
    }
154
    if (candidates != NULL && candidates[0] != '\0') {
6!
155
        written = snprintf(buffer,
6✔
156
                           buffer_size,
157
                           "ambiguous prefix \"%s\" (matches: %s).",
158
                           value,
159
                           candidates);
160
    } else {
161
        written = snprintf(buffer,
×
162
                           buffer_size,
163
                           "ambiguous prefix \"%s\".",
164
                           value);
165
    }
166
    (void) written;
167
    sixel_helper_set_additional_message(buffer);
6✔
168
}
169

170
#define SIXEL_OPTION_SUGGESTION_LIMIT 5u
171
#define SIXEL_OPTION_SUGGESTION_NAME_WEIGHT 0.55
172
#define SIXEL_OPTION_SUGGESTION_EXTENSION_WEIGHT 0.25
173
#define SIXEL_OPTION_SUGGESTION_RECENCY_WEIGHT 0.20
174

175
/*
176
 * The suggestion engine mirrors the fuzzy finder heuristics used by the
177
 * converters before the refactor.  Filename similarity dominates the ranking
178
 * while matching extensions and recent modification times act as tiebreakers.
179
 */
180

181
typedef struct sixel_option_path_candidate {
182
    char *path;
183
    char const *name;
184
    time_t mtime;
185
    double name_score;
186
    double extension_score;
187
    double recency_score;
188
    double total_score;
189
} sixel_option_path_candidate_t;
190

191
static int
192
sixel_option_case_insensitive_equals(
14✔
193
    char const *lhs,
194
    char const *rhs)
195
{
196
    size_t index;
197
    unsigned char left;
198
    unsigned char right;
199

200
    index = 0u;
14✔
201

202
    if (lhs == NULL || rhs == NULL) {
14!
203
        return 0;
×
204
    }
205

206
    while (lhs[index] != '\0' && rhs[index] != '\0') {
38!
207
        left = (unsigned char)lhs[index];
30✔
208
        right = (unsigned char)rhs[index];
30✔
209
        if (tolower(left) != tolower(right)) {
30✔
210
            return 0;
6✔
211
        }
212
        ++index;
24✔
213
    }
214

215
    return lhs[index] == '\0' && rhs[index] == '\0';
8!
216
}
217

218
static char const *
219
sixel_option_basename_view(char const *path)
88✔
220
{
221
    char const *forward;
222
#if defined(_WIN32)
223
    char const *backward;
224
#endif
225
    char const *start;
226

227
    forward = NULL;
88✔
228
#if defined(_WIN32)
229
    backward = NULL;
230
#endif
231
    start = path;
88✔
232

233
    if (path == NULL) {
88!
234
        return NULL;
×
235
    }
236

237
    forward = strrchr(path, '/');
88✔
238
#if defined(_WIN32)
239
    backward = strrchr(path, '\\');
240
    if (backward != NULL && (forward == NULL || backward > forward)) {
241
        forward = backward;
242
    }
243
#endif
244

245
    if (forward != NULL) {
88✔
246
        return forward + 1;
86✔
247
    }
248

249
    return start;
2✔
250
}
251

252
static char const *
253
sixel_option_extension_view(char const *name)
164✔
254
{
255
    char const *dot;
256

257
    dot = NULL;
164✔
258

259
    if (name == NULL) {
164!
260
        return NULL;
×
261
    }
262

263
    dot = strrchr(name, '.');
164✔
264
    if (dot == NULL || dot == name) {
164!
265
        return NULL;
74✔
266
    }
267

268
    return dot + 1;
90✔
269
}
270

271
static double
272
sixel_option_normalized_levenshtein(
82✔
273
    char const *lhs,
274
    char const *rhs)
275
{
276
    size_t lhs_length;
277
    size_t rhs_length;
278
    size_t *previous;
279
    size_t *current;
280
    size_t column;
281
    size_t row;
282
    size_t cost;
283
    size_t deletion;
284
    size_t insertion;
285
    size_t substitution;
286
    double distance_double;
287
    double normalized;
288

289
    lhs_length = 0u;
82✔
290
    rhs_length = 0u;
82✔
291
    previous = NULL;
82✔
292
    current = NULL;
82✔
293
    column = 0u;
82✔
294
    row = 0u;
82✔
295
    cost = 0u;
82✔
296
    deletion = 0u;
82✔
297
    insertion = 0u;
82✔
298
    substitution = 0u;
82✔
299
    distance_double = 0.0;
82✔
300
    normalized = 0.0;
82✔
301

302
    if (lhs == NULL || rhs == NULL) {
82!
303
        return 0.0;
×
304
    }
305

306
    lhs_length = strlen(lhs);
82✔
307
    rhs_length = strlen(rhs);
82✔
308
    if (lhs_length == 0u && rhs_length == 0u) {
82!
309
        return 1.0;
×
310
    }
311

312
    previous = (size_t *)malloc((rhs_length + 1u) * sizeof(size_t));
82✔
313
    if (previous == NULL) {
82!
314
        return 0.0;
×
315
    }
316

317
    current = (size_t *)malloc((rhs_length + 1u) * sizeof(size_t));
82✔
318
    if (current == NULL) {
82!
319
        free(previous);
×
320
        return 0.0;
×
321
    }
322

323
    column = 0u;
82✔
324
    while (column <= rhs_length) {
1,546✔
325
        previous[column] = column;
1,464✔
326
        ++column;
1,464✔
327
    }
328

329
    row = 1u;
82✔
330
    while (row <= lhs_length) {
1,324✔
331
        current[0] = row;
1,242✔
332
        column = 1u;
1,242✔
333
        while (column <= rhs_length) {
22,364✔
334
            cost = (lhs[row - 1u] == rhs[column - 1u]) ? 0u : 1u;
21,122✔
335
            deletion = previous[column] + 1u;
21,122✔
336
            insertion = current[column - 1u] + 1u;
21,122✔
337
            substitution = previous[column - 1u] + cost;
21,122✔
338
            current[column] = deletion;
21,122✔
339
            if (insertion < current[column]) {
21,122✔
340
                current[column] = insertion;
9,958✔
341
            }
342
            if (substitution < current[column]) {
21,122✔
343
                current[column] = substitution;
2,730✔
344
            }
345
            ++column;
21,122✔
346
        }
347
        memcpy(previous, current, (rhs_length + 1u) * sizeof(size_t));
1,242✔
348
        ++row;
1,242✔
349
    }
350

351
    distance_double = (double)previous[rhs_length];
82✔
352
    free(current);
82✔
353
    free(previous);
82✔
354

355
    normalized = 1.0 - distance_double /
82✔
356
        (double)((lhs_length > rhs_length) ? lhs_length : rhs_length);
82✔
357
    if (normalized < 0.0) {
82!
358
        normalized = 0.0;
×
359
    }
360

361
    return normalized;
82✔
362
}
363

364
static double
365
sixel_option_extension_similarity(
82✔
366
    char const *lhs,
367
    char const *rhs)
368
{
369
    char const *lhs_extension;
370
    char const *rhs_extension;
371

372
    lhs_extension = sixel_option_extension_view(lhs);
82✔
373
    rhs_extension = sixel_option_extension_view(rhs);
82✔
374
    if (lhs_extension == NULL || rhs_extension == NULL) {
82!
375
        return 0.0;
68✔
376
    }
377
    if (sixel_option_case_insensitive_equals(lhs_extension, rhs_extension)) {
14!
378
        return 1.0;
×
379
    }
380

381
    return 0.0;
14✔
382
}
383

384
static char *
385
sixel_option_duplicate_string(char const *text)
2✔
386
{
387
    size_t length;
388
    char *copy;
389

390
    length = 0u;
2✔
391
    copy = NULL;
2✔
392

393
    if (text == NULL) {
2!
394
        return NULL;
×
395
    }
396

397
    length = strlen(text);
2✔
398
    copy = (char *)malloc(length + 1u);
2✔
399
    if (copy == NULL) {
2!
400
        return NULL;
×
401
    }
402

403
    if (length > 0u) {
2!
404
        memcpy(copy, text, length);
2✔
405
    }
406
    copy[length] = '\0';
2✔
407

408
    return copy;
2✔
409
}
410

411
static char *
412
sixel_option_duplicate_directory(char const *path)
6✔
413
{
414
    char const *forward;
415
#if defined(_WIN32)
416
    char const *backward;
417
#endif
418
    char const *separator;
419
    size_t length;
420
    char *copy;
421

422
    forward = NULL;
6✔
423
#if defined(_WIN32)
424
    backward = NULL;
425
#endif
426
    separator = NULL;
6✔
427
    length = 0u;
6✔
428
    copy = NULL;
6✔
429

430
    if (path == NULL || path[0] == '\0') {
6!
431
        return sixel_option_duplicate_string(".");
×
432
    }
433

434
    forward = strrchr(path, '/');
6✔
435
#if defined(_WIN32)
436
    backward = strrchr(path, '\\');
437
    if (backward != NULL && (forward == NULL || backward > forward)) {
438
        forward = backward;
439
    }
440
#endif
441
    separator = forward;
6✔
442

443
    if (separator == NULL) {
6✔
444
        return sixel_option_duplicate_string(".");
2✔
445
    }
446
    if (separator == path) {
4!
447
        return sixel_option_duplicate_string("/");
×
448
    }
449

450
    length = (size_t)(separator - path);
4✔
451
    copy = (char *)malloc(length + 1u);
4✔
452
    if (copy == NULL) {
4!
453
        return NULL;
×
454
    }
455
    if (length > 0u) {
4!
456
        memcpy(copy, path, length);
4✔
457
    }
458
    copy[length] = '\0';
4✔
459

460
    return copy;
4✔
461
}
462

463
static char *
464
sixel_option_join_directory_entry(
88✔
465
    char const *directory,
466
    char const *entry)
467
{
468
    size_t directory_length;
469
    size_t entry_length;
470
    int needs_separator;
471
    char *joined;
472

473
    directory_length = 0u;
88✔
474
    entry_length = 0u;
88✔
475
    needs_separator = 0;
88✔
476
    joined = NULL;
88✔
477

478
    if (directory == NULL || entry == NULL) {
88!
479
        return NULL;
×
480
    }
481

482
    directory_length = strlen(directory);
88✔
483
    entry_length = strlen(entry);
88✔
484
    if (directory_length == 0u) {
88!
485
        needs_separator = 0;
×
486
    } else if (directory[directory_length - 1u] == '/'
88!
487
#if defined(_WIN32)
488
               || directory[directory_length - 1u] == '\\'
489
#endif
490
               ) {
491
        needs_separator = 0;
×
492
    } else {
493
        needs_separator = 1;
88✔
494
    }
495

496
    joined = (char *)malloc(directory_length + entry_length +
88✔
497
                            (size_t)needs_separator + 1u);
88✔
498
    if (joined == NULL) {
88!
499
        return NULL;
×
500
    }
501

502
    if (directory_length > 0u) {
88!
503
        memcpy(joined, directory, directory_length);
88✔
504
    }
505
    if (needs_separator) {
88!
506
        joined[directory_length] = '/';
88✔
507
    }
508
    if (entry_length > 0u) {
88!
509
        memcpy(joined + directory_length + (size_t)needs_separator,
88✔
510
               entry,
511
               entry_length);
512
    }
513
    joined[directory_length + (size_t)needs_separator + entry_length] = '\0';
88✔
514

515
    return joined;
88✔
516
}
517

518
static void
519
sixel_option_format_timestamp(
22✔
520
    time_t value,
521
    char *buffer,
522
    size_t buffer_size)
523
{
524
    struct tm *time_pointer;
525
#if defined(HAVE_LOCALTIME_R)
526
    struct tm time_view;
527
#endif
528

529
    if (buffer == NULL || buffer_size == 0u) {
22!
530
        return;
×
531
    }
532

533
#if defined(HAVE_LOCALTIME_R)
534
    if (localtime_r(&value, &time_view) != NULL) {
535
        (void)strftime(buffer, buffer_size, "%Y-%m-%d %H:%M", &time_view);
536
        return;
537
    }
538
#endif
539
    time_pointer = localtime(&value);
22✔
540
    if (time_pointer != NULL) {
22!
541
        (void)strftime(buffer, buffer_size, "%Y-%m-%d %H:%M", time_pointer);
22✔
542
        return;
22✔
543
    }
544

545
    (void)snprintf(buffer, buffer_size, "unknown");
×
546
}
547

548
static int
549
sixel_option_candidate_compare(void const *lhs, void const *rhs)
275✔
550
{
551
    sixel_option_path_candidate_t const *left;
552
    sixel_option_path_candidate_t const *right;
553

554
    left = (sixel_option_path_candidate_t const *)lhs;
275✔
555
    right = (sixel_option_path_candidate_t const *)rhs;
275✔
556

557
    if (left->total_score < right->total_score) {
275✔
558
        return 1;
127✔
559
    }
560
    if (left->total_score > right->total_score) {
148✔
561
        return -1;
120✔
562
    }
563
    if (left->mtime < right->mtime) {
28!
564
        return 1;
×
565
    }
566
    if (left->mtime > right->mtime) {
28!
567
        return -1;
×
568
    }
569

570
    if (left->path == NULL || right->path == NULL) {
28!
571
        return 0;
×
572
    }
573

574
    return strcmp(left->path, right->path);
28✔
575
}
576

577
static char *
578
sixel_option_strerror(
×
579
    int error_number,
580
    char *buffer,
581
    size_t buffer_size)
582
{
583
#if defined(_MSC_VER)
584
    errno_t status;
585
#elif defined(_WIN32)
586
# if defined(__STDC_LIB_EXT1__)
587
    errno_t status;
588
# else
589
    char *message;
590
    size_t copy_length;
591
# endif
592
#else
593
# if defined(_GNU_SOURCE)
594
    char *message;
595
    size_t copy_length;
596
# endif
597
#endif
598

599
    if (buffer == NULL || buffer_size == 0u) {
×
600
        return NULL;
×
601
    }
602

603
#if defined(_MSC_VER)
604
    status = strerror_s(buffer, buffer_size, error_number);
605
    if (status != 0) {
606
        buffer[0] = '\0';
607
        return NULL;
608
    }
609
    return buffer;
610
#elif defined(_WIN32)
611
# if defined(__STDC_LIB_EXT1__)
612
    status = strerror_s(buffer, buffer_size, error_number);
613
    if (status != 0) {
614
        buffer[0] = '\0';
615
        return NULL;
616
    }
617
    return buffer;
618
# else
619
    message = strerror(error_number);
620
    if (message == NULL) {
621
        buffer[0] = '\0';
622
        return NULL;
623
    }
624
    copy_length = buffer_size - 1u;
625
    (void)strncpy(buffer, message, copy_length);
626
    buffer[buffer_size - 1u] = '\0';
627
    return buffer;
628
# endif
629
#else
630
# if defined(_GNU_SOURCE)
631
    message = strerror_r(error_number, buffer, buffer_size);
632
    if (message == NULL) {
633
        return NULL;
634
    }
635
    if (message != buffer) {
636
        copy_length = buffer_size - 1u;
637
        (void)strncpy(buffer, message, copy_length);
638
        buffer[buffer_size - 1u] = '\0';
639
    }
640
    return buffer;
641
# else
642
    if (strerror_r(error_number, buffer, buffer_size) != 0) {
×
643
        buffer[0] = '\0';
×
644
        return NULL;
×
645
    }
646
    return buffer;
×
647
# endif
648
#endif
649
}
650

651
static int
652
sixel_option_path_is_clipboard(char const *argument)
228✔
653
{
654
    char const *marker;
655

656
    marker = NULL;
228✔
657

658
    if (argument == NULL) {
228!
659
        return 0;
×
660
    }
661

662
    marker = strstr(argument, "clipboard:");
228✔
663
    if (marker == NULL) {
228!
664
        return 0;
228✔
665
    }
UNCOV
666
    if (marker[10] != '\0') {
×
667
        return 0;
×
668
    }
UNCOV
669
    if (marker == argument) {
×
UNCOV
670
        return 1;
×
671
    }
672
    if (marker > argument && marker[-1] == ':') {
×
673
        return 1;
×
674
    }
675

676
    return 0;
×
677
}
678

679
static int
680
sixel_option_path_looks_remote(char const *path)
228✔
681
{
682
    char const *separator;
683
    size_t prefix_length;
684
    size_t index;
685
    unsigned char value;
686

687
    separator = NULL;
228✔
688
    prefix_length = 0u;
228✔
689
    index = 0u;
228✔
690
    value = 0u;
228✔
691

692
    if (path == NULL) {
228!
693
        return 0;
×
694
    }
695

696
    separator = strstr(path, "://");
228✔
697
    if (separator == NULL) {
228!
698
        return 0;
228✔
699
    }
700

701
    prefix_length = (size_t)(separator - path);
×
702
    if (prefix_length == 0u) {
×
703
        return 0;
×
704
    }
705

706
    while (index < prefix_length) {
×
707
        value = (unsigned char)path[index];
×
708
        if (!(isalpha(value) || value == '+' || value == '-' ||
×
709
              value == '.')) {
710
            return 0;
×
711
        }
712
        ++index;
×
713
    }
714

715
    return 1;
×
716
}
717

718
/*
719
 * Compose a multi-line diagnostic highlighting the missing path along with
720
 * nearby suggestions.  The first line reports the original token supplied by
721
 * the caller so CLI wrappers can relay the exact argument the user typed.
722
 */
723
static int
724
sixel_option_build_missing_path_message(
6✔
725
    char const *argument,
726
    char const *resolved_path,
727
    char *buffer,
728
    size_t buffer_size)
729
{
730
    char *directory_copy;
731
    char const *argument_view;
732
    char const *target_name;
733
    size_t offset;
734
    int written;
735
    int result;
736
#if HAVE_DIRENT_H && HAVE_SYS_STAT_H
737
    DIR *directory_stream;
738
    struct dirent *entry;
739
    sixel_option_path_candidate_t *candidates;
740
    sixel_option_path_candidate_t *grown;
741
    size_t candidate_count;
742
    size_t candidate_capacity;
743
    size_t index;
744
    size_t new_capacity;
745
    struct stat entry_stat;
746
    char *candidate_path;
747
    time_t min_mtime;
748
    time_t max_mtime;
749
    double recency_range;
750
    double percent_double;
751
    int percent_int;
752
    char time_buffer[64];
753
    int error_code;
754
    char error_buffer[128];
755
#else
756
    (void)resolved_path;
757
#endif
758

759
    directory_copy = sixel_option_duplicate_directory(resolved_path);
6✔
760
    if (directory_copy == NULL) {
6!
761
        return -1;
×
762
    }
763
    argument_view = (argument != NULL && argument[0] != '\0')
6!
764
        ? argument : resolved_path;
12!
765
    target_name = sixel_option_basename_view(resolved_path);
6✔
766
    offset = 0u;
6✔
767
    written = 0;
6✔
768
    result = 0;
6✔
769

770
    if (buffer == NULL || buffer_size == 0u) {
6!
771
        free(directory_copy);
×
772
        return -1;
×
773
    }
774

775
    written = snprintf(buffer,
6!
776
                       buffer_size,
777
                       "path \"%s\" not found.\n",
778
                       argument_view != NULL ? argument_view : "");
779
    if (written < 0) {
6!
780
        written = 0;
×
781
    }
782
    if ((size_t)written >= buffer_size) {
6!
783
        offset = buffer_size - 1u;
×
784
    } else {
785
        offset = (size_t)written;
6✔
786
    }
787

788
#if !(HAVE_DIRENT_H && HAVE_SYS_STAT_H)
789
    if (offset < buffer_size - 1u) {
790
        written = snprintf(buffer + offset,
791
                           buffer_size - offset,
792
                           "Suggestion lookup unavailable on this build.\n");
793
        if (written < 0) {
794
            written = 0;
795
        }
796
    }
797
    free(directory_copy);
798
    return 0;
799
#else
800
    directory_stream = NULL;
6✔
801
    entry = NULL;
6✔
802
    candidates = NULL;
6✔
803
    grown = NULL;
6✔
804
    candidate_count = 0u;
6✔
805
    candidate_capacity = 0u;
6✔
806
    index = 0u;
6✔
807
    new_capacity = 0u;
6✔
808
    memset(&entry_stat, 0, sizeof(entry_stat));
6✔
809
    candidate_path = NULL;
6✔
810
    min_mtime = 0;
6✔
811
    max_mtime = 0;
6✔
812
    recency_range = 0.0;
6✔
813
    percent_double = 0.0;
6✔
814
    percent_int = 0;
6✔
815
    memset(time_buffer, 0, sizeof(time_buffer));
6✔
816
    error_code = 0;
6✔
817
    memset(error_buffer, 0, sizeof(error_buffer));
6✔
818

819
    directory_stream = opendir(directory_copy);
6✔
820
    if (directory_stream == NULL) {
6!
821
        error_code = errno;
×
822
        if (error_code == ENOENT) {
×
823
            if (offset < buffer_size - 1u) {
×
824
                written = snprintf(buffer + offset,
×
825
                                   buffer_size - offset,
826
                                   "Directory \"%s\" does not exist.\n",
827
                                   directory_copy != NULL
828
                                       ? directory_copy
829
                                       : "(null)");
830
                if (written < 0) {
×
831
                    written = 0;
×
832
                }
833
            }
834
        } else {
835
            if (sixel_option_strerror(error_code,
×
836
                                      error_buffer,
837
                                      sizeof(error_buffer)) == NULL) {
838
                error_buffer[0] = '\0';
×
839
            }
840
            if (offset < buffer_size - 1u) {
×
841
                written = snprintf(buffer + offset,
×
842
                                   buffer_size - offset,
843
                                   "Unable to inspect \"%s\": %s\n",
844
                                   directory_copy != NULL
845
                                       ? directory_copy
846
                                       : "(null)",
UNCOV
847
                                   error_buffer[0] != '\0'
×
848
                                       ? error_buffer
849
                                       : "unknown error");
850
                if (written < 0) {
×
851
                    written = 0;
×
852
                }
853
            }
854
        }
855
        free(directory_copy);
×
856
        return result;
×
857
    }
858

859
    while ((entry = readdir(directory_stream)) != NULL) {
106✔
860
        if (entry->d_name[0] == '.' &&
100!
861
                (entry->d_name[1] == '\0' ||
14✔
862
                 (entry->d_name[1] == '.' && entry->d_name[2] == '\0'))) {
8!
863
            continue;
12✔
864
        }
865
        candidate_path = sixel_option_join_directory_entry(directory_copy,
88✔
866
                                                            entry->d_name);
88✔
867
        if (candidate_path == NULL) {
88!
868
            continue;
×
869
        }
870
        if (stat(candidate_path, &entry_stat) != 0) {
88!
871
            free(candidate_path);
×
872
            candidate_path = NULL;
×
873
            continue;
×
874
        }
875
        if (!S_ISREG(entry_stat.st_mode)) {
88✔
876
#if defined(S_ISLNK)
877
            if (!S_ISLNK(entry_stat.st_mode)) {
6!
878
                free(candidate_path);
6✔
879
                candidate_path = NULL;
6✔
880
                continue;
6✔
881
            }
882
#else
883
            free(candidate_path);
884
            candidate_path = NULL;
885
            continue;
886
#endif
887
        }
888
        if (candidate_count == candidate_capacity) {
82✔
889
            new_capacity = (candidate_capacity == 0u)
12✔
890
                ? 8u
891
                : candidate_capacity * 2u;
12✔
892
            grown = (sixel_option_path_candidate_t *)realloc(
12✔
893
                candidates,
894
                new_capacity * sizeof(sixel_option_path_candidate_t));
895
            if (grown == NULL) {
12!
896
                free(candidate_path);
×
897
                candidate_path = NULL;
×
898
                break;
×
899
            }
900
            candidates = grown;
12✔
901
            candidate_capacity = new_capacity;
12✔
902
        }
903
        candidates[candidate_count].path = candidate_path;
82✔
904
        candidates[candidate_count].name =
164✔
905
            sixel_option_basename_view(candidate_path);
82✔
906
        candidates[candidate_count].mtime = entry_stat.st_mtime;
82✔
907
        candidates[candidate_count].name_score = 0.0;
82✔
908
        candidates[candidate_count].extension_score = 0.0;
82✔
909
        candidates[candidate_count].recency_score = 0.0;
82✔
910
        candidates[candidate_count].total_score = 0.0;
82✔
911
        ++candidate_count;
82✔
912
        candidate_path = NULL;
82✔
913
    }
914

915
    if (directory_stream != NULL) {
6!
916
        (void)closedir(directory_stream);
6✔
917
        directory_stream = NULL;
6✔
918
    }
919

920
    if (candidate_count == 0u) {
6!
921
        if (offset < buffer_size - 1u) {
×
922
            written = snprintf(buffer + offset,
×
923
                               buffer_size - offset,
924
                               "No nearby matches were found in \"%s\".\n",
925
                               directory_copy != NULL
926
                                   ? directory_copy
927
                                   : "(null)");
928
            if (written < 0) {
×
929
                written = 0;
×
930
            }
931
        }
932
        free(directory_copy);
×
933
        return 0;
×
934
    }
935

936
    min_mtime = candidates[0].mtime;
6✔
937
    max_mtime = candidates[0].mtime;
6✔
938
    for (index = 0u; index < candidate_count; ++index) {
88✔
939
        candidates[index].name_score =
164✔
940
            sixel_option_normalized_levenshtein(target_name,
82✔
941
                                                 candidates[index].name);
82✔
942
        candidates[index].extension_score =
164✔
943
            sixel_option_extension_similarity(target_name,
82✔
944
                                              candidates[index].name);
82✔
945
        if (index == 0u || candidates[index].mtime < min_mtime) {
82✔
946
            min_mtime = candidates[index].mtime;
12✔
947
        }
948
        if (index == 0u || candidates[index].mtime > max_mtime) {
82✔
949
            max_mtime = candidates[index].mtime;
8✔
950
        }
951
    }
952

953
    recency_range = (double)(max_mtime - min_mtime);
6✔
954
    for (index = 0u; index < candidate_count; ++index) {
88✔
955
        if (recency_range <= 0.0) {
82✔
956
            candidates[index].recency_score = 1.0;
2✔
957
        } else {
958
            candidates[index].recency_score =
80✔
959
                (double)(candidates[index].mtime - min_mtime) /
80✔
960
                recency_range;
961
        }
962
        candidates[index].total_score =
82✔
963
            SIXEL_OPTION_SUGGESTION_NAME_WEIGHT *
82✔
964
                candidates[index].name_score +
82✔
965
            SIXEL_OPTION_SUGGESTION_EXTENSION_WEIGHT *
82✔
966
                candidates[index].extension_score +
82✔
967
            SIXEL_OPTION_SUGGESTION_RECENCY_WEIGHT *
82✔
968
                candidates[index].recency_score;
82✔
969
    }
970

971
    qsort(candidates,
6✔
972
          candidate_count,
973
          sizeof(sixel_option_path_candidate_t),
974
          sixel_option_candidate_compare);
975

976
    if (offset < buffer_size - 1u) {
6!
977
        written = snprintf(buffer + offset,
6✔
978
                           buffer_size - offset,
979
                           "Suggestions:\n");
980
        if (written < 0) {
6!
981
            written = 0;
×
982
        }
983
        if ((size_t)written >= buffer_size - offset) {
6!
984
            offset = buffer_size - 1u;
×
985
        } else {
986
            offset += (size_t)written;
6✔
987
        }
988
    }
989

990
    for (index = 0u; index < candidate_count &&
28✔
991
            index < SIXEL_OPTION_SUGGESTION_LIMIT; ++index) {
22✔
992
        percent_double = candidates[index].total_score * 100.0;
22✔
993
        if (percent_double < 0.0) {
22!
994
            percent_double = 0.0;
×
995
        }
996
        if (percent_double > 100.0) {
22!
997
            percent_double = 100.0;
×
998
        }
999
        percent_int = (int)(percent_double + 0.5);
22✔
1000
        sixel_option_format_timestamp(candidates[index].mtime,
22✔
1001
                                      time_buffer,
1002
                                      sizeof(time_buffer));
1003
        if (offset < buffer_size - 1u) {
22!
UNCOV
1004
            written = snprintf(buffer + offset,
×
1005
                               buffer_size - offset,
1006
                               "  * %s (similarity score %d%%, "
1007
                               "modified %s)\n",
1008
                               candidates[index].path != NULL
22!
1009
                                   ? candidates[index].path
22✔
1010
                                   : "(unknown)",
1011
                               percent_int,
1012
                               time_buffer);
1013
            if (written < 0) {
22!
1014
                written = 0;
×
1015
            }
1016
            if ((size_t)written >= buffer_size - offset) {
22!
1017
                offset = buffer_size - 1u;
×
1018
            } else {
1019
                offset += (size_t)written;
22✔
1020
            }
1021
        }
1022
    }
1023

1024
    if (directory_stream != NULL) {
6!
1025
        (void)closedir(directory_stream);
×
1026
        directory_stream = NULL;
×
1027
    }
1028
    if (candidates != NULL) {
6!
1029
        for (index = 0u; index < candidate_count; ++index) {
88✔
1030
            free(candidates[index].path);
82✔
1031
            candidates[index].path = NULL;
82✔
1032
        }
1033
        free(candidates);
6✔
1034
        candidates = NULL;
6✔
1035
    }
1036
    free(directory_copy);
6✔
1037

1038
    return result;
6✔
1039
#endif
1040
}
1041

1042
int
1043
sixel_option_validate_filesystem_path(
230✔
1044
    char const *argument,
1045
    char const *resolved_path,
1046
    unsigned int flags)
1047
{
1048
#if !(HAVE_SYS_STAT_H)
1049
    (void)argument;
1050
    (void)resolved_path;
1051
    (void)flags;
1052
    return 0;
1053
#else
1054
    struct stat path_stat;
1055
    int stat_result;
1056
    int error_value;
1057
    int allow_stdin;
1058
    int allow_clipboard;
1059
    int allow_remote;
1060
    int allow_empty;
1061
    char const *remote_view;
1062
    char message_buffer[1024];
1063

1064
    memset(&path_stat, 0, sizeof(path_stat));
230✔
1065
    stat_result = 0;
230✔
1066
    error_value = 0;
230✔
1067
    allow_stdin = (flags & SIXEL_OPTION_PATH_ALLOW_STDIN) != 0u;
230✔
1068
    allow_clipboard = (flags & SIXEL_OPTION_PATH_ALLOW_CLIPBOARD) != 0u;
230✔
1069
    allow_remote = (flags & SIXEL_OPTION_PATH_ALLOW_REMOTE) != 0u;
230✔
1070
    allow_empty = (flags & SIXEL_OPTION_PATH_ALLOW_EMPTY) != 0u;
230✔
1071
    remote_view = resolved_path != NULL ? resolved_path : argument;
230!
1072
    memset(message_buffer, 0, sizeof(message_buffer));
230✔
1073

1074
    /*
1075
     * Reject empty path arguments unless the caller explicitly opts in.
1076
     * Historically the CLI rejected blank -i payloads while tolerating empty
1077
     * -m mapfile values, so the new flag preserves that behaviour without
1078
     * reintroducing option-specific strings here.
1079
     */
1080
    if ((argument == NULL || argument[0] == '\0') &&
230!
1081
            (resolved_path == NULL || resolved_path[0] == '\0')) {
×
1082
        if (!allow_empty) {
×
1083
            sixel_helper_set_additional_message(
×
1084
                "path argument is empty.");
1085
            return -1;
×
1086
        }
1087
        return 0;
×
1088
    }
1089

1090
    if (resolved_path == NULL || resolved_path[0] == '\0') {
230!
1091
        if (!allow_empty) {
×
1092
            sixel_helper_set_additional_message(
×
1093
                "path argument is empty.");
1094
            return -1;
×
1095
        }
1096
        return 0;
×
1097
    }
1098
    if (allow_stdin && argument != NULL && strcmp(argument, "-") == 0) {
230!
1099
        return 0;
2✔
1100
    }
1101
    if (allow_clipboard && sixel_option_path_is_clipboard(argument)) {
228!
UNCOV
1102
        return 0;
×
1103
    }
1104
    if (allow_remote && sixel_option_path_looks_remote(remote_view)) {
228!
1105
        return 0;
×
1106
    }
1107

1108
    errno = 0;
228✔
1109
    stat_result = stat(resolved_path, &path_stat);
228✔
1110
    if (stat_result == 0) {
228✔
1111
        return 0;
222✔
1112
    }
1113

1114
    error_value = errno;
6✔
1115
    if (error_value != ENOENT && error_value != ENOTDIR) {
6!
1116
        return 0;
×
1117
    }
1118

1119
    if (sixel_option_build_missing_path_message(argument,
6!
1120
                                                resolved_path,
1121
                                                message_buffer,
1122
                                                sizeof(message_buffer)) != 0) {
1123
        sixel_helper_set_additional_message(
×
1124
            "path validation failed.");
1125
    } else {
1126
        sixel_helper_set_additional_message(message_buffer);
6✔
1127
    }
1128

1129
    return -1;
6✔
1130
#endif
1131
}
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