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

saitoha / libsixel / 19765269522

28 Nov 2025 01:30PM UTC coverage: 39.983% (-1.6%) from 41.616%
19765269522

push

github

web-flow
Merge pull request #214 from saitoha/codex/add-logging-to-resize-processing

Limit scale logging and add timeline window controls

9788 of 35562 branches covered (27.52%)

9 of 63 new or added lines in 1 file covered. (14.29%)

281 existing lines in 19 files now uncovered.

12991 of 32491 relevant lines covered (39.98%)

619662.16 hits per line

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

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

26
#if !defined(_POSIX_C_SOURCE)
27
# define _POSIX_C_SOURCE 200809L
28
#endif
29

30
#include "config.h"
31

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

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

46
#include <sixel.h>
47
#include "compat_stub.h"
48
#include "options.h"
49
#include "output.h"
50

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

58
#define SIXEL_OPTION_CHOICE_SUGGESTION_LIMIT 5u
59
#define SIXEL_OPTION_CHOICE_SUGGESTION_THRESHOLD 0.6
60
#define SIXEL_OPTION_CHOICE_SHORT_NAME_LENGTH 3u
61

62
static void
63
sixel_option_apply_env_default(char const *variable);
64

65
static int
66
sixel_option_environment_is_enabled(char const *variable);
67

68
typedef struct sixel_option_choice_suggestion {
69
    char const *name;
70
    double score;
71
    size_t distance;
72
} sixel_option_choice_suggestion_t;
73

74
static double
75
sixel_option_normalized_levenshtein(
76
    char const *lhs,
77
    char const *rhs,
78
    size_t *distance_out);
79

80
void
81
sixel_option_apply_cli_suggestion_defaults(void)
418✔
82
{
83
    sixel_option_apply_env_default(
418✔
84
        SIXEL_OPTION_ENV_PREFIX_SUGGESTIONS);
85
    sixel_option_apply_env_default(
418✔
86
        SIXEL_OPTION_ENV_FUZZY_SUGGESTIONS);
87
    /* Path suggestions stay opt-in for the CLI frontends. */
88
}
418✔
89

90
static void
91
sixel_option_apply_env_default(char const *variable)
836✔
92
{
93
    char const *existing;
94
    int status;
95

96
    existing = NULL;
836✔
97
    status = 0;
836✔
98

99
    if (variable == NULL) {
836!
100
        return;
×
101
    }
102

103
    existing = sixel_compat_getenv(variable);
836✔
104
    if (existing != NULL) {
836!
105
        return;
×
106
    }
107

108
    status = sixel_compat_setenv(variable, "1");
836✔
109
    (void)status;
110
}
111

112
static int
113
sixel_option_environment_is_enabled(char const *variable)
36✔
114
{
115
    char const *value;
116

117
    value = NULL;
36✔
118

119
    if (variable == NULL) {
36!
120
        return 0;
×
121
    }
122

123
    value = sixel_compat_getenv(variable);
36✔
124
    if (value == NULL) {
36✔
125
        return 0;
6✔
126
    }
127
    if (value[0] == '1' && value[1] == '\0') {
30!
128
        return 1;
30✔
129
    }
130

131
    return 0;
×
132
}
133

134
/*
135
 * Build a ranked suggestion list for choice arguments.  The helper evaluates
136
 * every prefix that would be accepted by the prefix-matching logic, compares
137
 * the user input against those prefixes, and then projects the highest scoring
138
 * matches back to their canonical option names.  The diagnostic string reuses
139
 * the comma-separated format consumed by the CLI frontends.
140
 */
141
static size_t
142
sixel_option_collect_choice_suggestions(
143
    char const *value,
144
    sixel_option_choice_t const *choices,
145
    size_t choice_count,
146
    char *diagnostic,
147
    size_t diagnostic_size);
148

149
static int
150
sixel_option_choice_prefix_accepts(
151
    sixel_option_choice_t const *choices,
152
    size_t choice_count,
153
    char const *name,
154
    size_t prefix_length);
155

156
sixel_option_choice_result_t
157
sixel_option_match_choice(
192✔
158
    char const *value,
159
    sixel_option_choice_t const *choices,
160
    size_t choice_count,
161
    int *matched_value,
162
    char *diagnostic,
163
    size_t diagnostic_size)
164
{
165
    size_t index;
166
    size_t value_length;
167
    int candidate_index;
168
    size_t match_count;
169
    int base_value;
170
    int base_value_set;
171
    int ambiguous_values;
172
    size_t diag_length;
173
    size_t copy_length;
174

175
    if (diagnostic != NULL && diagnostic_size > 0u) {
192!
176
        diagnostic[0] = '\0';
192✔
177
    }
178
    if (value == NULL) {
192!
179
        return SIXEL_OPTION_CHOICE_NONE;
×
180
    }
181

182
    value_length = strlen(value);
192✔
183
    if (value_length == 0u) {
192!
184
        return SIXEL_OPTION_CHOICE_NONE;
×
185
    }
186

187
    index = 0u;
192✔
188
    candidate_index = (-1);
192✔
189
    match_count = 0u;
192✔
190
    base_value = 0;
192✔
191
    base_value_set = 0;
192✔
192
    ambiguous_values = 0;
192✔
193
    diag_length = 0u;
192✔
194

195
    while (index < choice_count) {
1,030✔
196
        if (strncmp(choices[index].name, value, value_length) == 0) {
970✔
197
            if (choices[index].name[value_length] == '\0') {
176✔
198
                *matched_value = choices[index].value;
132✔
199
                return SIXEL_OPTION_CHOICE_MATCH;
132✔
200
            }
201
            if (!base_value_set) {
44✔
202
                base_value = choices[index].value;
34✔
203
                base_value_set = 1;
34✔
204
            } else if (choices[index].value != base_value) {
10✔
205
                ambiguous_values = 1;
6✔
206
            }
207
            if (candidate_index == (-1)) {
44✔
208
                candidate_index = (int)index;
34✔
209
            }
210
            ++match_count;
44✔
211
            if (diagnostic != NULL && diagnostic_size > 0u) {
44!
212
                if (diag_length > 0u && diag_length + 2u < diagnostic_size) {
44!
213
                    diagnostic[diag_length] = ',';
10✔
214
                    diagnostic[diag_length + 1u] = ' ';
10✔
215
                    diag_length += 2u;
10✔
216
                    diagnostic[diag_length] = '\0';
10✔
217
                }
218
                copy_length = strlen(choices[index].name);
44✔
219
                if (copy_length > diagnostic_size - diag_length - 1u) {
44!
220
                    copy_length = diagnostic_size - diag_length - 1u;
×
221
                }
222
                memcpy(diagnostic + diag_length,
44✔
223
                       choices[index].name,
44✔
224
                       copy_length);
225
                diag_length += copy_length;
44✔
226
                diagnostic[diag_length] = '\0';
44✔
227
            }
228
        }
229
        ++index;
838✔
230
    }
231

232
    if (match_count == 0u || candidate_index == (-1)) {
60!
233
        (void)sixel_option_collect_choice_suggestions(
26✔
234
            value,
235
            choices,
236
            choice_count,
237
            diagnostic,
238
            diagnostic_size);
239
        return SIXEL_OPTION_CHOICE_NONE;
26✔
240
    }
241
    if (!ambiguous_values) {
34✔
242
        *matched_value = choices[candidate_index].value;
30✔
243
        return SIXEL_OPTION_CHOICE_MATCH;
30✔
244
    }
245

246
    return SIXEL_OPTION_CHOICE_AMBIGUOUS;
4✔
247
}
248

