/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, ×) == 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 | | }; |