Fossil SCM

fossil-scm / tools / codecheck1.c
Blame History Raw 701 lines
1
/*
2
** Copyright (c) 2014 D. Richard Hipp
3
**
4
** This program is free software; you can redistribute it and/or
5
** modify it under the terms of the Simplified BSD License (also
6
** known as the "2-Clause License" or "FreeBSD License".)
7
**
8
** This program is distributed in the hope that it will be useful,
9
** but without any warranty; without even the implied warranty of
10
** merchantability or fitness for a particular purpose.
11
**
12
** Author contact information:
13
** [email protected]
14
** http://www.hwaci.com/drh/
15
**
16
*******************************************************************************
17
**
18
** This program reads Fossil source code files and tries to verify that
19
** printf-style format strings are correct.
20
**
21
** This program implements a compile-time validation step on the Fossil
22
** source code. Running this program is entirely optional. Its role is
23
** similar to the -Wall compiler switch on gcc, or the scan-build utility
24
** of clang, or other static analyzers. The purpose is to try to identify
25
** problems in the source code at compile-time. The difference is that this
26
** static checker is specifically designed for the particular printf formatter
27
** implementation used by Fossil.
28
**
29
** Checks include:
30
**
31
** * Verify that vararg formatting routines like blob_printf() or
32
** db_multi_exec() have the correct number of arguments for their
33
** format string.
34
**
35
** * For routines designed to generate SQL or HTML or a URL or JSON,
36
** detect and warn about possible injection attacks.
37
*/
38
#include <stdio.h>
39
#include <stdlib.h>
40
#include <ctype.h>
41
#include <string.h>
42
#include <assert.h>
43
44
/*
45
** Debugging switch
46
*/
47
static int eVerbose = 0;
48
49
/*
50
** Malloc, aborting if it fails.
51
*/
52
void *safe_malloc(int nByte){
53
void *x = malloc(nByte);
54
if( x==0 ){
55
fprintf(stderr, "failed to allocate %d bytes\n", nByte);
56
exit(1);
57
}
58
return x;
59
}
60
void *safe_realloc(void *pOld, int nByte){
61
void *x = realloc(pOld, nByte);
62
if( x==0 ){
63
fprintf(stderr, "failed to allocate %d bytes\n", nByte);
64
exit(1);
65
}
66
return x;
67
}
68
69
/*
70
** Read the entire content of the file named zFilename into memory obtained
71
** from malloc(). Add a zero-terminator to the end.
72
** Return a pointer to that memory.
73
*/
74
static char *read_file(const char *zFilename){
75
FILE *in;
76
char *z;
77
int nByte;
78
int got;
79
in = fopen(zFilename, "rb");
80
if( in==0 ){
81
return 0;
82
}
83
fseek(in, 0, SEEK_END);
84
nByte = ftell(in);
85
fseek(in, 0, SEEK_SET);
86
z = safe_malloc( nByte+1 );
87
got = fread(z, 1, nByte, in);
88
z[got] = 0;
89
fclose(in);
90
return z;
91
}
92
93
/*
94
** When parsing the input file, the following token types are recognized.
95
*/
96
#define TK_SPACE 1 /* Whitespace or comments */
97
#define TK_ID 2 /* An identifier */
98
#define TK_STR 3 /* A string literal in double-quotes */
99
#define TK_OTHER 4 /* Any other token */
100
#define TK_EOF 99 /* End of file */
101
102
/*
103
** Determine the length and type of the token beginning at z[0]
104
*/
105
static int token_length(const char *z, int *pType, int *pLN){
106
int i;
107
if( z[0]==0 ){
108
*pType = TK_EOF;
109
return 0;
110
}
111
if( z[0]=='"' || z[0]=='\'' ){
112
for(i=1; z[i] && z[i]!=z[0]; i++){
113
if( z[i]=='\\' && z[i+1]!=0 ){
114
if( z[i+1]=='\n' ) (*pLN)++;
115
i++;
116
}
117
}
118
if( z[i]!=0 ) i++;
119
*pType = z[0]=='"' ? TK_STR : TK_OTHER;
120
return i;
121
}
122
if( isalnum(z[0]) || z[0]=='_' ){
123
for(i=1; isalnum(z[i]) || z[i]=='_'; i++){}
124
*pType = isalpha(z[0]) || z[0]=='_' ? TK_ID : TK_OTHER;
125
return i;
126
}
127
if( isspace(z[0]) ){
128
if( z[0]=='\n' ) (*pLN)++;
129
for(i=1; isspace(z[i]); i++){
130
if( z[i]=='\n' ) (*pLN)++;
131
}
132
*pType = TK_SPACE;
133
return i;
134
}
135
if( z[0]=='/' && z[1]=='*' ){
136
for(i=2; z[i] && (z[i]!='*' || z[i+1]!='/'); i++){
137
if( z[i]=='\n' ) (*pLN)++;
138
}
139
if( z[i] ) i += 2;
140
*pType = TK_SPACE;
141
return i;
142
}
143
if( z[0]=='/' && z[1]=='/' ){
144
for(i=2; z[i] && z[i]!='\n'; i++){}
145
if( z[i] ){
146
(*pLN)++;
147
i++;
148
}
149
*pType = TK_SPACE;
150
return i;
151
}
152
if( z[0]=='\\' && (z[1]=='\n' || (z[1]=='\r' && z[2]=='\n')) ){
153
*pType = TK_SPACE;
154
return 1;
155
}
156
*pType = TK_OTHER;
157
return 1;
158
}
159
160
/*
161
** Return the next non-whitespace token
162
*/
163
const char *next_non_whitespace(const char *z, int *pLen, int *pType){
164
int len;
165
int eType;
166
int ln = 0;
167
while( (len = token_length(z, &eType, &ln))>0 && eType==TK_SPACE ){
168
z += len;
169
}
170
*pLen = len;
171
*pType = eType;
172
return z;
173
}
174
175
/*
176
** Return index into z[] for the first balanced TK_OTHER token with
177
** value cValue.
178
*/
179
static int distance_to(const char *z, char cVal){
180
int len;
181
int dist = 0;
182
int eType;
183
int nNest = 0;
184
int ln = 0;
185
while( z[0] && (len = token_length(z, &eType, &ln))>0 ){
186
if( eType==TK_OTHER ){
187
if( z[0]==cVal && nNest==0 ){
188
break;
189
}else if( z[0]=='(' ){
190
nNest++;
191
}else if( z[0]==')' ){
192
nNest--;
193
}
194
}
195
dist += len;
196
z += len;
197
}
198
return dist;
199
}
200
201
/*
202
** Return the first non-whitespace characters in z[]
203
*/
204
static const char *skip_space(const char *z){
205
while( isspace(z[0]) ){ z++; }
206
return z;
207
}
208
209
/*
210
** Remove excess whitespace and nested "()" from string z.
211
*/
212
static char *simplify_expr(char *z){
213
int n = (int)strlen(z);
214
while( n>0 ){
215
if( isspace(z[0]) ){
216
z++;
217
n--;
218
continue;
219
}
220
if( z[0]=='(' && z[n-1]==')' ){
221
z++;
222
n -= 2;
223
continue;
224
}
225
break;
226
}
227
z[n] = 0;
228
return z;
229
}
230
231
/*
232
** Return true if the input is a string literal.
233
*/
234
static int is_string_lit(const char *z){
235
int nu1, nu2;
236
z = next_non_whitespace(z, &nu1, &nu2);
237
if( strcmp(z, "NULL")==0 ) return 1;
238
return z[0]=='"';
239
}
240
241
/*
242
** Return true if the input is an expression of string literals:
243
**
244
** EXPR ? "..." : "..."
245
*/
246
static int is_string_expr(const char *z){
247
int len = 0, eType;
248
const char *zOrig = z;
249
len = distance_to(z, '?');
250
if( z[len]==0 && skip_space(z)[0]=='(' ){
251
z = skip_space(z) + 1;
252
len = distance_to(z, '?');
253
}
254
z += len;
255
if( z[0]=='?' ){
256
z++;
257
z = next_non_whitespace(z, &len, &eType);
258
if( eType==TK_STR ){
259
z += len;
260
z = next_non_whitespace(z, &len, &eType);
261
if( eType==TK_OTHER && z[0]==':' ){
262
z += len;
263
z = next_non_whitespace(z, &len, &eType);
264
if( eType==TK_STR ){
265
z += len;
266
z = next_non_whitespace(z, &len, &eType);
267
if( eType==TK_EOF ) return 1;
268
if( eType==TK_OTHER && z[0]==')' && skip_space(zOrig)[0]=='(' ){
269
z += len;
270
z = next_non_whitespace(z, &len, &eType);
271
if( eType==TK_EOF ) return 1;
272
}
273
}
274
}
275
}
276
}
277
return 0;
278
}
279
280
/*
281
** A list of functions that return strings that are safe to insert into
282
** SQL using %s.
283
*/
284
static const char *azSafeFunc[] = {
285
"filename_collation",
286
"leaf_is_closed_sql",
287
"timeline_query_for_www",
288
"timeline_query_for_tty",
289
"blob_sql_text",
290
"glob_expr",
291
"fossil_all_reserved_names",
292
"configure_inop_rhs",
293
"db_setting_inop_rhs",
294
};
295
296
/*
297
** Return true if the input is an argument that is safe to use with %s
298
** while building an SQL statement.
299
*/
300
static int is_sql_safe(const char *z){
301
int len, eType;
302
int i;
303
304
/* A string literal is safe for use with %s */
305
if( is_string_lit(z) ) return 1;
306
307
/* Certain functions are guaranteed to return a string that is safe
308
** for use with %s */
309
z = next_non_whitespace(z, &len, &eType);
310
for(i=0; i<sizeof(azSafeFunc)/sizeof(azSafeFunc[0]); i++){
311
if( eType==TK_ID
312
&& strncmp(z, azSafeFunc[i], len)==0
313
&& strlen(azSafeFunc[i])==len
314
){
315
return 1;
316
}
317
}
318
319
/* Expressions of the form: EXPR ? "..." : "...." can count as
320
** a string literal. */
321
if( is_string_expr(z) ) return 1;
322
323
/* If the "safe-for-%s" comment appears in the argument, then
324
** let it through */
325
if( strstr(z, "/*safe-for-%s*/")!=0 ) return 1;
326
327
return 0;
328
}
329
330
/*
331
** Return true if the input is an argument that is never safe for use
332
** with %s.
333
*/
334
static int never_safe(const char *z){
335
if( strstr(z,"/*safe-for-%s*/")!=0 ) return 0;
336
if( z[0]=='P' ){
337
if( strncmp(z,"PIF(",4)==0 ) return 0;
338
if( strncmp(z,"PCK(",4)==0 ) return 0;
339
return 1;
340
}
341
if( strncmp(z,"cgi_param",9)==0 ) return 1;
342
return 0;
343
}
344
345
/*
346
** Processing flags
347
*/
348
#define FMT_SQL 0x00001 /* Generator for SQL text */
349
#define FMT_HTML 0x00002 /* Generator for HTML text */
350
#define FMT_URL 0x00004 /* Generator for URLs */
351
#define FMT_JSON 0x00008 /* Generator for JSON */
352
#define FMT_SAFE 0x00010 /* Generator for human-readable text */
353
#define FMT_LIT 0x00020 /* Just verify that a string literal */
354
#define FMT_PX 0x00040 /* Must have a literal prefix in format string */
355
356
/*
357
** A list of internal Fossil interfaces that take a printf-style format
358
** string.
359
*/
360
struct FmtFunc {
361
const char *zFName; /* Name of the function */
362
int iFmtArg; /* Index of format argument. Leftmost is 1. */
363
unsigned fmtFlags; /* Processing flags */
364
} aFmtFunc[] = {
365
{ "admin_log", 1, FMT_SAFE },
366
{ "ajax_route_error", 2, FMT_SAFE },
367
{ "audit_append", 3, FMT_SAFE },
368
{ "backofficeTrace", 1, FMT_SAFE },
369
{ "backoffice_log", 1, FMT_SAFE },
370
{ "blob_append_sql", 2, FMT_SQL },
371
{ "blob_appendf", 2, FMT_SAFE },
372
{ "cgi_debug", 1, FMT_SAFE },
373
{ "cgi_panic", 1, FMT_SAFE },
374
{ "cgi_printf", 1, FMT_HTML },
375
{ "cgi_printf_header", 1, FMT_HTML },
376
{ "cgi_redirectf", 1, FMT_URL },
377
{ "chref", 2, FMT_URL },
378
{ "CX", 1, FMT_HTML },
379
{ "db_blob", 2, FMT_SQL },
380
{ "db_debug", 1, FMT_SQL },
381
{ "db_double", 2, FMT_SQL },
382
{ "db_err", 1, FMT_SAFE },
383
{ "db_exists", 1, FMT_SQL },
384
{ "db_get_mprintf", 2, FMT_SAFE },
385
{ "db_int", 2, FMT_SQL },
386
{ "db_int64", 2, FMT_SQL },
387
{ "db_lset", 1, FMT_LIT },
388
{ "db_lset_int", 1, FMT_LIT },
389
{ "db_multi_exec", 1, FMT_SQL },
390
{ "db_optional_sql", 2, FMT_SQL },
391
{ "db_prepare", 2, FMT_SQL },
392
{ "db_prepare_ignore_error", 2, FMT_SQL },
393
{ "db_set", 1, FMT_LIT },
394
{ "db_set_int", 1, FMT_LIT },
395
{ "db_set_mprintf", 3, FMT_PX },
396
{ "db_static_prepare", 2, FMT_SQL },
397
{ "db_text", 2, FMT_SQL },
398
{ "db_unset", 1, FMT_LIT },
399
{ "db_unset_mprintf", 2, FMT_PX },
400
{ "emailerError", 2, FMT_SAFE },
401
{ "entry_attribute", 4, FMT_LIT },
402
{ "fileedit_ajax_error", 2, FMT_SAFE },
403
{ "form_begin", 2, FMT_URL },
404
{ "fossil_error", 2, FMT_SAFE },
405
{ "fossil_errorlog", 1, FMT_SAFE },
406
{ "fossil_fatal", 1, FMT_SAFE },
407
{ "fossil_fatal_recursive", 1, FMT_SAFE },
408
{ "fossil_panic", 1, FMT_SAFE },
409
{ "fossil_print", 1, FMT_SAFE },
410
{ "fossil_trace", 1, FMT_SAFE },
411
{ "fossil_warning", 1, FMT_SAFE },
412
{ "gitmirror_message", 2, FMT_SAFE },
413
{ "href", 1, FMT_URL },
414
{ "json_new_string_f", 1, FMT_SAFE },
415
{ "json_set_err", 2, FMT_SAFE },
416
{ "json_warn", 2, FMT_SAFE },
417
{ "mprintf", 1, FMT_SAFE },
418
{ "multiple_choice_attribute", 3, FMT_LIT },
419
{ "onoff_attribute", 3, FMT_LIT },
420
{ "pop3_print", 2, FMT_SAFE },
421
{ "smtp_send_line", 2, FMT_SAFE },
422
{ "smtp_server_send", 2, FMT_SAFE },
423
{ "socket_set_errmsg", 1, FMT_SAFE },
424
{ "ssl_set_errmsg", 1, FMT_SAFE },
425
{ "style_copy_button", 5, FMT_SAFE },
426
{ "style_header", 1, FMT_HTML },
427
{ "style_set_current_page", 1, FMT_URL },
428
{ "style_submenu_element", 2, FMT_URL },
429
{ "style_submenu_sql", 3, FMT_SQL },
430
{ "textarea_attribute", 5, FMT_LIT },
431
{ "tktsetup_generic", 1, FMT_LIT },
432
{ "webpage_error", 1, FMT_SAFE },
433
{ "webpage_notfound_error", 1, FMT_SAFE },
434
{ "xfersetup_generic", 1, FMT_LIT },
435
{ "xhref", 2, FMT_URL },
436
};
437
438
/*
439
** Comparison function for two FmtFunc entries
440
*/
441
static int fmtfunc_cmp(const void *pAA, const void *pBB){
442
const struct FmtFunc *pA = (const struct FmtFunc*)pAA;
443
const struct FmtFunc *pB = (const struct FmtFunc*)pBB;
444
return strcmp(pA->zFName, pB->zFName);
445
}
446
447
/*
448
** Determine if the identifier zIdent of length nIndent is a Fossil
449
** internal interface that uses a printf-style argument. Return zero if not.
450
** Return the index of the format string if true with the left-most
451
** argument having an index of 1.
452
*/
453
static int isFormatFunc(const char *zIdent, int nIdent, unsigned *pFlags){
454
int upr, lwr;
455
lwr = 0;
456
upr = sizeof(aFmtFunc)/sizeof(aFmtFunc[0]) - 1;
457
while( lwr<=upr ){
458
unsigned x = (lwr + upr)/2;
459
int c = strncmp(zIdent, aFmtFunc[x].zFName, nIdent);
460
if( c==0 ){
461
if( aFmtFunc[x].zFName[nIdent]==0 ){
462
*pFlags = aFmtFunc[x].fmtFlags;
463
return aFmtFunc[x].iFmtArg;
464
}
465
c = -1;
466
}
467
if( c<0 ){
468
upr = x - 1;
469
}else{
470
lwr = x + 1;
471
}
472
}
473
*pFlags = 0;
474
return 0;
475
}
476
477
/*
478
** Return the expected number of arguments for the format string.
479
** Return -1 if the value cannot be computed.
480
**
481
** For each argument less than nType, store the conversion character
482
** for that argument in cType[i].
483
**
484
** Store the number of initial literal characters of the format string
485
** in *pInit.
486
*/
487
static int formatArgCount(const char *z, int nType, char *cType, int *pInit){
488
int nArg = 0;
489
int i, k;
490
int len;
491
int eType;
492
int ln = 0;
493
*pInit = 0;
494
while( z[0] ){
495
len = token_length(z, &eType, &ln);
496
if( eType==TK_STR ){
497
for(i=1; i<len-1 && isalpha(z[i]); i++){}
498
*pInit = i-1;
499
for(i=1; i<len-1; i++){
500
if( z[i]!='%' ) continue;
501
if( z[i+1]=='%' ){ i++; continue; }
502
for(k=i+1; k<len && !isalpha(z[k]); k++){
503
if( z[k]=='*' || z[k]=='#' ){
504
if( nArg<nType ) cType[nArg] = z[k];
505
nArg++;
506
}
507
}
508
if( z[k]!='R' ){
509
if( nArg<nType ) cType[nArg] = z[k];
510
nArg++;
511
}
512
}
513
}
514
z += len;
515
}
516
return nArg;
517
}
518
519
/*
520
** The function call that begins at zFCall[0] (which is on line lnFCall of the
521
** original file) is a function that uses a printf-style format string
522
** on argument number fmtArg. It has processings flags fmtFlags. Do
523
** compile-time checking on this function, output any errors, and return
524
** the number of errors.
525
*/
526
static int checkFormatFunc(
527
const char *zFilename, /* Name of the file being processed */
528
const char *zFCall, /* Pointer to start of function call */
529
int lnFCall, /* Line number that holds z[0] */
530
int fmtArg, /* Format string should be this argument */
531
int fmtFlags /* Extra processing flags */
532
){
533
int szFName;
534
int eToken;
535
int ln = lnFCall;
536
int len;
537
const char *zStart;
538
char *z;
539
char *zCopy;
540
int nArg = 0;
541
const char **azArg = 0;
542
int i, k;
543
int nErr = 0;
544
char *acType;
545
int nInit = 0;
546
547
szFName = token_length(zFCall, &eToken, &ln);
548
zStart = next_non_whitespace(zFCall+szFName, &len, &eToken);
549
assert( zStart[0]=='(' && len==1 );
550
len = distance_to(zStart+1, ')');
551
zCopy = safe_malloc( len + 1 );
552
memcpy(zCopy, zStart+1, len);
553
zCopy[len] = 0;
554
azArg = 0;
555
nArg = 0;
556
z = zCopy;
557
while( z[0] ){
558
char cEnd;
559
len = distance_to(z, ',');
560
cEnd = z[len];
561
z[len] = 0;
562
azArg = safe_realloc((char*)azArg, (sizeof(azArg[0])+1)*(nArg+1));
563
azArg[nArg++] = simplify_expr(z);
564
if( cEnd==0 ) break;
565
z += len + 1;
566
}
567
acType = (char*)&azArg[nArg];
568
if( fmtArg>nArg ){
569
printf("%s:%d: too few arguments to %.*s()\n",
570
zFilename, lnFCall, szFName, zFCall);
571
nErr++;
572
}else{
573
const char *zFmt = azArg[fmtArg-1];
574
const char *zOverride = strstr(zFmt, "/*works-like:");
575
if( zOverride ) zFmt = zOverride + sizeof("/*works-like:")-1;
576
if( fmtFlags & FMT_LIT ){
577
if( !is_string_lit(zFmt) ){
578
printf("%s:%d: argument %d to %.*s() should be a string literal\n",
579
zFilename, lnFCall, fmtArg, szFName, zFCall);
580
nErr++;
581
}
582
}else if( !is_string_lit(zFmt) ){
583
printf("%s:%d: %.*s() has non-constant format on arg[%d]\n",
584
zFilename, lnFCall, szFName, zFCall, fmtArg-1);
585
nErr++;
586
}else if( (k = formatArgCount(zFmt, nArg, acType, &nInit))>=0
587
&& nArg!=fmtArg+k ){
588
printf("%s:%d: too %s arguments to %.*s() "
589
"- got %d and expected %d\n",
590
zFilename, lnFCall, (nArg<fmtArg+k ? "few" : "many"),
591
szFName, zFCall, nArg, fmtArg+k);
592
nErr++;
593
}else if( (fmtFlags & FMT_PX)!=0 ){
594
if( nInit==0 ){
595
printf("%s:%d: format string on %.*s() should have"
596
" an ASCII character prefix\n",
597
zFilename, lnFCall, szFName, zFCall);
598
nErr++;
599
}
600
}else if( (fmtFlags & FMT_SAFE)==0 ){
601
for(i=0; i<nArg && i<k; i++){
602
if( (acType[i]=='s' || acType[i]=='z' || acType[i]=='b') ){
603
const char *zExpr = azArg[fmtArg+i];
604
if( never_safe(zExpr) ){
605
printf("%s:%d: Argument %d to %.*s() is not safe for"
606
" a query parameter\n",
607
zFilename, lnFCall, i+fmtArg, szFName, zFCall);
608
nErr++;
609
610
}else if( (fmtFlags & FMT_SQL)!=0 && !is_sql_safe(zExpr) ){
611
printf("%s:%d: Argument %d to %.*s() not safe for SQL\n",
612
zFilename, lnFCall, i+fmtArg, szFName, zFCall);
613
nErr++;
614
}
615
}
616
}
617
}
618
}
619
if( nErr ){
620
for(i=0; i<nArg; i++){
621
printf(" arg[%d]: %s\n", i, azArg[i]);
622
}
623
}else if( eVerbose>1 ){
624
printf("%s:%d: %.*s() ok for %d arguments\n",
625
zFilename, lnFCall, szFName, zFCall, nArg);
626
}
627
free((char*)azArg);
628
free(zCopy);
629
return nErr;
630
}
631
632
633
/*
634
** Do a design-rule check of format strings for the file named zName
635
** with content zContent. Write errors on standard output. Return
636
** the number of errors.
637
*/
638
static int scan_file(const char *zName, const char *zContent){
639
const char *z;
640
int ln = 0;
641
int szToken;
642
int eToken;
643
const char *zPrev = 0;
644
int ePrev = 0;
645
int szPrev = 0;
646
int lnPrev = 0;
647
int nCurly = 0;
648
int x;
649
unsigned fmtFlags = 0;
650
int nErr = 0;
651
652
if( zContent==0 ){
653
printf("cannot read file: %s\n", zName);
654
return 1;
655
}
656
for(z=zContent; z[0]; z += szToken){
657
szToken = token_length(z, &eToken, &ln);
658
if( eToken==TK_SPACE ) continue;
659
if( eToken==TK_OTHER ){
660
if( z[0]=='{' ){
661
nCurly++;
662
}else if( z[0]=='}' ){
663
nCurly--;
664
}else if( nCurly>0 && z[0]=='(' && ePrev==TK_ID
665
&& (x = isFormatFunc(zPrev,szPrev,&fmtFlags))>0 ){
666
nErr += checkFormatFunc(zName, zPrev, lnPrev, x, fmtFlags);
667
}
668
}
669
zPrev = z;
670
ePrev = eToken;
671
szPrev = szToken;
672
lnPrev = ln;
673
}
674
return nErr;
675
}
676
677
/*
678
** Check for format-string design rule violations on all files listed
679
** on the command-line.
680
**
681
** The eVerbose global variable is incremented with each "-v" argument.
682
*/
683
int main(int argc, char **argv){
684
int i;
685
int nErr = 0;
686
qsort(aFmtFunc, sizeof(aFmtFunc)/sizeof(aFmtFunc[0]),
687
sizeof(aFmtFunc[0]), fmtfunc_cmp);
688
for(i=1; i<argc; i++){
689
char *zFile;
690
if( strcmp(argv[i],"-v")==0 ){
691
eVerbose++;
692
continue;
693
}
694
if( eVerbose>0 ) printf("Processing %s...\n", argv[i]);
695
zFile = read_file(argv[i]);
696
nErr += scan_file(argv[i], zFile);
697
free(zFile);
698
}
699
return nErr;
700
}
701

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button