249
void
250
sixel_option_report_ambiguous_prefix(
4✔
251
    char const *value,
252
    char const *candidates,
253
    char *buffer,
254
    size_t buffer_size)
255
{
256
    int written;
257
    int suggestions_enabled;
258
    char const *active_candidates;
259

260
    if (buffer == NULL || buffer_size == 0u) {
4!
261
        return;
×
262
    }
263
    suggestions_enabled = sixel_option_environment_is_enabled(
4✔
264
        SIXEL_OPTION_ENV_PREFIX_SUGGESTIONS);
265
    active_candidates = suggestions_enabled ? candidates : NULL;
4!
266
    if (active_candidates != NULL && active_candidates[0] != '\0') {
4!
267
        written = snprintf(buffer,
4✔
268
                           buffer_size,
269
                           "ambiguous prefix \"%s\" (matches: \\fB%s\\fP).",
270
                           value,
271
                           active_candidates);
272
    } else {
273
        written = snprintf(buffer,
×
274
                           buffer_size,
275
                           "ambiguous prefix \"%s\".",
276
                           value);
277
    }
278
    (void) written;
279
    sixel_helper_set_additional_message(buffer);
4✔
280
}
281

282
void
283
sixel_option_report_invalid_choice(
26✔
284
    char const *base_message,
285
    char const *suggestions,
286
    char *buffer,
287
    size_t buffer_size)
288
{
289
    int written;
290

291
    if (base_message == NULL) {
26!
292
        return;
×
293
    }
294

295
    if (suggestions != NULL && suggestions[0] != '\0' && buffer != NULL &&
26!
296
        buffer_size > 0u) {
297
        buffer[0] = '\0';
6✔
298
        written = snprintf(buffer,
6✔
299
                           buffer_size,
300
                           "%s Did you mean: \\fW%s\\fP?",
301
                           base_message,
302
                           suggestions);
303
        if (written < 0) {
6!
304
            sixel_helper_set_additional_message(base_message);
×
305
            return;
×
306
        }
307
        sixel_helper_set_additional_message(buffer);
6✔
308
        return;
6✔
309
    }
310

311
    sixel_helper_set_additional_message(base_message);
20✔
312
}
313

314
static int
315
sixel_option_choice_suggestion_compare(
4✔
316
    void const *lhs,
317
    void const *rhs)
318
{
319
    sixel_option_choice_suggestion_t const *left;
320
    sixel_option_choice_suggestion_t const *right;
321
    size_t left_length;
322
    size_t right_length;
323

324
    left = (sixel_option_choice_suggestion_t const *)lhs;
4✔
325
    right = (sixel_option_choice_suggestion_t const *)rhs;
4✔
326
    left_length = strlen(left->name);
4✔
327
    right_length = strlen(right->name);
4✔
328

329
    if (left->score < right->score) {
4!
330
        return 1;
×
331
    }
332
    if (left->score > right->score) {
4!
333
        return -1;
×
334
    }
335
    if (left->distance > right->distance) {
4!
336
        return 1;
×
337
    }
338
    if (left->distance < right->distance) {
4!
339
        return -1;
×
340
    }
341
    if (left_length > right_length) {
4!
342
        return 1;
×
343
    }
344
    if (left_length < right_length) {
4!
345
        return -1;
×
346
    }
347

348
    return strcmp(left->name, right->name);
4✔
349
}
350

351
static size_t
352
sixel_option_collect_choice_suggestions(
26✔
353
    char const *value,
354
    sixel_option_choice_t const *choices,
355
    size_t choice_count,
356
    char *diagnostic,
357
    size_t diagnostic_size)
