Coverage Report

Created: 2026-10-28 06:47

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/httrack/src/htsopt_selftest.c
Line
Count
Source
1
/* ------------------------------------------------------------ */
2
/*
3
HTTrack Website Copier, Offline Browser for Windows and Unix
4
Copyright (C) 2026 Xavier Roche and other contributors
5
6
SPDX-License-Identifier: GPL-3.0-or-later
7
8
This program is free software: you can redistribute it and/or modify
9
it under the terms of the GNU General Public License as published by
10
the Free Software Foundation, either version 3 of the License, or
11
(at your option) any later version.
12
13
This program is distributed in the hope that it will be useful,
14
but WITHOUT ANY WARRANTY; without even the implied warranty of
15
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
GNU General Public License for more details.
17
18
You should have received a copy of the GNU General Public License
19
along with this program. If not, see <http://www.gnu.org/licenses/>.
20
21
Ethical use: we kindly ask that you NOT use this software to harvest email
22
addresses or to collect any other private information about people. Doing so
23
would dishonor our work and waste the many hours we have spent on it.
24
25
Please visit our Website: http://www.httrack.com
26
*/
27
28
/* ------------------------------------------------------------ */
29
/* File: htsopt_selftest.c subroutines:                         */
30
/*       self-tests for options, aliases and the log callback   */
31
/* Author: Xavier Roche                                         */
32
/* ------------------------------------------------------------ */
33
34
#include "htsselftest_int.h"
35
#include <limits.h>
36
#include <stdint.h>
37
38
/* What the build's optional features came out as, so a test gates on the binary
39
   rather than on the platform it guessed from. */
40
0
static int st_features(httrackp *opt, int argc, char **argv) {
41
0
  (void) opt;
42
0
  (void) argc;
43
0
  (void) argv;
44
0
  printf("brotli %d\n", HTS_USEBROTLI ? 1 : 0);
45
0
  printf("zstd %d\n", HTS_USEZSTD ? 1 : 0);
46
0
#if (defined(__linux) && defined(HAVE_BACKTRACE))
47
0
  printf("backtrace 1\n");
48
#else
49
  printf("backtrace 0\n");
50
#endif
51
#if (defined(HTS_USEICONV) && (HTS_USEICONV == 0))
52
  printf("iconv 0\n");
53
#else
54
0
  printf("iconv 1\n");
55
0
#endif
56
0
#ifdef HTS_CRASH_TEST
57
0
  printf("crashtest 1\n");
58
#else
59
  printf("crashtest 0\n");
60
#endif
61
  /* The kernel, not the build, decides whether MPTCP can be opened. */
62
0
  printf("mptcp %d\n", hts_mptcp_available() ? 1 : 0);
63
0
  printf("mptcpinfo %d\n", hts_mptcp_reports() ? 1 : 0);
64
0
  return 0;
65
0
}
66
67
// -#test=footerfmt <template>: expand a -%F footer with fixed values (drives
68
// tests/01_engine-footerfmt.test). Also asserts the published field names and
69
// the overflow/zero-size returns the CLI cap keeps out of reach.
70
0
static int st_footerfmt(httrackp *opt, int argc, char **argv) {
71
  // Spelled out, not read back from the engine: these ten are the published
72
  // contract front ends validate their templates against.
73
0
  static const char *const names[] = {
74
0
      "addr",    "path", "url",     "date",   "lastmodified",
75
0
      "version", "mime", "charset", "status", "size"};
76
  // Filled by id, as htsparse.c does, so the .test's expected strings pin each
77
  // name to its enum slot; a positional list would not see them drift apart.
78
0
  const char *values[HTS_FOOTER_FIELD_COUNT] = {NULL};
79
0
  size_t i;
80
0
  char out[1024];
81
0
  char tiny[4];
82
83
0
  (void) opt;
84
0
  values[HTS_FOOTER_ADDR] = "host.example";
85
0
  values[HTS_FOOTER_PATH] = "/dir/page.html";
86
0
  values[HTS_FOOTER_URL] = "http://host.example/dir/page.html";
87
0
  values[HTS_FOOTER_DATE] = "DATE";
88
0
  values[HTS_FOOTER_LASTMODIFIED] = "LASTMOD";
89
0
  values[HTS_FOOTER_VERSION] = "VER";
90
0
  values[HTS_FOOTER_MIME] = "text/html";
91
0
  values[HTS_FOOTER_CHARSET] = "utf-8";
92
0
  values[HTS_FOOTER_STATUS] = "200";
93
0
  values[HTS_FOOTER_SIZE] = "1234";
94
95
0
  for (i = 0; i < sizeof(names) / sizeof(names[0]); i++) {
96
0
    assertf(hts_footer_field_ok(names[i]) == HTS_TRUE);
97
0
  }
98
0
  assertf(!hts_footer_field_ok(NULL));
99
0
  assertf(!hts_footer_field_ok(""));
100
0
  assertf(!hts_footer_field_ok("nosuchfield"));
101
  // A prefix or a longer name must not match, or "{addr}" and "{addrx}" would
102
  // validate alike.
103
0
  assertf(!hts_footer_field_ok("add"));
104
0
  assertf(!hts_footer_field_ok("addrx"));
105
  // Matching is exact: the expander is case-sensitive, so a validator that
106
  // accepted "{ADDR}" would green-light a template the crawl emits verbatim.
107
0
  assertf(!hts_footer_field_ok("Addr"));
108
  // Overflow (named and legacy) and a zero-size buffer must return <0, never
109
  // truncate silently or write out of bounds.
110
0
  assertf(hts_footer_format(tiny, sizeof(tiny), "{addr}", values) < 0);
111
0
  assertf(hts_footer_format(tiny, sizeof(tiny), "a %s b", values) < 0);
112
0
  assertf(hts_footer_format(out, 0, "", values) < 0);
113
  // An empty template yields an empty, terminated string.
114
0
  assertf(hts_footer_format(out, sizeof(out), "", values) == 1 &&
115
0
          out[0] == '\0');
116
0
  if (argc < 1) {
117
0
    fprintf(stderr, "footerfmt: needs a template\n");
118
0
    return 1;
119
0
  }
120
0
  if (hts_footer_format(out, sizeof(out), argv[0], values) < 0) {
121
0
    fprintf(stderr, "footerfmt: overflow\n");
122
0
    return 1;
123
0
  }
124
0
  printf("%s\n", out);
125
0
  return 0;
126
0
}
127
128
// hts_split_cmdline(): the vector must grow with the argument count, and a
129
// quote inside a value must not end the argument and hand -V to the parser.
130
0
static int st_cmdlinesplit(httrackp *opt, int argc, char **argv) {
131
0
  char line[512];
132
0
  char **args;
133
0
  int nargs = 0;
134
135
0
  (void) opt;
136
0
  (void) argc;
137
0
  (void) argv;
138
139
  // control: every separator splits, and argv[0] is the program name
140
0
  strcpybuff(line, "httrack http://x/ --quiet\t-c8\n-O out");
141
0
  args = hts_split_cmdline(line, &nargs);
142
0
  assertf(args != NULL && nargs == 6);
143
0
  assertf(args[nargs] == NULL); // callers may walk to the terminator
144
0
  assertf(strcmp(args[0], "httrack") == 0);
145
0
  assertf(strcmp(args[1], "http://x/") == 0);
146
0
  assertf(strcmp(args[2], "--quiet") == 0);
147
0
  assertf(strcmp(args[3], "-c8") == 0);
148
0
  assertf(strcmp(args[4], "-O") == 0);
149
0
  assertf(strcmp(args[5], "out") == 0);
150
0
  freet(args);
151
152
  // the template pads with whitespace: empty arguments are kept (the engine
153
  // skips them), so the count is one per separator
154
0
  strcpybuff(line, "httrack  --quiet");
155
0
  args = hts_split_cmdline(line, &nargs);
156
0
  assertf(nargs == 3 && args[1][0] == '\0');
157
0
  assertf(strcmp(args[2], "--quiet") == 0);
158
0
  freet(args);
159
160
  // a quoted run keeps both its spaces and its quotes: the engine unquotes
161
0
  strcpybuff(line, "httrack --user-agent \"Mozilla 5.0\" -c8");
162
0
  args = hts_split_cmdline(line, &nargs);
163
0
  assertf(nargs == 4);
164
0
  assertf(strcmp(args[2], "\"Mozilla 5.0\"") == 0);
165
0
  assertf(strcmp(args[3], "-c8") == 0);
166
0
  freet(args);
167
168
  // an escaped quote is a literal quote, not the end of the argument: the
169
  // engine strips only the outer pair
170
0
  strcpybuff(line, "httrack --user-agent \"x\\\" -V \\\"touch /tmp/pwn\" -c8");
171
0
  args = hts_split_cmdline(line, &nargs);
172
0
  assertf(nargs == 4);
173
0
  assertf(strcmp(args[2], "\"x\" -V \"touch /tmp/pwn\"") == 0);
174
0
  assertf(strcmp(args[3], "-c8") == 0);
175
0
  freet(args);
176
177
  // \\ is a literal backslash, so a Windows path survives
178
0
  strcpybuff(line, "httrack --path \"C:\\\\dir\\\\sub\"");
179
0
  args = hts_split_cmdline(line, &nargs);
180
0
  assertf(nargs == 3);
181
0
  assertf(strcmp(args[2], "\"C:\\dir\\sub\"") == 0);
182
0
  freet(args);
183
184
  // outside a quoted run a backslash is literal: the url and wildcard-filter
185
  // fields, which the wizard cannot quote, read as before
186
0
  strcpybuff(line, "httrack -*\\** +*.png");
187
0
  args = hts_split_cmdline(line, &nargs);
188
0
  assertf(nargs == 3);
189
0
  assertf(strcmp(args[1], "-*\\**") == 0);
190
0
  assertf(strcmp(args[2], "+*.png") == 0);
191
0
  freet(args);
192
193
  // a quoted run leaves slots unused, so the terminator has to be written and
194
  // not inherited: size the vector from a full line first, so freeing it hands
195
  // the same chunk back with stale pointers in those slots
196
0
  strcpybuff(line, "httrack a b c d e");
197
0
  args = hts_split_cmdline(line, &nargs);
198
0
  assertf(nargs == 6);
199
0
  freet(args);
200
0
  strcpybuff(line, "httrack \"a b c d e\"");
201
0
  args = hts_split_cmdline(line, &nargs);
202
0
  assertf(nargs == 2);
203
0
  assertf(args[nargs] == NULL);
204
0
  freet(args);
205
206
  // an unterminated quote protects the rest of the line, as one argument
207
0
  strcpybuff(line, "httrack --footer \"unbalanced -V x");
208
0
  args = hts_split_cmdline(line, &nargs);
209
0
  assertf(nargs == 3);
210
0
  assertf(strcmp(args[2], "\"unbalanced -V x") == 0);
211
0
  freet(args);
212
213
  // past the 1024 entries the vector used to hold: distinct arguments, so a
214
  // write beyond the allocation cannot read back as the expected parse
215
0
  {
216
0
    const int n = 2000;
217
0
    const size_t size = 16 * (size_t) n + 16;
218
0
    char *big = malloct(size);
219
0
    size_t pos = 0;
220
0
    int i;
221
222
0
    assertf(big != NULL);
223
0
    pos = (size_t) snprintf(big, size, "httrack");
224
0
    assertf(pos < size);
225
0
    for (i = 0; i < n; i++) {
226
      // snprintf returns what it wanted to write, so accumulating it blind
227
      // would let the next size argument wrap
228
0
      const int len = snprintf(big + pos, size - pos, " a%d", i);
229
230
0
      assertf(len > 0 && (size_t) len < size - pos);
231
0
      pos += (size_t) len;
232
0
    }
233
0
    args = hts_split_cmdline(big, &nargs);
234
0
    assertf(args != NULL && nargs == n + 1);
235
0
    assertf(args[nargs] == NULL);
236
0
    for (i = 0; i < n; i++) {
237
0
      char expect[16];
238
239
0
      snprintf(expect, sizeof(expect), "a%d", i);
240
0
      assertf(strcmp(args[i + 1], expect) == 0);
241
0
    }
242
0
    freet(args);
243
0
    freet(big);
244
0
  }
245
246
0
  printf("cmdline-split self-test OK\n");
247
0
  return 0;
248
0
}
249
250
0
static int st_copyopt(httrackp *opt, int argc, char **argv) {
251
0
  httrackp *from = hts_create_opt();
252
0
  httrackp *to = hts_create_opt();
253
0
  int err = 0;
254
255
0
  (void) opt;
256
0
  (void) argc;
257
0
  (void) argv;
258
  /* from-values differ from both the to-values and the hts_create_opt()
259
     defaults (nearlink FALSE, errpage/parseall TRUE), so a copy that no-ops or
260
     just resets to defaults is caught too, not only the unsigned-guard bug. */
261
0
  from->retry = 7; /* int field: positive control */
262
0
  to->retry = 0;
263
0
  from->nearlink = HTS_TRUE;
264
0
  to->nearlink = HTS_FALSE;
265
0
  from->errpage = HTS_FALSE;
266
0
  to->errpage = HTS_TRUE;
267
0
  from->parseall = HTS_FALSE;
268
0
  to->parseall = HTS_TRUE;
269
270
0
  copy_htsopt(from, to);
271
272
0
  if (to->retry != 7)
273
0
    err = 1;
274
0
  if (to->nearlink != HTS_TRUE)
275
0
    err = 1;
276
0
  if (to->errpage != HTS_FALSE)
277
0
    err = 1;
278
0
  if (to->parseall != HTS_FALSE)
279
0
    err = 1;
280
281
  /* HTS_DEFAULT (-1) is "unspecified": copy_htsopt must skip it, leaving the
282
     target intact. Only a signed (int-backed) field can hold -1, so this also
283
     guards the type against regressing to an unsigned hts_boolean. */
284
0
  from->parseall = HTS_DEFAULT;
285
0
  to->parseall = HTS_TRUE;
286
0
  copy_htsopt(from, to);
287
0
  if (to->parseall != HTS_TRUE)
288
0
    err = 1;
289
290
  /* String field: a non-empty source deep-copies across, an empty source
291
     leaves the target intact (StringNotEmpty guard). Covers the exported
292
     copy_htsopt String path that no crawl test reaches. */
293
0
  StringCopy(from->cookies_file, "/tmp/jar.txt");
294
0
  StringCopy(to->cookies_file, "");
295
0
  copy_htsopt(from, to);
296
0
  if (strcmp(StringBuff(to->cookies_file), "/tmp/jar.txt") != 0)
297
0
    err = 1;
298
0
  StringCopy(from->cookies_file, "");
299
0
  copy_htsopt(from, to);
300
0
  if (strcmp(StringBuff(to->cookies_file), "/tmp/jar.txt") != 0)
301
0
    err = 1;
302
303
  /* warc_file: same String deep-copy path as cookies_file */
304
0
  StringCopy(from->warc_file, "run.warc.gz");
305
0
  StringCopy(to->warc_file, "");
306
0
  copy_htsopt(from, to);
307
0
  if (strcmp(StringBuff(to->warc_file), "run.warc.gz") != 0)
308
0
    err = 1;
309
310
  /* accept_kinds deep-copies, and an explicit -%a or -%l carries its flag. */
311
0
  StringCopy(from->accept_kinds, "image=a/b");
312
0
  StringCopy(from->accept, "x/y");
313
0
  from->accept_given = HTS_TRUE;
314
0
  StringCopy(from->lang_iso, "fr");
315
0
  from->lang_given = HTS_TRUE;
316
0
  copy_htsopt(from, to);
317
0
  if (strcmp(StringBuff(to->accept_kinds), "image=a/b") != 0 ||
318
0
      strcmp(StringBuff(to->accept), "x/y") != 0 || !to->accept_given ||
319
0
      strcmp(StringBuff(to->lang_iso), "fr") != 0 || !to->lang_given)
320
0
    err = 1;
321
  /* A default -%a or -%l never overwrites an explicit one. */
322
0
  StringCopy(from->accept, HTS_ACCEPT_LEGACY);
323
0
  from->accept_given = HTS_FALSE;
324
0
  StringCopy(from->lang_iso, HTS_ACCEPT_LANGUAGE_LEGACY);
325
0
  from->lang_given = HTS_FALSE;
326
0
  copy_htsopt(from, to);
327
0
  if (strcmp(StringBuff(to->accept), "x/y") != 0 || !to->accept_given ||
328
0
      strcmp(StringBuff(to->lang_iso), "fr") != 0 || !to->lang_given)
329
0
    err = 1;
330
  /* An explicit -%a or -%l equal to the built-in value still overwrites. */
331
0
  from->accept_given = HTS_TRUE;
332
0
  from->lang_given = HTS_TRUE;
333
0
  copy_htsopt(from, to);
334
0
  if (strcmp(StringBuff(to->accept), HTS_ACCEPT_LEGACY) != 0 ||
335
0
      strcmp(StringBuff(to->lang_iso), HTS_ACCEPT_LANGUAGE_LEGACY) != 0)
336
0
    err = 1;
337
338
0
  from->changes = HTS_TRUE;
339
0
  to->changes = HTS_FALSE;
340
0
  copy_htsopt(from, to);
341
0
  if (to->changes != HTS_TRUE)
342
0
    err = 1;
343
344
  /* single_file pair: the cap is guarded by >0, so an unset source must not
345
     overwrite the default the target already carries */
346
0
  from->single_file = HTS_TRUE;
347
0
  from->single_file_max_size = 4096;
348
0
  to->single_file = HTS_FALSE;
349
0
  to->single_file_max_size = SINGLEFILE_DEFAULT_MAX_SIZE;
350
0
  copy_htsopt(from, to);
351
0
  if (!to->single_file || to->single_file_max_size != 4096)
352
0
    err = 1;
353
0
  from->single_file_max_size = 0;
354
0
  copy_htsopt(from, to);
355
0
  if (to->single_file_max_size != 4096)
356
0
    err = 1;
357
358
  /* sitemap pair: the flag latches on, the URL takes the String deep copy */
359
0
  from->sitemap = HTS_TRUE;
360
0
  StringCopy(from->sitemap_url, "http://h.test/sitemap.xml");
361
0
  to->sitemap = HTS_FALSE;
362
0
  StringCopy(to->sitemap_url, "");
363
0
  copy_htsopt(from, to);
364
0
  if (!to->sitemap ||
365
0
      strcmp(StringBuff(to->sitemap_url), "http://h.test/sitemap.xml") != 0)
366
0
    err = 1;
367
0
  from->sitemap = HTS_FALSE;
368
0
  StringCopy(from->sitemap_url, "");
369
0
  copy_htsopt(from, to);
370
0
  if (!to->sitemap ||
371
0
      strcmp(StringBuff(to->sitemap_url), "http://h.test/sitemap.xml") != 0)
372
0
    err = 1;
373
374
  /* #185 pause pair: copied when enabled (max>0), the 0 sentinel skips */
375
0
  from->pause_min_ms = 5000;
376
0
  from->pause_max_ms = 10000;
377
0
  to->pause_min_ms = to->pause_max_ms = 0;
378
0
  copy_htsopt(from, to);
379
0
  if (to->pause_min_ms != 5000 || to->pause_max_ms != 10000)
380
0
    err = 1;
381
0
  from->pause_min_ms = from->pause_max_ms = 0;
382
0
  copy_htsopt(from, to);
383
0
  if (to->pause_min_ms != 5000 || to->pause_max_ms != 10000)
384
0
    err = 1;
385
386
  /* max_retry_after: 0 is a real setting (no wait), so only a negative skips */
387
0
  from->max_retry_after = 0;
388
0
  to->max_retry_after = 60;
389
0
  copy_htsopt(from, to);
390
0
  if (to->max_retry_after != 0)
391
0
    err = 1;
392
0
  from->max_retry_after = -1;
393
0
  copy_htsopt(from, to);
394
0
  if (to->max_retry_after != 0)
395
0
    err = 1;
396
397
0
  hts_free_opt(from);
398
0
  hts_free_opt(to);
399
0
  printf("copy-htsopt: %s\n", err ? "FAIL" : "OK");
400
0
  return err;
401
0
}
402
403
/* A run with no log file must still count its errors (#1681). */
404
0
static int st_logcounters(httrackp *opt, int argc, char **argv) {
405
0
  httrackp *o = hts_create_opt();
406
0
  const hts_stat_struct *stats;
407
0
  int err = 0;
408
409
0
  (void) opt;
410
0
  (void) argc;
411
0
  (void) argv;
412
413
  /* -Q sets no log file and default verbosity. */
414
0
  o->log = NULL;
415
0
  o->errlog = NULL;
416
0
  o->debug = LOG_NOTICE;
417
418
0
  hts_log_print(o, LOG_ERROR, "first");
419
  /* LOG_ERRNO rides in the high bits and must not shift the level. */
420
0
  hts_log_print(o, LOG_ERROR | LOG_ERRNO, "second");
421
0
  hts_log_print(o, LOG_WARNING, "third");
422
0
  hts_log_print(o, LOG_WARNING, "fourth");
423
0
  hts_log_print(o, LOG_WARNING, "fifth");
424
0
  hts_log_print(o, LOG_NOTICE, "sixth");
425
426
  /* Three distinct totals, so a swapped counter cannot pass. */
427
0
  stats = hts_get_stats(o);
428
0
  if (stats == NULL || stats->stat_errors != 2 || stats->stat_warnings != 3 ||
429
0
      stats->stat_infos != 1)
430
0
    err = 1;
431
432
  /* Polling hts_get_stats() must not increment the count. */
433
0
  stats = hts_get_stats(o);
434
0
  if (stats == NULL || stats->stat_errors != 2)
435
0
    err = 1;
436
437
  /* LOG_INFO is above the default verbosity, so it is not counted. */
438
0
  hts_log_print(o, LOG_INFO, "seventh");
439
0
  stats = hts_get_stats(o);
440
0
  if (stats == NULL || stats->stat_infos != 1)
441
0
    err = 1;
442
443
  /* Raising the verbosity lets the same level through. */
444
0
  o->debug = LOG_INFO;
445
0
  hts_log_print(o, LOG_INFO, "eighth");
446
0
  stats = hts_get_stats(o);
447
0
  if (stats == NULL || stats->stat_infos != 2)
448
0
    err = 1;
449
450
  /* A panic is fatal, so it counts as an error rather than as nothing. */
451
0
  hts_log_print(o, LOG_PANIC, "ninth");
452
0
  stats = hts_get_stats(o);
453
0
  if (stats == NULL || stats->stat_errors != 3)
454
0
    err = 1;
455
456
0
  hts_free_opt(o);
457
0
  printf("log-counters: %s\n", err ? "FAIL" : "OK");
458
0
  return err;
459
0
}
460
461
/* The note is built twice from one buffer, so a caller reusing it must never
462
   read the previous mirror's text back out of a silent run. */
