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

saitoha / libsixel / 19918707358

04 Dec 2025 05:12AM UTC coverage: 38.402% (-4.0%) from 42.395%
19918707358

push

github

saitoha
tests: fix meson msys dll lookup

9738 of 38220 branches covered (25.48%)

12841 of 33438 relevant lines covered (38.4%)

782420.02 hits per line

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

60.88
/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)
600✔
82
{
83
    sixel_option_apply_env_default(
600✔
84
        SIXEL_OPTION_ENV_PREFIX_SUGGESTIONS);
85
    sixel_option_apply_env_default(
600✔
86
        SIXEL_OPTION_ENV_FUZZY_SUGGESTIONS);
87
    /* Path suggestions stay opt-in for the CLI frontends. */
88
}
600✔
89

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

96
    existing = NULL;
1,200✔
97
    status = 0;
1,200✔
98

99
    if (variable == NULL) {
1,200!
100
        return;
101
    }
102

103
    existing = sixel_compat_getenv(variable);
1,200✔
104
    if (existing != NULL) {
1,200!
105
        return;
106
    }
107

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

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

117
    value = NULL;
51✔
118

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

123
    value = sixel_compat_getenv(variable);
51✔
124
    if (value == NULL) {
51✔
125
        return 0;
126
    }
127
    if (value[0] == '1' && value[1] == '\0') {
45!
128
        return 1;
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(
285✔
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;
285✔
166
    size_t value_length;
285✔
167
    int candidate_index;
285✔
168
    size_t match_count;
285✔
169
    int base_value;
285✔
170
    int base_value_set;
285✔
171
    int ambiguous_values;
285✔
172
    size_t diag_length;
285✔
173
    size_t copy_length;
285✔
174

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

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

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

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

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

246
    return SIXEL_OPTION_CHOICE_AMBIGUOUS;
247
}
248

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

260
    if (buffer == NULL || buffer_size == 0u) {
6!
261
        return;
262
    }
263
    suggestions_enabled = sixel_option_environment_is_enabled(
6✔
264
        SIXEL_OPTION_ENV_PREFIX_SUGGESTIONS);
265
    active_candidates = suggestions_enabled ? candidates : NULL;
6!
266
    if (active_candidates != NULL && active_candidates[0] != '\0') {
6!
267
        written = snprintf(buffer,
6✔
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;
6✔
279
    sixel_helper_set_additional_message(buffer);
6✔
280
}
1!
281

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

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

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

311
    sixel_helper_set_additional_message(base_message);
30✔
312
}
1!
313

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

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

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

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

351
static size_t
352
sixel_option_collect_choice_suggestions(
39✔
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;
39✔
360
    size_t candidate_count;
39✔
361
    size_t index;
39✔
362
    size_t diag_length;
39✔
363
    size_t copy_length;
39✔
364
    size_t distance;
39✔
365
    size_t name_length;
39✔
366
    size_t prefix_length;
39✔
367
    size_t existing_index;
39✔
368
    double score;
39✔
369
    char *prefix;
39✔
370
    char const *name;
39✔
371
    int has_existing;
39✔
372
    int suggestions_enabled;
39✔
373

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

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

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

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

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

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

477
    if (candidate_count > 1u) {
9✔
478
        qsort(candidates,
6✔
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) {
9!
489
        for (index = 0u; index < candidate_count; ++index) {
24✔
490
            if (diag_length > 0u && diag_length + 2u < diagnostic_size) {
15!
491
                diagnostic[diag_length] = ',';
6✔
492
                diagnostic[diag_length + 1u] = ' ';
6✔
493
                diag_length += 2u;
6✔
494
                diagnostic[diag_length] = '\0';
6✔
495
            }
496
            copy_length = strlen(candidates[index].name);
15✔
497
            if (copy_length > diagnostic_size - diag_length - 1u) {
15!
498
                copy_length = diagnostic_size - diag_length - 1u;
499
            }
500
            memcpy(diagnostic + diag_length,
15!
501
                   candidates[index].name,
502
                   copy_length);
503
            diag_length += copy_length;
15✔
504
            diagnostic[diag_length] = '\0';
15✔
505
            if (diag_length >= diagnostic_size - 1u) {
15!
506
                break;
507
            }
508
        }
509
    }
510

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

515
static int
516
sixel_option_choice_prefix_accepts(
1,803✔
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;
1,803✔
523
    int base_value;
1,803✔
524
    int base_set;
1,803✔
525

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

530
    base_value = 0;
531
    base_set = 0;
532
    for (index = 0u; index < choice_count; ++index) {
16,737✔
533
        if (strncmp(choices[index].name, name, prefix_length) != 0) {
15,528✔
534
            continue;
13,107✔
535
        }
536
        if (!base_set) {
2,421✔
537
            base_value = choices[index].value;
1,803✔
538
            base_set = 1;
1,803✔
539
            continue;
1,803✔
540
        }
541
        if (choices[index].value != base_value) {
618✔
542
            return 0;
543
        }
544
    }
545

546
    return base_set;
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;
6✔
601
#if defined(_WIN32)
602
    char const *backward;
603
#endif
604
    char const *start;
6✔
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;
6✔
626
    }
627

628
    return start;
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(
1,209✔
652
    char const *lhs,
653
    char const *rhs,
654
    size_t *distance_out)
655
{
656
    size_t lhs_length;
1,209✔
657
    size_t rhs_length;
1,209✔
658
    size_t *previous;
1,209✔
659
    size_t *current;
1,209✔
660
    size_t column;
1,209✔
661
    size_t row;
1,209✔
662
    size_t cost;
1,209✔
663
    size_t deletion;
1,209✔
664
    size_t insertion;
1,209✔
665
    size_t substitution;
1,209✔
666
    size_t distance_value;
1,209✔
667
    unsigned char left_char;
1,209✔
668
    unsigned char right_char;
1,209✔
669
    double normalized;
1,209✔
670

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

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

690
    if (lhs == NULL || rhs == NULL) {
1,209!
691
        return 0.0;
692
    }
693

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

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

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

711
    column = 0u;
712
    while (column <= rhs_length) {
7,581✔
713
        previous[column] = column;
6,372✔
714
        ++column;
6,372✔
715
    }
716

717
    row = 1u;
718
    while (row <= lhs_length) {
13,779✔
719
        current[0] = row;
12,570✔
720
        column = 1u;
12,570✔
721
        while (column <= rhs_length) {
64,989✔
722
            left_char = (unsigned char)lhs[row - 1u];
52,419✔
723
            right_char = (unsigned char)rhs[column - 1u];
52,419✔
724
            cost = (tolower(left_char) == tolower(right_char)) ? 0u : 1u;
52,419✔
725
            deletion = previous[column] + 1u;
52,419✔
726
            insertion = current[column - 1u] + 1u;
52,419✔
727
            substitution = previous[column - 1u] + cost;
52,419✔
728
            current[column] = deletion;
52,419✔
729
            if (insertion < current[column]) {
52,419✔
730
                current[column] = insertion;
10,434✔
731
            }
732
            if (substitution < current[column]) {
52,419✔
733
                current[column] = substitution;
8,604✔
734
            }
735
            ++column;
52,419✔
736
        }
737
        memcpy(previous, current, (rhs_length + 1u) * sizeof(size_t));
12,570✔
738
        ++row;
12,570✔
739
    }
740

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

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

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

755
    return normalized;
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)
×
780
{
781
    size_t length;
×
782
    char *copy;
×
783

784
    length = 0u;
×
785
    copy = NULL;
×
786

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

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

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

802
    return copy;
×
803
}
804

805
static char *
806
sixel_option_duplicate_directory(char const *path)
6✔
807
{
808
    char const *forward;
6✔
809
#if defined(_WIN32)
810
    char const *backward;
811
#endif
812
    char const *separator;
6✔
813
    size_t length;
6✔
814
    char *copy;
6✔
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(".");
×
839
    }
840
    if (separator == path) {
6!
841
        return sixel_option_duplicate_string("/");
×
842
    }
843

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

854
    return copy;
6✔
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)
336✔
1047
{
1048
    char const *marker;
336✔
1049

1050
    marker = NULL;
336✔
1051

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

1056
    marker = strstr(argument, "clipboard:");
336✔
1057
    if (marker == NULL) {
336!
1058
        return 0;
1059
    }
1060
    if (marker[10] != '\0') {
×
1061
        return 0;
1062
    }
1063
    if (marker == argument) {
×
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)
336✔
1075
{
1076
    char const *separator;
336✔
1077
    size_t prefix_length;
336✔
1078
    size_t index;
336✔
1079
    unsigned char value;
336✔
1080

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

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

1090
    separator = strstr(path, "://");
336✔
1091
    if (separator == NULL) {
336!
1092
        return 0;
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;
6✔
1125
    char const *argument_view;
6✔
1126
    char const *target_name;
6✔
1127
    size_t offset;
6✔
1128
    int written;
6✔
1129
    int result;
6✔
1130
    int suggestions_enabled;
6✔
1131
#if HAVE_DIRENT_H && HAVE_SYS_STAT_H
1132
    DIR *directory_stream;
6✔
1133
    struct dirent *entry;
6✔
1134
    sixel_option_path_candidate_t *candidates;
6✔
1135
    sixel_option_path_candidate_t *grown;
6✔
1136
    size_t candidate_count;
6✔
1137
    size_t candidate_capacity;
6✔
1138
    size_t index;
6✔
1139
    size_t new_capacity;
6✔
1140
    struct stat entry_stat;
6✔
1141
    char *candidate_path;
6✔
1142
    time_t min_mtime;
6✔
1143
    time_t max_mtime;
6✔
1144
    double recency_range;
6✔
1145
    double percent_double;
6✔
1146
    int percent_int;
6✔
1147
    char time_buffer[64];
6✔
1148
    int error_code;
6✔
1149
    char error_buffer[128];
6✔
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;
6!
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;
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)",
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 =
×
1368
            SIXEL_OPTION_SUGGESTION_NAME_WEIGHT *
×
1369
                candidates[index].name_score +
×
1370
            SIXEL_OPTION_SUGGESTION_EXTENSION_WEIGHT *
×
1371
                candidates[index].extension_score +
×
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",
1413
                               candidates[index].path != NULL
×
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(
339✔
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;
339✔
1460
    int stat_result;
339✔
1461
    int error_value;
339✔
1462
    int allow_stdin;
339✔
1463
    int allow_clipboard;
339✔
1464
    int allow_remote;
339✔
1465
    int allow_empty;
339✔
1466
    char const *remote_view;
339✔
1467
    char message_buffer[1024];
339✔
1468

1469
    memset(&path_stat, 0, sizeof(path_stat));
339!
1470
    stat_result = 0;
339✔
1471
    error_value = 0;
339✔
1472
    allow_stdin = (flags & SIXEL_OPTION_PATH_ALLOW_STDIN) != 0u;
339✔
1473
    allow_clipboard = (flags & SIXEL_OPTION_PATH_ALLOW_CLIPBOARD) != 0u;
339✔
1474
    allow_remote = (flags & SIXEL_OPTION_PATH_ALLOW_REMOTE) != 0u;
339✔
1475
    allow_empty = (flags & SIXEL_OPTION_PATH_ALLOW_EMPTY) != 0u;
339✔
1476
    remote_view = resolved_path != NULL ? resolved_path : argument;
339!
1477
    memset(message_buffer, 0, sizeof(message_buffer));
339!
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') &&
339!
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') {
339!
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) {
339!
1504
        return 0;
1505
    }
1506
    if (allow_clipboard && sixel_option_path_is_clipboard(argument)) {
336!
1507
        return 0;
1508
    }
1509
    if (allow_remote && sixel_option_path_looks_remote(remote_view)) {
336!
1510
        return 0;
1511
    }
1512

1513
    errno = 0;
336✔
1514
    stat_result = stat(resolved_path, &path_stat);
336✔
1515
    if (stat_result == 0) {
336✔
1516
        return 0;
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;
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