358
{
359
    sixel_option_choice_suggestion_t *candidates;
360
    size_t candidate_count;
361
    size_t index;
362
    size_t diag_length;
363
    size_t copy_length;
364
    size_t distance;
365
    size_t name_length;
366
    size_t prefix_length;
367
    size_t existing_index;
368
    double score;
369
    char *prefix;
370
    char const *name;
371
    int has_existing;
372
    int suggestions_enabled;
373

374
    candidates = NULL;
26✔
375
    candidate_count = 0u;
26✔
376
    index = 0u;
26✔
377
    diag_length = 0u;
26✔
378
    copy_length = 0u;
26✔
379
    distance = 0u;
26✔
380
    name_length = 0u;
26✔
381
    prefix_length = 0u;
26✔
382
    existing_index = 0u;
26✔
383
    score = 0.0;
26✔
384
    prefix = NULL;
26✔
385
    name = NULL;
26✔
386
    has_existing = 0;
26✔
387
    suggestions_enabled = 0;
26✔
388

389
    if (diagnostic != NULL && diagnostic_size > 0u) {
26!
390
        diagnostic[0] = '\0';
26✔
391
    }
392
    if (value == NULL || choices == NULL || choice_count == 0u) {
26!
393
        return 0u;
×
394
    }
395

396
    suggestions_enabled = sixel_option_environment_is_enabled(
26✔
397
        SIXEL_OPTION_ENV_FUZZY_SUGGESTIONS);
398
    if (!suggestions_enabled) {
26!
399
        return 0u;
×
400
    }
401

402
    candidates = (sixel_option_choice_suggestion_t *)malloc(
26✔
403
        choice_count * sizeof(sixel_option_choice_suggestion_t));
404
    if (candidates == NULL) {
26!
405
        return 0u;
×
406
    }
407

408
    for (index = 0u; index < choice_count; ++index) {
218✔
409
        name = choices[index].name;
192✔
410
        name_length = strlen(name);
192✔
411
        for (prefix_length = 1u;
192✔
412
             prefix_length <= name_length;
1,394✔
413
             ++prefix_length) {
1,202✔
414
            if (!sixel_option_choice_prefix_accepts(
1,202✔
415
                    choices,
416
                    choice_count,
417
                    name,
418
                    prefix_length)) {
419
                continue;
396✔
420
            }
421
            prefix = (char *)malloc(prefix_length + 1u);
806✔
422
            if (prefix == NULL) {
806!
423
                free(candidates);
×
424
                return 0u;
×
425
            }
426
            memcpy(prefix, name, prefix_length);
806✔
427
            prefix[prefix_length] = '\0';
806✔
428
            score = sixel_option_normalized_levenshtein(
806✔
429
                value,
430
                prefix,
431
                &distance);
432
            free(prefix);
806✔
433
            prefix = NULL;
806✔
434
            if (distance == 0u) {
806!
435
                continue;
×
436
            }
437
            if (distance > 2u) {
806✔
438
                continue;
788✔
439
            }
440
            if (score < SIXEL_OPTION_CHOICE_SUGGESTION_THRESHOLD) {
18!
441
                continue;
×
442
            }
443
            if (prefix_length <= SIXEL_OPTION_CHOICE_SHORT_NAME_LENGTH &&
18!
444
                distance > 1u) {
×
445
                continue;
×
446
            }
447
            has_existing = 0;
18✔
448
            for (existing_index = 0u;
18✔
449
                 existing_index < candidate_count;
24✔
450
                 ++existing_index) {
6✔
451
                if (strcmp(candidates[existing_index].name, name) == 0) {
14✔
452
                    has_existing = 1;
8✔
453
                    break;
8✔
454
                }
455
            }
456
            if (has_existing) {
18✔
457
                if (score > candidates[existing_index].score ||
8!
458
                    (score == candidates[existing_index].score &&
2!
459
                     distance < candidates[existing_index].distance)) {
2!
460
                    candidates[existing_index].score = score;
6✔
461
                    candidates[existing_index].distance = distance;
6✔
462
                }
463
                continue;
8✔
464
            }
465
            candidates[candidate_count].name = name;
10✔
466
            candidates[candidate_count].score = score;
10✔
467
            candidates[candidate_count].distance = distance;
10✔
468
            ++candidate_count;
10✔
469
        }
470
    }
471

472
    if (candidate_count == 0u) {
26✔
473
        free(candidates);
20✔
474
        return 0u;
20✔
475
    }
476

477
    if (candidate_count > 1u) {
6✔
478
        qsort(candidates,
4✔
479
              candidate_count,
480
              sizeof(sixel_option_choice_suggestion_t),
481
              sixel_option_choice_suggestion_compare);
482
    }
483

484
    if (candidate_count > SIXEL_OPTION_CHOICE_SUGGESTION_LIMIT) {
6!
485
        candidate_count = SIXEL_OPTION_CHOICE_SUGGESTION_LIMIT;
×
486
    }
487

488
    if (diagnostic != NULL && diagnostic_size > 0u) {
6!
489
        for (index = 0u; index < candidate_count; ++index) {
16✔
490
            if (diag_length > 0u && diag_length + 2u < diagnostic_size) {
10!
491
                diagnostic[diag_length] = ',';
4✔
492
                diagnostic[diag_length + 1u] = ' ';
4✔
493
                diag_length += 2u;
4✔
494
                diagnostic[diag_length] = '\0';
4✔
495
            }
496
            copy_length = strlen(candidates[index].name);
10✔
497
            if (copy_length > diagnostic_size - diag_length - 1u) {
10!
498
                copy_length = diagnostic_size - diag_length - 1u;
×
499
            }
500
            memcpy(diagnostic + diag_length,
10✔
501
                   candidates[index].name,
10✔
502
                   copy_length);
503
            diag_length += copy_length;
10✔
504
            diagnostic[diag_length] = '\0';
10✔
505
            if (diag_length >= diagnostic_size - 1u) {
10!
506
                break;
×
507
            }
508
        }
509
    }
510

511
    free(candidates);
6✔
512
    return candidate_count;
6✔
513
}
514

515
static int
516
sixel_option_choice_prefix_accepts(
1,202✔
517
    sixel_option_choice_t const *choices,
518
    size_t choice_count,
519
    char const *name,
520
    size_t prefix_length)
521
{
522
    size_t index;
523
    int base_value;
524
    int base_set;
525

526
    if (choices == NULL || name == NULL || prefix_length == 0u) {
1,202!
527
        return 0;
×
528
    }
529

530
    base_value = 0;
1,202✔
531
    base_set = 0;
1,202✔
532
    for (index = 0u; index < choice_count; ++index) {
11,158✔
533
        if (strncmp(choices[index].name, name, prefix_length) != 0) {
10,352✔
534
            continue;
8,738✔
535
        }
536
        if (!base_set) {
1,614✔
537
            base_value = choices[index].value;
1,202✔
538
            base_set = 1;
1,202✔
539
            continue;
1,202✔
540
        }
541
        if (choices[index].value != base_value) {
412✔
542
            return 0;
396✔
543
        }
544
    }
545

546
    return base_set;
806✔
547
}
548

549
#define SIXEL_OPTION_SUGGESTION_LIMIT 5u
550
#define SIXEL_OPTION_SUGGESTION_NAME_WEIGHT 0.55
551
#define SIXEL_OPTION_SUGGESTION_EXTENSION_WEIGHT 0.25
552
#define SIXEL_OPTION_SUGGESTION_RECENCY_WEIGHT 0.20
553

554
/*
555
 * The suggestion engine mirrors the fuzzy finder heuristics used by the
556
 * converters before the refactor.  Filename similarity dominates the ranking
557
 * while matching extensions and recent modification times act as tiebreakers.
558
 */
559

560
typedef struct sixel_option_path_candidate {
561
    char *path;
562
    char const *name;
563
    time_t mtime;
564
    double name_score;
565
    double extension_score;
566
    double recency_score;
567
    double total_score;
568
} sixel_option_path_candidate_t;
569

570
static int
571
sixel_option_case_insensitive_equals(
×
572
    char const *lhs,
573
    char const *rhs)
574
{
575
    size_t index;
576
    unsigned char left;
577
    unsigned char right;
578

579
    index = 0u;
×
580

581
    if (lhs == NULL || rhs == NULL) {
×
582
        return 0;
×
583
    }
584

585
    while (lhs[index] != '\0' && rhs[index] != '\0') {
×
586
        left = (unsigned char)lhs[index];
×
587
        right = (unsigned char)rhs[index];
×
588
        if (tolower(left) != tolower(right)) {
×
589
            return 0;
×
590
        }
591
        ++index;
×
592
    }
593

594
    return lhs[index] == '\0' && rhs[index] == '\0';
×
595
}
596