463
0
static int st_upperlinksnote(httrackp *opt, int argc, char **argv) {
464
0
  httrackp *from = hts_create_opt();
465
0
  httrackp *to = hts_create_opt();
466
0
  char note[256];
467
0
  int err = 0;
468
469
0
  (void) opt;
470
0
  (void) argc;
471
0
  (void) argv;
472
473
0
  strcpybuff(note, "stale");
474
0
  if (hts_upper_links_note(to, note, sizeof(note)) || note[0] != '\0')
475
0
    err = 1; /* a fresh opt refused nothing, and must empty the buffer */
476
477
0
  to->upper_links_refused = HTS_TRUE;
478
0
  if (!hts_upper_links_note(to, note, sizeof(note)) ||
479
0
      strstr(note, "-B") == NULL)
480
0
    err = 1; /* the note must name the option that lifts the refusal */
481
482
0
  to->upper_links_refused = HTS_FALSE;
483
0
  if (hts_upper_links_note(to, note, sizeof(note)) || note[0] != '\0')
484
0
    err = 1;
485
486
  /* the flag never travels onto another opt */
487
0
  from->upper_links_refused = HTS_TRUE;
488
0
  copy_htsopt(from, to);
489
0
  if (to->upper_links_refused)
490
0
    err = 1;
491
492
0
  hts_free_opt(from);
493
0
  hts_free_opt(to);
494
0
  printf("upper-links-note: %s\n", err ? "FAIL" : "OK");
495
0
  return err;
496
0
}
497
498
/* The handler below pins the enumerator NAME; these pin the NUMBERS, which are
499
   ABI in the installed htsopt.h and are what -C prints. */
