Coverage Report

Created: 2026-09-14 06:54

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/jq/src/linker.c
Line
Count
Source
1
#include <assert.h>
2
#include <errno.h>
3
#include <limits.h>
4
#include <string.h>
5
#include <stdio.h>
6
#include <stdlib.h>
7
#include <stdint.h>
8
#include <sys/stat.h>
9
#include <libgen.h>
10
11
#ifdef WIN32
12
#include <shlwapi.h>
13
#endif
14
15
#include "jq_parser.h"
16
#include "locfile.h"
17
#include "jv.h"
18
#include "jq.h"
19
#include "util.h"
20
#include "compile.h"
21
#include "jv_alloc.h"
22
23
struct lib_entry {
24
  char *name;
25
  block def;
26
  int loading;
27
};
28
struct lib_loading_state {
29
  struct lib_entry *entries;
30
  uint64_t ct;
31
};
32
static int load_library(jq_state *jq, jv lib_path,
33
                        int is_data, int raw, int optional,
34
                        const char *as, block *out_block,
35
                        struct lib_loading_state *lib_state);
36
37
0
static int path_is_relative(jv p) {
38
0
  const char *s = jv_string_value(p);
39
40
#ifdef WIN32
41
  int res = PathIsRelativeA(s);
42
#else
43
0
  int res = *s != '/';
44
0
#endif
45
0
  jv_free(p);
46
0
  return res;
47
0
}
48
49
50
// Given a lib_path to search first, creates a chain of search paths
51
// in the following order:
52
// 1. lib_path
53
// 2. -L paths passed in on the command line (from jq_state*) or builtin list
54
5.75k
static jv build_lib_search_chain(jq_state *jq, jv search_path, jv jq_origin, jv lib_origin) {
55
5.75k
  assert(jv_get_kind(search_path) == JV_KIND_ARRAY);
56
5.75k
  jv expanded = jv_array();
57
5.75k
  jv expanded_elt;
58
5.75k
  jv err = jv_null();
59
30.6k
  jv_array_foreach(search_path, i, path) {
60
30.6k
    if (jv_get_kind(path) != JV_KIND_STRING) {
61
0
      jv_free(path);
62
0
      continue;
63
0
    }
64
30.6k
    path = expand_path(path);
65
30.6k
    if (!jv_is_valid(path)) {
66
0
      jv_free(err);
67
0
      err = path;
68
0
      path = jv_null();
69
0
      continue;
70
0
    }
71
30.6k
    if (strcmp(".",jv_string_value(path)) == 0) {
72
136
      expanded_elt = jv_copy(path);
73
30.5k
    } else if (strncmp("$ORIGIN/",jv_string_value(path),sizeof("$ORIGIN/") - 1) == 0) {
74
38
      expanded_elt = jv_string_fmt("%s/%s",
75
38
                               jv_string_value(jq_origin),
76
38
                               jv_string_value(path) + sizeof ("$ORIGIN/") - 1);
77
30.4k
    } else if (jv_get_kind(lib_origin) == JV_KIND_STRING &&
78
0
               path_is_relative(jv_copy(path))) {
79
0
      expanded_elt = jv_string_fmt("%s/%s",
80
0
                               jv_string_value(lib_origin),
81
0
                               jv_string_value(path));
82
30.4k
    } else {
83
30.4k
      expanded_elt = path;
84
30.4k
      path = jv_invalid();
85
30.4k
    }
86
30.6k
    expanded = jv_array_append(expanded, expanded_elt);
87
30.6k
    jv_free(path);
88
30.6k
  }
89
5.75k
  jv_free(jq_origin);
90
5.75k
  jv_free(lib_origin);
91
5.75k
  jv_free(search_path);
92
5.75k
  return JV_ARRAY(expanded, err);
93
5.75k
}
94
95
// Doesn't actually check that name not be an absolute path, and we
96
// don't have to: we always append relative paths to others (with a '/'
97
// in between).
98
7.67k
static jv validate_relpath(jv name) {
99
7.67k
  const char *s = jv_string_value(name);
100
7.67k
  if (strlen(s) != (size_t)jv_string_length_bytes(jv_copy(name))) {
101
120
    jv_free(name);
102
120
    return jv_invalid_with_msg(jv_string("Module path contains a NUL byte"));
103
120
  }
104
7.55k
  if (strchr(s, '\\')) {
105
60
    jv res = jv_invalid_with_msg(jv_string_fmt("Modules must be named by relative paths using '/', not '\\' (%s)", s));
106
60
    jv_free(name);
107
60
    return res;
108
60
  }
109
7.49k
  jv components = jv_string_split(jv_copy(name), jv_string("/"));
110
9.51k
  jv_array_foreach(components, i, x) {
111
9.51k
    if (!strcmp(jv_string_value(x), "..")) {
112
1.74k
      jv_free(x);
113
1.74k
      jv_free(components);
114
1.74k
      jv res = jv_invalid_with_msg(jv_string_fmt("Relative paths to modules may not traverse to parent directories (%s)", s));
115
1.74k
      jv_free(name);
116
1.74k
      return res;
117
1.74k
    }
118
7.77k
    if (i > 0 && jv_equal(jv_copy(x), jv_array_get(jv_copy(components), i - 1))) {
119
0
      jv_free(x);
120
0
      jv_free(components);
121
0
      jv res = jv_invalid_with_msg(jv_string_fmt("module names must not have equal consecutive components: %s",
122
0
                                                 jv_string_value(name)));
123
0
      jv_free(name);
124
0
      return res;
125
0
    }
126
7.77k
    jv_free(x);
127
7.77k
  }
128
5.75k
  jv_free(components);
129
5.75k
  return name;
130
7.49k
}
131
132
// Assumes name has been validated
133
5.75k
static jv jv_basename(jv name) {
134
5.75k
  const char *s = jv_string_value(name);
135
5.75k
  const char *p = strrchr(s, '/');
136
5.75k
  if (!p)
137
5.66k
    return name;
138
91
  jv res = jv_string_fmt("%s", p);
139
91
  jv_free(name);
140
91
  return res;
141
5.75k
}
142
143
// Asummes validated relative path to module
144
7.67k
static jv find_lib(jq_state *jq, jv rel_path, jv search, const char *suffix, jv jq_origin, jv lib_origin) {
145
7.67k
  if (!jv_is_valid(rel_path)) {
146
1.92k
    jv_free(search);
147
1.92k
    jv_free(jq_origin);
148
1.92k
    jv_free(lib_origin);
149
1.92k
    return rel_path;
150
1.92k
  }
151
5.75k
  if (jv_get_kind(rel_path) != JV_KIND_STRING) {
152
0
    jv_free(rel_path);
153
0
    jv_free(search);
154
0
    jv_free(jq_origin);
155
0
    jv_free(lib_origin);
156
0
    return jv_invalid_with_msg(jv_string_fmt("Module path must be a string"));
157
0
  }
158
5.75k
  if (jv_get_kind(search) != JV_KIND_ARRAY) {
159
0
    jv_free(rel_path);
160
0
    jv_free(search);
161
0
    jv_free(jq_origin);
162
0
    jv_free(lib_origin);
163
0
    return jv_invalid_with_msg(jv_string_fmt("Module search path must be an array"));
164
0
  }
165
166
5.75k
  struct stat st;
167
5.75k
  int ret;
168
169
  // Ideally we should cache this somewhere
170
5.75k
  search = build_lib_search_chain(jq, search, jq_origin, lib_origin);
171
5.75k
  jv err = jv_array_get(jv_copy(search), 1);
172
5.75k
  search = jv_array_get(search, 0);
173
174
5.75k
  jv bname = jv_basename(jv_copy(rel_path));
175
176
30.6k
  jv_array_foreach(search, i, spath) {
177
30.6k
    if (jv_get_kind(spath) == JV_KIND_NULL) {
178
0
      jv_free(spath);
179
0
      break;
180
0
    }
181
30.6k
    if (jv_get_kind(spath) != JV_KIND_STRING ||
182
30.6k
        strcmp(jv_string_value(spath), "") == 0) {
183
19.7k
      jv_free(spath);
184
19.7k
      continue; /* XXX report non-strings in search path?? */
185
19.7k
    }
186
    // Try ${search_dir}/${rel_path}.jq
187
10.9k
    jv testpath = jq_realpath(jv_string_fmt("%s/%s%s",
188
10.9k
                                            jv_string_value(spath),
189
10.9k
                                            jv_string_value(rel_path),
190
10.9k
                                            suffix));
191
10.9k
    ret = stat(jv_string_value(testpath),&st);
192
10.9k
    if (ret == -1 && errno == ENOENT) {
193
10.8k
      jv_free(testpath);
194
      // Try ${search_dir}/$(dirname ${rel_path})/jq/main.jq
195
10.8k
      testpath = jq_realpath(jv_string_fmt("%s/%s/%s%s",
196
10.8k
                                           jv_string_value(spath),
197
10.8k
                                           jv_string_value(rel_path),
198
10.8k
                                           "jq/main",
199
10.8k
                                           suffix));
200
10.8k
      ret = stat(jv_string_value(testpath),&st);
201
10.8k
    }
202
10.9k
    if (ret == -1 && errno == ENOENT) {
203
10.8k
      jv_free(testpath);
204
      // Try ${search_dir}/${rel_path}/$(basename ${rel_path}).jq
205
10.8k
      testpath = jq_realpath(jv_string_fmt("%s/%s/%s%s",
206
10.8k
                                           jv_string_value(spath),
207
10.8k
                                           jv_string_value(rel_path),
208
10.8k
                                           jv_string_value(bname),
209
10.8k
                                           suffix));
210
10.8k
      ret = stat(jv_string_value(testpath),&st);
211
10.8k
    }
212
10.9k
    if (ret == 0) {
213
0
      jv_free(err);
214
0
      jv_free(rel_path);
215
0
      jv_free(search);
216
0
      jv_free(bname);
217
0
      jv_free(spath);
218
0
      return testpath;
219
0
    }
220
10.9k
    jv_free(testpath);
221
10.9k
    jv_free(spath);
222
10.9k
  }
223
5.75k
  jv output;
224
5.75k
  if (!jv_is_valid(err)) {
225
0
    err = jv_invalid_get_msg(err);
226
0
    output = jv_invalid_with_msg(jv_string_fmt("module not found: %s (%s)",
227
0
                                               jv_string_value(rel_path),
228
0
                                               jv_string_value(err)));
229
5.75k
  } else {
230
5.75k
    output = jv_invalid_with_msg(jv_string_fmt("module not found: %s",
231
5.75k
                                               jv_string_value(rel_path)));
232
5.75k
  }
233
5.75k
  jv_free(err);
234
5.75k
  jv_free(rel_path);
235
5.75k
  jv_free(search);
236
5.75k
  jv_free(bname);
237
5.75k
  return output;
238
5.75k
}
239
240
3.25k
static jv default_search(jq_state *jq, jv value) {
241
3.25k
  if (!jv_is_valid(value)) {
242
    // dependent didn't say; prepend . to system search path listj
243
4
    jv_free(value);
244
4
    return jv_array_concat(JV_ARRAY(jv_string(".")), jq_get_lib_dirs(jq));
245
4
  }
246
3.25k
  if (jv_get_kind(value) != JV_KIND_ARRAY)
247
3.23k
    return JV_ARRAY(value);
248
22
  return value;
249
3.25k
}
250
251
// XXX Split this into a util that takes a callback, and then...
252
3.25k
static int process_dependencies(jq_state *jq, jv jq_origin, jv lib_origin, block *src_block, struct lib_loading_state *lib_state) {
253
3.25k
  jv deps = block_take_imports(src_block);
254
3.25k
  block bk = *src_block;
255
3.25k
  int nerrors = 0;
256
257
  // XXX This is a backward jv_array_foreach because bindings go in reverse
258
6.48k
  for (int i = jv_array_length(jv_copy(deps)); i > 0; ) {
259
3.25k
    i--;
260
3.25k
    jv dep = jv_array_get(jv_copy(deps), i);
261
262
3.25k
    const char *as_str = NULL;
263
3.25k
    int is_data = jv_get_kind(jv_object_get(jv_copy(dep), jv_string("is_data"))) == JV_KIND_TRUE;
264
3.25k
    int raw = 0;
265
3.25k
    jv v = jv_object_get(jv_copy(dep), jv_string("raw"));
266
3.25k
    if (jv_get_kind(v) == JV_KIND_TRUE)
267
0
      raw = 1;
268
3.25k
    int optional = 0;
269
3.25k
    if (jv_get_kind(jv_object_get(jv_copy(dep), jv_string("optional"))) == JV_KIND_TRUE)
270
3.22k
      optional = 1;
271
3.25k
    jv_free(v);
272
3.25k
    jv relpath = validate_relpath(jv_object_get(jv_copy(dep), jv_string("relpath")));
273
3.25k
    jv as = jv_object_get(jv_copy(dep), jv_string("as"));
274
3.25k
    assert(!jv_is_valid(as) || jv_get_kind(as) == JV_KIND_STRING);
275
3.25k
    if (jv_get_kind(as) == JV_KIND_STRING)
276
29
      as_str = jv_string_value(as);
277
3.25k
    jv search = default_search(jq, jv_object_get(dep, jv_string("search")));
278
    // dep is now freed; do not reuse
279
280
    // find_lib does a lot of work that could be cached...
281
3.25k
    jv resolved = find_lib(jq, relpath, search, is_data ? ".json" : ".jq", jv_copy(jq_origin), jv_copy(lib_origin));
282
    // XXX ...move the rest of this into a callback.
283
3.25k
    if (!jv_is_valid(resolved)) {
284
3.25k
      jv_free(as);
285
3.25k
      if (optional) {
286
3.22k
        jv_free(resolved);
287
3.22k
        continue;
288
3.22k
      }
289
29
      jv emsg = jv_invalid_get_msg(resolved);
290
29
      jq_report_error(jq, jv_string_fmt("jq: error: %s\n",jv_string_value(emsg)));
291
29
      jv_free(emsg);
292
29
      jv_free(deps);
293
29
      jv_free(jq_origin);
294
29
      jv_free(lib_origin);
295
29
      return 1;
296
3.25k
    }
297
298
0
    if (is_data) {
299
      // Can't reuse data libs because the wrong name is bound
300
0
      block dep_def_block;
301
0
      nerrors += load_library(jq, resolved, is_data, raw, optional, as_str, &dep_def_block, lib_state);
302
0
      if (nerrors == 0) {
303
        // Bind as both $data::data and $data for backward compatibility vs common sense
304
0
        bk = block_bind_library(dep_def_block, bk, OP_IS_CALL_PSEUDO, as_str);
305
0
        bk = block_bind_library(dep_def_block, bk, OP_IS_CALL_PSEUDO, NULL);
306
0
      }
307
0
    } else {
308
0
      uint64_t state_idx = 0;
309
0
      for (; state_idx < lib_state->ct; ++state_idx) {
310
0
        if (strcmp(lib_state->entries[state_idx].name, jv_string_value(resolved)) == 0)
311
0
          break;
312
0
      }
313
314
0
      if (state_idx < lib_state->ct) { // Found
315
0
        if (lib_state->entries[state_idx].loading) {
316
0
          jq_report_error(jq, jv_string_fmt("jq: error: circular import of %s\n",
317
0
                                            jv_string_value(resolved)));
318
0
          jv_free(resolved);
319
0
          jv_free(as);
320
0
          jv_free(deps);
321
0
          jv_free(jq_origin);
322
0
          jv_free(lib_origin);
323
0
          return 1;
324
0
        }
325
0
        jv_free(resolved);
326
        // Bind the library to the program
327
0
        bk = block_bind_library(lib_state->entries[state_idx].def, bk, OP_IS_CALL_PSEUDO, as_str);
328
0
      } else { // Not found.   Add it to the table before binding.
329
0
        block dep_def_block = gen_noop();
330
0
        nerrors += load_library(jq, resolved, is_data, raw, optional, as_str, &dep_def_block, lib_state);
331
        // resolved has been freed
332
0
        if (nerrors == 0) {
333
          // Bind the library to the program
334
0
          bk = block_bind_library(dep_def_block, bk, OP_IS_CALL_PSEUDO, as_str);
335
0
        }
336
0
      }
337
0
    }
338
339
0
    jv_free(as);
340
0
  }
341
3.22k
  jv_free(lib_origin);
342
3.22k
  jv_free(jq_origin);
343
3.22k
  jv_free(deps);
344
3.22k
  return nerrors;
345
3.25k
}
346
347
// Loads the library at lib_path into lib_state, putting the library's defs
348
// into *out_block
349
0
static int load_library(jq_state *jq, jv lib_path, int is_data, int raw, int optional, const char *as, block *out_block, struct lib_loading_state *lib_state) {
350
0
  int nerrors = 0;
351
0
  struct locfile* src = NULL;
352
0
  block program;
353
0
  jv data;
354
0
  if (is_data && !raw)
355
0
    data = jv_load_file(jv_string_value(lib_path), 0);
356
0
  else
357
0
    data = jv_load_file(jv_string_value(lib_path), 1);
358
0
  int state_idx;
359
0
  if (!jv_is_valid(data)) {
360
0
    program = gen_noop();
361
0
    if (!optional) {
362
0
      if (jv_invalid_has_msg(jv_copy(data)))
363
0
        data = jv_invalid_get_msg(data);
364
0
      else
365
0
        data = jv_string("unknown error");
366
0
      jq_report_error(jq, jv_string_fmt("jq: error loading data file %s: %s\n", jv_string_value(lib_path), jv_string_value(data)));
367
0
      nerrors++;
368
0
    }
369
0
  } else if (is_data) {
370
    // import "foo" as $bar;
371
0
    program = gen_const_global(jv_copy(data), as);
372
0
    state_idx = lib_state->ct++;
373
0
    lib_state->entries = jv_mem_realloc(lib_state->entries, lib_state->ct * sizeof(struct lib_entry));
374
0
    lib_state->entries[state_idx].name = strdup(jv_string_value(lib_path));
375
0
    lib_state->entries[state_idx].def = program;
376
0
    lib_state->entries[state_idx].loading = 0;
377
0
  } else {
378
    // import "foo" as bar;
379
0
    src = locfile_init(jq, jv_string_value(lib_path), jv_string_value(data), jv_string_length_bytes(jv_copy(data)));
380
0
    nerrors += jq_parse_library(src, &program);
381
0
    locfile_free(src);
382
0
    if (nerrors == 0) {
383
      // Register the library before processing its dependencies so that
384
      // circular imports can be detected.
385
0
      state_idx = lib_state->ct++;
386
0
      lib_state->entries = jv_mem_realloc(lib_state->entries, lib_state->ct * sizeof(struct lib_entry));
387
0
      lib_state->entries[state_idx].name = strdup(jv_string_value(lib_path));
388
0
      lib_state->entries[state_idx].def = gen_noop();
389
0
      lib_state->entries[state_idx].loading = 1;
390
391
0
      char *lib_origin = strdup(jv_string_value(lib_path));
392
0
      nerrors += process_dependencies(jq, jq_get_jq_origin(jq),
393
0
                                      jv_string(dirname(lib_origin)),
394
0
                                      &program, lib_state);
395
0
      free(lib_origin);
396
0
      program = block_bind_self(program, OP_IS_CALL_PSEUDO);
397
0
      lib_state->entries[state_idx].def = program;
398
0
      lib_state->entries[state_idx].loading = 0;
399
0
    }
400
0
  }
401
0
  *out_block = program;
402
0
  jv_free(lib_path);
403
0
  jv_free(data);
404
0
  return nerrors;
405
0
}
406
407
// FIXME It'd be nice to have an option to search the same search path
408
// as we do in process_dependencies.
409
4.41k
jv load_module_meta(jq_state *jq, jv mod_relpath) {
410
  // We can't know the caller's origin; we could though, if it was passed in
411
4.41k
  jv lib_path = find_lib(jq, validate_relpath(mod_relpath), jq_get_lib_dirs(jq), ".jq", jq_get_jq_origin(jq), jv_null());
412
4.41k
  if (!jv_is_valid(lib_path))
413
4.41k
    return lib_path;
414
0
  jv meta = jv_null();
415
0
  jv data = jv_load_file(jv_string_value(lib_path), 1);
416
0
  if (jv_is_valid(data)) {
417
0
    block program;
418
0
    struct locfile* src = locfile_init(jq, jv_string_value(lib_path), jv_string_value(data), jv_string_length_bytes(jv_copy(data)));
419
0
    int nerrors = jq_parse_library(src, &program);
420
0
    if (nerrors == 0) {
421
0
      meta = block_module_meta(program);
422
0
      if (jv_get_kind(meta) == JV_KIND_NULL)
423
0
        meta = jv_object();
424
0
      meta = jv_object_set(meta, jv_string("deps"), block_take_imports(&program));
425
0
      meta = jv_object_set(meta, jv_string("defs"), block_list_funcs(program, 0));
426
0
    }
427
0
    locfile_free(src);
428
0
    block_free(program);
429
0
  }
430
0
  jv_free(lib_path);
431
0
  jv_free(data);
432
0
  return meta;
433
4.41k
}
434
435
3.81k
int load_program(jq_state *jq, struct locfile* src, block *out_block) {
436
3.81k
  int nerrors = 0;
437
3.81k
  block program;
438
3.81k
  struct lib_loading_state lib_state = {0,0};
439
3.81k
  nerrors = jq_parse(src, &program);
440
3.81k
  if (nerrors)
441
554
    return nerrors;
442
443
3.26k
  if (!block_has_main(program)) {
444
5
    jq_report_error(jq, jv_string("jq: error: Top-level program not given (try \".\")"));
445
5
    block_free(program);
446
5
    return 1;
447
5
  }
448
449
3.25k
  jv home = get_home();
450
3.25k
  if (jv_is_valid(home)) {
451
    /* Import ~/.jq as a library named "" found in $HOME or %USERPROFILE% */
452
3.25k
    block import = gen_import_meta(gen_import(jv_string(""), jv_invalid(), 0),
453
3.25k
        gen_const(JV_OBJECT(
454
3.25k
            jv_string("optional"), jv_true(),
455
3.25k
            jv_string("search"), home)));
456
3.25k
    program = BLOCK(import, program);
457
3.25k
  } else {    // silently ignore if home dir not determined
458
0
    jv_free(home);
459
0
  }
460
461
3.25k
  nerrors = process_dependencies(jq, jq_get_jq_origin(jq), jq_get_prog_origin(jq), &program, &lib_state);
462
3.25k
  block libs = gen_noop();
463
3.25k
  for (uint64_t i = 0; i < lib_state.ct; ++i) {
464
0
    free(lib_state.entries[i].name);
465
0
    if (nerrors == 0 && !block_is_const(lib_state.entries[i].def))
466
0
      libs = block_join(libs, lib_state.entries[i].def);
467
0
    else
468
0
      block_free(lib_state.entries[i].def);
469
0
  }
470
3.25k
  free(lib_state.entries);
471
3.25k
  if (nerrors)
472
29
    block_free(program);
473
3.22k
  else
474
3.22k
    *out_block = block_drop_unreferenced(block_join(libs, program));
475
476
3.25k
  return nerrors;
477
3.26k
}