597
static char const *
598
sixel_option_basename_view(char const *path)
6✔
599
{
600
    char const *forward;
601
#if defined(_WIN32)
602
    char const *backward;
603
#endif
604
    char const *start;
605

606
    forward = NULL;
6✔
607
#if defined(_WIN32)
608
    backward = NULL;
609
#endif
610
    start = path;
6✔
611

612
    if (path == NULL) {
6!
613
        return NULL;
×
614
    }
615

616
    forward = strrchr(path, '/');
6✔
617
#if defined(_WIN32)
618
    backward = strrchr(path, '\\');
619
    if (backward != NULL && (forward == NULL || backward > forward)) {
620
        forward = backward;
621
    }
622
#endif
623

624
    if (forward != NULL) {
6✔
625
        return forward + 1;
4✔
626
    }
627

628
    return start;
2✔
629
}
630

631
static char const *
632
sixel_option_extension_view(char const *name)
×
633
{
634
    char const *dot;
635

636
    dot = NULL;
×
637

638
    if (name == NULL) {
×
639
        return NULL;
×
640
    }
641

642
    dot = strrchr(name, '.');
×
643
    if (dot == NULL || dot == name) {
×
644
        return NULL;
×
645
    }
646

647
    return dot + 1;
×
648
}
649

650
static double
651
sixel_option_normalized_levenshtein(
806✔
652
    char const *lhs,
653
    char const *rhs,
654
    size_t *distance_out)
655
{
656
    size_t lhs_length;
657
    size_t rhs_length;
658
    size_t *previous;
659
    size_t *current;
660
    size_t column;
661
    size_t row;
662
    size_t cost;
663
    size_t deletion;
664
    size_t insertion;
665
    size_t substitution;
666
    size_t distance_value;
667
    unsigned char left_char;
668
    unsigned char right_char;
669
    double normalized;
670

671
    lhs_length = 0u;
806✔
672
    rhs_length = 0u;
806✔
673
    previous = NULL;
806✔
674
    current = NULL;
806✔
675
    column = 0u;
806✔
676
    row = 0u;
806✔
677
    cost = 0u;
806✔
678
    deletion = 0u;
806✔
679
    insertion = 0u;
806✔
680
    substitution = 0u;
806✔
681
    distance_value = 0u;
806✔
682
    left_char = 0u;
806✔
683
    right_char = 0u;
806✔
684
    normalized = 0.0;
806✔
685

686
    if (distance_out != NULL) {
806!
687
        *distance_out = 0u;
806✔
688
    }
689

690
    if (lhs == NULL || rhs == NULL) {
806!
691
        return 0.0;
×
692
    }
693

694
    lhs_length = strlen(lhs);
806✔
695
    rhs_length = strlen(rhs);
806✔
696
    if (lhs_length == 0u && rhs_length == 0u) {
806!
697
        return 1.0;
×
698
    }
699

700
    previous = (size_t *)malloc((rhs_length + 1u) * sizeof(size_t));
806✔
701
    if (previous == NULL) {
806!
702
        return 0.0;
×
703
    }
704

705
    current = (size_t *)malloc((rhs_length + 1u) * sizeof(size_t));
806✔
706
    if (current == NULL) {
806!
707
        free(previous);
×
708
        return 0.0;
×
709
    }
710

711
    column = 0u;
806✔
712
    while (column <= rhs_length) {
5,054✔
713
        previous[column] = column;
4,248✔
714
        ++column;
4,248✔
715
    }
716

717
    row = 1u;
806✔
718
    while (row <= lhs_length) {
9,186✔
719
        current[0] = row;
8,380✔
720
        column = 1u;
8,380✔
721
        while (column <= rhs_length) {
43,326✔
722
            left_char = (unsigned char)lhs[row - 1u];
34,946✔
723
            right_char = (unsigned char)rhs[column - 1u];
34,946✔
724
            cost = (tolower(left_char) == tolower(right_char)) ? 0u : 1u;
34,946✔
725
            deletion = previous[column] + 1u;
34,946✔
726
            insertion = current[column - 1u] + 1u;
34,946✔
727
            substitution = previous[column - 1u] + cost;
34,946✔
728
            current[column] = deletion;
34,946✔
729
            if (insertion < current[column]) {
34,946✔
730
                current[column] = insertion;
6,956✔
731
            }
732
            if (substitution < current[column]) {
34,946✔
733
                current[column] = substitution;
5,736✔
734
            }
735
            ++column;
34,946✔
736
        }
737
        memcpy(previous, current, (rhs_length + 1u) * sizeof(size_t));
8,380✔
738
        ++row;
8,380✔
739
    }
740

741
    distance_value = previous[rhs_length];
806✔
742
    free(current);
806✔
743
    free(previous);
806✔
744

745
    if (distance_out != NULL) {
806!
746
        *distance_out = distance_value;
806✔
747
    }
748

749
    normalized = 1.0 - (double)distance_value /
1,612✔
750
        (double)((lhs_length > rhs_length) ? lhs_length : rhs_length);
806✔
751
    if (normalized < 0.0) {
806!
752
        normalized = 0.0;
×
753
    }
754

755
    return normalized;
806✔
756
}
757

758
static double
759
sixel_option_extension_similarity(
×
760
    char const *lhs,
761
    char const *rhs)
762
{
763
    char const *lhs_extension;
764
    char const *rhs_extension;
765

766
    lhs_extension = sixel_option_extension_view(lhs);
×
767
    rhs_extension = sixel_option_extension_view(rhs);
×
768
    if (lhs_extension == NULL || rhs_extension == NULL) {
×
769
        return 0.0;
×
770
    }
771
    if (sixel_option_case_insensitive_equals(lhs_extension, rhs_extension)) {
×
772
        return 1.0;
×
773
    }
774

775
    return 0.0;
×
776
}
777

778
static char *
779
sixel_option_duplicate_string(char const *text)
2✔
780
{
781
    size_t length;
782
    char *copy;
783

784
    length = 0u;
2✔
785
    copy = NULL;
2✔
786

787
    if (text == NULL) {
2!
788
        return NULL;
×
789
    }
790

791
    length = strlen(text);
2✔
792
    copy = (char *)malloc(length + 1u);
2✔
793
    if (copy == NULL) {
2!
794
        return NULL;
×
795
    }
796

797
    if (length > 0u) {
2!
798
        memcpy(copy, text, length);
2✔
799
    }
800
    copy[length] = '\0';
2✔
801

802
    return copy;
2✔
803
}
804