500
HTS_STATIC_ASSERT(HTS_CACHE_NONE == 0, cache_none_is_0);
501
HTS_STATIC_ASSERT(HTS_CACHE_PRIORITY == 1, cache_priority_is_1);
502
HTS_STATIC_ASSERT(HTS_CACHE_TEST_UPDATE == 2, cache_test_update_is_2);
503
504
/* -C's own default (no -C given) is C1 cache-priority, not the C2 test-update
505
   the man page and this enum's comment claimed for a long time. */
506
0
static int st_cachedefault(httrackp *opt, int argc, char **argv) {
507
0
  httrackp *const fresh = hts_create_opt();
508
0
  int err = 0;
509
510
0
  (void) opt;
511
0
  (void) argc;
512
0
  (void) argv;
513
0
  if (fresh->cache != HTS_CACHE_PRIORITY)
514
0
    err = 1;
515
0
  hts_free_opt(fresh);
516
0
  printf("cache-default: %s\n", err ? "FAIL" : "OK");
517
0
  return err;
518
0
}
519
520
static char st_log_callback_seen[256];
521
522
static HTS_PRINTF_FUN(3, 0) void st_log_callback(httrackp *opt, int type,
523
                                                 const char *format,
524
0
                                                 va_list args) {
525
0
  (void) opt;
526
0
  (void) type;
527
0
  (void) vsnprintf(st_log_callback_seen, sizeof(st_log_callback_seen), format,
528
0
                   args);
529
0
}
530
531
/* The callback must not consume the va_list the log file's vfprintf() needs. */
532
0
static int st_logcallback(httrackp *opt, int argc, char **argv) {
533
0
  static const char want[] = "42 sentinel";
534
0
  static const char want_filtered[] = "7 filtered";
535
0
  char BIGSTK seen[sizeof(st_log_callback_seen)];
536
0
  char BIGSTK line[256];
537
0
  FILE *fp;
538
0
  int rc = 1;
539
540
0
  (void) argc;
541
0
  (void) argv;
542
543
0
  fp = tmpfile();
544
0
  if (fp == NULL) {
545
0
    fprintf(stderr, "logcallback: tmpfile() failed\n");
546
0
    return 1;
547
0
  }
548
0
  opt->log = fp;
549
0
  opt->debug = LOG_NOTICE;
550
0
  st_log_callback_seen[0] = '\0';
551
0
  hts_set_log_vprint_callback(st_log_callback);
552
0
  hts_log_print(opt, LOG_NOTICE, "%d %s", 42, "sentinel");
553
0
  hts_set_log_vprint_callback(NULL);
554
0
  opt->log = NULL;
555
0
  strcpybuff(seen, st_log_callback_seen);
556
557
0
  rewind(fp);
558
0
  if (fgets(line, (int) sizeof(line), fp) == NULL) {
559
0
    fprintf(stderr, "logcallback: log file is empty, nothing was written\n");
560
0
    fclose(fp);
561
0
    return 1;
562
0
  }
563
0
  fclose(fp);
564
565
  /* The callback runs above the level filter and without a log file at all;
566
     the front-ends that install one usually have no opt->log open. */
567
0
  st_log_callback_seen[0] = '\0';
568
0
  hts_set_log_vprint_callback(st_log_callback);
569
0
  hts_log_print(opt, LOG_DEBUG, "%d %s", 7, "filtered");
570
0
  hts_set_log_vprint_callback(NULL);
571
572
  /* Same arguments both ways; the file line carries a level prefix. */
573
0
  if (strcmp(seen, want) != 0)
574
0
    fprintf(stderr, "logcallback: callback got '%s' want '%s'\n", seen, want);
575
0
  else if (strstr(line, want) == NULL)
576
0
    fprintf(stderr, "logcallback: log file got '%s' want it to carry '%s'\n",
577
0
            line, want);
578
0
  else if (strcmp(st_log_callback_seen, want_filtered) != 0)
579
0
    fprintf(stderr, "logcallback: unfiltered callback got '%s' want '%s'\n",
580
0
            st_log_callback_seen, want_filtered);
581
0
  else
582
0
    rc = 0;
583
584
0
  if (rc == 0)
585
0
    printf("logcallback self-test OK\n");
586
0
  return rc;
587
0
}
588
589
#ifdef HTS_CRASH_TEST
590
static char st_crash_seen[256];
591
static int st_crash_level;
592
static hts_boolean st_crash_had_opt;
593
594
static HTS_PRINTF_FUN(3, 0) void st_crash_log(httrackp *opt, int type,
595
                                              const char *format,
596
0
                                              va_list args) {
597
0
  st_crash_had_opt = opt != NULL ? HTS_TRUE : HTS_FALSE;
598
0
  st_crash_level = type;
599
0
  (void) vsnprintf(st_crash_seen, sizeof(st_crash_seen), format, args);
600
0
}
601
602
static FILE *st_crash_fp;
603
604
static HTS_PRINTF_FUN(3, 0) void st_crash_file_log(httrackp *opt, int type,
605
                                                   const char *format,
606
0
                                                   va_list args) {
607
0
  (void) opt;
608
0
  (void) type;
609
0
  vfprintf(st_crash_fp, format, args);
610
0
  fputc('\n', st_crash_fp);
611
  /* Flushed line by line, because the caller below is about to fault. */
612
0
  fflush(st_crash_fp);
613
0
}
614
615
/* See htscrashtest.h for why both channels carry the marker. Given a file, arm
616
   a real -#c fault and write what the log callback receives there: that run
617
   crashes, which is the path httrack-android recovers. */
618
0
static int st_crashannounce(httrackp *opt, int argc, char **argv) {
619
0
  static const char want[] = "crash announce 42";
620
0
  int rc = 1;
621
622
0
  (void) opt;
623
624
0
  if (argc >= 1) {
625
0
    st_crash_fp = fopen(argv[0], "wb");
626
0
    if (st_crash_fp == NULL) {
627
0
      fprintf(stderr, "crashannounce: cannot write %s\n", argv[0]);
628
0
      return 1;
629
0
    }
630
0
    hts_set_log_vprint_callback(st_crash_file_log);
631
0
    (void) hts_crash_test("dnssegv");
632
0
    hts_crash_test_worker(HTS_CRASH_WORKER_DNS);
633
    /* Only a handler that swallowed the fault gets here, so hand the log
634
       channel back rather than funnelling a later case into this file. */
635
0
    hts_set_log_vprint_callback(NULL);
636
0
    fclose(st_crash_fp);
637
0
    st_crash_fp = NULL;
638
0
    fprintf(stderr, "crashannounce: the armed fault did not crash\n");
639
0
    return 1;
640
0
  }
641
642
0
  hts_set_log_vprint_callback(st_crash_log);
643
  /* The engine must not need an opt to reach the front end here. */
644
0
  hts_crash_test_announce("crash announce %d", 42);
645
0
  hts_set_log_vprint_callback(NULL);
646
647
0
  if (strcmp(st_crash_seen, want) != 0)
648
0
    fprintf(stderr, "crashannounce: callback got '%s' want '%s'\n",
649
0
            st_crash_seen, want);
650
0
  else if (st_crash_level != LOG_ERROR)
651
0
    fprintf(stderr, "crashannounce: logged at level %d, want %d (LOG_ERROR)\n",
652
0
            st_crash_level, (int) LOG_ERROR);
653
0
  else if (st_crash_had_opt)
654
0
    fprintf(stderr, "crashannounce: the callback was handed an opt\n");
655
0
  else
656
0
    rc = 0;
657
658
0
  if (rc == 0)
659
0
    printf("crashannounce self-test OK\n");
660
0
  return rc;
661
0
}
662
#endif
663
664
/* The short form optalias_check() emits for the option words, both joined by a
665
   space when it returns two, or NULL when it refuses them; *used counts the
666
   words consumed and warn takes the message an accepted option still drew. */
667
static const char *st_optalias_expand(char *dest, size_t dest_size,
668
                                      const char *word, const char *next,
669
0
                                      int *used, char *warn, size_t warn_size) {
670
0
  char BIGSTK out[2][HTS_CDLMAXSIZE];
671
0
  char *outv[2] = {out[0], out[1]};
672
0
  const char *argv[2];
673
0
  char error[256];
674
0
  const int argc = next != NULL ? 2 : 1;
675
0
  int outc = 0;
676
677
0
  argv[0] = word;
678
0
  argv[1] = next;
679
0
  out[0][0] = out[1][0] = dest[0] = warn[0] = '\0';
680
0
  *used = optalias_check(argc, argv, 0, &outc, outv, sizeof(out[0]), error,
681
0
                         sizeof(error));
682
0
  if (*used == 0) {
683
0
    assertf(error[0] != '\0'); /* a refusal has to say why */
684
0
    return NULL;
685
0
  }
686
0
  strlcpybuff(warn, error, warn_size);
687
0
  assertf(outc >= 1 && outc <= 2);
688
0
  strlcpybuff(dest, out[0], dest_size);
689
0
  if (outc == 2) {
690
0
    strlcatbuff(dest, " ", dest_size);
691
0
    strlcatbuff(dest, out[1], dest_size);
692
0
  }
693
0
  return dest;
694
0
}
695
696
/* Long-option value handling (#1195): a value the option's class did not take
697
   was dropped, so --index=0 read back as the enabling bare --index. */