805
static char *
806
sixel_option_duplicate_directory(char const *path)
6✔
807
{
808
    char const *forward;
809
#if defined(_WIN32)
810
    char const *backward;
811
#endif
812
    char const *separator;
813
    size_t length;
814
    char *copy;
815

816
    forward = NULL;
6✔
817
#if defined(_WIN32)
818
    backward = NULL;
819
#endif
820
    separator = NULL;
6✔
821
    length = 0u;
6✔
822
    copy = NULL;
6✔
823

824
    if (path == NULL || path[0] == '\0') {
6!
825
        return sixel_option_duplicate_string(".");
×
826
    }
827

828
    forward = strrchr(path, '/');
6✔
829
#if defined(_WIN32)
830
    backward = strrchr(path, '\\');
831
    if (backward != NULL && (forward == NULL || backward > forward)) {
832
        forward = backward;
833
    }
834
#endif
835
    separator = forward;
6✔
836

837
    if (separator == NULL) {
6✔
838
        return sixel_option_duplicate_string(".");
2✔
839
    }
840
    if (separator == path) {
4!
841
        return sixel_option_duplicate_string("/");
×
842
    }
843

844
    length = (size_t)(separator - path);
4✔
845
    copy = (char *)malloc(length + 1u);
4✔
846
    if (copy == NULL) {
4!
847
        return NULL;
×
848
    }
849
    if (length > 0u) {
4!
850
        memcpy(copy, path, length);
4✔
851
    }
852
    copy[length] = '\0';
4✔
853

854
    return copy;
4✔
855
}
856

857
static char *
858
sixel_option_join_directory_entry(
×
859
    char const *directory,
860
    char const *entry)
861
{
862
    size_t directory_length;
863
    size_t entry_length;
864
    int needs_separator;
865
    char *joined;
866

867
    directory_length = 0u;
×
868
    entry_length = 0u;
×
869
    needs_separator = 0;
×
870
    joined = NULL;
×
871

872
    if (directory == NULL || entry == NULL) {
×
873
        return NULL;
×
874
    }
875

876
    directory_length = strlen(directory);
×
877
    entry_length = strlen(entry);
×
878
    if (directory_length == 0u) {
×
879
        needs_separator = 0;
×
880
    } else if (directory[directory_length - 1u] == '/'
×
881
#if defined(_WIN32)
882
               || directory[directory_length - 1u] == '\\'
883
#endif
884
               ) {
885
        needs_separator = 0;
×
886
    } else {
887
        needs_separator = 1;
×
888
    }
889

890
    joined = (char *)malloc(directory_length + entry_length +
×
891
                            (size_t)needs_separator + 1u);
×
892
    if (joined == NULL) {
×
893
        return NULL;
×
894
    }
895

896
    if (directory_length > 0u) {
×
897
        memcpy(joined, directory, directory_length);
×
898
    }
899
    if (needs_separator) {
×
900
        joined[directory_length] = '/';
×
901
    }
902
    if (entry_length > 0u) {
×
903
        memcpy(joined + directory_length + (size_t)needs_separator,
×
904
               entry,
905
               entry_length);
906
    }
907
    joined[directory_length + (size_t)needs_separator + entry_length] = '\0';
×
908

909
    return joined;
×
910
}
911

912
static void
913
sixel_option_format_timestamp(
×
914
    time_t value,
915
    char *buffer,
916
    size_t buffer_size)
917
{
918
    struct tm *time_pointer;
919
#if defined(HAVE_LOCALTIME_R)
920
    struct tm time_view;
921
#endif
922

923
    if (buffer == NULL || buffer_size == 0u) {
×
924
        return;
×
925
    }
926

927
#if defined(HAVE_LOCALTIME_R)
928
    if (localtime_r(&value, &time_view) != NULL) {
929
        (void)strftime(buffer, buffer_size, "%Y-%m-%d %H:%M", &time_view);
930
        return;
931
    }
932
#endif
933
    time_pointer = localtime(&value);
×
934
    if (time_pointer != NULL) {
×
935
        (void)strftime(buffer, buffer_size, "%Y-%m-%d %H:%M", time_pointer);
×
936
        return;
×
937
    }
938

939
    (void)snprintf(buffer, buffer_size, "unknown");
×
940
}
941

942
static int
943
sixel_option_candidate_compare(void const *lhs, void const *rhs)
×
944
{
945
    sixel_option_path_candidate_t const *left;
946
    sixel_option_path_candidate_t const *right;
947

948
    left = (sixel_option_path_candidate_t const *)lhs;
×
949
    right = (sixel_option_path_candidate_t const *)rhs;
×
950

951
    if (left->total_score < right->total_score) {
×
952
        return 1;
×
953
    }
954
    if (left->total_score > right->total_score) {
×
955
        return -1;
×
956
    }
957
    if (left->mtime < right->mtime) {
×
958
        return 1;
×
959
    }
960
    if (left->mtime > right->mtime) {
×
961
        return -1;
×
962
    }
963

964
    if (left->path == NULL || right->path == NULL) {
×
965
        return 0;
×
966
    }
967

968
    return strcmp(left->path, right->path);
×
969
}
970

971
static char *
972
sixel_option_strerror(
×
973
    int error_number,
974
    char *buffer,
975
    size_t buffer_size)
976
{
977
#if defined(_MSC_VER)
978
    errno_t status;
979
#elif defined(_WIN32)
980
# if defined(__STDC_LIB_EXT1__)
981
    errno_t status;
982
# else
983
    char *message;
984
    size_t copy_length;
985
# endif
986
#else
987
# if defined(_GNU_SOURCE)
988
    char *message;
989
    size_t copy_length;
990
# endif
991
#endif
992

993
    if (buffer == NULL || buffer_size == 0u) {
×
994
        return NULL;
×
995
    }
996

997
#if defined(_MSC_VER)
998
    status = strerror_s(buffer, buffer_size, error_number);
999
    if (status != 0) {
1000
        buffer[0] = '\0';
1001
        return NULL;
1002
    }
1003
    return buffer;
1004
#elif defined(_WIN32)
1005
# if defined(__STDC_LIB_EXT1__)
1006
    status = strerror_s(buffer, buffer_size, error_number);
1007
    if (status != 0) {
1008
        buffer[0] = '\0';
1009
        return NULL;
1010
    }
1011
    return buffer;
1012
# else
1013
    message = strerror(error_number);
1014
    if (message == NULL) {
1015
        buffer[0] = '\0';
1016
        return NULL;
1017
    }
1018
    copy_length = buffer_size - 1u;
1019
    (void)strncpy(buffer, message, copy_length);
1020
    buffer[buffer_size - 1u] = '\0';
1021
    return buffer;
1022
# endif
1023
#else
1024
# if defined(_GNU_SOURCE)
1025
    message = strerror_r(error_number, buffer, buffer_size);
1026
    if (message == NULL) {
1027
        return NULL;
1028
    }
1029
    if (message != buffer) {
1030
        copy_length = buffer_size - 1u;
1031
        (void)strncpy(buffer, message, copy_length);
1032
        buffer[buffer_size - 1u] = '\0';
1033
    }
1034
    return buffer;
1035
# else
1036
    if (strerror_r(error_number, buffer, buffer_size) != 0) {
×
1037
        buffer[0] = '\0';
×
1038
        return NULL;
×
1039
    }
1040
    return buffer;
×
1041
# endif
1042
#endif
1043
}
1044

1045
static int
1046
sixel_option_path_is_clipboard(char const *argument)
228✔
1047
{
1048
    char const *marker;
1049

1050
    marker = NULL;
228✔
1051

1052
    if (argument == NULL) {
228!
1053
        return 0;
×
1054
    }
1055

1056
    marker = strstr(argument, "clipboard:");
228✔
1057
    if (marker == NULL) {
228!
1058
        return 0;
228✔
1059
    }
UNCOV
1060
    if (marker[10] != '\0') {
×
1061
        return 0;
×
1062
    }
UNCOV
1063
    if (marker == argument) {
×
UNCOV
1064
        return 1;
×
1065
    }
1066
    if (marker > argument && marker[-1] == ':') {
×
1067
        return 1;
×
1068
    }
1069

1070
    return 0;
×
1071
}
1072

1073
static int
1074
sixel_option_path_looks_remote(char const *path)
228✔
1075
{
1076
    char const *separator;
1077
    size_t prefix_length;
1078
    size_t index;
1079
    unsigned char value;
1080

1081
    separator = NULL;
228✔
1082
    prefix_length = 0u;
228✔
1083
    index = 0u;
228✔
1084
    value = 0u;
228✔
1085

1086
    if (path == NULL) {
228!
1087
        return 0;
×
1088
    }
1089

1090
    separator = strstr(path, "://");
228✔
1091
    if (separator == NULL) {
228!
1092
        return 0;
228✔
1093
    }
1094

1095
    prefix_length = (size_t)(separator - path);
×
1096
    if (prefix_length == 0u) {
×
1097
        return 0;
×
1098
    }
1099

1100
    while (index < prefix_length) {
×
1101
        value = (unsigned char)path[index];
×
1102
        if (!(isalpha(value) || value == '+' || value == '-' ||
×
1103
              value == '.')) {
1104
            return 0;
×
1105
        }
1106
        ++index;
×
1107
    }
1108

1109
    return 1;
×
1110
}
1111

1112
/*
1113
 * Compose a multi-line diagnostic highlighting the missing path along with
1114
 * nearby suggestions.  The first line reports the original token supplied by
1115
 * the caller so CLI wrappers can relay the exact argument the user typed.
1116
 */
1117
static int
1118
sixel_option_build_missing_path_message(
6✔
1119
    char const *argument,
1120
    char const *resolved_path,
1121
    char *buffer,
1122
    size_t buffer_size)