698
0
static int st_optalias(httrackp *opt, int argc, char **argv) {
699
0
  char got[HTS_CDLMAXSIZE * 2], warn[256];
700
0
  int i, used, params = 0;
701
702
0
  (void) opt;
703
  /* -list gives 282 the whole table: the rows to try against the engine's
704
     own parser, and the class of every name the wizard writes a value to */
705
0
  if (argc == 1 && strcmp(argv[0], "-list") == 0) {
706
0
    for (i = 0; optalias_value(i)[0] != '\0'; i++)
707
0
      printf("%s %s\n", opttype_value(i), optalias_value(i));
708
0
    return 0;
709
0
  }
710
0
  if (argc >= 1) {
711
0
    const char *const out = st_optalias_expand(got, sizeof(got), argv[0],
712
0
                                               argc >= 2 ? argv[1] : NULL,
713
0
                                               &used, warn, sizeof(warn));
714
715
0
    printf("%s\n", out != NULL ? out : "(refused)");
716
0
    return out != NULL ? 0 : 1;
717
0
  }
718
0
#define EXPANDS(want, word, next)                                              \
719
0
  do {                                                                         \
720
0
    const char *const out__ = st_optalias_expand(                              \
721
0
        got, sizeof(got), (word), (next), &used, warn, sizeof(warn));          \
722
0
    assertf(out__ != NULL && strcmp(out__, (want)) == 0);                      \
723
0
    assertf(warn[0] == '\0');                                                  \
724
0
  } while (0)
725
/* accepted, expanding to WANT, but drawing a warning on the way */
726
0
#define WARNS(want, word, next)                                                \
727
0
  do {                                                                         \
728
0
    const char *const out__ = st_optalias_expand(                              \
729
0
        got, sizeof(got), (word), (next), &used, warn, sizeof(warn));          \
730
0
    assertf(out__ != NULL && strcmp(out__, (want)) == 0);                      \
731
0
    assertf(warn[0] != '\0');                                                  \
732
0
  } while (0)
733
0
#define REFUSES(word, next)                                                    \
734
0
  assertf(st_optalias_expand(got, sizeof(got), (word), (next), &used, warn,    \
735
0
                             sizeof(warn)) == NULL)
736
737
  /* -I0 has always disabled the index; now the long form can say it too */
738
0
  EXPANDS("-I0", "--index=0", NULL);
739
0
  EXPANDS("-I0", "--index=off", NULL);
740
0
  EXPANDS("-I0", "--noindex", NULL);
741
0
  EXPANDS("-I", "--index=1", NULL);
742
0
  EXPANDS("-I", "--index=on", NULL);
743
  /* detached, as a config file writes it ("index off"), and never a URL */
744
0
  EXPANDS("-I0", "--index", "off");
745
0
  assertf(used == 2);
746
0
  EXPANDS("-I", "--index", "http://foo/");
747
0
  assertf(used == 1);
748
0
  EXPANDS("-I", "--index", NULL);
749
0
  assertf(used == 1);
750
  /* -I takes 0 alone, so an out-of-range value is an error, not a bare -I */
751
0
  REFUSES("--index=2", NULL);
752
0
  REFUSES("--index=yes", NULL);
753
754
  /* a numeric level reaches the short form whole */
755
0
  EXPANDS("-%I0", "--search-index=0", NULL);
756
0
  EXPANDS("-%I2", "--search-index=2", NULL);
757
0
  EXPANDS("-%v2", "--display=2", NULL);
758
0
  EXPANDS("-%N0", "--delayed-type-check=0", NULL);
759
0
  EXPANDS("-o0", "--generate-errors=0", NULL);
760
0
  REFUSES("--display=full", NULL);
761
0
  REFUSES("--display=99999999999999999999", NULL);
762
763
  /* three rows were "param", a class that demands a following token: the URL
764
     after --purge-old became its value, and =1 died on "invalid option 1" */
765
0
  EXPANDS("-X", "--purge-old", NULL);
766
0
  EXPANDS("-X", "--purge-old", "http://foo/");
767
0
  assertf(used == 1);
768
0
  EXPANDS("-X0", "--purge-old=0", NULL);
769
0
  EXPANDS("-X", "--purge-old=1", NULL);
770
0
  EXPANDS("-X0", "--purge-old", "0");
771
0
  assertf(used == 2);
772
0
  EXPANDS("-X", "--purge-old", "1");
773
0
  EXPANDS("-X0", "--nopurge-old", NULL);
774
0
  EXPANDS("-%f0", "--httpproxy-ftp=0", NULL);
775
0
  EXPANDS("-%f", "--httpproxy-ftp=1", NULL);
776
0
  EXPANDS("-%P0", "--extended-parsing=0", NULL);
777
0
  EXPANDS("-%P", "--extended-parsing=1", NULL);
778
779
  /* a flag that takes no value refuses one rather than dropping it */
780
0
  REFUSES("--mirror=0", NULL);
781
0
  REFUSES("--nomirror", NULL);
782
0
  REFUSES("--warc=0", NULL);
783
0
  REFUSES("--single-file=s", NULL);
784
  /* ...and so does a compound alias, whose value would land in the cluster */
785
0
  REFUSES("--spider=0", NULL);
786
787
  /* an alias whose own name begins with "no" is not a negation */
788
0
  EXPANDS("-%T0", "--no-utf8-conversion", NULL);
789
0
  EXPANDS("-y0", "--no-background-on-suspend", NULL);
790
0
  EXPANDS("-%T", "--utf8-conversion", NULL);
791
792
  /* the --wide-/--tiny- prefix glues the --wide/--tiny connection count onto
793
     the plain clusters, and is refused wherever the glue would be misread */
794
0
  EXPANDS("-wc32", "--wide-mirror", NULL);
795
0
  assertf(used == 1);
796
0
  EXPANDS("-wc1", "--tiny-mirror", NULL);
797
0
  EXPANDS("-p0C0I0tc32", "--wide-spider", NULL);
798
0
  EXPANDS("-qgc1", "--tiny-get", NULL);
799
0
  EXPANDS("-Xc32", "--wide-purge-old", NULL);
800
0
  EXPANDS("-o2c1", "--tiny-generate-errors", "2");
801
0
  assertf(used == 2);
802
  /* elsewhere the alias applies without the count, and says so */
803
0
  WARNS("-O /tmp", "--wide-path", "/tmp"); /* the value is a word of its own */
804
0
  WARNS("+*.gif", "--tiny-allow", "*.gif");
805
0
  WARNS("--clean", "--wide-clean", NULL); /* a long form */
806
0
  WARNS("-c8", "--wide-sockets", "8");    /* -c8 already: 8 or 32? */
807
0
  WARNS("-c32", "--tiny-wide", NULL);     /* -c32 already: 32 or 1? */
808
0
  WARNS("-N1", "--wide-structure", "1");  /* -N takes a template */
809
0
  WARNS("-%r", "--wide-warc", NULL);      /* -%rc is --warc-cdx, not -%r -c */
810
0
  WARNS("-#h", "--wide-version", NULL);   /* -#h is matched as a whole word */
811
0
  WARNS("-h", "--tiny-help", NULL);
812
813
  /* the value-taking classes are untouched */
814
0
  EXPANDS("-C0", "--cache=0", NULL);
815
0
  EXPANDS("-C0", "--nocache", NULL);
816
0
  EXPANDS("-C2", "--cache", "2");
817
0
  EXPANDS("-P proxy:8080", "--proxy", "proxy:8080");
818
0
  EXPANDS("+*.gif", "--allow", "*.gif");
819
820
  /* #1426, both directions and table-wide over the "param" rows */
821
0
  for (i = 0; optalias_value(i)[0] != '\0'; i++) {
822
0
    const int p = optalias_find(optalias_value(i));
823
0
    char word[HTS_CDLMAXSIZE], want[HTS_CDLMAXSIZE];
824
825
0
    if (strcmp(opttype_value(p), "param") != 0)
826
0
      continue;
827
0
    params++;
828
0
    snprintf(word, sizeof(word), "--%s=yes", optalias_value(i));
829
0
    REFUSES(word, NULL);
830
0
    snprintf(word, sizeof(word), "--%s=none", optalias_value(i));
831
0
    REFUSES(word, NULL);
832
0
    snprintf(word, sizeof(word), "--%s=OFF", optalias_value(i));
833
0
    REFUSES(word, NULL);
834
0
    snprintf(word, sizeof(word), "--%s", optalias_value(i));
835
0
    REFUSES(word, "http://foo/");
836
    /* control: a number, and the on/off mapping, still pass in silence */
837
0
    snprintf(word, sizeof(word), "--%s=2", optalias_value(i));
838
0
    snprintf(want, sizeof(want), "%s2", optreal_value(p));
839
0
    EXPANDS(want, word, NULL);
840
0
    snprintf(word, sizeof(word), "--%s=off", optalias_value(i));
841
0
    snprintf(want, sizeof(want), "%s0", optreal_value(p));
842
0
    EXPANDS(want, word, NULL);
843
0
    snprintf(word, sizeof(word), "--%s=on", optalias_value(i));
844
0
    EXPANDS(optreal_value(p), word, NULL);
845
    /* #1437, table-wide too: the bound is the destination's magnitude, so
846
       padding rides through and a run past every destination does not */
847
0
    snprintf(word, sizeof(word), "--%s=00000000000000000002",
848
0
             optalias_value(i));
849
0
    snprintf(want, sizeof(want), "%s00000000000000000002", optreal_value(p));
850
0
    EXPANDS(want, word, NULL);
851
0
    snprintf(word, sizeof(word), "--%s=2147483647", optalias_value(i));
852
0
    snprintf(want, sizeof(want), "%s2147483647", optreal_value(p));
853
0
    EXPANDS(want, word, NULL);
854
0
    snprintf(word, sizeof(word), "--%s=99999999999999999999",
855
0
             optalias_value(i));
856
0
    REFUSES(word, NULL);
857
    /* a sign is not a digit run: -2 would spill into the cluster loop */
858
0
    snprintf(word, sizeof(word), "--%s=-2", optalias_value(i));
859
0
    REFUSES(word, NULL);
860
0
    snprintf(word, sizeof(word), "--%s=+2", optalias_value(i));
861
0
    REFUSES(word, NULL);
862
0
  }
863
0
  assertf(params >= 27); /* the rows the issue enumerates were all walked */
864
865
  /* the separator -m and -%c read besides digits, anchored to one occurrence
866
     with a digit after it: 1.2.3 otherwise reached maxconn as 1.2 */
867
0
  EXPANDS("-m,5000", "--max-files=,5000", NULL);
868
0
  EXPANDS("-m100,5000", "--max-files=100,5000", NULL);
869
0
  EXPANDS("-%c0.5", "--connection-per-second=0.5", NULL);
870
0
  EXPANDS("-%c.5", "--connection-per-second=.5", NULL);
871
0
  REFUSES("--connection-per-second=1.2.3", NULL);
872
0
  REFUSES("--connection-per-second=0.", NULL);
873
0
  REFUSES("--max-files=,", NULL);
874
0
  REFUSES("--max-files=100,", NULL);
875
0
  REFUSES("--max-files=,,,5", NULL);
876
0
  REFUSES("--max-files=1,2,3", NULL);
877
0
  REFUSES("--sockets=8,4", NULL);
878
0
  REFUSES("--sockets=0.5", NULL);
879
0
  REFUSES("--advanced-maxlinks=99999999999999999999", NULL);
880
  /* --sockets=8I0 mirrored without a top index: the tail reached -I0 */
881
0
  REFUSES("--sockets=8I0", NULL);
882
  /* an empty value is the bare short form, as it has always been */
883
0
  EXPANDS("-c", "--sockets=", NULL);
884
885
  /* --structure glues a preset and detaches a template (#1380): the engine
886
     reads a detached -N value as a user template whatever it holds */
887
0
  EXPANDS("-N1", "--structure=1", NULL);
888
0
  EXPANDS("-N100", "--structure=100", NULL);
889
0
  EXPANDS("-N1", "--structure", "1");
890
0
  assertf(used == 2);
891
0
  EXPANDS("-N %h%p/%n%q.%t", "--structure=%h%p/%n%q.%t", NULL);
892
0
  assertf(used == 1);
893
0
  EXPANDS("-N %h%p/%n%q.%t", "--structure", "%h%p/%n%q.%t");
894
0
  assertf(used == 2);
895
0
  EXPANDS("-N %h%p/%n%q.%t", "--user-structure", "%h%p/%n%q.%t");
896
  /* an empty value is -N "", which the engine reads as "back to the default" */
897
0
  EXPANDS("-N ", "--structure=", NULL);
898
  /* a preset trailed by more short options is one value asking for two, so it
899
     is refused whatever the tail holds; a % in it buys no template either */
900
0
  REFUSES("--structure=1L0", NULL);
901
0
  REFUSES("--structure", "1L0");
902
0
  REFUSES("--structure=1c8", NULL);
903
0
  REFUSES("--structure=1%c8", NULL);
904
0
  REFUSES("--structure=8I0", NULL);
905
  /* on/off are whole values rather than tails, and still map onto the preset */
906
0
  EXPANDS("-N0", "--structure=off", NULL);
907
  /* both name the default preset: a bare -N would take the next word, and
908
     with a URL there it mirrored nothing (#1434) */
909
0
  EXPANDS("-N0", "--structure=on", NULL);
910
0
  EXPANDS("-N0", "--structure", "on");
911
0
  assertf(used == 2);
912
  /* a template may open with a digit, so the leading digits do not decide it */
913
0
  EXPANDS("-N 2col/%n.%t", "--structure=2col/%n.%t", NULL);
914
0
  EXPANDS("-N 2col/%n.%t", "--structure", "2col/%n.%t");
915
  /* past 9 digits sscanf("%d") wraps onto -1, and with no % to make it a
916
     template either the value has nowhere left to go */
917
0
  EXPANDS("-N999999999", "--structure=999999999", NULL);
918
0
  REFUSES("--structure=4294967295", NULL);
919
  /* leading zeros are not part of the number: this is the preset 1 */
920
0
  EXPANDS("-N0000000001", "--structure=0000000001", NULL);
921
0
  EXPANDS("-N0000000001", "-N", "0000000001");
922
0
  assertf(used == 2);
923
  /* a %-free value would map every URL onto one name, so it is a typo */
924
0
  REFUSES("--structure=OFF", NULL);
925
0
  REFUSES("--structure=flat", NULL);
926
0
  REFUSES("--structure", "none");
927
0
  REFUSES("--structure=-1", NULL);
928
  /* --user-structure is how to ask for such a value on purpose */
929
0
  EXPANDS("-N flat", "--user-structure", "flat");
930
  /* the short form agrees, rather than reading -N 1 as a template named 1 */
931
0
  EXPANDS("-N1", "-N", "1");
932
0
  assertf(used == 2);
933
0
  EXPANDS("-N %h%p/%n%q.%t", "-N", "%h%p/%n%q.%t");
934
0
  assertf(used == 2);
935
  /* and there a bare digit run is the only thing it glues: 1L0 and an overlong
936
     run stay templates, as they were before the class existed */
937
0
  EXPANDS("-N 2col/%n.%t", "-N", "2col/%n.%t");
938
0
  assertf(used == 2);
939
0
  EXPANDS("-N 4294967295", "-N", "4294967295");
940
0
  assertf(used == 2);
941
0
  EXPANDS("-N 1L0", "-N", "1L0");
942
0
  assertf(used == 2);
943
  /* an empty word is still the template, not a URL */
944
0
  EXPANDS("-N ", "-N", "");
945
0
  assertf(used == 2);
946
  /* the parse loop takes no template opening with '-', nor a missing one */
947
0
  EXPANDS("-N", "-N", "-r1");
948
0
  assertf(used == 1);
949
0
  EXPANDS("-N", "-N", NULL);
950
0
  assertf(used == 1);
951
952
  /* a -# row means the arm its name and help name, not a neighbour: -#C lists
953
     the cache where -#E extracts its meta-data, -#T logs transfer ops where
954
     -#t is the autocheck, and -#0 went with the single-letter tests (#427) */
955
0
  EXPANDS("-#E", "--extract-cache", NULL);
956
0
  EXPANDS("-#C *.gif", "--debug-cache", "*.gif");
957
0
  EXPANDS("-#t", "--autotest", NULL);
958
0
  EXPANDS("-#T", "--debug-xfrstats", NULL);
959
0
  REFUSES("--debug-testfilters", NULL);
960
0
  REFUSES("--extract-cache=1", NULL);
961
  /* and the reverse lookup the generated help prints takes the first row */
962
0
  assertf(strcmp(optalias_value(optreal_find("-#E")), "extract-cache") == 0);
963
0
  assertf(strcmp(optalias_value(optreal_find("-#C")), "debug-cache") == 0);
964
0
  assertf(strcmp(optalias_value(optreal_find("-#t")), "autotest") == 0);
965
0
  assertf(strcmp(optalias_value(optreal_find("-#T")), "debug-xfrstats") == 0);
966
967
  /* invariant: every name's bare long form still emits its short form, and a
968
     param name still demands its own value. A duplicate name (test, continue)
969
     resolves to its first row */
970
0
  for (i = 0; optalias_value(i)[0] != '\0'; i++) {
971
0
    const int p = optalias_find(optalias_value(i));
972
0
    char word[HTS_CDLMAXSIZE];
973
974
0
    assertf(p >= 0);
975
0
    snprintf(word, sizeof(word), "--%s", optalias_value(i));
976
0
    if (strncmp(opttype_value(p), "param", 5) == 0) {
977
0
      REFUSES(word, NULL);
978
0
    } else {
979
0
      EXPANDS(optreal_value(p), word, NULL);
980
0
      assertf(used == 1);
981
0
    }
982
0
  }
983
0
  assertf(i > 100); /* the table was walked, not skipped */
984
0
#undef EXPANDS
985
0
#undef WARNS
986
0
#undef REFUSES
987
988
0
  printf("optalias self-test OK\n");
989
0
  return 0;
990
0
}
991
992
/* Runs hts_scan_token() into an arena of destsize bytes poisoned past its end,
993
   and reports whether anything past that moved. */