1123
{
1124
    char *directory_copy;
1125
    char const *argument_view;
1126
    char const *target_name;
1127
    size_t offset;
1128
    int written;
1129
    int result;
1130
    int suggestions_enabled;
1131
#if HAVE_DIRENT_H && HAVE_SYS_STAT_H
1132
    DIR *directory_stream;
1133
    struct dirent *entry;
1134
    sixel_option_path_candidate_t *candidates;
1135
    sixel_option_path_candidate_t *grown;
1136
    size_t candidate_count;
1137
    size_t candidate_capacity;
1138
    size_t index;
1139
    size_t new_capacity;
1140
    struct stat entry_stat;
1141
    char *candidate_path;
1142
    time_t min_mtime;
1143
    time_t max_mtime;
1144
    double recency_range;
1145
    double percent_double;
1146
    int percent_int;
1147
    char time_buffer[64];
1148
    int error_code;
1149
    char error_buffer[128];
1150
#else
1151
    (void)resolved_path;
1152
#endif
1153

1154
    directory_copy = sixel_option_duplicate_directory(resolved_path);
6✔
1155
    if (directory_copy == NULL) {
6!
1156
        return -1;
×
1157
    }
1158
    argument_view = (argument != NULL && argument[0] != '\0')
6!
1159
        ? argument : resolved_path;
12!
1160
    target_name = sixel_option_basename_view(resolved_path);
6✔
1161
    offset = 0u;
6✔
1162
    written = 0;
6✔
1163
    result = 0;
6✔
1164
    suggestions_enabled = 0;
6✔
1165

1166
    if (buffer == NULL || buffer_size == 0u) {
6!
1167
        free(directory_copy);
×
1168
        return -1;
×
1169
    }
1170

1171
    written = snprintf(buffer,
6!
1172
                       buffer_size,
1173
                       "path \"%s\" not found.\n",
1174
                       argument_view != NULL ? argument_view : "");
1175
    if (written < 0) {
6!
1176
        written = 0;
×
1177
    }
1178
    if ((size_t)written >= buffer_size) {
6!
1179
        offset = buffer_size - 1u;
×
1180
    } else {
1181
        offset = (size_t)written;
6✔
1182
    }
1183

1184
#if !(HAVE_DIRENT_H && HAVE_SYS_STAT_H)
1185
    suggestions_enabled = sixel_option_environment_is_enabled(
1186
        SIXEL_OPTION_ENV_PATH_SUGGESTIONS);
1187
    if (suggestions_enabled && offset < buffer_size - 1u) {
1188
        written = snprintf(buffer + offset,
1189
                           buffer_size - offset,
1190
                           "Suggestion lookup unavailable on this build.\n");
1191
        if (written < 0) {
1192
            written = 0;
1193
        }
1194
    }
1195
    free(directory_copy);
1196
    return 0;
1197
#else
1198
    suggestions_enabled = sixel_option_environment_is_enabled(
6✔
1199
        SIXEL_OPTION_ENV_PATH_SUGGESTIONS);
1200
    if (!suggestions_enabled) {
6!
1201
        free(directory_copy);
6✔
1202
        return 0;
6✔
1203
    }
1204
    directory_stream = NULL;
×
1205
    entry = NULL;
×
1206
    candidates = NULL;
×
1207
    grown = NULL;
×
1208
    candidate_count = 0u;
×
1209
    candidate_capacity = 0u;
×
1210
    index = 0u;
×
1211
    new_capacity = 0u;
×
1212
    memset(&entry_stat, 0, sizeof(entry_stat));
×
1213
    candidate_path = NULL;
×
1214
    min_mtime = 0;
×
1215
    max_mtime = 0;
×
1216
    recency_range = 0.0;
×
1217
    percent_double = 0.0;
×
1218
    percent_int = 0;
×
1219
    memset(time_buffer, 0, sizeof(time_buffer));
×
1220
    error_code = 0;
×
1221
    memset(error_buffer, 0, sizeof(error_buffer));
×
1222

1223
    directory_stream = opendir(directory_copy);
×
1224
    if (directory_stream == NULL) {
×
1225
        error_code = errno;
×
1226
        if (error_code == ENOENT) {
×
1227
            if (offset < buffer_size - 1u) {
×
1228
                written = snprintf(buffer + offset,
×
1229
                                   buffer_size - offset,
1230
                                   "Directory \"%s\" does not exist.\n",
1231
                                   directory_copy != NULL
1232
                                       ? directory_copy
1233
                                       : "(null)");
1234
                if (written < 0) {
×
1235
                    written = 0;
×
1236
                }
1237
            }
1238
        } else {
1239
            if (sixel_option_strerror(error_code,
×
1240
                                      error_buffer,
1241
                                      sizeof(error_buffer)) == NULL) {
1242
                error_buffer[0] = '\0';
×
1243
            }
1244
            if (offset < buffer_size - 1u) {
×
1245
                written = snprintf(buffer + offset,
×
1246
                                   buffer_size - offset,
1247
                                   "Unable to inspect \"%s\": %s\n",
1248
                                   directory_copy != NULL
1249
                                       ? directory_copy
1250
                                       : "(null)",
UNCOV
1251
                                   error_buffer[0] != '\0'
×
1252
                                       ? error_buffer
1253
                                       : "unknown error");
1254
                if (written < 0) {
×
1255
                    written = 0;
×
1256
                }
1257
            }
1258
        }
1259
        free(directory_copy);
×
1260
        return result;
×
1261
    }
1262

1263
    while ((entry = readdir(directory_stream)) != NULL) {
×
1264
        if (entry->d_name[0] == '.' &&
×
1265
                (entry->d_name[1] == '\0' ||
×
1266
                 (entry->d_name[1] == '.' && entry->d_name[2] == '\0'))) {
×
1267
            continue;
×
1268
        }
1269
        candidate_path = sixel_option_join_directory_entry(directory_copy,
×
1270
                                                            entry->d_name);
×
1271
        if (candidate_path == NULL) {
×
1272
            continue;
×
1273
        }
1274
        if (stat(candidate_path, &entry_stat) != 0) {
×
1275
            free(candidate_path);
×
1276
            candidate_path = NULL;
×
1277
            continue;
×
1278
        }
1279
        if (!S_ISREG(entry_stat.st_mode)) {
×
1280
#if defined(S_ISLNK)
1281
            if (!S_ISLNK(entry_stat.st_mode)) {
×
1282
                free(candidate_path);
×
1283
                candidate_path = NULL;
×
1284
                continue;
×
1285
            }
1286
#else
1287
            free(candidate_path);
1288
            candidate_path = NULL;
1289
            continue;
1290
#endif
1291
        }
1292
        if (candidate_count == candidate_capacity) {
×
1293
            new_capacity = (candidate_capacity == 0u)
×
1294
                ? 8u
1295
                : candidate_capacity * 2u;
×
1296
            grown = (sixel_option_path_candidate_t *)realloc(
×
1297
                candidates,
1298
                new_capacity * sizeof(sixel_option_path_candidate_t));
1299
            if (grown == NULL) {
×
1300
                free(candidate_path);
×
1301
                candidate_path = NULL;
×
1302
                break;
×
1303
            }
1304
            candidates = grown;
×
1305
            candidate_capacity = new_capacity;
×
1306
        }
1307
        candidates[candidate_count].path = candidate_path;
×
1308
        candidates[candidate_count].name =
×
1309
            sixel_option_basename_view(candidate_path);
×
1310
        candidates[candidate_count].mtime = entry_stat.st_mtime;
×
1311
        candidates[candidate_count].name_score = 0.0;
×
1312
        candidates[candidate_count].extension_score = 0.0;
×
1313
        candidates[candidate_count].recency_score = 0.0;
×
1314
        candidates[candidate_count].total_score = 0.0;
×
1315
        ++candidate_count;
×
1316
        candidate_path = NULL;
×
1317
    }
1318

1319
    if (directory_stream != NULL) {
×
1320
        (void)closedir(directory_stream);
×
1321
        directory_stream = NULL;
×
1322
    }
1323

1324
    if (candidate_count == 0u) {
×
1325
        if (offset < buffer_size - 1u) {
×
1326
            written = snprintf(buffer + offset,
×
1327
                               buffer_size - offset,
1328
                               "No nearby matches were found in \"%s\".\n",
1329
                               directory_copy != NULL
1330
                                   ? directory_copy
1331
                                   : "(null)");
1332
            if (written < 0) {
×
1333
                written = 0;
×
1334
            }
1335
        }
1336
        free(directory_copy);
×
1337
        return 0;
×
1338
    }
1339

1340
    min_mtime = candidates[0].mtime;
×
1341
    max_mtime = candidates[0].mtime;
×
1342
    for (index = 0u; index < candidate_count; ++index) {
×
1343
        candidates[index].name_score =
×
1344
            sixel_option_normalized_levenshtein(target_name,
×
1345
                                                 candidates[index].name,
×
1346
                                                 NULL);
1347
        candidates[index].extension_score =
×
1348
            sixel_option_extension_similarity(target_name,
×
1349
                                              candidates[index].name);
×
1350
        if (index == 0u || candidates[index].mtime < min_mtime) {
×
1351
            min_mtime = candidates[index].mtime;
×
1352
        }
1353
        if (index == 0u || candidates[index].mtime > max_mtime) {
×
1354
            max_mtime = candidates[index].mtime;
×
1355
        }
1356
    }
1357

1358
    recency_range = (double)(max_mtime - min_mtime);
×
1359
    for (index = 0u; index < candidate_count; ++index) {
×
1360
        if (recency_range <= 0.0) {
×
1361
            candidates[index].recency_score = 1.0;
×
1362
        } else {
1363
            candidates[index].recency_score =
×
1364
                (double)(candidates[index].mtime - min_mtime) /
×
1365
                recency_range;
1366
        }
1367
        candidates[index].total_score =
×
UNCOV
1368
            SIXEL_OPTION_SUGGESTION_NAME_WEIGHT *
×
1369
                candidates[index].name_score +
×
1370
            SIXEL_OPTION_SUGGESTION_EXTENSION_WEIGHT *
×
1371
                candidates[index].extension_score +
×
UNCOV
1372
            SIXEL_OPTION_SUGGESTION_RECENCY_WEIGHT *
×
1373
                candidates[index].recency_score;
×
1374
    }
1375

1376
    qsort(candidates,
×
1377
          candidate_count,
1378
          sizeof(sixel_option_path_candidate_t),
1379
          sixel_option_candidate_compare);
1380

1381
    if (offset < buffer_size - 1u) {
×
1382
        written = snprintf(buffer + offset,
×
1383
                           buffer_size - offset,
1384
                           "Suggestions:\n");
1385
        if (written < 0) {
×
1386
            written = 0;
×
1387
        }
1388
        if ((size_t)written >= buffer_size - offset) {
×
1389
            offset = buffer_size - 1u;
×
1390
        } else {
1391
            offset += (size_t)written;
×
1392
        }
1393
    }
1394

1395
    for (index = 0u; index < candidate_count &&
×
1396
            index < SIXEL_OPTION_SUGGESTION_LIMIT; ++index) {
×
1397
        percent_double = candidates[index].total_score * 100.0;
×
1398
        if (percent_double < 0.0) {
×
1399
            percent_double = 0.0;
×
1400
        }
1401
        if (percent_double > 100.0) {
×
1402
            percent_double = 100.0;
×
1403
        }
1404
        percent_int = (int)(percent_double + 0.5);
×
1405
        sixel_option_format_timestamp(candidates[index].mtime,
×
1406
                                      time_buffer,
1407
                                      sizeof(time_buffer));
1408
        if (offset < buffer_size - 1u) {
×
1409
            written = snprintf(buffer + offset,
×
1410
                               buffer_size - offset,
1411
                               "  * %s (similarity score %d%%, "
1412
                               "modified %s)\n",
UNCOV
1413
                               candidates[index].path != NULL
×
UNCOV
1414
                                   ? candidates[index].path
×
1415
                                   : "(unknown)",
1416
                               percent_int,
1417
                               time_buffer);
1418
            if (written < 0) {
×
1419
                written = 0;
×
1420
            }
1421
            if ((size_t)written >= buffer_size - offset) {
×
1422
                offset = buffer_size - 1u;
×
1423
            } else {
1424
                offset += (size_t)written;
×
1425
            }
1426
        }
1427
    }
1428

1429
    if (directory_stream != NULL) {
×
1430
        (void)closedir(directory_stream);
×
1431
        directory_stream = NULL;
×
1432
    }
1433
    if (candidates != NULL) {
×
1434
        for (index = 0u; index < candidate_count; ++index) {
×
1435
            free(candidates[index].path);
×
1436
            candidates[index].path = NULL;
×
1437
        }
1438
        free(candidates);
×
1439
        candidates = NULL;
×
1440
    }
1441
    free(directory_copy);
×
1442

1443
    return result;
×
1444
#endif
1445
}
1446