994
static hts_boolean st_scantoken_run(char *src, size_t destsize, char *arena,
995
                                    size_t guard, hts_boolean *fit,
996
0
                                    size_t *advance) {
997
0
  char *p = src;
998
0
  size_t i;
999
1000
  /* poisoned with '#', not 0, or the stray NUL of an off-by-one would read as
1001
     untouched */
1002
0
  memset(arena, '#', destsize + guard);
1003
0
  *fit = hts_scan_token(&p, arena, destsize);
1004
0
  *advance = (size_t) (p - src);
1005
0
  for (i = destsize; i < destsize + guard; i++) {
1006
0
    if (arena[i] != '#')
1007
0
      return HTS_FALSE;
1008
0
  }
1009
0
  return HTS_TRUE;
1010
0
}
1011
1012
/* A token longer than the buffer it is copied into must be refused, never
1013
   written past the end (#1271). */
1014
0
static int st_scantoken(httrackp *opt, int argc, char **argv) {
1015
  /* the engine's own filter bound, and a small one the same code must honour */
1016
0
  const size_t caps[] = {8, HTS_URLMAXSIZE * 2 - 1};
1017
0
  const size_t guard = 64;
1018
0
  size_t c;
1019
1020
0
  (void) opt;
1021
0
  (void) argc;
1022
0
  (void) argv;
1023
0
  for (c = 0; c < sizeof(caps) / sizeof(caps[0]); c++) {
1024
0
    const size_t destsize = caps[c];
1025
0
    const size_t room = destsize - 1; /* characters, the NUL aside */
1026
    /* empty, short, and the four lengths that straddle the bound */
1027
0
    const size_t lens[] = {0, 1, room - 1, room, room + 1, room + guard};
1028
0
    char *arena = malloct(destsize + guard);
1029
0
    char *src = malloct(room + guard + 8);
1030
0
    size_t k;
1031
1032
0
    assertf(arena != NULL && src != NULL);
1033
0
    for (k = 0; k < sizeof(lens) / sizeof(lens[0]); k++) {
1034
0
      const size_t len = lens[k];
1035
0
      const size_t kept = len < room ? len : room;
1036
0
      hts_boolean fit;
1037
0
      size_t advance, i;
1038
1039
      /* the token, the whitespace run after it, and the token to land on. Its
1040
         bytes vary, or a copy landing on the wrong index would still match */
1041
0
      for (i = 0; i < len; i++)
1042
0
        src[i] = (char) ('a' + i % 26);
1043
0
      memcpy(src + len, " \tb", 4);
1044
0
      assertf(st_scantoken_run(src, destsize, arena, guard, &fit, &advance));
1045
0
      assertf(fit == (len <= room ? HTS_TRUE : HTS_FALSE));
1046
0
      assertf(strlen(arena) == kept && memcmp(arena, src, kept) == 0);
1047
0
      assertf(advance == len + 2);
1048
1049
      /* the same token ending the string: the cursor stops on its NUL */
1050
0
      src[len] = '\0';
1051
0
      assertf(st_scantoken_run(src, destsize, arena, guard, &fit, &advance));
1052
0
      assertf(fit == (len <= room ? HTS_TRUE : HTS_FALSE));
1053
0
      assertf(strlen(arena) == kept && memcmp(arena, src, kept) == 0);
1054
0
      assertf(advance == len);
1055
0
    }
1056
0
    freet(arena);
1057
0
    freet(src);
1058
0
  }
1059
0
  printf("scantoken self-test OK\n");
1060
0
  return 0;
1061
0
}
1062
1063
/* One http_postfile_body() case, run into a request block followed by a
1064
   poisoned guard. A missing terminator shows up as a returned position that
1065
   walked into the poison instead of stopping inside the block. */
1066
static int st_postfile_case(const char *path, size_t start, size_t expect,
1067
0
                            const char *what) {
1068
0
  enum { capacity = 64, guard = 16 };
1069
1070
0
  char *arena = malloct(capacity + guard);
1071
0
  FILE *fp = FOPEN(path, "rb");
1072
0
  size_t pos;
1073
0
  size_t i;
1074
0
  int err = 0;
1075
1076
0
  if (arena == NULL || fp == NULL) {
1077
0
    printf("  FAIL %s: cannot open %s\n", what, path);
1078
0
    freet(arena);
1079
0
    if (fp != NULL)
1080
0
      fclose(fp);
1081
0
    return 1;
1082
0
  }
1083
0
  memset(arena, '#', capacity + guard);
1084
0
  arena[capacity + guard - 1] = '\0'; /* a runaway strlen ends here, not past */
1085
0
  memset(arena, 'R', start);          /* what the request line already wrote */
1086
0
  arena[start] = '\0';
1087
1088
0
  pos = http_postfile_body(arena, capacity, start, fp);
1089
0
  fclose(fp);
1090
1091
0
  if (pos != expect) {
1092
0
    printf("  FAIL %s: position %d, expected %d\n", what, (int) pos,
1093
0
           (int) expect);
1094
0
    err = 1;
1095
0
  } else if (arena[pos] != '\0') {
1096
0
    printf("  FAIL %s: no terminator at %d\n", what, (int) pos);
1097
0
    err = 1;
1098
0
  }
1099
0
  for (i = capacity; i + 1 < capacity + guard; i++) {
1100
0
    if (arena[i] != '#') {
1101
0
      printf("  FAIL %s: guard byte %d written\n", what, (int) (i - capacity));
1102
0
      err = 1;
1103
0
      break;
1104
0
    }
1105
0
  }
1106
0
  freet(arena);
1107
0
  return err;
1108
0
}
1109
1110
/* >postfile: bodies, read raw into the request block. Fixture paths in argv:
1111
   shorter than the room left, longer than it, one holding a NUL, and empty. */
1112
0
static int st_postfile(httrackp *opt, int argc, char **argv) {
1113
0
  int err = 0;
1114
1115
0
  (void) opt;
1116
0
  if (argc < 4) {
1117
0
    printf("usage: -#test=postfile <short> <long> <embedded-nul> <empty>\n");
1118
0
    return 1;
1119
0
  }
1120
  /* room left is 64 - 8 - 1 = 55 bytes */
1121
0
  err |= st_postfile_case(argv[0], 8, 8 + 3, "short");
1122
0
  err |= st_postfile_case(argv[1], 8, 8 + 55, "long");
1123
0
  err |= st_postfile_case(argv[2], 8, 8 + 2, "embedded-nul");
1124
0
  err |= st_postfile_case(argv[3], 8, 8, "empty");
1125
1126
0
  printf("postfile self-test: %s\n", err ? "FAIL" : "OK");
1127
0
  return err;
1128
0
}
1129
1130
// -#test=abortlock|stoplock <dir>: hts_take_lock_request() takes a request only
1131
// when it is stamped after this run's hts-in_progress.lock and the engine could
1132
// also delete it. It leaves every other lock file alone.
1133
0
static hts_boolean st_lockrule_touch(const char *path) {
1134
0
  FILE *const fp = FOPEN(path, "wb");
1135
1136
0
  if (fp == NULL)
1137
0
    return HTS_FALSE;
1138
0
  fclose(fp);
1139
0
  return HTS_TRUE;
1140
0
}
1141
1142
0
static hts_boolean st_lockrule_stamp(const char *path, time_t when) {
1143
0
  STRUCT_UTIMBUF times;
1144
1145
0
  times.actime = times.modtime = when;
1146
0
  return UTIME(path, &times) == 0 ? HTS_TRUE : HTS_FALSE;
1147
0
}
1148
1149
/* Stamp PATH at SEC plus NSEC, both in the units hts_file_mtime() reports, so
1150
   the caller reads SEC back rather than taking it from a clock. False on
1151
   failure, and NSEC is dropped where this build has no sub-second call. */
1152
static hts_boolean st_lockrule_stamp_ns(const char *path, int64_t sec,
1153
0
                                        int32_t nsec) {
1154
#if defined(_WIN32)
1155
  LPWSTR wpath = hts_pathToUCS2(path);
1156
  ULARGE_INTEGER ticks;
1157
  FILETIME ft;
1158
  HANDLE h;
1159
  hts_boolean ok;
1160
1161
  if (wpath == NULL)
1162
    return HTS_FALSE;
1163
  h = CreateFileW(wpath, FILE_WRITE_ATTRIBUTES,
1164
                  FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
1165
                  OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
1166
  freet(wpath);
1167
  if (h == INVALID_HANDLE_VALUE)
1168
    return HTS_FALSE;
1169
  ticks.QuadPart = (ULONGLONG) sec * 10000000ULL + (ULONGLONG) (nsec / 100);
1170
  ft.dwLowDateTime = ticks.LowPart;
1171
  ft.dwHighDateTime = ticks.HighPart;
1172
  ok = SetFileTime(h, NULL, NULL, &ft) ? HTS_TRUE : HTS_FALSE;
1173
  CloseHandle(h);
1174
  return ok;
1175
#elif defined(HAVE_UTIMENSAT)
1176
  struct timespec times[2];
1177
1178
0
  times[0].tv_sec = times[1].tv_sec = (time_t) sec;
1179
0
  times[0].tv_nsec = times[1].tv_nsec = (long) nsec;
1180
0
  return utimensat(AT_FDCWD, path, times, 0) == 0 ? HTS_TRUE : HTS_FALSE;
1181
#else
1182
  return st_lockrule_stamp(path, (time_t) sec);
1183
#endif
1184
0
}
1185
1186
/* Does hts_file_mtime() report the seconds and nanoseconds the filesystem
1187
   holds? This is what tells a reader that invents or drops either apart from a
1188
   filesystem that rounds. Vacuously true where STAT() has no nanosecond field,
1189
   and on Windows, where it offers nothing independent of the reader's own
1190
   GetFileAttributesExW. */
1191
static hts_boolean st_lockrule_time_matches_stat(const char *path, int64_t sec,
1192
0
                                                 int32_t nsec) {
1193
0
#if !defined(_WIN32) && (defined(HAVE_STRUCT_STAT_ST_MTIM_TV_NSEC) ||          \
1194
0
                         defined(HAVE_STRUCT_STAT_ST_MTIMESPEC_TV_NSEC))
1195
0
  STRUCT_STAT buf;
1196
0
  long raw;
1197
1198
0
  if (STAT(path, &buf) != 0)
1199
0
    return HTS_FALSE;
1200
0
#if defined(HAVE_STRUCT_STAT_ST_MTIM_TV_NSEC)
1201
0
  raw = (long) buf.st_mtim.tv_nsec;
1202
#else
1203
  raw = (long) buf.st_mtimespec.tv_nsec;
1204
#endif
1205
0
  return sec == (int64_t) buf.st_mtime && nsec == (int32_t) raw ? HTS_TRUE
1206
0
                                                                : HTS_FALSE;
1207
#else
1208
  (void) path;
1209
  (void) sec;
1210
  (void) nsec;
1211
  return HTS_TRUE;
1212
#endif
1213
0
}
1214
1215
/* The sub-second arm of the rule. A script that asks as soon as it sees
1216
   hts-in_progress.lock lands in the second the mirror started, which is the
1217
   boundary here. Reports what covered it, because a filesystem keeping whole
1218
   seconds holds none of these stamps. */
1219
static int st_lockrule_subsecond(httrackp *opt, const char *tag,
1220
                                 const char *name, const char *lock,
1221
0
                                 const char *progress) {
1222
  /* A microsecond apart, because 436 and 459 write their request as soon as
1223
     the progress lock appears and nothing spaces the two writes. */
1224
0
  const int32_t later = 500001000, same = 500000000, earlier = 499999000;
1225
0
  hts_filetime_t started, probe;
1226
0
  int err = 0;
1227
1228
0
  if (!hts_file_mtime(progress, &started)) {
1229
0
    fprintf(stderr, "%s: %s has no timestamp\n", tag, progress);
1230
0
    return 1;
1231
0
  }
1232
0
  if (!st_lockrule_touch(lock) ||
1233
0
      !st_lockrule_stamp_ns(lock, started.sec, later) ||
1234
0
      !hts_file_mtime(lock, &probe)) {
1235
0
    fprintf(stderr, "%s: cannot stamp %s\n", tag, lock);
1236
0
    return 1;
1237
0
  }
1238
  /* Before believing the read back, pin it to what the filesystem holds. */
1239
0
  if (!st_lockrule_time_matches_stat(lock, probe.sec, probe.nsec)) {
1240
0
    fprintf(stderr,
1241
0
            "%s: hts_file_mtime() does not report %s's own seconds and"
1242
0
            " nanoseconds\n",
1243
0
            tag, lock);
1244
0
    (void) UNLINK(lock);
1245
0
    return 1;
1246
0
  }
1247
0
  if (probe.sec != started.sec || probe.nsec != later) {
1248
    /* The stamp did not come back as asked, so none of the cases below can be
1249
       placed. A FAT or exFAT volume rounds it to whole, even seconds. */
1250
0
    printf("%s: same-second request: NOT COVERED (asked for %d ns, read back"
1251
0
           " %d%s)\n",
1252
0
           tag, (int) later, (int) probe.nsec,
1253
0
           probe.sec != started.sec ? ", in another second" : "");
1254
0
    (void) UNLINK(lock);
1255
0
    return err;
1256
0
  }
1257
1258
0
  if (!st_lockrule_stamp_ns(progress, started.sec, same)) {
1259
0
    fprintf(stderr, "%s: cannot stamp %s\n", tag, progress);
1260
0
    return 1;
1261
0
  }
1262
0
  if (!hts_take_lock_request(opt, name)) {
1263
0
    fprintf(stderr,
1264
0
            "%s: a request made in the second the mirror started was"
1265
0
            " ignored\n",
1266
0
            tag);
1267
0
    err = 1;
1268
0
  }
1269
0
  if (fexist_utf8(lock)) {
1270
0
    fprintf(stderr, "%s: the request outlived the action it caused\n", tag);
1271
0
    err = 1;
1272
0
  }
1273
1274
  /* Equal to the nanosecond is not later, so this is still an earlier run's. */
1275
0
  if (!st_lockrule_touch(lock) ||
1276
0
      !st_lockrule_stamp_ns(lock, started.sec, same)) {
1277
0
    fprintf(stderr, "%s: cannot stamp %s\n", tag, lock);
1278
0
    return 1;
1279
0
  }
1280
0
  if (hts_take_lock_request(opt, name)) {
1281
0
    fprintf(stderr,
1282
0
            "%s: a request stamped exactly when the mirror started was"
1283
0
            " taken\n",
1284
0
            tag);
1285
0
    err = 1;
1286
0
  }
1287
1288
  /* Same second, earlier nanosecond: the progress lock's own sub-second part
1289
     has to be read, or this one reads as newer than a zero. Re-created,
1290
     because the case above fails by deleting it. */
1291
0
  if (!st_lockrule_touch(lock) ||
1292
0
      !st_lockrule_stamp_ns(lock, started.sec, earlier)) {
1293
0
    fprintf(stderr, "%s: cannot stamp %s\n", tag, lock);
1294
0
    return 1;
1295
0
  }
1296
0
  if (hts_take_lock_request(opt, name)) {
1297
0
    fprintf(stderr,
1298
0
            "%s: a request older than the mirror by a fraction was taken\n",
1299
0
            tag);
1300
0
    err = 1;
1301
0
  }
1302
0
  if (!fexist_utf8(lock)) {
1303
0
    fprintf(stderr, "%s: a request the engine refused was deleted\n", tag);
1304
0
    err = 1;
1305
0
  }
1306
1307
  /* An earlier second is earlier despite the fraction, or a request left
1308
     by the run before would be taken for one aimed at this mirror. */
1309
0
  if (!st_lockrule_stamp_ns(lock, started.sec - 1, later)) {
1310
0
    fprintf(stderr, "%s: cannot stamp %s\n", tag, lock);
1311
0
    return 1;
1312
0
  }
1313
0
  if (hts_take_lock_request(opt, name)) {
1314
0
    fprintf(stderr,
1315
0
            "%s: a request from the second before the mirror started was"
1316
0
            " taken\n",
1317
0
            tag);
1318
0
    err = 1;
1319
0
  }
1320
0
  if (!fexist_utf8(lock)) {
1321
0
    fprintf(stderr, "%s: a request the engine refused was deleted\n", tag);
1322
0
    err = 1;
1323
0
  }
1324
0
  (void) UNLINK(lock);
1325
1326
  /* Backdating keeps the fraction, or the second it buys the engine would be
1327
     short by up to another one. */
1328
0
  if (!hts_file_backdate(progress, 1) || !hts_file_mtime(progress, &probe)) {
1329
0
    fprintf(stderr, "%s: cannot backdate %s\n", tag, progress);
1330
0
    return 1;
1331
0
  }
1332
0
  if (probe.sec != started.sec - 1 || probe.nsec != same) {
1333
0
    fprintf(stderr,
1334
0
            "%s: backdating %s moved it by something other than one second\n",
1335
0
            tag, progress);
1336
0
    err = 1;
1337
0
  }
1338
0
  printf("%s: same-second request: covered (stamps a microsecond apart)\n",
1339
0
         tag);
1340
0
  return err;
1341
0
}
1342
1343
/* The other request file, which taking NAME must leave untouched. */
1344
0
static const char *st_lockrule_other(const char *name) {
1345
0
  return strcmp(name, HTS_ABORT_LOCKNAME) == 0 ? HTS_PAUSE_LOCKNAME
1346
0
                                               : HTS_ABORT_LOCKNAME;
1347
0
}
1348
1349
/* Set up <dir>/, its progress lock and both requests, NAME stamped at WHEN. */
1350
static hts_boolean st_lockrule_setup(const char *dir, const char *name,
1351
                                     char *lock, size_t locksz, char *progress,
1352
                                     size_t progresssz, char *other,
1353
0
                                     size_t othersz, time_t when) {
1354
0
  strlcpybuff(lock, dir, locksz);
1355
0
  strlcatbuff(lock, name, locksz);
1356
0
  strlcpybuff(progress, dir, progresssz);
1357
0
  strlcatbuff(progress, "hts-in_progress.lock", progresssz);
1358
0
  strlcpybuff(other, dir, othersz);
1359
0
  strlcatbuff(other, st_lockrule_other(name), othersz);
1360
0
  structcheck(lock);
1361
0
  return st_lockrule_touch(progress) && st_lockrule_touch(other) &&
1362
0
         st_lockrule_touch(lock) && st_lockrule_stamp(lock, when);
1363
0
}
1364
1365
/* A request the engine cannot remove must never be taken. Only a real failed
1366
   unlink proves that, and nothing portable makes one happen, so this names
1367
   what covered it rather than skipping in silence. */