1447
int
1448
sixel_option_validate_filesystem_path(
230✔
1449
    char const *argument,
1450
    char const *resolved_path,
1451
    unsigned int flags)
1452
{
1453
#if !(HAVE_SYS_STAT_H)
1454
    (void)argument;
1455
    (void)resolved_path;
1456
    (void)flags;
1457
    return 0;
1458
#else
1459
    struct stat path_stat;
1460
    int stat_result;
1461
    int error_value;
1462
    int allow_stdin;
1463
    int allow_clipboard;
1464
    int allow_remote;
1465
    int allow_empty;
1466
    char const *remote_view;
1467
    char message_buffer[1024];
1468

1469
    memset(&path_stat, 0, sizeof(path_stat));
230✔
1470
    stat_result = 0;
230✔
1471
    error_value = 0;
230✔
1472
    allow_stdin = (flags & SIXEL_OPTION_PATH_ALLOW_STDIN) != 0u;
230✔
1473
    allow_clipboard = (flags & SIXEL_OPTION_PATH_ALLOW_CLIPBOARD) != 0u;
230✔
1474
    allow_remote = (flags & SIXEL_OPTION_PATH_ALLOW_REMOTE) != 0u;
230✔
1475
    allow_empty = (flags & SIXEL_OPTION_PATH_ALLOW_EMPTY) != 0u;
230✔
1476
    remote_view = resolved_path != NULL ? resolved_path : argument;
230!
1477
    memset(message_buffer, 0, sizeof(message_buffer));
230✔
1478

1479
    /*
1480
     * Reject empty path arguments unless the caller explicitly opts in.
1481
     * Historically the CLI rejected blank -i payloads while tolerating empty
1482
     * -m mapfile values, so the new flag preserves that behaviour without
1483
     * reintroducing option-specific strings here.
1484
     */
1485
    if ((argument == NULL || argument[0] == '\0') &&
230!
1486
            (resolved_path == NULL || resolved_path[0] == '\0')) {
×
1487
        if (!allow_empty) {
×
1488
            sixel_helper_set_additional_message(
×
1489
                "path argument is empty.");
1490
            return -1;
×
1491
        }
1492
        return 0;
×
1493
    }
1494

1495
    if (resolved_path == NULL || resolved_path[0] == '\0') {
230!
1496
        if (!allow_empty) {
×
1497
            sixel_helper_set_additional_message(
×
1498
                "path argument is empty.");
1499
            return -1;
×
1500
        }
1501
        return 0;
×
1502
    }
1503
    if (allow_stdin && argument != NULL && strcmp(argument, "-") == 0) {
230!
1504
        return 0;
2✔
1505
    }
1506
    if (allow_clipboard && sixel_option_path_is_clipboard(argument)) {
228!
UNCOV
1507
        return 0;
×
1508
    }
1509
    if (allow_remote && sixel_option_path_looks_remote(remote_view)) {
228!
1510
        return 0;
×
1511
    }
1512

1513
    errno = 0;
228✔
1514
    stat_result = stat(resolved_path, &path_stat);
228✔
1515
    if (stat_result == 0) {
228✔
1516
        return 0;
222✔
1517
    }
1518

1519
    error_value = errno;
6✔
1520
    if (error_value != ENOENT && error_value != ENOTDIR) {
6!
1521
        return 0;
×
1522
    }
1523

1524
    if (sixel_option_build_missing_path_message(argument,
6!
1525
                                                resolved_path,
1526
                                                message_buffer,
1527
                                                sizeof(message_buffer)) != 0) {
1528
        sixel_helper_set_additional_message(
×
1529
            "path validation failed.");
1530
    } else {
1531
        sixel_helper_set_additional_message(message_buffer);
6✔
1532
    }
1533

1534
    return -1;
6✔
1535
#endif
1536
}
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