1368
static int st_lockrule_undeletable(httrackp *opt, const char *base,
1369
0
                                   const char *tag, const char *name) {
1370
#ifdef _WIN32
1371
  (void) opt;
1372
  (void) base;
1373
  (void) name;
1374
  /* The real case is a sharing violation, which needs a second process. */
1375
  printf("%s: undeletable request: NOT COVERED (no portable way to make"
1376
         " DeleteFile fail here)\n",
1377
         tag);
1378
  return 0;
1379
#else
1380
0
  char BIGSTK rodir[HTS_URLMAXSIZE];
1381
0
  char BIGSTK rolock[HTS_URLMAXSIZE * 2];
1382
0
  char BIGSTK roprogress[HTS_URLMAXSIZE * 2];
1383
0
  char BIGSTK roother[HTS_URLMAXSIZE * 2];
1384
0
  const uid_t self = geteuid();
1385
0
  uid_t drop = self;
1386
0
  int rofd;
1387
0
  int poll;
1388
0
  int err = 0;
1389
1390
0
  strlcpybuff(rodir, base, sizeof(rodir));
1391
0
  strlcatbuff(rodir, "readonly/", sizeof(rodir));
1392
0
  if (!st_lockrule_setup(rodir, name, rolock, sizeof(rolock), roprogress,
1393
0
                         sizeof(roprogress), roother, sizeof(roother),
1394
0
                         time(NULL) + 60)) {
1395
0
    fprintf(stderr, "%s: cannot set up %s\n", tag, rolock);
1396
0
    return 1;
1397
0
  }
1398
  /* One descriptor for both mode changes, so the name is resolved once. */
1399
0
  rofd = open(rodir, O_RDONLY | O_DIRECTORY);
1400
0
  if (rofd == -1) {
1401
0
    fprintf(stderr, "%s: cannot open %s\n", tag, rodir);
1402
0
    return 1;
1403
0
  }
1404
  /* Still readable, because an engine that saw no request at all would pass
1405
     this for the wrong reason. */
1406
0
  if (fchmod(rofd, 0555) != 0) {
1407
0
    fprintf(stderr, "%s: cannot make %s read-only\n", tag, rodir);
1408
0
    close(rofd);
1409
0
    return 1;
1410
0
  }
1411
  /* Root may write to a read-only directory, so borrow an unprivileged euid. */
1412
0
  if (self == 0) {
1413
0
    const struct passwd *const pw = getpwnam("nobody");
1414
1415
0
    drop = pw != NULL ? pw->pw_uid : (uid_t) 65534;
1416
0
    if (seteuid(drop) != 0) {
1417
0
      printf("%s: undeletable request: NOT COVERED (root, and euid %d "
1418
0
             "is not reachable)\n",
1419
0
             tag, (int) drop);
1420
0
      (void) fchmod(rofd, 0700);
1421
0
      close(rofd);
1422
0
      return 0;
1423
0
    }
1424
0
  }
1425
1426
0
  StringCopy(opt->path_log, rodir);
1427
  /* Probing by removal keeps no stat that could go stale before the engine's
1428
     own unlink, and ENOENT means the euid cannot reach the request at all. */
1429
0
  errno = 0;
1430
0
  if (UNLINK(rolock) == 0) {
1431
0
    printf("%s: undeletable request: NOT COVERED (euid %d could still"
1432
0
           " delete %s)\n",
1433
0
           tag, (int) drop, rolock);
1434
0
  } else if (errno == ENOENT) {
1435
0
    printf("%s: undeletable request: NOT COVERED (%s is out of reach of"
1436
0
           " euid %d)\n",
1437
0
           tag, rodir, (int) drop);
1438
0
  } else {
1439
0
    for (poll = 0; poll < 2; poll++) {
1440
0
      if (hts_take_lock_request(opt, name)) {
1441
0
        fprintf(stderr,
1442
0
                "%s: a request that cannot be deleted was taken (poll %d)\n",
1443
0
                tag, poll);
1444
0
        err = 1;
1445
0
      }
1446
0
    }
1447
0
    errno = 0;
1448
0
    if (UNLINK(rolock) == 0 || errno == ENOENT) {
1449
0
      fprintf(stderr, "%s: the undeletable request went away\n", tag);
1450
0
      err = 1;
1451
0
    }
1452
0
    printf("%s: undeletable request: covered (unwritable directory,"
1453
0
           " euid %d)\n",
1454
0
           tag, (int) drop);
1455
0
  }
1456
1457
0
  if (self == 0 && seteuid(self) != 0) {
1458
0
    fprintf(stderr, "%s: cannot get euid %d back\n", tag, (int) self);
1459
0
    err = 1;
1460
0
  }
1461
0
  (void) fchmod(rofd, 0700);
1462
0
  close(rofd);
1463
0
  return err;
1464
0
#endif
1465
0
}
1466
1467
static int st_lockrule(httrackp *opt, int argc, char **argv, const char *tag,
1468
0
                       const char *name) {
1469
0
  char BIGSTK base[HTS_URLMAXSIZE];
1470
0
  char BIGSTK lock[HTS_URLMAXSIZE * 2];
1471
0
  char BIGSTK progress[HTS_URLMAXSIZE * 2];
1472
0
  char BIGSTK other[HTS_URLMAXSIZE * 2];
1473
0
  String saved = STRING_EMPTY;
1474
0
  time_t started;
1475
0
  int err = 0;
1476
1477
0
  if (argc < 1) {
1478
0
    fprintf(stderr, "usage: -#test=%s <writable directory>\n", tag);
1479
0
    return 1;
1480
0
  }
1481
0
  strcpybuff(base, argv[0]);
1482
0
  if (base[0] != '\0' && hts_lastchar(base) != '/')
1483
0
    strcatbuff(base, "/");
1484
  /* The engine still has to shut down through its own path_log. */
1485
0
  StringCopy(saved, StringBuff(opt->path_log));
1486
0
  StringCopy(opt->path_log, base);
1487
1488
0
  strlcpybuff(lock, base, sizeof(lock));
1489
0
  strlcatbuff(lock, name, sizeof(lock));
1490
0
  structcheck(lock);
1491
0
  if (hts_take_lock_request(opt, name)) {
1492
0
    fprintf(stderr, "%s: a mirror with no request took one\n", tag);
1493
0
    err = 1;
1494
0
  }
1495
1496
0
  if (!st_lockrule_setup(base, name, lock, sizeof(lock), progress,
1497
0
                         sizeof(progress), other, sizeof(other), time(NULL))) {
1498
0
    fprintf(stderr, "%s: cannot set up %s\n", tag, base);
1499
0
    err = 1;
1500
0
  }
1501
0
  started = get_filetime(progress);
1502
0
  if (started == (time_t) -1) {
1503
0
    fprintf(stderr, "%s: %s has no timestamp\n", tag, progress);
1504
0
    err = 1;
1505
0
  }
1506
1507
  /* Left behind by an earlier mirror, in the run's own start second. On a
1508
     filesystem keeping fractions this reads as the earlier-fraction case, so it
1509
     pins the boundary only where the stamps are whole seconds. */
1510
0
  if (!st_lockrule_stamp(lock, started)) {
1511
0
    fprintf(stderr, "%s: cannot stamp %s\n", tag, lock);
1512
0
    err = 1;
1513
0
  }
1514
0
  if (hts_take_lock_request(opt, name)) {
1515
0
    fprintf(stderr,
1516
0
            "%s: a request no newer than the mirror it found was taken\n", tag);
1517
0
    err = 1;
1518
0
  }
1519
0
  if (!fexist_utf8(lock)) {
1520
0
    fprintf(stderr, "%s: a request the engine refused was deleted\n", tag);
1521
0
    err = 1;
1522
0
  }
1523
1524
  /* The engine backdates its own progress lock by a second, so a request
1525
     written the instant that lock appears is newer than it. */
1526
0
  if (!hts_file_backdate(progress, 1)) {
1527
0
    fprintf(stderr, "%s: cannot backdate %s\n", tag, progress);
1528
0
    err = 1;
1529
0
  }
1530
0
  if (!hts_take_lock_request(opt, name)) {
1531
0
    fprintf(stderr,
1532
0
            "%s: with the progress lock backdated, a request stamped when the"
1533
0
            " mirror started was still refused\n",
1534
0
            tag);
1535
0
    err = 1;
1536
0
  }
1537
  /* A minute earlier is an earlier run's, backdating or not. */
1538
0
  if (!st_lockrule_touch(lock) || !st_lockrule_stamp(lock, started - 60)) {
1539
0
    fprintf(stderr, "%s: cannot stamp %s\n", tag, lock);
1540
0
    err = 1;
1541
0
  }
1542
0
  if (hts_take_lock_request(opt, name)) {
1543
0
    fprintf(stderr, "%s: a request a minute older than the mirror was taken\n",
1544
0
            tag);
1545
0
    err = 1;
1546
0
  }
1547
0
  if (!fexist_utf8(lock)) {
1548
0
    fprintf(stderr, "%s: a request the engine refused was deleted\n", tag);
1549
0
    err = 1;
1550
0
  }
1551
0
  if (!st_lockrule_stamp(progress, started)) {
1552
0
    fprintf(stderr, "%s: cannot put %s back to when the mirror started\n", tag,
1553
0
            progress);
1554
0
    err = 1;
1555
0
  }
1556
1557
  /* Stamped after the mirror started: aimed at this run. */
1558
0
  if (!st_lockrule_touch(lock) || !st_lockrule_stamp(lock, started + 1)) {
1559
0
    fprintf(stderr, "%s: cannot stamp %s\n", tag, lock);
1560
0
    err = 1;
1561
0
  }
1562
0
  if (!hts_take_lock_request(opt, name)) {
1563
0
    fprintf(stderr, "%s: a request the engine can delete was ignored\n", tag);
1564
0
    err = 1;
1565
0
  }
1566
0
  if (fexist_utf8(lock)) {
1567
0
    fprintf(stderr, "%s: the request outlived the action it caused\n", tag);
1568
0
    err = 1;
1569
0
  }
1570
0
  if (!fexist_utf8(progress) || !fexist_utf8(other)) {
1571
0
    fprintf(stderr, "%s: taking a request deleted a neighbouring lock\n", tag);
1572
0
    err = 1;
1573
0
  }
1574
0
  if (hts_take_lock_request(opt, name)) {
1575
0
    fprintf(stderr, "%s: one request was taken twice\n", tag);
1576
0
    err = 1;
1577
0
  }
1578
1579
0
  err |= st_lockrule_subsecond(opt, tag, name, lock, progress);
1580
1581
  /* No progress lock means no mirror to reach, whatever the request says. */
1582
0
  (void) UNLINK(progress);
1583
0
  if (!st_lockrule_touch(lock) || !st_lockrule_stamp(lock, time(NULL) + 60)) {
1584
0
    fprintf(stderr, "%s: cannot re-create %s\n", tag, lock);
1585
0
    err = 1;
1586
0
  }
1587
0
  if (hts_take_lock_request(opt, name)) {
1588
0
    fprintf(stderr, "%s: a request reached a mirror that never ran\n", tag);
1589
0
    err = 1;
1590
0
  }
1591
1592
0
  err |= st_lockrule_undeletable(opt, base, tag, name);
1593
1594
0
  StringCopy(opt->path_log, StringBuff(saved));
1595
0
  StringFree(saved);
1596
0
  printf("%s self-test: %s\n", tag, err ? "FAIL" : "OK");
1597
0
  return err;
1598
0
}
1599
1600
0
static int st_abortlock(httrackp *opt, int argc, char **argv) {
1601
0
  return st_lockrule(opt, argc, argv, "abortlock", HTS_ABORT_LOCKNAME);
1602
0
}
1603
1604
0
static int st_stoplock(httrackp *opt, int argc, char **argv) {
1605
0
  return st_lockrule(opt, argc, argv, "stoplock", HTS_PAUSE_LOCKNAME);
1606
0
}
1607
1608
/* What a postprocess-html callback may claim, and out of whose storage. Two
1609
   capacities and every offset, or a bound that ignored "size" or looked only at
1610
   the base pointer would pass. */
1611
0
static int st_postprocsize(httrackp *opt, int argc, char **argv) {
1612
0
  char buf[16] = {0}, own[16] = {0};
1613
0
  const size_t capa = sizeof(buf);
1614
0
  size_t size;
1615
1616
0
  (void) opt;
1617
0
  (void) argc;
1618
0
  (void) argv;
1619
  /* the engine's own storage, whatever the reply points at inside it */
1620
0
  assertf(hts_postprocess_reply_inplace(buf, buf, capa));
1621
0
  assertf(hts_postprocess_reply_inplace(buf + capa - 1, buf, capa));
1622
0
  assertf(!hts_postprocess_reply_inplace(buf + capa, buf, capa));
1623
0
  assertf(!hts_postprocess_reply_inplace(own, buf, capa));
1624
0
  assertf(!hts_postprocess_reply_inplace(buf, buf, 0));
1625
0
  for (size = 4; size <= capa; size *= 2) {
1626
0
    size_t at;
1627
1628
0
    for (at = 0; at <= size; at++) {
1629
      /* in place, a reply owns what is left of "size" past its own offset */
1630
0
      assertf(hts_postprocess_reply_ok(buf + at, (int) (size - at), buf, size,
1631
0
                                       capa));
1632
      /* one past the storage is another object's business, not ours to bound */
1633
0
      if (at < capa)
1634
0
        assertf(!hts_postprocess_reply_ok(buf + at, (int) (size - at) + 1, buf,
1635
0
                                          size, capa));
1636
0
    }
1637
    /* in the buffer's slack, past every byte the engine handed out */
1638
0
    if (size < capa)
1639
0
      assertf(!hts_postprocess_reply_ok(buf + size + 1, 0, buf, size, capa));
1640
    /* a buffer of the callback's own is bounded by the callback */
1641
0
    assertf(hts_postprocess_reply_ok(own, (int) capa + 1, buf, size, capa));
1642
    /* neither may report a negative count */
1643
0
    assertf(!hts_postprocess_reply_ok(own, -1, buf, size, capa));
1644
0
    assertf(!hts_postprocess_reply_ok(buf, -1, buf, size, capa));
1645
0
  }
1646
0
  printf("postprocess reply self-test OK\n");
1647
0
  return 0;
1648
0
}
1649
1650
/* Print the verdict for a -V template, then the exact vector the engine would
1651
   hand the shell, one "ARG <index> <value>" line each. The save name is a
1652
   vector entry rather than part of the command, so a caller that wants to
1653
   replay it has to pass it the same way. */
1654
0
static int st_usercmd(httrackp *opt, int argc, char **argv) {
1655
0
  char BIGSTK dest[8192];
1656
  /* An optional third argument caps the room. The command line cannot reach
1657
     the real cap any more: a -V template stops at HTS_CDLMAXSIZE, and the save
1658
     name no longer lands in the command. */
1659
0
  size_t room = sizeof(dest);
1660
0
  const char *why = "unnamed";
1661
1662
0
  (void) opt;
1663
0
  if (argc < 2) {
1664
0
    fprintf(stderr, "usercmd: needs a -V template and a filename\n");
1665
0
    return 1;
1666
0
  }
1667
0
  if (argc > 2) {
1668
0
    const int asked = atoi(argv[2]);
1669
1670
0
    if (asked <= 0 || (size_t) asked > sizeof(dest)) {
1671
0
      fprintf(stderr, "usercmd: room must be within 1..%d\n",
1672
0
              (int) sizeof(dest));
1673
0
      return 1;
1674
0
    }
1675
0
    room = (size_t) asked;
1676
0
  }
1677
0
  switch (usercommand_expand(dest, room, argv[0], argv[1], &why)) {
1678
0
  case USERCOMMAND_EXPAND_OK:
1679
0
    break;
1680
0
  case USERCOMMAND_EXPAND_REFUSED:
1681
0
    printf("REFUSED %s\n", why);
1682
0
    return 0;
1683
0
  default:
1684
0
    printf("TOOLONG\n");
1685
0
    return 0;
1686
0
  }
1687
0
  printf("OK\n");
1688
0
#ifndef _WIN32
1689
0
  {
1690
0
    char *vector[USERCOMMAND_ARGV_MAX];
1691
0
    const size_t n = usercommand_argv(vector, dest, argv[1]);
1692
0
    size_t i;
1693
1694
0
    for (i = 0; i < n; i++)
1695
0
      printf("ARG %d %s\n", (int) i, vector[i]);
1696
0
  }
1697
#else
1698
  /* cmd.exe takes no positional parameters, so the name is inside the text */
1699
  printf("ARG 0 cmd\nARG 1 /c\nARG 2 %s\n", dest);
1700
#endif
1701
0
  return 0;
1702
0
}
1703
1704
/* Run a -V template for real, through the engine's own spawn path, and report
1705
   what the shell exited with. The child's output goes to our stdout. */
1706
0
static int st_usercmdrun(httrackp *opt, int argc, char **argv) {
1707
0
  if (argc < 2) {
1708
0
    fprintf(stderr, "usercmdrun: needs a -V template and a filename\n");
1709
0
    return 1;
1710
0
  }
1711
0
  fflush(stdout);
1712
0
  printf("EXIT %d\n", usercommand_exe(opt, argv[0], argv[1]));
1713
0
  return 0;
1714
0
}
1715
1716
/* What no digit run can produce, so an untouched field shows. */
1717
0
#define ST_GLUED_UNSET (-7)
1718
1719
/* Read text as the glued digits of an option and print "got=G value=V adv=N".
1720
   N is the number of bytes the option pointer moved. */
1721
static void st_glued_read(hts_boolean wide, LLint min, LLint max,
1722
0
                          const char *text, char *out, size_t size) {
1723
0
  char buf[256];
1724
0
  char *com = buf;
1725
0
  LLint value = ST_GLUED_UNSET;
1726
0
  int narrow = ST_GLUED_UNSET;
1727
0
  hts_scan_result got;
1728
1729
0
  buf[0] = 'X';
1730
0
  buf[1] = '\0';
1731
0
  strlncatbuff(buf, text, sizeof(buf), sizeof(buf) - 1);
1732
0
  if (wide) {
1733
0
    got = cmdl_glued_llint(&com, min, max, &value);
1734
0
  } else {
1735
0
    got = cmdl_glued_int(&com, (int) min, (int) max, &narrow);
1736
0
    value = narrow;
1737
0
  }
1738
0
  snprintf(out, size, "got=%d value=" LLintP " adv=%d", (int) got, value,
1739
0
           (int) (com - buf));
1740
0
}
1741
1742
/* The same text through the frozen sscanf() and isdigit() loop it replaced. */
1743
static void st_glued_legacy(hts_boolean wide, const char *text, LLint *value,
1744
0
                            int *adv) {
1745
0
  if (wide) {
1746
0
    *value = ST_GLUED_UNSET;
1747
0
    (void) sscanf(text, LLintP, value);
1748
0
  } else {
1749
0
    int narrow = ST_GLUED_UNSET;
1750
1751
0
    (void) sscanf(text, "%d", &narrow);
1752
0
    *value = narrow;
1753
0
  }
1754
0
  for (*adv = 0; isdigit((unsigned char) text[*adv]); (*adv)++)
1755
0
    ;
1756
0
}
1757
1758
/* <int|llint> <min> <max> <text>: one glued read. With no argument, every
1759
   converted option against its legacy parse over a generated corpus. */
1760
0
static int st_gluedint(httrackp *opt, int argc, char **argv) {
1761
  /* Each glued option htscoremain.c reads, with the bounds it passes. */
1762
0
  static const struct {
1763
0
    const char *name;
1764
0
    hts_boolean wide;
1765
0
    LLint max;
1766
0
  } sites[] = {
1767
0
      {"-r", HTS_FALSE, INT_MAX - 1}, {"-c", HTS_FALSE, INT_MAX},
1768
0
      {"-p", HTS_FALSE, INT_MAX},     {"-G", HTS_TRUE, INT64_MAX},
1769
0
      {"-M", HTS_TRUE, INT64_MAX},    {"-m", HTS_TRUE, INT64_MAX},
1770
0
      {"-m,", HTS_TRUE, INT64_MAX},   {"-T", HTS_FALSE, INT_MAX},
1771
0
      {"-J", HTS_FALSE, INT_MAX},     {"-R", HTS_FALSE, INT_MAX},
1772
0
      {"-E", HTS_FALSE, INT_MAX},     {"-H", HTS_FALSE, INT_MAX},
1773
0
      {"-A", HTS_FALSE, INT_MAX},     {"-j", HTS_FALSE, INT_MAX},
1774
0
      {"-%e", HTS_FALSE, INT_MAX},    {"-@i", HTS_FALSE, INT_MAX},
1775
0
      {"-@m", HTS_FALSE, INT_MAX},    {"-#C", HTS_FALSE, INT_MAX},
1776
0
      {"-#L", HTS_FALSE, INT_MAX},    {"-#F", HTS_FALSE, HTS_FILTERS_MAX},
1777
0
      {"-#u", HTS_FALSE, INT_MAX}};
1778
1779
0
  static const char *const spaces[] = {"", " ", "\t"};
1780
0
  static const char *const signs[] = {"", "+", "-"};
1781
0
  static const char *const digits[] = {"",
1782
0
                                       "0",
1783
0
                                       "00",
1784
0
                                       "7",
1785
0
                                       "42",
1786
0
                                       "0042",
1787
0
                                       "65535",
1788
0
                                       "2147483647",
1789
0
                                       "2147483648",
1790
0
                                       "4294967295",
1791
0
                                       "4294967297",
1792
0
                                       "9223372036854775807",
1793
0
                                       "9223372036854775808",
1794
0
                                       "99999999999999999999"};
1795
0
  static const char *const suffixes[] = {"", "x", ".5", ",3", "-1"};
1796
0
  size_t s, w, g, d, x;
1797
0
  int compared = 0, unsigned_only = 0, too_large = 0, failures = 0;
1798
0
  char in[128], cur[128], want[128];
1799
1800
0
  (void) opt;
1801
0
  if (argc == 4) {
1802
0
    st_glued_read(strcmp(argv[0], "llint") == 0, strtoll(argv[1], NULL, 10),
1803
0
                  strtoll(argv[2], NULL, 10), argv[3], cur, sizeof(cur));
1804
0
    printf("%s\n", cur);
1805
0
    return 0;
1806
0
  }
1807
0
  for (s = 0; s < sizeof(sites) / sizeof(sites[0]); s++)
1808
0
    for (w = 0; w < sizeof(spaces) / sizeof(spaces[0]); w++)
1809
0
      for (g = 0; g < sizeof(signs) / sizeof(signs[0]); g++)
1810
0
        for (d = 0; d < sizeof(digits) / sizeof(digits[0]); d++)
1811
0
          for (x = 0; x < sizeof(suffixes) / sizeof(suffixes[0]); x++) {
1812
0
            LLint old, fits;
1813
0
            int adv;
1814
0
            hts_scan_result expect = HTS_SCAN_OK;
1815
1816
0
            snprintf(in, sizeof(in), "%s%s%s%s", spaces[w], signs[g], digits[d],
1817
0
                     suffixes[x]);
1818
0
            st_glued_legacy(sites[s].wide, in, &old, &adv);
1819
            /* Only the digits at the start are read now: sscanf() also took
1820
               a space or a sign, then wrapped what was too large. */
1821
0
            if (adv == 0) {
1822
0
              expect = HTS_SCAN_NONE;
1823
0
            } else if (!hts_parse_llint(in, NULL, 0, sites[s].max, &fits)) {
1824
0
              expect = HTS_SCAN_REFUSED;
1825
0
            }
1826
0
            if (expect != HTS_SCAN_OK && old != ST_GLUED_UNSET) {
1827
0
              old = ST_GLUED_UNSET;
1828
0
              if (expect == HTS_SCAN_NONE)
1829
0
                unsigned_only++;
1830
0
              else
1831
0
                too_large++;
1832
0
            }
1833
0
            snprintf(want, sizeof(want), "got=%d value=" LLintP " adv=%d",
1834
0
                     (int) expect, old, adv);
1835
0
            st_glued_read(sites[s].wide, 0, sites[s].max, in, cur, sizeof(cur));
1836
0
            compared++;
1837
0
            if (strcmp(cur, want) != 0) {
1838
0
              printf("FAIL %s \"%s\": want \"%s\" now \"%s\"\n", sites[s].name,
1839
0
                     in, want, cur);
1840
0
              failures++;
1841
0
            }
1842
0
          }
1843
  printf("glued-int: %d compared, %d signed or spaced now unread, %d too "
1844
0
         "large now refused, %d failures\n",
1845
0
         compared, unsigned_only, too_large, failures);
1846
0
  return failures != 0;
1847
0
}
1848
1849
/* ------------------------------------------------------------ */
1850
/* Registry: this module's tests, in the order -#test lists them. */
1851
/* ------------------------------------------------------------ */
1852
1853
const struct selftest_entry selftests_opt[] = {
1854
    {"glued-int", "[<int|llint> <min> <max> <text>]",
1855
     "glued numeric options (-c8) read as before, out-of-range refused",
1856
     st_gluedint},
1857
    {"usercmd", "<-V template> <filename> [room]",
1858
     "rewrite a -V template's $0 and print the shell vector it would run",
1859
     st_usercmd},
1860
    {"usercmdrun", "<-V template> <filename>",
1861
     "run a -V template through the engine's own spawn path", st_usercmdrun},
1862
    {"log-counters", "",
1863
     "error and warning counts survive a run with no log file (#1681)",
1864
     st_logcounters},
1865
    {"scantoken", "",
1866
     "option-string token copy is bounded and reports a refusal (#1271)",
1867
     st_scantoken},
1868
    {"abortlock", "<writable directory>",
1869
     "an abort request stops the mirror only if it is fresh and deletable",
1870
     st_abortlock},
1871
    {"stoplock", "<writable directory>",
1872
     "a pause request pauses the mirror only if it is fresh and deletable",
1873
     st_stoplock},
1874
    {"postprocsize", "",
1875
     "a postprocess-html callback cannot claim bytes it was not handed",
1876
     st_postprocsize},
1877
    {"postfile", "<short> <long> <embedded-nul> <empty>",
1878
     "a >postfile: body is clipped to the request block and terminated",
1879
     st_postfile},
1880
    {"optalias", "[-list | <option> [<value>]]",
1881
     "long-option alias expansion (--index=0 and friends)", st_optalias},
1882
    {"features", "", "which optional features this build has", st_features},
1883
    {"footerfmt", "<template>", "-%F footer positional/named expansion",
1884
     st_footerfmt},
1885
    {"cmdline-split", "",
1886
     "webhttrack command-line to argv split (bounds, quoting)",
1887
     st_cmdlinesplit},
1888
    {"copyopt", "", "copy_htsopt option-copy self-test", st_copyopt},
1889
    {"upperlinksnote", "",
1890
     "the above-the-start-directory note empties its buffer when silent",
1891
     st_upperlinksnote},
1892
    {"cachedefault", "", "-C default is C1 cache-priority, not C2",
1893
     st_cachedefault},
1894
    {"logcallback", "", "log callback must not consume the log file's va_list",
1895
     st_logcallback},
1896
#ifdef HTS_CRASH_TEST
1897
    {"crashannounce", "[logfile]",
1898
     "a crash-test marker reaches the log callback too", st_crashannounce},
1899
#endif
1900
    {NULL, NULL, NULL, NULL},
1901
};